diff --git a/crates/codegraph-core/src/ast_db.rs b/crates/codegraph-core/src/ast_db.rs index 4f317db1..b67b94fc 100644 --- a/crates/codegraph-core/src/ast_db.rs +++ b/crates/codegraph-core/src/ast_db.rs @@ -74,15 +74,26 @@ pub fn bulk_insert_ast_nodes(db_path: String, batches: Vec) -> u32 } let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let mut conn = match Connection::open_with_flags(&db_path, flags) { + let conn = match Connection::open_with_flags(&db_path, flags) { Ok(c) => c, Err(_) => return 0, }; // Match the JS-side performance pragmas (including busy_timeout for WAL contention) - let _ = conn.execute_batch( - "PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000", - ); + let _ = conn.execute_batch("PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000"); + + do_insert_ast_nodes(&conn, &batches).unwrap_or(0) +} + +/// Internal implementation: insert AST nodes using an existing connection. +/// Used by both the standalone `bulk_insert_ast_nodes` function and `NativeDatabase`. +pub(crate) fn do_insert_ast_nodes( + conn: &Connection, + batches: &[FileAstBatch], +) -> rusqlite::Result { + if batches.is_empty() { + return Ok(0); + } // Bail out if the ast_nodes table doesn't exist (schema too old) let has_table: bool = conn @@ -90,19 +101,15 @@ pub fn bulk_insert_ast_nodes(db_path: String, batches: Vec) -> u32 .and_then(|mut s| s.query_row([], |_| Ok(true))) .unwrap_or(false); if !has_table { - return 0; + return Ok(0); } // ── Phase 1: Pre-fetch node definitions for parent resolution ──────── let mut file_defs: HashMap> = HashMap::new(); { - let Ok(mut stmt) = - conn.prepare("SELECT id, line, end_line FROM nodes WHERE file = ?1") - else { - return 0; - }; + let mut stmt = conn.prepare("SELECT id, line, end_line FROM nodes WHERE file = ?1")?; - for batch in &batches { + for batch in batches { if batch.nodes.is_empty() || file_defs.contains_key(&batch.file) { continue; } @@ -118,30 +125,26 @@ pub fn bulk_insert_ast_nodes(db_path: String, batches: Vec) -> u32 .unwrap_or_default(); file_defs.insert(batch.file.clone(), defs); } - } // `stmt` dropped — releases the immutable borrow on `conn` + } // ── Phase 2: Bulk insert in a single transaction ───────────────────── - let Ok(tx) = conn.transaction() else { - return 0; - }; + let tx = conn.unchecked_transaction()?; let mut total = 0u32; { - let Ok(mut insert_stmt) = tx.prepare( + let mut insert_stmt = tx.prepare( "INSERT INTO ast_nodes (file, line, kind, name, text, receiver, parent_node_id) \ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - ) else { - return 0; - }; + )?; - for batch in &batches { + for batch in batches { let empty = Vec::new(); let defs = file_defs.get(&batch.file).unwrap_or(&empty); for node in &batch.nodes { let parent_id = find_parent_id(defs, node.line); - match insert_stmt.execute(params![ + insert_stmt.execute(params![ &batch.file, node.line, &node.kind, @@ -149,17 +152,12 @@ pub fn bulk_insert_ast_nodes(db_path: String, batches: Vec) -> u32 &node.text, &node.receiver, parent_id, - ]) { - Ok(_) => total += 1, - Err(_) => return 0, // abort; tx rolls back on drop - } + ])?; + total += 1; } } - } // `insert_stmt` dropped - - if tx.commit().is_err() { - return 0; } - total + tx.commit()?; + Ok(total) } diff --git a/crates/codegraph-core/src/edges_db.rs b/crates/codegraph-core/src/edges_db.rs index 25f1ae51..1d4ba297 100644 --- a/crates/codegraph-core/src/edges_db.rs +++ b/crates/codegraph-core/src/edges_db.rs @@ -32,20 +32,20 @@ pub fn bulk_insert_edges(db_path: String, edges: Vec) -> bool { return true; } let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let mut conn = match Connection::open_with_flags(&db_path, flags) { + let conn = match Connection::open_with_flags(&db_path, flags) { Ok(c) => c, Err(_) => return false, }; let _ = conn.execute_batch("PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000"); - do_insert(&mut conn, &edges).is_ok() + do_insert_edges(&conn, &edges).is_ok() } /// 199 rows × 5 params = 995 bind parameters per statement, safely under /// the legacy `SQLITE_MAX_VARIABLE_NUMBER` default of 999. const CHUNK: usize = 199; -fn do_insert(conn: &mut Connection, edges: &[EdgeRow]) -> rusqlite::Result<()> { - let tx = conn.transaction()?; +pub(crate) fn do_insert_edges(conn: &Connection, edges: &[EdgeRow]) -> rusqlite::Result<()> { + let tx = conn.unchecked_transaction()?; for chunk in edges.chunks(CHUNK) { let placeholders: Vec = (0..chunk.len()) diff --git a/crates/codegraph-core/src/insert_nodes.rs b/crates/codegraph-core/src/insert_nodes.rs index e49006b0..1f594d19 100644 --- a/crates/codegraph-core/src/insert_nodes.rs +++ b/crates/codegraph-core/src/insert_nodes.rs @@ -76,14 +76,14 @@ pub fn bulk_insert_nodes( removed_files: Vec, ) -> bool { let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let mut conn = match Connection::open_with_flags(&db_path, flags) { + let conn = match Connection::open_with_flags(&db_path, flags) { Ok(c) => c, Err(_) => return false, }; let _ = conn.execute_batch("PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000"); - do_insert(&mut conn, &batches, &file_hashes, &removed_files).is_ok() + do_insert_nodes(&conn, &batches, &file_hashes, &removed_files).is_ok() } // ── Internal implementation ───────────────────────────────────────── @@ -108,13 +108,13 @@ fn query_node_ids( Ok(map) } -fn do_insert( - conn: &mut Connection, +pub(crate) fn do_insert_nodes( + conn: &Connection, batches: &[InsertNodesBatch], file_hashes: &[FileHashEntry], removed_files: &[String], ) -> rusqlite::Result<()> { - let tx = conn.transaction()?; + let tx = conn.unchecked_transaction()?; // ── Phase 1: Insert file nodes + definitions + export nodes ────── { diff --git a/crates/codegraph-core/src/native_db.rs b/crates/codegraph-core/src/native_db.rs index 11e15886..df7fe69b 100644 --- a/crates/codegraph-core/src/native_db.rs +++ b/crates/codegraph-core/src/native_db.rs @@ -11,6 +11,11 @@ use napi_derive::napi; use rusqlite::{params, Connection, OpenFlags}; use send_wrapper::SendWrapper; +use crate::ast_db::{self, FileAstBatch}; +use crate::edges_db::{self, EdgeRow}; +use crate::insert_nodes::{self, FileHashEntry, InsertNodesBatch}; +use crate::roles_db::{self, RoleSummary}; + // ── Migration DDL (mirrored from src/db/migrations.ts) ────────────────── struct Migration { @@ -543,6 +548,120 @@ impl NativeDatabase { .map_err(|e| napi::Error::from_reason(format!("commit setBuildMeta failed: {e}")))?; Ok(()) } + + // ── Phase 6.15: Build pipeline write operations ───────────────────── + + /// Bulk-insert nodes, children, containment edges, exports, and file hashes. + /// Reuses the persistent connection instead of opening a new one. + /// Returns `true` on success, `false` on failure. + #[napi] + pub fn bulk_insert_nodes( + &self, + batches: Vec, + file_hashes: Vec, + removed_files: Vec, + ) -> napi::Result { + let conn = self.conn()?; + Ok(insert_nodes::do_insert_nodes(conn, &batches, &file_hashes, &removed_files) + .inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_nodes failed: {e}")) + .is_ok()) + } + + /// Bulk-insert edge rows using chunked multi-value INSERT statements. + /// Returns `true` on success, `false` on failure. + #[napi] + pub fn bulk_insert_edges(&self, edges: Vec) -> napi::Result { + if edges.is_empty() { + return Ok(true); + } + let conn = self.conn()?; + Ok(edges_db::do_insert_edges(conn, &edges) + .inspect_err(|e| eprintln!("[NativeDatabase] bulk_insert_edges failed: {e}")) + .is_ok()) + } + + /// Bulk-insert AST nodes, resolving parent_node_id from the nodes table. + /// Returns the number of rows inserted (0 on failure). + #[napi] + pub fn bulk_insert_ast_nodes(&self, batches: Vec) -> napi::Result { + let conn = self.conn()?; + Ok(ast_db::do_insert_ast_nodes(conn, &batches).unwrap_or(0)) + } + + /// Full role classification: queries all nodes, computes fan-in/fan-out, + /// classifies roles, and batch-updates the `role` column. + #[napi] + pub fn classify_roles_full(&self) -> napi::Result> { + let conn = self.conn()?; + Ok(roles_db::do_classify_full(conn).ok()) + } + + /// Incremental role classification: only reclassifies nodes from changed + /// files plus their immediate edge neighbours. + #[napi] + pub fn classify_roles_incremental( + &self, + changed_files: Vec, + ) -> napi::Result> { + let conn = self.conn()?; + Ok(roles_db::do_classify_incremental(conn, &changed_files).ok()) + } + + /// Cascade-delete all graph data for the specified files across all tables. + /// Order: dependent tables first (embeddings, cfg, dataflow, complexity, + /// metrics, ast_nodes), then edges, then nodes, then optionally file_hashes. + #[napi] + pub fn purge_files_data( + &self, + files: Vec, + purge_hashes: Option, + ) -> napi::Result<()> { + if files.is_empty() { + return Ok(()); + } + let conn = self.conn()?; + let purge_hashes = purge_hashes.unwrap_or(true); + + let tx = conn + .unchecked_transaction() + .map_err(|e| napi::Error::from_reason(format!("purge transaction failed: {e}")))?; + + // Purge each file across all tables. Optional tables are silently + // skipped if they don't exist. Order: dependents → edges → nodes → hashes. + let purge_sql: &[(&str, bool)] = &[ + ("DELETE FROM embeddings WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?1) OR target_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)", false), + ("DELETE FROM ast_nodes WHERE file = ?1", false), + // Core tables — errors propagated + ("DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?1) OR target_id IN (SELECT id FROM nodes WHERE file = ?1)", true), + ("DELETE FROM nodes WHERE file = ?1", true), + ]; + + for file in &files { + for &(sql, required) in purge_sql { + match tx.execute(sql, params![file]) { + Ok(_) => {} + Err(e) if required => { + return Err(napi::Error::from_reason(format!( + "purge failed for \"{file}\": {e}" + ))); + } + Err(_) => {} // optional table missing — skip + } + } + if purge_hashes { + let _ = tx.execute("DELETE FROM file_hashes WHERE file = ?1", params![file]); + } + } + + tx.commit() + .map_err(|e| napi::Error::from_reason(format!("purge commit failed: {e}")))?; + Ok(()) + } } // ── Private helpers ───────────────────────────────────────────────────── diff --git a/crates/codegraph-core/src/roles_db.rs b/crates/codegraph-core/src/roles_db.rs index 784787d7..24f36e84 100644 --- a/crates/codegraph-core/src/roles_db.rs +++ b/crates/codegraph-core/src/roles_db.rs @@ -74,9 +74,9 @@ pub struct RoleSummary { #[napi] pub fn classify_roles_full(db_path: String) -> Option { let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let mut conn = Connection::open_with_flags(&db_path, flags).ok()?; + let conn = Connection::open_with_flags(&db_path, flags).ok()?; let _ = conn.execute_batch("PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000"); - do_classify_full(&mut conn).ok() + do_classify_full(&conn).ok() } /// Incremental role classification: only reclassifies nodes from changed files @@ -88,9 +88,9 @@ pub fn classify_roles_incremental( changed_files: Vec, ) -> Option { let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let mut conn = Connection::open_with_flags(&db_path, flags).ok()?; + let conn = Connection::open_with_flags(&db_path, flags).ok()?; let _ = conn.execute_batch("PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000"); - do_classify_incremental(&mut conn, &changed_files).ok() + do_classify_incremental(&conn, &changed_files).ok() } // ── Shared helpers ─────────────────────────────────────────────────── @@ -228,8 +228,8 @@ fn batch_update_roles( // ── Full classification ────────────────────────────────────────────── -fn do_classify_full(conn: &mut Connection) -> rusqlite::Result { - let tx = conn.transaction()?; +pub(crate) fn do_classify_full(conn: &Connection) -> rusqlite::Result { + let tx = conn.unchecked_transaction()?; let mut summary = RoleSummary::default(); // 1. Leaf kinds → dead-leaf (skip expensive fan-in/fan-out JOINs) @@ -351,11 +351,11 @@ fn do_classify_full(conn: &mut Connection) -> rusqlite::Result { // ── Incremental classification ─────────────────────────────────────── -fn do_classify_incremental( - conn: &mut Connection, +pub(crate) fn do_classify_incremental( + conn: &Connection, changed_files: &[String], ) -> rusqlite::Result { - let tx = conn.transaction()?; + let tx = conn.unchecked_transaction()?; let mut summary = RoleSummary::default(); // Build placeholders for changed files diff --git a/src/domain/graph/builder/pipeline.ts b/src/domain/graph/builder/pipeline.ts index 4067b272..f450c76e 100644 --- a/src/domain/graph/builder/pipeline.ts +++ b/src/domain/graph/builder/pipeline.ts @@ -34,6 +34,7 @@ function initializeEngine(ctx: PipelineContext): void { engine: ctx.opts.engine || 'auto', dataflow: ctx.opts.dataflow !== false, ast: ctx.opts.ast !== false, + nativeDb: ctx.nativeDb, }; const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts); ctx.engineName = engineName as 'native' | 'wasm'; diff --git a/src/domain/graph/builder/stages/build-edges.ts b/src/domain/graph/builder/stages/build-edges.ts index 2869ada5..8aafcb91 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -673,15 +673,17 @@ export async function buildEdges(ctx: PipelineContext): Promise { // When using native edge insert, skip JS insert here — do it after tx commits. // Otherwise insert edges within this transaction for atomicity. - if (!native?.bulkInsertEdges) { + const useNativeEdgeInsert = !!(ctx.nativeDb?.bulkInsertEdges || native?.bulkInsertEdges); + if (!useNativeEdgeInsert) { batchInsertEdges(db, allEdgeRows); } }); computeEdgesTx(); // Phase 2: Native rusqlite bulk insert (outside better-sqlite3 transaction - // since rusqlite opens its own connection — avoids SQLITE_BUSY contention) - if (native?.bulkInsertEdges && allEdgeRows.length > 0) { + // to avoid SQLITE_BUSY contention). Prefer NativeDatabase persistent + // connection (6.15), fall back to standalone function (6.12). + if ((ctx.nativeDb?.bulkInsertEdges || native?.bulkInsertEdges) && allEdgeRows.length > 0) { const nativeEdges = allEdgeRows.map((r) => ({ sourceId: r[0], targetId: r[1], @@ -689,7 +691,12 @@ export async function buildEdges(ctx: PipelineContext): Promise { confidence: r[3], dynamic: r[4], })); - const ok = native.bulkInsertEdges(db.name, nativeEdges); + let ok: boolean; + if (ctx.nativeDb?.bulkInsertEdges) { + ok = ctx.nativeDb.bulkInsertEdges(nativeEdges); + } else { + ok = native!.bulkInsertEdges(db.name, nativeEdges); + } if (!ok) { debug('Native bulkInsertEdges failed — falling back to JS batchInsertEdges'); batchInsertEdges(db, allEdgeRows); diff --git a/src/domain/graph/builder/stages/build-structure.ts b/src/domain/graph/builder/stages/build-structure.ts index 212ce367..bb638806 100644 --- a/src/domain/graph/builder/stages/build-structure.ts +++ b/src/domain/graph/builder/stages/build-structure.ts @@ -89,8 +89,28 @@ export async function buildStructure(ctx: PipelineContext): Promise { try { let roleSummary: Record | null = null; - // Try native rusqlite path first (eliminates JS<->SQLite round-trips) - if (ctx.engineName === 'native') { + // Try NativeDatabase persistent connection first (6.15), then standalone (6.12) + if (ctx.nativeDb?.classifyRolesFull) { + const nativeResult = + changedFileList && changedFileList.length > 0 + ? ctx.nativeDb.classifyRolesIncremental(changedFileList) + : ctx.nativeDb.classifyRolesFull(); + if (nativeResult) { + roleSummary = { + entry: nativeResult.entry, + core: nativeResult.core, + utility: nativeResult.utility, + adapter: nativeResult.adapter, + dead: nativeResult.dead, + 'dead-leaf': nativeResult.deadLeaf, + 'dead-entry': nativeResult.deadEntry, + 'dead-ffi': nativeResult.deadFfi, + 'dead-unresolved': nativeResult.deadUnresolved, + 'test-only': nativeResult.testOnly, + leaf: nativeResult.leaf, + }; + } + } else if (ctx.engineName === 'native') { const native = loadNative(); if (native?.classifyRolesFull) { const dbPath = db.name; diff --git a/src/domain/graph/builder/stages/detect-changes.ts b/src/domain/graph/builder/stages/detect-changes.ts index 045f2e0b..045340ba 100644 --- a/src/domain/graph/builder/stages/detect-changes.ts +++ b/src/domain/graph/builder/stages/detect-changes.ts @@ -326,7 +326,13 @@ function purgeAndAddReverseDeps( ): void { const { db, rootDir } = ctx; if (changePaths.length > 0 || ctx.removed.length > 0) { - purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false }); + const filesToPurge = [...ctx.removed, ...changePaths]; + // Prefer NativeDatabase persistent connection for purge (6.15) + if (ctx.nativeDb?.purgeFilesData) { + ctx.nativeDb.purgeFilesData(filesToPurge, false); + } else { + purgeFilesFromGraph(db, filesToPurge, { purgeHashes: false }); + } } if (reverseDeps.size > 0) { const deleteOutgoingEdgesForFile = db.prepare( diff --git a/src/domain/graph/builder/stages/finalize.ts b/src/domain/graph/builder/stages/finalize.ts index a5b731b6..763f9c96 100644 --- a/src/domain/graph/builder/stages/finalize.ts +++ b/src/domain/graph/builder/stages/finalize.ts @@ -85,7 +85,7 @@ export async function finalize(ctx: PipelineContext): Promise { built_at: buildNow.toISOString(), node_count: String(nodeCount), edge_count: String(actualEdgeCount), - }).map(([key, value]) => ({ key, value })), + }).map(([key, value]) => ({ key, value: String(value) })), ); } else { setBuildMeta(db, { diff --git a/src/domain/graph/builder/stages/insert-nodes.ts b/src/domain/graph/builder/stages/insert-nodes.ts index da8c62c7..c6197a55 100644 --- a/src/domain/graph/builder/stages/insert-nodes.ts +++ b/src/domain/graph/builder/stages/insert-nodes.ts @@ -40,11 +40,13 @@ interface PrecomputedFileData { // ── Native fast-path ───────────────────────────────────────────────── function tryNativeInsert(ctx: PipelineContext): boolean { - const native = loadNative(); - if (!native?.bulkInsertNodes) return false; + // Prefer NativeDatabase persistent connection (6.15), fall back to standalone (6.12) + const hasNativeDb = !!ctx.nativeDb?.bulkInsertNodes; + const native = hasNativeDb ? null : loadNative(); + if (!hasNativeDb && !native?.bulkInsertNodes) return false; const { dbPath, allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx; - if (!dbPath) return false; + if (!hasNativeDb && !dbPath) return false; // Marshal allSymbols → InsertNodesBatch[] const batches: Array<{ @@ -139,7 +141,11 @@ function tryNativeInsert(ctx: PipelineContext): boolean { fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size }); } - return native.bulkInsertNodes(dbPath, batches, fileHashes, removed); + // Route through persistent NativeDatabase when available (6.15) + if (ctx.nativeDb?.bulkInsertNodes) { + return ctx.nativeDb.bulkInsertNodes(batches, fileHashes, removed); + } + return native!.bulkInsertNodes(dbPath!, batches, fileHashes, removed); } // ── JS fallback: Phase 1 ──────────────────────────────────────────── diff --git a/src/features/ast.ts b/src/features/ast.ts index 6edd428f..b92a9e4e 100644 --- a/src/features/ast.ts +++ b/src/features/ast.ts @@ -66,11 +66,28 @@ export async function buildAstNodes( db: BetterSqlite3Database, fileSymbols: Map, _rootDir: string, - _engineOpts?: unknown, + engineOpts?: { + nativeDb?: { + bulkInsertAstNodes( + batches: Array<{ + file: string; + nodes: Array<{ + line: number; + kind: string; + name: string; + text?: string | null; + receiver?: string | null; + }>; + }>, + ): number; + }; + }, ): Promise { // ── Native bulk-insert fast path ────────────────────────────────────── - const native = loadNative(); - if (native?.bulkInsertAstNodes) { + // Prefer NativeDatabase persistent connection (6.15), then standalone (6.12) + const nativeDb = engineOpts?.nativeDb; + const native = nativeDb ? null : loadNative(); + if (nativeDb?.bulkInsertAstNodes || native?.bulkInsertAstNodes) { let needsJsFallback = false; const batches: Array<{ file: string; @@ -103,7 +120,9 @@ export async function buildAstNodes( if (!needsJsFallback) { const expectedNodes = batches.reduce((s, b) => s + b.nodes.length, 0); - const inserted = native.bulkInsertAstNodes(db.name, batches); + const inserted = nativeDb + ? nativeDb.bulkInsertAstNodes(batches) + : native!.bulkInsertAstNodes(db.name, batches); if (inserted === expectedNodes) { debug(`AST extraction (native bulk): ${inserted} nodes stored`); return; diff --git a/src/types.ts b/src/types.ts index 51116293..b6d8f83e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -881,6 +881,8 @@ export interface EngineOpts { engine: EngineMode; dataflow: boolean; ast: boolean; + /** Persistent NativeDatabase connection for build writes (Phase 6.15). */ + nativeDb?: NativeDatabase; } /** A file change detected during incremental builds. */ @@ -1893,8 +1895,9 @@ export interface NativeParseTreeCache { clear(): void; } -/** Native rusqlite database wrapper instance (Phase 6.13). */ +/** Native rusqlite database wrapper instance (Phase 6.13 + 6.15). */ export interface NativeDatabase { + // ── Lifecycle (6.13) ──────────────────────────────────────────────── initSchema(): void; getBuildMeta(key: string): string | null; setBuildMeta(entries: Array<{ key: string; value: string }>): void; @@ -1903,6 +1906,78 @@ export interface NativeDatabase { close(): void; readonly dbPath: string; readonly isOpen: boolean; + + // ── Build pipeline writes (6.15) ─────────────────────────────────── + bulkInsertNodes( + batches: Array<{ + file: string; + definitions: Array<{ + name: string; + kind: string; + line: number; + endLine?: number | null; + visibility?: string | null; + children: Array<{ + name: string; + kind: string; + line: number; + endLine?: number | null; + visibility?: string | null; + }>; + }>; + exports: Array<{ name: string; kind: string; line: number }>; + }>, + fileHashes: Array<{ file: string; hash: string; mtime: number; size: number }>, + removedFiles: string[], + ): boolean; + bulkInsertEdges( + edges: Array<{ + sourceId: number; + targetId: number; + kind: string; + confidence: number; + dynamic: number; + }>, + ): boolean; + bulkInsertAstNodes( + batches: Array<{ + file: string; + nodes: Array<{ + line: number; + kind: string; + name: string; + text?: string | null; + receiver?: string | null; + }>; + }>, + ): number; + classifyRolesFull(): { + entry: number; + core: number; + utility: number; + adapter: number; + dead: number; + deadLeaf: number; + deadEntry: number; + deadFfi: number; + deadUnresolved: number; + testOnly: number; + leaf: number; + } | null; + classifyRolesIncremental(changedFiles: string[]): { + entry: number; + core: number; + utility: number; + adapter: number; + dead: number; + deadLeaf: number; + deadEntry: number; + deadFfi: number; + deadUnresolved: number; + testOnly: number; + leaf: number; + } | null; + purgeFilesData(files: string[], purgeHashes?: boolean): void; } // ════════════════════════════════════════════════════════════════════════