@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/sequence.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* sequence-diagram conventions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { openReadonlyOrFail } from './db.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { findCallees, openReadonlyOrFail } from './db.js';
|
|
10
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
11
|
+
import { paginateResult } from './paginate.js';
|
|
12
|
+
import { findMatchingNodes } from './queries.js';
|
|
12
13
|
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
|
|
13
14
|
|
|
14
15
|
// ─── Alias generation ────────────────────────────────────────────────
|
|
@@ -85,208 +86,203 @@ function buildAliases(files) {
|
|
|
85
86
|
*/
|
|
86
87
|
export function sequenceData(name, dbPath, opts = {}) {
|
|
87
88
|
const db = openReadonlyOrFail(dbPath);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!matchNode) {
|
|
104
|
-
db.close();
|
|
105
|
-
return {
|
|
106
|
-
entry: null,
|
|
107
|
-
participants: [],
|
|
108
|
-
messages: [],
|
|
109
|
-
depth: maxDepth,
|
|
110
|
-
totalMessages: 0,
|
|
111
|
-
truncated: false,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const entry = {
|
|
116
|
-
name: matchNode.name,
|
|
117
|
-
file: matchNode.file,
|
|
118
|
-
kind: matchNode.kind,
|
|
119
|
-
line: matchNode.line,
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// BFS forward — track edges, not just nodes
|
|
123
|
-
const visited = new Set([matchNode.id]);
|
|
124
|
-
let frontier = [matchNode.id];
|
|
125
|
-
const messages = [];
|
|
126
|
-
const fileSet = new Set([matchNode.file]);
|
|
127
|
-
const idToNode = new Map();
|
|
128
|
-
idToNode.set(matchNode.id, matchNode);
|
|
129
|
-
let truncated = false;
|
|
130
|
-
|
|
131
|
-
const getCallees = db.prepare(
|
|
132
|
-
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
133
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
134
|
-
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
for (let d = 1; d <= maxDepth; d++) {
|
|
138
|
-
const nextFrontier = [];
|
|
139
|
-
|
|
140
|
-
for (const fid of frontier) {
|
|
141
|
-
const callees = getCallees.all(fid);
|
|
142
|
-
|
|
143
|
-
const caller = idToNode.get(fid);
|
|
144
|
-
|
|
145
|
-
for (const c of callees) {
|
|
146
|
-
if (noTests && isTestFile(c.file)) continue;
|
|
147
|
-
|
|
148
|
-
// Always record the message (even for visited nodes — different caller path)
|
|
149
|
-
fileSet.add(c.file);
|
|
150
|
-
messages.push({
|
|
151
|
-
from: caller.file,
|
|
152
|
-
to: c.file,
|
|
153
|
-
label: c.name,
|
|
154
|
-
type: 'call',
|
|
155
|
-
depth: d,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (visited.has(c.id)) continue;
|
|
159
|
-
|
|
160
|
-
visited.add(c.id);
|
|
161
|
-
nextFrontier.push(c.id);
|
|
162
|
-
idToNode.set(c.id, c);
|
|
89
|
+
try {
|
|
90
|
+
const maxDepth = opts.depth || 10;
|
|
91
|
+
const noTests = opts.noTests || false;
|
|
92
|
+
const withDataflow = opts.dataflow || false;
|
|
93
|
+
|
|
94
|
+
// Phase 1: Direct LIKE match
|
|
95
|
+
let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
|
|
96
|
+
|
|
97
|
+
// Phase 2: Prefix-stripped matching
|
|
98
|
+
if (!matchNode) {
|
|
99
|
+
for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
|
|
100
|
+
matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
|
|
101
|
+
if (matchNode) break;
|
|
163
102
|
}
|
|
164
103
|
}
|
|
165
104
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
105
|
+
if (!matchNode) {
|
|
106
|
+
return {
|
|
107
|
+
entry: null,
|
|
108
|
+
participants: [],
|
|
109
|
+
messages: [],
|
|
110
|
+
depth: maxDepth,
|
|
111
|
+
totalMessages: 0,
|
|
112
|
+
truncated: false,
|
|
113
|
+
};
|
|
173
114
|
}
|
|
174
|
-
}
|
|
175
115
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
if (hasTable) {
|
|
183
|
-
// Build name|file lookup for O(1) target node access
|
|
184
|
-
const nodeByNameFile = new Map();
|
|
185
|
-
for (const n of idToNode.values()) {
|
|
186
|
-
nodeByNameFile.set(`${n.name}|${n.file}`, n);
|
|
187
|
-
}
|
|
116
|
+
const entry = {
|
|
117
|
+
name: matchNode.name,
|
|
118
|
+
file: matchNode.file,
|
|
119
|
+
kind: matchNode.kind,
|
|
120
|
+
line: matchNode.line,
|
|
121
|
+
};
|
|
188
122
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
123
|
+
// BFS forward — track edges, not just nodes
|
|
124
|
+
const visited = new Set([matchNode.id]);
|
|
125
|
+
let frontier = [matchNode.id];
|
|
126
|
+
const messages = [];
|
|
127
|
+
const fileSet = new Set([matchNode.file]);
|
|
128
|
+
const idToNode = new Map();
|
|
129
|
+
idToNode.set(matchNode.id, matchNode);
|
|
130
|
+
let truncated = false;
|
|
131
|
+
|
|
132
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
133
|
+
const nextFrontier = [];
|
|
198
134
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const msg of [...messages]) {
|
|
202
|
-
if (msg.type !== 'call') continue;
|
|
203
|
-
const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
|
|
204
|
-
if (!targetNode) continue;
|
|
135
|
+
for (const fid of frontier) {
|
|
136
|
+
const callees = findCallees(db, fid);
|
|
205
137
|
|
|
206
|
-
const
|
|
207
|
-
if (seenReturns.has(returnKey)) continue;
|
|
138
|
+
const caller = idToNode.get(fid);
|
|
208
139
|
|
|
209
|
-
const
|
|
140
|
+
for (const c of callees) {
|
|
141
|
+
if (noTests && isTestFile(c.file)) continue;
|
|
210
142
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const expr = returns[0].expression || 'result';
|
|
143
|
+
// Always record the message (even for visited nodes — different caller path)
|
|
144
|
+
fileSet.add(c.file);
|
|
214
145
|
messages.push({
|
|
215
|
-
from:
|
|
216
|
-
to:
|
|
217
|
-
label:
|
|
218
|
-
type: '
|
|
219
|
-
depth:
|
|
146
|
+
from: caller.file,
|
|
147
|
+
to: c.file,
|
|
148
|
+
label: c.name,
|
|
149
|
+
type: 'call',
|
|
150
|
+
depth: d,
|
|
220
151
|
});
|
|
152
|
+
|
|
153
|
+
if (visited.has(c.id)) continue;
|
|
154
|
+
|
|
155
|
+
visited.add(c.id);
|
|
156
|
+
nextFrontier.push(c.id);
|
|
157
|
+
idToNode.set(c.id, c);
|
|
221
158
|
}
|
|
222
159
|
}
|
|
223
160
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
161
|
+
frontier = nextFrontier;
|
|
162
|
+
if (frontier.length === 0) break;
|
|
163
|
+
|
|
164
|
+
if (d === maxDepth && frontier.length > 0) {
|
|
165
|
+
// Only mark truncated if at least one frontier node has further callees
|
|
166
|
+
const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
|
|
167
|
+
if (hasMoreCalls) truncated = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Dataflow annotations: add return arrows
|
|
172
|
+
if (withDataflow && messages.length > 0) {
|
|
173
|
+
const hasTable = db
|
|
174
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
|
|
175
|
+
.get();
|
|
176
|
+
|
|
177
|
+
if (hasTable) {
|
|
178
|
+
// Build name|file lookup for O(1) target node access
|
|
179
|
+
const nodeByNameFile = new Map();
|
|
180
|
+
for (const n of idToNode.values()) {
|
|
181
|
+
nodeByNameFile.set(`${n.name}|${n.file}`, n);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const getReturns = db.prepare(
|
|
185
|
+
`SELECT d.expression FROM dataflow d
|
|
186
|
+
WHERE d.source_id = ? AND d.kind = 'returns'`,
|
|
187
|
+
);
|
|
188
|
+
const getFlowsTo = db.prepare(
|
|
189
|
+
`SELECT d.expression FROM dataflow d
|
|
190
|
+
WHERE d.target_id = ? AND d.kind = 'flows_to'
|
|
191
|
+
ORDER BY d.param_index`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// For each called function, check if it has return edges
|
|
195
|
+
const seenReturns = new Set();
|
|
196
|
+
for (const msg of [...messages]) {
|
|
197
|
+
if (msg.type !== 'call') continue;
|
|
198
|
+
const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
|
|
199
|
+
if (!targetNode) continue;
|
|
200
|
+
|
|
201
|
+
const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
|
|
202
|
+
if (seenReturns.has(returnKey)) continue;
|
|
203
|
+
|
|
204
|
+
const returns = getReturns.all(targetNode.id);
|
|
205
|
+
|
|
206
|
+
if (returns.length > 0) {
|
|
207
|
+
seenReturns.add(returnKey);
|
|
208
|
+
const expr = returns[0].expression || 'result';
|
|
209
|
+
messages.push({
|
|
210
|
+
from: msg.to,
|
|
211
|
+
to: msg.from,
|
|
212
|
+
label: expr,
|
|
213
|
+
type: 'return',
|
|
214
|
+
depth: msg.depth,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Annotate call messages with parameter names
|
|
220
|
+
for (const msg of messages) {
|
|
221
|
+
if (msg.type !== 'call') continue;
|
|
222
|
+
const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
|
|
223
|
+
if (!targetNode) continue;
|
|
224
|
+
|
|
225
|
+
const params = getFlowsTo.all(targetNode.id);
|
|
226
|
+
|
|
227
|
+
if (params.length > 0) {
|
|
228
|
+
const paramNames = params
|
|
229
|
+
.map((p) => p.expression)
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.slice(0, 3);
|
|
232
|
+
if (paramNames.length > 0) {
|
|
233
|
+
msg.label = `${msg.label}(${paramNames.join(', ')})`;
|
|
234
|
+
}
|
|
239
235
|
}
|
|
240
236
|
}
|
|
241
237
|
}
|
|
242
238
|
}
|
|
243
|
-
}
|
|
244
239
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
240
|
+
// Sort messages by depth, then call before return
|
|
241
|
+
messages.sort((a, b) => {
|
|
242
|
+
if (a.depth !== b.depth) return a.depth - b.depth;
|
|
243
|
+
if (a.type === 'call' && b.type === 'return') return -1;
|
|
244
|
+
if (a.type === 'return' && b.type === 'call') return 1;
|
|
245
|
+
return 0;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Build participant list from files
|
|
249
|
+
const aliases = buildAliases([...fileSet]);
|
|
250
|
+
const participants = [...fileSet].map((file) => ({
|
|
251
|
+
id: aliases.get(file),
|
|
252
|
+
label: file.split('/').pop(),
|
|
253
|
+
file,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
// Sort participants: entry file first, then alphabetically
|
|
257
|
+
participants.sort((a, b) => {
|
|
258
|
+
if (a.file === entry.file) return -1;
|
|
259
|
+
if (b.file === entry.file) return 1;
|
|
260
|
+
return a.file.localeCompare(b.file);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Replace file paths with alias IDs in messages
|
|
264
|
+
for (const msg of messages) {
|
|
265
|
+
msg.from = aliases.get(msg.from);
|
|
266
|
+
msg.to = aliases.get(msg.to);
|
|
267
|
+
}
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
269
|
+
const base = {
|
|
270
|
+
entry,
|
|
271
|
+
participants,
|
|
272
|
+
messages,
|
|
273
|
+
depth: maxDepth,
|
|
274
|
+
totalMessages: messages.length,
|
|
275
|
+
truncated,
|
|
276
|
+
};
|
|
277
|
+
const result = paginateResult(base, 'messages', { limit: opts.limit, offset: opts.offset });
|
|
278
|
+
if (opts.limit !== undefined || opts.offset !== undefined) {
|
|
279
|
+
const activeFiles = new Set(result.messages.flatMap((m) => [m.from, m.to]));
|
|
280
|
+
result.participants = result.participants.filter((p) => activeFiles.has(p.id));
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
} finally {
|
|
284
|
+
db.close();
|
|
288
285
|
}
|
|
289
|
-
return result;
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
// ─── Mermaid formatter ───────────────────────────────────────────────
|
|
@@ -327,43 +323,3 @@ export function sequenceToMermaid(seqResult) {
|
|
|
327
323
|
|
|
328
324
|
return lines.join('\n');
|
|
329
325
|
}
|
|
330
|
-
|
|
331
|
-
// ─── CLI formatter ───────────────────────────────────────────────────
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* CLI entry point — format sequence data as mermaid, JSON, or ndjson.
|
|
335
|
-
*/
|
|
336
|
-
export function sequence(name, dbPath, opts = {}) {
|
|
337
|
-
const data = sequenceData(name, dbPath, opts);
|
|
338
|
-
|
|
339
|
-
if (opts.ndjson) {
|
|
340
|
-
printNdjson(data, 'messages');
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (opts.json) {
|
|
345
|
-
console.log(JSON.stringify(data, null, 2));
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Default: mermaid format
|
|
350
|
-
if (!data.entry) {
|
|
351
|
-
console.log(`No matching function found for "${name}".`);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const e = data.entry;
|
|
356
|
-
console.log(`\nSequence from: [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
357
|
-
console.log(`Participants: ${data.participants.length} Messages: ${data.totalMessages}`);
|
|
358
|
-
if (data.truncated) {
|
|
359
|
-
console.log(` (truncated at depth ${data.depth})`);
|
|
360
|
-
}
|
|
361
|
-
console.log();
|
|
362
|
-
|
|
363
|
-
if (data.messages.length === 0) {
|
|
364
|
-
console.log(' (leaf node — no callees)');
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
console.log(sequenceToMermaid(data));
|
|
369
|
-
}
|