fcis 0.1.0 → 0.2.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/003-code-cleanup-consolidation.md +242 -0
- package/.plans/004-directory-depth-rollup.md +408 -0
- package/.plans/005-code-refinements.md +210 -0
- package/.plans/006-minor-refinements.md +149 -0
- package/.plans/007-compositional-function-scoring.md +514 -0
- package/README.md +38 -3
- package/TECHNICAL.md +125 -2
- package/dist/cli.js +595 -327
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +409 -240
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli-utils.ts +201 -0
- package/src/cli.ts +99 -117
- package/src/detection/markers.ts +0 -222
- package/src/extraction/extract-functions.ts +106 -2
- package/src/extraction/extractor.ts +35 -74
- package/src/reporting/report-console.ts +188 -102
- package/src/reporting/report-json.ts +26 -3
- package/src/scoring/scorer.ts +425 -160
- package/src/types.ts +9 -2
- package/tests/classifier.test.ts +0 -1
- package/tests/cli.test.ts +356 -0
- package/tests/detect-markers.test.ts +1 -3
- package/tests/extractor.test.ts +95 -1
- package/tests/integration.test.ts +344 -0
- package/tests/report-console.test.ts +92 -0
- package/tests/scorer.test.ts +886 -0
|
@@ -16,20 +16,179 @@ import type {
|
|
|
16
16
|
RefactoringCandidate,
|
|
17
17
|
StatusBreakdown,
|
|
18
18
|
} from '../types.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getDiagnosticInsights,
|
|
21
|
+
rollupDirectoriesByDepth,
|
|
22
|
+
} from '../scoring/scorer.js'
|
|
20
23
|
import {
|
|
21
24
|
getStatusColor,
|
|
22
25
|
getStatusEmoji,
|
|
23
26
|
} from '../classification/derive-status.js'
|
|
24
27
|
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Color Helpers
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
type ChalkFn = typeof chalk.green
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the appropriate chalk color function for a metric value (0-100)
|
|
36
|
+
* - Green: >= 70
|
|
37
|
+
* - Yellow: >= 50
|
|
38
|
+
* - Red: < 50
|
|
39
|
+
*/
|
|
40
|
+
function getMetricColor(value: number): ChalkFn {
|
|
41
|
+
if (value >= 70) return chalk.green
|
|
42
|
+
if (value >= 50) return chalk.yellow
|
|
43
|
+
return chalk.red
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the appropriate chalk color function for a nullable metric value
|
|
48
|
+
* Returns gray for null values
|
|
49
|
+
*/
|
|
50
|
+
function getMetricColorNullable(value: number | null): ChalkFn {
|
|
51
|
+
if (value === null) return chalk.gray
|
|
52
|
+
return getMetricColor(value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Path Helpers
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert an absolute path to a path relative to the current working directory
|
|
61
|
+
*/
|
|
62
|
+
function toRelativePath(absolutePath: string): string {
|
|
63
|
+
const cwd = process.cwd()
|
|
64
|
+
if (absolutePath.startsWith(cwd)) {
|
|
65
|
+
const relative = path.relative(cwd, absolutePath)
|
|
66
|
+
return relative || '.'
|
|
67
|
+
}
|
|
68
|
+
return absolutePath
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Relativize directory paths for use with pure scoring functions
|
|
73
|
+
* This is a shell helper that performs path I/O before calling pure functions
|
|
74
|
+
*/
|
|
75
|
+
export function relativizeDirectoryPaths(
|
|
76
|
+
directories: DirectoryScore[],
|
|
77
|
+
projectRoot: string,
|
|
78
|
+
): DirectoryScore[] {
|
|
79
|
+
return directories.map(d => ({
|
|
80
|
+
...d,
|
|
81
|
+
dirPath: path.relative(projectRoot, d.dirPath).replace(/\\/g, '/'),
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Directory Table Printing
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
type DirectoryTableOptions = {
|
|
90
|
+
title: string
|
|
91
|
+
includeQualityColumn: boolean
|
|
92
|
+
sortBy: 'health-asc' | 'preserve-order'
|
|
93
|
+
emptyMessage?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Unified function for printing directory tables
|
|
98
|
+
*/
|
|
99
|
+
function printDirectoryTable(
|
|
100
|
+
directories: DirectoryScore[],
|
|
101
|
+
options: DirectoryTableOptions,
|
|
102
|
+
): void {
|
|
103
|
+
const { title, includeQualityColumn, sortBy, emptyMessage } = options
|
|
104
|
+
|
|
105
|
+
// Filter to directories with functions
|
|
106
|
+
let filtered = directories.filter(d => d.pureCount + d.impureCount > 0)
|
|
107
|
+
|
|
108
|
+
// Sort based on option
|
|
109
|
+
if (sortBy === 'health-asc') {
|
|
110
|
+
filtered = [...filtered].sort((a, b) => a.health - b.health)
|
|
111
|
+
}
|
|
112
|
+
// 'preserve-order' - keep input order (caller is responsible for pre-sorting if needed)
|
|
113
|
+
|
|
114
|
+
console.log(chalk.bold(title))
|
|
115
|
+
console.log(chalk.gray('─'.repeat(80)))
|
|
116
|
+
|
|
117
|
+
if (includeQualityColumn) {
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray(
|
|
120
|
+
padEnd('Directory', 40) +
|
|
121
|
+
padEnd('Health', 10) +
|
|
122
|
+
padEnd('Purity', 10) +
|
|
123
|
+
padEnd('Quality', 10) +
|
|
124
|
+
padEnd('Functions', 10),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
} else {
|
|
128
|
+
console.log(
|
|
129
|
+
chalk.gray(
|
|
130
|
+
padEnd('Directory', 45) +
|
|
131
|
+
padEnd('Health', 10) +
|
|
132
|
+
padEnd('Purity', 10) +
|
|
133
|
+
padEnd('Functions', 15),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.gray('─'.repeat(80)))
|
|
138
|
+
|
|
139
|
+
if (filtered.length === 0) {
|
|
140
|
+
if (emptyMessage) {
|
|
141
|
+
console.log(chalk.gray(` ${emptyMessage}`))
|
|
142
|
+
}
|
|
143
|
+
console.log()
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const dir of filtered) {
|
|
148
|
+
const healthColor = getMetricColor(dir.health)
|
|
149
|
+
const purityColor = getMetricColor(dir.purity)
|
|
150
|
+
const total = dir.pureCount + dir.impureCount
|
|
151
|
+
|
|
152
|
+
if (includeQualityColumn) {
|
|
153
|
+
const qualityStr =
|
|
154
|
+
dir.impurityQuality !== null
|
|
155
|
+
? dir.impurityQuality.toFixed(0) + '%'
|
|
156
|
+
: '—'
|
|
157
|
+
const qualityColor = getMetricColorNullable(dir.impurityQuality)
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
padEnd(dir.dirPath, 40) +
|
|
161
|
+
healthColor(padEnd(dir.health.toFixed(0) + '%', 10)) +
|
|
162
|
+
purityColor(padEnd(dir.purity.toFixed(0) + '%', 10)) +
|
|
163
|
+
qualityColor(padEnd(qualityStr, 10)) +
|
|
164
|
+
padEnd(`${dir.pureCount}/${total}`, 10),
|
|
165
|
+
)
|
|
166
|
+
} else {
|
|
167
|
+
const relativePath = toRelativePath(dir.dirPath)
|
|
168
|
+
console.log(
|
|
169
|
+
padEnd(relativePath, 45) +
|
|
170
|
+
healthColor(padEnd(dir.health.toFixed(0) + '%', 10)) +
|
|
171
|
+
purityColor(padEnd(dir.purity.toFixed(0) + '%', 10)) +
|
|
172
|
+
padEnd(`${dir.pureCount}/${total}`, 15),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Main Report Functions
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
25
184
|
/**
|
|
26
185
|
* Generate and print the full console report
|
|
27
186
|
*/
|
|
28
187
|
export function printConsoleReport(
|
|
29
188
|
score: ProjectScore,
|
|
30
|
-
options: { verbose?: boolean } = {},
|
|
189
|
+
options: { verbose?: boolean; dirDepth?: number } = {},
|
|
31
190
|
): void {
|
|
32
|
-
const { verbose = false } = options
|
|
191
|
+
const { verbose = false, dirDepth } = options
|
|
33
192
|
|
|
34
193
|
printHeader()
|
|
35
194
|
printProjectSummary(score)
|
|
@@ -39,8 +198,25 @@ export function printConsoleReport(
|
|
|
39
198
|
)
|
|
40
199
|
printInsights(score)
|
|
41
200
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
201
|
+
if (dirDepth !== undefined) {
|
|
202
|
+
// Relativize paths in shell layer, then call pure rollup function
|
|
203
|
+
const relativized = relativizeDirectoryPaths(
|
|
204
|
+
score.directoryScores,
|
|
205
|
+
process.cwd(),
|
|
206
|
+
)
|
|
207
|
+
const rolledUp = rollupDirectoriesByDepth(relativized, dirDepth)
|
|
208
|
+
printDirectoryTable(rolledUp, {
|
|
209
|
+
title: `Directory Breakdown (depth=${dirDepth}):`,
|
|
210
|
+
includeQualityColumn: true,
|
|
211
|
+
sortBy: 'preserve-order',
|
|
212
|
+
emptyMessage: 'No directories with functions found',
|
|
213
|
+
})
|
|
214
|
+
} else if (verbose) {
|
|
215
|
+
printDirectoryTable(score.directoryScores, {
|
|
216
|
+
title: 'Directory Breakdown:',
|
|
217
|
+
includeQualityColumn: false,
|
|
218
|
+
sortBy: 'health-asc',
|
|
219
|
+
})
|
|
44
220
|
} else {
|
|
45
221
|
printWorstDirectories(score.directoryScores)
|
|
46
222
|
}
|
|
@@ -71,19 +247,8 @@ function printHeader(): void {
|
|
|
71
247
|
*/
|
|
72
248
|
function printProjectSummary(score: ProjectScore): void {
|
|
73
249
|
const healthBar = createProgressBar(score.health, 25)
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
score.health >= 70
|
|
77
|
-
? chalk.green
|
|
78
|
-
: score.health >= 50
|
|
79
|
-
? chalk.yellow
|
|
80
|
-
: chalk.red
|
|
81
|
-
const purityColor =
|
|
82
|
-
score.purity >= 70
|
|
83
|
-
? chalk.green
|
|
84
|
-
: score.purity >= 50
|
|
85
|
-
? chalk.yellow
|
|
86
|
-
: chalk.red
|
|
250
|
+
const healthColor = getMetricColor(score.health)
|
|
251
|
+
const purityColor = getMetricColor(score.purity)
|
|
87
252
|
|
|
88
253
|
console.log(
|
|
89
254
|
`Project Health: ${healthColor(score.health.toFixed(0) + '%')} ${healthBar}`,
|
|
@@ -93,12 +258,7 @@ function printProjectSummary(score: ProjectScore): void {
|
|
|
93
258
|
)
|
|
94
259
|
|
|
95
260
|
if (score.impurityQuality !== null) {
|
|
96
|
-
const qualityColor =
|
|
97
|
-
score.impurityQuality >= 70
|
|
98
|
-
? chalk.green
|
|
99
|
-
: score.impurityQuality >= 50
|
|
100
|
-
? chalk.yellow
|
|
101
|
-
: chalk.red
|
|
261
|
+
const qualityColor = getMetricColor(score.impurityQuality)
|
|
102
262
|
console.log(
|
|
103
263
|
` Impurity Quality: ${qualityColor(score.impurityQuality.toFixed(0) + '%')} average`,
|
|
104
264
|
)
|
|
@@ -148,58 +308,7 @@ function printInsights(score: ProjectScore): void {
|
|
|
148
308
|
}
|
|
149
309
|
|
|
150
310
|
/**
|
|
151
|
-
* Print
|
|
152
|
-
*/
|
|
153
|
-
function printDirectoryBreakdown(directories: DirectoryScore[]): void {
|
|
154
|
-
// Sort by health ascending (worst first)
|
|
155
|
-
const sorted = [...directories]
|
|
156
|
-
.filter(d => d.pureCount + d.impureCount > 0)
|
|
157
|
-
.sort((a, b) => a.health - b.health)
|
|
158
|
-
|
|
159
|
-
if (sorted.length === 0) return
|
|
160
|
-
|
|
161
|
-
console.log(chalk.bold('Directory Breakdown:'))
|
|
162
|
-
console.log(chalk.gray('─'.repeat(80)))
|
|
163
|
-
console.log(
|
|
164
|
-
chalk.gray(
|
|
165
|
-
padEnd('Directory', 45) +
|
|
166
|
-
padEnd('Health', 10) +
|
|
167
|
-
padEnd('Purity', 10) +
|
|
168
|
-
padEnd('Functions', 15),
|
|
169
|
-
),
|
|
170
|
-
)
|
|
171
|
-
console.log(chalk.gray('─'.repeat(80)))
|
|
172
|
-
|
|
173
|
-
for (const dir of sorted) {
|
|
174
|
-
const healthColor =
|
|
175
|
-
dir.health >= 70
|
|
176
|
-
? chalk.green
|
|
177
|
-
: dir.health >= 50
|
|
178
|
-
? chalk.yellow
|
|
179
|
-
: chalk.red
|
|
180
|
-
const purityColor =
|
|
181
|
-
dir.purity >= 70
|
|
182
|
-
? chalk.green
|
|
183
|
-
: dir.purity >= 50
|
|
184
|
-
? chalk.yellow
|
|
185
|
-
: chalk.red
|
|
186
|
-
|
|
187
|
-
const relativePath = toRelativePath(dir.dirPath)
|
|
188
|
-
const total = dir.pureCount + dir.impureCount
|
|
189
|
-
|
|
190
|
-
console.log(
|
|
191
|
-
padEnd(relativePath, 45) +
|
|
192
|
-
healthColor(padEnd(dir.health.toFixed(0) + '%', 10)) +
|
|
193
|
-
purityColor(padEnd(dir.purity.toFixed(0) + '%', 10)) +
|
|
194
|
-
padEnd(`${dir.pureCount}/${total}`, 15),
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Print worst directories (non-verbose mode)
|
|
311
|
+
* Print worst directories (non-verbose mode) - compact format
|
|
203
312
|
*/
|
|
204
313
|
function printWorstDirectories(directories: DirectoryScore[]): void {
|
|
205
314
|
const sorted = [...directories]
|
|
@@ -212,7 +321,7 @@ function printWorstDirectories(directories: DirectoryScore[]): void {
|
|
|
212
321
|
console.log(chalk.bold('Directories Needing Attention:'))
|
|
213
322
|
|
|
214
323
|
for (const dir of sorted) {
|
|
215
|
-
const healthColor = dir.health
|
|
324
|
+
const healthColor = getMetricColor(dir.health)
|
|
216
325
|
const total = dir.pureCount + dir.impureCount
|
|
217
326
|
const relativePath = toRelativePath(dir.dirPath)
|
|
218
327
|
|
|
@@ -321,13 +430,7 @@ function createProgressBar(percent: number, width: number): string {
|
|
|
321
430
|
|
|
322
431
|
const bar = filledChar.repeat(filled) + emptyChar.repeat(empty)
|
|
323
432
|
|
|
324
|
-
|
|
325
|
-
return chalk.green(bar)
|
|
326
|
-
} else if (percent >= 50) {
|
|
327
|
-
return chalk.yellow(bar)
|
|
328
|
-
} else {
|
|
329
|
-
return chalk.red(bar)
|
|
330
|
-
}
|
|
433
|
+
return getMetricColor(percent)(bar)
|
|
331
434
|
}
|
|
332
435
|
|
|
333
436
|
/**
|
|
@@ -346,18 +449,6 @@ export function generateSummaryLine(score: ProjectScore): string {
|
|
|
346
449
|
return parts.join(' | ')
|
|
347
450
|
}
|
|
348
451
|
|
|
349
|
-
/**
|
|
350
|
-
* Convert an absolute path to a path relative to the current working directory
|
|
351
|
-
*/
|
|
352
|
-
function toRelativePath(absolutePath: string): string {
|
|
353
|
-
const cwd = process.cwd()
|
|
354
|
-
if (absolutePath.startsWith(cwd)) {
|
|
355
|
-
const relative = path.relative(cwd, absolutePath)
|
|
356
|
-
return relative || '.'
|
|
357
|
-
}
|
|
358
|
-
return absolutePath
|
|
359
|
-
}
|
|
360
|
-
|
|
361
452
|
/**
|
|
362
453
|
* Pad end of string to a fixed length
|
|
363
454
|
*/
|
|
@@ -370,12 +461,7 @@ function padEnd(str: string, length: number): string {
|
|
|
370
461
|
* Print a file-level report (for verbose output)
|
|
371
462
|
*/
|
|
372
463
|
export function printFileReport(fileScore: FileScore): void {
|
|
373
|
-
const healthColor =
|
|
374
|
-
fileScore.health >= 70
|
|
375
|
-
? chalk.green
|
|
376
|
-
: fileScore.health >= 50
|
|
377
|
-
? chalk.yellow
|
|
378
|
-
: chalk.red
|
|
464
|
+
const healthColor = getMetricColor(fileScore.health)
|
|
379
465
|
const total = fileScore.pureCount + fileScore.impureCount
|
|
380
466
|
const relativePath = toRelativePath(fileScore.filePath)
|
|
381
467
|
|
|
@@ -10,19 +10,23 @@
|
|
|
10
10
|
* - Directory and file breakdowns
|
|
11
11
|
* - Refactoring candidates
|
|
12
12
|
* - Any errors encountered during analysis
|
|
13
|
+
* - Rolled-up directories (when --dir-depth is used)
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import * as fs from 'node:fs'
|
|
16
17
|
import * as path from 'node:path'
|
|
17
18
|
|
|
18
19
|
import type { ProjectScore } from '../types.js'
|
|
20
|
+
import { rollupDirectoriesByDepth } from '../scoring/scorer.js'
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* JSON report options
|
|
22
24
|
*/
|
|
23
25
|
export type JsonReportOptions = {
|
|
24
26
|
pretty?: boolean // Pretty print with indentation (default: true)
|
|
25
|
-
includeFunction?: boolean // Include function details (default:
|
|
27
|
+
includeFunction?: boolean // Include function details (default: true)
|
|
28
|
+
dirDepth?: number // Roll up directories to specified depth
|
|
29
|
+
projectRoot?: string // Project root for computing relative paths (default: cwd)
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -36,10 +40,29 @@ export function generateJsonReport(
|
|
|
36
40
|
score: ProjectScore,
|
|
37
41
|
options: JsonReportOptions = {},
|
|
38
42
|
): string {
|
|
39
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
pretty = true,
|
|
45
|
+
includeFunction = true,
|
|
46
|
+
dirDepth,
|
|
47
|
+
projectRoot = process.cwd(),
|
|
48
|
+
} = options
|
|
40
49
|
|
|
41
50
|
// Create a copy to potentially filter out function details
|
|
42
|
-
|
|
51
|
+
let reportData = prepareReportData(score, includeFunction)
|
|
52
|
+
|
|
53
|
+
// Add rolled-up directories if depth is specified
|
|
54
|
+
if (dirDepth !== undefined) {
|
|
55
|
+
// Relativize paths in shell layer before calling pure rollup function
|
|
56
|
+
const relativized = score.directoryScores.map(d => ({
|
|
57
|
+
...d,
|
|
58
|
+
dirPath: path.relative(projectRoot, d.dirPath).replace(/\\/g, '/'),
|
|
59
|
+
}))
|
|
60
|
+
const rolledUp = rollupDirectoriesByDepth(relativized, dirDepth)
|
|
61
|
+
reportData = {
|
|
62
|
+
...reportData,
|
|
63
|
+
rolledUpDirectories: rolledUp,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
43
66
|
|
|
44
67
|
if (pretty) {
|
|
45
68
|
return JSON.stringify(reportData, null, 2)
|