@optave/codegraph 3.0.0 → 3.0.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.
@@ -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
@@ -38,6 +38,9 @@ function grammarPath(name) {
38
38
 
39
39
  let _initialized = false;
40
40
 
41
+ // Memoized parsers — avoids reloading WASM grammars on every createParsers() call
42
+ let _cachedParsers = null;
43
+
41
44
  // Query cache for JS/TS/TSX extractors (populated during createParsers)
42
45
  const _queryCache = new Map();
43
46
 
@@ -66,6 +69,8 @@ const TS_EXTRA_PATTERNS = [
66
69
  ];
67
70
 
68
71
  export async function createParsers() {
72
+ if (_cachedParsers) return _cachedParsers;
73
+
69
74
  if (!_initialized) {
70
75
  await Parser.init();
71
76
  _initialized = true;
@@ -94,6 +99,7 @@ export async function createParsers() {
94
99
  parsers.set(entry.id, null);
95
100
  }
96
101
  }
102
+ _cachedParsers = parsers;
97
103
  return parsers;
98
104
  }
99
105
 
@@ -104,6 +110,63 @@ export function getParser(parsers, filePath) {
104
110
  return parsers.get(entry.id) || null;
105
111
  }
106
112
 
113
+ /**
114
+ * Pre-parse files missing `_tree` via WASM so downstream phases (CFG, dataflow)
115
+ * don't each need to create parsers and re-parse independently.
116
+ * Only parses files whose extension is in SUPPORTED_EXTENSIONS.
117
+ *
118
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId, ... }>
119
+ * @param {string} rootDir - absolute project root
120
+ */
121
+ export async function ensureWasmTrees(fileSymbols, rootDir) {
122
+ // Check if any file needs a tree
123
+ let needsParse = false;
124
+ for (const [relPath, symbols] of fileSymbols) {
125
+ if (!symbols._tree) {
126
+ const ext = path.extname(relPath).toLowerCase();
127
+ if (_extToLang.has(ext)) {
128
+ needsParse = true;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ if (!needsParse) return;
134
+
135
+ const parsers = await createParsers();
136
+
137
+ for (const [relPath, symbols] of fileSymbols) {
138
+ if (symbols._tree) continue;
139
+ const ext = path.extname(relPath).toLowerCase();
140
+ const entry = _extToLang.get(ext);
141
+ if (!entry) continue;
142
+ const parser = parsers.get(entry.id);
143
+ if (!parser) continue;
144
+
145
+ const absPath = path.join(rootDir, relPath);
146
+ let code;
147
+ try {
148
+ code = fs.readFileSync(absPath, 'utf-8');
149
+ } catch {
150
+ continue;
151
+ }
152
+ try {
153
+ symbols._tree = parser.parse(code);
154
+ symbols._langId = entry.id;
155
+ } catch {
156
+ // skip files that fail to parse
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Check whether the required WASM grammar files exist on disk.
163
+ */
164
+ export function isWasmAvailable() {
165
+ return LANGUAGE_REGISTRY.filter((e) => e.required).every((e) =>
166
+ fs.existsSync(grammarPath(e.grammarFile)),
167
+ );
168
+ }
169
+
107
170
  // ── Unified API ──────────────────────────────────────────────────────────────
108
171
 
109
172
  function resolveEngine(opts = {}) {
@@ -183,6 +246,13 @@ function normalizeNativeSymbols(result) {
183
246
  kind: e.kind,
184
247
  line: e.line,
185
248
  })),
249
+ astNodes: (result.astNodes ?? result.ast_nodes ?? []).map((n) => ({
250
+ kind: n.kind,
251
+ name: n.name,
252
+ line: n.line,
253
+ text: n.text ?? null,
254
+ receiver: n.receiver ?? null,
255
+ })),
186
256
  };
187
257
  }
188
258
 
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) {