Skip to content
Open
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
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ All notable changes to this project will be documented in this file. See [commit

* **native:** fix function-scoped `const` declarations being incorrectly extracted as top-level constants ([#344](https://github.com/optave/codegraph/pull/344))


## [3.0.2](https://github.com/optave/codegraph/compare/v3.0.1...v3.0.2) (2026-03-04)

**Dataflow goes multi-language, build performance recovery, and native engine parity fixes.** This patch extends dataflow analysis from JS/TS-only to all 11 supported languages, recovers build performance lost after CFG/dataflow became default-on, fixes language-aware identifier collection in dataflow, and closes a native engine scoping bug for constants.
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
| `boundaries.js` | Architecture boundary rules with onion architecture preset |
| `owners.js` | CODEOWNERS integration for ownership queries |
| `snapshot.js` | SQLite DB backup and restore |
| `sequence.js` | Mermaid sequence diagram generation from call graph edges |
| `paginate.js` | Pagination helpers for bounded query results |
| `logger.js` | Structured logging (`warn`, `debug`, `info`, `error`) |

Expand Down Expand Up @@ -132,6 +133,7 @@ node src/cli.js deps src/<file>.js # File-level imports and importers
node src/cli.js diff-impact main # Impact of current branch vs main
node src/cli.js complexity -T # Per-function complexity metrics
node src/cli.js communities -T # Community detection & drift analysis
node src/cli.js sequence <name> -T # Mermaid sequence diagram from call edges
node src/cli.js check -T # Rule engine pass/fail check
node src/cli.js audit <target> -T # Combined structural summary + impact + health report
node src/cli.js triage -T # Ranked audit priority queue
Expand Down
33 changes: 33 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,39 @@ program
});
});

program
.command('sequence <name>')
.description('Generate a Mermaid sequence diagram from call graph edges (participants = files)')
.option('--depth <n>', 'Max forward traversal depth', '10')
.option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
.option('-d, --db <path>', 'Path to graph.db')
.option('-f, --file <path>', 'Scope to a specific 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)')
.option('-j, --json', 'Output as JSON')
.option('--limit <number>', 'Max results to return')
.option('--offset <number>', 'Skip N results (default: 0)')
.option('--ndjson', 'Newline-delimited JSON output')
.action(async (name, opts) => {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
process.exit(1);
}
const { sequence } = await import('./sequence.js');
sequence(name, opts.db, {
depth: parseInt(opts.depth, 10),
file: opts.file,
kind: opts.kind,
noTests: resolveNoTests(opts),
json: opts.json,
dataflow: opts.dataflow,
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
ndjson: opts.ndjson,
});
});

program
.command('dataflow <name>')
.description('Show data flow for a function: parameters, return consumers, mutations')
Expand Down
73 changes: 3 additions & 70 deletions src/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { openReadonlyOrFail } from './db.js';
import { paginateResult, printNdjson } from './paginate.js';
import { isTestFile, kindIcon } from './queries.js';
import { findMatchingNodes, isTestFile, kindIcon } from './queries.js';
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';

/**
Expand Down Expand Up @@ -95,12 +95,12 @@ export function flowData(name, dbPath, opts = {}) {
const noTests = opts.noTests || false;

// Phase 1: Direct LIKE match on full name
let matchNode = findBestMatch(db, name, opts);
let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;

// Phase 2: Prefix-stripped matching — try adding framework prefixes
if (!matchNode) {
for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
matchNode = findBestMatch(db, `${prefix}${name}`, opts);
matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
if (matchNode) break;
}
}
Expand Down Expand Up @@ -219,73 +219,6 @@ export function flowData(name, dbPath, opts = {}) {
return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
}

/**
* Find the best matching node using the same relevance scoring as queries.js findMatchingNodes.
*/
function findBestMatch(db, name, opts = {}) {
const kinds = opts.kind
? [opts.kind]
: [
'function',
'method',
'class',
'interface',
'type',
'struct',
'enum',
'trait',
'record',
'module',
];
const placeholders = kinds.map(() => '?').join(', ');
const params = [`%${name}%`, ...kinds];

let fileCondition = '';
if (opts.file) {
fileCondition = ' AND n.file LIKE ?';
params.push(`%${opts.file}%`);
}

const rows = db
.prepare(
`SELECT n.*, COALESCE(fi.cnt, 0) AS fan_in
FROM nodes n
LEFT JOIN (
SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY target_id
) fi ON fi.target_id = n.id
WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`,
)
.all(...params);

const noTests = opts.noTests || false;
const nodes = noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;

if (nodes.length === 0) return null;

const lowerQuery = name.toLowerCase();
for (const node of nodes) {
const lowerName = node.name.toLowerCase();
const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName;

let matchScore;
if (lowerName === lowerQuery || bareName === lowerQuery) {
matchScore = 100;
} else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) {
matchScore = 60;
} else if (lowerName.includes(`.${lowerQuery}`) || lowerName.includes(`${lowerQuery}.`)) {
matchScore = 40;
} else {
matchScore = 10;
}

const fanInBonus = Math.min(Math.log2(node.fan_in + 1) * 5, 25);
node._relevance = matchScore + fanInBonus;
}

nodes.sort((a, b) => b._relevance - a._relevance);
return nodes[0];
}

/**
* CLI formatter — text or JSON output.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export { isNativeAvailable } from './native.js';
export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
// Pagination utilities
export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';

// Unified parser API
export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
// Query functions (data-returning)
Expand Down Expand Up @@ -170,6 +169,8 @@ export {
saveRegistry,
unregisterRepo,
} from './registry.js';
// Sequence diagram generation
export { sequence, sequenceData, sequenceToMermaid } from './sequence.js';
// Snapshot management
export {
snapshotDelete,
Expand Down
54 changes: 54 additions & 0 deletions src/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,43 @@ const BASE_TOOLS = [
},
},
},
{
name: 'sequence',
description:
'Generate a Mermaid sequence diagram from call graph edges. Participants are files, messages are function calls between them.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Entry point or function name to trace from (partial match)',
},
depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
format: {
type: 'string',
enum: ['mermaid', 'json'],
description: 'Output format (default: mermaid)',
},
dataflow: {
type: 'boolean',
description: 'Annotate with parameter names and return arrows',
default: false,
},
file: {
type: 'string',
description: 'Scope search to functions in this file (partial match)',
},
kind: {
type: 'string',
enum: EVERY_SYMBOL_KIND,
description: 'Filter to a specific symbol kind',
},
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
...PAGINATION_PROPS,
},
required: ['name'],
},
},
{
name: 'complexity',
description:
Expand Down Expand Up @@ -1165,6 +1202,23 @@ export async function startMCPServer(customDbPath, options = {}) {
}
break;
}
case 'sequence': {
const { sequenceData, sequenceToMermaid } = await import('./sequence.js');
const seqResult = sequenceData(args.name, dbPath, {
depth: args.depth,
file: args.file,
kind: args.kind,
dataflow: args.dataflow,
noTests: args.no_tests,
limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
offset: args.offset ?? 0,
});
result =
args.format === 'json'
? seqResult
: { text: sequenceToMermaid(seqResult), ...seqResult };
break;
}
case 'complexity': {
const { complexityData } = await import('./complexity.js');
result = complexityData(dbPath, {
Expand Down
2 changes: 1 addition & 1 deletion src/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function resolveMethodViaHierarchy(db, methodName) {
* Find nodes matching a name query, ranked by relevance.
* Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
*/
function findMatchingNodes(db, name, opts = {}) {
export function findMatchingNodes(db, name, opts = {}) {
const kinds = opts.kind ? [opts.kind] : FUNCTION_KINDS;
const placeholders = kinds.map(() => '?').join(', ');
const params = [`%${name}%`, ...kinds];
Expand Down
Loading