diff --git a/docs/cli.md b/docs/cli.md index f6e936d0a6..c51ebef579 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -796,7 +796,9 @@ operation status. `--format md` escapes backslashes and pipes in table headers and cells, and renders cell line breaks as `
`. These are presentation changes; retained -Evidence and JSON output keep the original values. Table widths are measured +Evidence and JSON output keep the original values. Nested objects and arrays +use JSON inside their owning Markdown cells, retaining the field names and +values instead of replacing them with an object label. Table widths are measured iteratively, so an admitted row page is not limited by the JavaScript function argument count. diff --git a/src/cli/cliOutputFormatter.ts b/src/cli/cliOutputFormatter.ts index 51e8a0bf78..ced0f34004 100644 --- a/src/cli/cliOutputFormatter.ts +++ b/src/cli/cliOutputFormatter.ts @@ -77,10 +77,18 @@ const renderTable = ( return lines.join("\n"); }; +/** Keep nested JSON facts inside their owning table cell. */ +const markdownCell = (value: unknown): string => + value !== null && typeof value === "object" + ? JSON.stringify(value) + : value === undefined + ? "" + : String(value); + const formatKeyValueTable = (value: Record): string => renderTable( ["Key", "Value"], - Object.entries(value).map(([key, child]) => [key, String(child)]), + Object.entries(value).map(([key, child]) => [key, markdownCell(child)]), ); const formatObjectTable = ( @@ -96,7 +104,7 @@ const formatObjectTable = ( } return renderTable( headers, - items.map((item) => headers.map((key) => String(item[key] ?? ""))), + items.map((item) => headers.map((key) => markdownCell(item[key]))), ); }; @@ -114,7 +122,7 @@ const formatMarkdown = ( const table = formatObjectTable(value); return path.length === 0 ? table : `## ${path.join(".")}\n\n${table}`; } - return formatMarkdown(String(value), path); + return formatMarkdown(markdownCell(value), path); } const object = value as Record; @@ -144,7 +152,7 @@ const formatMarkdown = ( ? `${heading}${formatKeyValueTable(nested)}` : formatMarkdown(nested, childPath); } - return `${heading}${String(child)}`; + return `${heading}${markdownCell(child)}`; }) .join("\n\n"); }; diff --git a/tests/boundary/cli/markdownNestedFacts.test.ts b/tests/boundary/cli/markdownNestedFacts.test.ts new file mode 100644 index 0000000000..3a15cbe090 --- /dev/null +++ b/tests/boundary/cli/markdownNestedFacts.test.ts @@ -0,0 +1,48 @@ +import { join } from "node:path"; +import { expect, it } from "vitest"; + +import { renderCliDocument } from "../../../src/cliOutput.js"; +import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js"; + +it("retains nested facts in standalone Markdown table cells and mixed arrays", () => { + const value = [ + { + details: { path: "alpha.txt" }, + entries: [{ path: "sub/beta.txt" }], + mixed: ["selected", { nested: "caller|value" }], + }, + ]; + const markdown = renderCliDocument(value, "md", false); + expect(markdown).toContain('{"path":"alpha.txt"}'); + expect(markdown).toContain('[{"path":"sub/beta.txt"}]'); + expect(markdown).toContain("caller\\|value"); + expect(markdown).not.toContain("[object Object]"); + expect( + renderCliDocument({ mixed: [1, { path: "retained" }] }, "md", false), + ).toContain('{"path":"retained"}'); + expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value); +}); + +workspaceCliTest( + "preserves archive inventory facts through public Markdown and JSON output", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-markdown-archive-facts-"); + const archive = join(root, "fixture.zip"); + await writeOrderedZip(archive, ["alpha.txt", "sub/beta.txt"]); + const markdown = await cli.run({ + arguments: ["inspect-artifact", archive, "--format", "md"], + }); + const json = await cli.run({ + arguments: ["inspect-artifact", archive, "--json"], + }); + expect(markdown.exitCode).toBe(0); + expect(json.exitCode).toBe(0); + for (const entry of ["alpha.txt", "sub/beta.txt"]) { + expect(markdown.stdout).toContain(entry); + expect(json.stdout).toContain(entry); + } + expect(markdown.stdout).not.toContain("[object Object]"); + }, +);