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

  1. Re-run the analyze — the aborted run left the crash-recovery dirty flag, so the next run performs a clean full rebuild
  2. If it reproduces on the same repo, capture the stack and report a GitNexus bug: a BasicBlock write bypassed the streaming sink
  3. 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
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/81344faeb9f21fdb. Report an issue: GitHub.