docs: add HWLAB first-principles showcase
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / unidesk-host- Success

This commit is contained in:
Codex
2026-07-10 18:25:29 +02:00
parent a29bc218e6
commit 7e2e8fbbc3
4 changed files with 2180 additions and 0 deletions
@@ -0,0 +1,404 @@
const STAGES = [
{
title: "分析工程",
summary: "识别 TIM2 输入捕获路径与 APB 时钟倍频条件。",
log: "scan TIM2 capture path · APB1 prescaler = 2",
},
{
title: "修改代码",
summary: "修正定时器时钟计算,并注入实板标定偏移。",
log: "patch Core/Src/frequency.c · +5 -3",
},
{
title: "Keil 编译",
summary: "使用受控工程配置生成可追溯固件产物。",
log: "build 0 error(s), 0 warning(s) · freq.axf",
},
{
title: "下载固件",
summary: "通过 CMSIS-DAP 将本次构建产物写入目标板。",
log: "flash 32.18 KiB · verify OK · reset target",
},
{
title: "UART 采集",
summary: "读取固件运行状态,确认输入捕获链路稳定。",
log: "uart capture stable · sample_count = 128",
},
{
title: "物理测量",
summary: "由 ioProbe 独立测量 FREQ_OUT,形成物理事实。",
log: "probe CH1 frequency · 999.8 Hz · error -0.02%",
},
];
const EVIDENCE = [
{
idle: "任务尚未进入受控执行。",
passed: "任务 freq-1khz-accuracy 已绑定 Workspace、Board、Probe 与目标指标。",
},
{
idle: "尚无 Agent 执行 trace。",
active: "Agent 正在受控工程上下文中分析、修改与执行。",
passed: "Agent trace 已封存:工程分析与代码变更均可回放。",
},
{
idle: "尚无本次构建产物。",
passed: "Keil 构建成功:0 error(s)0 warning(s),产物摘要已记录。",
},
{
idle: "目标板尚未写入本次固件。",
passed: "CMSIS-DAP 下载与校验通过,固件摘要与构建产物一致。",
},
{
idle: "UART 运行事实尚未采集。",
passed: "UART 连续采样 128 次,输入捕获状态稳定,无溢出。",
},
{
idle: "ioProbe 物理测量尚未开始。",
active: "ioProbe 正在独立采集 FREQ_OUT 波形。",
passed: "ioProbe 实测 999.8 Hz,误差 0.02%,满足 ±0.10% 阈值。",
failed: "ioProbe 实测 972.4 Hz,误差 2.76%,超出 ±0.10% 阈值。",
},
{
idle: "Aggregate 尚未汇总,不能保存为回归 Case。",
passed: "代码、构建、下载、运行与物理测量证据完整:Aggregate PASS。",
blocked: "物理测量未通过:Aggregate BLOCKED,阻止错误结果进入回归集。",
},
];
const stageButtons = [...document.querySelectorAll("[data-stage]")];
const evidenceButtons = [...document.querySelectorAll("[data-evidence]")];
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 runState = document.querySelector("#runState");
const stageDetail = document.querySelector("#stageDetail");
const runLog = document.querySelector("#runLog");
const traceCounter = document.querySelector("#traceCounter");
const evidenceDetail = document.querySelector("#evidenceDetail");
const actualFrequency = document.querySelector("#actualFrequency");
const frequencyError = document.querySelector("#frequencyError");
const scopeStatus = document.querySelector("#scopeStatus");
const benchState = document.querySelector("#benchState");
const uartOutput = document.querySelector("#uartOutput");
const uartIndicator = document.querySelector("#uartIndicator");
const toast = document.querySelector("#toast");
const scopeCanvas = document.querySelector("#scopeCanvas");
const scopeContext = scopeCanvas.getContext("2d");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
let runToken = 0;
let currentState = "idle";
let selectedEvidence = -1;
let selectedStage = -1;
let scopeAnimationFrame = 0;
let toastTimer = 0;
function delay(milliseconds, token) {
const duration = reducedMotion.matches ? Math.min(milliseconds, 120) : milliseconds;
return new Promise((resolve) => {
window.setTimeout(() => resolve(token === runToken), duration);
});
}
function setRunState(state, label) {
currentState = state;
app.dataset.runState = state;
runState.textContent = label;
runButton.disabled = state === "running";
failureMode.disabled = state === "running";
saveCaseButton.disabled = state !== "passed";
if (state === "running" && !scopeAnimationFrame) {
animateScope();
}
}
function setStageStatus(index, status) {
const button = stageButtons[index];
if (!button) return;
if (status === "idle") {
button.removeAttribute("data-status");
return;
}
button.dataset.status = status;
}
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 selectStage(index) {
selectedStage = index;
stageButtons.forEach((button, buttonIndex) => {
button.classList.toggle("is-selected", buttonIndex === index);
});
const stage = STAGES[index];
if (!stage) {
stageDetail.innerHTML = `
<span class="detail-index">READY</span>
<h3>等待开始工程闭环</h3>
<p>执行过程将绑定代码、构建、下载、串口与物理测量事实。</p>
`;
return;
}
const status = stageButtons[index].dataset.status || "idle";
const statusLabel = {
idle: "PENDING",
active: "RUNNING",
passed: "VERIFIED",
failed: "FAILED",
}[status];
stageDetail.innerHTML = `
<span class="detail-index">${String(index + 1).padStart(2, "0")} · ${statusLabel}</span>
<h3>${stage.title}</h3>
<p>${stage.summary}</p>
`;
}
function showEvidence(index) {
selectedEvidence = index;
evidenceButtons.forEach((button, buttonIndex) => {
button.classList.toggle("is-selected", buttonIndex === index);
});
const status = evidenceButtons[index]?.dataset.status || "idle";
const detail = EVIDENCE[index]?.[status] || EVIDENCE[index]?.idle;
evidenceDetail.textContent = detail || "暂无证据详情。";
}
function appendLog(message, tone = "") {
const line = document.createElement("span");
line.className = tone ? `log-${tone}` : "";
line.textContent = `${runLog.textContent.trim() ? "\n" : ""}${message}`;
runLog.append(line);
runLog.scrollTop = runLog.scrollHeight;
}
function resetDemo({ keepFailure = true } = {}) {
runToken += 1;
window.cancelAnimationFrame(scopeAnimationFrame);
scopeAnimationFrame = 0;
if (!keepFailure) failureMode.checked = false;
stageButtons.forEach((button) => {
button.removeAttribute("data-status");
button.classList.remove("is-selected");
});
evidenceButtons.forEach((button, index) => {
button.removeAttribute("data-status");
button.classList.remove("is-selected");
if (index === 5) button.querySelector("span").textContent = "ioProbe 待测";
if (index === 6) button.querySelector("span").textContent = "Aggregate 待定";
});
selectedStage = -1;
selectedEvidence = -1;
traceCounter.textContent = "0 / 6";
runLog.innerHTML = '<span class="log-muted">$ hwlab case run freq-1khz-accuracy</span>\n<span class="log-muted"> 尚未执行</span>';
evidenceDetail.textContent = "选择证据节点查看边界事实。";
actualFrequency.innerHTML = "— <small>Hz</small>";
frequencyError.textContent = "—";
scopeStatus.textContent = "等待采集";
benchState.textContent = "设备待命";
uartOutput.textContent = "[idle] waiting for firmware\n[idle] measurement channel ready";
uartIndicator.classList.remove("is-active");
setRunState("idle", "等待指令");
selectStage(-1);
drawScope(0);
}
async function runDemo() {
if (currentState === "running") return;
resetDemo();
const token = runToken;
const shouldFail = failureMode.checked;
setRunState("running", "闭环执行中");
benchState.textContent = "HWPOD 已锁定";
scopeStatus.textContent = "同步设备";
runLog.innerHTML = "";
appendLog("$ hwlab case run freq-1khz-accuracy", "muted");
setEvidenceStatus(0, "passed");
setEvidenceStatus(1, "active");
for (let index = 0; index < STAGES.length; index += 1) {
if (token !== runToken) return;
setStageStatus(index, "active");
selectStage(index);
traceCounter.textContent = `${index} / 6`;
appendLog(`${STAGES[index].title}`, "active");
if (index === 3) benchState.textContent = "正在写入固件";
if (index === 4) {
benchState.textContent = "固件运行中";
uartIndicator.classList.add("is-active");
uartOutput.textContent = "[boot] 71-FREQ rev.C\n[capture] timer input ready";
}
if (index === 5) {
benchState.textContent = "ioProbe 采集中";
scopeStatus.textContent = "采集中";
setEvidenceStatus(5, "active", "ioProbe 采集中");
}
const shouldContinue = await delay(index === 5 ? 850 : 650, token);
if (!shouldContinue) return;
if (index === 5 && shouldFail) {
setStageStatus(index, "failed");
appendLog("probe CH1 frequency · 972.4 Hz · outside tolerance", "fail");
traceCounter.textContent = "5 / 6";
setEvidenceStatus(5, "failed", "ioProbe 972.4 Hz");
setEvidenceStatus(6, "blocked", "Aggregate BLOCKED");
actualFrequency.innerHTML = "972.4 <small>Hz</small>";
frequencyError.textContent = "2.76%";
scopeStatus.textContent = "阈值超限";
benchState.textContent = "物理验证未通过";
uartOutput.textContent = "[capture] stable, samples=128\n[result] freq=972.4Hz, tolerance=FAIL";
setRunState("failed", "证据阻断");
selectStage(index);
showEvidence(6);
drawScope(0);
return;
}
setStageStatus(index, "passed");
appendLog(STAGES[index].log, "pass");
traceCounter.textContent = `${index + 1} / 6`;
selectStage(index);
if (index === 1) setEvidenceStatus(1, "passed");
if (index === 2) setEvidenceStatus(2, "passed");
if (index === 3) setEvidenceStatus(3, "passed");
if (index === 4) {
setEvidenceStatus(4, "passed");
uartOutput.textContent = "[capture] stable, samples=128\n[timer] clock=72000000Hz, overflow=0";
}
}
setEvidenceStatus(5, "passed", "ioProbe 999.8 Hz");
setEvidenceStatus(6, "passed", "Aggregate PASS");
actualFrequency.innerHTML = "999.8 <small>Hz</small>";
frequencyError.textContent = "0.02%";
scopeStatus.textContent = "测量通过";
benchState.textContent = "验证完成 · PASS";
uartOutput.textContent = "[capture] stable, samples=128\n[result] freq=999.8Hz, tolerance=PASS";
appendLog("verdict Aggregate PASS · evidence sealed", "pass");
setRunState("passed", "Aggregate PASS");
showEvidence(6);
drawScope(0);
}
function showToast(message) {
window.clearTimeout(toastTimer);
toast.textContent = message;
toast.classList.add("is-visible");
toastTimer = window.setTimeout(() => toast.classList.remove("is-visible"), 2600);
}
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 = "#0c1418";
scopeContext.fillRect(0, 0, cssWidth, cssHeight);
scopeContext.strokeStyle = "rgba(94, 132, 143, 0.18)";
scopeContext.lineWidth = 1;
for (let x = 0; x <= cssWidth; x += cssWidth / 10) {
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 amplitude = cssHeight * 0.27;
const center = cssHeight * 0.5;
const period = currentState === "failed" ? 51 : 43;
const waveColor = currentState === "failed" ? "#ff766e" : "#53dce1";
const muted = currentState === "idle";
scopeContext.beginPath();
for (let x = 0; x <= cssWidth; x += 2) {
const signal = muted
? Math.sin(x / 25) * 1.2
: Math.sin((x + phase) * ((Math.PI * 2) / period)) * amplitude;
const noise = currentState === "running" ? Math.sin((x + phase * 1.9) / 5) * 0.8 : 0;
const y = center + signal + noise;
if (x === 0) scopeContext.moveTo(x, y);
else scopeContext.lineTo(x, y);
}
scopeContext.strokeStyle = muted ? "#496169" : waveColor;
scopeContext.lineWidth = 1.4;
scopeContext.shadowColor = muted ? "transparent" : waveColor;
scopeContext.shadowBlur = muted ? 0 : 5;
scopeContext.stroke();
scopeContext.shadowBlur = 0;
}
function animateScope(timestamp = 0) {
if (currentState !== "running") {
scopeAnimationFrame = 0;
return;
}
drawScope(timestamp * 0.08);
scopeAnimationFrame = window.requestAnimationFrame(animateScope);
}
stageButtons.forEach((button, index) => {
button.addEventListener("click", () => selectStage(index));
});
evidenceButtons.forEach((button, index) => {
button.addEventListener("click", () => showEvidence(index));
});
runButton.addEventListener("click", runDemo);
resetButton.addEventListener("click", () => resetDemo({ keepFailure: true }));
failureMode.addEventListener("change", () => {
if (currentState !== "idle") resetDemo({ keepFailure: true });
showToast(failureMode.checked ? "失败演示已开启:物理测量将阻断聚合结果" : "已恢复通过路径");
});
saveCaseButton.addEventListener("click", () => {
if (currentState !== "passed") return;
showToast("回归 Case 已保存:freq-1khz-accuracy / evidence sealed");
});
window.addEventListener("resize", () => drawScope(0));
window.addEventListener("load", () => drawScope(0));
resetDemo({ keepFailure: false });
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@@ -0,0 +1,251 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="HWLAB 第一性原理概念展示:从工程任务到真实硬件证据闭环。"
/>
<title>HWLAB · 真实硬件研发闭环</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="app-shell" id="app" data-run-state="idle">
<header class="topbar">
<div class="brand-block" aria-label="HWLAB">
<span class="brand-mark" aria-hidden="true">HW</span>
<span class="brand-name">HWLAB</span>
<span class="brand-divider" aria-hidden="true"></span>
<span class="brand-tagline">云端真实硬件研发闭环</span>
</div>
<div class="target-context" aria-label="当前目标">
<span class="context-label">TARGET</span>
<strong>71-FREQ / STM32F103</strong>
</div>
<div class="system-status" aria-label="系统状态">
<span class="status-pill"><i class="status-dot"></i>NC01 · v03</span>
<span class="status-pill status-pill--hardware"><i class="status-dot"></i>HWPOD 已占用</span>
<span class="status-pill"><i class="status-dot"></i>Session 就绪</span>
</div>
</header>
<main class="workspace">
<aside class="task-panel" aria-label="项目与任务上下文">
<div class="task-panel-heading">
<span class="eyebrow">PROJECT</span>
<div class="project-copy">
<h1>71-FREQ</h1>
<p>信号调理模块</p>
</div>
<span class="project-code" aria-hidden="true">71</span>
</div>
<nav class="task-list" aria-label="研发任务">
<button class="task-item task-item--active" type="button" title="修复 1 kHz 输入频率误差">
<span class="task-marker">01</span>
<span class="task-text">
<strong>修复 1 kHz 输入频率误差</strong>
<small><i class="state-dot state-dot--active"></i>等待验证</small>
</span>
</button>
<button class="task-item" type="button" title="校准输入增益曲线">
<span class="task-marker">02</span>
<span class="task-text">
<strong>校准输入增益曲线</strong>
<small><i class="state-dot state-dot--done"></i>已完成</small>
</span>
</button>
<button class="task-item" type="button" title="验证低频抗抖动策略">
<span class="task-marker">03</span>
<span class="task-text">
<strong>验证低频抗抖动策略</strong>
<small><i class="state-dot"></i>待排期</small>
</span>
</button>
</nav>
<div class="context-facts">
<span class="eyebrow">BOUND CONTEXT</span>
<dl>
<div>
<dt>Workspace</dt>
<dd>CONSTAR_workspace</dd>
</div>
<div>
<dt>Board</dt>
<dd>STM32F103</dd>
</div>
<div>
<dt>Probe</dt>
<dd>CMSIS-DAP</dd>
</div>
<div>
<dt>Case</dt>
<dd>freq-1khz-accuracy</dd>
</div>
</dl>
</div>
</aside>
<section class="agent-panel" aria-labelledby="agent-title">
<div class="panel-title-row">
<div>
<span class="eyebrow">ENGINEERING AGENT</span>
<h2 id="agent-title">受控工程工作台</h2>
</div>
<span class="run-state" id="runState" aria-live="polite">等待指令</span>
</div>
<div class="command-bar" aria-label="工程指令">
<span class="command-symbol" aria-hidden="true">_</span>
<p>修复 1 kHz 输入下的频率误差,并在真实板上完成验证</p>
<label class="failure-toggle" title="演示物理测量未通过时的阻断状态">
<input id="failureMode" type="checkbox" />
<span class="toggle-track" aria-hidden="true"><i></i></span>
<span>失败演示</span>
</label>
<button class="icon-button" id="resetButton" type="button" title="重置演示" aria-label="重置演示">
</button>
<button class="run-button" id="runButton" type="button">
<span aria-hidden="true"></span>
运行验证
</button>
</div>
<ol class="stage-strip" aria-label="执行阶段">
<li><button type="button" data-stage="0"><span>01</span><strong>分析工程</strong><i></i></button></li>
<li><button type="button" data-stage="1"><span>02</span><strong>修改代码</strong><i></i></button></li>
<li><button type="button" data-stage="2"><span>03</span><strong>Keil 编译</strong><i></i></button></li>
<li><button type="button" data-stage="3"><span>04</span><strong>下载固件</strong><i></i></button></li>
<li><button type="button" data-stage="4"><span>05</span><strong>UART 采集</strong><i></i></button></li>
<li><button type="button" data-stage="5"><span>06</span><strong>物理测量</strong><i></i></button></li>
</ol>
<div class="work-surface">
<section class="editor-pane" aria-label="代码变更">
<header class="surface-header">
<div class="file-tabs">
<button class="file-tab file-tab--active" type="button">frequency.c <i></i></button>
<button class="file-tab" type="button">timer_capture.h</button>
</div>
<span class="change-summary"><b>+5</b> <em>3</em></span>
</header>
<div class="code-view" role="region" aria-label="frequency.c 代码差异" tabindex="0">
<div class="code-line"><span>118</span><code>static float capture_to_hz(uint32_t ticks)</code></div>
<div class="code-line"><span>119</span><code>{</code></div>
<div class="code-line code-line--removed"><span>120</span><code>- return TIMER_CLOCK_HZ / ticks;</code></div>
<div class="code-line code-line--removed"><span>121</span><code>- // APB prescaler assumed to be 1</code></div>
<div class="code-line code-line--added"><span>120</span><code>+ const uint32_t timer_clock = get_timer_clock_hz();</code></div>
<div class="code-line code-line--added"><span>121</span><code>+ const float calibrated_ticks =</code></div>
<div class="code-line code-line--added"><span>122</span><code>+ (float)ticks - capture_offset_ticks;</code></div>
<div class="code-line code-line--added"><span>123</span><code>+ return timer_clock / calibrated_ticks;</code></div>
<div class="code-line"><span>124</span><code>}</code></div>
<div class="code-line"><span>125</span><code></code></div>
<div class="code-line code-line--context"><span>126</span><code>float frequency_read(void)</code></div>
<div class="code-line code-line--context"><span>127</span><code>{</code></div>
<div class="code-line code-line--added"><span>128</span><code>+ capture_offset_ticks = calibration.capture_offset;</code></div>
<div class="code-line code-line--context"><span>129</span><code> return capture_to_hz(last_capture_ticks);</code></div>
<div class="code-line code-line--context"><span>130</span><code>}</code></div>
</div>
</section>
<aside class="trace-pane" aria-label="Agent 执行记录">
<header class="surface-header">
<strong>执行记录</strong>
<span id="traceCounter">0 / 6</span>
</header>
<div class="stage-detail" id="stageDetail">
<span class="detail-index">READY</span>
<h3>等待开始工程闭环</h3>
<p>执行过程将绑定代码、构建、下载、串口与物理测量事实。</p>
</div>
<pre class="run-log" id="runLog" aria-live="polite"><span class="log-muted">$ hwlab case run freq-1khz-accuracy</span>
<span class="log-muted"> 尚未执行</span></pre>
</aside>
</div>
</section>
<aside class="hwpod-panel" aria-labelledby="hwpod-title">
<div class="hwpod-heading">
<div>
<span class="eyebrow">REAL HARDWARE</span>
<h2 id="hwpod-title">HWPOD 现场</h2>
</div>
<span class="live-indicator"><i></i>LIVE</span>
</div>
<figure class="bench-view">
<img
src="./assets/hwpod-bench.png"
alt="STM32 开发板、调试探针与 ioProbe 测量设备组成的真实硬件台架"
/>
<figcaption class="sr-only">当前任务绑定的真实 HWPOD 硬件现场</figcaption>
<span class="hardware-label hardware-label--board"><i></i>STM32F103</span>
<span class="hardware-label hardware-label--probe"><i></i>CMSIS-DAP</span>
<span class="hardware-label hardware-label--ioprobe"><i></i>ioProbe</span>
<span class="bench-state" id="benchState">设备待命</span>
</figure>
<section class="measurement-panel" aria-label="物理测量结果">
<div class="scope-header">
<span>CH1 · FREQ_OUT</span>
<strong id="scopeStatus">等待采集</strong>
</div>
<canvas id="scopeCanvas" aria-label="ioProbe 输出波形"></canvas>
<dl class="measurement-grid">
<div>
<dt>目标频率</dt>
<dd>1000.0 <small>Hz</small></dd>
</div>
<div>
<dt>实测频率</dt>
<dd id="actualFrequency"><small>Hz</small></dd>
</div>
<div>
<dt>测量误差</dt>
<dd id="frequencyError"></dd>
</div>
</dl>
</section>
<section class="uart-panel" aria-label="UART 现场输出">
<header>
<span>UART2 · 115200</span>
<i id="uartIndicator"></i>
</header>
<pre id="uartOutput">[idle] waiting for firmware
[idle] measurement channel ready</pre>
</section>
</aside>
</main>
<footer class="evidence-band" aria-labelledby="evidence-title">
<div class="evidence-heading">
<div>
<span class="eyebrow">HARNESSRL EVIDENCE</span>
<h2 id="evidence-title">可审计证据链</h2>
</div>
<p id="evidenceDetail">选择证据节点查看边界事实。</p>
<button id="saveCaseButton" type="button" disabled>保存为回归 Case</button>
</div>
<ol class="evidence-chain">
<li><button type="button" data-evidence="0"><i></i><span>任务接收</span><small>Task</small></button></li>
<li><button type="button" data-evidence="1"><i></i><span>Agent 执行</span><small>Trace</small></button></li>
<li><button type="button" data-evidence="2"><i></i><span>Build 通过</span><small>Artifact</small></button></li>
<li><button type="button" data-evidence="3"><i></i><span>Flash 通过</span><small>Probe</small></button></li>
<li><button type="button" data-evidence="4"><i></i><span>UART 正常</span><small>Runtime</small></button></li>
<li><button type="button" data-evidence="5"><i></i><span>ioProbe 待测</span><small>Physical</small></button></li>
<li class="evidence-final"><button type="button" data-evidence="6"><i></i><span>Aggregate 待定</span><small>Verdict</small></button></li>
</ol>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="./app.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff