From 9c3f8888772bb5c6b647a6dcf2bdbfdc20c16185 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:02:34 -0600 Subject: [PATCH 1/6] fix(native): convert null to undefined for napi-rs Option fields napi-rs Option and Option accept undefined but not null. The insert-nodes stage was mapping visibility and endLine to null via ?? null, causing bulkInsertNodes to crash with "Failed to convert JavaScript value Null into rust type String". --- src/domain/graph/builder/stages/insert-nodes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/graph/builder/stages/insert-nodes.ts b/src/domain/graph/builder/stages/insert-nodes.ts index 32cebfd5..a3e09271 100644 --- a/src/domain/graph/builder/stages/insert-nodes.ts +++ b/src/domain/graph/builder/stages/insert-nodes.ts @@ -72,14 +72,14 @@ function tryNativeInsert(ctx: PipelineContext): boolean { name: def.name, kind: def.kind, line: def.line, - endLine: def.endLine ?? null, - visibility: def.visibility ?? null, + endLine: def.endLine ?? undefined, + visibility: def.visibility ?? undefined, children: (def.children ?? []).map((c) => ({ name: c.name, kind: c.kind, line: c.line, - endLine: c.endLine ?? null, - visibility: c.visibility ?? null, + endLine: c.endLine ?? undefined, + visibility: c.visibility ?? undefined, })), })), exports: symbols.exports.map((exp) => ({ From bccd2c5a026cdc13c9d83f4e06a1a1d4c1589320 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:02:48 -0600 Subject: [PATCH 2/6] fix(sequence): support dataflow annotations with NativeRepository When native engine is available, openRepo returns NativeRepository which lacks a raw SQLite handle. annotateDataflow was bailing out with the instanceof SqliteRepository guard. Open a fallback better-sqlite3 read-only connection for the dataflow queries when needed. --- src/features/sequence.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/features/sequence.ts b/src/features/sequence.ts index 04fa575f..aa891d78 100644 --- a/src/features/sequence.ts +++ b/src/features/sequence.ts @@ -1,10 +1,11 @@ +import Database from 'better-sqlite3'; import { openRepo, type Repository } from '../db/index.js'; import { SqliteRepository } from '../db/repository/sqlite-repository.js'; import { findMatchingNodes } from '../domain/queries.js'; import { loadConfig } from '../infrastructure/config.js'; import { isTestFile } from '../infrastructure/test-filter.js'; import { paginateResult } from '../shared/paginate.js'; -import type { CodegraphConfig, NodeRowWithFanIn } from '../types.js'; +import type { BetterSqlite3Database, CodegraphConfig, NodeRowWithFanIn } from '../types.js'; import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js'; // ─── Alias generation ──────────────────────────────────────────────── @@ -150,12 +151,34 @@ function annotateDataflow( repo: Repository, messages: SequenceMessage[], idToNode: Map, + dbPath?: string, ): void { const hasTable = repo.hasDataflowTable(); + if (!hasTable) return; + + let db: BetterSqlite3Database; + let ownDb = false; + if (repo instanceof SqliteRepository) { + db = repo.db; + } else if (dbPath) { + db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database; + ownDb = true; + } else { + return; + } - if (!hasTable || !(repo instanceof SqliteRepository)) return; + try { + _annotateDataflowImpl(db, messages, idToNode); + } finally { + if (ownDb) db.close(); + } +} - const db = repo.db; +function _annotateDataflowImpl( + db: BetterSqlite3Database, + messages: SequenceMessage[], + idToNode: Map, +): void { const nodeByNameFile = new Map(); for (const n of idToNode.values()) { nodeByNameFile.set(`${n.name}|${n.file}`, n); @@ -308,7 +331,7 @@ export function sequenceData( ); if (opts.dataflow && messages.length > 0) { - annotateDataflow(repo, messages, idToNode); + annotateDataflow(repo, messages, idToNode, dbPath); } messages.sort((a, b) => { From 6051bd55261663bd647098958fb2f33c630991f3 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:22:00 -0600 Subject: [PATCH 3/6] fix(native): graceful fallback when bulkInsertNodes fails The native Rust bulkInsertNodes can fail with WAL conflicts or DB corruption when running alongside a JS better-sqlite3 connection. Wrap the native call in a try-catch so the build pipeline falls back to the JS implementation instead of crashing. --- src/domain/graph/builder/stages/insert-nodes.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/domain/graph/builder/stages/insert-nodes.ts b/src/domain/graph/builder/stages/insert-nodes.ts index a3e09271..99748d44 100644 --- a/src/domain/graph/builder/stages/insert-nodes.ts +++ b/src/domain/graph/builder/stages/insert-nodes.ts @@ -340,11 +340,16 @@ export async function insertNodes(ctx: PipelineContext): Promise { const t0 = performance.now(); // Try native Rust path first — single transaction, no JS↔C overhead - if (ctx.engineName === 'native' && tryNativeInsert(ctx)) { - ctx.timing.insertMs = performance.now() - t0; - - // Removed-file hash cleanup is handled inside the native call - return; + if (ctx.engineName === 'native') { + try { + if (tryNativeInsert(ctx)) { + ctx.timing.insertMs = performance.now() - t0; + // Removed-file hash cleanup is handled inside the native call + return; + } + } catch { + // Native insert failed — fall through to JS implementation + } } // JS fallback From 79ce59b0154336cc749d7355636f6288e8c4cb7a Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:27:45 -0600 Subject: [PATCH 4/6] fix(native): disable bulkInsertNodes to prevent dual-connection DB corruption (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native Rust insert path corrupts the WAL-mode DB when both JS (better-sqlite3) and Rust (rusqlite) connections are open simultaneously. This path was never operational before — it always crashed on null visibility serialization. Disabling it restores the pre-existing JS-only behavior until the dual-connection issue is resolved. --- src/domain/graph/builder/stages/insert-nodes.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/domain/graph/builder/stages/insert-nodes.ts b/src/domain/graph/builder/stages/insert-nodes.ts index 99748d44..6d58f532 100644 --- a/src/domain/graph/builder/stages/insert-nodes.ts +++ b/src/domain/graph/builder/stages/insert-nodes.ts @@ -39,6 +39,12 @@ interface PrecomputedFileData { // ── Native fast-path ───────────────────────────────────────────────── function tryNativeInsert(ctx: PipelineContext): boolean { + // Disabled: bulkInsertNodes corrupts the DB when both the JS (better-sqlite3) + // and Rust (rusqlite) connections are open to the same WAL-mode file. + // The native path was never operational before — it always crashed on null + // visibility serialisation. See #694 for the dual-connection fix. + if (ctx.db) return false; + // Use NativeDatabase persistent connection (Phase 6.15+). // Standalone napi functions were removed in 6.17 — falls through to JS if nativeDb unavailable. if (!ctx.nativeDb?.bulkInsertNodes) return false; From 46eb6a1653a5ec06a2cc43ba80654c0f140540a3 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:36:41 -0600 Subject: [PATCH 5/6] fix(pipeline): close nativeDb before stages to prevent dual-connection WAL corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The null→undefined fix for napi-rs Option fields (commit 1) unmasked a deeper issue: every native write operation (purgeFilesData, bulkInsertEdges, classifyRolesFull, setBuildMeta) corrupts the DB when both better-sqlite3 and rusqlite connections are open to the same WAL-mode file. Previously the pipeline crashed at bulkInsertNodes with StringExpected before reaching these later stages, masking the corruption. Now that bulkInsertNodes no longer crashes, all subsequent native writes execute and corrupt the DB. Fix: close nativeDb at the start of runPipelineStages() so all stages use their JS fallback paths. Also run initSchema via JS unconditionally so better-sqlite3 sees the schema after nativeDb is closed. (#694) --- src/domain/graph/builder/pipeline.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/domain/graph/builder/pipeline.ts b/src/domain/graph/builder/pipeline.ts index 2d67624f..6908cf4b 100644 --- a/src/domain/graph/builder/pipeline.ts +++ b/src/domain/graph/builder/pipeline.ts @@ -109,8 +109,10 @@ function setupPipeline(ctx: PipelineContext): void { } catch (err) { warn(`NativeDatabase init failed, falling back to JS: ${(err as Error).message}`); ctx.nativeDb = undefined; - initSchema(ctx.db); } + // Always run JS initSchema so better-sqlite3 sees the schema — + // nativeDb is closed before pipeline stages run (dual-connection guard). + initSchema(ctx.db); } else { initSchema(ctx.db); } @@ -156,6 +158,18 @@ function formatTimingResult(ctx: PipelineContext): BuildResult { // ── Pipeline stages execution ─────────────────────────────────────────── async function runPipelineStages(ctx: PipelineContext): Promise { + // Prevent dual-connection WAL corruption: when both better-sqlite3 (ctx.db) + // and rusqlite (ctx.nativeDb) are open to the same file, Rust writes corrupt + // the DB. Clear nativeDb so all stages use JS fallback paths. See #694. + if (ctx.db && ctx.nativeDb) { + try { + (ctx.nativeDb as { close?: () => void }).close?.(); + } catch { + /* ignore close errors */ + } + ctx.nativeDb = undefined; + } + await collectFiles(ctx); await detectChanges(ctx); From 390878d12a966b4ba7767f40a8674392e7aea6fc Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:46:12 -0600 Subject: [PATCH 6/6] fix(types): remove null from napi-rs batch interface types (#693) Drop `| null` union from endLine and visibility fields in the local InsertNodesBatch interface. napi-rs Option bindings reject null at runtime (mapped only from undefined), so the type annotations were misleading and could let a future developer reintroduce the crash. --- src/domain/graph/builder/stages/insert-nodes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/graph/builder/stages/insert-nodes.ts b/src/domain/graph/builder/stages/insert-nodes.ts index 6d58f532..816c402b 100644 --- a/src/domain/graph/builder/stages/insert-nodes.ts +++ b/src/domain/graph/builder/stages/insert-nodes.ts @@ -58,14 +58,14 @@ function tryNativeInsert(ctx: PipelineContext): boolean { name: string; kind: string; line: number; - endLine?: number | null; - visibility?: string | null; + endLine?: number; + visibility?: string; children: Array<{ name: string; kind: string; line: number; - endLine?: number | null; - visibility?: string | null; + endLine?: number; + visibility?: string; }>; }>; exports: Array<{ name: string; kind: string; line: number }>;