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.
- package/.plans/001-fcis-analyzer.md +832 -0
- package/.plans/002-fcis-analyzer-improvements.md +205 -0
- package/README.md +272 -0
- package/TECHNICAL.md +386 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1836 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +709 -0
- package/dist/index.js +1845 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/pnpm-workspace.yaml +0 -0
- package/src/analyzer.ts +266 -0
- package/src/classification/classifier.ts +156 -0
- package/src/classification/derive-status.ts +171 -0
- package/src/classification/quality-scorer.ts +481 -0
- package/src/cli.ts +286 -0
- package/src/detection/detect-markers.ts +480 -0
- package/src/detection/markers.ts +332 -0
- package/src/extraction/extract-functions.ts +570 -0
- package/src/extraction/extractor.ts +188 -0
- package/src/index.ts +111 -0
- package/src/reporting/report-console.ts +416 -0
- package/src/reporting/report-json.ts +232 -0
- package/src/scoring/scorer.ts +504 -0
- package/src/types.ts +248 -0
- package/tests/classifier.test.ts +480 -0
- package/tests/derive-status.test.ts +464 -0
- package/tests/detect-markers.test.ts +639 -0
- package/tests/extractor.test.ts +155 -0
- package/tests/integration.test.ts +706 -0
- package/tests/quality-scorer.test.ts +650 -0
- package/tests/scorer.test.ts +768 -0
- package/tsconfig.json +34 -0
- package/tsup.config.ts +17 -0
- package/vendor/ts-morph/.editorconfig +10 -0
- package/vendor/ts-morph/.gitattributes +11 -0
- package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
- package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
- package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
- package/vendor/ts-morph/.vscode/settings.json +10 -0
- package/vendor/ts-morph/CONTRIBUTING.md +23 -0
- package/vendor/ts-morph/DEVELOPMENT.md +32 -0
- package/vendor/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/deno.json +8 -0
- package/vendor/ts-morph/deno.lock +1233 -0
- package/vendor/ts-morph/docs/CNAME +1 -0
- package/vendor/ts-morph/docs/Gemfile +2 -0
- package/vendor/ts-morph/docs/_config.yml +5 -0
- package/vendor/ts-morph/docs/_layouts/default.html +159 -0
- package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
- package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
- package/vendor/ts-morph/docs/details/ambient.md +38 -0
- package/vendor/ts-morph/docs/details/async.md +31 -0
- package/vendor/ts-morph/docs/details/classes.md +314 -0
- package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
- package/vendor/ts-morph/docs/details/comments.md +122 -0
- package/vendor/ts-morph/docs/details/decorators.md +119 -0
- package/vendor/ts-morph/docs/details/documentation.md +73 -0
- package/vendor/ts-morph/docs/details/enums.md +117 -0
- package/vendor/ts-morph/docs/details/exports.md +308 -0
- package/vendor/ts-morph/docs/details/expressions.md +46 -0
- package/vendor/ts-morph/docs/details/functions.md +150 -0
- package/vendor/ts-morph/docs/details/generators.md +27 -0
- package/vendor/ts-morph/docs/details/identifiers.md +79 -0
- package/vendor/ts-morph/docs/details/imports.md +191 -0
- package/vendor/ts-morph/docs/details/index.md +52 -0
- package/vendor/ts-morph/docs/details/initializers.md +40 -0
- package/vendor/ts-morph/docs/details/interfaces.md +218 -0
- package/vendor/ts-morph/docs/details/literals.md +20 -0
- package/vendor/ts-morph/docs/details/modifiers.md +38 -0
- package/vendor/ts-morph/docs/details/modules.md +113 -0
- package/vendor/ts-morph/docs/details/namespaces.md +7 -0
- package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
- package/vendor/ts-morph/docs/details/parameters.md +64 -0
- package/vendor/ts-morph/docs/details/signatures.md +41 -0
- package/vendor/ts-morph/docs/details/source-files.md +292 -0
- package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
- package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
- package/vendor/ts-morph/docs/details/types.md +254 -0
- package/vendor/ts-morph/docs/details/variables.md +110 -0
- package/vendor/ts-morph/docs/emitting.md +151 -0
- package/vendor/ts-morph/docs/index.md +25 -0
- package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
- package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
- package/vendor/ts-morph/docs/manipulation/index.md +136 -0
- package/vendor/ts-morph/docs/manipulation/order.md +14 -0
- package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
- package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
- package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
- package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
- package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
- package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
- package/vendor/ts-morph/docs/metrics/performance.json +4 -0
- package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
- package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
- package/vendor/ts-morph/docs/navigation/directories.md +287 -0
- package/vendor/ts-morph/docs/navigation/example.md +50 -0
- package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
- package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
- package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
- package/vendor/ts-morph/docs/navigation/index.md +94 -0
- package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
- package/vendor/ts-morph/docs/navigation/program.md +25 -0
- package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
- package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
- package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
- package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
- package/vendor/ts-morph/docs/setup/file-system.md +106 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
- package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
- package/vendor/ts-morph/docs/setup/index.md +94 -0
- package/vendor/ts-morph/docs/utilities.md +55 -0
- package/vendor/ts-morph/dprint.json +23 -0
- package/vendor/ts-morph/package.json +30 -0
- package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
- package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
- package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
- package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
- package/vendor/ts-morph/packages/common/LICENSE +21 -0
- package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
- package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
- package/vendor/ts-morph/packages/common/package.json +65 -0
- package/vendor/ts-morph/packages/common/readme.md +5 -0
- package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
- package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
- package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
- package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
- package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
- package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
- package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
- package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
- package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
- package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
- package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
- package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
- package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
- package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
- package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
- package/vendor/ts-morph/readme.md +14 -0
- package/vendor/ts-morph/rfcs/README.md +13 -0
- package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
- package/vendor/ts-morph/tsconfig.common.json +17 -0
- 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
|