// 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 interruptibleRunnerSleep(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 interruptibleRunnerSleep(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 interruptibleRunnerSleep(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 interruptibleRunnerSleep(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 interruptibleRunnerSleep(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;
}
`;
}
