fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110) (#2287)

* fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110)

An explicit projectPath resolves to the nearest .codegraph/ walking up,
and that walk crosses git boundaries. Inside an independent repository
nested under an indexed parent that excludes it (typically gitignored),
every call silently answered from the parent's code.

After resolving the index root, ToolHandler now asks whether the path
sits in a different git repository strictly below that root (git
toplevel + common dir, memoized per path and root) and, if so, whether
the index holds any file under it (one primary-key range probe per
call). None means the success-shaped "isn't indexed" guidance. Nested
repos the parent does index (submodules, gitlinks, embedded clones),
ordinary subdirectories, linked worktrees of the same repository (the
when git can't run, behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): nested repositories the index leaves out (#2110)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-02 04:24:34 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5271c89f08
commit 455175da67
6 changed files with 497 additions and 0 deletions
+13
View File
@@ -280,6 +280,7 @@ export class QueryBuilder {
deleteFile?: SqliteStatement;
getFileByPath?: SqliteStatement;
getAllFiles?: SqliteStatement;
hasFilesUnder?: SqliteStatement;
insertUnresolved?: SqliteStatement;
deleteUnresolvedByNode?: SqliteStatement;
getUnresolvedByName?: SqliteStatement;
@@ -3184,6 +3185,18 @@ export class QueryBuilder {
return row ? rowToFileRecord(row) : null;
}
/**
* Whether any tracked file lives under the project-relative POSIX directory
* `dir`. A range scan on the `path` primary key — `dir/` up to `dir0` ('0'
* is the byte after '/') — so it costs one index probe at any repo size.
*/
hasFilesUnder(dir: string): boolean {
if (!this.stmts.hasFilesUnder) {
this.stmts.hasFilesUnder = this.db.prepare('SELECT 1 FROM files WHERE path >= ? AND path < ? LIMIT 1');
}
return this.stmts.hasFilesUnder.get(`${dir}/`, `${dir}0`) !== undefined;
}
/**
* Get all tracked files
*/