From 6ac743ed1d60c8b6e039b357fba51683107b4114 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Tue, 7 Jul 2026 00:39:04 -0400 Subject: [PATCH] feat: add -f/--format flag with strict msgpack rejection msgpack is a recognized-but-unimplemented value: it must exit non-zero with a clear message, never silently fall back to json. Unknown values are likewise rejected, mirroring the existing --emit validation style. --- README.md | 2 ++ src/cli.ts | 12 ++++++++++++ src/options/options.ts | 5 +++++ test/dataflow.test.ts | 30 ++++++++++++++++++++++++++++++ test/neo4j-bolt.test.ts | 1 + test/neo4j-schema.test.ts | 2 +- test/schema-v2.test.ts | 1 + 7 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d96769..5cbbb3d 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,8 @@ Options: of union) (default: "union") --tsc-only use the tsc resolver only — opt out of Jelly edges (overrides --call-graph-provider) + -f, --format output format: json (default) | msgpack (not + yet implemented) (default: "json") -c, --cache-dir cache/intermediate directory -v, --verbose increase verbosity (repeatable) -h, --help display help for command diff --git a/src/cli.ts b/src/cli.ts index c2bdb46..ac2cb3c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -63,6 +63,7 @@ export function buildProgram(): Command { "union", ) .option("--tsc-only", "use the tsc resolver only — opt out of Jelly edges (overrides --call-graph-provider)") + .option("-f, --format ", "output format: json (default) | msgpack (not yet implemented)", "json") .option("-c, --cache-dir ", "cache/intermediate directory") .option("-v, --verbose", "increase verbosity (repeatable)", (_v: string, prev: number) => prev + 1, 0) .allowExcessArguments(true); @@ -98,6 +99,16 @@ export function parseArgs(argv: string[]): AnalysisOptions { level = 4; // force max implemented depth so the projected graph is the full CPG } + // --format: strict validation — an unrecognized value must error, never silently fall back to + // json; msgpack is a recognized-but-unimplemented value and must error too, not silently degrade. + const format = String(o.format ?? "json"); + if (!["json", "msgpack"].includes(format)) { + program.error(`error: invalid --format '${format}' (expected: json, msgpack)`); + } + if (format === "msgpack") { + program.error("error: msgpack output is not yet implemented; use --format json"); + } + // --graphs: strict validation (never a silent fallback). Default = all rungs at or below the level; // cfg/dfg/pdg require -a 3, sdg requires -a 4. let graphs: GraphSelector[] = level >= 4 ? [...ALL_GRAPHS] : level === 3 ? ["cfg", "dfg", "pdg"] : []; @@ -158,6 +169,7 @@ export function parseArgs(argv: string[]): AnalysisOptions { input: o.input ? path.resolve(String(o.input)) : "", output: o.output ? path.resolve(String(o.output)) : null, emit, + format: format as "json", appName: o.appName ? String(o.appName) : null, neo4jUri: o.neo4jUri ? String(o.neo4jUri) : null, neo4jUser: String(o.neo4jUser), diff --git a/src/options/options.ts b/src/options/options.ts index 80d10d8..6a05383 100644 --- a/src/options/options.ts +++ b/src/options/options.ts @@ -11,6 +11,11 @@ export interface AnalysisOptions { output: string | null; /** Output target: json (analysis.json, default) or neo4j (graph.cypher / live Bolt push). */ emit: EmitTarget; + /** + * Output serialization format (`-f/--format`). Only "json" is implemented; msgpack is rejected + * at the CLI layer (`parseArgs`), so it never reaches here — the type reflects that invariant. + */ + format: "json"; /** Logical application name for the graph's :Application anchor; null ⇒ derived from input. */ appName: string | null; /** Bolt URI for a live Neo4j push (incremental). null ⇒ write a graph.cypher snapshot to -o. */ diff --git a/test/dataflow.test.ts b/test/dataflow.test.ts index 155c23a..d0f381a 100644 --- a/test/dataflow.test.ts +++ b/test/dataflow.test.ts @@ -20,6 +20,7 @@ function options(level: 1 | 2 | 3, cacheDir: string, jobs: number): AnalysisOpti input: FIXTURE, output: null, emit: "json", + format: "json", appName: null, neo4jUri: null, neo4jUser: "neo4j", @@ -423,3 +424,32 @@ describe("--graphs flag validation", () => { expect(r.stderr.toString()).toContain("invalid --jobs"); }); }); + +describe("--format flag validation", () => { + const cli = (...args: string[]) => + Bun.spawnSync(["bun", "run", path.resolve(import.meta.dir, "../src/index.ts"), ...args], { + cwd: path.resolve(import.meta.dir, ".."), + }); + + test("--format msgpack is rejected — not yet implemented, never a silent fallback to json", () => { + const r = cli("-i", FIXTURE, "--format", "msgpack", "--no-build"); + expect(r.exitCode).not.toBe(0); + expect(r.stderr.toString()).toContain("msgpack output is not yet implemented"); + }); + + test("unknown --format value fails with a clear error", () => { + const r = cli("-i", FIXTURE, "--format", "bogus", "--no-build"); + expect(r.exitCode).not.toBe(0); + expect(r.stderr.toString()).toContain("invalid --format 'bogus'"); + }); + + test("--format json succeeds (exit 0)", () => { + const r = cli("-i", FIXTURE, "--format", "json", "--no-build"); + expect(r.exitCode).toBe(0); + }); + + test("omitting --format succeeds (defaults to json)", () => { + const r = cli("-i", FIXTURE, "--no-build"); + expect(r.exitCode).toBe(0); + }); +}); diff --git a/test/neo4j-bolt.test.ts b/test/neo4j-bolt.test.ts index c82e71b..4473b9f 100644 --- a/test/neo4j-bolt.test.ts +++ b/test/neo4j-bolt.test.ts @@ -31,6 +31,7 @@ function optsFor(overrides: Partial = {}): AnalysisOptions { input: FIXTURE, output: null, emit: "json", + format: "json", appName: null, neo4jUri: null, neo4jUser: "neo4j", diff --git a/test/neo4j-schema.test.ts b/test/neo4j-schema.test.ts index 389e766..fcab4ad 100644 --- a/test/neo4j-schema.test.ts +++ b/test/neo4j-schema.test.ts @@ -26,7 +26,7 @@ async function fixtureRows() { const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-schema-test-")); // --emit neo4j is always full-depth, so exercise every node/edge kind at L4. const opts: AnalysisOptions = { - input: FIXTURE, output: null, emit: "neo4j", appName: "dataflow-app", + input: FIXTURE, output: null, emit: "neo4j", format: "json", appName: "dataflow-app", neo4jUri: null, neo4jUser: "neo4j", neo4jPassword: "", neo4jDatabase: null, analysisLevel: 4, graphs: ["cfg", "dfg", "pdg", "sdg"], graphFieldDepth: 3, jobs: 1, targetFiles: null, skipTests: true, eager: true, diff --git a/test/schema-v2.test.ts b/test/schema-v2.test.ts index 6e151e3..5f31605 100644 --- a/test/schema-v2.test.ts +++ b/test/schema-v2.test.ts @@ -23,6 +23,7 @@ function options(): AnalysisOptions { input: FIXTURE, output: null, emit: "json", + format: "json", appName: null, neo4jUri: null, neo4jUser: "neo4j",