Parcourir la source

feat(visualization): add fake workflow contract api

SamLee il y a 3 jours
Parent
commit
373ec714b0
70 fichiers modifiés avec 2755 ajouts et 23809 suppressions
  1. 1 2
      visualization/backend/app/__init__.py
  2. 0 570
      visualization/backend/app/business_detail_text.py
  3. 0 3
      visualization/backend/app/card_details/__init__.py
  4. 0 273
      visualization/backend/app/card_details/common.py
  5. 0 396
      visualization/backend/app/card_details/implementation.py
  6. 0 221
      visualization/backend/app/card_details/planning.py
  7. 0 130
      visualization/backend/app/card_details/projector.py
  8. 0 124
      visualization/backend/app/card_details/results.py
  9. 0 288
      visualization/backend/app/card_details/retrieval.py
  10. 0 148
      visualization/backend/app/card_details/schema.py
  11. 0 399
      visualization/backend/app/card_details/summaries.py
  12. 524 0
      visualization/backend/app/contracts.py
  13. 0 337
      visualization/backend/app/creative_event_projection.py
  14. 0 896
      visualization/backend/app/decision_projection.py
  15. 0 156
      visualization/backend/app/evaluation_event_projection.py
  16. 0 593
      visualization/backend/app/evaluation_report_parser.py
  17. 0 1975
      visualization/backend/app/execution_builder.py
  18. 0 66
      visualization/backend/app/execution_view_payload.py
  19. 1785 0
      visualization/backend/app/fake_data.py
  20. 0 1646
      visualization/backend/app/inspector_projection.py
  21. 31 447
      visualization/backend/app/main.py
  22. 0 712
      visualization/backend/app/main_agent_decision_projection.py
  23. 0 274
      visualization/backend/app/main_decision_text.py
  24. 106 0
      visualization/backend/app/models.py
  25. 0 5
      visualization/backend/app/module_audit/__init__.py
  26. 0 55
      visualization/backend/app/module_audit/log_context.py
  27. 0 136
      visualization/backend/app/module_audit/repository.py
  28. 0 121
      visualization/backend/app/module_audit/router.py
  29. 0 1165
      visualization/backend/app/module_audit/service.py
  30. 0 805
      visualization/backend/app/module_audit/static/app.js
  31. 0 50
      visualization/backend/app/module_audit/static/index.html
  32. 0 277
      visualization/backend/app/module_audit/static/styles.css
  33. 0 72
      visualization/backend/app/prompt_projection.py
  34. 0 64
      visualization/backend/app/providers.py
  35. 0 797
      visualization/backend/app/repositories.py
  36. 0 426
      visualization/backend/app/retrieval_detail_projection.py
  37. 0 567
      visualization/backend/app/retrieval_event_projection.py
  38. 0 83
      visualization/backend/app/runtime_bridge.py
  39. 0 362
      visualization/backend/app/runtime_event_index.py
  40. 0 85
      visualization/backend/app/runtime_event_projection.py
  41. 0 69
      visualization/backend/app/runtime_tool_catalog.py
  42. 0 51
      visualization/backend/app/sanitizer.py
  43. 0 5
      visualization/backend/app/source_lineage/__init__.py
  44. 0 1052
      visualization/backend/app/source_lineage/comparison.py
  45. 0 2056
      visualization/backend/app/source_lineage/projector.py
  46. 0 774
      visualization/backend/app/source_lineage/resolver.py
  47. 0 85
      visualization/backend/app/source_lineage/schema.py
  48. 1 2
      visualization/backend/pytest.ini
  49. 5 7
      visualization/backend/requirements.txt
  50. 116 0
      visualization/backend/scripts/validate_host_contracts.py
  51. 0 411
      visualization/backend/scripts/validate_source_inspector.py
  52. 0 259
      visualization/backend/tests/conftest.py
  53. 186 136
      visualization/backend/tests/test_api.py
  54. 0 176
      visualization/backend/tests/test_business_detail_text.py
  55. 0 101
      visualization/backend/tests/test_card_data.py
  56. 0 265
      visualization/backend/tests/test_decision_projection_v8.py
  57. 0 219
      visualization/backend/tests/test_evaluation_report_parser_v8.py
  58. 0 79
      visualization/backend/tests/test_execution_view_payload_v8.py
  59. 0 690
      visualization/backend/tests/test_main_agent_decision_projection.py
  60. 0 88
      visualization/backend/tests/test_main_decision_text.py
  61. 0 261
      visualization/backend/tests/test_module_audit.py
  62. 0 126
      visualization/backend/tests/test_repository_rules.py
  63. 0 159
      visualization/backend/tests/test_retrieval_detail_projection.py
  64. 0 75
      visualization/backend/tests/test_runtime_bridge.py
  65. 0 401
      visualization/backend/tests/test_runtime_event_projection.py
  66. 0 27
      visualization/backend/tests/test_sanitizer.py
  67. 0 134
      visualization/backend/tests/test_script_table_projection.py
  68. 0 687
      visualization/backend/tests/test_source_lineage.py
  69. 0 687
      visualization/backend/tests/test_v8_builder.py
  70. 0 1
      visualization/exports/.gitkeep

+ 1 - 2
visualization/backend/app/__init__.py

@@ -1,2 +1 @@
-"""Read-only script-build visualization backend."""
-
+"""Fake-data-only visualization backend."""

+ 0 - 570
visualization/backend/app/business_detail_text.py

@@ -1,570 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from typing import Any
-
-from .evaluation_report_parser import EvaluationReportParser
-from .runtime_tool_catalog import TOOL_CATALOG
-
-
-_STATUS_LABELS = {
-    "merged": "已采用",
-    "parked": "暂存",
-    "discarded": "未采用",
-    "open": "等待处理",
-    "running": "进行中",
-    "success": "成功",
-    "failure": "失败",
-    "failed": "失败",
-    "ok": "完成",
-    "completed": "完成",
-}
-
-_FIELD_LABELS = {
-    "full_description": "详细执行描述",
-    "content_range": "内容范围",
-    "theme_elements": "主题信息",
-    "form_elements": "形式信息",
-    "function_elements": "作用信息",
-    "feeling_elements": "感受信息",
-    "element_count": "元素数量",
-    "theme": "主题",
-    "form": "形式",
-    "function": "作用",
-    "feeling": "感受",
-}
-
-
-def business_text(value: Any) -> str:
-    """Translate stored business prose without inventing a new conclusion."""
-    text = _string(value)
-    if not text:
-        return ""
-    text = _decode_visible_escapes(text)
-    for field, label in _FIELD_LABELS.items():
-        text = re.sub(
-            rf"(?<![a-z0-9_]){re.escape(field)}(?![a-z0-9_])",
-            label,
-            text,
-            flags=re.I,
-        )
-    text = re.sub(r"(?i)(?<![a-z0-9_])branch_id\s*[=::]\s*(\d+)", r"方案 \1", text)
-    text = re.sub(r"(?i)(?<![a-z0-9_])branch\s*#?\s*(\d+)", r"方案 \1", text)
-    text = re.sub(r"(?i)(?<![a-z0-9_])base(?![a-z0-9_])", "主脚本", text)
-    text = re.sub(
-        r"选\s+方案\s*(\d+)\s*合入(?:\s*主脚本)?\s*[,,、]\s*弃\s+方案\s*(\d+)",
-        r"采用方案 \1 并合入主脚本,方案 \2 未采用",
-        text,
-    )
-    text = re.sub(
-        r"组\s+方案\s*(\d+)\s*与\s*方案\s*(\d+)\s*合入",
-        r"组合方案 \1 与方案 \2,并合入主脚本",
-        text,
-    )
-    text = re.sub(r"(?i)(?<![a-z0-9_])target\s*[=::]", "任务目标:", text)
-    text = re.sub(r"(?i)(?<![a-z0-9_])target(?![a-z0-9_])", "任务目标", text)
-    text = re.sub(r"(?i)(?<![a-z0-9_])scope\s*[=::]", "账号:", text)
-    text = re.sub(r"(?i)(?<![a-z0-9_])null(?![a-z0-9_])", "未填写", text)
-    text = (
-        text.replace("主脚本 脚本表", "主脚本")
-        .replace("账号 账号:", "账号:")
-        .replace("同一 任务目标", "同一个任务")
-    )
-    text = re.sub(r"(?i)patch\s*分支", "候选方案", text)
-    text = re.sub(r"[ \t]+([,。;!?、:)】,.])", r"\1", text)
-    return _tidy(text)
-
-
-def branch_task_text(value: Any) -> str:
-    """Remove implementation headers while preserving the complete business brief."""
-    text = business_text(value)
-    if not text:
-        return ""
-    kept: list[str] = []
-    for line in text.splitlines():
-        stripped = line.strip()
-        if re.match(r"^(?:路序号|action|path_type)\s*[::]", stripped, re.I):
-            continue
-        if re.match(r"^任务目标\s*[::]", stripped):
-            stripped = re.sub(r"^任务目标\s*[::]\s*", "", stripped)
-            if stripped:
-                kept.extend(["### 实现范围", stripped])
-            continue
-        stripped = re.sub(r"^取证方向与约束\s*[::]?", "### 参考依据与约束", stripped)
-        stripped = re.sub(r"^总目标方向(?:([^)]*))?\s*[::]?", "### 整体方向", stripped)
-        stripped = re.sub(r"^本轮当前目标\s*[::]?", "**本轮重点:**", stripped)
-        stripped = re.sub(r"^禁止事项\s*[::]?\s*", "### 需要避免\n", stripped)
-        kept.append(stripped)
-    return _tidy("\n".join(kept))
-
-
-def branch_task_sections(value: Any) -> list[dict[str, str]]:
-    """Split a delegated task into optional display sections without data loss.
-
-    Real runs mostly follow the dispatch contract, but some repair tasks are
-    free-form.  No field is therefore required: recognized labels become
-    sections and every remaining line stays in a generic task note.
-    """
-    text = branch_task_text(value)
-    if not text:
-        return []
-
-    sections: list[dict[str, str]] = []
-    current: dict[str, Any] | None = None
-    recognized = False
-
-    def flush() -> None:
-        nonlocal current
-        if not current:
-            return
-        content = _tidy("\n".join(current.pop("lines", [])))
-        if content:
-            sections.append({**current, "content": content})
-        current = None
-
-    def begin(kind: str, title: str, inline: str = "") -> None:
-        nonlocal current, recognized
-        flush()
-        recognized = True
-        current = {"kind": kind, "title": title, "lines": []}
-        if inline.strip():
-            current["lines"].append(inline.strip())
-
-    for line in text.splitlines():
-        stripped = line.strip()
-        heading = re.match(r"^#{1,6}\s+(.+?)\s*$", stripped)
-        if heading:
-            label = heading.group(1).strip()
-            if label == "实现范围":
-                begin("scope", label)
-                continue
-            if label in {"参考依据与约束", "整体方向"}:
-                begin("constraints", label)
-                continue
-            if label == "需要避免":
-                begin("avoid", label)
-                continue
-
-        labelled = _task_section_label(stripped)
-        if labelled:
-            kind, title, inline = labelled
-            begin(kind, title, inline)
-            continue
-
-        if current is None:
-            current = {"kind": "notes", "title": "任务说明", "lines": []}
-        current["lines"].append(line)
-
-    flush()
-    if not recognized:
-        return [{"kind": "notes", "title": "任务说明", "content": text}]
-    return sections
-
-
-def _task_section_label(value: str) -> tuple[str, str, str] | None:
-    plain = re.sub(r"^\*\*(.+?)\*\*\s*([::]?.*)$", r"\1\2", value).strip()
-    patterns = (
-        ("method", "实施方法", r"(?i)^method\s*[::]\s*(.*)$"),
-        ("goal", "目标", r"^目标\s*[::]\s*(.*)$"),
-        ("materials", None, r"^(必用素材(?:([^\n]*))?)\s*[::]\s*(.*)$"),
-        ("context", None, r"^(前情提要(?:([^\n]*))?)\s*[::]\s*(.*)$"),
-        (
-            "constraints",
-            None,
-            r"^((?:极其重要的|统一基调|严格|重要)?约束(?:([^\n]*))?)\s*[::]\s*(.*)$",
-        ),
-    )
-    for kind, fixed_title, pattern in patterns:
-        match = re.match(pattern, plain)
-        if not match:
-            continue
-        if fixed_title is not None:
-            return kind, fixed_title, match.group(1)
-        return kind, match.group(1), match.group(2)
-    return None
-
-
-def assessment_text(value: Any) -> str:
-    """Keep the implementer's declaration; remove thought/process and cost telemetry."""
-    text = clean_report(value)
-    if not text:
-        return ""
-    marker = re.search(r"(?i)#{1,6}\s*完成度申报", text)
-    if marker:
-        text = text[marker.end() :]
-    else:
-        text = re.sub(
-            r"(?is)^.*?(?=^\s*[-*]\s*\*{0,2}(?:完成|未尽|存疑)\*{0,2}\s*[::])",
-            "",
-            text,
-        )
-    return _tidy(text)
-
-
-def clean_report(value: Any) -> str:
-    """Turn evaluator/agent Markdown into business prose; raw input remains technical."""
-    text = business_text(value)
-    if not text:
-        return ""
-    text = re.sub(r"(?ms)^\s*```[^\n]*\n.*?^\s*```\s*$", "", text)
-    text = re.sub(r"(?ms)^\s*```[^\n]*\n.*\Z", "", text)
-    lines: list[str] = []
-    skip_stats = False
-    for line in text.splitlines():
-        stripped = line.strip()
-        if re.match(r"^#{1,6}\s*(?:委托任务完成|评估报告)\b", stripped):
-            continue
-        if re.match(r"^(?:\*\*)?执行统计(?:\*\*)?\s*[::]?", stripped):
-            skip_stats = True
-            continue
-        if skip_stats and (
-            not stripped
-            or re.match(r"^[-*]\s*(?:消息数|Tokens?|成本)\s*[::]", stripped, re.I)
-        ):
-            continue
-        if skip_stats and stripped:
-            skip_stats = False
-        if re.match(r"^[-*]?\s*(?:消息数|Tokens?|Token\s*usage|成本)\s*[::]", stripped, re.I):
-            continue
-        if "分支元信息" in stripped:
-            continue
-        line = re.sub(
-            r"[((]\s*段落\s*(?:ID|id)\s*[::]\s*[\d,,、\s]+[))]",
-            "",
-            line,
-        )
-        line = re.sub(r"(?i)script_build_id\s*[=:]\s*\d+\s*[·|,,]?", "", line)
-        line = re.sub(r"(?i)\bpost_id\s*[=::]\s*[^\s,,;;))]+", "来源记录", line)
-        line = re.sub(r"(?i)\bround(?:_index)?\s*[=:]\s*(\d+)\b", r"第 \1 轮", line)
-        line = re.sub(r"(?i)\b(?:ID|id)\s*[=::]\s*\d+\b", "", line)
-        line = re.sub(r"[((]\s*[,,·|\s-]*[))]", "", line)
-        line = re.sub(r"(?<=\d)中", " 中", line)
-        line = re.sub(r"`?\[元素:ID\]`?\s*格式", "规范格式", line, flags=re.I)
-        lines.append(line.rstrip())
-    return _tidy("\n".join(lines))
-
-
-def event_business_sections(event: dict[str, Any]) -> list[dict[str, str]]:
-    name = str(event.get("event_name") or "")
-    task = _event_value(event, "input", "task")
-    result = _event_value(event, "output", "summary")
-    if name == "script_multipath_evaluator":
-        report = EvaluationReportParser().parse(result, kind="multipath")
-        scope = "、".join(report["subjects"]) or branch_task_text(task)
-        return _compact_sections(
-            [
-                ("scope", "评审了哪些方案", scope, "summary"),
-                (
-                    "conclusion",
-                    "各方案结论",
-                    _evaluation_item_text(report["itemConclusions"]),
-                    "default",
-                ),
-                ("recommendation", "评审建议", report["recommendation"], "report"),
-            ]
-        )
-
-    if name == "script_evaluator":
-        report = EvaluationReportParser().parse(result, kind="overall")
-        return _compact_sections(
-            [
-                ("conclusion", "主脚本是否达标", report["conclusion"], "summary"),
-                ("scope", "评审范围", branch_task_text(task), "default"),
-                ("achievements", "已达成", "\n".join(report["achievements"]), "report"),
-                (
-                    "problems",
-                    "仍需解决",
-                    _evaluation_problem_text(report["problems"]),
-                    "report",
-                ),
-                ("next", "下一步建议", report["nextGoal"], "report"),
-            ]
-        )
-
-    action = friendly_event_name(event)
-    request = branch_task_text(task)
-    outcome = event_result_text(event)
-    return _compact_sections(
-        [
-            ("action", "做了什么", action, "summary"),
-            ("request", "处理内容", request, "default"),
-            ("result", "得到什么", outcome, "report"),
-        ]
-    )
-
-
-def _evaluation_item_text(items: list[Any]) -> str:
-    lines: list[str] = []
-    for item in items:
-        if not isinstance(item, dict):
-            continue
-        subject = business_text(item.get("subject") or item.get("label"))
-        conclusion = business_text(item.get("conclusion") or item.get("summary"))
-        if subject and conclusion:
-            lines.append(f"{subject}:{conclusion}")
-    return "\n".join(lines)
-
-
-def _evaluation_problem_text(items: list[Any]) -> str:
-    values = []
-    for item in items:
-        if isinstance(item, dict):
-            value = business_text(item.get("summary") or item.get("label"))
-        else:
-            value = business_text(item)
-        if value:
-            values.append(value)
-    return "\n".join(values)
-
-
-def source_markdown(sources: list[Any]) -> str:
-    items: list[str] = []
-    for source in sources:
-        if not isinstance(source, dict):
-            continue
-        source_type = business_text(source.get("data_type") or source.get("type")) or "参考信息"
-        content = business_text(
-            source.get("data_content")
-            or source.get("content")
-            or source.get("summary")
-        )
-        if content:
-            items.append(f"- **{source_type}:** {content}")
-    return "\n".join(items)
-
-
-def decisions_markdown(values: list[Any]) -> str:
-    items = [business_text(value) for value in values]
-    return "\n\n".join(f"{index}. {value}" for index, value in enumerate(items, 1) if value)
-
-
-def branches_markdown(branches: list[dict[str, Any]], *, raw: bool = False) -> str:
-    parts: list[str] = []
-    for index, branch in enumerate(branches, 1):
-        branch_id = branch.get("branch_id") if raw else branch.get("branchId")
-        title = (
-            branch.get("target") or branch.get("impl_task")
-            if raw
-            else branch.get("title") or (branch.get("summary") or {}).get("task")
-        )
-        status = branch.get("status")
-        output = None if raw else (branch.get("summary") or {}).get("output")
-        disposition = (
-            branch.get("decision_reasoning")
-            if raw
-            else (branch.get("outcome") or {}).get("reasoning")
-        )
-        heading = f"### 方案 {branch_id or index}"
-        if status:
-            heading += f" · {status_label(status)}"
-        rows = [heading, business_text(title)]
-        if output:
-            rows.append(f"**候选产出:** {business_text(output)}")
-        if disposition:
-            label = "选择理由" if raw else "最终选择"
-            rows.append(f"**{label}:** {business_text(disposition)}")
-        parts.append("\n\n".join(item for item in rows if item))
-    return "\n\n".join(parts)
-
-
-def batches_markdown(batches: list[dict[str, Any]]) -> str:
-    if not batches:
-        return "未记录主 Agent 多路决策。"
-    parts = []
-    for index, batch in enumerate(batches, 1):
-        branch_ids = "、".join(str(value) for value in batch.get("branchIds") or [])
-        lines = [f"### 决策 {index}"]
-        if branch_ids:
-            lines.append(f"**比较方案:** {branch_ids}")
-        if batch.get("decision"):
-            lines.append(f"**最终决定:** {business_text(batch['decision'])}")
-        if batch.get("reasoning"):
-            lines.append(f"**原因:** {business_text(batch['reasoning'])}")
-        parts.append("\n\n".join(lines))
-    return "\n\n".join(parts)
-
-
-def status_label(value: Any) -> str:
-    raw = str(value or "").strip()
-    return _STATUS_LABELS.get(raw.lower(), business_text(raw) or "状态未记录")
-
-
-def friendly_event_name(event: dict[str, Any]) -> str:
-    name = str(event.get("event_name") or "")
-    title = str(event.get("title") or "")
-    labels = {
-        "script_evaluator": "对本轮主脚本进行整体评审",
-        "script_multipath_evaluator": "比较候选方案并给出独立评审",
-        "record_data_decision": "记录实现方案的数据取舍",
-        "get_branch_data_decisions": "读取方案的数据取舍记录",
-        "get_script_build_context": "读取本次构建的业务背景",
-    }
-    if name in labels:
-        return labels[name]
-    definition = TOOL_CATALOG.get(name)
-    if definition:
-        if definition.category == "decision-input":
-            return f"读取{definition.business_label}"
-        return definition.business_label
-    cleaned = re.sub(r"\s*\([^)]*\)\s*$", "", title).strip()
-    if cleaned and not re.search(r"[a-z]+_[a-z_]", cleaned, re.I):
-        return business_text(cleaned)
-    lowered = name.lower()
-    if any(token in lowered for token in ("search", "find", "query", "retrieve")):
-        return "查找相关参考信息"
-    if lowered.startswith("get_"):
-        return "读取相关业务资料"
-    if lowered.startswith(("record_", "save_")):
-        return "保存本步骤的业务结果"
-    return "完成一次运行活动"
-
-
-def event_result_text(event: dict[str, Any]) -> str:
-    name = str(event.get("event_name") or "")
-    status = str(event.get("status") or "").lower()
-    output = _event_object(event, "output")
-    if status in {"error", "failed", "failure"} or output.get("success") is False:
-        message = business_text(output.get("message") or output.get("error"))
-        return f"执行失败。{message}" if message else "执行失败,具体原因请查看技术详情。"
-
-    if name == "get_script_direction":
-        direction = _event_value(event, "output", "script_direction")
-        return clean_report(direction) or "已读取当前创作方向。"
-    if name == "get_account_script_section_patterns":
-        summary = _event_value(event, "output", "分段规律摘要")
-        return clean_report(summary) or "已读取账号常用的段落结构。"
-    if name == "get_domain_info":
-        count = _list_count(output.get("domain_info"))
-        return f"已读取 {count} 条已核实领域信息。" if count is not None else "已读取已核实的领域信息。"
-    if name == "get_script_snapshot":
-        paragraphs = _list_count(output.get("paragraphs"))
-        elements = _list_count(output.get("elements"))
-        if paragraphs is not None and elements is not None:
-            return f"已读取当前主脚本,共 {paragraphs} 个段落、{elements} 个元素。"
-        return "已读取当前主脚本内容。"
-    if name == "get_branch_data_decisions":
-        count = _list_count(output.get("decisions"))
-        return f"找到 {count} 条数据取舍记录。" if count is not None else "已读取方案的数据取舍记录。"
-
-    summary = output.get("summary") or output.get("message")
-    if isinstance(summary, str):
-        readable = clean_report(summary)
-        if readable:
-            return readable
-    count = _first_count(output)
-    if count is not None:
-        return f"已完成,共返回 {count} 条结果。"
-    return "已完成。具体输入输出保留在技术详情中。"
-
-
-def _event_value(event: dict[str, Any], side: str, key: str | None = None) -> Any:
-    body = event.get(side)
-    content = body.get("content") if isinstance(body, dict) else body
-    value = _nested_value(content, key)
-    if value not in (None, ""):
-        return value
-    if key and isinstance(content, str) and not content.lstrip().startswith(("{", "[")):
-        return content
-    preview = event.get(f"{side}_preview")
-    if key:
-        extracted = _json_string_preview(preview, key)
-        if extracted not in (None, ""):
-            return extracted
-    return _nested_value(preview, key)
-
-
-def _event_object(event: dict[str, Any], side: str) -> dict[str, Any]:
-    body = event.get(side)
-    content = body.get("content") if isinstance(body, dict) else body
-    for value in (content, event.get(f"{side}_preview")):
-        if isinstance(value, dict):
-            return value
-        if isinstance(value, str):
-            try:
-                parsed = json.loads(value)
-            except json.JSONDecodeError:
-                continue
-            if isinstance(parsed, dict):
-                return parsed
-    return {}
-
-
-def _nested_value(value: Any, key: str | None) -> Any:
-    if key is None:
-        return value
-    if isinstance(value, dict):
-        return value.get(key)
-    if isinstance(value, str):
-        try:
-            parsed = json.loads(value)
-        except (TypeError, json.JSONDecodeError):
-            return None
-        return parsed.get(key) if isinstance(parsed, dict) else None
-    return None
-
-
-def _json_string_preview(value: Any, key: str) -> str | None:
-    text = _string(value)
-    if not text:
-        return None
-    match = re.search(rf'"{re.escape(key)}"\s*:\s*"', text)
-    if not match:
-        return None
-    start = match.end()
-    escaped = False
-    end = None
-    for index in range(start, len(text)):
-        char = text[index]
-        if char == '"' and not escaped:
-            end = index
-            break
-        if char == "\\" and not escaped:
-            escaped = True
-        else:
-            escaped = False
-    fragment = text[start:end] if end is not None else text[start:].rstrip("…")
-    try:
-        return json.loads(f'"{fragment}"')
-    except json.JSONDecodeError:
-        return _decode_visible_escapes(fragment)
-
-
-def _compact_sections(
-    values: list[tuple[str, str, Any, str]],
-) -> list[dict[str, str]]:
-    return [
-        {"id": section_id, "title": title, "content": content, "variant": variant}
-        for section_id, title, raw, variant in values
-        if (content := business_text(raw))
-    ]
-
-
-def _list_count(value: Any) -> int | None:
-    return len(value) if isinstance(value, list) else None
-
-
-def _first_count(value: dict[str, Any]) -> int | None:
-    for key in ("count", "result_count", "path_count", "source_count", "total"):
-        count = value.get(key)
-        if isinstance(count, int) and not isinstance(count, bool):
-            return count
-    return None
-
-
-def _decode_visible_escapes(value: str) -> str:
-    return value.replace("\\r\\n", "\n").replace("\\n", "\n").replace('\\"', '"')
-
-
-def _tidy(value: str) -> str:
-    value = re.sub(r"[ \t]+\n", "\n", value)
-    value = re.sub(r"\n{3,}", "\n\n", value)
-    return value.strip()
-
-
-def _unique(values: list[str]) -> list[str]:
-    return list(dict.fromkeys(values))
-
-
-def _string(value: Any) -> str:
-    return value.strip() if isinstance(value, str) else ""

+ 0 - 3
visualization/backend/app/card_details/__init__.py

@@ -1,3 +0,0 @@
-from .projector import CardBusinessDataProjector, CardDataNotFound
-
-__all__ = ["CardBusinessDataProjector", "CardDataNotFound"]

+ 0 - 273
visualization/backend/app/card_details/common.py

@@ -1,273 +0,0 @@
-from __future__ import annotations
-
-import re
-from collections.abc import Callable, Iterable
-from typing import Any
-
-from .schema import RuntimeUnit, field
-
-
-EventLoader = Callable[[int], dict[str, Any]]
-
-
-def integer(value: Any) -> int | None:
-    try:
-        return int(value)
-    except (TypeError, ValueError):
-        return None
-
-
-def suffix_int(ref: str) -> int | None:
-    return integer(str(ref).rsplit(":", 1)[-1])
-
-
-def event_content(side: Any) -> Any:
-    return side.get("content") if isinstance(side, dict) else None
-
-
-def event_completeness(event: dict[str, Any]) -> str:
-    has_input = event_content(event.get("input")) not in (None, "", [], {})
-    has_output = event_content(event.get("output")) not in (None, "", [], {})
-    if has_input and has_output:
-        return "complete"
-    if has_input or has_output or event:
-        return "partial"
-    return "missing"
-
-
-def event_unit(event: dict[str, Any], *, label: str | None = None) -> RuntimeUnit:
-    event_id = integer(event.get("id")) or 0
-    status = str(event.get("status") or "unknown")
-    event_label = label or str(event.get("businessLabel") or event.get("title") or event.get("event_name") or f"运行事件 {event_id}")
-    return {
-        "id": f"runtime:event:{event_id}",
-        "label": event_label,
-        "status": status,
-        "summary": event_summary(event, event_label),
-        "input": event.get("input"),
-        "output": event.get("output"),
-        "durationMs": integer(event.get("duration_ms")),
-        "eventRef": f"event:{event_id}",
-        "completeness": event_completeness(event),  # type: ignore[typeddict-item]
-        "startedAt": event.get("started_at"),
-        "endedAt": event.get("ended_at"),
-    }
-
-
-def event_summary(event: dict[str, Any], fallback: str) -> str:
-    if event.get("error_message"):
-        return f"技术失败:{event['error_message']}"
-    output = event_content(event.get("output"))
-    if isinstance(output, dict):
-        for key in ("summary", "message", "conclusion"):
-            if output.get(key):
-                return str(output[key])
-    if event.get("output_preview"):
-        return str(event["output_preview"])
-    return fallback
-
-
-def load_events(loader: EventLoader, refs: Iterable[Any]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    seen: set[int] = set()
-    for ref in refs:
-        event_id = suffix_int(str(ref)) if isinstance(ref, str) else integer(ref)
-        if event_id is None or event_id in seen:
-            continue
-        seen.add(event_id)
-        try:
-            values.append(loader(event_id))
-        except LookupError:
-            continue
-    return values
-
-
-def event_notices(events: Iterable[dict[str, Any]]) -> list[dict[str, str]]:
-    notices: list[dict[str, str]] = []
-    omitted = 0
-    missing_body = 0
-    for event in events:
-        for side_name in ("input", "output"):
-            side = event.get(side_name)
-            if isinstance(side, dict) and side.get("truncated"):
-                omitted += int(side.get("omittedCharacters") or 0)
-        if event.get("input") is None and event.get("output") is None:
-            missing_body += 1
-    if omitted:
-        notices.append({"level": "warning", "message": f"Event Body 超过读取上限,共省略 {omitted} 个字符。"})
-    if missing_body:
-        notices.append({"level": "warning", "message": f"{missing_body} 个运行事件没有保存可用的 Event Body。"})
-    return notices
-
-
-def find_by_id(items: Iterable[dict[str, Any]], record_id: int | None) -> dict[str, Any] | None:
-    return next((item for item in items if integer(item.get("id")) == record_id), None)
-
-
-def find_round(view: dict[str, Any], round_index: int) -> dict[str, Any] | None:
-    return next((item for item in view.get("rounds") or [] if integer(item.get("roundIndex")) == round_index), None)
-
-
-def find_raw_round(bundle: dict[str, Any], round_index: int) -> dict[str, Any] | None:
-    return next((item for item in bundle.get("rounds") or [] if integer(item.get("round_index")) == round_index), None)
-
-
-def find_branch(view: dict[str, Any], round_index: int, branch_id: int) -> dict[str, Any] | None:
-    round_ = find_round(view, round_index)
-    return next((item for item in (round_ or {}).get("branches") or [] if integer(item.get("branchId")) == branch_id), None)
-
-
-def find_raw_branch(bundle: dict[str, Any], branch_id: int) -> dict[str, Any] | None:
-    return next((item for item in bundle.get("branches") or [] if integer(item.get("branch_id")) == branch_id), None)
-
-
-def find_detail(value: Any, detail_ref: str) -> dict[str, Any] | None:
-    if isinstance(value, dict):
-        if value.get("detailRef") == detail_ref:
-            return value
-        for child in value.values():
-            found = find_detail(child, detail_ref)
-            if found is not None:
-                return found
-    elif isinstance(value, list):
-        for child in value:
-            found = find_detail(child, detail_ref)
-            if found is not None:
-                return found
-    return None
-
-
-def decision_context(node: dict[str, Any] | None) -> dict[str, Any]:
-    if not isinstance(node, dict):
-        return {}
-    decision = node.get("decision")
-    if isinstance(decision, dict):
-        body = decision.get("body")
-        if isinstance(body, dict):
-            return {**decision, **body}
-        return decision
-    return node
-
-
-def context_input_fields(
-    context: dict[str, Any],
-    prefix: str,
-    events: list[dict[str, Any]] | None = None,
-) -> list[dict[str, Any]]:
-    relation_map = {
-        "direct-read": "direct-input",
-        "standing-constraint": "standing-constraint",
-        "persisted-record": "previous-result",
-        "explicit-basis": "explicit-basis",
-    }
-    fields: list[dict[str, Any]] = []
-    events_by_ref = {
-        f"event:{integer(event.get('id'))}": event
-        for event in events or []
-        if integer(event.get("id"))
-    }
-    for index, item in enumerate(context.get("inputs") or [], 1):
-        if not isinstance(item, dict):
-            continue
-        raw_relation = str(item.get("relation") or "")
-        relation = relation_map.get(raw_relation, "available-upstream")
-        detail_ref = str(item.get("detailRef") or "")
-        label = str(item.get("label") or f"上游输入 {index}")
-        if not detail_ref:
-            detail_ref = _implicit_context_ref(prefix, label)
-        event = events_by_ref.get(detail_ref) or {}
-        event_name = str(event.get("event_name") or "")
-        source_kind, source_label, field_path = _context_source(
-            detail_ref, raw_relation, event_name
-        )
-        fields.append(field(
-            f"{prefix}:input:{index}",
-            label,
-            item.get("summary"),
-            source_kind=source_kind,
-            source_label=source_label,
-            source_ref=detail_ref or None,
-            field_path=field_path,
-            relation=relation,
-        ))
-    return fields
-
-
-def _context_source(
-    detail_ref: str,
-    raw_relation: str,
-    event_name: str,
-) -> tuple[str, str, str]:
-    """Classify context sources from their real reference contract."""
-    event_paths = {
-        "save_script_direction": "input.content.script_direction",
-        "begin_round": "input.content.goal",
-        "record_multipath_plan": "input.content",
-    }
-    if event_name or detail_ref.startswith("event:"):
-        return (
-            "runtime-event",
-            "明确读取的运行事件" if raw_relation == "direct-read" else "上游运行事件",
-            event_paths.get(event_name, ""),
-        )
-    database_refs = (
-        (r"^run:objective$", "script_build_record", "script_direction"),
-        (r"^round:\d+:goal$", "script_build_round", "goal"),
-        (r"^multipath-decision:\d+$", "script_build_multipath_decision", "decision"),
-        (r"^data-decision:\d+$", "script_build_data_decision", "decision"),
-        (r"^domain-info:\d+$", "script_build_domain_info", "content"),
-    )
-    for pattern, table, field_path in database_refs:
-        if re.match(pattern, detail_ref):
-            return "database", table, field_path
-    if detail_ref.startswith("artifact:"):
-        return "artifact", "脚本或候选产物", ""
-    return "database", "上游业务结果", ""
-
-
-def _implicit_context_ref(prefix: str, label: str) -> str:
-    """Recover only deterministic upstream refs omitted by historical views."""
-    if prefix == "objective" and label in {"选题", "账号人设"}:
-        return ""
-    if re.match(r"^round:\d+:goal$", prefix) and label == "当前保存的创作目标":
-        return "run:objective"
-    match = re.match(r"^(round:\d+):plan$", prefix)
-    if match and label == "本轮目标":
-        return f"{match.group(1)}:goal"
-    return ""
-
-
-def decision_event_refs(context: dict[str, Any]) -> list[str]:
-    refs: list[str] = []
-    for key in ("revisionRefs", "technicalRefs", "eventRefs"):
-        for ref in context.get(key) or []:
-            if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
-                refs.append(ref)
-    for key in ("currentRevisionRef", "eventRef"):
-        ref = context.get(key)
-        if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
-            refs.append(ref)
-    for item in context.get("inputs") or []:
-        ref = item.get("detailRef") if isinstance(item, dict) else None
-        if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
-            refs.append(ref)
-    for item in context.get("revisions") or []:
-        ref = item.get("detailRef") if isinstance(item, dict) else None
-        if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
-            refs.append(ref)
-    return refs
-
-
-def output_field_from_event(event: dict[str, Any], prefix: str, label: str, relation: str = "persisted-output") -> dict[str, Any]:
-    event_id = integer(event.get("id")) or 0
-    return field(
-        f"{prefix}:event:{event_id}:output",
-        label,
-        event_content(event.get("output")),
-        source_kind="runtime-event",
-        source_label="Event Body 输出",
-        source_ref=f"event:{event_id}",
-        field_path="output.content",
-        relation=relation,
-        completeness=event_completeness(event),
-    )

+ 0 - 396
visualization/backend/app/card_details/implementation.py

@@ -1,396 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from .common import (
-    EventLoader,
-    decision_context,
-    decision_event_refs,
-    event_content,
-    event_notices,
-    event_unit,
-    find_branch,
-    find_by_id,
-    find_detail,
-    find_raw_branch,
-    integer,
-    load_events,
-    suffix_int,
-)
-from .schema import field, payload
-
-
-def project_implementation_task(
-    detail_ref: str,
-    round_index: int,
-    branch_id: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    raw = find_raw_branch(bundle, branch_id) or {}
-    branch = find_branch(view, round_index, branch_id) or {}
-    stage = branch.get("retrievalStage") or {}
-    implementer_id = integer(stage.get("implementerEventId"))
-    events = load_events(load_event, [implementer_id] if implementer_id else [])
-    full_task = _agent_task(events[0]) if events else None
-    task_value = full_task or raw.get("impl_task") or raw.get("target")
-    task_source = "runtime-event" if full_task else "database"
-    task_source_label = "实现 Agent Event Body" if full_task else "script_build_branch(历史回退)"
-    task_source_ref = f"event:{implementer_id}" if full_task and implementer_id else detail_ref
-    inputs = [
-        field(
-            f"branch:{branch_id}:task",
-            "完整实现任务",
-            task_value,
-            source_kind=task_source,
-            source_label=task_source_label,
-            source_ref=task_source_ref,
-            field_path="input.content.task" if full_task else "impl_task",
-            relation="direct-input",
-            completeness="complete" if full_task else "partial" if task_value else "missing",
-        ),
-        field(
-            f"branch:{branch_id}:path-type",
-            "实现范围",
-            raw.get("path_type"),
-            source_kind="database",
-            source_label="script_build_branch",
-            source_ref=detail_ref,
-            field_path="path_type",
-            relation="standing-constraint",
-        ),
-        field(
-            f"branch:{branch_id}:target",
-            "实现目标",
-            raw.get("target"),
-            source_kind="database",
-            source_label="script_build_branch",
-            source_ref=detail_ref,
-            field_path="target",
-            relation="standing-constraint",
-        ),
-    ]
-    notices = event_notices(events)
-    if task_value and not full_task:
-        notices.append({"level": "warning", "message": "未找到完整实现 Agent 输入,当前使用 Branch 字段作历史回退。"})
-    return payload(
-        "implementation-task",
-        "single-event" if events else "business-record",
-        inputs=inputs,
-        units=[event_unit(item, label="实现 Agent") for item in events],
-        outputs=[],
-        runtime_summary="任务作为实现 Agent 的完整输入执行。" if events else "该历史记录没有可用的实现 Agent Event Body。",
-        notices=notices,
-        completeness="complete" if full_task else "partial" if task_value else "missing",
-        business_modules=[
-            # The readable Inspector sections are parsed from the complete
-            # implementer task.  Keep all six source modules on that exact
-            # Event input so the source Inspector can bind every displayed
-            # section to a text span.  Branch.target remains available to the
-            # canvas summary below, but must not masquerade as the section's
-            # original source.
-            {"id": "scope", "title": "实现范围", "sourceIds": [inputs[0]["id"]]},
-            {"id": "method", "title": "实施方法", "sourceIds": [inputs[0]["id"]]},
-            {"id": "goal", "title": "完整目标", "sourceIds": [inputs[0]["id"]]},
-            {"id": "constraints", "title": "参考依据与约束", "sourceIds": [inputs[0]["id"]]},
-            {"id": "avoid", "title": "需要避免", "sourceIds": [inputs[0]["id"]]},
-            {"id": "deliverable", "title": "预期交付", "sourceIds": [inputs[0]["id"]]},
-        ],
-        card_fields=[
-            {"key": "scope", "label": "范围", "sourceIds": [inputs[2]["id"]], "transform": "直接使用结构化 target"},
-            {"key": "method", "label": "方法", "sourceIds": [inputs[0]["id"]], "transform": "使用任务中的实施方法段"},
-            {"key": "target", "label": "目标", "sourceIds": [inputs[2]["id"], inputs[0]["id"]], "transform": "优先结构化 target"},
-        ],
-    )
-
-
-def project_data_decision(
-    detail_ref: str,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    record_id = suffix_int(detail_ref)
-    row = find_by_id(bundle.get("dataDecisions") or [], record_id)
-    if row is None:
-        raise KeyError(detail_ref)
-    node = find_detail(view, detail_ref)
-    context = decision_context(node)
-    events = load_events(load_event, decision_event_refs(context))
-    sources = row.get("sources") if isinstance(row.get("sources"), list) else []
-    inputs = [
-        field(
-            f"data-decision:{record_id}:source:{index}",
-            f"参与判断的数据 {index}",
-            source,
-            source_kind="database",
-            source_label="script_build_data_decision.sources",
-            source_ref=detail_ref,
-            field_path=f"sources[{index - 1}]",
-            relation="explicit-basis",
-        )
-        for index, source in enumerate(sources, 1)
-    ]
-    outputs = [
-        field(f"data-decision:{record_id}:decision", "明确取舍", row.get("decision"), source_kind="database", source_label="script_build_data_decision", source_ref=detail_ref, field_path="decision", relation="persisted-output"),
-        field(f"data-decision:{record_id}:reasoning", "取舍理由", row.get("reasoning"), source_kind="database", source_label="script_build_data_decision", source_ref=detail_ref, field_path="reasoning", relation="persisted-output"),
-    ]
-    return payload(
-        "data-decision",
-        "business-record",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary="这是独立落库的业务取舍记录;不伪造一次不存在的 Agent I/O。",
-        notices=event_notices(events) + ([] if events else [{"level": "info", "message": "未找到能与该取舍记录一对一安全关联的运行事件。"}]),
-        completeness="complete" if row.get("decision") and sources else "partial",
-        business_modules=[
-            {"id": "sources", "title": "参与判断的数据", "sourceIds": [item["id"] for item in inputs]},
-            {"id": "tradeoff", "title": "采用 / 组合 / 舍弃", "sourceIds": [outputs[0]["id"]]},
-            {"id": "decision", "title": "明确取舍", "sourceIds": [outputs[0]["id"]]},
-            {"id": "reason", "title": "理由", "sourceIds": [outputs[1]["id"]]},
-            {"id": "source-completeness", "title": "来源完整性", "sourceIds": [item["id"] for item in inputs]},
-        ],
-        card_fields=[
-            {"key": "conclusion", "label": "取舍结论", "sourceIds": [outputs[0]["id"]], "transform": "直接使用业务记录"},
-            {"key": "sourceCount", "label": "来源数", "sourceIds": [item["id"] for item in inputs], "transform": "明确来源数量"},
-            {"key": "reason", "label": "核心理由", "sourceIds": [outputs[1]["id"]], "transform": "使用结构化理由"},
-        ],
-    )
-
-
-def project_creative(
-    detail_ref: str,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    node = find_detail(view, detail_ref)
-    if node is None:
-        raise KeyError(detail_ref)
-    context = decision_context(node)
-    seed_id = suffix_int(detail_ref)
-    seed = find_by_id(bundle.get("events") or [], seed_id)
-    round_index = integer((seed or {}).get("round_index"))
-    branch_id = integer((seed or {}).get("branch_id"))
-    refs = decision_event_refs(context)
-    if seed_id is not None:
-        refs.append(f"event:{seed_id}")
-    # Explicit round/branch/scope metadata is safe lineage.  It also captures
-    # script mutation calls omitted by the old think_and_plan-only refs.
-    implementer_ids = {
-        integer(item.get("id"))
-        for item in bundle.get("events") or []
-        if item.get("event_name") == "script_implementer"
-        and integer(item.get("round_index")) == round_index
-        and integer(item.get("branch_id")) == branch_id
-    }
-    for item in bundle.get("events") or []:
-        if integer(item.get("round_index")) != round_index or integer(item.get("branch_id")) != branch_id:
-            continue
-        if integer(item.get("scope_event_id")) in implementer_ids or integer(item.get("id")) in implementer_ids:
-            refs.append(f"event:{item.get('id')}")
-    events = load_events(load_event, refs)
-    raw_branch = find_raw_branch(bundle, branch_id or -1) or {}
-    candidate = raw_branch.get("candidate_snapshot") or {}
-    snapshot = candidate.get("snapshot") if isinstance(candidate, dict) else None
-    visible_inputs = []
-    for index, source in enumerate(context.get("evidence") or context.get("inputs") or [], 1):
-        if not isinstance(source, dict):
-            continue
-        observed = str(source.get("observedRelation") or "")
-        relation = "direct-input" if observed == "read-by-actor" else "previous-result" if observed == "returned-to-actor" else "available-upstream"
-        visible_inputs.append(field(f"creative:{seed_id}:input:{index}", str(source.get("label") or f"创作依据 {index}"), source.get("summary") or source.get("value"), source_kind="runtime-event" if source.get("detailRef") else "database", source_label="实现过程中可确认的数据", source_ref=source.get("detailRef"), field_path="output.content" if source.get("detailRef") else "", relation=relation))
-    explicit_basis = [
-        field(
-            f"creative:{seed_id}:explicit-basis:{index}",
-            str(item.get("label") or f"明确采用的数据取舍 {index}"),
-            {"decision": item.get("label"), "reasoning": item.get("value")},
-            source_kind="database",
-            source_label="script_build_data_decision",
-            source_ref=item.get("detailRef"),
-            field_path="",
-            relation="explicit-basis",
-        )
-        for index, item in enumerate(context.get("explicitBasis") or [], 1)
-        if isinstance(item, dict) and str(item.get("detailRef") or "").startswith("data-decision:")
-    ]
-    change_fields = [
-        field(
-            f"creative:{seed_id}:change:{index}",
-            str(item.get("label") or f"脚本改动 {index}"),
-            {"status": item.get("value"), "note": item.get("note")},
-            source_kind="runtime-event",
-            source_label="脚本修改运行事件",
-            source_ref=item.get("detailRef"),
-            field_path="",
-            relation="run-output",
-        )
-        for index, item in enumerate(context.get("runtimeActions") or [], 1)
-        if isinstance(item, dict) and str(item.get("detailRef") or "").startswith("event:")
-    ]
-    outputs = [
-        field(f"creative:{seed_id}:snapshot", "产出的候选表", snapshot, source_kind="artifact", source_label="候选分支快照", source_ref=f"artifact:branch:{branch_id}" if branch_id else None, field_path="candidate_snapshot.snapshot", relation="persisted-output", completeness="complete" if snapshot else "missing"),
-        field(f"creative:{seed_id}:assessment", "实现者自评", raw_branch.get("self_assessment"), source_kind="database", source_label="script_build_branch", source_ref=f"round:{round_index}:branch:{branch_id}:output", field_path="self_assessment", relation="persisted-output"),
-    ]
-    notices = event_notices(events)
-    if visible_inputs:
-        notices.append({"level": "info", "message": "可确认是上游信息,但无法确认是否被本次决策采用。"})
-    if not events:
-        notices.append({"level": "warning", "message": "候选产出存在,但未保存可安全关联的完整创作过程。"})
-    card_kind = "creative" if events else "candidate-output-fallback"
-    return payload(
-        card_kind,
-        "event-sequence" if events else "business-record",
-        inputs=[*visible_inputs, *explicit_basis],
-        units=[event_unit(item) for item in events],
-        outputs=[*outputs, *change_fields],
-        runtime_summary=(f"按明确的轮次、分支和实现 Agent scope 串起 {len(events)} 个创作/修改事件。" if events else "只能展示候选快照和实现者自评。"),
-        notices=notices,
-        completeness="complete" if events and snapshot else "partial" if snapshot or raw_branch.get("self_assessment") else "missing",
-        business_modules=[
-            {"id": "process", "title": "创作处理", "sourceIds": []},
-            {"id": "basis", "title": "明确采用的数据取舍", "sourceIds": [item["id"] for item in explicit_basis]},
-            {"id": "available", "title": "可确认的上游信息", "sourceIds": [item["id"] for item in visible_inputs]},
-            {"id": "changes", "title": "实际脚本改动", "sourceIds": [item["id"] for item in change_fields]},
-            {"id": "candidate", "title": "产出的候选表", "sourceIds": [outputs[0]["id"]]},
-            {"id": "uncertainty", "title": "存疑项 / 完整性", "sourceIds": [outputs[1]["id"]]},
-        ],
-    )
-
-
-def project_multipath_decision(
-    detail_ref: str,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    record_id = suffix_int(detail_ref)
-    row = find_by_id(bundle.get("multipathDecisions") or [], record_id)
-    if row is None:
-        raise KeyError(detail_ref)
-    node = find_detail(view, detail_ref)
-    context = decision_context(node)
-    events = load_events(load_event, decision_event_refs(context))
-    branch_ids = row.get("branch_ids") if isinstance(row.get("branch_ids"), list) else []
-    inputs = [field(f"multipath:{record_id}:branch:{branch_id}", f"候选方案 {branch_id}", find_raw_branch(bundle, integer(branch_id) or -1), source_kind="database", source_label="script_build_branch", source_ref=f"round:{row.get('round_index')}:branch:{branch_id}", field_path="", relation="available-upstream") for branch_id in branch_ids]
-    review_events = [
-        item for item in bundle.get("events") or []
-        if item.get("event_name") == "script_multipath_evaluator"
-        and integer(item.get("round_index")) == integer(row.get("round_index"))
-    ]
-    attached_ids = {integer(item.get("id")) for item in events}
-    unscoped_reviews = [item for item in review_events if integer(item.get("id")) not in attached_ids]
-    for item in unscoped_reviews:
-        inputs.append(field(f"multipath:{record_id}:review:{item.get('id')}", "同轮多方案评审", item.get("agentOutputData") or item.get("output_preview"), source_kind="runtime-event", source_label="同轮评审事件(候选范围未安全关联)", source_ref=f"event:{item.get('id')}", field_path="output", relation="available-upstream", completeness="partial"))
-    outputs = [
-        field(f"multipath:{record_id}:decision", "最终决定", row.get("decision"), source_kind="database", source_label="script_build_multipath_decision", source_ref=detail_ref, field_path="decision", relation="persisted-output"),
-        field(f"multipath:{record_id}:reasoning", "决策理由", row.get("reasoning"), source_kind="database", source_label="script_build_multipath_decision", source_ref=detail_ref, field_path="reasoning", relation="persisted-output"),
-    ]
-    notices = event_notices(events)
-    notices.append({"level": "info", "message": "候选分支可确认是本次取舍对象;除非有明确评审关联,不宣称其内容已被采用。"})
-    if unscoped_reviews:
-        notices.append({"level": "warning", "message": "发现同轮评审,但无法安全关联到具体候选。"})
-    return payload(
-        "multipath-decision",
-        "business-record",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary="主 Agent 多路决策以业务表为最终事实,评审 Event 只在有可靠关联时作为明确依据。",
-        notices=notices,
-        completeness="complete" if row.get("decision") and branch_ids else "partial",
-        business_modules=[
-            {"id": "scope", "title": "取舍对象", "sourceIds": [item["id"] for item in inputs if ":branch:" in item["id"]]},
-            {"id": "comparison", "title": "评审建议与最终决定", "sourceIds": [item["id"] for item in inputs if ":review:" in item["id"]] + [outputs[0]["id"]]},
-            {"id": "outcomes", "title": "逐方案处置", "sourceIds": [item["id"] for item in inputs if ":branch:" in item["id"]]},
-            {"id": "reason", "title": "决策理由", "sourceIds": [outputs[1]["id"]]},
-            {"id": "strength", "title": "关联强度", "sourceIds": [item["id"] for item in inputs]},
-        ],
-    )
-
-
-def project_branch_output(
-    detail_ref: str,
-    round_index: int,
-    branch_id: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-) -> dict[str, Any]:
-    raw = find_raw_branch(bundle, branch_id)
-    branch = find_branch(view, round_index, branch_id)
-    if raw is None or branch is None:
-        raise KeyError(detail_ref)
-    output = branch.get("output") or {}
-    if output.get("type") == "domain-info":
-        facts = [
-            item for item in bundle.get("domainInfo") or []
-            if integer(item.get("branch_id")) == branch_id
-        ]
-        outputs = [field(f"branch:{branch_id}:facts", "已核实领域事实", facts, source_kind="database", source_label="script_build_domain_info", source_ref=detail_ref, field_path="domainInfo", relation="persisted-output")]
-        return payload(
-            "domain-facts",
-            "aggregate",
-            outputs=outputs,
-            runtime_summary="按 branch_id 聚合该领域信息方案写入的事实。",
-            calculation="只按显式 branch_id 关联,不使用文本相似度。",
-            # A successful branch-scoped lookup returning zero rows is a
-            # complete empty result, not a missing source record.
-            completeness="complete",
-            business_modules=[
-                {"id": "facts", "title": "已核实事实", "sourceIds": [outputs[0]["id"]]},
-                {"id": "verification", "title": "核实说明", "sourceIds": [outputs[0]["id"]]},
-                {"id": "sources", "title": "出处", "sourceIds": [outputs[0]["id"]]},
-                {"id": "usage", "title": "用途与存疑状态", "sourceIds": [outputs[0]["id"]]},
-            ],
-        )
-    candidate = raw.get("candidate_snapshot") or {}
-    snapshot = candidate.get("snapshot") if isinstance(candidate, dict) else None
-    outputs = [
-        field(f"branch:{branch_id}:candidate", "形成了什么", snapshot, source_kind="artifact", source_label="候选分支快照", source_ref=f"artifact:branch:{branch_id}", field_path="candidate_snapshot.snapshot", relation="persisted-output", completeness="complete" if snapshot else "missing"),
-        field(f"branch:{branch_id}:assessment", "实现者自评", raw.get("self_assessment"), source_kind="database", source_label="script_build_branch", source_ref=detail_ref, field_path="self_assessment", relation="persisted-output"),
-    ]
-    return payload(
-        "candidate-output-fallback",
-        "business-record",
-        outputs=outputs,
-        runtime_summary="候选产出回退详情只展示候选快照和自评,不伪造未保存的创作过程。",
-        notices=[{"level": "warning", "message": "未保存可安全关联的完整创作过程。"}],
-        completeness="partial" if snapshot or raw.get("self_assessment") else "missing",
-        business_modules=[
-            {"id": "output", "title": "形成了什么", "sourceIds": [outputs[0]["id"]]},
-            {"id": "assessment", "title": "实现者自评", "sourceIds": [outputs[1]["id"]]},
-            {"id": "missing", "title": "创作过程缺失说明", "sourceIds": [item["id"] for item in outputs]},
-        ],
-    )
-
-
-def project_domain_info(detail_ref: str, bundle: dict[str, Any]) -> dict[str, Any]:
-    record_id = suffix_int(detail_ref)
-    row = find_by_id(bundle.get("domainInfo") or [], record_id)
-    if row is None:
-        raise KeyError(detail_ref)
-    outputs = [
-        field(f"domain:{record_id}:fact", "已核实事实", row.get("content"), source_kind="database", source_label="script_build_domain_info", source_ref=detail_ref, field_path="content", relation="persisted-output"),
-        field(f"domain:{record_id}:note", "核实说明", row.get("note"), source_kind="database", source_label="script_build_domain_info", source_ref=detail_ref, field_path="note", relation="persisted-output"),
-        field(f"domain:{record_id}:source", "信息出处", row.get("source"), source_kind="database", source_label="script_build_domain_info", source_ref=detail_ref, field_path="source", relation="persisted-output"),
-    ]
-    return payload(
-        "domain-fact",
-        "business-record",
-        outputs=outputs,
-        runtime_summary="这是独立落库的已核实领域事实。",
-        completeness="complete" if row.get("content") and row.get("source") else "partial",
-        business_modules=[
-            {"id": "fact", "title": "已核实事实", "sourceIds": [outputs[0]["id"]]},
-            {"id": "verification", "title": "核实说明", "sourceIds": [outputs[1]["id"]]},
-            {"id": "source", "title": "出处", "sourceIds": [outputs[2]["id"]]},
-            {"id": "usage", "title": "用途与存疑状态", "sourceIds": [item["id"] for item in outputs]},
-        ],
-    )
-
-
-def _agent_task(event: dict[str, Any]) -> Any:
-    content = event_content(event.get("input"))
-    return content.get("task") if isinstance(content, dict) else content

+ 0 - 221
visualization/backend/app/card_details/planning.py

@@ -1,221 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from .common import (
-    EventLoader,
-    context_input_fields,
-    decision_context,
-    decision_event_refs,
-    event_notices,
-    event_unit,
-    find_raw_round,
-    find_round,
-    load_events,
-)
-from .schema import field, payload
-
-
-def project_objective(
-    bundle: dict[str, Any], view: dict[str, Any], load_event: EventLoader
-) -> dict[str, Any]:
-    record = bundle.get("record") or {}
-    node = view.get("objective") or {}
-    context = decision_context(node)
-    events = load_events(load_event, decision_event_refs(context))
-    inputs = context_input_fields(context, "objective", events)
-    output = field(
-        "objective:current-value",
-        "完整创作方向",
-        record.get("script_direction"),
-        source_kind="database",
-        source_label="script_build_record",
-        source_ref="run:objective",
-        field_path="script_direction",
-        relation="persisted-output",
-    )
-    outputs = [output]
-    if context.get("decisionItems"):
-        outputs.append(field(
-            "objective:goals",
-            "创作目标",
-            context.get("decisionItems"),
-            source_kind="calculation",
-            source_label="创作方向结构化解析",
-            source_ref="run:objective",
-            field_path="decision.decisionItems",
-            relation="persisted-output",
-        ))
-    if context.get("constraints"):
-        outputs.append(field(
-            "objective:dimensions",
-            "评估维度与约束",
-            context.get("constraints"),
-            source_kind="calculation",
-            source_label="创作方向结构化解析",
-            source_ref="run:objective",
-            field_path="decision.constraints",
-            relation="persisted-output",
-        ))
-    notices = event_notices(events)
-    if inputs and any(item["relation"] == "available-upstream" for item in inputs):
-        notices.append({"level": "info", "message": "可确认是上游信息,但无法确认是否被本次决策采用。"})
-    return payload(
-        "objective",
-        "business-record",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary=(f"找到 {len(events)} 个保存/修订事件。" if events else "只能确认当前保存的业务值,未找到完整形成过程。"),
-        notices=notices,
-        completeness=context.get("completeness") if context.get("completeness") in {"complete", "partial", "missing"} else None,
-        business_modules=[
-            {"id": "direction", "title": "完整创作方向原文", "sourceIds": [output["id"]]},
-            {"id": "inputs", "title": "形成前读取", "sourceIds": [item["id"] for item in inputs]},
-            {"id": "structure", "title": "目标与评估维度", "sourceIds": [item["id"] for item in outputs[1:]]},
-            {"id": "revisions", "title": "修订记录", "sourceIds": [output["id"]]},
-        ],
-        card_fields=[
-            {"key": "headline", "label": "核心目标", "sourceIds": [output["id"]], "transform": "使用结构化解析的 headline"},
-            {"key": "targetCount", "label": "目标数", "sourceIds": ["objective:goals"], "transform": "列表计数"},
-            {"key": "dimensionCount", "label": "评估维度数", "sourceIds": ["objective:dimensions"], "transform": "列表计数"},
-        ],
-    )
-
-
-def project_round_goal(
-    detail_ref: str,
-    round_index: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    raw = find_raw_round(bundle, round_index) or {}
-    round_ = find_round(view, round_index) or {}
-    node = round_.get("goal") or {}
-    context = decision_context(node)
-    refs = decision_event_refs(context)
-    refs.extend(
-        f"event:{item.get('id')}"
-        for item in bundle.get("events") or []
-        if item.get("event_name") == "begin_round"
-    )
-    events = load_events(load_event, refs)
-    inputs = context_input_fields(context, f"round:{round_index}:goal", events)
-    begin_round_fields = [
-        field(
-            f"round:{round_index}:goal:begin:{event.get('id')}",
-            "进入本轮时提交的目标",
-            ((event.get("input") or {}).get("content") or {}).get("goal")
-            if isinstance((event.get("input") or {}).get("content"), dict)
-            else (event.get("input") or {}).get("content"),
-            source_kind="runtime-event",
-            source_label="begin_round 运行事件",
-            source_ref=f"event:{event.get('id')}",
-            field_path="input.content.goal",
-            relation="run-output",
-        )
-        for event in events
-        if event.get("event_name") == "begin_round"
-        and (
-            (
-                isinstance((event.get("output") or {}).get("content"), dict)
-                and int(((event.get("output") or {}).get("content") or {}).get("round_index") or 0) == round_index
-            )
-            or (
-                isinstance((event.get("input") or {}).get("content"), dict)
-                and ((event.get("input") or {}).get("content") or {}).get("goal") == raw.get("goal")
-            )
-        )
-    ]
-    outputs = [field(
-        f"round:{round_index}:goal:value",
-        "Goal 原文",
-        raw.get("goal"),
-        source_kind="database",
-        source_label="script_build_round",
-        source_ref=detail_ref,
-        field_path="goal",
-        relation="persisted-output",
-    )]
-    return payload(
-        "round-goal",
-        "business-record",
-        inputs=[*inputs, *begin_round_fields],
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary=(f"通过 {len(events)} 个 begin_round/读取事件形成本轮目标。" if events else "本轮目标有业务记录,但形成过程未完整保存。"),
-        notices=event_notices(events) + ([{"level": "info", "message": "可确认是上游信息,但无法确认是否被本次决策采用。"}] if any(item["relation"] == "available-upstream" for item in inputs) else []),
-        completeness=context.get("completeness") if context.get("completeness") in {"complete", "partial", "missing"} else None,
-        business_modules=[
-            {"id": "goal", "title": "Goal 原文", "sourceIds": [outputs[0]["id"]]},
-            {"id": "why", "title": "进入本轮时提交的目标", "sourceIds": [item["id"] for item in begin_round_fields]},
-            {"id": "dependencies", "title": "本轮形成前可见的上游", "sourceIds": [item["id"] for item in inputs]},
-        ],
-        card_fields=[
-            {"key": "headline", "label": "本轮目标", "sourceIds": [outputs[0]["id"]], "transform": "使用结构化目标首项"},
-            {"key": "basis", "label": "形成前依据", "sourceIds": [item["id"] for item in inputs[:1]], "transform": "取第一个明确上游依据"},
-        ],
-    )
-
-
-def project_implementation_plan(
-    detail_ref: str,
-    round_index: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    raw = find_raw_round(bundle, round_index) or {}
-    round_ = find_round(view, round_index) or {}
-    node = ((round_.get("plan") or {}).get("node") or {})
-    context = decision_context(node)
-    refs = decision_event_refs(context)
-    refs.extend(
-        f"event:{item.get('id')}"
-        for item in bundle.get("events") or []
-        if item.get("event_name") == "record_multipath_plan"
-        and int(item.get("round_index") or 0) == round_index
-    )
-    events = load_events(load_event, refs)
-    inputs = context_input_fields(context, f"round:{round_index}:plan", events)
-    outputs = [
-        field(f"round:{round_index}:plan:mode", "规划方式", raw.get("race_or_divide"), source_kind="database", source_label="script_build_round", source_ref=detail_ref, field_path="race_or_divide", relation="persisted-output"),
-        field(f"round:{round_index}:plan:routes", "实现路线", raw.get("multipath_plan") or [], source_kind="database", source_label="script_build_round", source_ref=detail_ref, field_path="multipath_plan", relation="persisted-output"),
-        field(f"round:{round_index}:plan:reason", "规划理由", raw.get("plan_note"), source_kind="database", source_label="script_build_round", source_ref=detail_ref, field_path="plan_note", relation="persisted-output"),
-    ]
-    revision_fields = [
-        field(
-            f"round:{round_index}:plan:revision:{event.get('id')}",
-            f"规划保存 Event {event.get('id')}",
-            (event.get("input") or {}).get("content"),
-            source_kind="runtime-event",
-            source_label="record_multipath_plan 运行事件",
-            source_ref=f"event:{event.get('id')}",
-            field_path="input.content",
-            relation="run-output",
-        )
-        for event in events
-        if event.get("event_name") == "record_multipath_plan"
-    ]
-    return payload(
-        "implementation-plan",
-        "business-record",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=[*outputs, *revision_fields],
-        runtime_summary=(f"找到 {len(events)} 个规划保存/修订事件。" if events else "展示数据库当前保存的实现规划。"),
-        notices=event_notices(events),
-        completeness=context.get("completeness") if context.get("completeness") in {"complete", "partial", "missing"} else None,
-        business_modules=[
-            {"id": "mode", "title": "规划模式与理由", "sourceIds": [outputs[0]["id"], outputs[2]["id"]]},
-            {"id": "routes", "title": "逐条实现路线", "sourceIds": [outputs[1]["id"]]},
-            {"id": "basis", "title": "规划依据", "sourceIds": [item["id"] for item in inputs]},
-            {"id": "revisions", "title": "规划保存与修订记录", "sourceIds": [item["id"] for item in revision_fields]},
-        ],
-        card_fields=[
-            {"key": "mode", "label": "模式", "sourceIds": [outputs[0]["id"]], "transform": "规范化为单路/赛马/分工"},
-            {"key": "routeCount", "label": "路线数", "sourceIds": [outputs[1]["id"]], "transform": "列表计数"},
-            {"key": "routeScope", "label": "路线作用范围", "sourceIds": [outputs[1]["id"]], "transform": "取每路结构化 target"},
-        ],
-    )

+ 0 - 130
visualization/backend/app/card_details/projector.py

@@ -1,130 +0,0 @@
-from __future__ import annotations
-
-import re
-from collections.abc import Callable
-from typing import Any
-
-from .implementation import (
-    project_branch_output,
-    project_creative,
-    project_data_decision,
-    project_domain_info,
-    project_implementation_task,
-    project_multipath_decision,
-)
-from .planning import (
-    project_implementation_plan,
-    project_objective,
-    project_round_goal,
-)
-from .results import project_final_result, project_round_result
-from .summaries import (
-    project_branch_summary,
-    project_missing_convergence,
-    project_missing_round_evaluation,
-    project_round_summary,
-)
-from .retrieval import (
-    project_direct_group,
-    project_event,
-    project_retrieval_agent,
-    project_retrieval_stage,
-)
-
-
-class CardDataNotFound(LookupError):
-    pass
-
-
-class CardBusinessDataProjector:
-    """Read-only application projector for one card's complete data flow.
-
-    The HTTP adapter supplies plain bundle/view dictionaries and a lazy event
-    loader.  The projector has no database or FastAPI dependency.
-    """
-
-    def project(
-        self,
-        detail_ref: str,
-        *,
-        bundle: dict[str, Any],
-        view: dict[str, Any],
-        load_event: Callable[[int], dict[str, Any]],
-    ) -> dict[str, Any]:
-        ref = str(detail_ref or "").strip()
-        try:
-            if ref == "run:objective":
-                return project_objective(bundle, view, load_event)
-            if ref in {"run:final-result", "artifact:base:current", "base:current"}:
-                return project_final_result("run:final-result", bundle, view)
-            if ref.startswith("event:"):
-                return project_event(ref, load_event)
-            if ref.startswith("retrieval-direct:"):
-                return project_direct_group(ref, view, load_event)
-            if ref.startswith("retrieval-agent:"):
-                return project_retrieval_agent(ref, view, load_event)
-            if ref.startswith("data-decision:"):
-                return project_data_decision(ref, bundle, view, load_event)
-            if ref.startswith("creative:"):
-                return project_creative(ref, bundle, view, load_event)
-            if ref.startswith("multipath-decision:"):
-                return project_multipath_decision(ref, bundle, view, load_event)
-            if ref.startswith("domain-info:"):
-                return project_domain_info(ref, bundle)
-
-            match = re.fullmatch(r"round:(\d+)", ref)
-            if match:
-                return project_round_summary(
-                    ref, int(match.group(1)), bundle, view, load_event
-                )
-
-            match = re.fullmatch(r"round:(\d+):branch:(\d+)", ref)
-            if match:
-                return project_branch_summary(
-                    ref,
-                    int(match.group(1)),
-                    int(match.group(2)),
-                    bundle,
-                    view,
-                    load_event,
-                )
-
-            match = re.fullmatch(r"round:(\d+):(goal|plan|result)", ref)
-            if match:
-                round_index = int(match.group(1))
-                role = match.group(2)
-                if role == "goal":
-                    return project_round_goal(ref, round_index, bundle, view, load_event)
-                if role == "plan":
-                    return project_implementation_plan(ref, round_index, bundle, view, load_event)
-                return project_round_result(ref, round_index, bundle, view, load_event)
-
-            match = re.fullmatch(r"round:(\d+):evaluation", ref)
-            if match:
-                return project_missing_round_evaluation(
-                    ref, int(match.group(1)), bundle, view
-                )
-
-            match = re.fullmatch(
-                r"round:(\d+):[^:]+:(review|decision)-missing", ref
-            )
-            if match:
-                return project_missing_convergence(
-                    ref,
-                    int(match.group(1)),
-                    match.group(2),
-                    view,
-                )
-
-            match = re.fullmatch(r"round:(\d+):branch:(\d+):(task|retrieval|output)", ref)
-            if match:
-                round_index, branch_id = int(match.group(1)), int(match.group(2))
-                role = match.group(3)
-                if role == "task":
-                    return project_implementation_task(ref, round_index, branch_id, bundle, view, load_event)
-                if role == "retrieval":
-                    return project_retrieval_stage(ref, round_index, branch_id, view, load_event)
-                return project_branch_output(ref, round_index, branch_id, bundle, view)
-        except KeyError as exc:
-            raise CardDataNotFound(f"未找到卡片数据 {ref}") from exc
-        raise CardDataNotFound(f"未找到卡片数据 {ref}")

+ 0 - 124
visualization/backend/app/card_details/results.py

@@ -1,124 +0,0 @@
-from __future__ import annotations
-
-from collections import Counter
-from typing import Any
-
-from .common import (
-    EventLoader,
-    decision_context,
-    decision_event_refs,
-    event_notices,
-    event_unit,
-    find_round,
-    load_events,
-)
-from .schema import field, payload
-
-
-def project_round_result(
-    detail_ref: str,
-    round_index: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    round_ = find_round(view, round_index)
-    if round_ is None:
-        raise KeyError(detail_ref)
-    branches = round_.get("branches") or []
-    result = round_.get("result") or {}
-    evaluation = round_.get("overallEvaluation") or {}
-    events = load_events(load_event, decision_event_refs(decision_context(evaluation)))
-    domain_rows = [
-        item for item in bundle.get("domainInfo") or []
-        if int(item.get("round_index") or 0) == round_index
-    ]
-    statuses = Counter(str(item.get("status") or "unknown") for item in branches)
-    inputs = [
-        field(f"round:{round_index}:result:goal", "本轮目标", ((round_.get("goal") or {}).get("card") or {}).get("primary", {}).get("value"), source_kind="database", source_label="script_build_round", source_ref=f"round:{round_index}:goal", field_path="goal", relation="standing-constraint"),
-        field(f"round:{round_index}:result:review", "整体评审", evaluation, source_kind="runtime-event", source_label="整体评审运行事件", source_ref=evaluation.get("detailRef"), relation="explicit-basis", completeness="complete" if evaluation.get("detailRef") else "partial"),
-    ]
-    outputs = [
-        field(f"round:{round_index}:result:summary", "本轮目标完成情况", (result.get("card") or {}).get("primary", {}).get("value"), source_kind="calculation", source_label="本轮业务记录聚合", source_ref=detail_ref, field_path="result.card.primary", relation="persisted-output"),
-        field(f"round:{round_index}:result:dispositions", "方案处置", dict(statuses), source_kind="calculation", source_label="分支状态统计", source_ref=detail_ref, field_path="branches.status", relation="persisted-output"),
-        field(f"round:{round_index}:result:domain", "领域事实", domain_rows, source_kind="calculation", source_label="按 round_index 筛选 script_build_domain_info", source_ref=detail_ref, field_path="", relation="persisted-output", completeness="complete"),
-        field(f"round:{round_index}:result:changes", "主脚本变化", [item.get("output") for item in branches if item.get("output")], source_kind="calculation", source_label="候选处置与当前产物聚合", source_ref=f"artifact:round:{round_index}", field_path="branches[].output", relation="persisted-output"),
-        field(f"round:{round_index}:result:next", "下一步", next((item.get("value") for item in (result.get("card") or {}).get("secondary") or [] if item.get("key") == "next"), None), source_kind="calculation", source_label="本轮状态与下一轮记录", source_ref=detail_ref, field_path="result.card.secondary.next", relation="persisted-output"),
-    ]
-    same_round_reviews = [
-        item for item in bundle.get("events") or []
-        if item.get("event_name") == "script_evaluator"
-        and int(item.get("round_index") or 0) == round_index
-    ]
-    notices = event_notices(events)
-    if not events and same_round_reviews:
-        notices.append({"level": "warning", "message": "发现同轮评审,但无法安全关联到具体候选。"})
-    return payload(
-        "round-result",
-        "calculated",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary="本轮产出由候选分支处置、领域事实、整体评审和下一轮状态计算聚合。",
-        calculation="按 round_index 聚合 Branch、DomainInfo、Evaluation 和下一轮记录;不伪造独立 Agent I/O。",
-        notices=notices,
-        completeness="complete" if outputs[0]["value"] else "partial",
-        business_modules=[
-            {"id": "completion", "title": "本轮目标完成情况", "sourceIds": [inputs[0]["id"], outputs[0]["id"]]},
-            {"id": "dispositions", "title": "方案处置", "sourceIds": [outputs[1]["id"]]},
-            {"id": "domain", "title": "领域事实", "sourceIds": [outputs[2]["id"]]},
-            {"id": "review", "title": "整体评审", "sourceIds": [inputs[1]["id"]]},
-            {"id": "changes", "title": "主脚本变化", "sourceIds": [outputs[3]["id"]]},
-            {"id": "next", "title": "下一步", "sourceIds": [outputs[4]["id"]]},
-        ],
-        card_fields=[
-            {"key": "status", "label": "状态", "sourceIds": [outputs[0]["id"]], "transform": "使用轮次业务状态"},
-            {"key": "accepted", "label": "采用数", "sourceIds": [outputs[1]["id"]], "transform": "merged 计数"},
-            {"key": "next", "label": "下一步", "sourceIds": [outputs[4]["id"]], "transform": "直接使用结构化下一步"},
-        ],
-    )
-
-
-def project_final_result(
-    detail_ref: str, bundle: dict[str, Any], view: dict[str, Any]
-) -> dict[str, Any]:
-    record = bundle.get("record") or {}
-    final = view.get("finalResult") or {}
-    artifact = bundle.get("currentArtifact") or {}
-    paragraphs = artifact.get("paragraphs") or []
-    elements = artifact.get("elements") or []
-    links = artifact.get("paragraphElements") or artifact.get("paragraph_elements") or []
-    branches = bundle.get("branches") or []
-    status_counts = Counter(str(item.get("status") or "unknown") for item in branches)
-    outputs = [
-        field("final:status", "构建结论", record.get("status"), source_kind="database", source_label="script_build_record", source_ref=detail_ref, field_path="status", relation="persisted-output"),
-        field("final:summary", "摘要", record.get("summary") or record.get("error_message"), source_kind="database", source_label="script_build_record", source_ref=detail_ref, field_path="summary" if record.get("summary") else "error_message", relation="persisted-output"),
-        field("final:rounds", "轮次与方案统计", {"roundCount": len(bundle.get("rounds") or []), "branchCounts": dict(status_counts)}, source_kind="calculation", source_label="Round / Branch 记录计数", source_ref=detail_ref, field_path="rounds / branches", relation="persisted-output"),
-        field("final:artifact-scale", "最终脚本规模", {"paragraphs": len(paragraphs), "elements": len(elements), "links": len(links)}, source_kind="artifact", source_label="当前主脚本", source_ref="artifact:base:current", field_path="recordCounts", relation="persisted-output"),
-        field("final:artifact", "主脚本入口", "artifact:base:current", source_kind="artifact", source_label="当前主脚本", source_ref="artifact:base:current", field_path="snapshotRef", relation="persisted-output"),
-        field("final:version", "版本说明", "当前保存的最终主脚本,不是历史轮次快照。", source_kind="calculation", source_label="产物精度规则", source_ref="artifact:base:current", field_path="historicalVersion", relation="persisted-output"),
-    ]
-    return payload(
-        "final-result",
-        "calculated",
-        inputs=[],
-        units=[],
-        outputs=outputs,
-        runtime_summary="最终结果是 Run 记录、全部轮次/分支和当前主脚本的计算聚合。",
-        calculation="直接读取 Run 状态与摘要,对 Round / Branch / Artifact 做确定性计数。",
-        notices=[] if artifact else [{"level": "warning", "message": "未找到当前主脚本产物。"}],
-        completeness="complete" if record and artifact else "partial" if record else "missing",
-        business_modules=[
-            {"id": "conclusion", "title": "构建结论", "sourceIds": [outputs[0]["id"]]},
-            {"id": "summary", "title": "摘要或失败原因", "sourceIds": [outputs[1]["id"]]},
-            {"id": "stats", "title": "轮次与方案统计", "sourceIds": [outputs[2]["id"]]},
-            {"id": "scale", "title": "最终脚本规模", "sourceIds": [outputs[3]["id"]]},
-            {"id": "artifact", "title": "主脚本入口", "sourceIds": [outputs[4]["id"]]},
-            {"id": "version", "title": "版本说明", "sourceIds": [outputs[5]["id"]]},
-        ],
-        card_fields=[
-            {"key": "status", "label": "状态", "sourceIds": [outputs[0]["id"]], "transform": "直接使用 Run 状态"},
-            {"key": "summary", "label": "摘要", "sourceIds": [outputs[1]["id"]], "transform": "优先 summary,失败时使用 error_message"},
-            {"key": "scriptScale", "label": "脚本规模", "sourceIds": [outputs[3]["id"]], "transform": "段落/元素/关联计数"},
-        ],
-    )

+ 0 - 288
visualization/backend/app/card_details/retrieval.py

@@ -1,288 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from .common import (
-    EventLoader,
-    event_completeness,
-    event_content,
-    event_notices,
-    event_unit,
-    find_branch,
-    find_detail,
-    integer,
-    load_events,
-    suffix_int,
-)
-from .schema import field, payload
-
-
-def project_event(detail_ref: str, load_event: EventLoader) -> dict[str, Any]:
-    event_id = suffix_int(detail_ref)
-    if event_id is None:
-        raise KeyError(detail_ref)
-    event = load_event(event_id)
-    event_name = str(event.get("event_name") or "")
-    input_value = event_content(event.get("input"))
-    output_value = event_content(event.get("output"))
-    inputs = [field(
-        f"event:{event_id}:input",
-        "运行输入",
-        input_value,
-        source_kind="runtime-event",
-        source_label="Event Body 输入",
-        source_ref=detail_ref,
-        field_path="input.content",
-        relation="direct-input",
-        completeness="complete" if input_value not in (None, "", [], {}) else "missing",
-    )]
-    outputs = [field(
-        f"event:{event_id}:output",
-        "运行输出",
-        output_value,
-        source_kind="runtime-event",
-        source_label="Event Body 输出",
-        source_ref=detail_ref,
-        field_path="output.content",
-        relation="persisted-output",
-        completeness="complete" if output_value not in (None, "", [], {}) else "missing",
-    )]
-    card_kind = _event_card_kind(event)
-    modules = _event_modules(event)
-    return payload(
-        card_kind,
-        "single-event",
-        inputs=inputs,
-        units=[event_unit(event)],
-        outputs=outputs,
-        runtime_summary=f"{event_name or '运行事件'} 是一次可独立下钻的运行 I/O。",
-        notices=event_notices([event]),
-        completeness=event_completeness(event),  # type: ignore[arg-type]
-        business_modules=modules(inputs[0]["id"], outputs[0]["id"]),
-    )
-
-
-def project_retrieval_stage(
-    detail_ref: str,
-    round_index: int,
-    branch_id: int,
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    branch = find_branch(view, round_index, branch_id)
-    if branch is None:
-        raise KeyError(detail_ref)
-    stage = branch.get("retrievalStage") or {}
-    refs: list[Any] = [stage.get("implementerEventId")]
-    for group in stage.get("directToolGroups") or []:
-        refs.extend(item.get("eventId") for item in group.get("calls") or [])
-    for run in stage.get("agentRuns") or []:
-        refs.append(run.get("eventId"))
-        refs.extend(item.get("eventId") for item in run.get("attempts") or [])
-    events = load_events(load_event, refs)
-    inputs = [field(
-        f"round:{round_index}:branch:{branch_id}:retrieval:purpose",
-        "取数目的",
-        ((branch.get("task") or {}).get("card") or {}).get("primary", {}).get("value"),
-        source_kind="database",
-        source_label="实现任务",
-        source_ref=f"round:{round_index}:branch:{branch_id}:task",
-        field_path="impl_task",
-        relation="standing-constraint",
-    )]
-    outputs = [
-        field(f"round:{round_index}:branch:{branch_id}:retrieval:mode", "执行方式", stage.get("observedMode") or "unknown", source_kind="calculation", source_label="按执行时间和依赖关系确定", source_ref=detail_ref, field_path="observedMode", relation="persisted-output", completeness="complete"),
-        field(f"round:{round_index}:branch:{branch_id}:retrieval:waves", "执行波次", stage.get("waves") or [], source_kind="calculation", source_label="按明确 Event 时间聚合", source_ref=detail_ref, field_path="waves", relation="persisted-output", completeness="complete"),
-        field(f"round:{round_index}:branch:{branch_id}:retrieval:groups", "工具取数", stage.get("directToolGroups") or [], source_kind="calculation", source_label="按明确 Event scope 分组", source_ref=detail_ref, field_path="directToolGroups", relation="persisted-output", completeness="complete"),
-        field(f"round:{round_index}:branch:{branch_id}:retrieval:agents", "Agent 取数", stage.get("agentRuns") or [], source_kind="calculation", source_label="按 parent_event_id 聚合", source_ref=detail_ref, field_path="agentRuns", relation="persisted-output", completeness="complete"),
-        field(f"round:{round_index}:branch:{branch_id}:retrieval:result", "阶段结果与失败", {"status": stage.get("status"), "directToolGroups": stage.get("directToolGroups") or [], "agentRuns": stage.get("agentRuns") or []}, source_kind="calculation", source_label="取数阶段状态确定性聚合", source_ref=detail_ref, field_path="status", relation="persisted-output", completeness="complete"),
-    ]
-    return payload(
-        "retrieval-stage",
-        "aggregate",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary=f"按实现 Agent 的明确 scope 聚合 {len(events)} 个运行事件,观测模式为 {stage.get('observedMode') or 'unknown'}。",
-        notices=event_notices(events),
-        completeness="complete" if events and stage.get("status") not in {"missing", "unknown"} else "partial" if events else "missing",
-        business_modules=[
-            {"id": "purpose", "title": "取数目的", "sourceIds": [inputs[0]["id"]]},
-            {"id": "mode", "title": "执行方式", "sourceIds": [outputs[0]["id"]]},
-            {"id": "waves", "title": "执行波次", "sourceIds": [outputs[1]["id"]]},
-            {"id": "direct", "title": "工具取数", "sourceIds": [outputs[2]["id"]]},
-            {"id": "agents", "title": "Agent 取数", "sourceIds": [outputs[3]["id"]]},
-            {"id": "result", "title": "阶段结果与失败", "sourceIds": [outputs[4]["id"]]},
-        ],
-    )
-
-
-def project_direct_group(
-    detail_ref: str, view: dict[str, Any], load_event: EventLoader
-) -> dict[str, Any]:
-    group = find_detail(view, detail_ref)
-    if group is None:
-        raise KeyError(detail_ref)
-    calls = group.get("calls") or []
-    events = load_events(load_event, [item.get("eventId") for item in calls])
-    inputs = [field(
-        f"{detail_ref}:purpose",
-        "读取目的",
-        [item.get("businessLabel") for item in calls if item.get("businessLabel")],
-        source_kind="calculation",
-        source_label="工具类型业务标签",
-        source_ref=detail_ref,
-        field_path="calls.businessLabel",
-        relation="direct-input",
-    )]
-    outputs = [field(
-        f"{detail_ref}:results",
-        "实际返回的信息",
-        [event_content(item.get("output")) for item in events],
-        source_kind="runtime-event",
-        source_label="逐次工具 Event Body",
-        source_ref=detail_ref,
-        field_path="events[].output.content",
-        relation="persisted-output",
-        completeness="complete" if events and all(event_content(item.get("output")) is not None for item in events) else "partial",
-    )]
-    return payload(
-        "retrieval-direct-group",
-        "event-sequence",
-        inputs=inputs,
-        units=[event_unit(item) for item in events],
-        outputs=outputs,
-        runtime_summary=f"该工具取数组包含 {len(events)} 次直接读取。",
-        notices=event_notices(events),
-        completeness="complete" if events and len(events) == len(calls) else "partial" if events else "missing",
-        business_modules=[
-            {"id": "purpose", "title": "读取目的", "sourceIds": [inputs[0]["id"]]},
-            {"id": "calls", "title": "逐次工具调用", "sourceIds": [outputs[0]["id"]]},
-            {"id": "results", "title": "实际返回的信息", "sourceIds": [outputs[0]["id"]]},
-            {"id": "failures", "title": "失败与缺失", "sourceIds": [outputs[0]["id"]]},
-        ],
-    )
-
-
-def project_retrieval_agent(
-    detail_ref: str, view: dict[str, Any], load_event: EventLoader
-) -> dict[str, Any]:
-    run = find_detail(view, detail_ref)
-    if run is None:
-        raise KeyError(detail_ref)
-    agent_id = integer(run.get("eventId")) or suffix_int(detail_ref)
-    refs = [agent_id, *[item.get("eventId") for item in run.get("attempts") or []]]
-    events = load_events(load_event, refs)
-    agent = next((item for item in events if integer(item.get("id")) == agent_id), {})
-    queries = [item for item in events if integer(item.get("id")) != agent_id]
-    task = _agent_task(agent) or run.get("taskPreview")
-    screening = _agent_screening(agent) or (run.get("screening") or {}).get("preview")
-    inputs = [field(
-        f"{detail_ref}:task",
-        "完整取数任务",
-        task,
-        source_kind="runtime-event",
-        source_label="取数 Agent Event Body",
-        source_ref=f"event:{agent_id}",
-        field_path="input.content.task",
-        relation="direct-input",
-        completeness="complete" if _agent_task(agent) else "partial" if task else "missing",
-    )]
-    outputs = [
-        field(f"{detail_ref}:raw-results", "原始命中", [event_content(item.get("output")) for item in queries], source_kind="runtime-event", source_label="查询 Event Body", source_ref=detail_ref, field_path="queryEvents[].output.content", relation="persisted-output", completeness="complete" if queries and all(event_content(item.get("output")) is not None for item in queries) else "partial" if queries else "missing"),
-        field(f"{detail_ref}:screening", "初筛整理", screening, source_kind="runtime-event", source_label="取数 Agent Event Body", source_ref=f"event:{agent_id}", field_path="output.content", relation="persisted-output", completeness="complete" if _agent_screening(agent) else "partial" if screening else "missing"),
-        field(f"{detail_ref}:query-summary", "查询统计", run.get("querySummary"), source_kind="calculation", source_label="查询 Event 状态聚合", source_ref=detail_ref, field_path="querySummary", relation="persisted-output"),
-    ]
-    return payload(
-        "retrieval-agent",
-        "aggregate",
-        inputs=inputs,
-        units=[event_unit(item, label=(run.get("businessLabel") if integer(item.get("id")) == agent_id else None)) for item in events],
-        outputs=outputs,
-        runtime_summary=f"一个取数 Agent 和 {len(queries)} 次查询组成该运行过程。",
-        notices=event_notices(events),
-        completeness="complete" if _agent_task(agent) and _agent_screening(agent) else "partial" if events else "missing",
-        business_modules=[
-            {"id": "task", "title": "完整取数任务", "sourceIds": [inputs[0]["id"]]},
-            {"id": "queries", "title": "查询过程", "sourceIds": [outputs[2]["id"]]},
-            {"id": "raw", "title": "原始命中", "sourceIds": [outputs[0]["id"]]},
-            {"id": "screening", "title": "初筛整理", "sourceIds": [outputs[1]["id"]]},
-            {"id": "failures", "title": "失败与空结果", "sourceIds": [outputs[2]["id"]]},
-        ],
-        card_fields=[
-            {"key": "target", "label": "取数目标", "sourceIds": [inputs[0]["id"]], "transform": "使用完整任务的结构化目标"},
-            {"key": "queryStats", "label": "查询统计", "sourceIds": [outputs[2]["id"]], "transform": "命中/空/失败数量"},
-            {"key": "screening", "label": "初筛结论", "sourceIds": [outputs[1]["id"]], "transform": "使用 Agent 完整结果中的 summary"},
-        ],
-    )
-
-
-def _agent_task(event: dict[str, Any]) -> Any:
-    content = event_content(event.get("input"))
-    return content.get("task") if isinstance(content, dict) else content
-
-
-def _agent_screening(event: dict[str, Any]) -> Any:
-    content = event_content(event.get("output"))
-    return content.get("summary") if isinstance(content, dict) and content.get("summary") else content
-
-
-def _event_card_kind(event: dict[str, Any]) -> str:
-    name = str(event.get("event_name") or "")
-    if name == "think_and_plan" and str(event.get("agent_role") or "") == "main":
-        return "planning-analysis"
-    if name == "script_multipath_evaluator":
-        return "multipath-review"
-    if name == "script_evaluator":
-        return "overall-review"
-    if name == "script_implementer":
-        return "implementation-task"
-    if str(event.get("event_type") or "") == "tool_call":
-        return "query" if name.startswith(("search_", "query_", "get_")) else "tool-call"
-    return "runtime-event"
-
-
-def _event_modules(event: dict[str, Any]):
-    kind = _event_card_kind(event)
-    def modules(input_id: str, output_id: str) -> list[dict[str, Any]]:
-        if kind == "planning-analysis":
-            return [
-                {"id": "summary", "title": "规划概要", "sourceIds": [input_id]},
-                {"id": "thought", "title": "完整思考", "sourceIds": [input_id]},
-                {"id": "plan", "title": "执行计划", "sourceIds": [input_id]},
-                {"id": "action", "title": "下一步", "sourceIds": [input_id]},
-                {"id": "completeness", "title": "记录完整性", "sourceIds": [input_id, output_id]},
-            ]
-        if kind == "multipath-review":
-            return [
-                {"id": "scope", "title": "评审范围", "sourceIds": [input_id]},
-                {"id": "criteria", "title": "评审标准", "sourceIds": [input_id]},
-                {"id": "per-branch", "title": "逐方案评审", "sourceIds": [output_id]},
-                {"id": "comparison", "title": "方案对比", "sourceIds": [output_id]},
-                {"id": "recommendation", "title": "评审建议", "sourceIds": [output_id]},
-            ]
-        if kind == "overall-review":
-            return [
-                {"id": "subject", "title": "评审对象", "sourceIds": [input_id]},
-                {"id": "criteria", "title": "标准", "sourceIds": [input_id]},
-                {"id": "achieved", "title": "已达成", "sourceIds": [output_id]},
-                {"id": "issues", "title": "问题", "sourceIds": [output_id]},
-                {"id": "conclusion", "title": "总结论", "sourceIds": [output_id]},
-                {"id": "next", "title": "下一轮建议", "sourceIds": [output_id]},
-            ]
-        if kind == "implementation-task":
-            return [
-                {"id": "task", "title": "完整实现任务", "sourceIds": [input_id]},
-                {"id": "result", "title": "实现 Agent 返回", "sourceIds": [output_id]},
-            ]
-        if kind in {"query", "tool-call"}:
-            return [
-                {"id": "purpose", "title": "目的 / 查询条件", "sourceIds": [input_id]},
-                {"id": "result", "title": "返回内容 / 结果列表", "sourceIds": [output_id]},
-                {"id": "status", "title": "状态、耗时与完整性", "sourceIds": [input_id, output_id]},
-            ]
-        return [
-            {"id": "input", "title": "运行输入", "sourceIds": [input_id]},
-            {"id": "output", "title": "运行输出", "sourceIds": [output_id]},
-        ]
-    return modules

+ 0 - 148
visualization/backend/app/card_details/schema.py

@@ -1,148 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Literal, TypedDict
-
-
-RelationKind = Literal[
-    "single-event",
-    "event-sequence",
-    "business-record",
-    "aggregate",
-    "calculated",
-    "missing",
-]
-Completeness = Literal["complete", "partial", "missing"]
-
-
-class CardDataSource(TypedDict, total=False):
-    kind: Literal["database", "runtime-event", "artifact", "calculation", "log-anchor"]
-    label: str
-    ref: str
-    fieldPath: str
-
-
-class CardDataField(TypedDict):
-    id: str
-    label: str
-    value: Any
-    source: CardDataSource
-    relation: Literal[
-        "direct-input",
-        "previous-result",
-        "standing-constraint",
-        "explicit-basis",
-        "available-upstream",
-        "persisted-output",
-        "run-output",
-    ]
-    completeness: Completeness
-
-
-class RuntimeUnit(TypedDict, total=False):
-    id: str
-    label: str
-    status: str
-    summary: str
-    input: Any
-    output: Any
-    durationMs: int | None
-    eventRef: str
-    completeness: Completeness
-    startedAt: str | None
-    endedAt: str | None
-
-
-def field(
-    field_id: str,
-    label: str,
-    value: Any,
-    *,
-    source_kind: str,
-    source_label: str,
-    source_ref: str | None = None,
-    field_path: str | None = None,
-    relation: str,
-    completeness: Completeness | None = None,
-) -> CardDataField:
-    missing = value is None or value == "" or value == [] or value == {}
-    source: CardDataSource = {"kind": source_kind, "label": source_label}  # type: ignore[typeddict-item]
-    if source_ref:
-        source["ref"] = source_ref
-    if field_path:
-        source["fieldPath"] = field_path
-    return {
-        "id": field_id,
-        "label": label,
-        "value": value,
-        "source": source,
-        "relation": relation,  # type: ignore[typeddict-item]
-        "completeness": completeness or ("missing" if missing else "complete"),
-    }
-
-
-def payload(
-    card_kind: str,
-    relation_kind: RelationKind,
-    *,
-    inputs: list[CardDataField] | None = None,
-    units: list[RuntimeUnit] | None = None,
-    outputs: list[CardDataField] | None = None,
-    runtime_summary: str = "",
-    calculation: str | None = None,
-    notices: list[dict[str, str]] | None = None,
-    business_modules: list[dict[str, Any]] | None = None,
-    card_fields: list[dict[str, Any]] | None = None,
-    completeness: Completeness | None = None,
-) -> dict[str, Any]:
-    inputs = inputs or []
-    units = units or []
-    outputs = outputs or []
-    all_fields = [*inputs, *outputs]
-    available = [item for item in all_fields if item["completeness"] != "missing"]
-    if completeness is None:
-        if not available and not units:
-            completeness = "missing"
-        elif any(item["completeness"] != "complete" for item in all_fields) or any(
-            item.get("completeness") != "complete" for item in units
-        ):
-            completeness = "partial"
-        else:
-            completeness = "complete"
-    if business_modules is None:
-        business_modules = [
-            {"id": "business-inputs", "title": "业务输入", "sourceIds": [item["id"] for item in inputs]},
-            {"id": "runtime", "title": "运行过程 / I/O", "sourceIds": [item["id"] for item in all_fields]},
-            {"id": "business-outputs", "title": "业务输出", "sourceIds": [item["id"] for item in outputs]},
-        ]
-    if card_fields is None:
-        headline = next((item for item in outputs if item["completeness"] != "missing"), None)
-        if headline is None:
-            headline = next((item for item in inputs if item["completeness"] != "missing"), None)
-        card_fields = [] if headline is None else [{
-            "key": "headline",
-            "label": headline["label"],
-            "sourceIds": [headline["id"]],
-            "transform": "使用结构化业务字段;卡片仅做视觉截行",
-        }]
-    used = {
-        source_id
-        for item in [*business_modules, *card_fields]
-        for source_id in item.get("sourceIds", [])
-    }
-    runtime: dict[str, Any] = {"summary": runtime_summary, "units": units}
-    if calculation:
-        runtime["calculation"] = calculation
-    return {
-        "cardKind": card_kind,
-        "relationKind": relation_kind,
-        "completeness": completeness,
-        "businessInputs": inputs,
-        "runtime": runtime,
-        "businessOutputs": outputs,
-        "displayUse": {
-            "businessModules": business_modules,
-            "cardFields": card_fields,
-            "unusedSourceIds": [item["id"] for item in all_fields if item["id"] not in used],
-        },
-        "notices": notices or [],
-    }

+ 0 - 399
visualization/backend/app/card_details/summaries.py

@@ -1,399 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from .common import (
-    EventLoader,
-    event_notices,
-    event_unit,
-    find_branch,
-    find_raw_branch,
-    find_raw_round,
-    find_round,
-    integer,
-    load_events,
-)
-from .schema import field, payload
-
-
-def project_round_summary(
-    detail_ref: str,
-    round_index: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    round_ = find_round(view, round_index)
-    raw_round = find_raw_round(bundle, round_index)
-    if round_ is None or raw_round is None:
-        raise KeyError(detail_ref)
-
-    raw_branches = [
-        row
-        for row in bundle.get("branches") or []
-        if integer(row.get("round_index")) == round_index
-    ]
-    decisions = [
-        row
-        for row in bundle.get("multipathDecisions") or []
-        if integer(row.get("round_index")) == round_index
-    ]
-    evaluation = round_.get("overallEvaluation") or {}
-    evaluation_ref = str(evaluation.get("detailRef") or "")
-    evaluation_events = load_events(
-        load_event,
-        [evaluation_ref] if evaluation_ref.startswith("event:") else [],
-    )
-
-    goal = field(
-        f"round:{round_index}:summary:goal",
-        "本轮目标",
-        raw_round.get("goal"),
-        source_kind="database",
-        source_label="script_build_round",
-        source_ref=detail_ref,
-        field_path="goal",
-        relation="standing-constraint",
-    )
-    plan = field(
-        f"round:{round_index}:summary:plan",
-        "当前保存的规划",
-        {
-            "mode": raw_round.get("race_or_divide"),
-            "paths": raw_round.get("multipath_plan"),
-            "note": raw_round.get("plan_note"),
-        },
-        source_kind="database",
-        source_label="script_build_round",
-        source_ref=detail_ref,
-        field_path="multipath_plan",
-        relation="persisted-output",
-    )
-    branch_fields = [
-        field(
-            f"round:{round_index}:summary:branch:{row.get('branch_id')}",
-            f"候选方案 {row.get('branch_id')}",
-            row,
-            source_kind="database",
-            source_label="script_build_branch",
-            source_ref=f"round:{round_index}:branch:{row.get('branch_id')}",
-            relation="persisted-output",
-        )
-        for row in raw_branches
-    ]
-    decision_fields = [
-        field(
-            f"round:{round_index}:summary:decision:{row.get('id')}",
-            f"主 Agent 决策 {row.get('id')}",
-            row,
-            source_kind="database",
-            source_label="script_build_multipath_decision",
-            source_ref=f"multipath-decision:{row.get('id')}",
-            relation="persisted-output",
-        )
-        for row in decisions
-    ]
-    convergence = field(
-        f"round:{round_index}:summary:convergence",
-        "候选评审与主 Agent 决策",
-        round_.get("convergenceBatches") or [],
-        source_kind="calculation",
-        source_label="按当前轮次的候选范围与明确关联记录汇总",
-        source_ref=detail_ref,
-        field_path="convergenceBatches",
-        relation="persisted-output",
-        completeness="complete",
-    )
-    evaluation_field = field(
-        f"round:{round_index}:summary:evaluation",
-        "主脚本整体评审",
-        evaluation_events[0] if evaluation_events else None,
-        source_kind="runtime-event",
-        source_label="主脚本整体评审运行事件",
-        source_ref=evaluation_ref if evaluation_ref.startswith("event:") else None,
-        relation="persisted-output",
-        completeness="complete" if evaluation_events else "missing",
-    )
-    result = field(
-        f"round:{round_index}:summary:result",
-        "本轮产出",
-        round_.get("result"),
-        source_kind="calculation",
-        source_label="本轮业务记录确定性聚合",
-        source_ref=detail_ref,
-        field_path="result",
-        relation="persisted-output",
-        completeness="complete" if round_.get("result") else "missing",
-    )
-    outputs = [
-        plan,
-        *branch_fields,
-        *decision_fields,
-        convergence,
-        evaluation_field,
-        result,
-    ]
-    notices = event_notices(evaluation_events)
-    if not evaluation_events:
-        notices.append(
-            {
-                "level": "warning",
-                "message": "本轮没有执行或保存主脚本整体评审运行事件。",
-            }
-        )
-    return payload(
-        "round-summary",
-        "aggregate",
-        inputs=[goal],
-        units=[event_unit(item) for item in evaluation_events],
-        outputs=outputs,
-        runtime_summary="按 round_index 聚合本轮目标、规划、候选分支、决策、整体评审和本轮产出。",
-        calculation="只聚合当前轮次的结构化业务记录;缺失的整体评审保持缺失。",
-        notices=notices,
-        completeness="partial" if not evaluation_events else "complete",
-        business_modules=[
-            {"id": "goal", "title": "本轮目标", "sourceIds": [goal["id"]]},
-            {"id": "plan", "title": "当前保存的规划", "sourceIds": [plan["id"]]},
-            {"id": "branches", "title": "候选方案", "sourceIds": [item["id"] for item in branch_fields]},
-            {
-                "id": "decisions",
-                "title": "候选评审与主 Agent 决策",
-                "sourceIds": [
-                    *[item["id"] for item in decision_fields],
-                    convergence["id"],
-                ],
-            },
-            {"id": "evaluation", "title": "主脚本整体评审", "sourceIds": [evaluation_field["id"]]},
-            {"id": "result", "title": "本轮产出", "sourceIds": [result["id"]]},
-        ],
-    )
-
-
-def project_branch_summary(
-    detail_ref: str,
-    round_index: int,
-    branch_id: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-    load_event: EventLoader,
-) -> dict[str, Any]:
-    branch = find_branch(view, round_index, branch_id)
-    raw_branch = find_raw_branch(bundle, branch_id)
-    if branch is None or raw_branch is None:
-        raise KeyError(detail_ref)
-
-    decisions = [
-        row
-        for row in bundle.get("dataDecisions") or []
-        if integer(row.get("branch_id")) == branch_id
-        and integer(row.get("round_index")) in {None, round_index}
-    ]
-    stage = branch.get("retrievalStage") or {}
-    retrieval_refs: list[Any] = []
-    for group in stage.get("directToolGroups") or []:
-        retrieval_refs.extend(
-            item.get("eventId") for item in group.get("calls") or []
-        )
-    for agent_run in stage.get("agentRuns") or []:
-        retrieval_refs.append(agent_run.get("eventId"))
-        retrieval_refs.extend(
-            item.get("eventId") for item in agent_run.get("attempts") or []
-        )
-    retrieval_events = load_events(load_event, retrieval_refs)
-    task = field(
-        f"branch:{branch_id}:summary:task",
-        "实现任务",
-        raw_branch.get("impl_task"),
-        source_kind="database",
-        source_label="script_build_branch",
-        source_ref=detail_ref,
-        field_path="impl_task",
-        relation="direct-input",
-    )
-    decision_fields = [
-        field(
-            f"branch:{branch_id}:summary:decision:{row.get('id')}",
-            f"数据取舍 {row.get('id')}",
-            row,
-            source_kind="database",
-            source_label="script_build_data_decision",
-            source_ref=f"data-decision:{row.get('id')}",
-            relation="persisted-output",
-        )
-        for row in decisions
-    ]
-    retrieval_fields = [
-        field(
-            f"branch:{branch_id}:summary:retrieval:{event.get('id')}",
-            str(event.get("event_name") or f"取数 Event {event.get('id')}"),
-            event,
-            source_kind="runtime-event",
-            source_label=str(event.get("event_name") or "取数运行事件"),
-            source_ref=f"event:{event.get('id')}",
-            field_path="",
-            relation="available-upstream",
-        )
-        for event in retrieval_events
-    ]
-    output = field(
-        f"branch:{branch_id}:summary:output",
-        "候选产出",
-        raw_branch,
-        source_kind="database",
-        source_label="script_build_branch",
-        source_ref=detail_ref,
-        relation="persisted-output",
-    )
-    return payload(
-        "branch-summary",
-        "aggregate",
-        inputs=[task],
-        units=[event_unit(item) for item in retrieval_events],
-        outputs=[*retrieval_fields, *decision_fields, output],
-        runtime_summary="按 branch_id 聚合实现任务、取数、数据取舍和候选产出。",
-        calculation="只使用当前方案的 Branch、明确 scope 取数 Event 与 DataDecision 记录。",
-        completeness="complete",
-        business_modules=[
-            {"id": "task", "title": "实现任务", "sourceIds": [task["id"]]},
-            {
-                "id": "data",
-                "title": "取数与数据取舍",
-                "sourceIds": [
-                    *[item["id"] for item in retrieval_fields],
-                    *[item["id"] for item in decision_fields],
-                ],
-            },
-            {"id": "output", "title": "候选产出", "sourceIds": [output["id"]]},
-        ],
-    )
-
-
-def project_missing_round_evaluation(
-    detail_ref: str,
-    round_index: int,
-    bundle: dict[str, Any],
-    view: dict[str, Any],
-) -> dict[str, Any]:
-    round_ = find_round(view, round_index)
-    raw_round = find_raw_round(bundle, round_index)
-    if round_ is None or raw_round is None:
-        raise KeyError(detail_ref)
-    evaluation = round_.get("overallEvaluation") or {}
-    if str(evaluation.get("detailRef") or "").startswith("event:"):
-        raise KeyError(detail_ref)
-    missing = field(
-        f"round:{round_index}:evaluation:missing",
-        "记录状态",
-        None,
-        source_kind="runtime-event",
-        source_label="script_evaluator Event",
-        source_ref=None,
-        relation="persisted-output",
-        completeness="missing",
-    )
-    return payload(
-        "evaluation-missing",
-        "missing",
-        outputs=[missing],
-        runtime_summary="本轮没有执行或保存主脚本整体评审运行事件。",
-        notices=[
-            {
-                "level": "warning",
-                "message": "本轮没有执行或保存主脚本整体评审运行事件。",
-            }
-        ],
-        completeness="missing",
-        business_modules=[
-            {"id": "primary", "title": "记录状态", "sourceIds": [missing["id"]]}
-        ],
-    )
-
-
-def project_missing_convergence(
-    detail_ref: str,
-    round_index: int,
-    record_type: str,
-    view: dict[str, Any],
-) -> dict[str, Any]:
-    round_ = find_round(view, round_index)
-    if round_ is None:
-        raise KeyError(detail_ref)
-    found_node: dict[str, Any] | None = None
-    found_batch: dict[str, Any] | None = None
-    key = "missingReview" if record_type == "review" else "missingDecision"
-    for batch in round_.get("convergenceBatches") or []:
-        node = batch.get(key)
-        if isinstance(node, dict) and detail_ref in {
-            node.get("id"),
-            node.get("detailRef"),
-        }:
-            found_node, found_batch = node, batch
-            break
-    if found_node is None or found_batch is None:
-        raise KeyError(detail_ref)
-
-    branch_ids = found_batch.get("branchIds") or []
-    unscoped_refs = found_node.get("unscopedReviewRefs") or []
-    status_value = ((found_node.get("card") or {}).get("primary") or {}).get("value")
-    if unscoped_refs:
-        source_kind = "calculation"
-        source_label = "同轮评审未安全关联到具体候选"
-        source_ref = detail_ref
-        value: Any = {
-            "roundIndex": round_index,
-            "branchIds": branch_ids,
-            "unscopedReviewRefs": unscoped_refs,
-        }
-        completeness = "partial"
-    else:
-        source_kind = "database"
-        source_label = (
-            "script_multipath_evaluator Event 查询结果"
-            if record_type == "review"
-            else "script_build_multipath_decision 查询结果"
-        )
-        source_ref = (
-            f"missing-convergence:{record_type}:round:{round_index}:"
-            + ",".join(str(item) for item in branch_ids)
-        )
-        value = []
-        completeness = "complete"
-    status = field(
-        f"{detail_ref}:status",
-        "记录状态",
-        status_value,
-        source_kind=source_kind,
-        source_label=source_label,
-        source_ref=source_ref,
-        relation="persisted-output",
-        completeness=completeness,
-    )
-    scope = field(
-        f"{detail_ref}:scope",
-        "实际查询范围",
-        {
-            "roundIndex": round_index,
-            "branchIds": branch_ids,
-            "resultCount": len(unscoped_refs),
-        },
-        source_kind="calculation",
-        source_label="可视化按当前轮次和候选范围确定",
-        source_ref=detail_ref,
-        field_path="value",
-        relation="persisted-output",
-    )
-    return payload(
-        f"multipath-{record_type}-missing",
-        "aggregate",
-        outputs=[status, scope],
-        runtime_summary=str(status_value or "未找到对应记录"),
-        notices=(
-            [{"level": "warning", "message": "存在同轮记录,但无法安全关联到具体候选。"}]
-            if unscoped_refs
-            else [{"level": "info", "message": "按当前轮次与候选范围查询完成,结果为空。"}]
-        ),
-        completeness=completeness,
-        business_modules=[
-            {"id": "status", "title": "记录状态", "sourceIds": [status["id"], scope["id"]]}
-        ],
-    )

+ 524 - 0
visualization/backend/app/contracts.py

@@ -0,0 +1,524 @@
+"""Exact field names mirrored from the current Host domain and Agent wire models.
+
+This module intentionally has no dependency on the parent source tree.  The
+visualization stays independently runnable while its tests reject incomplete or
+invented fake records.
+"""
+
+from __future__ import annotations
+
+SCHEMA_CATALOG: dict[str, tuple[str, ...]] = {
+    "ScriptBuildRecord": (
+        "id",
+        "execution_id",
+        "topic_build_id",
+        "topic_id",
+        "agent_type",
+        "agent_config",
+        "data_source_url",
+        "status",
+        "reson_trace_id",
+        "is_deleted",
+        "is_favorited",
+        "summary",
+        "error_message",
+        "script_direction",
+        "build_workflow",
+        "paragraph_count",
+        "element_count",
+        "input_tokens",
+        "output_tokens",
+        "cost_usd",
+        "strategies_config",
+        "start_time",
+        "end_time",
+    ),
+    "ScriptBuildInputSnapshotV1": (
+        "snapshot_id",
+        "script_build_id",
+        "execution_id",
+        "topic_build_id",
+        "topic_id",
+        "topic",
+        "account",
+        "persona_points",
+        "section_patterns",
+        "strategies",
+        "prompt_manifest",
+        "datasource_manifest",
+        "model_manifest",
+        "canonical_sha256",
+        "created_at",
+        "parent_snapshot_id",
+        "parent_snapshot_sha256",
+        "business_input_sha256",
+    ),
+    "InputSnapshotRow": (
+        "id",
+        "script_build_id",
+        "version",
+        "canonical_json",
+        "canonical_sha256",
+        "prompt_manifest_json",
+        "schema_version",
+        "created_at",
+    ),
+    "MissionBinding": (
+        "binding_id",
+        "script_build_id",
+        "root_trace_id",
+        "input_snapshot_id",
+        "active_direction_artifact_version_id",
+        "accepted_root_artifact_version_id",
+        "engine_version",
+        "schema_version",
+        "created_at",
+        "updated_at",
+        "owner_epoch",
+        "stop_epoch",
+        "owner_instance_id",
+        "owner_acquired_at",
+    ),
+    "MissionOwnerToken": (
+        "script_build_id",
+        "root_trace_id",
+        "owner_epoch",
+        "stop_epoch",
+        "owner_instance_id",
+    ),
+    "TaskView": (
+        "task_id",
+        "goal_id",
+        "parent_task_id",
+        "display_path",
+        "specs",
+        "current_spec_version",
+        "status",
+        "child_task_ids",
+        "attempt_ids",
+        "validation_ids",
+        "decision_ids",
+        "repair_count_by_version",
+        "blocked_reason",
+        "superseded_by",
+        "created_at",
+        "updated_at",
+    ),
+    "TaskSpecView": (
+        "version",
+        "objective",
+        "acceptance_criteria",
+        "context_refs",
+        "created_at",
+    ),
+    "ScriptTaskContractV1": (
+        "schema_version",
+        "task_kind",
+        "scope_ref",
+        "intent_class",
+        "objective",
+        "input_decision_refs",
+        "base_artifact_ref",
+        "write_scope",
+        "gap_ref",
+        "output_schema",
+        "criteria",
+        "budget",
+        "supersedes_decision_ids",
+        "candidate_closure_decision_refs",
+        "adopted_decision_ids",
+        "held_or_rejected_decision_ids",
+        "compose_order",
+        "comparison_decision_refs",
+    ),
+    "OperationView": (
+        "operation_id",
+        "root_trace_id",
+        "kind",
+        "status",
+        "task_ids",
+        "attempt_ids",
+        "validation_ids",
+        "result_ref",
+        "execution_epoch",
+        "deadline_at",
+        "error",
+        "started_at",
+        "completed_at",
+        "created_at",
+        "updated_at",
+    ),
+    "AttemptView": (
+        "attempt_id",
+        "task_id",
+        "spec_version",
+        "worker_trace_id",
+        "worker_preset",
+        "execution_mode",
+        "accepted_child_decision_ids",
+        "status",
+        "operation_id",
+        "execution_epoch",
+        "continue_from_trace_id",
+        "snapshot_id",
+        "submission",
+        "execution_stats",
+        "error",
+        "started_at",
+        "completed_at",
+        "duration_ms",
+        "created_at",
+        "updated_at",
+    ),
+    "ValidationView": (
+        "validation_id",
+        "task_id",
+        "attempt_id",
+        "spec_version",
+        "snapshot_id",
+        "validator_trace_id",
+        "validator_preset",
+        "validation_plan",
+        "evidence_queries_used",
+        "status",
+        "operation_id",
+        "execution_epoch",
+        "verdict",
+        "criterion_results",
+        "summary",
+        "evidence_refs",
+        "unverified_claims",
+        "risks",
+        "recommendation",
+        "execution_stats",
+        "error",
+        "started_at",
+        "completed_at",
+        "duration_ms",
+        "created_at",
+        "updated_at",
+    ),
+    "PlannerDecisionView": (
+        "decision_id",
+        "task_id",
+        "action",
+        "reason",
+        "from_status",
+        "to_status",
+        "attempt_id",
+        "validation_id",
+        "payload",
+        "created_at",
+    ),
+    "EvidenceRecordV1": (
+        "schema_version",
+        "evidence_id",
+        "source_type",
+        "tool_name",
+        "query",
+        "source_refs",
+        "raw_artifact_ref",
+        "summary",
+        "supports",
+        "confidence",
+        "limitations",
+        "created_at",
+    ),
+    "ScriptDirectionArtifactV1": (
+        "schema_version",
+        "goals",
+        "criteria",
+        "domain_criteria",
+        "topic_refs",
+        "persona_refs",
+        "strategy_refs",
+        "evidence_refs",
+        "legacy_markdown",
+    ),
+    "CandidateLineageV1": (
+        "scope_ref",
+        "input_snapshot_ref",
+        "input_closure_digest",
+        "base_artifact_ref",
+        "base_artifact_digest",
+        "base_revision",
+        "write_scope",
+        "supersedes_decision_ids",
+        "source_lineage",
+    ),
+    "StructureArtifactV1": (
+        "schema_version",
+        "scope_ref",
+        "input_snapshot_ref",
+        "input_closure_digest",
+        "base_artifact_ref",
+        "base_artifact_digest",
+        "base_revision",
+        "write_scope",
+        "supersedes_decision_ids",
+        "source_lineage",
+        "paragraphs",
+    ),
+    "ParagraphArtifactV1": (
+        "schema_version",
+        "scope_ref",
+        "input_snapshot_ref",
+        "input_closure_digest",
+        "base_artifact_ref",
+        "base_artifact_digest",
+        "base_revision",
+        "write_scope",
+        "supersedes_decision_ids",
+        "source_lineage",
+        "paragraphs",
+        "elements",
+        "paragraph_element_links",
+        "change_manifest",
+    ),
+    "ElementSetArtifactV1": (
+        "schema_version",
+        "scope_ref",
+        "input_snapshot_ref",
+        "input_closure_digest",
+        "base_artifact_ref",
+        "base_artifact_digest",
+        "base_revision",
+        "write_scope",
+        "supersedes_decision_ids",
+        "source_lineage",
+        "paragraphs",
+        "elements",
+        "paragraph_element_links",
+        "change_manifest",
+    ),
+    "ComparisonArtifactV1": (
+        "schema_version",
+        "scope_ref",
+        "input_snapshot_ref",
+        "input_closure_digest",
+        "base_artifact_ref",
+        "base_artifact_digest",
+        "base_revision",
+        "write_scope",
+        "supersedes_decision_ids",
+        "source_lineage",
+        "candidate_artifact_refs",
+        "criterion_results",
+        "conflicts",
+        "recommendation",
+    ),
+    "StructuredScriptArtifactV1": (
+        "schema_version",
+        "direction_ref",
+        "input_closure_digest",
+        "paragraphs",
+        "elements",
+        "paragraph_element_links",
+        "source_artifact_refs",
+        "evidence_refs",
+        "acceptance_notes",
+    ),
+    "CandidatePortfolioArtifactV1": (
+        "schema_version",
+        "adopted_structured_script_ref",
+        "candidate_structured_script_refs",
+        "accepted_decision_ids",
+        "superseded_decision_ids",
+        "rejected_or_held_decision_ids",
+        "input_closure_digest",
+        "unresolved_defects",
+        "compose_order",
+    ),
+    "ArtifactVersion": (
+        "artifact_version_id",
+        "script_build_id",
+        "task_id",
+        "attempt_id",
+        "spec_version",
+        "artifact_type",
+        "canonical_sha256",
+        "state",
+        "artifact",
+        "created_at",
+        "frozen_at",
+    ),
+    "ArtifactVersionRow": (
+        "id",
+        "script_build_id",
+        "task_id",
+        "attempt_id",
+        "spec_version",
+        "artifact_type",
+        "legacy_branch_id",
+        "base_revision",
+        "parent_artifact_version_id",
+        "canonical_json",
+        "canonical_sha256",
+        "state",
+        "created_at",
+        "frozen_at",
+        "published_at",
+    ),
+    "ParagraphRow": (
+        "id",
+        "script_build_id",
+        "branch_id",
+        "base_ref_id",
+        "paragraph_index",
+        "level",
+        "parent_id",
+        "name",
+        "content_range",
+        "theme",
+        "form",
+        "function",
+        "feeling",
+        "theme_elements",
+        "form_elements",
+        "function_elements",
+        "feeling_elements",
+        "description",
+        "full_description",
+        "is_active",
+        "created_at",
+        "updated_at",
+    ),
+    "ElementRow": (
+        "id",
+        "script_build_id",
+        "branch_id",
+        "base_ref_id",
+        "name",
+        "dimension_primary",
+        "dimension_secondary",
+        "commonality_analysis",
+        "topic_support",
+        "weight_score",
+        "support_elements",
+        "is_active",
+        "created_at",
+        "updated_at",
+    ),
+    "ParagraphElementLinkRow": (
+        "id",
+        "script_build_id",
+        "branch_id",
+        "paragraph_id",
+        "element_id",
+    ),
+    "RootDeliveryManifestV1": (
+        "schema_version",
+        "direction_ref",
+        "candidate_portfolio_ref",
+        "structured_script_ref",
+        "input_closure_digest",
+        "legacy_projection_digest",
+        "build_summary",
+        "blocking_defects",
+    ),
+    "LegacyProjectionCanonicalV1": (
+        "schema_version",
+        "direction",
+        "summary",
+        "paragraph_count",
+        "element_count",
+        "link_count",
+        "paragraphs",
+        "elements",
+        "paragraph_element_links",
+    ),
+    "Publication": (
+        "publication_id",
+        "script_build_id",
+        "publication_type",
+        "accept_decision_id",
+        "artifact_version_id",
+        "expected_sha256",
+        "state",
+        "attempt_count",
+        "publication_revision",
+        "last_error_code",
+        "last_error_summary",
+        "created_at",
+        "updated_at",
+        "published_at",
+    ),
+    "PublicationResult": (
+        "script_build_id",
+        "publication_id",
+        "publication_type",
+        "state",
+        "legacy_projection_digest",
+        "committed",
+        "post_commit_readback_ok",
+    ),
+    "HttpCommandRecord": (
+        "id",
+        "principal_scope_sha256",
+        "route_family",
+        "idempotency_key",
+        "request_fingerprint",
+        "preallocated_root_trace_id",
+        "resource_id",
+        "state",
+        "response_status",
+        "response_json",
+        "created_at",
+        "updated_at",
+    ),
+}
+
+ENUM_CATALOG: dict[str, tuple[str, ...]] = {
+    "BuildStatus": ("running", "stopping", "success", "failed", "partial", "stopped"),
+    "TaskStatus": (
+        "pending",
+        "running",
+        "awaiting_validation",
+        "validating",
+        "awaiting_decision",
+        "needs_replan",
+        "waiting_children",
+        "completed",
+        "superseded",
+        "blocked",
+        "cancelled",
+    ),
+    "AttemptStatus": ("running", "submitted", "failed", "stopped", "expired"),
+    "ValidationRunStatus": (
+        "pending",
+        "running",
+        "completed",
+        "error",
+        "stopped",
+        "expired",
+    ),
+    "ValidationVerdict": ("passed", "failed", "inconclusive"),
+    "DecisionAction": (
+        "accept",
+        "repair",
+        "retry",
+        "revalidate",
+        "revise",
+        "split",
+        "block",
+        "unblock",
+        "cancel",
+        "supersede",
+    ),
+    "ArtifactState": ("draft", "frozen", "published", "discarded"),
+    "PublicationState": ("pending", "publishing", "published", "failed"),
+    "ScriptTaskKind": (
+        "direction",
+        "pattern-retrieval",
+        "decode-retrieval",
+        "external-retrieval",
+        "knowledge-retrieval",
+        "structure",
+        "paragraph",
+        "element-set",
+        "compare",
+        "compose",
+        "candidate-portfolio",
+        "root-delivery",
+    ),
+}

+ 0 - 337
visualization/backend/app/creative_event_projection.py

@@ -1,337 +0,0 @@
-from __future__ import annotations
-
-import re
-from collections import defaultdict
-from typing import Any
-
-from .business_detail_text import branch_task_text, business_text
-from .decision_projection import AgentDecisionProjector
-from .runtime_event_index import RuntimeEventIndex, event_succeeded, integer
-from .runtime_tool_catalog import (
-    RETRIEVAL_AGENT_LABELS,
-    TOOL_CATALOG,
-    business_tool_label,
-    tools_in_category,
-)
-
-
-_CREATIVE_ACTION_TOOLS = tools_in_category("creative-action")
-_DECISION_INPUT_TOOLS = tools_in_category("decision-input")
-
-
-class CreativeEventProjector:
-    """Project only safely owned implementer ``think_and_plan`` records."""
-
-    def __init__(self, decisions: AgentDecisionProjector | None = None):
-        self._decisions = decisions or AgentDecisionProjector()
-
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        valid_rounds: set[int],
-        valid_branches: set[int],
-    ) -> dict[str, Any]:
-        implementers: dict[int, dict[str, Any]] = {}
-        for event in index.ordered:
-            if (
-                str(event.get("event_type") or "") == "agent_invoke"
-                and str(event.get("event_name") or "") == "script_implementer"
-                and (branch_id := integer(event.get("branch_id"))) in valid_branches
-            ):
-                implementers[branch_id] = event
-
-        events_by_branch: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        for event in index.ordered:
-            if (
-                str(event.get("event_type") or "") != "tool_call"
-                or str(event.get("event_name") or "") != "think_and_plan"
-                or not event_succeeded(event)
-            ):
-                continue
-            branch_id = integer(event.get("branch_id"))
-            round_index = integer(event.get("round_index"))
-            implementer = implementers.get(branch_id or -1)
-            if (
-                branch_id not in valid_branches
-                or round_index not in valid_rounds
-                or implementer is None
-                or integer(event.get("scope_event_id"))
-                not in {integer(implementer.get("id")), integer(implementer.get("scope_event_id"))}
-                or str(event.get("agent_role") or "") not in {"script_implementer", "implementer"}
-            ):
-                continue
-            events_by_branch[branch_id].append(event)
-
-        projected: dict[int, dict[str, Any]] = {}
-        for branch_id, records in events_by_branch.items():
-            implementer = implementers[branch_id]
-            latest = records[-1]
-            task = _task(implementer)
-            uncertainty = _uncertainties(records)
-            steps = _steps(records)
-            reasoning = _thought(latest)
-            runtime_actions = _runtime_actions(index, implementer)
-            available_upstream = _available_upstream(index, implementer)
-            decision = self._decisions.creative(
-                {
-                    "id": f"creative:{integer(latest.get('id')) or 0}",
-                    "detailRef": f"creative:{integer(latest.get('id')) or 0}",
-                    "promptRef": f"prompt:event:{integer(implementer.get('id')) or 0}",
-                    "safeLink": True,
-                    "event": latest,
-                    "task": task,
-                    "steps": steps,
-                    "reasoning": reasoning,
-                    "uncertainties": uncertainty,
-                    "inputs": available_upstream,
-                    "completeness": "partial",
-                    "notices": [
-                        {
-                            "code": "creative-record-partial",
-                            "message": "当前运行只保存了通用思考与计划,没有稳定的完整创作四元组。",
-                        }
-                    ],
-                }
-            )
-            if decision is None:
-                continue
-            decision["body"] = {
-                **(decision.get("body") or {}),
-                "availableUpstream": available_upstream,
-                "runtimeActions": runtime_actions,
-            }
-            blocks = list((decision.get("detail") or {}).get("blocks") or [])
-            insert_at = next(
-                (
-                    index_ + 1
-                    for index_, block in enumerate(blocks)
-                    if isinstance(block, dict)
-                    and block.get("type") == "creative-process"
-                ),
-                0,
-            )
-            additions = []
-            if available_upstream:
-                additions.append(
-                    {
-                        "type": "items",
-                        "title": "可确认的上游信息",
-                        "items": [
-                            {
-                                "label": item["label"],
-                                "value": item.get("summary"),
-                                "note": "可确认在上游存在或已返回,但无法确认是否被本次创作采用。",
-                                "detailRef": item.get("detailRef"),
-                            }
-                            for item in available_upstream
-                        ],
-                        "visualRole": "evidence",
-                        "presentation": "list",
-                        "collapsible": True,
-                    }
-                )
-            if runtime_actions:
-                additions.append(
-                    {
-                        "type": "items",
-                        "title": "实际脚本改动",
-                        "items": runtime_actions,
-                        "visualRole": "section",
-                        "presentation": "list",
-                        "collapsible": True,
-                    }
-                )
-            blocks[insert_at:insert_at] = additions
-            decision["detail"] = {
-                **(decision.get("detail") or {}),
-                "blocks": blocks,
-            }
-            ref_set = {
-                *(f"event:{integer(item.get('id')) or 0}" for item in records),
-                *(str(item.get("detailRef")) for item in available_upstream if item.get("detailRef")),
-                *(str(item.get("detailRef")) for item in runtime_actions if item.get("detailRef")),
-            }
-            decision["eventRefs"] = [
-                ref
-                for item in index.ordered
-                if (ref := f"event:{integer(item.get('id')) or 0}") in ref_set
-            ]
-            decision["technicalRefs"] = decision["eventRefs"]
-            projected[branch_id] = decision
-        return {"creativeByBranch": projected, "unassigned": []}
-
-
-def _task(event: dict[str, Any]) -> str | None:
-    value = event.get("inputData")
-    if isinstance(value, dict) and isinstance(value.get("task"), str):
-        text = value["task"].strip()
-        if match := re.search(r"(?:^|\s)目标\s*[::]\s*(.+)$", text, re.S):
-            text = match.group(1).strip()
-        return branch_task_text(text) or None
-    return None
-
-
-def _steps(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    for fallback_index, event in enumerate(events, start=1):
-        payload = event.get("inputData")
-        if not isinstance(payload, dict):
-            continue
-        summary = _creative_action_text(payload.get("thought_summary"))
-        action = _creative_action_text(payload.get("action")) or summary
-        plan = _creative_action_text(payload.get("plan"))
-        reasoning = _creative_action_text(payload.get("thought"))
-        if not any((action, plan, summary, reasoning)):
-            continue
-        values.append(
-            {
-                "stepIndex": integer(payload.get("thought_number")) or fallback_index,
-                "action": action or f"步骤 {fallback_index}",
-                "summary": summary,
-                "plan": plan,
-                "reasoning": reasoning,
-                "eventRef": f"event:{integer(event.get('id')) or 0}",
-            }
-        )
-    return values
-
-
-def _creative_action_text(value: Any) -> str | None:
-    if not isinstance(value, str) or not value.strip():
-        return None
-    text = value.strip()
-    exact = text.lower()
-    if exact in RETRIEVAL_AGENT_LABELS:
-        return f"委派{RETRIEVAL_AGENT_LABELS[exact]}取数"
-    if exact in TOOL_CATALOG:
-        definition = TOOL_CATALOG[exact]
-        if definition.category == "decision-input":
-            return f"读取{definition.business_label}"
-        return definition.business_label
-    if re.fullmatch(r"[a-z][a-z0-9_]{2,}", exact):
-        return None
-    replacements = {
-        **{
-            name: f"委派{label}取数"
-            for name, label in RETRIEVAL_AGENT_LABELS.items()
-        },
-        **{
-            name: (
-                f"读取{definition.business_label}"
-                if definition.category == "decision-input"
-                else definition.business_label
-            )
-            for name, definition in TOOL_CATALOG.items()
-        },
-    }
-    for name in sorted(replacements, key=len, reverse=True):
-        text = re.sub(
-            rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])",
-            replacements[name],
-            text,
-            flags=re.I,
-        )
-    text = business_text(text)
-    text = re.sub(r"调用\s*(读取|记录|新增|更新|补充|批量)", r"\1", text)
-    text = re.sub(r"委派\s*委派", "委派", text)
-    return text.strip() or None
-
-
-def _thought(event: dict[str, Any]) -> str | None:
-    payload = event.get("inputData")
-    if not isinstance(payload, dict):
-        return None
-    value = payload.get("thought_summary") or payload.get("thought")
-    return value.strip() if isinstance(value, str) and value.strip() else None
-
-
-def _uncertainties(events: list[dict[str, Any]]) -> list[str]:
-    values: list[str] = []
-    for event in events:
-        payload = event.get("inputData")
-        if not isinstance(payload, dict):
-            continue
-        text = "\n".join(
-            str(payload.get(key) or "") for key in ("thought", "thought_summary", "plan")
-        )
-        for sentence in re.split(r"[。!?\n]+", text):
-            if any(token in sentence for token in ("缺少直接证据", "未验证", "常识", "推导", "无法确认")):
-                cleaned = sentence.strip()
-                if cleaned and cleaned not in values:
-                    values.append(cleaned)
-    return values[:3]
-
-
-def _runtime_actions(
-    index: RuntimeEventIndex, implementer: dict[str, Any]
-) -> list[dict[str, Any]]:
-    scope_id = integer(implementer.get("id"))
-    if scope_id is None:
-        return []
-    values: list[dict[str, Any]] = []
-    for event in index.events_in_scope(scope_id):
-        name = str(event.get("event_name") or "")
-        if str(event.get("event_type") or "") != "tool_call" or name not in _CREATIVE_ACTION_TOOLS:
-            continue
-        event_id = integer(event.get("id")) or 0
-        status = str(event.get("status") or "unknown")
-        values.append(
-            {
-                "label": business_tool_label(name),
-                "value": "已完成" if event_succeeded(event) else "未完成",
-                "note": f"运行状态:{status}",
-                "detailRef": f"event:{event_id}",
-            }
-        )
-    return values
-
-
-def _available_upstream(
-    index: RuntimeEventIndex, implementer: dict[str, Any]
-) -> list[dict[str, Any]]:
-    implementer_id = integer(implementer.get("id"))
-    branch_id = integer(implementer.get("branch_id"))
-    round_index = integer(implementer.get("round_index"))
-    if implementer_id is None:
-        return []
-    values: list[dict[str, Any]] = []
-    for event in index.events_in_scope(implementer_id):
-        name = str(event.get("event_name") or "")
-        if str(event.get("event_type") or "") != "tool_call" or name not in _DECISION_INPUT_TOOLS:
-            continue
-        if not event_succeeded(event):
-            continue
-        values.append(
-            {
-                "label": business_tool_label(name),
-                "summary": "实现 Agent 在创作过程中直接读取",
-                "observedRelation": "read-by-actor",
-                "decisionUse": "not-recorded",
-                "detailRef": f"event:{integer(event.get('id')) or 0}",
-            }
-        )
-    for event in index.ordered:
-        name = str(event.get("event_name") or "")
-        if (
-            str(event.get("event_type") or "") != "agent_invoke"
-            or name not in RETRIEVAL_AGENT_LABELS
-            or integer(event.get("parent_event_id")) != implementer_id
-            or integer(event.get("branch_id")) != branch_id
-            or integer(event.get("round_index")) != round_index
-            or not event_succeeded(event)
-        ):
-            continue
-        values.append(
-            {
-                "label": RETRIEVAL_AGENT_LABELS[name],
-                "summary": "取数结果已返回实现 Agent",
-                "observedRelation": "returned-to-actor",
-                "decisionUse": "not-recorded",
-                "detailRef": f"event:{integer(event.get('id')) or 0}",
-            }
-        )
-    unique: dict[tuple[Any, Any], dict[str, Any]] = {}
-    for item in values:
-        unique[(item.get("label"), item.get("detailRef"))] = item
-    return list(unique.values())

+ 0 - 896
visualization/backend/app/decision_projection.py

@@ -1,896 +0,0 @@
-from __future__ import annotations
-
-import re
-from typing import Any, Iterable, Mapping
-
-from .business_detail_text import business_text
-
-
-_SUCCESS_STATUSES = {"ok", "success", "succeeded", "completed", "complete"}
-_DECISION_BASIS_TITLE = "根据以下信息做出的决策"
-_RELATIONS = {
-    "direct-read": "read-by-actor",
-    "read-by-actor": "read-by-actor",
-    "returned-report": "returned-to-actor",
-    "returned-to-actor": "returned-to-actor",
-    "persisted-record": "persisted-before-decision",
-    "persisted-before-decision": "persisted-before-decision",
-    "standing-constraint": "standing-constraint",
-}
-
-
-class AgentDecisionProjector:
-    """Create the V8 decision union from already-associated, plain records.
-
-    The projector deliberately does not scan event timelines or infer causal
-    links.  Callers must first establish ownership/association, then pass those
-    facts here.  In particular an observed read remains a read unless its input
-    explicitly records ``decisionUse=explicit-basis``.
-    """
-
-    def direction(self, payload: Mapping[str, Any]) -> dict[str, Any]:
-        subtype = _machine(payload.get("subtype")) or "direction"
-        actor = _actor(payload.get("actor"), default_role="main", default_label="主 Agent")
-        decision_items = _strings(
-            payload.get("decisionItems") or payload.get("decisions")
-        )
-        reasoning = _text(payload.get("reasoning") or payload.get("explicitReasoning"))
-        constraints = _strings(payload.get("constraints"))
-        inputs = _inputs(payload.get("inputs"))
-        primary = decision_items[0] if decision_items else _text(payload.get("decision"))
-        secondary = []
-        if inputs:
-            secondary.append(_card_line("inputs", _DECISION_BASIS_TITLE, _input_labels(inputs)))
-        if constraints:
-            secondary.append(_card_line("constraints", "关键约束", _join(constraints, 2)))
-        if reasoning:
-            secondary.append(_card_line("reasoning", "决策理由", reasoning))
-        body = {
-            "type": "direction",
-            "decisionItems": decision_items,
-            "reasoning": reasoning,
-            "constraints": constraints,
-        }
-        if subtype == "implementation-plan":
-            implementation_plan = _implementation_plan(payload.get("implementationPlan"), reasoning)
-            body["implementationPlan"] = implementation_plan
-            blocks = [
-                _implementation_plan_block(implementation_plan),
-                _items_block(_DECISION_BASIS_TITLE, [_input_business_item(item) for item in inputs], visual_role="evidence"),
-                _items_block("关键约束", constraints),
-            ]
-        elif subtype == "objective" and _source_text(payload.get("sourceDocument")):
-            source_document = _source_text(payload.get("sourceDocument"))
-            body["sourceDocument"] = source_document
-            blocks = [
-                _source_document_block(
-                    "当前保存的创作方向",
-                    source_document,
-                ),
-                _items_block(
-                    _DECISION_BASIS_TITLE,
-                    [_input_business_item(item) for item in inputs],
-                    visual_role="evidence",
-                ),
-            ]
-        elif subtype == "round-goal" and _source_text(payload.get("sourceDocument")):
-            source_document = _source_text(payload.get("sourceDocument"))
-            body["sourceDocument"] = source_document
-            blocks = [
-                _source_document_block(
-                    "本轮目标构成",
-                    source_document,
-                ),
-                _items_block(
-                    _DECISION_BASIS_TITLE,
-                    [_input_business_item(item) for item in inputs],
-                    visual_role="evidence",
-                ),
-                _summary_block("目标来源", reasoning, visual_role="reason"),
-            ]
-        else:
-            blocks = [
-                _decision_items_block("最终方向", decision_items or ([primary] if primary else []), visual_role="key-result"),
-                _items_block(_DECISION_BASIS_TITLE, [_input_business_item(item) for item in inputs], visual_role="evidence"),
-                _items_block("关键约束", constraints),
-                _summary_block("明确理由", reasoning, visual_role="reason"),
-            ]
-        return _decision(
-            payload,
-            decision_type="direction",
-            subtype=subtype,
-            actor=actor,
-            authority="final",
-            question=_text(payload.get("question")) or _direction_question(subtype),
-            inputs=inputs,
-            body=body,
-            primary_label=_text(payload.get("primaryLabel")) or "最终方向",
-            primary=primary,
-            secondary=secondary,
-            blocks=blocks,
-        )
-
-    def data_tradeoff(self, row: Mapping[str, Any]) -> dict[str, Any]:
-        inputs = _inputs(row.get("inputs") or _source_inputs(row.get("sources")))
-        decision_text = _text(row.get("decision"))
-        reasoning = _text(row.get("reasoning"))
-        scope_items = _strings(row.get("scopeItems"))
-        body = {
-            "type": "tradeoff",
-            "scopeItems": scope_items,
-            "decision": decision_text,
-            "selected": _strings(row.get("selected")),
-            "combined": _strings(row.get("combined")),
-            "parked": _strings(row.get("parked")),
-            "rejected": _strings(row.get("rejected")),
-            "reasoning": reasoning,
-        }
-        secondary = []
-        if inputs:
-            secondary.append(_card_line("scope", "参与判断", f"{len(inputs)} 类信息"))
-        if reasoning:
-            secondary.append(_card_line("reasoning", "理由", reasoning))
-        return _decision(
-            row,
-            decision_type="tradeoff",
-            subtype="data-tradeoff",
-            actor=_actor(
-                row.get("actor"),
-                default_role="implementer",
-                default_label="实现 Agent",
-            ),
-            authority="implementation-scope",
-            question=_text(row.get("question")) or "哪些信息用于当前实现方案?",
-            inputs=inputs,
-            body=body,
-            primary_label="取舍结论",
-            primary=decision_text,
-            secondary=secondary,
-            blocks=[
-                _items_block("取舍对象", scope_items),
-                _items_block("参与判断", [_input_business_item(item) for item in inputs], visual_role="evidence", collapsible=True),
-                _summary_block("明确取舍", decision_text, visual_role="key-result"),
-                _summary_block("理由", reasoning, visual_role="reason"),
-            ],
-        )
-
-    def multipath_tradeoff(self, row: Mapping[str, Any]) -> dict[str, Any]:
-        branch_ids = _unique_values(row.get("branchIds") or row.get("branch_ids"))
-        inputs = _inputs(row.get("inputs"))
-        decision_text = _text(row.get("decision"))
-        reasoning = _text(row.get("reasoning"))
-        selected = _strings(row.get("selected"))
-        rejected = _strings(row.get("rejected"))
-        body = {
-            "type": "tradeoff",
-            "scopeItems": _strings(row.get("scopeItems")),
-            "decision": decision_text,
-            "selected": selected,
-            "combined": _strings(row.get("combined")),
-            "parked": _strings(row.get("parked")),
-            "rejected": rejected,
-            "reasoning": reasoning,
-            # Identity is useful to flow wiring but is not rendered as business copy.
-            "branchIds": branch_ids,
-            "reviewComparison": row.get("reviewComparison"),
-        }
-        secondary = []
-        if branch_ids:
-            secondary.append(
-                _card_line("scope", "取舍范围", f"比较 {len(branch_ids)} 个候选方案")
-            )
-        if reasoning:
-            secondary.append(_card_line("reasoning", "决策理由", reasoning))
-        if row.get("reviewComparison"):
-            secondary.append(_card_line("review", "评审对照", "可在详情中查看"))
-        return _decision(
-            row,
-            decision_type="tradeoff",
-            subtype="multipath-tradeoff",
-            actor=_actor(row.get("actor"), default_role="main", default_label="主 Agent"),
-            authority="final",
-            question=_text(row.get("question")) or "多个候选方案最终如何处理?",
-            inputs=inputs,
-            body=body,
-            primary_label="最终决定",
-            primary=decision_text,
-            secondary=secondary,
-            blocks=[
-                _summary_block(
-                    "取舍对象",
-                    f"{len(branch_ids)} 个候选方案" if branch_ids else None,
-                ),
-                _items_block("参与判断", [_input_business_item(item) for item in inputs], visual_role="evidence", collapsible=True),
-                _summary_block("最终决定", decision_text, visual_role="key-result"),
-                _summary_block("决策理由", reasoning, visual_role="reason"),
-                _comparison_block(row.get("reviewComparison")),
-            ],
-        )
-
-    def evaluation(self, payload: Mapping[str, Any]) -> dict[str, Any]:
-        subtype = _machine(payload.get("subtype")) or "overall-evaluation"
-        is_multipath = subtype == "multipath-evaluation"
-        inputs = _inputs(payload.get("inputs"))
-        conclusion = _text(payload.get("conclusion"))
-        recommendation = _text(payload.get("recommendation"))
-        subjects = _strings(payload.get("subjects"))
-        criteria = _strings(payload.get("criteria"))
-        item_conclusions = _business_dicts(payload.get("itemConclusions"))
-        achievements = _strings(payload.get("achievements"))
-        problems = _business_dicts(payload.get("problems"))
-        branch_evaluations = _mapping_list(payload.get("branchEvaluations"))
-        comparison_rows = _mapping_list(payload.get("comparisonRows"))
-        body = {
-            "type": "evaluation",
-            "subjects": subjects,
-            "criteria": criteria,
-            "itemConclusions": item_conclusions,
-            "achievements": achievements,
-            "problems": problems,
-            "branchEvaluations": branch_evaluations,
-            "comparisonRows": comparison_rows,
-            "conclusion": conclusion,
-            "recommendation": recommendation,
-            "nextGoal": _text(payload.get("nextGoal")),
-        }
-        secondary = []
-        if achievements:
-            secondary.append(_card_line("achievement", "已达成", _join(achievements, 2)))
-        if problems:
-            secondary.append(
-                _card_line("problems", "需要关注", _problem_summary(problems), "warning")
-            )
-        if recommendation:
-            secondary.append(_card_line("recommendation", "评审建议", recommendation))
-        primary = conclusion or _item_conclusion_summary(item_conclusions) or recommendation
-        if is_multipath and branch_evaluations:
-            detail_blocks = [
-                _evaluation_branches_block(branch_evaluations),
-                _evaluation_matrix_block(comparison_rows) or _items_block("评审标准", criteria),
-                _summary_block("评审建议", recommendation),
-            ]
-        else:
-            detail_blocks = [
-                _items_block("评审对象", subjects),
-                _items_block("评审标准", criteria),
-                _items_block("逐项结论", item_conclusions),
-                _items_block("已达成", achievements),
-                _items_block("问题", problems),
-                _summary_block("总结论", conclusion, visual_role="key-result"),
-                _summary_block("评审建议", recommendation),
-            ]
-        return _decision(
-            payload,
-            decision_type="evaluation",
-            subtype=subtype,
-            actor=_actor(
-                payload.get("actor"),
-                default_role=("multipath-evaluator" if is_multipath else "overall-evaluator"),
-                default_label=("多方案评审 Agent" if is_multipath else "整体评审 Agent"),
-            ),
-            authority="recommendation",
-            question=_text(payload.get("question"))
-            or ("哪些候选方案更值得采用?" if is_multipath else "当前主脚本是否达到目标?"),
-            inputs=inputs,
-            body=body,
-            primary_label="评审结论",
-            primary=primary,
-            secondary=secondary,
-            blocks=detail_blocks,
-        )
-
-    def creative(self, payload: Mapping[str, Any]) -> dict[str, Any] | None:
-        """Project an implementer creative decision only from a safe event link.
-
-        An artifact reference may be attached to a real creative record, but it
-        is never sufficient to create that record.  This avoids inferring the
-        implementer's plan from the eventual candidate script.
-        """
-
-        if payload.get("safeLink") is not True:
-            return None
-        event = payload.get("event")
-        event = event if isinstance(event, Mapping) else payload
-        if not _is_successful_think_and_plan(event):
-            return None
-        raw_input = event.get("inputData") or event.get("input")
-        if not isinstance(raw_input, Mapping):
-            return None
-        thought = _text(
-            payload.get("reasoning")
-            or raw_input.get("thought")
-            or raw_input.get("thought_summary")
-        )
-        steps = _creative_steps(payload.get("steps"))
-        actions = _strings(payload.get("actions"))
-        if not steps:
-            fallback_action = _text(raw_input.get("action"))
-            fallback_plan = _source_text(raw_input.get("plan"))
-            if fallback_action or fallback_plan or thought:
-                steps = [{
-                    "stepIndex": 1,
-                    "action": fallback_action or "创作处理",
-                    "summary": _text(raw_input.get("thought_summary")),
-                    "plan": fallback_plan,
-                    "reasoning": _source_text(raw_input.get("thought")),
-                }]
-        if not actions:
-            actions = [str(step["action"]) for step in steps if step.get("action")]
-        if not any((thought, actions, steps)):
-            return None
-        task = _text(payload.get("task"))
-        explicit_basis = _strings(payload.get("basis"))
-        uncertainties = _strings(payload.get("uncertainties"))
-        body = {
-            "type": "creative",
-            "task": task,
-            "basis": explicit_basis,
-            "actions": actions,
-            "steps": steps,
-            "reasoning": thought,
-            "output": _text(payload.get("output")),
-            "uncertainties": uncertainties,
-        }
-        secondary = []
-        projected_payload = dict(payload)
-        projected_payload.setdefault("id", f"creative-event:{event.get('id')}")
-        return _decision(
-            projected_payload,
-            decision_type="creative",
-            subtype="creative-handling",
-            actor=_actor(
-                payload.get("actor"),
-                default_role="implementer",
-                default_label="实现 Agent",
-            ),
-            authority="implementation-scope",
-            question=_text(payload.get("question")) or "当前实现方案如何形成候选产出?",
-            inputs=_inputs(payload.get("inputs")),
-            body=body,
-            primary_label="创作处理",
-            primary=actions[0] if actions else thought,
-            secondary=secondary,
-            blocks=[
-                _items_block("明确依据", explicit_basis, visual_role="evidence", collapsible=True),
-                _creative_process_block(steps) or _items_block("创作处理", actions),
-                _summary_block("产出的候选表", body["output"], visual_role="key-result"),
-                _items_block("存疑项", uncertainties, visual_role="notice"),
-            ],
-        )
-
-
-def _decision(
-    payload: Mapping[str, Any],
-    *,
-    decision_type: str,
-    subtype: str,
-    actor: dict[str, str],
-    authority: str,
-    question: str,
-    inputs: list[dict[str, Any]],
-    body: dict[str, Any],
-    primary_label: str,
-    primary: str | None,
-    secondary: list[dict[str, Any]],
-    blocks: list[dict[str, Any] | None],
-) -> dict[str, Any]:
-    completeness = _completeness(payload.get("completeness"), primary)
-    notices = _notices(payload.get("notices"))
-    if not primary and not any(item.get("code") == "record-missing" for item in notices):
-        notices.append({"code": "record-missing", "message": "没有找到结构化决策结论。"})
-    decision_id = _machine(payload.get("id") or payload.get("detailRef")) or f"{subtype}:unknown"
-    detail_ref = _machine(payload.get("detailRef")) or decision_id
-    result = {
-        "id": decision_id,
-        "semanticKind": "decision",
-        "decisionType": decision_type,
-        "subtype": subtype,
-        "actor": actor,
-        "authority": authority,
-        "question": question,
-        "inputs": inputs,
-        "body": body,
-        "card": {
-            "primary": {
-                "label": primary_label,
-                "value": primary or "决策结论未记录",
-            },
-            "secondary": [item for item in secondary if item.get("value")][:3],
-        },
-        "detailRef": detail_ref,
-        "promptRef": _machine(payload.get("promptRef")),
-        "artifactRef": _machine(payload.get("artifactRef")),
-        "completeness": completeness,
-        "notices": notices,
-        "detail": {
-            "detailKind": "agent-decision",
-            "decisionType": decision_type,
-            "actor": actor,
-            "authority": authority,
-            "question": question,
-            "blocks": [block for block in blocks if block],
-        },
-    }
-    return result
-
-
-def _inputs(value: Any) -> list[dict[str, Any]]:
-    if not isinstance(value, Iterable) or isinstance(value, (str, bytes, Mapping)):
-        return []
-    result: list[dict[str, Any]] = []
-    for raw in value:
-        if not isinstance(raw, Mapping):
-            continue
-        observed = _RELATIONS.get(
-            _text(raw.get("observedRelation") or raw.get("relation")) or "",
-            "persisted-before-decision",
-        )
-        explicit_use = _machine(raw.get("decisionUse"))
-        decision_use = explicit_use if explicit_use in {"explicit-basis", "not-recorded"} else "not-recorded"
-        if not (raw.get("observedRelation") or raw.get("relation")):
-            continue
-        label = _text(raw.get("label") or raw.get("name") or raw.get("type"))
-        if not label:
-            continue
-        result.append(
-            {
-                "label": label,
-                "summary": _text(raw.get("summary")),
-                "observedRelation": observed,
-                "decisionUse": decision_use,
-                "detailRef": _text(raw.get("detailRef")),
-            }
-        )
-    return result
-
-
-def _source_inputs(value: Any) -> list[dict[str, Any]]:
-    if not isinstance(value, list):
-        return []
-    result = []
-    for index, item in enumerate(value, 1):
-        if isinstance(item, Mapping):
-            label = _text(
-                item.get("label")
-                or item.get("data_type")
-                or item.get("source_type")
-                or item.get("type")
-            )
-            summary = _text(
-                item.get("summary")
-                or item.get("data_content")
-                or item.get("title")
-                or item.get("name")
-            )
-            explicit_use = item.get("decisionUse")
-        else:
-            label, summary, explicit_use = _text(item), None, None
-        result.append(
-            {
-                "label": label or f"信息来源 {index}",
-                "summary": summary,
-                "observedRelation": "persisted-before-decision",
-                "decisionUse": explicit_use or "not-recorded",
-            }
-        )
-    return result
-
-
-def _actor(value: Any, *, default_role: str, default_label: str) -> dict[str, str]:
-    if isinstance(value, Mapping):
-        return {
-            "role": _machine(value.get("role")) or default_role,
-            "label": _text(value.get("label")) or default_label,
-        }
-    return {"role": default_role, "label": default_label}
-
-
-def _summary_block(
-    title: str,
-    value: Any,
-    *,
-    visual_role: str = "section",
-    presentation: str = "prose",
-) -> dict[str, Any] | None:
-    text = _text(value)
-    return {
-        "type": "summary",
-        "title": title,
-        "value": text,
-        "visualRole": visual_role,
-        "presentation": presentation,
-    } if text else None
-
-
-def _source_document_block(title: str, value: Any) -> dict[str, Any] | None:
-    text = _source_text(value)
-    return {
-        "type": "summary",
-        "title": title,
-        "value": text,
-        "visualRole": "key-result",
-        "presentation": "document",
-    } if text else None
-
-
-def _items_block(
-    title: str,
-    items: Any,
-    *,
-    visual_role: str = "section",
-    presentation: str = "list",
-    collapsible: bool = False,
-    default_open: bool = False,
-) -> dict[str, Any] | None:
-    if not isinstance(items, list) or not items:
-        return None
-    return {
-        "type": "items",
-        "title": title,
-        "items": items,
-        "visualRole": visual_role,
-        "presentation": presentation,
-        "collapsible": collapsible,
-        "defaultOpen": default_open,
-    }
-
-
-def _creative_process_block(steps: list[dict[str, Any]]) -> dict[str, Any] | None:
-    if not steps:
-        return None
-    return {
-        "type": "creative-process",
-        "title": "创作处理",
-        "steps": steps,
-        "visualRole": "section",
-        "presentation": "process",
-    }
-
-
-def _evaluation_branches_block(branches: list[dict[str, Any]]) -> dict[str, Any] | None:
-    if not branches:
-        return None
-    return {
-        "type": "evaluation-branches",
-        "title": "逐方案评审",
-        "branches": branches,
-        "visualRole": "section",
-        "presentation": "grouped-review",
-    }
-
-
-def _evaluation_matrix_block(rows: list[dict[str, Any]]) -> dict[str, Any] | None:
-    if not rows:
-        return None
-    return {
-        "type": "evaluation-matrix",
-        "title": "评审标准与方案对比",
-        "rows": rows,
-        "visualRole": "section",
-        "presentation": "matrix",
-    }
-
-
-def _decision_items_block(
-    title: str,
-    items: list[str],
-    *,
-    visual_role: str = "section",
-) -> dict[str, Any] | None:
-    if not items:
-        return None
-    if len(items) == 1:
-        return _summary_block(title, items[0], visual_role=visual_role)
-    return _items_block(title, items, visual_role=visual_role)
-
-
-def _implementation_plan(value: Any, reasoning: str | None) -> dict[str, Any]:
-    plan = value if isinstance(value, Mapping) else {}
-    routes = plan.get("routes") if isinstance(plan.get("routes"), list) else []
-    return {
-        "mode": _text(plan.get("mode")) or _text(plan.get("rawMode")),
-        "reasoning": _text(plan.get("reasoning")) or reasoning,
-        "routes": [
-            {
-                "pathIndex": route.get("pathIndex") or index,
-                "pathType": _business_text(route.get("pathType")),
-                "action": _business_text(route.get("action")),
-                "target": _business_text(route.get("target")),
-                "method": _business_text(route.get("method")),
-                "emphasis": _business_text(route.get("emphasis")),
-            }
-            for index, route in enumerate(routes, 1)
-            if isinstance(route, Mapping)
-        ],
-    }
-
-
-def _implementation_plan_block(plan: Mapping[str, Any]) -> dict[str, Any] | None:
-    routes = plan.get("routes") if isinstance(plan.get("routes"), list) else []
-    if not routes and not plan.get("reasoning"):
-        return None
-    return {
-        "type": "implementation-plan",
-        "title": "本轮实施路线",
-        "visualRole": "section",
-        "presentation": "plan",
-        "mode": plan.get("mode"),
-        "routes": routes,
-        "reasoning": plan.get("reasoning"),
-    }
-
-
-def _item_conclusion_summary(items: list[dict[str, Any]]) -> str | None:
-    summaries = []
-    for item in items[:3]:
-        subject = _text(item.get("subject") or item.get("label"))
-        conclusion = _text(item.get("conclusion") or item.get("value"))
-        if subject and conclusion:
-            summaries.append(f"{subject}:{conclusion}")
-    if not summaries:
-        return None
-    suffix = f";另有 {len(items) - 3} 项" if len(items) > 3 else ""
-    return ";".join(summaries) + suffix
-
-
-def _comparison_block(value: Any) -> dict[str, Any] | None:
-    if not isinstance(value, list) or not value:
-        return None
-    return {
-        "type": "comparison",
-        "title": "评审建议与最终决定",
-        "visualRole": "section",
-        "presentation": "comparison",
-        "rows": _business_value(value),
-    }
-
-
-def _input_business_item(item: Mapping[str, Any]) -> dict[str, str]:
-    result = {"label": str(item.get("label") or "信息")}
-    summary = _business_text(item.get("summary"))
-    if summary:
-        result["value"] = summary
-    relation = {
-        "read-by-actor": "决策前直接读取",
-        "returned-to-actor": "评审返回给决策者",
-        "persisted-before-decision": "此前已保存",
-        "standing-constraint": "持续约束",
-    }.get(str(item.get("observedRelation") or ""))
-    use = (
-        "本轮必须遵守"
-        if item.get("observedRelation") == "standing-constraint"
-        else "明确作为决策依据"
-        if item.get("decisionUse") == "explicit-basis"
-        else "是否采用未记录"
-    )
-    result["note"] = ";".join(part for part in (relation, use) if part)
-    return result
-
-
-def _business_text(value: Any) -> str | None:
-    text = business_text(value)
-    return text or None
-
-
-def _card_line(key: str, label: str, value: Any, tone: str | None = None) -> dict[str, Any]:
-    line = {"key": key, "label": label, "value": _preview(value, 160)}
-    if tone:
-        line["tone"] = tone
-    return line
-
-
-def _notices(value: Any) -> list[dict[str, str]]:
-    if not isinstance(value, list):
-        return []
-    result = []
-    for item in value:
-        if isinstance(item, Mapping):
-            code = _machine(item.get("code"))
-            message = _text(item.get("message"))
-        else:
-            code, message = "notice", _text(item)
-        if code and message:
-            result.append({"code": code, "message": message})
-    return result
-
-
-def _business_dicts(value: Any) -> list[Any]:
-    if not isinstance(value, list):
-        return []
-    result: list[Any] = []
-    for item in value:
-        if isinstance(item, Mapping):
-            cleaned = {
-                key: _business_value(item[key])
-                for key in (
-                    "label",
-                    "subject",
-                    "conclusion",
-                    "summary",
-                    "severity",
-                    "recommendation",
-                    "achievements",
-                    "problems",
-                )
-                if item.get(key) not in (None, "")
-            }
-            if cleaned:
-                result.append(cleaned)
-        elif text := _text(item):
-            result.append(text)
-    return result
-
-
-def _mapping_list(value: Any) -> list[dict[str, Any]]:
-    if not isinstance(value, list):
-        return []
-    return [
-        _business_value(dict(item))
-        for item in value
-        if isinstance(item, Mapping)
-    ]
-
-
-def _creative_steps(value: Any) -> list[dict[str, Any]]:
-    if not isinstance(value, list):
-        return []
-    result: list[dict[str, Any]] = []
-    for fallback_index, item in enumerate(value, start=1):
-        if not isinstance(item, Mapping):
-            continue
-        action = _text(item.get("action"))
-        plan = _source_text(item.get("plan"))
-        summary = _text(item.get("summary"))
-        reasoning = _source_text(item.get("reasoning"))
-        if not any((action, plan, summary, reasoning)):
-            continue
-        result.append(
-            {
-                "stepIndex": int(item.get("stepIndex") or fallback_index),
-                "action": action or f"步骤 {fallback_index}",
-                "summary": summary,
-                "plan": plan,
-                "reasoning": reasoning,
-                "eventRef": _text(item.get("eventRef")),
-            }
-        )
-    return result
-
-
-def _problem_summary(problems: list[Any]) -> str:
-    values = []
-    for item in problems[:2]:
-        if isinstance(item, Mapping):
-            values.append(_text(item.get("summary") or item.get("label")) or "待处理问题")
-        else:
-            values.append(str(item))
-    return _join(values, 2)
-
-
-def _input_labels(inputs: list[dict[str, Any]]) -> str:
-    return _join([str(item["label"]) for item in inputs], 3)
-
-
-def _direction_question(subtype: str) -> str:
-    return {
-        "objective": "这次构建要形成什么创作方向?",
-        "round-goal": "本轮优先解决什么问题?",
-        "implementation-plan": "本轮应如何组织实现方案?",
-    }.get(subtype, "这一步确定了什么方向?")
-
-
-def _is_successful_think_and_plan(event: Mapping[str, Any]) -> bool:
-    return (
-        str(event.get("event_type") or "") == "tool_call"
-        and str(event.get("event_name") or "") == "think_and_plan"
-        and str(event.get("agent_role") or "")
-        in {"script_implementer", "implementer"}
-        and str(event.get("status") or "").lower() in _SUCCESS_STATUSES
-    )
-
-
-def _completeness(value: Any, primary: str | None) -> str:
-    explicit = _machine(value)
-    if explicit in {"complete", "partial", "missing"}:
-        return explicit
-    return "complete" if primary else "missing"
-
-
-def _strings(value: Any) -> list[str]:
-    if not isinstance(value, list):
-        return []
-    return [text for item in value if (text := _text(item))]
-
-
-def _unique_values(value: Any) -> list[Any]:
-    if not isinstance(value, list):
-        return []
-    result = []
-    for item in value:
-        if item not in result:
-            result.append(item)
-    return result
-
-
-def _join(values: list[str], limit: int) -> str:
-    kept = [value for value in values if value][:limit]
-    suffix = f" · 另有 {len(values) - limit} 项" if len(values) > limit else ""
-    return " · ".join(kept) + suffix
-
-
-def _preview(value: Any, limit: int) -> str:
-    text = _text(value) or ""
-    if len(text) <= limit:
-        return text
-    cut = text[:limit]
-    boundary = max(cut.rfind(mark) for mark in "。!?;")
-    if boundary >= max(24, limit // 2):
-        return cut[: boundary + 1]
-    return cut.rstrip() + "…"
-
-
-def _text(value: Any) -> str | None:
-    if value is None:
-        return None
-    if isinstance(value, (dict, list, tuple, set)):
-        return None
-    text = str(value).replace("\\n", "\n")
-    text = re.sub(r"[`*_#]+", "", text)
-    text = re.sub(r"\s+", " ", text).strip(" ::")
-    return _business_copy(text) or None
-
-
-def _source_text(value: Any) -> str | None:
-    """Keep authoritative saved Markdown intact for source-document rendering."""
-
-    if value is None or isinstance(value, (dict, list, tuple, set)):
-        return None
-    text = str(value).replace("\\n", "\n").strip()
-    return text or None
-
-
-def _business_value(value: Any) -> Any:
-    if isinstance(value, str):
-        return _business_copy(value)
-    if isinstance(value, list):
-        return [_business_value(item) for item in value]
-    if isinstance(value, Mapping):
-        return {key: _business_value(item) for key, item in value.items()}
-    return value
-
-
-def _business_copy(text: str) -> str:
-    """Translate machine-facing tokens without deleting ordinary numbers."""
-
-    replacements = (
-        (r"\bretrieve_data_decode_case\b|\bretrievedatadecodecase\b", "解构案例取数"),
-        (r"\bretrieve_data_knowledge\b|\bretrievedataknowledge\b", "知识取数"),
-        (r"\bget_account_script_persona_points\b|\bgetaccountscriptpersonapoints\b", "账号人设"),
-        (r"\bget_account_script_section_patterns\b", "账号段落模式"),
-        (r"\bget_topic_detail\b", "选题内容"),
-        (r"\bget_script_snapshot\b", "当前主脚本"),
-        (r"\brecord_data_decision\b", "记录数据取舍"),
-        (r"\bthink_and_plan\b", "实现思考与计划"),
-    )
-    for pattern, label in replacements:
-        text = re.sub(pattern, label, text, flags=re.IGNORECASE)
-    text = re.sub(
-        r"\bpost_?id\s*[:=]?(?:\s*[A-Za-z0-9-]+)?",
-        "账号内容样本",
-        text,
-        flags=re.IGNORECASE,
-    )
-    text = re.sub(r"\bscript_build_id\s*[:=]\s*\d+\b", "", text, flags=re.IGNORECASE)
-    text = re.sub(r"\bround_index\s*[:=]\s*(\d+)\b", r"第 \1 轮", text, flags=re.IGNORECASE)
-    text = re.sub(r"\bbranch[_\s-]*(\d+)\b", r"方案 \1", text, flags=re.IGNORECASE)
-    text = re.sub(r"\bP(\d+)\b", r"段落 \1", text)
-    text = re.sub(r"\bD(\d+)\b", r"评估维度 \1", text)
-    text = re.sub(r"\bbase\b", "主脚本", text, flags=re.IGNORECASE)
-    return re.sub(r"\s+", " ", text).strip(" ::")
-
-
-def _machine(value: Any) -> str | None:
-    if value is None or isinstance(value, (dict, list, tuple, set)):
-        return None
-    text = str(value).strip()
-    return text or None

+ 0 - 156
visualization/backend/app/evaluation_event_projection.py

@@ -1,156 +0,0 @@
-from __future__ import annotations
-
-import re
-from collections import defaultdict
-from typing import Any
-
-from .evaluation_report_parser import EvaluationReportParser
-from .runtime_event_index import (
-    RuntimeEventIndex,
-    event_input,
-    event_output,
-    event_summary,
-    integer,
-)
-
-
-class EvaluationEventProjector:
-    """Project evaluator calls using event metadata for identity and one parser.
-
-    The report body never decides build/round association.  It only supplies
-    business-facing evaluation content after the event has been placed by its
-    structured metadata.
-    """
-
-    def __init__(self, parser: EvaluationReportParser | None = None):
-        self._parser = parser or EvaluationReportParser()
-
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        valid_rounds: set[int],
-        valid_branches: set[int],
-    ) -> dict[str, Any]:
-        multipath: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        overall: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        unassigned: list[dict[str, Any]] = []
-
-        for event in index.ordered:
-            if str(event.get("event_type") or "") != "agent_invoke":
-                continue
-            name = str(event.get("event_name") or "")
-            if name not in {"script_multipath_evaluator", "script_evaluator"}:
-                continue
-            round_index = integer(event.get("round_index"))
-            if round_index not in valid_rounds:
-                unassigned.append(event_summary(event, "missing-business-context"))
-                continue
-
-            report = _agent_summary(event) or str(event.get("output_preview") or "")
-            parsed = self._parser.parse(
-                report,
-                kind="multipath" if name == "script_multipath_evaluator" else "overall",
-            )
-            if name == "script_evaluator":
-                overall[round_index].append(
-                    {
-                        **event_summary(event, "structured-round"),
-                        **parsed,
-                        "id": f"overall-evaluation:{integer(event.get('id')) or 0}",
-                        "roundIndex": round_index,
-                        "detailRef": f"event:{integer(event.get('id')) or 0}",
-                        "promptRef": f"prompt:event:{integer(event.get('id')) or 0}",
-                    }
-                )
-                continue
-
-            task = _agent_task(event) or str(event.get("input_preview") or "")
-            branch_ids = [value for value in _branch_ids(task) if value in valid_branches]
-            review = {
-                **event_summary(event, "explicit-task-branch-ids"),
-                **parsed,
-                "id": f"multipath-review:{integer(event.get('id')) or 0}",
-                "roundIndex": round_index,
-                "branchIds": branch_ids,
-                "branchConclusions": _branch_conclusions(parsed, branch_ids),
-                "comparison": parsed.get("recommendation"),
-                "detailRef": f"event:{integer(event.get('id')) or 0}",
-                "promptRef": f"prompt:event:{integer(event.get('id')) or 0}",
-            }
-            if branch_ids:
-                multipath[round_index].append(review)
-            else:
-                unassigned.append({**review, "association": "missing-branch-scope"})
-
-        return {
-            "multipathReviewsByRound": dict(multipath),
-            "overallEvaluationsByRound": dict(overall),
-            "unassigned": unassigned,
-        }
-
-
-def _branch_ids(text: str) -> list[int]:
-    values: list[int] = []
-    source = text or ""
-
-    # Agent tasks commonly express one batch as ``branch_id = 4, 5, 6, 7``.
-    # Capture the complete labelled list instead of keeping only its first id.
-    list_pattern = (
-        r"\bbranch[_\s-]*ids?\s*[:=]\s*\[?\s*"
-        r"(\d+(?:\s*[,,、/]\s*\d+)*)\s*\]?"
-    )
-    for match in re.finditer(list_pattern, source, flags=re.IGNORECASE):
-        for raw_value in re.findall(r"\d+", match.group(1)):
-            value = int(raw_value)
-            if value not in values:
-                values.append(value)
-
-    # Keep supporting prose that names branches individually.
-    for match in re.finditer(r"(?:方案|分支)\s*(\d+)", source, flags=re.IGNORECASE):
-        value = int(match.group(1))
-        if value not in values:
-            values.append(value)
-    return values
-
-
-def _branch_conclusions(
-    parsed: dict[str, Any], branch_ids: list[int]
-) -> list[dict[str, Any]]:
-    by_subject = {
-        str(item.get("subject") or ""): item
-        for item in parsed.get("itemConclusions") or []
-        if isinstance(item, dict)
-    }
-    return [
-        {
-            "branchId": branch_id,
-            "conclusion": (by_subject.get(f"方案 {branch_id}") or {}).get("conclusion"),
-            "summary": _item_summary(by_subject.get(f"方案 {branch_id}")),
-        }
-        for branch_id in branch_ids
-    ]
-
-
-def _item_summary(item: Any) -> str | None:
-    if not isinstance(item, dict):
-        return None
-    values = [
-        *(str(value) for value in item.get("achievements") or [] if value),
-        *(str(value) for value in item.get("problems") or [] if value),
-    ]
-    return ";".join(values[:2]) or None
-
-
-def _agent_task(event: dict[str, Any]) -> str | None:
-    value = event_input(event)
-    if isinstance(value, dict) and isinstance(value.get("task"), str):
-        return value["task"]
-    return None
-
-
-def _agent_summary(event: dict[str, Any]) -> str | None:
-    value = event_output(event)
-    if isinstance(value, dict) and isinstance(value.get("summary"), str):
-        return value["summary"]
-    return None

+ 0 - 593
visualization/backend/app/evaluation_report_parser.py

@@ -1,593 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from dataclasses import dataclass
-from typing import Any, Literal
-
-
-EvaluationKind = Literal["multipath", "overall", "unknown"]
-
-
-@dataclass(frozen=True)
-class _Section:
-    title: str
-    body: str
-
-
-class EvaluationReportParser:
-    """Parse evaluator Markdown into business-safe, deterministic fields.
-
-    The parser deliberately does not accept a build or round identifier. Runtime
-    metadata remains the only source for association; identifiers written inside
-    a report are treated as technical prose and ignored.
-    """
-
-    VERSION = 1
-
-    def parse(
-        self,
-        value: Any,
-        *,
-        kind: EvaluationKind = "unknown",
-    ) -> dict[str, Any]:
-        report = _report_text(value)
-        visible = _remove_code_fences(report)
-        sections = _sections(visible)
-        tables = _tables(visible)
-        branch_evaluations = _branch_evaluations(sections)
-        comparison_rows = _comparison_rows(tables)
-
-        subjects = _subjects(sections)
-        criteria = _collect_sections(
-            sections,
-            ("评审标准", "评估标准", "评价标准", "评审维度", "评估维度", "评价维度", "维度评估", "领域评估维度"),
-        )
-        achievements = _collect_positive_sections(
-            sections,
-            ("已达成", "达成情况", "目标达成", "目标推进", "完成情况", "通过项", "达成项", "主要优点"),
-        )
-        problems = _problem_sections(sections)
-        if not problems:
-            inline_problem = _labeled_value(
-                visible,
-                ("问题详情", "主要问题", "未通过项", "遗留问题", "存疑项", "问题"),
-            )
-            if inline_problem:
-                problems.append({"summary": inline_problem})
-        item_conclusions = _item_conclusions(sections)
-        if branch_evaluations:
-            item_conclusions = [
-                {
-                    "subject": item["subject"],
-                    "conclusion": item.get("conclusion"),
-                    "achievements": item.get("achievements") or [],
-                    "problems": item.get("problems") or [],
-                }
-                for item in branch_evaluations
-            ]
-
-        for table in tables:
-            _merge_table(
-                table,
-                criteria=criteria,
-                item_conclusions=item_conclusions,
-                achievements=achievements,
-                problems=problems,
-            )
-
-        problems = [
-            item
-            for item in problems
-            if not _means_no_problem(str(item.get("summary") or ""))
-        ]
-        for item in item_conclusions:
-            item["problems"] = [
-                problem
-                for problem in item.get("problems") or []
-                if not _means_no_problem(str(problem))
-            ]
-
-        conclusion = _labeled_value(
-            visible,
-            ("整体结论", "总结论", "评审结论", "评估结论"),
-        )
-        recommendation = _section_value(
-            sections,
-            ("评审建议", "对比与建议", "跨方案对比", "方案对比", "采纳建议", "组合建议", "建议采用", "建议合入", "推荐"),
-        )
-        next_goal = _section_value(
-            sections,
-            ("下一步引领目标", "下一轮引领目标", "下一轮目标", "下一步目标", "引领目标"),
-        )
-
-        # Some reports use bold inline labels without a Markdown heading.
-        if not recommendation:
-            recommendation = _labeled_value(
-                visible,
-                ("采纳建议", "组合建议", "评审建议", "建议采用", "建议合入", "推荐"),
-            )
-        if not next_goal:
-            next_goal = _labeled_value(
-                visible,
-                ("下一步引领目标", "下一轮引领目标", "下一轮目标", "下一步目标", "引领目标"),
-            )
-
-        result = {
-            "parserVersion": self.VERSION,
-            "kind": kind,
-            "subjects": _unique(subjects),
-            "criteria": _unique(criteria),
-            "itemConclusions": _unique_dicts(item_conclusions),
-            "achievements": _unique(achievements),
-            "problems": _unique_dicts(problems),
-            "conclusion": _business_value(conclusion),
-            "recommendation": _business_value(recommendation),
-            "nextGoal": _business_value(next_goal),
-            "notices": [],
-        }
-        if branch_evaluations:
-            result["branchEvaluations"] = branch_evaluations
-        if comparison_rows:
-            result["comparisonRows"] = comparison_rows
-        if not any(
-            result[key]
-            for key in (
-                "subjects",
-                "criteria",
-                "itemConclusions",
-                "achievements",
-                "problems",
-                "conclusion",
-                "recommendation",
-                "nextGoal",
-            )
-        ):
-            result["notices"].append("unparsed-report")
-        return result
-
-
-def _means_no_problem(value: str) -> bool:
-    sentences = [
-        re.sub(r"\s+", "", item)
-        for item in re.split(r"[。!?!?;;]+", value)
-        if item.strip()
-    ]
-    if not sentences:
-        return False
-    marker = re.fullmatch(
-        r"(?:无|没有|无(?:显著|明显)?(?:质量)?问题|无硬伤|无未通过项|未发现问题|未发现硬伤)",
-        sentences[0],
-    )
-    if not marker:
-        return False
-    remainder = "".join(sentences[1:])
-    return not re.search(r"(?:但|不过|仍|需|建议|风险|问题|不足|缺失|待|未)", remainder)
-
-
-def _report_text(value: Any) -> str:
-    if value is None:
-        return ""
-    if isinstance(value, dict):
-        summary = value.get("summary")
-        return str(summary) if isinstance(summary, str) else ""
-    text = str(value).replace("\r\n", "\n").replace("\r", "\n").strip()
-    if text.startswith("{"):
-        try:
-            payload = json.loads(text)
-        except (TypeError, ValueError):
-            return text
-        if isinstance(payload, dict) and isinstance(payload.get("summary"), str):
-            return payload["summary"]
-    return text.replace("\\n", "\n")
-
-
-def _remove_code_fences(text: str) -> str:
-    return re.sub(r"(?ms)^\s*```[^\n]*\n.*?^\s*```\s*$", "", text)
-
-
-def _sections(text: str) -> list[_Section]:
-    lines = text.splitlines()
-    sections: list[_Section] = []
-    current_title = ""
-    body: list[str] = []
-
-    def flush() -> None:
-        nonlocal body
-        if current_title or any(line.strip() for line in body):
-            sections.append(_Section(_clean_heading(current_title), "\n".join(body).strip()))
-        body = []
-
-    for line in lines:
-        heading = re.match(r"^\s*#{1,6}\s+(.+?)\s*#*\s*$", line)
-        if heading:
-            flush()
-            current_title = heading.group(1)
-            continue
-        bold_heading = re.match(
-            r"^\s*(?:[-*+]\s*)?\*\*([^*\n]{2,40})\*\*\s*(?:[((][^))\n]{0,40}[))])?\s*[::]?\s*$",
-            line,
-        )
-        if bold_heading:
-            flush()
-            current_title = bold_heading.group(1)
-            continue
-        body.append(line)
-    flush()
-    return sections
-
-
-def _tables(text: str) -> list[list[dict[str, str]]]:
-    lines = text.splitlines()
-    tables: list[list[dict[str, str]]] = []
-    index = 0
-    while index + 1 < len(lines):
-        headers = _table_cells(lines[index])
-        separator = _table_cells(lines[index + 1])
-        if not headers or not separator or not all(re.fullmatch(r":?-{3,}:?", cell) for cell in separator):
-            index += 1
-            continue
-        rows: list[dict[str, str]] = []
-        index += 2
-        while index < len(lines):
-            cells = _table_cells(lines[index])
-            if not cells:
-                break
-            cells += [""] * max(0, len(headers) - len(cells))
-            rows.append(dict(zip((_plain(cell) for cell in headers), cells)))
-            index += 1
-        if rows:
-            tables.append(rows)
-    return tables
-
-
-def _table_cells(line: str) -> list[str]:
-    stripped = line.strip()
-    if "|" not in stripped:
-        return []
-    return [cell.strip() for cell in stripped.strip("|").split("|")]
-
-
-def _subjects(sections: list[_Section]) -> list[str]:
-    values: list[str] = []
-    for section in sections:
-        title = _business_value(section.title)
-        if not title:
-            continue
-        match = re.search(r"(?:分支评估|方案评估|候选评审)\s*(?:方案\s*)?(\d+)", title)
-        if not match:
-            match = re.search(r"方案\s*(\d+)\b", title)
-        if match:
-            values.append(f"方案 {match.group(1)}")
-        elif re.search(r"主脚本|整体评审|整体评估", title):
-            values.append("当前主脚本")
-    return values
-
-
-def _collect_sections(sections: list[_Section], labels: tuple[str, ...]) -> list[str]:
-    values: list[str] = []
-    for section in sections:
-        if not _matches_label(section.title, labels):
-            continue
-        values.extend(_business_items(section.body))
-    return values
-
-
-def _collect_positive_sections(sections: list[_Section], labels: tuple[str, ...]) -> list[str]:
-    values: list[str] = []
-    for section in sections:
-        if _is_problem_title(section.title) or not _matches_label(section.title, labels):
-            continue
-        values.extend(_business_items(section.body))
-    return values
-
-
-def _branch_evaluations(sections: list[_Section]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    current: dict[str, Any] | None = None
-
-    for section in sections:
-        subject = _branch_section_subject(section.title)
-        if subject:
-            current = {
-                "subject": subject,
-                "conclusion": _business_value(
-                    _labeled_value(section.body, ("该支结论", "评审结论", "结论"))
-                ),
-                "achievements": [],
-                "problems": [],
-                "evidence": _business_value(_labeled_value(section.body, ("有据扎实度",))),
-            }
-            inline_achievement = _labeled_value(section.body, ("目标推进", "已达成", "主要优点"))
-            inline_problem = _labeled_value(section.body, ("未通过项", "部分通过项", "主要问题", "问题", "存疑项"))
-            if inline_achievement:
-                current["achievements"].append(inline_achievement)
-            if inline_problem and not _means_no_problem(inline_problem):
-                current["problems"].append(inline_problem)
-            values.append(current)
-            continue
-
-        if current is None:
-            continue
-        if _is_comparison_section(section.title):
-            current = None
-            continue
-
-        evidence = _labeled_value(section.body, ("有据扎实度",))
-        if evidence:
-            current["evidence"] = _business_value(evidence)
-        body = _without_labeled_lines(section.body, ("有据扎实度",))
-        if _is_problem_title(section.title):
-            current["problems"].extend(
-                item for item in _business_items(body) if not _means_no_problem(item)
-            )
-        elif _is_positive_section_title(section.title):
-            current["achievements"].extend(_business_items(body))
-
-    for item in values:
-        item["achievements"] = _unique(item.get("achievements") or [])
-        item["problems"] = _unique(item.get("problems") or [])
-    return values
-
-
-def _comparison_rows(tables: list[list[dict[str, str]]]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    for rows in tables:
-        if not rows:
-            continue
-        headers = list(rows[0])
-        criterion_key = _first_header(headers, ("维度", "标准"))
-        candidate_keys = [
-            header for header in headers
-            if re.search(r"(?:branch|方案|分支)\s*#?\s*\d+", header, re.I)
-        ]
-        basis_key = _first_header(headers, ("差异依据", "比较依据", "依据"))
-        if not criterion_key or not candidate_keys:
-            continue
-        for row in rows:
-            criterion = _business_value(row.get(criterion_key))
-            if not criterion:
-                continue
-            values.append(
-                {
-                    "criterion": criterion,
-                    "candidates": [
-                        {
-                            "subject": _business_value(header),
-                            "value": _business_value(row.get(header)),
-                        }
-                        for header in candidate_keys
-                        if _business_value(row.get(header))
-                    ],
-                    "basis": _business_value(row.get(basis_key)) if basis_key else None,
-                }
-            )
-    return values
-
-
-def _is_positive_section_title(value: str) -> bool:
-    return not _is_problem_title(value) and _matches_label(
-        value,
-        ("已达成", "达成情况", "目标达成", "目标推进", "完成情况", "通过项", "达成项", "主要优点"),
-    )
-
-
-def _is_problem_title(value: str) -> bool:
-    normalized = _clean_heading(value)
-    return any(label in normalized for label in ("未通过", "部分通过项", "问题", "存疑", "遗留", "风险"))
-
-
-def _is_comparison_section(value: str) -> bool:
-    normalized = _clean_heading(value)
-    return any(label in normalized for label in ("对比与建议", "跨方案对比", "方案对比", "综合建议", "执行统计"))
-
-
-def _without_labeled_lines(text: str, labels: tuple[str, ...]) -> str:
-    pattern = "|".join(re.escape(label) for label in labels)
-    return "\n".join(
-        line for line in text.splitlines()
-        if not re.match(rf"^\s*(?:[-*+]\s*)?(?:\*\*)?(?:{pattern})(?:\*\*)?\s*[::]", line)
-    )
-
-
-def _problem_sections(sections: list[_Section]) -> list[dict[str, str]]:
-    values: list[dict[str, str]] = []
-    for section in sections:
-        if not _is_problem_title(section.title):
-            continue
-        body = _without_labeled_lines(section.body, ("有据扎实度",))
-        for item in _business_items(body):
-            problem: dict[str, str] = {"summary": item}
-            severity = _severity(item)
-            if severity:
-                problem["severity"] = severity
-            values.append(problem)
-    return values
-
-
-def _item_conclusions(sections: list[_Section]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    for section in sections:
-        subject = _subject_from_title(section.title)
-        if not subject:
-            continue
-        conclusion = _labeled_value(section.body, ("该支结论", "评审结论", "结论"))
-        achievements = _inline_or_section_items(section.body, ("目标推进", "已达成", "主要优点"))
-        problems = _inline_or_section_items(section.body, ("未通过项", "主要问题", "问题"))
-        if conclusion or achievements or problems:
-            values.append(
-                {
-                    "subject": subject,
-                    "conclusion": _business_value(conclusion),
-                    "achievements": _unique(achievements),
-                    "problems": _unique(problems),
-                }
-            )
-    return values
-
-
-def _merge_table(
-    rows: list[dict[str, str]],
-    *,
-    criteria: list[str],
-    item_conclusions: list[dict[str, Any]],
-    achievements: list[str],
-    problems: list[dict[str, str]],
-) -> None:
-    if not rows:
-        return
-    headers = list(rows[0])
-    subject_key = _first_header(headers, ("方案", "分支", "对象", "候选", "维度", "标准"))
-    conclusion_key = _first_header(headers, ("结论", "结果", "判断"))
-    achievement_key = _first_header(headers, ("已达成", "达成", "优点", "通过项"))
-    problem_key = _first_header(headers, ("问题", "未通过", "遗留", "风险"))
-
-    for row in rows:
-        subject = _business_value(row.get(subject_key)) if subject_key else None
-        conclusion = _business_value(row.get(conclusion_key)) if conclusion_key else None
-        row_achievements = _business_items(row.get(achievement_key, "")) if achievement_key else []
-        row_problems = _business_items(row.get(problem_key, "")) if problem_key else []
-
-        if subject_key and any(term in subject_key for term in ("维度", "标准")) and subject:
-            criteria.append(subject)
-        if subject and conclusion:
-            item_conclusions.append(
-                {
-                    "subject": subject,
-                    "conclusion": conclusion,
-                    "achievements": row_achievements,
-                    "problems": row_problems,
-                }
-            )
-        achievements.extend(row_achievements)
-        for item in row_problems:
-            problem: dict[str, str] = {"summary": item}
-            severity = _severity(item)
-            if severity:
-                problem["severity"] = severity
-            problems.append(problem)
-
-
-def _section_value(sections: list[_Section], labels: tuple[str, ...]) -> str | None:
-    for section in sections:
-        if _matches_label(section.title, labels):
-            items = _business_items(section.body)
-            return ";".join(items) if items else None
-    return None
-
-
-def _labeled_value(text: str, labels: tuple[str, ...]) -> str | None:
-    if not text:
-        return None
-    label_pattern = "|".join(re.escape(label) for label in labels)
-    match = re.search(
-        rf"(?im)^\s*(?:#{{1,6}}\s+)?(?:[-*+]\s*)?(?:\*\*)?(?:{label_pattern})(?:\*\*)?\s*[::]\s*(.+?)\s*$",
-        text,
-    )
-    return _business_value(match.group(1)) if match else None
-
-
-def _inline_or_section_items(text: str, labels: tuple[str, ...]) -> list[str]:
-    value = _labeled_value(text, labels)
-    return [value] if value else []
-
-
-def _business_items(text: str) -> list[str]:
-    values: list[str] = []
-    for paragraph in re.split(r"\n\s*\n", text or ""):
-        lines = paragraph.splitlines()
-        for line in lines:
-            if re.match(r"^\s*\|.*\|\s*$", line):
-                continue
-            value = re.sub(r"^\s*(?:[-*+]\s+|\d+[.)、]\s*)", "", line).strip()
-            value = _business_value(value)
-            if value:
-                values.append(value)
-    return values
-
-
-def _business_value(value: Any) -> str | None:
-    text = _plain(str(value or ""))
-    if not text:
-        return None
-    # Remove only named technical assignments. Never remove bare numbers or
-    # generic "ID" tokens: business content such as "领域信息 31/32" must survive.
-    text = re.sub(r"(?i)\bscript_build_id\s*[=::]\s*\d+\s*[·|,,;;]?", "", text)
-    text = re.sub(r"(?i)\bround(?:_index)?\s*[=::]\s*\d+\s*[·|,,;;]?", "", text)
-    text = re.sub(r"(?i)\b(?:paragraph|element|post)_id\s*[=::]\s*[^\s::,,;;))]+\s*[·|,,;;]?", "", text)
-    text = re.sub(r"(?i)\bbranch_id\s*[=::]\s*(\d+)", r"方案 \1", text)
-    text = re.sub(r"(?i)\bbranch\s*#?\s*(\d+)\b", r"方案 \1", text)
-    text = re.sub(r"^(?:D|P)\d+\s*[::|·-]?\s*(?=\D)", "", text, flags=re.I)
-    text = re.sub(r"(?i)\b(?:action|target|path_type|dimension_pass)\s*[=::]\s*[^,,;;]+", "", text)
-    text = re.sub(r"^\s*[::·|,,;;-]+\s*", "", text)
-    text = re.sub(r"\s+([,。;!?、:,.])", r"\1", text)
-    text = re.sub(r"\s{2,}", " ", text).strip(" ::,,;;|-_")
-    return text or None
-
-
-def _plain(text: str) -> str:
-    text = text.replace("\\n", "\n")
-    text = re.sub(r"`([^`]*)`", r"\1", text)
-    text = re.sub(r"[*#]", "", text)
-    return text.strip()
-
-
-def _clean_heading(value: str) -> str:
-    return _plain(value).strip(" ::")
-
-
-def _matches_label(value: str, labels: tuple[str, ...]) -> bool:
-    normalized = _clean_heading(value)
-    return any(label in normalized for label in labels)
-
-
-def _subject_from_title(title: str) -> str | None:
-    cleaned = _business_value(title)
-    if not cleaned:
-        return None
-    match = re.search(r"(?:分支评估|方案评估|候选评审)?\s*方案\s*(\d+)\b", cleaned)
-    if match:
-        return f"方案 {match.group(1)}"
-    return None
-
-
-def _branch_section_subject(title: str) -> str | None:
-    cleaned = _business_value(title)
-    if not cleaned or not re.search(r"分支评估|方案评估|候选评审", cleaned):
-        return None
-    return _subject_from_title(title)
-
-
-def _first_header(headers: list[str], labels: tuple[str, ...]) -> str | None:
-    return next((header for header in headers if any(label in header for label in labels)), None)
-
-
-def _severity(value: str) -> str | None:
-    match = re.search(r"(?:严重程度|级别|等级)\s*[::]\s*(严重|高|中|低)", value)
-    if match:
-        return match.group(1)
-    for label in ("严重", "高", "中", "低"):
-        if re.match(rf"^(?:[【\[]{label}[】\]]|{label}\s*[::])", value):
-            return label
-    return None
-
-
-def _unique(values: list[str]) -> list[str]:
-    seen: set[str] = set()
-    result: list[str] = []
-    for value in values:
-        if value and value not in seen:
-            seen.add(value)
-            result.append(value)
-    return result
-
-
-def _unique_dicts(values: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    seen: set[str] = set()
-    result: list[dict[str, Any]] = []
-    for value in values:
-        marker = json.dumps(value, ensure_ascii=False, sort_keys=True)
-        if marker not in seen:
-            seen.add(marker)
-            result.append(value)
-    return result

+ 0 - 1975
visualization/backend/app/execution_builder.py

@@ -1,1975 +0,0 @@
-from __future__ import annotations
-
-import re
-from collections import Counter, defaultdict
-from datetime import datetime, timezone
-from typing import Any
-from zoneinfo import ZoneInfo
-
-from .business_detail_text import branch_task_sections, branch_task_text, business_text
-from .decision_projection import AgentDecisionProjector
-from .main_decision_text import (
-    parse_goal_text,
-    parse_objective_text,
-    plan_mode_label,
-    plan_path_summary,
-    preview as decision_preview,
-)
-from .runtime_event_projection import RuntimeEventProjector
-
-
-_DECISIONS = AgentDecisionProjector()
-
-
-TERMINAL_STATUSES = {
-    "success",
-    "partial",
-    "failed",
-    "stopped",
-    "completed",
-    "cancelled",
-    "cancel",
-    "cancle",
-}
-
-
-class ExecutionViewBuilder:
-    """Deterministic V8 business narrative projector.
-
-    The projector consumes plain dictionaries and has no database, HTTP or
-    framework dependencies.  Business rows win over runtime events; events may
-    only fill clearly labelled evaluation/activity detail.
-    """
-
-    def __init__(self, event_projector: RuntimeEventProjector | None = None):
-        self._event_projector = event_projector or RuntimeEventProjector()
-
-    def from_bundle(self, bundle: dict[str, Any]) -> dict[str, Any]:
-        record = bundle.get("record") or {}
-        raw_rounds = sorted(
-            bundle.get("rounds") or [],
-            key=lambda item: (_integer(item.get("round_index")) or 0, _integer(item.get("id")) or 0),
-        )
-        raw_branches = sorted(
-            bundle.get("branches") or [],
-            key=lambda item: (
-                _integer(item.get("round_index")) or 0,
-                _integer(item.get("branch_id")) or 0,
-            ),
-        )
-        data_decisions = [
-            item
-            for item in bundle.get("dataDecisions") or []
-            if (_integer(item.get("branch_id")) or 0) > 0
-        ]
-        multipath_decisions = bundle.get("multipathDecisions") or []
-        domain_info = bundle.get("domainInfo") or []
-        events = bundle.get("events") or []
-
-        valid_rounds = {
-            value
-            for value in (_integer(item.get("round_index")) for item in raw_rounds)
-            if value is not None
-        }
-        valid_branches = {
-            value
-            for value in (_integer(item.get("branch_id")) for item in raw_branches)
-            if value is not None
-        }
-        captured_at = datetime.now(timezone.utc)
-        runtime = self._event_projector.project(
-            events,
-            valid_rounds=valid_rounds,
-            valid_branches=valid_branches,
-            captured_at=captured_at,
-            run_status=str(record.get("status") or "unknown"),
-            script_direction=record.get("script_direction"),
-            rounds=raw_rounds,
-            multipath_decisions=multipath_decisions,
-        )
-        implementer_tasks = _implementer_tasks_by_branch(events)
-
-        decisions_by_branch: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        for item in data_decisions:
-            branch_id = _integer(item.get("branch_id"))
-            if branch_id is not None:
-                decisions_by_branch[branch_id].append(item)
-        domain_by_branch: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        domain_by_round: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        for item in domain_info:
-            branch_id = _integer(item.get("branch_id"))
-            round_index = _integer(item.get("round_index"))
-            if branch_id is not None:
-                domain_by_branch[branch_id].append(item)
-            if round_index is not None:
-                domain_by_round[round_index].append(item)
-        multipath_by_round: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        unassigned_business: list[dict[str, Any]] = []
-        for item in multipath_decisions:
-            round_index = _integer(item.get("round_index"))
-            if round_index in valid_rounds:
-                multipath_by_round[round_index].append(item)
-            else:
-                unassigned_business.append(
-                    {
-                        "type": "multipath-decision",
-                        "id": item.get("id"),
-                        "reason": "没有可确认的所属轮次",
-                    }
-                )
-
-        projected_rounds: list[dict[str, Any]] = []
-        for index, raw_round in enumerate(raw_rounds):
-            round_index = _integer(raw_round.get("round_index")) or index + 1
-            branch_rows = [
-                item
-                for item in raw_branches
-                if _integer(item.get("round_index")) == round_index
-            ]
-            next_round = raw_rounds[index + 1] if index + 1 < len(raw_rounds) else None
-            projected_rounds.append(
-                self._round_story(
-                    record,
-                    raw_round,
-                    branch_rows,
-                    decisions_by_branch,
-                    domain_by_branch,
-                    domain_by_round.get(round_index, []),
-                    multipath_by_round.get(round_index, []),
-                    runtime,
-                    implementer_tasks,
-                    next_round,
-                )
-            )
-
-        data_shape = _data_shape(
-            raw_branches, multipath_decisions, domain_info, events
-        )
-        warnings = _warnings(
-            data_shape=data_shape,
-            ignored_legacy_count=_integer(
-                bundle.get("ignoredLegacyDataDecisionCount")
-            )
-            or 0,
-            rounds=projected_rounds,
-            runtime_unassigned=runtime.get("unassigned") or [],
-            business_unassigned=unassigned_business,
-        )
-        artifact = bundle.get("currentArtifact") or {}
-        current_round = projected_rounds[-1]["roundIndex"] if projected_rounds else None
-        view = {
-            "schemaVersion": "8",
-            "capturedAt": captured_at.isoformat(),
-            "dataShape": data_shape,
-            "header": _header(record, current_round),
-            "planning": _planning_analysis(
-                (runtime.get("mainAgentDecisions") or {}).get("planningAnalysis"),
-            ),
-            "objective": _objective(
-                record,
-                (runtime.get("mainAgentDecisions") or {}).get("objective"),
-            ),
-            "rounds": projected_rounds,
-            "finalResult": _final_result(record, projected_rounds, artifact),
-            "unassigned": {
-                "runtimeEvents": runtime.get("unassigned") or [],
-                "businessRecords": unassigned_business,
-            },
-            "warnings": warnings,
-        }
-        if view["planning"] is None:
-            view.pop("planning")
-        return view
-
-    def _round_story(
-        self,
-        record: dict[str, Any],
-        raw_round: dict[str, Any],
-        branch_rows: list[dict[str, Any]],
-        decisions_by_branch: dict[int, list[dict[str, Any]]],
-        domain_by_branch: dict[int, list[dict[str, Any]]],
-        round_domain_info: list[dict[str, Any]],
-        multipath_rows: list[dict[str, Any]],
-        runtime: dict[str, Any],
-        implementer_tasks: dict[int, str],
-        next_round: dict[str, Any] | None,
-    ) -> dict[str, Any]:
-        round_index = _integer(raw_round.get("round_index")) or 0
-        main_decisions = runtime.get("mainAgentDecisions") or {}
-        goal_context = (main_decisions.get("roundGoalsByRound") or {}).get(round_index)
-        plan_context = (main_decisions.get("implementationPlansByRound") or {}).get(
-            round_index
-        )
-        plan = _plan_projection(
-            raw_round,
-            plan_context,
-        )
-        branches = [
-            _branch_story(
-                round_index,
-                row,
-                decisions_by_branch.get(_integer(row.get("branch_id")) or 0, []),
-                domain_by_branch.get(_integer(row.get("branch_id")) or 0, []),
-                runtime.get("retrievalStagesByBranch", {}).get(
-                    _integer(row.get("branch_id")) or 0
-                ),
-                runtime.get("creativeByBranch", {}).get(
-                    _integer(row.get("branch_id")) or 0
-                ),
-                implementer_tasks.get(_integer(row.get("branch_id")) or 0),
-                str(record.get("status") or "unknown"),
-            )
-            for row in branch_rows
-        ]
-        branch_ids = {branch["branchId"] for branch in branches}
-        decision_batches = [
-            _decision_batch(round_index, row, branch_ids, branches)
-            for row in sorted(multipath_rows, key=_record_order)
-        ]
-        convergence_batches, unassigned_reviews = _convergence_batches(
-            round_index,
-            runtime.get("multipathReviewsByRound", {}).get(round_index, []),
-            decision_batches,
-            str(record.get("status") or "unknown"),
-        )
-        if not convergence_batches and branches:
-            convergence_batches = [
-                _convergence_batch(
-                    round_index,
-                    None,
-                    None,
-                    "records-missing",
-                    str(record.get("status") or "unknown"),
-                    branch_ids=sorted(branch_ids),
-                )
-            ]
-        _annotate_unscoped_round_reviews(
-            convergence_batches,
-            runtime.get("unassigned") or [],
-            round_index,
-        )
-        if unassigned_reviews:
-            runtime.setdefault("unassigned", []).extend(unassigned_reviews)
-        overall_evaluation = _round_evaluation(
-            round_index,
-            runtime.get("overallEvaluationsByRound", {}).get(round_index, []),
-            str(record.get("status") or "unknown"),
-        )
-        state = _round_state(record, raw_round, overall_evaluation)
-        result = _round_result(
-            record,
-            round_index,
-            branches,
-            round_domain_info,
-            state,
-            next_round,
-        )
-        goal_summary = _goal_summary(raw_round.get("goal"))
-        round_summary = _round_summary(
-            round_index=round_index,
-            goal=goal_summary,
-            plan=plan,
-            branches=branches,
-            batches=decision_batches,
-            domain_info=round_domain_info,
-            run_status=str(record.get("status") or "unknown"),
-        )
-        goal_parsed = parse_goal_text(raw_round.get("goal"))
-        goal_lines: list[dict[str, Any]] = []
-        focus_items = goal_parsed.get("focusItems") or []
-        if focus_items:
-            focus_summary = ";".join(str(item) for item in focus_items[:2])
-            if len(focus_items) > 2:
-                focus_summary += f";另有 {len(focus_items) - 2} 项,详情查看"
-            goal_lines.append(_line("focus", "本轮聚焦", focus_summary))
-        visible_inputs = _context_input_labels(goal_context)
-        if visible_inputs:
-            goal_lines.append(_line("visible-before", "形成前可见", " · ".join(visible_inputs)))
-        goal_decision = _direction_decision(
-            id=f"round:{round_index}:goal",
-            subtype="round-goal",
-            context=goal_context,
-            decision_items=(goal_context or {}).get("decisionItems")
-            or [goal_parsed["headline"]],
-            primary_label="要解决",
-            primary_value=goal_parsed["headline"],
-            secondary=goal_lines,
-            prompt_ref="prompt:main",
-        )
-        return {
-            "id": f"round:{round_index}",
-            "roundIndex": round_index,
-            "state": state,
-            "summary": round_summary,
-            "goal": _story(
-                id=f"round:{round_index}:goal",
-                role="goal",
-                kind="decision",
-                title="本轮目标",
-                label="要解决",
-                value=goal_parsed["headline"],
-                secondary=goal_lines,
-                detail_ref=f"round:{round_index}:goal",
-                source_notice=None if raw_round.get("goal") else "record-missing",
-                decision=goal_decision,
-            ),
-            "plan": plan,
-            "branches": branches,
-            "convergenceBatches": convergence_batches,
-            "overallEvaluation": overall_evaluation,
-            "result": result,
-            "detailRef": f"round:{round_index}",
-        }
-
-
-def _header(record: dict[str, Any], current_round: int | None) -> dict[str, Any]:
-    agent_config = record.get("agent_config")
-    model = None
-    if isinstance(agent_config, dict):
-        model = agent_config.get("model_name") or agent_config.get("model")
-    return {
-        "id": str(record.get("id") or ""),
-        "title": f"脚本构建 #{record.get('id')}",
-        "status": str(record.get("status") or "unknown"),
-        "summary": _plain(record.get("summary")),
-        "errorMessage": _plain(record.get("error_message")),
-        "model": model,
-        "totalCost": record.get("cost_usd"),
-        "currentRound": current_round,
-        "createdAt": record.get("start_time"),
-        "updatedAt": record.get("end_time"),
-        "direction": {
-            "currentValue": _plain(record.get("script_direction")),
-            "note": "这是数据库当前保存的创作方向。",
-        },
-    }
-
-
-def _objective(
-    record: dict[str, Any], decision_context: dict[str, Any] | None
-) -> dict[str, Any]:
-    raw_direction = record.get("script_direction")
-    direction = _plain(raw_direction)
-    # Section parsing needs the persisted Markdown line breaks. ``direction``
-    # is only the compact truthy/display fallback used by the surrounding view.
-    parsed = parse_objective_text(raw_direction)
-    secondary: list[dict[str, Any]] = []
-    input_labels = _context_input_labels(decision_context, relation="direct-read")
-    if input_labels:
-        secondary.append(
-            _line("inputs", "决策前读取", " · ".join(input_labels))
-        )
-    structure: list[str] = []
-    target_count = int(parsed.get("targetCount") or 0)
-    dimension_count = int(parsed.get("domainDimensionCount") or 0)
-    if target_count:
-        structure.append(f"{target_count} 个创作目标")
-    if dimension_count:
-        structure.append(f"{dimension_count} 个领域评估维度")
-    if structure:
-        secondary.append(_line("structure", "目标结构", " · ".join(structure)))
-    decision = _direction_decision(
-        id="run:objective",
-        subtype="objective",
-        context=decision_context,
-        decision_items=(decision_context or {}).get("decisionItems")
-        or ([parsed.get("headline")] if parsed.get("headline") else []),
-        primary_label="核心目标",
-        primary_value=parsed.get("headline"),
-        secondary=secondary,
-        prompt_ref="prompt:main",
-    )
-    return _story(
-        id="run:objective",
-        role="objective",
-        kind="decision" if direction else "missing",
-        title="创作目标",
-        label="核心目标",
-        value=parsed.get("headline") or "创作方向未记录",
-        secondary=secondary,
-        detail_ref="run:objective",
-        source_notice=None if direction else "record-missing",
-        decision=decision,
-    )
-
-
-def _planning_analysis(context: dict[str, Any] | None) -> dict[str, Any] | None:
-    if not context or context.get("completeness") != "complete" or not context.get("eventRef"):
-        return None
-    secondary = [
-        _line("plan", "执行计划", context.get("plan")),
-        _line("next", "下一步", context.get("action")),
-    ]
-    return _story(
-        id="run:planning-analysis",
-        role="planning-analysis",
-        kind="execution",
-        title="创作规划解析",
-        label="规划概要",
-        value=context.get("summary") or "主 Agent 已完成首次创作规划",
-        secondary=secondary,
-        detail_ref=context.get("eventRef"),
-        status="completed",
-    )
-
-
-def _plan_projection(
-    raw_round: dict[str, Any], decision_context: dict[str, Any] | None
-) -> dict[str, Any]:
-    round_index = _integer(raw_round.get("round_index")) or 0
-    paths = raw_round.get("multipath_plan")
-    if not isinstance(paths, list):
-        paths = []
-    raw_mode = _plain(raw_round.get("race_or_divide"))
-    mode = plan_mode_label(raw_mode, len(paths))
-    summary = (
-        f"{mode or '方式未记录'} · {len(paths)} 个方案"
-        if paths
-        else "多路规划未记录"
-    )
-    decision = _direction_decision(
-        id=f"round:{round_index}:plan",
-        subtype="implementation-plan",
-        context=decision_context,
-        decision_items=(decision_context or {}).get("decisionItems")
-        or [summary],
-        primary_label="实现方式",
-        primary_value=summary,
-        secondary=_plan_card_lines(paths, raw_round.get("plan_note")),
-        prompt_ref="prompt:main",
-    )
-    node = _story(
-        id=f"round:{round_index}:plan",
-        role="plan",
-        kind="decision" if paths else "missing",
-        title="实现规划",
-        label="实现方式",
-        value=summary,
-        secondary=_plan_card_lines(paths, raw_round.get("plan_note")),
-        detail_ref=f"round:{round_index}:plan",
-        source_notice=None if paths else "record-missing",
-        decision=decision,
-    )
-    return {
-        "mode": mode,
-        "rawMode": raw_mode,
-        "paths": paths,
-        "note": _plain(raw_round.get("plan_note")),
-        "currentOnly": True,
-        "runtimeRevisionCount": len((decision_context or {}).get("revisionRefs") or []),
-        "runtimeRevisionRefs": list((decision_context or {}).get("revisionRefs") or []),
-        "node": node,
-    }
-
-
-def _branch_story(
-    round_index: int,
-    row: dict[str, Any],
-    decisions: list[dict[str, Any]],
-    domain_info: list[dict[str, Any]],
-    retrieval_stage: dict[str, Any] | None,
-    creative_decision: dict[str, Any] | None,
-    implementer_task: str | None,
-    run_status: str,
-) -> dict[str, Any]:
-    branch_id = _integer(row.get("branch_id")) or 0
-    path_type = str(row.get("path_type") or "unknown")
-    if path_type not in {"内容", "领域信息"}:
-        path_type = "unknown"
-    task = (
-        branch_task_text(implementer_task)
-        or branch_task_text(row.get("impl_task"))
-        or business_text(row.get("target"))
-    )
-    retrieval_stage = retrieval_stage or _missing_retrieval_stage(
-        round_index, branch_id, run_status
-    )
-    projected_data_decisions = [
-        _DECISIONS.data_tradeoff(
-            {
-                **item,
-                "decision": business_text(item.get("decision")),
-                "reasoning": business_text(item.get("reasoning")),
-                "id": f"data-decision:{item.get('id')}",
-                "detailRef": f"data-decision:{item.get('id')}",
-                "promptRef": _implementer_prompt_ref(retrieval_stage),
-            }
-        )
-        for item in decisions
-    ]
-    for item, projection in zip(decisions, projected_data_decisions):
-        full_conclusion = business_text(item.get("decision"))
-        if full_conclusion:
-            projection.setdefault("card", {}).setdefault("primary", {})[
-                "value"
-            ] = full_conclusion
-    decision_nodes = [
-        _story(
-            id=f"data-decision:{item.get('id')}",
-            role="data-decision",
-            kind="decision",
-            title="数据取舍",
-            label="取舍结论",
-            value=projection["card"]["primary"]["value"],
-            secondary=projection["card"]["secondary"],
-            detail_ref=f"data-decision:{item.get('id')}",
-            decision=projection,
-        )
-        for item, projection in zip(decisions, projected_data_decisions)
-    ]
-    output = _branch_output(row, path_type, domain_info)
-    if creative_decision and output.get("type") == "script-artifact":
-        creative_decision = dict(creative_decision)
-        explicit_basis = [
-            {
-                "label": business_text(item.get("decision")) or "已记录一项数据取舍",
-                "value": business_text(item.get("reasoning")) or None,
-                "detailRef": f"data-decision:{item.get('id')}",
-            }
-            for item in decisions
-            if item.get("decision")
-        ]
-        creative_decision["artifactRef"] = output.get("snapshotRef")
-        creative_decision["body"] = {
-            **(creative_decision.get("body") or {}),
-            "output": output.get("summary"),
-            "explicitBasis": explicit_basis,
-            "candidateSnapshot": {
-                "snapshotRef": output.get("snapshotRef"),
-                "paragraphCount": output.get("paragraphCount"),
-                "elementCount": output.get("elementCount"),
-                "linkCount": output.get("linkCount"),
-                "exactness": _candidate_output_exactness(output),
-            },
-        }
-        detail = creative_decision.get("detail")
-        if isinstance(detail, dict):
-            blocks = [
-                block
-                for block in detail.get("blocks") or []
-                if not (
-                    isinstance(block, dict)
-                    and block.get("title") in {"实际产出", "产出的候选表"}
-                )
-            ]
-            if explicit_basis:
-                blocks.insert(
-                    0,
-                    {
-                        "type": "items",
-                        "title": "明确采用的数据取舍",
-                        "items": explicit_basis,
-                        "visualRole": "evidence",
-                        "presentation": "list",
-                        "collapsible": True,
-                    },
-                )
-            blocks.append(
-                {
-                    "type": "summary",
-                    "title": "产出的候选表",
-                    "value": output.get("summary"),
-                    "visualRole": "key-result",
-                    "presentation": "prose",
-                }
-            )
-            creative_decision["detail"] = {**detail, "blocks": blocks}
-        secondary = list((creative_decision.get("card") or {}).get("secondary") or [])
-        if explicit_basis:
-            secondary.append(_line("basis", "采用数据", f"{len(explicit_basis)} 项明确取舍"))
-        secondary.append(_line("output", "产出的候选表", output.get("summary")))
-        creative_decision["card"] = {
-            **(creative_decision.get("card") or {}),
-            "secondary": secondary[:3],
-        }
-        output["node"] = _story(
-            id=creative_decision["id"],
-            role="candidate-output",
-            kind="decision",
-            title="产生候选创作表",
-            label="创作处理",
-            value=creative_decision["card"]["primary"]["value"],
-            secondary=creative_decision["card"]["secondary"],
-            detail_ref=creative_decision["detailRef"],
-            artifact_ref=output.get("snapshotRef"),
-            has_changes=bool(output.get("snapshotRef")),
-            decision=creative_decision,
-        )
-    status = str(row.get("status") or "open")
-    disposition_context = _branch_disposition_context(status, run_status)
-    title = _plain(row.get("target")) or f"方案 {branch_id}"
-    return {
-        "id": f"round:{round_index}:branch:{branch_id}",
-        "branchId": branch_id,
-        "pathType": path_type,
-        "status": status,
-        "title": title,
-        "summary": {
-            "task": task or "实现任务未记录",
-            "output": output["summary"],
-        },
-        "task": _branch_task_story(
-            id=f"round:{round_index}:branch:{branch_id}:task",
-            task=task,
-            detail_ref=f"round:{round_index}:branch:{branch_id}:task",
-        ),
-        "retrievalStage": retrieval_stage,
-        "dataDecisions": [
-            {
-                "id": f"data-decision:{item.get('id')}",
-                "recordId": item.get("id"),
-                "decision": _plain(item.get("decision")),
-                "reasoning": _plain(item.get("reasoning")),
-                "sources": item.get("sources") or [],
-                "createdAt": item.get("created_at"),
-                "node": node,
-                "decisionProjection": projection,
-            }
-            for item, node, projection in zip(
-                decisions, decision_nodes, projected_data_decisions
-            )
-        ],
-        "output": output,
-        "outcome": {
-            "status": status,
-            "label": disposition_context["label"],
-            "reasoning": _plain(row.get("decision_reasoning")),
-            "sourceNotice": disposition_context["sourceNotice"],
-        },
-        "detailRef": f"round:{round_index}:branch:{branch_id}",
-    }
-
-
-def _branch_output(
-    row: dict[str, Any], path_type: str, domain_info: list[dict[str, Any]]
-) -> dict[str, Any]:
-    branch_id = _integer(row.get("branch_id")) or 0
-    round_index = _integer(row.get("round_index")) or 0
-    if path_type == "领域信息":
-        facts = [
-            item
-            for item in domain_info
-            if _integer(item.get("branch_id")) == branch_id
-            and _integer(item.get("round_index")) in {None, round_index}
-        ]
-        summary = (
-            f"新增 {len(facts)} 条已核实领域事实"
-            if facts
-            else "已查询完成,本方案没有新增领域事实"
-        )
-        node = _story(
-            id=f"round:{round_index}:branch:{branch_id}:output",
-            role="domain-output",
-            kind="result",
-            title="领域事实",
-            label="产出",
-            value=summary,
-            detail_ref=f"round:{round_index}:branch:{branch_id}:output",
-            has_changes=bool(facts),
-            source_notice=None,
-        )
-        return {
-            "type": "domain-info",
-            "summary": summary,
-            "factCount": len(facts),
-            "factRefs": [f"domain-info:{item.get('id')}" for item in facts],
-            "node": node,
-        }
-
-    candidate = row.get("candidate_snapshot") or {}
-    snapshot = candidate.get("snapshot") if isinstance(candidate, dict) else None
-    counts = _snapshot_counts(snapshot)
-    if any(counts.values()):
-        summary = (
-            f"候选脚本包含 {counts['paragraphs']} 个段落、"
-            f"{counts['elements']} 个元素"
-        )
-    elif row.get("self_assessment"):
-        assessment = business_text(row.get("self_assessment"))
-        first_item = next(
-            (line.strip(" -*") for line in assessment.splitlines() if line.strip()),
-            assessment,
-        )
-        summary = f"实现 Agent 自评:{first_item}"
-    else:
-        summary = "候选脚本内容未记录"
-    has_output = bool(snapshot) or bool(row.get("self_assessment"))
-    node = _story(
-        id=f"round:{round_index}:branch:{branch_id}:output",
-        role="candidate-output",
-        kind="result" if has_output else "missing",
-        title="候选产出",
-        label="产出",
-        value=summary,
-        detail_ref=f"round:{round_index}:branch:{branch_id}:output",
-        has_changes=bool(snapshot),
-        source_notice=None if has_output else "record-missing",
-        artifact_ref=f"artifact:branch:{branch_id}" if snapshot else None,
-    )
-    return {
-        "type": "script-artifact" if has_output else "missing",
-        "summary": summary,
-        "snapshotRef": f"artifact:branch:{branch_id}" if snapshot else None,
-        "paragraphCount": counts["paragraphs"],
-        "elementCount": counts["elements"],
-        "linkCount": counts["links"],
-        "accuracy": {
-            "historical": candidate.get("historicalAccuracy")
-            if isinstance(candidate, dict)
-            else "unknown",
-            "currentProjection": candidate.get("currentProjectionAccuracy")
-            if isinstance(candidate, dict)
-            else "unknown",
-        },
-        "node": node,
-    }
-
-
-def _branch_task_story(
-    *, id: str, task: str | None, detail_ref: str
-) -> dict[str, Any]:
-    if not task:
-        return _story(
-            id=id,
-            role="branch-task",
-            kind="missing",
-            title="实现任务",
-            label="实现任务",
-            value="实现任务未记录",
-            detail_ref=detail_ref,
-            source_notice="record-missing",
-        )
-    sections = branch_task_sections(task)
-    preferred = next(
-        (
-            item
-            for kind in ("scope", "method", "goal", "notes")
-            for item in sections
-            if item.get("kind") == kind and item.get("content")
-        ),
-        None,
-    )
-    primary = (preferred or {}).get("content") or task
-    secondary = [
-        _line(str(item.get("kind") or index), item.get("title") or "任务说明", item.get("content"))
-        for index, item in enumerate(sections, 1)
-        if item is not preferred and item.get("content")
-    ][:3]
-    node = _story(
-        id=id,
-        role="branch-task",
-        kind="execution",
-        title="实现任务",
-        label=(preferred or {}).get("title") or "实现任务",
-        value=primary,
-        secondary=secondary,
-        detail_ref=detail_ref,
-    )
-    node["taskStructure"] = {
-        "sectionCount": len(sections),
-        "sectionKinds": [item.get("kind") for item in sections],
-    }
-    return node
-
-
-def _candidate_output_exactness(output: dict[str, Any]) -> str:
-    historical = (output.get("accuracy") or {}).get("historical")
-    if historical == "exact":
-        return "historical-snapshot"
-    return "current-only" if output.get("snapshotRef") else "unknown"
-
-
-def _missing_retrieval_stage(
-    round_index: int, branch_id: int, run_status: str
-) -> dict[str, Any]:
-    running = str(run_status or "").lower() not in TERMINAL_STATUSES
-    return {
-        "id": f"round:{round_index}:branch:{branch_id}:retrieval",
-        "roundIndex": round_index,
-        "branchId": branch_id,
-        "implementerEventId": None,
-        "observedMode": "unknown",
-        "waves": [],
-        "directToolGroups": [],
-        "agentRuns": [],
-        "status": "running" if running else "missing",
-        "startedAt": None,
-        "endedAt": None,
-        "detailRef": f"round:{round_index}:branch:{branch_id}:retrieval",
-        "sourceNotice": None if running else "record-missing",
-    }
-
-
-def _decision_batch(
-    round_index: int,
-    row: dict[str, Any],
-    valid_branch_ids: set[int],
-    branches: list[dict[str, Any]],
-) -> dict[str, Any]:
-    branch_ids = _ids(row.get("branch_ids"))
-    missing = [branch_id for branch_id in branch_ids if branch_id not in valid_branch_ids]
-    record_id = row.get("id")
-    decision = _plain(row.get("decision")) or "决策内容未记录"
-    display_decision = _business_terms(decision)
-    secondary = [
-        _line(
-            "branches",
-            "比较方案",
-            "、".join(f"方案 {branch_id}" for branch_id in branch_ids)
-            or "涉及方案未记录",
-        )
-    ]
-    if row.get("reasoning"):
-        secondary.append(
-            _line("reason", "理由", _preview(row.get("reasoning"), 120))
-        )
-    branch_by_id = {branch["branchId"]: branch for branch in branches}
-    outcomes = [
-        {
-            "branchId": branch_id,
-            "status": branch_by_id.get(branch_id, {}).get("outcome", {}).get("status", "unknown"),
-            "label": branch_by_id.get(branch_id, {}).get("outcome", {}).get("label", "状态未记录"),
-            "reasoning": branch_by_id.get(branch_id, {}).get("outcome", {}).get("reasoning"),
-        }
-        for branch_id in branch_ids
-    ]
-    if outcomes:
-        secondary.insert(
-            1,
-            _line(
-                "outcomes",
-                "各方案结果",
-                ";".join(
-                    f"方案 {item['branchId']} · {item['label']}" for item in outcomes
-                ),
-            ),
-        )
-    projection = _DECISIONS.multipath_tradeoff(
-        {
-            "id": f"multipath-decision:{record_id}",
-            "detailRef": f"multipath-decision:{record_id}",
-            "promptRef": "prompt:main",
-            "branchIds": branch_ids,
-            "scopeItems": [f"方案 {branch_id}" for branch_id in branch_ids],
-            "decision": display_decision,
-            "reasoning": business_text(row.get("reasoning")),
-            "selected": [
-                f"方案 {item['branchId']}"
-                for item in outcomes
-                if item["status"] == "merged"
-            ],
-            "parked": [
-                f"方案 {item['branchId']}"
-                for item in outcomes
-                if item["status"] == "parked"
-            ],
-            "rejected": [
-                f"方案 {item['branchId']}"
-                for item in outcomes
-                if item["status"] == "discarded"
-            ],
-            "completeness": "partial" if missing or not branch_ids else "complete",
-        }
-    )
-    projection.setdefault("card", {}).setdefault("primary", {})[
-        "value"
-    ] = display_decision
-    return {
-        "id": f"multipath-decision:{record_id}",
-        "recordId": record_id,
-        "roundIndex": round_index,
-        "branchIds": branch_ids,
-        "decision": decision,
-        "reasoning": _plain(row.get("reasoning")),
-        "createdAt": row.get("created_at"),
-        "completeness": "partial" if missing or not branch_ids else "complete",
-        "missingBranchIds": missing,
-        "branchOutcomes": outcomes,
-        "decisionProjection": projection,
-        "node": _story(
-            id=f"multipath-decision:{record_id}",
-            role="multipath-decision",
-            kind="decision",
-            title="主 Agent 多路决策",
-            label="决定",
-            value=projection["card"]["primary"]["value"],
-            secondary=projection["card"]["secondary"],
-            detail_ref=f"multipath-decision:{record_id}",
-            source_notice="record-missing" if missing else None,
-            decision=projection,
-        ),
-    }
-
-
-def _convergence_batches(
-    round_index: int,
-    reviews: list[dict[str, Any]],
-    decisions: list[dict[str, Any]],
-    run_status: str,
-) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
-    ordered_reviews = sorted(reviews, key=_runtime_order)
-    ordered_decisions = sorted(decisions, key=_decision_order)
-    unmatched_reviews = set(range(len(ordered_reviews)))
-    batches: list[dict[str, Any]] = []
-
-    for decision in ordered_decisions:
-        decision_set = set(decision.get("branchIds") or [])
-        decision_time = _date_value(decision.get("createdAt"))
-        candidates = [
-            index
-            for index in unmatched_reviews
-            if set(ordered_reviews[index].get("branchIds") or []) == decision_set
-            and _not_after(ordered_reviews[index].get("endedAt"), decision_time)
-        ]
-        review_index = max(
-            candidates,
-            key=lambda index: _runtime_order(ordered_reviews[index]),
-            default=None,
-        )
-        review = ordered_reviews[review_index] if review_index is not None else None
-        if review_index is not None:
-            unmatched_reviews.remove(review_index)
-        batches.append(
-            _convergence_batch(
-                round_index,
-                review,
-                decision,
-                "exact-branch-set-and-time" if review else "decision-only",
-                run_status,
-            )
-        )
-
-    for index in sorted(unmatched_reviews, key=lambda value: _runtime_order(ordered_reviews[value])):
-        review = ordered_reviews[index]
-        batches.append(
-            _convergence_batch(
-                round_index, review, None, "review-only", run_status
-            )
-        )
-
-    batches.sort(key=_convergence_order)
-    return batches, []
-
-
-def _annotate_unscoped_round_reviews(
-    batches: list[dict[str, Any]],
-    unassigned: list[dict[str, Any]],
-    round_index: int,
-) -> None:
-    """Keep unscoped reviews visible without fabricating a candidate link."""
-
-    reviews = [
-        item
-        for item in unassigned
-        if str(item.get("name") or "") == "script_multipath_evaluator"
-        and _integer(item.get("roundIndex")) == round_index
-        and str(item.get("association") or "") == "missing-branch-scope"
-    ]
-    if not reviews:
-        return
-    refs = [item.get("detailRef") for item in reviews if item.get("detailRef")]
-    for batch in batches:
-        missing = batch.get("missingReview")
-        if not isinstance(missing, dict):
-            continue
-        missing["card"] = {
-            "primary": {
-                "label": "评审关联",
-                "value": "发现同轮评审,但无法安全关联到具体候选",
-            },
-            "secondary": [
-                _line("records", "未关联记录", f"{len(refs)} 条评审记录")
-            ],
-        }
-        missing["sourceNotice"] = "association-incomplete"
-        missing["unscopedReviewRefs"] = refs
-
-
-def _convergence_batch(
-    round_index: int,
-    review: dict[str, Any] | None,
-    decision: dict[str, Any] | None,
-    association: str,
-    run_status: str,
-    branch_ids: list[int] | None = None,
-) -> dict[str, Any]:
-    branch_ids = branch_ids or list(
-        dict.fromkeys(
-            (review or {}).get("branchIds")
-            or (decision or {}).get("branchIds")
-            or []
-        )
-    )
-    suffix = (
-        f"review-{review.get('eventId')}"
-        if review
-        else f"decision-{decision.get('recordId')}"
-        if decision
-        else "records-missing"
-    )
-    missing_context = _missing_multipath_context(run_status)
-    if review is not None and not review.get("decisionProjection"):
-        review = dict(review)
-        review["decisionProjection"] = _evaluation_decision(
-            review,
-            subtype="multipath-evaluation",
-            detail_ref=review.get("detailRef"),
-            prompt_ref=review.get("promptRef"),
-        )
-    missing_review = None
-    if review is None:
-        missing_review_id = f"round:{round_index}:{suffix}:review-missing"
-        missing_review = _story(
-            id=missing_review_id,
-            role="multipath-review",
-            kind=missing_context["kind"],
-            title="多方案评审",
-            label="评审记录",
-            value="未找到可与本次决策安全关联的多方案评审记录",
-            detail_ref=missing_review_id,
-            source_notice=missing_context["sourceNotice"],
-            status=missing_context.get("status"),
-        )
-    missing_decision = None
-    if decision is None:
-        missing_decision_id = f"round:{round_index}:{suffix}:decision-missing"
-        missing_decision = _story(
-            id=missing_decision_id,
-            role="multipath-decision",
-            kind=missing_context["kind"],
-            title="主 Agent 多路决策",
-            label=missing_context["label"],
-            value=missing_context["value"],
-            detail_ref=missing_decision_id,
-            source_notice=missing_context["sourceNotice"],
-            status=missing_context.get("status"),
-        )
-    if review is not None and decision is not None:
-        _attach_review_comparison(review, decision, association)
-    return {
-        "id": f"round:{round_index}:convergence:{suffix}",
-        "roundIndex": round_index,
-        "branchIds": branch_ids,
-        "review": review,
-        "decision": decision,
-        "missingReview": missing_review,
-        "missingDecision": missing_decision,
-        "association": association,
-    }
-
-
-def _round_evaluation(
-    round_index: int, evaluations: list[dict[str, Any]], run_status: str
-) -> dict[str, Any]:
-    if not evaluations:
-        context = _missing_evaluation_context(run_status)
-        status = context.get("status")
-        if not status:
-            status = (
-                "not-evaluated"
-                if context["sourceNotice"] == "record-missing"
-                else "unknown"
-            )
-        return _story(
-            id=f"round:{round_index}:evaluation",
-            role="round-evaluation",
-            kind=context["kind"],
-            title="主脚本整体评审",
-            label=context["label"],
-            value=context["value"],
-            detail_ref=f"round:{round_index}:evaluation",
-            source_notice=context["sourceNotice"],
-            status=status,
-        )
-    latest = evaluations[-1]
-    conclusion = _plain(latest.get("conclusion"))
-    projection = _evaluation_decision(
-        latest,
-        subtype="overall-evaluation",
-        detail_ref=latest.get("detailRef"),
-        prompt_ref=latest.get("promptRef"),
-    )
-    return _story(
-        id=f"round:{round_index}:evaluation",
-        role="round-evaluation",
-        kind="decision" if conclusion else "missing",
-        title="主脚本整体评审",
-        label="评审结论",
-        value=projection["card"]["primary"]["value"],
-        secondary=projection["card"]["secondary"],
-        detail_ref=latest.get("detailRef"),
-        source_notice="runtime-associated",
-        status=_evaluation_state(conclusion),
-        decision=projection,
-    )
-
-
-def _evaluation_decision(
-    evaluation: dict[str, Any],
-    *,
-    subtype: str,
-    detail_ref: Any,
-    prompt_ref: Any,
-) -> dict[str, Any]:
-    projection = _DECISIONS.evaluation(
-        {
-            "id": evaluation.get("id") or detail_ref,
-            "detailRef": detail_ref,
-            "promptRef": prompt_ref,
-            "subtype": subtype,
-            "subjects": evaluation.get("subjects") or [
-                f"方案 {value}" for value in evaluation.get("branchIds") or []
-            ],
-            "criteria": evaluation.get("criteria") or [],
-            "itemConclusions": evaluation.get("itemConclusions")
-            or evaluation.get("branchConclusions")
-            or [],
-            "branchEvaluations": evaluation.get("branchEvaluations") or [],
-            "comparisonRows": evaluation.get("comparisonRows") or [],
-            "achievements": evaluation.get("achievements") or [],
-            "problems": evaluation.get("problems") or [],
-            "conclusion": evaluation.get("conclusion"),
-            "recommendation": evaluation.get("recommendation")
-            or evaluation.get("comparison"),
-            "nextGoal": evaluation.get("nextGoal"),
-            "completeness": "partial"
-            if evaluation.get("notices")
-            else "complete",
-        }
-    )
-    full_conclusion = business_text(
-        evaluation.get("conclusion")
-        or evaluation.get("recommendation")
-        or evaluation.get("comparison")
-    )
-    if full_conclusion:
-        projection.setdefault("card", {}).setdefault("primary", {})[
-            "value"
-        ] = full_conclusion
-    return projection
-
-
-def _attach_review_comparison(
-    review: dict[str, Any], decision: dict[str, Any], association: str
-) -> None:
-    projection = decision.get("decisionProjection")
-    if not isinstance(projection, dict):
-        return
-    conclusions = {
-        int(item.get("branchId") or 0): item
-        for item in review.get("branchConclusions") or []
-        if isinstance(item, dict)
-    }
-    outcomes = {
-        int(item.get("branchId") or 0): item
-        for item in decision.get("branchOutcomes") or []
-        if isinstance(item, dict)
-    }
-    reasoning = business_text(decision.get("reasoning"))
-    rows: list[dict[str, Any]] = []
-    for branch_id in decision.get("branchIds") or []:
-        row = {
-            "candidate": f"方案 {branch_id}",
-            "recommendation": (conclusions.get(branch_id) or {}).get("conclusion")
-            or review.get("recommendation")
-            or "评审未给出单独结论",
-            "finalDecision": (outcomes.get(branch_id) or {}).get("label")
-            or "最终状态未记录",
-        }
-        handling = _explicit_recommendation_handling(reasoning, branch_id)
-        if handling:
-            row["handling"] = handling
-        rows.append(row)
-    projection.setdefault("body", {})["reviewComparison"] = rows
-    projection["body"]["reviewAssociation"] = association
-    review_input = {
-        "label": "同批候选方案的评审建议",
-        "summary": business_text(
-            review.get("recommendation") or review.get("comparison")
-        ),
-        "observedRelation": "returned-to-actor",
-        "decisionUse": "not-recorded",
-        "detailRef": review.get("detailRef"),
-    }
-    projection.setdefault("inputs", []).append(review_input)
-    projection.setdefault("notices", []).append(
-        {
-            "code": "same-batch-association",
-            "message": "评审与最终决定按同一批候选方案和发生顺序关联;系统没有保存直接的一对一关系。",
-        }
-    )
-    if rows:
-        blocks = projection.setdefault("detail", {}).setdefault("blocks", [])
-        blocks.insert(
-            0,
-            {
-                "type": "items",
-                "title": "参与判断",
-                "items": [
-                    {
-                        "label": review_input["label"],
-                        "value": review_input["summary"],
-                        "note": "可确认评审在最终决定前返回;是否采用应以下方的逐方案对照为准。",
-                        "detailRef": review_input["detailRef"],
-                    }
-                ],
-                "visualRole": "evidence",
-                "presentation": "list",
-                "collapsible": True,
-            },
-        )
-        blocks.insert(
-            3,
-            {
-                "type": "comparison",
-                "title": "评审建议与最终决定",
-                "rows": rows,
-                "visualRole": "section",
-                "presentation": "comparison",
-            },
-        )
-
-
-def _explicit_recommendation_handling(reasoning: str, branch_id: int) -> str | None:
-    if not reasoning:
-        return None
-    for sentence in re.split(r"[。;;\n]+", reasoning):
-        if not re.search(rf"方案\s*{branch_id}\b", sentence):
-            continue
-        if any(
-            token in sentence
-            for token in (
-                "采纳",
-                "吸收",
-                "保留",
-                "延后",
-                "转入",
-                "下一轮",
-                "转译",
-                "不直接合并",
-                "未采用",
-                "拒绝",
-                "不采纳",
-            )
-        ):
-            return sentence.strip()
-    return None
-
-
-_DATABASE_TIMEZONE = ZoneInfo("Asia/Shanghai")
-
-
-def _date_value(value: Any) -> datetime | None:
-    if isinstance(value, datetime):
-        parsed = value
-    elif isinstance(value, str) and value:
-        try:
-            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
-        except ValueError:
-            return None
-    else:
-        return None
-    if parsed.tzinfo is None:
-        parsed = parsed.replace(tzinfo=_DATABASE_TIMEZONE)
-    return parsed.astimezone(timezone.utc)
-
-
-def _not_after(value: Any, boundary: datetime | None) -> bool:
-    current = _date_value(value)
-    return boundary is None or current is None or current <= boundary
-
-
-def _record_order(row: dict[str, Any]) -> tuple[str, int]:
-    return (str(row.get("created_at") or ""), _integer(row.get("id")) or 0)
-
-
-def _runtime_order(row: dict[str, Any]) -> tuple[str, int]:
-    return (
-        str(row.get("startedAt") or row.get("endedAt") or ""),
-        _integer(row.get("eventId")) or 0,
-    )
-
-
-def _decision_order(row: dict[str, Any]) -> tuple[str, int]:
-    return (str(row.get("createdAt") or ""), _integer(row.get("recordId")) or 0)
-
-
-def _convergence_order(row: dict[str, Any]) -> tuple[str, int]:
-    review = row.get("review") or {}
-    decision = row.get("decision") or {}
-    return (
-        str(review.get("startedAt") or decision.get("createdAt") or ""),
-        _integer(review.get("eventId") or decision.get("recordId")) or 0,
-    )
-
-
-def _round_result(
-    record: dict[str, Any],
-    round_index: int,
-    branches: list[dict[str, Any]],
-    domain_info: list[dict[str, Any]],
-    state: str,
-    next_round: dict[str, Any] | None,
-) -> dict[str, Any]:
-    merged_content = [
-        branch
-        for branch in branches
-        if branch["pathType"] != "领域信息" and branch["status"] == "merged"
-    ]
-    script_summary = (
-        f"确认采用 {len(merged_content)} 个内容方案"
-        if merged_content
-        else "没有可确认的内容方案合入记录"
-    )
-    domain_summary = (
-        f"新增 {len(domain_info)} 条领域事实"
-        if domain_info
-        else "本轮没有新增领域事实记录"
-    )
-    if next_round:
-        next_step = f"数据库已记录第 {next_round.get('round_index')} 轮"
-    elif str(record.get("status") or "").lower() in TERMINAL_STATUSES:
-        next_step = "构建已经结束"
-    else:
-        next_step = "等待本轮收口"
-    primary = f"{script_summary};{domain_summary}"
-    return _story(
-        id=f"round:{round_index}:result",
-        role="round-result",
-        kind="result",
-        title="本轮产出",
-        label="产出",
-        value=primary,
-        secondary=[_line("next", "下一步", next_step)],
-        detail_ref=f"round:{round_index}:result",
-        artifact_ref=f"artifact:round:{round_index}",
-        has_changes=bool(merged_content or domain_info),
-        status=state,
-    )
-
-
-def _round_state(
-    record: dict[str, Any],
-    raw_round: dict[str, Any],
-    evaluation: dict[str, Any] | None,
-) -> str:
-    if evaluation and evaluation.get("status") not in {None, "unknown"}:
-        return str(evaluation["status"])
-    run_status = str(record.get("status") or "").lower()
-    if str(raw_round.get("status") or "").lower() == "open" and run_status not in TERMINAL_STATUSES:
-        return "running"
-    return "unknown"
-
-
-def _evaluation_state(conclusion: str | None) -> str:
-    text = str(conclusion or "")
-    if any(token in text for token in ("未通过", "不通过", "回退", "重试")):
-        return "needs-retry"
-    if "部分通过" in text:
-        return "partially-passed"
-    if "通过" in text:
-        return "passed"
-    return "unknown"
-
-
-def _final_result(
-    record: dict[str, Any], rounds: list[dict[str, Any]], artifact: dict[str, Any]
-) -> dict[str, Any]:
-    branches = [branch for round_ in rounds for branch in round_["branches"]]
-    statuses = Counter(branch["status"] for branch in branches)
-    counts = _snapshot_counts(artifact)
-    status = str(record.get("status") or "unknown")
-    summary = _plain(record.get("summary"))
-    error = _plain(record.get("error_message"))
-    primary = summary or error or _final_status_text(status)
-    return {
-        "id": "run:final-result",
-        "role": "final-result",
-        "kind": "result",
-        "title": "最终结果",
-        "status": status,
-        "terminal": status.lower() in TERMINAL_STATUSES,
-        "roundCount": len(rounds),
-        "branchCounts": {
-            "total": len(branches),
-            "merged": statuses.get("merged", 0),
-            "parked": statuses.get("parked", 0),
-            "discarded": statuses.get("discarded", 0),
-            "open": statuses.get("open", 0),
-        },
-        "summary": _plain(record.get("summary")),
-        "errorMessage": _plain(record.get("error_message")),
-        "artifact": {
-            "snapshotRef": "artifact:base:current",
-            "paragraphCount": counts["paragraphs"],
-            "elementCount": counts["elements"],
-            "linkCount": counts["links"],
-            "historicalVersion": "current-only",
-        },
-        "card": {
-            "primary": {"label": "构建结论", "value": primary},
-            "secondary": [
-                _line("rounds", "轮次", f"{len(rounds)} 轮"),
-                _line(
-                    "branches",
-                    "方案处置",
-                    f"采用 {statuses.get('merged', 0)} · 暂存 {statuses.get('parked', 0)} · 未采用 {statuses.get('discarded', 0)}",
-                ),
-                _line(
-                    "artifact",
-                    "主脚本规模",
-                    f"{counts['paragraphs']} 段落 · {counts['elements']} 元素 · {counts['links']} 关联",
-                ),
-            ],
-        },
-        "artifactRef": "artifact:base:current",
-        "detailRef": "run:final-result",
-    }
-
-
-def _final_status_text(status: str) -> str:
-    return {
-        "success": "构建已完成",
-        "completed": "构建已完成",
-        "partial": "构建部分完成",
-        "failed": "构建失败",
-        "stopped": "构建已停止",
-        "cancelled": "构建已取消",
-        "running": "构建进行中",
-    }.get(str(status or "").lower(), "构建状态已记录")
-
-
-def _story(
-    *,
-    id: str,
-    role: str,
-    kind: str,
-    title: str,
-    label: str,
-    value: Any,
-    secondary: list[dict[str, Any]] | None = None,
-    detail_ref: str | None = None,
-    has_changes: bool = False,
-    source_notice: str | None = None,
-    status: str | None = None,
-    artifact_ref: str | None = None,
-    decision: dict[str, Any] | None = None,
-) -> dict[str, Any]:
-    node = {
-        "id": id,
-        "role": role,
-        "kind": kind,
-        "title": title,
-        "card": (decision or {}).get("card")
-        or {
-            # Callers pass a deliberately selected card field. Keep that field
-            # intact; line clamping is a view concern, not a data projection.
-            "primary": {"label": label, "value": _plain(value)},
-            "secondary": [item for item in (secondary or []) if item.get("value")],
-        },
-        "hasChanges": bool(has_changes),
-    }
-    if detail_ref:
-        node["detailRef"] = detail_ref
-    if source_notice:
-        node["sourceNotice"] = source_notice
-    if status:
-        node["status"] = status
-    if artifact_ref:
-        node["artifactRef"] = artifact_ref
-    if decision:
-        node["decision"] = decision
-    return node
-
-
-def _line(key: str, label: str, value: Any, tone: str = "normal") -> dict[str, Any]:
-    return {"key": key, "label": label, "value": _plain(value), "tone": tone}
-
-
-def _plan_card_lines(paths: list[Any], note: Any) -> list[dict[str, Any]]:
-    lines: list[dict[str, Any]] = []
-    if paths:
-        route_values = [plan_path_summary(path, index) for index, path in enumerate(paths[:2], 1)]
-        route_summary = ";".join(route_values)
-        if len(paths) > 2:
-            route_summary += f";另有 {len(paths) - 2} 路,详情查看"
-        lines.append(_line("routes", "各路任务", route_summary))
-    reason = _plain(note)
-    if reason:
-        lines.append(_line("reason", "规划理由", reason))
-    return lines[:2]
-
-
-def _context_input_labels(
-    context: dict[str, Any] | None, *, relation: str | None = None
-) -> list[str]:
-    result: list[str] = []
-    for item in (context or {}).get("inputs") or []:
-        if not isinstance(item, dict):
-            continue
-        if relation and item.get("relation") != relation:
-            continue
-        label = _plain(item.get("label"))
-        if label and label not in result:
-            result.append(label)
-    return result[:3]
-
-
-def _direction_decision(
-    *,
-    id: str,
-    subtype: str,
-    context: dict[str, Any] | None,
-    decision_items: list[Any],
-    primary_label: str,
-    primary_value: Any,
-    secondary: list[dict[str, Any]],
-    prompt_ref: str,
-) -> dict[str, Any]:
-    context = context if isinstance(context, dict) else {}
-    decision = _DECISIONS.direction(
-        {
-            "id": id,
-            "detailRef": id,
-            "promptRef": prompt_ref,
-            "subtype": subtype,
-            "decisionItems": [str(item) for item in decision_items if item],
-            "explicitReasoning": context.get("explicitReasoning"),
-            "constraints": context.get("constraints") or [],
-            "inputs": context.get("inputs") or [],
-            "sourceDocument": context.get("sourceDocument"),
-            "implementationPlan": {
-                "mode": context.get("displayMode"),
-                "rawMode": context.get("rawMode"),
-                "routes": context.get("routeItems") or [],
-                "reasoning": context.get("explicitReasoning"),
-            }
-            if subtype == "implementation-plan"
-            else None,
-            "completeness": context.get("completeness"),
-        }
-    )
-    decision["card"] = {
-        "primary": {
-            "label": primary_label,
-            "value": _plain(primary_value) or "决策结论未记录",
-        },
-        "secondary": [item for item in secondary if item.get("value")][:3],
-    }
-    revisions = []
-    for revision in context.get("revisions") or []:
-        if not isinstance(revision, dict):
-            continue
-        paths = revision.get("paths") if isinstance(revision.get("paths"), list) else []
-        parts = [str(revision.get("mode"))] if revision.get("mode") else []
-        if paths:
-            parts.append(f"{len(paths)} 路方案")
-        revisions.append(
-            {
-                "current": bool(revision.get("current")),
-                "detailRef": revision.get("detailRef"),
-                "summary": " · ".join(parts) or "已保存一次版本",
-            }
-        )
-    if revisions:
-        decision["body"]["revisions"] = revisions
-    decision["technicalRefs"] = list(context.get("technicalRefs") or [])
-    return decision
-
-
-def _implementer_prompt_ref(retrieval_stage: dict[str, Any] | None) -> str | None:
-    event_id = _integer((retrieval_stage or {}).get("implementerEventId"))
-    return f"prompt:event:{event_id}" if event_id is not None else None
-
-
-def _snapshot_counts(value: Any) -> dict[str, int]:
-    if isinstance(value, dict) and isinstance(value.get("snapshot"), dict):
-        value = value["snapshot"]
-    if not isinstance(value, dict):
-        return {"paragraphs": 0, "elements": 0, "links": 0}
-    stats = value.get("stats") if isinstance(value.get("stats"), dict) else {}
-    if stats:
-        return {
-            "paragraphs": int(stats.get("paragraph_count") or 0),
-            "elements": int(stats.get("element_count") or 0),
-            "links": int(stats.get("link_count") or 0),
-        }
-    return {
-        "paragraphs": len(value.get("paragraphs") or []),
-        "elements": len(value.get("elements") or []),
-        "links": len(value.get("paragraphElements") or value.get("paragraph_elements") or []),
-    }
-
-
-def _ids(value: Any) -> list[int]:
-    if not isinstance(value, list):
-        return []
-    result: list[int] = []
-    for item in value:
-        number = _integer(item)
-        if number is not None and number not in result:
-            result.append(number)
-    return result
-
-
-def _implementer_tasks_by_branch(
-    events: list[dict[str, Any]],
-) -> dict[int, str]:
-    """Return complete dispatch tasks when the lightweight event carries them.
-
-    Historical bundles may only retain previews; callers deliberately keep the
-    branch business record as a labelled fallback in that case.
-    """
-
-    result: dict[int, str] = {}
-    for event in events:
-        if (
-            str(event.get("event_type") or "") != "agent_invoke"
-            or str(event.get("event_name") or "") != "script_implementer"
-        ):
-            continue
-        branch_id = _integer(event.get("branch_id"))
-        if branch_id is None:
-            continue
-        data = event.get("inputData")
-        task = data.get("task") if isinstance(data, dict) else None
-        if not task:
-            wrapped = event.get("input")
-            content = wrapped.get("content") if isinstance(wrapped, dict) else None
-            task = content.get("task") if isinstance(content, dict) else None
-        text = branch_task_text(task)
-        if text:
-            result[branch_id] = text
-    return result
-
-
-def _round_summary(
-    *,
-    round_index: int,
-    goal: dict[str, Any],
-    plan: dict[str, Any],
-    branches: list[dict[str, Any]],
-    batches: list[dict[str, Any]],
-    domain_info: list[dict[str, Any]],
-    run_status: str,
-) -> dict[str, Any]:
-    paths = plan.get("paths") or []
-    branch_ids = sorted(
-        {
-            branch_id
-            for batch in batches
-            for branch_id in batch.get("branchIds") or []
-        }
-    )
-    decision_context = _missing_multipath_context(run_status)
-    decision_summary = (
-        ";".join(_preview(_business_terms(batch.get("decision")), 64) for batch in batches[:2])
-        if batches
-        else decision_context["value"]
-    )
-    if len(batches) > 2:
-        decision_summary += f";另有 {len(batches) - 2} 个决策批次"
-    merged_content_count = sum(
-        1
-        for branch in branches
-        if branch.get("pathType") != "领域信息" and branch.get("status") == "merged"
-    )
-    script_summary = (
-        f"{merged_content_count} 个内容方案进入主脚本"
-        if merged_content_count
-        else "没有可确认的内容方案合入记录"
-    )
-    domain_summary = (
-        f"新增 {len(domain_info)} 条领域事实"
-        if domain_info
-        else "没有新增领域事实记录"
-    )
-    return {
-        "title": f"第 {round_index} 轮",
-        "goal": goal,
-        "approach": {
-            "mode": plan.get("mode"),
-            "candidateCount": len(branches) or len(paths),
-            "plannedPathCount": len(paths),
-            "contentBranchCount": sum(
-                1 for branch in branches if branch.get("pathType") == "内容"
-            ),
-            "domainBranchCount": sum(
-                1 for branch in branches if branch.get("pathType") == "领域信息"
-            ),
-            "summary": _plan_summary(plan),
-        },
-        "decision": {
-            "batchCount": len(batches),
-            "branchIds": branch_ids,
-            "summary": decision_summary,
-        },
-        "output": {
-            "mergedContentCount": merged_content_count,
-            "domainFactCount": len(domain_info),
-            "scriptSummary": script_summary,
-            "domainSummary": domain_summary,
-            "summary": f"{script_summary};{domain_summary}",
-        },
-    }
-
-
-def _goal_summary(value: Any) -> dict[str, Any]:
-    parsed = parse_goal_text(value)
-    if not parsed.get("raw"):
-        return {
-            "headline": "本轮目标未记录",
-            "itemCount": 0,
-            "previewItems": [],
-            "truncated": False,
-        }
-    items = list(parsed.get("focusItems") or [])
-    headline_source = str(parsed.get("headline") or parsed.get("raw"))
-    headline = _preview(headline_source, 96)
-    preview_items = [_preview(item, 72) for item in items[:3]]
-    return {
-        "headline": headline,
-        "itemCount": len(items),
-        "previewItems": preview_items,
-        "truncated": bool(
-            items
-            or len(headline_source) > len(headline)
-            or len(items) > len(preview_items)
-        ),
-    }
-
-
-def _business_terms(value: Any) -> str:
-    text = _plain(value) or "未记录"
-    text = re.sub(r"(?i)\s*\bbranch\s*#?(\d+)\b\s*", r"方案 \1 ", text)
-    text = re.sub(r"(?i)\s*\bbase\b", "主脚本", text)
-    text = re.sub(r"\s+([,。;!?、:)】,.])", r"\1", text)
-    return text
-
-
-def _missing_multipath_context(run_status: str) -> dict[str, Any]:
-    status = str(run_status or "unknown").lower()
-    if status == "running":
-        return {
-            "kind": "execution",
-            "label": "当前进度",
-            "value": "等待主 Agent 比较候选方案",
-            "sourceNotice": None,
-            "status": "running",
-        }
-    interrupted = {
-        "stopped": "构建停止前尚未形成多路决策",
-        "cancelled": "构建取消前尚未形成多路决策",
-        "cancel": "构建取消前尚未形成多路决策",
-        "cancle": "构建取消前尚未形成多路决策",
-        "failed": "构建失败前尚未形成多路决策",
-    }
-    if status in interrupted:
-        return {
-            "kind": "missing",
-            "label": "流程进度",
-            "value": interrupted[status],
-            "sourceNotice": "process-incomplete",
-        }
-    return {
-        "kind": "missing",
-        "label": "记录状态",
-        "value": "构建已经结束,但未找到主 Agent 多路决策记录",
-        "sourceNotice": "record-missing",
-    }
-
-
-def _missing_evaluation_context(run_status: str) -> dict[str, Any]:
-    status = str(run_status or "unknown").lower()
-    if status == "running":
-        return {
-            "kind": "execution",
-            "label": "当前进度",
-            "value": "等待整体评审",
-            "sourceNotice": None,
-            "status": "running",
-        }
-    interrupted = {
-        "stopped": "构建停止前尚未进行整体评审",
-        "cancelled": "构建取消前尚未进行整体评审",
-        "cancel": "构建取消前尚未进行整体评审",
-        "cancle": "构建取消前尚未进行整体评审",
-        "failed": "构建失败前尚未形成整体评审结论",
-    }
-    if status in interrupted:
-        return {
-            "kind": "missing",
-            "label": "流程进度",
-            "value": interrupted[status],
-            "sourceNotice": "process-incomplete",
-        }
-    return {
-        "kind": "missing",
-        "label": "记录状态",
-        "value": "构建已经结束,但未找到明确的整体评审结论",
-        "sourceNotice": "record-missing",
-    }
-
-
-def _branch_disposition_context(branch_status: str, run_status: str) -> dict[str, Any]:
-    if branch_status != "open":
-        return {
-            "kind": "decision",
-            "label": _status_label(branch_status),
-            "sourceNotice": None,
-        }
-    status = str(run_status or "unknown").lower()
-    if status == "running":
-        return {
-            "kind": "execution",
-            "label": "等待主 Agent 处置",
-            "sourceNotice": None,
-        }
-    interrupted = {
-        "stopped": "构建停止时尚未处置",
-        "cancelled": "构建取消时尚未处置",
-        "cancel": "构建取消时尚未处置",
-        "cancle": "构建取消时尚未处置",
-        "failed": "构建失败时尚未处置",
-    }
-    if status in interrupted:
-        return {
-            "kind": "missing",
-            "label": interrupted[status],
-            "sourceNotice": "process-incomplete",
-        }
-    return {
-        "kind": "missing",
-        "label": "构建结束时仍未处置",
-        "sourceNotice": "record-missing",
-    }
-
-
-def _plan_summary(plan: dict[str, Any]) -> str:
-    paths = plan.get("paths") or []
-    if not paths:
-        return "多路规划未记录"
-    return f"{plan.get('mode') or '方式未记录'}:{len(paths)} 个候选方案"
-
-
-def _status_label(status: str) -> str:
-    return {
-        "merged": "已采用",
-        "parked": "暂存",
-        "discarded": "未采用",
-        "open": "尚未处置",
-    }.get(status, status or "状态未记录")
-
-
-def _data_shape(
-    branches: list[dict[str, Any]],
-    multipath_decisions: list[dict[str, Any]],
-    domain_info: list[dict[str, Any]],
-    events: list[dict[str, Any]],
-) -> str:
-    if (
-        multipath_decisions
-        or domain_info
-        or events
-        or any(branch.get("path_type") for branch in branches)
-    ):
-        return "current"
-    return "legacy-incomplete"
-
-
-def _warnings(
-    *,
-    data_shape: str,
-    ignored_legacy_count: int,
-    rounds: list[dict[str, Any]],
-    runtime_unassigned: list[dict[str, Any]],
-    business_unassigned: list[dict[str, Any]],
-) -> list[dict[str, str]]:
-    warnings: list[dict[str, str]] = []
-    if data_shape == "legacy-incomplete":
-        warnings.append(
-            {
-                "id": "legacy-shape",
-                "level": "warning",
-                "message": "这是旧结构记录,只展示新数据契约能够直接确认的内容。",
-            }
-        )
-    if ignored_legacy_count:
-        warnings.append(
-            {
-                "id": "ignored-legacy-data-decisions",
-                "level": "info",
-                "message": f"已忽略 {ignored_legacy_count} 条旧语义主决策记录。",
-            }
-        )
-    missing_rounds = [
-        round_["roundIndex"]
-        for round_ in rounds
-        if round_["branches"]
-        and not any(
-            batch.get("decision")
-            for batch in round_.get("convergenceBatches") or []
-        )
-        and any(
-            (batch.get("missingDecision") or {}).get("sourceNotice")
-            == "record-missing"
-            for batch in round_.get("convergenceBatches") or []
-        )
-    ]
-    if missing_rounds:
-        warnings.append(
-            {
-                "id": "missing-multipath-decisions",
-                "level": "warning",
-                "message": "第 " + "、".join(map(str, missing_rounds)) + " 轮未记录主 Agent 多路决策。",
-            }
-        )
-    if runtime_unassigned:
-        warnings.append(
-            {
-                "id": "unassigned-runtime-events",
-                "level": "info",
-                "message": f"有 {len(runtime_unassigned)} 条运行记录无法安全归属,已留在技术详情中。",
-            }
-        )
-    if business_unassigned:
-        warnings.append(
-            {
-                "id": "unassigned-business-records",
-                "level": "warning",
-                "message": f"有 {len(business_unassigned)} 条多路决策缺少可确认的轮次归属。",
-            }
-        )
-    return warnings
-
-
-def _plain(value: Any) -> str | None:
-    if value is None:
-        return None
-    text = str(value).replace("\r\n", "\n").strip()
-    if not text:
-        return None
-    text = re.sub(r"```[a-zA-Z0-9_-]*", "", text).replace("```", "")
-    text = re.sub(r"^\s{0,3}#{1,6}\s*", "", text, flags=re.M)
-    text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
-    text = re.sub(r"(?m)^\s*[-*]\s+", "• ", text)
-    return text.strip()
-
-
-def _preview(value: Any, limit: int = 140) -> str:
-    text = _plain(value) or "未记录"
-    text = re.sub(r"\s+", " ", text)
-    if len(text) <= limit:
-        return text
-    window = text[:limit]
-    cut = max(window.rfind(mark) for mark in "。!?;")
-    if cut >= max(30, limit // 2):
-        return window[: cut + 1]
-    return window.rstrip(",、;:,. ") + "…"
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value) if value is not None else None
-    except (TypeError, ValueError):
-        return None

+ 0 - 66
visualization/backend/app/execution_view_payload.py

@@ -1,66 +0,0 @@
-from __future__ import annotations
-
-from copy import deepcopy
-from typing import Any
-
-
-_REVIEW_KEYS = {
-    "id",
-    "eventId",
-    "roundIndex",
-    "branchIds",
-    "branchConclusions",
-    "comparison",
-    "recommendation",
-    "conclusion",
-    "status",
-    "startedAt",
-    "endedAt",
-    "detailRef",
-    "promptRef",
-    "decisionProjection",
-}
-
-
-def compact_execution_view(view: dict[str, Any]) -> dict[str, Any]:
-    """Return the card-level V8 payload used by the main canvas.
-
-    Inspector blocks, evaluator bodies and raw runtime previews remain available
-    through the round/activity endpoints.  Keeping this boundary here prevents
-    the browser from downloading the same large report once per card.
-    """
-
-    compact = deepcopy(view)
-    _strip_decision_details(compact)
-    for round_ in compact.get("rounds") or []:
-        for branch in round_.get("branches") or []:
-            branch["dataDecisions"] = [
-                {
-                    "id": item.get("id"),
-                    "node": item.get("node"),
-                }
-                for item in branch.get("dataDecisions") or []
-                if isinstance(item, dict)
-            ]
-        for batch in round_.get("convergenceBatches") or []:
-            review = batch.get("review")
-            if isinstance(review, dict):
-                batch["review"] = {
-                    key: value for key, value in review.items() if key in _REVIEW_KEYS
-                }
-    return compact
-
-
-def _strip_decision_details(value: Any) -> None:
-    if isinstance(value, dict):
-        if value.get("semanticKind") == "decision":
-            value.pop("detail", None)
-        value.pop("inputPreview", None)
-        value.pop("outputPreview", None)
-        value.pop("agentType", None)
-        for child in value.values():
-            _strip_decision_details(child)
-        return
-    if isinstance(value, list):
-        for child in value:
-            _strip_decision_details(child)

+ 1785 - 0
visualization/backend/app/fake_data.py

@@ -0,0 +1,1785 @@
+"""Deterministic, internally consistent fake run for the current pipeline."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from dataclasses import dataclass
+from decimal import Decimal
+from typing import Any
+
+from .contracts import ENUM_CATALOG, SCHEMA_CATALOG
+from .models import (
+    FlowEdge,
+    FlowNode,
+    Metric,
+    NodeDetail,
+    PhaseFrame,
+    RecordEnvelope,
+    RunGraph,
+    RunSummary,
+)
+
+RUN_ID = 10001
+ROOT_TRACE_ID = "trace_fake_phase3_10001"
+STAMP = "2026-07-20T10:24:36.482000+08:00"
+START = "2026-07-20T10:18:04.103000+08:00"
+
+
+def digest(label: str) -> str:
+    return hashlib.sha256(label.encode()).hexdigest()
+
+
+def wire_digest(label: str) -> str:
+    return f"sha256:{digest(label)}"
+
+
+def normalize_json(value: Any) -> Any:
+    if value is None or isinstance(value, (str, bool, int)):
+        return value
+    if isinstance(value, float):
+        rendered = format(Decimal(str(value)), "f")
+        if "." in rendered:
+            rendered = rendered.rstrip("0").rstrip(".")
+        return "0" if rendered in {"-0", ""} else rendered
+    if isinstance(value, dict):
+        return {key: normalize_json(value[key]) for key in sorted(value)}
+    if isinstance(value, (list, tuple)):
+        return [normalize_json(item) for item in value]
+    raise TypeError(f"unsupported fake canonical JSON value: {type(value).__name__}")
+
+
+def canonical_digest(payload: Any) -> str:
+    encoded = json.dumps(
+        normalize_json(payload),
+        ensure_ascii=False,
+        allow_nan=False,
+        sort_keys=True,
+        separators=(",", ":"),
+    ).encode("utf-8")
+    return hashlib.sha256(encoded).hexdigest()
+
+
+def canonical_wire_digest(payload: Any) -> str:
+    return f"sha256:{canonical_digest(payload)}"
+
+
+def input_canonical_payload() -> dict[str, Any]:
+    return {
+        "schema_version": "script-build-input/v1",
+        "script_build_id": RUN_ID,
+        "execution_id": 88001,
+        "topic_build_id": 67001,
+        "topic_id": 7701,
+        "topic": {"id": 7701, "title": "为什么 AI Agent 项目总在最后一步失控"},
+        "account": {"id": 230, "name": "产品深水区"},
+        "persona_points": [
+            {
+                "point_id": 7,
+                "point_type": "voice",
+                "point_result": "克制、具体、敢于下判断",
+            }
+        ],
+        "section_patterns": [{"pattern_id": 14, "name": "事故切入—框架拆解—行动清单"}],
+        "strategies": [{"strategy_id": 3, "version": 5, "name": "反常识开场"}],
+        "prompt_manifest": [
+            {
+                "role": "planner",
+                "name": "script_planner",
+                "sha256": digest("planner-prompt"),
+            }
+        ],
+        "datasource_manifest": {"adapter": "deterministic-fake", "version": "1"},
+        "model_manifest": {
+            "planner": "deterministic-fake-model",
+            "worker": "deterministic-fake-model",
+        },
+    }
+
+
+ARTIFACT_DIGESTS: dict[int, str] = {}
+ARTIFACT_KINDS: dict[int, str] = {}
+
+
+def artifact_uri(version: int) -> str:
+    return f"script-build://artifact-versions/{version}"
+
+
+def artifact_ref(version: int, kind: str | None = None) -> dict[str, Any]:
+    return {
+        "uri": artifact_uri(version),
+        "kind": kind or ARTIFACT_KINDS[version],
+        "version": str(version),
+        "digest": ARTIFACT_DIGESTS.get(version, digest(f"artifact-{version}")),
+        "summary": None,
+        "metadata": {},
+    }
+
+
+def criterion(identifier: str, description: str) -> dict[str, Any]:
+    return {"criterion_id": identifier, "description": description, "hard": True}
+
+
+def decision_ref(decision_id: str, version: int, task_kind: str) -> dict[str, Any]:
+    return {
+        "decision_id": decision_id,
+        "artifact_ref": artifact_ref(version),
+        "scope_ref": f"script-build://decisions/{decision_id}",
+        "expected_task_kind": task_kind,
+    }
+
+
+def task_spec(objective: str, kind: str) -> dict[str, Any]:
+    return {
+        "version": 1,
+        "objective": objective,
+        "acceptance_criteria": [criterion("content_complete", "产物完整且引用闭合")],
+        "context_refs": [f"script-build://task-kinds/{kind}"],
+        "created_at": START,
+    }
+
+
+def task_payload(
+    task_id: str, objective: str, kind: str, parent: str | None = "task-root"
+) -> dict[str, Any]:
+    is_root = task_id == "task-root"
+    return {
+        "task_id": task_id,
+        "goal_id": "goal-script-delivery",
+        "parent_task_id": parent,
+        "display_path": "Root" if is_root else f"Root / {objective[:24]}",
+        "specs": [task_spec(objective, kind)],
+        "current_spec_version": 1,
+        "status": "completed",
+        "child_task_ids": ["direction", "portfolio"] if is_root else [],
+        "attempt_ids": [f"attempt-{task_id}"],
+        "validation_ids": [f"validation-{task_id}"],
+        "decision_ids": [f"decision-{task_id}"],
+        "repair_count_by_version": {"1": 0},
+        "blocked_reason": None,
+        "superseded_by": None,
+        "created_at": START,
+        "updated_at": STAMP,
+    }
+
+
+def contract_payload(
+    task_id: str, objective: str, kind: str, output_schema: str
+) -> dict[str, Any]:
+    intent = {
+        "compare": "compare",
+        "compose": "compose",
+        "candidate-portfolio": "portfolio",
+        "root-delivery": "deliver",
+        "paragraph": "expand",
+        "element-set": "expand",
+        "structure": "expand",
+    }.get(kind, "explore")
+    payload: dict[str, Any] = {
+        "schema_version": "script-task-contract/v1",
+        "task_kind": kind,
+        "scope_ref": f"script-build://tasks/{task_id}",
+        "intent_class": intent,
+        "objective": objective,
+        "input_decision_refs": [],
+        "base_artifact_ref": None,
+        "write_scope": [f"script-build://tasks/{task_id}/workspace"],
+        "gap_ref": None,
+        "output_schema": output_schema,
+        "criteria": [criterion("content_complete", "产物完整且引用闭合")],
+        "budget": {
+            "max_attempts": 4,
+            "max_tokens": 32000,
+            "max_seconds": 900,
+            "max_external_queries": 40,
+            "max_no_improvement": 3,
+        },
+        "supersedes_decision_ids": [],
+        "candidate_closure_decision_refs": [],
+        "adopted_decision_ids": [],
+        "held_or_rejected_decision_ids": [],
+        "compose_order": [],
+        "comparison_decision_refs": [],
+    }
+    if kind == "compare":
+        payload["comparison_decision_refs"] = [
+            decision_ref("decision-paragraph:a", 121, "paragraph"),
+            decision_ref("decision-elements:a", 122, "element-set"),
+        ]
+    elif kind == "compose":
+        closure = [
+            decision_ref("decision-paragraph:a", 121, "paragraph"),
+            decision_ref("decision-elements:a", 122, "element-set"),
+        ]
+        payload["candidate_closure_decision_refs"] = closure
+        payload["adopted_decision_ids"] = [item["decision_id"] for item in closure]
+        payload["compose_order"] = [item["decision_id"] for item in closure]
+        payload["comparison_decision_refs"] = [
+            decision_ref("decision-compare:ab", 130, "compare")
+        ]
+    elif kind == "candidate-portfolio":
+        closure = [
+            decision_ref("decision-compose:final", 140, "compose"),
+            decision_ref("decision-compose:alternate", 141, "compose"),
+        ]
+        payload["candidate_closure_decision_refs"] = closure
+        payload["adopted_decision_ids"] = ["decision-compose:final"]
+        payload["held_or_rejected_decision_ids"] = ["decision-compose:alternate"]
+        payload["compose_order"] = ["decision-compose:final"]
+    elif kind == "root-delivery":
+        payload["input_decision_refs"] = [
+            decision_ref("decision-direction", 110, "direction"),
+            decision_ref("decision-portfolio", 150, "candidate-portfolio"),
+        ]
+        payload["scope_ref"] = "script-build://scope/root"
+        payload["write_scope"] = []
+    return payload
+
+
+def operation_payload(task_id: str, index: int) -> dict[str, Any]:
+    return {
+        "operation_id": f"operation-{index:02d}",
+        "root_trace_id": ROOT_TRACE_ID,
+        "kind": "dispatch",
+        "status": "completed",
+        "task_ids": [task_id],
+        "attempt_ids": [f"attempt-{task_id}"],
+        "validation_ids": [f"validation-{task_id}"],
+        "result_ref": {"task_id": task_id, "status": "completed"},
+        "execution_epoch": 1,
+        "deadline_at": "2026-07-20T10:34:00+08:00",
+        "error": None,
+        "started_at": START,
+        "completed_at": STAMP,
+        "created_at": START,
+        "updated_at": STAMP,
+    }
+
+
+def attempt_payload(
+    task_id: str, preset: str, artifact_version: int, index: int
+) -> dict[str, Any]:
+    return {
+        "attempt_id": f"attempt-{task_id}",
+        "task_id": task_id,
+        "spec_version": 1,
+        "worker_trace_id": f"worker-trace-{index:02d}",
+        "worker_preset": preset,
+        "execution_mode": "agent",
+        "accepted_child_decision_ids": (
+            ["decision-direction", "decision-portfolio"]
+            if task_id == "root-delivery"
+            else []
+        ),
+        "status": "submitted",
+        "operation_id": f"operation-{index:02d}",
+        "execution_epoch": 1,
+        "continue_from_trace_id": None,
+        "snapshot_id": f"artifact-snapshot-{index:02d}",
+        "submission": {
+            "summary": "已保存唯一冻结产物并提交验证。",
+            "artifact_refs": [artifact_ref(artifact_version)],
+            "evidence_refs": [],
+        },
+        "execution_stats": {
+            "primary_model": "deterministic-fake-model",
+            "total_tokens": 1840 + index * 31,
+            "total_cost": 0.012 + index * 0.001,
+            "failure_code": None,
+        },
+        "error": None,
+        "started_at": START,
+        "completed_at": STAMP,
+        "duration_ms": 2180 + index * 73,
+        "created_at": START,
+        "updated_at": STAMP,
+    }
+
+
+def validation_payload(task_id: str, preset: str, index: int) -> dict[str, Any]:
+    return {
+        "validation_id": f"validation-{task_id}",
+        "task_id": task_id,
+        "attempt_id": f"attempt-{task_id}",
+        "spec_version": 1,
+        "snapshot_id": f"artifact-snapshot-{index:02d}",
+        "validator_trace_id": f"validator-trace-{index:02d}",
+        "validator_preset": preset,
+        "validation_plan": {
+            "mode": "agent",
+            "validator_preset": preset,
+            "rule_ids": [],
+            "max_evidence_queries": 10,
+            "max_evidence_items_per_query": 50,
+            "evidence_timeout_seconds": 10.0,
+        },
+        "evidence_queries_used": 0,
+        "status": "completed",
+        "operation_id": f"operation-{index:02d}",
+        "execution_epoch": 1,
+        "verdict": "passed",
+        "criterion_results": [
+            {
+                "criterion_id": "content_complete",
+                "verdict": "passed",
+                "reason": "字段、引用、digest 与冻结快照一致。",
+                "evidence_refs": [],
+            }
+        ],
+        "summary": "所有硬性标准均通过。",
+        "evidence_refs": [],
+        "unverified_claims": [],
+        "risks": [],
+        "recommendation": "accept",
+        "execution_stats": {
+            "primary_model": "deterministic-fake-model",
+            "total_tokens": 620 + index * 11,
+            "total_cost": 0.004,
+            "failure_code": None,
+        },
+        "error": None,
+        "started_at": START,
+        "completed_at": STAMP,
+        "duration_ms": 840 + index * 29,
+        "created_at": START,
+        "updated_at": STAMP,
+    }
+
+
+def decision_payload(
+    task_id: str, index: int, action: str = "accept"
+) -> dict[str, Any]:
+    return {
+        "decision_id": f"decision-{task_id}",
+        "task_id": task_id,
+        "action": action,
+        "reason": "Validator 已通过,冻结产物满足当前合同。",
+        "from_status": "awaiting_decision",
+        "to_status": "completed" if action == "accept" else "running",
+        "attempt_id": f"attempt-{task_id}",
+        "validation_id": f"validation-{task_id}",
+        "payload": {"artifact_ref": artifact_uri(100 + index), "fenced_epoch": 1},
+        "created_at": STAMP,
+    }
+
+
+def lineage(scope: str) -> dict[str, Any]:
+    return {
+        "scope_ref": scope,
+        "input_snapshot_ref": "script-build://input-snapshots/snapshot-10001-v1",
+        "input_closure_digest": wire_digest(f"closure-{scope}"),
+        "base_artifact_ref": None,
+        "base_artifact_digest": None,
+        "base_revision": None,
+        "write_scope": [f"{scope}/workspace"],
+        "supersedes_decision_ids": [],
+        "source_lineage": [
+            {"source": "accepted-decision", "decision_id": "decision-direction"}
+        ],
+    }
+
+
+PARAGRAPHS = [
+    {
+        "paragraph_id": 1,
+        "paragraph_index": 1,
+        "level": 1,
+        "parent_id": None,
+        "name": "反常识开场",
+        "content_range": {"min_chars": 80, "max_chars": 130},
+        "theme": "AI 不是答案机",
+        "form": "场景反转",
+        "function": "建立冲突",
+        "feeling": "惊讶",
+        "theme_elements": [{"name": "认知落差"}],
+        "form_elements": [{"name": "短问句"}],
+        "function_elements": [{"name": "钩子"}],
+        "feeling_elements": [{"name": "紧迫"}],
+        "description": "用失败场景破题",
+        "full_description": "从团队盲目堆 Agent 的失败现场切入,指出流程边界比模型数量更重要。",
+        "is_active": True,
+    },
+    {
+        "paragraph_id": 2,
+        "paragraph_index": 2,
+        "level": 2,
+        "parent_id": 1,
+        "name": "三个边界",
+        "content_range": {"min_chars": 180, "max_chars": 260},
+        "theme": "合同、验证、发布",
+        "form": "递进清单",
+        "function": "交付方法",
+        "feeling": "笃定",
+        "theme_elements": [{"name": "可验证"}],
+        "form_elements": [{"name": "三段式"}],
+        "function_elements": [{"name": "解释"}],
+        "feeling_elements": [{"name": "掌控"}],
+        "description": "拆解三层工程边界",
+        "full_description": "说明冻结合同、独立验证与原子发布如何形成可恢复的内容生产线。",
+        "is_active": True,
+    },
+]
+
+ELEMENTS = [
+    {
+        "element_id": 1,
+        "name": "失败现场",
+        "dimension_primary": "实质",
+        "dimension_secondary": "案例",
+        "commonality_analysis": {"pattern": "先展示代价"},
+        "topic_support": {"score": 0.94, "reason": "直接支撑流程治理主题"},
+        "weight_score": {"value": 9.2, "scale": 10},
+        "support_elements": [{"type": "quote", "ref": "evidence-pattern-1"}],
+        "is_active": True,
+    },
+    {
+        "element_id": 2,
+        "name": "三层护栏",
+        "dimension_primary": "形式",
+        "dimension_secondary": "结构",
+        "commonality_analysis": {"pattern": "三段递进"},
+        "topic_support": {"score": 0.91, "reason": "形成清晰记忆点"},
+        "weight_score": {"value": 8.8, "scale": 10},
+        "support_elements": [{"type": "strategy", "ref": "strategy-3"}],
+        "is_active": True,
+    },
+]
+
+LINKS = [{"paragraph_id": 1, "element_id": 1}, {"paragraph_id": 2, "element_id": 2}]
+
+
+def evidence_payload(kind: str, index: int) -> dict[str, Any]:
+    return {
+        "schema_version": "evidence-record/v1",
+        "evidence_id": f"evidence-{kind}-{index}",
+        "source_type": kind,
+        "tool_name": f"retrieve_{kind.replace('-', '_')}",
+        "query": {"topic_id": 7701, "limit": 12, "intent": "AI Agent 内容生产流程"},
+        "source_refs": [f"{kind}://fake/source/{index}"],
+        "raw_artifact_ref": f"script-build://raw-artifacts/sha256/{digest(kind)}",
+        "summary": f"{kind} 返回了可用于方向与结构决策的高置信证据。",
+        "supports": ["goal-retention", "criterion-evidence"],
+        "confidence": "high",
+        "limitations": ["fake 数据不代表真实检索覆盖率"],
+        "created_at": STAMP,
+    }
+
+
+def direction_payload() -> dict[str, Any]:
+    return {
+        "schema_version": "script-direction/v1",
+        "goals": [
+            {
+                "goal_id": "goal-retention",
+                "statement": "用工程事故切入,讲清智能创作系统的三层边界",
+                "rationale": "目标用户需要可落地的流程而非泛泛 AI 概念。",
+            }
+        ],
+        "criteria": [
+            {"criterion_id": "criterion-evidence", "description": "关键主张均可追溯"}
+        ],
+        "domain_criteria": [
+            {
+                "criterion_id": "criterion-fencing",
+                "description": "正确描述 fencing 与原子发布",
+            }
+        ],
+        "topic_refs": ["script-build://inputs/topics/7701"],
+        "persona_refs": ["script-build://inputs/personas/230"],
+        "strategy_refs": ["script-build://inputs/strategies/3"],
+        "evidence_refs": [artifact_uri(101), artifact_uri(102)],
+        "legacy_markdown": "# 创作方向\n用一个失控的多 Agent 项目开场,落到合同、验证、发布三层护栏。",
+    }
+
+
+def structured_payload() -> dict[str, Any]:
+    return {
+        "schema_version": "structured-script/v1",
+        "direction_ref": artifact_uri(110),
+        "input_closure_digest": wire_digest("structured-input-closure"),
+        "paragraphs": PARAGRAPHS,
+        "elements": ELEMENTS,
+        "paragraph_element_links": LINKS,
+        "source_artifact_refs": [
+            artifact_uri(120),
+            artifact_uri(121),
+            artifact_uri(122),
+        ],
+        "evidence_refs": [
+            artifact_uri(101),
+            artifact_uri(102),
+            artifact_uri(103),
+            artifact_uri(104),
+        ],
+        "acceptance_notes": ["采用候选 A 的冲突开场", "保留候选 B 的三层递进结构"],
+    }
+
+
+def legacy_canonical_payload() -> dict[str, Any]:
+    paragraph_keys = {
+        item["paragraph_id"]: item["paragraph_index"] for item in PARAGRAPHS
+    }
+    paragraphs = []
+    for item in PARAGRAPHS:
+        paragraphs.append(
+            {
+                "logical_key": item["paragraph_index"],
+                "paragraph_index": item["paragraph_index"],
+                "level": item["level"],
+                "parent_logical_key": (
+                    paragraph_keys[item["parent_id"]]
+                    if item["parent_id"] is not None
+                    else None
+                ),
+                "name": item["name"],
+                "content_range": item["content_range"],
+                "theme": item["theme"],
+                "form": item["form"],
+                "function": item["function"],
+                "feeling": item["feeling"],
+                "theme_elements": item["theme_elements"],
+                "form_elements": item["form_elements"],
+                "function_elements": item["function_elements"],
+                "feeling_elements": item["feeling_elements"],
+                "description": item["description"],
+                "full_description": item["full_description"],
+            }
+        )
+    element_keys: dict[int, str] = {}
+    elements = []
+    for item in ELEMENTS:
+        key = canonical_digest(
+            [item["dimension_primary"], item["dimension_secondary"], item["name"]]
+        )
+        element_keys[item["element_id"]] = key
+        elements.append(
+            {
+                "logical_key": key,
+                "name": item["name"],
+                "dimension_primary": item["dimension_primary"],
+                "dimension_secondary": item["dimension_secondary"],
+                "commonality_analysis": item["commonality_analysis"],
+                "topic_support": item["topic_support"],
+                "weight_score": item["weight_score"],
+                "support_elements": item["support_elements"],
+            }
+        )
+    links = [
+        {
+            "paragraph_logical_key": paragraph_keys[item["paragraph_id"]],
+            "element_logical_key": element_keys[item["element_id"]],
+        }
+        for item in LINKS
+    ]
+    paragraphs.sort(key=lambda item: int(item["logical_key"]))
+    elements.sort(key=lambda item: str(item["logical_key"]))
+    links.sort(
+        key=lambda item: (
+            int(item["paragraph_logical_key"]),
+            str(item["element_logical_key"]),
+        )
+    )
+    return {
+        "schema_version": "legacy-projection-canonical/v1",
+        "direction": "用失控案例讲清合同、验证与原子发布。",
+        "summary": "以事故切入,完整解释智能创作系统的合同、验证和原子发布。",
+        "paragraph_count": len(paragraphs),
+        "element_count": len(elements),
+        "link_count": len(links),
+        "paragraphs": [normalize_json(item) for item in paragraphs],
+        "elements": [normalize_json(item) for item in elements],
+        "paragraph_element_links": [normalize_json(item) for item in links],
+    }
+
+
+def root_input_closure_digest() -> str:
+    return canonical_wire_digest(
+        {
+            "schema_version": "root-delivery-input-closure/v1",
+            "direction": {
+                "ref": artifact_uri(110),
+                "digest": ARTIFACT_DIGESTS[110],
+            },
+            "candidate_portfolio": {
+                "ref": artifact_uri(150),
+                "digest": ARTIFACT_DIGESTS[150],
+            },
+            "structured_script": {
+                "ref": artifact_uri(140),
+                "digest": ARTIFACT_DIGESTS[140],
+            },
+        }
+    )
+
+
+def record(model_name: str, payload: dict[str, Any]) -> RecordEnvelope:
+    expected = set(SCHEMA_CATALOG[model_name])
+    actual = set(payload)
+    if actual != expected:
+        missing = sorted(expected - actual)
+        extra = sorted(actual - expected)
+        raise AssertionError(
+            f"{model_name} fake field mismatch: missing={missing}, extra={extra}"
+        )
+    schema_version = payload.get("schema_version")
+    return RecordEnvelope(
+        model_name=model_name,
+        schema_version=str(schema_version) if schema_version is not None else None,
+        payload=payload,
+    )
+
+
+@dataclass
+class Builder:
+    nodes: list[FlowNode]
+    edges: list[FlowEdge]
+    sequence: int = 0
+
+    def add(
+        self,
+        node_id: str,
+        node_type: str,
+        phase: str,
+        lane: str,
+        title: str,
+        subtitle: str,
+        status: str,
+        x: int,
+        y: int,
+        model_name: str,
+        payload: dict[str, Any],
+        *,
+        agent_role: str | None = None,
+        task_kind: str | None = None,
+        tags: list[str] | None = None,
+        metrics: list[tuple[str, str]] | None = None,
+    ) -> str:
+        self.sequence += 1
+        self.nodes.append(
+            FlowNode(
+                id=node_id,
+                node_type=node_type,  # type: ignore[arg-type]
+                phase=phase,  # type: ignore[arg-type]
+                lane=lane,
+                title=title,
+                subtitle=subtitle,
+                status=status,
+                agent_role=agent_role,
+                task_kind=task_kind,
+                sequence=self.sequence,
+                x=x,
+                y=y,
+                tags=tags or [],
+                metrics=[
+                    Metric(label=label, value=value) for label, value in (metrics or [])
+                ],
+                record=record(model_name, payload),
+            )
+        )
+        return node_id
+
+    def link(
+        self,
+        source: str,
+        target: str,
+        label: str | None = None,
+        kind: str = "control",
+        animated: bool = False,
+    ) -> None:
+        self.edges.append(
+            FlowEdge(
+                id=f"edge:{source}:{target}",
+                source=source,
+                target=target,
+                label=label,
+                kind=kind,
+                animated=animated,  # type: ignore[arg-type]
+            )
+        )
+
+
+def _pipeline_unit(
+    b: Builder,
+    *,
+    prefix: str,
+    phase: str,
+    lane: str,
+    kind: str,
+    output_schema: str,
+    objective: str,
+    artifact_model: str,
+    artifact_payload: dict[str, Any],
+    x: int,
+    y: int,
+    index: int,
+    artifact_version: int,
+    preset: str,
+    validator_preset: str = "script_candidate_validator",
+) -> tuple[str, str]:
+    artifact_kind = {
+        "EvidenceRecordV1": "evidence",
+        "ScriptDirectionArtifactV1": "direction",
+        "StructureArtifactV1": "structure",
+        "ParagraphArtifactV1": "paragraph",
+        "ElementSetArtifactV1": "element_set",
+        "ComparisonArtifactV1": "comparison",
+        "StructuredScriptArtifactV1": "structured_script",
+        "CandidatePortfolioArtifactV1": "candidate_portfolio",
+        "RootDeliveryManifestV1": "root_delivery_manifest",
+    }[artifact_model]
+    ARTIFACT_KINDS[artifact_version] = artifact_kind
+    ARTIFACT_DIGESTS[artifact_version] = canonical_wire_digest(artifact_payload)
+    contract_id = b.add(
+        f"{prefix}:contract",
+        "contract",
+        phase,
+        lane,
+        "冻结 TaskContract",
+        objective,
+        "frozen",
+        x,
+        y,
+        "ScriptTaskContractV1",
+        contract_payload(prefix, objective, kind, output_schema),
+        task_kind=kind,
+        tags=["immutable", output_schema],
+        metrics=[("spec", "v1"), ("预算", "32k tokens")],
+    )
+    op_id = b.add(
+        f"{prefix}:operation",
+        "operation",
+        phase,
+        lane,
+        "Durable Operation",
+        "dispatch · execution_epoch=1",
+        "completed",
+        x + 300,
+        y,
+        "OperationView",
+        operation_payload(prefix, index),
+        task_kind=kind,
+        tags=["durable", "fenced"],
+        metrics=[("耗时", f"{2 + index % 4}.2s")],
+    )
+    attempt_id = b.add(
+        f"{prefix}:attempt",
+        "attempt",
+        phase,
+        lane,
+        f"{preset} Attempt",
+        "受控工具白名单内执行",
+        "submitted",
+        x + 600,
+        y,
+        "AttemptView",
+        attempt_payload(prefix, preset, artifact_version, index),
+        agent_role="worker",
+        task_kind=kind,
+        tags=["worker", "snapshot"],
+        metrics=[("tokens", str(1840 + index * 31)), ("attempt", "1 / 4")],
+    )
+    artifact_id = b.add(
+        f"{prefix}:artifact",
+        "artifact",
+        phase,
+        lane,
+        artifact_model.removesuffix("V1"),
+        f"{artifact_uri(artifact_version)} · canonical",
+        "frozen",
+        x + 900,
+        y,
+        artifact_model,
+        artifact_payload,
+        task_kind=kind,
+        tags=["frozen", "sha256"],
+        metrics=[("version", str(artifact_version)), ("state", "frozen")],
+    )
+    validation_id = b.add(
+        f"{prefix}:validation",
+        "validation",
+        phase,
+        lane,
+        f"{validator_preset} Validation",
+        "独立快照验证 · passed",
+        "passed",
+        x + 1200,
+        y,
+        "ValidationView",
+        validation_payload(prefix, validator_preset, index),
+        agent_role="validator",
+        task_kind=kind,
+        tags=["independent", "passed"],
+        metrics=[("verdict", "passed"), ("缺陷", "0")],
+    )
+    decision_id = b.add(
+        f"{prefix}:decision",
+        "decision",
+        phase,
+        lane,
+        "Planner ACCEPT",
+        "显式接受,不由 Validator 自动发布",
+        "accepted",
+        x + 1500,
+        y,
+        "PlannerDecisionView",
+        decision_payload(prefix, index),
+        agent_role="planner",
+        task_kind=kind,
+        tags=["authority", "accept"],
+        metrics=[("action", "ACCEPT")],
+    )
+    b.link(contract_id, op_id, "dispatch")
+    b.link(op_id, attempt_id, "worker", animated=True)
+    b.link(attempt_id, artifact_id, "freeze", "artifact")
+    b.link(artifact_id, validation_id, "snapshot", "validation")
+    b.link(validation_id, decision_id, "passed", "validation")
+    return contract_id, decision_id
+
+
+def build_graph() -> RunGraph:
+    b = Builder([], [])
+
+    build = b.add(
+        "run:record",
+        "input",
+        "phase-1",
+        "mission",
+        "Script Build #10001",
+        "当前 Build 业务记录",
+        "success",
+        80,
+        150,
+        "ScriptBuildRecord",
+        {
+            "id": RUN_ID,
+            "execution_id": 88001,
+            "topic_build_id": 67001,
+            "topic_id": 7701,
+            "agent_type": "AigcAgent",
+            "agent_config": {"model": "deterministic-fake-model", "temperature": 0},
+            "data_source_url": None,
+            "status": "success",
+            "reson_trace_id": ROOT_TRACE_ID,
+            "is_deleted": False,
+            "is_favorited": True,
+            "summary": "完成一篇讲解智能创作系统工程边界的结构化脚本。",
+            "error_message": None,
+            "script_direction": "用失控案例讲清合同、验证与原子发布。",
+            "build_workflow": {
+                "checkpoint": "PHASE_THREE_PUBLISHED",
+                "schema_version": 3,
+            },
+            "paragraph_count": 2,
+            "element_count": 2,
+            "input_tokens": 28420,
+            "output_tokens": 9360,
+            "cost_usd": 0.1842,
+            "strategies_config": {"always_on": [3], "on_demand": [8]},
+            "start_time": START,
+            "end_time": STAMP,
+        },
+        tags=["fake", "success"],
+        metrics=[("段落", "2"), ("元素", "2"), ("成本", "$0.1842")],
+    )
+    input_payload = input_canonical_payload()
+    input_sha256 = canonical_digest(input_payload)
+    snapshot = b.add(
+        "mission:input-snapshot",
+        "input",
+        "phase-1",
+        "mission",
+        "冻结 InputSnapshot",
+        "选题 · 账号 · 人设 · 策略 · Prompt 清单",
+        "frozen",
+        380,
+        150,
+        "ScriptBuildInputSnapshotV1",
+        {
+            "snapshot_id": "301",
+            "script_build_id": input_payload["script_build_id"],
+            "execution_id": input_payload["execution_id"],
+            "topic_build_id": input_payload["topic_build_id"],
+            "topic_id": input_payload["topic_id"],
+            "topic": input_payload["topic"],
+            "account": input_payload["account"],
+            "persona_points": input_payload["persona_points"],
+            "section_patterns": input_payload["section_patterns"],
+            "strategies": input_payload["strategies"],
+            "prompt_manifest": input_payload["prompt_manifest"],
+            "datasource_manifest": input_payload["datasource_manifest"],
+            "model_manifest": input_payload["model_manifest"],
+            "canonical_sha256": f"sha256:{input_sha256}",
+            "created_at": START,
+            "parent_snapshot_id": None,
+            "parent_snapshot_sha256": None,
+            "business_input_sha256": None,
+        },
+        tags=["immutable", "v1"],
+        metrics=[("persona", "1"), ("strategy", "1"), ("prompt", "1")],
+    )
+    snapshot_row = b.add(
+        "storage:input-snapshot-row",
+        "artifact",
+        "phase-1",
+        "storage",
+        "InputSnapshot 持久化行",
+        "script_build_input_snapshot · version=1",
+        "frozen",
+        80,
+        500,
+        "InputSnapshotRow",
+        {
+            "id": 301,
+            "script_build_id": RUN_ID,
+            "version": 1,
+            "canonical_json": input_payload,
+            "canonical_sha256": input_sha256,
+            "prompt_manifest_json": input_payload["prompt_manifest"],
+            "schema_version": "script-build-input/v1",
+            "created_at": START,
+        },
+        tags=["physical-row", "immutable"],
+        metrics=[("table", "input_snapshot"), ("version", "1")],
+    )
+    binding = b.add(
+        "mission:binding",
+        "checkpoint",
+        "phase-1",
+        "mission",
+        "Mission Binding",
+        "Build ↔ Root Trace ↔ Snapshot 唯一绑定",
+        "bound",
+        680,
+        150,
+        "MissionBinding",
+        {
+            "binding_id": 501,
+            "script_build_id": RUN_ID,
+            "root_trace_id": ROOT_TRACE_ID,
+            "input_snapshot_id": 301,
+            "active_direction_artifact_version_id": 110,
+            "accepted_root_artifact_version_id": 160,
+            "engine_version": "host-phase3/1",
+            "schema_version": "3",
+            "created_at": START,
+            "updated_at": STAMP,
+            "owner_epoch": 1,
+            "stop_epoch": 0,
+            "owner_instance_id": "host-fake-01",
+            "owner_acquired_at": START,
+        },
+        tags=["unique", "binding"],
+        metrics=[("owner epoch", "1"), ("stop epoch", "0")],
+    )
+    owner = b.add(
+        "mission:owner",
+        "checkpoint",
+        "phase-1",
+        "mission",
+        "Owner Lease + Fencing",
+        "fd lock + owner_epoch CAS",
+        "active",
+        980,
+        150,
+        "MissionOwnerToken",
+        {
+            "script_build_id": RUN_ID,
+            "root_trace_id": ROOT_TRACE_ID,
+            "owner_epoch": 1,
+            "stop_epoch": 0,
+            "owner_instance_id": "host-fake-01",
+        },
+        tags=["lease", "fenced"],
+        metrics=[("epoch", "1"), ("stop", "0")],
+    )
+    b.link(build, snapshot, "freeze")
+    b.link(snapshot, snapshot_row, "persist", "artifact")
+    b.link(snapshot, binding, "bind")
+    b.link(binding, owner, "acquire")
+
+    direction_contract, direction_decision = _pipeline_unit(
+        b,
+        prefix="direction",
+        phase="phase-1",
+        lane="direction",
+        kind="direction",
+        output_schema="script-direction/v1",
+        objective="确定脚本目标、标准与证据闭包",
+        artifact_model="ScriptDirectionArtifactV1",
+        artifact_payload=direction_payload(),
+        x=380,
+        y=500,
+        index=1,
+        artifact_version=110,
+        preset="script_direction_worker",
+    )
+    b.link(owner, direction_contract, "plan")
+    direction_publication = b.add(
+        "direction:publication",
+        "publication",
+        "phase-1",
+        "direction",
+        "Direction Publication",
+        "独立 direction publication identity",
+        "published",
+        2180,
+        500,
+        "Publication",
+        {
+            "publication_id": 701,
+            "script_build_id": RUN_ID,
+            "publication_type": "direction",
+            "accept_decision_id": "decision-direction",
+            "artifact_version_id": 110,
+            "expected_sha256": ARTIFACT_DIGESTS[110],
+            "state": "published",
+            "attempt_count": 1,
+            "publication_revision": 1,
+            "last_error_code": None,
+            "last_error_summary": None,
+            "created_at": START,
+            "updated_at": STAMP,
+            "published_at": STAMP,
+        },
+        tags=["published", "idempotent"],
+        metrics=[("revision", "1")],
+    )
+    b.link(direction_decision, direction_publication, "publish", "publication")
+
+    retrieval_specs = [
+        ("pattern", "pattern-retrieval", "Pattern 结构模式", 180, 2, 101),
+        ("decode", "decode-retrieval", "Case 解构证据", 500, 3, 102),
+        ("knowledge", "knowledge-retrieval", "知识库事实", 820, 4, 103),
+        ("external", "external-retrieval", "外部检索证据", 1140, 5, 104),
+    ]
+    retrieval_decisions: list[str] = []
+    for name, kind, label, y, index, version in retrieval_specs:
+        contract_id, decision_id = _pipeline_unit(
+            b,
+            prefix=f"retrieval:{name}",
+            phase="phase-2",
+            lane=f"retrieval-{name}",
+            kind=kind,
+            output_schema="evidence-record/v1",
+            objective=f"检索并冻结{label}",
+            artifact_model="EvidenceRecordV1",
+            artifact_payload=evidence_payload(kind, index),
+            x=2540,
+            y=y,
+            index=index,
+            artifact_version=version,
+            preset="script_retrieval_worker",
+            validator_preset="script_retrieval_validator",
+        )
+        b.link(direction_decision, contract_id, "accepted direction")
+        retrieval_decisions.append(decision_id)
+
+    structure_artifact = {
+        "schema_version": "structure-artifact/v1",
+        **lineage("script-build://scopes/structure-a"),
+        "paragraphs": PARAGRAPHS,
+    }
+    structure_contract, structure_decision = _pipeline_unit(
+        b,
+        prefix="structure:a",
+        phase="phase-2",
+        lane="creative-a",
+        kind="structure",
+        output_schema="structure-artifact/v1",
+        objective="生成冲突递进式脚本结构候选 A",
+        artifact_model="StructureArtifactV1",
+        artifact_payload=structure_artifact,
+        x=4540,
+        y=1500,
+        index=6,
+        artifact_version=120,
+        preset="script_structure_worker",
+    )
+    for item in retrieval_decisions:
+        b.link(item, structure_contract, "evidence", "artifact")
+
+    paragraph_artifact = {
+        "schema_version": "paragraph-artifact/v1",
+        **lineage("script-build://scopes/paragraph-a"),
+        "paragraphs": PARAGRAPHS,
+        "elements": [],
+        "paragraph_element_links": [],
+        "change_manifest": {"created": [1, 2], "updated": [], "deleted": []},
+    }
+    paragraph_contract, paragraph_decision = _pipeline_unit(
+        b,
+        prefix="paragraph:a",
+        phase="phase-2",
+        lane="creative-a",
+        kind="paragraph",
+        output_schema="paragraph-artifact/v1",
+        objective="细化候选 A 的段落内容与父子关系",
+        artifact_model="ParagraphArtifactV1",
+        artifact_payload=paragraph_artifact,
+        x=4840,
+        y=1810,
+        index=7,
+        artifact_version=121,
+        preset="script_paragraph_worker",
+    )
+    b.link(structure_decision, paragraph_contract, "base structure", "artifact")
+
+    element_artifact = {
+        "schema_version": "element-set-artifact/v1",
+        **lineage("script-build://scopes/elements-a"),
+        "paragraphs": PARAGRAPHS,
+        "elements": ELEMENTS,
+        "paragraph_element_links": LINKS,
+        "change_manifest": {"created": [1, 2], "linked": 2},
+    }
+    element_contract, element_decision = _pipeline_unit(
+        b,
+        prefix="elements:a",
+        phase="phase-2",
+        lane="creative-a",
+        kind="element-set",
+        output_schema="element-set-artifact/v1",
+        objective="生成元素集合并闭合 Paragraph–Element Link",
+        artifact_model="ElementSetArtifactV1",
+        artifact_payload=element_artifact,
+        x=5140,
+        y=1500,
+        index=8,
+        artifact_version=122,
+        preset="script_element_set_worker",
+    )
+    b.link(paragraph_decision, element_contract, "paragraph closure", "artifact")
+
+    comparison_artifact = {
+        "schema_version": "comparison-artifact/v1",
+        **lineage("script-build://scopes/comparison"),
+        "candidate_artifact_refs": [artifact_uri(121), artifact_uri(122)],
+        "criterion_results": [
+            {
+                "criterion_id": "content_complete",
+                "candidate_results": [
+                    {
+                        "artifact_ref": artifact_uri(121),
+                        "verdict": "passed",
+                        "reason": "段落闭合",
+                    },
+                    {
+                        "artifact_ref": artifact_uri(122),
+                        "verdict": "passed",
+                        "reason": "元素关联闭合",
+                    },
+                ],
+            }
+        ],
+        "conflicts": ["候选 A 更强冲突,候选 B 信息密度更高"],
+        "recommendation": artifact_uri(122),
+    }
+    compare_contract, compare_decision = _pipeline_unit(
+        b,
+        prefix="compare:ab",
+        phase="phase-2",
+        lane="convergence",
+        kind="compare",
+        output_schema="comparison-artifact/v1",
+        objective="按同一标准比较候选并给出建议",
+        artifact_model="ComparisonArtifactV1",
+        artifact_payload=comparison_artifact,
+        x=5440,
+        y=1810,
+        index=9,
+        artifact_version=130,
+        preset="script_comparison_worker",
+    )
+    b.link(paragraph_decision, compare_contract, "candidate A", "artifact")
+    b.link(element_decision, compare_contract, "candidate B", "artifact")
+
+    compose_contract, compose_decision = _pipeline_unit(
+        b,
+        prefix="compose:final",
+        phase="phase-2",
+        lane="convergence",
+        kind="compose",
+        output_schema="structured-script/v1",
+        objective="按 accepted closure 合成 StructuredScript",
+        artifact_model="StructuredScriptArtifactV1",
+        artifact_payload=structured_payload(),
+        x=5740,
+        y=1500,
+        index=10,
+        artifact_version=140,
+        preset="script_compose_worker",
+    )
+    b.link(compare_decision, compose_contract, "comparison accepted")
+
+    alternate_script = structured_payload()
+    alternate_script["acceptance_notes"] = [
+        "保留候选 B 的信息密度",
+        "作为 Portfolio 中未采用但完整闭合的备选脚本",
+    ]
+    alternate_contract, alternate_decision = _pipeline_unit(
+        b,
+        prefix="compose:alternate",
+        phase="phase-2",
+        lane="convergence-alternate",
+        kind="compose",
+        output_schema="structured-script/v1",
+        objective="生成完整备选 StructuredScript 供 Portfolio 显式处置",
+        artifact_model="StructuredScriptArtifactV1",
+        artifact_payload=alternate_script,
+        x=5740,
+        y=2110,
+        index=13,
+        artifact_version=141,
+        preset="script_compose_worker",
+    )
+    b.link(compare_decision, alternate_contract, "alternate candidate")
+
+    portfolio_payload = {
+        "schema_version": "candidate-portfolio/v1",
+        "adopted_structured_script_ref": artifact_uri(140),
+        "candidate_structured_script_refs": [artifact_uri(140), artifact_uri(141)],
+        "accepted_decision_ids": ["decision-compose:final"],
+        "superseded_decision_ids": [],
+        "rejected_or_held_decision_ids": ["decision-compose:alternate"],
+        "input_closure_digest": wire_digest("portfolio-input-closure"),
+        "unresolved_defects": [],
+        "compose_order": ["decision-compose:final"],
+    }
+    portfolio_contract, portfolio_decision = _pipeline_unit(
+        b,
+        prefix="portfolio",
+        phase="phase-2",
+        lane="convergence",
+        kind="candidate-portfolio",
+        output_schema="candidate-portfolio/v1",
+        objective="恰好采用一个 StructuredScript 并处置其余候选",
+        artifact_model="CandidatePortfolioArtifactV1",
+        artifact_payload=portfolio_payload,
+        x=6040,
+        y=1810,
+        index=11,
+        artifact_version=150,
+        preset="script_portfolio_worker",
+    )
+    b.link(compose_decision, portfolio_contract, "candidate closure")
+    b.link(alternate_decision, portfolio_contract, "candidate closure")
+    checkpoint = b.add(
+        "phase2:checkpoint",
+        "checkpoint",
+        "phase-2",
+        "boundary",
+        "Phase 2 Checkpoint",
+        "PHASE_TWO_CANDIDATE_PORTFOLIO_READY",
+        "partial",
+        7840,
+        1810,
+        "TaskView",
+        task_payload("task-root", "交付最终脚本", "root-delivery", parent=None),
+        agent_role="planner",
+        tags=["partial", "closure-ready"],
+        metrics=[
+            ("accepted direction", "1"),
+            ("accepted portfolio", "1"),
+            ("active op", "0"),
+        ],
+    )
+    b.link(portfolio_decision, checkpoint, "closure ready")
+
+    migration = b.add(
+        "phase3:migration",
+        "checkpoint",
+        "phase-3",
+        "continuation",
+        "Policy Continuation",
+        "script-build-phase-three/v1 + 唯一 migration event",
+        "migrated",
+        8200,
+        180,
+        "TaskSpecView",
+        task_spec("执行 Root Delivery 并发布", "root-delivery"),
+        agent_role="planner",
+        task_kind="root-delivery",
+        tags=["reentrant", "policy-v1"],
+        metrics=[("message parent", "closed"), ("migration", "unique")],
+    )
+    b.link(checkpoint, migration, "advance")
+
+    canonical_payload = legacy_canonical_payload()
+    canonical_sha256 = canonical_wire_digest(canonical_payload)
+    root_manifest_payload = {
+        "schema_version": "root-delivery-manifest/v1",
+        "direction_ref": artifact_uri(110),
+        "candidate_portfolio_ref": artifact_uri(150),
+        "structured_script_ref": artifact_uri(140),
+        "input_closure_digest": root_input_closure_digest(),
+        "legacy_projection_digest": canonical_sha256,
+        "build_summary": "以事故切入,完整解释智能创作系统的合同、验证和原子发布。",
+        "blocking_defects": [],
+    }
+    root_contract, root_decision = _pipeline_unit(
+        b,
+        prefix="root-delivery",
+        phase="phase-3",
+        lane="root-delivery",
+        kind="root-delivery",
+        output_schema="root-delivery-manifest/v1",
+        objective="锁定 Direction、Portfolio、StructuredScript 并生成唯一交付 Manifest",
+        artifact_model="RootDeliveryManifestV1",
+        artifact_payload=root_manifest_payload,
+        x=8200,
+        y=500,
+        index=12,
+        artifact_version=160,
+        preset="script_root_worker",
+        validator_preset="script_root_validator",
+    )
+    b.link(migration, root_contract, "host-frozen contract")
+
+    artifact_version_row = b.add(
+        "storage:artifact-version-160",
+        "artifact",
+        "phase-3",
+        "storage",
+        "ArtifactVersion #160",
+        "root_delivery_manifest · frozen canonical_json",
+        "frozen",
+        9100,
+        1020,
+        "ArtifactVersionRow",
+        {
+            "id": 160,
+            "script_build_id": RUN_ID,
+            "task_id": "root-delivery",
+            "attempt_id": "attempt-root-delivery",
+            "spec_version": 1,
+            "artifact_type": "root_delivery_manifest",
+            "legacy_branch_id": None,
+            "base_revision": None,
+            "parent_artifact_version_id": None,
+            "canonical_json": root_manifest_payload,
+            "canonical_sha256": ARTIFACT_DIGESTS[160].removeprefix("sha256:"),
+            "state": "frozen",
+            "created_at": STAMP,
+            "frozen_at": STAMP,
+            "published_at": None,
+        },
+        task_kind="root-delivery",
+        tags=["physical-row", "artifact-version"],
+        metrics=[("artifact_type", "manifest"), ("branch", "null")],
+    )
+    b.link("root-delivery:artifact", artifact_version_row, "persist", "artifact")
+
+    canonical = b.add(
+        "publication:canonical-dry-run",
+        "readback",
+        "phase-3",
+        "root-delivery",
+        "Legacy Projection Dry-run",
+        "物理 ID 无关 canonical graph",
+        "matched",
+        9100,
+        820,
+        "LegacyProjectionCanonicalV1",
+        canonical_payload,
+        tags=["canonical", "sha256"],
+        metrics=[("digest", canonical_sha256[:12]), ("links", "2")],
+    )
+    b.link("root-delivery:attempt", canonical, "dry run", "artifact")
+    b.link(canonical, "root-delivery:artifact", "digest", "artifact")
+
+    journal = b.add(
+        "http:finalize-command",
+        "checkpoint",
+        "delivery",
+        "http",
+        "HTTP Idempotency Journal",
+        "POST /finalize · bounded response replay",
+        "completed",
+        10100,
+        180,
+        "HttpCommandRecord",
+        {
+            "id": 801,
+            "principal_scope_sha256": digest("tenant:fake"),
+            "route_family": "script-build-finalize",
+            "idempotency_key": "fake-finalize-10001",
+            "request_fingerprint": digest("finalize-request"),
+            "preallocated_root_trace_id": ROOT_TRACE_ID,
+            "resource_id": RUN_ID,
+            "state": "completed",
+            "response_status": 200,
+            "response_json": {"script_build_id": RUN_ID, "state": "published"},
+            "created_at": START,
+            "updated_at": STAMP,
+        },
+        tags=["idempotent", "replay"],
+        metrics=[("HTTP", "200"), ("state", "completed")],
+    )
+    b.link(root_decision, journal, "finalize")
+
+    publication = b.add(
+        "publication:identity",
+        "publication",
+        "delivery",
+        "publisher",
+        "Final Publication Identity",
+        "(script_build_id, publication_type) 唯一",
+        "published",
+        10400,
+        500,
+        "Publication",
+        {
+            "publication_id": 702,
+            "script_build_id": RUN_ID,
+            "publication_type": "final",
+            "accept_decision_id": "decision-root-delivery",
+            "artifact_version_id": 160,
+            "expected_sha256": ARTIFACT_DIGESTS[160],
+            "state": "published",
+            "attempt_count": 1,
+            "publication_revision": 1,
+            "last_error_code": None,
+            "last_error_summary": None,
+            "created_at": START,
+            "updated_at": STAMP,
+            "published_at": STAMP,
+        },
+        tags=["unique", "final"],
+        metrics=[("attempt", "1"), ("revision", "1")],
+    )
+    b.link(journal, publication, "reserve", "publication")
+
+    tx_steps = [
+        ("locks", "固定锁序", "binding → build → publication → content", 10700, 260),
+        ("clear", "清理 branch 0", "Link → Paragraph → Element", 11000, 260),
+        ("paragraphs", "插入 Paragraph", "父段 → 子段 · logical map", 11300, 260),
+        ("elements", "插入 Element", "natural key → physical ID", 11600, 260),
+        ("links", "插入 Link", "paragraph/element logical pair", 11900, 260),
+        (
+            "readback",
+            "同 Session canonical 回读",
+            "digest 必须与 Manifest 一致",
+            12200,
+            260,
+        ),
+        (
+            "pointers",
+            "发布指针与状态",
+            "accepted pointer + artifacts + build",
+            12500,
+            260,
+        ),
+        ("commit", "单次 Commit", "全部成功或全部 rollback", 12800, 260),
+    ]
+    physical_rows: dict[str, tuple[str, dict[str, Any]]] = {
+        "paragraphs": (
+            "ParagraphRow",
+            {
+                "id": 9001,
+                "script_build_id": RUN_ID,
+                "branch_id": 0,
+                "base_ref_id": None,
+                "paragraph_index": 1,
+                "level": 1,
+                "parent_id": None,
+                "name": PARAGRAPHS[0]["name"],
+                "content_range": PARAGRAPHS[0]["content_range"],
+                "theme": PARAGRAPHS[0]["theme"],
+                "form": PARAGRAPHS[0]["form"],
+                "function": PARAGRAPHS[0]["function"],
+                "feeling": PARAGRAPHS[0]["feeling"],
+                "theme_elements": PARAGRAPHS[0]["theme_elements"],
+                "form_elements": PARAGRAPHS[0]["form_elements"],
+                "function_elements": PARAGRAPHS[0]["function_elements"],
+                "feeling_elements": PARAGRAPHS[0]["feeling_elements"],
+                "description": PARAGRAPHS[0]["description"],
+                "full_description": PARAGRAPHS[0]["full_description"],
+                "is_active": True,
+                "created_at": STAMP,
+                "updated_at": STAMP,
+            },
+        ),
+        "elements": (
+            "ElementRow",
+            {
+                "id": 9101,
+                "script_build_id": RUN_ID,
+                "branch_id": 0,
+                "base_ref_id": None,
+                "name": ELEMENTS[0]["name"],
+                "dimension_primary": ELEMENTS[0]["dimension_primary"],
+                "dimension_secondary": ELEMENTS[0]["dimension_secondary"],
+                "commonality_analysis": ELEMENTS[0]["commonality_analysis"],
+                "topic_support": ELEMENTS[0]["topic_support"],
+                "weight_score": ELEMENTS[0]["weight_score"],
+                "support_elements": ELEMENTS[0]["support_elements"],
+                "is_active": True,
+                "created_at": STAMP,
+                "updated_at": STAMP,
+            },
+        ),
+        "links": (
+            "ParagraphElementLinkRow",
+            {
+                "id": 9201,
+                "script_build_id": RUN_ID,
+                "branch_id": 0,
+                "paragraph_id": 9001,
+                "element_id": 9101,
+            },
+        ),
+    }
+    previous = publication
+    for offset, (key, title, subtitle, x, y) in enumerate(tx_steps, 20):
+        default_record = (
+            "MissionOwnerToken",
+            {
+                "script_build_id": RUN_ID,
+                "root_trace_id": ROOT_TRACE_ID,
+                "owner_epoch": 1,
+                "stop_epoch": 0,
+                "owner_instance_id": "host-fake-01",
+            },
+        )
+        model_name, row_payload = physical_rows.get(key, default_record)
+        node = b.add(
+            f"uow:{key}",
+            "transaction",
+            "delivery",
+            "final-uow",
+            title,
+            subtitle,
+            "committed",
+            x,
+            y,
+            model_name,
+            row_payload,
+            tags=[
+                "one-session",
+                "physical-row" if key in physical_rows else "fenced",
+            ],
+            metrics=[("step", str(offset - 19)), ("tx", "same")],
+        )
+        b.link(previous, node, None, "publication", animated=True)
+        previous = node
+
+    paragraph_two = dict(physical_rows["paragraphs"][1])
+    paragraph_two.update(
+        {
+            "id": 9002,
+            "paragraph_index": 2,
+            "level": 2,
+            "parent_id": 9001,
+            "name": PARAGRAPHS[1]["name"],
+            "content_range": PARAGRAPHS[1]["content_range"],
+            "theme": PARAGRAPHS[1]["theme"],
+            "form": PARAGRAPHS[1]["form"],
+            "function": PARAGRAPHS[1]["function"],
+            "feeling": PARAGRAPHS[1]["feeling"],
+            "theme_elements": PARAGRAPHS[1]["theme_elements"],
+            "form_elements": PARAGRAPHS[1]["form_elements"],
+            "function_elements": PARAGRAPHS[1]["function_elements"],
+            "feeling_elements": PARAGRAPHS[1]["feeling_elements"],
+            "description": PARAGRAPHS[1]["description"],
+            "full_description": PARAGRAPHS[1]["full_description"],
+        }
+    )
+    element_two = dict(physical_rows["elements"][1])
+    element_two.update(
+        {
+            "id": 9102,
+            "name": ELEMENTS[1]["name"],
+            "dimension_primary": ELEMENTS[1]["dimension_primary"],
+            "dimension_secondary": ELEMENTS[1]["dimension_secondary"],
+            "commonality_analysis": ELEMENTS[1]["commonality_analysis"],
+            "topic_support": ELEMENTS[1]["topic_support"],
+            "weight_score": ELEMENTS[1]["weight_score"],
+            "support_elements": ELEMENTS[1]["support_elements"],
+        }
+    )
+    extra_rows = [
+        ("paragraphs", "ParagraphRow #2", "ParagraphRow", paragraph_two, 11300),
+        ("elements", "ElementRow #2", "ElementRow", element_two, 11600),
+        (
+            "links",
+            "ParagraphElementLinkRow #2",
+            "ParagraphElementLinkRow",
+            {
+                "id": 9202,
+                "script_build_id": RUN_ID,
+                "branch_id": 0,
+                "paragraph_id": 9002,
+                "element_id": 9102,
+            },
+            11900,
+        ),
+    ]
+    for key, title, model_name, row_payload, x in extra_rows:
+        extra = b.add(
+            f"uow:{key}-row-2",
+            "transaction",
+            "delivery",
+            "final-uow-rows",
+            title,
+            "完整发布集合 · row 2 / 2",
+            "committed",
+            x,
+            620,
+            model_name,
+            row_payload,
+            tags=["one-session", "physical-row"],
+            metrics=[("row", "2 / 2"), ("branch", "0")],
+        )
+        b.link(f"uow:{key}", extra, "row 2", "artifact")
+
+    result = b.add(
+        "publication:result",
+        "publication",
+        "delivery",
+        "publisher",
+        "Publication Result",
+        "Manifest 与 StructuredScript 已 published",
+        "published",
+        13100,
+        500,
+        "PublicationResult",
+        {
+            "script_build_id": RUN_ID,
+            "publication_id": 702,
+            "publication_type": "final",
+            "state": "published",
+            "legacy_projection_digest": canonical_sha256,
+            "committed": True,
+            "post_commit_readback_ok": True,
+        },
+        tags=["committed", "readback-ok"],
+        metrics=[("build", "success"), ("readback", "OK")],
+    )
+    b.link(previous, result, "commit ack", "publication")
+    legacy = b.add(
+        "legacy:http-readback",
+        "readback",
+        "delivery",
+        "compatibility",
+        "Legacy HTTP Detail",
+        "GET /api/pattern/script_builds/10001",
+        "200",
+        13400,
+        500,
+        "ScriptBuildRecord",
+        {
+            "id": RUN_ID,
+            "execution_id": 88001,
+            "topic_build_id": 67001,
+            "topic_id": 7701,
+            "agent_type": "AigcAgent",
+            "agent_config": {"model": "deterministic-fake-model"},
+            "data_source_url": None,
+            "status": "success",
+            "reson_trace_id": ROOT_TRACE_ID,
+            "is_deleted": False,
+            "is_favorited": True,
+            "summary": "完成一篇讲解智能创作系统工程边界的结构化脚本。",
+            "error_message": None,
+            "script_direction": "用失控案例讲清合同、验证与原子发布。",
+            "build_workflow": {"checkpoint": "PHASE_THREE_PUBLISHED"},
+            "paragraph_count": 2,
+            "element_count": 2,
+            "input_tokens": 28420,
+            "output_tokens": 9360,
+            "cost_usd": 0.1842,
+            "strategies_config": {"always_on": [3], "on_demand": [8]},
+            "start_time": START,
+            "end_time": STAMP,
+        },
+        tags=["compat", "200"],
+        metrics=[("paragraphs", "2"), ("elements", "2")],
+    )
+    b.link(result, legacy, "health readback", "publication")
+
+    frames = [
+        PhaseFrame(
+            id="frame-phase-1",
+            title="Phase 1 · Mission 与方向",
+            subtitle="冻结输入、绑定 Root、方向 Worker / Validator / ACCEPT",
+            x=0,
+            y=0,
+            width=2460,
+            height=1040,
+            tone="coral",
+        ),
+        PhaseFrame(
+            id="frame-phase-2",
+            title="Phase 2 · Evidence 与候选闭包",
+            subtitle="四路取数、结构、段落、元素、比较、合成与 Portfolio",
+            x=2480,
+            y=0,
+            width=5600,
+            height=2400,
+            tone="blue",
+        ),
+        PhaseFrame(
+            id="frame-phase-3",
+            title="Phase 3 · Root Delivery",
+            subtitle="Policy continuation、唯一 Manifest、Root Validation 与 ACCEPT",
+            x=8120,
+            y=0,
+            width=1920,
+            height=1320,
+            tone="purple",
+        ),
+        PhaseFrame(
+            id="frame-delivery",
+            title="Final Delivery · 原子发布",
+            subtitle="HTTP journal、唯一 publication、单 Session / 单 Commit 与旧 API 回读",
+            x=10060,
+            y=0,
+            width=3740,
+            height=1120,
+            tone="green",
+        ),
+    ]
+    summary = RunSummary(
+        script_build_id=RUN_ID,
+        title="AI Agent 项目为什么总在最后一步失控",
+        status="success",
+        checkpoint="PHASE_THREE_PUBLISHED",
+        root_trace_id=ROOT_TRACE_ID,
+        publication_state="published",
+        node_count=len(b.nodes),
+        created_at=START,
+        updated_at=STAMP,
+    )
+    return RunGraph(
+        schema_version="script-build-visualization/v1",
+        data_mode="fake",
+        generated_at=STAMP,
+        run=summary,
+        frames=frames,
+        nodes=b.nodes,
+        edges=b.edges,
+        schema_catalog={key: list(value) for key, value in SCHEMA_CATALOG.items()},
+        enum_catalog={key: list(value) for key, value in ENUM_CATALOG.items()},
+        declared_gaps=[
+            "当前 Host 没有面向可视化的聚合 execution-view API;此后端只返回 fake graph。",
+            "TraceStore 的消息父链、policy migration 与 fencing 审计尚无统一只读投影。",
+            "真实运行中模型 prompt 全量快照、工具调用参数与 Operation 时间线需要专用脱敏接口。",
+            "候选 Artifact 的 logical lineage 可以由现有字段还原,但尚无稳定分页/批量读取 API。",
+        ],
+    )
+
+
+GRAPH = build_graph()
+
+
+def node_detail(node_id: str) -> NodeDetail | None:
+    node = next((item for item in GRAPH.nodes if item.id == node_id), None)
+    if node is None:
+        return None
+    return NodeDetail(
+        data_mode="fake",
+        run_id=RUN_ID,
+        node=node,
+        incoming=[edge for edge in GRAPH.edges if edge.target == node_id],
+        outgoing=[edge for edge in GRAPH.edges if edge.source == node_id],
+    )

+ 0 - 1646
visualization/backend/app/inspector_projection.py

@@ -1,1646 +0,0 @@
-from __future__ import annotations
-
-import re
-from typing import Any
-
-from .business_detail_text import (
-    assessment_text,
-    batches_markdown,
-    branch_task_sections,
-    branch_task_text,
-    branches_markdown,
-    business_text,
-    decisions_markdown,
-    event_business_sections,
-    source_markdown,
-    clean_report,
-)
-from .sanitizer import sanitize
-from .retrieval_detail_projection import (
-    is_retrieval_detail_event,
-    project_retrieval_event,
-)
-
-
-def project_round_detail(
-    script_build_id: int,
-    round_: dict[str, Any],
-    bundle: dict[str, Any] | None = None,
-) -> dict[str, Any]:
-    raw_round = next(
-        (
-            item
-            for item in (bundle or {}).get("rounds", [])
-            if int(item.get("round_index") or 0) == int(round_.get("roundIndex") or 0)
-        ),
-        None,
-    )
-    sections = [
-        _section(
-            "goal",
-            "本轮目标",
-            (raw_round or {}).get("goal")
-            or (round_.get("summary", {}).get("goal") or {}).get("headline"),
-        ),
-        _section("plan", "当前保存的规划", _plan_text(round_.get("plan") or {})),
-        _section("branches", "候选方案", branches_markdown(round_.get("branches") or [])),
-        _section(
-            "decisions",
-            "候选评审与主 Agent 决策",
-            _convergence_markdown(round_.get("convergenceBatches") or []),
-        ),
-        _section(
-            "evaluation",
-            "主脚本整体评审",
-            _node_value(round_.get("overallEvaluation")) or "未找到明确整轮评审结论",
-        ),
-        _section("result", "本轮产出", _node_value(round_.get("result"))),
-    ]
-    result = round_.get("result") or {}
-    changes = None
-    if result.get("hasChanges"):
-        changes = {
-            "summary": _node_value(result),
-            "sections": [
-                item
-                for item in [
-                    _section(
-                        "content",
-                        "内容方案",
-                        _round_content_changes(round_.get("branches") or []),
-                    ),
-                    _section(
-                        "domain",
-                        "领域信息",
-                        _round_domain_changes(round_.get("branches") or []),
-                    ),
-                ]
-                if item
-            ],
-            "artifactRef": "artifact:base:current",
-            "exactness": "current-only",
-        }
-    return _payload(
-        sections,
-        {
-            "scriptBuildId": script_build_id,
-            "round": raw_round,
-            "multipathDecisions": [
-                item
-                for item in (bundle or {}).get("multipathDecisions", [])
-                if int(item.get("round_index") or 0) == int(round_.get("roundIndex") or 0)
-            ],
-        },
-        changes,
-    )
-
-
-def project_activity_detail(
-    script_build_id: int,
-    activity_id: str,
-    view: dict[str, Any],
-    bundle: dict[str, Any],
-    event_detail: dict[str, Any] | None = None,
-) -> dict[str, Any]:
-    if activity_id == "run:final-result":
-        return _final_result_detail(script_build_id, view, bundle)
-
-    missing_convergence = _find_missing_convergence(view, activity_id)
-    if missing_convergence is not None:
-        node, batch = missing_convergence
-        return _payload(
-            [
-                _section(
-                    "status",
-                    str(node.get("title") or "记录状态"),
-                    _node_value(node),
-                    "summary",
-                )
-            ],
-            {
-                "scriptBuildId": script_build_id,
-                "roundIndex": batch.get("roundIndex"),
-                "branchIds": batch.get("branchIds") or [],
-                "association": batch.get("association"),
-                "unscopedReviewRefs": node.get("unscopedReviewRefs") or [],
-            },
-        )
-
-    decision = _find_decision_projection(view, activity_id)
-    if decision is not None:
-        return _decision_detail_payload(
-            script_build_id, decision, bundle, activity_id, event_detail
-        )
-
-    if activity_id.startswith("domain-info:"):
-        record_id = _suffix_int(activity_id)
-        row = _find_by_id(bundle.get("domainInfo") or [], record_id)
-        if not row:
-            raise KeyError(activity_id)
-        return _payload(
-            [
-                _section("fact", "已核实领域事实", row.get("content")),
-                _section("verification", "核实说明", row.get("note")),
-                _section("source", "信息出处", _source_value(row.get("source"))),
-                _section(
-                    "use",
-                    "用途与存疑状态",
-                    "已作为领域事实独立保存;是否进入具体候选脚本文案未记录。",
-                ),
-            ],
-            {"scriptBuildId": script_build_id, "table": "script_build_domain_info", "record": row},
-            {
-                "summary": "这条领域事实已经独立写入领域信息库。",
-                "sections": [],
-                "exactness": "exact",
-            },
-        )
-
-    if activity_id.startswith("retrieval-direct:"):
-        group = _find_retrieval_item(view, "directToolGroups", activity_id)
-        if not group:
-            raise KeyError(activity_id)
-        calls = group.get("calls") or []
-        payload = _payload(
-            [
-                _section("purpose", "读取方式", "实现 Agent 直接调用只读工具,补齐创作所需的上下文。"),
-                _section("sources", "读取了什么", _direct_sources_markdown(group)),
-                _section("result", "得到什么", _direct_result_markdown(calls), "summary"),
-            ],
-            {
-                "scriptBuildId": script_build_id,
-                "retrievalGroup": group,
-                "events": _events_for_ids(bundle, [item.get("eventId") for item in calls]),
-            },
-        )
-        payload.update(
-            {
-                "detailKind": "retrieval-group",
-                "retrievalKind": "direct-tools",
-                "activities": [
-                    {
-                        "id": item.get("id") or item.get("detailRef"),
-                        "kind": "tool",
-                        "label": item.get("businessLabel") or "读取相关信息",
-                        "status": item.get("status") or "unknown",
-                        "summary": item.get("resultSummary") or "",
-                        "detailRef": item.get("detailRef"),
-                    }
-                    for item in calls
-                ],
-            }
-        )
-        return payload
-
-    if activity_id.startswith("retrieval-agent:"):
-        run = _find_retrieval_item(view, "agentRuns", activity_id)
-        if not run:
-            raise KeyError(activity_id)
-        event = event_detail or _event_by_id(bundle, run.get("eventId"))
-        screening = run.get("screening") or {}
-        full_task = _event_nested_value(event, "input", "task")
-        raw_screening_text = _event_nested_value(event, "output", "summary")
-        if not raw_screening_text:
-            raw_screening_text = screening.get("preview")
-        screening_text = clean_report(raw_screening_text)
-        payload = _payload(
-            [
-                _section("task", "完整取数任务", full_task or run.get("taskPreview") or "取数目标未记录"),
-                _section("queries", "查询过程", _agent_query_markdown(run)),
-                _section("hits", "原始命中", _agent_hit_markdown(run)),
-                _section(
-                    "screening",
-                    "初筛整理",
-                    screening_text or _screening_fallback(screening.get("state")),
-                    "report",
-                ),
-            ],
-            {
-                "scriptBuildId": script_build_id,
-                "agentRun": run,
-                "agentEvent": event,
-                "queryEvents": _events_for_ids(bundle, [item.get("eventId") for item in run.get("attempts") or []]),
-            },
-        )
-        activities = [
-            {
-                "id": item.get("id") or item.get("detailRef"),
-                "kind": "query",
-                "label": item.get("queryLabel") or "查询内容详见详情",
-                "status": item.get("status") or "unknown",
-                "resultCount": item.get("resultCount"),
-                "detailRef": item.get("detailRef"),
-            }
-            for item in run.get("attempts") or []
-        ]
-        payload.update(
-            {
-                "detailKind": "retrieval-agent",
-                "retrievalKind": "agent",
-                "activities": activities,
-            }
-        )
-        if not activities:
-            if script_build_id <= 433:
-                payload["missingActivityNotice"] = "该历史运行没有结构化查询明细。"
-            elif run.get("agentType") == "retrieve_data_pattern_relation":
-                payload["missingActivityNotice"] = (
-                    "本次运行未保存可下钻的结构化查询记录。"
-                )
-        return payload
-
-    round_index, branch_id, role = _activity_parts(activity_id)
-    round_ = _find_round(view, round_index) if round_index is not None else None
-    if round_ is None:
-        raise KeyError(activity_id)
-    if branch_id is None:
-        if role is None:
-            return project_round_detail(script_build_id, round_, bundle)
-        node = _round_node(round_, role)
-        if not node:
-            raise KeyError(activity_id)
-        raw_round = next(
-            (
-                item
-                for item in bundle.get("rounds") or []
-                if int(item.get("round_index") or 0) == round_index
-            ),
-            None,
-        )
-        if role == "result":
-            return _round_result_detail(script_build_id, round_, raw_round, bundle)
-        return _node_detail(script_build_id, node, raw_round)
-
-    branch = next(
-        (
-            item
-            for item in round_.get("branches") or []
-            if int(item.get("branchId") or 0) == branch_id
-        ),
-        None,
-    )
-    raw_branch = next(
-        (
-            item
-            for item in bundle.get("branches") or []
-            if int(item.get("branch_id") or 0) == branch_id
-        ),
-        None,
-    )
-    if not branch:
-        raise KeyError(activity_id)
-    if role is None:
-        return _branch_detail(script_build_id, branch, raw_branch, bundle)
-    if role == "retrieval":
-        return _retrieval_stage_detail(script_build_id, branch, raw_branch, bundle)
-    node = _branch_node(branch, role)
-    if not node:
-        raise KeyError(activity_id)
-    if role == "output":
-        return _branch_output_detail(script_build_id, branch, raw_branch, bundle)
-    if role == "task":
-        return _branch_task_detail(
-            script_build_id,
-            branch,
-            raw_branch,
-            bundle,
-            round_index=round_index,
-            branch_id=branch_id,
-        )
-    return _node_detail(script_build_id, node, raw_branch)
-
-
-def project_event_detail(
-    script_build_id: int,
-    event: dict[str, Any],
-    *,
-    run_status: str | None = None,
-) -> dict[str, Any]:
-    if is_retrieval_detail_event(event):
-        payload = project_retrieval_event(event, run_status=run_status)
-        payload["technical"] = sanitize(
-            {
-                "source": {
-                    "scriptBuildId": script_build_id,
-                    "table": "script_build_event / script_build_event_body",
-                    "detailRef": f"event:{event.get('id')}",
-                },
-                **_event_technical_groups(event),
-            },
-            max_text=None,
-        )
-        return sanitize(payload, max_text=None)
-    if (
-        event.get("event_name") == "think_and_plan"
-        and event.get("agent_role") == "main"
-        and int(event.get("agent_depth") or 0) == 0
-    ):
-        raw_input = event.get("input")
-        content = raw_input.get("content") if isinstance(raw_input, dict) else None
-        data = content if isinstance(content, dict) else event.get("inputData")
-        data = data if isinstance(data, dict) else {}
-        return _payload(
-            [
-                _section(
-                    "summary",
-                    "规划概要",
-                    data.get("thought_summary"),
-                    "summary",
-                    visual_role="key-result",
-                ),
-                _section(
-                    "thought",
-                    "完整思考",
-                    data.get("thought"),
-                    "report",
-                    visual_role="section",
-                ),
-                _section(
-                    "plan",
-                    "执行计划",
-                    data.get("plan"),
-                    visual_role="section",
-                ),
-                _section(
-                    "action",
-                    "下一步",
-                    data.get("action"),
-                    visual_role="reason",
-                ),
-            ],
-            {
-                "scriptBuildId": script_build_id,
-                "table": "script_build_event / script_build_event_body",
-                "event": event,
-            },
-        )
-    return _payload(
-        event_business_sections(event),
-        {"scriptBuildId": script_build_id, "table": "script_build_event / script_build_event_body", "event": event},
-    )
-
-
-def project_artifact_detail(
-    script_build_id: int, snapshot_ref: str, artifact: dict[str, Any]
-) -> dict[str, Any]:
-    paragraphs, elements, links = _normalized_artifact_records(artifact)
-    exactness = "current-only" if "base:current" in snapshot_ref else _artifact_exactness(artifact)
-    context = artifact.get("artifactContext") if isinstance(artifact.get("artifactContext"), dict) else {}
-    context = {
-        "type": context.get("type") or ("final" if "base:current" in snapshot_ref else "candidate"),
-        **context,
-        "exactness": exactness,
-    }
-    candidate = context.get("type") == "candidate"
-    scale_title = "候选脚本规模" if candidate else "主脚本规模"
-    content_summary = "候选脚本内容" if candidate else "当前主脚本内容"
-    payload = _payload(
-        [
-            _section("summary", scale_title, f"{len(paragraphs)} 个段落、{len(elements)} 个元素、{len(links)} 条关联"),
-        ],
-        {
-            "scriptBuildId": script_build_id,
-            "snapshotRef": snapshot_ref,
-            "sourceOrigin": artifact.get("sourceOrigin") or "database",
-            "exactness": exactness,
-            "artifactContext": context,
-            "recordCounts": {
-                "paragraphs": len(paragraphs),
-                "elements": len(elements),
-                "links": len(links),
-            },
-        },
-        {
-            "summary": content_summary,
-            "sections": [
-                _section(
-                    "counts",
-                    "内容规模",
-                    f"{len(paragraphs)} 个段落、{len(elements)} 个元素、{len(links)} 条关联",
-                ),
-            ],
-            "artifactRef": snapshot_ref,
-            "exactness": exactness,
-        },
-    )
-    payload["scriptTable"] = sanitize(
-        _script_table_projection(snapshot_ref, artifact, exactness),
-        max_text=None,
-    )
-    payload["artifactContext"] = sanitize(context, max_text=None)
-    return payload
-
-
-_ELEMENT_REFERENCE = re.compile(r"\[([^\]:]+):(\d+)\]")
-_SCRIPT_SECTIONS = (
-    ("theme", "主题"),
-    ("form", "形式"),
-    ("function", "作用"),
-    ("feeling", "感受"),
-)
-
-
-def _script_table_projection(
-    snapshot_ref: str, artifact: dict[str, Any], exactness: str
-) -> dict[str, Any]:
-    snapshot = artifact.get("snapshot") if isinstance(artifact.get("snapshot"), dict) else artifact
-    snapshot = snapshot if isinstance(snapshot, dict) else {}
-    paragraphs, elements, links = _normalized_artifact_records(artifact)
-
-    element_names: dict[int, str] = {}
-    for element in elements:
-        element_id = _as_int(element.get("effectiveId") or element.get("id"))
-        row_id = _as_int(element.get("rowId") or element.get("id"))
-        name = str(element.get("name") or "").strip()
-        if element_id is not None:
-            element_names[element_id] = name
-        if row_id is not None:
-            element_names[row_id] = name
-
-    paragraph_rows = _ordered_paragraphs(
-        [_script_paragraph(row, element_names) for row in paragraphs]
-    )
-
-    paragraph_lookup: dict[int, dict[str, Any]] = {}
-    for paragraph in paragraph_rows:
-        for key in ("id", "rowId"):
-            value = _as_int(paragraph.get(key))
-            if value is not None:
-                paragraph_lookup[value] = paragraph
-
-    linked_paragraphs: dict[int, list[dict[str, Any]]] = {}
-    for link in links:
-        element_id = _as_int(link.get("element_id") or link.get("elementId"))
-        paragraph_id = _as_int(link.get("paragraph_id") or link.get("paragraphId"))
-        paragraph = paragraph_lookup.get(paragraph_id) if paragraph_id is not None else None
-        if element_id is None or paragraph is None:
-            continue
-        linked_paragraphs.setdefault(element_id, []).append(
-            {
-                "id": paragraph.get("id"),
-                "index": paragraph.get("index"),
-                "name": paragraph.get("name"),
-            }
-        )
-
-    element_rows = []
-    for element in elements:
-        element_id = element.get("effectiveId") or element.get("id")
-        row_id = element.get("rowId") or element.get("id")
-        link_keys = {
-            value
-            for value in (_as_int(row_id), _as_int(element_id))
-            if value is not None
-        }
-        linked = [
-            paragraph
-            for key in link_keys
-            for paragraph in linked_paragraphs.get(key, [])
-        ]
-        element_rows.append(
-            {
-                "id": element_id,
-                "rowId": row_id,
-                "name": element.get("name") or "未命名元素",
-                "primaryDimension": element.get("dimension_primary"),
-                "secondaryDimension": element.get("dimension_secondary"),
-                "paragraphs": _unique_records(linked, "id"),
-            }
-        )
-    element_rows.sort(key=lambda row: (str(row.get("name") or ""), _sort_number(row.get("id"))))
-
-    return {
-        "snapshotRef": snapshot_ref,
-        "exactness": exactness,
-        "sourceOrigin": artifact.get("sourceOrigin") or snapshot.get("sourceOrigin") or "database",
-        "counts": {
-            "paragraphs": len(paragraph_rows),
-            "elements": len(element_rows),
-            "links": len(links),
-        },
-        "maxDepth": (
-            max((int(row.get("depth") or 0) for row in paragraph_rows), default=0) + 1
-            if paragraph_rows
-            else 0
-        ),
-        "paragraphs": paragraph_rows,
-        "elements": element_rows,
-    }
-
-
-def _normalized_artifact_records(
-    artifact: dict[str, Any],
-) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
-    """Normalize current overlays and frozen branch snapshots to one table shape.
-
-    Current overlays store a flat ``paragraphs`` list plus top-level
-    ``paragraphElements``.  Frozen ``content_snapshot`` rows store second-level
-    paragraphs under ``sub_paragraphs`` and links under each paragraph's
-    ``linked_elements``.  The visualizer must preserve both without pretending
-    that a truncated top-level list is the complete candidate script.
-    """
-    snapshot = artifact.get("snapshot") if isinstance(artifact.get("snapshot"), dict) else artifact
-    snapshot = snapshot if isinstance(snapshot, dict) else {}
-    paragraphs: list[dict[str, Any]] = []
-    seen_paragraphs: set[tuple[str, str]] = set()
-
-    def visit(row: Any, parent_id: Any = None) -> None:
-        if not isinstance(row, dict):
-            return
-        current = dict(row)
-        children = current.pop("sub_paragraphs", None) or current.pop("subParagraphs", None) or []
-        if parent_id is not None and current.get("parent_id") is None and current.get("parentId") is None:
-            current["parent_id"] = parent_id
-        row_id = current.get("rowId") or current.get("id")
-        key = (str(row_id), str(current.get("baseRefId") or current.get("base_ref_id") or ""))
-        if key not in seen_paragraphs:
-            seen_paragraphs.add(key)
-            paragraphs.append(current)
-        effective_parent = current.get("effectiveId") or current.get("id") or row_id
-        for child in children if isinstance(children, list) else []:
-            visit(child, effective_parent)
-
-    for paragraph in snapshot.get("paragraphs") or []:
-        visit(paragraph)
-
-    elements = [item for item in snapshot.get("elements") or [] if isinstance(item, dict)]
-    links: list[dict[str, Any]] = []
-    seen_links: set[tuple[str, str]] = set()
-
-    def append_link(link: Any, paragraph_id: Any = None) -> None:
-        if not isinstance(link, dict):
-            return
-        current = dict(link)
-        if paragraph_id is not None and current.get("paragraph_id") is None and current.get("paragraphId") is None:
-            current["paragraph_id"] = paragraph_id
-        element_id = current.get("element_id") or current.get("elementId")
-        linked_paragraph = current.get("paragraph_id") or current.get("paragraphId")
-        if element_id is None or linked_paragraph is None:
-            return
-        key = (str(linked_paragraph), str(element_id))
-        if key in seen_links:
-            return
-        seen_links.add(key)
-        links.append(current)
-
-    for link in snapshot.get("paragraphElements") or snapshot.get("paragraph_elements") or []:
-        append_link(link)
-    for paragraph in paragraphs:
-        paragraph_id = paragraph.get("effectiveId") or paragraph.get("id") or paragraph.get("rowId")
-        nested_links = paragraph.get("linked_elements") or paragraph.get("linkedElements") or []
-        if isinstance(nested_links, list) and nested_links:
-            for link in nested_links:
-                append_link(link, paragraph_id)
-        else:
-            for element_id in paragraph.get("linked_element_ids") or paragraph.get("linkedElementIds") or []:
-                append_link({"element_id": element_id}, paragraph_id)
-
-    return paragraphs, elements, links
-
-
-def _script_paragraph(row: dict[str, Any], element_names: dict[int, str]) -> dict[str, Any]:
-    full_description = str(row.get("full_description") or "")
-    return {
-        "id": row.get("effectiveId") or row.get("id"),
-        "rowId": row.get("rowId") or row.get("id"),
-        "parentId": row.get("parent_id"),
-        "depth": 0,
-        "index": row.get("paragraph_index"),
-        "level": row.get("level"),
-        "name": row.get("name") or "未命名段落",
-        "contentRange": _content_range(row.get("content_range")),
-        "sections": {
-            key: {
-                "label": label,
-                "description": row.get(key),
-                "descriptionParts": _description_parts(
-                    str(row.get(key) or ""), element_names
-                ),
-                "dimensions": _dimensions(row.get(f"{key}_elements")),
-            }
-            for key, label in _SCRIPT_SECTIONS
-        },
-        "description": row.get("description"),
-        "fullDescription": full_description,
-        "fullDescriptionParts": _description_parts(full_description, element_names),
-    }
-
-
-def _ordered_paragraphs(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    """Keep every child next to its real parent without guessing from level labels."""
-    by_reference: dict[int, dict[str, Any]] = {}
-    for row in rows:
-        for value in (row.get("rowId"), row.get("id")):
-            reference = _as_int(value)
-            if reference is not None:
-                by_reference[reference] = row
-
-    children: dict[int, list[dict[str, Any]]] = {}
-    roots: list[dict[str, Any]] = []
-    for row in rows:
-        parent = by_reference.get(_as_int(row.get("parentId")))
-        if parent is None or parent is row:
-            row["parentId"] = None
-            roots.append(row)
-            continue
-        row["parentId"] = parent.get("id")
-        children.setdefault(id(parent), []).append(row)
-
-    ordered: list[dict[str, Any]] = []
-    visited: set[int] = set()
-
-    def visit(row: dict[str, Any], depth: int, ancestry: set[int]) -> None:
-        marker = id(row)
-        if marker in visited:
-            return
-        if marker in ancestry:
-            row["parentId"] = None
-            depth = 0
-        visited.add(marker)
-        row["depth"] = depth
-        ordered.append(row)
-        next_ancestry = {*ancestry, marker}
-        for child in sorted(children.get(marker, []), key=_paragraph_sort_key):
-            visit(child, depth + 1, next_ancestry)
-
-    for root in sorted(roots, key=_paragraph_sort_key):
-        visit(root, 0, set())
-    for row in sorted(rows, key=_paragraph_sort_key):
-        if id(row) not in visited:
-            row["parentId"] = None
-            visit(row, 0, set())
-    return ordered
-
-
-def _paragraph_sort_key(row: dict[str, Any]) -> tuple[Any, ...]:
-    return (_sort_number(row.get("index")), _sort_number(row.get("id")))
-
-
-def _content_range(value: Any) -> list[str]:
-    if not isinstance(value, dict):
-        return [_display_value(value)] if value not in (None, "") else []
-
-    tags: list[str] = []
-    for key, item in value.items():
-        if key == "标题":
-            tags.append("标题")
-        elif key == "图片" and isinstance(item, list):
-            if not item:
-                continue
-            values = [str(entry) for entry in item]
-            tags.append(f"图{values[0]}" if len(values) == 1 else f"图{values[0]}-{values[-1]}")
-        elif key == "视频":
-            tags.append(_display_value(item))
-        elif key == "正文":
-            body = "\n".join(str(entry) for entry in item) if isinstance(item, list) else _display_value(item)
-            tags.append(f"正文: {body}")
-        else:
-            rendered = ",".join(str(entry) for entry in item) if isinstance(item, list) else _display_value(item)
-            tags.append(f"{key}: {rendered}")
-    return tags
-
-
-def _dimensions(value: Any) -> list[dict[str, Any]]:
-    rows = value if isinstance(value, list) else [value] if isinstance(value, dict) else []
-    return [
-        {
-            "type": row.get("维度类型") or row.get("dimension_type"),
-            "name": row.get("维度") or row.get("dimension"),
-            "value": row.get("原子点") or row.get("value"),
-            **({"id": row.get("id")} if row.get("id") is not None else {}),
-        }
-        for row in rows
-        if isinstance(row, dict)
-    ]
-
-
-def _description_parts(value: str, element_names: dict[int, str]) -> list[dict[str, Any]]:
-    parts: list[dict[str, Any]] = []
-    cursor = 0
-    for match in _ELEMENT_REFERENCE.finditer(value):
-        if match.start() > cursor:
-            parts.append({"type": "text", "text": value[cursor : match.start()]})
-        element_id = int(match.group(2))
-        name = element_names.get(element_id)
-        parts.append(
-            {
-                "type": "element",
-                "id": element_id,
-                "name": name,
-                "resolved": name is not None,
-                "text": match.group(0),
-            }
-        )
-        cursor = match.end()
-        if name:
-            duplicate = re.match(r"\s*" + re.escape(name), value[cursor:])
-            if duplicate:
-                cursor += duplicate.end()
-    if cursor < len(value):
-        parts.append({"type": "text", "text": value[cursor:]})
-    return parts
-
-
-def _display_value(value: Any) -> str:
-    if value is True:
-        return "包含"
-    if value is False:
-        return "不包含"
-    if value is None:
-        return ""
-    return str(value)
-
-
-def _as_int(value: Any) -> int | None:
-    try:
-        return int(value) if value is not None else None
-    except (TypeError, ValueError):
-        return None
-
-
-def _sort_number(value: Any) -> tuple[int, str]:
-    number = _as_int(value)
-    return (0, str(number).zfill(12)) if number is not None else (1, str(value or ""))
-
-
-def _unique_records(rows: list[dict[str, Any]], key: str) -> list[dict[str, Any]]:
-    seen: set[Any] = set()
-    result = []
-    for row in rows:
-        value = row.get(key)
-        if value in seen:
-            continue
-        seen.add(value)
-        result.append(row)
-    return result
-
-
-def _branch_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
-    facts = _branch_facts(branch, bundle)
-    return _payload(
-        [
-            _section("task", "实现任务", branch_task_text(branch.get("summary", {}).get("task"))),
-            _section("data", "数据取舍", decisions_markdown([item.get("decision") for item in branch.get("dataDecisions") or []])),
-            _section("output", "候选产出", branch.get("summary", {}).get("output")),
-        ],
-        {"scriptBuildId": script_build_id, "branch": raw},
-        {
-            "summary": branch.get("summary", {}).get("output"),
-            "sections": [_section("facts", "领域事实", _domain_facts_markdown(facts))] if facts else [],
-            "artifactRef": branch.get("output", {}).get("snapshotRef"),
-            "exactness": _output_exactness(branch.get("output") or {}),
-        }
-        if branch.get("output", {}).get("node", {}).get("hasChanges")
-        else None,
-    )
-
-
-def _branch_task_detail(
-    script_build_id: int,
-    branch: dict[str, Any],
-    raw: Any,
-    bundle: dict[str, Any],
-    *,
-    round_index: int,
-    branch_id: int,
-) -> dict[str, Any]:
-    task_event = next(
-        (
-            event
-            for event in reversed(bundle.get("events") or [])
-            if str(event.get("event_type") or "") == "agent_invoke"
-            and str(event.get("event_name") or "") == "script_implementer"
-            and int(event.get("round_index") or 0) == round_index
-            and int(event.get("branch_id") or 0) == branch_id
-        ),
-        None,
-    )
-    event_input = (task_event or {}).get("inputData")
-    event_task = event_input.get("task") if isinstance(event_input, dict) else None
-    raw_task = (raw or {}).get("impl_task") if isinstance(raw, dict) else None
-    complete_task = branch_task_text(
-        event_task
-        or raw_task
-        or branch.get("summary", {}).get("task")
-    )
-    task_sections = branch_task_sections(
-        event_task
-        or raw_task
-        or branch.get("summary", {}).get("task")
-    )
-    visual_roles = {
-        "goal": ("summary", "key-result"),
-        "materials": ("report", "evidence"),
-        "avoid": ("report", "notice"),
-        "context": ("report", "evidence"),
-    }
-    return _payload(
-        [
-            _section(
-                f"task-{section['kind']}-{index}",
-                section["title"],
-                section["content"],
-                visual_roles.get(section["kind"], ("default", "section"))[0],
-                visual_role=visual_roles.get(section["kind"], ("default", "section"))[1],
-                presentation="document" if section["kind"] in {"materials", "constraints", "avoid", "context"} else "prose",
-            )
-            for index, section in enumerate(task_sections, 1)
-        ] or [_section("task-notes-1", "任务说明", complete_task)],
-        {
-            "scriptBuildId": script_build_id,
-            "branch": raw,
-            "taskEvent": task_event,
-            "taskSource": (
-                "script_build_event.inputData.task"
-                if event_task
-                else "script_build_branch.impl_task"
-                if raw_task
-                else "execution-view summary"
-            ),
-        },
-    )
-
-
-def _branch_output_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
-    output = branch.get("output") or {}
-    facts = _branch_facts(branch, bundle)
-    changes = {
-        "summary": output.get("summary"),
-        "sections": [_section("facts", "已核实领域事实", _domain_facts_markdown(facts))] if facts else [],
-        "artifactRef": output.get("snapshotRef"),
-        "exactness": _output_exactness(output),
-    }
-    return _payload(
-        [
-            _section("output", "形成了什么", output.get("summary")),
-            _section("assessment", "实现者说明", assessment_text((raw or {}).get("self_assessment")), "report"),
-        ],
-        {"scriptBuildId": script_build_id, "branch": raw, "output": output},
-        changes if output.get("node", {}).get("hasChanges") else None,
-    )
-
-
-def _retrieval_stage_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
-    stage = branch.get("retrievalStage") or {}
-    direct = stage.get("directToolGroups") or []
-    agents = stage.get("agentRuns") or []
-    return _payload(
-        [
-            _section(
-                "purpose",
-                "取数目的",
-                branch_task_text(branch.get("summary", {}).get("task"))
-                or "为当前实现任务补齐可用的上游信息。",
-            ),
-            _section("mode", "执行方式", _retrieval_mode_text(stage.get("observedMode"))),
-            _section("waves", "执行波次", _retrieval_waves_markdown(stage.get("waves") or [])),
-            _section("direct", "工具取数", _direct_groups_markdown(direct)),
-            _section("agents", "Agent 取数", _agent_runs_markdown(agents)),
-            _section("result", "阶段结果与失败", _retrieval_stage_result(stage)),
-        ],
-        {
-            "scriptBuildId": script_build_id,
-            "branch": raw,
-            "retrievalStage": stage,
-            "note": "取数和初筛来自结构化运行事件;正式数据取舍来自独立业务表,不在本详情内反推。",
-        },
-    )
-
-
-def _node_detail(script_build_id: int, node: dict[str, Any], technical: Any) -> dict[str, Any]:
-    fields = []
-    primary = node.get("card", {}).get("primary")
-    if primary:
-        fields.append(_section("primary", primary.get("label") or node.get("title"), primary.get("value")))
-    for line in node.get("card", {}).get("secondary") or []:
-        fields.append(_section(str(line.get("key")), line.get("label") or "补充说明", line.get("value")))
-    return _payload(fields, {"scriptBuildId": script_build_id, "record": technical})
-
-
-def _round_result_detail(
-    script_build_id: int,
-    round_: dict[str, Any],
-    raw_round: dict[str, Any] | None,
-    bundle: dict[str, Any],
-) -> dict[str, Any]:
-    result = round_.get("result") or {}
-    branches = round_.get("branches") or []
-    round_index = int(round_.get("roundIndex") or 0)
-    facts = [
-        item
-        for item in bundle.get("domainInfo") or []
-        if int(item.get("round_index") or 0) == round_index
-    ]
-    evaluation = round_.get("overallEvaluation") or {}
-    next_step = next(
-        (
-            line.get("value")
-            for line in (result.get("card") or {}).get("secondary") or []
-            if line.get("key") == "next"
-        ),
-        None,
-    )
-    sections = [
-        _section(
-            "goal",
-            "本轮目标完成情况",
-            "\n".join(
-                part
-                for part in (
-                    business_text((raw_round or {}).get("goal")),
-                    f"收口状态:{result.get('status') or round_.get('state') or '未记录'}",
-                )
-                if part
-            ),
-        ),
-        _section("outcomes", "方案处置", _branch_outcomes_markdown(branches)),
-        _section("facts", "领域事实", _domain_facts_markdown(facts) or "本轮没有新增领域事实记录。"),
-        _section(
-            "evaluation",
-            "整体评审",
-            _node_value(evaluation) or "未找到明确的本轮整体评审结论。",
-        ),
-        _section(
-            "changes",
-            "主脚本变化",
-            _round_content_changes(branches),
-        ),
-        _section("next", "下一步", next_step or "下一步未记录。"),
-    ]
-    changes = None
-    if result.get("hasChanges"):
-        changes = {
-            "summary": _node_value(result),
-            "sections": [
-                _section("content", "内容方案", _round_content_changes(branches)),
-                _section("domain", "领域信息", f"本轮独立保存 {len(facts)} 条领域事实。"),
-            ],
-            "artifactRef": "artifact:base:current",
-            "exactness": "current-only",
-        }
-    return _payload(
-        sections,
-        {
-            "scriptBuildId": script_build_id,
-            "round": raw_round,
-            "resultProjection": result,
-            "note": "本轮主脚本历史快照无法完整还原,产物入口指向当前主脚本。",
-        },
-        changes,
-    )
-
-
-def _final_result_detail(
-    script_build_id: int, view: dict[str, Any], bundle: dict[str, Any]
-) -> dict[str, Any]:
-    final = view.get("finalResult") or {}
-    record = bundle.get("record") or {}
-    branches = final.get("branchCounts") or {}
-    artifact = final.get("artifact") or {}
-    status = str(final.get("status") or record.get("status") or "unknown")
-    conclusion = {
-        "success": "构建已完成。",
-        "completed": "构建已完成。",
-        "partial": "构建部分完成。",
-        "failed": "构建失败。",
-        "stopped": "构建已停止。",
-        "running": "构建仍在进行。",
-    }.get(status.lower(), f"构建状态:{status}。")
-    summary_or_error = final.get("summary")
-    if final.get("errorMessage"):
-        summary_or_error = f"失败原因:{final['errorMessage']}"
-    sections = [
-        _section("conclusion", "构建结论", conclusion, "summary"),
-        _section("summary", "构建摘要或失败原因", summary_or_error or "未另外记录构建摘要。"),
-        _section(
-            "statistics",
-            "轮次与方案统计",
-            (
-                f"共 {int(final.get('roundCount') or 0)} 轮,"
-                f"{int(branches.get('total') or 0)} 个方案;"
-                f"采用 {int(branches.get('merged') or 0)},"
-                f"暂存 {int(branches.get('parked') or 0)},"
-                f"未采用 {int(branches.get('discarded') or 0)},"
-                f"待处理 {int(branches.get('open') or 0)}。"
-            ),
-        ),
-        _section(
-            "artifact",
-            "最终主脚本规模",
-            (
-                f"{int(artifact.get('paragraphCount') or 0)} 个段落、"
-                f"{int(artifact.get('elementCount') or 0)} 个元素、"
-                f"{int(artifact.get('linkCount') or 0)} 条关联。"
-            ),
-        ),
-        _section(
-            "version",
-            "版本说明",
-            "当前只能确认数据库保存的最新主脚本,不将它伪装成某一轮结束时的历史快照。",
-        ),
-    ]
-    return _payload(
-        sections,
-        {
-            "scriptBuildId": script_build_id,
-            "table": "script_build_record",
-            "record": record,
-            "finalResultProjection": final,
-        },
-        {
-            "summary": "打开当前主脚本。",
-            "sections": [],
-            "artifactRef": artifact.get("snapshotRef") or "artifact:base:current",
-            "exactness": "current-only",
-        },
-    )
-
-
-def _find_decision_projection(
-    view: dict[str, Any], detail_ref: str
-) -> dict[str, Any] | None:
-    candidates: list[dict[str, Any]] = []
-    objective = view.get("objective") or {}
-    if isinstance(objective.get("decision"), dict):
-        candidates.append(objective["decision"])
-    for round_ in view.get("rounds") or []:
-        for node in (
-            round_.get("goal"),
-            (round_.get("plan") or {}).get("node"),
-            round_.get("overallEvaluation"),
-        ):
-            if isinstance(node, dict) and isinstance(node.get("decision"), dict):
-                candidates.append(node["decision"])
-        for branch in round_.get("branches") or []:
-            for item in branch.get("dataDecisions") or []:
-                projection = item.get("decisionProjection")
-                if isinstance(projection, dict):
-                    candidates.append(projection)
-            output_node = (branch.get("output") or {}).get("node") or {}
-            if isinstance(output_node.get("decision"), dict):
-                candidates.append(output_node["decision"])
-        for batch in round_.get("convergenceBatches") or []:
-            review = batch.get("review") or {}
-            decision = batch.get("decision") or {}
-            if isinstance(review.get("decisionProjection"), dict):
-                candidates.append(review["decisionProjection"])
-            if isinstance(decision.get("decisionProjection"), dict):
-                candidates.append(decision["decisionProjection"])
-    return next(
-        (
-            item
-            for item in candidates
-            if detail_ref in {item.get("id"), item.get("detailRef")}
-        ),
-        None,
-    )
-
-
-def _find_missing_convergence(
-    view: dict[str, Any], detail_ref: str
-) -> tuple[dict[str, Any], dict[str, Any]] | None:
-    for round_ in view.get("rounds") or []:
-        for batch in round_.get("convergenceBatches") or []:
-            for key in ("missingReview", "missingDecision"):
-                node = batch.get(key)
-                if isinstance(node, dict) and detail_ref in {
-                    node.get("id"),
-                    node.get("detailRef"),
-                }:
-                    return node, batch
-    return None
-
-
-def _decision_detail_payload(
-    script_build_id: int,
-    decision: dict[str, Any],
-    bundle: dict[str, Any],
-    activity_id: str,
-    event_detail: dict[str, Any] | None = None,
-) -> dict[str, Any]:
-    detail = decision.get("detail") if isinstance(decision.get("detail"), dict) else {}
-    blocks = [
-        block
-        for block in detail.get("blocks") or []
-        if isinstance(block, dict)
-    ]
-    for notice in decision.get("notices") or []:
-        if not isinstance(notice, dict) or not notice.get("message"):
-            continue
-        blocks.append(
-            {
-                "type": "notice",
-                "level": "warning" if "missing" in str(notice.get("code")) else "info",
-                "title": "记录说明",
-                "value": notice["message"],
-                "visualRole": "notice",
-                "presentation": "prose",
-            }
-        )
-    event_refs = [
-        ref
-        for ref in [
-            *(decision.get("eventRefs") or []),
-            *(decision.get("technicalRefs") or []),
-        ]
-        if isinstance(ref, str) and ref.startswith("event:")
-    ]
-    event_refs = list(dict.fromkeys(event_refs))
-    technical: dict[str, Any] = {
-        "source": {
-            "scriptBuildId": script_build_id,
-            "detailRef": activity_id,
-            "technicalRefs": event_refs,
-        },
-        "association": {
-            "completeness": decision.get("completeness"),
-            "notices": decision.get("notices") or [],
-        },
-        "rawDecisionProjection": decision,
-        "rawBusinessRecord": _decision_raw_record(bundle, activity_id),
-    }
-    if event_refs:
-        technical["runtimeEvents"] = _events_for_ids(
-            bundle, [_suffix_int(ref) for ref in event_refs]
-        )
-    if event_detail:
-        technical.update(_event_technical_groups(event_detail))
-    payload: dict[str, Any] = {
-        "detailKind": "agent-decision",
-        "decisionType": decision.get("decisionType"),
-        "actor": decision.get("actor"),
-        "authority": decision.get("authority"),
-        "question": decision.get("question"),
-        "promptRef": decision.get("promptRef"),
-        "blocks": sanitize(blocks, max_text=None),
-        "technical": sanitize(technical, max_text=None),
-    }
-    artifact_ref = decision.get("artifactRef")
-    if artifact_ref:
-        payload["changes"] = {
-            "summary": (decision.get("body") or {}).get("output")
-            or "已形成可查看的候选产出。",
-            "sections": [],
-            "artifactRef": artifact_ref,
-            "exactness": "historical-snapshot",
-        }
-    return payload
-
-
-def _event_technical_groups(event: dict[str, Any]) -> dict[str, Any]:
-    """Split a lazily loaded event body into the Inspector's technical groups.
-
-    The execution bundle only carries bounded previews.  Once a user opens an
-    event-backed decision, the full (sanitized) body belongs here rather than
-    being silently replaced by those previews.
-    """
-
-    metadata = {
-        key: value
-        for key, value in event.items()
-        if key not in {"input", "output", "duration_ms"}
-    }
-    groups: dict[str, Any] = {
-        "input": event.get("input"),
-        "output": event.get("output"),
-        "rawEvent": metadata,
-    }
-    if event.get("duration_ms") is not None:
-        groups["cost"] = {"durationMs": event.get("duration_ms")}
-    return groups
-
-
-def _decision_raw_record(bundle: dict[str, Any], activity_id: str) -> Any:
-    if activity_id.startswith("data-decision:"):
-        return _find_by_id(bundle.get("dataDecisions") or [], _suffix_int(activity_id))
-    if activity_id.startswith("multipath-decision:"):
-        return _find_by_id(
-            bundle.get("multipathDecisions") or [], _suffix_int(activity_id)
-        )
-    if activity_id.startswith("event:"):
-        return _event_by_id(bundle, _suffix_int(activity_id))
-    if activity_id.startswith("creative:"):
-        event_id = _suffix_int(activity_id)
-        event = _event_by_id(bundle, event_id)
-        branch_id = int((event or {}).get("branch_id") or 0)
-        branch = next(
-            (
-                item
-                for item in bundle.get("branches") or []
-                if int(item.get("branch_id") or 0) == branch_id
-            ),
-            None,
-        )
-        return {
-            "anchorEvent": event,
-            "candidateSnapshot": (branch or {}).get("candidate_snapshot"),
-        }
-    if activity_id == "run:objective":
-        return bundle.get("record")
-    round_index, _, role = _activity_parts(activity_id)
-    if role in {"goal", "plan"}:
-        return next(
-            (
-                item
-                for item in bundle.get("rounds") or []
-                if int(item.get("round_index") or 0) == int(round_index or 0)
-            ),
-            None,
-        )
-    return None
-
-
-def _payload(sections: list[dict[str, Any] | None], technical: Any, changes: dict[str, Any] | None = None) -> dict[str, Any]:
-    payload: dict[str, Any] = {
-        "businessSections": [item for item in sections if item],
-        "technical": sanitize(technical, max_text=None),
-    }
-    if changes:
-        changes["sections"] = [item for item in changes.get("sections", []) if item]
-        payload["changes"] = sanitize(changes, max_text=None)
-    return payload
-
-
-def _section(
-    section_id: str,
-    title: str,
-    content: Any,
-    variant: str = "default",
-    *,
-    visual_role: str | None = None,
-    presentation: str = "prose",
-    collapsible: bool = False,
-    default_open: bool = False,
-) -> dict[str, Any] | None:
-    if content is None or content == "" or content == []:
-        return None
-    if not isinstance(content, str):
-        return None
-    content = business_text(content)
-    if not content:
-        return None
-    return {
-        "id": section_id,
-        "title": title,
-        "content": sanitize(content, max_text=None),
-        "variant": variant,
-        "visualRole": visual_role or ("key-result" if variant == "summary" else "section"),
-        "presentation": presentation,
-        "collapsible": collapsible,
-        "defaultOpen": default_open,
-    }
-
-
-def _items_section(
-    section_id: str,
-    title: str,
-    items: list[dict[str, Any] | None],
-    *,
-    fallback: Any = None,
-    variant: str = "default",
-    visual_role: str | None = None,
-    collapsible: bool = False,
-    default_open: bool = False,
-) -> dict[str, Any] | None:
-    values = [item for item in items if item]
-    if not values and fallback in (None, "", []):
-        return None
-    content = "\n".join(
-        f"{item.get('label')}:{item.get('value')}" for item in values
-    ) or str(fallback)
-    section = _section(
-        section_id,
-        title,
-        content,
-        variant,
-        visual_role=visual_role,
-        presentation="list",
-        collapsible=collapsible,
-        default_open=default_open,
-    )
-    if section is not None and values:
-        section["items"] = sanitize(values, max_text=None)
-    return section
-
-
-def _detail_item(label: Any, value: Any) -> dict[str, Any] | None:
-    if value in (None, "", []):
-        return None
-    return {
-        "label": sanitize(business_text(str(label)), max_text=None),
-        "value": sanitize(business_text(str(value)), max_text=None),
-    }
-
-
-def _activity_parts(activity_id: str) -> tuple[int | None, int | None, str | None]:
-    parts = activity_id.split(":")
-    if len(parts) < 2 or parts[0] != "round":
-        return None, None, None
-    try:
-        round_index = int(parts[1])
-    except ValueError:
-        return None, None, None
-    if len(parts) >= 4 and parts[2] == "branch":
-        try:
-            branch_id = int(parts[3])
-        except ValueError:
-            return round_index, None, None
-        return round_index, branch_id, parts[4] if len(parts) > 4 else None
-    return round_index, None, parts[2] if len(parts) > 2 else None
-
-
-def _round_node(round_: dict[str, Any], role: str) -> dict[str, Any] | None:
-    if role == "goal":
-        return round_.get("goal")
-    if role == "plan":
-        return (round_.get("plan") or {}).get("node")
-    if role == "result":
-        return round_.get("result")
-    if role == "evaluation":
-        return round_.get("overallEvaluation")
-    return None
-
-
-def _branch_node(branch: dict[str, Any], role: str) -> dict[str, Any] | None:
-    if role == "task":
-        return branch.get("task")
-    if role == "output":
-        return branch.get("output", {}).get("node")
-    return None
-
-
-def _convergence_markdown(batches: list[dict[str, Any]]) -> str:
-    if not batches:
-        return "未找到候选评审或主 Agent 多路决策记录。"
-    rows: list[str] = []
-    for index, batch in enumerate(batches, 1):
-        review = batch.get("review") or {}
-        decision = batch.get("decision") or {}
-        branch_ids = batch.get("branchIds") or []
-        rows.append(
-            f"### 第 {index} 批 · "
-            + ("、".join(f"方案 {value}" for value in branch_ids) or "方案范围未记录")
-        )
-        rows.append(
-            f"**多方案评审:** {review.get('recommendation') or review.get('comparison') or review.get('conclusion') or '评审结论未记录'}"
-        )
-        rows.append(
-            f"**主 Agent 决策:** {business_text(decision.get('decision')) or '决策尚未记录'}"
-        )
-    return "\n\n".join(rows)
-
-
-def _find_retrieval_item(view: dict[str, Any], collection: str, item_id: str) -> dict[str, Any] | None:
-    for round_ in view.get("rounds") or []:
-        for branch in round_.get("branches") or []:
-            for item in (branch.get("retrievalStage") or {}).get(collection) or []:
-                if item.get("id") == item_id:
-                    return item
-    return None
-
-
-def _event_by_id(bundle: dict[str, Any], event_id: Any) -> dict[str, Any] | None:
-    return next(
-        (item for item in bundle.get("events") or [] if int(item.get("id") or 0) == int(event_id or 0)),
-        None,
-    )
-
-
-def _events_for_ids(bundle: dict[str, Any], event_ids: list[Any]) -> list[dict[str, Any]]:
-    wanted = {int(value) for value in event_ids if value is not None}
-    return [item for item in bundle.get("events") or [] if int(item.get("id") or 0) in wanted]
-
-
-def _event_nested_value(
-    event: dict[str, Any] | None, side: str, key: str
-) -> Any:
-    if not isinstance(event, dict):
-        return None
-    aliases = (
-        ("inputData", "input_data")
-        if side == "input"
-        else ("agentOutputData", "outputData", "output_data")
-    )
-    for alias in aliases:
-        value = event.get(alias)
-        if isinstance(value, dict) and value.get(key) not in (None, ""):
-            return value.get(key)
-    wrapped = event.get(side)
-    if isinstance(wrapped, dict):
-        content = wrapped.get("content")
-        if isinstance(content, dict):
-            return content.get(key)
-        if key == "summary" and isinstance(content, str):
-            return content
-    return None
-
-
-def _direct_sources_markdown(group: dict[str, Any]) -> str:
-    rows = group.get("sources") or []
-    return "\n".join(
-        f"- {item.get('businessLabel') or '相关信息'}:{int(item.get('callCount') or 0)} 次"
-        for item in rows
-    ) or "未记录具体读取项"
-
-
-def _direct_result_markdown(calls: list[dict[str, Any]]) -> str:
-    success = sum(1 for item in calls if item.get("status") == "success")
-    failure = sum(1 for item in calls if item.get("status") == "failure")
-    running = sum(1 for item in calls if item.get("status") == "running")
-    interrupted = sum(1 for item in calls if item.get("status") == "interrupted")
-    return f"共 {len(calls)} 次读取:{success} 次完成,{failure} 次失败,{running} 次进行中,{interrupted} 次因构建中断。"
-
-
-def _agent_query_markdown(run: dict[str, Any]) -> str:
-    summary = run.get("querySummary") or {}
-    total = int(summary.get("total") or 0)
-    if total == 0 and not (run.get("attempts") or []):
-        return "Agent 已完成,没有发起查询。"
-    header = (
-        f"共 {total} 次查询:"
-        f"{int(summary.get('hit') or 0)} 次命中,"
-        f"{int(summary.get('empty') or 0)} 次无结果,"
-        f"{int(summary.get('failure') or 0)} 次失败,"
-        f"{int(summary.get('interrupted') or 0)} 次因构建中断。"
-    )
-    rows = []
-    for index, attempt in enumerate(run.get("attempts") or [], 1):
-        query = attempt.get("queryLabel") or attempt.get("queryPreview") or attempt.get("businessLabel") or "查询"
-        label = {"hit": "命中", "empty": "无结果", "failure": "失败", "running": "进行中", "interrupted": "构建中断", "unknown": "结果数不明"}.get(attempt.get("status"), "结果数不明")
-        count = attempt.get("resultCount")
-        suffix = f"· {count} 条" if isinstance(count, int) and count > 0 else f"· {label}"
-        rows.append(f"{index:02d}. {query} {suffix}")
-    return "\n".join([header, *rows])
-
-
-def _agent_hit_markdown(run: dict[str, Any]) -> str:
-    attempts = run.get("attempts") or []
-    hits = [item for item in attempts if item.get("status") == "hit"]
-    empty = [item for item in attempts if item.get("status") == "empty"]
-    failures = [
-        item
-        for item in attempts
-        if item.get("status") in {"failure", "interrupted"}
-    ]
-    rows = [
-        f"- {item.get('queryLabel') or '查询'}:{int(item.get('resultCount') or 0)} 条"
-        for item in hits
-    ]
-    rows.append(f"成功命中 {len(hits)} 次,成功为空 {len(empty)} 次,技术失败或中断 {len(failures)} 次。")
-    return "\n".join(rows)
-
-
-def _screening_fallback(state: Any) -> str:
-    return {"running": "Agent 仍在整理查询结果。", "missing": "未找到 Agent 的初筛整理记录。"}.get(str(state), "初筛整理未记录。")
-
-
-def _retrieval_mode_text(mode: Any) -> str:
-    return {
-        "single": "本次只有一个取数操作。",
-        "sequential": "各项取数依次执行。",
-        "parallel": "各项取数在同一时间段并行执行。",
-        "mixed": "本次同时存在并行取数和后续取数。",
-        "unknown": "运行时间记录不足,无法确认执行先后。",
-    }.get(str(mode), "执行方式未记录。")
-
-
-def _retrieval_waves_markdown(waves: list[dict[str, Any]]) -> str:
-    if not waves:
-        return "运行时间或并发信息不足,无法安全还原执行波次。"
-    rows = []
-    for index, wave in enumerate(waves, 1):
-        operations = wave.get("operations") or wave.get("operationIds") or []
-        rows.append(
-            f"- 第 {index} 波:{len(operations)} 个取数操作"
-        )
-    return "\n".join(rows)
-
-
-def _retrieval_stage_result(stage: dict[str, Any]) -> str:
-    direct_calls = [
-        call
-        for group in stage.get("directToolGroups") or []
-        for call in group.get("calls") or []
-    ]
-    attempts = [
-        attempt
-        for run in stage.get("agentRuns") or []
-        for attempt in run.get("attempts") or []
-    ]
-    failures = sum(
-        1
-        for item in [*direct_calls, *attempts]
-        if item.get("status") in {"failure", "interrupted"}
-    )
-    hits = sum(1 for item in attempts if item.get("status") == "hit")
-    empty = sum(1 for item in attempts if item.get("status") == "empty")
-    return (
-        f"阶段状态:{stage.get('status') or '未记录'}。"
-        f"工具读取 {len(direct_calls)} 次,Agent 查询 {len(attempts)} 次;"
-        f"命中 {hits} 次,成功为空 {empty} 次,失败或中断 {failures} 次。"
-    )
-
-
-def _direct_groups_markdown(groups: list[dict[str, Any]]) -> str:
-    if not groups:
-        return "本次未记录实现 Agent 直接读取工具。"
-    return "\n".join(
-        f"- 第 {index} 组:{'、'.join(item.get('businessLabel') or '相关信息' for item in group.get('sources') or [])}({int(group.get('callCount') or 0)} 次)"
-        for index, group in enumerate(groups, 1)
-    )
-
-
-def _agent_runs_markdown(runs: list[dict[str, Any]]) -> str:
-    if not runs:
-        return "本次未记录委派取数 Agent。"
-    return "\n".join(
-        f"- {item.get('businessLabel') or '取数 Agent'}:{int((item.get('querySummary') or {}).get('total') or 0)} 次查询,初筛{'已记录' if (item.get('screening') or {}).get('state') == 'available' else '未完整记录'}"
-        for item in runs
-    )
-
-
-def _find_round(view: dict[str, Any], round_index: int | None) -> dict[str, Any] | None:
-    return next((item for item in view.get("rounds") or [] if int(item.get("roundIndex") or 0) == int(round_index or 0)), None)
-
-
-def _find_by_id(rows: list[dict[str, Any]], record_id: int) -> dict[str, Any] | None:
-    return next((item for item in rows if int(item.get("id") or 0) == record_id), None)
-
-
-def _suffix_int(value: str) -> int:
-    try:
-        return int(value.rsplit(":", 1)[1])
-    except (IndexError, ValueError) as exc:
-        raise KeyError(value) from exc
-
-
-def _node_value(node: dict[str, Any] | None) -> str | None:
-    return ((node or {}).get("card") or {}).get("primary", {}).get("value")
-
-
-def _plan_text(plan: dict[str, Any]) -> str:
-    paths = plan.get("paths") or []
-    parts = [f"方式:{plan.get('mode') or '未记录'}", f"当前保存 {len(paths)} 个方案"]
-    if plan.get("note"):
-        parts.append(f"说明:{plan['note']}")
-    revisions = int(plan.get("runtimeRevisionCount") or 0)
-    if revisions > 1:
-        parts.append(f"运行事件中找到 {revisions} 次规划记录;历史版本只在技术详情中查看。")
-    return "\n".join(parts)
-
-
-def _round_content_changes(branches: list[dict[str, Any]]) -> str:
-    merged = [item for item in branches if item.get("pathType") != "领域信息" and item.get("status") == "merged"]
-    return f"确认采用 {len(merged)} 个内容方案;无法据此还原本轮结束时的完整主脚本快照。"
-
-
-def _branch_outcomes_markdown(branches: list[dict[str, Any]]) -> str:
-    if not branches:
-        return "本轮没有可展示的方案处置记录。"
-    rows = []
-    for item in branches:
-        outcome = item.get("outcome") or {}
-        line = (
-            f"- 方案 {item.get('branchId')}:"
-            f"{outcome.get('label') or item.get('status') or '状态未记录'}"
-        )
-        if outcome.get("reasoning"):
-            line += f"。{business_text(outcome.get('reasoning'))}"
-        rows.append(line)
-    return "\n".join(rows)
-
-
-def _round_domain_changes(branches: list[dict[str, Any]]) -> str:
-    count = sum(int(item.get("output", {}).get("factCount") or 0) for item in branches if item.get("pathType") == "领域信息")
-    return f"新增 {count} 条独立保存的领域事实。"
-
-
-def _branch_facts(branch: dict[str, Any], bundle: dict[str, Any]) -> list[dict[str, Any]]:
-    refs = set(branch.get("output", {}).get("factRefs") or [])
-    return [item for item in bundle.get("domainInfo") or [] if f"domain-info:{item.get('id')}" in refs]
-
-
-def _output_exactness(output: dict[str, Any]) -> str:
-    if output.get("type") == "domain-info":
-        return "exact"
-    historical = output.get("accuracy", {}).get("historical")
-    return "historical-snapshot" if historical == "exact" else "current-only" if output.get("snapshotRef") else "unknown"
-
-
-def _artifact_exactness(artifact: dict[str, Any]) -> str:
-    return "historical-snapshot" if artifact.get("historicalAccuracy") == "exact" else "current-only"
-
-
-def _domain_facts_markdown(rows: list[dict[str, Any]]) -> str:
-    values = [business_text(row.get("content")) for row in rows]
-    return "\n".join(f"- {value}" for value in values if value)
-
-
-def _source_value(value: Any) -> str:
-    if isinstance(value, str):
-        return value
-    if isinstance(value, list):
-        return source_markdown(value)
-    if isinstance(value, dict):
-        return source_markdown([value]) or business_text(value.get("url") or value.get("name"))
-    return ""

+ 31 - 447
visualization/backend/app/main.py

@@ -1,468 +1,52 @@
 from __future__ import annotations
 
-import os
-import time
-from threading import Lock
-from typing import Optional
-
-from fastapi import FastAPI, HTTPException, Query
+from fastapi import FastAPI, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
-from fastapi.middleware.gzip import GZipMiddleware
-
-from .execution_builder import ExecutionViewBuilder
-from .execution_view_payload import compact_execution_view
-from .inspector_projection import (
-    project_activity_detail,
-    project_artifact_detail,
-    project_event_detail,
-    project_round_detail,
-)
-from .providers import LocalDatabaseProvider
-from .repositories import ActivityNotFound, BuildNotFound
-from .runtime_bridge import database_host_override, runtime_dir
-from .prompt_projection import project_prompt
-from .sanitizer import sanitize
-from .module_audit import router as module_audit_router
-from .module_audit.repository import AuditRepository
-from .card_details import CardBusinessDataProjector, CardDataNotFound
-from .card_details.common import find_detail
-from .source_lineage import InspectorWorkbenchProjector, InspectorViewNotFound
 
+from .contracts import ENUM_CATALOG, SCHEMA_CATALOG
+from .fake_data import GRAPH, RUN_ID, node_detail
+from .models import NodeDetail, RunGraph, RunSummary
 
-app = FastAPI(
-    title="Script Build Runtime Visualization",
-    description="Read-only V8 projection of script-build decisions and structured runtime events.",
-    version="8.0.0",
-)
-
-origins = [
-    value.strip()
-    for value in os.getenv(
-        "VISUALIZATION_CORS_ORIGINS",
-        "http://127.0.0.1:3008,http://localhost:3008",
-    ).split(",")
-    if value.strip()
-]
+app = FastAPI(title="Script Build Mission Control", version="1.0.0")
 app.add_middleware(
     CORSMiddleware,
-    allow_origins=origins,
-    allow_credentials=False,
+    allow_origins=["http://127.0.0.1:3008", "http://localhost:3008"],
     allow_methods=["GET"],
-    allow_headers=["Accept", "Content-Type"],
+    allow_headers=["*"],
 )
-app.add_middleware(GZipMiddleware, minimum_size=1_000, compresslevel=5)
-app.include_router(module_audit_router)
-
-provider = LocalDatabaseProvider()
-builder = ExecutionViewBuilder()
-card_data_projector = CardBusinessDataProjector()
-inspector_workbench_projector = InspectorWorkbenchProjector()
-audit_repository = AuditRepository()
-_inspector_cache: dict[tuple[int, str], tuple[float, dict]] = {}
-_inspector_cache_lock = Lock()
-_build_view_cache: dict[int, tuple[float, dict, dict]] = {}
-_build_view_cache_lock = Lock()
 
 
-def _load_bundle_and_view(script_build_id: int) -> tuple[dict, dict]:
-    """Share one read-only run snapshot across Inspector source requests."""
-    now = time.monotonic()
-    with _build_view_cache_lock:
-        cached = _build_view_cache.get(script_build_id)
-        if cached and cached[0] > now:
-            return cached[1], cached[2]
-        # The first parallel Inspector burst for a run must not issue the same
-        # ORM bundle query from several worker threads.  Build the immutable
-        # snapshot once while holding the short per-cache critical section.
-        bundle = provider.load_bundle(script_build_id)
-        view = builder.from_bundle(bundle)
-        terminal = str((bundle.get("record") or {}).get("status") or "").lower() in {
-            "success", "completed", "partial", "failed", "stopped", "cancelled"
-        }
-        ttl = 300.0 if terminal else 3.0
-        _build_view_cache[script_build_id] = (now + ttl, bundle, view)
-        if len(_build_view_cache) > 16:
-            expired = [key for key, value in _build_view_cache.items() if value[0] <= now]
-            for key in expired or list(_build_view_cache)[:4]:
-                _build_view_cache.pop(key, None)
-        return bundle, view
+@app.get("/api/health")
+def health() -> dict[str, str]:
+    return {"status": "ok", "data_mode": "fake", "schema_version": GRAPH.schema_version}
 
 
-def execution_view(script_build_id: int) -> dict:
-    _bundle, view = _load_bundle_and_view(script_build_id)
-    return compact_execution_view(view)
+@app.get("/api/runs", response_model=list[RunSummary])
+def runs() -> list[RunSummary]:
+    return [GRAPH.run]
 
 
-def _safe_load_log_windows(
-    script_build_id: int,
-    event_details: dict[int, dict],
-) -> dict[int, str]:
-    try:
-        anchors = [
-            (
-                event_id,
-                str(event.get("msg_id") or "").strip(),
-                int(event.get("event_seq")) if event.get("event_seq") is not None else None,
-            )
-            for event_id, event in event_details.items()
-            if str(event.get("msg_id") or "").strip()
-        ]
-        return audit_repository.load_event_log_windows(script_build_id, anchors)
-    except Exception:
-        # Log context is supplemental; DB/Event lineage must remain usable
-        # when the historical log table is absent or temporarily unavailable.
-        return {}
+@app.get("/api/runs/{script_build_id}/graph", response_model=RunGraph)
+def graph(script_build_id: int) -> RunGraph:
+    if script_build_id != RUN_ID:
+        raise HTTPException(status_code=404, detail="Fake run not found")
+    return GRAPH
 
 
-@app.get("/api/health")
-def health():
-    source_ok = False
-    source_error = None
-    try:
-        provider.list_builds(limit=1)
-        source_ok = True
-    except Exception as exc:  # Health remains inspectable when DB is unavailable.
-        source_error = f"{type(exc).__name__}: {exc}"
-    return {
-        "ok": True,
-        "source": {
-            "mode": provider.mode,
-            "available": source_ok,
-            "error": sanitize(source_error),
-        },
-        "runtimeDir": str(runtime_dir()),
-        "readOnly": True,
-        "databaseHostOverrideEnabled": bool(database_host_override()),
-        "databaseSessionReadOnly": True,
-    }
+@app.get("/api/runs/{script_build_id}/nodes/{node_id:path}", response_model=NodeDetail)
+def detail(script_build_id: int, node_id: str) -> NodeDetail:
+    if script_build_id != RUN_ID:
+        raise HTTPException(status_code=404, detail="Fake run not found")
+    value = node_detail(node_id)
+    if value is None:
+        raise HTTPException(status_code=404, detail="Fake node not found")
+    return value
 
 
-@app.get("/api/capabilities")
-def capabilities():
+@app.get("/api/schema-catalog")
+def schema_catalog() -> dict[str, object]:
     return {
-        "schemaVersion": "8",
-        "sourceMode": provider.mode,
-        "businessSources": [
-            "script_build_record",
-            "script_build_round",
-            "script_build_branch",
-            "script_build_data_decision",
-            "script_build_multipath_decision",
-            "script_build_domain_info",
-            "script_build_paragraph",
-            "script_build_element",
-            "script_build_paragraph_element",
-        ],
-        "runtimeSupplement": ["script_build_event", "script_build_event_body"],
-        "readOnly": True,
-        "databaseHostOverrideEnabled": bool(database_host_override()),
-        "databaseSessionReadOnly": True,
+        "data_mode": "fake",
+        "schemas": {key: list(value) for key, value in SCHEMA_CATALOG.items()},
+        "enums": {key: list(value) for key, value in ENUM_CATALOG.items()},
     }
-
-
-@app.get("/api/script-builds")
-def list_script_builds(
-    limit: int = Query(30, ge=1, le=100), status: Optional[str] = None
-):
-    try:
-        return {
-            "items": provider.list_builds(limit=limit, status=status),
-            "sourceMode": provider.mode,
-            "warnings": [],
-        }
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取构建列表失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/execution-view")
-def get_execution_view(script_build_id: int):
-    try:
-        return execution_view(script_build_id)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取构建失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/card-data/{detail_ref:path}")
-def get_card_data(script_build_id: int, detail_ref: str):
-    try:
-        bundle, view = _load_bundle_and_view(script_build_id)
-        return sanitize(card_data_projector.project(
-            detail_ref,
-            bundle=bundle,
-            view=view,
-            load_event=lambda event_id: provider.load_event_detail(
-                script_build_id, event_id
-            ),
-        ))
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except (ActivityNotFound, CardDataNotFound) as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取卡片数据流失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/inspector-view/{detail_ref:path}")
-def get_inspector_view(script_build_id: int, detail_ref: str):
-    """Return business, evidence and raw runtime columns in one response."""
-    try:
-        key = (script_build_id, detail_ref)
-        now = time.monotonic()
-        with _inspector_cache_lock:
-            cached = _inspector_cache.get(key)
-            if cached and cached[0] > now:
-                return cached[1]
-
-        bundle, view = _load_bundle_and_view(script_build_id)
-        record = bundle.get("record") or {}
-        event_ids = inspector_workbench_projector.related_event_ids(
-            detail_ref,
-            bundle=bundle,
-            view=view,
-        )
-        event_details = provider.load_event_details(script_build_id, event_ids)
-        # Event bodies have already been redacted and bounded by the
-        # repository.  Do not impose a second lower text limit here, which
-        # would silently invalidate selectors without updating completeness.
-        result = sanitize(
-            inspector_workbench_projector.project(
-                detail_ref,
-                script_build_id=script_build_id,
-                bundle=bundle,
-                view=view,
-                event_details=event_details,
-                log_contents=_safe_load_log_windows(script_build_id, event_details),
-            ),
-            max_text=None,
-        )
-        terminal = str(record.get("status") or "").lower() in {
-            "success", "completed", "partial", "failed", "stopped", "cancelled"
-        }
-        ttl = 300.0 if terminal else 3.0
-        with _inspector_cache_lock:
-            _inspector_cache[key] = (now + ttl, result)
-            if len(_inspector_cache) > 256:
-                expired = [item for item, value in _inspector_cache.items() if value[0] <= now]
-                for item in expired or list(_inspector_cache)[:64]:
-                    _inspector_cache.pop(item, None)
-        return result
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except (ActivityNotFound, InspectorViewNotFound) as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取 Inspector 数据来源失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/rounds/{round_index}")
-def get_round_detail(script_build_id: int, round_index: int):
-    try:
-        bundle = provider.load_bundle(script_build_id)
-        view = builder.from_bundle(bundle)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取轮次失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-    round_ = next(
-        (
-            item
-            for item in view.get("rounds", [])
-            if int(item.get("roundIndex") or 0) == round_index
-        ),
-        None,
-    )
-    if round_ is None:
-        raise HTTPException(status_code=404, detail=f"未找到第 {round_index} 轮")
-    return project_round_detail(script_build_id, round_, bundle)
-
-
-@app.get("/api/script-builds/{script_build_id}/activities/{activity_id:path}")
-def get_activity_detail(script_build_id: int, activity_id: str):
-    if activity_id.startswith("event:"):
-        try:
-            bundle, view = _load_bundle_and_view(script_build_id)
-            event_id = int(activity_id.rsplit(":", 1)[1])
-            event_detail = provider.load_event_detail(script_build_id, event_id)
-            try:
-                return project_activity_detail(
-                    script_build_id,
-                    activity_id,
-                    view,
-                    bundle,
-                    event_detail=event_detail,
-                )
-            except KeyError:
-                pass
-            return project_event_detail(
-                script_build_id,
-                event_detail,
-                run_status=(view.get("header") or {}).get("status"),
-            )
-        except (ValueError, ActivityNotFound) as exc:
-            raise HTTPException(status_code=404, detail=f"未找到活动 {activity_id}") from exc
-        except Exception as exc:
-            raise HTTPException(
-                status_code=503,
-                detail=f"读取运行事件失败:{type(exc).__name__}: {sanitize(str(exc))}",
-            ) from exc
-    try:
-        bundle, view = _load_bundle_and_view(script_build_id)
-        event_detail = None
-        if activity_id.startswith("retrieval-agent:"):
-            run = find_detail(view, activity_id)
-            event_id = int((run or {}).get("eventId") or 0)
-            if event_id:
-                event_detail = provider.load_event_detail(script_build_id, event_id)
-        return project_activity_detail(
-            script_build_id,
-            activity_id,
-            view,
-            bundle,
-            event_detail=event_detail,
-        )
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except KeyError as exc:
-        raise HTTPException(status_code=404, detail=f"未找到活动 {activity_id}") from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取活动详情失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/decision-prompts/{prompt_ref:path}")
-def get_decision_prompt(script_build_id: int, prompt_ref: str):
-    try:
-        return project_prompt(
-            provider.load_prompt_context(script_build_id, prompt_ref)
-        )
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except ActivityNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取 Agent 提示词失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@app.get("/api/script-builds/{script_build_id}/artifacts/{snapshot_ref:path}")
-def get_artifact(script_build_id: int, snapshot_ref: str):
-    if snapshot_ref in {"base:current", "artifact:base:current"}:
-        try:
-            artifact = provider.load_current_artifact(script_build_id)
-        except BuildNotFound as exc:
-            raise HTTPException(status_code=404, detail=str(exc)) from exc
-        except Exception as exc:
-            raise HTTPException(
-                status_code=503,
-                detail=f"读取创作表失败:{type(exc).__name__}: {sanitize(str(exc))}",
-            ) from exc
-        return project_artifact_detail(
-            script_build_id,
-            "artifact:base:current",
-            {
-                **artifact,
-                "artifactContext": {
-                    "type": "final",
-                    "versionKind": "current-base",
-                    "note": "当前保存的最终主脚本",
-                },
-            },
-        )
-    round_ref = snapshot_ref.removeprefix("artifact:").removeprefix("round:")
-    if round_ref.isdigit() and "round:" in snapshot_ref:
-        round_index = int(round_ref)
-        try:
-            bundle = provider.load_bundle(script_build_id)
-        except BuildNotFound as exc:
-            raise HTTPException(status_code=404, detail=str(exc)) from exc
-        except Exception as exc:
-            raise HTTPException(
-                status_code=503,
-                detail=f"读取本轮产出脚本表失败:{type(exc).__name__}: {sanitize(str(exc))}",
-            ) from exc
-        if not any(int(item.get("round_index") or 0) == round_index for item in bundle.get("rounds", [])):
-            raise HTTPException(status_code=404, detail=f"未找到第 {round_index} 轮")
-        return project_artifact_detail(
-            script_build_id,
-            f"artifact:round:{round_index}",
-            {
-                **(bundle.get("currentArtifact") or {}),
-                "artifactContext": {
-                    "type": "round",
-                    "roundIndex": round_index,
-                    "versionKind": "current-base",
-                    "note": f"当前数据库未保存第 {round_index} 轮结束时的独立快照;这里展示当前保存的主脚本。",
-                },
-            },
-        )
-    try:
-        bundle = provider.load_bundle(script_build_id)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取创作表失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-    match = snapshot_ref.removeprefix("artifact:").removeprefix("branch:")
-    if match.isdigit():
-        branch = next(
-            (
-                item
-                for item in bundle.get("branches", [])
-                if int(item.get("branch_id") or 0) == int(match)
-            ),
-            None,
-        )
-        if branch:
-            if str(branch.get("path_type") or "").strip() == "领域信息":
-                raise HTTPException(
-                    status_code=404,
-                    detail="领域信息方案不产出候选脚本表",
-                )
-            candidate = branch.get("candidate_snapshot") or {}
-            if not isinstance(candidate, dict) or not isinstance(candidate.get("snapshot"), dict):
-                raise HTTPException(
-                    status_code=404,
-                    detail="该实现方案没有可用的候选脚本快照",
-                )
-            return project_artifact_detail(
-                script_build_id,
-                f"artifact:branch:{match}",
-                {
-                    **candidate,
-                    "artifactContext": {
-                        "type": "candidate",
-                        "roundIndex": branch.get("round_index"),
-                        "branchId": branch.get("branch_id"),
-                        "branchStatus": branch.get("status"),
-                        "pathType": branch.get("path_type"),
-                        "versionKind": candidate.get("snapshotKind"),
-                        "historicalAccuracy": candidate.get("historicalAccuracy"),
-                        "currentProjectionAccuracy": candidate.get("currentProjectionAccuracy"),
-                        "note": candidate.get("note"),
-                    },
-                },
-            )
-    raise HTTPException(status_code=404, detail=f"未找到创作表快照 {snapshot_ref}")

+ 0 - 712
visualization/backend/app/main_agent_decision_projection.py

@@ -1,712 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from datetime import datetime, timezone
-from typing import Any, Iterable
-
-from .evaluation_report_parser import EvaluationReportParser
-from .main_decision_text import parse_goal_text, parse_objective_text
-from .runtime_event_index import (
-    EventBounds,
-    RuntimeEventIndex,
-    detail_ref,
-    event_input,
-    event_output,
-    event_sequence,
-    event_summary,
-    event_text_output,
-    integer,
-)
-from .runtime_tool_catalog import business_tool_label
-
-
-class MainAgentDecisionProjector:
-    """Build honest decision contexts for the first three main-Agent stages.
-
-    Database rows are passed in by the caller and remain the current business
-    truth.  Runtime events only describe how that persisted result was reached.
-    """
-
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        script_direction: str | None,
-        rounds: Iterable[dict[str, Any]],
-        multipath_decisions: Iterable[dict[str, Any]] = (),
-    ) -> dict[str, Any]:
-        round_rows = sorted(
-            (dict(row) for row in rounds),
-            key=lambda row: integer(row.get("round_index")) or 0,
-        )
-        decisions = [dict(item) for item in multipath_decisions]
-        planning_analysis = PlanningAnalysisProjector().project(index)
-        objective = ObjectiveDecisionProjector().project(
-            index, script_direction=script_direction
-        )
-        goals: dict[int, dict[str, Any]] = {}
-        plans: dict[int, dict[str, Any]] = {}
-        for position, row in enumerate(round_rows):
-            round_index = integer(row.get("round_index"))
-            if round_index is None:
-                continue
-            goals[round_index] = RoundGoalDecisionProjector().project(
-                index,
-                round_row=row,
-                objective_context=objective,
-                previous_round=(round_rows[position - 1] if position else None),
-                multipath_decisions=decisions,
-            )
-            plans[round_index] = ImplementationPlanDecisionProjector().project(
-                index, round_row=row, goal_context=goals[round_index]
-            )
-        projection = {
-            "planningAnalysis": planning_analysis,
-            "objective": objective,
-            "roundGoalsByRound": goals,
-            "implementationPlansByRound": plans,
-        }
-        assigned_technical_refs = {
-            ref
-            for context in [planning_analysis, objective, *goals.values(), *plans.values()]
-            for ref in context.get("technicalRefs") or []
-        }
-        projection["unassigned"] = [
-            event_summary(event, "failed-main-decision-call-unassigned")
-            for name in ("save_script_direction", "begin_round", "record_multipath_plan")
-            for event in index.anchors(name, successful=False)
-            if detail_ref(event) not in assigned_technical_refs
-        ]
-        return projection
-
-
-class PlanningAnalysisProjector:
-    """Project the last real main-Agent planning note before direction is saved."""
-
-    def project(self, index: RuntimeEventIndex) -> dict[str, Any]:
-        direction_saves = index.anchors("save_script_direction", successful=True)
-        first_save_seq = event_sequence(direction_saves[0]) if direction_saves else None
-        candidates = index.anchors(
-            "think_and_plan",
-            successful=True,
-            bounds=EventBounds(before_seq=first_save_seq),
-        )
-        event = candidates[-1] if candidates else None
-        raw_input = event_input(event) if event else None
-        data = raw_input if isinstance(raw_input, dict) else {}
-        event_ref = detail_ref(event) if event else None
-        return {
-            "stage": "planning-analysis",
-            "summary": _detail_text(data.get("thought_summary")),
-            "thought": _detail_text(data.get("thought")),
-            "plan": _detail_text(data.get("plan")),
-            "action": _detail_text(data.get("action")),
-            "eventRef": event_ref,
-            "technicalRefs": [event_ref] if event_ref else [],
-            "completeness": "complete" if event_ref and data else "missing",
-        }
-
-
-class ObjectiveDecisionProjector:
-    def project(
-        self, index: RuntimeEventIndex, *, script_direction: str | None
-    ) -> dict[str, Any]:
-        successful = index.anchors("save_script_direction", successful=True)
-        failed = index.anchors("save_script_direction", successful=False)
-        current_anchor = successful[-1] if successful else None
-        previous_seq = event_sequence(successful[-2]) if len(successful) > 1 else None
-        current_seq = event_sequence(current_anchor) if current_anchor else None
-        scope_id = integer(current_anchor.get("scope_event_id")) if current_anchor else None
-        bounds = EventBounds(after_seq=previous_seq, before_seq=current_seq)
-        reads = index.main_direct_reads(
-            scope_id=scope_id,
-            bounds=bounds,
-        )
-        failed_reads = index.main_direct_reads(
-            scope_id=scope_id, bounds=bounds, successful=False
-        )
-        parsed = parse_script_direction(script_direction)
-        revisions = [_direction_revision(event, script_direction) for event in successful]
-        return {
-            "stage": "objective",
-            "inputs": [_decision_read(event) for event in reads],
-            "decisionItems": parsed["goals"],
-            "explicitReasoning": parsed["valueReasoning"],
-            "constraints": parsed["constraints"],
-            "revisionRefs": [ref for event in successful if (ref := detail_ref(event))],
-            "completeness": _completeness(bool(script_direction), bool(current_anchor)),
-            "revisions": revisions,
-            "currentRevisionRef": _current_revision_ref(revisions),
-            "technicalRefs": [
-                ref
-                for event in [*failed_reads, *failed]
-                if (ref := detail_ref(event))
-            ],
-            "currentSavedValue": script_direction,
-            "sourceDocument": parsed["raw"],
-            "valueJudgements": parsed["valueJudgements"],
-        }
-
-
-class RoundGoalDecisionProjector:
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        round_row: dict[str, Any],
-        objective_context: dict[str, Any],
-        previous_round: dict[str, Any] | None,
-        multipath_decisions: Iterable[dict[str, Any]],
-    ) -> dict[str, Any]:
-        round_index = integer(round_row.get("round_index"))
-        goal = _text(round_row.get("goal"))
-        begin_events = index.successful_begin_rounds().get(round_index or -1, [])
-        current_anchor = begin_events[-1] if begin_events else None
-        current_seq = event_sequence(current_anchor) if current_anchor else None
-        inputs: list[dict[str, Any]] = []
-        constraints: list[str] = []
-        late_decision_refs: list[str] = []
-
-        if round_index == 1:
-            if objective_context.get("decisionItems"):
-                inputs.append(
-                    {
-                        "label": "当前保存的创作目标",
-                        "summary": _detail_text(
-                            "\n\n".join(
-                                str(item)
-                                for item in objective_context["decisionItems"]
-                            )
-                        ),
-                        "actor": "main",
-                        "relation": "standing-constraint",
-                        "detailRef": objective_context.get("currentRevisionRef"),
-                    }
-                )
-            lower_seq = _last_objective_save_seq(index)
-        else:
-            previous_index = integer((previous_round or {}).get("round_index"))
-            lower_seq = None
-            reports = index.agent_returns(
-                "script_evaluator",
-                round_index=previous_index,
-                before_seq=current_seq,
-            )
-            if reports:
-                evaluator, relation = reports[-1]
-                parsed_report = EvaluationReportParser().parse(
-                    event_text_output(evaluator) or "", kind="overall"
-                )
-                inputs.append(
-                    {
-                        "label": "上一轮整体评审",
-                        "summary": _parsed_report_summary(parsed_report),
-                        "actor": "overall-evaluator",
-                        "relation": relation,
-                        "detailRef": detail_ref(evaluator),
-                    }
-                )
-                constraints.extend(_parsed_report_guidance(parsed_report))
-                lower_seq = event_sequence(evaluator)
-
-            previous_decisions = sorted(
-                [
-                item
-                for item in multipath_decisions
-                if integer(item.get("round_index")) == previous_index
-                ],
-                key=lambda item: (str(item.get("created_at") or ""), integer(item.get("id")) or 0),
-            )
-            for decision in previous_decisions:
-                decision_ref = (
-                    f"multipath-decision:{decision.get('id')}"
-                    if decision.get("id") is not None
-                    else None
-                )
-                if not _record_precedes_event(decision, current_anchor):
-                    if decision_ref:
-                        late_decision_refs.append(decision_ref)
-                    continue
-                inputs.append(
-                    {
-                        "label": "上一轮主 Agent 多路决策",
-                        "summary": _detail_text(decision.get("decision")),
-                        "actor": "main",
-                        "relation": "persisted-record",
-                        "detailRef": decision_ref,
-                    }
-                )
-
-        if current_anchor is not None:
-            scope_id = integer(current_anchor.get("scope_event_id"))
-            read_bounds = EventBounds(after_seq=lower_seq, before_seq=current_seq)
-            reads = index.main_direct_reads(
-                scope_id=scope_id,
-                bounds=read_bounds,
-            )
-            inputs.extend(_decision_read(event) for event in reads)
-            failed_reads = index.main_direct_reads(
-                scope_id=scope_id, bounds=read_bounds, successful=False
-            )
-        else:
-            failed_reads = []
-
-        failed = [
-            event
-            for event in index.anchors("begin_round", successful=False)
-            if _explicit_output_round_hint(event) == round_index
-        ]
-        return {
-            "stage": "round-goal",
-            "inputs": _dedupe_inputs(inputs),
-            "decisionItems": [goal] if goal else [],
-            "sourceDocument": goal,
-            "explicitReasoning": _labelled_reasoning(goal),
-            "constraints": _unique(constraints),
-            "revisionRefs": [ref for event in begin_events if (ref := detail_ref(event))],
-            "completeness": _completeness(bool(goal), bool(current_anchor)),
-            "technicalRefs": [
-                ref
-                for event in [*failed_reads, *failed]
-                if (ref := detail_ref(event))
-            ] + late_decision_refs,
-            "currentSavedValue": goal,
-            "roundIndex": round_index,
-        }
-
-
-class ImplementationPlanDecisionProjector:
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        round_row: dict[str, Any],
-        goal_context: dict[str, Any],
-    ) -> dict[str, Any]:
-        round_index = integer(round_row.get("round_index"))
-        paths = round_row.get("multipath_plan")
-        paths = paths if isinstance(paths, list) else []
-        mode = _text(round_row.get("race_or_divide"))
-        note = _text(round_row.get("plan_note"))
-        successful: list[dict[str, Any]] = []
-        failed: list[dict[str, Any]] = []
-        for event in index.anchors("record_multipath_plan", successful=None):
-            resolved_round = _round_hint(event)
-            if resolved_round != round_index:
-                continue
-            (successful if _anchor_success(event) else failed).append(event)
-
-        current_anchor = successful[-1] if successful else None
-        begin_events = index.successful_begin_rounds().get(round_index or -1, [])
-        lower_seq = event_sequence(begin_events[-1]) if begin_events else None
-        current_seq = event_sequence(current_anchor) if current_anchor else None
-        scope_id = integer(current_anchor.get("scope_event_id")) if current_anchor else None
-        inputs: list[dict[str, Any]] = []
-        goal_items = goal_context.get("decisionItems") or []
-        if goal_items:
-            inputs.append(
-                {
-                    "label": "本轮目标",
-                    "summary": _detail_text("\n".join(str(item) for item in goal_items)),
-                    "actor": "main",
-                    "relation": "standing-constraint",
-                    "detailRef": (goal_context.get("revisionRefs") or [None])[-1],
-                }
-            )
-        if current_anchor is not None:
-            read_bounds = EventBounds(after_seq=lower_seq, before_seq=current_seq)
-            inputs.extend(
-                _decision_read(event)
-                for event in index.main_direct_reads(
-                    scope_id=scope_id,
-                    bounds=read_bounds,
-                )
-            )
-            failed_reads = index.main_direct_reads(
-                scope_id=scope_id, bounds=read_bounds, successful=False
-            )
-        else:
-            failed_reads = []
-
-        revisions = [_plan_revision(event, paths, mode, note) for event in successful]
-        display_mode = "单路" if len(paths) == 1 else mode
-        return {
-            "stage": "implementation-plan",
-            "inputs": _dedupe_inputs(inputs),
-            "decisionItems": [_path_summary(path, index + 1) for index, path in enumerate(paths)],
-            "explicitReasoning": note,
-            "constraints": [],
-            "revisionRefs": [ref for event in successful if (ref := detail_ref(event))],
-            "completeness": _completeness(bool(paths), bool(current_anchor)),
-            "revisions": revisions,
-            "currentRevisionRef": _current_revision_ref(revisions),
-            "technicalRefs": [
-                ref
-                for event in [*failed_reads, *failed]
-                if (ref := detail_ref(event))
-            ],
-            "roundIndex": round_index,
-            "displayMode": display_mode,
-            "rawMode": mode,
-            "pathCount": len(paths),
-            "routeItems": [_path_item(path, index + 1) for index, path in enumerate(paths)],
-        }
-
-
-def parse_script_direction(value: str | None) -> dict[str, Any]:
-    parsed = parse_objective_text(value)
-    goals = list(parsed.get("targets") or [])
-    if not goals and parsed.get("raw"):
-        goals = [str(parsed.get("headline"))]
-    constraints = _unique(
-        [
-            *(str(item) for item in parsed.get("domainDimensions") or []),
-            *(str(item) for item in parsed.get("constraints") or []),
-        ]
-    )
-    value_judgements = [
-        {"label": label, "value": content}
-        for label, content in (
-            ("选题价值主张", parsed.get("topicValue")),
-            ("账号价值主张", parsed.get("accountValue")),
-        )
-        if content
-    ]
-    reasoning = "\n".join(
-        f"{item['label']}:{item['value']}" for item in value_judgements
-    ) or None
-    return {
-        "goals": _unique(goals),
-        "constraints": _unique(constraints),
-        "valueJudgements": value_judgements,
-        "valueReasoning": reasoning,
-        "raw": str(parsed.get("raw") or ""),
-    }
-
-
-def parse_goal_items(value: str | None) -> list[str]:
-    parsed = parse_goal_text(value)
-    if not parsed.get("raw"):
-        return []
-    return [str(parsed["raw"])]
-
-
-def _decision_read(event: dict[str, Any]) -> dict[str, Any]:
-    return {
-        "label": business_tool_label(str(event.get("event_name") or "")),
-        "summary": _read_result_summary(event),
-        "actor": "main",
-        "relation": "direct-read",
-        "detailRef": detail_ref(event),
-    }
-
-
-def _read_result_summary(event: dict[str, Any]) -> str:
-    name = str(event.get("event_name") or "")
-    output = event_output(event)
-    if isinstance(output, dict):
-        if name == "get_topic_detail":
-            topic = output.get("topic") if isinstance(output.get("topic"), dict) else {}
-            points = output.get("points") if isinstance(output.get("points"), list) else []
-            point_name = next(
-                (
-                    _text(point.get("point_result"))
-                    for point in points
-                    if isinstance(point, dict) and _text(point.get("point_result"))
-                ),
-                None,
-            )
-            topic_result = _text(topic.get("result"))
-            summary = ":".join(value for value in (point_name, topic_result) if value)
-            if summary:
-                return _detail_text(summary) or "已读取选题"
-        if name == "get_account_script_section_patterns":
-            summary = _detail_text(output.get("分段规律摘要"))
-            if summary:
-                return summary
-        if name == "get_account_script_persona_points":
-            persona = _persona_summary(output)
-            if persona:
-                return persona
-        for key in ("count", "returned_count", "result_count", "total"):
-            count = integer(output.get(key))
-            if count is not None:
-                return f"返回 {count} 条记录"
-        for key in ("summary", "message"):
-            value = _detail_text(output.get(key))
-            if value:
-                return value
-    raw_preview = str(event.get("output_preview") or "")
-    if name == "get_topic_detail":
-        topic_result = _json_string_field(raw_preview, "result")
-        point_name = _json_string_field(raw_preview, "point_result")
-        summary = ":".join(value for value in (point_name, topic_result) if value)
-        if summary:
-            return _detail_text(summary) or "已读取选题"
-    if name == "get_account_script_section_patterns":
-        summary = _json_string_field(raw_preview, "分段规律摘要")
-        if summary:
-            return _detail_text(summary) or "已读取账号段落模式"
-    if name == "get_account_script_persona_points":
-        count_match = re.search(r'"returned_count"\s*:\s*(\d+)', raw_preview)
-        columns = _json_string_fields(raw_preview, "列")
-        if count_match:
-            suffix = f",主要涉及{'、'.join(columns[:3])}" if columns else ""
-            return f"返回 {count_match.group(1)} 个账号表达特征{suffix}"
-    return "已读取"
-
-
-def _persona_summary(output: dict[str, Any]) -> str | None:
-    count = integer(output.get("returned_count"))
-    records = output.get("records") if isinstance(output.get("records"), list) else []
-    columns = _unique(
-        str(item.get("列"))
-        for item in records
-        if isinstance(item, dict) and item.get("列")
-    )
-    if count is None:
-        count = len(records) if records else None
-    if count is None:
-        return None
-    suffix = f",主要涉及{'、'.join(columns[:3])}" if columns else ""
-    return f"返回 {count} 个账号表达特征{suffix}"
-
-
-def _json_string_field(value: str, key: str) -> str | None:
-    values = _json_string_fields(value, key)
-    return values[0] if values else None
-
-
-def _json_string_fields(value: str, key: str) -> list[str]:
-    results: list[str] = []
-    pattern = re.compile(
-        rf'"{re.escape(key)}"\s*:\s*"((?:\\.|[^"\\])*)"'
-    )
-    for match in pattern.finditer(value):
-        try:
-            decoded = json.loads(f'"{match.group(1)}"')
-        except json.JSONDecodeError:
-            decoded = match.group(1)
-        text = _text(decoded)
-        if text and text not in results:
-            results.append(text)
-    return results
-
-
-def _direction_revision(
-    event: dict[str, Any], current_direction: str | None
-) -> dict[str, Any]:
-    payload = event_input(event)
-    saved = payload.get("script_direction") if isinstance(payload, dict) else None
-    return {
-        "eventId": integer(event.get("id")),
-        "eventSeq": event_sequence(event),
-        "savedValue": saved,
-        "current": _normalized_compare(saved) == _normalized_compare(current_direction),
-        "detailRef": detail_ref(event),
-    }
-
-
-def _plan_revision(
-    event: dict[str, Any], current_paths: list[Any], current_mode: str | None, current_note: str | None
-) -> dict[str, Any]:
-    payload = event_input(event)
-    payload = payload if isinstance(payload, dict) else {}
-    paths = payload.get("paths") if isinstance(payload.get("paths"), list) else []
-    mode = _normalized_plan_mode(payload.get("mode"))
-    normalized_current_mode = _normalized_plan_mode(current_mode)
-    note = _text(payload.get("note"))
-    current = (
-        _canonical_json(paths) == _canonical_json(current_paths)
-        and (mode or normalized_current_mode) == normalized_current_mode
-        and (note if note is not None else current_note) == current_note
-    )
-    return {
-        "eventId": integer(event.get("id")),
-        "eventSeq": event_sequence(event),
-        "paths": paths,
-        "mode": mode,
-        "note": note,
-        "current": current,
-        "detailRef": detail_ref(event),
-    }
-
-
-def _normalized_plan_mode(value: Any) -> str | None:
-    mode = _text(value)
-    aliases = {"race": "竞争", "赛马": "竞争", "divide": "分工"}
-    return aliases.get((mode or "").lower(), mode)
-
-
-def _path_item(path: Any, fallback_index: int) -> dict[str, Any]:
-    data = path if isinstance(path, dict) else {}
-    return {
-        "pathIndex": integer(data.get("path_index")) or fallback_index,
-        "pathType": _text(data.get("path_type")),
-        "action": _text(data.get("action")),
-        "target": _text(data.get("target")),
-        "method": _text(data.get("method")),
-        "emphasis": _text(data.get("emphasis")),
-    }
-
-
-def _path_summary(path: Any, fallback_index: int) -> str:
-    item = _path_item(path, fallback_index)
-    parts = [part for part in (item["action"], item["target"]) if part]
-    headline = ":".join(parts) if parts else f"方案 {item['pathIndex']}"
-    return f"方案 {item['pathIndex']} · {headline}"
-
-
-def _parsed_report_summary(report: dict[str, Any]) -> str | None:
-    if report.get("conclusion"):
-        return _detail_text(report["conclusion"])
-    if report.get("recommendation"):
-        return _detail_text(report["recommendation"])
-    problems = report.get("problems") or []
-    if problems and isinstance(problems[0], dict):
-        return _detail_text(problems[0].get("summary"))
-    return None
-
-
-def _parsed_report_guidance(report: dict[str, Any]) -> list[str]:
-    values: list[str] = []
-    for item in report.get("problems") or []:
-        if isinstance(item, dict) and (summary := _text(item.get("summary"))):
-            values.append(f"问题:{summary}")
-    if next_goal := _text(report.get("nextGoal")):
-        values.append(f"下一步:{next_goal}")
-    return _unique(values)
-
-
-def _record_precedes_event(
-    record: dict[str, Any], event: dict[str, Any] | None
-) -> bool:
-    if event is None:
-        return False
-    record_time = _date_value(record.get("created_at"))
-    event_time = _date_value(event.get("started_at") or event.get("ended_at"))
-    return record_time is not None and event_time is not None and record_time <= event_time
-
-
-def _date_value(value: Any) -> datetime | None:
-    if isinstance(value, datetime):
-        parsed = value
-    elif isinstance(value, str) and value.strip():
-        try:
-            parsed = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
-        except ValueError:
-            return None
-    else:
-        return None
-    if parsed.tzinfo is None:
-        parsed = parsed.replace(tzinfo=timezone.utc)
-    return parsed.astimezone(timezone.utc)
-
-
-def _labelled_reasoning(text: str | None) -> str | None:
-    value = _normalize_markdown(text)
-    match = re.search(r"(?im)^\s*(?:\u7406\u7531|\u76ee\u6807\u6765\u6e90)\s*[\uff1a:]\s*(.+)$", value)
-    return _clean_line(match.group(1)) if match else None
-
-
-def _last_objective_save_seq(index: RuntimeEventIndex) -> int | None:
-    saves = index.anchors("save_script_direction", successful=True)
-    return event_sequence(saves[-1]) if saves else None
-
-
-def _round_hint(event: dict[str, Any]) -> int | None:
-    output = event_output(event)
-    if isinstance(output, dict):
-        value = integer(output.get("round_index"))
-        if value is not None:
-            return value
-    return integer(event.get("round_index"))
-
-
-def _explicit_output_round_hint(event: dict[str, Any]) -> int | None:
-    output = event_output(event)
-    return integer(output.get("round_index")) if isinstance(output, dict) else None
-
-
-def _anchor_success(event: dict[str, Any]) -> bool:
-    # Imported lazily to keep the public API above concise and avoid duplicating
-    # success/error interpretation.
-    from .runtime_event_index import event_succeeded
-
-    return event_succeeded(event)
-
-
-def _current_revision_ref(revisions: list[dict[str, Any]]) -> str | None:
-    for revision in reversed(revisions):
-        if revision.get("current"):
-            return revision.get("detailRef")
-    return None
-
-
-def _normalize_markdown(value: Any) -> str:
-    text = str(value or "").strip()
-    if "\\n" in text:
-        text = text.replace("\\n", "\n")
-    return text.replace("\r\n", "\n").replace("\r", "\n")
-
-
-def _clean_line(value: Any) -> str:
-    text = str(value or "")
-    text = re.sub(r"^\s*(?:[-*+]\s+|\d+[.、]\s+|[\u2460-\u2473]\s*)", "", text)
-    text = re.sub(r"[`*#]", "", text)
-    return re.sub(r"\s+", " ", text).strip(" ::")
-
-
-def _plain_text(value: Any) -> str:
-    lines = [_clean_line(line) for line in _normalize_markdown(value).splitlines()]
-    return " ".join(line for line in lines if line)
-
-
-def _detail_text(value: Any) -> str:
-    text = _normalize_markdown(value)
-    text = re.sub(r"(?m)^\s{0,3}#{1,6}\s*", "", text)
-    text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
-    text = re.sub(r"`([^`]*)`", r"\1", text)
-    return text.strip()
-
-
-def _text(value: Any) -> str | None:
-    text = str(value).strip() if value is not None else ""
-    return text or None
-
-
-def _canonical_json(value: Any) -> str:
-    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
-
-
-def _normalized_compare(value: Any) -> str:
-    return re.sub(r"\s+", "", str(value or ""))
-
-
-def _completeness(has_fact: bool, has_runtime_anchor: bool) -> str:
-    if has_fact and has_runtime_anchor:
-        return "complete"
-    if has_fact or has_runtime_anchor:
-        return "partial"
-    return "missing"
-
-
-def _unique(values: Iterable[str]) -> list[str]:
-    result: list[str] = []
-    for value in values:
-        if value and value not in result:
-            result.append(value)
-    return result
-
-
-def _dedupe_inputs(values: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
-    result: list[dict[str, Any]] = []
-    seen: set[tuple[Any, ...]] = set()
-    for value in values:
-        key = (value.get("label"), value.get("detailRef"), value.get("relation"))
-        if key in seen:
-            continue
-        seen.add(key)
-        result.append(value)
-    return result

+ 0 - 274
visualization/backend/app/main_decision_text.py

@@ -1,274 +0,0 @@
-from __future__ import annotations
-
-import re
-from typing import Any
-
-
-_HEADING = re.compile(r"(?m)^\s*#{1,6}\s+(.+?)\s*$")
-_TARGET_SENTENCE = re.compile(r"(?im)^\s*[-*]?\s*目标语句\s*[::]\s*(.+?)\s*$")
-_FIELD_LINE = re.compile(r"(?im)^\s*[-*]?\s*([^::\n]{1,24})\s*[::]\s*(.+?)\s*$")
-_BULLET = re.compile(r"^\s*(?:[-*+]\s+|[①②③④⑤⑥⑦⑧⑨⑩]\s*|\d{1,2}[.、)]\s*)")
-
-
-def parse_objective_text(value: Any) -> dict[str, Any]:
-    """Extract business sections from the persisted direction without inventing text."""
-
-    text = _text(value)
-    if not text:
-        return {
-            "headline": "创作方向未记录",
-            "targets": [],
-            "topicValue": None,
-            "accountValue": None,
-            "domainDimensions": [],
-            "constraints": [],
-            "constraintItems": [],
-            "constraintGroups": [],
-            "targetCount": 0,
-            "domainDimensionCount": 0,
-            "structured": False,
-            "raw": "",
-        }
-
-    sections = _markdown_sections(text)
-    targets = [_clean(match.group(1)) for match in _TARGET_SENTENCE.finditer(text)]
-    if not targets:
-        target_section = _first_section(sections, "创作总目标", "创作目标", "总目标")
-        targets = _meaningful_lines(target_section)[:3]
-
-    constraint_groups = _objective_constraint_groups(text)
-    domain_dimensions = [
-        str(group.get("dimensionName"))
-        for group in constraint_groups
-        if group.get("kind") == "domain" and group.get("dimensionName")
-    ]
-    if not domain_dimensions:
-        domain_section = _first_section(sections, "领域评估维度", "领域维度")
-        domain_dimensions = [
-            line
-            for line in _meaningful_lines(domain_section)
-            if not re.match(
-                r"^(?:评估内容|通过标准|强制性|前置条件)\s*[::]", line
-            )
-        ]
-    constraint_items = [
-        {"group": group["title"], **item}
-        for group in constraint_groups
-        for item in group.get("items") or []
-        if item.get("label") in {"通过标准", "强制性", "前置条件", "约束条件"}
-    ]
-    constraints = [item["value"] for item in constraint_items]
-    topic_value = _section_preview(_first_section(sections, "选题价值主张"))
-    account_value = _section_preview(_first_section(sections, "账号价值主张"))
-    headline = targets[0] if targets else _first_business_sentence(text)
-    return {
-        "headline": headline or _clean(text),
-        "targets": _unique(targets),
-        "topicValue": topic_value,
-        "accountValue": account_value,
-        "domainDimensions": _unique(domain_dimensions),
-        "constraints": _unique(constraints),
-        "constraintItems": _unique_records(constraint_items),
-        "constraintGroups": constraint_groups,
-        "targetCount": len(_unique(targets)),
-        "domainDimensionCount": len(_unique(domain_dimensions)),
-        "structured": bool(sections or targets),
-        "raw": text,
-    }
-
-
-def parse_goal_text(value: Any) -> dict[str, Any]:
-    """Return the persisted round goal as one authoritative business statement."""
-
-    text = _text(value)
-    if not text:
-        return {
-            "headline": "本轮目标未记录",
-            "focusItems": [],
-            "raw": "",
-        }
-    return {
-        "headline": text,
-        "focusItems": [],
-        "raw": text,
-    }
-
-
-def plan_path_summary(path: Any, index: int) -> str:
-    if not isinstance(path, dict):
-        return _clean(path) or f"方案 {index}"
-    path_index = path.get("path_index") or index
-    target = _clean(path.get("target"))
-    action = _business_plan_term(path.get("action"))
-    method = _business_plan_term(path.get("method"))
-    detail = plan_path_detail(path)
-    return f"方案 {path_index}:{detail}" if detail else f"方案 {path_index}"
-
-
-def plan_path_detail(path: Any) -> str:
-    if not isinstance(path, dict):
-        return _clean(path)
-    target = _clean(path.get("target"))
-    action = _business_plan_term(path.get("action"))
-    method = _business_plan_term(path.get("method"))
-    return " · ".join(value for value in (action, target, method) if value)
-
-
-def plan_mode_label(mode: Any, path_count: int) -> str:
-    if path_count == 1:
-        return "单路"
-    value = _clean(mode)
-    aliases = {"race": "竞争", "赛马": "竞争", "divide": "分工"}
-    return aliases.get(value.lower(), value or "方式未记录")
-
-
-def preview(value: Any, limit: int = 140) -> str | None:
-    text = _clean(value)
-    if not text:
-        return None
-    if len(text) <= limit:
-        return text
-    cut = text[: limit + 1]
-    punctuation = max(cut.rfind(token) for token in "。!?;")
-    if punctuation >= max(24, int(limit * 0.55)):
-        return cut[: punctuation + 1]
-    return text[: limit - 1].rstrip() + "…"
-
-
-def _markdown_sections(text: str) -> dict[str, str]:
-    matches = list(_HEADING.finditer(text))
-    sections: dict[str, str] = {}
-    for index, match in enumerate(matches):
-        title = _clean(match.group(1)).replace("**", "")
-        end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
-        body = text[match.end() : end].strip()
-        if title and body:
-            sections[title] = body
-    return sections
-
-
-def _first_section(sections: dict[str, str], *labels: str) -> str | None:
-    for label in labels:
-        for title, body in sections.items():
-            if label in title:
-                return body
-    return None
-
-
-def _objective_constraint_groups(text: str) -> list[dict[str, Any]]:
-    headings = list(_HEADING.finditer(text))
-    groups: list[dict[str, Any]] = []
-    for index, heading in enumerate(headings):
-        title = _clean(heading.group(1)).replace("**", "")
-        if not re.match(r"^(?:目标\s*\d+|领域维度\s*\d+)", title):
-            continue
-        end = headings[index + 1].start() if index + 1 < len(headings) else len(text)
-        body = text[heading.end() : end]
-        fields = [
-            {"label": label.strip(), "value": _clean(value)}
-            for label, value in _FIELD_LINE.findall(body)
-            if label.strip() in {
-                "name",
-                "维度名",
-                "名称",
-                "评估内容",
-                "通过标准",
-                "强制性",
-                "前置条件",
-                "约束条件",
-            }
-            and _clean(value)
-        ]
-        dimension_name = next(
-            (
-                item["value"]
-                for item in fields
-                if item["label"].lower() in {"name", "维度名", "名称"}
-            ),
-            None,
-        )
-        business_items = [
-            item
-            for item in fields
-            if item["label"].lower() not in {"name", "维度名", "名称"}
-        ]
-        if not business_items and not dimension_name:
-            continue
-        groups.append(
-            {
-                "title": title,
-                "kind": "domain" if title.startswith("领域维度") else "target",
-                "dimensionName": dimension_name,
-                "items": business_items,
-            }
-        )
-    return groups
-
-
-def _section_preview(value: str | None) -> str | None:
-    lines = _meaningful_lines(value)
-    return lines[0] if lines else None
-
-
-def _meaningful_lines(value: Any) -> list[str]:
-    result: list[str] = []
-    for raw in _text(value).splitlines():
-        line = _clean(_BULLET.sub("", raw))
-        if not line or line.startswith("#"):
-            continue
-        if re.match(r"^(?:评估维度|评估内容|通过标准|强制性|前置条件|name)\s*[::]", line, re.I):
-            continue
-        result.append(line)
-    return _unique(result)
-
-
-def _first_business_sentence(text: str) -> str:
-    without_headings = _HEADING.sub("", text)
-    lines = _meaningful_lines(without_headings)
-    return lines[0] if lines else _clean(text)
-
-
-def _clean(value: Any) -> str:
-    text = str(value or "").replace("\\n", "\n")
-    text = re.sub(r"[`*_]", "", text)
-    text = re.sub(r"\s+", " ", text).strip(" #::")
-    return text
-
-
-def _text(value: Any) -> str:
-    return str(value or "").replace("\r\n", "\n").replace("\\n", "\n").strip()
-
-
-def _unique(values: list[str]) -> list[str]:
-    result: list[str] = []
-    for value in values:
-        if value and value not in result:
-            result.append(value)
-    return result
-
-
-def _unique_records(values: list[dict[str, str]]) -> list[dict[str, str]]:
-    result: list[dict[str, str]] = []
-    seen: set[tuple[str, str, str]] = set()
-    for value in values:
-        key = (
-            value.get("group") or "",
-            value.get("label") or "",
-            value.get("value") or "",
-        )
-        if key not in seen:
-            seen.add(key)
-            result.append(value)
-    return result
-
-
-def _business_plan_term(value: Any) -> str:
-    text = _clean(value)
-    return {
-        "增": "新增",
-        "改": "修改",
-        "删": "删除",
-        "组": "组合",
-        "产生脉络": "形成段落结构",
-        "产生元素": "补充脚本元素",
-    }.get(text, text)

+ 106 - 0
visualization/backend/app/models.py

@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+from typing import Any, Literal
+
+from pydantic import BaseModel, ConfigDict
+
+
+class StrictModel(BaseModel):
+    model_config = ConfigDict(extra="forbid")
+
+
+class Metric(StrictModel):
+    label: str
+    value: str
+
+
+class RecordEnvelope(StrictModel):
+    model_name: str
+    schema_version: str | None = None
+    payload: dict[str, Any]
+
+
+class FlowNode(StrictModel):
+    id: str
+    node_type: Literal[
+        "input",
+        "contract",
+        "operation",
+        "worker",
+        "attempt",
+        "artifact",
+        "validator",
+        "validation",
+        "decision",
+        "checkpoint",
+        "publication",
+        "transaction",
+        "readback",
+    ]
+    phase: Literal["phase-1", "phase-2", "phase-3", "delivery"]
+    lane: str
+    title: str
+    subtitle: str
+    status: str
+    agent_role: str | None = None
+    task_kind: str | None = None
+    sequence: int
+    x: int
+    y: int
+    width: int = 268
+    tags: list[str]
+    metrics: list[Metric]
+    record: RecordEnvelope
+
+
+class FlowEdge(StrictModel):
+    id: str
+    source: str
+    target: str
+    label: str | None = None
+    kind: Literal["control", "artifact", "validation", "publication"] = "control"
+    animated: bool = False
+
+
+class PhaseFrame(StrictModel):
+    id: str
+    title: str
+    subtitle: str
+    x: int
+    y: int
+    width: int
+    height: int
+    tone: Literal["coral", "blue", "purple", "green"]
+
+
+class RunSummary(StrictModel):
+    script_build_id: int
+    title: str
+    status: str
+    checkpoint: str
+    root_trace_id: str
+    publication_state: str
+    node_count: int
+    created_at: str
+    updated_at: str
+
+
+class RunGraph(StrictModel):
+    schema_version: Literal["script-build-visualization/v1"]
+    data_mode: Literal["fake"]
+    generated_at: str
+    run: RunSummary
+    frames: list[PhaseFrame]
+    nodes: list[FlowNode]
+    edges: list[FlowEdge]
+    schema_catalog: dict[str, list[str]]
+    enum_catalog: dict[str, list[str]]
+    declared_gaps: list[str]
+
+
+class NodeDetail(StrictModel):
+    data_mode: Literal["fake"]
+    run_id: int
+    node: FlowNode
+    incoming: list[FlowEdge]
+    outgoing: list[FlowEdge]

+ 0 - 5
visualization/backend/app/module_audit/__init__.py

@@ -1,5 +0,0 @@
-"""Read-only card and Inspector lineage audit."""
-
-from .router import router
-
-__all__ = ["router"]

+ 0 - 55
visualization/backend/app/module_audit/log_context.py

@@ -1,55 +0,0 @@
-from __future__ import annotations
-
-import re
-from typing import Any
-
-
-_ANCHOR_RE = re.compile(
-    r"^\[(?:MSG|TOOL)_ANCHOR:msg_id=([^:\]]+):seq=(\d+)(?::[^\]]+)?\]\s*$",
-    re.MULTILINE,
-)
-_BLOCK_START_RE = re.compile(r"^\[(?:AGENT_TASK|FOLD):[^\]]+\]\s*$", re.MULTILINE)
-
-
-def locate_log_module(log_content: str | None, event: dict[str, Any]) -> dict[str, Any] | None:
-    """Return only the anchored log module associated with an event."""
-
-    if not log_content:
-        return None
-    msg_id = str(event.get("msg_id") or "").strip()
-    if not msg_id:
-        return None
-    anchors = list(_ANCHOR_RE.finditer(log_content))
-    matched = next(
-        (
-            match
-            for match in anchors
-            if match.group(1) == msg_id
-        ),
-        None,
-    )
-    if matched is None:
-        return None
-
-    previous_starts = [
-        item
-        for item in _BLOCK_START_RE.finditer(log_content, max(0, matched.start() - 2_000), matched.start())
-    ]
-    start = previous_starts[-1].start() if previous_starts else matched.start()
-    next_anchor = next((item for item in anchors if item.start() > matched.start()), None)
-    end = next_anchor.start() if next_anchor else len(log_content)
-    content = log_content[start:end].strip()
-    return {
-        "anchor": matched.group(0),
-        "msgId": matched.group(1),
-        "sequence": int(matched.group(2)),
-        "content": content,
-        "characters": len(content),
-    }
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value) if value is not None else None
-    except (TypeError, ValueError):
-        return None

+ 0 - 136
visualization/backend/app/module_audit/repository.py

@@ -1,136 +0,0 @@
-from __future__ import annotations
-
-from datetime import date, datetime
-from typing import Any
-
-from sqlalchemy import case, func
-
-from ..runtime_bridge import load_runtime_modules, new_session
-
-
-def _value(value: Any) -> Any:
-    if isinstance(value, (datetime, date)):
-        return value.isoformat()
-    if isinstance(value, dict):
-        return {str(key): _value(item) for key, item in value.items()}
-    if isinstance(value, (list, tuple)):
-        return [_value(item) for item in value]
-    return value
-
-
-def _row(row: Any) -> dict[str, Any]:
-    if row is None:
-        return {}
-    mapping = getattr(row, "_mapping", None)
-    if mapping is not None:
-        return {str(key): _value(value) for key, value in mapping.items()}
-    return {
-        column.name: _value(getattr(row, column.name))
-        for column in row.__table__.columns
-    }
-
-
-class AuditRepository:
-    """Loads only audit-only full records; every query is read-only."""
-
-    def load_event(self, script_build_id: int, event_id: int) -> dict[str, Any] | None:
-        _, models = load_runtime_modules()
-        session = new_session()
-        try:
-            event = (
-                session.query(models.ScriptBuildEvent)
-                .filter(
-                    models.ScriptBuildEvent.script_build_id == script_build_id,
-                    models.ScriptBuildEvent.id == event_id,
-                )
-                .first()
-            )
-            if event is None:
-                return None
-            body = (
-                session.query(models.ScriptBuildEventBody)
-                .filter(
-                    models.ScriptBuildEventBody.script_build_id == script_build_id,
-                    models.ScriptBuildEventBody.event_id == event_id,
-                )
-                .first()
-            )
-            payload = _row(event)
-            if body is not None:
-                body_row = _row(body)
-                payload["eventBody"] = {
-                    "id": body_row.get("id"),
-                    "input_content_type": body_row.get("input_content_type"),
-                    "input_content": body_row.get("input_content"),
-                    "output_content_type": body_row.get("output_content_type"),
-                    "output_content": body_row.get("output_content"),
-                    "created_at": body_row.get("created_at"),
-                    "updated_at": body_row.get("updated_at"),
-                }
-            return payload
-        finally:
-            session.close()
-
-    def load_log(self, script_build_id: int) -> str | None:
-        _, models = load_runtime_modules()
-        session = new_session()
-        try:
-            row = (
-                session.query(models.ScriptBuildLog)
-                .filter(models.ScriptBuildLog.script_build_id == script_build_id)
-                .first()
-            )
-            return str(row.log_content) if row and row.log_content is not None else None
-        finally:
-            session.close()
-
-    def load_event_log_windows(
-        self,
-        script_build_id: int,
-        event_anchors: list[tuple[int, str, int | None]],
-        *,
-        window_size: int = 50_000,
-    ) -> dict[int, str]:
-        """Read only anchored log windows in one query, never the whole Run log."""
-        # `script_build_event.event_seq` is the structured event stream order,
-        # while the seq stored in script_build_log anchors is the model-message
-        # stream order.  They are not interchangeable.  Only an exact msg_id is
-        # safe enough for the source Inspector.
-        unique = [
-            item
-            for item in dict.fromkeys(event_anchors)
-            if str(item[1] or "").strip()
-        ]
-        if not unique:
-            return {}
-        _, models = load_runtime_modules()
-        session = new_session()
-        try:
-            content = models.ScriptBuildLog.log_content
-            expressions = []
-            for _event_id, msg_id, _event_seq in unique:
-                position = func.greatest(
-                    func.locate(f"[MSG_ANCHOR:msg_id={msg_id}", content),
-                    func.locate(f"[TOOL_ANCHOR:msg_id={msg_id}", content),
-                )
-                start = func.greatest(position - 2_000, 1)
-                expressions.append(
-                    case(
-                        (position > 0, func.substr(content, start, window_size)),
-                        else_=None,
-                    )
-                )
-            row = (
-                session.query(*expressions)
-                .filter(models.ScriptBuildLog.script_build_id == script_build_id)
-                .first()
-            )
-            if row is None:
-                return {}
-            return {
-                event_id: str(row[index])
-                for index, (event_id, _msg_id, _event_seq) in enumerate(unique)
-                if row[index] is not None
-            }
-        finally:
-            session.close()

+ 0 - 121
visualization/backend/app/module_audit/router.py

@@ -1,121 +0,0 @@
-from __future__ import annotations
-
-import json
-from concurrent.futures import ThreadPoolExecutor, as_completed
-from pathlib import Path
-
-from fastapi import APIRouter, HTTPException
-from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
-
-from ..repositories import BuildNotFound
-from ..sanitizer import sanitize
-from .service import ModuleAuditService, ModuleNotFound
-
-
-router = APIRouter(tags=["module-audit"])
-service = ModuleAuditService()
-_STATIC = Path(__file__).resolve().parent / "static"
-_ASSETS = {
-    "app.js": ("application/javascript; charset=utf-8", "app.js"),
-    "styles.css": ("text/css; charset=utf-8", "styles.css"),
-}
-
-
-@router.get("/audit/{script_build_id}", response_class=HTMLResponse)
-def audit_page(script_build_id: int):
-    template = (_STATIC / "index.html").read_text(encoding="utf-8")
-    return HTMLResponse(template.replace("__RUN_ID__", str(script_build_id)))
-
-
-@router.get("/audit-assets/{asset_name}")
-def audit_asset(asset_name: str):
-    asset = _ASSETS.get(asset_name)
-    if asset is None:
-        raise HTTPException(status_code=404, detail="未找到审计页面资源")
-    media_type, filename = asset
-    return FileResponse(_STATIC / filename, media_type=media_type)
-
-
-@router.get("/api/script-builds/{script_build_id}/module-audit")
-def module_audit_index(script_build_id: int):
-    try:
-        return service.index(script_build_id)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取模块数据对照失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-@router.get("/api/script-builds/{script_build_id}/module-audit-stream")
-def module_audit_stream(script_build_id: int):
-    """Stream every module automatically; this is full loading, not click-to-load."""
-    try:
-        index = service.index(script_build_id)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取模块数据对照失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-    def rows():
-        yield _ndjson({"type": "index", "data": index})
-        modules = index.get("modules") or []
-        with ThreadPoolExecutor(max_workers=4, thread_name_prefix="module-audit") as pool:
-            futures = {
-                pool.submit(service.detail, script_build_id, str(module["id"])): str(module["id"])
-                for module in modules
-            }
-            for future in as_completed(futures):
-                module_id = futures[future]
-                try:
-                    yield _ndjson(
-                        {
-                            "type": "detail",
-                            "moduleId": module_id,
-                            "data": future.result(),
-                        }
-                    )
-                except Exception as exc:  # One bad module must not block the full audit.
-                    yield _ndjson(
-                        {
-                            "type": "error",
-                            "moduleId": module_id,
-                            "message": f"{type(exc).__name__}: {sanitize(str(exc))}",
-                        }
-                    )
-        yield _ndjson({"type": "complete", "moduleCount": len(modules)})
-
-    return StreamingResponse(
-        rows(),
-        media_type="application/x-ndjson; charset=utf-8",
-        headers={
-            "Cache-Control": "no-store",
-            "X-Content-Type-Options": "nosniff",
-        },
-    )
-
-
-@router.get("/api/script-builds/{script_build_id}/module-audit/{module_id:path}")
-def module_audit_detail(script_build_id: int, module_id: str):
-    try:
-        return service.detail(script_build_id, module_id)
-    except BuildNotFound as exc:
-        raise HTTPException(status_code=404, detail=str(exc)) from exc
-    except ModuleNotFound as exc:
-        raise HTTPException(status_code=404, detail=f"未找到模块 {module_id}") from exc
-    except Exception as exc:
-        raise HTTPException(
-            status_code=503,
-            detail=f"读取模块完整数据失败:{type(exc).__name__}: {sanitize(str(exc))}",
-        ) from exc
-
-
-def _ndjson(value: dict) -> bytes:
-    return (json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n").encode(
-        "utf-8"
-    )

+ 0 - 1165
visualization/backend/app/module_audit/service.py

@@ -1,1165 +0,0 @@
-from __future__ import annotations
-
-import json
-from hashlib import sha1
-from dataclasses import dataclass
-from threading import RLock
-from time import monotonic
-from typing import Any, Iterable
-
-from ..execution_builder import ExecutionViewBuilder
-from ..inspector_projection import (
-    project_activity_detail,
-    project_event_detail,
-    project_round_detail,
-)
-from ..providers import LocalDatabaseProvider
-from ..sanitizer import sanitize
-from .log_context import locate_log_module
-from .repository import AuditRepository
-
-
-class ModuleNotFound(LookupError):
-    pass
-
-
-@dataclass(frozen=True)
-class _RunSnapshot:
-    bundle: dict[str, Any]
-    view: dict[str, Any]
-    modules: list[dict[str, Any]]
-    expires_at: float
-
-
-class ModuleAuditService:
-    def __init__(
-        self,
-        provider: LocalDatabaseProvider | None = None,
-        builder: ExecutionViewBuilder | None = None,
-        repository: AuditRepository | None = None,
-    ):
-        self.provider = provider or LocalDatabaseProvider()
-        self.builder = builder or ExecutionViewBuilder()
-        self.repository = repository or AuditRepository()
-        self._snapshot_cache: dict[int, _RunSnapshot] = {}
-        self._detail_cache: dict[tuple[int, str], tuple[float, dict[str, Any]]] = {}
-        self._event_cache: dict[tuple[int, int], tuple[float, dict[str, Any] | None]] = {}
-        self._log_cache: dict[int, tuple[float, str | None]] = {}
-        self._snapshot_lock = RLock()
-
-    def _snapshot(self, script_build_id: int) -> _RunSnapshot:
-        now = monotonic()
-        with self._snapshot_lock:
-            cached = self._snapshot_cache.get(script_build_id)
-            if cached is not None and cached.expires_at > now:
-                return cached
-
-        bundle = self.provider.load_bundle(script_build_id)
-        view = self.builder.from_bundle(bundle)
-        modules = _collect_modules(view)
-        status = str((view.get("header") or {}).get("status") or "").lower()
-        ttl = 300.0 if status in {"completed", "success", "failed", "interrupted", "cancelled"} else 3.0
-        snapshot = _RunSnapshot(
-            bundle=bundle,
-            view=view,
-            modules=modules,
-            expires_at=monotonic() + ttl,
-        )
-        with self._snapshot_lock:
-            self._snapshot_cache[script_build_id] = snapshot
-            if len(self._snapshot_cache) > 12:
-                stale_ids = sorted(
-                    self._snapshot_cache,
-                    key=lambda item: self._snapshot_cache[item].expires_at,
-                )[:-12]
-                for stale_id in stale_ids:
-                    self._snapshot_cache.pop(stale_id, None)
-        return snapshot
-
-    def index(self, script_build_id: int) -> dict[str, Any]:
-        snapshot = self._snapshot(script_build_id)
-        view = snapshot.view
-        modules = snapshot.modules
-        return sanitize(
-            {
-                "schemaVersion": "1",
-                "run": {
-                    "id": script_build_id,
-                    "status": (view.get("header") or {}).get("status"),
-                    "capturedAt": view.get("capturedAt"),
-                    "dataShape": view.get("dataShape"),
-                },
-                "columns": ["模块实际使用", "模块实际读取", "原始记录与上下文"],
-                "modules": [
-                    {
-                        "id": item["id"],
-                        "title": item["title"],
-                        "group": item["group"],
-                        "path": item["path"],
-                        "kind": item["kind"],
-                        "role": item.get("role"),
-                        "roundIndex": item.get("roundIndex"),
-                        "branchId": item.get("branchId"),
-                        "detailRef": item.get("detailRef"),
-                        "displayVariant": item.get("displayVariant", "expanded"),
-                        "cardCharacters": _characters(_card_use(item)),
-                        "completeness": "available",
-                    }
-                    for item in modules
-                ],
-                "summary": {
-                    "moduleCount": len(modules),
-                    "groupCount": len({item["group"] for item in modules}),
-                    "cardCharacters": sum(_characters(_card_use(item)) for item in modules),
-                },
-            },
-            max_text=None,
-        )
-
-    def detail(self, script_build_id: int, module_id: str) -> dict[str, Any]:
-        snapshot = self._snapshot(script_build_id)
-        cache_key = (script_build_id, module_id)
-        now = monotonic()
-        with self._snapshot_lock:
-            cached_detail = self._detail_cache.get(cache_key)
-            if cached_detail is not None and cached_detail[0] > now:
-                return cached_detail[1]
-        bundle = snapshot.bundle
-        view = snapshot.view
-        module = next((item for item in snapshot.modules if item["id"] == module_id), None)
-        if module is None:
-            raise ModuleNotFound(module_id)
-
-        inspector, inspector_notice = self._inspector(script_build_id, module, view, bundle)
-        card = _card_use(module)
-        reads = self._reads(script_build_id, module, inspector, bundle)
-        reads = _annotate_usage_areas(reads, inspector)
-        use_by_consumer = {
-            "card": _strings(card),
-            "inspector": _strings(inspector),
-        }
-        use_snippets = sorted(
-            set(use_by_consumer["card"] + use_by_consumer["inspector"]),
-            key=len,
-            reverse=True,
-        )
-        contexts = self._contexts(
-            script_build_id,
-            reads,
-            use_snippets,
-            expires_at=snapshot.expires_at,
-        )
-        reads, contexts = _annotate_lineage(module["id"], reads, contexts)
-        completeness = _completeness(inspector_notice, reads, contexts)
-        payload = sanitize(
-            {
-                "schemaVersion": "1",
-                "id": module["id"],
-                "title": module["title"],
-                "group": module["group"],
-                "path": module["path"],
-                "actualUse": {
-                    "card": card,
-                    "inspector": inspector,
-                    "inspectorNotice": inspector_notice,
-                    "characters": {
-                        "card": _characters(card),
-                        "inspectorBusiness": _characters((inspector or {}).get("businessSections")),
-                        "inspectorDecision": _characters((inspector or {}).get("blocks")),
-                        "inspectorChanges": _characters((inspector or {}).get("changes")),
-                        "inspectorTechnical": _characters((inspector or {}).get("technical")),
-                    },
-                },
-                "actualReads": [
-                    _public_read(item, use_by_consumer) for item in reads
-                ],
-                "contexts": contexts,
-                "completeness": completeness,
-            },
-            max_text=None,
-        )
-        with self._snapshot_lock:
-            self._detail_cache[cache_key] = (snapshot.expires_at, payload)
-            if len(self._detail_cache) > 512:
-                stale_keys = sorted(
-                    self._detail_cache,
-                    key=lambda item: self._detail_cache[item][0],
-                )[:-512]
-                for stale_key in stale_keys:
-                    self._detail_cache.pop(stale_key, None)
-        return payload
-
-    def _inspector(
-        self,
-        script_build_id: int,
-        module: dict[str, Any],
-        view: dict[str, Any],
-        bundle: dict[str, Any],
-    ) -> tuple[dict[str, Any] | None, str | None]:
-        detail_ref = str(module.get("detailRef") or "")
-        if not detail_ref:
-            return None, "该模块没有 Inspector 入口。"
-        if module["kind"] == "final-result" or detail_ref.startswith("artifact:"):
-            return None, "该按钮进入脚本表查看器,不属于 Inspector。"
-        try:
-            if module["kind"] == "round-summary":
-                return project_round_detail(
-                    script_build_id,
-                    module["payload"],
-                    bundle,
-                ), None
-            if detail_ref.startswith("event:"):
-                event_id = int(detail_ref.rsplit(":", 1)[1])
-                event_detail = self.provider.load_event_detail(script_build_id, event_id)
-                try:
-                    return project_activity_detail(
-                        script_build_id,
-                        detail_ref,
-                        view,
-                        bundle,
-                        event_detail=event_detail,
-                    ), None
-                except KeyError:
-                    return project_event_detail(
-                        script_build_id,
-                        event_detail,
-                        run_status=(view.get("header") or {}).get("status"),
-                    ), None
-            return project_activity_detail(
-                script_build_id,
-                detail_ref,
-                view,
-                bundle,
-            ), None
-        except (KeyError, ValueError, LookupError) as exc:
-            return None, f"Inspector 记录不可用:{type(exc).__name__}"
-
-    def _reads(
-        self,
-        script_build_id: int,
-        module: dict[str, Any],
-        inspector: dict[str, Any] | None,
-        bundle: dict[str, Any],
-    ) -> list[dict[str, Any]]:
-        reads = _explicit_reads(module, bundle)
-        technical = (inspector or {}).get("technical")
-        if isinstance(technical, dict):
-            for key, value in technical.items():
-                if key in {"scriptBuildId", "table"} or value in (None, "", [], {}):
-                    continue
-                source = _source_from_technical(key, value)
-                reads.append(
-                    {
-                        "consumer": "inspector",
-                        "label": _technical_label(key),
-                        "disposition": "selected",
-                        "relation": "exact-record" if isinstance(value, (dict, list)) else "projected",
-                        "source": source,
-                        "fieldPath": f"technical.{key}",
-                        "value": value,
-                        "characters": _characters(value),
-                        "context": value,
-                    }
-                )
-
-        event_ids = _event_ids(module, reads)
-        known = {
-            int(item["source"]["eventId"])
-            for item in reads
-            if isinstance(item.get("source"), dict)
-            and item["source"].get("eventId") is not None
-        }
-        for event_id in event_ids:
-            if event_id in known:
-                continue
-            event = self.repository.load_event(script_build_id, event_id)
-            if event:
-                reads.append(_event_read(event, "module", "关联运行事件", "calculation-input"))
-        return _dedupe_reads(reads)
-
-    def _contexts(
-        self,
-        script_build_id: int,
-        reads: list[dict[str, Any]],
-        use_snippets: list[str],
-        *,
-        expires_at: float,
-    ) -> list[dict[str, Any]]:
-        contexts: list[dict[str, Any]] = []
-        event_ids: set[int] = set()
-        for read in reads:
-            source = read.get("source") or {}
-            event_id = _integer(source.get("eventId"))
-            if event_id is not None:
-                event_ids.add(event_id)
-                if source.get("sourceKind") == "database":
-                    continue
-            content = read.get("context")
-            if content in (None, "", [], {}):
-                continue
-            function_result = source.get("sourceKind") == "function"
-            contexts.append(
-                {
-                    "id": _context_id(source, read.get("fieldPath")),
-                    "kind": "function-result" if function_result else "database-record",
-                    "title": (
-                        f"函数处理结果 · {read.get('label') or '数据'}"
-                        if function_result
-                        else read.get("label") or "原始数据库记录"
-                    ),
-                    "source": source,
-                    "sourceKey": _source_key(source),
-                    "relatedSourceKey": (
-                        f"database:event:{source.get('eventId')}"
-                        if function_result and source.get("eventId") is not None
-                        else None
-                    ),
-                    "content": content,
-                    "characters": _characters(content),
-                    "readPaths": ["$"] if function_result else [read.get("fieldPath")],
-                    "usedFragments": _matching_snippets(content, use_snippets),
-                    "match": read.get("relation"),
-                    "completeness": "complete",
-                }
-            )
-
-        log_content = self._load_log(script_build_id, expires_at) if event_ids else None
-        for event_id in sorted(event_ids):
-            event = self._load_event(script_build_id, event_id, expires_at)
-            if not event:
-                contexts.append(
-                    {
-                        "id": f"event:{event_id}:missing",
-                        "kind": "event",
-                        "title": f"Event {event_id}",
-                        "source": {
-                            "sourceKind": "database",
-                            "table": "script_build_event + script_build_event_body",
-                            "eventId": event_id,
-                        },
-                        "sourceKey": f"database:event:{event_id}",
-                        "content": "事件记录不存在。",
-                        "characters": 8,
-                        "readPaths": [],
-                        "usedFragments": [],
-                        "match": "missing",
-                        "completeness": "missing",
-                    }
-                )
-                continue
-            contexts.append(
-                {
-                    "id": f"event:{event_id}",
-                    "kind": "event-record",
-                    "title": f"script_build_event #{event_id}",
-                    "source": {
-                        "sourceKind": "database",
-                        "table": "script_build_event + script_build_event_body",
-                        "eventId": event_id,
-                    },
-                    "sourceKey": f"database:event:{event_id}",
-                    "content": event,
-                    "characters": _characters(event),
-                    "readPaths": ["eventBody.input_content", "eventBody.output_content", "inputData", "outputPreview"],
-                    "usedFragments": _matching_snippets(event, use_snippets),
-                    "match": "exact-record",
-                    "completeness": "complete" if event.get("eventBody") else "body-missing",
-                }
-            )
-            log_module = locate_log_module(log_content, event)
-            if log_module:
-                contexts.append(
-                    {
-                        "id": f"event:{event_id}:log",
-                        "kind": "log-module",
-                        "title": f"锚定日志模块 · Event {event_id}",
-                        "source": {
-                            "sourceKind": "log",
-                            "table": "script_build_log",
-                            "eventId": event_id,
-                            "anchor": log_module["anchor"],
-                        },
-                        "sourceKey": f"log:event:{event_id}",
-                        "relatedSourceKey": f"database:event:{event_id}",
-                        "content": log_module["content"],
-                        "characters": log_module["characters"],
-                        "readPaths": [],
-                        "usedFragments": _matching_snippets(log_module["content"], use_snippets),
-                        "match": "anchored",
-                        "completeness": "complete",
-                    }
-                )
-        return _dedupe_contexts(contexts)
-
-    def _load_event(
-        self,
-        script_build_id: int,
-        event_id: int,
-        expires_at: float,
-    ) -> dict[str, Any] | None:
-        key = (script_build_id, event_id)
-        now = monotonic()
-        with self._snapshot_lock:
-            cached = self._event_cache.get(key)
-            if cached is not None and cached[0] > now:
-                return cached[1]
-            event = self.repository.load_event(script_build_id, event_id)
-            self._event_cache[key] = (expires_at, event)
-            if len(self._event_cache) > 512:
-                stale_keys = sorted(
-                    self._event_cache,
-                    key=lambda item: self._event_cache[item][0],
-                )[:-512]
-                for stale_key in stale_keys:
-                    self._event_cache.pop(stale_key, None)
-            return event
-
-    def _load_log(self, script_build_id: int, expires_at: float) -> str | None:
-        now = monotonic()
-        with self._snapshot_lock:
-            cached = self._log_cache.get(script_build_id)
-            if cached is not None and cached[0] > now:
-                return cached[1]
-            log_content = self.repository.load_log(script_build_id)
-            self._log_cache[script_build_id] = (expires_at, log_content)
-            if len(self._log_cache) > 12:
-                stale_ids = sorted(
-                    self._log_cache,
-                    key=lambda item: self._log_cache[item][0],
-                )[:-12]
-                for stale_id in stale_ids:
-                    self._log_cache.pop(stale_id, None)
-            return log_content
-
-
-def _collect_modules(view: dict[str, Any]) -> list[dict[str, Any]]:
-    modules: list[dict[str, Any]] = []
-
-    def add(
-        payload: Any,
-        *,
-        group: str,
-        path: list[str],
-        kind: str,
-        round_index: int | None = None,
-        branch_id: int | None = None,
-        display_variant: str = "expanded",
-        forced_id: str | None = None,
-        fallback_id: str | None = None,
-        fallback_title: str | None = None,
-        fallback_detail_ref: str | None = None,
-    ) -> None:
-        if not isinstance(payload, dict):
-            return
-        module_id = str(forced_id or payload.get("id") or fallback_id or "").strip()
-        if not module_id:
-            return
-        modules.append(
-            {
-                "id": module_id,
-                "title": str(payload.get("title") or fallback_title or module_id),
-                "group": group,
-                "path": path,
-                "kind": kind,
-                "role": payload.get("role"),
-                "roundIndex": round_index,
-                "branchId": branch_id,
-                "detailRef": payload.get("detailRef") or fallback_detail_ref,
-                "displayVariant": display_variant,
-                "payload": payload,
-            }
-        )
-
-    add(view.get("planning"), group="构建前", path=["构建前"], kind="story")
-    add(view.get("objective"), group="构建前", path=["构建前"], kind="story")
-    for round_ in view.get("rounds") or []:
-        round_index = _integer(round_.get("roundIndex")) or 0
-        group = f"第 {round_index} 轮"
-        add(round_.get("goal"), group=group, path=[group, "主 Agent"], kind="story", round_index=round_index)
-        add((round_.get("plan") or {}).get("node"), group=group, path=[group, "主 Agent"], kind="story", round_index=round_index)
-        for branch in round_.get("branches") or []:
-            branch_id = _integer(branch.get("branchId")) or 0
-            branch_label = f"方案 {branch_id}"
-            add(branch.get("task"), group=group, path=[group, branch_label], kind="story", round_index=round_index, branch_id=branch_id)
-            stage = branch.get("retrievalStage") or {}
-            add(stage, group=group, path=[group, branch_label, "取数"], kind="retrieval-stage", round_index=round_index, branch_id=branch_id, fallback_title="取数阶段")
-            for direct in stage.get("directToolGroups") or []:
-                add(direct, group=group, path=[group, branch_label, "取数", "工具取数"], kind="retrieval-direct", round_index=round_index, branch_id=branch_id)
-                for call in direct.get("calls") or []:
-                    add(call, group=group, path=[group, branch_label, "取数", "工具调用"], kind="retrieval-call", round_index=round_index, branch_id=branch_id, fallback_title=call.get("businessLabel"))
-            for agent in stage.get("agentRuns") or []:
-                add(agent, group=group, path=[group, branch_label, "取数", "Agent 取数"], kind="retrieval-agent", round_index=round_index, branch_id=branch_id, fallback_title=agent.get("businessLabel"))
-                for attempt in agent.get("attempts") or []:
-                    add(attempt, group=group, path=[group, branch_label, "取数", "单次查询"], kind="query-attempt", round_index=round_index, branch_id=branch_id, fallback_title=attempt.get("queryLabel"))
-            for decision in branch.get("dataDecisions") or []:
-                add(decision.get("node") or decision, group=group, path=[group, branch_label], kind="story", round_index=round_index, branch_id=branch_id)
-            add((branch.get("output") or {}).get("node"), group=group, path=[group, branch_label], kind="story", round_index=round_index, branch_id=branch_id)
-        for batch in round_.get("convergenceBatches") or []:
-            add(batch.get("review") or batch.get("missingReview"), group=group, path=[group, "收敛"], kind="review", round_index=round_index)
-            add((batch.get("decision") or {}).get("node") or batch.get("decision") or batch.get("missingDecision"), group=group, path=[group, "收敛"], kind="decision", round_index=round_index)
-        add(round_.get("overallEvaluation"), group=group, path=[group, "整体评审"], kind="story", round_index=round_index)
-        add(round_.get("result"), group=group, path=[group, "本轮产出"], kind="story", round_index=round_index)
-    add(view.get("finalResult"), group="最终结果", path=["最终结果"], kind="final-result")
-    return _unique_modules(modules)
-
-
-def _card_use(module: dict[str, Any]) -> dict[str, Any]:
-    payload = module["payload"]
-    card = payload.get("card")
-    if not isinstance(card, dict):
-        card = ((payload.get("decisionProjection") or {}).get("card") if isinstance(payload.get("decisionProjection"), dict) else None)
-    if isinstance(card, dict):
-        fields = []
-        primary = card.get("primary")
-        if isinstance(primary, dict):
-            fields.append({"label": primary.get("label"), "value": primary.get("value"), "kind": "primary"})
-        for item in card.get("secondary") or []:
-            if isinstance(item, dict):
-                fields.append({"label": item.get("label"), "value": item.get("value"), "kind": "secondary"})
-        return {"title": module["title"], "fields": fields}
-
-    kind = module["kind"]
-    if kind == "round-summary":
-        summary = payload.get("summary") or {}
-        return {
-            "title": f"第 {module.get('roundIndex')} 轮摘要",
-            "fields": [
-                {"label": "本轮目标", "value": (summary.get("goal") or {}).get("headline")},
-                {"label": "尝试方案", "value": summary.get("approach")},
-                {"label": "主 Agent 决策", "value": summary.get("decision")},
-                {"label": "本轮产出", "value": summary.get("output")},
-            ],
-        }
-    if kind == "branch-summary":
-        summary = payload.get("summary") or {}
-        return {"title": module["title"], "fields": [{"label": "任务", "value": summary.get("task")}, {"label": "产出", "value": summary.get("output")}, {"label": "状态", "value": payload.get("status")}]}
-    if kind == "retrieval-stage":
-        return {"title": "取数阶段", "fields": [{"label": "模式", "value": payload.get("observedMode")}, {"label": "状态", "value": payload.get("status")}, {"label": "取数模块", "value": f"{len(payload.get('directToolGroups') or [])} 个工具组、{len(payload.get('agentRuns') or [])} 个 Agent"}]}
-    if kind == "retrieval-direct":
-        sources = "、".join(str(item.get("businessLabel") or "") for item in payload.get("sources") or [] if isinstance(item, dict))
-        result = f"{payload.get('failureCount')} 次失败" if payload.get("failureCount") else f"{payload.get('interruptedCount')} 次中断" if payload.get("interruptedCount") else "进行中" if payload.get("runningCount") else "已完成"
-        return {"title": sources or "工具取数", "fields": [{"label": "读取", "value": f"{payload.get('callCount') or 0} 次"}, {"label": "结果", "value": result}]}
-    if kind == "retrieval-agent":
-        query = payload.get("querySummary") or {}
-        return {"title": payload.get("businessLabel") or "Agent 取数", "fields": [{"label": "取数目标", "value": payload.get("taskPreview")}, {"label": "查询过程", "value": query}, {"label": "初筛整理", "value": (payload.get("screening") or {}).get("preview")}]}
-    if kind in {"query-attempt", "retrieval-call"}:
-        return {"title": module["title"], "fields": [{"label": "状态", "value": payload.get("status")}, {"label": "结果数量", "value": payload.get("resultCount")}, {"label": "结果摘要", "value": payload.get("resultSummary")}]}
-    if kind == "final-result":
-        return {"title": module["title"], "fields": [{"label": "执行结果", "value": payload.get("summary")}, {"label": "状态", "value": payload.get("status")}, {"label": "轮次", "value": payload.get("roundCount")}, {"label": "方案", "value": payload.get("branchCounts")}]}
-    return {"title": module["title"], "fields": [{"label": "模块数据", "value": payload}]}
-
-
-def _explicit_reads(module: dict[str, Any], bundle: dict[str, Any]) -> list[dict[str, Any]]:
-    reads: list[dict[str, Any]] = []
-    role = str(module.get("role") or "")
-    round_index = module.get("roundIndex")
-    branch_id = module.get("branchId")
-    raw_round = _find(bundle.get("rounds"), "round_index", round_index)
-    raw_branch = _find(bundle.get("branches"), "branch_id", branch_id)
-
-    if role == "planning-analysis":
-        _append_related_events(reads, module, bundle, "card", "规划事件")
-    elif role == "objective":
-        record = bundle.get("record") or {}
-        reads.append(_record_read("card", "创作目标", "script_build_record", record, "script_direction"))
-    elif role == "goal" and raw_round:
-        reads.append(_record_read("card", "本轮目标", "script_build_round", raw_round, "goal"))
-    elif role == "plan" and raw_round:
-        for field, label in (("multipath_plan", "实现路线"), ("race_or_divide", "组织方式"), ("plan_note", "规划理由")):
-            if raw_round.get(field) not in (None, "", [], {}):
-                reads.append(_record_read("card", label, "script_build_round", raw_round, field))
-    elif role == "branch-task" and raw_branch:
-        reads.append(_record_read("card", "实现任务", "script_build_branch", raw_branch, "impl_task"))
-        task_event = next(
-            (
-                event
-                for event in reversed(bundle.get("events") or [])
-                if event.get("event_name") == "script_implementer"
-                and _integer(event.get("round_index")) == round_index
-                and _integer(event.get("branch_id")) == branch_id
-            ),
-            None,
-        )
-        if task_event:
-            event_value = (task_event.get("inputData") or {}).get("task") if isinstance(task_event.get("inputData"), dict) else None
-            reads.append(_event_summary_read(task_event, "inspector", "实现 Agent 派发任务", "inputData.task", event_value, "selected" if event_value else "missing"))
-            reads.append({**_record_read("inspector", "历史回退任务", "script_build_branch", raw_branch, "impl_task"), "disposition": "fallback" if event_value else "selected"})
-        else:
-            reads.append({**_record_read("inspector", "实现任务", "script_build_branch", raw_branch, "impl_task"), "disposition": "selected"})
-    elif role == "data-decision":
-        row = _find(bundle.get("dataDecisions"), "id", _suffix_int(module["id"]))
-        if row:
-            reads.append(_whole_record_read("card", "取数取舍记录", "script_build_data_decision", row))
-    elif role in {"domain-output", "candidate-output"} and raw_branch:
-        reads.append(_whole_record_read("card", "实现方案记录", "script_build_branch", raw_branch))
-        if role == "domain-output":
-            for row in bundle.get("domainInfo") or []:
-                if _integer(row.get("branch_id")) == branch_id:
-                    reads.append(_whole_record_read("card", "领域事实", "script_build_domain_info", row))
-    elif role == "multipath-decision":
-        row = _find(bundle.get("multipathDecisions"), "id", _suffix_int(module["id"]))
-        if row:
-            reads.append(_whole_record_read("card", "多方案决定", "script_build_multipath_decision", row))
-    elif role in {"round-evaluation", "round-result"} and raw_round:
-        reads.append(_whole_record_read("card", "轮次记录", "script_build_round", raw_round))
-        _append_related_events(reads, module, bundle, "card", "轮次运行事件")
-
-    kind = module["kind"]
-    payload = module["payload"]
-    if kind == "round-summary" and raw_round:
-        reads.append(_whole_record_read("card", "折叠态轮次记录", "script_build_round", raw_round))
-    elif kind == "branch-summary" and raw_branch:
-        reads.append(_whole_record_read("card", "折叠态方案记录", "script_build_branch", raw_branch))
-    elif kind.startswith("retrieval") or kind == "query-attempt":
-        event_ids = _event_ids_from_payload(payload)
-        for event_id in event_ids:
-            event = next((item for item in bundle.get("events") or [] if _integer(item.get("id")) == event_id), None)
-            if event:
-                reads.append(_event_summary_read(event, "card", "取数运行事件", "event", event, "calculation-input"))
-    elif kind == "review":
-        _append_related_events(reads, module, bundle, "card", "评审事件")
-    elif kind == "final-result":
-        reads.append(_whole_record_read("card", "构建记录", "script_build_record", bundle.get("record") or {}))
-        reads.append(_whole_record_read("card", "当前主脚本快照", "script_build_artifact", bundle.get("currentArtifact") or {}))
-
-    if not reads:
-        reads.append(
-            {
-                "consumer": "module",
-                "label": "来源未确认",
-                "disposition": "unconfirmed",
-                "relation": "unconfirmed",
-                "source": {"type": "unknown", "sourceKind": "unknown"},
-                "fieldPath": None,
-                "value": "当前审计规则无法可靠确认这个模块的字段来源。",
-                "characters": 24,
-                "context": None,
-            }
-        )
-    return reads
-
-
-def _record_read(consumer: str, label: str, table: str, row: dict[str, Any], field: str) -> dict[str, Any]:
-    value = row.get(field)
-    return {
-        "consumer": consumer,
-        "label": label,
-        "disposition": "selected",
-        "relation": (
-            "projected"
-            if consumer == "card"
-            else "exact" if isinstance(value, str) else "structured"
-        ),
-        "source": {
-            "type": "business-record",
-            "sourceKind": "database",
-            "table": table,
-            "recordId": row.get("id"),
-        },
-        "fieldPath": field,
-        "value": value,
-        "characters": _characters(value),
-        "context": row,
-    }
-
-
-def _whole_record_read(consumer: str, label: str, table: str, row: dict[str, Any]) -> dict[str, Any]:
-    return {
-        "consumer": consumer,
-        "label": label,
-        "disposition": "selected",
-        "relation": (
-            "combined"
-            if table == "script_build_artifact"
-            else "projected" if consumer == "card" else "exact-record"
-        ),
-        "source": {
-            "type": "business-record",
-            "sourceKind": "database",
-            "table": table,
-            "recordId": row.get("id"),
-        },
-        "fieldPath": "$",
-        "value": row,
-        "characters": _characters(row),
-        "context": row,
-    }
-
-
-def _event_summary_read(event: dict[str, Any], consumer: str, label: str, field_path: str, value: Any, disposition: str) -> dict[str, Any]:
-    return {
-        "consumer": consumer,
-        "label": label,
-        "disposition": disposition,
-        "relation": (
-            "calculation-input"
-            if disposition == "calculation-input"
-            else "exact" if isinstance(value, str) else "structured"
-        ),
-        "source": {
-            "type": "runtime-event",
-            "sourceKind": "database",
-            "table": "script_build_event + script_build_event_body",
-            "eventId": event.get("id"),
-            "eventName": event.get("event_name"),
-        },
-        "fieldPath": field_path,
-        "value": value,
-        "characters": _characters(value),
-        "context": None,
-    }
-
-
-def _event_read(event: dict[str, Any], consumer: str, label: str, disposition: str) -> dict[str, Any]:
-    return {
-        "consumer": consumer,
-        "label": label,
-        "disposition": disposition,
-        "relation": "exact-record",
-        "source": {
-            "type": "runtime-event",
-            "sourceKind": "database",
-            "table": "script_build_event + script_build_event_body",
-            "eventId": event.get("id"),
-            "eventName": event.get("event_name"),
-        },
-        "fieldPath": "$",
-        "value": event,
-        "characters": _characters(event),
-        "context": None,
-    }
-
-
-def _append_related_events(reads: list[dict[str, Any]], module: dict[str, Any], bundle: dict[str, Any], consumer: str, label: str) -> None:
-    for event_id in _event_ids_from_payload(module.get("payload") or {}):
-        event = next((item for item in bundle.get("events") or [] if _integer(item.get("id")) == event_id), None)
-        if event:
-            reads.append(_event_summary_read(event, consumer, label, "event", event, "calculation-input"))
-
-
-def _source_from_technical(key: str, value: Any) -> dict[str, Any]:
-    event_id = None
-    table = None
-    if isinstance(value, dict):
-        event_id = _integer(value.get("eventId") or value.get("id")) if "event" in key.lower() else None
-        table = value.get("table")
-    return {
-        "type": "inspector-projection",
-        "sourceKind": "function",
-        "functionName": "Inspector 详情组装",
-        "table": table,
-        "eventId": event_id,
-        "technicalGroup": key,
-    }
-
-
-def _event_ids(module: dict[str, Any], reads: list[dict[str, Any]]) -> list[int]:
-    values = _event_ids_from_payload(module.get("payload") or {})
-    values.extend(
-        _integer((item.get("source") or {}).get("eventId"))
-        for item in reads
-    )
-    return sorted({value for value in values if value is not None})
-
-
-def _event_ids_from_payload(value: Any) -> list[int]:
-    result: list[int] = []
-    if isinstance(value, dict):
-        for key, nested in value.items():
-            if key in {"eventId", "implementerEventId", "eventRef"}:
-                parsed = _integer(nested)
-                if parsed is None:
-                    parsed = _suffix_int(nested)
-                if parsed is not None:
-                    result.append(parsed)
-            elif key == "detailRef" and str(nested or "").startswith("event:"):
-                parsed = _suffix_int(nested)
-                if parsed is not None:
-                    result.append(parsed)
-            elif key in {"technicalRefs", "runtimeRevisionRefs"} and isinstance(nested, list):
-                result.extend(_suffix_int(item) for item in nested if _suffix_int(item) is not None)
-            elif key not in {"card", "summary", "body", "content_snapshot", "candidate_snapshot"}:
-                result.extend(_event_ids_from_payload(nested))
-    elif isinstance(value, list):
-        for item in value:
-            result.extend(_event_ids_from_payload(item))
-    return sorted({item for item in result if item is not None})
-
-
-def _public_read(
-    value: dict[str, Any], use_by_consumer: dict[str, list[str]]
-) -> dict[str, Any]:
-    public = {key: item for key, item in value.items() if key != "context"}
-    source = value.get("source") or {}
-    public["sourceKey"] = _source_key(source)
-    disposition = str(value.get("disposition") or "")
-    relation = str(value.get("relation") or "")
-    public["sourcePriority"] = {
-        "selected": "已采用来源",
-        "fallback": "备用来源",
-        "calculation-input": "计算输入",
-        "unconfirmed": "来源未确认",
-        "missing": "来源缺失",
-    }.get(disposition, "已读取来源")
-    public["transformation"] = {
-        "exact": "直接摘取",
-        "exact-record": "完整记录读取",
-        "structured": "结构化字段读取",
-        "projected": "展示处理",
-        "combined": "多来源重组",
-        "calculation-input": "参与计算",
-        "unconfirmed": "无法确认",
-    }.get(relation, relation or "未标记")
-    ratio = _usage_ratio(
-        value.get("value"),
-        use_by_consumer.get(str(value.get("consumer") or ""), []),
-        relation,
-        disposition,
-    )
-    public["usageRatio"] = (
-        {
-            "calculable": True,
-            "value": ratio,
-            "label": f"{ratio * 100:.1f}%",
-        }
-        if ratio is not None
-        else {
-            "calculable": False,
-            "value": None,
-            "label": "不可直接计算",
-        }
-    )
-    return public
-
-
-def _annotate_lineage(
-    module_id: str,
-    reads: list[dict[str, Any]],
-    contexts: list[dict[str, Any]],
-) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
-    """Attach field-level pointers without inventing lineage for derived values."""
-    contexts_by_source: dict[str, list[dict[str, Any]]] = {}
-    for context in contexts:
-        contexts_by_source.setdefault(str(context.get("sourceKey") or ""), []).append(context)
-
-    for index, read in enumerate(reads):
-        source_key = _source_key(read.get("source") or {})
-        identity = "|".join(
-            (
-                module_id,
-                str(read.get("consumer") or "module"),
-                source_key,
-                str(read.get("fieldPath") or "$"),
-                str(read.get("label") or "data"),
-                str(index),
-            )
-        )
-        read_id = f"read:{sha1(identity.encode('utf-8')).hexdigest()[:14]}"
-        read["readId"] = read_id
-        refs: list[dict[str, Any]] = []
-        for context in contexts_by_source.get(source_key, []):
-            context_path, relation = _lineage_path(read, context)
-            refs.append(
-                {
-                    "contextId": context.get("id"),
-                    "sourceKey": source_key,
-                    "contextFieldPath": context_path,
-                    "relation": relation,
-                    "exact": relation in {"direct", "normalized-alias", "whole-record"},
-                }
-            )
-        read["contextRefs"] = refs
-
-    read_ids_by_context: dict[str, list[str]] = {}
-    paths_by_context: dict[str, list[str]] = {}
-    for read in reads:
-        for ref in read.get("contextRefs") or []:
-            context_id = str(ref.get("contextId") or "")
-            if not context_id:
-                continue
-            read_ids_by_context.setdefault(context_id, []).append(str(read["readId"]))
-            field_path = str(ref.get("contextFieldPath") or "$")
-            paths_by_context.setdefault(context_id, []).append(field_path)
-
-    for context in contexts:
-        context_id = str(context.get("id") or "")
-        context["readIds"] = list(dict.fromkeys(read_ids_by_context.get(context_id, [])))
-        mapped_paths = list(dict.fromkeys(paths_by_context.get(context_id, [])))
-        if mapped_paths:
-            context["readPaths"] = mapped_paths
-    return reads, contexts
-
-
-def _lineage_path(
-    read: dict[str, Any], context: dict[str, Any]
-) -> tuple[str, str]:
-    field_path = str(read.get("fieldPath") or "$")
-    content = context.get("content")
-    source_kind = str((read.get("source") or {}).get("sourceKind") or "")
-
-    if source_kind == "function":
-        return "$", "transformed"
-    if field_path in {"$", "event"}:
-        return "$", "whole-record"
-
-    candidates = [field_path]
-    if field_path.startswith("inputData."):
-        suffix = field_path.removeprefix("inputData.")
-        candidates.append(f"eventBody.input_content.{suffix}")
-    elif field_path == "inputData":
-        candidates.append("eventBody.input_content")
-    if field_path.startswith("agentOutputData."):
-        suffix = field_path.removeprefix("agentOutputData.")
-        candidates.append(f"eventBody.output_content.{suffix}")
-    elif field_path in {"agentOutputData", "outputPreview", "output_preview"}:
-        candidates.append("eventBody.output_content")
-
-    for candidate in candidates:
-        if _value_at_path(content, candidate) is not _MISSING:
-            relation = "direct" if candidate == field_path else "normalized-alias"
-            return candidate, relation
-    return "$", "record-only"
-
-
-_MISSING = object()
-
-
-def _value_at_path(value: Any, field_path: str) -> Any:
-    if not field_path or field_path == "$":
-        return value
-    current = value
-    for part in field_path.removeprefix("$.").split("."):
-        if isinstance(current, str):
-            text = current.strip()
-            if text[:1] in {"{", "["}:
-                try:
-                    current = json.loads(text)
-                except (TypeError, ValueError, json.JSONDecodeError):
-                    return _MISSING
-        if not isinstance(current, dict) or part not in current:
-            return _MISSING
-        current = current[part]
-    return current
-
-
-def _annotate_usage_areas(
-    reads: list[dict[str, Any]], inspector: dict[str, Any] | None
-) -> list[dict[str, Any]]:
-    has_business = bool(
-        (inspector or {}).get("businessSections") or (inspector or {}).get("blocks")
-    )
-    has_changes = bool((inspector or {}).get("changes"))
-    has_technical = isinstance((inspector or {}).get("technical"), dict)
-
-    for read in reads:
-        consumer = read.get("consumer")
-        field_path = str(read.get("fieldPath") or "")
-        disposition = read.get("disposition")
-        areas: list[str] = []
-        if disposition == "fallback":
-            read["usageAreas"] = areas
-            continue
-        if consumer == "card":
-            areas.append("卡片")
-            if has_business:
-                areas.append("Inspector 业务详情")
-            if has_changes:
-                areas.append("Inspector 产出与改动")
-        elif consumer == "inspector":
-            areas.append(
-                "Inspector 技术详情"
-                if field_path.startswith("technical.")
-                else "Inspector 业务详情"
-            )
-        elif consumer == "module":
-            areas.append("模块处理")
-        read["usageAreas"] = areas
-
-    if has_technical:
-        technical_refs = {
-            _underlying_record_key(read.get("source") or {})
-            for read in reads
-            if str(read.get("fieldPath") or "").startswith("technical.")
-        }
-        technical_refs.discard(None)
-        for read in reads:
-            source = read.get("source") or {}
-            if (
-                source.get("sourceKind") == "database"
-                and _underlying_record_key(source) in technical_refs
-                and read.get("disposition") != "fallback"
-                and "Inspector 技术详情" not in read["usageAreas"]
-            ):
-                read["usageAreas"].append("Inspector 技术详情")
-    return reads
-
-
-def _underlying_record_key(source: dict[str, Any]) -> str | None:
-    event_id = _integer(source.get("eventId"))
-    if event_id is not None:
-        return f"event:{event_id}"
-    table = source.get("table")
-    record_id = source.get("recordId")
-    if table and record_id is not None:
-        return f"record:{table}:{record_id}"
-    return None
-
-
-def _source_key(source: dict[str, Any]) -> str:
-    kind = str(source.get("sourceKind") or "unknown")
-    underlying = _underlying_record_key(source)
-    if kind == "database" and underlying:
-        return f"database:{underlying}"
-    if kind == "log":
-        return f"log:event:{source.get('eventId') or 'unknown'}"
-    if kind == "function":
-        group = source.get("technicalGroup") or source.get("functionName") or "detail"
-        event = source.get("eventId")
-        return f"function:{group}:{event or 'module'}"
-    return f"unknown:{source.get('type') or 'source'}"
-
-
-def _usage_ratio(
-    value: Any, snippets: list[str], relation: str, disposition: str
-) -> float | None:
-    if (
-        disposition != "selected"
-        or relation != "exact"
-        or not isinstance(value, str)
-        or not value
-    ):
-        return None
-    normalized_source = value.strip()
-    if not normalized_source:
-        return None
-    candidates = []
-    for snippet in snippets:
-        normalized = snippet.strip().removesuffix("…").rstrip()
-        if len(normalized) >= 8 and normalized in normalized_source:
-            candidates.append(normalized)
-    if not candidates:
-        return None
-    return min(1.0, len(max(candidates, key=len)) / len(normalized_source))
-
-
-def _dedupe_reads(reads: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    seen: set[tuple[str, str, str, str]] = set()
-    result = []
-    for item in reads:
-        source = item.get("source") or {}
-        key = (str(item.get("consumer")), str(source.get("table")), str(source.get("eventId") or source.get("recordId")), str(item.get("fieldPath")))
-        if key in seen:
-            continue
-        seen.add(key)
-        result.append(item)
-    return result
-
-
-def _dedupe_contexts(contexts: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    result = []
-    seen = set()
-    for item in contexts:
-        key = item.get("id")
-        if key in seen:
-            continue
-        seen.add(key)
-        result.append(item)
-    return result
-
-
-def _unique_modules(modules: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    seen = set()
-    result = []
-    for item in modules:
-        if item["id"] in seen:
-            continue
-        seen.add(item["id"])
-        result.append(item)
-    return result
-
-
-def _find(values: Any, key: str, expected: Any) -> dict[str, Any] | None:
-    if expected is None:
-        return None
-    return next((item for item in values or [] if _integer(item.get(key)) == _integer(expected)), None)
-
-
-def _characters(value: Any) -> int:
-    if value is None:
-        return 0
-    if isinstance(value, str):
-        return len(value)
-    try:
-        return len(json.dumps(value, ensure_ascii=False, sort_keys=True, default=str, separators=(",", ":")))
-    except (TypeError, ValueError):
-        return len(str(value))
-
-
-def _strings(value: Any) -> list[str]:
-    result: list[str] = []
-    if isinstance(value, str):
-        if len(value.strip()) >= 8:
-            result.append(value.strip())
-    elif isinstance(value, dict):
-        for nested in value.values():
-            result.extend(_strings(nested))
-    elif isinstance(value, list):
-        for nested in value:
-            result.extend(_strings(nested))
-    return sorted(set(result), key=len, reverse=True)
-
-
-def _matching_snippets(value: Any, snippets: list[str]) -> list[str]:
-    text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
-    return [snippet for snippet in snippets if snippet in text][:20]
-
-
-def _technical_label(key: str) -> str:
-    return {
-        "source": "数据来源与定位",
-        "association": "关联方式与完整性",
-        "input": "原始输入",
-        "output": "原始输出",
-        "cost": "耗时与成本",
-        "record": "原始业务记录",
-        "event": "原始运行事件",
-        "taskEvent": "实现任务事件",
-        "taskSource": "任务来源判定",
-        "rawEvent": "运行事件摘要",
-        "branch": "实现方案记录",
-        "round": "轮次记录",
-    }.get(key, key)
-
-
-def _context_id(source: dict[str, Any], field_path: Any) -> str:
-    return ":".join(str(item) for item in (source.get("table") or source.get("type"), source.get("recordId") or source.get("eventId") or "record", field_path or "$") )
-
-
-def _completeness(notice: str | None, reads: list[dict[str, Any]], contexts: list[dict[str, Any]]) -> dict[str, Any]:
-    missing = [item for item in contexts if item.get("completeness") in {"missing", "body-missing"}]
-    unconfirmed = [item for item in reads if item.get("disposition") == "unconfirmed"]
-    return {
-        "state": "partial" if notice or missing or unconfirmed else "complete",
-        "inspectorAvailable": notice is None,
-        "sourceCount": len(reads),
-        "contextCount": len(contexts),
-        "missingContextCount": len(missing),
-        "unconfirmedSourceCount": len(unconfirmed),
-        "notice": notice,
-        "redacted": True,
-    }
-
-
-def _suffix_int(value: Any) -> int | None:
-    text = str(value or "")
-    try:
-        return int(text.rsplit(":", 1)[-1])
-    except (TypeError, ValueError):
-        return None
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value) if value is not None else None
-    except (TypeError, ValueError):
-        return None

+ 0 - 805
visualization/backend/app/module_audit/static/app.js

@@ -1,805 +0,0 @@
-(() => {
-  const runId = document.body.dataset.runId;
-  const root = document.getElementById("audit-root");
-  const runSummary = document.getElementById("run-summary");
-  const moduleJump = document.getElementById("module-jump");
-  const onlyUsedFields = document.getElementById("only-used-fields");
-  const sourceFilter = document.getElementById("source-filter");
-  const readColumnTitle = document.getElementById("read-column-title");
-  const contextColumnTitle = document.getElementById("context-column-title");
-  const detailCache = new Map();
-  const renderedModules = new Set();
-  const renderQueue = [];
-  let renderScheduled = false;
-  let runMeta = null;
-  let moduleTotal = 0;
-  let streamComplete = false;
-
-  const jsonText = (value) => {
-    if (value === null || value === undefined) return "";
-    if (typeof value === "string") return value;
-    try { return JSON.stringify(value, null, 2); } catch { return String(value); }
-  };
-  const chars = (value) => Array.from(jsonText(value)).length;
-  const escapeHtml = (value) => String(value ?? "")
-    .replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
-    .replaceAll('"', "&quot;").replaceAll("'", "&#039;");
-  const truncate = (value, length = 240) => {
-    const text = jsonText(value).replace(/\s+/g, " ").trim();
-    return Array.from(text).length > length ? `${Array.from(text).slice(0, length).join("")}…` : text;
-  };
-  const excerptAround = (value, needle, length = 300) => {
-    const text = jsonText(value).replace(/\s+/g, " ").trim();
-    if (!needle || !text.includes(needle)) return truncate(text, length);
-    const charsBefore = 80;
-    const index = text.indexOf(needle);
-    const start = Math.max(0, index - charsBefore);
-    const end = Math.min(text.length, start + Math.max(length, needle.length + charsBefore));
-    return `${start > 0 ? "…" : ""}${text.slice(start, end)}${end < text.length ? "…" : ""}`;
-  };
-  const correlationText = (value) => {
-    if (value === null || value === undefined) return "";
-    if (typeof value === "string") return value.trim();
-    if (typeof value === "number" || typeof value === "boolean") return String(value);
-    return jsonText(value).trim();
-  };
-  const makeSegments = (scope, items) => items
-    .filter((item) => item && correlationText(item.value))
-    .map((item, index) => ({ ...item, id: `${scope}-${index + 1}`, number: index + 1, color: index % 8, text: correlationText(item.value) }));
-  const matchingSegments = (value, segments) => {
-    const text = correlationText(value);
-    if (!text) return [];
-    return segments.filter((segment) => segment.text.length >= 2 && text.includes(segment.text));
-  };
-  const labelForDisposition = (value) => ({ selected: "已采用", fallback: "备用来源", "calculation-input": "参与计算", unconfirmed: "来源未确认", missing: "记录缺失" }[value] || value || "已读取");
-  const sourceKind = (source = {}) => source.sourceKind || "unknown";
-  const sourceKindLabel = (kind) => ({ database: "数据库", function: "函数", log: "日志", unknown: "未确认" }[kind] || "未确认");
-  const relationLabel = (value) => ({ exact: "直接摘取", "exact-record": "完整记录", structured: "结构化字段", projected: "展示处理", combined: "多来源重组", "calculation-input": "参与计算", anchored: "日志定位", missing: "记录缺失" }[value] || value || "未标记");
-  const completenessLabel = (value) => ({ complete: "完整", "body-missing": "Event Body 缺失", missing: "记录缺失", partial: "部分可用" }[value] || value || "未知");
-
-  function sourceName(source = {}) {
-    const kind = sourceKind(source);
-    if (kind === "database") return `数据库:${source.table || "表名未记录"}`;
-    if (kind === "function") return `函数:${source.functionName || "Inspector 详情组装"}`;
-    if (kind === "log") return `日志:${source.table || "script_build_log"}`;
-    return "来源未确认";
-  }
-
-  function sourceDetail(source = {}) {
-    return [
-      source.recordId !== null && source.recordId !== undefined ? `记录 #${source.recordId}` : null,
-      source.eventId !== null && source.eventId !== undefined ? `Event ${source.eventId}` : null,
-      source.eventName || null,
-      source.technicalGroup ? `处理组 ${source.technicalGroup}` : null,
-    ].filter(Boolean).join(" · ");
-  }
-
-  function buildSourceRegistry(reads, contexts) {
-    const registry = new Map();
-    const counters = { database: 0, function: 0, log: 0, unknown: 0 };
-    [...reads, ...contexts].forEach((item) => {
-      const key = item.sourceKey || `unknown:${registry.size + 1}`;
-      if (registry.has(key)) return;
-      const kind = sourceKind(item.source || {});
-      counters[kind] = (counters[kind] || 0) + 1;
-      registry.set(key, { key, kind, label: `${sourceKindLabel(kind)} ${counters[kind]}`, source: item.source || {} });
-    });
-    return registry;
-  }
-
-  function groupReads(reads) {
-    const groups = new Map();
-    reads.forEach((read, index) => {
-      const key = read.sourceKey || `unknown:${index}`;
-      if (!groups.has(key)) groups.set(key, { key, source: read.source || {}, items: [], usageAreas: new Set() });
-      const group = groups.get(key);
-      group.items.push(read);
-      (Array.isArray(read.usageAreas) ? read.usageAreas : []).forEach((area) => group.usageAreas.add(area));
-    });
-    return [...groups.values()];
-  }
-
-  function groupsForAreas(groups, areas) {
-    const wanted = new Set(areas);
-    return groups.filter((group) => [...group.usageAreas].some((area) => wanted.has(area)));
-  }
-
-  function referencesForGroups(groups, registry) {
-    return groups.map((group) => registry.get(group.key)).filter(Boolean);
-  }
-
-  function contextsForGroups(contexts, groups) {
-    const keys = new Set(groups.map((group) => group.key));
-    return contexts.filter((context) => keys.has(context.sourceKey) || (context.kind === "log-module" && context.relatedSourceKey && keys.has(context.relatedSourceKey)));
-  }
-
-  function activeSourceKind() {
-    return sourceFilter?.value || "all";
-  }
-
-  function filterGroups(groups) {
-    const wanted = activeSourceKind();
-    return wanted === "all" ? groups : groups.filter((group) => sourceKind(group.source) === wanted);
-  }
-
-  function filterContexts(contexts) {
-    const wanted = activeSourceKind();
-    return wanted === "all" ? contexts : contexts.filter((context) => sourceKind(context.source) === wanted);
-  }
-
-  function parseStructuredText(value) {
-    if (typeof value !== "string") return value;
-    const text = value.trim();
-    if (!text || !["{", "["].includes(text[0])) return value;
-    try { return JSON.parse(text); } catch { return value; }
-  }
-
-  function sourceLinks(links = [], usageAreas = []) {
-    if (!links.length && !usageAreas.length) return "";
-    const refs = links.map((item) => `<span class="sourceTag source-${escapeHtml(item.kind)}" role="button" tabindex="0" aria-pressed="false" data-source-key="${escapeHtml(item.key)}" title="突出显示三列中的对应内容">${escapeHtml(item.label)}</span>`).join("");
-    const usages = usageAreas.map((area) => `<span class="usageTag">${escapeHtml(area)}</span>`).join("");
-    return `<span class="sourceLinks">${refs}${usages}</span>`;
-  }
-
-  function correlationLinks(segments = []) {
-    if (!segments.length) return "";
-    return `<span class="correlationLinks" aria-label="字段对应关系">${segments.map((segment) => `<span class="correlationChip correlation-${segment.color}" role="button" tabindex="0" aria-pressed="false" data-correlation-id="${escapeHtml(segment.id)}" title="突出显示三列中的对应字段"><b>${String(segment.number).padStart(2, "0")}</b>${escapeHtml(segment.label)}</span>`).join("")}</span>`;
-  }
-
-  function contentBox(title, value, options = {}) {
-    const count = options.characters ?? chars(value);
-    const long = count > 800;
-    const body = options.html ?? `<pre>${escapeHtml(jsonText(value))}</pre>`;
-    const meta = [options.meta, `${count.toLocaleString("zh-CN")} 字`].filter(Boolean).join(" · ");
-    const sourceKeys = [...new Set([...(options.links || []).map((item) => item.key), ...(options.sourceKeys || [])].filter(Boolean))].join(" ");
-    return `<details class="contentBox ${options.tone ? `tone-${options.tone}` : ""}" data-source-keys="${escapeHtml(sourceKeys)}" ${long ? "" : "open"}>
-      <summary><span class="summaryLine"><b>${escapeHtml(title)}</b><small>${escapeHtml(meta)}</small></span>${sourceLinks(options.links, options.usageAreas)}${correlationLinks(options.correlationSegments)}${long && options.showPreview ? `<em>${escapeHtml(truncate(value))}</em>` : ""}</summary>
-      <div class="contentBody">${body}</div>
-    </details>`;
-  }
-
-  function cardSegments(card) {
-    return makeSegments("card", (card?.fields || []).map((field) => ({ label: field?.label || "内容", value: field?.value })));
-  }
-
-  function businessSegments(inspector) {
-    const items = [];
-    (inspector?.businessSections || []).forEach((section) => items.push({ label: section.title || "内容", value: section.content ?? section.items ?? section }));
-    (inspector?.blocks || []).forEach((block) => items.push({ label: block.title || block.type || "决策内容", value: block.value ?? block.items ?? block }));
-    if (inspector?.changes) items.push({ label: "产出与改动", value: inspector.changes });
-    return makeSegments("business", items);
-  }
-
-  function technicalSegments(inspector) {
-    const technical = inspector?.technical;
-    if (!technical || typeof technical !== "object") return [];
-    return makeSegments("technical", Object.entries(technical).map(([label, value]) => ({ label, value })));
-  }
-
-  function targetAttrs(segment) {
-    if (!segment) return "";
-    return ` role="button" tabindex="0" data-correlation-id="${escapeHtml(segment.id)}"`;
-  }
-
-  function renderCard(card, links, segments) {
-    if (!card) return contentBox("卡片", "该模块没有卡片展示数据。", { tone: "muted" });
-    const fields = (card.fields || []).filter((field) => field && field.value !== null && field.value !== undefined && field.value !== "");
-    const html = `<div class="displayCard"><h4>${escapeHtml(card.title || "卡片")}</h4>${fields.map((field, index) => {
-      const segment = segments[index];
-      return `<div class="displayField${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><b>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(field.label || "内容")}</b><div>${renderPlainValue(field.value)}</div></div>`;
-    }).join("") || `<p class="empty">卡片没有可显示字段。</p>`}</div>`;
-    return contentBox("卡片", card, { html, characters: chars(card), meta: "最终显示内容", tone: "used", links, correlationSegments: segments });
-  }
-
-  function businessDetailPayload(inspector) {
-    return {
-      businessSections: inspector?.businessSections || [],
-      blocks: inspector?.blocks || [],
-      changes: inspector?.changes ?? null,
-    };
-  }
-
-  function renderBusinessDetail(inspector, notice, links, segments) {
-    if (!inspector) return contentBox("业务详情", notice || "该模块没有业务详情。", { tone: "muted", links });
-    const parts = [];
-    let segmentIndex = 0;
-    (inspector.businessSections || []).forEach((section) => {
-      const segment = segments[segmentIndex++];
-      parts.push(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(section.title || "内容")}</h4>${renderPlainValue(section.content ?? section.items ?? section)}</section>`);
-    });
-    (inspector.blocks || []).forEach((block) => {
-      const segment = segments[segmentIndex++];
-      parts.push(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(block.title || block.type || "决策内容")}</h4>${renderPlainValue(block.value ?? block.items ?? block)}</section>`);
-    });
-    if (inspector.changes) {
-      const segment = segments[segmentIndex];
-      parts.push(`<section class="businessPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}产出与改动</h4>${renderPlainValue(inspector.changes)}</section>`);
-    }
-    const payload = businessDetailPayload(inspector);
-    return contentBox("业务详情", payload, {
-      html: parts.length ? `<div class="businessDetailBody">${parts.join("")}</div>` : `<p class="empty">本次运行没有记录业务详情。</p>`,
-      characters: chars(payload),
-      meta: "Inspector 最终显示内容",
-      tone: "used",
-      links,
-      correlationSegments: segments,
-    });
-  }
-
-  function renderTechnicalDetail(inspector, notice, links, segments) {
-    if (!inspector || !Object.prototype.hasOwnProperty.call(inspector, "technical")) {
-      return contentBox("技术详情", notice || "该模块没有技术详情。", { tone: "muted", links });
-    }
-    const technical = inspector.technical;
-    const html = technical && typeof technical === "object"
-      ? `<div class="technicalDetailBody">${Object.entries(technical).map(([label, value], index) => {
-        const segment = segments[index];
-        return `<section class="technicalPart${segment ? ` correspondenceTarget correlation-${segment.color}` : ""}"${targetAttrs(segment)}><h4>${segment ? `<span class="correlationNumber">${String(segment.number).padStart(2, "0")}</span>` : ""}${escapeHtml(label)}</h4>${renderPlainValue(value)}</section>`;
-      }).join("")}</div>`
-      : renderPlainValue(technical);
-    return contentBox("技术详情", technical, { html, meta: "Inspector 最终显示内容", tone: "read", links, correlationSegments: segments });
-  }
-
-  function renderReadGroup(group, registry, segments) {
-    const entry = registry.get(group.key);
-    const usageAreas = [...group.usageAreas];
-    const count = group.items.reduce((sum, item) => sum + Number(item.characters || 0), 0);
-    const correlated = segments.filter((segment) => group.items.some((item) => matchingSegments(item.value, [segment]).length));
-    const html = group.items.map((read) => {
-      const status = `<span class="readStatus status-${escapeHtml(read.disposition)}">${escapeHtml(labelForDisposition(read.disposition))}</span>`;
-      const ratio = read.usageRatio?.label || "不可直接计算";
-      const field = read.fieldPath && read.fieldPath !== "$" ? `读取字段:${read.fieldPath}` : "读取完整记录";
-      const exactRef = (read.contextRefs || []).find((ref) => ref.exact);
-      const locateLabel = exactRef ? "定位原始字段" : "定位原始记录";
-      return `<div class="readItem" data-read-id="${escapeHtml(read.readId || "")}"><div class="readItemHeader"><b>${escapeHtml(read.label || "数据")}</b><code>${escapeHtml(field)}</code></div><div class="readMeta">${status}<span>${escapeHtml(read.transformation || relationLabel(read.relation))}</span><span>使用比例 ${escapeHtml(ratio)}</span><button type="button" class="locateContextButton" data-locate-read-id="${escapeHtml(read.readId || "")}">${locateLabel}</button></div><div class="contextTree readValueTree">${renderContextValue(read.value, "", [], [], correlated)}</div></div>`;
-    }).join("");
-    const detail = sourceDetail(group.source);
-    return contentBox(sourceName(group.source), group.items.map((item) => item.value), { html, characters: count, meta: [detail, `${group.items.length} 项读取`].filter(Boolean).join(" · "), tone: sourceKind(group.source), links: entry ? [entry] : [], usageAreas: usageAreas.length ? usageAreas : ["未进入界面"], correlationSegments: correlated });
-  }
-
-  function refsForContext(groups, context) {
-    return groups.flatMap((group) => group.items).flatMap((read) =>
-      (read.contextRefs || [])
-        .filter((ref) => ref.contextId === context.id)
-        .map((ref) => ({ read, ref }))
-    );
-  }
-
-  function renderContext(context, groups, registry, segments) {
-    const source = context.source || {};
-    const entry = registry.get(context.sourceKey);
-    const meta = [context.title, sourceDetail(source), relationLabel(context.match), completenessLabel(context.completeness)].filter(Boolean).join(" · ");
-    const readPaths = Array.isArray(context.readPaths) ? context.readPaths : [];
-    const usedFragments = Array.isArray(context.usedFragments) ? context.usedFragments : [];
-    const correlated = matchingSegments(context.content, segments);
-    const exactFields = refsForContext(groups, context)
-      .filter(({ ref }) => ref.contextFieldPath)
-      .map(({ read, ref }) => ({ read, ref, value: resolveFieldPath(context.content, ref.contextFieldPath) }))
-      .filter(({ value }) => value !== undefined);
-    const raw = exactFields.length
-      ? `<section class="exactFieldMap"><h4>第二列对应的原始字段</h4>${exactFields.map(({ read, ref, value }) => renderRawField(read, value, segments, [], ref)).join("")}</section>`
-      : "";
-    const html = renderContextValue(context.content, "", readPaths, usedFragments, correlated);
-    return contentBox(sourceName(source), context.content, { html: `${raw}<div class="contextTree">${html}</div>`, characters: context.characters, meta, tone: context.completeness === "complete" ? sourceKind(source) : "warning", links: entry ? [entry] : [], sourceKeys: [context.relatedSourceKey], correlationSegments: correlated });
-  }
-
-  function renderReads(groups, registry, segments) {
-    return groups.map((group) => renderReadGroup(group, registry, segments)).join("") || contentBox("读取来源", "这一部分没有可确认的读取数据。", { tone: "muted" });
-  }
-
-  function renderContexts(contexts, groups, registry, segments) {
-    const matched = filterContexts(contextsForGroups(contexts, groups));
-    return matched.map((context) => renderContext(context, groups, registry, segments)).join("") || contentBox("原始记录与上下文", "这一部分没有可定位的原始记录或日志上下文。", { tone: "muted" });
-  }
-
-  function compactSourceLabel(group) {
-    const source = group.source || {};
-    const event = group.items.map((item) => item.value).find((value) => value && typeof value === "object" && value.event_name);
-    const eventName = source.eventName || event?.event_name;
-    const record = source.eventId !== null && source.eventId !== undefined ? `Event ${source.eventId}` : source.recordId !== null && source.recordId !== undefined ? `记录 ${source.recordId}` : "";
-    return [sourceKindLabel(sourceKind(source)), eventName || source.functionName || source.table, record].filter(Boolean).join(" · ");
-  }
-
-  function previewWithMatch(value, needle) {
-    const preview = excerptAround(value, needle, 300);
-    if (!needle || !preview.includes(needle)) return escapeHtml(preview);
-    const index = preview.indexOf(needle);
-    return `${escapeHtml(preview.slice(0, index))}<mark class="valueMatch">${escapeHtml(needle)}</mark>${escapeHtml(preview.slice(index + needle.length))}`;
-  }
-
-  function renderValueDisclosure(value, label = "实际读取值", needle = "") {
-    const count = chars(value);
-    if (count <= 260 && typeof parseStructuredText(value) !== "object") {
-      return `<div class="fieldActualValue"><b>${escapeHtml(label)}</b><p>${previewWithMatch(value, needle)}</p></div>`;
-    }
-    return `<details class="valueDisclosure"><summary><span>${escapeHtml(label)}</span><small>${count.toLocaleString("zh-CN")} 字 · 展开全文</small><em>${previewWithMatch(value, needle)}</em></summary><div class="valueDisclosureBody">${renderPlainValue(value)}</div></details>`;
-  }
-
-  function renderCompactSource(group, segments) {
-    const unique = new Map();
-    group.items.forEach((read) => {
-      if (read.value === null || read.value === undefined || read.value === "") return;
-      const key = read.readId || `${read.fieldPath || "$"}::${jsonText(read.value)}`;
-      if (!unique.has(key)) unique.set(key, read);
-    });
-    const items = [...unique.values()];
-    const visible = items.slice(0, 8);
-    const hidden = items.length - visible.length;
-    return `<section class="lineageSource" data-source-key="${escapeHtml(group.key)}">
-      <header class="lineageSourceHeader"><span class="sourceKindPill source-${escapeHtml(sourceKind(group.source))}">${escapeHtml(compactSourceLabel(group))}</span></header>
-      ${visible.map((read) => {
-        const related = matchingSegments(read.value, segments);
-        const ids = related.map((segment) => segment.id).join(" ");
-        const tone = related[0] ? ` correspondenceTarget correlation-${related[0].color}` : "";
-        const exactRef = (read.contextRefs || []).find((ref) => ref.exact);
-        return `<div class="readValueEntry${tone}" data-read-id="${escapeHtml(read.readId || "")}"${ids ? ` data-correlation-ids="${escapeHtml(ids)}"` : ""}><div class="readValueMeta"><span>${escapeHtml(read.label || "读取字段")}</span><code>${escapeHtml(read.fieldPath || "$" )}</code>${correlationLinks(related)}<button type="button" class="locateContextButton" data-locate-read-id="${escapeHtml(read.readId || "")}" data-locate-correlation="${escapeHtml(related[0]?.id || "")}">${exactRef ? "定位原始字段" : "定位原始记录"}</button></div>${renderValueDisclosure(read.value, "实际读取值", related[0]?.text || "")}${related.length ? "" : `<small class="lineageUnproven">参与展示处理,但无法逐字定位到左侧单一字段</small>`}</div>`;
-      }).join("")}
-      ${hidden > 0 ? `<small class="moreFieldsNotice">另有 ${hidden} 个读取字段,可在完整记录模式查看。</small>` : ""}
-    </section>`;
-  }
-
-  function renderCompactReads(groups, contexts, segments) {
-    if (!groups.length) return `<div class="compactEmpty">当前来源筛选下没有业务读取字段。</div>`;
-    const items = groups.map((group) => renderCompactSource(group, segments)).join("");
-    return `<section class="compactPanel"><header><h4>字段来源</h4><span>${groups.reduce((sum, group) => sum + group.items.length, 0)} 个实际读取字段</span></header><div class="lineageSources">${items}</div></section>`;
-  }
-
-  function contextDigest(context) {
-    const content = context.content && typeof context.content === "object" ? context.content : {};
-    const eventName = content.event_name || context.source?.eventName || context.source?.functionName || context.title;
-    const eventId = context.source?.eventId;
-    return { context, eventName, eventId };
-  }
-
-  function resolveFieldPath(value, fieldPath) {
-    if (!fieldPath || fieldPath === "$" || fieldPath === "event") return value;
-    const parts = String(fieldPath).replace(/^\$\.?/, "").replaceAll("[]", "").split(".").filter(Boolean);
-    let current = value;
-    for (const part of parts) {
-      current = parseStructuredText(current);
-      if (!current || typeof current !== "object" || !(part in current)) return undefined;
-      current = current[part];
-    }
-    return parseStructuredText(current);
-  }
-
-  function renderRawField(read, value, segments, derivedSegments, ref = {}) {
-    const exact = matchingSegments(value, segments);
-    const related = exact.length ? exact : derivedSegments;
-    const ids = related.map((segment) => segment.id).join(" ");
-    const tone = related[0] ? ` correlation-${related[0].color}` : "";
-    const relation = ref.relation === "record-only" ? "只能定位到记录" : ref.relation === "transformed" ? "函数处理结果" : "精确字段对应";
-    return `<div class="rawField${tone}" tabindex="-1" data-read-ids="${escapeHtml(read.readId || "")}"${ids ? ` data-correlation-ids="${escapeHtml(ids)}"` : ""}><header><div><span>${escapeHtml(read.label || "读取字段")}</span><code>${escapeHtml(ref.contextFieldPath || read.fieldPath || "完整记录")}</code></div><em class="lineageRelation">${escapeHtml(relation)}</em>${correlationLinks(related)}</header>${renderValueDisclosure(value, "原始字段值", exact[0]?.text || "")}</div>`;
-  }
-
-  function renderCompactContexts(contexts, groups, segments) {
-    const matched = filterContexts(contextsForGroups(contexts, groups));
-    if (!matched.length) return `<div class="compactEmpty">当前来源筛选下没有相关原始记录。</div>`;
-    const grouped = new Map();
-    matched.forEach((context) => {
-      const kind = sourceKind(context.source);
-      const key = `${kind}::${context.sourceKey || context.id}`;
-      if (!grouped.has(key)) grouped.set(key, { context, contexts: [], eventIds: [] });
-      const bucket = grouped.get(key);
-      bucket.contexts.push(context);
-      if (context.source?.eventId !== null && context.source?.eventId !== undefined) bucket.eventIds.push(context.source.eventId);
-    });
-    const html = [...grouped.values()].map((bucket) => {
-      const context = bucket.context;
-      const kind = sourceKind(context.source);
-      const digest = contextDigest(context);
-      const records = bucket.eventIds.length ? `Event ${[...new Set(bucket.eventIds)].join("、")}` : sourceDetail(context.source);
-      const sourceKey = context.sourceKey || "";
-      const relatedGroups = groups.filter((group) => group.key === sourceKey || (context.relatedSourceKey && group.key === context.relatedSourceKey));
-      const reads = relatedGroups.flatMap((group) => group.items);
-      const uniqueReads = new Map();
-      reads.forEach((read) => {
-        (read.contextRefs || []).forEach((ref) => {
-          const targetContext = bucket.contexts.find((candidate) => candidate.id === ref.contextId);
-          if (!targetContext) return;
-          const value = resolveFieldPath(targetContext.content, ref.contextFieldPath);
-          if (value === undefined) return;
-          const key = `${read.readId || "read"}::${ref.contextId}::${ref.contextFieldPath}`;
-          if (!uniqueReads.has(key)) uniqueReads.set(key, { read, value, ref });
-        });
-      });
-      if (kind === "log") return `<article class="sourceDigest tone-log" tabindex="-1" data-source-key="${escapeHtml(context.relatedSourceKey || sourceKey)}"><header><span class="sourceKindPill source-log">日志</span><strong>${escapeHtml(digest.eventName)}</strong><em>${escapeHtml(records || `${context.characters || 0} 字`)}</em></header>${renderValueDisclosure(context.content, "锚定日志上下文")}</article>`;
-      const fields = [...uniqueReads.values()].slice(0, 6);
-      const hidden = uniqueReads.size - fields.length;
-      return `<article class="sourceDigest tone-${escapeHtml(kind)}" tabindex="-1" data-source-key="${escapeHtml(sourceKey)}"><header><span class="sourceKindPill source-${escapeHtml(kind)}">${escapeHtml(sourceKindLabel(kind))}</span><strong>${escapeHtml(digest.eventName)}</strong><em>${escapeHtml(records)}</em></header><div class="recordLocation"><span>原始记录位置</span><code>${escapeHtml(context.source?.table || sourceName(context.source))}</code>${context.source?.recordId !== null && context.source?.recordId !== undefined ? `<code>#${escapeHtml(context.source.recordId)}</code>` : ""}${context.source?.eventId !== null && context.source?.eventId !== undefined ? `<code>Event ${escapeHtml(context.source.eventId)}</code>` : ""}</div>${fields.length ? `<div class="rawFields">${fields.map(({ read, value, ref }) => renderRawField(read, value, segments, [], ref)).join("")}</div>` : `<p>这条记录参与了处理,但没有可确认的独立读取字段。</p>`}${hidden > 0 ? `<small class="moreFieldsNotice">另有 ${hidden} 个字段,可在完整记录模式查看。</small>` : ""}</article>`;
-    }).join("");
-    return `<section class="compactPanel"><header><h4>原始字段与记录</h4><span>${grouped.size} 组</span></header>${html}</section>`;
-  }
-
-  function correspondenceRow(kind, actualUse, groups, contextGroups, contexts, registry, segments, compact) {
-    return `<section class="correspondenceRow" data-area="${escapeHtml(kind)}">
-      <div class="correspondenceCell" data-column="use"><div class="columnLabel">模块实际使用</div>${actualUse}</div>
-      <div class="correspondenceCell" data-column="read"><div class="columnLabel">${compact ? "字段来源与读取" : "模块实际读取"}</div>${compact ? renderCompactReads(groups, contexts, segments) : renderReads(groups, registry, segments)}</div>
-      <div class="correspondenceCell" data-column="context"><div class="columnLabel">${compact ? "原始字段与记录" : "原始记录与上下文"}</div>${compact ? renderCompactContexts(contexts, contextGroups, segments) : renderContexts(contexts, contextGroups, registry, segments)}</div>
-    </section>`;
-  }
-
-  function renderPlainValue(value) {
-    if (value === null || value === undefined || value === "") return `<span class="empty">空</span>`;
-    if (typeof value === "object") return `<pre>${escapeHtml(jsonText(value))}</pre>`;
-    return `<p>${escapeHtml(String(value)).replaceAll("\n", "<br>")}</p>`;
-  }
-
-  function renderContextValue(value, path, readPaths, snippets, segments = []) {
-    if (Array.isArray(value)) return `<ol>${value.map((item, index) => `<li>${renderContextValue(item, `${path}[${index}]`, readPaths, snippets, segments)}</li>`).join("")}</ol>`;
-    if (value && typeof value === "object") return `<dl>${Object.entries(value).map(([key, nested]) => {
-      const childPath = path ? `${path}.${key}` : key;
-      const read = readPaths.some((item) => item === "$" || childPath === item || childPath.endsWith(`.${item}`) || item.startsWith(`${childPath}.`));
-      const correlated = matchingSegments(nested, segments).length > 0;
-      return `<div class="contextEntry ${read ? "isRead" : "isDim"}${correlated ? " hasCorrelationData" : ""}"><dt>${escapeHtml(key)}</dt><dd>${renderContextValue(nested, childPath, readPaths, snippets, segments)}</dd></div>`;
-    }).join("")}</dl>`;
-    const text = String(value ?? "");
-    return `<span>${highlightCorrelations(text, segments) || highlightUsed(text, snippets)}</span>`;
-  }
-
-  function highlightCorrelations(text, segments) {
-    const matches = [];
-    segments.forEach((segment) => {
-      if (!segment.text || segment.text.length < 2) return;
-      let from = 0;
-      let found = 0;
-      while (from <= text.length && found < 24) {
-        const index = text.indexOf(segment.text, from);
-        if (index < 0) break;
-        matches.push({ start: index, end: index + segment.text.length, segment });
-        from = index + Math.max(segment.text.length, 1);
-        found += 1;
-      }
-    });
-    if (!matches.length) return "";
-    matches.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
-    const accepted = [];
-    let cursor = -1;
-    matches.forEach((match) => {
-      if (match.start < cursor) return;
-      accepted.push(match);
-      cursor = match.end;
-    });
-    let output = "";
-    cursor = 0;
-    accepted.forEach((match) => {
-      output += escapeHtml(text.slice(cursor, match.start));
-      output += `<mark class="correlationMark correlation-${match.segment.color}" role="button" tabindex="0" data-correlation-id="${escapeHtml(match.segment.id)}" title="对应 ${escapeHtml(match.segment.label)}">${escapeHtml(text.slice(match.start, match.end))}</mark>`;
-      cursor = match.end;
-    });
-    output += escapeHtml(text.slice(cursor));
-    return output.replaceAll("\n", "<br>");
-  }
-
-  function highlightUsed(text, snippets) {
-    const candidates = (Array.isArray(snippets) ? snippets : []).filter((snippet) => snippet && snippet.length >= 8 && text.includes(snippet)).sort((a, b) => b.length - a.length);
-    if (!candidates.length) return escapeHtml(text).replaceAll("\n", "<br>");
-    const snippet = candidates[0];
-    const index = text.indexOf(snippet);
-    return `${escapeHtml(text.slice(0, index))}<mark>${escapeHtml(snippet)}</mark>${escapeHtml(text.slice(index + snippet.length))}`.replaceAll("\n", "<br>");
-  }
-
-  function moduleRow(module) {
-    const path = (module.path || []).join(" / ");
-    const variant = module.displayVariant === "collapsed-summary" ? `<span class="variant">折叠态摘要</span>` : "";
-    return `<article class="moduleRow" data-module-id="${escapeHtml(module.id)}" tabindex="-1">
-      <header><div><span>${escapeHtml(path)}</span><h3>${escapeHtml(module.title)}</h3></div><div class="moduleMeta">${variant}<code>${escapeHtml(module.id)}</code><span>${Number(module.cardCharacters || 0).toLocaleString("zh-CN")} 字卡片内容</span><span class="moduleStreamState">正在自动加载</span></div></header>
-      <div class="moduleDeferred"><div class="cellLoading"><i></i><i></i></div></div>
-    </article>`;
-  }
-
-  function populateModuleJump(modules) {
-    if (!moduleJump) return;
-    const groups = new Map();
-    modules.forEach((module) => {
-      const group = module.group || "其他";
-      if (!groups.has(group)) groups.set(group, []);
-      groups.get(group).push(module);
-    });
-    const options = [...groups.entries()].map(([group, items]) => `<optgroup label="${escapeHtml(group)}">${items.map((module) => {
-      const path = (module.path || []).filter(Boolean).join(" / ");
-      const label = path ? `${module.title} · ${path}` : module.title;
-      return `<option value="${escapeHtml(module.id)}">${escapeHtml(label)}</option>`;
-    }).join("")}</optgroup>`).join("");
-    moduleJump.innerHTML = `<option value="">选择一张卡片…</option>${options}`;
-    moduleJump.disabled = modules.length === 0;
-  }
-
-  function jumpToModule(moduleId, options = {}) {
-    if (!moduleId) return;
-    const row = [...document.querySelectorAll(".moduleRow")].find((item) => item.dataset.moduleId === moduleId);
-    if (!row) return;
-    const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
-    row.scrollIntoView({ behavior: reduceMotion || options.instant ? "auto" : "smooth", block: "start" });
-    row.focus({ preventScroll: true });
-    row.classList.remove("isJumpTarget");
-    void row.offsetWidth;
-    row.classList.add("isJumpTarget");
-    window.setTimeout(() => row.classList.remove("isJumpTarget"), 1600);
-    if (options.updateHash !== false) history.replaceState(null, "", `#module=${encodeURIComponent(moduleId)}`);
-  }
-
-  function renderModuleDetail(row, detail) {
-    const reads = detail.actualReads || [];
-    const contexts = detail.contexts || [];
-    const registry = buildSourceRegistry(reads, contexts);
-    const sourceGroups = groupReads(reads);
-    const cardContextGroups = groupsForAreas(sourceGroups, ["卡片"]);
-    const businessContextGroups = groupsForAreas(sourceGroups, ["Inspector 业务详情", "Inspector 产出与改动"]);
-    const assigned = new Set([...cardContextGroups, ...businessContextGroups].map((group) => group.key));
-    const technicalContextGroups = [
-      ...groupsForAreas(sourceGroups, ["Inspector 技术详情", "模块处理"]),
-      ...sourceGroups.filter((group) => !assigned.has(group.key) && group.usageAreas.size === 0),
-    ].filter((group, index, all) => all.findIndex((candidate) => candidate.key === group.key) === index);
-    const cardGroups = filterGroups(cardContextGroups);
-    const businessGroups = filterGroups(businessContextGroups);
-    const technicalGroups = filterGroups(technicalContextGroups);
-    const cardLinks = referencesForGroups(cardGroups, registry);
-    const businessLinks = referencesForGroups(businessGroups, registry);
-    const technicalLinks = referencesForGroups(technicalGroups, registry);
-    const cardFieldSegments = cardSegments(detail.actualUse?.card);
-    const businessFieldSegments = businessSegments(detail.actualUse?.inspector);
-    const technicalFieldSegments = technicalSegments(detail.actualUse?.inspector);
-    const compact = onlyUsedFields?.checked !== false;
-    const rows = [
-      correspondenceRow("卡片", renderCard(detail.actualUse?.card, cardLinks, cardFieldSegments), cardGroups, cardContextGroups, contexts, registry, cardFieldSegments, compact),
-      correspondenceRow("业务详情", renderBusinessDetail(detail.actualUse?.inspector, detail.actualUse?.inspectorNotice, businessLinks, businessFieldSegments), businessGroups, businessContextGroups, contexts, registry, businessFieldSegments, compact),
-      correspondenceRow("技术详情", renderTechnicalDetail(detail.actualUse?.inspector, detail.actualUse?.inspectorNotice, technicalLinks, technicalFieldSegments), technicalGroups, technicalContextGroups, contexts, registry, technicalFieldSegments, compact),
-    ];
-    const target = row.querySelector(".moduleDeferred, .moduleColumns, .correspondenceRows");
-    if (target) target.outerHTML = `<div class="correspondenceRows ${compact ? "isCompact" : "isComplete"}">${rows.join("")}</div>`;
-    row.classList.remove("hasSourceFocus", "hasCorrelationFocus");
-    delete row.dataset.activeSource;
-    delete row.dataset.activeCorrelation;
-    row.classList.add("isLoaded");
-    const state = row.querySelector(".moduleStreamState");
-    if (state) state.textContent = "已加载";
-  }
-
-  function updateLoadProgress() {
-    if (!runMeta) return;
-    const loadedCount = detailCache.size;
-    const renderedCount = renderedModules.size;
-    const progress = moduleTotal ? `${loadedCount}/${moduleTotal} 数据` : "正在建立模块目录";
-    const dataDone = streamComplete && loadedCount >= moduleTotal;
-    const renderDone = renderedCount >= moduleTotal;
-    const state = dataDone && renderDone ? "全部加载完成" : dataDone ? `正在展示 ${renderedCount}/${moduleTotal}` : "正在加载全部数据";
-    runSummary.innerHTML = `<strong>${progress}</strong><span>${Number(runMeta.summary?.groupCount || 0)} 个流程分组</span><span>卡片内容 ${Number(runMeta.summary?.cardCharacters || 0).toLocaleString("zh-CN")} 字</span><em>${state}</em>`;
-  }
-
-  function drainRenderQueue() {
-    renderScheduled = false;
-    const started = performance.now();
-    while (renderQueue.length && performance.now() - started < 10) {
-      const { moduleId, detail } = renderQueue.shift();
-      const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(moduleId)}"]`);
-      if (row) {
-        renderModuleDetail(row, detail);
-        renderedModules.add(moduleId);
-      }
-    }
-    updateLoadProgress();
-    if (renderQueue.length) scheduleRenderQueue();
-  }
-
-  function scheduleRenderQueue() {
-    if (renderScheduled) return;
-    renderScheduled = true;
-    requestAnimationFrame(drainRenderQueue);
-  }
-
-  function enqueueDetail(moduleId, detail) {
-    detailCache.set(moduleId, detail);
-    renderQueue.push({ moduleId, detail });
-    updateLoadProgress();
-    scheduleRenderQueue();
-  }
-
-  function refreshLoadedModules() {
-    const compact = onlyUsedFields?.checked !== false;
-    if (readColumnTitle) readColumnTitle.textContent = compact ? "字段来源与读取" : "模块实际读取";
-    if (contextColumnTitle) contextColumnTitle.textContent = compact ? "原始字段与记录" : "原始记录与上下文";
-    const entries = [...detailCache.entries()];
-    let cursor = 0;
-    const renderBatch = () => {
-      const limit = Math.min(cursor + 3, entries.length);
-      for (; cursor < limit; cursor += 1) {
-        const [moduleId, detail] = entries[cursor];
-        const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(moduleId)}"]`);
-        if (row) renderModuleDetail(row, detail);
-      }
-      if (cursor < entries.length) requestAnimationFrame(renderBatch);
-    };
-    requestAnimationFrame(renderBatch);
-  }
-
-  function locateOriginalField(target) {
-    const row = target.closest(".moduleRow");
-    if (!row) return;
-    const readId = target.dataset.locateReadId;
-    const match = readId
-      ? row.querySelector(`[data-column="context"] [data-read-ids~="${CSS.escape(readId)}"]`)
-      : null;
-    if (!match) return;
-    const correlationId = target.dataset.locateCorrelation;
-    if (correlationId) activateCorrelation(row, correlationId, target);
-    match.classList.remove("isLocated");
-    void match.offsetWidth;
-    match.classList.add("isLocated");
-    match.scrollIntoView({ behavior: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "center", inline: "nearest" });
-    match.focus({ preventScroll: true });
-    window.setTimeout(() => match.classList.remove("isLocated"), 1800);
-  }
-
-  function setSourceFocus(target) {
-    const row = target.closest(".moduleRow");
-    if (!row) return;
-    const key = target.dataset.sourceKey;
-    const alreadyActive = row.dataset.activeSource === key;
-    row.querySelectorAll(".sourceTag").forEach((tag) => tag.setAttribute("aria-pressed", "false"));
-    row.querySelectorAll(".contentBox").forEach((box) => box.classList.remove("isSourceMatch"));
-    row.classList.remove("hasSourceFocus");
-    delete row.dataset.activeSource;
-    if (alreadyActive || !key) return;
-    row.dataset.activeSource = key;
-    row.classList.add("hasSourceFocus");
-    row.querySelectorAll(".sourceTag").forEach((tag) => {
-      if (tag.dataset.sourceKey === key) tag.setAttribute("aria-pressed", "true");
-    });
-    row.querySelectorAll(".contentBox").forEach((box) => {
-      const keys = (box.dataset.sourceKeys || "").split(" ");
-      if (keys.includes(key)) box.classList.add("isSourceMatch");
-    });
-  }
-
-  function activateCorrelation(row, id, colorTarget) {
-    const alreadyActive = row.dataset.activeCorrelation === id;
-    row.querySelectorAll("[data-correlation-id], [data-correlation-ids]").forEach((item) => {
-      item.classList.remove("isCorrelationMatch");
-      item.setAttribute("aria-pressed", "false");
-    });
-    row.classList.remove("hasCorrelationFocus");
-    delete row.dataset.activeCorrelation;
-    row.style.removeProperty("--active-correlation-color");
-    if (alreadyActive || !id) return;
-    row.dataset.activeCorrelation = id;
-    row.classList.add("hasCorrelationFocus");
-    const activeColor = getComputedStyle(colorTarget).getPropertyValue("--correlation-color").trim();
-    if (activeColor) row.style.setProperty("--active-correlation-color", activeColor);
-    row.querySelectorAll(`[data-correlation-id="${CSS.escape(id)}"], [data-correlation-ids~="${CSS.escape(id)}"]`).forEach((item) => {
-      item.classList.add("isCorrelationMatch");
-      item.setAttribute("aria-pressed", "true");
-      const details = item.closest("details");
-      if (details) details.open = true;
-    });
-  }
-
-  function setCorrelationFocus(target) {
-    const row = target.closest(".moduleRow");
-    if (!row) return;
-    activateCorrelation(row, target.dataset.correlationId, target);
-  }
-
-  root.addEventListener("click", (event) => {
-    const locateTarget = event.target.closest("[data-locate-read-id]");
-    if (locateTarget) {
-      event.preventDefault();
-      event.stopPropagation();
-      locateOriginalField(locateTarget);
-      return;
-    }
-    const correlationTarget = event.target.closest("[data-correlation-id]");
-    if (correlationTarget) {
-      event.preventDefault();
-      event.stopPropagation();
-      setCorrelationFocus(correlationTarget);
-      return;
-    }
-    const target = event.target.closest(".sourceTag");
-    if (!target) return;
-    event.preventDefault();
-    event.stopPropagation();
-    setSourceFocus(target);
-  });
-  root.addEventListener("keydown", (event) => {
-    if (event.key !== "Enter" && event.key !== " ") return;
-    const locateTarget = event.target.closest("[data-locate-read-id]");
-    if (locateTarget) {
-      event.preventDefault();
-      event.stopPropagation();
-      locateOriginalField(locateTarget);
-      return;
-    }
-    const correlationTarget = event.target.closest("[data-correlation-id]");
-    if (correlationTarget) {
-      event.preventDefault();
-      event.stopPropagation();
-      setCorrelationFocus(correlationTarget);
-      return;
-    }
-    const target = event.target.closest(".sourceTag");
-    if (!target) return;
-    event.preventDefault();
-    event.stopPropagation();
-    setSourceFocus(target);
-  });
-
-  moduleJump?.addEventListener("change", () => jumpToModule(moduleJump.value));
-  onlyUsedFields?.addEventListener("change", refreshLoadedModules);
-  sourceFilter?.addEventListener("change", refreshLoadedModules);
-
-  function initializeIndex(data) {
-    runMeta = data;
-    const modules = data.modules || [];
-    moduleTotal = modules.length;
-    updateLoadProgress();
-    const groups = new Map();
-    modules.forEach((module) => { if (!groups.has(module.group)) groups.set(module.group, []); groups.get(module.group).push(module); });
-    root.innerHTML = [...groups.entries()].map(([group, items]) => `<section class="auditGroup"><header class="groupHeader"><h2>${escapeHtml(group)}</h2><span>${items.length} 个模块</span></header>${items.map(moduleRow).join("")}</section>`).join("") || `<p class="fatal">这个 Run 没有可展示的模块。</p>`;
-    populateModuleJump(modules);
-    const initialModule = new URLSearchParams(location.hash.slice(1)).get("module");
-    if (initialModule) {
-      moduleJump.value = initialModule;
-      jumpToModule(initialModule, { instant: true, updateHash: false });
-    }
-  }
-
-  function handleStreamMessage(message) {
-    if (message.type === "index") {
-      initializeIndex(message.data || {});
-      return;
-    }
-    if (message.type === "detail") {
-      enqueueDetail(String(message.moduleId || message.data?.id || ""), message.data || {});
-      return;
-    }
-    if (message.type === "error") {
-      const row = document.querySelector(`.moduleRow[data-module-id="${CSS.escape(String(message.moduleId || ""))}"]`);
-      const target = row?.querySelector(".moduleDeferred");
-      if (target) target.innerHTML = `<p class="loadError">读取失败:${escapeHtml(message.message || "未知错误")}</p>`;
-      const state = row?.querySelector(".moduleStreamState");
-      if (state) state.textContent = "读取失败";
-      return;
-    }
-    if (message.type === "complete") {
-      streamComplete = true;
-      updateLoadProgress();
-    }
-  }
-
-  function consumeLines(text, carry = "") {
-    const lines = `${carry}${text}`.split("\n");
-    const nextCarry = lines.pop() || "";
-    lines.forEach((line) => {
-      if (!line.trim()) return;
-      handleStreamMessage(JSON.parse(line));
-    });
-    return nextCarry;
-  }
-
-  async function start() {
-    try {
-      const response = await fetch(`/api/script-builds/${encodeURIComponent(runId)}/module-audit-stream`, { headers: { Accept: "application/x-ndjson" }, cache: "no-store" });
-      if (!response.ok) throw new Error(`API ${response.status}`);
-      if (!response.body) {
-        consumeLines(await response.text());
-        return;
-      }
-      const reader = response.body.getReader();
-      const decoder = new TextDecoder();
-      let carry = "";
-      while (true) {
-        const { value, done } = await reader.read();
-        if (done) break;
-        carry = consumeLines(decoder.decode(value, { stream: true }), carry);
-      }
-      carry = consumeLines(decoder.decode(), carry);
-      if (carry.trim()) handleStreamMessage(JSON.parse(carry));
-    } catch (error) {
-      runSummary.textContent = "读取失败";
-      root.innerHTML = `<div class="fatal"><h2>无法读取模块数据对照</h2><p>${escapeHtml(error instanceof Error ? error.message : String(error))}</p></div>`;
-    }
-  }
-
-  start();
-})();

+ 0 - 50
visualization/backend/app/module_audit/static/index.html

@@ -1,50 +0,0 @@
-<!doctype html>
-<html lang="zh-CN">
-<head>
-  <meta charset="utf-8" />
-  <meta name="viewport" content="width=device-width, initial-scale=1" />
-  <title>Run #__RUN_ID__ · 模块数据对照</title>
-  <link rel="stylesheet" href="/audit-assets/styles.css" />
-</head>
-<body data-run-id="__RUN_ID__">
-  <header class="auditHeader">
-    <div class="headerIntro">
-      <span class="eyeline">脚本构建 #__RUN_ID__</span>
-      <h1>模块数据对照</h1>
-      <p>对齐模块实际使用、模块实际读取和原始记录上下文。</p>
-    </div>
-    <div class="runSummary" id="run-summary" aria-live="polite">正在读取真实运行数据…</div>
-  </header>
-  <nav class="auditToolbar" aria-label="审计页面工具栏">
-    <strong class="toolbarRun">Run #__RUN_ID__</strong>
-    <label class="moduleJump" for="module-jump">
-      <span>卡片</span>
-      <select id="module-jump" disabled>
-        <option value="">正在读取卡片列表…</option>
-      </select>
-    </label>
-    <label class="compactToggle" for="only-used-fields">
-      <input id="only-used-fields" type="checkbox" checked />
-      <span>只看使用字段</span>
-    </label>
-    <label class="sourceFilter" for="source-filter">
-      <span>来源</span>
-      <select id="source-filter">
-        <option value="all">全部</option>
-        <option value="database">数据库</option>
-        <option value="function">函数</option>
-        <option value="log">日志</option>
-      </select>
-    </label>
-  </nav>
-  <main>
-    <div class="columnHeader" aria-hidden="true">
-      <strong>模块实际使用</strong>
-      <strong id="read-column-title">字段来源与读取</strong>
-      <strong id="context-column-title">原始字段与记录</strong>
-    </div>
-    <div id="audit-root"><div class="loadingBlock"><i></i><i></i><i></i></div></div>
-  </main>
-  <script src="/audit-assets/app.js" defer></script>
-</body>
-</html>

+ 0 - 277
visualization/backend/app/module_audit/static/styles.css

@@ -1,277 +0,0 @@
-:root {
-  color-scheme: dark;
-  --bg: #101114;
-  --canvas: #15171b;
-  --surface: #202228;
-  --raised: #292c33;
-  --border: #383c45;
-  --text: #f2f3f5;
-  --muted: #adb2bd;
-  --subtle: #777e8c;
-  --accent: #ff6d5a;
-  --read: #62a8ff;
-  --success: #63c883;
-  --warning: #f4c152;
-  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-  background: var(--bg);
-  color: var(--text);
-}
-* { box-sizing: border-box; }
-body { margin: 0; min-height: 100vh; background-color: var(--bg); background-image: radial-gradient(circle at 1px 1px, #2b2e35 1px, transparent 0); background-size: 22px 22px; }
-button, summary { font: inherit; }
-.auditHeader { min-height: 116px; padding: 24px 30px; display: flex; align-items: flex-end; justify-content: space-between; gap: 28px; background: rgba(21, 23, 27, .97); border-bottom: 1px solid var(--border); }
-.headerIntro { min-width: 0; }
-.auditHeader h1 { margin: 4px 0 6px; font-size: 24px; line-height: 1.25; }
-.auditHeader p { margin: 0; color: var(--muted); font-size: 14px; }
-.eyeline { color: var(--accent); font-size: 13px; font-weight: 700; }
-.auditToolbar { position: sticky; top: 0; z-index: 30; min-height: 52px; padding: 8px 18px; display: flex; align-items: center; gap: 12px 18px; background: rgba(25, 27, 32, .98); border-bottom: 1px solid var(--border); box-shadow: 0 8px 20px rgba(0,0,0,.20); }
-.toolbarRun { flex: none; color: #dfe2e7; font-size: 13px; }
-.moduleJump, .sourceFilter { display: flex; align-items: center; gap: 8px; color: #c7cbd3; font-size: 12px; font-weight: 700; }
-.moduleJump > span, .sourceFilter > span { flex: none; white-space: nowrap; }
-.moduleJump { min-width: 300px; flex: 1 1 520px; max-width: 620px; }
-.moduleJump select { width: 100%; }
-.moduleJump select, .sourceFilter select { height: 34px; padding: 0 9px; color: var(--text); background: var(--surface); border: 1px solid #4a4f5b; border-radius: 7px; font: 12px/1.2 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
-.sourceFilter select { min-width: 94px; }
-.moduleJump select:hover:not(:disabled), .sourceFilter select:hover { border-color: #666d7b; background: #252830; }
-.moduleJump select:focus-visible, .sourceFilter select:focus-visible { outline: 2px solid var(--read); outline-offset: 2px; border-color: var(--read); }
-.moduleJump select:disabled { cursor: wait; color: var(--subtle); opacity: .72; }
-.compactToggle { flex: none; min-height: 34px; padding: 0 10px; display: inline-flex; align-items: center; gap: 7px; color: #e3e6eb; background: #24272d; border: 1px solid #484d58; border-radius: 7px; cursor: pointer; font-size: 12px; font-weight: 700; }
-.compactToggle:hover { background: #2a2e36; border-color: #626977; }
-.compactToggle:focus-within { outline: 2px solid var(--read); outline-offset: 2px; }
-.compactToggle input { width: 15px; height: 15px; margin: 0; accent-color: var(--read); }
-.runSummary { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 8px 14px; color: var(--muted); font-size: 13px; }
-.runSummary strong { color: var(--text); font-size: 15px; }
-.runSummary em { color: var(--success); font-style: normal; }
-main { width: 100%; min-width: 1080px; }
-.columnHeader { position: sticky; top: 52px; z-index: 20; display: grid; grid-template-columns: minmax(320px, .92fr) minmax(360px, 1fr) minmax(430px, 1.18fr); background: #191b20; border-bottom: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.22); }
-.columnHeader strong { padding: 14px 18px; color: #d8dbe1; font-size: 13px; }
-.columnHeader strong + strong { border-left: 1px solid var(--border); }
-.auditGroup { padding-bottom: 24px; }
-.groupHeader { display: flex; align-items: center; gap: 12px; padding: 22px 30px 10px; }
-.groupHeader h2 { margin: 0; font-size: 18px; }
-.groupHeader span { color: var(--subtle); font-size: 12px; }
-.moduleRow { scroll-margin-top: 108px; margin: 0 18px 16px; background: var(--canvas); border: 1px solid var(--border); border-radius: 10px; overflow: clip; contain: layout paint style; box-shadow: 0 10px 30px rgba(0,0,0,.14); transition: border-color 180ms ease-out, box-shadow 180ms ease-out; }
-.moduleRow:focus { outline: none; }
-.moduleRow.isJumpTarget { border-color: var(--read); box-shadow: 0 0 0 2px rgba(98, 168, 255, .28), 0 14px 36px rgba(0,0,0,.24); }
-.moduleRow > header { min-height: 66px; padding: 13px 18px; display: flex; justify-content: space-between; align-items: center; gap: 20px; background: #1c1e23; border-bottom: 1px solid var(--border); }
-.moduleRow > header span { display: block; color: var(--subtle); font-size: 12px; }
-.moduleRow > header h3 { margin: 3px 0 0; font-size: 16px; }
-.moduleMeta { display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
-.moduleMeta code, .readMeta code { color: #c7cbd3; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
-.moduleStreamState { min-height: 23px; padding: 3px 7px; display: inline-flex !important; align-items: center; border: 1px solid #48505d; border-radius: 999px; color: #cbd5e2 !important; background: #252932; font-size: 10px !important; }
-.moduleRow.isLoaded .moduleStreamState { color: #9be2b1 !important; border-color: #315d3f; background: #1e3527; }
-.moduleLoadButton, .locateContextButton { border: 1px solid #555b67; border-radius: 6px; color: #dfe3e9; background: #272a31; cursor: pointer; font-weight: 700; }
-.moduleLoadButton { min-height: 30px; padding: 4px 9px; font-size: 11px; }
-.moduleLoadButton:hover:not(:disabled), .locateContextButton:hover { color: #fff; border-color: #748093; background: #30343d; }
-.moduleLoadButton:focus-visible, .locateContextButton:focus-visible { outline: 2px solid var(--read); outline-offset: 2px; }
-.moduleLoadButton:disabled { cursor: wait; opacity: .64; }
-.moduleRow.isLoaded .moduleLoadButton { display: none; }
-.moduleDeferred { min-height: 48px; padding: 13px 18px; display: flex; align-items: center; color: var(--subtle); font-size: 12px; }
-.moduleDeferred .cellLoading { width: min(520px, 100%); padding: 0; }
-.variant { padding: 3px 7px; border: 1px solid #555b67; border-radius: 999px; color: #cbd0d9 !important; }
-.moduleColumns { display: grid; grid-template-columns: minmax(320px, .92fr) minmax(360px, 1fr) minmax(430px, 1.18fr); align-items: start; }
-.moduleColumns > section { min-width: 0; padding: 14px; }
-.moduleColumns > section + section { border-left: 1px solid var(--border); }
-.correspondenceRows { display: block; }
-.correspondenceRow { display: grid; grid-template-columns: minmax(320px, .92fr) minmax(360px, 1fr) minmax(430px, 1.18fr); align-items: start; }
-.correspondenceRow + .correspondenceRow { border-top: 1px solid #464b56; }
-.correspondenceCell { min-width: 0; padding: 14px; }
-.correspondenceCell + .correspondenceCell { border-left: 1px solid var(--border); }
-.columnLabel { display: none; margin: 0 0 10px; color: var(--muted); font-size: 12px; font-weight: 700; }
-.contentBox { margin-bottom: 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
-.contentBox:last-child { margin-bottom: 0; }
-.contentBox > summary { cursor: pointer; list-style: none; padding: 11px 12px; display: flex; flex-direction: column; gap: 7px; color: var(--text); }
-.contentBox > summary::-webkit-details-marker { display: none; }
-.contentBox > summary > .summaryLine { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
-.contentBox > summary b { font-size: 13px; }
-.contentBox > summary b::before { content: "›"; display: inline-block; width: 16px; color: var(--muted); font-size: 19px; line-height: 10px; transform: rotate(0); transform-origin: center; transition: transform 160ms ease-out; }
-.contentBox[open] > summary b::before { transform: rotate(90deg); }
-.contentBox > summary small { flex: none; color: var(--subtle); font-size: 11px; font-weight: 500; }
-.contentBox > summary em { color: #c2c6ce; font-size: 12px; font-style: normal; line-height: 1.55; }
-.contentBody { padding: 0 12px 13px; border-top: 1px solid #30333b; }
-.contentBody p, .contentBody pre { margin: 10px 0 0; color: #dfe1e5; font-size: 13px; line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; }
-.contentBody pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
-.tone-used { border-color: #66433e; }
-.tone-used > summary b { color: #ff8d7e; }
-.tone-read { border-color: #344f70; }
-.tone-read > summary b { color: #8fc0ff; }
-.tone-database { border-color: #3a5272; }
-.tone-database > summary b { color: #9bc8ff; }
-.tone-function { border-color: #38605d; }
-.tone-function > summary b { color: #8fd8cf; }
-.tone-log { border-color: #655735; }
-.tone-log > summary b { color: #f1ce7a; }
-.tone-context { background: #1d1f24; }
-.tone-muted { opacity: .82; }
-.tone-warning { border-color: #695a30; }
-.displayCard { padding: 12px 0 2px; }
-.displayCard h4 { margin: 0 0 12px; font-size: 16px; }
-.displayField { display: grid; grid-template-columns: 92px minmax(0,1fr); gap: 10px; padding: 9px 0; border-top: 1px solid #343740; }
-.displayField b { color: var(--muted); font-size: 12px; }
-.displayField p, .displayField pre { margin: 0; }
-.businessDetailBody { padding-top: 2px; }
-.businessPart { padding: 13px 0; border-top: 1px solid #343740; }
-.businessPart:first-child { border-top: 0; }
-.businessPart h4 { margin: 0 0 8px; color: #f1a196; font-size: 14px; line-height: 1.4; }
-.businessPart p, .businessPart pre { margin: 0; }
-.technicalDetailBody { padding-top: 2px; }
-.technicalPart { padding: 12px 0; border-top: 1px solid #343740; }
-.technicalPart:first-child { border-top: 0; }
-.technicalPart h4 { margin: 0 0 7px; color: #9bc8ff; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
-.technicalPart p, .technicalPart pre { margin: 0; }
-.correspondenceTarget { margin-inline: -8px; padding-inline: 9px; border-left: 3px solid var(--correlation-color); border-radius: 5px; background: var(--correlation-bg); cursor: pointer; transition: opacity 160ms ease-out, box-shadow 160ms ease-out, background 160ms ease-out; }
-.correspondenceTarget:focus-visible, .correlationChip:focus-visible, .correlationMark:focus-visible { outline: 2px solid var(--correlation-color); outline-offset: 2px; }
-.correlationNumber { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 18px; margin-right: 7px; border-radius: 4px; color: #101114; background: var(--correlation-color); font-size: 10px; font-weight: 800; vertical-align: 1px; }
-.correlationLinks { display: flex; flex-wrap: wrap; gap: 6px; }
-.correlationChip { display: inline-flex; align-items: center; gap: 5px; min-height: 26px; padding: 3px 8px; border: 1px solid var(--correlation-color); border-radius: 999px; color: var(--correlation-color); background: var(--correlation-bg); cursor: pointer; font-size: 10px; font-weight: 650; }
-.correlationChip b { color: inherit !important; font-size: 10px !important; }
-.correlationChip b::before { content: none !important; }
-.correlationMark { padding: 1px 3px; color: #fff; background: var(--correlation-bg-strong); border-bottom: 2px solid var(--correlation-color); border-radius: 3px; cursor: pointer; }
-.correlation-0 { --correlation-color: #ff8c7c; --correlation-bg: rgba(255, 140, 124, .10); --correlation-bg-strong: rgba(255, 140, 124, .34); }
-.correlation-1 { --correlation-color: #78b8ff; --correlation-bg: rgba(120, 184, 255, .10); --correlation-bg-strong: rgba(120, 184, 255, .32); }
-.correlation-2 { --correlation-color: #70d7aa; --correlation-bg: rgba(112, 215, 170, .10); --correlation-bg-strong: rgba(112, 215, 170, .30); }
-.correlation-3 { --correlation-color: #d5a2ff; --correlation-bg: rgba(213, 162, 255, .10); --correlation-bg-strong: rgba(213, 162, 255, .30); }
-.correlation-4 { --correlation-color: #f0ca67; --correlation-bg: rgba(240, 202, 103, .10); --correlation-bg-strong: rgba(240, 202, 103, .30); }
-.correlation-5 { --correlation-color: #75d8e8; --correlation-bg: rgba(117, 216, 232, .10); --correlation-bg-strong: rgba(117, 216, 232, .30); }
-.correlation-6 { --correlation-color: #ff9dcb; --correlation-bg: rgba(255, 157, 203, .10); --correlation-bg-strong: rgba(255, 157, 203, .30); }
-.correlation-7 { --correlation-color: #add574; --correlation-bg: rgba(173, 213, 116, .10); --correlation-bg-strong: rgba(173, 213, 116, .30); }
-.isCompact [data-column="use"] .sourceLinks { display: none; }
-.compactPanel { overflow: hidden; background: #1c1e23; border: 1px solid var(--border); border-radius: 8px; }
-.compactPanel > header { min-height: 42px; padding: 9px 12px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #343740; }
-.compactPanel > header h4 { margin: 0; color: #e8eaee; font-size: 13px; }
-.compactPanel > header span { color: var(--subtle); font-size: 11px; }
-.lineageItem { margin: 0; padding: 11px 12px; background: transparent; border: 0; border-top: 1px solid #31343c; border-radius: 0; }
-.lineageItem:first-of-type { border-top: 0; }
-.lineageItem.correspondenceTarget { margin: 0; padding: 11px 12px; border-left: 0; border-radius: 0; background: var(--correlation-bg); }
-.lineageItem > header { display: flex; align-items: center; gap: 7px; }
-.lineageItem > header strong { color: #eef0f3; font-size: 12px; }
-.lineageItem > header em { margin-left: auto; color: var(--correlation-color, var(--subtle)); font-size: 10px; font-style: normal; }
-.lineageSources { padding-top: 8px; display: grid; gap: 7px; }
-.lineageSource { min-width: 0; display: grid; gap: 5px; }
-.lineageSource small { color: var(--subtle); font-size: 11px; line-height: 1.45; }
-.lineageSourceSummary { grid-template-columns: max-content minmax(0, 1fr); align-items: start; column-gap: 8px; }
-.lineageSource + .lineageSource { padding-top: 9px; border-top: 1px solid #333741; }
-.lineageSourceHeader { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
-.locateContextButton { flex: none; min-height: 27px; padding: 3px 8px; font-size: 10px; }
-.sourceKindPill { display: inline-flex; width: fit-content; align-items: center; min-height: 22px; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; font-size: 10px; font-weight: 700; }
-.pathList { display: flex; flex-wrap: wrap; gap: 5px; }
-.pathList code { max-width: 100%; padding: 3px 6px; color: #dce7f7; background: #2a3039; border: 1px solid #3b4655; border-radius: 4px; font: 10px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
-.sourceDigest { padding: 11px 12px; border-top: 1px solid #31343c; }
-.sourceDigest:first-of-type { border-top: 0; }
-.sourceDigest > header { display: flex; align-items: center; gap: 7px; }
-.sourceDigest > header strong { min-width: 0; color: #eef0f3; font-size: 12px; overflow-wrap: anywhere; }
-.sourceDigest > header em { margin-left: auto; flex: none; max-width: 44%; color: var(--subtle); font-size: 10px; font-style: normal; text-align: right; overflow-wrap: anywhere; }
-.sourceDigest > p { margin: 8px 0 0; color: #c5c9d1; font-size: 11px; line-height: 1.5; }
-.sourceDigest.isLocated { outline: 2px solid var(--active-correlation-color, var(--read)); outline-offset: -2px; animation: locate-pulse 680ms ease-out 2; }
-.rawField.isLocated { outline: 2px solid var(--active-correlation-color, var(--read)); outline-offset: 2px; border-radius: 5px; animation: locate-pulse 680ms ease-out 2; }
-.readValueEntry { padding: 8px 0 2px; }
-.readValueEntry + .readValueEntry { margin-top: 5px; border-top: 1px solid #343842; }
-.readValueMeta { padding-bottom: 6px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 5px 9px; }
-.readValueMeta > span { color: #cdd2da; font-size: 11px; font-weight: 700; }
-.readValueMeta code, .recordLocation code, .rawField code { color: #dce7f7; font: 10px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
-.fieldActualValue { display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 8px; padding: 7px 9px; background: rgba(255,255,255,.025); border-radius: 5px; }
-.fieldActualValue > b { color: var(--muted); font-size: 10px; }
-.fieldActualValue p, .fieldActualValue pre { margin: 0; color: #eef0f3; font-size: 11px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
-.valueDisclosure { background: rgba(255,255,255,.025); border-radius: 5px; }
-.valueDisclosure > summary { min-height: 34px; padding: 7px 9px; display: grid; grid-template-columns: max-content 1fr; gap: 3px 9px; cursor: pointer; list-style: none; }
-.valueDisclosure > summary::-webkit-details-marker { display: none; }
-.valueDisclosure > summary > span { color: var(--muted); font-size: 10px; font-weight: 700; }
-.valueDisclosure > summary > span::before { content: "›"; display: inline-block; width: 13px; font-size: 16px; line-height: 9px; transform-origin: center; transition: transform 160ms ease-out; }
-.valueDisclosure[open] > summary > span::before { transform: rotate(90deg); }
-.valueDisclosure > summary > small { justify-self: end; color: var(--subtle); font-size: 10px; }
-.valueDisclosure > summary > em { grid-column: 1 / -1; color: #cfd3db; font-size: 11px; font-style: normal; line-height: 1.5; }
-.valueDisclosure .valueMatch, .fieldActualValue .valueMatch { padding: 1px 2px; color: #fff; background: rgba(255, 109, 90, .42); border-radius: 2px; }
-.valueDisclosure[open] > summary > em { display: none; }
-.valueDisclosureBody { padding: 8px 9px 10px; border-top: 1px solid #363a43; }
-.valueDisclosureBody p, .valueDisclosureBody pre { margin: 0; color: #eef0f3; font-size: 11px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
-.moreFieldsNotice { display: block; padding-top: 7px; }
-.lineageUnproven { display: block; padding: 7px 9px 2px; color: var(--subtle); font-size: 10px; line-height: 1.45; }
-.recordLocation { margin: 9px 0 2px; display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
-.recordLocation > span { margin-right: 3px; color: var(--muted); font-size: 10px; font-weight: 700; }
-.recordLocation code { padding: 3px 5px; background: #292d35; border-radius: 4px; }
-.rawFields { padding-top: 7px; }
-.rawField { padding: 9px 0; border-top: 1px solid #343842; }
-.rawField > header { padding-bottom: 6px; display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
-.rawField > header > div { min-width: 0; display: grid; gap: 3px; }
-.rawField > header span { color: #dfe2e7; font-size: 11px; font-weight: 700; }
-.rawField .correlationLinks { justify-content: flex-end; }
-.lineageRelation { flex: none; margin-left: auto; color: #8fc0ff; font-size: 10px; font-style: normal; }
-.exactFieldMap { margin: 10px 0 14px; padding: 10px 11px; border: 1px solid #3a526d; border-radius: 7px; background: rgba(98,168,255,.045); }
-.exactFieldMap > h4 { margin: 0 0 7px; color: #9bc8ff; font-size: 12px; }
-.digestFields { padding-top: 8px; display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 7px; align-items: start; }
-.digestFields > b { color: var(--muted); font-size: 10px; line-height: 1.5; }
-.digestFields > span { color: var(--subtle); font-size: 11px; line-height: 1.45; }
-.compactEmpty { min-height: 78px; padding: 18px; display: flex; align-items: center; justify-content: center; color: var(--subtle); background: #1c1e23; border: 1px dashed #454a55; border-radius: 8px; font-size: 12px; text-align: center; }
-.sourceLinks { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 6px; }
-.sourceTag, .usageTag { display: inline-flex; align-items: center; min-height: 28px; padding: 4px 9px; border-radius: 999px; font-size: 11px; line-height: 1.2; }
-.sourceTag { cursor: pointer; border: 1px solid currentColor; font-weight: 700; transition: background 160ms ease-out, color 160ms ease-out, opacity 160ms ease-out; }
-.sourceTag:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
-.source-database { color: #9bc8ff; background: rgba(98, 168, 255, .09); }
-.source-function { color: #8fd8cf; background: rgba(82, 190, 176, .09); }
-.source-log { color: #f1ce7a; background: rgba(241, 206, 122, .09); }
-.source-unknown { color: #c6cad2; background: rgba(198, 202, 210, .08); }
-.source-database[aria-pressed="true"] { color: #101114; background: #9bc8ff; }
-.source-function[aria-pressed="true"] { color: #101114; background: #8fd8cf; }
-.source-log[aria-pressed="true"] { color: #101114; background: #f1ce7a; }
-.source-unknown[aria-pressed="true"] { color: #101114; background: #c6cad2; }
-.usageTag { color: #c7cbd3; background: #30333a; }
-.readItem { padding: 11px 0; border-top: 1px solid #343740; }
-.readItem:first-child { border-top: 0; }
-.readItemHeader { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 12px; }
-.readItemHeader b { color: #e7e9ed; font-size: 13px; }
-.readItemHeader code { color: var(--muted); font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
-.readMeta { display: flex; flex-wrap: wrap; align-items: center; gap: 7px 10px; padding-top: 8px; }
-.readMeta > span:not(.readStatus) { color: var(--subtle); font-size: 11px; }
-.readStatus { padding: 3px 7px; border-radius: 999px; color: #dbeeff; background: #29445f; font-size: 11px; }
-.status-fallback { color: #d9dce2; background: #424751; }
-.status-calculation-input { color: #fff0bc; background: #5e4e20; }
-.status-unconfirmed, .status-missing { color: #ffe1d4; background: #603528; }
-.contextTree { padding-top: 10px; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
-.contextTree dl { margin: 0; }
-.contextTree ol { margin: 4px 0; padding-left: 22px; }
-.contextEntry { display: grid; grid-template-columns: minmax(128px, .32fr) minmax(0, 1fr); gap: 10px; padding: 4px 0; }
-.contextEntry dt { color: #aeb4c0; overflow-wrap: anywhere; }
-.contextEntry dd { margin: 0; color: #d9dce3; overflow-wrap: anywhere; }
-.contextEntry.isDim { opacity: .34; }
-.contextEntry.isRead { padding: 5px 7px; margin: 2px -7px; border-radius: 5px; background: rgba(98, 168, 255, .09); opacity: 1; }
-.contextEntry.hasCorrelationData { opacity: 1; }
-mark { padding: 1px 2px; color: #fff; background: rgba(255, 109, 90, .48); border-radius: 2px; }
-.moduleRow .contentBox { transition: opacity 160ms ease-out, border-color 160ms ease-out, box-shadow 160ms ease-out; }
-.moduleRow.hasSourceFocus .contentBox { opacity: .3; }
-.moduleRow.hasSourceFocus .contentBox.isSourceMatch { opacity: 1; box-shadow: 0 0 0 1px rgba(242, 243, 245, .2); }
-.moduleRow.hasCorrelationFocus [data-correlation-id], .moduleRow.hasCorrelationFocus [data-correlation-ids] { opacity: .18; }
-.moduleRow.hasCorrelationFocus [data-correlation-id].isCorrelationMatch, .moduleRow.hasCorrelationFocus [data-correlation-ids].isCorrelationMatch { opacity: 1; box-shadow: 0 0 0 2px var(--active-correlation-color, var(--correlation-color)); }
-.empty { color: var(--subtle) !important; }
-.loadError, .fatal { margin: 18px; padding: 18px; color: #ffd8d2; background: #35211f; border: 1px solid #643a34; border-radius: 8px; }
-.loadingBlock, .cellLoading { padding: 18px; }
-.loadingBlock i, .cellLoading i { display: block; height: 12px; margin: 9px 0; border-radius: 4px; background: linear-gradient(90deg, #25282e, #333740, #25282e); background-size: 200% 100%; animation: loading 1.3s linear infinite; }
-.loadingBlock i:nth-child(2), .cellLoading i:nth-child(2) { width: 72%; }
-@keyframes loading { to { background-position: -200% 0; } }
-@keyframes locate-pulse { 50% { box-shadow: inset 0 0 0 2px var(--active-correlation-color, var(--read)); } }
-@media (prefers-reduced-motion: reduce) { *, *::before { animation: none !important; transition: none !important; } }
-@media (max-width: 900px) {
-  main { min-width: 0; }
-  .auditHeader { align-items: flex-start; flex-direction: column; padding: 20px; }
-  .headerIntro { width: 100%; }
-  .auditToolbar { padding: 8px 10px; flex-wrap: wrap; }
-  .toolbarRun { display: none; }
-  .moduleJump { min-width: 0; max-width: none; flex: 1 1 100%; }
-  .moduleJump select { width: 100%; }
-  .compactToggle { flex: 1 1 auto; justify-content: center; }
-  .sourceFilter { flex: 1 1 auto; }
-  .sourceFilter select { flex: 1; }
-  .runSummary { justify-content: flex-start; }
-  .columnHeader { display: none; }
-  .groupHeader { padding-inline: 14px; }
-  .moduleRow { scroll-margin-top: 104px; margin-inline: 10px; }
-  .moduleRow > header { align-items: flex-start; flex-direction: column; }
-  .moduleMeta { justify-content: flex-start; }
-  .lineageSourceHeader { align-items: flex-start; flex-direction: column; }
-  .locateContextButton { min-height: 34px; }
-  .fieldActualValue { grid-template-columns: 1fr; }
-  .moduleColumns { display: block; }
-  .moduleColumns > section + section { border-left: 0; border-top: 1px solid var(--border); }
-  .correspondenceRow { display: block; }
-  .correspondenceCell + .correspondenceCell { border-left: 0; border-top: 1px solid var(--border); }
-  .columnLabel { display: block; }
-}

+ 0 - 72
visualization/backend/app/prompt_projection.py

@@ -1,72 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime, timezone
-from typing import Any
-
-from .sanitizer import sanitize
-
-
-_MAX_PROMPT_CHARS = 60_000
-
-
-def project_prompt(payload: dict[str, Any]) -> dict[str, Any]:
-    """Build a truthful, lazy prompt view.
-
-    Runtime events preserve the exact delegated task, but the application does
-    not persist a system-prompt snapshot for each invocation.  The current
-    prompt is therefore deliberately labelled as current configuration.
-    """
-
-    task = _text(payload.get("task"))
-    prompt = _text(payload.get("promptContent"))
-    truncated = bool(prompt and len(prompt) > _MAX_PROMPT_CHARS)
-    if truncated:
-        prompt = prompt[:_MAX_PROMPT_CHARS].rstrip() + "\n\n…(当前规则过长,已截断)"
-
-    result: dict[str, Any] = {
-        "promptRef": str(payload.get("promptRef") or ""),
-        "actor": payload.get("actor") or {"role": "main", "label": "主 Agent"},
-        "notices": [
-            {
-                "code": "PROMPT_NOT_SNAPSHOTTED",
-                "message": "系统没有保存这次运行所用提示词的历史快照;下方展示当前提示词。",
-            }
-        ],
-    }
-    if mode_notice := _text(payload.get("modeNotice")):
-        result["notices"].append(
-            {"code": "CURRENT_MODE_CONTEXT", "message": mode_notice}
-        )
-    if task:
-        result["exactTask"] = {
-            "content": sanitize(task, max_text=_MAX_PROMPT_CHARS),
-            "accuracy": "run-exact",
-        }
-    if prompt:
-        result["systemPrompt"] = {
-            "content": sanitize(prompt, max_text=_MAX_PROMPT_CHARS + 200),
-            "source": payload.get("promptSource") or "current-file",
-            "version": payload.get("promptVersion"),
-            "accuracy": "current-not-run-snapshot",
-            "capturedAt": datetime.now(timezone.utc).isoformat(),
-            "truncated": truncated,
-        }
-    else:
-        result["notices"].append(
-            {
-                "code": "PROMPT_UNAVAILABLE",
-                "message": "当前提示词暂时不可读取。",
-            }
-        )
-    if truncated:
-        result["notices"].append(
-            {"code": "PROMPT_TRUNCATED", "message": "当前提示词已按安全长度截断。"}
-        )
-    return sanitize(result, max_text=_MAX_PROMPT_CHARS + 400)
-
-
-def _text(value: Any) -> str | None:
-    if not isinstance(value, str):
-        return None
-    value = value.strip()
-    return value or None

+ 0 - 64
visualization/backend/app/providers.py

@@ -1,64 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Protocol
-
-from .repositories import ScriptBuildRepository
-
-
-class RuntimeDataProvider(Protocol):
-    mode: str
-
-    def list_builds(
-        self, *, limit: int = 30, status: str | None = None
-    ) -> list[dict[str, Any]]: ...
-
-    def load_bundle(self, script_build_id: int) -> dict[str, Any]: ...
-
-    def load_current_artifact(self, script_build_id: int) -> dict[str, Any]: ...
-
-    def load_event_detail(
-        self, script_build_id: int, event_id: int
-    ) -> dict[str, Any]: ...
-
-    def load_event_details(
-        self, script_build_id: int, event_ids: list[int]
-    ) -> dict[int, dict[str, Any]]: ...
-
-    def load_prompt_context(
-        self, script_build_id: int, prompt_ref: str
-    ) -> dict[str, Any]: ...
-
-
-class LocalDatabaseProvider:
-    """V8 production provider: direct, visualization-only, read-only DB access."""
-
-    mode = "local-db-read-only"
-
-    def __init__(self, repository: ScriptBuildRepository | None = None):
-        self.repository = repository or ScriptBuildRepository()
-
-    def list_builds(
-        self, *, limit: int = 30, status: str | None = None
-    ) -> list[dict[str, Any]]:
-        return self.repository.list_builds(limit=limit, status=status)
-
-    def load_bundle(self, script_build_id: int) -> dict[str, Any]:
-        return self.repository.load_bundle(script_build_id)
-
-    def load_current_artifact(self, script_build_id: int) -> dict[str, Any]:
-        return self.repository.load_current_artifact(script_build_id)
-
-    def load_event_detail(
-        self, script_build_id: int, event_id: int
-    ) -> dict[str, Any]:
-        return self.repository.load_event_detail(script_build_id, event_id)
-
-    def load_event_details(
-        self, script_build_id: int, event_ids: list[int]
-    ) -> dict[int, dict[str, Any]]:
-        return self.repository.load_event_details(script_build_id, event_ids)
-
-    def load_prompt_context(
-        self, script_build_id: int, prompt_ref: str
-    ) -> dict[str, Any]:
-        return self.repository.load_prompt_context(script_build_id, prompt_ref)

+ 0 - 797
visualization/backend/app/repositories.py

@@ -1,797 +0,0 @@
-from __future__ import annotations
-
-import json
-from datetime import date, datetime
-from collections.abc import Mapping
-from threading import Lock
-from typing import Any
-
-from sqlalchemy import MetaData, Table
-
-from .runtime_bridge import load_runtime_modules, new_session, runtime_dir
-from .runtime_tool_catalog import RETRIEVAL_AGENT_LABELS
-from .retrieval_detail_projection import (
-    QUERY_TOOL_NAMES,
-    summarize_retrieval_output,
-)
-from .sanitizer import sanitize
-
-
-_DETAIL_BODY_MAX = 240_000
-
-
-class BuildNotFound(LookupError):
-    pass
-
-
-class ActivityNotFound(LookupError):
-    pass
-
-
-def _json_value(value: Any) -> Any:
-    if isinstance(value, (datetime, date)):
-        return value.isoformat()
-    return sanitize(value)
-
-
-def row_dict(row: Any) -> dict[str, Any]:
-    if row is None:
-        return {}
-    if isinstance(row, Mapping):
-        return {str(key): _json_value(value) for key, value in row.items()}
-    mapping = getattr(row, "_mapping", None)
-    if mapping is not None:
-        return {str(key): _json_value(value) for key, value in mapping.items()}
-    return {
-        column.name: _json_value(getattr(row, column.name))
-        for column in row.__table__.columns
-    }
-
-
-class ScriptBuildRepository:
-    """The only database adapter used by V8.
-
-    The adapter is deliberately query-only.  Sessions come from a dedicated
-    MySQL read-only pool and this class never flushes, commits or mutates ORM
-    objects.
-    """
-
-    def __init__(self, session_factory=new_session):
-        self._session_factory = session_factory
-        self._metadata = MetaData()
-        self._reflected_tables: dict[str, Table] = {}
-        self._table_lock = Lock()
-
-    def list_builds(
-        self, *, limit: int = 30, status: str | None = None
-    ) -> list[dict[str, Any]]:
-        _, models = load_runtime_modules()
-        session = self._session_factory()
-        try:
-            query = session.query(models.ScriptBuildRecord).filter(
-                models.ScriptBuildRecord.is_deleted.is_(False)
-            )
-            if status:
-                query = query.filter(models.ScriptBuildRecord.status == status)
-            rows = (
-                query.order_by(models.ScriptBuildRecord.id.desc())
-                .limit(max(1, min(limit, 100)))
-                .all()
-            )
-            return [self._build_summary(row) for row in rows]
-        finally:
-            session.close()
-
-    def load_bundle(self, script_build_id: int) -> dict[str, Any]:
-        _, models = load_runtime_modules()
-        session = self._session_factory()
-        try:
-            record = (
-                session.query(models.ScriptBuildRecord)
-                .filter(models.ScriptBuildRecord.id == script_build_id)
-                .first()
-            )
-            if record is None:
-                raise BuildNotFound(f"未找到脚本构建 #{script_build_id}")
-
-            rounds = (
-                session.query(models.ScriptBuildRound)
-                .filter(models.ScriptBuildRound.script_build_id == script_build_id)
-                .order_by(models.ScriptBuildRound.round_index.asc())
-                .all()
-            )
-            branches = (
-                session.query(models.ScriptBuildBranch)
-                .filter(models.ScriptBuildBranch.script_build_id == script_build_id)
-                .order_by(
-                    models.ScriptBuildBranch.round_index.asc(),
-                    models.ScriptBuildBranch.branch_id.asc(),
-                )
-                .all()
-            )
-            data_decisions = (
-                session.query(models.ScriptBuildDataDecision)
-                .filter(
-                    models.ScriptBuildDataDecision.script_build_id == script_build_id,
-                    models.ScriptBuildDataDecision.branch_id > 0,
-                )
-                .order_by(
-                    models.ScriptBuildDataDecision.created_at.asc(),
-                    models.ScriptBuildDataDecision.id.asc(),
-                )
-                .all()
-            )
-            ignored_legacy_decisions = (
-                session.query(models.ScriptBuildDataDecision)
-                .filter(
-                    models.ScriptBuildDataDecision.script_build_id == script_build_id,
-                    models.ScriptBuildDataDecision.branch_id <= 0,
-                )
-                .count()
-            )
-            multipath_decisions = (
-                session.query(models.ScriptBuildMultipathDecision)
-                .filter(
-                    models.ScriptBuildMultipathDecision.script_build_id
-                    == script_build_id
-                )
-                .order_by(
-                    models.ScriptBuildMultipathDecision.created_at.asc(),
-                    models.ScriptBuildMultipathDecision.id.asc(),
-                )
-                .all()
-            )
-            domain_info = (
-                session.query(models.ScriptBuildDomainInfo)
-                .filter(models.ScriptBuildDomainInfo.script_build_id == script_build_id)
-                .order_by(
-                    models.ScriptBuildDomainInfo.created_at.asc(),
-                    models.ScriptBuildDomainInfo.id.asc(),
-                )
-                .all()
-            )
-            event_table = self._table(session, "script_build_event")
-            events = (
-                session.query(event_table)
-                .filter(event_table.c.script_build_id == script_build_id)
-                .order_by(
-                    event_table.c.event_seq.asc(),
-                    event_table.c.id.asc(),
-                )
-                .all()
-            )
-            event_payloads = [self._event_summary(row) for row in events]
-            self._attach_light_event_bodies(session, event_payloads)
-
-            paragraphs = (
-                session.query(models.ScriptBuildParagraph)
-                .filter(models.ScriptBuildParagraph.script_build_id == script_build_id)
-                .order_by(
-                    models.ScriptBuildParagraph.branch_id.asc(),
-                    models.ScriptBuildParagraph.id.asc(),
-                )
-                .all()
-            )
-            elements = (
-                session.query(models.ScriptBuildElement)
-                .filter(models.ScriptBuildElement.script_build_id == script_build_id)
-                .order_by(
-                    models.ScriptBuildElement.branch_id.asc(),
-                    models.ScriptBuildElement.id.asc(),
-                )
-                .all()
-            )
-            links = (
-                session.query(models.ScriptBuildParagraphElement)
-                .filter(
-                    models.ScriptBuildParagraphElement.script_build_id
-                    == script_build_id
-                )
-                .order_by(
-                    models.ScriptBuildParagraphElement.branch_id.asc(),
-                    models.ScriptBuildParagraphElement.id.asc(),
-                )
-                .all()
-            )
-
-            branch_payloads: list[dict[str, Any]] = []
-            for branch in branches:
-                payload = row_dict(branch)
-                payload["candidate_snapshot"] = self._candidate_snapshot(
-                    branch, paragraphs=paragraphs, elements=elements, links=links
-                )
-                branch_payloads.append(payload)
-
-            return {
-                "record": row_dict(record),
-                "rounds": [row_dict(row) for row in rounds],
-                "branches": branch_payloads,
-                "dataDecisions": [row_dict(row) for row in data_decisions],
-                "multipathDecisions": [
-                    row_dict(row) for row in multipath_decisions
-                ],
-                "domainInfo": [row_dict(row) for row in domain_info],
-                "events": event_payloads,
-                "ignoredLegacyDataDecisionCount": int(
-                    ignored_legacy_decisions or 0
-                ),
-                "currentArtifact": self._snapshot_for_branch(
-                    0, paragraphs, elements, links
-                ),
-            }
-        finally:
-            session.close()
-
-    def load_current_artifact(self, script_build_id: int) -> dict[str, Any]:
-        """Read only the active Base rows needed by the full script viewer."""
-        _, models = load_runtime_modules()
-        session = self._session_factory()
-        try:
-            exists = (
-                session.query(models.ScriptBuildRecord.id)
-                .filter(models.ScriptBuildRecord.id == script_build_id)
-                .first()
-            )
-            if exists is None:
-                raise BuildNotFound(f"未找到脚本构建 #{script_build_id}")
-
-            paragraphs = (
-                session.query(models.ScriptBuildParagraph)
-                .filter(
-                    models.ScriptBuildParagraph.script_build_id == script_build_id,
-                    models.ScriptBuildParagraph.branch_id == 0,
-                    models.ScriptBuildParagraph.is_active.is_not(False),
-                )
-                .order_by(
-                    models.ScriptBuildParagraph.paragraph_index.asc(),
-                    models.ScriptBuildParagraph.id.asc(),
-                )
-                .all()
-            )
-            elements = (
-                session.query(models.ScriptBuildElement)
-                .filter(
-                    models.ScriptBuildElement.script_build_id == script_build_id,
-                    models.ScriptBuildElement.branch_id == 0,
-                    models.ScriptBuildElement.is_active.is_not(False),
-                )
-                .order_by(models.ScriptBuildElement.id.asc())
-                .all()
-            )
-            paragraph_ids = [row.id for row in paragraphs]
-            element_ids = [row.id for row in elements]
-            links = []
-            if paragraph_ids and element_ids:
-                links = (
-                    session.query(models.ScriptBuildParagraphElement)
-                    .filter(
-                        models.ScriptBuildParagraphElement.script_build_id
-                        == script_build_id,
-                        models.ScriptBuildParagraphElement.branch_id == 0,
-                        models.ScriptBuildParagraphElement.paragraph_id.in_(paragraph_ids),
-                        models.ScriptBuildParagraphElement.element_id.in_(element_ids),
-                    )
-                    .order_by(models.ScriptBuildParagraphElement.id.asc())
-                    .all()
-                )
-            return self._snapshot_for_branch(0, paragraphs, elements, links)
-        finally:
-            session.close()
-
-    def load_event_detail(
-        self, script_build_id: int, event_id: int
-    ) -> dict[str, Any]:
-        values = self.load_event_details(script_build_id, [event_id])
-        event = values.get(int(event_id))
-        if event is None:
-            raise ActivityNotFound(f"未找到运行事件 {event_id}")
-        return event
-
-    def load_event_details(
-        self, script_build_id: int, event_ids: list[int]
-    ) -> dict[int, dict[str, Any]]:
-        """Batch-load full Event + Event Body records for one Inspector.
-
-        A creative card can reference dozens of events.  Loading the rows and
-        bodies in two queries keeps the three-column endpoint independent of
-        that cardinality.
-        """
-        wanted: list[int] = []
-        for value in event_ids:
-            try:
-                event_id = int(value)
-            except (TypeError, ValueError):
-                continue
-            if event_id > 0 and event_id not in wanted:
-                wanted.append(event_id)
-        wanted.sort()
-        if not wanted:
-            return {}
-        session = self._session_factory()
-        try:
-            event_table = self._table(session, "script_build_event")
-            events = (
-                session.query(event_table)
-                .filter(
-                    event_table.c.script_build_id == script_build_id,
-                    event_table.c.id.in_(wanted),
-                )
-                .order_by(event_table.c.event_seq.asc(), event_table.c.id.asc())
-                .all()
-            )
-            body_table = self._table(session, "script_build_event_body")
-            bodies = (
-                session.query(body_table)
-                .filter(
-                    body_table.c.script_build_id == script_build_id,
-                    body_table.c.event_id.in_(wanted),
-                )
-                .all()
-            )
-            body_by_event = {
-                int(parsed["eventId"]): parsed
-                for row in bodies
-                for parsed in [self._event_body(row)]
-                if parsed.get("eventId") is not None
-            }
-            result: dict[int, dict[str, Any]] = {}
-            for row in events:
-                payload = self._event_summary(row)
-                event_id = int(payload["id"])
-                parsed = body_by_event.get(event_id)
-                payload["input"] = parsed.get("input") if parsed else None
-                payload["output"] = parsed.get("output") if parsed else None
-                result[event_id] = payload
-            return result
-        finally:
-            session.close()
-
-    def load_prompt_context(
-        self, script_build_id: int, prompt_ref: str
-    ) -> dict[str, Any]:
-        """Read the exact delegated task plus the *current* prompt config.
-
-        Prompt history is intentionally not guessed from event time: runtime
-        events do not store a prompt snapshot or version reference.
-        """
-
-        ref = str(prompt_ref or "").strip()
-        event: dict[str, Any] | None = None
-        if ref.startswith("prompt:event:") or ref.startswith("event:"):
-            try:
-                event_id = int(ref.rsplit(":", 1)[1])
-            except ValueError as exc:
-                raise ActivityNotFound(f"无效的规则引用 {prompt_ref}") from exc
-            event = self.load_event_detail(script_build_id, event_id)
-            event_name = str(event.get("event_name") or "")
-        elif ref in {"prompt:main", "main"}:
-            event_name = "main"
-        else:
-            raise ActivityNotFound(f"未找到规则引用 {prompt_ref}")
-
-        config = _prompt_config(event_name)
-        task = None
-        if event:
-            side = event.get("input")
-            content = side.get("content") if isinstance(side, dict) else None
-            if isinstance(content, dict):
-                task = content.get("task")
-            elif isinstance(content, str):
-                task = content
-
-        session = self._session_factory()
-        try:
-            _, models = load_runtime_modules()
-            exists = (
-                session.query(models.ScriptBuildRecord.id)
-                .filter(models.ScriptBuildRecord.id == script_build_id)
-                .first()
-            )
-            if exists is None:
-                raise BuildNotFound(f"未找到脚本构建 #{script_build_id}")
-            prompt_row = None
-            for biz_type in config["bizTypes"]:
-                prompt_row = (
-                    session.query(models.Prompt)
-                    .filter(models.Prompt.biz_type == biz_type)
-                    .order_by(models.Prompt.id.asc())
-                    .first()
-                )
-                if prompt_row is not None:
-                    break
-            if prompt_row is not None:
-                content = prompt_row.prompt_content
-                source = "current-db"
-                version = prompt_row.current_version
-            else:
-                path = runtime_dir() / "prompts" / "script" / config["file"]
-                content = path.read_text(encoding="utf-8") if path.exists() else None
-                source = "current-file"
-                version = None
-            return {
-                "promptRef": ref,
-                "actor": config["actor"],
-                "task": task,
-                "promptContent": content,
-                "promptSource": source,
-                "promptVersion": version,
-                "modeNotice": config.get("modeNotice"),
-            }
-        finally:
-            session.close()
-
-    def _table(self, session: Any, name: str) -> Table:
-        table = self._reflected_tables.get(name)
-        if table is not None:
-            return table
-        # SQLAlchemy registers a Table in MetaData before reflection has
-        # populated all columns. Parallel Inspector requests must not observe
-        # that half-built object.
-        with self._table_lock:
-            table = self._reflected_tables.get(name)
-            if table is None:
-                table = Table(name, self._metadata, autoload_with=session.get_bind())
-                self._reflected_tables[name] = table
-            return table
-
-    @staticmethod
-    def _build_summary(row: Any) -> dict[str, Any]:
-        return {
-            "id": str(row.id),
-            "status": row.status,
-            "title": f"脚本构建 #{row.id}",
-            "scriptDirection": row.script_direction,
-            "createdAt": _json_value(row.start_time),
-            "updatedAt": _json_value(row.end_time),
-        }
-
-    @staticmethod
-    def _event_summary(row: Any) -> dict[str, Any]:
-        payload = row_dict(row)
-        payload["detailRef"] = f"event:{payload.get('id')}"
-        return payload
-
-    @staticmethod
-    def _event_body(row: Any) -> dict[str, Any]:
-        mapping = getattr(row, "_mapping", None)
-        data = dict(mapping) if mapping is not None else {
-            column.name: getattr(row, column.name)
-            for column in row.__table__.columns
-        }
-        return {
-            "id": data.get("id"),
-            "eventId": data.get("event_id"),
-            "input": ScriptBuildRepository._body_side(
-                data.get("input_content_type"), data.get("input_content")
-            ),
-            "output": ScriptBuildRepository._body_side(
-                data.get("output_content_type"), data.get("output_content")
-            ),
-        }
-
-    @staticmethod
-    def _body_side(content_type: Any, raw: Any) -> dict[str, Any] | None:
-        if raw is None:
-            return None
-        raw_text = str(raw)
-        content: Any = raw
-        parsed = False
-        if content_type == "json" or raw_text.lstrip().startswith(("{", "[")):
-            try:
-                content = json.loads(raw_text)
-                parsed = True
-            except (TypeError, json.JSONDecodeError):
-                content = raw
-        truncated = len(raw_text) > _DETAIL_BODY_MAX
-        safe_content = (
-            sanitize(raw_text, max_text=_DETAIL_BODY_MAX)
-            if truncated
-            # A valid JSON body below the declared detail limit must not have
-            # an individual string silently clipped at a second, lower limit.
-            else sanitize(content, max_text=_DETAIL_BODY_MAX)
-        )
-        return {
-            "contentType": content_type,
-            "content": safe_content,
-            "truncated": truncated,
-            **(
-                {"omittedCharacters": len(raw_text) - _DETAIL_BODY_MAX}
-                if truncated
-                else {}
-            ),
-        }
-
-    def _attach_light_event_bodies(
-        self, session: Any, events: list[dict[str, Any]]
-    ) -> None:
-        """Attach only the small event fields needed by the business projector.
-
-        Tool outputs can be very large.  The execution view intentionally reads
-        only tool inputs and the event's bounded output preview; full output is
-        loaded by ``load_event_detail`` after the user opens one query.
-        """
-        agent_ids = [
-            int(item["id"])
-            for item in events
-            if item.get("id") is not None and self._needs_light_agent_body(item)
-        ]
-        tool_ids = [
-            int(item["id"])
-            for item in events
-            if item.get("id") is not None and item.get("event_type") == "tool_call"
-        ]
-        query_tool_ids = [
-            int(item["id"])
-            for item in events
-            if item.get("id") is not None
-            and item.get("event_type") == "tool_call"
-            and str(item.get("event_name") or "") in QUERY_TOOL_NAMES
-        ]
-        if not agent_ids and not tool_ids:
-            return
-        body_table = self._table(session, "script_build_event_body")
-        build_ids = {
-            int(item["script_build_id"])
-            for item in events
-            if item.get("script_build_id") is not None
-        }
-        body_by_event: dict[int, dict[str, Any]] = {}
-        if agent_ids:
-            rows = (
-                session.query(
-                    body_table.c.event_id,
-                    body_table.c.input_content_type,
-                    body_table.c.input_content,
-                    body_table.c.output_content_type,
-                    body_table.c.output_content,
-                )
-                .filter(
-                    body_table.c.event_id.in_(agent_ids),
-                    body_table.c.script_build_id.in_(build_ids),
-                )
-                .all()
-            )
-            for row in rows:
-                data = row_dict(row)
-                body_by_event[int(data["event_id"])] = {
-                    "inputData": self._light_content(
-                        data.get("input_content_type"), data.get("input_content")
-                    ),
-                    "agentOutputData": self._light_content(
-                        data.get("output_content_type"), data.get("output_content")
-                    ),
-                }
-        if tool_ids:
-            rows = (
-                session.query(
-                    body_table.c.event_id,
-                    body_table.c.input_content_type,
-                    body_table.c.input_content,
-                )
-                .filter(
-                    body_table.c.event_id.in_(tool_ids),
-                    body_table.c.script_build_id.in_(build_ids),
-                )
-                .all()
-            )
-            for row in rows:
-                data = row_dict(row)
-                body_by_event.setdefault(int(data["event_id"]), {})["inputData"] = self._light_content(
-                    data.get("input_content_type"), data.get("input_content")
-                )
-        if query_tool_ids:
-            rows = (
-                session.query(
-                    body_table.c.event_id,
-                    body_table.c.output_content,
-                )
-                .filter(
-                    body_table.c.event_id.in_(query_tool_ids),
-                    body_table.c.script_build_id.in_(build_ids),
-                )
-                .all()
-            )
-            event_by_id = {
-                int(item["id"]): item
-                for item in events
-                if item.get("id") is not None
-            }
-            for row in rows:
-                mapping = getattr(row, "_mapping", {})
-                event_id = int(mapping.get("event_id"))
-                event = event_by_id.get(event_id) or {}
-                body_by_event.setdefault(event_id, {})["retrievalOutcome"] = (
-                    summarize_retrieval_output(
-                        str(event.get("event_name") or ""),
-                        raw_status=event.get("status"),
-                        ended_at=event.get("ended_at"),
-                        output=mapping.get("output_content"),
-                    )
-                )
-        for event in events:
-            event.update(body_by_event.get(int(event.get("id") or 0), {}))
-
-    @staticmethod
-    def _needs_light_agent_body(event: dict[str, Any]) -> bool:
-        if event.get("event_type") != "agent_invoke":
-            return False
-        name = str(event.get("event_name") or "")
-        return name in RETRIEVAL_AGENT_LABELS or name in {
-            "script_implementer",
-            "script_multipath_evaluator",
-            "script_evaluator",
-        }
-
-    @staticmethod
-    def _light_content(content_type: Any, raw: Any) -> Any:
-        if raw is None:
-            return None
-        if content_type == "json":
-            try:
-                return sanitize(json.loads(str(raw)), max_text=20_000)
-            except (TypeError, json.JSONDecodeError):
-                return sanitize(raw, max_text=20_000)
-        return sanitize(raw, max_text=20_000)
-
-    def _candidate_snapshot(
-        self,
-        branch: Any,
-        *,
-        paragraphs: list[Any],
-        elements: list[Any],
-        links: list[Any],
-    ) -> dict[str, Any]:
-        if branch.status in {"merged", "discarded"}:
-            if branch.content_snapshot:
-                return {
-                    "snapshot": sanitize(branch.content_snapshot),
-                    "sourceOrigin": "database",
-                    "snapshotKind": "saved-at-disposition",
-                    "historicalAccuracy": "exact",
-                    "currentProjectionAccuracy": "not-applicable",
-                    "note": "分支处置前保存的候选快照",
-                }
-            return {
-                "snapshot": None,
-                "sourceOrigin": "missing",
-                "snapshotKind": "missing-disposition-snapshot",
-                "historicalAccuracy": "missing",
-                "currentProjectionAccuracy": "not-applicable",
-                "note": "终态候选未保存处置前快照,不能用当前主脚本反推当时候选。",
-            }
-        return {
-            "snapshot": self._snapshot_for_branch(
-                int(branch.branch_id), paragraphs, elements, links
-            ),
-            "sourceOrigin": "database",
-            "snapshotKind": "current-overlay",
-            "historicalAccuracy": "unknown",
-            "currentProjectionAccuracy": "exact",
-            "note": (
-                "基于当前主脚本计算的 overlay;不是暂存时的历史快照"
-                if branch.status == "parked"
-                else "基于当前主脚本计算的分支 overlay"
-            ),
-        }
-
-    @staticmethod
-    def _snapshot_for_branch(
-        branch_id: int,
-        paragraphs: list[Any],
-        elements: list[Any],
-        links: list[Any],
-    ) -> dict[str, Any]:
-        base_paras = [
-            row
-            for row in paragraphs
-            if int(row.branch_id or 0) == 0 and row.is_active is not False
-        ]
-        base_elems = [
-            row
-            for row in elements
-            if int(row.branch_id or 0) == 0 and row.is_active is not False
-        ]
-        active_base_para_ids = {row.id for row in base_paras}
-        active_base_elem_ids = {row.id for row in base_elems}
-        base_links = [
-            row
-            for row in links
-            if int(row.branch_id or 0) == 0
-            and row.paragraph_id in active_base_para_ids
-            and row.element_id in active_base_elem_ids
-        ]
-        if branch_id == 0:
-            para_rows, elem_rows, link_rows = base_paras, base_elems, base_links
-            origin = "database"
-        else:
-            branch_paras = [
-                row
-                for row in paragraphs
-                if int(row.branch_id or 0) == branch_id
-                and row.is_active is not False
-            ]
-            branch_elems = [
-                row
-                for row in elements
-                if int(row.branch_id or 0) == branch_id
-                and row.is_active is not False
-            ]
-            overridden_para_ids = {
-                row.base_ref_id
-                for row in branch_paras
-                if row.base_ref_id is not None
-            }
-            overridden_elem_ids = {
-                row.base_ref_id
-                for row in branch_elems
-                if row.base_ref_id is not None
-            }
-            para_rows = [
-                row for row in base_paras if row.id not in overridden_para_ids
-            ] + branch_paras
-            elem_rows = [
-                row for row in base_elems if row.id not in overridden_elem_ids
-            ] + branch_elems
-            visible_para_ids = {row.id for row in para_rows} | overridden_para_ids
-            visible_elem_ids = {row.id for row in elem_rows} | overridden_elem_ids
-            branch_links = [
-                row
-                for row in links
-                if int(row.branch_id or 0) == branch_id
-                and row.paragraph_id in visible_para_ids
-                and row.element_id in visible_elem_ids
-            ]
-            seen = {(row.paragraph_id, row.element_id) for row in base_links}
-            link_rows = base_links + [
-                row
-                for row in branch_links
-                if (row.paragraph_id, row.element_id) not in seen
-            ]
-            origin = "database-overlay"
-        return {
-            "paragraphs": [_artifact_row(row) for row in para_rows],
-            "elements": [_artifact_row(row) for row in elem_rows],
-            "paragraphElements": [row_dict(row) for row in link_rows],
-            "sourceOrigin": origin,
-        }
-
-
-def _artifact_row(row: Any) -> dict[str, Any]:
-    payload = row_dict(row)
-    row_id = payload.get("id")
-    base_ref_id = payload.get("base_ref_id")
-    payload["rowId"] = row_id
-    payload["baseRefId"] = base_ref_id
-    payload["effectiveId"] = base_ref_id if base_ref_id is not None else row_id
-    return payload
-
-
-def _prompt_config(event_name: str) -> dict[str, Any]:
-    configs = {
-        "main": {
-            "bizTypes": ("script_build_system",),
-            "file": "script_build_system_v2.md",
-            "actor": {"role": "main", "label": "主 Agent"},
-        },
-        "script_implementer": {
-            "bizTypes": ("script_implementer",),
-            "file": "script_implementer.md",
-            "actor": {"role": "implementer", "label": "实现 Agent"},
-        },
-        "script_multipath_evaluator": {
-            "bizTypes": ("script_build_eval_agent", "script_build_evaluator"),
-            "file": "script_build_eval_agent.md",
-            "actor": {"role": "multipath-evaluator", "label": "多方案评审 Agent"},
-            "modeNotice": "当前调用使用多方案对比评审模式。",
-        },
-        "script_evaluator": {
-            "bizTypes": ("script_build_eval_agent", "script_build_evaluator"),
-            "file": "script_build_eval_agent.md",
-            "actor": {"role": "overall-evaluator", "label": "整体评审 Agent"},
-            "modeNotice": "当前调用使用主脚本整体评审模式。",
-        },
-    }
-    config = configs.get(str(event_name))
-    if config is None:
-        raise ActivityNotFound(f"该活动没有可展示的 Agent 规则:{event_name}")
-    return config

+ 0 - 426
visualization/backend/app/retrieval_detail_projection.py

@@ -1,426 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from typing import Any
-
-from .runtime_tool_catalog import (
-    MAIN_DECISION_READ_TOOLS,
-    RETRIEVAL_AGENT_LABELS,
-    business_tool_label,
-)
-
-
-QUERY_TOOL_LABELS = {
-    "search_script_decode_case": "解构 Case 查询",
-    "external_search_case": "外部内容查询",
-    "search_knowledge": "创作知识查询",
-}
-QUERY_TOOL_NAMES = frozenset(QUERY_TOOL_LABELS)
-RETRIEVAL_TOOL_NAMES = frozenset({*QUERY_TOOL_NAMES, *MAIN_DECISION_READ_TOOLS})
-RETRIEVAL_RESULT_COLLECTION_KEYS: dict[str, str | None] = {
-    "search_script_decode_case": "results",
-    "external_search_case": "data",
-    "search_knowledge": None,
-    "get_domain_info": "domain_info",
-}
-TERMINAL_RUN_STATUSES = frozenset(
-    {"success", "partial", "failed", "stopped", "completed", "cancelled", "cancel", "cancle"}
-)
-
-_CONDITION_LABELS = {
-    "keyword": "关键词",
-    "account_name": "账号",
-    "match_fields": "匹配范围",
-    "top_k": "最多返回",
-    "return_field": "返回内容",
-    "platform_channel": "平台",
-    "max_count": "最多返回",
-    "message": "查询需求",
-    "script_build_id": "构建记录",
-}
-_HIDDEN_CONDITIONS = {"execution_id", "account_name_guard"}
-
-
-def is_retrieval_detail_event(event: dict[str, Any]) -> bool:
-    return (
-        str(event.get("event_type") or "") == "tool_call"
-        and (
-            str(event.get("event_name") or "") in RETRIEVAL_TOOL_NAMES
-            or str(event.get("agent_role") or "") in RETRIEVAL_AGENT_LABELS
-        )
-    )
-
-
-def summarize_retrieval_output(
-    event_name: str,
-    *,
-    raw_status: Any,
-    ended_at: Any,
-    output: Any,
-    run_status: Any = None,
-) -> dict[str, Any]:
-    """Return only the bounded status/count needed by the execution view."""
-
-    parsed = _parse_jsonish(output)
-    state, count, error = _outcome(
-        event_name,
-        raw_status=raw_status,
-        ended_at=ended_at,
-        output=parsed,
-        run_status=run_status,
-    )
-    return {
-        "state": state,
-        "count": count,
-        "error": error,
-    }
-
-
-def project_retrieval_event(
-    event: dict[str, Any], *, run_status: Any = None
-) -> dict[str, Any]:
-    name = str(event.get("event_name") or "")
-    source_kind = "direct-tool" if name in MAIN_DECISION_READ_TOOLS else "agent-query"
-    input_value = _event_side(event, "input")
-    output_value = _event_side(event, "output")
-    output_side = event.get("output")
-    output_truncated = isinstance(output_side, dict) and bool(output_side.get("truncated"))
-    summary_output = (
-        _parse_jsonish(event.get("output_preview"))
-        if output_truncated
-        else output_value
-    )
-    summary = summarize_retrieval_output(
-        name,
-        raw_status=event.get("status"),
-        ended_at=event.get("ended_at"),
-        output=summary_output,
-        run_status=run_status,
-    )
-    state = summary["state"]
-    count = summary.get("count")
-    error = summary.get("error")
-    items = _result_items(name, output_value) if state == "hit" and not output_truncated else []
-    completeness = _completeness(event.get("output"), output_value)
-    return {
-        "detailKind": "retrieval-event",
-        "eventId": _integer(event.get("id")) or 0,
-        "sourceKind": source_kind,
-        "toolName": name,
-        "businessLabel": QUERY_TOOL_LABELS.get(name)
-        or business_tool_label(name),
-        "queryConditions": _query_conditions(input_value),
-        "outcome": {
-            "state": state,
-            **({"count": count} if count is not None else {}),
-            "message": _outcome_message(state, count, error, source_kind),
-            **({"errorKind": _error_kind(error)} if error else {}),
-        },
-        "items": items,
-        "completeness": completeness,
-    }
-
-
-def _outcome(
-    name: str,
-    *,
-    raw_status: Any,
-    ended_at: Any,
-    output: Any,
-    run_status: Any,
-) -> tuple[str, int | None, str | None]:
-    status = str(raw_status or "").lower()
-    terminal_run = str(run_status or "").lower() in TERMINAL_RUN_STATUSES
-    if terminal_run and (status == "running" or not ended_at):
-        return "interrupted", None, None
-
-    error = _error_text(output)
-    if status in {"error", "failed", "failure"} or error:
-        return "failure", None, error
-
-    count = _result_count(name, output)
-    if count is not None:
-        return ("hit" if count > 0 else "empty"), count, None
-
-    if status == "running" or not ended_at:
-        return "running", None, None
-
-    if name in MAIN_DECISION_READ_TOOLS and output not in (None, "", [], {}):
-        return "hit", None, None
-    return "unknown", None, None
-
-
-def _error_text(value: Any) -> str | None:
-    if isinstance(value, dict):
-        success = value.get("success")
-        if success is False or (isinstance(success, str) and success.lower() == "false"):
-            candidate = value.get("error") or value.get("message") or "返回结果标记为失败"
-            return _clean_error(candidate)
-        error = value.get("error")
-        if error not in (None, "", False, [], {}):
-            return _clean_error(error)
-        status = str(value.get("status") or "").lower()
-        if status in {"error", "failed", "failure"}:
-            return _clean_error(value.get("message") or value.get("output") or status)
-    if isinstance(value, str):
-        text = value.strip()
-        if re.match(r"^(?:错误|失败)\s*[::]", text) or text.startswith("❌"):
-            return _clean_error(text)
-    return None
-
-
-def _clean_error(value: Any) -> str:
-    text = str(value or "").strip()
-    text = re.sub(r"^(?:错误|失败)\s*[::]\s*", "", text)
-    text = re.sub(r"^❌\s*", "", text)
-    return text or "执行失败,未保存更多错误说明"
-
-
-def _result_count(name: str, value: Any) -> int | None:
-    if name == "search_knowledge" and isinstance(value, list):
-        return len(value)
-    if isinstance(value, list):
-        return len(value)
-    if not isinstance(value, dict):
-        return None
-    for key in ("count", "returned_count", "result_count"):
-        count = _integer(value.get(key))
-        if count is not None:
-            return count
-    for key in ("results", "data", "items", "domain_info"):
-        if isinstance(value.get(key), list):
-            return len(value[key])
-    return None
-
-
-def _result_items(name: str, output: Any) -> list[dict[str, Any]]:
-    if name == "search_script_decode_case":
-        values = _result_collection(name, output)
-        return [_case_item(item, index) for index, item in enumerate(values or [], 1)]
-    if name == "external_search_case":
-        values = _result_collection(name, output)
-        return [_external_item(item, index, output) for index, item in enumerate(values or [], 1)]
-    if name == "search_knowledge":
-        return [
-            _knowledge_item(item, index)
-            for index, item in enumerate(_result_collection(name, output) or [], 1)
-        ]
-    return _direct_items(name, output)
-
-
-def _result_collection(name: str, output: Any) -> Any:
-    key = RETRIEVAL_RESULT_COLLECTION_KEYS.get(name)
-    if key is None:
-        return output
-    return output.get(key) if isinstance(output, dict) else None
-
-
-def _case_item(item: Any, index: int) -> dict[str, Any]:
-    data = item if isinstance(item, dict) else {"data": item}
-    account = str(data.get("account") or "未知账号")
-    return {
-        "id": str(data.get("post_id") or index),
-        "title": f"{account} · Case {index}",
-        "subtitle": str(data.get("post_id") or "来源标识未记录"),
-        "meta": _meta(
-            ("匹配分数", data.get("score")),
-            ("返回内容", data.get("return_field")),
-        ),
-        "sections": [{"title": "完整解构内容", "value": data.get("data")}],
-    }
-
-
-def _external_item(item: Any, index: int, output: dict[str, Any]) -> dict[str, Any]:
-    data = item if isinstance(item, dict) else {"body_text": item}
-    return {
-        "id": str(data.get("channel_content_id") or index),
-        "title": str(data.get("title") or f"外部内容 {index}"),
-        "subtitle": str(data.get("channel") or output.get("platform_channel") or "外部平台"),
-        "href": data.get("link"),
-        "meta": _meta(
-            ("内容类型", data.get("content_type")),
-            ("点赞", data.get("like_count")),
-            ("发布时间", data.get("publish_timestamp")),
-        ),
-        "sections": [
-            value
-            for value in (
-                _value_section("正文", data.get("body_text")),
-                _value_section("图片内容理解", data.get("images_text_escape")),
-                _value_section("图片", data.get("images")),
-                _value_section("视频", data.get("videos")),
-            )
-            if value
-        ],
-    }
-
-
-def _knowledge_item(item: Any, index: int) -> dict[str, Any]:
-    data = item if isinstance(item, dict) else {"content": item}
-    return {
-        "id": str(data.get("id") or index),
-        "title": str(data.get("title") or f"知识条目 {index}"),
-        "subtitle": str(data.get("purpose") or ""),
-        "meta": [],
-        "sections": [section for section in [_value_section("完整内容", data.get("content"))] if section],
-    }
-
-
-def _direct_items(name: str, output: Any) -> list[dict[str, Any]]:
-    if name == "get_domain_info" and isinstance(output, dict):
-        values = _result_collection(name, output)
-        return [
-            {
-                "id": str(item.get("id") or index),
-                "title": f"领域事实 {index}",
-                "subtitle": str(item.get("note") or ""),
-                "meta": _meta(("轮次", item.get("round_index"))),
-                "sections": [
-                    section
-                    for section in (
-                        _value_section("事实内容", item.get("content")),
-                        _value_section("核实说明", item.get("note")),
-                        _value_section("来源", item.get("source")),
-                    )
-                    if section
-                ],
-            }
-            for index, item in enumerate(values or [], 1)
-            if isinstance(item, dict)
-        ]
-
-    if output in (None, "", [], {}):
-        return []
-    data = output if isinstance(output, dict) else {"完整返回": output}
-    title = {
-        "get_topic_detail": "选题完整信息",
-        "get_script_direction": "当前创作方向",
-        "get_script_snapshot": "当前主脚本快照",
-        "get_account_script_section_patterns": "账号段落模式",
-        "get_account_script_persona_points": "账号人设",
-    }.get(name, business_tool_label(name))
-    preferred = {
-        "get_topic_detail": [("选题", "topic"), ("创作点", "points"), ("内容关系", "item_relations")],
-        "get_script_snapshot": [("脚本概况", "script_build"), ("段落", "paragraphs"), ("元素", "elements"), ("段落元素关联", "paragraph_element_links")],
-        "get_account_script_section_patterns": [("分段规律摘要", "分段规律摘要"), ("典型分段依据", "典型分段依据"), ("段落结构模式", "段落结构模式列表"), ("分析规模", "分析帖子数")],
-    }.get(name)
-    if preferred:
-        sections = [_value_section(label, data.get(key)) for label, key in preferred]
-    else:
-        sections = [_value_section(_condition_label(key), value) for key, value in data.items()]
-    return [{
-        "id": name,
-        "title": title,
-        "subtitle": "",
-        "meta": [],
-        "sections": [section for section in sections if section],
-    }]
-
-
-def _query_conditions(value: Any) -> list[dict[str, Any]]:
-    if not isinstance(value, dict):
-        if value in (None, ""):
-            return []
-        return [{"key": "input", "label": "调用输入", "value": value}]
-    return [
-        {"key": str(key), "label": _condition_label(str(key)), "value": item}
-        for key, item in value.items()
-        if key not in _HIDDEN_CONDITIONS and item not in (None, "", [], {})
-    ]
-
-
-def _condition_label(key: str) -> str:
-    return _CONDITION_LABELS.get(key, key.replace("_", " "))
-
-
-def _outcome_message(
-    state: str, count: int | None, error: str | None, source_kind: str
-) -> str:
-    if state == "failure":
-        return f"查询执行失败:{error}" if error else "查询执行失败。"
-    if state == "empty":
-        return "查询执行成功,没有匹配数据。"
-    if state == "hit":
-        if source_kind == "direct-tool":
-            return "读取成功。" if count is None else f"读取成功,共返回 {count} 条记录。"
-        return f"查询成功,返回 {count} 条结果。" if count is not None else "查询成功。"
-    if state == "interrupted":
-        return "构建已经结束,但这次活动没有完成。"
-    if state == "running":
-        return "查询仍在执行。"
-    return "现有记录不足以可靠判断查询结果。"
-
-
-def _error_kind(error: str | None) -> str:
-    text = str(error or "").lower()
-    if any(token in text for token in ("参数", "仅支持", "必须", "不能为空", "return_field")):
-        return "parameter"
-    if any(token in text for token in ("http", "timeout", "timed out", "connection", "请求失败")):
-        return "transport"
-    if any(token in text for token in ("upstream", "openrouter", "403", "502", "503")):
-        return "upstream"
-    return "unknown"
-
-
-def _completeness(side: Any, output: Any) -> dict[str, Any]:
-    meta = side if isinstance(side, dict) else {}
-    truncated = bool(meta.get("truncated"))
-    omitted = _integer(meta.get("omittedCharacters"))
-    if isinstance(output, str):
-        match = re.search(r"…\[truncated\s+(\d+)\s+chars\]", output)
-        if match:
-            truncated = True
-            omitted = int(match.group(1))
-    return {
-        "bodyAvailable": side is not None and output is not None,
-        "truncated": truncated,
-        **({"omittedCharacters": omitted} if omitted is not None else {}),
-    }
-
-
-def _event_side(event: dict[str, Any], side: str) -> Any:
-    wrapped = event.get(side)
-    if isinstance(wrapped, dict) and "content" in wrapped:
-        return _parse_jsonish(wrapped.get("content"))
-    if wrapped is not None:
-        return _parse_jsonish(wrapped)
-    fallback = event.get("inputData" if side == "input" else "agentOutputData")
-    if fallback is not None:
-        return _parse_jsonish(fallback)
-    return _parse_jsonish(event.get(f"{side}_preview"))
-
-
-def _parse_jsonish(value: Any) -> Any:
-    if not isinstance(value, str):
-        return value
-    text = value.strip()
-    if not text or text[0] not in "[{":
-        return value
-    try:
-        return json.loads(text)
-    except json.JSONDecodeError:
-        return value
-
-
-def _value_section(title: str, value: Any) -> dict[str, Any] | None:
-    if value in (None, "", [], {}):
-        return None
-    return {"title": title, "value": value}
-
-
-def _meta(*items: tuple[str, Any]) -> list[dict[str, Any]]:
-    return [
-        {"label": label, "value": value}
-        for label, value in items
-        if value not in (None, "", [], {})
-    ]
-
-
-def _integer(value: Any) -> int | None:
-    if isinstance(value, bool):
-        return None
-    try:
-        return int(value)
-    except (TypeError, ValueError):
-        return None

+ 0 - 567
visualization/backend/app/retrieval_event_projection.py

@@ -1,567 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from collections import Counter
-from datetime import datetime, timezone
-from typing import Any
-from zoneinfo import ZoneInfo
-
-from .business_detail_text import clean_report
-from .runtime_event_index import (
-    RuntimeEventIndex,
-    event_input,
-    event_order,
-    event_output,
-    event_summary,
-    integer,
-)
-from .runtime_tool_catalog import (
-    MAIN_DECISION_READ_TOOLS,
-    RETRIEVAL_AGENT_LABELS,
-    business_tool_label,
-)
-
-_AGENT_CONTEXT_TOOLS = {"think_and_plan", "get_script_snapshot"}
-_DATABASE_TIMEZONE = ZoneInfo("Asia/Shanghai")
-
-
-class RetrievalEventProjector:
-    """Project implementer retrieval activity from an explicit event tree."""
-
-    def project(
-        self,
-        index: RuntimeEventIndex,
-        *,
-        valid_rounds: set[int],
-        valid_branches: set[int],
-        captured_at: datetime,
-        run_status: str | None = None,
-    ) -> dict[str, Any]:
-        ordered = index.ordered
-        stages_by_branch: dict[int, dict[str, Any]] = {}
-        unassigned: list[dict[str, Any]] = []
-
-        implementers: list[dict[str, Any]] = []
-        for event in ordered:
-            if (
-                str(event.get("event_type") or "") != "agent_invoke"
-                or str(event.get("event_name") or "") != "script_implementer"
-            ):
-                continue
-            round_index = integer(event.get("round_index"))
-            branch_id = integer(event.get("branch_id"))
-            if round_index in valid_rounds and branch_id in valid_branches:
-                implementers.append(event)
-            else:
-                unassigned.append(event_summary(event, "missing-business-context"))
-
-        retrieval_agents = [
-            event
-            for event in ordered
-            if str(event.get("event_type") or "") == "agent_invoke"
-            and str(event.get("event_name") or "") in RETRIEVAL_AGENT_LABELS
-        ]
-        attached_retrieval_ids: set[int] = set()
-
-        for implementer in implementers:
-            implementer_id = integer(implementer.get("id"))
-            branch_id = integer(implementer.get("branch_id"))
-            round_index = integer(implementer.get("round_index"))
-            if implementer_id is None or branch_id is None or round_index is None:
-                continue
-
-            child_agents = [
-                event
-                for event in retrieval_agents
-                if integer(event.get("parent_event_id")) == implementer_id
-                and integer(event.get("branch_id")) == branch_id
-                and integer(event.get("round_index")) == round_index
-            ]
-            attached_retrieval_ids.update(
-                value
-                for event in child_agents
-                if (value := integer(event.get("id"))) is not None
-            )
-            direct_groups = _direct_tool_groups(index, implementer, run_status)
-            agent_runs = [
-                _retrieval_agent_run(event, index, run_status)
-                for event in sorted(child_agents, key=event_order)
-            ]
-            operations: list[dict[str, Any]] = [*direct_groups, *agent_runs]
-            waves, observed_mode = _assign_waves(operations, captured_at)
-            status = _stage_status(operations, implementer, run_status)
-            stage = {
-                "id": f"round:{round_index}:branch:{branch_id}:retrieval",
-                "roundIndex": round_index,
-                "branchId": branch_id,
-                "implementerEventId": implementer_id,
-                "observedMode": observed_mode,
-                "waves": waves,
-                "directToolGroups": direct_groups,
-                "agentRuns": agent_runs,
-                "status": status,
-                "startedAt": _iso_value(
-                    min(
-                        (_start(item) for item in operations if _start(item)),
-                        default=None,
-                    )
-                ),
-                "endedAt": _iso_value(
-                    max(
-                        (
-                            _end(item, captured_at)
-                            for item in operations
-                            if _end(item, captured_at)
-                        ),
-                        default=None,
-                    )
-                ),
-                "detailRef": f"round:{round_index}:branch:{branch_id}:retrieval",
-            }
-            previous = stages_by_branch.get(branch_id)
-            if previous is None:
-                stages_by_branch[branch_id] = stage
-                continue
-
-            # Multiple implementer invocations for one branch are rare but
-            # valid. Preserve every operation instead of overwriting history.
-            combined = [
-                *previous.get("directToolGroups", []),
-                *previous.get("agentRuns", []),
-                *direct_groups,
-                *agent_runs,
-            ]
-            waves, mode = _assign_waves(combined, captured_at)
-            previous["directToolGroups"].extend(direct_groups)
-            previous["agentRuns"].extend(agent_runs)
-            previous["waves"] = waves
-            previous["observedMode"] = mode
-            previous["status"] = _combined_stage_status(previous["status"], status)
-
-        for event in retrieval_agents:
-            event_id = integer(event.get("id"))
-            if event_id not in attached_retrieval_ids:
-                unassigned.append(event_summary(event, "missing-parent-implementer"))
-
-        return {
-            "retrievalStagesByBranch": stages_by_branch,
-            "unassigned": unassigned,
-        }
-
-
-def _direct_tool_groups(
-    index: RuntimeEventIndex,
-    implementer: dict[str, Any],
-    run_status: str | None,
-) -> list[dict[str, Any]]:
-    implementer_id = integer(implementer.get("id"))
-    if implementer_id is None:
-        return []
-    scoped = [
-        event
-        for event in index.events_in_scope(implementer_id)
-        if integer(event.get("id")) != implementer_id
-    ]
-    groups: list[list[dict[str, Any]]] = []
-    pending: list[dict[str, Any]] = []
-    for event in sorted(scoped, key=event_order):
-        is_direct = (
-            str(event.get("event_type") or "") == "tool_call"
-            and str(event.get("agent_role") or "") == "script_implementer"
-            and str(event.get("event_name") or "") in MAIN_DECISION_READ_TOOLS
-        )
-        if is_direct:
-            pending.append(event)
-            continue
-        if pending:
-            groups.append(pending)
-            pending = []
-    if pending:
-        groups.append(pending)
-    return [_direct_group(group, run_status) for group in groups]
-
-
-def _direct_group(
-    events: list[dict[str, Any]], run_status: str | None
-) -> dict[str, Any]:
-    first_id = integer(events[0].get("id")) or 0
-    calls = [_direct_call(event, run_status) for event in events]
-    counts = Counter(call["status"] for call in calls)
-    labels = Counter(call["businessLabel"] for call in calls)
-    result = {
-        "id": f"retrieval-direct:{first_id}",
-        "kind": "direct-tools",
-        "owner": "script_implementer",
-        "callCount": len(calls),
-        "successCount": counts["success"],
-        "failureCount": counts["failure"],
-        "runningCount": counts["running"],
-        "sources": [
-            {"businessLabel": label, "callCount": count}
-            for label, count in labels.items()
-        ],
-        "calls": calls,
-        "startedAt": _iso_value(
-            min((_date_value(item.get("started_at")) for item in events), default=None)
-        ),
-        "endedAt": _iso_value(
-            max((_date_value(item.get("ended_at")) for item in events), default=None)
-        ),
-        "detailRef": f"retrieval-direct:{first_id}",
-    }
-    if counts["interrupted"]:
-        result["interruptedCount"] = counts["interrupted"]
-    return result
-
-
-def _direct_call(event: dict[str, Any], run_status: str | None) -> dict[str, Any]:
-    event_id = integer(event.get("id")) or 0
-    status = _operation_status(event, run_status)
-    return {
-        "id": f"event:{event_id}",
-        "eventId": event_id,
-        "businessLabel": business_tool_label(
-            str(event.get("event_name") or "")
-        ),
-        "status": status,
-        "resultSummary": (
-            "读取失败"
-            if status == "failure"
-            else "构建中断"
-            if status == "interrupted"
-            else "正在读取"
-            if status == "running"
-            else "已读取"
-        ),
-        "detailRef": f"event:{event_id}",
-    }
-
-
-def _retrieval_agent_run(
-    agent: dict[str, Any], index: RuntimeEventIndex, run_status: str | None
-) -> dict[str, Any]:
-    event_id = integer(agent.get("id")) or 0
-    agent_type = str(agent.get("event_name") or "")
-    child_tools = [
-        event
-        for event in index.ordered
-        if str(event.get("event_type") or "") == "tool_call"
-        and (
-            integer(event.get("scope_event_id")) == event_id
-            or integer(event.get("parent_event_id")) == event_id
-        )
-    ]
-    attempts = [
-        _query_attempt(event, run_status)
-        for event in sorted(child_tools, key=event_order)
-        if str(event.get("event_name") or "") not in _AGENT_CONTEXT_TOOLS
-    ]
-    counts = Counter(attempt["status"] for attempt in attempts)
-    screening = clean_report(_agent_summary(agent))
-    status = _agent_status(agent, run_status)
-    result = {
-        "id": f"retrieval-agent:{event_id}",
-        "kind": "retrieval-agent",
-        "eventId": event_id,
-        "agentType": agent_type,
-        "businessLabel": RETRIEVAL_AGENT_LABELS.get(agent_type, "取数 Agent"),
-        "taskPreview": _preview(_agent_task(agent), 180),
-        "status": status,
-        "querySummary": {
-            "total": len(attempts),
-            "hit": counts["hit"],
-            "empty": counts["empty"],
-            "failure": counts["failure"],
-            "unknown": counts["unknown"] + counts["running"],
-        },
-        "attempts": attempts,
-        "screening": {
-            "state": (
-                "available"
-                if screening
-                else "running"
-                if status == "running"
-                else "missing"
-            ),
-            "preview": _preview(screening, 220),
-        },
-        "startedAt": _iso_value(_date_value(agent.get("started_at"))),
-        "endedAt": _iso_value(_date_value(agent.get("ended_at"))),
-        "durationMs": integer(agent.get("duration_ms")),
-        "detailRef": f"retrieval-agent:{event_id}",
-    }
-    if counts["interrupted"]:
-        result["querySummary"]["interrupted"] = counts["interrupted"]
-    return result
-
-
-def _query_attempt(event: dict[str, Any], run_status: str | None) -> dict[str, Any]:
-    event_id = integer(event.get("id")) or 0
-    status, count = _query_status(event, run_status)
-    return {
-        "id": f"event:{event_id}",
-        "eventId": event_id,
-        "sequence": integer(event.get("event_seq")) or 0,
-        "queryLabel": _query_label(event_input(event)),
-        "status": status,
-        "resultCount": count,
-        "detailRef": f"event:{event_id}",
-    }
-
-
-def _query_status(
-    event: dict[str, Any], run_status: str | None = None
-) -> tuple[str, int | None]:
-    summary = event.get("retrievalOutcome")
-    if isinstance(summary, dict) and summary.get("state"):
-        state = str(summary.get("state"))
-        if (
-            str(run_status or "").lower()
-            in {"success", "partial", "failed", "stopped", "completed", "cancelled", "cancel", "cancle"}
-            and (str(event.get("status") or "").lower() == "running" or not event.get("ended_at"))
-        ):
-            state = "interrupted"
-        return state, integer(summary.get("count"))
-    raw_status = str(event.get("status") or "").lower()
-    if (
-        str(run_status or "").lower()
-        in {"success", "partial", "failed", "stopped", "completed", "cancelled", "cancel", "cancle"}
-        and (raw_status == "running" or not event.get("ended_at"))
-    ):
-        return "interrupted", None
-    if raw_status in {"error", "failed", "failure"}:
-        return "failure", None
-    if raw_status == "running" or not event.get("ended_at"):
-        return "running", None
-    output = event_output(event)
-    if isinstance(output, dict) and _output_reports_failure(output):
-        return "failure", None
-    count = _result_count(output)
-    if count is not None:
-        return ("hit" if count > 0 else "empty"), count
-    text = str(event.get("output_preview") or "").strip()
-    if text == "[]":
-        return "empty", 0
-    return "unknown", None
-
-
-def _output_reports_failure(value: dict[str, Any]) -> bool:
-    success = value.get("success")
-    if success is False or (isinstance(success, str) and success.lower() == "false"):
-        return True
-    error = value.get("error")
-    return error not in (None, "", False, [], {})
-
-
-def _result_count(value: Any) -> int | None:
-    parsed = value
-    if isinstance(value, str):
-        try:
-            parsed = json.loads(value)
-        except json.JSONDecodeError:
-            match = re.search(r'"count"\s*:\s*(\d+)', value)
-            return int(match.group(1)) if match else None
-    if isinstance(parsed, list):
-        return len(parsed)
-    if not isinstance(parsed, dict):
-        return None
-    for key in ("count", "returned_count", "result_count"):
-        number = integer(parsed.get(key))
-        if number is not None:
-            return number
-    for key in ("results", "data", "items"):
-        items = parsed.get(key)
-        if isinstance(items, list):
-            return len(items)
-    return None
-
-
-def _query_label(value: Any) -> str:
-    if not isinstance(value, dict):
-        return "查询内容详见详情"
-    parts: list[str] = []
-    keyword = value.get("keyword") or value.get("query") or value.get("topic_name")
-    if isinstance(keyword, str) and keyword.strip():
-        parts.append(keyword.strip())
-    if not parts and isinstance(value.get("account_name"), str):
-        parts.append(f"账号 {value['account_name'].strip()}")
-    for key in ("names", "category_names"):
-        names = value.get(key)
-        if isinstance(names, list) and names:
-            parts.append("、".join(str(item) for item in names[:4]))
-    return_field = value.get("return_field")
-    if isinstance(return_field, str) and return_field.strip():
-        parts.append(f"返回{return_field.strip()}")
-    platform = value.get("platform_channel")
-    if isinstance(platform, str) and platform.strip():
-        parts.append(platform.strip())
-    return " · ".join(parts) or "查询内容详见详情"
-
-
-def _assign_waves(
-    operations: list[dict[str, Any]], captured_at: datetime
-) -> tuple[list[dict[str, Any]], str]:
-    if not operations:
-        return [], "unknown"
-    if any(_start(item) is None for item in operations):
-        for item in operations:
-            item["waveIndex"] = None
-        return [], "unknown"
-    ordered = sorted(operations, key=lambda item: (_start(item), item.get("id")))
-    waves: list[dict[str, Any]] = []
-    current: list[dict[str, Any]] = []
-    current_end: datetime | None = None
-    for operation in ordered:
-        start = _start(operation)
-        end = _end(operation, captured_at) or start
-        if current and current_end is not None and start is not None and start >= current_end:
-            waves.append(_wave(len(waves), current, current_end))
-            current = []
-            current_end = None
-        current.append(operation)
-        current_end = max(filter(None, (current_end, end)), default=None)
-    if current:
-        waves.append(_wave(len(waves), current, current_end))
-    by_id = {item.get("id"): item for item in operations}
-    for wave in waves:
-        for operation_id in wave["operationIds"]:
-            by_id[operation_id]["waveIndex"] = wave["index"]
-    if len(operations) == 1:
-        mode = "single"
-    elif len(waves) == 1:
-        mode = "parallel"
-    elif any(len(wave["operationIds"]) > 1 for wave in waves):
-        mode = "mixed"
-    else:
-        mode = "sequential"
-    return waves, mode
-
-
-def _wave(
-    index: int, operations: list[dict[str, Any]], end: datetime | None
-) -> dict[str, Any]:
-    return {
-        "index": index,
-        "operationIds": [item.get("id") for item in operations],
-        "startedAt": _iso_value(
-            min((_start(item) for item in operations), default=None)
-        ),
-        "endedAt": _iso_value(end),
-    }
-
-
-def _stage_status(
-    operations: list[dict[str, Any]],
-    implementer: dict[str, Any],
-    run_status: str | None = None,
-) -> str:
-    implementer_status = _operation_status(implementer, run_status)
-    if not operations:
-        return "partial" if implementer_status == "interrupted" else implementer_status if implementer_status == "running" else "missing"
-    statuses = {_operation_status(item, run_status) for item in operations}
-    if "running" in statuses or implementer_status == "running":
-        return "running"
-    if "failure" in statuses or "interrupted" in statuses or implementer_status == "interrupted":
-        return "partial"
-    return "completed"
-
-
-def _combined_stage_status(left: str, right: str) -> str:
-    if "running" in {left, right}:
-        return "running"
-    if "partial" in {left, right}:
-        return "partial"
-    if "completed" in {left, right}:
-        return "completed"
-    return "missing"
-
-
-def _operation_status(
-    value: dict[str, Any], run_status: str | None = None
-) -> str:
-    if int(value.get("failureCount") or 0) > 0:
-        return "failure"
-    if int(value.get("runningCount") or 0) > 0:
-        return "running"
-    status = str(value.get("status") or "").lower()
-    if status == "interrupted":
-        return "interrupted"
-    if (
-        str(run_status or "").lower()
-        in {"success", "partial", "failed", "stopped", "completed", "cancelled", "cancel", "cancle"}
-        and (status == "running" or not value.get("ended_at") and not value.get("endedAt"))
-    ):
-        return "interrupted"
-    if status in {"error", "failed", "failure"}:
-        return "failure"
-    if status == "running" or not value.get("ended_at") and not value.get("endedAt"):
-        return "running"
-    return "success"
-
-
-def _agent_status(value: dict[str, Any], run_status: str | None = None) -> str:
-    status = _operation_status(value, run_status)
-    return (
-        "failed"
-        if status == "failure"
-        else "interrupted"
-        if status == "interrupted"
-        else "running"
-        if status == "running"
-        else "completed"
-    )
-
-
-def _agent_task(event: dict[str, Any]) -> str | None:
-    value = event_input(event)
-    if isinstance(value, dict) and isinstance(value.get("task"), str):
-        return value["task"]
-    return None
-
-
-def _agent_summary(event: dict[str, Any]) -> str | None:
-    value = event_output(event)
-    if isinstance(value, dict) and isinstance(value.get("summary"), str):
-        return value["summary"]
-    return None
-
-
-def _start(item: dict[str, Any]) -> datetime | None:
-    return _date_value(item.get("startedAt") or item.get("started_at"))
-
-
-def _end(item: dict[str, Any], captured_at: datetime) -> datetime | None:
-    value = _date_value(item.get("endedAt") or item.get("ended_at"))
-    if value is None and _operation_status(item) == "running":
-        return captured_at
-    return value
-
-
-def _date_value(value: Any) -> datetime | None:
-    if isinstance(value, datetime):
-        parsed = value
-        if parsed.tzinfo is None:
-            parsed = parsed.replace(tzinfo=_DATABASE_TIMEZONE)
-        return parsed.astimezone(timezone.utc)
-    if isinstance(value, str) and value:
-        try:
-            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
-            if parsed.tzinfo is None:
-                parsed = parsed.replace(tzinfo=_DATABASE_TIMEZONE)
-            return parsed.astimezone(timezone.utc)
-        except ValueError:
-            return None
-    return None
-
-
-def _iso_value(value: datetime | None) -> str | None:
-    return value.isoformat() if value is not None else None
-
-
-def _preview(value: Any, limit: int) -> str | None:
-    text = str(value or "").strip()
-    if not text:
-        return None
-    text = re.sub(r"\s+", " ", text)
-    return text if len(text) <= limit else text[: limit - 1].rstrip() + "…"

+ 0 - 83
visualization/backend/app/runtime_bridge.py

@@ -1,83 +0,0 @@
-from __future__ import annotations
-
-import importlib
-import os
-import sys
-from functools import lru_cache
-from pathlib import Path
-from typing import Any
-
-from sqlalchemy import create_engine, event
-from sqlalchemy.orm import sessionmaker
-
-
-def runtime_dir() -> Path:
-    configured = os.getenv("PATTERN_RUNTIME_DIR")
-    if configured:
-        return Path(configured).expanduser().resolve()
-    return Path(__file__).resolve().parents[3]
-
-
-def database_host_override() -> str | None:
-    """Return a visualization-only DB host override.
-
-    The value is deliberately host-only: credentials, database name and driver
-    continue to come from the parent runtime's existing SQLAlchemy URL.
-    """
-    value = os.getenv("PATTERN_DB_HOST_OVERRIDE", "").strip()
-    if not value:
-        return None
-    if any(token in value for token in ("/", "@", "?", "#", ":")) or any(char.isspace() for char in value):
-        raise ValueError("PATTERN_DB_HOST_OVERRIDE 只能是不含端口的主机名或 IPv4 地址")
-    return value
-
-
-def load_runtime_modules() -> tuple[Any, Any]:
-    """Load only the parent's DB manager and ORM models.
-
-    Importing pattern_service would initialize unrelated mining/config modules, so
-    the visualization intentionally avoids it.
-    """
-    root = str(runtime_dir())
-    if root not in sys.path:
-        sys.path.insert(0, root)
-    return importlib.import_module("db_manager"), importlib.import_module("models")
-
-
-def new_session():
-    """Return a session backed by a pool that is read-only at MySQL level.
-
-    V8 does not borrow the parent application's ordinary session because that
-    pool is intentionally write-capable.  The visualization always owns a
-    separate pool, even when no host override is configured.
-    """
-    return _read_only_session_factory(
-        str(runtime_dir()), database_host_override() or ""
-    )()
-
-
-@lru_cache(maxsize=4)
-def _read_only_session_factory(runtime_path: str, host: str):
-    """Build one cached visualization-only read-only connection pool."""
-    del runtime_path  # Included in the cache key so runtime switches cannot reuse a stale pool.
-    db_manager, _ = load_runtime_modules()
-    parent_manager = db_manager.DatabaseManager()
-    parent_engine = parent_manager.engine
-    try:
-        override_url = parent_engine.url.set(host=host) if host else parent_engine.url
-    finally:
-        # The parent engine is lazy and has not connected; do not retain a second pool.
-        parent_engine.dispose()
-
-    engine = create_engine(override_url, pool_pre_ping=True, pool_recycle=3600)
-    event.listen(engine, "connect", _set_mysql_session_read_only)
-    return sessionmaker(bind=engine, autoflush=False, autocommit=False)
-
-
-def _set_mysql_session_read_only(dbapi_connection, _connection_record) -> None:
-    """Make every transaction on the visualization pool read-only at MySQL level."""
-    cursor = dbapi_connection.cursor()
-    try:
-        cursor.execute("SET SESSION TRANSACTION READ ONLY")
-    finally:
-        cursor.close()

+ 0 - 362
visualization/backend/app/runtime_event_index.py

@@ -1,362 +0,0 @@
-from __future__ import annotations
-
-import json
-from collections import defaultdict
-from dataclasses import dataclass
-from typing import Any, Iterable, Iterator
-
-from .runtime_tool_catalog import MAIN_DECISION_READ_TOOLS
-
-
-_SUCCESS_STATUSES = {"ok", "success", "succeeded", "completed", "complete"}
-_FAILURE_STATUSES = {"error", "failed", "failure"}
-
-
-@dataclass(frozen=True)
-class EventBounds:
-    """Sequence bounds used by projectors; both ends are exclusive by default."""
-
-    after_seq: int | None = None
-    before_seq: int | None = None
-
-
-class RuntimeEventIndex:
-    """Read-only structural index over ``script_build_event`` projections.
-
-    This class intentionally knows nothing about card wording.  It establishes
-    event identity, order, hierarchy, scope and actor once, then exposes strict
-    queries used by the three main-Agent decision projectors.
-    """
-
-    def __init__(self, events: Iterable[dict[str, Any]]):
-        ordered = sorted((dict(item) for item in events), key=event_order)
-        self._ordered: tuple[dict[str, Any], ...] = tuple(ordered)
-        self._by_id: dict[int, dict[str, Any]] = {}
-        self._by_name: dict[str, list[dict[str, Any]]] = defaultdict(list)
-        self._by_scope: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        self._children: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        self._by_actor: dict[str, list[dict[str, Any]]] = defaultdict(list)
-        self._by_round: dict[int, list[dict[str, Any]]] = defaultdict(list)
-
-        for event in ordered:
-            event_id = integer(event.get("id"))
-            if event_id is not None:
-                self._by_id[event_id] = event
-            self._by_name[str(event.get("event_name") or "")].append(event)
-            scope_id = integer(event.get("scope_event_id"))
-            if scope_id is not None:
-                self._by_scope[scope_id].append(event)
-            parent_id = integer(event.get("parent_event_id"))
-            if parent_id is not None:
-                self._children[parent_id].append(event)
-            self._by_actor[event_actor(event)].append(event)
-            round_index = integer(event.get("round_index"))
-            if round_index is not None:
-                self._by_round[round_index].append(event)
-
-        self._main_scope_ids = frozenset(
-            event_id
-            for event in ordered
-            if is_main_scope(event)
-            and (event_id := integer(event.get("scope_event_id")) or integer(event.get("id")))
-            is not None
-        )
-
-    @property
-    def ordered(self) -> tuple[dict[str, Any], ...]:
-        return self._ordered
-
-    @property
-    def main_scope_ids(self) -> frozenset[int]:
-        return self._main_scope_ids
-
-    def by_id(self, event_id: int | None) -> dict[str, Any] | None:
-        return self._by_id.get(int(event_id)) if event_id is not None else None
-
-    def children_of(self, event_id: int) -> tuple[dict[str, Any], ...]:
-        return tuple(self._children.get(int(event_id), ()))
-
-    def events_in_scope(self, scope_id: int) -> tuple[dict[str, Any], ...]:
-        return tuple(self._by_scope.get(int(scope_id), ()))
-
-    def events_for_actor(self, actor: str) -> tuple[dict[str, Any], ...]:
-        return tuple(self._by_actor.get(str(actor), ()))
-
-    def events_for_round(self, round_index: int) -> tuple[dict[str, Any], ...]:
-        return tuple(self._by_round.get(int(round_index), ()))
-
-    def tools_in_scope(
-        self, scope_id: int, *, names: set[str] | frozenset[str] | None = None
-    ) -> list[dict[str, Any]]:
-        return [
-            event
-            for event in self.events_in_scope(scope_id)
-            if str(event.get("event_type") or "") == "tool_call"
-            and (names is None or str(event.get("event_name") or "") in names)
-        ]
-
-    def named(
-        self,
-        name: str,
-        *,
-        event_type: str | None = None,
-        successful: bool | None = None,
-        bounds: EventBounds | None = None,
-    ) -> list[dict[str, Any]]:
-        events = self._by_name.get(str(name), ())
-        return [
-            event
-            for event in events
-            if (event_type is None or str(event.get("event_type") or "") == event_type)
-            and (successful is None or event_succeeded(event) is successful)
-            and _within_bounds(event, bounds)
-        ]
-
-    def anchors(
-        self,
-        name: str,
-        *,
-        successful: bool | None = None,
-        bounds: EventBounds | None = None,
-    ) -> list[dict[str, Any]]:
-        """Return only anchor calls made directly by a real main scope."""
-
-        return [
-            event
-            for event in self.named(
-                name,
-                event_type="tool_call",
-                successful=successful,
-                bounds=bounds,
-            )
-            if self.is_main_direct_event(event)
-        ]
-
-    def is_main_direct_event(self, event: dict[str, Any]) -> bool:
-        return (
-            str(event.get("agent_role") or "") == "main"
-            and integer(event.get("agent_depth")) == 0
-            and integer(event.get("scope_event_id")) in self._main_scope_ids
-        )
-
-    def main_direct_reads(
-        self,
-        *,
-        scope_id: int | None = None,
-        names: set[str] | frozenset[str] | None = None,
-        bounds: EventBounds | None = None,
-        successful: bool | None = True,
-    ) -> list[dict[str, Any]]:
-        allowed = names or MAIN_DECISION_READ_TOOLS
-        return [
-            event
-            for event in self._ordered
-            if str(event.get("event_type") or "") == "tool_call"
-            and str(event.get("event_name") or "") in allowed
-            and self.is_main_direct_event(event)
-            and (scope_id is None or integer(event.get("scope_event_id")) == scope_id)
-            and _within_bounds(event, bounds)
-            and (successful is None or event_succeeded(event) is successful)
-        ]
-
-    def agent_returns(
-        self,
-        name: str,
-        *,
-        round_index: int | None = None,
-        before_seq: int | None = None,
-    ) -> list[tuple[dict[str, Any], str]]:
-        """Return evaluator calls and their honest association strength.
-
-        ``returned-report`` requires an explicit parent main scope.  A matching
-        structured round without that hierarchy is only ``runtime-associated``.
-        Report text is never inspected to infer its round.
-        """
-
-        results: list[tuple[dict[str, Any], str]] = []
-        for event in self.named(name, event_type="agent_invoke", successful=True):
-            if before_seq is not None and event_sequence(event) >= before_seq:
-                continue
-            if round_index is not None and integer(event.get("round_index")) != round_index:
-                continue
-            parent_id = integer(event.get("parent_event_id"))
-            relation = (
-                "returned-report"
-                if parent_id in self._main_scope_ids
-                else "runtime-associated"
-            )
-            results.append((event, relation))
-        return results
-
-    def resolved_begin_round(self, event: dict[str, Any]) -> int | None:
-        """Resolve the newly opened round, never trusting the pre-call context first."""
-
-        output = event_output(event)
-        if isinstance(output, dict):
-            round_index = integer(output.get("round_index"))
-            if output.get("success") is not False and round_index is not None:
-                return round_index
-
-        seq = event_sequence(event)
-        scope_id = integer(event.get("scope_event_id"))
-        for marker in self._ordered:
-            marker_seq = event_sequence(marker)
-            if marker_seq <= seq:
-                continue
-            if str(marker.get("event_type") or "") == "tool_call" and str(
-                marker.get("event_name") or ""
-            ) == "begin_round":
-                break
-            if str(marker.get("event_type") or "") != "round_begin":
-                continue
-            marker_scope = integer(marker.get("scope_event_id"))
-            if scope_id is not None and marker_scope not in {None, scope_id}:
-                continue
-            payload = marker.get("payload")
-            round_index = integer(payload.get("round_index")) if isinstance(payload, dict) else None
-            return round_index or integer(marker.get("event_name"))
-        return None
-
-    def successful_begin_rounds(self) -> dict[int, list[dict[str, Any]]]:
-        result: dict[int, list[dict[str, Any]]] = defaultdict(list)
-        for event in self.anchors("begin_round", successful=True):
-            round_index = self.resolved_begin_round(event)
-            if round_index is not None:
-                result[round_index].append(event)
-        return dict(result)
-
-
-def is_main_scope(event: dict[str, Any]) -> bool:
-    return (
-        str(event.get("event_type") or "") == "agent_scope"
-        and str(event.get("event_name") or "") == "main"
-        and str(event.get("agent_role") or "") == "main"
-        and integer(event.get("agent_depth")) == 0
-    )
-
-
-def event_actor(event: dict[str, Any]) -> str:
-    role = str(event.get("agent_role") or "")
-    name = str(event.get("event_name") or "")
-    if role == "main" and integer(event.get("agent_depth")) == 0:
-        return "main"
-    if role == "script_evaluator" or name == "script_evaluator":
-        return "overall-evaluator"
-    if role == "script_multipath_evaluator" or name == "script_multipath_evaluator":
-        return "multipath-evaluator"
-    return role or "unknown"
-
-
-def event_order(event: dict[str, Any]) -> tuple[int, int]:
-    return (event_sequence(event), integer(event.get("id")) or 0)
-
-
-def event_sequence(event: dict[str, Any]) -> int:
-    return integer(event.get("event_seq")) or 0
-
-
-def event_succeeded(event: dict[str, Any]) -> bool:
-    status = str(event.get("status") or "").lower()
-    if status in _FAILURE_STATUSES or status == "running":
-        return False
-    output = event_output(event)
-    if isinstance(output, dict):
-        if output.get("success") is False:
-            return False
-        if output.get("error") not in (None, "", False, [], {}):
-            return False
-    return status in _SUCCESS_STATUSES or bool(event.get("ended_at"))
-
-
-def event_input(event: dict[str, Any]) -> Any:
-    if "inputData" in event:
-        return event.get("inputData")
-    return _json_value(event.get("input_preview"))
-
-
-def event_output(event: dict[str, Any]) -> Any:
-    for key in ("agentOutputData", "outputData"):
-        if key in event and event.get(key) is not None:
-            return _unwrap_output(event.get(key))
-    return _unwrap_output(_json_value(event.get("output_preview")))
-
-
-def event_text_output(event: dict[str, Any]) -> str | None:
-    output = event_output(event)
-    if isinstance(output, dict):
-        for key in ("summary", "report", "content", "message"):
-            value = output.get(key)
-            if isinstance(value, str) and value.strip():
-                return value.strip()
-        return None
-    return output.strip() if isinstance(output, str) and output.strip() else None
-
-
-def detail_ref(event: dict[str, Any]) -> str | None:
-    event_id = integer(event.get("id"))
-    return f"event:{event_id}" if event_id is not None else None
-
-
-def event_summary(
-    event: dict[str, Any], association: str | None
-) -> dict[str, Any]:
-    """Return a lightweight shared event reference without full body data."""
-
-    event_id = integer(event.get("id"))
-    return {
-        "id": f"event:{event_id}" if event_id is not None else "event:unknown",
-        "eventId": event_id,
-        "eventSeq": event.get("event_seq"),
-        "type": event.get("event_type"),
-        "name": event.get("event_name"),
-        "status": event.get("status"),
-        "title": event.get("title"),
-        "inputPreview": event.get("input_preview"),
-        "outputPreview": event.get("output_preview"),
-        "startedAt": event.get("started_at"),
-        "endedAt": event.get("ended_at"),
-        "association": association,
-        "detailRef": f"event:{event_id}" if event_id is not None else None,
-    }
-
-
-def integer(value: Any) -> int | None:
-    try:
-        return int(value) if value is not None else None
-    except (TypeError, ValueError):
-        return None
-
-
-def _within_bounds(event: dict[str, Any], bounds: EventBounds | None) -> bool:
-    if bounds is None:
-        return True
-    seq = event_sequence(event)
-    if bounds.after_seq is not None and seq <= bounds.after_seq:
-        return False
-    if bounds.before_seq is not None and seq >= bounds.before_seq:
-        return False
-    return True
-
-
-def _json_value(value: Any) -> Any:
-    if not isinstance(value, str):
-        return value
-    text = value.strip()
-    if not text:
-        return None
-    try:
-        return json.loads(text)
-    except json.JSONDecodeError:
-        return value
-
-
-def _unwrap_output(value: Any) -> Any:
-    # Tool bodies currently store many JSON results as a JSON-looking string.
-    # Unwrap at most twice so malformed or ordinary prose remains untouched.
-    result = value
-    for _ in range(2):
-        parsed = _json_value(result)
-        if parsed is result or parsed == result:
-            break
-        result = parsed
-    return result

+ 0 - 85
visualization/backend/app/runtime_event_projection.py

@@ -1,85 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime, timezone
-from typing import Any, Iterable
-
-from .evaluation_event_projection import EvaluationEventProjector
-from .creative_event_projection import CreativeEventProjector
-from .main_agent_decision_projection import MainAgentDecisionProjector
-from .retrieval_event_projection import RetrievalEventProjector
-from .runtime_event_index import RuntimeEventIndex
-
-
-class RuntimeEventProjector:
-    """Facade that builds one event index and composes business projections."""
-
-    def __init__(
-        self,
-        *,
-        retrieval_projector: RetrievalEventProjector | None = None,
-        evaluation_projector: EvaluationEventProjector | None = None,
-        main_agent_projector: MainAgentDecisionProjector | None = None,
-        creative_projector: CreativeEventProjector | None = None,
-    ):
-        self._retrieval_projector = retrieval_projector or RetrievalEventProjector()
-        self._evaluation_projector = evaluation_projector or EvaluationEventProjector()
-        self._main_agent_projector = main_agent_projector or MainAgentDecisionProjector()
-        self._creative_projector = creative_projector or CreativeEventProjector()
-
-    def project(
-        self,
-        events: list[dict[str, Any]],
-        *,
-        valid_rounds: set[int],
-        valid_branches: set[int],
-        captured_at: datetime | None = None,
-        run_status: str | None = None,
-        script_direction: str | None = None,
-        rounds: Iterable[dict[str, Any]] = (),
-        multipath_decisions: Iterable[dict[str, Any]] = (),
-    ) -> dict[str, Any]:
-        captured_at = captured_at or datetime.now(timezone.utc)
-        index = RuntimeEventIndex(events)
-        retrieval = self._retrieval_projector.project(
-            index,
-            valid_rounds=valid_rounds,
-            valid_branches=valid_branches,
-            captured_at=captured_at,
-            run_status=run_status,
-        )
-        evaluations = self._evaluation_projector.project(
-            index,
-            valid_rounds=valid_rounds,
-            valid_branches=valid_branches,
-        )
-        main_agent = self._main_agent_projector.project(
-            index,
-            script_direction=script_direction,
-            rounds=rounds,
-            multipath_decisions=multipath_decisions,
-        )
-        creative = self._creative_projector.project(
-            index,
-            valid_rounds=valid_rounds,
-            valid_branches=valid_branches,
-        )
-        unassigned = [
-            *retrieval.get("unassigned", []),
-            *evaluations.get("unassigned", []),
-            *main_agent.get("unassigned", []),
-            *creative.get("unassigned", []),
-        ]
-        return {
-            "multipathReviewsByRound": evaluations.get(
-                "multipathReviewsByRound", {}
-            ),
-            "overallEvaluationsByRound": evaluations.get(
-                "overallEvaluationsByRound", {}
-            ),
-            "retrievalStagesByBranch": retrieval.get(
-                "retrievalStagesByBranch", {}
-            ),
-            "mainAgentDecisions": main_agent,
-            "creativeByBranch": creative.get("creativeByBranch", {}),
-            "unassigned": unassigned,
-        }

+ 0 - 69
visualization/backend/app/runtime_tool_catalog.py

@@ -1,69 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass
-
-
-@dataclass(frozen=True)
-class ToolDefinition:
-    """Stable business metadata for one runtime tool.
-
-    The event projection layer owns this translation.  Business projectors and
-    UI adapters should never maintain their own copies of tool-name mappings.
-    """
-
-    name: str
-    business_label: str
-    category: str
-
-
-_TOOLS: tuple[ToolDefinition, ...] = (
-    ToolDefinition("get_topic_detail", "选题", "decision-input"),
-    ToolDefinition("get_script_direction", "创作总目标", "decision-input"),
-    ToolDefinition("get_script_snapshot", "当前主脚本", "decision-input"),
-    ToolDefinition("get_account_script_section_patterns", "账号段落模式", "decision-input"),
-    ToolDefinition("get_account_script_persona_points", "账号人设", "decision-input"),
-    ToolDefinition("get_domain_info", "领域信息", "decision-input"),
-    ToolDefinition("save_script_direction", "保存创作目标", "decision-anchor"),
-    ToolDefinition("begin_round", "开启新一轮", "decision-anchor"),
-    ToolDefinition("record_multipath_plan", "记录实现规划", "decision-anchor"),
-    ToolDefinition("implement_paths", "并发派发多路实现", "orchestration"),
-    ToolDefinition("record_multipath_decision", "记录多路决策", "decision-anchor"),
-    ToolDefinition("record_data_decision", "记录数据取舍", "creative-action"),
-    ToolDefinition("record_task_plan", "记录任务规划", "creative-action"),
-    ToolDefinition("update_task_plan_step", "更新规划步骤", "creative-action"),
-    ToolDefinition("get_task_plan", "查看任务规划", "creative-action"),
-    ToolDefinition("create_script_paragraph", "新增脚本段落", "creative-action"),
-    ToolDefinition("batch_update_script_paragraphs", "更新候选脚本内容", "creative-action"),
-    ToolDefinition("append_paragraph_atoms", "补充段落原子点", "creative-action"),
-    ToolDefinition("create_script_element", "新增脚本元素", "creative-action"),
-    ToolDefinition("batch_create_script_elements", "批量新增脚本元素", "creative-action"),
-    ToolDefinition("load_images", "查看图片", "creative-action"),
-)
-
-TOOL_CATALOG: dict[str, ToolDefinition] = {item.name: item for item in _TOOLS}
-
-MAIN_DECISION_READ_TOOLS: frozenset[str] = frozenset(
-    item.name for item in _TOOLS if item.category == "decision-input"
-)
-
-MAIN_DECISION_ANCHORS: frozenset[str] = frozenset(
-    item.name for item in _TOOLS if item.category == "decision-anchor"
-)
-
-RETRIEVAL_AGENT_LABELS: dict[str, str] = {
-    "retrieve_data_decode_case": "解构 Case Agent",
-    "retrieve_data_external_search": "外部搜索 Agent",
-    "retrieve_data_knowledge": "知识 Agent",
-    "retrieve_data_pattern_relation": "Pattern / Relation Agent",
-}
-
-
-def business_tool_label(name: str) -> str:
-    """Return a non-technical label without inventing an unknown translation."""
-
-    definition = TOOL_CATALOG.get(str(name or ""))
-    return definition.business_label if definition else "运行记录"
-
-
-def tools_in_category(category: str) -> frozenset[str]:
-    return frozenset(item.name for item in _TOOLS if item.category == category)

+ 0 - 51
visualization/backend/app/sanitizer.py

@@ -1,51 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-from typing import Any
-
-
-_SECRET_PATTERNS = (
-    re.compile(
-        r"(?i)([\"']?authorization[\"']?\s*[:=]\s*[\"']?)(?:(?:bearer|basic)\s+)?([^\s,\"'\]}]+)"
-    ),
-    re.compile(
-        r"(?i)([\"']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|secret)[\"']?\s*[:=]\s*[\"']?)([^\s,\"'\]}]+)"
-    ),
-    re.compile(r"(?i)(mysql(?:\+pymysql)?://[^:/\s]+:)([^@\s]+)(@)"),
-    re.compile(r"(?i)((?:postgres(?:ql)?|redis|mongodb(?:\+srv)?|amqp)://[^:/\s]+:)([^@\s]+)(@)"),
-)
-
-
-def redact_text(value: str) -> str:
-    result = value
-    for pattern in _SECRET_PATTERNS:
-        if pattern.groups == 3:
-            result = pattern.sub(r"\1***\3", result)
-        else:
-            result = pattern.sub(r"\1***", result)
-    return result
-
-
-def sanitize(value: Any, *, max_text: int | None = 120_000) -> Any:
-    if isinstance(value, str):
-        redacted = redact_text(value)
-        if max_text is not None and len(redacted) > max_text:
-            return redacted[:max_text] + f"\n…[truncated {len(redacted) - max_text} chars]"
-        return redacted
-    if isinstance(value, dict):
-        result = {}
-        for key, nested in value.items():
-            key_text = str(key)
-            if re.search(r"(?i)(authorization|api[_-]?key|access[_-]?token|secret|password|credential|dsn|database[_-]?url|connection[_-]?string|data[_-]?source[_-]?url)", key_text):
-                result[key_text] = "***"
-            else:
-                result[key_text] = sanitize(nested, max_text=max_text)
-        return result
-    if isinstance(value, (list, tuple)):
-        return [sanitize(v, max_text=max_text) for v in value]
-    try:
-        json.dumps(value)
-        return value
-    except TypeError:
-        return str(value)

+ 0 - 5
visualization/backend/app/source_lineage/__init__.py

@@ -1,5 +0,0 @@
-"""Unified business/source/runtime projection for the three-column Inspector."""
-
-from .projector import InspectorWorkbenchProjector, InspectorViewNotFound
-
-__all__ = ["InspectorWorkbenchProjector", "InspectorViewNotFound"]

+ 0 - 1052
visualization/backend/app/source_lineage/comparison.py

@@ -1,1052 +0,0 @@
-from __future__ import annotations
-
-import re
-from copy import deepcopy
-from typing import Any
-
-from .resolver import SourceResolver, pointer_lookup
-from .schema import SourceBinding, unresolved_binding
-
-
-# These mappings are keyed by the stable business projection shape, never by
-# fuzzy title matching.  Values point at the richer source modules built by the
-# card-specific projectors.
-_MODULE_MAP: dict[str, dict[str, list[str]]] = {
-    "objective": {
-        "block:direction": ["direction"],
-        "block:inputs": ["inputs"],
-    },
-    "round-goal": {
-        "block:goal": ["goal", "why"],
-        "block:dependencies": ["dependencies"],
-    },
-    "implementation-plan": {
-        "block:plan": ["mode", "routes", "revisions"],
-        "block:basis": ["basis", "revisions"],
-    },
-    "implementation-task": {
-        "section:scope": ["scope"],
-        "section:method": ["method"],
-        "section:goal": ["goal"],
-        "section:constraints": ["constraints"],
-        "section:avoid": ["avoid"],
-        "section:deliverable": ["deliverable"],
-        "section:action": ["action"],
-        "section:request": ["request"],
-        "section:result": ["result"],
-    },
-    "creative": {
-        "block:basis": ["basis"],
-        "block:process": ["process"],
-        "block:available": ["available"],
-        "block:changes": ["changes"],
-        "block:candidate": ["candidate"],
-        "block:uncertainty": ["uncertainty"],
-        "block:record-note": ["record-note"],
-    },
-    "data-decision": {
-        "block:0": ["sources"],
-        "block:1": ["decision", "tradeoff"],
-        "block:2": ["reason"],
-    },
-    "multipath-decision": {
-        "block:0": ["strength"],
-        "block:1": ["scope"],
-        "block:2": ["comparison"],
-        "block:3": ["comparison"],
-        "block:4": ["reason"],
-        "block:5": ["strength"],
-    },
-    "domain-facts": {
-        "section:output": ["facts", "verification", "sources", "usage"],
-        "section:assessment": [],
-    },
-    "candidate-output-fallback": {
-        "section:output": ["output"],
-        "section:assessment": ["assessment"],
-    },
-    "round-result": {
-        "section:goal": ["completion"],
-        "section:outcomes": ["dispositions"],
-        "section:facts": ["domain"],
-        "section:evaluation": ["review"],
-        "section:changes": ["changes"],
-        "section:next": ["next"],
-    },
-    "final-result": {
-        "section:conclusion": ["conclusion"],
-        "section:summary": ["summary"],
-        "section:statistics": ["stats"],
-        "section:artifact": ["scale", "artifact"],
-        "section:version": ["version"],
-    },
-    "retrieval-agent": {
-        "section:task": ["task"],
-        "section:queries": ["queries"],
-        "section:hits": ["raw"],
-        "section:screening": ["screening"],
-        "special:activities": ["activities"],
-    },
-    "retrieval-direct-group": {
-        "section:purpose": ["purpose"],
-        "section:sources": ["calls"],
-        "section:result": ["results", "failures"],
-        "special:activities": ["calls", "results", "failures"],
-    },
-    "retrieval-event": {
-        "special:conditions": ["purpose"],
-        "special:outcome": ["status"],
-        "special:results": ["result", "results"],
-        "special:completeness": ["status"],
-    },
-}
-
-
-def business_only_projection(detail: dict[str, Any]) -> dict[str, Any]:
-    """Keep exactly what the readable Inspector uses, without duplicating raw JSON."""
-    hidden = {"technical", "changes"}
-    return deepcopy({key: value for key, value in detail.items() if key not in hidden})
-
-
-def align_business_projection(
-    *,
-    detail_ref: str,
-    card_kind: str,
-    business: dict[str, Any],
-    source_modules: list[dict[str, Any]],
-    resolver: SourceResolver,
-    bundle: dict[str, Any],
-) -> list[dict[str, Any]]:
-    """Turn the readable Inspector projection into row-aligned source modules."""
-    projection = business_only_projection(business)
-    normalized = _normalize_business(projection, card_kind=card_kind)
-    source_by_id = {str(module.get("id")): module for module in source_modules}
-    aligned: list[dict[str, Any]] = []
-    for module_index, module in enumerate(normalized, 1):
-        semantic_key = str(module.pop("_semanticKey"))
-        source_ids = _source_module_ids(card_kind, semantic_key, module_index)
-        candidates = [source_by_id[source_id] for source_id in source_ids if source_id in source_by_id]
-        if not candidates and card_kind == "implementation-task" and "historical-task" in source_by_id:
-            candidates = [source_by_id["historical-task"]]
-        rows: list[dict[str, Any]] = []
-        module_bindings: list[SourceBinding] = []
-        for row_index, row in enumerate(module.get("rows") or [], 1):
-            found, business_value = pointer_lookup(
-                projection, str(row.get("businessSelector") or "")
-            )
-            if card_kind == "domain-facts" and semantic_key == "section:assessment":
-                branch_id = _branch_id(detail_ref)
-                branch = next((item for item in bundle.get("branches") or [] if _integer(item.get("branch_id")) == branch_id), None)
-                bindings = [resolver.binding_for_field({
-                    "id": f"branch:{branch_id}:assessment",
-                    "label": "实现者说明",
-                    "value": (branch or {}).get("self_assessment"),
-                    "source": {
-                        "kind": "database",
-                        "label": "script_build_branch",
-                        "ref": detail_ref,
-                        "fieldPath": "self_assessment",
-                    },
-                    "relation": "persisted-output",
-                    "completeness": "complete" if branch and branch.get("self_assessment") else "partial",
-                }, binding_id=f"{row['id']}:source:1", role="output")]
-            elif semantic_key in {"special:question", "block:record-note"}:
-                bindings = [resolver.binding_for_field({
-                    "id": f"{module['id']}:display-text",
-                    "label": "业务详情确定性文案",
-                    "value": business_value if found else None,
-                    "source": {
-                        "kind": "calculation",
-                        "label": "业务详情确定性说明",
-                        "ref": f"calculation:{module['id']}:display-text",
-                        "fieldPath": "value",
-                    },
-                    "relation": "persisted-output",
-                    "completeness": "complete",
-                }, binding_id=f"{row['id']}:source:1", role="status")]
-            elif card_kind == "retrieval-event" and semantic_key in {
-                "special:outcome",
-                "special:completeness",
-            }:
-                event_id = _event_id(detail_ref)
-                output_key = str(row.get("id") or "").rsplit(":", 1)[-1]
-                path = "output.content" if semantic_key == "special:outcome" else ""
-                binding = resolver.binding_for_event(
-                    event_id or 0,
-                    binding_id=f"{row['id']}:source:1",
-                    path=path,
-                    role="output" if semantic_key == "special:outcome" else "status",
-                    availability=(
-                        "returned-to-agent"
-                        if semantic_key == "special:outcome"
-                        else "produced-by-run"
-                    ),
-                )
-                binding["transform"] = {
-                    "kind": "parsed",
-                    "operation": (
-                        "retrieval-outcome-v1"
-                        if semantic_key == "special:outcome"
-                        else "event-body-completeness-v1"
-                    ),
-                    "outputKey": output_key,
-                }
-                binding["evidence"]["confidence"] = "deterministic"
-                bindings = [binding]
-            elif semantic_key == "special:activities":
-                ref = ""
-                if found and isinstance(business_value, dict):
-                    ref = str(
-                        business_value.get("detailRef")
-                        or business_value.get("id")
-                        or ""
-                    )
-                event_id = _event_id(ref)
-                if event_id is not None:
-                    binding = resolver.binding_for_event(
-                        event_id,
-                        binding_id=f"{row['id']}:source:1",
-                        role="process",
-                        availability="produced-by-run",
-                    )
-                    binding["transform"] = {
-                        "kind": "parsed",
-                        "operation": "activity-summary-v1",
-                        "outputKey": str(row.get("id") or "activity"),
-                    }
-                    binding["evidence"]["confidence"] = "deterministic"
-                    bindings = [binding]
-                elif found and (
-                    business_value == []
-                    or str(row.get("businessSelector") or "")
-                    == "/missingActivityNotice"
-                ):
-                    empty_binding = resolver.binding_for_field(
-                        {
-                            "id": f"{row['id']}:empty",
-                            "label": "可见查询运行记录数量",
-                            "value": business_value,
-                            "source": {
-                                "kind": "calculation",
-                                "label": "取数 Agent 结构化查询记录确定性检查",
-                                "ref": f"calculation:{row['id']}:empty",
-                                "fieldPath": "value",
-                            },
-                            "relation": "persisted-output",
-                            "completeness": "complete",
-                        },
-                        binding_id=f"{row['id']}:source:1",
-                        role="status",
-                    )
-                    empty_source = resolver.sources.get(empty_binding["sourceId"])
-                    if empty_source is not None:
-                        empty_source["resultState"] = "empty"
-                        empty_source.setdefault("locator", {})["resultCount"] = 0
-                    bindings = [empty_binding]
-                else:
-                    bindings = []
-            else:
-                bindings = _bindings_for_row(
-                    module_id=str(module["id"]),
-                    row=row,
-                    row_index=row_index,
-                    candidates=candidates,
-                    resolver=resolver,
-                    business_value=business_value if found else None,
-                    narrow_event_text=card_kind == "implementation-task",
-                )
-                if card_kind == "implementation-plan" and semantic_key == "block:plan":
-                    revision = _implementation_plan_revision_binding(
-                        row=row,
-                        business_value=business_value if found else None,
-                        candidates=candidates,
-                        resolver=resolver,
-                    )
-                    if revision is not None:
-                        bindings.append(revision)
-            if not bindings:
-                binding = unresolved_binding(
-                    f"{module['id']}:row:{row_index}:unresolved",
-                    reason=f"业务详情「{module['title']}」的这一项没有保存可安全定位的原始记录",
-                    code="binding-map-missing",
-                )
-                resolver.add_unresolved_source(binding)
-                bindings = [binding]
-            _mark_row_transform(
-                bindings,
-                business_value if found else None,
-                resolver,
-                str(module["id"]),
-            )
-            row = {key: value for key, value in row.items() if not key.startswith("_")}
-            row["bindingIds"] = [binding["id"] for binding in bindings]
-            rows.append(row)
-            module_bindings.extend(bindings)
-        module["rows"] = rows
-        module["bindings"] = _unique_bindings(module_bindings)
-        module["runtimeRefs"] = list(dict.fromkeys(
-            binding["sourceId"]
-            for binding in module["bindings"]
-            if binding["sourceId"].startswith("event:")
-        ))
-        aligned.append(module)
-    return aligned
-
-
-def _source_module_ids(card_kind: str, semantic_key: str, index: int) -> list[str]:
-    explicit = _MODULE_MAP.get(card_kind, {}).get(semantic_key)
-    if explicit is not None:
-        return explicit
-    if semantic_key.startswith("section:"):
-        return [semantic_key.split(":", 1)[1]]
-    if semantic_key.startswith("block:"):
-        try:
-            return [f"block-{int(semantic_key.split(':', 1)[1]) + 1}"]
-        except ValueError:
-            return [f"block-{index}"]
-    return [semantic_key.split(":", 1)[-1]]
-
-
-def _normalize_business(
-    projection: dict[str, Any], *, card_kind: str = ""
-) -> list[dict[str, Any]]:
-    modules: list[dict[str, Any]] = []
-    question = projection.get("question")
-    if question not in (None, ""):
-        modules.append(_module(
-            "question",
-            "这一步要回答什么",
-            "/question",
-            "special:question",
-            [_row("question:value", None, "/question")],
-            default_open=True,
-        ))
-
-    for index, section in enumerate(projection.get("businessSections") or []):
-        if not isinstance(section, dict):
-            continue
-        section_id = str(section.get("id") or f"section-{index + 1}")
-        path = f"/businessSections/{index}"
-        rows = []
-        if isinstance(section.get("items"), list):
-            for item_index, item in enumerate(section.get("items") or []):
-                label = str((item or {}).get("label") or f"第 {item_index + 1} 项") if isinstance(item, dict) else f"第 {item_index + 1} 项"
-                item_path = f"{path}/items/{item_index}"
-                rows.append(_row(f"{section_id}:item:{item_index + 1}", label, item_path, item_index=item_index))
-        elif section.get("content") is not None:
-            rows.append(_row(f"{section_id}:content", None, f"{path}/content"))
-        semantic_id = _section_semantic_id(section_id)
-        modules.append(_module(
-            section_id,
-            str(section.get("title") or f"业务模块 {index + 1}"),
-            path,
-            f"section:{semantic_id}",
-            rows,
-            presentation=str(section.get("presentation") or "text"),
-            default_open=not bool(section.get("collapsible")) or bool(section.get("defaultOpen")),
-        ))
-
-    for index, block in enumerate(projection.get("blocks") or []):
-        if not isinstance(block, dict):
-            continue
-        path = f"/blocks/{index}"
-        semantic_id = _block_semantic_id(card_kind, block, index)
-        module_id = semantic_id or f"block-{index + 1}"
-        modules.append(_module(
-            module_id,
-            str(block.get("title") or "记录说明"),
-            path,
-            f"block:{semantic_id}" if semantic_id else f"block:{index}",
-            _block_rows(module_id, path, block),
-            presentation=str(block.get("presentation") or block.get("type") or "text"),
-            default_open=not bool(block.get("collapsible")) or bool(block.get("defaultOpen")),
-        ))
-
-    conditions = projection.get("queryConditions")
-    if projection.get("detailKind") == "retrieval-event" and isinstance(conditions, list):
-        rows = [
-            _row(
-                f"conditions:item:{index + 1}",
-                str(item.get("label") or item.get("key") or f"条件 {index + 1}"),
-                f"/queryConditions/{index}/value",
-                item_index=index,
-            )
-            for index, item in enumerate(conditions)
-            if isinstance(item, dict)
-        ]
-        if not rows:
-            rows = [_row("conditions:empty", None, "/queryConditions")]
-        modules.append(_module("conditions", "查询条件", "/queryConditions", "special:conditions", rows))
-
-    outcome = projection.get("outcome")
-    if isinstance(outcome, dict):
-        rows = []
-        for key, label in (("message", "执行结果"), ("count", "返回数量"), ("errorKind", "失败类型")):
-            if outcome.get(key) is not None:
-                rows.append(_row(f"outcome:{key}", label, f"/outcome/{key}"))
-        modules.append(_module("outcome", "查询结果", "/outcome", "special:outcome", rows))
-
-    result_items = projection.get("items")
-    if isinstance(result_items, list) and result_items:
-        rows = [
-            _row(
-                f"results:item:{index + 1}",
-                str((item or {}).get("title") or f"结果 {index + 1}") if isinstance(item, dict) else f"结果 {index + 1}",
-                f"/items/{index}",
-                item_index=index,
-            )
-            for index, item in enumerate(result_items)
-        ]
-        modules.append(_module("results", "返回结果", "/items", "special:results", rows))
-
-    activities = projection.get("activities")
-    if isinstance(activities, list):
-        rows = [
-            _row(
-                f"activities:item:{index + 1}",
-                str((item or {}).get("label") or f"运行记录 {index + 1}") if isinstance(item, dict) else f"运行记录 {index + 1}",
-                f"/activities/{index}",
-                item_index=index,
-            )
-            for index, item in enumerate(activities)
-        ]
-        if not rows:
-            notice_path = "/missingActivityNotice" if projection.get("missingActivityNotice") else "/activities"
-            rows = [_row("activities:empty", None, notice_path)]
-        modules.append(_module("activities", "运行记录", "/activities", "special:activities", rows, presentation="process"))
-
-    completeness = projection.get("completeness")
-    if projection.get("detailKind") == "retrieval-event" and isinstance(completeness, dict):
-        rows = []
-        if completeness.get("bodyAvailable") is False:
-            rows.append(_row("completeness:body", "完整返回", "/completeness/bodyAvailable"))
-        if completeness.get("truncated"):
-            rows.append(_row(
-                "completeness:truncated",
-                "截断说明",
-                "/completeness/omittedCharacters" if completeness.get("omittedCharacters") is not None else "/completeness/truncated",
-            ))
-        if rows:
-            modules.append(_module("completeness", "记录完整性", "/completeness", "special:completeness", rows, presentation="notice"))
-    return modules
-
-
-def _block_rows(module_id: str, path: str, block: dict[str, Any]) -> list[dict[str, Any]]:
-    block_type = str(block.get("type") or "summary")
-    if block_type in {"summary", "notice"}:
-        return [_row(f"{module_id}:value", None, f"{path}/value")]
-    if block_type == "items":
-        return [
-            _row(
-                f"{module_id}:item:{index + 1}",
-                str(
-                    item.get("label")
-                    or item.get("title")
-                    or item.get("subject")
-                    or item.get("summary")
-                    or f"第 {index + 1} 项"
-                ) if isinstance(item, dict) else f"第 {index + 1} 项",
-                f"{path}/items/{index}",
-                item_index=index,
-            )
-            for index, item in enumerate(block.get("items") or [])
-        ]
-    if block_type == "implementation-plan":
-        rows = []
-        if block.get("mode") is not None:
-            rows.append(_row(f"{module_id}:mode", "规划方式", f"{path}/mode", binding_slot="mode:0"))
-        for route_index, route in enumerate(block.get("routes") or []):
-            if not isinstance(route, dict):
-                continue
-            route_number = route.get("pathIndex") or route_index + 1
-            for key, label in (("target", "作用范围"), ("method", "实施方法"), ("action", "具体动作"), ("emphasis", "路线侧重"), ("pathType", "路线类型")):
-                if route.get(key) is None:
-                    continue
-                source_key = {"pathIndex": "path_index", "pathType": "path_type"}.get(key, key)
-                rows.append(_row(
-                    f"{module_id}:route:{route_index + 1}:{key}",
-                    f"路线 {route_number} · {label}",
-                    f"{path}/routes/{route_index}/{key}",
-                    binding_slot="routes:0",
-                    source_suffix=f"/{route_index}/{source_key}",
-                ))
-        if block.get("reasoning") is not None:
-            rows.append(_row(f"{module_id}:reasoning", "为什么这样规划", f"{path}/reasoning", binding_slot="mode:1"))
-        return rows
-    if block_type == "creative-process":
-        rows = []
-        for step_index, step in enumerate(block.get("steps") or []):
-            if not isinstance(step, dict):
-                continue
-            number = step.get("stepIndex") or step_index + 1
-            for key, label in (("action", "动作"), ("summary", "结果"), ("plan", "执行计划"), ("reasoning", "思考依据")):
-                if step.get(key) is not None:
-                    row = _row(f"{module_id}:step:{step_index + 1}:{key}", f"步骤 {number} · {label}", f"{path}/steps/{step_index}/{key}", item_index=step_index)
-                    row["_fieldKey"] = key
-                    rows.append(row)
-        return rows
-    if block_type == "evaluation-branches":
-        rows = []
-        for branch_index, branch in enumerate(block.get("branches") or []):
-            if not isinstance(branch, dict):
-                continue
-            subject = str(branch.get("subject") or f"方案 {branch_index + 1}")
-            for key, label in (("conclusion", "结论"), ("achievements", "已达成"), ("problems", "需关注"), ("evidence", "依据")):
-                if branch.get(key) not in (None, [], ""):
-                    rows.append(_row(f"{module_id}:branch:{branch_index + 1}:{key}", f"{subject} · {label}", f"{path}/branches/{branch_index}/{key}", item_index=branch_index))
-        return rows
-    if block_type == "evaluation-matrix":
-        return [
-            _row(f"{module_id}:row:{index + 1}", str((row or {}).get("criterion") or f"标准 {index + 1}"), f"{path}/rows/{index}", item_index=index)
-            for index, row in enumerate(block.get("rows") or [])
-        ]
-    if block_type == "comparison":
-        return [
-            _row(f"{module_id}:row:{index + 1}", str((row or {}).get("candidate") or f"方案 {index + 1}"), f"{path}/rows/{index}", item_index=index)
-            for index, row in enumerate(block.get("rows") or [])
-        ]
-    if block_type == "artifact":
-        return [
-            _row(f"{module_id}:artifact:{index + 1}", str((ref or {}).get("label") or f"产物 {index + 1}"), f"{path}/refs/{index}", item_index=index)
-            for index, ref in enumerate(block.get("refs") or [])
-        ]
-    return [_row(f"{module_id}:record", None, path)]
-
-
-def _bindings_for_row(
-    *,
-    module_id: str,
-    row: dict[str, Any],
-    row_index: int,
-    candidates: list[dict[str, Any]],
-    resolver: SourceResolver,
-    business_value: Any,
-    narrow_event_text: bool = False,
-) -> list[SourceBinding]:
-    slot = str(row.get("_bindingSlot") or "")
-    bindings: list[SourceBinding] = []
-    if slot:
-        source_id, _, raw_index = slot.partition(":")
-        source_module = next((module for module in candidates if str(module.get("id")) == source_id), None)
-        source_bindings = list((source_module or {}).get("bindings") or [])
-        try:
-            binding = source_bindings[int(raw_index or 0)]
-        except (IndexError, ValueError):
-            binding = None
-        if binding is not None:
-            cloned = _clone_binding(
-                binding,
-                binding_id=f"{row['id']}:source:1",
-                source_suffix=str(row.get("_sourceSuffix") or ""),
-                resolver=resolver,
-            )
-            bindings = [
-                _narrow_event_text_binding(cloned, business_value, resolver, module_id)
-                if narrow_event_text
-                else cloned
-            ]
-    if not bindings:
-        item_index = row.get("_itemIndex")
-        for source_module in candidates:
-            items = ((source_module.get("business") or {}).get("items") or [])
-            source_bindings = list(source_module.get("bindings") or [])
-            if isinstance(item_index, int) and item_index < len(items):
-                item = items[item_index] or {}
-                item_bindings = list(item.get("bindings") or [])
-                field_key = str(row.get("_fieldKey") or "")
-                if field_key and item_bindings:
-                    event_id = _event_id(item_bindings[0].get("sourceId"))
-                    event_paths = {
-                        "action": "input.content.action",
-                        "summary": "input.content.thought_summary",
-                        "plan": "input.content.plan",
-                        "reasoning": "input.content.thought",
-                    }
-                    if event_id and field_key in event_paths:
-                        binding = resolver.binding_for_event(
-                            event_id,
-                            binding_id=f"{row['id']}:source:1",
-                            path=event_paths[field_key],
-                            role="process",
-                        )
-                        found, selected = pointer_lookup(
-                            (resolver.sources.get(binding["sourceId"]) or {}).get("rawRecord"),
-                            str((binding.get("selector") or {}).get("path") or ""),
-                        )
-                        if not found or selected != business_value:
-                            binding["transform"] = {
-                                "kind": "parsed",
-                                "operation": "creative-step-business-label-v1",
-                                "outputKey": field_key,
-                            }
-                            binding["evidence"]["confidence"] = "deterministic"
-                        return [binding]
-                matched = _bindings_matching_value(
-                    item.get("value"), item_bindings, business_value
-                )
-                bindings.extend(matched or item_bindings)
-            elif (
-                isinstance(item_index, int)
-                and isinstance(((source_module.get("business") or {}).get("value")), list)
-                and item_index < len(source_bindings)
-            ):
-                # The readable Inspector may split one source list into one row
-                # per item while CardData keeps it as one aggregate module.
-                # The persisted source order is explicit, so bind the same
-                # index instead of repeating every source on every row.
-                bindings.append(source_bindings[item_index])
-            elif isinstance(item_index, int) and item_index < len(items):
-                bindings.extend((items[item_index] or {}).get("bindings") or [])
-        if not bindings:
-            bindings = [
-                binding
-                for source_module in candidates
-                for binding in source_module.get("bindings") or []
-            ]
-        physical_bindings = _unique_physical_bindings(bindings)
-        usable_bindings = [
-            binding
-            for binding in physical_bindings
-            if binding.get("resolution") != "missing"
-        ]
-        if usable_bindings:
-            physical_bindings = usable_bindings
-        bindings = [
-            _narrow_event_text_binding(cloned, business_value, resolver, module_id)
-            if narrow_event_text
-            else cloned
-            for index, binding in enumerate(physical_bindings, 1)
-            for cloned in [
-                _clone_binding(
-                binding,
-                binding_id=f"{row['id']}:source:{index}",
-                source_suffix="",
-                resolver=resolver,
-                )
-            ]
-        ]
-    return bindings
-
-
-def _mark_row_transform(
-    bindings: list[SourceBinding],
-    business_value: Any,
-    resolver: SourceResolver,
-    module_id: str,
-) -> None:
-    """Never call a normalized/combined business row a direct projection."""
-    mismatched: list[SourceBinding] = []
-    for binding in bindings:
-        if (binding.get("transform") or {}).get("kind") != "direct":
-            continue
-        source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
-        selector = binding.get("selector") or {}
-        selected: Any = None
-        found = False
-        if selector.get("kind") == "json-pointer":
-            found, selected = pointer_lookup(
-                source.get("rawRecord"), str(selector.get("path") or "")
-            )
-        elif selector.get("kind") == "whole-record":
-            found, selected = True, source.get("rawRecord")
-        elif selector.get("kind") == "text-span":
-            selected = selector.get("exactText")
-            found = True
-        if found and selected != business_value:
-            mismatched.append(binding)
-    for binding in mismatched:
-        binding["transform"] = (
-            {
-                "kind": "combined",
-                "operation": f"business-row:{module_id}",
-            }
-            if len(bindings) > 1
-            else {
-                "kind": "parsed",
-                "operation": "business-row-projection-v1",
-                "outputKey": module_id,
-            }
-        )
-        binding["evidence"]["confidence"] = "deterministic"
-
-
-def _section_semantic_id(section_id: str) -> str:
-    """Map task section instance ids to the stable business module ids."""
-    if not section_id.startswith("task-"):
-        return section_id
-    match = re.match(r"^task-([a-z-]+)-\d+$", section_id)
-    kind = match.group(1) if match else "notes"
-    return {
-        "scope": "scope",
-        "method": "method",
-        "goal": "goal",
-        "materials": "constraints",
-        "context": "constraints",
-        "constraints": "constraints",
-        "avoid": "avoid",
-        "notes": "deliverable",
-    }.get(kind, "deliverable")
-
-
-def _block_semantic_id(
-    card_kind: str, block: dict[str, Any], index: int
-) -> str | None:
-    """Return a stable semantic id for optional/reordered decision blocks."""
-    explicit = str(block.get("semanticId") or "").strip()
-    if explicit:
-        return explicit
-    block_type = str(block.get("type") or "").strip()
-    title = str(block.get("title") or "").strip()
-    by_title = ({
-        "当前保存的创作方向": "direction",
-        "最终方向": "direction",
-        "根据以下信息做出的决策": "inputs",
-    } if card_kind == "objective" else {
-        "明确依据": "basis",
-        "明确采用的数据取舍": "basis",
-        "创作处理": "process",
-        "可确认的上游信息": "available",
-        "实际脚本改动": "changes",
-        "实际产出": "candidate",
-        "产出的候选表": "candidate",
-        "存疑项": "uncertainty",
-        "记录说明": "record-note",
-    } if card_kind == "creative" else {
-        "本轮目标构成": "goal",
-        "根据以下信息做出的决策": "dependencies",
-    } if card_kind == "round-goal" else {
-        "本轮实施路线": "plan",
-        "根据以下信息做出的决策": "basis",
-    } if card_kind == "implementation-plan" else {})
-    if title in by_title:
-        return by_title[title]
-    if card_kind == "creative" and block_type == "creative-process":
-        return "process"
-    if card_kind == "creative" and block_type == "notice":
-        return "record-note"
-    return None
-
-
-def _narrow_event_text_binding(
-    binding: SourceBinding,
-    business_value: Any,
-    resolver: SourceResolver,
-    module_id: str = "",
-) -> SourceBinding:
-    """Narrow a complete implementation task binding to its exact visible section."""
-    if not isinstance(business_value, str) or not business_value.strip():
-        return binding
-    event_id = _event_id(binding.get("sourceId"))
-    selector = binding.get("selector") or {}
-    if event_id is None or selector.get("kind") != "json-pointer":
-        return binding
-    path = str(selector.get("path") or "")
-    source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
-    found, raw_text = pointer_lookup(source.get("rawRecord"), path)
-    if not found or not isinstance(raw_text, str):
-        return binding
-    exact_text = business_value if business_value in raw_text else _task_source_section(raw_text, module_id)
-    if not exact_text:
-        return binding
-    narrowed = resolver.text_span_binding(
-        event_id,
-        binding_id=str(binding.get("id") or "implementation-task:span"),
-        path=path.lstrip("/").replace("/", "."),
-        exact_text=exact_text,
-        role=str(binding.get("role") or "input"),
-    )
-    if exact_text != business_value:
-        narrowed["transform"] = {
-            "kind": "parsed",
-            "operation": "implementation-task-section-normalization-v1",
-            "outputKey": module_id,
-        }
-        narrowed["evidence"]["confidence"] = "deterministic"
-    return narrowed
-
-
-def _task_source_section(task: str, module_id: str) -> str | None:
-    """Return the exact raw task section behind a normalized business block."""
-    module_id = _section_semantic_id(module_id)
-    line_keys = {
-        "scope": ("target", "实现范围"),
-        "method": ("method", "实施方法"),
-    }
-    if module_id in line_keys:
-        keys = "|".join(re.escape(key) for key in line_keys[module_id])
-        match = re.search(rf"(?im)^(?:{keys})\s*[::]\s*(.+)$", task)
-        return match.group(1).strip() if match else None
-    headings = {
-        "goal": (r"目标",),
-        "constraints": (
-            r"取证方向与约束",
-            r"参考依据与约束",
-            r"严格约束",
-            r"约束(?:([^)]*))?",
-        ),
-        "avoid": (r"禁止事项", r"需要避免"),
-        "deliverable": (r"预期交付", r"交付"),
-    }
-    constraint_pattern = "|".join(headings["constraints"])
-    if module_id == "deliverable" and not re.search(
-        rf"(?im)^(?:{'|'.join(headings['deliverable'])})\s*[::]", task
-    ):
-        # Unlabelled task notes are the exact prefix before a later explicit
-        # constraint section.  The readable Inspector may normalize terms
-        # inside it, so retain the raw prefix as a parsed text span.
-        match = re.search(
-            rf"(?ims)^(.*?)(?=\n\s*\n(?:{constraint_pattern})\s*[::]|\Z)",
-            task,
-        )
-        return match.group(1).strip() if match else task.strip()
-    names = headings.get(module_id)
-    if not names:
-        return None
-    all_names = [name for values in headings.values() for name in values]
-    start_pattern = "|".join(names)
-    next_pattern = "|".join(all_names)
-    match = re.search(
-        rf"(?ims)^(?:{start_pattern})\s*[::]\s*(.*?)(?=\n\s*\n(?:{next_pattern})\s*[::]|\Z)",
-        task,
-    )
-    return match.group(1).strip() if match else None
-
-
-def _implementation_plan_revision_binding(
-    *,
-    row: dict[str, Any],
-    business_value: Any,
-    candidates: list[dict[str, Any]],
-    resolver: SourceResolver,
-) -> SourceBinding | None:
-    """Bind the displayed final plan row to the revision that formed it.
-
-    A round may call ``record_multipath_plan`` several times.  The database
-    value is authoritative, while the latest revision containing the exact
-    displayed value is the formation record.  This prevents round 4 from
-    stopping at Events 4301/4369 and omitting the final Event 4434.
-    """
-    revision = next(
-        (module for module in candidates if str(module.get("id")) == "revisions"),
-        None,
-    )
-    event_ids = sorted(
-        {
-            event_id
-            for binding in (revision or {}).get("bindings") or []
-            for event_id in [_event_id(binding.get("sourceId"))]
-            if event_id is not None
-        },
-        reverse=True,
-    )
-    row_id = str(row.get("id") or "")
-    path = ""
-    if row_id.endswith(":mode"):
-        path = "input.content.mode"
-    elif row_id.endswith(":reasoning"):
-        path = "input.content.note"
-    else:
-        match = re.search(r":route:(\d+):([A-Za-z]+)$", row_id)
-        if match:
-            route_index = int(match.group(1)) - 1
-            key = {"pathType": "path_type"}.get(match.group(2), match.group(2))
-            path = f"input.content.paths.{route_index}.{key}"
-    if not path:
-        return None
-    fallback_event: int | None = None
-    for event_id in event_ids:
-        event = resolver.event_details.get(event_id) or {}
-        found, selected = pointer_lookup(event, "/" + path.replace(".", "/"))
-        if not found:
-            continue
-        fallback_event = fallback_event or event_id
-        if selected != business_value:
-            continue
-        return resolver.binding_for_event(
-            event_id,
-            binding_id=f"{row_id}:revision",
-            path=path,
-            role="process",
-            availability="produced-by-run",
-        )
-    # The readable mode can normalize a one-route saved plan to "单路".
-    # Retain the latest concrete revision but mark that normalization openly.
-    if row_id.endswith(":mode") and fallback_event:
-        binding = resolver.binding_for_event(
-            fallback_event,
-            binding_id=f"{row_id}:revision",
-            path=path,
-            role="process",
-            availability="produced-by-run",
-        )
-        binding["transform"] = {
-            "kind": "parsed",
-            "operation": "single-route-mode-normalization-v1",
-            "outputKey": "mode",
-        }
-        binding["evidence"]["confidence"] = "deterministic"
-        return binding
-    return None
-
-
-def _bindings_matching_value(
-    source_value: Any,
-    bindings: list[SourceBinding],
-    business_value: Any,
-) -> list[SourceBinding]:
-    source_leaves = _leaf_texts(source_value)
-    wanted = set(_leaf_texts(business_value))
-    if not wanted or len(source_leaves) != len(bindings):
-        return []
-    return [
-        binding
-        for leaf, binding in zip(source_leaves, bindings)
-        if leaf in wanted
-    ]
-
-
-def _leaf_texts(value: Any) -> list[str]:
-    if isinstance(value, str):
-        return [value.strip()] if value.strip() else []
-    if isinstance(value, dict):
-        result: list[str] = []
-        for child in value.values():
-            result.extend(_leaf_texts(child))
-        return result
-    if isinstance(value, list):
-        result = []
-        for child in value:
-            result.extend(_leaf_texts(child))
-        return result
-    return [str(value)] if value is not None else []
-
-
-def _event_id(source_id: Any) -> int | None:
-    value = str(source_id or "")
-    if not value.startswith("event:"):
-        return None
-    try:
-        return int(value.split(":", 1)[1])
-    except ValueError:
-        return None
-
-
-def _clone_binding(
-    binding: SourceBinding,
-    *,
-    binding_id: str,
-    source_suffix: str,
-    resolver: SourceResolver,
-) -> SourceBinding:
-    cloned: SourceBinding = deepcopy(binding)
-    cloned["id"] = binding_id
-    selector = cloned.get("selector") or {}
-    if source_suffix and selector.get("kind") == "json-pointer":
-        pointer = f"{str(selector.get('path') or '').rstrip('/')}{source_suffix}"
-        source = resolver.sources.get(cloned["sourceId"]) or {}
-        found, selected = pointer_lookup(source.get("rawRecord"), pointer)
-        if found:
-            cloned["selector"] = {"kind": "json-pointer", "path": pointer}
-            source.setdefault("selectedValues", []).append({
-                "bindingId": binding_id,
-                "fieldId": binding_id,
-                "label": binding_id,
-                "path": pointer,
-                "value": selected,
-            })
-        else:
-            cloned["selector"] = {
-                "kind": "unresolved",
-                "reason": f"原始记录中无法定位字段 {pointer}",
-                "code": "field-path-mismatch",
-            }
-            cloned["resolution"] = "partial"
-            cloned["evidence"]["confidence"] = "unconfirmed"
-    return cloned
-
-
-def _module(
-    module_id: str,
-    title: str,
-    business_path: str,
-    semantic_key: str,
-    rows: list[dict[str, Any]],
-    *,
-    presentation: str = "text",
-    default_open: bool = True,
-) -> dict[str, Any]:
-    return {
-        "id": module_id,
-        "title": title,
-        "businessPath": business_path,
-        "presentation": presentation,
-        "rows": rows,
-        "defaultOpen": default_open,
-        "_semanticKey": semantic_key,
-    }
-
-
-def _row(
-    row_id: str,
-    label: str | None,
-    business_selector: str,
-    *,
-    item_index: int | None = None,
-    binding_slot: str | None = None,
-    source_suffix: str | None = None,
-) -> dict[str, Any]:
-    row: dict[str, Any] = {
-        "id": row_id,
-        "label": label,
-        "businessSelector": business_selector,
-    }
-    if item_index is not None:
-        row["_itemIndex"] = item_index
-    if binding_slot is not None:
-        row["_bindingSlot"] = binding_slot
-    if source_suffix is not None:
-        row["_sourceSuffix"] = source_suffix
-    return row
-
-
-def _unique_bindings(bindings: list[SourceBinding]) -> list[SourceBinding]:
-    result: list[SourceBinding] = []
-    seen: set[str] = set()
-    for binding in bindings:
-        key = str(binding.get("id") or "")
-        if not key or key in seen:
-            continue
-        seen.add(key)
-        result.append(binding)
-    return result
-
-
-def _unique_physical_bindings(bindings: list[SourceBinding]) -> list[SourceBinding]:
-    result: list[SourceBinding] = []
-    seen: set[str] = set()
-    for binding in bindings:
-        selector = binding.get("selector") or {}
-        key = "|".join((
-            str(binding.get("sourceId") or ""),
-            str(selector.get("kind") or ""),
-            str(selector.get("path") or selector.get("exactText") or selector.get("reason") or selector),
-            str(binding.get("role") or ""),
-        ))
-        if not key or key in seen:
-            continue
-        seen.add(key)
-        result.append(binding)
-    return result
-
-
-def _branch_id(value: str) -> int | None:
-    import re
-    match = re.search(r":branch:(\d+)", value)
-    return _integer(match.group(1)) if match else None
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value)
-    except (TypeError, ValueError):
-        return None

+ 0 - 2056
visualization/backend/app/source_lineage/projector.py

@@ -1,2056 +0,0 @@
-from __future__ import annotations
-
-import re
-from copy import deepcopy
-from typing import Any
-
-from ..card_details import CardBusinessDataProjector, CardDataNotFound
-from ..card_details.common import find_detail
-from ..inspector_projection import project_activity_detail, project_event_detail
-from ..module_audit.log_context import locate_log_module
-from ..retrieval_detail_projection import (
-    QUERY_TOOL_NAMES,
-    RETRIEVAL_RESULT_COLLECTION_KEYS,
-)
-from .comparison import align_business_projection, business_only_projection
-from .resolver import SourceResolver, pointer_value
-from .schema import SourceBinding, unresolved_binding
-
-
-class InspectorViewNotFound(LookupError):
-    pass
-
-
-class InspectorWorkbenchProjector:
-    """Application service that makes all three Inspector columns together."""
-
-    def __init__(self) -> None:
-        self._card_projector = CardBusinessDataProjector()
-
-    def related_event_ids(
-        self, detail_ref: str, *, bundle: dict[str, Any], view: dict[str, Any]
-    ) -> list[int]:
-        ref = str(detail_ref or "")
-        ids: set[int] = set()
-        if ref.startswith("event:"):
-            value = _suffix_int(ref)
-            if value:
-                ids.add(value)
-
-        node = find_detail(view, ref)
-        _collect_event_ids(node, ids)
-
-        if ref == "run:objective":
-            _collect_event_ids(view.get("objective"), ids)
-        round_match = re.fullmatch(r"round:(\d+):(goal|plan|result|evaluation)", ref)
-        if round_match:
-            round_index = int(round_match.group(1))
-            round_ = next(
-                (
-                    item
-                    for item in view.get("rounds") or []
-                    if _integer(item.get("roundIndex")) == round_index
-                ),
-                {},
-            )
-            role = round_match.group(2)
-            target = {
-                "goal": round_.get("goal"),
-                "plan": (round_.get("plan") or {}).get("node"),
-                "result": round_.get("overallEvaluation"),
-                "evaluation": round_.get("overallEvaluation"),
-            }.get(role)
-            _collect_event_ids(target, ids)
-            event_names = {
-                "goal": {"begin_round"},
-                "plan": {"record_multipath_plan"},
-            }.get(role, set())
-            for event in bundle.get("events") or []:
-                if (
-                    event.get("event_name") in event_names
-                    and (
-                        role == "goal"
-                        or _integer(event.get("round_index")) == round_index
-                    )
-                ):
-                    ids.add(_integer(event.get("id")) or 0)
-
-        branch_match = re.fullmatch(
-            r"round:(\d+):branch:(\d+):(task|retrieval|output)", ref
-        )
-        if branch_match:
-            round_index, branch_id = int(branch_match.group(1)), int(branch_match.group(2))
-            branch = _view_branch(view, round_index, branch_id)
-            _collect_event_ids(branch, ids)
-
-        if ref.startswith("creative:"):
-            seed_id = _suffix_int(ref)
-            seed = _bundle_event(bundle, seed_id)
-            round_index = _integer((seed or {}).get("round_index"))
-            branch_id = _integer((seed or {}).get("branch_id"))
-            implementers = {
-                _integer(event.get("id"))
-                for event in bundle.get("events") or []
-                if event.get("event_name") == "script_implementer"
-                and _integer(event.get("round_index")) == round_index
-                and _integer(event.get("branch_id")) == branch_id
-            }
-            for event in bundle.get("events") or []:
-                if _integer(event.get("round_index")) != round_index or _integer(event.get("branch_id")) != branch_id:
-                    continue
-                event_id = _integer(event.get("id"))
-                if event_id in implementers or _integer(event.get("scope_event_id")) in implementers:
-                    ids.add(event_id or 0)
-
-        if ref.startswith("multipath-decision:"):
-            decision_id = _suffix_int(ref)
-            decision = next(
-                (
-                    item
-                    for item in bundle.get("multipathDecisions") or []
-                    if _integer(item.get("id")) == decision_id
-                ),
-                {},
-            )
-            round_index = _integer(decision.get("round_index"))
-            for event in bundle.get("events") or []:
-                if (
-                    event.get("event_name") == "script_multipath_evaluator"
-                    and _integer(event.get("round_index")) == round_index
-                ):
-                    ids.add(_integer(event.get("id")) or 0)
-
-        if ref.startswith("retrieval-agent:"):
-            agent_id = _suffix_int(ref)
-            for event in bundle.get("events") or []:
-                if (
-                    _integer(event.get("id")) == agent_id
-                    or _integer(event.get("parent_event_id")) == agent_id
-                    or _integer(event.get("scope_event_id")) == agent_id
-                ):
-                    ids.add(_integer(event.get("id")) or 0)
-
-        return sorted(value for value in ids if value > 0)
-
-    def project(
-        self,
-        detail_ref: str,
-        *,
-        script_build_id: int,
-        bundle: dict[str, Any],
-        view: dict[str, Any],
-        event_details: dict[int, dict[str, Any]],
-        log_contents: dict[int, str] | None = None,
-    ) -> dict[str, Any]:
-        ref = str(detail_ref or "").strip()
-
-        def event_loader(event_id: int) -> dict[str, Any]:
-            event = event_details.get(int(event_id))
-            if event is None:
-                raise KeyError(event_id)
-            return event
-
-        business = self._business_detail(
-            ref,
-            script_build_id=script_build_id,
-            bundle=bundle,
-            view=view,
-            event_details=event_details,
-        )
-        try:
-            card_data = self._card_projector.project(
-                ref,
-                bundle=bundle,
-                view=view,
-                load_event=event_loader,
-            )
-        except (CardDataNotFound, KeyError):
-            card_data = _fallback_card_data(ref, business)
-
-        resolver = SourceResolver(bundle=bundle, event_details=event_details)
-        source_modules = self._modules(ref, business, card_data, resolver, event_details, bundle)
-        if not source_modules:
-            raise InspectorViewNotFound(f"未找到活动 {ref}")
-        alignment_kind = (
-            "retrieval-event"
-            if business.get("detailKind") == "retrieval-event"
-            else str(card_data.get("cardKind") or _fallback_card_kind(ref))
-        )
-        modules = align_business_projection(
-            detail_ref=ref,
-            card_kind=alignment_kind,
-            business=business,
-            source_modules=source_modules,
-            resolver=resolver,
-            bundle=bundle,
-        )
-        if not modules:
-            raise InspectorViewNotFound(f"活动 {ref} 没有可展示的业务详情")
-        if log_contents:
-            log_refs: dict[int, str] = {}
-            for event_id, event in event_details.items():
-                log_module = locate_log_module(log_contents.get(event_id) or "", event)
-                if log_module:
-                    log_refs[event_id] = resolver.add_log_anchor(
-                        event_id,
-                        log_module,
-                        event_msg_id=str(event.get("msg_id") or ""),
-                    )
-            for module in modules:
-                runtime_refs = list(module.get("runtimeRefs") or [])
-                for event_id, log_ref in log_refs.items():
-                    if f"event:{event_id}" in runtime_refs and log_ref not in runtime_refs:
-                        runtime_refs.append(log_ref)
-                module["runtimeRefs"] = runtime_refs
-
-        completeness = str(card_data.get("completeness") or "partial")
-        if any(
-            binding.get("resolution") != "resolved"
-            for binding in _all_bindings(modules)
-        ):
-            completeness = "partial" if completeness != "missing" else "missing"
-        notices = [
-            *[item for item in card_data.get("notices") or [] if isinstance(item, dict)],
-            *_business_notices(business),
-        ]
-        used_source_ids = _used_source_ids(modules)
-        return {
-            "schemaVersion": "inspector-source-v2",
-            "detailRef": ref,
-            "cardKind": card_data.get("cardKind") or _fallback_card_kind(ref),
-            "completeness": completeness,
-            "businessProjection": business_only_projection(business),
-            "modules": modules,
-            "sources": {
-                source_id: source
-                for source_id, source in resolver.sources.items()
-                if source_id in used_source_ids
-            },
-            "notices": _unique_notices(notices),
-        }
-
-    def _business_detail(
-        self,
-        detail_ref: str,
-        *,
-        script_build_id: int,
-        bundle: dict[str, Any],
-        view: dict[str, Any],
-        event_details: dict[int, dict[str, Any]],
-    ) -> dict[str, Any]:
-        if detail_ref.startswith("event:"):
-            event_id = _suffix_int(detail_ref)
-            event = event_details.get(event_id or -1)
-            if event is None:
-                raise InspectorViewNotFound(f"未找到运行事件 {event_id}")
-            try:
-                return project_activity_detail(
-                    script_build_id,
-                    detail_ref,
-                    view,
-                    bundle,
-                    event_detail=event,
-                )
-            except KeyError:
-                return project_event_detail(
-                    script_build_id,
-                    event,
-                    run_status=((view.get("header") or {}).get("status")),
-                )
-        event_detail = None
-        if detail_ref.startswith("retrieval-agent:"):
-            run = find_detail(view, detail_ref) or {}
-            event_detail = event_details.get(_integer(run.get("eventId")) or -1)
-        try:
-            return project_activity_detail(
-                script_build_id,
-                detail_ref,
-                view,
-                bundle,
-                event_detail=event_detail,
-            )
-        except KeyError as exc:
-            raise InspectorViewNotFound(f"未找到活动 {detail_ref}") from exc
-
-    def _modules(
-        self,
-        detail_ref: str,
-        business: dict[str, Any],
-        card_data: dict[str, Any],
-        resolver: SourceResolver,
-        event_details: dict[int, dict[str, Any]],
-        bundle: dict[str, Any],
-    ) -> list[dict[str, Any]]:
-        if detail_ref.startswith("retrieval-direct:"):
-            return _direct_group_modules(card_data, resolver, event_details)
-        if detail_ref.startswith("retrieval-agent:"):
-            return _retrieval_agent_modules(card_data, resolver, event_details)
-        special = self._event_modules(detail_ref, business, resolver, event_details)
-        if special is not None:
-            return special
-
-        business_values = _business_modules(business)
-        planned = [
-            item
-            for item in (card_data.get("displayUse") or {}).get("businessModules") or []
-            if isinstance(item, dict)
-        ]
-        fields = {
-            str(item.get("id")): item
-            for item in [
-                *(card_data.get("businessInputs") or []),
-                *(card_data.get("businessOutputs") or []),
-            ]
-            if isinstance(item, dict) and item.get("id")
-        }
-        if card_data.get("cardKind") == "implementation-task" and not any(
-            str((field.get("source") or {}).get("kind") or "") == "runtime-event"
-            for field in fields.values()
-        ):
-            return _historical_implementation_task_modules(fields, resolver)
-        modules: list[dict[str, Any]] = []
-        consumed: set[str] = set()
-        for index, plan in enumerate(planned, 1):
-            module_id = str(plan.get("id") or f"module-{index}")
-            title = str(plan.get("title") or f"业务模块 {index}")
-            business_module = _match_business_module(business_values, module_id, title)
-            if business_module:
-                consumed.add(str(business_module.get("id")))
-            source_ids = [str(value) for value in plan.get("sourceIds") or []]
-            bindings = [
-                resolver.binding_for_field(
-                    fields[source_id],
-                    binding_id=f"{module_id}:source:{position}",
-                )
-                for position, source_id in enumerate(source_ids, 1)
-                if source_id in fields
-            ]
-            if card_data.get("cardKind") == "implementation-task" and business_module:
-                task_field = next(
-                    (
-                        fields[source_id]
-                        for source_id in source_ids
-                        if source_id in fields
-                        and str((fields[source_id].get("source") or {}).get("kind")) == "runtime-event"
-                        and str((fields[source_id].get("source") or {}).get("fieldPath")) == "input.content.task"
-                    ),
-                    None,
-                )
-                task_event_id = _suffix_int(str((task_field or {}).get("source", {}).get("ref") or ""))
-                if task_event_id and _module_value(business_module):
-                    bindings = [
-                        resolver.text_span_binding(
-                            task_event_id,
-                            binding_id=f"{module_id}:task-span",
-                            path="input.content.task",
-                            exact_text=_module_value(business_module),
-                            role="input",
-                        ),
-                        *[
-                            binding
-                            for binding in bindings
-                            if binding.get("sourceId") != f"event:{task_event_id}"
-                        ],
-                    ]
-            aggregate = _aggregate_binding(
-                str(card_data.get("cardKind") or ""),
-                module_id,
-                source_ids,
-                fields,
-                resolver,
-                bundle,
-            )
-            if aggregate is not None:
-                bindings = [aggregate]
-            if module_id == "process" and not bindings:
-                bindings = [
-                    resolver.binding_for_event(
-                        event_id,
-                        binding_id=f"{module_id}:event:{event_id}",
-                        role="process",
-                    )
-                    for event_id in sorted(event_details)
-                ]
-            if card_data.get("cardKind") == "multipath-decision" and module_id == "outcomes":
-                for binding in bindings:
-                    source_record = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
-                    raw_record = source_record.get("rawRecord")
-                    branch_status = str((raw_record or {}).get("status") or "").lower() if isinstance(raw_record, dict) else ""
-                    if branch_status in {"merged", "adopted", "accepted", "selected"}:
-                        binding["evidence"]["adoption"] = "explicitly-adopted"
-                        binding["evidence"]["confidence"] = "exact"
-                    elif branch_status in {"discarded", "rejected", "not_adopted"}:
-                        binding["evidence"]["adoption"] = "explicitly-rejected"
-                        binding["evidence"]["confidence"] = "exact"
-            if not bindings:
-                binding = unresolved_binding(
-                    f"{module_id}:unresolved",
-                    reason=f"模块「{title}」没有保存可安全定位的原始记录",
-                    code="binding-map-missing",
-                )
-                resolver.add_unresolved_source(binding)
-                bindings = [binding]
-            value = _module_value(business_module)
-            if value in (None, "", [], {}) and source_ids:
-                values = [fields[source_id].get("value") for source_id in source_ids if source_id in fields]
-                value = values[0] if len(values) == 1 else values
-            _align_transforms_with_business(value, bindings, resolver, module_id)
-            module = {
-                "id": module_id,
-                "title": title,
-                "presentation": _presentation(business_module, title),
-                "business": {"value": value},
-                "bindings": bindings,
-                "runtimeRefs": _runtime_refs(bindings),
-                "defaultOpen": index <= 3,
-            }
-            if business_module and business_module.get("items"):
-                business_items = _normalize_items(business_module.get("items") or [])
-                item_rows = []
-                child_bindings: list[SourceBinding] = []
-                for item_index, item in enumerate(business_items, 1):
-                    item_bindings = (
-                        [_multipath_comparison_binding(
-                            item,
-                            item_index,
-                            bindings,
-                            resolver,
-                            bundle,
-                        )]
-                        if card_data.get("cardKind") == "multipath-decision" and module_id == "comparison"
-                        else _item_bindings(
-                            module_id,
-                            item_index,
-                            item,
-                            len(business_items),
-                            bindings,
-                            resolver,
-                        )
-                    )
-                    child_bindings.extend(item_bindings)
-                    item_rows.append(
-                        {
-                            "id": f"{module_id}:item:{item_index}",
-                            "label": item.get("label"),
-                            "value": item.get("value"),
-                            "bindings": item_bindings,
-                        }
-                    )
-                module["business"] = {"items": item_rows}
-                module["bindings"] = _unique_bindings(child_bindings)
-                module["runtimeRefs"] = _runtime_refs(module["bindings"])
-            elif business_module and business_module.get("steps"):
-                step_rows = []
-                step_bindings: list[SourceBinding] = []
-                for step_index, step in enumerate(business_module.get("steps") or [], 1):
-                    if not isinstance(step, dict):
-                        continue
-                    step_event_id = _suffix_int(str(step.get("eventRef") or ""))
-                    if step_event_id and step_event_id in event_details:
-                        step_binding = resolver.binding_for_event(
-                            step_event_id,
-                            binding_id=f"{module_id}:step:{step_index}",
-                            path="",
-                            role="process",
-                        )
-                        step_binding["transform"] = {
-                            "kind": "parsed",
-                            "operation": "creative-step-v1",
-                            "outputKey": str(step_index),
-                        }
-                        step_binding["evidence"]["confidence"] = "deterministic"
-                    else:
-                        step_binding = unresolved_binding(
-                            f"{module_id}:step:{step_index}:unresolved",
-                            reason=f"创作步骤 {step_index} 没有保存可安全关联的 Event",
-                            source_id=(f"event:{step_event_id}" if step_event_id else None),
-                            resolution="unsafe",
-                            code="unsafe-association",
-                        )
-                        if step_binding["sourceId"] not in resolver.sources:
-                            resolver.add_unresolved_source(step_binding)
-                    step_bindings.append(step_binding)
-                    step_rows.append({
-                        "id": f"{module_id}:step:{step_index}",
-                        "label": step.get("action") or f"步骤 {step_index}",
-                        "value": step,
-                        "bindings": [step_binding],
-                    })
-                module["business"] = {"items": step_rows}
-                module["bindings"] = step_bindings
-                module["runtimeRefs"] = _runtime_refs(step_bindings)
-            modules.append(module)
-
-        # The formal Inspector exposes only modules declared by the unified
-        # card contract.  Legacy business blocks are an input adapter, not a
-        # second set of UI modules; appending unmatched blocks here created
-        # duplicates such as Goal + “本轮目标构成” with no trustworthy binding.
-        return modules
-
-    def _event_modules(
-        self,
-        detail_ref: str,
-        business: dict[str, Any],
-        resolver: SourceResolver,
-        event_details: dict[int, dict[str, Any]],
-    ) -> list[dict[str, Any]] | None:
-        if not detail_ref.startswith("event:"):
-            return None
-        event_id = _suffix_int(detail_ref)
-        event = event_details.get(event_id or -1)
-        if event is None:
-            return None
-        if business.get("detailKind") == "retrieval-event":
-            return _retrieval_event_modules(business, event, resolver)
-
-        if event.get("event_name") == "script_implementer":
-            input_side = event.get("input") if isinstance(event.get("input"), dict) else {}
-            content = input_side.get("content") if isinstance(input_side, dict) else None
-            task = content.get("task") if isinstance(content, dict) else content
-            sections = {
-                str(item.get("id")): item
-                for item in business.get("businessSections") or []
-                if isinstance(item, dict)
-            }
-            action_binding = resolver.binding_for_event(
-                event_id or 0,
-                binding_id="action:event",
-                path="event_name",
-                role="process",
-            )
-            action_binding["transform"] = {
-                "kind": "parsed",
-                "operation": "implementation-activity-label-v1",
-                "outputKey": "action",
-            }
-            action_binding["evidence"]["confidence"] = "deterministic"
-            request_binding = resolver.binding_for_event(
-                event_id or 0,
-                binding_id="request:event",
-                path="input.content.task",
-                role="input",
-                availability="direct-read",
-            )
-            request_binding["transform"] = {
-                "kind": "parsed",
-                "operation": "implementation-task-readable-sections-v1",
-                "outputKey": "request",
-            }
-            request_binding["evidence"]["confidence"] = "deterministic"
-            result_value = (sections.get("result") or {}).get("content")
-            result_binding = resolver.text_span_binding(
-                event_id or 0,
-                binding_id="result:event",
-                path="output.content.summary",
-                exact_text=result_value,
-                role="output",
-            )
-            return [
-                {
-                    "id": "action",
-                    "title": "做了什么",
-                    "presentation": "text",
-                    "business": {"value": (sections.get("action") or {}).get("content")},
-                    "bindings": [action_binding],
-                    "runtimeRefs": [detail_ref],
-                    "defaultOpen": True,
-                },
-                {
-                    "id": "request",
-                    "title": "处理内容",
-                    "presentation": "text",
-                    "business": {"value": (sections.get("request") or {}).get("content")},
-                    "bindings": [request_binding],
-                    "runtimeRefs": [detail_ref],
-                    "defaultOpen": True,
-                },
-                {
-                    "id": "result",
-                    "title": "得到什么",
-                    "presentation": "text",
-                    "business": {"value": result_value},
-                    "bindings": [result_binding],
-                    "runtimeRefs": [detail_ref],
-                    "defaultOpen": True,
-                },
-            ]
-
-        values = _business_modules(business)
-        planning_paths = {
-            "summary": "input.content.thought_summary",
-            "thought": "input.content.thought",
-            "plan": "input.content.plan",
-            "action": "input.content.action",
-        }
-        modules: list[dict[str, Any]] = []
-        is_planning = (
-            event.get("event_name") == "think_and_plan"
-            and event.get("agent_role") == "main"
-            and _integer(event.get("agent_depth")) == 0
-        )
-        is_review = event.get("event_name") in {"script_multipath_evaluator", "script_evaluator"}
-        if not is_planning and not is_review:
-            return _generic_event_modules(
-                detail_ref, business, event, resolver, event_id or 0
-            )
-        for index, item in enumerate(values, 1):
-            module_id = str(item.get("id") or f"event-module-{index}")
-            title = str(item.get("title") or f"运行模块 {index}")
-            value = _module_value(item)
-            input_title = title in {"评审范围", "评审标准", "评审对象", "标准"}
-            if is_planning and module_id in planning_paths:
-                binding = resolver.binding_for_event(
-                    event_id or 0,
-                    binding_id=f"{module_id}:event:{event_id}",
-                    path=planning_paths[module_id],
-                    role="input",
-                    availability="direct-read",
-                )
-            elif title == "记录说明" and not event.get("output"):
-                binding = resolver.binding_for_event(
-                    event_id or 0,
-                    binding_id=f"{module_id}:event:{event_id}",
-                    role="status",
-                    availability="produced-by-run",
-                )
-                binding["transform"] = {
-                    "kind": "calculated",
-                    "operation": "runtime-missing-decision-notice-v1",
-                }
-                binding["evidence"]["confidence"] = "deterministic"
-            else:
-                side = "input" if input_title else "output"
-                binding = resolver.text_span_binding(
-                    event_id or 0,
-                    binding_id=f"{module_id}:event:{event_id}",
-                    path=_event_text_path(event, side),
-                    exact_text=value,
-                    role="input" if input_title else "output",
-                )
-            business_payload: dict[str, Any] = {"value": value}
-            bindings = [binding]
-            if item.get("items"):
-                rows = []
-                bindings = []
-                for item_index, child in enumerate(_normalize_items(item.get("items") or []), 1):
-                    child_bindings = []
-                    for leaf_index, leaf in enumerate(_leaf_texts(child.get("value")), 1):
-                        child_bindings.append(resolver.text_span_binding(
-                            event_id or 0,
-                            binding_id=f"{module_id}:item:{item_index}:leaf:{leaf_index}",
-                            path=_event_text_path(event, "input" if input_title else "output"),
-                            exact_text=leaf,
-                            role="input" if input_title else "output",
-                        ))
-                    if not child_bindings:
-                        fallback = resolver.binding_for_event(
-                            event_id or 0,
-                            binding_id=f"{module_id}:item:{item_index}:record",
-                            path=_event_text_path(event, "input" if input_title else "output"),
-                            role="input" if input_title else "output",
-                            availability="direct-read" if input_title else "returned-to-agent",
-                        )
-                        fallback["transform"] = {
-                            "kind": "parsed",
-                            "operation": "review-item-v1",
-                            "outputKey": str(item_index),
-                        }
-                        fallback["evidence"]["confidence"] = "deterministic"
-                        child_bindings = [fallback]
-                    bindings.extend(child_bindings)
-                    rows.append(
-                        {
-                            "id": f"{module_id}:item:{item_index}",
-                            "label": child.get("label"),
-                            "value": child.get("value"),
-                            "bindings": child_bindings,
-                        }
-                    )
-                business_payload = {"items": rows}
-            modules.append(
-                {
-                    "id": module_id,
-                    "title": title,
-                    "presentation": _presentation(item, title),
-                    "business": business_payload,
-                    "bindings": bindings,
-                    "runtimeRefs": [detail_ref],
-                    "defaultOpen": index <= 3,
-                }
-            )
-        return modules
-
-
-def _generic_event_modules(
-    detail_ref: str,
-    business: dict[str, Any],
-    event: dict[str, Any],
-    resolver: SourceResolver,
-    event_id: int,
-) -> list[dict[str, Any]] | None:
-    """Bind the readable action/request/result projection to the real Event Body."""
-    sections = [
-        item
-        for item in business.get("businessSections") or []
-        if isinstance(item, dict)
-    ]
-    if not sections:
-        return None
-    modules: list[dict[str, Any]] = []
-    for index, section in enumerate(sections, 1):
-        module_id = str(section.get("id") or f"section-{index}")
-        value = section.get("content")
-        if module_id == "action":
-            path, role, availability = "event_name", "process", "produced-by-run"
-            operation = "runtime-activity-label-v1"
-        elif module_id == "request":
-            path, role, availability = _event_text_path(event, "input"), "input", "direct-read"
-            operation = "runtime-request-readable-projection-v1"
-        elif module_id == "result":
-            path, role, availability = _event_text_path(event, "output"), "output", "returned-to-agent"
-            operation = "runtime-result-readable-projection-v1"
-        else:
-            # The section is visibly derived from this Event, but there is no
-            # card-specific field contract. Bind the complete Event instead of
-            # falsely claiming that its original record is missing.
-            path, role, availability = "", "process", "produced-by-run"
-            operation = "runtime-business-section-v1"
-        binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"{module_id}:event:{event_id}",
-            path=path,
-            role=role,
-            availability=availability,
-        )
-        binding["transform"] = {
-            "kind": "parsed",
-            "operation": operation,
-            "outputKey": module_id,
-        }
-        binding["evidence"]["confidence"] = "deterministic"
-        modules.append({
-            "id": module_id,
-            "title": str(section.get("title") or f"业务模块 {index}"),
-            "presentation": "text",
-            "business": {"value": value},
-            "bindings": [binding],
-            "runtimeRefs": [detail_ref],
-            "defaultOpen": True,
-        })
-    return modules
-
-
-def _retrieval_event_modules(
-    business: dict[str, Any], event: dict[str, Any], resolver: SourceResolver
-) -> list[dict[str, Any]]:
-    event_id = _integer(event.get("id")) or 0
-    conditions = business.get("queryConditions") or []
-    condition_items = []
-    condition_bindings: list[SourceBinding] = []
-    for index, item in enumerate(conditions, 1):
-        key = str(item.get("key") or index)
-        binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"purpose:{key}",
-            path=f"input.content.{key}",
-            role="input",
-            availability="direct-read",
-        )
-        condition_bindings.append(binding)
-        condition_items.append(
-            {
-                "id": f"purpose:item:{key}",
-                "label": item.get("label"),
-                "value": item.get("value"),
-                "bindings": [binding],
-            }
-        )
-    if not condition_bindings:
-        binding = resolver.binding_for_event(
-            event_id,
-            binding_id="purpose:event-input",
-            path="input.content",
-            role="input",
-            availability="direct-read",
-        )
-        condition_bindings = [binding]
-
-    name = str(event.get("event_name") or "")
-    result_key = RETRIEVAL_RESULT_COLLECTION_KEYS.get(name)
-    root = "output.content" + (f".{result_key}" if result_key else "")
-    root_value = pointer_value(event, f"/{root.replace('.', '/')}")
-    result_items = []
-    result_bindings: list[SourceBinding] = []
-    for index, item in enumerate(business.get("items") or [], 1):
-        item_path = (
-            f"{root}.{index - 1}"
-            if isinstance(root_value, list)
-            else root
-        )
-        binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"result:item:{index}",
-            path=item_path,
-            role="output",
-            availability="returned-to-agent",
-        )
-        binding["transform"] = {
-            "kind": "parsed",
-            "operation": "retrieval-result-item-v1",
-            "outputKey": str(index - 1),
-        }
-        binding["evidence"]["confidence"] = "deterministic"
-        result_bindings.append(binding)
-        result_items.append(
-            {
-                "id": f"result:item:{index}",
-                "label": item.get("title"),
-                "value": item,
-                "bindings": [binding],
-            }
-        )
-    if not result_bindings:
-        empty_result = resolver.binding_for_event(
-                event_id,
-                binding_id="result:event-output",
-                path="output.content",
-                role="output",
-                availability="returned-to-agent",
-            )
-        empty_result["transform"] = {
-            "kind": "parsed",
-            "operation": "retrieval-empty-or-failure-message-v1",
-            "outputKey": "message",
-        }
-        empty_result["evidence"]["confidence"] = "deterministic"
-        result_bindings = [empty_result]
-    status_bindings = _event_status_bindings(
-        resolver, event_id, prefix="status", include_output=True
-    )
-    event_source = resolver.sources.get(f"event:{event_id}")
-    if event_source is not None and str((business.get("outcome") or {}).get("state")) == "empty":
-        event_source["resultState"] = "empty"
-    return [
-        {
-            "id": "purpose",
-            "title": "目的 / 查询条件",
-            "presentation": "items",
-            "business": {"items": condition_items} if condition_items else {"value": "未记录调用输入"},
-            "bindings": condition_bindings,
-            "runtimeRefs": [f"event:{event_id}"],
-            "defaultOpen": True,
-        },
-        {
-            "id": "result",
-            "title": "返回内容 / 结果列表",
-            "presentation": "items",
-            "business": {"items": result_items} if result_items else {"value": (business.get("outcome") or {}).get("message")},
-            "bindings": result_bindings,
-            "runtimeRefs": [f"event:{event_id}"],
-            "defaultOpen": True,
-        },
-        {
-            "id": "status",
-            "title": "状态、耗时与完整性",
-            "presentation": "notice",
-            "business": {"value": {"outcome": business.get("outcome"), "completeness": business.get("completeness")}},
-            "bindings": status_bindings,
-            "runtimeRefs": [f"event:{event_id}"],
-            "defaultOpen": True,
-        },
-    ]
-
-
-def _direct_group_modules(
-    card_data: dict[str, Any],
-    resolver: SourceResolver,
-    event_details: dict[int, dict[str, Any]],
-) -> list[dict[str, Any]]:
-    events = [event_details[event_id] for event_id in sorted(event_details)]
-
-    def rows(kind: str) -> tuple[list[dict[str, Any]], list[SourceBinding]]:
-        values: list[dict[str, Any]] = []
-        bindings: list[SourceBinding] = []
-        for index, event in enumerate(events, 1):
-            event_id = _integer(event.get("id")) or 0
-            if kind == "input":
-                path, role, availability = "input.content", "input", "direct-read"
-                label = str(event.get("event_name") or f"调用 {index}")
-                value = ((event.get("input") or {}).get("content") if isinstance(event.get("input"), dict) else None)
-            elif kind == "output":
-                path, role, availability = "output.content", "output", "returned-to-agent"
-                label = str(event.get("event_name") or f"调用 {index}")
-                value = ((event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else None)
-            else:
-                path, role, availability = "status", "status", "produced-by-run"
-                label = str(event.get("event_name") or f"调用 {index}")
-                value = {
-                    "status": event.get("status"),
-                    "durationMs": event.get("duration_ms"),
-                    "error": event.get("error_message"),
-                }
-            item_bindings = (
-                _event_status_bindings(resolver, event_id, prefix=kind)
-                if kind == "status"
-                else [resolver.binding_for_event(
-                    event_id,
-                    binding_id=f"{kind}:event:{event_id}",
-                    path=path,
-                    role=role,
-                    availability=availability,
-                )]
-            )
-            bindings.extend(item_bindings)
-            values.append(
-                {
-                    "id": f"{kind}:item:{event_id}",
-                    "label": label,
-                    "value": value,
-                    "bindings": item_bindings,
-                }
-            )
-        return values, bindings
-
-    input_rows, input_bindings = rows("input")
-    output_rows, output_bindings = rows("output")
-    status_rows, status_bindings = rows("status")
-    failed_rows = [
-        row
-        for row in status_rows
-        if str((row.get("value") or {}).get("status") or "").lower()
-        in {"failed", "failure", "error", "interrupted"}
-    ]
-    failed_ids = {row["id"].rsplit(":", 1)[-1] for row in failed_rows}
-    failed_bindings = [
-        binding
-        for binding in status_bindings
-        if binding["id"].rsplit(":", 1)[-1] in failed_ids
-    ]
-    if not failed_rows:
-        failed_bindings = [resolver.members_binding(
-            {
-                "id": "failures:none",
-                "label": "失败调用数量",
-                "value": 0,
-                "relation": "persisted-output",
-            },
-            binding_id="failures:none-calculated",
-            members=[{
-                "id": f"failures:status:{event.get('id')}",
-                "label": str(event.get("event_name") or event.get("id")),
-                "value": event.get("status"),
-                "source": {
-                    "kind": "runtime-event",
-                    "label": str(event.get("event_name") or f"Event {event.get('id')}"),
-                    "ref": f"event:{event.get('id')}",
-                    "fieldPath": "status",
-                },
-                "relation": "persisted-output",
-                "completeness": "complete",
-            } for event in events],
-            reducer="no-failed-tool-status-v1",
-            role="status",
-        )]
-    return [
-        {
-            "id": "purpose",
-            "title": "读取目的",
-            "presentation": "items",
-            "business": {"items": input_rows},
-            "bindings": input_bindings,
-            "runtimeRefs": _runtime_refs(input_bindings),
-            "defaultOpen": True,
-        },
-        {
-            "id": "calls",
-            "title": "逐次工具调用",
-            "presentation": "items",
-            "business": {"items": status_rows},
-            "bindings": status_bindings,
-            "runtimeRefs": _runtime_refs(status_bindings),
-            "defaultOpen": True,
-        },
-        {
-            "id": "results",
-            "title": "实际返回的信息",
-            "presentation": "items",
-            "business": {"items": output_rows},
-            "bindings": output_bindings,
-            "runtimeRefs": _runtime_refs(output_bindings),
-            "defaultOpen": True,
-        },
-        {
-            "id": "failures",
-            "title": "失败与缺失",
-            "presentation": "notice",
-            "business": {"items": failed_rows} if failed_rows else {"value": "未发现失败或中断的工具调用。"},
-            "bindings": failed_bindings,
-            "runtimeRefs": _runtime_refs(failed_bindings),
-            "defaultOpen": bool(failed_rows),
-        },
-    ]
-
-
-def _retrieval_agent_modules(
-    card_data: dict[str, Any],
-    resolver: SourceResolver,
-    event_details: dict[int, dict[str, Any]],
-) -> list[dict[str, Any]]:
-    events = [event_details[event_id] for event_id in sorted(event_details)]
-    agent = next(
-        (event for event in events if str(event.get("event_type") or "") == "agent_invoke"),
-        events[0] if events else {},
-    )
-    agent_id = _integer(agent.get("id")) or 0
-    children = [event for event in events if _integer(event.get("id")) != agent_id]
-    queries = [
-        event
-        for event in children
-        if str(event.get("event_name") or "") in QUERY_TOOL_NAMES
-    ]
-    task_binding = resolver.binding_for_event(
-        agent_id,
-        binding_id="task:agent-input",
-        path="input.content.task",
-        role="input",
-        availability="direct-read",
-    )
-    screening_binding = resolver.binding_for_event(
-        agent_id,
-        binding_id="screening:agent-output",
-        path="output.content",
-        role="output",
-        availability="produced-by-run",
-    )
-    task = ((agent.get("input") or {}).get("content") if isinstance(agent.get("input"), dict) else None)
-    task = task.get("task") if isinstance(task, dict) else task
-    screening = ((agent.get("output") or {}).get("content") if isinstance(agent.get("output"), dict) else None)
-    query_rows: list[dict[str, Any]] = []
-    result_rows: list[dict[str, Any]] = []
-    status_rows: list[dict[str, Any]] = []
-    query_bindings: list[SourceBinding] = []
-    result_bindings: list[SourceBinding] = []
-    status_bindings: list[SourceBinding] = []
-    for index, event in enumerate(queries, 1):
-        event_id = _integer(event.get("id")) or 0
-        label = str(event.get("event_name") or f"查询 {index}")
-        input_binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"queries:event:{event_id}",
-            path="input.content",
-            role="input",
-            availability="direct-read",
-        )
-        output_binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"raw:event:{event_id}",
-            path="output.content",
-            role="output",
-            availability="returned-to-agent",
-        )
-        status_item_bindings = _event_status_bindings(
-            resolver, event_id, prefix="failures"
-        )
-        query_bindings.append(input_binding)
-        result_bindings.append(output_binding)
-        status_bindings.extend(status_item_bindings)
-        query_rows.append({
-            "id": f"queries:item:{event_id}", "label": label,
-            "value": ((event.get("input") or {}).get("content") if isinstance(event.get("input"), dict) else None),
-            "bindings": [input_binding],
-        })
-        result_rows.append({
-            "id": f"raw:item:{event_id}", "label": label,
-            "value": ((event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else None),
-            "bindings": [output_binding],
-        })
-        status_rows.append({
-            "id": f"failures:item:{event_id}", "label": label,
-            "value": {"status": event.get("status"), "error": event.get("error_message")},
-            "bindings": status_item_bindings,
-        })
-    if not queries:
-        query_bindings = [
-            _calculation_binding(
-                resolver,
-                binding_id="queries:empty",
-                label="取数 Agent 查询事件计数",
-                value=[],
-                role="process",
-            )
-        ]
-        result_bindings = [
-            _calculation_binding(
-                resolver,
-                binding_id="raw:empty",
-                label="取数 Agent 原始命中计数",
-                value=[],
-                role="output",
-            )
-        ]
-        status_bindings = [
-            _calculation_binding(
-                resolver,
-                binding_id="failures:empty",
-                label="取数 Agent 失败查询计数",
-                value=[],
-                role="status",
-            )
-        ]
-    activity_rows: list[dict[str, Any]] = []
-    activity_bindings: list[SourceBinding] = []
-    for event in events:
-        event_id = _integer(event.get("id")) or 0
-        binding = resolver.binding_for_event(
-            event_id,
-            binding_id=f"activities:event:{event_id}",
-            role="process",
-        )
-        activity_bindings.append(binding)
-        activity_rows.append(
-            {
-                "id": f"activities:item:{event_id}",
-                "label": str(event.get("event_name") or f"Event {event_id}"),
-                "value": {
-                    "eventName": event.get("event_name"),
-                    "eventType": event.get("event_type"),
-                    "status": event.get("status"),
-                },
-                "bindings": [binding],
-            }
-        )
-    return [
-        {
-            "id": "task", "title": "完整取数任务", "presentation": "text",
-            "business": {"value": task}, "bindings": [task_binding],
-            "runtimeRefs": [f"event:{agent_id}"], "defaultOpen": True,
-        },
-        {
-            "id": "queries", "title": "查询过程", "presentation": "items",
-            "business": {"items": query_rows}, "bindings": query_bindings,
-            "runtimeRefs": _runtime_refs(query_bindings), "defaultOpen": True,
-        },
-        {
-            "id": "raw", "title": "原始命中", "presentation": "items",
-            "business": {"items": result_rows}, "bindings": result_bindings,
-            "runtimeRefs": _runtime_refs(result_bindings), "defaultOpen": True,
-        },
-        {
-            "id": "screening", "title": "初筛整理", "presentation": "text",
-            "business": {"value": screening}, "bindings": [screening_binding],
-            "runtimeRefs": [f"event:{agent_id}"], "defaultOpen": True,
-        },
-        {
-            "id": "failures", "title": "失败与空结果", "presentation": "items",
-            "business": {"items": status_rows}, "bindings": status_bindings,
-            "runtimeRefs": _runtime_refs(status_bindings), "defaultOpen": False,
-        },
-        {
-            "id": "activities", "title": "运行记录", "presentation": "process",
-            "business": {"items": activity_rows}, "bindings": activity_bindings,
-            "runtimeRefs": _runtime_refs(activity_bindings), "defaultOpen": False,
-        },
-    ]
-
-
-def _calculation_binding(
-    resolver: SourceResolver,
-    *,
-    binding_id: str,
-    label: str,
-    value: Any,
-    role: str,
-) -> SourceBinding:
-    binding = resolver.binding_for_field(
-        {
-            "id": binding_id,
-            "label": label,
-            "value": value,
-            "source": {
-                "kind": "calculation",
-                "label": label,
-                "ref": f"calculation:{binding_id}",
-                "fieldPath": "value",
-            },
-            "relation": "persisted-output",
-            "completeness": "complete",
-        },
-        binding_id=binding_id,
-        role=role,
-    )
-    binding["evidence"]["confidence"] = "deterministic"
-    return binding
-
-
-def _event_status_bindings(
-    resolver: SourceResolver,
-    event_id: int,
-    *,
-    prefix: str,
-    include_output: bool = False,
-) -> list[SourceBinding]:
-    """Bind every Event value rendered by a status/diagnostic module."""
-    bindings = [
-        resolver.binding_for_event(
-            event_id,
-            binding_id=f"{prefix}:status:event:{event_id}",
-            path="status",
-            role="status",
-        ),
-    ]
-    event = resolver.event_details.get(int(event_id)) or {}
-    if "duration_ms" in event:
-        bindings.append(resolver.binding_for_event(
-            event_id,
-            binding_id=f"{prefix}:duration:event:{event_id}",
-            path="duration_ms",
-            role="status",
-        ))
-    if pointer_value(event, "/error_message") is not None or "error_message" in event:
-        bindings.append(resolver.binding_for_event(
-            event_id,
-            binding_id=f"{prefix}:error:event:{event_id}",
-            path="error_message",
-            role="status",
-        ))
-    if include_output:
-        outcome = resolver.binding_for_event(
-            event_id,
-            binding_id=f"{prefix}:outcome:event:{event_id}",
-            path="output.content",
-            role="output",
-            availability="returned-to-agent",
-        )
-        outcome["transform"] = {
-            "kind": "parsed",
-            "operation": "retrieval-outcome-v1",
-            "outputKey": "outcome",
-        }
-        outcome["evidence"]["confidence"] = "deterministic"
-        bindings.append(outcome)
-        completeness = resolver.binding_for_event(
-            event_id,
-            binding_id=f"{prefix}:completeness:event:{event_id}",
-            path="",
-            role="status",
-        )
-        completeness["transform"] = {
-            "kind": "parsed",
-            "operation": "event-body-completeness-v1",
-            "outputKey": "completeness",
-        }
-        completeness["evidence"]["confidence"] = "deterministic"
-        bindings.append(completeness)
-    return bindings
-
-
-def _business_modules(detail: dict[str, Any]) -> list[dict[str, Any]]:
-    if isinstance(detail.get("blocks"), list):
-        modules: list[dict[str, Any]] = []
-        for index, item in enumerate(detail.get("blocks") or [], 1):
-            if not isinstance(item, dict):
-                continue
-            # Preserve the complete semantic block.  Specialized business
-            # blocks carry their real payload in routes/steps/branches/rows,
-            # not in the generic value/items pair.
-            module = dict(item)
-            module["id"] = str(item.get("id") or f"block-{index}")
-            module["title"] = item.get("title") or f"业务模块 {index}"
-            if not module.get("items"):
-                module["items"] = item.get("branches") or item.get("rows")
-            modules.append(module)
-        return modules
-    return [
-        dict(item)
-        for item in detail.get("businessSections") or []
-        if isinstance(item, dict)
-    ]
-
-
-def _match_business_module(
-    modules: list[dict[str, Any]], module_id: str, title: str
-) -> dict[str, Any] | None:
-    exact = next((item for item in modules if str(item.get("id")) == module_id), None)
-    if exact:
-        return exact
-    normalized = _normalized_title(title)
-    exact_title = next(
-        (item for item in modules if _normalized_title(str(item.get("title") or "")) == normalized),
-        None,
-    )
-    if exact_title:
-        return exact_title
-    # Do not use substring similarity here. A nearby title is not proof that
-    # two independently shaped modules contain the same business value.
-    return None
-
-
-def _module_value(module: dict[str, Any] | None) -> Any:
-    if not module:
-        return None
-    if module.get("content") is not None:
-        return module.get("content")
-    if module.get("value") is not None:
-        return module.get("value")
-    block_type = str(module.get("type") or "")
-    if block_type == "implementation-plan":
-        return {
-            "mode": module.get("mode"),
-            "routes": module.get("routes") or [],
-            "reasoning": module.get("reasoning"),
-        }
-    if block_type == "creative-process":
-        return module.get("steps") or []
-    if block_type == "evaluation-branches":
-        return module.get("branches") or []
-    if module.get("rows") is not None:
-        return module.get("rows")
-    if module.get("routes") is not None:
-        return module.get("routes")
-    if module.get("steps") is not None:
-        return module.get("steps")
-    return None
-
-
-def _presentation(module: dict[str, Any] | None, title: str) -> str:
-    raw = str((module or {}).get("presentation") or (module or {}).get("type") or "")
-    if raw in {"items", "list"}:
-        return "items"
-    if any(token in title for token in ("规划", "路线", "计划")):
-        return "plan"
-    if any(token in title for token in ("过程", "处理", "调用")):
-        return "process"
-    if "评审" in title:
-        return "review"
-    if any(token in title for token in ("候选表", "主脚本", "产物")):
-        return "artifact"
-    if any(token in title for token in ("说明", "缺失", "状态", "完整性")):
-        return "notice"
-    return "text"
-
-
-def _runtime_refs(bindings: list[SourceBinding]) -> list[str]:
-    refs: list[str] = []
-    for binding in bindings:
-        source_id = str(binding.get("sourceId") or "")
-        if source_id.startswith("event:"):
-            refs.append(source_id)
-        selector = binding.get("selector") or {}
-        if selector.get("kind") == "members":
-            refs.extend(
-                str(member.get("sourceId") or "")
-                for member in selector.get("members") or []
-                if str(member.get("sourceId") or "").startswith("event:")
-            )
-    return list(dict.fromkeys(refs))
-
-
-def _historical_implementation_task_modules(
-    fields: dict[str, dict[str, Any]],
-    resolver: SourceResolver,
-) -> list[dict[str, Any]]:
-    """Keep old Branch tasks readable without copying one blob into six modules."""
-    task_field = next(
-        (field for field in fields.values() if str(field.get("label") or "") == "完整实现任务"),
-        None,
-    )
-    target_field = next(
-        (field for field in fields.values() if str(field.get("label") or "") == "实现目标"),
-        None,
-    )
-    modules: list[dict[str, Any]] = []
-    if target_field and target_field.get("value") not in (None, ""):
-        binding = resolver.binding_for_field(target_field, binding_id="scope:historical-target")
-        modules.append({
-            "id": "scope",
-            "title": "实现范围",
-            "presentation": "text",
-            "business": {"value": target_field.get("value")},
-            "bindings": [binding],
-            "runtimeRefs": [],
-            "defaultOpen": True,
-        })
-    if task_field and task_field.get("value") not in (None, ""):
-        binding = resolver.binding_for_field(task_field, binding_id="task:historical-full")
-        modules.append({
-            "id": "historical-task",
-            "title": "完整实现任务(历史回退)",
-            "presentation": "text",
-            "business": {"value": task_field.get("value")},
-            "bindings": [binding],
-            "runtimeRefs": [],
-            "defaultOpen": True,
-        })
-    if modules:
-        return modules
-    binding = unresolved_binding(
-        "historical-task:missing",
-        reason="该历史方案没有保存实现 Agent 输入,也没有可用的 Branch 实现任务字段",
-        code="source-record-not-saved",
-    )
-    resolver.add_unresolved_source(binding)
-    return [{
-        "id": "historical-task",
-        "title": "实现任务记录缺失",
-        "presentation": "notice",
-        "business": {"value": "该历史方案未保存可恢复的完整实现任务。"},
-        "bindings": [binding],
-        "runtimeRefs": [],
-        "defaultOpen": True,
-    }]
-
-
-def _multipath_comparison_binding(
-    item: dict[str, Any],
-    item_index: int,
-    module_bindings: list[SourceBinding],
-    resolver: SourceResolver,
-    bundle: dict[str, Any],
-) -> SourceBinding:
-    """Bind one comparison row to its review, Branch status and final decision."""
-    text = str(item.get("value") or item.get("label") or "")
-    match = re.search(r"方案\s*(\d+)", text)
-    branch_id = int(match.group(1)) if match else None
-    decision_id = next(
-        (
-            _suffix_int(str(binding.get("sourceId") or ""))
-            for binding in module_bindings
-            if "script_build_multipath_decision" in str(binding.get("sourceId") or "")
-        ),
-        None,
-    )
-    members: list[dict[str, Any]] = []
-    if branch_id is not None:
-        branch = next(
-            (row for row in bundle.get("branches") or [] if _integer(row.get("branch_id")) == branch_id),
-            None,
-        )
-        if branch is not None:
-            members.append({
-                "id": f"comparison:{item_index}:branch",
-                "label": f"方案 {branch_id} 处置状态",
-                "value": branch.get("status"),
-                "source": {
-                    "kind": "database",
-                    "label": "script_build_branch",
-                    "ref": f"round:{branch.get('round_index')}:branch:{branch_id}:output",
-                    "fieldPath": "status",
-                },
-                "relation": "persisted-output",
-            })
-    if decision_id is not None:
-        decision = next(
-            (row for row in bundle.get("multipathDecisions") or [] if _integer(row.get("id")) == decision_id),
-            None,
-        )
-        if decision is not None:
-            members.append({
-                "id": f"comparison:{item_index}:decision",
-                "label": "主 Agent 最终决定",
-                "value": decision.get("decision"),
-                "source": {
-                    "kind": "database",
-                    "label": "script_build_multipath_decision",
-                    "ref": f"multipath-decision:{decision_id}",
-                    "fieldPath": "decision",
-                },
-                "relation": "persisted-output",
-            })
-    for binding in module_bindings:
-        source_id = str(binding.get("sourceId") or "")
-        event_id = _suffix_int(source_id) if source_id.startswith("event:") else None
-        if event_id is None:
-            continue
-        event = resolver.event_details.get(event_id)
-        if event is None:
-            continue
-        members.append({
-            "id": f"comparison:{item_index}:review:{event_id}",
-            "label": "多方案评审",
-            "value": (event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else event.get("output"),
-            "source": {
-                "kind": "runtime-event",
-                "label": str(event.get("event_name") or f"Event {event_id}"),
-                "ref": f"event:{event_id}",
-                "fieldPath": "output.content",
-            },
-            "relation": "available-upstream",
-        })
-    return resolver.members_binding(
-        {
-            "id": f"comparison:item:{item_index}",
-            "label": str(item.get("label") or f"方案对照 {item_index}"),
-            "value": item.get("value"),
-        },
-        binding_id=f"comparison:item:{item_index}",
-        members=members,
-        reducer="multipath-review-and-decision-row-v1",
-        role="basis",
-    )
-
-
-def _align_transforms_with_business(
-    value: Any,
-    bindings: list[SourceBinding],
-    resolver: SourceResolver,
-    module_id: str,
-) -> None:
-    """Make the transform describe what the first column actually renders."""
-    if value is None or not bindings:
-        return
-    if len(bindings) > 1:
-        for binding in bindings:
-            if binding.get("transform", {}).get("kind") == "direct":
-                binding["transform"] = {
-                    "kind": "combined",
-                    "operation": f"business-module:{module_id}",
-                }
-                binding["evidence"]["confidence"] = "deterministic"
-        return
-    binding = bindings[0]
-    if binding.get("transform", {}).get("kind") != "direct":
-        return
-    source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
-    selector = binding.get("selector") or {}
-    if selector.get("kind") == "json-pointer":
-        selected = pointer_value(source.get("rawRecord"), str(selector.get("path") or ""))
-    elif selector.get("kind") == "whole-record":
-        selected = source.get("rawRecord")
-    else:
-        return
-    if not _same_value(value, selected):
-        binding["transform"] = {
-            "kind": "parsed",
-            "operation": "business-module-projection-v1",
-            "outputKey": module_id,
-        }
-        binding["evidence"]["confidence"] = "deterministic"
-
-
-def _normalize_items(items: list[Any]) -> list[dict[str, Any]]:
-    values: list[dict[str, Any]] = []
-    for index, item in enumerate(items, 1):
-        if isinstance(item, dict):
-            label = (
-                item.get("label")
-                or item.get("title")
-                or item.get("subject")
-                or item.get("summary")
-                or f"第 {index} 项"
-            )
-            value = item.get("value") if "value" in item else item
-            values.append({"label": label, "value": value})
-        else:
-            values.append({"label": f"第 {index} 项", "value": item})
-    return values
-
-
-def _item_bindings(
-    module_id: str,
-    item_index: int,
-    item: dict[str, Any],
-    item_count: int,
-    module_bindings: list[SourceBinding],
-    resolver: SourceResolver,
-) -> list[SourceBinding]:
-    if not module_bindings:
-        binding = unresolved_binding(
-            f"{module_id}:item:{item_index}:unresolved",
-            reason=f"子项「{item.get('label')}」没有可定位的来源绑定",
-            code="binding-map-missing",
-        )
-        resolver.add_unresolved_source(binding)
-        return [binding]
-
-    if len(module_bindings) == item_count and item_index <= len(module_bindings):
-        binding = deepcopy(module_bindings[item_index - 1])
-        binding["id"] = f"{module_id}:item:{item_index}"
-        source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
-        selector = binding.get("selector") or {}
-        selected = pointer_value(source.get("rawRecord"), str(selector.get("path") or ""))
-        if not _same_value(item.get("value"), selected):
-            binding["transform"] = {
-                "kind": "parsed",
-                "operation": "business-item-v1",
-                "outputKey": str(item_index),
-            }
-            binding["evidence"]["confidence"] = "deterministic"
-        return [binding]
-
-    parent = module_bindings[0]
-    selector = parent.get("selector") or {}
-    source_id = str(parent.get("sourceId") or "")
-    source_record = resolver.sources.get(source_id) or {}
-    selected_parent = pointer_value(
-        source_record.get("rawRecord"), str(selector.get("path") or "")
-    )
-    if selector.get("kind") == "json-pointer" and isinstance(selected_parent, list):
-        binding = deepcopy(parent)
-        binding["id"] = f"{module_id}:item:{item_index}"
-        binding["selector"] = {
-            "kind": "json-pointer",
-            "path": f"{str(selector.get('path') or '').rstrip('/')}/{item_index - 1}",
-        }
-        return [binding]
-
-    event_id = _suffix_int(source_id) if source_id.startswith("event:") else None
-    if event_id:
-        role = str(parent.get("role") or "basis")
-        path = str(selector.get("path") or "")
-        if not path:
-            path = "input.content.task" if role in {"input", "constraint", "basis"} else "output.content.summary"
-        return [
-            resolver.text_span_binding(
-                event_id,
-                binding_id=f"{module_id}:item:{item_index}",
-                path=path.lstrip("/").replace("/", "."),
-                exact_text=_item_text(item.get("value")),
-                role=role,
-            )
-        ]
-
-    binding = unresolved_binding(
-        f"{module_id}:item:{item_index}:unresolved",
-        reason=f"子项「{item.get('label')}」是结构化重组结果,无法安全定位到单一原始区间",
-        source_id=source_id or None,
-        resolution="unsafe",
-        code="unsafe-association",
-    )
-    if source_id and source_id in resolver.sources:
-        return [binding]
-    resolver.add_unresolved_source(binding)
-    return [binding]
-
-
-def _item_text(value: Any) -> str:
-    if isinstance(value, str):
-        return value
-    if not isinstance(value, dict):
-        return str(value or "")
-    for key in ("summary", "conclusion", "subject", "title", "evidence"):
-        if value.get(key):
-            return str(value[key])
-    return ""
-
-
-def _leaf_texts(value: Any) -> list[str]:
-    """Return every visible textual leaf so a structured review item is fully bound."""
-    result: list[str] = []
-    if isinstance(value, str):
-        cleaned = value.strip()
-        if cleaned:
-            result.append(cleaned)
-    elif isinstance(value, dict):
-        for child in value.values():
-            result.extend(_leaf_texts(child))
-    elif isinstance(value, list):
-        for child in value:
-            result.extend(_leaf_texts(child))
-    elif value is not None:
-        result.append(str(value))
-    return list(dict.fromkeys(result))
-
-
-def _same_value(left: Any, right: Any) -> bool:
-    try:
-        import json
-        return json.dumps(left, ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
-            right, ensure_ascii=False, sort_keys=True, default=str
-        )
-    except (TypeError, ValueError):
-        return str(left) == str(right)
-
-
-def _unique_bindings(bindings: list[SourceBinding]) -> list[SourceBinding]:
-    result: list[SourceBinding] = []
-    seen: set[str] = set()
-    for binding in bindings:
-        key = str(binding.get("id") or "")
-        if key in seen:
-            continue
-        seen.add(key)
-        result.append(binding)
-    return result
-
-
-def _all_bindings(modules: list[dict[str, Any]]) -> list[SourceBinding]:
-    result: list[SourceBinding] = []
-    for module in modules:
-        result.extend(module.get("bindings") or [])
-        business = module.get("business") or {}
-        for item in business.get("items") or []:
-            if isinstance(item, dict):
-                result.extend(item.get("bindings") or [])
-    return result
-
-
-def _used_source_ids(modules: list[dict[str, Any]]) -> set[str]:
-    used: set[str] = set()
-
-    def add_selector_members(selector: dict[str, Any]) -> None:
-        if selector.get("kind") != "members":
-            return
-        for member in selector.get("members") or []:
-            source_id = str(member.get("sourceId") or "")
-            if source_id:
-                used.add(source_id)
-            child = member.get("selector")
-            if isinstance(child, dict):
-                add_selector_members(child)
-
-    for module in modules:
-        for binding in module.get("bindings") or []:
-            source_id = str(binding.get("sourceId") or "")
-            if source_id:
-                used.add(source_id)
-            selector = binding.get("selector")
-            if isinstance(selector, dict):
-                add_selector_members(selector)
-        for source_id in module.get("runtimeRefs") or []:
-            if isinstance(source_id, str) and source_id:
-                used.add(source_id)
-    return used
-
-
-def _aggregate_binding(
-    card_kind: str,
-    module_id: str,
-    source_ids: list[str],
-    fields: dict[str, dict[str, Any]],
-    resolver: SourceResolver,
-    bundle: dict[str, Any],
-) -> SourceBinding | None:
-    if not source_ids:
-        return None
-    target = next((fields[source_id] for source_id in source_ids if source_id in fields), None)
-    if target is None:
-        return None
-    eligible = (
-        (card_kind == "round-result" and module_id in {"completion", "dispositions", "domain", "changes"})
-        or (card_kind == "round-summary" and module_id == "plan")
-        or (card_kind == "final-result" and module_id in {"stats", "scale"})
-        or (card_kind == "domain-facts" and module_id in {"facts", "verification", "sources", "usage"})
-        or (card_kind == "retrieval-stage" and module_id in {"mode", "waves", "direct", "agents", "result"})
-    )
-    if not eligible:
-        return None
-    members: list[dict[str, Any]] = []
-    reducer = str((target.get("source") or {}).get("label") or "确定性聚合")
-    if card_kind == "round-summary" and module_id == "plan":
-        round_index = _round_index_from_field_id(str(target.get("id") or ""))
-        row = next(
-            (
-                item
-                for item in bundle.get("rounds") or []
-                if _integer(item.get("round_index")) == round_index
-            ),
-            None,
-        )
-        if row:
-            for path in ("race_or_divide", "multipath_plan", "plan_note"):
-                members.append(
-                    _member_field(
-                        "script_build_round",
-                        f"round:{round_index}",
-                        row,
-                        path,
-                    )
-                )
-    elif card_kind == "retrieval-stage":
-        value = target.get("value")
-        event_ids: list[int] = []
-
-        def collect_event_ids(node: Any) -> None:
-            if isinstance(node, dict):
-                event_id = _integer(node.get("eventId"))
-                if event_id:
-                    event_ids.append(event_id)
-                for child in node.values():
-                    collect_event_ids(child)
-            elif isinstance(node, list):
-                for child in node:
-                    collect_event_ids(child)
-
-        collect_event_ids(value)
-        # Mode/wave/result are deterministic summaries of the whole scoped
-        # retrieval stage, so their members are every Event loaded for this
-        # card. Direct/Agent modules only retain the Events named by their
-        # own structured projection.
-        if module_id in {"mode", "waves", "result"}:
-            event_ids.extend(resolver.event_details)
-        for event_id in dict.fromkeys(event_ids):
-            event = resolver.event_details.get(event_id)
-            if not event:
-                continue
-            members.append(
-                {
-                    "id": f"aggregate:retrieval:{module_id}:event:{event_id}",
-                    "label": str(event.get("event_name") or f"Event {event_id}"),
-                    "value": event,
-                    "source": {
-                        "kind": "runtime-event",
-                        "label": str(event.get("event_name") or f"Event {event_id}"),
-                        "ref": f"event:{event_id}",
-                        "fieldPath": "",
-                    },
-                    "relation": "persisted-output",
-                    "completeness": "complete",
-                }
-            )
-    elif card_kind == "round-result":
-        round_index = _round_index_from_field_id(str(target.get("id") or ""))
-        if module_id in {"dispositions", "changes", "completion"}:
-            for row in bundle.get("branches") or []:
-                if _integer(row.get("round_index")) != round_index:
-                    continue
-                branch_id = _integer(row.get("branch_id"))
-                path = "status" if module_id == "dispositions" else "candidate_snapshot"
-                members.append(
-                    {
-                        "id": f"aggregate:{module_id}:branch:{branch_id}",
-                        "label": f"方案 {branch_id}",
-                        "value": row.get(path),
-                        "source": {
-                            "kind": "database",
-                            "label": "script_build_branch",
-                            "ref": f"round:{round_index}:branch:{branch_id}:output",
-                            "fieldPath": path,
-                        },
-                        "relation": "persisted-output",
-                        "completeness": "complete",
-                    }
-                )
-        elif module_id == "domain":
-            for row in bundle.get("domainInfo") or []:
-                if _integer(row.get("round_index")) != round_index:
-                    continue
-                members.append(
-                    {
-                        "id": f"aggregate:domain:{row.get('id')}",
-                        "label": "领域事实",
-                        "value": row,
-                        "source": {
-                            "kind": "database",
-                            "label": "script_build_domain_info",
-                            "ref": f"domain-info:{row.get('id')}",
-                            "fieldPath": "",
-                        },
-                        "relation": "persisted-output",
-                        "completeness": "complete",
-                    }
-                )
-    elif card_kind == "final-result" and module_id == "stats":
-        for row in bundle.get("rounds") or []:
-            members.append(_member_field("script_build_round", f"round:{row.get('round_index')}:result", row, "status"))
-        for row in bundle.get("branches") or []:
-            members.append(_member_field("script_build_branch", f"round:{row.get('round_index')}:branch:{row.get('branch_id')}:output", row, "status"))
-    elif card_kind == "final-result" and module_id == "scale":
-        artifact = bundle.get("currentArtifact") or {}
-        for collection in ("paragraphs", "elements", "paragraphElements"):
-            members.append(
-                {
-                    "id": f"aggregate:artifact:{collection}",
-                    "label": collection,
-                    "value": artifact.get(collection) or [],
-                    "source": {
-                        "kind": "artifact",
-                        "label": "当前主脚本",
-                        "ref": "artifact:base:current",
-                        "fieldPath": collection,
-                    },
-                    "relation": "persisted-output",
-                    "completeness": "complete",
-                }
-            )
-    elif card_kind == "domain-facts":
-        match = re.search(r"branch:(\d+)", str(target.get("id") or ""))
-        branch_id = _integer(match.group(1)) if match else None
-        path = {
-            "facts": "content",
-            "verification": "note",
-            "sources": "source",
-            "usage": "content",
-        }.get(module_id, "content")
-        for row in bundle.get("domainInfo") or []:
-            if _integer(row.get("branch_id")) != branch_id:
-                continue
-            members.append(
-                {
-                    "id": f"aggregate:domain:{row.get('id')}:{path}",
-                    "label": f"领域事实 {row.get('id')}",
-                    "value": row.get(path),
-                    "source": {
-                        "kind": "database",
-                        "label": "script_build_domain_info",
-                        "ref": f"domain-info:{row.get('id')}",
-                        "fieldPath": path,
-                    },
-                    "relation": "persisted-output",
-                    "completeness": "complete" if row.get(path) not in (None, "", [], {}) else "partial",
-                }
-            )
-    if not members and card_kind == "domain-facts":
-        match = re.search(r"branch:(\d+)", str(target.get("id") or ""))
-        branch_id = _integer(match.group(1)) if match else None
-        return resolver.empty_database_result_binding(
-            binding_id=f"{module_id}:empty-result",
-            table="script_build_domain_info",
-            filters={"branch_id": branch_id},
-            label="领域事实查询结果",
-        )
-    if not members:
-        return None
-    return resolver.members_binding(
-        target,
-        binding_id=f"{module_id}:aggregate",
-        members=members,
-        reducer=reducer,
-    )
-
-
-def _member_field(table: str, ref: str, row: dict[str, Any], path: str) -> dict[str, Any]:
-    return {
-        "id": f"aggregate:{table}:{row.get('id')}:{path}",
-        "label": f"{table}.{path}",
-        "value": row.get(path),
-        "source": {
-            "kind": "database",
-            "label": table,
-            "ref": ref,
-            "fieldPath": path,
-        },
-        "relation": "persisted-output",
-        "completeness": "complete",
-    }
-
-
-def _round_index_from_field_id(value: str) -> int | None:
-    match = re.search(r"round:(\d+)", value)
-    return _integer(match.group(1)) if match else None
-
-
-def _business_notices(detail: dict[str, Any]) -> list[dict[str, Any]]:
-    notices: list[dict[str, Any]] = []
-    for item in detail.get("notices") or []:
-        if isinstance(item, dict) and item.get("message"):
-            notices.append(item)
-    if detail.get("missingActivityNotice"):
-        notices.append({"level": "warning", "message": detail["missingActivityNotice"]})
-    return notices
-
-
-def _unique_notices(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
-    result: list[dict[str, Any]] = []
-    seen: set[str] = set()
-    for item in items:
-        message = str(item.get("message") or "")
-        if not message or message in seen:
-            continue
-        seen.add(message)
-        result.append({"level": item.get("level") or "info", "message": message})
-    return result
-
-
-def _fallback_card_data(detail_ref: str, business: dict[str, Any]) -> dict[str, Any]:
-    modules = _business_modules(business)
-    return {
-        "cardKind": _fallback_card_kind(detail_ref),
-        "completeness": "partial",
-        "businessInputs": [],
-        "businessOutputs": [],
-        "runtime": {"summary": "", "units": []},
-        "displayUse": {
-            "businessModules": [
-                {"id": item.get("id") or f"module-{index}", "title": item.get("title") or f"业务模块 {index}", "sourceIds": []}
-                for index, item in enumerate(modules, 1)
-            ],
-            "cardFields": [],
-            "unusedSourceIds": [],
-        },
-        "notices": [
-            {
-                "level": "warning",
-                "message": "该历史卡片没有完整结构化来源投影,已保留业务详情并显式标记断链。",
-            }
-        ],
-    }
-
-
-def _fallback_card_kind(detail_ref: str) -> str:
-    if detail_ref.startswith("event:"):
-        return "runtime-event"
-    if detail_ref.startswith("round:"):
-        return detail_ref.rsplit(":", 1)[-1]
-    return detail_ref.split(":", 1)[0] or "unknown"
-
-
-def _event_text_path(event: dict[str, Any], side: str) -> str:
-    wrapped = event.get(side)
-    content = wrapped.get("content") if isinstance(wrapped, dict) else None
-    if isinstance(content, dict):
-        preferred = "task" if side == "input" else "summary"
-        if isinstance(content.get(preferred), str):
-            return f"{side}.content.{preferred}"
-    return f"{side}.content"
-
-
-def _normalized_title(value: str) -> str:
-    return re.sub(r"[\s/\u3001,,()()]+", "", value).replace("完整", "")
-
-
-def _collect_event_ids(value: Any, ids: set[int], key: str = "") -> None:
-    if isinstance(value, dict):
-        for child_key, child in value.items():
-            lowered = str(child_key).lower()
-            if lowered in {
-                "eventid",
-                "event_id",
-                "implementereventid",
-                "scopeeventid",
-                "parenteventid",
-            }:
-                event_id = _integer(child)
-                if event_id:
-                    ids.add(event_id)
-            elif lowered in {"eventref", "currentrevisionref", "detailref"}:
-                event_id = _suffix_int(str(child))
-                if event_id:
-                    ids.add(event_id)
-            elif lowered in {"eventrefs", "technicalrefs", "revisionrefs"}:
-                for ref in child or []:
-                    event_id = _suffix_int(str(ref))
-                    if event_id:
-                        ids.add(event_id)
-            _collect_event_ids(child, ids, lowered)
-    elif isinstance(value, list):
-        for child in value:
-            _collect_event_ids(child, ids, key)
-
-
-def _view_branch(view: dict[str, Any], round_index: int, branch_id: int) -> dict[str, Any]:
-    round_ = next(
-        (item for item in view.get("rounds") or [] if _integer(item.get("roundIndex")) == round_index),
-        {},
-    )
-    return next(
-        (item for item in round_.get("branches") or [] if _integer(item.get("branchId")) == branch_id),
-        {},
-    )
-
-
-def _bundle_event(bundle: dict[str, Any], event_id: int | None) -> dict[str, Any] | None:
-    return next(
-        (item for item in bundle.get("events") or [] if _integer(item.get("id")) == event_id),
-        None,
-    )
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value)
-    except (TypeError, ValueError):
-        return None
-
-
-def _suffix_int(value: str) -> int | None:
-    return _integer(str(value or "").rsplit(":", 1)[-1])

+ 0 - 774
visualization/backend/app/source_lineage/resolver.py

@@ -1,774 +0,0 @@
-from __future__ import annotations
-
-import hashlib
-import json
-import re
-from typing import Any
-
-from .schema import EvidenceSpec, SourceBinding, SourceRecord, unresolved_binding
-
-
-_INDEX = re.compile(r"\[([0-9]+)\]")
-
-
-def json_pointer(path: str | None) -> str:
-    """Convert the visualization's legacy dotted paths to RFC 6901 pointers."""
-    value = str(path or "").strip()
-    if not value:
-        return ""
-    if " / " in value:
-        return ""
-    value = _INDEX.sub(r".\1", value)
-    parts = [part for part in value.split(".") if part]
-    return "/" + "/".join(
-        part.replace("~", "~0").replace("/", "~1") for part in parts
-    )
-
-
-def pointer_lookup(value: Any, pointer: str) -> tuple[bool, Any]:
-    if not pointer:
-        return True, value
-    current = value
-    for raw in pointer.lstrip("/").split("/"):
-        part = raw.replace("~1", "/").replace("~0", "~")
-        if isinstance(current, list):
-            try:
-                current = current[int(part)]
-            except (ValueError, IndexError):
-                return False, None
-        elif isinstance(current, dict):
-            if part not in current:
-                return False, None
-            current = current[part]
-        else:
-            return False, None
-    return True, current
-
-
-def pointer_value(value: Any, pointer: str) -> Any:
-    return pointer_lookup(value, pointer)[1]
-
-
-def source_digest(value: Any) -> str:
-    serialized = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
-    return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
-
-
-class SourceResolver:
-    """Maps semantic CardData fields to deduplicated physical source records.
-
-    It deliberately does not infer adoption from matching text.  The only
-    evidence inputs are the field's declared relation and physical origin.
-    """
-
-    def __init__(
-        self,
-        *,
-        bundle: dict[str, Any],
-        event_details: dict[int, dict[str, Any]],
-    ) -> None:
-        self.bundle = bundle
-        self.event_details = event_details
-        self.sources: dict[str, SourceRecord] = {}
-        self._span_offsets: dict[tuple[int, str, str], int] = {}
-
-    def binding_for_field(
-        self,
-        field: dict[str, Any],
-        *,
-        binding_id: str,
-        role: str | None = None,
-    ) -> SourceBinding:
-        source = field.get("source") if isinstance(field.get("source"), dict) else {}
-        source_kind = str(source.get("kind") or "")
-        source_ref = str(source.get("ref") or "")
-        raw, physical = self._raw_source(source_kind, source_ref, source)
-        if source_kind == "calculation":
-            raw = {
-                "value": field.get("value"),
-                "calculation": source.get("label"),
-            }
-        source_id = self._source_id(source_kind, source_ref, source, physical)
-        field_path = str(source.get("fieldPath") or "")
-        pointer = json_pointer(field_path)
-        resolution = "resolved"
-
-        if raw is None:
-            missing = self._ensure_missing_source(
-                source_id,
-                source,
-                reason=f"未找到 {source.get('label') or source_ref or '原始记录'}",
-            )
-            return unresolved_binding(
-                binding_id,
-                reason=missing["locator"]["reason"],
-                role=role or self._role(field),
-                source_id=source_id,
-                code="source-record-not-saved",
-            )
-
-        selector: dict[str, Any]
-        found, selected = pointer_lookup(raw, pointer) if pointer else (True, raw)
-        if source_kind == "calculation":
-            selector = {"kind": "json-pointer", "path": "/value"}
-            selected = field.get("value")
-        elif source_kind == "artifact" and field_path in {"snapshotRef", "recordCounts", "historicalVersion"}:
-            selector = {"kind": "whole-record"}
-            selected = field.get("value")
-        elif pointer and found:
-            selector = {"kind": "json-pointer", "path": pointer}
-        elif pointer:
-            # The legacy CardData path may refer to a derived projection rather
-            # than a persisted record.  Keep the source visible, but do not
-            # pretend that the pointer resolves against it.
-            selector = {
-                "kind": "unresolved",
-                "reason": f"原始记录中无法安全定位字段 {field_path}",
-                "code": "field-path-mismatch",
-            }
-            selected = field.get("value")
-            resolution = "partial"
-        else:
-            selector = {"kind": "whole-record"}
-            selected = field.get("value")
-
-        record = self._ensure_source(source_id, source_kind, source, raw, physical)
-        record.setdefault("selectedValues", []).append(
-            {
-                "bindingId": binding_id,
-                "fieldId": field.get("id"),
-                "label": field.get("label"),
-                "path": pointer or field_path or None,
-                "value": selected,
-            }
-        )
-        transform = self._transform(source_kind, source)
-        if (
-            transform.get("kind") == "direct"
-            and found
-            and not _equivalent_value(field.get("value"), selected)
-        ):
-            transform = {
-                "kind": "parsed",
-                "operation": "business-projection-v1",
-                "outputKey": str(field.get("id") or binding_id),
-            }
-        binding: SourceBinding = {
-            "id": binding_id,
-            "sourceId": source_id,
-            "role": role or self._role(field),  # type: ignore[typeddict-item]
-            "selector": selector,
-            "transform": transform,
-            "evidence": self._evidence(field, source_kind),
-            "resolution": resolution,  # type: ignore[typeddict-item]
-        }
-        if transform.get("kind") == "parsed":
-            binding["evidence"]["confidence"] = "deterministic"
-        if binding["resolution"] != "resolved" and transform.get("kind") == "direct":
-            binding["transform"] = {
-                "kind": "parsed",
-                "operation": "unresolved-source-projection-v1",
-                "outputKey": str(field.get("id") or binding_id),
-            }
-            binding["evidence"]["confidence"] = "unconfirmed"
-        if "未安全关联" in str(source.get("label") or ""):
-            binding["resolution"] = "unsafe"
-            binding["evidence"]["confidence"] = "unconfirmed"
-        return binding
-
-    def binding_for_event(
-        self,
-        event_id: int,
-        *,
-        binding_id: str,
-        path: str = "",
-        role: str = "process",
-        availability: str = "produced-by-run",
-    ) -> SourceBinding:
-        event = self.event_details.get(int(event_id))
-        source_id = f"event:{event_id}"
-        if event is None:
-            self._ensure_missing_source(
-                source_id,
-                {"label": f"Event {event_id}", "ref": source_id},
-                reason=f"Event {event_id} 或 Event Body 未保存",
-            )
-            return unresolved_binding(
-                binding_id,
-                reason=f"Event {event_id} 或 Event Body 未保存",
-                role=role,
-                source_id=source_id,
-                code="event-body-not-saved",
-            )
-        source = {
-            "kind": "runtime-event",
-            "label": str(event.get("event_name") or f"Event {event_id}"),
-            "ref": source_id,
-            "fieldPath": path,
-        }
-        pointer = json_pointer(path)
-        found, selected = pointer_lookup(event, pointer) if pointer else (True, event)
-        resolution = "resolved" if found else "partial"
-        if not pointer:
-            selector = {"kind": "whole-record"}
-        elif found:
-            selector = {"kind": "json-pointer", "path": pointer}
-        else:
-            selector = {
-                "kind": "unresolved",
-                "reason": f"Event {event_id} 中不存在字段 {path}",
-                "code": "field-path-mismatch",
-            }
-        record = self._ensure_source(source_id, "runtime-event", source, event, event)
-        record.setdefault("selectedValues", []).append(
-            {"bindingId": binding_id, "fieldId": binding_id, "label": source["label"], "path": pointer or None, "value": selected}
-        )
-        return {
-            "id": binding_id,
-            "sourceId": source_id,
-            "role": role,  # type: ignore[typeddict-item]
-            "selector": selector,
-            "transform": {"kind": "direct"},
-            "evidence": {
-                "availability": availability,
-                "adoption": "not-applicable" if availability == "produced-by-run" else "not-recorded",
-                "confidence": "exact",
-            },  # type: ignore[typeddict-item]
-            "resolution": resolution,  # type: ignore[typeddict-item]
-        }
-
-    def text_span_binding(
-        self,
-        event_id: int,
-        *,
-        binding_id: str,
-        path: str,
-        exact_text: Any,
-        role: str,
-    ) -> SourceBinding:
-        event = self.event_details.get(int(event_id))
-        source_id = f"event:{event_id}"
-        if event is None:
-            self._ensure_missing_source(
-                source_id,
-                {"label": f"Event {event_id}", "ref": source_id},
-                reason=f"Event {event_id} 或 Event Body 未保存",
-            )
-            return unresolved_binding(
-                binding_id,
-                reason=f"Event {event_id} 或 Event Body 未保存",
-                role=role,
-                source_id=source_id,
-                code="event-body-not-saved",
-            )
-        pointer = json_pointer(path)
-        raw_text = pointer_value(event, pointer)
-        needle = str(exact_text or "")
-        if not isinstance(raw_text, str) or not needle:
-            binding = self.binding_for_event(
-                event_id,
-                binding_id=binding_id,
-                path=path,
-                role=role,
-                availability="returned-to-agent" if role == "output" else "direct-read",
-            )
-            binding["transform"] = {
-                "kind": "parsed",
-                "operation": "structured-text-section-v1",
-                "outputKey": binding_id,
-            }
-            binding["evidence"]["confidence"] = "deterministic"
-            return binding
-        span_key = (int(event_id), pointer, needle)
-        search_from = self._span_offsets.get(span_key, 0)
-        start = raw_text.find(needle, search_from)
-        if start < 0 and search_from:
-            start = raw_text.find(needle)
-        if start < 0:
-            binding = self.binding_for_event(
-                event_id,
-                binding_id=binding_id,
-                path=path,
-                role=role,
-                availability="returned-to-agent" if role == "output" else "direct-read",
-            )
-            binding["transform"] = {
-                "kind": "parsed",
-                "operation": "normalized-text-section-v1",
-                "outputKey": binding_id,
-            }
-            binding["evidence"]["confidence"] = "deterministic"
-            return binding
-        record = self._ensure_source(
-            source_id,
-            "runtime-event",
-            {"label": str(event.get("event_name") or f"Event {event_id}"), "ref": source_id},
-            event,
-            event,
-        )
-        record.setdefault("selectedValues", []).append(
-            {"bindingId": binding_id, "fieldId": binding_id, "label": binding_id, "path": pointer, "value": needle}
-        )
-        self._span_offsets[span_key] = start + len(needle)
-        return {
-            "id": binding_id,
-            "sourceId": source_id,
-            "role": role,  # type: ignore[typeddict-item]
-            "selector": {
-                "kind": "text-span",
-                "path": pointer,
-                "startCodePoint": start,
-                "endCodePoint": start + len(needle),
-                "exactText": needle,
-                "sourceDigest": source_digest(raw_text),
-            },
-            "transform": {"kind": "direct"},
-            "evidence": {
-                "availability": "returned-to-agent" if role == "output" else "direct-read",
-                "adoption": "not-recorded",
-                "confidence": "exact",
-            },
-            "resolution": "resolved",
-        }
-
-    def add_unresolved_source(self, binding: SourceBinding) -> None:
-        source_id = binding["sourceId"]
-        reason = str(binding.get("selector", {}).get("reason") or "原始记录无法定位")
-        self._ensure_missing_source(
-            source_id,
-            {"label": "记录缺失"},
-            reason=reason,
-        )
-
-    def add_log_anchor(
-        self, event_id: int, module: dict[str, Any], *, event_msg_id: str
-    ) -> str:
-        source_id = f"log:event:{event_id}"
-        self.sources[source_id] = {
-            "id": source_id,
-            "kind": "log-anchor",
-            "label": f"锚定日志模块 · Event {event_id}",
-            "locator": {
-                "table": "script_build_log",
-                "eventId": event_id,
-                "msgId": event_msg_id,
-                "logAnchor": module.get("anchor"),
-            },
-            "completeness": "complete",
-            "resultState": "present",
-            "truncated": False,
-            "selectedValues": [],
-            "rawRecord": module.get("content"),
-        }
-        return source_id
-
-    def empty_database_result_binding(
-        self,
-        *,
-        binding_id: str,
-        table: str,
-        filters: dict[str, Any],
-        label: str,
-        role: str = "output",
-    ) -> SourceBinding:
-        """Represent a successful zero-row lookup without calling it missing."""
-        filter_key = ",".join(f"{key}={filters[key]}" for key in sorted(filters))
-        source_id = f"db-result:{table}:{filter_key or 'all'}"
-        self.sources[source_id] = {
-            "id": source_id,
-            "kind": "database",
-            "label": label,
-            "locator": {
-                "table": table,
-                "filters": filters,
-                "resultCount": 0,
-            },
-            "completeness": "complete",
-            "resultState": "empty",
-            "truncated": False,
-            "selectedValues": [{
-                "bindingId": binding_id,
-                "fieldId": binding_id,
-                "label": "查询结果",
-                "path": "",
-                "value": [],
-            }],
-            "rawRecord": [],
-        }
-        return {
-            "id": binding_id,
-            "sourceId": source_id,
-            "role": role,  # type: ignore[typeddict-item]
-            "selector": {"kind": "whole-record"},
-            "transform": {
-                "kind": "calculated",
-                "operation": "数据库查询结果计数为 0",
-            },
-            "evidence": {
-                "availability": "visualization-derived",
-                "adoption": "not-applicable",
-                "confidence": "deterministic",
-            },
-            "resolution": "resolved",
-        }
-
-    def members_binding(
-        self,
-        field: dict[str, Any],
-        *,
-        binding_id: str,
-        members: list[dict[str, Any]],
-        reducer: str,
-        role: str = "output",
-    ) -> SourceBinding:
-        """Create a deterministic aggregate while retaining every member ref."""
-        base_field = {
-            **field,
-            "source": {
-                "kind": "calculation",
-                "label": reducer,
-                "ref": f"calculation:{binding_id}",
-                "fieldPath": "value",
-            },
-            "relation": "persisted-output",
-        }
-        base = self.binding_for_field(base_field, binding_id=binding_id, role=role)
-        member_selectors: list[dict[str, Any]] = []
-        member_resolutions: list[str] = []
-        for index, member in enumerate(members, 1):
-            member_binding = self.binding_for_field(
-                member,
-                binding_id=f"{binding_id}:member:{index}",
-                role=role,
-            )
-            member_selectors.append(
-                {
-                    "sourceId": member_binding["sourceId"],
-                    "selector": member_binding["selector"],
-                }
-            )
-            member_resolutions.append(str(member_binding.get("resolution") or "missing"))
-            member_source = self.sources.get(member_binding["sourceId"])
-            if member_source and member_source.get("selectedValues"):
-                member_source["selectedValues"][-1]["aggregateBindingId"] = binding_id
-        if not member_selectors:
-            base["selector"] = {
-                "kind": "unresolved",
-                "reason": "确定性聚合没有找到可定位的参与记录",
-                "code": "binding-map-missing",
-            }
-            base["resolution"] = "missing"
-            base["evidence"]["confidence"] = "unconfirmed"
-            return base
-        base["selector"] = {
-            "kind": "members",
-            "members": member_selectors,
-            "reducer": reducer,
-        }
-        base["transform"] = {"kind": "calculated", "operation": reducer}
-        base["evidence"] = {
-            "availability": "visualization-derived",
-            "adoption": "not-applicable",
-            "confidence": "deterministic",
-        }
-        if any(value in {"missing", "unsafe"} for value in member_resolutions):
-            base["resolution"] = "partial"
-            base["evidence"]["confidence"] = "unconfirmed"
-        elif any(value == "partial" for value in member_resolutions):
-            base["resolution"] = "partial"
-        return base
-
-    def _raw_source(
-        self, source_kind: str, source_ref: str, source: dict[str, Any]
-    ) -> tuple[Any, dict[str, Any]]:
-        if source_kind == "runtime-event":
-            event_id = _suffix_int(source_ref)
-            if event_id is not None:
-                event = self.event_details.get(event_id)
-                return event, event or {}
-            # A runtime source is only trustworthy when it names one concrete
-            # Event.  Treating an empty ref as "all related Events" previously
-            # let a multipath evaluator impersonate a missing script_evaluator
-            # in round summaries.
-            return None, {"detailRef": source_ref, "reason": "未保存可定位的 Event ref"}
-        if source_kind == "database":
-            return self._database_record(source_ref, str(source.get("label") or ""))
-        if source_kind == "artifact":
-            if "branch:" in source_ref:
-                branch_id = _suffix_int(source_ref)
-                row = _by_int(self.bundle.get("branches"), "branch_id", branch_id)
-                # CardData paths are rooted at the branch record
-                # (`candidate_snapshot.snapshot`), so retain that root rather
-                # than pre-extracting candidate_snapshot and breaking the
-                # declared JSON Pointer.
-                return row, {"table": "script_build_branch", "recordId": (row or {}).get("id")}
-            artifact = self.bundle.get("currentArtifact")
-            return artifact, {"artifactRef": source_ref or "artifact:base:current"}
-        if source_kind == "calculation":
-            return {"value": None, "calculation": source.get("label")}, {"calculation": source.get("label")}
-        return None, {"ref": source_ref}
-
-    def _database_record(self, source_ref: str, label: str) -> tuple[Any, dict[str, Any]]:
-        normalized = label.lower()
-        if source_ref.startswith("missing-convergence:"):
-            parts = source_ref.split(":", 4)
-            record_type = parts[1] if len(parts) > 1 else "unknown"
-            round_index = _integer(parts[3]) if len(parts) > 3 else None
-            branch_ids = [
-                _integer(value)
-                for value in (parts[4].split(",") if len(parts) > 4 else [])
-                if _integer(value) is not None
-            ]
-            return [], {
-                "tables": (
-                    ["script_build_event", "script_build_event_body"]
-                    if record_type == "review"
-                    else ["script_build_multipath_decision"]
-                ),
-                "filters": {
-                    "round_index": round_index,
-                    "branch_ids": branch_ids,
-                },
-                "resultCount": 0,
-            }
-        if "script_build_record" in normalized or source_ref == "run:objective" or source_ref == "run:final-result":
-            row = self.bundle.get("record")
-            return row, {"table": "script_build_record", "recordId": (row or {}).get("id")}
-        if "data_decision" in normalized or source_ref.startswith("data-decision:"):
-            row = _by_int(self.bundle.get("dataDecisions"), "id", _suffix_int(source_ref))
-            return row, {"table": "script_build_data_decision", "recordId": (row or {}).get("id")}
-        if "multipath" in normalized or source_ref.startswith("multipath-decision:"):
-            row = _by_int(self.bundle.get("multipathDecisions"), "id", _suffix_int(source_ref))
-            return row, {"table": "script_build_multipath_decision", "recordId": (row or {}).get("id")}
-        if "domain_info" in normalized or source_ref.startswith("domain-info:"):
-            row = _by_int(self.bundle.get("domainInfo"), "id", _suffix_int(source_ref))
-            return row, {"table": "script_build_domain_info", "recordId": (row or {}).get("id")}
-        if "script_build_round" in normalized or source_ref.startswith("round:") and ":branch:" not in source_ref:
-            round_index = _round_index(source_ref)
-            row = _by_int(self.bundle.get("rounds"), "round_index", round_index)
-            return row, {"table": "script_build_round", "recordId": (row or {}).get("id")}
-        if "script_build_branch" in normalized or ":branch:" in source_ref:
-            branch_id = _branch_id(source_ref)
-            row = _by_int(self.bundle.get("branches"), "branch_id", branch_id)
-            return row, {"table": "script_build_branch", "recordId": (row or {}).get("id")}
-        return None, {"table": label or "unknown", "ref": source_ref}
-
-    def _ensure_source(
-        self,
-        source_id: str,
-        source_kind: str,
-        source: dict[str, Any],
-        raw: Any,
-        physical: dict[str, Any],
-    ) -> SourceRecord:
-        existing = self.sources.get(source_id)
-        if existing is not None:
-            return existing
-        kind = {
-            "database": "database",
-            "runtime-event": "runtime-event",
-            "artifact": "artifact",
-            "calculation": "calculation",
-            "log-anchor": "log-anchor",
-        }.get(source_kind, "missing")
-        if "历史回退" in str(source.get("label") or ""):
-            kind = "historical-fallback"
-        completeness = "complete"
-        truncated = False
-        omitted = 0
-        if kind == "runtime-event" and isinstance(raw, dict):
-            sides = [raw.get("input"), raw.get("output")]
-            if raw.get("input") is None and raw.get("output") is None:
-                completeness = "partial"
-            for side in sides:
-                if isinstance(side, dict) and side.get("truncated"):
-                    truncated = True
-                    omitted += int(side.get("omittedCharacters") or 0)
-        record: SourceRecord = {
-            "id": source_id,
-            "kind": kind,  # type: ignore[typeddict-item]
-            "label": str(source.get("label") or source.get("ref") or source_id),
-            "locator": physical,
-            "completeness": "partial" if truncated else completeness,  # type: ignore[typeddict-item]
-            "resultState": (
-                "empty"
-                if raw == []
-                or (
-                    kind == "calculation"
-                    and isinstance(raw, dict)
-                    and raw.get("value") == []
-                )
-                else "present"
-            ),
-            "truncated": truncated,
-            "selectedValues": [],
-            "rawRecord": raw,
-        }
-        if omitted:
-            record["omittedCharacters"] = omitted
-        if isinstance(raw, dict) and kind == "runtime-event":
-            record["status"] = str(raw.get("status") or "unknown")
-            record["durationMs"] = _integer(raw.get("duration_ms"))
-            record["locator"] = {
-                "eventId": raw.get("id"),
-                "eventName": raw.get("event_name"),
-                "eventType": raw.get("event_type"),
-                "table": "script_build_event + script_build_event_body",
-            }
-        self.sources[source_id] = record
-        return record
-
-    def _ensure_missing_source(
-        self, source_id: str, source: dict[str, Any], *, reason: str
-    ) -> SourceRecord:
-        record = self.sources.get(source_id)
-        if record is None:
-            record = {
-                "id": source_id,
-                "kind": "missing",
-                "label": str(source.get("label") or source_id),
-                "locator": {"ref": source.get("ref"), "reason": reason},
-                "completeness": "missing",
-                "resultState": "missing",
-                "truncated": False,
-                "selectedValues": [],
-                "rawRecord": None,
-            }
-            self.sources[source_id] = record
-        return record
-
-    @staticmethod
-    def _source_id(
-        source_kind: str,
-        source_ref: str,
-        source: dict[str, Any],
-        physical: dict[str, Any],
-    ) -> str:
-        if source_kind == "runtime-event" and _suffix_int(source_ref) is not None:
-            return f"event:{_suffix_int(source_ref)}"
-        if source_kind == "database" and physical.get("table"):
-            return f"db:{physical['table']}:{physical.get('recordId') or source_ref or 'unknown'}"
-        if source_kind == "artifact":
-            return f"artifact:{source_ref or physical.get('artifactRef') or 'unknown'}"
-        key = f"{source_kind}:{source_ref}:{source.get('label')}"
-        return f"source:{hashlib.sha1(key.encode('utf-8')).hexdigest()[:12]}"
-
-    @staticmethod
-    def _role(field: dict[str, Any]) -> str:
-        relation = str(field.get("relation") or "")
-        return {
-            "direct-input": "input",
-            "previous-result": "basis",
-            "standing-constraint": "constraint",
-            "explicit-basis": "basis",
-            "available-upstream": "basis",
-            "persisted-output": "output",
-            "run-output": "output",
-        }.get(relation, "basis")
-
-    @staticmethod
-    def _transform(source_kind: str, source: dict[str, Any]) -> dict[str, Any]:
-        if source_kind == "calculation":
-            return {"kind": "calculated", "operation": str(source.get("label") or "确定性计算")}
-        if "历史回退" in str(source.get("label") or ""):
-            return {
-                "kind": "fallback",
-                "selectedSourceId": str(source.get("ref") or ""),
-                "candidateSourceIds": [str(source.get("ref") or "")],
-            }
-        return {"kind": "direct"}
-
-    @staticmethod
-    def _evidence(field: dict[str, Any], source_kind: str) -> EvidenceSpec:
-        relation = str(field.get("relation") or "")
-        if source_kind == "calculation":
-            return {
-                "availability": "visualization-derived",
-                "adoption": "not-applicable",
-                "confidence": "deterministic",
-            }
-        if relation == "explicit-basis":
-            return {
-                "availability": "direct-read",
-                "adoption": "explicitly-adopted",
-                "confidence": "exact",
-            }
-        if relation in {"direct-input", "standing-constraint"}:
-            return {
-                "availability": "direct-read",
-                "adoption": "not-recorded",
-                "confidence": "exact",
-            }
-        if relation == "previous-result":
-            return {
-                "availability": "returned-to-agent" if source_kind == "runtime-event" else "available-upstream",
-                "adoption": "not-recorded",
-                "confidence": "exact" if source_kind == "runtime-event" else "safe-association",
-            }
-        if relation == "available-upstream":
-            return {
-                "availability": "available-upstream",
-                "adoption": "not-recorded",
-                "confidence": "safe-association",
-            }
-        if relation == "persisted-output":
-            availability = "returned-to-agent" if source_kind == "runtime-event" else "produced-by-run"
-            return {
-                "availability": availability,
-                "adoption": "not-recorded" if availability == "returned-to-agent" else "not-applicable",
-                "confidence": "exact",
-            }
-        if relation == "run-output":
-            return {
-                "availability": "produced-by-run",
-                "adoption": "not-applicable",
-                "confidence": "exact",
-            }
-        return {
-            "availability": "available-upstream",
-            "adoption": "not-recorded",
-            "confidence": "unconfirmed",
-        }
-
-
-def _integer(value: Any) -> int | None:
-    try:
-        return int(value)
-    except (TypeError, ValueError):
-        return None
-
-
-def _suffix_int(value: str) -> int | None:
-    return _integer(str(value or "").rsplit(":", 1)[-1])
-
-
-def _round_index(ref: str) -> int | None:
-    match = re.search(r"(?:^|:)round:(\d+)", f":{ref}")
-    return _integer(match.group(1)) if match else None
-
-
-def _branch_id(ref: str) -> int | None:
-    match = re.search(r":branch:(\d+)", ref)
-    return _integer(match.group(1)) if match else None
-
-
-def _by_int(items: Any, key: str, wanted: int | None) -> dict[str, Any] | None:
-    if wanted is None:
-        return None
-    return next(
-        (
-            item
-            for item in items or []
-            if isinstance(item, dict) and _integer(item.get(key)) == wanted
-        ),
-        None,
-    )
-
-
-def _equivalent_value(left: Any, right: Any) -> bool:
-    try:
-        return json.dumps(left, ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
-            right, ensure_ascii=False, sort_keys=True, default=str
-        )
-    except (TypeError, ValueError):
-        return str(left) == str(right)

+ 0 - 85
visualization/backend/app/source_lineage/schema.py

@@ -1,85 +0,0 @@
-from __future__ import annotations
-
-from typing import Any, Literal, TypedDict
-
-
-Completeness = Literal["complete", "partial", "missing"]
-Resolution = Literal["resolved", "partial", "missing", "unsafe"]
-
-
-class EvidenceSpec(TypedDict):
-    availability: Literal[
-        "direct-read",
-        "returned-to-agent",
-        "available-upstream",
-        "produced-by-run",
-        "visualization-derived",
-        "unavailable",
-    ]
-    adoption: Literal[
-        "explicitly-adopted",
-        "explicitly-rejected",
-        "not-recorded",
-        "not-applicable",
-    ]
-    confidence: Literal["exact", "safe-association", "deterministic", "unconfirmed"]
-
-
-class SourceBinding(TypedDict):
-    id: str
-    sourceId: str
-    role: Literal["input", "basis", "constraint", "process", "output", "status"]
-    selector: dict[str, Any]
-    transform: dict[str, Any]
-    evidence: EvidenceSpec
-    resolution: Resolution
-
-
-class SourceRecord(TypedDict, total=False):
-    id: str
-    kind: Literal[
-        "database",
-        "runtime-event",
-        "artifact",
-        "log-anchor",
-        "calculation",
-        "historical-fallback",
-        "missing",
-    ]
-    label: str
-    locator: dict[str, Any]
-    status: str
-    durationMs: int | None
-    completeness: Completeness
-    resultState: Literal["present", "empty", "missing", "unavailable"]
-    truncated: bool
-    omittedCharacters: int
-    selectedValues: list[dict[str, Any]]
-    rawRecord: Any
-
-
-def unresolved_binding(
-    binding_id: str,
-    *,
-    reason: str,
-    role: str = "basis",
-    source_id: str | None = None,
-    resolution: Resolution = "missing",
-    code: str | None = None,
-) -> SourceBinding:
-    selector = {"kind": "unresolved", "reason": reason}
-    if code:
-        selector["code"] = code
-    return {
-        "id": binding_id,
-        "sourceId": source_id or f"missing:{binding_id}",
-        "role": role,  # type: ignore[typeddict-item]
-        "selector": selector,
-        "transform": {"kind": "direct"},
-        "evidence": {
-            "availability": "unavailable",
-            "adoption": "not-recorded",
-            "confidence": "unconfirmed",
-        },
-        "resolution": resolution,
-    }

+ 1 - 2
visualization/backend/pytest.ini

@@ -1,5 +1,4 @@
 [pytest]
 testpaths = tests
-pythonpath = .
-cache_dir = ../.cache/pytest
+addopts = -q
 

+ 5 - 7
visualization/backend/requirements.txt

@@ -1,8 +1,6 @@
-fastapi==0.128.1
-uvicorn==0.39.0
-pydantic==2.12.5
-sqlalchemy>=2.0,<3
-pymysql>=1.1,<2
-pytest==8.4.2
-httpx>=0.27,<1
+fastapi==0.116.1
+httpx==0.28.1
+pydantic==2.11.7
+pytest==8.4.1
+uvicorn==0.35.0
 

+ 116 - 0
visualization/backend/scripts/validate_host_contracts.py

@@ -0,0 +1,116 @@
+"""Validate fake records against the live parent Host and Agent contracts.
+
+Run from visualization/backend with both parent source roots and Host dependencies
+on PYTHONPATH.  This remains a verification command, never a runtime dependency.
+"""
+
+from __future__ import annotations
+
+from app.contracts import SCHEMA_CATALOG
+from app.fake_data import ARTIFACT_DIGESTS, GRAPH
+from agent.orchestration.wire import (
+    AttemptView,
+    OperationView,
+    PlannerDecisionView,
+    TaskSpecView,
+    TaskView,
+    ValidationView,
+)
+from script_build_host.domain.phase_three_artifacts import (
+    canonicalize_legacy_projection,
+    hydrate_root_delivery_manifest,
+    root_delivery_input_closure_digest,
+)
+from script_build_host.domain.phase_two_artifacts import hydrate_phase_two_artifact
+from script_build_host.domain.task_contracts import ScriptTaskContractV1
+from script_build_host.infrastructure.legacy_tables import (
+    script_build_element,
+    script_build_paragraph,
+    script_build_paragraph_element,
+)
+from script_build_host.infrastructure.tables import (
+    artifact_version_table,
+    input_snapshot_table,
+)
+from script_build_host.repositories.sqlalchemy import _snapshot_from_row
+
+WIRE_MODELS = {
+    "TaskView": TaskView,
+    "TaskSpecView": TaskSpecView,
+    "OperationView": OperationView,
+    "AttemptView": AttemptView,
+    "ValidationView": ValidationView,
+    "PlannerDecisionView": PlannerDecisionView,
+}
+
+PHASE_TWO_MODELS = {
+    "StructureArtifactV1",
+    "ParagraphArtifactV1",
+    "ElementSetArtifactV1",
+    "ComparisonArtifactV1",
+    "StructuredScriptArtifactV1",
+    "CandidatePortfolioArtifactV1",
+}
+
+
+def _assert_table_fields(model_name: str, table: object) -> None:
+    columns = tuple(column.name for column in table.columns)  # type: ignore[attr-defined]
+    assert SCHEMA_CATALOG[model_name] == columns, (model_name, columns)
+
+
+def main() -> None:
+    for node in GRAPH.nodes:
+        if node.record.model_name in WIRE_MODELS:
+            WIRE_MODELS[node.record.model_name].model_validate(node.record.payload)
+        if node.record.model_name == "ScriptTaskContractV1":
+            contract = ScriptTaskContractV1.from_payload(node.record.payload)
+            if contract.task_kind.value in {"compose", "candidate-portfolio"}:
+                contract.require_execution_ready()
+        if node.record.model_name in PHASE_TWO_MODELS:
+            hydrate_phase_two_artifact(node.record.payload)
+
+    records = {node.record.model_name: node.record.payload for node in GRAPH.nodes}
+    manifest = hydrate_root_delivery_manifest(records["RootDeliveryManifestV1"])
+    structured = hydrate_phase_two_artifact(records["StructuredScriptArtifactV1"])
+    canonical = canonicalize_legacy_projection(
+        structured,
+        direction=records["LegacyProjectionCanonicalV1"]["direction"],
+        summary=records["LegacyProjectionCanonicalV1"]["summary"],
+    )
+    assert canonical.content_payload() == records["LegacyProjectionCanonicalV1"]
+    assert manifest.legacy_projection_digest == canonical.canonical_sha256
+
+    refs = records["RootDeliveryManifestV1"]
+    assert manifest.input_closure_digest == root_delivery_input_closure_digest(
+        direction_ref=refs["direction_ref"],
+        direction_digest=ARTIFACT_DIGESTS[110],
+        candidate_portfolio_ref=refs["candidate_portfolio_ref"],
+        candidate_portfolio_digest=ARTIFACT_DIGESTS[150],
+        structured_script_ref=refs["structured_script_ref"],
+        structured_script_digest=ARTIFACT_DIGESTS[140],
+    )
+
+    snapshot_row = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "InputSnapshotRow"
+    )
+    hydrated_snapshot = _snapshot_from_row(snapshot_row)
+    logical_snapshot = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "ScriptBuildInputSnapshotV1"
+    )
+    assert hydrated_snapshot.snapshot_id == logical_snapshot["snapshot_id"]
+    assert hydrated_snapshot.canonical_sha256 == logical_snapshot["canonical_sha256"]
+
+    _assert_table_fields("InputSnapshotRow", input_snapshot_table)
+    _assert_table_fields("ArtifactVersionRow", artifact_version_table)
+    _assert_table_fields("ParagraphRow", script_build_paragraph)
+    _assert_table_fields("ElementRow", script_build_element)
+    _assert_table_fields("ParagraphElementLinkRow", script_build_paragraph_element)
+    print(f"validated {len(GRAPH.nodes)} fake nodes against live source contracts")
+
+
+if __name__ == "__main__":
+    main()

+ 0 - 411
visualization/backend/scripts/validate_source_inspector.py

@@ -1,411 +0,0 @@
-#!/usr/bin/env python3
-"""Read-only validator for every visible data-source Inspector in real runs."""
-
-from __future__ import annotations
-
-import argparse
-import hashlib
-import json
-import sys
-import urllib.error
-import urllib.parse
-import urllib.request
-from collections import Counter
-from concurrent.futures import ThreadPoolExecutor, as_completed
-from typing import Any
-
-
-HIDDEN_BUSINESS_KEYS = {"technical", "changes"}
-
-
-def request_json(base_url: str, path: str) -> tuple[int, Any]:
-    url = f"{base_url.rstrip('/')}{path}"
-    try:
-        with urllib.request.urlopen(url, timeout=60) as response:
-            return response.status, json.load(response)
-    except urllib.error.HTTPError as exc:
-        try:
-            payload = json.load(exc)
-        except Exception:
-            payload = {"detail": str(exc)}
-        return exc.code, payload
-
-
-def quote_ref(value: str) -> str:
-    return urllib.parse.quote(value, safe="")
-
-
-def collect_detail_refs(value: Any) -> list[str]:
-    found: list[str] = []
-
-    def visit(node: Any) -> None:
-        if isinstance(node, dict):
-            for key, child in node.items():
-                if key == "detailRef" and isinstance(child, str) and child:
-                    found.append(child)
-                visit(child)
-        elif isinstance(node, list):
-            for child in node:
-                visit(child)
-
-    visit(value)
-    return list(dict.fromkeys(found))
-
-
-def pointer_lookup(value: Any, pointer: str) -> tuple[bool, Any]:
-    if not pointer:
-        return True, value
-    current = value
-    for raw in pointer.lstrip("/").split("/"):
-        part = raw.replace("~1", "/").replace("~0", "~")
-        if isinstance(current, list):
-            try:
-                current = current[int(part)]
-            except (ValueError, IndexError):
-                return False, None
-        elif isinstance(current, dict) and part in current:
-            current = current[part]
-        else:
-            return False, None
-    return True, current
-
-
-def digest(value: Any) -> str:
-    serialized = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
-    return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
-
-
-def validate_binding(binding: dict[str, Any], sources: dict[str, Any]) -> list[str]:
-    errors: list[str] = []
-    source_id = str(binding.get("sourceId") or "")
-    selector = binding.get("selector") or {}
-    resolution = binding.get("resolution")
-    source = sources.get(source_id)
-    if source is None:
-        return [f"binding {binding.get('id')} 的 sourceId {source_id} 未返回"]
-    if resolution == "resolved" and source.get("resultState") in {"missing", "unavailable"}:
-        errors.append(
-            f"binding {binding.get('id')} 标记 resolved,但来源状态为 {source.get('resultState')}"
-        )
-    if selector.get("kind") == "unresolved":
-        code = str(selector.get("code") or "")
-        reason = str(selector.get("reason") or "")
-        if not reason:
-            errors.append(f"binding {binding.get('id')} 的 unresolved 原因为空")
-        if not code:
-            errors.append(f"binding {binding.get('id')} 的 unresolved 缺少原因码")
-        if code in {"binding-map-missing", "field-path-mismatch"}:
-            errors.append(f"binding {binding.get('id')} 存在可视化映射错误 {code}")
-        elif code not in {
-            "source-record-not-saved",
-            "event-body-not-saved",
-            "unsafe-association",
-            "reliable-log-anchor-unavailable",
-        }:
-            errors.append(f"binding {binding.get('id')} 使用了未允许的缺失原因码 {code}")
-        if code in {"source-record-not-saved", "event-body-not-saved"} and source.get("resultState") not in {"missing", "unavailable"}:
-            errors.append(
-                f"binding {binding.get('id')} 声称原始记录未保存,"
-                f"但来源状态为 {source.get('resultState')}"
-            )
-        return errors
-    if selector.get("kind") == "json-pointer":
-        found, _selected = pointer_lookup(source.get("rawRecord"), selector.get("path") or "")
-        if not found:
-            errors.append(f"binding {binding.get('id')} 无法反向定位 {selector.get('path')}")
-    elif selector.get("kind") == "text-span":
-        found, text = pointer_lookup(source.get("rawRecord"), selector.get("path") or "")
-        if not found or not isinstance(text, str):
-            errors.append(f"binding {binding.get('id')} 的原文字段不存在")
-        else:
-            characters = list(text)
-            start = int(selector.get("startCodePoint") or 0)
-            end = int(selector.get("endCodePoint") or 0)
-            if "".join(characters[start:end]) != selector.get("exactText"):
-                errors.append(f"binding {binding.get('id')} 的原文区间不匹配")
-            if digest(text) != selector.get("sourceDigest"):
-                errors.append(f"binding {binding.get('id')} 的 sourceDigest 不匹配")
-    elif selector.get("kind") == "members":
-        members = selector.get("members") or []
-        if not members:
-            errors.append(f"binding {binding.get('id')} 的聚合成员为空")
-        for member in members:
-            errors.extend(validate_binding({
-                "id": f"{binding.get('id')}:member",
-                "sourceId": member.get("sourceId"),
-                "selector": member.get("selector"),
-                "resolution": "resolved",
-            }, sources))
-    return errors
-
-
-def selected_value(binding: dict[str, Any], source: dict[str, Any]) -> tuple[bool, Any]:
-    selector = binding.get("selector") or {}
-    kind = selector.get("kind")
-    raw = source.get("rawRecord")
-    if kind == "json-pointer":
-        return pointer_lookup(raw, str(selector.get("path") or ""))
-    if kind == "whole-record":
-        return True, raw
-    if kind == "text-span":
-        found, text = pointer_lookup(raw, str(selector.get("path") or ""))
-        if not found or not isinstance(text, str):
-            return False, None
-        start = int(selector.get("startCodePoint") or 0)
-        end = int(selector.get("endCodePoint") or 0)
-        return True, "".join(list(text)[start:end])
-    return False, None
-
-
-def equivalent(left: Any, right: Any) -> bool:
-    return json.dumps(left, ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
-        right, ensure_ascii=False, sort_keys=True, default=str
-    )
-
-
-def validate_business_value(
-    binding: dict[str, Any], source: dict[str, Any], business_value: Any
-) -> list[str]:
-    """Direct bindings must select the exact value shown in column one."""
-    if (binding.get("transform") or {}).get("kind") != "direct":
-        return []
-    found, selected = selected_value(binding, source)
-    if found and not equivalent(selected, business_value):
-        return [
-            f"binding {binding.get('id')} 声明直接展示,但 selector 值与业务项不一致"
-        ]
-    return []
-
-
-def validate_empty_sources(sources: dict[str, Any]) -> list[str]:
-    errors: list[str] = []
-    for source_id, source in sources.items():
-        if source.get("resultState") != "empty":
-            continue
-        kind = source.get("kind")
-        locator = source.get("locator") or {}
-        raw = source.get("rawRecord")
-        if kind == "database" and not (
-            raw == [] and int(locator.get("resultCount") or 0) == 0
-        ):
-            errors.append(f"空结果来源 {source_id} 无法证明数据库查询返回 0 条")
-        if kind == "runtime-event" and not isinstance(raw, dict):
-            errors.append(f"空结果来源 {source_id} 没有保存运行 Event")
-    return errors
-
-
-def validate_view(payload: dict[str, Any]) -> tuple[Counter, list[str]]:
-    counts: Counter = Counter()
-    errors: list[str] = []
-    if payload.get("schemaVersion") != "inspector-source-v2":
-        return counts, ["schemaVersion 不是 inspector-source-v2"]
-    business = payload.get("businessProjection") or {}
-    sources = payload.get("sources") or {}
-    for source in sources.values():
-        counts[f"source:{source.get('resultState') or 'present'}"] += 1
-    actual_selectors: list[str] = []
-    for module in payload.get("modules") or []:
-        bindings = {item.get("id"): item for item in module.get("bindings") or []}
-        for row in module.get("rows") or []:
-            counts["rows"] += 1
-            business_selector = str(row.get("businessSelector") or "")
-            actual_selectors.append(business_selector)
-            found, business_value = pointer_lookup(business, business_selector)
-            if not found:
-                errors.append(f"业务行 {row.get('id')} 无法定位 {row.get('businessSelector')}")
-            ids = row.get("bindingIds") or []
-            if not ids:
-                errors.append(f"业务行 {row.get('id')} 没有 binding")
-            for binding_id in ids:
-                binding = bindings.get(binding_id)
-                if binding is None:
-                    errors.append(f"业务行 {row.get('id')} 引用了不存在的 binding {binding_id}")
-                    continue
-                counts[str(binding.get("resolution") or "unknown")] += 1
-                errors.extend(validate_binding(binding, sources))
-                source = sources.get(str(binding.get("sourceId") or "")) or {}
-                errors.extend(validate_business_value(binding, source, business_value))
-    errors.extend(validate_log_sources(sources))
-    errors.extend(validate_empty_sources(sources))
-    expected_selectors = visible_business_selectors(business)
-    missing_selectors = [value for value in expected_selectors if value not in actual_selectors]
-    if missing_selectors:
-        errors.append(f"旧业务 Inspector 的可见项没有生成对照行: {missing_selectors}")
-    return counts, errors
-
-
-def validate_log_sources(sources: dict[str, Any]) -> list[str]:
-    errors: list[str] = []
-    for source_id, source in sources.items():
-        if source.get("kind") != "log-anchor":
-            continue
-        locator = source.get("locator") or {}
-        event_id = locator.get("eventId")
-        if event_id is None:
-            errors.append(f"日志来源 {source_id} 没有 eventId")
-            continue
-        event = sources.get(f"event:{event_id}") or {}
-        event_raw = event.get("rawRecord") or {}
-        expected_msg_id = str(event_raw.get("msg_id") or locator.get("msgId") or "")
-        if not expected_msg_id:
-            errors.append(f"Event {event_id} 没有 msg_id,不应附带日志来源")
-            continue
-        raw_log = source.get("rawRecord")
-        rendered = raw_log if isinstance(raw_log, str) else json.dumps(raw_log, ensure_ascii=False)
-        if f"msg_id={expected_msg_id}" not in rendered and f"msg_id:{expected_msg_id}" not in rendered:
-            errors.append(f"日志来源 {source_id} 与 Event {event_id} 的 msg_id 不一致")
-    return errors
-
-
-def visible_business_selectors(projection: dict[str, Any]) -> list[str]:
-    """Mirror the readable Inspector's visible leaves, including fixed empty notices."""
-    selectors: list[str] = []
-    if projection.get("question") not in (None, ""):
-        selectors.append("/question")
-    for section_index, section in enumerate(projection.get("businessSections") or []):
-        if not isinstance(section, dict):
-            continue
-        root = f"/businessSections/{section_index}"
-        if isinstance(section.get("items"), list):
-            selectors.extend(f"{root}/items/{index}" for index, _ in enumerate(section["items"]))
-        elif section.get("content") is not None:
-            selectors.append(f"{root}/content")
-    for block_index, block in enumerate(projection.get("blocks") or []):
-        if not isinstance(block, dict):
-            continue
-        root = f"/blocks/{block_index}"
-        kind = str(block.get("type") or "summary")
-        if kind in {"summary", "notice"}:
-            selectors.append(f"{root}/value")
-        elif kind == "items":
-            selectors.extend(f"{root}/items/{index}" for index, _ in enumerate(block.get("items") or []))
-        elif kind == "implementation-plan":
-            if block.get("mode") is not None:
-                selectors.append(f"{root}/mode")
-            for route_index, route in enumerate(block.get("routes") or []):
-                if not isinstance(route, dict):
-                    continue
-                for key in ("target", "method", "action", "emphasis", "pathType"):
-                    if route.get(key) is not None:
-                        selectors.append(f"{root}/routes/{route_index}/{key}")
-            if block.get("reasoning") is not None:
-                selectors.append(f"{root}/reasoning")
-        elif kind == "creative-process":
-            for step_index, step in enumerate(block.get("steps") or []):
-                if not isinstance(step, dict):
-                    continue
-                for key in ("action", "summary", "plan", "reasoning"):
-                    if step.get(key) is not None:
-                        selectors.append(f"{root}/steps/{step_index}/{key}")
-        elif kind == "evaluation-branches":
-            for branch_index, branch in enumerate(block.get("branches") or []):
-                if not isinstance(branch, dict):
-                    continue
-                for key in ("conclusion", "achievements", "problems", "evidence"):
-                    if branch.get(key) not in (None, [], ""):
-                        selectors.append(f"{root}/branches/{branch_index}/{key}")
-        elif kind in {"evaluation-matrix", "comparison"}:
-            selectors.extend(f"{root}/rows/{index}" for index, _ in enumerate(block.get("rows") or []))
-        elif kind == "artifact":
-            selectors.extend(f"{root}/refs/{index}" for index, _ in enumerate(block.get("refs") or []))
-        else:
-            selectors.append(root)
-    if projection.get("detailKind") == "retrieval-event":
-        conditions = projection.get("queryConditions") or []
-        selectors.extend(
-            [f"/queryConditions/{index}/value" for index, _ in enumerate(conditions)]
-            or ["/queryConditions"]
-        )
-        outcome = projection.get("outcome") or {}
-        selectors.extend(
-            f"/outcome/{key}"
-            for key in ("message", "count", "errorKind")
-            if outcome.get(key) is not None
-        )
-        selectors.extend(f"/items/{index}" for index, _ in enumerate(projection.get("items") or []))
-        completeness = projection.get("completeness") or {}
-        if completeness.get("bodyAvailable") is False:
-            selectors.append("/completeness/bodyAvailable")
-        if completeness.get("truncated"):
-            selectors.append(
-                "/completeness/omittedCharacters"
-                if completeness.get("omittedCharacters") is not None
-                else "/completeness/truncated"
-            )
-    if isinstance(projection.get("activities"), list):
-        activities = projection.get("activities") or []
-        selectors.extend(
-            [f"/activities/{index}" for index, _ in enumerate(activities)]
-            or ["/missingActivityNotice" if projection.get("missingActivityNotice") else "/activities"]
-        )
-    return selectors
-
-
-def validate_detail(base_url: str, run_id: int, detail_ref: str) -> tuple[str, Counter, list[str], bool]:
-    encoded = quote_ref(detail_ref)
-    status, payload = request_json(
-        base_url, f"/api/script-builds/{run_id}/inspector-view/{encoded}"
-    )
-    if status == 404:
-        return detail_ref, Counter(), [], False
-    if status != 200:
-        return detail_ref, Counter(), [f"inspector-view HTTP {status}: {payload}"], True
-    counts, errors = validate_view(payload)
-    old_status, old = request_json(
-        base_url, f"/api/script-builds/{run_id}/activities/{encoded}"
-    )
-    if old_status == 200:
-        expected = {key: value for key, value in old.items() if key not in HIDDEN_BUSINESS_KEYS}
-        if payload.get("businessProjection") != expected:
-            errors.append("第一列与旧业务 Inspector 投影不一致")
-    return detail_ref, counts, errors, True
-
-
-def validate_run(base_url: str, run_id: int, *, workers: int) -> dict[str, Any]:
-    status, execution = request_json(base_url, f"/api/script-builds/{run_id}/execution-view")
-    if status != 200:
-        return {"runId": run_id, "fatal": f"execution-view HTTP {status}: {execution}"}
-    refs = collect_detail_refs(execution)
-    totals: Counter = Counter()
-    failures: list[dict[str, Any]] = []
-    checked = 0
-    with ThreadPoolExecutor(max_workers=max(1, workers)) as executor:
-        futures = {
-            executor.submit(validate_detail, base_url, run_id, detail_ref): detail_ref
-            for detail_ref in refs
-        }
-        for future in as_completed(futures):
-            detail_ref, counts, errors, did_check = future.result()
-            checked += int(did_check)
-            totals.update(counts)
-            if errors:
-                failures.append({"detailRef": detail_ref, "errors": errors})
-    failures.sort(key=lambda item: refs.index(item["detailRef"]))
-    return {
-        "runId": run_id,
-        "visibleRefs": len(refs),
-        "checkedInspectors": checked,
-        "counts": dict(totals),
-        "failures": failures,
-    }
-
-
-def main() -> int:
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--base-url", default="http://127.0.0.1:8788")
-    parser.add_argument("--runs", type=int, nargs="+", default=[422, 443, 444, 446])
-    parser.add_argument("--output")
-    parser.add_argument("--workers", type=int, default=8)
-    args = parser.parse_args()
-    results = [validate_run(args.base_url, run_id, workers=args.workers) for run_id in args.runs]
-    report = {"baseUrl": args.base_url, "runs": results}
-    rendered = json.dumps(report, ensure_ascii=False, indent=2)
-    print(rendered)
-    if args.output:
-        with open(args.output, "w", encoding="utf-8") as handle:
-            handle.write(rendered + "\n")
-    return 1 if any(result.get("fatal") or result.get("failures") for result in results) else 0
-
-
-if __name__ == "__main__":
-    sys.exit(main())

+ 0 - 259
visualization/backend/tests/conftest.py

@@ -1,259 +0,0 @@
-from __future__ import annotations
-
-from copy import deepcopy
-
-import pytest
-
-
-@pytest.fixture
-def current_bundle():
-    bundle = {
-        "record": {
-            "id": 88001,
-            "status": "running",
-            "script_direction": "解释一家工厂怎样通过柔性排产减少浪费。",
-            "summary": None,
-            "error_message": None,
-            "agent_config": {"model_name": "test-model"},
-            "cost_usd": 1.2,
-            "start_time": "2026-07-13T10:00:00",
-            "end_time": None,
-        },
-        "rounds": [
-            {
-                "id": 1,
-                "round_index": 1,
-                "goal": "确定文章结构并补齐可靠行业事实",
-                "multipath_plan": [
-                    {"path_index": 1, "target": "文章结构"},
-                    {"path_index": 2, "target": "行业事实"},
-                    {"path_index": 3, "target": "结尾表达"},
-                ],
-                "race_or_divide": "分工",
-                "plan_note": "内容方案和领域信息方案并行推进。",
-                "status": "done",
-            },
-            {
-                "id": 2,
-                "round_index": 2,
-                "goal": "补齐正文细节",
-                "multipath_plan": [{"path_index": 1, "target": "正文"}],
-                "race_or_divide": "赛马",
-                "plan_note": None,
-                "status": "open",
-            },
-        ],
-        "branches": [
-            _branch(1, 1, "内容", "形成三段式文章结构", "merged"),
-            _branch(1, 2, "领域信息", "核实柔性排产数据", "discarded"),
-            _branch(1, 3, "内容", "形成行动建议结尾", "parked"),
-            _branch(2, 4, "内容", "补齐正文案例", "open"),
-        ],
-        "dataDecisions": [
-            _data_decision(11, 1, 1, "采用案例中的三段结构", "结构能解释因果关系"),
-            _data_decision(12, 1, 2, "采用两条交叉印证的数据", "两类来源结论一致"),
-            _data_decision(13, 1, 3, "采用行动建议写法", None),
-        ],
-        "multipathDecisions": [
-            {
-                "id": 21,
-                "script_build_id": 88001,
-                "round_index": 1,
-                "branch_ids": [1, 2],
-                "decision": "采用方案 1;方案 2 的领域事实保留,但内容分支不采用。",
-                "reasoning": "结构可直接进入主脚本,领域事实独立沉淀。",
-                "created_at": "2026-07-13T10:03:00",
-            },
-            {
-                "id": 22,
-                "script_build_id": 88001,
-                "round_index": 1,
-                "branch_ids": [3],
-                "decision": "方案 3 暂存。",
-                "reasoning": "方向可用,但与正文衔接仍需调整。",
-                "created_at": "2026-07-13T10:04:00",
-            },
-        ],
-        "domainInfo": [
-            {
-                "id": 31,
-                "script_build_id": 88001,
-                "round_index": 1,
-                "branch_id": 2,
-                "content": "柔性排产可降低小批量切换损耗。",
-                "source": [{"type": "report", "id": "R-1", "loc": "p.4"}],
-                "note": "两份报告交叉印证",
-                "created_at": "2026-07-13T10:02:30",
-            }
-        ],
-        "events": [
-            {
-                "id": 39,
-                "event_seq": 6,
-                "event_type": "agent_invoke",
-                "event_name": "script_multipath_evaluator",
-                "round_index": 1,
-                "status": "ok",
-                "inputData": {"task": "比较 branch_id=1, branch_id=2"},
-                "agentOutputData": {"summary": "### 分支评估 branch_id=1\n**该支结论**:通过\n**目标推进**:结构清晰。\n\n### 分支评估 branch_id=2\n**该支结论**:部分通过\n**目标推进**:事实可靠。\n\n### 对比与建议\n**采纳建议**:优先采用 branch_id=1。"},
-                "started_at": "2026-07-13T10:02:40",
-                "ended_at": "2026-07-13T10:02:50",
-            },
-            {
-                "id": 40,
-                "event_seq": 7,
-                "event_type": "agent_invoke",
-                "event_name": "script_multipath_evaluator",
-                "round_index": 1,
-                "status": "ok",
-                "inputData": {"task": "评审 branch_id=3"},
-                "agentOutputData": {"summary": "### 分支评估 branch_id=3\n**该支结论**:通过\n**目标推进**:结尾方向可用。\n\n### 对比与建议\n**采纳建议**:可以暂存后调整。"},
-                "started_at": "2026-07-13T10:03:30",
-                "ended_at": "2026-07-13T10:03:40",
-            },
-            {
-                "id": 41,
-                "event_seq": 8,
-                "event_type": "agent_invoke",
-                "event_name": "script_evaluator",
-                "round_index": 1,
-                "status": "ok",
-                "title": "派发 script_evaluator (ok)",
-                "input_preview": "请评估第1轮",
-                "output_preview": "### 整体结论:部分通过\n仍需完善正文细节。",
-                "agentOutputData": {"summary": "### 整体结论:部分通过\n仍需完善正文细节。"},
-                "started_at": "2026-07-13T10:05:00",
-                "ended_at": "2026-07-13T10:05:10",
-            },
-            {
-                "id": 42,
-                "event_seq": 9,
-                "event_type": "agent_invoke",
-                "event_name": "retrieve_data_knowledge",
-                "status": "ok",
-                "title": "无法归属的取数 Agent",
-                "started_at": "2026-07-13T10:06:00",
-                "ended_at": "2026-07-13T10:06:10",
-            },
-            {
-                "id": 50,
-                "event_seq": 10,
-                "event_type": "agent_invoke",
-                "event_name": "script_implementer",
-                "agent_role": "script_implementer",
-                "round_index": 1,
-                "branch_id": 1,
-                "status": "ok",
-                "started_at": "2026-07-13T10:00:00",
-                "ended_at": "2026-07-13T10:02:00",
-            },
-            {
-                "id": 51,
-                "event_seq": 11,
-                "event_type": "tool_call",
-                "event_name": "get_script_snapshot",
-                "agent_role": "script_implementer",
-                "scope_event_id": 50,
-                "round_index": 1,
-                "branch_id": 1,
-                "status": "ok",
-                "inputData": {},
-                "output_preview": "{\"count\": 1}",
-                "started_at": "2026-07-13T10:00:05",
-                "ended_at": "2026-07-13T10:00:06",
-            },
-            {
-                "id": 52,
-                "event_seq": 12,
-                "event_type": "agent_invoke",
-                "event_name": "retrieve_data_decode_case",
-                "parent_event_id": 50,
-                "scope_event_id": 50,
-                "round_index": 1,
-                "branch_id": 1,
-                "status": "ok",
-                "inputData": {"task": "查找可说明因果结构的解构 Case"},
-                "agentOutputData": {"summary": "初筛出两个可用的三段式案例。"},
-                "started_at": "2026-07-13T10:00:10",
-                "ended_at": "2026-07-13T10:00:40",
-            },
-            {
-                "id": 53,
-                "event_seq": 13,
-                "event_type": "tool_call",
-                "event_name": "search_script_decode_case",
-                "scope_event_id": 52,
-                "parent_event_id": 52,
-                "round_index": 1,
-                "branch_id": 1,
-                "status": "ok",
-                "inputData": {"keyword": "因果结构"},
-                "output_preview": "{\"count\": 2}",
-                "started_at": "2026-07-13T10:00:15",
-                "ended_at": "2026-07-13T10:00:20",
-            },
-            {
-                "id": 54,
-                "event_seq": 14,
-                "event_type": "tool_call",
-                "event_name": "search_script_decode_case",
-                "scope_event_id": 52,
-                "parent_event_id": 52,
-                "round_index": 1,
-                "branch_id": 1,
-                "status": "ok",
-                "inputData": {"keyword": "柔性排产"},
-                "output_preview": "{\"count\": 0}",
-                "started_at": "2026-07-13T10:00:25",
-                "ended_at": "2026-07-13T10:00:30",
-            },
-        ],
-        "ignoredLegacyDataDecisionCount": 2,
-        "currentArtifact": {
-            "paragraphs": [{"id": 1}, {"id": 2}, {"id": 3}],
-            "elements": [{"id": 10}, {"id": 11}],
-            "paragraphElements": [{"id": 100, "paragraph_id": 1, "element_id": 10}],
-            "sourceOrigin": "database",
-        },
-    }
-    return deepcopy(bundle)
-
-
-def _branch(round_index: int, branch_id: int, path_type: str, task: str, status: str):
-    snapshot = {
-        "paragraphs": [{"id": branch_id * 100, "name": task}],
-        "elements": [{"id": branch_id * 1000}],
-        "paragraphElements": [],
-    }
-    return {
-        "id": branch_id,
-        "script_build_id": 88001,
-        "branch_id": branch_id,
-        "round_index": round_index,
-        "path_type": path_type,
-        "target": task,
-        "impl_task": task,
-        "self_assessment": f"已经完成:{task}",
-        "status": status,
-        "decision_reasoning": f"方案 {branch_id} 的当前处理理由",
-        "candidate_snapshot": {
-            "snapshot": snapshot if path_type == "内容" else None,
-            "snapshotKind": "saved-at-disposition" if status in {"merged", "discarded"} else "current-overlay",
-            "historicalAccuracy": "exact" if status in {"merged", "discarded"} else "unknown",
-            "currentProjectionAccuracy": "exact" if status in {"open", "parked"} else "not-applicable",
-            "note": "测试候选版本",
-        },
-    }
-
-
-def _data_decision(record_id: int, round_index: int, branch_id: int, decision: str, reasoning: str | None):
-    return {
-        "id": record_id,
-        "script_build_id": 88001,
-        "round_index": round_index,
-        "branch_id": branch_id,
-        "sources": [{"data_type": "领域信息", "data_id": f"S-{record_id}", "data_content": "已核实材料"}],
-        "decision": decision,
-        "reasoning": reasoning,
-        "created_at": "2026-07-13T10:02:00",
-    }

+ 186 - 136
visualization/backend/tests/test_api.py

@@ -1,154 +1,204 @@
-from copy import deepcopy
-
 from fastapi.testclient import TestClient
 
-from app import main
-
-
-client = TestClient(main.app)
+from app.contracts import SCHEMA_CATALOG
+from app.fake_data import (
+    ARTIFACT_DIGESTS,
+    GRAPH,
+    RUN_ID,
+    canonical_digest,
+    canonical_wire_digest,
+)
+from app.main import app
 
+client = TestClient(app)
 
-def test_v8_execution_round_activity_and_artifact_endpoints(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_current_artifact", lambda _build_id: deepcopy(bundle["currentArtifact"]))
-    monkeypatch.setattr(main.provider, "list_builds", lambda **_kwargs: [{"id": "88001", "status": "running"}])
 
-    execution = client.get("/api/script-builds/88001/execution-view")
-    assert execution.status_code == 200
-    assert execution.json()["schemaVersion"] == "8"
-
-    round_detail = client.get("/api/script-builds/88001/rounds/1")
-    assert round_detail.status_code == 200
-    assert round_detail.json()["businessSections"]
-
-    data_detail = client.get("/api/script-builds/88001/activities/data-decision:11")
-    assert data_detail.status_code == 200
-    assert data_detail.json()["detailKind"] == "agent-decision"
-    assert data_detail.json()["decisionType"] == "tradeoff"
-    assert "明确取舍" in [block["title"] for block in data_detail.json()["blocks"]]
+def test_health_declares_fake_mode() -> None:
+    response = client.get("/api/health")
+    assert response.status_code == 200
+    assert response.json()["data_mode"] == "fake"
 
-    artifact = client.get("/api/script-builds/88001/artifacts/base:current")
-    assert artifact.status_code == 200
-    assert artifact.json()["changes"]["exactness"] == "current-only"
-    assert artifact.json()["scriptTable"]["counts"] == {"paragraphs": 3, "elements": 2, "links": 1}
 
-    round_artifact = client.get("/api/script-builds/88001/artifacts/artifact:round:1")
-    assert round_artifact.status_code == 200
-    assert round_artifact.json()["artifactContext"] == {
-        "type": "round",
-        "roundIndex": 1,
-        "versionKind": "current-base",
-        "note": "当前数据库未保存第 1 轮结束时的独立快照;这里展示当前保存的主脚本。",
-        "exactness": "current-only",
+def test_graph_is_fine_grained_and_connected() -> None:
+    response = client.get(f"/api/runs/{RUN_ID}/graph")
+    assert response.status_code == 200
+    body = response.json()
+    assert body["data_mode"] == "fake"
+    assert len(body["nodes"]) >= 55
+    assert len(body["edges"]) >= 50
+    assert {item["phase"] for item in body["nodes"]} == {
+        "phase-1",
+        "phase-2",
+        "phase-3",
+        "delivery",
     }
-
-    candidate = client.get("/api/script-builds/88001/artifacts/artifact:branch:1")
-    assert candidate.status_code == 200
-    assert candidate.json()["artifactContext"] == {
-        "type": "candidate",
-        "roundIndex": 1,
-        "branchId": 1,
-        "branchStatus": "merged",
-        "pathType": "内容",
-        "versionKind": "saved-at-disposition",
-        "historicalAccuracy": "exact",
-        "currentProjectionAccuracy": "not-applicable",
-        "note": "测试候选版本",
-        "exactness": "historical-snapshot",
+    assert {item["node_type"] for item in body["nodes"]} >= {
+        "contract",
+        "operation",
+        "attempt",
+        "artifact",
+        "validation",
+        "decision",
+        "publication",
+        "transaction",
+        "readback",
     }
 
-    domain = client.get("/api/script-builds/88001/artifacts/artifact:branch:2")
-    assert domain.status_code == 404
-    assert "领域信息方案" in domain.json()["detail"]
-
 
-def test_terminal_branch_without_snapshot_does_not_fake_a_candidate(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    branch = bundle["branches"][0]
-    branch["candidate_snapshot"] = {
-        "snapshot": None,
-        "snapshotKind": "missing-disposition-snapshot",
-        "historicalAccuracy": "missing",
-        "currentProjectionAccuracy": "not-applicable",
+def test_every_fake_record_uses_exact_contract_fields() -> None:
+    used_models: set[str] = set()
+    for node in GRAPH.nodes:
+        model = node.record.model_name
+        used_models.add(model)
+        assert set(node.record.payload) == set(SCHEMA_CATALOG[model]), node.id
+    required = {
+        "ScriptBuildRecord",
+        "ScriptBuildInputSnapshotV1",
+        "MissionBinding",
+        "MissionOwnerToken",
+        "ScriptTaskContractV1",
+        "OperationView",
+        "AttemptView",
+        "ValidationView",
+        "PlannerDecisionView",
+        "EvidenceRecordV1",
+        "ScriptDirectionArtifactV1",
+        "StructureArtifactV1",
+        "ParagraphArtifactV1",
+        "ElementSetArtifactV1",
+        "ComparisonArtifactV1",
+        "StructuredScriptArtifactV1",
+        "CandidatePortfolioArtifactV1",
+        "RootDeliveryManifestV1",
+        "LegacyProjectionCanonicalV1",
+        "Publication",
+        "PublicationResult",
+        "HttpCommandRecord",
+        "InputSnapshotRow",
+        "ArtifactVersionRow",
+        "ParagraphRow",
+        "ElementRow",
+        "ParagraphElementLinkRow",
     }
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-
-    response = client.get("/api/script-builds/88001/artifacts/artifact:branch:1")
-    assert response.status_code == 404
-    assert "没有可用的候选脚本快照" in response.json()["detail"]
-
-
-def test_event_body_is_loaded_only_from_activity_endpoint(monkeypatch, current_bundle):
-    full_report = "评审原文" * 8_000
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(current_bundle))
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: {
-        "id": 41,
-        "event_name": "script_evaluator",
-        "title": "整体评审",
-        "input": {"content": "评审第1轮"},
-        "output": {"content": f"整体结论:部分通过\n{full_report}"},
-    })
-
-    execution = client.get("/api/script-builds/88001/execution-view").json()
-    assert "评审第1轮" not in str(execution)
-
-    detail = client.get("/api/script-builds/88001/activities/event:41")
-    assert detail.status_code == 200
-    payload = detail.json()
-    assert payload["detailKind"] == "agent-decision"
-    assert payload["decisionType"] == "evaluation"
-    assert any(
-        block.get("title") == "总结论" and block.get("value") == "部分通过"
-        for block in payload["blocks"]
+    assert required <= used_models
+
+
+def test_manifest_and_legacy_projection_use_real_logical_closure() -> None:
+    records = {node.record.model_name: node.record.payload for node in GRAPH.nodes}
+    canonical = records["LegacyProjectionCanonicalV1"]
+    manifest = records["RootDeliveryManifestV1"]
+    assert manifest["legacy_projection_digest"] == canonical_wire_digest(canonical)
+    assert manifest["input_closure_digest"] != manifest["legacy_projection_digest"]
+    for paragraph in canonical["paragraphs"]:
+        assert "logical_key" in paragraph
+        assert "parent_logical_key" in paragraph
+        assert not {"paragraph_id", "parent_id", "is_active"} & set(paragraph)
+    for element in canonical["elements"]:
+        assert "logical_key" in element
+        assert not {"element_id", "is_active"} & set(element)
+    for link in canonical["paragraph_element_links"]:
+        assert set(link) == {"paragraph_logical_key", "element_logical_key"}
+
+
+def test_artifact_refs_pin_specific_kind_and_canonical_digest() -> None:
+    attempts = [
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "AttemptView"
+    ]
+    for attempt in attempts:
+        for ref in attempt["submission"]["artifact_refs"]:
+            version = int(ref["version"])
+            assert ref["kind"] != "business-artifact"
+            assert ref["digest"] == ARTIFACT_DIGESTS[version]
+    node_ids = {node.id for node in GRAPH.nodes}
+    assert "compose:alternate:artifact" in node_ids
+    assert "compose:alternate:decision" in node_ids
+
+
+def test_root_task_has_no_self_parent_and_declares_children() -> None:
+    root = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "TaskView"
+        and node.record.payload["task_id"] == "task-root"
     )
-    assert "script_evaluator" not in str(payload["blocks"])
-    assert full_report in payload["technical"]["output"]["content"]
-    assert "[truncated" not in payload["technical"]["output"]["content"]
-
-
-def test_removed_fixture_and_raw_log_routes_are_not_exposed():
-    assert client.get("/api/fixtures/current-schema/execution-view").status_code == 404
-    assert client.get("/api/script-builds/1/raw-log").status_code == 404
+    assert root["parent_task_id"] is None
+    assert root["child_task_ids"] == ["direction", "portfolio"]
+    contract = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.id == "root-delivery:contract"
+    )
+    assert contract["scope_ref"] == "script-build://scope/root"
+    assert contract["write_scope"] == []
+    assert [item["expected_task_kind"] for item in contract["input_decision_refs"]] == [
+        "direction",
+        "candidate-portfolio",
+    ]
+    attempt = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.id == "root-delivery:attempt"
+    )
+    assert attempt["accepted_child_decision_ids"] == [
+        "decision-direction",
+        "decision-portfolio",
+    ]
 
 
-def test_capabilities_only_advertise_new_db_contract():
-    response = client.get("/api/capabilities")
-    assert response.status_code == 200
-    payload = response.json()
-    assert payload["schemaVersion"] == "8"
-    assert payload["readOnly"] is True
-    assert "script_build_multipath_decision" in payload["businessSources"]
-    assert payload["runtimeSupplement"] == ["script_build_event", "script_build_event_body"]
-
-
-def test_prompt_endpoint_marks_current_rules_as_not_a_run_snapshot(monkeypatch):
-    monkeypatch.setattr(
-        main.provider,
-        "load_prompt_context",
-        lambda _build_id, _prompt_ref: {
-            "promptRef": "prompt:event:50",
-            "actor": {"role": "implementer", "label": "实现 Agent"},
-            "task": "补齐烹饪步骤",
-            "promptContent": "当前规则\nAuthorization: Bearer secret-token",
-            "promptSource": "current-db",
-            "promptVersion": 8,
-        },
+def test_input_snapshot_row_and_logical_view_share_one_canonical_payload() -> None:
+    row = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "InputSnapshotRow"
     )
-
-    response = client.get(
-        "/api/script-builds/88001/decision-prompts/prompt:event:50"
+    logical = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "ScriptBuildInputSnapshotV1"
     )
-    assert response.status_code == 200
-    payload = response.json()
-    assert payload["exactTask"] == {
-        "content": "补齐烹饪步骤",
-        "accuracy": "run-exact",
-    }
-    assert payload["systemPrompt"]["source"] == "current-db"
-    assert payload["systemPrompt"]["accuracy"] == "current-not-run-snapshot"
-    assert "secret-token" not in str(payload)
-    assert any(
-        item["code"] == "PROMPT_NOT_SNAPSHOTTED"
-        for item in payload["notices"]
+    assert logical["snapshot_id"] == str(row["id"]) == "301"
+    assert row["canonical_sha256"] == canonical_digest(row["canonical_json"])
+    assert logical["canonical_sha256"] == f"sha256:{row['canonical_sha256']}"
+    for key in (
+        "script_build_id",
+        "execution_id",
+        "topic_build_id",
+        "topic_id",
+        "topic",
+        "account",
+        "persona_points",
+        "section_patterns",
+        "strategies",
+        "prompt_manifest",
+        "datasource_manifest",
+        "model_manifest",
+    ):
+        assert logical[key] == row["canonical_json"][key]
+
+
+def test_final_physical_row_counts_match_canonical_counts() -> None:
+    canonical = next(
+        node.record.payload
+        for node in GRAPH.nodes
+        if node.record.model_name == "LegacyProjectionCanonicalV1"
     )
+    counts = {
+        model: sum(1 for node in GRAPH.nodes if node.record.model_name == model)
+        for model in ("ParagraphRow", "ElementRow", "ParagraphElementLinkRow")
+    }
+    assert counts == {
+        "ParagraphRow": canonical["paragraph_count"],
+        "ElementRow": canonical["element_count"],
+        "ParagraphElementLinkRow": canonical["link_count"],
+    }
+
+
+def test_node_detail_and_not_found() -> None:
+    node_id = GRAPH.nodes[10].id
+    response = client.get(f"/api/runs/{RUN_ID}/nodes/{node_id}")
+    assert response.status_code == 200
+    assert response.json()["node"]["id"] == node_id
+    assert client.get(f"/api/runs/999/nodes/{node_id}").status_code == 404
+    assert client.get(f"/api/runs/{RUN_ID}/nodes/missing").status_code == 404

+ 0 - 176
visualization/backend/tests/test_business_detail_text.py

@@ -1,176 +0,0 @@
-from app.business_detail_text import (
-    assessment_text,
-    branch_task_sections,
-    branch_task_text,
-    business_text,
-    clean_report,
-    event_business_sections,
-)
-
-
-def test_business_items_translate_branch_and_base_decision_language():
-    assert business_text("选 Branch 1 合入 Base,弃 Branch 2。") == (
-        "采用方案 1 并合入主脚本,方案 2 未采用。"
-    )
-    assert business_text("组 Branch 3 与 Branch 4 合入。") == (
-        "组合方案 3 与方案 4,并合入主脚本。"
-    )
-
-
-def test_evaluator_preview_becomes_business_markdown_not_json():
-    event = {
-        "event_type": "subagent_dispatch",
-        "event_name": "script_multipath_evaluator",
-        "title": "派发 script_multipath_evaluator (ok)",
-        "status": "ok",
-        "input_preview": '{"agent_type":"script_multipath_evaluator","task":"本轮为赛马:branch 1 与 branch 2。\\n\\n当前目标:选出更清晰的骨架。"}',
-        "output_preview": '{"status":"completed","summary":"## 委托任务完成\\n\\n## 评估报告 script_build_id=99\\n\\n#### 1. 分支评估 branch_id=1\\n* **分支元信息**:round_index=1,action=增\\n* **该支结论**:**通过**\\n* **目标推进**:结构清晰。"}',
-    }
-
-    sections = event_business_sections(event)
-    serialized = str(sections)
-
-    assert sections[0]["title"] == "评审了哪些方案"
-    assert sections[0]["content"] == "方案 1"
-    assert sections[1]["title"] == "各方案结论"
-    assert sections[1]["content"] == "方案 1:通过"
-    assert "script_multipath_evaluator" not in serialized
-    assert "script_build_id" not in serialized
-    assert "分支元信息" not in serialized
-    assert all("\\n" not in section["content"] for section in sections)
-    assert not any(section["content"].lstrip().startswith("{") for section in sections)
-
-
-def test_branch_task_and_assessment_remove_technical_headers_and_telemetry():
-    task = """路序号:1
-action:增
-target:整篇骨架
-path_type:内容
-
-目标:完成五段结构
-
-取证方向与约束:
-- scope=创业邦
-
-禁止事项:不得写成已上市
-"""
-    assessment = """## 委托任务完成
-
-思维过程:这是内部执行过程。
-
-### 完成度申报
-- **完成**:形成五段骨架。
-- **未尽**:无。
-
-**执行统计**:
-- Tokens: 739446
-- 成本: $0.24
-"""
-
-    readable_task = branch_task_text(task)
-    readable_assessment = assessment_text(assessment)
-
-    assert "路序号" not in readable_task
-    assert "action" not in readable_task
-    assert "path_type" not in readable_task
-    assert "实现范围" in readable_task
-    assert "账号:创业邦" in readable_task
-    assert "思维过程" not in readable_assessment
-    assert "Tokens" not in readable_assessment
-    assert "成本" not in readable_assessment
-    assert "形成五段骨架" in readable_assessment
-
-
-def test_branch_task_sections_are_optional_and_preserve_recognized_content():
-    task = """路序号:1
-action:改
-target:五段骨架
-path_type:内容
-method:产生脉络
-
-目标:补齐每段的完整文字。
-
-必用素材(已核实):
-- 领域事实 A
-
-取证方向与约束:
-- 保持概念准确
-
-禁止事项:不得改动原文"""
-
-    sections = branch_task_sections(task)
-
-    assert [item["kind"] for item in sections] == [
-        "scope", "method", "goal", "materials", "constraints", "avoid"
-    ]
-    assert sections[0]["content"] == "五段骨架"
-    assert sections[3]["title"] == "必用素材(已核实)"
-    assert sections[3]["content"] == "- 领域事实 A"
-    assert sections[-1]["content"] == "不得改动原文"
-
-
-def test_branch_task_sections_keep_free_form_task_as_one_complete_note():
-    task = "这是一个单一数据修复任务。\n\n任务:删除一条悬空边。\n\n执行方式:精确删除。"
-
-    assert branch_task_sections(task) == [
-        {"kind": "notes", "title": "任务说明", "content": task}
-    ]
-
-
-def test_tool_event_business_view_never_prints_raw_json_fields():
-    event = {
-        "event_type": "tool_call",
-        "event_name": "get_script_snapshot",
-        "title": "🔧 get_script_snapshot",
-        "status": "ok",
-        "input_preview": '{"script_build_id":431}',
-        "output_preview": '{"script_build":{"id":431},"paragraphs":[{"id":1}],"elements":[{"id":2}]}',
-    }
-
-    sections = event_business_sections(event)
-    serialized = str(sections)
-
-    assert sections[0]["content"] == "读取当前主脚本"
-    assert sections[1]["content"] == "已读取当前主脚本,共 1 个段落、1 个元素。"
-    assert "script_build_id" not in serialized
-    assert "get_script_snapshot" not in serialized
-    assert not any(section["content"].lstrip().startswith("{") for section in sections)
-
-
-def test_retrieval_screening_removes_record_ids_and_runtime_telemetry():
-    report = """### 初筛结果
-- 找到可用的构图案例,post_id=abc123
-- 保留粉白配色的业务结论。
-
-**执行统计**:
-- 消息数: 16
-- Tokens: 739446
-- 成本: $0.24
-"""
-    readable = clean_report(report)
-    assert "可用的构图案例" in readable
-    assert "粉白配色" in readable
-    assert "post_id" not in readable
-    assert "abc123" not in readable
-    assert "Tokens" not in readable
-    assert "成本" not in readable
-
-
-def test_retrieval_screening_keeps_business_summary_but_removes_json_fence():
-    readable = clean_report(
-        """
-已找到 3 条与烹饪步骤相关的候选证据。
-
-```json
-[{"post_id": "abc", "raw": "large payload"}]
-```
-
-初筛后保留防碎处理和火候控制两类信息。
-"""
-    )
-
-    assert "已找到 3 条" in readable
-    assert "保留防碎处理" in readable
-    assert "```" not in readable
-    assert "post_id" not in readable
-    assert "large payload" not in readable

+ 0 - 101
visualization/backend/tests/test_card_data.py

@@ -1,101 +0,0 @@
-from __future__ import annotations
-
-from copy import deepcopy
-
-from fastapi.testclient import TestClient
-
-from app import main
-
-
-client = TestClient(main.app)
-
-
-def _event_detail(bundle, event_id: int):
-    event = next(item for item in bundle["events"] if int(item["id"]) == event_id)
-    input_content = deepcopy(event.get("inputData"))
-    output_content = deepcopy(event.get("agentOutputData"))
-    if event_id == 50:
-        input_content = {
-            "task": "范围:文章结构\n方法:补齐因果链\n目标:形成三段式文章结构\n避免:不要虚构事实"
-        }
-        output_content = {"summary": "实现完成"}
-    if output_content is None and event.get("output_preview") is not None:
-        output_content = event.get("output_preview")
-    return {
-        **deepcopy(event),
-        "input": {"contentType": "json", "content": input_content, "truncated": False}
-        if input_content is not None else None,
-        "output": {"contentType": "json", "content": output_content, "truncated": False}
-        if output_content is not None else None,
-    }
-
-
-def test_card_data_protocol_and_primary_kinds(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, event_id: _event_detail(bundle, event_id))
-
-    objective = client.get("/api/script-builds/88001/card-data/run%3Aobjective")
-    assert objective.status_code == 200
-    assert objective.json()["relationKind"] == "business-record"
-    assert objective.json()["businessOutputs"][0]["source"] == {
-        "kind": "database",
-        "label": "script_build_record",
-        "ref": "run:objective",
-        "fieldPath": "script_direction",
-    }
-
-    task = client.get(
-        "/api/script-builds/88001/card-data/round%3A1%3Abranch%3A1%3Atask"
-    )
-    assert task.status_code == 200
-    assert task.json()["relationKind"] == "single-event"
-    assert "不要虚构事实" in task.json()["businessInputs"][0]["value"]
-    assert task.json()["runtime"]["units"][0]["eventRef"] == "event:50"
-
-    tradeoff = client.get(
-        "/api/script-builds/88001/card-data/data-decision%3A11"
-    )
-    assert tradeoff.status_code == 200
-    assert tradeoff.json()["relationKind"] == "business-record"
-    assert tradeoff.json()["businessInputs"][0]["relation"] == "explicit-basis"
-    assert tradeoff.json()["runtime"]["units"] == []
-
-    final = client.get(
-        "/api/script-builds/88001/card-data/run%3Afinal-result"
-    )
-    assert final.status_code == 200
-    assert final.json()["cardKind"] == "final-result"
-    assert final.json()["relationKind"] == "calculated"
-    assert next(item for item in final.json()["businessOutputs"] if item["id"] == "final:artifact-scale")["value"] == {
-        "paragraphs": 3,
-        "elements": 2,
-        "links": 1,
-    }
-
-
-def test_retrieval_agent_loads_full_parent_and_query_event_bodies(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, event_id: _event_detail(bundle, event_id))
-
-    response = client.get(
-        "/api/script-builds/88001/card-data/retrieval-agent%3A52"
-    )
-    assert response.status_code == 200
-    payload = response.json()
-    assert payload["cardKind"] == "retrieval-agent"
-    assert payload["relationKind"] == "aggregate"
-    assert payload["businessInputs"][0]["value"] == "查找可说明因果结构的解构 Case"
-    assert [item["eventRef"] for item in payload["runtime"]["units"]] == [
-        "event:52",
-        "event:53",
-        "event:54",
-    ]
-    assert payload["businessOutputs"][1]["value"] == "初筛出两个可用的三段式案例。"
-
-
-def test_unknown_card_data_ref_is_404(monkeypatch, current_bundle):
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(current_bundle))
-    response = client.get("/api/script-builds/88001/card-data/not-a-card")
-    assert response.status_code == 404

+ 0 - 265
visualization/backend/tests/test_decision_projection_v8.py

@@ -1,265 +0,0 @@
-from __future__ import annotations
-
-from app.decision_projection import AgentDecisionProjector
-
-
-def test_direction_keeps_observed_reads_separate_from_explicit_basis():
-    decision = AgentDecisionProjector().direction(
-        {
-            "id": "run:objective",
-            "subtype": "objective",
-            "decisionItems": ["讲清柔性排产为什么能减少浪费"],
-            "constraints": ["数据可信", "结构清楚"],
-            "inputs": [
-                {"label": "选题", "relation": "direct-read"},
-                {
-                    "label": "账号段落模式",
-                    "relation": "direct-read",
-                    "decisionUse": "explicit-basis",
-                },
-            ],
-            "promptRef": "main:direction",
-        }
-    )
-
-    assert decision["semanticKind"] == "decision"
-    assert decision["decisionType"] == "direction"
-    assert decision["authority"] == "final"
-    assert decision["inputs"][0] == {
-        "label": "选题",
-        "summary": None,
-        "observedRelation": "read-by-actor",
-        "decisionUse": "not-recorded",
-        "detailRef": None,
-    }
-    assert decision["inputs"][1]["decisionUse"] == "explicit-basis"
-    input_block = next(
-        block for block in decision["detail"]["blocks"] if block["title"] == "根据以下信息做出的决策"
-    )
-    assert input_block["items"][0]["note"] == "决策前直接读取;是否采用未记录"
-    assert input_block["items"][1]["note"] == "决策前直接读取;明确作为决策依据"
-    assert input_block["visualRole"] == "evidence"
-    assert input_block["presentation"] == "list"
-    assert input_block["collapsible"] is False
-    final_block = next(
-        block for block in decision["detail"]["blocks"] if block["title"] == "最终方向"
-    )
-    assert final_block["visualRole"] == "key-result"
-    assert len(decision["card"]["secondary"]) <= 3
-    assert decision["detail"]["detailKind"] == "agent-decision"
-
-
-def test_implementation_plan_uses_dedicated_routes_and_full_evidence():
-    full_goal = "修复结构完整性。" * 40
-    decision = AgentDecisionProjector().direction(
-        {
-            "id": "round:4:plan",
-            "subtype": "implementation-plan",
-            "decisionItems": ["方案 1 · 改:段落 2207、2208"],
-            "explicitReasoning": "这是收敛阶段的定点修复,不需要竞争。",
-            "inputs": [
-                {"label": "本轮目标", "summary": full_goal, "relation": "standing-constraint"},
-            ],
-            "implementationPlan": {
-                "mode": "单路",
-                "routes": [{
-                    "pathIndex": 1,
-                    "pathType": "内容",
-                    "action": "改",
-                    "target": "段落 2207、2208 的 form_elements",
-                    "method": "产生元素",
-                    "emphasis": "结构完整性",
-                }],
-            },
-        }
-    )
-
-    route_block = decision["detail"]["blocks"][0]
-    evidence_block = decision["detail"]["blocks"][1]
-    assert route_block["type"] == "implementation-plan"
-    assert route_block["routes"][0]["target"] == "段落 2207、2208 的 形式信息"
-    assert route_block["routes"][0]["emphasis"] == "结构完整性"
-    assert evidence_block["title"] == "根据以下信息做出的决策"
-    assert evidence_block["visualRole"] == "evidence"
-    assert evidence_block["collapsible"] is False
-    assert evidence_block["items"][0]["value"] == full_goal
-    assert evidence_block["items"][0]["note"] == "持续约束;本轮必须遵守"
-
-
-def test_data_tradeoff_is_implementer_scope_and_sources_are_not_auto_adopted():
-    decision = AgentDecisionProjector().data_tradeoff(
-        {
-            "id": "data-decision:12",
-            "decision": "采用两条交叉印证的数据,暂不采用单一来源估算。",
-            "reasoning": "两类来源结论一致。",
-            "sources": [
-                {"type": "行业报告", "title": "报告 A"},
-                {"type": "搜索结果", "title": "结果 B"},
-            ],
-        }
-    )
-
-    assert decision["decisionType"] == "tradeoff"
-    assert decision["subtype"] == "data-tradeoff"
-    assert decision["actor"]["role"] == "implementer"
-    assert decision["authority"] == "implementation-scope"
-    assert {item["decisionUse"] for item in decision["inputs"]} == {"not-recorded"}
-    assert decision["body"]["selected"] == []
-    assert len(decision["card"]["secondary"]) == 2
-    blocks = {block["title"]: block for block in decision["detail"]["blocks"]}
-    assert blocks["参与判断"]["visualRole"] == "evidence"
-    assert blocks["明确取舍"]["visualRole"] == "key-result"
-    assert blocks["理由"]["visualRole"] == "reason"
-
-
-def test_data_tradeoff_uses_real_source_type_and_content_fields():
-    decision = AgentDecisionProjector().data_tradeoff(
-        {
-            "id": "data-decision:13",
-            "decision": "组合领域信息与账号模式。",
-            "sources": [
-                {
-                    "data_type": "领域信息",
-                    "data_content": "米豆腐热量约 50-60kcal/100g",
-                }
-            ],
-        }
-    )
-
-    source = decision["detail"]["blocks"][0]["items"][0]
-    assert source["label"] == "领域信息"
-    assert source["value"] == "米豆腐热量约 50-60kcal/100g"
-
-
-def test_multipath_tradeoff_is_main_final_and_does_not_render_internal_ids():
-    decision = AgentDecisionProjector().multipath_tradeoff(
-        {
-            "id": "multipath-decision:21",
-            "branch_ids": [3, 4],
-            "decision": "组合两个候选方案后合入主脚本。",
-            "reasoning": "两路内容互补。",
-            "reviewComparison": [
-                {
-                    "recommendation": "建议组合",
-                    "finalDecision": "组合采用",
-                    "handling": None,
-                }
-            ],
-        }
-    )
-
-    assert decision["authority"] == "final"
-    assert decision["actor"] == {"role": "main", "label": "主 Agent"}
-    assert decision["body"]["branchIds"] == [3, 4]
-    business_text = str(decision["card"]) + str(decision["detail"]["blocks"])
-    assert "branch_id" not in business_text
-    assert "比较 2 个候选方案" in business_text
-
-
-def test_evaluator_is_recommendation_with_structured_business_blocks():
-    decision = AgentDecisionProjector().evaluation(
-        {
-            "id": "multipath-review:40",
-            "subtype": "multipath-evaluation",
-            "subjects": ["候选方案 A", "候选方案 B"],
-            "criteria": ["结构完整", "证据可信"],
-            "itemConclusions": [
-                {"subject": "候选方案 A", "conclusion": "通过"},
-                {"subject": "候选方案 B", "conclusion": "部分通过"},
-            ],
-            "achievements": ["核心结构已经形成"],
-            "problems": [{"severity": "一般", "summary": "结尾仍需收束"}],
-            "conclusion": "部分通过",
-            "recommendation": "优先采用候选方案 A。",
-        }
-    )
-
-    assert decision["decisionType"] == "evaluation"
-    assert decision["authority"] == "recommendation"
-    assert decision["actor"]["role"] == "multipath-evaluator"
-    assert [block["title"] for block in decision["detail"]["blocks"]] == [
-        "评审对象",
-        "评审标准",
-        "逐项结论",
-        "已达成",
-        "问题",
-        "总结论",
-        "评审建议",
-    ]
-    conclusion = next(block for block in decision["detail"]["blocks"] if block["title"] == "总结论")
-    assert conclusion["visualRole"] == "key-result"
-
-
-def test_creative_requires_safe_think_and_plan_and_never_infers_from_artifact():
-    projector = AgentDecisionProjector()
-    event = {
-        "id": 70,
-        "event_type": "tool_call",
-        "event_name": "think_and_plan",
-        "agent_role": "script_implementer",
-        "status": "ok",
-        "inputData": {
-            "thought": "现有证据不足,抗性淀粉只作为待验证推导。",
-            "action": "补充烹饪贴士段",
-            "plan": "先写可确认内容,再标注待核验项。",
-        },
-    }
-
-    assert projector.creative({"safeLink": False, "event": event}) is None
-    assert projector.creative({"safeLink": True, "artifactRef": "artifact:9"}) is None
-
-    decision = projector.creative(
-        {
-            "safeLink": True,
-            "event": event,
-            "task": "补齐烹饪贴士",
-            "output": "形成候选脚本段落",
-            "uncertainties": ["抗性淀粉结论需要进一步核验"],
-            "artifactRef": "artifact:9",
-            "promptRef": "implementer:70",
-        }
-    )
-
-    assert decision is not None
-    assert decision["decisionType"] == "creative"
-    assert decision["authority"] == "implementation-scope"
-    assert decision["artifactRef"] == "artifact:9"
-    assert "待验证推导" in decision["body"]["reasoning"]
-    assert decision["body"]["uncertainties"] == ["抗性淀粉结论需要进一步核验"]
-    assert all(item["key"] != "task" for item in decision["card"]["secondary"])
-    assert "创作任务" not in [block["title"] for block in decision["detail"]["blocks"]]
-
-
-def test_missing_decision_is_explicit_without_fabricated_reasoning():
-    decision = AgentDecisionProjector().evaluation(
-        {"id": "overall-review:missing", "subtype": "overall-evaluation"}
-    )
-
-    assert decision["completeness"] == "missing"
-    assert decision["card"]["primary"]["value"] == "决策结论未记录"
-    assert decision["body"]["recommendation"] is None
-    assert decision["notices"] == [
-        {"code": "record-missing", "message": "没有找到结构化决策结论。"}
-    ]
-
-
-def test_business_copy_translates_internal_ids_without_deleting_business_numbers():
-    projection = AgentDecisionProjector().evaluation(
-        {
-            "id": "event:9",
-            "subtype": "multipath-evaluation",
-            "achievements": [
-                "branch3 完成 P2,参考 postid: 67c6c723,保留领域信息 31/32。"
-            ],
-            "recommendation": "建议合入 base。",
-        }
-    )
-
-    business = str(projection["detail"]["blocks"])
-    assert "方案 3" in business
-    assert "段落 2" in business
-    assert "账号内容样本" in business
-    assert "主脚本" in business
-    assert "领域信息 31/32" in business
-    assert "branch3" not in business
-    assert "postid" not in business

+ 0 - 219
visualization/backend/tests/test_evaluation_report_parser_v8.py

@@ -1,219 +0,0 @@
-from app.evaluation_report_parser import EvaluationReportParser
-
-
-def test_multipath_report_becomes_structured_business_data():
-    report = """## 评估报告 script_build_id=436
-
-### 分支评估 branch_id=3
-**该支结论**:通过
-**目标推进**:完成核心教学段落。
-
-### 分支评估 branch_id=4
-**该支结论**:部分通过
-**未通过项**:工具属性说明还需补充。
-
-### 对比与建议
-**采纳建议**:组合方案 3 与方案 4。
-"""
-    parsed = EvaluationReportParser().parse(report, kind="multipath")
-
-    assert parsed["subjects"] == ["方案 3", "方案 4"]
-    assert [item["conclusion"] for item in parsed["itemConclusions"]] == ["通过", "部分通过"]
-    assert parsed["itemConclusions"][0]["achievements"] == ["完成核心教学段落。"]
-    assert parsed["recommendation"] == "采纳建议:组合方案 3 与方案 4。"
-    assert "script_build_id" not in str(parsed)
-    assert "branch_id" not in str(parsed)
-
-
-def test_overall_report_extracts_criteria_achievements_problems_and_next_goal():
-    report = """### 评审标准
-- 内容完整性
-- 证据可靠性
-
-### 已达成
-- 三个核心段落已完成。
-
-### 问题详情
-- **paragraph_id=9**:收尾段落缺失。
-- 【中】数据说明仍需补充。
-
-### 整体结论:部分通过
-
-### 下一步引领目标
-补齐收尾并形成闭环。
-"""
-    parsed = EvaluationReportParser().parse(report, kind="overall")
-
-    assert parsed["criteria"] == ["内容完整性", "证据可靠性"]
-    assert parsed["achievements"] == ["三个核心段落已完成。"]
-    assert [item["summary"] for item in parsed["problems"]] == ["收尾段落缺失。", "【中】数据说明仍需补充。"]
-    assert parsed["problems"][1]["severity"] == "中"
-    assert parsed["conclusion"] == "部分通过"
-    assert parsed["nextGoal"] == "补齐收尾并形成闭环。"
-
-
-def test_markdown_table_is_parsed_without_exposing_internal_dimension_codes():
-    report = """### 维度评估
-
-| 维度 | 结论 | 已达成 | 问题 |
-| --- | --- | --- | --- |
-| D1 维度体系一致性 | 部分通过 | 主线统一 | 段 3 命名重复 |
-| P1 元素完整性 | 通过 | 元素齐全 |  |
-"""
-    parsed = EvaluationReportParser().parse(report, kind="overall")
-
-    assert parsed["criteria"] == ["维度体系一致性", "元素完整性"]
-    assert [item["subject"] for item in parsed["itemConclusions"]] == [
-        "维度体系一致性",
-        "元素完整性",
-    ]
-    assert parsed["problems"][0]["summary"] == "段 3 命名重复"
-    assert "D1" not in str(parsed)
-    assert "P1" not in str(parsed)
-
-
-def test_json_fence_and_wrong_build_id_never_become_business_content_or_association():
-    report = """## 评估报告 script_build_id=1393
-
-```json
-{"script_build_id": 434, "branch_id": 99, "dimension_pass": false}
-```
-
-### 整体结论:通过
-
-### 评审建议
-保留当前主线。
-"""
-    parsed = EvaluationReportParser().parse(report, kind="overall")
-
-    assert parsed["conclusion"] == "通过"
-    assert parsed["recommendation"] == "保留当前主线。"
-    serialized = str(parsed)
-    assert "1393" not in serialized
-    assert "434" not in serialized
-    assert "99" not in serialized
-    assert "dimension_pass" not in serialized
-
-
-def test_missing_headings_and_broken_markdown_still_extract_explicit_labels():
-    report = """**整体结论**:需要重试
-遗留问题:开头和结尾未呼应
-**下一轮目标**:重写收尾
-*** 未闭合的 Markdown
-"""
-    parsed = EvaluationReportParser().parse(report, kind="overall")
-
-    assert parsed["conclusion"] == "需要重试"
-    assert parsed["problems"] == [{"summary": "开头和结尾未呼应"}]
-    assert parsed["nextGoal"] == "重写收尾"
-    assert parsed["notices"] == []
-
-
-def test_business_numbers_are_not_removed_by_identifier_sanitizing():
-    report = """### 已达成
-- 领域信息 31/32 已确认。
-- 完成 5 个段落、12 个元素和 4:3 画面说明。
-
-### 整体结论:通过
-"""
-    parsed = EvaluationReportParser().parse(report)
-
-    assert parsed["achievements"] == [
-        "领域信息 31/32 已确认。",
-        "完成 5 个段落、12 个元素和 4:3 画面说明。",
-    ]
-
-
-def test_empty_or_unparseable_report_returns_stable_empty_shape():
-    parsed = EvaluationReportParser().parse("```json\n{broken}\n```", kind="overall")
-
-    assert parsed == {
-        "parserVersion": 1,
-        "kind": "overall",
-        "subjects": [],
-        "criteria": [],
-        "itemConclusions": [],
-        "achievements": [],
-        "problems": [],
-        "conclusion": None,
-        "recommendation": None,
-        "nextGoal": None,
-        "notices": ["unparsed-report"],
-    }
-
-
-def test_no_problem_markers_are_not_presented_as_warnings_and_suffix_heading_splits():
-    parsed = EvaluationReportParser().parse(
-        """
-### 分支评估 branch_id=5
-**该支结论**:通过
-**目标推进**:已完成收尾。
-**未通过项**:无显著质量问题。
-
-**领域评估维度**(已激活并判定):
-- 烹饪常识:通过
-""",
-        kind="multipath",
-    )
-
-    assert parsed["problems"] == []
-    assert parsed["itemConclusions"][0]["problems"] == []
-    assert "烹饪常识:通过" in parsed["criteria"]
-
-
-def test_positive_sentence_after_no_problem_marker_is_not_a_problem():
-    parsed = EvaluationReportParser().parse(
-        "## 问题\n无显著质量问题。脚本已达到发布标准。\n\n## 整体结论\n通过",
-        kind="overall",
-    )
-    assert parsed["problems"] == []
-
-
-def test_multipath_report_keeps_branch_groups_and_does_not_treat_not_passed_as_achievement():
-    report = """### 分支评估 branch_id=1
-**该支结论**:部分通过
-
-**目标推进**:
-- 领域信息库新增 5 条事实(id 54-58)。
-
-**未通过/存疑项**:
-- 元素创建尚未完整落地。
-
-**有据扎实度**:来源可指位。
-
-### 分支评估 branch_id=2
-**该支结论**:部分通过
-
-**目标推进**:
-- 已搭建五段骨架。
-
-**未通过/部分通过项**:
-- 数量红线违规。
-- function_elements 全部为空。
-
-### 对比与建议
-| 维度 | branch1 | branch2 | 差异依据 |
-| --- | --- | --- | --- |
-| 与本轮目标的契合度 | 高 | 高 | 各自完成主任务 |
-| 结构完备性 | 不适用 | 部分通过 | 仅方案二涉及 |
-"""
-    parsed = EvaluationReportParser().parse(report, kind="multipath")
-
-    assert parsed["branchEvaluations"][0] == {
-        "subject": "方案 1",
-        "conclusion": "部分通过",
-        "achievements": ["领域信息库新增 5 条事实(id 54-58)。"],
-        "problems": ["元素创建尚未完整落地。"],
-        "evidence": "来源可指位。",
-    }
-    assert parsed["branchEvaluations"][1]["achievements"] == ["已搭建五段骨架。"]
-    assert parsed["branchEvaluations"][1]["problems"] == ["数量红线违规。", "function_elements 全部为空。"]
-    assert "数量红线违规。" not in parsed["achievements"]
-    assert parsed["comparisonRows"][0] == {
-        "criterion": "与本轮目标的契合度",
-        "candidates": [
-            {"subject": "方案 1", "value": "高"},
-            {"subject": "方案 2", "value": "高"},
-        ],
-        "basis": "各自完成主任务",
-    }

+ 0 - 79
visualization/backend/tests/test_execution_view_payload_v8.py

@@ -1,79 +0,0 @@
-from app.execution_view_payload import compact_execution_view
-
-
-def test_main_payload_removes_decision_details_and_runtime_previews():
-    source = {
-        "schemaVersion": "8",
-        "objective": {
-            "decision": {
-                "semanticKind": "decision",
-                "card": {"primary": {"value": "目标"}},
-                "detail": {"blocks": [{"value": "完整业务详情"}]},
-            }
-        },
-        "rounds": [
-            {
-                "branches": [
-                    {
-                        "dataDecisions": [
-                            {
-                                "id": "data-decision:1",
-                                "recordId": 1,
-                                "reasoning": "原始理由",
-                                "sources": [{"post_id": "secret"}],
-                                "node": {"id": "data-decision:1", "card": {}},
-                            },
-                        ]
-                    },
-                    {
-                        "retrievalStage": {
-                            "agentRuns": [
-                                {
-                                    "businessLabel": "知识 Agent",
-                                    "agentType": "retrieve_data_knowledge",
-                                }
-                            ]
-                        }
-                    },
-                ],
-                "convergenceBatches": [
-                    {
-                        "review": {
-                            "id": "multipath-review:7",
-                            "eventId": 7,
-                            "roundIndex": 1,
-                            "branchIds": [1, 2],
-                            "status": "ok",
-                            "inputPreview": "完整任务",
-                            "outputPreview": "完整评审报告",
-                            "achievements": ["很长的报告内容"],
-                            "detailRef": "event:7",
-                            "decisionProjection": {
-                                "semanticKind": "decision",
-                                "card": {"primary": {"value": "通过"}},
-                                "detail": {"blocks": [{"value": "完整评审"}]},
-                            },
-                        }
-                    }
-                ]
-            }
-        ],
-    }
-
-    result = compact_execution_view(source)
-
-    assert "detail" not in result["objective"]["decision"]
-    review = result["rounds"][0]["convergenceBatches"][0]["review"]
-    assert "inputPreview" not in review
-    assert "outputPreview" not in review
-    assert "achievements" not in review
-    assert "detail" not in review["decisionProjection"]
-    data_decision = result["rounds"][0]["branches"][0]["dataDecisions"][0]
-    assert data_decision == {
-        "id": "data-decision:1",
-        "node": {"id": "data-decision:1", "card": {}},
-    }
-    agent_run = result["rounds"][0]["branches"][1]["retrievalStage"]["agentRuns"][0]
-    assert agent_run == {"businessLabel": "知识 Agent"}
-    assert "retrieve_data_" not in str(result)
-    assert source["objective"]["decision"]["detail"]

+ 0 - 690
visualization/backend/tests/test_main_agent_decision_projection.py

@@ -1,690 +0,0 @@
-from __future__ import annotations
-
-from app.main_agent_decision_projection import (
-    MainAgentDecisionProjector,
-    parse_goal_items,
-    parse_script_direction,
-)
-from app.runtime_event_index import RuntimeEventIndex
-from app.runtime_tool_catalog import business_tool_label
-
-
-DIRECTION = """## 选题价值主张
-用真实事件解释行业变化。
-
-## 账号价值主张
-保持专业深度与信息密度。
-
-## 创作总目标
-### 目标 1
-- 目标语句:拆解机制并让读者看懂因果链。
-- 通过标准:结构清楚。
-### 目标 2
-- 目标语句:用可核验数据支撑关键结论。
-
-## 领域评估维度
-1. 数据可信度
-2. 适用边界
-"""
-
-
-def _event(
-    event_id: int,
-    event_seq: int,
-    event_type: str,
-    event_name: str,
-    **extra,
-):
-    return {
-        "id": event_id,
-        "event_seq": event_seq,
-        "event_type": event_type,
-        "event_name": event_name,
-        "status": "ok",
-        "ended_at": f"2026-07-14T10:00:{event_seq:02d}",
-        **extra,
-    }
-
-
-def _main_scope(event_id: int = 1):
-    return _event(
-        event_id,
-        1,
-        "agent_scope",
-        "main",
-        agent_role="main",
-        agent_depth=0,
-        scope_event_id=event_id,
-        parent_event_id=None,
-    )
-
-
-def _main_tool(event_id: int, event_seq: int, name: str, **extra):
-    return _event(
-        event_id,
-        event_seq,
-        "tool_call",
-        name,
-        agent_role="main",
-        agent_depth=0,
-        scope_event_id=1,
-        parent_event_id=1,
-        **extra,
-    )
-
-
-def test_runtime_index_requires_real_main_scope_for_direct_reads():
-    events = [
-        _main_scope(),
-        _main_tool(2, 2, "get_topic_detail"),
-        _event(
-            3,
-            3,
-            "tool_call",
-            "get_topic_detail",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=30,
-            parent_event_id=30,
-        ),
-        _event(
-            4,
-            4,
-            "tool_call",
-            "get_topic_detail",
-            agent_role="main",
-            agent_depth=0,
-            scope_event_id=999,
-            parent_event_id=999,
-        ),
-        _main_tool(5, 5, "get_domain_info", status="error"),
-    ]
-    index = RuntimeEventIndex(events)
-
-    assert index.main_scope_ids == {1}
-    assert [event["id"] for event in index.main_direct_reads()] == [2]
-    assert [event["id"] for event in index.main_direct_reads(successful=False)] == [5]
-    assert [event["id"] for event in index.events_for_actor("main")] == [1, 2, 4, 5]
-    assert [event["id"] for event in index.events_for_actor("overall-evaluator")] == [3]
-    assert business_tool_label("get_topic_detail") == "选题"
-
-
-def test_planning_analysis_uses_last_real_main_thought_before_direction_save():
-    events = [
-        _main_scope(),
-        _main_tool(2, 2, "think_and_plan", inputData={
-            "thought_number": 1,
-            "thought_summary": "先形成旧规划",
-            "thought": "旧思考",
-            "plan": "旧计划",
-            "action": "旧下一步",
-        }),
-        _main_tool(3, 3, "think_and_plan", inputData={
-            "thought_number": 2,
-            "thought_summary": "锁定创作目标与首轮骨架",
-            "thought": "完整分析账号结构与领域下限。",
-            "plan": "保存目标,然后开启首轮。",
-            "action": "写入创作总目标",
-        }),
-        _main_tool(4, 4, "save_script_direction", inputData={"script_direction": DIRECTION}),
-        _main_tool(5, 5, "think_and_plan", inputData={
-            "thought_summary": "保存后的其他规划",
-            "thought": "不应成为开场规划",
-            "plan": "后续计划",
-            "action": "继续",
-        }),
-    ]
-
-    planning = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction=DIRECTION,
-        rounds=[],
-    )["planningAnalysis"]
-
-    assert planning == {
-        "stage": "planning-analysis",
-        "summary": "锁定创作目标与首轮骨架",
-        "thought": "完整分析账号结构与领域下限。",
-        "plan": "保存目标,然后开启首轮。",
-        "action": "写入创作总目标",
-        "eventRef": "event:3",
-        "technicalRefs": ["event:3"],
-        "completeness": "complete",
-    }
-
-
-def test_begin_round_prefers_success_output_then_falls_back_to_round_marker():
-    events = [
-        _main_scope(),
-        _main_tool(
-            2,
-            2,
-            "begin_round",
-            round_index=0,
-            output_preview='{"success": true, "round_index": 1}',
-        ),
-        _main_tool(
-            3,
-            3,
-            "begin_round",
-            round_index=1,
-            output_preview='{"success": true}',
-        ),
-        _event(
-            4,
-            4,
-            "round_begin",
-            "2",
-            agent_role="main",
-            agent_depth=0,
-            scope_event_id=1,
-            parent_event_id=1,
-            payload={"round_index": 2, "goal": "第二轮"},
-        ),
-    ]
-    index = RuntimeEventIndex(events)
-
-    assert index.resolved_begin_round(events[1]) == 1
-    assert index.resolved_begin_round(events[2]) == 2
-    assert {key: [event["id"] for event in value] for key, value in index.successful_begin_rounds().items()} == {1: [2], 2: [3]}
-
-
-def test_evaluator_association_uses_hierarchy_and_structured_round_not_report_text():
-    events = [
-        _main_scope(),
-        _event(
-            10,
-            2,
-            "agent_invoke",
-            "script_evaluator",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=10,
-            parent_event_id=1,
-            round_index=1,
-            agentOutputData={"summary": "## 评估报告 round=99\n### 整体结论:部分通过"},
-        ),
-        _event(
-            11,
-            3,
-            "agent_invoke",
-            "script_evaluator",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=11,
-            parent_event_id=None,
-            round_index=1,
-            agentOutputData={"summary": "### 整体结论:通过"},
-        ),
-    ]
-    associations = RuntimeEventIndex(events).agent_returns(
-        "script_evaluator", round_index=1
-    )
-
-    assert [(event["id"], relation) for event, relation in associations] == [
-        (10, "returned-report"),
-        (11, "runtime-associated"),
-    ]
-    assert RuntimeEventIndex(events).agent_returns("script_evaluator", round_index=99) == []
-
-
-def test_direction_parser_selects_real_goals_instead_of_value_proposition():
-    parsed = parse_script_direction(DIRECTION)
-
-    assert parsed["goals"] == [
-        "拆解机制并让读者看懂因果链。",
-        "用可核验数据支撑关键结论。",
-    ]
-    assert parsed["constraints"] == ["数据可信度", "适用边界", "结构清楚。"]
-    assert parsed["valueJudgements"][0]["label"] == "选题价值主张"
-    assert parsed["goals"][0] != parsed["valueJudgements"][0]["value"]
-    assert parsed["raw"] == DIRECTION.strip()
-
-
-def test_full_projection_preserves_db_truth_and_three_honest_decision_chains():
-    first_paths = [
-        {
-            "path_index": 1,
-            "path_type": "内容",
-            "action": "新增",
-            "target": "机制拆解段",
-            "method": "案例对照",
-            "emphasis": "结构可读性",
-        },
-        {
-            "path_index": 2,
-            "path_type": "领域信息",
-            "action": "核验",
-            "target": "行业数据",
-            "method": "交叉验证",
-        },
-    ]
-    second_paths = [
-        {"path_index": 1, "path_type": "内容", "action": "补齐", "target": "结尾"}
-    ]
-    events = [
-        _main_scope(),
-        _main_tool(2, 2, "get_topic_detail", output_preview='{"topic":{"result":"解释真实机制"},"points":[{"point_result":"测试选题"}]}'),
-        _main_tool(3, 3, "get_account_script_section_patterns", output_preview='{"\u5206\u6bb5\u89c4\u5f8b\u6458\u8981":"成品展示到机制拆解的四段结构"}'),
-        _main_tool(4, 4, "get_account_script_persona_points", output_preview='{"returned_count":82,"records":[{"\u5217":"形式"},{"\u5217":"作用"}]}'),
-        _event(
-            40,
-            5,
-            "tool_call",
-            "get_topic_detail",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=39,
-            parent_event_id=39,
-            output_preview='{"count":1}',
-        ),
-        _main_tool(
-            5,
-            6,
-            "save_script_direction",
-            inputData={"script_direction": DIRECTION},
-            output_preview='{"success": true}',
-        ),
-        _main_tool(
-            6,
-            7,
-            "begin_round",
-            round_index=0,
-            inputData={"goal": "搭建机制拆解主结构"},
-            output_preview='{"success": true, "round_index": 1}',
-        ),
-        _event(
-            7,
-            8,
-            "round_begin",
-            "1",
-            agent_role="main",
-            agent_depth=0,
-            scope_event_id=1,
-            parent_event_id=1,
-            payload={"round_index": 1},
-        ),
-        _main_tool(8, 9, "get_script_snapshot", round_index=1, output_preview='{"count":0}'),
-        _main_tool(
-            9,
-            10,
-            "record_multipath_plan",
-            round_index=1,
-            inputData={"paths": first_paths, "mode": "竞争", "note": "一路搭结构,一路核验数据。"},
-            output_preview='{"success": true, "round_index": 1, "path_count": 2}',
-        ),
-        _main_tool(
-            90,
-            11,
-            "record_multipath_plan",
-            round_index=1,
-            inputData={"paths": [], "mode": "竞争"},
-            status="error",
-            output_preview='{"success": false, "error": "paths required"}',
-        ),
-        _event(
-            10,
-            12,
-            "agent_invoke",
-            "script_evaluator",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=10,
-            parent_event_id=1,
-            round_index=1,
-            agentOutputData={
-                "summary": "## 评估报告 round=7\n### 整体结论:部分通过\n引领目标:补齐结尾\n遗留问题:缺少行动建议"
-            },
-        ),
-        _main_tool(
-            11,
-            13,
-            "begin_round",
-            round_index=1,
-            inputData={"goal": "补齐结尾。\n具体任务:增加行动建议"},
-            output_preview='{"success": true, "round_index": 2}',
-        ),
-        _event(
-            12,
-            14,
-            "round_begin",
-            "2",
-            agent_role="main",
-            agent_depth=0,
-            scope_event_id=1,
-            parent_event_id=1,
-            payload={"round_index": 2},
-        ),
-        _main_tool(
-            13,
-            15,
-            "record_multipath_plan",
-            round_index=2,
-            inputData={"paths": second_paths, "mode": "分工", "note": "任务已收敛,不需要重复探索。"},
-            output_preview='{"success": true, "round_index": 2, "path_count": 1}',
-        ),
-    ]
-    rounds = [
-        {
-            "round_index": 1,
-            "goal": "搭建机制拆解主结构",
-            "multipath_plan": first_paths,
-            "race_or_divide": "竞争",
-            "plan_note": "一路搭结构,一路核验数据。",
-        },
-        {
-            "round_index": 2,
-            "goal": "补齐结尾。\n具体任务:增加行动建议",
-            "multipath_plan": second_paths,
-            "race_or_divide": "分工",
-            "plan_note": "任务已收敛,不需要重复探索。",
-        },
-    ]
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction=DIRECTION,
-        rounds=rounds,
-        multipath_decisions=[
-            {
-                "id": 501,
-                "round_index": 1,
-                "decision": "采用结构方案,保留数据核验结果。",
-                "reasoning": "两路结果可组合。",
-                "created_at": "2026-07-14T10:00:11",
-            }
-        ],
-    )
-
-    objective = projection["objective"]
-    assert objective["decisionItems"][0] == "拆解机制并让读者看懂因果链。"
-    assert [item["label"] for item in objective["inputs"]] == [
-        "选题",
-        "账号段落模式",
-        "账号人设",
-    ]
-    assert [item["summary"] for item in objective["inputs"]] == [
-        "测试选题:解释真实机制",
-        "成品展示到机制拆解的四段结构",
-        "返回 82 个账号表达特征,主要涉及形式、作用",
-    ]
-    assert 40 not in [int(item["detailRef"].split(":")[1]) for item in objective["inputs"]]
-
-    round_two = projection["roundGoalsByRound"][2]
-    assert [item["relation"] for item in round_two["inputs"][:2]] == [
-        "returned-report",
-        "persisted-record",
-    ]
-    assert round_two["inputs"][0]["summary"] == "部分通过"
-    assert "下一步:补齐结尾" in round_two["constraints"]
-    assert round_two["revisionRefs"] == ["event:11"]
-
-    first_plan = projection["implementationPlansByRound"][1]
-    assert first_plan["decisionItems"] == [
-        "方案 1 · 新增:机制拆解段",
-        "方案 2 · 核验:行业数据",
-    ]
-    assert first_plan["inputs"][1]["label"] == "当前主脚本"
-    assert first_plan["currentRevisionRef"] == "event:9"
-    assert first_plan["technicalRefs"] == ["event:90"]
-    assert first_plan["routeItems"][0] == {
-        "pathIndex": 1,
-        "pathType": "内容",
-        "action": "新增",
-        "target": "机制拆解段",
-        "method": "案例对照",
-        "emphasis": "结构可读性",
-    }
-
-    second_plan = projection["implementationPlansByRound"][2]
-    assert second_plan["displayMode"] == "单路"
-    assert second_plan["rawMode"] == "分工"
-
-    assert projection["objective"]["sourceDocument"] == DIRECTION.strip()
-    first_goal = projection["roundGoalsByRound"][1]
-    assert first_goal["decisionItems"] == ["搭建机制拆解主结构"]
-    assert first_goal["sourceDocument"] == "搭建机制拆解主结构"
-    assert "goalSections" not in first_goal
-
-
-def test_round_goal_inputs_keep_complete_business_text():
-    long_goal = "完整创作目标需要保留全部约束和判断依据。" * 24
-    long_report = "上一轮整体评审的完整结论需要原样传给下一轮。" * 20
-    long_decision = "上一轮主 Agent 的完整多路决策需要原样保留。" * 20
-    direction = f"## 创作总目标\n- 目标语句:{long_goal}"
-    events = [
-        _main_scope(),
-        _main_tool(
-            2,
-            2,
-            "save_script_direction",
-            inputData={"script_direction": direction},
-            output_preview='{"success": true}',
-        ),
-        _main_tool(
-            3,
-            3,
-            "begin_round",
-            round_index=0,
-            output_preview='{"success": true, "round_index": 1}',
-        ),
-        _event(
-            4,
-            4,
-            "agent_invoke",
-            "script_evaluator",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=4,
-            parent_event_id=1,
-            round_index=1,
-            agentOutputData={"summary": f"### 整体结论:{long_report}"},
-        ),
-        _main_tool(
-            5,
-            5,
-            "begin_round",
-            round_index=1,
-            output_preview='{"success": true, "round_index": 2}',
-        ),
-    ]
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction=direction,
-        rounds=[
-            {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
-            {"round_index": 2, "goal": "第二轮", "multipath_plan": []},
-        ],
-        multipath_decisions=[
-            {
-                "id": 88,
-                "round_index": 1,
-                "decision": long_decision,
-                "created_at": "2026-07-14T10:00:04",
-            }
-        ],
-    )
-
-    first_round_input = projection["roundGoalsByRound"][1]["inputs"][0]["summary"]
-    second_round_inputs = projection["roundGoalsByRound"][2]["inputs"]
-    assert first_round_input == "\n\n".join(projection["objective"]["decisionItems"])
-    assert len(first_round_input) > 220
-    assert second_round_inputs[0]["summary"] == long_report
-    assert second_round_inputs[1]["summary"] == long_decision
-    assert all(not str(item["summary"]).endswith("…") for item in second_round_inputs[:2])
-
-
-def test_db_only_projection_degrades_without_inventing_runtime_evidence():
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex([]),
-        script_direction="做一个清晰的行业解释。",
-        rounds=[
-            {
-                "round_index": 1,
-                "goal": "搭建结构",
-                "multipath_plan": [{"path_index": 1, "target": "开场"}],
-                "race_or_divide": "分工",
-                "plan_note": None,
-            }
-        ],
-    )
-
-    assert projection["objective"]["completeness"] == "partial"
-    assert projection["objective"]["inputs"] == []
-    assert projection["roundGoalsByRound"][1]["completeness"] == "partial"
-    assert projection["implementationPlansByRound"][1]["completeness"] == "partial"
-    assert projection["implementationPlansByRound"][1]["explicitReasoning"] is None
-
-
-def test_multiple_saves_and_plan_versions_preserve_history_and_separate_failures():
-    old_direction = "## 创作总目标\n- 目标语句:旧目标"
-    current_direction = "## 创作总目标\n- 目标语句:新目标"
-    old_paths = [{"path_index": 1, "action": "新增", "target": "开场"}]
-    current_paths = [{"path_index": 1, "action": "改写", "target": "开场"}]
-    events = [
-        _main_scope(),
-        _main_tool(2, 2, "save_script_direction", inputData={"script_direction": old_direction}, output_preview='{"success":true}'),
-        _main_tool(3, 3, "save_script_direction", inputData={"script_direction": current_direction}, output_preview='{"success":true}'),
-        _main_tool(4, 4, "save_script_direction", status="error", inputData={"script_direction": "失败版"}, output_preview='{"success":false,"error":"write failed"}'),
-        _main_tool(5, 5, "begin_round", round_index=0, output_preview='{"success":true,"round_index":1}'),
-        _main_tool(6, 6, "record_multipath_plan", round_index=1, inputData={"paths": old_paths, "mode": "竞争", "note": "初版"}, output_preview='{"success":true,"round_index":1}'),
-        _main_tool(7, 7, "record_multipath_plan", round_index=1, inputData={"paths": current_paths, "mode": "分工", "note": "修订版"}, output_preview='{"success":true,"round_index":1}'),
-    ]
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction=current_direction,
-        rounds=[{
-            "round_index": 1,
-            "goal": "修正开场",
-            "multipath_plan": current_paths,
-            "race_or_divide": "分工",
-            "plan_note": "修订版",
-        }],
-    )
-
-    assert projection["objective"]["revisionRefs"] == ["event:2", "event:3"]
-    assert projection["objective"]["currentRevisionRef"] == "event:3"
-    assert projection["objective"]["technicalRefs"] == ["event:4"]
-    plan = projection["implementationPlansByRound"][1]
-    assert plan["revisionRefs"] == ["event:6", "event:7"]
-    assert [item["current"] for item in plan["revisions"]] == [False, True]
-    assert plan["currentRevisionRef"] == "event:7"
-
-
-def test_goal_parser_keeps_the_saved_goal_as_one_item():
-    goal = "本轮要补齐结构\n① 增加开场\n② 补齐数据"
-    assert parse_goal_items(goal) == [goal]
-
-
-def test_plan_revision_matches_business_alias_without_changing_raw_db_mode():
-    paths = [{"path_index": 1, "action": "改写", "target": "开场"}]
-    events = [
-        _main_scope(),
-        _main_tool(2, 2, "begin_round", round_index=0, output_preview='{"success":true,"round_index":1}'),
-        _main_tool(
-            3,
-            3,
-            "record_multipath_plan",
-            round_index=1,
-            inputData={"paths": paths, "mode": "竞争", "note": "比较两路表达"},
-            output_preview='{"success":true,"round_index":1}',
-        ),
-    ]
-    plan = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction=None,
-        rounds=[{
-            "round_index": 1,
-            "goal": "修正开场",
-            "multipath_plan": paths,
-            "race_or_divide": "赛马",
-            "plan_note": "比较两路表达",
-        }],
-    )["implementationPlansByRound"][1]
-
-    assert plan["rawMode"] == "赛马"
-    assert plan["currentRevisionRef"] == "event:3"
-
-
-def test_failed_begin_round_does_not_reuse_its_old_round_context():
-    events = [
-        _main_scope(),
-        _main_tool(
-            2,
-            2,
-            "begin_round",
-            round_index=1,
-            status="error",
-            output_preview='{"success":false,"error":"write failed"}',
-        ),
-    ]
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction="创作目标",
-        rounds=[
-            {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
-            {"round_index": 2, "goal": "第二轮", "multipath_plan": []},
-        ],
-    )
-
-    assert projection["roundGoalsByRound"][1]["technicalRefs"] == []
-    assert projection["roundGoalsByRound"][2]["technicalRefs"] == []
-    assert projection["unassigned"][0]["eventId"] == 2
-    assert projection["unassigned"][0]["association"] == "failed-main-decision-call-unassigned"
-
-
-def test_round_goal_keeps_report_problems_and_rejects_late_db_decisions():
-    events = [
-        _main_scope(),
-        _event(
-            10,
-            2,
-            "agent_invoke",
-            "script_evaluator",
-            agent_role="script_evaluator",
-            agent_depth=1,
-            scope_event_id=10,
-            parent_event_id=1,
-            round_index=1,
-            agentOutputData={
-                "summary": """### 整体结论:部分通过
-
-**问题详情**:
-- **paragraph_id=9**:收尾段落缺失。
-
-**下一步引领目标**:
-**补齐收尾并形成闭环。**
-"""
-            },
-        ),
-        _main_tool(
-            11,
-            5,
-            "begin_round",
-            round_index=1,
-            started_at="2026-07-14T10:00:05",
-            output_preview='{"success":true,"round_index":2}',
-        ),
-    ]
-    projection = MainAgentDecisionProjector().project(
-        RuntimeEventIndex(events),
-        script_direction="创作目标",
-        rounds=[
-            {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
-            {"round_index": 2, "goal": "补齐收尾", "multipath_plan": []},
-        ],
-        multipath_decisions=[
-            {
-                "id": 71,
-                "round_index": 1,
-                "decision": "采用方案 1",
-                "created_at": "2026-07-14T10:00:06",
-            }
-        ],
-    )
-    goal = projection["roundGoalsByRound"][2]
-
-    assert "问题:收尾段落缺失。" in goal["constraints"]
-    assert "下一步:补齐收尾并形成闭环。" in goal["constraints"]
-    assert not any(value.endswith("---") for value in goal["constraints"])
-    assert not any(item["label"] == "上一轮主 Agent 多路决策" for item in goal["inputs"])
-    assert "multipath-decision:71" in goal["technicalRefs"]

+ 0 - 88
visualization/backend/tests/test_main_decision_text.py

@@ -1,88 +0,0 @@
-from app.main_decision_text import (
-    parse_goal_text,
-    parse_objective_text,
-    plan_mode_label,
-    plan_path_summary,
-)
-
-
-def test_objective_parser_prefers_real_target_statement_over_value_proposition():
-    value = """## 选题价值主张
-这是选题价值,不是目标。
-
-## 账号价值主张
-这是账号价值。
-
-## 创作总目标
-### 目标 1
-- 目标语句:形成清晰可执行的内容主线。
-- 通过标准:必须包含完整结构。
-### 目标 2
-- 目标语句:补齐可靠的数据说明。
-
-## 领域评估维度
-### 领域维度 1
-- name:行业事实准确性
-"""
-    parsed = parse_objective_text(value)
-
-    assert parsed["headline"] == "形成清晰可执行的内容主线。"
-    assert parsed["targets"] == ["形成清晰可执行的内容主线。", "补齐可靠的数据说明。"]
-    assert parsed["topicValue"] == "这是选题价值,不是目标。"
-    assert parsed["accountValue"] == "这是账号价值。"
-    assert parsed["targetCount"] == 2
-    assert parsed["domainDimensionCount"] == 1
-    assert parsed["constraintItems"] == [
-        {"label": "通过标准", "value": "必须包含完整结构。", "group": "目标 1"}
-    ]
-    assert parsed["constraintGroups"] == [
-        {
-            "title": "目标 1",
-            "kind": "target",
-            "dimensionName": None,
-            "items": [{"label": "通过标准", "value": "必须包含完整结构。"}],
-        },
-        {
-            "title": "领域维度 1",
-            "kind": "domain",
-            "dimensionName": "行业事实准确性",
-            "items": [],
-        },
-    ]
-
-
-def test_objective_parser_falls_back_without_inventing_a_summary():
-    parsed = parse_objective_text("把产品机制解释清楚,并给出真实案例。")
-    assert parsed["headline"] == "把产品机制解释清楚,并给出真实案例。"
-    assert parsed["targets"] == []
-    assert parsed["structured"] is False
-
-
-def test_goal_parser_keeps_the_complete_saved_goal_as_one_value():
-    value = "补齐烹饪步骤。\n① 写清关键动作\n② 补充火候说明\n③ 加入避坑提示"
-    parsed = parse_goal_text(value)
-    assert parsed["headline"] == value
-    assert parsed["focusItems"] == []
-    assert parsed["raw"] == value
-    assert "sections" not in parsed
-
-
-def test_goal_parser_does_not_infer_roles_from_sentence_keywords():
-    value = (
-        "立起脚本的段落骨架。"
-        "竞争不同的框架切分与编排策略,选出整体最佳方案。"
-        "本轮只做段落结构,不填充具体文案。"
-    )
-
-    parsed = parse_goal_text(value)
-
-    assert parsed == {"headline": value, "focusItems": [], "raw": value}
-
-
-def test_single_path_is_business_single_even_when_raw_mode_is_divide():
-    assert plan_mode_label("分工", 1) == "单路"
-    assert plan_mode_label("竞争", 2) == "竞争"
-    assert plan_path_summary(
-        {"path_index": 2, "action": "增", "target": "烹饪步骤", "method": "补充细节"},
-        2,
-    ) == "方案 2:新增 · 烹饪步骤 · 补充细节"

+ 0 - 261
visualization/backend/tests/test_module_audit.py

@@ -1,261 +0,0 @@
-from __future__ import annotations
-
-from copy import deepcopy
-from importlib import import_module
-
-from fastapi.testclient import TestClient
-
-from app import main
-from app.module_audit.service import ModuleAuditService
-
-
-class _Provider:
-    mode = "test"
-
-    def __init__(self, bundle):
-        self.bundle = deepcopy(bundle)
-        self.load_bundle_calls = 0
-
-    def load_bundle(self, _script_build_id: int):
-        self.load_bundle_calls += 1
-        return deepcopy(self.bundle)
-
-    def load_event_detail(self, _script_build_id: int, event_id: int):
-        event = next(item for item in self.bundle["events"] if item["id"] == event_id)
-        return {
-            **deepcopy(event),
-            "input": {"content": deepcopy(event.get("inputData"))},
-            "output": {
-                "content": deepcopy(
-                    event.get("agentOutputData") or event.get("output_preview")
-                )
-            },
-        }
-
-
-class _Repository:
-    def __init__(self, bundle):
-        self.events = {item["id"]: deepcopy(item) for item in bundle["events"]}
-        self.load_event_calls = 0
-        self.load_log_calls = 0
-
-    def load_event(self, _script_build_id: int, event_id: int):
-        self.load_event_calls += 1
-        event = deepcopy(self.events.get(event_id))
-        if event is None:
-            return None
-        event["eventBody"] = {
-            "input_content_type": "application/json",
-            "input_content": '{"task":"完整派发任务"}',
-            "output_content_type": "text/plain",
-            "output_content": "完整运行返回",
-        }
-        return event
-
-    def load_log(self, _script_build_id: int):
-        self.load_log_calls += 1
-        return (
-            "[AGENT_TASK:script_implementer]\n"
-            "[MSG_ANCHOR:msg_id=task-50:seq=10]\n"
-            "完整派发任务\n"
-            "[TOOL_ANCHOR:msg_id=tool-51:seq=11]\n"
-            "后续工具调用"
-        )
-
-
-def _service(current_bundle):
-    return ModuleAuditService(
-        provider=_Provider(current_bundle), repository=_Repository(current_bundle)
-    )
-
-
-def test_audit_index_is_lightweight_and_collects_full_flow_modules(current_bundle):
-    payload = _service(current_bundle).index(88001)
-
-    module_ids = {item["id"] for item in payload["modules"]}
-    assert "run:objective" in module_ids
-    assert "round:1:branch:1:task" in module_ids
-    assert "event:53" in module_ids
-    assert "round:1:result" in module_ids
-    assert not any(item["displayVariant"] == "collapsed-summary" for item in payload["modules"])
-    assert "round:1:summary" not in module_ids
-    assert "round:1:branch:1:summary" not in module_ids
-    assert "eventBody" not in str(payload)
-    assert "contexts" not in payload
-
-
-def test_audit_reuses_completed_run_snapshot_between_index_and_detail(current_bundle):
-    service = _service(current_bundle)
-
-    service.index(88001)
-    service.detail(88001, "round:1:branch:1:task")
-
-    assert service.provider.load_bundle_calls == 1
-
-
-def test_audit_reuses_completed_module_detail(current_bundle):
-    service = _service(current_bundle)
-
-    first = service.detail(88001, "round:1:branch:1:task")
-    event_calls = service.repository.load_event_calls
-    log_calls = service.repository.load_log_calls
-    second = service.detail(88001, "round:1:branch:1:task")
-
-    assert second is first
-    assert service.repository.load_event_calls == event_calls
-    assert service.repository.load_log_calls == log_calls
-
-
-def test_branch_task_audit_separates_card_event_and_fallback_sources(current_bundle):
-    current_bundle["events"][4]["msg_id"] = "task-50"
-    current_bundle["events"][4]["inputData"] = {
-        "task": "完整的实现 Agent 派发任务"
-    }
-    payload = _service(current_bundle).detail(88001, "round:1:branch:1:task")
-
-    assert payload["actualUse"]["card"]["title"] == "实现任务"
-    reads = payload["actualReads"]
-    assert any(
-        item["consumer"] == "card"
-        and item["fieldPath"] == "impl_task"
-        and item["disposition"] == "selected"
-        for item in reads
-    )
-    event_read = next(
-        item
-        for item in reads
-        if item["consumer"] == "inspector"
-        and item["fieldPath"] == "inputData.task"
-    )
-    assert event_read["value"] == "完整的实现 Agent 派发任务"
-    assert event_read["disposition"] == "selected"
-    assert event_read["transformation"] == "直接摘取"
-    assert event_read["source"]["sourceKind"] == "database"
-    assert event_read["sourceKey"] == "database:event:50"
-    assert event_read["usageAreas"] == [
-        "Inspector 业务详情",
-        "Inspector 技术详情",
-    ]
-    assert event_read["usageRatio"] == {
-        "calculable": True,
-        "value": 1.0,
-        "label": "100.0%",
-    }
-    assert event_read["readId"].startswith("read:")
-    assert event_read["contextRefs"] == [
-        {
-            "contextId": "event:50",
-            "sourceKey": "database:event:50",
-            "contextFieldPath": "inputData.task",
-            "relation": "direct",
-            "exact": True,
-        }
-    ]
-    fallback_read = next(
-        item
-        for item in reads
-        if item["consumer"] == "inspector"
-        and item["fieldPath"] == "impl_task"
-    )
-    assert fallback_read["disposition"] == "fallback"
-    assert fallback_read["sourcePriority"] == "备用来源"
-    assert fallback_read["usageRatio"]["calculable"] is False
-    assert any(
-        item["consumer"] == "inspector"
-        and item["fieldPath"] == "technical.branch"
-        and item["transformation"] == "完整记录读取"
-        for item in reads
-    )
-    assert any(
-        context["kind"] == "event-record"
-        and context["content"]["eventBody"]["output_content"] == "完整运行返回"
-        and isinstance(context["usedFragments"], list)
-        for context in payload["contexts"]
-    )
-    assert not any(item["label"] == "table" for item in reads)
-    assert any(
-        context["kind"] == "function-result"
-        and context["source"]["sourceKind"] == "function"
-        for context in payload["contexts"]
-    )
-    assert any(
-        context["kind"] == "log-module"
-        and context["source"]["sourceKind"] == "log"
-        and context["relatedSourceKey"] == "database:event:50"
-        for context in payload["contexts"]
-    )
-    assert any(context["kind"] == "log-module" for context in payload["contexts"])
-
-
-def test_audit_routes_serve_page_assets_and_full_stream(monkeypatch, current_bundle):
-    module_audit_router = import_module("app.module_audit.router")
-    audit_service = _service(current_bundle)
-    monkeypatch.setattr(module_audit_router, "service", audit_service)
-    client = TestClient(main.app)
-
-    page = client.get("/audit/88001")
-    assert page.status_code == 200
-    assert 'data-run-id="88001"' in page.text
-    assert 'id="module-jump"' in page.text
-    assert "只看使用字段" in page.text
-    assert 'id="source-filter"' in page.text
-    assert client.get("/audit-assets/styles.css").status_code == 200
-    assert client.get("/audit-assets/app.js").status_code == 200
-
-    index = client.get("/api/script-builds/88001/module-audit")
-    assert index.status_code == 200
-    assert "eventBody" not in index.text
-
-    detail = client.get(
-        "/api/script-builds/88001/module-audit/round%3A1%3Abranch%3A1%3Atask"
-    )
-    assert detail.status_code == 200
-    assert detail.json()["id"] == "round:1:branch:1:task"
-
-    stream = client.get("/api/script-builds/88001/module-audit-stream")
-    assert stream.status_code == 200
-    assert stream.headers["content-type"].startswith("application/x-ndjson")
-    assert '"type":"index"' in stream.text
-    assert '"type":"detail"' in stream.text
-    assert '"type":"complete"' in stream.text
-
-
-def test_audit_page_collapses_large_blocks_without_truncating_source():
-    script = (
-        main.__file__.replace("main.py", "module_audit/static/app.js")
-    )
-    with open(script, encoding="utf-8") as handle:
-        source = handle.read()
-
-    assert "count > 800" in source
-    assert "slice(0, length)" in source
-    assert "jsonText(value)" in source
-    assert 'contentBox("技术详情"' in source
-    assert 'contentBox("业务详情"' in source
-    assert 'class="businessPart${' in source
-    assert 'class="correspondenceRow"' in source
-    assert "contextsForGroups" in source
-    assert "correlationMark" in source
-    assert "setCorrelationFocus" in source
-    assert "businessSegments" in source
-    assert "cardSegments" in source
-    assert "populateModuleJump" in source
-    assert "jumpToModule" in source
-    assert "scrollIntoView" in source
-    assert "renderCompactReads" in source
-    assert "renderCompactContexts" in source
-    assert "实际读取值" in source
-    assert "原始字段值" in source
-    assert "原始记录位置" in source
-    assert "定位原始字段" in source
-    assert "data-locate-read-id" in source
-    assert "data-read-ids" in source
-    assert "module-audit-stream" in source
-    assert "requestAnimationFrame" in source
-    assert 'get("module")' in source
-    assert "data-load-module" not in source
-    assert "IntersectionObserver" not in source
-    assert "Inspector 业务详情 ·" not in source
-    assert "Inspector 读取 · table" not in source
-    assert "来源投影" not in source
-    assert "数据库:" in source and "函数:" in source and "日志:" in source

+ 0 - 126
visualization/backend/tests/test_repository_rules.py

@@ -1,126 +0,0 @@
-import inspect
-from types import SimpleNamespace
-
-from app.repositories import ScriptBuildRepository
-
-
-class Row:
-    def __init__(self, **values):
-        self.__dict__.update(values)
-        self.__table__ = SimpleNamespace(
-            columns=[SimpleNamespace(name=name) for name in values]
-        )
-
-
-def test_final_snapshot_filters_branch_zero_and_overlay_replaces_base_rows():
-    repository = ScriptBuildRepository()
-    paragraphs = [
-        Row(id=1, branch_id=0, is_active=True, base_ref_id=None, content="base"),
-        Row(id=2, branch_id=7, is_active=True, base_ref_id=1, content="overlay"),
-        Row(id=3, branch_id=9, is_active=True, base_ref_id=None, content="other"),
-    ]
-    elements = [
-        Row(id=10, branch_id=0, is_active=True, base_ref_id=None, content="base-element"),
-        Row(id=11, branch_id=7, is_active=True, base_ref_id=10, content="overlay-element"),
-    ]
-    links = [
-        Row(id=20, branch_id=0, paragraph_id=1, element_id=10),
-        Row(id=21, branch_id=7, paragraph_id=2, element_id=11),
-    ]
-
-    final_snapshot = repository._snapshot_for_branch(0, paragraphs, elements, links)
-    assert {row["branch_id"] for row in final_snapshot["paragraphs"]} == {0}
-    assert {row["branch_id"] for row in final_snapshot["elements"]} == {0}
-    assert {row["branch_id"] for row in final_snapshot["paragraphElements"]} == {0}
-
-    overlay = repository._snapshot_for_branch(7, paragraphs, elements, links)
-    assert [row["content"] for row in overlay["paragraphs"]] == ["overlay"]
-    assert [row["content"] for row in overlay["elements"]] == ["overlay-element"]
-
-
-def test_candidate_snapshot_uses_saved_content_for_terminal_branch_and_overlay_for_park():
-    repository = ScriptBuildRepository()
-    merged = Row(status="merged", branch_id=1, content_snapshot={"paragraphs": [{"id": "saved"}]})
-    parked = Row(status="parked", branch_id=2, content_snapshot=None)
-
-    saved = repository._candidate_snapshot(merged, paragraphs=[], elements=[], links=[])
-    assert saved["snapshot"]["paragraphs"][0]["id"] == "saved"
-    assert saved["historicalAccuracy"] == "exact"
-    assert saved["snapshotKind"] == "saved-at-disposition"
-
-    overlay = repository._candidate_snapshot(parked, paragraphs=[], elements=[], links=[])
-    assert overlay["sourceOrigin"] == "database"
-    assert overlay["historicalAccuracy"] == "unknown"
-    assert overlay["currentProjectionAccuracy"] == "exact"
-    assert "不是暂存时的历史快照" in overlay["note"]
-
-
-def test_terminal_branch_without_snapshot_is_not_reconstructed_from_current_base():
-    repository = ScriptBuildRepository()
-    merged = Row(status="merged", branch_id=7, content_snapshot=None)
-    result = repository._candidate_snapshot(merged, paragraphs=[], elements=[], links=[])
-    assert result["snapshot"] is None
-    assert result["historicalAccuracy"] == "missing"
-    assert result["currentProjectionAccuracy"] == "not-applicable"
-    assert result["snapshotKind"] == "missing-disposition-snapshot"
-
-
-def test_base_links_require_both_active_endpoints_and_override_has_effective_id():
-    repository = ScriptBuildRepository()
-    paragraphs = [
-        Row(id=1, branch_id=0, is_active=True, base_ref_id=None),
-        Row(id=2, branch_id=0, is_active=False, base_ref_id=None),
-        Row(id=3, branch_id=7, is_active=True, base_ref_id=1),
-    ]
-    elements = [Row(id=10, branch_id=0, is_active=True, base_ref_id=None)]
-    links = [
-        Row(id=20, branch_id=0, paragraph_id=1, element_id=10),
-        Row(id=21, branch_id=0, paragraph_id=2, element_id=10),
-    ]
-    base = repository._snapshot_for_branch(0, paragraphs, elements, links)
-    assert [row["id"] for row in base["paragraphElements"]] == [20]
-    overlay = repository._snapshot_for_branch(7, paragraphs, elements, links)
-    assert overlay["paragraphs"][0]["rowId"] == 3
-    assert overlay["paragraphs"][0]["effectiveId"] == 1
-
-
-def test_repository_implementation_contains_no_write_or_commit_calls():
-    source = inspect.getsource(ScriptBuildRepository)
-    for forbidden in (".commit(", ".flush(", ".add(", ".delete(", ".execute("):
-        assert forbidden not in source
-
-
-def test_latest_split_event_body_fields_are_parsed_without_old_schema():
-    repository = ScriptBuildRepository()
-    body = Row(
-        id=1,
-        event_id=9,
-        input_content_type="json",
-        input_content='{"task": "查找案例"}',
-        output_content_type="json",
-        output_content='{"summary": "初筛完成"}',
-    )
-    parsed = repository._event_body(body)
-    assert parsed["input"]["content"] == {"task": "查找案例"}
-    assert parsed["output"]["content"] == {"summary": "初筛完成"}
-    source = inspect.getsource(ScriptBuildRepository._event_body)
-    assert "input_body_id" not in source
-    assert "output_body_id" not in source
-
-
-def test_oversized_event_body_is_safely_bounded_and_reports_omitted_characters():
-    raw = '{"content":"' + ("x" * 250_000) + '"}'
-    parsed = ScriptBuildRepository._body_side("json", raw)
-
-    assert parsed["truncated"] is True
-    assert parsed["omittedCharacters"] == len(raw) - 240_000
-    assert "[truncated" in parsed["content"]
-    assert len(parsed["content"]) < len(raw)
-
-
-def test_main_view_reads_bodies_only_for_retrieval_evaluation_and_creative_agents():
-    repository = ScriptBuildRepository()
-    assert repository._needs_light_agent_body({"event_type": "agent_invoke", "event_name": "retrieve_data_knowledge"})
-    assert repository._needs_light_agent_body({"event_type": "agent_invoke", "event_name": "script_multipath_evaluator"})
-    assert repository._needs_light_agent_body({"event_type": "agent_invoke", "event_name": "script_implementer"})
-    assert not repository._needs_light_agent_body({"event_type": "tool_call", "event_name": "retrieve_data_knowledge"})

+ 0 - 159
visualization/backend/tests/test_retrieval_detail_projection.py

@@ -1,159 +0,0 @@
-from app.retrieval_detail_projection import (
-    project_retrieval_event,
-    summarize_retrieval_output,
-)
-
-
-def _event(event_id, name, input_value, output_value, **overrides):
-    event = {
-        "id": event_id,
-        "event_type": "tool_call",
-        "event_name": name,
-        "status": "ok",
-        "ended_at": "2026-07-14T20:00:00",
-        "input": {"contentType": "json", "content": input_value},
-        "output": {"contentType": "json", "content": output_value},
-    }
-    event.update(overrides)
-    return event
-
-
-def test_case_hit_preserves_conditions_and_complete_case_content():
-    detail = project_retrieval_event(
-        _event(
-            4607,
-            "search_script_decode_case",
-            {"keyword": "毛驴效应", "top_k": 3, "return_field": "主脉络"},
-            {
-                "success": True,
-                "count": 2,
-                "results": [
-                    {
-                        "post_id": "case-a",
-                        "account": "每天心理学",
-                        "score": 0.7032,
-                        "return_field": "主脉络",
-                        "data": [{"id": "段落1", "详细描述": "完整解构内容"}],
-                    },
-                    {
-                        "post_id": "case-b",
-                        "account": "每天心理学",
-                        "score": 0.68,
-                        "return_field": "主脉络",
-                        "data": [{"id": "段落1", "详细描述": "第二条"}],
-                    },
-                ],
-            },
-        )
-    )
-
-    assert detail["detailKind"] == "retrieval-event"
-    assert detail["sourceKind"] == "agent-query"
-    assert detail["outcome"] == {
-        "state": "hit",
-        "count": 2,
-        "message": "查询成功,返回 2 条结果。",
-    }
-    assert [item["title"] for item in detail["items"]] == [
-        "每天心理学 · Case 1",
-        "每天心理学 · Case 2",
-    ]
-    assert detail["items"][0]["sections"][0]["value"][0]["详细描述"] == "完整解构内容"
-    assert [field["label"] for field in detail["queryConditions"]] == ["关键词", "最多返回", "返回内容"]
-
-
-def test_successful_empty_is_not_a_failure_and_keeps_query_conditions():
-    detail = project_retrieval_event(
-        _event(
-            4603,
-            "search_script_decode_case",
-            {"keyword": "毛驴效应", "account_name": "每天一点心理学", "top_k": 3},
-            {"success": True, "count": 0, "results": []},
-        )
-    )
-
-    assert detail["outcome"] == {
-        "state": "empty",
-        "count": 0,
-        "message": "查询执行成功,没有匹配数据。",
-    }
-    assert detail["items"] == []
-    assert {item["label"] for item in detail["queryConditions"]} == {"关键词", "账号", "最多返回"}
-
-
-def test_text_failure_exposes_readable_parameter_error():
-    detail = project_retrieval_event(
-        _event(
-            4066,
-            "search_script_decode_case",
-            {"return_field": "写法"},
-            "错误: return_field 仅支持 ['主脉络', '主题']",
-            status="error",
-        )
-    )
-
-    assert detail["outcome"]["state"] == "failure"
-    assert detail["outcome"]["errorKind"] == "parameter"
-    assert "return_field 仅支持" in detail["outcome"]["message"]
-
-
-def test_knowledge_list_count_and_content_are_projected():
-    detail = project_retrieval_event(
-        _event(
-            4574,
-            "search_knowledge",
-            {"keyword": "决策瘫痪", "max_count": 3},
-            [
-                {"id": 1, "title": "知识 A", "purpose": "用于核对", "content": "正文 A"},
-                {"id": 2, "title": "知识 B", "purpose": "用于行动建议", "content": "正文 B"},
-            ],
-        )
-    )
-
-    assert detail["outcome"]["count"] == 2
-    assert detail["outcome"]["state"] == "hit"
-    assert detail["items"][1]["sections"] == [{"title": "完整内容", "value": "正文 B"}]
-
-
-def test_direct_tool_has_direct_source_and_structured_output():
-    detail = project_retrieval_event(
-        _event(
-            4650,
-            "get_script_snapshot",
-            {"script_build_id": 444},
-            {"script_build": {"id": 444}, "paragraphs": [{"id": 2284}], "elements": []},
-        )
-    )
-
-    assert detail["sourceKind"] == "direct-tool"
-    assert detail["outcome"]["state"] == "hit"
-    assert [section["title"] for section in detail["items"][0]["sections"]] == ["脚本概况", "段落"]
-
-
-def test_terminal_run_marks_unfinished_event_interrupted_before_other_rules():
-    detail = project_retrieval_event(
-        _event(
-            5000,
-            "search_knowledge",
-            {"keyword": "决策"},
-            [{"title": "还未完成的结果"}],
-            status="running",
-            ended_at=None,
-        ),
-        run_status="failed",
-    )
-
-    assert detail["outcome"]["state"] == "interrupted"
-    assert detail["items"] == []
-
-
-def test_light_summary_does_not_include_full_results():
-    summary = summarize_retrieval_output(
-        "search_script_decode_case",
-        raw_status="ok",
-        ended_at="2026-07-14T20:00:00",
-        output={"success": True, "count": 1, "results": [{"data": "x" * 1000}]},
-    )
-
-    assert summary == {"state": "hit", "count": 1, "error": None}
-    assert "results" not in summary

+ 0 - 75
visualization/backend/tests/test_runtime_bridge.py

@@ -1,75 +0,0 @@
-from types import SimpleNamespace
-
-import pytest
-from sqlalchemy import create_engine
-
-from app import runtime_bridge
-
-
-def test_database_host_override_is_host_only(monkeypatch):
-    monkeypatch.setenv("PATTERN_DB_HOST_OVERRIDE", "192.168.202.204")
-    assert runtime_bridge.database_host_override() == "192.168.202.204"
-
-    for invalid in ("mysql://host", "host:3306", "user@host", "host name"):
-        monkeypatch.setenv("PATTERN_DB_HOST_OVERRIDE", invalid)
-        with pytest.raises(ValueError):
-            runtime_bridge.database_host_override()
-
-
-def test_override_factory_replaces_only_host_and_keeps_parent_database(monkeypatch):
-    class FakeDatabaseManager:
-        def __init__(self):
-            self.engine = create_engine("mysql+pymysql://reader:secret@old-host:3306/open_aigc_pattern")
-
-    monkeypatch.setattr(
-        runtime_bridge,
-        "load_runtime_modules",
-        lambda: (SimpleNamespace(DatabaseManager=FakeDatabaseManager), SimpleNamespace()),
-    )
-    runtime_bridge._read_only_session_factory.cache_clear()
-    factory = runtime_bridge._read_only_session_factory("/runtime", "192.168.202.204")
-    engine = factory.kw["bind"]
-    try:
-        assert engine.url.host == "192.168.202.204"
-        assert engine.url.port == 3306
-        assert engine.url.database == "open_aigc_pattern"
-        assert engine.url.username == "reader"
-        assert engine.url.password == "secret"
-    finally:
-        engine.dispose()
-        runtime_bridge._read_only_session_factory.cache_clear()
-
-
-def test_read_only_factory_keeps_original_host_without_override(monkeypatch):
-    class FakeDatabaseManager:
-        def __init__(self):
-            self.engine = create_engine("mysql+pymysql://reader:secret@db-host:3306/open_aigc_pattern")
-
-    monkeypatch.setattr(
-        runtime_bridge,
-        "load_runtime_modules",
-        lambda: (SimpleNamespace(DatabaseManager=FakeDatabaseManager), SimpleNamespace()),
-    )
-    runtime_bridge._read_only_session_factory.cache_clear()
-    factory = runtime_bridge._read_only_session_factory("/runtime", "")
-    engine = factory.kw["bind"]
-    try:
-        assert engine.url.host == "db-host"
-        assert engine.url.database == "open_aigc_pattern"
-    finally:
-        engine.dispose()
-        runtime_bridge._read_only_session_factory.cache_clear()
-
-
-def test_read_only_hook_executes_only_session_guard():
-    statements = []
-
-    class Cursor:
-        def execute(self, statement):
-            statements.append(statement)
-
-        def close(self):
-            statements.append("closed")
-
-    runtime_bridge._set_mysql_session_read_only(SimpleNamespace(cursor=lambda: Cursor()), None)
-    assert statements == ["SET SESSION TRANSACTION READ ONLY", "closed"]

+ 0 - 401
visualization/backend/tests/test_runtime_event_projection.py

@@ -1,401 +0,0 @@
-from copy import deepcopy
-from datetime import datetime, timezone
-
-from app.runtime_event_projection import RuntimeEventProjector
-
-
-def _implementer_events():
-    return [
-        {"id": 10, "event_seq": 1, "event_type": "agent_invoke", "event_name": "script_implementer", "round_index": 1, "branch_id": 7, "status": "ok", "started_at": "2026-07-13T10:00:00", "ended_at": "2026-07-13T10:02:00"},
-        {"id": 11, "event_seq": 2, "event_type": "tool_call", "event_name": "get_topic_detail", "agent_role": "script_implementer", "scope_event_id": 10, "round_index": 1, "branch_id": 7, "status": "ok", "started_at": "2026-07-13T10:00:01", "ended_at": "2026-07-13T10:00:02"},
-        {"id": 20, "event_seq": 3, "event_type": "agent_invoke", "event_name": "retrieve_data_decode_case", "parent_event_id": 10, "scope_event_id": 10, "round_index": 1, "branch_id": 7, "status": "ok", "inputData": {"task": "查找叙事案例"}, "agentOutputData": {"summary": "初筛出两个案例"}, "started_at": "2026-07-13T10:00:10", "ended_at": "2026-07-13T10:00:30"},
-        {"id": 21, "event_seq": 4, "event_type": "tool_call", "event_name": "think_and_plan", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "started_at": "2026-07-13T10:00:11", "ended_at": "2026-07-13T10:00:12"},
-        {"id": 22, "event_seq": 5, "event_type": "tool_call", "event_name": "search_script_decode_case", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "inputData": {"keyword": "叙事"}, "output_preview": '{"count": 2}', "started_at": "2026-07-13T10:00:13", "ended_at": "2026-07-13T10:00:17"},
-        {"id": 23, "event_seq": 6, "event_type": "tool_call", "event_name": "search_script_decode_case", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "inputData": {"keyword": "构图"}, "output_preview": '{"count": 0}', "started_at": "2026-07-13T10:00:18", "ended_at": "2026-07-13T10:00:22"},
-    ]
-
-
-def test_explicit_event_tree_separates_direct_tools_agent_queries_and_screening():
-    projection = RuntimeEventProjector().project(_implementer_events(), valid_rounds={1}, valid_branches={7})
-    stage = projection["retrievalStagesByBranch"][7]
-
-    assert stage["observedMode"] == "sequential"
-    assert stage["directToolGroups"][0]["sources"] == [{"businessLabel": "选题", "callCount": 1}]
-    assert len(stage["agentRuns"]) == 1
-    run = stage["agentRuns"][0]
-    assert run["id"] == "retrieval-agent:20"
-    assert run["taskPreview"] == "查找叙事案例"
-    assert run["screening"]["preview"] == "初筛出两个案例"
-    assert run["querySummary"] == {"total": 2, "hit": 1, "empty": 1, "failure": 0, "unknown": 0}
-    assert [item["eventId"] for item in run["attempts"]] == [22, 23]
-
-
-def test_same_agent_type_invoked_twice_stays_two_instances():
-    events = _implementer_events()
-    second = deepcopy(events[2])
-    second.update({"id": 30, "event_seq": 7, "started_at": "2026-07-13T10:00:40", "ended_at": "2026-07-13T10:00:50"})
-    events.append(second)
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
-    assert [item["id"] for item in projection["retrievalStagesByBranch"][7]["agentRuns"]] == ["retrieval-agent:20", "retrieval-agent:30"]
-
-
-def test_missing_parent_is_unassigned_instead_of_text_matched():
-    event = {"id": 3, "event_type": "agent_invoke", "event_name": "retrieve_data_knowledge", "round_index": 1, "branch_id": 7, "inputData": {"task": "文字里写了方案 7"}}
-    projection = RuntimeEventProjector().project([event], valid_rounds={1}, valid_branches={7})
-    assert projection["retrievalStagesByBranch"] == {}
-    assert projection["unassigned"][0]["eventId"] == 3
-    assert projection["unassigned"][0]["association"] == "missing-parent-implementer"
-
-
-def test_parallel_and_mixed_modes_come_from_time_overlap():
-    events = _implementer_events()
-    first_agent = events[2]
-    direct = events[1]
-    direct["ended_at"] = "2026-07-13T10:00:20"
-    first_agent["started_at"] = "2026-07-13T10:00:10"
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
-    assert projection["retrievalStagesByBranch"][7]["observedMode"] == "parallel"
-
-    later = deepcopy(first_agent)
-    later.update({"id": 40, "event_seq": 9, "started_at": "2026-07-13T10:00:40", "ended_at": "2026-07-13T10:00:50"})
-    events.append(later)
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
-    assert projection["retrievalStagesByBranch"][7]["observedMode"] == "mixed"
-
-
-def test_unknown_time_does_not_claim_parallelism():
-    events = _implementer_events()
-    events[2]["started_at"] = None
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
-    stage = projection["retrievalStagesByBranch"][7]
-    assert stage["observedMode"] == "unknown"
-    assert stage["waves"] == []
-
-
-def test_failed_direct_read_makes_stage_partial():
-    events = _implementer_events()
-    events[1]["status"] = "error"
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
-    stage = projection["retrievalStagesByBranch"][7]
-    assert stage["directToolGroups"][0]["failureCount"] == 1
-    assert stage["status"] == "partial"
-
-
-def test_running_implementer_without_retrieval_calls_is_preparing_not_missing():
-    events = [{
-        "id": 10, "event_seq": 1, "event_type": "agent_invoke",
-        "event_name": "script_implementer", "round_index": 1,
-        "branch_id": 7, "status": "running",
-        "started_at": "2026-07-14T12:00:00", "ended_at": None,
-    }]
-    projection = RuntimeEventProjector().project(
-        events,
-        valid_rounds={1},
-        valid_branches={7},
-        captured_at=datetime(2026, 7, 14, 4, 0, 5, tzinfo=timezone.utc),
-    )
-    stage = projection["retrievalStagesByBranch"][7]
-    assert stage["status"] == "running"
-    assert stage["observedMode"] == "unknown"
-
-
-def test_terminal_run_reclassifies_unfinished_retrieval_as_interrupted():
-    events = _implementer_events()
-    events[0].update({"status": "running", "ended_at": None})
-    events[1].update({"status": "running", "ended_at": None})
-    events[2].update({"status": "running", "ended_at": None})
-    events[4].update({"status": "running", "ended_at": None})
-    projection = RuntimeEventProjector().project(
-        events,
-        valid_rounds={1},
-        valid_branches={7},
-        run_status="failed",
-    )
-    stage = projection["retrievalStagesByBranch"][7]
-
-    assert stage["status"] == "partial"
-    assert stage["directToolGroups"][0]["calls"][0]["status"] == "interrupted"
-    assert stage["agentRuns"][0]["status"] == "interrupted"
-    assert stage["agentRuns"][0]["attempts"][0]["status"] == "interrupted"
-
-
-def test_running_naive_database_time_uses_asia_shanghai_before_wave_projection():
-    events = _implementer_events()
-    events[0].update({"status": "running", "ended_at": None})
-    events[1].update({"status": "running", "started_at": "2026-07-14T12:00:00", "ended_at": None})
-    events[2].update({"status": "running", "started_at": "2026-07-14T12:00:02", "ended_at": None})
-    projection = RuntimeEventProjector().project(
-        events,
-        valid_rounds={1},
-        valid_branches={7},
-        captured_at=datetime(2026, 7, 14, 4, 0, 5, tzinfo=timezone.utc),
-    )
-    stage = projection["retrievalStagesByBranch"][7]
-    assert stage["observedMode"] == "parallel"
-    assert stage["startedAt"] == "2026-07-14T04:00:00+00:00"
-    assert stage["endedAt"] == "2026-07-14T04:00:05+00:00"
-
-
-def test_query_output_error_precedes_empty_count_and_unstructured_object_is_unknown():
-    events = _implementer_events()
-    events[4]["output_preview"] = '{"error":"knowledge unavailable","items":[]}'
-    events[5]["output_preview"] = '{"message":"query accepted"}'
-    stage = RuntimeEventProjector().project(
-        events, valid_rounds={1}, valid_branches={7}
-    )["retrievalStagesByBranch"][7]
-    assert [attempt["status"] for attempt in stage["agentRuns"][0]["attempts"]] == ["failure", "unknown"]
-    assert stage["agentRuns"][0]["querySummary"] == {
-        "total": 2, "hit": 0, "empty": 0, "failure": 1, "unknown": 1,
-    }
-
-
-def test_agent_card_screening_preview_uses_business_cleaning():
-    events = _implementer_events()
-    events[2]["agentOutputData"] = {
-        "summary": "## 委托任务完成\n\n### 初筛结果\n- 找到可用案例 post_id=abc123\n\n**执行统计**:\n- Tokens: 1200\n- 成本: $0.1"
-    }
-    stage = RuntimeEventProjector().project(
-        events, valid_rounds={1}, valid_branches={7}
-    )["retrievalStagesByBranch"][7]
-    preview = stage["agentRuns"][0]["screening"]["preview"]
-    assert "可用案例" in preview
-    assert "post_id" not in preview
-    assert "abc123" not in preview
-    assert "Tokens" not in preview
-    assert "成本" not in preview
-
-
-def test_creative_business_projection_translates_runtime_tool_names():
-    events = [
-        {
-            "id": 10,
-            "event_seq": 1,
-            "event_type": "agent_invoke",
-            "event_name": "script_implementer",
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-            "inputData": {
-                "task": "路序号:1 action:增 target:烹饪贴士 path_type:内容 目标:补齐烹饪贴士与总结。"
-            },
-        },
-        {
-            "id": 11,
-            "event_seq": 2,
-            "event_type": "tool_call",
-            "event_name": "think_and_plan",
-            "agent_role": "script_implementer",
-            "scope_event_id": 10,
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-            "inputData": {
-                "thought": "缺少直接证据,先保留为待验证推导。",
-                "action": "record_data_decision",
-                "plan": "1. 调用 get_topic_detail。\n2. 委派 retrieve_data_knowledge。\n3. 调用 batch_update_script_paragraphs。",
-            },
-        },
-    ]
-    decision = RuntimeEventProjector().project(
-        events, valid_rounds={1}, valid_branches={7}
-    )["creativeByBranch"][7]
-
-    assert decision["body"]["task"] == "补齐烹饪贴士与总结。"
-    assert decision["body"]["actions"] == ["记录数据取舍"]
-    assert decision["body"]["steps"] == [
-        {
-            "stepIndex": 1,
-            "action": "记录数据取舍",
-            "summary": None,
-            "plan": "1. 读取选题。\n2. 委派知识 Agent取数。\n3. 更新候选脚本内容。",
-            "reasoning": "缺少直接证据,先保留为待验证推导。",
-            "eventRef": "event:11",
-        }
-    ]
-    process_block = decision["detail"]["blocks"][0]
-    assert process_block["type"] == "creative-process"
-    assert process_block["steps"][0]["action"] == "记录数据取舍"
-    business = str(decision["detail"]["blocks"])
-    assert "record_data_decision" not in business
-    assert "get_topic_detail" not in business
-    assert "batch_update_script_paragraphs" not in business
-    assert "path_type" not in business
-
-
-def test_creative_projection_keeps_upstream_and_actual_mutation_refs_separate():
-    events = [
-        {
-            "id": 100,
-            "event_seq": 1,
-            "event_type": "agent_invoke",
-            "event_name": "script_implementer",
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-            "inputData": {"task": "目标:补齐正文。"},
-        },
-        {
-            "id": 101,
-            "event_seq": 2,
-            "event_type": "tool_call",
-            "event_name": "get_script_snapshot",
-            "agent_role": "script_implementer",
-            "scope_event_id": 100,
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-        },
-        {
-            "id": 102,
-            "event_seq": 3,
-            "event_type": "tool_call",
-            "event_name": "think_and_plan",
-            "agent_role": "script_implementer",
-            "scope_event_id": 100,
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-            "inputData": {"action": "更新候选正文", "thought": "先根据已有结构补写。"},
-        },
-        {
-            "id": 103,
-            "event_seq": 4,
-            "event_type": "tool_call",
-            "event_name": "batch_update_script_paragraphs",
-            "agent_role": "script_implementer",
-            "scope_event_id": 100,
-            "round_index": 1,
-            "branch_id": 7,
-            "status": "ok",
-        },
-    ]
-
-    decision = RuntimeEventProjector().project(
-        events, valid_rounds={1}, valid_branches={7}
-    )["creativeByBranch"][7]
-
-    assert decision["eventRefs"] == ["event:101", "event:102", "event:103"]
-    assert decision["body"]["availableUpstream"][0]["decisionUse"] == "not-recorded"
-    assert decision["body"]["runtimeActions"][0]["label"] == "更新候选脚本内容"
-    blocks = {item["title"]: item for item in decision["detail"]["blocks"]}
-    upstream_item = blocks["可确认的上游信息"]["items"][0]
-    assert "无法确认是否被本次创作采用" in upstream_item["note"]
-    assert blocks["实际脚本改动"]["items"][0]["detailRef"] == "event:103"
-
-
-def test_exact_evaluator_names_create_two_different_business_stages():
-    events = [
-        {
-            "id": 80, "event_seq": 1, "event_type": "agent_invoke",
-            "event_name": "script_multipath_evaluator", "round_index": 1,
-            "status": "ok", "inputData": {"task": "比较 branch_id=7, branch_id=8"},
-            "agentOutputData": {"summary": "### 分支评估 branch_id=7\n**该支结论**:通过\n**目标推进**:结构完整。\n\n### 分支评估 branch_id=8\n**该支结论**:部分通过\n**目标推进**:仍需补充。\n\n### 对比与建议\n**采纳建议**:优先采用 branch_id=7。"},
-            "started_at": "2026-07-13T10:03:00", "ended_at": "2026-07-13T10:03:10",
-        },
-        {
-            "id": 81, "event_seq": 2, "event_type": "agent_invoke",
-            "event_name": "script_evaluator", "round_index": 1,
-            "status": "ok", "agentOutputData": {"summary": "### 整体结论:部分通过"},
-            "started_at": "2026-07-13T10:04:00", "ended_at": "2026-07-13T10:04:10",
-        },
-        {
-            "id": 82, "event_seq": 3, "event_type": "agent_invoke",
-            "event_name": "some_future_evaluator", "round_index": 1,
-            "status": "ok", "started_at": "2026-07-13T10:05:00", "ended_at": "2026-07-13T10:05:10",
-        },
-    ]
-    projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7, 8})
-
-    review = projection["multipathReviewsByRound"][1][0]
-    assert review["branchIds"] == [7, 8]
-    assert [item["conclusion"] for item in review["branchConclusions"]] == ["通过", "部分通过"]
-    assert "优先采用" in review["recommendation"]
-    assert projection["overallEvaluationsByRound"][1][0]["eventId"] == 81
-    assert "evaluationsByBranch" not in projection
-    assert "evaluationsByRound" not in projection
-
-
-def test_multipath_evaluator_parses_one_labelled_comma_separated_branch_list():
-    event = {
-        "id": 3687,
-        "event_seq": 1,
-        "event_type": "agent_invoke",
-        "event_name": "script_multipath_evaluator",
-        "round_index": 2,
-        "status": "ok",
-        "inputData": {
-            "task": "评估对象:branch_id = 4, 5, 6, 7(分工,各填充一段)"
-        },
-        "agentOutputData": {
-            "summary": "\n".join(
-                f"### 分支评估 branch_id={branch_id}\n**该支结论**:通过"
-                for branch_id in (4, 5, 6, 7)
-            )
-        },
-        "started_at": "2026-07-14T17:17:09",
-        "ended_at": "2026-07-14T17:18:06",
-    }
-
-    projection = RuntimeEventProjector().project(
-        [event], valid_rounds={2}, valid_branches={4, 5, 6, 7}
-    )
-
-    assert projection["multipathReviewsByRound"][2][0]["branchIds"] == [4, 5, 6, 7]
-
-
-def test_facade_builds_one_index_and_merges_projector_outputs():
-    received = []
-
-    class RetrievalSpy:
-        def project(self, index, **kwargs):
-            received.append(index)
-            return {
-                "retrievalStagesByBranch": {7: {"id": "retrieval"}},
-                "unassigned": [{"id": "retrieval-unassigned"}],
-            }
-
-    class EvaluationSpy:
-        def project(self, index, **kwargs):
-            received.append(index)
-            return {
-                "multipathReviewsByRound": {1: [{"id": "review"}]},
-                "overallEvaluationsByRound": {1: [{"id": "evaluation"}]},
-                "unassigned": [{"id": "evaluation-unassigned"}],
-            }
-
-    class MainAgentSpy:
-        def project(self, index, **kwargs):
-            received.append(index)
-            return {
-                "objective": {"stage": "objective"},
-                "roundGoalsByRound": {},
-                "implementationPlansByRound": {},
-            }
-
-    projection = RuntimeEventProjector(
-        retrieval_projector=RetrievalSpy(),
-        evaluation_projector=EvaluationSpy(),
-        main_agent_projector=MainAgentSpy(),
-    ).project(
-        [{
-            "id": 9,
-            "event_seq": 1,
-            "event_type": "tool_call",
-            "event_name": "record_multipath_plan",
-            "round_index": 1,
-            "status": "ok",
-        }],
-        valid_rounds={1},
-        valid_branches={7},
-    )
-
-    assert len({id(index) for index in received}) == 1
-    assert projection["retrievalStagesByBranch"][7]["id"] == "retrieval"
-    assert projection["multipathReviewsByRound"][1][0]["id"] == "review"
-    assert projection["overallEvaluationsByRound"][1][0]["id"] == "evaluation"
-    assert projection["mainAgentDecisions"]["objective"]["stage"] == "objective"
-    assert "planRevisionsByRound" not in projection
-    assert projection["unassigned"] == [
-        {"id": "retrieval-unassigned"},
-        {"id": "evaluation-unassigned"},
-    ]

+ 0 - 27
visualization/backend/tests/test_sanitizer.py

@@ -1,27 +0,0 @@
-from app.sanitizer import sanitize
-
-
-def test_sensitive_mapping_values_are_redacted():
-    payload = sanitize({
-        "api_key": "sk-secret",
-        "nested": {"Authorization": "Bearer token"},
-        "data_source_url": "postgresql://user:pass@db/name",
-        "safe": "ok",
-    })
-    assert payload == {
-        "api_key": "***",
-        "nested": {"Authorization": "***"},
-        "data_source_url": "***",
-        "safe": "ok",
-    }
-
-
-def test_connection_strings_are_redacted_inside_text():
-    assert sanitize("connect postgresql://user:pass@db/name") == "connect postgresql://user:***@db/name"
-
-
-def test_json_strings_and_authorization_schemes_are_fully_redacted():
-    assert sanitize('{"api_key":"SECRET","safe":"ok"}') == '{"api_key":"***","safe":"ok"}'
-    assert sanitize("Authorization: Bearer SECRET") == "Authorization: ***"
-    assert sanitize("Authorization=Basic dXNlcjpwYXNz") == "Authorization=***"
-    assert sanitize('{"access_token":"TOKEN"}') == '{"access_token":"***"}'

+ 0 - 134
visualization/backend/tests/test_script_table_projection.py

@@ -1,134 +0,0 @@
-from app.inspector_projection import project_artifact_detail
-
-
-def test_script_table_projection_normalizes_facets_links_and_element_references():
-    artifact = {
-        "sourceOrigin": "database",
-        "paragraphs": [
-            {
-                "id": 1,
-                "rowId": 1,
-                "effectiveId": 1,
-                "paragraph_index": 2,
-                "level": 1,
-                "name": "机制拆解",
-                "content_range": {"图片": ["1", "2"], "标题": True, "正文": "第二段"},
-                "theme": "通过[元素:10]解释机制",
-                "theme_elements": [{"维度类型": "主", "维度": "核心竞争力", "原子点": "份额换订单"}],
-                "form": "案例拆解",
-                "form_elements": [],
-                "function": "承接上文",
-                "function_elements": [],
-                "feeling": "建立可信度",
-                "feeling_elements": [],
-                "full_description": "通过[元素:10]解释机制,并保留[元素:999]。",
-            }
-        ],
-        "elements": [
-            {"id": 10, "rowId": 10, "effectiveId": 10, "name": "份额换订单", "dimension_primary": "核心竞争力", "dimension_secondary": "商业模式"}
-        ],
-        "paragraphElements": [{"id": 100, "paragraph_id": 1, "element_id": 10}],
-    }
-
-    payload = project_artifact_detail(431, "artifact:base:current", artifact)
-    table = payload["scriptTable"]
-
-    assert table["counts"] == {"paragraphs": 1, "elements": 1, "links": 1}
-    assert table["paragraphs"][0]["contentRange"] == ["图1-2", "标题", "正文: 第二段"]
-    assert table["paragraphs"][0]["sections"]["theme"]["dimensions"][0] == {
-        "type": "主",
-        "name": "核心竞争力",
-        "value": "份额换订单",
-    }
-    parts = table["paragraphs"][0]["fullDescriptionParts"]
-    assert parts[1] == {"type": "element", "id": 10, "name": "份额换订单", "resolved": True, "text": "[元素:10]"}
-    assert parts[3]["resolved"] is False
-    assert table["paragraphs"][0]["sections"]["theme"]["descriptionParts"][1] == {
-        "type": "element",
-        "id": 10,
-        "name": "份额换订单",
-        "resolved": True,
-        "text": "[元素:10]",
-    }
-    assert table["elements"][0]["paragraphs"] == [{"id": 1, "index": 2, "name": "机制拆解"}]
-    assert payload["changes"]["sections"][0]["content"] == "1 个段落、1 个元素、1 条关联"
-    assert "artifact" not in payload["technical"]
-
-
-def test_script_table_keeps_children_beside_their_real_parent():
-    def paragraph(row_id, index, name, parent_id=None, level=1):
-        return {
-            "id": row_id,
-            "rowId": row_id,
-            "effectiveId": row_id,
-            "paragraph_index": index,
-            "parent_id": parent_id,
-            "level": level,
-            "name": name,
-        }
-
-    artifact = {
-        "sourceOrigin": "database",
-        "paragraphs": [
-            paragraph(1, 1, "第一章"),
-            paragraph(2, 1, "第一章子段", parent_id=1, level=2),
-            paragraph(3, 2, "第二章"),
-            paragraph(4, 1, "第二章子段", parent_id=3, level=2),
-        ],
-        "elements": [],
-        "paragraphElements": [],
-    }
-
-    table = project_artifact_detail(1, "artifact:base:current", artifact)["scriptTable"]
-    assert [row["name"] for row in table["paragraphs"]] == [
-        "第一章",
-        "第一章子段",
-        "第二章",
-        "第二章子段",
-    ]
-    assert [row["depth"] for row in table["paragraphs"]] == [0, 1, 0, 1]
-    assert table["maxDepth"] == 2
-
-
-def test_frozen_candidate_snapshot_restores_nested_paragraphs_and_embedded_links():
-    artifact = {
-        "sourceOrigin": "database",
-        "historicalAccuracy": "exact",
-        "artifactContext": {"type": "candidate", "branchStatus": "merged"},
-        "snapshot": {
-            "stats": {"paragraph_count": 2, "element_count": 1, "link_count": 2},
-            "paragraphs": [
-                {
-                    "id": 1,
-                    "paragraph_index": 1,
-                    "level": 1,
-                    "name": "主段落",
-                    "linked_elements": [{"link_id": 10, "element_id": 100}],
-                    "sub_paragraphs": [
-                        {
-                            "id": 2,
-                            "paragraph_index": 1,
-                            "level": 2,
-                            "name": "子段落",
-                            "linked_elements": [],
-                            "linked_element_ids": [100],
-                        }
-                    ],
-                }
-            ],
-            "elements": [{"id": 100, "name": "候选元素"}],
-        },
-    }
-
-    payload = project_artifact_detail(1, "artifact:branch:7", artifact)
-    table = payload["scriptTable"]
-
-    assert table["counts"] == {"paragraphs": 2, "elements": 1, "links": 2}
-    assert [row["name"] for row in table["paragraphs"]] == ["主段落", "子段落"]
-    assert [row["depth"] for row in table["paragraphs"]] == [0, 1]
-    assert table["elements"][0]["paragraphs"] == [
-        {"id": 1, "index": 1, "name": "主段落"},
-        {"id": 2, "index": 1, "name": "子段落"},
-    ]
-    assert payload["businessSections"][0]["title"] == "候选脚本规模"
-    assert payload["changes"]["summary"] == "候选脚本内容"

+ 0 - 687
visualization/backend/tests/test_source_lineage.py

@@ -1,687 +0,0 @@
-from __future__ import annotations
-
-from copy import deepcopy
-
-from fastapi.testclient import TestClient
-import pytest
-
-from app import main
-from app.card_details.common import context_input_fields
-from app.module_audit.log_context import locate_log_module
-from app.source_lineage.comparison import _normalize_business, _source_module_ids
-from app.source_lineage.projector import _all_bindings
-from app.source_lineage.resolver import SourceResolver
-
-
-client = TestClient(main.app)
-
-
-@pytest.fixture(autouse=True)
-def clear_source_inspector_caches():
-    main._inspector_cache.clear()
-    main._build_view_cache.clear()
-    yield
-    main._inspector_cache.clear()
-    main._build_view_cache.clear()
-
-
-def _module(payload: dict, module_id: str) -> dict:
-    return next(item for item in payload["modules"] if item["id"] == module_id)
-
-
-def _row_bindings(module: dict, row_index: int = 0) -> list[dict]:
-    ids = set(module["rows"][row_index]["bindingIds"])
-    return [binding for binding in module["bindings"] if binding["id"] in ids]
-
-
-def _event_detail(summary: dict, *, input_content=None, output_content=None):
-    return {
-        **summary,
-        "input": {"contentType": "json", "content": input_content, "truncated": False},
-        "output": {"contentType": "json", "content": output_content, "truncated": False},
-    }
-
-
-def test_inspector_view_planning_fields_have_exact_independent_bindings(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    event = {
-        "id": 60,
-        "event_seq": 1,
-        "event_type": "tool_call",
-        "event_name": "think_and_plan",
-        "agent_role": "main",
-        "agent_depth": 0,
-        "status": "completed",
-        "started_at": "2026-07-13T09:59:00",
-        "ended_at": "2026-07-13T09:59:01",
-    }
-    bundle["events"].insert(0, event)
-    detail = _event_detail(
-        event,
-        input_content={
-            "thought_summary": "先确定承重结构",
-            "thought": "完整思考原文",
-            "plan": "1. 写入方向\n2. 开始本轮",
-            "action": "保存创作目标",
-        },
-        output_content={"ok": True},
-    )
-    calls = []
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(
-        main.provider,
-        "load_event_details",
-        lambda _build_id, ids: calls.append(list(ids)) or {60: deepcopy(detail)},
-    )
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
-    main._inspector_cache.clear()
-
-    response = client.get("/api/script-builds/88001/inspector-view/event:60")
-    assert response.status_code == 200
-    payload = response.json()
-    assert payload["schemaVersion"] == "inspector-source-v2"
-    assert payload["businessProjection"]["businessSections"][0]["content"] == "先确定承重结构"
-    assert calls == [[60]]
-    paths = {
-        module["id"]: _row_bindings(module)[0]["selector"].get("path")
-        for module in payload["modules"]
-    }
-    assert paths == {
-        "summary": "/input/content/thought_summary",
-        "thought": "/input/content/thought",
-        "plan": "/input/content/plan",
-        "action": "/input/content/action",
-    }
-    assert list(payload["sources"]) == ["event:60"]
-
-
-def test_optional_objective_and_creative_blocks_use_stable_semantic_ids():
-    objective = _normalize_business({
-        "blocks": [
-            {"type": "items", "title": "根据以下信息做出的决策", "items": [{"label": "账号", "value": "A"}]},
-            {"type": "summary", "title": "当前保存的创作方向", "value": "方向原文"},
-        ]
-    }, card_kind="objective")
-    assert [item["_semanticKey"] for item in objective] == [
-        "block:inputs",
-        "block:direction",
-    ]
-    assert _source_module_ids("objective", "block:inputs", 1) == ["inputs"]
-    assert _source_module_ids("objective", "block:direction", 2) == ["direction"]
-
-    creative = _normalize_business({
-        "blocks": [
-            {"type": "items", "title": "可确认的上游信息", "items": [{"label": "案例", "value": "A"}]},
-            {"type": "creative-process", "title": "创作处理", "steps": [{"action": "改写"}]},
-            {"type": "summary", "title": "产出的候选表", "value": "候选 A"},
-            {"type": "notice", "title": "记录说明", "value": "过程只保存了一部分"},
-        ]
-    }, card_kind="creative")
-    assert [item["_semanticKey"] for item in creative] == [
-        "block:available",
-        "block:process",
-        "block:candidate",
-        "block:record-note",
-    ]
-    assert [
-        _source_module_ids("creative", item["_semanticKey"], index)
-        for index, item in enumerate(creative, 1)
-        ] == [["available"], ["process"], ["candidate"], ["record-note"]]
-
-
-def test_context_input_fields_classifies_business_refs_as_database_records():
-    context = {
-        "inputs": [
-            {"label": "多路决策", "summary": "采用方案 1", "detailRef": "multipath-decision:21"},
-            {"label": "数据取舍", "summary": "采用两条证据", "detailRef": "data-decision:11"},
-            {"label": "领域事实", "summary": "事实 A", "detailRef": "domain-info:31"},
-            {"label": "开启本轮", "summary": "目标 A", "detailRef": "event:60", "relation": "direct-read"},
-        ]
-    }
-    event = {"id": 60, "event_name": "begin_round"}
-    fields = context_input_fields(context, "test", [event])
-    assert [item["source"]["kind"] for item in fields] == [
-        "database", "database", "database", "runtime-event"
-    ]
-    assert [item["source"]["label"] for item in fields[:3]] == [
-        "script_build_multipath_decision",
-        "script_build_data_decision",
-        "script_build_domain_info",
-    ]
-    assert [item["source"]["fieldPath"] for item in fields] == [
-        "decision", "decision", "content", "input.content.goal"
-    ]
-
-
-def test_implementation_task_sections_bind_to_exact_task_spans(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    task = """交付说明:输出一份完整候选表
-任务目标:形成三段式文章结构
-method: 先搭建段落骨架
-目标:让读者理解因果关系
-必用素材:柔性排产案例
-前情提要:本轮已核实领域事实
-重要约束:不改动事实数字
-禁止事项:不得伪造引用"""
-    event = next(item for item in bundle["events"] if item["id"] == 50)
-    event["inputData"] = {"task": task}
-    detail = _event_detail(event, input_content={"task": task}, output_content={"summary": "完成"})
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(
-        main.provider,
-        "load_event_details",
-        lambda _build_id, ids: {50: deepcopy(detail)} if 50 in ids else {},
-    )
-    main._inspector_cache.clear()
-
-    response = client.get(
-        "/api/script-builds/88001/inspector-view/round:1:branch:1:task"
-    )
-    assert response.status_code == 200
-    payload = response.json()
-    expected_modules = {
-        "task-notes-1": "deliverable",
-        "task-scope-2": "scope",
-        "task-method-3": "method",
-        "task-goal-4": "goal",
-        "task-materials-5": "constraints",
-        "task-context-6": "constraints",
-        "task-constraints-7": "constraints",
-        "task-avoid-8": "avoid",
-    }
-    for module_id, source_module_id in expected_modules.items():
-        module = _module(payload, module_id)
-        bindings = _row_bindings(module)
-        assert bindings, module_id
-        assert all(binding["resolution"] == "resolved" for binding in bindings)
-        assert all(
-            binding["selector"]["kind"] == "text-span"
-            for binding in bindings
-            if binding["sourceId"] == "event:50"
-        )
-        if source_module_id != "scope":
-            event_binding = next(binding for binding in bindings if binding["sourceId"] == "event:50")
-            assert event_binding["selector"]["kind"] == "text-span"
-            assert event_binding["selector"]["path"] == "/input/content/task"
-            section = next(
-                item
-                for item in payload["businessProjection"]["businessSections"]
-                if item["id"] == module_id
-            )
-            assert event_binding["selector"]["exactText"] == section["content"]
-
-
-def test_inspector_view_query_binds_each_condition_and_case(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    event = next(item for item in bundle["events"] if item["id"] == 53)
-    detail = _event_detail(
-        event,
-        input_content={"keyword": "因果结构", "top_k": 2},
-        output_content={
-            "count": 2,
-            "results": [
-                {"post_id": "A", "account": "账号A", "data": "Case A"},
-                {"post_id": "B", "account": "账号B", "data": "Case B"},
-            ],
-        },
-    )
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {53: deepcopy(detail)})
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
-    main._inspector_cache.clear()
-
-    payload = client.get("/api/script-builds/88001/inspector-view/event:53").json()
-    purpose = _module(payload, "conditions")
-    result = _module(payload, "results")
-    status = _module(payload, "outcome")
-    assert [_row_bindings(purpose, index)[0]["selector"]["path"] for index in range(2)] == [
-        "/input/content/keyword",
-        "/input/content/top_k",
-    ]
-    assert [_row_bindings(result, index)[0]["selector"]["path"] for index in range(2)] == [
-        "/output/content/results/0",
-        "/output/content/results/1",
-    ]
-    assert len({binding["id"] for binding in result["bindings"]}) == 2
-    assert all(
-        binding["selector"].get("path") == "/output/content"
-        and binding["transform"]["operation"] == "retrieval-outcome-v1"
-        for binding in _row_bindings(status)
-    )
-    assert result["bindings"][0]["evidence"] == {
-        "availability": "returned-to-agent",
-        "adoption": "not-recorded",
-        "confidence": "deterministic",
-    }
-    assert result["bindings"][0]["transform"]["operation"] == "retrieval-result-item-v1"
-    assert all(binding["resolution"] == "resolved" for binding in status["bindings"])
-    assert not any(":error:" in binding["id"] for binding in status["bindings"])
-    status_binding_ids = {binding["id"] for binding in status["bindings"]}
-    assert all(
-        set(row["bindingIds"]).issubset(status_binding_ids)
-        for row in status["rows"]
-    )
-
-
-@pytest.mark.parametrize(
-    ("event_name", "output_content", "expected_paths"),
-    [
-        (
-            "get_domain_info",
-            {"domain_info": [{"id": 1, "content": "事实 A"}, {"id": 2, "content": "事实 B"}]},
-            [
-                "/output/content/domain_info/0",
-                "/output/content/domain_info/1",
-            ],
-        ),
-        (
-            "get_script_snapshot",
-            {"script_build": {"status": "running"}, "paragraphs": []},
-            ["/output/content"],
-        ),
-    ],
-)
-def test_direct_tool_result_paths_follow_the_shared_business_contract(
-    monkeypatch, current_bundle, event_name, output_content, expected_paths
-):
-    bundle = deepcopy(current_bundle)
-    event = {
-        "id": 60,
-        "event_seq": 60,
-        "event_type": "tool_call",
-        "event_name": event_name,
-        "agent_role": "main",
-        "agent_depth": 0,
-        "status": "completed",
-        "ended_at": "2026-07-13T10:00:01",
-    }
-    bundle["events"].append(event)
-    detail = _event_detail(event, input_content={}, output_content=output_content)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(
-        main.provider,
-        "load_event_details",
-        lambda _build_id, ids: {60: deepcopy(detail)} if 60 in ids else {},
-    )
-    main._inspector_cache.clear()
-
-    payload = client.get("/api/script-builds/88001/inspector-view/event:60").json()
-    results = _module(payload, "results")
-    paths = [
-        _row_bindings(results, index)[0]["selector"].get("path")
-        for index in range(len(results["rows"]))
-    ]
-    assert paths == expected_paths
-
-
-def test_generic_event_with_saved_body_never_falls_back_to_false_missing(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    event = {
-        "id": 60,
-        "event_seq": 60,
-        "event_type": "tool_call",
-        "event_name": "save_script_direction",
-        "agent_role": "main",
-        "agent_depth": 0,
-        "status": "completed",
-    }
-    bundle["events"].insert(0, event)
-    detail = _event_detail(
-        event,
-        input_content={"direction": "完整创作方向"},
-        output_content="创作脚本方向已保存",
-    )
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(
-        main.provider,
-        "load_event_details",
-        lambda _build_id, ids: {60: deepcopy(detail)} if 60 in ids else {},
-    )
-    main._inspector_cache.clear()
-
-    payload = client.get("/api/script-builds/88001/inspector-view/event:60").json()
-    assert payload["sources"]["event:60"]["resultState"] == "present"
-    bindings = [
-        binding
-        for module in payload["modules"]
-        for binding in module["bindings"]
-    ]
-    assert bindings
-    assert all(binding["sourceId"] == "event:60" for binding in bindings)
-    assert all(binding["resolution"] == "resolved" for binding in bindings)
-
-
-def test_data_decision_and_final_result_bind_to_real_database_fields(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    bundle["record"].update({"status": "success", "summary": "构建摘要"})
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
-    main._inspector_cache.clear()
-
-    decision = client.get(
-        "/api/script-builds/88001/inspector-view/data-decision:11"
-    ).json()
-    source_module = _module(decision, "block-1")
-    for index, row in enumerate(source_module["rows"]):
-        row_bindings = _row_bindings(source_module, index)
-        assert len(row_bindings) == 1
-        assert row_bindings[0]["selector"]["path"] == f"/sources/{index}"
-        assert row_bindings[0]["evidence"]["adoption"] == "explicitly-adopted"
-        assert decision["sources"][row_bindings[0]["sourceId"]]["locator"]["table"] == "script_build_data_decision"
-    assert len(_row_bindings(_module(decision, "block-2"))) == 1
-
-    final = client.get(
-        "/api/script-builds/88001/inspector-view/run:final-result"
-    ).json()
-    conclusion = _module(final, "conclusion")
-    summary = _module(final, "summary")
-    assert final["businessProjection"]["businessSections"][0]["content"] == "构建已完成。"
-    assert _row_bindings(conclusion)[0]["selector"]["path"] == "/status"
-    assert _row_bindings(conclusion)[0]["transform"]["kind"] == "parsed"
-    assert _row_bindings(summary)[0]["selector"]["path"] == "/summary"
-    stats = _module(final, "statistics")
-    assert _row_bindings(stats)[0]["selector"]["kind"] == "members"
-    assert _row_bindings(stats)[0]["selector"]["members"]
-    assert _row_bindings(stats)[0]["selector"]["reducer"] == "Round / Branch 记录计数"
-
-
-def test_every_visible_module_has_a_resolved_or_explicit_unresolved_binding(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
-    main._inspector_cache.clear()
-
-    payload = client.get(
-        "/api/script-builds/88001/inspector-view/round:1:goal"
-    ).json()
-    assert payload["modules"]
-    for module in payload["modules"]:
-        assert module["rows"]
-        for row in module["rows"]:
-            assert row["businessSelector"]
-            bindings = _row_bindings(module, module["rows"].index(row))
-            assert bindings
-            for binding in bindings:
-                assert binding["resolution"] in {"resolved", "partial", "missing", "unsafe"}
-                if binding["resolution"] == "missing":
-                    assert binding["selector"]["kind"] == "unresolved"
-                    assert binding["selector"]["reason"]
-                if binding["resolution"] == "unsafe":
-                    assert binding["evidence"]["confidence"] == "unconfirmed"
-                assert binding["sourceId"] in payload["sources"]
-
-
-def test_business_projection_is_the_same_projection_as_readable_inspector(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
-    main._inspector_cache.clear()
-
-    for detail_ref in (
-        "run:objective",
-        "round:1:goal",
-        "round:1:plan",
-        "data-decision:11",
-        "round:1:branch:1:task",
-        "round:1:result",
-        "run:final-result",
-    ):
-        old = client.get(f"/api/script-builds/88001/activities/{detail_ref}")
-        source = client.get(f"/api/script-builds/88001/inspector-view/{detail_ref}")
-        assert old.status_code == 200, detail_ref
-        assert source.status_code == 200, detail_ref
-        expected = {
-            key: value
-            for key, value in old.json().items()
-            if key not in {"technical", "changes"}
-        }
-        assert source.json()["businessProjection"] == expected, detail_ref
-
-
-def test_zero_domain_facts_are_a_complete_empty_query_not_missing(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    bundle["domainInfo"] = []
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
-    main._inspector_cache.clear()
-
-    payload = client.get(
-        "/api/script-builds/88001/inspector-view/round:1:branch:2:output"
-    ).json()
-    assert payload["businessProjection"]["businessSections"][0]["content"] == (
-        "已查询完成,本方案没有新增领域事实"
-    )
-    output = _module(payload, "output")
-    binding = _row_bindings(output)[0]
-    source = payload["sources"][binding["sourceId"]]
-    assert binding["resolution"] == "resolved"
-    assert source["completeness"] == "complete"
-    assert source["resultState"] == "empty"
-    assert source["locator"] == {
-        "table": "script_build_domain_info",
-        "filters": {"branch_id": 2},
-        "resultCount": 0,
-    }
-    assert source["rawRecord"] == []
-
-    round_result = client.get(
-        "/api/script-builds/88001/inspector-view/round:1:result"
-    ).json()
-    domain = _module(round_result, "facts")
-    domain_source = round_result["sources"][_row_bindings(domain)[0]["sourceId"]]
-    assert domain_source["resultState"] == "empty"
-    assert domain_source["rawRecord"]["value"] == []
-
-
-def test_completeness_scan_includes_item_level_bindings():
-    modules = [
-        {
-            "bindings": [{"id": "module", "resolution": "resolved"}],
-            "business": {
-                "items": [
-                    {
-                        "bindings": [
-                            {
-                                "id": "child",
-                                "resolution": "missing",
-                                "selector": {"kind": "unresolved", "reason": "missing"},
-                            }
-                        ]
-                    }
-                ]
-            },
-        }
-    ]
-    assert {item["resolution"] for item in _all_bindings(modules)} == {
-        "resolved",
-        "missing",
-    }
-
-
-def test_direct_tool_group_has_one_item_binding_per_call(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    summary = next(item for item in bundle["events"] if item["id"] == 51)
-    detail = _event_detail(summary, input_content={}, output_content={"paragraphs": [1, 2]})
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {51: deepcopy(detail)})
-    monkeypatch.setattr(main.provider, "load_event_detail", lambda _build_id, _event_id: deepcopy(detail))
-    main._inspector_cache.clear()
-
-    payload = client.get(
-        "/api/script-builds/88001/inspector-view/retrieval-direct:51"
-    ).json()
-    activities = _module(payload, "activities")
-    assert len(activities["rows"]) == 1
-    activity_binding = _row_bindings(activities)[0]
-    assert activity_binding["selector"]["kind"] == "whole-record"
-    assert activity_binding["transform"]["operation"] == "activity-summary-v1"
-
-
-def test_whole_event_and_parsed_text_fallback_are_resolved():
-    event = _event_detail(
-        {
-            "id": 91,
-            "event_name": "script_evaluator",
-            "status": "completed",
-            "duration_ms": 20,
-        },
-        input_content={"task": "评审两个方案"},
-        output_content={"summary": "原始 Markdown **结论**"},
-    )
-    resolver = SourceResolver(bundle={}, event_details={91: event})
-    whole = resolver.binding_for_event(91, binding_id="whole", path="")
-    assert whole["selector"] == {"kind": "whole-record"}
-    assert whole["resolution"] == "resolved"
-
-    parsed = resolver.text_span_binding(
-        91,
-        binding_id="parsed",
-        path="output.content.summary",
-        exact_text="清理后的结论",
-        role="output",
-    )
-    assert parsed["selector"] == {
-        "kind": "json-pointer",
-        "path": "/output/content/summary",
-    }
-    assert parsed["transform"]["kind"] == "parsed"
-    assert parsed["resolution"] == "resolved"
-
-
-def test_round_goal_and_plan_bind_their_real_save_events(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    begin = {
-        "id": 60,
-        "event_seq": 60,
-        "event_type": "tool_call",
-        "event_name": "begin_round",
-        "round_index": 1,
-        "status": "completed",
-    }
-    revision = {
-        "id": 61,
-        "event_seq": 61,
-        "event_type": "tool_call",
-        "event_name": "record_multipath_plan",
-        "round_index": 1,
-        "status": "completed",
-    }
-    bundle["events"].extend([begin, revision])
-    details = {
-        60: _event_detail(begin, input_content={"goal": bundle["rounds"][0]["goal"]}, output_content={"round_index": 1}),
-        61: _event_detail(revision, input_content={"paths": bundle["rounds"][0]["multipath_plan"], "mode": "分工", "note": "保存规划"}, output_content={"saved": True}),
-    }
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, ids: {event_id: deepcopy(details[event_id]) for event_id in ids if event_id in details})
-    main._inspector_cache.clear()
-
-    goal = client.get("/api/script-builds/88001/inspector-view/round:1:goal").json()
-    goal_block = _module(goal, "goal")
-    entered = next(binding for binding in _row_bindings(goal_block) if binding["sourceId"] == "event:60")
-    assert entered["selector"]["path"] == "/input/content/goal"
-    assert entered["evidence"]["availability"] == "produced-by-run"
-
-    plan = client.get("/api/script-builds/88001/inspector-view/round:1:plan").json()
-    formation = _module(plan, "basis")
-    revisions = next(binding for binding in _row_bindings(formation) if binding["sourceId"] == "event:61")
-    assert revisions["selector"]["path"] == "/input/content"
-    assert revisions["evidence"]["availability"] == "produced-by-run"
-
-
-def test_round_result_review_uses_the_real_event_record(monkeypatch, current_bundle):
-    bundle = deepcopy(current_bundle)
-    summary = next(item for item in bundle["events"] if item["id"] == 41)
-    detail = _event_detail(
-        summary,
-        input_content={"task": "评审第 1 轮主脚本"},
-        output_content={"summary": summary["agentOutputData"]["summary"]},
-    )
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, ids: {41: deepcopy(detail)} if 41 in ids else {})
-    main._inspector_cache.clear()
-
-    payload = client.get("/api/script-builds/88001/inspector-view/round:1:result").json()
-    review_bindings = [binding for binding in _all_bindings(payload["modules"]) if binding["sourceId"] == "event:41"]
-    assert review_bindings
-    assert all(binding["selector"]["kind"] == "whole-record" for binding in review_bindings)
-    assert all(binding["resolution"] == "resolved" for binding in review_bindings)
-
-
-def test_log_window_loader_ignores_events_without_msg_id(monkeypatch):
-    captured = []
-    monkeypatch.setattr(
-        main.audit_repository,
-        "load_event_log_windows",
-        lambda build_id, anchors: captured.extend(anchors) or {},
-    )
-    result = main._safe_load_log_windows(443, {9: {"msg_id": None, "event_seq": 77}})
-    assert captured == []
-    assert result == {}
-
-
-def test_log_context_requires_the_exact_event_msg_id():
-    content = """[FOLD:第一段]
-[MSG_ANCHOR:msg_id=message-a:seq=7]
-内容 A
-[FOLD:第二段]
-[TOOL_ANCHOR:msg_id=message-b:seq=7:tool=test]
-内容 B
-"""
-    matched = locate_log_module(content, {"msg_id": "message-b", "event_seq": 7})
-    assert matched is not None
-    assert matched["msgId"] == "message-b"
-    assert "内容 B" in matched["content"]
-    assert "内容 A" not in matched["content"]
-    assert locate_log_module(content, {"msg_id": None, "event_seq": 7}) is None
-    assert locate_log_module(content, {"msg_id": "message-c", "event_seq": 7}) is None
-
-
-def test_missing_review_and_decision_cards_have_resolved_empty_sources(
-    monkeypatch, current_bundle
-):
-    bundle = deepcopy(current_bundle)
-    bundle["events"] = [
-        event
-        for event in bundle["events"]
-        if event.get("event_name") != "script_multipath_evaluator"
-    ]
-    bundle["multipathDecisions"] = []
-    monkeypatch.setattr(main.provider, "load_bundle", lambda _build_id: deepcopy(bundle))
-    monkeypatch.setattr(main.provider, "load_event_details", lambda _build_id, _ids: {})
-    main._inspector_cache.clear()
-    main._build_view_cache.clear()
-
-    execution = client.get("/api/script-builds/88001/execution-view").json()
-    batch = execution["rounds"][0]["convergenceBatches"][0]
-    refs = [
-        batch["missingReview"]["detailRef"],
-        batch["missingDecision"]["detailRef"],
-    ]
-    for detail_ref in refs:
-        response = client.get(
-            f"/api/script-builds/88001/inspector-view/{detail_ref}"
-        )
-        assert response.status_code == 200
-        payload = response.json()
-        assert payload["businessProjection"]["businessSections"][0]["id"] == "status"
-        bindings = _row_bindings(_module(payload, "status"))
-        assert bindings
-        assert all(binding["resolution"] == "resolved" for binding in bindings)
-        assert any(
-            source["resultState"] == "empty"
-            for source in payload["sources"].values()
-        )

+ 0 - 687
visualization/backend/tests/test_v8_builder.py

@@ -1,687 +0,0 @@
-from copy import deepcopy
-
-from app.execution_builder import ExecutionViewBuilder
-from app.inspector_projection import project_activity_detail, project_event_detail, project_round_detail
-
-
-def test_v8_contract_separates_multipath_review_main_decision_and_overall_review(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-
-    assert view["schemaVersion"] == "8"
-    assert view["dataShape"] == "current"
-    first = view["rounds"][0]
-    assert len(first["convergenceBatches"]) == 2
-    assert [batch["branchIds"] for batch in first["convergenceBatches"]] == [[1, 2], [3]]
-    assert [batch["review"]["eventId"] for batch in first["convergenceBatches"]] == [39, 40]
-    assert [batch["decision"]["recordId"] for batch in first["convergenceBatches"]] == [21, 22]
-    assert first["convergenceBatches"][0]["decision"]["branchOutcomes"] == [
-        {"branchId": 1, "status": "merged", "label": "已采用", "reasoning": "方案 1 的当前处理理由"},
-        {"branchId": 2, "status": "discarded", "label": "未采用", "reasoning": "方案 2 的当前处理理由"},
-    ]
-    assert first["overallEvaluation"]["detailRef"] == "event:41"
-    assert first["branches"][0]["dataDecisions"][0]["decision"] == "采用案例中的三段结构"
-    assert all(decision["recordId"] > 0 for branch in first["branches"] for decision in branch["dataDecisions"])
-    assert "dataDecisions" not in first["convergenceBatches"][0]
-    assert "evaluation" not in first["branches"][0]
-    assert "disposition" not in first["branches"][0]
-    retrieval = first["branches"][0]["retrievalStage"]
-    assert retrieval["directToolGroups"][0]["calls"][0]["businessLabel"] == "当前主脚本"
-    assert retrieval["agentRuns"][0]["businessLabel"] == "解构 Case Agent"
-    assert retrieval["agentRuns"][0]["querySummary"]["empty"] == 1
-    review_projection = first["convergenceBatches"][0]["review"]["decisionProjection"]
-    main_projection = first["convergenceBatches"][0]["decision"]["decisionProjection"]
-    data_projection = first["branches"][0]["dataDecisions"][0]["decisionProjection"]
-    overall_projection = first["overallEvaluation"]["decision"]
-    assert (review_projection["decisionType"], review_projection["authority"]) == (
-        "evaluation",
-        "recommendation",
-    )
-    assert (main_projection["decisionType"], main_projection["authority"]) == (
-        "tradeoff",
-        "final",
-    )
-    assert (data_projection["decisionType"], data_projection["authority"]) == (
-        "tradeoff",
-        "implementation-scope",
-    )
-    assert (overall_projection["decisionType"], overall_projection["authority"]) == (
-        "evaluation",
-        "recommendation",
-    )
-
-
-def test_multipath_review_keeps_grouped_branches_and_comparison_matrix(current_bundle):
-    review_event = next(item for item in current_bundle["events"] if item["id"] == 39)
-    review_event["agentOutputData"]["summary"] = """### 分支评估 branch_id=1(领域信息路)
-**该支结论**:部分通过
-
-**目标推进**:
-- 事实链完整。
-
-**未通过/存疑项**:
-- 元素尚未全部落库。
-
-**有据扎实度**:来源可指位。
-
-### 分支评估 branch_id=2(内容路)
-**该支结论**:部分通过
-
-**目标推进**:
-- 骨架已形成。
-
-**未通过/部分通过项**:
-- 数量红线违规。
-
-**有据扎实度**:与快照一致。
-
-### 对比与建议
-
-| 维度 | branch1 | branch2 | 差异依据 |
-|------|---------|---------|----------|
-| 目标契合度 | 高 | 高 | 各自完成主任务 |
-
-**采纳建议**:组合两个方案。"""
-
-    first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
-    projection = first["convergenceBatches"][0]["review"]["decisionProjection"]
-    blocks = projection["detail"]["blocks"]
-
-    assert [block["type"] for block in blocks[:2]] == [
-        "evaluation-branches",
-        "evaluation-matrix",
-    ]
-    branches = blocks[0]["branches"]
-    assert [item["subject"] for item in branches] == ["方案 1", "方案 2"]
-    assert branches[1]["achievements"] == ["骨架已形成。"]
-    assert branches[1]["problems"] == ["数量红线违规。"]
-    assert blocks[1]["rows"][0]["criterion"] == "目标契合度"
-
-
-def test_same_round_multipath_decisions_remain_separate_and_ordered(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    batches = [item["decision"] for item in view["rounds"][0]["convergenceBatches"]]
-
-    assert [item["recordId"] for item in batches] == [21, 22]
-    assert batches[0]["decision"] != batches[1]["decision"]
-    assert batches[0]["node"]["detailRef"] == "multipath-decision:21"
-
-
-def test_comma_separated_review_scope_joins_matching_main_decision(current_bundle):
-    review_event = next(item for item in current_bundle["events"] if item["id"] == 39)
-    review_event["inputData"]["task"] = "评估对象:branch_id = 1, 2(分工)"
-
-    first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
-    batch = next(
-        item
-        for item in first["convergenceBatches"]
-        if item.get("decision") and item["decision"]["recordId"] == 21
-    )
-
-    assert batch["association"] == "exact-branch-set-and-time"
-    assert batch["review"]["eventId"] == 39
-    assert batch["branchIds"] == [1, 2]
-    assert batch["missingReview"] is None
-    assert batch["missingDecision"] is None
-
-
-def test_review_without_main_decision_remains_a_review_only_batch(current_bundle):
-    current_bundle["multipathDecisions"] = []
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    batches = view["rounds"][0]["convergenceBatches"]
-
-    assert [item["association"] for item in batches] == ["review-only", "review-only"]
-    assert [item["review"]["eventId"] for item in batches] == [39, 40]
-    assert all(item["decision"] is None for item in batches)
-    assert all(item["missingDecision"] for item in batches)
-
-
-def test_unscoped_same_round_review_is_reported_as_unlinked_not_missing(current_bundle):
-    current_bundle["events"] = [
-        item
-        for item in current_bundle["events"]
-        if item.get("event_name") != "script_multipath_evaluator"
-    ]
-    current_bundle["events"].append(
-        {
-            "id": 4995,
-            "event_seq": 99,
-            "event_type": "agent_invoke",
-            "event_name": "script_multipath_evaluator",
-            "round_index": 1,
-            "status": "ok",
-            "inputData": {"task": "请评审本轮候选方案"},
-            "agentOutputData": {"summary": "评审内容完整,但任务未保存方案范围。"},
-        }
-    )
-
-    first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
-    missing = first["convergenceBatches"][0]["missingReview"]
-
-    assert missing["card"]["primary"]["value"] == (
-        "发现同轮评审,但无法安全关联到具体候选"
-    )
-    assert missing["sourceNotice"] == "association-incomplete"
-    assert missing["unscopedReviewRefs"] == ["event:4995"]
-
-
-def test_main_canvas_translates_agent_terms_without_changing_recorded_decision(current_bundle):
-    current_bundle["multipathDecisions"][0]["decision"] = "选择 Branch 1 合入 Base。"
-    first = ExecutionViewBuilder().from_bundle(current_bundle)["rounds"][0]
-    batch = first["convergenceBatches"][0]["decision"]
-
-    assert batch["decision"] == "选择 Branch 1 合入 Base。"
-    assert batch["node"]["card"]["primary"]["value"] == "选择方案 1 合入主脚本。"
-    assert "方案 1 合入主脚本" in first["summary"]["decision"]["summary"]
-
-
-def test_round_summary_is_structured_and_keeps_multiple_decision_batches(current_bundle):
-    current_bundle["rounds"][0]["goal"] = (
-        "将三段骨架扩展到可模拟完整观感的程度\n"
-        "① 为各段补充具体形式和脚本元素\n"
-        "② 将详细执行描述改成可执行画面\n"
-        "③ 拆分核心呈现段的二级子段\n"
-        "④ 保持三个方案互不依赖"
-    )
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    summary = view["rounds"][0]["summary"]
-
-    assert summary["goal"]["headline"].startswith("将三段骨架扩展到可模拟完整观感的程度")
-    assert summary["goal"]["itemCount"] == 0
-    assert summary["goal"]["previewItems"] == []
-    assert summary["goal"]["truncated"] is False
-    assert summary["approach"]["mode"] == "分工"
-    assert summary["approach"]["candidateCount"] == 3
-    assert summary["decision"]["batchCount"] == 2
-    assert summary["decision"]["branchIds"] == [1, 2, 3]
-    assert summary["output"]["mergedContentCount"] == 1
-    assert summary["output"]["domainFactCount"] == 1
-    assert view["rounds"][0]["result"]["artifactRef"] == "artifact:round:1"
-
-
-def test_round_detail_keeps_complete_goal_out_of_compact_summary(current_bundle):
-    full_goal = "核心目标\n① 第一项很长的具体任务\n② 第二项很长的具体任务\n③ 第三项很长的具体任务\n④ 第四项只在详情中完整保留"
-    current_bundle["rounds"][0]["goal"] = full_goal
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    first = view["rounds"][0]
-    detail = project_round_detail(88001, first, current_bundle)
-
-    assert first["summary"]["goal"]["previewItems"] == []
-    assert detail["businessSections"][0]["content"] == full_goal
-
-
-def test_branch_task_detail_uses_complete_dispatch_task_instead_of_card_preview(current_bundle):
-    full_task = "目标:" + "完整创作任务。" * 80
-    current_bundle["events"].append({
-        "id": 99,
-        "event_seq": 99,
-        "event_type": "agent_invoke",
-        "event_name": "script_implementer",
-        "agent_role": "script_implementer",
-        "round_index": 1,
-        "branch_id": 1,
-        "status": "ok",
-        "inputData": {"task": full_task},
-    })
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    task_card = view["rounds"][0]["branches"][0]["task"]["card"]["primary"]
-    detail = project_activity_detail(
-        88001,
-        "round:1:branch:1:task",
-        view,
-        current_bundle,
-    )
-
-    assert detail["businessSections"][0]["title"] == "目标"
-    assert detail["businessSections"][0]["content"] == "完整创作任务。" * 80
-    assert len(detail["businessSections"][0]["content"]) > 140
-    assert task_card["label"] == "目标"
-    assert task_card["value"] == "完整创作任务。" * 80
-    assert "…" not in task_card["value"]
-
-
-def test_final_result_has_independent_detail_and_current_artifact_entry(current_bundle):
-    current_bundle["record"]["status"] = "success"
-    current_bundle["record"]["summary"] = "已形成可交付的主脚本。"
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    final = view["finalResult"]
-    detail = project_activity_detail(
-        88001, "run:final-result", view, current_bundle
-    )
-
-    assert final["detailRef"] == "run:final-result"
-    assert final["artifactRef"] == "artifact:base:current"
-    assert final["card"]["primary"]["value"] == "已形成可交付的主脚本。"
-    assert [item["title"] for item in detail["businessSections"]] == [
-        "构建结论",
-        "构建摘要或失败原因",
-        "轮次与方案统计",
-        "最终主脚本规模",
-        "版本说明",
-    ]
-    assert detail["changes"]["artifactRef"] == "artifact:base:current"
-    assert detail["changes"]["exactness"] == "current-only"
-
-
-def test_round_result_uses_dedicated_aggregate_detail(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    detail = project_activity_detail(
-        88001, "round:1:result", view, current_bundle
-    )
-
-    assert [item["title"] for item in detail["businessSections"]] == [
-        "本轮目标完成情况",
-        "方案处置",
-        "领域事实",
-        "整体评审",
-        "主脚本变化",
-        "下一步",
-    ]
-    assert "方案 1:已采用" in detail["businessSections"][1]["content"]
-    assert "柔性排产可降低" in detail["businessSections"][2]["content"]
-    assert detail["changes"]["artifactRef"] == "artifact:base:current"
-
-
-def test_retrieval_agent_detail_prefers_lazy_loaded_complete_event(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    full_task = "查找并对比可说明因果结构的解构 Case,保留完整限制条件。"
-    full_screening = "### 初筛整理\n- 两个案例可用。\n- 一个案例因结构不符而舍弃。"
-    event_detail = {
-        "id": 52,
-        "event_name": "retrieve_data_decode_case",
-        "input": {"content": {"task": full_task}},
-        "output": {"content": {"summary": full_screening}},
-    }
-    detail = project_activity_detail(
-        88001,
-        "retrieval-agent:52",
-        view,
-        current_bundle,
-        event_detail=event_detail,
-    )
-
-    sections = {item["title"]: item["content"] for item in detail["businessSections"]}
-    assert sections["完整取数任务"] == full_task
-    assert "一个案例因结构不符而舍弃" in sections["初筛整理"]
-    assert "成功为空 1 次" in sections["原始命中"]
-
-
-def test_first_three_main_agent_cards_and_details_use_real_decision_context(current_bundle):
-    direction = """## 选题价值主张
-用真实工厂解释柔性排产的价值。
-
-## 账号价值主张
-保持专业、可核验的表达。
-
-## 创作总目标
-### 目标 1
-- 目标语句:让读者看懂柔性排产如何减少浪费。
-### 目标 2
-- 目标语句:用可核验的行业事实支撑结论。
-
-## 领域评估维度
-- 维度名:数据可信度
-"""
-    current_bundle["record"]["script_direction"] = direction
-    first_round = current_bundle["rounds"][0]
-    first_round["goal"] = "完成主结构。\n① 补齐机制拆解\n② 核实行业数据"
-    paths = first_round["multipath_plan"]
-    current_bundle["events"] = [
-        {
-            "id": 100,
-            "event_seq": 0,
-            "event_type": "agent_scope",
-            "event_name": "main",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "status": "ok",
-        },
-        *[
-            {
-                "id": event_id,
-                "event_seq": 1,
-                "event_type": "tool_call",
-                "event_name": name,
-                "agent_role": "main",
-                "agent_depth": 0,
-                "scope_event_id": 100,
-                "parent_event_id": 100,
-                "status": "ok",
-                "ended_at": "2026-07-13T09:59:01",
-            }
-            for event_id, name in (
-                (101, "get_topic_detail"),
-                (102, "get_account_script_section_patterns"),
-                (103, "get_account_script_persona_points"),
-            )
-        ],
-        {
-            "id": 108,
-            "event_seq": 2,
-            "event_type": "tool_call",
-            "event_name": "think_and_plan",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "parent_event_id": 100,
-            "status": "ok",
-            "inputData": {
-                "thought_number": 1,
-                "thought_summary": "先锁定创作目标,再竞争首轮结构。",
-                "thought": "完整分析账号结构、选题价值与领域下限。",
-                "plan": "保存创作目标,然后开启首轮。",
-                "action": "写入创作总目标",
-            },
-            "ended_at": "2026-07-13T09:59:02",
-        },
-        {
-            "id": 104,
-            "event_seq": 3,
-            "event_type": "tool_call",
-            "event_name": "save_script_direction",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "parent_event_id": 100,
-            "status": "ok",
-            "inputData": {"script_direction": direction},
-            "outputData": {"success": True},
-            "ended_at": "2026-07-13T09:59:02",
-        },
-        {
-            "id": 105,
-            "event_seq": 4,
-            "event_type": "tool_call",
-            "event_name": "begin_round",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "parent_event_id": 100,
-            "round_index": 0,
-            "status": "ok",
-            "outputData": {"success": True, "round_index": 1},
-            "ended_at": "2026-07-13T09:59:03",
-        },
-        {
-            "id": 106,
-            "event_seq": 5,
-            "event_type": "tool_call",
-            "event_name": "get_script_snapshot",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "parent_event_id": 100,
-            "round_index": 1,
-            "status": "ok",
-            "outputData": {"count": 0},
-            "ended_at": "2026-07-13T09:59:04",
-        },
-        {
-            "id": 107,
-            "event_seq": 6,
-            "event_type": "tool_call",
-            "event_name": "record_multipath_plan",
-            "agent_role": "main",
-            "agent_depth": 0,
-            "scope_event_id": 100,
-            "parent_event_id": 100,
-            "round_index": 1,
-            "status": "ok",
-            "inputData": {
-                "paths": paths,
-                "mode": "分工",
-                "note": first_round["plan_note"],
-            },
-            "outputData": {"success": True, "round_index": 1},
-            "ended_at": "2026-07-13T09:59:05",
-        },
-        *current_bundle["events"],
-    ]
-
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    planning = view["planning"]
-    objective = view["objective"]
-    first = view["rounds"][0]
-
-    assert planning["title"] == "创作规划解析"
-    assert planning["detailRef"] == "event:108"
-    assert planning["card"]["primary"]["value"] == "先锁定创作目标,再竞争首轮结构。"
-    planning_event = next(item for item in current_bundle["events"] if item["id"] == 108)
-    planning_detail = project_event_detail(88001, planning_event)
-    assert [section["title"] for section in planning_detail["businessSections"]] == [
-        "规划概要",
-        "完整思考",
-        "执行计划",
-        "下一步",
-    ]
-    assert planning_detail["businessSections"][1]["content"] == "完整分析账号结构、选题价值与领域下限。"
-    assert objective["card"]["primary"]["value"] == "让读者看懂柔性排产如何减少浪费。"
-    assert [line["label"] for line in objective["card"]["secondary"]] == [
-        "决策前读取",
-        "目标结构",
-    ]
-    assert objective["card"]["secondary"][0]["value"] == "选题 · 账号段落模式 · 账号人设"
-    assert "2 个创作目标" in objective["card"]["secondary"][1]["value"]
-    assert "完成主结构" in first["goal"]["card"]["primary"]["value"]
-    assert "补齐机制拆解" in first["goal"]["card"]["primary"]["value"]
-    assert first["goal"]["card"]["secondary"][0]["label"] == "形成前可见"
-    assert first["plan"]["node"]["card"]["secondary"][0]["label"] == "各路任务"
-    assert first["plan"]["runtimeRevisionRefs"] == ["event:107"]
-    assert "decisionContext" not in str(view)
-
-    objective_detail = project_activity_detail(88001, "run:objective", view, current_bundle)
-    goal_detail = project_activity_detail(88001, "round:1:goal", view, current_bundle)
-    plan_detail = project_activity_detail(88001, "round:1:plan", view, current_bundle)
-    objective_titles = [block["title"] for block in objective_detail["blocks"]]
-    assert objective_titles == ["当前保存的创作方向", "根据以下信息做出的决策"]
-    assert objective_detail["blocks"][0]["value"] == direction.strip()
-    assert objective_detail["blocks"][0]["presentation"] == "document"
-    assert "关键约束" not in objective_titles
-    assert "构建总结" not in str(objective_detail["blocks"])
-    goal_titles = [block["title"] for block in goal_detail["blocks"]]
-    assert goal_titles[0] == "本轮目标构成"
-    assert goal_detail["blocks"][0]["presentation"] == "document"
-    assert goal_detail["blocks"][0]["value"] == current_bundle["rounds"][0]["goal"]
-    assert "核心目标" not in str(goal_detail["blocks"][0])
-    plan_block = plan_detail["blocks"][0]
-    assert plan_block["type"] == "implementation-plan"
-    assert plan_block["title"] == "本轮实施路线"
-    assert plan_block["routes"][0]["target"] == "文章结构"
-    assert any(block["title"] == "根据以下信息做出的决策" for block in plan_detail["blocks"])
-    assert "人设详细原文" not in str(view)
-
-
-def test_running_round_waits_for_main_agent_without_claiming_record_loss(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    second = view["rounds"][1]
-
-    batch = second["convergenceBatches"][0]
-    assert batch["missingDecision"]["kind"] == "execution"
-    assert batch["missingDecision"]["card"]["primary"]["value"] == "等待主 Agent 比较候选方案"
-    assert "sourceNotice" not in batch["missingDecision"]
-    assert second["branches"][0]["outcome"]["label"] == "等待主 Agent 处置"
-    assert not any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
-
-
-def test_stopped_round_is_process_incomplete_not_record_missing(current_bundle):
-    stopped = deepcopy(current_bundle)
-    stopped["record"]["status"] = "stopped"
-    stopped["multipathDecisions"] = []
-    stopped["events"] = []
-    view = ExecutionViewBuilder().from_bundle(stopped)
-    first = view["rounds"][0]
-
-    batch = first["convergenceBatches"][0]
-    assert batch["missingDecision"]["card"]["primary"]["value"] == "构建停止前尚未形成多路决策"
-    assert batch["missingDecision"]["sourceNotice"] == "process-incomplete"
-    assert first["overallEvaluation"]["card"]["primary"]["value"] == "构建停止前尚未进行整体评审"
-    assert first["overallEvaluation"]["sourceNotice"] == "process-incomplete"
-    open_branch = view["rounds"][1]["branches"][0]
-    assert open_branch["outcome"]["label"] == "构建停止时尚未处置"
-    assert open_branch["outcome"]["sourceNotice"] == "process-incomplete"
-    assert not any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
-
-
-def test_completed_round_without_main_decision_is_a_real_record_gap(current_bundle):
-    completed = deepcopy(current_bundle)
-    completed["record"]["status"] = "success"
-    completed["multipathDecisions"] = []
-    completed["events"] = []
-    view = ExecutionViewBuilder().from_bundle(completed)
-    first = view["rounds"][0]
-
-    batch = first["convergenceBatches"][0]
-    assert batch["missingDecision"]["card"]["primary"]["value"] == "构建已经结束,但未找到主 Agent 多路决策记录"
-    assert batch["missingDecision"]["sourceNotice"] == "record-missing"
-    assert first["overallEvaluation"]["card"]["primary"]["value"] == "构建已经结束,但未找到明确的整体评审结论"
-    assert first["overallEvaluation"]["sourceNotice"] == "record-missing"
-    assert first["overallEvaluation"]["status"] == "not-evaluated"
-    assert first["state"] == "not-evaluated"
-    assert first["result"]["status"] == "not-evaluated"
-    assert any(item["id"] == "missing-multipath-decisions" for item in view["warnings"])
-
-
-def test_domain_information_is_an_independent_output(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    branch = next(item for item in view["rounds"][0]["branches"] if item["branchId"] == 2)
-
-    assert branch["pathType"] == "领域信息"
-    assert branch["output"]["type"] == "domain-info"
-    assert branch["output"]["factCount"] == 1
-    assert branch["output"]["summary"] == "新增 1 条已核实领域事实"
-    assert branch["status"] == "discarded"
-
-
-def test_runtime_event_only_supplies_explicit_evaluation_and_keeps_orphan_agent_unassigned(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-
-    first = view["rounds"][0]
-    assert first["overallEvaluation"]["sourceNotice"] == "runtime-associated"
-    assert first["state"] == "partially-passed"
-    assert view["rounds"][1]["state"] == "running"
-    assert [item["eventId"] for item in view["unassigned"]["runtimeEvents"]] == [42]
-
-
-def test_branch_zero_rows_cannot_enter_new_projection(current_bundle):
-    current_bundle["dataDecisions"].append({
-        "id": 999,
-        "round_index": 1,
-        "branch_id": 0,
-        "decision": "危险旧决策内容",
-        "sources": [],
-    })
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    serialized = str(view)
-
-    assert "危险旧决策内容" not in serialized
-    assert [item["decision"]["recordId"] for item in view["rounds"][0]["convergenceBatches"]] == [21, 22]
-
-
-def test_old_record_is_visible_but_not_reinterpreted(current_bundle):
-    legacy = deepcopy(current_bundle)
-    legacy["events"] = []
-    legacy["multipathDecisions"] = []
-    legacy["domainInfo"] = []
-    for branch in legacy["branches"]:
-        branch["path_type"] = None
-
-    view = ExecutionViewBuilder().from_bundle(legacy)
-    assert view["dataShape"] == "legacy-incomplete"
-    assert all(not any(batch.get("decision") for batch in round_["convergenceBatches"]) for round_ in view["rounds"])
-    assert any(item["id"] == "legacy-shape" for item in view["warnings"])
-
-
-def test_main_payload_does_not_embed_event_bodies_or_candidate_snapshot(current_bundle):
-    current_bundle["events"][0]["output_body"] = "secret complete report"
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    serialized = str(view)
-
-    assert "secret complete report" not in serialized
-    assert "candidate_snapshot" not in serialized
-    assert "script_build_event_body" not in serialized
-
-
-def test_inspector_exposes_business_decisions_and_only_real_changes(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    first = view["rounds"][0]
-    round_detail = project_round_detail(88001, first, current_bundle)
-    data_detail = project_activity_detail(88001, "data-decision:11", view, current_bundle)
-    main_detail = project_activity_detail(88001, "multipath-decision:21", view, current_bundle)
-    goal_detail = project_activity_detail(88001, "round:1:goal", view, current_bundle)
-
-    assert round_detail["changes"]["exactness"] == "current-only"
-    assert data_detail["detailKind"] == "agent-decision"
-    assert [block["title"] for block in data_detail["blocks"]] == [
-        "参与判断",
-        "明确取舍",
-        "理由",
-    ]
-    assert main_detail["detailKind"] == "agent-decision"
-    assert "最终决定" in [block["title"] for block in main_detail["blocks"]]
-    assert "评审建议与最终决定" in [
-        block["title"] for block in main_detail["blocks"]
-    ]
-    assert "changes" not in goal_detail
-
-
-def test_explicit_next_round_translation_is_shown_as_recommendation_handling(current_bundle):
-    changed = deepcopy(current_bundle)
-    changed["multipathDecisions"][0]["reasoning"] = (
-        "方案 1 直接采用。方案 2 的数据优势很有价值,"
-        "但不直接合并,决定在下一轮转译为增强目标。"
-    )
-
-    view = ExecutionViewBuilder().from_bundle(changed)
-    rows = view["rounds"][0]["convergenceBatches"][0]["decision"][
-        "decisionProjection"
-    ]["body"]["reviewComparison"]
-
-    assert "handling" in rows[1]
-    assert "下一轮转译" in rows[1]["handling"]
-
-
-def test_every_projected_business_section_is_readable_text(current_bundle):
-    view = ExecutionViewBuilder().from_bundle(current_bundle)
-    refs = [
-        "run:objective",
-        "data-decision:11",
-        "multipath-decision:21",
-        "domain-info:31",
-        "round:1:goal",
-        "round:1:plan",
-        "round:1:result",
-        "round:1:branch:1",
-        "round:1:branch:1:task",
-        "round:1:branch:1:retrieval",
-        "round:1:branch:1:output",
-    ]
-    details = [project_round_detail(88001, view["rounds"][0], current_bundle)]
-    details.extend(project_activity_detail(88001, ref, view, current_bundle) for ref in refs)
-
-    for detail in details:
-        if detail.get("detailKind") == "agent-decision":
-            assert detail["blocks"]
-            _assert_business_blocks_hide_technical_fields(detail["blocks"])
-        else:
-            assert detail["businessSections"]
-            assert all(isinstance(section["content"], str) for section in detail["businessSections"])
-        assert all(
-            isinstance(section["content"], str)
-            for section in (detail.get("changes") or {}).get("sections", [])
-        )
-
-
-def _assert_business_blocks_hide_technical_fields(blocks):
-    serialized = str(blocks)
-    for forbidden in (
-        "script_build_id",
-        "branch_id",
-        "paragraph_id",
-        "post_id",
-        "event ID",
-        "Tool 名",
-        "path_type",
-        "Base",
-        "patch",
-    ):
-        assert forbidden not in serialized

+ 0 - 1
visualization/exports/.gitkeep

@@ -1 +0,0 @@
-