Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions USER_GUIDE.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions changelog.d/unreleased/3787-3714.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
category: added
issues:
- 3787
- 3714
- 3731
- 3710
- 3694
- 3693
- 3670
- 3696
affected:
- src/CodeIndex/Cli/SearchAuditRecipes.cs
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Cli/CliFlagSchema.cs
- src/CodeIndex/Cli/ConsoleUi.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- src/CodeIndex/Database/DbSearchReader.cs
- src/CodeIndex/Mcp/McpToolDefinitions.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- src/CodeIndex/Mcp/McpToolArgumentContracts.cs
- USER_GUIDE.md
---

## English

- **Search audit recipes now cover focused .NET, JSON, XML, filesystem, and token-review audits (#3787, #3714, #3731, #3710, #3694, #3693, #3670, #3696)** — `cdidx search --recipe` and MCP recipe runs add grouped API-family recipes, honor the same default source scope, narrow the default token audit, keep a broad token fallback, report bounded per-query top-file/truncation metadata, preserve recipe pagination across deduplicated overlap rows, and can include opt-in exclusion diagnostics with `--show-excluded`.

## 日本語

- **検索監査レシピが .NET、JSON、XML、ファイルシステム、トークン確認の焦点を絞った監査を扱うようになりました (#3787, #3714, #3731, #3710, #3694, #3693, #3670, #3696)** — `cdidx search --recipe` と MCP のレシピ実行は API ファミリーごとにまとめたレシピを追加し、同じ既定のソース範囲を適用し、既定のトークン監査を絞り込み、広いトークン予備検索を維持し、クエリごとの上限付き `top_files` / `truncated` メタデータを返し、重複排除された重なり行をまたいだレシピのページネーションを維持し、`--show-excluded` で任意の除外診断を含められるようになりました。
1 change: 1 addition & 0 deletions src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--exclude-query", ValuePlaceholder = "<name>", 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 = "<source|all>", Description = "Search recipes: use production source defaults or include all indexed paths", Commands = Set("search") },
new() { Name = "--show-excluded", Description = "Search recipes: include effective scope and exclusion diagnostics in recipe output", Commands = Set("search") },
new() { Name = "--named-query", ValuePlaceholder = "<name>=<query>", Description = "Search: add one named ad hoc batch query", Commands = Set("search") },
new() { Name = "--open-issues", ValuePlaceholder = "<path|github|github:owner/name>", Description = "Preflight issue drafts against open issue JSON or GitHub open issues", Commands = Set("search", "suggestions") },
new() { Name = "--repo", ValuePlaceholder = "<owner/name>", Description = "Issue-drafts: GitHub repository for --open-issues github", Commands = Set("search", "suggestions") },
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("index-commits", "cdidx index <projectPath> --commits <commit-ref> [commit-ref ...] [--db <path>] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]]"),
("index-changed-between", "cdidx index <projectPath> --changed-between <old-ref> <new-ref> [--db <path>] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]]"),
("index-files", "cdidx index <projectPath> --files <path> [path ...] [--db <path>] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]]"),
("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>|--max-results <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>] [--show-excluded] [--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>|--max-results <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]"),
Expand Down
1 change: 1 addition & 0 deletions src/CodeIndex/Cli/JsonOutputContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ internal sealed record VersionInfoJsonResult(
[JsonSerializable(typeof(SearchRecipeQueryResultJsonResult))]
[JsonSerializable(typeof(SearchRecipeRunJsonResult))]
[JsonSerializable(typeof(SearchRecipeScopeJsonResult))]
[JsonSerializable(typeof(SearchRecipeExcludedDiagnosticJsonResult))]
[JsonSerializable(typeof(SearchIssueDraftExportJsonResult))]
[JsonSerializable(typeof(SearchIssueDraftJsonResult))]
[JsonSerializable(typeof(SearchIssueDraftSourceJsonResult))]
Expand Down
108 changes: 102 additions & 6 deletions src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public static partial class QueryCommandRunner
private const int SearchOriginFilterOverFetchFactor = 50;
private const int SearchOriginFilterMaxCandidates = 10_000;
private const int SearchOriginFilterMaxPages = 50;
private const int SearchEnvelopeMinCandidates = 200;
private const int SearchEnvelopeOverFetchFactor = 50;
private const int SearchEnvelopeMaxCandidates = 10_000;
private const string HotspotsGroupedByNameKind = "name_kind";
private const string HotspotsGroupedBySymbol = "symbol";
private const string HotspotsGroupedByFile = "file";
Expand Down Expand Up @@ -401,6 +404,14 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
"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.ShowExcluded && options.RecipeName == null)
{
WriteUsageError(
"--show-excluded is only supported with `cdidx search --recipe <name>`.",
GetUsageLineOrThrow("search"),
"Use it with a recipe run to include the effective scope and exclusion diagnostics in JSON output.");
return CommandExitCodes.UsageError;
}
if ((options.IssueTitle != null || options.IssueLabels.Count > 0) && options.OutputFormat != OutputFormatIssueDrafts)
{
WriteUsageError(
Expand Down Expand Up @@ -1150,6 +1161,18 @@ private static int RunSearchRecipe(QueryCommandOptions options, JsonSerializerOp
if (scope.ExcludePaths.Count > 0)
Console.WriteLine($"Excludes: {string.Join(", ", scope.ExcludePaths)}");
Console.WriteLine($"Exclude tests: {scope.ExcludeTests.ToString().ToLowerInvariant()}");
if (scope.ExcludedDiagnostics is { Count: > 0 })
{
Console.WriteLine("Excluded diagnostics:");
foreach (var diagnostic in scope.ExcludedDiagnostics)
{
var patterns = diagnostic.Patterns.Count == 0
? string.Empty
: $" ({string.Join(", ", diagnostic.Patterns)})";
Console.WriteLine($" - {diagnostic.Reason}: applied={diagnostic.Applied.ToString().ToLowerInvariant()}{patterns}");
Console.WriteLine($" {diagnostic.Description}");
}
}
Console.WriteLine();
foreach (var queryResult in queryResults)
{
Expand Down Expand Up @@ -1255,6 +1278,8 @@ private static int RunSearchIssueDrafts(QueryCommandOptions options, JsonSeriali
"Review the evidence paths and surrounding code before filing.",
exact,
rows.Count,
BuildSearchRecipeTopFiles(rows),
false,
null,
rows.Select(row => row.Compact).ToList());
var drafts = rows.Count == 0
Expand Down Expand Up @@ -1294,7 +1319,7 @@ private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryR
var exact = userExact || recipeQuery.ExactSubstring;
var results = reader.Search(
recipeQuery.Query,
options.Limit,
FetchLimitForSearchEnvelope(options.Limit),
options.Lang,
false,
scope.PathPatterns,
Expand All @@ -1309,6 +1334,7 @@ private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryR
guardFilters: options.GuardFilters,
guardWindow: options.GuardWindow);
var rows = BuildSearchDisplayRows(results, options, exact, recipeQuery.Query, rawFtsOverride: false);
var truncated = TrimSearchRowsToRequestedLimit(rows, options.Limit);
total += rows.Count;
queryResults.Add(new SearchRecipeQueryResultJsonResult(
recipeQuery.Name,
Expand All @@ -1318,7 +1344,9 @@ private static List<SearchRecipeQueryResultJsonResult> CollectSearchRecipeQueryR
recipeQuery.FalsePositiveGuidance,
exact,
rows.Count,
rows.Count > 0 ? FormatSearchCursor(rows[^1].Result) : null,
BuildSearchRecipeTopFiles(rows),
truncated,
truncated && rows.Count > 0 ? FormatSearchCursor(rows[^1].Result) : null,
rows.Select(row => row.Compact).ToList()));
}

Expand All @@ -1340,7 +1368,7 @@ private static List<SearchRecipeCompactQueryResultJsonResult> CollectSearchRecip
var exact = userExact || recipeQuery.ExactSubstring;
var results = reader.Search(
recipeQuery.Query,
options.Limit,
FetchLimitForSearchEnvelope(options.Limit),
options.Lang,
false,
scope.PathPatterns,
Expand All @@ -1355,14 +1383,16 @@ private static List<SearchRecipeCompactQueryResultJsonResult> CollectSearchRecip
guardFilters: options.GuardFilters,
guardWindow: options.GuardWindow);
var rows = BuildSearchDisplayRows(results, options, exact, recipeQuery.Query);
var truncated = TrimSearchRowsToRequestedLimit(rows, options.Limit);
total += rows.Count;
queryResults.Add(new SearchRecipeCompactQueryResultJsonResult(
recipeQuery.Name,
recipeQuery.Query,
recipeQuery.Description,
rows.Count,
BuildSearchRecipeTopFiles(rows),
rows.Count > 0 ? FormatSearchCursor(rows[^1].Result) : null,
truncated,
truncated && rows.Count > 0 ? FormatSearchCursor(rows[^1].Result) : null,
rows.Select(row => new SearchRecipeCompactResultJsonResult(
row.Result.Path,
row.Result.Lang,
Expand Down Expand Up @@ -1398,7 +1428,42 @@ private static SearchRecipeScopeJsonResult BuildSearchRecipeScope(SearchAuditRec
excludePaths,
excludeTests,
[.. recipe.DefaultPathPatterns],
[.. recipe.DefaultExcludePaths]);
[.. recipe.DefaultExcludePaths],
options.ShowExcluded ? BuildSearchRecipeExcludedDiagnostics(recipe, options, scopeName, excludeTests) : null);
}

private static List<SearchRecipeExcludedDiagnosticJsonResult> BuildSearchRecipeExcludedDiagnostics(
SearchAuditRecipe recipe,
QueryCommandOptions options,
string scopeName,
bool excludeTests)
{
var diagnostics = new List<SearchRecipeExcludedDiagnosticJsonResult>();
var sourceScope = string.Equals(scopeName, SearchAuditRecipes.DefaultAuditScope, StringComparison.OrdinalIgnoreCase);
diagnostics.Add(new SearchRecipeExcludedDiagnosticJsonResult(
"recipe_default_path_patterns",
sourceScope && options.PathPatterns.Count == 0 && recipe.DefaultPathPatterns.Count > 0,
[.. recipe.DefaultPathPatterns],
"Default source-scope include patterns applied when a recipe runs without user --path filters."));
diagnostics.Add(new SearchRecipeExcludedDiagnosticJsonResult(
"recipe_default_exclude_paths",
sourceScope && recipe.DefaultExcludePaths.Count > 0,
[.. recipe.DefaultExcludePaths],
"Default source-scope exclusions suppress recipe definitions, tests, docs, changelog text, and agent/workflow metadata."));
if (options.ExcludePaths.Count > 0)
{
diagnostics.Add(new SearchRecipeExcludedDiagnosticJsonResult(
"user_exclude_paths",
true,
[.. options.ExcludePaths],
"User-provided --exclude-path filters are applied after recipe defaults."));
}
diagnostics.Add(new SearchRecipeExcludedDiagnosticJsonResult(
"exclude_tests",
excludeTests,
[],
"The test-file classifier is enabled for this recipe scope; exact excluded paths depend on indexed file metadata."));
return diagnostics;
}

private static void AddDistinct(List<string> target, IEnumerable<string> values)
Expand All @@ -1419,6 +1484,27 @@ private static List<SearchRecipeTopFileJsonResult> BuildSearchRecipeTopFiles(IRe
.Take(10)
.ToList();

private static int FetchLimitForSearchEnvelope(int limit)
{
if (limit >= int.MaxValue)
return int.MaxValue;
if (limit <= 0)
return 1;

var requested = (long)limit + 1;
var overFetched = requested * SearchEnvelopeOverFetchFactor;
var candidateLimit = Math.Max(SearchEnvelopeMinCandidates, Math.Max(requested, overFetched));
return (int)Math.Min(SearchEnvelopeMaxCandidates, Math.Min(int.MaxValue, candidateLimit));
}

private static bool TrimSearchRowsToRequestedLimit(List<SearchDisplayRow> rows, int limit)
{
if (rows.Count <= limit)
return false;
rows.RemoveRange(limit, rows.Count - limit);
return true;
}

private static List<SearchNamedBatchQueryResultJsonResult> CollectSearchNamedBatchQueryResults(
DbReader reader,
QueryCommandOptions options,
Expand All @@ -1431,7 +1517,7 @@ private static List<SearchNamedBatchQueryResultJsonResult> CollectSearchNamedBat
{
var results = reader.Search(
namedQuery.Query,
options.Limit,
FetchLimitForSearchEnvelope(options.Limit),
options.Lang,
options.RawFts,
options.PathPatterns,
Expand All @@ -1445,6 +1531,7 @@ private static List<SearchNamedBatchQueryResultJsonResult> CollectSearchNamedBat
guardFilters: options.GuardFilters,
guardWindow: options.GuardWindow);
var rows = BuildSearchDisplayRows(results, options, userExact, namedQuery.Query);
var truncated = TrimSearchRowsToRequestedLimit(rows, options.Limit);
AttachExactSubstringHint(
rows.Select(row => row.Compact),
SearchQueryAdvisor.BuildExactSubstringHint(namedQuery.Query, options.RawFts, userExact, options.Prefix));
Expand All @@ -1454,6 +1541,9 @@ private static List<SearchNamedBatchQueryResultJsonResult> CollectSearchNamedBat
namedQuery.Query,
userExact,
rows.Count,
BuildSearchRecipeTopFiles(rows),
truncated,
null,
rows.Select(row => row.Compact).ToList()));
}

Expand Down Expand Up @@ -7467,6 +7557,7 @@ public static QueryCommandOptions ParseArgs(
string? recipeName = null;
var includeRecipeQueries = new List<string>();
var excludeRecipeQueries = new List<string>();
bool showExcluded = false;
bool listRecipes = false;
string? openIssuesPath = null;
string auditScope = SearchAuditRecipes.DefaultAuditScope;
Expand Down Expand Up @@ -7815,6 +7906,9 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
else
AddParseError(excludeQueryError!);
break;
case "--show-excluded":
showExcluded = true;
break;
case "--list-recipes":
listRecipes = true;
break;
Expand Down Expand Up @@ -8603,6 +8697,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
RecipeName = recipeName,
IncludeRecipeQueries = includeRecipeQueries,
ExcludeRecipeQueries = excludeRecipeQueries,
ShowExcluded = showExcluded,
ListRecipes = listRecipes,
OpenIssuesPath = openIssuesPath,
AuditScope = auditScope,
Expand Down Expand Up @@ -12057,6 +12152,7 @@ public sealed class QueryCommandOptions
public string? RecipeName { get; init; }
public List<string> IncludeRecipeQueries { get; init; } = [];
public List<string> ExcludeRecipeQueries { get; init; } = [];
public bool ShowExcluded { get; init; }
public bool ListRecipes { get; init; }
public string? OpenIssuesPath { get; init; }
public string AuditScope { get; init; } = SearchAuditRecipes.DefaultAuditScope;
Expand Down
Loading
Loading