@optave/codegraph 3.1.1 → 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/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -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 +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +48 -179
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +20 -19
- 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 +12 -83
- package/src/complexity.js +42 -356
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- 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 +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +23 -19
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/owners.js +1 -56
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/ast.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { AST_TYPE_MAPS } from './ast-analysis/rules/index.js';
|
|
11
11
|
import { buildExtensionSet } from './ast-analysis/shared.js';
|
|
12
|
-
import {
|
|
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';
|
|
13
16
|
import { debug } from './logger.js';
|
|
14
17
|
import { paginateResult } from './paginate.js';
|
|
15
18
|
|
|
16
|
-
import { outputResult } from './result-formatter.js';
|
|
17
|
-
|
|
18
19
|
// ─── Constants ────────────────────────────────────────────────────────
|
|
19
20
|
|
|
20
21
|
export const AST_NODE_KINDS = ['call', 'new', 'string', 'regex', 'throw', 'await'];
|
|
@@ -28,9 +29,6 @@ const KIND_ICONS = {
|
|
|
28
29
|
await: '\u22B3', // ⊳
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
/** Max length for the `text` column. */
|
|
32
|
-
const TEXT_MAX = 200;
|
|
33
|
-
|
|
34
32
|
/** tree-sitter node types that map to our AST node kinds — imported from rules. */
|
|
35
33
|
const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
|
|
36
34
|
|
|
@@ -38,77 +36,8 @@ const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
|
|
|
38
36
|
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
|
|
39
37
|
|
|
40
38
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!s) return null;
|
|
44
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Extract the constructor name from a `new_expression` node.
|
|
49
|
-
* Handles `new Foo()`, `new a.Foo()`, `new Foo.Bar()`.
|
|
50
|
-
*/
|
|
51
|
-
function extractNewName(node) {
|
|
52
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
53
|
-
const child = node.child(i);
|
|
54
|
-
if (child.type === 'identifier') return child.text;
|
|
55
|
-
if (child.type === 'member_expression') {
|
|
56
|
-
// e.g. new a.Foo() → "a.Foo"
|
|
57
|
-
return child.text;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return node.text?.split('(')[0]?.replace('new ', '').trim() || '?';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extract the expression text from a throw/await node.
|
|
65
|
-
*/
|
|
66
|
-
function extractExpressionText(node) {
|
|
67
|
-
// Skip keyword child, take the rest
|
|
68
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
69
|
-
const child = node.child(i);
|
|
70
|
-
if (child.type !== 'throw' && child.type !== 'await') {
|
|
71
|
-
return truncate(child.text);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return truncate(node.text);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Extract a meaningful name from throw/await nodes.
|
|
79
|
-
* For throw: the constructor or expression type.
|
|
80
|
-
* For await: the called function name.
|
|
81
|
-
*/
|
|
82
|
-
function extractName(kind, node) {
|
|
83
|
-
if (kind === 'throw') {
|
|
84
|
-
// throw new Error(...) → "Error"; throw x → "x"
|
|
85
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
86
|
-
const child = node.child(i);
|
|
87
|
-
if (child.type === 'new_expression') return extractNewName(child);
|
|
88
|
-
if (child.type === 'call_expression') {
|
|
89
|
-
const fn = child.childForFieldName('function');
|
|
90
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
91
|
-
}
|
|
92
|
-
if (child.type === 'identifier') return child.text;
|
|
93
|
-
}
|
|
94
|
-
return truncate(node.text);
|
|
95
|
-
}
|
|
96
|
-
if (kind === 'await') {
|
|
97
|
-
// await fetch(...) → "fetch"; await this.foo() → "this.foo"
|
|
98
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
99
|
-
const child = node.child(i);
|
|
100
|
-
if (child.type === 'call_expression') {
|
|
101
|
-
const fn = child.childForFieldName('function');
|
|
102
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
103
|
-
}
|
|
104
|
-
if (child.type === 'identifier' || child.type === 'member_expression') {
|
|
105
|
-
return child.text;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return truncate(node.text);
|
|
109
|
-
}
|
|
110
|
-
return truncate(node.text);
|
|
111
|
-
}
|
|
39
|
+
// Node extraction helpers (extractNewName, extractName, etc.) moved to
|
|
40
|
+
// ast-analysis/visitors/ast-store-visitor.js as part of the visitor framework.
|
|
112
41
|
|
|
113
42
|
/**
|
|
114
43
|
* Find the narrowest enclosing definition for a given line.
|
|
@@ -147,9 +76,6 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
147
76
|
return;
|
|
148
77
|
}
|
|
149
78
|
|
|
150
|
-
// Bulk-fetch all node IDs per file (replaces per-def getNodeId calls)
|
|
151
|
-
const bulkGetNodeIds = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
|
|
152
|
-
|
|
153
79
|
const tx = db.transaction((rows) => {
|
|
154
80
|
for (const r of rows) {
|
|
155
81
|
insertStmt.run(r.file, r.line, r.kind, r.name, r.text, r.receiver, r.parentNodeId);
|
|
@@ -163,7 +89,7 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
163
89
|
|
|
164
90
|
// Pre-load all node IDs for this file into a map (read-only, fast)
|
|
165
91
|
const nodeIdMap = new Map();
|
|
166
|
-
for (const row of
|
|
92
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
167
93
|
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
168
94
|
}
|
|
169
95
|
|
|
@@ -228,66 +154,13 @@ export async function buildAstNodes(db, fileSymbols, _rootDir, _engineOpts) {
|
|
|
228
154
|
|
|
229
155
|
/**
|
|
230
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.
|
|
231
158
|
*/
|
|
232
|
-
function walkAst(
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
let name;
|
|
239
|
-
let text = null;
|
|
240
|
-
|
|
241
|
-
if (kind === 'new') {
|
|
242
|
-
name = extractNewName(node);
|
|
243
|
-
text = truncate(node.text);
|
|
244
|
-
} else if (kind === 'throw') {
|
|
245
|
-
name = extractName('throw', node);
|
|
246
|
-
text = extractExpressionText(node);
|
|
247
|
-
} else if (kind === 'await') {
|
|
248
|
-
name = extractName('await', node);
|
|
249
|
-
text = extractExpressionText(node);
|
|
250
|
-
} else if (kind === 'string') {
|
|
251
|
-
// Skip trivial strings (length < 2 after removing quotes)
|
|
252
|
-
const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
|
|
253
|
-
if (content.length < 2) {
|
|
254
|
-
// Still recurse children
|
|
255
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
256
|
-
walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
name = truncate(content, 100);
|
|
261
|
-
text = truncate(node.text);
|
|
262
|
-
} else if (kind === 'regex') {
|
|
263
|
-
name = node.text || '?';
|
|
264
|
-
text = truncate(node.text);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const parentDef = findParentDef(defs, line);
|
|
268
|
-
let parentNodeId = null;
|
|
269
|
-
if (parentDef) {
|
|
270
|
-
parentNodeId = nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
rows.push({
|
|
274
|
-
file: relPath,
|
|
275
|
-
line,
|
|
276
|
-
kind,
|
|
277
|
-
name,
|
|
278
|
-
text,
|
|
279
|
-
receiver: null,
|
|
280
|
-
parentNodeId,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Don't recurse into the children of matched nodes for new/throw/await
|
|
284
|
-
// (we already extracted what we need, and nested strings inside them are noise)
|
|
285
|
-
if (kind !== 'string' && kind !== 'regex') return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
289
|
-
walkAst(node.child(i), defs, relPath, rows, nodeIdMap);
|
|
290
|
-
}
|
|
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);
|
|
291
164
|
}
|
|
292
165
|
|
|
293
166
|
// ─── Query ────────────────────────────────────────────────────────────
|
package/src/audit.js
CHANGED
|
@@ -9,10 +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 { outputResult } from './result-formatter.js';
|
|
15
|
-
import { isTestFile } from './test-filter.js';
|
|
14
|
+
import { explainData } from './queries.js';
|
|
16
15
|
|
|
17
16
|
// ─── Threshold resolution ───────────────────────────────────────────
|
|
18
17
|
|
|
@@ -336,87 +335,3 @@ function defaultHealth() {
|
|
|
336
335
|
thresholdBreaches: [],
|
|
337
336
|
};
|
|
338
337
|
}
|
|
339
|
-
|
|
340
|
-
// ─── CLI formatter ──────────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
export function audit(target, customDbPath, opts = {}) {
|
|
343
|
-
const data = auditData(target, customDbPath, opts);
|
|
344
|
-
|
|
345
|
-
if (outputResult(data, null, opts)) return;
|
|
346
|
-
|
|
347
|
-
if (data.functions.length === 0) {
|
|
348
|
-
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
console.log(`\n# Audit: ${target} (${data.kind})`);
|
|
353
|
-
console.log(` ${data.functions.length} function(s) analyzed\n`);
|
|
354
|
-
|
|
355
|
-
for (const fn of data.functions) {
|
|
356
|
-
const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
|
|
357
|
-
const roleTag = fn.role ? ` [${fn.role}]` : '';
|
|
358
|
-
console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
|
|
359
|
-
console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
|
|
360
|
-
if (fn.summary) console.log(` ${fn.summary}`);
|
|
361
|
-
if (fn.signature) {
|
|
362
|
-
if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
|
|
363
|
-
if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Health metrics
|
|
367
|
-
if (fn.health.cognitive != null) {
|
|
368
|
-
console.log(`\n Health:`);
|
|
369
|
-
console.log(
|
|
370
|
-
` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
|
|
371
|
-
);
|
|
372
|
-
console.log(` MI: ${fn.health.maintainabilityIndex}`);
|
|
373
|
-
if (fn.health.halstead.volume) {
|
|
374
|
-
console.log(
|
|
375
|
-
` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
if (fn.health.loc) {
|
|
379
|
-
console.log(
|
|
380
|
-
` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Threshold breaches
|
|
386
|
-
if (fn.health.thresholdBreaches.length > 0) {
|
|
387
|
-
console.log(`\n Threshold Breaches:`);
|
|
388
|
-
for (const b of fn.health.thresholdBreaches) {
|
|
389
|
-
const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
|
|
390
|
-
console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Impact
|
|
395
|
-
console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
|
|
396
|
-
for (const [level, nodes] of Object.entries(fn.impact.levels)) {
|
|
397
|
-
console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Call edges
|
|
401
|
-
if (fn.callees.length > 0) {
|
|
402
|
-
console.log(`\n Calls (${fn.callees.length}):`);
|
|
403
|
-
for (const c of fn.callees) {
|
|
404
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (fn.callers.length > 0) {
|
|
408
|
-
console.log(`\n Called by (${fn.callers.length}):`);
|
|
409
|
-
for (const c of fn.callers) {
|
|
410
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
if (fn.relatedTests.length > 0) {
|
|
414
|
-
console.log(`\n Tests (${fn.relatedTests.length}):`);
|
|
415
|
-
for (const t of fn.relatedTests) {
|
|
416
|
-
console.log(` ${t.file}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
console.log();
|
|
421
|
-
}
|
|
422
|
-
}
|
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,9 +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 } from './infrastructure/test-filter.js';
|
|
15
16
|
import { kindIcon } from './queries.js';
|
|
16
|
-
import { outputResult } from './result-formatter.js';
|
|
17
|
-
import { isTestFile } from './test-filter.js';
|
|
18
17
|
|
|
19
18
|
// ─── Git Helpers ────────────────────────────────────────────────────────
|
|
20
19
|
|
|
@@ -477,97 +476,3 @@ export function branchCompareMermaid(data) {
|
|
|
477
476
|
|
|
478
477
|
return lines.join('\n');
|
|
479
478
|
}
|
|
480
|
-
|
|
481
|
-
// ─── Text Formatting ────────────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
function formatText(data) {
|
|
484
|
-
if (data.error) return `Error: ${data.error}`;
|
|
485
|
-
|
|
486
|
-
const lines = [];
|
|
487
|
-
const shortBase = data.baseSha.slice(0, 7);
|
|
488
|
-
const shortTarget = data.targetSha.slice(0, 7);
|
|
489
|
-
|
|
490
|
-
lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
|
|
491
|
-
lines.push(` Base: ${data.baseRef} (${shortBase})`);
|
|
492
|
-
lines.push(` Target: ${data.targetRef} (${shortTarget})`);
|
|
493
|
-
lines.push(` Files changed: ${data.changedFiles.length}`);
|
|
494
|
-
|
|
495
|
-
if (data.added.length > 0) {
|
|
496
|
-
lines.push('');
|
|
497
|
-
lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
|
|
498
|
-
for (const sym of data.added) {
|
|
499
|
-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (data.removed.length > 0) {
|
|
504
|
-
lines.push('');
|
|
505
|
-
lines.push(
|
|
506
|
-
` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
|
|
507
|
-
);
|
|
508
|
-
for (const sym of data.removed) {
|
|
509
|
-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
|
|
510
|
-
if (sym.impact && sym.impact.length > 0) {
|
|
511
|
-
lines.push(
|
|
512
|
-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (data.changed.length > 0) {
|
|
519
|
-
lines.push('');
|
|
520
|
-
lines.push(
|
|
521
|
-
` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
|
|
522
|
-
);
|
|
523
|
-
for (const sym of data.changed) {
|
|
524
|
-
const parts = [];
|
|
525
|
-
if (sym.changes.lineCount !== 0) {
|
|
526
|
-
parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
|
|
527
|
-
}
|
|
528
|
-
if (sym.changes.fanIn !== 0) {
|
|
529
|
-
parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
|
|
530
|
-
}
|
|
531
|
-
if (sym.changes.fanOut !== 0) {
|
|
532
|
-
parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
|
|
533
|
-
}
|
|
534
|
-
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
535
|
-
lines.push(
|
|
536
|
-
` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
|
|
537
|
-
);
|
|
538
|
-
if (sym.impact && sym.impact.length > 0) {
|
|
539
|
-
lines.push(
|
|
540
|
-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const s = data.summary;
|
|
547
|
-
lines.push('');
|
|
548
|
-
lines.push(
|
|
549
|
-
` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
|
|
550
|
-
` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
|
|
551
|
-
(s.filesAffected > 0
|
|
552
|
-
? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
|
|
553
|
-
: ''),
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
return lines.join('\n');
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// ─── CLI Display Function ───────────────────────────────────────────────
|
|
560
|
-
|
|
561
|
-
export async function branchCompare(baseRef, targetRef, opts = {}) {
|
|
562
|
-
const data = await branchCompareData(baseRef, targetRef, opts);
|
|
563
|
-
|
|
564
|
-
if (opts.format === 'json') opts = { ...opts, json: true };
|
|
565
|
-
if (outputResult(data, null, opts)) return;
|
|
566
|
-
|
|
567
|
-
if (opts.format === 'mermaid') {
|
|
568
|
-
console.log(branchCompareMermaid(data));
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log(formatText(data));
|
|
573
|
-
}
|