Parcourir la source

refactor(db)!: node:sqlite is the sole backend; drop better-sqlite3 + wasm

Now that distribution will bundle a Node 24 runtime, node:sqlite (real SQLite
with WAL + FTS5) is always available. Collapse the three-backend adapter to
node:sqlite only and remove the machinery the other two needed:

- Remove better-sqlite3 (optionalDependency) and node-sqlite3-wasm (dependency).
- Remove WasmDatabaseAdapter, the named->positional param translation, the
  SQLITE_BUSY read-retry, the wasm fallback banner, the backend env override,
  and the native/node-sqlite/wasm selection chain.
- createDatabase now opens node:sqlite directly, with a clear error pointing at
  the bundled release / Node 22.5+ when the module is absent.
- NodeSqliteAdapter.close() is idempotent and pragma() supports { simple }, to
  match the better-sqlite3 behavior callers relied on.
- status (CLI + MCP) reports the single node:sqlite backend; journal-mode
  diagnostics and the getCodeGraph single-connection fix are retained.
- Tests repointed off better-sqlite3 onto node:sqlite.

Net -1044 lines. Running from source now requires Node 22.5+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry il y a 1 mois
Parent
commit
afb62d1be4

+ 10 - 143
__tests__/concurrent-locking.test.ts

@@ -1,14 +1,12 @@
 /**
  * Issue #238 — "database is locked" on concurrent MCP tool calls.
  *
- * The reporter's suggested fix (enable WAL / busy_timeout) was already in place,
- * so these tests pin the ACTUAL fixes:
+ * With node:sqlite (real WAL) as the backend, the fixes that remain relevant:
  *  1. busy_timeout is a bounded few-second wait (not a 2-minute hang) and WAL is
- *     active on the native backend — the property concurrent reads rely on.
+ *     active — so a reader never blocks on a concurrent writer.
  *  2. The MCP ToolHandler reuses the default instance when a tool passes a
  *     projectPath pointing at the default project, instead of opening a SECOND
- *     connection to the same DB (the lock amplifier).
- *  3. The wasm backend (which can't do WAL) retries reads on SQLITE_BUSY.
+ *     connection to the same DB.
  */
 
 import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
@@ -18,14 +16,8 @@ import * as os from 'os';
 import CodeGraph from '../src';
 import { ToolHandler } from '../src/mcp/tools';
 import { DatabaseConnection } from '../src/db';
-import { withBusyRetry, isDatabaseLockedError } from '../src/db/sqlite-adapter';
 
-// The bundled wasm fallback backend — the one the actual reporters run on and the
-// only one without WAL. Loaded the same way the adapter loads it (CJS require).
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const { Database: WasmDatabase } = require('node-sqlite3-wasm');
-
-/** Normalize a PRAGMA read across backends (array | object | scalar) to a value. */
+/** Normalize a PRAGMA read across return shapes (array | object | scalar). */
 function pragmaValue(raw: unknown, key: string): unknown {
   const row = Array.isArray(raw) ? raw[0] : raw;
   if (row !== null && typeof row === 'object') return (row as Record<string, unknown>)[key];
@@ -52,21 +44,17 @@ describe('issue #238 — connection PRAGMAs (#1)', () => {
     expect(ms).toBeLessThanOrEqual(30000); // far below the old 120000
   });
 
-  it('runs WAL on native (the mode that lets readers proceed during a write)', () => {
+  it('runs in WAL mode — the mode that lets readers proceed during a write', () => {
     const mode = String(pragmaValue(conn.getDb().pragma('journal_mode'), 'journal_mode')).toLowerCase();
-    // Native supports WAL; the wasm fallback is forced to DELETE (no WAL).
-    expect(mode).toBe(conn.getBackend() === 'wasm' ? 'delete' : 'wal');
+    expect(mode).toBe('wal');
   });
 
   it('getJournalMode() surfaces the effective mode for status triage', () => {
-    // The conclusive data point for triaging "database is locked": 'wal' means
-    // readers can't be blocked by a writer; anything else means they can.
-    const mode = conn.getJournalMode();
-    expect(mode).toBe(conn.getBackend() === 'wasm' ? 'delete' : 'wal');
+    expect(conn.getJournalMode()).toBe('wal');
   });
 });
 
-describe('issue #238 — native WAL lets a reader proceed during a writer', () => {
+describe('issue #238 — WAL lets a reader proceed during a writer', () => {
   let dir: string;
 
   beforeAll(() => {
@@ -80,9 +68,8 @@ describe('issue #238 — native WAL lets a reader proceed during a writer', () =
   it('a read on a 2nd connection succeeds while a writer holds the lock', () => {
     const dbPath = path.join(dir, 'codegraph.db');
     const writer = DatabaseConnection.initialize(dbPath);
-    // This property only holds under WAL; on the wasm fallback (DELETE) an
-    // EXCLUSIVE writer correctly blocks readers, so the assertion is native-only.
-    if (writer.getBackend() !== 'native') {
+    // The property only holds under WAL; skip if the filesystem couldn't enable it.
+    if (writer.getJournalMode() !== 'wal') {
       writer.close();
       return;
     }
@@ -163,123 +150,3 @@ describe('issue #238 — ToolHandler reuses the default instance (#2)', () => {
     }
   });
 });
-
-describe('issue #238 — withBusyRetry / isDatabaseLockedError (#3)', () => {
-  const locked = () => Object.assign(new Error('database is locked'), { code: 'SQLITE_BUSY' });
-
-  it('retries a locked read and then succeeds', () => {
-    const sleeps: number[] = [];
-    let calls = 0;
-    const result = withBusyRetry(
-      () => {
-        calls++;
-        if (calls < 3) throw locked();
-        return 'ok';
-      },
-      { attempts: 5, backoffMs: [10, 20], sleep: (ms) => sleeps.push(ms) }
-    );
-    expect(result).toBe('ok');
-    expect(calls).toBe(3);
-    expect(sleeps).toEqual([10, 20]); // backed off between the two retries
-  });
-
-  it('gives up after the attempt budget and rethrows the lock error', () => {
-    let calls = 0;
-    expect(() =>
-      withBusyRetry(
-        () => { calls++; throw locked(); },
-        { attempts: 3, backoffMs: [0], sleep: () => {} }
-      )
-    ).toThrow(/database is locked/i);
-    expect(calls).toBe(3);
-  });
-
-  it('does not retry a non-lock error', () => {
-    let calls = 0;
-    expect(() =>
-      withBusyRetry(
-        () => { calls++; throw new Error('no such table: nodes'); },
-        { attempts: 5, sleep: () => {} }
-      )
-    ).toThrow(/no such table/);
-    expect(calls).toBe(1);
-  });
-
-  it('isDatabaseLockedError recognizes lock errors across backends', () => {
-    expect(isDatabaseLockedError(Object.assign(new Error('x'), { code: 'SQLITE_BUSY' }))).toBe(true);
-    expect(isDatabaseLockedError(Object.assign(new Error('x'), { code: 'SQLITE_LOCKED' }))).toBe(true);
-    expect(isDatabaseLockedError(new Error('database is locked'))).toBe(true);
-    expect(isDatabaseLockedError(new Error('database is busy'))).toBe(true);
-    expect(isDatabaseLockedError(new Error('SQLITE_BUSY: database is locked'))).toBe(true);
-    expect(isDatabaseLockedError(new Error('no such column'))).toBe(false);
-    expect(isDatabaseLockedError(null)).toBe(false);
-  });
-});
-
-describe('issue #238 — wasm backend rides out a REAL lock via retry (#3, end-to-end)', () => {
-  // Exercises an actual node-sqlite3-wasm connection against a real held write
-  // lock — the path the reporters are on. Native (WAL) never reaches this code,
-  // so it cannot be covered by the native CI backend; we drive wasm directly.
-  let dir: string;
-  let dbPath: string;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  let writer: any;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  let reader: any;
-
-  beforeAll(() => {
-    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg238-wasm-'));
-    dbPath = path.join(dir, 'codegraph.db');
-    const seed = new WasmDatabase(dbPath);
-    seed.exec('PRAGMA journal_mode = DELETE'); // what the adapter forces for wasm (no WAL)
-    seed.exec('CREATE TABLE nodes(id INTEGER PRIMARY KEY, name TEXT)');
-    seed.exec("INSERT INTO nodes(name) VALUES ('seed')");
-    seed.close();
-  });
-
-  afterAll(() => {
-    fs.rmSync(dir, { recursive: true, force: true });
-  });
-
-  beforeEach(() => {
-    writer = new WasmDatabase(dbPath);
-    writer.exec('BEGIN EXCLUSIVE');                       // real, held write lock
-    writer.exec("INSERT INTO nodes(name) VALUES ('writer')");
-    reader = new WasmDatabase(dbPath);                    // separate connection, no busy wait
-  });
-
-  afterEach(() => {
-    try { reader.close(); } catch { /* ignore */ }
-    try { writer.close(); } catch { /* ignore */ }
-  });
-
-  it('precondition: a wasm read hits a real lock while a writer holds EXCLUSIVE', () => {
-    expect(() => reader.get('SELECT COUNT(*) AS c FROM nodes')).toThrow(/lock|busy/i);
-  });
-
-  it('withBusyRetry rides out a writer that clears mid-wait → the read succeeds', () => {
-    let released = false;
-    // The injected sleep stands in for the gap during which a cross-process
-    // writer finishes; we release the held lock on the first retry. This proves
-    // the wasm read path recovers instead of surfacing "database is locked".
-    const row = withBusyRetry(
-      () => reader.get('SELECT COUNT(*) AS c FROM nodes') as { c: number },
-      {
-        attempts: 4,
-        backoffMs: [1],
-        sleep: () => { if (!released) { writer.exec('COMMIT'); released = true; } },
-      }
-    );
-    expect(released).toBe(true);  // the first attempt really did hit the lock and retry
-    expect(row.c).toBe(2);        // seed + writer, visible once the writer committed
-  });
-
-  it('exhausting retries against a writer that never clears still throws a lock error', () => {
-    expect(() =>
-      withBusyRetry(
-        () => reader.get('SELECT COUNT(*) AS c FROM nodes'),
-        { attempts: 3, backoffMs: [1], sleep: () => { /* writer never releases */ } }
-      )
-    ).toThrow(/lock|busy/i);
-  });
-});

+ 4 - 9
__tests__/node-sqlite-backend.test.ts

@@ -1,10 +1,9 @@
 /**
  * node:sqlite backend (issue #238 follow-up).
  *
- * Proves Node's built-in node:sqlite works as a real CodeGraph backend — the
- * fallback that replaces the no-WAL wasm path when better-sqlite3 can't load.
- * Forces it via CODEGRAPH_SQLITE_BACKEND and drives a real index + queries, so
- * WAL, FTS5 search, and @named-param writes are all exercised end-to-end.
+ * node:sqlite (Node's built-in real SQLite) is now the sole backend. This drives
+ * a real index + queries through it, so WAL, FTS5 search, and @named-param
+ * writes are all exercised end-to-end.
  *
  * Skipped on Node < 22.5 where node:sqlite doesn't exist.
  */
@@ -27,10 +26,8 @@ try {
 describe.skipIf(!nodeSqliteAvailable)('node:sqlite backend — real index + queries', () => {
   let dir: string;
   let cg: CodeGraph;
-  const prevEnv = process.env.CODEGRAPH_SQLITE_BACKEND;
 
   beforeAll(async () => {
-    process.env.CODEGRAPH_SQLITE_BACKEND = 'node-sqlite'; // force the backend under test
     dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-nodesqlite-'));
     fs.writeFileSync(path.join(dir, 'a.ts'), 'export function helper(): number { return 1; }\n');
     fs.writeFileSync(
@@ -42,12 +39,10 @@ describe.skipIf(!nodeSqliteAvailable)('node:sqlite backend — real index + quer
 
   afterAll(() => {
     cg?.close();
-    if (prevEnv === undefined) delete process.env.CODEGRAPH_SQLITE_BACKEND;
-    else process.env.CODEGRAPH_SQLITE_BACKEND = prevEnv;
     fs.rmSync(dir, { recursive: true, force: true });
   });
 
-  it('actually selected the node:sqlite backend (env override took effect)', () => {
+  it('uses the node:sqlite backend', () => {
     expect(cg.getBackend()).toBe('node-sqlite');
   });
 

+ 3 - 3
__tests__/pr19-improvements.test.ts

@@ -45,11 +45,11 @@ function cleanupTempDir(dir: string): void {
   }
 }
 
-// Check if better-sqlite3 native bindings are available
+// Check if the node:sqlite backend is available (Node >= 22.5)
 function hasSqliteBindings(): boolean {
   try {
-    const Database = require('better-sqlite3');
-    const db = new Database(':memory:');
+    const { DatabaseSync } = require('node:sqlite');
+    const db = new DatabaseSync(':memory:');
     db.close();
     return true;
   } catch {

+ 9 - 51
__tests__/sqlite-backend.test.ts

@@ -1,59 +1,18 @@
 /**
- * SQLite backend visibility tests
+ * SQLite backend reporting.
  *
- * Pins the WASM-fallback banner content + the per-instance backend
- * tracking. Closes the visibility gap behind issue #138.
+ * node:sqlite (Node's built-in real SQLite) is the sole backend. Pin that
+ * DatabaseConnection / CodeGraph report it and come up in WAL.
  */
 
 import { describe, it, expect, beforeEach, afterEach } from 'vitest';
 import * as fs from 'fs';
 import * as path from 'path';
 import * as os from 'os';
-import {
-  buildWasmFallbackBanner,
-  WASM_FALLBACK_FIX_RECIPE,
-} from '../src/db/sqlite-adapter';
 import { DatabaseConnection } from '../src/db';
 import { CodeGraph } from '../src';
 
-describe('buildWasmFallbackBanner — fix-recipe content', () => {
-  it('includes the macOS / Linux / cross-platform fix commands', () => {
-    const banner = buildWasmFallbackBanner();
-    expect(banner).toContain('WASM SQLite fallback active');
-    expect(banner).toContain('5-10x slower');
-    expect(banner).toContain('xcode-select --install');
-    expect(banner).toContain('apt install build-essential');
-    expect(banner).toContain('npm rebuild better-sqlite3');
-    expect(banner).toContain('npm install better-sqlite3 --save');
-    expect(banner).toContain('codegraph status');
-  });
-
-  it('appends the native load error when one is provided', () => {
-    const banner = buildWasmFallbackBanner(
-      "Cannot find module 'better-sqlite3'"
-    );
-    expect(banner).toContain(
-      "Native load error: Cannot find module 'better-sqlite3'"
-    );
-  });
-
-  it('omits the load-error block when no error is supplied', () => {
-    const banner = buildWasmFallbackBanner();
-    expect(banner).not.toContain('Native load error:');
-  });
-});
-
-describe('WASM_FALLBACK_FIX_RECIPE — single source of truth', () => {
-  it('mentions the three recovery commands', () => {
-    expect(WASM_FALLBACK_FIX_RECIPE).toContain('xcode-select --install');
-    expect(WASM_FALLBACK_FIX_RECIPE).toContain('npm rebuild better-sqlite3');
-    expect(WASM_FALLBACK_FIX_RECIPE).toContain(
-      'npm install better-sqlite3 --save'
-    );
-  });
-});
-
-describe('DatabaseConnection — per-instance backend reporting', () => {
+describe('DatabaseConnection — backend reporting', () => {
   let dir: string;
 
   beforeEach(() => {
@@ -66,11 +25,10 @@ describe('DatabaseConnection — per-instance backend reporting', () => {
     }
   });
 
-  it('reports a concrete backend (native or wasm) for an initialized DB', () => {
-    const dbPath = path.join(dir, 'test.db');
-    const conn = DatabaseConnection.initialize(dbPath);
-    const backend = conn.getBackend();
-    expect(['native', 'node-sqlite', 'wasm']).toContain(backend);
+  it('reports the node-sqlite backend in WAL for an initialized DB', () => {
+    const conn = DatabaseConnection.initialize(path.join(dir, 'test.db'));
+    expect(conn.getBackend()).toBe('node-sqlite');
+    expect(conn.getJournalMode()).toBe('wal');
     conn.close();
   });
 
@@ -78,7 +36,7 @@ describe('DatabaseConnection — per-instance backend reporting', () => {
     fs.writeFileSync(path.join(dir, 'x.ts'), `export function x(): void {}\n`);
     const cg = await CodeGraph.init(dir, { index: true });
     try {
-      expect(['native', 'wasm']).toContain(cg.getBackend());
+      expect(cg.getBackend()).toBe('node-sqlite');
     } finally {
       cg.destroy();
     }

+ 2 - 2
__tests__/symbol-lookup.test.ts

@@ -25,8 +25,8 @@ beforeAll(async () => {
 
 function hasSqliteBindings(): boolean {
   try {
-    const Database = require('better-sqlite3');
-    const db = new Database(':memory:');
+    const { DatabaseSync } = require('node:sqlite');
+    const db = new DatabaseSync(':memory:');
     db.close();
     return true;
   } catch {

+ 0 - 499
package-lock.json

@@ -14,7 +14,6 @@
         "fast-string-width": "^3.0.2",
         "fast-wrap-ansi": "^0.2.0",
         "jsonc-parser": "^3.3.1",
-        "node-sqlite3-wasm": "^0.8.30",
         "picomatch": "^4.0.3",
         "sisteransi": "^1.0.5",
         "tree-sitter-wasms": "^0.1.11",
@@ -32,9 +31,6 @@
       },
       "engines": {
         "node": ">=20.0.0 <25.0.0"
-      },
-      "optionalDependencies": {
-        "better-sqlite3": "^12.4.1"
       }
     },
     "node_modules/@clack/core": {
@@ -970,89 +966,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/base64-js": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
-      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/better-sqlite3": {
-      "version": "12.10.0",
-      "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
-      "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
-      "hasInstallScript": true,
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "bindings": "^1.5.0",
-        "prebuild-install": "^7.1.1"
-      },
-      "engines": {
-        "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
-      }
-    },
-    "node_modules/bindings": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
-      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "file-uri-to-path": "1.0.0"
-      }
-    },
-    "node_modules/bl": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
-      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "buffer": "^5.5.0",
-        "inherits": "^2.0.4",
-        "readable-stream": "^3.4.0"
-      }
-    },
-    "node_modules/buffer": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
-      }
-    },
     "node_modules/cac": {
       "version": "6.7.14",
       "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -1090,13 +1003,6 @@
         "node": ">= 16"
       }
     },
-    "node_modules/chownr": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
-      "license": "ISC",
-      "optional": true
-    },
     "node_modules/commander": {
       "version": "14.0.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@@ -1124,22 +1030,6 @@
         }
       }
     },
-    "node_modules/decompress-response": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
-      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "mimic-response": "^3.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/deep-eql": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -1150,36 +1040,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/deep-extend": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
-      "license": "MIT",
-      "optional": true,
-      "engines": {
-        "node": ">=4.0.0"
-      }
-    },
-    "node_modules/detect-libc": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
-      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
-      "license": "Apache-2.0",
-      "optional": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/end-of-stream": {
-      "version": "1.4.5",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
-      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "once": "^1.4.0"
-      }
-    },
     "node_modules/es-module-lexer": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -1236,16 +1096,6 @@
         "@types/estree": "^1.0.0"
       }
     },
-    "node_modules/expand-template": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
-      "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
-      "license": "(MIT OR WTFPL)",
-      "optional": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/expect-type": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -1280,20 +1130,6 @@
         "fast-string-width": "^3.0.2"
       }
     },
-    "node_modules/file-uri-to-path": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
-      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/fs-constants": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
-      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
-      "license": "MIT",
-      "optional": true
-    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1309,48 +1145,6 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
-    "node_modules/github-from-package": {
-      "version": "0.0.0",
-      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
-      "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "BSD-3-Clause",
-      "optional": true
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "license": "ISC",
-      "optional": true
-    },
-    "node_modules/ini": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-      "license": "ISC",
-      "optional": true
-    },
     "node_modules/jsonc-parser": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
@@ -1374,36 +1168,6 @@
         "@jridgewell/sourcemap-codec": "^1.5.5"
       }
     },
-    "node_modules/mimic-response": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
-      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
-      "license": "MIT",
-      "optional": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/minimist": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-      "license": "MIT",
-      "optional": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/mkdirp-classic": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
-      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
-      "license": "MIT",
-      "optional": true
-    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1430,42 +1194,6 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
-    "node_modules/napi-build-utils": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
-      "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/node-abi": {
-      "version": "3.87.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
-      "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "semver": "^7.3.5"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/node-sqlite3-wasm": {
-      "version": "0.8.53",
-      "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.53.tgz",
-      "integrity": "sha512-HPuGOPj3L+h3WSf0XikIXTDpsRxlVmzBC3RMgqi3yDg9CEbm/4Hw3rrDodeITqITjm07X4atWLlDMMI8KERMiQ==",
-      "license": "MIT"
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "license": "ISC",
-      "optional": true,
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
     "node_modules/pathe": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -1531,75 +1259,6 @@
         "node": "^10 || ^12 || >=14"
       }
     },
-    "node_modules/prebuild-install": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
-      "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "detect-libc": "^2.0.0",
-        "expand-template": "^2.0.3",
-        "github-from-package": "0.0.0",
-        "minimist": "^1.2.3",
-        "mkdirp-classic": "^0.5.3",
-        "napi-build-utils": "^2.0.0",
-        "node-abi": "^3.3.0",
-        "pump": "^3.0.0",
-        "rc": "^1.2.7",
-        "simple-get": "^4.0.0",
-        "tar-fs": "^2.0.0",
-        "tunnel-agent": "^0.6.0"
-      },
-      "bin": {
-        "prebuild-install": "bin.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/pump": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
-      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "node_modules/rc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
-      "optional": true,
-      "dependencies": {
-        "deep-extend": "^0.6.0",
-        "ini": "~1.3.0",
-        "minimist": "^1.2.0",
-        "strip-json-comments": "~2.0.1"
-      },
-      "bin": {
-        "rc": "cli.js"
-      }
-    },
-    "node_modules/readable-stream": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
-      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/rollup": {
       "version": "4.57.1",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
@@ -1645,40 +1304,6 @@
         "fsevents": "~2.3.2"
       }
     },
-    "node_modules/safe-buffer": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/semver": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
-      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
-      "license": "ISC",
-      "optional": true,
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/siginfo": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1686,53 +1311,6 @@
       "dev": true,
       "license": "ISC"
     },
-    "node_modules/simple-concat": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
-      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "optional": true
-    },
-    "node_modules/simple-get": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
-      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "decompress-response": "^6.0.0",
-        "once": "^1.3.1",
-        "simple-concat": "^1.0.0"
-      }
-    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -1763,56 +1341,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/string_decoder": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "safe-buffer": "~5.2.0"
-      }
-    },
-    "node_modules/strip-json-comments": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
-      "license": "MIT",
-      "optional": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/tar-fs": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
-      "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "chownr": "^1.1.1",
-        "mkdirp-classic": "^0.5.2",
-        "pump": "^3.0.0",
-        "tar-stream": "^2.1.4"
-      }
-    },
-    "node_modules/tar-stream": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
-      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "bl": "^4.0.3",
-        "end-of-stream": "^1.4.1",
-        "fs-constants": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^3.1.1"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/tinybench": {
       "version": "2.9.0",
       "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -1866,19 +1394,6 @@
         "tree-sitter-wasms": "^0.1.11"
       }
     },
-    "node_modules/tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-      "license": "Apache-2.0",
-      "optional": true,
-      "dependencies": {
-        "safe-buffer": "^5.0.1"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/typescript": {
       "version": "5.9.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1900,13 +1415,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "license": "MIT",
-      "optional": true
-    },
     "node_modules/vite": {
       "version": "5.4.21",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
@@ -2087,13 +1595,6 @@
       "engines": {
         "node": ">=8"
       }
-    },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "license": "ISC",
-      "optional": true
     }
   }
 }

+ 0 - 4
package.json

@@ -37,7 +37,6 @@
     "fast-string-width": "^3.0.2",
     "fast-wrap-ansi": "^0.2.0",
     "jsonc-parser": "^3.3.1",
-    "node-sqlite3-wasm": "^0.8.30",
     "picomatch": "^4.0.3",
     "sisteransi": "^1.0.5",
     "tree-sitter-wasms": "^0.1.11",
@@ -50,9 +49,6 @@
     "typescript": "^5.0.0",
     "vitest": "^2.1.9"
   },
-  "optionalDependencies": {
-    "better-sqlite3": "^12.4.1"
-  },
   "engines": {
     "node": ">=20.0.0 <25.0.0"
   }

+ 6 - 11
src/bin/codegraph.ts

@@ -736,19 +736,14 @@ program
       console.log(`  Nodes:     ${formatNumber(stats.nodeCount)}`);
       console.log(`  Edges:     ${formatNumber(stats.edgeCount)}`);
       console.log(`  DB Size:   ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
-      // Surface the active SQLite backend so users can spot the silent
-      // WASM fallback (5-10x slower). better-sqlite3 is in
-      // `optionalDependencies`, so `npm install` succeeds without it
-      // when the native build fails.
-      const backendLabel =
-        backend === 'native' ? chalk.green('native')
-        : backend === 'node-sqlite' ? chalk.green(`node:sqlite ${getGlyphs().dash} built-in (full WAL)`)
-        : chalk.yellow(`wasm ${getGlyphs().dash} slower fallback; run \`npm rebuild better-sqlite3\``);
+      // Surface the active SQLite backend (node:sqlite — Node's built-in real
+      // SQLite, full WAL + FTS5, no native build).
+      const backendLabel = chalk.green(`node:sqlite ${getGlyphs().dash} built-in (full WAL)`);
       console.log(`  Backend:   ${backendLabel}`);
       // Effective journal mode: 'wal' means concurrent reads never block on a
-      // writer; anything else means they can ("database is locked"). Native can
-      // silently fall back to DELETE on filesystems without shared-memory
-      // support (network mounts, WSL2 /mnt). See issue #238.
+      // writer; anything else means they can ("database is locked"). node:sqlite
+      // supports WAL everywhere, so a non-wal mode means the filesystem can't
+      // (network mounts, WSL2 /mnt). See issue #238.
       const journalLabel = journalMode === 'wal'
         ? chalk.green('wal')
         : chalk.yellow(`${journalMode || 'unknown'} ${getGlyphs().dash} WAL inactive; reads can block on writes`);

+ 5 - 6
src/db/index.ts

@@ -10,7 +10,7 @@ import * as path from 'path';
 import { SchemaVersion } from '../types';
 import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
 
-export { SqliteDatabase, SqliteBackend, WASM_FALLBACK_FIX_RECIPE } from './sqlite-adapter';
+export { SqliteDatabase, SqliteBackend } from './sqlite-adapter';
 
 /**
  * Apply connection-level PRAGMAs. Shared by `initialize` and `open` so the two
@@ -22,15 +22,14 @@ export { SqliteDatabase, SqliteBackend, WASM_FALLBACK_FIX_RECIPE } from './sqlit
  * the lock instead of throwing "database is locked" immediately. See issue #238.
  *
  * The 5s window (was 120s) rides out a normal incremental sync; the old
- * 2-minute wait presented as a frozen, hung agent. Reads on the native WAL
- * backend never wait at all, so this timeout only governs cross-process write
- * contention and the wasm fallback — which can't do WAL (the adapter downgrades
- * it to DELETE) and so layers a bounded read retry on top (see sqlite-adapter).
+ * 2-minute wait presented as a frozen, hung agent. With WAL, reads never block
+ * on a writer, so this timeout only governs cross-process write contention
+ * (e.g. the git-hook `codegraph sync` running while the MCP server writes).
  */
 function configureConnection(db: SqliteDatabase): void {
   db.pragma('busy_timeout = 5000');      // MUST be first — see above
   db.pragma('foreign_keys = ON');
-  db.pragma('journal_mode = WAL');       // downgraded to DELETE on the wasm backend
+  db.pragma('journal_mode = WAL');       // node:sqlite supports WAL on every platform
   db.pragma('synchronous = NORMAL');     // safe with WAL mode
   db.pragma('cache_size = -64000');      // 64 MB page cache
   db.pragma('temp_store = MEMORY');      // temp tables in memory

+ 41 - 377
src/db/sqlite-adapter.ts

@@ -1,8 +1,13 @@
 /**
  * SQLite Adapter
  *
- * Provides a unified interface over better-sqlite3 (native) and
- * node-sqlite3-wasm (WASM fallback) for universal cross-platform support.
+ * Thin wrapper over Node's built-in `node:sqlite` (`DatabaseSync`), exposed
+ * through a small better-sqlite3-shaped interface so the rest of the codebase
+ * is storage-agnostic.
+ *
+ * CodeGraph ships with a bundled Node runtime, so `node:sqlite` (real SQLite,
+ * with WAL + FTS5) is always available — there is no native build step and no
+ * wasm fallback. When run from source instead, it requires Node >= 22.5.
  */
 
 export interface SqliteStatement {
@@ -14,306 +19,26 @@ export interface SqliteStatement {
 export interface SqliteDatabase {
   prepare(sql: string): SqliteStatement;
   exec(sql: string): void;
-  pragma(str: string): any;
+  pragma(str: string, options?: { simple?: boolean }): any;
   transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T;
   close(): void;
   readonly open: boolean;
 }
 
-export type SqliteBackend = 'native' | 'node-sqlite' | 'wasm';
-
-/**
- * One-line summary of the recovery steps shown when WASM fallback is
- * active. Single source of truth so the recipe can't drift between the
- * stderr banner and the MCP status formatter.
- */
-export const WASM_FALLBACK_FIX_RECIPE =
-  '`xcode-select --install` (macOS) or `apt install build-essential` (Debian/Ubuntu), ' +
-  'then `npm rebuild better-sqlite3`, or `npm install better-sqlite3 --save` to force-include it.';
-
-/**
- * Multi-line banner shown to stderr when `createDatabase` falls back to
- * WASM. Replaces a one-line `console.warn` that MCP transports (which
- * take stdout for the protocol) typically swallow, leaving users on a
- * 5-10x slower backend with no signal.
- *
- * Exported for unit testing — pinning the recipe content prevents
- * future edits from silently stripping the recovery commands.
- */
-export function buildWasmFallbackBanner(nativeError?: string): string {
-  const sep = '─'.repeat(72);
-  const lines = [
-    sep,
-    '[CodeGraph] WASM SQLite fallback active (better-sqlite3 unavailable)',
-    sep,
-    'Indexing and sync will be 5-10x slower than the native backend.',
-    '',
-    'Fix on macOS:',
-    '  xcode-select --install        # install C build tools',
-    '  npm rebuild better-sqlite3    # rebuild native binding for current Node',
-    '',
-    'Fix on Linux:',
-    '  sudo apt install build-essential python3 make    # Debian/Ubuntu',
-    '  # or: sudo yum groupinstall "Development Tools"  # RHEL/Fedora',
-    '  npm rebuild better-sqlite3',
-    '',
-    'Or force-include as a hard dependency on any platform:',
-    '  npm install better-sqlite3 --save',
-    '',
-    'Verify after fix: `codegraph status` should show `Backend: native`.',
-  ];
-  if (nativeError) {
-    lines.push('', `Native load error: ${nativeError}`);
-  }
-  lines.push(sep);
-  return lines.join('\n');
-}
-
 /**
- * Translate @named parameters (better-sqlite3 style) to positional ? params
- * for node-sqlite3-wasm, which only supports positional binding.
- *
- * Returns the rewritten SQL and an ordered list of parameter names.
- * If no named params are found, returns null for paramOrder (positional mode).
+ * The active SQLite backend. Only one now (`node:sqlite`); kept as a named type
+ * so `codegraph status` and the per-instance reporting have a stable shape.
  */
-function translateNamedParams(sql: string): { sql: string; paramOrder: string[] | null } {
-  const paramOrder: string[] = [];
-  const rewritten = sql.replace(/@(\w+)/g, (_match, name: string) => {
-    paramOrder.push(name);
-    return '?';
-  });
-  if (paramOrder.length === 0) {
-    return { sql, paramOrder: null };
-  }
-  return { sql: rewritten, paramOrder };
-}
-
-/**
- * Convert better-sqlite3-style params to a positional array for node-sqlite3-wasm.
- *
- * Handles three calling conventions:
- * - Named object: run({ id: '1', name: 'a' }) → positional array via paramOrder
- * - Positional args: run('a', 'b') → ['a', 'b']
- * - No args: run() → undefined
- */
-function resolveParams(params: any[], paramOrder: string[] | null): any {
-  if (params.length === 0) return undefined;
-
-  // If paramOrder exists and first arg is a plain object, do named→positional translation
-  if (paramOrder && params.length === 1 && params[0] !== null && typeof params[0] === 'object' && !Array.isArray(params[0]) && !(params[0] instanceof Buffer) && !(params[0] instanceof Uint8Array)) {
-    const obj = params[0];
-    return paramOrder.map(name => obj[name]);
-  }
-
-  // Positional: single value or already an array
-  if (params.length === 1) return params[0];
-  return params;
-}
-
-/**
- * Whether an error is SQLite's SQLITE_BUSY / SQLITE_LOCKED ("database is
- * locked"). Checks better-sqlite3's `code` first, then falls back to message
- * text for the wasm backend (which throws a plain Error). Exported for tests.
- */
-export function isDatabaseLockedError(err: unknown): boolean {
-  const code = (err as { code?: unknown } | null)?.code;
-  if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED') return true;
-  const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
-  return (
-    msg.includes('database is locked') ||
-    msg.includes('database is busy') ||
-    msg.includes('database table is locked') ||
-    msg.includes('sqlite_busy') ||
-    msg.includes('sqlite_locked')
-  );
-}
-
-/**
- * Sleep synchronously for `ms` without spinning the CPU. The wasm backend is
- * single-threaded and synchronous, so an async sleep is useless at the
- * (synchronous) query call site — we have to actually block this turn while a
- * writer in another process clears.
- */
-function sleepSync(ms: number): void {
-  if (ms <= 0) return;
-  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
-}
-
-export interface BusyRetryOptions {
-  /** Total attempts, including the first. */
-  attempts?: number;
-  /** Backoff per retry (ms); the last entry repeats if more retries remain. */
-  backoffMs?: number[];
-  /** Sleep implementation — injectable so tests don't actually wait. */
-  sleep?: (ms: number) => void;
-}
-
-/**
- * Run a read, retrying on SQLITE_BUSY with bounded backoff.
- *
- * Used only by the wasm backend: it can't use WAL (downgraded to DELETE), so a
- * writer in ANOTHER process (e.g. the git-hook `codegraph sync`) briefly blocks
- * readers. `busy_timeout` helps but can return immediately when SQLite detects a
- * would-be deadlock; a short retry rides out the writer. Reads only — never wrap
- * writes, which run inside transactions guarded by the cross-process FileLock.
- * The native backend doesn't use this: WAL lets readers proceed during a write.
- * See issue #238.
- */
-export function withBusyRetry<T>(fn: () => T, opts: BusyRetryOptions = {}): T {
-  const attempts = opts.attempts ?? 3;
-  const backoff = opts.backoffMs ?? [150, 400];
-  const sleep = opts.sleep ?? sleepSync;
-  let lastErr: unknown;
-  for (let i = 0; i < attempts; i++) {
-    try {
-      return fn();
-    } catch (err) {
-      lastErr = err;
-      if (i === attempts - 1 || !isDatabaseLockedError(err)) throw err;
-      sleep(backoff.length > 0 ? backoff[Math.min(i, backoff.length - 1)]! : 0);
-    }
-  }
-  throw lastErr;
-}
-
-/**
- * Wraps node-sqlite3-wasm to match the better-sqlite3 interface.
- *
- * Key differences handled:
- * - better-sqlite3 uses @named params; node-sqlite3-wasm uses positional ? only
- * - better-sqlite3 uses variadic args: stmt.run(a, b, c)
- * - node-sqlite3-wasm uses a single array/object: stmt.run([a, b, c])
- * - node-sqlite3-wasm has `isOpen` instead of `open`
- * - node-sqlite3-wasm doesn't have a `pragma()` method
- * - node-sqlite3-wasm doesn't have a `transaction()` method
- */
-class WasmDatabaseAdapter implements SqliteDatabase {
-  private _db: any;
-  // Track raw WASM statements so we can finalize them on close.
-  // node-sqlite3-wasm won't release its file lock if statements are left open.
-  private _openStmts = new Set<any>();
-
-  constructor(dbPath: string) {
-    // eslint-disable-next-line @typescript-eslint/no-require-imports
-    const { Database } = require('node-sqlite3-wasm');
-    this._db = new Database(dbPath);
-  }
-
-  get open(): boolean {
-    return this._db.isOpen;
-  }
-
-  prepare(sql: string): SqliteStatement {
-    const { sql: rewrittenSql, paramOrder } = translateNamedParams(sql);
-    const stmt = this._db.prepare(rewrittenSql);
-    this._openStmts.add(stmt);
-    return {
-      run(...params: any[]) {
-        const resolved = resolveParams(params, paramOrder);
-        const result = resolved !== undefined ? stmt.run(resolved) : stmt.run();
-        return {
-          changes: result?.changes ?? 0,
-          lastInsertRowid: result?.lastInsertRowid ?? 0,
-        };
-      },
-      get(...params: any[]) {
-        // Reads retry on SQLITE_BUSY — the wasm backend has no WAL, so a writer
-        // in another process can briefly block this read. See issue #238.
-        return withBusyRetry(() => {
-          const resolved = resolveParams(params, paramOrder);
-          return resolved !== undefined ? stmt.get(resolved) : stmt.get();
-        });
-      },
-      all(...params: any[]) {
-        return withBusyRetry(() => {
-          const resolved = resolveParams(params, paramOrder);
-          return resolved !== undefined ? stmt.all(resolved) : stmt.all();
-        });
-      },
-    };
-  }
-
-  exec(sql: string): void {
-    this._db.exec(sql);
-  }
-
-  pragma(str: string): any {
-    const trimmed = str.trim();
-
-    // Write pragma: "key = value"
-    if (trimmed.includes('=')) {
-      const eqIdx = trimmed.indexOf('=');
-      const key = trimmed.substring(0, eqIdx).trim();
-      const value = trimmed.substring(eqIdx + 1).trim();
-
-      // WAL is not supported in WASM SQLite — use DELETE journal mode
-      if (key === 'journal_mode' && value.toUpperCase() === 'WAL') {
-        this._db.exec('PRAGMA journal_mode = DELETE');
-        return;
-      }
-
-      // mmap is not available in WASM — silently skip
-      if (key === 'mmap_size') {
-        return;
-      }
-
-      // synchronous = NORMAL is unsafe without WAL — use FULL
-      if (key === 'synchronous' && value.toUpperCase() === 'NORMAL') {
-        this._db.exec('PRAGMA synchronous = FULL');
-        return;
-      }
-
-      this._db.exec(`PRAGMA ${key} = ${value}`);
-      return;
-    }
-
-    // Read pragma: "key" — return the value
-    const stmt = this._db.prepare(`PRAGMA ${trimmed}`);
-    const result = stmt.get();
-    stmt.finalize();
-    return result;
-  }
-
-  transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
-    return (...args: any[]) => {
-      this._db.exec('BEGIN');
-      try {
-        const result = fn(...args);
-        this._db.exec('COMMIT');
-        return result;
-      } catch (error) {
-        this._db.exec('ROLLBACK');
-        throw error;
-      }
-    };
-  }
-
-  close(): void {
-    // Finalize all tracked statements before closing.
-    // node-sqlite3-wasm won't release its directory-based file lock
-    // if any prepared statements remain open.
-    for (const stmt of this._openStmts) {
-      try { stmt.finalize(); } catch { /* already finalized */ }
-    }
-    this._openStmts.clear();
-    this._db.close();
-  }
-}
+export type SqliteBackend = 'node-sqlite';
 
 /**
  * Wraps Node's built-in `node:sqlite` (`DatabaseSync`) to match the
- * better-sqlite3 interface.
+ * better-sqlite3 interface the rest of the code expects.
  *
- * Unlike the wasm adapter this is REAL SQLite compiled into Node, so it supports
- * WAL, FTS5, mmap, and `@named` params natively — the only shims needed are the
+ * node:sqlite is real SQLite compiled into Node, so it supports WAL, FTS5,
+ * mmap, and `@named` params natively — the only shims needed are the
  * better-sqlite3 conveniences node:sqlite omits: a `.pragma()` helper, a
- * `.transaction()` helper, and `open` (node:sqlite exposes `isOpen`). It also
- * needs no statement finalization on close (node-sqlite3-wasm did).
- *
- * Available on Node >= 22.5 (the module is simply absent on older Node, so
- * `createDatabase` falls through to wasm there). The API is still flagged
- * experimental; `node:sqlite` emits a one-time ExperimentalWarning to stderr on
- * load, which is harmless for the MCP stdout protocol.
+ * `.transaction()` helper, and `open` (node:sqlite exposes `isOpen`).
  */
 class NodeSqliteAdapter implements SqliteDatabase {
   private _db: any;
@@ -331,7 +56,7 @@ class NodeSqliteAdapter implements SqliteDatabase {
   prepare(sql: string): SqliteStatement {
     // node:sqlite matches better-sqlite3's calling convention (variadic
     // positional args, or a single object for @named params), so params forward
-    // through unchanged — no positional translation like the wasm adapter needs.
+    // through unchanged.
     const stmt = this._db.prepare(sql);
     return {
       run(...params: any[]) {
@@ -354,16 +79,21 @@ class NodeSqliteAdapter implements SqliteDatabase {
     this._db.exec(sql);
   }
 
-  pragma(str: string): any {
+  pragma(str: string, options?: { simple?: boolean }): any {
     const trimmed = str.trim();
     // Write pragma ("key = value"): node:sqlite is real SQLite, so every pragma
-    // (WAL, mmap, synchronous, …) applies as-is — no special-casing like wasm.
+    // (WAL, mmap, synchronous, …) applies as-is.
     if (trimmed.includes('=')) {
       this._db.exec(`PRAGMA ${trimmed}`);
       return;
     }
-    // Read pragma: return the row object (e.g. { journal_mode: 'wal' }).
-    return this._db.prepare(`PRAGMA ${trimmed}`).get();
+    // Read pragma. Default: the row object (e.g. { journal_mode: 'wal' }).
+    // `{ simple: true }` returns just the single column value, like better-sqlite3.
+    const row = this._db.prepare(`PRAGMA ${trimmed}`).get();
+    if (options?.simple) {
+      return row && typeof row === 'object' ? Object.values(row)[0] : row;
+    }
+    return row;
   }
 
   transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
@@ -381,95 +111,29 @@ class NodeSqliteAdapter implements SqliteDatabase {
   }
 
   close(): void {
-    this._db.close();
+    // node:sqlite's DatabaseSync.close() throws if already closed; make it
+    // idempotent to match better-sqlite3 (callers may close more than once).
+    if (this._db.isOpen) this._db.close();
   }
 }
 
 /**
- * Concise stderr notice shown when better-sqlite3 is unavailable but Node's
- * built-in node:sqlite is, so we use that instead of the slow wasm fallback.
- * Unlike wasm, node:sqlite has full WAL + FTS5 and near-native speed, so this is
- * informational — not a "fix me" warning. Exported for tests.
- */
-export function buildNodeSqliteNotice(nativeError?: string): string {
-  const lines = [
-    '[CodeGraph] better-sqlite3 unavailable — using the built-in node:sqlite backend.',
-    'Full WAL + FTS5 support, no native build required. To restore the (fastest)',
-    `native backend: ${WASM_FALLBACK_FIX_RECIPE}`,
-  ];
-  if (nativeError) lines.push(`(better-sqlite3 load error: ${nativeError})`);
-  return lines.join('\n') + '\n';
-}
-
-/**
- * Create a database connection, trying backends in order of preference:
- *   1. better-sqlite3 (native)  — fastest, but needs a compiled binding
- *   2. node:sqlite (Node ≥22.5) — real WAL + FTS5, no native build, no wasm
- *   3. node-sqlite3-wasm        — last resort (no WAL); only ancient Node
- *
- * node:sqlite sits ahead of wasm so that when the native binding fails to load
- * (common on Windows / locked-down CI), users land on a backend WITH WAL instead
- * of the no-WAL wasm path that causes concurrent-read lock errors (issue #238).
- *
- * `CODEGRAPH_SQLITE_BACKEND=native|node-sqlite|wasm` forces a single backend
- * (used for A/B testing and to opt into node:sqlite); a forced backend that
- * can't load throws rather than silently falling through.
+ * Create a database connection backed by `node:sqlite`.
  *
  * Returns the active backend alongside the db so each `DatabaseConnection` can
- * report its own backend per-instance — MCP can open multiple project DBs in one
- * process, so a process-global would race / overwrite.
+ * report it per-instance — MCP can open multiple project DBs in one process, so
+ * a process-global would race.
  */
 export function createDatabase(dbPath: string): { db: SqliteDatabase; backend: SqliteBackend } {
-  const forced = (process.env.CODEGRAPH_SQLITE_BACKEND || '').trim().toLowerCase();
-  const errors: { native?: string; nodeSqlite?: string; wasm?: string } = {};
-  const toMsg = (e: unknown) => (e instanceof Error ? e.message : String(e));
-
-  const tryNative = !forced || forced === 'native';
-  const tryNodeSqlite = !forced || forced === 'node-sqlite' || forced === 'node:sqlite';
-  const tryWasm = !forced || forced === 'wasm';
-
-  // 1. Native better-sqlite3
-  if (tryNative) {
-    try {
-      // eslint-disable-next-line @typescript-eslint/no-require-imports
-      const Database = require('better-sqlite3');
-      return { db: new Database(dbPath) as SqliteDatabase, backend: 'native' };
-    } catch (error) {
-      errors.native = toMsg(error);
-    }
+  try {
+    return { db: new NodeSqliteAdapter(dbPath), backend: 'node-sqlite' };
+  } catch (error) {
+    const msg = error instanceof Error ? error.message : String(error);
+    throw new Error(
+      'Failed to open SQLite via the built-in node:sqlite module.\n' +
+      'CodeGraph requires node:sqlite (Node.js 22.5+). Install the self-contained\n' +
+      'CodeGraph release (it bundles a compatible Node), or run on Node 22.5+.\n' +
+      `Underlying error: ${msg}`
+    );
   }
-
-  // 2. Node's built-in node:sqlite (real WAL, no native build)
-  if (tryNodeSqlite) {
-    try {
-      const db = new NodeSqliteAdapter(dbPath);
-      // Announce only when this is a genuine fallback (native was tried & failed),
-      // not when the caller explicitly forced node-sqlite.
-      if (!forced && errors.native) {
-        process.stderr.write(buildNodeSqliteNotice(errors.native));
-      }
-      return { db, backend: 'node-sqlite' };
-    } catch (error) {
-      errors.nodeSqlite = toMsg(error);
-    }
-  }
-
-  // 3. WASM (no WAL) — last resort
-  if (tryWasm) {
-    try {
-      const db = new WasmDatabaseAdapter(dbPath);
-      console.warn(buildWasmFallbackBanner(errors.native));
-      return { db, backend: 'wasm' };
-    } catch (error) {
-      errors.wasm = toMsg(error);
-    }
-  }
-
-  throw new Error(
-    `Failed to load a SQLite backend.\n` +
-    (errors.native ? `  Native (better-sqlite3): ${errors.native}\n` : '') +
-    (errors.nodeSqlite ? `  node:sqlite: ${errors.nodeSqlite}\n` : '') +
-    (errors.wasm ? `  WASM (node-sqlite3-wasm): ${errors.wasm}\n` : '') +
-    (forced ? `  (CODEGRAPH_SQLITE_BACKEND=${forced} restricted which backends were tried)` : '')
-  );
 }

+ 3 - 4
src/index.ts

@@ -613,10 +613,9 @@ export class CodeGraph {
   }
 
   /**
-   * Active SQLite backend for this project's connection. `wasm` means
-   * the native better-sqlite3 install failed and the WASM fallback is
-   * serving requests at 5-10x the latency. Surfaced via `codegraph
-   * status` and the `codegraph_status` MCP tool.
+   * Active SQLite backend for this project's connection (`node-sqlite` — Node's
+   * built-in real-SQLite module). Surfaced via `codegraph status` and the
+   * `codegraph_status` MCP tool alongside the effective journal mode.
    */
   getBackend(): import('./db').SqliteBackend {
     return this.db.getBackend();

+ 7 - 25
src/mcp/tools.ts

@@ -11,7 +11,6 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
 import { clamp, validatePathWithinRoot } from '../utils';
 import { tmpdir } from 'os';
 import { join } from 'path';
-import { WASM_FALLBACK_FIX_RECIPE } from '../db';
 
 /** Maximum output length to prevent context bloat (characters) */
 const MAX_OUTPUT_LENGTH = 15000;
@@ -1332,38 +1331,21 @@ export class ToolHandler {
       `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`,
     ];
 
-    // Surface the active SQLite backend. Without this, users on the
-    // silent WASM fallback (better-sqlite3 install failed) see "slow"
-    // indexing and DB-lock errors with no signal of why.
-    const backend = cg.getBackend();
-    if (backend === 'native') {
-      lines.push(`**Backend:** native (better-sqlite3)`);
-    } else if (backend === 'node-sqlite') {
-      lines.push(
-        `**Backend:** node:sqlite (Node built-in) — full WAL + FTS5. ` +
-        `For maximum speed, restore native: ${WASM_FALLBACK_FIX_RECIPE}`
-      );
-    } else {
-      lines.push(
-        `**Backend:** ⚠ wasm (better-sqlite3 unavailable) — ` +
-        `5-10x slower than native, no WAL. Fix: ${WASM_FALLBACK_FIX_RECIPE}`
-      );
-    }
+    // Surface the active SQLite backend (node:sqlite, Node's built-in real
+    // SQLite — full WAL + FTS5, no native build).
+    lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
 
     // Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
-    // anything else ⇒ they can ("database is locked"). The wasm backend can't do
-    // WAL, and even native silently falls back to DELETE on filesystems without
-    // shared-memory (network/virtualized mounts, WSL2 /mnt). See issue #238.
+    // anything else ⇒ they can ("database is locked"). node:sqlite supports WAL
+    // everywhere, so a non-wal mode means the filesystem can't (network/
+    // virtualized mounts, WSL2 /mnt). See issue #238.
     const journalMode = cg.getJournalMode();
     if (journalMode === 'wal') {
       lines.push(`**Journal mode:** wal (concurrent reads safe)`);
     } else {
       lines.push(
         `**Journal mode:** ⚠ ${journalMode || 'unknown'} — WAL not active, so reads ` +
-        `can block on a concurrent write` +
-        // wasm can't do WAL at all; the real-SQLite backends only lack it when the
-        // filesystem doesn't support shared memory (network mounts, WSL2 /mnt).
-        (backend === 'wasm' ? '' : ' (WAL appears unsupported on this filesystem)')
+        `can block on a concurrent write (WAL appears unsupported on this filesystem)`
       );
     }