pi-lens 2.0.39 → 2.0.40
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 +7 -1
- package/clients/metrics-history.js +54 -7
- package/clients/metrics-history.ts +57 -8
- package/index.ts +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.0.40] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Passive capture on every file edit**: `captureSnapshot()` now called from `tool_call` hook with 5s debounce. Zero latency — reuses complexity metrics already computed for real-time feedback.
|
|
9
|
+
- **Skip duplicate snapshots**: Same commit + same MI = no write (reduces noise).
|
|
10
|
+
|
|
5
11
|
## [2.0.39] - 2026-03-27
|
|
6
12
|
|
|
7
13
|
### Added
|
|
8
14
|
- **Historical metrics tracking**: New `clients/metrics-history.ts` module captures complexity snapshots per commit. Tracks MI, cognitive complexity, and nesting depth across sessions.
|
|
9
15
|
- **Trend analysis in `/lens-metrics`**: New "Trend" column shows 📈/📉/➡️ with MI delta. "Trend Summary" section aggregates improving/stable/regressing counts with worst regressions.
|
|
10
|
-
- **Passive capture**: Snapshots captured on
|
|
16
|
+
- **Passive capture**: Snapshots captured on every file edit (tool_call hook) + `/lens-metrics` run. Max 20 snapshots per file (sliding window).
|
|
11
17
|
|
|
12
18
|
## [2.0.38] - 2026-03-27
|
|
13
19
|
|
|
@@ -64,11 +64,19 @@ export function saveHistory(history) {
|
|
|
64
64
|
const historyPath = path.join(historyDir, "metrics-history.json");
|
|
65
65
|
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
66
66
|
}
|
|
67
|
+
// In-memory cache to avoid loading/saving on every capture
|
|
68
|
+
let pendingHistory = null;
|
|
69
|
+
let saveTimer = null;
|
|
70
|
+
const SAVE_DEBOUNCE_MS = 5000; // Save at most every 5 seconds
|
|
67
71
|
/**
|
|
68
72
|
* Capture a snapshot for a file's current metrics
|
|
73
|
+
* Auto-saves to disk (debounced) for passive tracking
|
|
69
74
|
*/
|
|
70
|
-
export function captureSnapshot(filePath, metrics
|
|
71
|
-
|
|
75
|
+
export function captureSnapshot(filePath, metrics) {
|
|
76
|
+
// Use in-memory cache if available, otherwise load from disk
|
|
77
|
+
if (!pendingHistory) {
|
|
78
|
+
pendingHistory = loadHistory();
|
|
79
|
+
}
|
|
72
80
|
const relativePath = path.relative(process.cwd(), filePath);
|
|
73
81
|
const commit = getCurrentCommit();
|
|
74
82
|
const snapshot = {
|
|
@@ -79,8 +87,13 @@ export function captureSnapshot(filePath, metrics, history) {
|
|
|
79
87
|
nesting: metrics.maxNestingDepth,
|
|
80
88
|
lines: metrics.linesOfCode,
|
|
81
89
|
};
|
|
82
|
-
const existing =
|
|
90
|
+
const existing = pendingHistory.files[relativePath];
|
|
83
91
|
if (existing) {
|
|
92
|
+
// Skip if same commit + same MI (no change worth recording)
|
|
93
|
+
const latest = existing.latest;
|
|
94
|
+
if (latest.commit === commit && latest.mi === snapshot.mi) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
84
97
|
// Append to history (cap at MAX_HISTORY_PER_FILE)
|
|
85
98
|
existing.history.push(snapshot);
|
|
86
99
|
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
@@ -91,21 +104,55 @@ export function captureSnapshot(filePath, metrics, history) {
|
|
|
91
104
|
}
|
|
92
105
|
else {
|
|
93
106
|
// New file
|
|
94
|
-
|
|
107
|
+
pendingHistory.files[relativePath] = {
|
|
95
108
|
latest: snapshot,
|
|
96
109
|
history: [snapshot],
|
|
97
110
|
trend: "stable",
|
|
98
111
|
};
|
|
99
112
|
}
|
|
100
|
-
|
|
113
|
+
// Debounced save to disk
|
|
114
|
+
if (saveTimer)
|
|
115
|
+
clearTimeout(saveTimer);
|
|
116
|
+
saveTimer = setTimeout(() => {
|
|
117
|
+
if (pendingHistory) {
|
|
118
|
+
saveHistory(pendingHistory);
|
|
119
|
+
pendingHistory = null;
|
|
120
|
+
}
|
|
121
|
+
}, SAVE_DEBOUNCE_MS);
|
|
101
122
|
}
|
|
102
123
|
/**
|
|
103
|
-
* Capture snapshots for multiple files
|
|
124
|
+
* Capture snapshots for multiple files (explicit, immediate save)
|
|
125
|
+
* Used by /lens-metrics for batch capture
|
|
104
126
|
*/
|
|
105
127
|
export function captureSnapshots(files) {
|
|
106
128
|
let history = loadHistory();
|
|
107
129
|
for (const file of files) {
|
|
108
|
-
|
|
130
|
+
const relativePath = path.relative(process.cwd(), file.filePath);
|
|
131
|
+
const commit = getCurrentCommit();
|
|
132
|
+
const snapshot = {
|
|
133
|
+
commit,
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
mi: Math.round(file.metrics.maintainabilityIndex * 10) / 10,
|
|
136
|
+
cognitive: file.metrics.cognitiveComplexity,
|
|
137
|
+
nesting: file.metrics.maxNestingDepth,
|
|
138
|
+
lines: file.metrics.linesOfCode,
|
|
139
|
+
};
|
|
140
|
+
const existing = history.files[relativePath];
|
|
141
|
+
if (existing) {
|
|
142
|
+
existing.history.push(snapshot);
|
|
143
|
+
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
144
|
+
existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
|
|
145
|
+
}
|
|
146
|
+
existing.latest = snapshot;
|
|
147
|
+
existing.trend = computeTrend(existing.history);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
history.files[relativePath] = {
|
|
151
|
+
latest: snapshot,
|
|
152
|
+
history: [snapshot],
|
|
153
|
+
trend: "stable",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
109
156
|
}
|
|
110
157
|
saveHistory(history);
|
|
111
158
|
return history;
|
|
@@ -99,8 +99,14 @@ export function saveHistory(history: MetricsHistory): void {
|
|
|
99
99
|
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// In-memory cache to avoid loading/saving on every capture
|
|
103
|
+
let pendingHistory: MetricsHistory | null = null;
|
|
104
|
+
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
105
|
+
const SAVE_DEBOUNCE_MS = 5000; // Save at most every 5 seconds
|
|
106
|
+
|
|
102
107
|
/**
|
|
103
108
|
* Capture a snapshot for a file's current metrics
|
|
109
|
+
* Auto-saves to disk (debounced) for passive tracking
|
|
104
110
|
*/
|
|
105
111
|
export function captureSnapshot(
|
|
106
112
|
filePath: string,
|
|
@@ -110,9 +116,12 @@ export function captureSnapshot(
|
|
|
110
116
|
maxNestingDepth: number;
|
|
111
117
|
linesOfCode: number;
|
|
112
118
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
): void {
|
|
120
|
+
// Use in-memory cache if available, otherwise load from disk
|
|
121
|
+
if (!pendingHistory) {
|
|
122
|
+
pendingHistory = loadHistory();
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
const relativePath = path.relative(process.cwd(), filePath);
|
|
117
126
|
const commit = getCurrentCommit();
|
|
118
127
|
|
|
@@ -125,9 +134,14 @@ export function captureSnapshot(
|
|
|
125
134
|
lines: metrics.linesOfCode,
|
|
126
135
|
};
|
|
127
136
|
|
|
128
|
-
const existing =
|
|
137
|
+
const existing = pendingHistory.files[relativePath];
|
|
129
138
|
|
|
130
139
|
if (existing) {
|
|
140
|
+
// Skip if same commit + same MI (no change worth recording)
|
|
141
|
+
const latest = existing.latest;
|
|
142
|
+
if (latest.commit === commit && latest.mi === snapshot.mi) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
131
145
|
// Append to history (cap at MAX_HISTORY_PER_FILE)
|
|
132
146
|
existing.history.push(snapshot);
|
|
133
147
|
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
@@ -137,18 +151,26 @@ export function captureSnapshot(
|
|
|
137
151
|
existing.trend = computeTrend(existing.history);
|
|
138
152
|
} else {
|
|
139
153
|
// New file
|
|
140
|
-
|
|
154
|
+
pendingHistory.files[relativePath] = {
|
|
141
155
|
latest: snapshot,
|
|
142
156
|
history: [snapshot],
|
|
143
157
|
trend: "stable",
|
|
144
158
|
};
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
|
|
161
|
+
// Debounced save to disk
|
|
162
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
163
|
+
saveTimer = setTimeout(() => {
|
|
164
|
+
if (pendingHistory) {
|
|
165
|
+
saveHistory(pendingHistory);
|
|
166
|
+
pendingHistory = null;
|
|
167
|
+
}
|
|
168
|
+
}, SAVE_DEBOUNCE_MS);
|
|
148
169
|
}
|
|
149
170
|
|
|
150
171
|
/**
|
|
151
|
-
* Capture snapshots for multiple files
|
|
172
|
+
* Capture snapshots for multiple files (explicit, immediate save)
|
|
173
|
+
* Used by /lens-metrics for batch capture
|
|
152
174
|
*/
|
|
153
175
|
export function captureSnapshots(
|
|
154
176
|
files: Array<{
|
|
@@ -164,7 +186,34 @@ export function captureSnapshots(
|
|
|
164
186
|
let history = loadHistory();
|
|
165
187
|
|
|
166
188
|
for (const file of files) {
|
|
167
|
-
|
|
189
|
+
const relativePath = path.relative(process.cwd(), file.filePath);
|
|
190
|
+
const commit = getCurrentCommit();
|
|
191
|
+
|
|
192
|
+
const snapshot: MetricSnapshot = {
|
|
193
|
+
commit,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
mi: Math.round(file.metrics.maintainabilityIndex * 10) / 10,
|
|
196
|
+
cognitive: file.metrics.cognitiveComplexity,
|
|
197
|
+
nesting: file.metrics.maxNestingDepth,
|
|
198
|
+
lines: file.metrics.linesOfCode,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const existing = history.files[relativePath];
|
|
202
|
+
|
|
203
|
+
if (existing) {
|
|
204
|
+
existing.history.push(snapshot);
|
|
205
|
+
if (existing.history.length > MAX_HISTORY_PER_FILE) {
|
|
206
|
+
existing.history = existing.history.slice(-MAX_HISTORY_PER_FILE);
|
|
207
|
+
}
|
|
208
|
+
existing.latest = snapshot;
|
|
209
|
+
existing.trend = computeTrend(existing.history);
|
|
210
|
+
} else {
|
|
211
|
+
history.files[relativePath] = {
|
|
212
|
+
latest: snapshot,
|
|
213
|
+
history: [snapshot],
|
|
214
|
+
trend: "stable",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
168
217
|
}
|
|
169
218
|
|
|
170
219
|
saveHistory(history);
|
package/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { buildInterviewer } from "./clients/interviewer.js";
|
|
|
14
14
|
import { JscpdClient } from "./clients/jscpd-client.js";
|
|
15
15
|
import { KnipClient } from "./clients/knip-client.js";
|
|
16
16
|
import { MetricsClient } from "./clients/metrics-client.js";
|
|
17
|
-
import { captureSnapshots, getTrendSummary, formatTrendCell } from "./clients/metrics-history.js";
|
|
17
|
+
import { captureSnapshot, captureSnapshots, getTrendSummary, formatTrendCell } from "./clients/metrics-history.js";
|
|
18
18
|
import { RuffClient } from "./clients/ruff-client.js";
|
|
19
19
|
import { RustClient } from "./clients/rust-client.js";
|
|
20
20
|
import { getSourceFiles } from "./clients/scan-utils.js";
|
|
@@ -902,7 +902,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
902
902
|
);
|
|
903
903
|
if (!nodeFs.existsSync(filePath)) return;
|
|
904
904
|
|
|
905
|
-
// Record complexity baseline for TS/JS files
|
|
905
|
+
// Record complexity baseline for TS/JS files + capture history snapshot
|
|
906
906
|
if (
|
|
907
907
|
complexityClient.isSupportedFile(filePath) &&
|
|
908
908
|
!complexityBaselines.has(filePath)
|
|
@@ -910,6 +910,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
910
910
|
const baseline = complexityClient.analyzeFile(filePath);
|
|
911
911
|
if (baseline) {
|
|
912
912
|
complexityBaselines.set(filePath, baseline);
|
|
913
|
+
// Capture snapshot for historical tracking (async, non-blocking)
|
|
914
|
+
captureSnapshot(filePath, {
|
|
915
|
+
maintainabilityIndex: baseline.maintainabilityIndex,
|
|
916
|
+
cognitiveComplexity: baseline.cognitiveComplexity,
|
|
917
|
+
maxNestingDepth: baseline.maxNestingDepth,
|
|
918
|
+
linesOfCode: baseline.linesOfCode,
|
|
919
|
+
});
|
|
913
920
|
}
|
|
914
921
|
}
|
|
915
922
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.40",
|
|
4
4
|
"description": "Real-time code quality feedback for pi — TypeScript LSP, Biome, ast-grep, Ruff, complexity metrics, duplicate detection. Includes automated fix loop (/lens-booboo-fix) and interactive architectural refactoring (/lens-booboo-refactor) with browser-based interviews.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|