fcis 0.1.0 → 0.2.1

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.
@@ -0,0 +1,210 @@
1
+ # Plan 005: Code Refinements
2
+
3
+ ## Background
4
+
5
+ Following the implementation of Plan 004 (Directory Depth Rollup), a code review identified several opportunities for simplification, reducing redundancy, and improving adherence to the Functional Core / Imperative Shell (FC/IS) pattern.
6
+
7
+ The FCIS analyzer is designed to follow FC/IS principles itself, with `scorer.ts` documented as a "PURE module." However, the recent addition of `rollupDirectoriesByDepth()` introduced a `path` import that performs I/O-adjacent operations, breaking this contract.
8
+
9
+ Additionally, several patterns are repeated throughout the codebase that could be consolidated for maintainability.
10
+
11
+ ## Problem Statement
12
+
13
+ 1. **FC/IS violation:** `scorer.ts` imports `node:path` and uses `path.relative()` in `rollupDirectoriesByDepth()`, violating its documented pure module contract
14
+ 2. **Duplicate color logic:** Health/purity/quality color selection is repeated 6+ times in `report-console.ts`
15
+ 3. **Duplicate empty-case handling:** `rollupDirectoriesByDepth()` manually constructs empty metrics instead of reusing `aggregateMetrics([])`
16
+ 4. **Similar directory printing functions:** `printDirectoryBreakdown()` and `printRolledUpDirectories()` share nearly identical structure
17
+
18
+ ## Success Criteria
19
+
20
+ 1. `scorer.ts` has no `node:path` import — path relativization happens in the shell layer
21
+ 2. A single `getMetricColor()` function replaces all repeated color logic
22
+ 3. Empty directory metrics use `aggregateMetrics([])` for consistency
23
+ 4. Directory printing functions are unified into a single parameterized function
24
+ 5. All existing tests continue to pass
25
+ 6. No regression in CLI behavior
26
+
27
+ ## The Gap
28
+
29
+ | Aspect | Current State | Target State |
30
+ |--------|---------------|--------------|
31
+ | `scorer.ts` purity | Imports `node:path` | Pure module, no I/O imports |
32
+ | Color logic | Repeated 6+ times | Single reusable function |
33
+ | Empty metrics | Manual construction | Uses `aggregateMetrics([])` |
34
+ | Directory printing | Two similar functions | Single unified function |
35
+
36
+ ## Phases and Tasks
37
+
38
+ ### Phase 1: Extract Color Helper ✅
39
+
40
+ - Create `getMetricColor()` function in `report-console.ts` ✅
41
+ - Create `getMetricColorNullable()` for nullable values ✅
42
+ - Replace all repeated color logic with helper calls ✅
43
+ - Verify console output unchanged ✅
44
+
45
+ Function signatures:
46
+ ```typescript
47
+ type ChalkFn = typeof chalk.green
48
+
49
+ function getMetricColor(value: number): ChalkFn
50
+
51
+ function getMetricColorNullable(value: number | null): ChalkFn
52
+ ```
53
+
54
+ ### Phase 2: Unify Directory Printing ✅
55
+
56
+ - Create unified `printDirectoryTable()` function ✅
57
+ - Define `DirectoryTableOptions` type for configuration ✅
58
+ - Refactor `printDirectoryBreakdown()` to use unified function ✅
59
+ - Refactor `printRolledUpDirectories()` to use unified function ✅
60
+ - Verify console output unchanged ✅
61
+
62
+ Type definition:
63
+ ```typescript
64
+ type DirectoryTableOptions = {
65
+ title: string
66
+ includeQualityColumn: boolean
67
+ sortBy: 'health-asc' | 'alphabetical'
68
+ emptyMessage?: string
69
+ }
70
+ ```
71
+
72
+ ### Phase 3: Move Path Relativization to Shell ✅
73
+
74
+ - Remove `path` import from `scorer.ts` ✅
75
+ - Modify `rollupDirectoriesByDepth()` to accept pre-relativized paths ✅
76
+ - Create helper in `report-console.ts` to relativize paths before calling rollup ✅
77
+ - Update `report-json.ts` similarly ✅
78
+ - Update tests to use relative paths directly ✅
79
+
80
+ New function signature:
81
+ ```typescript
82
+ // scorer.ts - now pure, expects relative paths in input
83
+ export function rollupDirectoriesByDepth(
84
+ directories: DirectoryScore[],
85
+ depth: number,
86
+ ): DirectoryScore[]
87
+
88
+ // report-console.ts - shell helper
89
+ function relativizeDirectoryPaths(
90
+ directories: DirectoryScore[],
91
+ projectRoot: string,
92
+ ): DirectoryScore[]
93
+ ```
94
+
95
+ ### Phase 4: Use aggregateMetrics for Empty Case ✅
96
+
97
+ - Replace manual empty metrics construction in `rollupDirectoriesByDepth()` ✅
98
+ - Use `aggregateMetrics([])` spread with `dirPath` and `fileScores` overrides ✅
99
+ - Verify behavior unchanged with existing tests ✅
100
+
101
+ ### Phase 5: Documentation ✅
102
+
103
+ - Update TECHNICAL.md to reflect refactored architecture ✅
104
+ - Verify `scorer.ts` header comment still accurate ✅
105
+
106
+ ## Tests
107
+
108
+ ### Existing Tests
109
+
110
+ All changes are refactoring — existing tests in `scorer.test.ts` should continue to pass without modification. The tests for `rollupDirectoriesByDepth()` may need minor updates to pass relative paths instead of absolute paths.
111
+
112
+ ### New Tests
113
+
114
+ No new test files needed. Verify:
115
+
116
+ 1. **Color helper:** Visual inspection of CLI output
117
+ 2. **Unified printing:** Visual inspection of CLI output
118
+ 3. **Path relativization:** Update existing `rollupDirectoriesByDepth` tests to use relative paths
119
+
120
+ ### Test Modifications
121
+
122
+ ```typescript
123
+ // scorer.test.ts - update rollup tests to use relative paths
124
+ describe('rollupDirectoriesByDepth', () => {
125
+ it('should aggregate directories at depth 1', () => {
126
+ const dirs = [
127
+ // Before: createDirScoreForRollup('/project/src/services/auth', ...)
128
+ // After: paths are already relative
129
+ createDirScoreForRollup('src/services/auth', ...),
130
+ createDirScoreForRollup('src/services/users', ...),
131
+ createDirScoreForRollup('src/utils/format', ...),
132
+ ]
133
+
134
+ // Before: rollupDirectoriesByDepth(dirs, 1, '/project')
135
+ // After: no projectRoot parameter
136
+ const rolled = rollupDirectoriesByDepth(dirs, 1)
137
+ // ... assertions unchanged
138
+ })
139
+ })
140
+ ```
141
+
142
+ ## Transitive Effect Analysis
143
+
144
+ ### Phase 1 (Color Helper)
145
+ - `report-console.ts` → internal refactoring only
146
+ - No external API changes
147
+ - No transitive effects
148
+
149
+ ### Phase 2 (Unified Printing)
150
+ - `report-console.ts` → internal refactoring only
151
+ - No external API changes
152
+ - No transitive effects
153
+
154
+ ### Phase 3 (Path Relativization)
155
+ - `scorer.ts` → **API change**: `rollupDirectoriesByDepth()` signature changes
156
+ - `report-console.ts` → must call new helper before rollup
157
+ - `report-json.ts` → must call new helper before rollup
158
+ - `scorer.test.ts` → must update test data to use relative paths
159
+ - No external consumers affected (rollup is internal)
160
+
161
+ ### Phase 4 (Empty Case)
162
+ - `scorer.ts` → internal refactoring
163
+ - No API changes
164
+ - No transitive effects
165
+
166
+ ## Resources for Implementation
167
+
168
+ **Files to modify:**
169
+ - `fcis/src/scoring/scorer.ts` — Phase 3, Phase 4
170
+ - `fcis/src/reporting/report-console.ts` — Phase 1, Phase 2, Phase 3
171
+ - `fcis/src/reporting/report-json.ts` — Phase 3
172
+ - `fcis/tests/scorer.test.ts` — Phase 3
173
+
174
+ **Reference:**
175
+ - Current `rollupDirectoriesByDepth()` implementation
176
+ - Current `printDirectoryBreakdown()` and `printRolledUpDirectories()` implementations
177
+ - `aggregateMetrics()` empty case return value
178
+
179
+ ## Documentation Updates
180
+
181
+ ### TECHNICAL.md
182
+
183
+ Update the architecture diagram description to note:
184
+ - `scorer.ts` is a pure module with no I/O dependencies
185
+ - Path relativization happens in the shell layer before calling scoring functions
186
+
187
+ ### scorer.ts Header
188
+
189
+ Verify the header comment still accurately describes the module:
190
+ ```typescript
191
+ /**
192
+ * Scoring Engine - Pure Core
193
+ *
194
+ * Aggregates classification and quality scores into metrics at
195
+ * file, directory, and project levels. This is a PURE module -
196
+ * all functions take data in and return data out with no I/O.
197
+ */
198
+ ```
199
+
200
+ ## Estimated Line Impact
201
+
202
+ - Phase 1 (Color Helper): ~-20 lines (net reduction)
203
+ - Phase 2 (Unified Printing): ~-40 lines (net reduction)
204
+ - Phase 3 (Path Relativization): ~+10 lines (moves code, slight increase)
205
+ - Phase 4 (Empty Case): ~-10 lines (net reduction)
206
+ - Phase 5 (Documentation): ~5 lines
207
+
208
+ **Total: ~-55 lines** — net simplification
209
+
210
+ This is well under the 2000 line PR limit.
@@ -0,0 +1,149 @@
1
+ # Plan 006: Minor Refinements
2
+
3
+ ## Background
4
+
5
+ Following the implementation of Plan 005 (Code Refinements), a code review identified three minor issues:
6
+
7
+ 1. **Misleading `sortBy` option name:** The `DirectoryTableOptions.sortBy` type includes `'alphabetical'`, but the implementation actually preserves input order rather than sorting alphabetically.
8
+
9
+ 2. **Inconsistent empty case handling:** `rollupDirectoriesByDepth()` now uses `aggregateMetrics([])` for empty groups, but `scoreDirectory()` and `scoreProject()` still manually construct empty metrics inline.
10
+
11
+ 3. **Untested shell helper:** The `relativizeDirectoryPaths()` function in `report-console.ts` has no direct unit tests.
12
+
13
+ ## Problem Statement
14
+
15
+ 1. The `sortBy: 'alphabetical'` option is misleading — it implies active sorting but actually means "preserve input order"
16
+ 2. Empty metric construction is inconsistent across scoring functions, increasing maintenance burden
17
+ 3. `relativizeDirectoryPaths()` is only tested indirectly through CLI integration tests
18
+
19
+ ## Success Criteria
20
+
21
+ 1. `sortBy` option renamed to `'preserve-order'` with accurate documentation
22
+ 2. `scoreDirectory()` and `scoreProject()` use `aggregateMetrics([])` for empty cases
23
+ 3. `relativizeDirectoryPaths()` has focused unit tests
24
+ 4. All existing tests continue to pass
25
+
26
+ ## The Gap
27
+
28
+ | Aspect | Current State | Target State |
29
+ |--------|---------------|--------------|
30
+ | `sortBy` naming | `'alphabetical'` (misleading) | `'preserve-order'` (accurate) |
31
+ | Empty metrics in `scoreDirectory` | Manual construction | Uses `aggregateMetrics([])` |
32
+ | Empty metrics in `scoreProject` | Manual construction | Uses `aggregateMetrics([])` |
33
+ | `relativizeDirectoryPaths` tests | None | Focused unit tests |
34
+
35
+ ## Phases and Tasks
36
+
37
+ ### Phase 1: Rename sortBy Option ✅
38
+
39
+ - Rename `'alphabetical'` to `'preserve-order'` in `DirectoryTableOptions` type ✅
40
+ - Update call site in `printConsoleReport()` ✅
41
+ - Update comment to accurately describe behavior ✅
42
+
43
+ Type change:
44
+ ```typescript
45
+ type DirectoryTableOptions = {
46
+ title: string
47
+ includeQualityColumn: boolean
48
+ sortBy: 'health-asc' | 'preserve-order' // was 'alphabetical'
49
+ emptyMessage?: string
50
+ }
51
+ ```
52
+
53
+ ### Phase 2: Unify Empty Metrics Construction ✅
54
+
55
+ - Refactor `scoreDirectory()` empty case to use `aggregateMetrics([])` ✅
56
+ - Refactor `scoreProject()` empty case to use `aggregateMetrics([])` ✅
57
+ - Verify existing tests still pass ✅
58
+
59
+ ### Phase 3: Add relativizeDirectoryPaths Tests ✅
60
+
61
+ - Export `relativizeDirectoryPaths()` for testing (or test via module internals) ✅
62
+ - Add unit tests for path relativization ✅
63
+ - Test cross-platform path handling (backslash normalization) ✅
64
+
65
+ ## Tests
66
+
67
+ ### Phase 1 Tests
68
+
69
+ No new tests needed — existing tests verify behavior, only the name changes.
70
+
71
+ ### Phase 2 Tests
72
+
73
+ Existing tests in `scorer.test.ts` cover empty cases. Verify they still pass.
74
+
75
+ ### Phase 3 Tests
76
+
77
+ Add to a new or existing test file:
78
+
79
+ ```typescript
80
+ describe('relativizeDirectoryPaths', () => {
81
+ it('should convert absolute paths to relative paths', () => {
82
+ const dirs = [
83
+ { dirPath: '/project/src/services', ...otherProps },
84
+ { dirPath: '/project/src/utils', ...otherProps },
85
+ ]
86
+ const result = relativizeDirectoryPaths(dirs, '/project')
87
+ expect(result[0]?.dirPath).toBe('src/services')
88
+ expect(result[1]?.dirPath).toBe('src/utils')
89
+ })
90
+
91
+ it('should normalize backslashes to forward slashes', () => {
92
+ const dirs = [{ dirPath: 'C:\\project\\src', ...otherProps }]
93
+ const result = relativizeDirectoryPaths(dirs, 'C:\\project')
94
+ expect(result[0]?.dirPath).toBe('src')
95
+ })
96
+
97
+ it('should preserve other directory properties', () => {
98
+ const dirs = [{ dirPath: '/project/src', pureCount: 5, impureCount: 3, ...otherProps }]
99
+ const result = relativizeDirectoryPaths(dirs, '/project')
100
+ expect(result[0]?.pureCount).toBe(5)
101
+ expect(result[0]?.impureCount).toBe(3)
102
+ })
103
+ })
104
+ ```
105
+
106
+ ## Transitive Effect Analysis
107
+
108
+ ### Phase 1 (sortBy Rename)
109
+ - `report-console.ts` → internal type and call site change
110
+ - No external API changes
111
+ - No transitive effects
112
+
113
+ ### Phase 2 (Empty Metrics)
114
+ - `scorer.ts` → internal refactoring
115
+ - Behavior unchanged (same default values)
116
+ - Existing tests verify correct output
117
+ - No transitive effects
118
+
119
+ ### Phase 3 (relativizeDirectoryPaths Tests)
120
+ - `report-console.ts` → may need to export function for testing
121
+ - Alternative: test via integration or keep as private with indirect coverage
122
+ - No production code changes needed if testing via integration
123
+
124
+ ## Resources for Implementation
125
+
126
+ **Files to modify:**
127
+ - `fcis/src/reporting/report-console.ts` — Phase 1, Phase 3
128
+ - `fcis/src/scoring/scorer.ts` — Phase 2
129
+
130
+ **Files to add/update tests:**
131
+ - `fcis/tests/report-console.test.ts` (new) — Phase 3
132
+ - Or `fcis/tests/scorer.test.ts` — verify Phase 2
133
+
134
+ **Reference:**
135
+ - Current `DirectoryTableOptions` type definition
136
+ - Current `scoreDirectory()` and `scoreProject()` empty case handling
137
+ - Current `relativizeDirectoryPaths()` implementation
138
+
139
+ ## Documentation Updates
140
+
141
+ No documentation updates needed — these are internal implementation details not exposed in README or TECHNICAL.md.
142
+
143
+ ## Estimated Line Impact
144
+
145
+ - Phase 1 (sortBy Rename): ~5 lines changed
146
+ - Phase 2 (Empty Metrics): ~20 lines changed (net reduction)
147
+ - Phase 3 (Tests): ~30 lines added
148
+
149
+ **Total: ~55 lines** — well under the 2000 line PR limit.