Capability clusters
C9 · Platform ops & delivery
The operational backbone of the eli workspace: scheduled cron jobs, the SSE live-update channel, dev/test escape hatches, ops CLIs, install self-heal, MCP binary packaging, and the test pyramid with its CI gates — everything that keeps the platform running, fresh, and honest.
C9 is eli's operational backbone: the scheduled maintenance jobs (daily AI digest, 30-day trash purge) driven by Vercel Cron, the in-process event bus + SSE channel that pushes vault change events to every open browser tab, the test-only dev endpoints that let Playwright manipulate server state, deterministic seeding, benchmark, and ops CLIs (embedding backfill, KMS key rotation), the native-module (better-sqlite3) ABI self-heal that runs on every install, the esbuild packaging of the standalone eli-mcp stdio binary, and the full test pyramid (colocated unit, PGlite harness, adapter contract, MCP harness, pgTAP RLS, Playwright e2e) plus the two GitHub Actions CI gates (perf, RLS). The chokidar watcher and events bus are shared with C1 — C1 owns the indexing side; this page covers the delivery side: the SSE endpoint, its subscribers, heartbeats, and kill switches.
- Modes: cron routes serve both (local single-vault / saas workspace fan-out); SSE + watcher + dev endpoints + CLIs are local-only; the pgTAP gate validates the saas Postgres layer.
- Key dirs:
app/api/cron/,app/api/events/,app/api/dev/,lib/watch/,scripts/,tests/,.github/workflows/,vercel.json,instrumentation.ts. - Depends on: C1 storage adapters + SQLite index, C4
runDigest, C7 service-role Supabase client. - Feeds: C2 vault UI (live refresh of every
/vpage), C5 (packaged MCP binary), every cluster (test + CI coverage).
Capabilities
| Capability | What it does | Entry points | Modes |
|---|---|---|---|
| Daily digest cron | Generates a daily digest note per vault (local) or per workspace (saas, service-role fan-out). Bearer CRON_SECRET auth; refuses when the secret is unset. ?date=YYYY-MM-DDoverrides the default "yesterday" UTC window; reruns are idempotent (same digest path upserted). Delegates to runDigest()— same code path as the manual "Generate digest now" button. | GET/POST /api/cron/digest, vercel.json cron 0 13 * * * | both (Vercel Cron only fires on the Vercel/SaaS deployment) |
| Trash auto-prune cron | Hard-deletes soft-deleted notes older than 30 days (?days=N override, min 1). Same bearer auth. Uses updatedAt as trashed-at proxy; caps at 5,000 trashed notes per workspace per run; purges via storage.deleteNote(id, {trash:false}), skipping individual failures. | GET/POST /api/cron/trash-prune, vercel.json cron 0 4 * * * | both |
| SSE live-update channel | GET /api/events streams vault ChangeEvents (note:created/updated/deleted + path + at) as text/event-stream. 25 s comment heartbeats; X-Accel-Buffering: no; deliberately no backlog replay on connect. Browser reacts with a 500 ms-debounced router.refresh(). | EventSource('/api/events'), components/vault-events-bridge.tsx (mounted in app/(local)/v/layout.tsx:54), hooks/use-vault-events.ts (public hook, currently unmounted) | local |
| Live-events kill switches | Server env ELI_DISABLE_LIVE_EVENTS=1 skips mounting VaultEventsBridge; client-side window.__ELI_DISABLE_EVENTS__=true disables it per tab. | env var (set by playwright.config.ts:45), window flag (components/vault-events-bridge.tsx:23) | local |
| Dev/test endpoint: rescan | Drops LocalAdapter caches, re-runs scanVault on the active vault, revalidatePath('/v','layout'). Double-guarded: 403 unless NODE_ENV !== 'production' AND ELI_HOME contains .eli-test. | POST /api/dev/rescan | local (test env only) |
| Dev/test endpoint: reset | Closes every cached better-sqlite3 connection (closeAll), resets LocalAdapter + config caches. Exists because Playwright reuses a dev server whose fds point at a deleted DB inode after global-setup reseeds. Same double guard. | POST /api/dev/reset | local (test env only) |
| Dev/test endpoint: seed-note | Zod-validated {path, body, frontmatter?} creates (or hard-delete-then-recreates) a note via getStorage(), revalidates /v, returns {id, path, versionId}. Gives each mutating e2e test a fresh unique note. Same double guard. | POST /api/dev/seed-note | local (test env only) |
| Vault seeder CLI | Generates N markdown notes with frontmatter, 3–8 paragraphs, wikilinks, inline tags, across 7 folders. Deterministic mulberry32 PRNG (default seed 42) → byte-identical reruns, so CI can content-address the fixture. Batches of 200 writes; ~10k notes in ~5 s. | pnpm seed-vault <dir> <count> [seed] (scripts/seed-vault.mjs) | local |
| Config seeder CLI | Writes a ready-made ~/.eli/config.json (mode 0600, dir 0700) with one registered active vault, random 32-byte base64url localToken + mcpToken, openclawCompat: true, git auto-commit on with 30 s debounce. For CI/smoke boots. Not in package.json scripts. | node scripts/seed-config.mjs <vault-path> [name] | local |
| Search benchmark CLI | Boots LocalAdapter on a seeded vault, runs 100 queries, reports p50/p90/p95/max; exits code 3 when p95 exceeds ELI_PERF_P95_BUDGET_MS (default 200), code 2 on usage error. | pnpm bench-search <vault-dir> (scripts/bench-search.mjs, via tsx) | local |
| Embedding backfill CLI | Batch-embeds every note in a vault/workspace outside HTTP — skips the AI rate-limit bucket and needs no user session; key resolution mirrors lib/ai/resolver.ts (AI_GATEWAY_API_KEY env or config aiKey), exits with a hint when neither is set. Idempotent via listNotesMissingEmbedding; --batch N (default 32), --dry-run. | pnpm embed:backfill --vault <path> or --workspace <uuid> (scripts/embed-backfill.mjs, via tsx) | both |
| AI-key KMS rotation CLI | Re-encrypts every ai_keys.ciphertext row from ELI_KMS_KEY_OLD to ELI_KMS_KEY (compromise response / quarterly rotation). One transaction per row, decrypt-verifies new ciphertext before commit, refuses keys shorter than 16 bytes; needs SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY; exits 2 on usage error. | pnpm rotate-ai-keys (scripts/rotate-ai-keys.mjs) | saas |
| Native ABI self-heal | Postinstall probe of better-sqlite3 by actually opening :memory: and running SELECT 1 (bare require() would mask ABI mismatch); on NODE_MODULE_VERSION/ERR_DLOPEN_FAILED/invalid-ELF signatures, detects the package manager from the lockfile and runs the right rebuild. SKIP_POSTINSTALL=1 opts out. Never fails the install. | pnpm postinstall (automatic), pnpm rebuild-native | local |
| MCP stdio binary packaging | esbuild-bundles lib/mcp/stdio.ts → dist/lib/mcp/stdio.js (ESM, node22, path aliases, createRequire banner; server-only/client-only aliased to the test stub). Natives + SDKs stay external. Runs in prepack; published bin is bin/eli-mcp.mjs. | pnpm build:mcp, prepack hook, npx eli-mcp | local |
| Test commands | pnpm test / test:watch (vitest), mcp:test (tests/mcp only), test:e2e / test:e2e:ui (Playwright), plus lint, lint:fix, typecheck, format, format:check. Dev server on :3737, e2e server on :3838. | package.json:23-32 | local |
| CI: perf gate | On push/PR touching local-storage/SQLite paths (plus workflow_dispatch): Node 22 + pnpm 10, seeds a 10k-note vault (cached by hash of the deterministic seed script), runs bench-search with ELI_PERF_P95_BUDGET_MS=450; exit 3 fails the job. | .github/workflows/perf.yml | local |
| CI: RLS conformance gate | On push/PR touching pg migrations or tests/rls: boots pgvector/pgvector:pg17 container, installs pgTAP + pg_prove, applies every lib/db/migrations/pg/*.sql, runs pg_prove tests/rls/*.sql. | .github/workflows/pgtap.yml | saas (validates the Postgres/Supabase RLS layer) |
| Boot-time file watcher | instrumentation.ts register() — Next calls once at server boot. Guards: NEXT_RUNTIME === 'nodejs' and ELI_MODE unset-or-local. Starts the chokidar watcher for the active vault so external edits update the SQLite index without manual rescan; failures non-fatal (lazy start on first storage-touching request). | Next.js instrumentation register() | local |
System view — simplified
This cluster is everything that keeps eli running, fresh, and honest: scheduled jobs that the app itself never has to remember to run, a push channel that makes multiple tabs (and external editors) converge on the same view, and the tooling/test/CI layer that keeps the platform installable and correct. Standalone, it is fully usable against just a checkout: pnpm install self-heals the native SQLite binding, seed-vault + bench-search benchmark search on a synthetic 10k-note vault, the entire test pyramid runs without any external service (PGlite gives it an in-process Postgres), and both cron endpoints can be curl'd with a bearer secret against a local next dev server. The only piece that needs external infrastructure is the automatic schedulingof crons (Vercel) and the pgTAP gate's Postgres container (CI-provisioned).
System view — detailed
| Component | Role | Files |
|---|---|---|
| Cron digest route | Daily digest generator; bearer-secret-authed; fans out per-vault (local) or per-workspace via Supabase service role (saas); delegates to runDigest | app/api/cron/digest/route.ts |
| Cron trash-prune route | Auto-purge of soft-deleted notes older than N days (default 30); same auth + fan-out pattern; hard-deletes via deleteNote({trash:false}) | app/api/cron/trash-prune/route.ts |
| Cron schedule / deployment manifest | Vercel Cron config — digest 13:00 UTC, trash-prune 04:00 UTC. Only deployment-topology file in the repo: SaaS = Next.js on Vercel + Supabase; Local = self-hosted next dev/start -p 3737 with no automatic cron invoker | vercel.json |
| SSE events endpoint | Streams ChangeEvents as text/event-stream; 25 s heartbeat; no backlog replay; unsubscribes + closes on request abort; force-dynamic nodejs runtime | app/api/events/route.ts |
| In-process events bus | Process-global singleton (globalThis.__ELI_EVENT_BUS__) with listener Set + 256-entry recent ring buffer; emit() swallows per-listener errors; recentEvents() exists but the SSE route intentionally ignores it | lib/watch/events-bus.ts |
| Chokidar watcher (bus publisher #1) | Watches vault dir for .md add/change/unlink with ignoreInitial + awaitWriteFinish (60 ms stability, 30 ms poll), ignoring .git/node_modules/.next path segments at any depth; suppresses adapter-write echoes via markEcho() (750 ms TTL); updates incremental SQLite index then emits; process-global registry | lib/watch/chokidar.ts |
| Adapter-write emits (bus publisher #2) | note:created/deleted emitted directly from LocalAdapter write paths (lines 234, 316, 365, 372, 406) so in-app writes propagate without the fs round-trip | lib/storage/local/writes.ts |
| SSE client subscribers | VaultEventsBridge mounted once in the vault layout: EventSource('/api/events') → 500 ms-debounced router.refresh(); useVaultEvents is an undebounced public variant (optional onEventcallback, refreshes per event relying on Next's internal coalescing), currently unmounted | components/vault-events-bridge.tsx, hooks/use-vault-events.ts, app/(local)/v/layout.tsx |
| Boot instrumentation | register() starts the watcher at server boot on the Node runtime in local mode; failures non-fatal | instrumentation.ts |
| Dev/test endpoints | Three POST-only escape hatches for Playwright, each 403 unless non-prod AND ELI_HOME contains .eli-test | app/api/dev/rescan/route.ts, app/api/dev/reset/route.ts, app/api/dev/seed-note/route.ts |
| Seeding tooling | Deterministic N-note fixture generator; ready-made ~/.eli/config.json writer for CI/smoke | scripts/seed-vault.mjs, scripts/seed-config.mjs |
| Ops CLIs | embed:backfill batch-embeds outside HTTP (no rate bucket, no session, idempotent; arg parsing split into embed-backfill-args.mjs for the tests/unit suite); rotate-ai-keys re-encrypts ai_keys.ciphertext old→new KMS key, one verified transaction per row | scripts/embed-backfill.mjs, scripts/embed-backfill-args.mjs, scripts/rotate-ai-keys.mjs |
| Native module handling | Probe-based better-sqlite3 ABI check, lockfile-based PM detection, auto-rebuild, SKIP_POSTINSTALL=1 opt-out; never fails install | scripts/ensure-native.mjs |
| MCP bin builder | esbuild bundle of lib/mcp/stdio.ts → dist/lib/mcp/stdio.js; natives/SDKs external; server-only/client-only stubbed; runs in prepack | scripts/build-mcp-bin.mjs, bin/eli-mcp.mjs |
| Vitest config | Node env; colocated lib/**+app/** .test.ts plus tests/unit, tests/_helpers, tests/adapter, tests/mcp; excludes tests/e2e; coverage scoped to lib/; aliases server-only/client-only → empty stub | vitest.config.ts, tests/_helpers/server-only-stub.ts |
| Playwright config + fixtures | Port 3838, chromium-only, workers=1, serial, retries 2 in CI, trace/video retain-on-failure; webServer = next dev --turbopack with .eli-test fixture home/vault, ELI_DISABLE_LIVE_EVENTS=1, ELI_LEGACY_EDITOR=1; global setup reseeds 3-note fixture (git.auto=false) + best-effort /api/dev/reset | playwright.config.ts, tests/e2e/global.setup.ts, tests/e2e/_setup.ts, tests/e2e/_paths.ts |
| Colocated unit suite | ~75 .test.ts files under lib/ and app/ — storage helpers, MCP tools/auth/rate-limit/stdio, AI digest/chunking/pricing, git, config, clipper, templates, server actions, route handlers | lib/**/*.test.ts, app/**/*.test.ts |
| tests/unit | One suite: parseEmbedBackfillArgs CLI arg parsing for the embed:backfill script | tests/unit/scripts/embed-backfill-args.test.mjs |
| PGlite harness | In-process Postgres with pgvector + auth.uid() stub; runs all 16 pg migrations; ~20 tests: migration idempotency, notes_search ranking, notes_save 40001 conflict, wikilink rewrite, bootstrap trigger, RLS-enabled on 11 tables, policy-name inventory | tests/_helpers/pglite.ts, tests/_helpers/pglite.test.ts |
| Adapter contract suite | Shared StorageAdapter conformance (ADR-0004) with per-adapter caps flags; runs against Local (favorites skipped), Memory, JsonFile | tests/adapter/contract.ts, tests/adapter/{local,memory,json-file}.spec.ts |
| MCP harness | Same registerTools surface over stdio (InMemoryTransport linked pair) and HTTP (createMcpHandler + fake Request); locks the exact 16-tool list and a create→edit→search→diff→restore lifecycle | tests/mcp/harness.ts, tests/mcp/tools.spec.ts |
| pgTAP RLS suite | _setup.sql stubs Supabase env (auth.uid(), roles, act_as helpers); 21 assertions across notes select/write and share-link scoping | tests/rls/_setup.sql, tests/rls/notes_select.sql, tests/rls/notes_write.sql, tests/rls/share_links.sql |
| Playwright e2e (7 specs) | Redirects/lenses, note view + autosave/revert, command palette, daily note + theme, new-note dialog + breadcrumbs, vault picker, zero-console-error net | tests/e2e/01…07-*.spec.ts |
| CI workflows | perf.yml 10k-note search-latency gate; pgtap.yml RLS conformance vs pgvector:pg17 container. No lint/typecheck/vitest workflow exists | .github/workflows/perf.yml, .github/workflows/pgtap.yml |
| ESLint config | Flat config: next + core-web-vitals + typescript-eslint with relaxations; ignores build/test artifacts and scripts/ | eslint.config.mjs |
Behavior contracts locked by tests. The colocated suites are not just unit coverage — several pin exact behavioral contracts: app/api/_e2e.test.ts locks status-code/payload contracts for /v1/openapi.json, /v1/models, /api/import (existing path → updated, new → created), /api/search/hybrid (kind whitelist fts/semantic/hybrid, else 400), and both cron routes (401 on absent/mismatched bearer, digest 200 skipped:['no-active-vault'], prune default 30 days) with storage mocked; app/api/ai/_routes.test.ts locks all five /api/ai/* endpoints' 400 / 503-no-AI-key / 429 contracts, including the shared 60-per-minute 'local' rate bucket from lib/mcp/ratelimit that MCP and the AI routes both consume, and graceful degradation (extract-tasks returns 200 created:[] on non-JSON model output); lib/mcp/tools/p3-tools-04.test.ts pins add_comment version-pinning, daily_note real-calendar-date validation, and apply_template pass-through; server-action suites (app/_actions/{pin,inbox-toast,digest,projects,templates}.test.ts) lock semantic invariants (unpin deletes _pinned rather than setting false, dual-signal leftInbox, frontmatter sibling-field preservation). No suite makes live model calls — an AI-sandbox e2e is noted as future work (app/api/ai/_routes.test.ts:7-12).
Data flow — simplified
The load-bearing runtime flow: any change to a .md file on disk (external editor, agent, git checkout) is picked up by the chokidar watcher, incrementally re-indexed into SQLite, and broadcast over the in-process bus to every connected SSE client. Each browser tab collapses bursts with a 500 ms debounce and calls router.refresh(), so React Server Components re-render against the freshly updated index. In-app saves take a shortcut: lib/storage/local/writes.ts emits directly onto the bus and calls markEcho() so the watcher ignores the resulting filesystem echo.
Data flow — detailed
(a) Vault change → bus → SSE → debounced refresh across tabs (local mode)
(b) Cron digest run across modes
The trash-prune cron (app/api/cron/trash-prune/route.ts) follows the identical shape: same badAuth bearer check, same local/saas fan-out, but per adapter it lists up to 5,000 onlyTrashed notes, skips any with updatedAt newer than the cutoff (?days or 30), and hard-purges the rest one by one, swallowing individual failures.
Works separately / works together
Standalone
With every other cluster switched off, C9 still delivers: a self-healing install (ensure-native.mjs makes pnpm install survive Node ABI bumps with zero user action), deterministic fixture generation (seed-vault.mjs produces byte-identical 10k-note vaults), a search benchmark with a hard exit-code budget, a config bootstrapper for headless CI boots (seed-config.mjs), and a complete test pyramid that runs with no external services — PGlite provides in-process Postgres, the MCP harness uses in-memory transports, and Playwright brings its own fixture vault under .eli-test/. The cron endpoints work against any running instance via curl -H "Authorization: Bearer $CRON_SECRET"; only automaticscheduling needs Vercel. The SSE channel standalone is a generic "this vault changed" push feed any client could consume via EventSource('/api/events').
Composed
- C4 AI platform — the digest cron delegates to
runDigest()inlib/ai/digest-runner.ts, the exact code path behind the UI's manual "Generate digest now" action (app/api/cron/digest/route.ts:33-35docstring); digest logic itself lives in C4, C9 only schedules and auths it. The two callers are distinguishable by route tag: manual runs passapp/_actions/digest.generateNowand in SaaS are session-scoped to the singleeli_ws-cookie workspace via the user's RLS client — cross-workspace service-role fan-out is reserved for the cron. - C1 core data & storage — both cron routes consume the storage abstraction (
LocalAdapter.forVault/SupabaseAdapter.fromClient); the events bus is fedby C1's chokidar watcher (which also driveslib/index/incremental.updatePath) and bylib/storage/local/writes.tsemit calls; the dev endpoints reach into C1 internals (lib/db/client.sqlite.closeAll,LocalAdapter._resetCache,lib/index/scanner.scanVault,lib/config._resetCacheForTests). - C2 authoring UX —
VaultEventsBridgeis mounted inapp/(local)/v/layout.tsx:54, so every RSC page under/vis indirectly refreshed by change events; C2's note list, backlinks, and lenses stay live because of this cluster. - C7 SaaS collaboration — cron fan-out uses
lib/auth/supabase.createSupabaseServiceRoleClient(RLS-bypassing, tracked asSERVICE-ROLEcomments); the pgTAP gate is the CI enforcement of C7's RLS policies. - C5 agent surface —
build-mcp-bin.mjspackages C5'slib/mcp/stdio.tsinto the publishedeli-mcpbin;tests/mcplocks C5's 16-tool surface over both transports. - C3 semantic search —
tests/unit/scripts/embed-backfill-args.test.mjscovers the embed-backfill CLI's arg parsing;perf.ymlgates C1/C3's search latency. - e2e ↔ dev endpoints — Playwright global setup POSTs
/api/dev/reset, spec 02 mints per-test notes via/api/dev/seed-note, spec 06 busts the config cache via/api/dev/rescanafter directly editing.eli-test/home/config.json.
Load-bearing details
- Cron auth is strict equality against
`Bearer ${env.CRON_SECRET}`; an unsetCRON_SECRETmakesbadAuth()return true → permanent 401, never wide-open (app/api/cron/digest/route.ts:46-51,app/api/cron/trash-prune/route.ts:32-36). Thex-vercel-cronheader is explicitly documented as untrusted (digest route comment, lines 15-18).CRON_SECRETis zod-optional (env.ts:98). - Both cron routes alias GET→POST because Vercel Cron sends GET (
digest/route.ts:57-61,trash-prune/route.ts:113-115); both exportdynamic = 'force-dynamic'. - The digest window is exactly 00:00:00Z→24:00:00Z of the target day, so multiple runs the same day are idempotent — they upsert the same digest path (
app/api/cron/digest/route.ts:20-24docstring,53-55,68-76). - Trash prune has no
trashedAtcolumn — it proxies viaupdatedAt("the soft-delete write was the last update on a trashed row, by construction") and caps at 5,000 trashed notes per workspace per run (app/api/cron/trash-prune/route.ts:19-25,43-44). - The SSE route deliberately does not replay the 256-entry backlog on connect — replay caused
router.refresh()flurries that masked navigations and made e2e flaky (app/api/events/route.ts:31-37);recentEvents()(lib/watch/events-bus.ts:43-45) is therefore dead to the SSE path. - Bus and watcher registry survive Next dev HMR via globalThis singletons:
__ELI_EVENT_BUS__(lib/watch/events-bus.ts:13-22) and__ELI_WATCHERS__(lib/watch/chokidar.ts:27-34). The flip side: fan-out is single-process only and cannot span serverless instances — the SaaS answer is Supabase Realtime postgres-changes onnotes(components/realtime/note-watcher.tsx), which shows a refresh-invite toast (never auto-replaces the buffer) and filters self-edits via a window-pinned set of version ids. GET /api/eventsperforms no auth check of its own — any client that can reach the local server can subscribe to the change feed (app/api/events/route.ts:20).- Echo suppression: adapter writes call
markEcho(vault, relPath)with a 750 ms TTL so chokidar doesn't double-index files the adapter itself just wrote (lib/watch/chokidar.ts:36-58). - All three
/api/dev/*endpoints share a double guard: 403 whenNODE_ENV === 'production'or whenELI_HOMEdoesn't contain the substring.eli-test— they can't fire on a real install even in dev mode (rescan/route.ts:17-23,reset/route.ts:19-24,seed-note/route.ts:21-26). /api/dev/resetexists specifically becauseplaywright.config.tsreuseExistingServerkeeps better-sqlite3 fds open on a deleted DB inode after global-setup reseeds;closeAll()+ cache resets force reopening the new file (app/api/dev/reset/route.ts:7-17).seed-vault.mjsis byte-deterministic via mulberry32 (seed 42), whichperf.ymlexploits to cache the 10k-note fixture keyed onhashFiles('scripts/seed-vault.mjs')(.github/workflows/perf.yml:73-84).bench-search.mjsexits code 3 when p95 >ELI_PERF_P95_BUDGET_MS(default 200; CI sets 450 for shared runners) — that exit code is the perf gate's entire failure mechanism (scripts/bench-search.mjs:89-92).ensure-native.mjsprobes better-sqlite3 by instantiating a:memory:DB and runningSELECT 1, because many native packages only dlopen on first use, not on require (scripts/ensure-native.mjs:42-52); ABI-mismatch signatures matched by regex/NODE_MODULE_VERSION|ERR_DLOPEN_FAILED|invalid ELF header|not a valid Win32 application|Could not locate the bindings file/i(line 86); it never fails the install.build-mcp-bin.mjsreusestests/_helpers/server-only-stub.tsas the esbuild alias for bothserver-onlyandclient-onlyin the shipped MCP binary — a test helper is load-bearing in the production bin build (scripts/build-mcp-bin.mjs:83-86).- e2e runs with
ELI_LEGACY_EDITOR=1(forces the legacy<textarea data-testid="note-editor">; BlockNote's contenteditable breaks End-key semantics) andELI_DISABLE_LIVE_EVENTS=1(SSE refresh racesrouter.push) — so e2e covers neither BlockNote editing nor the live SSE bridge (playwright.config.ts:38-54). - e2e is single-worker, serial, chromium-only, on port 3838 against
next dev --turbopack— not a production build (playwright.config.ts:17-34). instrumentation.tsonly starts the watcher whenNEXT_RUNTIME === 'nodejs'andELI_MODEis unset or'local'; SaaS deployments never run chokidar (instrumentation.ts:16-17).tests/adapter/local.spec.tsskips the favorites capability because LocalAdapter favorites persist in~/.eli/config.jsonkeyed by registered vault id, and the contract factory creates an unregistered tmp vault (tests/adapter/local.spec.ts:50-54).- The MCP tool surface is locked by test to exactly 16 tools (
tests/mcp/tools.spec.ts:44-67); the HTTP path callscreateMcpHandlerwith a fake Request — no real server; auth/rate-limit failure modes live separately inlib/mcp/auth.test.ts. vercel.jsoncontains onlythe two cron entries — the sole Vercel-specific deployment config in the repo. SaaS topology is "Next.js on Vercel + Supabase"; local topology is self-hostednext dev/start -p 3737(package.json:15,22) where crons must be invoked manually.package.jsonpinsengines.node >= 22.11.0, andpnpm.onlyBuiltDependenciesallowlists build scripts for exactly better-sqlite3, esbuild, and sharp — pnpm blocks every other dependency's postinstall, soensure-native.mjsonly ever has one native module to heal.
Gaps and partially wired
- No CI for the bulk of the pyramid. Only two workflows exist (
perf.yml,pgtap.yml); nothing in.github/workflows/runs lint, typecheck, the ~75-file vitest suite, or the e2e suite — those gates are local-only conventions. - Stale SSE docstring.
app/api/events/route.ts:17-18still claims "On open, recent backlog is replayed" while the code comment at lines 31-37 (and the code) deliberately does the opposite. The doc lies; the code is right. recentEvents()is dead code on the delivery path (lib/watch/events-bus.ts:43-45) — kept on the bus but intentionally unused by the SSE route.hooks/use-vault-events.tsis a public hook with zero mounts —VaultEventsBridgeinlines its own EventSource to own the 500 ms debounce; the hook itself is undebounced (per-eventrouter.refresh()plus an optionalonEventcallback) and exists for future consumers only.- Missing Supabase adapter-contract run.
tests/_helpers/pglite.ts:17-19referencestests/adapter/supabase.spec.ts, but no such file exists — the shared conformance suite runs only against Local/Memory/JsonFile, so the SaaS adapter is untested at the contract level. CRON_SECRETis optional in the env schema (env.ts:98): a deployment that forgets to set it gets both cron endpoints permanently returning 401 — safe, but silently no-op (digests and trash purge just stop happening).- No cron invoker in local mode.
vercel.jsonschedules only apply on Vercel; a self-hosted local install mustcurlthe cron endpoints itself (or never get digests/pruning). - e2e blind spots by design: BlockNote editing (
ELI_LEGACY_EDITOR=1) and the live SSE bridge (ELI_DISABLE_LIVE_EVENTS=1) are excluded from Playwright coverage; the suite also runs againstnext dev --turbopack, not a production build. - Trash prune approximations: no
trashedAtcolumn (updatedAt proxy can extend a note's trash life if it is touched post-trash), and the 5,000-per-workspace cap means a larger backlog only drains across multiple daily runs. seed-config.mjsis unwired frompackage.jsonscripts — invocable only asnode scripts/seed-config.mjs.- tests/unit is nearly empty — a single CLI-arg-parsing suite (
tests/unit/scripts/embed-backfill-args.test.mjs); everything else is colocated.
See also