npm package: @fn-code-blocks/core
Description: The SDK, CLI, build tools, and runtime servers for **Fn-Code-Blocks** — a functional programming framework for building API services with typed, language-agnostic function composition.
Latest version: 0.1.5

# @fn-code-blocks/core

The SDK, CLI, build tools, and runtime servers for **Fn-Code-Blocks** — a functional programming framework for building API services with typed, language-agnostic function composition.

Write functions in TypeScript and Python. Compose them into API routes. The language a function is written in is an implementation detail.

```ts
// A route composing functions — languages are invisible
const user      = await fn.fetch_user({ id: 123 });
const inventory = await fn.check_inventory({ sku: "A1" });

const [pricing, risk] = await Promise.all([
  fn.fetch_pricing({ sku: "A1" }),
  fn.assess_risk({ user_id: 123, amount: 500 }),
]);
```

## Quick start

```bash
npx create-fn-code-blocks my-project
code my-project
# Click "Reopen in Container" when VS Code prompts
npm run dev
# Visit http://localhost:3000/hello
```

## Prerequisites

Your local machine needs only:

- **Docker Desktop** — runs the development environment
- **VS Code** — editor
- **VS Code extension: Dev Containers** (ID: `ms-vscode-remote.remote-containers`)
- **Node.js** — only for the initial `npx create-fn-code-blocks` scaffold command

After scaffolding, Node.js is no longer needed locally. Everything runs inside the Dev Container with exact version parity to production (Node 24, Python 3.12).

## Three concepts

| Concept | What it is | Where it lives |
|---|---|---|
| **Function** | `(input, ctx) → output`. Typed, testable, language-agnostic. | `src/functions/` |
| **Route** | Composes functions into a workflow endpoint. | `src/routes/` |
| **Config** | Global env, logging transports, project settings. | `fn-code-blocks.config.ts` |

## Architecture

Three services run in Docker. The framework manages all of them.

```
Client → Orchestrator :3000 → TS Runtime :3001
                             → Python Runtime :3002
```

- **Client → Orchestrator:** Standard HTTP REST
- **Orchestrator → Runtimes:** `POST /invoke/:fnName` (internal HTTP)
- **Runtimes → Log Collector:** Persistent WebSocket log streams

## Writing functions

Each function lives in its own folder under `src/functions/`. The language is determined by the entry point file.

### TypeScript

```ts
// src/functions/fetch_user/index.ts
import type { Context } from "@fn-code-blocks/core/context";

export interface Input {
  id: number;
}

export interface Output {
  name: string;
  email: string;
}

export default async function fetch_user(input: Input, ctx: Context): Promise<Output> {
  ctx.trace.info("Fetching user", { id: input.id });
  // ... your logic
  return { name: "Alice", email: "alice@example.com" };
}
```

### Python

```python
# src/functions/analyze_data/index.py
from typing import TypedDict
from fn_code_blocks import Context

class Input(TypedDict):
    dataset: str

class Output(TypedDict):
    result: str
    score: float

def main(input: Input, ctx: Context) -> Output:
    ctx.trace.info("Analyzing dataset", {"dataset": input["dataset"]})
    # ... your logic
    return {"result": "positive", "score": 0.95}
```

> Also create an empty `__init__.py` in the function folder.

### Rules

- One entry point per folder: `index.ts` OR `index.py`
- Must export `Input` and `Output` types
- Folder names use `snake_case`
- Must include trace logging and tests

## Writing routes

Routes compose functions into workflow endpoints. A route handler receives exactly five things: `input`, `params`, `fn`, `ctx`, `respond`.

```ts
// src/routes/orders.ts
import { defineRoute } from "@fn-code-blocks/core";
import { Hono } from "hono";

const orders = new Hono();

orders.post("/", defineRoute(async ({ input, fn, ctx, respond }) => {
  ctx.trace.info("Processing order", { item: input.sku });

  const inventory = await fn.check_inventory({ sku: input.sku });

  if (!inventory.available) {
    return respond.error(400, "Out of stock");
  }

  const order = await fn.create_order({
    sku: input.sku,
    quantity: input.quantity,
  });

  return respond.json(201, order);
}));

export default orders;
```

Routes auto-mount by filename: `orders.ts` → `/orders`.

**The rule:** If it can't be expressed with `fn.*`, `if/else`, `await`, and `respond.*`, it belongs in a function.

## Configuration

All project-specific decisions live in `fn-code-blocks.config.ts`:

```ts
import { defineConfig, ConsoleTransport } from "@fn-code-blocks/core";

export default defineConfig({
  // Orchestrator port (default: 3000)
  port: 3000,

  // Header to extract trace IDs from incoming requests
  traceIdHeader: "x-trace-id",

  // Global environment — available to every route and function via ctx.env
  env: {
    SERVICE_NAME: "my-project",
    REGION: process.env.REGION ?? "eu-west-1",
    DATABASE_URL: process.env.DATABASE_URL ?? "postgres://localhost:5432/dev",
    API_KEY: process.env.API_KEY ?? "",
  },

  // Keys to redact in trace logs (values replaced with [REDACTED])
  redactKeys: ["API_KEY", "DATABASE_URL"],

  // Logging — wire transports explicitly
  logging(registry) {
    registry.addTransport("log", new ConsoleTransport());
    registry.addTransport("trace", new ConsoleTransport());
  },

  // Runtime-specific configuration (optional)
  runtimes: {
    python: {
      systemDeps: [],    // Extra apt packages for Docker image
      extraDeps: [],     // Extra pip packages
    },
    typescript: {
      extraDeps: [],     // Extra npm packages
    },
  },
});
```

### Configuration options

| Option | Type | Default | Description |
|---|---|---|---|
| `port` | `number` | `3000` | Orchestrator HTTP port |
| `traceIdHeader` | `string` | `"x-trace-id"` | Request header to extract trace IDs from |
| `env` | `Record<string, string>` | `{}` | Global environment passed to all routes and functions via `ctx.env` |
| `redactKeys` | `string[]` | `[]` | Env keys whose values are replaced with `[REDACTED]` in trace logs |
| `logging` | `(registry) => void` | — | Wire logging transports for `"log"` and `"trace"` channels |
| `runtimes` | `object` | — | Per-runtime system deps and extra packages |

## Environment passing

Functions receive `ctx.env` containing the global env from config. You can override per-call:

```ts
// Default — function gets full global env
await fn.fetch_user({ id: 123 });

// Filtered — function only sees what you pass
await fn.call_third_party(
  { query: "..." },
  { env: { SERVICE_NAME: ctx.env.SERVICE_NAME } }
);

// Injected — function sees global env plus extra values
await fn.process_order(
  { order_id: 456 },
  { env: { ...ctx.env, TENANT_ID: "acme-corp" } }
);
```

## Logging

Two channels, each wired independently:

| Channel | Purpose | Use for |
|---|---|---|
| `trace` | Diagnostic visibility | Every decision point, external call, data transformation |
| `log` | Operational output | Business events, metrics, alerts |

```ts
ctx.trace.info("Calling payment API", { amount, currency });
ctx.log.error("Payment declined", { reason });
```

### Custom transports

Implement the `LogTransport` interface:

```ts
import type { LogTransport, LogEntry } from "@fn-code-blocks/core";

const myTransport: LogTransport = {
  write(entry: LogEntry) {
    // Send to your logging service
  },
};

export default defineConfig({
  logging(registry) {
    registry.addTransport("log", myTransport);
    registry.addTransport("trace", myTransport);
  },
});
```

### Pipe to pino

```ts
import pino from "pino";
import { defineConfig, type LogTransport, type LogEntry } from "@fn-code-blocks/core";

const logger = pino({ level: "debug" });

const pinoTransport: LogTransport = {
  write(entry: LogEntry) {
    logger.child({ channel: entry.channel, fn: entry.fn, traceId: entry.traceId })
      [entry.level](entry.args[0] ?? {}, entry.message);
  },
};

export default defineConfig({
  logging(registry) {
    registry.addTransport("log", pinoTransport);
    registry.addTransport("trace", pinoTransport);
  },
});
```

### Silence trace in production

```ts
logging(registry) {
  registry.addTransport("log", new ConsoleTransport());
  if (process.env.NODE_ENV !== "production") {
    registry.addTransport("trace", new ConsoleTransport());
  }
},
```

## Testing

Functions are tested with zero infrastructure. Each language has a `createTestContext` helper that returns a `Context` backed by in-memory transports.

### TypeScript

```ts
import { describe, it, expect } from "vitest";
import { createTestContext } from "@fn-code-blocks/core/testing";
import my_function from "./index";

describe("my_function", () => {
  it("works", async () => {
    const { ctx, traceSink, logSink } = createTestContext("my_function");
    const result = await my_function({ key: "value" }, ctx);

    expect(result.field).toBe("expected");
    expect(traceSink.entries).toHaveLength(2);
    expect(logSink.filter((e) => e.level === "error")).toHaveLength(0);
  });
});
```

### Python

```python
from fn_code_blocks import create_test_context
from .index import main

def test_my_function():
    ctx, trace_sink, log_sink = create_test_context("my_function")
    result = main({"key": "value"}, ctx)

    assert result["field"] == "expected"
    assert len(trace_sink.entries) == 2
```

## CLI commands

All commands run inside the Dev Container terminal:

| Command | What it does |
|---|---|
| `npm run dev` | Start all services with hot reload |
| `npm test` | Run all tests (TS, Python) |
| `npm test -- --fn my_function` | Test a single function (auto-detects language) |
| `npm run test:ts` | TypeScript tests only |
| `npm run test:py` | Python tests only |
| `npm run build` | Rebuild manifests |
| `npm run docker:build` | Build production Docker images |
| `npm run docker:up` | Start production containers |

## Error handling

The `fn` proxy throws typed errors that routes can catch:

```ts
import { FnTimeoutError } from "@fn-code-blocks/core/errors";

defineRoute(async ({ fn, respond }) => {
  try {
    const user = await fn.fetch_user({ id: input.user_id });
    return respond.json(200, user);
  } catch (err) {
    if (err instanceof FnTimeoutError) {
      return respond.error(504, "Function timed out");
    }
    throw err; // re-throw — framework returns 500
  }
});
```

| Error class | When |
|---|---|
| `FnExecutionError` | Function threw an exception |
| `FnTimeoutError` | Invocation exceeded deadline |
| `FnServiceError` | Runtime unreachable |
| `FnNotFoundError` | Function not in manifest |

## Production Docker

```bash
npm run docker         # Generate Dockerfiles + docker-compose.yml
npm run docker:build   # Build production images
npm run docker:up      # Start production containers
```

Each production image contains only the runtime it needs. Language versions match the Dev Container exactly.

## Project structure

```
my-project/
├── fn-code-blocks.config.ts        # Configuration
├── package.json                     # Single dependency: @fn-code-blocks/core
├── .devcontainer/                   # Dev Container (auto-generated)
├── .vscode/launch.json              # Debug configurations
├── src/
│   ├── functions/                   # Your functions
│   │   ├── fetch_user/index.ts
│   │   ├── analyze_data/index.py
│   │   └── shared/                  # Cross-function utilities
│   └── routes/                      # Your routes
│       └── orders.ts
└── .fn-code-blocks/                 # Generated (gitignored)
```

## Exports

```ts
// Main entry
import { defineConfig, defineRoute, ConsoleTransport } from "@fn-code-blocks/core";

// Context type (for function signatures)
import type { Context } from "@fn-code-blocks/core/context";

// Testing
import { createTestContext } from "@fn-code-blocks/core/testing";

// Logger
import { LoggerRegistry, MemoryTransport } from "@fn-code-blocks/core/logger";
import type { LogEntry, LogLevel, LogChannel, LogTransport } from "@fn-code-blocks/core/logger";

// Errors
import { FnExecutionError, FnTimeoutError, FnServiceError, FnNotFoundError } from "@fn-code-blocks/core/errors";
```

## License

MIT