@optave/codegraph 3.0.4 → 3.1.1

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 (49) hide show
  1. package/README.md +59 -52
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +9 -10
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +274 -159
  19. package/src/cfg.js +111 -341
  20. package/src/check.js +3 -3
  21. package/src/cli.js +122 -167
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +274 -697
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -392
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +160 -228
  35. package/src/index.js +36 -2
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +97 -20
  39. package/src/owners.js +132 -132
  40. package/src/parser.js +58 -131
  41. package/src/queries-cli.js +866 -0
  42. package/src/queries.js +1356 -2261
  43. package/src/resolve.js +11 -2
  44. package/src/result-formatter.js +21 -0
  45. package/src/sequence.js +364 -0
  46. package/src/structure.js +200 -199
  47. package/src/test-filter.js +7 -0
  48. package/src/triage.js +120 -162
  49. package/src/viewer.js +1 -1
package/src/triage.js CHANGED
@@ -1,7 +1,8 @@
1
- import { openReadonlyOrFail } from './db.js';
1
+ import { findNodesForTriage, openReadonlyOrFail } from './db.js';
2
2
  import { warn } from './logger.js';
3
- import { paginateResult, printNdjson } from './paginate.js';
4
- import { isTestFile } from './queries.js';
3
+ import { paginateResult } from './paginate.js';
4
+ import { outputResult } from './result-formatter.js';
5
+ import { isTestFile } from './test-filter.js';
5
6
 
6
7
  // ─── Constants ────────────────────────────────────────────────────────
7
8
 
@@ -46,165 +47,129 @@ function minMaxNormalize(values) {
46
47
  */
47
48
  export function triageData(customDbPath, opts = {}) {
48
49
  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 || {}) };
50
+ try {
51
+ const noTests = opts.noTests || false;
52
+ const fileFilter = opts.file || null;
53
+ const kindFilter = opts.kind || null;
54
+ const roleFilter = opts.role || null;
55
+ const minScore = opts.minScore != null ? Number(opts.minScore) : null;
56
+ const sort = opts.sort || 'risk';
57
+ const weights = { ...DEFAULT_WEIGHTS, ...(opts.weights || {}) };
58
+
59
+ let rows;
60
+ try {
61
+ rows = findNodesForTriage(db, {
62
+ noTests,
63
+ file: fileFilter,
64
+ kind: kindFilter,
65
+ role: roleFilter,
66
+ });
67
+ } catch (err) {
68
+ warn(`triage query failed: ${err.message}`);
69
+ return {
70
+ items: [],
71
+ summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
72
+ };
73
+ }
56
74
 
57
- // Build WHERE clause
58
- let where = "WHERE n.kind IN ('function','method','class')";
59
- const params = [];
75
+ // Post-filter test files (belt-and-suspenders)
76
+ const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
60
77
 
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
- }
78
+ if (filtered.length === 0) {
79
+ return {
80
+ items: [],
81
+ summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
82
+ };
83
+ }
80
84
 
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: {} },
85
+ // Extract raw signal arrays
86
+ const fanIns = filtered.map((r) => r.fan_in);
87
+ const cognitives = filtered.map((r) => r.cognitive);
88
+ const churns = filtered.map((r) => r.churn);
89
+ const mis = filtered.map((r) => r.mi);
90
+
91
+ // Min-max normalize
92
+ const normFanIns = minMaxNormalize(fanIns);
93
+ const normCognitives = minMaxNormalize(cognitives);
94
+ const normChurns = minMaxNormalize(churns);
95
+ // MI: higher is better, so invert: 1 - norm(mi)
96
+ const normMIsRaw = minMaxNormalize(mis);
97
+ const normMIs = normMIsRaw.map((v) => round4(1 - v));
98
+
99
+ // Compute risk scores
100
+ const items = filtered.map((r, i) => {
101
+ const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
102
+ const riskScore =
103
+ weights.fanIn * normFanIns[i] +
104
+ weights.complexity * normCognitives[i] +
105
+ weights.churn * normChurns[i] +
106
+ weights.role * roleWeight +
107
+ weights.mi * normMIs[i];
108
+
109
+ return {
110
+ name: r.name,
111
+ kind: r.kind,
112
+ file: r.file,
113
+ line: r.line,
114
+ role: r.role || null,
115
+ fanIn: r.fan_in,
116
+ cognitive: r.cognitive,
117
+ churn: r.churn,
118
+ maintainabilityIndex: r.mi,
119
+ normFanIn: round4(normFanIns[i]),
120
+ normComplexity: round4(normCognitives[i]),
121
+ normChurn: round4(normChurns[i]),
122
+ normMI: round4(normMIs[i]),
123
+ roleWeight,
124
+ riskScore: round4(riskScore),
125
+ };
126
+ });
127
+
128
+ // Apply minScore filter
129
+ const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
130
+
131
+ // Sort
132
+ const sortFns = {
133
+ risk: (a, b) => b.riskScore - a.riskScore,
134
+ complexity: (a, b) => b.cognitive - a.cognitive,
135
+ churn: (a, b) => b.churn - a.churn,
136
+ 'fan-in': (a, b) => b.fanIn - a.fanIn,
137
+ mi: (a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
107
138
  };
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: {} },
139
+ scored.sort(sortFns[sort] || sortFns.risk);
140
+
141
+ // Signal coverage: % of items with non-zero signal
142
+ const signalCoverage = {
143
+ complexity: round4(filtered.filter((r) => r.cognitive > 0).length / filtered.length),
144
+ churn: round4(filtered.filter((r) => r.churn > 0).length / filtered.length),
145
+ fanIn: round4(filtered.filter((r) => r.fan_in > 0).length / filtered.length),
146
+ mi: round4(filtered.filter((r) => r.mi > 0).length / filtered.length),
118
147
  };
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
148
 
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
-
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),
149
+ const scores = scored.map((it) => it.riskScore);
150
+ const avgScore =
151
+ scores.length > 0 ? round4(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;
152
+ const maxScore = scores.length > 0 ? round4(Math.max(...scores)) : 0;
153
+
154
+ const result = {
155
+ items: scored,
156
+ summary: {
157
+ total: filtered.length,
158
+ analyzed: scored.length,
159
+ avgScore,
160
+ maxScore,
161
+ weights,
162
+ signalCoverage,
163
+ },
161
164
  };
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
-
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
165
 
202
- db.close();
203
-
204
- return paginateResult(result, 'items', {
205
- limit: opts.limit,
206
- offset: opts.offset,
207
- });
166
+ return paginateResult(result, 'items', {
167
+ limit: opts.limit,
168
+ offset: opts.offset,
169
+ });
170
+ } finally {
171
+ db.close();
172
+ }
208
173
  }
209
174
 
210
175
  // ─── CLI Formatter ────────────────────────────────────────────────────
@@ -218,14 +183,7 @@ export function triageData(customDbPath, opts = {}) {
218
183
  export function triage(customDbPath, opts = {}) {
219
184
  const data = triageData(customDbPath, opts);
220
185
 
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
- }
186
+ if (outputResult(data, 'items', opts)) return;
229
187
 
230
188
  if (data.items.length === 0) {
231
189
  if (data.summary.total === 0) {
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 './test-filter.js';
6
6
 
7
7
  const DEFAULT_MIN_CONFIDENCE = 0.5;
8
8