fix(cli): preserve selected JSON members and Markdown table text

Preserve boundary facts and uncertainty through their consumers. Add focused production-boundary regressions and update affected documentation.

Fixes #6756
Fixes #6752
Fixes #6755
This commit is contained in:
morluto committed 2026-10-23 23:49:26 +08:00
1 parent 52b0a54afd
commit 17cd07342d
7 files changed
+411 -18

No files matched your search

+7
View File
@@ -592,10 +592,17 @@ redirected, which keeps large results smaller for agents and scripts; pipe
through `jq .` for indented files. Output selection and formatting do not change
operation status.
`--format md` escapes backslashes and pipes in table headers and cells, and
renders cell line breaks as `<br>`. These are presentation changes; retained
Evidence and JSON output keep the original values. Table widths are measured
iteratively, so an admitted row page is not limited by the JavaScript function
argument count.
`--filter-output` selects fields from successful command data. Classified failures
retain their complete typed diagnostic projection even when the selector matches
a field such as `code` or `message`, or names a missing field. `--full-output`
controls whether the outer `{ok, error, meta}` envelope is included.
Selected own JSON members, including `__proto__`, remain ordinary output data.
Sibling array slices that select different fields are combined by original row
index and emitted in source order, regardless of selector order. Selecting a
+150
View File
@@ -0,0 +1,150 @@
/*
* Markdown output layout is adapted from Incur's Formatter.
* Copyright (c) 2025-present weth, LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Formatter } from "incur";
/** Keep REA's Markdown layout compatible with Incur while owning table safety. */
export const formatCliOutput = (
value: unknown,
format: Formatter.Format,
): string =>
format === "md" ? formatMarkdown(value) : Formatter.format(value, format);
const isScalar = (value: unknown): boolean =>
value === null || value === undefined || typeof value !== "object";
const isFlatObject = (value: Record<string, unknown>): boolean =>
Object.values(value).every(isScalar);
const isObjectArray = (value: unknown): value is Record<string, unknown>[] =>
Array.isArray(value) &&
value.length > 0 &&
value.every(
(item) => typeof item === "object" && item !== null && !Array.isArray(item),
);
const escapeTableCell = (value: string): string =>
value
.replace(/\\/g, "\\\\")
.replace(/\|/g, "\\|")
.replace(/\r\n|\r|\n/g, "<br>");
const renderTable = (
headers: readonly string[],
rows: readonly string[][],
): string => {
const escapedHeaders = headers.map(escapeTableCell);
const widths = escapedHeaders.map((header) => header.length);
for (const row of rows) {
for (let index = 0; index < escapedHeaders.length; index += 1) {
const width = escapeTableCell(row[index] ?? "").length;
if (width > widths[index]!) widths[index] = width;
}
}
const pad = (value: string, index: number): string =>
value.padEnd(widths[index]!);
const lines = [
`| ${escapedHeaders.map(pad).join(" | ")} |`,
`|${widths.map((width) => "-".repeat(width + 2)).join("|")}|`,
];
for (const row of rows) {
lines.push(
`| ${escapedHeaders
.map((_, index) => pad(escapeTableCell(row[index] ?? ""), index))
.join(" | ")} |`,
);
}
return lines.join("\n");
};
const formatKeyValueTable = (value: Record<string, unknown>): string =>
renderTable(
["Key", "Value"],
Object.entries(value).map(([key, child]) => [key, String(child)]),
);
const formatObjectTable = (
items: readonly Record<string, unknown>[],
): string => {
const headers: string[] = [];
const seen = new Set<string>();
for (const item of items)
for (const key of Object.keys(item))
if (!seen.has(key)) {
seen.add(key);
headers.push(key);
}
return renderTable(
headers,
items.map((item) => headers.map((key) => String(item[key] ?? ""))),
);
};
const formatMarkdown = (
value: unknown,
path: readonly string[] = [],
): string => {
if (value == null && path.length === 0) return "";
if (isScalar(value)) {
if (path.length === 0) return String(value);
return `## ${path.join(".")}\n\n${String(value)}`;
}
if (Array.isArray(value)) {
if (isObjectArray(value)) {
const table = formatObjectTable(value);
return path.length === 0 ? table : `## ${path.join(".")}\n\n${table}`;
}
return formatMarkdown(String(value), path);
}
const object = value as Record<string, unknown>;
const entries = Object.entries(object);
if (path.length === 0 && isFlatObject(object))
return formatKeyValueTable(object);
const needsHeadings =
path.length > 0 ||
entries.length > 1 ||
entries.some(([, child]) => !isScalar(child));
if (!needsHeadings) return formatKeyValueTable(object);
return entries
.map(([key, child]) => {
const childPath = [...path, key];
const heading = `## ${childPath.join(".")}\n\n`;
if (isScalar(child)) return `${heading}${String(child)}`;
if (isObjectArray(child)) return `${heading}${formatObjectTable(child)}`;
if (
typeof child === "object" &&
child !== null &&
!Array.isArray(child)
) {
const nested = child as Record<string, unknown>;
return isFlatObject(nested)
? `${heading}${formatKeyValueTable(nested)}`
: formatMarkdown(nested, childPath);
}
return `${heading}${String(child)}`;
})
.join("\n\n");
};
+35 -18
View File
@@ -1,7 +1,7 @@
import { constants } from "node:buffer";
import type { Writable } from "node:stream";
import { Filter, Formatter } from "incur";
import { Filter } from "incur";
import { estimateTokenCount } from "tokenx";
import {
@@ -18,6 +18,7 @@ import {
projectAnalysisError,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import { formatCliOutput } from "./cliOutputFormatter.js";
import {
jsonOutputParts,
writeJsonOutput,
@@ -164,7 +165,7 @@ export const createCliResultOutput = (
: error;
// Recovery must remain actionable even when a token window/count was
// requested. Do not apply those controls to this small typed failure.
const text = Formatter.format(document, format);
const text = formatCliOutput(document, format);
handled = true;
try {
await writeOutputText(destination, `${text}\n`);
@@ -247,9 +248,10 @@ export const createCliResultOutput = (
!options.tokenCount &&
format !== "json" &&
format !== "jsonl";
// Delegate unfiltered successful non-JSON rendering to Incur. This boundary
// owns filtered successes, requested failure projections, and token windows.
// Delegate other human formats to Incur. Markdown stays here so admission
// and emitted table text use the same safe formatter.
if (
format !== "md" &&
format !== "json" &&
format !== "jsonl" &&
operationFailure === undefined &&
@@ -280,7 +282,7 @@ export const createCliResultOutput = (
}
}
if (characters <= constants.MAX_STRING_LENGTH) {
const count = estimateTokenCount(Formatter.format(tokenValue, format));
const count = estimateTokenCount(formatCliOutput(tokenValue, format));
handled = true;
try {
await writeOutputText(destination, `${count}\n`);
@@ -334,7 +336,7 @@ export const createCliResultOutput = (
? {}
: filtered;
if (ownsSuccessfulTokenWindow) {
const formatted = Formatter.format(filtered, format);
const formatted = formatCliOutput(filtered, format);
const window = sliceTextByTokenWindow(
formatted,
options.tokenOffset ?? 0,
@@ -349,7 +351,7 @@ export const createCliResultOutput = (
? `${window.text}\n[truncated: showing tokens ${options.tokenOffset ?? 0}–${window.end} of ${window.total}]`
: formatted;
const outputDocument = options.fullOutput
? Formatter.format(
? formatCliOutput(
window.truncated
? {
ok: true,
@@ -383,11 +385,20 @@ export const createCliResultOutput = (
// Once writing starts, a failed destination must not receive a second
// document from Incur's error formatter.
const formattedText =
(operationFailure !== undefined || ownsFilteredOutput) &&
format !== "json" &&
format !== "jsonl"
? Formatter.format(document, format)
format === "md" ||
((operationFailure !== undefined || ownsFilteredOutput) &&
format !== "json" &&
format !== "jsonl")
? formatCliOutput(document, format)
: undefined;
if (
format === "md" &&
formattedText === "" &&
!options.fullOutput &&
!ownsFilteredOutput &&
operationFailure === undefined
)
return false;
handled = true;
try {
if (formattedText !== undefined) {
@@ -422,7 +433,7 @@ const fullFailureEnvelope = (
): Readonly<Record<string, unknown>> => {
if (tokenLimit === undefined && tokenOffset === undefined)
return { ok: false, error: failure, meta };
const formatted = Formatter.format(failure, format);
const formatted = formatCliOutput(failure, format);
const offset = tokenOffset ?? 0;
const window = sliceTextByTokenWindow(formatted, offset, tokenLimit);
if (!window.truncated) return { ok: false, error: failure, meta };
@@ -449,7 +460,12 @@ const applyCliOutputFilter = (value: unknown, paths: FilterPaths): unknown => {
"key" in paths[0][0]
) {
const key = paths[0][0].key;
if (typeof value !== "object" || value === null) return undefined;
if (
typeof value !== "object" ||
value === null ||
!Object.hasOwn(value, key)
)
return undefined;
const selected = (value as Record<string, unknown>)[key];
return typeof selected !== "object" || selected === null
? selected
@@ -470,31 +486,32 @@ const projectObjectPaths = (value: unknown, paths: FilterPaths): unknown => {
byKey.set(segment.key, siblings);
}
const result: Record<string, unknown> = {};
const result = new Map<string, unknown>();
for (const key of [...byKey.keys()].sort()) {
const siblings = byKey.get(key)!;
if (!Object.hasOwn(value, key)) continue;
const child = (value as Record<string, unknown>)[key];
if (child === undefined) continue;
// Selecting a parent retains its complete original subtree and dominates
// any narrower selections below it, independent of path order.
if (siblings.some((path) => path.length === 0)) {
result[key] = child;
result.set(key, child);
continue;
}
const slicePaths = siblings.filter(
(path) => path[0] !== undefined && "start" in path[0],
);
if (slicePaths.length > 0 && Array.isArray(child)) {
result[key] = projectArraySlices(child, slicePaths);
result.set(key, projectArraySlices(child, slicePaths));
continue;
}
const objectPaths = siblings.filter(
(path) => path[0] !== undefined && "key" in path[0],
);
if (objectPaths.length > 0 && typeof child === "object" && child !== null)
result[key] = projectObjectPaths(child, objectPaths);
result.set(key, projectObjectPaths(child, objectPaths));
}
return result;
return Object.fromEntries(result);
};
/** Union sliced rows by their original index, then emit them in source order. */
@@ -552,6 +552,92 @@ cliTest(
expect(complete.json).toEqual(bundle);
},
);
cliTest(
"formats the full caller-admitted native inventory page as Markdown and JSON",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-native-inventory-markdown-",
);
const input = join(root, "view.json");
const rowCount = 150_000;
{
const parent = createEvidence(
{
path: "/fixtures/native.exe",
format: "pe",
sha256: "a".repeat(64),
},
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "list_names",
parameters: {},
result: Array.from({ length: rowCount }, (_, index) => ({
address: `0x${index.toString(16)}`,
value: `item${index}`,
})),
},
);
const request = {
source: { kind: "inline", evidence: parent },
view: {
kind: "native-inventory",
limit: rowCount,
fields: ["value"],
},
};
await writeFile(input, JSON.stringify(request));
}
const environment = {
HOME: root,
XDG_CONFIG_HOME: root,
XDG_CACHE_HOME: root,
REA_LOG_LEVEL: "silent",
};
{
const json = await cli.run({
arguments: [
"inspect-analysis-view",
input,
"--format",
"json",
"--filter-output",
"normalized_result.rows",
],
environment,
timeoutMs: 60_000,
});
expect(json.exitCode).toBe(0);
expect(
(json.json as { normalized_result: { rows: unknown[] } })
.normalized_result.rows,
).toHaveLength(rowCount);
}
const markdown = await cli.run({
arguments: [
"inspect-analysis-view",
input,
"--format",
"md",
"--filter-output",
"normalized_result.rows",
],
environment,
timeoutMs: 60_000,
});
expect(markdown.exitCode).toBe(0);
expect(markdown.stdout).toContain("## normalized_result.rows");
expect(markdown.stdout).toContain("| source_index");
let lineBreaks = 0;
for (const character of markdown.stdout)
if (character === "\n") lineBreaks += 1;
expect(lineBreaks).toBeGreaterThan(rowCount);
expect(markdown.stdout).toContain("149999");
},
);
cliTest(
"projects one section from a JSON file of inline layout Evidence",
async ({ cli }) => {
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { expect } from "vitest";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import {
@@ -147,3 +148,75 @@ cliTest(
}
},
);
cliTest(
"preserves a selected own __proto__ member from complete bundle data",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-evidence-filter-proto-");
const path = join(root, "bundle.json");
const result = jsonValueSchema.parse(
JSON.parse('{"__proto__":{"answer":7},"normal":{"answer":7}}'),
);
const record = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "ordinary-json", parameters: {}, result },
);
await writeFile(path, JSON.stringify(createEvidenceBundle([record])));
const output = await cli.run({
arguments: [
"inspect-evidence-bundle",
JSON.stringify({ path, detail: "complete" }),
"--json",
"--filter-output",
"records[0,1].normalized_result.__proto__.answer",
],
});
expect(output.exitCode).toBe(0);
const records = (
output.json as {
records: Array<{ normalized_result: unknown }>;
}
).records;
const normalizedResult = records[0]?.normalized_result;
expect(typeof normalizedResult).toBe("object");
expect(normalizedResult).not.toBeNull();
expect(Object.hasOwn(normalizedResult as object, "__proto__")).toBe(true);
expect((normalizedResult as Record<string, unknown>)["__proto__"]).toEqual({
answer: 7,
});
},
);
cliTest(
"preserves cell text when formatting retained Evidence as Markdown",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-evidence-markdown-cells-");
const path = join(root, "bundle.json");
const result = { text: "left|right\\tail\nnext" };
const record = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "ordinary-json",
parameters: {},
result,
},
);
await writeFile(path, JSON.stringify(createEvidenceBundle([record])));
const base = [
"inspect-evidence-bundle",
JSON.stringify({ path, detail: "complete" }),
"--filter-output",
"records.0.normalized_result",
];
const json = await cli.run({ arguments: [...base, "--json"] });
expect(json.exitCode, json.stderr).toBe(0);
expect(json.stdout).toContain(JSON.stringify(result.text));
const markdown = await cli.run({ arguments: [...base, "--format", "md"] });
expect(markdown.exitCode, markdown.stderr).toBe(0);
expect(markdown.stdout).toContain("left\\|right\\\\tail<br>next");
},
);
@@ -210,6 +210,35 @@ it("finishes JSONL after a bounded large record before a missing selection", asy
expect(output.failed).toBe(false);
});
it("does not invent selected JSON members from the object prototype", async () => {
const value = freezeRecursively({ selected: { present: 7 } });
const output = await runFilteredCli(value, "json", "selected.__proto__");
expect(JSON.parse(output)).toEqual({ selected: {} });
expect(value).toEqual({ selected: { present: 7 } });
});
it("escapes table delimiters and line breaks in Markdown cells", async () => {
const value = freezeRecursively({
selected: {
pipe: "left|right",
backslash: "left\\right",
lines: "first line\nsecond line",
},
});
const output = await runFilteredCli(value, "md", "selected");
expect(output).toContain("left\\|right");
expect(output).toContain("left\\\\right");
expect(output).toContain("first line<br>second line");
expect(output).not.toContain("left|right");
expect(value.selected).toEqual({
pipe: "left|right",
backslash: "left\\right",
lines: "first line\nsecond line",
});
});
const projectionSource = freezeRecursively([
{
rows: [
@@ -120,6 +120,37 @@ describe("CLI formatting stage admission", () => {
});
describe("CLI formatting recovery outcomes", () => {
it("formats a recovered Markdown result through the CLI Markdown owner", async () => {
const cause = new RangeError("Invalid string length");
const chunks: Buffer[] = [];
const output = createCliResultOutput(
["--format", "md"],
new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
}),
);
const value = new Proxy(
{},
{
ownKeys: () => {
throw cause;
},
},
);
expect(await output?.write(value, { ...metadata, format: "md" })).toBe(
true,
);
expect(output?.error).toMatchObject({ code: "resource_constraint" });
const text = Buffer.concat(chunks).toString("utf8");
expect(text).toContain("resource_constraint");
expect(text).toContain("original result was not written");
expect(output?.failed).toBe(false);
});
it("retains ownership if the recovery document cannot be written", async () => {
const cause = new RangeError("Invalid string length");
const output = createCliResultOutput(