Files
rea/src/cliLogging.ts
T
morluto fd6da70920 fix(cli): report string capacity failures during result formatting
Recover completed ordinary command results when pinned Incur formatting
exceeds the runtime string limit before stdout begins. Emit a small typed
memory constraint with an explicit unknown formatted size, the runtime limit
and JSON/filter remediation. Preserve full-output framing and leave normal
CTA, human and token-window formatting with Incur. Never rerun analysis.

Capture canonical failure identity at its logging owner, including thrown
AnalysisError projections. Recovery reads compact captured facts without
revalidating or retaining large diagnostics. Keep destination failure ownership,
prevent a second stdout document and retain quiet SIGPIPE status during recovery.

Validation: 111 distinct focused tests across 10 files; check:fast,
docs:check and discovery of all 1555 test files. Sixteen authored formatter
controls fail unchanged e103ae0ce and pass with this fix. A separate closed
reader control caught and now covers recovery EPIPE handling. The heavy JSON
string-limit regression remains excluded; no large Electron reproduction ran.

Twelve small public CLI artifact inspections preserve identical raw replies
and token estimates in TOON and JSON. Timings are noise-level, with no speedup,
cold-cache, large-result memory or real-provider qualification claim. Sentinel
and generator protocols keep their existing ownership.

Fixes #4806
2026-10-18 04:00:15 +08:00

149 lines
4.8 KiB
TypeScript

import {
analysisCliErrorEnvelopeSchema,
analysisErrorProjectionSchema,
} from "./contracts/errorSchemas.js";
import { AnalysisError } from "./domain/analysisErrorBase.js";
import {
projectAnalysisError,
type AnalysisErrorProjection,
} from "./domain/analysisErrorProjection.js";
import type { Logger } from "pino";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
type CliFailureIdentity = Pick<
AnalysisErrorProjection,
"code" | "category" | "retryable"
>;
interface CliOperationFailure {
readonly payload: unknown;
readonly identity?: CliFailureIdentity;
}
const loggedFailures = new WeakMap<object, CliOperationFailure | undefined>();
const canonicalFailure = (
projection: AnalysisErrorProjection,
): CliOperationFailure => ({
payload: projection,
identity: {
code: projection.code,
category: projection.category,
retryable: projection.retryable,
},
});
/** Return the canonical failure projection owned by the CLI result boundary. */
const projectCliOperationFailure = (
value: unknown,
): AnalysisErrorProjection | undefined => {
if (!isRecord(value)) return undefined;
const direct = analysisErrorProjectionSchema.safeParse(value);
if (direct.success) return value as AnalysisErrorProjection;
const envelope = analysisCliErrorEnvelopeSchema.safeParse(value);
if (envelope.success) {
const {
error: _label,
input_path: inputPath,
input_reason: inputReason,
...projection
} = value;
const parsed = projection as AnalysisErrorProjection;
if (typeof inputPath !== "string" && typeof inputReason !== "string")
return parsed;
return {
...parsed,
details: {
...parsed.details,
...(typeof inputPath === "string" ? { input_path: inputPath } : {}),
...(typeof inputReason === "string"
? { input_reason: inputReason }
: {}),
},
};
}
if (isRecord(value.error)) {
const nested = analysisErrorProjectionSchema.safeParse(value.error);
if (nested.success) return value.error as AnalysisErrorProjection;
}
return undefined;
};
/** Keep the classified payload and its canonical identity under one owner. */
const inferCliOperationFailure = (
value: unknown,
): CliOperationFailure | undefined => {
const projection = projectCliOperationFailure(value);
if (projection !== undefined) return canonicalFailure(projection);
if (isRecord(value) && value.healthy === false) return { payload: value };
return undefined;
};
export const cliOperationFailurePayload = (
value: unknown,
): unknown | undefined => {
if (isRecord(value) && loggedFailures.has(value))
return loggedFailures.get(value)?.payload;
const failure = inferCliOperationFailure(value);
if (failure !== undefined && isRecord(value))
loggedFailures.set(value, failure);
return failure?.payload;
};
/** Identify caller-visible CLI outcomes that mean the requested operation failed. */
export const isCliOperationFailure = (value: unknown): boolean =>
cliOperationFailurePayload(value) !== undefined;
/** Already classified identity, without traversing payloads during recovery. */
export const cliOperationFailureIdentity = (
value: unknown,
): CliFailureIdentity | undefined =>
isRecord(value) ? loggedFailures.get(value)?.identity : undefined;
/** Log one CLI command with duration and a stable success or failure status. */
export const logCliCommand = async <Value>(
logger: Logger,
command: string,
execute: () => Promise<Value>,
isCommandFailure?: (value: Value) => boolean,
): Promise<Value | AnalysisErrorProjection> => {
const startedAt = performance.now();
try {
const value = await execute();
const inferredFailure = inferCliOperationFailure(value);
const failed = isCommandFailure?.(value) ?? inferredFailure !== undefined;
if (isRecord(value))
loggedFailures.set(
value,
failed ? (inferredFailure ?? { payload: value }) : undefined,
);
logger[failed ? "error" : "info"](
{
command,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: failed ? "error" : "ok",
},
failed ? "CLI command failed" : "CLI command completed",
);
if (failed) process.exitCode = 1;
return value;
} catch (cause: unknown) {
process.exitCode = 1;
logger.error(
{
command,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: "error",
},
"CLI command failed",
);
if (cause instanceof AnalysisError) {
process.exitCode = 1;
const projection = projectAnalysisError(cause);
loggedFailures.set(projection, canonicalFailure(projection));
return projection;
}
throw cause;
}
};