fcis 0.2.0 → 0.2.1
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/README.md +205 -158
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/docs/images/fc-is-submarine.webp +0 -0
- package/package.json +2 -1
- package/src/cli.ts +6 -1
package/README.md
CHANGED
|
@@ -2,133 +2,144 @@
|
|
|
2
2
|
|
|
3
3
|
**Functional Core, Imperative Shell** analyzer for TypeScript codebases.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="docs/images/fc-is-submarine.webp" alt="FCIS Submarine: Keep the chaos in the waves, keep the math underwater" width="600" />
|
|
7
|
+
</p>
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
<p align="center"><em>Keep the chaos in the waves. Keep the math underwater.</em></p>
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Philosophy
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
pnpm install
|
|
13
|
-
pnpm build
|
|
14
|
-
```
|
|
13
|
+
FCIS is built on a simple observation: **some code is easier to trust than others.**
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# Analyze a project
|
|
20
|
-
npx fcis tsconfig.json
|
|
15
|
+
Consider two functions:
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
```typescript
|
|
18
|
+
// Function A: Pure
|
|
19
|
+
function calculateDiscount(price: number, memberYears: number): number {
|
|
20
|
+
if (memberYears >= 5) return price * 0.20
|
|
21
|
+
if (memberYears >= 2) return price * 0.10
|
|
22
|
+
return 0
|
|
23
|
+
}
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// Function B: Impure
|
|
26
|
+
async function applyDiscount(userId: string) {
|
|
27
|
+
const user = await db.user.findFirst({ where: { id: userId } })
|
|
28
|
+
const cart = await db.cart.findFirst({ where: { userId } })
|
|
29
|
+
let discount = 0
|
|
30
|
+
if (user.memberSince) {
|
|
31
|
+
const years = (Date.now() - user.memberSince.getTime()) / (365 * 24 * 60 * 60 * 1000)
|
|
32
|
+
if (years >= 5) discount = cart.total * 0.20
|
|
33
|
+
else if (years >= 2) discount = cart.total * 0.10
|
|
34
|
+
}
|
|
35
|
+
await db.cart.update({ where: { id: cart.id }, data: { discount } })
|
|
36
|
+
await sendEmail(user.email, `You saved $${discount}!`)
|
|
37
|
+
}
|
|
30
38
|
```
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
**Function A** can be tested with a simple assertion: `expect(calculateDiscount(100, 5)).toBe(20)`. No mocks, no setup, no database. You can run it a thousand times in milliseconds and know exactly what it does.
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
**Function B** requires a test database, mock email service, careful setup of user and cart records, and you still can't be sure the discount logic is correct because it's tangled up with I/O operations.
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
- Take arguments and return values
|
|
38
|
-
- Have no side effects
|
|
39
|
-
- Can be tested without mocking
|
|
44
|
+
This is the core insight of the **Functional Core, Imperative Shell** pattern:
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
> Separate the code you need to **think hard about** (business logic) from the code that **talks to the outside world** (I/O). Test the thinking. Integration-test the talking.
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
- **High (≥70):** I/O is organized, calls pure functions, follows GATHER→DECIDE→EXECUTE pattern
|
|
45
|
-
- **Medium (40-69):** Some structure, room for improvement
|
|
46
|
-
- **Low (<40):** Tangled code, business logic mixed with I/O
|
|
48
|
+
## What This Tool Measures
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
FCIS doesn't try to eliminate impure code — **you need I/O to build useful software**. Instead, it measures:
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
### 1. How much of your logic is testable without mocks? → **Purity**
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
A function is **pure** if it has no I/O markers (database calls, network requests, file system access, etc.). Pure functions are trivially testable and easy to reason about.
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
- Inflated function counts (a 301-line function with 6 callbacks is 1 function, not 7)
|
|
56
|
-
- Diluted health scores (callbacks can't mask a problematic parent)
|
|
57
|
-
- Double-counted line counts
|
|
56
|
+
*Purity = pure functions / total functions*
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
### 2. When you do have I/O, is it well-organized? → **Impurity Quality**
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
Impure functions aren't bad — they're necessary. But there's a difference between:
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
- **Well-structured:** Gathers data, calls pure functions for decisions, executes effects (GATHER → DECIDE → EXECUTE)
|
|
63
|
+
- **Tangled:** Business logic interleaved with database calls, conditionals mixed with I/O, impossible to test in pieces
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
FCIS scores impure functions from 0-100 based on structural signals. A score of 70+ means "this I/O code is well-organized."
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|---------------|----------|------------------------|
|
|
69
|
-
| **Pure** | No I/O markers | ✅ Yes |
|
|
70
|
-
| **Impure** | Has I/O markers (await, db, fetch, fs, etc.) | ❌ No |
|
|
67
|
+
### 3. Overall: How confident can you be in this codebase? → **Health**
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
**Health** combines purity and quality into a single number:
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Impure | ≥ 70 | ✓ OK | Well-structured |
|
|
78
|
-
| Impure | 40-69 | ◐ Review | Consider improving |
|
|
79
|
-
| Impure | < 40 | ✗ Refactor | Prioritize cleanup |
|
|
71
|
+
- Pure functions are automatically "healthy" (trivially testable)
|
|
72
|
+
- Impure functions with quality ≥70 are "healthy" (well-structured, integration-testable)
|
|
73
|
+
- Impure functions with quality <70 need attention
|
|
80
74
|
|
|
81
|
-
|
|
75
|
+
*Health = functions with OK status / total functions*
|
|
82
76
|
|
|
83
|
-
The
|
|
77
|
+
**The goal isn't 100% purity.** A codebase with 40% purity and 90% health is better than one with 80% purity and 50% health. The first has well-organized I/O; the second has tangled messes.
|
|
84
78
|
|
|
85
|
-
|
|
86
|
-
|--------|----------|
|
|
87
|
-
| `await-expression` | `await fetch()`, `await db.query()` |
|
|
88
|
-
| `database-call` | `db.user.findFirst()`, `prisma.post.create()` |
|
|
89
|
-
| `network-fetch` | `fetch(url)` |
|
|
90
|
-
| `network-http` | `axios.get()`, imports from `axios` |
|
|
91
|
-
| `fs-call` | `fs.readFile()`, `fs.writeFile()` |
|
|
92
|
-
| `env-access` | `process.env.NODE_ENV` |
|
|
93
|
-
| `console-log` | `console.log()`, `console.error()` |
|
|
94
|
-
| `logging` | `logger.info()`, imports from logger |
|
|
95
|
-
| `telemetry` | `trackEvent()`, `analytics.track()` |
|
|
96
|
-
| `queue-enqueue` | `queue.enqueue()`, `queue.add()` |
|
|
97
|
-
| `event-emit` | `emitter.emit()`, `dispatcher.dispatch()` |
|
|
98
|
-
|
|
99
|
-
**Note:** `async` alone does NOT make a function impure — only actual I/O markers count.
|
|
79
|
+
## The FCIS Pattern
|
|
100
80
|
|
|
101
|
-
|
|
81
|
+
The pattern this tool encourages:
|
|
102
82
|
|
|
103
83
|
```
|
|
104
|
-
|
|
84
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
85
|
+
│ IMPERATIVE SHELL │
|
|
86
|
+
│ │
|
|
87
|
+
│ async function handleRequest(id: string) { │
|
|
88
|
+
│ // GATHER - get data from the outside world │
|
|
89
|
+
│ const user = await db.user.findFirst(...) │
|
|
90
|
+
│ const permissions = await authService.check(...) │
|
|
91
|
+
│ │
|
|
92
|
+
│ // DECIDE - call pure functions (testable!) │
|
|
93
|
+
│ const plan = planUserAction(user, permissions) │
|
|
94
|
+
│ │
|
|
95
|
+
│ // EXECUTE - write to the outside world │
|
|
96
|
+
│ await db.audit.create({ data: plan.auditEntry }) │
|
|
97
|
+
│ return plan.response │
|
|
98
|
+
│ } │
|
|
99
|
+
└─────────────────────────────────────────────────────────────┘
|
|
100
|
+
│
|
|
101
|
+
▼
|
|
102
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
103
|
+
│ FUNCTIONAL CORE │
|
|
104
|
+
│ │
|
|
105
|
+
│ function planUserAction(user: User, perms: Permissions) { │
|
|
106
|
+
│ // Pure logic - no I/O, no side effects │
|
|
107
|
+
│ // Easy to test: input → output │
|
|
108
|
+
│ if (!perms.canAct) { │
|
|
109
|
+
│ return { allowed: false, reason: 'forbidden' } │
|
|
110
|
+
│ } │
|
|
111
|
+
│ return { │
|
|
112
|
+
│ allowed: true, │
|
|
113
|
+
│ auditEntry: { userId: user.id, action: 'acted' }, │
|
|
114
|
+
│ response: { success: true } │
|
|
115
|
+
│ } │
|
|
116
|
+
│ } │
|
|
117
|
+
└─────────────────────────────────────────────────────────────┘
|
|
118
|
+
```
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
tsconfig Path to tsconfig.json
|
|
120
|
+
## Installation
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
--min-purity <N> Exit code 1 if purity < N (0-100)
|
|
114
|
-
--min-quality <N> Exit code 1 if impurity quality < N (0-100)
|
|
115
|
-
--files, -f <glob> Analyze only matching files
|
|
116
|
-
--format <fmt> Output: console (default), json, summary
|
|
117
|
-
--dir-depth <N> Roll up directory metrics to depth N (e.g., 1 for top-level)
|
|
118
|
-
--quiet, -q Suppress output, use exit code only
|
|
119
|
-
--verbose, -v Show per-file details
|
|
120
|
-
--help Show help
|
|
121
|
-
--version Show version
|
|
122
|
+
```bash
|
|
123
|
+
npm install -g fcis
|
|
124
|
+
# or
|
|
125
|
+
pnpm add -g fcis
|
|
122
126
|
```
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
## Quick Start
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
```bash
|
|
131
|
+
# Analyze a project
|
|
132
|
+
fcis tsconfig.json
|
|
133
|
+
|
|
134
|
+
# Set a health threshold for CI
|
|
135
|
+
fcis tsconfig.json --min-health 70
|
|
136
|
+
|
|
137
|
+
# Output JSON for further processing
|
|
138
|
+
fcis tsconfig.json --format json --output report.json
|
|
139
|
+
|
|
140
|
+
# Analyze specific files (for pre-commit hooks)
|
|
141
|
+
fcis tsconfig.json --files "src/services/**/*.ts"
|
|
142
|
+
```
|
|
132
143
|
|
|
133
144
|
## Example Output
|
|
134
145
|
|
|
@@ -157,29 +168,86 @@ Top Refactoring Candidates:
|
|
|
157
168
|
Markers: database-call, await-expression
|
|
158
169
|
```
|
|
159
170
|
|
|
160
|
-
|
|
171
|
+
## What Makes a Function Impure?
|
|
161
172
|
|
|
162
|
-
|
|
173
|
+
FCIS detects these I/O patterns:
|
|
163
174
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
| Marker | Examples |
|
|
176
|
+
|--------|----------|
|
|
177
|
+
| `await-expression` | `await fetch()`, `await db.query()` |
|
|
178
|
+
| `database-call` | `db.user.findFirst()`, `prisma.post.create()` |
|
|
179
|
+
| `network-fetch` | `fetch(url)` |
|
|
180
|
+
| `network-http` | `axios.get()` |
|
|
181
|
+
| `fs-call` | `fs.readFile()`, `fs.writeFile()` |
|
|
182
|
+
| `env-access` | `process.env.NODE_ENV` |
|
|
183
|
+
| `console-log` | `console.log()`, `console.error()` |
|
|
184
|
+
| `logging` | `logger.info()` |
|
|
185
|
+
| `telemetry` | `trackEvent()`, `analytics.track()` |
|
|
186
|
+
| `queue-enqueue` | `queue.enqueue()`, `queue.add()` |
|
|
187
|
+
| `event-emit` | `emitter.emit()` |
|
|
167
188
|
|
|
168
|
-
|
|
189
|
+
**Note:** `async` alone does NOT make a function impure — only actual I/O operations count.
|
|
190
|
+
|
|
191
|
+
## What Makes Impure Code "High Quality"?
|
|
192
|
+
|
|
193
|
+
FCIS rewards structural patterns that make impure code easier to understand and test:
|
|
194
|
+
|
|
195
|
+
| Signal | Why It's Good |
|
|
196
|
+
|--------|---------------|
|
|
197
|
+
| Calls `.pure.ts` imports | Explicitly separates pure logic |
|
|
198
|
+
| Calls `plan*/derive*/compute*` | Uses pure functions for decisions |
|
|
199
|
+
| I/O at start (GATHER) | Clear data-fetching phase |
|
|
200
|
+
| I/O at end (EXECUTE) | Clear effect-execution phase |
|
|
201
|
+
| Low complexity | Simple orchestration |
|
|
202
|
+
| Calls `is*/has*/should*` | Uses pure predicates |
|
|
203
|
+
|
|
204
|
+
And penalizes patterns that make code hard to reason about:
|
|
205
|
+
|
|
206
|
+
| Signal | Why It's Bad |
|
|
207
|
+
|--------|--------------|
|
|
208
|
+
| I/O interleaved throughout | Can't separate "what" from "how" |
|
|
209
|
+
| High cyclomatic complexity | Too much logic mixed with I/O |
|
|
210
|
+
| Multiple I/O types | Too many responsibilities |
|
|
211
|
+
| No pure function calls | All logic is inline and untestable |
|
|
212
|
+
| Very long function | God function, needs decomposition |
|
|
213
|
+
|
|
214
|
+
## Compositional Scoring
|
|
215
|
+
|
|
216
|
+
Inline callbacks (passed to `map`, `filter`, `forEach`, etc.) are absorbed into their parent function's score. This means:
|
|
217
|
+
|
|
218
|
+
- A 301-line function with 6 callbacks counts as **1 function**, not 7
|
|
219
|
+
- If a callback is impure, the parent is considered impure
|
|
220
|
+
- Quality scores blend parent and children by line count
|
|
221
|
+
|
|
222
|
+
This prevents gaming the metrics with lots of small callbacks while leaving a tangled parent function.
|
|
223
|
+
|
|
224
|
+
## CLI Reference
|
|
169
225
|
|
|
170
226
|
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
fcis <tsconfig> [options]
|
|
228
|
+
|
|
229
|
+
Options:
|
|
230
|
+
--min-health <N> Exit code 1 if health < N (0-100)
|
|
231
|
+
--min-purity <N> Exit code 1 if purity < N (0-100)
|
|
232
|
+
--min-quality <N> Exit code 1 if impurity quality < N (0-100)
|
|
233
|
+
--files, -f <glob> Analyze only matching files
|
|
234
|
+
--format <fmt> Output: console (default), json, summary
|
|
235
|
+
--output, -o <file> Write JSON report to file
|
|
236
|
+
--dir-depth <N> Roll up directory metrics to depth N
|
|
237
|
+
--quiet, -q Suppress output, use exit code only
|
|
238
|
+
--verbose, -v Show per-file details
|
|
239
|
+
--help Show help
|
|
240
|
+
--version Show version
|
|
180
241
|
```
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
### Exit Codes
|
|
244
|
+
|
|
245
|
+
| Code | Meaning |
|
|
246
|
+
|------|---------|
|
|
247
|
+
| 0 | Success, all thresholds passed |
|
|
248
|
+
| 1 | Below threshold |
|
|
249
|
+
| 2 | Configuration error |
|
|
250
|
+
| 3 | Analysis error |
|
|
183
251
|
|
|
184
252
|
## CI Integration
|
|
185
253
|
|
|
@@ -187,10 +255,10 @@ This shows ALL directories at the specified depth with aggregated metrics, provi
|
|
|
187
255
|
|
|
188
256
|
```yaml
|
|
189
257
|
- name: FCIS Analysis
|
|
190
|
-
run:
|
|
258
|
+
run: fcis tsconfig.json --min-health 70 --format summary
|
|
191
259
|
```
|
|
192
260
|
|
|
193
|
-
### Pre-commit Hook
|
|
261
|
+
### Pre-commit Hook
|
|
194
262
|
|
|
195
263
|
```json
|
|
196
264
|
{
|
|
@@ -200,25 +268,10 @@ This shows ALL directories at the specified depth with aggregated metrics, provi
|
|
|
200
268
|
}
|
|
201
269
|
```
|
|
202
270
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
The **Functional Core, Imperative Shell** pattern separates code into:
|
|
206
|
-
|
|
207
|
-
### Pure Core (Functional)
|
|
208
|
-
- Business logic, calculations, validations
|
|
209
|
-
- Takes data in, returns data out
|
|
210
|
-
- No side effects
|
|
211
|
-
- Trivially testable
|
|
271
|
+
## Refactoring Example
|
|
212
272
|
|
|
213
|
-
|
|
214
|
-
- I/O operations (database, network, file system)
|
|
215
|
-
- Orchestrates: GATHER data → call pure functions → EXECUTE effects
|
|
216
|
-
- Thin wrapper around pure core
|
|
217
|
-
- Requires integration tests
|
|
273
|
+
**Before (tangled — quality score ~25):**
|
|
218
274
|
|
|
219
|
-
### Example Refactoring
|
|
220
|
-
|
|
221
|
-
**Before (tangled):**
|
|
222
275
|
```typescript
|
|
223
276
|
async function acceptInvite(inviteId: string) {
|
|
224
277
|
const invite = await db.invitation.findFirst({ where: { id: inviteId } })
|
|
@@ -226,7 +279,7 @@ async function acceptInvite(inviteId: string) {
|
|
|
226
279
|
|
|
227
280
|
const org = await db.organization.findFirst({ where: { id: invite.orgId } })
|
|
228
281
|
|
|
229
|
-
// Business logic mixed with I/O
|
|
282
|
+
// Business logic mixed with I/O — hard to test!
|
|
230
283
|
if (invite.expiresAt < new Date()) {
|
|
231
284
|
await db.invitation.update({ where: { id: inviteId }, data: { status: 'expired' } })
|
|
232
285
|
throw new Error('Expired')
|
|
@@ -241,10 +294,14 @@ async function acceptInvite(inviteId: string) {
|
|
|
241
294
|
}
|
|
242
295
|
```
|
|
243
296
|
|
|
244
|
-
**After (FCIS):**
|
|
297
|
+
**After (FCIS pattern — quality score ~80):**
|
|
298
|
+
|
|
245
299
|
```typescript
|
|
246
|
-
//
|
|
247
|
-
|
|
300
|
+
// PURE: Testable with simple assertions
|
|
301
|
+
function planAcceptInvite(
|
|
302
|
+
invite: Invitation,
|
|
303
|
+
org: Organization
|
|
304
|
+
): { action: 'accept', member: MemberData } | { action: 'reject', reason: string } {
|
|
248
305
|
if (invite.expiresAt < new Date()) {
|
|
249
306
|
return { action: 'reject', reason: 'expired' }
|
|
250
307
|
}
|
|
@@ -253,17 +310,18 @@ export function planAcceptInvite(invite: Invitation, org: Organization): AcceptI
|
|
|
253
310
|
}
|
|
254
311
|
return {
|
|
255
312
|
action: 'accept',
|
|
256
|
-
|
|
313
|
+
member: { userId: invite.userId, orgId: org.id }
|
|
257
314
|
}
|
|
258
315
|
}
|
|
259
316
|
|
|
260
|
-
//
|
|
317
|
+
// IMPURE: Thin shell, clear GATHER → DECIDE → EXECUTE
|
|
261
318
|
async function acceptInvite(inviteId: string) {
|
|
262
319
|
// GATHER
|
|
263
320
|
const invite = await db.invitation.findFirst({ where: { id: inviteId } })
|
|
321
|
+
if (!invite) throw new Error('Not found')
|
|
264
322
|
const org = await db.organization.findFirst({ where: { id: invite.orgId } })
|
|
265
323
|
|
|
266
|
-
// DECIDE
|
|
324
|
+
// DECIDE
|
|
267
325
|
const plan = planAcceptInvite(invite, org)
|
|
268
326
|
|
|
269
327
|
// EXECUTE
|
|
@@ -271,37 +329,26 @@ async function acceptInvite(inviteId: string) {
|
|
|
271
329
|
await db.invitation.update({ where: { id: inviteId }, data: { status: plan.reason } })
|
|
272
330
|
throw new Error(plan.reason)
|
|
273
331
|
}
|
|
274
|
-
|
|
275
|
-
await db.member.create({ data: plan.memberData })
|
|
332
|
+
await db.member.create({ data: plan.member })
|
|
276
333
|
await db.invitation.update({ where: { id: inviteId }, data: { status: 'accepted' } })
|
|
277
334
|
}
|
|
278
335
|
```
|
|
279
336
|
|
|
280
|
-
|
|
337
|
+
The business logic (expiration check, capacity check) is now in a pure function that can be tested with simple input/output assertions. The shell just orchestrates I/O.
|
|
281
338
|
|
|
282
|
-
|
|
283
|
-
- Calls functions from `.pure.ts` files (+30)
|
|
284
|
-
- Calls `plan*/derive*/compute*/transform*` functions (+20)
|
|
285
|
-
- I/O concentrated at start (GATHER pattern) (+15)
|
|
286
|
-
- I/O concentrated at end (EXECUTE pattern) (+15)
|
|
287
|
-
- Low cyclomatic complexity (+10)
|
|
288
|
-
- Shell naming convention (`handle*/fetch*/save*`) (+5)
|
|
289
|
-
- Calls predicate functions (`is*/has*/should*`) (+5)
|
|
339
|
+
## Limitations
|
|
290
340
|
|
|
291
|
-
|
|
292
|
-
- I/O
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
- No pure function calls (-10)
|
|
296
|
-
- Very long function (>100 lines) (-10)
|
|
341
|
+
- Analyzes `.ts` files only (`.tsx` support planned)
|
|
342
|
+
- Pattern matching is heuristic — may miss custom I/O patterns
|
|
343
|
+
- Does not trace transitive purity (a function calling another function)
|
|
344
|
+
- Quality weights are opinionated and tuned for specific patterns
|
|
297
345
|
|
|
298
|
-
##
|
|
346
|
+
## Further Reading
|
|
299
347
|
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
- Quality scoring weights are tuned for SchoolAI patterns
|
|
348
|
+
- [TECHNICAL.md](./TECHNICAL.md) — Implementation details, scoring weights, extension points
|
|
349
|
+
- [Gary Bernhardt's "Boundaries" talk](https://www.destroyallsoftware.com/talks/boundaries) — Original FCIS concept
|
|
350
|
+
- [Mark Seemann's "Impureim Sandwich"](https://blog.ploeh.dk/2020/03/02/impureim-sandwich/) — Similar pattern
|
|
304
351
|
|
|
305
352
|
## License
|
|
306
353
|
|
|
307
|
-
MIT
|
|
354
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import { cli } from 'cleye';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import * as fs3 from 'fs';
|
|
@@ -1912,6 +1913,8 @@ var EXIT_SUCCESS = 0;
|
|
|
1912
1913
|
var EXIT_THRESHOLD_FAILED = 1;
|
|
1913
1914
|
var EXIT_CONFIG_ERROR = 2;
|
|
1914
1915
|
var EXIT_ANALYSIS_ERROR = 3;
|
|
1916
|
+
var require2 = createRequire(import.meta.url);
|
|
1917
|
+
var pkg = require2("../package.json");
|
|
1915
1918
|
function handleAnalysisOutput(score, flags) {
|
|
1916
1919
|
if (!flags.quiet) {
|
|
1917
1920
|
if (flags.json || flags.format === "json") {
|
|
@@ -1963,7 +1966,7 @@ function ensureOutputDirectory(outputPath) {
|
|
|
1963
1966
|
}
|
|
1964
1967
|
var argv = cli({
|
|
1965
1968
|
name: "fcis",
|
|
1966
|
-
version:
|
|
1969
|
+
version: pkg.version,
|
|
1967
1970
|
flags: {
|
|
1968
1971
|
json: {
|
|
1969
1972
|
type: Boolean,
|