@optave/codegraph 3.9.0 → 3.9.2

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.
Files changed (196) hide show
  1. package/README.md +12 -13
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +78 -48
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/cli/commands/batch.d.ts.map +1 -1
  9. package/dist/cli/commands/batch.js +5 -17
  10. package/dist/cli/commands/batch.js.map +1 -1
  11. package/dist/cli/commands/structure.d.ts.map +1 -1
  12. package/dist/cli/commands/structure.js +18 -1
  13. package/dist/cli/commands/structure.js.map +1 -1
  14. package/dist/db/connection.d.ts +3 -0
  15. package/dist/db/connection.d.ts.map +1 -1
  16. package/dist/db/connection.js +24 -6
  17. package/dist/db/connection.js.map +1 -1
  18. package/dist/db/index.d.ts +1 -1
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +1 -1
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/repository/base.d.ts +35 -0
  23. package/dist/db/repository/base.d.ts.map +1 -1
  24. package/dist/db/repository/base.js +8 -0
  25. package/dist/db/repository/base.js.map +1 -1
  26. package/dist/db/repository/index.d.ts +1 -0
  27. package/dist/db/repository/index.d.ts.map +1 -1
  28. package/dist/db/repository/index.js.map +1 -1
  29. package/dist/db/repository/native-repository.d.ts +7 -1
  30. package/dist/db/repository/native-repository.d.ts.map +1 -1
  31. package/dist/db/repository/native-repository.js +46 -1
  32. package/dist/db/repository/native-repository.js.map +1 -1
  33. package/dist/domain/analysis/context.d.ts.map +1 -1
  34. package/dist/domain/analysis/context.js +5 -15
  35. package/dist/domain/analysis/context.js.map +1 -1
  36. package/dist/domain/analysis/dependencies.d.ts +6 -33
  37. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  38. package/dist/domain/analysis/dependencies.js +18 -16
  39. package/dist/domain/analysis/dependencies.js.map +1 -1
  40. package/dist/domain/analysis/fn-impact.js +2 -2
  41. package/dist/domain/analysis/fn-impact.js.map +1 -1
  42. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  43. package/dist/domain/analysis/implementations.js +3 -13
  44. package/dist/domain/analysis/implementations.js.map +1 -1
  45. package/dist/domain/graph/builder/context.d.ts +4 -0
  46. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/context.js +4 -0
  48. package/dist/domain/graph/builder/context.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +18 -0
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/native-db-proxy.d.ts +24 -0
  53. package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -0
  54. package/dist/domain/graph/builder/native-db-proxy.js +87 -0
  55. package/dist/domain/graph/builder/native-db-proxy.js.map +1 -0
  56. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/pipeline.js +410 -349
  58. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/stages/build-edges.js +44 -4
  61. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-structure.js +2 -2
  63. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/detect-changes.js +6 -28
  66. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/finalize.js +1 -1
  68. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  69. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  70. package/dist/domain/graph/builder/stages/insert-nodes.js +16 -12
  71. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  72. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  73. package/dist/domain/graph/builder/stages/resolve-imports.js +21 -26
  74. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  75. package/dist/domain/graph/watcher.d.ts.map +1 -1
  76. package/dist/domain/graph/watcher.js +99 -95
  77. package/dist/domain/graph/watcher.js.map +1 -1
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +7 -2
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/queries.d.ts +1 -1
  82. package/dist/domain/queries.d.ts.map +1 -1
  83. package/dist/domain/queries.js +1 -1
  84. package/dist/domain/queries.js.map +1 -1
  85. package/dist/extractors/go.js +53 -35
  86. package/dist/extractors/go.js.map +1 -1
  87. package/dist/extractors/javascript.js +66 -27
  88. package/dist/extractors/javascript.js.map +1 -1
  89. package/dist/features/audit.d.ts.map +1 -1
  90. package/dist/features/audit.js +3 -2
  91. package/dist/features/audit.js.map +1 -1
  92. package/dist/features/boundaries.d.ts.map +1 -1
  93. package/dist/features/boundaries.js +3 -5
  94. package/dist/features/boundaries.js.map +1 -1
  95. package/dist/features/branch-compare.d.ts.map +1 -1
  96. package/dist/features/branch-compare.js +2 -1
  97. package/dist/features/branch-compare.js.map +1 -1
  98. package/dist/features/complexity.d.ts.map +1 -1
  99. package/dist/features/complexity.js +78 -58
  100. package/dist/features/complexity.js.map +1 -1
  101. package/dist/features/dataflow.d.ts.map +1 -1
  102. package/dist/features/dataflow.js +109 -118
  103. package/dist/features/dataflow.js.map +1 -1
  104. package/dist/features/flow.d.ts.map +1 -1
  105. package/dist/features/flow.js +2 -1
  106. package/dist/features/flow.js.map +1 -1
  107. package/dist/features/manifesto.d.ts.map +1 -1
  108. package/dist/features/manifesto.js +15 -1
  109. package/dist/features/manifesto.js.map +1 -1
  110. package/dist/features/structure.d.ts.map +1 -1
  111. package/dist/features/structure.js +147 -97
  112. package/dist/features/structure.js.map +1 -1
  113. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  114. package/dist/graph/algorithms/louvain.js +4 -2
  115. package/dist/graph/algorithms/louvain.js.map +1 -1
  116. package/dist/graph/classifiers/roles.d.ts +2 -0
  117. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  118. package/dist/graph/classifiers/roles.js +13 -5
  119. package/dist/graph/classifiers/roles.js.map +1 -1
  120. package/dist/infrastructure/config.d.ts +1 -0
  121. package/dist/infrastructure/config.d.ts.map +1 -1
  122. package/dist/infrastructure/config.js +1 -0
  123. package/dist/infrastructure/config.js.map +1 -1
  124. package/dist/presentation/batch.d.ts.map +1 -1
  125. package/dist/presentation/batch.js +1 -0
  126. package/dist/presentation/batch.js.map +1 -1
  127. package/dist/presentation/communities.d.ts.map +1 -1
  128. package/dist/presentation/communities.js +38 -34
  129. package/dist/presentation/communities.js.map +1 -1
  130. package/dist/presentation/manifesto.d.ts.map +1 -1
  131. package/dist/presentation/manifesto.js +31 -33
  132. package/dist/presentation/manifesto.js.map +1 -1
  133. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  134. package/dist/presentation/queries-cli/inspect.js +47 -46
  135. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  136. package/dist/presentation/structure.d.ts +1 -1
  137. package/dist/presentation/structure.d.ts.map +1 -1
  138. package/dist/presentation/structure.js +1 -1
  139. package/dist/presentation/structure.js.map +1 -1
  140. package/dist/shared/file-utils.d.ts.map +1 -1
  141. package/dist/shared/file-utils.js +94 -72
  142. package/dist/shared/file-utils.js.map +1 -1
  143. package/dist/shared/normalize.d.ts +12 -0
  144. package/dist/shared/normalize.d.ts.map +1 -1
  145. package/dist/shared/normalize.js +4 -0
  146. package/dist/shared/normalize.js.map +1 -1
  147. package/dist/types.d.ts +82 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/package.json +7 -7
  150. package/src/ast-analysis/engine.ts +99 -55
  151. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  152. package/src/cli/commands/batch.ts +5 -26
  153. package/src/cli/commands/structure.ts +21 -1
  154. package/src/db/connection.ts +26 -7
  155. package/src/db/index.ts +2 -0
  156. package/src/db/repository/base.ts +43 -0
  157. package/src/db/repository/index.ts +1 -0
  158. package/src/db/repository/native-repository.ts +67 -1
  159. package/src/domain/analysis/context.ts +5 -15
  160. package/src/domain/analysis/dependencies.ts +19 -16
  161. package/src/domain/analysis/fn-impact.ts +2 -2
  162. package/src/domain/analysis/implementations.ts +3 -13
  163. package/src/domain/graph/builder/context.ts +4 -0
  164. package/src/domain/graph/builder/incremental.ts +21 -0
  165. package/src/domain/graph/builder/native-db-proxy.ts +98 -0
  166. package/src/domain/graph/builder/pipeline.ts +514 -416
  167. package/src/domain/graph/builder/stages/build-edges.ts +45 -3
  168. package/src/domain/graph/builder/stages/build-structure.ts +2 -2
  169. package/src/domain/graph/builder/stages/detect-changes.ts +11 -33
  170. package/src/domain/graph/builder/stages/finalize.ts +1 -1
  171. package/src/domain/graph/builder/stages/insert-nodes.ts +17 -14
  172. package/src/domain/graph/builder/stages/resolve-imports.ts +22 -23
  173. package/src/domain/graph/watcher.ts +118 -98
  174. package/src/domain/parser.ts +8 -2
  175. package/src/domain/queries.ts +1 -1
  176. package/src/extractors/go.ts +57 -32
  177. package/src/extractors/javascript.ts +67 -27
  178. package/src/features/audit.ts +3 -2
  179. package/src/features/boundaries.ts +3 -5
  180. package/src/features/branch-compare.ts +2 -3
  181. package/src/features/complexity.ts +94 -58
  182. package/src/features/dataflow.ts +153 -132
  183. package/src/features/flow.ts +2 -1
  184. package/src/features/manifesto.ts +15 -1
  185. package/src/features/structure.ts +167 -95
  186. package/src/graph/algorithms/louvain.ts +5 -2
  187. package/src/graph/classifiers/roles.ts +14 -5
  188. package/src/infrastructure/config.ts +1 -0
  189. package/src/presentation/batch.ts +1 -0
  190. package/src/presentation/communities.ts +44 -39
  191. package/src/presentation/manifesto.ts +35 -38
  192. package/src/presentation/queries-cli/inspect.ts +48 -46
  193. package/src/presentation/structure.ts +2 -2
  194. package/src/shared/file-utils.ts +116 -77
  195. package/src/shared/normalize.ts +10 -0
  196. package/src/types.ts +86 -0
@@ -119,6 +119,23 @@ function buildImportEdges(
119
119
  : 'imports';
120
120
  allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
121
121
 
122
+ // Type-only imports: create symbol-level edges so the target symbols
123
+ // get fan-in credit and aren't falsely classified as dead code.
124
+ if (imp.typeOnly && ctx.nodesByNameAndFile) {
125
+ for (const name of imp.names) {
126
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
127
+ let targetFile = resolvedPath;
128
+ if (isBarrelFile(ctx, resolvedPath)) {
129
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
130
+ if (actual) targetFile = actual;
131
+ }
132
+ const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
133
+ if (candidates && candidates.length > 0) {
134
+ allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
135
+ }
136
+ }
137
+ }
138
+
122
139
  if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
123
140
  buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
124
141
  }
@@ -280,7 +297,18 @@ function buildImportEdgesNative(
280
297
  }
281
298
  }
282
299
 
283
- // 6. Call native
300
+ // 6. Build symbol node entries for type-only import resolution
301
+ const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
302
+ if (ctx.nodesByNameAndFile) {
303
+ for (const [key, nodes] of ctx.nodesByNameAndFile) {
304
+ if (nodes.length > 0) {
305
+ const [name, file] = key.split('|');
306
+ symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
307
+ }
308
+ }
309
+ }
310
+
311
+ // 7. Call native
284
312
  const nativeEdges = native.buildImportEdges!(
285
313
  files,
286
314
  resolvedImports,
@@ -288,6 +316,7 @@ function buildImportEdgesNative(
288
316
  fileNodeIds,
289
317
  barrelFiles,
290
318
  rootDir,
319
+ symbolNodes,
291
320
  ) as NativeEdge[];
292
321
 
293
322
  for (const e of nativeEdges) {
@@ -313,7 +342,7 @@ function buildCallEdgesNative(
313
342
  if (!fileNodeRow) continue;
314
343
 
315
344
  const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir);
316
- const typeMap: Array<{ name: string; typeName: string; confidence: number }> =
345
+ const typeMapRaw: Array<{ name: string; typeName: string; confidence: number }> =
317
346
  symbols.typeMap instanceof Map
318
347
  ? [...symbols.typeMap.entries()].map(([name, entry]) => ({
319
348
  name,
@@ -323,6 +352,19 @@ function buildCallEdgesNative(
323
352
  : Array.isArray(symbols.typeMap)
324
353
  ? (symbols.typeMap as Array<{ name: string; typeName: string; confidence: number }>)
325
354
  : [];
355
+ // Deduplicate: keep highest-confidence entry per name (first-wins on tie),
356
+ // matching JS setTypeMapEntry semantics. The Map branch is already
357
+ // deduped by setTypeMapEntry — this loop is only needed for the Array
358
+ // branch (pre-rebuilt native addon) but runs unconditionally as
359
+ // belt-and-suspenders since it's a cheap O(n) pass.
360
+ const typeMapDedup = new Map<string, { name: string; typeName: string; confidence: number }>();
361
+ for (const entry of typeMapRaw) {
362
+ const existing = typeMapDedup.get(entry.name);
363
+ if (!existing || entry.confidence > existing.confidence) {
364
+ typeMapDedup.set(entry.name, entry);
365
+ }
366
+ }
367
+ const typeMap = [...typeMapDedup.values()];
326
368
  nativeFiles.push({
327
369
  file: relPath,
328
370
  fileNodeId: fileNodeRow.id,
@@ -670,7 +712,7 @@ function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolea
670
712
  const nodeKindFilter = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
671
713
 
672
714
  // Gate: only scope for small incremental on large codebases
673
- if (!isFullBuild && fileSymbols.size <= 5) {
715
+ if (!isFullBuild && fileSymbols.size <= ctx.config.build.smallFilesThreshold) {
674
716
  const existingFileCount = (
675
717
  db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get() as { c: number }
676
718
  ).c;
@@ -37,7 +37,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
37
37
  // For small incremental builds on large codebases, use a fast path that
38
38
  // updates only the changed files' metrics via targeted SQL instead of
39
39
  // loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
40
- // Gate: ≤5 changed files AND significantly more existing files (>20) to
40
+ // Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
41
41
  // avoid triggering on small test fixtures where directory metrics matter.
42
42
  const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
43
43
  const existingFileCount = !isFullBuild
@@ -52,7 +52,7 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
52
52
  const useSmallIncrementalFastPath =
53
53
  !isFullBuild &&
54
54
  changedFileList != null &&
55
- changedFileList.length <= 5 &&
55
+ changedFileList.length <= ctx.config.build.smallFilesThreshold &&
56
56
  existingFileCount > 20;
57
57
 
58
58
  if (!isFullBuild && !useSmallIncrementalFastPath) {
@@ -58,26 +58,9 @@ function getChangedFiles(
58
58
  db: BetterSqlite3Database,
59
59
  allFiles: string[],
60
60
  rootDir: string,
61
- nativeDb?: NativeDatabase,
62
61
  ): ChangeResult {
63
- // Batched native path: single napi call for table check + all rows + max mtime
64
- if (nativeDb?.getFileHashData) {
65
- const data = nativeDb.getFileHashData();
66
- if (!data.exists) {
67
- return {
68
- changed: allFiles.map((f) => ({ file: f })),
69
- removed: [],
70
- isFullBuild: true,
71
- };
72
- }
73
- const existing = new Map<string, FileHashRow>(data.rows.map((r) => [r.file, r]));
74
- const removed = detectRemovedFiles(existing, allFiles, rootDir);
75
- const journalResult = tryJournalTier(db, existing, rootDir, removed, data.maxMtime);
76
- if (journalResult) return journalResult;
77
- return mtimeAndHashTiers(existing, allFiles, rootDir, removed);
78
- }
79
-
80
- // WASM / fallback path
62
+ // NativeDatabase is not open during change detection (deferred to after
63
+ // early-exit check). All queries use better-sqlite3 here.
81
64
  let hasTable = false;
82
65
  try {
83
66
  db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
@@ -294,14 +277,14 @@ async function runPendingAnalysis(ctx: PipelineContext): Promise<boolean> {
294
277
  rootDir,
295
278
  analysisOpts,
296
279
  );
297
- if (needsCfg) {
298
- const { buildCFGData } = await import('../../../../features/cfg.js');
299
- await buildCFGData(db, analysisSymbols, rootDir, engineOpts);
300
- }
301
- if (needsDataflow) {
302
- const { buildDataflowEdges } = await import('../../../../features/dataflow.js');
303
- await buildDataflowEdges(db, analysisSymbols, rootDir, engineOpts);
304
- }
280
+ const { runAnalyses } = await import('../../../../ast-analysis/engine.js');
281
+ await runAnalyses(
282
+ db,
283
+ analysisSymbols,
284
+ rootDir,
285
+ { ast: false, complexity: false, cfg: needsCfg, dataflow: needsDataflow },
286
+ engineOpts,
287
+ );
305
288
  return true;
306
289
  }
307
290
 
@@ -487,12 +470,7 @@ export async function detectChanges(ctx: PipelineContext): Promise<void> {
487
470
  }
488
471
  const increResult =
489
472
  incremental && !forceFullRebuild
490
- ? getChangedFiles(
491
- db,
492
- allFiles,
493
- rootDir,
494
- ctx.engineName === 'native' ? ctx.nativeDb : undefined,
495
- )
473
+ ? getChangedFiles(db, allFiles, rootDir)
496
474
  : {
497
475
  changed: allFiles.map((f): ChangedFile => ({ file: f })),
498
476
  removed: [] as string[],
@@ -258,7 +258,7 @@ export async function finalize(ctx: PipelineContext): Promise<void> {
258
258
  // immediately after build.
259
259
  const pair = { db: ctx.db, nativeDb: ctx.nativeDb };
260
260
  const isTempDir = path.resolve(rootDir).startsWith(path.resolve(tmpdir()));
261
- if (!isFullBuild && allSymbols.size <= 5 && !isTempDir) {
261
+ if (!isFullBuild && allSymbols.size <= ctx.config.build.smallFilesThreshold && !isTempDir) {
262
262
  closeDbPairDeferred(pair);
263
263
  } else {
264
264
  closeDbPair(pair);
@@ -159,23 +159,26 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
159
159
  }
160
160
  const fileHashes = buildFileHashes(allSymbols, precomputedData, metadataUpdates, rootDir);
161
161
 
162
- // WAL guard: same suspendJsDb/resumeJsDb pattern used by feature modules
163
- // (ast, cfg, complexity, dataflow). Checkpoint JS side before native write,
164
- // then checkpoint native side after, so neither library reads WAL frames
165
- // written by the other (#696, #709, #715, #717).
162
+ // In native-first mode (single rusqlite connection), no WAL dance is needed.
163
+ // In dual-connection mode, checkpoint JS side before native write, then
164
+ // checkpoint native side after (#696, #709, #715, #717).
166
165
  let result: boolean;
167
- try {
168
- if (ctx.db) {
169
- ctx.db.pragma('wal_checkpoint(TRUNCATE)');
170
- }
166
+ if (ctx.nativeFirstProxy) {
171
167
  result = ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
172
- } finally {
168
+ } else {
173
169
  try {
174
- ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
175
- } catch (e) {
176
- debug(
177
- `tryNativeInsert: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
178
- );
170
+ if (ctx.db) {
171
+ ctx.db.pragma('wal_checkpoint(TRUNCATE)');
172
+ }
173
+ result = ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed);
174
+ } finally {
175
+ try {
176
+ ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
177
+ } catch (e) {
178
+ debug(
179
+ `tryNativeInsert: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
180
+ );
181
+ }
179
182
  }
180
183
  }
181
184
  return result;
@@ -34,15 +34,14 @@ function buildReexportMap(ctx: PipelineContext): void {
34
34
 
35
35
  /**
36
36
  * Find barrel files related to changed files for scoped re-parsing.
37
- * For small incremental builds (<=5 files), only barrels that re-export from
37
+ * For small incremental builds (<=smallFilesThreshold files), only barrels that re-export from
38
38
  * or are imported by the changed files. For larger changes, all barrels.
39
39
  */
40
40
  function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
41
41
  const { db, fileSymbols, rootDir, aliases } = ctx;
42
42
  const changedRelPaths = new Set<string>(fileSymbols.keys());
43
43
 
44
- const SMALL_CHANGE_THRESHOLD = 5;
45
- if (changedRelPaths.size <= SMALL_CHANGE_THRESHOLD) {
44
+ if (changedRelPaths.size <= ctx.config.build.smallFilesThreshold) {
46
45
  const allBarrelFiles = new Set(
47
46
  (
48
47
  db
@@ -180,6 +179,13 @@ export function isBarrelFile(ctx: PipelineContext, relPath: string): boolean {
180
179
  return reexports.length >= ownDefs;
181
180
  }
182
181
 
182
+ /** Check if a re-export source directly defines the symbol. */
183
+ function sourceDefinesSymbol(ctx: PipelineContext, source: string, symbolName: string): boolean {
184
+ const targetSymbols = ctx.fileSymbols.get(source);
185
+ if (!targetSymbols) return false;
186
+ return targetSymbols.definitions.some((d) => d.name === symbolName);
187
+ }
188
+
183
189
  export function resolveBarrelExport(
184
190
  ctx: PipelineContext,
185
191
  barrelPath: string,
@@ -188,31 +194,24 @@ export function resolveBarrelExport(
188
194
  ): string | null {
189
195
  if (visited.has(barrelPath)) return null;
190
196
  visited.add(barrelPath);
197
+
191
198
  const reexports = ctx.reexportMap.get(barrelPath) as ReexportEntry[] | undefined;
192
199
  if (!reexports) return null;
200
+
193
201
  for (const re of reexports) {
202
+ // Named re-export: only follow if the symbol is in the export list
194
203
  if (re.names.length > 0 && !re.wildcardReexport) {
195
- if (re.names.includes(symbolName)) {
196
- const targetSymbols = ctx.fileSymbols.get(re.source);
197
- if (targetSymbols) {
198
- const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
199
- if (hasDef) return re.source;
200
- const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
201
- if (deeper) return deeper;
202
- }
203
- return re.source;
204
- }
205
- continue;
206
- }
207
- if (re.wildcardReexport || re.names.length === 0) {
208
- const targetSymbols = ctx.fileSymbols.get(re.source);
209
- if (targetSymbols) {
210
- const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
211
- if (hasDef) return re.source;
212
- const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
213
- if (deeper) return deeper;
214
- }
204
+ if (!re.names.includes(symbolName)) continue;
205
+ if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
206
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
207
+ return deeper ?? re.source;
215
208
  }
209
+
210
+ // Wildcard or namespace re-export: check if target defines the symbol
211
+ if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
212
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
213
+ if (deeper) return deeper;
216
214
  }
215
+
217
216
  return null;
218
217
  }
@@ -2,20 +2,16 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
4
4
  import { debug, info } from '../../infrastructure/logger.js';
5
- import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../shared/constants.js';
5
+ import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
6
6
  import { DbError } from '../../shared/errors.js';
7
7
  import { createParseTreeCache, getActiveEngine } from '../parser.js';
8
8
  import { type IncrementalStmts, rebuildFile } from './builder/incremental.js';
9
9
  import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
10
10
  import { appendJournalEntries } from './journal.js';
11
11
 
12
- function shouldIgnore(filePath: string): boolean {
12
+ function shouldIgnorePath(filePath: string): boolean {
13
13
  const parts = filePath.split(path.sep);
14
- return parts.some((p) => IGNORE_DIRS.has(p));
15
- }
16
-
17
- function isTrackedExt(filePath: string): boolean {
18
- return EXTENSIONS.has(path.extname(filePath));
14
+ return parts.some((p) => shouldIgnore(p));
19
15
  }
20
16
 
21
17
  /** Prepare all SQL statements needed by the watcher's incremental rebuild. */
@@ -141,24 +137,35 @@ function collectTrackedFiles(dir: string, result: string[]): void {
141
137
  let entries: fs.Dirent[];
142
138
  try {
143
139
  entries = fs.readdirSync(dir, { withFileTypes: true });
144
- } catch {
140
+ } catch (e: unknown) {
141
+ debug(`collectTrackedFiles: cannot read ${dir}: ${(e as Error).message}`);
145
142
  return;
146
143
  }
147
144
  for (const entry of entries) {
148
- if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
145
+ if (shouldIgnore(entry.name)) continue;
149
146
  const full = path.join(dir, entry.name);
150
147
  if (entry.isDirectory()) {
151
148
  collectTrackedFiles(full, result);
152
- } else if (EXTENSIONS.has(path.extname(entry.name))) {
149
+ } else if (isSupportedFile(entry.name)) {
153
150
  result.push(full);
154
151
  }
155
152
  }
156
153
  }
157
154
 
158
- export async function watchProject(
159
- rootDir: string,
160
- opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
161
- ): Promise<void> {
155
+ /** Shared watcher state passed between setup and watcher sub-functions. */
156
+ interface WatcherContext {
157
+ rootDir: string;
158
+ db: ReturnType<typeof openDb>;
159
+ stmts: IncrementalStmts;
160
+ engineOpts: import('../../types.js').EngineOpts;
161
+ cache: ReturnType<typeof createParseTreeCache>;
162
+ pending: Set<string>;
163
+ timer: ReturnType<typeof setTimeout> | null;
164
+ debounceMs: number;
165
+ }
166
+
167
+ /** Initialize DB, engine, cache, and statements for watch mode. */
168
+ function setupWatcher(rootDir: string, opts: { engine?: string }): WatcherContext {
162
169
  const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
163
170
  if (!fs.existsSync(dbPath)) {
164
171
  throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
@@ -183,111 +190,124 @@ export async function watchProject(
183
190
 
184
191
  const stmts = prepareWatcherStatements(db);
185
192
 
186
- const pending = new Set<string>();
187
- let timer: ReturnType<typeof setTimeout> | null = null;
188
- const DEBOUNCE_MS = 300;
189
-
190
- const usePoll = opts.poll ?? process.platform === 'win32';
191
- const POLL_INTERVAL_MS = opts.pollInterval ?? 2000;
193
+ return {
194
+ rootDir,
195
+ db,
196
+ stmts,
197
+ engineOpts,
198
+ cache,
199
+ pending: new Set<string>(),
200
+ timer: null,
201
+ debounceMs: 300,
202
+ };
203
+ }
192
204
 
193
- info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
194
- info('Press Ctrl+C to stop.');
205
+ /** Schedule debounced processing of pending files. */
206
+ function scheduleDebouncedProcess(ctx: WatcherContext): void {
207
+ if (ctx.timer) clearTimeout(ctx.timer);
208
+ ctx.timer = setTimeout(async () => {
209
+ const files = [...ctx.pending];
210
+ ctx.pending.clear();
211
+ await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
212
+ }, ctx.debounceMs);
213
+ }
195
214
 
196
- let cleanup: () => void;
215
+ /** Start polling-based file watcher. Returns cleanup function. */
216
+ function startPollingWatcher(ctx: WatcherContext, pollIntervalMs: number): () => void {
217
+ const mtimeMap = new Map<string, number>();
218
+
219
+ const initial: string[] = [];
220
+ collectTrackedFiles(ctx.rootDir, initial);
221
+ for (const f of initial) {
222
+ try {
223
+ mtimeMap.set(f, fs.statSync(f).mtimeMs);
224
+ } catch {
225
+ /* deleted between collect and stat */
226
+ }
227
+ }
228
+ info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
197
229
 
198
- if (usePoll) {
199
- // Polling mode: avoids native OS file watchers (NtNotifyChangeDirectoryFileEx)
200
- // which can crash ReFS drivers on Windows Dev Drives.
201
- const mtimeMap = new Map<string, number>();
230
+ const pollTimer = setInterval(() => {
231
+ const current: string[] = [];
232
+ collectTrackedFiles(ctx.rootDir, current);
233
+ const currentSet = new Set(current);
202
234
 
203
- // Seed initial mtimes
204
- const initial: string[] = [];
205
- collectTrackedFiles(rootDir, initial);
206
- for (const f of initial) {
235
+ for (const f of current) {
207
236
  try {
208
- mtimeMap.set(f, fs.statSync(f).mtimeMs);
237
+ const mtime = fs.statSync(f).mtimeMs;
238
+ const prev = mtimeMap.get(f);
239
+ if (prev === undefined || mtime !== prev) {
240
+ mtimeMap.set(f, mtime);
241
+ ctx.pending.add(f);
242
+ }
209
243
  } catch {
210
244
  /* deleted between collect and stat */
211
245
  }
212
246
  }
213
- info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
214
-
215
- const pollTimer = setInterval(() => {
216
- const current: string[] = [];
217
- collectTrackedFiles(rootDir, current);
218
- const currentSet = new Set(current);
219
-
220
- // Detect modified or new files
221
- for (const f of current) {
222
- try {
223
- const mtime = fs.statSync(f).mtimeMs;
224
- const prev = mtimeMap.get(f);
225
- if (prev === undefined || mtime !== prev) {
226
- mtimeMap.set(f, mtime);
227
- pending.add(f);
228
- }
229
- } catch {
230
- /* deleted between collect and stat */
231
- }
232
- }
233
247
 
234
- // Detect deleted files
235
- for (const f of mtimeMap.keys()) {
236
- if (!currentSet.has(f)) {
237
- mtimeMap.delete(f);
238
- pending.add(f);
239
- }
248
+ for (const f of mtimeMap.keys()) {
249
+ if (!currentSet.has(f)) {
250
+ mtimeMap.delete(f);
251
+ ctx.pending.add(f);
240
252
  }
253
+ }
241
254
 
242
- if (pending.size > 0) {
243
- if (timer) clearTimeout(timer);
244
- timer = setTimeout(async () => {
245
- const files = [...pending];
246
- pending.clear();
247
- await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
248
- }, DEBOUNCE_MS);
249
- }
250
- }, POLL_INTERVAL_MS);
251
-
252
- cleanup = () => clearInterval(pollTimer);
253
- } else {
254
- // Native OS watcher — efficient but can trigger ReFS crashes on Windows Dev Drives.
255
- // Use --poll if you experience BSOD/HYPERVISOR_ERROR on ReFS volumes.
256
- const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
257
- if (!filename) return;
258
- if (shouldIgnore(filename)) return;
259
- if (!isTrackedExt(filename)) return;
260
-
261
- const fullPath = path.join(rootDir, filename);
262
- pending.add(fullPath);
263
-
264
- if (timer) clearTimeout(timer);
265
- timer = setTimeout(async () => {
266
- const files = [...pending];
267
- pending.clear();
268
- await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
269
- }, DEBOUNCE_MS);
270
- });
271
-
272
- cleanup = () => watcher.close();
273
- }
255
+ if (ctx.pending.size > 0) {
256
+ scheduleDebouncedProcess(ctx);
257
+ }
258
+ }, pollIntervalMs);
259
+
260
+ return () => clearInterval(pollTimer);
261
+ }
262
+
263
+ /** Start native OS file watcher. Returns cleanup function. */
264
+ function startNativeWatcher(ctx: WatcherContext): () => void {
265
+ const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
266
+ if (!filename) return;
267
+ if (shouldIgnorePath(filename)) return;
268
+ if (!isSupportedFile(filename)) return;
274
269
 
275
- process.on('SIGINT', () => {
270
+ ctx.pending.add(path.join(ctx.rootDir, filename));
271
+ scheduleDebouncedProcess(ctx);
272
+ });
273
+
274
+ return () => watcher.close();
275
+ }
276
+
277
+ /** Register SIGINT handler to flush journal and clean up. */
278
+ function setupShutdownHandler(ctx: WatcherContext, cleanup: () => void): void {
279
+ process.once('SIGINT', () => {
276
280
  info('Stopping watcher...');
277
281
  cleanup();
278
- // Flush any pending file paths to journal before exit
279
- if (pending.size > 0) {
280
- const entries = [...pending].map((filePath) => ({
281
- file: normalizePath(path.relative(rootDir, filePath)),
282
+ if (ctx.pending.size > 0) {
283
+ const entries = [...ctx.pending].map((filePath) => ({
284
+ file: normalizePath(path.relative(ctx.rootDir, filePath)),
282
285
  }));
283
286
  try {
284
- appendJournalEntries(rootDir, entries);
287
+ appendJournalEntries(ctx.rootDir, entries);
285
288
  } catch (e: unknown) {
286
289
  debug(`Journal flush on exit failed (non-fatal): ${(e as Error).message}`);
287
290
  }
288
291
  }
289
- if (cache) cache.clear();
290
- closeDb(db);
292
+ if (ctx.cache) ctx.cache.clear();
293
+ closeDb(ctx.db);
291
294
  process.exit(0);
292
295
  });
293
296
  }
297
+
298
+ export async function watchProject(
299
+ rootDir: string,
300
+ opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
301
+ ): Promise<void> {
302
+ const ctx = setupWatcher(rootDir, opts);
303
+
304
+ const usePoll = opts.poll ?? process.platform === 'win32';
305
+ const pollIntervalMs = opts.pollInterval ?? 2000;
306
+
307
+ info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
308
+ info('Press Ctrl+C to stop.');
309
+
310
+ const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
311
+
312
+ setupShutdownHandler(ctx, cleanup);
313
+ }
@@ -5,7 +5,7 @@ import type { Tree } from 'web-tree-sitter';
5
5
  import { Language, Parser, Query } from 'web-tree-sitter';
6
6
  import { debug, warn } from '../infrastructure/logger.js';
7
7
  import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js';
8
- import { toErrorMessage } from '../shared/errors.js';
8
+ import { ParseError, toErrorMessage } from '../shared/errors.js';
9
9
  import type {
10
10
  EngineMode,
11
11
  ExtractorOutput,
@@ -143,6 +143,8 @@ const COMMON_QUERY_PATTERNS: string[] = [
143
143
  '(call_expression function: (identifier) @callfn_name) @callfn_node',
144
144
  '(call_expression function: (member_expression) @callmem_fn) @callmem_node',
145
145
  '(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
146
+ '(new_expression constructor: (identifier) @newfn_name) @newfn_node',
147
+ '(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
146
148
  '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
147
149
  ];
148
150
 
@@ -186,7 +188,11 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
186
188
  _queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
187
189
  }
188
190
  } catch (e: unknown) {
189
- if (entry.required) throw e;
191
+ if (entry.required)
192
+ throw new ParseError(`Required parser ${entry.id} failed to initialize`, {
193
+ file: entry.grammarFile,
194
+ cause: e as Error,
195
+ });
190
196
  warn(
191
197
  `${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
192
198
  );
@@ -22,7 +22,7 @@ export {
22
22
  VALID_ROLES,
23
23
  } from '../shared/kinds.js';
24
24
  // ── Shared utilities ─────────────────────────────────────────────────────
25
- export { kindIcon, normalizeSymbol } from '../shared/normalize.js';
25
+ export { kindIcon, normalizeSymbol, toSymbolRef } from '../shared/normalize.js';
26
26
  export { briefData } from './analysis/brief.js';
27
27
  export { contextData, explainData } from './analysis/context.js';
28
28
  export { fileDepsData, filePathData, fnDepsData, pathData } from './analysis/dependencies.js';