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