optave-codegraph 3.13.0 → 3.14.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.
Files changed (34) hide show
  1. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  2. package/dist/domain/graph/builder/call-resolver.js +34 -18
  3. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  4. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  5. package/dist/domain/graph/builder/helpers.js +9 -23
  6. package/dist/domain/graph/builder/helpers.js.map +1 -1
  7. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  8. package/dist/domain/graph/builder/pipeline.js +4 -3
  9. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  10. package/dist/domain/graph/builder/stages/collect-files.js +1 -1
  11. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  12. package/dist/domain/graph/builder/stages/parse-files.d.ts +2 -1
  13. package/dist/domain/graph/builder/stages/parse-files.d.ts.map +1 -1
  14. package/dist/domain/graph/builder/stages/parse-files.js +2 -2
  15. package/dist/domain/graph/builder/stages/parse-files.js.map +1 -1
  16. package/dist/domain/parser.d.ts +5 -1
  17. package/dist/domain/parser.d.ts.map +1 -1
  18. package/dist/domain/parser.js +21 -3
  19. package/dist/domain/parser.js.map +1 -1
  20. package/dist/shared/globs.d.ts +5 -1
  21. package/dist/shared/globs.d.ts.map +1 -1
  22. package/dist/shared/globs.js +29 -2
  23. package/dist/shared/globs.js.map +1 -1
  24. package/dist/types.d.ts +2 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +8 -7
  27. package/src/domain/graph/builder/call-resolver.ts +36 -18
  28. package/src/domain/graph/builder/helpers.ts +17 -20
  29. package/src/domain/graph/builder/pipeline.ts +6 -4
  30. package/src/domain/graph/builder/stages/collect-files.ts +1 -1
  31. package/src/domain/graph/builder/stages/parse-files.ts +3 -3
  32. package/src/domain/parser.ts +31 -4
  33. package/src/shared/globs.ts +35 -2
  34. package/src/types.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optave-codegraph",
3
- "version": "3.13.0",
3
+ "version": "3.14.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": "dist/index.js",
@@ -91,6 +91,7 @@
91
91
  "lint:fix": "biome check --write src/ tests/",
92
92
  "format": "biome format --write src/ tests/",
93
93
  "prepack": "npm run build",
94
+ "prepublishOnly": "npm version patch --no-git-tag-version",
94
95
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('.tsbuildinfo',{force:true})\"",
95
96
  "prepare": "npm run build:wasm && npm run build && husky && npm run deps:tree",
96
97
  "deps:tree": "node scripts/node-ts.js scripts/gen-deps.ts",
@@ -132,12 +133,12 @@
132
133
  },
133
134
  "optionalDependencies": {
134
135
  "@modelcontextprotocol/sdk": "^1.0.0",
135
- "@optave/codegraph-darwin-arm64": "3.13.0",
136
- "@optave/codegraph-darwin-x64": "3.13.0",
137
- "@optave/codegraph-linux-arm64-gnu": "3.13.0",
138
- "@optave/codegraph-linux-x64-gnu": "3.13.0",
139
- "@optave/codegraph-linux-x64-musl": "3.13.0",
140
- "@optave/codegraph-win32-x64-msvc": "3.13.0"
136
+ "@optave/codegraph-darwin-arm64": "3.14.1",
137
+ "@optave/codegraph-darwin-x64": "3.14.1",
138
+ "@optave/codegraph-linux-arm64-gnu": "3.14.1",
139
+ "@optave/codegraph-linux-x64-gnu": "3.14.1",
140
+ "@optave/codegraph-linux-x64-musl": "3.14.1",
141
+ "@optave/codegraph-win32-x64-msvc": "3.14.1"
141
142
  },
142
143
  "devDependencies": {
143
144
  "@biomejs/biome": "^2.4.4",
@@ -66,23 +66,32 @@ function findEnclosingCallable(
66
66
  definitions: ReadonlyArray<Def>,
67
67
  relPath: string,
68
68
  ): CallerMatch {
69
- let best: CallerMatch = null;
70
- let bestSpan = Infinity;
69
+ const candidates: Array<{ def: Def; span: number }> = [];
70
+
71
71
  for (const def of definitions) {
72
72
  if (!CALLABLE_KINDS.has(def.kind)) continue;
73
73
  if (def.line > callLine) continue;
74
74
  const end = def.endLine ?? Infinity;
75
75
  if (callLine > end) continue;
76
76
  const span = end === Infinity ? Infinity : end - def.line;
77
- if (span < bestSpan) {
78
- const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
79
- if (row) {
80
- best = { ...row, name: def.name };
81
- bestSpan = span;
82
- }
77
+ candidates.push({ def, span });
78
+ }
79
+
80
+ candidates.sort((a, b) => {
81
+ if (a.span === b.span) return 0;
82
+ if (a.span === Infinity) return 1;
83
+ if (b.span === Infinity) return -1;
84
+ return a.span - b.span;
85
+ });
86
+
87
+ for (const { def } of candidates) {
88
+ const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
89
+ if (row) {
90
+ return { ...row, name: def.name };
83
91
  }
84
92
  }
85
- return best;
93
+
94
+ return null;
86
95
  }
87
96
 
88
97
  /**
@@ -97,23 +106,32 @@ function findEnclosingBinding(
97
106
  definitions: ReadonlyArray<Def>,
98
107
  relPath: string,
99
108
  ): CallerMatch {
100
- let best: CallerMatch = null;
101
- let bestSpan = -1; // looking for WIDEST span, so start at -1
109
+ const candidates: Array<{ def: Def; span: number }> = [];
110
+
102
111
  for (const def of definitions) {
103
112
  if (!TOP_LEVEL_BINDING_KINDS.has(def.kind)) continue;
104
113
  if (def.line > callLine) continue;
105
114
  const end = def.endLine ?? Infinity;
106
115
  if (callLine > end) continue;
107
116
  const span = end === Infinity ? Infinity : end - def.line;
108
- if (span > bestSpan) {
109
- const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
110
- if (row) {
111
- best = { ...row, name: def.name };
112
- bestSpan = span;
113
- }
117
+ candidates.push({ def, span });
118
+ }
119
+
120
+ candidates.sort((a, b) => {
121
+ if (a.span === b.span) return 0;
122
+ if (a.span === Infinity) return -1;
123
+ if (b.span === Infinity) return 1;
124
+ return b.span - a.span; // Descending (widest first)
125
+ });
126
+
127
+ for (const { def } of candidates) {
128
+ const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
129
+ if (row) {
130
+ return { ...row, name: def.name };
114
131
  }
115
132
  }
116
- return best;
133
+
134
+ return null;
117
135
  }
118
136
 
119
137
  export function findCaller(
@@ -9,7 +9,12 @@ import path from 'node:path';
9
9
  import { purgeFilesData } from '../../../db/index.js';
10
10
  import { debug, warn } from '../../../infrastructure/logger.js';
11
11
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
12
- import { compileGlobs, globToRegex, matchesAny } from '../../../shared/globs.js';
12
+ import {
13
+ compileGlobs,
14
+ globToRegex,
15
+ matchesAny,
16
+ transformExcludePatterns,
17
+ } from '../../../shared/globs.js';
13
18
  import type {
14
19
  BetterSqlite3Database,
15
20
  CodegraphConfig,
@@ -107,25 +112,17 @@ export function readGitignorePatterns(rootDir: string): readonly RegExp[] {
107
112
  }
108
113
 
109
114
  const regexes: RegExp[] = [];
110
- for (const rawLine of contents.split(/\r?\n/)) {
111
- const line = rawLine.trim();
112
- // Skip empty lines, comments, and negation patterns
113
- if (!line || line.startsWith('#') || line.startsWith('!')) continue;
114
- // Strip trailing slash (directory indicator) — we match files by path regardless
115
- const pattern = line.endsWith('/') ? line.slice(0, -1) : line;
115
+ // Apply the same transformation logic as for exclude patterns
116
+ const transformedGitignorePatterns = transformExcludePatterns(
117
+ contents
118
+ .split(/\r?\n/)
119
+ .map((line) => line.trim())
120
+ .filter((line) => line && !line.startsWith('#') && !line.startsWith('!')),
121
+ );
122
+
123
+ for (const pattern of transformedGitignorePatterns) {
116
124
  try {
117
- // If pattern contains no '/', it should match at any depth — prefix with `**/`.
118
- // If pattern starts with '/', it is anchored to root — strip the leading slash.
119
- // Otherwise use as-is (e.g. `crates/codegraph-core/index.js`).
120
- let normalized: string;
121
- if (pattern.startsWith('/')) {
122
- normalized = pattern.slice(1);
123
- } else if (!pattern.includes('/')) {
124
- normalized = `**/${pattern}`;
125
- } else {
126
- normalized = pattern;
127
- }
128
- regexes.push(globToRegex(normalized));
125
+ regexes.push(globToRegex(pattern));
129
126
  } catch {
130
127
  // Ignore patterns that don't compile (e.g. those with unsupported syntax)
131
128
  }
@@ -235,7 +232,7 @@ export function collectFiles(
235
232
  ): string[] | { files: string[]; directories: Set<string> } {
236
233
  const trackDirs = directories instanceof Set;
237
234
  const includeRegexes = compileGlobs(config.include);
238
- const excludeRegexes = compileGlobs(config.exclude);
235
+ const excludeRegexes = compileGlobs(config.exclude, true);
239
236
  const gitignoreRegexes = readGitignorePatterns(dir);
240
237
  const ctx: CollectContext = {
241
238
  rootDir: dir,
@@ -28,7 +28,7 @@ import { loadNative } from '../../../infrastructure/native.js';
28
28
  import { toErrorMessage } from '../../../shared/errors.js';
29
29
  import { CODEGRAPH_VERSION } from '../../../shared/version.js';
30
30
  import type { BuildGraphOpts, BuildResult } from '../../../types.js';
31
- import { getActiveEngine } from '../../parser.js';
31
+ import { getActiveEngine, type ParseFileOpts } from '../../parser.js';
32
32
  import { writeJournalHeader } from '../journal.js';
33
33
  import { setWorkspaces } from '../resolve.js';
34
34
  import { PipelineContext } from './context.js';
@@ -274,7 +274,7 @@ function formatTimingResult(ctx: PipelineContext): BuildResult {
274
274
 
275
275
  // ── Pipeline stages execution ───────────────────────────────────────────
276
276
 
277
- async function runPipelineStages(ctx: PipelineContext): Promise<void> {
277
+ async function runPipelineStages(ctx: PipelineContext, options: ParseFileOpts = {}): Promise<void> {
278
278
  // ── WASM / fallback dual-connection mode ─────────────────────────────
279
279
  // NativeDatabase is deferred — not opened during setup. collectFiles and
280
280
  // detectChanges only need better-sqlite3. If no files changed, we exit
@@ -297,7 +297,7 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
297
297
 
298
298
  if (ctx.earlyExit) return;
299
299
 
300
- await parseFiles(ctx);
300
+ await parseFiles(ctx, options);
301
301
 
302
302
  // For small incremental builds (≤smallFilesThreshold files), skip the nativeDb open/close
303
303
  // cycle for insertNodes — the WAL checkpoint + connection churn (~5-10ms)
@@ -388,6 +388,8 @@ export async function buildGraph(
388
388
  rootDir: string,
389
389
  opts: BuildGraphOpts = {},
390
390
  ): Promise<BuildResult | undefined> {
391
+ const { parseFileOptions } = opts;
392
+
391
393
  const ctx = new PipelineContext();
392
394
  ctx.buildStart = performance.now();
393
395
  ctx.opts = opts;
@@ -486,7 +488,7 @@ export async function buildGraph(
486
488
  }
487
489
  }
488
490
 
489
- await runPipelineStages(ctx);
491
+ await runPipelineStages(ctx, parseFileOptions);
490
492
  } catch (err) {
491
493
  if (!ctx.earlyExit) {
492
494
  // Release WASM trees before closing DB to prevent V8 crash during
@@ -82,7 +82,7 @@ function tryFastCollect(
82
82
  // Also apply gitignore patterns so the incremental fast path is consistent
83
83
  // with the full filesystem walk (which calls readGitignorePatterns too).
84
84
  const includeRegexes = compileGlobs(config?.include);
85
- const excludeRegexes = compileGlobs(config?.exclude);
85
+ const excludeRegexes = compileGlobs(config?.exclude, true);
86
86
  const hasGlobFilters = includeRegexes.length > 0 || excludeRegexes.length > 0;
87
87
  const gitignoreRegexes = readGitignorePatterns(rootDir);
88
88
 
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import { performance } from 'node:perf_hooks';
8
8
  import { info } from '../../../../infrastructure/logger.js';
9
- import { parseFilesAuto } from '../../../parser.js';
9
+ import { type ParseFileOpts, parseFilesAuto } from '../../../parser.js';
10
10
  import type { PipelineContext } from '../context.js';
11
11
 
12
- export async function parseFiles(ctx: PipelineContext): Promise<void> {
12
+ export async function parseFiles(ctx: PipelineContext, options: ParseFileOpts = {}): Promise<void> {
13
13
  const { allFiles, parseChanges, isFullBuild, engineOpts, rootDir } = ctx;
14
14
 
15
15
  ctx.filesToParse = isFullBuild ? allFiles.map((f) => ({ file: f })) : parseChanges;
@@ -17,7 +17,7 @@ export async function parseFiles(ctx: PipelineContext): Promise<void> {
17
17
 
18
18
  const filePaths = ctx.filesToParse.map((item) => item.file);
19
19
  const t0 = performance.now();
20
- ctx.allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
20
+ ctx.allSymbols = await parseFilesAuto(filePaths, rootDir, { ...engineOpts, ...options });
21
21
  ctx.timing.parseMs = performance.now() - t0;
22
22
 
23
23
  const parsed = ctx.allSymbols.size;
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { setTimeout } from 'node:timers/promises';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import type { Tree } from 'web-tree-sitter';
5
6
  import { Language, Parser, Query } from 'web-tree-sitter';
@@ -1154,6 +1155,11 @@ async function backfillTypeMapBatch(
1154
1155
  }
1155
1156
  }
1156
1157
 
1158
+ export interface ParseFileOpts {
1159
+ throttlePerFileInMs?: number;
1160
+ onFileProcessed?: (filePath: string, processed: number, total: number) => void;
1161
+ }
1162
+
1157
1163
  /**
1158
1164
  * Parse files via WASM engine, returning a Map<relPath, symbols>.
1159
1165
  *
@@ -1169,10 +1175,18 @@ async function backfillTypeMapBatch(
1169
1175
  async function parseFilesWasm(
1170
1176
  filePaths: string[],
1171
1177
  rootDir: string,
1172
- analysis: WorkerAnalysisOpts = FULL_ANALYSIS,
1178
+ options?: WorkerAnalysisOpts & ParseFileOpts,
1173
1179
  ): Promise<Map<string, ExtractorOutput>> {
1180
+ const { throttlePerFileInMs, onFileProcessed, ...analysis } = options || { ...FULL_ANALYSIS };
1181
+
1182
+ if (Object.keys(analysis).length === 0) {
1183
+ Object.assign(analysis, FULL_ANALYSIS);
1184
+ }
1185
+
1174
1186
  const result = new Map<string, ExtractorOutput>();
1175
1187
  const pool = getWasmWorkerPool();
1188
+ let processed = 0;
1189
+
1176
1190
  for (const filePath of filePaths) {
1177
1191
  if (!_extToLang.has(path.extname(filePath).toLowerCase())) continue;
1178
1192
  let code: string;
@@ -1186,6 +1200,13 @@ async function parseFilesWasm(
1186
1200
  if (output) {
1187
1201
  const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
1188
1202
  result.set(relPath, output);
1203
+ if (throttlePerFileInMs) {
1204
+ await setTimeout(throttlePerFileInMs);
1205
+ }
1206
+ }
1207
+ processed++;
1208
+ if (onFileProcessed) {
1209
+ onFileProcessed(filePath, processed, filePaths.length);
1189
1210
  }
1190
1211
  }
1191
1212
  return result;
@@ -1359,10 +1380,16 @@ async function backfillNativeDrops(
1359
1380
  export async function parseFilesAuto(
1360
1381
  filePaths: string[],
1361
1382
  rootDir: string,
1362
- opts: ParseEngineOpts = {},
1383
+ opts: ParseEngineOpts & ParseFileOpts = {},
1363
1384
  ): Promise<Map<string, ExtractorOutput>> {
1364
- const { native } = resolveEngine(opts);
1365
- if (!native) return parseFilesWasm(filePaths, rootDir);
1385
+ const { throttlePerFileInMs, onFileProcessed, ...engOpt } = opts;
1386
+ const { native } = resolveEngine(engOpt);
1387
+ if (!native)
1388
+ return parseFilesWasm(filePaths, rootDir, {
1389
+ throttlePerFileInMs,
1390
+ onFileProcessed,
1391
+ ...FULL_ANALYSIS,
1392
+ });
1366
1393
 
1367
1394
  const result = new Map<string, ExtractorOutput>();
1368
1395
  const { nativeParsed, needsTypeMap } = ingestNativeResults(native, filePaths, rootDir, result);
@@ -74,13 +74,17 @@ function buildCacheKey(patterns: readonly string[]): string {
74
74
  * with the same include/exclude lists reuse the compiled regexes. The
75
75
  * returned array is shared across callers and must not be mutated.
76
76
  */
77
- export function compileGlobs(patterns: readonly string[] | undefined): readonly RegExp[] {
77
+ export function compileGlobs(
78
+ patterns: readonly string[] | undefined,
79
+ isExclusion: boolean = false,
80
+ ): readonly RegExp[] {
78
81
  if (!patterns || patterns.length === 0) return EMPTY_REGEX_LIST;
79
82
  const key = buildCacheKey(patterns);
80
83
  const cached = compileCache.get(key);
81
84
  if (cached) return cached;
82
85
  const out: RegExp[] = [];
83
- for (const p of patterns) {
86
+ const transformedPatterns = isExclusion ? transformExcludePatterns(patterns) : patterns;
87
+ for (const p of transformedPatterns) {
84
88
  if (typeof p !== 'string' || p.length === 0) continue;
85
89
  try {
86
90
  out.push(globToRegex(p));
@@ -119,3 +123,32 @@ export function matchesAny(regexes: readonly RegExp[], path: string): boolean {
119
123
  }
120
124
  return false;
121
125
  }
126
+
127
+ /**
128
+ * Transforms exclude patterns to behave more like gitignore rules.
129
+ */
130
+ export function transformExcludePatterns(patterns: readonly string[]): string[] {
131
+ const transformed: string[] = [];
132
+ for (const pattern of patterns) {
133
+ let p = pattern;
134
+
135
+ if (p.startsWith('/')) {
136
+ p = p.slice(1);
137
+ } else if (!p.includes('/') && !p.startsWith('**')) {
138
+ p = `**/${p}`;
139
+ }
140
+
141
+ if (
142
+ pattern.endsWith('/') ||
143
+ (!pattern.includes('/') && !pattern.includes('*') && !pattern.includes('?'))
144
+ ) {
145
+ if (!p.endsWith('**') && !p.endsWith('/*')) {
146
+ p += '/**';
147
+ }
148
+ } else if (p.endsWith('/')) {
149
+ p += '**';
150
+ }
151
+ transformed.push(p);
152
+ }
153
+ return transformed;
154
+ }
package/src/types.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * parsers, builders, visitors, features, config, and the graph model.
8
8
  */
9
9
 
10
+ import type { ParseFileOpts } from '#domain/parser.js';
11
+
10
12
  // ════════════════════════════════════════════════════════════════════════
11
13
  // §1 Symbol & Edge Kind Enumerations
12
14
  // ════════════════════════════════════════════════════════════════════════
@@ -1308,6 +1310,8 @@ export interface BuildGraphOpts {
1308
1310
  * build command when stdin/stdout are TTYs and CI is not set.
1309
1311
  */
1310
1312
  promptForConsent?: boolean;
1313
+
1314
+ parseFileOptions?: ParseFileOpts;
1311
1315
  }
1312
1316
 
1313
1317
  /** Build timing result from buildGraph. */