Summary
extractCallbackReferenceCalls (and its native mirror) unconditionally emits a dynamic call edge for every bare identifier argument passed to any call expression, with no gating on the callee — unlike member_expression args, which are gated by CALLBACK_ACCEPTING_CALLEES/HTTP_VERB_CALLEES (fixed in #1190). When the identifier's name happens to collide with an unrelated exported function elsewhere in the repo, the resolver's global-fallback confidence scoring (0.5, same-grandparent-dir heuristic) binds the two together, fabricating a call edge — and, if it closes a loop, a phantom architectural cycle.
This is not hypothetical: it just produced a false cycle in codegraph's own source during a Titan Paradigm SYNC/FORGE run.
Repro (on this repo)
src/features/communities.ts has:
function analyzeDrift(communities: CommunityObject[], communityDirs: ...): DriftResult {
...
const mergeCandidates = findMergeCandidates(communities); // line 136 — `communities` is a parameter, passed as a plain arg
...
}
export function communitiesData(...): Record<string, unknown> {
const { communities, communityDirs } = buildCommunityObjects(...);
const { splitCandidates, mergeCandidates, driftScore } = analyzeDrift(communities, communityDirs); // line 208 — `communities` is a local var, passed as a plain arg
...
}
Neither function contains a communities(...) call expression anywhere — communities is only ever a parameter/local-variable name here. But the only function named communities in the repo is the unrelated CLI entry point export function communities(...) in src/presentation/communities.ts:89.
codegraph cycles --functions -T --json reports:
analyzeDrift|src/features/communities.ts
-> communitiesData|src/features/communities.ts
-> communities|src/presentation/communities.ts
-> (back to analyzeDrift)
Querying the DB directly confirms both edges closing the loop are fabricated:
analyzeDrift -> communities kind=calls confidence=0.5 dynamic=1 technique=ts-native dynamic_kind=null
communitiesData -> communities kind=calls confidence=0.5 dynamic=1 technique=ts-native dynamic_kind=null
```//
while the real, correct edge (`presentation/communities.ts:communities` calling `features/communities.ts:communitiesData`) is confidence=1, dynamic=0 — and `features/communities.ts` has zero imports of, or references to, `presentation/communities.ts`. There is no real dependency cycle; the only thing closing the loop is the fabricated edge.
This is not an isolated case — the same `dynamic=1 AND confidence=0.5 AND kind='calls'` signature appears on **326 edges** in this repo's own graph, several of which look like the same pattern (e.g. `extractObjCSymbols` -> `ctx` in `src/cli/index.ts`, matched purely because some unrelated local var/param elsewhere is also named `ctx`).
## Root cause
- Rust: `extract_callback_reference_calls`, `crates/codegraph-core/src/extractors/javascript.rs:2280-2291` — the `"identifier"` arm unconditionally does `calls.push(Call { name: <ident>, dynamic: Some(true), receiver: None, .. })` for every identifier argument of any call expression.
- TS mirror: `extractCallbackReferenceCalls`, `src/extractors/javascript.ts:3377-3378` — `result.push({ name: child.text, line: callLine, dynamic: true })`, same lack of gating.
- The design trade-off is explicitly documented at `src/extractors/javascript.ts:3214-3216`: *"Identifier args ... are always emitted — the collateral damage of dropping them is larger than the FP risk, since plain identifier data args rarely collide with real function names."* That assumption doesn't hold here.
- Resolver: `compute_confidence`, `crates/codegraph-core/src/domain/graph/resolve.rs:291-292` — when a dynamic call has no receiver and no import match, it falls back to a same-grandparent-directory global match at confidence 0.5, which is what binds the stray `communities` identifier to the unrelated exported function.
## Impact
- Fabricates false architectural cycles (as seen here), which can send `/titan-sync` / `/titan-forge` down unnecessary refactor work fixing non-existent problems.
- `codegraph cycles` does not currently discriminate on edge `confidence`/`dynamic` — low-confidence speculative edges are weighted identically to statically-certain ones for cycle detection.
## Suggested fix
- Gate identifier-argument dynamic-call emission the same way `member_expression` args are gated (`CALLBACK_ACCEPTING_CALLEES`/`HTTP_VERB_CALLEES`), in both `javascript.rs` and `javascript.ts` — re-evaluate/replace the "collateral damage" trade-off now that we have a concrete counter-example.
- Separately (lower priority, may want its own issue): consider having `codegraph cycles`/`codegraph check --cycles` exclude or separately flag cycles whose only closing edge(s) are low-confidence dynamic (`confidence < 1 && dynamic = 1`), since these are speculative resolutions, not confirmed structural dependencies.
Not fixed here — this is a resolver/extractor change spanning both engines plus the 326 already-affected edges repo-wide, out of scope for the Titan phase (communities.ts cycle-break) that surfaced it.
Summary
extractCallbackReferenceCalls(and its native mirror) unconditionally emits adynamiccall edge for every bare identifier argument passed to any call expression, with no gating on the callee — unlikemember_expressionargs, which are gated byCALLBACK_ACCEPTING_CALLEES/HTTP_VERB_CALLEES(fixed in #1190). When the identifier's name happens to collide with an unrelated exported function elsewhere in the repo, the resolver's global-fallback confidence scoring (0.5, same-grandparent-dir heuristic) binds the two together, fabricating a call edge — and, if it closes a loop, a phantom architectural cycle.This is not hypothetical: it just produced a false cycle in codegraph's own source during a Titan Paradigm SYNC/FORGE run.
Repro (on this repo)
src/features/communities.tshas:Neither function contains a
communities(...)call expression anywhere —communitiesis only ever a parameter/local-variable name here. But the only function namedcommunitiesin the repo is the unrelated CLI entry pointexport function communities(...)insrc/presentation/communities.ts:89.codegraph cycles --functions -T --jsonreports:Querying the DB directly confirms both edges closing the loop are fabricated: