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,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
|