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.
Files changed (53) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +175 -13
  3. package/clients/cache/rule-cache.js +72 -0
  4. package/clients/cache/rule-cache.ts +104 -0
  5. package/clients/dispatch/integration.js +48 -1
  6. package/clients/dispatch/integration.ts +60 -2
  7. package/clients/dispatch/plan.js +5 -2
  8. package/clients/dispatch/plan.ts +5 -2
  9. package/clients/dispatch/runners/ast-grep-napi.js +175 -56
  10. package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
  11. package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
  12. package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
  13. package/clients/dispatch/runners/similarity.js +1 -1
  14. package/clients/dispatch/runners/similarity.ts +2 -2
  15. package/clients/dispatch/runners/tree-sitter.js +137 -10
  16. package/clients/dispatch/runners/tree-sitter.ts +168 -13
  17. package/clients/dispatch/runners/ts-lsp.js +3 -2
  18. package/clients/dispatch/runners/ts-lsp.ts +3 -2
  19. package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
  20. package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
  21. package/clients/dispatch/types.js +1 -1
  22. package/clients/dispatch/types.ts +1 -1
  23. package/clients/lsp/__tests__/service.test.js +3 -0
  24. package/clients/lsp/__tests__/service.test.ts +3 -0
  25. package/clients/lsp/client.js +42 -0
  26. package/clients/lsp/client.ts +79 -0
  27. package/clients/lsp/index.js +27 -0
  28. package/clients/lsp/index.ts +35 -0
  29. package/clients/lsp/launch.js +11 -6
  30. package/clients/lsp/launch.ts +11 -6
  31. package/clients/metrics-client.js +3 -160
  32. package/clients/metrics-client.tdr.test.js +78 -0
  33. package/clients/metrics-client.test.js +30 -43
  34. package/clients/metrics-client.test.ts +30 -54
  35. package/clients/metrics-client.ts +5 -219
  36. package/clients/metrics-history.js +33 -7
  37. package/clients/metrics-history.ts +47 -10
  38. package/clients/pipeline.js +272 -0
  39. package/clients/pipeline.ts +371 -0
  40. package/clients/sg-runner.js +21 -3
  41. package/clients/sg-runner.ts +22 -3
  42. package/clients/tree-sitter-client.js +23 -2
  43. package/clients/tree-sitter-client.ts +27 -2
  44. package/index.ts +604 -771
  45. package/package.json +1 -1
  46. package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
  47. package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
  48. package/rules/ast-grep-rules/slop-patterns.yml +85 -62
  49. package/skills/ast-grep/SKILL.md +42 -1
  50. package/skills/lsp-navigation/SKILL.md +62 -0
  51. package/tsconfig.json +1 -1
  52. package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
  53. package/rules/ast-grep-rules/rules/no-default-export.yml +0 -19
@@ -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
  */
@@ -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
- const globalHasExt = /\.(exe|cmd|bat)$/i.test(spawnCommand);
153
- needsShell = isWindows && (spawnCommand.includes(" ") || !globalHasExt);
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 globalHasExt = /\.(exe|cmd|bat)$/i.test(npmGlobalPath);
170
- const needsShellGlobal = isWindows && (npmGlobalPath.includes(" ") || !globalHasExt);
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.`));
@@ -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
- const globalHasExt = /\.(exe|cmd|bat)$/i.test(spawnCommand);
209
- needsShell = isWindows && (spawnCommand.includes(" ") || !globalHasExt);
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 && (npmGlobalPath.includes(" ") || !globalHasExt);
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, initialTdr = 0) {
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, tdr: initialTdr });
39
- this.fileSessionWrites.set(absolutePath, 0);
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("recordWrite", () => {
66
- it("should track agent-written lines", () => {
67
- const original = "const x = 1;\n";
68
- const filePath = createTempFile(tmpDir, "test.ts", original);
69
- client.recordBaseline(filePath);
70
- const modified = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
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 calculate AI code ratio", () => {
77
- const file1 = createTempFile(tmpDir, "file1.ts", "original content line 1\noriginal content line 2\n");
78
- const file2 = createTempFile(tmpDir, "file2.ts", "original\n");
79
- client.recordBaseline(file1);
80
- client.recordBaseline(file2);
81
- // Simulate agent writing new content
82
- const newContent1 = "original content line 1\noriginal content line 2\nagent line 3\nagent line 4\n";
83
- fs.writeFileSync(file1, newContent1);
84
- client.recordWrite(file1, newContent1);
85
- const aiRatio = client.getAICodeRatio();
86
- expect(aiRatio.fileCount).toBe(2);
87
- expect(aiRatio.ratio).toBeGreaterThanOrEqual(0);
88
- expect(aiRatio.ratio).toBeLessThanOrEqual(1);
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
- const aiRatio = client.getAICodeRatio();
137
- expect(aiRatio.fileCount).toBe(0);
138
- expect(client.formatSessionSummary()).toBe("");
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("recordWrite", () => {
89
- it("should track agent-written lines", () => {
90
- const original = "const x = 1;\n";
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
- client.recordBaseline(filePath);
94
-
95
- const modified = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
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 calculate AI code ratio", () => {
104
- const file1 = createTempFile(
105
- tmpDir,
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(file1);
112
- client.recordBaseline(file2);
101
+ client.recordBaseline(filePath);
113
102
 
114
- // Simulate agent writing new content
115
- const newContent1 =
116
- "original content line 1\noriginal content line 2\nagent line 3\nagent line 4\n";
117
- fs.writeFileSync(file1, newContent1);
118
- client.recordWrite(file1, newContent1);
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 aiRatio = client.getAICodeRatio();
121
- expect(aiRatio.fileCount).toBe(2);
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
- const aiRatio = client.getAICodeRatio();
183
- expect(aiRatio.fileCount).toBe(0);
184
- expect(client.formatSessionSummary()).toBe("");
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
  });