abhigyanpatwari/GitNexus · critical
Streaming PDG manifest collides with a structural node CSV f
Error message
Streaming PDG manifest collides with a structural node CSV for "${table}" — the in-memory graph should hold zero ${table} nodes when streaming. A ${table} node leaked into the graph during a streamed emit. What it means
Invariant guard in loadGraphToLbug's mergeManifestNodeFiles (gitnexus/src/core/lbug/lbug-adapter.ts:1151). When an analyze runs with the streaming PDG sink (#2202), BasicBlock nodes are routed straight to CSV on disk and the in-memory KnowledgeGraph must hold zero of them. At load time the streamed manifest is merged into the node-COPY plan; if the structural emit (streamAllCSVsToDisk) ALSO produced a CSV for the same table, a BasicBlock leaked into the real graph during the streamed emit. The run fails loudly instead of double-COPYing or silently dropping rows.
Source
Thrown at gitnexus/src/core/lbug/lbug-adapter.ts:1151
// The single writable connection (LadybugDB is single-writer). Captured as a
// const so the node-COPY closure has a non-null reference — TS cannot narrow
// the reassignable module-level `conn` across the callback boundary.
const writeConn = conn;
const validTables = new Set<string>(NODE_TABLES as readonly string[]);
// Merge the streamed PDG-emit node CSVs (#2202) into a node-file map. Collision
// guard: a BasicBlock in the in-memory graph during a streamed run is an
// invariant violation (streamAllCSVsToDisk would also emit basicblock.csv), so
// fail loudly rather than drop rows (#2202 review #3). Runs at the node-phase
// boundary so the manifest BasicBlock table COPYs with the structural CSVs.
const mergeManifestNodeFiles = (
nodeFilesMap: Map<NodeTableName, { csvPath: string; rows: number }>,
): void => {
if (!pdgEmitManifest) return;
for (const [table, meta] of pdgEmitManifest.nodeFiles) {
if (nodeFilesMap.has(table)) {
throw new Error(
`Streaming PDG manifest collides with a structural node CSV for "${table}" — ` +
`the in-memory graph should hold zero ${table} nodes when streaming. ` +
`A ${table} node leaked into the graph during a streamed emit.`,
);
}
nodeFilesMap.set(table, meta);
}
};
// Node COPY is the only DB write that can overlap relationship CSV emit: the
// rel pass writes new rel_*.csv files and never touches `conn`, while node COPY
// uses `conn` and never touches the rel files. We start node COPY at the
// node-phase boundary and let the rel pass run concurrently — the only
// single-writer-safe parallelism (#2203). The rel COPY still waits for node
// COPY (FK precondition), so the DB load order is unchanged.
let nodeCopyPromise: Promise<void> | undefined;
let nodeCopyError: unknown;
const beginNodeCopy = (View on GitHub (pinned to aac7515d2a)
Solutions
- Re-run the analyze — the aborted run left the crash-recovery dirty flag, so the next run performs a clean full rebuild
- If it reproduces on the same repo, capture the stack and report a GitNexus bug: a BasicBlock write bypassed the streaming sink
- If you maintain emit code, route every BasicBlock addNode through the PdgEmitSink façade so it lands in the streamed CSV, never in the in-memory graph
- As a fork mitigation, run the emit without the streaming sink so the whole-graph path owns the BasicBlock CSV
Example fix
// before — writes a BasicBlock into the real graph during a streamed emit realGraph.addNode(basicBlockNode); // after — route it through the sink so it lands in the streamed basicblock.csv pdgSink.addNode(basicBlockNode);
Defensive patterns
Strategy: try-catch
Validate before calling
// before loadGraphToLbug, when a streamed manifest is present:
const leaked = [...graph.iterNodes()].filter((n) => n.label === 'BasicBlock');
if (pdgEmitManifest && leaked.length > 0) {
throw new Error(
`emit bug: ${leaked.length} BasicBlock(s) bypassed the streaming sink`,
);
} Type guard
const isPdgManifestNodeCollision = (e: unknown): boolean =>
e instanceof Error &&
e.message.includes('Streaming PDG manifest collides with a structural node CSV'); Try / catch
try {
await loadGraphToLbug(graph, repoPath, storagePath, onProgress, manifest);
} catch (e) {
if (isPdgManifestNodeCollision(e)) {
// invariant bug — do NOT retry the same graph; force a clean full rebuild
await markIndexDirtyAndRebuild(repoPath);
}
throw e;
} Prevention
- Never add BasicBlock nodes to the real KnowledgeGraph during a streamed --pdg emit — route all writes through the PdgEmitSink façade
- Register every new PDG edge type in PdgEmitSink's PDG_EDGE_TYPES so it streams instead of landing in memory
- Run the #2202 differential fingerprint test after any change to emit routing
When it happens
Trigger: Calling loadGraphToLbug(graph, ..., pdgEmitManifest) where graph still contains BasicBlock nodes — i.e. some emit path added a BasicBlock directly to the real KnowledgeGraph instead of routing it through the PdgEmitSink façade (addNode on the unwrapped graph, a new PDG emit phase that bypasses the sink, or a regression in how runScopeResolution threads the sink into per-language passes).
Common situations: Upgrading GitNexus to a version where a new or changed PDG emit path writes BasicBlocks outside PdgEmitSink; forked emit loops that pass the real graph; scale large enough that streaming activates and exposes routing that was harmless in whole-graph mode.
Related errors
- Streaming PDG manifest collides with a structural relationsh
- PdgEmitSink: ${errors.length} streamed CSV writer(s) hit an
- Connection pool integrity error: expected ${MAX_CONNS_PER_RE
- No source files found in the knowledge graph. Nothing to doc
- content filter triggered mid-stream. The generated content w
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/81344faeb9f21fdb.
Report an issue: GitHub.