@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.
- package/README.md +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- 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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 './
|
|
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:
|
|
189
|
-
|
|
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
|
),
|