
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
Functional Core, Imperative Shell analyzer for TypeScript codebases.
Keep the chaos in the waves. Keep the math underwater.
FCIS is built on a simple observation: some code is easier to trust than others.
Consider two functions:
// Function A: Pure
function calculateDiscount(price: number, memberYears: number): number {
if (memberYears >= 5) return price * 0.20
if (memberYears >= 2) return price * 0.10
return 0
}
// Function B: Impure
async function applyDiscount(userId: string) {
const user = await db.user.findFirst({ where: { id: userId } })
const cart = await db.cart.findFirst({ where: { userId } })
let discount = 0
if (user.memberSince) {
const years = (Date.now() - user.memberSince.getTime()) / (365 * 24 * 60 * 60 * 1000)
if (years >= 5) discount = cart.total * 0.20
else if (years >= 2) discount = cart.total * 0.10
}
await db.cart.update({ where: { id: cart.id }, data: { discount } })
await sendEmail(user.email, `You saved $${discount}!`)
}
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.
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.
This is the core insight of the Functional Core, Imperative Shell pattern:
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.
FCIS doesn't try to eliminate impure code — you need I/O to build useful software. Instead, it measures:
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.
Purity = pure functions / total functions
Impure functions aren't bad — they're necessary. But there's a difference between:
FCIS scores impure functions from 0-100 based on structural signals. A score of 70+ means "this I/O code is well-organized."
Health combines purity and quality into a single number:
Health = functions with OK status / total functions
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.
The pattern this tool encourages:
┌─────────────────────────────────────────────────────────────┐
│ IMPERATIVE SHELL │
│ │
│ async function handleRequest(id: string) { │
│ // GATHER - get data from the outside world │
│ const user = await db.user.findFirst(...) │
│ const permissions = await authService.check(...) │
│ │
│ // DECIDE - call pure functions (testable!) │
│ const plan = planUserAction(user, permissions) │
│ │
│ // EXECUTE - write to the outside world │
│ await db.audit.create({ data: plan.auditEntry }) │
│ return plan.response │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ FUNCTIONAL CORE │
│ │
│ function planUserAction(user: User, perms: Permissions) { │
│ // Pure logic - no I/O, no side effects │
│ // Easy to test: input → output │
│ if (!perms.canAct) { │
│ return { allowed: false, reason: 'forbidden' } │
│ } │
│ return { │
│ allowed: true, │
│ auditEntry: { userId: user.id, action: 'acted' }, │
│ response: { success: true } │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
npm install -g fcis
# or
pnpm add -g fcis
# Analyze a project
fcis tsconfig.json
# Set a health threshold for CI
fcis tsconfig.json --min-health 70
# Output JSON for further processing
fcis tsconfig.json --format json --output report.json
# Analyze specific files (for pre-commit hooks)
fcis tsconfig.json --files "src/services/**/*.ts"
FCIS Analysis
═══════════════════════════════════════════════════════════
Project Health: 77% ████████████████████░░░░░
Purity: 45% (234 pure / 520 total)
Impurity Quality: 68% average
Status Breakdown:
✓ OK: 312 functions (60%) — no action needed
◐ Review: 89 functions (17%) — could be improved
✗ Refactor: 119 functions (23%) — tangled, needs work
Top Refactoring Candidates:
(Sorted by impact: size × complexity)
1. 25/100 processOrder (150 lines)
/src/services/orders.ts:45
Markers: database-call, network-fetch, console-log
2. 32/100 handleUserUpdate (98 lines)
/src/services/users.ts:120
Markers: database-call, await-expression
FCIS detects these I/O patterns:
| Marker | Examples |
|---|---|
await-expression | await fetch(), await db.query() |
database-call | db.user.findFirst(), prisma.post.create() |
network-fetch | fetch(url) |
network-http | axios.get() |
fs-call | fs.readFile(), fs.writeFile() |
env-access | process.env.NODE_ENV |
console-log | console.log(), console.error() |
logging | logger.info() |
telemetry | trackEvent(), analytics.track() |
queue-enqueue | queue.enqueue(), queue.add() |
event-emit | emitter.emit() |
Note: async alone does NOT make a function impure — only actual I/O operations count.
FCIS rewards structural patterns that make impure code easier to understand and test:
| Signal | Why It's Good |
|---|---|
Calls .pure.ts imports | Explicitly separates pure logic |
Calls plan*/derive*/compute* | Uses pure functions for decisions |
| I/O at start (GATHER) | Clear data-fetching phase |
| I/O at end (EXECUTE) | Clear effect-execution phase |
| Low complexity | Simple orchestration |
Calls is*/has*/should* | Uses pure predicates |
And penalizes patterns that make code hard to reason about:
| Signal | Why It's Bad |
|---|---|
| I/O interleaved throughout | Can't separate "what" from "how" |
| High cyclomatic complexity | Too much logic mixed with I/O |
| Multiple I/O types | Too many responsibilities |
| No pure function calls | All logic is inline and untestable |
| Very long function | God function, needs decomposition |
Inline callbacks (passed to map, filter, forEach, etc.) are absorbed into their parent function's score. This means:
This prevents gaming the metrics with lots of small callbacks while leaving a tangled parent function.
fcis <tsconfig> [options]
Options:
--min-health <N> Exit code 1 if health < N (0-100)
--min-purity <N> Exit code 1 if purity < N (0-100)
--min-quality <N> Exit code 1 if impurity quality < N (0-100)
--files, -f <glob> Analyze only matching files
--format <fmt> Output: console (default), json, summary
--output, -o <file> Write JSON report to file
--dir-depth <N> Roll up directory metrics to depth N
--quiet, -q Suppress output, use exit code only
--verbose, -v Show per-file details
--help Show help
--version Show version
| Code | Meaning |
|---|---|
| 0 | Success, all thresholds passed |
| 1 | Below threshold |
| 2 | Configuration error |
| 3 | Analysis error |
- name: FCIS Analysis
run: fcis tsconfig.json --min-health 70 --format summary
{
"lint-staged": {
"*.ts": ["fcis tsconfig.json --files"]
}
}
Before (tangled — quality score ~25):
async function acceptInvite(inviteId: string) {
const invite = await db.invitation.findFirst({ where: { id: inviteId } })
if (!invite) throw new Error('Not found')
const org = await db.organization.findFirst({ where: { id: invite.orgId } })
// Business logic mixed with I/O — hard to test!
if (invite.expiresAt < new Date()) {
await db.invitation.update({ where: { id: inviteId }, data: { status: 'expired' } })
throw new Error('Expired')
}
if (org.memberCount >= org.maxMembers) {
throw new Error('Org full')
}
await db.member.create({ data: { userId: invite.userId, orgId: org.id } })
await db.invitation.update({ where: { id: inviteId }, data: { status: 'accepted' } })
}
After (FCIS pattern — quality score ~80):
// PURE: Testable with simple assertions
function planAcceptInvite(
invite: Invitation,
org: Organization
): { action: 'accept', member: MemberData } | { action: 'reject', reason: string } {
if (invite.expiresAt < new Date()) {
return { action: 'reject', reason: 'expired' }
}
if (org.memberCount >= org.maxMembers) {
return { action: 'reject', reason: 'org-full' }
}
return {
action: 'accept',
member: { userId: invite.userId, orgId: org.id }
}
}
// IMPURE: Thin shell, clear GATHER → DECIDE → EXECUTE
async function acceptInvite(inviteId: string) {
// GATHER
const invite = await db.invitation.findFirst({ where: { id: inviteId } })
if (!invite) throw new Error('Not found')
const org = await db.organization.findFirst({ where: { id: invite.orgId } })
// DECIDE
const plan = planAcceptInvite(invite, org)
// EXECUTE
if (plan.action === 'reject') {
await db.invitation.update({ where: { id: inviteId }, data: { status: plan.reason } })
throw new Error(plan.reason)
}
await db.member.create({ data: plan.member })
await db.invitation.update({ where: { id: inviteId }, data: { status: 'accepted' } })
}
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.
.ts files only (.tsx support planned)MIT
FAQs
Functional Core, Imperative Shell analyzer for TypeScript codebases
We found that fcis demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.