Summary
#1771 gave object-literal property-value references ({ resolve: someFn }-style dispatch tables) a real calls edge (dynamic=1, dynamicKind: 'value-ref') once invocation evidence is confirmed (#1895). The classifier's hasActiveFileSiblings+fanOut > 0 rescue in classifyUnreferencedNode (src/graph/classifiers/roles.ts) still has a documented, explicitly-acknowledged fallback for a different, not-yet-fixed value-reference shape: a bare function referenced as a logical-or default, e.g.
const fetchFn = options._fetchLatest || fetchLatestVersion;
(the real, pre-existing example is fetchLatestVersion in src/infrastructure/update-check.ts). This pattern still produces no edge at all — the function is only kept out of --role dead output via the heuristic rescue, not a real edge, unlike the object-literal case.
Why this now matters more
#2032 added a transitive-reachability check: a function/method is only exempted from the new downgrade pass if it's a confirmed-live root (exported, framework-dispatched, Commander dispatch, or a method-kind interface-dispatch rescue — see isLiveRoot/isInterfaceDispatchMethodRoot). Deliberately, a function-kind logical-or-fallback rescue is NOT treated as a root (see applyReachabilityDowngrade's doc comment) — promoting it to root status would silently rescue genuinely-dead intermediate functions, defeating #2032's own fix in the common case.
One side effect, confirmed on this repo's own self-build: fetchLatestVersion has fanIn === 0 itself (rescued only via the heuristic, not exported), so collectResponseBody — called only by fetchLatestVersion — is now flagged dead-unresolved, even though fetchLatestVersion is genuinely invoked in production via the logical-or pattern. A second, structurally identical case: resolveCalleeNameAndReceiverType in src/domain/graph/resolver/ts-resolver.ts, whose only caller visit is itself only rescued the same way.
Suggested fix
Extend the #1771/#1895 value-ref edge mechanism to also cover the logical-or-fallback shape (and the other shapes the existing comment already lists as not-yet-extracted: ternary defaults, array-of-functions elements, default parameter values) — give these a real calls edge from the enclosing scope once invocation evidence is confirmed, the same way object-literal property values now do. This removes the need for the fanOut > 0 heuristic fallback entirely for these shapes, and makes them properly participate in #2032's reachability check instead of silently depending on whichever function is directly downstream of them staying unflagged.
Needs the equivalent change in both src/extractors/*.ts (or wherever call-edge extraction lives for this pattern) and the mirrored Rust extractor, per this project's dual-engine parity invariant.
Known affected call sites (as of #2032)
src/infrastructure/update-check.ts: collectResponseBody (called only by fetchLatestVersion)
src/domain/graph/resolver/ts-resolver.ts: resolveCalleeNameAndReceiverType (called only by visit)
Related
Summary
#1771 gave object-literal property-value references (
{ resolve: someFn }-style dispatch tables) a realcallsedge (dynamic=1,dynamicKind: 'value-ref') once invocation evidence is confirmed (#1895). The classifier'shasActiveFileSiblings+fanOut > 0rescue inclassifyUnreferencedNode(src/graph/classifiers/roles.ts) still has a documented, explicitly-acknowledged fallback for a different, not-yet-fixed value-reference shape: a bare function referenced as a logical-or default, e.g.(the real, pre-existing example is
fetchLatestVersioninsrc/infrastructure/update-check.ts). This pattern still produces no edge at all — the function is only kept out of--role deadoutput via the heuristic rescue, not a real edge, unlike the object-literal case.Why this now matters more
#2032 added a transitive-reachability check: a
function/methodis only exempted from the new downgrade pass if it's a confirmed-live root (exported, framework-dispatched, Commander dispatch, or amethod-kind interface-dispatch rescue — seeisLiveRoot/isInterfaceDispatchMethodRoot). Deliberately, afunction-kind logical-or-fallback rescue is NOT treated as a root (seeapplyReachabilityDowngrade's doc comment) — promoting it to root status would silently rescue genuinely-dead intermediate functions, defeating #2032's own fix in the common case.One side effect, confirmed on this repo's own self-build:
fetchLatestVersionhasfanIn === 0itself (rescued only via the heuristic, not exported), socollectResponseBody— called only byfetchLatestVersion— is now flaggeddead-unresolved, even thoughfetchLatestVersionis genuinely invoked in production via the logical-or pattern. A second, structurally identical case:resolveCalleeNameAndReceiverTypeinsrc/domain/graph/resolver/ts-resolver.ts, whose only callervisitis itself only rescued the same way.Suggested fix
Extend the #1771/#1895 value-ref edge mechanism to also cover the logical-or-fallback shape (and the other shapes the existing comment already lists as not-yet-extracted: ternary defaults, array-of-functions elements, default parameter values) — give these a real
callsedge from the enclosing scope once invocation evidence is confirmed, the same way object-literal property values now do. This removes the need for thefanOut > 0heuristic fallback entirely for these shapes, and makes them properly participate in #2032's reachability check instead of silently depending on whichever function is directly downstream of them staying unflagged.Needs the equivalent change in both
src/extractors/*.ts(or wherever call-edge extraction lives for this pattern) and the mirrored Rust extractor, per this project's dual-engine parity invariant.Known affected call sites (as of #2032)
src/infrastructure/update-check.ts:collectResponseBody(called only byfetchLatestVersion)src/domain/graph/resolver/ts-resolver.ts:resolveCalleeNameAndReceiverType(called only byvisit)Related