const NODE_DEFINITIONS = [
  {
    id: "task",
    code: "N00",
    kind: "trigger",
    icon: "▶",
    role: "TASK TRIGGER",
    title: "任务接管",
    summary: "目标与控制边界",
    x: 20,
    y: 25,
    input: "修复 1 kHz 输入频率误差，并在真实板上完成验证",
    decision: "先绑定 Workspace、Board、Probe 与 Session，再允许任何工具动作",
    tool: "task.bind",
    output: "CONSTAR_workspace / STM32F103 / CMSIS-DAP 已绑定",
    evidenceRef: "Evidence E00 · Task",
    log: "$ task.bind freq-1khz-accuracy\nworkspace CONSTAR_workspace\nboard     STM32F103\nprobe     CMSIS-DAP\nresult    session boundary ready",
    observed: "任务、工程和真实硬件已完成上下文绑定",
    hypothesis: "待验证：TIM2 时钟推导与板级配置不一致",
    action: "接管任务并建立控制边界",
    next: "读取 Keil 工程与 TIM2 配置",
    confidence: 42,
    target: "none",
    evidence: 0,
  },
  {
    id: "read",
    code: "A01",
    kind: "agent",
    icon: "A",
    role: "AGENT · OBSERVE",
    title: "读取工程事实",
    summary: "解析工程与时钟树",
    x: 155,
    y: 25,
    input: ".uvprojx、system_stm32f1xx.c、tim.c、frequency.c",
    decision: "不直接修改代码，先确认 TIM2 的真实时钟来源",
    tool: "workspace.read",
    output: "APB1 prescaler=2；TIM2 输入捕获使用 CH1",
    evidenceRef: "Agent Trace A01",
    log: "$ workspace.read --scope clock,capture\nread  71-FREQ.uvprojx\nread  Core/Src/system_stm32f1xx.c\nread  Core/Src/tim.c\nfound APB1 prescaler = 2\nfound TIM2 input capture on CH1",
    observed: "APB1 分频为 2；TIM2 位于 APB1，输入捕获使用 CH1",
    hypothesis: "固定 TIMER_CLOCK_HZ 可能忽略定时器倍频规则",
    action: "对照时钟树与频率换算实现",
    next: "建立可证伪的根因假设",
    confidence: 58,
    target: "none",
  },
  {
    id: "h1",
    code: "A02",
    kind: "agent",
    icon: "H1",
    role: "AGENT · HYPOTHESIS",
    title: "形成假设 H1",
    summary: "时钟推导为主因",
    x: 290,
    y: 25,
    input: "TIM2 时钟规则、固定常量、历史 1 kHz 偏差",
    decision: "先修正时钟来源，再应用已有板级 capture offset；不修改滤波策略",
    tool: "decision.record",
    output: "H1：修复后物理误差应进入 ±0.10%",
    evidenceRef: "Agent Trace A02 · H1",
    log: "$ decision.record H1\nfact     TIM2 clock derives from APB1 x2 rule\nconflict fixed TIMER_CLOCK_HZ bypasses board clock tree\nscope    clock derivation + calibrated capture offset\nexclude  input filter policy",
    observed: "代码使用固定时钟常量，板级配置包含 capture offset",
    hypothesis: "H1：时钟推导错误是主因，capture offset 是剩余偏差",
    action: "记录假设与验收边界",
    next: "制定最小修改与实板验证计划",
    confidence: 71,
    target: "none",
  },
  {
    id: "plan",
    code: "A03",
    kind: "agent",
    icon: "P",
    role: "AGENT · PLAN",
    title: "提交验证计划",
    summary: "软件到物理闭环",
    x: 425,
    y: 25,
    input: "H1、受控工具清单、HWPOD 资源",
    decision: "软件输出不能替代 ioProbe 独立物理事实",
    tool: "plan.commit",
    output: "Patch → Build → Policy → Flash → UART → ioProbe",
    evidenceRef: "Agent Trace A03 · Plan",
    log: "$ plan.commit\nP1 apply minimal patch\nP2 build Keil target\nP3 verify session policy\nP4 flash through CMSIS-DAP\nP5 inspect UART\nP6 measure with ioProbe",
    observed: "代码工具与真实硬件工具均已就绪",
    hypothesis: "H1 可通过单次受控闭环验证",
    action: "锁定计划和证据边界",
    next: "生成最小代码变更",
    confidence: 74,
    target: "none",
  },
  {
    id: "patch",
    code: "T04",
    kind: "tool",
    icon: "Δ",
    role: "TOOL · PATCH",
    title: "生成代码变更",
    summary: "frequency.c +5 −3",
    x: 560,
    y: 25,
    input: "frequency.c、board clock、calibration.capture_offset",
    decision: "只修改 capture_to_hz() 边界，不触碰输入滤波与其他模块",
    tool: "apply_patch",
    output: "动态时钟推导 + 板级 capture offset",
    evidenceRef: "Evidence E04 · Patch 8a91c2d",
    log: "$ apply_patch Core/Src/frequency.c\nremove TIMER_CLOCK_HZ assumption\nadd    get_timer_clock_hz()\nadd    calibration.capture_offset\nresult  +5 -3 · patch_sha=8a91c2d",
    observed: "修改范围可限制在 capture_to_hz()",
    hypothesis: "最小变更足以消除主偏差，且不会改变滤波行为",
    action: "生成并审查代码 Diff",
    next: "调用 Keil 构建固件",
    confidence: 79,
    target: "none",
    evidence: 2,
    patch: true,
  },
  {
    id: "build",
    code: "T05",
    kind: "tool",
    icon: "K",
    role: "TOOL · KEIL",
    title: "Keil 构建",
    summary: "生成可追溯产物",
    x: 695,
    y: 25,
    input: "71-FREQ.uvprojx、Patch 8a91c2d",
    decision: "只有构建产物与当前 Patch 一致，才能进入真实板下载",
    tool: "keil.build",
    output: "0 errors、0 warnings；freq.axf 32.18 KiB",
    evidenceRef: "Evidence E05 · Artifact 4f7c...9b2a",
    log: "$ keil.build --target 71-FREQ\ncompile frequency.c\nlink    freq.axf\nProgram Size: Code=28964 RO-data=1984\n0 Error(s), 0 Warning(s)\nartifact sha256:4f7c...9b2a",
    observed: "当前 Patch 已生成唯一固件产物",
    hypothesis: "软件修复可进入真实目标板验证",
    action: "审查构建结果与产物摘要",
    next: "验证 Session 与危险工具权限",
    confidence: 84,
    target: "none",
    evidence: 3,
  },
  {
    id: "policy",
    code: "G05",
    kind: "gate",
    icon: "✓",
    role: "CONTROL GATE",
    title: "Session Policy",
    summary: "校验租约与产物摘要",
    x: 695,
    y: 130,
    input: "Session、HWPOD lease、Artifact SHA、CMSIS-DAP 权限",
    decision: "危险硬件动作只允许在资源租约和产物摘要一致时继续",
    tool: "session.policy.verify",
    output: "HWPOD lease 有效；Artifact 匹配；Flash 工具获准",
    evidenceRef: "Control Evidence G05",
    log: "$ session.policy.verify\nsession   agt_71freq_024\nhwpod     lease active\nartifact  sha matched\ntool       cmsis-dap.flash allowed\nresult     PASS",
    observed: "HWPOD 租约有效，产物摘要与构建节点一致",
    hypothesis: "真实硬件动作具备受控执行条件",
    action: "检查 Session Policy",
    next: "允许 CMSIS-DAP 写入目标板",
    confidence: 88,
    target: "none",
  },
  {
    id: "flash",
    code: "H06",
    kind: "hardware",
    icon: "F",
    role: "HWPOD · PROBE",
    title: "CMSIS-DAP Flash",
    summary: "写入并校验固件",
    x: 560,
    y: 130,
    input: "freq.axf 4f7c...9b2a、STM32F103",
    decision: "Flash verify 必须通过，且写入摘要必须匹配 Build 产物",
    tool: "cmsis-dap.flash",
    output: "32.18 KiB 写入并校验通过；目标板复位",
    evidenceRef: "Evidence E06 · Probe",
    log: "$ cmsis-dap.flash freq.axf\nconnect STM32F103 · SWD 4MHz\nerase   sectors 0-31\nwrite   32.18 KiB\nverify  OK · artifact matched\nreset   target",
    observed: "真实目标板已运行当前构建产物",
    hypothesis: "若 H1 正确，运行输出应接近 1 kHz",
    action: "操作调试探针下载与校验",
    next: "读取 UART 运行事实",
    confidence: 89,
    target: "probe",
    evidence: 4,
  },
  {
    id: "uart",
    code: "H07",
    kind: "hardware",
    icon: "U",
    role: "HWPOD · UART",
    title: "UART Capture",
    summary: "128 samples · stable",
    x: 425,
    y: 130,
    input: "UART2 115200、目标板运行态",
    decision: "UART 证明固件链路正常，但不能作为最终精度证据",
    tool: "uart.capture",
    output: "128 samples；capture stable；overflow=0",
    evidenceRef: "Evidence E07 · Runtime",
    log: "$ uart.capture --port UART2 --samples 128\n[boot] 71-FREQ rev.C\n[timer] clock=72000000Hz\n[capture] stable samples=128 overflow=0\n[reported] frequency=1000.1Hz",
    observed: "固件稳定运行，软件自报频率 1000.1 Hz",
    hypothesis: "H1 获得运行态支持，但仍需外部测量排除自证循环",
    action: "读取并界定 UART 事实",
    next: "调用 ioProbe 测量 FREQ_OUT",
    confidence: 92,
    target: "uart",
    evidence: 5,
  },
  {
    id: "ioprobe",
    code: "H08",
    kind: "hardware",
    icon: "IO",
    role: "HWPOD · PHYSICAL",
    title: "ioProbe Measure",
    summary: "独立采集 FREQ_OUT",
    x: 290,
    y: 130,
    input: "ioProbe CH1、FREQ_OUT、目标 1000.0 Hz",
    decision: "以独立物理测量决定是否接受 H1 与当前 Patch",
    tool: "ioprobe.measure",
    output: "等待物理采样结果",
    evidenceRef: "Evidence E08 · Physical",
    log: "$ ioprobe.measure --channel CH1 --metric frequency\narm     FREQ_OUT\nsample  physical waveform\nresult  pending",
    observed: "物理输出已接入 ioProbe CH1",
    hypothesis: "物理频率应落入 1000.0 Hz ±0.10%",
    action: "采集真实波形并计算频率",
    next: "将物理事实交给 Tolerance Gate",
    confidence: 94,
    target: "ioprobe",
    evidence: 6,
  },
  {
    id: "gate",
    code: "G08",
    kind: "gate",
    icon: "IF",
    role: "DECISION GATE",
    title: "Tolerance Gate",
    summary: "Physical ≤ ±0.10%?",
    x: 155,
    y: 130,
    input: "ioProbe 独立频率与 ±0.10% 验收边界",
    decision: "只依据物理事实选择 PASS 或 FAIL 分支",
    tool: "evidence.compare",
    output: "等待 ioProbe 结果",
    evidenceRef: "Verdict Input G08",
    log: "$ evidence.compare --source ioprobe --tolerance 0.10%\nresult pending",
    observed: "等待独立物理测量进入裁决节点",
    hypothesis: "物理结果决定当前能力是否可封存",
    action: "比较物理事实与验收边界",
    next: "选择 PASS 或 FAIL 分支",
    confidence: 94,
    target: "none",
  },
  {
    id: "aggregate",
    code: "E09",
    kind: "evidence",
    icon: "Σ",
    role: "EVIDENCE",
    title: "Aggregate PASS",
    summary: "汇总代码到物理证据",
    x: 290,
    y: 235,
    input: "Patch、Build、Policy、Flash、UART、ioProbe 999.8 Hz",
    decision: "只有完整路径通过才形成可复用能力",
    tool: "evidence.aggregate",
    output: "Aggregate PASS",
    evidenceRef: "Evidence E09 · Verdict",
    log: "$ evidence.aggregate\npatch    PASS\nbuild    PASS\npolicy   PASS\nflash    PASS\nuart     PASS\nphysical PASS · 999.8Hz\nverdict  Aggregate PASS",
    observed: "代码、工具、运行态和物理事实全部通过",
    hypothesis: "H1 已被多层独立证据支持",
    action: "形成 Aggregate PASS",
    next: "封存回归 Case",
    confidence: 99,
    target: "none",
    evidence: 7,
  },
  {
    id: "case",
    code: "E10",
    kind: "evidence",
    icon: "C",
    role: "CAPABILITY",
    title: "Regression Case",
    summary: "保存可重复研发能力",
    x: 425,
    y: 235,
    input: "Aggregate PASS 与完整 Agent Trace",
    decision: "封存任务、计划、工具、产物和物理事实的引用，不复制运行面数据",
    tool: "case.seal",
    output: "freq-1khz-accuracy 可保存",
    evidenceRef: "Reusable Capability E10",
    log: "$ case.seal freq-1khz-accuracy\ntrace     agt_71freq_024\nevidence  complete\nstatus    ready to persist",
    observed: "完整证据链满足能力封存条件",
    hypothesis: "当前任务可转化为稳定回归能力",
    action: "准备封存回归 Case",
    next: "等待用户确认保存",
    confidence: 99,
    target: "none",
  },
  {
    id: "h2",
    code: "A09",
    kind: "agent",
    icon: "H2",
    role: "AGENT · RE-DIAGNOSE",
    title: "生成假设 H2",
    summary: "失败后返回工程诊断",
    x: 155,
    y: 305,
    input: "软件链路通过，但 ioProbe 仅测得 972.4 Hz",
    decision: "软件自报值不能覆盖物理失败；拒绝封存并重新打开诊断",
    tool: "plan.revise",
    output: "H2：滤波窗口或 capture offset 仍有板级偏差",
    evidenceRef: "Blocked Verdict A09 · H2",
    log: "$ plan.revise\nphysical FAIL · 972.4Hz (-2.76%)\nreject   completion\nnew H2   inspect filter window + capture offset\nloop     back to workspace.read",
    observed: "ioProbe 972.4 Hz，与 UART 自报值冲突",
    hypothesis: "H2：输入滤波窗口或 capture offset 仍存在板级偏差",
    action: "拒绝封存并重新规划",
    next: "回到 Read Workspace 读取原始 capture 样本",
    confidence: 56,
    target: "none",
  },
];

const EDGE_DEFINITIONS = [
  { id: "task-read", from: "task", to: "read", path: "M140 53 L155 53" },
  { id: "read-h1", from: "read", to: "h1", path: "M275 53 L290 53" },
  { id: "h1-plan", from: "h1", to: "plan", path: "M410 53 L425 53" },
  { id: "plan-patch", from: "plan", to: "patch", path: "M545 53 L560 53" },
  { id: "patch-build", from: "patch", to: "build", path: "M680 53 L695 53" },
  { id: "build-policy", from: "build", to: "policy", path: "M755 81 L755 130" },
  { id: "policy-flash", from: "policy", to: "flash", path: "M695 158 L680 158" },
  { id: "flash-uart", from: "flash", to: "uart", path: "M560 158 L545 158" },
  { id: "uart-ioprobe", from: "uart", to: "ioprobe", path: "M425 158 L410 158" },
  { id: "ioprobe-gate", from: "ioprobe", to: "gate", path: "M290 158 L275 158" },
  { id: "gate-aggregate", from: "gate", to: "aggregate", path: "M215 186 L215 211 L350 211 L350 235", label: "PASS", labelX: 294, labelY: 207, labelClass: "is-pass" },
  { id: "aggregate-case", from: "aggregate", to: "case", path: "M410 263 L425 263" },
  { id: "gate-h2", from: "gate", to: "h2", path: "M215 186 L215 305", label: "FAIL", labelX: 222, labelY: 248, labelClass: "is-fail" },
  { id: "h2-read", from: "h2", to: "read", path: "M155 333 C86 333 86 53 155 53", label: "RE-DIAGNOSE", labelX: 91, labelY: 192, labelClass: "is-loop", loop: true },
];

const MAIN_PATH = ["task", "read", "h1", "plan", "patch", "build", "policy", "flash", "uart", "ioprobe", "gate"];
const EVIDENCE_DETAILS = [
  { idle: "Task Trigger 尚未执行。", passed: "N00：任务和控制边界已绑定当前 Session。" },
  { idle: "尚无 Agent 执行图证据。", active: "AGENT 正在逐节点执行并记录输入输出。", passed: "AGENT 执行图完整，可按节点回放。" },
  { idle: "T04 Patch 尚未执行。", passed: "T04：frequency.c +5 −3，与 H1 明确关联。" },
  { idle: "T05 Build 尚未执行。", passed: "T05：Keil 0 errors、0 warnings，产物摘要已记录。" },
  { idle: "H06 Flash 尚未执行。", passed: "H06：CMSIS-DAP 写入、校验和复位通过。" },
  { idle: "H07 UART 尚未执行。", passed: "H07：128 samples，capture stable，overflow=0。" },
  { idle: "H08 Physical 尚未执行。", active: "H08：ioProbe 正在独立采集 FREQ_OUT。", passed: "H08：999.8 Hz，误差 −0.02%。", failed: "H08：972.4 Hz，误差 −2.76%，推翻完成假设。" },
  { idle: "G08 尚未选择执行分支。", passed: "E09：PASS 分支完成，可封存回归 Case。", blocked: "A09：FAIL 分支完成，AGENT 已回到 Read Workspace。" },
];

const PATCH_HTML = `
  <div class="diff-line"><span>118</span><code>static float capture_to_hz(uint32_t ticks)</code></div>
  <div class="diff-line"><span>119</span><code>{</code></div>
  <div class="diff-line is-remove"><span>120</span><code>-  return TIMER_CLOCK_HZ / ticks;</code></div>
  <div class="diff-line is-remove"><span>121</span><code>-  // APB prescaler assumed to be 1</code></div>
  <div class="diff-line is-add"><span>120</span><code>+  const uint32_t timer_clock = get_timer_clock_hz();</code></div>
  <div class="diff-line is-add"><span>121</span><code>+  const float calibrated_ticks =</code></div>
  <div class="diff-line is-add"><span>122</span><code>+      (float)ticks - calibration.capture_offset;</code></div>
  <div class="diff-line is-add"><span>123</span><code>+  return timer_clock / calibrated_ticks;</code></div>
  <div class="diff-line"><span>124</span><code>}</code></div>
  <div class="diff-line is-add"><span>128</span><code>+  // Evidence: H1 / T04 / patch 8a91c2d</code></div>
`;

const app = document.querySelector("#app");
const runButton = document.querySelector("#runButton");
const resetButton = document.querySelector("#resetButton");
const failureMode = document.querySelector("#failureMode");
const saveCaseButton = document.querySelector("#saveCaseButton");
const agentState = document.querySelector("#agentState");
const observedFact = document.querySelector("#observedFact");
const currentHypothesis = document.querySelector("#currentHypothesis");
const currentAction = document.querySelector("#currentAction");
const nextAction = document.querySelector("#nextAction");
const confidenceMeter = document.querySelector("#confidenceMeter");
const confidenceLabel = document.querySelector("#confidenceLabel");
const graphProgress = document.querySelector("#graphProgress");
const canvasStage = document.querySelector("#canvasStage");
const workflowViewport = document.querySelector("#workflowViewport");
const workflowBoard = document.querySelector("#workflowBoard");
const workflowEdges = document.querySelector("#workflowEdges");
const workflowNodes = document.querySelector("#workflowNodes");
const zoomOutButton = document.querySelector("#zoomOutButton");
const fitGraphButton = document.querySelector("#fitGraphButton");
const zoomInButton = document.querySelector("#zoomInButton");
const selectedNodeKind = document.querySelector("#selectedNodeKind");
const inspectorTitle = document.querySelector("#inspector-title");
const tabButtons = [...document.querySelectorAll("[data-tab]")];
const inspectorViews = [...document.querySelectorAll("[data-view]")];
const detailEventId = document.querySelector("#detailEventId");
const detailTitle = document.querySelector("#detailTitle");
const detailSummary = document.querySelector("#detailSummary");
const detailInput = document.querySelector("#detailInput");
const detailDecision = document.querySelector("#detailDecision");
const detailTool = document.querySelector("#detailTool");
const detailOutput = document.querySelector("#detailOutput");
const patchAction = document.querySelector("#patchAction");
const patchScore = document.querySelector("#patchScore");
const codeDiff = document.querySelector("#codeDiff");
const activeTool = document.querySelector("#activeTool");
const toolStatus = document.querySelector("#toolStatus");
const toolOutput = document.querySelector("#toolOutput");
const hardwareCommand = document.querySelector("#hardwareCommand");
const benchVerdict = document.querySelector("#benchVerdict");
const scopeState = document.querySelector("#scopeState");
const scopeCanvas = document.querySelector("#scopeCanvas");
const scopeContext = scopeCanvas.getContext("2d");
const actualFrequency = document.querySelector("#actualFrequency");
const frequencyError = document.querySelector("#frequencyError");
const boardFact = document.querySelector("#boardFact");
const uartFact = document.querySelector("#uartFact");
const evidenceButtons = [...document.querySelectorAll("[data-evidence]")];
const evidenceDetail = document.querySelector("#evidenceDetail");
const toast = document.querySelector("#toast");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");

let nodes = [];
let runToken = 0;
let runState = "idle";
let selectedNodeId = "task";
let selectedEvidence = -1;
let patchGenerated = false;
let activeTab = "decision";
let scopeMode = "idle";
let scopeAnimationFrame = 0;
let toastTimer = 0;
let graphScale = 1;
let graphPanX = 0;
let graphPanY = 0;
let dragState = null;

function getNode(id) {
  return nodes.find((node) => node.id === id);
}

function getEdge(id) {
  return workflowEdges.querySelector(`[data-edge-id="${id}"]`);
}

function wait(milliseconds, token) {
  const duration = reducedMotion.matches ? Math.min(milliseconds, 70) : milliseconds;
  return new Promise((resolve) => {
    window.setTimeout(() => resolve(token === runToken), duration);
  });
}

function kindClass(kind) {
  return {
    agent: "node-agent",
    tool: "node-tool",
    hardware: "node-hardware",
    gate: "node-gate",
    evidence: "node-evidence",
  }[kind] || "node-trigger";
}

function stateLabel(status) {
  return {
    idle: "QUEUED",
    active: "RUNNING",
    passed: "PASSED",
    failed: "FAILED",
    blocked: "BLOCKED",
    skipped: "SKIPPED",
  }[status] || status.toUpperCase();
}

function renderGraph() {
  workflowEdges.innerHTML = EDGE_DEFINITIONS.map((edge) => `
    <path class="flow-edge${edge.loop ? " is-loop" : ""}" data-edge-id="${edge.id}" d="${edge.path}"></path>
    ${edge.label ? `<text class="edge-label ${edge.labelClass || ""}" x="${edge.labelX}" y="${edge.labelY}">${edge.label}</text>` : ""}
  `).join("");

  workflowNodes.innerHTML = "";
  for (const node of nodes) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = `flow-node ${kindClass(node.kind)}`;
    button.dataset.nodeId = node.id;
    button.style.left = `${node.x}px`;
    button.style.top = `${node.y}px`;
    button.addEventListener("click", (event) => {
      event.stopPropagation();
      selectNode(node.id);
    });
    workflowNodes.append(button);
    refreshNode(node.id);
  }
}

function refreshNode(id) {
  const node = getNode(id);
  const button = workflowNodes.querySelector(`[data-node-id="${id}"]`);
  if (!node || !button) return;
  if (node.status === "idle") button.removeAttribute("data-status");
  else button.dataset.status = node.status;
  button.setAttribute("aria-label", `${node.code} ${node.title}，${stateLabel(node.status)}`);
  const icon = node.kind === "gate" ? `<span>${node.icon}</span>` : node.icon;
  button.innerHTML = `
    <i class="node-icon">${icon}</i>
    <span class="node-topline"><span>${node.code} · ${node.role}</span><i class="node-state"></i></span>
    <strong class="node-title">${node.title}</strong>
    <small class="node-summary">${node.summary}</small>
  `;
}

function setRunState(state, label) {
  runState = state;
  app.dataset.runState = state;
  agentState.textContent = label;
  runButton.disabled = state === "running";
  failureMode.disabled = state === "running";
  saveCaseButton.disabled = state !== "passed";
}

function setNodeStatus(id, status) {
  const node = getNode(id);
  if (!node) return;
  node.status = status;
  refreshNode(id);
  updateGraphProgress();
  if (selectedNodeId === id) updateInspector(node);
}

function setEdgeStatus(id, status) {
  const edge = getEdge(id);
  if (!edge) return;
  if (status === "idle") edge.removeAttribute("data-status");
  else edge.dataset.status = status;
}

function updateGraphProgress() {
  const touched = nodes.filter((node) => node.status !== "idle").length;
  graphProgress.textContent = `${touched} / ${nodes.length} NODES`;
}

function applyDecisionBoard(node) {
  observedFact.textContent = node.observed;
  currentHypothesis.textContent = node.hypothesis;
  currentAction.textContent = node.action;
  nextAction.textContent = node.next;
  confidenceMeter.style.width = `${node.confidence}%`;
  confidenceMeter.style.background = node.confidence >= 90 ? "var(--green)" : "var(--amber)";
  confidenceLabel.textContent = `${node.confidence >= 90 ? "证据" : "当前"} ${node.confidence}%`;
}

function selectNode(id, autoTab = true) {
  const node = getNode(id);
  if (!node) return;
  selectedNodeId = id;
  workflowNodes.querySelectorAll(".flow-node").forEach((button) => {
    button.classList.toggle("is-selected", button.dataset.nodeId === id);
  });
  updateInspector(node);
  if (autoTab) {
    if (node.patch && patchGenerated) selectTab("patch");
    else if (node.kind === "tool" || node.kind === "hardware") selectTab("tool");
    else selectTab("decision");
  }
}

function updateInspector(node) {
  selectedNodeKind.textContent = `${node.role} · ${stateLabel(node.status)}`;
  inspectorTitle.textContent = node.title;
  detailEventId.textContent = `${node.code} · ${node.role}`;
  detailTitle.textContent = node.title;
  detailSummary.textContent = `${node.summary} · ${node.evidenceRef}`;
  detailInput.textContent = node.input;
  detailDecision.textContent = node.decision;
  detailTool.textContent = node.tool;
  detailOutput.textContent = `${node.output}；${node.evidenceRef}`;
  activeTool.textContent = node.tool;
  toolStatus.textContent = stateLabel(node.status);
  toolOutput.textContent = node.log;
}

function selectTab(tab) {
  activeTab = tab;
  tabButtons.forEach((button) => {
    const active = button.dataset.tab === tab;
    button.classList.toggle("is-active", active);
    button.setAttribute("aria-selected", String(active));
  });
  inspectorViews.forEach((view) => {
    const active = view.dataset.view === tab;
    view.classList.toggle("is-active", active);
    view.hidden = !active;
  });
}

function revealPatch() {
  patchGenerated = true;
  patchAction.textContent = "T04 · H1 · frequency.c";
  patchScore.textContent = "+5 −3 · REVIEWED";
  codeDiff.innerHTML = PATCH_HTML;
}

function setEvidenceStatus(index, status, label) {
  const button = evidenceButtons[index];
  if (!button) return;
  if (status === "idle") button.removeAttribute("data-status");
  else button.dataset.status = status;
  if (label) button.querySelector("span").textContent = label;
  if (selectedEvidence === index) showEvidence(index);
}

function showEvidence(index) {
  selectedEvidence = index;
  evidenceButtons.forEach((button, buttonIndex) => {
    button.classList.toggle("is-selected", index === buttonIndex);
  });
  const status = evidenceButtons[index]?.dataset.status || "idle";
  evidenceDetail.textContent = EVIDENCE_DETAILS[index]?.[status] || EVIDENCE_DETAILS[index]?.idle;
}

function setHardwareTarget(target, command = "等待工具调用") {
  app.dataset.hardwareTarget = target;
  hardwareCommand.textContent = command;
}

function applyGraphTransform() {
  workflowBoard.style.transform = `translate(${graphPanX}px, ${graphPanY}px) scale(${graphScale})`;
}

function fitGraph() {
  const bounds = workflowViewport.getBoundingClientRect();
  if (!bounds.width || !bounds.height) return;
  const fitScale = Math.min((bounds.width - 18) / 840, (bounds.height - 18) / 375, 1.15);
  const minimumScale = bounds.width <= 430 ? 0.55 : 0.45;
  graphScale = Math.max(minimumScale, fitScale);
  graphPanX = (bounds.width - 840 * graphScale) / 2;
  graphPanY = (bounds.height - 375 * graphScale) / 2;
  applyGraphTransform();
}

function zoomGraph(nextScale, anchorX, anchorY) {
  const bounds = workflowViewport.getBoundingClientRect();
  const next = Math.max(0.45, Math.min(1.5, nextScale));
  const pointX = anchorX ?? bounds.width / 2;
  const pointY = anchorY ?? bounds.height / 2;
  const worldX = (pointX - graphPanX) / graphScale;
  const worldY = (pointY - graphPanY) / graphScale;
  graphScale = next;
  graphPanX = pointX - worldX * graphScale;
  graphPanY = pointY - worldY * graphScale;
  applyGraphTransform();
}

function ensureNodeVisible(id) {
  const node = getNode(id);
  if (!node) return;
  const bounds = workflowViewport.getBoundingClientRect();
  const left = graphPanX + node.x * graphScale;
  const top = graphPanY + node.y * graphScale;
  const right = left + 120 * graphScale;
  const bottom = top + 56 * graphScale;
  const margin = 20;
  if (left < margin) graphPanX += margin - left;
  if (right > bounds.width - margin) graphPanX -= right - (bounds.width - margin);
  if (top < margin) graphPanY += margin - top;
  if (bottom > bounds.height - margin) graphPanY -= bottom - (bounds.height - margin);
  applyGraphTransform();
}

function setScopeMode(mode) {
  scopeMode = mode;
  window.cancelAnimationFrame(scopeAnimationFrame);
  scopeAnimationFrame = 0;
  if (mode === "sampling") animateScope();
  else drawScope(0);
}

function drawScope(phase = 0) {
  const bounds = scopeCanvas.getBoundingClientRect();
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
  const width = Math.max(1, Math.round(bounds.width * ratio));
  const height = Math.max(1, Math.round(bounds.height * ratio));
  if (scopeCanvas.width !== width || scopeCanvas.height !== height) {
    scopeCanvas.width = width;
    scopeCanvas.height = height;
  }
  scopeContext.setTransform(ratio, 0, 0, ratio, 0, 0);
  const cssWidth = width / ratio;
  const cssHeight = height / ratio;
  scopeContext.clearRect(0, 0, cssWidth, cssHeight);
  scopeContext.fillStyle = "#0b1418";
  scopeContext.fillRect(0, 0, cssWidth, cssHeight);
  scopeContext.strokeStyle = "rgba(95, 135, 145, 0.18)";
  scopeContext.lineWidth = 1;
  for (let x = 0; x <= cssWidth; x += cssWidth / 9) {
    scopeContext.beginPath();
    scopeContext.moveTo(x, 0);
    scopeContext.lineTo(x, cssHeight);
    scopeContext.stroke();
  }
  for (let y = 0; y <= cssHeight; y += cssHeight / 4) {
    scopeContext.beginPath();
    scopeContext.moveTo(0, y);
    scopeContext.lineTo(cssWidth, y);
    scopeContext.stroke();
  }
  const idle = scopeMode === "idle";
  const failed = scopeMode === "failed";
  const amplitude = cssHeight * 0.28;
  const center = cssHeight * 0.5;
  const period = failed ? 52 : 43;
  scopeContext.beginPath();
  for (let x = 0; x <= cssWidth; x += 2) {
    const signal = idle ? Math.sin(x / 24) * 1.1 : Math.sin((x + phase) * ((Math.PI * 2) / period)) * amplitude;
    const noise = scopeMode === "sampling" ? Math.sin((x + phase * 1.7) / 5) * 0.8 : 0;
    const y = center + signal + noise;
    if (x === 0) scopeContext.moveTo(x, y);
    else scopeContext.lineTo(x, y);
  }
  const color = failed ? "#ff766e" : idle ? "#486169" : "#50dbe0";
  scopeContext.strokeStyle = color;
  scopeContext.lineWidth = 1.3;
  scopeContext.shadowColor = idle ? "transparent" : color;
  scopeContext.shadowBlur = idle ? 0 : 5;
  scopeContext.stroke();
  scopeContext.shadowBlur = 0;
}

function animateScope(timestamp = 0) {
  if (scopeMode !== "sampling") {
    scopeAnimationFrame = 0;
    return;
  }
  drawScope(timestamp * 0.08);
  scopeAnimationFrame = window.requestAnimationFrame(animateScope);
}

function updateNodeResult(id, updates) {
  Object.assign(getNode(id), updates);
  refreshNode(id);
  if (selectedNodeId === id) updateInspector(getNode(id));
}

async function executeNode(id, incomingEdgeId, token, shouldFail) {
  const node = getNode(id);
  if (incomingEdgeId) setEdgeStatus(incomingEdgeId, "active");
  setNodeStatus(id, "active");
  applyDecisionBoard(node);
  selectNode(id);
  ensureNodeVisible(id);
  canvasStage.textContent = `${node.code} · ${node.title}`;
  agentState.textContent = `${node.code} · ${node.title}`;
  setHardwareTarget(node.target, node.target === "none" ? `${node.tool} · AGENT action` : `${node.tool} · dispatched`);

  if (node.patch) {
    revealPatch();
    selectTab("patch");
  }
  if (id === "build") boardFact.textContent = "固件已构建 · 待授权";
  if (id === "policy") boardFact.textContent = "Session Policy 校验中";
  if (id === "flash") boardFact.textContent = "正在写入目标板";
  if (id === "uart") {
    boardFact.textContent = "固件运行中";
    uartFact.textContent = "采集 128 samples";
  }
  if (id === "ioprobe") {
    scopeState.textContent = "AGENT 正在采样";
    setEvidenceStatus(6, "active");
    setScopeMode("sampling");
  }

  const continued = await wait(id === "ioprobe" ? 620 : 350, token);
  if (!continued) return false;

  if (id === "ioprobe") {
    if (shouldFail) {
      updateNodeResult("ioprobe", {
        summary: "972.4 Hz · −2.76%",
        output: "ioProbe 972.4 Hz；误差 −2.76%；超出阈值",
        log: "$ ioprobe.measure --channel CH1 --metric frequency\nactual  972.4 Hz\nerror   -2.76%\nresult  OUTSIDE TOLERANCE",
      });
      setNodeStatus("ioprobe", "failed");
      if (incomingEdgeId) setEdgeStatus(incomingEdgeId, "passed");
      setEvidenceStatus(6, "failed");
      actualFrequency.innerHTML = "972.4 <small>Hz</small>";
      frequencyError.textContent = "−2.76%";
      scopeState.textContent = "物理阈值超限";
      benchVerdict.textContent = "PHYSICAL FAIL";
      setScopeMode("failed");
      return true;
    }
    updateNodeResult("ioprobe", {
      summary: "999.8 Hz · −0.02%",
      output: "ioProbe 999.8 Hz；误差 −0.02%；满足阈值",
      log: "$ ioprobe.measure --channel CH1 --metric frequency\nactual  999.8 Hz\nerror   -0.02%\nresult  WITHIN TOLERANCE",
    });
    actualFrequency.innerHTML = "999.8 <small>Hz</small>";
    frequencyError.textContent = "−0.02%";
    scopeState.textContent = "独立测量通过";
    setScopeMode("passed");
  }

  if (id === "gate" && shouldFail) {
    updateNodeResult("gate", {
      summary: "FAIL · route to H2",
      output: "FAIL：972.4 Hz 超出 ±0.10%；阻断 PASS 分支",
      log: "$ evidence.compare\nactual    972.4 Hz\ntolerance ±0.10%\nbranch    FAIL → Hypothesis H2",
    });
    setNodeStatus("gate", "blocked");
    if (incomingEdgeId) setEdgeStatus(incomingEdgeId, "failed");
    return true;
  }

  if (id === "gate") {
    updateNodeResult("gate", {
      summary: "PASS · route to Aggregate",
      output: "PASS：999.8 Hz 满足 ±0.10%；进入聚合分支",
      log: "$ evidence.compare\nactual    999.8 Hz\ntolerance ±0.10%\nbranch    PASS → Aggregate Evidence",
    });
  }

  setNodeStatus(id, "passed");
  if (incomingEdgeId) setEdgeStatus(incomingEdgeId, "passed");
  if (node.evidence !== undefined) setEvidenceStatus(node.evidence, "passed");
  return true;
}

function resetDemo({ keepFailure = true } = {}) {
  runToken += 1;
  if (!keepFailure) failureMode.checked = false;
  nodes = NODE_DEFINITIONS.map((node) => ({ ...node, status: "idle" }));
  selectedNodeId = "task";
  selectedEvidence = -1;
  patchGenerated = false;
  renderGraph();
  evidenceButtons.forEach((button, index) => {
    button.removeAttribute("data-status");
    button.classList.remove("is-selected");
    if (index === 7) button.querySelector("span").textContent = "Verdict 待定";
  });
  patchAction.textContent = "尚未生成变更";
  patchScore.textContent = "WAITING";
  codeDiff.innerHTML = '<div class="empty-change"><span>NO PATCH YET</span><p>AGENT 必须先获得工程事实并验证假设，代码变更才会出现。</p></div>';
  evidenceDetail.textContent = "每个证据节点都引用产生它的 AGENT 行动。";
  actualFrequency.innerHTML = "— <small>Hz</small>";
  frequencyError.textContent = "—";
  scopeState.textContent = "等待 AGENT 调用";
  benchVerdict.textContent = "SESSION BOUND";
  boardFact.textContent = "已绑定 · 待操作";
  uartFact.textContent = "等待固件";
  canvasStage.textContent = "READY";
  setHardwareTarget("none");
  setScopeMode("idle");
  setRunState("idle", "已接管 · 等待执行");
  applyDecisionBoard(getNode("task"));
  selectNode("task", false);
  selectTab("decision");
  window.setTimeout(fitGraph, 0);
}

async function runDemo() {
  if (runState === "running") return;
  resetDemo();
  const token = runToken;
  const shouldFail = failureMode.checked;
  setRunState("running", "AGENT 正在执行工作流");
  setEvidenceStatus(1, "active");

  let previous = null;
  for (const id of MAIN_PATH) {
    const incoming = previous ? `${previous}-${id}` : null;
    const continued = await executeNode(id, incoming, token, shouldFail);
    if (!continued || token !== runToken) return;
    previous = id;
  }

  setHardwareTarget("none", shouldFail ? "physical fact rejected completion" : "PASS branch selected");

  if (shouldFail) {
    setEdgeStatus("gate-aggregate", "skipped");
    setEdgeStatus("aggregate-case", "skipped");
    setNodeStatus("aggregate", "skipped");
    setNodeStatus("case", "skipped");
    const continued = await executeNode("h2", "gate-h2", token, true);
    if (!continued || token !== runToken) return;
    setEdgeStatus("h2-read", "active");
    setNodeStatus("read", "active");
    applyDecisionBoard(getNode("h2"));
    selectNode("h2", false);
    canvasStage.textContent = "LOOP → READ WORKSPACE";
    setEvidenceStatus(1, "passed");
    setEvidenceStatus(7, "blocked", "Verdict BLOCKED");
    benchVerdict.textContent = "BLOCKED · BACK TO DIAGNOSIS";
    boardFact.textContent = "保持现场 · 等待新计划";
    uartFact.textContent = "软件事实保留";
    setRunState("failed", "拒绝封存 · 已回到 Read Workspace");
    showEvidence(7);
    return;
  }

  setEdgeStatus("gate-h2", "skipped");
  setEdgeStatus("h2-read", "skipped");
  setNodeStatus("h2", "skipped");
  let continued = await executeNode("aggregate", "gate-aggregate", token, false);
  if (!continued || token !== runToken) return;
  continued = await executeNode("case", "aggregate-case", token, false);
  if (!continued || token !== runToken) return;
  setEvidenceStatus(1, "passed");
  setEvidenceStatus(7, "passed", "Verdict PASS");
  benchVerdict.textContent = "AGGREGATE PASS";
  boardFact.textContent = "验证完成 · 固件保留";
  uartFact.textContent = "稳定 · 128 samples";
  canvasStage.textContent = "WORKFLOW PASS";
  setRunState("passed", "PASS 分支完成 · Case 可封存");
  showEvidence(7);
  selectNode("case", false);
}

function showToast(message) {
  window.clearTimeout(toastTimer);
  toast.textContent = message;
  toast.classList.add("is-visible");
  toastTimer = window.setTimeout(() => toast.classList.remove("is-visible"), 2500);
}

tabButtons.forEach((button) => {
  button.addEventListener("click", () => selectTab(button.dataset.tab));
});
evidenceButtons.forEach((button, index) => {
  button.addEventListener("click", () => showEvidence(index));
});
runButton.addEventListener("click", runDemo);
resetButton.addEventListener("click", () => resetDemo({ keepFailure: true }));
failureMode.addEventListener("change", () => {
  if (runState !== "idle") resetDemo({ keepFailure: true });
  showToast(failureMode.checked ? "FAIL 分支已启用：物理失败后将回环到 Read Workspace" : "已恢复 PASS 分支");
});
saveCaseButton.addEventListener("click", () => {
  if (runState !== "passed") return;
  showToast("回归 Case 已封存：Agent Graph + Tool Outputs + Physical Evidence");
});

zoomOutButton.addEventListener("click", () => zoomGraph(graphScale - 0.12));
zoomInButton.addEventListener("click", () => zoomGraph(graphScale + 0.12));
fitGraphButton.addEventListener("click", fitGraph);
workflowViewport.addEventListener("wheel", (event) => {
  event.preventDefault();
  const bounds = workflowViewport.getBoundingClientRect();
  const factor = event.deltaY < 0 ? 0.1 : -0.1;
  zoomGraph(graphScale + factor, event.clientX - bounds.left, event.clientY - bounds.top);
}, { passive: false });
workflowViewport.addEventListener("pointerdown", (event) => {
  if (event.target.closest(".flow-node")) return;
  dragState = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, panX: graphPanX, panY: graphPanY };
  workflowViewport.classList.add("is-dragging");
  workflowViewport.setPointerCapture(event.pointerId);
});
workflowViewport.addEventListener("pointermove", (event) => {
  if (!dragState || dragState.pointerId !== event.pointerId) return;
  graphPanX = dragState.panX + event.clientX - dragState.x;
  graphPanY = dragState.panY + event.clientY - dragState.y;
  applyGraphTransform();
});
function endDrag(event) {
  if (!dragState || dragState.pointerId !== event.pointerId) return;
  dragState = null;
  workflowViewport.classList.remove("is-dragging");
}
workflowViewport.addEventListener("pointerup", endDrag);
workflowViewport.addEventListener("pointercancel", endDrag);

window.addEventListener("resize", () => {
  fitGraph();
  drawScope(0);
});
window.addEventListener("load", () => {
  fitGraph();
  drawScope(0);
});

resetDemo({ keepFailure: false });
