@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/snapshot.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { findDbPath } from './db.js';
|
|
5
|
+
import { debug } from './logger.js';
|
|
6
|
+
|
|
7
|
+
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate a snapshot name (alphanumeric, hyphens, underscores only).
|
|
11
|
+
* Throws on invalid input.
|
|
12
|
+
*/
|
|
13
|
+
export function validateSnapshotName(name) {
|
|
14
|
+
if (!name || !NAME_RE.test(name)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Invalid snapshot name "${name}". Use only letters, digits, hyphens, and underscores.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return the snapshots directory for a given DB path.
|
|
23
|
+
*/
|
|
24
|
+
export function snapshotsDir(dbPath) {
|
|
25
|
+
return path.join(path.dirname(dbPath), 'snapshots');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Save a snapshot of the current graph database.
|
|
30
|
+
* Uses VACUUM INTO for an atomic, WAL-free copy.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} name - Snapshot name
|
|
33
|
+
* @param {object} [options]
|
|
34
|
+
* @param {string} [options.dbPath] - Explicit path to graph.db
|
|
35
|
+
* @param {boolean} [options.force] - Overwrite existing snapshot
|
|
36
|
+
* @returns {{ name: string, path: string, size: number }}
|
|
37
|
+
*/
|
|
38
|
+
export function snapshotSave(name, options = {}) {
|
|
39
|
+
validateSnapshotName(name);
|
|
40
|
+
const dbPath = options.dbPath || findDbPath();
|
|
41
|
+
if (!fs.existsSync(dbPath)) {
|
|
42
|
+
throw new Error(`Database not found: ${dbPath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dir = snapshotsDir(dbPath);
|
|
46
|
+
const dest = path.join(dir, `${name}.db`);
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(dest)) {
|
|
49
|
+
if (!options.force) {
|
|
50
|
+
throw new Error(`Snapshot "${name}" already exists. Use --force to overwrite.`);
|
|
51
|
+
}
|
|
52
|
+
fs.unlinkSync(dest);
|
|
53
|
+
debug(`Deleted existing snapshot: ${dest}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const db = new Database(dbPath, { readonly: true });
|
|
59
|
+
try {
|
|
60
|
+
db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
61
|
+
} finally {
|
|
62
|
+
db.close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const stat = fs.statSync(dest);
|
|
66
|
+
debug(`Snapshot saved: ${dest} (${stat.size} bytes)`);
|
|
67
|
+
return { name, path: dest, size: stat.size };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Restore a snapshot over the current graph database.
|
|
72
|
+
* Removes WAL/SHM sidecar files before overwriting.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} name - Snapshot name
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @param {string} [options.dbPath] - Explicit path to graph.db
|
|
77
|
+
*/
|
|
78
|
+
export function snapshotRestore(name, options = {}) {
|
|
79
|
+
validateSnapshotName(name);
|
|
80
|
+
const dbPath = options.dbPath || findDbPath();
|
|
81
|
+
const dir = snapshotsDir(dbPath);
|
|
82
|
+
const src = path.join(dir, `${name}.db`);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(src)) {
|
|
85
|
+
throw new Error(`Snapshot "${name}" not found at ${src}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Remove WAL/SHM sidecar files for a clean restore
|
|
89
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
90
|
+
const sidecar = dbPath + suffix;
|
|
91
|
+
if (fs.existsSync(sidecar)) {
|
|
92
|
+
fs.unlinkSync(sidecar);
|
|
93
|
+
debug(`Removed sidecar: ${sidecar}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.copyFileSync(src, dbPath);
|
|
98
|
+
debug(`Restored snapshot "${name}" → ${dbPath}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* List all saved snapshots.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [options]
|
|
105
|
+
* @param {string} [options.dbPath] - Explicit path to graph.db
|
|
106
|
+
* @returns {Array<{ name: string, path: string, size: number, createdAt: Date }>}
|
|
107
|
+
*/
|
|
108
|
+
export function snapshotList(options = {}) {
|
|
109
|
+
const dbPath = options.dbPath || findDbPath();
|
|
110
|
+
const dir = snapshotsDir(dbPath);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(dir)) return [];
|
|
113
|
+
|
|
114
|
+
return fs
|
|
115
|
+
.readdirSync(dir)
|
|
116
|
+
.filter((f) => f.endsWith('.db'))
|
|
117
|
+
.map((f) => {
|
|
118
|
+
const filePath = path.join(dir, f);
|
|
119
|
+
const stat = fs.statSync(filePath);
|
|
120
|
+
return {
|
|
121
|
+
name: f.replace(/\.db$/, ''),
|
|
122
|
+
path: filePath,
|
|
123
|
+
size: stat.size,
|
|
124
|
+
createdAt: stat.birthtime,
|
|
125
|
+
};
|
|
126
|
+
})
|
|
127
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a named snapshot.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} name - Snapshot name
|
|
134
|
+
* @param {object} [options]
|
|
135
|
+
* @param {string} [options.dbPath] - Explicit path to graph.db
|
|
136
|
+
*/
|
|
137
|
+
export function snapshotDelete(name, options = {}) {
|
|
138
|
+
validateSnapshotName(name);
|
|
139
|
+
const dbPath = options.dbPath || findDbPath();
|
|
140
|
+
const dir = snapshotsDir(dbPath);
|
|
141
|
+
const target = path.join(dir, `${name}.db`);
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(target)) {
|
|
144
|
+
throw new Error(`Snapshot "${name}" not found at ${target}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fs.unlinkSync(target);
|
|
148
|
+
debug(`Deleted snapshot: ${target}`);
|
|
149
|
+
}
|
package/src/structure.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { normalizePath } from './constants.js';
|
|
3
3
|
import { openReadonlyOrFail } from './db.js';
|
|
4
4
|
import { debug } from './logger.js';
|
|
5
|
+
import { paginateResult } from './paginate.js';
|
|
5
6
|
import { isTestFile } from './queries.js';
|
|
6
7
|
|
|
7
8
|
// ─── Build-time: insert directory nodes, contains edges, and metrics ────
|
|
@@ -33,8 +34,11 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
33
34
|
`);
|
|
34
35
|
|
|
35
36
|
// Clean previous directory nodes/edges (idempotent rebuild)
|
|
37
|
+
// Scope contains-edge delete to directory-sourced edges only,
|
|
38
|
+
// preserving symbol-level contains edges (file→def, class→method, etc.)
|
|
36
39
|
db.exec(`
|
|
37
|
-
DELETE FROM edges WHERE kind = 'contains'
|
|
40
|
+
DELETE FROM edges WHERE kind = 'contains'
|
|
41
|
+
AND source_id IN (SELECT id FROM nodes WHERE kind = 'directory');
|
|
38
42
|
DELETE FROM node_metrics;
|
|
39
43
|
DELETE FROM nodes WHERE kind = 'directory';
|
|
40
44
|
`);
|
|
@@ -463,7 +467,8 @@ export function structureData(customDbPath, opts = {}) {
|
|
|
463
467
|
}
|
|
464
468
|
}
|
|
465
469
|
|
|
466
|
-
|
|
470
|
+
const base = { directories: result, count: result.length };
|
|
471
|
+
return paginateResult(base, 'directories', { limit: opts.limit, offset: opts.offset });
|
|
467
472
|
}
|
|
468
473
|
|
|
469
474
|
/**
|
|
@@ -534,7 +539,8 @@ export function hotspotsData(customDbPath, opts = {}) {
|
|
|
534
539
|
}));
|
|
535
540
|
|
|
536
541
|
db.close();
|
|
537
|
-
|
|
542
|
+
const base = { metric, level, limit, hotspots };
|
|
543
|
+
return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
|
|
538
544
|
}
|
|
539
545
|
|
|
540
546
|
/**
|
package/src/triage.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { openReadonlyOrFail } from './db.js';
|
|
2
|
+
import { warn } from './logger.js';
|
|
3
|
+
import { paginateResult, printNdjson } from './paginate.js';
|
|
4
|
+
import { isTestFile } from './queries.js';
|
|
5
|
+
|
|
6
|
+
// ─── Constants ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const DEFAULT_WEIGHTS = {
|
|
9
|
+
fanIn: 0.25,
|
|
10
|
+
complexity: 0.3,
|
|
11
|
+
churn: 0.2,
|
|
12
|
+
role: 0.15,
|
|
13
|
+
mi: 0.1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ROLE_WEIGHTS = {
|
|
17
|
+
core: 1.0,
|
|
18
|
+
utility: 0.9,
|
|
19
|
+
entry: 0.8,
|
|
20
|
+
adapter: 0.5,
|
|
21
|
+
leaf: 0.2,
|
|
22
|
+
dead: 0.1,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_ROLE_WEIGHT = 0.5;
|
|
26
|
+
|
|
27
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Min-max normalize an array of numbers. All-equal → all zeros. */
|
|
30
|
+
function minMaxNormalize(values) {
|
|
31
|
+
const min = Math.min(...values);
|
|
32
|
+
const max = Math.max(...values);
|
|
33
|
+
if (max === min) return values.map(() => 0);
|
|
34
|
+
const range = max - min;
|
|
35
|
+
return values.map((v) => (v - min) / range);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Data Function ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compute composite risk scores for all symbols.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [customDbPath] - Path to graph.db
|
|
44
|
+
* @param {object} [opts]
|
|
45
|
+
* @returns {{ items: object[], summary: object, _pagination?: object }}
|
|
46
|
+
*/
|
|
47
|
+
export function triageData(customDbPath, opts = {}) {
|
|
48
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
49
|
+
const noTests = opts.noTests || false;
|
|
50
|
+
const fileFilter = opts.file || null;
|
|
51
|
+
const kindFilter = opts.kind || null;
|
|
52
|
+
const roleFilter = opts.role || null;
|
|
53
|
+
const minScore = opts.minScore != null ? Number(opts.minScore) : null;
|
|
54
|
+
const sort = opts.sort || 'risk';
|
|
55
|
+
const weights = { ...DEFAULT_WEIGHTS, ...(opts.weights || {}) };
|
|
56
|
+
|
|
57
|
+
// Build WHERE clause
|
|
58
|
+
let where = "WHERE n.kind IN ('function','method','class')";
|
|
59
|
+
const params = [];
|
|
60
|
+
|
|
61
|
+
if (noTests) {
|
|
62
|
+
where += ` AND n.file NOT LIKE '%.test.%'
|
|
63
|
+
AND n.file NOT LIKE '%.spec.%'
|
|
64
|
+
AND n.file NOT LIKE '%__test__%'
|
|
65
|
+
AND n.file NOT LIKE '%__tests__%'
|
|
66
|
+
AND n.file NOT LIKE '%.stories.%'`;
|
|
67
|
+
}
|
|
68
|
+
if (fileFilter) {
|
|
69
|
+
where += ' AND n.file LIKE ?';
|
|
70
|
+
params.push(`%${fileFilter}%`);
|
|
71
|
+
}
|
|
72
|
+
if (kindFilter) {
|
|
73
|
+
where += ' AND n.kind = ?';
|
|
74
|
+
params.push(kindFilter);
|
|
75
|
+
}
|
|
76
|
+
if (roleFilter) {
|
|
77
|
+
where += ' AND n.role = ?';
|
|
78
|
+
params.push(roleFilter);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let rows;
|
|
82
|
+
try {
|
|
83
|
+
rows = db
|
|
84
|
+
.prepare(
|
|
85
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
|
|
86
|
+
COALESCE(fi.cnt, 0) AS fan_in,
|
|
87
|
+
COALESCE(fc.cognitive, 0) AS cognitive,
|
|
88
|
+
COALESCE(fc.maintainability_index, 0) AS mi,
|
|
89
|
+
COALESCE(fc.cyclomatic, 0) AS cyclomatic,
|
|
90
|
+
COALESCE(fc.max_nesting, 0) AS max_nesting,
|
|
91
|
+
COALESCE(fcc.commit_count, 0) AS churn
|
|
92
|
+
FROM nodes n
|
|
93
|
+
LEFT JOIN (SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind='calls' GROUP BY target_id) fi
|
|
94
|
+
ON n.id = fi.target_id
|
|
95
|
+
LEFT JOIN function_complexity fc ON fc.node_id = n.id
|
|
96
|
+
LEFT JOIN file_commit_counts fcc ON n.file = fcc.file
|
|
97
|
+
${where}
|
|
98
|
+
ORDER BY n.file, n.line`,
|
|
99
|
+
)
|
|
100
|
+
.all(...params);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
warn(`triage query failed: ${err.message}`);
|
|
103
|
+
db.close();
|
|
104
|
+
return {
|
|
105
|
+
items: [],
|
|
106
|
+
summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Post-filter test files (belt-and-suspenders)
|
|
111
|
+
const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
|
|
112
|
+
|
|
113
|
+
if (filtered.length === 0) {
|
|
114
|
+
db.close();
|
|
115
|
+
return {
|
|
116
|
+
items: [],
|
|
117
|
+
summary: { total: 0, analyzed: 0, avgScore: 0, maxScore: 0, weights, signalCoverage: {} },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract raw signal arrays
|
|
122
|
+
const fanIns = filtered.map((r) => r.fan_in);
|
|
123
|
+
const cognitives = filtered.map((r) => r.cognitive);
|
|
124
|
+
const churns = filtered.map((r) => r.churn);
|
|
125
|
+
const mis = filtered.map((r) => r.mi);
|
|
126
|
+
|
|
127
|
+
// Min-max normalize
|
|
128
|
+
const normFanIns = minMaxNormalize(fanIns);
|
|
129
|
+
const normCognitives = minMaxNormalize(cognitives);
|
|
130
|
+
const normChurns = minMaxNormalize(churns);
|
|
131
|
+
// MI: higher is better, so invert: 1 - norm(mi)
|
|
132
|
+
const normMIsRaw = minMaxNormalize(mis);
|
|
133
|
+
const normMIs = normMIsRaw.map((v) => round4(1 - v));
|
|
134
|
+
|
|
135
|
+
// Compute risk scores
|
|
136
|
+
const items = filtered.map((r, i) => {
|
|
137
|
+
const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
|
|
138
|
+
const riskScore =
|
|
139
|
+
weights.fanIn * normFanIns[i] +
|
|
140
|
+
weights.complexity * normCognitives[i] +
|
|
141
|
+
weights.churn * normChurns[i] +
|
|
142
|
+
weights.role * roleWeight +
|
|
143
|
+
weights.mi * normMIs[i];
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
name: r.name,
|
|
147
|
+
kind: r.kind,
|
|
148
|
+
file: r.file,
|
|
149
|
+
line: r.line,
|
|
150
|
+
role: r.role || null,
|
|
151
|
+
fanIn: r.fan_in,
|
|
152
|
+
cognitive: r.cognitive,
|
|
153
|
+
churn: r.churn,
|
|
154
|
+
maintainabilityIndex: r.mi,
|
|
155
|
+
normFanIn: round4(normFanIns[i]),
|
|
156
|
+
normComplexity: round4(normCognitives[i]),
|
|
157
|
+
normChurn: round4(normChurns[i]),
|
|
158
|
+
normMI: round4(normMIs[i]),
|
|
159
|
+
roleWeight,
|
|
160
|
+
riskScore: round4(riskScore),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Apply minScore filter
|
|
165
|
+
const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
|
|
166
|
+
|
|
167
|
+
// Sort
|
|
168
|
+
const sortFns = {
|
|
169
|
+
risk: (a, b) => b.riskScore - a.riskScore,
|
|
170
|
+
complexity: (a, b) => b.cognitive - a.cognitive,
|
|
171
|
+
churn: (a, b) => b.churn - a.churn,
|
|
172
|
+
'fan-in': (a, b) => b.fanIn - a.fanIn,
|
|
173
|
+
mi: (a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
|
|
174
|
+
};
|
|
175
|
+
scored.sort(sortFns[sort] || sortFns.risk);
|
|
176
|
+
|
|
177
|
+
// Signal coverage: % of items with non-zero signal
|
|
178
|
+
const signalCoverage = {
|
|
179
|
+
complexity: round4(filtered.filter((r) => r.cognitive > 0).length / filtered.length),
|
|
180
|
+
churn: round4(filtered.filter((r) => r.churn > 0).length / filtered.length),
|
|
181
|
+
fanIn: round4(filtered.filter((r) => r.fan_in > 0).length / filtered.length),
|
|
182
|
+
mi: round4(filtered.filter((r) => r.mi > 0).length / filtered.length),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const scores = scored.map((it) => it.riskScore);
|
|
186
|
+
const avgScore =
|
|
187
|
+
scores.length > 0 ? round4(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;
|
|
188
|
+
const maxScore = scores.length > 0 ? round4(Math.max(...scores)) : 0;
|
|
189
|
+
|
|
190
|
+
const result = {
|
|
191
|
+
items: scored,
|
|
192
|
+
summary: {
|
|
193
|
+
total: filtered.length,
|
|
194
|
+
analyzed: scored.length,
|
|
195
|
+
avgScore,
|
|
196
|
+
maxScore,
|
|
197
|
+
weights,
|
|
198
|
+
signalCoverage,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
db.close();
|
|
203
|
+
|
|
204
|
+
return paginateResult(result, 'items', {
|
|
205
|
+
limit: opts.limit,
|
|
206
|
+
offset: opts.offset,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── CLI Formatter ────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Print triage results to console.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} [customDbPath]
|
|
216
|
+
* @param {object} [opts]
|
|
217
|
+
*/
|
|
218
|
+
export function triage(customDbPath, opts = {}) {
|
|
219
|
+
const data = triageData(customDbPath, opts);
|
|
220
|
+
|
|
221
|
+
if (opts.ndjson) {
|
|
222
|
+
printNdjson(data, 'items');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (opts.json) {
|
|
226
|
+
console.log(JSON.stringify(data, null, 2));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (data.items.length === 0) {
|
|
231
|
+
if (data.summary.total === 0) {
|
|
232
|
+
console.log('\nNo symbols found. Run "codegraph build" first.\n');
|
|
233
|
+
} else {
|
|
234
|
+
console.log('\nNo symbols match the given filters.\n');
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('\n# Risk Audit Queue\n');
|
|
240
|
+
|
|
241
|
+
console.log(
|
|
242
|
+
` ${'Symbol'.padEnd(35)} ${'File'.padEnd(28)} ${'Role'.padEnd(8)} ${'Score'.padStart(6)} ${'Fan-In'.padStart(7)} ${'Cog'.padStart(4)} ${'Churn'.padStart(6)} ${'MI'.padStart(5)}`,
|
|
243
|
+
);
|
|
244
|
+
console.log(
|
|
245
|
+
` ${'─'.repeat(35)} ${'─'.repeat(28)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(7)} ${'─'.repeat(4)} ${'─'.repeat(6)} ${'─'.repeat(5)}`,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
for (const it of data.items) {
|
|
249
|
+
const name = it.name.length > 33 ? `${it.name.slice(0, 32)}…` : it.name;
|
|
250
|
+
const file = it.file.length > 26 ? `…${it.file.slice(-25)}` : it.file;
|
|
251
|
+
const role = (it.role || '-').padEnd(8);
|
|
252
|
+
const score = it.riskScore.toFixed(2).padStart(6);
|
|
253
|
+
const fanIn = String(it.fanIn).padStart(7);
|
|
254
|
+
const cog = String(it.cognitive).padStart(4);
|
|
255
|
+
const churn = String(it.churn).padStart(6);
|
|
256
|
+
const mi = it.maintainabilityIndex > 0 ? String(it.maintainabilityIndex).padStart(5) : ' -';
|
|
257
|
+
console.log(
|
|
258
|
+
` ${name.padEnd(35)} ${file.padEnd(28)} ${role} ${score} ${fanIn} ${cog} ${churn} ${mi}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const s = data.summary;
|
|
263
|
+
console.log(
|
|
264
|
+
`\n ${s.analyzed} symbols scored (of ${s.total} total) | avg: ${s.avgScore.toFixed(2)} | max: ${s.maxScore.toFixed(2)} | sort: ${opts.sort || 'risk'}`,
|
|
265
|
+
);
|
|
266
|
+
console.log();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Utilities ────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function round4(n) {
|
|
272
|
+
return Math.round(n * 10000) / 10000;
|
|
273
|
+
}
|