pi-lens 2.0.37 → 2.0.39
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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/clients/architect-client.js +50 -20
- package/clients/architect-client.ts +61 -22
- package/clients/ast-grep-client.js +32 -127
- package/clients/ast-grep-client.test.js +2 -14
- package/clients/ast-grep-client.test.ts +2 -16
- package/clients/ast-grep-client.ts +34 -150
- package/clients/auto-loop.js +117 -0
- package/clients/auto-loop.ts +171 -0
- package/clients/biome-client.js +25 -22
- package/clients/biome-client.ts +28 -25
- package/clients/complexity-client.js +111 -74
- package/clients/complexity-client.ts +149 -105
- package/clients/dependency-checker.js +16 -30
- package/clients/dependency-checker.ts +15 -35
- package/clients/fix-scanners.js +195 -0
- package/clients/fix-scanners.ts +297 -0
- package/clients/interviewer-templates.js +75 -0
- package/clients/interviewer-templates.ts +90 -0
- package/clients/interviewer.js +73 -101
- package/clients/interviewer.ts +195 -140
- package/clients/knip-client.js +12 -4
- package/clients/knip-client.ts +21 -16
- package/clients/metrics-history.js +215 -0
- package/clients/metrics-history.ts +300 -0
- package/clients/scan-architectural-debt.js +62 -72
- package/clients/scan-architectural-debt.ts +79 -64
- package/clients/scan-utils.js +98 -0
- package/clients/scan-utils.ts +112 -0
- package/clients/sg-runner.js +138 -0
- package/clients/sg-runner.ts +168 -0
- package/clients/subprocess-client.js +0 -37
- package/clients/subprocess-client.ts +0 -60
- package/clients/ts-service.ts +1 -7
- package/clients/type-safety-client.js +3 -8
- package/clients/type-safety-client.ts +10 -14
- package/clients/typescript-client.js +55 -56
- package/clients/typescript-client.ts +94 -52
- package/default-architect.yaml +87 -0
- package/index.ts +143 -1165
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/large-class.yml +6 -2
- package/rules/ast-grep-rules/rules/long-method.yml +1 -1
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
- package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics History Tracker for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Persists complexity metrics per commit to track trends over time.
|
|
5
|
+
* Captures snapshots passively (session start) and explicitly (/lens-metrics).
|
|
6
|
+
*
|
|
7
|
+
* Storage: .pi-lens/metrics-history.json
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
// --- Constants ---
|
|
12
|
+
const HISTORY_FILE = ".pi-lens/metrics-history.json";
|
|
13
|
+
const MAX_HISTORY_PER_FILE = 20;
|
|
14
|
+
// --- Git Helpers ---
|
|
15
|
+
/**
|
|
16
|
+
* Get current git commit hash (short)
|
|
17
|
+
*/
|
|
18
|
+
function getCurrentCommit() {
|
|
19
|
+
try {
|
|
20
|
+
const { execSync } = require("node:child_process");
|
|
21
|
+
return execSync("git rev-parse --short HEAD", {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "unknown";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// --- History Management ---
|
|
31
|
+
/**
|
|
32
|
+
* Load history from disk (or return empty)
|
|
33
|
+
*/
|
|
34
|
+
export function loadHistory() {
|
|
35
|
+
const historyPath = path.join(process.cwd(), HISTORY_FILE);
|
|
36
|
+
if (!fs.existsSync(historyPath)) {
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
files: {},
|
|
40
|
+
capturedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(historyPath, "utf-8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {
|
|
49
|
+
version: 1,
|
|
50
|
+
files: {},
|
|
51
|
+
capturedAt: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Save history to disk
|
|
57
|
+
*/
|
|
58
|
+
export function saveHistory(history) {
|
|
59
|
+
const historyDir = path.join(process.cwd(), ".pi-lens");
|
|
60
|
+
if (!fs.existsSync(historyDir)) {
|
|
61
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
history.capturedAt = new Date().toISOString();
|
|
64
|
+
const historyPath = path.join(historyDir, "metrics-history.json");
|
|
65
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Capture a snapshot for a file's current metrics
|
|
69
|
+
*/
|
|
70
|
+
export function captureSnapshot(filePath, metrics, history) {
|
|
71
|
+
const hist = history ?? loadHistory();
|
|
72
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
73
|
+
const commit = getCurrentCommit();
|
|
74
|
+
const snapshot = {
|
|
75
|
+
commit,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
mi: Math.round(metrics.maintainabilityIndex * 10) / 10,
|
|
78
|
+
cognitive: metrics.cognitiveComplexity,
|
|
79
|
+
nesting: metrics.maxNestingDepth,
|
|
80
|
+
lines: metrics.linesOfCode,
|
|
81
|
+
};
|
|
82
|
+
const existing = hist.files[relativePath];
|
|
83
|
+
if (existing) {
|
|
84
|
+
// Append to history (cap at MAX_HISTORY_PER_FILE)
|
|
85
|
+
existing.history.push(snapshot);
|
|
86
|
+
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
87
|
+
existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
|
|
88
|
+
}
|
|
89
|
+
existing.latest = snapshot;
|
|
90
|
+
existing.trend = computeTrend(existing.history);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// New file
|
|
94
|
+
hist.files[relativePath] = {
|
|
95
|
+
latest: snapshot,
|
|
96
|
+
history: [snapshot],
|
|
97
|
+
trend: "stable",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return hist;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Capture snapshots for multiple files
|
|
104
|
+
*/
|
|
105
|
+
export function captureSnapshots(files) {
|
|
106
|
+
let history = loadHistory();
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
history = captureSnapshot(file.filePath, file.metrics, history);
|
|
109
|
+
}
|
|
110
|
+
saveHistory(history);
|
|
111
|
+
return history;
|
|
112
|
+
}
|
|
113
|
+
// --- Trend Analysis ---
|
|
114
|
+
/**
|
|
115
|
+
* Compute trend direction from history snapshots
|
|
116
|
+
* Uses last 3 snapshots for stability (or 2 if only 2 available)
|
|
117
|
+
*/
|
|
118
|
+
export function computeTrend(history) {
|
|
119
|
+
if (history.length < 2)
|
|
120
|
+
return "stable";
|
|
121
|
+
const recent = history.slice(-3);
|
|
122
|
+
const first = recent[0];
|
|
123
|
+
const last = recent[recent.length - 1];
|
|
124
|
+
// Use MI as primary indicator, cognitive as secondary
|
|
125
|
+
const miDelta = last.mi - first.mi;
|
|
126
|
+
const cogDelta = last.cognitive - first.cognitive;
|
|
127
|
+
// Thresholds (MI changes < 2 are noise)
|
|
128
|
+
if (miDelta > 2)
|
|
129
|
+
return "improving";
|
|
130
|
+
if (miDelta < -2)
|
|
131
|
+
return "regressing";
|
|
132
|
+
// If MI is stable, check cognitive
|
|
133
|
+
if (cogDelta < -10)
|
|
134
|
+
return "improving";
|
|
135
|
+
if (cogDelta > 10)
|
|
136
|
+
return "regressing";
|
|
137
|
+
return "stable";
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get delta between current snapshot and previous
|
|
141
|
+
*/
|
|
142
|
+
export function getDelta(history) {
|
|
143
|
+
if (!history || history.history.length < 2)
|
|
144
|
+
return null;
|
|
145
|
+
const current = history.history[history.history.length - 1];
|
|
146
|
+
const previous = history.history[history.history.length - 2];
|
|
147
|
+
return {
|
|
148
|
+
mi: Math.round((current.mi - previous.mi) * 10) / 10,
|
|
149
|
+
cognitive: current.cognitive - previous.cognitive,
|
|
150
|
+
trend: history.trend,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get trend emoji for display
|
|
155
|
+
*/
|
|
156
|
+
export function getTrendEmoji(trend) {
|
|
157
|
+
switch (trend) {
|
|
158
|
+
case "improving":
|
|
159
|
+
return "📈";
|
|
160
|
+
case "regressing":
|
|
161
|
+
return "📉";
|
|
162
|
+
default:
|
|
163
|
+
return "➡️";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get trend summary across all files
|
|
168
|
+
*/
|
|
169
|
+
export function getTrendSummary(history) {
|
|
170
|
+
let improving = 0;
|
|
171
|
+
let regressing = 0;
|
|
172
|
+
let stable = 0;
|
|
173
|
+
const regressions = [];
|
|
174
|
+
for (const [file, fileHistory] of Object.entries(history.files)) {
|
|
175
|
+
switch (fileHistory.trend) {
|
|
176
|
+
case "improving":
|
|
177
|
+
improving++;
|
|
178
|
+
break;
|
|
179
|
+
case "regressing":
|
|
180
|
+
regressing++;
|
|
181
|
+
const delta = getDelta(fileHistory);
|
|
182
|
+
if (delta) {
|
|
183
|
+
regressions.push({ file, miDelta: delta.mi });
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
stable++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Sort regressions by MI delta (worst first)
|
|
191
|
+
regressions.sort((a, b) => a.miDelta - b.miDelta);
|
|
192
|
+
return {
|
|
193
|
+
improving,
|
|
194
|
+
regressing,
|
|
195
|
+
stable,
|
|
196
|
+
worstRegressions: regressions.slice(0, 5),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Format trend for metrics table
|
|
201
|
+
*/
|
|
202
|
+
export function formatTrendCell(filePath, history) {
|
|
203
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
204
|
+
const fileHistory = history.files[relativePath];
|
|
205
|
+
if (!fileHistory || fileHistory.history.length < 2) {
|
|
206
|
+
return "—"; // No history
|
|
207
|
+
}
|
|
208
|
+
const delta = getDelta(fileHistory);
|
|
209
|
+
if (!delta)
|
|
210
|
+
return "—";
|
|
211
|
+
const emoji = getTrendEmoji(delta.trend);
|
|
212
|
+
const miSign = delta.mi > 0 ? "+" : "";
|
|
213
|
+
const miColor = delta.mi > 0 ? "🟢" : delta.mi < 0 ? "🔴" : "⚪";
|
|
214
|
+
return `${emoji} ${miColor}${miSign}${delta.mi}`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics History Tracker for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Persists complexity metrics per commit to track trends over time.
|
|
5
|
+
* Captures snapshots passively (session start) and explicitly (/lens-metrics).
|
|
6
|
+
*
|
|
7
|
+
* Storage: .pi-lens/metrics-history.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
// --- Types ---
|
|
14
|
+
|
|
15
|
+
export interface MetricSnapshot {
|
|
16
|
+
commit: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
mi: number;
|
|
19
|
+
cognitive: number;
|
|
20
|
+
nesting: number;
|
|
21
|
+
lines: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FileHistory {
|
|
25
|
+
latest: MetricSnapshot;
|
|
26
|
+
history: MetricSnapshot[];
|
|
27
|
+
trend: "improving" | "stable" | "regressing";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MetricsHistory {
|
|
31
|
+
version: number;
|
|
32
|
+
files: Record<string, FileHistory>;
|
|
33
|
+
capturedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TrendDirection = "improving" | "stable" | "regressing";
|
|
37
|
+
|
|
38
|
+
// --- Constants ---
|
|
39
|
+
|
|
40
|
+
const HISTORY_FILE = ".pi-lens/metrics-history.json";
|
|
41
|
+
const MAX_HISTORY_PER_FILE = 20;
|
|
42
|
+
|
|
43
|
+
// --- Git Helpers ---
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get current git commit hash (short)
|
|
47
|
+
*/
|
|
48
|
+
function getCurrentCommit(): string {
|
|
49
|
+
try {
|
|
50
|
+
const { execSync } = require("node:child_process");
|
|
51
|
+
return execSync("git rev-parse --short HEAD", {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return "unknown";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- History Management ---
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load history from disk (or return empty)
|
|
64
|
+
*/
|
|
65
|
+
export function loadHistory(): MetricsHistory {
|
|
66
|
+
const historyPath = path.join(process.cwd(), HISTORY_FILE);
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(historyPath)) {
|
|
69
|
+
return {
|
|
70
|
+
version: 1,
|
|
71
|
+
files: {},
|
|
72
|
+
capturedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(historyPath, "utf-8");
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
files: {},
|
|
83
|
+
capturedAt: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Save history to disk
|
|
90
|
+
*/
|
|
91
|
+
export function saveHistory(history: MetricsHistory): void {
|
|
92
|
+
const historyDir = path.join(process.cwd(), ".pi-lens");
|
|
93
|
+
if (!fs.existsSync(historyDir)) {
|
|
94
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
history.capturedAt = new Date().toISOString();
|
|
98
|
+
const historyPath = path.join(historyDir, "metrics-history.json");
|
|
99
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Capture a snapshot for a file's current metrics
|
|
104
|
+
*/
|
|
105
|
+
export function captureSnapshot(
|
|
106
|
+
filePath: string,
|
|
107
|
+
metrics: {
|
|
108
|
+
maintainabilityIndex: number;
|
|
109
|
+
cognitiveComplexity: number;
|
|
110
|
+
maxNestingDepth: number;
|
|
111
|
+
linesOfCode: number;
|
|
112
|
+
},
|
|
113
|
+
history?: MetricsHistory,
|
|
114
|
+
): MetricsHistory {
|
|
115
|
+
const hist = history ?? loadHistory();
|
|
116
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
117
|
+
const commit = getCurrentCommit();
|
|
118
|
+
|
|
119
|
+
const snapshot: MetricSnapshot = {
|
|
120
|
+
commit,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
mi: Math.round(metrics.maintainabilityIndex * 10) / 10,
|
|
123
|
+
cognitive: metrics.cognitiveComplexity,
|
|
124
|
+
nesting: metrics.maxNestingDepth,
|
|
125
|
+
lines: metrics.linesOfCode,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const existing = hist.files[relativePath];
|
|
129
|
+
|
|
130
|
+
if (existing) {
|
|
131
|
+
// Append to history (cap at MAX_HISTORY_PER_FILE)
|
|
132
|
+
existing.history.push(snapshot);
|
|
133
|
+
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
134
|
+
existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
|
|
135
|
+
}
|
|
136
|
+
existing.latest = snapshot;
|
|
137
|
+
existing.trend = computeTrend(existing.history);
|
|
138
|
+
} else {
|
|
139
|
+
// New file
|
|
140
|
+
hist.files[relativePath] = {
|
|
141
|
+
latest: snapshot,
|
|
142
|
+
history: [snapshot],
|
|
143
|
+
trend: "stable",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return hist;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Capture snapshots for multiple files
|
|
152
|
+
*/
|
|
153
|
+
export function captureSnapshots(
|
|
154
|
+
files: Array<{
|
|
155
|
+
filePath: string;
|
|
156
|
+
metrics: {
|
|
157
|
+
maintainabilityIndex: number;
|
|
158
|
+
cognitiveComplexity: number;
|
|
159
|
+
maxNestingDepth: number;
|
|
160
|
+
linesOfCode: number;
|
|
161
|
+
};
|
|
162
|
+
}>,
|
|
163
|
+
): MetricsHistory {
|
|
164
|
+
let history = loadHistory();
|
|
165
|
+
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
history = captureSnapshot(file.filePath, file.metrics, history);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
saveHistory(history);
|
|
171
|
+
return history;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Trend Analysis ---
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Compute trend direction from history snapshots
|
|
178
|
+
* Uses last 3 snapshots for stability (or 2 if only 2 available)
|
|
179
|
+
*/
|
|
180
|
+
export function computeTrend(history: MetricSnapshot[]): TrendDirection {
|
|
181
|
+
if (history.length < 2) return "stable";
|
|
182
|
+
|
|
183
|
+
const recent = history.slice(-3);
|
|
184
|
+
const first = recent[0];
|
|
185
|
+
const last = recent[recent.length - 1];
|
|
186
|
+
|
|
187
|
+
// Use MI as primary indicator, cognitive as secondary
|
|
188
|
+
const miDelta = last.mi - first.mi;
|
|
189
|
+
const cogDelta = last.cognitive - first.cognitive;
|
|
190
|
+
|
|
191
|
+
// Thresholds (MI changes < 2 are noise)
|
|
192
|
+
if (miDelta > 2) return "improving";
|
|
193
|
+
if (miDelta < -2) return "regressing";
|
|
194
|
+
|
|
195
|
+
// If MI is stable, check cognitive
|
|
196
|
+
if (cogDelta < -10) return "improving";
|
|
197
|
+
if (cogDelta > 10) return "regressing";
|
|
198
|
+
|
|
199
|
+
return "stable";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get delta between current snapshot and previous
|
|
204
|
+
*/
|
|
205
|
+
export function getDelta(history: FileHistory | null): {
|
|
206
|
+
mi: number;
|
|
207
|
+
cognitive: number;
|
|
208
|
+
trend: TrendDirection;
|
|
209
|
+
} | null {
|
|
210
|
+
if (!history || history.history.length < 2) return null;
|
|
211
|
+
|
|
212
|
+
const current = history.history[history.history.length - 1];
|
|
213
|
+
const previous = history.history[history.history.length - 2];
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
mi: Math.round((current.mi - previous.mi) * 10) / 10,
|
|
217
|
+
cognitive: current.cognitive - previous.cognitive,
|
|
218
|
+
trend: history.trend,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get trend emoji for display
|
|
224
|
+
*/
|
|
225
|
+
export function getTrendEmoji(trend: TrendDirection): string {
|
|
226
|
+
switch (trend) {
|
|
227
|
+
case "improving":
|
|
228
|
+
return "📈";
|
|
229
|
+
case "regressing":
|
|
230
|
+
return "📉";
|
|
231
|
+
default:
|
|
232
|
+
return "➡️";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get trend summary across all files
|
|
238
|
+
*/
|
|
239
|
+
export function getTrendSummary(history: MetricsHistory): {
|
|
240
|
+
improving: number;
|
|
241
|
+
regressing: number;
|
|
242
|
+
stable: number;
|
|
243
|
+
worstRegressions: Array<{ file: string; miDelta: number }>;
|
|
244
|
+
} {
|
|
245
|
+
let improving = 0;
|
|
246
|
+
let regressing = 0;
|
|
247
|
+
let stable = 0;
|
|
248
|
+
const regressions: Array<{ file: string; miDelta: number }> = [];
|
|
249
|
+
|
|
250
|
+
for (const [file, fileHistory] of Object.entries(history.files)) {
|
|
251
|
+
switch (fileHistory.trend) {
|
|
252
|
+
case "improving":
|
|
253
|
+
improving++;
|
|
254
|
+
break;
|
|
255
|
+
case "regressing":
|
|
256
|
+
regressing++;
|
|
257
|
+
const delta = getDelta(fileHistory);
|
|
258
|
+
if (delta) {
|
|
259
|
+
regressions.push({ file, miDelta: delta.mi });
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
stable++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Sort regressions by MI delta (worst first)
|
|
268
|
+
regressions.sort((a, b) => a.miDelta - b.miDelta);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
improving,
|
|
272
|
+
regressing,
|
|
273
|
+
stable,
|
|
274
|
+
worstRegressions: regressions.slice(0, 5),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Format trend for metrics table
|
|
280
|
+
*/
|
|
281
|
+
export function formatTrendCell(
|
|
282
|
+
filePath: string,
|
|
283
|
+
history: MetricsHistory,
|
|
284
|
+
): string {
|
|
285
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
286
|
+
const fileHistory = history.files[relativePath];
|
|
287
|
+
|
|
288
|
+
if (!fileHistory || fileHistory.history.length < 2) {
|
|
289
|
+
return "—"; // No history
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const delta = getDelta(fileHistory);
|
|
293
|
+
if (!delta) return "—";
|
|
294
|
+
|
|
295
|
+
const emoji = getTrendEmoji(delta.trend);
|
|
296
|
+
const miSign = delta.mi > 0 ? "+" : "";
|
|
297
|
+
const miColor = delta.mi > 0 ? "🟢" : delta.mi < 0 ? "🔴" : "⚪";
|
|
298
|
+
|
|
299
|
+
return `${emoji} ${miColor}${miSign}${delta.mi}`;
|
|
300
|
+
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Shared architectural debt scanning — used by booboo-fix and booboo-refactor.
|
|
3
3
|
* Scans ast-grep skip rules + complexity metrics + architect.yaml rules.
|
|
4
4
|
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
import * as path from "node:path";
|
|
8
|
+
import { getSourceFiles, parseAstGrepJson } from "./scan-utils.js";
|
|
7
9
|
/**
|
|
8
10
|
* Scan for skip-category ast-grep violations grouped by absolute file path.
|
|
9
11
|
*/
|
|
@@ -11,34 +13,35 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
|
|
|
11
13
|
const skipByFile = new Map();
|
|
12
14
|
if (!astGrepClient.isAvailable())
|
|
13
15
|
return skipByFile;
|
|
14
|
-
const { spawnSync } = require("node:child_process");
|
|
15
16
|
const sgResult = spawnSync("npx", [
|
|
16
|
-
"sg",
|
|
17
|
-
"
|
|
18
|
-
"--
|
|
17
|
+
"sg",
|
|
18
|
+
"scan",
|
|
19
|
+
"--config",
|
|
20
|
+
configPath,
|
|
21
|
+
"--json",
|
|
22
|
+
"--globs",
|
|
23
|
+
"!**/*.test.ts",
|
|
24
|
+
"--globs",
|
|
25
|
+
"!**/*.spec.ts",
|
|
26
|
+
"--globs",
|
|
27
|
+
"!**/test-utils.ts",
|
|
28
|
+
"--globs",
|
|
29
|
+
"!**/.pi-lens/**",
|
|
19
30
|
...(isTsProject ? ["--globs", "!**/*.js"] : []),
|
|
20
31
|
targetPath,
|
|
21
|
-
], {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
catch {
|
|
29
|
-
return [];
|
|
30
|
-
} })()
|
|
31
|
-
: raw.split("\n").flatMap((l) => { try {
|
|
32
|
-
return [JSON.parse(l)];
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return [];
|
|
36
|
-
} });
|
|
32
|
+
], {
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
shell: true,
|
|
36
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
37
|
+
});
|
|
38
|
+
const items = parseAstGrepJson(sgResult.stdout?.trim() ?? "");
|
|
37
39
|
for (const item of items) {
|
|
38
40
|
const rule = item.ruleId || item.rule?.title || item.name || "unknown";
|
|
39
41
|
if (!skipRules.has(rule))
|
|
40
42
|
continue;
|
|
41
|
-
const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
|
|
43
|
+
const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
|
|
44
|
+
1;
|
|
42
45
|
const absFile = path.resolve(item.file ?? "");
|
|
43
46
|
const list = skipByFile.get(absFile) ?? [];
|
|
44
47
|
list.push({ rule, line, note: ruleActions[rule]?.note ?? "" });
|
|
@@ -51,26 +54,19 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
|
|
|
51
54
|
*/
|
|
52
55
|
export function scanComplexityMetrics(complexityClient, targetPath, isTsProject) {
|
|
53
56
|
const metricsByFile = new Map();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
!/\.(test|spec)\.[jt]sx?$/.test(entry.name) &&
|
|
66
|
-
!(isTsProject && /\.js$/.test(entry.name))) {
|
|
67
|
-
const m = complexityClient.analyzeFile(full);
|
|
68
|
-
if (m)
|
|
69
|
-
metricsByFile.set(full, { mi: m.maintainabilityIndex, cognitive: m.cognitiveComplexity, nesting: m.maxNestingDepth });
|
|
70
|
-
}
|
|
57
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
58
|
+
for (const full of files) {
|
|
59
|
+
if (complexityClient.isSupportedFile(full) &&
|
|
60
|
+
!/\.(test|spec)\.[jt]sx?$/.test(path.basename(full))) {
|
|
61
|
+
const m = complexityClient.analyzeFile(full);
|
|
62
|
+
if (m)
|
|
63
|
+
metricsByFile.set(full, {
|
|
64
|
+
mi: m.maintainabilityIndex,
|
|
65
|
+
cognitive: m.cognitiveComplexity,
|
|
66
|
+
nesting: m.maxNestingDepth,
|
|
67
|
+
});
|
|
71
68
|
}
|
|
72
|
-
}
|
|
73
|
-
scanDir(targetPath);
|
|
69
|
+
}
|
|
74
70
|
return metricsByFile;
|
|
75
71
|
}
|
|
76
72
|
/**
|
|
@@ -81,37 +77,27 @@ export function scanArchitectViolations(architectClient, targetPath) {
|
|
|
81
77
|
const violationsByFile = new Map();
|
|
82
78
|
if (!architectClient.hasConfig())
|
|
83
79
|
return violationsByFile;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
96
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
97
|
-
const lineCount = content.split("\n").length;
|
|
98
|
-
const msgs = [];
|
|
99
|
-
// Check pattern violations
|
|
100
|
-
for (const v of architectClient.checkFile(relPath, content)) {
|
|
101
|
-
msgs.push(v.message);
|
|
102
|
-
}
|
|
103
|
-
// Check file size
|
|
104
|
-
const sizeV = architectClient.checkFileSize(relPath, lineCount);
|
|
105
|
-
if (sizeV) {
|
|
106
|
-
msgs.push(sizeV.message);
|
|
107
|
-
}
|
|
108
|
-
if (msgs.length > 0) {
|
|
109
|
-
violationsByFile.set(full, msgs);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
80
|
+
const isTsProject = fs.existsSync(path.join(targetPath, "tsconfig.json"));
|
|
81
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
82
|
+
for (const full of files) {
|
|
83
|
+
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
84
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
85
|
+
const lineCount = content.split("\n").length;
|
|
86
|
+
const msgs = [];
|
|
87
|
+
// Check pattern violations
|
|
88
|
+
for (const v of architectClient.checkFile(relPath, content)) {
|
|
89
|
+
const lineStr = v.line ? `L${v.line}: ` : "";
|
|
90
|
+
msgs.push(`${lineStr}${v.message}`);
|
|
112
91
|
}
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
// Check file size
|
|
93
|
+
const sizeV = architectClient.checkFileSize(relPath, lineCount);
|
|
94
|
+
if (sizeV) {
|
|
95
|
+
msgs.push(sizeV.message);
|
|
96
|
+
}
|
|
97
|
+
if (msgs.length > 0) {
|
|
98
|
+
violationsByFile.set(full, msgs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
115
101
|
return violationsByFile;
|
|
116
102
|
}
|
|
117
103
|
/**
|
|
@@ -170,9 +156,13 @@ export function scoreFiles(skipByFile, metricsByFile, architectViolations) {
|
|
|
170
156
|
export function extractCodeSnippet(filePath, firstLine, contextLines = 2, maxLines = 45) {
|
|
171
157
|
try {
|
|
172
158
|
const fileLines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
173
|
-
const start = Math.max(0,
|
|
159
|
+
const start = Math.max(0, firstLine - 1 - contextLines);
|
|
174
160
|
const end = Math.min(fileLines.length, start + maxLines);
|
|
175
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
snippet: fileLines.slice(start, end).join("\n"),
|
|
163
|
+
start: start + 1,
|
|
164
|
+
end,
|
|
165
|
+
};
|
|
176
166
|
}
|
|
177
167
|
catch {
|
|
178
168
|
return null;
|