@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/triage.js CHANGED
@@ -1,7 +1,7 @@
1
- import { openReadonlyOrFail } from './db.js';
1
+ import { findNodesForTriage, openReadonlyOrFail } from './db.js';
2
+ import { isTestFile } from './infrastructure/test-filter.js';
2
3
  import { warn } from './logger.js';
3
- import { paginateResult, printNdjson } from './paginate.js';
4
- import { isTestFile } from './queries.js';
4
+ import { paginateResult } from './paginate.js';
5
5
 
6
6
  // ─── Constants ────────────────────────────────────────────────────────
7
7
 
@@ -46,224 +46,129 @@ function minMaxNormalize(values) {
46
46
  */
47
47
  export function triageData(customDbPath, opts = {}) {
48
48
  const db = openReadonlyOrFail(customDbPath);
49
- const noTests = opts.noTests || false;
50
- const fileFilter = opts.file || null;
51
- const kindFilter = opts.kind || null;
52
- const roleFilter = opts.role || null;
53
- const minScore = opts.minScore != null ? Number(opts.minScore) : null;
54
- const sort = opts.sort || 'risk';
55
- const weights = { ...DEFAULT_WEIGHTS, ...(opts.weights || {}) };
49
+ try {
50
+ const noTests = opts.noTests || false;
51
+ const fileFilter = opts.file || null;
52
+ const kindFilter = opts.kind || null;
53
+ const roleFilter = opts.role || null;
54
+ const minScore = opts.minScore != null ? Number(opts.minScore) : null;
55
+ const sort = opts.sort || 'risk';
56
+ const weights = { ...DEFAULT_WEIGHTS, ...(opts.weights || {}) };
57
+
58
+ let rows;
59
+ try {
60
+ rows = findNodesForTriage(db, {
61
+ noTests,
62
+ file: fileFilter,
63
+ kind: kindFilter,
64
+ role: roleFilter,
65
+ });
66
+ } catch (err) {
67
+ warn(`triage query failed: ${err.message}`);
68
+ return {
69
+ items: [],
70
+ summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
71
+ };
72
+ }
56
73
 
57
- // Build WHERE clause
58
- let where = "WHERE n.kind IN ('function','method','class')";
59
- const params = [];
74
+ // Post-filter test files (belt-and-suspenders)
75
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
60
76
 
61
- if (noTests) {
62
- where += ` AND n.file NOT LIKE '%.test.%'
63
- AND n.file NOT LIKE '%.spec.%'
64
- AND n.file NOT LIKE '%__test__%'
65
- AND n.file NOT LIKE '%__tests__%'
66
- AND n.file NOT LIKE '%.stories.%'`;
67
- }
68
- if (fileFilter) {
69
- where += ' AND n.file LIKE ?';
70
- params.push(`%${fileFilter}%`);
71
- }
72
- if (kindFilter) {
73
- where += ' AND n.kind = ?';
74
- params.push(kindFilter);
75
- }
76
- if (roleFilter) {
77
- where += ' AND n.role = ?';
78
- params.push(roleFilter);
79
- }
77
+ if (filtered.length === 0) {
78
+ return {
79
+ items: [],
80
+ summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
81
+ };
82
+ }
80
83
 
81
- let rows;
82
- try {
83
- rows = db
84
- .prepare(
85
- `SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
86
- COALESCE(fi.cnt, 0) AS fan_in,
87
- COALESCE(fc.cognitive, 0) AS cognitive,
88
- COALESCE(fc.maintainability_index, 0) AS mi,
89
- COALESCE(fc.cyclomatic, 0) AS cyclomatic,
90
- COALESCE(fc.max_nesting, 0) AS max_nesting,
91
- COALESCE(fcc.commit_count, 0) AS churn
92
- FROM nodes n
93
- LEFT JOIN (SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind='calls' GROUP BY target_id) fi
94
- ON n.id = fi.target_id
95
- LEFT JOIN function_complexity fc ON fc.node_id = n.id
96
- LEFT JOIN file_commit_counts fcc ON n.file = fcc.file
97
- ${where}
98
- ORDER BY n.file, n.line`,
99
- )
100
- .all(...params);
101
- } catch (err) {
102
- warn(`triage query failed: ${err.message}`);
103
- db.close();
104
- return {
105
- items: [],
106
- summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
84
+ // Extract raw signal arrays
85
+ const fanIns = filtered.map((r) => r.fan_in);
86
+ const cognitives = filtered.map((r) => r.cognitive);
87
+ const churns = filtered.map((r) => r.churn);
88
+ const mis = filtered.map((r) => r.mi);
89
+
90
+ // Min-max normalize
91
+ const normFanIns = minMaxNormalize(fanIns);
92
+ const normCognitives = minMaxNormalize(cognitives);
93
+ const normChurns = minMaxNormalize(churns);
94
+ // MI: higher is better, so invert: 1 - norm(mi)
95
+ const normMIsRaw = minMaxNormalize(mis);
96
+ const normMIs = normMIsRaw.map((v) => round4(1 - v));
97
+
98
+ // Compute risk scores
99
+ const items = filtered.map((r, i) => {
100
+ const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
101
+ const riskScore =
102
+ weights.fanIn * normFanIns[i] +
103
+ weights.complexity * normCognitives[i] +
104
+ weights.churn * normChurns[i] +
105
+ weights.role * roleWeight +
106
+ weights.mi * normMIs[i];
107
+
108
+ return {
109
+ name: r.name,
110
+ kind: r.kind,
111
+ file: r.file,
112
+ line: r.line,
113
+ role: r.role || null,
114
+ fanIn: r.fan_in,
115
+ cognitive: r.cognitive,
116
+ churn: r.churn,
117
+ maintainabilityIndex: r.mi,
118
+ normFanIn: round4(normFanIns[i]),
119
+ normComplexity: round4(normCognitives[i]),
120
+ normChurn: round4(normChurns[i]),
121
+ normMI: round4(normMIs[i]),
122
+ roleWeight,
123
+ riskScore: round4(riskScore),
124
+ };
125
+ });
126
+
127
+ // Apply minScore filter
128
+ const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
129
+
130
+ // Sort
131
+ const sortFns = {
132
+ risk: (a, b) => b.riskScore - a.riskScore,
133
+ complexity: (a, b) => b.cognitive - a.cognitive,
134
+ churn: (a, b) => b.churn - a.churn,
135
+ 'fan-in': (a, b) => b.fanIn - a.fanIn,
136
+ mi: (a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
107
137
  };
108
- }
109
-
110
- // Post-filter test files (belt-and-suspenders)
111
- const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
112
-
113
- if (filtered.length === 0) {
114
- db.close();
115
- return {
116
- items: [],
117
- summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
138
+ scored.sort(sortFns[sort] || sortFns.risk);
139
+
140
+ // Signal coverage: % of items with non-zero signal
141
+ const signalCoverage = {
142
+ complexity: round4(filtered.filter((r) => r.cognitive > 0).length / filtered.length),
143
+ churn: round4(filtered.filter((r) => r.churn > 0).length / filtered.length),
144
+ fanIn: round4(filtered.filter((r) => r.fan_in > 0).length / filtered.length),
145
+ mi: round4(filtered.filter((r) => r.mi > 0).length / filtered.length),
118
146
  };
119
- }
120
-
121
- // Extract raw signal arrays
122
- const fanIns = filtered.map((r) => r.fan_in);
123
- const cognitives = filtered.map((r) => r.cognitive);
124
- const churns = filtered.map((r) => r.churn);
125
- const mis = filtered.map((r) => r.mi);
126
-
127
- // Min-max normalize
128
- const normFanIns = minMaxNormalize(fanIns);
129
- const normCognitives = minMaxNormalize(cognitives);
130
- const normChurns = minMaxNormalize(churns);
131
- // MI: higher is better, so invert: 1 - norm(mi)
132
- const normMIsRaw = minMaxNormalize(mis);
133
- const normMIs = normMIsRaw.map((v) => round4(1 - v));
134
147
 
135
- // Compute risk scores
136
- const items = filtered.map((r, i) => {
137
- const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
138
- const riskScore =
139
- weights.fanIn * normFanIns[i] +
140
- weights.complexity * normCognitives[i] +
141
- weights.churn * normChurns[i] +
142
- weights.role * roleWeight +
143
- weights.mi * normMIs[i];
144
-
145
- return {
146
- name: r.name,
147
- kind: r.kind,
148
- file: r.file,
149
- line: r.line,
150
- role: r.role || null,
151
- fanIn: r.fan_in,
152
- cognitive: r.cognitive,
153
- churn: r.churn,
154
- maintainabilityIndex: r.mi,
155
- normFanIn: round4(normFanIns[i]),
156
- normComplexity: round4(normCognitives[i]),
157
- normChurn: round4(normChurns[i]),
158
- normMI: round4(normMIs[i]),
159
- roleWeight,
160
- riskScore: round4(riskScore),
148
+ const scores = scored.map((it) => it.riskScore);
149
+ const avgScore =
150
+ scores.length > 0 ? round4(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;
151
+ const maxScore = scores.length > 0 ? round4(Math.max(...scores)) : 0;
152
+
153
+ const result = {
154
+ items: scored,
155
+ summary: {
156
+ total: filtered.length,
157
+ analyzed: scored.length,
158
+ avgScore,
159
+ maxScore,
160
+ weights,
161
+ signalCoverage,
162
+ },
161
163
  };
162
- });
163
-
164
- // Apply minScore filter
165
- const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
166
-
167
- // Sort
168
- const sortFns = {
169
- risk: (a, b) => b.riskScore - a.riskScore,
170
- complexity: (a, b) => b.cognitive - a.cognitive,
171
- churn: (a, b) => b.churn - a.churn,
172
- 'fan-in': (a, b) => b.fanIn - a.fanIn,
173
- mi: (a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
174
- };
175
- scored.sort(sortFns[sort] || sortFns.risk);
176
-
177
- // Signal coverage: % of items with non-zero signal
178
- const signalCoverage = {
179
- complexity: round4(filtered.filter((r) => r.cognitive > 0).length / filtered.length),
180
- churn: round4(filtered.filter((r) => r.churn > 0).length / filtered.length),
181
- fanIn: round4(filtered.filter((r) => r.fan_in > 0).length / filtered.length),
182
- mi: round4(filtered.filter((r) => r.mi > 0).length / filtered.length),
183
- };
184
-
185
- const scores = scored.map((it) => it.riskScore);
186
- const avgScore =
187
- scores.length > 0 ? round4(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;
188
- const maxScore = scores.length > 0 ? round4(Math.max(...scores)) : 0;
189
164
 
190
- const result = {
191
- items: scored,
192
- summary: {
193
- total: filtered.length,
194
- analyzed: scored.length,
195
- avgScore,
196
- maxScore,
197
- weights,
198
- signalCoverage,
199
- },
200
- };
201
-
202
- db.close();
203
-
204
- return paginateResult(result, 'items', {
205
- limit: opts.limit,
206
- offset: opts.offset,
207
- });
208
- }
209
-
210
- // ─── CLI Formatter ────────────────────────────────────────────────────
211
-
212
- /**
213
- * Print triage results to console.
214
- *
215
- * @param {string} [customDbPath]
216
- * @param {object} [opts]
217
- */
218
- export function triage(customDbPath, opts = {}) {
219
- const data = triageData(customDbPath, opts);
220
-
221
- if (opts.ndjson) {
222
- printNdjson(data, 'items');
223
- return;
224
- }
225
- if (opts.json) {
226
- console.log(JSON.stringify(data, null, 2));
227
- return;
228
- }
229
-
230
- if (data.items.length === 0) {
231
- if (data.summary.total === 0) {
232
- console.log('\nNo symbols found. Run "codegraph build" first.\n');
233
- } else {
234
- console.log('\nNo symbols match the given filters.\n');
235
- }
236
- return;
237
- }
238
-
239
- console.log('\n# Risk Audit Queue\n');
240
-
241
- console.log(
242
- ` ${'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)}`,
243
- );
244
- console.log(
245
- ` ${'─'.repeat(35)} ${'─'.repeat(28)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(7)} ${'─'.repeat(4)} ${'─'.repeat(6)} ${'─'.repeat(5)}`,
246
- );
247
-
248
- for (const it of data.items) {
249
- const name = it.name.length > 33 ? `${it.name.slice(0, 32)}…` : it.name;
250
- const file = it.file.length > 26 ? `…${it.file.slice(-25)}` : it.file;
251
- const role = (it.role || '-').padEnd(8);
252
- const score = it.riskScore.toFixed(2).padStart(6);
253
- const fanIn = String(it.fanIn).padStart(7);
254
- const cog = String(it.cognitive).padStart(4);
255
- const churn = String(it.churn).padStart(6);
256
- const mi = it.maintainabilityIndex > 0 ? String(it.maintainabilityIndex).padStart(5) : ' -';
257
- console.log(
258
- ` ${name.padEnd(35)} ${file.padEnd(28)} ${role} ${score} ${fanIn} ${cog} ${churn} ${mi}`,
259
- );
165
+ return paginateResult(result, 'items', {
166
+ limit: opts.limit,
167
+ offset: opts.offset,
168
+ });
169
+ } finally {
170
+ db.close();
260
171
  }
261
-
262
- const s = data.summary;
263
- console.log(
264
- `\n ${s.analyzed} symbols scored (of ${s.total} total) | avg: ${s.avgScore.toFixed(2)} | max: ${s.maxScore.toFixed(2)} | sort: ${opts.sort || 'risk'}`,
265
- );
266
- console.log();
267
172
  }
268
173
 
269
174
  // ─── Utilities ────────────────────────────────────────────────────────
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 './queries.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
  ),