From 51119b8dc2a35c412c21b62aab925322bdaea494 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 15:57:56 +0900 Subject: [PATCH 1/3] Fix CLI project filter root resolution (#3189) --- changelog.d/unreleased/3189.fixed.md | 16 +++++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 31 ++++++++++++++-- .../QueryCommandRunnerTests.cs | 36 +++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/3189.fixed.md diff --git a/changelog.d/unreleased/3189.fixed.md b/changelog.d/unreleased/3189.fixed.md new file mode 100644 index 0000000000..3c7339fe56 --- /dev/null +++ b/changelog.d/unreleased/3189.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3189 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **CLI project filters now resolve against the indexed project root (#3189)** - query commands with `--project` now expand solution project filters from the active indexed project root, so explicit `--db` queries launched from another current directory do not inspect the wrong workspace. + +## 日本語 + +- **CLI の project filter が indexed project root 基準で解決されるようになりました (#3189)** - `--project` 付き query コマンドは active な indexed project root から solution project filter を展開するため、別の current directory から明示 `--db` query を実行しても誤った workspace を参照しません。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index a3daf22939..07b41cf104 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -44,6 +44,10 @@ public static class QueryCommandRunner [ThreadStatic] private static DbReader? s_batchReader; + [ThreadStatic] + private static string? s_batchDbPath; + [ThreadStatic] + private static bool s_batchDbPathExplicit; private static DateTime GetUtcNow() => TimeProvider.GetUtcNow().UtcDateTime; @@ -268,6 +272,7 @@ private sealed record StatusReadinessField( public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) { var dbPath = Path.Combine(".cdidx", "codeindex.db"); + var dbPathExplicit = false; for (var i = 0; i < cmdArgs.Length; i++) { var arg = cmdArgs[i]; @@ -279,6 +284,7 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) return CommandExitCodes.UsageError; } dbPath = cmdArgs[++i]; + dbPathExplicit = true; continue; } @@ -290,6 +296,7 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) Console.Error.WriteLine(BuildMissingOptionValueError("--db")); return CommandExitCodes.UsageError; } + dbPathExplicit = true; continue; } @@ -314,6 +321,8 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) db.TryMigrateForRead(); s_batchReader = new DbReader(db); + s_batchDbPath = dbPath; + s_batchDbPathExplicit = dbPathExplicit; var firstFailure = CommandExitCodes.Success; var lineNumber = 0; while (TryReadBatchLine(Console.In, out var line, out var lineExceededLimit)) @@ -347,6 +356,8 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) finally { s_batchReader = null; + s_batchDbPath = null; + s_batchDbPathExplicit = false; } } @@ -6736,11 +6747,15 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) } } + var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, dbPath, dataDir); + var resolvedDbPath = dbResolution.DbPath; + if (parseErrors == null && projectFilters.Count > 0) { try { - foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projectFilters, solutionFilter)) + var projectRoot = ResolveProjectFilterRoot(resolvedDbPath, dbPathExplicit); + foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(projectRoot, projectFilters, solutionFilter)) pathPatterns.Add(glob); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) @@ -6760,8 +6775,6 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) if (validateDefaultMaxLineWidth && !maxLineWidthExplicit && defaultMaxLineWidthError != null) AddParseError(defaultMaxLineWidthError); - var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, dbPath, dataDir); - var resolvedDbPath = dbResolution.DbPath; if (readOnly) { var canAppendReadOnlyFlags = !SqliteFileUri.StartsWithFileScheme(resolvedDbPath) || @@ -6853,6 +6866,18 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) }; } + private static string ResolveProjectFilterRoot(string dbPath, bool dbPathExplicit) + { + var effectiveDbPath = s_batchReader != null && !string.IsNullOrWhiteSpace(s_batchDbPath) + ? s_batchDbPath! + : dbPath; + var effectiveDbPathExplicit = s_batchReader != null && !string.IsNullOrWhiteSpace(s_batchDbPath) + ? s_batchDbPathExplicit + : dbPathExplicit; + return DbPathResolver.ResolveProjectRootForQuery(effectiveDbPath, effectiveDbPathExplicit) + ?? Environment.CurrentDirectory; + } + private static List ParseMapSections(string rawValue, Action addParseError) { var sections = new List(); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 0513058e2f..bc884b56c8 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -299,6 +299,42 @@ public void ParseArgs_ProjectFilterExpandsSolutionProjectToPathGlob_Issue1707() } } + [Fact] + public void ParseArgs_ProjectFilterUsesIndexedProjectRootForExplicitDb_Issue3189() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_solution_filter_explicit_db"); + var otherRoot = TestProjectHelper.CreateTempProject("cdidx_solution_filter_other_cwd"); + var originalCurrentDirectory = Environment.CurrentDirectory; + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src", "App")); + File.WriteAllText(Path.Combine(projectRoot, "CodeIndex.sln"), """ + Microsoft Visual Studio Solution File, Format Version 12.00 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}" + EndProject + """); + File.WriteAllText(Path.Combine(projectRoot, "src", "App", "App.csproj"), ""); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + Environment.CurrentDirectory = otherRoot; + var options = QueryCommandRunner.ParseArgs( + ["Auth", "--db", dbPath, "--project", "App"], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Equal("Auth", options.Query); + Assert.Equal(["App"], options.ProjectFilters); + Assert.Equal(["src/App/*"], options.PathPatterns); + Assert.Null(options.ParseError); + } + finally + { + Environment.CurrentDirectory = originalCurrentDirectory; + TestProjectHelper.DeleteDirectory(otherRoot); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Theory] [InlineData("30m", 30 * 60)] [InlineData("2h", 2 * 60 * 60)] From 0122614f6b4c20928ccc0077d3cede8b7456c53a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 15:59:34 +0900 Subject: [PATCH 2/3] Fix MCP project filter root resolution (#3183) --- changelog.d/unreleased/3183.fixed.md | 16 ++++++++++ src/CodeIndex/Mcp/McpToolHandlers.cs | 13 +++++--- tests/CodeIndex.Tests/McpServerTests.cs | 42 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 changelog.d/unreleased/3183.fixed.md diff --git a/changelog.d/unreleased/3183.fixed.md b/changelog.d/unreleased/3183.fixed.md new file mode 100644 index 0000000000..2938f88b7f --- /dev/null +++ b/changelog.d/unreleased/3183.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3183 +affected: + - src/CodeIndex/Mcp/McpToolHandlers.cs + - tests/CodeIndex.Tests/McpServerTests.cs +--- + +## English + +- **MCP project filters now resolve against the indexed project root (#3183)** - MCP tools that accept `project` now expand solution project filters from the database's active indexed project root instead of the server process current directory. + +## 日本語 + +- **MCP の project filter が indexed project root 基準で解決されるようになりました (#3183)** - `project` を受け取る MCP tool は server process の current directory ではなく、database の active indexed project root から solution project filter を展開します。 diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index e46b109331..b2a6cbc88a 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -1014,7 +1014,7 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis) return string.IsNullOrWhiteSpace(value) ? null : new List { value }; } - private static List? ReadScopedPathList(JsonNode? args) + private List? ReadScopedPathList(JsonNode? args) { var paths = ReadPathList(args, "path") ?? []; var projects = ReadPathList(args, "project") ?? []; @@ -1022,12 +1022,13 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis) return paths.Count == 0 ? null : paths; var solution = args?["solution"]?.GetValue(); - foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projects, solution)) + var projectRoot = ResolveProjectFilterRoot(); + foreach (var glob in SolutionProjectResolver.ResolveProjectDirectoryGlobs(projectRoot, projects, solution)) paths.Add(glob); return paths.Count == 0 ? null : paths; } - private static JsonObject? ValidateProjectFilterArguments(JsonNode? args) + private JsonObject? ValidateProjectFilterArguments(JsonNode? args) { var projects = ReadPathList(args, "project") ?? []; if (projects.Count == 0) @@ -1036,7 +1037,7 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis) var solution = args?["solution"]?.GetValue(); try { - _ = SolutionProjectResolver.ResolveProjectDirectoryGlobs(Environment.CurrentDirectory, projects, solution); + _ = SolutionProjectResolver.ResolveProjectDirectoryGlobs(ResolveProjectFilterRoot(), projects, solution); return null; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) @@ -1053,6 +1054,10 @@ private JsonObject ToAnalyzeSymbolJsonObject(SymbolAnalysisResult analysis) } } + private string ResolveProjectFilterRoot() + => DbPathResolver.ResolveProjectRootForQuery(_dbPath, _dbPathExplicit) + ?? Environment.CurrentDirectory; + private static bool TryReadSinceArgument(JsonNode? args, out DateTime? since, out string? error) { var sinceStr = args?["since"]?.GetValue(); diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 23f28dcee2..fa71fa6744 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -9999,6 +9999,48 @@ public void Call(ServiceB service) } } + [Fact] + public void ToolsCall_ProjectScopeUsesIndexedProjectRootWhenCurrentDirectoryDiffers_Issue3183() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_mcp_project_scope_indexed_root"); + var otherRoot = TestProjectHelper.CreateTempProject("cdidx_mcp_project_scope_other_cwd"); + var originalCurrentDirectory = Environment.CurrentDirectory; + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src", "AppA")); + Directory.CreateDirectory(Path.Combine(projectRoot, "src", "AppB")); + File.WriteAllText(Path.Combine(projectRoot, "Repo.sln"), """ + Microsoft Visual Studio Solution File, Format Version 12.00 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppA", "src\AppA\AppA.csproj", "{11111111-1111-1111-1111-111111111111}" + EndProject + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppB", "src\AppB\AppB.csproj", "{22222222-2222-2222-2222-222222222222}" + EndProject + """); + File.WriteAllText(Path.Combine(projectRoot, "src", "AppA", "AppA.csproj"), ""); + File.WriteAllText(Path.Combine(projectRoot, "src", "AppB", "AppB.csproj"), ""); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/AppA/ServiceA.cs", "csharp", "public class ServiceA { }\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/AppB/ServiceB.cs", "csharp", "public class ServiceB { }\n"); + + Environment.CurrentDirectory = otherRoot; + using var server = new McpServer(dbPath, ConsoleUi.LoadVersion(), dbPathExplicit: true); + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"Service","project":"AppA","exactSubstring":true}}}""")!; + var response = server.HandleMessage(request)!; + + Assert.False(response["result"]!["isError"]?.GetValue() ?? false); + var results = response["result"]!["structuredContent"]!["results"]!.AsArray(); + var result = Assert.Single(results); + Assert.Equal("src/AppA/ServiceA.cs", result!["path"]!.GetValue()); + } + finally + { + Environment.CurrentDirectory = originalCurrentDirectory; + TestProjectHelper.DeleteDirectory(otherRoot); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void ToolsCall_Index_Rebuild_IgnoresUnreadableDirectoriesWhenCollectingMarkerFingerprints() { From 28d506579e1157428d41f11f54779338b114618d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 16:01:06 +0900 Subject: [PATCH 3/3] Fix CLI LSP path root resolution (#3151) --- changelog.d/unreleased/3151.fixed.md | 16 ++++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 21 ++++++++-- .../QueryCommandRunnerTests.cs | 40 +++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/3151.fixed.md diff --git a/changelog.d/unreleased/3151.fixed.md b/changelog.d/unreleased/3151.fixed.md new file mode 100644 index 0000000000..b2740082e8 --- /dev/null +++ b/changelog.d/unreleased/3151.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3151 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **CLI LSP locations now resolve indexed relative paths from the indexed project root (#3151)** - `--format lsp` now builds file URIs from the active indexed project root when indexed paths are relative, falling back to the current directory only when no indexed root is available. + +## 日本語 + +- **CLI LSP location が indexed relative path を indexed project root から解決するようになりました (#3151)** - `--format lsp` は indexed path が相対パスの場合に active な indexed project root から file URI を構築し、indexed root が利用できない場合だけ current directory にフォールバックします。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 07b41cf104..0bbb78028c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -48,6 +48,8 @@ public static class QueryCommandRunner private static string? s_batchDbPath; [ThreadStatic] private static bool s_batchDbPathExplicit; + [ThreadStatic] + private static string? s_activeQueryProjectRoot; private static DateTime GetUtcNow() => TimeProvider.GetUtcNow().UtcDateTime; @@ -1158,11 +1160,14 @@ public static void AttachLspLocations(IEnumerable results) } } - public static LspLocation BuildLspLocation(string path, int startLine, int startColumn, int endLine, int endColumn) + public static LspLocation BuildLspLocation(string path, int startLine, int startColumn, int endLine, int endColumn, string? projectRoot = null) { + var baseRoot = string.IsNullOrWhiteSpace(projectRoot) + ? s_activeQueryProjectRoot ?? Environment.CurrentDirectory + : projectRoot; var absolutePath = Path.IsPathFullyQualified(path) ? path - : Path.GetFullPath(path, Environment.CurrentDirectory); + : Path.GetFullPath(path, baseRoot); return new LspLocation { Uri = new Uri(absolutePath).AbsoluteUri, @@ -7441,7 +7446,17 @@ private static int WithDb(QueryCommandOptions options, JsonSerializerOptions jso } reader.IncludeGenerated = options.IncludeGenerated; - var exitCode = reader.RunWithGeneratedScope(() => action(reader)); + var previousProjectRoot = s_activeQueryProjectRoot; + s_activeQueryProjectRoot = ResolveProjectFilterRoot(dbPath, options.DbPathExplicit); + int exitCode; + try + { + exitCode = reader.RunWithGeneratedScope(() => action(reader)); + } + finally + { + s_activeQueryProjectRoot = previousProjectRoot; + } var profileEntries = profiling ? Database.DbDebug.EndProfile() : []; if (options.Profile) WriteProfilePayload(profileEntries, jsonOptions); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index bc884b56c8..f3bd743fea 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -335,6 +335,46 @@ public void ParseArgs_ProjectFilterUsesIndexedProjectRootForExplicitDb_Issue3189 } } + [Fact] + public void RunDefinition_LspFormatUsesIndexedProjectRootForExplicitDb_Issue3151() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_explicit_db_root"); + var otherRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_other_cwd"); + var originalCurrentDirectory = Environment.CurrentDirectory; + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App/Service.cs", + "csharp", + """ + public class Service + { + public void Run() { } + } + """); + + Environment.CurrentDirectory = otherRoot; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition( + ["Service", "--db", dbPath, "--format", "lsp", "--exact-name", "--lang", "csharp", "--kind", "class"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = JsonDocument.Parse(stdout); + var location = Assert.Single(document.RootElement.EnumerateArray()); + var expectedUri = new Uri(Path.Combine(projectRoot, "src", "App", "Service.cs")).AbsoluteUri; + Assert.Equal(expectedUri, location.GetProperty("uri").GetString()); + } + finally + { + Environment.CurrentDirectory = originalCurrentDirectory; + TestProjectHelper.DeleteDirectory(otherRoot); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Theory] [InlineData("30m", 30 * 60)] [InlineData("2h", 2 * 60 * 60)]