From 92cd1cadf4166935306d7b9f8bd8a7cf7611f446 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 15:41:32 +0000 Subject: [PATCH] fix: render observability diagnostics as tables --- AGENTS.md | 6 + scripts/src/platform-infra-observability.ts | 479 ++++++++++++++++++-- 2 files changed, 440 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b342af6..bf459006 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,12 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - P0: `scripts/cli.ts`、`scripts/src/**`、`trans`、`tran` 和轻量 helper 的 CLI 改动,除非用户明确要求,否则不做单元测试、合同测试或新增测试脚本;默认最多只做语法检查和必要的帮助/命令形态人工确认。 - P0: 历史合同测试和 prompt lint 属于过度门禁,不再作为 CLI 变更的默认验证、派单前置或关闭依据;发现旧入口阻碍最新目标时直接删除,不做兼容保留。 +## P0 最高优先级:CLI 输出渐进披露规则 + +- P0: CLI 默认输出必须优先采用类似 Kubernetes CLI 的简洁表格、短摘要和可复制的 drill-down 命令;JSON 只作为 `--json`、`--raw`、`--full` 或明确机器消费模式输出,禁止把大段 JSON 当作面向人的默认阅读界面。 +- P0: 看到高频 CLI 默认输出仍是 JSON、长对象、长数组或容易超过 YAML 配置阈值的噪声输出时,必须见到一个改一个,优先把该命令改成表格优先、可再展开的渐进式披露形态,而不是反复依赖 dump 后再人工抽取。 +- P0: 自动 dump 只作为防止终端爆炸的兜底能力;一旦某个常用命令反复触发 dump,必须把 warning 视为 CLI 可用性缺陷并改进命令自身输出,不能把 dump 文件路径变成长期交互入口。 + ## P0 最高优先级:自有配置 YAML 优先规则 - P0: UniDesk 自有配置一律优先使用 YAML(`.yaml`/`.yml`),包括 `config/` 下的运行面、平台基础设施、节点/lane、部署参数和可调版本配置;除非外部工具硬性要求 JSON/TOML/ENV 等格式,禁止新增 JSON 作为 UniDesk 自有配置真相。 diff --git a/scripts/src/platform-infra-observability.ts b/scripts/src/platform-infra-observability.ts index d0b60354..f887b801 100644 --- a/scripts/src/platform-infra-observability.ts +++ b/scripts/src/platform-infra-observability.ts @@ -4,6 +4,7 @@ import { Buffer } from "node:buffer"; import { readFileSync } from "node:fs"; import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; +import type { RenderedCliResult } from "./output"; import { compactCapture, createYamlFieldReader, @@ -151,7 +152,7 @@ interface DiagnoseCodeAgentOptions extends CommonOptions { export function observabilityHelp(): Record { return { command: "platform-infra observability plan|apply|status|validate|trace|search|diagnose-code-agent", - output: "json", + output: "text/table by default for search and diagnose-code-agent; use --full or --raw for structured JSON", configTruth: "config/platform-infra/observability.yaml", spec: "PJ2026-01060501 OTel追踪 draft-2026-06-19-p0", usage: [ @@ -169,7 +170,7 @@ export function observabilityHelp(): Record { }; } -export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise> { +export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (action === "plan") return plan(parseCommonOptions(args.slice(1))); if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1))); @@ -638,15 +639,30 @@ async function validate(config: UniDeskConfig, options: CommonOptions): Promise< }; } -async function trace(config: UniDeskConfig, options: TraceOptions): Promise> { +async function trace(config: UniDeskConfig, options: TraceOptions): Promise | RenderedCliResult> { if (options.traceId === null) throw new Error("observability trace requires --trace-id "); const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const tracePath = observability.probes.traceQueryPathTemplate.replaceAll("{{traceId}}", encodeURIComponent(options.traceId)); const result = await capture(config, target.route, ["sh"], traceScript(observability, target, tracePath, options)); const parsed = parseJsonOutput(result.stdout); + const ok = result.exitCode === 0 && parsed?.ok === true; + if (!options.full && !options.raw) { + return renderTraceTable({ + ok, + target, + options, + tracePath, + query: { + backend: observability.traceBackend.type, + service: observability.traceBackend.serviceName, + path: tracePath, + }, + result: parsed === null ? { ok: false, remote: compactCapture(result, { full: false }) } : asPlainRecord(redactSensitiveUnknown(parsed)) ?? {}, + }); + } return { - ok: result.exitCode === 0 && parsed?.ok === true, + ok, action: "platform-infra-observability-trace", mutation: false, target: targetSummary(target), @@ -660,7 +676,7 @@ async function trace(config: UniDeskConfig, options: TraceOptions): Promise> { +async function search(config: UniDeskConfig, options: SearchOptions): Promise | RenderedCliResult> { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const endSeconds = Math.floor(Date.now() / 1000); @@ -675,29 +691,38 @@ async function search(config: UniDeskConfig, options: SearchOptions): Promise> { +async function diagnoseCodeAgent(config: UniDeskConfig, options: DiagnoseCodeAgentOptions): Promise | RenderedCliResult> { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const endSeconds = Math.floor(Date.now() / 1000); @@ -725,30 +750,386 @@ async function diagnoseCodeAgent(config: UniDeskConfig, options: DiagnoseCodeAge } const result = await capture(config, target.route, ["sh"], diagnoseCodeAgentScript(observability, target, searchPath, options)); const parsed = parseJsonOutput(result.stdout); + const ok = result.exitCode === 0 && parsed?.ok === true; + const query = { + backend: observability.traceBackend.type, + service: observability.traceBackend.serviceName, + businessTraceId: options.businessTraceId, + traceId: options.traceId, + path: searchPath, + lookbackMinutes: options.lookbackMinutes, + candidateLimit: options.candidateLimit, + limit: options.limit, + }; + if (!options.full && !options.raw) { + return renderDiagnoseCodeAgentTable({ + ok, + target, + options, + query, + result: parsed === null ? { ok: false, remote: compactCapture(result, { full: false }) } : compactDiagnoseCodeAgentResult(parsed), + }); + } const exposedResult = parsed === null ? compactCapture(result, { full: true }) - : options.full || options.raw - ? redactSensitiveUnknown(parsed) - : redactSensitiveUnknown(compactDiagnoseCodeAgentResult(parsed)); + : redactSensitiveUnknown(parsed); return { - ok: result.exitCode === 0 && parsed?.ok === true, + ok, action: "platform-infra-observability-diagnose-code-agent", mutation: false, target: targetSummary(target), - query: { - backend: observability.traceBackend.type, - service: observability.traceBackend.serviceName, - businessTraceId: options.businessTraceId, - traceId: options.traceId, - path: searchPath, - lookbackMinutes: options.lookbackMinutes, - candidateLimit: options.candidateLimit, - limit: options.limit, - }, + query, result: exposedResult, }; } +function renderTraceTable(input: { + ok: boolean; + target: ObservabilityTarget; + options: TraceOptions; + tracePath: string; + query: Record; + result: Record; +}): RenderedCliResult { + const spanRows = uniqueSpanRecords([...asArray(input.result.errorSpans), ...asArray(input.result.spans)]) + .slice(0, 10) + .map((span) => { + return [ + shortenEnd(textValue(span.name), 48), + shortenEnd(textValue(span.service), 28), + spanDetail(span, 140), + ]; + }); + const countRows = asArray(input.result.spanNameCounts).slice(0, 8).map((item) => { + const row = asPlainRecord(item) ?? {}; + return [shortenEnd(textValue(row.name), 64), textValue(row.count)]; + }); + const next = asPlainRecord(input.result.next); + const lines = [ + `platform-infra observability trace (${input.ok ? "ok" : "not-ok"})`, + "", + formatTable(["TRACE", "SPANS", "ERR", "MATCH", "SERVICES", "BUSINESS_TRACE"], [[ + shortenMiddle(input.options.traceId ?? "-", 20), + textValue(input.result.spanCount), + textValue(input.result.errorSpanCount), + textValue(input.result.matchedSpanCount), + joinValues(input.result.services, 44), + joinValues(input.result.businessTraceIds, 34), + ]]), + "", + "Key spans:", + formatTable(["NAME", "SERVICE", "DETAIL"], spanRows.length > 0 ? spanRows : [["-", "-", "-"]]), + "", + "Span name counts:", + formatTable(["NAME", "COUNT"], countRows.length > 0 ? countRows : [["-", "-"]]), + "", + "Summary:", + ` target=${input.target.id} backend=${textValue(input.query.backend)} service=${textValue(input.query.service)} path=${input.tracePath}`, + ` grep=${textValue(input.result.grep)} limit=${textValue(input.result.limit)} traceFound=${textValue(input.result.traceFound)} parseOk=${textValue(input.result.parseOk)}`, + "", + "Next:", + ]; + const nextCommands = [ + textValue(next?.fullSummary), + textValue(next?.grepProviderStream), + textValue(next?.raw), + ].filter((item) => item !== "-"); + if (nextCommands.length > 0) { + for (const command of nextCommands.slice(0, 3)) lines.push(` ${command}`); + } else { + lines.push(` ${buildTraceCommand(input.target, input.options, true)}`); + } + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use --full for structured span summary JSON or --raw for backend response."); + return { + ok: input.ok, + command: "platform-infra observability trace", + contentType: "text/plain", + renderedText: lines.join("\n"), + }; +} + +function renderSearchTable(input: { + ok: boolean; + target: ObservabilityTarget; + options: SearchOptions; + query: Record; + result: Record; +}): RenderedCliResult { + const traces = asArray(input.result.traces).map((item) => asPlainRecord(item) ?? {}); + const rows = traces.slice(0, 12).map((trace) => [ + shortenMiddle(textValue(trace.traceId), 20), + searchTraceStatus(trace), + numberValue(trace.spanCount), + numberValue(trace.errorSpanCount), + numberValue(trace.matchedSpanCount), + joinValues(trace.services, 46), + ]); + const lines = [ + `platform-infra observability search (${input.ok ? "ok" : "not-ok"})`, + "", + formatTable(["TRACE", "STATUS", "SPANS", "ERR", "MATCH", "SERVICES"], rows.length > 0 ? rows : [["-", "no-match", "-", "-", "-", "-"]]), + "", + "Summary:", + ` target=${input.target.id} backend=${textValue(input.query.backend)} service=${textValue(input.query.service)}`, + ` grep=${textValue(input.query.grep)} path=${textValue(input.query.pathFilter)} status=${textValue(input.query.statusFilter)} tempoQuery=${shortenMiddle(textValue(input.query.tempoQuery), 80)}`, + ` lookbackMinutes=${textValue(input.query.lookbackMinutes)} candidateLimit=${textValue(input.query.candidateLimit)} limit=${textValue(input.query.limit)}`, + ` candidates=${textValue(input.result.candidateTraceCount)} scanned=${textValue(input.result.scannedTraceCount)} matched=${textValue(input.result.matchedTraceCount)} stopped=${textValue(input.result.scanStopped)}`, + ]; + const firstTraceId = traces.length > 0 ? textValue(traces[0].traceId) : ""; + lines.push("", "Next:"); + if (firstTraceId.length > 0 && firstTraceId !== "-") { + lines.push(` bun scripts/cli.ts platform-infra observability trace --target ${input.target.id} --trace-id ${firstTraceId}`); + } + lines.push(` ${buildSearchCommand(input.target, input.options, true)}`); + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use --full for structured diagnosis JSON or trace --trace-id for one trace."); + return { + ok: input.ok, + command: "platform-infra observability search", + contentType: "text/plain", + renderedText: lines.join("\n"), + }; +} + +function renderDiagnoseCodeAgentTable(input: { + ok: boolean; + target: ObservabilityTarget; + options: DiagnoseCodeAgentOptions; + query: Record; + result: Record; +}): RenderedCliResult { + const mapping = asPlainRecord(input.result.mapping); + const identity = asPlainRecord(input.result.identity); + const agentrun = asPlainRecord(input.result.agentrun); + const projectionLag = asPlainRecord(input.result.projectionLag); + const summary = asPlainRecord(input.result.summary); + const evidence = asPlainRecord(input.result.evidence); + const rootCauses = asArray(input.result.rootCauseCandidates).map((item) => asPlainRecord(item) ?? {}); + const http = asPlainRecord(input.result.http); + const services = joinValues(input.result.services, 54); + const traceId = textValue(mapping?.otelTraceId ?? input.query.traceId); + const rootCause = textValue(summary?.rootCause ?? rootCauses[0]?.code); + const rows = [[ + shortenMiddle(traceId, 20), + shortenEnd(rootCause, 34), + textValue(agentrun?.terminalStatus), + textValue(agentrun?.runnerProviderClassification), + textValue(projectionLag?.status), + textValue(evidence?.errorSpanCount), + services, + ]]; + const rootRows = rootCauses.slice(0, 5).map((candidate) => [ + shortenEnd(textValue(candidate.code), 40), + textValue(candidate.confidence), + shortenEnd(textValue(candidate.summary ?? candidate.label), 80), + ]); + const httpRows = httpTableRows(http); + const lines = [ + `platform-infra observability diagnose-code-agent (${input.ok ? "ok" : "not-ok"})`, + "", + formatTable(["TRACE", "ROOT_CAUSE", "TERMINAL", "RUNNER", "PROJECTION", "ERR", "SERVICES"], rows), + "", + "Identity:", + ` businessTraceId=${textValue(mapping?.businessTraceId ?? input.query.businessTraceId)} otelTraceId=${traceId}`, + ` runId=${textValue(identity?.runId)} commandId=${textValue(identity?.commandId)} runnerJobId=${textValue(identity?.runnerJobId)} runnerId=${textValue(identity?.runnerId)}`, + ` backendProfile=${textValue(identity?.backendProfile)} sourceCommit=${shortenMiddle(textValue(identity?.sourceCommit), 20)}`, + "", + "Root causes:", + formatTable(["CODE", "CONF", "SUMMARY"], rootRows.length > 0 ? rootRows : [["-", "-", "-"]]), + "", + "HTTP:", + formatTable(["METHOD", "ROUTE", "STATUS", "COUNT"], httpRows.length > 0 ? httpRows : [["-", "-", "-", "-"]]), + "", + "Summary:", + ` target=${input.target.id} spanCount=${textValue(input.result.spanCount)} servicePath=${joinValues(input.result.servicePath, 60)}`, + ` readModel=${shortenEnd(JSON.stringify(input.result.hwlabReadModel ?? null), 140)}`, + ]; + const next = asPlainRecord(input.result.next); + lines.push("", "Next:"); + const nextCommands = [ + textValue(next?.diagnoseFull), + textValue(next?.traceSummary), + textValue(next?.traceReads), + ].filter((item) => item !== "-"); + if (nextCommands.length > 0) { + for (const command of nextCommands.slice(0, 3)) lines.push(` ${command}`); + } else { + lines.push(` ${buildDiagnoseCommand(input.target, input.options, true)}`); + if (traceId.length > 0 && traceId !== "-") lines.push(` bun scripts/cli.ts platform-infra observability trace --target ${input.target.id} --trace-id ${traceId}`); + } + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use --full for structured diagnosis JSON or trace --grep for span-level drill-down."); + return { + ok: input.ok, + command: "platform-infra observability diagnose-code-agent", + contentType: "text/plain", + renderedText: lines.join("\n"), + }; +} + +function searchTraceStatus(trace: Record): string { + if (trace.queryOk === false) return "query-bad"; + if (trace.parseOk === false) return "parse-bad"; + const errorCount = numberFromUnknown(trace.errorSpanCount); + if (errorCount > 0) return "error"; + const matchedCount = numberFromUnknown(trace.matchedSpanCount); + if (matchedCount > 0 || trace.rawMatched === true) return "matched"; + return "ok"; +} + +function uniqueSpanRecords(values: unknown[]): Record[] { + const rows: Record[] = []; + const seen = new Set(); + for (const value of values) { + const span = asPlainRecord(value) ?? {}; + const key = `${textValue(span.name)}\n${textValue(span.service)}\n${spanDetail(span, 200)}`; + if (seen.has(key)) continue; + seen.add(key); + rows.push(span); + } + return rows; +} + +function spanDetail(span: Record, maxLength: number): string { + const attrs = asPlainRecord(span.attributes); + const parts = [ + attrPart(attrs, "failureKind"), + attrPart(attrs, "errorSummary"), + attrPart(attrs, "error.message"), + attrPart(attrs, "exception.message"), + attrPart(attrs, "terminalStatus"), + attrPart(attrs, "status"), + attrPart(attrs, "toolName"), + attrPart(attrs, "exitCode"), + attrPart(attrs, "outputSummary"), + attrPart(attrs, "outputBytes"), + attrPart(attrs, "durationMs"), + attrPart(attrs, "cwd"), + attrPart(attrs, "waitingFor"), + attrPart(attrs, "lastEventLabel"), + attrPart(attrs, "idleMs"), + attrPart(attrs, "idleSeconds"), + attrPart(attrs, "commandFingerprint"), + attrPart(attrs, "http.route"), + attrPart(attrs, "http.response.status_code"), + ].filter((item) => item !== null) as string[]; + if (parts.length === 0) return "-"; + return shortenEnd(parts.join(" "), maxLength); +} + +function attrPart(attrs: Record | null, key: string): string | null { + if (attrs === null) return null; + const value = attrs[key]; + if (value === null || value === undefined || value === "") return null; + return `${key}=${textValue(value)}`; +} + +function httpTableRows(http: Record | null): string[][] { + if (http === null) return []; + const problemRows = asArray(http.problemCounts).map((item) => { + const row = asPlainRecord(item) ?? {}; + return [ + textValue(row.method), + shortenEnd(textValue(row.route ?? row.path), 44), + textValue(row.status ?? row.statusCode), + textValue(row.count), + ]; + }); + const statusRows = asArray(http.statusCounts).map((item) => { + const row = asPlainRecord(item) ?? {}; + return [ + textValue(row.method), + shortenEnd(textValue(row.route ?? row.path), 44), + textValue(row.status ?? row.statusCode), + textValue(row.count), + ]; + }); + return [...problemRows, ...statusRows].slice(0, 8); +} + +function buildTraceCommand(target: ObservabilityTarget, options: TraceOptions, full: boolean): string { + const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "trace", "--target", target.id]; + if (options.traceId !== null) parts.push("--trace-id", options.traceId); + if (options.grep !== null) parts.push("--grep", options.grep); + parts.push("--limit", String(options.limit)); + if (full) parts.push("--full"); + return parts.map(cliArg).join(" "); +} + +function buildSearchCommand(target: ObservabilityTarget, options: SearchOptions, full: boolean): string { + const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "search", "--target", target.id]; + if (options.grep !== null) parts.push("--grep", options.grep); + if (options.query !== null) parts.push("--query", options.query); + if (options.path !== null) parts.push("--path", options.path); + if (options.status !== null) parts.push("--status", String(options.status)); + parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit)); + if (full) parts.push("--full"); + return parts.map(cliArg).join(" "); +} + +function buildDiagnoseCommand(target: ObservabilityTarget, options: DiagnoseCodeAgentOptions, full: boolean): string { + const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "diagnose-code-agent", "--target", target.id]; + if (options.businessTraceId !== null) parts.push("--business-trace-id", options.businessTraceId); + if (options.traceId !== null) parts.push("--trace-id", options.traceId); + parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit)); + if (full) parts.push("--full"); + return parts.map(cliArg).join(" "); +} + +function cliArg(value: string): string { + return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : shQuote(value); +} + +function formatTable(headers: string[], rows: string[][]): string { + const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? "")); + const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length))); + return [ + headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(), + ...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()), + ].join("\n"); +} + +function textValue(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function numberValue(value: unknown): string { + const number = numberFromUnknown(value); + return Number.isFinite(number) ? String(number) : "-"; +} + +function numberFromUnknown(value: unknown): number { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return Number.NaN; +} + +function joinValues(value: unknown, maxLength: number): string { + const joined = asArray(value).map(textValue).filter((item) => item !== "-").join(","); + return shortenEnd(joined.length > 0 ? joined : textValue(value), maxLength); +} + +function shortenEnd(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + if (maxLength <= 1) return value.slice(0, maxLength); + return `${value.slice(0, maxLength - 1)}~`; +} + +function shortenMiddle(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + if (maxLength <= 3) return value.slice(0, maxLength); + const left = Math.ceil((maxLength - 1) / 2); + const right = Math.floor((maxLength - 1) / 2); + return `${value.slice(0, left)}~${value.slice(value.length - right)}`; +} + function compactSearchResult(value: unknown): Record { const source = asPlainRecord(value) ?? {}; const traces = asArray(source.traces).map(compactSearchTrace); @@ -822,9 +1203,9 @@ function compactDiagnoseCodeAgentResult(value: unknown): Record selectedQuality: mapping.selectedQuality ?? null, selectedLowConfidence: mapping.selectedLowConfidence ?? null, selectedRejectedReason: mapping.selectedRejectedReason ?? null, - selectedReasons: limitArray(mapping.selectedReasons, 8), + selectedReasons: limitArray(mapping.selectedReasons, 4), selectedMeta: compactMetaRecord(mapping.selectedMeta), - candidatePreview: asArray(mapping.candidatePreview).slice(0, 3).map(compactDiagnoseCandidate), + bestCandidate: asArray(mapping.candidatePreview).slice(0, 1).map(compactDiagnoseCandidate)[0] ?? null, searchStderrTail: mapping.searchStderrTail ?? null, }, tracePath: source.tracePath ?? null, @@ -841,7 +1222,7 @@ function compactDiagnoseCodeAgentResult(value: unknown): Record projectionLag: source.projectionLag ?? null, summary: source.summary ?? null, rootCauseCandidates: asArray(source.rootCauseCandidates).slice(0, 5).map(compactRootCauseCandidate), - spanNameCounts: compactNameCounts(source.spanNameCounts, 10), + spanNameCounts: compactNameCounts(source.spanNameCounts, 6), evidence: evidence === null ? null : { httpProblemSpanCount: evidence.httpProblemSpanCount ?? null, terminalSpanCount: evidence.terminalSpanCount ?? null, @@ -852,7 +1233,7 @@ function compactDiagnoseCodeAgentResult(value: unknown): Record idleWarningSpanCount: evidence.idleWarningSpanCount ?? null, idleWarningSpanTail: compactSpanList(evidence.idleWarningSpanTail, 3), }, - next: source.next ?? null, + next: compactDiagnoseNext(source.next), stderrTail: source.stderrTail ?? null, disclosure: { defaultView: "compact diagnosis only; detailed span evidence is omitted to keep stdout below config/unidesk-cli.yaml output.maxStdoutBytes", @@ -866,7 +1247,7 @@ function compactDiagnoseCandidate(value: unknown): Record { return { traceId: candidate.traceId ?? null, score: candidate.score ?? null, - reasons: limitArray(candidate.reasons, 6), + reasons: limitArray(candidate.reasons, 3), parseOk: candidate.parseOk ?? null, queryOk: candidate.queryOk ?? null, spanCount: candidate.spanCount ?? null, @@ -877,13 +1258,21 @@ function compactDiagnoseCandidate(value: unknown): Record { errorSpanCount: candidate.errorSpanCount ?? null, candidateQuality: candidate.candidateQuality ?? null, lowConfidence: candidate.lowConfidence ?? null, - rootTraceName: candidate.rootTraceName ?? null, - rootServiceName: candidate.rootServiceName ?? null, - spanNamePreview: limitArray(candidate.spanNamePreview, 8), + spanNamePreview: limitArray(candidate.spanNamePreview, 5), traceCommand: candidate.traceCommand ?? null, }; } +function compactDiagnoseNext(value: unknown): Record | null { + const next = asPlainRecord(value); + if (next === null) return null; + return { + diagnoseFull: next.diagnoseFull ?? null, + traceSummary: next.traceSummary ?? null, + traceReads: next.traceReads ?? null, + }; +} + function compactRootCauseCandidate(value: unknown): Record { const candidate = asPlainRecord(value) ?? {}; return {