npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fcis

v0.2.1

Published

Functional Core, Imperative Shell analyzer for TypeScript codebases

Downloads

308

Readme

FCIS Analyzer

Functional Core, Imperative Shell analyzer for TypeScript codebases.

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:

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

What Makes Impure Code "High Quality"?

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 |

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

| Code | Meaning | |------|---------| | 0 | Success, all thresholds passed | | 1 | Below threshold | | 2 | Configuration error | | 3 | Analysis 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

License

MIT