fix: 固化 Workbench Trace 可读性探针

This commit is contained in:
Codex
2026-07-10 18:00:24 +02:00
parent 92686ff213
commit b6f5e5efd4
21 changed files with 1378 additions and 16 deletions
@@ -0,0 +1,577 @@
// SPEC: PJ2026-010401080313 Workbench 实时权威。
// 职责:在持久 Web observer 中验证产品 Workbench Trace 的可读性。
export function nodeWebObserveRunnerTraceReadabilitySource(): string {
return String.raw`
function parseWorkbenchTraceReadabilityConfig(raw) {
if (!raw) return null;
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw new Error("UNIDESK_WEB_OBSERVE_WORKBENCH_TRACE_READABILITY_JSON 不是合法 JSON" + (error instanceof Error ? error.message : String(error)));
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("UNIDESK_WEB_OBSERVE_WORKBENCH_TRACE_READABILITY_JSON 必须是对象");
}
if (typeof parsed.enabled !== "boolean") throw new Error("Workbench Trace 可读性 enabled 必须由 owning YAML 显式声明为布尔值");
const positiveInteger = (key) => {
const value = Number(parsed[key]);
if (!Number.isInteger(value) || value <= 0) throw new Error("Workbench Trace 可读性 " + key + " 必须是 owning YAML 中的正整数");
return value;
};
return {
enabled: parsed.enabled,
runningCardTimeoutMs: positiveInteger("runningCardTimeoutMs"),
disclosureActionTimeoutMs: positiveInteger("disclosureActionTimeoutMs"),
disclosureOpenTimeoutMs: positiveInteger("disclosureOpenTimeoutMs"),
runningReadableTimeoutMs: positiveInteger("runningReadableTimeoutMs"),
terminalTimeoutMs: positiveInteger("terminalTimeoutMs"),
domStableQuietMs: positiveInteger("domStableQuietMs"),
domStableTimeoutMs: positiveInteger("domStableTimeoutMs"),
pollIntervalMs: positiveInteger("pollIntervalMs"),
valuesRedacted: true
};
}
function workbenchTraceReadabilityTerminalStatus(value) {
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"]
.includes(String(value || "").trim().toLowerCase().replace(/_/gu, "-"));
}
async function validateWorkbenchTraceReadability(command) {
const config = workbenchTraceReadability;
if (!config || config.enabled !== true) throw new Error("owning YAML 未启用 validateWorkbenchTraceReadability");
const reportDir = path.join(stateDir, "artifacts", "workbench-trace-readability", safeId(command.id));
const report = {
ok: false,
contract: "web-probe-workbench-trace-readability-v1",
commandId: command.id,
origin,
expected: {
runningCardTimeoutMs: config.runningCardTimeoutMs,
disclosureActionTimeoutMs: config.disclosureActionTimeoutMs,
disclosureOpenTimeoutMs: config.disclosureOpenTimeoutMs,
runningReadableTimeoutMs: config.runningReadableTimeoutMs,
terminalTimeoutMs: config.terminalTimeoutMs,
domStableQuietMs: config.domStableQuietMs,
domStableTimeoutMs: config.domStableTimeoutMs,
pollIntervalMs: config.pollIntervalMs,
scope: "#conversation-list product WorkbenchMessageCard",
sequenceAuthority: "source",
valuesRedacted: true
},
startedAt: new Date().toISOString(),
valuesRedacted: true
};
try {
const controlRecovery = await ensureControlPageResponsiveForCommand("validateWorkbenchTraceReadability");
Object.assign(report, { controlRecovery });
const currentPath = safeUrlPath(currentPageUrl()) || "";
if (!isWorkbenchPathname(currentPath)) throw new Error("validateWorkbenchTraceReadability 必须在原 Workbench 页面执行");
const session = await workbenchSessionSnapshot();
const sessionId = session?.activeSessionId || session?.routeSessionId || routeSessionIdFromUrl(currentPageUrl());
if (!sessionId) throw new Error("validateWorkbenchTraceReadability 需要当前已有 Workbench session");
Object.assign(report, { sessionId });
const target = await workbenchTraceReadabilityWaitForCurrentProductCard(
config.runningCardTimeoutMs,
config.pollIntervalMs
);
Object.assign(report, { traceId: target.traceId, scope: target.scope });
const before = await workbenchTraceReadabilitySnapshot(target.card, target.traceId);
const startedDuringRunning = !workbenchTraceReadabilityTerminalStatus(before.status);
const openBefore = before.disclosure.open === true;
let forcedExpansion = false;
Object.assign(report, {
before,
startedDuringRunning,
forcedExpansion,
disclosure: {
openBefore,
openAfterExpansion: null,
openAfterTerminal: null,
autoReadable: false,
valuesRedacted: true
}
});
if (!openBefore) {
const summary = target.card.locator("details.trace-disclosure > summary.trace-disclosure-summary").first();
await summary.waitFor({ state: "visible", timeout: config.disclosureActionTimeoutMs });
await summary.click({ timeout: config.disclosureActionTimeoutMs });
forcedExpansion = true;
report.forcedExpansion = true;
}
const afterExpansion = await workbenchTraceReadabilityWaitForDisclosureOpen(
target.card,
target.traceId,
config.disclosureOpenTimeoutMs,
config.pollIntervalMs
);
report.disclosure.openAfterExpansion = afterExpansion.disclosure?.open ?? null;
const running = startedDuringRunning
? await workbenchTraceReadabilityWaitForReadableRunning(
target.card,
target.traceId,
config.runningReadableTimeoutMs,
config.pollIntervalMs
)
: { observed: false, reason: "command-started-after-terminal", snapshot: before, valuesRedacted: true };
Object.assign(report, { running });
report.disclosure.autoReadable = openBefore && (startedDuringRunning
? running.observed === true
: before.rowCount > 0 && before.sourceAuthorityRowCount > 0 && before.readableSourceRowCount > 0);
const terminalArrival = await workbenchTraceReadabilityWaitForTerminal(
target.card,
target.traceId,
config.terminalTimeoutMs,
config.pollIntervalMs
);
Object.assign(report, { terminalArrival });
const terminal = await workbenchTraceReadabilityWaitForStableTerminal(
target.card,
target.traceId,
config.domStableQuietMs,
config.domStableTimeoutMs,
config.pollIntervalMs
);
Object.assign(report, { terminal });
report.disclosure.openAfterTerminal = terminal.snapshot?.disclosure?.open ?? null;
const runningRetention = workbenchTraceReadabilityRetention(running.snapshot, terminal.snapshot);
const retainedRunningRowCount = runningRetention.retainedRowCount;
Object.assign(report, { runningRetention, retainedRunningRowCount });
const screenshot = await captureScreenshot("workbench-trace-readability-" + safeId(command.id));
const evidence = {
sessionId,
traceId: target.traceId,
scope: target.scope,
startedDuringRunning,
forcedExpansion,
disclosure: {
openBefore,
openAfterExpansion: afterExpansion.disclosure?.open ?? null,
openAfterTerminal: terminal.snapshot?.disclosure?.open ?? null,
autoReadable: openBefore && (startedDuringRunning
? running.observed === true
: before.rowCount > 0 && before.sourceAuthorityRowCount > 0 && before.readableSourceRowCount > 0),
valuesRedacted: true
},
running,
terminalArrival,
terminal,
runningRetention,
retainedRunningRowCount,
screenshot: { path: screenshot.path, sha256: screenshot.sha256, valuesRedacted: true },
controlRecovery,
valuesRedacted: true
};
Object.assign(report, evidence);
const failures = workbenchTraceReadabilityFailures(evidence);
report.failures = failures;
report.ok = failures.length === 0;
report.status = failures.length === 0 ? "passed" : "failed";
report.completedAt = new Date().toISOString();
const reportArtifact = await workbenchTraceReadabilityWriteReport(reportDir, report);
report.reportPath = reportArtifact.path;
report.reportSha256 = reportArtifact.sha256;
if (failures.length > 0) {
throw workbenchTraceReadabilityEvidenceError(
"Workbench 产品 Trace 可读性验证失败:" + failures.map((failure) => failure.code).join(","),
report
);
}
await appendJsonl(files.control, eventRecord("workbench-trace-readability-validated", {
sessionId,
traceId: report.traceId,
disclosure: report.disclosure,
running: report.running,
terminal: report.terminal,
runningRetention: report.runningRetention,
retainedRunningRowCount,
reportPath: report.reportPath,
reportSha256: report.reportSha256,
screenshot: report.screenshot,
valuesRedacted: true
}));
return {
ok: true,
status: "passed",
sessionId,
traceId: report.traceId,
scope: report.scope,
disclosure: report.disclosure,
running: report.running,
terminalArrival: report.terminalArrival,
terminal: report.terminal,
runningRetention: report.runningRetention,
retainedRunningRowCount,
reportPath: report.reportPath,
reportSha256: report.reportSha256,
screenshot: report.screenshot,
valuesRedacted: true
};
} catch (error) {
const wrapped = error instanceof Error ? error : new Error(String(error));
const partial = wrapped.details && typeof wrapped.details === "object" && !Array.isArray(wrapped.details)
? wrapped.details
: {};
for (const key of ["sessionId", "traceId", "scope", "before", "disclosure", "running", "terminalArrival", "terminal", "runningRetention", "retainedRunningRowCount", "failures"]) {
if ((report[key] === undefined || report[key] === null) && partial[key] !== undefined && partial[key] !== null) report[key] = partial[key];
}
if (!report.completedAt) {
Object.assign(report, {
ok: false,
status: "failed",
completedAt: new Date().toISOString(),
failure: errorSummary(error),
valuesRedacted: true
});
}
if (!Array.isArray(report.failures)) {
report.failures = [{ code: "probe_exception", message: wrapped.message, valuesRedacted: true }];
}
if (!report.screenshot) {
report.screenshot = await captureScreenshot("workbench-trace-readability-failed-" + safeId(command.id))
.then((screenshot) => ({ path: screenshot.path, sha256: screenshot.sha256, valuesRedacted: true }))
.catch((screenshotError) => ({ available: false, error: errorSummary(screenshotError), valuesRedacted: true }));
}
const existingReportPath = wrapped.details?.reportPath || report.reportPath || null;
const existingReportSha256 = wrapped.details?.reportSha256 || report.reportSha256 || null;
const reportArtifact = existingReportPath && existingReportSha256
? null
: await workbenchTraceReadabilityWriteReport(reportDir, report).catch(() => null);
wrapped.details = {
...(wrapped.details || {}),
sessionId: report.sessionId || wrapped.details?.sessionId || null,
traceId: report.traceId || wrapped.details?.traceId || null,
scope: report.scope || wrapped.details?.scope || null,
before: report.before || wrapped.details?.before || null,
disclosure: report.disclosure || wrapped.details?.disclosure || null,
running: report.running || wrapped.details?.running || null,
terminalArrival: report.terminalArrival || wrapped.details?.terminalArrival || null,
terminal: report.terminal || wrapped.details?.terminal || null,
runningRetention: report.runningRetention || wrapped.details?.runningRetention || null,
retainedRunningRowCount: report.retainedRunningRowCount ?? wrapped.details?.retainedRunningRowCount ?? null,
failures: report.failures || wrapped.details?.failures || null,
screenshot: report.screenshot || wrapped.details?.screenshot || null,
reportPath: existingReportPath || reportArtifact?.path || null,
reportSha256: existingReportSha256 || reportArtifact?.sha256 || null,
valuesRedacted: true
};
throw wrapped;
}
}
async function workbenchTraceReadabilityWaitForCurrentProductCard(timeoutMs, pollIntervalMs) {
const deadline = Date.now() + timeoutMs;
let lastScope = null;
while (Date.now() <= deadline) {
const conversation = page.locator("#conversation-list").first();
const conversationCount = await page.locator("#conversation-list").count();
const cards = conversation.locator(':scope > .message-card[data-role="agent"][data-trace-id]');
const productCardCount = conversationCount === 1 ? await cards.count() : 0;
const debugPanelCountInsideConversation = conversationCount === 1
? await conversation.locator('[data-testid="workbench-kafka-debug-panel"]').count()
: 0;
lastScope = {
selector: '#conversation-list > .message-card[data-role="agent"][data-trace-id]',
conversationCount,
productCardCount,
debugPanelCountInsideConversation,
isolatedDebugExcluded: debugPanelCountInsideConversation === 0,
valuesRedacted: true
};
if (conversationCount === 1 && productCardCount > 0) {
const runningTraceIds = await cards.evaluateAll((elements) => elements
.filter((element) => {
const timeline = element.querySelector(":scope > .trace-timeline");
const status = String(element.getAttribute("data-status") || timeline?.getAttribute("data-status") || "").trim().toLowerCase();
return status === "running" || status === "pending";
})
.map((element) => String(element.getAttribute("data-trace-id") || "").trim())
.filter((traceId) => /^trc_[A-Za-z0-9_-]+$/u.test(traceId)));
const traceId = runningTraceIds.at(-1) || null;
if (traceId) {
const card = conversation.locator(':scope > .message-card[data-role="agent"][data-trace-id="' + traceId + '"]').first();
const stableCardCount = await card.count();
const timelineCount = stableCardCount === 1 ? await card.locator(":scope > .trace-timeline").count() : 0;
if (stableCardCount === 1 && timelineCount === 1) return { card, traceId, scope: lastScope };
}
}
await sleep(pollIntervalMs);
}
const error = new Error("在 owning YAML 指定的 " + timeoutMs + "ms 内未找到当前新运行中的产品 Trace 卡片");
error.details = { scope: lastScope, valuesRedacted: true };
throw error;
}
async function workbenchTraceReadabilityWaitForDisclosureOpen(card, traceId, timeoutMs, pollIntervalMs) {
const deadline = Date.now() + timeoutMs;
let latest = await workbenchTraceReadabilitySnapshot(card, traceId);
while (Date.now() <= deadline) {
latest = await workbenchTraceReadabilitySnapshot(card, traceId);
if (latest.disclosure.open === true) return latest;
await sleep(pollIntervalMs);
}
return latest;
}
async function workbenchTraceReadabilitySnapshot(card, expectedTraceId) {
const raw = await card.evaluate((root) => {
const timeline = root.querySelector(":scope > .trace-timeline");
const disclosure = timeline?.querySelector(":scope > details.trace-disclosure");
const rows = disclosure ? Array.from(disclosure.querySelectorAll('[data-testid="trace-render-row"]')) : [];
const normalized = (value) => String(value || "").replace(/\s+/gu, " ").trim();
return {
connected: root.isConnected,
status: root.getAttribute("data-status") || timeline?.getAttribute("data-status") || null,
traceId: root.getAttribute("data-trace-id") || timeline?.getAttribute("data-trace-id") || null,
disclosure: {
available: Boolean(disclosure),
open: disclosure instanceof HTMLDetailsElement ? disclosure.open : null,
eventCount: disclosure?.getAttribute("data-event-count") || null,
readableRowCount: disclosure?.getAttribute("data-readable-row-count") || null,
renderedRowCount: disclosure?.getAttribute("data-rendered-row-count") || null,
rowWindowed: disclosure?.getAttribute("data-row-windowed") || null
},
rows: rows.map((row) => ({
seq: row.getAttribute("data-event-seq"),
authority: row.getAttribute("data-event-seq-authority"),
kind: row.getAttribute("data-event-kind"),
status: row.getAttribute("data-event-status"),
terminal: row.getAttribute("data-terminal") === "true",
text: normalized(row.textContent)
}))
};
});
if (raw.connected !== true || raw.traceId !== expectedTraceId) {
const error = new Error("产品 Trace 卡片身份在验证期间发生变化");
error.details = {
traceId: expectedTraceId,
observedTraceId: raw.traceId || null,
connected: raw.connected === true,
valuesRedacted: true
};
throw error;
}
const rows = raw.rows.map((row) => ({
seq: row.seq,
authority: row.authority,
kind: row.kind,
status: row.status,
terminal: row.terminal,
identity: [row.authority || "-", row.seq || "-", row.kind || "-"].join(":"),
textPresent: row.text.length > 0,
textBytes: Buffer.byteLength(row.text),
textHash: sha256Text(row.text),
valuesRedacted: true
}));
const sourceRows = rows.filter((row) => row.authority === "source");
const projectedRows = rows.filter((row) => row.authority === "projected");
const sourceSeqs = sourceRows
.map((row) => Number(row.seq))
.filter((value) => Number.isInteger(value));
const integerAttribute = (value) => {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
};
return {
connected: raw.connected,
status: raw.status,
traceId: raw.traceId,
disclosure: {
...raw.disclosure,
eventCount: integerAttribute(raw.disclosure.eventCount),
readableRowCount: integerAttribute(raw.disclosure.readableRowCount),
renderedRowCount: integerAttribute(raw.disclosure.renderedRowCount),
rowWindowed: raw.disclosure.rowWindowed === "true"
? true
: raw.disclosure.rowWindowed === "false"
? false
: null
},
rowCount: rows.length,
readableRowCount: rows.filter((row) => row.textPresent).length,
sourceAuthorityRowCount: sourceRows.length,
readableSourceRowCount: sourceRows.filter((row) => row.textPresent).length,
projectedAuthorityRowCount: projectedRows.length,
sourceSeqMin: sourceSeqs.length > 0 ? Math.min(...sourceSeqs) : null,
sourceSeqMax: sourceSeqs.length > 0 ? Math.max(...sourceSeqs) : null,
rowIdentities: rows.map((row) => row.identity),
rows: rows.slice(-12),
rowSetHash: sha256Text(rows.map((row) => row.identity + ":" + row.textHash).join("|")),
valuesRedacted: true
};
}
async function workbenchTraceReadabilityWaitForReadableRunning(card, traceId, timeoutMs, pollIntervalMs) {
const startedAtMs = Date.now();
const deadline = startedAtMs + timeoutMs;
let latest = await workbenchTraceReadabilitySnapshot(card, traceId);
while (Date.now() <= deadline) {
latest = await workbenchTraceReadabilitySnapshot(card, traceId);
if (!workbenchTraceReadabilityTerminalStatus(latest.status)
&& latest.disclosure.open === true
&& latest.rowCount > 0
&& latest.sourceAuthorityRowCount > 0
&& latest.readableSourceRowCount > 0) {
return { observed: true, elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
if (workbenchTraceReadabilityTerminalStatus(latest.status)) {
return { observed: false, reason: "terminal-before-readable-running-sample", elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
await sleep(pollIntervalMs);
}
return { observed: false, reason: "readable-running-timeout", elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
async function workbenchTraceReadabilityWaitForTerminal(card, traceId, timeoutMs, pollIntervalMs) {
const startedAtMs = Date.now();
const deadline = startedAtMs + timeoutMs;
let latest = await workbenchTraceReadabilitySnapshot(card, traceId);
while (Date.now() <= deadline) {
latest = await workbenchTraceReadabilitySnapshot(card, traceId);
if (workbenchTraceReadabilityTerminalStatus(latest.status)) {
return { observed: true, status: latest.status, elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
await sleep(pollIntervalMs);
}
return { observed: false, status: latest.status, elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
async function workbenchTraceReadabilityWaitForStableTerminal(card, traceId, quietMs, timeoutMs, pollIntervalMs) {
const startedAtMs = Date.now();
const deadline = startedAtMs + timeoutMs;
let stableSinceMs = null;
let previousSignature = null;
let latest = await workbenchTraceReadabilitySnapshot(card, traceId);
let sampleCount = 0;
while (Date.now() <= deadline) {
latest = await workbenchTraceReadabilitySnapshot(card, traceId);
sampleCount += 1;
const signature = [latest.status, latest.disclosure.open, latest.rowCount, latest.sourceAuthorityRowCount, latest.rowSetHash].join("|");
const ready = workbenchTraceReadabilityTerminalStatus(latest.status)
&& latest.disclosure.open === true
&& latest.rowCount > 0
&& latest.sourceAuthorityRowCount > 0
&& latest.readableSourceRowCount > 0;
if (ready && signature === previousSignature) {
stableSinceMs ??= Date.now();
if (Date.now() - stableSinceMs >= quietMs) {
return { observed: true, stable: true, sampleCount, elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
} else {
stableSinceMs = null;
}
previousSignature = signature;
await sleep(pollIntervalMs);
}
return { observed: workbenchTraceReadabilityTerminalStatus(latest.status), stable: false, sampleCount, elapsedMs: Date.now() - startedAtMs, snapshot: latest, valuesRedacted: true };
}
function workbenchTraceReadabilityRetention(running, terminal) {
const runningRowIds = new Set(running?.rowIdentities || []);
const terminalRowIds = new Set(terminal?.rowIdentities || []);
const retainedRowCount = [...runningRowIds].filter((identity) => terminalRowIds.has(identity)).length;
const windowed = terminal?.disclosure?.rowWindowed === true;
if (!windowed) {
return {
mode: "identity-overlap",
passed: retainedRowCount > 0,
retainedRowCount,
valuesRedacted: true
};
}
const runningEventCount = running?.disclosure?.eventCount;
const terminalEventCount = terminal?.disclosure?.eventCount;
const countsAdvance = Number.isInteger(runningEventCount)
&& Number.isInteger(terminalEventCount)
&& terminalEventCount >= runningEventCount;
const sequenceWindowAdvances = Number.isInteger(running?.sourceSeqMin)
&& Number.isInteger(running?.sourceSeqMax)
&& Number.isInteger(terminal?.sourceSeqMin)
&& Number.isInteger(terminal?.sourceSeqMax)
&& terminal.sourceSeqMin >= running.sourceSeqMin
&& terminal.sourceSeqMax >= running.sourceSeqMax;
return {
mode: "window-progress",
passed: countsAdvance && sequenceWindowAdvances,
retainedRowCount,
runningEventCount: runningEventCount ?? null,
terminalEventCount: terminalEventCount ?? null,
runningSourceSeqMin: running?.sourceSeqMin ?? null,
runningSourceSeqMax: running?.sourceSeqMax ?? null,
terminalSourceSeqMin: terminal?.sourceSeqMin ?? null,
terminalSourceSeqMax: terminal?.sourceSeqMax ?? null,
valuesRedacted: true
};
}
function workbenchTraceReadabilityFailures(evidence) {
const failures = [];
const add = (code, message) => failures.push({ code, message, valuesRedacted: true });
if (evidence.scope?.conversationCount !== 1 || evidence.scope?.debugPanelCountInsideConversation !== 0) {
add("product_trace_scope_invalid", "产品 Trace 作用域必须只有一个 #conversation-list,且不得包含隔离调试面板。");
}
if (evidence.startedDuringRunning !== true || evidence.running?.observed !== true) {
add("running_phase_not_observed", "命令未在当前 turn 运行期间观测到可读的产品 Trace。");
}
const running = evidence.running?.snapshot;
if (evidence.running?.observed === true
&& (running?.sourceAuthorityRowCount !== running?.rowCount || running?.projectedAuthorityRowCount !== 0)) {
add("running_source_authority_missing", "运行中的产品 Trace 行必须全部使用 source sequence authority。");
}
if (evidence.disclosure?.openBefore !== true || evidence.disclosure?.autoReadable !== true) {
add("trace_not_auto_expanded", "产品 Trace 在命令主动展开前没有默认保持可读。");
}
if (evidence.terminalArrival?.observed !== true) add("terminal_not_observed", "当前产品 Trace 未在 YAML 超时内进入终态。");
const terminal = evidence.terminal?.snapshot;
if (evidence.terminal?.stable !== true || terminal?.disclosure?.open !== true || terminal?.rowCount <= 0) {
add("terminal_trace_not_retained", "产品 Trace 行在终态后没有保留在展开的详情中。");
}
if (terminal?.sourceAuthorityRowCount <= 0
|| terminal?.readableSourceRowCount <= 0
|| terminal?.sourceAuthorityRowCount !== terminal?.rowCount
|| terminal?.projectedAuthorityRowCount !== 0) {
add("source_authority_missing", "终态产品 Trace 必须展示可读的 source-authority 行,且不得出现 projected-authority 行。");
}
if (evidence.running?.observed === true && evidence.runningRetention?.passed !== true) {
add("running_rows_not_retained", "运行期 Trace 未通过身份重叠或窗口前进合同证明终态保留。");
}
return failures;
}
function workbenchTraceReadabilityEvidenceError(message, evidence) {
const error = new Error(message);
error.details = {
sessionId: evidence?.sessionId || null,
traceId: evidence?.traceId || null,
scope: evidence?.scope || null,
disclosure: evidence?.disclosure || null,
running: evidence?.running || null,
terminalArrival: evidence?.terminalArrival || null,
terminal: evidence?.terminal || null,
runningRetention: evidence?.runningRetention || null,
retainedRunningRowCount: evidence?.retainedRunningRowCount ?? null,
failures: evidence?.failures || null,
screenshot: evidence?.screenshot || null,
reportPath: evidence?.reportPath || null,
reportSha256: evidence?.reportSha256 || null,
valuesRedacted: true
};
return error;
}
async function workbenchTraceReadabilityWriteReport(reportDir, report) {
await mkdir(reportDir, { recursive: true, mode: 0o700 });
const file = path.join(reportDir, "report.json");
await writeFile(file, JSON.stringify(sanitize(report), null, 2) + "\n", { mode: 0o600 });
const meta = await fileMeta(file);
const artifact = { kind: "workbench-trace-readability-report", path: file, byteCount: meta.byteCount, sha256: meta.sha256, commandId: activeCommandId, valuesRedacted: true };
await appendJsonl(files.artifacts, { ts: new Date().toISOString(), ...artifact });
return artifact;
}
`;
}