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,832 @@
1
+ # Plan 001: FCIS Analyzer
2
+
3
+ ## Revision History
4
+
5
+ **Rev 4** — Scope focused on `.ts` files; React/TSX deferred to v2:
6
+
7
+ | # | Issue | Fix |
8
+ |---|-------|-----|
9
+ | 12 | React analysis adds complexity | **Deferred to v2:** Frontend component analysis moved to Addendum; v1 focuses on `.ts` files only |
10
+ | 11 | "Shell" classification conflates structure and quality | **Major restructure:** Binary classification (`pure`/`impure`) + quality score (0–100) for impure functions + derived status (`ok`/`review`/`refactor`) for UX |
11
+ | 10 | `async` false-positives | `async` alone does NOT make a function impure; only actual I/O markers count |
12
+ | 9 | `callExpressions` loses position context | Replaced with `CallSite[]` containing `expression`, `line`, and `isAwaited` |
13
+ | 8 | Engineering improvements | Added: strict marker types, input validation, error handling strategy, edge case handling, library recommendations |
14
+ | 7 | CLI not linter-ready | Expanded Phase 7 with exit codes, file targeting, output formats, performance targets, CI examples |
15
+ | 6 | Real data validation comes last | Added early validation spike in Phase 2 to inform marker design |
16
+
17
+ ---
18
+
19
+ ## Background
20
+
21
+ **Functional Core, Imperative Shell (FCIS)** is an architectural pattern that separates pure business logic (the core) from I/O and side effects (the shell). When applied well, business logic becomes trivially testable — no mocks, no setup, just input/output assertions. The shell becomes thin orchestration: gather data, call pure functions, execute effects.
22
+
23
+ The SchoolAI codebase has begun adopting this pattern in isolated areas:
24
+
25
+ - `apps/web/src/server/services/organizations.pure.ts` — a `planAcceptInvite()` pure decision function
26
+ - `apps/web/src/hooks/useSettingsRoutes.ts` — `deriveSettingsPageRoutes()` / `deriveOrgMenuRoutes()` pure derivations with hook shells
27
+ - `ai-platform/apps/server/src/v1/blueprints/nodes/llm-v2/prepare-provider-request.ts` — pure provider request transformation
28
+
29
+ However, the vast majority of service code interleaves business logic with database calls, HTTP requests, and other I/O. There is no way to measure adherence, track progress, or identify refactoring targets.
30
+
31
+ No existing tool in the TypeScript ecosystem measures FCIS adherence. Tools like `code-health-meter` (Maintainability Index, Cyclomatic Complexity) and `cognitive-complexity-ts` measure code complexity, not structural separation of logic from I/O. Two abandoned ESLint plugins (`eslint-plugin-purecheck`, `eslint-plugin-pure-function`) attempted to enforce purity but are unmaintained and predate modern TypeScript.
32
+
33
+ ## Problem Statement
34
+
35
+ We need a static analysis tool that answers, for any TypeScript function:
36
+
37
+ > **"Can this function's logic be tested without mocking anything?"**
38
+
39
+ And aggregates that answer into a score at function, file, directory, and project levels — trackable over time across git history.
40
+
41
+ ## Success Criteria
42
+
43
+ 1. Analyze `.ts` files using the TypeScript compiler (via ts-morph) — not regex. **Note:** `.tsx` files are deferred to v2.
44
+ 2. Classify every function as **pure** or **impure** (binary, objective)
45
+ 3. Compute a **quality score** (0–100) for impure functions measuring structural quality
46
+ 4. Derive a **status** (`ok` / `review` / `refactor`) for developer UX
47
+ 5. Produce metrics at function, file, directory, and project levels:
48
+ - **Purity:** percentage of pure functions
49
+ - **Impurity Quality:** average quality score of impure functions
50
+ - **Project Health:** percentage of functions with status `ok`
51
+ 6. Generate JSON output suitable for storage, diffing, and trend analysis
52
+ 7. Generate human-readable console output with refactoring candidates
53
+ 8. Run against the SchoolAI monorepo (`.ts` files) and produce a meaningful baseline report
54
+ 9. The tool itself follows FCIS: pure core for detection/classification/scoring, shell only for I/O
55
+
56
+ ## The Gap
57
+
58
+ - No measurement exists today — the team cannot answer "what percentage of our business logic is testable without mocking?"
59
+ - Existing FCIS implementations are isolated experiments, not tracked
60
+ - Service files like `organizations.ts` (2300+ lines, 1 pure function extracted of ~50 total) represent the dominant pattern
61
+ - There is no convention enforcement or CI feedback loop
62
+
63
+ ## Phases and Tasks
64
+
65
+ ### Phase 1: Project Scaffolding 🔴
66
+
67
+ - Set up `fcis/` as a standalone TypeScript package with `tsup` for build 🔴
68
+ - Add dependencies (from npm, not vendored): 🔴
69
+ - `ts-morph` — AST parsing and analysis
70
+ - `zod` — CLI argument validation and config schema
71
+ - `cleye` — typed CLI argument parsing with auto-generated help
72
+ - `chalk` — colored console output
73
+ - Add dev dependencies: 🔴
74
+ - `vitest` — test runner with snapshot support
75
+ - `ts-pattern` — exhaustive pattern matching for classification logic (optional but recommended)
76
+ - Configure `tsconfig.json` targeting ES2022/Node with strict mode 🔴
77
+ - Establish directory structure mirroring FCIS architecture (see Architecture section) 🔴
78
+ - Create `README.md` and `TECHNICAL.md` 🔴
79
+
80
+ ### Phase 2: AST Extraction Layer 🔴
81
+
82
+ - Implement `extractor.ts` (shell): load a `Project` from a tsconfig path, enumerate `.ts` source files (exclude `.tsx` for v1) 🔴
83
+ - Implement `extract-functions.ts` (shell): for each source file, extract all function-like nodes into a normalized `ExtractedFunction` structure 🔴
84
+ - Handle all function forms: `FunctionDeclaration`, `MethodDeclaration`, `ArrowFunction`, `FunctionExpression`, `GetAccessorDeclaration`, `SetAccessorDeclaration` 🔴
85
+ - Extract per-function: name, file path, start/end lines, isAsync, isExported, body text length, parent context (class name, variable name) 🔴
86
+ - Extract per-function: all call sites with position and await context (see `CallSite` type), all `PropertyAccessExpression` chains 🔴
87
+ - Extract per-file: all `ImportDeclaration` module specifiers and named imports 🔴
88
+ - Write integration test: feed a multi-function `.ts` string to ts-morph in-memory, verify extraction output 🔴
89
+ - **Early validation spike:** Run extraction against 5–10 representative `.ts` files from the SchoolAI codebase and document observed patterns 🔴
90
+ - Target files: `organizations.pure.ts`, `organizations.ts`, `moderation.ts`, `students.ts`, `chat.ts`, `prepare-provider-request.ts`
91
+ - Document: call expression formats (e.g., `db.user.findFirst` vs `({ db }) => db.user.findFirst`), import styles
92
+ - Use findings to inform Phase 3 marker design — adjust detection rules BEFORE implementing them
93
+
94
+ ### Phase 3: Impurity Marker Detection 🔴
95
+
96
+ This is the pure core of the analyzer — all functions in this phase take data in and return data out with no I/O.
97
+
98
+ - Define `ImpurityMarker` type with **strict string literal union** for `type` field (see Critical Type Declarations) 🔴
99
+ - Define marker catalog in `markers.ts` as a **data structure**, not hardcoded logic — enables extensibility 🔴
100
+ - Implement `detect-markers.ts`: given an `ExtractedFunction` and file-level imports, return `ImpurityMarker[]` 🔴
101
+ - Marker categories and detection rules: 🔴
102
+
103
+ **Async/Await**
104
+ - `async` keyword on function → **NOT a marker** (async alone is just a calling convention; does not make a function impure)
105
+ - `await` expression in body → marker `await-expression` (indicates I/O suspension point)
106
+ - **Classification rule:** A function is impure if it has ANY I/O marker. `async` without `await` and without other I/O markers is **pure**.
107
+
108
+ **Database**
109
+ - Call matching `db.<entity>.<operation>` or `prisma.<entity>.<operation>` where operation ∈ {`findFirst`, `findMany`, `findUnique`, `create`, `update`, `delete`, `upsert`, `aggregate`, `count`, `groupBy`} → marker `database-call`
110
+
111
+ **Network**
112
+ - Call to `fetch(` → marker `network-fetch`
113
+ - Import from `axios`, `node-fetch`, or call to `axios.*` → marker `network-http`
114
+
115
+ **File System**
116
+ - Import from `fs`, `node:fs`, `fs/promises`, `node:fs/promises` → marker `fs-import`
117
+ - Call matching `fs.<operation>` → marker `fs-call`
118
+
119
+ **Environment**
120
+ - Property access `process.env.*` → marker `env-access`
121
+
122
+ **Logging/Telemetry**
123
+ - Call matching `console.(log|error|warn|info)` → marker `console-log`
124
+ - Call matching `logger.*` or `log(` (from `@sai/logger`) → marker `logging`
125
+ - Calls to analytics/tracking functions → marker `telemetry`
126
+
127
+ **Queue/Event**
128
+ - Call matching `*.enqueue(` → marker `queue-enqueue`
129
+ - Call matching `*.emit(` → marker `event-emit`
130
+
131
+ - Write unit tests for each marker category: provide `ExtractedFunction` data, assert correct markers returned 🔴
132
+
133
+ #### Error Handling in Extraction 🔴
134
+
135
+ - Files that fail to parse (syntax errors, encoding issues) are logged and **skipped** — do not fail the entire analysis
136
+ - Collect parse errors with file path and error message
137
+ - Continue analysis with remaining files
138
+ - Exit code 3 only if **zero files** could be analyzed successfully
139
+ - The JSON output includes an `errors: { filePath: string, error: string }[]` array
140
+
141
+ ### Phase 4: Function Classification & Quality Scoring 🔴
142
+
143
+ Pure core — classifies functions and computes quality scores.
144
+
145
+ #### Binary Classification 🔴
146
+
147
+ - Implement `classifier.ts`: given an `ExtractedFunction` with its `ImpurityMarker[]`, return a `FunctionClassification` 🔴
148
+ - Classification is **binary and objective**: 🔴
149
+
150
+ **Pure**: Zero I/O markers. Function takes arguments and returns values with no side effects. Note: `async` without `await` (and without other I/O markers) is still **pure**.
151
+
152
+ **Impure**: Has one or more I/O markers (`await-expression`, `database-call`, `network-fetch`, `fs-call`, etc.). This function requires mocking to test.
153
+
154
+ #### Quality Score for Impure Functions 🔴
155
+
156
+ - Implement `quality-scorer.ts`: given an impure `ClassifiedFunction`, compute a quality score 0–100 🔴
157
+ - Quality score measures **how well-structured** the impurity is, not whether it exists
158
+ - Quality signals (additive, max 100): 🔴
159
+
160
+ | Signal | Points | Rationale |
161
+ |--------|--------|-----------|
162
+ | Calls a function from `.pure.ts` file | +30 | Explicit FCIS pattern |
163
+ | Calls `plan*/derive*/compute*/calculate*/transform*` function | +20 | Strong pure naming convention |
164
+ | I/O calls concentrated at start of function (GATHER pattern) | +15 | Good structure |
165
+ | I/O calls concentrated at end of function (EXECUTE pattern) | +15 | Good structure |
166
+ | Low cyclomatic complexity (≤ 5) | +10 | Simple orchestration |
167
+ | Function name matches shell conventions (`handle*/fetch*/save*/send*`) | +5 | Intent signal |
168
+ | Calls predicate functions (`is*/has*/should*`) | +5 | Uses pure helpers |
169
+
170
+ - Quality penalties (subtractive): 🔴
171
+
172
+ | Signal | Penalty | Rationale |
173
+ |--------|---------|-----------|
174
+ | I/O calls interleaved throughout function body | -20 | Tangled structure |
175
+ | High cyclomatic complexity (> 10) | -15 | Complex logic mixed with I/O |
176
+ | Multiple I/O types (db + http + fs in same function) | -10 | Too many responsibilities |
177
+ | No pure function calls at all | -10 | All logic is inline |
178
+ | Very long function (> 100 lines) | -10 | God function |
179
+
180
+ - Quality score is clamped to 0–100 🔴
181
+
182
+ #### Derived Status for UX 🔴
183
+
184
+ - Implement `derive-status.ts`: given classification and quality score, return a `Status` 🔴
185
+ - Status provides actionable guidance for developers: 🔴
186
+
187
+ | Classification | Quality Score | Status | Developer Action |
188
+ |---------------|---------------|--------|------------------|
189
+ | pure | n/a | ✓ `ok` | None — testable without mocks |
190
+ | impure | ≥ 70 | ✓ `ok` | None — well-structured, mocks are manageable |
191
+ | impure | 40–69 | ◐ `review` | Consider improving if touching this code |
192
+ | impure | < 40 | ✗ `refactor` | Tangled; prioritize cleanup |
193
+
194
+ - Status thresholds are configurable (can be adjusted based on team standards) 🔴
195
+
196
+ #### File and Function Exclusions 🔴
197
+
198
+ - Implement file-level exclusions: 🔴
199
+ - Files matching `*.test.ts`, `*.spec.ts`, `*.stories.tsx` → excluded from analysis
200
+ - Files matching `*.d.ts` → excluded
201
+ - Files in `/generated/`, `/node_modules/` → excluded
202
+ - Type-only files (only `type`, `interface`, `enum` exports, no function bodies) → excluded, flagged as `typeOnly`
203
+
204
+ - Implement trivial function exclusion: functions with < 3 statements and no conditionals → excluded from scoring 🔴
205
+
206
+ - Write unit tests: 🔴
207
+ - Classification: provide function data with various marker combinations, assert correct pure/impure
208
+ - Quality scoring: provide impure functions with various structural patterns, assert expected scores
209
+ - Status derivation: verify thresholds produce correct status
210
+
211
+ ### Phase 5: Scoring Engine 🔴
212
+
213
+ Pure core — aggregates classifications and quality scores into project metrics.
214
+
215
+ - Implement `scorer.ts` with the following pure functions: 🔴
216
+
217
+ `scoreFile(functions: ClassifiedFunction[]): FileScore` — computes:
218
+ - **Purity:** `pure / (pure + impure)` — percentage of functions testable without mocking
219
+ - **Impurity Quality:** average quality score of impure functions
220
+ - **Health:** `(pure + impure with quality ≥ 70) / total` — percentage of functions with status `ok`
221
+
222
+ `scoreDirectory(fileScores: FileScore[]): DirectoryScore` — weighted average by function count
223
+
224
+ `scoreProject(directoryScores: DirectoryScore[]): ProjectScore` — weighted average by function count
225
+
226
+ - Each score includes: 🔴
227
+ - `purity`: 0–100 (percentage of pure functions)
228
+ - `impurityQuality`: 0–100 (average quality of impure functions; null if no impure functions)
229
+ - `health`: 0–100 (percentage of functions with status `ok`)
230
+ - `pureCount`, `impureCount`, `excludedCount`
231
+ - `statusBreakdown`: `{ ok: number, review: number, refactor: number }`
232
+ - `refactoringCandidates`: list of impure functions with status `refactor`, **sorted by `bodyLineCount × (100 - qualityScore)` descending**
233
+
234
+ - **Diagnostic insights:** 🔴
235
+ - High purity + low impurity quality = "Most code is pure, but the impure code is tangled"
236
+ - Low purity + high impurity quality = "Lots of I/O, but it's well-structured"
237
+ - Low purity + low impurity quality = "Significant technical debt in I/O code"
238
+
239
+ - **Edge case handling:** 🔴
240
+ - If all functions are excluded, health is `100` with flag `allExcluded: true`
241
+ - Type-only files get `health: 100` with flag `typeOnly: true`
242
+ - If no impure functions, `impurityQuality` is `null` (not 0 or 100)
243
+ - Division by zero is never possible due to these rules
244
+
245
+ - Write unit tests: provide classification arrays with quality scores, assert correct metric computation, including edge cases (empty files, all-pure, all-impure, all-excluded) 🔴
246
+
247
+ ### Phase 6: Reporting 🔴
248
+
249
+ Shell layer — formats and outputs results.
250
+
251
+ - Implement `report-json.ts`: serialize full analysis to JSON (for storage and diffing) 🔴
252
+ - Implement `report-console.ts`: produce human-readable CLI output including: 🔴
253
+
254
+ **Top-level summary:**
255
+ ```
256
+ FCIS Analysis
257
+ ═══════════════════════════════════════════════════════════
258
+
259
+ Project Health: 77% ████████████████████░░░░░
260
+ Purity: 45% (234 pure / 520 total)
261
+ Impurity Quality: 68% average
262
+
263
+ Status Breakdown:
264
+ ✓ OK: 312 functions (60%) — no action needed
265
+ ◐ Review: 89 functions (17%) — could be improved
266
+ ✗ Refactor: 119 functions (23%) — tangled, needs work
267
+ ```
268
+
269
+ - Per-directory breakdown sorted by health (ascending — worst first)
270
+ - Top 10 refactoring candidates, **sorted by `bodyLineCount × (100 - qualityScore)` descending** (surfaces large, low-quality functions first)
271
+ - Summary counts: pure / impure / excluded
272
+ - Quality distribution: histogram or percentile breakdown of impure function quality scores
273
+ - JSON schema includes a `timestamp` and `commitHash` field for historical tracking 🔴
274
+
275
+ ### Phase 7: CLI Entry Point 🔴
276
+
277
+ Build a CLI suitable for running alongside linters (ESLint, TypeScript) in CI pipelines and pre-commit hooks.
278
+
279
+ - Implement `cli.ts` using `commander` or simple `process.argv` parsing 🔴
280
+
281
+ #### Commands and Options 🔴
282
+
283
+ ```
284
+ fcis analyze <path-to-tsconfig> [options]
285
+
286
+ Options:
287
+ --json Output JSON to stdout (for piping/parsing)
288
+ --output <file> Write JSON report to file
289
+ --min-health <N> Exit with code 1 if project health < N (CI gate, default metric)
290
+ --min-purity <N> Exit with code 1 if purity < N
291
+ --min-quality <N> Exit with code 1 if impurity quality < N
292
+ --files <glob> Analyze only files matching glob (for incremental/pre-commit)
293
+ --format <fmt> Output format: console (default), json, summary
294
+ --quiet Suppress all output except errors; rely on exit code
295
+ --verbose Show per-file scores and all classified functions
296
+ --no-frontend Skip frontend component analysis (faster for backend-only)
297
+ ```
298
+
299
+ #### Input Validation 🔴
300
+
301
+ Validate CLI inputs using zod schemas:
302
+ - `--min-health` must be a number 0–100; invalid → exit 2 with message
303
+ - `--min-purity` must be a number 0–100; invalid → exit 2 with message
304
+ - `--min-quality` must be a number 0–100; invalid → exit 2 with message
305
+ - `<path-to-tsconfig>` must exist and be a valid JSON file; invalid → exit 2 with message
306
+ - `--files` glob must be a valid glob pattern; invalid → exit 2 with message
307
+ - `--output` directory must exist (or be creatable); invalid → exit 2 with message
308
+
309
+ Provide clear, actionable error messages for validation failures.
310
+
311
+ #### Exit Codes 🔴
312
+
313
+ | Code | Meaning |
314
+ |------|---------|
315
+ | 0 | Success, all thresholds passed |
316
+ | 1 | Analysis completed but below threshold (`--min-health`, `--min-purity`, or `--min-quality`) |
317
+ | 2 | Configuration error (tsconfig not found, invalid options, validation failure) |
318
+ | 3 | Analysis error (zero files could be analyzed — all files had parse errors) |
319
+
320
+ #### File Targeting 🔴
321
+
322
+ Support analyzing a subset of files for faster feedback:
323
+
324
+ ```bash
325
+ # Analyze only changed files (useful for pre-commit)
326
+ fcis analyze tsconfig.json --files "src/services/organizations.ts"
327
+
328
+ # Analyze a directory
329
+ fcis analyze tsconfig.json --files "src/components/**/*.tsx"
330
+ ```
331
+
332
+ When `--files` is used:
333
+ - Only matching files are **extracted and classified**
334
+ - Imports from non-matching files are still resolved for `.pure.ts` detection (import graph is still walked)
335
+ - Score represents **only the subset**, not the whole project
336
+ - JSON output includes `"subset": true` and `"filesGlob": "<pattern>"`
337
+ - Useful for pre-commit hooks and editor integration
338
+ - **Note:** Subset scores are not directly comparable to full-project scores
339
+
340
+ #### Output Formats 🔴
341
+
342
+ - **console** (default): Human-readable with colors, visual bars, refactoring candidates
343
+ - **json**: Full analysis result (same as `--json`)
344
+ - **summary**: Single-line output suitable for commit messages or PR comments
345
+ - Example: `FCIS Health: 77% | Purity: 45% | Impurity Quality: 68%`
346
+
347
+ #### Performance Target 🔴
348
+
349
+ - Full monorepo analysis: < 60 seconds
350
+ - Single file analysis: < 2 seconds
351
+ - Suitable for pre-commit hooks with `--files` targeting
352
+
353
+ #### Caching (Post-v1) 🔴
354
+
355
+ For v1, run full extraction on every invocation. Post-v1 optimization:
356
+ - Cache `ExtractedFunction[]` per file, keyed by file content hash
357
+ - Store cache in `.fcis-cache/` directory (gitignored)
358
+ - On subsequent runs, only re-extract files whose content hash changed
359
+ - Add `--cache` (default: on) and `--no-cache` flags
360
+ - Cache invalidation: ts-morph version change, fcis version change
361
+
362
+ #### CI Integration Examples 🔴
363
+
364
+ **GitHub Actions:**
365
+ ```yaml
366
+ - name: FCIS Analysis
367
+ run: npx fcis analyze tsconfig.json --min-health 70 --format summary
368
+ ```
369
+
370
+ **Pre-commit hook (with lint-staged):**
371
+ ```json
372
+ {
373
+ "lint-staged": {
374
+ "*.{ts,tsx}": ["fcis analyze tsconfig.json --files", "eslint --fix"]
375
+ }
376
+ }
377
+ ```
378
+
379
+ **Turborepo task:**
380
+ ```json
381
+ {
382
+ "pipeline": {
383
+ "fcis": {
384
+ "inputs": ["src/**/*.ts", "src/**/*.tsx"],
385
+ "outputs": [".fcis-report.json"]
386
+ }
387
+ }
388
+ }
389
+ ```
390
+
391
+ - Wire up: load project → extract → detect → classify → score → report 🔴
392
+
393
+ ### Phase 8: Baseline Analysis 🔴
394
+
395
+ **Note:** The Phase 2 early validation spike should have already informed marker design. This phase is full-scale validation, not discovery. Major heuristic changes at this point indicate the spike was insufficient.
396
+
397
+ - Run analyzer against the SchoolAI monorepo (`apps/web/src/`, `ai-platform/`, `packages/`) 🔴
398
+ - Validate results against known patterns: 🔴
399
+ - `organizations.pure.ts` functions should classify as **pure**
400
+ - `planAcceptInvite` should classify as **pure**
401
+ - `acceptInvite` in `organizations.ts` should classify as **impure** with **high quality score** (≥70, calls pure function)
402
+ - `deriveSettingsPageRoutes` should classify as **pure**
403
+ - `moderateStudentMessages` should classify as **impure** with **low quality score** (<40, tangled)
404
+ - `shouldModerate`, `shouldSendRedAlertEmail`, `getRedAlertType` should classify as **pure**
405
+ - `prepareProviderRequest` should classify as **pure**
406
+ - Fine-tune quality scoring weights based on observed patterns (expect minor adjustments) 🔴
407
+ - Verify metrics produce meaningful differentiation:
408
+ - Check that low impurity quality correlates with files developers find painful
409
+ - Verify refactoring candidates list surfaces high-value targets
410
+ - Validate status thresholds feel right (adjust 70/40 if needed)
411
+ - Save baseline JSON report 🔴
412
+
413
+ ## Architecture
414
+
415
+ ```
416
+ fcis/
417
+ ├── src/
418
+ │ ├── cli.ts # CLI entry point (SHELL)
419
+ │ ├── analyzer.ts # Orchestration (SHELL)
420
+ │ ├── extraction/
421
+ │ │ ├── extractor.ts # ts-morph project loading (SHELL)
422
+ │ │ └── extract-functions.ts # AST → ExtractedFunction (SHELL)
423
+ │ ├── detection/
424
+ │ │ ├── markers.ts # Marker type definitions & catalog (PURE)
425
+ │ │ └── detect-markers.ts # ExtractedFunction → ImpurityMarker[] (PURE)
426
+ │ ├── classification/
427
+ │ │ ├── classifier.ts # Markers → Classification (PURE)
428
+ │ │ ├── quality-scorer.ts # Impure function → Quality score (PURE)
429
+ │ │ └── derive-status.ts # Classification + Quality → Status (PURE)
430
+ │ ├── scoring/
431
+ │ │ └── scorer.ts # Classifications → Scores (PURE)
432
+ │ ├── reporting/
433
+ │ │ ├── report-json.ts # JSON output (SHELL)
434
+ │ │ └── report-console.ts # Console output (SHELL)
435
+ │ └── types.ts # Shared type definitions (INFRASTRUCTURE)
436
+ ├── tests/
437
+ │ ├── detect-markers.test.ts
438
+ │ ├── classifier.test.ts
439
+ │ ├── quality-scorer.test.ts
440
+ │ ├── derive-status.test.ts
441
+ │ ├── scorer.test.ts
442
+ │ └── integration.test.ts
443
+ ├── plans/
444
+ │ └── 001-fcis-analyzer.md
445
+ ├── vendor/
446
+ │ └── ts-morph/ # Reference only — use npm package
447
+ ├── package.json
448
+ ├── tsconfig.json
449
+ ├── README.md
450
+ └── TECHNICAL.md
451
+ ```
452
+
453
+ ## Critical Type Declarations
454
+
455
+ ```typescript
456
+ // types.ts
457
+
458
+ // CallSite captures position and context for each call expression,
459
+ // enabling future sequence analysis (GATHER→DECIDE→EXECUTE detection)
460
+ type CallSite = {
461
+ expression: string // e.g. "db.user.findFirst", "planAcceptInvite"
462
+ line: number // position within the function (relative to function start)
463
+ isAwaited: boolean // was this call preceded by `await`?
464
+ }
465
+
466
+ type ExtractedFunction = {
467
+ name: string | null
468
+ filePath: string
469
+ startLine: number
470
+ endLine: number
471
+ isAsync: boolean
472
+ isExported: boolean
473
+ bodyLineCount: number
474
+ statementCount: number
475
+ hasConditionals: boolean
476
+ parentContext: string | null // e.g. class name, variable name
477
+ callSites: CallSite[] // enriched call expressions with position and await context
478
+ hasAwait: boolean // convenience flag: true if any callSite.isAwaited
479
+ propertyAccessChains: string[] // e.g. ["process.env.NODE_ENV", "db.user"]
480
+ kind: 'function' | 'method' | 'arrow' | 'function-expression' | 'getter' | 'setter'
481
+ }
482
+
483
+ type FileImports = {
484
+ filePath: string
485
+ imports: {
486
+ moduleSpecifier: string // e.g. "@sai/database", "./organizations.pure"
487
+ namedImports: string[] // e.g. ["planAcceptInvite", "AcceptInviteDecision"]
488
+ }[]
489
+ }
490
+
491
+ // Strict marker type union — prevents typos, enables exhaustive matching
492
+ // Note: 'async-function' removed — async alone is not an impurity marker
493
+ // Note: React hook markers deferred to v2 (see Addendum)
494
+ type MarkerType =
495
+ | 'await-expression'
496
+ | 'database-call'
497
+ | 'network-fetch'
498
+ | 'network-http'
499
+ | 'fs-import'
500
+ | 'fs-call'
501
+ | 'env-access'
502
+ | 'console-log'
503
+ | 'logging'
504
+ | 'telemetry'
505
+ | 'queue-enqueue'
506
+ | 'event-emit'
507
+
508
+ type ImpurityMarker = {
509
+ type: MarkerType
510
+ detail: string // e.g. "db.user.findFirst"
511
+ }
512
+
513
+ // Binary classification — objective, no heuristics
514
+ type FunctionClassification = 'pure' | 'impure'
515
+
516
+ // Derived status for UX — actionable guidance
517
+ type Status = 'ok' | 'review' | 'refactor'
518
+
519
+ type ClassifiedFunction = ExtractedFunction & {
520
+ markers: ImpurityMarker[]
521
+ classification: FunctionClassification
522
+ qualityScore: number | null // 0-100 for impure functions; null for pure
523
+ status: Status // derived from classification + qualityScore
524
+ }
525
+
526
+ type StatusBreakdown = {
527
+ ok: number
528
+ review: number
529
+ refactor: number
530
+ }
531
+
532
+ type FileScore = {
533
+ filePath: string
534
+ purity: number // 0-100: pure / (pure + impure)
535
+ impurityQuality: number | null // 0-100: avg quality of impure functions; null if none
536
+ health: number // 0-100: functions with status 'ok' / total
537
+ pureCount: number
538
+ impureCount: number
539
+ excludedCount: number
540
+ statusBreakdown: StatusBreakdown
541
+ pureLineCount: number // total lines in pure functions
542
+ impureLineCount: number // total lines in impure functions
543
+ refactoringCandidates: {
544
+ name: string | null
545
+ startLine: number
546
+ bodyLineCount: number
547
+ qualityScore: number
548
+ markers: MarkerType[]
549
+ }[]
550
+ // Edge case flags
551
+ allExcluded?: boolean // true if all functions were excluded
552
+ typeOnly?: boolean // true if file has only type/interface/enum exports
553
+ }
554
+
555
+ type DirectoryScore = {
556
+ dirPath: string
557
+ purity: number
558
+ impurityQuality: number | null
559
+ health: number
560
+ pureCount: number
561
+ impureCount: number
562
+ statusBreakdown: StatusBreakdown
563
+ pureLineCount: number
564
+ impureLineCount: number
565
+ fileScores: FileScore[]
566
+ }
567
+
568
+ type ProjectScore = {
569
+ purity: number // 0-100: percentage of pure functions
570
+ impurityQuality: number | null // 0-100: avg quality of impure functions
571
+ health: number // 0-100: percentage with status 'ok'
572
+ pureCount: number
573
+ impureCount: number
574
+ excludedCount: number
575
+ statusBreakdown: StatusBreakdown
576
+ pureLineCount: number
577
+ impureLineCount: number
578
+ directoryScores: DirectoryScore[]
579
+ timestamp: string
580
+ commitHash: string | null
581
+ // Sorted by bodyLineCount × (100 - qualityScore) descending (largest, lowest-quality first)
582
+ refactoringCandidates: {
583
+ name: string | null
584
+ filePath: string
585
+ startLine: number
586
+ bodyLineCount: number
587
+ qualityScore: number
588
+ markers: MarkerType[]
589
+ }[]
590
+ // Edge case flags
591
+ allExcluded?: boolean // true if all functions were excluded
592
+ // Subset analysis metadata (when --files is used)
593
+ subset?: boolean // true if this is a subset analysis
594
+ filesGlob?: string // the glob pattern used for --files
595
+ // Errors encountered during analysis
596
+ errors?: { filePath: string; error: string }[]
597
+ }
598
+ ```
599
+
600
+ ## Tests
601
+
602
+ ### Unit Tests (Pure Core — No Mocks Needed)
603
+
604
+ **`detect-markers.test.ts`** — For each marker category, provide an `ExtractedFunction` with known `callSites` / `propertyAccessChains` values and assert the correct markers are returned. High-risk areas:
605
+ - Database call pattern matching (various `db.*.operation` forms)
606
+ - `async` alone (without await or other I/O) produces NO markers
607
+ - Network fetch detection
608
+ - File system import detection
609
+
610
+ **`classifier.test.ts`** — Provide function data with marker combinations and assert classification:
611
+ - Zero markers → **pure**
612
+ - Only `async` keyword (no `await`, no other markers) → **pure**
613
+ - Has `await-expression` marker → **impure**
614
+ - Has `database-call` marker → **impure**
615
+ - Has any I/O marker → **impure**
616
+ - Trivial function (< 3 statements, no conditionals) → excluded
617
+
618
+ **`quality-scorer.test.ts`** — Provide impure function data and assert quality scores:
619
+ - Impure function calling `.pure.ts` import → high score (≥70)
620
+ - Impure function calling `planAcceptInvite` → high score (≥70)
621
+ - Impure function with I/O at start and end only → medium-high score
622
+ - Impure function with I/O interleaved throughout → low score (<40)
623
+ - Impure function with high cyclomatic complexity → score penalty applied
624
+ - Impure function with no pure function calls → score penalty applied
625
+
626
+ **`derive-status.test.ts`** — Verify status derivation:
627
+ - Pure function → status `ok`
628
+ - Impure with qualityScore ≥ 70 → status `ok`
629
+ - Impure with qualityScore 40-69 → status `review`
630
+ - Impure with qualityScore < 40 → status `refactor`
631
+
632
+ **`scorer.test.ts`** — Provide classification arrays with quality scores and assert metrics:
633
+ - All pure → purity: 100, health: 100
634
+ - All impure with high quality (≥70) → purity: 0, impurityQuality: high, health: 100
635
+ - All impure with low quality (<40) → purity: 0, impurityQuality: low, health: 0
636
+ - 50/50 pure/impure with mixed quality → verify purity, impurityQuality, health calculations
637
+ - Empty file → health: 100 (no functions to score)
638
+ - All excluded → health: 100 with `allExcluded: true`
639
+ - Verify `refactoringCandidates` is sorted by `bodyLineCount × (100 - qualityScore)` descending
640
+
641
+ ### Integration Test
642
+
643
+ **`integration.test.ts`** — Use ts-morph's in-memory file system to create a small multi-file project with known patterns, run the full pipeline (extract → detect → classify → score), and assert the end-to-end result. This test validates that the extraction layer and pure core agree on real TypeScript ASTs.
644
+
645
+ **Snapshot Testing:**
646
+ - Snapshot the full JSON output for known fixtures using Vitest's `toMatchSnapshot()`
647
+ - Snapshots serve as documentation of expected output format
648
+ - Review snapshot changes carefully during PR review
649
+
650
+ Test fixture should include:
651
+ - A pure function (no I/O markers)
652
+ - An async function without await (should classify as pure)
653
+ - An impure function with high quality score (calls `.pure.ts` import, I/O at edges)
654
+ - An impure function with low quality score (interleaved `db.*` calls, high complexity)
655
+ - A type-only file (should be excluded)
656
+ - A file with only trivial functions (should be excluded)
657
+
658
+ ## Transitive Effect Analysis
659
+
660
+ This tool is a new standalone package. It has **no transitive effects** on the SchoolAI codebase — it reads source files but does not modify them. The only integration point is if added to CI, where a `--min-score` failure could block a PR.
661
+
662
+ Within the tool itself:
663
+ - Changes to `markers.ts` (marker catalog) affect `detect-markers.ts` outputs, which cascade to `classifier.ts` and `scorer.ts`. All three are covered by unit tests.
664
+ - Changes to `types.ts` affect every module. Keep types minimal and stable.
665
+ - Changes to `extractor.ts` or `extract-functions.ts` (AST extraction) affect all downstream modules but are isolated behind the `ExtractedFunction` interface boundary — downstream modules never see ts-morph types.
666
+ - ts-morph version updates could change AST behavior. Pin the version and test against known fixtures.
667
+
668
+ ## Extensibility (Post-v1)
669
+
670
+ The marker catalog is defined as a data structure in `markers.ts`, enabling future extensibility:
671
+
672
+ - Custom markers can be added by extending the `MarkerType` union and catalog
673
+ - Post-v1: Support a `fcis.config.ts` file for project-specific marker definitions:
674
+ ```typescript
675
+ // fcis.config.ts
676
+ export default {
677
+ customMarkers: [
678
+ { pattern: '@sai/analytics.*', type: 'custom-analytics', severity: 'medium' },
679
+ { pattern: 'trackEvent', type: 'custom-analytics', severity: 'medium' },
680
+ ]
681
+ }
682
+ ```
683
+ - Custom markers appear in output alongside built-in markers
684
+ - This enables SchoolAI-specific patterns without forking the core tool
685
+
686
+ ## Resources for Implementation Context
687
+
688
+ These files should be included in context when implementing each phase:
689
+
690
+ **Phase 2 (Extraction):**
691
+ - `fcis/vendor/ts-morph/packages/ts-morph/src/compiler/ast/base/AsyncableNode.ts` — `isAsync()` API
692
+ - `fcis/vendor/ts-morph/packages/ts-morph/src/compiler/ast/expression/AwaitExpression.ts` — await detection
693
+ - `fcis/vendor/ts-morph/packages/ts-morph/src/compiler/ast/expression/CallExpression.ts` — call expression API
694
+ - `fcis/vendor/ts-morph/packages/ts-morph/src/compiler/ast/common/Node.ts` — `getDescendantsOfKind`, `forEachDescendant`
695
+ - `fcis/vendor/ts-morph/packages/ts-morph/src/compiler/ast/function/FunctionDeclaration.ts` — function declaration API
696
+ - `fcis/vendor/ts-morph/docs/navigation/example.md` — ts-morph navigation patterns
697
+ - `fcis/vendor/ts-morph/docs/setup/index.md` — project setup
698
+
699
+ **Phase 3 (Detection) — Target Codebase Patterns:**
700
+ - `apps/web/src/server/services/organizations.pure.ts` — gold standard pure function
701
+ - `apps/web/src/server/services/organizations.ts` L97-186 — shell pattern (acceptInvite with GATHER→DECIDE→EXECUTE)
702
+ - `apps/web/src/server/services/moderation.ts` — mixed file with embedded pure functions (`shouldModerate`, `shouldSendRedAlertEmail`, `getRedAlertType`)
703
+ - `apps/web/src/server/services/students.ts` — typical mixed service code
704
+ - `apps/web/src/server/services/chat.ts` — heavily mixed service code
705
+ - `ai-platform/apps/server/src/v1/blueprints/nodes/llm-v2/prepare-provider-request.ts` — large pure function
706
+
707
+ **Phase 4 (Classification) — Target Codebase Patterns:**
708
+ - `apps/web/src/components/Carousel/CarouselCommon.ts` — extracted pure utility
709
+ - `apps/web/src/components/MissionControl/Participants/Header/sortByUtil.ts` — pure sort functions
710
+
711
+ **Phase 8 (Baseline) — Validation Targets:**
712
+ - `apps/web/src/server/services/organizations.pure.test.ts` — demonstrates pure function testing
713
+ - `ai-platform/apps/server/src/v1/blueprints/nodes/llm-v2/prepare-provider-request.unit.test.ts` — pure function tests with no mocks
714
+ - `apps/web/src/server/services/moderation.test.ts` — mixed testing requiring mocks
715
+ - `ai-platform/apps/server/src/v1/services/projects/projects.ts` — `buildProjectService` pattern with `toPublicProject` pure transformer
716
+ - `packages/utils/src/fuzzy-enum.ts` — naturally pure utility
717
+ - `packages/utils/src/string-template.ts` — naturally pure utility
718
+
719
+ ## Documentation
720
+
721
+ ### README.md
722
+
723
+ Create with: overview, installation, CLI usage examples, score interpretation guide, and a worked example of refactoring a mixed function into pure + shell.
724
+
725
+ ### TECHNICAL.md
726
+
727
+ Create with: architecture diagram, type definitions reference, marker catalog, classification rules, scoring formula, and extension guide (how to add new markers).
728
+
729
+ ---
730
+
731
+ ## Addendum: Frontend Component Analysis (v2)
732
+
733
+ **Status:** Deferred to v2. This section documents the planned approach for analyzing `.tsx` files and React components.
734
+
735
+ ### Rationale for Deferral
736
+
737
+ React components require different metrics than backend functions:
738
+ - The core FCIS question for backend code is: *"Can this function's logic be tested without mocking?"*
739
+ - The equivalent question for frontend components is: *"Can this component's visual states be demonstrated in Storybook without mocking the data layer?"*
740
+
741
+ These are related but distinct problems. v1 focuses on the backend question for `.ts` files. v2 will extend to React components.
742
+
743
+ ### Planned Approach for v2
744
+
745
+ #### React Hook Markers
746
+
747
+ Add markers for React hooks (impurity in component context):
748
+ - `use[A-Z]*` pattern → marker `react-hook`
749
+ - `useState`, `useEffect`, `useLayoutEffect`, `useReducer`, `useRef`, `useContext`, `useCallback` → `react-state-hook` or `react-effect-hook`
750
+ - `useQuery`, `useMutation`, `useSWR` → `react-data-hook`
751
+ - `useRouter`, `useNavigate`, `useSearchParams` → `react-navigation-hook`
752
+
753
+ #### JSX Complexity Detection
754
+
755
+ For `.tsx` files with JSX, extract additional markers:
756
+ - Conditional expressions in JSX (`{x ? a : b}`) → marker `jsx-ternary`
757
+ - Logical AND rendering (`{x && <Foo />}`) → marker `jsx-logical-and`
758
+ - Nested ternaries → marker `jsx-nested-ternary` (high severity)
759
+ - Inline callbacks with > 1 statement → marker `jsx-complex-callback`
760
+
761
+ Compute `jsxComplexity` score based on these markers.
762
+
763
+ #### Component Classification
764
+
765
+ For React components, apply a separate classification:
766
+
767
+ | Classification | Criteria | Storybook-able? |
768
+ |---------------|----------|-----------------|
769
+ | **Presentational** | No hooks, all state via props | Yes |
770
+ | **Container** | Has hooks, low jsxComplexity, delegates rendering | Partial — children are |
771
+ | **Entangled** | Has hooks AND high jsxComplexity | No |
772
+ | **Page/Layout** | Route-level orchestration | N/A — infrastructure |
773
+
774
+ #### Effect Concentration Metric
775
+
776
+ Measure where hooks are used in the component tree:
777
+ - Healthy: Most hooks in pages/containers (top of tree)
778
+ - Problematic: Hooks scattered throughout leaf components
779
+
780
+ `effectConcentrationScore = (effectsInPages + effectsInContainers) / totalHookCalls`
781
+
782
+ #### Frontend-Specific Types (v2)
783
+
784
+ ```typescript
785
+ type JSXMarker = {
786
+ type: 'jsx-ternary' | 'jsx-logical-and' | 'jsx-nested-ternary' | 'jsx-complex-callback'
787
+ detail: string
788
+ line: number
789
+ }
790
+
791
+ type ComponentClassification = 'presentational' | 'container' | 'entangled' | 'page'
792
+
793
+ type ClassifiedComponent = ClassifiedFunction & {
794
+ jsxComplexity: number
795
+ jsxMarkers: JSXMarker[]
796
+ componentClassification: ComponentClassification
797
+ }
798
+
799
+ type FrontendScore = {
800
+ presentationalCount: number
801
+ containerCount: number
802
+ entangledCount: number
803
+ pageCount: number
804
+ effectsInPages: number
805
+ effectsInContainers: number
806
+ effectsInLeaves: number
807
+ effectConcentrationScore: number // 0-100, higher is better
808
+ representabilityScore: number // (presentational + container) / (total - pages)
809
+ topEntangledComponents: {
810
+ name: string | null
811
+ filePath: string
812
+ startLine: number
813
+ jsxComplexity: number
814
+ hookCount: number
815
+ }[]
816
+ }
817
+ ```
818
+
819
+ #### v2 CLI Extensions
820
+
821
+ ```
822
+ --include-tsx Include .tsx files in analysis (v2)
823
+ --min-frontend-score <N> Exit with code 1 if frontend representability score < N
824
+ ```
825
+
826
+ #### v2 Target Files for Validation
827
+
828
+ - `apps/web/src/hooks/useSettingsRoutes.ts` — FCIS in React (pure derivations + hook shell)
829
+ - `apps/web/src/hooks/useSpaceLaunchStatus.ts` — hook with `useMemo` wrapping pure logic
830
+ - `apps/web/src/components/Carousel/Cards/SpaceCard.tsx` — mixed component with inline logic
831
+ - `apps/web/src/components/` — scan for JSX complexity patterns
832
+ - `apps/web/src/app/` — Next.js app router pages for page classification baseline