@optave/codegraph 3.1.1 → 3.1.3

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 (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/ast.js CHANGED
@@ -9,12 +9,13 @@
9
9
  import path from 'node:path';
10
10
  import { AST_TYPE_MAPS } from './ast-analysis/rules/index.js';
11
11
  import { buildExtensionSet } from './ast-analysis/shared.js';
12
- import { openReadonlyOrFail } from './db.js';
12
+ import { walkWithVisitors } from './ast-analysis/visitor.js';
13
+ import { createAstStoreVisitor } from './ast-analysis/visitors/ast-store-visitor.js';
14
+ import { bulkNodeIdsByFile, openReadonlyOrFail } from './db.js';
15
+ import { outputResult } from './infrastructure/result-formatter.js';
13
16
  import { debug } from './logger.js';
14
17
  import { paginateResult } from './paginate.js';
15
18
 
16
- import { outputResult } from './result-formatter.js';
17
-
18
19
  // ─── Constants ────────────────────────────────────────────────────────
19
20
 
20
21
  export const AST_NODE_KINDS = ['call', 'new', 'string', 'regex', 'throw', 'await'];
@@ -28,9 +29,6 @@ const KIND_ICONS = {
28
29
  await: '\u22B3', // ⊳
29
30
  };
30
31
 
31
- /** Max length for the `text` column. */
32
- const TEXT_MAX = 200;
33
-
34
32
  /** tree-sitter node types that map to our AST node kinds — imported from rules. */
35
33
  const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
36
34
 
@@ -38,77 +36,8 @@ const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
38
36
  const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
39
37
 
40
38
  // ─── Helpers ──────────────────────────────────────────────────────────
41
-
42
- function truncate(s, max = TEXT_MAX) {
43
- if (!s) return null;
44
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
45
- }
46
-
47
- /**
48
- * Extract the constructor name from a `new_expression` node.
49
- * Handles `new Foo()`, `new a.Foo()`, `new Foo.Bar()`.
50
- */
51
- function extractNewName(node) {
52
- for (let i = 0; i < node.childCount; i++) {
53
- const child = node.child(i);
54
- if (child.type === 'identifier') return child.text;
55
- if (child.type === 'member_expression') {
56
- // e.g. new a.Foo() → "a.Foo"
57
- return child.text;
58
- }
59
- }
60
- return node.text?.split('(')[0]?.replace('new ', '').trim() || '?';
61
- }
62
-
63
- /**
64
- * Extract the expression text from a throw/await node.
65
- */
66
- function extractExpressionText(node) {
67
- // Skip keyword child, take the rest
68
- for (let i = 0; i < node.childCount; i++) {
69
- const child = node.child(i);
70
- if (child.type !== 'throw' && child.type !== 'await') {
71
- return truncate(child.text);
72
- }
73
- }
74
- return truncate(node.text);
75
- }
76
-
77
- /**
78
- * Extract a meaningful name from throw/await nodes.
79
- * For throw: the constructor or expression type.
80
- * For await: the called function name.
81
- */
82
- function extractName(kind, node) {
83
- if (kind === 'throw') {
84
- // throw new Error(...) → "Error"; throw x → "x"
85
- for (let i = 0; i < node.childCount; i++) {
86
- const child = node.child(i);
87
- if (child.type === 'new_expression') return extractNewName(child);
88
- if (child.type === 'call_expression') {
89
- const fn = child.childForFieldName('function');
90
- return fn ? fn.text : child.text?.split('(')[0] || '?';
91
- }
92
- if (child.type === 'identifier') return child.text;
93
- }
94
- return truncate(node.text);
95
- }
96
- if (kind === 'await') {
97
- // await fetch(...) → "fetch"; await this.foo() → "this.foo"
98
- for (let i = 0; i < node.childCount; i++) {
99
- const child = node.child(i);
100
- if (child.type === 'call_expression') {
101
- const fn = child.childForFieldName('function');
102
- return fn ? fn.text : child.text?.split('(')[0] || '?';
103
- }
104
- if (child.type === 'identifier' || child.type === 'member_expression') {
105
- return child.text;
106
- }
107
- }
108
- return truncate(node.text);
109
- }
110
- return truncate(node.text);
111
- }
39
+ // Node extraction helpers (extractNewName, extractName, etc.) moved to
40
+ // ast-analysis/visitors/ast-store-visitor.js as part of the visitor framework.
112
41
 
113
42
  /**
114
43
  * Find the narrowest enclosing definition for a given line.
@@ -147,9 +76,6 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
147
76
  return;
148
77
  }
149
78
 
150
- // Bulk-fetch all node IDs per file (replaces per-def getNodeId calls)
151
- const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
152
-
153
79
  const tx = db.transaction((rows) => {
154
80
  for (const r of rows) {
155
81
  insertStmt.run(r.file, r.line, r.kind, r.name, r.text, r.receiver, r.parentNodeId);
@@ -163,7 +89,7 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
163
89
 
164
90
  // Pre-load all node IDs for this file into a map (read-only, fast)
165
91
  const nodeIdMap = new Map();
166
- for (const row of bulkGetNodeIds.all(relPath)) {
92
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
167
93
  nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
168
94
  }
169
95
 
@@ -228,66 +154,13 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
228
154
 
229
155
  /**
230
156
  * Walk a tree-sitter AST and collect new/throw/await/string/regex nodes.
157
+ * Delegates to the ast-store visitor via the unified walker.
231
158
  */
232
- function walkAst(node, defs, relPath, rows, nodeIdMap) {
233
- const kind = JS_TS_AST_TYPES[node.type];
234
- if (kind) {
235
- // tree-sitter lines are 0-indexed, our DB uses 1-indexed
236
- const line = node.startPosition.row + 1;
237
-
238
- let name;
239
- let text = null;
240
-
241
- if (kind === 'new') {
242
- name = extractNewName(node);
243
- text = truncate(node.text);
244
- } else if (kind === 'throw') {
245
- name = extractName('throw', node);
246
- text = extractExpressionText(node);
247
- } else if (kind === 'await') {
248
- name = extractName('await', node);
249
- text = extractExpressionText(node);
250
- } else if (kind === 'string') {
251
- // Skip trivial strings (length < 2 after removing quotes)
252
- const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
253
- if (content.length < 2) {
254
- // Still recurse children
255
- for (let i = 0; i < node.childCount; i++) {
256
- walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
257
- }
258
- return;
259
- }
260
- name = truncate(content, 100);
261
- text = truncate(node.text);
262
- } else if (kind === 'regex') {
263
- name = node.text || '?';
264
- text = truncate(node.text);
265
- }
266
-
267
- const parentDef = findParentDef(defs, line);
268
- let parentNodeId = null;
269
- if (parentDef) {
270
- parentNodeId = nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
271
- }
272
-
273
- rows.push({
274
- file: relPath,
275
- line,
276
- kind,
277
- name,
278
- text,
279
- receiver: null,
280
- parentNodeId,
281
- });
282
-
283
- // Don't recurse into the children of matched nodes for new/throw/await
284
- // (we already extracted what we need, and nested strings inside them are noise)
285
- if (kind !== 'string' && kind !== 'regex') return;
286
- }
287
-
288
- for (let i = 0; i < node.childCount; i++) {
289
- walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
290
- }
159
+ function walkAst(rootNode, defs, relPath, rows, nodeIdMap) {
160
+ const visitor = createAstStoreVisitor(JS_TS_AST_TYPES, defs, relPath, nodeIdMap);
161
+ const results = walkWithVisitors(rootNode, [visitor], 'javascript');
162
+ const collected = results['ast-store'] || [];
163
+ rows.push(...collected);
291
164
  }
292
165
 
293
166
  // ─── Query ────────────────────────────────────────────────────────────
package/src/audit.js CHANGED
@@ -9,10 +9,9 @@
9
9
  import path from 'node:path';
10
10
  import { loadConfig } from './config.js';
11
11
  import { openReadonlyOrFail } from './db.js';
12
+ import { isTestFile } from './infrastructure/test-filter.js';
12
13
  import { RULE_DEFS } from './manifesto.js';
13
- import { explainData, kindIcon } from './queries.js';
14
- import { outputResult } from './result-formatter.js';
15
- import { isTestFile } from './test-filter.js';
14
+ import { explainData } from './queries.js';
16
15
 
17
16
  // ─── Threshold resolution ───────────────────────────────────────────
18
17
 
@@ -336,87 +335,3 @@ function defaultHealth() {
336
335
  thresholdBreaches: [],
337
336
  };
338
337
  }
339
-
340
- // ─── CLI formatter ──────────────────────────────────────────────────
341
-
342
- export function audit(target, customDbPath, opts = {}) {
343
- const data = auditData(target, customDbPath, opts);
344
-
345
- if (outputResult(data, null, opts)) return;
346
-
347
- if (data.functions.length === 0) {
348
- console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
349
- return;
350
- }
351
-
352
- console.log(`\n# Audit: ${target} (${data.kind})`);
353
- console.log(` ${data.functions.length} function(s) analyzed\n`);
354
-
355
- for (const fn of data.functions) {
356
- const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
357
- const roleTag = fn.role ? ` [${fn.role}]` : '';
358
- console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
359
- console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
360
- if (fn.summary) console.log(` ${fn.summary}`);
361
- if (fn.signature) {
362
- if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
363
- if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
364
- }
365
-
366
- // Health metrics
367
- if (fn.health.cognitive != null) {
368
- console.log(`\n Health:`);
369
- console.log(
370
- ` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
371
- );
372
- console.log(` MI: ${fn.health.maintainabilityIndex}`);
373
- if (fn.health.halstead.volume) {
374
- console.log(
375
- ` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
376
- );
377
- }
378
- if (fn.health.loc) {
379
- console.log(
380
- ` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
381
- );
382
- }
383
- }
384
-
385
- // Threshold breaches
386
- if (fn.health.thresholdBreaches.length > 0) {
387
- console.log(`\n Threshold Breaches:`);
388
- for (const b of fn.health.thresholdBreaches) {
389
- const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
390
- console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
391
- }
392
- }
393
-
394
- // Impact
395
- console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
396
- for (const [level, nodes] of Object.entries(fn.impact.levels)) {
397
- console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
398
- }
399
-
400
- // Call edges
401
- if (fn.callees.length > 0) {
402
- console.log(`\n Calls (${fn.callees.length}):`);
403
- for (const c of fn.callees) {
404
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
405
- }
406
- }
407
- if (fn.callers.length > 0) {
408
- console.log(`\n Called by (${fn.callers.length}):`);
409
- for (const c of fn.callers) {
410
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
411
- }
412
- }
413
- if (fn.relatedTests.length > 0) {
414
- console.log(`\n Tests (${fn.relatedTests.length}):`);
415
- for (const t of fn.relatedTests) {
416
- console.log(` ${t.file}`);
417
- }
418
- }
419
-
420
- console.log();
421
- }
422
- }
package/src/batch.js CHANGED
@@ -83,14 +83,6 @@ export function batchData(command, targets, customDbPath, opts = {}) {
83
83
  return { command, total: targets.length, succeeded, failed, results };
84
84
  }
85
85
 
86
- /**
87
- * CLI wrapper — calls batchData and prints JSON to stdout.
88
- */
89
- export function batch(command, targets, customDbPath, opts = {}) {
90
- const data = batchData(command, targets, customDbPath, opts);
91
- console.log(JSON.stringify(data, null, 2));
92
- }
93
-
94
86
  /**
95
87
  * Expand comma-separated positional args into individual entries.
96
88
  * `['a,b', 'c']` → `['a', 'b', 'c']`.
@@ -161,20 +153,3 @@ export function multiBatchData(items, customDbPath, sharedOpts = {}) {
161
153
 
162
154
  return { mode: 'multi', total: items.length, succeeded, failed, results };
163
155
  }
164
-
165
- /**
166
- * CLI wrapper for batch-query — detects multi-command mode (objects with .command)
167
- * or falls back to single-command batchData (default: 'where').
168
- */
169
- export function batchQuery(targets, customDbPath, opts = {}) {
170
- const { command: defaultCommand = 'where', ...rest } = opts;
171
- const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
172
-
173
- let data;
174
- if (isMulti) {
175
- data = multiBatchData(targets, customDbPath, rest);
176
- } else {
177
- data = batchData(defaultCommand, targets, customDbPath, rest);
178
- }
179
- console.log(JSON.stringify(data, null, 2));
180
- }
package/src/boundaries.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { isTestFile } from './infrastructure/test-filter.js';
1
2
  import { debug } from './logger.js';
2
- import { isTestFile } from './test-filter.js';
3
3
 
4
4
  // ─── Glob-to-Regex ───────────────────────────────────────────────────
5
5
 
@@ -12,9 +12,8 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import Database from 'better-sqlite3';
14
14
  import { buildGraph } from './builder.js';
15
+ import { isTestFile } from './infrastructure/test-filter.js';
15
16
  import { kindIcon } from './queries.js';
16
- import { outputResult } from './result-formatter.js';
17
- import { isTestFile } from './test-filter.js';
18
17
 
19
18
  // ─── Git Helpers ────────────────────────────────────────────────────────
20
19
 
@@ -477,97 +476,3 @@ export function branchCompareMermaid(data) {
477
476
 
478
477
  return lines.join('\n');
479
478
  }
480
-
481
- // ─── Text Formatting ────────────────────────────────────────────────────
482
-
483
- function formatText(data) {
484
- if (data.error) return `Error: ${data.error}`;
485
-
486
- const lines = [];
487
- const shortBase = data.baseSha.slice(0, 7);
488
- const shortTarget = data.targetSha.slice(0, 7);
489
-
490
- lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
491
- lines.push(` Base: ${data.baseRef} (${shortBase})`);
492
- lines.push(` Target: ${data.targetRef} (${shortTarget})`);
493
- lines.push(` Files changed: ${data.changedFiles.length}`);
494
-
495
- if (data.added.length > 0) {
496
- lines.push('');
497
- lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
498
- for (const sym of data.added) {
499
- lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
500
- }
501
- }
502
-
503
- if (data.removed.length > 0) {
504
- lines.push('');
505
- lines.push(
506
- ` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
507
- );
508
- for (const sym of data.removed) {
509
- lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
510
- if (sym.impact && sym.impact.length > 0) {
511
- lines.push(
512
- ` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
513
- );
514
- }
515
- }
516
- }
517
-
518
- if (data.changed.length > 0) {
519
- lines.push('');
520
- lines.push(
521
- ` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
522
- );
523
- for (const sym of data.changed) {
524
- const parts = [];
525
- if (sym.changes.lineCount !== 0) {
526
- parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
527
- }
528
- if (sym.changes.fanIn !== 0) {
529
- parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
530
- }
531
- if (sym.changes.fanOut !== 0) {
532
- parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
533
- }
534
- const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
535
- lines.push(
536
- ` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
537
- );
538
- if (sym.impact && sym.impact.length > 0) {
539
- lines.push(
540
- ` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
541
- );
542
- }
543
- }
544
- }
545
-
546
- const s = data.summary;
547
- lines.push('');
548
- lines.push(
549
- ` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
550
- ` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
551
- (s.filesAffected > 0
552
- ? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
553
- : ''),
554
- );
555
-
556
- return lines.join('\n');
557
- }
558
-
559
- // ─── CLI Display Function ───────────────────────────────────────────────
560
-
561
- export async function branchCompare(baseRef, targetRef, opts = {}) {
562
- const data = await branchCompareData(baseRef, targetRef, opts);
563
-
564
- if (opts.format === 'json') opts = { ...opts, json: true };
565
- if (outputResult(data, null, opts)) return;
566
-
567
- if (opts.format === 'mermaid') {
568
- console.log(branchCompareMermaid(data));
569
- return;
570
- }
571
-
572
- console.log(formatText(data));
573
- }