diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js b/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js index cb23b209..950cd3e0 100644 --- a/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js +++ b/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js @@ -1,71 +1,91 @@ -const INITIAL_EVENT = { - id: "E00", - phase: "observe", - actor: "AGENT", - title: "任务接管与控制边界绑定", - summary: "ALTA-7 接收目标,并绑定工程、目标板、探针与证据空间。", - input: "任务目标、CONSTAR_workspace、STM32F103、CMSIS-DAP", - decision: "先读取工程事实,不把预设代码差异当作问题结论", - tool: "task.bind", - output: "控制边界建立;下一步读取工程与时钟树", - log: "$ task.bind freq-1khz-accuracy\ncontext workspace=CONSTAR_workspace\ntarget board=STM32F103 probe=CMSIS-DAP\nresult session boundary ready", - observed: "任务、工程和真实硬件已完成上下文绑定", - hypothesis: "待验证:TIM2 时钟推导与板级配置不一致", - action: "等待授权读取工程事实", - next: "检查 Keil 工程与 TIM2 配置", - confidence: 42, - target: "none", -}; - -const RUN_EVENTS = [ +const NODE_DEFINITIONS = [ { - id: "E01", - phase: "observe", - actor: "AGENT", - title: "读取工程事实,而不是直接改代码", - summary: "扫描 Keil 工程、时钟树和 TIM2 输入捕获链路。", + 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: "优先确认运行时钟来源,避免围绕错误前提修改换算公式", + decision: "不直接修改代码,先确认 TIM2 的真实时钟来源", tool: "workspace.read", - output: "APB1 prescaler=2;TIM2 时钟需应用定时器倍频规则", + 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: "建立可证伪的误差假设", + next: "建立可证伪的根因假设", confidence: 58, target: "none", }, { - id: "E02", - phase: "hypothesize", - actor: "AGENT", - title: "形成可证伪的根因假设", - summary: "把时钟推导错误与捕获偏移拆成两个可独立验证的变量。", - input: "TIM2 实际时钟规则、固定常量、历史 1 kHz 偏差", - decision: "先修正时钟来源,再应用已有板级 capture offset;不改变滤波策略", + 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%", + 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: "代码使用固定时钟常量,板级配置包含可用标定偏移", + observed: "代码使用固定时钟常量,板级配置包含 capture offset", hypothesis: "H1:时钟推导错误是主因,capture offset 是剩余偏差", - action: "记录假设与验收阈值", + action: "记录假设与验收边界", next: "制定最小修改与实板验证计划", confidence: 71, target: "none", }, { - id: "E03", - phase: "plan", - actor: "AGENT", - title: "提交最小可验证计划", - summary: "代码修改、构建、下载、UART 与独立物理测量形成闭环。", - input: "假设 H1、受控工具清单、真实 HWPOD 资源", - decision: "任何软件输出都不能替代 ioProbe 的独立物理事实", + 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: "P1 Patch → P2 Build → P3 Flash → P4 UART → P5 ioProbe", - log: "$ plan.commit\nP1 apply minimal clock/capture patch\nP2 build Keil target 71-FREQ\nP3 flash through CMSIS-DAP\nP4 inspect UART runtime facts\nP5 measure FREQ_OUT with ioProbe", - observed: "代码工具和真实硬件工具均已就绪", + 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: "生成最小代码变更", @@ -73,177 +93,271 @@ const RUN_EVENTS = [ target: "none", }, { - id: "E04", - phase: "act", - actor: "AGENT", - title: "基于假设生成最小代码变更", - summary: "动态读取定时器时钟,并应用已有板级捕获偏移。", - input: "frequency.c、board_clock、calibration.capture_offset", - decision: "只修改频率换算边界,不触碰输入滤波与其他模块", + 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: "frequency.c +5 −3;变更绑定假设 H1", + 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 构建验证编译事实", + next: "调用 Keil 构建固件", confidence: 79, target: "none", + evidence: 2, patch: true, }, { - id: "E05", - phase: "act", - actor: "TOOL", - title: "AGENT 调用 Keil 构建固件", - summary: "构建工具返回可追溯产物,AGENT 检查错误、警告与摘要。", - input: "71-FREQ.uvprojx、E04 patch_sha=8a91c2d", + 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", - log: "$ keil.build --target 71-FREQ\ncompile frequency.c\nlink freq.axf\nProgram Size: Code=28964 RO-data=1984 RW-data=312 ZI-data=4168\n0 Error(s), 0 Warning(s)\nartifact sha256:4f7c...9b2a", + 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: "软件修复可进入目标板运行验证", + hypothesis: "软件修复可进入真实目标板验证", action: "审查构建结果与产物摘要", - next: "通过 CMSIS-DAP 写入目标板", + next: "验证 Session 与危险工具权限", confidence: 84, target: "none", evidence: 3, }, { - id: "E06", - phase: "act", - actor: "TOOL", - title: "AGENT 操作 CMSIS-DAP 写入固件", - summary: "目标板擦除、下载、校验和复位均由当前 Session 记录。", - input: "freq.axf sha256:4f7c...9b2a、STM32F103 target", - decision: "Flash verify 必须通过,且写入摘要必须匹配 E05 产物", + 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 写入并校验通过;目标板已复位", + 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: "真实目标板已运行 E05 对应固件", - hypothesis: "若 H1 正确,UART 频率输出应接近 1 kHz", - action: "控制调试探针完成下载与校验", + observed: "真实目标板已运行当前构建产物", + hypothesis: "若 H1 正确,运行输出应接近 1 kHz", + action: "操作调试探针下载与校验", next: "读取 UART 运行事实", - confidence: 87, + confidence: 89, target: "probe", evidence: 4, }, { - id: "E07", - phase: "verify", - actor: "HWPOD", - title: "AGENT 读取 UART 运行事实", - summary: "连续采样确认捕获链路稳定,但不把软件自报值当作最终证据。", - input: "UART2 115200、当前目标板运行态", - decision: "UART 可证明固件链路正常;最终精度仍交给 ioProbe 独立判断", + 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: 91, + confidence: 92, target: "uart", evidence: 5, }, { - id: "E08", - phase: "verify", - actor: "HWPOD", - title: "AGENT 调用 ioProbe 独立测量", - summary: "物理测量不依赖固件自报值,直接采集 FREQ_OUT。", + 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: "等待物理采样结果", - log: "$ ioprobe.measure --channel CH1 --metric frequency\narm FREQ_OUT\nsample physical waveform\nwindow stable\nresult pending", - observed: "UART 链路通过,物理输出已接入 ioProbe CH1", + 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: "根据物理事实接受或拒绝封存", - confidence: 93, + 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 SUCCESS_EVENT = { - id: "E09", - phase: "reflect", - actor: "AGENT", - title: "汇总证据并接受当前能力", - summary: "物理结果支持 H1;AGENT 仅在完整证据链通过后允许封存。", - input: "Patch、Build、Flash、UART、ioProbe 999.8 Hz", - decision: "误差 −0.02% 满足阈值,H1 被证据支持,当前 Patch 可复用", - tool: "evidence.aggregate", - output: "Aggregate PASS;允许封存 freq-1khz-accuracy", - log: "$ evidence.aggregate\npatch PASS · E04\nbuild PASS · E05\nflash PASS · E06\nuart PASS · E07\nphysical PASS · 999.8Hz (-0.02%)\nverdict Aggregate PASS", - observed: "ioProbe 实测 999.8 Hz,误差 −0.02%", - hypothesis: "H1 已被软件、运行态和独立物理证据共同支持", - action: "形成 Aggregate PASS 裁决", - next: "封存为可重复回归 Case", - confidence: 99, - target: "none", -}; - -const FAILURE_EVENT = { - id: "E09", - phase: "reflect", - actor: "AGENT", - title: "拒绝封存,并返回诊断循环", - summary: "物理事实推翻当前完成假设;AGENT 阻断结果并生成下一假设。", - input: "Patch、Build、Flash、UART 通过,但 ioProbe 仅测得 972.4 Hz", - decision: "软件链路通过不能覆盖物理失败;拒绝封存并重新打开诊断", - tool: "plan.revise", - output: "Aggregate BLOCKED;新假设 H2:滤波窗口或 capture offset 仍有偏差", - log: "$ evidence.aggregate\nsoftware chain PASS\nphysical FAIL · 972.4Hz (-2.76%)\nverdict Aggregate BLOCKED\n$ plan.revise\nnext H2 inspect filter window + capture offset samples", - observed: "ioProbe 实测 972.4 Hz,与 UART 自报值冲突", - hypothesis: "H2:输入滤波窗口或 capture offset 仍存在板级偏差", - action: "拒绝封存,计划回到工程诊断", - next: "读取滤波窗口与原始 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: "E00 尚未进入执行态。", - passed: "E00:任务、Workspace、Board 和 Probe 已绑定当前 Session。", - }, - { - idle: "尚无 Agent 行动 Trace。", - active: "E01–E09:AGENT 正在持续记录观察、决策、工具与结果。", - passed: "E01–E09:Agent Trace 完整,可回放每次决策与工具调用。", - }, - { - idle: "E04 尚未生成代码变更。", - passed: "E04:Patch +5 −3,与根因假设 H1 建立明确关联。", - }, - { - idle: "E05 尚无构建产物。", - passed: "E05:Keil 0 errors、0 warnings,产物摘要已封存。", - }, - { - idle: "E06 尚未操作真实目标板。", - passed: "E06:CMSIS-DAP 下载、校验和复位通过,产物摘要一致。", - }, - { - idle: "E07 尚未采集运行事实。", - passed: "E07:UART 连续 128 samples,捕获稳定且 overflow=0。", - }, - { - idle: "E08 尚未获得独立物理事实。", - active: "E08:AGENT 正在调用 ioProbe 采集 FREQ_OUT。", - passed: "E08:ioProbe 999.8 Hz,误差 −0.02%,满足阈值。", - failed: "E08:ioProbe 972.4 Hz,误差 −2.76%,推翻完成假设。", - }, - { - idle: "E09 尚未形成聚合裁决。", - passed: "E09:完整证据链 Aggregate PASS,可封存回归 Case。", - blocked: "E09:Aggregate BLOCKED;AGENT 已拒绝封存并返回诊断。", - }, + { 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 = ` @@ -256,7 +370,7 @@ const PATCH_HTML = `
122+ (float)ticks - calibration.capture_offset;
123+ return timer_clock / calibrated_ticks;
124}
-
128+ // Evidence link: hypothesis H1 / action E04
+
128+ // Evidence: H1 / T04 / patch 8a91c2d
`; const app = document.querySelector("#app"); @@ -271,9 +385,17 @@ const currentAction = document.querySelector("#currentAction"); const nextAction = document.querySelector("#nextAction"); const confidenceMeter = document.querySelector("#confidenceMeter"); const confidenceLabel = document.querySelector("#confidenceLabel"); -const loopButtons = [...document.querySelectorAll("[data-loop]")]; -const timeline = document.querySelector("#agentTimeline"); -const eventCounter = document.querySelector("#eventCounter"); +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"); @@ -303,25 +425,96 @@ 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 shownEvents = []; -let selectedEventId = "E00"; +let selectedNodeId = "task"; let selectedEvidence = -1; -let currentLoop = "observe"; 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, 90) : milliseconds; + 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) => ` + + ${edge.label ? `${edge.label}` : ""} + `).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" ? `${node.icon}` : node.icon; + button.innerHTML = ` + ${icon} + ${node.code} · ${node.role} + ${node.title} + ${node.summary} + `; +} + function setRunState(state, label) { runState = state; app.dataset.runState = state; @@ -331,98 +524,65 @@ function setRunState(state, label) { saveCaseButton.disabled = state !== "passed"; } -function applyDecisionBoard(event) { - observedFact.textContent = event.observed; - currentHypothesis.textContent = event.hypothesis; - currentAction.textContent = event.action; - nextAction.textContent = event.next; - confidenceMeter.style.width = `${event.confidence}%`; - confidenceMeter.style.background = event.confidence >= 90 ? "var(--green)" : "var(--amber)"; - confidenceLabel.textContent = `${event.confidence >= 90 ? "证据" : "当前"} ${event.confidence}%`; +function setNodeStatus(id, status) { + const node = getNode(id); + if (!node) return; + node.status = status; + refreshNode(id); + updateGraphProgress(); + if (selectedNodeId === id) updateInspector(node); } -function setLoop(name, status = "active") { - if (currentLoop && currentLoop !== name) { - const previous = loopButtons.find((button) => button.dataset.loop === currentLoop); - if (previous?.dataset.status === "active") previous.dataset.status = "passed"; - } - currentLoop = name; - const button = loopButtons.find((item) => item.dataset.loop === name); - if (button) button.dataset.status = status; +function setEdgeStatus(id, status) { + const edge = getEdge(id); + if (!edge) return; + if (status === "idle") edge.removeAttribute("data-status"); + else edge.dataset.status = status; } -function completeLoop(name, status = "passed") { - const button = loopButtons.find((item) => item.dataset.loop === name); - if (button) button.dataset.status = status; +function updateGraphProgress() { + const touched = nodes.filter((node) => node.status !== "idle").length; + graphProgress.textContent = `${touched} / ${nodes.length} NODES`; } -function setHardwareTarget(target, command = "等待工具调用") { - app.dataset.hardwareTarget = target; - hardwareCommand.textContent = command; +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 eventClass(status) { - if (status === "active") return "is-active"; - if (status === "failed" || status === "blocked") return "is-failed"; - if (status === "passed") return "is-passed"; - return ""; -} - -function appendEvent(event, status = "active") { - const eventState = { ...event, status }; - shownEvents.push(eventState); - - const item = document.createElement("li"); - item.className = `timeline-event ${eventClass(status)}`.trim(); - item.dataset.eventId = event.id; - const button = document.createElement("button"); - button.type = "button"; - button.innerHTML = ` - ${event.id} · ${event.actor} - - +${String(shownEvents.length - 1).padStart(2, "0")}:${String((shownEvents.length - 1) * 2).padStart(2, "0")} - ${event.title} - ${event.summary} - ${event.tool} - `; - button.addEventListener("click", () => selectEvent(event.id)); - item.append(button); - timeline.append(item); - eventCounter.textContent = `${String(shownEvents.length).padStart(2, "0")} EVENTS`; - selectEvent(event.id, false); - timeline.scrollTop = timeline.scrollHeight; -} - -function updateEventStatus(eventId, status) { - const event = shownEvents.find((item) => item.id === eventId); - if (event) event.status = status; - const item = timeline.querySelector(`[data-event-id="${eventId}"]`); - if (!item) return; - item.classList.remove("is-active", "is-passed", "is-failed"); - const className = eventClass(status); - if (className) item.classList.add(className); -} - -function selectEvent(eventId, switchView = true) { - const event = shownEvents.find((item) => item.id === eventId); - if (!event) return; - selectedEventId = eventId; - timeline.querySelectorAll("button").forEach((button) => { - button.classList.toggle("is-selected", button.parentElement?.dataset.eventId === eventId); +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"); + } +} - detailEventId.textContent = `${event.id} · ${event.actor} · ${event.phase.toUpperCase()}`; - detailTitle.textContent = event.title; - detailSummary.textContent = event.summary; - detailInput.textContent = event.input; - detailDecision.textContent = event.decision; - detailTool.textContent = event.tool; - detailOutput.textContent = event.output; - activeTool.textContent = event.tool; - toolStatus.textContent = event.status === "active" ? "RUNNING" : event.status.toUpperCase(); - toolOutput.textContent = event.log; - - if (switchView) selectTab(event.patch ? "patch" : event.actor === "AGENT" ? "decision" : "tool"); +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) { @@ -441,7 +601,7 @@ function selectTab(tab) { function revealPatch() { patchGenerated = true; - patchAction.textContent = "E04 · H1 · frequency.c"; + patchAction.textContent = "T04 · H1 · frequency.c"; patchScore.textContent = "+5 −3 · REVIEWED"; codeDiff.innerHTML = PATCH_HTML; } @@ -464,6 +624,55 @@ function showEvidence(index) { 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); @@ -477,12 +686,10 @@ function drawScope(phase = 0) { 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; @@ -491,7 +698,6 @@ function drawScope(phase = 0) { 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); @@ -504,7 +710,6 @@ function drawScope(phase = 0) { scopeContext.lineTo(cssWidth, y); scopeContext.stroke(); } - const idle = scopeMode === "idle"; const failed = scopeMode === "failed"; const amplitude = cssHeight * 0.28; @@ -536,23 +741,109 @@ function animateScope(timestamp = 0) { 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 Hz"; + 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 Hz"; + 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; - shownEvents = []; - selectedEventId = "E00"; + nodes = NODE_DEFINITIONS.map((node) => ({ ...node, status: "idle" })); + selectedNodeId = "task"; selectedEvidence = -1; - currentLoop = "observe"; patchGenerated = false; - timeline.innerHTML = ""; - loopButtons.forEach((button) => button.removeAttribute("data-status")); - loopButtons.find((button) => button.dataset.loop === "observe").dataset.status = "active"; + 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 = '
NO PATCH YET

AGENT 必须先获得工程事实并验证假设,代码变更才会出现。

'; @@ -563,12 +854,14 @@ function resetDemo({ keepFailure = true } = {}) { benchVerdict.textContent = "SESSION BOUND"; boardFact.textContent = "已绑定 · 待操作"; uartFact.textContent = "等待固件"; + canvasStage.textContent = "READY"; setHardwareTarget("none"); setScopeMode("idle"); setRunState("idle", "已接管 · 等待执行"); - applyDecisionBoard(INITIAL_EVENT); - appendEvent(INITIAL_EVENT, "idle"); + applyDecisionBoard(getNode("task")); + selectNode("task", false); selectTab("decision"); + window.setTimeout(fitGraph, 0); } async function runDemo() { @@ -576,113 +869,57 @@ async function runDemo() { resetDemo(); const token = runToken; const shouldFail = failureMode.checked; - - setRunState("running", "AGENT 正在观察工程"); - updateEventStatus("E00", "passed"); - setEvidenceStatus(0, "passed"); + setRunState("running", "AGENT 正在执行工作流"); setEvidenceStatus(1, "active"); - for (const event of RUN_EVENTS) { - if (token !== runToken) return; - setLoop(event.phase); - applyDecisionBoard(event); - appendEvent(event, "active"); - setHardwareTarget(event.target, event.target === "none" ? `${event.tool} · local action` : `${event.tool} · command dispatched`); - agentState.textContent = `${event.id} · ${event.title}`; - - if (event.patch) { - revealPatch(); - selectTab("patch"); - } else if (event.actor !== "AGENT") { - selectTab("tool"); - } else { - selectTab("decision"); - } - - if (event.id === "E05") boardFact.textContent = "固件已构建 · 待写入"; - if (event.id === "E06") boardFact.textContent = "正在写入固件"; - if (event.id === "E07") { - boardFact.textContent = "固件运行中"; - uartFact.textContent = "采集 128 samples"; - } - if (event.id === "E08") { - scopeState.textContent = "AGENT 正在采样"; - setEvidenceStatus(6, "active"); - setScopeMode("sampling"); - } - - const continued = await wait(event.id === "E08" ? 720 : 520, token); - if (!continued) return; - - if (event.id === "E08" && shouldFail) { - updateEventStatus(event.id, "failed"); - const failedMeasurement = shownEvents.find((item) => item.id === event.id); - failedMeasurement.output = "ioProbe 972.4 Hz;误差 −2.76%;超出阈值"; - failedMeasurement.log = "$ ioprobe.measure --channel CH1 --metric frequency\narm FREQ_OUT\nsample physical waveform\nactual 972.4 Hz\nerror -2.76%\nresult OUTSIDE TOLERANCE"; - setEvidenceStatus(6, "failed"); - actualFrequency.innerHTML = "972.4 Hz"; - frequencyError.textContent = "−2.76%"; - scopeState.textContent = "物理阈值超限"; - benchVerdict.textContent = "PHYSICAL FAIL"; - setScopeMode("failed"); - selectEvent(event.id, false); - break; - } - - if (event.id === "E08") { - const passedMeasurement = shownEvents.find((item) => item.id === event.id); - passedMeasurement.output = "ioProbe 999.8 Hz;误差 −0.02%;满足阈值"; - passedMeasurement.log = "$ ioprobe.measure --channel CH1 --metric frequency\narm FREQ_OUT\nsample physical waveform\nactual 999.8 Hz\nerror -0.02%\nresult WITHIN TOLERANCE"; - actualFrequency.innerHTML = "999.8 Hz"; - frequencyError.textContent = "−0.02%"; - scopeState.textContent = "独立测量通过"; - setScopeMode("passed"); - } - - updateEventStatus(event.id, "passed"); - if (event.id === "E04") setEvidenceStatus(2, "passed"); - if (event.evidence !== undefined) setEvidenceStatus(event.evidence, "passed"); - selectEvent(event.id, false); + 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; } - if (token !== runToken) return; - const verdict = shouldFail ? FAILURE_EVENT : SUCCESS_EVENT; - setLoop("reflect"); - applyDecisionBoard(verdict); - appendEvent(verdict, "active"); - selectTab("decision"); - setHardwareTarget("none", shouldFail ? "physical fact rejected completion" : "evidence aggregation"); - agentState.textContent = shouldFail ? "AGENT 正在拒绝错误结果" : "AGENT 正在聚合证据"; - - const continued = await wait(620, token); - if (!continued) return; - - updateEventStatus(verdict.id, "passed"); - completeLoop("reflect", "passed"); - setEvidenceStatus(1, "passed"); + 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"); - setLoop("hypothesize", "active"); - applyDecisionBoard(FAILURE_EVENT); benchVerdict.textContent = "BLOCKED · BACK TO DIAGNOSIS"; boardFact.textContent = "保持现场 · 等待新计划"; uartFact.textContent = "软件事实保留"; - setRunState("failed", "拒绝封存 · 已返回诊断"); - showEvidence(7); - } else { - setEvidenceStatus(7, "passed", "Verdict PASS"); - actualFrequency.innerHTML = "999.8 Hz"; - frequencyError.textContent = "−0.02%"; - scopeState.textContent = "独立测量通过"; - benchVerdict.textContent = "AGGREGATE PASS"; - boardFact.textContent = "验证完成 · 固件保留"; - uartFact.textContent = "稳定 · 128 samples"; - setScopeMode("passed"); - setRunState("passed", "证据完整 · Aggregate PASS"); + setRunState("failed", "拒绝封存 · 已回到 Read Workspace"); showEvidence(7); + return; } - selectEvent(verdict.id, false); + + 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) { @@ -695,29 +932,56 @@ function showToast(message) { tabButtons.forEach((button) => { button.addEventListener("click", () => selectTab(button.dataset.tab)); }); - -loopButtons.forEach((button) => { - button.addEventListener("click", () => { - const matching = [...shownEvents].reverse().find((event) => event.phase === button.dataset.loop); - if (matching) selectEvent(matching.id); - }); -}); - 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 ? "失败演示开启:AGENT 将拒绝封存并返回诊断" : "已恢复通过路径"); + showToast(failureMode.checked ? "FAIL 分支已启用:物理失败后将回环到 Read Workspace" : "已恢复 PASS 分支"); }); saveCaseButton.addEventListener("click", () => { if (runState !== "passed") return; - showToast("回归 Case 已封存:freq-1khz-accuracy · Agent Trace + Physical Evidence"); + 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); }); -window.addEventListener("resize", () => drawScope(0)); -window.addEventListener("load", () => drawScope(0)); resetDemo({ keepFailure: false }); diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html b/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html index e4414149..19397611 100644 --- a/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html +++ b/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html @@ -135,32 +135,56 @@ - - -
-
-
+
+
+
- LIVE AGENT TRACE -

行动时间线

+ AGENT EXECUTION GRAPH +

任务到物理证据的执行画布

+
+
+ 0 / 14 NODES + READY +
+
+ + +
- 01 EVENT
-
    + +
    +
    + +
    +
    +
    + +
    + AGENT 决策 + 工具调用 + 真实硬件 + 控制 / 裁决 + 重新诊断回环 + 拖动画布 · 点击节点检查输入输出 +
    -
    +
    -
    + +