962 lines
45 KiB
TypeScript
962 lines
45 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 exactCommand = record(observer?.exactCommand);
|
||
const exactResult = nullableRecord(exactCommand?.result);
|
||
const exactError = record(exactCommand?.error);
|
||
const exactErrorDetails = nullableRecord(exactError?.details);
|
||
const exactReplayEvidence = exactResult ?? exactErrorDetails;
|
||
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 heartbeatReadiness = record(heartbeatError?.readiness);
|
||
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),
|
||
]]),
|
||
"",
|
||
] : []),
|
||
...(heartbeatReadiness !== null ? [
|
||
"Target readiness:",
|
||
webObserveTable(["REASON", "PATH", "BLOCKED", "READY", "SHELL", "CREATE", "RAIL", "INPUT", "TAB", "LOGIN", "BODY_BYTES", "BODY_HASH"], [[
|
||
heartbeatReadiness.reason,
|
||
webObserveShort(webObserveText(heartbeatReadiness.path), 40),
|
||
heartbeatReadiness.blockedReason,
|
||
heartbeatReadiness.readyState,
|
||
heartbeatReadiness.workbenchShellVisible,
|
||
heartbeatReadiness.sessionCreateVisible === true ? "visible" : heartbeatReadiness.sessionCreatePresent === true ? "hidden" : "missing",
|
||
heartbeatReadiness.sessionRailCollapsed === true ? "collapsed" : heartbeatReadiness.sessionRailPresent === true ? "present" : "missing",
|
||
heartbeatReadiness.commandInputPresent,
|
||
heartbeatReadiness.activeTabPresent,
|
||
heartbeatReadiness.loginVisible,
|
||
heartbeatReadiness.bodyTextBytes,
|
||
webObserveShort(webObserveText(heartbeatReadiness.bodyTextHash), 24),
|
||
]]),
|
||
"",
|
||
] : []),
|
||
...(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),
|
||
]]),
|
||
"",
|
||
] : []),
|
||
...(Object.keys(exactCommand).length > 0 ? [
|
||
"Exact command:",
|
||
webObserveTable(["COMMAND", "TYPE", "PHASE", "OK", "STATUS", "REPORT_SHA", "DETAIL"], [[
|
||
webObserveShort(webObserveText(exactCommand.commandId), 40),
|
||
exactCommand.type,
|
||
exactCommand.phase,
|
||
exactCommand.ok,
|
||
exactResult?.status,
|
||
webObserveShort(webObserveText(exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256), 32),
|
||
webObserveShort(webObserveText(exactError.message ?? exactResult?.reportPath ?? exactErrorDetails?.reportPath), 96),
|
||
]]),
|
||
"",
|
||
] : []),
|
||
...(exactCommand.type === "validateExistingSessionRefresh" && exactReplayEvidence !== null ? [
|
||
...renderExistingSessionRefreshEvidence(
|
||
exactReplayEvidence,
|
||
exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256,
|
||
record(exactResult?.screenshot ?? exactErrorDetails?.screenshot)?.sha256,
|
||
),
|
||
"",
|
||
] : []),
|
||
...(exactCommand.type === "validateWorkbenchKafkaDebugReplay" && exactReplayEvidence !== null ? [
|
||
...renderWorkbenchKafkaDebugReplayEvidence(
|
||
exactReplayEvidence,
|
||
exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256,
|
||
record(exactResult?.screenshot ?? exactErrorDetails?.screenshot)?.sha256,
|
||
),
|
||
"",
|
||
] : []),
|
||
...(exactCommand.type === "validateWorkbenchTraceReadability" && exactReplayEvidence !== null ? [
|
||
...renderWorkbenchTraceReadabilityEvidence(
|
||
exactReplayEvidence,
|
||
exactResult?.reportSha256 ?? exactErrorDetails?.reportSha256,
|
||
record(exactResult?.screenshot ?? exactErrorDetails?.screenshot)?.sha256,
|
||
),
|
||
"",
|
||
] : []),
|
||
"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 renderExistingSessionRefreshEvidence(
|
||
evidence: Record<string, unknown>,
|
||
reportSha: unknown,
|
||
screenshotSha: unknown,
|
||
): string[] {
|
||
const before = record(evidence.before);
|
||
const after = record(evidence.after);
|
||
const replay = record(evidence.refreshReplay);
|
||
const counts = record(replay?.counts);
|
||
const product = record(evidence.productSse);
|
||
const failure = nullableRecord(evidence.failure);
|
||
const traceIds = Array.isArray(evidence.traceIds) ? evidence.traceIds.slice(0, 4).join(",") : "";
|
||
const partitions = Array.isArray(replay?.topicPartitions) ? replay.topicPartitions.slice(0, 8).join(",") : "";
|
||
const barrier = Array.isArray(replay?.barrier)
|
||
? replay.barrier.slice(0, 8).map((item) => {
|
||
const row = record(item);
|
||
return `${webObserveText(row.partition)}:${webObserveText(row.endOffset)}`;
|
||
}).join(",")
|
||
: "";
|
||
return [
|
||
"Workbench 既有 Session 刷新:",
|
||
webObserveTable(["SESSION", "TRACE_IDS", "PHASE", "CODE", "MSG_BEFORE", "MSG_AFTER", "SOURCES", "OPEN", "CONNECTED", "BUSINESS", "FAILURES", "FORBIDDEN", "REPORT_SHA", "SCREENSHOT_SHA"], [[
|
||
webObserveShort(webObserveText(evidence.sessionId), 32),
|
||
webObserveShort(webObserveText(traceIds), 40),
|
||
evidence.phase,
|
||
evidence.code,
|
||
before?.messageCount,
|
||
after?.messageCount,
|
||
product?.browserSourceCount,
|
||
product?.openCount,
|
||
product?.connectedEventCount,
|
||
product?.businessEventCount,
|
||
product?.typedFailureCount,
|
||
evidence.forbiddenRequestCount,
|
||
webObserveShort(webObserveText(reportSha), 32),
|
||
webObserveShort(webObserveText(screenshotSha), 32),
|
||
]]),
|
||
"Kafka refresh handoff:",
|
||
webObserveTable(["TOPIC", "PARTITIONS", "BARRIER", "MATCHED", "REPLAYED", "BUFFERED", "BUFFERED_DELIVERED", "LIVE_DELIVERED", "DEDUPLICATED", "LIVE"], [[
|
||
webObserveShort(webObserveText(replay?.topic), 32),
|
||
webObserveShort(webObserveText(partitions), 24),
|
||
webObserveShort(webObserveText(barrier), 56),
|
||
counts?.matched,
|
||
counts?.replayed,
|
||
counts?.buffered,
|
||
counts?.bufferedDelivered,
|
||
counts?.liveDelivered,
|
||
counts?.deduplicated,
|
||
product?.livePhase,
|
||
]]),
|
||
...(failure !== null ? [
|
||
"Typed failure:",
|
||
webObserveTable(["PHASE", "CODE", "STREAM_PHASE", "STREAM_CODE", "MESSAGE"], [[
|
||
failure.phase,
|
||
failure.code,
|
||
failure.streamPhase,
|
||
failure.streamCode,
|
||
webObserveShort(webObserveText(failure.message), 96),
|
||
]]),
|
||
] : []),
|
||
];
|
||
}
|
||
|
||
function renderWorkbenchKafkaDebugReplayEvidence(
|
||
evidence: Record<string, unknown>,
|
||
reportSha: unknown,
|
||
screenshotSha: unknown,
|
||
): string[] {
|
||
const layers = record(evidence.layerCounts);
|
||
const server = record(layers?.server);
|
||
const client = record(layers?.client);
|
||
const trace = record(evidence.traceTimeline);
|
||
const raw = record(evidence.rawHwlabEventWindow);
|
||
const rawCounts = record(raw?.counts);
|
||
const layerDom = record(evidence.layerDom);
|
||
const layerStable = record(evidence.layerDomStabilization);
|
||
return [
|
||
"Workbench Kafka debug replay:",
|
||
webObserveTable(["TRACE", "TOPIC", "GROUP", "PHASE", "CODE", "RECEIVED", "APPLIED", "TERMINAL", "REPORT_SHA", "SCREENSHOT_SHA"], [[
|
||
webObserveShort(webObserveText(evidence.traceId), 28),
|
||
webObserveShort(webObserveText(evidence.topic), 32),
|
||
webObserveShort(webObserveText(evidence.groupId), 48),
|
||
evidence.phase,
|
||
evidence.code,
|
||
evidence.receivedCount,
|
||
evidence.appliedCount,
|
||
evidence.terminalStatus,
|
||
webObserveShort(webObserveText(reportSha), 32),
|
||
webObserveShort(webObserveText(screenshotSha), 32),
|
||
]]),
|
||
"Replay layers:",
|
||
webObserveTable(["SERVER_SCANNED", "SERVER_MATCHED", "SERVER_DELIVERED", "CLIENT_RECEIVED", "CLIENT_DECODED", "CLIENT_APPLIED"], [[
|
||
server?.scanned,
|
||
server?.matched,
|
||
server?.delivered,
|
||
client?.received,
|
||
client?.decoded,
|
||
client?.applied,
|
||
]]),
|
||
"分层 DOM 诊断:",
|
||
webObserveTable(["MATCH", "VISIBLE", "CONNECTED", "TEXT_BYTES", "TEXT_HASH", "STABLE", "TIMED_OUT", "SAMPLES", "ELAPSED_MS"], [[
|
||
layerDom?.matchCount,
|
||
layerDom?.visibleCount,
|
||
layerDom?.connectedCount,
|
||
layerDom?.textBytes,
|
||
webObserveShort(webObserveText(layerDom?.textHash), 32),
|
||
layerStable?.stable,
|
||
layerStable?.timedOut,
|
||
layerStable?.sampleCount,
|
||
layerStable?.elapsedMs,
|
||
]]),
|
||
"Trace and raw HWLAB Event:",
|
||
webObserveTable(["AUTHORITY", "SOURCE_ROWS", "READABLE_ROWS", "RAW_AVAILABLE", "SAME_PAGE", "UNFILTERED", "RAW_RECEIVED", "RAW_RETAINED", "RAW_REJECTED", "RAW_EVICTED", "RAW_BYTES", "EXTRA_PRODUCT_SSE"], [[
|
||
trace?.contract,
|
||
trace?.sourceAuthorityRowCount,
|
||
trace?.readableSourceRowCount,
|
||
raw?.available,
|
||
raw?.samePage,
|
||
raw?.unfilteredContractPresent,
|
||
rawCounts?.received,
|
||
rawCounts?.retained,
|
||
rawCounts?.rejected,
|
||
rawCounts?.evicted,
|
||
rawCounts?.bytes,
|
||
raw?.productEventSourceRequestCount,
|
||
]]),
|
||
];
|
||
}
|
||
|
||
function renderWorkbenchTraceReadabilityEvidence(
|
||
evidence: Record<string, unknown>,
|
||
reportSha: unknown,
|
||
screenshotSha: unknown,
|
||
): string[] {
|
||
const scope = record(evidence.scope);
|
||
const disclosure = record(evidence.disclosure);
|
||
const running = record(evidence.running);
|
||
const runningSnapshot = record(running?.snapshot);
|
||
const terminal = record(evidence.terminal);
|
||
const terminalSnapshot = record(terminal?.snapshot);
|
||
const retention = record(evidence.runningRetention);
|
||
const failures = Array.isArray(evidence.failures)
|
||
? evidence.failures.map(record).filter((item): item is Record<string, unknown> => item !== null)
|
||
: [];
|
||
return [
|
||
"Workbench 产品 Trace 可读性:",
|
||
webObserveTable(["TRACE", "STARTED_RUNNING", "RUNNING_OBSERVED", "AUTO_READABLE", "OPEN_BEFORE", "FORCED", "OPEN_TERMINAL", "RETENTION_MODE", "RETENTION_OK", "RETAINED", "REPORT_SHA", "SCREENSHOT_SHA"], [[
|
||
webObserveShort(webObserveText(evidence.traceId), 28),
|
||
evidence.startedDuringRunning,
|
||
running?.observed,
|
||
disclosure?.autoReadable,
|
||
disclosure?.openBefore,
|
||
evidence.forcedExpansion,
|
||
disclosure?.openAfterTerminal,
|
||
retention?.mode,
|
||
retention?.passed,
|
||
evidence.retainedRunningRowCount,
|
||
webObserveShort(webObserveText(reportSha), 32),
|
||
webObserveShort(webObserveText(screenshotSha), 32),
|
||
]]),
|
||
"产品 Trace DOM:",
|
||
webObserveTable(["CONVERSATION", "PRODUCT_CARDS", "DEBUG_IN_SCOPE", "RUN_STATUS", "RUN_ROWS", "RUN_SOURCE", "TERM_STATUS", "TERM_ROWS", "TERM_SOURCE", "TERM_PROJECTED", "TERM_STABLE"], [[
|
||
scope?.conversationCount,
|
||
scope?.productCardCount,
|
||
scope?.debugPanelCountInsideConversation,
|
||
runningSnapshot?.status,
|
||
runningSnapshot?.rowCount,
|
||
runningSnapshot?.sourceAuthorityRowCount,
|
||
terminalSnapshot?.status,
|
||
terminalSnapshot?.rowCount,
|
||
terminalSnapshot?.sourceAuthorityRowCount,
|
||
terminalSnapshot?.projectedAuthorityRowCount,
|
||
terminal?.stable,
|
||
]]),
|
||
"失败项:",
|
||
failures.length > 0
|
||
? failures.map((failure) => webObserveText(failure.code)).join(",")
|
||
: "-",
|
||
];
|
||
}
|
||
|
||
function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||
const observerCommand = record(value.observerCommand);
|
||
const observer = record(value.observer);
|
||
const control = record(value.control);
|
||
const asyncTurn = record(value.asyncTurn);
|
||
const result = nullableRecord(observer?.result);
|
||
const resultScreenshot = record(result?.screenshot);
|
||
const error = record(observer?.error);
|
||
const details = nullableRecord(error?.details);
|
||
const replayEvidence = result ?? details;
|
||
const replayScreenshot = record(result?.screenshot ?? details?.screenshot);
|
||
const rawReadiness = record(error?.navigationReadiness) ?? record(details?.readiness) ?? record(details?.readinessAfterWait) ?? record(details?.readinessBeforeClick);
|
||
const readiness = record(rawReadiness?.snapshot) ?? rawReadiness;
|
||
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", "CONTROL", "TURN_SUBMIT", "TURN_TERMINAL", "TRACE", "DETAIL"], [[
|
||
id,
|
||
value.commandId,
|
||
observerCommand?.type,
|
||
control?.executionStatus ?? (observer?.ok === false ? "failed" : observer?.completedAt !== undefined ? "completed" : value.status),
|
||
asyncTurn?.submissionStatus ?? "not-observed",
|
||
asyncTurn?.terminalObserved === true ? asyncTurn.terminalStatus : "not-observed",
|
||
webObserveShort(webObserveText(asyncTurn?.traceId), 24),
|
||
webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80),
|
||
]]),
|
||
...(observerCommand?.type === "validateWorkbenchKafkaDebugReplay" && replayEvidence !== null ? [
|
||
"",
|
||
...renderWorkbenchKafkaDebugReplayEvidence(
|
||
replayEvidence,
|
||
result?.reportSha256 ?? details?.reportSha256,
|
||
replayScreenshot?.sha256 ?? resultScreenshot?.sha256,
|
||
),
|
||
] : []),
|
||
...(observerCommand?.type === "validateExistingSessionRefresh" && replayEvidence !== null ? [
|
||
"",
|
||
...renderExistingSessionRefreshEvidence(
|
||
replayEvidence,
|
||
result?.reportSha256 ?? details?.reportSha256,
|
||
replayScreenshot?.sha256 ?? resultScreenshot?.sha256,
|
||
),
|
||
] : []),
|
||
...(observerCommand?.type === "validateWorkbenchTraceReadability" && replayEvidence !== null ? [
|
||
"",
|
||
...renderWorkbenchTraceReadabilityEvidence(
|
||
replayEvidence,
|
||
result?.reportSha256 ?? details?.reportSha256,
|
||
replayScreenshot?.sha256 ?? resultScreenshot?.sha256,
|
||
),
|
||
] : []),
|
||
...(readiness !== null ? [
|
||
"",
|
||
"Target readiness:",
|
||
webObserveTable(["REASON", "PATH", "BLOCKED", "READY", "SHELL", "CREATE", "RAIL", "INPUT", "TAB", "LOGIN", "BODY_BYTES", "BODY_HASH"], [[
|
||
rawReadiness?.reason ?? readiness.reason,
|
||
webObserveShort(webObserveText(readiness.path), 40),
|
||
readiness.blockedReason,
|
||
readiness.readyState,
|
||
readiness.workbenchShellVisible,
|
||
readiness.sessionCreateVisible === true ? "visible" : readiness.sessionCreatePresent === true ? "hidden" : "missing",
|
||
readiness.sessionRailCollapsed === true ? "collapsed" : readiness.sessionRailPresent === true ? "present" : "missing",
|
||
readiness.commandInputPresent,
|
||
readiness.activeTabPresent,
|
||
readiness.loginVisible,
|
||
readiness.bodyTextBytes,
|
||
webObserveShort(webObserveText(readiness.bodyTextHash), 24),
|
||
]]),
|
||
] : []),
|
||
...(full ? [
|
||
"",
|
||
"Full command result:",
|
||
"```json",
|
||
JSON.stringify(observer ?? {}, null, 2),
|
||
"```",
|
||
] : []),
|
||
"",
|
||
"Next:",
|
||
` bun scripts/cli.ts web-probe observe status ${id} --command-id ${webObserveText(value.commandId)}`,
|
||
` bun scripts/cli.ts web-probe observe analyze ${id}`,
|
||
` bun scripts/cli.ts web-probe observe collect ${id} --view turn-summary --command-id ${webObserveText(value.commandId)}`,
|
||
"",
|
||
"Disclosure:",
|
||
" default view is a bounded command result; pass --full to expose the complete command result JSON.",
|
||
" CONTROL completed means the observer browser action finished; it does not imply the asynchronous business turn reached a terminal state.",
|
||
" TURN_SUBMIT and TURN_TERMINAL are separate evidence fields; use the exact command-id turn-summary for sampled terminal 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 skippedFiles = Array.isArray(collect?.skippedFiles) ? collect.skippedFiles.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : [];
|
||
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) {
|
||
const stdoutRecovery = record(value.stdoutRecovery) ?? record(result.stdoutRecovery);
|
||
const artifact = record(stdoutRecovery?.artifact);
|
||
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),
|
||
]]),
|
||
"",
|
||
"Stdout recovery:",
|
||
webObserveTable(["REQUESTED_TYPE", "STDOUT_KIND", "SOURCE", "FALLBACK_REASON", "DUMP_PATH", "ARTIFACT_PATH", "NEXT"], [[
|
||
webObserveShort(webObserveText(stdoutRecovery?.requestedStdoutType), 42),
|
||
webObserveShort(webObserveText(stdoutRecovery?.stdoutKind), 28),
|
||
webObserveShort(webObserveText(stdoutRecovery?.source), 20),
|
||
webObserveShort(webObserveText(stdoutRecovery?.fallbackReason), 40),
|
||
webObserveShort(webObserveText(stdoutRecovery?.dumpPath), 72),
|
||
webObserveShort(webObserveText(artifact?.path ?? artifact?.requestedPath), 72),
|
||
webObserveShort(webObserveText(artifact?.nextCommand), 96),
|
||
]]),
|
||
"",
|
||
"Disclosure:",
|
||
" collect stdout could not be recovered as a valid collect contract; inspect dumpPath or artifact path when present, otherwise rerun with a narrower --file.",
|
||
);
|
||
return lines.join("\n");
|
||
}
|
||
|
||
if (skippedFiles.length > 0) {
|
||
lines.push(
|
||
"Skipped files:",
|
||
webObserveTable(["RELATIVE", "REASON", "MESSAGE"], skippedFiles.map((item) => [
|
||
webObserveShort(webObserveText(item.relative ?? item.path), 72),
|
||
webObserveShort(webObserveText(item.reason), 28),
|
||
webObserveShort(webObserveText(item.message), 96),
|
||
])),
|
||
"",
|
||
);
|
||
}
|
||
|
||
if (collect.mode === "file") {
|
||
const requestedRelative = file.requestedRelative ?? collect.requestedFile ?? value.requestedFile;
|
||
const resolvedRelative = file.resolvedRelative ?? collect.resolvedFile ?? file.relative;
|
||
if (webObserveText(requestedRelative) !== webObserveText(resolvedRelative) || file.fallbackReason || collect.fallbackReason) {
|
||
lines.push(
|
||
"File resolution:",
|
||
webObserveTable(["REQUESTED", "RESOLVED", "FALLBACK_REASON"], [[
|
||
webObserveShort(webObserveText(requestedRelative), 64),
|
||
webObserveShort(webObserveText(resolvedRelative), 64),
|
||
webObserveShort(webObserveText(file.fallbackReason ?? collect.fallbackReason), 40),
|
||
]]),
|
||
"",
|
||
);
|
||
}
|
||
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;
|
||
}
|