Split out of the #678 review, where it surfaced as the thing an eviction receipt gate was detecting but could not repair.
What happens
A file-scoped eviction deletes every edge touching a doomed node, including edges whose SOURCE is an unchanged file. Before the eviction, restubIncomingRefs parks the incoming edges it can, but it is gated on IsResolvableRefEdge:
// internal/graph/stub.go
case EdgeCalls, EdgeReferences, EdgeReads, EdgeWrites,
EdgeTypedAs, EdgeReturns, EdgeInstantiates,
EdgeImplements, EdgeExtends, EdgeComposes:
Every other kind is destroyed outright. On a real Go package that means accesses_field, arg_of and tests edges, in quantity: the #678 review instrumented the eviction on a flat copy of internal/graph and counted accesses_field alone in the dozens per evicted file. Independently of the exact count, 24 of 25 single-file reindexes of that tree had at least one surviving-source edge of a non-restubbed kind, so this is the common shape rather than the exceptional one.
Nothing recreates them. Resolution retargets edges that still exist and are parked under a stub, so a deleted edge is out of its reach, and the edge only comes back when the SOURCE file is next reparsed for its own reasons. Editing b.go therefore silently degrades queries anchored on the unchanged a.go.
Reproduction
TestResolveAllDoesNotRestoreAnEvictionDestroyedCapabilityEdge in internal/resolver/eviction_edge_loss_test.go is the minimal case, and lands with #678:
a.go::Caller --accesses_field--> b.go::T.F, resolved.
- Reindex
b.go: evict the file, re-add the identical definition.
- The edge is gone, and a subsequent
ResolveAll does not bring it back.
The test asserts the loss, so it will start failing when this is fixed - that is deliberate, and the fix should invert it.
Why it is filed rather than fixed in place
The obvious fix, widening restubIncomingRefs to park these kinds, does not work as-is. Those kinds are not name-resolvable, so parking them under unresolved::<Name> mints stubs that nothing will ever rebind, trading silent edge loss for a growing population of permanently pending edges. Making them resolvable is a resolver-semantics change with its own ambiguity and cost questions - accesses_field targets field names, which collide heavily.
Directions worth weighing:
- Re-derive the affected edges for surviving sources as part of the eviction, from the surviving side's existing parse, rather than parking or resolving them.
- Give the non-resolvable kinds their own parked form with a targeted rebind pass keyed on the doomed node identity rather than on a name.
- Record the destroyed edges in the mutation so a consumer can decide to reparse the affected sources.
Scope note
This is not a receipt-exactness problem. The eviction's resolution delta stays exactly describable, because the edge is equally absent whichever pass runs afterwards. #678 originally failed the receipt closed on this shape, which forced a whole-graph resolve that reaches an identical graph; that gate has been removed and the damage tracked here instead.
Split out of the #678 review, where it surfaced as the thing an eviction receipt gate was detecting but could not repair.
What happens
A file-scoped eviction deletes every edge touching a doomed node, including edges whose SOURCE is an unchanged file. Before the eviction,
restubIncomingRefsparks the incoming edges it can, but it is gated onIsResolvableRefEdge:Every other kind is destroyed outright. On a real Go package that means
accesses_field,arg_ofandtestsedges, in quantity: the #678 review instrumented the eviction on a flat copy ofinternal/graphand countedaccesses_fieldalone in the dozens per evicted file. Independently of the exact count, 24 of 25 single-file reindexes of that tree had at least one surviving-source edge of a non-restubbed kind, so this is the common shape rather than the exceptional one.Nothing recreates them. Resolution retargets edges that still exist and are parked under a stub, so a deleted edge is out of its reach, and the edge only comes back when the SOURCE file is next reparsed for its own reasons. Editing
b.gotherefore silently degrades queries anchored on the unchangeda.go.Reproduction
TestResolveAllDoesNotRestoreAnEvictionDestroyedCapabilityEdgeininternal/resolver/eviction_edge_loss_test.gois the minimal case, and lands with #678:a.go::Caller --accesses_field--> b.go::T.F, resolved.b.go: evict the file, re-add the identical definition.ResolveAlldoes not bring it back.The test asserts the loss, so it will start failing when this is fixed - that is deliberate, and the fix should invert it.
Why it is filed rather than fixed in place
The obvious fix, widening
restubIncomingRefsto park these kinds, does not work as-is. Those kinds are not name-resolvable, so parking them underunresolved::<Name>mints stubs that nothing will ever rebind, trading silent edge loss for a growing population of permanently pending edges. Making them resolvable is a resolver-semantics change with its own ambiguity and cost questions -accesses_fieldtargets field names, which collide heavily.Directions worth weighing:
Scope note
This is not a receipt-exactness problem. The eviction's resolution delta stays exactly describable, because the edge is equally absent whichever pass runs afterwards. #678 originally failed the receipt closed on this shape, which forced a whole-graph resolve that reaches an identical graph; that gate has been removed and the damage tracked here instead.