fix(cli): preserve output and comparison input validation (#981)

This commit is contained in:
morluto authored and GitHub committed 2026-10-07 23:04:54 +08:00
1 parent a45fe86a39
commit d5c43d65f0
4 files changed
+172 -21

No files matched your search

+35 -10
View File
@@ -10,7 +10,10 @@ import {
} from "../application/BrowserObservationService.js";
import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
import { logCliCommand } from "../cliLogging.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import {
AnalysisInputError,
type AnalysisInputIssue,
} from "../domain/analysisErrorCore.js";
import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js";
import { discoverWebMcpToolsInputSchema } from "../domain/webMcpDiscovery.js";
import {
@@ -89,19 +92,32 @@ const registerCaptureDiff = (
}),
run: ({ args, options }) =>
logCliCommand(logger, "compare-web-captures", async () => {
const before = parseJson(args.beforeJson);
const after = parseJson(args.afterJson);
const before = safeParseJson(args.beforeJson);
const after = safeParseJson(args.afterJson);
const normalization = safeParseJson(options.normalizationJson);
if (!before.ok)
return inputError("compare_web_captures", [
invalidJsonIssue("before"),
]);
if (!after.ok)
return inputError("compare_web_captures", [
invalidJsonIssue("after"),
]);
if (!normalization.ok)
return inputError("compare_web_captures", [
invalidJsonIssue("normalization"),
]);
const scenarioComparison =
browserCaptureComparisonInputSchema.safeParse({
before_scenario: before,
after_scenario: after,
normalization: parseJson(options.normalizationJson),
before_scenario: before.value,
after_scenario: after.value,
normalization: normalization.value,
});
const parsed = scenarioComparison.success
? scenarioComparison
: browserCaptureComparisonInputSchema.safeParse({
before,
after,
before: before.value,
after: after.value,
});
if (!parsed.success) return inputError("compare_web_captures");
const result = await compareWebCaptureEvidence(
@@ -186,5 +202,14 @@ const parseJson = (value: string): unknown => {
return parsed.ok ? parsed.value : undefined;
};
const inputError = (operation: string): JsonValue =>
browserCliError(new AnalysisInputError(operation));
const invalidJsonIssue = (field: string): AnalysisInputIssue => ({
path: [field],
reason: "invalid_format",
expected: "JSON",
});
const inputError = (
operation: string,
issues: readonly AnalysisInputIssue[] = [],
): JsonValue =>
browserCliError(new AnalysisInputError(operation, undefined, issues));
+20 -11
View File
@@ -6,6 +6,7 @@ const UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE =
"Token windows cannot preserve structured output. Remove --token-limit/--token-offset and use --filter-output or command pagination.";
type StructuredOutputFormat = "json" | "jsonl" | "yaml";
const INCUR_OUTPUT_FORMATS = new Set(["toon", "json", "yaml", "md", "jsonl"]);
export type CliOutputArgumentValidation =
| { readonly ok: true }
@@ -21,6 +22,7 @@ type JsonRecord = Record<string, unknown>;
interface ParsedCliOutputArguments {
readonly filterOutput: boolean;
readonly format: string;
readonly parseError: boolean;
readonly tokenWindow: boolean;
}
@@ -50,40 +52,45 @@ const parseCliOutputArguments = (
}
if (argument === "--format") {
const value = arguments_[index + 1];
if (value !== undefined) {
if (value) {
// Incur stops parsing at an invalid format before applying any later
// output controls. Let it report that malformed flag itself.
if (!INCUR_OUTPUT_FORMATS.has(value))
return { filterOutput, format, parseError: true, tokenWindow: false };
format = value;
index += 1;
}
continue;
}
if (argument === "--filter-output") {
if (arguments_[index + 1] !== undefined) {
if (arguments_[index + 1]) {
filterOutput = true;
index += 1;
}
continue;
}
if (argument === "--token-limit" || argument === "--token-offset") {
if (arguments_[index + 1] !== undefined) {
const value = arguments_[index + 1];
if (value) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue) || value.trim() === "")
return { filterOutput, format, parseError: true, tokenWindow: false };
tokenWindow = true;
index += 1;
}
continue;
}
if (
argument?.startsWith("--token-limit=") ||
argument?.startsWith("--token-offset=")
)
tokenWindow = true;
}
return { filterOutput, format, tokenWindow };
return { filterOutput, format, parseError: false, tokenWindow };
};
/** Reject text-window controls that would corrupt a structured document. */
export const validateCliOutputArguments = (
arguments_: readonly string[],
): CliOutputArgumentValidation => {
const { format, tokenWindow } = parseCliOutputArguments(arguments_);
const { format, parseError, tokenWindow } =
parseCliOutputArguments(arguments_);
if (parseError) return { ok: true };
if (
tokenWindow &&
(format === "json" || format === "jsonl" || format === "yaml")
@@ -101,7 +108,9 @@ export const validateCliOutputArguments = (
export const renderEmptyFilteredCliOutput = (
arguments_: readonly string[],
): string | undefined => {
const { filterOutput, format } = parseCliOutputArguments(arguments_);
const { filterOutput, format, parseError } =
parseCliOutputArguments(arguments_);
if (parseError) return undefined;
if (
filterOutput &&
(format === "json" || format === "jsonl" || format === "yaml")