Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-runner-trace-readability-source.ts
T
2026-07-10 19:23:36 +02:00

583 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 concreteRunningRowIds = [...runningRowIds].filter((identity) => !identity.endsWith(":trace-noise-summary"));
const retainedConcreteRowCount = concreteRunningRowIds.filter((identity) => terminalRowIds.has(identity)).length;
const summaryOnly = runningRowIds.size > 0 && concreteRunningRowIds.length === 0;
const windowed = terminal?.disclosure?.rowWindowed === 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;
if (!windowed && !summaryOnly) {
return {
mode: "identity-overlap",
passed: retainedConcreteRowCount > 0,
retainedRowCount,
retainedConcreteRowCount,
valuesRedacted: true
};
}
return {
mode: windowed ? "window-progress" : "summary-progress",
passed: countsAdvance && sequenceWindowAdvances,
retainedRowCount,
retainedConcreteRowCount,
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;
}
`;
}