Files
rea/src/cli/jsonLinesOutput.ts
T
Kaoru0822andClaude Opus 5.5 e06be06530 fix(cli): report JSON Lines selection refusals as caller errors (#7124)
* fix(cli): report JSON Lines selection refusals as caller errors

A --format jsonl refusal because the selected value is not a record
collection is the caller's output selection, not unreadable analysis
output. Report it as invalid_request/invalid_input that names the
selection and its observed shape, keep the --format json /
--filter-output remediation, and drop the rea doctor advice.

AnalysisInputError gains an optional producer remediation, mirroring
the other refusal classes. The JSON Lines owner examines its documented
boundaries once for both admission and refusal facts.

docs/cli.md now states that columnar results such as
inspect-sqlite-database --table rows are not record collections.

Fixes #7110

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): carry JSON Lines refusal facts as structured observations

JSON Lines refusals now put the location and shape of each refused value
in details.issues[].observed, and the message is built from those facts.
Tests assert the classification, issue path, observed facts, and the
shared remediation constant instead of fixed sentences.

The filtered analysis-view export check now uses a non-empty request
and compares the emitted rows with the unfiltered export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:26:13 -07:00

117 lines
3.9 KiB
TypeScript

import { constants } from "node:buffer";
import type { Writable } from "node:stream";
import type { Filter } from "incur";
import { estimateTokenCount } from "tokenx";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
projectAnalysisError,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import {
jsonLineParts,
jsonLinesErrorRecord,
jsonLinesParts,
jsonLinesRecords,
prepareJsonLines,
} from "./jsonLines.js";
import {
writeJsonOutput,
writeOutputParts,
writeOutputText,
} from "./jsonOutput.js";
interface CliJsonLinesInput {
readonly source: unknown;
readonly selected: unknown;
readonly operationFailure: unknown;
readonly command: string;
readonly duration: string;
readonly fullOutput: boolean;
readonly tokenCount: boolean;
readonly filterOutput: string | undefined;
readonly filterPaths: ReturnType<typeof Filter.parse> | undefined;
}
/** Render one already classified result, sharing a plan for output and counting. */
export const writeCliJsonLines = async (
options: CliJsonLinesInput,
destination: Writable,
): Promise<AnalysisErrorProjection | undefined> => {
const { source, selected, operationFailure } = options;
let error: AnalysisErrorProjection | undefined;
const meta = { command: options.command, duration: options.duration };
const envelope = (data: unknown) =>
options.fullOutput ? { ok: true, data, meta } : data;
let failureRecord =
operationFailure === undefined
? undefined
: jsonLinesErrorRecord(
options.fullOutput
? { ok: false, error: operationFailure, meta }
: operationFailure,
);
const plan =
failureRecord === undefined
? prepareJsonLines(envelope(selected), {
command: options.command,
source: envelope(source),
...(options.filterOutput === undefined
? {}
: { filter: options.filterOutput }),
...(options.filterPaths === undefined
? {}
: { filterPaths: options.filterPaths }),
fullOutput: options.fullOutput,
})
: undefined;
if (plan?.ok === false) {
error = projectAnalysisError(plan.error);
failureRecord = jsonLinesErrorRecord(
options.fullOutput ? { ok: false, error, meta } : error,
);
}
if (failureRecord !== undefined) {
await writeJsonOutput(failureRecord, destination, "jsonl");
} else if (plan?.ok === true) {
if (options.tokenCount) {
let count = 0;
for (const record of jsonLinesRecords(plan.value)) {
let characters = 0;
for (const part of jsonLineParts(record)) characters += part.length;
if (characters > constants.MAX_STRING_LENGTH) {
error = projectAnalysisError(
new AnalysisResourceConstraintError(
options.command,
"memory",
"A JSON Lines record exceeds the token estimator's runtime string limit.",
{
formatted_characters: characters,
max_string_characters: constants.MAX_STRING_LENGTH,
},
{
remediationAction:
"Remove --token-count to stream the records, or select smaller fields with --filter-output.",
},
),
);
break;
}
// Newlines split tokenx segments, so summing complete lines is
// identical to estimating the emitted stream, with one-line memory.
count += estimateTokenCount([...jsonLineParts(record)].join(""));
}
if (error !== undefined)
await writeJsonOutput(
jsonLinesErrorRecord(
options.fullOutput ? { ok: false, error, meta } : error,
),
destination,
"jsonl",
);
else await writeOutputText(destination, `${count}\n`);
} else await writeOutputParts(destination, jsonLinesParts(plan.value));
}
return error;
};