From 2b6a00d2a80d4f03e3080dd55c4f3fb7b8a086e3 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sun, 5 Jul 2026 11:24:15 -0600 Subject: [PATCH] fix: scope codegraph batch complexity targets to file paths BATCH_COMMANDS entries with sig: 'dbOnly' (currently only complexity) always wrote their target into opts.target. complexityData treats opts.target as a symbol-name filter and opts.file as the file-path filter, so batch complexity never matched anything and the function list silently fell back to empty with a whole-repo summary regardless of which file was requested. Add an optional targetKey on BatchCommandEntry so each dbOnly command can declare which opts key its targets map to, and set it to 'file' for complexity. batchData and multiBatchData now route the target through the declared key instead of assuming opts.target. Fixes #1721 Impact: 4 functions changed, 6 affected --- src/features/batch.ts | 28 ++++++++++++++-- tests/integration/batch.test.ts | 57 ++++++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/features/batch.ts b/src/features/batch.ts index 12a6aefb0..d8f343e34 100644 --- a/src/features/batch.ts +++ b/src/features/batch.ts @@ -18,6 +18,15 @@ type BatchSig = 'name' | 'target' | 'file' | 'dbOnly'; interface BatchCommandEntry { fn: (...args: unknown[]) => unknown; sig: BatchSig; + /** + * For `sig: 'dbOnly'` commands only: the `opts` key each target should be + * written to before calling `fn(customDbPath, opts)`. Defaults to `'target'` + * (a symbol/name filter). Commands whose underlying query function expects a + * *file path* target (rather than a symbol name) — e.g. `complexityData`, + * which treats `opts.target` as a name filter and `opts.file` as the file + * scope — must override this so the batch target lands in the right filter. + */ + targetKey?: string; } export const BATCH_COMMANDS: Record = { @@ -31,9 +40,22 @@ export const BATCH_COMMANDS: Record = { exports: { fn: exportsData as (...args: unknown[]) => unknown, sig: 'file' }, flow: { fn: flowData as (...args: unknown[]) => unknown, sig: 'name' }, dataflow: { fn: dataflowData as (...args: unknown[]) => unknown, sig: 'name' }, - complexity: { fn: complexityData as (...args: unknown[]) => unknown, sig: 'dbOnly' }, + complexity: { + fn: complexityData as (...args: unknown[]) => unknown, + sig: 'dbOnly', + targetKey: 'file', + }, }; +/** Resolve the opts key a `dbOnly` command's target should be written to. */ +function dbOnlyTargetOpts( + entry: BatchCommandEntry, + opts: Record, + target: string, +): Record { + return { ...opts, [entry.targetKey ?? 'target']: target }; +} + interface BatchResultOk { target: string; ok: true; @@ -77,7 +99,7 @@ export function batchData( try { let data: unknown; if (entry.sig === 'dbOnly') { - data = entry.fn(customDbPath, { ...opts, target }); + data = entry.fn(customDbPath, dbOnlyTargetOpts(entry, opts, target)); } else { data = entry.fn(target, customDbPath, opts); } @@ -166,7 +188,7 @@ export function multiBatchData( try { let data: unknown; if (entry.sig === 'dbOnly') { - data = entry.fn(customDbPath, { ...merged, target }); + data = entry.fn(customDbPath, dbOnlyTargetOpts(entry, merged, target)); } else { data = entry.fn(target, customDbPath, merged); } diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts index 0dc44e3b0..8d84ea344 100644 --- a/tests/integration/batch.test.ts +++ b/tests/integration/batch.test.ts @@ -39,6 +39,12 @@ function insertEdge(db, sourceId, targetId, kind = 'calls', confidence = 1.0) { ).run(sourceId, targetId, kind, confidence); } +function insertComplexity(db, nodeId, cognitive, cyclomatic, maxNesting = 0) { + db.prepare( + 'INSERT INTO function_complexity (node_id, cognitive, cyclomatic, max_nesting) VALUES (?, ?, ?, ?)', + ).run(nodeId, cognitive, cyclomatic, maxNesting); +} + // ─── Fixture DB ──────────────────────────────────────────────────────── let tmpDir: string, dbPath: string; @@ -71,6 +77,13 @@ beforeAll(() => { insertEdge(db, fnRoute, fnAuth); insertEdge(db, fnRoute, fnFormat); + // Complexity data, distinct per file, so batch complexity can be asserted + // to scope results to the requested file rather than the whole repo. + insertComplexity(db, fnAuth, 5, 2); + insertComplexity(db, fnValidate, 3, 1); + insertComplexity(db, fnRoute, 10, 4); + insertComplexity(db, fnFormat, 1, 1); + db.close(); }); @@ -197,12 +210,46 @@ describe('batchData — edge cases', () => { // ─── complexity (dbOnly sig) ───────────────────────────────────────── describe('batchData — complexity (dbOnly signature)', () => { - test('complexity command uses target as opts.target', () => { - const data = batchData('complexity', ['authenticate'], dbPath); - expect(data.total).toBe(1); - // complexityData returns functions array — it won't error for unknown targets + test('complexity command scopes each target to a file, not a symbol name (#1721)', () => { + // Regression test for #1721: batch previously forwarded the file-path + // target as opts.target (a symbol-name filter), so complexityData never + // matched anything and silently fell back to identical, unfiltered + // whole-repo results for every target. It must land in opts.file instead. + const data = batchData('complexity', ['src/auth.js', 'src/routes.js'], dbPath); + expect(data.total).toBe(2); + expect(data.succeeded).toBe(2); + + const auth = data.results.find((r) => r.target === 'src/auth.js'); + const routes = data.results.find((r) => r.target === 'src/routes.js'); + expect(auth.ok).toBe(true); + expect(routes.ok).toBe(true); + + // src/auth.js has authenticate (cognitive 5) + validateToken (cognitive 3) + const authNames = auth.data.functions.map((f) => f.name).sort(); + expect(authNames).toEqual(['authenticate', 'validateToken']); + + // src/routes.js has only handleRoute (cognitive 10) + const routeNames = routes.data.functions.map((f) => f.name); + expect(routeNames).toEqual(['handleRoute']); + + // The two targets must not collapse into identical, unfiltered results. + // (Note: `summary` is a whole-repo health metric independent of the file + // filter, by design of complexityData — see #1807 for a separate, + // pre-existing gap where it should but doesn't reflect the file scope. + // The bug this test guards against is that `functions` came back empty + // for every target because the file path was routed into a symbol-name + // filter instead of the file filter.) + expect(auth.data.functions).not.toEqual(routes.data.functions); + }); + + test('complexity command still accepts a bare symbol name via opts.target passthrough', () => { + // BATCH_COMMANDS commands other than complexity keep symbol-name targets; + // this guards that complexity's own --target-style filtering (via shared + // opts, not the batch target) continues to work for direct callers. + const data = batchData('complexity', ['src/auth.js'], dbPath, { target: 'valid' }); expect(data.results[0].ok).toBe(true); - expect(data.results[0].data).toHaveProperty('functions'); + const names = data.results[0].data.functions.map((f) => f.name); + expect(names).toEqual(['validateToken']); }); });