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,650 @@
1
+ /**
2
+ * Quality Scorer Tests
3
+ *
4
+ * Tests for the pure core quality scoring logic.
5
+ * These tests verify that quality scores are correctly computed
6
+ * for impure functions based on their structural patterns.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+
11
+ import {
12
+ computeQualityScore,
13
+ analyzeFunction,
14
+ QUALITY_WEIGHTS,
15
+ } from '../src/classification/quality-scorer.js'
16
+ import type {
17
+ ExtractedFunction,
18
+ ImpurityMarker,
19
+ DetectionContext,
20
+ FileImports,
21
+ } from '../src/types.js'
22
+ import { createDetectionContext } from '../src/detection/detect-markers.js'
23
+
24
+ /**
25
+ * Helper to create a minimal extracted function for testing
26
+ */
27
+ function createTestFunction(
28
+ overrides: Partial<ExtractedFunction> = {},
29
+ ): ExtractedFunction {
30
+ return {
31
+ name: 'testFunction',
32
+ filePath: '/test/file.ts',
33
+ startLine: 1,
34
+ endLine: 10,
35
+ isAsync: true,
36
+ isExported: false,
37
+ bodyLineCount: 10,
38
+ statementCount: 5,
39
+ hasConditionals: false,
40
+ parentContext: null,
41
+ callSites: [],
42
+ hasAwait: true,
43
+ propertyAccessChains: [],
44
+ kind: 'function',
45
+ ...overrides,
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Helper to create a minimal detection context
51
+ */
52
+ function createTestContext(
53
+ imports: FileImports['imports'] = [],
54
+ pureImports: string[] = [],
55
+ ): DetectionContext {
56
+ const fileImports: FileImports = {
57
+ filePath: '/test/file.ts',
58
+ imports,
59
+ }
60
+ const context = createDetectionContext(fileImports)
61
+
62
+ // Add pure file imports
63
+ for (const pureImport of pureImports) {
64
+ context.pureFileImports.add(pureImport)
65
+ }
66
+
67
+ return context
68
+ }
69
+
70
+ /**
71
+ * Helper to create markers
72
+ */
73
+ function createMarkers(
74
+ types: Array<{ type: ImpurityMarker['type']; line?: number }>,
75
+ ): ImpurityMarker[] {
76
+ return types.map(({ type, line }) => ({
77
+ type,
78
+ detail: `test ${type}`,
79
+ line,
80
+ }))
81
+ }
82
+
83
+ describe('computeQualityScore', () => {
84
+ describe('baseline scoring', () => {
85
+ it('should return a score between 0 and 100', () => {
86
+ const fn = createTestFunction()
87
+ const markers = createMarkers([{ type: 'await-expression', line: 5 }])
88
+ const context = createTestContext()
89
+
90
+ const score = computeQualityScore(fn, markers, context)
91
+
92
+ expect(score).toBeGreaterThanOrEqual(0)
93
+ expect(score).toBeLessThanOrEqual(100)
94
+ })
95
+
96
+ it('should start with neutral score of 50', () => {
97
+ const fn = createTestFunction({
98
+ bodyLineCount: 20,
99
+ statementCount: 10,
100
+ callSites: [],
101
+ })
102
+ const markers = createMarkers([{ type: 'await-expression', line: 10 }])
103
+ const context = createTestContext()
104
+
105
+ const score = computeQualityScore(fn, markers, context)
106
+
107
+ // Base score is 50, with various adjustments
108
+ // Without any positive signals, it should get penalties
109
+ expect(score).toBeLessThanOrEqual(50)
110
+ })
111
+ })
112
+
113
+ describe('positive signals', () => {
114
+ it('should give high score for calling .pure file imports', () => {
115
+ const fn = createTestFunction({
116
+ callSites: [
117
+ { expression: 'planAcceptInvite', line: 5, isAwaited: false },
118
+ { expression: 'db.user.findFirst', line: 2, isAwaited: true },
119
+ ],
120
+ })
121
+ const markers = createMarkers([
122
+ { type: 'database-call', line: 2 },
123
+ { type: 'await-expression', line: 2 },
124
+ ])
125
+ const context = createTestContext(
126
+ [
127
+ {
128
+ moduleSpecifier: './organizations.pure',
129
+ namedImports: ['planAcceptInvite'],
130
+ },
131
+ ],
132
+ ['./organizations.pure'],
133
+ )
134
+
135
+ const score = computeQualityScore(fn, markers, context)
136
+
137
+ // Should get +30 for calling pure file
138
+ expect(score).toBeGreaterThanOrEqual(70)
139
+ })
140
+
141
+ it('should reward calling functions with pure naming conventions', () => {
142
+ const fn = createTestFunction({
143
+ callSites: [
144
+ { expression: 'deriveSettings', line: 3, isAwaited: false },
145
+ { expression: 'computeTotal', line: 4, isAwaited: false },
146
+ { expression: 'db.user.update', line: 6, isAwaited: true },
147
+ ],
148
+ })
149
+ const markers = createMarkers([{ type: 'database-call', line: 6 }])
150
+ const context = createTestContext()
151
+
152
+ const score = computeQualityScore(fn, markers, context)
153
+
154
+ // Should get +20 for pure naming conventions
155
+ expect(score).toBeGreaterThanOrEqual(60)
156
+ })
157
+
158
+ it('should reward calling predicate functions', () => {
159
+ const fn = createTestFunction({
160
+ callSites: [
161
+ { expression: 'isValid', line: 2, isAwaited: false },
162
+ { expression: 'hasPermission', line: 3, isAwaited: false },
163
+ { expression: 'shouldProcess', line: 4, isAwaited: false },
164
+ { expression: 'db.user.update', line: 6, isAwaited: true },
165
+ ],
166
+ })
167
+ const markers = createMarkers([{ type: 'database-call', line: 6 }])
168
+ const context = createTestContext()
169
+
170
+ const score = computeQualityScore(fn, markers, context)
171
+
172
+ // Should get +5 for predicate functions
173
+ expect(score).toBeGreaterThanOrEqual(50)
174
+ })
175
+
176
+ it('should reward shell naming conventions', () => {
177
+ const fn = createTestFunction({
178
+ name: 'handleUserUpdate',
179
+ callSites: [{ expression: 'db.user.update', line: 5, isAwaited: true }],
180
+ })
181
+ const markers = createMarkers([{ type: 'database-call', line: 5 }])
182
+ const context = createTestContext()
183
+
184
+ const score = computeQualityScore(fn, markers, context)
185
+
186
+ // Should get +5 for shell naming
187
+ expect(score).toBeGreaterThanOrEqual(40)
188
+ })
189
+
190
+ it('should reward I/O concentrated at start (GATHER pattern)', () => {
191
+ const fn = createTestFunction({
192
+ bodyLineCount: 30,
193
+ callSites: [
194
+ { expression: 'db.user.findFirst', line: 2, isAwaited: true },
195
+ { expression: 'db.posts.findMany', line: 3, isAwaited: true },
196
+ { expression: 'transform', line: 15, isAwaited: false },
197
+ { expression: 'validate', line: 20, isAwaited: false },
198
+ ],
199
+ })
200
+ const markers = createMarkers([
201
+ { type: 'database-call', line: 2 },
202
+ { type: 'database-call', line: 3 },
203
+ ])
204
+ const context = createTestContext()
205
+
206
+ const score = computeQualityScore(fn, markers, context)
207
+
208
+ // Should get bonus for GATHER pattern
209
+ expect(score).toBeGreaterThanOrEqual(55)
210
+ })
211
+
212
+ it('should reward I/O concentrated at end (EXECUTE pattern)', () => {
213
+ const fn = createTestFunction({
214
+ bodyLineCount: 30,
215
+ callSites: [
216
+ { expression: 'computeResult', line: 5, isAwaited: false },
217
+ { expression: 'validateInput', line: 10, isAwaited: false },
218
+ { expression: 'db.user.update', line: 25, isAwaited: true },
219
+ { expression: 'db.audit.create', line: 28, isAwaited: true },
220
+ ],
221
+ })
222
+ const markers = createMarkers([
223
+ { type: 'database-call', line: 25 },
224
+ { type: 'database-call', line: 28 },
225
+ ])
226
+ const context = createTestContext()
227
+
228
+ const score = computeQualityScore(fn, markers, context)
229
+
230
+ // Should get bonus for EXECUTE pattern
231
+ expect(score).toBeGreaterThanOrEqual(55)
232
+ })
233
+
234
+ it('should reward low complexity functions', () => {
235
+ const fn = createTestFunction({
236
+ bodyLineCount: 10,
237
+ statementCount: 3,
238
+ hasConditionals: false,
239
+ callSites: [
240
+ { expression: 'db.user.findFirst', line: 5, isAwaited: true },
241
+ ],
242
+ })
243
+ const markers = createMarkers([{ type: 'database-call', line: 5 }])
244
+ const context = createTestContext()
245
+
246
+ const score = computeQualityScore(fn, markers, context)
247
+
248
+ // Should get +10 for low complexity
249
+ expect(score).toBeGreaterThanOrEqual(50)
250
+ })
251
+ })
252
+
253
+ describe('negative signals (penalties)', () => {
254
+ it('should penalize interleaved I/O', () => {
255
+ const fn = createTestFunction({
256
+ bodyLineCount: 30,
257
+ callSites: [
258
+ { expression: 'db.user.findFirst', line: 3, isAwaited: true },
259
+ { expression: 'transform', line: 8, isAwaited: false },
260
+ { expression: 'db.posts.create', line: 12, isAwaited: true },
261
+ { expression: 'validate', line: 18, isAwaited: false },
262
+ { expression: 'db.audit.create', line: 25, isAwaited: true },
263
+ ],
264
+ })
265
+ const markers = createMarkers([
266
+ { type: 'database-call', line: 3 },
267
+ { type: 'database-call', line: 12 },
268
+ { type: 'database-call', line: 25 },
269
+ ])
270
+ const context = createTestContext()
271
+
272
+ const score = computeQualityScore(fn, markers, context)
273
+
274
+ // Should get -20 for interleaved I/O
275
+ expect(score).toBeLessThanOrEqual(50)
276
+ })
277
+
278
+ it('should penalize high complexity functions', () => {
279
+ const fn = createTestFunction({
280
+ bodyLineCount: 150,
281
+ statementCount: 50,
282
+ hasConditionals: true,
283
+ callSites: [
284
+ { expression: 'db.user.findFirst', line: 50, isAwaited: true },
285
+ ],
286
+ })
287
+ const markers = createMarkers([{ type: 'database-call', line: 50 }])
288
+ const context = createTestContext()
289
+
290
+ const score = computeQualityScore(fn, markers, context)
291
+
292
+ // Should get -15 for high complexity
293
+ expect(score).toBeLessThanOrEqual(40)
294
+ })
295
+
296
+ it('should penalize multiple I/O types', () => {
297
+ const fn = createTestFunction({
298
+ callSites: [
299
+ { expression: 'db.user.findFirst', line: 2, isAwaited: true },
300
+ { expression: 'fetch', line: 5, isAwaited: true },
301
+ { expression: 'fs.readFile', line: 8, isAwaited: true },
302
+ ],
303
+ })
304
+ const markers = createMarkers([
305
+ { type: 'database-call', line: 2 },
306
+ { type: 'network-fetch', line: 5 },
307
+ { type: 'fs-call', line: 8 },
308
+ ])
309
+ const context = createTestContext()
310
+
311
+ const score = computeQualityScore(fn, markers, context)
312
+
313
+ // Should get -10 for multiple I/O types
314
+ expect(score).toBeLessThanOrEqual(50)
315
+ })
316
+
317
+ it('should penalize no pure function calls', () => {
318
+ const fn = createTestFunction({
319
+ callSites: [
320
+ { expression: 'db.user.findFirst', line: 2, isAwaited: true },
321
+ { expression: 'db.user.update', line: 5, isAwaited: true },
322
+ ],
323
+ })
324
+ const markers = createMarkers([
325
+ { type: 'database-call', line: 2 },
326
+ { type: 'database-call', line: 5 },
327
+ ])
328
+ const context = createTestContext()
329
+
330
+ const score = computeQualityScore(fn, markers, context)
331
+
332
+ // Should get -10 for no pure function calls
333
+ expect(score).toBeLessThanOrEqual(50)
334
+ })
335
+
336
+ it('should penalize very long functions', () => {
337
+ const fn = createTestFunction({
338
+ bodyLineCount: 150,
339
+ callSites: [
340
+ { expression: 'db.user.findFirst', line: 50, isAwaited: true },
341
+ ],
342
+ })
343
+ const markers = createMarkers([{ type: 'database-call', line: 50 }])
344
+ const context = createTestContext()
345
+
346
+ const score = computeQualityScore(fn, markers, context)
347
+
348
+ // Should get -10 for very long function
349
+ expect(score).toBeLessThanOrEqual(40)
350
+ })
351
+ })
352
+
353
+ describe('combined scenarios', () => {
354
+ it('should give high score for well-structured impure function', () => {
355
+ // This simulates the acceptInvite pattern from organizations.ts
356
+ // - Calls a pure function (planAcceptInvite)
357
+ // - I/O at the end (EXECUTE pattern)
358
+ // - Shell naming convention (handleX)
359
+ const fn = createTestFunction({
360
+ name: 'handleAcceptInvite',
361
+ bodyLineCount: 30,
362
+ statementCount: 8,
363
+ hasConditionals: true,
364
+ callSites: [
365
+ { expression: 'planAcceptInvite', line: 5, isAwaited: false },
366
+ { expression: 'db.invitation.update', line: 22, isAwaited: true },
367
+ { expression: 'db.member.create', line: 25, isAwaited: true },
368
+ ],
369
+ })
370
+ const markers = createMarkers([
371
+ { type: 'database-call', line: 22 },
372
+ { type: 'database-call', line: 25 },
373
+ ])
374
+ const context = createTestContext(
375
+ [
376
+ {
377
+ moduleSpecifier: './organizations.pure',
378
+ namedImports: ['planAcceptInvite'],
379
+ },
380
+ ],
381
+ ['./organizations.pure'],
382
+ )
383
+
384
+ const score = computeQualityScore(fn, markers, context)
385
+
386
+ expect(score).toBeGreaterThanOrEqual(70)
387
+ })
388
+
389
+ it('should give low score for tangled impure function', () => {
390
+ // This simulates a poorly structured function
391
+ // - No pure function calls
392
+ // - I/O interleaved throughout
393
+ // - High complexity
394
+ // - Multiple I/O types
395
+ const fn = createTestFunction({
396
+ name: 'processData',
397
+ bodyLineCount: 100,
398
+ statementCount: 40,
399
+ hasConditionals: true,
400
+ callSites: [
401
+ { expression: 'db.user.findFirst', line: 5, isAwaited: true },
402
+ { expression: 'fetch', line: 20, isAwaited: true },
403
+ { expression: 'db.cache.get', line: 35, isAwaited: true },
404
+ { expression: 'console.log', line: 50, isAwaited: false },
405
+ { expression: 'db.result.create', line: 70, isAwaited: true },
406
+ { expression: 'fetch', line: 85, isAwaited: true },
407
+ ],
408
+ })
409
+ const markers = createMarkers([
410
+ { type: 'database-call', line: 5 },
411
+ { type: 'network-fetch', line: 20 },
412
+ { type: 'database-call', line: 35 },
413
+ { type: 'console-log', line: 50 },
414
+ { type: 'database-call', line: 70 },
415
+ { type: 'network-fetch', line: 85 },
416
+ ])
417
+ const context = createTestContext()
418
+
419
+ const score = computeQualityScore(fn, markers, context)
420
+
421
+ expect(score).toBeLessThan(40)
422
+ })
423
+
424
+ it('should give medium score for moderately structured function', () => {
425
+ // Some good patterns, some bad - interleaved I/O with no pure file calls
426
+ const fn = createTestFunction({
427
+ name: 'updateUser',
428
+ bodyLineCount: 100,
429
+ statementCount: 40,
430
+ hasConditionals: true,
431
+ callSites: [
432
+ { expression: 'db.user.findFirst', line: 10, isAwaited: true },
433
+ { expression: 'someHelper', line: 30, isAwaited: false },
434
+ { expression: 'db.user.update', line: 50, isAwaited: true },
435
+ { expression: 'anotherHelper', line: 70, isAwaited: false },
436
+ { expression: 'db.audit.create', line: 90, isAwaited: true },
437
+ ],
438
+ })
439
+ const markers = createMarkers([
440
+ { type: 'database-call', line: 10 },
441
+ { type: 'database-call', line: 50 },
442
+ { type: 'database-call', line: 90 },
443
+ ])
444
+ const context = createTestContext()
445
+
446
+ const score = computeQualityScore(fn, markers, context)
447
+
448
+ expect(score).toBeGreaterThanOrEqual(0)
449
+ expect(score).toBeLessThan(70)
450
+ })
451
+ })
452
+
453
+ describe('edge cases', () => {
454
+ it('should handle function with no call sites', () => {
455
+ const fn = createTestFunction({
456
+ callSites: [],
457
+ propertyAccessChains: ['process.env.NODE_ENV'],
458
+ })
459
+ const markers = createMarkers([{ type: 'env-access' }])
460
+ const context = createTestContext()
461
+
462
+ const score = computeQualityScore(fn, markers, context)
463
+
464
+ expect(score).toBeGreaterThanOrEqual(0)
465
+ expect(score).toBeLessThanOrEqual(100)
466
+ })
467
+
468
+ it('should handle function with single line', () => {
469
+ const fn = createTestFunction({
470
+ bodyLineCount: 1,
471
+ statementCount: 1,
472
+ callSites: [
473
+ { expression: 'db.user.findFirst', line: 0, isAwaited: true },
474
+ ],
475
+ })
476
+ const markers = createMarkers([{ type: 'database-call', line: 0 }])
477
+ const context = createTestContext()
478
+
479
+ const score = computeQualityScore(fn, markers, context)
480
+
481
+ expect(score).toBeGreaterThanOrEqual(0)
482
+ expect(score).toBeLessThanOrEqual(100)
483
+ })
484
+
485
+ it('should clamp score to 0 for extremely bad functions', () => {
486
+ const fn = createTestFunction({
487
+ name: 'terribleFunction',
488
+ bodyLineCount: 500,
489
+ statementCount: 200,
490
+ hasConditionals: true,
491
+ callSites: Array.from({ length: 20 }, (_, i) => ({
492
+ expression:
493
+ i % 3 === 0
494
+ ? 'db.user.findFirst'
495
+ : i % 3 === 1
496
+ ? 'fetch'
497
+ : 'fs.readFile',
498
+ line: i * 25,
499
+ isAwaited: true,
500
+ })),
501
+ })
502
+ const markers = createMarkers([
503
+ ...Array.from({ length: 7 }, () => ({
504
+ type: 'database-call' as const,
505
+ })),
506
+ ...Array.from({ length: 7 }, () => ({
507
+ type: 'network-fetch' as const,
508
+ })),
509
+ ...Array.from({ length: 6 }, () => ({ type: 'fs-call' as const })),
510
+ ])
511
+ const context = createTestContext()
512
+
513
+ const score = computeQualityScore(fn, markers, context)
514
+
515
+ expect(score).toBeGreaterThanOrEqual(0)
516
+ })
517
+
518
+ it('should clamp score to 100 for extremely good functions', () => {
519
+ const fn = createTestFunction({
520
+ name: 'handlePerfectFunction',
521
+ bodyLineCount: 10,
522
+ statementCount: 3,
523
+ hasConditionals: false,
524
+ callSites: [
525
+ { expression: 'planOperation', line: 2, isAwaited: false },
526
+ { expression: 'deriveResult', line: 3, isAwaited: false },
527
+ { expression: 'computeValue', line: 4, isAwaited: false },
528
+ { expression: 'isValid', line: 5, isAwaited: false },
529
+ { expression: 'db.user.create', line: 8, isAwaited: true },
530
+ ],
531
+ })
532
+ const markers = createMarkers([{ type: 'database-call', line: 8 }])
533
+ const context = createTestContext(
534
+ [{ moduleSpecifier: './domain.pure', namedImports: ['planOperation'] }],
535
+ ['./domain.pure'],
536
+ )
537
+
538
+ const score = computeQualityScore(fn, markers, context)
539
+
540
+ expect(score).toBeLessThanOrEqual(100)
541
+ })
542
+ })
543
+ })
544
+
545
+ describe('analyzeFunction', () => {
546
+ it('should return analysis with all expected fields', () => {
547
+ const fn = createTestFunction({
548
+ callSites: [
549
+ { expression: 'db.user.findFirst', line: 5, isAwaited: true },
550
+ ],
551
+ })
552
+ const markers = createMarkers([{ type: 'database-call', line: 5 }])
553
+ const context = createTestContext()
554
+
555
+ const analysis = analyzeFunction(fn, markers, context)
556
+
557
+ expect(analysis).toHaveProperty('callsPureFile')
558
+ expect(analysis).toHaveProperty('callsPureNamingConvention')
559
+ expect(analysis).toHaveProperty('ioConcentratedAtStart')
560
+ expect(analysis).toHaveProperty('ioConcentratedAtEnd')
561
+ expect(analysis).toHaveProperty('lowComplexity')
562
+ expect(analysis).toHaveProperty('shellNamingConvention')
563
+ expect(analysis).toHaveProperty('callsPredicateFunctions')
564
+ expect(analysis).toHaveProperty('ioInterleaved')
565
+ expect(analysis).toHaveProperty('highComplexity')
566
+ expect(analysis).toHaveProperty('multipleIoTypes')
567
+ expect(analysis).toHaveProperty('noPureFunctionCalls')
568
+ expect(analysis).toHaveProperty('veryLongFunction')
569
+ expect(analysis).toHaveProperty('estimatedComplexity')
570
+ expect(analysis).toHaveProperty('ioMarkerCount')
571
+ expect(analysis).toHaveProperty('uniqueIoTypes')
572
+ expect(analysis).toHaveProperty('pureCallCount')
573
+ expect(analysis).toHaveProperty('predicateCallCount')
574
+ })
575
+
576
+ it('should detect pure file imports correctly', () => {
577
+ const fn = createTestFunction({
578
+ callSites: [
579
+ { expression: 'planAcceptInvite', line: 5, isAwaited: false },
580
+ ],
581
+ })
582
+ const markers: ImpurityMarker[] = []
583
+ const context = createTestContext(
584
+ [
585
+ {
586
+ moduleSpecifier: './organizations.pure',
587
+ namedImports: ['planAcceptInvite'],
588
+ },
589
+ ],
590
+ ['./organizations.pure'],
591
+ )
592
+
593
+ const analysis = analyzeFunction(fn, markers, context)
594
+
595
+ expect(analysis.callsPureFile).toBe(true)
596
+ })
597
+
598
+ it('should detect pure naming conventions', () => {
599
+ const fn = createTestFunction({
600
+ callSites: [
601
+ { expression: 'deriveSettings', line: 2, isAwaited: false },
602
+ { expression: 'computeTotal', line: 3, isAwaited: false },
603
+ { expression: 'transformData', line: 4, isAwaited: false },
604
+ ],
605
+ })
606
+ const markers: ImpurityMarker[] = []
607
+ const context = createTestContext()
608
+
609
+ const analysis = analyzeFunction(fn, markers, context)
610
+
611
+ expect(analysis.callsPureNamingConvention).toBe(true)
612
+ expect(analysis.pureCallCount).toBe(3)
613
+ })
614
+
615
+ it('should detect predicate function calls', () => {
616
+ const fn = createTestFunction({
617
+ callSites: [
618
+ { expression: 'isValid', line: 2, isAwaited: false },
619
+ { expression: 'hasPermission', line: 3, isAwaited: false },
620
+ ],
621
+ })
622
+ const markers: ImpurityMarker[] = []
623
+ const context = createTestContext()
624
+
625
+ const analysis = analyzeFunction(fn, markers, context)
626
+
627
+ expect(analysis.callsPredicateFunctions).toBe(true)
628
+ expect(analysis.predicateCallCount).toBe(2)
629
+ })
630
+ })
631
+
632
+ describe('QUALITY_WEIGHTS', () => {
633
+ it('should have positive weights for good patterns', () => {
634
+ expect(QUALITY_WEIGHTS.callsPureFile).toBeGreaterThan(0)
635
+ expect(QUALITY_WEIGHTS.callsPureNamingConvention).toBeGreaterThan(0)
636
+ expect(QUALITY_WEIGHTS.ioConcentratedAtStart).toBeGreaterThan(0)
637
+ expect(QUALITY_WEIGHTS.ioConcentratedAtEnd).toBeGreaterThan(0)
638
+ expect(QUALITY_WEIGHTS.lowComplexity).toBeGreaterThan(0)
639
+ expect(QUALITY_WEIGHTS.shellNamingConvention).toBeGreaterThan(0)
640
+ expect(QUALITY_WEIGHTS.callsPredicateFunctions).toBeGreaterThan(0)
641
+ })
642
+
643
+ it('should have negative weights for bad patterns', () => {
644
+ expect(QUALITY_WEIGHTS.ioInterleaved).toBeLessThan(0)
645
+ expect(QUALITY_WEIGHTS.highComplexity).toBeLessThan(0)
646
+ expect(QUALITY_WEIGHTS.multipleIoTypes).toBeLessThan(0)
647
+ expect(QUALITY_WEIGHTS.noPureFunctionCalls).toBeLessThan(0)
648
+ expect(QUALITY_WEIGHTS.veryLongFunction).toBeLessThan(0)
649
+ })
650
+ })