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.
@@ -16,20 +16,179 @@ import type {
16
16
  RefactoringCandidate,
17
17
  StatusBreakdown,
18
18
  } from '../types.js'
19
- import { getDiagnosticInsights } from '../scoring/scorer.js'
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 (verbose) {
43
- printDirectoryBreakdown(score.directoryScores)
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 healthColor =
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 directory breakdown (verbose mode)
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 >= 50 ? chalk.yellow : chalk.red
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
- if (percent >= 70) {
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: false for smaller output)
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 { pretty = true, includeFunction = false } = options
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
- const reportData = prepareReportData(score, includeFunction)
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)