Incremental Code Graph Update

Changed symbols are updated in place. Stable edges survive, stale outgoing call sites are removed, and LSP discovery is limited to the affected ranges. Full-file rebuilds are a fallback, not the normal path.

6
Symbol categories
2
Patch rounds
6
Severed-edge fields
O(V+E)
Range-index refresh
1

Classify Symbols

Match old graph symbols with the LSP outline and classify deleted, added, affected, shifted, unchanged, and invisible definitions.

change_mgr + patcher_base
2

Prepare Vertices

Remove deleted vertices, create additions, and update shifted or affected vertices in place so incoming edges remain attached.

subgraph_mgr
3

Invalidate Stale Edges

Drop affected outgoing anchors surgically when line counts match; otherwise clear that symbol's outgoing references.

subgraph_mgr
4

Connect + Reindex

Discover affected/added references, batch edge writes, then rebuild line-range indexes from the final graph.

patcher_base

change_mgr.py

Git-based change detection. Returns modified/added/deleted/renamed files and changed line ranges.

detect_changed_files() get_changed_line_ranges()

patcher_base.py

High-level orchestration. Classifies symbols, drives two patch rounds, handles fallback remap logic, and refreshes range indexes.

_classify_symbols(old, new) _remap_severed_edges()

subgraph_mgr.py

Core graph ops: deletion, reconstruction, edge reconnection, location matching.

delete_file_subgraph() rebuild_file_subgraph() match_location_to_vertex()

lsp_client.py

JSON-RPC wrapper for rust-analyzer, basedpyright, gopls, and typescript-language-server. C/C++ uses refreshed clangd .idx data.

document_symbol() references() semantic_tokens_range() definition()

graph_patcher.py

Public API router. Creates language-specific patcher instances.

patch_files(changed) detect_changed_files()

patcher_rust/python/ts/go/cpp

Language-specific subclasses. Override unified_name construction and token type filtering.

_build_unified_name() _get_crossfile_token_types()
1 / 5
Symbol Classification
class
field
method
1 / 5
Graph Update
File
Class/Struct
Method/Func
Field
Contain
Reference
Remapped
Discovered

Key Algorithms

Three algorithms underpin the incremental update system.

1
unified_name — language-agnostic symbol identity

Every vertex stores a unified_name (file path + dotted symbol path, no line number). This enables matching old vertices to new ones even when lines shift. Vertex names are unified_name + ":" + start_line.

Rust
impl Checker { fn check() }
→ parent: "Checker", name: "check"
→ "checker.rs:Checker.check()"

impl Violation for EqWithoutHash
→ "checker.rs:EqWithoutHash<Violation>"
Rust module (transparent)
mod tests { fn test_new() }
→ kind=2 Module is SKIPPED in path
→ "lib.rs:test_new()"
(matches SCIP decoder output)
Python
class Checker: def check(self)
→ "checker.py:Checker.check()"
TypeScript
class Router { handle(req) }
→ "router.ts:Router.handle()"

2
Two-level location matching

LSP returns file:line:character positions. Match an exact symbol start first, then fall back to the innermost symbol span containing the line.

LSP → checker.rs:15:0
Level 1
exact start_line == 15
✓ Checker.check():15
exact match
LSP → linter.rs:45:12
Level 2
no exact match
(inside body)
find smallest range
containing line 45
✓ Linter.run()
(L40–L60)

3
Severed-edge remap contract

A full-file rebuild or fallback records one six-field entry per call-site anchor: (src_name, tgt_name, src_uname, tgt_uname, anchor_file, anchor_line). The ordinary affected-symbol path updates the vertex in place and does not need this remap.

FOR EACH severed six-tuple:
  IF INCOMING (source is external):
    → resolve renamed source by src_uname if needed
    → match rebuilt target by tgt_uname
    → REMAP with the recorded/rebased anchor

  IF OUTGOING (source was rebuilt):
    IF source body is still trustworthy:
      → REMAP, preserving each distinct call site
    OTHERWISE:
      → SKIP and rediscover with LSP

4
Range-query visibility

Every successful patch batch ends with build_range_indexes(). The refresh scans the final vertices and anchored edges in O(V + E), so query_range() sees added, moved, and removed spans immediately. C/C++ upholds the same contract after its .idx refresh.