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,266 @@
1
+ /**
2
+ * Analyzer - Main Orchestration Layer (Shell)
3
+ *
4
+ * This module orchestrates the full analysis pipeline:
5
+ * 1. Load project via ts-morph
6
+ * 2. Extract functions from source files
7
+ * 3. Detect impurity markers
8
+ * 4. Classify functions
9
+ * 5. Compute quality scores
10
+ * 6. Aggregate into file, directory, and project scores
11
+ *
12
+ * This is a SHELL module - it coordinates I/O and calls pure functions.
13
+ */
14
+
15
+ import type { SourceFile } from 'ts-morph'
16
+
17
+ import type {
18
+ AnalysisError,
19
+ AnalyzerConfig,
20
+ ClassifiedFunction,
21
+ DirectoryScore,
22
+ FileScore,
23
+ ProjectScore,
24
+ } from './types.js'
25
+ import { DEFAULT_QUALITY_THRESHOLDS } from './types.js'
26
+
27
+ import {
28
+ extractFunctions,
29
+ extractImports,
30
+ isTypeOnlyFile,
31
+ } from './extraction/extract-functions.js'
32
+ import { loadProject, getCommitHash } from './extraction/extractor.js'
33
+ import {
34
+ detectMarkers,
35
+ createDetectionContext,
36
+ } from './detection/detect-markers.js'
37
+ import {
38
+ classifyFunction,
39
+ shouldExcludeFunction,
40
+ createClassifiedFunction,
41
+ } from './classification/classifier.js'
42
+ import { computeQualityScore } from './classification/quality-scorer.js'
43
+ import { deriveStatus } from './classification/derive-status.js'
44
+ import {
45
+ scoreFile,
46
+ scoreDirectory,
47
+ scoreProject,
48
+ groupFilesByDirectory,
49
+ } from './scoring/scorer.js'
50
+
51
+ /**
52
+ * Run the full analysis pipeline
53
+ *
54
+ * @param config - Analyzer configuration
55
+ * @returns Project score with all metrics
56
+ */
57
+ export async function analyze(config: AnalyzerConfig): Promise<ProjectScore> {
58
+ const errors: AnalysisError[] = []
59
+
60
+ // Step 1: Load project
61
+ const extractorOptions: { tsconfigPath: string; filesGlob?: string } = {
62
+ tsconfigPath: config.tsconfigPath,
63
+ }
64
+ if (config.filesGlob !== undefined) {
65
+ extractorOptions.filesGlob = config.filesGlob
66
+ }
67
+ const { sourceFiles } = loadProject(extractorOptions)
68
+
69
+ if (sourceFiles.length === 0) {
70
+ throw new Error('No source files found to analyze')
71
+ }
72
+
73
+ // Step 2-5: Extract, detect, classify each file
74
+ const fileScores: FileScore[] = []
75
+
76
+ for (const sourceFile of sourceFiles) {
77
+ try {
78
+ const fileScore = analyzeFile(sourceFile)
79
+ fileScores.push(fileScore)
80
+ } catch (error) {
81
+ errors.push({
82
+ filePath: sourceFile.getFilePath(),
83
+ error: error instanceof Error ? error.message : String(error),
84
+ })
85
+ }
86
+ }
87
+
88
+ // Check if any files were successfully analyzed
89
+ if (fileScores.length === 0 && errors.length > 0) {
90
+ throw new Error(
91
+ `Failed to analyze any files. ${errors.length} files had errors.`,
92
+ )
93
+ }
94
+
95
+ // Step 6: Aggregate into directory scores
96
+ const directoryGroups = groupFilesByDirectory(fileScores)
97
+ const directoryScores: DirectoryScore[] = []
98
+
99
+ for (const [dirPath, files] of directoryGroups) {
100
+ const dirScore = scoreDirectory(dirPath, files)
101
+ directoryScores.push(dirScore)
102
+ }
103
+
104
+ // Sort directories by path for consistent output
105
+ directoryScores.sort((a, b) => a.dirPath.localeCompare(b.dirPath))
106
+
107
+ // Step 7: Aggregate into project score
108
+ const projectOptions: {
109
+ timestamp?: string
110
+ commitHash?: string | null
111
+ subset?: boolean
112
+ filesGlob?: string
113
+ errors?: { filePath: string; error: string }[]
114
+ } = {
115
+ timestamp: new Date().toISOString(),
116
+ commitHash: getCommitHash(),
117
+ }
118
+
119
+ if (config.filesGlob !== undefined) {
120
+ projectOptions.subset = true
121
+ projectOptions.filesGlob = config.filesGlob
122
+ }
123
+
124
+ if (errors.length > 0) {
125
+ projectOptions.errors = errors
126
+ }
127
+
128
+ const projectScore = scoreProject(directoryScores, projectOptions)
129
+
130
+ return projectScore
131
+ }
132
+
133
+ /**
134
+ * Analyze a single source file
135
+ *
136
+ * @param sourceFile - The ts-morph source file to analyze
137
+ * @returns FileScore with all metrics
138
+ */
139
+ export function analyzeFile(sourceFile: SourceFile): FileScore {
140
+ const filePath = sourceFile.getFilePath()
141
+
142
+ // Check if type-only file
143
+ if (isTypeOnlyFile(sourceFile)) {
144
+ return scoreFile(filePath, [], true)
145
+ }
146
+
147
+ // Extract functions and imports
148
+ const functions = extractFunctions(sourceFile)
149
+ const imports = extractImports(sourceFile)
150
+
151
+ // Create detection context
152
+ const context = createDetectionContext(imports)
153
+
154
+ // Classify each function
155
+ const classifiedFunctions: ClassifiedFunction[] = []
156
+
157
+ for (const fn of functions) {
158
+ // Check if function should be excluded
159
+ if (shouldExcludeFunction(fn)) {
160
+ continue
161
+ }
162
+
163
+ // Detect markers
164
+ const markers = detectMarkers(fn, context)
165
+
166
+ // Classify
167
+ const classification = classifyFunction(fn, markers)
168
+
169
+ // Compute quality score (only for impure functions)
170
+ let qualityScore: number | null = null
171
+ if (classification === 'impure') {
172
+ qualityScore = computeQualityScore(fn, markers, context)
173
+ }
174
+
175
+ // Derive status
176
+ const status = deriveStatus(
177
+ classification,
178
+ qualityScore,
179
+ DEFAULT_QUALITY_THRESHOLDS,
180
+ )
181
+
182
+ // Create classified function
183
+ const classifiedFn = createClassifiedFunction(
184
+ fn,
185
+ markers,
186
+ qualityScore,
187
+ status,
188
+ )
189
+ classifiedFunctions.push(classifiedFn)
190
+ }
191
+
192
+ // Score the file
193
+ return scoreFile(filePath, classifiedFunctions, false)
194
+ }
195
+
196
+ /**
197
+ * Analyze a single file from a file path (convenience function)
198
+ *
199
+ * @param tsconfigPath - Path to tsconfig.json
200
+ * @param filePath - Path to the file to analyze
201
+ * @returns FileScore with all metrics
202
+ */
203
+ export async function analyzeFilePath(
204
+ tsconfigPath: string,
205
+ filePath: string,
206
+ ): Promise<FileScore | null> {
207
+ const { sourceFiles } = loadProject({
208
+ tsconfigPath,
209
+ filesGlob: filePath,
210
+ })
211
+
212
+ if (sourceFiles.length === 0) {
213
+ return null
214
+ }
215
+
216
+ const sourceFile = sourceFiles[0]
217
+ if (!sourceFile) {
218
+ return null
219
+ }
220
+
221
+ return analyzeFile(sourceFile)
222
+ }
223
+
224
+ /**
225
+ * Check if analysis passes thresholds
226
+ *
227
+ * @param score - Project score to check
228
+ * @param config - Configuration with threshold values
229
+ * @returns Object indicating pass/fail status and messages
230
+ */
231
+ export function checkThresholds(
232
+ score: ProjectScore,
233
+ config: Pick<AnalyzerConfig, 'minHealth' | 'minPurity' | 'minQuality'>,
234
+ ): {
235
+ passed: boolean
236
+ failures: string[]
237
+ } {
238
+ const failures: string[] = []
239
+
240
+ if (config.minHealth !== undefined && score.health < config.minHealth) {
241
+ failures.push(
242
+ `Health ${score.health.toFixed(1)}% is below minimum ${config.minHealth}%`,
243
+ )
244
+ }
245
+
246
+ if (config.minPurity !== undefined && score.purity < config.minPurity) {
247
+ failures.push(
248
+ `Purity ${score.purity.toFixed(1)}% is below minimum ${config.minPurity}%`,
249
+ )
250
+ }
251
+
252
+ if (
253
+ config.minQuality !== undefined &&
254
+ score.impurityQuality !== null &&
255
+ score.impurityQuality < config.minQuality
256
+ ) {
257
+ failures.push(
258
+ `Impurity Quality ${score.impurityQuality.toFixed(1)}% is below minimum ${config.minQuality}%`,
259
+ )
260
+ }
261
+
262
+ return {
263
+ passed: failures.length === 0,
264
+ failures,
265
+ }
266
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Function Classifier - Pure Core
3
+ *
4
+ * Classifies functions as pure or impure based on their markers.
5
+ * This is a PURE module - all functions take data in and return data out.
6
+ *
7
+ * Classification is binary and objective:
8
+ * - Pure: Zero I/O markers
9
+ * - Impure: Has one or more I/O markers
10
+ *
11
+ * Note: `async` without `await` (and without other I/O markers) is still pure.
12
+ */
13
+
14
+ import type {
15
+ ClassifiedFunction,
16
+ ExtractedFunction,
17
+ FunctionClassification,
18
+ ImpurityMarker,
19
+ Status,
20
+ } from '../types.js'
21
+
22
+ /**
23
+ * Minimum statements for a function to be considered for scoring
24
+ * Functions with fewer statements are considered trivial
25
+ */
26
+ const TRIVIAL_STATEMENT_THRESHOLD = 3
27
+
28
+ /**
29
+ * Classify a function based on its markers
30
+ *
31
+ * @param fn - The extracted function
32
+ * @param markers - Detected impurity markers
33
+ * @returns The function classification ('pure' or 'impure')
34
+ */
35
+ export function classifyFunction(
36
+ _fn: ExtractedFunction,
37
+ markers: ImpurityMarker[],
38
+ ): FunctionClassification {
39
+ // A function is pure if it has no impurity markers
40
+ if (markers.length === 0) {
41
+ return 'pure'
42
+ }
43
+
44
+ return 'impure'
45
+ }
46
+
47
+ /**
48
+ * Check if a function should be excluded from scoring
49
+ *
50
+ * Excluded functions:
51
+ * - Trivial functions (< 3 statements and no conditionals)
52
+ * - Test files (should already be filtered at extraction)
53
+ * - Type-only files (should already be filtered at extraction)
54
+ */
55
+ export function shouldExcludeFunction(fn: ExtractedFunction): boolean {
56
+ // Exclude trivial functions: less than threshold statements and no conditionals
57
+ if (fn.statementCount < TRIVIAL_STATEMENT_THRESHOLD && !fn.hasConditionals) {
58
+ return true
59
+ }
60
+
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * Create a fully classified function with all computed properties
66
+ *
67
+ * @param fn - The extracted function
68
+ * @param markers - Detected impurity markers
69
+ * @param qualityScore - Quality score (null for pure functions)
70
+ * @param status - Derived status
71
+ * @returns A fully classified function
72
+ */
73
+ export function createClassifiedFunction(
74
+ fn: ExtractedFunction,
75
+ markers: ImpurityMarker[],
76
+ qualityScore: number | null,
77
+ status: Status,
78
+ ): ClassifiedFunction {
79
+ const classification = classifyFunction(fn, markers)
80
+
81
+ return {
82
+ ...fn,
83
+ markers,
84
+ classification,
85
+ qualityScore,
86
+ status,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Batch classify multiple functions
92
+ *
93
+ * @param functions - Array of extracted functions with their markers
94
+ * @returns Array of classified functions
95
+ */
96
+ export function classifyFunctions(
97
+ functions: Array<{
98
+ fn: ExtractedFunction
99
+ markers: ImpurityMarker[]
100
+ qualityScore: number | null
101
+ status: Status
102
+ }>,
103
+ ): ClassifiedFunction[] {
104
+ return functions.map(({ fn, markers, qualityScore, status }) =>
105
+ createClassifiedFunction(fn, markers, qualityScore, status),
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Partition functions into pure and impure
111
+ *
112
+ * @param functions - Array of classified functions
113
+ * @returns Object with pure and impure function arrays
114
+ */
115
+ export function partitionByClassification(functions: ClassifiedFunction[]): {
116
+ pure: ClassifiedFunction[]
117
+ impure: ClassifiedFunction[]
118
+ } {
119
+ const pure: ClassifiedFunction[] = []
120
+ const impure: ClassifiedFunction[] = []
121
+
122
+ for (const fn of functions) {
123
+ if (fn.classification === 'pure') {
124
+ pure.push(fn)
125
+ } else {
126
+ impure.push(fn)
127
+ }
128
+ }
129
+
130
+ return { pure, impure }
131
+ }
132
+
133
+ /**
134
+ * Get classification summary for a set of functions
135
+ *
136
+ * @param functions - Array of classified functions
137
+ * @returns Summary statistics
138
+ */
139
+ export function getClassificationSummary(functions: ClassifiedFunction[]): {
140
+ total: number
141
+ pureCount: number
142
+ impureCount: number
143
+ purityPercentage: number
144
+ } {
145
+ const total = functions.length
146
+ const pureCount = functions.filter(f => f.classification === 'pure').length
147
+ const impureCount = total - pureCount
148
+ const purityPercentage = total > 0 ? (pureCount / total) * 100 : 100
149
+
150
+ return {
151
+ total,
152
+ pureCount,
153
+ impureCount,
154
+ purityPercentage,
155
+ }
156
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Status Derivation - Pure Core
3
+ *
4
+ * Derives actionable status from classification and quality score.
5
+ * This is a PURE module - all functions take data in and return data out with no I/O.
6
+ *
7
+ * Status provides developer guidance:
8
+ * - ok: No action needed (pure function, or well-structured impure function)
9
+ * - review: Consider improving if touching this code
10
+ * - refactor: Tangled code, prioritize cleanup
11
+ */
12
+
13
+ import type {
14
+ ClassifiedFunction,
15
+ FunctionClassification,
16
+ QualityThresholds,
17
+ Status,
18
+ } from '../types.js'
19
+ import { DEFAULT_QUALITY_THRESHOLDS } from '../types.js'
20
+
21
+ /**
22
+ * Derive status from classification and quality score
23
+ *
24
+ * Status rules:
25
+ * - Pure functions always get 'ok' (testable without mocks)
26
+ * - Impure functions with quality >= okThreshold (default 70) get 'ok'
27
+ * - Impure functions with quality >= reviewThreshold (default 40) get 'review'
28
+ * - Impure functions with quality < reviewThreshold get 'refactor'
29
+ *
30
+ * @param classification - 'pure' or 'impure'
31
+ * @param qualityScore - Quality score for impure functions (0-100), null for pure
32
+ * @param thresholds - Optional custom thresholds
33
+ * @returns Status: 'ok' | 'review' | 'refactor'
34
+ */
35
+ export function deriveStatus(
36
+ classification: FunctionClassification,
37
+ qualityScore: number | null,
38
+ thresholds: QualityThresholds = DEFAULT_QUALITY_THRESHOLDS,
39
+ ): Status {
40
+ // Pure functions are always ok - testable without mocks
41
+ if (classification === 'pure') {
42
+ return 'ok'
43
+ }
44
+
45
+ // Impure functions need quality score evaluation
46
+ if (qualityScore === null) {
47
+ // This shouldn't happen for impure functions, but handle defensively
48
+ return 'review'
49
+ }
50
+
51
+ if (qualityScore >= thresholds.okThreshold) {
52
+ return 'ok'
53
+ }
54
+
55
+ if (qualityScore >= thresholds.reviewThreshold) {
56
+ return 'review'
57
+ }
58
+
59
+ return 'refactor'
60
+ }
61
+
62
+ /**
63
+ * Get status description for display
64
+ */
65
+ export function getStatusDescription(status: Status): string {
66
+ switch (status) {
67
+ case 'ok':
68
+ return 'No action needed'
69
+ case 'review':
70
+ return 'Consider improving if touching this code'
71
+ case 'refactor':
72
+ return 'Tangled code, prioritize cleanup'
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get status emoji for display
78
+ */
79
+ export function getStatusEmoji(status: Status): string {
80
+ switch (status) {
81
+ case 'ok':
82
+ return '✓'
83
+ case 'review':
84
+ return '◐'
85
+ case 'refactor':
86
+ return '✗'
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get status color for terminal display
92
+ */
93
+ export function getStatusColor(status: Status): 'green' | 'yellow' | 'red' {
94
+ switch (status) {
95
+ case 'ok':
96
+ return 'green'
97
+ case 'review':
98
+ return 'yellow'
99
+ case 'refactor':
100
+ return 'red'
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Check if a classified function should be flagged for attention
106
+ * (status is 'review' or 'refactor')
107
+ */
108
+ export function needsAttention(fn: ClassifiedFunction): boolean {
109
+ return fn.status === 'review' || fn.status === 'refactor'
110
+ }
111
+
112
+ /**
113
+ * Check if a classified function is in good standing
114
+ * (status is 'ok')
115
+ */
116
+ export function isOk(fn: ClassifiedFunction): boolean {
117
+ return fn.status === 'ok'
118
+ }
119
+
120
+ /**
121
+ * Sort functions by status priority (refactor first, then review, then ok)
122
+ */
123
+ export function sortByStatusPriority(
124
+ functions: ClassifiedFunction[],
125
+ ): ClassifiedFunction[] {
126
+ const priorityMap: Record<Status, number> = {
127
+ refactor: 0,
128
+ review: 1,
129
+ ok: 2,
130
+ }
131
+
132
+ return [...functions].sort((a, b) => {
133
+ const priorityDiff = priorityMap[a.status] - priorityMap[b.status]
134
+ if (priorityDiff !== 0) return priorityDiff
135
+
136
+ // Secondary sort by quality score (lower first for impure functions)
137
+ if (a.qualityScore !== null && b.qualityScore !== null) {
138
+ return a.qualityScore - b.qualityScore
139
+ }
140
+
141
+ // Tertiary sort by body line count (larger first)
142
+ return b.bodyLineCount - a.bodyLineCount
143
+ })
144
+ }
145
+
146
+ /**
147
+ * Validate threshold configuration
148
+ */
149
+ export function validateThresholds(thresholds: QualityThresholds): {
150
+ valid: boolean
151
+ errors: string[]
152
+ } {
153
+ const errors: string[] = []
154
+
155
+ if (thresholds.okThreshold < 0 || thresholds.okThreshold > 100) {
156
+ errors.push('okThreshold must be between 0 and 100')
157
+ }
158
+
159
+ if (thresholds.reviewThreshold < 0 || thresholds.reviewThreshold > 100) {
160
+ errors.push('reviewThreshold must be between 0 and 100')
161
+ }
162
+
163
+ if (thresholds.reviewThreshold >= thresholds.okThreshold) {
164
+ errors.push('reviewThreshold must be less than okThreshold')
165
+ }
166
+
167
+ return {
168
+ valid: errors.length === 0,
169
+ errors,
170
+ }
171
+ }