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
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Quality Scorer - Pure Core
3
+ *
4
+ * Computes quality scores (0-100) for impure functions measuring how
5
+ * well-structured the impurity is. This is a PURE module - all functions
6
+ * take data in and return data out with no I/O.
7
+ *
8
+ * Quality signals measure structural quality, not whether impurity exists.
9
+ * A high score means the impure function is well-organized and maintainable.
10
+ */
11
+
12
+ import type {
13
+ CallSite,
14
+ DetectionContext,
15
+ ExtractedFunction,
16
+ ImpurityMarker,
17
+ MarkerType,
18
+ } from '../types.js'
19
+
20
+ /**
21
+ * Quality scoring weights and thresholds
22
+ */
23
+ export const QUALITY_WEIGHTS = {
24
+ // Positive signals (additive)
25
+ callsPureFile: 30, // Calls a function from .pure.ts file
26
+ callsPureNamingConvention: 20, // Calls plan*/derive*/compute*/calculate*/transform*
27
+ ioConcentratedAtStart: 15, // I/O calls concentrated at start (GATHER pattern)
28
+ ioConcentratedAtEnd: 15, // I/O calls concentrated at end (EXECUTE pattern)
29
+ lowComplexity: 10, // Low cyclomatic complexity (≤ 5)
30
+ shellNamingConvention: 5, // Function name matches shell conventions
31
+ callsPredicateFunctions: 5, // Calls is*/has*/should* functions
32
+
33
+ // Negative signals (penalties)
34
+ ioInterleaved: -20, // I/O calls interleaved throughout
35
+ highComplexity: -15, // High cyclomatic complexity (> 10)
36
+ multipleIoTypes: -10, // Multiple I/O types in same function
37
+ noPureFunctionCalls: -10, // No pure function calls at all
38
+ veryLongFunction: -10, // > 100 lines
39
+ } as const
40
+
41
+ /**
42
+ * Pure function naming patterns
43
+ */
44
+ const PURE_FUNCTION_PATTERNS = [
45
+ /^plan[A-Z]/,
46
+ /^derive[A-Z]/,
47
+ /^compute[A-Z]/,
48
+ /^calculate[A-Z]/,
49
+ /^transform[A-Z]/,
50
+ /^build[A-Z]/,
51
+ /^create[A-Z](?!.*Service)/, // createX but not createXService
52
+ /^parse[A-Z]/,
53
+ /^format[A-Z]/,
54
+ /^validate[A-Z]/,
55
+ /^normalize[A-Z]/,
56
+ /^convert[A-Z]/,
57
+ /^map[A-Z]/,
58
+ /^filter[A-Z]/,
59
+ /^reduce[A-Z]/,
60
+ /^merge[A-Z]/,
61
+ /^extract[A-Z]/,
62
+ /^to[A-Z]/, // toPublicX, toDTO, etc.
63
+ ]
64
+
65
+ /**
66
+ * Predicate function patterns (return boolean)
67
+ */
68
+ const PREDICATE_PATTERNS = [
69
+ /^is[A-Z]/,
70
+ /^has[A-Z]/,
71
+ /^should[A-Z]/,
72
+ /^can[A-Z]/,
73
+ /^will[A-Z]/,
74
+ /^was[A-Z]/,
75
+ /^did[A-Z]/,
76
+ /^are[A-Z]/,
77
+ /^does[A-Z]/,
78
+ /^needs[A-Z]/,
79
+ ]
80
+
81
+ /**
82
+ * Shell function naming patterns
83
+ */
84
+ const SHELL_FUNCTION_PATTERNS = [
85
+ /^handle[A-Z]/,
86
+ /^fetch[A-Z]/,
87
+ /^save[A-Z]/,
88
+ /^send[A-Z]/,
89
+ /^load[A-Z]/,
90
+ /^get[A-Z]/, // getData, getUser, etc.
91
+ /^set[A-Z]/,
92
+ /^update[A-Z]/,
93
+ /^delete[A-Z]/,
94
+ /^remove[A-Z]/,
95
+ /^create[A-Z].*Service/, // createXService
96
+ /^init[A-Z]/,
97
+ /^process[A-Z]/,
98
+ /^execute[A-Z]/,
99
+ /^run[A-Z]/,
100
+ /^perform[A-Z]/,
101
+ ]
102
+
103
+ /**
104
+ * I/O marker types for concentration analysis
105
+ */
106
+ const IO_MARKER_TYPES: Set<MarkerType> = new Set([
107
+ 'await-expression',
108
+ 'database-call',
109
+ 'network-fetch',
110
+ 'network-http',
111
+ 'fs-call',
112
+ ])
113
+
114
+ /**
115
+ * Compute quality score for an impure function
116
+ *
117
+ * @param fn - The extracted function data
118
+ * @param markers - The detected impurity markers
119
+ * @param context - Detection context with import information
120
+ * @returns Quality score from 0-100
121
+ */
122
+ export function computeQualityScore(
123
+ fn: ExtractedFunction,
124
+ markers: ImpurityMarker[],
125
+ context: DetectionContext,
126
+ ): number {
127
+ let score = 50 // Start at neutral
128
+
129
+ // Analyze the function and accumulate score adjustments
130
+ const analysis = analyzeFunction(fn, markers, context)
131
+
132
+ // Apply positive signals
133
+ if (analysis.callsPureFile) {
134
+ score += QUALITY_WEIGHTS.callsPureFile
135
+ }
136
+ if (analysis.callsPureNamingConvention) {
137
+ score += QUALITY_WEIGHTS.callsPureNamingConvention
138
+ }
139
+ if (analysis.ioConcentratedAtStart) {
140
+ score += QUALITY_WEIGHTS.ioConcentratedAtStart
141
+ }
142
+ if (analysis.ioConcentratedAtEnd) {
143
+ score += QUALITY_WEIGHTS.ioConcentratedAtEnd
144
+ }
145
+ if (analysis.lowComplexity) {
146
+ score += QUALITY_WEIGHTS.lowComplexity
147
+ }
148
+ if (analysis.shellNamingConvention) {
149
+ score += QUALITY_WEIGHTS.shellNamingConvention
150
+ }
151
+ if (analysis.callsPredicateFunctions) {
152
+ score += QUALITY_WEIGHTS.callsPredicateFunctions
153
+ }
154
+
155
+ // Apply negative signals (penalties)
156
+ if (analysis.ioInterleaved) {
157
+ score += QUALITY_WEIGHTS.ioInterleaved
158
+ }
159
+ if (analysis.highComplexity) {
160
+ score += QUALITY_WEIGHTS.highComplexity
161
+ }
162
+ if (analysis.multipleIoTypes) {
163
+ score += QUALITY_WEIGHTS.multipleIoTypes
164
+ }
165
+ if (analysis.noPureFunctionCalls) {
166
+ score += QUALITY_WEIGHTS.noPureFunctionCalls
167
+ }
168
+ if (analysis.veryLongFunction) {
169
+ score += QUALITY_WEIGHTS.veryLongFunction
170
+ }
171
+
172
+ // Clamp score to 0-100
173
+ return Math.max(0, Math.min(100, score))
174
+ }
175
+
176
+ /**
177
+ * Analysis result for a function
178
+ */
179
+ export type FunctionAnalysis = {
180
+ // Positive signals
181
+ callsPureFile: boolean
182
+ callsPureNamingConvention: boolean
183
+ ioConcentratedAtStart: boolean
184
+ ioConcentratedAtEnd: boolean
185
+ lowComplexity: boolean
186
+ shellNamingConvention: boolean
187
+ callsPredicateFunctions: boolean
188
+
189
+ // Negative signals
190
+ ioInterleaved: boolean
191
+ highComplexity: boolean
192
+ multipleIoTypes: boolean
193
+ noPureFunctionCalls: boolean
194
+ veryLongFunction: boolean
195
+
196
+ // Metrics
197
+ estimatedComplexity: number
198
+ ioMarkerCount: number
199
+ uniqueIoTypes: Set<MarkerType>
200
+ pureCallCount: number
201
+ predicateCallCount: number
202
+ }
203
+
204
+ /**
205
+ * Analyze a function for quality signals
206
+ */
207
+ export function analyzeFunction(
208
+ fn: ExtractedFunction,
209
+ markers: ImpurityMarker[],
210
+ context: DetectionContext,
211
+ ): FunctionAnalysis {
212
+ // Get I/O markers with line information
213
+ const ioMarkers = markers.filter(
214
+ m => IO_MARKER_TYPES.has(m.type) && m.line !== undefined,
215
+ )
216
+
217
+ // Unique I/O types
218
+ const uniqueIoTypes = new Set(markers.map(m => m.type))
219
+
220
+ // Check for pure file imports being called
221
+ const callsPureFile = checkCallsPureFile(fn, context)
222
+
223
+ // Check for pure naming convention calls
224
+ const { callsPureNaming, pureCallCount } = checkCallsPureNamingConvention(
225
+ fn.callSites,
226
+ )
227
+
228
+ // Check for predicate function calls
229
+ const { callsPredicates, predicateCallCount } = checkCallsPredicateFunctions(
230
+ fn.callSites,
231
+ )
232
+
233
+ // Check I/O concentration
234
+ const { concentratedAtStart, concentratedAtEnd, interleaved } =
235
+ analyzeIoConcentration(fn, ioMarkers)
236
+
237
+ // Estimate complexity
238
+ const estimatedComplexity = estimateCyclomaticComplexity(fn)
239
+
240
+ // Check shell naming convention
241
+ const shellNamingConvention = checkShellNamingConvention(fn.name)
242
+
243
+ return {
244
+ callsPureFile,
245
+ callsPureNamingConvention: callsPureNaming,
246
+ ioConcentratedAtStart: concentratedAtStart,
247
+ ioConcentratedAtEnd: concentratedAtEnd,
248
+ lowComplexity: estimatedComplexity <= 5,
249
+ shellNamingConvention,
250
+ callsPredicateFunctions: callsPredicates,
251
+
252
+ ioInterleaved: interleaved,
253
+ highComplexity: estimatedComplexity > 10,
254
+ multipleIoTypes: uniqueIoTypes.size >= 3,
255
+ noPureFunctionCalls: pureCallCount === 0 && predicateCallCount === 0,
256
+ veryLongFunction: fn.bodyLineCount > 100,
257
+
258
+ estimatedComplexity,
259
+ ioMarkerCount: ioMarkers.length,
260
+ uniqueIoTypes,
261
+ pureCallCount,
262
+ predicateCallCount,
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Check if function calls any imports from .pure files
268
+ */
269
+ function checkCallsPureFile(
270
+ fn: ExtractedFunction,
271
+ context: DetectionContext,
272
+ ): boolean {
273
+ // Get names of functions imported from .pure files
274
+ const pureImports = new Set<string>()
275
+ for (const imp of context.imports.imports) {
276
+ if (context.pureFileImports.has(imp.moduleSpecifier)) {
277
+ for (const name of imp.namedImports) {
278
+ pureImports.add(name)
279
+ }
280
+ }
281
+ }
282
+
283
+ // Check if any call site calls a pure import
284
+ for (const callSite of fn.callSites) {
285
+ const calledFn = extractFunctionName(callSite.expression)
286
+ if (pureImports.has(calledFn)) {
287
+ return true
288
+ }
289
+ }
290
+
291
+ return false
292
+ }
293
+
294
+ /**
295
+ * Check if function calls functions with pure naming conventions
296
+ */
297
+ function checkCallsPureNamingConvention(callSites: CallSite[]): {
298
+ callsPureNaming: boolean
299
+ pureCallCount: number
300
+ } {
301
+ let pureCallCount = 0
302
+
303
+ for (const callSite of callSites) {
304
+ const calledFn = extractFunctionName(callSite.expression)
305
+ if (matchesAnyPattern(calledFn, PURE_FUNCTION_PATTERNS)) {
306
+ pureCallCount++
307
+ }
308
+ }
309
+
310
+ return {
311
+ callsPureNaming: pureCallCount > 0,
312
+ pureCallCount,
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Check if function calls predicate functions
318
+ */
319
+ function checkCallsPredicateFunctions(callSites: CallSite[]): {
320
+ callsPredicates: boolean
321
+ predicateCallCount: number
322
+ } {
323
+ let predicateCallCount = 0
324
+
325
+ for (const callSite of callSites) {
326
+ const calledFn = extractFunctionName(callSite.expression)
327
+ if (matchesAnyPattern(calledFn, PREDICATE_PATTERNS)) {
328
+ predicateCallCount++
329
+ }
330
+ }
331
+
332
+ return {
333
+ callsPredicates: predicateCallCount > 0,
334
+ predicateCallCount,
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Analyze I/O concentration within a function
340
+ *
341
+ * Good FCIS patterns:
342
+ * - GATHER: I/O at the start, then pure logic
343
+ * - EXECUTE: Pure logic, then I/O at the end
344
+ * - GATHER-DECIDE-EXECUTE: I/O at start, pure in middle, I/O at end
345
+ *
346
+ * Bad pattern:
347
+ * - Interleaved: I/O calls scattered throughout
348
+ */
349
+ function analyzeIoConcentration(
350
+ fn: ExtractedFunction,
351
+ ioMarkers: ImpurityMarker[],
352
+ ): {
353
+ concentratedAtStart: boolean
354
+ concentratedAtEnd: boolean
355
+ interleaved: boolean
356
+ } {
357
+ if (ioMarkers.length === 0) {
358
+ return {
359
+ concentratedAtStart: false,
360
+ concentratedAtEnd: false,
361
+ interleaved: false,
362
+ }
363
+ }
364
+
365
+ // Get I/O positions as percentages through the function
366
+ const positions = ioMarkers
367
+ .map(m => (m.line ?? 0) / Math.max(fn.bodyLineCount, 1))
368
+ .sort((a, b) => a - b)
369
+
370
+ // Calculate concentration metrics
371
+ const startThreshold = 0.33 // First third
372
+ const endThreshold = 0.67 // Last third
373
+
374
+ const atStart = positions.filter(p => p <= startThreshold).length
375
+ const atEnd = positions.filter(p => p >= endThreshold).length
376
+ const inMiddle = positions.filter(
377
+ p => p > startThreshold && p < endThreshold,
378
+ ).length
379
+
380
+ const total = positions.length
381
+
382
+ // Concentrated at start: most I/O in first third
383
+ const concentratedAtStart = atStart >= total * 0.6 && inMiddle <= 1
384
+
385
+ // Concentrated at end: most I/O in last third
386
+ const concentratedAtEnd = atEnd >= total * 0.6 && inMiddle <= 1
387
+
388
+ // Interleaved: I/O scattered throughout
389
+ // Consider it interleaved if there's significant I/O in all sections
390
+ // or if there are many transitions between I/O and non-I/O regions
391
+ const interleaved =
392
+ inMiddle >= 2 ||
393
+ (atStart > 0 && atEnd > 0 && inMiddle > 0) ||
394
+ (total >= 4 && !concentratedAtStart && !concentratedAtEnd)
395
+
396
+ return {
397
+ concentratedAtStart,
398
+ concentratedAtEnd,
399
+ interleaved,
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Estimate cyclomatic complexity from extracted function data
405
+ *
406
+ * Cyclomatic complexity = 1 + number of decision points
407
+ * Since we don't have full AST access here, we estimate based on:
408
+ * - hasConditionals flag
409
+ * - statement count
410
+ * - body line count
411
+ */
412
+ function estimateCyclomaticComplexity(fn: ExtractedFunction): number {
413
+ let complexity = 1 // Base complexity
414
+
415
+ // If has conditionals, estimate based on statement density
416
+ if (fn.hasConditionals) {
417
+ // Rough estimate: conditionals add 1-2 per ~10 statements
418
+ complexity += Math.ceil(fn.statementCount / 10)
419
+ }
420
+
421
+ // Factor in line count for very long functions
422
+ if (fn.bodyLineCount > 50) {
423
+ complexity += Math.floor((fn.bodyLineCount - 50) / 25)
424
+ }
425
+
426
+ return complexity
427
+ }
428
+
429
+ /**
430
+ * Check if function name matches shell naming conventions
431
+ */
432
+ function checkShellNamingConvention(name: string | null): boolean {
433
+ if (!name) return false
434
+ return matchesAnyPattern(name, SHELL_FUNCTION_PATTERNS)
435
+ }
436
+
437
+ /**
438
+ * Extract the function name from a call expression
439
+ * e.g., "db.user.findFirst" -> "findFirst"
440
+ * "planAcceptInvite" -> "planAcceptInvite"
441
+ * "this.validate" -> "validate"
442
+ */
443
+ function extractFunctionName(expression: string): string {
444
+ const parts = expression.split('.')
445
+ return parts[parts.length - 1] ?? expression
446
+ }
447
+
448
+ /**
449
+ * Check if a string matches any of the given patterns
450
+ */
451
+ function matchesAnyPattern(str: string, patterns: RegExp[]): boolean {
452
+ return patterns.some(pattern => pattern.test(str))
453
+ }
454
+
455
+ /**
456
+ * Get quality score interpretation
457
+ */
458
+ export function interpretQualityScore(score: number): {
459
+ level: 'high' | 'medium' | 'low'
460
+ description: string
461
+ } {
462
+ if (score >= 70) {
463
+ return {
464
+ level: 'high',
465
+ description:
466
+ 'Well-structured impure function. I/O is organized and pure logic is separated.',
467
+ }
468
+ } else if (score >= 40) {
469
+ return {
470
+ level: 'medium',
471
+ description:
472
+ 'Moderately structured. Could benefit from extracting pure logic.',
473
+ }
474
+ } else {
475
+ return {
476
+ level: 'low',
477
+ description:
478
+ 'Tangled structure. Business logic mixed with I/O. Consider refactoring.',
479
+ }
480
+ }
481
+ }