Skip to content

Incremental index silently drops all inbound CALLS edges when only the callee's file changes #1988

Description

@mayangzz

Summary

When a file containing a callee is modified, incremental indexing purges every inbound CALLS edge pointing at symbols in that file, then only re-parses the changed file itself. The call sites living in other (unchanged) files are never re-parsed, so those edges are lost permanently. trace_path(direction="inbound") then silently returns an incomplete — often empty — caller list.

force: true does not help; only deleting the .db and re-indexing restores the edges.

Environment

  • codebase-memory-mcp 0.8.1, macOS (darwin/arm64)
  • Language: Go (also expected to affect other languages, not verified)

Minimal reproduction

/tmp/cbm-repro
├── go.mod                 module example.com/repro
├── main.go                func main() { _ = pkg.Target("from main") }
└── pkg
    ├── target.go          func Target(s string) string { return s + "!" }
    ├── caller_a.go        func CallerA() string { return Target("a") }
    ├── caller_b.go        func CallerB() string { return Target("b") }
    │                      func CallerC() string { return Target("c") }
    └── caller_d.go        func CallerD() string { return Target("d") }
  1. codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-repro"}'
  2. codebase-memory-mcp cli trace_path '{"project":"private-tmp-cbm-repro","function_name":"Target","direction":"inbound","depth":1}'
    correct: CallerA, CallerB, CallerC, CallerD, main (5 callers)
  3. Edit only pkg/target.go (e.g. change the function body / a comment — signature unchanged). Do not touch any caller file.
  4. Re-run index_repository (incremental path), then repeat the trace_path from step 2.

Actual: {"function":"Target","direction":"inbound","callers":[]} — all 5 callers gone.
Expected: the same 5 callers.

Adding a new caller file works fine (that file is re-parsed), so the bug is specific to inbound edges of a changed callee file.

Relevant log lines from step 4

incremental.classify changed=1 unchanged=5 deleted=0 mode_skipped=0
incremental.reparse files=1
incremental.purge elapsed_ms=0
pass.done pass=definitions defs=2 calls=0 imports=0 errors=0
pass.done pass=calls total=0 resolved=0 unresolved=0 errors=0
incremental.done elapsed_ms=18

purge removes the edges; pass=calls total=0 shows nothing is rebuilt, because the only re-parsed file contains no calls to Target.

Impact on a real repository

On a ~3.6k-node Go repo that had been incrementally indexed for a few weeks:

query stale index after deleting the .db and re-indexing grep ground truth
trace_path(SendBotMessage, inbound) 2 callers 9 callers 13 call sites across exactly those 9 functions
trace_path(chatKey, inbound) 2 callers 6 callers 14 call sites across exactly those 6 functions

Edge count went from 9,401 to 10,446 on a full rebuild with essentially the same node count (3,605 → 3,603), i.e. ~10% of edges had been silently dropped over time.

The dangerous part is that it fails silently: impact analysis looks like it succeeded and simply reports fewer callers, so a caller can easily be missed during a refactor.

Suggested fix

When purging inbound edges for symbols in a changed file, also re-parse (or at least re-resolve calls for) the files that previously had edges into it — the pre-purge edge set already identifies exactly which files those are.

Workaround

rm ~/.cache/codebase-memory-mcp/<project-slug>.db
codebase-memory-mcp cli index_repository '{"repo_path":"<repo>"}'

Metadata

Metadata

Assignees

No one assigned

    Labels

    parsing/qualityGraph extraction bugs, false positives, missing edges

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions