{"record":{"id":"2f1795a83c685547","repo":"abhigyanpatwari/GitNexus","slug":"cannot-remove-relationship-relationshipid-it","errorCode":null,"errorMessage":"Cannot remove relationship \"${relationshipId}\": it has already been streamed to CSV and cannot be recalled. A phase that removes relationships must run before the GraphEmitSink is installed (see the parse-boundary construction in pipeline.ts).","messagePattern":"Cannot remove relationship \"(.+?)\": it has already been streamed to CSV and cannot be recalled\\. A phase that removes relationships must run before the GraphEmitSink is installed \\(see the parse-boundary construction in pipeline\\.ts\\)\\.","errorType":"exception","errorClass":"StreamedRelationshipRemovalError","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/lbug/graph-emit-sink.ts","lineNumber":659,"sourceCode":"   * Deliberately conservative. The dedup Set holds compact keys derived from a\n   * relationship's endpoints ({@link dedupKey}), and a bare id alone cannot be\n   * turned back into one — so a streamed edge is not directly identifiable here.\n   *\n   * Rather than risk the silent case (returning `false` for an edge that IS on\n   * disk and cannot be recalled), anything the real graph does not hold is\n   * treated as possibly-streamed once streaming has begun, and fails loudly. A\n   * genuinely-absent id therefore throws too, where the object-based graph would\n   * return `false`; that is acceptable because the only production caller is the\n   * COBOL resolver, which runs BEFORE the sink is armed and so takes the branch\n   * below.\n   *\n   * NOTE this diverges from {@link KnowledgeGraph.removeRelationship}, which\n   * returns `false` for an id it does not hold. Pinned by a test so the\n   * divergence stays deliberate.\n   */\n  removeRelationship(relationshipId: string): boolean {\n    if (this.real.removeRelationship(relationshipId)) return true;\n    if (this.srcIx.length > 0) throw new StreamedRelationshipRemovalError(relationshipId);\n    return false;\n  }\n}\n","sourceCodeStart":641,"sourceCodeEnd":663,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/core/lbug/graph-emit-sink.ts#L641-L663","documentation":"StreamedRelationshipRemovalError from GraphEmitSink (graph-emit-sink.ts:659): once streaming has begun (srcIx.length > 0), relationship rows may already be flushed to per-pair CSVs and cannot be recalled, so removeRelationship() throws instead of pretending to succeed. Phases that remove relationships must run before the sink is armed at the parse boundary (see pipeline.ts). This deliberately diverges from KnowledgeGraph.removeRelationship (which returns false for unknown ids); the only production caller, the COBOL resolver, runs before the sink is armed.","triggerScenarios":"Custom pipeline code or a plugin calling graph.removeRelationship(id) after the GraphEmitSink has been installed and streaming started — e.g. adding a late dedup/cleanup phase after the parse boundary, or refactoring the COBOL resolver so it runs after sink installation. Note a genuinely-absent id also throws here (any id is treated as possibly-streamed).","commonSituations":"Extending the ingestion pipeline with a post-emit normalization pass; reordering phases during a refactor; new code assuming the graph API behaves like the in-memory KnowledgeGraph (false return) instead of the streaming sink.","solutions":["Move the phase that removes relationships to before the GraphEmitSink is installed (the parse-boundary construction in pipeline.ts)","If dedup must happen late, filter/suppress relationship rows before emitting them instead of recalling them after the fact","If you only needed the boolean semantics of KnowledgeGraph.removeRelationship, perform removals against the real graph before wrapping it in the sink"],"exampleFix":"// before: removal after sink is armed\nconst sink = createGraphEmitSink(graph, dir);\nsink.graph.removeRelationship(relId); // throws\n// after: remove before installing the sink\ngraph.removeRelationship(relId);\nconst sink = createGraphEmitSink(graph, dir);","handlingStrategy":"validation","validationCode":"// Order phases so removals happen while the plain graph is still unwrapped\nconst graph = buildObjectGraph();\nresolveCobol(graph);            // any removeRelationship callers run HERE\ngraph.removeRelationship?.(staleId); // last-chance removal, still safe\nconst sink = createGraphEmitSink(graph, stagingDir); // sink armed only AFTER all removals\nawait runPipeline(sink);","typeGuard":null,"tryCatchPattern":"import { StreamedRelationshipRemovalError } from './core/lbug/graph-emit-sink.js';\ntry {\n  graph.removeRelationship(id);\n} catch (err) {\n  if (err instanceof StreamedRelationshipRemovalError) {\n    // phase-ordering bug: move this caller before sink installation in pipeline.ts; do not catch-and-ignore\n  }\n  throw err;\n}","preventionTips":["Treat sink installation as a one-way gate: no graph mutations that remove rows after it","When adding a resolution/dedup phase, place it before the parse-boundary sink construction in pipeline.ts","Do not rely on KnowledgeGraph.removeRelationship's false-return semantics once the sink wraps the graph"],"tags":["graph","emit-sink","pipeline-ordering","relationships","streaming"],"backgroundTag":"invalid-state-transition","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}