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
@@ -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,
@@ -21,30 +19,14 @@ import * as path from "node:path";
21
19
  export interface FileMetrics {
22
20
  filePath: string;
23
21
  totalLines: number;
24
- agentLines: number; // lines written by agent this session
25
- preExistingLines: number; // lines that existed before session
26
22
  entropyStart: number; // Shannon entropy at first touch
27
23
  entropyCurrent: number; // Current Shannon entropy
28
24
  entropyDelta: number; // Change in entropy
29
- tdrStart: number; // New field
30
- tdrCurrent: number; // New field
31
- tdrContributors: TDREntry[];
32
- }
33
-
34
- export interface TDREntry {
35
- category: string;
36
- count: number;
37
- severity: "error" | "warning" | "info";
38
25
  }
39
26
 
40
27
  export interface SessionMetrics {
41
28
  filesModified: number;
42
- totalAgentLines: number;
43
- totalPreExistingLines: number;
44
- aiCodeRatio: number; // 0-1, agent lines / total lines
45
29
  avgEntropyDelta: number; // average across files
46
- tdrScore: number; // 0-100, lower is better
47
- tdrByCategory: Map<string, number>;
48
30
  fileDetails: Map<string, FileMetrics>;
49
31
  }
50
32
 
@@ -52,12 +34,8 @@ export interface SessionMetrics {
52
34
 
53
35
  export class MetricsClient {
54
36
  private log: (msg: string) => void;
55
- private fileBaselines: Map<
56
- string,
57
- { content: string; entropy: number; tdr: number }
58
- > = new Map();
59
- private fileSessionWrites: Map<string, number> = new Map(); // agent-written lines
60
- private tdrFindings: Map<string, TDREntry[]> = new Map();
37
+ private fileBaselines: Map<string, { content: string; entropy: number }> =
38
+ new Map();
61
39
 
62
40
  constructor(verbose = false) {
63
41
  this.log = verbose
@@ -68,65 +46,17 @@ export class MetricsClient {
68
46
  /**
69
47
  * Record initial state of a file when first touched this session
70
48
  */
71
- recordBaseline(filePath: string, initialTdr = 0): void {
49
+ recordBaseline(filePath: string): void {
72
50
  const absolutePath = path.resolve(filePath);
73
51
  if (!fs.existsSync(absolutePath)) return;
74
52
  if (this.fileBaselines.has(absolutePath)) return; // Already recorded
75
53
 
76
54
  const content = fs.readFileSync(absolutePath, "utf-8");
77
55
  const entropy = this.calculateEntropy(content);
78
- this.fileBaselines.set(absolutePath, { content, entropy, tdr: initialTdr });
79
- this.fileSessionWrites.set(absolutePath, 0);
80
-
81
- this.log(
82
- `Baseline recorded: ${path.basename(filePath)} (entropy: ${entropy.toFixed(2)}, tdr: ${initialTdr})`,
83
- );
84
- }
85
-
86
- /**
87
- * Update TDR findings for a file
88
- */
89
- updateTDR(filePath: string, entries: TDREntry[]): void {
90
- const absolutePath = path.resolve(filePath);
91
- this.tdrFindings.set(absolutePath, entries);
92
- }
93
-
94
- /**
95
- * Get overall TDR score for the session
96
- * 0-100, where 100 is high debt.
97
- */
98
- getTDRScore(): number {
99
- let totalScore = 0;
100
- for (const entries of this.tdrFindings.values()) {
101
- for (const entry of entries) {
102
- // Each entry adds to the debt index based on its Grade (count as the Grade value)
103
- totalScore += entry.count;
104
- }
105
- }
106
- // Normalize to 0-100? Or just return the raw Index.
107
- // SCA.md says "Technical Debt Index"
108
- return totalScore;
109
- }
110
-
111
- /**
112
- * Record that the agent wrote/replaced content in a file
113
- * @param newContent The new content after the write
114
- */
115
- recordWrite(filePath: string, newContent: string): void {
116
- const absolutePath = path.resolve(filePath);
117
- this.recordBaseline(absolutePath);
118
-
119
- const baseline = this.fileBaselines.get(absolutePath)!;
120
- const _baselineLines = baseline.content.split("\n").length;
121
- const _newLines = newContent.split("\n").length;
122
-
123
- // Estimate agent-written lines: count the diff
124
- const diffLines = this.estimateDiffLines(baseline.content, newContent);
125
- const currentAgentLines = this.fileSessionWrites.get(absolutePath) || 0;
126
- this.fileSessionWrites.set(absolutePath, currentAgentLines + diffLines);
56
+ this.fileBaselines.set(absolutePath, { content, entropy });
127
57
 
128
58
  this.log(
129
- `Write recorded: ${path.basename(filePath)} (+~${diffLines} agent lines)`,
59
+ `Baseline recorded: ${path.basename(filePath)} (entropy: ${entropy.toFixed(2)})`,
130
60
  );
131
61
  }
132
62
 
@@ -142,66 +72,16 @@ export class MetricsClient {
142
72
 
143
73
  const currentContent = fs.readFileSync(absolutePath, "utf-8");
144
74
  const totalLines = currentContent.split("\n").length;
145
- const agentLines = this.fileSessionWrites.get(absolutePath) || 0;
146
75
 
147
76
  const entropyCurrent = this.calculateEntropy(currentContent);
148
77
  const entropyDelta = entropyCurrent - baseline.entropy;
149
78
 
150
- const currentTdrFindings = this.tdrFindings.get(absolutePath) || [];
151
- const tdrCurrent = currentTdrFindings.reduce((a, b) => a + b.count, 0);
152
-
153
79
  return {
154
80
  filePath: path.relative(process.cwd(), absolutePath),
155
81
  totalLines,
156
- agentLines: Math.min(agentLines, totalLines),
157
- preExistingLines: Math.max(0, totalLines - agentLines),
158
82
  entropyStart: baseline.entropy,
159
83
  entropyCurrent,
160
84
  entropyDelta,
161
- tdrStart: baseline.tdr,
162
- tdrCurrent,
163
- tdrContributors: currentTdrFindings,
164
- };
165
- }
166
-
167
- /**
168
- * Calculate AI Code Ratio for the session
169
- * Returns 0-1 where 1 = all code written by agent
170
- */
171
- getAICodeRatio(): {
172
- ratio: number;
173
- agentLines: number;
174
- preExistingLines: number;
175
- fileCount: number;
176
- } {
177
- let totalAgentLines = 0;
178
- let totalPreExistingLines = 0;
179
- let fileCount = 0;
180
-
181
- for (const [filePath, agentLines] of this.fileSessionWrites) {
182
- if (!fs.existsSync(filePath)) continue;
183
-
184
- const content = fs.readFileSync(filePath, "utf-8");
185
- const totalLines = content.split("\n").length;
186
- const baseline = this.fileBaselines.get(filePath);
187
- const _baselineLines = baseline
188
- ? baseline.content.split("\n").length
189
- : totalLines;
190
-
191
- // Pre-existing = lines that existed before this session and weren't replaced
192
- const preExisting = Math.max(0, totalLines - agentLines);
193
-
194
- totalAgentLines += agentLines;
195
- totalPreExistingLines += preExisting;
196
- fileCount++;
197
- }
198
-
199
- const total = totalAgentLines + totalPreExistingLines;
200
- return {
201
- ratio: total > 0 ? totalAgentLines / total : 0, // fixed
202
- agentLines: totalAgentLines,
203
- preExistingLines: totalPreExistingLines,
204
- fileCount,
205
85
  };
206
86
  }
207
87
 
@@ -223,7 +103,6 @@ export class MetricsClient {
223
103
 
224
104
  for (const [filePath, baseline] of this.fileBaselines) {
225
105
  if (!fs.existsSync(filePath)) continue;
226
- if (!this.fileSessionWrites.has(filePath)) continue;
227
106
 
228
107
  const content = fs.readFileSync(filePath, "utf-8");
229
108
  const current = this.calculateEntropy(content);
@@ -264,104 +143,11 @@ export class MetricsClient {
264
143
  return entropy;
265
144
  }
266
145
 
267
- /**
268
- * Format metrics for session summary
269
- */
270
- formatSessionSummary(): string {
271
- const aiRatio = this.getAICodeRatio();
272
- const entropyDeltas = this.getEntropyDeltas();
273
- const fileCount = this.fileSessionWrites.size;
274
-
275
- if (fileCount === 0) return ""; // No files touched
276
-
277
- const parts: string[] = [];
278
-
279
- // Aggregate TDR from details
280
- let totalTdrCurrent = 0;
281
- let totalTdrStart = 0;
282
- for (const path of this.fileSessionWrites.keys()) {
283
- const m = this.getFileMetrics(path);
284
- if (m) {
285
- totalTdrCurrent += m.tdrCurrent;
286
- totalTdrStart += m.tdrStart;
287
- }
288
- }
289
-
290
- // Technical Debt Index
291
- if (totalTdrCurrent > 0 || totalTdrStart > 0) {
292
- const delta = totalTdrCurrent - totalTdrStart;
293
- const deltaStr =
294
- delta !== 0
295
- ? ` (${delta > 0 ? "+" : ""}${delta.toFixed(1)} this session)`
296
- : "";
297
- parts.push(
298
- `[TDR Index] Total Debt: ${totalTdrCurrent.toFixed(1)}${deltaStr}`,
299
- );
300
- }
301
-
302
- // AI Code Ratio
303
- const pct = (aiRatio.ratio * 100).toFixed(1);
304
- parts.push(
305
- `[AI Code] ${pct}% of ${fileCount} file(s) written by agent this session (${aiRatio.agentLines} lines, ${aiRatio.preExistingLines} pre-existing)`,
306
- );
307
-
308
- // Entropy deltas (only show files with significant changes)
309
- const significant = entropyDeltas.filter((e) => Math.abs(e.delta) > 0.1);
310
- if (significant.length > 0) {
311
- const topChanges = significant.slice(0, 5);
312
- parts.push(
313
- `[Entropy] ${significant.length} file(s) with complexity changes:`,
314
- );
315
- for (const e of topChanges) {
316
- const arrow = e.delta > 0 ? "↑" : "↓";
317
- const sign = e.delta > 0 ? "+" : "";
318
- parts.push(
319
- ` ${arrow} ${e.file}: ${e.start.toFixed(2)} → ${e.current.toFixed(2)} (${sign}${e.delta.toFixed(2)} bits)`,
320
- );
321
- }
322
- if (significant.length > 5) {
323
- parts.push(` ... and ${significant.length - 5} more`);
324
- }
325
- }
326
-
327
- return parts.join("\n");
328
- }
329
-
330
146
  /**
331
147
  * Reset session state (for new session)
332
148
  */
333
149
  reset(): void {
334
150
  this.fileBaselines.clear();
335
- this.fileSessionWrites.clear();
336
151
  this.log("Session metrics reset");
337
152
  }
338
-
339
- // --- Internal ---
340
-
341
- /**
342
- * Estimate number of lines that changed between two texts
343
- * Simple line-based diff (not Myers, but good enough for metrics)
344
- */
345
- private estimateDiffLines(oldText: string, newText: string): number {
346
- const oldLines = new Set(oldText.split("\n"));
347
- const newLines = newText.split("\n");
348
-
349
- let changed = 0;
350
- for (const line of newLines) {
351
- if (!oldLines.has(line)) {
352
- changed++;
353
- }
354
- }
355
-
356
- // Also count deleted lines
357
- const newLinesSet = new Set(newLines);
358
- for (const line of oldLines) {
359
- if (!newLinesSet.has(line)) {
360
- changed++;
361
- }
362
- }
363
-
364
- // Return roughly half (additions + deletions / 2)
365
- return Math.max(1, Math.ceil(changed / 2));
366
- }
367
153
  }
@@ -86,6 +86,8 @@ export function captureSnapshot(filePath, metrics) {
86
86
  cognitive: metrics.cognitiveComplexity,
87
87
  nesting: metrics.maxNestingDepth,
88
88
  lines: metrics.linesOfCode,
89
+ maxCyclomatic: metrics.maxCyclomatic,
90
+ entropy: Math.round(metrics.entropy * 100) / 100,
89
91
  };
90
92
  const existing = pendingHistory.files[relativePath];
91
93
  if (existing) {
@@ -136,6 +138,8 @@ export function captureSnapshots(files) {
136
138
  cognitive: file.metrics.cognitiveComplexity,
137
139
  nesting: file.metrics.maxNestingDepth,
138
140
  lines: file.metrics.linesOfCode,
141
+ maxCyclomatic: file.metrics.maxCyclomatic,
142
+ entropy: Math.round(file.metrics.entropy * 100) / 100,
139
143
  };
140
144
  const existing = history.files[relativePath];
141
145
  if (existing) {
@@ -275,7 +279,13 @@ export function computeTDI(history) {
275
279
  totalCognitive: 0,
276
280
  filesAnalyzed: 0,
277
281
  filesWithDebt: 0,
278
- byCategory: { complexity: 0, maintainability: 0, nesting: 0 },
282
+ byCategory: {
283
+ maintainability: 0,
284
+ cognitive: 0,
285
+ nesting: 0,
286
+ maxCyclomatic: 0,
287
+ entropy: 0,
288
+ },
279
289
  };
280
290
  }
281
291
  let totalMI = 0;
@@ -285,6 +295,8 @@ export function computeTDI(history) {
285
295
  let debtFromMI = 0;
286
296
  let debtFromCognitive = 0;
287
297
  let debtFromNesting = 0;
298
+ let debtFromMaxCyclomatic = 0; // NEW
299
+ let debtFromEntropy = 0; // NEW
288
300
  for (const file of files) {
289
301
  const snap = file.latest;
290
302
  totalMI += snap.mi;
@@ -301,17 +313,29 @@ export function computeTDI(history) {
301
313
  // Nesting debt: 0 at 1-3, max at 10+
302
314
  const nestDebt = Math.min(1, Math.max(0, snap.nesting - 3) / 7);
303
315
  debtFromNesting += nestDebt;
304
- fileDebt = miDebt + cogDebt + nestDebt;
305
- if (fileDebt > 1)
306
- filesWithDebt++; // File has at least some debt
316
+ // Max Cyclomatic debt: 0 at max<=10, 1 at max>=30
317
+ const maxCycDebt = Math.min(1, Math.max(0, snap.maxCyclomatic - 10) / 20);
318
+ debtFromMaxCyclomatic += maxCycDebt;
319
+ // Entropy debt: 0 at entropy<=4.0, 1 at entropy>=7.0
320
+ const entropyDebt = Math.min(1, Math.max(0, snap.entropy - 4.0) / 3.0);
321
+ debtFromEntropy += entropyDebt;
322
+ fileDebt = miDebt + cogDebt + nestDebt + maxCycDebt + entropyDebt;
323
+ if (fileDebt > 0.5)
324
+ filesWithDebt++; // Lowered threshold since we have more factors
307
325
  }
308
326
  const avgMI = totalMI / files.length;
309
327
  // Normalize to 0-100 scale
310
328
  const avgMIDebt = debtFromMI / files.length; // 0-1
311
329
  const avgCogDebt = debtFromCognitive / files.length; // 0-1
312
330
  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;
331
+ const avgMaxCycDebt = debtFromMaxCyclomatic / files.length; // NEW
332
+ const avgEntropyDebt = debtFromEntropy / files.length; // NEW
333
+ // Weighted: MI (45%), cognitive (30%), nesting (10%), maxCyc (10%), entropy (5%)
334
+ const rawScore = avgMIDebt * 45 +
335
+ avgCogDebt * 30 +
336
+ avgNestDebt * 10 +
337
+ avgMaxCycDebt * 10 +
338
+ avgEntropyDebt * 5;
315
339
  const score = Math.round(rawScore * 100) / 100;
316
340
  // Grade
317
341
  let grade;
@@ -333,9 +357,11 @@ export function computeTDI(history) {
333
357
  filesAnalyzed: files.length,
334
358
  filesWithDebt,
335
359
  byCategory: {
336
- complexity: Math.round(avgCogDebt * 100),
337
360
  maintainability: Math.round(avgMIDebt * 100),
361
+ cognitive: Math.round(avgCogDebt * 100),
338
362
  nesting: Math.round(avgNestDebt * 100),
363
+ maxCyclomatic: Math.round(avgMaxCycDebt * 100),
364
+ entropy: Math.round(avgEntropyDebt * 100),
339
365
  },
340
366
  };
341
367
  }
@@ -19,6 +19,8 @@ export interface MetricSnapshot {
19
19
  cognitive: number;
20
20
  nesting: number;
21
21
  lines: number;
22
+ maxCyclomatic: number; // NEW: worst function complexity
23
+ entropy: number; // NEW: code unpredictability in bits
22
24
  }
23
25
 
24
26
  export interface FileHistory {
@@ -115,6 +117,8 @@ export function captureSnapshot(
115
117
  cognitiveComplexity: number;
116
118
  maxNestingDepth: number;
117
119
  linesOfCode: number;
120
+ maxCyclomatic: number;
121
+ entropy: number;
118
122
  },
119
123
  ): void {
120
124
  // Use in-memory cache if available, otherwise load from disk
@@ -132,6 +136,8 @@ export function captureSnapshot(
132
136
  cognitive: metrics.cognitiveComplexity,
133
137
  nesting: metrics.maxNestingDepth,
134
138
  lines: metrics.linesOfCode,
139
+ maxCyclomatic: metrics.maxCyclomatic,
140
+ entropy: Math.round(metrics.entropy * 100) / 100,
135
141
  };
136
142
 
137
143
  const existing = pendingHistory.files[relativePath];
@@ -180,6 +186,8 @@ export function captureSnapshots(
180
186
  cognitiveComplexity: number;
181
187
  maxNestingDepth: number;
182
188
  linesOfCode: number;
189
+ maxCyclomatic: number;
190
+ entropy: number;
183
191
  };
184
192
  }>,
185
193
  ): MetricsHistory {
@@ -196,6 +204,8 @@ export function captureSnapshots(
196
204
  cognitive: file.metrics.cognitiveComplexity,
197
205
  nesting: file.metrics.maxNestingDepth,
198
206
  lines: file.metrics.linesOfCode,
207
+ maxCyclomatic: file.metrics.maxCyclomatic,
208
+ entropy: Math.round(file.metrics.entropy * 100) / 100,
199
209
  };
200
210
 
201
211
  const existing = history.files[relativePath];
@@ -359,9 +369,11 @@ export interface ProjectTDI {
359
369
  filesAnalyzed: number;
360
370
  filesWithDebt: number;
361
371
  byCategory: {
362
- complexity: number;
363
- maintainability: number;
364
- nesting: number;
372
+ maintainability: number; // 45% - MI-based
373
+ cognitive: number; // 30%
374
+ nesting: number; // 10%
375
+ maxCyclomatic: number; // 10% - NEW
376
+ entropy: number; // 5% - NEW
365
377
  };
366
378
  }
367
379
 
@@ -379,7 +391,13 @@ export function computeTDI(history: MetricsHistory): ProjectTDI {
379
391
  totalCognitive: 0,
380
392
  filesAnalyzed: 0,
381
393
  filesWithDebt: 0,
382
- byCategory: { complexity: 0, maintainability: 0, nesting: 0 },
394
+ byCategory: {
395
+ maintainability: 0,
396
+ cognitive: 0,
397
+ nesting: 0,
398
+ maxCyclomatic: 0,
399
+ entropy: 0,
400
+ },
383
401
  };
384
402
  }
385
403
 
@@ -390,6 +408,8 @@ export function computeTDI(history: MetricsHistory): ProjectTDI {
390
408
  let debtFromMI = 0;
391
409
  let debtFromCognitive = 0;
392
410
  let debtFromNesting = 0;
411
+ let debtFromMaxCyclomatic = 0; // NEW
412
+ let debtFromEntropy = 0; // NEW
393
413
 
394
414
  for (const file of files) {
395
415
  const snap = file.latest;
@@ -412,8 +432,16 @@ export function computeTDI(history: MetricsHistory): ProjectTDI {
412
432
  const nestDebt = Math.min(1, Math.max(0, snap.nesting - 3) / 7);
413
433
  debtFromNesting += nestDebt;
414
434
 
415
- fileDebt = miDebt + cogDebt + nestDebt;
416
- if (fileDebt > 1) filesWithDebt++; // File has at least some debt
435
+ // Max Cyclomatic debt: 0 at max<=10, 1 at max>=30
436
+ const maxCycDebt = Math.min(1, Math.max(0, snap.maxCyclomatic - 10) / 20);
437
+ debtFromMaxCyclomatic += maxCycDebt;
438
+
439
+ // Entropy debt: 0 at entropy<=4.0, 1 at entropy>=7.0
440
+ const entropyDebt = Math.min(1, Math.max(0, snap.entropy - 4.0) / 3.0);
441
+ debtFromEntropy += entropyDebt;
442
+
443
+ fileDebt = miDebt + cogDebt + nestDebt + maxCycDebt + entropyDebt;
444
+ if (fileDebt > 0.5) filesWithDebt++; // Lowered threshold since we have more factors
417
445
  }
418
446
 
419
447
  const avgMI = totalMI / files.length;
@@ -422,9 +450,16 @@ export function computeTDI(history: MetricsHistory): ProjectTDI {
422
450
  const avgMIDebt = debtFromMI / files.length; // 0-1
423
451
  const avgCogDebt = debtFromCognitive / files.length; // 0-1
424
452
  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;
453
+ const avgMaxCycDebt = debtFromMaxCyclomatic / files.length; // NEW
454
+ const avgEntropyDebt = debtFromEntropy / files.length; // NEW
455
+
456
+ // Weighted: MI (45%), cognitive (30%), nesting (10%), maxCyc (10%), entropy (5%)
457
+ const rawScore =
458
+ avgMIDebt * 45 +
459
+ avgCogDebt * 30 +
460
+ avgNestDebt * 10 +
461
+ avgMaxCycDebt * 10 +
462
+ avgEntropyDebt * 5;
428
463
  const score = Math.round(rawScore * 100) / 100;
429
464
 
430
465
  // Grade
@@ -443,9 +478,11 @@ export function computeTDI(history: MetricsHistory): ProjectTDI {
443
478
  filesAnalyzed: files.length,
444
479
  filesWithDebt,
445
480
  byCategory: {
446
- complexity: Math.round(avgCogDebt * 100),
447
481
  maintainability: Math.round(avgMIDebt * 100),
482
+ cognitive: Math.round(avgCogDebt * 100),
448
483
  nesting: Math.round(avgNestDebt * 100),
484
+ maxCyclomatic: Math.round(avgMaxCycDebt * 100),
485
+ entropy: Math.round(avgEntropyDebt * 100),
449
486
  },
450
487
  };
451
488
  }