@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/ast.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { AST_TYPE_MAPS } from './ast-analysis/rules/index.js';
|
|
11
|
+
import { buildExtensionSet } from './ast-analysis/shared.js';
|
|
12
|
+
import { walkWithVisitors } from './ast-analysis/visitor.js';
|
|
13
|
+
import { createAstStoreVisitor } from './ast-analysis/visitors/ast-store-visitor.js';
|
|
14
|
+
import { bulkNodeIdsByFile, openReadonlyOrFail } from './db.js';
|
|
15
|
+
import { outputResult } from './infrastructure/result-formatter.js';
|
|
11
16
|
import { debug } from './logger.js';
|
|
12
|
-
import { paginateResult
|
|
13
|
-
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
17
|
+
import { paginateResult } from './paginate.js';
|
|
14
18
|
|
|
15
19
|
// ─── Constants ────────────────────────────────────────────────────────
|
|
16
20
|
|
|
@@ -25,99 +29,15 @@ const KIND_ICONS = {
|
|
|
25
29
|
await: '\u22B3', // ⊳
|
|
26
30
|
};
|
|
27
31
|
|
|
28
|
-
/**
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
/** tree-sitter node types that map to our AST node kinds (JS/TS/TSX). */
|
|
32
|
-
const JS_TS_AST_TYPES = {
|
|
33
|
-
new_expression: 'new',
|
|
34
|
-
throw_statement: 'throw',
|
|
35
|
-
await_expression: 'await',
|
|
36
|
-
string: 'string',
|
|
37
|
-
template_string: 'string',
|
|
38
|
-
regex: 'regex',
|
|
39
|
-
};
|
|
32
|
+
/** tree-sitter node types that map to our AST node kinds — imported from rules. */
|
|
33
|
+
const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
|
|
40
34
|
|
|
41
35
|
/** Extensions that support full AST walk (new/throw/await/string/regex). */
|
|
42
|
-
const WALK_EXTENSIONS =
|
|
43
|
-
for (const lang of Object.values(LANGUAGE_REGISTRY)) {
|
|
44
|
-
if (['javascript', 'typescript', 'tsx'].includes(lang.id)) {
|
|
45
|
-
for (const ext of lang.extensions) WALK_EXTENSIONS.add(ext);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
36
|
+
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
|
|
48
37
|
|
|
49
38
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!s) return null;
|
|
53
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract the constructor name from a `new_expression` node.
|
|
58
|
-
* Handles `new Foo()`, `new a.Foo()`, `new Foo.Bar()`.
|
|
59
|
-
*/
|
|
60
|
-
function extractNewName(node) {
|
|
61
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
62
|
-
const child = node.child(i);
|
|
63
|
-
if (child.type === 'identifier') return child.text;
|
|
64
|
-
if (child.type === 'member_expression') {
|
|
65
|
-
// e.g. new a.Foo() → "a.Foo"
|
|
66
|
-
return child.text;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return node.text?.split('(')[0]?.replace('new ', '').trim() || '?';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Extract the expression text from a throw/await node.
|
|
74
|
-
*/
|
|
75
|
-
function extractExpressionText(node) {
|
|
76
|
-
// Skip keyword child, take the rest
|
|
77
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
78
|
-
const child = node.child(i);
|
|
79
|
-
if (child.type !== 'throw' && child.type !== 'await') {
|
|
80
|
-
return truncate(child.text);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return truncate(node.text);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Extract a meaningful name from throw/await nodes.
|
|
88
|
-
* For throw: the constructor or expression type.
|
|
89
|
-
* For await: the called function name.
|
|
90
|
-
*/
|
|
91
|
-
function extractName(kind, node) {
|
|
92
|
-
if (kind === 'throw') {
|
|
93
|
-
// throw new Error(...) → "Error"; throw x → "x"
|
|
94
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
95
|
-
const child = node.child(i);
|
|
96
|
-
if (child.type === 'new_expression') return extractNewName(child);
|
|
97
|
-
if (child.type === 'call_expression') {
|
|
98
|
-
const fn = child.childForFieldName('function');
|
|
99
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
100
|
-
}
|
|
101
|
-
if (child.type === 'identifier') return child.text;
|
|
102
|
-
}
|
|
103
|
-
return truncate(node.text);
|
|
104
|
-
}
|
|
105
|
-
if (kind === 'await') {
|
|
106
|
-
// await fetch(...) → "fetch"; await this.foo() → "this.foo"
|
|
107
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
108
|
-
const child = node.child(i);
|
|
109
|
-
if (child.type === 'call_expression') {
|
|
110
|
-
const fn = child.childForFieldName('function');
|
|
111
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
112
|
-
}
|
|
113
|
-
if (child.type === 'identifier' || child.type === 'member_expression') {
|
|
114
|
-
return child.text;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return truncate(node.text);
|
|
118
|
-
}
|
|
119
|
-
return truncate(node.text);
|
|
120
|
-
}
|
|
39
|
+
// Node extraction helpers (extractNewName, extractName, etc.) moved to
|
|
40
|
+
// ast-analysis/visitors/ast-store-visitor.js as part of the visitor framework.
|
|
121
41
|
|
|
122
42
|
/**
|
|
123
43
|
* Find the narrowest enclosing definition for a given line.
|
|
@@ -156,9 +76,6 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
156
76
|
return;
|
|
157
77
|
}
|
|
158
78
|
|
|
159
|
-
// Bulk-fetch all node IDs per file (replaces per-def getNodeId calls)
|
|
160
|
-
const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
|
|
161
|
-
|
|
162
79
|
const tx = db.transaction((rows) => {
|
|
163
80
|
for (const r of rows) {
|
|
164
81
|
insertStmt.run(r.file, r.line, r.kind, r.name, r.text, r.receiver, r.parentNodeId);
|
|
@@ -172,7 +89,7 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
172
89
|
|
|
173
90
|
// Pre-load all node IDs for this file into a map (read-only, fast)
|
|
174
91
|
const nodeIdMap = new Map();
|
|
175
|
-
for (const row of
|
|
92
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
176
93
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
177
94
|
}
|
|
178
95
|
|
|
@@ -237,66 +154,13 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
237
154
|
|
|
238
155
|
/**
|
|
239
156
|
* Walk a tree-sitter AST and collect new/throw/await/string/regex nodes.
|
|
157
|
+
* Delegates to the ast-store visitor via the unified walker.
|
|
240
158
|
*/
|
|
241
|
-
function walkAst(
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
let name;
|
|
248
|
-
let text = null;
|
|
249
|
-
|
|
250
|
-
if (kind === 'new') {
|
|
251
|
-
name = extractNewName(node);
|
|
252
|
-
text = truncate(node.text);
|
|
253
|
-
} else if (kind === 'throw') {
|
|
254
|
-
name = extractName('throw', node);
|
|
255
|
-
text = extractExpressionText(node);
|
|
256
|
-
} else if (kind === 'await') {
|
|
257
|
-
name = extractName('await', node);
|
|
258
|
-
text = extractExpressionText(node);
|
|
259
|
-
} else if (kind === 'string') {
|
|
260
|
-
// Skip trivial strings (length < 2 after removing quotes)
|
|
261
|
-
const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
|
|
262
|
-
if (content.length < 2) {
|
|
263
|
-
// Still recurse children
|
|
264
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
265
|
-
walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
|
|
266
|
-
}
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
name = truncate(content, 100);
|
|
270
|
-
text = truncate(node.text);
|
|
271
|
-
} else if (kind === 'regex') {
|
|
272
|
-
name = node.text || '?';
|
|
273
|
-
text = truncate(node.text);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const parentDef = findParentDef(defs, line);
|
|
277
|
-
let parentNodeId = null;
|
|
278
|
-
if (parentDef) {
|
|
279
|
-
parentNodeId = nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
rows.push({
|
|
283
|
-
file: relPath,
|
|
284
|
-
line,
|
|
285
|
-
kind,
|
|
286
|
-
name,
|
|
287
|
-
text,
|
|
288
|
-
receiver: null,
|
|
289
|
-
parentNodeId,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Don't recurse into the children of matched nodes for new/throw/await
|
|
293
|
-
// (we already extracted what we need, and nested strings inside them are noise)
|
|
294
|
-
if (kind !== 'string' && kind !== 'regex') return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
298
|
-
walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
|
|
299
|
-
}
|
|
159
|
+
function walkAst(rootNode, defs, relPath, rows, nodeIdMap) {
|
|
160
|
+
const visitor = createAstStoreVisitor(JS_TS_AST_TYPES, defs, relPath, nodeIdMap);
|
|
161
|
+
const results = walkWithVisitors(rootNode, [visitor], 'javascript');
|
|
162
|
+
const collected = results['ast-store'] || [];
|
|
163
|
+
rows.push(...collected);
|
|
300
164
|
}
|
|
301
165
|
|
|
302
166
|
// ─── Query ────────────────────────────────────────────────────────────
|
|
@@ -351,8 +215,12 @@ export function astQueryData(pattern, customDbPath, opts = {}) {
|
|
|
351
215
|
ORDER BY a.file, a.line
|
|
352
216
|
`;
|
|
353
217
|
|
|
354
|
-
|
|
355
|
-
|
|
218
|
+
let rows;
|
|
219
|
+
try {
|
|
220
|
+
rows = db.prepare(sql).all(...params);
|
|
221
|
+
} finally {
|
|
222
|
+
db.close();
|
|
223
|
+
}
|
|
356
224
|
|
|
357
225
|
const results = rows.map((r) => ({
|
|
358
226
|
kind: r.kind,
|
|
@@ -382,15 +250,7 @@ export function astQueryData(pattern, customDbPath, opts = {}) {
|
|
|
382
250
|
export function astQuery(pattern, customDbPath, opts = {}) {
|
|
383
251
|
const data = astQueryData(pattern, customDbPath, opts);
|
|
384
252
|
|
|
385
|
-
if (opts
|
|
386
|
-
printNdjson(data, 'results');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (opts.json) {
|
|
391
|
-
console.log(JSON.stringify(data, null, 2));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
253
|
+
if (outputResult(data, 'results', opts)) return;
|
|
394
254
|
|
|
395
255
|
// Human-readable output
|
|
396
256
|
if (data.results.length === 0) {
|
package/src/audit.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { loadConfig } from './config.js';
|
|
11
11
|
import { openReadonlyOrFail } from './db.js';
|
|
12
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
12
13
|
import { RULE_DEFS } from './manifesto.js';
|
|
13
|
-
import { explainData
|
|
14
|
+
import { explainData } from './queries.js';
|
|
14
15
|
|
|
15
16
|
// ─── Threshold resolution ───────────────────────────────────────────
|
|
16
17
|
|
|
@@ -334,90 +335,3 @@ function defaultHealth() {
|
|
|
334
335
|
thresholdBreaches: [],
|
|
335
336
|
};
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
-
// ─── CLI formatter ──────────────────────────────────────────────────
|
|
339
|
-
|
|
340
|
-
export function audit(target, customDbPath, opts = {}) {
|
|
341
|
-
const data = auditData(target, customDbPath, opts);
|
|
342
|
-
|
|
343
|
-
if (opts.json) {
|
|
344
|
-
console.log(JSON.stringify(data, null, 2));
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (data.functions.length === 0) {
|
|
349
|
-
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
console.log(`\n# Audit: ${target} (${data.kind})`);
|
|
354
|
-
console.log(` ${data.functions.length} function(s) analyzed\n`);
|
|
355
|
-
|
|
356
|
-
for (const fn of data.functions) {
|
|
357
|
-
const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
|
|
358
|
-
const roleTag = fn.role ? ` [${fn.role}]` : '';
|
|
359
|
-
console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
|
|
360
|
-
console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
|
|
361
|
-
if (fn.summary) console.log(` ${fn.summary}`);
|
|
362
|
-
if (fn.signature) {
|
|
363
|
-
if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
|
|
364
|
-
if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Health metrics
|
|
368
|
-
if (fn.health.cognitive != null) {
|
|
369
|
-
console.log(`\n Health:`);
|
|
370
|
-
console.log(
|
|
371
|
-
` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
|
|
372
|
-
);
|
|
373
|
-
console.log(` MI: ${fn.health.maintainabilityIndex}`);
|
|
374
|
-
if (fn.health.halstead.volume) {
|
|
375
|
-
console.log(
|
|
376
|
-
` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (fn.health.loc) {
|
|
380
|
-
console.log(
|
|
381
|
-
` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Threshold breaches
|
|
387
|
-
if (fn.health.thresholdBreaches.length > 0) {
|
|
388
|
-
console.log(`\n Threshold Breaches:`);
|
|
389
|
-
for (const b of fn.health.thresholdBreaches) {
|
|
390
|
-
const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
|
|
391
|
-
console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Impact
|
|
396
|
-
console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
|
|
397
|
-
for (const [level, nodes] of Object.entries(fn.impact.levels)) {
|
|
398
|
-
console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Call edges
|
|
402
|
-
if (fn.callees.length > 0) {
|
|
403
|
-
console.log(`\n Calls (${fn.callees.length}):`);
|
|
404
|
-
for (const c of fn.callees) {
|
|
405
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (fn.callers.length > 0) {
|
|
409
|
-
console.log(`\n Called by (${fn.callers.length}):`);
|
|
410
|
-
for (const c of fn.callers) {
|
|
411
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (fn.relatedTests.length > 0) {
|
|
415
|
-
console.log(`\n Tests (${fn.relatedTests.length}):`);
|
|
416
|
-
for (const t of fn.relatedTests) {
|
|
417
|
-
console.log(` ${t.file}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
console.log();
|
|
422
|
-
}
|
|
423
|
-
}
|
package/src/batch.js
CHANGED
|
@@ -83,14 +83,6 @@ export function batchData(command, targets, customDbPath, opts = {}) {
|
|
|
83
83
|
return { command, total: targets.length, succeeded, failed, results };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
* CLI wrapper — calls batchData and prints JSON to stdout.
|
|
88
|
-
*/
|
|
89
|
-
export function batch(command, targets, customDbPath, opts = {}) {
|
|
90
|
-
const data = batchData(command, targets, customDbPath, opts);
|
|
91
|
-
console.log(JSON.stringify(data, null, 2));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
86
|
/**
|
|
95
87
|
* Expand comma-separated positional args into individual entries.
|
|
96
88
|
* `['a,b', 'c']` → `['a', 'b', 'c']`.
|
|
@@ -161,20 +153,3 @@ export function multiBatchData(items, customDbPath, sharedOpts = {}) {
|
|
|
161
153
|
|
|
162
154
|
return { mode: 'multi', total: items.length, succeeded, failed, results };
|
|
163
155
|
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* CLI wrapper for batch-query — detects multi-command mode (objects with .command)
|
|
167
|
-
* or falls back to single-command batchData (default: 'where').
|
|
168
|
-
*/
|
|
169
|
-
export function batchQuery(targets, customDbPath, opts = {}) {
|
|
170
|
-
const { command: defaultCommand = 'where', ...rest } = opts;
|
|
171
|
-
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
|
|
172
|
-
|
|
173
|
-
let data;
|
|
174
|
-
if (isMulti) {
|
|
175
|
-
data = multiBatchData(targets, customDbPath, rest);
|
|
176
|
-
} else {
|
|
177
|
-
data = batchData(defaultCommand, targets, customDbPath, rest);
|
|
178
|
-
}
|
|
179
|
-
console.log(JSON.stringify(data, null, 2));
|
|
180
|
-
}
|
package/src/boundaries.js
CHANGED
package/src/branch-compare.js
CHANGED
|
@@ -12,7 +12,8 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import Database from 'better-sqlite3';
|
|
14
14
|
import { buildGraph } from './builder.js';
|
|
15
|
-
import { isTestFile
|
|
15
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
16
|
+
import { kindIcon } from './queries.js';
|
|
16
17
|
|
|
17
18
|
// ─── Git Helpers ────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -81,55 +82,57 @@ function makeSymbolKey(kind, file, name) {
|
|
|
81
82
|
|
|
82
83
|
function loadSymbolsFromDb(dbPath, changedFiles, noTests) {
|
|
83
84
|
const db = new Database(dbPath, { readonly: true });
|
|
84
|
-
|
|
85
|
+
try {
|
|
86
|
+
const symbols = new Map();
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
88
|
+
if (changedFiles.length === 0) {
|
|
89
|
+
return symbols;
|
|
90
|
+
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
92
|
+
// Query nodes in changed files
|
|
93
|
+
const placeholders = changedFiles.map(() => '?').join(', ');
|
|
94
|
+
const rows = db
|
|
95
|
+
.prepare(
|
|
96
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
97
|
+
FROM nodes n
|
|
98
|
+
WHERE n.file IN (${placeholders})
|
|
99
|
+
AND n.kind NOT IN ('file', 'directory')
|
|
100
|
+
ORDER BY n.file, n.line`,
|
|
101
|
+
)
|
|
102
|
+
.all(...changedFiles);
|
|
103
|
+
|
|
104
|
+
// Compute fan_in and fan_out for each node
|
|
105
|
+
const fanInStmt = db.prepare(
|
|
106
|
+
`SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ? AND kind = 'calls'`,
|
|
107
|
+
);
|
|
108
|
+
const fanOutStmt = db.prepare(
|
|
109
|
+
`SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ? AND kind = 'calls'`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
if (noTests && isTestFile(row.file)) continue;
|
|
114
|
+
|
|
115
|
+
const lineCount = row.end_line ? row.end_line - row.line + 1 : 0;
|
|
116
|
+
const fanIn = fanInStmt.get(row.id).cnt;
|
|
117
|
+
const fanOut = fanOutStmt.get(row.id).cnt;
|
|
118
|
+
const key = makeSymbolKey(row.kind, row.file, row.name);
|
|
119
|
+
|
|
120
|
+
symbols.set(key, {
|
|
121
|
+
id: row.id,
|
|
122
|
+
name: row.name,
|
|
123
|
+
kind: row.kind,
|
|
124
|
+
file: row.file,
|
|
125
|
+
line: row.line,
|
|
126
|
+
lineCount,
|
|
127
|
+
fanIn,
|
|
128
|
+
fanOut,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
return symbols;
|
|
133
|
+
} finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// ─── Caller BFS ─────────────────────────────────────────────────────────
|
|
@@ -138,40 +141,43 @@ function loadCallersFromDb(dbPath, nodeIds, maxDepth, noTests) {
|
|
|
138
141
|
if (nodeIds.length === 0) return [];
|
|
139
142
|
|
|
140
143
|
const db = new Database(dbPath, { readonly: true });
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
visited.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
144
|
+
try {
|
|
145
|
+
const allCallers = new Set();
|
|
146
|
+
|
|
147
|
+
for (const startId of nodeIds) {
|
|
148
|
+
const visited = new Set([startId]);
|
|
149
|
+
let frontier = [startId];
|
|
150
|
+
|
|
151
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
152
|
+
const nextFrontier = [];
|
|
153
|
+
for (const fid of frontier) {
|
|
154
|
+
const callers = db
|
|
155
|
+
.prepare(
|
|
156
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
157
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
158
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
159
|
+
)
|
|
160
|
+
.all(fid);
|
|
161
|
+
|
|
162
|
+
for (const c of callers) {
|
|
163
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
164
|
+
visited.add(c.id);
|
|
165
|
+
nextFrontier.push(c.id);
|
|
166
|
+
allCallers.add(
|
|
167
|
+
JSON.stringify({ name: c.name, kind: c.kind, file: c.file, line: c.line }),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
165
170
|
}
|
|
166
171
|
}
|
|
172
|
+
frontier = nextFrontier;
|
|
173
|
+
if (frontier.length === 0) break;
|
|
167
174
|
}
|
|
168
|
-
frontier = nextFrontier;
|
|
169
|
-
if (frontier.length === 0) break;
|
|
170
175
|
}
|
|
171
|
-
}
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
return [...allCallers].map((s) => JSON.parse(s));
|
|
178
|
+
} finally {
|
|
179
|
+
db.close();
|
|
180
|
+
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
// ─── Symbol Comparison ──────────────────────────────────────────────────
|
|
@@ -470,99 +476,3 @@ export function branchCompareMermaid(data) {
|
|
|
470
476
|
|
|
471
477
|
return lines.join('\n');
|
|
472
478
|
}
|
|
473
|
-
|
|
474
|
-
// ─── Text Formatting ────────────────────────────────────────────────────
|
|
475
|
-
|
|
476
|
-
function formatText(data) {
|
|
477
|
-
if (data.error) return `Error: ${data.error}`;
|
|
478
|
-
|
|
479
|
-
const lines = [];
|
|
480
|
-
const shortBase = data.baseSha.slice(0, 7);
|
|
481
|
-
const shortTarget = data.targetSha.slice(0, 7);
|
|
482
|
-
|
|
483
|
-
lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
|
|
484
|
-
lines.push(` Base: ${data.baseRef} (${shortBase})`);
|
|
485
|
-
lines.push(` Target: ${data.targetRef} (${shortTarget})`);
|
|
486
|
-
lines.push(` Files changed: ${data.changedFiles.length}`);
|
|
487
|
-
|
|
488
|
-
if (data.added.length > 0) {
|
|
489
|
-
lines.push('');
|
|
490
|
-
lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
|
|
491
|
-
for (const sym of data.added) {
|
|
492
|
-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (data.removed.length > 0) {
|
|
497
|
-
lines.push('');
|
|
498
|
-
lines.push(
|
|
499
|
-
` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
|
|
500
|
-
);
|
|
501
|
-
for (const sym of data.removed) {
|
|
502
|
-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
|
|
503
|
-
if (sym.impact && sym.impact.length > 0) {
|
|
504
|
-
lines.push(
|
|
505
|
-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (data.changed.length > 0) {
|
|
512
|
-
lines.push('');
|
|
513
|
-
lines.push(
|
|
514
|
-
` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
|
|
515
|
-
);
|
|
516
|
-
for (const sym of data.changed) {
|
|
517
|
-
const parts = [];
|
|
518
|
-
if (sym.changes.lineCount !== 0) {
|
|
519
|
-
parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
|
|
520
|
-
}
|
|
521
|
-
if (sym.changes.fanIn !== 0) {
|
|
522
|
-
parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
|
|
523
|
-
}
|
|
524
|
-
if (sym.changes.fanOut !== 0) {
|
|
525
|
-
parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
|
|
526
|
-
}
|
|
527
|
-
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
528
|
-
lines.push(
|
|
529
|
-
` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
|
|
530
|
-
);
|
|
531
|
-
if (sym.impact && sym.impact.length > 0) {
|
|
532
|
-
lines.push(
|
|
533
|
-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const s = data.summary;
|
|
540
|
-
lines.push('');
|
|
541
|
-
lines.push(
|
|
542
|
-
` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
|
|
543
|
-
` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
|
|
544
|
-
(s.filesAffected > 0
|
|
545
|
-
? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
|
|
546
|
-
: ''),
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
return lines.join('\n');
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ─── CLI Display Function ───────────────────────────────────────────────
|
|
553
|
-
|
|
554
|
-
export async function branchCompare(baseRef, targetRef, opts = {}) {
|
|
555
|
-
const data = await branchCompareData(baseRef, targetRef, opts);
|
|
556
|
-
|
|
557
|
-
if (opts.json || opts.format === 'json') {
|
|
558
|
-
console.log(JSON.stringify(data, null, 2));
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (opts.format === 'mermaid') {
|
|
563
|
-
console.log(branchCompareMermaid(data));
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
console.log(formatText(data));
|
|
568
|
-
}
|