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,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scorer Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the pure core scoring engine.
|
|
5
|
+
* These tests verify that file, directory, and project scores
|
|
6
|
+
* are correctly computed from classified functions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
scoreFile,
|
|
13
|
+
scoreDirectory,
|
|
14
|
+
scoreProject,
|
|
15
|
+
groupFilesByDirectory,
|
|
16
|
+
calculateDelta,
|
|
17
|
+
getDiagnosticInsights,
|
|
18
|
+
} from '../src/scoring/scorer.js'
|
|
19
|
+
import type {
|
|
20
|
+
ClassifiedFunction,
|
|
21
|
+
FileScore,
|
|
22
|
+
DirectoryScore,
|
|
23
|
+
ProjectScore,
|
|
24
|
+
} from '../src/types.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper to create a classified function for testing
|
|
28
|
+
*/
|
|
29
|
+
function createClassifiedFunction(
|
|
30
|
+
overrides: Partial<ClassifiedFunction> = {},
|
|
31
|
+
): ClassifiedFunction {
|
|
32
|
+
return {
|
|
33
|
+
name: 'testFunction',
|
|
34
|
+
filePath: '/test/file.ts',
|
|
35
|
+
startLine: 1,
|
|
36
|
+
endLine: 10,
|
|
37
|
+
isAsync: false,
|
|
38
|
+
isExported: false,
|
|
39
|
+
bodyLineCount: 10,
|
|
40
|
+
statementCount: 5,
|
|
41
|
+
hasConditionals: false,
|
|
42
|
+
parentContext: null,
|
|
43
|
+
callSites: [],
|
|
44
|
+
hasAwait: false,
|
|
45
|
+
propertyAccessChains: [],
|
|
46
|
+
kind: 'function',
|
|
47
|
+
markers: [],
|
|
48
|
+
classification: 'pure',
|
|
49
|
+
qualityScore: null,
|
|
50
|
+
status: 'ok',
|
|
51
|
+
...overrides,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Helper to create a pure function
|
|
57
|
+
*/
|
|
58
|
+
function createPureFunction(
|
|
59
|
+
name: string,
|
|
60
|
+
bodyLineCount: number = 10,
|
|
61
|
+
): ClassifiedFunction {
|
|
62
|
+
return createClassifiedFunction({
|
|
63
|
+
name,
|
|
64
|
+
bodyLineCount,
|
|
65
|
+
classification: 'pure',
|
|
66
|
+
qualityScore: null,
|
|
67
|
+
status: 'ok',
|
|
68
|
+
markers: [],
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Helper to create an impure function
|
|
74
|
+
*/
|
|
75
|
+
function createImpureFunction(
|
|
76
|
+
name: string,
|
|
77
|
+
qualityScore: number,
|
|
78
|
+
status: 'ok' | 'review' | 'refactor' = 'ok',
|
|
79
|
+
bodyLineCount: number = 10,
|
|
80
|
+
): ClassifiedFunction {
|
|
81
|
+
return createClassifiedFunction({
|
|
82
|
+
name,
|
|
83
|
+
bodyLineCount,
|
|
84
|
+
classification: 'impure',
|
|
85
|
+
qualityScore,
|
|
86
|
+
status,
|
|
87
|
+
markers: [{ type: 'database-call', detail: 'db.user.findFirst' }],
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('scoreFile', () => {
|
|
92
|
+
describe('basic scoring', () => {
|
|
93
|
+
it('should score file with all pure functions as 100% purity', () => {
|
|
94
|
+
const functions = [
|
|
95
|
+
createPureFunction('fn1'),
|
|
96
|
+
createPureFunction('fn2'),
|
|
97
|
+
createPureFunction('fn3'),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
101
|
+
|
|
102
|
+
expect(score.purity).toBe(100)
|
|
103
|
+
expect(score.pureCount).toBe(3)
|
|
104
|
+
expect(score.impureCount).toBe(0)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should score file with all impure functions as 0% purity', () => {
|
|
108
|
+
const functions = [
|
|
109
|
+
createImpureFunction('fn1', 70),
|
|
110
|
+
createImpureFunction('fn2', 60),
|
|
111
|
+
createImpureFunction('fn3', 50),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
115
|
+
|
|
116
|
+
expect(score.purity).toBe(0)
|
|
117
|
+
expect(score.pureCount).toBe(0)
|
|
118
|
+
expect(score.impureCount).toBe(3)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should score file with mixed functions correctly', () => {
|
|
122
|
+
const functions = [
|
|
123
|
+
createPureFunction('pure1'),
|
|
124
|
+
createPureFunction('pure2'),
|
|
125
|
+
createImpureFunction('impure1', 70),
|
|
126
|
+
createImpureFunction('impure2', 60),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
130
|
+
|
|
131
|
+
expect(score.purity).toBe(50)
|
|
132
|
+
expect(score.pureCount).toBe(2)
|
|
133
|
+
expect(score.impureCount).toBe(2)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('impurity quality', () => {
|
|
138
|
+
it('should calculate average impurity quality', () => {
|
|
139
|
+
const functions = [
|
|
140
|
+
createImpureFunction('fn1', 80),
|
|
141
|
+
createImpureFunction('fn2', 60),
|
|
142
|
+
createImpureFunction('fn3', 40),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
146
|
+
|
|
147
|
+
expect(score.impurityQuality).toBe(60)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should return null impurity quality for all pure functions', () => {
|
|
151
|
+
const functions = [createPureFunction('fn1'), createPureFunction('fn2')]
|
|
152
|
+
|
|
153
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
154
|
+
|
|
155
|
+
expect(score.impurityQuality).toBeNull()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should calculate impurity quality for single impure function', () => {
|
|
159
|
+
const functions = [createImpureFunction('fn1', 75)]
|
|
160
|
+
|
|
161
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
162
|
+
|
|
163
|
+
expect(score.impurityQuality).toBe(75)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('health calculation', () => {
|
|
168
|
+
it('should calculate 100% health when all functions are ok', () => {
|
|
169
|
+
const functions = [
|
|
170
|
+
createPureFunction('pure1'),
|
|
171
|
+
createImpureFunction('impure1', 75, 'ok'),
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
175
|
+
|
|
176
|
+
expect(score.health).toBe(100)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should calculate health based on ok status percentage', () => {
|
|
180
|
+
const functions = [
|
|
181
|
+
createPureFunction('pure1'), // ok
|
|
182
|
+
createImpureFunction('impure1', 75, 'ok'),
|
|
183
|
+
createImpureFunction('impure2', 50, 'review'),
|
|
184
|
+
createImpureFunction('impure3', 30, 'refactor'),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
188
|
+
|
|
189
|
+
expect(score.health).toBe(50) // 2 ok out of 4
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should calculate 0% health when all functions need attention', () => {
|
|
193
|
+
const functions = [
|
|
194
|
+
createImpureFunction('fn1', 30, 'refactor'),
|
|
195
|
+
createImpureFunction('fn2', 35, 'refactor'),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
199
|
+
|
|
200
|
+
expect(score.health).toBe(0)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('status breakdown', () => {
|
|
205
|
+
it('should count functions by status', () => {
|
|
206
|
+
const functions = [
|
|
207
|
+
createPureFunction('pure1'), // ok
|
|
208
|
+
createPureFunction('pure2'), // ok
|
|
209
|
+
createImpureFunction('impure1', 75, 'ok'),
|
|
210
|
+
createImpureFunction('impure2', 50, 'review'),
|
|
211
|
+
createImpureFunction('impure3', 50, 'review'),
|
|
212
|
+
createImpureFunction('impure4', 30, 'refactor'),
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
216
|
+
|
|
217
|
+
expect(score.statusBreakdown.ok).toBe(3)
|
|
218
|
+
expect(score.statusBreakdown.review).toBe(2)
|
|
219
|
+
expect(score.statusBreakdown.refactor).toBe(1)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('line counts', () => {
|
|
224
|
+
it('should sum line counts by classification', () => {
|
|
225
|
+
const functions = [
|
|
226
|
+
createPureFunction('pure1', 10),
|
|
227
|
+
createPureFunction('pure2', 20),
|
|
228
|
+
createImpureFunction('impure1', 70, 'ok', 15),
|
|
229
|
+
createImpureFunction('impure2', 60, 'ok', 25),
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
233
|
+
|
|
234
|
+
expect(score.pureLineCount).toBe(30)
|
|
235
|
+
expect(score.impureLineCount).toBe(40)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('refactoring candidates', () => {
|
|
240
|
+
it('should include functions with refactor status', () => {
|
|
241
|
+
const functions = [
|
|
242
|
+
createPureFunction('pure1'),
|
|
243
|
+
createImpureFunction('impure1', 75, 'ok'),
|
|
244
|
+
createImpureFunction('refactorMe', 30, 'refactor', 50),
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
248
|
+
|
|
249
|
+
expect(score.refactoringCandidates).toHaveLength(1)
|
|
250
|
+
expect(score.refactoringCandidates[0]?.name).toBe('refactorMe')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should sort refactoring candidates by impact', () => {
|
|
254
|
+
const functions = [
|
|
255
|
+
createImpureFunction('small', 35, 'refactor', 10), // impact: 10 * 65 = 650
|
|
256
|
+
createImpureFunction('large', 25, 'refactor', 100), // impact: 100 * 75 = 7500
|
|
257
|
+
createImpureFunction('medium', 30, 'refactor', 50), // impact: 50 * 70 = 3500
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
const score = scoreFile('/test/file.ts', functions)
|
|
261
|
+
|
|
262
|
+
expect(score.refactoringCandidates[0]?.name).toBe('large')
|
|
263
|
+
expect(score.refactoringCandidates[1]?.name).toBe('medium')
|
|
264
|
+
expect(score.refactoringCandidates[2]?.name).toBe('small')
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('edge cases', () => {
|
|
269
|
+
it('should handle empty file', () => {
|
|
270
|
+
const score = scoreFile('/test/file.ts', [])
|
|
271
|
+
|
|
272
|
+
expect(score.purity).toBe(100)
|
|
273
|
+
expect(score.health).toBe(100)
|
|
274
|
+
expect(score.impurityQuality).toBeNull()
|
|
275
|
+
expect(score.allExcluded).toBe(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should handle type-only file', () => {
|
|
279
|
+
const score = scoreFile('/test/types.ts', [], true)
|
|
280
|
+
|
|
281
|
+
expect(score.purity).toBe(100)
|
|
282
|
+
expect(score.health).toBe(100)
|
|
283
|
+
expect(score.typeOnly).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('scoreDirectory', () => {
|
|
289
|
+
/**
|
|
290
|
+
* Helper to create a file score for testing
|
|
291
|
+
*/
|
|
292
|
+
function createFileScore(overrides: Partial<FileScore> = {}): FileScore {
|
|
293
|
+
return {
|
|
294
|
+
filePath: '/test/file.ts',
|
|
295
|
+
purity: 50,
|
|
296
|
+
impurityQuality: 60,
|
|
297
|
+
health: 75,
|
|
298
|
+
pureCount: 5,
|
|
299
|
+
impureCount: 5,
|
|
300
|
+
excludedCount: 0,
|
|
301
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
302
|
+
pureLineCount: 50,
|
|
303
|
+
impureLineCount: 100,
|
|
304
|
+
functions: [],
|
|
305
|
+
refactoringCandidates: [],
|
|
306
|
+
...overrides,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
it('should aggregate file scores with weighted averages', () => {
|
|
311
|
+
const fileScores = [
|
|
312
|
+
createFileScore({
|
|
313
|
+
filePath: '/test/a.ts',
|
|
314
|
+
purity: 80,
|
|
315
|
+
pureCount: 8,
|
|
316
|
+
impureCount: 2,
|
|
317
|
+
}),
|
|
318
|
+
createFileScore({
|
|
319
|
+
filePath: '/test/b.ts',
|
|
320
|
+
purity: 40,
|
|
321
|
+
pureCount: 4,
|
|
322
|
+
impureCount: 6,
|
|
323
|
+
}),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
const dirScore = scoreDirectory('/test', fileScores)
|
|
327
|
+
|
|
328
|
+
// Total: 12 pure, 8 impure = 60% purity
|
|
329
|
+
expect(dirScore.purity).toBe(60)
|
|
330
|
+
expect(dirScore.pureCount).toBe(12)
|
|
331
|
+
expect(dirScore.impureCount).toBe(8)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('should calculate weighted impurity quality', () => {
|
|
335
|
+
const fileScores = [
|
|
336
|
+
createFileScore({
|
|
337
|
+
filePath: '/test/a.ts',
|
|
338
|
+
impurityQuality: 80,
|
|
339
|
+
impureCount: 2,
|
|
340
|
+
}),
|
|
341
|
+
createFileScore({
|
|
342
|
+
filePath: '/test/b.ts',
|
|
343
|
+
impurityQuality: 40,
|
|
344
|
+
impureCount: 6,
|
|
345
|
+
}),
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
const dirScore = scoreDirectory('/test', fileScores)
|
|
349
|
+
|
|
350
|
+
// Weighted: (80*2 + 40*6) / 8 = 50
|
|
351
|
+
expect(dirScore.impurityQuality).toBe(50)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should aggregate status breakdown', () => {
|
|
355
|
+
const fileScores = [
|
|
356
|
+
createFileScore({
|
|
357
|
+
statusBreakdown: { ok: 5, review: 2, refactor: 1 },
|
|
358
|
+
}),
|
|
359
|
+
createFileScore({
|
|
360
|
+
statusBreakdown: { ok: 3, review: 4, refactor: 2 },
|
|
361
|
+
}),
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
const dirScore = scoreDirectory('/test', fileScores)
|
|
365
|
+
|
|
366
|
+
expect(dirScore.statusBreakdown.ok).toBe(8)
|
|
367
|
+
expect(dirScore.statusBreakdown.review).toBe(6)
|
|
368
|
+
expect(dirScore.statusBreakdown.refactor).toBe(3)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('should handle empty directory', () => {
|
|
372
|
+
const dirScore = scoreDirectory('/test', [])
|
|
373
|
+
|
|
374
|
+
expect(dirScore.purity).toBe(100)
|
|
375
|
+
expect(dirScore.health).toBe(100)
|
|
376
|
+
expect(dirScore.impurityQuality).toBeNull()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should exclude type-only files from aggregation', () => {
|
|
380
|
+
const fileScores = [
|
|
381
|
+
createFileScore({
|
|
382
|
+
filePath: '/test/a.ts',
|
|
383
|
+
purity: 60,
|
|
384
|
+
pureCount: 6,
|
|
385
|
+
impureCount: 4,
|
|
386
|
+
}),
|
|
387
|
+
createFileScore({
|
|
388
|
+
filePath: '/test/types.ts',
|
|
389
|
+
purity: 100,
|
|
390
|
+
pureCount: 0,
|
|
391
|
+
impureCount: 0,
|
|
392
|
+
typeOnly: true,
|
|
393
|
+
}),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
const dirScore = scoreDirectory('/test', fileScores)
|
|
397
|
+
|
|
398
|
+
expect(dirScore.purity).toBe(60)
|
|
399
|
+
expect(dirScore.pureCount).toBe(6)
|
|
400
|
+
expect(dirScore.impureCount).toBe(4)
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('scoreProject', () => {
|
|
405
|
+
/**
|
|
406
|
+
* Helper to create a directory score for testing
|
|
407
|
+
*/
|
|
408
|
+
function createDirScore(
|
|
409
|
+
overrides: Partial<DirectoryScore> = {},
|
|
410
|
+
): DirectoryScore {
|
|
411
|
+
return {
|
|
412
|
+
dirPath: '/test',
|
|
413
|
+
purity: 50,
|
|
414
|
+
impurityQuality: 60,
|
|
415
|
+
health: 75,
|
|
416
|
+
pureCount: 10,
|
|
417
|
+
impureCount: 10,
|
|
418
|
+
excludedCount: 0,
|
|
419
|
+
statusBreakdown: { ok: 15, review: 3, refactor: 2 },
|
|
420
|
+
pureLineCount: 100,
|
|
421
|
+
impureLineCount: 200,
|
|
422
|
+
fileScores: [],
|
|
423
|
+
...overrides,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
it('should aggregate directory scores', () => {
|
|
428
|
+
const dirScores = [
|
|
429
|
+
createDirScore({
|
|
430
|
+
dirPath: '/src/services',
|
|
431
|
+
purity: 40,
|
|
432
|
+
pureCount: 4,
|
|
433
|
+
impureCount: 6,
|
|
434
|
+
}),
|
|
435
|
+
createDirScore({
|
|
436
|
+
dirPath: '/src/utils',
|
|
437
|
+
purity: 90,
|
|
438
|
+
pureCount: 9,
|
|
439
|
+
impureCount: 1,
|
|
440
|
+
}),
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
const projectScore = scoreProject(dirScores)
|
|
444
|
+
|
|
445
|
+
// Total: 13 pure, 7 impure = 65% purity
|
|
446
|
+
expect(projectScore.purity).toBe(65)
|
|
447
|
+
expect(projectScore.pureCount).toBe(13)
|
|
448
|
+
expect(projectScore.impureCount).toBe(7)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('should include timestamp and commit hash', () => {
|
|
452
|
+
const dirScores = [createDirScore()]
|
|
453
|
+
|
|
454
|
+
const projectScore = scoreProject(dirScores, {
|
|
455
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
456
|
+
commitHash: 'abc123',
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
expect(projectScore.timestamp).toBe('2024-01-01T00:00:00.000Z')
|
|
460
|
+
expect(projectScore.commitHash).toBe('abc123')
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('should mark subset analysis', () => {
|
|
464
|
+
const dirScores = [createDirScore()]
|
|
465
|
+
|
|
466
|
+
const projectScore = scoreProject(dirScores, {
|
|
467
|
+
subset: true,
|
|
468
|
+
filesGlob: 'src/services/**/*.ts',
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
expect(projectScore.subset).toBe(true)
|
|
472
|
+
expect(projectScore.filesGlob).toBe('src/services/**/*.ts')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('should include errors when provided', () => {
|
|
476
|
+
const dirScores = [createDirScore()]
|
|
477
|
+
const errors = [
|
|
478
|
+
{ filePath: '/test/bad.ts', error: 'Syntax error' },
|
|
479
|
+
{ filePath: '/test/worse.ts', error: 'Parse failed' },
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
const projectScore = scoreProject(dirScores, { errors })
|
|
483
|
+
|
|
484
|
+
expect(projectScore.errors).toHaveLength(2)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('should collect refactoring candidates from all files', () => {
|
|
488
|
+
const dirScores = [
|
|
489
|
+
createDirScore({
|
|
490
|
+
fileScores: [
|
|
491
|
+
{
|
|
492
|
+
filePath: '/src/a.ts',
|
|
493
|
+
purity: 50,
|
|
494
|
+
impurityQuality: 60,
|
|
495
|
+
health: 75,
|
|
496
|
+
pureCount: 5,
|
|
497
|
+
impureCount: 5,
|
|
498
|
+
excludedCount: 0,
|
|
499
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
500
|
+
pureLineCount: 50,
|
|
501
|
+
impureLineCount: 100,
|
|
502
|
+
functions: [],
|
|
503
|
+
refactoringCandidates: [
|
|
504
|
+
{
|
|
505
|
+
name: 'fn1',
|
|
506
|
+
startLine: 10,
|
|
507
|
+
bodyLineCount: 50,
|
|
508
|
+
qualityScore: 30,
|
|
509
|
+
markers: ['database-call'],
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
}),
|
|
515
|
+
createDirScore({
|
|
516
|
+
fileScores: [
|
|
517
|
+
{
|
|
518
|
+
filePath: '/src/b.ts',
|
|
519
|
+
purity: 50,
|
|
520
|
+
impurityQuality: 60,
|
|
521
|
+
health: 75,
|
|
522
|
+
pureCount: 5,
|
|
523
|
+
impureCount: 5,
|
|
524
|
+
excludedCount: 0,
|
|
525
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
526
|
+
pureLineCount: 50,
|
|
527
|
+
impureLineCount: 100,
|
|
528
|
+
functions: [],
|
|
529
|
+
refactoringCandidates: [
|
|
530
|
+
{
|
|
531
|
+
name: 'fn2',
|
|
532
|
+
startLine: 20,
|
|
533
|
+
bodyLineCount: 100,
|
|
534
|
+
qualityScore: 20,
|
|
535
|
+
markers: ['network-fetch'],
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
}),
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
const projectScore = scoreProject(dirScores)
|
|
544
|
+
|
|
545
|
+
expect(projectScore.refactoringCandidates).toHaveLength(2)
|
|
546
|
+
// Should be sorted by impact (bodyLineCount * (100 - qualityScore))
|
|
547
|
+
// fn2: 100 * 80 = 8000
|
|
548
|
+
// fn1: 50 * 70 = 3500
|
|
549
|
+
expect(projectScore.refactoringCandidates[0]?.name).toBe('fn2')
|
|
550
|
+
expect(projectScore.refactoringCandidates[1]?.name).toBe('fn1')
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('should handle empty project', () => {
|
|
554
|
+
const projectScore = scoreProject([])
|
|
555
|
+
|
|
556
|
+
expect(projectScore.purity).toBe(100)
|
|
557
|
+
expect(projectScore.health).toBe(100)
|
|
558
|
+
expect(projectScore.allExcluded).toBe(true)
|
|
559
|
+
})
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
describe('groupFilesByDirectory', () => {
|
|
563
|
+
it('should group files by their directory path', () => {
|
|
564
|
+
const fileScores: FileScore[] = [
|
|
565
|
+
{
|
|
566
|
+
filePath: '/src/services/a.ts',
|
|
567
|
+
purity: 50,
|
|
568
|
+
impurityQuality: 60,
|
|
569
|
+
health: 75,
|
|
570
|
+
pureCount: 5,
|
|
571
|
+
impureCount: 5,
|
|
572
|
+
excludedCount: 0,
|
|
573
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
574
|
+
pureLineCount: 50,
|
|
575
|
+
impureLineCount: 100,
|
|
576
|
+
functions: [],
|
|
577
|
+
refactoringCandidates: [],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
filePath: '/src/services/b.ts',
|
|
581
|
+
purity: 60,
|
|
582
|
+
impurityQuality: 70,
|
|
583
|
+
health: 80,
|
|
584
|
+
pureCount: 6,
|
|
585
|
+
impureCount: 4,
|
|
586
|
+
excludedCount: 0,
|
|
587
|
+
statusBreakdown: { ok: 8, review: 1, refactor: 1 },
|
|
588
|
+
pureLineCount: 60,
|
|
589
|
+
impureLineCount: 80,
|
|
590
|
+
functions: [],
|
|
591
|
+
refactoringCandidates: [],
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
filePath: '/src/utils/c.ts',
|
|
595
|
+
purity: 90,
|
|
596
|
+
impurityQuality: 80,
|
|
597
|
+
health: 95,
|
|
598
|
+
pureCount: 9,
|
|
599
|
+
impureCount: 1,
|
|
600
|
+
excludedCount: 0,
|
|
601
|
+
statusBreakdown: { ok: 9, review: 1, refactor: 0 },
|
|
602
|
+
pureLineCount: 90,
|
|
603
|
+
impureLineCount: 10,
|
|
604
|
+
functions: [],
|
|
605
|
+
refactoringCandidates: [],
|
|
606
|
+
},
|
|
607
|
+
]
|
|
608
|
+
|
|
609
|
+
const groups = groupFilesByDirectory(fileScores)
|
|
610
|
+
|
|
611
|
+
expect(groups.size).toBe(2)
|
|
612
|
+
expect(groups.get('/src/services')).toHaveLength(2)
|
|
613
|
+
expect(groups.get('/src/utils')).toHaveLength(1)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('should handle files in root directory', () => {
|
|
617
|
+
const fileScores: FileScore[] = [
|
|
618
|
+
{
|
|
619
|
+
filePath: 'root.ts',
|
|
620
|
+
purity: 50,
|
|
621
|
+
impurityQuality: 60,
|
|
622
|
+
health: 75,
|
|
623
|
+
pureCount: 5,
|
|
624
|
+
impureCount: 5,
|
|
625
|
+
excludedCount: 0,
|
|
626
|
+
statusBreakdown: { ok: 7, review: 2, refactor: 1 },
|
|
627
|
+
pureLineCount: 50,
|
|
628
|
+
impureLineCount: 100,
|
|
629
|
+
functions: [],
|
|
630
|
+
refactoringCandidates: [],
|
|
631
|
+
},
|
|
632
|
+
]
|
|
633
|
+
|
|
634
|
+
const groups = groupFilesByDirectory(fileScores)
|
|
635
|
+
|
|
636
|
+
expect(groups.size).toBe(1)
|
|
637
|
+
expect(groups.get('.')).toHaveLength(1)
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
describe('calculateDelta', () => {
|
|
642
|
+
function createProjectScore(
|
|
643
|
+
overrides: Partial<ProjectScore> = {},
|
|
644
|
+
): ProjectScore {
|
|
645
|
+
return {
|
|
646
|
+
purity: 50,
|
|
647
|
+
impurityQuality: 60,
|
|
648
|
+
health: 75,
|
|
649
|
+
pureCount: 10,
|
|
650
|
+
impureCount: 10,
|
|
651
|
+
excludedCount: 0,
|
|
652
|
+
statusBreakdown: { ok: 15, review: 3, refactor: 2 },
|
|
653
|
+
pureLineCount: 100,
|
|
654
|
+
impureLineCount: 200,
|
|
655
|
+
directoryScores: [],
|
|
656
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
657
|
+
commitHash: null,
|
|
658
|
+
refactoringCandidates: [],
|
|
659
|
+
...overrides,
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
it('should calculate positive delta for improvement', () => {
|
|
664
|
+
const previous = createProjectScore({
|
|
665
|
+
purity: 40,
|
|
666
|
+
health: 60,
|
|
667
|
+
pureCount: 8,
|
|
668
|
+
impureCount: 12,
|
|
669
|
+
})
|
|
670
|
+
const current = createProjectScore({
|
|
671
|
+
purity: 50,
|
|
672
|
+
health: 75,
|
|
673
|
+
pureCount: 10,
|
|
674
|
+
impureCount: 10,
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
const delta = calculateDelta(current, previous)
|
|
678
|
+
|
|
679
|
+
expect(delta.purityDelta).toBe(10)
|
|
680
|
+
expect(delta.healthDelta).toBe(15)
|
|
681
|
+
expect(delta.pureCountDelta).toBe(2)
|
|
682
|
+
expect(delta.impureCountDelta).toBe(-2)
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
it('should calculate negative delta for regression', () => {
|
|
686
|
+
const previous = createProjectScore({
|
|
687
|
+
purity: 60,
|
|
688
|
+
health: 80,
|
|
689
|
+
})
|
|
690
|
+
const current = createProjectScore({
|
|
691
|
+
purity: 50,
|
|
692
|
+
health: 75,
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
const delta = calculateDelta(current, previous)
|
|
696
|
+
|
|
697
|
+
expect(delta.purityDelta).toBe(-10)
|
|
698
|
+
expect(delta.healthDelta).toBe(-5)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('should handle null impurity quality', () => {
|
|
702
|
+
const previous = createProjectScore({ impurityQuality: null })
|
|
703
|
+
const current = createProjectScore({ impurityQuality: 70 })
|
|
704
|
+
|
|
705
|
+
const delta = calculateDelta(current, previous)
|
|
706
|
+
|
|
707
|
+
expect(delta.impurityQualityDelta).toBeNull()
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
it('should calculate impurity quality delta when both have values', () => {
|
|
711
|
+
const previous = createProjectScore({ impurityQuality: 50 })
|
|
712
|
+
const current = createProjectScore({ impurityQuality: 70 })
|
|
713
|
+
|
|
714
|
+
const delta = calculateDelta(current, previous)
|
|
715
|
+
|
|
716
|
+
expect(delta.impurityQualityDelta).toBe(20)
|
|
717
|
+
})
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
describe('getDiagnosticInsights', () => {
|
|
721
|
+
it('should provide insight for high purity + low impurity quality', () => {
|
|
722
|
+
const score = { purity: 70, impurityQuality: 40, health: 60 }
|
|
723
|
+
|
|
724
|
+
const insights = getDiagnosticInsights(score)
|
|
725
|
+
|
|
726
|
+
expect(
|
|
727
|
+
insights.some(i => i.includes('pure') && i.includes('tangled')),
|
|
728
|
+
).toBe(true)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
it('should provide insight for low purity + high impurity quality', () => {
|
|
732
|
+
const score = { purity: 30, impurityQuality: 80, health: 70 }
|
|
733
|
+
|
|
734
|
+
const insights = getDiagnosticInsights(score)
|
|
735
|
+
|
|
736
|
+
expect(
|
|
737
|
+
insights.some(i => i.includes('I/O') && i.includes('well-structured')),
|
|
738
|
+
).toBe(true)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
it('should provide insight for low purity + low impurity quality', () => {
|
|
742
|
+
const score = { purity: 30, impurityQuality: 40, health: 40 }
|
|
743
|
+
|
|
744
|
+
const insights = getDiagnosticInsights(score)
|
|
745
|
+
|
|
746
|
+
expect(insights.some(i => i.includes('technical debt'))).toBe(true)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('should provide insight for low health', () => {
|
|
750
|
+
const score = { purity: 50, impurityQuality: 60, health: 40 }
|
|
751
|
+
|
|
752
|
+
const insights = getDiagnosticInsights(score)
|
|
753
|
+
|
|
754
|
+
expect(
|
|
755
|
+
insights.some(i => i.includes('attention') || i.includes('refactoring')),
|
|
756
|
+
).toBe(true)
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it('should return empty array for healthy codebase', () => {
|
|
760
|
+
const score = { purity: 70, impurityQuality: 80, health: 85 }
|
|
761
|
+
|
|
762
|
+
const insights = getDiagnosticInsights(score)
|
|
763
|
+
|
|
764
|
+
// High purity, high quality, high health - may have some insights or none
|
|
765
|
+
// At least verify no errors
|
|
766
|
+
expect(Array.isArray(insights)).toBe(true)
|
|
767
|
+
})
|
|
768
|
+
})
|