@optave/codegraph 3.1.0 → 3.1.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 (83) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. package/src/watcher.js +7 -4
package/src/sequence.js CHANGED
@@ -6,9 +6,10 @@
6
6
  * sequence-diagram conventions.
7
7
  */
8
8
 
9
- import { openReadonlyOrFail } from './db.js';
10
- import { paginateResult, printNdjson } from './paginate.js';
11
- import { findMatchingNodes, isTestFile, kindIcon } from './queries.js';
9
+ import { findCallees, openReadonlyOrFail } from './db.js';
10
+ import { isTestFile } from './infrastructure/test-filter.js';
11
+ import { paginateResult } from './paginate.js';
12
+ import { findMatchingNodes } from './queries.js';
12
13
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
13
14
 
14
15
  // ─── Alias generation ────────────────────────────────────────────────
@@ -85,208 +86,203 @@ function buildAliases(files) {
85
86
  */
86
87
  export function sequenceData(name, dbPath, opts = {}) {
87
88
  const db = openReadonlyOrFail(dbPath);
88
- const maxDepth = opts.depth || 10;
89
- const noTests = opts.noTests || false;
90
- const withDataflow = opts.dataflow || false;
91
-
92
- // Phase 1: Direct LIKE match
93
- let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
94
-
95
- // Phase 2: Prefix-stripped matching
96
- if (!matchNode) {
97
- for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
98
- matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
99
- if (matchNode) break;
100
- }
101
- }
102
-
103
- if (!matchNode) {
104
- db.close();
105
- return {
106
- entry: null,
107
- participants: [],
108
- messages: [],
109
- depth: maxDepth,
110
- totalMessages: 0,
111
- truncated: false,
112
- };
113
- }
114
-
115
- const entry = {
116
- name: matchNode.name,
117
- file: matchNode.file,
118
- kind: matchNode.kind,
119
- line: matchNode.line,
120
- };
121
-
122
- // BFS forward — track edges, not just nodes
123
- const visited = new Set([matchNode.id]);
124
- let frontier = [matchNode.id];
125
- const messages = [];
126
- const fileSet = new Set([matchNode.file]);
127
- const idToNode = new Map();
128
- idToNode.set(matchNode.id, matchNode);
129
- let truncated = false;
130
-
131
- const getCallees = db.prepare(
132
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
133
- FROM edges e JOIN nodes n ON e.target_id = n.id
134
- WHERE e.source_id = ? AND e.kind = 'calls'`,
135
- );
136
-
137
- for (let d = 1; d <= maxDepth; d++) {
138
- const nextFrontier = [];
139
-
140
- for (const fid of frontier) {
141
- const callees = getCallees.all(fid);
142
-
143
- const caller = idToNode.get(fid);
144
-
145
- for (const c of callees) {
146
- if (noTests && isTestFile(c.file)) continue;
147
-
148
- // Always record the message (even for visited nodes — different caller path)
149
- fileSet.add(c.file);
150
- messages.push({
151
- from: caller.file,
152
- to: c.file,
153
- label: c.name,
154
- type: 'call',
155
- depth: d,
156
- });
157
-
158
- if (visited.has(c.id)) continue;
159
-
160
- visited.add(c.id);
161
- nextFrontier.push(c.id);
162
- idToNode.set(c.id, c);
89
+ try {
90
+ const maxDepth = opts.depth || 10;
91
+ const noTests = opts.noTests || false;
92
+ const withDataflow = opts.dataflow || false;
93
+
94
+ // Phase 1: Direct LIKE match
95
+ let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
96
+
97
+ // Phase 2: Prefix-stripped matching
98
+ if (!matchNode) {
99
+ for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
100
+ matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
101
+ if (matchNode) break;
163
102
  }
164
103
  }
165
104
 
166
- frontier = nextFrontier;
167
- if (frontier.length === 0) break;
168
-
169
- if (d === maxDepth && frontier.length > 0) {
170
- // Only mark truncated if at least one frontier node has further callees
171
- const hasMoreCalls = frontier.some((fid) => getCallees.all(fid).length > 0);
172
- if (hasMoreCalls) truncated = true;
105
+ if (!matchNode) {
106
+ return {
107
+ entry: null,
108
+ participants: [],
109
+ messages: [],
110
+ depth: maxDepth,
111
+ totalMessages: 0,
112
+ truncated: false,
113
+ };
173
114
  }
174
- }
175
115
 
176
- // Dataflow annotations: add return arrows
177
- if (withDataflow && messages.length > 0) {
178
- const hasTable = db
179
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
180
- .get();
181
-
182
- if (hasTable) {
183
- // Build name|file lookup for O(1) target node access
184
- const nodeByNameFile = new Map();
185
- for (const n of idToNode.values()) {
186
- nodeByNameFile.set(`${n.name}|${n.file}`, n);
187
- }
116
+ const entry = {
117
+ name: matchNode.name,
118
+ file: matchNode.file,
119
+ kind: matchNode.kind,
120
+ line: matchNode.line,
121
+ };
188
122
 
189
- const getReturns = db.prepare(
190
- `SELECT d.expression FROM dataflow d
191
- WHERE d.source_id = ? AND d.kind = 'returns'`,
192
- );
193
- const getFlowsTo = db.prepare(
194
- `SELECT d.expression FROM dataflow d
195
- WHERE d.target_id = ? AND d.kind = 'flows_to'
196
- ORDER BY d.param_index`,
197
- );
123
+ // BFS forward — track edges, not just nodes
124
+ const visited = new Set([matchNode.id]);
125
+ let frontier = [matchNode.id];
126
+ const messages = [];
127
+ const fileSet = new Set([matchNode.file]);
128
+ const idToNode = new Map();
129
+ idToNode.set(matchNode.id, matchNode);
130
+ let truncated = false;
131
+
132
+ for (let d = 1; d <= maxDepth; d++) {
133
+ const nextFrontier = [];
198
134
 
199
- // For each called function, check if it has return edges
200
- const seenReturns = new Set();
201
- for (const msg of [...messages]) {
202
- if (msg.type !== 'call') continue;
203
- const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
204
- if (!targetNode) continue;
135
+ for (const fid of frontier) {
136
+ const callees = findCallees(db, fid);
205
137
 
206
- const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
207
- if (seenReturns.has(returnKey)) continue;
138
+ const caller = idToNode.get(fid);
208
139
 
209
- const returns = getReturns.all(targetNode.id);
140
+ for (const c of callees) {
141
+ if (noTests && isTestFile(c.file)) continue;
210
142
 
211
- if (returns.length > 0) {
212
- seenReturns.add(returnKey);
213
- const expr = returns[0].expression || 'result';
143
+ // Always record the message (even for visited nodes — different caller path)
144
+ fileSet.add(c.file);
214
145
  messages.push({
215
- from: msg.to,
216
- to: msg.from,
217
- label: expr,
218
- type: 'return',
219
- depth: msg.depth,
146
+ from: caller.file,
147
+ to: c.file,
148
+ label: c.name,
149
+ type: 'call',
150
+ depth: d,
220
151
  });
152
+
153
+ if (visited.has(c.id)) continue;
154
+
155
+ visited.add(c.id);
156
+ nextFrontier.push(c.id);
157
+ idToNode.set(c.id, c);
221
158
  }
222
159
  }
223
160
 
224
- // Annotate call messages with parameter names
225
- for (const msg of messages) {
226
- if (msg.type !== 'call') continue;
227
- const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
228
- if (!targetNode) continue;
229
-
230
- const params = getFlowsTo.all(targetNode.id);
231
-
232
- if (params.length > 0) {
233
- const paramNames = params
234
- .map((p) => p.expression)
235
- .filter(Boolean)
236
- .slice(0, 3);
237
- if (paramNames.length > 0) {
238
- msg.label = `${msg.label}(${paramNames.join(', ')})`;
161
+ frontier = nextFrontier;
162
+ if (frontier.length === 0) break;
163
+
164
+ if (d === maxDepth && frontier.length > 0) {
165
+ // Only mark truncated if at least one frontier node has further callees
166
+ const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
167
+ if (hasMoreCalls) truncated = true;
168
+ }
169
+ }
170
+
171
+ // Dataflow annotations: add return arrows
172
+ if (withDataflow && messages.length > 0) {
173
+ const hasTable = db
174
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
175
+ .get();
176
+
177
+ if (hasTable) {
178
+ // Build name|file lookup for O(1) target node access
179
+ const nodeByNameFile = new Map();
180
+ for (const n of idToNode.values()) {
181
+ nodeByNameFile.set(`${n.name}|${n.file}`, n);
182
+ }
183
+
184
+ const getReturns = db.prepare(
185
+ `SELECT d.expression FROM dataflow d
186
+ WHERE d.source_id = ? AND d.kind = 'returns'`,
187
+ );
188
+ const getFlowsTo = db.prepare(
189
+ `SELECT d.expression FROM dataflow d
190
+ WHERE d.target_id = ? AND d.kind = 'flows_to'
191
+ ORDER BY d.param_index`,
192
+ );
193
+
194
+ // For each called function, check if it has return edges
195
+ const seenReturns = new Set();
196
+ for (const msg of [...messages]) {
197
+ if (msg.type !== 'call') continue;
198
+ const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
199
+ if (!targetNode) continue;
200
+
201
+ const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
202
+ if (seenReturns.has(returnKey)) continue;
203
+
204
+ const returns = getReturns.all(targetNode.id);
205
+
206
+ if (returns.length > 0) {
207
+ seenReturns.add(returnKey);
208
+ const expr = returns[0].expression || 'result';
209
+ messages.push({
210
+ from: msg.to,
211
+ to: msg.from,
212
+ label: expr,
213
+ type: 'return',
214
+ depth: msg.depth,
215
+ });
216
+ }
217
+ }
218
+
219
+ // Annotate call messages with parameter names
220
+ for (const msg of messages) {
221
+ if (msg.type !== 'call') continue;
222
+ const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
223
+ if (!targetNode) continue;
224
+
225
+ const params = getFlowsTo.all(targetNode.id);
226
+
227
+ if (params.length > 0) {
228
+ const paramNames = params
229
+ .map((p) => p.expression)
230
+ .filter(Boolean)
231
+ .slice(0, 3);
232
+ if (paramNames.length > 0) {
233
+ msg.label = `${msg.label}(${paramNames.join(', ')})`;
234
+ }
239
235
  }
240
236
  }
241
237
  }
242
238
  }
243
- }
244
239
 
245
- // Sort messages by depth, then call before return
246
- messages.sort((a, b) => {
247
- if (a.depth !== b.depth) return a.depth - b.depth;
248
- if (a.type === 'call' && b.type === 'return') return -1;
249
- if (a.type === 'return' && b.type === 'call') return 1;
250
- return 0;
251
- });
252
-
253
- // Build participant list from files
254
- const aliases = buildAliases([...fileSet]);
255
- const participants = [...fileSet].map((file) => ({
256
- id: aliases.get(file),
257
- label: file.split('/').pop(),
258
- file,
259
- }));
260
-
261
- // Sort participants: entry file first, then alphabetically
262
- participants.sort((a, b) => {
263
- if (a.file === entry.file) return -1;
264
- if (b.file === entry.file) return 1;
265
- return a.file.localeCompare(b.file);
266
- });
267
-
268
- // Replace file paths with alias IDs in messages
269
- for (const msg of messages) {
270
- msg.from = aliases.get(msg.from);
271
- msg.to = aliases.get(msg.to);
272
- }
240
+ // Sort messages by depth, then call before return
241
+ messages.sort((a, b) => {
242
+ if (a.depth !== b.depth) return a.depth - b.depth;
243
+ if (a.type === 'call' && b.type === 'return') return -1;
244
+ if (a.type === 'return' && b.type === 'call') return 1;
245
+ return 0;
246
+ });
247
+
248
+ // Build participant list from files
249
+ const aliases = buildAliases([...fileSet]);
250
+ const participants = [...fileSet].map((file) => ({
251
+ id: aliases.get(file),
252
+ label: file.split('/').pop(),
253
+ file,
254
+ }));
255
+
256
+ // Sort participants: entry file first, then alphabetically
257
+ participants.sort((a, b) => {
258
+ if (a.file === entry.file) return -1;
259
+ if (b.file === entry.file) return 1;
260
+ return a.file.localeCompare(b.file);
261
+ });
262
+
263
+ // Replace file paths with alias IDs in messages
264
+ for (const msg of messages) {
265
+ msg.from = aliases.get(msg.from);
266
+ msg.to = aliases.get(msg.to);
267
+ }
273
268
 
274
- db.close();
275
-
276
- const base = {
277
- entry,
278
- participants,
279
- messages,
280
- depth: maxDepth,
281
- totalMessages: messages.length,
282
- truncated,
283
- };
284
- const result = paginateResult(base, 'messages', { limit: opts.limit, offset: opts.offset });
285
- if (opts.limit !== undefined || opts.offset !== undefined) {
286
- const activeFiles = new Set(result.messages.flatMap((m) => [m.from, m.to]));
287
- result.participants = result.participants.filter((p) => activeFiles.has(p.id));
269
+ const base = {
270
+ entry,
271
+ participants,
272
+ messages,
273
+ depth: maxDepth,
274
+ totalMessages: messages.length,
275
+ truncated,
276
+ };
277
+ const result = paginateResult(base, 'messages', { limit: opts.limit, offset: opts.offset });
278
+ if (opts.limit !== undefined || opts.offset !== undefined) {
279
+ const activeFiles = new Set(result.messages.flatMap((m) => [m.from, m.to]));
280
+ result.participants = result.participants.filter((p) => activeFiles.has(p.id));
281
+ }
282
+ return result;
283
+ } finally {
284
+ db.close();
288
285
  }
289
- return result;
290
286
  }
291
287
 
292
288
  // ─── Mermaid formatter ───────────────────────────────────────────────
@@ -327,43 +323,3 @@ export function sequenceToMermaid(seqResult) {
327
323
 
328
324
  return lines.join('\n');
329
325
  }
330
-
331
- // ─── CLI formatter ───────────────────────────────────────────────────
332
-
333
- /**
334
- * CLI entry point — format sequence data as mermaid, JSON, or ndjson.
335
- */
336
- export function sequence(name, dbPath, opts = {}) {
337
- const data = sequenceData(name, dbPath, opts);
338
-
339
- if (opts.ndjson) {
340
- printNdjson(data, 'messages');
341
- return;
342
- }
343
-
344
- if (opts.json) {
345
- console.log(JSON.stringify(data, null, 2));
346
- return;
347
- }
348
-
349
- // Default: mermaid format
350
- if (!data.entry) {
351
- console.log(`No matching function found for "${name}".`);
352
- return;
353
- }
354
-
355
- const e = data.entry;
356
- console.log(`\nSequence from: [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
357
- console.log(`Participants: ${data.participants.length} Messages: ${data.totalMessages}`);
358
- if (data.truncated) {
359
- console.log(` (truncated at depth ${data.depth})`);
360
- }
361
- console.log();
362
-
363
- if (data.messages.length === 0) {
364
- console.log(' (leaf node — no callees)');
365
- return;
366
- }
367
-
368
- console.log(sequenceToMermaid(data));
369
- }