pi-lens 2.2.6 → 2.2.8

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.
@@ -261,3 +261,81 @@ export function formatTrendCell(filePath, history) {
261
261
  const miColor = delta.mi > 0 ? "🟢" : delta.mi < 0 ? "šŸ”“" : "⚪";
262
262
  return `${emoji} ${miColor}${miSign}${delta.mi}`;
263
263
  }
264
+ /**
265
+ * Calculate Technical Debt Index for the project.
266
+ * Score: 0 = perfect, 100 = maximum debt.
267
+ */
268
+ export function computeTDI(history) {
269
+ const files = Object.values(history.files);
270
+ if (files.length === 0) {
271
+ return {
272
+ score: 0,
273
+ grade: "N/A",
274
+ avgMI: 100,
275
+ totalCognitive: 0,
276
+ filesAnalyzed: 0,
277
+ filesWithDebt: 0,
278
+ byCategory: { complexity: 0, maintainability: 0, nesting: 0 },
279
+ };
280
+ }
281
+ let totalMI = 0;
282
+ let totalCognitive = 0;
283
+ let totalNesting = 0;
284
+ let filesWithDebt = 0;
285
+ let debtFromMI = 0;
286
+ let debtFromCognitive = 0;
287
+ let debtFromNesting = 0;
288
+ for (const file of files) {
289
+ const snap = file.latest;
290
+ totalMI += snap.mi;
291
+ totalCognitive += snap.cognitive;
292
+ totalNesting += snap.nesting;
293
+ // Accumulate debt points
294
+ let fileDebt = 0;
295
+ // MI debt: 0 at MI=100, max at MI=0
296
+ const miDebt = Math.max(0, (100 - snap.mi) / 100);
297
+ debtFromMI += miDebt;
298
+ // Cognitive debt: 0 at 0, max at 500+
299
+ const cogDebt = Math.min(1, snap.cognitive / 200);
300
+ debtFromCognitive += cogDebt;
301
+ // Nesting debt: 0 at 1-3, max at 10+
302
+ const nestDebt = Math.min(1, Math.max(0, snap.nesting - 3) / 7);
303
+ debtFromNesting += nestDebt;
304
+ fileDebt = miDebt + cogDebt + nestDebt;
305
+ if (fileDebt > 1)
306
+ filesWithDebt++; // File has at least some debt
307
+ }
308
+ const avgMI = totalMI / files.length;
309
+ // Normalize to 0-100 scale
310
+ const avgMIDebt = debtFromMI / files.length; // 0-1
311
+ const avgCogDebt = debtFromCognitive / files.length; // 0-1
312
+ const avgNestDebt = debtFromNesting / files.length; // 0-1
313
+ // Weighted: MI matters most (50%), cognitive (35%), nesting (15%)
314
+ const rawScore = avgMIDebt * 50 + avgCogDebt * 35 + avgNestDebt * 15;
315
+ const score = Math.round(rawScore * 100) / 100;
316
+ // Grade
317
+ let grade;
318
+ if (score <= 15)
319
+ grade = "A";
320
+ else if (score <= 30)
321
+ grade = "B";
322
+ else if (score <= 50)
323
+ grade = "C";
324
+ else if (score <= 70)
325
+ grade = "D";
326
+ else
327
+ grade = "F";
328
+ return {
329
+ score,
330
+ grade,
331
+ avgMI: Math.round(avgMI * 10) / 10,
332
+ totalCognitive,
333
+ filesAnalyzed: files.length,
334
+ filesWithDebt,
335
+ byCategory: {
336
+ complexity: Math.round(avgCogDebt * 100),
337
+ maintainability: Math.round(avgMIDebt * 100),
338
+ nesting: Math.round(avgNestDebt * 100),
339
+ },
340
+ };
341
+ }
@@ -348,3 +348,104 @@ export function formatTrendCell(
348
348
 
349
349
  return `${emoji} ${miColor}${miSign}${delta.mi}`;
350
350
  }
351
+
352
+ // --- Technical Debt Index (TDI) ---
353
+
354
+ export interface ProjectTDI {
355
+ score: number; // 0-100, higher = more debt
356
+ grade: string; // A-F
357
+ avgMI: number;
358
+ totalCognitive: number;
359
+ filesAnalyzed: number;
360
+ filesWithDebt: number;
361
+ byCategory: {
362
+ complexity: number;
363
+ maintainability: number;
364
+ nesting: number;
365
+ };
366
+ }
367
+
368
+ /**
369
+ * Calculate Technical Debt Index for the project.
370
+ * Score: 0 = perfect, 100 = maximum debt.
371
+ */
372
+ export function computeTDI(history: MetricsHistory): ProjectTDI {
373
+ const files = Object.values(history.files);
374
+ if (files.length === 0) {
375
+ return {
376
+ score: 0,
377
+ grade: "N/A",
378
+ avgMI: 100,
379
+ totalCognitive: 0,
380
+ filesAnalyzed: 0,
381
+ filesWithDebt: 0,
382
+ byCategory: { complexity: 0, maintainability: 0, nesting: 0 },
383
+ };
384
+ }
385
+
386
+ let totalMI = 0;
387
+ let totalCognitive = 0;
388
+ let totalNesting = 0;
389
+ let filesWithDebt = 0;
390
+ let debtFromMI = 0;
391
+ let debtFromCognitive = 0;
392
+ let debtFromNesting = 0;
393
+
394
+ for (const file of files) {
395
+ const snap = file.latest;
396
+ totalMI += snap.mi;
397
+ totalCognitive += snap.cognitive;
398
+ totalNesting += snap.nesting;
399
+
400
+ // Accumulate debt points
401
+ let fileDebt = 0;
402
+
403
+ // MI debt: 0 at MI=100, max at MI=0
404
+ const miDebt = Math.max(0, (100 - snap.mi) / 100);
405
+ debtFromMI += miDebt;
406
+
407
+ // Cognitive debt: 0 at 0, max at 500+
408
+ const cogDebt = Math.min(1, snap.cognitive / 200);
409
+ debtFromCognitive += cogDebt;
410
+
411
+ // Nesting debt: 0 at 1-3, max at 10+
412
+ const nestDebt = Math.min(1, Math.max(0, snap.nesting - 3) / 7);
413
+ debtFromNesting += nestDebt;
414
+
415
+ fileDebt = miDebt + cogDebt + nestDebt;
416
+ if (fileDebt > 1) filesWithDebt++; // File has at least some debt
417
+ }
418
+
419
+ const avgMI = totalMI / files.length;
420
+
421
+ // Normalize to 0-100 scale
422
+ const avgMIDebt = debtFromMI / files.length; // 0-1
423
+ const avgCogDebt = debtFromCognitive / files.length; // 0-1
424
+ const avgNestDebt = debtFromNesting / files.length; // 0-1
425
+
426
+ // Weighted: MI matters most (50%), cognitive (35%), nesting (15%)
427
+ const rawScore = avgMIDebt * 50 + avgCogDebt * 35 + avgNestDebt * 15;
428
+ const score = Math.round(rawScore * 100) / 100;
429
+
430
+ // Grade
431
+ let grade: string;
432
+ if (score <= 15) grade = "A";
433
+ else if (score <= 30) grade = "B";
434
+ else if (score <= 50) grade = "C";
435
+ else if (score <= 70) grade = "D";
436
+ else grade = "F";
437
+
438
+ return {
439
+ score,
440
+ grade,
441
+ avgMI: Math.round(avgMI * 10) / 10,
442
+ totalCognitive,
443
+ filesAnalyzed: files.length,
444
+ filesWithDebt,
445
+ byCategory: {
446
+ complexity: Math.round(avgCogDebt * 100),
447
+ maintainability: Math.round(avgMIDebt * 100),
448
+ nesting: Math.round(avgNestDebt * 100),
449
+ },
450
+ };
451
+ }
package/commands/fix.js CHANGED
@@ -112,41 +112,57 @@ function generatePlan(results, session, _isTsProject, prevCounts) {
112
112
  }
113
113
  }
114
114
  nodeFs.writeFileSync(reportPath, tsvRows.join("\n"), "utf-8");
115
- // --- Build compact summary for terminal ---
115
+ // --- Build actionable list for terminal (no TSV reading needed) ---
116
116
  const lines = [];
117
- lines.push(`šŸ“‹ FIX PLAN — Iteration ${session.iteration}/${MAX_ITERATIONS} — ${totalFixable} issues`);
118
- lines.push(`šŸ“„ Full plan: .pi-lens/reports/fix-plan.tsv\n`);
119
- // Summary by category with counts
117
+ lines.push(`šŸ“‹ FIX PLAN — Iteration ${session.iteration}/${MAX_ITERATIONS} — ${totalFixable} issues:\n`);
118
+ // Duplicates
120
119
  if (filteredDups.length > 0) {
121
- lines.push(`šŸ” Duplicates: ${filteredDups.length} block(s) — extract to shared utils`);
120
+ for (const clone of filteredDups.slice(0, 10)) {
121
+ lines.push(`šŸ” ${clone.fileA}:${clone.startA} — ${clone.lines} dup from ${clone.fileB}:${clone.startB}`);
122
+ }
123
+ if (filteredDups.length > 10)
124
+ lines.push(` ... +${filteredDups.length - 10} more`);
125
+ lines.push("");
122
126
  }
127
+ // Dead code
123
128
  if (filteredDeadCode.length > 0) {
124
- lines.push(`šŸ—‘ļø Dead code: ${filteredDeadCode.length} item(s) — remove unused exports`);
129
+ for (const issue of filteredDeadCode.slice(0, 10)) {
130
+ lines.push(`šŸ—‘ļø ${issue.file || issue.name} — ${issue.name} unused`);
131
+ }
132
+ if (filteredDeadCode.length > 10)
133
+ lines.push(` ... +${filteredDeadCode.length - 10} more`);
134
+ lines.push("");
125
135
  }
136
+ // AST lint
126
137
  if (agentTasks.length > 0) {
127
- // Group by rule, show top 5 rules
128
- const grouped = new Map();
129
- for (const t of agentTasks) {
130
- grouped.set(t.rule, (grouped.get(t.rule) ?? 0) + 1);
138
+ for (const issue of agentTasks.slice(0, 15)) {
139
+ lines.push(`šŸ”Ø ${issue.file}:${issue.line} — ${issue.rule}`);
131
140
  }
132
- const sorted = [...grouped.entries()].sort((a, b) => b[1] - a[1]);
133
- lines.push(`šŸ”Ø Lint: ${agentTasks.length} item(s)`);
134
- for (const [rule, count] of sorted.slice(0, 5)) {
135
- lines.push(` ${rule}: ${count}`);
136
- }
137
- if (sorted.length > 5)
138
- lines.push(` ... and ${sorted.length - 5} more rules`);
141
+ if (agentTasks.length > 15)
142
+ lines.push(` ... +${agentTasks.length - 15} more`);
143
+ lines.push("");
139
144
  }
145
+ // Biome
140
146
  if (filteredBiome.length > 0) {
141
- lines.push(`🟠 Biome: ${filteredBiome.length} item(s) — auto-fixable`);
147
+ for (const issue of filteredBiome.slice(0, 10)) {
148
+ lines.push(`🟠 ${issue.file}:${issue.line} — ${issue.rule}`);
149
+ }
150
+ if (filteredBiome.length > 10)
151
+ lines.push(` ... +${filteredBiome.length - 10} more`);
152
+ lines.push("");
142
153
  }
154
+ // AI Slop
143
155
  if (filteredSlop.length > 0) {
144
- lines.push(`šŸ¤– AI Slop: ${filteredSlop.length} file(s) — review complexity`);
156
+ for (const { file, warnings } of filteredSlop.slice(0, 5)) {
157
+ lines.push(`šŸ¤– ${file} — ${warnings[0]}`);
158
+ }
159
+ if (filteredSlop.length > 5)
160
+ lines.push(` ... +${filteredSlop.length - 5} more`);
161
+ lines.push("");
145
162
  }
146
- lines.push("\n---");
147
- lines.push("šŸ“– **Read plan**: `read .pi-lens/reports/fix-plan.tsv` for full details");
148
- lines.push("šŸš€ **Fix & loop**: Fix items, then run `/lens-booboo-fix --loop`");
149
- lines.push('🚫 **False positive**: `/lens-booboo-fix --false-positive "type:file:line"`');
163
+ lines.push("---");
164
+ lines.push("šŸš€ Fix items above, then run `/lens-booboo-fix --loop`");
165
+ lines.push('🚫 False positive: `/lens-booboo-fix --false-positive "type:file:line"`');
150
166
  return lines.join("\n");
151
167
  }
152
168
  // --- Main handler ---
package/commands/fix.ts CHANGED
@@ -183,56 +183,68 @@ function generatePlan(
183
183
 
184
184
  nodeFs.writeFileSync(reportPath, tsvRows.join("\n"), "utf-8");
185
185
 
186
- // --- Build compact summary for terminal ---
186
+ // --- Build actionable list for terminal (no TSV reading needed) ---
187
187
  const lines: string[] = [];
188
188
  lines.push(
189
- `šŸ“‹ FIX PLAN — Iteration ${session.iteration}/${MAX_ITERATIONS} — ${totalFixable} issues`,
189
+ `šŸ“‹ FIX PLAN — Iteration ${session.iteration}/${MAX_ITERATIONS} — ${totalFixable} issues:\n`,
190
190
  );
191
- lines.push(`šŸ“„ Full plan: .pi-lens/reports/fix-plan.tsv\n`);
192
191
 
193
- // Summary by category with counts
192
+ // Duplicates
194
193
  if (filteredDups.length > 0) {
195
- lines.push(
196
- `šŸ” Duplicates: ${filteredDups.length} block(s) — extract to shared utils`,
197
- );
194
+ for (const clone of filteredDups.slice(0, 10)) {
195
+ lines.push(
196
+ `šŸ” ${clone.fileA}:${clone.startA} — ${clone.lines} dup from ${clone.fileB}:${clone.startB}`,
197
+ );
198
+ }
199
+ if (filteredDups.length > 10)
200
+ lines.push(` ... +${filteredDups.length - 10} more`);
201
+ lines.push("");
198
202
  }
203
+
204
+ // Dead code
199
205
  if (filteredDeadCode.length > 0) {
200
- lines.push(
201
- `šŸ—‘ļø Dead code: ${filteredDeadCode.length} item(s) — remove unused exports`,
202
- );
206
+ for (const issue of filteredDeadCode.slice(0, 10)) {
207
+ lines.push(`šŸ—‘ļø ${issue.file || issue.name} — ${issue.name} unused`);
208
+ }
209
+ if (filteredDeadCode.length > 10)
210
+ lines.push(` ... +${filteredDeadCode.length - 10} more`);
211
+ lines.push("");
203
212
  }
213
+
214
+ // AST lint
204
215
  if (agentTasks.length > 0) {
205
- // Group by rule, show top 5 rules
206
- const grouped = new Map<string, number>();
207
- for (const t of agentTasks) {
208
- grouped.set(t.rule, (grouped.get(t.rule) ?? 0) + 1);
209
- }
210
- const sorted = [...grouped.entries()].sort((a, b) => b[1] - a[1]);
211
- lines.push(`šŸ”Ø Lint: ${agentTasks.length} item(s)`);
212
- for (const [rule, count] of sorted.slice(0, 5)) {
213
- lines.push(` ${rule}: ${count}`);
216
+ for (const issue of agentTasks.slice(0, 15)) {
217
+ lines.push(`šŸ”Ø ${issue.file}:${issue.line} — ${issue.rule}`);
214
218
  }
215
- if (sorted.length > 5)
216
- lines.push(` ... and ${sorted.length - 5} more rules`);
219
+ if (agentTasks.length > 15)
220
+ lines.push(` ... +${agentTasks.length - 15} more`);
221
+ lines.push("");
217
222
  }
223
+
224
+ // Biome
218
225
  if (filteredBiome.length > 0) {
219
- lines.push(`🟠 Biome: ${filteredBiome.length} item(s) — auto-fixable`);
226
+ for (const issue of filteredBiome.slice(0, 10)) {
227
+ lines.push(`🟠 ${issue.file}:${issue.line} — ${issue.rule}`);
228
+ }
229
+ if (filteredBiome.length > 10)
230
+ lines.push(` ... +${filteredBiome.length - 10} more`);
231
+ lines.push("");
220
232
  }
233
+
234
+ // AI Slop
221
235
  if (filteredSlop.length > 0) {
222
- lines.push(
223
- `šŸ¤– AI Slop: ${filteredSlop.length} file(s) — review complexity`,
224
- );
236
+ for (const { file, warnings } of filteredSlop.slice(0, 5)) {
237
+ lines.push(`šŸ¤– ${file} — ${warnings[0]}`);
238
+ }
239
+ if (filteredSlop.length > 5)
240
+ lines.push(` ... +${filteredSlop.length - 5} more`);
241
+ lines.push("");
225
242
  }
226
243
 
227
- lines.push("\n---");
228
- lines.push(
229
- "šŸ“– **Read plan**: `read .pi-lens/reports/fix-plan.tsv` for full details",
230
- );
231
- lines.push(
232
- "šŸš€ **Fix & loop**: Fix items, then run `/lens-booboo-fix --loop`",
233
- );
244
+ lines.push("---");
245
+ lines.push("šŸš€ Fix items above, then run `/lens-booboo-fix --loop`");
234
246
  lines.push(
235
- '🚫 **False positive**: `/lens-booboo-fix --false-positive "type:file:line"`',
247
+ '🚫 False positive: `/lens-booboo-fix --false-positive "type:file:line"`',
236
248
  );
237
249
 
238
250
  return lines.join("\n");
@@ -83,16 +83,18 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
83
83
  : "";
84
84
  // First violation line for quick reference
85
85
  const firstViolationLine = issues.length > 0 ? issues[0].line : null;
86
- // --- Compact terminal summary ---
86
+ // --- Full ranked list in terminal (agent won't read TSV) ---
87
87
  const topFiles = scored
88
- .slice(0, 5)
88
+ .slice(0, 15)
89
89
  .map((f, i) => {
90
90
  const name = path.relative(targetPath, f.file).replace(/\\/g, "/");
91
- return ` ${i + 1}. ${name} (score: ${f.score})`;
91
+ const m = metricsByFile.get(f.file);
92
+ const mi = m ? `MI:${m.mi.toFixed(0)}` : "";
93
+ return ` ${i + 1}. ${name} (${f.score} pts${mi ? `, ${mi}` : ""})`;
92
94
  })
93
95
  .join("\n");
94
96
  ctx.ui.notify(`šŸ—ļø Worst: ${relFile} (score: ${score}) — ${scored.length} files with debt`, "info");
95
- console.log(`\nšŸ“Š Top ${Math.min(scored.length, 5)} worst offenders:\n${topFiles}\nšŸ“„ Full ranked list: .pi-lens/reports/refactor-ranked.tsv\n`);
97
+ console.log(`\nšŸ“Š Ranked by debt score:\n${topFiles}${scored.length > 15 ? `\n ... and ${scored.length - 15} more` : ""}\n`);
96
98
  // --- Steer message for agent ---
97
99
  const steer = [
98
100
  `šŸ—ļø BOOBOO REFACTOR — worst offender identified`,
@@ -107,7 +109,7 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
107
109
  : "",
108
110
  firstViolationLine ? `First violation at line ${firstViolationLine}` : "",
109
111
  "",
110
- `šŸ“„ Full details: .pi-lens/reports/refactor-ranked.tsv — read \`${relFile}\` when ready`,
112
+ `šŸ“„ Read \`${relFile}\` when ready to implement`,
111
113
  "",
112
114
  "**Your job**:",
113
115
  "1. Analyze this code — what's the most impactful refactoring for this file?",
@@ -148,12 +148,14 @@ export async function handleRefactor(
148
148
  // First violation line for quick reference
149
149
  const firstViolationLine = issues.length > 0 ? issues[0].line : null;
150
150
 
151
- // --- Compact terminal summary ---
151
+ // --- Full ranked list in terminal (agent won't read TSV) ---
152
152
  const topFiles = scored
153
- .slice(0, 5)
153
+ .slice(0, 15)
154
154
  .map((f, i) => {
155
155
  const name = path.relative(targetPath, f.file).replace(/\\/g, "/");
156
- return ` ${i + 1}. ${name} (score: ${f.score})`;
156
+ const m = metricsByFile.get(f.file);
157
+ const mi = m ? `MI:${m.mi.toFixed(0)}` : "";
158
+ return ` ${i + 1}. ${name} (${f.score} pts${mi ? `, ${mi}` : ""})`;
157
159
  })
158
160
  .join("\n");
159
161
 
@@ -162,7 +164,7 @@ export async function handleRefactor(
162
164
  "info",
163
165
  );
164
166
  console.log(
165
- `\nšŸ“Š Top ${Math.min(scored.length, 5)} worst offenders:\n${topFiles}\nšŸ“„ Full ranked list: .pi-lens/reports/refactor-ranked.tsv\n`,
167
+ `\nšŸ“Š Ranked by debt score:\n${topFiles}${scored.length > 15 ? `\n ... and ${scored.length - 15} more` : ""}\n`,
166
168
  );
167
169
 
168
170
  // --- Steer message for agent ---
@@ -179,7 +181,7 @@ export async function handleRefactor(
179
181
  : "",
180
182
  firstViolationLine ? `First violation at line ${firstViolationLine}` : "",
181
183
  "",
182
- `šŸ“„ Full details: .pi-lens/reports/refactor-ranked.tsv — read \`${relFile}\` when ready`,
184
+ `šŸ“„ Read \`${relFile}\` when ready to implement`,
183
185
  "",
184
186
  "**Your job**:",
185
187
  "1. Analyze this code — what's the most impactful refactoring for this file?",
package/index.ts CHANGED
@@ -634,6 +634,40 @@ export default function (pi: ExtensionAPI) {
634
634
  },
635
635
  });
636
636
 
637
+ pi.registerCommand("lens-tdi", {
638
+ description:
639
+ "Show Technical Debt Index (TDI) and project health trend. Usage: /lens-tdi",
640
+ handler: async (_args, ctx) => {
641
+ const { loadHistory, computeTDI } = await import(
642
+ "./clients/metrics-history.js"
643
+ );
644
+ const history = loadHistory();
645
+ const tdi = computeTDI(history);
646
+
647
+ const lines = [
648
+ `šŸ“Š TECHNICAL DEBT INDEX: ${tdi.score}/100 (${tdi.grade})`,
649
+ ``,
650
+ `Files analyzed: ${tdi.filesAnalyzed}`,
651
+ `Files with debt: ${tdi.filesWithDebt}`,
652
+ `Avg MI: ${tdi.avgMI}`,
653
+ `Total cognitive complexity: ${tdi.totalCognitive}`,
654
+ ``,
655
+ `Debt breakdown:`,
656
+ ` Maintainability: ${tdi.byCategory.maintainability}%`,
657
+ ` Complexity: ${tdi.byCategory.complexity}%`,
658
+ ` Nesting: ${tdi.byCategory.nesting}%`,
659
+ ``,
660
+ tdi.score <= 30
661
+ ? "āœ… Codebase is healthy!"
662
+ : tdi.score <= 60
663
+ ? "āš ļø Moderate debt — consider refactoring"
664
+ : "šŸ”“ High debt — run /lens-booboo-refactor",
665
+ ];
666
+
667
+ ctx.ui.notify(lines.join("\n"), "info");
668
+ },
669
+ });
670
+
637
671
  pi.registerCommand("lens-format", {
638
672
  description:
639
673
  "Apply Biome formatting to files. Usage: /lens-format [file-path] or /lens-format --all",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "type": "module",
5
5
  "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.",
6
6
  "repository": {