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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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: {
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
314
|
-
const
|
|
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
|
-
|
|
363
|
-
|
|
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: {
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
}
|