Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/batch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Batch query orchestration — run the same query command against multiple targets
* and return all results in a single JSON payload.
*
* Designed for multi-agent swarms that need to dispatch 20+ queries in one call.
*/

import { complexityData } from './complexity.js';
import { flowData } from './flow.js';
import {
contextData,
explainData,
fileDepsData,
fnDepsData,
fnImpactData,
impactAnalysisData,
queryNameData,
whereData,
} from './queries.js';

/**
* Map of supported batch commands → their data function + first-arg semantics.
* `sig` describes how the target string is passed to the data function:
* - 'name' → dataFn(target, dbPath, opts)
* - 'target' → dataFn(target, dbPath, opts)
* - 'file' → dataFn(target, dbPath, opts)
* - 'dbOnly' → dataFn(dbPath, { ...opts, target }) (target goes into opts)
*/
export const BATCH_COMMANDS = {
'fn-impact': { fn: fnImpactData, sig: 'name' },
context: { fn: contextData, sig: 'name' },
explain: { fn: explainData, sig: 'target' },
where: { fn: whereData, sig: 'target' },
query: { fn: queryNameData, sig: 'name' },
fn: { fn: fnDepsData, sig: 'name' },
impact: { fn: impactAnalysisData, sig: 'file' },
deps: { fn: fileDepsData, sig: 'file' },
flow: { fn: flowData, sig: 'name' },
complexity: { fn: complexityData, sig: 'dbOnly' },
};

/**
* Run a query command against multiple targets, returning all results.
*
* @param {string} command - One of the keys in BATCH_COMMANDS
* @param {string[]} targets - List of target names/paths
* @param {string} [customDbPath] - Path to graph.db
* @param {object} [opts] - Shared options passed to every invocation
* @returns {{ command: string, total: number, succeeded: number, failed: number, results: object[] }}
*/
export function batchData(command, targets, customDbPath, opts = {}) {
const entry = BATCH_COMMANDS[command];
if (!entry) {
throw new Error(
`Unknown batch command "${command}". Valid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
);
}

const results = [];
let succeeded = 0;
let failed = 0;

for (const target of targets) {
try {
let data;
if (entry.sig === 'dbOnly') {
// complexityData(dbPath, { ...opts, target })
data = entry.fn(customDbPath, { ...opts, target });
} else {
// All other: dataFn(target, dbPath, opts)
data = entry.fn(target, customDbPath, opts);
}
results.push({ target, ok: true, data });
succeeded++;
} catch (err) {
results.push({ target, ok: false, error: err.message });
failed++;
}
}

return { command, total: targets.length, succeeded, failed, results };
}

/**
* CLI wrapper — calls batchData and prints JSON to stdout.
*/
export function batch(command, targets, customDbPath, opts = {}) {
const data = batchData(command, targets, customDbPath, opts);
console.log(JSON.stringify(data, null, 2));
}
52 changes: 52 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { Command } from 'commander';
import { audit } from './audit.js';
import { BATCH_COMMANDS, batch } from './batch.js';
import { buildGraph } from './builder.js';
import { loadConfig } from './config.js';
import { findCycles, formatCycles } from './cycles.js';
Expand Down Expand Up @@ -1150,4 +1151,55 @@ program
}
});

program
.command('batch <command> [targets...]')
.description(
`Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
)
.option('-d, --db <path>', 'Path to graph.db')
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
.option('--stdin', 'Read targets from stdin (JSON array)')
.option('--depth <n>', 'Traversal depth passed to underlying command')
.option('-f, --file <path>', 'Scope to file (partial match)')
.option('-k, --kind <kind>', 'Filter by symbol kind')
.option('-T, --no-tests', 'Exclude test/spec files from results')
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
.action(async (command, positionalTargets, opts) => {
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
process.exit(1);
}

let targets;
if (opts.fromFile) {
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
if (raw.startsWith('[')) {
targets = JSON.parse(raw);
} else {
targets = raw.split(/\r?\n/).filter(Boolean);
}
} else if (opts.stdin) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
} else {
targets = positionalTargets;
}

if (!targets || targets.length === 0) {
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
process.exit(1);
}

const batchOpts = {
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
file: opts.file,
kind: opts.kind,
noTests: resolveNoTests(opts),
};

batch(command, targets, opts.db, batchOpts);
});

program.parse();
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

// Audit (composite report)
export { audit, auditData } from './audit.js';
// Batch querying
export { BATCH_COMMANDS, batch, batchData } from './batch.js';
// Branch comparison
export { branchCompareData, branchCompareMermaid } from './branch-compare.js';
// Graph building
Expand Down
56 changes: 56 additions & 0 deletions src/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,52 @@ const BASE_TOOLS = [
required: ['target'],
},
},
{
name: 'batch_query',
description:
'Run a query command against multiple targets in one call. Returns all results in a single JSON payload — ideal for multi-agent dispatch.',
inputSchema: {
type: 'object',
properties: {
command: {
type: 'string',
enum: [
'fn-impact',
'context',
'explain',
'where',
'query',
'fn',
'impact',
'deps',
'flow',
'complexity',
],
description: 'The query command to run for each target',
},
targets: {
type: 'array',
items: { type: 'string' },
description: 'List of target names (symbol names or file paths depending on command)',
},
depth: {
type: 'number',
description: 'Traversal depth (for fn-impact, context, fn, flow)',
},
file: {
type: 'string',
description: 'Scope to file (partial match)',
},
kind: {
type: 'string',
enum: ALL_SYMBOL_KINDS,
description: 'Filter symbol kind',
},
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
},
required: ['command', 'targets'],
},
},
{
name: 'branch_compare',
description:
Expand Down Expand Up @@ -1035,6 +1081,16 @@ export async function startMCPServer(customDbPath, options = {}) {
});
break;
}
case 'batch_query': {
const { batchData } = await import('./batch.js');
result = batchData(args.command, args.targets, dbPath, {
depth: args.depth,
file: args.file,
kind: args.kind,
noTests: args.no_tests,
});
break;
}
case 'branch_compare': {
const { branchCompareData, branchCompareMermaid } = await import('./branch-compare.js');
const bcData = await branchCompareData(args.base, args.target, {
Expand Down
Loading