pi-lens 2.2.7 → 2.2.9

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
+ }
@@ -68,11 +68,31 @@ const SECRET_PATTERNS = [
68
68
  message: "Possible secret in .env format",
69
69
  },
70
70
  ];
71
+ /**
72
+ * Check if file path is a test file (should skip secrets scan)
73
+ */
74
+ function isTestFile(filePath) {
75
+ const normalized = filePath.replace(/\\/g, "/");
76
+ return (normalized.includes(".test.") ||
77
+ normalized.includes(".spec.") ||
78
+ normalized.includes("/test/") ||
79
+ normalized.includes("/tests/") ||
80
+ normalized.includes("__tests__/") ||
81
+ normalized.includes("test-utils") ||
82
+ normalized.startsWith("test-") ||
83
+ normalized.includes(".fixture.") ||
84
+ normalized.includes(".mock."));
85
+ }
71
86
  /**
72
87
  * Scan content for potential secrets
73
- * Returns findings with line numbers
88
+ * Returns findings with line numbers.
89
+ * Skips test files to avoid false positives.
74
90
  */
75
- export function scanForSecrets(content) {
91
+ export function scanForSecrets(content, filePath) {
92
+ // Skip test files — secrets in tests are usually fake/test values
93
+ if (filePath && isTestFile(filePath)) {
94
+ return [];
95
+ }
76
96
  const findings = [];
77
97
  const lines = content.split("\n");
78
98
  for (let i = 0; i < lines.length; i++) {
@@ -83,11 +83,38 @@ export interface SecretFinding {
83
83
  message: string;
84
84
  }
85
85
 
86
+ /**
87
+ * Check if file path is a test file (should skip secrets scan)
88
+ */
89
+ function isTestFile(filePath: string): boolean {
90
+ const normalized = filePath.replace(/\\/g, "/");
91
+ return (
92
+ normalized.includes(".test.") ||
93
+ normalized.includes(".spec.") ||
94
+ normalized.includes("/test/") ||
95
+ normalized.includes("/tests/") ||
96
+ normalized.includes("__tests__/") ||
97
+ normalized.includes("test-utils") ||
98
+ normalized.startsWith("test-") ||
99
+ normalized.includes(".fixture.") ||
100
+ normalized.includes(".mock.")
101
+ );
102
+ }
103
+
86
104
  /**
87
105
  * Scan content for potential secrets
88
- * Returns findings with line numbers
106
+ * Returns findings with line numbers.
107
+ * Skips test files to avoid false positives.
89
108
  */
90
- export function scanForSecrets(content: string): SecretFinding[] {
109
+ export function scanForSecrets(
110
+ content: string,
111
+ filePath?: string,
112
+ ): SecretFinding[] {
113
+ // Skip test files — secrets in tests are usually fake/test values
114
+ if (filePath && isTestFile(filePath)) {
115
+ return [];
116
+ }
117
+
91
118
  const findings: SecretFinding[] = [];
92
119
  const lines = content.split("\n");
93
120
 
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",
@@ -1269,7 +1303,7 @@ export default function (pi: ExtensionAPI) {
1269
1303
 
1270
1304
  // --- Secrets scan (blocking - must check before other linting) ---
1271
1305
  if (fileContent) {
1272
- const secretFindings = scanForSecrets(fileContent);
1306
+ const secretFindings = scanForSecrets(fileContent, filePath);
1273
1307
  if (secretFindings.length > 0) {
1274
1308
  const secretsOutput = formatSecrets(secretFindings, filePath);
1275
1309
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
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": {