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.
- package/.plans/001-fcis-analyzer.md +832 -0
- package/.plans/002-fcis-analyzer-improvements.md +205 -0
- package/README.md +272 -0
- package/TECHNICAL.md +386 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1836 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +709 -0
- package/dist/index.js +1845 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/pnpm-workspace.yaml +0 -0
- package/src/analyzer.ts +266 -0
- package/src/classification/classifier.ts +156 -0
- package/src/classification/derive-status.ts +171 -0
- package/src/classification/quality-scorer.ts +481 -0
- package/src/cli.ts +286 -0
- package/src/detection/detect-markers.ts +480 -0
- package/src/detection/markers.ts +332 -0
- package/src/extraction/extract-functions.ts +570 -0
- package/src/extraction/extractor.ts +188 -0
- package/src/index.ts +111 -0
- package/src/reporting/report-console.ts +416 -0
- package/src/reporting/report-json.ts +232 -0
- package/src/scoring/scorer.ts +504 -0
- package/src/types.ts +248 -0
- package/tests/classifier.test.ts +480 -0
- package/tests/derive-status.test.ts +464 -0
- package/tests/detect-markers.test.ts +639 -0
- package/tests/extractor.test.ts +155 -0
- package/tests/integration.test.ts +706 -0
- package/tests/quality-scorer.test.ts +650 -0
- package/tests/scorer.test.ts +768 -0
- package/tsconfig.json +34 -0
- package/tsup.config.ts +17 -0
- package/vendor/ts-morph/.editorconfig +10 -0
- package/vendor/ts-morph/.gitattributes +11 -0
- package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
- package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
- package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
- package/vendor/ts-morph/.vscode/settings.json +10 -0
- package/vendor/ts-morph/CONTRIBUTING.md +23 -0
- package/vendor/ts-morph/DEVELOPMENT.md +32 -0
- package/vendor/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/deno.json +8 -0
- package/vendor/ts-morph/deno.lock +1233 -0
- package/vendor/ts-morph/docs/CNAME +1 -0
- package/vendor/ts-morph/docs/Gemfile +2 -0
- package/vendor/ts-morph/docs/_config.yml +5 -0
- package/vendor/ts-morph/docs/_layouts/default.html +159 -0
- package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
- package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
- package/vendor/ts-morph/docs/details/ambient.md +38 -0
- package/vendor/ts-morph/docs/details/async.md +31 -0
- package/vendor/ts-morph/docs/details/classes.md +314 -0
- package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
- package/vendor/ts-morph/docs/details/comments.md +122 -0
- package/vendor/ts-morph/docs/details/decorators.md +119 -0
- package/vendor/ts-morph/docs/details/documentation.md +73 -0
- package/vendor/ts-morph/docs/details/enums.md +117 -0
- package/vendor/ts-morph/docs/details/exports.md +308 -0
- package/vendor/ts-morph/docs/details/expressions.md +46 -0
- package/vendor/ts-morph/docs/details/functions.md +150 -0
- package/vendor/ts-morph/docs/details/generators.md +27 -0
- package/vendor/ts-morph/docs/details/identifiers.md +79 -0
- package/vendor/ts-morph/docs/details/imports.md +191 -0
- package/vendor/ts-morph/docs/details/index.md +52 -0
- package/vendor/ts-morph/docs/details/initializers.md +40 -0
- package/vendor/ts-morph/docs/details/interfaces.md +218 -0
- package/vendor/ts-morph/docs/details/literals.md +20 -0
- package/vendor/ts-morph/docs/details/modifiers.md +38 -0
- package/vendor/ts-morph/docs/details/modules.md +113 -0
- package/vendor/ts-morph/docs/details/namespaces.md +7 -0
- package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
- package/vendor/ts-morph/docs/details/parameters.md +64 -0
- package/vendor/ts-morph/docs/details/signatures.md +41 -0
- package/vendor/ts-morph/docs/details/source-files.md +292 -0
- package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
- package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
- package/vendor/ts-morph/docs/details/types.md +254 -0
- package/vendor/ts-morph/docs/details/variables.md +110 -0
- package/vendor/ts-morph/docs/emitting.md +151 -0
- package/vendor/ts-morph/docs/index.md +25 -0
- package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
- package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
- package/vendor/ts-morph/docs/manipulation/index.md +136 -0
- package/vendor/ts-morph/docs/manipulation/order.md +14 -0
- package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
- package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
- package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
- package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
- package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
- package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
- package/vendor/ts-morph/docs/metrics/performance.json +4 -0
- package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
- package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
- package/vendor/ts-morph/docs/navigation/directories.md +287 -0
- package/vendor/ts-morph/docs/navigation/example.md +50 -0
- package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
- package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
- package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
- package/vendor/ts-morph/docs/navigation/index.md +94 -0
- package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
- package/vendor/ts-morph/docs/navigation/program.md +25 -0
- package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
- package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
- package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
- package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
- package/vendor/ts-morph/docs/setup/file-system.md +106 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
- package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
- package/vendor/ts-morph/docs/setup/index.md +94 -0
- package/vendor/ts-morph/docs/utilities.md +55 -0
- package/vendor/ts-morph/dprint.json +23 -0
- package/vendor/ts-morph/package.json +30 -0
- package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
- package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
- package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
- package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
- package/vendor/ts-morph/packages/common/LICENSE +21 -0
- package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
- package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
- package/vendor/ts-morph/packages/common/package.json +65 -0
- package/vendor/ts-morph/packages/common/readme.md +5 -0
- package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
- package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
- package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
- package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
- package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
- package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
- package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
- package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
- package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
- package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
- package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
- package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
- package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
- package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
- package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
- package/vendor/ts-morph/readme.md +14 -0
- package/vendor/ts-morph/rfcs/README.md +13 -0
- package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
- package/vendor/ts-morph/tsconfig.common.json +17 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring Engine - Pure Core
|
|
3
|
+
*
|
|
4
|
+
* Aggregates classification and quality scores into metrics at
|
|
5
|
+
* file, directory, and project levels. This is a PURE module -
|
|
6
|
+
* all functions take data in and return data out with no I/O.
|
|
7
|
+
*
|
|
8
|
+
* Metrics:
|
|
9
|
+
* - Purity: percentage of pure functions
|
|
10
|
+
* - Impurity Quality: average quality score of impure functions
|
|
11
|
+
* - Health: percentage of functions with status 'ok'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ClassifiedFunction,
|
|
16
|
+
DirectoryScore,
|
|
17
|
+
FileScore,
|
|
18
|
+
ProjectScore,
|
|
19
|
+
RefactoringCandidate,
|
|
20
|
+
StatusBreakdown,
|
|
21
|
+
} from '../types.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Score a single file based on its classified functions
|
|
25
|
+
*
|
|
26
|
+
* @param filePath - Path to the file
|
|
27
|
+
* @param functions - Classified functions from the file
|
|
28
|
+
* @param isTypeOnly - Whether the file contains only types
|
|
29
|
+
* @returns FileScore with all computed metrics
|
|
30
|
+
*/
|
|
31
|
+
export function scoreFile(
|
|
32
|
+
filePath: string,
|
|
33
|
+
functions: ClassifiedFunction[],
|
|
34
|
+
isTypeOnly: boolean = false,
|
|
35
|
+
): FileScore {
|
|
36
|
+
// Handle type-only files
|
|
37
|
+
if (isTypeOnly) {
|
|
38
|
+
return {
|
|
39
|
+
filePath,
|
|
40
|
+
purity: 100,
|
|
41
|
+
impurityQuality: null,
|
|
42
|
+
health: 100,
|
|
43
|
+
pureCount: 0,
|
|
44
|
+
impureCount: 0,
|
|
45
|
+
excludedCount: 0,
|
|
46
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
47
|
+
pureLineCount: 0,
|
|
48
|
+
impureLineCount: 0,
|
|
49
|
+
functions: [],
|
|
50
|
+
refactoringCandidates: [],
|
|
51
|
+
typeOnly: true,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Partition functions by classification
|
|
56
|
+
const pureFunctions = functions.filter(f => f.classification === 'pure')
|
|
57
|
+
const impureFunctions = functions.filter(f => f.classification === 'impure')
|
|
58
|
+
|
|
59
|
+
const pureCount = pureFunctions.length
|
|
60
|
+
const impureCount = impureFunctions.length
|
|
61
|
+
const total = pureCount + impureCount
|
|
62
|
+
|
|
63
|
+
// Handle empty files
|
|
64
|
+
if (total === 0) {
|
|
65
|
+
return {
|
|
66
|
+
filePath,
|
|
67
|
+
purity: 100,
|
|
68
|
+
impurityQuality: null,
|
|
69
|
+
health: 100,
|
|
70
|
+
pureCount: 0,
|
|
71
|
+
impureCount: 0,
|
|
72
|
+
excludedCount: 0,
|
|
73
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
74
|
+
pureLineCount: 0,
|
|
75
|
+
impureLineCount: 0,
|
|
76
|
+
functions: [],
|
|
77
|
+
refactoringCandidates: [],
|
|
78
|
+
allExcluded: true,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Calculate purity (percentage of pure functions)
|
|
83
|
+
const purity = (pureCount / total) * 100
|
|
84
|
+
|
|
85
|
+
// Calculate impurity quality (average quality of impure functions)
|
|
86
|
+
let impurityQuality: number | null = null
|
|
87
|
+
if (impureCount > 0) {
|
|
88
|
+
const totalQuality = impureFunctions.reduce(
|
|
89
|
+
(sum, f) => sum + (f.qualityScore ?? 0),
|
|
90
|
+
0,
|
|
91
|
+
)
|
|
92
|
+
impurityQuality = totalQuality / impureCount
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate status breakdown
|
|
96
|
+
const statusBreakdown: StatusBreakdown = {
|
|
97
|
+
ok: functions.filter(f => f.status === 'ok').length,
|
|
98
|
+
review: functions.filter(f => f.status === 'review').length,
|
|
99
|
+
refactor: functions.filter(f => f.status === 'refactor').length,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Calculate health (percentage with status 'ok')
|
|
103
|
+
const health = (statusBreakdown.ok / total) * 100
|
|
104
|
+
|
|
105
|
+
// Calculate line counts
|
|
106
|
+
const pureLineCount = pureFunctions.reduce(
|
|
107
|
+
(sum, f) => sum + f.bodyLineCount,
|
|
108
|
+
0,
|
|
109
|
+
)
|
|
110
|
+
const impureLineCount = impureFunctions.reduce(
|
|
111
|
+
(sum, f) => sum + f.bodyLineCount,
|
|
112
|
+
0,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Get refactoring candidates (functions with status 'refactor')
|
|
116
|
+
// Sorted by impact: bodyLineCount × (100 - qualityScore) descending
|
|
117
|
+
const refactoringCandidates = impureFunctions
|
|
118
|
+
.filter(f => f.status === 'refactor')
|
|
119
|
+
.map(f => ({
|
|
120
|
+
name: f.name,
|
|
121
|
+
startLine: f.startLine,
|
|
122
|
+
bodyLineCount: f.bodyLineCount,
|
|
123
|
+
qualityScore: f.qualityScore ?? 0,
|
|
124
|
+
markers: f.markers.map(m => m.type),
|
|
125
|
+
}))
|
|
126
|
+
.sort((a, b) => {
|
|
127
|
+
const impactA = a.bodyLineCount * (100 - a.qualityScore)
|
|
128
|
+
const impactB = b.bodyLineCount * (100 - b.qualityScore)
|
|
129
|
+
return impactB - impactA
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
filePath,
|
|
134
|
+
purity,
|
|
135
|
+
impurityQuality,
|
|
136
|
+
health,
|
|
137
|
+
pureCount,
|
|
138
|
+
impureCount,
|
|
139
|
+
excludedCount: 0,
|
|
140
|
+
statusBreakdown,
|
|
141
|
+
pureLineCount,
|
|
142
|
+
impureLineCount,
|
|
143
|
+
functions,
|
|
144
|
+
refactoringCandidates,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Score a directory by aggregating its file scores
|
|
150
|
+
*
|
|
151
|
+
* @param dirPath - Path to the directory
|
|
152
|
+
* @param fileScores - Scores for all files in the directory
|
|
153
|
+
* @returns DirectoryScore with weighted averages
|
|
154
|
+
*/
|
|
155
|
+
export function scoreDirectory(
|
|
156
|
+
dirPath: string,
|
|
157
|
+
fileScores: FileScore[],
|
|
158
|
+
): DirectoryScore {
|
|
159
|
+
// Filter out type-only and empty files for aggregation
|
|
160
|
+
const scorableFiles = fileScores.filter(
|
|
161
|
+
f => !f.typeOnly && !f.allExcluded && f.pureCount + f.impureCount > 0,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if (scorableFiles.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
dirPath,
|
|
167
|
+
purity: 100,
|
|
168
|
+
impurityQuality: null,
|
|
169
|
+
health: 100,
|
|
170
|
+
pureCount: 0,
|
|
171
|
+
impureCount: 0,
|
|
172
|
+
excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
|
|
173
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
174
|
+
pureLineCount: 0,
|
|
175
|
+
impureLineCount: 0,
|
|
176
|
+
fileScores,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Calculate totals
|
|
181
|
+
const totalPure = scorableFiles.reduce((sum, f) => sum + f.pureCount, 0)
|
|
182
|
+
const totalImpure = scorableFiles.reduce((sum, f) => sum + f.impureCount, 0)
|
|
183
|
+
const total = totalPure + totalImpure
|
|
184
|
+
|
|
185
|
+
// Weighted purity by function count
|
|
186
|
+
const purity = total > 0 ? (totalPure / total) * 100 : 100
|
|
187
|
+
|
|
188
|
+
// Weighted impurity quality
|
|
189
|
+
let impurityQuality: number | null = null
|
|
190
|
+
if (totalImpure > 0) {
|
|
191
|
+
const weightedQuality = scorableFiles.reduce((sum, f) => {
|
|
192
|
+
if (f.impurityQuality !== null && f.impureCount > 0) {
|
|
193
|
+
return sum + f.impurityQuality * f.impureCount
|
|
194
|
+
}
|
|
195
|
+
return sum
|
|
196
|
+
}, 0)
|
|
197
|
+
impurityQuality = weightedQuality / totalImpure
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Aggregate status breakdown
|
|
201
|
+
const statusBreakdown: StatusBreakdown = {
|
|
202
|
+
ok: scorableFiles.reduce((sum, f) => sum + f.statusBreakdown.ok, 0),
|
|
203
|
+
review: scorableFiles.reduce((sum, f) => sum + f.statusBreakdown.review, 0),
|
|
204
|
+
refactor: scorableFiles.reduce(
|
|
205
|
+
(sum, f) => sum + f.statusBreakdown.refactor,
|
|
206
|
+
0,
|
|
207
|
+
),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Calculate health
|
|
211
|
+
const health = total > 0 ? (statusBreakdown.ok / total) * 100 : 100
|
|
212
|
+
|
|
213
|
+
// Aggregate line counts
|
|
214
|
+
const pureLineCount = scorableFiles.reduce(
|
|
215
|
+
(sum, f) => sum + f.pureLineCount,
|
|
216
|
+
0,
|
|
217
|
+
)
|
|
218
|
+
const impureLineCount = scorableFiles.reduce(
|
|
219
|
+
(sum, f) => sum + f.impureLineCount,
|
|
220
|
+
0,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
dirPath,
|
|
225
|
+
purity,
|
|
226
|
+
impurityQuality,
|
|
227
|
+
health,
|
|
228
|
+
pureCount: totalPure,
|
|
229
|
+
impureCount: totalImpure,
|
|
230
|
+
excludedCount: fileScores.reduce((sum, f) => sum + f.excludedCount, 0),
|
|
231
|
+
statusBreakdown,
|
|
232
|
+
pureLineCount,
|
|
233
|
+
impureLineCount,
|
|
234
|
+
fileScores,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Score an entire project by aggregating directory scores
|
|
240
|
+
*
|
|
241
|
+
* @param directoryScores - Scores for all directories in the project
|
|
242
|
+
* @param options - Additional options for the project score
|
|
243
|
+
* @returns ProjectScore with all metrics
|
|
244
|
+
*/
|
|
245
|
+
export function scoreProject(
|
|
246
|
+
directoryScores: DirectoryScore[],
|
|
247
|
+
options: {
|
|
248
|
+
timestamp?: string
|
|
249
|
+
commitHash?: string | null
|
|
250
|
+
subset?: boolean
|
|
251
|
+
filesGlob?: string
|
|
252
|
+
errors?: { filePath: string; error: string }[]
|
|
253
|
+
} = {},
|
|
254
|
+
): ProjectScore {
|
|
255
|
+
const {
|
|
256
|
+
timestamp = new Date().toISOString(),
|
|
257
|
+
commitHash = null,
|
|
258
|
+
subset = false,
|
|
259
|
+
filesGlob,
|
|
260
|
+
errors = [],
|
|
261
|
+
} = options
|
|
262
|
+
|
|
263
|
+
// Filter directories with actual functions
|
|
264
|
+
const scorableDirs = directoryScores.filter(
|
|
265
|
+
d => d.pureCount + d.impureCount > 0,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if (scorableDirs.length === 0) {
|
|
269
|
+
return {
|
|
270
|
+
purity: 100,
|
|
271
|
+
impurityQuality: null,
|
|
272
|
+
health: 100,
|
|
273
|
+
pureCount: 0,
|
|
274
|
+
impureCount: 0,
|
|
275
|
+
excludedCount: directoryScores.reduce(
|
|
276
|
+
(sum, d) => sum + d.excludedCount,
|
|
277
|
+
0,
|
|
278
|
+
),
|
|
279
|
+
statusBreakdown: { ok: 0, review: 0, refactor: 0 },
|
|
280
|
+
pureLineCount: 0,
|
|
281
|
+
impureLineCount: 0,
|
|
282
|
+
directoryScores,
|
|
283
|
+
timestamp,
|
|
284
|
+
commitHash,
|
|
285
|
+
refactoringCandidates: [],
|
|
286
|
+
allExcluded: true,
|
|
287
|
+
...(subset && { subset: true, filesGlob }),
|
|
288
|
+
...(errors.length > 0 && { errors }),
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Calculate totals
|
|
293
|
+
const totalPure = scorableDirs.reduce((sum, d) => sum + d.pureCount, 0)
|
|
294
|
+
const totalImpure = scorableDirs.reduce((sum, d) => sum + d.impureCount, 0)
|
|
295
|
+
const total = totalPure + totalImpure
|
|
296
|
+
|
|
297
|
+
// Weighted purity by function count
|
|
298
|
+
const purity = total > 0 ? (totalPure / total) * 100 : 100
|
|
299
|
+
|
|
300
|
+
// Weighted impurity quality
|
|
301
|
+
let impurityQuality: number | null = null
|
|
302
|
+
if (totalImpure > 0) {
|
|
303
|
+
const weightedQuality = scorableDirs.reduce((sum, d) => {
|
|
304
|
+
if (d.impurityQuality !== null && d.impureCount > 0) {
|
|
305
|
+
return sum + d.impurityQuality * d.impureCount
|
|
306
|
+
}
|
|
307
|
+
return sum
|
|
308
|
+
}, 0)
|
|
309
|
+
impurityQuality = weightedQuality / totalImpure
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Aggregate status breakdown
|
|
313
|
+
const statusBreakdown: StatusBreakdown = {
|
|
314
|
+
ok: scorableDirs.reduce((sum, d) => sum + d.statusBreakdown.ok, 0),
|
|
315
|
+
review: scorableDirs.reduce((sum, d) => sum + d.statusBreakdown.review, 0),
|
|
316
|
+
refactor: scorableDirs.reduce(
|
|
317
|
+
(sum, d) => sum + d.statusBreakdown.refactor,
|
|
318
|
+
0,
|
|
319
|
+
),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Calculate health
|
|
323
|
+
const health = total > 0 ? (statusBreakdown.ok / total) * 100 : 100
|
|
324
|
+
|
|
325
|
+
// Aggregate line counts
|
|
326
|
+
const pureLineCount = scorableDirs.reduce(
|
|
327
|
+
(sum, d) => sum + d.pureLineCount,
|
|
328
|
+
0,
|
|
329
|
+
)
|
|
330
|
+
const impureLineCount = scorableDirs.reduce(
|
|
331
|
+
(sum, d) => sum + d.impureLineCount,
|
|
332
|
+
0,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
// Collect all refactoring candidates across all files
|
|
336
|
+
const allCandidates: RefactoringCandidate[] = []
|
|
337
|
+
for (const dir of directoryScores) {
|
|
338
|
+
for (const file of dir.fileScores) {
|
|
339
|
+
for (const candidate of file.refactoringCandidates) {
|
|
340
|
+
allCandidates.push({
|
|
341
|
+
...candidate,
|
|
342
|
+
filePath: file.filePath,
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Sort by impact: bodyLineCount × (100 - qualityScore) descending
|
|
349
|
+
const refactoringCandidates = allCandidates.sort((a, b) => {
|
|
350
|
+
const impactA = a.bodyLineCount * (100 - a.qualityScore)
|
|
351
|
+
const impactB = b.bodyLineCount * (100 - b.qualityScore)
|
|
352
|
+
return impactB - impactA
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const result: ProjectScore = {
|
|
356
|
+
purity,
|
|
357
|
+
impurityQuality,
|
|
358
|
+
health,
|
|
359
|
+
pureCount: totalPure,
|
|
360
|
+
impureCount: totalImpure,
|
|
361
|
+
excludedCount: directoryScores.reduce((sum, d) => sum + d.excludedCount, 0),
|
|
362
|
+
statusBreakdown,
|
|
363
|
+
pureLineCount,
|
|
364
|
+
impureLineCount,
|
|
365
|
+
directoryScores,
|
|
366
|
+
timestamp,
|
|
367
|
+
commitHash,
|
|
368
|
+
refactoringCandidates,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (subset && filesGlob !== undefined) {
|
|
372
|
+
result.subset = true
|
|
373
|
+
result.filesGlob = filesGlob
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (errors.length > 0) {
|
|
377
|
+
result.errors = errors
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return result
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Group file scores by directory
|
|
385
|
+
*
|
|
386
|
+
* @param fileScores - Array of file scores
|
|
387
|
+
* @returns Map of directory path to file scores
|
|
388
|
+
*/
|
|
389
|
+
export function groupFilesByDirectory(
|
|
390
|
+
fileScores: FileScore[],
|
|
391
|
+
): Map<string, FileScore[]> {
|
|
392
|
+
const groups = new Map<string, FileScore[]>()
|
|
393
|
+
|
|
394
|
+
for (const fileScore of fileScores) {
|
|
395
|
+
const dirPath = getDirectoryPath(fileScore.filePath)
|
|
396
|
+
const existing = groups.get(dirPath)
|
|
397
|
+
if (existing) {
|
|
398
|
+
existing.push(fileScore)
|
|
399
|
+
} else {
|
|
400
|
+
groups.set(dirPath, [fileScore])
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return groups
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get directory path from file path
|
|
409
|
+
*/
|
|
410
|
+
function getDirectoryPath(filePath: string): string {
|
|
411
|
+
const lastSlash = filePath.lastIndexOf('/')
|
|
412
|
+
if (lastSlash === -1) return '.'
|
|
413
|
+
return filePath.slice(0, lastSlash)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Calculate delta between two project scores
|
|
418
|
+
*
|
|
419
|
+
* @param current - Current project score
|
|
420
|
+
* @param previous - Previous project score
|
|
421
|
+
* @returns Delta metrics
|
|
422
|
+
*/
|
|
423
|
+
export function calculateDelta(
|
|
424
|
+
current: ProjectScore,
|
|
425
|
+
previous: ProjectScore,
|
|
426
|
+
): {
|
|
427
|
+
purityDelta: number
|
|
428
|
+
impurityQualityDelta: number | null
|
|
429
|
+
healthDelta: number
|
|
430
|
+
pureCountDelta: number
|
|
431
|
+
impureCountDelta: number
|
|
432
|
+
} {
|
|
433
|
+
return {
|
|
434
|
+
purityDelta: current.purity - previous.purity,
|
|
435
|
+
impurityQualityDelta:
|
|
436
|
+
current.impurityQuality !== null && previous.impurityQuality !== null
|
|
437
|
+
? current.impurityQuality - previous.impurityQuality
|
|
438
|
+
: null,
|
|
439
|
+
healthDelta: current.health - previous.health,
|
|
440
|
+
pureCountDelta: current.pureCount - previous.pureCount,
|
|
441
|
+
impureCountDelta: current.impureCount - previous.impureCount,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get diagnostic insights based on metrics
|
|
447
|
+
*
|
|
448
|
+
* @param score - Project or file score
|
|
449
|
+
* @returns Array of insight messages
|
|
450
|
+
*/
|
|
451
|
+
export function getDiagnosticInsights(
|
|
452
|
+
score: Pick<ProjectScore, 'purity' | 'impurityQuality' | 'health'>,
|
|
453
|
+
): string[] {
|
|
454
|
+
const insights: string[] = []
|
|
455
|
+
|
|
456
|
+
// High purity + low impurity quality
|
|
457
|
+
if (
|
|
458
|
+
score.purity >= 60 &&
|
|
459
|
+
score.impurityQuality !== null &&
|
|
460
|
+
score.impurityQuality < 50
|
|
461
|
+
) {
|
|
462
|
+
insights.push(
|
|
463
|
+
'Most code is pure, but the impure code is tangled. Focus on improving the structure of impure functions.',
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Low purity + high impurity quality
|
|
468
|
+
if (
|
|
469
|
+
score.purity < 40 &&
|
|
470
|
+
score.impurityQuality !== null &&
|
|
471
|
+
score.impurityQuality >= 70
|
|
472
|
+
) {
|
|
473
|
+
insights.push(
|
|
474
|
+
"Lots of I/O code, but it's well-structured. Consider extracting more pure business logic.",
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Low purity + low impurity quality
|
|
479
|
+
if (
|
|
480
|
+
score.purity < 40 &&
|
|
481
|
+
score.impurityQuality !== null &&
|
|
482
|
+
score.impurityQuality < 50
|
|
483
|
+
) {
|
|
484
|
+
insights.push(
|
|
485
|
+
'Significant technical debt in I/O code. Both purity and structure need improvement.',
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// High health with low purity
|
|
490
|
+
if (score.health >= 80 && score.purity < 50) {
|
|
491
|
+
insights.push(
|
|
492
|
+
'Despite low purity, impure functions are well-structured. Good FCIS patterns in shell code.',
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Low health
|
|
497
|
+
if (score.health < 50) {
|
|
498
|
+
insights.push(
|
|
499
|
+
'More than half of functions need attention. Prioritize refactoring candidates.',
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return insights
|
|
504
|
+
}
|