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
8 changes: 8 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,9 @@ Literal-safe `search` queries are bounded in the reader before FTS5
sanitization: maximum 1000 characters and 128 whitespace terms. Keep this guard
in `DbReader` so CLI, MCP, and direct reader callers share the same failure mode;
raw `--fts` queries continue to use the raw FTS complexity limits instead.
The CLI normalizes exact-mode aliases and then rejects raw `--fts` combined with
`--exact`, `--exact-substring`, or `--token-boundary` before database dispatch.
This keeps query context and replay output on one matching model.

When you run:
```sql
Expand Down Expand Up @@ -4834,6 +4837,11 @@ WHERE fts_chunks MATCH 'content:authenticate'

### 検索の仕組み

CLI は exact-mode alias を正規化してから、raw `--fts` と `--exact`、
`--exact-substring`、`--token-boundary` の組み合わせを database dispatch 前に
拒否します。これにより query context と replay output は 1 つの一致モデルだけを
保持します。

以下のクエリを実行すると:
```sql
SELECT f.path, c.start_line, c.content
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
Excerpt range coverage reuses one indexed fixture for requested/effective context bounds, explicit EOF and clamp modes, Unicode content, and terminal-newline variants; strict numeric recovery, empty files, and inline zero/negative coordinates share one invalid-range fixture.
Inspect path-line exact/enclosing-symbol cases reuse one indexed source fixture and iterate read-only line queries within a fact.
Definition and symbols exact-mode conflict validation share one empty database and a cross-command flag-pair table.
Search raw-FTS/literal-mode conflict coverage shares one pre-dispatch case table across flag ordering, exact aliases, count/issue-draft output, and human/JSON errors; successful issue-draft replay must preserve only its selected matching mode.
Symbols compact flag/alias and summary-only JSON envelopes share one editor-format fixture.
Symbols JSON array, LSP, quickfix, and SARIF location formats share one editor-format fixture; definition SARIF severity coverage reuses that fixture and asserts informational `note` output separately from warning-level diagnostic output.
Validate JSON, compact, count, and SARIF pagination/severity coverage shares one mixed informational/actionable fixture so authoritative totals, limited rows, SARIF levels, and actionability metadata cannot drift across formats; keep missing-`file_issues` degradation coverage in a separate legacy-schema fixture because availability is a distinct mutable state.
Expand Down Expand Up @@ -1135,6 +1136,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
excerpt の range coverage は、requested / effective context 境界、明示的な EOF / clamp mode、Unicode content、末尾改行の有無を1つの indexed fixture で共有してください。strict numeric recovery、空ファイル、inline の zero / negative 座標は1つの invalid-range fixture を共有してください。
inspect path-line の exact/enclosing-symbol case は1つの indexed source fixture を再利用し、read-only line query を fact 内で反復してください。
definition と symbols の exact-mode conflict validation は1つの空databaseとcross-command flag-pair tableを共有してください。
search の raw-FTS/literal-mode conflict coverage は、flag 順序、exact alias、count/issue-draft output、human/JSON error を1つの pre-dispatch case table で共有してください。成功する issue-draft replay は選択した一致 mode だけを保持することも検証します。
symbols compact flag/aliasとsummary-only JSON envelopeは1つのeditor-format fixtureを共有してください。
symbols JSON array、LSP、quickfix、SARIF location format は1つの editor-format fixture を共有し、definition SARIF severity のテストも同じ fixture を再利用して、情報レベルの `note` 出力を warning レベルの診断出力とは分けて検証してください。
validate の JSON、compact、count、SARIF における pagination / severity coverage は、informational finding と actionable finding が混在する1つの fixture を共有し、authoritative な総件数、limited row、SARIF level、actionability metadata が format 間で drift しないことを検証してください。`file_issues` 欠落時の degradation coverage は availability が独立した mutable state なので、別の legacy-schema fixture に分けてください。
Expand Down
8 changes: 8 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ the release notes will announce the timeline before the alias stops working.
MCP mirrors the same split: use `exactSubstring` or `tokenBoundary` on
`search`, `exactName` on name-based tools, and keep `exact` only for
backward-compatible clients.
Raw `--fts` mode is mutually exclusive with the literal search modes
`--exact`, `--exact-substring`, and `--token-boundary`. Choose one matching
model per search. Conflicting combinations fail with a typed usage error before
query execution, and generated replay commands preserve only the selected mode.
In `search --json` and MCP `search` responses, exact substring highlights add
`literal_terms` / `literal_term_occurrences` (camelCase in MCP) so clients can
render only the requested literal phrase while keeping the broader diagnostic
Expand Down Expand Up @@ -3816,6 +3820,10 @@ name に対する NFKC + Unicode CaseFold の等価比較です。
削除する予定はありません。削除する場合は、alias が使えなくなる前に release notes で
timeline を告知します。MCP も同じ分割を反映します。`search` では `exactSubstring` または
`tokenBoundary`、name-based tools では `exactName` を使い、`exact` は後方互換 client 向けに残します。
raw `--fts` mode と literal search mode の `--exact`、`--exact-substring`、
`--token-boundary` は相互排他です。検索ごとに一致モデルを 1 つだけ選んでください。
競合する組み合わせは query 実行前に型付き usage error となり、生成される replay command
には選択した mode だけが保持されます。
`search --json` と MCP `search` の exact substring highlight には
`literal_terms` / `literal_term_occurrences`(MCP では camelCase)も追加されるため、
広めの診断用 `terms` / `term_occurrences` を残したまま、要求した literal phrase だけを
Expand Down
23 changes: 23 additions & 0 deletions changelog.d/unreleased/4879.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
category: fixed
issues:
- 4879
affected:
- src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs
- src/CodeIndex/Cli/QueryCommandRunner.Search.cs
- src/CodeIndex/Cli/CliFlagSchema.cs
- tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs
- tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
- tests/CodeIndex.Tests/ConsoleUiTests.cs
- USER_GUIDE.md
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **Search now rejects incompatible raw FTS and literal matching modes (#4879)** — `--fts` can no longer be combined with `--exact`, `--exact-substring`, or `--token-boundary`. Conflicts return a typed usage error before database dispatch, while query context and replay output retain only one matching model.

## 日本語

- **search が互換性のない raw FTS と literal matching mode を拒否するようになりました (#4879)** — `--fts` と `--exact`、`--exact-substring`、`--token-boundary` は同時指定できません。競合は database dispatch 前に型付き usage error となり、query context と replay output は 1 つの一致モデルだけを保持します。
8 changes: 4 additions & 4 deletions src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,11 +534,11 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--fields", ValuePlaceholder = "<csv>", Description = "Project bounded-response row fields; inspect selects top-level evidence groups; nested collections accept collection.field", PrimaryCommands = Set(InspectFieldCommands.Concat(BoundedProjectionCommands).ToArray()) },
new() { Name = "--body-only", Description = "Inspect: body-focused JSON shorthand for --body --fields definitions", PrimaryCommands = Set(InspectFieldCommands) },
new() { Name = "--outline-only", Description = "Inspect: outline-first JSON shorthand for --fields file,definitions,nearby_symbols", PrimaryCommands = Set(InspectFieldCommands) },
new() { Name = "--exact", Description = "Backward-compatible exact shorthand", PrimaryCommands = Set(ExactCommands) },
new() { Name = "--exact", Description = "Backward-compatible exact shorthand; search mode is incompatible with --fts", PrimaryCommands = Set(ExactCommands) },
new() { Name = "--regex", Description = "Use regular expression matching", PrimaryCommands = Set("find") },
new() { Name = "--exact-name", Description = "Exact symbol-name equality", PrimaryCommands = Set(ExactNameCommands), AlsoAcceptedBy = Set("search") },
new() { Name = "--exact-substring", Description = "Search-only exact substring match", PrimaryCommands = Set("search"), AlsoAcceptedBy = Set(ExactSubstringAccepted) },
new() { Name = "--token-boundary", Description = "Search-only exact substring match with identifier/token boundaries", PrimaryCommands = Set("search") },
new() { Name = "--exact-substring", Description = "Search-only exact substring match; incompatible with --fts", PrimaryCommands = Set("search"), AlsoAcceptedBy = Set(ExactSubstringAccepted) },
new() { Name = "--token-boundary", Description = "Search-only exact substring match with identifier/token boundaries; incompatible with --fts", PrimaryCommands = Set("search") },
new() { Name = "--prefix", Description = "Trailing-asterisk prefix shorthand", PrimaryCommands = Set("search") },
new() { Name = "--require-before", ValuePlaceholder = "<query>", Description = "Search: require a nearby guard query before each primary match", PrimaryCommands = Set("search") },
new() { Name = "--require-after", ValuePlaceholder = "<query>", Description = "Search: require a nearby guard query after each primary match", PrimaryCommands = Set("search") },
Expand Down Expand Up @@ -573,7 +573,7 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--max-line-width", ValuePlaceholder = "<n>", Description = "Clamp long single-line payloads (0 disables clamping)", PrimaryCommands = Set(MaxLineWidthCommands) },
new() { Name = "--snippet-lines", ValuePlaceholder = "<n>", Description = "Snippet length; issue-drafts accept 0 for path/line-only evidence", PrimaryCommands = Set("search", "audit", "find", "references", "callers", "callees", "impact") },
new() { Name = "--snippet-focus", ValuePlaceholder = "<leftmost|quality|proximity>", Description = "Search snippet long-line focus mode", PrimaryCommands = Set("search") },
new() { Name = "--fts", Description = "Raw FTS5 syntax", PrimaryCommands = Set("search") },
new() { Name = "--fts", Description = "Raw FTS5 syntax; incompatible with search exact/literal modes", PrimaryCommands = Set("search") },
new() { Name = "--no-dedup", Description = "Show duplicate chunks", PrimaryCommands = Set("search") },
new() { Name = "--no-visibility-rank", Description = "Keep legacy search ranking without symbol visibility weighting", PrimaryCommands = Set("search") },
new() { Name = "--line", ValuePlaceholder = "<line>", Description = "Inspect/excerpt: include one source line as source_excerpt or excerpt window", PrimaryCommands = Set(InspectSourceExcerptCommands) },
Expand Down
16 changes: 15 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,22 +848,36 @@ private static (TimeSpan Value, string? Error) ResolveStaleAfter(QueryCommandOpt
return (DefaultStaleAfter, null);
}

private static bool TryResolveSearchExactMode(QueryCommandOptions options, out bool exact, out string? error)
private static bool TryResolveSearchExactMode(
QueryCommandOptions options,
out bool exact,
out string? error,
out string? hint)
{
if (!TryRejectMultipleExactFlags(options, out error))
{
exact = false;
hint = "Choose one search matching mode: --fts, --exact-substring, or --token-boundary. Use --exact only as the backward-compatible alias for --exact-substring.";
return false;
}
if (options.ExactName)
{
exact = false;
error = "Error: --exact-name applies to name-based commands (symbols/definition/references/callers/callees/inspect), not search. Use --exact-substring for search, or keep --exact for backward compatibility.";
hint = "Use --exact-substring or --token-boundary for literal search matching, or remove the exact-name flag.";
return false;
}
if (options.RawFts && (options.Exact || options.ExactSubstring || options.TokenBoundary))
{
exact = false;
error = "Error: raw FTS mode (--fts) cannot be combined with literal search modes (--exact, --exact-substring, or --token-boundary).";
hint = "Remove --fts to use literal/exact-substring matching, or remove the exact-mode flag to keep raw FTS5 syntax.";
return false;
}

exact = options.Exact || options.ExactSubstring;
error = null;
hint = null;
return true;
}

Expand Down
33 changes: 15 additions & 18 deletions src/CodeIndex/Cli/QueryCommandRunner.Search.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,25 @@ private static int RunSearchCore(
invocationContext,
options.LanguageValidationError || invocationContext.StructuredMachineUsageErrors ? jsonOptions : null))
return CommandExitCodes.UsageError;
if (!TryResolveSearchExactMode(options, out var exact, out var exactError))
if (!TryResolveSearchExactMode(options, out var exact, out var exactError, out var exactHint))
{
var message = StripErrorPrefix(exactError!);
if (invocationContext.StructuredMachineUsageErrors)
{
WriteUsageError(
StripErrorPrefix(exactError!),
options,
"Use one compatible exact-search mode, or remove the exact-mode flags.");
}
else
{
CommandErrorWriter.WriteStderr(exactError);
WriteUsageError(message, options, exactHint!);
return CommandExitCodes.UsageError;
}
return CommandExitCodes.UsageError;

return CommandErrorWriter.WriteJsonOrHuman(
options.Json,
jsonOptions,
message,
CommandExitCodes.UsageError,
exactHint,
usage: null,
errorCode: CommandErrorCodes.UsageError,
command: invocationContext.CommandName,
omitNullUsage: true);
}
if (options.OpenIssuesPath != null && options.OutputFormat != OutputFormatIssueDrafts)
{
Expand Down Expand Up @@ -260,14 +265,6 @@ private static int RunSearchCore(
return CommandExitCodes.UsageError;
}
var exactSearch = exact || options.TokenBoundary;
if (options.TokenBoundary && options.RawFts)
{
WriteSearchValidationError(
"--token-boundary cannot be combined with --fts.",
options,
"Drop --fts to use exact token-boundary matching, or drop --token-boundary to keep raw FTS5 syntax.");
return CommandExitCodes.UsageError;
}
if (exactSearch && options.Prefix)
{
WriteSearchValidationError(
Expand Down
8 changes: 4 additions & 4 deletions tests/CodeIndex.Tests/ConsoleUiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ public void CompletionRenderer_ShellFormatSnapshotMatchesBaseline(string shell,
# Regenerate this script after upgrading cdidx.
complete -c cdidx -n '__fish_use_subcommand' -a 'index' -d 'index command'
complete -c cdidx -n '__fish_use_subcommand' -l license -d 'Show license summary'
complete -c cdidx -n '__fish_seen_subcommand_from search definition references callers callees symbols find inspect' -l exact -d 'Backward-compatible exact shorthand'
complete -c cdidx -n '__fish_seen_subcommand_from search definition references callers callees symbols find inspect' -l exact -d 'Backward-compatible exact shorthand; search mode is incompatible with --fts'
complete -c cdidx -n '__fish_seen_subcommand_from search references callers callees find excerpt inspect impact' -l max-line-width -r -d 'Clamp long single-line payloads (0 disables clamping)'
"""
},
Expand Down Expand Up @@ -1339,11 +1339,11 @@ public void CompletionRenderer_FishIncludesFindOptions()
// `__fish_seen_subcommand_from <list>` strings change to the canonical
// command-ordering used by `CliFlagSchema.AllCommands`, and descriptions use the
// schema's single source of truth (e.g. `--exact` → "Backward-compatible exact
// shorthand"). These assertions intentionally check the schema-ordered groupings
// shorthand; search mode is incompatible with --fts"). These assertions intentionally check the schema-ordered groupings
// (`--query` and `--before`/`--after` predicates) and key flag invariants while
// accepting the unified wording.
// #1570 によりスキーマ駆動。`__fish_seen_subcommand_from` の並びは `CliFlagSchema.AllCommands`
// 順、`--exact` の説明は統一表記 (`Backward-compatible exact shorthand`)。
// 順、`--exact` の説明は統一表記 (`Backward-compatible exact shorthand; search mode is incompatible with --fts`)。
var output = ConsoleCompletionRenderer.GetCompletionScript("fish");
Assert.Contains("__fish_seen_subcommand_from search recipes definition goto references callers callees symbols files find inspect impact", output);
Assert.Contains("__fish_seen_subcommand_from find excerpt", output);
Expand All @@ -1353,7 +1353,7 @@ public void CompletionRenderer_FishIncludesFindOptions()
Assert.Contains("-l query -r -d 'Literal query'", output);
Assert.Contains("-l before -r -d 'Context lines before'", output);
Assert.Contains("-l after -r -d 'Context lines after'", output);
Assert.Contains("-l exact -d 'Backward-compatible exact shorthand'", output);
Assert.Contains("-l exact -d 'Backward-compatible exact shorthand; search mode is incompatible with --fts'", output);
Assert.Contains("__fish_seen_subcommand_from hotspots", output);
Assert.Contains("-l group-by-name -d 'Collapse same-name rows across files'", output);
}
Expand Down
Loading
Loading