diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 28708a6008..14b342693b 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -319,7 +319,9 @@ top-level groups such as `definitions`, `file`, `graph`, `references`, `callers`, and `callees`; `--body-only` is shorthand for `--body --fields definitions`. When a definition body is longer than the returned slice, `body_content_next_start_line` points to the next source line to pass with -`--body-start`; use `--body-lines` to choose the page size. If a single long +`--body-start`; use `--body-lines` (or alias `--body-line-count`) to choose the page size. `inspect` +can also return a bounded `source_excerpt` when you pass `--line`, `--start-line` / `--end-line`, +and optional `--context`, `--before`, or `--after`. If a single long source line hits the body byte cap, continuation still advances to the following source line because body paging is line-based. `inspect --json` also includes `body_mode` metadata so clients can see whether body content was requested, @@ -340,6 +342,7 @@ cdidx inspect QueryCommandRunner --json --pretty cdidx map --compact # capped JSON with truncation metadata cdidx inspect Compute --body-only # definitions with body_content only cdidx inspect Compute --body --body-start 40 --body-lines 40 +cdidx inspect Compute --line 42 --context 2 --json ``` ## Editor and index portability @@ -1366,10 +1369,12 @@ same source location. | `--json` | All commands except `mcp` | JSON output (for AI/machine use). `search --json` writes newline-delimited result objects followed by a final `{"done":true,"count":N,"interrupted":false}` sentinel, including zero-result output, so stream consumers can detect clean completion. | | `--pretty` | JSON-capable commands except `mcp` | Pretty-print JSON output with indentation. Default `search --json` remains newline-delimited; use `search --json=array --pretty` for an indented search result array. | | `--compact` | `map`, `inspect`, `outline` | Emit AI-oriented compact JSON with capped list sections and `truncation.sections.*` metadata. The default cap is 5 unless `--limit` / `--top` is supplied. | -| `--fields ` | `inspect` | Select top-level inspect JSON groups: `file`, `workspace`, `graph`, `definitions`, `body`, `nearby_symbols`, `references`, `callers`, `callees`, or `all`. `body` includes definition bodies and maps to `definitions`. | +| `--fields ` | `inspect` | Select top-level inspect JSON groups: `file`, `workspace`, `graph`, `definitions`, `body`, `source_excerpt`, `nearby_symbols`, `references`, `callers`, `callees`, or `all`. `body` includes definition bodies and maps to `definitions`. | | `--body-only` | `inspect` | Shorthand for `--body --fields definitions`, useful when large audits need implementation text without graph context. | | `--body-start ` | `inspect` | Start the returned definition body slice at a 1-based source line inside the symbol body. Pair with `body_content_next_start_line` from JSON to page a long body. | -| `--body-lines ` | `inspect` | Return at most this many definition body lines for `--body`, `--body-only`, or `--fields body`; maximum 1000. | +| `--body-lines ` / `--body-line-count ` | `inspect` | Return at most this many definition body lines for `--body`, `--body-only`, or `--fields body`; maximum 1000. | +| `--line ` / `--start-line ` / `--end-line ` | `inspect` | Add a bounded `source_excerpt` to inspect output. Use `--path --line ` without a symbol query for a file/line excerpt. | +| `--context ` / `--before ` / `--after ` | `inspect` | Add symmetric or one-sided context lines to the `source_excerpt` window. | | `--status ` | `suggestions` | Filter local suggestion history by GitHub submission state. | | `--language ` / `--lang ` | `suggestions` | Filter local suggestion history by recorded target language. | | `--category ` | `suggestions` | Filter local suggestion history by suggestion category. | @@ -1416,7 +1421,7 @@ same source location. | `--exact-substring` | `search` | Preferred explicit name for search exactness: case-sensitive exact substring (FTS5 bypassed). | | `--prefix` | `search` | Opt into FTS5 prefix-phrase expansion for every token in the query. Without this flag the literal-safe path quotes each token as a strict FTS5 phrase, so a bare `search 計算` only matches the token `計算` and not `計算する` (unicode61 keeps adjacent CJK codepoints as one token). Appending `*` to a single token (`search 計算*`) opts in for that token only; `--prefix` opts in for the whole query. Cannot be combined with `--exact` / `--exact-substring` (those bypass FTS5 entirely). | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Preferred explicit name for symbol-name exactness: NFKC + Unicode CaseFold exact equality (`Ä` / `ä`, `Run` / `Run`, ligatures, sharp-S, and Greek final sigma collapse). Unicode CaseFold remains locale-invariant, so Turkish dotted `İ` is still distinct from plain `i`. For C#, pass the canonical extracted name (`operator +`, `operator checked +`, `explicit operator Money`, `implicit operator decimal`, `Item`) rather than source keywords like `this` / `explicit`. Falls back to ASCII `COLLATE NOCASE` while the DB still contains stale fold metadata; prefer `cdidx backfill-fold`, or use a plain `cdidx index .` if it rewrites or purges every stale row, otherwise `--rebuild`. `status --json` exposes `fold_ready` and `csharp_symbol_name_ready` so AI clients can tell which path is active. When a read-only legacy DB is missing the fallback exact-match indexes, human-readable output warns and CLI JSON / MCP `structuredContent` expose degraded-state metadata. | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind (case-insensitive; `--kind FUNCTION` is treated as `--kind function`). `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `lambda`, `async_function`, `generator`, `async_generator`, `test.method`, `class`, `struct`, `interface`, `protocol`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` accepts all indexed reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`, `type_reference`); `callers` / `callees` accept only the call-graph kinds (`call`, `instantiate`, `subscribe`) and reject non-call-graph kinds (`--kind attribute` / `--kind annotation` / `--kind type_reference`) with a usage error — metadata rows are attributed to the enclosing body-range symbol rather than the annotated target, and `type_reference` rows are compile-time type-position edges (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls, so `callers` / `callees` cannot answer either correctly; use `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` instead. `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`, `type_reference`). Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `inspect`, `hotspots`, `unused`, `validate` | Filter by kind (case-insensitive; `--kind FUNCTION` is treated as `--kind function`). `definition` / `symbols` / `inspect` / `hotspots` / `unused` use symbol kinds (`function`, `lambda`, `async_function`, `generator`, `async_generator`, `test.method`, `class`, `struct`, `interface`, `protocol`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` accepts all indexed reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`, `type_reference`); `callers` / `callees` accept only the call-graph kinds (`call`, `instantiate`, `subscribe`) and reject non-call-graph kinds (`--kind attribute` / `--kind annotation` / `--kind type_reference`) with a usage error — metadata rows are attributed to the enclosing body-range symbol rather than the annotated target, and `type_reference` rows are compile-time type-position edges (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls, so `callers` / `callees` cannot answer either correctly; use `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` instead. `inspect` filters the definition candidates and primary file context while keeping graph evidence keyed to the queried symbol name. `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`, `type_reference`). Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | | `--rank-by ` | `callers`, `callees` | Choose the caller/callee ranking model. `weighted` is the default and scores `instantiate=3.0`, `call=1.0`, `subscribe=0.1`; `count` sorts by raw `reference_count`; `kind` groups by reference kind first, then count. | | `--body` | `definition`, `references`, `callers`, `callees`, `impact`, `inspect` | Include reconstructed body content or capped graph-location excerpts | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | Return only counts. `search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` / `hotspots` ignore `--limit` and return authoritative totals; only `impact` still reports the visible page count and may truncate with `--limit` (with `--json`: a single count object; commands that expose file counts add `files`) | @@ -2855,7 +2860,9 @@ AI 向けに上限付き payload が必要な場合、`map`、`inspect`、`outli top-level group を選択します。`--body-only` は `--body --fields definitions` の shorthand です。definition body が返却 slice より長い場合は `body_content_next_start_line` が次に `--body-start` へ渡す source line を示します。 -`--body-lines` で page size を指定できます。`inspect --json` には `body_mode` +`--body-lines`(alias: `--body-line-count`)で page size を指定できます。`--line`、`--start-line` / `--end-line`、 +任意の `--context`、`--before`、`--after` を渡すと、`inspect` は範囲を絞った `source_excerpt` +も返します。`inspect --json` には `body_mode` metadata も含まれるため、body content が要求済みか、存在するか、次に使う flag が何かを client 側で判断できます。 count-only JSON(対応 command の `--count --json` または `--format count`)は、 @@ -2874,6 +2881,7 @@ cdidx inspect QueryCommandRunner --json --pretty cdidx map --compact # truncation metadata 付きの cap 済み JSON cdidx inspect Compute --body-only # body_content 付き definitions のみ cdidx inspect Compute --body --body-start 40 --body-lines 40 +cdidx inspect Compute --line 42 --context 2 --json ``` ## Editor / index portability @@ -3908,10 +3916,12 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--json` | `mcp` を除く全コマンド | JSON出力(AI/機械向け) | | `--pretty` | `mcp` を除く JSON 対応コマンド | JSON 出力をインデント付きで整形。既定の `search --json` は newline-delimited のまま維持されるため、検索結果配列を整形したい場合は `search --json=array --pretty` を使う。 | | `--compact` | `map`、`inspect`、`outline` | list section を cap した AI 向け compact JSON を出力し、`truncation.sections.*` metadata を含める。既定 cap は 5 件で、`--limit` / `--top` 指定時はその値を使う。 | -| `--fields ` | `inspect` | inspect JSON の top-level group を選択。`file`、`workspace`、`graph`、`definitions`、`body`、`nearby_symbols`、`references`、`callers`、`callees`、`all` を指定できる。`body` は definition body を含め、`definitions` に対応する。 | +| `--fields ` | `inspect` | inspect JSON の top-level group を選択。`file`、`workspace`、`graph`、`definitions`、`body`、`source_excerpt`、`nearby_symbols`、`references`、`callers`、`callees`、`all` を指定できる。`body` は definition body を含め、`definitions` に対応する。 | | `--body-only` | `inspect` | `--body --fields definitions` の shorthand。大規模 audit で graph context なしに実装本文だけが必要な場合に使う。 | | `--body-start ` | `inspect` | symbol body 内の 1-based source line から definition body slice を返す。長い body の page 送りでは JSON の `body_content_next_start_line` を次の値として渡す。 | -| `--body-lines ` | `inspect` | `--body`、`--body-only`、`--fields body` で返す definition body 行数の上限。最大 1000。 | +| `--body-lines ` / `--body-line-count ` | `inspect` | `--body`、`--body-only`、`--fields body` で返す definition body 行数の上限。最大 1000。 | +| `--line ` / `--start-line ` / `--end-line ` | `inspect` | inspect 出力に範囲を絞った `source_excerpt` を追加する。symbol query なしで `--path --line ` を渡すと file/line 抜粋だけを返せる。 | +| `--context ` / `--before ` / `--after ` | `inspect` | `source_excerpt` の前後または片側 context 行を追加する。 | | `--status ` | `suggestions` | ローカル提案履歴を GitHub 送信状態で絞り込みます。 | | `--language ` / `--lang ` | `suggestions` | ローカル提案履歴を記録済み対象言語で絞り込みます。 | | `--category ` | `suggestions` | ローカル提案履歴を提案カテゴリで絞り込みます。 | @@ -3957,7 +3967,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--prefix` | `search` | クエリの全トークンを FTS5 prefix phrase に昇格させる opt-in。フラグなしでは literal-safe 経路が各トークンを strict な FTS5 phrase として引用するため、素の `search 計算` は `計算` トークンにのみマッチし `計算する` は拾わない(unicode61 が連続 CJK コードポイントを 1 トークン扱いする仕様)。トークン末尾に `*` を付ける(`search 計算*`)とそのトークンだけが prefix phrase になる shorthand、`--prefix` はクエリ全体に適用する。`--exact` / `--exact-substring` と併用不可(exact は FTS5 を経由しないため)。 | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | symbol-name exactness 用の推奨 explicit alias。NFKC + Unicode CaseFold による完全一致(`Ä` / `ä`、全角 `Run` / `Run`、合字、sharp-S、Greek final sigma を畳み込む)。Unicode CaseFold は locale-invariant のため、トルコ語の dotted `İ` は plain `i` と同一視しない。C# では `this` / `explicit` のような source keyword ではなく、抽出済みの canonical name(`operator +`、`operator checked +`、`explicit operator Money`、`implicit operator decimal`、`Item`)を渡す。DB に stale な fold metadata が残る間は ASCII `COLLATE NOCASE` に fallback するため、まず `cdidx backfill-fold`、または stale row を全置換できる通常の `cdidx index .`、それが無理なら `--rebuild` を使う(`status --json` の `fold_ready` と `csharp_symbol_name_ready` で判定)。read-only な旧DBに fallback exact-match index が無い場合は、人間向け出力が WARN を表示し、CLI JSON と MCP `structuredContent` が縮退メタデータを返す。 | | `--lang ` | クエリ系 | 言語でフィルタ(大文字小文字を区別しない。`--lang Python` は `--lang python` と同じ扱い)。`c#`、`cs`、`kt`、`kts` のような一般的な別名も受け付ける。未知の値を指定すると、人間向け出力の 0 件応答に `Available: <言語一覧>` ヒントが付く。 | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ(大文字小文字を区別しない。`--kind FUNCTION` は `--kind function` と同じ扱い)。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`lambda`、`async_function`、`generator`、`async_generator`、`test.method`、`class`、`struct`、`interface`、`protocol`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` は全ての reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`、`type_reference`)を受け付ける。`callers` / `callees` は call-graph 種別のみ(`call`、`instantiate`、`subscribe`)を受け付け、非 call-graph 種別(`--kind attribute` / `--kind annotation` / `--kind type_reference`)は usage error で拒否する — metadata 行は注釈対象そのものではなく body-range 上の外側シンボルに帰属し、`type_reference` は宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型位置エッジであり実行時呼び出しではないため、`callers` / `callees` はいずれの kind にも正しく答えられない。metadata / 型位置参照の列挙は `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` を使う。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` / `type_reference` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `inspect`, `hotspots`, `unused`, `validate` | 種別でフィルタ(大文字小文字を区別しない。`--kind FUNCTION` は `--kind function` と同じ扱い)。`definition` / `symbols` / `inspect` / `hotspots` / `unused` は symbol kind(`function`、`lambda`、`async_function`、`generator`、`async_generator`、`test.method`、`class`、`struct`、`interface`、`protocol`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` は全ての reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`、`type_reference`)を受け付ける。`callers` / `callees` は call-graph 種別のみ(`call`、`instantiate`、`subscribe`)を受け付け、非 call-graph 種別(`--kind attribute` / `--kind annotation` / `--kind type_reference`)は usage error で拒否する — metadata 行は注釈対象そのものではなく body-range 上の外側シンボルに帰属し、`type_reference` は宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型位置エッジであり実行時呼び出しではないため、`callers` / `callees` はいずれの kind にも正しく答えられない。metadata / 型位置参照の列挙は `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` を使う。`inspect` は定義候補と primary file context を絞り込み、graph evidence はクエリした symbol name に紐づけたまま返す。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` / `type_reference` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | | `--body` | `definition`, `references`, `callers`, `callees`, `impact`, `inspect` | 再構成した本文、または上限付きの graph 位置抜粋を含める | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | 件数だけを返す。`search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` / `hotspots` は `--limit` を無視した総件数を返し、`impact` だけは visible page count のままで `--limit` によって切り詰められることがある(`--json` 併用時は単一の count オブジェクト。files 件数を出すコマンドは `files` も返す) | | `--group-by ` | `hotspots` | hotspot の集計単位を選ぶ。既定は非 SQL scope では `symbol`、`--lang sql` では既存の statement-oriented grouping を保つため `statement`。JSON には `grouped_by` が入り、mixed-language 呼び出しでも現在の単位を確認できる。`file` は symbol hotspot の参照量を対象ファイル単位にまとめる。 | diff --git a/changelog.d/unreleased/3663.fixed.md b/changelog.d/unreleased/3663.fixed.md new file mode 100644 index 0000000000..755a407a3a --- /dev/null +++ b/changelog.d/unreleased/3663.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3663 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- List every implemented command in the top-level `cdidx --help` command summary, including navigation helpers such as `files`, `find`, `excerpt`, and `languages`. + +## 日本語 + +- `cdidx --help` の top-level command summary に、`files`、`find`、`excerpt`、`languages` などを含む実装済みコマンドをすべて表示するようにしました。 diff --git a/changelog.d/unreleased/3665.fixed.md b/changelog.d/unreleased/3665.fixed.md new file mode 100644 index 0000000000..cfb7d26fcd --- /dev/null +++ b/changelog.d/unreleased/3665.fixed.md @@ -0,0 +1,20 @@ +--- +category: fixed +issues: + - 3665 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - USER_GUIDE.md + - tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **`inspect` accepts `--body-line-count` as a `--body-lines` alias (#3665)** — CLI parsing, help, and docs now recognize the natural body line count spelling. + +## 日本語 + +- **`inspect` が `--body-lines` の alias として `--body-line-count` を受け付けるようになりました (#3665)** — CLI parser、help、docs が自然な body line count の綴りを認識します。 diff --git a/changelog.d/unreleased/3666.fixed.md b/changelog.d/unreleased/3666.fixed.md new file mode 100644 index 0000000000..38369dda83 --- /dev/null +++ b/changelog.d/unreleased/3666.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 3666 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Database/DbSymbolReader.cs + - USER_GUIDE.md + - tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- Let `cdidx inspect --kind ` filter definition candidates and prefer production source definitions over test definitions when both match the same query. + +## 日本語 + +- `cdidx inspect --kind ` で定義候補を絞り込み、同じクエリで本番ソースとテスト定義が両方一致する場合は本番ソース定義を優先するようにしました。 diff --git a/changelog.d/unreleased/3786.fixed.md b/changelog.d/unreleased/3786.fixed.md new file mode 100644 index 0000000000..53b4438962 --- /dev/null +++ b/changelog.d/unreleased/3786.fixed.md @@ -0,0 +1,20 @@ +--- +category: fixed +issues: + - 3786 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - USER_GUIDE.md + - tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- Add `inspect` source excerpts via `--line`, `--start-line`, `--end-line`, and context flags so JSON includes bounded `source_excerpt` metadata and human output shows numbered source lines. + +## 日本語 + +- `inspect` に `--line`、`--start-line`、`--end-line`、context 系 flag による source excerpt を追加し、JSON では範囲付き `source_excerpt` metadata、人間向け出力では行番号付き source 行を表示するようにしました。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 6977b2858c..201e71ea5c 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -116,7 +116,7 @@ internal static class CliFlagSchema private static readonly string[] KindCommands = [ - "definition", "goto", "references", "callers", "callees", "symbols", "unused", "hotspots", "validate", + "definition", "goto", "references", "callers", "callees", "symbols", "inspect", "unused", "hotspots", "validate", ]; private static readonly string[] SeverityCommands = ["validate"]; private static readonly string[] VisibilityCommands = @@ -161,6 +161,7 @@ internal static class CliFlagSchema private static readonly string[] BodyCommands = ["definition", "references", "callers", "callees", "impact", "inspect"]; private static readonly string[] InspectFieldCommands = ["inspect"]; + private static readonly string[] InspectSourceExcerptCommands = ["inspect"]; private static readonly string[] MaxLineWidthCommands = [ @@ -300,7 +301,8 @@ private static IReadOnlyList BuildAll() new() { Name = "--body", Description = "Include definition body snippets in JSON-capable result rows", Commands = Set(BodyCommands) }, new() { Name = "--body-start", ValuePlaceholder = "", Description = "Inspect: start definition body slice at this 1-based source line", Commands = Set(InspectFieldCommands) }, new() { Name = "--body-lines", ValuePlaceholder = "", Description = "Inspect: return at most this many definition body lines", Commands = Set(InspectFieldCommands) }, - new() { Name = "--fields", ValuePlaceholder = "", Description = "Inspect: select top-level JSON evidence groups", Commands = Set(InspectFieldCommands) }, + new() { Name = "--body-line-count", ValuePlaceholder = "", Description = "Inspect: alias for --body-lines", Commands = Set(InspectFieldCommands) }, + new() { Name = "--fields", ValuePlaceholder = "", Description = "Inspect: select top-level JSON evidence groups", Commands = Set(InspectFieldCommands) }, new() { Name = "--body-only", Description = "Inspect: body-focused JSON shorthand for --body --fields definitions", Commands = Set(InspectFieldCommands) }, new() { Name = "--exact", Description = "Backward-compatible exact shorthand", Commands = Set(ExactCommands) }, new() { Name = "--regex", Description = "Use regular expression matching", Commands = Set("find") }, @@ -334,12 +336,14 @@ private static IReadOnlyList BuildAll() new() { Name = "--fts", Description = "Raw FTS5 syntax", Commands = Set("search") }, new() { Name = "--no-dedup", Description = "Show duplicate chunks", Commands = Set("search") }, new() { Name = "--no-visibility-rank", Description = "Keep legacy search ranking without symbol visibility weighting", Commands = Set("search") }, - new() { Name = "--before", ValuePlaceholder = "", Description = "Context lines before", Commands = Set("find", "excerpt") }, - new() { Name = "--after", ValuePlaceholder = "", Description = "Context lines after", Commands = Set("find", "excerpt") }, + new() { Name = "--line", ValuePlaceholder = "", Description = "Inspect: include one source line as source_excerpt", Commands = Set(InspectSourceExcerptCommands) }, + new() { Name = "--context", ValuePlaceholder = "", Description = "Inspect: source_excerpt context lines before and after", Commands = Set(InspectSourceExcerptCommands) }, + new() { Name = "--before", ValuePlaceholder = "", Description = "Context lines before", Commands = Set("find", "excerpt", "inspect") }, + new() { Name = "--after", ValuePlaceholder = "", Description = "Context lines after", Commands = Set("find", "excerpt", "inspect") }, new() { Name = "--start", ValuePlaceholder = "", Description = "Start line", Commands = Set("excerpt") }, - new() { Name = "--start-line", ValuePlaceholder = "", Description = "Alias for --start", Commands = Set("excerpt") }, + new() { Name = "--start-line", ValuePlaceholder = "", Description = "Alias for --start; inspect source_excerpt start line", Commands = Set("excerpt", "inspect") }, new() { Name = "--end", ValuePlaceholder = "", Description = "End line", Commands = Set("excerpt") }, - new() { Name = "--end-line", ValuePlaceholder = "", Description = "Alias for --end", Commands = Set("excerpt") }, + new() { Name = "--end-line", ValuePlaceholder = "", Description = "Alias for --end; inspect source_excerpt end line", Commands = Set("excerpt", "inspect") }, new() { Name = "--focus-line", ValuePlaceholder = "", Description = "Focused line to keep visible when clamping", Commands = Set("find", "excerpt") }, new() { Name = "--focus-column", ValuePlaceholder = "", Description = "Focused column to keep visible when clamping", Commands = Set("find", "excerpt") }, new() { Name = "--focus-length", ValuePlaceholder = "", Description = "Focused span width when clamping", Commands = Set("excerpt") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 2702a0903d..706bb17aa9 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -99,7 +99,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("find", "cdidx find (--path |--all) [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--exact] [--regex] [--count]"), ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), ("map", "cdidx map [--db ] [--json] [--format ] [--pretty] [--compact] [--summary-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--sections ] [--depth ] [--min-entrypoint-confidence <0.0..1.0>]"), - ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--format ] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--body-start ] [--body-lines ] [--max-line-width ] [--exact|--exact-name]"), + ("inspect", "cdidx inspect |--query |-- |--path --line [--db ] [--json] [--format ] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--body-start ] [--body-lines |--body-line-count ] [--line |--start-line ] [--end-line ] [--context |--before |--after ] [--max-line-width ] [--exact|--exact-name]"), ("outline", "cdidx outline [--db ] [--json] [--pretty] [--compact] [--verbose] [--limit |--top ]"), ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config] [--check-updates]"), ("workspace", "cdidx workspace [name] [--json]"), @@ -833,29 +833,7 @@ public static void PrintUsageBrief(bool showBanner = true) Console.WriteLine(" cdidx --help-all"); Console.WriteLine(" cdidx --help-flags"); Console.WriteLine(); - Console.WriteLine("Commands:"); - Console.WriteLine(" index Build or update the index for a project"); - Console.WriteLine(" search Full-text search across indexed chunks"); - Console.WriteLine(" definition Resolve symbol definitions with extracted ranges"); - Console.WriteLine(" references Find indexed references for a symbol (--kind uses reference kind)"); - Console.WriteLine(" callers Find callers of a symbol (--kind uses reference kind)"); - Console.WriteLine(" callees Find callees used by a caller (--kind uses reference kind)"); - Console.WriteLine(" symbols [query] Search symbols (functions, classes, imports)"); - Console.WriteLine(" map Show a repo-level overview for AI orientation"); - Console.WriteLine(" inspect Bundle definition, graph, and nearby symbol context"); - Console.WriteLine(" status Show database statistics, freshness, config, and logs"); - Console.WriteLine(" doctor Print a redacted environment summary for bug reports"); - Console.WriteLine(" validate Report encoding issues (U+FFFD origin/severity, BOM, null bytes, mixed line endings, UTF-16 BOM, likely non-UTF8)"); - Console.WriteLine(" impact Show transitive callers; type queries may return heuristic file-level dependency hints"); - Console.WriteLine(" deps Show file-level dependency edges from the reference graph"); - Console.WriteLine(" unused Find symbols defined but never referenced (dead code)"); - Console.WriteLine(" hotspots Find high-impact symbols; duplicate-name families may fall back conservatively"); - Console.WriteLine(" export Export ctags or a portable CodeIndex archive"); - Console.WriteLine(" import Import a portable CodeIndex archive"); - Console.WriteLine(" batch Run newline-delimited JSON query commands with one DB connection"); - Console.WriteLine(" mcp Start MCP server (for AI tools: Claude, Cursor, etc.)"); - Console.WriteLine(" lsp Start LSP server over stdio (for LSP-native editors)"); - Console.WriteLine(" completions Generate shell completions for bash, zsh, fish, or PowerShell"); + PrintCommandSummary(); Console.WriteLine(); Console.WriteLine("Run `cdidx --help-all` for every command and option, `cdidx --help-flags` for shared flags, or `cdidx --help` for one command."); Console.WriteLine(); @@ -929,6 +907,7 @@ private static void PrintCommandSummary() { Console.WriteLine("Commands:"); Console.WriteLine(" index Build or update the index for a project"); + Console.WriteLine(" hooks Install, uninstall, or inspect git hook integration"); Console.WriteLine(" backfill-fold Upgrade folded-name columns in an existing index DB"); Console.WriteLine(" optimize Optimize FTS5 segments in an existing index DB"); Console.WriteLine(" vacuum Reclaim free SQLite pages from an existing index DB"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 58ee4f5789..479b4b9e4d 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -158,12 +158,17 @@ public static partial class QueryCommandRunner "--visibility", "--exclude-visibility", "--since", + "--line", "--start", + "--start-line", "--end", + "--end-line", + "--context", "--before", "--after", "--body-start", "--body-lines", + "--body-line-count", "--name", "--snippet-lines", "--snippet-focus", @@ -5240,6 +5245,9 @@ private static void AddInspectFieldProperties(HashSet keep, string field case "definitions": keep.Add("definitions"); break; + case "source_excerpt": + keep.Add("source_excerpt"); + break; case "nearby_symbols": keep.Add("nearby_symbols"); break; @@ -5363,17 +5371,18 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions Console.Error.WriteLine(exactError); return CommandExitCodes.UsageError; } - if (TryWriteBlankQueryError(options, "inspect")) + var pathLineInspectMode = IsInspectPathLineMode(options); + if (!pathLineInspectMode && TryWriteBlankQueryError(options, "inspect")) return CommandExitCodes.UsageError; - if (string.IsNullOrWhiteSpace(options.Query)) + if (!pathLineInspectMode && string.IsNullOrWhiteSpace(options.Query)) { WriteUsageError( "inspect requires a symbol query argument", GetUsageLineOrThrow("inspect"), - "Add the symbol you want to inspect, for example: `cdidx inspect QueryCommandRunner`."); + "Add the symbol you want to inspect, for example: `cdidx inspect QueryCommandRunner`, or pass `--path --line ` for a source excerpt."); return CommandExitCodes.UsageError; } - if (IsBareVerbatimQueryToken(options.Query)) + if (options.Query != null && IsBareVerbatimQueryToken(options.Query)) { WriteUsageError( "inspect requires a symbol query argument", @@ -5383,13 +5392,24 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions } if (TryWriteUnexpectedExtraPositionals("inspect", options)) return CommandExitCodes.UsageError; + if (options.StartLine.HasValue && options.EndLine.HasValue && options.EndLine.Value < options.StartLine.Value) + { + WriteValidationError( + $"--start-line ({options.StartLine.Value}) must be less than or equal to --end-line ({options.EndLine.Value}).", + "Use `--start-line` less than or equal to `--end-line`, or omit `--end-line` to read one line."); + return CommandExitCodes.UsageError; + } return WithDb(options, jsonOptions, reader => { var compactLimit = GetCompactSectionLimit(options); var inspectLimit = options.Compact ? GetCompactSourceLimit(compactLimit) : options.Limit; + var inspectPath = pathLineInspectMode ? GetSingleSpecificPathPattern(options.PathPatterns) : null; + var inspectQuery = pathLineInspectMode + ? $"{inspectPath}:{options.StartLine!.Value}" + : options.Query!; var analysis = reader.AnalyzeSymbol( - options.Query, + inspectQuery, inspectLimit, options.Lang, options.IncludeBody, @@ -5399,7 +5419,9 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions exact, options.MaxLineWidth, options.BodyStartLine, - options.BodyLines); + options.BodyLines, + kind: options.Kind); + var sourceExcerpt = BuildInspectSourceExcerpt(reader, options, analysis, inspectPath); var sqlGraphSignal = NarrowSqlGraphContractSignal( reader.GetSqlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests), DbReader.IsSqlLanguage(options.Lang) @@ -5430,6 +5452,12 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions AddSqlGraphContractJsonFields(payload, sqlGraphSignal); if (compactTruncation != null) AddCompactJsonFields(payload, compactLimit, compactTruncation); + if (sourceExcerpt != null) + { + ExcerptRecoveryCommandFormatter.ApplyDbPath(sourceExcerpt, options.DbPath); + sourceExcerpt.SemanticTokens = BuildExcerptSemanticTokens(sourceExcerpt); + payload["source_excerpt"] = JsonSerializer.SerializeToNode(sourceExcerpt, CliJsonSerializerContextFactory.Create(jsonOptions).FileExcerptResult); + } ApplyInspectFieldSelection(payload, options, jsonOptions); AddInspectBodyModeJsonFields(payload, options, analysis); Console.WriteLine(payload.ToJsonString(jsonOptions)); @@ -5469,6 +5497,11 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions } WriteExactZeroHint(analysis.ExactZeroHint); WriteInspectBodyModeHint(analysis, options); + if (sourceExcerpt != null) + { + Console.WriteLine($"Source Excerpt : {sourceExcerpt.Path}:{sourceExcerpt.StartLine}-{sourceExcerpt.EndLine}"); + WriteNumberedExcerpt(sourceExcerpt.StartLine, sourceExcerpt.Content); + } WriteRepoMapSection("Definitions", analysis.Definitions.Select(item => $"{item.Kind,-10} {item.Name,-24} {item.Path}:{item.StartLine}-{item.EndLine}")); WriteRepoMapSection("Nearby symbols", analysis.NearbySymbols.Select(item => $"{item.Kind,-10} {item.Name,-24} {item.Path}:{item.StartLine}-{item.EndLine}")); WriteRepoMapSection("References", analysis.References.Select(item => $"{item.Path}:{item.Line}:{item.Column} {item.Context}")); @@ -5476,10 +5509,62 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions WriteRepoMapSection("Callees", analysis.Callees.Select(item => $"{item.CallerName ?? ""} -> {item.CalleeName} ({item.ReferenceCount} refs)")); } - return IsEmptySymbolAnalysis(analysis) ? ZeroResultExitCode(options) : CommandExitCodes.Success; + return IsEmptySymbolAnalysis(analysis) && sourceExcerpt == null ? ZeroResultExitCode(options) : CommandExitCodes.Success; }); } + private static bool IsInspectPathLineMode(QueryCommandOptions options) + => options.Query == null + && options.StartLine.HasValue + && GetSingleSpecificPathPattern(options.PathPatterns) != null; + + private static bool IsInspectSourceExcerptRequested(QueryCommandOptions options) + => options.StartLine.HasValue + || options.EndLine.HasValue + || options.ContextBefore > 0 + || options.ContextAfter > 0; + + private static FileExcerptResult? BuildInspectSourceExcerpt( + DbReader reader, + QueryCommandOptions options, + SymbolAnalysisResult analysis, + string? inspectPath) + { + if (!IsInspectSourceExcerptRequested(options)) + return null; + + var definition = analysis.Definitions.FirstOrDefault(); + var path = inspectPath + ?? GetSingleSpecificPathPattern(options.PathPatterns) + ?? definition?.Path + ?? analysis.File?.Path; + if (path == null) + return null; + + var startLine = options.StartLine ?? definition?.StartLine ?? 1; + var endLine = options.EndLine ?? options.StartLine ?? definition?.EndLine ?? startLine; + return reader.GetExcerpt( + path, + startLine, + endLine, + options.ContextBefore, + options.ContextAfter, + options.MaxLineWidth, + options.StartLine ?? startLine); + } + + private static string? GetSingleSpecificPathPattern(IReadOnlyList pathPatterns) + { + if (pathPatterns.Count != 1) + return null; + + var path = pathPatterns[0]; + return ContainsGlobWildcard(path) ? null : path; + } + + private static bool ContainsGlobWildcard(string value) + => value.IndexOfAny(['*', '?', '[', ']']) >= 0; + public static int RunOutline(string[] cmdArgs, JsonSerializerOptions jsonOptions) { if (cmdArgs.Length == 0 || cmdArgs[0].StartsWith('-')) @@ -8915,9 +9000,11 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) AddParseError(bodyStartError!); break; case "--body-lines": - if (!TryReadRawOptionValue(args, ref i, "--body-lines", inlineValue, out var bodyLinesValue, out var missingBodyLinesError)) + case "--body-line-count": + var bodyLinesFlag = normalizedArg; + if (!TryReadRawOptionValue(args, ref i, bodyLinesFlag, inlineValue, out var bodyLinesValue, out var missingBodyLinesError)) AddParseError(missingBodyLinesError!); - else if (TryParsePositiveInt(bodyLinesValue!, "--body-lines", out var parsedBodyLines, out var bodyLinesError)) + else if (TryParsePositiveInt(bodyLinesValue!, bodyLinesFlag, out var parsedBodyLines, out var bodyLinesError)) { WarnIfDuplicateSingleValueOption("--body-lines", bodyLinesValue!); bodyLines = parsedBodyLines; @@ -9259,6 +9346,19 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) else AddParseError($"Error: could not parse --since value '{ConsoleUi.FormatBoundedValue(sinceValue)}' as a date/time. Use ISO 8601 format (e.g. 2024-01-01 or 2024-01-01T00:00:00Z)."); break; + case "--line": + if (!TryReadRawOptionValue(args, ref i, "--line", inlineValue, out var lineValue, out var missingLineError)) + AddParseError(missingLineError!); + else if (TryParsePositiveInt(lineValue!, "--line", out var parsedLine, out var lineError)) + { + WarnIfDuplicateSingleValueOption("--start", lineValue!); + WarnIfDuplicateSingleValueOption("--end", lineValue!); + startLine = parsedLine; + endLine = parsedLine; + } + else + AddParseError(lineError!); + break; case "--start": case "--start-line": var startFlag = normalizedArg; @@ -9285,6 +9385,20 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) else AddParseError(endError!); break; + case "--context": + if (!TryReadRawOptionValue(args, ref i, "--context", inlineValue, out var contextValue, out var missingContextError)) + AddParseError(missingContextError!); + else if (TryParseNonNegativeInt(contextValue!, "--context", out var parsedContext, out var contextError)) + { + WarnIfDuplicateSingleValueOption("--before", contextValue!); + WarnIfDuplicateSingleValueOption("--after", contextValue!); + contextBefore = parsedContext; + contextAfter = parsedContext; + contextAfterExplicit = true; + } + else + AddParseError(contextError!); + break; case "--before": if (!TryReadRawOptionValue(args, ref i, "--before", inlineValue, out var beforeValue, out var missingBeforeError)) AddParseError(missingBeforeError!); @@ -9690,6 +9804,11 @@ private static List ParseMapSections(string rawValue, Action add canonical = "definitions"; includeBody = true; break; + case "source": + case "source_excerpt": + case "excerpt": + canonical = "source_excerpt"; + break; case "nearby": case "nearby_symbols": case "nearbysymbols": @@ -9709,7 +9828,7 @@ private static List ParseMapSections(string rawValue, Action add canonical = "callees"; break; default: - addParseError($"Error: unsupported --fields value '{ConsoleUi.FormatBoundedValue(rawField)}'. Use one or more of all, file, workspace, graph, definitions, body, nearby_symbols, references, callers, callees."); + addParseError($"Error: unsupported --fields value '{ConsoleUi.FormatBoundedValue(rawField)}'. Use one or more of all, file, workspace, graph, definitions, body, source_excerpt, nearby_symbols, references, callers, callees."); continue; } @@ -12765,6 +12884,7 @@ private static void WriteSqlGraphContractWarningIfNeeded(bool json, SqlGraphCont ["--slow-query-ms"] = 3_600_000, ["--body-start"] = 10_000_000, ["--body-lines"] = DbReader.DefinitionBodyMaxRequestedLines, + ["--body-line-count"] = DbReader.DefinitionBodyMaxRequestedLines, ["--max-hops"] = 64, ["--depth"] = 64, ["--before"] = 1_000, @@ -12791,6 +12911,7 @@ private static void WriteSqlGraphContractWarningIfNeeded(bool json, SqlGraphCont ["--top"] = "pass a positive integer, e.g. `--top 20` (alias for `--limit`, default 20).", ["--body-start"] = "pass a 1-based source line inside the symbol body, e.g. `--body-start 120`.", ["--body-lines"] = "pass a positive line count for the body slice, e.g. `--body-lines 40`.", + ["--body-line-count"] = "pass a positive line count for the body slice, e.g. `--body-line-count 40` (alias for `--body-lines`).", ["--lang"] = "pass a language identifier, e.g. `--lang csharp`. Run `cdidx languages` for the supported set.", ["--query"] = "pass a search literal, e.g. `--query \"authenticate\"`. Use the `--query` form when the literal starts with `-`.", ["--recipe"] = "pass a built-in audit recipe name, e.g. `--recipe risky-code`, or a child query selector such as `--recipe risky-code/raw-diagnostic-echo`; run `cdidx search --list-recipes` to list available recipes.", diff --git a/src/CodeIndex/Database/DbSymbolReader.cs b/src/CodeIndex/Database/DbSymbolReader.cs index 8df7027027..d3852b09f9 100644 --- a/src/CodeIndex/Database/DbSymbolReader.cs +++ b/src/CodeIndex/Database/DbSymbolReader.cs @@ -1537,7 +1537,7 @@ FROM symbols s /// Bundle definition, graph, and local file context for one symbol query. /// 単一シンボルクエリ向けに、定義・グラフ・ローカル文脈をまとめて返す。 /// - public SymbolAnalysisResult AnalyzeSymbol(string query, int limit = 10, string? lang = null, bool includeBody = false, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, int maxLineWidth = LineWidthFormatter.DefaultMaxLineWidth, int? bodyStartLine = null, int? bodyLineCount = null) + public SymbolAnalysisResult AnalyzeSymbol(string query, int limit = 10, string? lang = null, bool includeBody = false, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, int maxLineWidth = LineWidthFormatter.DefaultMaxLineWidth, int? bodyStartLine = null, int? bodyLineCount = null, string? kind = null) { if (string.IsNullOrWhiteSpace(query) || IsBareVerbatimQueryToken(query)) { @@ -1569,7 +1569,7 @@ public SymbolAnalysisResult AnalyzeSymbol(string query, int limit = 10, string? // が同じ WAL snapshot を参照するようにする。 using var txn = _conn.BeginTransaction(deferred: true); var definitionLimit = Math.Min(limit, 5); - var definitions = GetDefinitions(normalizedQuery, definitionLimit, kind: null, lang, includeBody, pathPatterns, excludePathPatterns, excludeTests, since: null, exact, bodyStartLine: bodyStartLine, bodyLineCount: bodyLineCount); + var definitions = PrioritizeSourceDefinitions(GetDefinitions(normalizedQuery, definitionLimit, kind: kind, lang, includeBody, pathPatterns, excludePathPatterns, excludeTests, since: null, exact, bodyStartLine: bodyStartLine, bodyLineCount: bodyLineCount)); DefinitionResult? primaryDefinition = definitions .FirstOrDefault(definition => ReferenceExtractor.SupportsLanguage(definition.Lang) == true && !IsCSharpEnumMemberDefinition(definition)) ?? definitions.FirstOrDefault(definition => ReferenceExtractor.SupportsLanguage(definition.Lang) == true) @@ -1653,6 +1653,19 @@ public SymbolAnalysisResult AnalyzeSymbol(string query, int limit = 10, string? return result; } + private static List PrioritizeSourceDefinitions(List definitions) + { + if (definitions.Count <= 1) + return definitions; + + return definitions + .Select((definition, index) => (definition, index)) + .OrderBy(item => SearchMatchClassifier.IsLikelyTestPath(item.definition.Path) ? 1 : 0) + .ThenBy(item => item.index) + .Select(item => item.definition) + .ToList(); + } + public HashSet GetUnsupportedExactGraphSymbolKinds( string query, string? lang, diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 9bd01e4776..3409a6d71b 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -96,6 +96,21 @@ public void PrintUsage_WithBanner_IncludesAsciiArt() Assert.Contains("cdidx index --commits [commit-ref ...]", output); } + [Fact] + public void PrintUsageBrief_ListsImplementedCommands_Issue3663() + { + var output = CaptureBriefUsageOutput(showBanner: false); + + foreach (var command in CliFlagSchema.AllCommands) + Assert.Contains($" {command}", output); + + Assert.Contains(" hooks", output); + Assert.Contains(" files [query]", output); + Assert.Contains(" find ", output); + Assert.Contains(" excerpt ", output); + Assert.Contains(" languages", output); + } + [Fact] public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() { @@ -118,7 +133,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes|--named-query = [--named-query = ...] [--include-query ] [--exclude-query ] [--cursor ] [--audit-scope ] [--show-excluded] [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--repo ] [--duplicate-confidence |--duplicate-threshold ] [--issue-title ] [--issue-label <label>] [--verbose] [--limit <n>|--top <n>|--max-results <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines <n>] [--snippet-focus <leftmost|quality|proximity>] [--max-line-width <n>] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by <file|symbol>] [--since <datetime>] [--no-dedup] [--no-visibility-rank] [--require-before <query>] [--require-after <query>] [--reject-before <query>] [--reject-after <query>] [--guard-window <n>]", output); Assert.Contains("cdidx definition <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since <datetime>]", output); Assert.Contains("cdidx references <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--snippet-lines <n>] [--max-line-width <n>] [--exact|--exact-name] [--count]", output); - Assert.Contains("cdidx inspect <query>|--query <query>|-- <query> [--db <path>] [--json] [--format <text|json|compact>] [--pretty] [--compact] [--fields <csv>] [--body-only] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--body-start <line>] [--body-lines <n>] [--max-line-width <n>] [--exact|--exact-name]", output); + Assert.Contains("cdidx inspect <query>|--query <query>|-- <query>|--path <file> --line <line> [--db <path>] [--json] [--format <text|json|compact>] [--pretty] [--compact] [--fields <csv>] [--body-only] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--kind <kind>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--body-start <line>] [--body-lines <n>|--body-line-count <n>] [--line <line>|--start-line <line>] [--end-line <line>] [--context <n>|--before <n>|--after <n>] [--max-line-width <n>] [--exact|--exact-name]", output); Assert.Contains("--snippet-lines <n> search/find snippet length (1-20, default: search 8; find 1)", output); Assert.Contains("--snippet-focus <mode> search only: long-line focus mode (leftmost|quality|proximity, default: quality)", output); Assert.Contains("--max-line-width <n> search/references/callers/callees/find/excerpt/impact/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: 512)", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs index b73acf50a3..dcea9d4d0f 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -228,6 +228,193 @@ public void RunInspect_ParseBodyRange_ImplyBodyAndValidateValues_Issue3394() Assert.Null(options.ParseError); } + [Fact] + public void RunInspect_ParseBodyLineCountAlias_MapsToBodyLines_Issue3665() + { + var options = QueryCommandRunner.ParseArgs( + ["--body-line-count=3"], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.True(options.IncludeBody); + Assert.Equal(3, options.BodyLines); + Assert.Null(options.ParseError); + } + + [Fact] + public void RunInspect_KindFilterJson_FiltersDefinitions_Issue3666() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_kind_filter_3666"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/target.py", + "python", + """ + class Target: + pass + + def Target(): + return 1 + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Target", "--db", dbPath, "--json", "--lang", "python", "--kind", "class", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var definitions = document.RootElement.GetProperty("definitions").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(definitions); + Assert.Equal("class", definitions[0].GetProperty("kind").GetString()); + Assert.Equal("Target", definitions[0].GetProperty("name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_JsonPrioritizesSourceDefinitionsOverTests_Issue3666() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_source_priority_3666"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "tests/RunImportTests.cs", + "csharp", + """ + public class RunImportTests + { + public void RunImport() { } + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/RunImport.cs", + "csharp", + """ + public class ImportService + { + public void RunImport() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["RunImport", "--db", dbPath, "--json", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var definitions = document.RootElement.GetProperty("definitions").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(definitions.Count >= 2); + Assert.Equal("src/RunImport.cs", definitions[0].GetProperty("path").GetString()); + Assert.Contains(definitions, definition => definition.GetProperty("path").GetString() == "tests/RunImportTests.cs"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_LineRangeJson_IncludesSourceExcerpt_Issue3786() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_source_excerpt_3786"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Target.cs", + "csharp", + """ + public class Target + { + public int Compute() + { + return 42; + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Compute", "--db", dbPath, "--json", "--line", "5", "--context", "1", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var sourceExcerpt = document.RootElement.GetProperty("source_excerpt"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/Target.cs", sourceExcerpt.GetProperty("path").GetString()); + Assert.Equal(4, sourceExcerpt.GetProperty("start_line").GetInt32()); + Assert.Equal(6, sourceExcerpt.GetProperty("end_line").GetInt32()); + Assert.Equal(4, sourceExcerpt.GetProperty("requested_start_line").GetInt32()); + Assert.Equal(6, sourceExcerpt.GetProperty("requested_end_line").GetInt32()); + Assert.Contains("return 42;", sourceExcerpt.GetProperty("content").GetString(), StringComparison.Ordinal); + Assert.False(sourceExcerpt.GetProperty("content_truncated").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_PathLineJson_WorksWithoutSymbolQuery_Issue3786() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_path_line_3786"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Target.cs", + "csharp", + """ + public class Target + { + public int Compute() + { + return 42; + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["--db", dbPath, "--json", "--path", "src/Target.cs", "--line", "5"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var root = document.RootElement; + var sourceExcerpt = root.GetProperty("source_excerpt"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/Target.cs:5", root.GetProperty("query").GetString()); + Assert.Empty(root.GetProperty("definitions").EnumerateArray()); + Assert.Equal("src/Target.cs", sourceExcerpt.GetProperty("path").GetString()); + Assert.Equal(5, sourceExcerpt.GetProperty("start_line").GetInt32()); + Assert.Equal(5, sourceExcerpt.GetProperty("end_line").GetInt32()); + Assert.Contains("return 42;", sourceExcerpt.GetProperty("content").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunInspect_FormatCompact_ActsLikeCompactJson_Issue3446() {