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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1934.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1934
affected:
- src/CodeIndex/Database/DbSymbolReader.cs
- src/CodeIndex/Mcp/McpToolDefinitions.cs
---

## English

- **Symbol hotspots now break ranking ties deterministically (#1934)** — hotspot results now sort equal scores and counts by path, line, name, kind, and symbol id, and the MCP tool description documents that order.

## 日本語

- **symbol hotspots の同点順位を決定的にしました (#1934)** — hotspot 結果は同じ score / count の行を path、line、name、kind、symbol id で並べ、MCP tool description でもその順序を明記しました。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1974.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1974
affected:
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs
- src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs
---

## English

- **Reference deduplication now keeps distinct containers (#1974)** — reference extraction now includes container kind and name in the in-memory dedupe key so same-position references from different enclosing symbols are preserved.

## 日本語

- **reference dedupe が異なる container を保持するようになりました (#1974)** — reference extraction はメモリ上の dedupe key に container kind / name を含めるため、異なる enclosing symbol からの同一位置 reference が保持されます。
15 changes: 15 additions & 0 deletions changelog.d/unreleased/2015.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
category: fixed
issues:
- 2015
affected:
- src/CodeIndex/Database/DbSymbolReader.cs
---

## English

- **Definition symbol ordering now has a stable column tie-breaker (#2015)** — symbol searches now order same-name, same-path, same-line results by `start_column` and symbol id so definition output stays deterministic.

## 日本語

- **definition のシンボル順序に安定した column tie-breaker を追加しました (#2015)** — symbol search は同じ name / path / line の結果を `start_column` と symbol id で並べるため、definition 出力が決定的になりました。
11 changes: 9 additions & 2 deletions src/CodeIndex/Database/DbSymbolReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ FROM symbols s
"WHEN @preferCaseInsensitiveNormalizedSqlMatch = 1 AND f.lang = 'sql' AND sql_segment_count(s.name) = @rawQuerySegmentCount AND sql_normalize_name_folded(s.name) = @rawQueryNormalizedFolded THEN 3 " +
"WHEN @preferCaseInsensitiveSqlLeafMatch = 1 AND f.lang = 'sql' AND sql_leaf_name_folded(s.name) = @rawQueryLeafFolded THEN 4 " +
"ELSE 5 END, " +
$"{PathBucketOrder}, {VisibilityOrder}, s.name, f.path, s.line LIMIT @limit";
$"{PathBucketOrder}, {VisibilityOrder}, s.name, f.path, s.line, " +
$"{GetSymbolColumnSql("start_column", "CAST(2147483647 AS INTEGER)")} ASC, s.id ASC LIMIT @limit";

cmd.CommandText = sql;
if (effectiveQueries != null)
Expand Down Expand Up @@ -2039,7 +2040,13 @@ LEFT JOIN conservative_reference_counts crc
FROM grouped_rows gr
JOIN reference_counts rc ON rc.symbol_id = gr.symbol_id
WHERE rc.ref_count > 0
ORDER BY rc.ref_score DESC, rc.ref_count DESC
ORDER BY rc.ref_score DESC,
rc.ref_count DESC,
gr.path COLLATE BINARY ASC,
gr.line ASC,
gr.name COLLATE BINARY ASC,
gr.kind COLLATE BINARY ASC,
gr.symbol_id ASC
LIMIT @limit";

using var cmd = _conn.CreateCommand();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ internal static void AddTypeReferenceSegment(
return;

int column = startInLine + 1; // 1-based / 1始まり
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, segment);
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, segment, container);
if (!seen.Add(dedupeKey))
return;

Expand Down Expand Up @@ -1791,7 +1791,7 @@ private static void AddChainReference(
int lineNumber,
SymbolRecord? container)
{
var dedupeKey = BuildReferenceDedupeKey(fileId, null, lineNumber, column, referenceKind, name);
var dedupeKey = BuildReferenceDedupeKey(fileId, null, lineNumber, column, referenceKind, name, container);
if (!seen.Add(dedupeKey))
return;

Expand Down
11 changes: 7 additions & 4 deletions src/CodeIndex/Indexer/References/ReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ internal static void AddReference(
string? language = null)
{
var column = nameIndex + 1;
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, name);
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, name, container);
if (!seen.Add(dedupeKey))
return;

Expand All @@ -1244,10 +1244,13 @@ internal static string BuildReferenceDedupeKey(
int lineNumber,
int column,
string referenceKind,
string name)
string name,
SymbolRecord? container)
{
var languageSegment = string.IsNullOrWhiteSpace(language) ? "-" : language;
return $"{fileId}:{languageSegment}:{lineNumber}:{column}:{referenceKind}:{name}";
var containerKindSegment = string.IsNullOrWhiteSpace(container?.Kind) ? "-" : container.Kind;
var containerNameSegment = string.IsNullOrWhiteSpace(container?.Name) ? "-" : container.Name;
return $"{fileId}:{languageSegment}:{lineNumber}:{column}:{referenceKind}:{containerKindSegment}:{containerNameSegment}:{name}";
}

private static void EmitCSharpLambdaCaptureReferences(
Expand Down Expand Up @@ -1761,7 +1764,7 @@ internal static void AddTypeReferenceSegments(
if (!IsIgnoredTypeReferenceSegment(language, normalizedSegment, isEscapedCSharpIdentifier))
{
int column = argStartInLine + offset + 1; // 1-based / 1始まり
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, "type_reference", normalizedSegment);
var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, "type_reference", normalizedSegment, container);
if (seen.Add(dedupeKey))
{
references.Add(new ReferenceRecord
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private static void AddChainReference(
SymbolRecord? container)
{
name = NormalizeMethodReferenceName(language, name);
var dedupeKey = ReferenceExtractor.BuildReferenceDedupeKey(fileId, language, lineNumber, column, "call", name);
var dedupeKey = ReferenceExtractor.BuildReferenceDedupeKey(fileId, language, lineNumber, column, "call", name, container);
if (!seen.Add(dedupeKey))
return;

Expand Down
4 changes: 2 additions & 2 deletions src/CodeIndex/Mcp/McpToolDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,9 @@ private JsonNode HandleToolsList(JsonNode? id)
CreateToolDefinition(
"symbol_hotspots",
"Find the most-referenced symbols in the codebase (hotspot analysis). "
+ "Returns symbols ordered by reference count. `groupBy` can be `symbol`, `file`, or `statement`; the default is symbol grouping for non-SQL scopes and statement grouping for SQL scopes to preserve existing SQL behavior. Names that are unique within the active language/kind candidate set use codebase-wide totals; duplicate-name families fall back to conservative same-file counts, and same-file duplicate rows may be grouped when the DB cannot disambiguate targets. Cross-file grouping of duplicate families is trusted only on indexes stamped with the current authoritative hotspot-family version. Useful for identifying central, high-impact code. "
+ "Returns symbols ordered by reference score, reference count, then deterministic ties by path, line, name, kind, and symbol id. `groupBy` can be `symbol`, `file`, or `statement`; the default is symbol grouping for non-SQL scopes and statement grouping for SQL scopes to preserve existing SQL behavior. Names that are unique within the active language/kind candidate set use codebase-wide totals; duplicate-name families fall back to conservative same-file counts, and same-file duplicate rows may be grouped when the DB cannot disambiguate targets. Cross-file grouping of duplicate families is trusted only on indexes stamped with the current authoritative hotspot-family version. Useful for identifying central, high-impact code. "
+ "/ コードベースで最も参照されるシンボルを検索する(ホットスポット分析)。"
+ "参照回数順にシンボルを返す。`groupBy` は `symbol` / `file` / `statement` を指定でき、既存 SQL 挙動を保つため既定は非 SQL scope では symbol、SQL scope では statement。active な言語/種別候補集合で一意な名前は codebase 全体の件数を使い、同名ファミリーは保守的な same-file 件数へフォールバックし、DB が対象を曖昧なく結べない同一ファイル重複行は集約される。duplicate family の cross-file 集約は current の authoritative hotspot-family version で stamp された index でのみ信頼する。中心的で影響の大きいコードの特定に有用。",
+ "参照スコア、参照回数の順にシンボルを返し、同点は path、line、name、kind、symbol id で決定的に並べる。`groupBy` は `symbol` / `file` / `statement` を指定でき、既存 SQL 挙動を保つため既定は非 SQL scope では symbol、SQL scope では statement。active な言語/種別候補集合で一意な名前は codebase 全体の件数を使い、同名ファミリーは保守的な same-file 件数へフォールバックし、DB が対象を曖昧なく結べない同一ファイル重複行は集約される。duplicate family の cross-file 集約は current の authoritative hotspot-family version で stamp された index でのみ信頼する。中心的で影響の大きいコードの特定に有用。",
new JsonObject
{
["type"] = "object",
Expand Down
62 changes: 62 additions & 0 deletions tests/CodeIndex.Tests/DbReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,46 @@ public void GetSymbolHotspots_RanksRealCallsAboveManyLowerWeightSubscribeEdges()
Assert.Equal(1.5, hotspots[1].ReferenceScore, precision: 6);
}

[Fact]
public void GetSymbolHotspots_BreaksEqualCountsByPathLineNameKind()
{
var betaFileId = _writer.UpsertFile(new FileRecord
{
Path = "src/z_hotspot_tie.cs",
Lang = "csharp",
Size = 100,
Lines = 10,
Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc),
});
var alphaFileId = _writer.UpsertFile(new FileRecord
{
Path = "src/a_hotspot_tie.cs",
Lang = "csharp",
Size = 100,
Lines = 10,
Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc),
});

_writer.InsertSymbols(
[
new SymbolRecord { FileId = betaFileId, Kind = "function", Name = "BetaTieTarget", Line = 5, StartLine = 5, EndLine = 7, BodyStartLine = 6, BodyEndLine = 7, Signature = "void BetaTieTarget()" },
new SymbolRecord { FileId = alphaFileId, Kind = "function", Name = "AlphaTieTarget", Line = 5, StartLine = 5, EndLine = 7, BodyStartLine = 6, BodyEndLine = 7, Signature = "void AlphaTieTarget()" },
]);
_writer.InsertReferences(
[
new ReferenceRecord { FileId = betaFileId, SymbolName = "BetaTieTarget", ReferenceKind = "call", Line = 8, Column = 9, Context = "BetaTieTarget();" },
new ReferenceRecord { FileId = alphaFileId, SymbolName = "AlphaTieTarget", ReferenceKind = "call", Line = 8, Column = 9, Context = "AlphaTieTarget();" },
]);

var hotspots = _reader.GetSymbolHotspots(limit: 10, kind: "function", lang: "csharp", pathPatterns: ["src/a_hotspot_tie.cs", "src/z_hotspot_tie.cs"], excludePathPatterns: null, excludeTests: false)
.Where(item => item.Symbol.Name.EndsWith("TieTarget", StringComparison.Ordinal))
.ToList();

Assert.Collection(hotspots,
first => Assert.Equal("src/a_hotspot_tie.cs", first.Symbol.Path),
second => Assert.Equal("src/z_hotspot_tie.cs", second.Symbol.Path));
}

[Fact]
public void GetCallers_DefaultWeightedRankingPrioritizesInstantiateOverNoisySubscriptions()
{
Expand Down Expand Up @@ -2055,6 +2095,28 @@ public void SearchSymbols_FindsByName()
Assert.Equal("src/auth.py", results[0].Path);
}

[Fact]
public void SearchSymbols_BreaksSameLineTiesByStartColumn()
{
var fileId = _writer.UpsertFile(new FileRecord
{
Path = "src/same_line_symbols.cs",
Lang = "csharp",
Size = 100,
Lines = 1,
Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc),
});
_writer.InsertSymbols(
[
new SymbolRecord { FileId = fileId, Kind = "function", Name = "SameLine", Line = 1, StartLine = 1, StartColumn = 30, EndLine = 1, Signature = "late SameLine()" },
new SymbolRecord { FileId = fileId, Kind = "function", Name = "SameLine", Line = 1, StartLine = 1, StartColumn = 10, EndLine = 1, Signature = "early SameLine()" },
]);

var results = _reader.SearchSymbols("SameLine", kind: "function", lang: "csharp", exact: true, pathPatterns: ["src/same_line_symbols.cs"]);

Assert.Equal(["early SameLine()", "late SameLine()"], results.Select(result => result.Signature).ToArray());
}

[Fact]
public void SearchSymbols_CSharpOperatorsConversionsAndIndexersUseNavigableNames()
{
Expand Down
16 changes: 14 additions & 2 deletions tests/CodeIndex.Tests/ReferenceExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,26 @@ def inspect_job():
[Fact]
public void BuildReferenceDedupeKey_IncludesFileIdAndLanguage()
{
var javaKey = ReferenceExtractor.BuildReferenceDedupeKey(1, "java", 3, 5, "type_reference", "Runner");
var rustKey = ReferenceExtractor.BuildReferenceDedupeKey(2, "rust", 3, 5, "type_reference", "Runner");
var javaKey = ReferenceExtractor.BuildReferenceDedupeKey(1, "java", 3, 5, "type_reference", "Runner", null);
var rustKey = ReferenceExtractor.BuildReferenceDedupeKey(2, "rust", 3, 5, "type_reference", "Runner", null);

Assert.NotEqual(javaKey, rustKey);
Assert.StartsWith("1:java:", javaKey, StringComparison.Ordinal);
Assert.StartsWith("2:rust:", rustKey, StringComparison.Ordinal);
}

[Fact]
public void BuildReferenceDedupeKey_IncludesContainerContext()
{
var leftContainer = new SymbolRecord { Kind = "function", Name = "A.foo" };
var rightContainer = new SymbolRecord { Kind = "function", Name = "B.foo" };

var leftKey = ReferenceExtractor.BuildReferenceDedupeKey(1, "csharp", 3, 5, "call", "Bar", leftContainer);
var rightKey = ReferenceExtractor.BuildReferenceDedupeKey(1, "csharp", 3, 5, "call", "Bar", rightContainer);

Assert.NotEqual(leftKey, rightKey);
}

[Fact]
public void AddTypeReferenceSegment_DedupesWithinFileAndLanguageOnly()
{
Expand Down
Loading