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,480 @@
1
+ /**
2
+ * Classifier Tests
3
+ *
4
+ * Tests for the pure core classification logic.
5
+ * These tests verify that functions are correctly classified as pure or impure
6
+ * based on their markers.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+
11
+ import {
12
+ classifyFunction,
13
+ shouldExcludeFunction,
14
+ createClassifiedFunction,
15
+ partitionByClassification,
16
+ getClassificationSummary,
17
+ } from '../src/classification/classifier.js'
18
+ import type { ExtractedFunction, ImpurityMarker, Status } from '../src/types.js'
19
+
20
+ /**
21
+ * Helper to create a minimal extracted function for testing
22
+ */
23
+ function createTestFunction(
24
+ overrides: Partial<ExtractedFunction> = {},
25
+ ): ExtractedFunction {
26
+ return {
27
+ name: 'testFunction',
28
+ filePath: '/test/file.ts',
29
+ startLine: 1,
30
+ endLine: 10,
31
+ isAsync: false,
32
+ isExported: false,
33
+ bodyLineCount: 10,
34
+ statementCount: 5,
35
+ hasConditionals: false,
36
+ parentContext: null,
37
+ callSites: [],
38
+ hasAwait: false,
39
+ propertyAccessChains: [],
40
+ kind: 'function',
41
+ ...overrides,
42
+ }
43
+ }
44
+
45
+ describe('classifyFunction', () => {
46
+ describe('pure classification', () => {
47
+ it('should classify function with no markers as pure', () => {
48
+ const fn = createTestFunction()
49
+ const markers: ImpurityMarker[] = []
50
+
51
+ const classification = classifyFunction(fn, markers)
52
+
53
+ expect(classification).toBe('pure')
54
+ })
55
+
56
+ it('should classify async function without await or I/O as pure', () => {
57
+ const fn = createTestFunction({
58
+ isAsync: true,
59
+ hasAwait: false,
60
+ })
61
+ const markers: ImpurityMarker[] = []
62
+
63
+ const classification = classifyFunction(fn, markers)
64
+
65
+ expect(classification).toBe('pure')
66
+ })
67
+ })
68
+
69
+ describe('impure classification', () => {
70
+ it('should classify function with await-expression marker as impure', () => {
71
+ const fn = createTestFunction({
72
+ isAsync: true,
73
+ hasAwait: true,
74
+ })
75
+ const markers: ImpurityMarker[] = [
76
+ { type: 'await-expression', detail: 'await fetchData()' },
77
+ ]
78
+
79
+ const classification = classifyFunction(fn, markers)
80
+
81
+ expect(classification).toBe('impure')
82
+ })
83
+
84
+ it('should classify function with database-call marker as impure', () => {
85
+ const fn = createTestFunction()
86
+ const markers: ImpurityMarker[] = [
87
+ { type: 'database-call', detail: 'db.user.findFirst' },
88
+ ]
89
+
90
+ const classification = classifyFunction(fn, markers)
91
+
92
+ expect(classification).toBe('impure')
93
+ })
94
+
95
+ it('should classify function with network-fetch marker as impure', () => {
96
+ const fn = createTestFunction()
97
+ const markers: ImpurityMarker[] = [
98
+ { type: 'network-fetch', detail: 'fetch' },
99
+ ]
100
+
101
+ const classification = classifyFunction(fn, markers)
102
+
103
+ expect(classification).toBe('impure')
104
+ })
105
+
106
+ it('should classify function with fs-call marker as impure', () => {
107
+ const fn = createTestFunction()
108
+ const markers: ImpurityMarker[] = [
109
+ { type: 'fs-call', detail: 'fs.readFile' },
110
+ ]
111
+
112
+ const classification = classifyFunction(fn, markers)
113
+
114
+ expect(classification).toBe('impure')
115
+ })
116
+
117
+ it('should classify function with env-access marker as impure', () => {
118
+ const fn = createTestFunction()
119
+ const markers: ImpurityMarker[] = [
120
+ { type: 'env-access', detail: 'process.env.NODE_ENV' },
121
+ ]
122
+
123
+ const classification = classifyFunction(fn, markers)
124
+
125
+ expect(classification).toBe('impure')
126
+ })
127
+
128
+ it('should classify function with console-log marker as impure', () => {
129
+ const fn = createTestFunction()
130
+ const markers: ImpurityMarker[] = [
131
+ { type: 'console-log', detail: 'console.log' },
132
+ ]
133
+
134
+ const classification = classifyFunction(fn, markers)
135
+
136
+ expect(classification).toBe('impure')
137
+ })
138
+
139
+ it('should classify function with multiple markers as impure', () => {
140
+ const fn = createTestFunction()
141
+ const markers: ImpurityMarker[] = [
142
+ { type: 'database-call', detail: 'db.user.findFirst' },
143
+ { type: 'console-log', detail: 'console.log' },
144
+ { type: 'env-access', detail: 'process.env.DEBUG' },
145
+ ]
146
+
147
+ const classification = classifyFunction(fn, markers)
148
+
149
+ expect(classification).toBe('impure')
150
+ })
151
+
152
+ it('should classify function with any I/O marker as impure', () => {
153
+ const allMarkerTypes: ImpurityMarker['type'][] = [
154
+ 'await-expression',
155
+ 'database-call',
156
+ 'network-fetch',
157
+ 'network-http',
158
+ 'fs-import',
159
+ 'fs-call',
160
+ 'env-access',
161
+ 'console-log',
162
+ 'logging',
163
+ 'telemetry',
164
+ 'queue-enqueue',
165
+ 'event-emit',
166
+ ]
167
+
168
+ for (const markerType of allMarkerTypes) {
169
+ const fn = createTestFunction()
170
+ const markers: ImpurityMarker[] = [{ type: markerType, detail: 'test' }]
171
+
172
+ const classification = classifyFunction(fn, markers)
173
+
174
+ expect(classification).toBe('impure')
175
+ }
176
+ })
177
+ })
178
+ })
179
+
180
+ describe('shouldExcludeFunction', () => {
181
+ it('should exclude trivial functions with < 3 statements and no conditionals', () => {
182
+ const fn = createTestFunction({
183
+ statementCount: 2,
184
+ hasConditionals: false,
185
+ })
186
+
187
+ expect(shouldExcludeFunction(fn)).toBe(true)
188
+ })
189
+
190
+ it('should exclude single-statement functions', () => {
191
+ const fn = createTestFunction({
192
+ statementCount: 1,
193
+ hasConditionals: false,
194
+ })
195
+
196
+ expect(shouldExcludeFunction(fn)).toBe(true)
197
+ })
198
+
199
+ it('should not exclude functions with >= 3 statements', () => {
200
+ const fn = createTestFunction({
201
+ statementCount: 3,
202
+ hasConditionals: false,
203
+ })
204
+
205
+ expect(shouldExcludeFunction(fn)).toBe(false)
206
+ })
207
+
208
+ it('should not exclude functions with conditionals even if < 3 statements', () => {
209
+ const fn = createTestFunction({
210
+ statementCount: 2,
211
+ hasConditionals: true,
212
+ })
213
+
214
+ expect(shouldExcludeFunction(fn)).toBe(false)
215
+ })
216
+
217
+ it('should not exclude complex functions', () => {
218
+ const fn = createTestFunction({
219
+ statementCount: 10,
220
+ hasConditionals: true,
221
+ })
222
+
223
+ expect(shouldExcludeFunction(fn)).toBe(false)
224
+ })
225
+ })
226
+
227
+ describe('createClassifiedFunction', () => {
228
+ it('should create classified function with all properties', () => {
229
+ const fn = createTestFunction({ name: 'myFunction' })
230
+ const markers: ImpurityMarker[] = [
231
+ { type: 'database-call', detail: 'db.user.findFirst' },
232
+ ]
233
+ const qualityScore = 75
234
+ const status: Status = 'ok'
235
+
236
+ const classified = createClassifiedFunction(
237
+ fn,
238
+ markers,
239
+ qualityScore,
240
+ status,
241
+ )
242
+
243
+ expect(classified.name).toBe('myFunction')
244
+ expect(classified.classification).toBe('impure')
245
+ expect(classified.markers).toEqual(markers)
246
+ expect(classified.qualityScore).toBe(75)
247
+ expect(classified.status).toBe('ok')
248
+ })
249
+
250
+ it('should classify as pure when no markers', () => {
251
+ const fn = createTestFunction({ name: 'pureFunction' })
252
+ const markers: ImpurityMarker[] = []
253
+ const qualityScore = null
254
+ const status: Status = 'ok'
255
+
256
+ const classified = createClassifiedFunction(
257
+ fn,
258
+ markers,
259
+ qualityScore,
260
+ status,
261
+ )
262
+
263
+ expect(classified.classification).toBe('pure')
264
+ expect(classified.qualityScore).toBeNull()
265
+ })
266
+
267
+ it('should preserve all original function properties', () => {
268
+ const fn = createTestFunction({
269
+ name: 'testFn',
270
+ filePath: '/src/test.ts',
271
+ startLine: 10,
272
+ endLine: 25,
273
+ isAsync: true,
274
+ isExported: true,
275
+ bodyLineCount: 15,
276
+ statementCount: 8,
277
+ hasConditionals: true,
278
+ parentContext: 'TestClass',
279
+ kind: 'method',
280
+ })
281
+ const markers: ImpurityMarker[] = []
282
+
283
+ const classified = createClassifiedFunction(fn, markers, null, 'ok')
284
+
285
+ expect(classified.name).toBe('testFn')
286
+ expect(classified.filePath).toBe('/src/test.ts')
287
+ expect(classified.startLine).toBe(10)
288
+ expect(classified.endLine).toBe(25)
289
+ expect(classified.isAsync).toBe(true)
290
+ expect(classified.isExported).toBe(true)
291
+ expect(classified.bodyLineCount).toBe(15)
292
+ expect(classified.statementCount).toBe(8)
293
+ expect(classified.hasConditionals).toBe(true)
294
+ expect(classified.parentContext).toBe('TestClass')
295
+ expect(classified.kind).toBe('method')
296
+ })
297
+ })
298
+
299
+ describe('partitionByClassification', () => {
300
+ it('should partition functions into pure and impure arrays', () => {
301
+ const pureFn1 = createClassifiedFunction(
302
+ createTestFunction({ name: 'pure1' }),
303
+ [],
304
+ null,
305
+ 'ok',
306
+ )
307
+ const pureFn2 = createClassifiedFunction(
308
+ createTestFunction({ name: 'pure2' }),
309
+ [],
310
+ null,
311
+ 'ok',
312
+ )
313
+ const impureFn = createClassifiedFunction(
314
+ createTestFunction({ name: 'impure1' }),
315
+ [{ type: 'database-call', detail: 'db.user.findFirst' }],
316
+ 70,
317
+ 'ok',
318
+ )
319
+
320
+ const result = partitionByClassification([pureFn1, impureFn, pureFn2])
321
+
322
+ expect(result.pure).toHaveLength(2)
323
+ expect(result.impure).toHaveLength(1)
324
+ expect(result.pure.map(f => f.name)).toContain('pure1')
325
+ expect(result.pure.map(f => f.name)).toContain('pure2')
326
+ expect(result.impure.map(f => f.name)).toContain('impure1')
327
+ })
328
+
329
+ it('should handle all pure functions', () => {
330
+ const pureFn1 = createClassifiedFunction(
331
+ createTestFunction({ name: 'pure1' }),
332
+ [],
333
+ null,
334
+ 'ok',
335
+ )
336
+ const pureFn2 = createClassifiedFunction(
337
+ createTestFunction({ name: 'pure2' }),
338
+ [],
339
+ null,
340
+ 'ok',
341
+ )
342
+
343
+ const result = partitionByClassification([pureFn1, pureFn2])
344
+
345
+ expect(result.pure).toHaveLength(2)
346
+ expect(result.impure).toHaveLength(0)
347
+ })
348
+
349
+ it('should handle all impure functions', () => {
350
+ const impureFn1 = createClassifiedFunction(
351
+ createTestFunction({ name: 'impure1' }),
352
+ [{ type: 'database-call', detail: 'db.user.findFirst' }],
353
+ 70,
354
+ 'ok',
355
+ )
356
+ const impureFn2 = createClassifiedFunction(
357
+ createTestFunction({ name: 'impure2' }),
358
+ [{ type: 'network-fetch', detail: 'fetch' }],
359
+ 50,
360
+ 'review',
361
+ )
362
+
363
+ const result = partitionByClassification([impureFn1, impureFn2])
364
+
365
+ expect(result.pure).toHaveLength(0)
366
+ expect(result.impure).toHaveLength(2)
367
+ })
368
+
369
+ it('should handle empty array', () => {
370
+ const result = partitionByClassification([])
371
+
372
+ expect(result.pure).toHaveLength(0)
373
+ expect(result.impure).toHaveLength(0)
374
+ })
375
+ })
376
+
377
+ describe('getClassificationSummary', () => {
378
+ it('should return correct summary for mixed functions', () => {
379
+ const pureFn = createClassifiedFunction(
380
+ createTestFunction({ name: 'pure' }),
381
+ [],
382
+ null,
383
+ 'ok',
384
+ )
385
+ const impureFn1 = createClassifiedFunction(
386
+ createTestFunction({ name: 'impure1' }),
387
+ [{ type: 'database-call', detail: 'db.user.findFirst' }],
388
+ 70,
389
+ 'ok',
390
+ )
391
+ const impureFn2 = createClassifiedFunction(
392
+ createTestFunction({ name: 'impure2' }),
393
+ [{ type: 'network-fetch', detail: 'fetch' }],
394
+ 50,
395
+ 'review',
396
+ )
397
+
398
+ const summary = getClassificationSummary([pureFn, impureFn1, impureFn2])
399
+
400
+ expect(summary.total).toBe(3)
401
+ expect(summary.pureCount).toBe(1)
402
+ expect(summary.impureCount).toBe(2)
403
+ expect(summary.purityPercentage).toBeCloseTo(33.33, 1)
404
+ })
405
+
406
+ it('should return 100% purity for all pure functions', () => {
407
+ const pureFn1 = createClassifiedFunction(
408
+ createTestFunction({ name: 'pure1' }),
409
+ [],
410
+ null,
411
+ 'ok',
412
+ )
413
+ const pureFn2 = createClassifiedFunction(
414
+ createTestFunction({ name: 'pure2' }),
415
+ [],
416
+ null,
417
+ 'ok',
418
+ )
419
+
420
+ const summary = getClassificationSummary([pureFn1, pureFn2])
421
+
422
+ expect(summary.total).toBe(2)
423
+ expect(summary.pureCount).toBe(2)
424
+ expect(summary.impureCount).toBe(0)
425
+ expect(summary.purityPercentage).toBe(100)
426
+ })
427
+
428
+ it('should return 0% purity for all impure functions', () => {
429
+ const impureFn1 = createClassifiedFunction(
430
+ createTestFunction({ name: 'impure1' }),
431
+ [{ type: 'database-call', detail: 'db.user.findFirst' }],
432
+ 70,
433
+ 'ok',
434
+ )
435
+ const impureFn2 = createClassifiedFunction(
436
+ createTestFunction({ name: 'impure2' }),
437
+ [{ type: 'network-fetch', detail: 'fetch' }],
438
+ 50,
439
+ 'review',
440
+ )
441
+
442
+ const summary = getClassificationSummary([impureFn1, impureFn2])
443
+
444
+ expect(summary.total).toBe(2)
445
+ expect(summary.pureCount).toBe(0)
446
+ expect(summary.impureCount).toBe(2)
447
+ expect(summary.purityPercentage).toBe(0)
448
+ })
449
+
450
+ it('should return 100% purity for empty array', () => {
451
+ const summary = getClassificationSummary([])
452
+
453
+ expect(summary.total).toBe(0)
454
+ expect(summary.pureCount).toBe(0)
455
+ expect(summary.impureCount).toBe(0)
456
+ expect(summary.purityPercentage).toBe(100)
457
+ })
458
+
459
+ it('should calculate correct percentages for 50/50 split', () => {
460
+ const pureFn = createClassifiedFunction(
461
+ createTestFunction({ name: 'pure' }),
462
+ [],
463
+ null,
464
+ 'ok',
465
+ )
466
+ const impureFn = createClassifiedFunction(
467
+ createTestFunction({ name: 'impure' }),
468
+ [{ type: 'database-call', detail: 'db.user.findFirst' }],
469
+ 70,
470
+ 'ok',
471
+ )
472
+
473
+ const summary = getClassificationSummary([pureFn, impureFn])
474
+
475
+ expect(summary.total).toBe(2)
476
+ expect(summary.pureCount).toBe(1)
477
+ expect(summary.impureCount).toBe(1)
478
+ expect(summary.purityPercentage).toBe(50)
479
+ })
480
+ })