pi-lens 3.3.0 → 3.6.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/CHANGELOG.md +91 -0
- package/README.md +175 -13
- package/clients/cache/rule-cache.js +72 -0
- package/clients/cache/rule-cache.ts +104 -0
- package/clients/dispatch/integration.js +48 -1
- package/clients/dispatch/integration.ts +60 -2
- package/clients/dispatch/plan.js +5 -2
- package/clients/dispatch/plan.ts +5 -2
- package/clients/dispatch/runners/ast-grep-napi.js +175 -56
- package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
- package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
- package/clients/dispatch/runners/similarity.js +1 -1
- package/clients/dispatch/runners/similarity.ts +2 -2
- package/clients/dispatch/runners/tree-sitter.js +137 -10
- package/clients/dispatch/runners/tree-sitter.ts +168 -13
- package/clients/dispatch/runners/ts-lsp.js +3 -2
- package/clients/dispatch/runners/ts-lsp.ts +3 -2
- package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
- package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
- package/clients/dispatch/types.js +1 -1
- package/clients/dispatch/types.ts +1 -1
- package/clients/lsp/__tests__/service.test.js +3 -0
- package/clients/lsp/__tests__/service.test.ts +3 -0
- package/clients/lsp/client.js +42 -0
- package/clients/lsp/client.ts +79 -0
- package/clients/lsp/index.js +27 -0
- package/clients/lsp/index.ts +35 -0
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/metrics-client.js +3 -160
- package/clients/metrics-client.tdr.test.js +78 -0
- package/clients/metrics-client.test.js +30 -43
- package/clients/metrics-client.test.ts +30 -54
- package/clients/metrics-client.ts +5 -219
- package/clients/metrics-history.js +33 -7
- package/clients/metrics-history.ts +47 -10
- package/clients/pipeline.js +272 -0
- package/clients/pipeline.ts +371 -0
- package/clients/sg-runner.js +21 -3
- package/clients/sg-runner.ts +22 -3
- package/clients/tree-sitter-client.js +23 -2
- package/clients/tree-sitter-client.ts +27 -2
- package/index.ts +604 -771
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
- package/rules/ast-grep-rules/slop-patterns.yml +85 -62
- package/skills/ast-grep/SKILL.md +42 -1
- package/skills/lsp-navigation/SKILL.md +62 -0
- package/tsconfig.json +1 -1
- package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
- package/rules/ast-grep-rules/rules/no-default-export.yml +0 -19
package/clients/lsp/index.ts
CHANGED
|
@@ -242,6 +242,41 @@ export class LSPService {
|
|
|
242
242
|
return spawned.client.implementation(filePath, line, character);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Navigation: prepare call hierarchy at position
|
|
247
|
+
*/
|
|
248
|
+
async prepareCallHierarchy(
|
|
249
|
+
filePath: string,
|
|
250
|
+
line: number,
|
|
251
|
+
character: number,
|
|
252
|
+
) {
|
|
253
|
+
const spawned = await this.getClientForFile(filePath);
|
|
254
|
+
if (!spawned) return [];
|
|
255
|
+
return spawned.client.prepareCallHierarchy(filePath, line, character);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Navigation: find incoming calls (callers)
|
|
260
|
+
*/
|
|
261
|
+
async incomingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
262
|
+
const spawned = await this.getClientForFile(
|
|
263
|
+
item.uri.replace("file://", ""),
|
|
264
|
+
);
|
|
265
|
+
if (!spawned) return [];
|
|
266
|
+
return spawned.client.incomingCalls(item);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Navigation: find outgoing calls (callees)
|
|
271
|
+
*/
|
|
272
|
+
async outgoingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
273
|
+
const spawned = await this.getClientForFile(
|
|
274
|
+
item.uri.replace("file://", ""),
|
|
275
|
+
);
|
|
276
|
+
if (!spawned) return [];
|
|
277
|
+
return spawned.client.outgoingCalls(item);
|
|
278
|
+
}
|
|
279
|
+
|
|
245
280
|
/**
|
|
246
281
|
* Get all diagnostics across all tracked files (for cascade checking)
|
|
247
282
|
*/
|
package/clients/lsp/launch.js
CHANGED
|
@@ -149,8 +149,11 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
149
149
|
if (npmGlobalPath) {
|
|
150
150
|
spawnCommand = npmGlobalPath;
|
|
151
151
|
// Recompute needsShell for npm global path
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
needsShell =
|
|
153
|
+
isWindows &&
|
|
154
|
+
(spawnCommand.includes(" ") ||
|
|
155
|
+
/\.(cmd|bat)$/i.test(spawnCommand) ||
|
|
156
|
+
!/\.(exe|cmd|bat)$/i.test(spawnCommand));
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
let proc;
|
|
@@ -166,8 +169,10 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
166
169
|
if (npmGlobalPath && npmGlobalPath !== spawnCommand) {
|
|
167
170
|
console.error(`[lsp] Trying npm global: ${npmGlobalPath}`);
|
|
168
171
|
// Recompute needsShell for npm global path
|
|
169
|
-
const
|
|
170
|
-
|
|
172
|
+
const needsShellGlobal = isWindows &&
|
|
173
|
+
(npmGlobalPath.includes(" ") ||
|
|
174
|
+
/\.(cmd|bat)$/i.test(npmGlobalPath) ||
|
|
175
|
+
!/\.(exe|cmd|bat)$/i.test(npmGlobalPath));
|
|
171
176
|
proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
172
177
|
}
|
|
173
178
|
else {
|
|
@@ -192,7 +197,7 @@ export async function launchLSP(command, args = [], options = {}) {
|
|
|
192
197
|
let settled = false;
|
|
193
198
|
// Attach error handler that can reject for immediate errors
|
|
194
199
|
proc.on("error", (err) => {
|
|
195
|
-
if (!settled && err.code === "ENOENT") {
|
|
200
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
196
201
|
settled = true;
|
|
197
202
|
reject(new Error(`LSP server binary not found: ${command}. ` +
|
|
198
203
|
`Install it or check your PATH.`));
|
|
@@ -254,7 +259,7 @@ export async function launchViaPackageManager(packageName, args = [], options =
|
|
|
254
259
|
await new Promise((resolve, reject) => {
|
|
255
260
|
let settled = false;
|
|
256
261
|
proc.on("error", (err) => {
|
|
257
|
-
if (!settled && err.code === "ENOENT") {
|
|
262
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
258
263
|
settled = true;
|
|
259
264
|
reject(new Error(`Package manager not found for: ${packageName}. ` +
|
|
260
265
|
`Install Node.js or check your PATH.`));
|
package/clients/lsp/launch.ts
CHANGED
|
@@ -205,8 +205,11 @@ export async function launchLSP(
|
|
|
205
205
|
if (npmGlobalPath) {
|
|
206
206
|
spawnCommand = npmGlobalPath;
|
|
207
207
|
// Recompute needsShell for npm global path
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
needsShell =
|
|
209
|
+
isWindows &&
|
|
210
|
+
(spawnCommand.includes(" ") ||
|
|
211
|
+
/\.(cmd|bat)$/i.test(spawnCommand) ||
|
|
212
|
+
!/\.(exe|cmd|bat)$/i.test(spawnCommand));
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -225,9 +228,11 @@ export async function launchLSP(
|
|
|
225
228
|
if (npmGlobalPath && npmGlobalPath !== spawnCommand) {
|
|
226
229
|
console.error(`[lsp] Trying npm global: ${npmGlobalPath}`);
|
|
227
230
|
// Recompute needsShell for npm global path
|
|
228
|
-
const globalHasExt = /\.(exe|cmd|bat)$/i.test(npmGlobalPath);
|
|
229
231
|
const needsShellGlobal =
|
|
230
|
-
isWindows &&
|
|
232
|
+
isWindows &&
|
|
233
|
+
(npmGlobalPath.includes(" ") ||
|
|
234
|
+
/\.(cmd|bat)$/i.test(npmGlobalPath) ||
|
|
235
|
+
!/\.(exe|cmd|bat)$/i.test(npmGlobalPath));
|
|
231
236
|
proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
232
237
|
} else {
|
|
233
238
|
throw err;
|
|
@@ -256,7 +261,7 @@ export async function launchLSP(
|
|
|
256
261
|
|
|
257
262
|
// Attach error handler that can reject for immediate errors
|
|
258
263
|
proc.on("error", (err: Error & { code?: string }) => {
|
|
259
|
-
if (!settled && err.code === "ENOENT") {
|
|
264
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
260
265
|
settled = true;
|
|
261
266
|
reject(
|
|
262
267
|
new Error(
|
|
@@ -346,7 +351,7 @@ export async function launchViaPackageManager(
|
|
|
346
351
|
let settled = false;
|
|
347
352
|
|
|
348
353
|
proc.on("error", (err: Error & { code?: string }) => {
|
|
349
|
-
if (!settled && err.code === "ENOENT") {
|
|
354
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
350
355
|
settled = true;
|
|
351
356
|
reject(
|
|
352
357
|
new Error(
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* Metrics are aggregated and shown in session summary only.
|
|
6
6
|
*
|
|
7
7
|
* Tracks:
|
|
8
|
-
* - TDR (Technical Debt Ratio): composite score from existing signals
|
|
9
|
-
* - AI Code Ratio: % of file written by agent this session vs pre-existing
|
|
10
8
|
* - Code Entropy: Shannon entropy delta per file
|
|
11
9
|
*
|
|
12
10
|
* These are observational metrics — they inform the human in session summary,
|
|
@@ -18,8 +16,6 @@ import * as path from "node:path";
|
|
|
18
16
|
export class MetricsClient {
|
|
19
17
|
constructor(verbose = false) {
|
|
20
18
|
this.fileBaselines = new Map();
|
|
21
|
-
this.fileSessionWrites = new Map(); // agent-written lines
|
|
22
|
-
this.tdrFindings = new Map();
|
|
23
19
|
this.log = verbose
|
|
24
20
|
? (msg) => console.error(`[metrics] ${msg}`)
|
|
25
21
|
: () => { };
|
|
@@ -27,7 +23,7 @@ export class MetricsClient {
|
|
|
27
23
|
/**
|
|
28
24
|
* Record initial state of a file when first touched this session
|
|
29
25
|
*/
|
|
30
|
-
recordBaseline(filePath
|
|
26
|
+
recordBaseline(filePath) {
|
|
31
27
|
const absolutePath = path.resolve(filePath);
|
|
32
28
|
if (!fs.existsSync(absolutePath))
|
|
33
29
|
return;
|
|
@@ -35,48 +31,8 @@ export class MetricsClient {
|
|
|
35
31
|
return; // Already recorded
|
|
36
32
|
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
37
33
|
const entropy = this.calculateEntropy(content);
|
|
38
|
-
this.fileBaselines.set(absolutePath, { content, entropy
|
|
39
|
-
this.
|
|
40
|
-
this.log(`Baseline recorded: ${path.basename(filePath)} (entropy: ${entropy.toFixed(2)}, tdr: ${initialTdr})`);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Update TDR findings for a file
|
|
44
|
-
*/
|
|
45
|
-
updateTDR(filePath, entries) {
|
|
46
|
-
const absolutePath = path.resolve(filePath);
|
|
47
|
-
this.tdrFindings.set(absolutePath, entries);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Get overall TDR score for the session
|
|
51
|
-
* 0-100, where 100 is high debt.
|
|
52
|
-
*/
|
|
53
|
-
getTDRScore() {
|
|
54
|
-
let totalScore = 0;
|
|
55
|
-
for (const entries of this.tdrFindings.values()) {
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
// Each entry adds to the debt index based on its Grade (count as the Grade value)
|
|
58
|
-
totalScore += entry.count;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Normalize to 0-100? Or just return the raw Index.
|
|
62
|
-
// SCA.md says "Technical Debt Index"
|
|
63
|
-
return totalScore;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Record that the agent wrote/replaced content in a file
|
|
67
|
-
* @param newContent The new content after the write
|
|
68
|
-
*/
|
|
69
|
-
recordWrite(filePath, newContent) {
|
|
70
|
-
const absolutePath = path.resolve(filePath);
|
|
71
|
-
this.recordBaseline(absolutePath);
|
|
72
|
-
const baseline = this.fileBaselines.get(absolutePath);
|
|
73
|
-
const _baselineLines = baseline.content.split("\n").length;
|
|
74
|
-
const _newLines = newContent.split("\n").length;
|
|
75
|
-
// Estimate agent-written lines: count the diff
|
|
76
|
-
const diffLines = this.estimateDiffLines(baseline.content, newContent);
|
|
77
|
-
const currentAgentLines = this.fileSessionWrites.get(absolutePath) || 0;
|
|
78
|
-
this.fileSessionWrites.set(absolutePath, currentAgentLines + diffLines);
|
|
79
|
-
this.log(`Write recorded: ${path.basename(filePath)} (+~${diffLines} agent lines)`);
|
|
34
|
+
this.fileBaselines.set(absolutePath, { content, entropy });
|
|
35
|
+
this.log(`Baseline recorded: ${path.basename(filePath)} (entropy: ${entropy.toFixed(2)})`);
|
|
80
36
|
}
|
|
81
37
|
/**
|
|
82
38
|
* Get metrics for a specific file
|
|
@@ -90,53 +46,14 @@ export class MetricsClient {
|
|
|
90
46
|
return null;
|
|
91
47
|
const currentContent = fs.readFileSync(absolutePath, "utf-8");
|
|
92
48
|
const totalLines = currentContent.split("\n").length;
|
|
93
|
-
const agentLines = this.fileSessionWrites.get(absolutePath) || 0;
|
|
94
49
|
const entropyCurrent = this.calculateEntropy(currentContent);
|
|
95
50
|
const entropyDelta = entropyCurrent - baseline.entropy;
|
|
96
|
-
const currentTdrFindings = this.tdrFindings.get(absolutePath) || [];
|
|
97
|
-
const tdrCurrent = currentTdrFindings.reduce((a, b) => a + b.count, 0);
|
|
98
51
|
return {
|
|
99
52
|
filePath: path.relative(process.cwd(), absolutePath),
|
|
100
53
|
totalLines,
|
|
101
|
-
agentLines: Math.min(agentLines, totalLines),
|
|
102
|
-
preExistingLines: Math.max(0, totalLines - agentLines),
|
|
103
54
|
entropyStart: baseline.entropy,
|
|
104
55
|
entropyCurrent,
|
|
105
56
|
entropyDelta,
|
|
106
|
-
tdrStart: baseline.tdr,
|
|
107
|
-
tdrCurrent,
|
|
108
|
-
tdrContributors: currentTdrFindings,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Calculate AI Code Ratio for the session
|
|
113
|
-
* Returns 0-1 where 1 = all code written by agent
|
|
114
|
-
*/
|
|
115
|
-
getAICodeRatio() {
|
|
116
|
-
let totalAgentLines = 0;
|
|
117
|
-
let totalPreExistingLines = 0;
|
|
118
|
-
let fileCount = 0;
|
|
119
|
-
for (const [filePath, agentLines] of this.fileSessionWrites) {
|
|
120
|
-
if (!fs.existsSync(filePath))
|
|
121
|
-
continue;
|
|
122
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
123
|
-
const totalLines = content.split("\n").length;
|
|
124
|
-
const baseline = this.fileBaselines.get(filePath);
|
|
125
|
-
const _baselineLines = baseline
|
|
126
|
-
? baseline.content.split("\n").length
|
|
127
|
-
: totalLines;
|
|
128
|
-
// Pre-existing = lines that existed before this session and weren't replaced
|
|
129
|
-
const preExisting = Math.max(0, totalLines - agentLines);
|
|
130
|
-
totalAgentLines += agentLines;
|
|
131
|
-
totalPreExistingLines += preExisting;
|
|
132
|
-
fileCount++;
|
|
133
|
-
}
|
|
134
|
-
const total = totalAgentLines + totalPreExistingLines;
|
|
135
|
-
return {
|
|
136
|
-
ratio: total > 0 ? totalAgentLines / total : 0, // fixed
|
|
137
|
-
agentLines: totalAgentLines,
|
|
138
|
-
preExistingLines: totalPreExistingLines,
|
|
139
|
-
fileCount,
|
|
140
57
|
};
|
|
141
58
|
}
|
|
142
59
|
/**
|
|
@@ -147,8 +64,6 @@ export class MetricsClient {
|
|
|
147
64
|
for (const [filePath, baseline] of this.fileBaselines) {
|
|
148
65
|
if (!fs.existsSync(filePath))
|
|
149
66
|
continue;
|
|
150
|
-
if (!this.fileSessionWrites.has(filePath))
|
|
151
|
-
continue;
|
|
152
67
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
153
68
|
const current = this.calculateEntropy(content);
|
|
154
69
|
const delta = current - baseline.entropy;
|
|
@@ -182,83 +97,11 @@ export class MetricsClient {
|
|
|
182
97
|
}
|
|
183
98
|
return entropy;
|
|
184
99
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Format metrics for session summary
|
|
187
|
-
*/
|
|
188
|
-
formatSessionSummary() {
|
|
189
|
-
const aiRatio = this.getAICodeRatio();
|
|
190
|
-
const entropyDeltas = this.getEntropyDeltas();
|
|
191
|
-
const fileCount = this.fileSessionWrites.size;
|
|
192
|
-
if (fileCount === 0)
|
|
193
|
-
return ""; // No files touched
|
|
194
|
-
const parts = [];
|
|
195
|
-
// Aggregate TDR from details
|
|
196
|
-
let totalTdrCurrent = 0;
|
|
197
|
-
let totalTdrStart = 0;
|
|
198
|
-
for (const path of this.fileSessionWrites.keys()) {
|
|
199
|
-
const m = this.getFileMetrics(path);
|
|
200
|
-
if (m) {
|
|
201
|
-
totalTdrCurrent += m.tdrCurrent;
|
|
202
|
-
totalTdrStart += m.tdrStart;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Technical Debt Index
|
|
206
|
-
if (totalTdrCurrent > 0 || totalTdrStart > 0) {
|
|
207
|
-
const delta = totalTdrCurrent - totalTdrStart;
|
|
208
|
-
const deltaStr = delta !== 0
|
|
209
|
-
? ` (${delta > 0 ? "+" : ""}${delta.toFixed(1)} this session)`
|
|
210
|
-
: "";
|
|
211
|
-
parts.push(`[TDR Index] Total Debt: ${totalTdrCurrent.toFixed(1)}${deltaStr}`);
|
|
212
|
-
}
|
|
213
|
-
// AI Code Ratio
|
|
214
|
-
const pct = (aiRatio.ratio * 100).toFixed(1);
|
|
215
|
-
parts.push(`[AI Code] ${pct}% of ${fileCount} file(s) written by agent this session (${aiRatio.agentLines} lines, ${aiRatio.preExistingLines} pre-existing)`);
|
|
216
|
-
// Entropy deltas (only show files with significant changes)
|
|
217
|
-
const significant = entropyDeltas.filter((e) => Math.abs(e.delta) > 0.1);
|
|
218
|
-
if (significant.length > 0) {
|
|
219
|
-
const topChanges = significant.slice(0, 5);
|
|
220
|
-
parts.push(`[Entropy] ${significant.length} file(s) with complexity changes:`);
|
|
221
|
-
for (const e of topChanges) {
|
|
222
|
-
const arrow = e.delta > 0 ? "↑" : "↓";
|
|
223
|
-
const sign = e.delta > 0 ? "+" : "";
|
|
224
|
-
parts.push(` ${arrow} ${e.file}: ${e.start.toFixed(2)} → ${e.current.toFixed(2)} (${sign}${e.delta.toFixed(2)} bits)`);
|
|
225
|
-
}
|
|
226
|
-
if (significant.length > 5) {
|
|
227
|
-
parts.push(` ... and ${significant.length - 5} more`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return parts.join("\n");
|
|
231
|
-
}
|
|
232
100
|
/**
|
|
233
101
|
* Reset session state (for new session)
|
|
234
102
|
*/
|
|
235
103
|
reset() {
|
|
236
104
|
this.fileBaselines.clear();
|
|
237
|
-
this.fileSessionWrites.clear();
|
|
238
105
|
this.log("Session metrics reset");
|
|
239
106
|
}
|
|
240
|
-
// --- Internal ---
|
|
241
|
-
/**
|
|
242
|
-
* Estimate number of lines that changed between two texts
|
|
243
|
-
* Simple line-based diff (not Myers, but good enough for metrics)
|
|
244
|
-
*/
|
|
245
|
-
estimateDiffLines(oldText, newText) {
|
|
246
|
-
const oldLines = new Set(oldText.split("\n"));
|
|
247
|
-
const newLines = newText.split("\n");
|
|
248
|
-
let changed = 0;
|
|
249
|
-
for (const line of newLines) {
|
|
250
|
-
if (!oldLines.has(line)) {
|
|
251
|
-
changed++;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// Also count deleted lines
|
|
255
|
-
const newLinesSet = new Set(newLines);
|
|
256
|
-
for (const line of oldLines) {
|
|
257
|
-
if (!newLinesSet.has(line)) {
|
|
258
|
-
changed++;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Return roughly half (additions + deletions / 2)
|
|
262
|
-
return Math.max(1, Math.ceil(changed / 2));
|
|
263
|
-
}
|
|
264
107
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { convertDiagnosticsToTDREntries, } from "./metrics-client.js";
|
|
3
|
+
describe("TDR conversion", () => {
|
|
4
|
+
test("converts type errors to TDR entries", () => {
|
|
5
|
+
const diagnostics = [
|
|
6
|
+
{
|
|
7
|
+
id: "ts-lsp:TS2345:10",
|
|
8
|
+
message: "Argument of type 'string' is not assignable",
|
|
9
|
+
filePath: "/test/file.ts",
|
|
10
|
+
line: 10,
|
|
11
|
+
column: 5,
|
|
12
|
+
severity: "error",
|
|
13
|
+
semantic: "blocking",
|
|
14
|
+
tool: "ts-lsp",
|
|
15
|
+
rule: "TS2345",
|
|
16
|
+
tdrCategory: "type_errors",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
const entries = convertDiagnosticsToTDREntries(diagnostics);
|
|
20
|
+
expect(entries).toHaveLength(1);
|
|
21
|
+
expect(entries[0]).toEqual({
|
|
22
|
+
category: "type_errors",
|
|
23
|
+
count: 1,
|
|
24
|
+
severity: "error",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
test("groups multiple diagnostics by category", () => {
|
|
28
|
+
const diagnostics = [
|
|
29
|
+
{
|
|
30
|
+
id: "1",
|
|
31
|
+
message: "Type error 1",
|
|
32
|
+
filePath: "/test.ts",
|
|
33
|
+
severity: "error",
|
|
34
|
+
semantic: "blocking",
|
|
35
|
+
tool: "ts-lsp",
|
|
36
|
+
tdrCategory: "type_errors",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "2",
|
|
40
|
+
message: "Type error 2",
|
|
41
|
+
filePath: "/test.ts",
|
|
42
|
+
severity: "error",
|
|
43
|
+
semantic: "blocking",
|
|
44
|
+
tool: "ts-lsp",
|
|
45
|
+
tdrCategory: "type_errors",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "3",
|
|
49
|
+
message: "Security issue",
|
|
50
|
+
filePath: "/test.ts",
|
|
51
|
+
severity: "error",
|
|
52
|
+
semantic: "blocking",
|
|
53
|
+
tool: "ast-grep-napi",
|
|
54
|
+
tdrCategory: "security",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const entries = convertDiagnosticsToTDREntries(diagnostics);
|
|
58
|
+
expect(entries).toHaveLength(2);
|
|
59
|
+
expect(entries.find((e) => e.category === "type_errors")?.count).toBe(2);
|
|
60
|
+
expect(entries.find((e) => e.category === "security")?.count).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
test("auto-categorizes diagnostics without tdrCategory", () => {
|
|
63
|
+
const diagnostics = [
|
|
64
|
+
{
|
|
65
|
+
id: "1",
|
|
66
|
+
message: "Unused variable",
|
|
67
|
+
filePath: "/test.ts",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
semantic: "warning",
|
|
70
|
+
tool: "biome",
|
|
71
|
+
rule: "no-unused",
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
const entries = convertDiagnosticsToTDREntries(diagnostics);
|
|
75
|
+
expect(entries).toHaveLength(1);
|
|
76
|
+
expect(entries[0].category).toBe("dead_code");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -62,34 +62,37 @@ describe("MetricsClient", () => {
|
|
|
62
62
|
expect(metrics?.entropyStart).toBe(client.calculateEntropy(content1));
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
-
describe("
|
|
66
|
-
it("should
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
client.
|
|
70
|
-
|
|
71
|
-
fs.writeFileSync(filePath, modified);
|
|
72
|
-
client.recordWrite(filePath, modified);
|
|
73
|
-
const aiRatio = client.getAICodeRatio();
|
|
74
|
-
expect(aiRatio.agentLines).toBeGreaterThan(0);
|
|
65
|
+
describe("getFileMetrics", () => {
|
|
66
|
+
it("should return null for file without baseline", () => {
|
|
67
|
+
const filePath = createTempFile(tmpDir, "test.ts", "content");
|
|
68
|
+
// Don't record baseline
|
|
69
|
+
const metrics = client.getFileMetrics(filePath);
|
|
70
|
+
expect(metrics).toBeNull();
|
|
75
71
|
});
|
|
76
|
-
it("should
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
client.recordBaseline(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
it("should track entropy changes", () => {
|
|
73
|
+
const simple = "const x = 1;\n";
|
|
74
|
+
const filePath = createTempFile(tmpDir, "test.ts", simple);
|
|
75
|
+
client.recordBaseline(filePath);
|
|
76
|
+
// Make file more complex
|
|
77
|
+
const complex = `
|
|
78
|
+
function complex(a: number, b: number, c: number): number {
|
|
79
|
+
if (a > 0) {
|
|
80
|
+
if (b > 0) {
|
|
81
|
+
if (c > 0) {
|
|
82
|
+
return a + b + c;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
fs.writeFileSync(filePath, complex);
|
|
90
|
+
const metrics = client.getFileMetrics(filePath);
|
|
91
|
+
expect(metrics?.entropyDelta).not.toBe(0);
|
|
89
92
|
});
|
|
90
93
|
});
|
|
91
94
|
describe("getEntropyDeltas", () => {
|
|
92
|
-
it("should track entropy changes", () => {
|
|
95
|
+
it("should track entropy changes for baselined files", () => {
|
|
93
96
|
const simple = "const x = 1;\n";
|
|
94
97
|
const filePath = createTempFile(tmpDir, "test.ts", simple);
|
|
95
98
|
client.recordBaseline(filePath);
|
|
@@ -107,35 +110,19 @@ function complex(a: number, b: number, c: number): number {
|
|
|
107
110
|
}
|
|
108
111
|
`;
|
|
109
112
|
fs.writeFileSync(filePath, complex);
|
|
110
|
-
client.recordWrite(filePath, complex);
|
|
111
113
|
const deltas = client.getEntropyDeltas();
|
|
112
114
|
expect(deltas.length).toBe(1);
|
|
113
115
|
expect(deltas[0].delta).not.toBe(0);
|
|
114
116
|
});
|
|
115
117
|
});
|
|
116
|
-
describe("formatSessionSummary", () => {
|
|
117
|
-
it("should return empty string when no files touched", () => {
|
|
118
|
-
expect(client.formatSessionSummary()).toBe("");
|
|
119
|
-
});
|
|
120
|
-
it("should format AI code ratio when files are modified", () => {
|
|
121
|
-
const filePath = createTempFile(tmpDir, "test.ts", "original\n");
|
|
122
|
-
client.recordBaseline(filePath);
|
|
123
|
-
const modified = "original\nnew line 1\nnew line 2\n";
|
|
124
|
-
fs.writeFileSync(filePath, modified);
|
|
125
|
-
client.recordWrite(filePath, modified);
|
|
126
|
-
const summary = client.formatSessionSummary();
|
|
127
|
-
expect(summary).toContain("AI Code");
|
|
128
|
-
expect(summary).toContain("file(s)");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
118
|
describe("reset", () => {
|
|
132
119
|
it("should clear all tracked data", () => {
|
|
133
120
|
const filePath = createTempFile(tmpDir, "test.ts", "content\n");
|
|
134
121
|
client.recordBaseline(filePath);
|
|
135
122
|
client.reset();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
expect(
|
|
123
|
+
// After reset, metrics should be null (baseline cleared)
|
|
124
|
+
const metrics = client.getFileMetrics(filePath);
|
|
125
|
+
expect(metrics).toBeNull();
|
|
139
126
|
});
|
|
140
127
|
});
|
|
141
128
|
});
|
|
@@ -85,47 +85,43 @@ describe("MetricsClient", () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
describe("
|
|
89
|
-
it("should
|
|
90
|
-
const
|
|
91
|
-
const filePath = createTempFile(tmpDir, "test.ts", original);
|
|
88
|
+
describe("getFileMetrics", () => {
|
|
89
|
+
it("should return null for file without baseline", () => {
|
|
90
|
+
const filePath = createTempFile(tmpDir, "test.ts", "content");
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
fs.writeFileSync(filePath, modified);
|
|
97
|
-
client.recordWrite(filePath, modified);
|
|
98
|
-
|
|
99
|
-
const aiRatio = client.getAICodeRatio();
|
|
100
|
-
expect(aiRatio.agentLines).toBeGreaterThan(0);
|
|
92
|
+
// Don't record baseline
|
|
93
|
+
const metrics = client.getFileMetrics(filePath);
|
|
94
|
+
expect(metrics).toBeNull();
|
|
101
95
|
});
|
|
102
96
|
|
|
103
|
-
it("should
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
"file1.ts",
|
|
107
|
-
"original content line 1\noriginal content line 2\n",
|
|
108
|
-
);
|
|
109
|
-
const file2 = createTempFile(tmpDir, "file2.ts", "original\n");
|
|
97
|
+
it("should track entropy changes", () => {
|
|
98
|
+
const simple = "const x = 1;\n";
|
|
99
|
+
const filePath = createTempFile(tmpDir, "test.ts", simple);
|
|
110
100
|
|
|
111
|
-
client.recordBaseline(
|
|
112
|
-
client.recordBaseline(file2);
|
|
101
|
+
client.recordBaseline(filePath);
|
|
113
102
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
103
|
+
// Make file more complex
|
|
104
|
+
const complex = `
|
|
105
|
+
function complex(a: number, b: number, c: number): number {
|
|
106
|
+
if (a > 0) {
|
|
107
|
+
if (b > 0) {
|
|
108
|
+
if (c > 0) {
|
|
109
|
+
return a + b + c;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
fs.writeFileSync(filePath, complex);
|
|
119
117
|
|
|
120
|
-
const
|
|
121
|
-
expect(
|
|
122
|
-
expect(aiRatio.ratio).toBeGreaterThanOrEqual(0);
|
|
123
|
-
expect(aiRatio.ratio).toBeLessThanOrEqual(1);
|
|
118
|
+
const metrics = client.getFileMetrics(filePath);
|
|
119
|
+
expect(metrics?.entropyDelta).not.toBe(0);
|
|
124
120
|
});
|
|
125
121
|
});
|
|
126
122
|
|
|
127
123
|
describe("getEntropyDeltas", () => {
|
|
128
|
-
it("should track entropy changes", () => {
|
|
124
|
+
it("should track entropy changes for baselined files", () => {
|
|
129
125
|
const simple = "const x = 1;\n";
|
|
130
126
|
const filePath = createTempFile(tmpDir, "test.ts", simple);
|
|
131
127
|
|
|
@@ -145,7 +141,6 @@ function complex(a: number, b: number, c: number): number {
|
|
|
145
141
|
}
|
|
146
142
|
`;
|
|
147
143
|
fs.writeFileSync(filePath, complex);
|
|
148
|
-
client.recordWrite(filePath, complex);
|
|
149
144
|
|
|
150
145
|
const deltas = client.getEntropyDeltas();
|
|
151
146
|
expect(deltas.length).toBe(1);
|
|
@@ -153,25 +148,6 @@ function complex(a: number, b: number, c: number): number {
|
|
|
153
148
|
});
|
|
154
149
|
});
|
|
155
150
|
|
|
156
|
-
describe("formatSessionSummary", () => {
|
|
157
|
-
it("should return empty string when no files touched", () => {
|
|
158
|
-
expect(client.formatSessionSummary()).toBe("");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("should format AI code ratio when files are modified", () => {
|
|
162
|
-
const filePath = createTempFile(tmpDir, "test.ts", "original\n");
|
|
163
|
-
client.recordBaseline(filePath);
|
|
164
|
-
|
|
165
|
-
const modified = "original\nnew line 1\nnew line 2\n";
|
|
166
|
-
fs.writeFileSync(filePath, modified);
|
|
167
|
-
client.recordWrite(filePath, modified);
|
|
168
|
-
|
|
169
|
-
const summary = client.formatSessionSummary();
|
|
170
|
-
expect(summary).toContain("AI Code");
|
|
171
|
-
expect(summary).toContain("file(s)");
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
151
|
describe("reset", () => {
|
|
176
152
|
it("should clear all tracked data", () => {
|
|
177
153
|
const filePath = createTempFile(tmpDir, "test.ts", "content\n");
|
|
@@ -179,9 +155,9 @@ function complex(a: number, b: number, c: number): number {
|
|
|
179
155
|
|
|
180
156
|
client.reset();
|
|
181
157
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(
|
|
158
|
+
// After reset, metrics should be null (baseline cleared)
|
|
159
|
+
const metrics = client.getFileMetrics(filePath);
|
|
160
|
+
expect(metrics).toBeNull();
|
|
185
161
|
});
|
|
186
162
|
});
|
|
187
163
|
});
|