fcis 0.1.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 (151) hide show
  1. package/.plans/001-fcis-analyzer.md +832 -0
  2. package/.plans/002-fcis-analyzer-improvements.md +205 -0
  3. package/README.md +272 -0
  4. package/TECHNICAL.md +386 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +1836 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +709 -0
  9. package/dist/index.js +1845 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +47 -0
  12. package/pnpm-workspace.yaml +0 -0
  13. package/src/analyzer.ts +266 -0
  14. package/src/classification/classifier.ts +156 -0
  15. package/src/classification/derive-status.ts +171 -0
  16. package/src/classification/quality-scorer.ts +481 -0
  17. package/src/cli.ts +286 -0
  18. package/src/detection/detect-markers.ts +480 -0
  19. package/src/detection/markers.ts +332 -0
  20. package/src/extraction/extract-functions.ts +570 -0
  21. package/src/extraction/extractor.ts +188 -0
  22. package/src/index.ts +111 -0
  23. package/src/reporting/report-console.ts +416 -0
  24. package/src/reporting/report-json.ts +232 -0
  25. package/src/scoring/scorer.ts +504 -0
  26. package/src/types.ts +248 -0
  27. package/tests/classifier.test.ts +480 -0
  28. package/tests/derive-status.test.ts +464 -0
  29. package/tests/detect-markers.test.ts +639 -0
  30. package/tests/extractor.test.ts +155 -0
  31. package/tests/integration.test.ts +706 -0
  32. package/tests/quality-scorer.test.ts +650 -0
  33. package/tests/scorer.test.ts +768 -0
  34. package/tsconfig.json +34 -0
  35. package/tsup.config.ts +17 -0
  36. package/vendor/ts-morph/.editorconfig +10 -0
  37. package/vendor/ts-morph/.gitattributes +11 -0
  38. package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
  39. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  40. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
  41. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  42. package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
  43. package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
  44. package/vendor/ts-morph/.vscode/settings.json +10 -0
  45. package/vendor/ts-morph/CONTRIBUTING.md +23 -0
  46. package/vendor/ts-morph/DEVELOPMENT.md +32 -0
  47. package/vendor/ts-morph/LICENSE +21 -0
  48. package/vendor/ts-morph/deno.json +8 -0
  49. package/vendor/ts-morph/deno.lock +1233 -0
  50. package/vendor/ts-morph/docs/CNAME +1 -0
  51. package/vendor/ts-morph/docs/Gemfile +2 -0
  52. package/vendor/ts-morph/docs/_config.yml +5 -0
  53. package/vendor/ts-morph/docs/_layouts/default.html +159 -0
  54. package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
  55. package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
  56. package/vendor/ts-morph/docs/details/ambient.md +38 -0
  57. package/vendor/ts-morph/docs/details/async.md +31 -0
  58. package/vendor/ts-morph/docs/details/classes.md +314 -0
  59. package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
  60. package/vendor/ts-morph/docs/details/comments.md +122 -0
  61. package/vendor/ts-morph/docs/details/decorators.md +119 -0
  62. package/vendor/ts-morph/docs/details/documentation.md +73 -0
  63. package/vendor/ts-morph/docs/details/enums.md +117 -0
  64. package/vendor/ts-morph/docs/details/exports.md +308 -0
  65. package/vendor/ts-morph/docs/details/expressions.md +46 -0
  66. package/vendor/ts-morph/docs/details/functions.md +150 -0
  67. package/vendor/ts-morph/docs/details/generators.md +27 -0
  68. package/vendor/ts-morph/docs/details/identifiers.md +79 -0
  69. package/vendor/ts-morph/docs/details/imports.md +191 -0
  70. package/vendor/ts-morph/docs/details/index.md +52 -0
  71. package/vendor/ts-morph/docs/details/initializers.md +40 -0
  72. package/vendor/ts-morph/docs/details/interfaces.md +218 -0
  73. package/vendor/ts-morph/docs/details/literals.md +20 -0
  74. package/vendor/ts-morph/docs/details/modifiers.md +38 -0
  75. package/vendor/ts-morph/docs/details/modules.md +113 -0
  76. package/vendor/ts-morph/docs/details/namespaces.md +7 -0
  77. package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
  78. package/vendor/ts-morph/docs/details/parameters.md +64 -0
  79. package/vendor/ts-morph/docs/details/signatures.md +41 -0
  80. package/vendor/ts-morph/docs/details/source-files.md +292 -0
  81. package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
  82. package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
  83. package/vendor/ts-morph/docs/details/types.md +254 -0
  84. package/vendor/ts-morph/docs/details/variables.md +110 -0
  85. package/vendor/ts-morph/docs/emitting.md +151 -0
  86. package/vendor/ts-morph/docs/index.md +25 -0
  87. package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
  88. package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
  89. package/vendor/ts-morph/docs/manipulation/index.md +136 -0
  90. package/vendor/ts-morph/docs/manipulation/order.md +14 -0
  91. package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
  92. package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
  93. package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
  94. package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
  95. package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
  96. package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
  97. package/vendor/ts-morph/docs/metrics/performance.json +4 -0
  98. package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
  99. package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
  100. package/vendor/ts-morph/docs/navigation/directories.md +287 -0
  101. package/vendor/ts-morph/docs/navigation/example.md +50 -0
  102. package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
  103. package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
  104. package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
  105. package/vendor/ts-morph/docs/navigation/index.md +94 -0
  106. package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
  107. package/vendor/ts-morph/docs/navigation/program.md +25 -0
  108. package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
  109. package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
  110. package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
  111. package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
  112. package/vendor/ts-morph/docs/setup/file-system.md +106 -0
  113. package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
  114. package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
  115. package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
  116. package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
  117. package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
  118. package/vendor/ts-morph/docs/setup/index.md +94 -0
  119. package/vendor/ts-morph/docs/utilities.md +55 -0
  120. package/vendor/ts-morph/dprint.json +23 -0
  121. package/vendor/ts-morph/package.json +30 -0
  122. package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
  123. package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
  124. package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
  125. package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
  126. package/vendor/ts-morph/packages/common/LICENSE +21 -0
  127. package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
  128. package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
  129. package/vendor/ts-morph/packages/common/package.json +65 -0
  130. package/vendor/ts-morph/packages/common/readme.md +5 -0
  131. package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
  132. package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
  133. package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
  134. package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
  135. package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
  136. package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
  137. package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
  138. package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
  139. package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
  140. package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
  141. package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
  142. package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
  143. package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
  144. package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
  145. package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
  146. package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
  147. package/vendor/ts-morph/readme.md +14 -0
  148. package/vendor/ts-morph/rfcs/README.md +13 -0
  149. package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
  150. package/vendor/ts-morph/tsconfig.common.json +17 -0
  151. package/vitest.config.ts +16 -0
package/dist/index.js ADDED
@@ -0,0 +1,1845 @@
1
+ import { SyntaxKind, Project } from 'ts-morph';
2
+ import * as path from 'path';
3
+ import * as fs2 from 'fs';
4
+ import chalk from 'chalk';
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+
13
+ // src/types.ts
14
+ var DEFAULT_QUALITY_THRESHOLDS = {
15
+ okThreshold: 70,
16
+ reviewThreshold: 40
17
+ };
18
+ function extractFunctions(sourceFile) {
19
+ const functions = [];
20
+ const filePath = sourceFile.getFilePath();
21
+ const addedPositions = /* @__PURE__ */ new Set();
22
+ const addFunction = (fn) => {
23
+ const posKey = `${fn.startLine}:${fn.endLine}`;
24
+ if (!addedPositions.has(posKey)) {
25
+ addedPositions.add(posKey);
26
+ functions.push(fn);
27
+ }
28
+ };
29
+ for (const func of sourceFile.getFunctions()) {
30
+ addFunction(extractFunctionData(func, filePath, "function"));
31
+ }
32
+ for (const classDecl of sourceFile.getClasses()) {
33
+ const className = classDecl.getName() ?? null;
34
+ for (const method of classDecl.getMethods()) {
35
+ addFunction(extractFunctionData(method, filePath, "method", className));
36
+ }
37
+ for (const getter of classDecl.getGetAccessors()) {
38
+ addFunction(extractFunctionData(getter, filePath, "getter", className));
39
+ }
40
+ for (const setter of classDecl.getSetAccessors()) {
41
+ addFunction(extractFunctionData(setter, filePath, "setter", className));
42
+ }
43
+ }
44
+ for (const varStatement of sourceFile.getVariableStatements()) {
45
+ const isExported = varStatement.isExported();
46
+ for (const decl of varStatement.getDeclarations()) {
47
+ const initializer = decl.getInitializer();
48
+ const varName = decl.getName();
49
+ if (initializer) {
50
+ if (initializer.getKind() === SyntaxKind.ArrowFunction) {
51
+ addFunction(
52
+ extractFunctionData(
53
+ initializer,
54
+ filePath,
55
+ "arrow",
56
+ varName,
57
+ isExported
58
+ )
59
+ );
60
+ } else if (initializer.getKind() === SyntaxKind.FunctionExpression) {
61
+ addFunction(
62
+ extractFunctionData(
63
+ initializer,
64
+ filePath,
65
+ "function-expression",
66
+ varName,
67
+ isExported
68
+ )
69
+ );
70
+ }
71
+ }
72
+ }
73
+ }
74
+ const defaultExport = sourceFile.getDefaultExportSymbol();
75
+ if (defaultExport) {
76
+ const declarations = defaultExport.getDeclarations();
77
+ for (const decl of declarations) {
78
+ if (decl.getKind() === SyntaxKind.ArrowFunction) {
79
+ addFunction(
80
+ extractFunctionData(
81
+ decl,
82
+ filePath,
83
+ "arrow",
84
+ "default",
85
+ true
86
+ )
87
+ );
88
+ } else if (decl.getKind() === SyntaxKind.FunctionExpression) {
89
+ addFunction(
90
+ extractFunctionData(
91
+ decl,
92
+ filePath,
93
+ "function-expression",
94
+ "default",
95
+ true
96
+ )
97
+ );
98
+ }
99
+ }
100
+ }
101
+ sourceFile.forEachDescendant((node) => {
102
+ if (node.getKind() === SyntaxKind.ArrowFunction) {
103
+ const arrowFn = node;
104
+ const parentContext = inferParentContext(arrowFn);
105
+ addFunction(
106
+ extractFunctionData(arrowFn, filePath, "arrow", parentContext, false)
107
+ );
108
+ } else if (node.getKind() === SyntaxKind.FunctionExpression) {
109
+ const funcExpr = node;
110
+ const parentContext = inferParentContext(funcExpr);
111
+ addFunction(
112
+ extractFunctionData(
113
+ funcExpr,
114
+ filePath,
115
+ "function-expression",
116
+ parentContext,
117
+ false
118
+ )
119
+ );
120
+ }
121
+ });
122
+ return functions;
123
+ }
124
+ function inferParentContext(node) {
125
+ const parent = node.getParent();
126
+ if (!parent) return null;
127
+ if (parent.getKind() === SyntaxKind.CallExpression) {
128
+ const callExpr = parent;
129
+ const expression = callExpr.getExpression();
130
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
131
+ const propAccess = expression;
132
+ return propAccess.getName();
133
+ }
134
+ if (expression.getKind() === SyntaxKind.Identifier) {
135
+ return expression.getText();
136
+ }
137
+ }
138
+ if (parent.getKind() === SyntaxKind.PropertyAssignment) {
139
+ const propAssignment = parent;
140
+ return propAssignment.getName();
141
+ }
142
+ if (parent.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
143
+ const shorthand = parent;
144
+ return shorthand.getName();
145
+ }
146
+ return null;
147
+ }
148
+ function extractFunctionData(node, filePath, kind, parentContext = null, isExportedOverride) {
149
+ const startLine = node.getStartLineNumber();
150
+ const endLine = node.getEndLineNumber();
151
+ const bodyLineCount = endLine - startLine + 1;
152
+ let name = null;
153
+ if ("getName" in node && typeof node.getName === "function") {
154
+ name = node.getName() ?? null;
155
+ }
156
+ if (name === null && parentContext !== null) {
157
+ name = parentContext;
158
+ }
159
+ const isAsync = "isAsync" in node ? node.isAsync?.() ?? false : false;
160
+ let isExported = isExportedOverride ?? false;
161
+ if (!isExported && "isExported" in node) {
162
+ isExported = node.isExported?.() ?? false;
163
+ }
164
+ const body = getBodyNode(node);
165
+ const statementCount = countStatements(body);
166
+ const hasConditionals = checkForConditionals(body);
167
+ const callSites = extractCallSites(node, startLine);
168
+ const hasAwait = callSites.some((cs) => cs.isAwaited) || checkForAwait(node);
169
+ const propertyAccessChains = extractPropertyAccessChains(node);
170
+ return {
171
+ name,
172
+ filePath,
173
+ startLine,
174
+ endLine,
175
+ isAsync,
176
+ isExported,
177
+ bodyLineCount,
178
+ statementCount,
179
+ hasConditionals,
180
+ parentContext,
181
+ callSites,
182
+ hasAwait,
183
+ propertyAccessChains,
184
+ kind
185
+ };
186
+ }
187
+ function getBodyNode(node) {
188
+ if ("getBody" in node) {
189
+ return node.getBody();
190
+ }
191
+ return void 0;
192
+ }
193
+ function countStatements(body) {
194
+ if (!body) return 0;
195
+ if (body.getKind() === SyntaxKind.Block) {
196
+ const statements = body.getChildrenOfKind(SyntaxKind.SyntaxList);
197
+ if (statements.length > 0 && statements[0]) {
198
+ return statements[0].getChildren().filter(isStatement).length;
199
+ }
200
+ }
201
+ return 1;
202
+ }
203
+ function isStatement(node) {
204
+ const kind = node.getKind();
205
+ return kind === SyntaxKind.VariableStatement || kind === SyntaxKind.ExpressionStatement || kind === SyntaxKind.ReturnStatement || kind === SyntaxKind.IfStatement || kind === SyntaxKind.ForStatement || kind === SyntaxKind.ForInStatement || kind === SyntaxKind.ForOfStatement || kind === SyntaxKind.WhileStatement || kind === SyntaxKind.DoStatement || kind === SyntaxKind.SwitchStatement || kind === SyntaxKind.TryStatement || kind === SyntaxKind.ThrowStatement || kind === SyntaxKind.BreakStatement || kind === SyntaxKind.ContinueStatement;
206
+ }
207
+ function checkForConditionals(body) {
208
+ if (!body) return false;
209
+ let hasConditionals = false;
210
+ body.forEachDescendant((node) => {
211
+ const kind = node.getKind();
212
+ if (kind === SyntaxKind.IfStatement || kind === SyntaxKind.ConditionalExpression || kind === SyntaxKind.SwitchStatement) {
213
+ hasConditionals = true;
214
+ }
215
+ });
216
+ return hasConditionals;
217
+ }
218
+ function checkForAwait(node) {
219
+ let hasAwait = false;
220
+ node.forEachDescendant((descendant, traversal) => {
221
+ const kind = descendant.getKind();
222
+ if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.MethodDeclaration) {
223
+ traversal.skip();
224
+ return;
225
+ }
226
+ if (kind === SyntaxKind.AwaitExpression) {
227
+ hasAwait = true;
228
+ }
229
+ });
230
+ return hasAwait;
231
+ }
232
+ function extractCallSites(node, functionStartLine) {
233
+ const callSites = [];
234
+ node.forEachDescendant((descendant, traversal) => {
235
+ const kind = descendant.getKind();
236
+ if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.MethodDeclaration) {
237
+ traversal.skip();
238
+ return;
239
+ }
240
+ if (kind === SyntaxKind.CallExpression) {
241
+ const callExpr = descendant;
242
+ const expression = getCallExpressionText(callExpr);
243
+ const line = callExpr.getStartLineNumber() - functionStartLine;
244
+ const parent = callExpr.getParent();
245
+ const isAwaited = parent?.getKind() === SyntaxKind.AwaitExpression;
246
+ callSites.push({
247
+ expression,
248
+ line,
249
+ isAwaited
250
+ });
251
+ }
252
+ });
253
+ return callSites;
254
+ }
255
+ function getCallExpressionText(callExpr) {
256
+ const expression = callExpr.getExpression();
257
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
258
+ return getPropertyAccessChainText(expression);
259
+ }
260
+ return expression.getText();
261
+ }
262
+ function getPropertyAccessChainText(node) {
263
+ const parts = [];
264
+ let current = node;
265
+ while (current.getKind() === SyntaxKind.PropertyAccessExpression) {
266
+ const propAccess = current;
267
+ parts.unshift(propAccess.getName());
268
+ current = propAccess.getExpression();
269
+ }
270
+ parts.unshift(current.getText());
271
+ return parts.join(".");
272
+ }
273
+ function extractPropertyAccessChains(node) {
274
+ const chains = /* @__PURE__ */ new Set();
275
+ node.forEachDescendant((descendant, traversal) => {
276
+ const kind = descendant.getKind();
277
+ if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.MethodDeclaration) {
278
+ traversal.skip();
279
+ return;
280
+ }
281
+ if (kind === SyntaxKind.PropertyAccessExpression) {
282
+ const propAccess = descendant;
283
+ const parent = propAccess.getParent();
284
+ if (parent?.getKind() !== SyntaxKind.PropertyAccessExpression) {
285
+ const chain = getPropertyAccessChainText(propAccess);
286
+ if (chain.includes(".")) {
287
+ chains.add(chain);
288
+ }
289
+ }
290
+ }
291
+ });
292
+ return Array.from(chains);
293
+ }
294
+ function extractImports(sourceFile) {
295
+ const filePath = sourceFile.getFilePath();
296
+ const imports = [];
297
+ for (const importDecl of sourceFile.getImportDeclarations()) {
298
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
299
+ const namedImports = [];
300
+ for (const namedImport of importDecl.getNamedImports()) {
301
+ namedImports.push(namedImport.getName());
302
+ }
303
+ const defaultImport = importDecl.getDefaultImport();
304
+ if (defaultImport) {
305
+ namedImports.push(defaultImport.getText());
306
+ }
307
+ const namespaceImport = importDecl.getNamespaceImport();
308
+ if (namespaceImport) {
309
+ namedImports.push(namespaceImport.getText());
310
+ }
311
+ imports.push({
312
+ moduleSpecifier,
313
+ namedImports
314
+ });
315
+ }
316
+ return {
317
+ filePath,
318
+ imports
319
+ };
320
+ }
321
+ function isTypeOnlyFile(sourceFile) {
322
+ if (sourceFile.getFunctions().length > 0) return false;
323
+ for (const classDecl of sourceFile.getClasses()) {
324
+ if (classDecl.getMethods().length > 0) return false;
325
+ if (classDecl.getGetAccessors().length > 0) return false;
326
+ if (classDecl.getSetAccessors().length > 0) return false;
327
+ }
328
+ for (const varStatement of sourceFile.getVariableStatements()) {
329
+ for (const decl of varStatement.getDeclarations()) {
330
+ const initializer = decl.getInitializer();
331
+ if (initializer) {
332
+ const kind = initializer.getKind();
333
+ if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) {
334
+ return false;
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return true;
340
+ }
341
+ function stripJsonComments(jsonString) {
342
+ let result = "";
343
+ let inString = false;
344
+ let inLineComment = false;
345
+ let inBlockComment = false;
346
+ let i = 0;
347
+ while (i < jsonString.length) {
348
+ const char = jsonString[i];
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++;
391
+ }
392
+ return result;
393
+ }
394
+ function loadProject(options) {
395
+ const { tsconfigPath, filesGlob } = options;
396
+ const errors = [];
397
+ const absoluteTsconfigPath = path.resolve(tsconfigPath);
398
+ if (!fs2.existsSync(absoluteTsconfigPath)) {
399
+ throw new Error(`tsconfig.json not found at: ${absoluteTsconfigPath}`);
400
+ }
401
+ try {
402
+ const content = fs2.readFileSync(absoluteTsconfigPath, "utf-8");
403
+ const strippedContent = stripJsonComments(content);
404
+ JSON.parse(strippedContent);
405
+ } catch (e) {
406
+ throw new Error(
407
+ `Invalid tsconfig.json at ${absoluteTsconfigPath}: ${e instanceof Error ? e.message : String(e)}`
408
+ );
409
+ }
410
+ const project = new Project({
411
+ tsConfigFilePath: absoluteTsconfigPath,
412
+ skipAddingFilesFromTsConfig: false
413
+ });
414
+ let sourceFiles = project.getSourceFiles().filter((sf) => {
415
+ 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/");
417
+ });
418
+ if (filesGlob) {
419
+ const globPattern = createGlobMatcher(filesGlob);
420
+ sourceFiles = sourceFiles.filter((sf) => globPattern(sf.getFilePath()));
421
+ }
422
+ return {
423
+ project,
424
+ sourceFiles,
425
+ errors
426
+ };
427
+ }
428
+ function createGlobMatcher(glob) {
429
+ const regexStr = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{DOUBLE_STAR\}\}/g, ".*");
430
+ const regex = new RegExp(regexStr);
431
+ return (filePath) => regex.test(filePath);
432
+ }
433
+ function getCommitHash() {
434
+ try {
435
+ const { execSync } = __require("child_process");
436
+ const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
437
+ return hash;
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+
443
+ // src/detection/detect-markers.ts
444
+ var DATABASE_OPERATIONS = /* @__PURE__ */ new Set([
445
+ "findFirst",
446
+ "findMany",
447
+ "findUnique",
448
+ "findUniqueOrThrow",
449
+ "findFirstOrThrow",
450
+ "create",
451
+ "createMany",
452
+ "update",
453
+ "updateMany",
454
+ "delete",
455
+ "deleteMany",
456
+ "upsert",
457
+ "aggregate",
458
+ "count",
459
+ "groupBy",
460
+ "$transaction",
461
+ "$queryRaw",
462
+ "$executeRaw"
463
+ ]);
464
+ var TELEMETRY_PATTERNS = [
465
+ /^track[A-Z]/,
466
+ /^analytics\./,
467
+ /^segment\./,
468
+ /^mixpanel\./,
469
+ /^amplitude\./,
470
+ /^posthog\./,
471
+ /^gtag\(/,
472
+ /^ga\(/
473
+ ];
474
+ function detectMarkers(fn, context) {
475
+ const markers = [];
476
+ detectAwaitMarkers(fn, markers);
477
+ detectDatabaseMarkers(fn, markers);
478
+ detectNetworkMarkers(fn, context, markers);
479
+ detectFileSystemMarkers(fn, context, markers);
480
+ detectEnvMarkers(fn, markers);
481
+ detectLoggingMarkers(fn, context, markers);
482
+ detectTelemetryMarkers(fn, markers);
483
+ detectQueueMarkers(fn, markers);
484
+ detectEventMarkers(fn, markers);
485
+ return markers;
486
+ }
487
+ function detectAwaitMarkers(fn, markers) {
488
+ if (fn.hasAwait) {
489
+ const awaitedCalls = fn.callSites.filter((cs) => cs.isAwaited);
490
+ if (awaitedCalls.length > 0) {
491
+ for (const call of awaitedCalls) {
492
+ markers.push({
493
+ type: "await-expression",
494
+ detail: `await ${call.expression}`,
495
+ line: call.line
496
+ });
497
+ }
498
+ } else {
499
+ markers.push({
500
+ type: "await-expression",
501
+ detail: "await expression"
502
+ });
503
+ }
504
+ }
505
+ }
506
+ function detectDatabaseMarkers(fn, markers) {
507
+ for (const callSite of fn.callSites) {
508
+ if (isDatabaseCall(callSite.expression)) {
509
+ markers.push({
510
+ type: "database-call",
511
+ detail: callSite.expression,
512
+ line: callSite.line
513
+ });
514
+ }
515
+ }
516
+ }
517
+ function isDatabaseCall(expression) {
518
+ const parts = expression.split(".");
519
+ if (parts.length < 3) {
520
+ if (parts.length === 2) {
521
+ const [base, operation] = parts;
522
+ if ((base === "db" || base === "prisma") && operation && DATABASE_OPERATIONS.has(operation)) {
523
+ return true;
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+ const lastPart = parts[parts.length - 1];
529
+ if (!lastPart || !DATABASE_OPERATIONS.has(lastPart)) {
530
+ return false;
531
+ }
532
+ return parts.some(
533
+ (part) => part === "db" || part === "prisma" || part === "database"
534
+ );
535
+ }
536
+ function detectNetworkMarkers(fn, _context, markers) {
537
+ for (const callSite of fn.callSites) {
538
+ if (callSite.expression === "fetch" || callSite.expression.endsWith(".fetch")) {
539
+ markers.push({
540
+ type: "network-fetch",
541
+ detail: callSite.expression,
542
+ line: callSite.line
543
+ });
544
+ }
545
+ if (callSite.expression.startsWith("axios") || callSite.expression === "axios") {
546
+ markers.push({
547
+ type: "network-http",
548
+ detail: callSite.expression,
549
+ line: callSite.line
550
+ });
551
+ }
552
+ }
553
+ }
554
+ function detectFileSystemMarkers(fn, _context, markers) {
555
+ for (const callSite of fn.callSites) {
556
+ if (isFsCall(callSite.expression)) {
557
+ markers.push({
558
+ type: "fs-call",
559
+ detail: callSite.expression,
560
+ line: callSite.line
561
+ });
562
+ }
563
+ }
564
+ }
565
+ function isFsCall(expression) {
566
+ const parts = expression.split(".");
567
+ return parts.some(
568
+ (part) => part === "fs" || part === "readFile" || part === "writeFile"
569
+ );
570
+ }
571
+ function detectEnvMarkers(fn, markers) {
572
+ for (const chain of fn.propertyAccessChains) {
573
+ if (chain.startsWith("process.env")) {
574
+ markers.push({
575
+ type: "env-access",
576
+ detail: chain
577
+ });
578
+ }
579
+ }
580
+ }
581
+ function detectLoggingMarkers(fn, _context, markers) {
582
+ for (const callSite of fn.callSites) {
583
+ if (isConsoleLog(callSite.expression)) {
584
+ markers.push({
585
+ type: "console-log",
586
+ detail: callSite.expression,
587
+ line: callSite.line
588
+ });
589
+ }
590
+ if (isLoggerCall(callSite.expression)) {
591
+ markers.push({
592
+ type: "logging",
593
+ detail: callSite.expression,
594
+ line: callSite.line
595
+ });
596
+ }
597
+ }
598
+ }
599
+ function isConsoleLog(expression) {
600
+ return expression === "console.log" || expression === "console.error" || expression === "console.warn" || expression === "console.info" || expression === "console.debug";
601
+ }
602
+ function isLoggerCall(expression) {
603
+ const parts = expression.split(".");
604
+ if (parts.length < 2) {
605
+ return expression === "log";
606
+ }
607
+ const base = parts[0];
608
+ const method = parts[parts.length - 1];
609
+ if (base === "logger" || base === "log") {
610
+ return true;
611
+ }
612
+ const logMethods = /* @__PURE__ */ new Set([
613
+ "info",
614
+ "warn",
615
+ "error",
616
+ "debug",
617
+ "trace",
618
+ "fatal"
619
+ ]);
620
+ return (parts.includes("logger") || parts.includes("log")) && method !== void 0 && logMethods.has(method);
621
+ }
622
+ function detectTelemetryMarkers(fn, markers) {
623
+ for (const callSite of fn.callSites) {
624
+ if (isTelemetryCall(callSite.expression)) {
625
+ markers.push({
626
+ type: "telemetry",
627
+ detail: callSite.expression,
628
+ line: callSite.line
629
+ });
630
+ }
631
+ }
632
+ }
633
+ function isTelemetryCall(expression) {
634
+ return TELEMETRY_PATTERNS.some((pattern) => pattern.test(expression));
635
+ }
636
+ function detectQueueMarkers(fn, markers) {
637
+ for (const callSite of fn.callSites) {
638
+ if (isQueueCall(callSite.expression)) {
639
+ markers.push({
640
+ type: "queue-enqueue",
641
+ detail: callSite.expression,
642
+ line: callSite.line
643
+ });
644
+ }
645
+ }
646
+ }
647
+ function isQueueCall(expression) {
648
+ return expression.endsWith(".enqueue") || expression.endsWith(".add") || expression.endsWith(".publish") || expression.includes("queue.") || expression.includes("Queue.");
649
+ }
650
+ function detectEventMarkers(fn, markers) {
651
+ for (const callSite of fn.callSites) {
652
+ if (isEventEmit(callSite.expression)) {
653
+ markers.push({
654
+ type: "event-emit",
655
+ detail: callSite.expression,
656
+ line: callSite.line
657
+ });
658
+ }
659
+ }
660
+ }
661
+ function isEventEmit(expression) {
662
+ return expression.endsWith(".emit") || expression.endsWith(".dispatch") || expression.endsWith(".trigger") || expression.endsWith(".fire");
663
+ }
664
+ function createDetectionContext(imports) {
665
+ const pureFileImports = /* @__PURE__ */ new Set();
666
+ for (const imp of imports.imports) {
667
+ if (imp.moduleSpecifier.endsWith(".pure") || imp.moduleSpecifier.includes(".pure/")) {
668
+ pureFileImports.add(imp.moduleSpecifier);
669
+ }
670
+ }
671
+ return {
672
+ imports,
673
+ pureFileImports
674
+ };
675
+ }
676
+ function getPureImportedFunctions(context) {
677
+ const pureFunctions = /* @__PURE__ */ new Set();
678
+ for (const imp of context.imports.imports) {
679
+ if (context.pureFileImports.has(imp.moduleSpecifier)) {
680
+ for (const name of imp.namedImports) {
681
+ pureFunctions.add(name);
682
+ }
683
+ }
684
+ }
685
+ return pureFunctions;
686
+ }
687
+
688
+ // src/classification/classifier.ts
689
+ var TRIVIAL_STATEMENT_THRESHOLD = 3;
690
+ function classifyFunction(_fn, markers) {
691
+ if (markers.length === 0) {
692
+ return "pure";
693
+ }
694
+ return "impure";
695
+ }
696
+ function shouldExcludeFunction(fn) {
697
+ if (fn.statementCount < TRIVIAL_STATEMENT_THRESHOLD && !fn.hasConditionals) {
698
+ return true;
699
+ }
700
+ return false;
701
+ }
702
+ function createClassifiedFunction(fn, markers, qualityScore, status) {
703
+ const classification = classifyFunction(fn, markers);
704
+ return {
705
+ ...fn,
706
+ markers,
707
+ classification,
708
+ qualityScore,
709
+ status
710
+ };
711
+ }
712
+
713
+ // src/classification/quality-scorer.ts
714
+ var QUALITY_WEIGHTS = {
715
+ // Positive signals (additive)
716
+ callsPureFile: 30,
717
+ // Calls a function from .pure.ts file
718
+ callsPureNamingConvention: 20,
719
+ // Calls plan*/derive*/compute*/calculate*/transform*
720
+ ioConcentratedAtStart: 15,
721
+ // I/O calls concentrated at start (GATHER pattern)
722
+ ioConcentratedAtEnd: 15,
723
+ // I/O calls concentrated at end (EXECUTE pattern)
724
+ lowComplexity: 10,
725
+ // Low cyclomatic complexity (≤ 5)
726
+ shellNamingConvention: 5,
727
+ // Function name matches shell conventions
728
+ callsPredicateFunctions: 5,
729
+ // Calls is*/has*/should* functions
730
+ // Negative signals (penalties)
731
+ ioInterleaved: -20,
732
+ // I/O calls interleaved throughout
733
+ highComplexity: -15,
734
+ // High cyclomatic complexity (> 10)
735
+ multipleIoTypes: -10,
736
+ // Multiple I/O types in same function
737
+ noPureFunctionCalls: -10,
738
+ // No pure function calls at all
739
+ veryLongFunction: -10
740
+ // > 100 lines
741
+ };
742
+ var PURE_FUNCTION_PATTERNS = [
743
+ /^plan[A-Z]/,
744
+ /^derive[A-Z]/,
745
+ /^compute[A-Z]/,
746
+ /^calculate[A-Z]/,
747
+ /^transform[A-Z]/,
748
+ /^build[A-Z]/,
749
+ /^create[A-Z](?!.*Service)/,
750
+ // createX but not createXService
751
+ /^parse[A-Z]/,
752
+ /^format[A-Z]/,
753
+ /^validate[A-Z]/,
754
+ /^normalize[A-Z]/,
755
+ /^convert[A-Z]/,
756
+ /^map[A-Z]/,
757
+ /^filter[A-Z]/,
758
+ /^reduce[A-Z]/,
759
+ /^merge[A-Z]/,
760
+ /^extract[A-Z]/,
761
+ /^to[A-Z]/
762
+ // toPublicX, toDTO, etc.
763
+ ];
764
+ var PREDICATE_PATTERNS = [
765
+ /^is[A-Z]/,
766
+ /^has[A-Z]/,
767
+ /^should[A-Z]/,
768
+ /^can[A-Z]/,
769
+ /^will[A-Z]/,
770
+ /^was[A-Z]/,
771
+ /^did[A-Z]/,
772
+ /^are[A-Z]/,
773
+ /^does[A-Z]/,
774
+ /^needs[A-Z]/
775
+ ];
776
+ var SHELL_FUNCTION_PATTERNS = [
777
+ /^handle[A-Z]/,
778
+ /^fetch[A-Z]/,
779
+ /^save[A-Z]/,
780
+ /^send[A-Z]/,
781
+ /^load[A-Z]/,
782
+ /^get[A-Z]/,
783
+ // getData, getUser, etc.
784
+ /^set[A-Z]/,
785
+ /^update[A-Z]/,
786
+ /^delete[A-Z]/,
787
+ /^remove[A-Z]/,
788
+ /^create[A-Z].*Service/,
789
+ // createXService
790
+ /^init[A-Z]/,
791
+ /^process[A-Z]/,
792
+ /^execute[A-Z]/,
793
+ /^run[A-Z]/,
794
+ /^perform[A-Z]/
795
+ ];
796
+ var IO_MARKER_TYPES = /* @__PURE__ */ new Set([
797
+ "await-expression",
798
+ "database-call",
799
+ "network-fetch",
800
+ "network-http",
801
+ "fs-call"
802
+ ]);
803
+ function computeQualityScore(fn, markers, context) {
804
+ let score = 50;
805
+ const analysis = analyzeFunction(fn, markers, context);
806
+ if (analysis.callsPureFile) {
807
+ score += QUALITY_WEIGHTS.callsPureFile;
808
+ }
809
+ if (analysis.callsPureNamingConvention) {
810
+ score += QUALITY_WEIGHTS.callsPureNamingConvention;
811
+ }
812
+ if (analysis.ioConcentratedAtStart) {
813
+ score += QUALITY_WEIGHTS.ioConcentratedAtStart;
814
+ }
815
+ if (analysis.ioConcentratedAtEnd) {
816
+ score += QUALITY_WEIGHTS.ioConcentratedAtEnd;
817
+ }
818
+ if (analysis.lowComplexity) {
819
+ score += QUALITY_WEIGHTS.lowComplexity;
820
+ }
821
+ if (analysis.shellNamingConvention) {
822
+ score += QUALITY_WEIGHTS.shellNamingConvention;
823
+ }
824
+ if (analysis.callsPredicateFunctions) {
825
+ score += QUALITY_WEIGHTS.callsPredicateFunctions;
826
+ }
827
+ if (analysis.ioInterleaved) {
828
+ score += QUALITY_WEIGHTS.ioInterleaved;
829
+ }
830
+ if (analysis.highComplexity) {
831
+ score += QUALITY_WEIGHTS.highComplexity;
832
+ }
833
+ if (analysis.multipleIoTypes) {
834
+ score += QUALITY_WEIGHTS.multipleIoTypes;
835
+ }
836
+ if (analysis.noPureFunctionCalls) {
837
+ score += QUALITY_WEIGHTS.noPureFunctionCalls;
838
+ }
839
+ if (analysis.veryLongFunction) {
840
+ score += QUALITY_WEIGHTS.veryLongFunction;
841
+ }
842
+ return Math.max(0, Math.min(100, score));
843
+ }
844
+ function analyzeFunction(fn, markers, context) {
845
+ const ioMarkers = markers.filter(
846
+ (m) => IO_MARKER_TYPES.has(m.type) && m.line !== void 0
847
+ );
848
+ const uniqueIoTypes = new Set(markers.map((m) => m.type));
849
+ const callsPureFile = checkCallsPureFile(fn, context);
850
+ const { callsPureNaming, pureCallCount } = checkCallsPureNamingConvention(
851
+ fn.callSites
852
+ );
853
+ const { callsPredicates, predicateCallCount } = checkCallsPredicateFunctions(
854
+ fn.callSites
855
+ );
856
+ const { concentratedAtStart, concentratedAtEnd, interleaved } = analyzeIoConcentration(fn, ioMarkers);
857
+ const estimatedComplexity = estimateCyclomaticComplexity(fn);
858
+ const shellNamingConvention = checkShellNamingConvention(fn.name);
859
+ return {
860
+ callsPureFile,
861
+ callsPureNamingConvention: callsPureNaming,
862
+ ioConcentratedAtStart: concentratedAtStart,
863
+ ioConcentratedAtEnd: concentratedAtEnd,
864
+ lowComplexity: estimatedComplexity <= 5,
865
+ shellNamingConvention,
866
+ callsPredicateFunctions: callsPredicates,
867
+ ioInterleaved: interleaved,
868
+ highComplexity: estimatedComplexity > 10,
869
+ multipleIoTypes: uniqueIoTypes.size >= 3,
870
+ noPureFunctionCalls: pureCallCount === 0 && predicateCallCount === 0,
871
+ veryLongFunction: fn.bodyLineCount > 100,
872
+ estimatedComplexity,
873
+ ioMarkerCount: ioMarkers.length,
874
+ uniqueIoTypes,
875
+ pureCallCount,
876
+ predicateCallCount
877
+ };
878
+ }
879
+ function checkCallsPureFile(fn, context) {
880
+ const pureImports = /* @__PURE__ */ new Set();
881
+ for (const imp of context.imports.imports) {
882
+ if (context.pureFileImports.has(imp.moduleSpecifier)) {
883
+ for (const name of imp.namedImports) {
884
+ pureImports.add(name);
885
+ }
886
+ }
887
+ }
888
+ for (const callSite of fn.callSites) {
889
+ const calledFn = extractFunctionName(callSite.expression);
890
+ if (pureImports.has(calledFn)) {
891
+ return true;
892
+ }
893
+ }
894
+ return false;
895
+ }
896
+ function checkCallsPureNamingConvention(callSites) {
897
+ let pureCallCount = 0;
898
+ for (const callSite of callSites) {
899
+ const calledFn = extractFunctionName(callSite.expression);
900
+ if (matchesAnyPattern(calledFn, PURE_FUNCTION_PATTERNS)) {
901
+ pureCallCount++;
902
+ }
903
+ }
904
+ return {
905
+ callsPureNaming: pureCallCount > 0,
906
+ pureCallCount
907
+ };
908
+ }
909
+ function checkCallsPredicateFunctions(callSites) {
910
+ let predicateCallCount = 0;
911
+ for (const callSite of callSites) {
912
+ const calledFn = extractFunctionName(callSite.expression);
913
+ if (matchesAnyPattern(calledFn, PREDICATE_PATTERNS)) {
914
+ predicateCallCount++;
915
+ }
916
+ }
917
+ return {
918
+ callsPredicates: predicateCallCount > 0,
919
+ predicateCallCount
920
+ };
921
+ }
922
+ function analyzeIoConcentration(fn, ioMarkers) {
923
+ if (ioMarkers.length === 0) {
924
+ return {
925
+ concentratedAtStart: false,
926
+ concentratedAtEnd: false,
927
+ interleaved: false
928
+ };
929
+ }
930
+ const positions = ioMarkers.map((m) => (m.line ?? 0) / Math.max(fn.bodyLineCount, 1)).sort((a, b) => a - b);
931
+ const startThreshold = 0.33;
932
+ const endThreshold = 0.67;
933
+ const atStart = positions.filter((p) => p <= startThreshold).length;
934
+ const atEnd = positions.filter((p) => p >= endThreshold).length;
935
+ const inMiddle = positions.filter(
936
+ (p) => p > startThreshold && p < endThreshold
937
+ ).length;
938
+ const total = positions.length;
939
+ const concentratedAtStart = atStart >= total * 0.6 && inMiddle <= 1;
940
+ const concentratedAtEnd = atEnd >= total * 0.6 && inMiddle <= 1;
941
+ const interleaved = inMiddle >= 2 || atStart > 0 && atEnd > 0 && inMiddle > 0 || total >= 4 && !concentratedAtStart && !concentratedAtEnd;
942
+ return {
943
+ concentratedAtStart,
944
+ concentratedAtEnd,
945
+ interleaved
946
+ };
947
+ }
948
+ function estimateCyclomaticComplexity(fn) {
949
+ let complexity = 1;
950
+ if (fn.hasConditionals) {
951
+ complexity += Math.ceil(fn.statementCount / 10);
952
+ }
953
+ if (fn.bodyLineCount > 50) {
954
+ complexity += Math.floor((fn.bodyLineCount - 50) / 25);
955
+ }
956
+ return complexity;
957
+ }
958
+ function checkShellNamingConvention(name) {
959
+ if (!name) return false;
960
+ return matchesAnyPattern(name, SHELL_FUNCTION_PATTERNS);
961
+ }
962
+ function extractFunctionName(expression) {
963
+ const parts = expression.split(".");
964
+ return parts[parts.length - 1] ?? expression;
965
+ }
966
+ function matchesAnyPattern(str, patterns) {
967
+ return patterns.some((pattern) => pattern.test(str));
968
+ }
969
+
970
+ // src/classification/derive-status.ts
971
+ function deriveStatus(classification, qualityScore, thresholds = DEFAULT_QUALITY_THRESHOLDS) {
972
+ if (classification === "pure") {
973
+ return "ok";
974
+ }
975
+ if (qualityScore === null) {
976
+ return "review";
977
+ }
978
+ if (qualityScore >= thresholds.okThreshold) {
979
+ return "ok";
980
+ }
981
+ if (qualityScore >= thresholds.reviewThreshold) {
982
+ return "review";
983
+ }
984
+ return "refactor";
985
+ }
986
+ function getStatusDescription(status) {
987
+ switch (status) {
988
+ case "ok":
989
+ return "No action needed";
990
+ case "review":
991
+ return "Consider improving if touching this code";
992
+ case "refactor":
993
+ return "Tangled code, prioritize cleanup";
994
+ }
995
+ }
996
+ function getStatusEmoji(status) {
997
+ switch (status) {
998
+ case "ok":
999
+ return "\u2713";
1000
+ case "review":
1001
+ return "\u25D0";
1002
+ case "refactor":
1003
+ return "\u2717";
1004
+ }
1005
+ }
1006
+ function getStatusColor(status) {
1007
+ switch (status) {
1008
+ case "ok":
1009
+ return "green";
1010
+ case "review":
1011
+ return "yellow";
1012
+ case "refactor":
1013
+ return "red";
1014
+ }
1015
+ }
1016
+
1017
+ // src/scoring/scorer.ts
1018
+ function scoreFile(filePath, functions, isTypeOnly = false) {
1019
+ if (isTypeOnly) {
1020
+ return {
1021
+ filePath,
1022
+ purity: 100,
1023
+ impurityQuality: null,
1024
+ health: 100,
1025
+ pureCount: 0,
1026
+ impureCount: 0,
1027
+ excludedCount: 0,
1028
+ statusBreakdown: { ok: 0, review: 0, refactor: 0 },
1029
+ pureLineCount: 0,
1030
+ impureLineCount: 0,
1031
+ functions: [],
1032
+ refactoringCandidates: [],
1033
+ typeOnly: true
1034
+ };
1035
+ }
1036
+ const pureFunctions = functions.filter((f) => f.classification === "pure");
1037
+ const impureFunctions = functions.filter((f) => f.classification === "impure");
1038
+ const pureCount = pureFunctions.length;
1039
+ const impureCount = impureFunctions.length;
1040
+ const total = pureCount + impureCount;
1041
+ if (total === 0) {
1042
+ return {
1043
+ filePath,
1044
+ purity: 100,
1045
+ impurityQuality: null,
1046
+ health: 100,
1047
+ pureCount: 0,
1048
+ impureCount: 0,
1049
+ excludedCount: 0,
1050
+ statusBreakdown: { ok: 0, review: 0, refactor: 0 },
1051
+ pureLineCount: 0,
1052
+ impureLineCount: 0,
1053
+ functions: [],
1054
+ refactoringCandidates: [],
1055
+ allExcluded: true
1056
+ };
1057
+ }
1058
+ const purity = pureCount / total * 100;
1059
+ let impurityQuality = null;
1060
+ if (impureCount > 0) {
1061
+ const totalQuality = impureFunctions.reduce(
1062
+ (sum, f) => sum + (f.qualityScore ?? 0),
1063
+ 0
1064
+ );
1065
+ impurityQuality = totalQuality / impureCount;
1066
+ }
1067
+ const statusBreakdown = {
1068
+ ok: functions.filter((f) => f.status === "ok").length,
1069
+ review: functions.filter((f) => f.status === "review").length,
1070
+ refactor: functions.filter((f) => f.status === "refactor").length
1071
+ };
1072
+ const health = statusBreakdown.ok / total * 100;
1073
+ const pureLineCount = pureFunctions.reduce(
1074
+ (sum, f) => sum + f.bodyLineCount,
1075
+ 0
1076
+ );
1077
+ const impureLineCount = impureFunctions.reduce(
1078
+ (sum, f) => sum + f.bodyLineCount,
1079
+ 0
1080
+ );
1081
+ const refactoringCandidates = impureFunctions.filter((f) => f.status === "refactor").map((f) => ({
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) => {
1088
+ const impactA = a.bodyLineCount * (100 - a.qualityScore);
1089
+ const impactB = b.bodyLineCount * (100 - b.qualityScore);
1090
+ return impactB - impactA;
1091
+ });
1092
+ return {
1093
+ filePath,
1094
+ purity,
1095
+ impurityQuality,
1096
+ health,
1097
+ pureCount,
1098
+ impureCount,
1099
+ excludedCount: 0,
1100
+ statusBreakdown,
1101
+ pureLineCount,
1102
+ impureLineCount,
1103
+ functions,
1104
+ refactoringCandidates
1105
+ };
1106
+ }
1107
+ function scoreDirectory(dirPath, fileScores) {
1108
+ const scorableFiles = fileScores.filter(
1109
+ (f) => !f.typeOnly && !f.allExcluded && f.pureCount + f.impureCount > 0
1110
+ );
1111
+ if (scorableFiles.length === 0) {
1112
+ return {
1113
+ dirPath,
1114
+ purity: 100,
1115
+ impurityQuality: null,
1116
+ health: 100,
1117
+ pureCount: 0,
1118
+ impureCount: 0,
1119
+ excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
1120
+ statusBreakdown: { ok: 0, review: 0, refactor: 0 },
1121
+ pureLineCount: 0,
1122
+ impureLineCount: 0,
1123
+ fileScores
1124
+ };
1125
+ }
1126
+ const totalPure = scorableFiles.reduce((sum, f) => sum + f.pureCount, 0);
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
+ );
1157
+ return {
1158
+ dirPath,
1159
+ purity,
1160
+ impurityQuality,
1161
+ health,
1162
+ pureCount: totalPure,
1163
+ impureCount: totalImpure,
1164
+ excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
1165
+ statusBreakdown,
1166
+ pureLineCount,
1167
+ impureLineCount,
1168
+ fileScores
1169
+ };
1170
+ }
1171
+ function scoreProject(directoryScores, options = {}) {
1172
+ const {
1173
+ timestamp = (/* @__PURE__ */ new Date()).toISOString(),
1174
+ commitHash = null,
1175
+ subset = false,
1176
+ filesGlob,
1177
+ errors = []
1178
+ } = options;
1179
+ const scorableDirs = directoryScores.filter(
1180
+ (d) => d.pureCount + d.impureCount > 0
1181
+ );
1182
+ if (scorableDirs.length === 0) {
1183
+ return {
1184
+ purity: 100,
1185
+ impurityQuality: null,
1186
+ health: 100,
1187
+ pureCount: 0,
1188
+ impureCount: 0,
1189
+ excludedCount: directoryScores.reduce(
1190
+ (sum, d) => sum + d.excludedCount,
1191
+ 0
1192
+ ),
1193
+ statusBreakdown: { ok: 0, review: 0, refactor: 0 },
1194
+ pureLineCount: 0,
1195
+ impureLineCount: 0,
1196
+ directoryScores,
1197
+ timestamp,
1198
+ commitHash,
1199
+ refactoringCandidates: [],
1200
+ allExcluded: true,
1201
+ ...subset && { subset: true, filesGlob },
1202
+ ...errors.length > 0 && { errors }
1203
+ };
1204
+ }
1205
+ const totalPure = scorableDirs.reduce((sum, d) => sum + d.pureCount, 0);
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
+ );
1236
+ const allCandidates = [];
1237
+ for (const dir of directoryScores) {
1238
+ for (const file of dir.fileScores) {
1239
+ for (const candidate of file.refactoringCandidates) {
1240
+ allCandidates.push({
1241
+ ...candidate,
1242
+ filePath: file.filePath
1243
+ });
1244
+ }
1245
+ }
1246
+ }
1247
+ const refactoringCandidates = allCandidates.sort((a, b) => {
1248
+ const impactA = a.bodyLineCount * (100 - a.qualityScore);
1249
+ const impactB = b.bodyLineCount * (100 - b.qualityScore);
1250
+ return impactB - impactA;
1251
+ });
1252
+ const result = {
1253
+ purity,
1254
+ impurityQuality,
1255
+ health,
1256
+ pureCount: totalPure,
1257
+ impureCount: totalImpure,
1258
+ excludedCount: directoryScores.reduce((sum, d) => sum + d.excludedCount, 0),
1259
+ statusBreakdown,
1260
+ pureLineCount,
1261
+ impureLineCount,
1262
+ directoryScores,
1263
+ timestamp,
1264
+ commitHash,
1265
+ refactoringCandidates
1266
+ };
1267
+ if (subset && filesGlob !== void 0) {
1268
+ result.subset = true;
1269
+ result.filesGlob = filesGlob;
1270
+ }
1271
+ if (errors.length > 0) {
1272
+ result.errors = errors;
1273
+ }
1274
+ return result;
1275
+ }
1276
+ function groupFilesByDirectory(fileScores) {
1277
+ const groups = /* @__PURE__ */ new Map();
1278
+ for (const fileScore of fileScores) {
1279
+ const dirPath = getDirectoryPath(fileScore.filePath);
1280
+ const existing = groups.get(dirPath);
1281
+ if (existing) {
1282
+ existing.push(fileScore);
1283
+ } else {
1284
+ groups.set(dirPath, [fileScore]);
1285
+ }
1286
+ }
1287
+ return groups;
1288
+ }
1289
+ function getDirectoryPath(filePath) {
1290
+ const lastSlash = filePath.lastIndexOf("/");
1291
+ if (lastSlash === -1) return ".";
1292
+ return filePath.slice(0, lastSlash);
1293
+ }
1294
+ function calculateDelta(current, previous) {
1295
+ return {
1296
+ purityDelta: current.purity - previous.purity,
1297
+ impurityQualityDelta: current.impurityQuality !== null && previous.impurityQuality !== null ? current.impurityQuality - previous.impurityQuality : null,
1298
+ healthDelta: current.health - previous.health,
1299
+ pureCountDelta: current.pureCount - previous.pureCount,
1300
+ impureCountDelta: current.impureCount - previous.impureCount
1301
+ };
1302
+ }
1303
+ function getDiagnosticInsights(score) {
1304
+ const insights = [];
1305
+ if (score.purity >= 60 && score.impurityQuality !== null && score.impurityQuality < 50) {
1306
+ insights.push(
1307
+ "Most code is pure, but the impure code is tangled. Focus on improving the structure of impure functions."
1308
+ );
1309
+ }
1310
+ if (score.purity < 40 && score.impurityQuality !== null && score.impurityQuality >= 70) {
1311
+ insights.push(
1312
+ "Lots of I/O code, but it's well-structured. Consider extracting more pure business logic."
1313
+ );
1314
+ }
1315
+ if (score.purity < 40 && score.impurityQuality !== null && score.impurityQuality < 50) {
1316
+ insights.push(
1317
+ "Significant technical debt in I/O code. Both purity and structure need improvement."
1318
+ );
1319
+ }
1320
+ if (score.health >= 80 && score.purity < 50) {
1321
+ insights.push(
1322
+ "Despite low purity, impure functions are well-structured. Good FCIS patterns in shell code."
1323
+ );
1324
+ }
1325
+ if (score.health < 50) {
1326
+ insights.push(
1327
+ "More than half of functions need attention. Prioritize refactoring candidates."
1328
+ );
1329
+ }
1330
+ return insights;
1331
+ }
1332
+
1333
+ // src/analyzer.ts
1334
+ async function analyze(config) {
1335
+ const errors = [];
1336
+ const extractorOptions = {
1337
+ tsconfigPath: config.tsconfigPath
1338
+ };
1339
+ if (config.filesGlob !== void 0) {
1340
+ extractorOptions.filesGlob = config.filesGlob;
1341
+ }
1342
+ const { sourceFiles } = loadProject(extractorOptions);
1343
+ if (sourceFiles.length === 0) {
1344
+ throw new Error("No source files found to analyze");
1345
+ }
1346
+ const fileScores = [];
1347
+ for (const sourceFile of sourceFiles) {
1348
+ try {
1349
+ const fileScore = analyzeFile(sourceFile);
1350
+ fileScores.push(fileScore);
1351
+ } catch (error) {
1352
+ errors.push({
1353
+ filePath: sourceFile.getFilePath(),
1354
+ error: error instanceof Error ? error.message : String(error)
1355
+ });
1356
+ }
1357
+ }
1358
+ if (fileScores.length === 0 && errors.length > 0) {
1359
+ throw new Error(
1360
+ `Failed to analyze any files. ${errors.length} files had errors.`
1361
+ );
1362
+ }
1363
+ const directoryGroups = groupFilesByDirectory(fileScores);
1364
+ const directoryScores = [];
1365
+ for (const [dirPath, files] of directoryGroups) {
1366
+ const dirScore = scoreDirectory(dirPath, files);
1367
+ directoryScores.push(dirScore);
1368
+ }
1369
+ directoryScores.sort((a, b) => a.dirPath.localeCompare(b.dirPath));
1370
+ const projectOptions = {
1371
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1372
+ commitHash: getCommitHash()
1373
+ };
1374
+ if (config.filesGlob !== void 0) {
1375
+ projectOptions.subset = true;
1376
+ projectOptions.filesGlob = config.filesGlob;
1377
+ }
1378
+ if (errors.length > 0) {
1379
+ projectOptions.errors = errors;
1380
+ }
1381
+ const projectScore = scoreProject(directoryScores, projectOptions);
1382
+ return projectScore;
1383
+ }
1384
+ function analyzeFile(sourceFile) {
1385
+ const filePath = sourceFile.getFilePath();
1386
+ if (isTypeOnlyFile(sourceFile)) {
1387
+ return scoreFile(filePath, [], true);
1388
+ }
1389
+ const functions = extractFunctions(sourceFile);
1390
+ const imports = extractImports(sourceFile);
1391
+ const context = createDetectionContext(imports);
1392
+ const classifiedFunctions = [];
1393
+ for (const fn of functions) {
1394
+ if (shouldExcludeFunction(fn)) {
1395
+ continue;
1396
+ }
1397
+ const markers = detectMarkers(fn, context);
1398
+ const classification = classifyFunction(fn, markers);
1399
+ let qualityScore = null;
1400
+ if (classification === "impure") {
1401
+ qualityScore = computeQualityScore(fn, markers, context);
1402
+ }
1403
+ const status = deriveStatus(
1404
+ classification,
1405
+ qualityScore,
1406
+ DEFAULT_QUALITY_THRESHOLDS
1407
+ );
1408
+ const classifiedFn = createClassifiedFunction(
1409
+ fn,
1410
+ markers,
1411
+ qualityScore,
1412
+ status
1413
+ );
1414
+ classifiedFunctions.push(classifiedFn);
1415
+ }
1416
+ return scoreFile(filePath, classifiedFunctions, false);
1417
+ }
1418
+ async function analyzeFilePath(tsconfigPath, filePath) {
1419
+ const { sourceFiles } = loadProject({
1420
+ tsconfigPath,
1421
+ filesGlob: filePath
1422
+ });
1423
+ if (sourceFiles.length === 0) {
1424
+ return null;
1425
+ }
1426
+ const sourceFile = sourceFiles[0];
1427
+ if (!sourceFile) {
1428
+ return null;
1429
+ }
1430
+ return analyzeFile(sourceFile);
1431
+ }
1432
+ function checkThresholds(score, config) {
1433
+ const failures = [];
1434
+ if (config.minHealth !== void 0 && score.health < config.minHealth) {
1435
+ failures.push(
1436
+ `Health ${score.health.toFixed(1)}% is below minimum ${config.minHealth}%`
1437
+ );
1438
+ }
1439
+ if (config.minPurity !== void 0 && score.purity < config.minPurity) {
1440
+ failures.push(
1441
+ `Purity ${score.purity.toFixed(1)}% is below minimum ${config.minPurity}%`
1442
+ );
1443
+ }
1444
+ if (config.minQuality !== void 0 && score.impurityQuality !== null && score.impurityQuality < config.minQuality) {
1445
+ failures.push(
1446
+ `Impurity Quality ${score.impurityQuality.toFixed(1)}% is below minimum ${config.minQuality}%`
1447
+ );
1448
+ }
1449
+ return {
1450
+ passed: failures.length === 0,
1451
+ failures
1452
+ };
1453
+ }
1454
+
1455
+ // src/detection/markers.ts
1456
+ var MARKER_CATALOG = {
1457
+ "await-expression": {
1458
+ type: "await-expression",
1459
+ description: "Await expression indicates I/O suspension point",
1460
+ category: "io",
1461
+ severity: "high"
1462
+ },
1463
+ "database-call": {
1464
+ type: "database-call",
1465
+ description: "Database operation (Prisma/similar ORM call)",
1466
+ category: "io",
1467
+ severity: "high"
1468
+ },
1469
+ "network-fetch": {
1470
+ type: "network-fetch",
1471
+ description: "Native fetch() call for HTTP requests",
1472
+ category: "io",
1473
+ severity: "high"
1474
+ },
1475
+ "network-http": {
1476
+ type: "network-http",
1477
+ description: "HTTP client library call (axios, got, etc.)",
1478
+ category: "io",
1479
+ severity: "high"
1480
+ },
1481
+ "fs-import": {
1482
+ type: "fs-import",
1483
+ description: "Import from file system module",
1484
+ category: "io",
1485
+ severity: "medium"
1486
+ },
1487
+ "fs-call": {
1488
+ type: "fs-call",
1489
+ description: "File system operation call",
1490
+ category: "io",
1491
+ severity: "high"
1492
+ },
1493
+ "env-access": {
1494
+ type: "env-access",
1495
+ description: "Access to process.env environment variables",
1496
+ category: "environment",
1497
+ severity: "medium"
1498
+ },
1499
+ "console-log": {
1500
+ type: "console-log",
1501
+ description: "Console logging call",
1502
+ category: "side-effect",
1503
+ severity: "low"
1504
+ },
1505
+ logging: {
1506
+ type: "logging",
1507
+ description: "Logger service call",
1508
+ category: "side-effect",
1509
+ severity: "low"
1510
+ },
1511
+ telemetry: {
1512
+ type: "telemetry",
1513
+ description: "Analytics/tracking/telemetry call",
1514
+ category: "side-effect",
1515
+ severity: "low"
1516
+ },
1517
+ "queue-enqueue": {
1518
+ type: "queue-enqueue",
1519
+ description: "Message queue enqueue operation",
1520
+ category: "io",
1521
+ severity: "high"
1522
+ },
1523
+ "event-emit": {
1524
+ type: "event-emit",
1525
+ description: "Event emitter emit call",
1526
+ category: "side-effect",
1527
+ severity: "medium"
1528
+ }
1529
+ };
1530
+ function getMarkerDefinition(type) {
1531
+ return MARKER_CATALOG[type];
1532
+ }
1533
+ function getAllMarkerTypes() {
1534
+ return Object.keys(MARKER_CATALOG);
1535
+ }
1536
+ function generateJsonReport(score, options = {}) {
1537
+ const { pretty = true, includeFunction = false } = options;
1538
+ const reportData = prepareReportData(score, includeFunction);
1539
+ if (pretty) {
1540
+ return JSON.stringify(reportData, null, 2);
1541
+ }
1542
+ return JSON.stringify(reportData);
1543
+ }
1544
+ function prepareReportData(score, includeFunctions) {
1545
+ if (includeFunctions) {
1546
+ return score;
1547
+ }
1548
+ return {
1549
+ ...score,
1550
+ directoryScores: score.directoryScores.map((dir) => ({
1551
+ ...dir,
1552
+ fileScores: dir.fileScores.map((file) => ({
1553
+ ...file,
1554
+ functions: []
1555
+ // Strip function details to reduce size
1556
+ }))
1557
+ }))
1558
+ };
1559
+ }
1560
+ function writeJsonReport(score, outputPath, options = {}) {
1561
+ const json = generateJsonReport(score, options);
1562
+ const dir = path.dirname(outputPath);
1563
+ if (!fs2.existsSync(dir)) {
1564
+ fs2.mkdirSync(dir, { recursive: true });
1565
+ }
1566
+ fs2.writeFileSync(outputPath, json, "utf-8");
1567
+ }
1568
+ function readJsonReport(inputPath) {
1569
+ if (!fs2.existsSync(inputPath)) {
1570
+ return null;
1571
+ }
1572
+ const content = fs2.readFileSync(inputPath, "utf-8");
1573
+ return JSON.parse(content);
1574
+ }
1575
+ function createSummaryJson(score) {
1576
+ return {
1577
+ health: Math.round(score.health * 10) / 10,
1578
+ purity: Math.round(score.purity * 10) / 10,
1579
+ impurityQuality: score.impurityQuality !== null ? Math.round(score.impurityQuality * 10) / 10 : null,
1580
+ pureCount: score.pureCount,
1581
+ impureCount: score.impureCount,
1582
+ statusBreakdown: score.statusBreakdown,
1583
+ timestamp: score.timestamp,
1584
+ commitHash: score.commitHash,
1585
+ topRefactoringCandidates: score.refactoringCandidates.slice(0, 5).map((c) => ({
1586
+ name: c.name,
1587
+ filePath: c.filePath,
1588
+ qualityScore: c.qualityScore
1589
+ }))
1590
+ };
1591
+ }
1592
+ function generateComparisonReport(current, previous) {
1593
+ const currentSummary = createSummaryJson(current);
1594
+ const previousSummary = createSummaryJson(previous);
1595
+ const delta = {
1596
+ health: Math.round((current.health - previous.health) * 10) / 10,
1597
+ purity: Math.round((current.purity - previous.purity) * 10) / 10,
1598
+ impurityQuality: current.impurityQuality !== null && previous.impurityQuality !== null ? Math.round(
1599
+ (current.impurityQuality - previous.impurityQuality) * 10
1600
+ ) / 10 : null,
1601
+ pureCount: current.pureCount - previous.pureCount,
1602
+ impureCount: current.impureCount - previous.impureCount
1603
+ };
1604
+ const currentCandidateKeys = new Set(
1605
+ current.refactoringCandidates.map((c) => `${c.filePath}:${c.name}`)
1606
+ );
1607
+ const previousCandidateKeys = new Set(
1608
+ previous.refactoringCandidates.map((c) => `${c.filePath}:${c.name}`)
1609
+ );
1610
+ const newRefactoringCandidates = current.refactoringCandidates.filter((c) => !previousCandidateKeys.has(`${c.filePath}:${c.name}`)).map((c) => ({ name: c.name, filePath: c.filePath }));
1611
+ const resolvedRefactoringCandidates = previous.refactoringCandidates.filter((c) => !currentCandidateKeys.has(`${c.filePath}:${c.name}`)).map((c) => ({ name: c.name, filePath: c.filePath }));
1612
+ const improved = delta.health > 0 || delta.health >= 0 && delta.purity > 0 || delta.health >= 0 && delta.impurityQuality !== null && delta.impurityQuality > 0;
1613
+ return {
1614
+ current: currentSummary,
1615
+ previous: previousSummary,
1616
+ delta,
1617
+ improved,
1618
+ newRefactoringCandidates,
1619
+ resolvedRefactoringCandidates
1620
+ };
1621
+ }
1622
+ function printConsoleReport(score, options = {}) {
1623
+ const { verbose = false } = options;
1624
+ printHeader();
1625
+ printProjectSummary(score);
1626
+ printStatusBreakdown(
1627
+ score.statusBreakdown,
1628
+ score.pureCount + score.impureCount
1629
+ );
1630
+ printInsights(score);
1631
+ if (verbose) {
1632
+ printDirectoryBreakdown(score.directoryScores);
1633
+ } else {
1634
+ printWorstDirectories(score.directoryScores);
1635
+ }
1636
+ printRefactoringCandidates(score.refactoringCandidates.slice(0, 10));
1637
+ if (score.errors && score.errors.length > 0) {
1638
+ printErrors(score.errors);
1639
+ }
1640
+ printFooter(score);
1641
+ }
1642
+ function printHeader() {
1643
+ console.log();
1644
+ console.log(chalk.bold("FCIS Analysis"));
1645
+ console.log(
1646
+ chalk.gray("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550")
1647
+ );
1648
+ console.log();
1649
+ }
1650
+ function printProjectSummary(score) {
1651
+ const healthBar = createProgressBar(score.health, 25);
1652
+ const healthColor = score.health >= 70 ? chalk.green : score.health >= 50 ? chalk.yellow : chalk.red;
1653
+ const purityColor = score.purity >= 70 ? chalk.green : score.purity >= 50 ? chalk.yellow : chalk.red;
1654
+ console.log(
1655
+ `Project Health: ${healthColor(score.health.toFixed(0) + "%")} ${healthBar}`
1656
+ );
1657
+ console.log(
1658
+ ` Purity: ${purityColor(score.purity.toFixed(0) + "%")} (${score.pureCount} pure / ${score.pureCount + score.impureCount} total)`
1659
+ );
1660
+ if (score.impurityQuality !== null) {
1661
+ const qualityColor = score.impurityQuality >= 70 ? chalk.green : score.impurityQuality >= 50 ? chalk.yellow : chalk.red;
1662
+ console.log(
1663
+ ` Impurity Quality: ${qualityColor(score.impurityQuality.toFixed(0) + "%")} average`
1664
+ );
1665
+ }
1666
+ console.log();
1667
+ }
1668
+ function printStatusBreakdown(breakdown, total) {
1669
+ console.log("Status Breakdown:");
1670
+ const okPercent = total > 0 ? (breakdown.ok / total * 100).toFixed(0) : "0";
1671
+ const reviewPercent = total > 0 ? (breakdown.review / total * 100).toFixed(0) : "0";
1672
+ const refactorPercent = total > 0 ? (breakdown.refactor / total * 100).toFixed(0) : "0";
1673
+ console.log(
1674
+ ` ${chalk.green("\u2713 OK:")} ${String(breakdown.ok).padStart(4)} functions (${okPercent}%) \u2014 no action needed`
1675
+ );
1676
+ console.log(
1677
+ ` ${chalk.yellow("\u25D0 Review:")} ${String(breakdown.review).padStart(4)} functions (${reviewPercent}%) \u2014 could be improved`
1678
+ );
1679
+ console.log(
1680
+ ` ${chalk.red("\u2717 Refactor:")} ${String(breakdown.refactor).padStart(4)} functions (${refactorPercent}%) \u2014 tangled, needs work`
1681
+ );
1682
+ console.log();
1683
+ }
1684
+ function printInsights(score) {
1685
+ const insights = getDiagnosticInsights(score);
1686
+ if (insights.length > 0) {
1687
+ console.log(chalk.bold("Insights:"));
1688
+ for (const insight of insights) {
1689
+ console.log(` ${chalk.cyan("\u2192")} ${insight}`);
1690
+ }
1691
+ console.log();
1692
+ }
1693
+ }
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
+ function printWorstDirectories(directories) {
1717
+ const sorted = [...directories].filter((d) => d.pureCount + d.impureCount > 0 && d.health < 70).sort((a, b) => a.health - b.health).slice(0, 5);
1718
+ if (sorted.length === 0) return;
1719
+ console.log(chalk.bold("Directories Needing Attention:"));
1720
+ for (const dir of sorted) {
1721
+ const healthColor = dir.health >= 50 ? chalk.yellow : chalk.red;
1722
+ const total = dir.pureCount + dir.impureCount;
1723
+ const relativePath = toRelativePath(dir.dirPath);
1724
+ console.log(
1725
+ ` ${healthColor(dir.health.toFixed(0).padStart(3) + "%")} ` + chalk.gray(`(${dir.pureCount}/${total} pure)`) + ` ${relativePath}`
1726
+ );
1727
+ }
1728
+ console.log();
1729
+ }
1730
+ function printRefactoringCandidates(candidates) {
1731
+ if (candidates.length === 0) {
1732
+ console.log(chalk.green("No refactoring candidates found. Great job!"));
1733
+ console.log();
1734
+ return;
1735
+ }
1736
+ console.log(chalk.bold("Top Refactoring Candidates:"));
1737
+ console.log(chalk.gray("(Sorted by impact: size \xD7 complexity)"));
1738
+ console.log();
1739
+ for (let i = 0; i < candidates.length; i++) {
1740
+ const candidate = candidates[i];
1741
+ if (!candidate) continue;
1742
+ const qualityColor = candidate.qualityScore >= 40 ? chalk.yellow : chalk.red;
1743
+ const relativePath = toRelativePath(candidate.filePath);
1744
+ console.log(
1745
+ chalk.gray(`${(i + 1).toString().padStart(2)}.`) + ` ${qualityColor(candidate.qualityScore.toFixed(0).padStart(3))}` + chalk.gray("/100") + ` ${chalk.cyan(candidate.name ?? "<anonymous>")}` + chalk.gray(` (${candidate.bodyLineCount} lines)`)
1746
+ );
1747
+ console.log(chalk.gray(` ${relativePath}:${candidate.startLine}`));
1748
+ if (candidate.markers.length > 0) {
1749
+ const markerStr = candidate.markers.slice(0, 3).join(", ");
1750
+ const more = candidate.markers.length > 3 ? ` +${candidate.markers.length - 3} more` : "";
1751
+ console.log(chalk.gray(` Markers: ${markerStr}${more}`));
1752
+ }
1753
+ console.log();
1754
+ }
1755
+ }
1756
+ function printErrors(errors) {
1757
+ console.log(chalk.yellow.bold(`Errors (${errors.length} files skipped):`));
1758
+ for (const err of errors.slice(0, 5)) {
1759
+ console.log(` ${chalk.yellow("!")} ${err.filePath}`);
1760
+ console.log(chalk.gray(` ${err.error}`));
1761
+ }
1762
+ if (errors.length > 5) {
1763
+ console.log(chalk.gray(` ... and ${errors.length - 5} more`));
1764
+ }
1765
+ console.log();
1766
+ }
1767
+ function printFooter(score) {
1768
+ console.log(
1769
+ chalk.gray("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550")
1770
+ );
1771
+ const timestamp = new Date(score.timestamp).toLocaleString();
1772
+ console.log(chalk.gray(`Generated: ${timestamp}`));
1773
+ if (score.commitHash) {
1774
+ console.log(chalk.gray(`Commit: ${score.commitHash.slice(0, 8)}`));
1775
+ }
1776
+ if (score.subset) {
1777
+ console.log(chalk.yellow(`Subset analysis: ${score.filesGlob}`));
1778
+ }
1779
+ console.log();
1780
+ }
1781
+ function createProgressBar(percent, width) {
1782
+ const filled = Math.round(percent / 100 * width);
1783
+ const empty = width - filled;
1784
+ const filledChar = "\u2588";
1785
+ const emptyChar = "\u2591";
1786
+ const bar = filledChar.repeat(filled) + emptyChar.repeat(empty);
1787
+ if (percent >= 70) {
1788
+ return chalk.green(bar);
1789
+ } else if (percent >= 50) {
1790
+ return chalk.yellow(bar);
1791
+ } else {
1792
+ return chalk.red(bar);
1793
+ }
1794
+ }
1795
+ function generateSummaryLine(score) {
1796
+ const parts = [
1797
+ `FCIS Health: ${score.health.toFixed(0)}%`,
1798
+ `Purity: ${score.purity.toFixed(0)}%`
1799
+ ];
1800
+ if (score.impurityQuality !== null) {
1801
+ parts.push(`Impurity Quality: ${score.impurityQuality.toFixed(0)}%`);
1802
+ }
1803
+ return parts.join(" | ");
1804
+ }
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
+ function padEnd(str, length) {
1814
+ if (str.length >= length) return str.slice(0, length);
1815
+ return str + " ".repeat(length - str.length);
1816
+ }
1817
+ function printFileReport(fileScore) {
1818
+ const healthColor = fileScore.health >= 70 ? chalk.green : fileScore.health >= 50 ? chalk.yellow : chalk.red;
1819
+ const total = fileScore.pureCount + fileScore.impureCount;
1820
+ const relativePath = toRelativePath(fileScore.filePath);
1821
+ console.log();
1822
+ console.log(chalk.bold(relativePath));
1823
+ console.log(
1824
+ ` Health: ${healthColor(fileScore.health.toFixed(0) + "%")} | Purity: ${fileScore.purity.toFixed(0)}% | Functions: ${fileScore.pureCount}/${total} pure`
1825
+ );
1826
+ if (fileScore.functions.length > 0 && fileScore.impureCount > 0) {
1827
+ console.log();
1828
+ console.log(" Functions:");
1829
+ for (const fn of fileScore.functions) {
1830
+ const statusEmoji = getStatusEmoji(fn.status);
1831
+ const statusColor = getStatusColor(fn.status);
1832
+ const colorFn = statusColor === "green" ? chalk.green : statusColor === "yellow" ? chalk.yellow : chalk.red;
1833
+ const name = fn.name ?? "<anonymous>";
1834
+ const classification = fn.classification === "pure" ? chalk.green("pure") : chalk.yellow("impure");
1835
+ const quality = fn.qualityScore !== null ? ` (${fn.qualityScore})` : "";
1836
+ console.log(
1837
+ ` ${colorFn(statusEmoji)} ${name} \u2014 ${classification}${quality}`
1838
+ );
1839
+ }
1840
+ }
1841
+ }
1842
+
1843
+ export { DEFAULT_QUALITY_THRESHOLDS, MARKER_CATALOG, QUALITY_WEIGHTS, analyze, analyzeFile, analyzeFilePath, analyzeFunction as analyzeFunctionQuality, calculateDelta, checkThresholds, classifyFunction, computeQualityScore, createClassifiedFunction, createDetectionContext, createSummaryJson, deriveStatus, detectMarkers, extractFunctions, extractImports, generateComparisonReport, generateJsonReport, generateSummaryLine, getAllMarkerTypes, getCommitHash, getDiagnosticInsights, getMarkerDefinition, getPureImportedFunctions, getStatusColor, getStatusDescription, getStatusEmoji, groupFilesByDirectory, isTypeOnlyFile, loadProject, printConsoleReport, printFileReport, readJsonReport, scoreDirectory, scoreFile, scoreProject, shouldExcludeFunction, writeJsonReport };
1844
+ //# sourceMappingURL=index.js.map
1845
+ //# sourceMappingURL=index.js.map