From 7af4fafa204adf0227983015d192e65199d98dad Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:04:45 +0900 Subject: [PATCH 1/3] Add validate result limits (#2992) --- README.md | 2 +- USER_GUIDE.md | 4 +- changelog.d/unreleased/2992.fixed.md | 18 ++++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 8 +++- src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 5 +- src/CodeIndex/Database/DbReader.cs | 6 ++- tests/CodeIndex.Tests/ConsoleUiTests.cs | 2 +- .../QueryCommandRunnerValidateTests.cs | 46 +++++++++++++++++++ 9 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/2992.fixed.md diff --git a/README.md b/README.md index 5a2385e2cd..c1ddc5e069 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,7 @@ GPG 検証が成功した後にこの fingerprint の設定も必須です。 ### Validate -`cdidx validate [--db ] [--json] [--verbose] [--kind ] [--path ]` +`cdidx validate [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--path ]` は、index 済みファイルの replacement character (`U+FFFD`)、BOM、NUL byte、 混在改行、UTF-16 BOM、非 UTF-8 らしい内容などを報告します。validation finding は 出力で報告され、それ自体では command failure になりません。DB を読めない場合や diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 70db1f8983..9842b15b4c 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -405,7 +405,7 @@ conflicting instructions. ```bash cdidx validate cdidx validate --kind replacement_char --path src/ -cdidx validate --json --path legacy/ +cdidx validate --json --limit 50 --path legacy/ ``` `validate` reports indexed files that are likely to produce misleading snippets @@ -2591,7 +2591,7 @@ render できます。 ```bash cdidx validate cdidx validate --kind replacement_char --path src/ -cdidx validate --json --path legacy/ +cdidx validate --json --limit 50 --path legacy/ ``` `validate` は、snippet や symbol name を誤らせやすい indexed file を報告します。 diff --git a/changelog.d/unreleased/2992.fixed.md b/changelog.d/unreleased/2992.fixed.md new file mode 100644 index 0000000000..71d906720e --- /dev/null +++ b/changelog.d/unreleased/2992.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 2992 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Database/DbReader.cs + - tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs +--- + +## English + +- **`validate` now supports explicit result limits (#2992)** — pass `--limit ` or `--top ` to cap reported validation issues for large repositories and agent audit loops while preserving the existing all-results default. + +## 日本語 + +- **`validate` が明示的な結果件数制限に対応しました (#2992)** — `--limit ` または `--top ` で、大きなリポジトリや agent audit loop の validation issue 表示件数を制限できます。既定の全件表示は維持されます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index d8ea77804f..8b092f33a2 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -75,10 +75,14 @@ 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", "deps", "impact", "unused", "hotspots", "validate", ]; - private static readonly string[] LangCapableCommands = LimitCapableCommands; + private static readonly string[] LangCapableCommands = + [ + "search", "definition", "goto", "references", "callers", "callees", "symbols", + "files", "find", "map", "inspect", "deps", "impact", "unused", "hotspots", + ]; private static readonly string[] PathFilterCommands = [ diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 30905a8a40..26cd45f9d5 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -99,7 +99,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"), ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), ("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"), - ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--kind ] [--path ]"), + ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--path ]"), ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), ("deps", "cdidx deps [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse] [--cycles]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 1ff98aad44..d05e1f9044 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -4917,7 +4917,10 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption return WithDb(options, jsonOptions, reader => { - var issues = reader.GetIssues(options.Kind, options.PathPatterns); + var issueLimit = HasOption(cmdArgs, "--limit") || HasOption(cmdArgs, "--top") + ? options.Limit + : (int?)null; + var issues = reader.GetIssues(options.Kind, options.PathPatterns, issueLimit); var issuesAvailable = reader._hasIssuesTable; if (issues.Count == 0) { diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 8b7a86925e..1c8381881d 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1744,7 +1744,7 @@ string text when DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeS /// Get all file validation issues from the index. /// インデックスから全ファイル検証問題を取得する。 /// - public List GetIssues(string? kind = null, IReadOnlyList? pathPatterns = null) + public List GetIssues(string? kind = null, IReadOnlyList? pathPatterns = null, int? limit = null) { if (!_hasIssuesTable) return new List(); using var cmd = _conn.CreateCommand(); @@ -1766,6 +1766,8 @@ FROM file_issues i sql += " AND (" + string.Join(" OR ", ors) + ")"; } sql += " ORDER BY f.path, i.line"; + if (limit.HasValue) + sql += " LIMIT @limit"; cmd.CommandText = sql; if (kind != null) @@ -1775,6 +1777,8 @@ FROM file_issues i for (int i = 0; i < pathPatterns.Count; i++) cmd.Parameters.AddWithValue($"@pathPattern{i}", BuildPathLikePattern(pathPatterns[i])); } + if (limit.HasValue) + cmd.Parameters.AddWithValue("@limit", limit.Value); var results = new List(); using var reader = cmd.ExecuteTrackedReader(); diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 056b0da6bf..658b6beffb 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -151,7 +151,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]", 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); + Assert.Contains("cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--path ]", output); Assert.Contains("Note: if a query itself starts with '-', pass it with --query or -- ", output); Assert.DoesNotContain("cdidx validate [--db ] [--json] [--limit ] [--lang ]", output); Assert.Contains("cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs index 7acc526210..12bd97b719 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs @@ -8,6 +8,52 @@ namespace CodeIndex.Tests; public partial class QueryCommandRunnerTests { + [Fact] + public void RunValidate_LimitAndTopCapReturnedIssues_Issue2992() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_limit"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllBytes( + Path.Combine(projectRoot, "src", "bom.cs"), + [0xEF, 0xBB, 0xBF, .. System.Text.Encoding.UTF8.GetBytes("class Bom {}\n")]); + File.WriteAllText( + Path.Combine(projectRoot, "src", "mixed.cs"), + "class Mixed {}\r\nclass Other {}\n"); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--db", dbPath, "--json", "--quiet"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + var (limitExitCode, limitStdout, limitStderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json", "--limit", "1"], + _jsonOptions)); + var (topExitCode, topStdout, topStderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json", "--top", "1"], + _jsonOptions)); + + using var limitDocument = ParseJsonOutput(limitStdout); + using var topDocument = ParseJsonOutput(topStdout); + + Assert.Equal(CommandExitCodes.Success, limitExitCode); + Assert.Equal(CommandExitCodes.Success, topExitCode); + Assert.Equal(string.Empty, limitStderr); + Assert.Equal(string.Empty, topStderr); + Assert.Equal(1, limitDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, limitDocument.RootElement.GetProperty("issues").GetArrayLength()); + Assert.Equal(1, topDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, topDocument.RootElement.GetProperty("issues").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunValidate_KindFilterNarrowsIssues() { From 408a175ac7ef4319ff16f2a9e88ecea2f31adf11 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:16:42 +0900 Subject: [PATCH 2/3] Add validate severity filtering (#3008) --- README.md | 6 +- USER_GUIDE.md | 8 ++- changelog.d/unreleased/3008.fixed.md | 17 +++++ src/CodeIndex/Cli/CliFlagSchema.cs | 2 + src/CodeIndex/Cli/ConsoleUi.cs | 3 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 26 +++++++- src/CodeIndex/Database/DbReader.cs | 10 ++- tests/CodeIndex.Tests/ConsoleUiTests.cs | 3 +- ...QueryCommandRunnerValidateMetadataTests.cs | 63 +++++++++++++++++++ 9 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 changelog.d/unreleased/3008.fixed.md diff --git a/README.md b/README.md index c1ddc5e069..2eafdc9277 100644 --- a/README.md +++ b/README.md @@ -424,11 +424,13 @@ GPG 検証が成功した後にこの fingerprint の設定も必須です。 ### Validate -`cdidx validate [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--path ]` +`cdidx validate [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]` は、index 済みファイルの replacement character (`U+FFFD`)、BOM、NUL byte、 混在改行、UTF-16 BOM、非 UTF-8 らしい内容などを報告します。validation finding は 出力で報告され、それ自体では command failure になりません。DB を読めない場合や -引数が不正な場合は non-zero で終了します。機械処理には `--json` を使えます。 +引数が不正な場合は non-zero で終了します。`--severity warning` を使うと、 +意図的な U+FFFD literal などの informational finding を除外できます。 +機械処理には `--json` を使えます。 ### シェル補完 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 9842b15b4c..0ef84c368f 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -405,6 +405,7 @@ conflicting instructions. ```bash cdidx validate cdidx validate --kind replacement_char --path src/ +cdidx validate --kind replacement_char --severity warning --path src/ cdidx validate --json --limit 50 --path legacy/ ``` @@ -414,6 +415,8 @@ CR-only line endings, likely non-UTF-8 content, and Git LFS pointer placeholders For `replacement_char`, JSON and MCP responses include `origin` (`source_literal` or `decode_replacement`) and `severity` so agents can distinguish intentional U+FFFD literals from likely encoding damage. +Use `--severity warning` to hide informational source literals and focus on +findings that indicate likely encoding damage. LFS pointers are recorded as `lfs_pointer_skipped` and their placeholder body is not indexed; run `git lfs pull` and then `cdidx index .` to index the real file content. @@ -2591,6 +2594,7 @@ render できます。 ```bash cdidx validate cdidx validate --kind replacement_char --path src/ +cdidx validate --kind replacement_char --severity warning --path src/ cdidx validate --json --limit 50 --path legacy/ ``` @@ -2599,7 +2603,9 @@ cdidx validate --json --limit 50 --path legacy/ ending、likely non-UTF-8 content、Git LFS pointer placeholder などです。 `replacement_char` の JSON / MCP response には `origin` (`source_literal` / `decode_replacement`) と `severity` が入り、意図的な U+FFFD literal と -エンコーディング破損の可能性を agent が区別できます。LFS pointer +エンコーディング破損の可能性を agent が区別できます。`--severity warning` +を使うと、informational な source literal を隠して、エンコーディング破損の +可能性がある finding に集中できます。LFS pointer は `lfs_pointer_skipped` として記録され、placeholder 本文は index されません。 実体を index するには `git lfs pull` の後に `cdidx index .` を再実行してください。 diff --git a/changelog.d/unreleased/3008.fixed.md b/changelog.d/unreleased/3008.fixed.md new file mode 100644 index 0000000000..099d277227 --- /dev/null +++ b/changelog.d/unreleased/3008.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 3008 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Database/DbReader.cs + - tests/CodeIndex.Tests/QueryCommandRunnerValidateMetadataTests.cs +--- + +## English + +- **`validate` can filter findings by severity (#3008)** — `--severity warning` focuses JSON and human output on warning-level findings such as likely decode replacements while hiding informational source literals. + +## 日本語 + +- **`validate` で finding の severity filter を指定できるようにしました (#3008)** — `--severity warning` により、informational な source literal を隠し、decode replacement の可能性がある warning-level finding に集中できます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 8b092f33a2..2ace4709f4 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -111,6 +111,7 @@ internal static class CliFlagSchema [ "definition", "goto", "references", "callers", "callees", "symbols", "unused", "hotspots", "validate", ]; + private static readonly string[] SeverityCommands = ["validate"]; private static readonly string[] VisibilityCommands = [ "definition", "symbols", "unused", "hotspots", @@ -229,6 +230,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--exclude-tests", Description = "Exclude tests", Commands = Set(ExcludeFilterCommands) }, new() { Name = "--include-generated", Description = "Include generated files", Commands = Set(ExcludeFilterCommands) }, new() { Name = "--kind", ValuePlaceholder = "", Description = "Filter by kind", Commands = Set(KindCommands) }, + new() { Name = "--severity", ValuePlaceholder = "", Description = "Validate: filter validation issues by severity", Commands = Set(SeverityCommands) }, new() { Name = "--visibility", ValuePlaceholder = "", Description = "Filter by symbol visibility", Commands = Set(VisibilityCommands) }, new() { Name = "--exclude-visibility", ValuePlaceholder = "", Description = "Exclude symbol visibility", Commands = Set(VisibilityCommands) }, new() { Name = "--by-bucket", Description = "Unused: include per-bucket grouped result arrays in JSON output", Commands = Set(ByBucketCommands) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 26cd45f9d5..3c6ad01871 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -99,7 +99,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"), ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), ("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"), - ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--path ]"), + ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]"), ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), ("deps", "cdidx deps [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse] [--cycles]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), @@ -975,6 +975,7 @@ private static void PrintFlagReference(Action WriteHelpLine) Console.WriteLine(" Legacy/stale-fold DBs fall back to ASCII NOCASE;"); Console.WriteLine(" run `cdidx backfill-fold` or check fold_ready."); WriteHelpLine(" --kind definition/symbols/hotspots/unused: symbol kind; references: reference kind (call/instantiate/subscribe/attribute/annotation); callers/callees: call-graph kinds only (call/instantiate/subscribe — metadata kinds rejected, use references instead); validate: issue kind"); + Console.WriteLine(" --severity validate only: filter issues by severity: info, warning, error"); Console.WriteLine(" --visibility Filter symbols/definitions/unused/hotspots by visibility: public, protected, internal, private"); WriteHelpLine(" --exclude-visibility Exclude symbols/definitions/unused/hotspots by visibility"); WriteHelpLine(" --count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index d05e1f9044..f74992880f 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -4893,6 +4893,8 @@ private static JsonObject BuildUnusedResultsByBucketJson(IEnumerable]"); + return CommandExitCodes.UsageError; + } return WithDb(options, jsonOptions, reader => { var issueLimit = HasOption(cmdArgs, "--limit") || HasOption(cmdArgs, "--top") ? options.Limit : (int?)null; - var issues = reader.GetIssues(options.Kind, options.PathPatterns, issueLimit); + var issues = reader.GetIssues(options.Kind, options.PathPatterns, issueLimit, options.Severity); var issuesAvailable = reader._hasIssuesTable; if (issues.Count == 0) { @@ -5085,6 +5095,7 @@ public static QueryCommandOptions ParseArgs( int limit = ResolveDefaultPositiveInt(DefaultLimitEnvironmentVariable, DefaultQueryLimit, "--limit", out var defaultLimitError); string? lang = null; string? kind = null; + string? severity = null; string? query = null; bool rawFts = false; bool includeBody = false; @@ -5420,6 +5431,17 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) else AddParseError(kindError!); break; + case "--severity": + if (TryReadStringOptionValue(args, ref i, "--severity", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var severityValue, out var severityError)) + { + WarnIfDuplicateSingleValueOption("--severity", severityValue!); + severity = severityValue?.ToLowerInvariant(); + } + else + { + AddParseError(severityError!); + } + break; case "--visibility": if (TryReadStringOptionValue(args, ref i, "--visibility", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var visibilityValue, out var visibilityError)) AddVisibilityFilterValues("--visibility", visibilityValue!, visibilityFilters, AddParseError); @@ -5881,6 +5903,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) Limit = limit, Lang = lang, Kind = kind, + Severity = severity, Query = query, RawFts = rawFts, IncludeBody = includeBody, @@ -8636,6 +8659,7 @@ public sealed class QueryCommandOptions public int Limit { get; init; } = 20; public string? Lang { get; init; } public string? Kind { get; init; } + public string? Severity { get; init; } public List VisibilityFilters { get; init; } = []; public List ExcludeVisibilityFilters { get; init; } = []; public string? Query { get; init; } diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 1c8381881d..734da52f28 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1744,7 +1744,11 @@ string text when DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeS /// Get all file validation issues from the index. /// インデックスから全ファイル検証問題を取得する。 /// - public List GetIssues(string? kind = null, IReadOnlyList? pathPatterns = null, int? limit = null) + public List GetIssues( + string? kind = null, + IReadOnlyList? pathPatterns = null, + int? limit = null, + string? severity = null) { if (!_hasIssuesTable) return new List(); using var cmd = _conn.CreateCommand(); @@ -1757,6 +1761,8 @@ FROM file_issues i WHERE 1=1"; if (kind != null) sql += " AND i.kind = @kind"; + if (severity != null) + sql += " AND " + severityColumn + " = @severity"; if (pathPatterns is { Count: > 0 }) { // OR multiple path filters / 複数パスフィルタを OR で結合 @@ -1772,6 +1778,8 @@ FROM file_issues i cmd.CommandText = sql; if (kind != null) cmd.Parameters.AddWithValue("@kind", kind); + if (severity != null) + cmd.Parameters.AddWithValue("@severity", severity); if (pathPatterns is { Count: > 0 }) { for (int i = 0; i < pathPatterns.Count; i++) diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 658b6beffb..6c2c671307 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -137,6 +137,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains(" Legacy/stale-fold DBs fall back to ASCII NOCASE;", output); Assert.Contains(" run `cdidx backfill-fold` or check fold_ready.", output); Assert.Contains("--kind definition/symbols/hotspots/unused: symbol kind; references: reference kind (call/instantiate/subscribe/attribute/annotation); callers/callees: call-graph kinds only (call/instantiate/subscribe — metadata kinds rejected, use references instead); validate: issue kind", output); + Assert.Contains("--severity validate only: filter issues by severity: info, warning, error", output); Assert.Contains("--count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts", output); Assert.Contains("--no-dedup search only: return every raw overlapping chunk hit (debug/density)", output); Assert.Contains("--commits [commit-ref ...]", output); @@ -151,7 +152,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]", 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] [--limit |--top ] [--kind ] [--path ]", output); + Assert.Contains("cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]", output); Assert.Contains("Note: if a query itself starts with '-', pass it with --query or -- ", output); Assert.DoesNotContain("cdidx validate [--db ] [--json] [--limit ] [--lang ]", output); Assert.Contains("cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerValidateMetadataTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerValidateMetadataTests.cs index a0e497ea3c..617844fe30 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerValidateMetadataTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerValidateMetadataTests.cs @@ -46,4 +46,67 @@ public void RunValidate_ReplacementCharJson_IncludesOriginAndSeverity() TestProjectHelper.DeleteDirectory(projectRoot); } } + + [Fact] + public void RunValidate_SeverityFilterNarrowsIssues_Issue3008() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_severity_filter"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "literal.cs"), + "class Literal { const char Value = '\uFFFD'; }\n"); + + var bytes = new List(); + void AddUtf8(string text) => bytes.AddRange(System.Text.Encoding.UTF8.GetBytes(text)); + AddUtf8("line1 clean\n"); + AddUtf8("line2 has "); + bytes.Add(0xFF); + AddUtf8(" here\n"); + for (var i = 0; i < 200; i++) + AddUtf8("filler ascii ascii ascii\n"); + File.WriteAllBytes(Path.Combine(projectRoot, "src", "decode.cs"), bytes.ToArray()); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--db", dbPath, "--json", "--quiet"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + var (warningExitCode, warningStdout, warningStderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json", "--kind", "replacement_char", "--severity", "warning"], + _jsonOptions)); + var (infoExitCode, infoStdout, infoStderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json", "--kind", "replacement_char", "--severity", "info"], + _jsonOptions)); + + using var warningDocument = ParseJsonOutput(warningStdout); + using var infoDocument = ParseJsonOutput(infoStdout); + var warningIssues = warningDocument.RootElement.GetProperty("issues"); + var infoIssues = infoDocument.RootElement.GetProperty("issues"); + + Assert.Equal(CommandExitCodes.Success, warningExitCode); + Assert.Equal(CommandExitCodes.Success, infoExitCode); + Assert.Equal(string.Empty, warningStderr); + Assert.Equal(string.Empty, infoStderr); + Assert.True(warningIssues.GetArrayLength() > 0); + Assert.Equal(warningIssues.GetArrayLength(), warningDocument.RootElement.GetProperty("count").GetInt32()); + Assert.All(warningIssues.EnumerateArray(), issue => + { + Assert.Equal("replacement_char", issue.GetProperty("kind").GetString()); + Assert.Equal(FileIssue.SeverityWarning, issue.GetProperty("severity").GetString()); + Assert.Equal(FileIssue.OriginDecodeReplacement, issue.GetProperty("origin").GetString()); + }); + Assert.Equal(1, infoDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, infoIssues.GetArrayLength()); + Assert.Equal(FileIssue.SeverityInfo, infoIssues[0].GetProperty("severity").GetString()); + Assert.Equal(FileIssue.OriginSourceLiteral, infoIssues[0].GetProperty("origin").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } } From 2892890cac237a1f48938184d4bb29f02f6cc9e4 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Thu, 4 Jun 2026 23:27:04 +0900 Subject: [PATCH 3/3] Add validate JSON array output (#3010) --- README.md | 4 +- USER_GUIDE.md | 7 +- changelog.d/unreleased/3010.fixed.md | 16 ++++ src/CodeIndex/Cli/CliFlagSchema.cs | 2 +- src/CodeIndex/Cli/ConsoleUi.cs | 4 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 34 +++++++-- tests/CodeIndex.Tests/ConsoleUiTests.cs | 4 +- .../QueryCommandRunnerValidateTests.cs | 76 +++++++++++++++++++ 8 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 changelog.d/unreleased/3010.fixed.md diff --git a/README.md b/README.md index 2eafdc9277..c6ec0467e1 100644 --- a/README.md +++ b/README.md @@ -424,13 +424,13 @@ GPG 検証が成功した後にこの fingerprint の設定も必須です。 ### Validate -`cdidx validate [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]` +`cdidx validate [--db ] [--json[=array]] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]` は、index 済みファイルの replacement character (`U+FFFD`)、BOM、NUL byte、 混在改行、UTF-16 BOM、非 UTF-8 らしい内容などを報告します。validation finding は 出力で報告され、それ自体では command failure になりません。DB を読めない場合や 引数が不正な場合は non-zero で終了します。`--severity warning` を使うと、 意図的な U+FFFD literal などの informational finding を除外できます。 -機械処理には `--json` を使えます。 +機械処理には `--json` を使え、配列だけが必要な pipeline では `--json=array` を使えます。 ### シェル補完 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 0ef84c368f..1af732828b 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -406,6 +406,7 @@ conflicting instructions. cdidx validate cdidx validate --kind replacement_char --path src/ cdidx validate --kind replacement_char --severity warning --path src/ +cdidx validate --json=array --limit 50 --path legacy/ cdidx validate --json --limit 50 --path legacy/ ``` @@ -417,6 +418,8 @@ or `decode_replacement`) and `severity` so agents can distinguish intentional U+FFFD literals from likely encoding damage. Use `--severity warning` to hide informational source literals and focus on findings that indicate likely encoding damage. +Use `--json=array` when a pipeline expects a bare issue array instead of the +default `{ "count": ..., "issues": [...] }` object. LFS pointers are recorded as `lfs_pointer_skipped` and their placeholder body is not indexed; run `git lfs pull` and then `cdidx index .` to index the real file content. @@ -2595,6 +2598,7 @@ render できます。 cdidx validate cdidx validate --kind replacement_char --path src/ cdidx validate --kind replacement_char --severity warning --path src/ +cdidx validate --json=array --limit 50 --path legacy/ cdidx validate --json --limit 50 --path legacy/ ``` @@ -2605,7 +2609,8 @@ ending、likely non-UTF-8 content、Git LFS pointer placeholder などです。 `decode_replacement`) と `severity` が入り、意図的な U+FFFD literal と エンコーディング破損の可能性を agent が区別できます。`--severity warning` を使うと、informational な source literal を隠して、エンコーディング破損の -可能性がある finding に集中できます。LFS pointer +可能性がある finding に集中できます。pipeline が既定の `{ "count": ..., "issues": [...] }` +object ではなく bare issue array を期待する場合は `--json=array` を使えます。LFS pointer は `lfs_pointer_skipped` として記録され、placeholder 本文は index されません。 実体を index するには `git lfs pull` の後に `cdidx index .` を再実行してください。 diff --git a/changelog.d/unreleased/3010.fixed.md b/changelog.d/unreleased/3010.fixed.md new file mode 100644 index 0000000000..af46d50dbb --- /dev/null +++ b/changelog.d/unreleased/3010.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3010 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs +--- + +## English + +- **`validate` supports JSON array shaping (#3010)** — `--json=array` now returns a bare issue array, including `[]` for clean indexes, while plain `--json` keeps the existing `{ count, issues }` object. + +## 日本語 + +- **`validate` が JSON array shaping に対応しました (#3010)** — `--json=array` は clean index では `[]`、finding がある場合は bare issue array を返し、通常の `--json` は従来どおり `{ count, issues }` object を維持します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 2ace4709f4..ff8cfa6488 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -211,7 +211,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--immutable", Description = "Alias for --read-only", Commands = Set(ReadOnlyDbCommands) }, 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 = "--json", Description = "JSON output; search and validate also accept --json=array for a single JSON array", 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 3c6ad01871..4005698d60 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -99,7 +99,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"), ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), ("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"), - ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]"), + ("validate", "cdidx validate [--db ] [--json[=array]] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]"), ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), ("deps", "cdidx deps [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse] [--cycles]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), @@ -943,7 +943,7 @@ private static void PrintFlagReference(Action WriteHelpLine) Console.WriteLine(); Console.WriteLine("Query options:"); Console.WriteLine(" --db Database file path (default: .cdidx/codeindex.db in current directory)"); - WriteHelpLine(" --json Output as JSON (search streams ndjson by default; use search --json=array for one array)"); + WriteHelpLine(" --json Output as JSON (search streams ndjson by default; use search/validate --json=array for one array)"); WriteHelpLine(" --verbose Query commands: emit debug diagnostics to stderr; with --json, append an _debug JSON object"); WriteHelpLine(" --quiet, -q, --silent Query commands: suppress informational stderr output, including zero-result hints and summaries; errors still print. Overrides --verbose stderr text."); WriteHelpLine(" --profile Read commands: append SQL timing, row-count, and EXPLAIN QUERY PLAN JSON after the normal result"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index f74992880f..1c15bba102 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -4938,6 +4938,13 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption { if (TryWriteEmptyFormattedResult(options, jsonOptions)) return CommandExitCodes.Success; + if (options.OutputFormat == OutputFormatJson && options.JsonOutputFormat == JsonOutputFormatArray) + { + Console.WriteLine(JsonSerializer.Serialize( + new List(), + CliJsonSerializerContextFactory.Create(jsonOptions).ListFileIssue)); + return CommandExitCodes.Success; + } Console.WriteLine(new JsonObject { ["count"] = 0, @@ -4978,6 +4985,13 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption WriteSarif(issues.Select(i => (i.Path, i.Line, 1, i.Message, i.Kind)), jsonOptions); return CommandExitCodes.Success; } + if (options.OutputFormat == OutputFormatJson && options.JsonOutputFormat == JsonOutputFormatArray) + { + Console.WriteLine(JsonSerializer.Serialize( + issues, + CliJsonSerializerContextFactory.Create(jsonOptions).ListFileIssue)); + return CommandExitCodes.Success; + } Console.WriteLine(new JsonObject { ["count"] = issues.Count, @@ -6842,16 +6856,26 @@ private static bool TryWriteUnsupportedOptionError(string commandName, string[] continue; } - var normalizedArg = TrySplitInlineOptionValue(arg, out var inlineOptionName) - ? inlineOptionName! - : arg; + var inlineValue = TrySplitInlineOptionValue(arg, out var inlineOptionName) + ? arg[(inlineOptionName!.Length + 1)..] + : null; + var normalizedArg = inlineOptionName ?? arg; if (arg.StartsWith("--check=", StringComparison.Ordinal) && supported.Contains("--check")) normalizedArg = "--check"; if (normalizedArg == "--json" && !string.Equals(arg, "--json", StringComparison.Ordinal) && commandName != "search") { + if (commandName == "validate" && string.Equals(inlineValue, JsonOutputFormatArray, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + CommandErrorWriter.Write( - "--json= is only supported by 'search'.", - "use plain `--json` here, or rerun search with `--json=array`.", + commandName == "validate" + ? "--json= for validate only supports 'array'." + : "--json= is only supported by 'search' and validate's array output.", + commandName == "validate" + ? "use plain `--json` or `--json=array`." + : "use plain `--json` here, rerun search with `--json=array`, or rerun validate with `--json=array`.", GetUsageLineOrThrow(commandName)); return true; } diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 6c2c671307..5e6ed9dfe9 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -152,12 +152,12 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]", 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] [--limit |--top ] [--kind ] [--severity ] [--path ]", output); + Assert.Contains("cdidx validate [--db ] [--json[=array]] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]", output); Assert.Contains("Note: if a query itself starts with '-', pass it with --query or -- ", output); Assert.DoesNotContain("cdidx validate [--db ] [--json] [--limit ] [--lang ]", output); Assert.Contains("cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output); Assert.Contains("cdidx hotspots [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--group-by ] [--group-by-name]", output); - Assert.Contains("--json Output as JSON (search streams ndjson by default; use search --json=array for one array)", output); + Assert.Contains("--json Output as JSON (search streams ndjson by default; use search/validate --json=array for one array)", output); Assert.Contains("--lang Filter by language (aliases: bat, cmd, cshtml, razor, ts, tsx, cts, mts)", output); Assert.Contains("--bytes Show raw byte counts in human output for files/map instead of binary units; JSON always keeps raw integer bytes", output); Assert.Contains("--group-by-name hotspots: collapse rows sharing (name, kind) across files; JSON count is the number of name/kind groups, not reference, file, or definition-site count", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs index 12bd97b719..da0d11a7cd 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs @@ -54,6 +54,82 @@ public void RunValidate_LimitAndTopCapReturnedIssues_Issue2992() } } + [Fact] + public void RunValidate_JsonArrayEmitsIssueArray_Issue3010() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_json_array"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllBytes( + Path.Combine(projectRoot, "src", "bom.cs"), + [0xEF, 0xBB, 0xBF, .. System.Text.Encoding.UTF8.GetBytes("class Bom {}\n")]); + File.WriteAllText( + Path.Combine(projectRoot, "src", "clean.cs"), + "class Clean {}\n"); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--db", dbPath, "--json", "--quiet"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json=array", "--limit", "1"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var root = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(JsonValueKind.Array, root.ValueKind); + Assert.Equal(1, root.GetArrayLength()); + Assert.Equal("bom", root[0].GetProperty("kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunValidate_JsonArrayEmptyEmitsEmptyArray_Issue3010() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_json_array_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "clean.cs"), + "class Clean {}\n"); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--db", dbPath, "--json", "--quiet"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( + ["--db", dbPath, "--json=array"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var root = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(JsonValueKind.Array, root.ValueKind); + Assert.Empty(root.EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunValidate_KindFilterNarrowsIssues() {