Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-render.ts
T
2026-07-11 17:06:07 +02:00

962 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}