New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

fcis

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fcis

Functional Core, Imperative Shell analyzer for TypeScript codebases

latest
npmnpm
Version
0.2.1
Version published
Maintainers
1
Created
Source

FCIS Analyzer

Functional Core, Imperative Shell analyzer for TypeScript codebases.

FCIS Submarine: Keep the chaos in the waves, keep the math underwater

Keep the chaos in the waves. Keep the math underwater.

Philosophy

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.

What This Tool Measures

FCIS doesn't try to eliminate impure code — you need I/O to build useful software. Instead, it measures:

1. How much of your logic is testable without mocks? → Purity

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

2. When you do have I/O, is it well-organized? → Impurity Quality

Impure functions aren't bad — they're necessary. But there's a difference between:

  • Well-structured: Gathers data, calls pure functions for decisions, executes effects (GATHER → DECIDE → EXECUTE)
  • Tangled: Business logic interleaved with database calls, conditionals mixed with I/O, impossible to test in pieces

FCIS scores impure functions from 0-100 based on structural signals. A score of 70+ means "this I/O code is well-organized."

3. Overall: How confident can you be in this codebase? → Health

Health combines purity and quality into a single number:

  • Pure functions are automatically "healthy" (trivially testable)
  • Impure functions with quality ≥70 are "healthy" (well-structured, integration-testable)
  • Impure functions with quality <70 need attention

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 FCIS Pattern

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 }                           │
│     }                                                       │
│   }                                                         │
└─────────────────────────────────────────────────────────────┘

Installation

npm install -g fcis
# or
pnpm add -g fcis

Quick Start

# 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"

Example Output

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

What Makes a Function Impure?

FCIS detects these I/O patterns:

MarkerExamples
await-expressionawait fetch(), await db.query()
database-calldb.user.findFirst(), prisma.post.create()
network-fetchfetch(url)
network-httpaxios.get()
fs-callfs.readFile(), fs.writeFile()
env-accessprocess.env.NODE_ENV
console-logconsole.log(), console.error()
logginglogger.info()
telemetrytrackEvent(), analytics.track()
queue-enqueuequeue.enqueue(), queue.add()
event-emitemitter.emit()

Note: async alone does NOT make a function impure — only actual I/O operations count.

What Makes Impure Code "High Quality"?

FCIS rewards structural patterns that make impure code easier to understand and test:

SignalWhy It's Good
Calls .pure.ts importsExplicitly 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 complexitySimple orchestration
Calls is*/has*/should*Uses pure predicates

And penalizes patterns that make code hard to reason about:

SignalWhy It's Bad
I/O interleaved throughoutCan't separate "what" from "how"
High cyclomatic complexityToo much logic mixed with I/O
Multiple I/O typesToo many responsibilities
No pure function callsAll logic is inline and untestable
Very long functionGod function, needs decomposition

Compositional Scoring

Inline callbacks (passed to map, filter, forEach, etc.) are absorbed into their parent function's score. This means:

  • A 301-line function with 6 callbacks counts as 1 function, not 7
  • If a callback is impure, the parent is considered impure
  • Quality scores blend parent and children by line count

This prevents gaming the metrics with lots of small callbacks while leaving a tangled parent function.

CLI Reference

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

Exit Codes

CodeMeaning
0Success, all thresholds passed
1Below threshold
2Configuration error
3Analysis error

CI Integration

GitHub Actions

- name: FCIS Analysis
  run: fcis tsconfig.json --min-health 70 --format summary

Pre-commit Hook

{
  "lint-staged": {
    "*.ts": ["fcis tsconfig.json --files"]
  }
}

Refactoring Example

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.

Limitations

  • Analyzes .ts files only (.tsx support planned)
  • Pattern matching is heuristic — may miss custom I/O patterns
  • Does not trace transitive purity (a function calling another function)
  • Quality weights are opinionated and tuned for specific patterns

Further Reading

  • TECHNICAL.md — Implementation details, scoring weights, extension points
  • Gary Bernhardt's "Boundaries" talk — Original FCIS concept
  • Mark Seemann's "Impureim Sandwich" — Similar pattern

License

MIT

Keywords

functional-core

FAQs

Package last updated on 14 Feb 2026

Did you know?

Socket

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.

Install

Related posts