## Summary
Replace the `didSave` sledgehammer (`pch_hashes.clear()` +
mark-all-dirty) with precise per-file dependency tracking that avoids
unnecessary recompilation.
### Two-layer staleness detection
After each successful compilation, a `DepsSnapshot` is captured
(interned path IDs + xxh3 content hashes + timestamp). On the next
feature request, `deps_changed()` checks:
- **Layer 1 (fast):** stat each dep, compare mtime against `build_at`.
If mtime is older → skip. Uses strict `<` so same-second modifications
fall through.
- **Layer 2 (precise):** for files with newer mtime, re-hash content and
compare. Catches touch-without-change (git checkout, backup restore)
without false rebuilds.
Special cases: files unreadable at build time (hash=0) always fall
through to Layer 2; disappeared files are detected via stat failure.
### Consolidated PCH state
Scatter of four maps (`pch_paths`, `pch_bounds`, `pch_hashes`,
`pch_building`) → single `PCHState` struct with `path`, `bound`, `hash`,
`deps`, `building` fields. `DepsSnapshot` and `SymbolInfo` moved out of
`MasterServer` to namespace scope.
### Concurrent compilation dedup
- **`ensure_compiled`:** `DocumentState::compiling` event prevents
duplicate AST compilations. Waiters `co_await` the event and check
`ast_dirty` after waking. When deps change is detected during an
in-flight build, `generation` is bumped so the builder's generation
check prevents it from incorrectly clearing `ast_dirty`.
- **`ensure_pch`:** `PCHState::building` event deduplicates PCH builds.
Waiters re-validate `preamble_hash` after waking to handle edits during
the wait. The `bound==0` path waits for in-flight builds before erasing.
Old PCH is deleted and path cleared before rebuild starts, so waiters
never see a stale path on failure.
### `didSave` changes
Removed the blanket `pch_hashes.clear()` + mark-all-dirty on save.
Staleness is now detected lazily via `deps_changed()` at the next
feature request. `didSave` still invalidates `CompileGraph` dependents
for module deps.
### FIXME noted
Rapid `didChange` edits (especially preamble changes) can cause the
stateful worker to compile with stale/concatenated text. Root cause is
in the worker, not in staleness tracking — noted as FIXME for a
follow-up PR.
## Test plan
13 integration tests covering:
- [x] Header mtime change → AST recompilation
- [x] Preamble header change → PCH rebuild
- [x] No change → fast path (cached AST reused)
- [x] Touch without content change → Layer 2 hash skips recompile
- [x] Header replaced with different content → detected
- [x] Fix error in header → diagnostics clear
- [x] Multiple files sharing header → each detects independently
- [x] Transitive header change → detected through include chain
- [x] didChange body edit → recompilation with updated diagnostics
- [x] didClose + reopen → compiles new disk content
- [x] didClose → hover returns None
- [x] didSave header → dependent file recompiles
- [x] didSave module → CompileGraph dependents invalidated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>