Skip to content

Commit 3923ab9

Browse files
authored
Merge pull request #2699 from Widthdom/codex/fix-issue1775-1773-1746
Add snippet and excerpt range metadata
2 parents cc8d10a + 9093a28 commit 3923ab9

12 files changed

Lines changed: 372 additions & 3 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,9 @@ Exact-match flag compatibility is documented in [USER_GUIDE.md](USER_GUIDE.md#fl
806806

807807
`search`, `definition`, `references`, `callers`, `callees`, `symbols`, and `files` also share path-aware narrowing via `--path`, repeatable `--exclude-path`, and `--exclude-tests`. The read layer ranks source files ahead of tests and docs, and `search` further boosts exact symbol-name and path matches so AI clients are more likely to land on implementation files first.
808808

809-
`search --json` and MCP `search` project full chunks into compact match-centered snippets with `chunk_start_line`, `chunk_end_line`, `snippet_start_line`, `snippet_end_line`, `snippet`, `match_lines`, `highlights`, `context_before`, `context_after`, `truncated_line_count`, and `truncation_context`. `--snippet-lines` caps the snippet length up front (default: 8, max: 20), and `--max-line-width` (CLI) / `maxLineWidth` (MCP) clamps each individual snippet line around the first match token via the shared `LineWidthFormatter.ClampLine` contract used by `find` / `references` / `excerpt` / `inspect` (default: 512, max: 4096) so a single match inside a minified / transpiled / generated single-line file no longer returns hundreds of KB per hit. Clamped lines surface `...(+N)...` markers inside the snippet and expose `truncation_context.char_counts`, `truncation_context.total_chars`, `highlights[].truncated`, `highlights[].original_line_length`, and `highlights[].truncated_char_counts` so AI clients can detect clamping and quantify omitted characters.
809+
`search --json` and MCP `search` project full chunks into compact match-centered snippets with `chunk_start_line`, `chunk_end_line`, `snippet_start_line`, `snippet_end_line`, `snippet`, `match_lines`, `highlights`, `context_before`, `context_after`, `truncated_line_count`, `dropped_match_line_count`, and `truncation_context`. `--snippet-lines` caps the snippet length up front (default: 8, max: 20), and `--max-line-width` (CLI) / `maxLineWidth` (MCP) clamps each individual snippet line around the first match token via the shared `LineWidthFormatter.ClampLine` contract used by `find` / `references` / `excerpt` / `inspect` (default: 512, max: 4096) so a single match inside a minified / transpiled / generated single-line file no longer returns hundreds of KB per hit. Clamped lines surface `...(+N)...` markers inside the snippet and expose `truncation_context.char_counts`, `truncation_context.total_chars`, `highlights[].truncated`, `highlights[].original_line_length`, and `highlights[].truncated_char_counts` so AI clients can detect clamping and quantify omitted characters. `highlights[].terms` remains a distinct term list for compatibility; `highlights[].term_occurrences` records every matched occurrence with `term`, 1-based `line`, 1-based `column`, and `length`. `dropped_match_line_count` reports match lines omitted because they fell outside the selected snippet window.
810+
811+
`excerpt --json` includes `semantic_tokens`, a lightweight range list with 1-based start/end positions, token `type`, and `modifiers`, so IDE and LLM clients can render or post-process excerpt spans without reparsing the raw `content` string.
810812

811813
`inspect` and MCP `analyze_symbol` bundle the primary definition, nearby symbols from the same file, references, callers, callees, file metadata, workspace freshness/git metadata, and graph-support metadata into one response. When those bundled graph sections actually depend on SQL-backed reads, the payload also mirrors `sql_graph_contract_ready` / `sql_graph_contract_degraded_reason` (plus the existing camelCase aliases on MCP responses); mixed-language bundles that only return C# / JS / etc. graph rows omit the SQL trust signal entirely. This is intended for symbol-oriented AI workflows that would otherwise need several back-to-back calls. Call graph sections remain language-aware: for unsupported languages, clients can now distinguish "unsupported" from "no hits" via `graphSupported` / `graphSupportReason`, and should prefer `search` instead of assuming graph data will exist.
812814

@@ -2376,7 +2378,9 @@ exact-match flag の互換性は [USER_GUIDE.md](USER_GUIDE.md#フラグ互換
23762378

23772379
`search``definition``references``callers``callees``symbols``files``--path`、繰り返し指定できる `--exclude-path``--exclude-tests` による絞り込みを共有します。読み取り層は tests や docs より source を優先し、`search` はシンボル名やパスがクエリと正確に一致する候補をさらに上位に出して、AIクライアントが実装ファイルへ早く到達できるようにします。
23782380

2379-
`search --json` と MCP の `search` は、フルチャンクを `chunk_start_line`、`chunk_end_line`、`snippet_start_line`、`snippet_end_line`、`snippet`、`match_lines`、`highlights`、`context_before`、`context_after`、`truncated_line_count`、`truncation_context` を持つ軽量スニペットへ投影します。`--snippet-lines` で抜粋長を先に制限でき(デフォルト: 8、最大: 20)、`--max-line-width`(CLI)/ `maxLineWidth`(MCP)は `find` / `references` / `excerpt` / `inspect` と同じ共有 `LineWidthFormatter.ClampLine` 契約(デフォルト: 512、最大: 4096、`0` で切り詰め解除)で各スニペット行を最初のマッチトークン周辺にクランプするため、minified / transpiled / 生成された 1 行ファイル内の 1 ヒットで数百 KB を返さなくなります。クランプされた行はスニペットに `...(+N)...` マーカーが入り、`truncation_context.char_counts`、`truncation_context.total_chars`、`highlights[].truncated`、`highlights[].original_line_length`、`highlights[].truncated_char_counts` で AI クライアントがクランプの有無と省略文字数を検出できます。
2381+
`search --json` と MCP の `search` は、フルチャンクを `chunk_start_line`、`chunk_end_line`、`snippet_start_line`、`snippet_end_line`、`snippet`、`match_lines`、`highlights`、`context_before`、`context_after`、`truncated_line_count`、`dropped_match_line_count`、`truncation_context` を持つ軽量スニペットへ投影します。`--snippet-lines` で抜粋長を先に制限でき(デフォルト: 8、最大: 20)、`--max-line-width`(CLI)/ `maxLineWidth`(MCP)は `find` / `references` / `excerpt` / `inspect` と同じ共有 `LineWidthFormatter.ClampLine` 契約(デフォルト: 512、最大: 4096、`0` で切り詰め解除)で各スニペット行を最初のマッチトークン周辺にクランプするため、minified / transpiled / 生成された 1 行ファイル内の 1 ヒットで数百 KB を返さなくなります。クランプされた行はスニペットに `...(+N)...` マーカーが入り、`truncation_context.char_counts`、`truncation_context.total_chars`、`highlights[].truncated`、`highlights[].original_line_length`、`highlights[].truncated_char_counts` で AI クライアントがクランプの有無と省略文字数を検出できます。`highlights[].terms` は互換性のため distinct な term list のまま残し、`highlights[].term_occurrences` は一致ごとの `term`、1-based の `line` / `column`、`length` を記録します。`dropped_match_line_count` は選択された snippet window 外に落ちた一致行数を示します。
2382+
2383+
`excerpt --json` は 1-based の開始/終了位置、token `type``modifiers` を持つ軽量 range list の `semantic_tokens` を返すため、IDE や LLM クライアントは生の `content` 文字列を再パースせずに抜粋範囲を描画・後処理できます。
23802384

23812385
`inspect` と MCP の `analyze_symbol` は、主定義、同一ファイル内の近傍シンボル、参照、caller、callee、ファイルメタデータ、さらにワークスペース鮮度/git メタデータと graph 対応メタデータを1レスポンスにまとめます。bundle 内の graph 節が実際に SQL ベースの read に依存する場合だけ、`sql_graph_contract_ready` / `sql_graph_contract_degraded_reason`(MCP では既存の camelCase alias も)も返します。mixed-language bundle で C# / JS などの graph row しか返っていない場合は SQL trust signal を出さないため、無関係なクエリが stale SQL state に引きずられません。複数の連続クエリを避けたい AI ワークフロー向けです。call graph 系の節は言語差分を考慮しており、未対応言語では `graphSupported` / `graphSupportReason` によって「未対応」と「ヒットなし」を区別できます。その場合は `search` を優先して使う前提です。
23822386

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
category: added
3+
issues:
4+
- 1746
5+
affected:
6+
- src/CodeIndex/Cli/QueryCommandRunner.cs
7+
- src/CodeIndex/Models/QueryResults.cs
8+
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
9+
---
10+
11+
## English
12+
13+
- **Excerpt JSON now includes semantic token ranges (#1746)**`excerpt --json` emits `semantic_tokens` with 1-based ranges and token types so IDE and LLM clients can render excerpt spans without reparsing `content`.
14+
15+
## 日本語
16+
17+
- **excerpt JSON が semantic token range を含むようになりました (#1746)**`excerpt --json` は 1-based range と token type を持つ `semantic_tokens` を返し、IDE や LLM クライアントが `content` を再パースせずに抜粋範囲を描画できるようにします。
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1773
5+
affected:
6+
- src/CodeIndex/Cli/SearchSnippetFormatter.cs
7+
- tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs
8+
---
9+
10+
## English
11+
12+
- **Search highlights now include per-occurrence positions (#1773)**`highlights[].term_occurrences` records every matched term with its line, column, and length while preserving the existing distinct `terms` list.
13+
14+
## 日本語
15+
16+
- **検索 highlight が一致ごとの位置を含むようになりました (#1773)** — 既存の distinct な `terms` list は維持しつつ、`highlights[].term_occurrences` に各一致の line、column、length を記録します。
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1775
5+
affected:
6+
- src/CodeIndex/Cli/SearchSnippetFormatter.cs
7+
- tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs
8+
---
9+
10+
## English
11+
12+
- **Search snippets now report omitted match lines (#1775)**`search --json` exposes `dropped_match_line_count` when additional matching lines fall outside the selected snippet window.
13+
14+
## 日本語
15+
16+
- **検索スニペットが省略された一致行数を返すようになりました (#1775)**`search --json` は、選択された snippet window 外に追加の一致行がある場合に `dropped_match_line_count` を返します。

src/CodeIndex/Cli/JsonOutputContracts.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ internal sealed record VersionInfoJsonResult(
282282
[JsonSerializable(typeof(DiffSummaryOnlyJsonResult))]
283283
[JsonSerializable(typeof(DiffSummaryJsonResult))]
284284
[JsonSerializable(typeof(ExactZeroHintResult))]
285+
[JsonSerializable(typeof(ExcerptSemanticToken))]
285286
[JsonSerializable(typeof(FileDependencyResult))]
286287
[JsonSerializable(typeof(FileExcerptResult))]
287288
[JsonSerializable(typeof(FileFindResult))]
@@ -337,6 +338,7 @@ internal sealed record VersionInfoJsonResult(
337338
[JsonSerializable(typeof(ReportBundleSummary))]
338339
[JsonSerializable(typeof(SearchHighlight))]
339340
[JsonSerializable(typeof(SearchResult))]
341+
[JsonSerializable(typeof(SearchTermOccurrence))]
340342
[JsonSerializable(typeof(SearchTruncationContext))]
341343
[JsonSerializable(typeof(StatusResult))]
342344
[JsonSerializable(typeof(StatusDbPragmaSettings))]

src/CodeIndex/Cli/QueryCommandRunner.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,8 @@ public static int RunExcerpt(string[] cmdArgs, JsonSerializerOptions jsonOptions
14971497
Console.Error.WriteLine("No excerpt found.");
14981498
return ZeroResultExitCode(options);
14991499
}
1500+
if (options.Json)
1501+
excerpt.SemanticTokens = BuildExcerptSemanticTokens(excerpt);
15001502

15011503
if (options.Json)
15021504
{
@@ -1511,6 +1513,57 @@ public static int RunExcerpt(string[] cmdArgs, JsonSerializerOptions jsonOptions
15111513
});
15121514
}
15131515

1516+
private static List<ExcerptSemanticToken> BuildExcerptSemanticTokens(FileExcerptResult excerpt)
1517+
{
1518+
var tokens = new List<ExcerptSemanticToken>();
1519+
var lines = excerpt.Content.Replace("\r\n", "\n").Split('\n');
1520+
for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
1521+
{
1522+
var line = lines[lineIndex];
1523+
var column = 0;
1524+
while (column < line.Length)
1525+
{
1526+
if (!IsSemanticTokenStart(line[column]))
1527+
{
1528+
column++;
1529+
continue;
1530+
}
1531+
1532+
var start = column;
1533+
column++;
1534+
while (column < line.Length && IsSemanticTokenPart(line[column]))
1535+
column++;
1536+
1537+
var tokenText = line[start..column];
1538+
tokens.Add(new ExcerptSemanticToken
1539+
{
1540+
StartLine = excerpt.StartLine + lineIndex,
1541+
StartColumn = start + 1,
1542+
EndLine = excerpt.StartLine + lineIndex,
1543+
EndColumn = column + 1,
1544+
Type = ClassifySemanticToken(tokenText),
1545+
});
1546+
}
1547+
}
1548+
1549+
return tokens;
1550+
}
1551+
1552+
private static bool IsSemanticTokenStart(char value) =>
1553+
char.IsLetter(value) || value == '_' || char.IsDigit(value);
1554+
1555+
private static bool IsSemanticTokenPart(char value) =>
1556+
char.IsLetterOrDigit(value) || value == '_';
1557+
1558+
private static string ClassifySemanticToken(string token)
1559+
{
1560+
if (token.All(char.IsDigit))
1561+
return "number";
1562+
if (char.IsUpper(token[0]))
1563+
return "type";
1564+
return "variable";
1565+
}
1566+
15141567
public static int RunFind(string[] cmdArgs, JsonSerializerOptions jsonOptions)
15151568
{
15161569
var preparedFindArgs = PrepareFindArgs(cmdArgs, out var preparationError);

0 commit comments

Comments
 (0)