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
package/TECHNICAL.md
CHANGED
|
@@ -64,11 +64,14 @@ type ExtractedFunction = {
|
|
|
64
64
|
bodyLineCount: number
|
|
65
65
|
statementCount: number
|
|
66
66
|
hasConditionals: boolean
|
|
67
|
-
parentContext: string | null
|
|
67
|
+
parentContext: string | null // class name, variable name, or HOF method name
|
|
68
68
|
callSites: CallSite[]
|
|
69
69
|
hasAwait: boolean
|
|
70
70
|
propertyAccessChains: string[]
|
|
71
71
|
kind: 'function' | 'method' | 'arrow' | 'function-expression' | 'getter' | 'setter'
|
|
72
|
+
// Compositional scoring fields
|
|
73
|
+
enclosingFunctionStartLine: number | null // parent function's startLine, null if module-scope
|
|
74
|
+
isInlineCallback: boolean // true if callback to known HOF (map, filter, etc.)
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// Marker types - strict union for exhaustive matching
|
|
@@ -77,7 +80,6 @@ type MarkerType =
|
|
|
77
80
|
| 'database-call'
|
|
78
81
|
| 'network-fetch'
|
|
79
82
|
| 'network-http'
|
|
80
|
-
| 'fs-import'
|
|
81
83
|
| 'fs-call'
|
|
82
84
|
| 'env-access'
|
|
83
85
|
| 'console-log'
|
|
@@ -374,6 +376,127 @@ project.createSourceFile('/test.ts', `
|
|
|
374
376
|
| `chalk` | Colored console output |
|
|
375
377
|
| `ts-pattern` | Pattern matching (optional) |
|
|
376
378
|
|
|
379
|
+
## Directory Rollup
|
|
380
|
+
|
|
381
|
+
When `--dir-depth N` is specified, directory metrics are aggregated hierarchically.
|
|
382
|
+
|
|
383
|
+
### FC/IS Architecture
|
|
384
|
+
|
|
385
|
+
The rollup feature follows the Functional Core / Imperative Shell pattern:
|
|
386
|
+
|
|
387
|
+
- **Shell layer** (`report-console.ts`, `report-json.ts`): Converts absolute paths to relative paths using `path.relative()`
|
|
388
|
+
- **Pure core** (`scorer.ts`): The `rollupDirectoriesByDepth()` function operates on already-relative paths, keeping the scoring module free of I/O dependencies
|
|
389
|
+
|
|
390
|
+
### Algorithm
|
|
391
|
+
|
|
392
|
+
1. Shell layer converts absolute directory paths to relative paths
|
|
393
|
+
2. Pure function truncates paths to N+1 segments (e.g., depth 1 → `src/services`)
|
|
394
|
+
3. Directories with the same truncated path are grouped together
|
|
395
|
+
4. Metrics are aggregated using weighted averages (weighted by function count):
|
|
396
|
+
- Health = (total ok functions) / (total functions) × 100
|
|
397
|
+
- Purity = (total pure functions) / (total functions) × 100
|
|
398
|
+
- Impurity Quality = weighted average by impure function count
|
|
399
|
+
5. All directories at the specified depth are shown, sorted alphabetically
|
|
400
|
+
|
|
401
|
+
This provides a high-level view of codebase health by area without the noise of deeply nested leaf directories.
|
|
402
|
+
|
|
403
|
+
### Edge Cases
|
|
404
|
+
|
|
405
|
+
- **Depth exceeds nesting:** Directories shallower than the requested depth are shown at their actual depth
|
|
406
|
+
- **Single directory:** Works correctly with a single input directory
|
|
407
|
+
- **Empty directories:** Directories with no functions are excluded from aggregation
|
|
408
|
+
|
|
409
|
+
### JSON Output
|
|
410
|
+
|
|
411
|
+
When `--dir-depth` is used with `--json` or `--output`, the JSON includes:
|
|
412
|
+
- `directoryScores` — full leaf-level data (always present)
|
|
413
|
+
- `rolledUpDirectories` — aggregated data at specified depth (only when `--dir-depth` used)
|
|
414
|
+
|
|
415
|
+
This is non-breaking for existing consumers.
|
|
416
|
+
|
|
417
|
+
## Compositional Function Scoring
|
|
418
|
+
|
|
419
|
+
FCIS uses compositional scoring to handle inline callbacks (arrow functions passed to `map`, `filter`, `forEach`, etc.). This prevents inflated function counts and diluted health scores.
|
|
420
|
+
|
|
421
|
+
### Inline Callback Detection
|
|
422
|
+
|
|
423
|
+
Functions are marked as inline callbacks (`isInlineCallback: true`) when passed to known higher-order functions:
|
|
424
|
+
|
|
425
|
+
**Included (absorbed into parent):**
|
|
426
|
+
- Array methods: `map`, `filter`, `reduce`, `forEach`, `find`, `findIndex`, `some`, `every`, `flatMap`, `sort`, `toSorted`
|
|
427
|
+
- Promise methods: `then`, `catch`, `finally`
|
|
428
|
+
- Timing/events: `setTimeout`, `setInterval`, `addEventListener`, `on`, `once`
|
|
429
|
+
|
|
430
|
+
**Excluded (remain independent):**
|
|
431
|
+
- tRPC handlers: `query`, `mutation`, `subscription`
|
|
432
|
+
- Express middleware: `use`, `get`, `post`, `put`, `delete`, `patch`
|
|
433
|
+
- Custom application HOFs
|
|
434
|
+
|
|
435
|
+
Framework-specific handlers represent distinct behavioral units and should be scored independently.
|
|
436
|
+
|
|
437
|
+
### Enclosing Function Discovery
|
|
438
|
+
|
|
439
|
+
Each inline callback is linked to its enclosing function via AST upward walk:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
type ExtractedFunction = {
|
|
443
|
+
// ... other fields ...
|
|
444
|
+
enclosingFunctionStartLine: number | null // null if module-scope
|
|
445
|
+
isInlineCallback: boolean
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Callbacks at module scope (no enclosing function) are treated as top-level functions.
|
|
450
|
+
|
|
451
|
+
### Impurity Bubbling
|
|
452
|
+
|
|
453
|
+
If an inline callback is impure, the enclosing function is **effectively impure**. The function's own classification remains unchanged (for drill-down), but metrics use the effective classification:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// In scorer.ts
|
|
457
|
+
function isEffectivelyImpure(entry: FunctionWithChildren): boolean {
|
|
458
|
+
if (entry.fn.classification === 'impure') return true
|
|
459
|
+
return entry.children.some(child => child.classification === 'impure')
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Markers from children are aggregated into the parent's refactoring candidate entry.
|
|
464
|
+
|
|
465
|
+
### Quality Composition
|
|
466
|
+
|
|
467
|
+
Parent quality is blended with impure children's quality, weighted by line count:
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
composedQuality = (parentQuality × parentOwnLines + Σ(childQuality × childLines)) / totalLines
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Where:
|
|
474
|
+
- `parentOwnLines = parent.bodyLineCount - Σ(child.bodyLineCount)` (clamped to min 1)
|
|
475
|
+
- `totalLines = parentOwnLines + Σ(child.bodyLineCount)`
|
|
476
|
+
|
|
477
|
+
**Example:**
|
|
478
|
+
- Parent: 50 total lines, 40 own lines, quality 60
|
|
479
|
+
- Child: 10 lines, quality 40
|
|
480
|
+
- Composed: `(60×40 + 40×10) / 50 = 56`
|
|
481
|
+
|
|
482
|
+
Pure children do not factor into the impurity quality calculation.
|
|
483
|
+
|
|
484
|
+
### Function Counts
|
|
485
|
+
|
|
486
|
+
Only top-level functions are counted in health/purity metrics:
|
|
487
|
+
- `isInlineCallback === false`, OR
|
|
488
|
+
- `isInlineCallback === true` but `enclosingFunctionStartLine === null` (module-scope)
|
|
489
|
+
|
|
490
|
+
This prevents a 301-line function with 6 callbacks from being counted as 7 functions.
|
|
491
|
+
|
|
492
|
+
### Line Counts
|
|
493
|
+
|
|
494
|
+
Line counts no longer double-count nested callbacks. Parent `bodyLineCount` already includes nested callback lines, so only top-level functions are summed.
|
|
495
|
+
|
|
496
|
+
### JSON Output
|
|
497
|
+
|
|
498
|
+
All functions (including inline callbacks) remain in the JSON output with `isInlineCallback: true` for detailed analysis. The metrics reflect composed scores, but drill-down data is preserved.
|
|
499
|
+
|
|
377
500
|
## File Exclusions
|
|
378
501
|
|
|
379
502
|
Automatically excluded from analysis:
|