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 YETAGENT 必须先获得工程事实并验证假设,代码变更才会出现。
';
@@ -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 @@
-
-
-
-
+
+