fcis 0.1.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.
Files changed (151) hide show
  1. package/.plans/001-fcis-analyzer.md +832 -0
  2. package/.plans/002-fcis-analyzer-improvements.md +205 -0
  3. package/README.md +272 -0
  4. package/TECHNICAL.md +386 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +1836 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +709 -0
  9. package/dist/index.js +1845 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +47 -0
  12. package/pnpm-workspace.yaml +0 -0
  13. package/src/analyzer.ts +266 -0
  14. package/src/classification/classifier.ts +156 -0
  15. package/src/classification/derive-status.ts +171 -0
  16. package/src/classification/quality-scorer.ts +481 -0
  17. package/src/cli.ts +286 -0
  18. package/src/detection/detect-markers.ts +480 -0
  19. package/src/detection/markers.ts +332 -0
  20. package/src/extraction/extract-functions.ts +570 -0
  21. package/src/extraction/extractor.ts +188 -0
  22. package/src/index.ts +111 -0
  23. package/src/reporting/report-console.ts +416 -0
  24. package/src/reporting/report-json.ts +232 -0
  25. package/src/scoring/scorer.ts +504 -0
  26. package/src/types.ts +248 -0
  27. package/tests/classifier.test.ts +480 -0
  28. package/tests/derive-status.test.ts +464 -0
  29. package/tests/detect-markers.test.ts +639 -0
  30. package/tests/extractor.test.ts +155 -0
  31. package/tests/integration.test.ts +706 -0
  32. package/tests/quality-scorer.test.ts +650 -0
  33. package/tests/scorer.test.ts +768 -0
  34. package/tsconfig.json +34 -0
  35. package/tsup.config.ts +17 -0
  36. package/vendor/ts-morph/.editorconfig +10 -0
  37. package/vendor/ts-morph/.gitattributes +11 -0
  38. package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
  39. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  40. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
  41. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  42. package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
  43. package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
  44. package/vendor/ts-morph/.vscode/settings.json +10 -0
  45. package/vendor/ts-morph/CONTRIBUTING.md +23 -0
  46. package/vendor/ts-morph/DEVELOPMENT.md +32 -0
  47. package/vendor/ts-morph/LICENSE +21 -0
  48. package/vendor/ts-morph/deno.json +8 -0
  49. package/vendor/ts-morph/deno.lock +1233 -0
  50. package/vendor/ts-morph/docs/CNAME +1 -0
  51. package/vendor/ts-morph/docs/Gemfile +2 -0
  52. package/vendor/ts-morph/docs/_config.yml +5 -0
  53. package/vendor/ts-morph/docs/_layouts/default.html +159 -0
  54. package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
  55. package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
  56. package/vendor/ts-morph/docs/details/ambient.md +38 -0
  57. package/vendor/ts-morph/docs/details/async.md +31 -0
  58. package/vendor/ts-morph/docs/details/classes.md +314 -0
  59. package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
  60. package/vendor/ts-morph/docs/details/comments.md +122 -0
  61. package/vendor/ts-morph/docs/details/decorators.md +119 -0
  62. package/vendor/ts-morph/docs/details/documentation.md +73 -0
  63. package/vendor/ts-morph/docs/details/enums.md +117 -0
  64. package/vendor/ts-morph/docs/details/exports.md +308 -0
  65. package/vendor/ts-morph/docs/details/expressions.md +46 -0
  66. package/vendor/ts-morph/docs/details/functions.md +150 -0
  67. package/vendor/ts-morph/docs/details/generators.md +27 -0
  68. package/vendor/ts-morph/docs/details/identifiers.md +79 -0
  69. package/vendor/ts-morph/docs/details/imports.md +191 -0
  70. package/vendor/ts-morph/docs/details/index.md +52 -0
  71. package/vendor/ts-morph/docs/details/initializers.md +40 -0
  72. package/vendor/ts-morph/docs/details/interfaces.md +218 -0
  73. package/vendor/ts-morph/docs/details/literals.md +20 -0
  74. package/vendor/ts-morph/docs/details/modifiers.md +38 -0
  75. package/vendor/ts-morph/docs/details/modules.md +113 -0
  76. package/vendor/ts-morph/docs/details/namespaces.md +7 -0
  77. package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
  78. package/vendor/ts-morph/docs/details/parameters.md +64 -0
  79. package/vendor/ts-morph/docs/details/signatures.md +41 -0
  80. package/vendor/ts-morph/docs/details/source-files.md +292 -0
  81. package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
  82. package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
  83. package/vendor/ts-morph/docs/details/types.md +254 -0
  84. package/vendor/ts-morph/docs/details/variables.md +110 -0
  85. package/vendor/ts-morph/docs/emitting.md +151 -0
  86. package/vendor/ts-morph/docs/index.md +25 -0
  87. package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
  88. package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
  89. package/vendor/ts-morph/docs/manipulation/index.md +136 -0
  90. package/vendor/ts-morph/docs/manipulation/order.md +14 -0
  91. package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
  92. package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
  93. package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
  94. package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
  95. package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
  96. package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
  97. package/vendor/ts-morph/docs/metrics/performance.json +4 -0
  98. package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
  99. package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
  100. package/vendor/ts-morph/docs/navigation/directories.md +287 -0
  101. package/vendor/ts-morph/docs/navigation/example.md +50 -0
  102. package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
  103. package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
  104. package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
  105. package/vendor/ts-morph/docs/navigation/index.md +94 -0
  106. package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
  107. package/vendor/ts-morph/docs/navigation/program.md +25 -0
  108. package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
  109. package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
  110. package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
  111. package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
  112. package/vendor/ts-morph/docs/setup/file-system.md +106 -0
  113. package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
  114. package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
  115. package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
  116. package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
  117. package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
  118. package/vendor/ts-morph/docs/setup/index.md +94 -0
  119. package/vendor/ts-morph/docs/utilities.md +55 -0
  120. package/vendor/ts-morph/dprint.json +23 -0
  121. package/vendor/ts-morph/package.json +30 -0
  122. package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
  123. package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
  124. package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
  125. package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
  126. package/vendor/ts-morph/packages/common/LICENSE +21 -0
  127. package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
  128. package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
  129. package/vendor/ts-morph/packages/common/package.json +65 -0
  130. package/vendor/ts-morph/packages/common/readme.md +5 -0
  131. package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
  132. package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
  133. package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
  134. package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
  135. package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
  136. package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
  137. package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
  138. package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
  139. package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
  140. package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
  141. package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
  142. package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
  143. package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
  144. package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
  145. package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
  146. package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
  147. package/vendor/ts-morph/readme.md +14 -0
  148. package/vendor/ts-morph/rfcs/README.md +13 -0
  149. package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
  150. package/vendor/ts-morph/tsconfig.common.json +17 -0
  151. package/vitest.config.ts +16 -0
@@ -0,0 +1,205 @@
1
+ # Plan 002: FCIS Analyzer Improvements
2
+
3
+ ## Background
4
+
5
+ Plan 001 implemented the FCIS (Functional Core, Imperative Shell) analyzer for TypeScript codebases. After running the analyzer against the SchoolAI monorepo (`apps/web` and `packages/`), several issues were identified that affect the accuracy and usability of the tool.
6
+
7
+ Key findings from baseline analysis:
8
+ - **apps/web**: 48% health, 38% purity, 52% impurity quality (2002 functions analyzed)
9
+ - **packages/utils**: 86% health, 85% purity (65 functions analyzed)
10
+ - **server/services**: 20% health, 14% purity, 47% impurity quality (high-priority refactoring area)
11
+
12
+ ## Problem Statement
13
+
14
+ Three issues undermine the tool's usefulness:
15
+
16
+ 1. **File-level import tainting (CRITICAL)**: Pure functions like `shouldModerate`, `shouldSendRedAlertEmail`, and `getRedAlertType` in `moderation.ts` are incorrectly classified as impure because the file imports `@sai/logger`, even though these functions never call the logger. This false positive problem undermines trust in the tool's signal.
17
+
18
+ 2. **Anonymous function names**: Many refactoring candidates appear as `<anonymous>` because they are arrow functions assigned to variables. This makes it harder for developers to locate and prioritize functions.
19
+
20
+ 3. **tsconfig with comments fails parsing**: TypeScript projects commonly use comments in `tsconfig.json`, but the current JSON parser fails on these files, preventing analysis of the ai-platform codebase.
21
+
22
+ ## Success Criteria
23
+
24
+ 1. Pure functions in files that import loggers/analytics but don't use them are classified as **pure** (not impure)
25
+ 2. Arrow functions assigned to variables display the variable name (e.g., `handleSubmit` instead of `<anonymous>`)
26
+ 3. tsconfig.json files with comments parse successfully
27
+ 4. Re-run baseline analysis against `moderation.ts` — `shouldModerate`, `shouldSendRedAlertEmail`, `getRedAlertType` should all classify as **pure**
28
+ 5. Re-run baseline analysis against `ai-platform/apps/server` — should complete without parser errors
29
+
30
+ ## The Gap
31
+
32
+ | Issue | Current Behavior | Expected Behavior |
33
+ |-------|------------------|-------------------|
34
+ | Logger import tainting | Any file importing `@sai/logger` marks ALL functions as having `logging` marker | Only functions that CALL `log()`, `logger.*`, etc. should get the marker |
35
+ | Anonymous function names | Arrow functions extracted with `name: null` | Should use the variable name from the declaration |
36
+ | tsconfig with comments | `JSON.parse()` throws on comments | Should strip comments or use JSON5/jsonc parser |
37
+
38
+ ## Phases and Tasks
39
+
40
+ ### Phase 1: Fix File-Level Import Tainting ✅
41
+
42
+ This is the highest-impact fix. Currently, the `detectLoggingMarkers` function in `detect-markers.ts` adds a marker for all functions if the file imports from a logger module.
43
+
44
+ - Remove file-level import marker logic from `detectLoggingMarkers` ✅
45
+ - Keep only call-site-based detection for logging (function must actually call `logger.*`, `log()`, etc.) ✅
46
+ - Apply same fix to HTTP module detection in `detectNetworkMarkers` — only flag if function uses the import ✅
47
+ - Apply same fix to FS module detection in `detectFileSystemMarkers` — only flag if function uses the import ✅
48
+ - Write unit tests verifying pure functions in files with logger imports remain pure ✅
49
+
50
+ ### Phase 2: Improve Arrow Function Naming ✅
51
+
52
+ Arrow functions assigned to variables should display the variable name.
53
+
54
+ - In `extract-functions.ts`, when extracting arrow functions from variable declarations, already capture `varName` as `parentContext` ✅
55
+ - Modify to also set `name` to `varName` when the arrow function has no intrinsic name ✅
56
+ - Update tests to verify arrow function variable names are captured ✅
57
+
58
+ ### Phase 2b: Extract Inline Arrow Functions ✅ (Additional Fix)
59
+
60
+ Arrow functions passed as arguments (e.g., tRPC handlers, callbacks) were not being extracted.
61
+
62
+ - Add extraction of ALL arrow functions in file, not just those assigned to variables ✅
63
+ - Infer parent context from call expression (e.g., `.mutation()` -> name is "mutation") ✅
64
+ - Use deduplication to avoid extracting the same function twice ✅
65
+ - Write tests for inline arrow function extraction ✅
66
+
67
+ This fix increased function detection from 829 to 1366 in `apps/web/src/server/**/*.ts`.
68
+
69
+ ### Phase 3: Handle tsconfig.json with Comments ✅
70
+
71
+ - Add `stripJsonComments` function (regex-based implementation) ✅
72
+ - In `extractor.ts`, strip comments from tsconfig content before parsing ✅
73
+ - Write test verifying tsconfig with comments parses successfully ✅
74
+
75
+ ### Phase 4: Validation and Documentation ✅
76
+
77
+ - Re-run analysis on `moderation.ts` and verify pure functions are classified correctly ✅
78
+ - Run analysis on `ai-platform/apps/server` and verify it completes ✅
79
+ - Update TECHNICAL.md with notes on marker detection behavior ✅
80
+ - Update README.md examples if needed ✅ (no changes needed)
81
+
82
+ ## Tests
83
+
84
+ ### Unit Tests
85
+
86
+ **`detect-markers.test.ts`** — Add tests for fixed import behavior:
87
+
88
+ ```typescript
89
+ describe('logging detection', () => {
90
+ it('should NOT mark function as impure just because file imports logger', () => {
91
+ const fn = createTestFunction({
92
+ callSites: [{ expression: 'Object.entries', line: 2, isAwaited: false }],
93
+ })
94
+ const context = createTestContext([
95
+ { moduleSpecifier: '@sai/logger', namedImports: ['log'] },
96
+ ])
97
+
98
+ const markers = detectMarkers(fn, context)
99
+ expect(markers.filter(m => m.type === 'logging')).toHaveLength(0)
100
+ })
101
+
102
+ it('should mark function as impure when it calls log()', () => {
103
+ const fn = createTestFunction({
104
+ callSites: [{ expression: 'log', line: 5, isAwaited: false }],
105
+ })
106
+ const context = createTestContext([
107
+ { moduleSpecifier: '@sai/logger', namedImports: ['log'] },
108
+ ])
109
+
110
+ const markers = detectMarkers(fn, context)
111
+ expect(markers).toContainEqual(expect.objectContaining({ type: 'logging' }))
112
+ })
113
+ })
114
+ ```
115
+
116
+ **`extract-functions.test.ts`** — Add test for arrow function naming:
117
+
118
+ ```typescript
119
+ it('should use variable name for arrow functions', () => {
120
+ const project = new Project({ useInMemoryFileSystem: true })
121
+ const sf = project.createSourceFile('/test.ts', `
122
+ export const handleSubmit = async (data: FormData) => {
123
+ const result = await processData(data)
124
+ return result
125
+ }
126
+ `)
127
+
128
+ const functions = extractFunctions(sf)
129
+ expect(functions[0]?.name).toBe('handleSubmit')
130
+ })
131
+ ```
132
+
133
+ **`extractor.test.ts`** — Add test for tsconfig with comments:
134
+
135
+ ```typescript
136
+ it('should parse tsconfig with comments', () => {
137
+ // Create temp tsconfig with comments
138
+ const content = `{
139
+ // This is a comment
140
+ "compilerOptions": {
141
+ "target": "es2022" // inline comment
142
+ }
143
+ }`
144
+
145
+ // Verify it doesn't throw
146
+ expect(() => loadProject({ tsconfigPath: tempPath })).not.toThrow()
147
+ })
148
+ ```
149
+
150
+ ### Integration Test
151
+
152
+ Re-validate against real codebase files:
153
+
154
+ - `apps/web/src/server/services/moderation.ts` — `shouldModerate`, `shouldSendRedAlertEmail`, `getRedAlertType` should be **pure**
155
+ - `ai-platform/apps/server/tsconfig.json` — should parse without errors
156
+
157
+ ## Transitive Effect Analysis
158
+
159
+ 1. **Marker Detection → Classification → Scoring**: Fixing marker detection directly affects classification (fewer false positives → higher purity scores), which affects file/directory/project scores. All downstream calculations will show improved metrics.
160
+
161
+ 2. **Pure function files**: Files that currently show 0% purity due to logger imports (like `moderation.ts`) will show mixed purity, revealing the actual pure helpers within.
162
+
163
+ 3. **CLI output**: Refactoring candidate lists may change as previously-flagged pure functions drop off the list.
164
+
165
+ 4. **Existing tests**: Current unit tests mock extracted function data directly, so they won't break. Integration tests that rely on specific marker counts may need adjustment.
166
+
167
+ ## Resources for Implementation
168
+
169
+ **Files to modify:**
170
+
171
+ - `fcis/src/detection/detect-markers.ts` — Lines 266-295 (`detectLoggingMarkers` and import-based logic)
172
+ - `fcis/src/detection/detect-markers.ts` — Lines 202-226 (`detectNetworkMarkers` HTTP module import logic)
173
+ - `fcis/src/extraction/extract-functions.ts` — Lines 75-95 (arrow function extraction)
174
+ - `fcis/src/extraction/extractor.ts` — Lines 38-48 (tsconfig parsing)
175
+
176
+ **Reference files:**
177
+
178
+ - `apps/web/src/server/services/moderation.ts` — Example of file with pure functions and logger import
179
+ - `ai-platform/apps/server/tsconfig.json` — Example of tsconfig with comments
180
+
181
+ **Dependencies to add:**
182
+
183
+ - `strip-json-comments` (or use regex-based stripping)
184
+
185
+ ## Documentation Updates
186
+
187
+ ### TECHNICAL.md
188
+
189
+ Add section clarifying marker detection behavior:
190
+
191
+ ```markdown
192
+ ### Marker Detection Scope
193
+
194
+ Markers are detected at the **function level**, not the file level:
195
+
196
+ - A function is marked with `logging` only if it **calls** a logging function
197
+ - A function is marked with `network-http` only if it **calls** an HTTP client function
198
+ - File-level imports do NOT automatically taint all functions in the file
199
+
200
+ This ensures pure helper functions in mixed files are correctly classified.
201
+ ```
202
+
203
+ ### README.md
204
+
205
+ No changes needed — the public API and usage remain the same.
package/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # FCIS Analyzer
2
+
3
+ **Functional Core, Imperative Shell** analyzer for TypeScript codebases.
4
+
5
+ FCIS is a static analysis tool that measures how well your TypeScript code separates pure business logic from I/O and side effects. It answers the question:
6
+
7
+ > "Can this function's logic be tested without mocking anything?"
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm install
13
+ pnpm build
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # Analyze a project
20
+ npx fcis tsconfig.json
21
+
22
+ # Analyze with health threshold (CI gate)
23
+ npx fcis tsconfig.json --min-health 70
24
+
25
+ # Output JSON report
26
+ npx fcis tsconfig.json --format json --output report.json
27
+
28
+ # Analyze specific files (for pre-commit hooks)
29
+ npx fcis tsconfig.json --files "src/services/**/*.ts"
30
+ ```
31
+
32
+ ## What It Measures
33
+
34
+ ### Purity (0-100%)
35
+
36
+ Percentage of functions that are **pure** — no I/O markers detected. Pure functions:
37
+ - Take arguments and return values
38
+ - Have no side effects
39
+ - Can be tested without mocking
40
+
41
+ ### Impurity Quality (0-100)
42
+
43
+ For impure functions, measures how **well-structured** the I/O code is:
44
+ - **High (≥70):** I/O is organized, calls pure functions, follows GATHER→DECIDE→EXECUTE pattern
45
+ - **Medium (40-69):** Some structure, room for improvement
46
+ - **Low (<40):** Tangled code, business logic mixed with I/O
47
+
48
+ ### Health (0-100%)
49
+
50
+ Percentage of functions with status **OK** (either pure, or impure with quality ≥70).
51
+
52
+ ## Function Classification
53
+
54
+ Every function is classified as:
55
+
56
+ | Classification | Criteria | Testable without mocks? |
57
+ |---------------|----------|------------------------|
58
+ | **Pure** | No I/O markers | ✅ Yes |
59
+ | **Impure** | Has I/O markers (await, db, fetch, fs, etc.) | ❌ No |
60
+
61
+ ### Status Derivation
62
+
63
+ | Classification | Quality Score | Status | Action |
64
+ |---------------|---------------|--------|--------|
65
+ | Pure | n/a | ✓ OK | None needed |
66
+ | Impure | ≥ 70 | ✓ OK | Well-structured |
67
+ | Impure | 40-69 | ◐ Review | Consider improving |
68
+ | Impure | < 40 | ✗ Refactor | Prioritize cleanup |
69
+
70
+ ## Impurity Markers
71
+
72
+ The analyzer detects these I/O patterns:
73
+
74
+ | Marker | Examples |
75
+ |--------|----------|
76
+ | `await-expression` | `await fetch()`, `await db.query()` |
77
+ | `database-call` | `db.user.findFirst()`, `prisma.post.create()` |
78
+ | `network-fetch` | `fetch(url)` |
79
+ | `network-http` | `axios.get()`, imports from `axios` |
80
+ | `fs-call` | `fs.readFile()`, `fs.writeFile()` |
81
+ | `fs-import` | Import from `fs`, `node:fs` |
82
+ | `env-access` | `process.env.NODE_ENV` |
83
+ | `console-log` | `console.log()`, `console.error()` |
84
+ | `logging` | `logger.info()`, imports from logger |
85
+ | `telemetry` | `trackEvent()`, `analytics.track()` |
86
+ | `queue-enqueue` | `queue.enqueue()`, `queue.add()` |
87
+ | `event-emit` | `emitter.emit()`, `dispatcher.dispatch()` |
88
+
89
+ **Note:** `async` alone does NOT make a function impure — only actual I/O markers count.
90
+
91
+ ## CLI Reference
92
+
93
+ ```
94
+ fcis <tsconfig> [options]
95
+
96
+ Arguments:
97
+ tsconfig Path to tsconfig.json
98
+
99
+ Options:
100
+ --json Output JSON to stdout
101
+ --output, -o <file> Write JSON report to file
102
+ --min-health <N> Exit code 1 if health < N (0-100)
103
+ --min-purity <N> Exit code 1 if purity < N (0-100)
104
+ --min-quality <N> Exit code 1 if impurity quality < N (0-100)
105
+ --files, -f <glob> Analyze only matching files
106
+ --format <fmt> Output: console (default), json, summary
107
+ --quiet, -q Suppress output, use exit code only
108
+ --verbose, -v Show per-file details
109
+ --help Show help
110
+ --version Show version
111
+ ```
112
+
113
+ ### Exit Codes
114
+
115
+ | Code | Meaning |
116
+ |------|---------|
117
+ | 0 | Success, all thresholds passed |
118
+ | 1 | Analysis completed but below threshold |
119
+ | 2 | Configuration error (invalid options, tsconfig not found) |
120
+ | 3 | Analysis error (no files could be analyzed) |
121
+
122
+ ## Example Output
123
+
124
+ ```
125
+ FCIS Analysis
126
+ ═══════════════════════════════════════════════════════════
127
+
128
+ Project Health: 77% ████████████████████░░░░░
129
+ Purity: 45% (234 pure / 520 total)
130
+ Impurity Quality: 68% average
131
+
132
+ Status Breakdown:
133
+ ✓ OK: 312 functions (60%) — no action needed
134
+ ◐ Review: 89 functions (17%) — could be improved
135
+ ✗ Refactor: 119 functions (23%) — tangled, needs work
136
+
137
+ Top Refactoring Candidates:
138
+ (Sorted by impact: size × complexity)
139
+
140
+ 1. 25/100 processOrder (150 lines)
141
+ /src/services/orders.ts:45
142
+ Markers: database-call, network-fetch, console-log
143
+
144
+ 2. 32/100 handleUserUpdate (98 lines)
145
+ /src/services/users.ts:120
146
+ Markers: database-call, await-expression
147
+ ```
148
+
149
+ ## CI Integration
150
+
151
+ ### GitHub Actions
152
+
153
+ ```yaml
154
+ - name: FCIS Analysis
155
+ run: npx fcis tsconfig.json --min-health 70 --format summary
156
+ ```
157
+
158
+ ### Pre-commit Hook (lint-staged)
159
+
160
+ ```json
161
+ {
162
+ "lint-staged": {
163
+ "*.ts": ["fcis tsconfig.json --files"]
164
+ }
165
+ }
166
+ ```
167
+
168
+ ## The FCIS Pattern
169
+
170
+ The **Functional Core, Imperative Shell** pattern separates code into:
171
+
172
+ ### Pure Core (Functional)
173
+ - Business logic, calculations, validations
174
+ - Takes data in, returns data out
175
+ - No side effects
176
+ - Trivially testable
177
+
178
+ ### Impure Shell (Imperative)
179
+ - I/O operations (database, network, file system)
180
+ - Orchestrates: GATHER data → call pure functions → EXECUTE effects
181
+ - Thin wrapper around pure core
182
+ - Requires integration tests
183
+
184
+ ### Example Refactoring
185
+
186
+ **Before (tangled):**
187
+ ```typescript
188
+ async function acceptInvite(inviteId: string) {
189
+ const invite = await db.invitation.findFirst({ where: { id: inviteId } })
190
+ if (!invite) throw new Error('Not found')
191
+
192
+ const org = await db.organization.findFirst({ where: { id: invite.orgId } })
193
+
194
+ // Business logic mixed with I/O
195
+ if (invite.expiresAt < new Date()) {
196
+ await db.invitation.update({ where: { id: inviteId }, data: { status: 'expired' } })
197
+ throw new Error('Expired')
198
+ }
199
+
200
+ if (org.memberCount >= org.maxMembers) {
201
+ throw new Error('Org full')
202
+ }
203
+
204
+ await db.member.create({ data: { userId: invite.userId, orgId: org.id } })
205
+ await db.invitation.update({ where: { id: inviteId }, data: { status: 'accepted' } })
206
+ }
207
+ ```
208
+
209
+ **After (FCIS):**
210
+ ```typescript
211
+ // Pure core - testable without mocks
212
+ export function planAcceptInvite(invite: Invitation, org: Organization): AcceptInvitePlan {
213
+ if (invite.expiresAt < new Date()) {
214
+ return { action: 'reject', reason: 'expired' }
215
+ }
216
+ if (org.memberCount >= org.maxMembers) {
217
+ return { action: 'reject', reason: 'org-full' }
218
+ }
219
+ return {
220
+ action: 'accept',
221
+ memberData: { userId: invite.userId, orgId: org.id }
222
+ }
223
+ }
224
+
225
+ // Impure shell - thin orchestration
226
+ async function acceptInvite(inviteId: string) {
227
+ // GATHER
228
+ const invite = await db.invitation.findFirst({ where: { id: inviteId } })
229
+ const org = await db.organization.findFirst({ where: { id: invite.orgId } })
230
+
231
+ // DECIDE (pure)
232
+ const plan = planAcceptInvite(invite, org)
233
+
234
+ // EXECUTE
235
+ if (plan.action === 'reject') {
236
+ await db.invitation.update({ where: { id: inviteId }, data: { status: plan.reason } })
237
+ throw new Error(plan.reason)
238
+ }
239
+
240
+ await db.member.create({ data: plan.memberData })
241
+ await db.invitation.update({ where: { id: inviteId }, data: { status: 'accepted' } })
242
+ }
243
+ ```
244
+
245
+ ## Quality Score Signals
246
+
247
+ ### Positive (increase score)
248
+ - Calls functions from `.pure.ts` files (+30)
249
+ - Calls `plan*/derive*/compute*/transform*` functions (+20)
250
+ - I/O concentrated at start (GATHER pattern) (+15)
251
+ - I/O concentrated at end (EXECUTE pattern) (+15)
252
+ - Low cyclomatic complexity (+10)
253
+ - Shell naming convention (`handle*/fetch*/save*`) (+5)
254
+ - Calls predicate functions (`is*/has*/should*`) (+5)
255
+
256
+ ### Negative (decrease score)
257
+ - I/O interleaved throughout (-20)
258
+ - High cyclomatic complexity (-15)
259
+ - Multiple I/O types (db + http + fs) (-10)
260
+ - No pure function calls (-10)
261
+ - Very long function (>100 lines) (-10)
262
+
263
+ ## Limitations
264
+
265
+ - **v1 analyzes `.ts` files only** — `.tsx` files are deferred to v2
266
+ - Pattern matching is heuristic-based — may miss custom I/O patterns
267
+ - Does not analyze transitive purity (function calling another function)
268
+ - Quality scoring weights are tuned for SchoolAI patterns
269
+
270
+ ## License
271
+
272
+ MIT