fix: render observability diagnostics as tables

This commit is contained in:
Codex
2026-06-21 15:41:32 +00:00
parent 177d747e92
commit 92cd1cadf4
2 changed files with 440 additions and 45 deletions
+6
View File
@@ -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 自有配置真相。
+434 -45
View File
@@ -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<string, unknown> {
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<string, unknown> {
};
}
export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | 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<Record<string, unknown>> {
async function trace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
if (options.traceId === null) throw new Error("observability trace requires --trace-id <traceId>");
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<Reco
};
}
async function search(config: UniDeskConfig, options: SearchOptions): Promise<Record<string, unknown>> {
async function search(config: UniDeskConfig, options: SearchOptions): Promise<Record<string, unknown> | 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<Re
const searchPath = `/api/search?${params.toString()}`;
const result = await capture(config, target.route, ["sh"], searchScript(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,
path: searchPath,
grep: options.grep,
tempoQuery,
explicitTempoQuery: options.query,
pathFilter: options.path,
statusFilter: options.status,
lookbackMinutes: options.lookbackMinutes,
candidateLimit: options.candidateLimit,
limit: options.limit,
};
if (!options.full && !options.raw) {
return renderSearchTable({
ok,
target,
options,
query,
result: parsed === null ? { ok: false, remote: compactCapture(result, { full: false }) } : compactSearchResult(parsed),
});
}
const exposedResult = parsed === null
? compactCapture(result, { full: true })
: options.full || options.raw
? redactSensitiveUnknown(parsed)
: redactSensitiveUnknown(compactSearchResult(parsed));
: redactSensitiveUnknown(parsed);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
ok,
action: "platform-infra-observability-search",
mutation: false,
target: targetSummary(target),
query: {
backend: observability.traceBackend.type,
service: observability.traceBackend.serviceName,
path: searchPath,
grep: options.grep,
tempoQuery,
explicitTempoQuery: options.query,
pathFilter: options.path,
statusFilter: options.status,
lookbackMinutes: options.lookbackMinutes,
candidateLimit: options.candidateLimit,
limit: options.limit,
},
query,
result: exposedResult,
};
}
@@ -708,7 +733,7 @@ function inferSearchTempoQuery(options: SearchOptions): string | null {
return null;
}
async function diagnoseCodeAgent(config: UniDeskConfig, options: DiagnoseCodeAgentOptions): Promise<Record<string, unknown>> {
async function diagnoseCodeAgent(config: UniDeskConfig, options: DiagnoseCodeAgentOptions): Promise<Record<string, unknown> | 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<string, unknown>;
result: Record<string, unknown>;
}): 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<string, unknown>;
result: Record<string, unknown>;
}): 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<string, unknown>;
result: Record<string, unknown>;
}): 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, unknown>): 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<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
const seen = new Set<string>();
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<string, unknown>, 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<string, unknown> | 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<string, unknown> | 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<string, unknown> {
const source = asPlainRecord(value) ?? {};
const traces = asArray(source.traces).map(compactSearchTrace);
@@ -822,9 +1203,9 @@ function compactDiagnoseCodeAgentResult(value: unknown): Record<string, unknown>
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<string, unknown>
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<string, unknown>
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<string, unknown> {
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<string, unknown> {
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<string, unknown> | 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<string, unknown> {
const candidate = asPlainRecord(value) ?? {};
return {