From f954f1c219752cde1567a69e48df073a343bcbc7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 22:49:14 +0900 Subject: [PATCH 1/9] Add pretty JSON output controls (#2996) --- USER_GUIDE.md | 13 +++- changelog.d/unreleased/2996.added.md | 20 ++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 1 + src/CodeIndex/Cli/ConsoleUi.cs | 8 +-- src/CodeIndex/Cli/ProgramRunner.cs | 43 ++++++++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 15 +++-- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 74 +++++++++++++++++++++ 7 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 changelog.d/unreleased/2996.added.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 335a9d32a9..0065e9ff65 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -302,11 +302,14 @@ Most query commands emit one complete JSON value when `--json` is set. `search per line as newline-delimited JSON (ndjson), then a final `{"done":true,...}` line. Stream consumers can parse each line as it arrives; array-oriented tools can use `jq -s '.'` or pass `--json=array` to `search` to emit the result set as -one JSON array. +one JSON array. Add `--pretty` with `--json` to indent single-document JSON +responses; for `search`, use `--json=array --pretty` when the result set itself +should be indented because default `search --json` stays newline-delimited. ```bash cdidx search authenticate --json # ndjson stream, one result per line cdidx search authenticate --json=array # single JSON array +cdidx inspect QueryCommandRunner --json --pretty ``` For `cdidx find --count --json`, `files` is the canonical matched-file count. @@ -1160,6 +1163,7 @@ same source location. |---|---|---| | `--db ` | All commands except `languages`; for `mcp`, only `--db` is supported | Database file path. `index` defaults to `/.cdidx/codeindex.db`; query commands default to `.cdidx/codeindex.db` in the current directory. Query commands without `--db` keep trusting that default `.cdidx/codeindex.db` sibling path, so moving or renaming the current repo does not leave stale workspace metadata behind. For explicit query DBs, workspace metadata such as `project_root`, `git_head`, and `git_is_dirty` comes from the persisted `indexed_project_root` stored in that DB when available. Legacy explicit DBs created before that metadata existed may return those fields as `null` / absent until you rerun `cdidx index --db ` or a scoped update that actually commits at least one file delete/update against the intended project, even if the explicit path itself looks like `.../.cdidx/codeindex.db`. | | `--json` | All commands except `mcp` | JSON output (for AI/machine use). `search --json` writes newline-delimited result objects followed by a final `{"done":true,"count":N,"interrupted":false}` sentinel, including zero-result output, so stream consumers can detect clean completion. | +| `--pretty` | JSON-capable commands except `mcp` | Pretty-print JSON output with indentation. Default `search --json` remains newline-delimited; use `search --json=array --pretty` for an indented search result array. | | `--status ` | `suggestions` | Filter local suggestion history by GitHub submission state. | | `--language ` / `--lang ` | `suggestions` | Filter local suggestion history by recorded target language. | | `--category ` | `suggestions` | Filter local suggestion history by suggestion category. | @@ -2498,11 +2502,15 @@ release changelog を source of truth とします。完全な syntax line は ` newline-delimited JSON (ndjson) として出力し、最後に `{"done":true,...}` 行を 出力します。stream consumer は各行を到着順に parse できます。array 前提の tool では `jq -s '.'` を使うか、`search` に `--json=array` を渡すと result set を -1 つの JSON array として出力できます。 +1 つの JSON array として出力できます。`--json` と一緒に `--pretty` を付けると +単一 document の JSON 応答をインデント付きで出力します。`search` の result set を +整形したい場合は、既定の `search --json` が newline-delimited のまま保たれるため +`--json=array --pretty` を使います。 ```bash cdidx search authenticate --json # ndjson stream、1 行 1 result cdidx search authenticate --json=array # 単一 JSON array +cdidx inspect QueryCommandRunner --json --pretty ``` ## Editor / index portability @@ -3358,6 +3366,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit |---|---|---| | `--db ` | `languages` を除く全コマンド。`mcp` は `--db` のみ対応 | DBファイルパス。`index` のデフォルトは `/.cdidx/codeindex.db`、クエリ系コマンドのデフォルトはカレントディレクトリの `.cdidx/codeindex.db`。`--db` を付けない query は、その既定の `.cdidx/codeindex.db` sibling path を引き続き正とするため、カレント repo を move/rename しても古い workspace metadata を引きずらない。明示指定 query DB の `project_root`、`git_head`、`git_is_dirty` などの workspace metadata は、利用可能な場合はその DB に保存された `indexed_project_root` から解決される。保存前の古い explicit DB では、意図した project に対して `cdidx index --db `、または少なくとも 1 件の file delete/update を実際に commit する scoped update を一度実行するまで、これらの項目が `null` / 未出力になることがあり、明示パス自体が `.../.cdidx/codeindex.db` でも同じ。 | | `--json` | `mcp` を除く全コマンド | JSON出力(AI/機械向け) | +| `--pretty` | `mcp` を除く JSON 対応コマンド | JSON 出力をインデント付きで整形。既定の `search --json` は newline-delimited のまま維持されるため、検索結果配列を整形したい場合は `search --json=array --pretty` を使う。 | | `--status ` | `suggestions` | ローカル提案履歴を GitHub 送信状態で絞り込みます。 | | `--language ` / `--lang ` | `suggestions` | ローカル提案履歴を記録済み対象言語で絞り込みます。 | | `--category ` | `suggestions` | ローカル提案履歴を提案カテゴリで絞り込みます。 | diff --git a/changelog.d/unreleased/2996.added.md b/changelog.d/unreleased/2996.added.md new file mode 100644 index 0000000000..097b640a80 --- /dev/null +++ b/changelog.d/unreleased/2996.added.md @@ -0,0 +1,20 @@ +--- +category: added +issues: + - 2996 +affected: + - src/CodeIndex/Cli/ProgramRunner.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ProgramRunnerTests.cs + - USER_GUIDE.md +--- + +## English + +- **JSON commands can now pretty-print output (#2996)** — `--pretty` enables indented JSON for commands that share the CLI JSON serializer, making large JSON payloads easier to inspect and diff. + +## 日本語 + +- **JSON コマンドが整形出力に対応しました (#2996)** — `--pretty` により CLI の JSON serializer を共有するコマンドでインデント付き JSON を出力でき、大きな JSON payload の確認や diff がしやすくなりました。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index d8ea77804f..890da16526 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -207,6 +207,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--workspace-db", ValuePlaceholder = "", Description = "Additional workspace member database path for dependency aggregation", Commands = Set(WorkspaceDbCommands) }, new() { Name = "--data-dir", ValuePlaceholder = "", Description = "Directory containing codeindex.db; overrides CDIDX_DATA_DIR/XDG/workspace defaults", Commands = Set(DataDirCommands) }, new() { Name = "--json", Description = "JSON output; search also accepts --json=array for a single JSON array", Commands = Set(JsonCommands) }, + new() { Name = "--pretty", Description = "Pretty-print JSON output with indentation", Commands = Set(JsonCommands) }, new() { Name = "--format", ValuePlaceholder = "", Description = "Standard output format for token budgets, editor integrations, and CI", Commands = Set(FormatCommands) }, new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()) }, new() { Name = "--silent", Description = "Alias for --quiet", Commands = Set(AllCommands.ToArray()) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 30905a8a40..9a936868f2 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -78,7 +78,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("index-commits", "cdidx index --commits [commit-ref ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-changed-between", "cdidx index --changed-between [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-files", "cdidx index --files [path ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), - ("search", "cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"), + ("search", "cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"), ("definition", "cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]"), ("goto", "cdidx goto |--query |-- [--db ] [--json] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--all]"), ("references", "cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), @@ -88,9 +88,9 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("files", "cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]"), ("find", "cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]"), ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), - ("map", "cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), - ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), - ("outline", "cdidx outline [--db ] [--json] [--verbose]"), + ("map", "cdidx map [--db ] [--json] [--pretty] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), + ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), + ("outline", "cdidx outline [--db ] [--json] [--pretty] [--verbose]"), ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config] [--check-updates]"), ("workspace", "cdidx workspace [name] [--json]"), ("config", "cdidx config show [--json]"), diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 700205b70b..b97b9cf818 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -35,6 +35,7 @@ internal static class ProgramRunner "--metrics", "--debug-unsafe", "--strict-version", + "--pretty", }; private static readonly HashSet TopLevelValueOptionNames = new(StringComparer.Ordinal) { @@ -125,6 +126,8 @@ internal static int Run( CommandErrorWriter.Write(StripErrorPrefix(strictVersionError), "use `--strict-version` without a value."); return CommandExitCodes.InvalidArgument; } + if (TryConsumePrettyJsonFlag(ref args)) + jsonOptions = new JsonSerializerOptions(jsonOptions) { WriteIndented = true }; using var jsonAnsiScope = ConsoleUi.SuppressAnsiForJsonOutput(ContainsJsonOutputFlag(args)); var commandStopwatch = Stopwatch.StartNew(); @@ -923,6 +926,46 @@ internal static bool TryConsumeQuietFlag(ref string[] args) return quiet; } + internal static bool TryConsumePrettyJsonFlag(ref string[] args) + { + if (args.Length == 0) + return false; + + var kept = new List(args.Length); + var pretty = false; + var passthrough = false; + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (passthrough) + { + kept.Add(arg); + continue; + } + if (arg == "--") + { + passthrough = true; + kept.Add(arg); + continue; + } + if (ShouldPreserveQueryCommandToken(args, i)) + { + kept.Add(arg); + continue; + } + if (arg == "--pretty") + { + pretty = true; + continue; + } + + kept.Add(arg); + } + + args = kept.ToArray(); + return pretty; + } + internal static bool TryConsumeGlobalLogFlags(ref string[] args, out string error) { error = string.Empty; diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 1ff98aad44..cbe4d46c9f 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -220,6 +220,7 @@ private sealed record StatusReadinessField( "--check-updates", "--read-only", "--immutable", + "--pretty", ]; private const string OutputFormatText = "text"; private const string OutputFormatJson = "json"; @@ -480,6 +481,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) return CommandExitCodes.UsageError; var exactSubstringHint = SearchQueryAdvisor.BuildExactSubstringHint(options.Query, options.RawFts, exact, options.Prefix); + var ndjsonOptions = options.JsonOutputFormat == JsonOutputFormatNdjson ? GetCompactJsonOptions(jsonOptions) : jsonOptions; int? jsonDoneCount = null; return WithDb(options, jsonOptions, reader => { @@ -531,7 +533,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) } else { - Console.WriteLine(BuildJsonZeroResultPayload(reader, jsonOptions, resultsKey: "results", query: options.Query, ftsQueryDiagnostics: ftsQueryDiagnostics, queryOptions: options, exactSubstringHint: exactSubstringHint).ToJsonString(jsonOptions)); + Console.WriteLine(BuildJsonZeroResultPayload(reader, ndjsonOptions, resultsKey: "results", query: options.Query, ftsQueryDiagnostics: ftsQueryDiagnostics, queryOptions: options, exactSubstringHint: exactSubstringHint).ToJsonString(ndjsonOptions)); jsonDoneCount = 0; } } @@ -560,7 +562,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) } else { - Console.WriteLine(BuildJsonZeroResultPayload(reader, jsonOptions, resultsKey: "results", query: options.Query, ftsQueryDiagnostics: ftsQueryDiagnostics, queryOptions: options, exactSubstringHint: exactSubstringHint).ToJsonString(jsonOptions)); + Console.WriteLine(BuildJsonZeroResultPayload(reader, ndjsonOptions, resultsKey: "results", query: options.Query, ftsQueryDiagnostics: ftsQueryDiagnostics, queryOptions: options, exactSubstringHint: exactSubstringHint).ToJsonString(ndjsonOptions)); jsonDoneCount = 0; } } @@ -613,7 +615,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) foreach (var result in compactResults) Console.WriteLine(JsonSerializer.Serialize( result, - CliJsonSerializerContextFactory.Create(jsonOptions).CompactSearchResult)); + CliJsonSerializerContextFactory.Create(ndjsonOptions).CompactSearchResult)); jsonDoneCount = compactResults.Length; } } @@ -636,7 +638,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) }, exitCode => { if (options.Json && options.JsonOutputFormat == JsonOutputFormatNdjson && jsonDoneCount.HasValue) - WriteJsonStreamDone(jsonDoneCount.Value, jsonOptions); + WriteJsonStreamDone(jsonDoneCount.Value, ndjsonOptions); }); } @@ -743,6 +745,9 @@ private static void WriteJsonStreamDone(int count, JsonSerializerOptions jsonOpt new JsonStreamDoneResult(Done: true, Count: count, Interrupted: false), CliJsonSerializerContextFactory.Create(jsonOptions).JsonStreamDoneResult)); + private static JsonSerializerOptions GetCompactJsonOptions(JsonSerializerOptions jsonOptions) + => jsonOptions.WriteIndented ? new JsonSerializerOptions(jsonOptions) { WriteIndented = false } : jsonOptions; + public static void AttachLspLocations(IEnumerable results) { foreach (var result in results) @@ -5263,6 +5268,8 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) case "--immutable": readOnly = true; break; + case "--pretty": + break; case "--workspace-db": if (TryReadStringOptionValue(args, ref i, "--workspace-db", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var workspaceDbPath, out var workspaceDbError)) workspaceDbPaths.Add(workspaceDbPath!); diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 3bdc81d516..2ac0ddb34e 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -60,6 +60,80 @@ public void ContainsJsonOutputFlag_AfterPassthrough_ReturnsFalse() Assert.False(ProgramRunner.ContainsJsonOutputFlag(["search", "--", "--json"])); } + [Fact] + public void RunLanguages_PrettyJson_IndentsOutput_Issue2996() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["languages", "--json", "--pretty"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Empty(stderr); + Assert.Contains(Environment.NewLine + " \"languages\": [", stdout); + using var document = JsonDocument.Parse(stdout); + Assert.True(document.RootElement.TryGetProperty("languages", out _)); + } + + [Fact] + public void RunSearch_FirstQueryLiteralMatchingPrettyFlag_IsNotConsumed_Issue2996() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_pretty_query_literal"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--pretty appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "--pretty", "--db", dbPath, "--path", "README.md", "--count", "--exact-substring"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1" + Environment.NewLine, stdout); + Assert.Empty(stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_NdjsonWithPretty_KeepsOneJsonValuePerLine_Issue2996() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_pretty_ndjson"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "class App { void Needle() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["search", "Needle", "--db", dbPath, "--json", "--pretty"], + appVersion: "1.10.0")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Empty(stderr); + var lines = stdout.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(2, lines.Length); + foreach (var line in lines) + { + Assert.False(line.StartsWith(" ", StringComparison.Ordinal)); + using var _ = JsonDocument.Parse(line); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_TestExtractor_PrintsIsolatedSymbols() { From 92c68a9355bb78efd2a5c3db883b4c60ebe817a3 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:08:38 +0900 Subject: [PATCH 2/9] Add compact section caps (#3009) --- USER_GUIDE.md | 12 ++ changelog.d/unreleased/3009.added.md | 20 +++ src/CodeIndex/Cli/CliFlagSchema.cs | 3 + src/CodeIndex/Cli/ConsoleUi.cs | 6 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 124 +++++++++++++++++- .../QueryCommandRunnerInspectTests.cs | 100 ++++++++++++++ .../QueryCommandRunnerMapTests.cs | 59 +++++++++ 7 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/3009.added.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 0065e9ff65..689b0d705a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -305,11 +305,16 @@ can use `jq -s '.'` or pass `--json=array` to `search` to emit the result set as one JSON array. Add `--pretty` with `--json` to indent single-document JSON responses; for `search`, use `--json=array --pretty` when the result set itself should be indented because default `search --json` stays newline-delimited. +For AI-oriented bounded payloads, `map`, `inspect`, and `outline` accept +`--compact`. It implies JSON output, caps list sections to 5 items by default +(or the explicit `--limit` / `--top` value), and adds `compact`, +`compact_limit`, and `truncation.sections.*` metadata. ```bash cdidx search authenticate --json # ndjson stream, one result per line cdidx search authenticate --json=array # single JSON array cdidx inspect QueryCommandRunner --json --pretty +cdidx map --compact # capped JSON with truncation metadata ``` For `cdidx find --count --json`, `files` is the canonical matched-file count. @@ -1164,6 +1169,7 @@ same source location. | `--db ` | All commands except `languages`; for `mcp`, only `--db` is supported | Database file path. `index` defaults to `/.cdidx/codeindex.db`; query commands default to `.cdidx/codeindex.db` in the current directory. Query commands without `--db` keep trusting that default `.cdidx/codeindex.db` sibling path, so moving or renaming the current repo does not leave stale workspace metadata behind. For explicit query DBs, workspace metadata such as `project_root`, `git_head`, and `git_is_dirty` comes from the persisted `indexed_project_root` stored in that DB when available. Legacy explicit DBs created before that metadata existed may return those fields as `null` / absent until you rerun `cdidx index --db ` or a scoped update that actually commits at least one file delete/update against the intended project, even if the explicit path itself looks like `.../.cdidx/codeindex.db`. | | `--json` | All commands except `mcp` | JSON output (for AI/machine use). `search --json` writes newline-delimited result objects followed by a final `{"done":true,"count":N,"interrupted":false}` sentinel, including zero-result output, so stream consumers can detect clean completion. | | `--pretty` | JSON-capable commands except `mcp` | Pretty-print JSON output with indentation. Default `search --json` remains newline-delimited; use `search --json=array --pretty` for an indented search result array. | +| `--compact` | `map`, `inspect`, `outline` | Emit AI-oriented compact JSON with capped list sections and `truncation.sections.*` metadata. The default cap is 5 unless `--limit` / `--top` is supplied. | | `--status ` | `suggestions` | Filter local suggestion history by GitHub submission state. | | `--language ` / `--lang ` | `suggestions` | Filter local suggestion history by recorded target language. | | `--category ` | `suggestions` | Filter local suggestion history by suggestion category. | @@ -2506,11 +2512,16 @@ newline-delimited JSON (ndjson) として出力し、最後に `{"done":true,... 単一 document の JSON 応答をインデント付きで出力します。`search` の result set を 整形したい場合は、既定の `search --json` が newline-delimited のまま保たれるため `--json=array --pretty` を使います。 +AI 向けに上限付き payload が必要な場合、`map`、`inspect`、`outline` は +`--compact` に対応しています。これは JSON 出力を暗黙に有効化し、list section を +既定 5 件(明示した `--limit` / `--top` があればその値)に cap し、 +`compact`、`compact_limit`、`truncation.sections.*` metadata を追加します。 ```bash cdidx search authenticate --json # ndjson stream、1 行 1 result cdidx search authenticate --json=array # 単一 JSON array cdidx inspect QueryCommandRunner --json --pretty +cdidx map --compact # truncation metadata 付きの cap 済み JSON ``` ## Editor / index portability @@ -3367,6 +3378,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--db ` | `languages` を除く全コマンド。`mcp` は `--db` のみ対応 | DBファイルパス。`index` のデフォルトは `/.cdidx/codeindex.db`、クエリ系コマンドのデフォルトはカレントディレクトリの `.cdidx/codeindex.db`。`--db` を付けない query は、その既定の `.cdidx/codeindex.db` sibling path を引き続き正とするため、カレント repo を move/rename しても古い workspace metadata を引きずらない。明示指定 query DB の `project_root`、`git_head`、`git_is_dirty` などの workspace metadata は、利用可能な場合はその DB に保存された `indexed_project_root` から解決される。保存前の古い explicit DB では、意図した project に対して `cdidx index --db `、または少なくとも 1 件の file delete/update を実際に commit する scoped update を一度実行するまで、これらの項目が `null` / 未出力になることがあり、明示パス自体が `.../.cdidx/codeindex.db` でも同じ。 | | `--json` | `mcp` を除く全コマンド | JSON出力(AI/機械向け) | | `--pretty` | `mcp` を除く JSON 対応コマンド | JSON 出力をインデント付きで整形。既定の `search --json` は newline-delimited のまま維持されるため、検索結果配列を整形したい場合は `search --json=array --pretty` を使う。 | +| `--compact` | `map`、`inspect`、`outline` | list section を cap した AI 向け compact JSON を出力し、`truncation.sections.*` metadata を含める。既定 cap は 5 件で、`--limit` / `--top` 指定時はその値を使う。 | | `--status ` | `suggestions` | ローカル提案履歴を GitHub 送信状態で絞り込みます。 | | `--language ` / `--lang ` | `suggestions` | ローカル提案履歴を記録済み対象言語で絞り込みます。 | | `--category ` | `suggestions` | ローカル提案履歴を提案カテゴリで絞り込みます。 | diff --git a/changelog.d/unreleased/3009.added.md b/changelog.d/unreleased/3009.added.md new file mode 100644 index 0000000000..79985353a9 --- /dev/null +++ b/changelog.d/unreleased/3009.added.md @@ -0,0 +1,20 @@ +--- +category: added +issues: + - 3009 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs + - USER_GUIDE.md +--- + +## English + +- **`map`, `inspect`, and `outline` now support compact JSON section caps (#3009)** — `--compact` emits AI-oriented JSON, caps list sections to 5 items by default or the explicit `--limit` / `--top` value, and adds `truncation.sections.*` metadata for bounded payloads. + +## 日本語 + +- **`map`、`inspect`、`outline` が compact JSON section cap に対応しました (#3009)** — `--compact` は AI 向け JSON を出力し、list section を既定 5 件または明示した `--limit` / `--top` 値で cap し、上限付き payload 用の `truncation.sections.*` metadata を追加します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 890da16526..1a5ccd463b 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -180,6 +180,8 @@ internal static class CliFlagSchema "validate", "deps", "impact", "unused", "hotspots", "languages", "db", "report", ]; + private static readonly string[] CompactJsonCommands = ["map", "inspect", "outline"]; + private static readonly string[] FormatCommands = [ "search", "definition", "references", "callers", "callees", "find", "validate", "deps", @@ -208,6 +210,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--data-dir", ValuePlaceholder = "", Description = "Directory containing codeindex.db; overrides CDIDX_DATA_DIR/XDG/workspace defaults", Commands = Set(DataDirCommands) }, new() { Name = "--json", Description = "JSON output; search also accepts --json=array for a single JSON array", Commands = Set(JsonCommands) }, new() { Name = "--pretty", Description = "Pretty-print JSON output with indentation", Commands = Set(JsonCommands) }, + new() { Name = "--compact", Description = "AI-oriented compact JSON with capped list sections and truncation metadata", Commands = Set(CompactJsonCommands) }, new() { Name = "--format", ValuePlaceholder = "", Description = "Standard output format for token budgets, editor integrations, and CI", Commands = Set(FormatCommands) }, new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()) }, new() { Name = "--silent", Description = "Alias for --quiet", Commands = Set(AllCommands.ToArray()) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 9a936868f2..9720f31f41 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -88,9 +88,9 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("files", "cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]"), ("find", "cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]"), ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), - ("map", "cdidx map [--db ] [--json] [--pretty] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), - ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), - ("outline", "cdidx outline [--db ] [--json] [--pretty] [--verbose]"), + ("map", "cdidx map [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), + ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), + ("outline", "cdidx outline [--db ] [--json] [--pretty] [--compact] [--verbose]"), ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config] [--check-updates]"), ("workspace", "cdidx workspace [name] [--json]"), ("config", "cdidx config show [--json]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index cbe4d46c9f..981db284da 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -21,6 +21,7 @@ public static class QueryCommandRunner { internal const int DefaultQueryLimit = 20; internal const int DefaultMapLimit = 10; + internal const int DefaultCompactSectionLimit = 5; internal const int DefaultImpactLimit = 50; internal const int BatchMaxLineChars = 1024 * 1024; internal const int BatchMaxArgumentCount = 256; @@ -221,6 +222,7 @@ private sealed record StatusReadinessField( "--read-only", "--immutable", "--pretty", + "--compact", ]; private const string OutputFormatText = "text"; private const string OutputFormatJson = "json"; @@ -2592,10 +2594,13 @@ public static int RunMap(string[] cmdArgs, JsonSerializerOptions jsonOptions) return WithDb(options, jsonOptions, reader => { - var map = reader.GetRepoMap(options.Limit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, options.MinEntrypointConfidence); + var compactLimit = GetCompactSectionLimit(options); + var mapLimit = options.Compact ? GetCompactSourceLimit(compactLimit) : options.Limit; + var map = reader.GetRepoMap(mapLimit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, options.MinEntrypointConfidence); WorkspaceMetadataEnricher.Enrich(map, options.DbPath, options.DbPathExplicit); if (options.ContextAfterExplicit) ApplyRepoMapDepth(map, options.ContextAfter); + var compactTruncation = options.Compact ? ApplyRepoMapCompactCaps(map, compactLimit, options) : null; // Return not-found only when a narrowing filter is active and produces zero files. // Unfiltered empty indexes return success (valid state for health probes). @@ -2606,7 +2611,8 @@ public static int RunMap(string[] cmdArgs, JsonSerializerOptions jsonOptions) { if (options.Json) { - Console.WriteLine(JsonSerializer.Serialize(map, CliJsonSerializerContextFactory.Create(jsonOptions).RepoMapResult)); + var payload = BuildRepoMapJsonPayload(map, options, jsonOptions, compactTruncation); + Console.WriteLine(payload.ToJsonString(jsonOptions)); } else { @@ -2617,7 +2623,7 @@ public static int RunMap(string[] cmdArgs, JsonSerializerOptions jsonOptions) if (options.Json) { - var payload = BuildRepoMapJsonPayload(map, options, jsonOptions); + var payload = BuildRepoMapJsonPayload(map, options, jsonOptions, compactTruncation); Console.WriteLine(payload.ToJsonString(jsonOptions)); } else @@ -2676,13 +2682,15 @@ private static void ApplyRepoMapDepth(RepoMapResult map, int depth) private static int GetPathDepth(string path) => string.IsNullOrEmpty(path) ? 0 : path.Split('/', StringSplitOptions.RemoveEmptyEntries).Length; - private static JsonObject BuildRepoMapJsonPayload(RepoMapResult map, QueryCommandOptions options, JsonSerializerOptions jsonOptions) + private static JsonObject BuildRepoMapJsonPayload(RepoMapResult map, QueryCommandOptions options, JsonSerializerOptions jsonOptions, JsonObject? compactTruncation = null) { var payload = JsonSerializer.SerializeToNode(map, CliJsonSerializerContextFactory.Create(jsonOptions).RepoMapResult)!.AsObject(); if (options.MapSections == null) { if (options.ContextAfterExplicit) payload["depth"] = options.ContextAfter; + if (options.Compact && compactTruncation != null) + AddCompactJsonFields(payload, GetCompactSectionLimit(options), compactTruncation); return payload; } @@ -2723,9 +2731,87 @@ private static JsonObject BuildRepoMapJsonPayload(RepoMapResult map, QueryComman payload["sections"] = new JsonArray(options.MapSections.Select(section => JsonValue.Create(section)).ToArray()); if (options.ContextAfterExplicit) payload["depth"] = options.ContextAfter; + if (options.Compact && compactTruncation != null) + AddCompactJsonFields(payload, GetCompactSectionLimit(options), compactTruncation); return payload; } + private static int GetCompactSectionLimit(QueryCommandOptions options) + => options.LimitExplicit ? options.Limit : DefaultCompactSectionLimit; + + private static int GetCompactSourceLimit(int compactLimit) + { + var sourceLimit = compactLimit + 1; + return NumericFlagUpperBounds.TryGetValue("--limit", out var maxLimit) + ? Math.Min(sourceLimit, maxLimit) + : sourceLimit; + } + + private static JsonObject ApplyRepoMapCompactCaps(RepoMapResult map, int sectionLimit, QueryCommandOptions options) + { + var sections = new JsonObject(); + if (MapSectionEnabled(options, "languages")) + TruncateCompactSection(map.Languages, sectionLimit, sections, "languages"); + if (MapSectionEnabled(options, "tree")) + TruncateCompactSection(map.Modules, sectionLimit, sections, "modules"); + if (MapSectionEnabled(options, "hotspots")) + { + TruncateCompactSection(map.TopFiles, sectionLimit, sections, "top_files"); + TruncateCompactSection(map.SymbolRichFiles, sectionLimit, sections, "symbol_rich_files"); + TruncateCompactSection(map.ReferenceRichFiles, sectionLimit, sections, "reference_rich_files"); + TruncateCompactSection(map.Entrypoints, sectionLimit, sections, "entrypoints"); + } + if (MapSectionEnabled(options, "metrics")) + TruncateCompactSection(map.LargestFiles, sectionLimit, sections, "largest_files"); + return BuildCompactTruncationMetadata(sectionLimit, sections); + } + + private static JsonObject ApplySymbolAnalysisCompactCaps(SymbolAnalysisResult analysis, int sectionLimit) + { + var sections = new JsonObject(); + TruncateCompactSection(analysis.Definitions, sectionLimit, sections, "definitions"); + TruncateCompactSection(analysis.NearbySymbols, sectionLimit, sections, "nearby_symbols"); + TruncateCompactSection(analysis.References, sectionLimit, sections, "references"); + TruncateCompactSection(analysis.Callers, sectionLimit, sections, "callers"); + TruncateCompactSection(analysis.Callees, sectionLimit, sections, "callees"); + return BuildCompactTruncationMetadata(sectionLimit, sections); + } + + private static JsonObject ApplyOutlineCompactCaps(OutlineResult outline, int sectionLimit) + { + var sections = new JsonObject(); + TruncateCompactSection(outline.Symbols, sectionLimit, sections, "symbols"); + return BuildCompactTruncationMetadata(sectionLimit, sections); + } + + private static JsonObject BuildCompactTruncationMetadata(int sectionLimit, JsonObject sections) + => new() + { + ["section_limit"] = sectionLimit, + ["sections"] = sections, + }; + + private static void AddCompactJsonFields(JsonObject payload, int compactLimit, JsonObject truncation) + { + payload["compact"] = true; + payload["compact_limit"] = compactLimit; + payload["truncation"] = truncation; + } + + private static void TruncateCompactSection(List items, int sectionLimit, JsonObject sections, string sectionName) + { + var sourceCount = items.Count; + if (sourceCount > sectionLimit) + items.RemoveRange(sectionLimit, sourceCount - sectionLimit); + + sections[sectionName] = new JsonObject + { + ["returned"] = items.Count, + ["source_count"] = sourceCount, + ["truncated"] = sourceCount > sectionLimit, + }; + } + public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions) { var previewOptionError = ValidatePreviewOptions("inspect", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); @@ -2771,7 +2857,9 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions return WithDb(options, jsonOptions, reader => { - var analysis = reader.AnalyzeSymbol(options.Query, options.Limit, options.Lang, options.IncludeBody, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.MaxLineWidth); + var compactLimit = GetCompactSectionLimit(options); + var inspectLimit = options.Compact ? GetCompactSourceLimit(compactLimit) : options.Limit; + var analysis = reader.AnalyzeSymbol(options.Query, inspectLimit, options.Lang, options.IncludeBody, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.MaxLineWidth); var sqlGraphSignal = NarrowSqlGraphContractSignal( reader.GetSqlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests), DbReader.IsSqlLanguage(options.Lang) @@ -2796,8 +2884,11 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions WriteSqlGraphContractWarningIfNeeded(options.Json, sqlGraphSignal, reader, options); if (options.Json) { + var compactTruncation = options.Compact ? ApplySymbolAnalysisCompactCaps(analysis, compactLimit) : null; var payload = JsonSerializer.SerializeToNode(analysis, CliJsonSerializerContextFactory.Create(jsonOptions).SymbolAnalysisResult)!.AsObject(); AddSqlGraphContractJsonFields(payload, sqlGraphSignal); + if (compactTruncation != null) + AddCompactJsonFields(payload, compactLimit, compactTruncation); Console.WriteLine(payload.ToJsonString(jsonOptions)); } else @@ -2890,7 +2981,18 @@ public static int RunOutline(string[] cmdArgs, JsonSerializerOptions jsonOptions if (options.Json) { - Console.WriteLine(JsonSerializer.Serialize(outline, CliJsonSerializerContextFactory.Create(jsonOptions).OutlineResult)); + if (options.Compact) + { + var compactLimit = GetCompactSectionLimit(options); + var compactTruncation = ApplyOutlineCompactCaps(outline, compactLimit); + var payload = JsonSerializer.SerializeToNode(outline, CliJsonSerializerContextFactory.Create(jsonOptions).OutlineResult)!.AsObject(); + AddCompactJsonFields(payload, compactLimit, compactTruncation); + Console.WriteLine(payload.ToJsonString(jsonOptions)); + } + else + { + Console.WriteLine(JsonSerializer.Serialize(outline, CliJsonSerializerContextFactory.Create(jsonOptions).OutlineResult)); + } } else { @@ -5136,6 +5238,7 @@ public static QueryCommandOptions ParseArgs( bool verbose = false; bool profile = false; int? slowQueryMs = null; + bool compact = false; double minEntrypointConfidence = 0; string? statusExplainField = null; bool statusLogPath = false; @@ -5270,6 +5373,11 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) break; case "--pretty": break; + case "--compact": + compact = true; + json = true; + outputFormat = OutputFormatJson; + break; case "--workspace-db": if (TryReadStringOptionValue(args, ref i, "--workspace-db", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var workspaceDbPath, out var workspaceDbError)) workspaceDbPaths.Add(workspaceDbPath!); @@ -5883,6 +5991,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) JsonOutputFormat = jsonOutputFormat, OutputFormat = outputFormat, Limit = limit, + LimitExplicit = limitExplicit, Lang = lang, Kind = kind, Query = query, @@ -5932,6 +6041,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) Verbose = verbose, Profile = profile, SlowQueryMs = slowQueryMs, + Compact = compact, MinEntrypointConfidence = minEntrypointConfidence, StatusExplainField = statusExplainField, StatusLogPath = statusLogPath, @@ -8638,6 +8748,7 @@ public sealed class QueryCommandOptions public string JsonOutputFormat { get; init; } = "ndjson"; public string OutputFormat { get; init; } = "text"; public int Limit { get; init; } = 20; + public bool LimitExplicit { get; init; } public string? Lang { get; init; } public string? Kind { get; init; } public List VisibilityFilters { get; init; } = []; @@ -8687,6 +8798,7 @@ public sealed class QueryCommandOptions public bool Verbose { get; init; } public bool Profile { get; init; } public int? SlowQueryMs { get; init; } + public bool Compact { get; init; } public double MinEntrypointConfidence { get; init; } public string? StatusExplainField { get; init; } public bool StatusLogPath { get; init; } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs index 30b4c9933c..5e5f96744e 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -59,6 +59,56 @@ public void RunInspect_AllowsPathValueThatLooksLikePreviewOption() } } + [Fact] + public void RunInspect_CompactJson_CapsNearbySymbolsAndReportsTruncation_Issue3009() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_compact_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/SharedTarget.cs", + "csharp", + """ + public class SharedTarget + { + public void Run0() { } + public void Run1() { } + public void Run2() { } + public void Run3() { } + public void Run4() { } + public void Run5() { } + public void Run6() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["SharedTarget", "--db", dbPath, "--compact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var nearbySymbols = json.GetProperty("nearby_symbols").EnumerateArray().ToList(); + var nearbySymbolsTruncation = json + .GetProperty("truncation") + .GetProperty("sections") + .GetProperty("nearby_symbols"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("compact").GetBoolean()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, nearbySymbols.Count); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, nearbySymbolsTruncation.GetProperty("returned").GetInt32()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit + 1, nearbySymbolsTruncation.GetProperty("source_count").GetInt32()); + Assert.True(nearbySymbolsTruncation.GetProperty("truncated").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunOutline_IndentsByContainerDepth() { @@ -117,6 +167,56 @@ public void Method() { } } } + [Fact] + public void RunOutline_CompactJson_CapsSymbolsAndReportsTruncation_Issue3009() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_compact_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/many.cs", + "csharp", + """ + public class Many + { + public void M0() { } + public void M1() { } + public void M2() { } + public void M3() { } + public void M4() { } + public void M5() { } + public void M6() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/many.cs", "--db", dbPath, "--compact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToList(); + var symbolTruncation = json + .GetProperty("truncation") + .GetProperty("sections") + .GetProperty("symbols"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("compact").GetBoolean()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, symbols.Count); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, symbolTruncation.GetProperty("returned").GetInt32()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit + 3, symbolTruncation.GetProperty("source_count").GetInt32()); + Assert.True(symbolTruncation.GetProperty("truncated").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunOutline_UsesNestedSymbolDepthInHumanOutput() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs index 017063cda7..e274b1ceea 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs @@ -24,6 +24,65 @@ public void RunMap_ParseSectionsAndDepth_StoresSelectors() Assert.Null(options.ParseError); } + [Fact] + public void RunMap_ParseCompact_ImpliesJsonAndPreservesExplicitLimit_Issue3009() + { + var options = QueryCommandRunner.ParseArgs( + ["--compact", "--limit", "3"], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.True(options.Json); + Assert.True(options.Compact); + Assert.True(options.LimitExplicit); + Assert.Equal(3, options.Limit); + Assert.Null(options.ParseError); + } + + [Fact] + public void RunMap_CompactJson_CapsSectionsAndReportsTruncation_Issue3009() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_compact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + for (var i = 0; i < QueryCommandRunner.DefaultCompactSectionLimit + 2; i++) + { + TestProjectHelper.InsertIndexedFile( + dbPath, + $"src/module{i}/App{i}.cs", + "csharp", + $"namespace Module{i}; public class App{i} {{ public void Run() {{ }} }}\n"); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--compact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var topFiles = json.GetProperty("top_files").EnumerateArray().ToList(); + var topFilesTruncation = json + .GetProperty("truncation") + .GetProperty("sections") + .GetProperty("top_files"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("compact").GetBoolean()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, json.GetProperty("compact_limit").GetInt32()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, topFiles.Count); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit, topFilesTruncation.GetProperty("returned").GetInt32()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit + 1, topFilesTruncation.GetProperty("source_count").GetInt32()); + Assert.True(topFilesTruncation.GetProperty("truncated").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunMap_WithJsonIncludesWorkspaceMetadataForProjectDb() { From 02c33c913aa148f762d66f5d3c3cee41bc031b4e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:22:56 +0900 Subject: [PATCH 3/9] Add inspect JSON field selection (#3056) --- USER_GUIDE.md | 14 ++ changelog.d/unreleased/3056.added.md | 19 ++ src/CodeIndex/Cli/CliFlagSchema.cs | 3 + src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 198 ++++++++++++++++++ .../QueryCommandRunnerInspectTests.cs | 105 ++++++++++ 6 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/3056.added.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 689b0d705a..0d4e2d6161 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -309,12 +309,17 @@ For AI-oriented bounded payloads, `map`, `inspect`, and `outline` accept `--compact`. It implies JSON output, caps list sections to 5 items by default (or the explicit `--limit` / `--top` value), and adds `compact`, `compact_limit`, and `truncation.sections.*` metadata. +For narrower `inspect` evidence, `--fields ` implies JSON and selects +top-level groups such as `definitions`, `file`, `graph`, `references`, +`callers`, and `callees`; `--body-only` is shorthand for `--body --fields +definitions`. ```bash cdidx search authenticate --json # ndjson stream, one result per line cdidx search authenticate --json=array # single JSON array cdidx inspect QueryCommandRunner --json --pretty cdidx map --compact # capped JSON with truncation metadata +cdidx inspect Compute --body-only # definitions with body_content only ``` For `cdidx find --count --json`, `files` is the canonical matched-file count. @@ -1170,6 +1175,8 @@ same source location. | `--json` | All commands except `mcp` | JSON output (for AI/machine use). `search --json` writes newline-delimited result objects followed by a final `{"done":true,"count":N,"interrupted":false}` sentinel, including zero-result output, so stream consumers can detect clean completion. | | `--pretty` | JSON-capable commands except `mcp` | Pretty-print JSON output with indentation. Default `search --json` remains newline-delimited; use `search --json=array --pretty` for an indented search result array. | | `--compact` | `map`, `inspect`, `outline` | Emit AI-oriented compact JSON with capped list sections and `truncation.sections.*` metadata. The default cap is 5 unless `--limit` / `--top` is supplied. | +| `--fields ` | `inspect` | Select top-level inspect JSON groups: `file`, `workspace`, `graph`, `definitions`, `body`, `nearby_symbols`, `references`, `callers`, `callees`, or `all`. `body` includes definition bodies and maps to `definitions`. | +| `--body-only` | `inspect` | Shorthand for `--body --fields definitions`, useful when large audits need implementation text without graph context. | | `--status ` | `suggestions` | Filter local suggestion history by GitHub submission state. | | `--language ` / `--lang ` | `suggestions` | Filter local suggestion history by recorded target language. | | `--category ` | `suggestions` | Filter local suggestion history by suggestion category. | @@ -2516,12 +2523,17 @@ AI 向けに上限付き payload が必要な場合、`map`、`inspect`、`outli `--compact` に対応しています。これは JSON 出力を暗黙に有効化し、list section を 既定 5 件(明示した `--limit` / `--top` があればその値)に cap し、 `compact`、`compact_limit`、`truncation.sections.*` metadata を追加します。 +`inspect` の証跡をさらに絞りたい場合、`--fields ` は JSON 出力を暗黙に有効化し、 +`definitions`、`file`、`graph`、`references`、`callers`、`callees` などの +top-level group を選択します。`--body-only` は `--body --fields definitions` の +shorthand です。 ```bash cdidx search authenticate --json # ndjson stream、1 行 1 result cdidx search authenticate --json=array # 単一 JSON array cdidx inspect QueryCommandRunner --json --pretty cdidx map --compact # truncation metadata 付きの cap 済み JSON +cdidx inspect Compute --body-only # body_content 付き definitions のみ ``` ## Editor / index portability @@ -3379,6 +3391,8 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--json` | `mcp` を除く全コマンド | JSON出力(AI/機械向け) | | `--pretty` | `mcp` を除く JSON 対応コマンド | JSON 出力をインデント付きで整形。既定の `search --json` は newline-delimited のまま維持されるため、検索結果配列を整形したい場合は `search --json=array --pretty` を使う。 | | `--compact` | `map`、`inspect`、`outline` | list section を cap した AI 向け compact JSON を出力し、`truncation.sections.*` metadata を含める。既定 cap は 5 件で、`--limit` / `--top` 指定時はその値を使う。 | +| `--fields ` | `inspect` | inspect JSON の top-level group を選択。`file`、`workspace`、`graph`、`definitions`、`body`、`nearby_symbols`、`references`、`callers`、`callees`、`all` を指定できる。`body` は definition body を含め、`definitions` に対応する。 | +| `--body-only` | `inspect` | `--body --fields definitions` の shorthand。大規模 audit で graph context なしに実装本文だけが必要な場合に使う。 | | `--status ` | `suggestions` | ローカル提案履歴を GitHub 送信状態で絞り込みます。 | | `--language ` / `--lang ` | `suggestions` | ローカル提案履歴を記録済み対象言語で絞り込みます。 | | `--category ` | `suggestions` | ローカル提案履歴を提案カテゴリで絞り込みます。 | diff --git a/changelog.d/unreleased/3056.added.md b/changelog.d/unreleased/3056.added.md new file mode 100644 index 0000000000..f1d8530358 --- /dev/null +++ b/changelog.d/unreleased/3056.added.md @@ -0,0 +1,19 @@ +--- +category: added +issues: + - 3056 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs + - USER_GUIDE.md +--- + +## English + +- **`inspect --json` can now select compact evidence fields (#3056)** — `--fields ` emits only requested top-level evidence groups, while `--body-only` is a shortcut for definition bodies without graph context. + +## 日本語 + +- **`inspect --json` が compact evidence field selection に対応しました (#3056)** — `--fields ` は要求した top-level evidence group だけを出力し、`--body-only` は graph context なしで definition body を取得する shortcut として使えます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 1a5ccd463b..254df691f1 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -144,6 +144,7 @@ internal static class CliFlagSchema ]; private static readonly string[] BodyCommands = ["definition", "references", "callers", "callees", "impact", "inspect"]; + private static readonly string[] InspectFieldCommands = ["inspect"]; private static readonly string[] MaxLineWidthCommands = [ @@ -245,6 +246,8 @@ private static IReadOnlyList BuildAll() new() { Name = "--cycles", Description = "Deps: return dependency cycles instead of edge rows", Commands = Set(DependencyCycleCommands) }, new() { Name = "--query", ValuePlaceholder = "", Description = "Literal query", Commands = Set(QueryCommands) }, new() { Name = "--body", Description = "Include body", Commands = Set(BodyCommands) }, + new() { Name = "--fields", ValuePlaceholder = "", Description = "Inspect: select top-level JSON evidence groups", Commands = Set(InspectFieldCommands) }, + new() { Name = "--body-only", Description = "Inspect: JSON shorthand for --body --fields definitions", Commands = Set(InspectFieldCommands) }, new() { Name = "--exact", Description = "Backward-compatible exact shorthand", Commands = Set(ExactCommands) }, new() { Name = "--regex", Description = "Use regular expression matching", Commands = Set("find") }, new() { Name = "--exact-name", Description = "Exact symbol-name equality", Commands = Set(ExactNameCommands), AlsoAcceptedBy = Set("search") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 9720f31f41..0809a19fe0 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -89,7 +89,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("find", "cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]"), ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), ("map", "cdidx map [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), - ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), + ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), ("outline", "cdidx outline [--db ] [--json] [--pretty] [--compact] [--verbose]"), ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config] [--check-updates]"), ("workspace", "cdidx workspace [name] [--json]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 981db284da..be9fd7669b 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -43,6 +43,8 @@ public static class QueryCommandRunner internal const int MaxSymbolQueryNames = 256; internal const int MaxMapSectionsCsvLength = 256; internal const int MaxMapSectionsCsvEntries = 16; + internal const int MaxInspectFieldsCsvLength = 256; + internal const int MaxInspectFieldsCsvEntries = 16; internal const int MaxStatusCheckScopesCsvLength = 256; internal const int MaxStatusCheckScopesCsvEntries = 16; internal const int MaxVisibilityFilterCsvLength = 256; @@ -116,6 +118,7 @@ public static class QueryCommandRunner "--format", "--min-entrypoint-confidence", "--sections", + "--fields", ]; private sealed record StatusReadinessField( string FieldName, @@ -223,6 +226,7 @@ private sealed record StatusReadinessField( "--immutable", "--pretty", "--compact", + "--body-only", ]; private const string OutputFormatText = "text"; private const string OutputFormatJson = "json"; @@ -2812,6 +2816,102 @@ private static void TruncateCompactSection(List items, int sectionLimit, J }; } + private static void ApplyInspectFieldSelection(JsonObject payload, QueryCommandOptions options, JsonSerializerOptions jsonOptions) + { + if (options.InspectFields == null) + return; + + payload["selected_fields"] = JsonSerializer.SerializeToNode(options.InspectFields, CliJsonSerializerContextFactory.Create(jsonOptions).ListString); + var keep = new HashSet(StringComparer.Ordinal) + { + "api_version", + "query", + "selected_fields", + }; + + foreach (var field in options.InspectFields) + AddInspectFieldProperties(keep, field); + + if (options.Compact) + { + keep.Add("compact"); + keep.Add("compact_limit"); + keep.Add("truncation"); + FilterInspectCompactTruncationSections(payload, options.InspectFields); + } + + foreach (var propertyName in payload.Select(property => property.Key).Where(key => !keep.Contains(key)).ToList()) + payload.Remove(propertyName); + } + + private static void AddInspectFieldProperties(HashSet keep, string field) + { + switch (field) + { + case "file": + keep.Add("file"); + break; + case "workspace": + keep.Add("workspace_indexed_at"); + keep.Add("workspace_latest_modified"); + keep.Add("project_root"); + keep.Add("git_head"); + keep.Add("git_is_dirty"); + keep.Add("indexed_head_commit"); + keep.Add("worktree_head_changed"); + break; + case "graph": + keep.Add("graph_language"); + keep.Add("graph_supported"); + keep.Add("graph_support_reason"); + keep.Add("graph_degraded"); + keep.Add("unsupported_symbol_kind"); + keep.Add("graph_table_available"); + keep.Add("sql_graph_contract_ready"); + keep.Add("sql_graph_contract_degraded_reason"); + keep.Add("exact_zero_hint"); + keep.Add("exact_index_available"); + keep.Add("degraded"); + keep.Add("degraded_reason"); + break; + case "definitions": + keep.Add("definitions"); + break; + case "nearby_symbols": + keep.Add("nearby_symbols"); + break; + case "references": + keep.Add("references"); + break; + case "callers": + keep.Add("callers"); + break; + case "callees": + keep.Add("callees"); + break; + } + } + + private static void FilterInspectCompactTruncationSections(JsonObject payload, IReadOnlyCollection inspectFields) + { + if (!payload.TryGetPropertyValue("truncation", out var truncationNode) + || truncationNode is not JsonObject truncation + || !truncation.TryGetPropertyValue("sections", out var sectionsNode) + || sectionsNode is not JsonObject sections) + { + return; + } + + var keepSections = inspectFields + .Where(IsInspectListField) + .ToHashSet(StringComparer.Ordinal); + foreach (var sectionName in sections.Select(section => section.Key).Where(section => !keepSections.Contains(section)).ToList()) + sections.Remove(sectionName); + } + + private static bool IsInspectListField(string field) + => field is "definitions" or "nearby_symbols" or "references" or "callers" or "callees"; + public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions) { var previewOptionError = ValidatePreviewOptions("inspect", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); @@ -2889,6 +2989,7 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions AddSqlGraphContractJsonFields(payload, sqlGraphSignal); if (compactTruncation != null) AddCompactJsonFields(payload, compactLimit, compactTruncation); + ApplyInspectFieldSelection(payload, options, jsonOptions); Console.WriteLine(payload.ToJsonString(jsonOptions)); } else @@ -5239,6 +5340,7 @@ public static QueryCommandOptions ParseArgs( bool profile = false; int? slowQueryMs = null; bool compact = false; + List? inspectFields = null; double minEntrypointConfidence = 0; string? statusExplainField = null; bool statusLogPath = false; @@ -5378,6 +5480,12 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) json = true; outputFormat = OutputFormatJson; break; + case "--body-only": + includeBody = true; + inspectFields = ["definitions"]; + json = true; + outputFormat = OutputFormatJson; + break; case "--workspace-db": if (TryReadStringOptionValue(args, ref i, "--workspace-db", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var workspaceDbPath, out var workspaceDbError)) workspaceDbPaths.Add(workspaceDbPath!); @@ -5565,6 +5673,20 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) else AddParseError(sectionsError!); break; + case "--fields": + if (TryReadStringOptionValue(args, ref i, "--fields", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var fieldsValue, out var fieldsError)) + { + WarnIfDuplicateSingleValueOption("--fields", fieldsValue!); + inspectFields = ParseInspectFields(fieldsValue!, AddParseError, out var includeBodyFromFields); + includeBody |= includeBodyFromFields; + json = true; + outputFormat = OutputFormatJson; + } + else + { + AddParseError(fieldsError!); + } + break; case "--fts": rawFts = true; break; @@ -6042,6 +6164,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) Profile = profile, SlowQueryMs = slowQueryMs, Compact = compact, + InspectFields = inspectFields, MinEntrypointConfidence = minEntrypointConfidence, StatusExplainField = statusExplainField, StatusLogPath = statusLogPath, @@ -6085,6 +6208,80 @@ private static List ParseMapSections(string rawValue, Action add return sections.Distinct(StringComparer.Ordinal).ToList(); } + private static List? ParseInspectFields(string rawValue, Action addParseError, out bool includeBody) + { + includeBody = false; + var fields = new List(); + var seen = new HashSet(StringComparer.Ordinal); + var all = false; + + if (!ValidateCsvBounds("--fields", rawValue, MaxInspectFieldsCsvLength, MaxInspectFieldsCsvEntries, addParseError)) + return fields; + + foreach (var rawField in rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var field = rawField.ToLowerInvariant().Replace('-', '_'); + string canonical; + switch (field) + { + case "all": + all = true; + continue; + case "file": + canonical = "file"; + break; + case "metadata": + case "workspace": + canonical = "workspace"; + break; + case "graph": + case "trust": + canonical = "graph"; + break; + case "definition": + case "definitions": + case "defs": + canonical = "definitions"; + break; + case "body": + canonical = "definitions"; + includeBody = true; + break; + case "nearby": + case "nearby_symbols": + case "nearbysymbols": + canonical = "nearby_symbols"; + break; + case "reference": + case "references": + case "refs": + canonical = "references"; + break; + case "caller": + case "callers": + canonical = "callers"; + break; + case "callee": + case "callees": + canonical = "callees"; + break; + default: + addParseError($"Error: unsupported --fields value '{rawField}'. Use one or more of all, file, workspace, graph, definitions, body, nearby_symbols, references, callers, callees."); + continue; + } + + if (seen.Add(canonical)) + fields.Add(canonical); + } + + if (all && fields.Count > 0) + addParseError("Error: --fields all cannot be combined with specific field names."); + if (!all && fields.Count == 0) + addParseError("Error: --fields requires at least one field name."); + + return all ? null : fields; + } + private static bool ValidateCsvBounds( string optionName, string rawValue, @@ -8799,6 +8996,7 @@ public sealed class QueryCommandOptions public bool Profile { get; init; } public int? SlowQueryMs { get; init; } public bool Compact { get; init; } + public List? InspectFields { get; init; } public double MinEntrypointConfidence { get; init; } public string? StatusExplainField { get; init; } public bool StatusLogPath { get; init; } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs index 5e5f96744e..0d2d520a32 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -109,6 +109,111 @@ public void Run6() { } } } + [Fact] + public void RunInspect_ParseFields_ImplyJsonAndCanonicalizeAliases_Issue3056() + { + var options = QueryCommandRunner.ParseArgs( + ["--fields", "body,refs,nearby-symbols"], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.True(options.Json); + Assert.True(options.IncludeBody); + Assert.Equal(["definitions", "references", "nearby_symbols"], options.InspectFields); + Assert.Null(options.ParseError); + } + + [Fact] + public void RunInspect_FieldsJson_EmitsOnlySelectedTopLevelGroups_Issue3056() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_fields_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Target.cs", + "csharp", + """ + public class Target + { + public void Compute() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Target", "--db", dbPath, "--fields", "definitions,file"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var selectedFields = json.GetProperty("selected_fields").EnumerateArray().Select(item => item.GetString()).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(["definitions", "file"], selectedFields); + Assert.True(json.TryGetProperty("api_version", out _)); + Assert.True(json.TryGetProperty("query", out _)); + Assert.True(json.TryGetProperty("definitions", out _)); + Assert.True(json.TryGetProperty("file", out _)); + Assert.False(json.TryGetProperty("nearby_symbols", out _)); + Assert.False(json.TryGetProperty("references", out _)); + Assert.False(json.TryGetProperty("callers", out _)); + Assert.False(json.TryGetProperty("callees", out _)); + Assert.False(json.TryGetProperty("graph_supported", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_BodyOnlyJson_EmitsDefinitionBodiesOnly_Issue3056() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_body_only_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Target.cs", + "csharp", + """ + public class Target + { + public int Compute() + { + return 42; + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Compute", "--db", dbPath, "--body-only"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var selectedFields = json.GetProperty("selected_fields").EnumerateArray().Select(item => item.GetString()).ToList(); + var definition = json.GetProperty("definitions").EnumerateArray().Single(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(["definitions"], selectedFields); + Assert.Contains("return 42;", definition.GetProperty("body_content").GetString(), StringComparison.Ordinal); + Assert.False(json.TryGetProperty("file", out _)); + Assert.False(json.TryGetProperty("references", out _)); + Assert.False(json.TryGetProperty("callers", out _)); + Assert.False(json.TryGetProperty("callees", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunOutline_IndentsByContainerDepth() { From 3e5fa17197063edf9aa7b5b88955f153fc40e24a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:39:29 +0900 Subject: [PATCH 4/9] Preserve version pin hardening (#2996) --- src/CodeIndex/Cli/ProgramRunner.cs | 112 ++++++++++++++++++-- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 103 ++++++++++++++++++ 2 files changed, 208 insertions(+), 7 deletions(-) diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index b97b9cf818..8ddc195286 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -20,6 +20,9 @@ internal static class ProgramRunner internal const string QuietEnvironmentVariable = "CDIDX_QUIET"; private const string InstallerScriptUrlTemplate = "https://raw.githubusercontent.com/Widthdom/CodeIndex/{0}/install.sh"; private const long MaxInstallerScriptBytes = 1024 * 1024; + internal const int WorkspaceVersionPinMaxBytes = 4096; + internal const int WorkspaceVersionPinMaxSkippedBlankLines = 16; + internal const int WorkspaceVersionPinMaxLineChars = 256; internal const long TestExtractorMaxInputBytes = 4 * 1024 * 1024; private static readonly TimeSpan InstallerRunTimeout = TimeSpan.FromMinutes(5); private static readonly TimeSpan InstallerKillWaitTimeout = TimeSpan.FromSeconds(5); @@ -1632,14 +1635,9 @@ private static int CheckWorkspaceVersionPin(string appVersion, string startDirec if (pinPath == null) return CommandExitCodes.Success; - string required; - try - { - required = File.ReadLines(pinPath).FirstOrDefault(line => !string.IsNullOrWhiteSpace(line))?.Trim() ?? ""; - } - catch (Exception ex) + if (!TryReadWorkspaceVersionPin(pinPath, out var required, out var warning)) { - Console.Error.WriteLine($"Warning: could not read .cdidx-version at {pinPath}: {ex.Message}"); + Console.Error.WriteLine(warning); return CommandExitCodes.Success; } @@ -1658,6 +1656,106 @@ private static int CheckWorkspaceVersionPin(string appVersion, string startDirec return CommandExitCodes.ExUsage; } + private static bool TryReadWorkspaceVersionPin(string pinPath, out string required, out string warning) + { + required = string.Empty; + warning = string.Empty; + + try + { + var bytes = ReadWorkspaceVersionPinBytes(pinPath); + if (bytes.Length > WorkspaceVersionPinMaxBytes) + { + warning = $"Warning: ignoring .cdidx-version at {pinPath}: file exceeds {WorkspaceVersionPinMaxBytes} bytes."; + return false; + } + + return TryParseWorkspaceVersionPin(DecodeWorkspaceVersionPinBytes(bytes), pinPath, out required, out warning); + } + catch (Exception ex) + { + warning = $"Warning: could not read .cdidx-version at {pinPath}: {ex.Message}"; + return false; + } + } + + private static byte[] ReadWorkspaceVersionPinBytes(string pinPath) + { + var buffer = new byte[WorkspaceVersionPinMaxBytes + 1]; + var totalRead = 0; + + using var stream = new FileStream( + pinPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete, + bufferSize: Math.Min(1024, buffer.Length), + FileOptions.SequentialScan); + + while (totalRead < buffer.Length) + { + var read = stream.Read(buffer, totalRead, buffer.Length - totalRead); + if (read == 0) + break; + totalRead += read; + } + + if (totalRead == buffer.Length) + return buffer; + + var result = new byte[totalRead]; + Array.Copy(buffer, result, totalRead); + return result; + } + + private static string DecodeWorkspaceVersionPinBytes(byte[] bytes) + { + using var stream = new MemoryStream(bytes, writable: false); + using var reader = new StreamReader( + stream, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: true, + bufferSize: Math.Min(1024, Math.Max(1, bytes.Length))); + return reader.ReadToEnd(); + } + + private static bool TryParseWorkspaceVersionPin(string content, string pinPath, out string required, out string warning) + { + required = string.Empty; + warning = string.Empty; + + using var reader = new StringReader(content); + var skippedBlankLines = 0; + var lineNumber = 0; + string? line; + while ((line = reader.ReadLine()) != null) + { + lineNumber++; + if (line.Length > WorkspaceVersionPinMaxLineChars) + { + warning = $"Warning: ignoring .cdidx-version at {pinPath}: line {lineNumber} exceeds {WorkspaceVersionPinMaxLineChars} characters."; + return false; + } + + if (string.IsNullOrWhiteSpace(line)) + { + skippedBlankLines++; + if (skippedBlankLines > WorkspaceVersionPinMaxSkippedBlankLines) + { + warning = $"Warning: ignoring .cdidx-version at {pinPath}: more than {WorkspaceVersionPinMaxSkippedBlankLines} leading blank lines."; + return false; + } + + continue; + } + + required = line.Trim(); + return true; + } + + return true; + } + internal static string? FindWorkspaceVersionPin(string startDirectory) { var current = Path.GetFullPath(startDirectory); diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index 2ac0ddb34e..a371f64e9b 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -463,6 +463,109 @@ public void Run_WorkspaceVersionPinMismatch_StrictFailsBeforeCommand() } } + [Fact] + public void Run_WorkspaceVersionPinUtf8Bom_MatchingStrictPinSucceeds() + { + var projectRoot = TestProjectHelper.CreateTempProject("version-pin-bom"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, ".cdidx-version"), + "1.10.0\n", + new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["--strict-version", "--version", "--json"], + appVersion: "1.10.0", + configStartDirectory: projectRoot)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("\"version\":\"1.10.0\"", stdout); + Assert.DoesNotContain("workspace requires cdidx", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Run_WorkspaceVersionPinTooLarge_WarnsAndIgnoresPin() + { + var projectRoot = TestProjectHelper.CreateTempProject("version-pin-large"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, ".cdidx-version"), + "9.9.9\n" + new string('x', ProgramRunner.WorkspaceVersionPinMaxBytes)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["--strict-version", "--version", "--json"], + appVersion: "1.10.0", + configStartDirectory: projectRoot)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("\"version\":\"1.10.0\"", stdout); + Assert.Contains($"file exceeds {ProgramRunner.WorkspaceVersionPinMaxBytes} bytes", stderr); + Assert.DoesNotContain("workspace requires cdidx v9.9.9", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Run_WorkspaceVersionPinTooManyBlankLines_WarnsAndIgnoresPin() + { + var projectRoot = TestProjectHelper.CreateTempProject("version-pin-blanks"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, ".cdidx-version"), + new string('\n', ProgramRunner.WorkspaceVersionPinMaxSkippedBlankLines + 1) + "9.9.9\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["--strict-version", "--version", "--json"], + appVersion: "1.10.0", + configStartDirectory: projectRoot)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("\"version\":\"1.10.0\"", stdout); + Assert.Contains($"more than {ProgramRunner.WorkspaceVersionPinMaxSkippedBlankLines} leading blank lines", stderr); + Assert.DoesNotContain("workspace requires cdidx v9.9.9", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Run_WorkspaceVersionPinLineTooLong_WarnsAndIgnoresPin() + { + var projectRoot = TestProjectHelper.CreateTempProject("version-pin-line"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, ".cdidx-version"), + new string('9', ProgramRunner.WorkspaceVersionPinMaxLineChars + 1) + "\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + ["--strict-version", "--version", "--json"], + appVersion: "1.10.0", + configStartDirectory: projectRoot)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("\"version\":\"1.10.0\"", stdout); + Assert.Contains($"line 1 exceeds {ProgramRunner.WorkspaceVersionPinMaxLineChars} characters", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void UpdateChecker_Check_ReportsNewerRelease() { From e232e843b8fc16dc66088f893b18b2513432941f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:45:40 +0900 Subject: [PATCH 5/9] Align inspect fields schema (#3056) --- src/CodeIndex/Cli/CliFlagSchema.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 254df691f1..b6c8b9e759 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -246,7 +246,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--cycles", Description = "Deps: return dependency cycles instead of edge rows", Commands = Set(DependencyCycleCommands) }, new() { Name = "--query", ValuePlaceholder = "", Description = "Literal query", Commands = Set(QueryCommands) }, new() { Name = "--body", Description = "Include body", Commands = Set(BodyCommands) }, - new() { Name = "--fields", ValuePlaceholder = "", Description = "Inspect: select top-level JSON evidence groups", Commands = Set(InspectFieldCommands) }, + new() { Name = "--fields", ValuePlaceholder = "", Description = "Inspect: select top-level JSON evidence groups", Commands = Set(InspectFieldCommands) }, new() { Name = "--body-only", Description = "Inspect: JSON shorthand for --body --fields definitions", Commands = Set(InspectFieldCommands) }, new() { Name = "--exact", Description = "Backward-compatible exact shorthand", Commands = Set(ExactCommands) }, new() { Name = "--regex", Description = "Use regular expression matching", Commands = Set("find") }, From 8d9982e6e0993cf8658c65ae013894c81f5f19f9 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:56:15 +0900 Subject: [PATCH 6/9] Allow outline compact limits (#3009) --- src/CodeIndex/Cli/CliFlagSchema.cs | 2 +- src/CodeIndex/Cli/ConsoleUi.cs | 2 +- .../QueryCommandRunnerInspectTests.cs | 51 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index b6c8b9e759..7ab2fbe9cf 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -75,7 +75,7 @@ internal static class CliFlagSchema private static readonly string[] LimitCapableCommands = [ "search", "definition", "goto", "references", "callers", "callees", "symbols", - "files", "find", "map", "inspect", "deps", "impact", "unused", "hotspots", + "files", "find", "map", "inspect", "outline", "deps", "impact", "unused", "hotspots", ]; private static readonly string[] LangCapableCommands = LimitCapableCommands; diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 0809a19fe0..fb78c94062 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -90,7 +90,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), ("map", "cdidx map [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), - ("outline", "cdidx outline [--db ] [--json] [--pretty] [--compact] [--verbose]"), + ("outline", "cdidx outline [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ]"), ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config] [--check-updates]"), ("workspace", "cdidx workspace [name] [--json]"), ("config", "cdidx config show [--json]"), diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs index 0d2d520a32..530371a123 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -322,6 +322,57 @@ public void M6() { } } } + [Fact] + public void RunOutline_CompactJson_UsesExplicitLimit_Issue3009() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_compact_limit_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/many.cs", + "csharp", + """ + public class Many + { + public void M0() { } + public void M1() { } + public void M2() { } + public void M3() { } + public void M4() { } + public void M5() { } + public void M6() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/many.cs", "--db", dbPath, "--compact", "--limit", "2"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToList(); + var symbolTruncation = json + .GetProperty("truncation") + .GetProperty("sections") + .GetProperty("symbols"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("compact").GetBoolean()); + Assert.Equal(2, json.GetProperty("compact_limit").GetInt32()); + Assert.Equal(2, symbols.Count); + Assert.Equal(2, symbolTruncation.GetProperty("returned").GetInt32()); + Assert.Equal(QueryCommandRunner.DefaultCompactSectionLimit + 3, symbolTruncation.GetProperty("source_count").GetInt32()); + Assert.True(symbolTruncation.GetProperty("truncated").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunOutline_UsesNestedSymbolDepthInHumanOutput() { From 537827d180563616ffcfd4139983738f5be4da06 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 01:58:08 +0900 Subject: [PATCH 7/9] Align console usage for pretty JSON (#2996) --- tests/CodeIndex.Tests/ConsoleUiTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 056b0da6bf..a3c5344c3a 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -115,7 +115,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx references |--query |-- ", output); Assert.Contains("cdidx callers |--query |-- ", output); Assert.Contains("cdidx callees |--query |-- ", output); - Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank]", output); + Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); Assert.Contains("cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]", output); Assert.Contains("cdidx inspect |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); @@ -265,7 +265,7 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() { var output = CaptureFullUsageOutput(showBanner: false); - Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank]", output); + Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); Assert.Contains("cdidx symbols [query|--query |-- ] [--name ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]", output); Assert.Contains("cdidx hotspots [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output); @@ -884,7 +884,7 @@ public void PrintCompletions_ReportFlagSetsMatchAcrossShells() var expected = new SortedSet(StringComparer.Ordinal) { - "db", "json", "quiet", "silent", "no-progress", "output", "log-lines", "no-log", "include-args", + "db", "json", "pretty", "quiet", "silent", "no-progress", "output", "log-lines", "no-log", "include-args", }; Assert.Equal(expected, bashReport); Assert.Equal(expected, zshReport); From 4d3b3abac27232c3c296bb5902483be47774e7ff Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 01:58:34 +0900 Subject: [PATCH 8/9] Align console usage for compact map (#3009) --- tests/CodeIndex.Tests/ConsoleUiTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index a3c5344c3a..5bf5bf654e 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -148,7 +148,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]", output); Assert.Contains("--focus-column find/excerpt: focus a specific 1-based column", output); Assert.Contains("--focus-line find/excerpt: focus a specific line", output); - Assert.Contains("cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]", output); + Assert.Contains("cdidx map [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]", output); Assert.Contains("cdidx symbols [query|--query |-- ] [--name ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]", output); Assert.Contains("cdidx validate [--db ] [--json] [--format ] [--verbose] [--kind ] [--path ]", output); From 3e1be8332b0aa3c15c0c9b6ac6b5d11e4ffde01d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 01:59:15 +0900 Subject: [PATCH 9/9] Align console usage for inspect fields (#3056) --- tests/CodeIndex.Tests/ConsoleUiTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 5bf5bf654e..86a4f7ae7b 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -118,7 +118,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); Assert.Contains("cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]", output); - Assert.Contains("cdidx inspect |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); + Assert.Contains("cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); Assert.Contains("--snippet-lines search/find snippet length (1-20, default: search 8; find 1)", output); Assert.Contains("--snippet-focus search only: long-line focus mode (leftmost|quality|proximity, default: quality)", output); Assert.Contains("--max-line-width search/references/callers/callees/find/excerpt/impact/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: 512)", output);