D181 — A cached repo path is validated before use, and a change of roots invalidates the cache¶
Decision. repoindex.lookupIndex no longer serves a cached candidate path
as-is: each candidate's paths are filtered down to the ones that currently hold
a live clone (a directory containing a .git entry, checked with the
symlink-following stat, one call per candidate), and ambiguity/emptiness is
evaluated on the survivors, not on the raw cache. Resolve also compares the
cache's Index.Roots against the roots argument (each element normalized
with expandHome, order-sensitive) before trusting the cache at all — a
difference skips lookupIndex entirely and falls through to a fresh Scan.
Context. Resolve served a cache hit with no filesystem check: moving a
clone left every {{repo:<key>}} citing it resolving to the old, now-wrong
location, silently and indefinitely — service restart did nothing, since the
stale value lives in ~/.config/cartographer/repos.json, not in server memory.
Only a deliberate miss (resolving a nonexistent key) rewrote the cache, by
accident. Two adjacent gaps made it worse: Index.Roots was persisted and never
read, so editing search_roots didn't invalidate anything either; and with
multiple clones of the same remote, a dead first-in-root-order entry kept
winning over a live second one. Worst case, a leftover clone at the old
path (an old copy, a .Trash copy) made the wrong resolution look like a
correct one — an agent following the placeholder read, or committed into, the
wrong working tree with nothing to signal it.
- Refresh-on-miss stays the only refresh path (D75). No watcher, no daemon, no TTL: a stale hit now degrades to a miss, which was already the trigger for a rescan. The behaviour that repairs staleness is the one D75 already specified — it just never used to fire.
- A hit stays cheap. Validation costs one
statper candidate path (following a symlinked clone correctly), never a filesystem walk — the depth cap exists precisely to keepScanoff the common resolution path, and this does not reintroduce it. - Root order still decides the winner among multiple live clones; the filter only removes candidates that are not really there anymore.
- An ambiguous short name that is only ambiguous on paper — because one of the colliding remotes has no live clone left — now resolves instead of erroring. That is the intended fix, not a side effect: the ambiguity was an artefact of a stale index, and D75's "ask for the full form" rule exists to disambiguate remotes that really do coexist on disk.
- An empty-vs-nonempty
Rootscounts as a difference. A cache written before this change carries noRoots, so the very first resolution after upgrade always rescans once — an intended, one-time cost, not a bug.
Consequences. No interface, configuration, CLI or MCP surface change — this
is a behavioural fix. The first resolution after upgrading to this version
rescans once per cache, per the Roots edge case above. fix: release.