From 53db780322f96480b11777119fe15f1b54238a62 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 20 Jun 2026 19:34:47 -0600 Subject: [PATCH 1/3] fix: purge dataflow rows keyed by call_edge_id before edge deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dataflow table has a call_edge_id column that REFERENCES edges(id). When a file is deleted during an incremental rebuild, purgeFilesData deleted dataflow rows matched by source_id/target_id and via dataflow_vertices, but missed rows whose call_edge_id pointed to a calls edge touching the deleted file — where the dataflow row's own source_id/target_id belonged to other files (cross-file inter-procedural stitching). With better-sqlite3's default PRAGMA foreign_keys = ON, the subsequent edge deletion failed silently, leaving stale nodes behind and causing a node count mismatch after file deletion in incremental builds. Fix: add a dataflowByCallEdge purge step in preparePurgeStmts that deletes dataflow rows referencing edges that touch the deleted file, executed immediately before the edges delete. The call_edge_id column is optional (added in schema v18), so the statement is wrapped in tryPrepare and invoked with ?. on the result. Closes #1645 Impact: 3 functions changed, 8 affected --- src/db/repository/build-stmts.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/db/repository/build-stmts.ts b/src/db/repository/build-stmts.ts index 4ba717ff5..05d62943e 100644 --- a/src/db/repository/build-stmts.ts +++ b/src/db/repository/build-stmts.ts @@ -6,6 +6,7 @@ interface PurgeStmts { cfgBlocks: SqliteStatement | null; dataflow: SqliteStatement | null; dataflowByVertex: SqliteStatement | null; + dataflowByCallEdge: SqliteStatement | null; dataflowSummary: SqliteStatement | null; dataflowVertices: SqliteStatement | null; complexity: SqliteStatement | null; @@ -51,6 +52,16 @@ function preparePurgeStmts(db: BetterSqlite3Database): PurgeStmts { `DELETE FROM dataflow WHERE source_vertex IN (SELECT id FROM dataflow_vertices WHERE func_id IN (SELECT id FROM nodes WHERE file = ?)) OR target_vertex IN (SELECT id FROM dataflow_vertices WHERE func_id IN (SELECT id FROM nodes WHERE file = ?))`, ), + // Delete dataflow rows whose call_edge_id references a calls edge that + // touches the deleted file (source or target). These rows are not caught by + // the source_id/target_id or vertex-based deletions above when the dataflow + // row's own nodes live in other files. Must run before the edges delete to + // avoid SQLITE_CONSTRAINT_FOREIGNKEY: dataflow.call_edge_id REFERENCES edges(id). + dataflowByCallEdge: tryPrepare( + `DELETE FROM dataflow WHERE call_edge_id IN + (SELECT id FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) + OR target_id IN (SELECT id FROM nodes WHERE file = @f))`, + ), dataflowSummary: tryPrepare( 'DELETE FROM dataflow_summary WHERE func_id IN (SELECT id FROM nodes WHERE file = ?)', ), @@ -94,6 +105,9 @@ function runPurge(stmts: PurgeStmts, file: string, opts: PurgeOpts = {}): void { stmts.cfgBlocks?.run(file); stmts.dataflow?.run(file, file); stmts.dataflowByVertex?.run(file, file); + // Clear dataflow rows keyed by call_edge_id before deleting edges so the FK + // (dataflow.call_edge_id REFERENCES edges(id)) does not block the edge purge. + stmts.dataflowByCallEdge?.run({ f: file }); stmts.dataflowSummary?.run(file); stmts.dataflowVertices?.run(file); stmts.complexity?.run(file); From 391eb155b19d4b24a810106aaae40765482c3729 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 20 Jun 2026 21:46:37 -0600 Subject: [PATCH 2/3] fix: restore PRAGMA foreign_keys = ON after buildGraph and remove stale JSDoc (#1662) - Narrow FK-off scope in native-orchestrator: restore PRAGMA foreign_keys = ON immediately after nativeDb.buildGraph() returns instead of relying on connection close, so post-build writes retain FK protection - Remove orphaned JSDoc block before runInterproceduralStitch that described the old buildInterproceduralStitch wrapper semantics; the correct JSDoc for the internal helper was already present Impact: 1 functions changed, 5 affected --- .../graph/builder/stages/native-orchestrator.ts | 14 ++++++++++++-- src/features/dataflow.ts | 9 --------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index 12442f620..e456c5eea 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -2046,8 +2046,9 @@ export async function tryNativeOrchestrator( // nodes/edges during incremental builds, so FK enforcement causes the purge // statements to fail silently — leaving stale nodes and edges that then get // duplicated when the barrel-candidate re-parse re-inserts them (issue #1644). - // Disabling FK before buildGraph() lets the purge succeed. FK enforcement is - // restored automatically when this connection is closed after the build. + // Disable FK only for the duration of the Rust-side buildGraph() call, then + // restore it immediately so subsequent writes on this connection retain FK + // protection. try { ctx.nativeDb.exec('PRAGMA foreign_keys = OFF'); } catch { @@ -2060,6 +2061,15 @@ export async function tryNativeOrchestrator( JSON.stringify(ctx.aliases), JSON.stringify(ctx.opts), ); + + // Restore FK enforcement immediately after buildGraph() so any subsequent + // writes to this connection (gap-repair, structure patch) retain FK protection. + try { + ctx.nativeDb.exec('PRAGMA foreign_keys = ON'); + } catch { + // safe to ignore on very old addon versions + } + const result = JSON.parse(resultJson) as NativeOrchestratorResult; if (result.earlyExit) { diff --git a/src/features/dataflow.ts b/src/features/dataflow.ts index 204d15201..9c1c8ef70 100644 --- a/src/features/dataflow.ts +++ b/src/features/dataflow.ts @@ -468,15 +468,6 @@ function buildDataflowVerticesAndEdges( // ── P2: interprocedural stitching ───────────────────────────────────────────── -/** - * Post-pass: connect arg-flow candidates to vertex-level inter-procedural edges. - * Runs after all per-file vertices + summaries have been committed. - * - * For each resolved argFlow (A calls B with arg x → B.param[j]): - * - Emits 'arg_in' inter edge: A's source vertex → B.param[j] vertex - * - If B's summary shows B.param[j] reaches B's return: emits 'return_out' - * inter edge: B.return → A's capture local (if any) - */ /** * Core stitch logic — must be called inside an already-open transaction. * From 62f91e81e94dc1150c551dd3ea557d81877fadf0 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 20 Jun 2026 22:30:17 -0600 Subject: [PATCH 3/3] fix: wrap buildGraph() in try/finally to guarantee FK restore on throw (#1662) Impact: 1 functions changed, 5 affected --- .../builder/stages/native-orchestrator.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index e456c5eea..274d9cb40 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -2055,19 +2055,23 @@ export async function tryNativeOrchestrator( // exec may not exist on very old addon versions — safe to ignore } - const resultJson = ctx.nativeDb.buildGraph( - ctx.rootDir, - JSON.stringify(ctx.config), - JSON.stringify(ctx.aliases), - JSON.stringify(ctx.opts), - ); - - // Restore FK enforcement immediately after buildGraph() so any subsequent - // writes to this connection (gap-repair, structure patch) retain FK protection. + let resultJson: string; try { - ctx.nativeDb.exec('PRAGMA foreign_keys = ON'); - } catch { - // safe to ignore on very old addon versions + resultJson = ctx.nativeDb.buildGraph( + ctx.rootDir, + JSON.stringify(ctx.config), + JSON.stringify(ctx.aliases), + JSON.stringify(ctx.opts), + ); + } finally { + // Restore FK enforcement so any subsequent writes to this connection + // (gap-repair, structure patch) retain FK protection — even if buildGraph() + // throws. + try { + ctx.nativeDb.exec('PRAGMA foreign_keys = ON'); + } catch { + // safe to ignore on very old addon versions + } } const result = JSON.parse(resultJson) as NativeOrchestratorResult;