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
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
C# `unused` partial-family coverage must reuse one multi-file fixture across regular JSON, compact, `--by-bucket`, and `--actionable`; include top-level and nested partial types, genuinely unused private members, an unrelated same-named family, a containing-type generic-arity collision, and a family-external occurrence in a matched peer file so semantic membership cannot regress into name-only, flattened-qualified-name, or whole-file evidence sharing.
`CliFlagSchemaTests.cs` and `ConsoleUiTests.cs` keep command help, runtime value validation, and each generated shell's option/value catalog aligned with `CliFlagSchema`, and check every Fish command scope against the shared per-command completion projection. When an accepted value or alias changes, assert its registry normalization plus command usage, runtime validation, and Bash / Zsh / Fish / PowerShell completion visibility; add the value to the schema instead of weakening this parity contract.
`ProgramRunnerTests.cs` enumerates `CliCommandCatalog.CommandSubcommands` and requires every valid nested verb to resolve to verb-specific usage with an example. It also pins destructive index confirmation and aggregate dependency-filter help, plus the read-only GitHub duplicate-preflight boundary for suggestion exports. Installer cancellation coverage must wait until the PID file contains a complete positive integer before cancelling, using a bounded dedicated waiter instead of a fixed-delay timer so parallel load cannot expose a redirection-created empty file.
Count-mode output coverage in `ProgramRunnerTests.cs` reuses one seeded search database across human text, JSON object, JSON envelope, incompatible format/stream (including `--results-only`), option-order, alias, and repeated-flag cases. Keep bare `--count` script output, `--format count` JSON output, and the rightmost repeated `--format` rule distinct while asserting that explicit incompatible formats never fall through to a count object.
Global-tool-log home-shorthand coverage resolves the normalized first candidate through the test-only no-write-probe seam, so sandbox permissions cannot turn an expansion assertion into a writability-fallback assertion.
Grouped-search limit coverage must seed more matches and files than the returned page, then assert query-wide matched/group/file totals separately from grouped, emitted, and omitted row counts so a bounded page cannot report itself as complete.
`goto --all` coverage shares one 63-definition fixture across the default bare LSP array and the plain `--json` argument path; it must assert complete deterministic output without a limit and retain a separate explicit-limit assertion in the same setup.
Expand Down Expand Up @@ -1119,6 +1120,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
C# `unused` の partial-family coverage では、通常の JSON、compact、`--by-bucket`、`--actionable` で1つの multi-file fixture を共有してください。top-level / nested partial type、本当に未使用の private member、無関係な同名 family、containing type の generic-arity collision、matched peer file 内の family 外 occurrence を含め、semantic membership が name-only、平坦化された qualified name、または file 全体の evidence 共有へ戻らないことを固定します。
`CliFlagSchemaTests.cs` と `ConsoleUiTests.cs` は command help、runtime value validation、各生成 shell の option / value 全カタログを `CliFlagSchema` と同期させ、Fish の全 command scope を共有の command 別 completion 射影と照合します。受理値または alias を変更するときは、registry normalization に加えて command usage、runtime validation、Bash / Zsh / Fish / PowerShell completion への露出を検証し、この parity 契約を弱めず schema に値を追加してください。
`ProgramRunnerTests.cs` は `CliCommandCatalog.CommandSubcommands` を列挙し、すべての有効な nested verb が例を含む verb 固有 usage に解決されることを必須とします。さらに destructive な index confirmation、aggregate dependency filter help、suggestion export の read-only GitHub duplicate-preflight 境界を固定します。installer cancellation coverage は固定時間タイマーではなく上限付きの専用 waiter を使い、PID ファイルが完全な正の整数を含むまで待ってから cancel してください。これにより並列負荷下でも redirection によって作成された空ファイルを読みません。
`ProgramRunnerTests.cs` の count-mode output coverage は、human text、JSON object、JSON envelope、`--results-only` を含む非互換 format / stream、option 順序、alias、flag 反復の各 case で 1 つの seeded search database を再利用します。bare `--count` の script 出力、`--format count` の JSON 出力、反復した `--format` の右端優先規則を区別して維持しつつ、明示した非互換 format が count object へ fall through しないことを検証してください。
global tool log の home shorthand coverage は、test 専用の write probe なし seam から正規化済みの先頭 candidate を解決します。これにより sandbox permission の影響で、展開の assertion が writability fallback の assertion に変わることを防ぎます。
grouped search の limit coverage では、返却 page より多い match と file を seed し、query 全体の match/group/file 総数と grouped、emitted、omitted row 数を別々に検証して、上限付き page が完了済みと報告できないようにしてください。
`goto --all` の coverage は、既定の bare LSP array と plain `--json` 引数経路で 1 つの 63 定義 fixture を共有し、limit 未指定時の完全かつ決定的な出力と、同じ setup 内での明示 limit の維持を検証してください。
Expand Down
17 changes: 17 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,15 @@ Output modifiers are validated as one contract: `--json` is rejected with
non-JSON formats such as `csv`, `tsv`, and `qf`, and `--pretty` is rejected with
`--json=ndjson` instead of being silently ignored. Use `--json=array --pretty`
when an indented search result is required.
Count mode follows the same contract. Bare `--count` preserves the
newline-terminated numeric value used by scripts, while `--count --json`,
`--format count`, and count output wrapped by `--json-envelope` use one JSON
count object. Explicit `--results-only`, `--json=array`, and `--json=ndjson`
streams are incompatible with count mode, and `--count` accepts only
`--format text`, `json`, or `count`;
formats such as `compact`, `grouped`, `csv`, `tsv`, `lsp`, `qf`, and `sarif`
return a usage error instead of being silently replaced. When `--format` is
repeated, its existing rightmost-value-wins rule still applies.
`search --named-query <name>=<query>` can be repeated to run an ad hoc grouped
batch with the same filters and snippet bounds. Named batches emit one grouped
JSON document, and `--format compact` keeps the per-result
Expand Down Expand Up @@ -3630,6 +3639,14 @@ output modifier は 1 つの contract として検証されます。`--json` と
などの非 JSON format の組み合わせ、および `--pretty` と `--json=ndjson` の組み合わせは、
黙って無視せず usage error として拒否します。indent 済みの search result が必要な場合は
`--json=array --pretty` を使ってください。
count mode も同じ contract に従います。bare `--count` は script が利用する改行終端の
数値を維持し、`--count --json`、`--format count`、`--json-envelope` で包んだ count
出力は 1 つの JSON count object を使います。明示的な `--results-only`、
`--json=array`、`--json=ndjson` stream は count mode と両立せず、`--count` と
組み合わせられる `--format` は `text`、`json`、`count` だけです。`compact`、
`grouped`、`csv`、`tsv`、
`lsp`、`qf`、`sarif` などは黙って count output に置き換えず usage error を返します。
`--format` を繰り返した場合は、既存どおり右端の値を優先します。
`search --named-query <name>=<query>` は繰り返し指定でき、同じ filter と snippet 上限で
ad hoc な grouped batch を実行します。名前付き batch は 1 つの grouped JSON document を
出力し、`--format compact` でも各 result の `CompactSearchResult` snippet / highlight
Expand Down
21 changes: 21 additions & 0 deletions changelog.d/unreleased/4878.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
category: fixed
issues:
- 4878
affected:
- src/CodeIndex/Cli/ProgramRunner.OutputFormatValidation.cs
- src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs
- src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.General.cs
- src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.Results.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
- USER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **Count-mode output formats are now validated consistently (#4878)** — `--count` and `--format count` now share output-mode validation, explicit results-only/JSON array/NDJSON streams and incompatible formats fail with a usage error instead of silently replacing the requested shape, and repeated `--format` options retain the documented rightmost-value-wins behavior.

## 日本語

- **count mode の出力形式を一貫して検証するようになりました (#4878)** — `--count` と `--format count` が同じ output-mode 検証を使い、明示的な results-only / JSON array / NDJSON stream や非互換 format は要求した形式を黙って置き換えず usage error となり、反復した `--format` は既存の右端優先規則を維持します。
54 changes: 50 additions & 4 deletions src/CodeIndex/Cli/ProgramRunner.OutputFormatValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ private static bool TryValidateOutputFormatOptions(
string? jsonStreamOption = null;
var jsonRequested = false;
var prettyRequested = false;
var compactRequested = false;
var resultsOnlyRequested = false;
var countFlagRequested = false;
var usesSingleDocumentJsonMode = false;
var hasExplicitPrettyJsonOutput = HasExplicitPrettyJsonOutputSelection(args);

Expand All @@ -36,6 +38,9 @@ private static bool TryValidateOutputFormatOptions(
break;
var tokenRole = GetQueryCommandTokenRole(args, i);

if (tokenRole != QueryCommandTokenRole.CommandOptionValue && arg == "--count")
countFlagRequested = true;

if (tokenRole != QueryCommandTokenRole.CommandOptionValue
&& (arg == "--count"
|| arg == "--summary-only"
Expand Down Expand Up @@ -80,6 +85,11 @@ private static bool TryValidateOutputFormatOptions(
}
continue;
}
if (tokenRole != QueryCommandTokenRole.CommandOptionValue && arg == "--compact")
{
compactRequested = true;
continue;
}
if (arg.StartsWith("--format=", StringComparison.Ordinal))
{
if (tokenRole == QueryCommandTokenRole.CommandOptionValue)
Expand All @@ -96,9 +106,44 @@ private static bool TryValidateOutputFormatOptions(
}
}

var countModeRequested = countFlagRequested
|| string.Equals(outputFormat, "count", StringComparison.Ordinal);
if (countModeRequested)
{
usesSingleDocumentJsonMode = true;
if (resultsOnlyRequested)
{
jsonStreamMode = "ndjson";
jsonStreamOption = "--results-only";
}
}

if (ShouldDeferOutputCombinationValidation(args, commandIndex, commandName, outputFormat, jsonStreamMode))
return true;

// Audit owns a structured JSON usage-error contract. Once count aliases are
// normalized, let its command runner report count-mode conflicts so --json
// callers keep receiving a machine-readable error on stdout.
if (countModeRequested && string.Equals(commandName, "audit", StringComparison.Ordinal))
return true;

if (countFlagRequested
&& outputFormat is not null and not "text" and not "json" and not "count")
{
error = $"--count cannot be combined with --format {outputFormat} because count mode supports only text, json, or count output.";
hint = $"remove --count to keep --format {outputFormat}, or use --format text, json, or count for count output.";
usage = ConsoleUi.GetUsageLine(commandName) ?? $"cdidx {commandName} --help";
return false;
}

if (countModeRequested && compactRequested)
{
error = "Bounded response controls cannot be combined with --count.";
hint = "remove --compact to keep count output, or remove --count/--format count to keep compact output.";
usage = ConsoleUi.GetUsageLine(commandName) ?? $"cdidx {commandName} --help";
return false;
}

var commandUsesImplicitNdjson = string.Equals(commandName, "search", StringComparison.Ordinal)
|| string.Equals(commandName, "files", StringComparison.Ordinal)
|| string.Equals(commandName, "symbols", StringComparison.Ordinal);
Expand Down Expand Up @@ -126,14 +171,15 @@ private static bool TryValidateOutputFormatOptions(
return false;
}

var schemaOutputFormat = countModeRequested ? "count" : outputFormat;
if (jsonStreamMode != null
&& outputFormat != null
&& CliOutputFormatCapabilities.TryGet(outputFormat, out var streamCapability)
&& schemaOutputFormat != null
&& CliOutputFormatCapabilities.TryGet(schemaOutputFormat, out var streamCapability)
&& !streamCapability.SupportsJsonStreamMode)
{
var streamOption = jsonStreamOption ?? $"--json={jsonStreamMode}";
error = $"{streamOption} cannot be combined with --format {outputFormat} because that format defines its own output schema.";
hint = $"remove {streamOption} to keep --format {outputFormat}, or use --format json.";
error = $"{streamOption} cannot be combined with --format {schemaOutputFormat} because that format defines its own output schema.";
hint = $"remove {streamOption} to keep --format {schemaOutputFormat}, or use --format json.";
usage = ConsoleUi.GetUsageLine(commandName) ?? $"cdidx {commandName} --help";
return false;
}
Expand Down
12 changes: 6 additions & 6 deletions src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.General.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,21 @@ private bool TryParseGeneralOption(string normalizedArg, string currentArg, stri
if (TryParseOutputFormat(formatValue!, out var parsedOutputFormat))
{
outputFormat = parsedOutputFormat;
countOutputFormatExplicit = parsedOutputFormat == OutputFormatCount;
outputFormatExplicit = true;
if (parsedOutputFormat == OutputFormatCompact)
compact = true;
if (parsedOutputFormat == OutputFormatCount)
countOnly = true;
if (parsedOutputFormat != OutputFormatText &&
outputFormatImpliesStructuredOutput =
parsedOutputFormat != OutputFormatText &&
parsedOutputFormat != OutputFormatDot &&
parsedOutputFormat != OutputFormatGraphMl)
json = true;
parsedOutputFormat != OutputFormatGraphMl;
}
else if (allowIssueDraftsFormat && string.Equals(formatValue, OutputFormatIssueDrafts, StringComparison.OrdinalIgnoreCase))
{
outputFormat = OutputFormatIssueDrafts;
countOutputFormatExplicit = false;
outputFormatExplicit = true;
json = true;
outputFormatImpliesStructuredOutput = true;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private bool TryParseResultOption(string normalizedArg, string currentArg, strin
AddParseError(bodyLinesError!);
break;
case "--count":
countOnly = true;
countFlagRequested = true;
break;
case "--group-partials":
groupPartials = true;
Expand Down
31 changes: 31 additions & 0 deletions src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private sealed partial class QueryArgumentParser
private int? bodyStartLine;
private int? bodyLines;
private bool countOnly;
private bool countFlagRequested;
private bool groupPartials;
private bool all;
private bool strictNotFound;
Expand Down Expand Up @@ -125,7 +126,9 @@ private sealed partial class QueryArgumentParser
private string? statusExplainField;
private bool statusLogPath;
private string outputFormat = OutputFormatText;
private bool countOutputFormatExplicit;
private bool outputFormatExplicit;
private bool outputFormatImpliesStructuredOutput;
private bool statusConfig;
private bool? redactPaths;
private bool limitExplicit;
Expand Down Expand Up @@ -210,6 +213,7 @@ internal QueryArgumentParser(
internal QueryCommandOptions Parse(string[] args)
{
ParseRawArguments(args);
NormalizeOutputMode();

if (unusedActionable)
{
Expand Down Expand Up @@ -243,6 +247,33 @@ internal QueryCommandOptions Parse(string[] args)
return BuildOptions(dbResolution, resolvedDbPath);
}

private void NormalizeOutputMode()
{
if (countOutputFormatExplicit)
outputFormat = OutputFormatCount;
countOnly = countFlagRequested || outputFormat == OutputFormatCount;
if (outputFormatImpliesStructuredOutput)
json = true;

if (countFlagRequested
&& outputFormatExplicit
&& outputFormat is not OutputFormatText and not OutputFormatJson and not OutputFormatCount)
{
AddParseError(
$"Error: --count cannot be combined with --format {outputFormat} because count mode supports only text, json, or count output.");
}
else if (countOnly && resultsOnly)
{
AddParseError(
"Error: --results-only cannot be combined with --format count because that format defines its own output schema.");
}
else if (countOnly && jsonOutputFormatExplicit)
{
AddParseError(
$"Error: --json={jsonOutputFormat} cannot be combined with --format count because that format defines its own output schema.");
}
}

private void ResolveProjectFilters(string resolvedDbPath)
{
if (parseErrors != null || projectFilters.Count == 0)
Expand Down
Loading
Loading