From 3fe33ff8b9fe12437324496e88994c8f20254820 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:14:53 +0900 Subject: [PATCH 1/6] Expand risky-code dogfooding recipes Issue: #3385 --- src/CodeIndex/Cli/SearchAuditRecipes.cs | 32 ++++++++++++++++++- .../QueryCommandRunnerSearchTests.cs | 5 ++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index 7e8afde3d2..2ed9e24624 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -39,7 +39,37 @@ internal static class SearchAuditRecipes "CancellationToken.None", "Find async or stream paths that may be ignoring caller cancellation.", ["audit", "bug"], - "False positives include intentionally fire-and-forget work and APIs that have no meaningful caller cancellation token.") + "False positives include intentionally fire-and-forget work and APIs that have no meaningful caller cancellation token."), + new( + "process-start-info", + "ProcessStartInfo", + "Find external process launch configuration that may need argument, environment, cwd, and shell-use review.", + ["audit", "security"], + "False positives include tests and launch wrappers that already validate arguments and disable shell expansion."), + new( + "process-start-direct", + "Process.Start", + "Find direct process launches that may need a shared safe-launch wrapper or explicit argument handling.", + ["audit", "security"], + "False positives include simple URL/document open helpers or test fixtures with trusted inputs."), + new( + "recursive-delete", + "Directory.Delete", + "Find recursive or broad delete operations that may need path-boundary and symlink/reparse-point review.", + ["audit", "security"], + "False positives include isolated temporary-directory cleanup guarded by test helpers or workspace-root containment checks."), + new( + "infinite-timeout", + "Timeout.InfiniteTimeSpan", + "Find infinite waits that may need bounded timeouts, cancellation, or liveness reporting.", + ["audit", "bug"], + "False positives include deliberate sentinel values that are never passed to blocking waits."), + new( + "path-case-heuristic", + "OrdinalIgnoreCase", + "Find case-insensitive path or identifier comparisons that may need filesystem case-sensitivity awareness.", + ["audit", "portability"], + "False positives include non-path protocol tokens, CLI option names, labels, and other intentionally case-insensitive domains.") ]) ]; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 8f1cd7d55d..68c779063a 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -301,6 +301,9 @@ public void RunSearch_ListRecipesJsonIncludesBuiltInAuditMetadata_Issue3144() Assert.True(query.GetProperty("exact_substring").GetBoolean()); Assert.Contains("redaction", query.GetProperty("description").GetString(), StringComparison.OrdinalIgnoreCase); Assert.Contains("False positives", query.GetProperty("false_positive_guidance").GetString(), StringComparison.OrdinalIgnoreCase); + Assert.Contains( + recipe.GetProperty("queries").EnumerateArray(), + item => item.GetProperty("name").GetString() == "process-start-info"); } [Theory] @@ -369,7 +372,7 @@ public void Run(Exception ex, CancellationToken token) .Single(item => item.GetProperty("name").GetString() == "unbounded-json-parse"); Assert.Equal("risky-code", root.GetProperty("recipe").GetProperty("name").GetString()); - Assert.Equal(5, root.GetProperty("query_count").GetInt32()); + Assert.Equal(10, root.GetProperty("query_count").GetInt32()); Assert.True(root.GetProperty("result_count").GetInt32() >= 4); Assert.Equal(1, unboundedJsonParse.GetProperty("count").GetInt32()); Assert.Equal("JsonDocument.Parse", unboundedJsonParse.GetProperty("query").GetString()); From b946d84571e246b6ddf74b4bbf0be944f490c32b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:15:52 +0900 Subject: [PATCH 2/6] Add C# smell audit recipe queries Issue: #3387 --- src/CodeIndex/Cli/SearchAuditRecipes.cs | 26 ++++++++++++++++++- .../QueryCommandRunnerSearchTests.cs | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index 2ed9e24624..3ae18d3fe5 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -40,6 +40,18 @@ internal static class SearchAuditRecipes "Find async or stream paths that may be ignoring caller cancellation.", ["audit", "bug"], "False positives include intentionally fire-and-forget work and APIs that have no meaningful caller cancellation token."), + new( + "empty-catch-review", + "catch", + "Find catch blocks that may be empty, overly broad, or swallowing diagnostic context.", + ["audit", "bug"], + "False positives include catch blocks that rethrow, translate exceptions safely, or intentionally ignore best-effort cleanup failures."), + new( + "broad-exception-catch", + "catch (Exception", + "Find broad C# exception catches that may need narrower exception types or explicit recovery boundaries.", + ["audit", "bug"], + "False positives include top-level command boundaries that intentionally normalize all recoverable failures."), new( "process-start-info", "ProcessStartInfo", @@ -69,7 +81,19 @@ internal static class SearchAuditRecipes "OrdinalIgnoreCase", "Find case-insensitive path or identifier comparisons that may need filesystem case-sensitivity awareness.", ["audit", "portability"], - "False positives include non-path protocol tokens, CLI option names, labels, and other intentionally case-insensitive domains.") + "False positives include non-path protocol tokens, CLI option names, labels, and other intentionally case-insensitive domains."), + new( + "regex-construction", + "new Regex", + "Find direct regex construction that may need a timeout, non-backtracking mode, or bounded input review.", + ["audit", "performance"], + "False positives include precompiled bounded patterns with explicit timeouts or tiny trusted inputs."), + new( + "regex-timeout-handling", + "RegexMatchTimeoutException", + "Find regex timeout handling boundaries that may need consistent diagnostics and recovery behavior.", + ["audit", "bug"], + "False positives include tests and already-normalized parse/validation errors.") ]) ]; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 68c779063a..86bbf3b507 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -372,7 +372,7 @@ public void Run(Exception ex, CancellationToken token) .Single(item => item.GetProperty("name").GetString() == "unbounded-json-parse"); Assert.Equal("risky-code", root.GetProperty("recipe").GetProperty("name").GetString()); - Assert.Equal(10, root.GetProperty("query_count").GetInt32()); + Assert.Equal(14, root.GetProperty("query_count").GetInt32()); Assert.True(root.GetProperty("result_count").GetInt32() >= 4); Assert.Equal(1, unboundedJsonParse.GetProperty("count").GetInt32()); Assert.Equal("JsonDocument.Parse", unboundedJsonParse.GetProperty("query").GetString()); From 822473c2988336fce75988ef56771c4bc0f5d499 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:18:54 +0900 Subject: [PATCH 3/6] Add security audit recipe queries Issue: #3390 --- src/CodeIndex/Cli/SearchAuditRecipes.cs | 41 ++++++++++++++++++- .../QueryCommandRunnerSearchTests.cs | 5 ++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index 3ae18d3fe5..c95afcfb11 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -93,7 +93,46 @@ internal static class SearchAuditRecipes "RegexMatchTimeoutException", "Find regex timeout handling boundaries that may need consistent diagnostics and recovery behavior.", ["audit", "bug"], - "False positives include tests and already-normalized parse/validation errors.") + "False positives include tests and already-normalized parse/validation errors."), + new( + "environment-secret-source", + "GetEnvironmentVariable", + "Find environment-variable reads that may source tokens, secrets, credentials, or operational policy.", + ["audit", "security"], + "False positives include non-secret feature flags and documented public configuration."), + new( + "authorization-handling", + "Authorization", + "Find authorization header or auth-boundary handling that may need redaction and egress review.", + ["audit", "security"], + "False positives include documentation, tests, and already-redacted header-name-only handling."), + new( + "bearer-token-handling", + "Bearer", + "Find bearer token handling that may need storage, logging, and outbound request review.", + ["audit", "security"], + "False positives include examples, tests, and redacted token placeholders."), + new( + "credential-term", + "credential", + "Find credential-related code paths that may need source, persistence, and redaction boundary review.", + ["audit", "security"], + "False positives include natural-language documentation or non-secret credential-type names.", + ExactSubstring: false), + new( + "secret-term", + "secret", + "Find secret-related code paths that may need source, persistence, and redaction boundary review.", + ["audit", "security"], + "False positives include documentation, labels, and comments that do not touch secret material.", + ExactSubstring: false), + new( + "token-term", + "token", + "Find token-related code paths that may need lexical-token versus auth-token triage.", + ["audit", "security"], + "False positives include parser/tokenizer code, syntax tokens, and non-auth identifiers.", + ExactSubstring: false) ]) ]; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 86bbf3b507..597cc75585 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -304,6 +304,9 @@ public void RunSearch_ListRecipesJsonIncludesBuiltInAuditMetadata_Issue3144() Assert.Contains( recipe.GetProperty("queries").EnumerateArray(), item => item.GetProperty("name").GetString() == "process-start-info"); + Assert.Contains( + recipe.GetProperty("queries").EnumerateArray(), + item => item.GetProperty("name").GetString() == "token-term"); } [Theory] @@ -372,7 +375,7 @@ public void Run(Exception ex, CancellationToken token) .Single(item => item.GetProperty("name").GetString() == "unbounded-json-parse"); Assert.Equal("risky-code", root.GetProperty("recipe").GetProperty("name").GetString()); - Assert.Equal(14, root.GetProperty("query_count").GetInt32()); + Assert.Equal(20, root.GetProperty("query_count").GetInt32()); Assert.True(root.GetProperty("result_count").GetInt32() >= 4); Assert.Equal(1, unboundedJsonParse.GetProperty("count").GetInt32()); Assert.Equal("JsonDocument.Parse", unboundedJsonParse.GetProperty("query").GetString()); From 28eefa0908da042a85ab6bcc741ed4b2a269d858 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:44:01 +0900 Subject: [PATCH 4/6] Add audit scope for search recipes Issue: #3440 --- src/CodeIndex/Cli/CliFlagSchema.cs | 1 + src/CodeIndex/Cli/ConsoleUi.cs | 3 +- src/CodeIndex/Cli/JsonOutputContracts.cs | 1 + src/CodeIndex/Cli/QueryCommandRunner.cs | 106 +++++++++++++++++- src/CodeIndex/Cli/SearchAuditRecipes.cs | 23 ++++ tests/CodeIndex.Tests/ConsoleUiTests.cs | 5 +- .../QueryCommandRunnerSearchTests.cs | 5 + .../QueryCommandRunnerTests.cs | 3 + 8 files changed, 139 insertions(+), 8 deletions(-) diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 9593377625..8a94e5cf91 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -278,6 +278,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--recipe", ValuePlaceholder = "", Description = "Search: run a built-in audit recipe query set", Commands = Set("search") }, new() { Name = "--list-recipes", Description = "Search: list built-in audit recipes", Commands = Set("search") }, new() { Name = "--open-issues", ValuePlaceholder = "", Description = "Preflight issue drafts against open issue JSON", Commands = Set("search", "suggestions") }, + new() { Name = "--audit-scope", ValuePlaceholder = "", Description = "Search recipes: use production source defaults or include all indexed paths", Commands = Set("search") }, new() { Name = "--status", ValuePlaceholder = "", Description = "Suggestions: filter by suggestion status", Commands = Set("suggestions") }, new() { Name = "--category", ValuePlaceholder = "", Description = "Suggestions: filter by category", Commands = Set("suggestions") }, new() { Name = "--agent", ValuePlaceholder = "", Description = "Suggestions: filter by agent", Commands = Set("suggestions") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index ba31188742..0d86548475 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -87,7 +87,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 |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--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 |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--audit-scope ] [--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]"), @@ -1034,6 +1034,7 @@ private static void PrintFlagReference(Action WriteHelpLine) WriteHelpLine($" --query Pass a query literal, useful when the query starts with '-' (`search`/`find` max {QueryLimits.MaxQueryLength} chars)"); Console.WriteLine(" --exclude-path Exclude glob-style path patterns (* and ?) (repeatable)"); Console.WriteLine(" --exclude-tests Exclude likely test files"); + WriteHelpLine(" --audit-scope search recipes only: source uses recipe default production-code paths/excludes; all searches docs, tests, changelog, and recipe definitions unless other filters exclude them"); Console.WriteLine(" --include-generated Include generated files in query results"); Console.WriteLine(" --snippet-lines search/find snippet length (1-20, default: search 8; find 1)"); Console.WriteLine(" --snippet-focus search only: long-line focus mode (leftmost|quality|proximity, default: quality)"); diff --git a/src/CodeIndex/Cli/JsonOutputContracts.cs b/src/CodeIndex/Cli/JsonOutputContracts.cs index bb5066b1ee..eecdb8b186 100644 --- a/src/CodeIndex/Cli/JsonOutputContracts.cs +++ b/src/CodeIndex/Cli/JsonOutputContracts.cs @@ -475,6 +475,7 @@ internal sealed record VersionInfoJsonResult( [JsonSerializable(typeof(SearchRecipeQueryListItemJsonResult))] [JsonSerializable(typeof(SearchRecipeQueryResultJsonResult))] [JsonSerializable(typeof(SearchRecipeRunJsonResult))] +[JsonSerializable(typeof(SearchRecipeScopeJsonResult))] [JsonSerializable(typeof(SearchIssueDraftExportJsonResult))] [JsonSerializable(typeof(SearchIssueDraftJsonResult))] [JsonSerializable(typeof(SearchIssueDraftSourceJsonResult))] diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 3b0a8f6536..9adf670508 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -509,6 +509,14 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) "Use an open-issues JSON file from `gh issue list --state open --json number,title,labels,url`."); return CommandExitCodes.UsageError; } + if (options.AuditScopeExplicit && options.RecipeName == null) + { + WriteUsageError( + "--audit-scope is only supported with `cdidx search --recipe `.", + GetUsageLineOrThrow("search"), + "Use `--audit-scope source` for the production-code default or `--audit-scope all` when intentionally auditing docs, tests, and recipe definitions."); + return CommandExitCodes.UsageError; + } if (options.ListRecipes) { if (options.Query != null || options.RecipeName != null || options.ExtraNames.Count > 0) @@ -801,6 +809,11 @@ private static int WriteSearchRecipeList(QueryCommandOptions options, JsonSerial { Console.WriteLine($"{recipe.Name}: {recipe.Description}"); Console.WriteLine($" labels: {string.Join(", ", recipe.RecommendedLabels)}"); + Console.WriteLine($" default scope: {recipe.DefaultScope}"); + if (recipe.DefaultPathPatterns.Count > 0) + Console.WriteLine($" default paths: {string.Join(", ", recipe.DefaultPathPatterns)}"); + if (recipe.DefaultExcludePaths.Count > 0) + Console.WriteLine($" default excludes: {string.Join(", ", recipe.DefaultExcludePaths)}"); foreach (var query in recipe.Queries) { var mode = query.ExactSubstring ? "exact-substring" : "fts"; @@ -827,7 +840,8 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp return WithDb(options, jsonOptions, reader => { - var queryResults = CollectSearchRecipeQueryResults(reader, recipe, options, userExact, out var total); + var scope = BuildSearchRecipeScope(recipe, options); + var queryResults = CollectSearchRecipeQueryResults(reader, recipe, scope, options, userExact, out var total); if (options.Json) { @@ -835,6 +849,7 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp new SearchRecipeRunJsonResult( JsonOutputContract.ApiVersion, ToSearchRecipeListItem(recipe), + scope, recipe.Queries.Count, total, queryResults), @@ -844,6 +859,12 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp Console.WriteLine($"Recipe: {recipe.Name}"); Console.WriteLine(recipe.Description); + Console.WriteLine($"Scope: {scope.Name}"); + if (scope.PathPatterns.Count > 0) + Console.WriteLine($"Paths: {string.Join(", ", scope.PathPatterns)}"); + if (scope.ExcludePaths.Count > 0) + Console.WriteLine($"Excludes: {string.Join(", ", scope.ExcludePaths)}"); + Console.WriteLine($"Exclude tests: {scope.ExcludeTests.ToString().ToLowerInvariant()}"); Console.WriteLine(); foreach (var queryResult in queryResults) { @@ -888,7 +909,8 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS return WithDb(options, jsonOptions, reader => { - var queryResults = CollectSearchRecipeQueryResults(reader, recipe, options, userExact, out var total); + var scope = BuildSearchRecipeScope(recipe, options); + var queryResults = CollectSearchRecipeQueryResults(reader, recipe, scope, options, userExact, out var total); var drafts = queryResults .Where(queryResult => queryResult.Count > 0) .Select(queryResult => ToSearchIssueDraft(recipe, queryResult, preflight)) @@ -897,6 +919,7 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS new SearchIssueDraftExportJsonResult( JsonOutputContract.ApiVersion, ToSearchRecipeListItem(recipe), + scope, recipe.Queries.Count, total, drafts.Count, @@ -913,6 +936,7 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS private static List CollectSearchRecipeQueryResults( DbReader reader, SearchAuditRecipe recipe, + SearchRecipeScopeJsonResult scope, QueryCommandOptions options, bool userExact, out int total) @@ -927,9 +951,9 @@ private static List CollectSearchRecipeQueryR options.Limit, options.Lang, false, - options.PathPatterns, - options.ExcludePaths, - options.ExcludeTests, + scope.PathPatterns, + scope.ExcludePaths, + scope.ExcludeTests, !options.NoDedup, options.Since, exact, @@ -953,6 +977,39 @@ private static List CollectSearchRecipeQueryR return queryResults; } + private static SearchRecipeScopeJsonResult BuildSearchRecipeScope(SearchAuditRecipe recipe, QueryCommandOptions options) + { + var scopeName = options.AuditScopeExplicit ? options.AuditScope : recipe.DefaultScope; + var pathPatterns = new List(options.PathPatterns); + var excludePaths = new List(options.ExcludePaths); + var excludeTests = options.ExcludeTests; + + if (string.Equals(scopeName, SearchAuditRecipes.DefaultAuditScope, StringComparison.OrdinalIgnoreCase)) + { + if (pathPatterns.Count == 0) + AddDistinct(pathPatterns, recipe.DefaultPathPatterns); + AddDistinct(excludePaths, recipe.DefaultExcludePaths); + excludeTests = true; + } + + return new SearchRecipeScopeJsonResult( + scopeName, + pathPatterns, + excludePaths, + excludeTests, + [.. recipe.DefaultPathPatterns], + [.. recipe.DefaultExcludePaths]); + } + + private static void AddDistinct(List target, IEnumerable values) + { + foreach (var value in values) + { + if (!target.Contains(value, StringComparer.Ordinal)) + target.Add(value); + } + } + private static SearchIssueDraftJsonResult ToSearchIssueDraft( SearchAuditRecipe recipe, SearchRecipeQueryResultJsonResult queryResult, @@ -1036,6 +1093,9 @@ private static string BuildSearchIssueDraftBody( recipe.Name, recipe.Description, recipe.RecommendedLabels, + recipe.DefaultScope, + [.. recipe.DefaultPathPatterns], + [.. recipe.DefaultExcludePaths], recipe.Queries.Select(query => new SearchRecipeQueryListItemJsonResult( query.Name, query.Query, @@ -6086,6 +6146,20 @@ private static bool TryNormalizeLanguageCapability(string value, out string capa return capability is LanguageCapabilityGraph or LanguageCapabilityReferences or LanguageCapabilitySymbols; } + private static bool TryNormalizeSearchAuditScope(string value, out string scope) + { + scope = value.Trim().ToLowerInvariant(); + if (scope is SearchAuditRecipes.DefaultAuditScope or SearchAuditRecipes.AllAuditScope) + return true; + if (scope is "production" or "production-only") + { + scope = SearchAuditRecipes.DefaultAuditScope; + return true; + } + + return false; + } + public static QueryCommandOptions ParseArgs( string[] args, bool jsonDefault, @@ -6174,6 +6248,8 @@ public static QueryCommandOptions ParseArgs( string? recipeName = null; bool listRecipes = false; string? openIssuesPath = null; + string auditScope = SearchAuditRecipes.DefaultAuditScope; + bool auditScopeExplicit = false; bool languagesIndexedOnly = false; var languageCapabilities = new List(); @@ -6453,6 +6529,22 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) else AddParseError(openIssuesError!); break; + case "--audit-scope": + if (!TryReadStringOptionValue(args, ref i, "--audit-scope", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var auditScopeValue, out var auditScopeError)) + { + AddParseError(auditScopeError!); + } + else if (TryNormalizeSearchAuditScope(auditScopeValue!, out var normalizedAuditScope)) + { + WarnIfDuplicateSingleValueOption("--audit-scope", auditScopeValue!); + auditScope = normalizedAuditScope; + auditScopeExplicit = true; + } + else + { + AddParseError($"Error: unsupported --audit-scope value '{ConsoleUi.FormatBoundedValue(auditScopeValue)}'. Use source or all."); + } + break; case "--require-before": if (TryReadStringOptionValue(args, ref i, "--require-before", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var requireBeforeValue, out var requireBeforeError)) AddSearchGuardFilter("--require-before", SearchGuardRole.Require, SearchGuardDirection.Before, requireBeforeValue!); @@ -7085,6 +7177,8 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) RecipeName = recipeName, ListRecipes = listRecipes, OpenIssuesPath = openIssuesPath, + AuditScope = auditScope, + AuditScopeExplicit = auditScopeExplicit, LanguagesIndexedOnly = languagesIndexedOnly, LanguageCapabilities = languageCapabilities, ParseError = parseErrors == null ? null : string.Join(Environment.NewLine, parseErrors), @@ -10068,6 +10162,8 @@ public sealed class QueryCommandOptions public string? RecipeName { get; init; } public bool ListRecipes { get; init; } public string? OpenIssuesPath { get; init; } + public string AuditScope { get; init; } = SearchAuditRecipes.DefaultAuditScope; + public bool AuditScopeExplicit { get; init; } public bool LanguagesIndexedOnly { get; init; } public List LanguageCapabilities { get; init; } = []; public string? ParseError { get; init; } diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index c95afcfb11..9cd5a9c1b3 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -4,6 +4,9 @@ namespace CodeIndex.Cli; internal static class SearchAuditRecipes { + internal const string DefaultAuditScope = "source"; + internal const string AllAuditScope = "all"; + private static readonly List Recipes = [ new( @@ -134,6 +137,9 @@ internal static class SearchAuditRecipes "False positives include parser/tokenizer code, syntax tokens, and non-auth identifiers.", ExactSubstring: false) ]) + { + DefaultPathPatterns = ["src/**"] + } ]; internal static IReadOnlyList All => Recipes; @@ -150,6 +156,10 @@ internal sealed record SearchAuditRecipe( string Description, List Queries) { + public string DefaultScope { get; init; } = SearchAuditRecipes.DefaultAuditScope; + public List DefaultPathPatterns { get; init; } = []; + public List DefaultExcludePaths { get; init; } = []; + public List RecommendedLabels => Queries .SelectMany(query => query.RecommendedLabels) @@ -175,6 +185,9 @@ internal sealed record SearchRecipeListItemJsonResult( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("recommended_labels")] List RecommendedLabels, + [property: JsonPropertyName("default_scope")] string DefaultScope, + [property: JsonPropertyName("default_path_patterns")] List DefaultPathPatterns, + [property: JsonPropertyName("default_exclude_paths")] List DefaultExcludePaths, [property: JsonPropertyName("queries")] List Queries); internal sealed record SearchRecipeQueryListItemJsonResult( @@ -188,10 +201,19 @@ internal sealed record SearchRecipeQueryListItemJsonResult( internal sealed record SearchRecipeRunJsonResult( [property: JsonPropertyName("api_version")] string ApiVersion, [property: JsonPropertyName("recipe")] SearchRecipeListItemJsonResult Recipe, + [property: JsonPropertyName("scope")] SearchRecipeScopeJsonResult Scope, [property: JsonPropertyName("query_count")] int QueryCount, [property: JsonPropertyName("result_count")] int ResultCount, [property: JsonPropertyName("queries")] List Queries); +internal sealed record SearchRecipeScopeJsonResult( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("path_patterns")] List PathPatterns, + [property: JsonPropertyName("exclude_paths")] List ExcludePaths, + [property: JsonPropertyName("exclude_tests")] bool ExcludeTests, + [property: JsonPropertyName("recipe_default_path_patterns")] List RecipeDefaultPathPatterns, + [property: JsonPropertyName("recipe_default_exclude_paths")] List RecipeDefaultExcludePaths); + internal sealed record SearchRecipeQueryResultJsonResult( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("query")] string Query, @@ -205,6 +227,7 @@ internal sealed record SearchRecipeQueryResultJsonResult( internal sealed record SearchIssueDraftExportJsonResult( [property: JsonPropertyName("api_version")] string ApiVersion, [property: JsonPropertyName("recipe")] SearchRecipeListItemJsonResult Recipe, + [property: JsonPropertyName("scope")] SearchRecipeScopeJsonResult Scope, [property: JsonPropertyName("query_count")] int QueryCount, [property: JsonPropertyName("result_count")] int ResultCount, [property: JsonPropertyName("count")] int Count, diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index aecbf9c203..b28c823e78 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 |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--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 search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--audit-scope ] [--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] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); @@ -269,7 +269,8 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() { var output = CaptureFullUsageOutput(showBanner: false); - Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--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 search |--query |-- |--recipe |--list-recipes", output); + Assert.Contains("[--audit-scope ]", 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[=ndjson|array]] [--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); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 597cc75585..800a6f0a97 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -297,6 +297,8 @@ public void RunSearch_ListRecipesJsonIncludesBuiltInAuditMetadata_Issue3144() Assert.Equal(1, root.GetProperty("count").GetInt32()); Assert.Contains(recipe.GetProperty("recommended_labels").EnumerateArray(), label => label.GetString() == "audit"); + Assert.Equal("source", recipe.GetProperty("default_scope").GetString()); + Assert.Contains(recipe.GetProperty("default_path_patterns").EnumerateArray(), path => path.GetString() == "src/**"); Assert.Equal("ex.Message", query.GetProperty("query").GetString()); Assert.True(query.GetProperty("exact_substring").GetBoolean()); Assert.Contains("redaction", query.GetProperty("description").GetString(), StringComparison.OrdinalIgnoreCase); @@ -376,6 +378,9 @@ public void Run(Exception ex, CancellationToken token) Assert.Equal("risky-code", root.GetProperty("recipe").GetProperty("name").GetString()); Assert.Equal(20, root.GetProperty("query_count").GetInt32()); + Assert.Equal("source", root.GetProperty("scope").GetProperty("name").GetString()); + Assert.Contains(root.GetProperty("scope").GetProperty("path_patterns").EnumerateArray(), path => path.GetString() == "src/**"); + Assert.True(root.GetProperty("scope").GetProperty("exclude_tests").GetBoolean()); Assert.True(root.GetProperty("result_count").GetInt32() >= 4); Assert.Equal(1, unboundedJsonParse.GetProperty("count").GetInt32()); Assert.Equal("JsonDocument.Parse", unboundedJsonParse.GetProperty("query").GetString()); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index c8e7a8dda2..230969564e 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -48,6 +48,7 @@ public void ParseArgs_ParsesFiltersFlagsAndAcceptsMaxSnippetLines() "--exclude-path", "tests/**", "--exclude-path", "docs/**", "--exclude-tests", + "--audit-scope", "all", "--start", "12", "--end", "18", "--before", "2", @@ -75,6 +76,8 @@ public void ParseArgs_ParsesFiltersFlagsAndAcceptsMaxSnippetLines() Assert.Equal(new[] { "src/**" }, options.PathPatterns); Assert.Equal(["tests/**", "docs/**"], options.ExcludePaths); Assert.True(options.ExcludeTests); + Assert.Equal("all", options.AuditScope); + Assert.True(options.AuditScopeExplicit); Assert.Equal(12, options.StartLine); Assert.Equal(18, options.EndLine); Assert.Equal(2, options.ContextBefore); From 3f5005aa26c4b2f46d2540b607e3214286e89a45 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:46:34 +0900 Subject: [PATCH 5/6] Suppress recipe documentation noise by default Issue: #3448 --- USER_GUIDE.md | 6 +- changelog.d/unreleased/3385.added.md | 23 +++++++ src/CodeIndex/Cli/SearchAuditRecipes.cs | 17 ++++- .../QueryCommandRunnerSearchTests.cs | 68 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/3385.added.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 4c49a86911..5b6831df1f 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1235,7 +1235,8 @@ same source location. | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Exclude symbols with the requested visibility values. Accepts the same comma-separated values and alias expansion as `--visibility`. | | `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | Restrict results to glob-style path patterns. `*` and `?` are wildcards. Repeatable; multiple values are OR'd together | | `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | Pass a query literal explicitly, useful when the query starts with `-`. Query commands except `find` also accept `-- ` as a one-token query escape while continuing to parse later options. | -| `--recipe ` | `search` | Run a reusable audit recipe such as `risky-code`. Normal search filters and snippet controls apply to every recipe query; text, `--json` / `--format json`, and `--format issue-drafts` are supported. | +| `--recipe ` | `search` | Run a reusable audit recipe such as `risky-code`. Recipe runs default to `--audit-scope source`, applying the recipe's production-code path and exclusion metadata before normal search filters and snippet controls; text, `--json` / `--format json`, and `--format issue-drafts` are supported. | +| `--audit-scope ` | `search --recipe ` | Choose recipe path scope. `source` is the default and suppresses tests, docs, changelog text, and recipe definitions using recipe metadata; `all` intentionally searches every indexed path unless other filters exclude it. JSON recipe output reports the effective scope, path filters, and exclusions. | | `--list-recipes` | `search` | List available search audit recipes with query text, recommended labels, exact-match mode, and false-positive guidance. | | `--open-issues ` | `search --recipe --format issue-drafts` | Preflight generated issue drafts against an open-issues JSON file such as `gh issue list --state open --json number,title,labels,url`. | | `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude glob-style path patterns. `*` and `?` are wildcards (repeatable) | @@ -3532,7 +3533,8 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | 指定した可視性のシンボルを除外する。値と alias 展開は `--visibility` と同じ | | `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合) | | `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | クエリを明示的なリテラルとして渡す。クエリが `-` で始まる場合に有用。`find` 以外のクエリ系コマンドでは `-- ` も1トークンのクエリエスケープとして受け付け、その後のオプション解析を続ける。 | -| `--recipe ` | `search` | `risky-code` などの再利用可能な audit recipe を実行する。通常の search filter と snippet control は recipe 内の各 query に適用され、text、`--json` / `--format json`、`--format issue-drafts` に対応する。 | +| `--recipe ` | `search` | `risky-code` などの再利用可能な audit recipe を実行する。Recipe 実行は既定で `--audit-scope source` になり、recipe の本番コード向け path / exclusion metadata を適用したうえで、通常の search filter と snippet control を各 query に適用する。text、`--json` / `--format json`、`--format issue-drafts` に対応する。 | +| `--audit-scope ` | `search --recipe ` | recipe の path scope を選ぶ。既定の `source` は recipe metadata により tests、docs、changelog text、recipe 定義を抑制する。`all` は他の filter で除外しない限り、すべての indexed path を意図的に検索する。Recipe の JSON 出力には有効な scope、path filter、exclusion が含まれる。 | | `--list-recipes` | `search` | 利用可能な search audit recipe を query text、推奨 label、exact-match mode、false-positive guidance 付きで一覧表示する。 | | `--open-issues ` | `search --recipe --format issue-drafts` | `gh issue list --state open --json number,title,labels,url` のような open issue JSON file と照合し、生成した issue draft を事前重複確認する。 | | `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | glob 形式のパスパターンを除外する。`*` と `?` がワイルドカード。繰り返し指定可 | diff --git a/changelog.d/unreleased/3385.added.md b/changelog.d/unreleased/3385.added.md new file mode 100644 index 0000000000..7e23757867 --- /dev/null +++ b/changelog.d/unreleased/3385.added.md @@ -0,0 +1,23 @@ +--- +category: added +issues: + - 3385 + - 3387 + - 3390 + - 3440 + - 3448 +affected: + - src/CodeIndex/Cli/SearchAuditRecipes.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - USER_GUIDE.md +--- + +## English + +- **Search audit recipes now default to production source scope and cover more security/code-smell patterns (#3385, #3387, #3390, #3440, #3448)** — `search --recipe risky-code` now suppresses recipe definitions, docs, changelog text, and tests by default, reports the effective audit scope in JSON, supports `--audit-scope all` for intentional docs/test audits, and includes additional risky-code, C# smell, process, regex, timeout, path-case, and credential/token/security queries. + +## 日本語 + +- **search audit recipe が既定で本番 source scope を使い、security / code smell pattern を拡充しました (#3385, #3387, #3390, #3440, #3448)** — `search --recipe risky-code` は既定で recipe 定義、docs、changelog text、tests を抑制し、JSON に有効な audit scope を出力し、docs / tests を意図的に監査するための `--audit-scope all` に対応しました。加えて risky code、C# smell、process、regex、timeout、path case、credential / token / security 関連 query を追加しました。 diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index 9cd5a9c1b3..5e3ee72a6d 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -138,7 +138,22 @@ internal static class SearchAuditRecipes ExactSubstring: false) ]) { - DefaultPathPatterns = ["src/**"] + DefaultPathPatterns = ["src/**"], + DefaultExcludePaths = + [ + "src/CodeIndex/Cli/SearchAuditRecipes.cs", + "tests/**", + "docs/**", + "CHANGELOG.md", + "changelog.d/**", + "README.md", + "USER_GUIDE.md", + "DEVELOPER_GUIDE.md", + "TESTING_GUIDE.md", + "AGENT_GUIDE.md", + ".codex/**", + ".github/**" + ] } ]; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 800a6f0a97..49edb9af4d 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -299,6 +299,7 @@ public void RunSearch_ListRecipesJsonIncludesBuiltInAuditMetadata_Issue3144() Assert.Contains(recipe.GetProperty("recommended_labels").EnumerateArray(), label => label.GetString() == "audit"); Assert.Equal("source", recipe.GetProperty("default_scope").GetString()); Assert.Contains(recipe.GetProperty("default_path_patterns").EnumerateArray(), path => path.GetString() == "src/**"); + Assert.Contains(recipe.GetProperty("default_exclude_paths").EnumerateArray(), path => path.GetString() == "src/CodeIndex/Cli/SearchAuditRecipes.cs"); Assert.Equal("ex.Message", query.GetProperty("query").GetString()); Assert.True(query.GetProperty("exact_substring").GetBoolean()); Assert.Contains("redaction", query.GetProperty("description").GetString(), StringComparison.OrdinalIgnoreCase); @@ -380,6 +381,7 @@ public void Run(Exception ex, CancellationToken token) Assert.Equal(20, root.GetProperty("query_count").GetInt32()); Assert.Equal("source", root.GetProperty("scope").GetProperty("name").GetString()); Assert.Contains(root.GetProperty("scope").GetProperty("path_patterns").EnumerateArray(), path => path.GetString() == "src/**"); + Assert.Contains(root.GetProperty("scope").GetProperty("exclude_paths").EnumerateArray(), path => path.GetString() == "src/CodeIndex/Cli/SearchAuditRecipes.cs"); Assert.True(root.GetProperty("scope").GetProperty("exclude_tests").GetBoolean()); Assert.True(root.GetProperty("result_count").GetInt32() >= 4); Assert.Equal(1, unboundedJsonParse.GetProperty("count").GetInt32()); @@ -392,6 +394,72 @@ public void Run(Exception ex, CancellationToken token) } } + [Fact] + public void RunSearch_RecipeSourceScopeSuppressesDefinitionsDocsChangelogAndTests_Issues3440_3448() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_recipe_scope"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + foreach (var path in new[] + { + "src/app.cs", + "src/CodeIndex/Cli/SearchAuditRecipes.cs", + "docs/audit.md", + "CHANGELOG.md", + "tests/AppTests.cs", + }) + { + TestProjectHelper.InsertIndexedFile( + dbPath, + path, + path.EndsWith(".cs", StringComparison.Ordinal) ? "csharp" : "markdown", + "ProcessStartInfo"); + } + + var sourceScope = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--recipe", "risky-code", "--db", dbPath, "--json", "--limit", "10"], + _jsonOptions)); + var allScope = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--recipe", "risky-code", "--db", dbPath, "--json", "--limit", "10", "--audit-scope", "all"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, sourceScope.Result); + Assert.Equal(string.Empty, sourceScope.Stderr); + using var sourceDocument = ParseJsonOutput(sourceScope.Stdout); + var sourceQuery = sourceDocument.RootElement + .GetProperty("queries") + .EnumerateArray() + .Single(item => item.GetProperty("name").GetString() == "process-start-info"); + var sourceResult = Assert.Single(sourceQuery.GetProperty("results").EnumerateArray()); + + Assert.Equal("source", sourceDocument.RootElement.GetProperty("scope").GetProperty("name").GetString()); + Assert.Equal("src/app.cs", sourceResult.GetProperty("path").GetString()); + + Assert.Equal(CommandExitCodes.Success, allScope.Result); + Assert.Equal(string.Empty, allScope.Stderr); + using var allDocument = ParseJsonOutput(allScope.Stdout); + var allPaths = allDocument.RootElement + .GetProperty("queries") + .EnumerateArray() + .Single(item => item.GetProperty("name").GetString() == "process-start-info") + .GetProperty("results") + .EnumerateArray() + .Select(item => item.GetProperty("path").GetString()) + .ToList(); + + Assert.Equal("all", allDocument.RootElement.GetProperty("scope").GetProperty("name").GetString()); + Assert.Contains("src/CodeIndex/Cli/SearchAuditRecipes.cs", allPaths); + Assert.Contains("docs/audit.md", allPaths); + Assert.Contains("CHANGELOG.md", allPaths); + Assert.Contains("tests/AppTests.cs", allPaths); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Theory] [InlineData("count")] [InlineData("compact")] From 89d19a796f0bd44a25f6a4a96c551d2b40053eaf Mon Sep 17 00:00:00 2001 From: Widthdom <125688807+Widthdom@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:41:09 +0900 Subject: [PATCH 6/6] Keep diagnostic dumps out of successful test artifacts Pipeline: #3577 --- .github/workflows/dotnet.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2efb3f3b78..a5f3b5c774 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -258,9 +258,19 @@ jobs: TestResults/**/*.trx TestResults/**/*.txt TestResults/**/*.xml + TestResults/**/*Sequence*.xml + + - name: Upload diagnostic dumps + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + continue-on-error: true + with: + name: DiagnosticDumps-${{ matrix.os }}-${{ matrix.test-framework }} + if-no-files-found: ignore + overwrite: true + path: | TestResults/**/*.dmp TestResults/**/*.dump - TestResults/**/*Sequence*.xml TestResults/**/*.hangdump - name: Upload coverage reports