@optave/codegraph 3.1.1 → 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 (68) hide show
  1. package/package.json +7 -7
  2. package/src/ast-analysis/engine.js +365 -0
  3. package/src/ast-analysis/metrics.js +118 -0
  4. package/src/ast-analysis/visitor-utils.js +176 -0
  5. package/src/ast-analysis/visitor.js +162 -0
  6. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  7. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  8. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  9. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  10. package/src/ast.js +13 -140
  11. package/src/audit.js +2 -87
  12. package/src/batch.js +0 -25
  13. package/src/boundaries.js +1 -1
  14. package/src/branch-compare.js +1 -96
  15. package/src/builder.js +48 -179
  16. package/src/cfg.js +89 -883
  17. package/src/check.js +1 -84
  18. package/src/cli.js +20 -19
  19. package/src/cochange.js +1 -39
  20. package/src/commands/audit.js +88 -0
  21. package/src/commands/batch.js +26 -0
  22. package/src/commands/branch-compare.js +97 -0
  23. package/src/commands/cfg.js +55 -0
  24. package/src/commands/check.js +82 -0
  25. package/src/commands/cochange.js +37 -0
  26. package/src/commands/communities.js +69 -0
  27. package/src/commands/complexity.js +77 -0
  28. package/src/commands/dataflow.js +110 -0
  29. package/src/commands/flow.js +70 -0
  30. package/src/commands/manifesto.js +77 -0
  31. package/src/commands/owners.js +52 -0
  32. package/src/commands/query.js +21 -0
  33. package/src/commands/sequence.js +33 -0
  34. package/src/commands/structure.js +64 -0
  35. package/src/commands/triage.js +49 -0
  36. package/src/communities.js +12 -83
  37. package/src/complexity.js +42 -356
  38. package/src/cycles.js +1 -1
  39. package/src/dataflow.js +12 -665
  40. package/src/db/repository/build-stmts.js +104 -0
  41. package/src/db/repository/cfg.js +83 -0
  42. package/src/db/repository/cochange.js +41 -0
  43. package/src/db/repository/complexity.js +15 -0
  44. package/src/db/repository/dataflow.js +12 -0
  45. package/src/db/repository/edges.js +259 -0
  46. package/src/db/repository/embeddings.js +40 -0
  47. package/src/db/repository/graph-read.js +39 -0
  48. package/src/db/repository/index.js +42 -0
  49. package/src/db/repository/nodes.js +236 -0
  50. package/src/db.js +40 -1
  51. package/src/embedder.js +14 -34
  52. package/src/export.js +1 -1
  53. package/src/extractors/javascript.js +130 -5
  54. package/src/flow.js +2 -70
  55. package/src/index.js +23 -19
  56. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  57. package/src/kinds.js +1 -0
  58. package/src/manifesto.js +0 -76
  59. package/src/owners.js +1 -56
  60. package/src/queries-cli.js +1 -1
  61. package/src/queries.js +79 -280
  62. package/src/sequence.js +5 -44
  63. package/src/structure.js +16 -75
  64. package/src/triage.js +1 -54
  65. package/src/viewer.js +1 -1
  66. package/src/watcher.js +7 -4
  67. package/src/db/repository.js +0 -134
  68. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/structure.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { normalizePath } from './constants.js';
3
- import { openReadonlyOrFail, testFilterSQL } from './db.js';
3
+ import { getNodeId, openReadonlyOrFail, testFilterSQL } from './db.js';
4
+ import { isTestFile } from './infrastructure/test-filter.js';
4
5
  import { debug } from './logger.js';
5
6
  import { paginateResult } from './paginate.js';
6
- import { isTestFile } from './test-filter.js';
7
7
 
8
8
  // ─── Build-time: insert directory nodes, contains edges, and metrics ────
9
9
 
@@ -21,9 +21,12 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
21
21
  const insertNode = db.prepare(
22
22
  'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
23
23
  );
24
- const getNodeId = db.prepare(
25
- 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
26
- );
24
+ const getNodeIdStmt = {
25
+ get: (name, kind, file, line) => {
26
+ const id = getNodeId(db, name, kind, file, line);
27
+ return id != null ? { id } : undefined;
28
+ },
29
+ };
27
30
  const insertEdge = db.prepare(
28
31
  'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
29
32
  );
@@ -56,12 +59,12 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
56
59
  }
57
60
  // Delete metrics for changed files
58
61
  for (const f of changedFiles) {
59
- const fileRow = getNodeId.get(f, 'file', f, 0);
62
+ const fileRow = getNodeIdStmt.get(f, 'file', f, 0);
60
63
  if (fileRow) deleteMetricForNode.run(fileRow.id);
61
64
  }
62
65
  // Delete metrics for affected directories
63
66
  for (const dir of affectedDirs) {
64
- const dirRow = getNodeId.get(dir, 'directory', dir, 0);
67
+ const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
65
68
  if (dirRow) deleteMetricForNode.run(dirRow.id);
66
69
  }
67
70
  })();
@@ -126,8 +129,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
126
129
  if (!dir || dir === '.') continue;
127
130
  // On incremental, skip dirs whose contains edges are intact
128
131
  if (affectedDirs && !affectedDirs.has(dir)) continue;
129
- const dirRow = getNodeId.get(dir, 'directory', dir, 0);
130
- const fileRow = getNodeId.get(relPath, 'file', relPath, 0);
132
+ const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
133
+ const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
131
134
  if (dirRow && fileRow) {
132
135
  insertEdge.run(dirRow.id, fileRow.id, 'contains', 1.0, 0);
133
136
  }
@@ -138,8 +141,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
138
141
  if (!parent || parent === '.' || parent === dir) continue;
139
142
  // On incremental, skip parent dirs whose contains edges are intact
140
143
  if (affectedDirs && !affectedDirs.has(parent)) continue;
141
- const parentRow = getNodeId.get(parent, 'directory', parent, 0);
142
- const childRow = getNodeId.get(dir, 'directory', dir, 0);
144
+ const parentRow = getNodeIdStmt.get(parent, 'directory', parent, 0);
145
+ const childRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
143
146
  if (parentRow && childRow) {
144
147
  insertEdge.run(parentRow.id, childRow.id, 'contains', 1.0, 0);
145
148
  }
@@ -169,7 +172,7 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
169
172
 
170
173
  const computeFileMetrics = db.transaction(() => {
171
174
  for (const [relPath, symbols] of fileSymbols) {
172
- const fileRow = getNodeId.get(relPath, 'file', relPath, 0);
175
+ const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
173
176
  if (!fileRow) continue;
174
177
 
175
178
  const lineCount = lineCountMap.get(relPath) || 0;
@@ -263,7 +266,7 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
263
266
 
264
267
  const computeDirMetrics = db.transaction(() => {
265
268
  for (const [dir, files] of dirFiles) {
266
- const dirRow = getNodeId.get(dir, 'directory', dir, 0);
269
+ const dirRow = getNodeIdStmt.get(dir, 'directory', dir, 0);
267
270
  if (!dirRow) continue;
268
271
 
269
272
  const fileCount = files.length;
@@ -640,68 +643,6 @@ export function moduleBoundariesData(customDbPath, opts = {}) {
640
643
  }
641
644
  }
642
645
 
643
- // ─── Formatters ───────────────────────────────────────────────────────
644
-
645
- export function formatStructure(data) {
646
- if (data.count === 0) return 'No directory structure found. Run "codegraph build" first.';
647
-
648
- const lines = [`\nProject structure (${data.count} directories):\n`];
649
- for (const d of data.directories) {
650
- const cohStr = d.cohesion !== null ? ` cohesion=${d.cohesion.toFixed(2)}` : '';
651
- const depth = d.directory.split('/').length - 1;
652
- const indent = ' '.repeat(depth);
653
- lines.push(
654
- `${indent}${d.directory}/ (${d.fileCount} files, ${d.symbolCount} symbols, <-${d.fanIn} ->${d.fanOut}${cohStr})`,
655
- );
656
- for (const f of d.files) {
657
- lines.push(
658
- `${indent} ${path.basename(f.file)} ${f.lineCount}L ${f.symbolCount}sym <-${f.fanIn} ->${f.fanOut}`,
659
- );
660
- }
661
- }
662
- if (data.warning) {
663
- lines.push('');
664
- lines.push(`⚠ ${data.warning}`);
665
- }
666
- return lines.join('\n');
667
- }
668
-
669
- export function formatHotspots(data) {
670
- if (data.hotspots.length === 0) return 'No hotspots found. Run "codegraph build" first.';
671
-
672
- const lines = [`\nHotspots by ${data.metric} (${data.level}-level, top ${data.limit}):\n`];
673
- let rank = 1;
674
- for (const h of data.hotspots) {
675
- const extra =
676
- h.kind === 'directory'
677
- ? `${h.fileCount} files, cohesion=${h.cohesion !== null ? h.cohesion.toFixed(2) : 'n/a'}`
678
- : `${h.lineCount || 0}L, ${h.symbolCount || 0} symbols`;
679
- lines.push(
680
- ` ${String(rank++).padStart(2)}. ${h.name} <-${h.fanIn || 0} ->${h.fanOut || 0} (${extra})`,
681
- );
682
- }
683
- return lines.join('\n');
684
- }
685
-
686
- export function formatModuleBoundaries(data) {
687
- if (data.count === 0) return `No modules found with cohesion >= ${data.threshold}.`;
688
-
689
- const lines = [`\nModule boundaries (cohesion >= ${data.threshold}, ${data.count} modules):\n`];
690
- for (const m of data.modules) {
691
- lines.push(
692
- ` ${m.directory}/ cohesion=${m.cohesion.toFixed(2)} (${m.fileCount} files, ${m.symbolCount} symbols)`,
693
- );
694
- lines.push(` Incoming: ${m.fanIn} edges Outgoing: ${m.fanOut} edges`);
695
- if (m.files.length > 0) {
696
- lines.push(
697
- ` Files: ${m.files.slice(0, 5).join(', ')}${m.files.length > 5 ? ` ... +${m.files.length - 5}` : ''}`,
698
- );
699
- }
700
- lines.push('');
701
- }
702
- return lines.join('\n');
703
- }
704
-
705
646
  // ─── Helpers ──────────────────────────────────────────────────────────
706
647
 
707
648
  function getSortFn(sortBy) {
package/src/triage.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { findNodesForTriage, openReadonlyOrFail } from './db.js';
2
+ import { isTestFile } from './infrastructure/test-filter.js';
2
3
  import { warn } from './logger.js';
3
4
  import { paginateResult } from './paginate.js';
4
- import { outputResult } from './result-formatter.js';
5
- import { isTestFile } from './test-filter.js';
6
5
 
7
6
  // ─── Constants ────────────────────────────────────────────────────────
8
7
 
@@ -172,58 +171,6 @@ export function triageData(customDbPath, opts = {}) {
172
171
  }
173
172
  }
174
173
 
175
- // ─── CLI Formatter ────────────────────────────────────────────────────
176
-
177
- /**
178
- * Print triage results to console.
179
- *
180
- * @param {string} [customDbPath]
181
- * @param {object} [opts]
182
- */
183
- export function triage(customDbPath, opts = {}) {
184
- const data = triageData(customDbPath, opts);
185
-
186
- if (outputResult(data, 'items', opts)) return;
187
-
188
- if (data.items.length === 0) {
189
- if (data.summary.total === 0) {
190
- console.log('\nNo symbols found. Run "codegraph build" first.\n');
191
- } else {
192
- console.log('\nNo symbols match the given filters.\n');
193
- }
194
- return;
195
- }
196
-
197
- console.log('\n# Risk Audit Queue\n');
198
-
199
- console.log(
200
- ` ${'Symbol'.padEnd(35)} ${'File'.padEnd(28)} ${'Role'.padEnd(8)} ${'Score'.padStart(6)} ${'Fan-In'.padStart(7)} ${'Cog'.padStart(4)} ${'Churn'.padStart(6)} ${'MI'.padStart(5)}`,
201
- );
202
- console.log(
203
- ` ${'─'.repeat(35)} ${'─'.repeat(28)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(7)} ${'─'.repeat(4)} ${'─'.repeat(6)} ${'─'.repeat(5)}`,
204
- );
205
-
206
- for (const it of data.items) {
207
- const name = it.name.length > 33 ? `${it.name.slice(0, 32)}…` : it.name;
208
- const file = it.file.length > 26 ? `…${it.file.slice(-25)}` : it.file;
209
- const role = (it.role || '-').padEnd(8);
210
- const score = it.riskScore.toFixed(2).padStart(6);
211
- const fanIn = String(it.fanIn).padStart(7);
212
- const cog = String(it.cognitive).padStart(4);
213
- const churn = String(it.churn).padStart(6);
214
- const mi = it.maintainabilityIndex > 0 ? String(it.maintainabilityIndex).padStart(5) : ' -';
215
- console.log(
216
- ` ${name.padEnd(35)} ${file.padEnd(28)} ${role} ${score} ${fanIn} ${cog} ${churn} ${mi}`,
217
- );
218
- }
219
-
220
- const s = data.summary;
221
- console.log(
222
- `\n ${s.analyzed} symbols scored (of ${s.total} total) | avg: ${s.avgScore.toFixed(2)} | max: ${s.maxScore.toFixed(2)} | sort: ${opts.sort || 'risk'}`,
223
- );
224
- console.log();
225
- }
226
-
227
174
  // ─── Utilities ────────────────────────────────────────────────────────
228
175
 
229
176
  function round4(n) {
package/src/viewer.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import Graph from 'graphology';
4
4
  import louvain from 'graphology-communities-louvain';
5
- import { isTestFile } from './test-filter.js';
5
+ import { isTestFile } from './infrastructure/test-filter.js';
6
6
 
7
7
  const DEFAULT_MIN_CONFIDENCE = 0.5;
8
8
 
package/src/watcher.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { readFileSafe } from './builder.js';
4
4
  import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
5
5
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
6
- import { closeDb, initSchema, openDb } from './db.js';
6
+ import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from './db.js';
7
7
  import { appendJournalEntries } from './journal.js';
8
8
  import { info, warn } from './logger.js';
9
9
  import { createParseTreeCache, getActiveEngine, parseFileIncremental } from './parser.js';
@@ -185,9 +185,12 @@ export async function watchProject(rootDir, opts = {}) {
185
185
  insertNode: db.prepare(
186
186
  'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
187
187
  ),
188
- getNodeId: db.prepare(
189
- 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
190
- ),
188
+ getNodeId: {
189
+ get: (name, kind, file, line) => {
190
+ const id = getNodeIdQuery(db, name, kind, file, line);
191
+ return id != null ? { id } : undefined;
192
+ },
193
+ },
191
194
  insertEdge: db.prepare(
192
195
  'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
193
196
  ),
@@ -1,134 +0,0 @@
1
- import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../kinds.js';
2
- import { NodeQuery } from './query-builder.js';
3
-
4
- /**
5
- * Find nodes matching a name pattern, with fan-in count.
6
- * Used by findMatchingNodes in queries.js.
7
- *
8
- * @param {object} db - Database instance
9
- * @param {string} namePattern - LIKE pattern (already wrapped with %)
10
- * @param {object} [opts]
11
- * @param {string[]} [opts.kinds] - Node kinds to match
12
- * @param {string} [opts.file] - File filter (partial match)
13
- * @returns {object[]}
14
- */
15
- export function findNodesWithFanIn(db, namePattern, opts = {}) {
16
- const q = new NodeQuery()
17
- .select('n.*, COALESCE(fi.cnt, 0) AS fan_in')
18
- .withFanIn()
19
- .where('n.name LIKE ?', namePattern);
20
-
21
- if (opts.kinds) {
22
- q.kinds(opts.kinds);
23
- }
24
- if (opts.file) {
25
- q.fileFilter(opts.file);
26
- }
27
-
28
- return q.all(db);
29
- }
30
-
31
- /**
32
- * Fetch nodes for triage scoring: fan-in + complexity + churn.
33
- * Used by triageData in triage.js.
34
- *
35
- * @param {object} db
36
- * @param {object} [opts]
37
- * @returns {object[]}
38
- */
39
- export function findNodesForTriage(db, opts = {}) {
40
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
41
- throw new Error(`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`);
42
- }
43
- if (opts.role && !VALID_ROLES.includes(opts.role)) {
44
- throw new Error(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
45
- }
46
-
47
- const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
48
- const q = new NodeQuery()
49
- .select(
50
- `n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
51
- COALESCE(fi.cnt, 0) AS fan_in,
52
- COALESCE(fc.cognitive, 0) AS cognitive,
53
- COALESCE(fc.maintainability_index, 0) AS mi,
54
- COALESCE(fc.cyclomatic, 0) AS cyclomatic,
55
- COALESCE(fc.max_nesting, 0) AS max_nesting,
56
- COALESCE(fcc.commit_count, 0) AS churn`,
57
- )
58
- .kinds(kindsToUse)
59
- .withFanIn()
60
- .withComplexity()
61
- .withChurn()
62
- .excludeTests(opts.noTests)
63
- .fileFilter(opts.file)
64
- .roleFilter(opts.role)
65
- .orderBy('n.file, n.line');
66
-
67
- return q.all(db);
68
- }
69
-
70
- /**
71
- * Shared query builder for function/method/class node listing.
72
- * @param {object} [opts]
73
- * @returns {NodeQuery}
74
- */
75
- function _functionNodeQuery(opts = {}) {
76
- return new NodeQuery()
77
- .select('name, kind, file, line, end_line, role')
78
- .kinds(['function', 'method', 'class'])
79
- .fileFilter(opts.file)
80
- .nameLike(opts.pattern)
81
- .excludeTests(opts.noTests)
82
- .orderBy('file, line');
83
- }
84
-
85
- /**
86
- * List function/method/class nodes with basic info.
87
- * Used by listFunctionsData in queries.js.
88
- *
89
- * @param {object} db
90
- * @param {object} [opts]
91
- * @returns {object[]}
92
- */
93
- export function listFunctionNodes(db, opts = {}) {
94
- return _functionNodeQuery(opts).all(db);
95
- }
96
-
97
- /**
98
- * Iterator version of listFunctionNodes for memory efficiency.
99
- * Used by iterListFunctions in queries.js.
100
- *
101
- * @param {object} db
102
- * @param {object} [opts]
103
- * @returns {IterableIterator}
104
- */
105
- export function iterateFunctionNodes(db, opts = {}) {
106
- return _functionNodeQuery(opts).iterate(db);
107
- }
108
-
109
- /**
110
- * Count total nodes.
111
- * @param {object} db
112
- * @returns {number}
113
- */
114
- export function countNodes(db) {
115
- return db.prepare('SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
116
- }
117
-
118
- /**
119
- * Count total edges.
120
- * @param {object} db
121
- * @returns {number}
122
- */
123
- export function countEdges(db) {
124
- return db.prepare('SELECT COUNT(*) AS cnt FROM edges').get().cnt;
125
- }
126
-
127
- /**
128
- * Count distinct files.
129
- * @param {object} db
130
- * @returns {number}
131
- */
132
- export function countFiles(db) {
133
- return db.prepare('SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
134
- }