Skip to content

fix(resolution): stop silent edge loss when a same-key reference group straddles the resolution batch boundary#1266

Open
w0lan wants to merge 1 commit into
colbymchenry:mainfrom
w0lan:fix/batch-boundary-edge-loss
Open

fix(resolution): stop silent edge loss when a same-key reference group straddles the resolution batch boundary#1266
w0lan wants to merge 1 commit into
colbymchenry:mainfrom
w0lan:fix/batch-boundary-edge-loss

Conversation

@w0lan

@w0lan w0lan commented Jul 12, 2026

Copy link
Copy Markdown

Fixes #1265.

Summary

On codegraph index (the batched resolution path, default batchSize=5000),
edges are silently lost whenever one caller invokes the same resolvable target
from more call sites than fit in the remainder of a batch. The first batch
resolves its slice of the group and creates its edges — then deletes the
entire key group from unresolved_refs, including the rows a later batch
has not read yet. Those references vanish before resolution; their edges are
never created, and the index reports a clean, complete state (pending=0,
failed=0), so nothing signals the loss.

Found while validating the Elixir support PR on a production codebase: a test
module calling one helper 26 times produced only 17 calls edges on the
batched path, and the count changed with batch size. The bug itself is
language-agnostic and reproduces on clean v1.4.1 in TypeScript — full analysis,
deterministic reproduction, and loss table in #1265.

Fix

Key deleteSpecificResolvedReferences and markReferencesFailed on the full
call-site identity — (from_node_id, reference_name, reference_kind, line, col). line/col are NOT NULL in unresolved_refs, so plain equality
suffices; they already flow through the resolver (ref.original.line/column).
All three call-site paths updated (resolveAndPersist,
resolveAndPersistListYielding, resolveAndPersistBatched). A straddling
group's tail now stays pending and is resolved by the next batch, which
preserves the drain-termination invariant from #1187 (every processed row
still leaves the pending set) and composes with the #1240 failed-refs retry.

The retry path turned out to be affected too, not just latently: the 3-tuple
delete has no status filter, so resolveAndPersistListYielding resolving a
subset of a same-key group also wiped that key's still-pending rows (pre-fix
it dropped 8 of 10 remaining pending rows in the new test).

No supporting index added: the existing idx_unresolved_from_name
(from_node_id, reference_name) narrows the delete to the key group and
SQLite filters line/col within it; adding a composite index would cost a
schema-migration version bump for marginal gain outside pathological group
sizes. Happy to add it here or as a follow-up if you prefer.

Verification

  • Regression test __tests__/batch-boundary-edge-loss.test.ts, three
    scenarios: a 15-call-site same-key group with an injected batchSize=10
    (8/15 edges before the fix, 15/15 after); an exact-fit control (group fills
    the batch exactly; passes pre- and post-fix); and the retry-path scenario
    above. Each bug-pinning scenario was watched fail on the unfixed code first.
  • Full suite green, build/typecheck clean.
  • End-to-end: the K=8000 generator project from Resolver drops calls/references edges when a same-key reference group straddles the 5000-row resolution batch boundary #1265 on the rebuilt CLI
    produces 8000/8000 edges (4998 before), pending=0, failed=0.
  • The change also went through an independent external adversarial code review
    of the final diff (verdict: approve). It independently reproduced the
    regression test's red-on-revert and confirmed via grep that no other
    3-tuple-keyed mutation of unresolved_refs remains; its one non-blocking
    note is the composite-index follow-up mentioned above.

Impact

Rare but unbounded and invisible: ordinary code seldom has one function
calling one resolvable target thousands of times, but generated code, dispatch
tables, and repetitive test files do (observed in the field: 26→17 edges on a
production Elixir repo; synthetic: 3002 edges lost at K=8000) — and status
reports the index as complete throughout.

…top batch-boundary edge loss (colbymchenry#1265)

The batched resolver (resolveAndPersistBatched) reads pending refs in
offset-0 LIMIT/OFFSET pages and, after each batch, deleted the resolved
rows keyed only on (from_node_id, reference_name, reference_kind). But
edge identity is (source, target, kind, line, col) (idx_edges_identity),
so one caller invoking the same target from many call sites is many
distinct rows AND edges. When such a same-key group is larger than the
batch size, the 3-tuple delete wiped the whole group — including tail
rows a later batch had not read yet — silently dropping their edges while
the index still reported pending=0.

Key the delete (and, for symmetry, markReferencesFailed) on the full
call-site identity including line/col. line/col are NOT NULL in
unresolved_refs, so plain equality suffices. Updates all call sites
(resolveAndPersist, resolveAndPersistListYielding, resolveAndPersistBatched)
and the query key types.

Tests: the regression test covers three scenarios — the straddling
same-key group (edges preserved across the batch boundary), an exact-fit
control (group fills the batch exactly; passes pre- and post-fix), and
the retry-list path (resolveAndPersistListYielding resolving a subset of
a same-key group must not clobber the pending siblings it never touched).
Adds a CHANGELOG entry under [Unreleased].

Fixes colbymchenry#1265
@w0lan

w0lan commented Jul 12, 2026

Copy link
Copy Markdown
Author

Field confirmation on a production Elixir codebase (the repo where this was originally noticed): an ExUnit test module calling the same helper from 26 call sites had 17 edges before this fix and exactly 26 after (total edges +9, node count unchanged, pending=0). ExUnit modules are a natural trigger for this bug — describe/test are macros, so every call in the file attributes to the module node and shares one (from_node, name, kind) key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolver drops calls/references edges when a same-key reference group straddles the 5000-row resolution batch boundary

1 participant