Skip to content

Development

Local setup

git clone https://github.com/HSPK/loom.git
cd loom
bun install
bun run dev          # http://localhost:3000 with hot reload

Loom uses bun as its package manager. Don't mix with npm / yarn / pnpm — bun.lock is the source of truth.

Scripts

Command What it does
bun run dev Next.js dev server with HMR
bun run build Builds the CLI bundle + production Next.js build
bun run start Runs the production server (after build)
bun run lint ESLint (baseline 107 problems; only call out new ones)
bun run test Vitest unit + component tests (node and dom projects)
bun run test:coverage Same, enforcing the 90% coverage thresholds
bun run bench Micro-benchmarks for the hot paths (see bench/README.md)
bun run test:browser Playwright functional suite in a real Chromium
bun run bench:web Responsiveness benchmarks — Web Vitals, INP, bundle budgets
bun run bench:web:report Render the last benchmark run as a table
bun run test:e2e Standalone end-to-end scripts against a real spawned server
bun run build:cli Re-bundle bin/loom.tsbin/loom.mjs (also runs in prepare)

Database migrations

Drizzle Kit generates the migrations from lib/server/db/schema.ts:

bunx drizzle-kit generate

This is an interactive prompt — run it inside a TTY or via script -qc '...' /dev/null. Migrations execute automatically on server boot.

Tests

Unit and component tests run on Vitest, split into two projects:

Project Environment Location Covers
node node tests/node/** lib/server/**, lib/cli/**, schemas, gateway, services
dom jsdom tests/dom/** React components, lib/api/**, stores, pages
bun run test              # both projects
bun run test:watch
bun run test:coverage     # enforces the thresholds below
bun run bench             # micro-benchmarks — see bench/README.md
bun run test:e2e          # the standalone e2e scripts (below)

Coverage thresholds are enforced in vitest.config.mts (90% statements / lines / functions, 85% branches) over lib/, components/, app/, and context/. components/ui/** is excluded — those are vendored shadcn primitives, regenerated by their CLI and exercised transitively.

Writing tests

  • tests/setup/node.ts gives every test file its own migrated SQLite database in a temp dir, so suites can seed freely without cross-talk. Migrations run on first import "@/lib/server/db".
  • tests/helpers/db.ts has the fixtures: resetDb(), seedUser, seedAdmin, seedProvider, seedModel, seedConversation, seedMessage, seedLog, seedTool, seedMcpServer, sessionUser. Call resetDb() in beforeEach for anything DB-backed.
  • tests/setup/dom.ts wires up jest-dom matchers, RTL auto-cleanup, and the browser APIs jsdom lacks (matchMedia, ResizeObserver, IntersectionObserver, Radix's pointer-capture methods).
  • server-only is aliased to a no-op so lib/server/** imports cleanly outside a React Server Component.
  • Never hit the network — mock global.fetch. Never spawn real MCP processes — mock node:child_process and the MCP SDK client.

No local Node?

better-sqlite3 is a native addon, so the suite needs Node (Bun cannot dlopen it). If your machine has no Node ≥ 20, use the container wrapper:

./scripts/vitest-docker.sh run --project node
./scripts/vitest-docker.sh run --coverage
./scripts/vitest-docker.sh bench --run

Browser suite and responsiveness benchmarks

e2e/ holds a Playwright suite that runs against a real Chromium and the production build (next start — dev bundles are unminified and lazily compiled, so numbers taken against them are fiction):

bun run build           # required first
bun run test:browser    # functional: auth, routing, no client errors
bun run bench:web       # Core Web Vitals, INP under streaming, byte budgets
bun run bench:web:report

./scripts/e2e-docker.sh              # no local browsers? run it in the container

A fake OpenAI-compatible upstream (e2e/support/fake-upstream.mjs) makes the streaming benchmark repeatable — tokens=N / delay=M in the prompt control token count and cadence. Current numbers live in e2e/perf/BASELINE.md; the budgets in bundle-budget.spec.ts are regression ratchets, not targets.

See e2e/README.md for the non-obvious traps (the Secure session cookie, and the composer not being the first textbox).

End-to-end scripts

The older standalone E2E scripts spawn a real gateway against a temp LOOM_USER_CWD and still run in CI via bun run test:e2e:

node scripts/e2e-mcp-loop.mjs
node scripts/e2e-mcp-check.mjs
node scripts/e2e-multimodal.mjs
node scripts/e2e-messages-pagination.mjs
node scripts/e2e-preferences.mjs
node scripts/e2e-tracing-fixes.mjs
node scripts/e2e-responses-variant.mjs
node scripts/e2e-latency-split.mjs

Add a unit test for logic and an e2e-*.mjs when a feature has non-trivial wire surface worth exercising end to end.

Design principles

Loom is built around "new features should require minimum code churn". The rules:

  • One domain per folder (lib/server/<domain>/)
  • Schemas in lib/schemas/*.ts are the single source of truth — every TS type derives from z.infer, never hand-written interfaces
  • Use factories — defineRoute, defineResource, defineCommand, registerCapability, registerAdapter
  • Add new functionality by adding files + a single registration line, never by editing core files

Concretely:

Adding a... Touches
Wire field zod schema + Drizzle column + 1 serializer line
CRUD endpoint zod schema + defineRoute + service function
Modality one capability file + one Route Handler
Protocol variant one adapter file + one registration line
FE domain one defineResource(...) call
CLI subcommand one file in lib/cli/commands/ + one line in main.ts

Documentation

Docs live under docs/ and are built with mkdocs-material.

pip install -r docs/requirements.txt
mkdocs serve              # preview at http://localhost:8000
mkdocs build --strict     # what CI runs — fails on broken cross-page links

On push to main, the Docs workflow builds the site with --strict and publishes to GitHub Pages at https://hspk.github.io/loom. Pull requests run the build step only, so broken links are caught before merge.

Contributing

Issues and pull requests are welcome. Please:

  1. Read .github/copilot-instructions.md for the full design contract.
  2. Keep each PR atomic — one feature or one fix.
  3. Verify bun run build, bun run lint, and bun run test (with coverage) pass.
  4. Don't widen the lint baseline of 107 problems.

Repository layout

app/                       # Next.js App Router (UI + Route Handlers)
  (dashboard)/             # Authenticated pages
  (auth)/login/            # Public login
  api/                     # Route Handlers (gateway + admin CRUD)
bin/loom.ts                # CLI entry shim (esbuild bundled to bin/loom.mjs)
components/                # React components (shadcn/ui composed)
context/                   # React context providers (auth, theme)
docs/                      # This documentation (mkdocs)
drizzle/                   # SQL migrations (generated)
lib/
  api/                     # Frontend HTTP client per domain
  cli/                     # CLI command tree
  schemas/                 # Zod schemas (wire types)
  server/                  # Server-only modules
    adapters/              # Per-protocol adapters
    capabilities/          # Per-modality handlers
    db/                    # Drizzle schema + SQLite setup
    gateway/               # OpenAI-compatible forwarder
    mcp/                   # MCP runtime
    <domain>/              # One folder per domain
  stores/                  # Zustand stores (client state)
public/                    # Static assets
scripts/                   # Build CLI + E2E tests