fcis 0.1.0 → 0.2.1
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/.plans/003-code-cleanup-consolidation.md +242 -0
- package/.plans/004-directory-depth-rollup.md +408 -0
- package/.plans/005-code-refinements.md +210 -0
- package/.plans/006-minor-refinements.md +149 -0
- package/.plans/007-compositional-function-scoring.md +514 -0
- package/README.md +214 -132
- package/TECHNICAL.md +125 -2
- package/dist/cli.js +599 -328
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +409 -240
- package/dist/index.js.map +1 -1
- package/docs/images/fc-is-submarine.webp +0 -0
- package/package.json +3 -1
- package/src/cli-utils.ts +201 -0
- package/src/cli.ts +105 -118
- package/src/detection/markers.ts +0 -222
- package/src/extraction/extract-functions.ts +106 -2
- package/src/extraction/extractor.ts +35 -74
- package/src/reporting/report-console.ts +188 -102
- package/src/reporting/report-json.ts +26 -3
- package/src/scoring/scorer.ts +425 -160
- package/src/types.ts +9 -2
- package/tests/classifier.test.ts +0 -1
- package/tests/cli.test.ts +356 -0
- package/tests/detect-markers.test.ts +1 -3
- package/tests/extractor.test.ts +95 -1
- package/tests/integration.test.ts +344 -0
- package/tests/report-console.test.ts +92 -0
- package/tests/scorer.test.ts +886 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SyntaxKind, Project } from 'ts-morph';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs2 from 'fs';
|
|
4
|
+
import * as jsonc from 'jsonc-parser';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
|
|
6
7
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -15,6 +16,31 @@ var DEFAULT_QUALITY_THRESHOLDS = {
|
|
|
15
16
|
okThreshold: 70,
|
|
16
17
|
reviewThreshold: 40
|
|
17
18
|
};
|
|
19
|
+
var INLINE_CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
20
|
+
// Array methods
|
|
21
|
+
"map",
|
|
22
|
+
"filter",
|
|
23
|
+
"reduce",
|
|
24
|
+
"forEach",
|
|
25
|
+
"find",
|
|
26
|
+
"findIndex",
|
|
27
|
+
"some",
|
|
28
|
+
"every",
|
|
29
|
+
"flatMap",
|
|
30
|
+
"sort",
|
|
31
|
+
"toSorted",
|
|
32
|
+
// Promise methods
|
|
33
|
+
"then",
|
|
34
|
+
"catch",
|
|
35
|
+
"finally",
|
|
36
|
+
// Timing
|
|
37
|
+
"setTimeout",
|
|
38
|
+
"setInterval",
|
|
39
|
+
// Events
|
|
40
|
+
"addEventListener",
|
|
41
|
+
"on",
|
|
42
|
+
"once"
|
|
43
|
+
]);
|
|
18
44
|
function extractFunctions(sourceFile) {
|
|
19
45
|
const functions = [];
|
|
20
46
|
const filePath = sourceFile.getFilePath();
|
|
@@ -102,25 +128,54 @@ function extractFunctions(sourceFile) {
|
|
|
102
128
|
if (node.getKind() === SyntaxKind.ArrowFunction) {
|
|
103
129
|
const arrowFn = node;
|
|
104
130
|
const parentContext = inferParentContext(arrowFn);
|
|
131
|
+
const isInlineCallback = isInlineCallbackToKnownHOF(parentContext);
|
|
132
|
+
const enclosingFunctionStartLine = isInlineCallback ? findEnclosingFunctionStartLine(arrowFn) : null;
|
|
105
133
|
addFunction(
|
|
106
|
-
extractFunctionData(
|
|
134
|
+
extractFunctionData(
|
|
135
|
+
arrowFn,
|
|
136
|
+
filePath,
|
|
137
|
+
"arrow",
|
|
138
|
+
parentContext,
|
|
139
|
+
false,
|
|
140
|
+
isInlineCallback,
|
|
141
|
+
enclosingFunctionStartLine
|
|
142
|
+
)
|
|
107
143
|
);
|
|
108
144
|
} else if (node.getKind() === SyntaxKind.FunctionExpression) {
|
|
109
145
|
const funcExpr = node;
|
|
110
146
|
const parentContext = inferParentContext(funcExpr);
|
|
147
|
+
const isInlineCallback = isInlineCallbackToKnownHOF(parentContext);
|
|
148
|
+
const enclosingFunctionStartLine = isInlineCallback ? findEnclosingFunctionStartLine(funcExpr) : null;
|
|
111
149
|
addFunction(
|
|
112
150
|
extractFunctionData(
|
|
113
151
|
funcExpr,
|
|
114
152
|
filePath,
|
|
115
153
|
"function-expression",
|
|
116
154
|
parentContext,
|
|
117
|
-
false
|
|
155
|
+
false,
|
|
156
|
+
isInlineCallback,
|
|
157
|
+
enclosingFunctionStartLine
|
|
118
158
|
)
|
|
119
159
|
);
|
|
120
160
|
}
|
|
121
161
|
});
|
|
122
162
|
return functions;
|
|
123
163
|
}
|
|
164
|
+
function isInlineCallbackToKnownHOF(parentContext) {
|
|
165
|
+
if (parentContext === null) return false;
|
|
166
|
+
return INLINE_CALLBACK_METHODS.has(parentContext);
|
|
167
|
+
}
|
|
168
|
+
function findEnclosingFunctionStartLine(node) {
|
|
169
|
+
let current = node.getParent();
|
|
170
|
+
while (current) {
|
|
171
|
+
const kind = current.getKind();
|
|
172
|
+
if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.MethodDeclaration || kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.GetAccessor || kind === SyntaxKind.SetAccessor) {
|
|
173
|
+
return current.getStartLineNumber();
|
|
174
|
+
}
|
|
175
|
+
current = current.getParent();
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
124
179
|
function inferParentContext(node) {
|
|
125
180
|
const parent = node.getParent();
|
|
126
181
|
if (!parent) return null;
|
|
@@ -145,7 +200,7 @@ function inferParentContext(node) {
|
|
|
145
200
|
}
|
|
146
201
|
return null;
|
|
147
202
|
}
|
|
148
|
-
function extractFunctionData(node, filePath, kind, parentContext = null, isExportedOverride) {
|
|
203
|
+
function extractFunctionData(node, filePath, kind, parentContext = null, isExportedOverride, isInlineCallback = false, enclosingFunctionStartLine = null) {
|
|
149
204
|
const startLine = node.getStartLineNumber();
|
|
150
205
|
const endLine = node.getEndLineNumber();
|
|
151
206
|
const bodyLineCount = endLine - startLine + 1;
|
|
@@ -181,7 +236,9 @@ function extractFunctionData(node, filePath, kind, parentContext = null, isExpor
|
|
|
181
236
|
callSites,
|
|
182
237
|
hasAwait,
|
|
183
238
|
propertyAccessChains,
|
|
184
|
-
kind
|
|
239
|
+
kind,
|
|
240
|
+
enclosingFunctionStartLine,
|
|
241
|
+
isInlineCallback
|
|
185
242
|
};
|
|
186
243
|
}
|
|
187
244
|
function getBodyNode(node) {
|
|
@@ -207,8 +264,12 @@ function isStatement(node) {
|
|
|
207
264
|
function checkForConditionals(body) {
|
|
208
265
|
if (!body) return false;
|
|
209
266
|
let hasConditionals = false;
|
|
210
|
-
body.forEachDescendant((node) => {
|
|
267
|
+
body.forEachDescendant((node, traversal) => {
|
|
211
268
|
const kind = node.getKind();
|
|
269
|
+
if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.MethodDeclaration) {
|
|
270
|
+
traversal.skip();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
212
273
|
if (kind === SyntaxKind.IfStatement || kind === SyntaxKind.ConditionalExpression || kind === SyntaxKind.SwitchStatement) {
|
|
213
274
|
hasConditionals = true;
|
|
214
275
|
}
|
|
@@ -338,73 +399,30 @@ function isTypeOnlyFile(sourceFile) {
|
|
|
338
399
|
}
|
|
339
400
|
return true;
|
|
340
401
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const nextChar = jsonString[i + 1];
|
|
350
|
-
if (!inLineComment && !inBlockComment && char === '"') {
|
|
351
|
-
let backslashCount = 0;
|
|
352
|
-
let j = i - 1;
|
|
353
|
-
while (j >= 0 && jsonString[j] === "\\") {
|
|
354
|
-
backslashCount++;
|
|
355
|
-
j--;
|
|
356
|
-
}
|
|
357
|
-
if (backslashCount % 2 === 0) {
|
|
358
|
-
inString = !inString;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (inString) {
|
|
362
|
-
result += char;
|
|
363
|
-
i++;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (!inBlockComment && char === "/" && nextChar === "/") {
|
|
367
|
-
inLineComment = true;
|
|
368
|
-
i += 2;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (inLineComment && (char === "\n" || char === "\r")) {
|
|
372
|
-
inLineComment = false;
|
|
373
|
-
result += char;
|
|
374
|
-
i++;
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
if (!inLineComment && char === "/" && nextChar === "*") {
|
|
378
|
-
inBlockComment = true;
|
|
379
|
-
i += 2;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (inBlockComment && char === "*" && nextChar === "/") {
|
|
383
|
-
inBlockComment = false;
|
|
384
|
-
i += 2;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (!inLineComment && !inBlockComment) {
|
|
388
|
-
result += char;
|
|
389
|
-
}
|
|
390
|
-
i++;
|
|
402
|
+
function validateTsconfigContent(content) {
|
|
403
|
+
const errors = [];
|
|
404
|
+
jsonc.parse(content, errors, { allowTrailingComma: true });
|
|
405
|
+
if (errors.length > 0 && errors[0]) {
|
|
406
|
+
return {
|
|
407
|
+
valid: false,
|
|
408
|
+
error: `Parse error at offset ${errors[0].offset}: ${jsonc.printParseErrorCode(errors[0].error)}`
|
|
409
|
+
};
|
|
391
410
|
}
|
|
392
|
-
return
|
|
411
|
+
return { valid: true };
|
|
393
412
|
}
|
|
394
413
|
function loadProject(options) {
|
|
395
414
|
const { tsconfigPath, filesGlob } = options;
|
|
396
415
|
const errors = [];
|
|
397
416
|
const absoluteTsconfigPath = path.resolve(tsconfigPath);
|
|
417
|
+
const projectDir = path.dirname(absoluteTsconfigPath);
|
|
398
418
|
if (!fs2.existsSync(absoluteTsconfigPath)) {
|
|
399
419
|
throw new Error(`tsconfig.json not found at: ${absoluteTsconfigPath}`);
|
|
400
420
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
JSON.parse(strippedContent);
|
|
405
|
-
} catch (e) {
|
|
421
|
+
const content = fs2.readFileSync(absoluteTsconfigPath, "utf-8");
|
|
422
|
+
const validationResult = validateTsconfigContent(content);
|
|
423
|
+
if (!validationResult.valid) {
|
|
406
424
|
throw new Error(
|
|
407
|
-
`Invalid tsconfig.json at ${absoluteTsconfigPath}: ${
|
|
425
|
+
`Invalid tsconfig.json at ${absoluteTsconfigPath}: ${validationResult.error}`
|
|
408
426
|
);
|
|
409
427
|
}
|
|
410
428
|
const project = new Project({
|
|
@@ -413,7 +431,7 @@ function loadProject(options) {
|
|
|
413
431
|
});
|
|
414
432
|
let sourceFiles = project.getSourceFiles().filter((sf) => {
|
|
415
433
|
const filePath = sf.getFilePath();
|
|
416
|
-
return filePath.endsWith(".ts") && !filePath.endsWith(".d.ts") && !filePath.endsWith(".test.ts") && !filePath.endsWith(".spec.ts") && !filePath.includes("/node_modules/") && !filePath.includes("/generated/");
|
|
434
|
+
return filePath.startsWith(projectDir + "/") && filePath.endsWith(".ts") && !filePath.endsWith(".d.ts") && !filePath.endsWith(".test.ts") && !filePath.endsWith(".spec.ts") && !filePath.includes("/node_modules/") && !filePath.includes("/generated/");
|
|
417
435
|
});
|
|
418
436
|
if (filesGlob) {
|
|
419
437
|
const globPattern = createGlobMatcher(filesGlob);
|
|
@@ -1015,6 +1033,122 @@ function getStatusColor(status) {
|
|
|
1015
1033
|
}
|
|
1016
1034
|
|
|
1017
1035
|
// src/scoring/scorer.ts
|
|
1036
|
+
function groupFunctionsByParent(functions) {
|
|
1037
|
+
const byStartLine = /* @__PURE__ */ new Map();
|
|
1038
|
+
for (const fn of functions) {
|
|
1039
|
+
byStartLine.set(fn.startLine, fn);
|
|
1040
|
+
}
|
|
1041
|
+
const topLevel = [];
|
|
1042
|
+
const childrenByParentLine = /* @__PURE__ */ new Map();
|
|
1043
|
+
for (const fn of functions) {
|
|
1044
|
+
const isTopLevel = !fn.isInlineCallback || fn.enclosingFunctionStartLine === null;
|
|
1045
|
+
if (isTopLevel) {
|
|
1046
|
+
topLevel.push({ fn, children: [] });
|
|
1047
|
+
} else if (fn.enclosingFunctionStartLine !== null) {
|
|
1048
|
+
const existing = childrenByParentLine.get(fn.enclosingFunctionStartLine);
|
|
1049
|
+
if (existing) {
|
|
1050
|
+
existing.push(fn);
|
|
1051
|
+
} else {
|
|
1052
|
+
childrenByParentLine.set(fn.enclosingFunctionStartLine, [fn]);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
for (const entry of topLevel) {
|
|
1057
|
+
const children = childrenByParentLine.get(entry.fn.startLine);
|
|
1058
|
+
if (children) {
|
|
1059
|
+
entry.children = children;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return topLevel;
|
|
1063
|
+
}
|
|
1064
|
+
function isEffectivelyImpure(entry) {
|
|
1065
|
+
if (entry.fn.classification === "impure") return true;
|
|
1066
|
+
return entry.children.some((child) => child.classification === "impure");
|
|
1067
|
+
}
|
|
1068
|
+
function getEffectiveStatus(entry) {
|
|
1069
|
+
const statuses = [entry.fn.status, ...entry.children.map((c) => c.status)];
|
|
1070
|
+
if (statuses.includes("refactor")) return "refactor";
|
|
1071
|
+
if (statuses.includes("review")) return "review";
|
|
1072
|
+
return "ok";
|
|
1073
|
+
}
|
|
1074
|
+
function computeComposedQuality(entry) {
|
|
1075
|
+
const impureChildren = entry.children.filter(
|
|
1076
|
+
(c) => c.classification === "impure"
|
|
1077
|
+
);
|
|
1078
|
+
if (impureChildren.length === 0) {
|
|
1079
|
+
return entry.fn.qualityScore ?? 50;
|
|
1080
|
+
}
|
|
1081
|
+
const childTotalLines = entry.children.reduce(
|
|
1082
|
+
(sum, c) => sum + c.bodyLineCount,
|
|
1083
|
+
0
|
|
1084
|
+
);
|
|
1085
|
+
const parentOwnLines = Math.max(1, entry.fn.bodyLineCount - childTotalLines);
|
|
1086
|
+
const totalLines = parentOwnLines + childTotalLines;
|
|
1087
|
+
const parentQuality = entry.fn.qualityScore ?? 50;
|
|
1088
|
+
const parentContribution = parentQuality * parentOwnLines;
|
|
1089
|
+
const childContribution = impureChildren.reduce(
|
|
1090
|
+
(sum, c) => sum + (c.qualityScore ?? 50) * c.bodyLineCount,
|
|
1091
|
+
0
|
|
1092
|
+
);
|
|
1093
|
+
return (parentContribution + childContribution) / totalLines;
|
|
1094
|
+
}
|
|
1095
|
+
function aggregateMetrics(items) {
|
|
1096
|
+
const scorableItems = items.filter(
|
|
1097
|
+
(item) => item.pureCount + item.impureCount > 0
|
|
1098
|
+
);
|
|
1099
|
+
const totalPure = items.reduce((sum, item) => sum + item.pureCount, 0);
|
|
1100
|
+
const totalImpure = items.reduce((sum, item) => sum + item.impureCount, 0);
|
|
1101
|
+
const total = totalPure + totalImpure;
|
|
1102
|
+
if (total === 0) {
|
|
1103
|
+
return {
|
|
1104
|
+
purity: 100,
|
|
1105
|
+
impurityQuality: null,
|
|
1106
|
+
health: 100,
|
|
1107
|
+
pureCount: 0,
|
|
1108
|
+
impureCount: 0,
|
|
1109
|
+
excludedCount: items.reduce((sum, item) => sum + item.excludedCount, 0),
|
|
1110
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
1111
|
+
pureLineCount: 0,
|
|
1112
|
+
impureLineCount: 0
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const purity = totalPure / total * 100;
|
|
1116
|
+
let impurityQuality = null;
|
|
1117
|
+
if (totalImpure > 0) {
|
|
1118
|
+
const weightedQuality = scorableItems.reduce((sum, item) => {
|
|
1119
|
+
if (item.impurityQuality !== null && item.impureCount > 0) {
|
|
1120
|
+
return sum + item.impurityQuality * item.impureCount;
|
|
1121
|
+
}
|
|
1122
|
+
return sum;
|
|
1123
|
+
}, 0);
|
|
1124
|
+
impurityQuality = weightedQuality / totalImpure;
|
|
1125
|
+
}
|
|
1126
|
+
const statusBreakdown = {
|
|
1127
|
+
ok: items.reduce((sum, item) => sum + item.statusBreakdown.ok, 0),
|
|
1128
|
+
review: items.reduce((sum, item) => sum + item.statusBreakdown.review, 0),
|
|
1129
|
+
refactor: items.reduce(
|
|
1130
|
+
(sum, item) => sum + item.statusBreakdown.refactor,
|
|
1131
|
+
0
|
|
1132
|
+
)
|
|
1133
|
+
};
|
|
1134
|
+
const health = statusBreakdown.ok / total * 100;
|
|
1135
|
+
const pureLineCount = items.reduce((sum, item) => sum + item.pureLineCount, 0);
|
|
1136
|
+
const impureLineCount = items.reduce(
|
|
1137
|
+
(sum, item) => sum + item.impureLineCount,
|
|
1138
|
+
0
|
|
1139
|
+
);
|
|
1140
|
+
return {
|
|
1141
|
+
purity,
|
|
1142
|
+
impurityQuality,
|
|
1143
|
+
health,
|
|
1144
|
+
pureCount: totalPure,
|
|
1145
|
+
impureCount: totalImpure,
|
|
1146
|
+
excludedCount: items.reduce((sum, item) => sum + item.excludedCount, 0),
|
|
1147
|
+
statusBreakdown,
|
|
1148
|
+
pureLineCount,
|
|
1149
|
+
impureLineCount
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1018
1152
|
function scoreFile(filePath, functions, isTypeOnly = false) {
|
|
1019
1153
|
if (isTypeOnly) {
|
|
1020
1154
|
return {
|
|
@@ -1033,12 +1167,8 @@ function scoreFile(filePath, functions, isTypeOnly = false) {
|
|
|
1033
1167
|
typeOnly: true
|
|
1034
1168
|
};
|
|
1035
1169
|
}
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
const pureCount = pureFunctions.length;
|
|
1039
|
-
const impureCount = impureFunctions.length;
|
|
1040
|
-
const total = pureCount + impureCount;
|
|
1041
|
-
if (total === 0) {
|
|
1170
|
+
const grouped = groupFunctionsByParent(functions);
|
|
1171
|
+
if (grouped.length === 0) {
|
|
1042
1172
|
return {
|
|
1043
1173
|
filePath,
|
|
1044
1174
|
purity: 100,
|
|
@@ -1055,36 +1185,49 @@ function scoreFile(filePath, functions, isTypeOnly = false) {
|
|
|
1055
1185
|
allExcluded: true
|
|
1056
1186
|
};
|
|
1057
1187
|
}
|
|
1058
|
-
|
|
1188
|
+
let pureCount = 0;
|
|
1189
|
+
let impureCount = 0;
|
|
1190
|
+
let pureLineCount = 0;
|
|
1191
|
+
let impureLineCount = 0;
|
|
1192
|
+
const statusBreakdown = { ok: 0, review: 0, refactor: 0 };
|
|
1193
|
+
for (const entry of grouped) {
|
|
1194
|
+
const effectivelyImpure = isEffectivelyImpure(entry);
|
|
1195
|
+
const effectiveStatus = getEffectiveStatus(entry);
|
|
1196
|
+
if (effectivelyImpure) {
|
|
1197
|
+
impureCount++;
|
|
1198
|
+
impureLineCount += entry.fn.bodyLineCount;
|
|
1199
|
+
} else {
|
|
1200
|
+
pureCount++;
|
|
1201
|
+
pureLineCount += entry.fn.bodyLineCount;
|
|
1202
|
+
}
|
|
1203
|
+
statusBreakdown[effectiveStatus]++;
|
|
1204
|
+
}
|
|
1205
|
+
const total = pureCount + impureCount;
|
|
1206
|
+
const purity = total > 0 ? pureCount / total * 100 : 100;
|
|
1059
1207
|
let impurityQuality = null;
|
|
1060
1208
|
if (impureCount > 0) {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1209
|
+
const impureEntries = grouped.filter((e) => isEffectivelyImpure(e));
|
|
1210
|
+
const totalQuality = impureEntries.reduce(
|
|
1211
|
+
(sum, e) => sum + computeComposedQuality(e),
|
|
1063
1212
|
0
|
|
1064
1213
|
);
|
|
1065
1214
|
impurityQuality = totalQuality / impureCount;
|
|
1066
1215
|
}
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
name: f.name,
|
|
1083
|
-
startLine: f.startLine,
|
|
1084
|
-
bodyLineCount: f.bodyLineCount,
|
|
1085
|
-
qualityScore: f.qualityScore ?? 0,
|
|
1086
|
-
markers: f.markers.map((m) => m.type)
|
|
1087
|
-
})).sort((a, b) => {
|
|
1216
|
+
const health = total > 0 ? statusBreakdown.ok / total * 100 : 100;
|
|
1217
|
+
const refactoringCandidates = grouped.filter((e) => isEffectivelyImpure(e) && getEffectiveStatus(e) === "refactor").map((e) => {
|
|
1218
|
+
const allMarkers = [
|
|
1219
|
+
...e.fn.markers.map((m) => m.type),
|
|
1220
|
+
...e.children.flatMap((c) => c.markers.map((m) => m.type))
|
|
1221
|
+
];
|
|
1222
|
+
const uniqueMarkers = [...new Set(allMarkers)];
|
|
1223
|
+
return {
|
|
1224
|
+
name: e.fn.name,
|
|
1225
|
+
startLine: e.fn.startLine,
|
|
1226
|
+
bodyLineCount: e.fn.bodyLineCount,
|
|
1227
|
+
qualityScore: e.fn.qualityScore ?? 0,
|
|
1228
|
+
markers: uniqueMarkers
|
|
1229
|
+
};
|
|
1230
|
+
}).sort((a, b) => {
|
|
1088
1231
|
const impactA = a.bodyLineCount * (100 - a.qualityScore);
|
|
1089
1232
|
const impactB = b.bodyLineCount * (100 - b.qualityScore);
|
|
1090
1233
|
return impactB - impactA;
|
|
@@ -1101,6 +1244,7 @@ function scoreFile(filePath, functions, isTypeOnly = false) {
|
|
|
1101
1244
|
pureLineCount,
|
|
1102
1245
|
impureLineCount,
|
|
1103
1246
|
functions,
|
|
1247
|
+
// Keep all functions for drill-down (includes isInlineCallback flag)
|
|
1104
1248
|
refactoringCandidates
|
|
1105
1249
|
};
|
|
1106
1250
|
}
|
|
@@ -1109,62 +1253,19 @@ function scoreDirectory(dirPath, fileScores) {
|
|
|
1109
1253
|
(f) => !f.typeOnly && !f.allExcluded && f.pureCount + f.impureCount > 0
|
|
1110
1254
|
);
|
|
1111
1255
|
if (scorableFiles.length === 0) {
|
|
1256
|
+
const emptyMetrics = aggregateMetrics([]);
|
|
1112
1257
|
return {
|
|
1113
1258
|
dirPath,
|
|
1114
|
-
|
|
1115
|
-
impurityQuality: null,
|
|
1116
|
-
health: 100,
|
|
1117
|
-
pureCount: 0,
|
|
1118
|
-
impureCount: 0,
|
|
1259
|
+
...emptyMetrics,
|
|
1119
1260
|
excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
|
|
1120
|
-
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
1121
|
-
pureLineCount: 0,
|
|
1122
|
-
impureLineCount: 0,
|
|
1123
1261
|
fileScores
|
|
1124
1262
|
};
|
|
1125
1263
|
}
|
|
1126
|
-
const
|
|
1127
|
-
const totalImpure = scorableFiles.reduce((sum, f) => sum + f.impureCount, 0);
|
|
1128
|
-
const total = totalPure + totalImpure;
|
|
1129
|
-
const purity = total > 0 ? totalPure / total * 100 : 100;
|
|
1130
|
-
let impurityQuality = null;
|
|
1131
|
-
if (totalImpure > 0) {
|
|
1132
|
-
const weightedQuality = scorableFiles.reduce((sum, f) => {
|
|
1133
|
-
if (f.impurityQuality !== null && f.impureCount > 0) {
|
|
1134
|
-
return sum + f.impurityQuality * f.impureCount;
|
|
1135
|
-
}
|
|
1136
|
-
return sum;
|
|
1137
|
-
}, 0);
|
|
1138
|
-
impurityQuality = weightedQuality / totalImpure;
|
|
1139
|
-
}
|
|
1140
|
-
const statusBreakdown = {
|
|
1141
|
-
ok: scorableFiles.reduce((sum, f) => sum + f.statusBreakdown.ok, 0),
|
|
1142
|
-
review: scorableFiles.reduce((sum, f) => sum + f.statusBreakdown.review, 0),
|
|
1143
|
-
refactor: scorableFiles.reduce(
|
|
1144
|
-
(sum, f) => sum + f.statusBreakdown.refactor,
|
|
1145
|
-
0
|
|
1146
|
-
)
|
|
1147
|
-
};
|
|
1148
|
-
const health = total > 0 ? statusBreakdown.ok / total * 100 : 100;
|
|
1149
|
-
const pureLineCount = scorableFiles.reduce(
|
|
1150
|
-
(sum, f) => sum + f.pureLineCount,
|
|
1151
|
-
0
|
|
1152
|
-
);
|
|
1153
|
-
const impureLineCount = scorableFiles.reduce(
|
|
1154
|
-
(sum, f) => sum + f.impureLineCount,
|
|
1155
|
-
0
|
|
1156
|
-
);
|
|
1264
|
+
const aggregated = aggregateMetrics(scorableFiles);
|
|
1157
1265
|
return {
|
|
1158
1266
|
dirPath,
|
|
1159
|
-
|
|
1160
|
-
impurityQuality,
|
|
1161
|
-
health,
|
|
1162
|
-
pureCount: totalPure,
|
|
1163
|
-
impureCount: totalImpure,
|
|
1267
|
+
...aggregated,
|
|
1164
1268
|
excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
|
|
1165
|
-
statusBreakdown,
|
|
1166
|
-
pureLineCount,
|
|
1167
|
-
impureLineCount,
|
|
1168
1269
|
fileScores
|
|
1169
1270
|
};
|
|
1170
1271
|
}
|
|
@@ -1180,19 +1281,13 @@ function scoreProject(directoryScores, options = {}) {
|
|
|
1180
1281
|
(d) => d.pureCount + d.impureCount > 0
|
|
1181
1282
|
);
|
|
1182
1283
|
if (scorableDirs.length === 0) {
|
|
1284
|
+
const emptyMetrics = aggregateMetrics([]);
|
|
1183
1285
|
return {
|
|
1184
|
-
|
|
1185
|
-
impurityQuality: null,
|
|
1186
|
-
health: 100,
|
|
1187
|
-
pureCount: 0,
|
|
1188
|
-
impureCount: 0,
|
|
1286
|
+
...emptyMetrics,
|
|
1189
1287
|
excludedCount: directoryScores.reduce(
|
|
1190
1288
|
(sum, d) => sum + d.excludedCount,
|
|
1191
1289
|
0
|
|
1192
1290
|
),
|
|
1193
|
-
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
1194
|
-
pureLineCount: 0,
|
|
1195
|
-
impureLineCount: 0,
|
|
1196
1291
|
directoryScores,
|
|
1197
1292
|
timestamp,
|
|
1198
1293
|
commitHash,
|
|
@@ -1202,37 +1297,7 @@ function scoreProject(directoryScores, options = {}) {
|
|
|
1202
1297
|
...errors.length > 0 && { errors }
|
|
1203
1298
|
};
|
|
1204
1299
|
}
|
|
1205
|
-
const
|
|
1206
|
-
const totalImpure = scorableDirs.reduce((sum, d) => sum + d.impureCount, 0);
|
|
1207
|
-
const total = totalPure + totalImpure;
|
|
1208
|
-
const purity = total > 0 ? totalPure / total * 100 : 100;
|
|
1209
|
-
let impurityQuality = null;
|
|
1210
|
-
if (totalImpure > 0) {
|
|
1211
|
-
const weightedQuality = scorableDirs.reduce((sum, d) => {
|
|
1212
|
-
if (d.impurityQuality !== null && d.impureCount > 0) {
|
|
1213
|
-
return sum + d.impurityQuality * d.impureCount;
|
|
1214
|
-
}
|
|
1215
|
-
return sum;
|
|
1216
|
-
}, 0);
|
|
1217
|
-
impurityQuality = weightedQuality / totalImpure;
|
|
1218
|
-
}
|
|
1219
|
-
const statusBreakdown = {
|
|
1220
|
-
ok: scorableDirs.reduce((sum, d) => sum + d.statusBreakdown.ok, 0),
|
|
1221
|
-
review: scorableDirs.reduce((sum, d) => sum + d.statusBreakdown.review, 0),
|
|
1222
|
-
refactor: scorableDirs.reduce(
|
|
1223
|
-
(sum, d) => sum + d.statusBreakdown.refactor,
|
|
1224
|
-
0
|
|
1225
|
-
)
|
|
1226
|
-
};
|
|
1227
|
-
const health = total > 0 ? statusBreakdown.ok / total * 100 : 100;
|
|
1228
|
-
const pureLineCount = scorableDirs.reduce(
|
|
1229
|
-
(sum, d) => sum + d.pureLineCount,
|
|
1230
|
-
0
|
|
1231
|
-
);
|
|
1232
|
-
const impureLineCount = scorableDirs.reduce(
|
|
1233
|
-
(sum, d) => sum + d.impureLineCount,
|
|
1234
|
-
0
|
|
1235
|
-
);
|
|
1300
|
+
const aggregated = aggregateMetrics(scorableDirs);
|
|
1236
1301
|
const allCandidates = [];
|
|
1237
1302
|
for (const dir of directoryScores) {
|
|
1238
1303
|
for (const file of dir.fileScores) {
|
|
@@ -1250,15 +1315,8 @@ function scoreProject(directoryScores, options = {}) {
|
|
|
1250
1315
|
return impactB - impactA;
|
|
1251
1316
|
});
|
|
1252
1317
|
const result = {
|
|
1253
|
-
|
|
1254
|
-
impurityQuality,
|
|
1255
|
-
health,
|
|
1256
|
-
pureCount: totalPure,
|
|
1257
|
-
impureCount: totalImpure,
|
|
1318
|
+
...aggregated,
|
|
1258
1319
|
excludedCount: directoryScores.reduce((sum, d) => sum + d.excludedCount, 0),
|
|
1259
|
-
statusBreakdown,
|
|
1260
|
-
pureLineCount,
|
|
1261
|
-
impureLineCount,
|
|
1262
1320
|
directoryScores,
|
|
1263
1321
|
timestamp,
|
|
1264
1322
|
commitHash,
|
|
@@ -1291,6 +1349,56 @@ function getDirectoryPath(filePath) {
|
|
|
1291
1349
|
if (lastSlash === -1) return ".";
|
|
1292
1350
|
return filePath.slice(0, lastSlash);
|
|
1293
1351
|
}
|
|
1352
|
+
function getPathAtDepth(relativePath, depth) {
|
|
1353
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
1354
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
1355
|
+
if (segments.length === 0) {
|
|
1356
|
+
return ".";
|
|
1357
|
+
}
|
|
1358
|
+
const segmentCount = depth + 1;
|
|
1359
|
+
if (segmentCount >= segments.length) {
|
|
1360
|
+
return segments.join("/");
|
|
1361
|
+
}
|
|
1362
|
+
return segments.slice(0, segmentCount).join("/");
|
|
1363
|
+
}
|
|
1364
|
+
function rollupDirectoriesByDepth(directories, depth) {
|
|
1365
|
+
if (directories.length === 0) {
|
|
1366
|
+
return [];
|
|
1367
|
+
}
|
|
1368
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1369
|
+
for (const dir of directories) {
|
|
1370
|
+
const truncatedPath = getPathAtDepth(dir.dirPath, depth);
|
|
1371
|
+
const existing = groups.get(truncatedPath);
|
|
1372
|
+
if (existing) {
|
|
1373
|
+
existing.push(dir);
|
|
1374
|
+
} else {
|
|
1375
|
+
groups.set(truncatedPath, [dir]);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const rolledUp = [];
|
|
1379
|
+
for (const [groupPath, groupDirs] of groups) {
|
|
1380
|
+
const scorableDirs = groupDirs.filter((d) => d.pureCount + d.impureCount > 0);
|
|
1381
|
+
if (scorableDirs.length === 0) {
|
|
1382
|
+
const emptyMetrics = aggregateMetrics([]);
|
|
1383
|
+
rolledUp.push({
|
|
1384
|
+
dirPath: groupPath,
|
|
1385
|
+
...emptyMetrics,
|
|
1386
|
+
excludedCount: groupDirs.reduce((sum, d) => sum + d.excludedCount, 0),
|
|
1387
|
+
fileScores: []
|
|
1388
|
+
});
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const aggregated = aggregateMetrics(scorableDirs);
|
|
1392
|
+
rolledUp.push({
|
|
1393
|
+
dirPath: groupPath,
|
|
1394
|
+
...aggregated,
|
|
1395
|
+
excludedCount: groupDirs.reduce((sum, d) => sum + d.excludedCount, 0),
|
|
1396
|
+
fileScores: []
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
rolledUp.sort((a, b) => a.dirPath.localeCompare(b.dirPath));
|
|
1400
|
+
return rolledUp;
|
|
1401
|
+
}
|
|
1294
1402
|
function calculateDelta(current, previous) {
|
|
1295
1403
|
return {
|
|
1296
1404
|
purityDelta: current.purity - previous.purity,
|
|
@@ -1478,12 +1586,6 @@ var MARKER_CATALOG = {
|
|
|
1478
1586
|
category: "io",
|
|
1479
1587
|
severity: "high"
|
|
1480
1588
|
},
|
|
1481
|
-
"fs-import": {
|
|
1482
|
-
type: "fs-import",
|
|
1483
|
-
description: "Import from file system module",
|
|
1484
|
-
category: "io",
|
|
1485
|
-
severity: "medium"
|
|
1486
|
-
},
|
|
1487
1589
|
"fs-call": {
|
|
1488
1590
|
type: "fs-call",
|
|
1489
1591
|
description: "File system operation call",
|
|
@@ -1534,8 +1636,24 @@ function getAllMarkerTypes() {
|
|
|
1534
1636
|
return Object.keys(MARKER_CATALOG);
|
|
1535
1637
|
}
|
|
1536
1638
|
function generateJsonReport(score, options = {}) {
|
|
1537
|
-
const {
|
|
1538
|
-
|
|
1639
|
+
const {
|
|
1640
|
+
pretty = true,
|
|
1641
|
+
includeFunction = true,
|
|
1642
|
+
dirDepth,
|
|
1643
|
+
projectRoot = process.cwd()
|
|
1644
|
+
} = options;
|
|
1645
|
+
let reportData = prepareReportData(score, includeFunction);
|
|
1646
|
+
if (dirDepth !== void 0) {
|
|
1647
|
+
const relativized = score.directoryScores.map((d) => ({
|
|
1648
|
+
...d,
|
|
1649
|
+
dirPath: path.relative(projectRoot, d.dirPath).replace(/\\/g, "/")
|
|
1650
|
+
}));
|
|
1651
|
+
const rolledUp = rollupDirectoriesByDepth(relativized, dirDepth);
|
|
1652
|
+
reportData = {
|
|
1653
|
+
...reportData,
|
|
1654
|
+
rolledUpDirectories: rolledUp
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1539
1657
|
if (pretty) {
|
|
1540
1658
|
return JSON.stringify(reportData, null, 2);
|
|
1541
1659
|
}
|
|
@@ -1619,8 +1737,79 @@ function generateComparisonReport(current, previous) {
|
|
|
1619
1737
|
resolvedRefactoringCandidates
|
|
1620
1738
|
};
|
|
1621
1739
|
}
|
|
1740
|
+
function getMetricColor(value) {
|
|
1741
|
+
if (value >= 70) return chalk.green;
|
|
1742
|
+
if (value >= 50) return chalk.yellow;
|
|
1743
|
+
return chalk.red;
|
|
1744
|
+
}
|
|
1745
|
+
function getMetricColorNullable(value) {
|
|
1746
|
+
if (value === null) return chalk.gray;
|
|
1747
|
+
return getMetricColor(value);
|
|
1748
|
+
}
|
|
1749
|
+
function toRelativePath(absolutePath) {
|
|
1750
|
+
const cwd = process.cwd();
|
|
1751
|
+
if (absolutePath.startsWith(cwd)) {
|
|
1752
|
+
const relative3 = path.relative(cwd, absolutePath);
|
|
1753
|
+
return relative3 || ".";
|
|
1754
|
+
}
|
|
1755
|
+
return absolutePath;
|
|
1756
|
+
}
|
|
1757
|
+
function relativizeDirectoryPaths(directories, projectRoot) {
|
|
1758
|
+
return directories.map((d) => ({
|
|
1759
|
+
...d,
|
|
1760
|
+
dirPath: path.relative(projectRoot, d.dirPath).replace(/\\/g, "/")
|
|
1761
|
+
}));
|
|
1762
|
+
}
|
|
1763
|
+
function printDirectoryTable(directories, options) {
|
|
1764
|
+
const { title, includeQualityColumn, sortBy, emptyMessage } = options;
|
|
1765
|
+
let filtered = directories.filter((d) => d.pureCount + d.impureCount > 0);
|
|
1766
|
+
if (sortBy === "health-asc") {
|
|
1767
|
+
filtered = [...filtered].sort((a, b) => a.health - b.health);
|
|
1768
|
+
}
|
|
1769
|
+
console.log(chalk.bold(title));
|
|
1770
|
+
console.log(chalk.gray("\u2500".repeat(80)));
|
|
1771
|
+
if (includeQualityColumn) {
|
|
1772
|
+
console.log(
|
|
1773
|
+
chalk.gray(
|
|
1774
|
+
padEnd("Directory", 40) + padEnd("Health", 10) + padEnd("Purity", 10) + padEnd("Quality", 10) + padEnd("Functions", 10)
|
|
1775
|
+
)
|
|
1776
|
+
);
|
|
1777
|
+
} else {
|
|
1778
|
+
console.log(
|
|
1779
|
+
chalk.gray(
|
|
1780
|
+
padEnd("Directory", 45) + padEnd("Health", 10) + padEnd("Purity", 10) + padEnd("Functions", 15)
|
|
1781
|
+
)
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
console.log(chalk.gray("\u2500".repeat(80)));
|
|
1785
|
+
if (filtered.length === 0) {
|
|
1786
|
+
if (emptyMessage) {
|
|
1787
|
+
console.log(chalk.gray(` ${emptyMessage}`));
|
|
1788
|
+
}
|
|
1789
|
+
console.log();
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
for (const dir of filtered) {
|
|
1793
|
+
const healthColor = getMetricColor(dir.health);
|
|
1794
|
+
const purityColor = getMetricColor(dir.purity);
|
|
1795
|
+
const total = dir.pureCount + dir.impureCount;
|
|
1796
|
+
if (includeQualityColumn) {
|
|
1797
|
+
const qualityStr = dir.impurityQuality !== null ? dir.impurityQuality.toFixed(0) + "%" : "\u2014";
|
|
1798
|
+
const qualityColor = getMetricColorNullable(dir.impurityQuality);
|
|
1799
|
+
console.log(
|
|
1800
|
+
padEnd(dir.dirPath, 40) + healthColor(padEnd(dir.health.toFixed(0) + "%", 10)) + purityColor(padEnd(dir.purity.toFixed(0) + "%", 10)) + qualityColor(padEnd(qualityStr, 10)) + padEnd(`${dir.pureCount}/${total}`, 10)
|
|
1801
|
+
);
|
|
1802
|
+
} else {
|
|
1803
|
+
const relativePath = toRelativePath(dir.dirPath);
|
|
1804
|
+
console.log(
|
|
1805
|
+
padEnd(relativePath, 45) + healthColor(padEnd(dir.health.toFixed(0) + "%", 10)) + purityColor(padEnd(dir.purity.toFixed(0) + "%", 10)) + padEnd(`${dir.pureCount}/${total}`, 15)
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
console.log();
|
|
1810
|
+
}
|
|
1622
1811
|
function printConsoleReport(score, options = {}) {
|
|
1623
|
-
const { verbose = false } = options;
|
|
1812
|
+
const { verbose = false, dirDepth } = options;
|
|
1624
1813
|
printHeader();
|
|
1625
1814
|
printProjectSummary(score);
|
|
1626
1815
|
printStatusBreakdown(
|
|
@@ -1628,8 +1817,24 @@ function printConsoleReport(score, options = {}) {
|
|
|
1628
1817
|
score.pureCount + score.impureCount
|
|
1629
1818
|
);
|
|
1630
1819
|
printInsights(score);
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1820
|
+
if (dirDepth !== void 0) {
|
|
1821
|
+
const relativized = relativizeDirectoryPaths(
|
|
1822
|
+
score.directoryScores,
|
|
1823
|
+
process.cwd()
|
|
1824
|
+
);
|
|
1825
|
+
const rolledUp = rollupDirectoriesByDepth(relativized, dirDepth);
|
|
1826
|
+
printDirectoryTable(rolledUp, {
|
|
1827
|
+
title: `Directory Breakdown (depth=${dirDepth}):`,
|
|
1828
|
+
includeQualityColumn: true,
|
|
1829
|
+
sortBy: "preserve-order",
|
|
1830
|
+
emptyMessage: "No directories with functions found"
|
|
1831
|
+
});
|
|
1832
|
+
} else if (verbose) {
|
|
1833
|
+
printDirectoryTable(score.directoryScores, {
|
|
1834
|
+
title: "Directory Breakdown:",
|
|
1835
|
+
includeQualityColumn: false,
|
|
1836
|
+
sortBy: "health-asc"
|
|
1837
|
+
});
|
|
1633
1838
|
} else {
|
|
1634
1839
|
printWorstDirectories(score.directoryScores);
|
|
1635
1840
|
}
|
|
@@ -1649,8 +1854,8 @@ function printHeader() {
|
|
|
1649
1854
|
}
|
|
1650
1855
|
function printProjectSummary(score) {
|
|
1651
1856
|
const healthBar = createProgressBar(score.health, 25);
|
|
1652
|
-
const healthColor = score.health
|
|
1653
|
-
const purityColor = score.purity
|
|
1857
|
+
const healthColor = getMetricColor(score.health);
|
|
1858
|
+
const purityColor = getMetricColor(score.purity);
|
|
1654
1859
|
console.log(
|
|
1655
1860
|
`Project Health: ${healthColor(score.health.toFixed(0) + "%")} ${healthBar}`
|
|
1656
1861
|
);
|
|
@@ -1658,7 +1863,7 @@ function printProjectSummary(score) {
|
|
|
1658
1863
|
` Purity: ${purityColor(score.purity.toFixed(0) + "%")} (${score.pureCount} pure / ${score.pureCount + score.impureCount} total)`
|
|
1659
1864
|
);
|
|
1660
1865
|
if (score.impurityQuality !== null) {
|
|
1661
|
-
const qualityColor = score.impurityQuality
|
|
1866
|
+
const qualityColor = getMetricColor(score.impurityQuality);
|
|
1662
1867
|
console.log(
|
|
1663
1868
|
` Impurity Quality: ${qualityColor(score.impurityQuality.toFixed(0) + "%")} average`
|
|
1664
1869
|
);
|
|
@@ -1691,34 +1896,12 @@ function printInsights(score) {
|
|
|
1691
1896
|
console.log();
|
|
1692
1897
|
}
|
|
1693
1898
|
}
|
|
1694
|
-
function printDirectoryBreakdown(directories) {
|
|
1695
|
-
const sorted = [...directories].filter((d) => d.pureCount + d.impureCount > 0).sort((a, b) => a.health - b.health);
|
|
1696
|
-
if (sorted.length === 0) return;
|
|
1697
|
-
console.log(chalk.bold("Directory Breakdown:"));
|
|
1698
|
-
console.log(chalk.gray("\u2500".repeat(80)));
|
|
1699
|
-
console.log(
|
|
1700
|
-
chalk.gray(
|
|
1701
|
-
padEnd("Directory", 45) + padEnd("Health", 10) + padEnd("Purity", 10) + padEnd("Functions", 15)
|
|
1702
|
-
)
|
|
1703
|
-
);
|
|
1704
|
-
console.log(chalk.gray("\u2500".repeat(80)));
|
|
1705
|
-
for (const dir of sorted) {
|
|
1706
|
-
const healthColor = dir.health >= 70 ? chalk.green : dir.health >= 50 ? chalk.yellow : chalk.red;
|
|
1707
|
-
const purityColor = dir.purity >= 70 ? chalk.green : dir.purity >= 50 ? chalk.yellow : chalk.red;
|
|
1708
|
-
const relativePath = toRelativePath(dir.dirPath);
|
|
1709
|
-
const total = dir.pureCount + dir.impureCount;
|
|
1710
|
-
console.log(
|
|
1711
|
-
padEnd(relativePath, 45) + healthColor(padEnd(dir.health.toFixed(0) + "%", 10)) + purityColor(padEnd(dir.purity.toFixed(0) + "%", 10)) + padEnd(`${dir.pureCount}/${total}`, 15)
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
console.log();
|
|
1715
|
-
}
|
|
1716
1899
|
function printWorstDirectories(directories) {
|
|
1717
1900
|
const sorted = [...directories].filter((d) => d.pureCount + d.impureCount > 0 && d.health < 70).sort((a, b) => a.health - b.health).slice(0, 5);
|
|
1718
1901
|
if (sorted.length === 0) return;
|
|
1719
1902
|
console.log(chalk.bold("Directories Needing Attention:"));
|
|
1720
1903
|
for (const dir of sorted) {
|
|
1721
|
-
const healthColor = dir.health
|
|
1904
|
+
const healthColor = getMetricColor(dir.health);
|
|
1722
1905
|
const total = dir.pureCount + dir.impureCount;
|
|
1723
1906
|
const relativePath = toRelativePath(dir.dirPath);
|
|
1724
1907
|
console.log(
|
|
@@ -1784,13 +1967,7 @@ function createProgressBar(percent, width) {
|
|
|
1784
1967
|
const filledChar = "\u2588";
|
|
1785
1968
|
const emptyChar = "\u2591";
|
|
1786
1969
|
const bar = filledChar.repeat(filled) + emptyChar.repeat(empty);
|
|
1787
|
-
|
|
1788
|
-
return chalk.green(bar);
|
|
1789
|
-
} else if (percent >= 50) {
|
|
1790
|
-
return chalk.yellow(bar);
|
|
1791
|
-
} else {
|
|
1792
|
-
return chalk.red(bar);
|
|
1793
|
-
}
|
|
1970
|
+
return getMetricColor(percent)(bar);
|
|
1794
1971
|
}
|
|
1795
1972
|
function generateSummaryLine(score) {
|
|
1796
1973
|
const parts = [
|
|
@@ -1802,20 +1979,12 @@ function generateSummaryLine(score) {
|
|
|
1802
1979
|
}
|
|
1803
1980
|
return parts.join(" | ");
|
|
1804
1981
|
}
|
|
1805
|
-
function toRelativePath(absolutePath) {
|
|
1806
|
-
const cwd = process.cwd();
|
|
1807
|
-
if (absolutePath.startsWith(cwd)) {
|
|
1808
|
-
const relative2 = path.relative(cwd, absolutePath);
|
|
1809
|
-
return relative2 || ".";
|
|
1810
|
-
}
|
|
1811
|
-
return absolutePath;
|
|
1812
|
-
}
|
|
1813
1982
|
function padEnd(str, length) {
|
|
1814
1983
|
if (str.length >= length) return str.slice(0, length);
|
|
1815
1984
|
return str + " ".repeat(length - str.length);
|
|
1816
1985
|
}
|
|
1817
1986
|
function printFileReport(fileScore) {
|
|
1818
|
-
const healthColor = fileScore.health
|
|
1987
|
+
const healthColor = getMetricColor(fileScore.health);
|
|
1819
1988
|
const total = fileScore.pureCount + fileScore.impureCount;
|
|
1820
1989
|
const relativePath = toRelativePath(fileScore.filePath);
|
|
1821
1990
|
console.log();
|