mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
5d2c8c2b78
* feat(cli): add JSON Lines output for row-oriented results (#5487) - Support opt-in JSON Lines format (--format jsonl) for row-oriented results and collection views with clear record boundaries - Preserve stable source identities and stream records deterministically line-by-line - Emit final coverage and limitation metadata as a distinct line with https://rea.dev/schemas/jsonl-coverage-v1.json to prevent confusion with data rows - Distinguish error outputs with https://rea.dev/schemas/jsonl-error-v1.json - Reject non-row-oriented results that lack record boundaries with an unreadable_output error - Document JSON Lines streaming conventions in docs/cli.md - Add boundary tests covering empty results, multiline strings, partial coverage, large fields, and non-row rejection * fix(cli): preserve diagnostics, provenance, and nested collections in JSONL * fix(cli): frame classified failures and reject unrepresented JSONL facets --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import { Writable } from "node:stream";
|
|
|
|
import { Cli, z } from "incur";
|
|
import { estimateTokenCount } from "tokenx";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
createCliResultOutput,
|
|
streamCliCommandResults,
|
|
} from "../../../src/cli/cliResultOutput.js";
|
|
import { jsonLinesErrorRecord } from "../../../src/cli/jsonLines.js";
|
|
import { parseCliOutputArguments } from "../../../src/cliOutput.js";
|
|
import { logCliCommand } from "../../../src/cliLogging.js";
|
|
import { silentLogger } from "../../../src/logger.js";
|
|
|
|
const capture = (args: readonly string[]) => {
|
|
const chunks: Buffer[] = [];
|
|
const destination = new Writable({
|
|
write(chunk: Buffer, _encoding, callback) {
|
|
chunks.push(chunk);
|
|
callback();
|
|
},
|
|
});
|
|
const output = createCliResultOutput(
|
|
parseCliOutputArguments(args),
|
|
destination,
|
|
);
|
|
if (output === undefined) throw new Error("Missing output owner");
|
|
return {
|
|
output,
|
|
destination,
|
|
text: () => Buffer.concat(chunks).toString("utf8"),
|
|
};
|
|
};
|
|
const lines = (text: string): unknown[] =>
|
|
text.trimEnd() === ""
|
|
? []
|
|
: text
|
|
.trimEnd()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line));
|
|
const metadata = { command: "fixture", duration: "1ms", format: "jsonl" };
|
|
|
|
describe("row-oriented CLI JSON Lines", () => {
|
|
it.each(["rows", "items", "records", "occurrences"])(
|
|
"preserves all context around %s without a field whitelist",
|
|
async (key) => {
|
|
const records = [
|
|
{ id: "one", index: 8, text: "first\n雪𝟠" },
|
|
{ id: "two", index: 9 },
|
|
];
|
|
const context = {
|
|
evidence_id: "ev:parent",
|
|
subject: { path: "/local/subject" },
|
|
provider: "actual-provider",
|
|
authority: "static",
|
|
confidence: "derived",
|
|
locations: [{ path: "/source", line: 4 }],
|
|
parameters: { field: "caller choice" },
|
|
raw_result: null,
|
|
evidence_links: ["ev:source"],
|
|
limitations: ["partial source"],
|
|
normalized_result: {
|
|
kind: "inventory",
|
|
parent_digest: "sha256:parent",
|
|
view_digest: "sha256:view",
|
|
artifact: { id: "art:one" },
|
|
coverage: { complete: false, examined: 2, total: null },
|
|
page: { offset: 8, has_more: true },
|
|
[key]: records,
|
|
},
|
|
};
|
|
const stream = capture(["--format", "jsonl"]);
|
|
await stream.output.write(context, metadata);
|
|
const { [key]: _records, ...viewContext } = context.normalized_result;
|
|
expect(lines(stream.text())).toEqual([
|
|
...records,
|
|
{
|
|
$rea: "jsonl/v1",
|
|
type: "metadata",
|
|
records: { path: ["normalized_result", key], emitted: 2, source: 2 },
|
|
context: { ...context, normalized_result: viewContext },
|
|
},
|
|
]);
|
|
expect(context.normalized_result[key]).toBe(records);
|
|
},
|
|
);
|
|
|
|
it.each([[], [{ id: "row" }]].map((rows) => ({ rows })))(
|
|
"preserves nested pagination without a coverage object (%j)",
|
|
async ({ rows }) => {
|
|
const stream = capture(["--format", "jsonl", "--full-output"]);
|
|
const value = {
|
|
page: { rows, offset: 12, total: 31, next_offset: 14 },
|
|
limitations: ["more pages"],
|
|
};
|
|
await stream.output.write(value, metadata);
|
|
expect(lines(stream.text())).toEqual([
|
|
...rows,
|
|
{
|
|
$rea: "jsonl/v1",
|
|
type: "metadata",
|
|
records: {
|
|
path: ["data", "page", "rows"],
|
|
emitted: rows.length,
|
|
source: rows.length,
|
|
},
|
|
context: {
|
|
ok: true,
|
|
data: {
|
|
page: { offset: 12, total: 31, next_offset: 14 },
|
|
limitations: value.limitations,
|
|
},
|
|
meta: { command: "fixture", duration: "1ms" },
|
|
},
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("JSON Lines admission and selection", () => {
|
|
it.each([
|
|
null,
|
|
{},
|
|
"scalar",
|
|
{ rows: [], items: [] },
|
|
{ rows: [], normalized_result: { rows: [] } },
|
|
Object.create({ rows: [{ inherited: true }] }),
|
|
{ rows: [{ $rea: "collision" }] },
|
|
])(
|
|
"refuses an unclear or reserved boundary before writing rows: %j",
|
|
async (value) => {
|
|
const stream = capture(["--format", "jsonl", "--token-count"]);
|
|
await stream.output.write(value, metadata);
|
|
expect(lines(stream.text())).toEqual([
|
|
jsonLinesErrorRecord(stream.output.error),
|
|
]);
|
|
expect(stream.output.error?.code).toBe("unreadable_output");
|
|
},
|
|
);
|
|
|
|
it.each(["missing", "count", "nested.value"])(
|
|
"does not admit a scalar/missing selection merely because a filter exists: %s",
|
|
async (filter) => {
|
|
const stream = capture(["--format", "jsonl", "--filter-output", filter]);
|
|
await stream.output.write(
|
|
{ rows: [{ id: 1 }], count: 0, nested: { value: null } },
|
|
metadata,
|
|
);
|
|
expect(lines(stream.text())).toHaveLength(1);
|
|
expect(lines(stream.text())[0]).toMatchObject({
|
|
$rea: "jsonl/v1",
|
|
type: "error",
|
|
result: { code: "unreadable_output" },
|
|
});
|
|
},
|
|
);
|
|
|
|
it("records output selection separately from source coverage and keeps provenance", async () => {
|
|
const value = {
|
|
evidence_id: "ev:parent",
|
|
raw_result: { excluded: "large observation" },
|
|
provider: "source-provider",
|
|
normalized_result: {
|
|
rows: [
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
{ id: "c", text: "third" },
|
|
],
|
|
coverage: { complete: true, examined: 3 },
|
|
},
|
|
};
|
|
const filter = "normalized_result.rows[1,3].id";
|
|
const stream = capture(["--format", "jsonl", "--filter-output", filter]);
|
|
await stream.output.write(value, metadata);
|
|
expect(lines(stream.text())).toEqual([
|
|
{ id: "b" },
|
|
{ id: "c" },
|
|
{
|
|
$rea: "jsonl/v1",
|
|
type: "metadata",
|
|
records: {
|
|
path: ["normalized_result", "rows"],
|
|
emitted: 2,
|
|
source: 3,
|
|
filter,
|
|
},
|
|
context: {
|
|
evidence_id: "ev:parent",
|
|
provider: "source-provider",
|
|
normalized_result: { coverage: { complete: true, examined: 3 } },
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("disambiguates selected collections without restoring excluded peer rows", async () => {
|
|
const stream = capture([
|
|
"--format",
|
|
"jsonl",
|
|
"--filter-output",
|
|
"normalized_result.rows",
|
|
]);
|
|
await stream.output.write(
|
|
{
|
|
evidence_id: "ev:parent",
|
|
raw_result: null,
|
|
normalized_result: {
|
|
rows: [{ id: 1 }],
|
|
items: [{ excluded: "peer" }],
|
|
coverage: { status: "unknown" },
|
|
},
|
|
},
|
|
metadata,
|
|
);
|
|
expect(lines(stream.text())).toEqual([
|
|
{ id: 1 },
|
|
{
|
|
$rea: "jsonl/v1",
|
|
type: "metadata",
|
|
records: {
|
|
path: ["normalized_result", "rows"],
|
|
emitted: 1,
|
|
source: 1,
|
|
filter: "normalized_result.rows",
|
|
},
|
|
context: {
|
|
evidence_id: "ev:parent",
|
|
normalized_result: { coverage: { status: "unknown" } },
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("JSON Lines counting and transport", () => {
|
|
it.each([false, true])(
|
|
"frames classified failures without filtering/counting them away (full: %s)",
|
|
async (full) => {
|
|
const stream = capture([
|
|
"--format",
|
|
"jsonl",
|
|
"--token-count",
|
|
"--filter-output",
|
|
"missing",
|
|
...(full ? ["--full-output"] : []),
|
|
]);
|
|
const failure = {
|
|
healthy: false,
|
|
checks: [{ ok: false, name: "source" }],
|
|
diagnostics: { path: "/caller/path" },
|
|
};
|
|
await stream.output.write(failure, metadata);
|
|
expect(lines(stream.text())).toEqual([
|
|
jsonLinesErrorRecord(
|
|
full
|
|
? {
|
|
ok: false,
|
|
error: failure,
|
|
meta: { command: "fixture", duration: "1ms" },
|
|
}
|
|
: failure,
|
|
),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"counts array-valued rows as single records (object collection: %s)",
|
|
async (wrapped) => {
|
|
const rows = [[false, null], [0], [], [{ nested: ["雪", "a\nb"] }]];
|
|
const value = wrapped ? { rows, coverage: { complete: false } } : rows;
|
|
const args = ["--format", "jsonl"];
|
|
const stream = capture(args);
|
|
await stream.output.write(value, metadata);
|
|
const counter = capture([...args, "--token-count"]);
|
|
await counter.output.write(value, metadata);
|
|
expect(counter.text()).toBe(`${estimateTokenCount(stream.text())}\n`);
|
|
},
|
|
);
|
|
|
|
it("counts exactly the emitted representation, including escaped multiline text and final context", async () => {
|
|
const value = {
|
|
rows: [{ text: "雪𝟠\nline".repeat(4096) }, { text: 'escaped"\\\t' }],
|
|
coverage: { complete: false, reasons: ["unknown remainder"] },
|
|
};
|
|
const args = ["--format", "jsonl", "--full-output"];
|
|
const stream = capture(args);
|
|
await stream.output.write(value, metadata);
|
|
const counter = capture([...args, "--token-count"]);
|
|
await counter.output.write(value, metadata);
|
|
expect(counter.text()).toBe(`${estimateTokenCount(stream.text())}\n`);
|
|
});
|
|
|
|
it("bounds a large escaped field and observes destination backpressure through the final metadata", async () => {
|
|
const chunks: Buffer[] = [];
|
|
let maxBytes = 0;
|
|
const destination = new Writable({
|
|
highWaterMark: 1,
|
|
write(chunk: Buffer, _encoding, callback) {
|
|
maxBytes = Math.max(maxBytes, chunk.length);
|
|
setTimeout(() => {
|
|
chunks.push(chunk);
|
|
callback();
|
|
}, 1);
|
|
},
|
|
});
|
|
const output = createCliResultOutput(
|
|
parseCliOutputArguments(["--format", "jsonl"]),
|
|
destination,
|
|
)!;
|
|
const rows = [{ id: "large", text: "\u0000雪𝟠".repeat(32 * 1024) }];
|
|
await output.write({ rows, coverage: { complete: false } }, metadata);
|
|
expect(maxBytes).toBeLessThan(128 * 1024);
|
|
const records = lines(Buffer.concat(chunks).toString("utf8"));
|
|
expect(records[0]).toEqual(rows[0]);
|
|
expect(records[1]).toMatchObject({
|
|
type: "metadata",
|
|
context: { coverage: { complete: false } },
|
|
});
|
|
expect(destination.writableEnded).toBe(false);
|
|
});
|
|
|
|
it("retains an explicitly successful error-shaped row as data", async () => {
|
|
const value = [
|
|
{
|
|
code: "not-an-error",
|
|
message: "ordinary row",
|
|
ok: false,
|
|
healthy: false,
|
|
},
|
|
];
|
|
const stream = capture(["--format", "jsonl"]);
|
|
const cli = Cli.create("explicit-success", { sync: false }).command(
|
|
"rows",
|
|
{
|
|
run: () =>
|
|
logCliCommand(
|
|
silentLogger,
|
|
"rows",
|
|
async () => value,
|
|
() => false,
|
|
),
|
|
},
|
|
);
|
|
streamCliCommandResults(cli, stream.output);
|
|
await cli.serve(["rows", "--format", "jsonl"], { stdout: () => undefined });
|
|
expect(lines(stream.text())).toEqual(value);
|
|
});
|
|
});
|
|
|
|
describe("SDK-owned JSON Lines outcomes", () => {
|
|
it("uses SDK status to frame validation errors and resets ownership between invocations", async () => {
|
|
const stream = capture(["--format", "jsonl"]);
|
|
const cli = Cli.create("sdk-status", { sync: false }).command("rows", {
|
|
args: z.object({ required: z.string() }),
|
|
run: () => [{ id: "row" }],
|
|
});
|
|
streamCliCommandResults(cli, stream.output);
|
|
const serve = async (args: string[]) => {
|
|
let status = 0;
|
|
stream.output.begin(parseCliOutputArguments(args));
|
|
await cli.serve(args, {
|
|
stdout: (text) => {
|
|
if (!stream.output.handled && !stream.output.captureSdkOutput(text))
|
|
throw new Error("Unexpected delegation");
|
|
},
|
|
exit: (code) => {
|
|
status = code;
|
|
},
|
|
});
|
|
await stream.output.finishSdkOutput(status);
|
|
return status;
|
|
};
|
|
expect(await serve(["rows", "--format", "jsonl"])).toBe(1);
|
|
expect(lines(stream.text())[0]).toMatchObject({
|
|
$rea: "jsonl/v1",
|
|
type: "error",
|
|
result: { code: "VALIDATION_ERROR" },
|
|
});
|
|
expect(await serve(["rows", "present", "--format", "jsonl"])).toBe(0);
|
|
expect(lines(stream.text())[1]).toEqual({ id: "row" });
|
|
expect(stream.output.error).toBeUndefined();
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"preserves sentinel data and frames sentinel failure using SDK outcome (%s)",
|
|
async (failure) => {
|
|
const stream = capture(["--format", "jsonl"]);
|
|
const cli = Cli.create("sdk-sentinel", { sync: false }).command(
|
|
"result",
|
|
{
|
|
run: (context) =>
|
|
failure
|
|
? context.error({
|
|
code: "EXPLICIT_FAILURE",
|
|
message: "Caller-owned error",
|
|
})
|
|
: context.ok({
|
|
code: "EXPLICIT_FAILURE",
|
|
message: "Successful data",
|
|
}),
|
|
},
|
|
);
|
|
streamCliCommandResults(cli, stream.output);
|
|
let status = 0;
|
|
await cli.serve(["result", "--format", "jsonl"], {
|
|
stdout: (text) => {
|
|
stream.output.captureSdkOutput(text);
|
|
},
|
|
exit: (code) => {
|
|
status = code;
|
|
},
|
|
});
|
|
expect(stream.text()).toBe("");
|
|
await stream.output.finishSdkOutput(status);
|
|
expect(lines(stream.text())[0]).toEqual(
|
|
failure
|
|
? jsonLinesErrorRecord({
|
|
code: "EXPLICIT_FAILURE",
|
|
message: "Caller-owned error",
|
|
})
|
|
: { code: "EXPLICIT_FAILURE", message: "Successful data" },
|
|
);
|
|
},
|
|
);
|
|
|
|
it("delegates generator events immediately without retaining the stream", async () => {
|
|
const stream = capture(["--format", "jsonl"]);
|
|
const delegated: string[] = [];
|
|
const cli = Cli.create("sdk-stream", { sync: false }).command("events", {
|
|
run: async function* () {
|
|
yield { text: "first" };
|
|
expect(delegated.length).toBeGreaterThan(0);
|
|
yield { text: "second" };
|
|
},
|
|
});
|
|
streamCliCommandResults(cli, stream.output);
|
|
await cli.serve(["events", "--format", "jsonl"], {
|
|
stdout: (text) => {
|
|
expect(stream.output.captureSdkOutput(text)).toBe(false);
|
|
delegated.push(text);
|
|
},
|
|
});
|
|
await stream.output.finishSdkOutput(0);
|
|
expect(stream.text()).toBe("");
|
|
expect(delegated.join("")).toContain("first");
|
|
expect(delegated.join("")).toContain("second");
|
|
});
|
|
});
|