@optave/codegraph 3.0.0 β†’ 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,8 +208,8 @@ Full agent setup: [AI Agent Guide](docs/guides/ai-agent-guide.md) · [CLAU
208
208
  | πŸ“‹ | **Composite audit** | Single `audit` command combining explain + impact + health metrics per function β€” one call instead of 3-4 |
209
209
  | 🚦 | **Triage queue** | `triage` merges connectivity, hotspots, roles, and complexity into a ranked audit priority queue |
210
210
  | πŸ“¦ | **Batch querying** | Accept a list of targets and return all results in one JSON payload β€” enables multi-agent parallel dispatch |
211
- | πŸ”¬ | **Dataflow analysis** | Track how data moves through functions with `flows_to`, `returns`, and `mutates` edges β€” opt-in via `build --dataflow` (JS/TS) |
212
- | 🧩 | **Control flow graph** | Intraprocedural CFG construction for all 11 languages β€” `cfg` command with text/DOT/Mermaid output, opt-in via `build --cfg` |
211
+ | πŸ”¬ | **Dataflow analysis** | Track how data moves through functions with `flows_to`, `returns`, and `mutates` edges β€” included by default (JS/TS), skip with `--no-dataflow` |
212
+ | 🧩 | **Control flow graph** | Intraprocedural CFG construction for all 11 languages β€” `cfg` command with text/DOT/Mermaid output, included by default, skip with `--no-cfg` |
213
213
  | πŸ”Ž | **AST node querying** | Stored queryable AST nodes (calls, `new`, string, regex, throw, await) β€” `ast` command with SQL GLOB pattern matching |
214
214
  | 🧬 | **Expanded node/edge types** | `parameter`, `property`, `constant` node kinds with `parent_id` for sub-declaration queries; `contains`, `parameter_of`, `receiver` edge kinds |
215
215
  | πŸ“Š | **Exports analysis** | `exports <file>` shows all exported symbols with per-symbol consumers, re-export detection, and counts |
@@ -327,7 +327,7 @@ codegraph ast -k call # Filter by kind: call, new, string, regex
327
327
  codegraph ast -k throw --file src/ # Combine kind and file filters
328
328
  ```
329
329
 
330
- > **Note:** Dataflow requires `codegraph build --dataflow` (JS/TS only). CFG requires `codegraph build --cfg`. Both are opt-in to keep default builds fast.
330
+ > **Note:** Dataflow (JS/TS only) and CFG are included by default. Use `--no-dataflow` / `--no-cfg` for faster builds.
331
331
 
332
332
  ### Audit, Triage & Batch
333
333
 
@@ -552,14 +552,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
552
552
 
553
553
  | Metric | Latest |
554
554
  |---|---|
555
- | Build speed (native) | **1.9 ms/file** |
556
- | Build speed (WASM) | **8.3 ms/file** |
555
+ | Build speed (native) | **4.4 ms/file** |
556
+ | Build speed (WASM) | **13.7 ms/file** |
557
557
  | Query time | **3ms** |
558
558
  | No-op rebuild (native) | **4ms** |
559
- | 1-file rebuild (native) | **124ms** |
560
- | Query: fn-deps | **1.4ms** |
561
- | Query: path | **1.4ms** |
562
- | ~50,000 files (est.) | **~95.0s build** |
559
+ | 1-file rebuild (native) | **325ms** |
560
+ | Query: fn-deps | **0.8ms** |
561
+ | Query: path | **0.8ms** |
562
+ | ~50,000 files (est.) | **~220.0s build** |
563
563
 
564
564
  Metrics are normalized per file for cross-version comparability. Times above are for a full initial build β€” incremental rebuilds only re-parse changed files.
565
565
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optave/codegraph",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Local code graph CLI β€” parse codebases with tree-sitter, build dependency graphs, query them",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -71,10 +71,10 @@
71
71
  },
72
72
  "optionalDependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.0",
74
- "@optave/codegraph-darwin-arm64": "3.0.0",
75
- "@optave/codegraph-darwin-x64": "3.0.0",
76
- "@optave/codegraph-linux-x64-gnu": "3.0.0",
77
- "@optave/codegraph-win32-x64-msvc": "3.0.0"
74
+ "@optave/codegraph-darwin-arm64": "3.0.1",
75
+ "@optave/codegraph-darwin-x64": "3.0.1",
76
+ "@optave/codegraph-linux-x64-gnu": "3.0.1",
77
+ "@optave/codegraph-win32-x64-msvc": "3.0.1"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@biomejs/biome": "^2.4.4",
package/src/ast.js CHANGED
@@ -156,9 +156,8 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
156
156
  return;
157
157
  }
158
158
 
159
- const getNodeId = db.prepare(
160
- 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
161
- );
159
+ // Bulk-fetch all node IDs per file (replaces per-def getNodeId calls)
160
+ const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
162
161
 
163
162
  const tx = db.transaction((rows) => {
164
163
  for (const r of rows) {
@@ -172,14 +171,20 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
172
171
  const rows = [];
173
172
  const defs = symbols.definitions || [];
174
173
 
174
+ // Pre-load all node IDs for this file into a map
175
+ const nodeIdMap = new Map();
176
+ for (const row of bulkGetNodeIds.all(relPath)) {
177
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
178
+ }
179
+
175
180
  // 1. Call nodes from symbols.calls (all languages)
176
181
  if (symbols.calls) {
177
182
  for (const call of symbols.calls) {
178
183
  const parentDef = findParentDef(defs, call.line);
179
184
  let parentNodeId = null;
180
185
  if (parentDef) {
181
- const row = getNodeId.get(parentDef.name, parentDef.kind, relPath, parentDef.line);
182
- if (row) parentNodeId = row.id;
186
+ parentNodeId =
187
+ nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
183
188
  }
184
189
  rows.push({
185
190
  file: relPath,
@@ -195,10 +200,32 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
195
200
 
196
201
  // 2. AST walk for JS/TS/TSX β€” extract new, throw, await, string, regex
197
202
  const ext = path.extname(relPath).toLowerCase();
198
- if (WALK_EXTENSIONS.has(ext) && symbols._tree) {
199
- const astRows = [];
200
- walkAst(symbols._tree.rootNode, defs, relPath, astRows, getNodeId);
201
- rows.push(...astRows);
203
+ if (WALK_EXTENSIONS.has(ext)) {
204
+ if (symbols._tree) {
205
+ // WASM path: walk the tree-sitter AST
206
+ const astRows = [];
207
+ walkAst(symbols._tree.rootNode, defs, relPath, astRows, nodeIdMap);
208
+ rows.push(...astRows);
209
+ } else if (symbols.astNodes?.length) {
210
+ // Native path: use pre-extracted AST nodes from Rust
211
+ for (const n of symbols.astNodes) {
212
+ const parentDef = findParentDef(defs, n.line);
213
+ let parentNodeId = null;
214
+ if (parentDef) {
215
+ parentNodeId =
216
+ nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
217
+ }
218
+ rows.push({
219
+ file: relPath,
220
+ line: n.line,
221
+ kind: n.kind,
222
+ name: n.name,
223
+ text: n.text || null,
224
+ receiver: n.receiver || null,
225
+ parentNodeId,
226
+ });
227
+ }
228
+ }
202
229
  }
203
230
 
204
231
  if (rows.length > 0) {
@@ -213,7 +240,7 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
213
240
  /**
214
241
  * Walk a tree-sitter AST and collect new/throw/await/string/regex nodes.
215
242
  */
216
- function walkAst(node, defs, relPath, rows, getNodeId) {
243
+ function walkAst(node, defs, relPath, rows, nodeIdMap) {
217
244
  const kind = JS_TS_AST_TYPES[node.type];
218
245
  if (kind) {
219
246
  // tree-sitter lines are 0-indexed, our DB uses 1-indexed
@@ -237,7 +264,7 @@ function walkAst(node, defs, relPath, rows, getNodeId) {
237
264
  if (content.length < 2) {
238
265
  // Still recurse children
239
266
  for (let i = 0; i < node.childCount; i++) {
240
- walkAst(node.child(i), defs, relPath, rows, getNodeId);
267
+ walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
241
268
  }
242
269
  return;
243
270
  }
@@ -251,8 +278,7 @@ function walkAst(node, defs, relPath, rows, getNodeId) {
251
278
  const parentDef = findParentDef(defs, line);
252
279
  let parentNodeId = null;
253
280
  if (parentDef) {
254
- const row = getNodeId.get(parentDef.name, parentDef.kind, relPath, parentDef.line);
255
- if (row) parentNodeId = row.id;
281
+ parentNodeId = nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
256
282
  }
257
283
 
258
284
  rows.push({
@@ -271,7 +297,7 @@ function walkAst(node, defs, relPath, rows, getNodeId) {
271
297
  }
272
298
 
273
299
  for (let i = 0; i < node.childCount; i++) {
274
- walkAst(node.child(i), defs, relPath, rows, getNodeId);
300
+ walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
275
301
  }
276
302
  }
277
303
 
package/src/builder.js CHANGED
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { performance } from 'node:perf_hooks';
5
5
  import { loadConfig } from './config.js';
6
6
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
7
- import { closeDb, getBuildMeta, initSchema, openDb, setBuildMeta } from './db.js';
7
+ import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb, setBuildMeta } from './db.js';
8
8
  import { readJournal, writeJournalHeader } from './journal.js';
9
9
  import { debug, info, warn } from './logger.js';
10
10
  import { getActiveEngine, parseFilesAuto } from './parser.js';
@@ -448,17 +448,21 @@ export async function buildGraph(rootDir, opts = {}) {
448
448
  const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
449
449
  info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
450
450
 
451
- // Check for engine/version mismatch β€” auto-promote to full rebuild
451
+ // Check for engine/schema mismatch β€” auto-promote to full rebuild
452
+ // Only trigger on engine change or schema version change (not every patch/minor bump)
453
+ const CURRENT_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
452
454
  let forceFullRebuild = false;
453
455
  if (incremental) {
454
456
  const prevEngine = getBuildMeta(db, 'engine');
455
- const prevVersion = getBuildMeta(db, 'codegraph_version');
456
457
  if (prevEngine && prevEngine !== engineName) {
457
458
  info(`Engine changed (${prevEngine} β†’ ${engineName}), promoting to full rebuild.`);
458
459
  forceFullRebuild = true;
459
460
  }
460
- if (prevVersion && prevVersion !== CODEGRAPH_VERSION) {
461
- info(`Version changed (${prevVersion} β†’ ${CODEGRAPH_VERSION}), promoting to full rebuild.`);
461
+ const prevSchema = getBuildMeta(db, 'schema_version');
462
+ if (prevSchema && Number(prevSchema) !== CURRENT_SCHEMA_VERSION) {
463
+ info(
464
+ `Schema version changed (${prevSchema} β†’ ${CURRENT_SCHEMA_VERSION}), promoting to full rebuild.`,
465
+ );
462
466
  forceFullRebuild = true;
463
467
  }
464
468
  }
@@ -522,9 +526,9 @@ export async function buildGraph(rootDir, opts = {}) {
522
526
  }
523
527
 
524
528
  if (!isFullBuild && parseChanges.length === 0 && removed.length === 0) {
525
- // Check if optional analysis was requested but never computed
529
+ // Check if default analyses were never computed (e.g. legacy DB)
526
530
  const needsCfg =
527
- opts.cfg &&
531
+ opts.cfg !== false &&
528
532
  (() => {
529
533
  try {
530
534
  return db.prepare('SELECT COUNT(*) as c FROM cfg_blocks').get().c === 0;
@@ -533,16 +537,10 @@ export async function buildGraph(rootDir, opts = {}) {
533
537
  }
534
538
  })();
535
539
  const needsDataflow =
536
- opts.dataflow &&
540
+ opts.dataflow !== false &&
537
541
  (() => {
538
542
  try {
539
- return (
540
- db
541
- .prepare(
542
- "SELECT COUNT(*) as c FROM edges WHERE kind IN ('flows_to','returns','mutates')",
543
- )
544
- .get().c === 0
545
- );
543
+ return db.prepare('SELECT COUNT(*) as c FROM dataflow').get().c === 0;
546
544
  } catch {
547
545
  return true;
548
546
  }
@@ -721,44 +719,66 @@ export async function buildGraph(rootDir, opts = {}) {
721
719
  }
722
720
  }
723
721
 
722
+ // Bulk-fetch all node IDs for a file in one query (replaces per-node getNodeId calls)
723
+ const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
724
+
724
725
  const insertAll = db.transaction(() => {
725
726
  for (const [relPath, symbols] of allSymbols) {
726
727
  fileSymbols.set(relPath, symbols);
727
728
 
729
+ // Phase 1: Insert file node + definitions + exports (no children yet)
728
730
  insertNode.run(relPath, 'file', relPath, 0, null, null);
729
- const fileRow = getNodeId.get(relPath, 'file', relPath, 0);
730
731
  for (const def of symbols.definitions) {
731
732
  insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null, null);
732
- const defRow = getNodeId.get(def.name, def.kind, relPath, def.line);
733
+ }
734
+ for (const exp of symbols.exports) {
735
+ insertNode.run(exp.name, exp.kind, relPath, exp.line, null, null);
736
+ }
737
+
738
+ // Phase 2: Bulk-fetch IDs for file + definitions
739
+ const nodeIdMap = new Map();
740
+ for (const row of bulkGetNodeIds.all(relPath)) {
741
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
742
+ }
743
+
744
+ // Phase 3: Insert children with parent_id from the map
745
+ for (const def of symbols.definitions) {
746
+ if (!def.children?.length) continue;
747
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
748
+ if (!defId) continue;
749
+ for (const child of def.children) {
750
+ insertNode.run(child.name, child.kind, relPath, child.line, child.endLine || null, defId);
751
+ }
752
+ }
753
+
754
+ // Phase 4: Re-fetch to include children IDs
755
+ nodeIdMap.clear();
756
+ for (const row of bulkGetNodeIds.all(relPath)) {
757
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
758
+ }
759
+
760
+ // Phase 5: Insert edges using the cached ID map
761
+ const fileId = nodeIdMap.get(`${relPath}|file|0`);
762
+ for (const def of symbols.definitions) {
763
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
733
764
  // File β†’ top-level definition contains edge
734
- if (fileRow && defRow) {
735
- insertEdge.run(fileRow.id, defRow.id, 'contains', 1.0, 0);
765
+ if (fileId && defId) {
766
+ insertEdge.run(fileId, defId, 'contains', 1.0, 0);
736
767
  }
737
- if (def.children?.length && defRow) {
768
+ if (def.children?.length && defId) {
738
769
  for (const child of def.children) {
739
- insertNode.run(
740
- child.name,
741
- child.kind,
742
- relPath,
743
- child.line,
744
- child.endLine || null,
745
- defRow.id,
746
- );
747
- // Parent β†’ child contains edge
748
- const childRow = getNodeId.get(child.name, child.kind, relPath, child.line);
749
- if (childRow) {
750
- insertEdge.run(defRow.id, childRow.id, 'contains', 1.0, 0);
770
+ const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
771
+ if (childId) {
772
+ // Parent β†’ child contains edge
773
+ insertEdge.run(defId, childId, 'contains', 1.0, 0);
751
774
  // Parameter β†’ parent parameter_of edge (inverse direction)
752
775
  if (child.kind === 'parameter') {
753
- insertEdge.run(childRow.id, defRow.id, 'parameter_of', 1.0, 0);
776
+ insertEdge.run(childId, defId, 'parameter_of', 1.0, 0);
754
777
  }
755
778
  }
756
779
  }
757
780
  }
758
781
  }
759
- for (const exp of symbols.exports) {
760
- insertNode.run(exp.name, exp.kind, relPath, exp.line, null, null);
761
- }
762
782
 
763
783
  // Update file hash with real mtime+size for incremental builds
764
784
  // Skip for reverse-dep files β€” they didn't actually change
@@ -1229,7 +1249,9 @@ export async function buildGraph(rootDir, opts = {}) {
1229
1249
  }
1230
1250
  try {
1231
1251
  const { buildStructure } = await import('./structure.js');
1232
- buildStructure(db, fileSymbols, rootDir, lineCountMap, relDirs);
1252
+ // Pass changed file paths so incremental builds can scope the rebuild
1253
+ const changedFilePaths = isFullBuild ? null : [...allSymbols.keys()];
1254
+ buildStructure(db, fileSymbols, rootDir, lineCountMap, relDirs, changedFilePaths);
1233
1255
  } catch (err) {
1234
1256
  debug(`Structure analysis failed: ${err.message}`);
1235
1257
  }
@@ -1250,29 +1272,53 @@ export async function buildGraph(rootDir, opts = {}) {
1250
1272
  }
1251
1273
  _t.rolesMs = performance.now() - _t.roles0;
1252
1274
 
1253
- // Always-on AST node extraction (calls, new, string, regex, throw, await)
1275
+ // For incremental builds, filter out reverse-dep-only files from AST/complexity
1276
+ // β€” their content didn't change, so existing ast_nodes/function_complexity rows are valid.
1277
+ let astComplexitySymbols = allSymbols;
1278
+ if (!isFullBuild) {
1279
+ const reverseDepFiles = new Set(
1280
+ filesToParse.filter((item) => item._reverseDepOnly).map((item) => item.relPath),
1281
+ );
1282
+ if (reverseDepFiles.size > 0) {
1283
+ astComplexitySymbols = new Map();
1284
+ for (const [relPath, symbols] of allSymbols) {
1285
+ if (!reverseDepFiles.has(relPath)) {
1286
+ astComplexitySymbols.set(relPath, symbols);
1287
+ }
1288
+ }
1289
+ debug(
1290
+ `AST/complexity: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
1291
+ );
1292
+ }
1293
+ }
1294
+
1295
+ // AST node extraction (calls, new, string, regex, throw, await)
1254
1296
  // Must run before complexity which releases _tree references
1255
1297
  _t.ast0 = performance.now();
1256
- try {
1257
- const { buildAstNodes } = await import('./ast.js');
1258
- await buildAstNodes(db, allSymbols, rootDir, engineOpts);
1259
- } catch (err) {
1260
- debug(`AST node extraction failed: ${err.message}`);
1298
+ if (opts.ast !== false) {
1299
+ try {
1300
+ const { buildAstNodes } = await import('./ast.js');
1301
+ await buildAstNodes(db, astComplexitySymbols, rootDir, engineOpts);
1302
+ } catch (err) {
1303
+ debug(`AST node extraction failed: ${err.message}`);
1304
+ }
1261
1305
  }
1262
1306
  _t.astMs = performance.now() - _t.ast0;
1263
1307
 
1264
1308
  // Compute per-function complexity metrics (cognitive, cyclomatic, nesting)
1265
1309
  _t.complexity0 = performance.now();
1266
- try {
1267
- const { buildComplexityMetrics } = await import('./complexity.js');
1268
- await buildComplexityMetrics(db, allSymbols, rootDir, engineOpts);
1269
- } catch (err) {
1270
- debug(`Complexity analysis failed: ${err.message}`);
1310
+ if (opts.complexity !== false) {
1311
+ try {
1312
+ const { buildComplexityMetrics } = await import('./complexity.js');
1313
+ await buildComplexityMetrics(db, astComplexitySymbols, rootDir, engineOpts);
1314
+ } catch (err) {
1315
+ debug(`Complexity analysis failed: ${err.message}`);
1316
+ }
1271
1317
  }
1272
1318
  _t.complexityMs = performance.now() - _t.complexity0;
1273
1319
 
1274
- // Opt-in CFG analysis (--cfg)
1275
- if (opts.cfg) {
1320
+ // CFG analysis (skip with --no-cfg)
1321
+ if (opts.cfg !== false) {
1276
1322
  _t.cfg0 = performance.now();
1277
1323
  try {
1278
1324
  const { buildCFGData } = await import('./cfg.js');
@@ -1283,8 +1329,8 @@ export async function buildGraph(rootDir, opts = {}) {
1283
1329
  _t.cfgMs = performance.now() - _t.cfg0;
1284
1330
  }
1285
1331
 
1286
- // Opt-in dataflow analysis (--dataflow)
1287
- if (opts.dataflow) {
1332
+ // Dataflow analysis (skip with --no-dataflow)
1333
+ if (opts.dataflow !== false) {
1288
1334
  _t.dataflow0 = performance.now();
1289
1335
  try {
1290
1336
  const { buildDataflowEdges } = await import('./dataflow.js');
@@ -1348,6 +1394,7 @@ export async function buildGraph(rootDir, opts = {}) {
1348
1394
  engine: engineName,
1349
1395
  engine_version: engineVersion || '',
1350
1396
  codegraph_version: CODEGRAPH_VERSION,
1397
+ schema_version: String(CURRENT_SCHEMA_VERSION),
1351
1398
  built_at: new Date().toISOString(),
1352
1399
  node_count: nodeCount,
1353
1400
  edge_count: actualEdgeCount,
@@ -1385,8 +1432,10 @@ export async function buildGraph(rootDir, opts = {}) {
1385
1432
  edgesMs: +_t.edgesMs.toFixed(1),
1386
1433
  structureMs: +_t.structureMs.toFixed(1),
1387
1434
  rolesMs: +_t.rolesMs.toFixed(1),
1435
+ astMs: +_t.astMs.toFixed(1),
1388
1436
  complexityMs: +_t.complexityMs.toFixed(1),
1389
1437
  ...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
1438
+ ...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
1390
1439
  },
1391
1440
  };
1392
1441
  }
package/src/cfg.js CHANGED
@@ -1241,7 +1241,8 @@ export function cfgData(name, customDbPath, opts = {}) {
1241
1241
  return {
1242
1242
  name,
1243
1243
  results: [],
1244
- warning: 'No CFG data found. Run `codegraph build --cfg` first.',
1244
+ warning:
1245
+ 'No CFG data found. Rebuild with `codegraph build` (CFG is now included by default).',
1245
1246
  };
1246
1247
  }
1247
1248
 
package/src/cli.js CHANGED
@@ -105,13 +105,17 @@ program
105
105
  .command('build [dir]')
106
106
  .description('Parse repo and build graph in .codegraph/graph.db')
107
107
  .option('--no-incremental', 'Force full rebuild (ignore file hashes)')
108
- .option('--dataflow', 'Extract data flow edges (flows_to, returns, mutates)')
109
- .option('--cfg', 'Build intraprocedural control flow graphs')
108
+ .option('--no-ast', 'Skip AST node extraction (calls, new, string, regex, throw, await)')
109
+ .option('--no-complexity', 'Skip complexity metrics computation')
110
+ .option('--no-dataflow', 'Skip data flow edge extraction')
111
+ .option('--no-cfg', 'Skip control flow graph building')
110
112
  .action(async (dir, opts) => {
111
113
  const root = path.resolve(dir || '.');
112
114
  const engine = program.opts().engine;
113
115
  await buildGraph(root, {
114
116
  incremental: opts.incremental,
117
+ ast: opts.ast,
118
+ complexity: opts.complexity,
115
119
  engine,
116
120
  dataflow: opts.dataflow,
117
121
  cfg: opts.cfg,
package/src/dataflow.js CHANGED
@@ -734,7 +734,8 @@ export function dataflowData(name, customDbPath, opts = {}) {
734
734
  return {
735
735
  name,
736
736
  results: [],
737
- warning: 'No dataflow data found. Run `codegraph build --dataflow` first.',
737
+ warning:
738
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
738
739
  };
739
740
  }
740
741
 
@@ -876,7 +877,8 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) {
876
877
  from,
877
878
  to,
878
879
  found: false,
879
- warning: 'No dataflow data found. Run `codegraph build --dataflow` first.',
880
+ warning:
881
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
880
882
  };
881
883
  }
882
884
 
@@ -1005,7 +1007,8 @@ export function dataflowImpactData(name, customDbPath, opts = {}) {
1005
1007
  return {
1006
1008
  name,
1007
1009
  results: [],
1008
- warning: 'No dataflow data found. Run `codegraph build --dataflow` first.',
1010
+ warning:
1011
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1009
1012
  };
1010
1013
  }
1011
1014
 
@@ -170,9 +170,60 @@ function extractSymbolsQuery(tree, query) {
170
170
  }
171
171
  }
172
172
 
173
+ // Extract top-level constants via targeted walk (query patterns don't cover these)
174
+ extractConstantsWalk(tree.rootNode, definitions);
175
+
173
176
  return { definitions, calls, imports, classes, exports: exps };
174
177
  }
175
178
 
179
+ /**
180
+ * Walk program-level children to extract `const x = <literal>` as constants.
181
+ * The query-based fast path has no pattern for lexical_declaration/variable_declaration,
182
+ * so constants are missed. This targeted walk fills that gap without a full tree traversal.
183
+ */
184
+ function extractConstantsWalk(rootNode, definitions) {
185
+ for (let i = 0; i < rootNode.childCount; i++) {
186
+ const node = rootNode.child(i);
187
+ if (!node) continue;
188
+
189
+ let declNode = node;
190
+ // Handle `export const …` β€” unwrap the export_statement to its declaration child
191
+ if (node.type === 'export_statement') {
192
+ const inner = node.childForFieldName('declaration');
193
+ if (!inner) continue;
194
+ declNode = inner;
195
+ }
196
+
197
+ const t = declNode.type;
198
+ if (t !== 'lexical_declaration' && t !== 'variable_declaration') continue;
199
+ if (!declNode.text.startsWith('const ')) continue;
200
+
201
+ for (let j = 0; j < declNode.childCount; j++) {
202
+ const declarator = declNode.child(j);
203
+ if (!declarator || declarator.type !== 'variable_declarator') continue;
204
+ const nameN = declarator.childForFieldName('name');
205
+ const valueN = declarator.childForFieldName('value');
206
+ if (!nameN || nameN.type !== 'identifier' || !valueN) continue;
207
+ // Skip functions β€” already captured by query patterns
208
+ const valType = valueN.type;
209
+ if (
210
+ valType === 'arrow_function' ||
211
+ valType === 'function_expression' ||
212
+ valType === 'function'
213
+ )
214
+ continue;
215
+ if (isConstantValue(valueN)) {
216
+ definitions.push({
217
+ name: nameN.text,
218
+ kind: 'constant',
219
+ line: declNode.startPosition.row + 1,
220
+ endLine: nodeEndLine(declNode),
221
+ });
222
+ }
223
+ }
224
+ }
225
+ }
226
+
176
227
  function handleCommonJSAssignment(left, right, node, imports) {
177
228
  if (!left || !right) return;
178
229
  const leftText = left.text;
package/src/flow.js CHANGED
@@ -45,7 +45,10 @@ export function listEntryPointsData(dbPath, opts = {}) {
45
45
  .prepare(
46
46
  `SELECT n.name, n.kind, n.file, n.line, n.role
47
47
  FROM nodes n
48
- WHERE (${prefixConditions})
48
+ WHERE (
49
+ (${prefixConditions})
50
+ OR n.role = 'entry'
51
+ )
49
52
  AND n.kind NOT IN ('file', 'directory')
50
53
  ORDER BY n.name`,
51
54
  )
@@ -59,7 +62,7 @@ export function listEntryPointsData(dbPath, opts = {}) {
59
62
  file: r.file,
60
63
  line: r.line,
61
64
  role: r.role,
62
- type: entryPointType(r.name),
65
+ type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
63
66
  }));
64
67
 
65
68
  const byType = {};
package/src/index.js CHANGED
@@ -123,7 +123,7 @@ export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from
123
123
  export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
124
124
 
125
125
  // Unified parser API
126
- export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
126
+ export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
127
127
  // Query functions (data-returning)
128
128
  export {
129
129
  ALL_SYMBOL_KINDS,
package/src/mcp.js CHANGED
@@ -638,7 +638,7 @@ const BASE_TOOLS = [
638
638
  },
639
639
  {
640
640
  name: 'cfg',
641
- description: 'Show intraprocedural control flow graph for a function. Requires build --cfg.',
641
+ description: 'Show intraprocedural control flow graph for a function.',
642
642
  inputSchema: {
643
643
  type: 'object',
644
644
  properties: {
@@ -658,7 +658,7 @@ const BASE_TOOLS = [
658
658
  },
659
659
  {
660
660
  name: 'dataflow',
661
- description: 'Show data flow edges or data-dependent blast radius. Requires build --dataflow.',
661
+ description: 'Show data flow edges or data-dependent blast radius.',
662
662
  inputSchema: {
663
663
  type: 'object',
664
664
  properties: {
package/src/parser.js CHANGED
@@ -104,6 +104,15 @@ export function getParser(parsers, filePath) {
104
104
  return parsers.get(entry.id) || null;
105
105
  }
106
106
 
107
+ /**
108
+ * Check whether the required WASM grammar files exist on disk.
109
+ */
110
+ export function isWasmAvailable() {
111
+ return LANGUAGE_REGISTRY.filter((e) => e.required).every((e) =>
112
+ fs.existsSync(grammarPath(e.grammarFile)),
113
+ );
114
+ }
115
+
107
116
  // ── Unified API ──────────────────────────────────────────────────────────────
108
117
 
109
118
  function resolveEngine(opts = {}) {
@@ -183,6 +192,13 @@ function normalizeNativeSymbols(result) {
183
192
  kind: e.kind,
184
193
  line: e.line,
185
194
  })),
195
+ astNodes: (result.astNodes ?? result.ast_nodes ?? []).map((n) => ({
196
+ kind: n.kind,
197
+ name: n.name,
198
+ line: n.line,
199
+ text: n.text ?? null,
200
+ receiver: n.receiver ?? null,
201
+ })),
186
202
  };
187
203
  }
188
204
 
package/src/structure.js CHANGED
@@ -17,7 +17,7 @@ import { isTestFile } from './queries.js';
17
17
  * @param {Map<string, number>} lineCountMap - Map of relPath β†’ line count
18
18
  * @param {Set<string>} directories - Set of relative directory paths
19
19
  */
20
- export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, directories) {
20
+ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, directories, changedFiles) {
21
21
  const insertNode = db.prepare(
22
22
  'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
23
23
  );
@@ -33,15 +33,49 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
33
33
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
34
34
  `);
35
35
 
36
- // Clean previous directory nodes/edges (idempotent rebuild)
37
- // Scope contains-edge delete to directory-sourced edges only,
38
- // preserving symbol-level contains edges (file→def, class→method, etc.)
39
- db.exec(`
40
- DELETE FROM edges WHERE kind = 'contains'
41
- AND source_id IN (SELECT id FROM nodes WHERE kind = 'directory');
42
- DELETE FROM node_metrics;
43
- DELETE FROM nodes WHERE kind = 'directory';
44
- `);
36
+ const isIncremental = changedFiles != null && changedFiles.length > 0;
37
+
38
+ if (isIncremental) {
39
+ // Incremental: only clean up data for changed files and their ancestor directories
40
+ const affectedDirs = new Set();
41
+ for (const f of changedFiles) {
42
+ let d = normalizePath(path.dirname(f));
43
+ while (d && d !== '.') {
44
+ affectedDirs.add(d);
45
+ d = normalizePath(path.dirname(d));
46
+ }
47
+ }
48
+ const deleteContainsForDir = db.prepare(
49
+ "DELETE FROM edges WHERE kind = 'contains' AND source_id IN (SELECT id FROM nodes WHERE name = ? AND kind = 'directory')",
50
+ );
51
+ const deleteMetricForNode = db.prepare('DELETE FROM node_metrics WHERE node_id = ?');
52
+ db.transaction(() => {
53
+ // Delete contains edges only from affected directories
54
+ for (const dir of affectedDirs) {
55
+ deleteContainsForDir.run(dir);
56
+ }
57
+ // Delete metrics for changed files
58
+ for (const f of changedFiles) {
59
+ const fileRow = getNodeId.get(f, 'file', f, 0);
60
+ if (fileRow) deleteMetricForNode.run(fileRow.id);
61
+ }
62
+ // Delete metrics for affected directories
63
+ for (const dir of affectedDirs) {
64
+ const dirRow = getNodeId.get(dir, 'directory', dir, 0);
65
+ if (dirRow) deleteMetricForNode.run(dirRow.id);
66
+ }
67
+ })();
68
+ } else {
69
+ // Full rebuild: clean previous directory nodes/edges (idempotent)
70
+ // Scope contains-edge delete to directory-sourced edges only,
71
+ // preserving symbol-level contains edges (file→def, class→method, etc.)
72
+ db.exec(`
73
+ DELETE FROM edges WHERE kind = 'contains'
74
+ AND source_id IN (SELECT id FROM nodes WHERE kind = 'directory');
75
+ DELETE FROM node_metrics;
76
+ DELETE FROM nodes WHERE kind = 'directory';
77
+ `);
78
+ }
45
79
 
46
80
  // Step 1: Ensure all directories are represented (including intermediate parents)
47
81
  const allDirs = new Set();
@@ -61,7 +95,7 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
61
95
  }
62
96
  }
63
97
 
64
- // Step 2: Insert directory nodes
98
+ // Step 2: Insert directory nodes (INSERT OR IGNORE β€” safe for incremental)
65
99
  const insertDirs = db.transaction(() => {
66
100
  for (const dir of allDirs) {
67
101
  insertNode.run(dir, 'directory', dir, 0, null);
@@ -70,11 +104,28 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
70
104
  insertDirs();
71
105
 
72
106
  // Step 3: Insert 'contains' edges (dir β†’ file, dir β†’ subdirectory)
107
+ // On incremental, only re-insert for affected directories (others are intact)
108
+ const affectedDirs = isIncremental
109
+ ? (() => {
110
+ const dirs = new Set();
111
+ for (const f of changedFiles) {
112
+ let d = normalizePath(path.dirname(f));
113
+ while (d && d !== '.') {
114
+ dirs.add(d);
115
+ d = normalizePath(path.dirname(d));
116
+ }
117
+ }
118
+ return dirs;
119
+ })()
120
+ : null;
121
+
73
122
  const insertContains = db.transaction(() => {
74
123
  // dir β†’ file
75
124
  for (const relPath of fileSymbols.keys()) {
76
125
  const dir = normalizePath(path.dirname(relPath));
77
126
  if (!dir || dir === '.') continue;
127
+ // On incremental, skip dirs whose contains edges are intact
128
+ if (affectedDirs && !affectedDirs.has(dir)) continue;
78
129
  const dirRow = getNodeId.get(dir, 'directory', dir, 0);
79
130
  const fileRow = getNodeId.get(relPath, 'file', relPath, 0);
80
131
  if (dirRow && fileRow) {
@@ -85,6 +136,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
85
136
  for (const dir of allDirs) {
86
137
  const parent = normalizePath(path.dirname(dir));
87
138
  if (!parent || parent === '.' || parent === dir) continue;
139
+ // On incremental, skip parent dirs whose contains edges are intact
140
+ if (affectedDirs && !affectedDirs.has(parent)) continue;
88
141
  const parentRow = getNodeId.get(parent, 'directory', parent, 0);
89
142
  const childRow = getNodeId.get(dir, 'directory', dir, 0);
90
143
  if (parentRow && childRow) {