@optave/codegraph 2.5.1 → 3.0.0
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 +216 -89
- package/package.json +8 -7
- package/src/ast.js +392 -0
- package/src/audit.js +423 -0
- package/src/batch.js +180 -0
- package/src/boundaries.js +346 -0
- package/src/builder.js +375 -92
- package/src/cfg.js +1451 -0
- package/src/change-journal.js +130 -0
- package/src/check.js +432 -0
- package/src/cli.js +734 -107
- package/src/cochange.js +5 -2
- package/src/communities.js +7 -1
- package/src/complexity.js +124 -17
- package/src/config.js +10 -0
- package/src/dataflow.js +1187 -0
- package/src/db.js +96 -0
- package/src/embedder.js +359 -47
- package/src/export.js +305 -0
- package/src/extractors/csharp.js +64 -1
- package/src/extractors/go.js +66 -1
- package/src/extractors/hcl.js +22 -0
- package/src/extractors/java.js +61 -1
- package/src/extractors/javascript.js +142 -0
- package/src/extractors/php.js +79 -0
- package/src/extractors/python.js +134 -0
- package/src/extractors/ruby.js +89 -0
- package/src/extractors/rust.js +71 -1
- package/src/flow.js +4 -4
- package/src/index.js +78 -3
- package/src/manifesto.js +69 -1
- package/src/mcp.js +702 -193
- package/src/owners.js +359 -0
- package/src/paginate.js +37 -2
- package/src/parser.js +8 -0
- package/src/queries.js +590 -50
- package/src/snapshot.js +149 -0
- package/src/structure.js +9 -3
- package/src/triage.js +273 -0
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
package/src/queries.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { evaluateBoundaries } from './boundaries.js';
|
|
4
5
|
import { coChangeForFiles } from './cochange.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
5
7
|
import { findCycles } from './cycles.js';
|
|
6
8
|
import { findDbPath, openReadonlyOrFail } from './db.js';
|
|
7
9
|
import { debug } from './logger.js';
|
|
8
|
-
import {
|
|
10
|
+
import { ownersForFiles } from './owners.js';
|
|
11
|
+
import { paginateResult, printNdjson } from './paginate.js';
|
|
9
12
|
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -56,7 +59,9 @@ export const FALSE_POSITIVE_NAMES = new Set([
|
|
|
56
59
|
export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
57
60
|
|
|
58
61
|
const FUNCTION_KINDS = ['function', 'method', 'class'];
|
|
59
|
-
|
|
62
|
+
|
|
63
|
+
// Original 10 kinds — used as default query scope
|
|
64
|
+
export const CORE_SYMBOL_KINDS = [
|
|
60
65
|
'function',
|
|
61
66
|
'method',
|
|
62
67
|
'class',
|
|
@@ -69,6 +74,39 @@ export const ALL_SYMBOL_KINDS = [
|
|
|
69
74
|
'module',
|
|
70
75
|
];
|
|
71
76
|
|
|
77
|
+
// Sub-declaration kinds (Phase 1)
|
|
78
|
+
export const EXTENDED_SYMBOL_KINDS = [
|
|
79
|
+
'parameter',
|
|
80
|
+
'property',
|
|
81
|
+
'constant',
|
|
82
|
+
// Phase 2 (reserved, not yet extracted):
|
|
83
|
+
// 'constructor', 'namespace', 'decorator', 'getter', 'setter',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Full set for --kind validation and MCP enum
|
|
87
|
+
export const EVERY_SYMBOL_KIND = [...CORE_SYMBOL_KINDS, ...EXTENDED_SYMBOL_KINDS];
|
|
88
|
+
|
|
89
|
+
// Backward compat: ALL_SYMBOL_KINDS stays as the core 10
|
|
90
|
+
export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
|
|
91
|
+
|
|
92
|
+
// ── Edge kind constants ─────────────────────────────────────────────
|
|
93
|
+
// Core edge kinds — coupling and dependency relationships
|
|
94
|
+
export const CORE_EDGE_KINDS = [
|
|
95
|
+
'imports',
|
|
96
|
+
'imports-type',
|
|
97
|
+
'reexports',
|
|
98
|
+
'calls',
|
|
99
|
+
'extends',
|
|
100
|
+
'implements',
|
|
101
|
+
'contains',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Structural edge kinds — parent/child and type relationships
|
|
105
|
+
export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
|
|
106
|
+
|
|
107
|
+
// Full set for MCP enum and validation
|
|
108
|
+
export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
|
|
109
|
+
|
|
72
110
|
export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
|
|
73
111
|
|
|
74
112
|
/**
|
|
@@ -187,6 +225,12 @@ export function kindIcon(kind) {
|
|
|
187
225
|
return 'I';
|
|
188
226
|
case 'type':
|
|
189
227
|
return 'T';
|
|
228
|
+
case 'parameter':
|
|
229
|
+
return 'p';
|
|
230
|
+
case 'property':
|
|
231
|
+
return '.';
|
|
232
|
+
case 'constant':
|
|
233
|
+
return 'C';
|
|
190
234
|
default:
|
|
191
235
|
return '-';
|
|
192
236
|
}
|
|
@@ -204,6 +248,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
204
248
|
return { query: name, results: [] };
|
|
205
249
|
}
|
|
206
250
|
|
|
251
|
+
const hc = new Map();
|
|
207
252
|
const results = nodes.map((node) => {
|
|
208
253
|
let callees = db
|
|
209
254
|
.prepare(`
|
|
@@ -227,10 +272,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
227
272
|
}
|
|
228
273
|
|
|
229
274
|
return {
|
|
230
|
-
|
|
231
|
-
kind: node.kind,
|
|
232
|
-
file: node.file,
|
|
233
|
-
line: node.line,
|
|
275
|
+
...normalizeSymbol(node, db, hc),
|
|
234
276
|
callees: callees.map((c) => ({
|
|
235
277
|
name: c.name,
|
|
236
278
|
kind: c.kind,
|
|
@@ -324,12 +366,12 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
|
324
366
|
const nodes = db
|
|
325
367
|
.prepare(`
|
|
326
368
|
SELECT n.*,
|
|
327
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind
|
|
328
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind
|
|
369
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
370
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
329
371
|
FROM nodes n
|
|
330
372
|
WHERE n.kind = 'file'
|
|
331
373
|
${testFilter}
|
|
332
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind
|
|
374
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
333
375
|
LIMIT ?
|
|
334
376
|
`)
|
|
335
377
|
.all(limit);
|
|
@@ -391,13 +433,15 @@ export function fileDepsData(file, customDbPath, opts = {}) {
|
|
|
391
433
|
});
|
|
392
434
|
|
|
393
435
|
db.close();
|
|
394
|
-
|
|
436
|
+
const base = { file, results };
|
|
437
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
395
438
|
}
|
|
396
439
|
|
|
397
440
|
export function fnDepsData(name, customDbPath, opts = {}) {
|
|
398
441
|
const db = openReadonlyOrFail(customDbPath);
|
|
399
442
|
const depth = opts.depth || 3;
|
|
400
443
|
const noTests = opts.noTests || false;
|
|
444
|
+
const hc = new Map();
|
|
401
445
|
|
|
402
446
|
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
403
447
|
if (nodes.length === 0) {
|
|
@@ -489,10 +533,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
489
533
|
}
|
|
490
534
|
|
|
491
535
|
return {
|
|
492
|
-
|
|
493
|
-
kind: node.kind,
|
|
494
|
-
file: node.file,
|
|
495
|
-
line: node.line,
|
|
536
|
+
...normalizeSymbol(node, db, hc),
|
|
496
537
|
callees: filteredCallees.map((c) => ({
|
|
497
538
|
name: c.name,
|
|
498
539
|
kind: c.kind,
|
|
@@ -511,13 +552,15 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
511
552
|
});
|
|
512
553
|
|
|
513
554
|
db.close();
|
|
514
|
-
|
|
555
|
+
const base = { name, results };
|
|
556
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
export function fnImpactData(name, customDbPath, opts = {}) {
|
|
518
560
|
const db = openReadonlyOrFail(customDbPath);
|
|
519
561
|
const maxDepth = opts.depth || 5;
|
|
520
562
|
const noTests = opts.noTests || false;
|
|
563
|
+
const hc = new Map();
|
|
521
564
|
|
|
522
565
|
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
523
566
|
if (nodes.length === 0) {
|
|
@@ -525,7 +568,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
525
568
|
return { name, results: [] };
|
|
526
569
|
}
|
|
527
570
|
|
|
528
|
-
const results = nodes.
|
|
571
|
+
const results = nodes.map((node) => {
|
|
529
572
|
const visited = new Set([node.id]);
|
|
530
573
|
const levels = {};
|
|
531
574
|
let frontier = [node.id];
|
|
@@ -554,17 +597,15 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
554
597
|
}
|
|
555
598
|
|
|
556
599
|
return {
|
|
557
|
-
|
|
558
|
-
kind: node.kind,
|
|
559
|
-
file: node.file,
|
|
560
|
-
line: node.line,
|
|
600
|
+
...normalizeSymbol(node, db, hc),
|
|
561
601
|
levels,
|
|
562
602
|
totalDependents: visited.size - 1,
|
|
563
603
|
};
|
|
564
604
|
});
|
|
565
605
|
|
|
566
606
|
db.close();
|
|
567
|
-
|
|
607
|
+
const base = { name, results };
|
|
608
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
568
609
|
}
|
|
569
610
|
|
|
570
611
|
export function pathData(from, to, customDbPath, opts = {}) {
|
|
@@ -998,20 +1039,60 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
998
1039
|
/* co_changes table doesn't exist — skip silently */
|
|
999
1040
|
}
|
|
1000
1041
|
|
|
1042
|
+
// Look up CODEOWNERS for changed + affected files
|
|
1043
|
+
let ownership = null;
|
|
1044
|
+
try {
|
|
1045
|
+
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
1046
|
+
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
1047
|
+
if (ownerResult.affectedOwners.length > 0) {
|
|
1048
|
+
ownership = {
|
|
1049
|
+
owners: Object.fromEntries(ownerResult.owners),
|
|
1050
|
+
affectedOwners: ownerResult.affectedOwners,
|
|
1051
|
+
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
} catch {
|
|
1055
|
+
/* CODEOWNERS missing or unreadable — skip silently */
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Check boundary violations scoped to changed files
|
|
1059
|
+
let boundaryViolations = [];
|
|
1060
|
+
let boundaryViolationCount = 0;
|
|
1061
|
+
try {
|
|
1062
|
+
const config = loadConfig(repoRoot);
|
|
1063
|
+
const boundaryConfig = config.manifesto?.boundaries;
|
|
1064
|
+
if (boundaryConfig) {
|
|
1065
|
+
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
1066
|
+
scopeFiles: [...changedRanges.keys()],
|
|
1067
|
+
noTests,
|
|
1068
|
+
});
|
|
1069
|
+
boundaryViolations = result.violations;
|
|
1070
|
+
boundaryViolationCount = result.violationCount;
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
/* boundary check failed — skip silently */
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1001
1076
|
db.close();
|
|
1002
|
-
|
|
1077
|
+
const base = {
|
|
1003
1078
|
changedFiles: changedRanges.size,
|
|
1004
1079
|
newFiles: [...newFiles],
|
|
1005
1080
|
affectedFunctions: functionResults,
|
|
1006
1081
|
affectedFiles: [...affectedFiles],
|
|
1007
1082
|
historicallyCoupled,
|
|
1083
|
+
ownership,
|
|
1084
|
+
boundaryViolations,
|
|
1085
|
+
boundaryViolationCount,
|
|
1008
1086
|
summary: {
|
|
1009
1087
|
functionsChanged: affectedFunctions.length,
|
|
1010
1088
|
callersAffected: allAffected.size,
|
|
1011
1089
|
filesAffected: affectedFiles.size,
|
|
1012
1090
|
historicallyCoupledCount: historicallyCoupled.length,
|
|
1091
|
+
ownersAffected: ownership ? ownership.affectedOwners.length : 0,
|
|
1092
|
+
boundaryViolationCount,
|
|
1013
1093
|
},
|
|
1014
1094
|
};
|
|
1095
|
+
return paginateResult(base, 'affectedFunctions', { limit: opts.limit, offset: opts.offset });
|
|
1015
1096
|
}
|
|
1016
1097
|
|
|
1017
1098
|
export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
@@ -1148,17 +1229,158 @@ export function listFunctionsData(customDbPath, opts = {}) {
|
|
|
1148
1229
|
|
|
1149
1230
|
let rows = db
|
|
1150
1231
|
.prepare(
|
|
1151
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1232
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1152
1233
|
)
|
|
1153
1234
|
.all(...params);
|
|
1154
1235
|
|
|
1155
1236
|
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
1156
1237
|
|
|
1238
|
+
const hc = new Map();
|
|
1239
|
+
const functions = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
1157
1240
|
db.close();
|
|
1158
|
-
const base = { count:
|
|
1241
|
+
const base = { count: functions.length, functions };
|
|
1159
1242
|
return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
|
|
1160
1243
|
}
|
|
1161
1244
|
|
|
1245
|
+
/**
|
|
1246
|
+
* Generator: stream functions one-by-one using .iterate() for memory efficiency.
|
|
1247
|
+
* @param {string} [customDbPath]
|
|
1248
|
+
* @param {object} [opts]
|
|
1249
|
+
* @param {boolean} [opts.noTests]
|
|
1250
|
+
* @param {string} [opts.file]
|
|
1251
|
+
* @param {string} [opts.pattern]
|
|
1252
|
+
* @yields {{ name: string, kind: string, file: string, line: number, role: string|null }}
|
|
1253
|
+
*/
|
|
1254
|
+
export function* iterListFunctions(customDbPath, opts = {}) {
|
|
1255
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
1256
|
+
try {
|
|
1257
|
+
const noTests = opts.noTests || false;
|
|
1258
|
+
const kinds = ['function', 'method', 'class'];
|
|
1259
|
+
const placeholders = kinds.map(() => '?').join(', ');
|
|
1260
|
+
|
|
1261
|
+
const conditions = [`kind IN (${placeholders})`];
|
|
1262
|
+
const params = [...kinds];
|
|
1263
|
+
|
|
1264
|
+
if (opts.file) {
|
|
1265
|
+
conditions.push('file LIKE ?');
|
|
1266
|
+
params.push(`%${opts.file}%`);
|
|
1267
|
+
}
|
|
1268
|
+
if (opts.pattern) {
|
|
1269
|
+
conditions.push('name LIKE ?');
|
|
1270
|
+
params.push(`%${opts.pattern}%`);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const stmt = db.prepare(
|
|
1274
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1275
|
+
);
|
|
1276
|
+
for (const row of stmt.iterate(...params)) {
|
|
1277
|
+
if (noTests && isTestFile(row.file)) continue;
|
|
1278
|
+
yield {
|
|
1279
|
+
name: row.name,
|
|
1280
|
+
kind: row.kind,
|
|
1281
|
+
file: row.file,
|
|
1282
|
+
line: row.line,
|
|
1283
|
+
endLine: row.end_line ?? null,
|
|
1284
|
+
role: row.role ?? null,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
} finally {
|
|
1288
|
+
db.close();
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Generator: stream role-classified symbols one-by-one.
|
|
1294
|
+
* @param {string} [customDbPath]
|
|
1295
|
+
* @param {object} [opts]
|
|
1296
|
+
* @param {boolean} [opts.noTests]
|
|
1297
|
+
* @param {string} [opts.role]
|
|
1298
|
+
* @param {string} [opts.file]
|
|
1299
|
+
* @yields {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string }}
|
|
1300
|
+
*/
|
|
1301
|
+
export function* iterRoles(customDbPath, opts = {}) {
|
|
1302
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
1303
|
+
try {
|
|
1304
|
+
const noTests = opts.noTests || false;
|
|
1305
|
+
const conditions = ['role IS NOT NULL'];
|
|
1306
|
+
const params = [];
|
|
1307
|
+
|
|
1308
|
+
if (opts.role) {
|
|
1309
|
+
conditions.push('role = ?');
|
|
1310
|
+
params.push(opts.role);
|
|
1311
|
+
}
|
|
1312
|
+
if (opts.file) {
|
|
1313
|
+
conditions.push('file LIKE ?');
|
|
1314
|
+
params.push(`%${opts.file}%`);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const stmt = db.prepare(
|
|
1318
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
1319
|
+
);
|
|
1320
|
+
for (const row of stmt.iterate(...params)) {
|
|
1321
|
+
if (noTests && isTestFile(row.file)) continue;
|
|
1322
|
+
yield {
|
|
1323
|
+
name: row.name,
|
|
1324
|
+
kind: row.kind,
|
|
1325
|
+
file: row.file,
|
|
1326
|
+
line: row.line,
|
|
1327
|
+
endLine: row.end_line ?? null,
|
|
1328
|
+
role: row.role ?? null,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
} finally {
|
|
1332
|
+
db.close();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Generator: stream symbol lookup results one-by-one.
|
|
1338
|
+
* @param {string} target - Symbol name to search for (partial match)
|
|
1339
|
+
* @param {string} [customDbPath]
|
|
1340
|
+
* @param {object} [opts]
|
|
1341
|
+
* @param {boolean} [opts.noTests]
|
|
1342
|
+
* @yields {{ name: string, kind: string, file: string, line: number, role: string|null, exported: boolean, uses: object[] }}
|
|
1343
|
+
*/
|
|
1344
|
+
export function* iterWhere(target, customDbPath, opts = {}) {
|
|
1345
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
1346
|
+
try {
|
|
1347
|
+
const noTests = opts.noTests || false;
|
|
1348
|
+
const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
|
|
1349
|
+
const stmt = db.prepare(
|
|
1350
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
|
|
1351
|
+
);
|
|
1352
|
+
const crossFileCallersStmt = db.prepare(
|
|
1353
|
+
`SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1354
|
+
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
1355
|
+
);
|
|
1356
|
+
const usesStmt = db.prepare(
|
|
1357
|
+
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1358
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1359
|
+
);
|
|
1360
|
+
for (const node of stmt.iterate(`%${target}%`, ...ALL_SYMBOL_KINDS)) {
|
|
1361
|
+
if (noTests && isTestFile(node.file)) continue;
|
|
1362
|
+
|
|
1363
|
+
const crossFileCallers = crossFileCallersStmt.get(node.id, node.file);
|
|
1364
|
+
const exported = crossFileCallers.cnt > 0;
|
|
1365
|
+
|
|
1366
|
+
let uses = usesStmt.all(node.id);
|
|
1367
|
+
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
1368
|
+
|
|
1369
|
+
yield {
|
|
1370
|
+
name: node.name,
|
|
1371
|
+
kind: node.kind,
|
|
1372
|
+
file: node.file,
|
|
1373
|
+
line: node.line,
|
|
1374
|
+
role: node.role || null,
|
|
1375
|
+
exported,
|
|
1376
|
+
uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
} finally {
|
|
1380
|
+
db.close();
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1162
1384
|
export function statsData(customDbPath, opts = {}) {
|
|
1163
1385
|
const db = openReadonlyOrFail(customDbPath);
|
|
1164
1386
|
const noTests = opts.noTests || false;
|
|
@@ -1553,8 +1775,7 @@ export function queryName(name, customDbPath, opts = {}) {
|
|
|
1553
1775
|
offset: opts.offset,
|
|
1554
1776
|
});
|
|
1555
1777
|
if (opts.ndjson) {
|
|
1556
|
-
|
|
1557
|
-
for (const r of data.results) console.log(JSON.stringify(r));
|
|
1778
|
+
printNdjson(data, 'results');
|
|
1558
1779
|
return;
|
|
1559
1780
|
}
|
|
1560
1781
|
if (opts.json) {
|
|
@@ -1586,7 +1807,11 @@ export function queryName(name, customDbPath, opts = {}) {
|
|
|
1586
1807
|
}
|
|
1587
1808
|
|
|
1588
1809
|
export function impactAnalysis(file, customDbPath, opts = {}) {
|
|
1589
|
-
const data = impactAnalysisData(file, customDbPath,
|
|
1810
|
+
const data = impactAnalysisData(file, customDbPath, opts);
|
|
1811
|
+
if (opts.ndjson) {
|
|
1812
|
+
printNdjson(data, 'sources');
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1590
1815
|
if (opts.json) {
|
|
1591
1816
|
console.log(JSON.stringify(data, null, 2));
|
|
1592
1817
|
return;
|
|
@@ -1645,7 +1870,11 @@ export function moduleMap(customDbPath, limit = 20, opts = {}) {
|
|
|
1645
1870
|
}
|
|
1646
1871
|
|
|
1647
1872
|
export function fileDeps(file, customDbPath, opts = {}) {
|
|
1648
|
-
const data = fileDepsData(file, customDbPath,
|
|
1873
|
+
const data = fileDepsData(file, customDbPath, opts);
|
|
1874
|
+
if (opts.ndjson) {
|
|
1875
|
+
printNdjson(data, 'results');
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1649
1878
|
if (opts.json) {
|
|
1650
1879
|
console.log(JSON.stringify(data, null, 2));
|
|
1651
1880
|
return;
|
|
@@ -1676,6 +1905,10 @@ export function fileDeps(file, customDbPath, opts = {}) {
|
|
|
1676
1905
|
|
|
1677
1906
|
export function fnDeps(name, customDbPath, opts = {}) {
|
|
1678
1907
|
const data = fnDepsData(name, customDbPath, opts);
|
|
1908
|
+
if (opts.ndjson) {
|
|
1909
|
+
printNdjson(data, 'results');
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1679
1912
|
if (opts.json) {
|
|
1680
1913
|
console.log(JSON.stringify(data, null, 2));
|
|
1681
1914
|
return;
|
|
@@ -1838,14 +2071,13 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1838
2071
|
const dbPath = findDbPath(customDbPath);
|
|
1839
2072
|
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
1840
2073
|
|
|
1841
|
-
|
|
2074
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
1842
2075
|
if (nodes.length === 0) {
|
|
1843
2076
|
db.close();
|
|
1844
2077
|
return { name, results: [] };
|
|
1845
2078
|
}
|
|
1846
2079
|
|
|
1847
|
-
//
|
|
1848
|
-
nodes = nodes.slice(0, 5);
|
|
2080
|
+
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
1849
2081
|
|
|
1850
2082
|
// File-lines cache to avoid re-reading the same file
|
|
1851
2083
|
const fileCache = new Map();
|
|
@@ -2033,6 +2265,17 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
2033
2265
|
/* table may not exist */
|
|
2034
2266
|
}
|
|
2035
2267
|
|
|
2268
|
+
// Children (parameters, properties, constants)
|
|
2269
|
+
let nodeChildren = [];
|
|
2270
|
+
try {
|
|
2271
|
+
nodeChildren = db
|
|
2272
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
2273
|
+
.all(node.id)
|
|
2274
|
+
.map((c) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null }));
|
|
2275
|
+
} catch {
|
|
2276
|
+
/* parent_id column may not exist */
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2036
2279
|
return {
|
|
2037
2280
|
name: node.name,
|
|
2038
2281
|
kind: node.kind,
|
|
@@ -2043,6 +2286,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
2043
2286
|
source,
|
|
2044
2287
|
signature,
|
|
2045
2288
|
complexity: complexityMetrics,
|
|
2289
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
2046
2290
|
callees,
|
|
2047
2291
|
callers,
|
|
2048
2292
|
relatedTests,
|
|
@@ -2050,11 +2294,16 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
2050
2294
|
});
|
|
2051
2295
|
|
|
2052
2296
|
db.close();
|
|
2053
|
-
|
|
2297
|
+
const base = { name, results };
|
|
2298
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2054
2299
|
}
|
|
2055
2300
|
|
|
2056
2301
|
export function context(name, customDbPath, opts = {}) {
|
|
2057
2302
|
const data = contextData(name, customDbPath, opts);
|
|
2303
|
+
if (opts.ndjson) {
|
|
2304
|
+
printNdjson(data, 'results');
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2058
2307
|
if (opts.json) {
|
|
2059
2308
|
console.log(JSON.stringify(data, null, 2));
|
|
2060
2309
|
return;
|
|
@@ -2077,6 +2326,15 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2077
2326
|
console.log();
|
|
2078
2327
|
}
|
|
2079
2328
|
|
|
2329
|
+
// Children
|
|
2330
|
+
if (r.children && r.children.length > 0) {
|
|
2331
|
+
console.log(`## Children (${r.children.length})`);
|
|
2332
|
+
for (const c of r.children) {
|
|
2333
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
2334
|
+
}
|
|
2335
|
+
console.log();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2080
2338
|
// Complexity
|
|
2081
2339
|
if (r.complexity) {
|
|
2082
2340
|
const cx = r.complexity;
|
|
@@ -2149,6 +2407,69 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2149
2407
|
}
|
|
2150
2408
|
}
|
|
2151
2409
|
|
|
2410
|
+
// ─── childrenData ───────────────────────────────────────────────────────
|
|
2411
|
+
|
|
2412
|
+
export function childrenData(name, customDbPath, opts = {}) {
|
|
2413
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
2414
|
+
const noTests = opts.noTests || false;
|
|
2415
|
+
|
|
2416
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
2417
|
+
if (nodes.length === 0) {
|
|
2418
|
+
db.close();
|
|
2419
|
+
return { name, results: [] };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const results = nodes.map((node) => {
|
|
2423
|
+
let children;
|
|
2424
|
+
try {
|
|
2425
|
+
children = db
|
|
2426
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
2427
|
+
.all(node.id);
|
|
2428
|
+
} catch {
|
|
2429
|
+
children = [];
|
|
2430
|
+
}
|
|
2431
|
+
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
2432
|
+
return {
|
|
2433
|
+
name: node.name,
|
|
2434
|
+
kind: node.kind,
|
|
2435
|
+
file: node.file,
|
|
2436
|
+
line: node.line,
|
|
2437
|
+
children: children.map((c) => ({
|
|
2438
|
+
name: c.name,
|
|
2439
|
+
kind: c.kind,
|
|
2440
|
+
line: c.line,
|
|
2441
|
+
endLine: c.end_line || null,
|
|
2442
|
+
})),
|
|
2443
|
+
};
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
db.close();
|
|
2447
|
+
const base = { name, results };
|
|
2448
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
export function children(name, customDbPath, opts = {}) {
|
|
2452
|
+
const data = childrenData(name, customDbPath, opts);
|
|
2453
|
+
if (opts.json) {
|
|
2454
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
if (data.results.length === 0) {
|
|
2458
|
+
console.log(`No symbol matching "${name}"`);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
for (const r of data.results) {
|
|
2462
|
+
console.log(`\n${kindIcon(r.kind)} ${r.name} ${r.file}:${r.line}`);
|
|
2463
|
+
if (r.children.length === 0) {
|
|
2464
|
+
console.log(' (no children)');
|
|
2465
|
+
} else {
|
|
2466
|
+
for (const c of r.children) {
|
|
2467
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2152
2473
|
// ─── explainData ────────────────────────────────────────────────────────
|
|
2153
2474
|
|
|
2154
2475
|
function isFileLikeTarget(target) {
|
|
@@ -2271,6 +2592,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2271
2592
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
2272
2593
|
if (nodes.length === 0) return [];
|
|
2273
2594
|
|
|
2595
|
+
const hc = new Map();
|
|
2274
2596
|
return nodes.slice(0, 10).map((node) => {
|
|
2275
2597
|
const fileLines = getFileLines(node.file);
|
|
2276
2598
|
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
@@ -2328,12 +2650,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2328
2650
|
}
|
|
2329
2651
|
|
|
2330
2652
|
return {
|
|
2331
|
-
|
|
2332
|
-
kind: node.kind,
|
|
2333
|
-
file: node.file,
|
|
2334
|
-
line: node.line,
|
|
2335
|
-
role: node.role || null,
|
|
2336
|
-
endLine: node.end_line || null,
|
|
2653
|
+
...normalizeSymbol(node, db, hc),
|
|
2337
2654
|
lineCount,
|
|
2338
2655
|
summary,
|
|
2339
2656
|
signature,
|
|
@@ -2410,11 +2727,16 @@ export function explainData(target, customDbPath, opts = {}) {
|
|
|
2410
2727
|
}
|
|
2411
2728
|
|
|
2412
2729
|
db.close();
|
|
2413
|
-
|
|
2730
|
+
const base = { target, kind, results };
|
|
2731
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2414
2732
|
}
|
|
2415
2733
|
|
|
2416
2734
|
export function explain(target, customDbPath, opts = {}) {
|
|
2417
2735
|
const data = explainData(target, customDbPath, opts);
|
|
2736
|
+
if (opts.ndjson) {
|
|
2737
|
+
printNdjson(data, 'results');
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2418
2740
|
if (opts.json) {
|
|
2419
2741
|
console.log(JSON.stringify(data, null, 2));
|
|
2420
2742
|
return;
|
|
@@ -2541,6 +2863,41 @@ export function explain(target, customDbPath, opts = {}) {
|
|
|
2541
2863
|
|
|
2542
2864
|
// ─── whereData ──────────────────────────────────────────────────────────
|
|
2543
2865
|
|
|
2866
|
+
function getFileHash(db, file) {
|
|
2867
|
+
const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file);
|
|
2868
|
+
return row ? row.hash : null;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
/**
|
|
2872
|
+
* Normalize a raw DB/query row into the stable 7-field symbol shape.
|
|
2873
|
+
* @param {object} row - Raw row (from SELECT * or explicit columns)
|
|
2874
|
+
* @param {object} [db] - Open DB handle; when null, fileHash will be null
|
|
2875
|
+
* @param {Map} [hashCache] - Optional per-file cache to avoid repeated getFileHash calls
|
|
2876
|
+
* @returns {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string|null, fileHash: string|null }}
|
|
2877
|
+
*/
|
|
2878
|
+
export function normalizeSymbol(row, db, hashCache) {
|
|
2879
|
+
let fileHash = null;
|
|
2880
|
+
if (db) {
|
|
2881
|
+
if (hashCache) {
|
|
2882
|
+
if (!hashCache.has(row.file)) {
|
|
2883
|
+
hashCache.set(row.file, getFileHash(db, row.file));
|
|
2884
|
+
}
|
|
2885
|
+
fileHash = hashCache.get(row.file);
|
|
2886
|
+
} else {
|
|
2887
|
+
fileHash = getFileHash(db, row.file);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return {
|
|
2891
|
+
name: row.name,
|
|
2892
|
+
kind: row.kind,
|
|
2893
|
+
file: row.file,
|
|
2894
|
+
line: row.line,
|
|
2895
|
+
endLine: row.end_line ?? row.endLine ?? null,
|
|
2896
|
+
role: row.role ?? null,
|
|
2897
|
+
fileHash,
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2544
2901
|
function whereSymbolImpl(db, target, noTests) {
|
|
2545
2902
|
const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
|
|
2546
2903
|
let nodes = db
|
|
@@ -2550,6 +2907,7 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2550
2907
|
.all(`%${target}%`, ...ALL_SYMBOL_KINDS);
|
|
2551
2908
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
2552
2909
|
|
|
2910
|
+
const hc = new Map();
|
|
2553
2911
|
return nodes.map((node) => {
|
|
2554
2912
|
const crossFileCallers = db
|
|
2555
2913
|
.prepare(
|
|
@@ -2568,11 +2926,7 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2568
2926
|
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
2569
2927
|
|
|
2570
2928
|
return {
|
|
2571
|
-
|
|
2572
|
-
kind: node.kind,
|
|
2573
|
-
file: node.file,
|
|
2574
|
-
line: node.line,
|
|
2575
|
-
role: node.role || null,
|
|
2929
|
+
...normalizeSymbol(node, db, hc),
|
|
2576
2930
|
exported,
|
|
2577
2931
|
uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
|
|
2578
2932
|
};
|
|
@@ -2622,6 +2976,7 @@ function whereFileImpl(db, target) {
|
|
|
2622
2976
|
|
|
2623
2977
|
return {
|
|
2624
2978
|
file: fn.file,
|
|
2979
|
+
fileHash: getFileHash(db, fn.file),
|
|
2625
2980
|
symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })),
|
|
2626
2981
|
imports,
|
|
2627
2982
|
importedBy,
|
|
@@ -2645,8 +3000,7 @@ export function whereData(target, customDbPath, opts = {}) {
|
|
|
2645
3000
|
export function where(target, customDbPath, opts = {}) {
|
|
2646
3001
|
const data = whereData(target, customDbPath, opts);
|
|
2647
3002
|
if (opts.ndjson) {
|
|
2648
|
-
|
|
2649
|
-
for (const r of data.results) console.log(JSON.stringify(r));
|
|
3003
|
+
printNdjson(data, 'results');
|
|
2650
3004
|
return;
|
|
2651
3005
|
}
|
|
2652
3006
|
if (opts.json) {
|
|
@@ -2718,7 +3072,7 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2718
3072
|
|
|
2719
3073
|
let rows = db
|
|
2720
3074
|
.prepare(
|
|
2721
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
3075
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
2722
3076
|
)
|
|
2723
3077
|
.all(...params);
|
|
2724
3078
|
|
|
@@ -2729,16 +3083,17 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2729
3083
|
summary[r.role] = (summary[r.role] || 0) + 1;
|
|
2730
3084
|
}
|
|
2731
3085
|
|
|
3086
|
+
const hc = new Map();
|
|
3087
|
+
const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
2732
3088
|
db.close();
|
|
2733
|
-
const base = { count:
|
|
3089
|
+
const base = { count: symbols.length, summary, symbols };
|
|
2734
3090
|
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
2735
3091
|
}
|
|
2736
3092
|
|
|
2737
3093
|
export function roles(customDbPath, opts = {}) {
|
|
2738
3094
|
const data = rolesData(customDbPath, opts);
|
|
2739
3095
|
if (opts.ndjson) {
|
|
2740
|
-
|
|
2741
|
-
for (const s of data.symbols) console.log(JSON.stringify(s));
|
|
3096
|
+
printNdjson(data, 'symbols');
|
|
2742
3097
|
return;
|
|
2743
3098
|
}
|
|
2744
3099
|
if (opts.json) {
|
|
@@ -2777,8 +3132,172 @@ export function roles(customDbPath, opts = {}) {
|
|
|
2777
3132
|
}
|
|
2778
3133
|
}
|
|
2779
3134
|
|
|
3135
|
+
// ─── exportsData ─────────────────────────────────────────────────────
|
|
3136
|
+
|
|
3137
|
+
function exportsFileImpl(db, target, noTests, getFileLines) {
|
|
3138
|
+
const fileNodes = db
|
|
3139
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
3140
|
+
.all(`%${target}%`);
|
|
3141
|
+
if (fileNodes.length === 0) return [];
|
|
3142
|
+
|
|
3143
|
+
return fileNodes.map((fn) => {
|
|
3144
|
+
const symbols = db
|
|
3145
|
+
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
3146
|
+
.all(fn.file);
|
|
3147
|
+
|
|
3148
|
+
// IDs of symbols that have incoming calls from other files (exported)
|
|
3149
|
+
const exportedIds = new Set(
|
|
3150
|
+
db
|
|
3151
|
+
.prepare(
|
|
3152
|
+
`SELECT DISTINCT e.target_id FROM edges e
|
|
3153
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
3154
|
+
JOIN nodes target ON e.target_id = target.id
|
|
3155
|
+
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
3156
|
+
)
|
|
3157
|
+
.all(fn.file, fn.file)
|
|
3158
|
+
.map((r) => r.target_id),
|
|
3159
|
+
);
|
|
3160
|
+
|
|
3161
|
+
const exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
3162
|
+
const internalCount = symbols.length - exported.length;
|
|
3163
|
+
|
|
3164
|
+
const results = exported.map((s) => {
|
|
3165
|
+
const fileLines = getFileLines(fn.file);
|
|
3166
|
+
|
|
3167
|
+
let consumers = db
|
|
3168
|
+
.prepare(
|
|
3169
|
+
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
3170
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
3171
|
+
)
|
|
3172
|
+
.all(s.id);
|
|
3173
|
+
if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file));
|
|
3174
|
+
|
|
3175
|
+
return {
|
|
3176
|
+
name: s.name,
|
|
3177
|
+
kind: s.kind,
|
|
3178
|
+
line: s.line,
|
|
3179
|
+
endLine: s.end_line ?? null,
|
|
3180
|
+
role: s.role || null,
|
|
3181
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
3182
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
3183
|
+
consumers: consumers.map((c) => ({ name: c.name, file: c.file, line: c.line })),
|
|
3184
|
+
consumerCount: consumers.length,
|
|
3185
|
+
};
|
|
3186
|
+
});
|
|
3187
|
+
|
|
3188
|
+
// Files that re-export this file (barrel → this file)
|
|
3189
|
+
const reexports = db
|
|
3190
|
+
.prepare(
|
|
3191
|
+
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
3192
|
+
WHERE e.target_id = ? AND e.kind = 'reexports'`,
|
|
3193
|
+
)
|
|
3194
|
+
.all(fn.id)
|
|
3195
|
+
.map((r) => ({ file: r.file }));
|
|
3196
|
+
|
|
3197
|
+
return {
|
|
3198
|
+
file: fn.file,
|
|
3199
|
+
results,
|
|
3200
|
+
reexports,
|
|
3201
|
+
totalExported: exported.length,
|
|
3202
|
+
totalInternal: internalCount,
|
|
3203
|
+
};
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
export function exportsData(file, customDbPath, opts = {}) {
|
|
3208
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
3209
|
+
const noTests = opts.noTests || false;
|
|
3210
|
+
|
|
3211
|
+
const dbFilePath = findDbPath(customDbPath);
|
|
3212
|
+
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
3213
|
+
|
|
3214
|
+
const fileCache = new Map();
|
|
3215
|
+
function getFileLines(file) {
|
|
3216
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
3217
|
+
try {
|
|
3218
|
+
const absPath = safePath(repoRoot, file);
|
|
3219
|
+
if (!absPath) {
|
|
3220
|
+
fileCache.set(file, null);
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
3224
|
+
fileCache.set(file, lines);
|
|
3225
|
+
return lines;
|
|
3226
|
+
} catch {
|
|
3227
|
+
fileCache.set(file, null);
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
const fileResults = exportsFileImpl(db, file, noTests, getFileLines);
|
|
3233
|
+
db.close();
|
|
3234
|
+
|
|
3235
|
+
if (fileResults.length === 0) {
|
|
3236
|
+
return paginateResult(
|
|
3237
|
+
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0 },
|
|
3238
|
+
'results',
|
|
3239
|
+
{ limit: opts.limit, offset: opts.offset },
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// For single-file match return flat; for multi-match return first (like explainData)
|
|
3244
|
+
const first = fileResults[0];
|
|
3245
|
+
const base = {
|
|
3246
|
+
file: first.file,
|
|
3247
|
+
results: first.results,
|
|
3248
|
+
reexports: first.reexports,
|
|
3249
|
+
totalExported: first.totalExported,
|
|
3250
|
+
totalInternal: first.totalInternal,
|
|
3251
|
+
};
|
|
3252
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
export function fileExports(file, customDbPath, opts = {}) {
|
|
3256
|
+
const data = exportsData(file, customDbPath, opts);
|
|
3257
|
+
if (opts.ndjson) {
|
|
3258
|
+
printNdjson(data, 'results');
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (opts.json) {
|
|
3262
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
if (data.results.length === 0) {
|
|
3267
|
+
console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
console.log(
|
|
3272
|
+
`\n# ${data.file} — ${data.totalExported} exported, ${data.totalInternal} internal\n`,
|
|
3273
|
+
);
|
|
3274
|
+
|
|
3275
|
+
for (const sym of data.results) {
|
|
3276
|
+
const icon = kindIcon(sym.kind);
|
|
3277
|
+
const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
|
|
3278
|
+
const role = sym.role ? ` [${sym.role}]` : '';
|
|
3279
|
+
console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
|
|
3280
|
+
if (sym.consumers.length === 0) {
|
|
3281
|
+
console.log(' (no consumers)');
|
|
3282
|
+
} else {
|
|
3283
|
+
for (const c of sym.consumers) {
|
|
3284
|
+
console.log(` <- ${c.name} (${c.file}:${c.line})`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
if (data.reexports.length > 0) {
|
|
3290
|
+
console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
|
|
3291
|
+
}
|
|
3292
|
+
console.log();
|
|
3293
|
+
}
|
|
3294
|
+
|
|
2780
3295
|
export function fnImpact(name, customDbPath, opts = {}) {
|
|
2781
3296
|
const data = fnImpactData(name, customDbPath, opts);
|
|
3297
|
+
if (opts.ndjson) {
|
|
3298
|
+
printNdjson(data, 'results');
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
2782
3301
|
if (opts.json) {
|
|
2783
3302
|
console.log(JSON.stringify(data, null, 2));
|
|
2784
3303
|
return;
|
|
@@ -2811,6 +3330,10 @@ export function diffImpact(customDbPath, opts = {}) {
|
|
|
2811
3330
|
return;
|
|
2812
3331
|
}
|
|
2813
3332
|
const data = diffImpactData(customDbPath, opts);
|
|
3333
|
+
if (opts.ndjson) {
|
|
3334
|
+
printNdjson(data, 'affectedFunctions');
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
2814
3337
|
if (opts.json || opts.format === 'json') {
|
|
2815
3338
|
console.log(JSON.stringify(data, null, 2));
|
|
2816
3339
|
return;
|
|
@@ -2845,11 +3368,28 @@ export function diffImpact(customDbPath, opts = {}) {
|
|
|
2845
3368
|
);
|
|
2846
3369
|
}
|
|
2847
3370
|
}
|
|
3371
|
+
if (data.ownership) {
|
|
3372
|
+
console.log(`\n Affected owners: ${data.ownership.affectedOwners.join(', ')}`);
|
|
3373
|
+
console.log(` Suggested reviewers: ${data.ownership.suggestedReviewers.join(', ')}`);
|
|
3374
|
+
}
|
|
3375
|
+
if (data.boundaryViolations && data.boundaryViolations.length > 0) {
|
|
3376
|
+
console.log(`\n Boundary violations (${data.boundaryViolationCount}):\n`);
|
|
3377
|
+
for (const v of data.boundaryViolations) {
|
|
3378
|
+
console.log(` [${v.name}] ${v.file} -> ${v.targetFile}`);
|
|
3379
|
+
if (v.message) console.log(` ${v.message}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
2848
3382
|
if (data.summary) {
|
|
2849
3383
|
let summaryLine = `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files`;
|
|
2850
3384
|
if (data.summary.historicallyCoupledCount > 0) {
|
|
2851
3385
|
summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
|
|
2852
3386
|
}
|
|
3387
|
+
if (data.summary.ownersAffected > 0) {
|
|
3388
|
+
summaryLine += `, ${data.summary.ownersAffected} owners affected`;
|
|
3389
|
+
}
|
|
3390
|
+
if (data.summary.boundaryViolationCount > 0) {
|
|
3391
|
+
summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
|
|
3392
|
+
}
|
|
2853
3393
|
console.log(`${summaryLine}\n`);
|
|
2854
3394
|
}
|
|
2855
3395
|
}
|