Engineering·Context graph infrastructure, part 1 of 3
Code changes. Context graphs don't.
Why context graphs go stale, what stale context actually costs, and how lgraph update keeps the graph at ROOT for a fraction of the cost of rebuilding it from scratch.
A map that stops matching the territory
A context graph is a living map of how a codebase fits together: dependency edges, call chains, module summaries, implicit runtime couplings. But codebases move fast — features land, files get renamed, modules split — so a graph accurate at ROOT~50 silently misrepresents ROOT, and because nothing updates it on its own, the gap widens with every commit. What makes this worse than ordinary documentation rot is who's reading: a developer sees a stale wiki's date and applies skepticism, but an LLM has no such signal. It takes old edges and summaries as ground truth, so a stale graph doesn't just fail — it misleads, with full confidence.
That failure lands in three distinct ways:
C-01
Wasted compute
The model reasons over stale chains and deleted abstractions, then re-orients from scratch every session — the larger the repo and the longer the gap, the more tokens burned.
C-02
Hallucinated behavior
When the graph claims Module A still depends on Module B but B was refactored away, the model fills the gap with plausible-but-fabricated behavior — an input failure, not a model one.
C-03
Wasted developer time
A developer chases a fix for thirty minutes before finding the call path the model suggested no longer exists, then starts the session over.
You could regenerate the whole graph after every change, but nobody does — building it runs LLM analysis across the entire repo, and that costs real dollars and real minutes. At the pace a team actually commits, rebuilding on every merge would cost far more than the graph is worth. So the update has to work from the diff: re-analyze what changed, and leave everything else exactly as it was.
SolutionUpdate only what changed
lgraph update does not re-index the codebase. It computes a precise diff between the last indexed commit and ROOT, then drives each pipeline phase with only the changed surface. Unchanged files reuse their cached results exactly. The result is a graph that reflects ROOT, produced at a fraction of the cost of a full run.
The key insight
No phase needs to see the whole codebase to stay current — only what changed, and what those changes touched.
The incremental update pipeline
Foundation
Git diff detection
commit-hash delta
Phase 1
Explicit deps
content-hash cache
Phase 2
Implicit deps
threshold 35%
Phase 3
File enrichment
threshold 60%
Phase 4
Wiki
threshold 50%
Phase 1 — Explicit dependencies: content hashing
The explicit dependency analyzer maintains a per-file SHA-256 content hash, computed on whitespace-normalized source (CRLF — carriage return + line feed — collapsed to LF, trailing whitespace stripped, blank lines collapsed). Before re-running LLM extraction on a changed file, the phase checks whether the hash matches the cached result. A file where only whitespace or comments were reformatted gets a cache hit and costs zero tokens.
The phase falls back to full extraction only when it has to:
Incremental eligibility check
# Falls back to full extraction when any of these are true
language not in INCREMENTAL_ELIGIBLE_LANGS # unsupported language
cache_data is None # first run, no prior state
prior_run.budget_stopped == True # incomplete prior run
prompt_version != cache.prompt_version # prompt changed, stale cache
model_id != cache.model_id # model changed, stale cache
That last pair matters: if we change the extraction prompt or the underlying model, the cache is treated as stale rather than silently mixed with new results.
Phase 2 — Implicit dependencies: threshold-gated diff exploration
Implicit couplings, like shared config keys, runtime event buses, and Redis channels, are harder to diff than explicit imports, because they aren't visible in any single file's text.
This phase computes a change ratio: changed_files / total_source_files. Below 35%, it runs a targeted diff-explorer: it loads the previously discovered entry-point signals and graph nodes, then runs LLM agents only on the modified surface. Above 35%, it falls back to a full scan.
Why a threshold, not always-incremental?
When a large fraction of the codebase changes at once (a major refactor, or a dependency upgrade touching hundreds of files), an incremental patch can accumulate stale edges faster than a clean full run. The 35% threshold is the crossover point where a fresh full scan becomes both cheaper and more accurate than a complex incremental patch. Incremental isn't dogma; it's a cost-accuracy trade the pipeline makes per run.
Phase 3 — File enrichment: tombstone, re-enrich, patch
The file enrichment delta runner treats the three change categories differently. Deleted files are tombstoned: their snapshot entries, graph nodes, and module file references are all removed, so a deleted file leaves no ghost edges behind. Added and modified files are re-enriched from scratch. And modified files that other files depend on get their edge summaries surgically patched, without re-enriching every dependent from scratch.
Phase 4 — Wiki: selective module recomputation
Wiki re-runs module aggregation only for modules that contain changed files. Module docs untouched by the change are preserved exactly, byte for byte. The module tree is updated with a targeted upsert, so fields not modified by this run (like PR-mined insights) are never touched.
How the pipeline knows where to diff from
A per-project metadata document stores the git SHA of the last successfully indexed ROOT (per branch, for non-main branches). The terminal phase of every successful run advances this pointer. On the next lgraph update, that SHA becomes the base of the git diff. Simple, and it means an interrupted run never advances the baseline past what was actually indexed.
The thresholds at a glance
| Threshold | Default | Phase | Behavior when exceeded |
|---|---|---|---|
INCREMENTAL_THRESHOLD | 35% | Implicit dependencies | Falls back to full dependency scan |
WIKI_INCREMENTAL_THRESHOLD | 50% | Wiki | Rebuilds the full module tree |
FILE_INDEX_INCREMENTAL_THRESHOLD | 60% | File enrichment | Re-enriches all files instead of the delta |
All three are tunable per deployment.
What this looks like in practice
We indexed localstack (1,987 files, 539K LOC) once, then made a normal ten-file change and ran lgraph update. Here is what each run actually cost.
Index once for $25. After that, folding in a normal ten-file change cost $0.10 and finished in under two minutes, about 0.4% of the index cost. Keeping the graph at ROOT is cheap enough to run on every commit.
One property matters most: every update calls the LLM only on what actually changed. The wiki and file-index phases compare content hashes against the saved snapshot and skip anything untouched, so the work scales with the size of the diff, not the size of the repo. The graph your agent reads stays the graph of the code you actually have.
Try it
Index once, then keep it at ROOT from then on.
npm install -g @latentforce/latentgraph
lgraph init # index once
lgraph update # keep it at ROOT
Part 2 of 3 · Read next
So far, we've fixed staleness for one developer. But a team isn't one person on one machine, so whose branch does the graph really describe?
Your codebase has branches. Your context graph should too. →