From 01dd21feaa03a70fd4a865ed3b56187bfe6a1253 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:31:49 -0700 Subject: [PATCH] fix: resolve three build bugs in builder.js (#287, #288, #289) - Normalize rootDir with path.resolve() at buildGraph entry to fix forward-slash paths on Windows from programmatic callers (#287) - Check for missing CFG/dataflow data in incremental no-op path and run pending analysis pass when --cfg or --dataflow was requested but never computed (#288) - Replace batch edgeCount accumulator with actual DB edge count for drift comparison and build_meta persistence, fixing false positive divergence warnings on incremental builds (#289) Impact: 1 functions changed, 11 affected Impact: 1 functions changed, 1 affected --- src/builder.js | 59 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/builder.js b/src/builder.js index c966de7d..6dd60f40 100644 --- a/src/builder.js +++ b/src/builder.js @@ -434,6 +434,7 @@ export function purgeFilesFromGraph(db, files, options = {}) { } export async function buildGraph(rootDir, opts = {}) { + rootDir = path.resolve(rootDir); const dbPath = path.join(rootDir, '.codegraph', 'graph.db'); const db = openDb(dbPath); initSchema(db); @@ -521,6 +522,48 @@ export async function buildGraph(rootDir, opts = {}) { } if (!isFullBuild && parseChanges.length === 0 && removed.length === 0) { + // Check if optional analysis was requested but never computed + const needsCfg = + opts.cfg && + (() => { + try { + return db.prepare('SELECT COUNT(*) as c FROM cfg_blocks').get().c === 0; + } catch { + return true; + } + })(); + const needsDataflow = + opts.dataflow && + (() => { + try { + return ( + db + .prepare( + "SELECT COUNT(*) as c FROM edges WHERE kind IN ('flows_to','returns','mutates')", + ) + .get().c === 0 + ); + } catch { + return true; + } + })(); + + if (needsCfg || needsDataflow) { + info('No file changes. Running pending analysis pass...'); + const analysisSymbols = await parseFilesAuto(files, rootDir, engineOpts); + if (needsCfg) { + const { buildCFGData } = await import('./cfg.js'); + await buildCFGData(db, analysisSymbols, rootDir, engineOpts); + } + if (needsDataflow) { + const { buildDataflowEdges } = await import('./dataflow.js'); + await buildDataflowEdges(db, analysisSymbols, rootDir, engineOpts); + } + closeDb(db); + writeJournalHeader(rootDir, Date.now()); + return; + } + // Still update metadata for self-healing even when no real changes if (metadataUpdates.length > 0) { try { @@ -911,7 +954,6 @@ export async function buildGraph(rootDir, opts = {}) { // Second pass: build edges _t.edges0 = performance.now(); - let edgeCount = 0; const buildEdges = db.transaction(() => { for (const [relPath, symbols] of fileSymbols) { // Skip barrel-only files — loaded for resolution, edges already in DB @@ -927,7 +969,6 @@ export async function buildGraph(rootDir, opts = {}) { if (targetRow) { const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports'; insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0); - edgeCount++; if (!imp.reexport && isBarrelFile(resolvedPath)) { const resolvedSources = new Set(); @@ -949,7 +990,6 @@ export async function buildGraph(rootDir, opts = {}) { 0.9, 0, ); - edgeCount++; } } } @@ -1050,7 +1090,6 @@ export async function buildGraph(rootDir, opts = {}) { seenCallEdges.add(edgeKey); const confidence = computeConfidence(relPath, t.file, importedFrom); insertEdge.run(caller.id, t.id, 'calls', confidence, isDynamic); - edgeCount++; } } @@ -1073,7 +1112,6 @@ export async function buildGraph(rootDir, opts = {}) { if (!seenCallEdges.has(recvKey)) { seenCallEdges.add(recvKey); insertEdge.run(caller.id, recvTarget.id, 'receiver', 0.7, 0); - edgeCount++; } } } @@ -1090,7 +1128,6 @@ export async function buildGraph(rootDir, opts = {}) { if (sourceRow) { for (const t of targetRows) { insertEdge.run(sourceRow.id, t.id, 'extends', 1.0, 0); - edgeCount++; } } } @@ -1106,7 +1143,6 @@ export async function buildGraph(rootDir, opts = {}) { if (sourceRow) { for (const t of targetRows) { insertEdge.run(sourceRow.id, t.id, 'implements', 1.0, 0); - edgeCount++; } } } @@ -1266,7 +1302,8 @@ export async function buildGraph(rootDir, opts = {}) { } const nodeCount = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c; - info(`Graph built: ${nodeCount} nodes, ${edgeCount} edges`); + const actualEdgeCount = db.prepare('SELECT COUNT(*) as c FROM edges').get().c; + info(`Graph built: ${nodeCount} nodes, ${actualEdgeCount} edges`); info(`Stored in ${dbPath}`); // Verify incremental build didn't diverge significantly from previous counts @@ -1278,11 +1315,11 @@ export async function buildGraph(rootDir, opts = {}) { const prevE = Number(prevEdges); if (prevN > 0) { const nodeDrift = Math.abs(nodeCount - prevN) / prevN; - const edgeDrift = prevE > 0 ? Math.abs(edgeCount - prevE) / prevE : 0; + const edgeDrift = prevE > 0 ? Math.abs(actualEdgeCount - prevE) / prevE : 0; const driftThreshold = config.build?.driftThreshold ?? 0.2; if (nodeDrift > driftThreshold || edgeDrift > driftThreshold) { warn( - `Incremental build diverged significantly from previous counts (nodes: ${prevN}→${nodeCount} [${(nodeDrift * 100).toFixed(1)}%], edges: ${prevE}→${edgeCount} [${(edgeDrift * 100).toFixed(1)}%], threshold: ${(driftThreshold * 100).toFixed(0)}%). Consider rebuilding with --no-incremental.`, + `Incremental build diverged significantly from previous counts (nodes: ${prevN}→${nodeCount} [${(nodeDrift * 100).toFixed(1)}%], edges: ${prevE}→${actualEdgeCount} [${(edgeDrift * 100).toFixed(1)}%], threshold: ${(driftThreshold * 100).toFixed(0)}%). Consider rebuilding with --no-incremental.`, ); } } @@ -1313,7 +1350,7 @@ export async function buildGraph(rootDir, opts = {}) { codegraph_version: CODEGRAPH_VERSION, built_at: new Date().toISOString(), node_count: nodeCount, - edge_count: edgeCount, + edge_count: actualEdgeCount, }); } catch (err) { warn(`Failed to write build metadata: ${err.message}`);