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.ts → bin/loom.mjs (also runs in prepare) |
Database migrations¶
Drizzle Kit generates the migrations from lib/server/db/schema.ts:
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.tsgives every test file its own migrated SQLite database in a temp dir, so suites can seed freely without cross-talk. Migrations run on firstimport "@/lib/server/db".tests/helpers/db.tshas the fixtures:resetDb(),seedUser,seedAdmin,seedProvider,seedModel,seedConversation,seedMessage,seedLog,seedTool,seedMcpServer,sessionUser. CallresetDb()inbeforeEachfor anything DB-backed.tests/setup/dom.tswires up jest-dom matchers, RTL auto-cleanup, and the browser APIs jsdom lacks (matchMedia,ResizeObserver,IntersectionObserver, Radix's pointer-capture methods).server-onlyis aliased to a no-op solib/server/**imports cleanly outside a React Server Component.- Never hit the network — mock
global.fetch. Never spawn real MCP processes — mocknode:child_processand 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/*.tsare the single source of truth — every TS type derives fromz.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:
- Read
.github/copilot-instructions.mdfor the full design contract. - Keep each PR atomic — one feature or one fix.
- Verify
bun run build,bun run lint, andbun run test(with coverage) pass. - 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