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/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: