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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
Argument-validation variants that only differ by invalid scalar input share one database fixture and iterate within a fact when no per-case state or discovery identity is required.
Positional `files` glob coverage shares one indexed-file fixture and iterates `*`, `?`, and recursive `**` patterns in a fact, matching the exact tokens that a quoted shell argument passes to the CLI.
Excerpt focus coverage reuses one indexed fixture for line-only leading-window behavior, the focus-length dependency, and focus-column range validation; zero and non-numeric focus-column values share one indexed Markdown fixture.
Excerpt range coverage reuses one indexed fixture for requested/effective context bounds, explicit EOF and clamp modes, Unicode content, and terminal-newline variants; strict numeric recovery, empty files, and inline zero/negative coordinates share one invalid-range fixture.
Inspect path-line exact/enclosing-symbol cases reuse one indexed source fixture and iterate read-only line queries within a fact.
Definition and symbols exact-mode conflict validation share one empty database and a cross-command flag-pair table.
Symbols compact flag/alias and summary-only JSON envelopes share one editor-format fixture.
Expand Down Expand Up @@ -1125,6 +1126,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
invalid scalar input だけが異なる argument-validation variant は、case ごとの state や discovery identity が不要なら1つの database fixture を共有し、fact 内で反復してください。
`files` の positional glob coverage は1つの indexed-file fixture を共有し、quote された shell 引数が CLI に渡す token と同じ `*`、`?`、recursive `**` pattern を fact 内で反復してください。
excerpt の focus coverage は、line-only 時の先頭側 window、focus-length の依存関係、focus-column の範囲検証を1つの indexed fixture で共有してください。focus-column の zero / non-numeric value も1つの indexed Markdown fixture を再利用してください。
excerpt の range coverage は、requested / effective context 境界、明示的な EOF / clamp mode、Unicode content、末尾改行の有無を1つの indexed fixture で共有してください。strict numeric recovery、空ファイル、inline の zero / negative 座標は1つの invalid-range fixture を共有してください。
inspect path-line の exact/enclosing-symbol case は1つの indexed source fixture を再利用し、read-only line query を fact 内で反復してください。
definition と symbols の exact-mode conflict validation は1つの空databaseとcross-command flag-pair tableを共有してください。
symbols compact flag/aliasとsummary-only JSON envelopeは1つのeditor-format fixtureを共有してください。
Expand Down
12 changes: 12 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1763,8 +1763,14 @@ For large files, `outline --json` supports `--kind <kind[,kind]>`, `--sort <sour
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 28
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 28 --before 3 --after 3 --json
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --line 24 --context 3 --json --no-semantic-tokens
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end eof
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 999999 --clamp --json
```

Excerpt coordinates are 1-based, and zero or negative coordinates return `E020_LINE_OUT_OF_RANGE`. Numeric `--end` values remain strict: overshooting the indexed file returns a range error with `range_recovery` guidance. Use `--end eof` to explicitly read through the indexed end of file, or add `--clamp` to explicitly clamp numeric overshoot to file boundaries.

JSON keeps the original request in `requested_start_line` / `requested_end_line` and the returned window in `effective_start_line` / `effective_end_line`, with `total_lines` reporting the indexed file length. Context expands only the effective window: for example, `--start 18 --end 22 --before 2 --after 2` reports requested lines 18–22 and effective lines 16–24. `requested_end_mode` distinguishes `numeric` from `eof`, and `range_clamped` reports whether explicit clamping changed the returned bounds.

### Find a substring inside a known file

```bash
Expand Down Expand Up @@ -4986,8 +4992,14 @@ cdidx outline src/CodeIndex/Cli/QueryCommandRunner.cs --compact --kind function
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 28
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 28 --before 3 --after 3 --json
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --line 24 --context 3 --json --no-semantic-tokens
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end eof
cdidx excerpt src/CodeIndex/Cli/GitHelper.cs --start 19 --end 999999 --clamp --json
```

excerpt の座標は 1-based で、0 以下の座標は `E020_LINE_OUT_OF_RANGE` を返します。数値の `--end` は従来どおり strict で、インデックス済みファイルの終端を超えると `range_recovery` guidance 付きの range error になります。インデックス済み EOF まで明示的に読むには `--end eof`、数値の超過範囲をファイル境界へ明示的に丸めるには `--clamp` を使います。

JSON は元の指定を `requested_start_line` / `requested_end_line`、実際に返した window を `effective_start_line` / `effective_end_line` に分け、`total_lines` でインデックス済みファイルの総行数を返します。context は effective window だけを拡張します。たとえば `--start 18 --end 22 --before 2 --after 2` は requested 18–22、effective 16–24 を返します。`requested_end_mode` は `numeric` と `eof` を区別し、`range_clamped` は明示的な clamp により返却境界が変わったかを示します。

### 既知ファイル内の部分文字列を探す

```bash
Expand Down
21 changes: 21 additions & 0 deletions changelog.d/unreleased/4877.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
category: fixed
issues:
- 4877
affected:
- src/CodeIndex/Cli/QueryCommandRunner.Excerpt.cs
- src/CodeIndex/Database/DbReader.FilesStatus.cs
- src/CodeIndex/Models/QueryResults.cs
- src/CodeIndex/Mcp/McpToolCatalog.cs
- USER_GUIDE.md
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
- tests/CodeIndex.Tests/McpServerToolsCallTests.cs
---

## English

- **Excerpt ranges now preserve requested and effective bounds separately (#4877)** — context expansion no longer overwrites the caller's requested range, CLI coordinates consistently reject zero and negative values, JSON and MCP responses report the indexed total line count, and explicit `--end eof` / `--clamp` modes recover safely from unknown or oversized end lines while numeric ranges remain strict by default.

## 日本語

- **excerpt range が requested 境界と effective 境界を分けて保持するようになりました (#4877)** — context 展開が呼び出し元の requested range を上書きしなくなり、CLI 座標は zero / negative value を一貫して拒否します。JSON / MCP response はインデックス済み総行数を返し、数値範囲の既定の strict 動作を維持しつつ、未知または過大な終端行には明示的な `--end eof` / `--clamp` mode で安全に対応できます。
13 changes: 11 additions & 2 deletions src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ internal sealed record CliOptionValueDomain
public IReadOnlyDictionary<string, string> Aliases { get; init; } =
new Dictionary<string, string>(StringComparer.Ordinal);
public bool NormalizeDashAndUnderscore { get; init; }
public string? DisplayPlaceholder { get; init; }

public string ValuePlaceholder => $"<{string.Join('|', CanonicalValues)}>";
public string ValuePlaceholder =>
DisplayPlaceholder ?? $"<{string.Join('|', CanonicalValues)}>";

public bool TryNormalize(string rawValue, out string normalizedValue)
{
Expand Down Expand Up @@ -366,6 +368,12 @@ public static bool HasAuthoritativeHelpOptions(string command) =>
("help", "help_text"),
("schema", "schema_description"));

private static readonly CliOptionValueDomain ExcerptEndValueDomain = new()
{
CanonicalValues = ["eof"],
DisplayPlaceholder = "<line|eof>",
};

private static readonly IReadOnlyDictionary<string, CliOptionValueDomain> OutputFormatValueDomains =
new Dictionary<string, CliOptionValueDomain>(StringComparer.Ordinal)
{
Expand Down Expand Up @@ -574,8 +582,9 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--after", ValuePlaceholder = "<n>", Description = "Context lines after", PrimaryCommands = Set("find", "excerpt", "inspect") },
new() { Name = "--start", ValuePlaceholder = "<line>", Description = "Start line", PrimaryCommands = Set("excerpt") },
new() { Name = "--start-line", ValuePlaceholder = "<line>", Description = "Alias for --start; inspect source_excerpt start line", PrimaryCommands = Set("excerpt", "inspect") },
new() { Name = "--end", ValuePlaceholder = "<line>", Description = "End line", PrimaryCommands = Set("excerpt") },
new() { Name = "--end", ValueDomain = ExcerptEndValueDomain, Description = "Excerpt end line; eof reads through the indexed end of file", PrimaryCommands = Set("excerpt") },
new() { Name = "--end-line", ValuePlaceholder = "<line>", Description = "Alias for --end; inspect source_excerpt end line", PrimaryCommands = Set("excerpt", "inspect") },
new() { Name = "--clamp", Description = "Excerpt: explicitly clamp numeric range overshoot to file boundaries", PrimaryCommands = Set("excerpt") },
new() { Name = "--focus-line", ValuePlaceholder = "<line>", Description = "Focused line to keep visible when clamping", PrimaryCommands = Set("find", "excerpt") },
new() { Name = "--focus-column", ValuePlaceholder = "<n>", Description = "Focused column to keep visible when clamping", PrimaryCommands = Set("find", "excerpt") },
new() { Name = "--focus-length", ValuePlaceholder = "<n>", Description = "Focused span width when clamping", PrimaryCommands = Set("excerpt") },
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("symbols", "cdidx symbols [query|--query <query>|-- <query>] [--name <name>] [--db <path>] [--json[=ndjson|array]] [--compact] [--format <text|json|count|compact|lsp|qf|sarif>] [--summary-only] [--cursor <next_cursor>] [--max-json-bytes <n>] [--allow-partial] [--verbose] [--limit <n>|--top <n>] [--sort <hotspot|references|size|complexity|path>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exact|--exact-name] [--count] [--group-partials] [--since <datetime>]"),
("files", "cdidx files [query|<glob>|--query <query>|-- <query>] [--db <path>] [--json[=ndjson|array]] [--format <text|json|count|compact>] [--summary-only] [--cursor <next_cursor>] [--max-json-bytes <n>] [--allow-partial] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--since <datetime>] [--bytes]"),
("find", "cdidx find <query> (--path <glob>|--all) [--db <path>] [--json] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--fields <csv>] [--cursor <next_cursor>] [--max-json-bytes <n>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--exclude-path <glob>] [--exclude-tests] [--context <n>] [--before <n>] [--after <n>] [--snippet-lines <n>] [--focus-line <line>] [--focus-column <n>] [--max-line-width <n>] [--line-scan-limit <n>] [--allow-partial] [--exact] [--regex] [--count]"),
("excerpt", "cdidx excerpt <path[:line|:start-end]> [--line <line>|--start <line>|--start-line <line>] [--end <line>|--end-line <line>] [--context <n>|--before <n>|--after <n>] [--max-line-width <n>] [--focus-line <line>] [--focus-column <n>] [--focus-length <n>] [--db <path>] [--json] [--redact-paths|--show-paths] [--no-semantic-tokens] [--max-json-bytes <n>] [--verbose]"),
("excerpt", "cdidx excerpt <path[:line|:start-end]> [--line <line>|--start <line>|--start-line <line>] [--end <line|eof>|--end-line <line>] [--clamp] [--context <n>|--before <n>|--after <n>] [--max-line-width <n>] [--focus-line <line>] [--focus-column <n>] [--focus-length <n>] [--db <path>] [--json] [--redact-paths|--show-paths] [--no-semantic-tokens] [--max-json-bytes <n>] [--verbose]"),
("map", "cdidx map [--db <path>] [--json] [--format <text|json|compact|issue-drafts>] [--pretty] [--compact] [--fields <csv>] [--cursor <next_cursor>] [--summary-only] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--bytes] [--sections <summary,tree,languages,hotspots,metrics|list>] [--depth <n>] [--min-entrypoint-confidence <0.0..1.0>] [--max-json-bytes <n>]"),
("inspect", "cdidx inspect <query>|--query <query>|-- <query> [--db <path>] [--json] [--redact-paths|--show-paths] [--format <text|json|compact>] [--pretty] [--compact] [--fields <csv>] [--outline-only] [--body-only] [--cursor <next_cursor>] [--max-json-bytes <n>] [--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>] [--context <n>|--before <n>|--after <n>] [--max-line-width <n>] [--exact|--exact-name] [--group-partials]"),
("inspect", "cdidx inspect --path <file> --line <line> [--end-line <line>] [--db <path>] [--json] [--redact-paths|--show-paths] [--format <text|json|compact>] [--pretty] [--compact] [--fields <csv>] [--outline-only] [--body-only] [--cursor <next_cursor>] [--max-json-bytes <n>] [--body] [--body-start <line>] [--body-lines <n>|--body-line-count <n>] [--context <n>|--before <n>|--after <n>] [--max-line-width <n>]"),
Expand Down
Loading
Loading