From 8736577919045d4bd721be7e809fddab3f90cd8e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 28 Aug 2026 23:34:48 +0900 Subject: [PATCH 1/3] Fix end-of-options bounded query parsing (#5208) --- TESTING_GUIDE.md | 4 +- USER_GUIDE.md | 4 +- changelog.d/unreleased/5208.fixed.md | 19 +++ .../Cli/JsonEnvelopeWrapper.Bounded.cs | 138 +++++++++++------- src/CodeIndex/Cli/JsonEnvelopeWrapper.cs | 21 ++- .../JsonEnvelopeWrapperTests.cs | 59 ++++++++ 6 files changed, 182 insertions(+), 63 deletions(-) create mode 100644 changelog.d/unreleased/5208.fixed.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 847567c24..0cb0e3e1c 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -250,7 +250,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result Suggestions usage-hint coverage for #5196 must keep unknown-option human diagnostics and missing-id / invalid-combination JSON errors aligned on `cdidx suggestions --help`, preserve the existing error-code, category, usage, and exit-code contracts, and assert that the literal `` placeholder is never emitted. `CommandErrorWriterTests.cs` must keep catalog-backed known-command hints and the global fallback for missing, unknown, nested, or control-bearing command identities. Explicit `--files` preflight coverage for #5091 / #5122 keeps real/dry-run and JSON/text failure behavior in one focused contract matrix: bare `--files`, absolute and relative outside-root paths, platform-supported symlink escapes, `none`-mode directory-segment rejection, `internal`/`all` acceptance, nonexistent, directory, filtered, and unsupported targets, canonical duplicates (including target-specific casing and native Unicode/alias spelling), mixed valid/invalid atomicity, raw-token provenance beside `--project` expansion, already-indexed cleanup and existing or deleted reconciliation-control exceptions, generated-code acceptance, unreadable membership snapshots, and the implicit empty full-scan control without `--files`. Include Windows 8.3 ancestor aliases that must map back to established database spelling. Indexed controls replaced by a directory, FIFO, or policy-disallowed symlink must remain cleanup tombstones without opening or following the replacement. TypeScript path-alias configuration tests must cover `none`, inside/outside-root `internal`, and `all`, prove that only regular resolved targets are secure-opened, and keep real/dry-run plus in-process/worker paths aligned. Assert `UsageError`, bounded and redacted per-input reason diagnostics, and unchanged indexed rows and metadata whenever any explicit token is rejected; snapshot-read failures fail closed with a database error. Authoritative C# scoped-update coverage treats bounded parallel extraction as a correctness and resource contract. Keep the `2 * workers` window bound, fixed-worker reuse, whole-window extraction barrier, target-ordered single-writer persistence, and mixed-language serial boundary in the same suite. Required fallbacks cover parallelism one, active symbol filters, content-load seams, post-extraction hooks, non-authoritative or undersized snapshots, ambiguous nullable language reuse, and speculative probe exceptions returning to the serial per-file boundary. Keep serial target disposition in its exact missing/delete, path-filter, indexability/language-probe, unsupported-purge, hardlink, stat-reuse, load/revalidate, reuse-cleanup, persistence, and catch order. Serial/parallel parity must compare summaries, files/chunks/all semantic symbol and reference columns, normalized reference-line and candidate identities, hotspot aggregates, issues, batch-marker cleanup, readiness/user-version metadata, and last-run readable-byte counters across normal, generated, cap, and oversized inputs. Retain all three file-stat barriers. The ordered worker-failure fixture keeps a failed authoritative target, a normal changed target, an unchanged but explicitly targeted authoritative target, and an oversized target in one four-file window; pin the exact persistence-event and commit-hook order, isolated failure phase, incomplete reason, metadata demotion, and separately observed post-update migration-marker cleanup. Cancellation before persistence, after validated load, and at extraction completion must preserve the expected committed prefix, batch ownership, and derived readiness. Global watchdog and direct-fatal tests must assert active phase, bounded return, and terminal side effects; source-negative ordering tests must cover both cap-discarded confirmed evidence and an earlier lexical contract candidate still blocked in symbol extraction. Abnormal-window fixtures release every injected block and wait for the snapshotted all-workers-stopped seam before restoring static hooks or deleting the project root. - `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. + `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. End-of-options coverage must keep the immediately escaped option-like query as command data while proving that bounded-response options after that one query token still parse normally. Status-explain coverage must enumerate the source-generated `StatusResult` serializer properties and require every serialized top-level key to succeed without opening a database. Keep major readiness/trust/extension/maintenance/cap-hit metadata assertions, dotted-member resolution, bounded valid unknown candidates, and redaction of hostile field input in the same focused fixture so serialization and explainability cannot drift independently. Keep every structured explain response key registered for status `--fields` projection, and exercise the response through a bounded JSON projection that proves the outer envelope also omits runtime and path metadata. Safety-recipe semantic coverage must keep safe and unsafe controls in separate indexed files: assert that `Regex.Escape` / `Regex.Unescape` and literal `UseShellExecute=false` are absent, while matching, source-defined, or unresolved Regex members, literal `true`, and propagated shell-policy values remain with classification evidence. Include alias trivia, alias-like comments and strings, a qualified BCL call in a file with a bare `BoundedRegex` alias, a line break before literal `false`, and computed continuations after block and line comments. Also retain a bare receiver from a legacy index without current reference identity, add enough safe helpers to saturate the normal result envelope, keep count output aligned with full JSON, and retain a separate `new Regex` construction positive. JSON parser-guard coverage in `QueryCommandRunnerSearchTests.cs` must share one indexed fixture across both `json-parse-apis` child queries and structured/compact JSON. Keep `BoundedJson`, direct materialization, async stream/cancellation, an unrelated-value guard, and a bound-plus-streaming precedence case together; require exactly one `parser_guard_evidence` classification per retained row, category counts that sum to the retained results, matching output order across shapes, and unchanged snippets. @@ -1400,7 +1400,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" #5196 の suggestions usage-hint coverage では、unknown option の human 診断と missing id / invalid combination の JSON error がともに `cdidx suggestions --help` を案内し、既存の error code、category、usage、exit code 契約を維持し、literal `` placeholder を出力しないことを固定してください。`CommandErrorWriterTests.cs` では catalog に基づく既知 command の hint と、command identity が欠落・未知・nested・control character を含む場合の global fallback を維持してください。 #5091 / #5122 の明示的な `--files` preflight coverage では、通常実行 / dry-run と JSON / text の failure behavior を 1 つの focused contract matrix に保ちます。path token のない `--files`、absolute / relative の project root 外 path、対応 platform での symlink escape、`none` mode での directory segment symlink の拒否、`internal` / `all` mode での受理、存在しない target、directory、filter 対象、未対応 target、canonical duplicate(target ごとの casing と native Unicode/alias spelling を含む)、valid / invalid 混在時の atomicity、`--project` 展開と並ぶ raw token provenance、既に index 済みの cleanup、既存または削除済みの reconciliation control の例外、generated code の受理、membership snapshot の読取不能、`--files` のない暗黙の空 full scan control を検証してください。Windows の 8.3 ancestor alias は DB に確立済みの spelling へ戻ることを検証してください。indexed control が directory、FIFO、または policy で禁止された symlink に置換された場合は、置換 object を open / follow せず cleanup tombstone を維持する必要があります。TypeScript path alias configuration test は `none`、root 内外の `internal`、`all` を網羅し、解決後の regular target だけが secure-open されることを証明し、通常実行 / dry-run と in-process / worker path を一致させてください。明示 token が 1 件でも拒否された場合は、`UsageError`、上限付き・伏字化済みの入力別 reason diagnostic、indexed row と metadata の不変性を assertion し、snapshot 読取失敗は database error で fail closed することを確認します。 authoritative C# scoped update の coverage では、bounded parallel extraction を correctness と resource の契約として扱います。`2 * workers` の window 上限、固定 worker の再利用、window 全体の extraction barrier、target 順の single-writer persistence、mixed-language の serial 境界を同じ suite で維持してください。必須 fallback は parallelism 1、active symbol filter、content-load seam、post-extraction hook、non-authoritative / target 不足 snapshot、ambiguous な nullable language reuse、speculative probe 例外から serial per-file boundary への復帰を含みます。serial target disposition は missing/delete、path filter、indexability / language probe、unsupported purge、hardlink、stat reuse、load / revalidate、reuse cleanup、persistence、catch の厳密な順序を維持してください。serial / parallel parity は normal、generated、cap、oversized input を横断し、summary、file / chunk、symbol / reference の全 semantic column、正規化した reference-line / candidate identity、hotspot aggregate、issue、batch marker cleanup、readiness / user-version metadata、last-run readable-byte counter を比較してください。3段階の file-stat barrier も維持します。順序付き worker-failure fixture では、失敗する authoritative target、通常の変更 target、明示対象だが変更なしの authoritative target、oversized target を4-file windowにまとめ、persistence event と commit hook の厳密な順序、分離された failure phase、incomplete reason、metadata demotion、update後に独立観測する migration marker cleanupを固定してください。persistence 前、validated load 後、extraction completion 時の cancellation は、想定 committed prefix、batch ownership、derived readiness を固定します。global watchdog と direct fatal は active phase、bounded return、terminal side effect を検証し、source-negative ordering は cap で payload から除かれた confirmed evidence と、symbol extraction 中に block した earlier lexical contract candidate の両方を含めます。異常 window の fixture は注入 block をすべて解放し、snapshot 済み all-workers-stopped seam を待ってから static hook の復元と project root の削除を行ってください。 - `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。 + `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。end-of-options の coverage では、直後の option 風 query を command data のまま保持し、その1トークンより後の bounded-response option が通常どおり解析されることも固定してください。 status explain の coverage は source-generated `StatusResult` serializer property を列挙し、database を開かずに serialized top-level key がすべて成功することを必須にします。主要な readiness / trust / extension / maintenance / cap-hit metadata、dot 区切り member resolution、unknown key に対する上限付きの有効な candidate、悪意ある field input の redaction を同じ focused fixture に置き、serialization と explainability が独立して drift しないようにしてください。structured explain response の全 key を status の `--fields` projection に登録し、outer envelope からも runtime / path metadata が省略されることを上限付き JSON projection で固定してください。 safety recipe の意味論 coverage では、安全側と危険側の control を別々の indexed file に置いてください。`Regex.Escape` / `Regex.Unescape` と literal `UseShellExecute=false` が除外され、matching、source-defined、または解決不能な Regex member、literal `true`、伝播された shell-policy 値が classification evidence 付きで残ることを検証します。alias の trivia、alias に見える comment / string、bare `BoundedRegex` alias と完全修飾 BCL call が同居する file、literal `false` の前の改行、block / line comment の後に続く計算式を含めます。また、現行 reference identity を持たない legacy index の bare receiver を残すこと、通常の result envelope を埋める数の safe helper、count 出力と full JSON の一致、別の `new Regex` construction 正例も維持してください。 `QueryCommandRunnerSearchTests.cs` の JSON parser guard coverage は、両方の `json-parse-apis` child query と structured / compact JSON で1つの indexed fixture を共有してください。`BoundedJson`、直接 materialization、async stream / cancellation、無関係な値への guard、bound と streaming が同居する precedence case をまとめ、保持された row ごとに `parser_guard_evidence` が必ず1つであること、category count の合計が保持結果数と一致すること、shape 間で出力順が一致すること、snippet が変わらないことを必須にします。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d412b799b..f5e84dabe 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2453,7 +2453,7 @@ same source location. | `--palette ` | All commands | Choose the ANSI palette used when color output is enabled. Accepts `basic` (8-color SGR 30–37, the default fallback for minimal SSH/CI terminals), `256` (256-color `\x1b[38;5;Nm`), or `truecolor` (24-bit RGB `\x1b[38;2;R;G;Bm`). Precedence: `--palette` flag > `CDIDX_COLOR_PALETTE` env var > `COLORTERM` / `TERM` auto-detect. The basic palette avoids `\x1b[90m` (bright-black / dim), which is unreadable on many minimal terminals. | | `--metrics ` | All commands (and MCP tool calls) | Append one JSONL metrics record per CLI command / MCP tool call to ``. The `CDIDX_METRICS=` environment variable provides the same destination as a fallback when the flag is not passed. If the destination cannot be opened at startup, cdidx emits a bounded warning, disables metrics, and continues the underlying command. Later write or rotation failures remain best-effort and never break the command. | -If a query itself begins with `-`, pass it as `--query ` or `-- `. If an option value itself begins with `--`, pass it as `--opt=` rather than a separated value, for example `--path=--json-dir` or `--db=--tmp.db`. +If a query itself begins with `-`, pass it as `--query ` or `-- `. The token immediately after `--` remains literal query data even when it matches a bounded-response option such as `--fields`; options after that one query token continue to be parsed normally. If an option value itself begins with `--`, pass it as `--opt=` rather than a separated value, for example `--path=--json-dir` or `--db=--tmp.db`. ### Exit codes @@ -6108,7 +6108,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--palette ` | 全コマンド | カラー出力が有効なときに用いる ANSI パレットを選択する。`basic`(標準8色 SGR 30–37、最小 SSH/CI 端末向けの既定フォールバック)、`256`(256色 `\x1b[38;5;Nm`)、`truecolor`(24ビット RGB `\x1b[38;2;R;G;Bm`)を受け付ける。優先順位: `--palette` フラグ > `CDIDX_COLOR_PALETTE` 環境変数 > `COLORTERM` / `TERM` 自動判定。`basic` パレットは最小端末で読みにくい `\x1b[90m`(暗灰 / dim)を避ける。 | | `--metrics ` | 全コマンド(および MCP ツール呼び出し) | CLI コマンド / MCP ツール呼び出し 1 回ごとに JSONL レコードを 1 行ずつ `` に追記する。フラグ未指定時のフォールバックとして `CDIDX_METRICS=` 環境変数でも同じ出力先を指定できる。起動時に出力先を開けない場合、cdidx は長さを制限した警告を出してメトリクスを無効化し、本体コマンドを続行する。その後の書き込みやローテーションの失敗はベストエフォートのまま扱われ、本体コマンドを壊さない。 | -クエリ自体が `-` で始まる場合は `--query ` または `-- ` で渡してください。オプション値自体が `--` で始まる場合は、分離形式ではなく `--opt=` で渡します。たとえば `--path=--json-dir` や `--db=--tmp.db` のように指定します。 +クエリ自体が `-` で始まる場合は `--query ` または `-- ` で渡してください。`--` 直後の1トークンは `--fields` のような bounded-response オプションと一致してもリテラルなクエリデータのままで、その1トークンより後のオプションは通常どおり解析されます。オプション値自体が `--` で始まる場合は、分離形式ではなく `--opt=` で渡します。たとえば `--path=--json-dir` や `--db=--tmp.db` のように指定します。 ### 終了コード diff --git a/changelog.d/unreleased/5208.fixed.md b/changelog.d/unreleased/5208.fixed.md new file mode 100644 index 000000000..8c3fa8950 --- /dev/null +++ b/changelog.d/unreleased/5208.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 5208 +affected: + - USER_GUIDE.md + - TESTING_GUIDE.md + - src/CodeIndex/Cli/JsonEnvelopeWrapper.cs + - src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs + - tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs +--- + +## English + +- **Search now honors `--` for option-like literal queries (#5208)** — bounded JSON routing and control parsing now preserve the token immediately after `--` as query data, including names such as `--fields`, while continuing to parse valid output options that follow that query token. + +## 日本語 + +- **option 風のリテラル query で `--` が正しく機能するようになりました (#5208)** — bounded JSON の routing と control parsing は、`--fields` のような名前を含め、`--` 直後のトークンを query data として保持し、その query token より後の正しい output option は引き続き解析するようになりました。 diff --git a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs index cc3fd6358..39c415bcb 100644 --- a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs +++ b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs @@ -111,17 +111,17 @@ internal static bool ShouldAutoWrapBoundedResponse(string command, string[] args return false; if (command == "find" && IsStandaloneFindCountContinuationRequest(args)) return false; - if (HasArgument(args, "--fields") || HasArgument(args, "--cursor")) + if (HasArgument(command, args, "--fields") || HasArgument(command, args, "--cursor")) return true; if (command == "languages" - && HasJsonOutputSelection(args) - && (HasArgument(args, "--limit") || HasArgument(args, "--top"))) + && HasJsonOutputSelection(command, args) + && (HasArgument(command, args, "--limit") || HasArgument(command, args, "--top"))) { return true; } - if (AutoWrapByteBudgetCommands.Contains(command) && HasArgument(args, "--max-json-bytes")) + if (AutoWrapByteBudgetCommands.Contains(command) && HasArgument(command, args, "--max-json-bytes")) return true; - return AutoWrapCompactCommands.Contains(command) && HasCompactOutputSelection(args); + return AutoWrapCompactCommands.Contains(command) && HasCompactOutputSelection(command, args); } private static bool IsBoundedResponseRequest(string command, string[] args) @@ -132,37 +132,47 @@ private static bool IsBoundedResponseRequest(string command, string[] args) if (command == "search" && IsSearchAggregateResponseRequest(args)) return false; - return HasArgument(args, "--fields") - || HasArgument(args, "--cursor") - || (command == "search" && HasEnvelopeFlag(command, args) && HasJsonArrayOutputSelection(args)) - || (command != "search" && HasEnvelopeFlag(command, args) && HasArgument(args, "--max-json-bytes")) + return HasArgument(command, args, "--fields") + || HasArgument(command, args, "--cursor") + || (command == "search" && HasEnvelopeFlag(command, args) && HasJsonArrayOutputSelection(command, args)) + || (command != "search" && HasEnvelopeFlag(command, args) && HasArgument(command, args, "--max-json-bytes")) || ShouldAutoWrapBoundedResponse(command, args); } private static bool IsStandaloneWholeRowByteBudgetRequest(string command, string[] args) => command is "outline" or "unused" - && HasArgument(args, "--max-json-bytes"); + && HasArgument(command, args, "--max-json-bytes"); private static bool HasUnsupportedStandaloneBoundedControl(string command, string[] args) => command is "outline" or "unused" - && (HasArgument(args, "--fields") - || HasArgument(args, "--format") - || args.Any(arg => arg.StartsWith("--json=", StringComparison.Ordinal))); + && (HasArgument(command, args, "--fields") + || HasArgument(command, args, "--format") + || ClassifyArgumentTokens(command, args) + .Any(token => token.IsOption && token.Value.StartsWith("--json=", StringComparison.Ordinal))); private static bool IsStandaloneFindCountContinuationRequest(string[] args) => IsFindCountResponseRequest(args) - && HasArgument(args, "--cursor") - && !HasArgument(args, "--fields") - && !HasArgument(args, "--max-json-bytes") - && !HasCompactOutputSelection(args) + && HasArgument("find", args, "--cursor") + && !HasArgument("find", args, "--fields") + && !HasArgument("find", args, "--max-json-bytes") + && !HasCompactOutputSelection("find", args) && !HasEnvelopeFlag("find", args); private static bool IsFindCountResponseRequest(string[] args) { + var nextTokenIsLiteralQuery = false; for (var i = 0; i < args.Length; i++) { + if (nextTokenIsLiteralQuery) + { + nextTokenIsLiteralQuery = false; + continue; + } if (string.Equals(args[i], "--", StringComparison.Ordinal)) - break; + { + nextTokenIsLiteralQuery = true; + continue; + } if (string.Equals(args[i], "--query", StringComparison.Ordinal) && i + 1 < args.Length) { @@ -185,27 +195,31 @@ private static bool IsFindCountResponseRequest(string[] args) } private static bool IsSearchAggregateResponseRequest(string[] args) - => HasArgument(args, "--recipe") - || HasArgument(args, "--list-recipes") - || HasArgument(args, "--named-query") - || HasArgument(args, "--count") - || HasArgument(args, "--group-by") - || HasArgument(args, "--unique") - || HasArgument(args, "--count-by") - || HasArgument(args, "--summary-only"); + => HasArgument("search", args, "--recipe") + || HasArgument("search", args, "--list-recipes") + || HasArgument("search", args, "--named-query") + || HasArgument("search", args, "--count") + || HasArgument("search", args, "--group-by") + || HasArgument("search", args, "--unique") + || HasArgument("search", args, "--count-by") + || HasArgument("search", args, "--summary-only"); - private static bool HasJsonOutputSelection(string[] args) - => args.Any(arg => string.Equals(arg, "--json", StringComparison.Ordinal) - || arg.StartsWith("--json=", StringComparison.Ordinal)); + private static bool HasJsonOutputSelection(string command, string[] args) + => HasArgument(command, args, "--json"); - private static bool HasJsonArrayOutputSelection(string[] args) - => args.Any(arg => string.Equals(arg, "--json=array", StringComparison.OrdinalIgnoreCase)); + private static bool HasJsonArrayOutputSelection(string command, string[] args) + => ClassifyArgumentTokens(command, args) + .Any(token => token.IsOption + && string.Equals(token.Value, "--json=array", StringComparison.OrdinalIgnoreCase)); - private static bool HasCompactOutputSelection(string[] args) + private static bool HasCompactOutputSelection(string command, string[] args) { - for (var i = 0; i < args.Length; i++) + var tokens = ClassifyArgumentTokens(command, args).ToArray(); + for (var i = 0; i < tokens.Length; i++) { - var arg = args[i]; + if (!tokens[i].IsOption) + continue; + var arg = tokens[i].Value; if (string.Equals(arg, "--compact", StringComparison.Ordinal) || string.Equals(arg, "--format=compact", StringComparison.OrdinalIgnoreCase)) return true; @@ -219,7 +233,7 @@ private static bool HasCompactOutputSelection(string[] args) private static bool IsStaticStatusExplainRequest(string command, string[] args) => string.Equals(command, "status", StringComparison.Ordinal) - && HasArgument(args, "--explain"); + && HasArgument(command, args, "--explain"); private static int RunBoundedResponse( string command, @@ -228,7 +242,7 @@ private static int RunBoundedResponse( JsonSerializerOptions jsonOptions, Func runInner) { - if (TryReadRequestedMaxJsonBytes(args, out var requestedBytes) + if (TryReadRequestedMaxJsonBytes(command, args, out var requestedBytes) && requestedBytes <= 0) { return CommandErrorWriter.WriteResponseBudgetError( @@ -250,7 +264,7 @@ private static int RunBoundedResponse( return WriteBoundedResponseUsageError(controlError!, "Use the command help to pass positive --limit/--max-json-bytes values and a next_cursor returned by the same query."); } if (HasUnsupportedStandaloneBoundedControl(command, args) - || command == "unused" && HasArgument(args, "--summary-only")) + || command == "unused" && HasArgument(command, args, "--summary-only")) { return RunStandaloneValidationWithinBudget( command, @@ -293,7 +307,7 @@ private static int RunBoundedResponse( var bodyIntentValidationArgs = PrepareBoundedGraphBodyIntentValidationArgs(command, args); if (!QueryCommandRunner.TryValidateBoundedGraphSnippetLinesOption(command, bodyIntentValidationArgs, bodyOutputHidden)) return CommandExitCodes.UsageError; - if (HasArgument(args, "--count") + if (HasArgument(command, args, "--count") || command == "find" && IsFindCountResponseRequest(args)) return WriteBoundedResponseUsageError("Bounded response controls cannot be combined with --count.", "Run --count --json separately for a count-only response, or remove --count to page projected rows."); if (command == "map" && ValidateMapProjectionControls(args, controls.Fields) is { } mapProjectionError) @@ -413,7 +427,7 @@ private static int RunBoundedResponse( command, extraction.PrimaryCollection, suppressRuntimeMetadata, - groupedSymbolsRequest: command == "symbols" && HasArgument(args, "--group-partials")); + groupedSymbolsRequest: command == "symbols" && HasArgument(command, args, "--group-partials")); var pageItems = availableItems .Take(controls.PageLimit) .Select(item => ProjectResponseItem( @@ -1655,12 +1669,12 @@ private static string[] PrepareBoundedInnerArgs(string command, string[] args, B { var stripped = StripResponseOptions(command, args, stripLimit: PageableResponseCommands.Contains(command)); var bodyProjected = HasExplicitBodyProjection(controls.Fields); - var bodyOptionRequested = args.Any(arg => string.Equals(arg, "--body", StringComparison.Ordinal)); + var bodyOptionRequested = HasArgument(command, args, "--body"); if (command is not ("outline" or "references" or "callers" or "callees") && !bodyProjected && (controls.Compact || controls.Fields is { Count: > 0 })) { - stripped.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal)); + RemoveFlagOptions(command, stripped, arg => string.Equals(arg, "--body", StringComparison.Ordinal)); } var additions = new List(); if (PageableResponseCommands.Contains(command)) @@ -1708,7 +1722,7 @@ private static bool HasExplicitBodyProjection(IReadOnlyList? fields) "entrypoints"); if (collection is null) return null; - if (HasArgument(args, "--summary-only")) + if (HasArgument("map", args, "--summary-only")) return $"Map collection projection '{collection}' cannot be combined with --summary-only."; var requestedSections = ReadMapSections(args); @@ -1756,9 +1770,12 @@ private static bool HasExplicitBodyProjection(IReadOnlyList? fields) private static string[] PrepareCountArgs(string command, string[] args) { var stripped = StripResponseOptions(command, args, stripLimit: true); - stripped.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal) - || string.Equals(arg, "--summary-only", StringComparison.Ordinal) - || string.Equals(arg, "--strict-not-found", StringComparison.Ordinal)); + RemoveFlagOptions( + command, + stripped, + arg => string.Equals(arg, "--body", StringComparison.Ordinal) + || string.Equals(arg, "--summary-only", StringComparison.Ordinal) + || string.Equals(arg, "--strict-not-found", StringComparison.Ordinal)); RemoveParsedGraphSnippetLinesOption(stripped); var additions = new List(); if (command == "impact") @@ -1772,6 +1789,19 @@ private static string[] PrepareCountArgs(string command, string[] args) return [.. stripped]; } + private static void RemoveFlagOptions( + string command, + List args, + Func shouldRemove) + { + var retained = ClassifyArgumentTokens(command, [.. args]) + .Where(token => !token.IsOption || !shouldRemove(token.Value)) + .Select(token => token.Value) + .ToArray(); + args.Clear(); + args.AddRange(retained); + } + private static void RemoveParsedGraphSnippetLinesOption(List args) { for (var i = 0; i < args.Count;) @@ -1893,11 +1923,14 @@ private static bool TryParseBoundedResponseControls( string? cursor = null; int? maxJsonBytes = null; var pageLimit = DefaultPageLimit; - var compact = HasCompactOutputSelection(args); + var compact = HasCompactOutputSelection(command, args); error = null; - for (var i = 0; i < args.Length; i++) + var tokens = ClassifyArgumentTokens(command, args).ToArray(); + for (var i = 0; i < tokens.Length; i++) { - var arg = args[i]; + if (!tokens[i].IsOption) + continue; + var arg = tokens[i].Value; if (!TryReadInlineOrSeparated(args, ref i, arg, "--fields", out var fieldsValue, out var matched, out error)) { controls = default!; @@ -2044,13 +2077,16 @@ private static bool TryReadPositiveIntControl( return true; } - private static bool TryReadRequestedMaxJsonBytes(string[] args, out long requestedBytes) + private static bool TryReadRequestedMaxJsonBytes(string command, string[] args, out long requestedBytes) { requestedBytes = 0; - for (var i = 0; i < args.Length; i++) + var tokens = ClassifyArgumentTokens(command, args).ToArray(); + for (var i = 0; i < tokens.Length; i++) { + if (!tokens[i].IsOption) + continue; const string option = "--max-json-bytes"; - var arg = args[i]; + var arg = tokens[i].Value; string? raw = null; if (arg.StartsWith(option + "=", StringComparison.Ordinal)) raw = arg[(option.Length + 1)..]; diff --git a/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs b/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs index 89ac0a5f8..8ae1368f6 100644 --- a/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs +++ b/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs @@ -100,12 +100,12 @@ internal static string[] PrepareInnerArgs(string command, string[] args) private static IEnumerable ClassifyArgumentTokens(string command, string[] args) { - var afterEndOfOptions = false; + var nextTokenIsLiteralQuery = false; var nextTokenIsValue = false; for (var i = 0; i < args.Length; i++) { var arg = args[i]; - var isOption = !afterEndOfOptions && !nextTokenIsValue; + var isOption = !nextTokenIsLiteralQuery && !nextTokenIsValue; yield return new ArgumentToken(arg, isOption); if (nextTokenIsValue) @@ -113,11 +113,14 @@ private static IEnumerable ClassifyArgumentTokens(string command, nextTokenIsValue = false; continue; } - if (afterEndOfOptions) + if (nextTokenIsLiteralQuery) + { + nextTokenIsLiteralQuery = false; continue; + } if (string.Equals(arg, "--", StringComparison.Ordinal)) { - afterEndOfOptions = true; + nextTokenIsLiteralQuery = true; continue; } nextTokenIsValue = IsValueConsumingOption(command, arg) @@ -162,7 +165,7 @@ internal static int RunWrapped( if (IsBoundedResponseRequest(command, args)) return RunBoundedResponse(command, args, appVersion, jsonOptions, runInner); - if (HasArgument(args, "--max-json-bytes")) + if (HasArgument(command, args, "--max-json-bytes")) { CommandErrorWriter.WriteStderr("Error [E010_USAGE_ERROR]: --json-envelope cannot be combined with --max-json-bytes because envelope serialization changes the final stdout byte count."); CommandErrorWriter.WriteStderr("Hint: use streaming --json=ndjson with --max-json-bytes, or remove the byte cap when a single JSON envelope is required."); @@ -333,9 +336,11 @@ private static int WriteResponseSnapshotChangedError( return CommandExitCodes.UsageError; } - private static bool HasArgument(string[] args, string option) - => args.Any(arg => string.Equals(arg, option, StringComparison.Ordinal) - || arg.StartsWith(option + "=", StringComparison.Ordinal)); + private static bool HasArgument(string command, string[] args, string option) + => ClassifyArgumentTokens(command, args) + .Any(token => token.IsOption + && (string.Equals(token.Value, option, StringComparison.Ordinal) + || token.Value.StartsWith(option + "=", StringComparison.Ordinal))); private static JsonObject BuildEnvelope( string command, diff --git a/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs b/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs index 4b32ec78a..cdb1e1d25 100644 --- a/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs +++ b/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs @@ -601,6 +601,65 @@ public void Search_JsonEnvelopeLiteralRemainsCommandDataAcrossQueryForms_Issue49 } } + [Fact] + public void Search_EndOfOptionsPreservesBoundedControlLikeQueries_Issue5208() + { + var projectRoot = TestProjectHelper.CreateTempProject("bounded_option_query_5208"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + string[] literalQueries = + [ + "--fields", + "--cursor", + "--max-json-bytes", + "--limit", + "--top", + "--compact", + "--format=compact", + "--json=array", + "--count", + "--summary-only", + ]; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.txt", + "text", + string.Join(' ', literalQueries) + "\n"); + + foreach (var literalQuery in literalQueries) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( + [ + "search", + "--json", + "--db", + dbPath, + "--exact-substring", + "--", + literalQuery, + "--fields", + "path", + ], + _jsonOptions, + "1.0.0-test")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = JsonDocument.Parse(stdout); + Assert.Equal( + literalQuery, + document.RootElement.GetProperty("metadata").GetProperty("query_normalized").GetString()); + var result = Assert.Single(document.RootElement.GetProperty("results").EnumerateArray()); + Assert.Equal("src/App.txt", result.GetProperty("path").GetString()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunWrapped_CapturedOutputExceedsLimit_ReturnsJsonErrorEnvelope_Issue2901() { From b0e5109be0e12866f445cfc3926845f64d34f016 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 29 Aug 2026 00:35:43 +0900 Subject: [PATCH 2/3] Address adversarial review findings (#5208) --- TESTING_GUIDE.md | 4 +- .../Cli/JsonEnvelopeWrapper.Bounded.cs | 35 ++++--- src/CodeIndex/Cli/JsonEnvelopeWrapper.cs | 29 +++--- .../JsonEnvelopeWrapperTests.cs | 95 ++++++++++++++++++- 4 files changed, 136 insertions(+), 27 deletions(-) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 0cb0e3e1c..a4caa03e0 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -250,7 +250,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result Suggestions usage-hint coverage for #5196 must keep unknown-option human diagnostics and missing-id / invalid-combination JSON errors aligned on `cdidx suggestions --help`, preserve the existing error-code, category, usage, and exit-code contracts, and assert that the literal `` placeholder is never emitted. `CommandErrorWriterTests.cs` must keep catalog-backed known-command hints and the global fallback for missing, unknown, nested, or control-bearing command identities. Explicit `--files` preflight coverage for #5091 / #5122 keeps real/dry-run and JSON/text failure behavior in one focused contract matrix: bare `--files`, absolute and relative outside-root paths, platform-supported symlink escapes, `none`-mode directory-segment rejection, `internal`/`all` acceptance, nonexistent, directory, filtered, and unsupported targets, canonical duplicates (including target-specific casing and native Unicode/alias spelling), mixed valid/invalid atomicity, raw-token provenance beside `--project` expansion, already-indexed cleanup and existing or deleted reconciliation-control exceptions, generated-code acceptance, unreadable membership snapshots, and the implicit empty full-scan control without `--files`. Include Windows 8.3 ancestor aliases that must map back to established database spelling. Indexed controls replaced by a directory, FIFO, or policy-disallowed symlink must remain cleanup tombstones without opening or following the replacement. TypeScript path-alias configuration tests must cover `none`, inside/outside-root `internal`, and `all`, prove that only regular resolved targets are secure-opened, and keep real/dry-run plus in-process/worker paths aligned. Assert `UsageError`, bounded and redacted per-input reason diagnostics, and unchanged indexed rows and metadata whenever any explicit token is rejected; snapshot-read failures fail closed with a database error. Authoritative C# scoped-update coverage treats bounded parallel extraction as a correctness and resource contract. Keep the `2 * workers` window bound, fixed-worker reuse, whole-window extraction barrier, target-ordered single-writer persistence, and mixed-language serial boundary in the same suite. Required fallbacks cover parallelism one, active symbol filters, content-load seams, post-extraction hooks, non-authoritative or undersized snapshots, ambiguous nullable language reuse, and speculative probe exceptions returning to the serial per-file boundary. Keep serial target disposition in its exact missing/delete, path-filter, indexability/language-probe, unsupported-purge, hardlink, stat-reuse, load/revalidate, reuse-cleanup, persistence, and catch order. Serial/parallel parity must compare summaries, files/chunks/all semantic symbol and reference columns, normalized reference-line and candidate identities, hotspot aggregates, issues, batch-marker cleanup, readiness/user-version metadata, and last-run readable-byte counters across normal, generated, cap, and oversized inputs. Retain all three file-stat barriers. The ordered worker-failure fixture keeps a failed authoritative target, a normal changed target, an unchanged but explicitly targeted authoritative target, and an oversized target in one four-file window; pin the exact persistence-event and commit-hook order, isolated failure phase, incomplete reason, metadata demotion, and separately observed post-update migration-marker cleanup. Cancellation before persistence, after validated load, and at extraction completion must preserve the expected committed prefix, batch ownership, and derived readiness. Global watchdog and direct-fatal tests must assert active phase, bounded return, and terminal side effects; source-negative ordering tests must cover both cap-discarded confirmed evidence and an earlier lexical contract candidate still blocked in symbol extraction. Abnormal-window fixtures release every injected block and wait for the snapshotted all-workers-stopped seam before restoring static hooks or deleting the project root. - `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. End-of-options coverage must keep the immediately escaped option-like query as command data while proving that bounded-response options after that one query token still parse normally. + `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. End-of-options coverage must keep the immediately escaped option-like query as command data while proving that bounded-response options after that one query token still parse normally, escaped `--db` / `--data-dir` queries cannot alter response-path metadata, and cursors remain bound to distinct escaped query values. Status-explain coverage must enumerate the source-generated `StatusResult` serializer properties and require every serialized top-level key to succeed without opening a database. Keep major readiness/trust/extension/maintenance/cap-hit metadata assertions, dotted-member resolution, bounded valid unknown candidates, and redaction of hostile field input in the same focused fixture so serialization and explainability cannot drift independently. Keep every structured explain response key registered for status `--fields` projection, and exercise the response through a bounded JSON projection that proves the outer envelope also omits runtime and path metadata. Safety-recipe semantic coverage must keep safe and unsafe controls in separate indexed files: assert that `Regex.Escape` / `Regex.Unescape` and literal `UseShellExecute=false` are absent, while matching, source-defined, or unresolved Regex members, literal `true`, and propagated shell-policy values remain with classification evidence. Include alias trivia, alias-like comments and strings, a qualified BCL call in a file with a bare `BoundedRegex` alias, a line break before literal `false`, and computed continuations after block and line comments. Also retain a bare receiver from a legacy index without current reference identity, add enough safe helpers to saturate the normal result envelope, keep count output aligned with full JSON, and retain a separate `new Regex` construction positive. JSON parser-guard coverage in `QueryCommandRunnerSearchTests.cs` must share one indexed fixture across both `json-parse-apis` child queries and structured/compact JSON. Keep `BoundedJson`, direct materialization, async stream/cancellation, an unrelated-value guard, and a bound-plus-streaming precedence case together; require exactly one `parser_guard_evidence` classification per retained row, category counts that sum to the retained results, matching output order across shapes, and unchanged snippets. @@ -1400,7 +1400,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" #5196 の suggestions usage-hint coverage では、unknown option の human 診断と missing id / invalid combination の JSON error がともに `cdidx suggestions --help` を案内し、既存の error code、category、usage、exit code 契約を維持し、literal `` placeholder を出力しないことを固定してください。`CommandErrorWriterTests.cs` では catalog に基づく既知 command の hint と、command identity が欠落・未知・nested・control character を含む場合の global fallback を維持してください。 #5091 / #5122 の明示的な `--files` preflight coverage では、通常実行 / dry-run と JSON / text の failure behavior を 1 つの focused contract matrix に保ちます。path token のない `--files`、absolute / relative の project root 外 path、対応 platform での symlink escape、`none` mode での directory segment symlink の拒否、`internal` / `all` mode での受理、存在しない target、directory、filter 対象、未対応 target、canonical duplicate(target ごとの casing と native Unicode/alias spelling を含む)、valid / invalid 混在時の atomicity、`--project` 展開と並ぶ raw token provenance、既に index 済みの cleanup、既存または削除済みの reconciliation control の例外、generated code の受理、membership snapshot の読取不能、`--files` のない暗黙の空 full scan control を検証してください。Windows の 8.3 ancestor alias は DB に確立済みの spelling へ戻ることを検証してください。indexed control が directory、FIFO、または policy で禁止された symlink に置換された場合は、置換 object を open / follow せず cleanup tombstone を維持する必要があります。TypeScript path alias configuration test は `none`、root 内外の `internal`、`all` を網羅し、解決後の regular target だけが secure-open されることを証明し、通常実行 / dry-run と in-process / worker path を一致させてください。明示 token が 1 件でも拒否された場合は、`UsageError`、上限付き・伏字化済みの入力別 reason diagnostic、indexed row と metadata の不変性を assertion し、snapshot 読取失敗は database error で fail closed することを確認します。 authoritative C# scoped update の coverage では、bounded parallel extraction を correctness と resource の契約として扱います。`2 * workers` の window 上限、固定 worker の再利用、window 全体の extraction barrier、target 順の single-writer persistence、mixed-language の serial 境界を同じ suite で維持してください。必須 fallback は parallelism 1、active symbol filter、content-load seam、post-extraction hook、non-authoritative / target 不足 snapshot、ambiguous な nullable language reuse、speculative probe 例外から serial per-file boundary への復帰を含みます。serial target disposition は missing/delete、path filter、indexability / language probe、unsupported purge、hardlink、stat reuse、load / revalidate、reuse cleanup、persistence、catch の厳密な順序を維持してください。serial / parallel parity は normal、generated、cap、oversized input を横断し、summary、file / chunk、symbol / reference の全 semantic column、正規化した reference-line / candidate identity、hotspot aggregate、issue、batch marker cleanup、readiness / user-version metadata、last-run readable-byte counter を比較してください。3段階の file-stat barrier も維持します。順序付き worker-failure fixture では、失敗する authoritative target、通常の変更 target、明示対象だが変更なしの authoritative target、oversized target を4-file windowにまとめ、persistence event と commit hook の厳密な順序、分離された failure phase、incomplete reason、metadata demotion、update後に独立観測する migration marker cleanupを固定してください。persistence 前、validated load 後、extraction completion 時の cancellation は、想定 committed prefix、batch ownership、derived readiness を固定します。global watchdog と direct fatal は active phase、bounded return、terminal side effect を検証し、source-negative ordering は cap で payload から除かれた confirmed evidence と、symbol extraction 中に block した earlier lexical contract candidate の両方を含めます。異常 window の fixture は注入 block をすべて解放し、snapshot 済み all-workers-stopped seam を待ってから static hook の復元と project root の削除を行ってください。 - `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。end-of-options の coverage では、直後の option 風 query を command data のまま保持し、その1トークンより後の bounded-response option が通常どおり解析されることも固定してください。 + `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。end-of-options の coverage では、直後の option 風 query を command data のまま保持し、その1トークンより後の bounded-response option が通常どおり解析され、escape 済みの `--db` / `--data-dir` query が response path metadata を変更せず、異なる escape 済み query 値へ cursor が流用されないことも固定してください。 status explain の coverage は source-generated `StatusResult` serializer property を列挙し、database を開かずに serialized top-level key がすべて成功することを必須にします。主要な readiness / trust / extension / maintenance / cap-hit metadata、dot 区切り member resolution、unknown key に対する上限付きの有効な candidate、悪意ある field input の redaction を同じ focused fixture に置き、serialization と explainability が独立して drift しないようにしてください。structured explain response の全 key を status の `--fields` projection に登録し、outer envelope からも runtime / path metadata が省略されることを上限付き JSON projection で固定してください。 safety recipe の意味論 coverage では、安全側と危険側の control を別々の indexed file に置いてください。`Regex.Escape` / `Regex.Unescape` と literal `UseShellExecute=false` が除外され、matching、source-defined、または解決不能な Regex member、literal `true`、伝播された shell-policy 値が classification evidence 付きで残ることを検証します。alias の trivia、alias に見える comment / string、bare `BoundedRegex` alias と完全修飾 BCL call が同居する file、literal `false` の前の改行、block / line comment の後に続く計算式を含めます。また、現行 reference identity を持たない legacy index の bare receiver を残すこと、通常の result envelope を埋める数の safe helper、count 出力と full JSON の一致、別の `new Regex` construction 正例も維持してください。 `QueryCommandRunnerSearchTests.cs` の JSON parser guard coverage は、両方の `json-parse-apis` child query と structured / compact JSON で1つの indexed fixture を共有してください。`BoundedJson`、直接 materialization、async stream / cancellation、無関係な値への guard、bound と streaming が同居する precedence case をまとめ、保持された row ごとに `parser_guard_evidence` が必ず1つであること、category count の合計が保持結果数と一致すること、shape 間で出力順が一致すること、snippet が変わらないことを必須にします。 diff --git a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs index 39c415bcb..920342b31 100644 --- a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs +++ b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs @@ -314,7 +314,7 @@ private static int RunBoundedResponse( return WriteBoundedResponseUsageError(mapProjectionError, "Remove the conflicting map filter, or select a collection enabled by --sections."); var queryNormalized = ExtractQueryArg(command, args); - var (resolvedDbPath, dbPathExplicit) = ResolveQueryDbPath(args); + var (resolvedDbPath, dbPathExplicit) = ResolveQueryDbPath(command, args); var queryFingerprint = BuildResponseFingerprint(command, args); var suppressRuntimeMetadata = IsStaticStatusExplainRequest(command, args); var snapshot = suppressRuntimeMetadata @@ -2117,9 +2117,11 @@ private static string BuildResponseFingerprint(string command, string[] args) ? IsFindCountResponseRequest(args) ? "count" : "rows" : null; var normalized = StripResponseOptions(command, args, stripLimit: true); - normalized.RemoveAll(arg => string.Equals(arg, "--body", StringComparison.Ordinal)); - normalized.RemoveAll(arg => arg is "--allow-partial" or "--results-only" or "--verbose" or "--profile"); - RemoveOptionWithValue(normalized, "--line-scan-limit"); + RemoveFlagOptions( + command, + normalized, + arg => arg is "--body" or "--allow-partial" or "--results-only" or "--verbose" or "--profile"); + RemoveOptionWithValue(command, normalized, "--line-scan-limit"); var input = command + "\0" + string.Join('\0', normalized); if (scanMode is not null) input += "\0scan-mode=" + scanMode; @@ -2127,21 +2129,30 @@ private static string BuildResponseFingerprint(string command, string[] args) return Convert.ToHexString(hash.AsSpan(0, 8)).ToLowerInvariant(); } - private static void RemoveOptionWithValue(List args, string option) + private static void RemoveOptionWithValue(string command, List args, string option) { - for (var i = args.Count - 1; i >= 0; i--) + var tokens = ClassifyArgumentTokens(command, [.. args]).ToArray(); + var retained = new List(args.Count); + for (var i = 0; i < tokens.Length; i++) { - if (args[i].StartsWith(option + "=", StringComparison.Ordinal)) + var token = tokens[i]; + if (!token.IsOption) { - args.RemoveAt(i); + retained.Add(token.Value); continue; } - if (!string.Equals(args[i], option, StringComparison.Ordinal)) + if (token.Value.StartsWith(option + "=", StringComparison.Ordinal)) continue; - args.RemoveAt(i); - if (i < args.Count) - args.RemoveAt(i); + if (string.Equals(token.Value, option, StringComparison.Ordinal)) + { + if (i + 1 < tokens.Length) + i++; + continue; + } + retained.Add(token.Value); } + args.Clear(); + args.AddRange(retained); } internal static string FormatResponseCursor( diff --git a/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs b/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs index 8ae1368f6..54727ff38 100644 --- a/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs +++ b/src/CodeIndex/Cli/JsonEnvelopeWrapper.cs @@ -174,7 +174,7 @@ internal static int RunWrapped( var innerArgs = PrepareInnerArgs(command, args); var queryNormalized = ExtractQueryArg(command, args); - var (resolvedDbPath, dbPathExplicit) = ResolveQueryDbPath(args); + var (resolvedDbPath, dbPathExplicit) = ResolveQueryDbPath(command, args); var responseSnapshot = SafeReadResponseSnapshot(resolvedDbPath, dbPathExplicit, appVersion); using var captured = new BoundedStringWriter(MaxCapturedOutputChars); @@ -630,17 +630,24 @@ private static bool IsTerminalResultRecord(string command, JsonNode node) return firstPositional; } - private static bool TryExtractDbPath(string[] args, out string? dbPath) - => TryExtractOptionValue(args, "--db", out dbPath); + private static bool TryExtractDbPath(string command, string[] args, out string? dbPath) + => TryExtractOptionValue(command, args, "--db", out dbPath); - private static bool TryExtractDataDir(string[] args, out string? dataDir) - => TryExtractOptionValue(args, "--data-dir", out dataDir); + private static bool TryExtractDataDir(string command, string[] args, out string? dataDir) + => TryExtractOptionValue(command, args, "--data-dir", out dataDir); - private static bool TryExtractOptionValue(string[] args, string option, out string? value) + private static bool TryExtractOptionValue( + string command, + string[] args, + string option, + out string? value) { - for (var i = 0; i < args.Length; i++) + var tokens = ClassifyArgumentTokens(command, args).ToArray(); + for (var i = 0; i < tokens.Length; i++) { - var arg = args[i]; + if (!tokens[i].IsOption) + continue; + var arg = tokens[i].Value; if (string.Equals(arg, option, StringComparison.Ordinal) && i + 1 < args.Length) { value = args[i + 1]; @@ -656,10 +663,10 @@ private static bool TryExtractOptionValue(string[] args, string option, out stri return false; } - private static (string DbPath, bool DbPathExplicit) ResolveQueryDbPath(string[] args) + private static (string DbPath, bool DbPathExplicit) ResolveQueryDbPath(string command, string[] args) { - var dbPathExplicit = TryExtractDbPath(args, out var explicitDbPath); - TryExtractDataDir(args, out var explicitDataDir); + var dbPathExplicit = TryExtractDbPath(command, args, out var explicitDbPath); + TryExtractDataDir(command, args, out var explicitDataDir); var resolution = DbPathResolver.ResolveForQuery( Environment.CurrentDirectory, explicitDbPath, diff --git a/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs b/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs index cdb1e1d25..0c4a1a6bb 100644 --- a/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs +++ b/tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs @@ -620,6 +620,16 @@ public void Search_EndOfOptionsPreservesBoundedControlLikeQueries_Issue5208() "--json=array", "--count", "--summary-only", + "--body", + "--allow-partial", + "--results-only", + "--verbose", + "--profile", + "--line-scan-limit", + "--db", + "--db=ignored.db", + "--data-dir", + "--data-dir=ignored", ]; TestProjectHelper.InsertIndexedFile( dbPath, @@ -633,11 +643,11 @@ public void Search_EndOfOptionsPreservesBoundedControlLikeQueries_Issue5208() [ "search", "--json", - "--db", - dbPath, "--exact-substring", "--", literalQuery, + "--db", + dbPath, "--fields", "path", ], @@ -650,6 +660,9 @@ public void Search_EndOfOptionsPreservesBoundedControlLikeQueries_Issue5208() Assert.Equal( literalQuery, document.RootElement.GetProperty("metadata").GetProperty("query_normalized").GetString()); + Assert.Equal( + dbPath, + document.RootElement.GetProperty("metadata").GetProperty("db_path").GetString()); var result = Assert.Single(document.RootElement.GetProperty("results").EnumerateArray()); Assert.Equal("src/App.txt", result.GetProperty("path").GetString()); } @@ -660,6 +673,84 @@ public void Search_EndOfOptionsPreservesBoundedControlLikeQueries_Issue5208() } } + [Fact] + public void Search_EndOfOptionsPreservesLiteralQueryInCursorFingerprint_Issue5208() + { + var projectRoot = TestProjectHelper.CreateTempProject("bounded_option_cursor_5208"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + foreach (var path in new[] { "src/First.txt", "src/Second.txt" }) + { + TestProjectHelper.InsertIndexedFile( + dbPath, + path, + "text", + "--body --allow-partial --line-scan-limit --profile\n"); + } + + foreach (var (firstQuery, secondQuery) in new[] + { + ("--body", "--allow-partial"), + ("--line-scan-limit", "--profile"), + }) + { + var (firstExitCode, firstStdout, firstStderr) = CaptureConsole(() => ProgramRunner.Run( + [ + "search", + "--json", + "--exact-substring", + "--", + firstQuery, + "--db", + dbPath, + "--fields", + "path", + "--limit", + "1", + ], + _jsonOptions, + "1.0.0-test")); + + Assert.Equal(CommandExitCodes.Success, firstExitCode); + Assert.Equal(string.Empty, firstStderr); + using var firstDocument = JsonDocument.Parse(firstStdout); + var cursor = firstDocument.RootElement + .GetProperty("metadata") + .GetProperty("next_cursor") + .GetString(); + Assert.False(string.IsNullOrWhiteSpace(cursor)); + + var (resumeExitCode, resumeStdout, resumeStderr) = CaptureConsole(() => ProgramRunner.Run( + [ + "search", + "--json", + "--exact-substring", + "--", + secondQuery, + "--db", + dbPath, + "--fields", + "path", + "--limit", + "1", + "--cursor", + cursor!, + ], + _jsonOptions, + "1.0.0-test")); + + Assert.Equal(CommandExitCodes.UsageError, resumeExitCode); + Assert.Equal(string.Empty, resumeStdout); + Assert.Contains("cursor_mismatch", resumeStderr, StringComparison.Ordinal); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunWrapped_CapturedOutputExceedsLimit_ReturnsJsonErrorEnvelope_Issue2901() { From 6db288571d63d2a188e1702d0f94abfcb6a69a1d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 29 Aug 2026 00:50:40 +0900 Subject: [PATCH 3/3] Clarify search end-of-options guidance (#5208) --- TESTING_GUIDE.md | 4 ++-- USER_GUIDE.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index a4caa03e0..746c6c4a3 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -250,7 +250,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result Suggestions usage-hint coverage for #5196 must keep unknown-option human diagnostics and missing-id / invalid-combination JSON errors aligned on `cdidx suggestions --help`, preserve the existing error-code, category, usage, and exit-code contracts, and assert that the literal `` placeholder is never emitted. `CommandErrorWriterTests.cs` must keep catalog-backed known-command hints and the global fallback for missing, unknown, nested, or control-bearing command identities. Explicit `--files` preflight coverage for #5091 / #5122 keeps real/dry-run and JSON/text failure behavior in one focused contract matrix: bare `--files`, absolute and relative outside-root paths, platform-supported symlink escapes, `none`-mode directory-segment rejection, `internal`/`all` acceptance, nonexistent, directory, filtered, and unsupported targets, canonical duplicates (including target-specific casing and native Unicode/alias spelling), mixed valid/invalid atomicity, raw-token provenance beside `--project` expansion, already-indexed cleanup and existing or deleted reconciliation-control exceptions, generated-code acceptance, unreadable membership snapshots, and the implicit empty full-scan control without `--files`. Include Windows 8.3 ancestor aliases that must map back to established database spelling. Indexed controls replaced by a directory, FIFO, or policy-disallowed symlink must remain cleanup tombstones without opening or following the replacement. TypeScript path-alias configuration tests must cover `none`, inside/outside-root `internal`, and `all`, prove that only regular resolved targets are secure-opened, and keep real/dry-run plus in-process/worker paths aligned. Assert `UsageError`, bounded and redacted per-input reason diagnostics, and unchanged indexed rows and metadata whenever any explicit token is rejected; snapshot-read failures fail closed with a database error. Authoritative C# scoped-update coverage treats bounded parallel extraction as a correctness and resource contract. Keep the `2 * workers` window bound, fixed-worker reuse, whole-window extraction barrier, target-ordered single-writer persistence, and mixed-language serial boundary in the same suite. Required fallbacks cover parallelism one, active symbol filters, content-load seams, post-extraction hooks, non-authoritative or undersized snapshots, ambiguous nullable language reuse, and speculative probe exceptions returning to the serial per-file boundary. Keep serial target disposition in its exact missing/delete, path-filter, indexability/language-probe, unsupported-purge, hardlink, stat-reuse, load/revalidate, reuse-cleanup, persistence, and catch order. Serial/parallel parity must compare summaries, files/chunks/all semantic symbol and reference columns, normalized reference-line and candidate identities, hotspot aggregates, issues, batch-marker cleanup, readiness/user-version metadata, and last-run readable-byte counters across normal, generated, cap, and oversized inputs. Retain all three file-stat barriers. The ordered worker-failure fixture keeps a failed authoritative target, a normal changed target, an unchanged but explicitly targeted authoritative target, and an oversized target in one four-file window; pin the exact persistence-event and commit-hook order, isolated failure phase, incomplete reason, metadata demotion, and separately observed post-update migration-marker cleanup. Cancellation before persistence, after validated load, and at extraction completion must preserve the expected committed prefix, batch ownership, and derived readiness. Global watchdog and direct-fatal tests must assert active phase, bounded return, and terminal side effects; source-negative ordering tests must cover both cap-discarded confirmed evidence and an earlier lexical contract candidate still blocked in symbol extraction. Abnormal-window fixtures release every injected block and wait for the snapshotted all-workers-stopped seam before restoring static hooks or deleting the project root. - `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. End-of-options coverage must keep the immediately escaped option-like query as command data while proving that bounded-response options after that one query token still parse normally, escaped `--db` / `--data-dir` queries cannot alter response-path metadata, and cursors remain bound to distinct escaped query values. + `JsonEnvelopeWrapperTests.cs` owns global JSON-envelope token-role coverage. Keep parser-accepted separated and inline query values, option ordering, the `--` end-of-options form, missing-value boundaries for other value-bearing options, and a genuine `--json-envelope` output request together so preprocessing cannot consume command data, suppress a structured error envelope, or place its injected `--json` after the positional boundary. Search end-of-options coverage must keep the immediately escaped option-like query as command data while proving that bounded-response options after that one query token still parse normally, escaped `--db` / `--data-dir` queries cannot alter response-path metadata, and cursors remain bound to distinct escaped query values. Status-explain coverage must enumerate the source-generated `StatusResult` serializer properties and require every serialized top-level key to succeed without opening a database. Keep major readiness/trust/extension/maintenance/cap-hit metadata assertions, dotted-member resolution, bounded valid unknown candidates, and redaction of hostile field input in the same focused fixture so serialization and explainability cannot drift independently. Keep every structured explain response key registered for status `--fields` projection, and exercise the response through a bounded JSON projection that proves the outer envelope also omits runtime and path metadata. Safety-recipe semantic coverage must keep safe and unsafe controls in separate indexed files: assert that `Regex.Escape` / `Regex.Unescape` and literal `UseShellExecute=false` are absent, while matching, source-defined, or unresolved Regex members, literal `true`, and propagated shell-policy values remain with classification evidence. Include alias trivia, alias-like comments and strings, a qualified BCL call in a file with a bare `BoundedRegex` alias, a line break before literal `false`, and computed continuations after block and line comments. Also retain a bare receiver from a legacy index without current reference identity, add enough safe helpers to saturate the normal result envelope, keep count output aligned with full JSON, and retain a separate `new Regex` construction positive. JSON parser-guard coverage in `QueryCommandRunnerSearchTests.cs` must share one indexed fixture across both `json-parse-apis` child queries and structured/compact JSON. Keep `BoundedJson`, direct materialization, async stream/cancellation, an unrelated-value guard, and a bound-plus-streaming precedence case together; require exactly one `parser_guard_evidence` classification per retained row, category counts that sum to the retained results, matching output order across shapes, and unchanged snippets. @@ -1400,7 +1400,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" #5196 の suggestions usage-hint coverage では、unknown option の human 診断と missing id / invalid combination の JSON error がともに `cdidx suggestions --help` を案内し、既存の error code、category、usage、exit code 契約を維持し、literal `` placeholder を出力しないことを固定してください。`CommandErrorWriterTests.cs` では catalog に基づく既知 command の hint と、command identity が欠落・未知・nested・control character を含む場合の global fallback を維持してください。 #5091 / #5122 の明示的な `--files` preflight coverage では、通常実行 / dry-run と JSON / text の failure behavior を 1 つの focused contract matrix に保ちます。path token のない `--files`、absolute / relative の project root 外 path、対応 platform での symlink escape、`none` mode での directory segment symlink の拒否、`internal` / `all` mode での受理、存在しない target、directory、filter 対象、未対応 target、canonical duplicate(target ごとの casing と native Unicode/alias spelling を含む)、valid / invalid 混在時の atomicity、`--project` 展開と並ぶ raw token provenance、既に index 済みの cleanup、既存または削除済みの reconciliation control の例外、generated code の受理、membership snapshot の読取不能、`--files` のない暗黙の空 full scan control を検証してください。Windows の 8.3 ancestor alias は DB に確立済みの spelling へ戻ることを検証してください。indexed control が directory、FIFO、または policy で禁止された symlink に置換された場合は、置換 object を open / follow せず cleanup tombstone を維持する必要があります。TypeScript path alias configuration test は `none`、root 内外の `internal`、`all` を網羅し、解決後の regular target だけが secure-open されることを証明し、通常実行 / dry-run と in-process / worker path を一致させてください。明示 token が 1 件でも拒否された場合は、`UsageError`、上限付き・伏字化済みの入力別 reason diagnostic、indexed row と metadata の不変性を assertion し、snapshot 読取失敗は database error で fail closed することを確認します。 authoritative C# scoped update の coverage では、bounded parallel extraction を correctness と resource の契約として扱います。`2 * workers` の window 上限、固定 worker の再利用、window 全体の extraction barrier、target 順の single-writer persistence、mixed-language の serial 境界を同じ suite で維持してください。必須 fallback は parallelism 1、active symbol filter、content-load seam、post-extraction hook、non-authoritative / target 不足 snapshot、ambiguous な nullable language reuse、speculative probe 例外から serial per-file boundary への復帰を含みます。serial target disposition は missing/delete、path filter、indexability / language probe、unsupported purge、hardlink、stat reuse、load / revalidate、reuse cleanup、persistence、catch の厳密な順序を維持してください。serial / parallel parity は normal、generated、cap、oversized input を横断し、summary、file / chunk、symbol / reference の全 semantic column、正規化した reference-line / candidate identity、hotspot aggregate、issue、batch marker cleanup、readiness / user-version metadata、last-run readable-byte counter を比較してください。3段階の file-stat barrier も維持します。順序付き worker-failure fixture では、失敗する authoritative target、通常の変更 target、明示対象だが変更なしの authoritative target、oversized target を4-file windowにまとめ、persistence event と commit hook の厳密な順序、分離された failure phase、incomplete reason、metadata demotion、update後に独立観測する migration marker cleanupを固定してください。persistence 前、validated load 後、extraction completion 時の cancellation は、想定 committed prefix、batch ownership、derived readiness を固定します。global watchdog と direct fatal は active phase、bounded return、terminal side effect を検証し、source-negative ordering は cap で payload から除かれた confirmed evidence と、symbol extraction 中に block した earlier lexical contract candidate の両方を含めます。異常 window の fixture は注入 block をすべて解放し、snapshot 済み all-workers-stopped seam を待ってから static hook の復元と project root の削除を行ってください。 - `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。end-of-options の coverage では、直後の option 風 query を command data のまま保持し、その1トークンより後の bounded-response option が通常どおり解析され、escape 済みの `--db` / `--data-dir` query が response path metadata を変更せず、異なる escape 済み query 値へ cursor が流用されないことも固定してください。 + `JsonEnvelopeWrapperTests.cs` は global JSON envelope の token role coverage を担当します。前処理が command data を消費したり、構造化 error envelope を抑止したり、補う `--json` を positional 境界より後ろへ置いたりしないよう、parser が受理する分離形式と inline 形式の query 値、option 順序、`--` end-of-options 形式、他の value-bearing option の missing-value 境界、実際の `--json-envelope` 出力要求を同じ fixture で維持してください。search の end-of-options coverage では、直後の option 風 query を command data のまま保持し、その1トークンより後の bounded-response option が通常どおり解析され、escape 済みの `--db` / `--data-dir` query が response path metadata を変更せず、異なる escape 済み query 値へ cursor が流用されないことも固定してください。 status explain の coverage は source-generated `StatusResult` serializer property を列挙し、database を開かずに serialized top-level key がすべて成功することを必須にします。主要な readiness / trust / extension / maintenance / cap-hit metadata、dot 区切り member resolution、unknown key に対する上限付きの有効な candidate、悪意ある field input の redaction を同じ focused fixture に置き、serialization と explainability が独立して drift しないようにしてください。structured explain response の全 key を status の `--fields` projection に登録し、outer envelope からも runtime / path metadata が省略されることを上限付き JSON projection で固定してください。 safety recipe の意味論 coverage では、安全側と危険側の control を別々の indexed file に置いてください。`Regex.Escape` / `Regex.Unescape` と literal `UseShellExecute=false` が除外され、matching、source-defined、または解決不能な Regex member、literal `true`、伝播された shell-policy 値が classification evidence 付きで残ることを検証します。alias の trivia、alias に見える comment / string、bare `BoundedRegex` alias と完全修飾 BCL call が同居する file、literal `false` の前の改行、block / line comment の後に続く計算式を含めます。また、現行 reference identity を持たない legacy index の bare receiver を残すこと、通常の result envelope を埋める数の safe helper、count 出力と full JSON の一致、別の `new Regex` construction 正例も維持してください。 `QueryCommandRunnerSearchTests.cs` の JSON parser guard coverage は、両方の `json-parse-apis` child query と structured / compact JSON で1つの indexed fixture を共有してください。`BoundedJson`、直接 materialization、async stream / cancellation、無関係な値への guard、bound と streaming が同居する precedence case をまとめ、保持された row ごとに `parser_guard_evidence` が必ず1つであること、category count の合計が保持結果数と一致すること、shape 間で出力順が一致すること、snippet が変わらないことを必須にします。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index f5e84dabe..51cba2908 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2453,7 +2453,7 @@ same source location. | `--palette ` | All commands | Choose the ANSI palette used when color output is enabled. Accepts `basic` (8-color SGR 30–37, the default fallback for minimal SSH/CI terminals), `256` (256-color `\x1b[38;5;Nm`), or `truecolor` (24-bit RGB `\x1b[38;2;R;G;Bm`). Precedence: `--palette` flag > `CDIDX_COLOR_PALETTE` env var > `COLORTERM` / `TERM` auto-detect. The basic palette avoids `\x1b[90m` (bright-black / dim), which is unreadable on many minimal terminals. | | `--metrics ` | All commands (and MCP tool calls) | Append one JSONL metrics record per CLI command / MCP tool call to ``. The `CDIDX_METRICS=` environment variable provides the same destination as a fallback when the flag is not passed. If the destination cannot be opened at startup, cdidx emits a bounded warning, disables metrics, and continues the underlying command. Later write or rotation failures remain best-effort and never break the command. | -If a query itself begins with `-`, pass it as `--query ` or `-- `. The token immediately after `--` remains literal query data even when it matches a bounded-response option such as `--fields`; options after that one query token continue to be parsed normally. If an option value itself begins with `--`, pass it as `--opt=` rather than a separated value, for example `--path=--json-dir` or `--db=--tmp.db`. +If a query itself begins with `-`, pass it as `--query ` or `-- `. Follow the command's usage when placing options: `find` requires its options before `--`, while `search` keeps the token immediately after `--` as literal query data even when it matches a bounded-response option such as `--fields` and continues parsing options after that one query token. If an option value itself begins with `--`, pass it as `--opt=` rather than a separated value, for example `--path=--json-dir` or `--db=--tmp.db`. ### Exit codes @@ -6108,7 +6108,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--palette ` | 全コマンド | カラー出力が有効なときに用いる ANSI パレットを選択する。`basic`(標準8色 SGR 30–37、最小 SSH/CI 端末向けの既定フォールバック)、`256`(256色 `\x1b[38;5;Nm`)、`truecolor`(24ビット RGB `\x1b[38;2;R;G;Bm`)を受け付ける。優先順位: `--palette` フラグ > `CDIDX_COLOR_PALETTE` 環境変数 > `COLORTERM` / `TERM` 自動判定。`basic` パレットは最小端末で読みにくい `\x1b[90m`(暗灰 / dim)を避ける。 | | `--metrics ` | 全コマンド(および MCP ツール呼び出し) | CLI コマンド / MCP ツール呼び出し 1 回ごとに JSONL レコードを 1 行ずつ `` に追記する。フラグ未指定時のフォールバックとして `CDIDX_METRICS=` 環境変数でも同じ出力先を指定できる。起動時に出力先を開けない場合、cdidx は長さを制限した警告を出してメトリクスを無効化し、本体コマンドを続行する。その後の書き込みやローテーションの失敗はベストエフォートのまま扱われ、本体コマンドを壊さない。 | -クエリ自体が `-` で始まる場合は `--query ` または `-- ` で渡してください。`--` 直後の1トークンは `--fields` のような bounded-response オプションと一致してもリテラルなクエリデータのままで、その1トークンより後のオプションは通常どおり解析されます。オプション値自体が `--` で始まる場合は、分離形式ではなく `--opt=` で渡します。たとえば `--path=--json-dir` や `--db=--tmp.db` のように指定します。 +クエリ自体が `-` で始まる場合は `--query ` または `-- ` で渡してください。オプションの配置は各コマンドの usage に従ってください。`find` ではすべてのオプションを `--` より前に置く必要がありますが、`search` では `--` 直後の1トークンが `--fields` のような bounded-response オプションと一致してもリテラルなクエリデータのままで、その1トークンより後のオプションも通常どおり解析されます。オプション値自体が `--` で始まる場合は、分離形式ではなく `--opt=` で渡します。たとえば `--path=--json-dir` や `--db=--tmp.db` のように指定します。 ### 終了コード