diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index fcd00697fd..85df3cf8d8 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -309,9 +309,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 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 3574d42852..634497ba96 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1320,9 +1320,10 @@ 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`. Use `recipe/query` form, such as `risky-code/raw-diagnostic-echo`, to run one child query directly. Normal search filters and snippet controls apply to every selected recipe query; `--limit` / `--top` is per child query. Text, `--json` / `--format json`, `--format compact`, and `--format issue-drafts` are supported, and issue drafts include a replay command. | +| `--recipe ` | `search` | Run a reusable audit recipe such as `risky-code`. Use `recipe/query` form, such as `risky-code/raw-diagnostic-echo`, to run one child query directly. Recipe runs default to `--audit-scope source`, applying the recipe's production-code path and exclusion metadata before normal search filters and snippet controls; `--limit` / `--top` is per child query. Text, `--json` / `--format json`, `--format compact`, and `--format issue-drafts` are supported, and issue drafts include a replay command. | | `--include-query ` / `--exclude-query ` | `search --recipe ` | Include or exclude child recipe queries by name. Repeatable and comma-separated; names are listed by `cdidx search --list-recipes`. | | `--cursor ` | `search --recipe ` | Fetch the next page for one selected recipe child query. Use the `next_cursor` returned by recipe JSON or compact output. | +| `--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, false-positive guidance, supported formats, filter support, and limit semantics. | | `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude glob-style path patterns. `*` and `?` are wildcards (repeatable) | | `--exclude-tests` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude likely test files and prefer production code | @@ -3757,9 +3758,10 @@ 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 を実行する。`risky-code/raw-diagnostic-echo` のような `recipe/query` 形式で child query を1つだけ直接実行できる。通常の search filter と snippet control は選択された各 query に適用され、`--limit` / `--top` は child query ごとの上限になる。text、`--json` / `--format json`、`--format compact`、`--format issue-drafts` に対応し、issue draft には再実行コマンドを含める。 | +| `--recipe ` | `search` | `risky-code` などの再利用可能な audit recipe を実行する。`risky-code/raw-diagnostic-echo` のような `recipe/query` 形式で child query を1つだけ直接実行できる。Recipe 実行は既定で `--audit-scope source` になり、recipe の本番コード向け path / exclusion metadata を適用したうえで、通常の search filter と snippet control を選択された各 query に適用する。`--limit` / `--top` は child query ごとの上限になる。text、`--json` / `--format json`、`--format compact`、`--format issue-drafts` に対応し、issue draft には再実行コマンドを含める。 | | `--include-query ` / `--exclude-query ` | `search --recipe ` | recipe 内の child query を名前で含める、または除外する。繰り返し指定とカンマ区切りに対応し、名前は `cdidx search --list-recipes` で確認できる。 | | `--cursor ` | `search --recipe ` | 選択した recipe child query の次ページを取得する。recipe JSON または compact output が返す `next_cursor` を指定する。 | +| `--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、対応 format、filter support、limit semantics 付きで一覧表示する。 | | `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | glob 形式のパスパターンを除外する。`*` と `?` がワイルドカード。繰り返し指定可 | | `--exclude-tests` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | テストらしいパスを除外し、本番コードを優先 | 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/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index c0d04c2a81..d3172c3a37 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -279,6 +279,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--include-query", ValuePlaceholder = "", Description = "Search recipe: include one child query; repeat or comma-separate values", Commands = Set("search") }, new() { Name = "--exclude-query", ValuePlaceholder = "", Description = "Search recipe: exclude one child query; repeat or comma-separate values", Commands = Set("search") }, new() { Name = "--list-recipes", Description = "Search: list built-in audit recipes", Commands = Set("search") }, + new() { Name = "--audit-scope", ValuePlaceholder = "", Description = "Search recipes: use production source defaults or include all indexed paths", Commands = Set("search") }, new() { Name = "--named-query", ValuePlaceholder = "=", Description = "Search: add one named ad hoc batch query", Commands = Set("search") }, new() { Name = "--open-issues", ValuePlaceholder = "", Description = "Preflight issue drafts against open issue JSON or GitHub open issues", Commands = Set("search", "suggestions") }, new() { Name = "--repo", ValuePlaceholder = "", Description = "Issue-drafts: GitHub repository for --open-issues github", Commands = Set("search", "suggestions") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index d555492481..b201f99386 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|--named-query = [--named-query = ...] [--include-query ] [--exclude-query ] [--cursor ] [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--repo ] [--issue-title ] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]"), + ("search", "cdidx search <query>|--query <query>|-- <query>|--recipe <name|name/query>|--list-recipes|--named-query <name>=<query> [--named-query <name>=<query> ...] [--include-query <name>] [--exclude-query <name>] [--cursor <cursor>] [--audit-scope <source|all>] [--db <path>] [--json[=ndjson|array]] [--pretty] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--issue-title <title>] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]"), ("definition", "cdidx definition <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since <datetime>]"), ("goto", "cdidx goto <query>|--query <query>|-- <query> [--db <path>] [--json] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exact|--exact-name] [--all]"), ("references", "cdidx references <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--snippet-lines <n>] [--max-line-width <n>] [--exact|--exact-name] [--count]"), @@ -1035,6 +1035,7 @@ private static void PrintFlagReference(Action<string> WriteHelpLine) WriteHelpLine(" --named-query <name>=<query> search only: add a named ad hoc batch query; repeat to run related searches with grouped compact results"); Console.WriteLine(" --exclude-path <glob> Exclude glob-style path patterns (* and ?) (repeatable)"); Console.WriteLine(" --exclude-tests Exclude likely test files"); + WriteHelpLine(" --audit-scope <source|all> 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(" --exclude-comments search only: suppress comment-only matches"); Console.WriteLine(" --exclude-strings search only: suppress string, regex, and help-text matches"); Console.WriteLine(" --exclude-fixtures search only: suppress fixture-only matches in tests"); diff --git a/src/CodeIndex/Cli/JsonOutputContracts.cs b/src/CodeIndex/Cli/JsonOutputContracts.cs index efe78a1d39..fafa58ae30 100644 --- a/src/CodeIndex/Cli/JsonOutputContracts.cs +++ b/src/CodeIndex/Cli/JsonOutputContracts.cs @@ -562,6 +562,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 03ebe873bb..d0ce9782a3 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -596,6 +596,14 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) "Use `--recipe risky-code/raw-diagnostic-echo --format compact --cursor <next_cursor>` to fetch the next page for one child query."); return CommandExitCodes.UsageError; } + if (options.AuditScopeExplicit && options.RecipeName == null) + { + WriteUsageError( + "--audit-scope is only supported with `cdidx search --recipe <name>`.", + 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.IssueTitle != null || options.IssueLabels.Count > 0) && options.OutputFormat != OutputFormatIssueDrafts) { WriteUsageError( @@ -1156,6 +1164,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"; @@ -1289,6 +1302,7 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp return CommandExitCodes.UsageError; } var recipe = selection.Recipe; + var scope = BuildSearchRecipeScope(recipe, options); if (options.SearchCursor.HasValue && selection.Queries.Count != 1) { WriteUsageError( @@ -1302,11 +1316,12 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp { if (options.OutputFormat == OutputFormatCompact) { - var compactQueryResults = CollectSearchRecipeCompactQueryResults(reader, selection.Queries, options, userExact, out var compactTotal); + var compactQueryResults = CollectSearchRecipeCompactQueryResults(reader, selection.Queries, scope, options, userExact, out var compactTotal); Console.WriteLine(JsonSerializer.Serialize( new SearchRecipeCompactRunJsonResult( JsonOutputContract.ApiVersion, ToSearchRecipeListItem(recipe, selection.Queries), + scope, selection.Queries.Count, compactTotal, compactQueryResults), @@ -1314,7 +1329,7 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp return CommandExitCodes.Success; } - var queryResults = CollectSearchRecipeQueryResults(reader, selection.Queries, options, userExact, out var total); + var queryResults = CollectSearchRecipeQueryResults(reader, selection.Queries, scope, options, userExact, out var total); if (options.Json) { @@ -1322,6 +1337,7 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp new SearchRecipeRunJsonResult( JsonOutputContract.ApiVersion, ToSearchRecipeListItem(recipe, selection.Queries), + scope, selection.Queries.Count, total, queryResults), @@ -1331,6 +1347,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) { @@ -1364,6 +1386,7 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS return CommandExitCodes.UsageError; } var recipe = selection.Recipe; + var scope = BuildSearchRecipeScope(recipe, options); if (!IssueDuplicatePreflight.TryLoad(options.OpenIssuesPath, options.OpenIssuesRepository, out var preflight, out var error)) { WriteUsageError( @@ -1375,7 +1398,7 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS return WithDb(options, jsonOptions, reader => { - var queryResults = CollectSearchRecipeQueryResults(reader, selection.Queries, options, userExact, out var total); + var queryResults = CollectSearchRecipeQueryResults(reader, selection.Queries, scope, options, userExact, out var total); var drafts = queryResults .Where(queryResult => queryResult.Count > 0) .Select(queryResult => ToSearchIssueDraft(recipe, queryResult, preflight, options)) @@ -1384,6 +1407,7 @@ private static int RunSearchRecipeIssueDrafts(QueryCommandOptions options, JsonS new SearchIssueDraftExportJsonResult( JsonOutputContract.ApiVersion, ToSearchRecipeListItem(recipe, selection.Queries), + scope, selection.Queries.Count, total, drafts.Count, @@ -1444,6 +1468,7 @@ private static int RunSearchIssueDrafts(QueryCommandOptions options, JsonSeriali new SearchIssueDraftExportJsonResult( JsonOutputContract.ApiVersion, null, + null, 1, rows.Count, drafts.Count, @@ -1460,6 +1485,7 @@ private static int RunSearchIssueDrafts(QueryCommandOptions options, JsonSeriali private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryResults( DbReader reader, IReadOnlyList<SearchAuditRecipeQuery> recipeQueries, + SearchRecipeScopeJsonResult scope, QueryCommandOptions options, bool userExact, out int total) @@ -1474,9 +1500,9 @@ private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryR options.Limit, options.Lang, false, - options.PathPatterns, - options.ExcludePaths, - options.ExcludeTests, + scope.PathPatterns, + scope.ExcludePaths, + scope.ExcludeTests, !options.NoDedup, options.Since, exact, @@ -1505,6 +1531,7 @@ private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryR private static List<SearchRecipeCompactQueryResultJsonResult> CollectSearchRecipeCompactQueryResults( DbReader reader, IReadOnlyList<SearchAuditRecipeQuery> recipeQueries, + SearchRecipeScopeJsonResult scope, QueryCommandOptions options, bool userExact, out int total) @@ -1519,9 +1546,9 @@ private static List<SearchRecipeCompactQueryResultJsonResult> CollectSearchRecip options.Limit, options.Lang, false, - options.PathPatterns, - options.ExcludePaths, - options.ExcludeTests, + scope.PathPatterns, + scope.ExcludePaths, + scope.ExcludeTests, !options.NoDedup, options.Since, exact, @@ -1553,6 +1580,39 @@ private static List<SearchRecipeCompactQueryResultJsonResult> CollectSearchRecip return queryResults; } + private static SearchRecipeScopeJsonResult BuildSearchRecipeScope(SearchAuditRecipe recipe, QueryCommandOptions options) + { + var scopeName = options.AuditScopeExplicit ? options.AuditScope : recipe.DefaultScope; + var pathPatterns = new List<string>(options.PathPatterns); + var excludePaths = new List<string>(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<string> target, IEnumerable<string> values) + { + foreach (var value in values) + { + if (!target.Contains(value, StringComparer.Ordinal)) + target.Add(value); + } + } + private static List<SearchRecipeTopFileJsonResult> BuildSearchRecipeTopFiles(IReadOnlyList<SearchDisplayRow> rows) => rows .GroupBy(row => row.Result.Path, StringComparer.Ordinal) @@ -1879,6 +1939,9 @@ private static string BuildAdHocSearchIssueDraftBody( recipe.Name, recipe.Description, recipe.RecommendedLabels, + recipe.DefaultScope, + [.. recipe.DefaultPathPatterns], + [.. recipe.DefaultExcludePaths], SearchRecipeSupportedFormats, SearchRecipeFilterSupport, SearchRecipeLimitSemantics, @@ -7377,6 +7440,20 @@ private static List<string> BuildLanguageCapabilityGaps(bool symbols, bool refer return gaps; } + 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, @@ -7474,6 +7551,8 @@ public static QueryCommandOptions ParseArgs( var excludeRecipeQueries = new List<string>(); bool listRecipes = false; string? openIssuesPath = null; + string auditScope = SearchAuditRecipes.DefaultAuditScope; + bool auditScopeExplicit = false; string? openIssuesRepository = null; string? issueTitle = null; var issueLabels = new List<string>(); @@ -7828,6 +7907,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 "--repo": if (TryReadStringOptionValue(args, ref i, "--repo", inlineValue, allowSeparatedDashPrefixedLiteralValue: false, out var repoValue, out var repoError)) { @@ -8573,6 +8668,8 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) ExcludeRecipeQueries = excludeRecipeQueries, ListRecipes = listRecipes, OpenIssuesPath = openIssuesPath, + AuditScope = auditScope, + AuditScopeExplicit = auditScopeExplicit, OpenIssuesRepository = openIssuesRepository, IssueTitle = issueTitle, IssueLabels = issueLabels, @@ -11912,6 +12009,8 @@ public sealed class QueryCommandOptions public List<string> ExcludeRecipeQueries { 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 string? OpenIssuesRepository { get; init; } public string? IssueTitle { get; init; } public List<string> IssueLabels { get; init; } = []; diff --git a/src/CodeIndex/Cli/SearchAuditRecipes.cs b/src/CodeIndex/Cli/SearchAuditRecipes.cs index 6079523e55..6e65e9e265 100644 --- a/src/CodeIndex/Cli/SearchAuditRecipes.cs +++ b/src/CodeIndex/Cli/SearchAuditRecipes.cs @@ -6,6 +6,8 @@ namespace CodeIndex.Cli; internal static class SearchAuditRecipes { + internal const string DefaultAuditScope = "source"; + internal const string AllAuditScope = "all"; internal const string RecipePathsEnvironmentVariable = "CDIDX_SEARCH_RECIPE_PATHS"; private const int MaxRecipeSourceFiles = 8; private const long MaxRecipeSourceBytes = 128 * 1024; @@ -54,8 +56,119 @@ 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( + "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", + "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."), + 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."), + 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) ]) + { + 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/**" + ] + } ]; internal static IReadOnlyList<SearchAuditRecipe> All => Load().Recipes; @@ -367,6 +480,10 @@ internal sealed record SearchAuditRecipe( string Description, List<SearchAuditRecipeQuery> Queries) { + public string DefaultScope { get; init; } = SearchAuditRecipes.DefaultAuditScope; + public List<string> DefaultPathPatterns { get; init; } = []; + public List<string> DefaultExcludePaths { get; init; } = []; + public List<string> RecommendedLabels => Queries .SelectMany(query => query.RecommendedLabels) @@ -392,6 +509,9 @@ internal sealed record SearchRecipeListItemJsonResult( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("recommended_labels")] List<string> RecommendedLabels, + [property: JsonPropertyName("default_scope")] string DefaultScope, + [property: JsonPropertyName("default_path_patterns")] List<string> DefaultPathPatterns, + [property: JsonPropertyName("default_exclude_paths")] List<string> DefaultExcludePaths, [property: JsonPropertyName("supported_formats")] List<string> SupportedFormats, [property: JsonPropertyName("filter_support")] SearchRecipeFilterSupportJsonResult FilterSupport, [property: JsonPropertyName("limit_semantics")] SearchRecipeLimitSemanticsJsonResult LimitSemantics, @@ -425,6 +545,7 @@ 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<SearchRecipeQueryResultJsonResult> Queries); @@ -456,6 +577,7 @@ internal sealed record SearchRecipeQueryResultJsonResult( internal sealed record SearchRecipeCompactRunJsonResult( [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<SearchRecipeCompactQueryResultJsonResult> Queries); @@ -486,6 +608,7 @@ internal sealed record SearchRecipeCompactResultJsonResult( 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, @@ -509,3 +632,11 @@ internal sealed record SearchIssueDraftSourceJsonResult( [property: JsonPropertyName("false_positive_guidance")] string FalsePositiveGuidance, [property: JsonPropertyName("exact_substring")] bool ExactSubstring, [property: JsonPropertyName("result_count")] int ResultCount); + +internal sealed record SearchRecipeScopeJsonResult( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("path_patterns")] List<string> PathPatterns, + [property: JsonPropertyName("exclude_paths")] List<string> ExcludePaths, + [property: JsonPropertyName("exclude_tests")] bool ExcludeTests, + [property: JsonPropertyName("recipe_default_path_patterns")] List<string> RecipeDefaultPathPatterns, + [property: JsonPropertyName("recipe_default_exclude_paths")] List<string> RecipeDefaultExcludePaths); diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 76489970b8..ae0ad16c21 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>|--query <query>|-- <query>", output); Assert.Contains("cdidx callers <query>|--query <query>|-- <query>", output); Assert.Contains("cdidx callees <query>|--query <query>|-- <query>", output); - Assert.Contains("cdidx search <query>|--query <query>|-- <query>|--recipe <name|name/query>|--list-recipes|--named-query <name>=<query> [--named-query <name>=<query> ...] [--include-query <name>] [--exclude-query <name>] [--cursor <cursor>] [--db <path>] [--json[=ndjson|array]] [--pretty] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--issue-title <title>] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]", output); + Assert.Contains("cdidx search <query>|--query <query>|-- <query>|--recipe <name|name/query>|--list-recipes|--named-query <name>=<query> [--named-query <name>=<query> ...] [--include-query <name>] [--exclude-query <name>] [--cursor <cursor>] [--audit-scope <source|all>] [--db <path>] [--json[=ndjson|array]] [--pretty] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--issue-title <title>] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]", output); Assert.Contains("cdidx definition <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since <datetime>]", output); Assert.Contains("cdidx references <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--snippet-lines <n>] [--max-line-width <n>] [--exact|--exact-name] [--count]", output); Assert.Contains("cdidx inspect <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|compact>] [--pretty] [--compact] [--fields <csv>] [--body-only] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--body-start <line>] [--body-lines <n>] [--max-line-width <n>] [--exact|--exact-name]", output); @@ -269,7 +269,7 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() { var output = CaptureFullUsageOutput(showBanner: false); - Assert.Contains("cdidx search <query>|--query <query>|-- <query>|--recipe <name|name/query>|--list-recipes|--named-query <name>=<query> [--named-query <name>=<query> ...] [--include-query <name>] [--exclude-query <name>] [--cursor <cursor>] [--db <path>] [--json[=ndjson|array]] [--pretty] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--issue-title <title>] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]", output); + Assert.Contains("cdidx search <query>|--query <query>|-- <query>|--recipe <name|name/query>|--list-recipes|--named-query <name>=<query> [--named-query <name>=<query> ...] [--include-query <name>] [--exclude-query <name>] [--cursor <cursor>] [--audit-scope <source|all>] [--db <path>] [--json[=ndjson|array]] [--pretty] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--issue-title <title>] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]", output); Assert.Contains("cdidx symbols [query|--query <query>|-- <query>] [--name <name>] [--db <path>] [--json] [--format <text|json|count>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exact|--exact-name] [--count] [--since <datetime>]", output); Assert.Contains("cdidx files [query|--query <query>|-- <query>] [--db <path>] [--json[=ndjson|array]] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--since <datetime>] [--bytes]", output); Assert.Contains("cdidx hotspots [--db <path>] [--json] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count]", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 2819d8a93d..4dfa284b89 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -950,6 +950,9 @@ 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.Contains(recipe.GetProperty("default_exclude_paths").EnumerateArray(), path => path.GetString() == "src/CodeIndex/Cli/SearchAuditRecipes.cs"); Assert.Contains(recipe.GetProperty("supported_formats").EnumerateArray(), format => format.GetString() == "issue-drafts"); Assert.True(recipe.GetProperty("filter_support").GetProperty("exclude_tests").GetBoolean()); Assert.True(recipe.GetProperty("filter_support").GetProperty("guard_filters").GetBoolean()); @@ -1027,7 +1030,11 @@ 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(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()); Assert.Equal("JsonDocument.Parse", unboundedJsonParse.GetProperty("query").GetString()); @@ -1039,6 +1046,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); + } + } + [Fact] public void RunSearch_RecipeJsonWithRawFtsReportsEffectiveSanitizedMode_Issue3558() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index abe41b731d..f78e7b3a4f 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);