@optave/codegraph 3.9.5 → 3.10.0

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 (108) hide show
  1. package/README.md +30 -16
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +4 -3
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/csharp.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/csharp.js +8 -1
  7. package/dist/ast-analysis/rules/csharp.js.map +1 -1
  8. package/dist/ast-analysis/rules/go.d.ts.map +1 -1
  9. package/dist/ast-analysis/rules/go.js +4 -1
  10. package/dist/ast-analysis/rules/go.js.map +1 -1
  11. package/dist/ast-analysis/rules/index.d.ts +6 -0
  12. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  13. package/dist/ast-analysis/rules/index.js +151 -4
  14. package/dist/ast-analysis/rules/index.js.map +1 -1
  15. package/dist/ast-analysis/rules/java.d.ts.map +1 -1
  16. package/dist/ast-analysis/rules/java.js +5 -1
  17. package/dist/ast-analysis/rules/java.js.map +1 -1
  18. package/dist/ast-analysis/rules/php.d.ts.map +1 -1
  19. package/dist/ast-analysis/rules/php.js +6 -1
  20. package/dist/ast-analysis/rules/php.js.map +1 -1
  21. package/dist/ast-analysis/rules/python.d.ts.map +1 -1
  22. package/dist/ast-analysis/rules/python.js +5 -1
  23. package/dist/ast-analysis/rules/python.js.map +1 -1
  24. package/dist/ast-analysis/rules/ruby.d.ts.map +1 -1
  25. package/dist/ast-analysis/rules/ruby.js +4 -1
  26. package/dist/ast-analysis/rules/ruby.js.map +1 -1
  27. package/dist/ast-analysis/rules/rust.d.ts.map +1 -1
  28. package/dist/ast-analysis/rules/rust.js +5 -1
  29. package/dist/ast-analysis/rules/rust.js.map +1 -1
  30. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts +2 -1
  31. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  32. package/dist/ast-analysis/visitors/ast-store-visitor.js +171 -37
  33. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  34. package/dist/domain/graph/builder/context.d.ts +10 -0
  35. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/context.js +10 -0
  37. package/dist/domain/graph/builder/context.js.map +1 -1
  38. package/dist/domain/graph/builder/helpers.d.ts +7 -2
  39. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/helpers.js +7 -2
  41. package/dist/domain/graph/builder/helpers.js.map +1 -1
  42. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.js +210 -34
  44. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  45. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  46. package/dist/domain/graph/builder/stages/collect-files.js +8 -0
  47. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  48. package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +9 -6
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/parser.d.ts +54 -1
  60. package/dist/domain/parser.d.ts.map +1 -1
  61. package/dist/domain/parser.js +181 -10
  62. package/dist/domain/parser.js.map +1 -1
  63. package/dist/domain/search/models.js +2 -2
  64. package/dist/domain/wasm-worker-entry.js +15 -14
  65. package/dist/domain/wasm-worker-entry.js.map +1 -1
  66. package/dist/features/ast.d.ts.map +1 -1
  67. package/dist/features/ast.js +11 -9
  68. package/dist/features/ast.js.map +1 -1
  69. package/dist/infrastructure/config.d.ts +1 -0
  70. package/dist/infrastructure/config.d.ts.map +1 -1
  71. package/dist/infrastructure/config.js +1 -0
  72. package/dist/infrastructure/config.js.map +1 -1
  73. package/dist/mcp/server.d.ts.map +1 -1
  74. package/dist/mcp/server.js +14 -8
  75. package/dist/mcp/server.js.map +1 -1
  76. package/dist/mcp/tool-registry.d.ts +1 -1
  77. package/dist/mcp/tool-registry.d.ts.map +1 -1
  78. package/dist/mcp/tool-registry.js +19 -5
  79. package/dist/mcp/tool-registry.js.map +1 -1
  80. package/dist/types.d.ts +1 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/grammars/tree-sitter-erlang.wasm +0 -0
  83. package/package.json +8 -7
  84. package/src/ast-analysis/engine.ts +14 -2
  85. package/src/ast-analysis/rules/csharp.ts +8 -1
  86. package/src/ast-analysis/rules/go.ts +4 -1
  87. package/src/ast-analysis/rules/index.ts +181 -4
  88. package/src/ast-analysis/rules/java.ts +5 -1
  89. package/src/ast-analysis/rules/php.ts +6 -1
  90. package/src/ast-analysis/rules/python.ts +5 -1
  91. package/src/ast-analysis/rules/ruby.ts +4 -1
  92. package/src/ast-analysis/rules/rust.ts +5 -1
  93. package/src/ast-analysis/visitors/ast-store-visitor.ts +165 -34
  94. package/src/domain/graph/builder/context.ts +10 -0
  95. package/src/domain/graph/builder/helpers.ts +8 -3
  96. package/src/domain/graph/builder/pipeline.ts +234 -36
  97. package/src/domain/graph/builder/stages/collect-files.ts +9 -0
  98. package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
  99. package/src/domain/graph/builder/stages/finalize.ts +9 -6
  100. package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
  101. package/src/domain/parser.ts +205 -9
  102. package/src/domain/search/models.ts +2 -2
  103. package/src/domain/wasm-worker-entry.ts +23 -13
  104. package/src/features/ast.ts +22 -9
  105. package/src/infrastructure/config.ts +1 -0
  106. package/src/mcp/server.ts +16 -9
  107. package/src/mcp/tool-registry.ts +23 -5
  108. package/src/types.ts +1 -0
@@ -172,4 +172,8 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({
172
172
 
173
173
  // ─── AST Node Types ───────────────────────────────────────────────────────
174
174
 
175
- export const astTypes: Record<string, string> | null = null;
175
+ export const astTypes: Record<string, string> | null = {
176
+ await_expression: 'await',
177
+ string_literal: 'string',
178
+ raw_string_literal: 'string',
179
+ };
@@ -5,9 +5,42 @@ import type {
5
5
  Visitor,
6
6
  VisitorContext,
7
7
  } from '../../types.js';
8
+ import type { AstStringConfig } from '../rules/index.js';
8
9
 
9
10
  const TEXT_MAX = 200;
10
11
 
12
+ // ── Cross-language node-type constants (mirror Rust `helpers.rs`) ────────
13
+ const IDENT_TYPES = new Set<string>([
14
+ 'identifier',
15
+ 'type_identifier',
16
+ 'name',
17
+ 'qualified_name',
18
+ 'scoped_identifier',
19
+ 'qualified_identifier',
20
+ 'member_expression',
21
+ 'member_access_expression',
22
+ 'field_expression',
23
+ 'attribute',
24
+ 'scoped_type_identifier',
25
+ ]);
26
+
27
+ const CALL_TYPES = new Set<string>([
28
+ 'call_expression',
29
+ 'call',
30
+ 'invocation_expression',
31
+ 'method_invocation',
32
+ 'function_call_expression',
33
+ 'member_call_expression',
34
+ 'scoped_call_expression',
35
+ ]);
36
+
37
+ const DEFAULT_STRING_CONFIG: AstStringConfig = { quoteChars: '\'"`', stringPrefixes: '' };
38
+
39
+ // Keyword tokens skipped when extracting the inner expression text of a
40
+ // throw/raise/await/new node. Module-level constant avoids reallocating on
41
+ // every call (can be hot in large files).
42
+ const CHILD_EXPR_SKIP_KEYWORDS = new Set<string>(['throw', 'raise', 'await', 'new']);
43
+
11
44
  interface AstStoreRow {
12
45
  file: string;
13
46
  line: number;
@@ -20,69 +53,150 @@ interface AstStoreRow {
20
53
 
21
54
  function truncate(s: string | null | undefined, max: number = TEXT_MAX): string | null {
22
55
  if (!s) return null;
23
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
56
+ return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
57
+ }
58
+
59
+ function trimLeadingChars(s: string, chars: string): string {
60
+ if (!chars) return s;
61
+ let i = 0;
62
+ while (i < s.length && chars.includes(s[i]!)) i++;
63
+ return i === 0 ? s : s.slice(i);
64
+ }
65
+
66
+ function trimTrailingChars(s: string, chars: string): string {
67
+ if (!chars) return s;
68
+ let i = s.length;
69
+ while (i > 0 && chars.includes(s[i - 1]!)) i--;
70
+ return i === s.length ? s : s.slice(0, i);
24
71
  }
25
72
 
26
- function extractNewName(node: TreeSitterNode): string {
73
+ /** Extract constructor name from a `new_expression` / `object_creation_expression`. */
74
+ function extractConstructorName(node: TreeSitterNode): string {
75
+ for (const field of ['type', 'class', 'constructor']) {
76
+ const f = node.childForFieldName(field);
77
+ if (f?.text) return f.text;
78
+ }
27
79
  for (let i = 0; i < node.childCount; i++) {
28
80
  const child = node.child(i);
29
81
  if (!child) continue;
30
- if (child.type === 'identifier') return child.text;
31
- if (child.type === 'member_expression') return child.text;
82
+ if (IDENT_TYPES.has(child.type)) return child.text;
32
83
  }
33
- return node.text?.split('(')[0]?.replace('new ', '').trim() || '?';
84
+ const raw = node.text || '';
85
+ const beforeParen = raw.split('(')[0] || raw;
86
+ return beforeParen.replace(/^new\s+/, '').trim() || '?';
34
87
  }
35
88
 
36
- function extractExpressionText(node: TreeSitterNode): string | null {
89
+ /** Extract function name from a call node. */
90
+ function extractCallName(node: TreeSitterNode): string {
91
+ for (const field of ['function', 'method', 'name']) {
92
+ const f = node.childForFieldName(field);
93
+ if (f?.text) return f.text;
94
+ }
95
+ const text = node.text || '';
96
+ return text.split('(')[0] || '?';
97
+ }
98
+
99
+ /** Extract name from a throw/raise statement — matches native `extract_throw_target`. */
100
+ function extractThrowName(node: TreeSitterNode, newTypes: Set<string>): string {
37
101
  for (let i = 0; i < node.childCount; i++) {
38
102
  const child = node.child(i);
39
103
  if (!child) continue;
40
- if (child.type !== 'throw' && child.type !== 'await') {
41
- return truncate(child.text);
42
- }
104
+ const ck = child.type;
105
+ if (newTypes.has(ck)) return extractConstructorName(child);
106
+ if (CALL_TYPES.has(ck)) return extractCallName(child);
107
+ if (IDENT_TYPES.has(ck)) return child.text;
43
108
  }
44
- return truncate(node.text);
109
+ return truncate(node.text) ?? node.text ?? '';
45
110
  }
46
111
 
47
- /** Extract the name from a throw statement's child nodes. */
48
- function extractThrowName(node: TreeSitterNode): string | null {
112
+ /** Extract name from an await expression matches native `extract_awaited_name`. */
113
+ function extractAwaitName(node: TreeSitterNode): string {
49
114
  for (let i = 0; i < node.childCount; i++) {
50
115
  const child = node.child(i);
51
116
  if (!child) continue;
52
- if (child.type === 'new_expression') return extractNewName(child);
53
- if (child.type === 'call_expression') {
54
- const fn = child.childForFieldName('function');
55
- return fn ? fn.text : child.text?.split('(')[0] || '?';
56
- }
57
- if (child.type === 'identifier') return child.text;
117
+ const ck = child.type;
118
+ if (CALL_TYPES.has(ck)) return extractCallName(child);
119
+ if (IDENT_TYPES.has(ck)) return child.text;
58
120
  }
59
- return truncate(node.text);
121
+ return truncate(node.text) ?? node.text ?? '';
60
122
  }
61
123
 
62
- /** Extract the name from an await expression's child nodes. */
63
- function extractAwaitName(node: TreeSitterNode): string | null {
124
+ /** Extract text of the expression inside a throw/await, skipping the keyword. */
125
+ function extractChildExpressionText(node: TreeSitterNode): string | null {
64
126
  for (let i = 0; i < node.childCount; i++) {
65
127
  const child = node.child(i);
66
128
  if (!child) continue;
67
- if (child.type === 'call_expression') {
68
- const fn = child.childForFieldName('function');
69
- return fn ? fn.text : child.text?.split('(')[0] || '?';
70
- }
71
- if (child.type === 'identifier' || child.type === 'member_expression') {
72
- return child.text;
73
- }
129
+ if (!CHILD_EXPR_SKIP_KEYWORDS.has(child.type)) return truncate(child.text);
74
130
  }
75
131
  return truncate(node.text);
76
132
  }
77
133
 
134
+ /**
135
+ * Count code points cheaply: skip the `[...s]` spread when `s.length` already
136
+ * decides the answer. Each code point is 1 or 2 UTF-16 units, so `.length < 2`
137
+ * implies `< 2` code points and `.length >= 3` already guarantees `>= 2` code
138
+ * points (worst case: one surrogate pair + one BMP char = 2 code points).
139
+ * Only `.length === 2` is genuinely ambiguous (could be a single surrogate
140
+ * pair = 1 code point, or two BMP chars = 2 code points) and needs the spread.
141
+ */
142
+ function codePointCountAtLeast2(s: string): boolean {
143
+ const len = s.length;
144
+ if (len < 2) return false;
145
+ if (len >= 3) return true;
146
+ return [...s].length >= 2;
147
+ }
148
+
149
+ /**
150
+ * Extract string content from a string-literal node, mirroring the native
151
+ * engine's `build_string_node` (`helpers.rs`). Returns `null` when the
152
+ * content is shorter than 2 Unicode code points.
153
+ */
154
+ function extractStringContent(node: TreeSitterNode, cfg: AstStringConfig): string | null {
155
+ const raw = node.text ?? '';
156
+ const isRawString = node.type.includes('raw_string');
157
+
158
+ let s = raw;
159
+ s = trimLeadingChars(s, '@');
160
+ if (cfg.stringPrefixes) s = trimLeadingChars(s, cfg.stringPrefixes);
161
+ if (isRawString) s = trimLeadingChars(s, 'r#');
162
+ s = trimLeadingChars(s, cfg.quoteChars);
163
+ if (isRawString) s = trimTrailingChars(s, '#');
164
+ s = trimTrailingChars(s, cfg.quoteChars);
165
+
166
+ return codePointCountAtLeast2(s) ? s : null;
167
+ }
168
+
169
+ // Per-astTypeMap cache for the set of node-types that map to kind 'new'.
170
+ // Computed once per unique astTypeMap reference (one per language) instead
171
+ // of once per file.
172
+ const _newTypesCache = new WeakMap<Record<string, string>, Set<string>>();
173
+ function newTypesFor(astTypeMap: Record<string, string>): Set<string> {
174
+ let s = _newTypesCache.get(astTypeMap);
175
+ if (s) return s;
176
+ s = new Set<string>();
177
+ for (const type in astTypeMap) {
178
+ if (astTypeMap[type] === 'new') s.add(type);
179
+ }
180
+ _newTypesCache.set(astTypeMap, s);
181
+ return s;
182
+ }
183
+
78
184
  export function createAstStoreVisitor(
79
185
  astTypeMap: Record<string, string>,
80
186
  defs: Definition[],
81
187
  relPath: string,
82
188
  nodeIdMap: Map<string, number>,
189
+ stringConfig: AstStringConfig = DEFAULT_STRING_CONFIG,
190
+ stopRecurseKinds: ReadonlySet<string> = new Set(),
83
191
  ): Visitor {
84
192
  const rows: AstStoreRow[] = [];
85
193
  const matched = new Set<number>();
194
+ const newTypes = newTypesFor(astTypeMap);
195
+ // When nodeIdMap is empty, parentNodeId resolution is wasted work — the
196
+ // worker passes an empty map and the main thread re-resolves against its
197
+ // own DB-populated map in features/ast.ts::collectFileAstRows. Skip the
198
+ // findParentDef linear scan in that case.
199
+ const skipParentLookup = nodeIdMap.size === 0;
86
200
 
87
201
  function findParentDef(line: number): Definition | null {
88
202
  let best: Definition | null = null;
@@ -97,6 +211,7 @@ export function createAstStoreVisitor(
97
211
  }
98
212
 
99
213
  function resolveParentNodeId(line: number): number | null {
214
+ if (skipParentLookup) return null;
100
215
  const parentDef = findParentDef(line);
101
216
  if (!parentDef) return null;
102
217
  return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
@@ -106,12 +221,15 @@ export function createAstStoreVisitor(
106
221
  type KindHandler = (node: TreeSitterNode) => NameTextResult;
107
222
 
108
223
  const kindHandlers: Record<string, KindHandler> = {
109
- new: (node) => ({ name: extractNewName(node), text: truncate(node.text) }),
110
- throw: (node) => ({ name: extractThrowName(node), text: extractExpressionText(node) }),
111
- await: (node) => ({ name: extractAwaitName(node), text: extractExpressionText(node) }),
224
+ new: (node) => ({ name: extractConstructorName(node), text: truncate(node.text) }),
225
+ throw: (node) => ({
226
+ name: extractThrowName(node, newTypes),
227
+ text: extractChildExpressionText(node),
228
+ }),
229
+ await: (node) => ({ name: extractAwaitName(node), text: extractChildExpressionText(node) }),
112
230
  string: (node) => {
113
- const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
114
- if (content.length < 2) return { name: null, text: null, skip: true };
231
+ const content = extractStringContent(node, stringConfig);
232
+ if (content == null) return { name: null, text: null, skip: true };
115
233
  return { name: truncate(content, 100), text: truncate(node.text) };
116
234
  },
117
235
  regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
@@ -151,12 +269,25 @@ export function createAstStoreVisitor(
151
269
  // unrelated subtree. The parent call's skipChildren handles the intended case.
152
270
  if (matched.has(node.id)) return;
153
271
 
272
+ // Gate with `hasOwn` because plain-object lookup walks Object.prototype:
273
+ // tree-sitter node types like `constructor` (Haskell sum-types: Left,
274
+ // Right) would otherwise resolve to `Object.prototype.constructor` (the
275
+ // Object() function), which then crashes the worker boundary with
276
+ // "function Object() { [native code] } could not be cloned" when the
277
+ // resulting astNodes row is structured-cloned back to the main thread.
278
+ if (!Object.hasOwn(astTypeMap, node.type)) return;
154
279
  const kind = astTypeMap[node.type];
155
280
  if (!kind) return;
156
281
 
157
282
  collectNode(node, kind);
158
283
 
159
- if (kind !== 'string' && kind !== 'regex') {
284
+ // Mirror the native walker's recursion policy. In JS/TS, the native
285
+ // javascript.rs walker returns after collecting `new` or `throw` to
286
+ // avoid double-counting the wrapped expression (e.g. `throw new
287
+ // Error('x')` emits one `throw` row, not throw+new+string). Other
288
+ // languages go through helpers.rs::walk_ast_nodes_with_config_depth
289
+ // which always recurses — so `stopRecurseKinds` is empty for them.
290
+ if (stopRecurseKinds.has(kind)) {
160
291
  return { skipChildren: true };
161
292
  }
162
293
  },
@@ -28,6 +28,16 @@ export class PipelineContext {
28
28
  engineOpts!: EngineOpts;
29
29
  engineName!: 'native' | 'wasm';
30
30
  engineVersion!: string | null;
31
+ /**
32
+ * The version reported by the native binary itself (CARGO_PKG_VERSION at
33
+ * build time), as opposed to `engineVersion` which prefers the platform
34
+ * package.json. The Rust orchestrator's check_version_mismatch compares
35
+ * `build_meta.engine_version` against CARGO_PKG_VERSION, so build_meta
36
+ * writes must use this value to avoid a perpetual full-rebuild loop when
37
+ * the binary and platform package.json drift apart (e.g., CI hot-swap
38
+ * via ci-install-native.mjs — #1066).
39
+ */
40
+ nativeBinaryVersion!: string | null;
31
41
  aliases!: PathAliases;
32
42
  incremental!: boolean;
33
43
  forceFullRebuild: boolean = false;
@@ -222,12 +222,17 @@ export function fileHash(content: string): string {
222
222
  }
223
223
 
224
224
  /**
225
- * Stat a file, returning { mtimeMs, size } or null on error.
225
+ * Stat a file, returning { mtime, size } or null on error.
226
+ *
227
+ * `mtime` is `Math.floor(stat.mtimeMs)` so it matches the integer column
228
+ * stored in the DB. Floor-once-here keeps every consumer honest: storing or
229
+ * comparing a non-floored `mtimeMs` against the integer DB column would cause
230
+ * spurious fast-skip misses on the next build.
226
231
  */
227
- export function fileStat(filePath: string): { mtimeMs: number; size: number } | null {
232
+ export function fileStat(filePath: string): { mtime: number; size: number } | null {
228
233
  try {
229
234
  const s = fs.statSync(filePath);
230
- return { mtimeMs: s.mtimeMs, size: s.size };
235
+ return { mtime: Math.floor(s.mtimeMs), size: s.size };
231
236
  } catch {
232
237
  return null;
233
238
  }