From 92799a006adff943f55ca348bcfcef7540620bb0 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Fri, 22 May 2026 14:08:56 +0000 Subject: [PATCH 1/3] feat: enrich cloud workbench hardware status --- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 37 ++++ web/hwlab-cloud-web/app.mjs | 188 +++++++++++++++--- web/hwlab-cloud-web/index.html | 2 +- web/hwlab-cloud-web/scripts/check.mjs | 24 +++ web/hwlab-cloud-web/styles.css | 94 ++++++++- 5 files changed, 314 insertions(+), 31 deletions(-) diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index f3460439..beab3c41 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -73,6 +73,21 @@ const requiredWiringColumns = Object.freeze([ "trace/evidence" ]); +const requiredHardwareStatusTerms = Object.freeze([ + "hardware-source-status", + "hardwareGroup", + "hardwareRow", + "Gateway/Box 在线", + "BOX-SIMU 端口", + "Patch Panel 连线", + "DO1 -> patch-panel -> DI1", + "AI/AO/FREQ", + "future capability", + "SOURCE", + "DEV-LIVE", + "BLOCKED" +]); + const workbenchMarkers = Object.freeze([ "data-app-shell", "workbench-shell", @@ -192,6 +207,11 @@ function runStaticSmoke() { evidence: requiredWiringColumns }); + addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", { + blocker: "contract_blocker", + evidence: requiredHardwareStatusTerms + }); + addCheck(checks, blockers, "outer-scroll-contract", hasScrollLockContract(files.styles), "Outer page scrolling is locked and internal workbench panes own scrolling.", { blocker: "contract_blocker", evidence: ["html/body overflow hidden", "workbench-shell 100vh/100dvh overflow hidden", ".view overflow auto"] @@ -427,6 +447,23 @@ function wiringTableContract(html) { return requiredWiringColumns.every((column) => wiringPanel.includes(`${column}`)); } +function hardwareStatusStructureContract({ html, app, styles }) { + const source = `${html}\n${app}\n${styles}`; + return ( + requiredHardwareStatusTerms.every((term) => source.includes(term)) && + /id=["']hardware-source-status["']/u.test(html) && + /function\s+renderHardwareStatus\s*\(/u.test(app) && + /function\s+hardwareGroup\s*\(/u.test(app) && + /function\s+hardwareRow\s*\(/u.test(app) && + /function\s+trustedM3Link\s*\(/u.test(app) && + /function\s+linkStatusLabel\s*\(/u.test(app) && + /function\s+normalizePort\s*\(/u.test(app) && + /Web 不提供硬件写 RPC;不会新增伪硬件写操作。/u.test(app) && + /\.hardware-section\s*\{/u.test(styles) && + /\.hardware-row\s*\{/u.test(styles) + ); +} + function finalRouteFallback(app) { const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? ""; const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index b7c98e57..c873f850 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -65,6 +65,7 @@ const el = { commandInput: byId("command-input"), commandSend: byId("command-send"), commandClear: byId("command-clear"), + hardwareSourceStatus: byId("hardware-source-status"), hardwareList: byId("hardware-list"), controlList: byId("control-list"), wiringBody: byId("wiring-body"), @@ -609,34 +610,167 @@ function renderUnblockList() { } function renderHardwareStatus(summary) { - const counts = summary?.counts ?? {}; - const cards = [ + const counts = summary?.counts ?? null; + const patchPanel = gateSummary.topology.patchPanel; + const liveLink = trustedM3Link(patchPanel.activeConnections); + const sourceKind = counts ? "DEV-LIVE" : "SOURCE"; + const sourceTone = counts ? "dev-live" : "source"; + const gatewayTone = counts ? "dev-live" : "source"; + const boxTone = counts ? "dev-live" : "source"; + const patchTone = patchPanel.state === "active" ? "live" : patchPanel.state; + const linkTone = counts && liveLink ? "live" : patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked"; + + el.hardwareSourceStatus.textContent = counts ? "DEV-LIVE" : "SOURCE"; + el.hardwareSourceStatus.className = `state-tag tone-${toneClass(sourceTone)}`; + + replaceChildren( + el.hardwareList, + hardwareGroup({ + title: "Gateway/Box 在线", + rows: [ + hardwareRow({ + label: "Gateway-SIMU", + value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看", + detail: counts + ? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime。` + : `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`, + sourceKind, + tone: gatewayTone + }), + hardwareRow({ + label: "BOX-SIMU", + value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看", + detail: counts + ? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime。` + : `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`, + sourceKind, + tone: boxTone + }) + ] + }), + hardwareGroup({ + title: "BOX-SIMU 端口", + rows: [ + hardwareRow({ + label: "DO1", + value: "not wired", + detail: counts ? "只读 runtime 未返回 DO1 电平;保持 SOURCE/DEV-LIVE 边界。" : "SOURCE 仅说明端口存在,不冒充实况。", + sourceKind: counts ? "DEV-LIVE" : "SOURCE", + tone: "blocked" + }), + hardwareRow({ + label: "DI1", + value: "not wired", + detail: counts ? "只读 runtime 未返回 DI1 回读;保持 SOURCE/DEV-LIVE 边界。" : "SOURCE 仅说明端口存在,不冒充实况。", + sourceKind: counts ? "DEV-LIVE" : "SOURCE", + tone: "blocked" + }), + hardwareRow({ + label: "AI/AO/FREQ", + value: "future capability", + detail: "当前未声明模拟量/频率闭环,按 future capability 处理。", + sourceKind: "SOURCE", + tone: "disabled" + }) + ] + }), + hardwareGroup({ + title: "Patch Panel 连线", + rows: [ + hardwareRow({ + label: "hwlab-patch-panel", + value: statusLabel(patchPanel.state), + detail: `${patchPanel.activeConnectionCount} 条 SOURCE 接线;当前 fixture 为 ${describePatchPanelConnections(patchPanel)}。`, + sourceKind: "SOURCE", + tone: patchTone + }), + hardwareRow({ + label: "DO1 -> patch-panel -> DI1", + value: linkStatusLabel(linkTone), + detail: m3LinkDetail(liveLink, patchPanel, counts), + sourceKind: counts && liveLink ? "DEV-LIVE" : "BLOCKED", + tone: linkTone + }) + ] + }), + hardwareGroup({ + title: "写入边界", + rows: [ + hardwareRow({ + label: "硬件控制", + value: "已禁用", + detail: "Web 不提供硬件写 RPC;不会新增伪硬件写操作。", + sourceKind: "SOURCE", + tone: "source" + }) + ] + }) + ); +} + +function hardwareGroup(group) { + const article = document.createElement("article"); + article.className = "hardware-section"; + article.append(textSpan(group.title, "hardware-section-title")); + const rows = document.createElement("div"); + rows.className = "hardware-rows"; + rows.append(...group.rows.map(hardwareRow)); + article.append(rows); + return article; +} + +function hardwareRow(item) { + const row = document.createElement("div"); + row.className = `hardware-row tone-border-${toneClass(item.tone)}`; + row.dataset.sourceKind = item.sourceKind; + const head = document.createElement("div"); + head.className = "hardware-row-head"; + head.append(textSpan(item.label, "hardware-row-label"), badge(item.sourceKind, item.sourceKind)); + row.append(head); + row.append(textSpan(item.value, `hardware-row-value tone-${toneClass(item.tone)}`)); + row.append(textSpan(item.detail, "hardware-row-detail")); + return row; +} + +function trustedM3Link(connections) { + return (connections ?? []).find( + (link) => + normalizePort(link.fromPort) === "DO1" && + normalizePort(link.toPort) === "DI1" && + String(link.fromResourceId ?? "").includes("boxsimu") && + String(link.toResourceId ?? "").includes("boxsimu") + ); +} + +function describePatchPanelConnections(patchPanel) { + if (!patchPanel.activeConnections.length) return "无活动连线"; + return patchPanel.activeConnections + .map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`) + .join(";"); +} + +function m3LinkDetail(liveLink, patchPanel, counts) { + if (liveLink && counts) { + return `DEV-LIVE runtime 仅证明只读观测,链路为 ${liveLink.fromResourceId}:${liveLink.fromPort} -> ${patchPanel.serviceId} -> ${liveLink.toResourceId}:${liveLink.toPort}。`; + } + if (patchPanel.activeConnectionCount > 0) { + return `SOURCE 仅看到 patch-panel 接线:${describePatchPanelConnections(patchPanel)};未冒充 live。`; + } + return "BLOCKED:当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。"; +} + +function linkStatusLabel(status) { + return ( { - title: "Gateway-SIMU", - value: valueOrUnavailable(counts.gatewaySessions, gateSummary.gatewaySessionCount), - meta: "只读数量;来源 SOURCE 回退", - tone: summary ? "dev-live" : "source" - }, - { - title: "BOX-SIMU", - value: valueOrUnavailable(counts.boxResources, gateSummary.boxResourceCount), - meta: "只读数量;来源 SOURCE 回退", - tone: summary ? "dev-live" : "source" - }, - { - title: "接线面板 hwlab-patch-panel", - value: statusLabel(gateSummary.topology.patchPanel.state), - meta: `${gateSummary.topology.patchPanel.activeConnectionCount} 条来源 SOURCE 接线`, - tone: gateSummary.blocked ? "blocked" : gateSummary.topology.patchPanel.state - }, - { - title: "硬件写入界面", - value: "已禁用", - meta: "Web 不提供硬件写 RPC", - tone: "source" - } - ]; - replaceChildren(el.hardwareList, ...cards.map(statusCard)); + live: "live", + degraded: "degraded", + blocked: "blocked" + }[status] ?? statusLabel(status) + ); +} + +function normalizePort(port) { + return String(port ?? "").trim().toUpperCase(); } function controlRows() { diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 45d56554..bbe74bed 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -211,7 +211,7 @@

运行界面

硬件状态:BOX-SIMU / Gateway-SIMU / hwlab-patch-panel

- 开发实况 DEV-LIVE 只读探测 + SOURCE
diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 6739512f..669f733c 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -77,6 +77,30 @@ for (const legacyEnglishTitle of [ assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle); } assert.match(html, /硬件状态:BOX-SIMU \/ Gateway-SIMU \/ hwlab-patch-panel/); +assert.match(html, /id="hardware-source-status"/); +assert.match(app, /function renderHardwareStatus/); +assert.match(app, /function hardwareGroup/); +assert.match(app, /function hardwareRow/); +assert.match(app, /function trustedM3Link/); +assert.match(app, /function linkStatusLabel/); +assert.match(app, /function normalizePort/); +for (const hardwareTerm of [ + "Gateway/Box 在线", + "BOX-SIMU 端口", + "Patch Panel 连线", + "DO1 -> patch-panel -> DI1", + "AI/AO/FREQ", + "future capability", + "SOURCE", + "DEV-LIVE", + "BLOCKED", + "Web 不提供硬件写 RPC;不会新增伪硬件写操作。" +]) { + assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`); +} +assert.match(app, /dataset\.sourceKind/); +assert.match(styles, /\.hardware-section\s*{/); +assert.match(styles, /\.hardware-row\s*{/); assert.match(html, /Gate \/ 诊断 \/ 验收/); assert.match(app, /new Set\(\["\/gate", "\/diagnostics\/gate"\]\)/); assert.match(html, /href="\/styles\.css"/); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 1f54e9d3..babaaf37 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -681,8 +681,74 @@ h3 { .hardware-list { display: grid; - grid-template-columns: 1fr 1fr; gap: 8px; + align-content: start; +} + +.hardware-section { + display: grid; + gap: 8px; + padding: 9px; + background: var(--surface-2); + border: 1px solid var(--line); +} + +.hardware-section-title { + color: var(--accent-2); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + letter-spacing: 0; + text-transform: uppercase; +} + +.hardware-rows { + display: grid; + gap: 6px; +} + +.hardware-row { + min-width: 0; + display: grid; + gap: 5px 8px; + padding: 8px; + background: var(--surface); + border: 1px solid var(--line); + border-left-width: 3px; +} + +.hardware-row-head { + grid-column: 1 / -1; + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + min-width: 0; +} + +.hardware-row-label, +.hardware-row-value, +.hardware-row-detail { + min-width: 0; + overflow-wrap: anywhere; +} + +.hardware-row-label { + color: var(--text); + font-weight: 760; +} + +.hardware-row-value { + color: var(--text); + font-family: var(--mono); + font-size: 14px; + font-weight: 800; + line-height: 1.1; +} + +.hardware-row-detail { + color: var(--muted); + font-size: 11px; } .status-card { @@ -915,7 +981,8 @@ tbody tr:last-child td { .tone-runtime_blocker, .tone-failed, .tone-rejected, -.tone-unavailable { +.tone-unavailable, +.tone-disabled { color: var(--bad); } @@ -957,10 +1024,31 @@ tbody tr:last-child td { .tone-bg-runtime_blocker, .tone-bg-failed, .tone-bg-rejected, -.tone-bg-unavailable { +.tone-bg-unavailable, +.tone-bg-disabled { background: var(--bad); } +.tone-border-live, +.tone-border-dev-live, +.tone-border-connected, +.tone-border-available, +.tone-border-active, +.tone-border-ok { + border-left-color: var(--ok); +} + +.tone-border-source, +.tone-border-disabled { + border-left-color: var(--dim); +} + +.tone-border-degraded, +.tone-border-blocked, +.tone-border-failed { + border-left-color: var(--bad); +} + .tone-bg-dry-run, .tone-bg-not_run, .tone-bg-warn, From 998b73674cc768e891f284927a1dec60d7445d14 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Fri, 22 May 2026 14:46:55 +0000 Subject: [PATCH 2/3] fix: guard hardware status M3 dev-live labels --- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 47 ++++++ web/hwlab-cloud-web/app.mjs | 151 +++++++++++++----- web/hwlab-cloud-web/scripts/check.mjs | 10 ++ 3 files changed, 170 insertions(+), 38 deletions(-) diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index beab3c41..7c3791aa 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -237,6 +237,15 @@ function runStaticSmoke() { evidence: ["M3 remains blocked without live loop evidence", "checked-in evidence records remain dry-run"] }); + addCheck(checks, blockers, "m3-fixture-counts-not-dev-live", m3FixtureCountsNotDevLive(files.app), "Fixture/source patch-panel wiring plus generic runtime counts cannot render M3 DEV-LIVE.", { + blocker: "observability_blocker", + evidence: [ + "hasTrustedM3LiveEvidence requires patchPanelLiveReport", + "operationId/traceId/auditId/evidenceId required", + "runtime counts are SOURCE-only" + ] + }); + const help = inspectHelpContract(files); addCheck(checks, blockers, "help-md-contract", help.status, help.summary, { blocker: help.blocker, @@ -456,6 +465,7 @@ function hardwareStatusStructureContract({ html, app, styles }) { /function\s+hardwareGroup\s*\(/u.test(app) && /function\s+hardwareRow\s*\(/u.test(app) && /function\s+trustedM3Link\s*\(/u.test(app) && + /function\s+hasTrustedM3LiveEvidence\s*\(/u.test(app) && /function\s+linkStatusLabel\s*\(/u.test(app) && /function\s+normalizePort\s*\(/u.test(app) && /Web 不提供硬件写 RPC;不会新增伪硬件写操作。/u.test(app) && @@ -526,6 +536,43 @@ function sourceEvidenceNotDevLive(source) { ); } +function m3FixtureCountsNotDevLive(app) { + const hardwareBody = functionBody(app, "renderHardwareStatus"); + const linkDetailBody = functionBody(app, "m3LinkDetail"); + const liveEvidenceBody = functionBody(app, "hasTrustedM3LiveEvidence"); + const patchPanelReportBody = functionBody(app, "hasTrustedPatchPanelLiveReport"); + const sourceOnlyRuntimeCounts = + /runtimeCountsSourceKind\s*=\s*["']SOURCE["']/u.test(hardwareBody) && + !/sourceKind:\s*counts\s*\?\s*["']DEV-LIVE["']/u.test(hardwareBody) && + !/counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*["']DEV-LIVE["']/u.test(hardwareBody); + return ( + sourceOnlyRuntimeCounts && + /hasTrustedM3LiveEvidence\(liveM3Evidence\)/u.test(hardwareBody) && + /patchPanelLiveReport/u.test(liveEvidenceBody) && + /LIVE_M3_ID_FIELDS\.every/u.test(liveEvidenceBody) && + /hasTrustedPatchPanelLiveReport/u.test(liveEvidenceBody) && + /serviceId\s*!==\s*M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(patchPanelReportBody) && + /trustedM3Link\(\[link\]\)/u.test(patchPanelReportBody) && + /counts 不是 patch-panel live report/u.test(linkDetailBody) && + /SOURCE fixture 包含目标接线/u.test(linkDetailBody) + ); +} + +function functionBody(source, functionName) { + const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); + if (!match) return ""; + let depth = 0; + for (let index = match.index + match[0].length - 1; index < source.length; index += 1) { + const char = source[index]; + if (char === "{") depth += 1; + if (char === "}") { + depth -= 1; + if (depth === 0) return source.slice(match.index, index + 1); + } + } + return ""; +} + function inspectHelpContract({ html, app }) { const markdownFiles = findHelpMarkdownFiles(); const rendererPackages = findMarkdownRenderers(); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index c873f850..33b7f02c 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -37,6 +37,18 @@ const STATUS_LABELS = Object.freeze({ blocked: "阻塞 BLOCKED" }); +const M3_TRUSTED_ROUTE = Object.freeze({ + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" +}); + +const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]); +const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]); +const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]); + const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); let rpcSequence = 0; @@ -612,16 +624,16 @@ function renderUnblockList() { function renderHardwareStatus(summary) { const counts = summary?.counts ?? null; const patchPanel = gateSummary.topology.patchPanel; - const liveLink = trustedM3Link(patchPanel.activeConnections); - const sourceKind = counts ? "DEV-LIVE" : "SOURCE"; - const sourceTone = counts ? "dev-live" : "source"; - const gatewayTone = counts ? "dev-live" : "source"; - const boxTone = counts ? "dev-live" : "source"; - const patchTone = patchPanel.state === "active" ? "live" : patchPanel.state; - const linkTone = counts && liveLink ? "live" : patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked"; + const sourceLink = trustedM3Link(patchPanel.activeConnections); + const liveM3Evidence = trustedM3LiveEvidenceFrom(summary); + const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence); + const runtimeCountsSourceKind = "SOURCE"; + const runtimeCountsTone = "source"; + const patchTone = m3Live ? "live" : patchPanel.state === "active" ? "source" : patchPanel.state; + const linkTone = m3Live ? "live" : sourceLink || patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked"; - el.hardwareSourceStatus.textContent = counts ? "DEV-LIVE" : "SOURCE"; - el.hardwareSourceStatus.className = `state-tag tone-${toneClass(sourceTone)}`; + el.hardwareSourceStatus.textContent = m3Live ? "DEV-LIVE" : "SOURCE"; + el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`; replaceChildren( el.hardwareList, @@ -632,19 +644,19 @@ function renderHardwareStatus(summary) { label: "Gateway-SIMU", value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看", detail: counts - ? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime。` + ? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime counts;不是 M3 DEV-LIVE 证据。` : `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`, - sourceKind, - tone: gatewayTone + sourceKind: runtimeCountsSourceKind, + tone: runtimeCountsTone }), hardwareRow({ label: "BOX-SIMU", value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看", detail: counts - ? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime。` + ? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime counts;不是 M3 DEV-LIVE 证据。` : `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`, - sourceKind, - tone: boxTone + sourceKind: runtimeCountsSourceKind, + tone: runtimeCountsTone }) ] }), @@ -653,17 +665,17 @@ function renderHardwareStatus(summary) { rows: [ hardwareRow({ label: "DO1", - value: "not wired", - detail: counts ? "只读 runtime 未返回 DO1 电平;保持 SOURCE/DEV-LIVE 边界。" : "SOURCE 仅说明端口存在,不冒充实况。", - sourceKind: counts ? "DEV-LIVE" : "SOURCE", - tone: "blocked" + value: m3Live ? "verified" : "not wired", + detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。", + sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", + tone: m3Live ? "live" : "blocked" }), hardwareRow({ label: "DI1", - value: "not wired", - detail: counts ? "只读 runtime 未返回 DI1 回读;保持 SOURCE/DEV-LIVE 边界。" : "SOURCE 仅说明端口存在,不冒充实况。", - sourceKind: counts ? "DEV-LIVE" : "SOURCE", - tone: "blocked" + value: m3Live ? "verified" : "not wired", + detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。", + sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", + tone: m3Live ? "live" : "blocked" }), hardwareRow({ label: "AI/AO/FREQ", @@ -680,15 +692,17 @@ function renderHardwareStatus(summary) { hardwareRow({ label: "hwlab-patch-panel", value: statusLabel(patchPanel.state), - detail: `${patchPanel.activeConnectionCount} 条 SOURCE 接线;当前 fixture 为 ${describePatchPanelConnections(patchPanel)}。`, - sourceKind: "SOURCE", + detail: m3Live + ? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route。` + : `${patchPanel.activeConnectionCount} 条 SOURCE 接线;当前 fixture 为 ${describePatchPanelConnections(patchPanel)}。`, + sourceKind: m3Live ? "DEV-LIVE" : "SOURCE", tone: patchTone }), hardwareRow({ label: "DO1 -> patch-panel -> DI1", value: linkStatusLabel(linkTone), - detail: m3LinkDetail(liveLink, patchPanel, counts), - sourceKind: counts && liveLink ? "DEV-LIVE" : "BLOCKED", + detail: m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }), + sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: linkTone }) ] @@ -735,10 +749,11 @@ function hardwareRow(item) { function trustedM3Link(connections) { return (connections ?? []).find( (link) => + String(link.fromResourceId ?? "") === M3_TRUSTED_ROUTE.fromResourceId && normalizePort(link.fromPort) === "DO1" && + String(link.toResourceId ?? "") === M3_TRUSTED_ROUTE.toResourceId && normalizePort(link.toPort) === "DI1" && - String(link.fromResourceId ?? "").includes("boxsimu") && - String(link.toResourceId ?? "").includes("boxsimu") + connectionPatchPanelServiceId(link) === M3_TRUSTED_ROUTE.patchPanelServiceId ); } @@ -749,9 +764,15 @@ function describePatchPanelConnections(patchPanel) { .join(";"); } -function m3LinkDetail(liveLink, patchPanel, counts) { - if (liveLink && counts) { - return `DEV-LIVE runtime 仅证明只读观测,链路为 ${liveLink.fromResourceId}:${liveLink.fromPort} -> ${patchPanel.serviceId} -> ${liveLink.toResourceId}:${liveLink.toPort}。`; +function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) { + if (hasTrustedM3LiveEvidence(liveM3Evidence)) { + return `DEV-LIVE M3:${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort};${m3LiveDetail(liveM3Evidence)}。`; + } + if (sourceLink) { + return `BLOCKED:SOURCE fixture 包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},但缺少 patch-panel live report 与 operation/trace/audit/evidence IDs。`; + } + if (counts) { + return "BLOCKED:runtime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。"; } if (patchPanel.activeConnectionCount > 0) { return `SOURCE 仅看到 patch-panel 接线:${describePatchPanelConnections(patchPanel)};未冒充 live。`; @@ -773,6 +794,54 @@ function normalizePort(port) { return String(port ?? "").trim().toUpperCase(); } +function trustedM3LiveEvidenceFrom(summary) { + const candidates = [ + summary?.m3TrustedLoop, + summary?.m3HardwareLoop, + summary?.hardware?.m3TrustedLoop, + summary?.patchPanel?.m3TrustedLoop, + summary?.liveM3Evidence + ].filter(Boolean); + + return candidates.find(hasTrustedM3LiveEvidence) ?? null; +} + +function hasTrustedM3LiveEvidence(candidate) { + if (!candidate || typeof candidate !== "object") return false; + if (candidate.dryRun === true || candidate.fixture === true || candidate.sourceKind === "SOURCE" || candidate.sourceKind === "DRY-RUN") return false; + if (!hasLiveSourceKind(candidate)) return false; + if (!LIVE_M3_PASS_STATUSES.includes(String(candidate.status ?? candidate.outcome ?? "verified").toLowerCase())) return false; + if (!LIVE_M3_ID_FIELDS.every((field) => isLiveIdentifier(candidate[field] ?? candidate.operation?.[field] ?? candidate.identifiers?.[field]))) return false; + const patchPanelLiveReport = candidate.patchPanelLiveReport ?? candidate.patchPanelReport; + return hasTrustedPatchPanelLiveReport(patchPanelLiveReport); +} + +function hasTrustedPatchPanelLiveReport(report) { + if (!report || typeof report !== "object") return false; + if (report.dryRun === true || report.fixture === true || report.sourceKind === "SOURCE" || report.sourceKind === "DRY-RUN") return false; + if (!hasLiveSourceKind(report)) return false; + if (report.serviceId !== M3_TRUSTED_ROUTE.patchPanelServiceId) return false; + if (!isLiveIdentifier(report.reportId ?? report.patchPanelStatusId ?? report.statusId ?? report.evidenceId)) return false; + return (report.activeConnections ?? report.connections ?? []).some((link) => trustedM3Link([link])); +} + +function hasLiveSourceKind(value) { + return value.live === true || value.sourceKind === "DEV-LIVE" || value.evidenceLevel === "DEV-LIVE"; +} + +function isLiveIdentifier(value) { + const text = String(value ?? "").trim(); + return text !== "" && !NON_LIVE_ID_VALUES.includes(text.toLowerCase()); +} + +function connectionPatchPanelServiceId(link) { + return String(link.patchPanelServiceId ?? link.patchPanel?.serviceId ?? link.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId); +} + +function m3LiveDetail(evidence) { + return `operation=${evidence.operationId} / trace=${evidence.traceId} / audit=${evidence.auditId} / evidence=${evidence.evidenceId}`; +} + function controlRows() { return [ { @@ -857,13 +926,19 @@ function renderRecords(live) { ); const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : []; - const liveCards = liveRecords.map((record) => - infoCard({ + const liveCards = liveRecords.map((record) => { + const m3Trusted = hasTrustedM3LiveEvidence(record); + return infoCard({ title: record.evidenceId, - detail: `${recordKindLabel(record.kind ?? "record")} / ${record.operationId ?? "n/a"} / ${record.serviceId ?? "未知服务"}`, - tone: "dev-live" - }) - ); + detail: [ + m3Trusted ? "开发实况 DEV-LIVE M3" : "阻塞 BLOCKED:只读 evidence 查询不是 M3 trusted loop", + recordKindLabel(record.kind ?? "record"), + record.operationId ?? "n/a", + record.serviceId ?? "未知服务" + ].join(" / "), + tone: m3Trusted ? "dev-live" : "blocked" + }); + }); if (live && !live.evidence.ok) { liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" })); } diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 669f733c..a9b5b1b3 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -82,8 +82,15 @@ assert.match(app, /function renderHardwareStatus/); assert.match(app, /function hardwareGroup/); assert.match(app, /function hardwareRow/); assert.match(app, /function trustedM3Link/); +assert.match(app, /function hasTrustedM3LiveEvidence/); +assert.match(app, /function hasTrustedPatchPanelLiveReport/); assert.match(app, /function linkStatusLabel/); assert.match(app, /function normalizePort/); +assert.match(app, /LIVE_M3_ID_FIELDS/); +assert.match(app, /patchPanelLiveReport/); +assert.match(app, /runtimeCountsSourceKind = "SOURCE"/); +assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/); +assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/); for (const hardwareTerm of [ "Gateway/Box 在线", "BOX-SIMU 端口", @@ -94,6 +101,9 @@ for (const hardwareTerm of [ "SOURCE", "DEV-LIVE", "BLOCKED", + "runtime counts;不是 M3 DEV-LIVE 证据", + "counts 不是 patch-panel live report", + "缺少 patch-panel live report 与 operation/trace/audit/evidence IDs", "Web 不提供硬件写 RPC;不会新增伪硬件写操作。" ]) { assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`); From c3ac190e95d5d8b67bc3c8842d6c1ea2e889479c Mon Sep 17 00:00:00 2001 From: unidesk-code-queue-runner Date: Fri, 22 May 2026 15:06:57 +0000 Subject: [PATCH 3/3] fix: render M3 workbench wiring --- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 24 +++++ web/hwlab-cloud-web/app.mjs | 91 ++++++++----------- web/hwlab-cloud-web/scripts/check.mjs | 21 +++++ 3 files changed, 83 insertions(+), 53 deletions(-) diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 7c3791aa..7b71e9eb 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -246,6 +246,15 @@ function runStaticSmoke() { ] }); + addCheck(checks, blockers, "m3-rendered-workbench-not-m5-fixture", m3RenderedWorkbenchNotM5Fixture(files.app), "Right workbench hardware status and wiring table render the M3 chain, not M5 fixture rows or undefined DOM-node rows.", { + blocker: "runtime_blocker", + evidence: [ + "renderHardwareStatus passes data objects into hardwareGroup", + "renderWiringList uses M3_TRUSTED_ROUTE", + "M5 activeConnections are not mapped into the workbench wiring table" + ] + }); + const help = inspectHelpContract(files); addCheck(checks, blockers, "help-md-contract", help.status, help.summary, { blocker: help.blocker, @@ -558,6 +567,21 @@ function m3FixtureCountsNotDevLive(app) { ); } +function m3RenderedWorkbenchNotM5Fixture(app) { + const hardwareBody = functionBody(app, "renderHardwareStatus"); + const hardwareGroupBody = functionBody(app, "hardwareGroup"); + const wiringBody = functionBody(app, "renderWiringList"); + return ( + /group\.rows\.map\(hardwareRow\)/u.test(hardwareGroupBody) && + !/hardwareRow\(\s*\{/u.test(hardwareBody) && + /M3_TRUSTED_ROUTE\.fromResourceId/u.test(wiringBody) && + /M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(wiringBody) && + /M3_TRUSTED_ROUTE\.toResourceId/u.test(wiringBody) && + !/activeConnections\.map/u.test(wiringBody) && + !/link\.fromResourceId/u.test(wiringBody) + ); +} + function functionBody(source, functionName) { const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); if (!match) return ""; diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 33b7f02c..429bf165 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -311,7 +311,7 @@ function renderStaticWorkbench() { renderUnblockList(); renderHardwareStatus(null); renderDrafts(); - renderWiringList(); + renderWiringList(null); renderRecords(null); renderDiagnostics(null); renderGateDiagnostics(); @@ -459,6 +459,7 @@ function renderLiveSurface(live) { : "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"; renderHardwareStatus(runtimeSummary); + renderWiringList(runtimeSummary); renderRecords(live); renderDiagnostics(live); renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded"); @@ -640,7 +641,7 @@ function renderHardwareStatus(summary) { hardwareGroup({ title: "Gateway/Box 在线", rows: [ - hardwareRow({ + { label: "Gateway-SIMU", value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看", detail: counts @@ -648,8 +649,8 @@ function renderHardwareStatus(summary) { : `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`, sourceKind: runtimeCountsSourceKind, tone: runtimeCountsTone - }), - hardwareRow({ + }, + { label: "BOX-SIMU", value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看", detail: counts @@ -657,66 +658,66 @@ function renderHardwareStatus(summary) { : `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`, sourceKind: runtimeCountsSourceKind, tone: runtimeCountsTone - }) + } ] }), hardwareGroup({ title: "BOX-SIMU 端口", rows: [ - hardwareRow({ + { label: "DO1", value: m3Live ? "verified" : "not wired", detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。", sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: m3Live ? "live" : "blocked" - }), - hardwareRow({ + }, + { label: "DI1", value: m3Live ? "verified" : "not wired", detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。", sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: m3Live ? "live" : "blocked" - }), - hardwareRow({ + }, + { label: "AI/AO/FREQ", value: "future capability", detail: "当前未声明模拟量/频率闭环,按 future capability 处理。", sourceKind: "SOURCE", tone: "disabled" - }) + } ] }), hardwareGroup({ title: "Patch Panel 连线", rows: [ - hardwareRow({ + { label: "hwlab-patch-panel", value: statusLabel(patchPanel.state), detail: m3Live ? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route。` - : `${patchPanel.activeConnectionCount} 条 SOURCE 接线;当前 fixture 为 ${describePatchPanelConnections(patchPanel)}。`, + : `${patchPanel.activeConnectionCount} 条 SOURCE fixture 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE。`, sourceKind: m3Live ? "DEV-LIVE" : "SOURCE", tone: patchTone - }), - hardwareRow({ + }, + { label: "DO1 -> patch-panel -> DI1", value: linkStatusLabel(linkTone), detail: m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }), sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED", tone: linkTone - }) + } ] }), hardwareGroup({ title: "写入边界", rows: [ - hardwareRow({ + { label: "硬件控制", value: "已禁用", detail: "Web 不提供硬件写 RPC;不会新增伪硬件写操作。", sourceKind: "SOURCE", tone: "source" - }) + } ] }) ); @@ -757,13 +758,6 @@ function trustedM3Link(connections) { ); } -function describePatchPanelConnections(patchPanel) { - if (!patchPanel.activeConnections.length) return "无活动连线"; - return patchPanel.activeConnections - .map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`) - .join(";"); -} - function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) { if (hasTrustedM3LiveEvidence(liveM3Evidence)) { return `DEV-LIVE M3:${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort};${m3LiveDetail(liveM3Evidence)}。`; @@ -775,7 +769,7 @@ function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) { return "BLOCKED:runtime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。"; } if (patchPanel.activeConnectionCount > 0) { - return `SOURCE 仅看到 patch-panel 接线:${describePatchPanelConnections(patchPanel)};未冒充 live。`; + return `SOURCE 仅看到非 M3 patch-panel fixture 接线;required M3 route ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍 BLOCKED,未冒充 live。`; } return "BLOCKED:当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。"; } @@ -885,35 +879,26 @@ function renderDrafts() { replaceChildren(el.controlList, ...rows.map(infoCard)); } -function renderWiringList() { +function renderWiringList(summary = null) { const patchPanel = gateSummary.topology.patchPanel; - const wiringStep = gateSummary.steps.find((step) => step.kind === "wiring"); - const traceEvidence = [wiringStep?.id, patchPanel.patchPanelStatusId].filter(Boolean).join(" / "); - const rows = patchPanel.activeConnections.map((link) => { - const tr = document.createElement("tr"); - tr.append(cell(link.fromResourceId, "mono wrap")); - tr.append(cell(link.fromPort, "mono")); - tr.append(cell(patchPanel.serviceId, "mono wrap")); - tr.append(cell(link.toResourceId, "mono wrap")); - tr.append(cell(link.toPort, "mono")); - const status = document.createElement("td"); - status.append(badge(statusLabel(patchPanel.state), patchPanel.state)); - tr.append(status); - tr.append(cell(`${patchPanel.environment} / SOURCE`, "wrap")); - tr.append(cell(traceEvidence || "无", "mono wrap")); - return tr; - }); + const liveM3Evidence = trustedM3LiveEvidenceFrom(summary); + const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence); + const traceEvidence = m3Live + ? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ") + : "m3-route-required / waiting-for-dev-live"; + const tr = document.createElement("tr"); + tr.append(cell(M3_TRUSTED_ROUTE.fromResourceId, "mono wrap")); + tr.append(cell(M3_TRUSTED_ROUTE.fromPort, "mono")); + tr.append(cell(M3_TRUSTED_ROUTE.patchPanelServiceId, "mono wrap")); + tr.append(cell(M3_TRUSTED_ROUTE.toResourceId, "mono wrap")); + tr.append(cell(M3_TRUSTED_ROUTE.toPort, "mono")); + const status = document.createElement("td"); + status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked")); + tr.append(status); + tr.append(cell(m3Live ? "DEV-LIVE" : "SOURCE / required M3 route", "wrap")); + tr.append(cell(traceEvidence, "mono wrap")); - if (rows.length === 0) { - const tr = document.createElement("tr"); - const td = document.createElement("td"); - td.colSpan = 8; - td.textContent = "当前没有接线记录。"; - tr.append(td); - rows.push(tr); - } - - replaceChildren(el.wiringBody, ...rows); + replaceChildren(el.wiringBody, tr); } function renderRecords(live) { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index a9b5b1b3..941b0f60 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -89,6 +89,8 @@ assert.match(app, /function normalizePort/); assert.match(app, /LIVE_M3_ID_FIELDS/); assert.match(app, /patchPanelLiveReport/); assert.match(app, /runtimeCountsSourceKind = "SOURCE"/); +assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s*\{/u); +assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u); assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/); assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/); for (const hardwareTerm of [ @@ -197,6 +199,10 @@ assert.match(html, /id="unblock-list"/); assert.match(app, /DB live readiness/); assert.match(app, /res_boxsimu_1:DO1 -> res_boxsimu_2:DI1/); assert.match(app, /patch-panel DO1 -> DI1/); +assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/); +assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/); +assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u); +assert.doesNotMatch(functionBody(app, "renderWiringList"), /link\.fromResourceId/u); assert.match(buildScript, /"help\.md"/); assert.match(buildScript, /"third_party\/marked\/marked\.esm\.js"/); assert.match(buildScript, /"third_party\/marked\/LICENSE"/); @@ -298,3 +304,18 @@ function beforeSection(source, viewId) { const index = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); return index === -1 ? source : source.slice(0, index); } + +function functionBody(source, functionName) { + const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); + if (!match) return ""; + let depth = 0; + for (let index = match.index + match[0].length - 1; index < source.length; index += 1) { + const char = source[index]; + if (char === "{") depth += 1; + if (char === "}") { + depth -= 1; + if (depth === 0) return source.slice(match.index, index + 1); + } + } + return ""; +}