627 lines
30 KiB
TypeScript
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;
|
|
}
|