Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-render.ts
T
2026-06-29 12:17:52 +00:00

627 lines
30 KiB
TypeScript

// SPEC: PJ2026-01040111 long-running Workbench observation.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// Responsibility: CLI text rendering for web-probe observe status/command/collect.
import type { RenderedCliResult } from "./output";
import { renderWebObserveWrapperContract } from "./hwlab-node-web-observe-wrapper-render";
export function withWebObserveStatusRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "web-probe observe status",
contentType: "text/plain",
renderedText: renderWebObserveStatusTable(value),
};
}
export function withWebObserveCommandRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "web-probe observe command",
contentType: "text/plain",
renderedText: renderWebObserveCommandTable(value),
};
}
export function withWebObserveCollectRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "web-probe observe collect",
contentType: "text/plain",
renderedText: renderWebObserveCollectTable(value),
};
}
function renderWebObserveStatusTable(value: Record<string, unknown>): string {
const observer = record(value.observer);
const manifest = record(observer?.manifest);
const heartbeat = record(observer?.heartbeat);
const diagnostics = record(observer?.diagnostics) ?? record(value.diagnostics);
const commands = record(observer?.commands);
const tails = record(observer?.tails);
const result = record(value.result);
const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-5) : [];
const controls = Array.isArray(tails?.control) ? tails.control.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-5) : [];
const completedControlIds = new Set(controls
.filter((item) => item.phase === "completed" || item.phase === "failed")
.map((item) => webObserveText(item.commandId))
.filter((item) => item !== "-"));
const activeControl = controls.slice().reverse().find((item) => item.phase === "started" && !completedControlIds.has(webObserveText(item.commandId))) ?? null;
const activeControlAgeSeconds = activeControl === null ? null : Math.max(0, Math.round((Date.now() - Date.parse(webObserveText(activeControl.ts))) / 1000));
const network = Array.isArray(tails?.network)
? tails.network.map(record).filter((item): item is Record<string, unknown> => item !== null).filter((item) => {
const status = typeof item.status === "number" ? item.status : null;
return status === null || status >= 400 || item.failure !== null && item.failure !== undefined;
}).slice(-5)
: [];
const next = record(value.next);
const heartbeatError = record(heartbeat?.error) ?? record(manifest?.error);
const heartbeatAuth = record(heartbeat?.auth) ?? record(heartbeatError?.auth);
const manifestNetwork = record(manifest?.network);
const manifestBrowser = record(manifestNetwork?.browser);
const manifestProxy = record(manifestNetwork?.proxy);
const heartbeatAge = diagnostics?.heartbeatAgeSeconds ?? heartbeat?.ageSeconds;
const heartbeatStale = diagnostics?.heartbeatStale ?? heartbeat?.stale;
const heartbeatLiveness = diagnostics?.effectiveLiveness ?? heartbeat?.effectiveLiveness;
const lines = [
`web-probe observe status (${webObserveText(value.status)})`,
"",
...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "LIVENESS", "ALIVE", "PID", "SAMPLE", "CMD_SEQ", "HB_AGE_S", "STALE", "UPDATED", "TARGET"], [[
value.id,
value.node,
value.lane,
heartbeatLiveness,
observer?.processAlive,
observer?.pid,
heartbeat?.sampleSeq,
heartbeat?.commandSeq,
heartbeatAge,
heartbeatStale,
webObserveShort(webObserveText(heartbeat?.updatedAt ?? heartbeat?.lastSampleAt), 24),
webObserveShort(`${webObserveText(manifest?.baseUrl)}${webObserveText(manifest?.targetPath) === "-" ? "" : webObserveText(manifest?.targetPath)}`, 52),
]]),
"",
"Runner diagnostics:",
webObserveTable(["HEARTBEAT_STALE_AFTER_S", "COMMAND_BACKLOG", "PENDING", "PROCESSING", "ABANDONED", "FAILED", "OLDEST_PENDING_S"], [[
diagnostics?.heartbeatStaleAfterSeconds,
diagnostics?.commandBacklog,
commands?.pendingCount,
commands?.processingCount,
commands?.abandonedCount,
commands?.failedCount,
diagnostics?.oldestPendingAgeSeconds,
]]),
"",
...(value.ok === false ? [
"Blocked detail:",
webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDOUT", "STDERR"], [[
webObserveShort(webObserveText(value.degradedReason), 48),
result.exitCode,
result.timedOut,
webObserveShort(webObserveText(result.stdoutTail), 120),
webObserveShort(webObserveText(result.stderr), 120),
]]),
"",
] : []),
...(heartbeatError !== null ? [
"Observer error:",
webObserveTable(["STATUS", "RETRY", "EXHAUSTED", "BROWSER_PROXY", "MESSAGE"], [[
heartbeat?.status ?? manifest?.status,
heartbeatAuth?.lastRetryLabel,
heartbeatAuth?.retryExhausted,
webObserveShort(webObserveText(manifestBrowser?.proxyMode ?? manifestProxy?.enabled), 32),
webObserveShort(webObserveText(heartbeatError.message ?? heartbeatAuth?.lastError), 120),
]]),
"",
] : []),
...(heartbeatAuth !== null ? [
"Auth progress:",
webObserveTable(["PHASE", "RETRY", "DELAY_MS", "STATUS", "RETRYABLE", "COOKIE", "EXHAUSTED", "LAST_ERROR"], [[
heartbeatAuth.phase,
heartbeatAuth.lastRetryLabel,
heartbeatAuth.retryDelayMs,
heartbeatAuth.lastStatusText === undefined ? heartbeatAuth.lastStatus : `${webObserveText(heartbeatAuth.lastStatus)} ${webObserveText(heartbeatAuth.lastStatusText)}`,
heartbeatAuth.retryable,
heartbeatAuth.cookiePresent,
heartbeatAuth.retryExhausted,
webObserveShort(webObserveText(heartbeatAuth.lastError), 80),
]]),
"",
] : []),
...(activeControl !== null ? [
"Active command:",
webObserveTable(["TYPE", "COMMAND", "AGE_S", "STARTED_AT", "DETAIL"], [[
activeControl.type,
webObserveShort(webObserveText(activeControl.commandId), 28),
activeControlAgeSeconds,
webObserveShort(webObserveText(activeControl.ts), 24),
webObserveShort(webObserveText(activeControl.detail), 80),
]]),
"",
] : []),
"Recent samples:",
webObserveTable(["SEQ", "TS", "PATH", "ROUTE_SESSION", "ACTIVE_SESSION", "MSG", "TRACE"], samples.length > 0 ? samples.map((sample) => [
sample.seq,
webObserveShort(webObserveText(sample.ts), 24),
webObserveShort(webObserveText(sample.path), 24),
webObserveShort(webObserveText(sample.routeSessionId), 20),
webObserveShort(webObserveText(sample.activeSessionId), 20),
sample.messageCount,
sample.traceRowCount,
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"Recent control:",
webObserveTable(["SEQ", "TS", "PHASE", "TYPE", "COMMAND", "RETRY", "DETAIL"], controls.length > 0 ? controls.map((control) => [
control.seq,
webObserveShort(webObserveText(control.ts), 24),
control.phase,
control.type,
webObserveShort(webObserveText(control.commandId), 28),
control.retry,
webObserveShort(webObserveText(control.detail), 80),
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"Network alerts:",
webObserveTable(["TS", "METHOD", "STATUS", "TYPE", "URL_OR_FAILURE"], network.length > 0 ? network.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
item.method,
item.status,
item.type,
webObserveShort(webObserveText(item.failure ?? item.url), 80),
]) : [["-", "-", "-", "-", "-"]]),
"",
"Next:",
];
for (const key of ["status", "command", "stop", "analyze"] as const) {
const command = webObserveText(next?.[key]);
if (command !== "-") lines.push(` ${command}`);
}
const forceStop = webObserveText(next?.forceStop);
if (forceStop !== "-") lines.push(` ${forceStop}`);
lines.push("", "Disclosure:");
lines.push(" default view is a bounded table; use observe collect/analyze for artifact-level drill-down.");
return lines.join("\n");
}
function renderWebObserveCommandTable(value: Record<string, unknown>): string {
const observerCommand = record(value.observerCommand);
const observer = record(value.observer);
const result = record(observer?.result);
const id = webObserveText(value.id);
const full = value.full === true;
const lines = [
`web-probe observe command (${webObserveText(value.status)})`,
"",
...renderWebObserveWrapperContract(value),
webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[
id,
value.commandId,
observerCommand?.type,
observer?.ok === false ? "failed" : observer?.completedAt !== undefined ? "completed" : value.status,
observerCommand?.textBytes,
webObserveShort(webObserveText(observerCommand?.textHash), 24),
webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80),
]]),
...(full ? [
"",
"Full command result:",
"```json",
JSON.stringify(observer ?? {}, null, 2),
"```",
] : []),
"",
"Next:",
` bun scripts/cli.ts web-probe observe status ${id}`,
` bun scripts/cli.ts web-probe observe analyze ${id}`,
"",
"Disclosure:",
" default view is a bounded command result; pass --full to expose the complete command result JSON.",
" use observe status/analyze for sampled state.",
];
return lines.join("\n");
}
function renderWebObserveCollectTable(value: Record<string, unknown>): string {
const collect = nullableRecord(value.collect);
if (typeof collect?.renderedText === "string") {
return [
`web-probe observe collect (${webObserveText(value.status)})`,
"",
...renderWebObserveWrapperContract(value),
collect.renderedText,
"",
"Source:",
webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[
value.id,
value.node,
value.lane,
collect.view ?? value.view,
webObserveShort(webObserveText(collect.stateDir), 88),
]]),
"",
"Disclosure:",
" collect views are rendered from existing samples/control/analysis artifacts; they do not create a new sampling source.",
].join("\n");
}
const collectView = webObserveText(collect?.view ?? value.view);
if (collect !== null && (collectView === "project-summary" || collectView === "project-mdtodo-summary")) {
return renderWebObserveProjectCollectTable(value, collect);
}
const result = record(value.result);
const file = record(collect?.file);
const files = Array.isArray(collect?.files) ? collect.files.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 20) : [];
const jsonlTail = Array.isArray(file.jsonlTail) ? file.jsonlTail.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
const jsonSummary = record(file.jsonSummary);
const grepSummary = record(file.grep);
const grepLines = Array.isArray(grepSummary?.lines) ? grepSummary.lines.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 40) : [];
const jsonContentPreview = file.jsonContent === undefined || file.jsonContent === null ? "" : JSON.stringify(file.jsonContent, null, 2);
const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
const jsonFindingDetail = record(jsonSummary.findingDetail);
const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : [];
const lines = [
`web-probe observe collect (${webObserveText(value.status)})`,
"",
...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[
value.id,
value.node,
value.lane,
collect?.mode ?? "files",
webObserveShort(webObserveText(collect?.stateDir), 88),
]]),
"",
];
if (collect === null) {
lines.push(
"Collect command:",
webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[
webObserveShort(webObserveText(value.requestedFile), 64),
webObserveShort(webObserveText(value.degradedReason), 40),
result.exitCode,
result.timedOut,
result.stdoutBytes,
webObserveShort(webObserveText(result.stdoutTail), 160),
webObserveShort(webObserveText(result.stderr), 160),
]]),
"",
"Disclosure:",
" collect stdout was not valid JSON; fix the collect command or rerun with a narrower --file after the root cause is visible.",
);
return lines.join("\n");
}
if (collect.mode === "file") {
lines.push(
"File:",
webObserveTable(["RELATIVE", "BYTES", "TRUNC", "SHA256"], [[
webObserveShort(webObserveText(file.relative), 64),
file.byteCount,
file.truncated,
webObserveShort(webObserveText(file.sha256), 28),
]]),
"",
);
if (Object.keys(jsonSummary).length > 0) {
lines.push(
"JSON summary:",
webObserveTable(["KEYS", "COUNTS", "PARSE_ERROR"], [[
webObserveShort(webObserveText(jsonSummary.topLevelKeys), 80),
webObserveShort(webObserveText(jsonSummary.counts), 80),
webObserveShort(webObserveText(jsonSummary.parseError), 80),
]]),
"",
);
}
if (grepSummary !== null) {
lines.push(
"Grep matches:",
webObserveTable(["PATTERN_SHA", "MATCHES", "RETURNED", "TRUNC"], [[
webObserveShort(webObserveText(grepSummary.patternSha256), 24),
grepSummary.matchCount,
grepSummary.returnedLineCount,
grepSummary.truncated,
]]),
webObserveTable(["LINE", "MATCH", "TEXT"], grepLines.length > 0 ? grepLines.map((item) => [
item.lineNo,
item.match === true,
webObserveShort(webObserveText(item.text), 360),
]) : [["-", "-", "-"]]),
"Grep context:",
grepLines.length > 0
? grepLines.map((item) => "- " + webObserveText(item.lineNo) + (item.match === true ? " * " : " ") + webObserveShort(webObserveText(item.text), 500)).join("\n")
: "-",
"",
);
}
if (jsonContentPreview.length > 0) {
lines.push("JSON content:", webObserveShort(jsonContentPreview, 2400), "");
}
if (jsonRunnerErrors.length > 0) {
lines.push(
"Runner errors:",
webObserveTable(["TS", "TYPE", "ATTEMPTS", "READY", "MESSAGE"], jsonRunnerErrors.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
webObserveShort(webObserveText(item.type), 20),
item.attemptCount,
webObserveShort(webObserveText(item.lastReadinessReason), 24),
webObserveShort(webObserveText(item.message ?? item.lastError), 96),
])),
"",
);
}
if (jsonFindings.length > 0) {
lines.push(
"Findings:",
webObserveTable(["KIND", "SEVERITY", "COUNT", "TIMING", "ROOT_CAUSE", "SUMMARY"], jsonFindings.map((item) => [
webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48),
item.severity ?? item.level,
item.count ?? item.sampleCount,
webObserveShort(webObserveFindingTiming(item), 42),
webObserveShort(webObserveText(item.rootCause ?? item.rootCauseStatus), 48),
webObserveShort(webObserveText(item.summary ?? item.message), 96),
])),
"",
);
}
if (jsonFindingDetail !== null) {
lines.push(
"Finding detail:",
webObserveTable(["ID", "SEVERITY", "COUNT", "SAMPLE_SOURCE", "SUMMARY"], [[
webObserveShort(webObserveText(jsonFindingDetail.id), 48),
jsonFindingDetail.severity,
jsonFindingDetail.count,
jsonFindingDetail.sampleSource,
webObserveShort(webObserveText(jsonFindingDetail.summary), 96),
]]),
"",
"Finding samples:",
webObserveTable(["SEQ", "TS", "KIND", "FROM", "TO", "DELTA", "SAMPLE_S", "ALLOWED", "EXCESS", "SESSION/PATH", "TRACE", "DETAIL"], jsonFindingSamples.length > 0 ? jsonFindingSamples.map((item) => [
item.seq ?? item.sampleSeq ?? item.fromSeq,
webObserveShort(webObserveText(item.ts ?? item.sampleTs ?? item.fromTs), 24),
webObserveShort(webObserveText(item.diffKind ?? item.kind ?? item.type ?? item.metric ?? item.anomaly), 28),
webObserveShort(webObserveText(item.fromValue ?? item.from ?? item.control ?? item.controlValue ?? item.beforeValue), 28),
webObserveShort(webObserveText(item.toValue ?? item.to ?? item.observer ?? item.observerValue ?? item.afterValue), 28),
item.delta,
item.sampleDeltaSeconds,
item.allowedIncreaseSeconds,
item.excessiveIncreaseSeconds,
webObserveShort(webObserveText(item.sessionId ?? item.routeSessionId ?? item.activeSessionId ?? item.path ?? item.urlPath ?? item.url), 48),
webObserveShort(webObserveText(item.trace ?? item.traceId ?? item.controlTraceId ?? item.observerTraceId), 48),
webObserveShort(webObserveText(item.detail ?? item.summary ?? item.message ?? item.preview), 96),
]) : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
);
}
if (jsonlTail.length > 0) {
lines.push(
"JSONL tail:",
webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "MSG", "TRACE", "TRACE_IDS", "MSG_STATUS", "LOAD", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => {
const error = record(item.error);
const details = record(error.details);
const attempts = Array.isArray(error.attempts) ? error.attempts : Array.isArray(details.attempts) ? details.attempts : [];
const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {};
const rawReadiness = record(lastAttempt.readiness ?? lastAttempt.readinessBeforeClick ?? details.readinessBeforeClick ?? details.readinessAfterWait ?? error.navigationReadiness);
const readiness = record(rawReadiness.snapshot ?? rawReadiness);
const createState = readiness.sessionCreateVisible === true ? "Y" : readiness.sessionCreatePresent === true ? "h" : "n";
const railState = readiness.sessionRailCollapsed === true ? "C" : readiness.sessionRailCollapsed === false ? "O" : "-";
const domBits = [
`shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`,
`create=${createState}`,
`rail=${railState}`,
`toggle=${readiness.sessionCollapseToggleVisible === true ? "Y" : "n"}`,
`input=${readiness.commandInputPresent === true ? "Y" : "n"}`,
`tab=${readiness.activeTabPresent === true ? "Y" : "n"}`,
`login=${readiness.loginVisible === true ? "Y" : "n"}`,
].join(" ");
return [
webObserveShort(webObserveText(item.ts), 24),
webObserveShort(webObserveText(item.pageRole ?? item.role), 12),
webObserveShort(webObserveText(item.type), 20),
webObserveShort(webObserveText(item.commandId), 28),
item.messageCount,
item.traceRowCount ?? item.traceEventCount,
webObserveShort(webObserveText(item.traceIds), 48),
webObserveShort(webObserveText(item.messageStatuses), 48),
item.loadingCount,
attempts.length,
webObserveShort(webObserveText(readiness.reason), 24),
webObserveShort(domBits, 48),
webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url ?? item.currentUrl ?? item.status), 60),
webObserveShort(webObserveText(error.message ?? item.message ?? item.text ?? item.preview), 96),
];
})),
"",
);
} else if (typeof file.content === "string" && file.content.length > 0) {
lines.push("Content preview:", webObserveShort(file.content, 4000), "");
}
} else {
lines.push(
"Files:",
webObserveTable(["RELATIVE", "BYTES", "SHA256"], files.length > 0 ? files.map((item) => [
webObserveShort(webObserveText(item.relative), 72),
item.byteCount,
webObserveShort(webObserveText(item.sha256), 28),
]) : [["-", "-", "-"]]),
"",
);
}
if (value.ok === false) {
lines.push(
"Blocked detail:",
webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDERR"], [[
webObserveShort(webObserveText(collect.reason ?? value.degradedReason), 48),
result.exitCode,
result.timedOut,
webObserveShort(webObserveText(result.stderr), 120),
]]),
"",
);
}
lines.push("Disclosure:", " collect is bounded; use --file <relative> plus --finding <id> for id-specific drill-down; exact --grep <finding-id> also expands finding samples.");
return lines.join("\n");
}
function renderWebObserveProjectCollectTable(value: Record<string, unknown>, collect: Record<string, unknown>): string {
const summary = record(collect.summary);
const sampleRows = Array.isArray(collect.sampleRows) ? collect.sampleRows.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-6) : [];
const commands = Array.isArray(collect.commands) ? collect.commands.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-14) : [];
const mutations = Array.isArray(collect.mutations) ? collect.mutations.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-10) : [];
const launches = Array.isArray(collect.launches) ? collect.launches.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-4) : [];
const findings = Array.isArray(collect.findings) ? collect.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 6) : [];
const lines = [
`web-probe observe collect (${webObserveText(value.status)})`,
"",
...renderWebObserveWrapperContract(value),
webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[
value.id,
value.node,
value.lane,
collect.view ?? value.view,
webObserveShort(webObserveText(collect.stateDir), 88),
]]),
"",
"Project MDTODO summary:",
webObserveTable(["ENABLED", "SAMPLES", "MDTODO", "LATEST", "SRC", "FILES", "TASKS", "SELECTED"], [[
summary.enabled,
summary.projectSampleCount,
summary.mdtodoSampleCount,
webObserveShort(`${webObserveText(summary.latestPageKind)} ${webObserveText(summary.latestPath)}`, 44),
summary.latestSourceCount,
summary.latestFileCount,
summary.latestTaskCount,
webObserveShort(webObserveText(summary.latestSelectedTaskRefHash ?? summary.latestSelectedTaskRefPreview), 32),
]]),
"",
"Project layout gaps:",
webObserveTable(["ACTIONABLE", "IGNORED_INITIAL_EMPTY_DETAIL", "MEANING"], [[
summary.severePaneGapSampleCount ?? 0,
summary.ignoredPaneGapSampleCount ?? 0,
"ignored rows do not override selected-task/control evidence",
]]),
"",
"Project command totals:",
webObserveTable(["COMMANDS", "MUTATIONS", "MUTATION_FAIL", "LAUNCH", "LAUNCH_OK", "LAUNCH_FAIL", "OTEL", "API_FAIL", "SLOW>10S"], [[
collect.commandCount ?? summary.projectCommandCount,
collect.mutationCount ?? summary.mutationCommandCount,
summary.mutationFailureCount,
collect.launchCount ?? summary.launchCommandCount,
summary.launchSuccessCount,
summary.launchFailureCount,
summary.launchWithOtelTraceHeaderCount,
`${webObserveText(summary.projectApiFailureCount)}/${webObserveText(summary.projectApiRequestFailedCount)}`,
summary.projectApiSlowPathCount,
]]),
"",
"Recent project samples:",
webObserveTable(["SEQ", "TS", "ROLE", "PATH", "KIND", "SRC", "FILES", "TASKS", "STATUS", "LAUNCH"], sampleRows.length > 0 ? sampleRows.map((item) => [
item.seq,
webObserveShort(webObserveText(item.ts), 24),
webObserveShort(webObserveText(item.pageRole), 18),
webObserveShort(webObserveText(item.path), 24),
webObserveShort(webObserveText(item.pageKind), 28),
item.sourceCount,
item.fileCount,
item.taskCount,
webObserveShort(webObserveText(item.selectedTaskStatus), 20),
item.launchButtonEnabled === true ? "yes" : "no",
]) : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
"Project commands:",
webObserveTable(["TS", "PHASE", "TYPE", "STATUS", "PATH", "TASK", "MESSAGE"], commands.length > 0 ? commands.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
item.phase,
webObserveShort(webObserveText(item.type), 28),
item.status,
webObserveShort(webObserveText(item.afterPath), 36),
webObserveShort(webObserveText(item.selectedTaskHash), 24),
webObserveShort(webObserveText(item.message), 72),
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"MDTODO mutations:",
webObserveTable(["TS", "PHASE", "TYPE", "STATUS", "TASK", "MESSAGE"], mutations.length > 0 ? mutations.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
item.phase,
webObserveShort(webObserveText(item.type), 28),
item.status,
webObserveShort(webObserveText(item.selectedTaskHash), 24),
webObserveShort(webObserveText(item.message), 72),
]) : [["-", "-", "-", "-", "-", "-"]]),
"",
"Workbench launches:",
webObserveTable(["TS", "PHASE", "STATUS", "SESSION", "OTEL_TRACE", "TASK", "URL"], launches.length > 0 ? launches.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
item.phase,
item.status,
webObserveShort(webObserveText(item.sessionId), 28),
webObserveShort(webObserveText(item.otelTraceId), 28),
webObserveShort(webObserveText(item.taskHash), 24),
webObserveShort(webObserveText(item.workbenchUrl), 52),
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"Findings:",
webObserveTable(["SEVERITY", "ID", "COUNT", "TIMING", "ROOT_CAUSE", "SUMMARY"], findings.length > 0 ? findings.map((item) => [
webObserveShort(webObserveText(item.severity ?? item.level), 12),
webObserveShort(webObserveText(item.id ?? item.kind ?? item.code), 48),
item.count ?? item.sampleCount,
webObserveShort(webObserveFindingTiming(item), 42),
webObserveShort(webObserveText(item.rootCause ?? item.rootCauseStatus), 48),
webObserveShort(webObserveText(item.summary ?? item.message), 96),
]) : [["-", "-", "-", "-", "-", "-"]]),
"",
"Disclosure:",
" collect project views are rendered from existing samples/control/analysis artifacts; they do not create a new sampling source.",
" row lists are bounded; use observe analyze or collect --file for artifact-level drill-down.",
];
return lines.join("\n");
}
function webObserveTable(headers: string[], rows: unknown[][]): string {
const stringRows = rows.map((row) => headers.map((_, index) => webObserveCell(row[index])));
const widths = headers.map((header, index) => Math.max(header.length, ...stringRows.map((row) => row[index]?.length ?? 0)));
const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
return [renderRow(headers), ...stringRows.map(renderRow)].join("\n");
}
function webObserveCell(value: unknown, maxLength = 96): string {
if (value === null || value === undefined) return "-";
const text = typeof value === "string" ? value : String(value);
const compact = text.replace(/\s+/gu, " ").trim();
if (compact.length === 0) return "-";
if (compact.length <= maxLength) return compact;
return `${compact.slice(0, Math.max(1, maxLength - 1))}...`;
}
function webObserveText(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 webObserveShort(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 webObserveFindingTiming(item: Record<string, unknown>): string {
const status = item.timingStatus ?? null;
const source = item.timingSourceOfTruth ?? item.expectedElapsedSource ?? item.evidenceKind ?? null;
const parts = [status, source].filter((value) => value !== null && value !== undefined && value !== "");
if (parts.length === 0) return "-";
return parts.map((value) => webObserveText(value)).join("/");
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function nullableRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
}