Revert "fix(resolution): filter stale-target edges so watch sync survives FK violations (#455) (#463)"

This reverts commit 1dfaf30a8b.

Switching to #462's approach — a single, lower-layer filter inside
QueryBuilder.insertEdges itself instead of three filters spread across
the resolution layer. The DB-layer filter protects every caller (current
and future) automatically and doesn't depend on the queries-layer
nodeCache invalidation staying perfect. See #455 for the bug.

The CHANGELOG entry for the user-facing fix is re-added on top of #462.
This commit is contained in:
Colby McHenry
2026-05-26 13:08:56 -05:00
parent 1dfaf30a8b
commit 028d25f3af
4 changed files with 6 additions and 276 deletions
+2 -15
View File
@@ -812,19 +812,6 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
seen.add(key);
merged.push(e);
}
if (merged.length > 0) {
// Defense-in-depth (issues #42, #455): drop edges whose source/target
// no longer resolves to a real node. Some channel maps cache native
// Node refs across a resolver lifetime (WeakMap-keyed by context), so
// a file rewrite between map build and synthesis can leave stale IDs
// here. One FK violation aborts the whole batch — better to skip the
// dead edges and emit the rest than lose every synthesized edge.
const allIds = new Set<string>();
for (const e of merged) { allIds.add(e.source); allIds.add(e.target); }
const existing = queries.getNodesByIds([...allIds]);
const validEdges = merged.filter((e) => existing.has(e.source) && existing.has(e.target));
if (validEdges.length > 0) queries.insertEdges(validEdges);
return validEdges.length;
}
return 0;
if (merged.length > 0) queries.insertEdges(merged);
return merged.length;
}
+4 -28
View File
@@ -607,28 +607,6 @@ export class ReferenceResolver {
});
}
/**
* Defense-in-depth: drop edges whose source or target is no longer in
* the nodes table. PR #62 (issue #42) applied this filter at the
* extraction-layer `insertEdges` site; #455 reports the same
* `FOREIGN KEY constraint failed` reappearing here at the
* resolution-layer site during watch sync, where a resolver lookup that
* crosses a framework-specific cache can hand us a target whose node
* was removed by a concurrent file rewrite. One batched, cache-aware
* `getNodesByIds` query is enough to skip those edges quietly instead
* of aborting the whole sync.
*/
private filterEdgesByExistingNodes(edges: Edge[]): Edge[] {
if (edges.length === 0) return edges;
const allIds = new Set<string>();
for (const e of edges) {
allIds.add(e.source);
allIds.add(e.target);
}
const existing = this.queries.getNodesByIds([...allIds]);
return edges.filter((e) => existing.has(e.source) && existing.has(e.target));
}
/**
* Resolve and persist edges to database
*/
@@ -642,9 +620,8 @@ export class ReferenceResolver {
const edges = this.createEdges(result.resolved);
// Insert edges into database
const validEdges = this.filterEdgesByExistingNodes(edges);
if (validEdges.length > 0) {
this.queries.insertEdges(validEdges);
if (edges.length > 0) {
this.queries.insertEdges(edges);
}
// Clean up resolved refs from unresolved_refs table so metrics are accurate
@@ -691,9 +668,8 @@ export class ReferenceResolver {
// Persist edges immediately
const edges = this.createEdges(result.resolved);
const validEdges = this.filterEdgesByExistingNodes(edges);
if (validEdges.length > 0) {
this.queries.insertEdges(validEdges);
if (edges.length > 0) {
this.queries.insertEdges(edges);
}
// Clean up resolved refs so they don't appear in the next batch