Skip to content

Commit d85a678

Browse files
committed
Harden exact graph identity semantics (#5187)
1 parent 6995e29 commit d85a678

11 files changed

Lines changed: 392 additions & 48 deletions

changelog.d/unreleased/5187.fixed.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ issues:
55
affected:
66
- src/CodeIndex/Database/DbReader.GraphIdentity.cs
77
- src/CodeIndex/Database/DbReader.GraphQueries.cs
8+
- src/CodeIndex/Database/DbReader.GraphReferenceQueries.cs
9+
- src/CodeIndex/Database/DbReader.GraphReferenceQueryExecution.cs
810
- src/CodeIndex/Database/DbReader.ImpactDefinitions.cs
911
- src/CodeIndex/Database/DbReader.References.cs
1012
- src/CodeIndex/Database/DbReader.ImpactTraversal.cs
1113
- src/CodeIndex/Database/DbSymbolReader.Analysis.cs
1214
- src/CodeIndex/Cli/CliFlagSchema.cs
1315
- src/CodeIndex/Cli/ConsoleUi.cs
16+
- src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs
1417
- src/CodeIndex/Cli/QueryCommandRunner.Graph.cs
1518
- src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs
19+
- src/CodeIndex/Cli/QueryCommandRunner.Locations.cs
1620
- src/CodeIndex/Cli/ProjectionFieldRegistry.cs
1721
- src/CodeIndex/Mcp/McpToolArgumentContracts.cs
1822
- src/CodeIndex/Mcp/McpToolCatalog.cs

src/CodeIndex/Cli/QueryCommandRunner.Graph.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti
6868
options.ExcludePaths,
6969
options.ExcludeTests,
7070
options.Kind,
71-
includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls)
71+
includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls,
72+
requireAuthoritativeIdentity: true)
7273
: reader.CountSearchReferencesTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.IncludeQualifiedCommonCalls);
7374
var effectiveSqlGraphSignal = NarrowSqlGraphContractSignal(
7475
baseSqlGraphSignal,
@@ -113,7 +114,8 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti
113114
options.MaxLineWidth,
114115
offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("references"),
115116
referenceKind: options.Kind,
116-
includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls)
117+
includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls,
118+
requireAuthoritativeIdentity: true)
117119
: reader.SearchReferences(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.MaxLineWidth, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("references"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls);
118120
if (options.IncludeBody && JsonEnvelopeWrapper.ShouldMaterializeBody("references"))
119121
AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth);

src/CodeIndex/Database/DbReader.GraphIdentity.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,22 @@ private GraphQueryIdentityMetadata BuildGraphQueryIdentityMetadata(
220220
CandidatesTruncated: truncated);
221221
}
222222

223+
private string BuildReferenceRootSymbolIdsSql(string referenceAlias)
224+
{
225+
if (!_referenceIdentityContractCurrent)
226+
return "NULL";
227+
228+
var targetSymbolIdSql = $"CAST({referenceAlias}.target_symbol_id AS TEXT)";
229+
if (!HasTable("symbol_reference_candidates"))
230+
return targetSymbolIdSql;
231+
232+
return $@"COALESCE((
233+
SELECT GROUP_CONCAT(identity_candidate.symbol_id)
234+
FROM symbol_reference_candidates AS identity_candidate
235+
WHERE identity_candidate.reference_id = {referenceAlias}.id
236+
), {targetSymbolIdSql})";
237+
}
238+
223239
private GraphQueryIdentityMetadata BuildSelectedGraphQueryIdentityMetadata(
224240
DefinitionResult selectedDefinition)
225241
=> new(

src/CodeIndex/Database/DbReader.GraphQueries.cs

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ ELSE 5
724724
/// SQL 側で要求された LIMIT/OFFSET を適用し、呼び出し側が要求以上の中間ページを
725725
/// materialize しないようにする。
726726
/// </summary>
727-
private List<CallerResult> GetCallersExactCore(string symbolName, int limit, int offset, string? lang, IReadOnlyList<string>? pathPatterns, IReadOnlyList<string>? excludePathPatterns, bool excludeTests, IReadOnlyList<long>? targetSymbolIds, bool includeAmbiguousMSource, bool includeMemberReads)
727+
private List<CallerResult> GetCallersExactCore(string symbolName, int limit, int offset, string? lang, IReadOnlyList<string>? pathPatterns, IReadOnlyList<string>? excludePathPatterns, bool excludeTests, IReadOnlyList<long>? targetSymbolIds, bool requireAuthoritativeIdentity, bool includeAmbiguousMSource, bool includeMemberReads)
728728
{
729729
if (!_hasReferencesTable) return new List<CallerResult>();
730730
using var cmd = _conn.CreateCommand();
@@ -744,12 +744,7 @@ private List<CallerResult> GetCallersExactCore(string symbolName, int limit, int
744744
var targetSymbolIdSql = hasIdentityTargetScope
745745
? $@"CASE
746746
WHEN r.resolution_state = 'resolved'
747-
AND EXISTS (
748-
SELECT 1
749-
FROM symbol_reference_candidates projected_identity_candidate
750-
WHERE projected_identity_candidate.reference_id = r.id
751-
AND projected_identity_candidate.symbol_id IN ({targetSymbolIdsSql})
752-
)
747+
AND r.target_symbol_id IN ({targetSymbolIdsSql})
753748
THEN r.target_symbol_id
754749
ELSE NULL
755750
END"
@@ -783,19 +778,25 @@ ELSE NULL
783778
? "(r.symbol_name = @symbolName COLLATE NOCASE OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@symbolName) COLLATE NOCASE)" + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))"
784779
: "(((f.lang = 'sql') AND sql_context_has_name_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND r.symbol_name = @symbolName COLLATE NOCASE) OR " + BuildCSharpQualifiedContextFallbackSql(BuildQualifiedContextMatchSql(contextSql, "r.column_number", folded: false, like: false)) + " OR " + BuildQualifiedLeafFallbackSql("r.symbol_name", "r.symbol_name_folded", folded: false) + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))";
785780
var nameCondition = "\n AND " + namePredicate;
786-
// Identity-scoped traversal admits only references whose candidate set contains the
787-
// requested canonical target. Unresolved/ambiguous same-leaf rows remain available to
788-
// broad reference discovery, but they are not confirmed call-graph edges.
789-
// identity scope の traversal は candidate set が要求 target を含む参照だけを採用する。
790-
// unresolved/ambiguous な同名 leaf は広い reference 探索には残すが、確定 call graph edge
791-
// としては扱わない。
792-
var identityTargetPredicate = $@"EXISTS (
781+
// Selector traversal admits only references authoritatively resolved to that target.
782+
// Name-resolved roots, including a partial family that currently has one member, retain
783+
// resolved-group evidence whose candidate set intersects the requested identities.
784+
// Unresolved/ambiguous same-leaf rows remain available to broad reference discovery,
785+
// but not as confirmed graph edges.
786+
// selector traversal は対象へ確定解決された参照だけを採用する。現在の member が1件の
787+
// partial family を含む name-resolved root は、candidate set が要求 identity と交差する
788+
// resolved-group evidence を維持する。未解決・曖昧な同名 leaf は広い reference 探索には
789+
// 残すが、確定 graph edge としては扱わない。
790+
var candidateIdentityTargetPredicate = $@"EXISTS (
793791
SELECT 1
794792
FROM symbol_reference_candidates identity_candidate
795793
WHERE identity_candidate.reference_id = r.id
796794
AND identity_candidate.symbol_id IN ({targetSymbolIdsSql})
797795
AND r.resolution_state IN ('resolved', 'resolved_group')
798796
)";
797+
var identityTargetPredicate = requireAuthoritativeIdentity
798+
? $"(r.resolution_state = 'resolved' AND r.target_symbol_id IN ({targetSymbolIdsSql}))"
799+
: candidateIdentityTargetPredicate;
799800
var targetCondition = !hasIdentityTargetContract
800801
? nameCondition
801802
: targetSymbolIds!.Count == 0
@@ -962,8 +963,17 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i
962963
lang = NormalizeQueryLanguage(lang);
963964
var resolvedName = selectedDefinition?.Name ?? ResolveSymbolName(symbolName, lang);
964965
var definitionOffset = string.Equals(responseCollection, "definitions", StringComparison.Ordinal) ? offset : 0;
966+
var selectedDefinitionMatchesFilters = selectedDefinition != null
967+
&& SelectedDefinitionMatchesImpactFilters(
968+
selectedDefinition,
969+
lang,
970+
pathPatterns,
971+
excludePathPatterns,
972+
excludeTests);
965973
var definitionResolution = selectedDefinition != null
966-
? ResolveSelectedImpactDefinition(selectedDefinition)
974+
? selectedDefinitionMatchesFilters
975+
? ResolveSelectedImpactDefinition(selectedDefinition)
976+
: EmptyImpactDefinitionResolution()
967977
: ResolveImpactDefinitions(symbolName, limit, lang, pathPatterns, excludePathPatterns, excludeTests, definitionOffset);
968978
if (selectedDefinition == null
969979
&& definitionResolution.Definitions.Count == 0
@@ -1080,7 +1090,8 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i
10801090
? offset
10811091
: 0;
10821092
var (callers, truncated, truncatedReason, terminationReason, cycles) = selectedDefinition != null
1083-
? GetTransitiveCallersForCandidate(
1093+
? selectedDefinitionMatchesFilters
1094+
? GetTransitiveCallersForCandidate(
10841095
selectedDefinition,
10851096
maxDepth,
10861097
limit,
@@ -1091,12 +1102,14 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i
10911102
withPaths,
10921103
callerOffset,
10931104
includeMemberReads)
1105+
: ([], false, null, ImpactTerminationReasons.Completed, [])
10941106
: GetTransitiveCallers(symbolName, maxDepth, limit, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths, resultOffset: callerOffset, includeMemberReads: includeMemberReads);
10951107
var callerExistsBeforeOffset = false;
10961108
if (callers.Count == 0 && callerOffset > 0)
10971109
{
10981110
var callerProbe = selectedDefinition != null
1099-
? GetTransitiveCallersForCandidate(
1111+
? selectedDefinitionMatchesFilters
1112+
? GetTransitiveCallersForCandidate(
11001113
selectedDefinition,
11011114
maxDepth,
11021115
1,
@@ -1107,6 +1120,7 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i
11071120
withPaths: false,
11081121
resultOffset: 0,
11091122
includeMemberReads)
1123+
: ([], false, null, ImpactTerminationReasons.Completed, [])
11101124
: GetTransitiveCallers(symbolName, maxDepth, 1, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths: false, resultOffset: 0, includeMemberReads: includeMemberReads);
11111125
callerExistsBeforeOffset = callerProbe.Results.Count > 0;
11121126
}

src/CodeIndex/Database/DbReader.GraphReferenceQueries.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ private string BuildCallerListSql(
327327
var selfReferenceSql = _referenceColumns.Contains("is_self_reference") ? "r.is_self_reference" : "0";
328328
var mutualRecursionSql = _referenceColumns.Contains("is_mutual_recursion") ? "r.is_mutual_recursion" : "0";
329329
var referenceSpanLengthSql = _referenceColumns.Contains("span_length") ? "r.span_length" : "NULL";
330-
var rootSymbolIdSql = _referenceIdentityContractCurrent ? "r.target_symbol_id" : "NULL";
330+
var rootSymbolIdSql = BuildReferenceRootSymbolIdsSql("r");
331331
var sql = @"
332332
WITH logical_references AS (
333333
SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name,
@@ -536,6 +536,13 @@ private string BuildCalleeQualifiedNameFilterSql(GraphReferenceQueryRequest requ
536536

537537
private string BuildCallerIdentityFilterSql(GraphReferenceQueryRequest request)
538538
{
539+
if (request.IdentitySymbolId != null)
540+
{
541+
return _referenceIdentityContractCurrent
542+
? " AND r.target_symbol_id = @targetSymbolId"
543+
: " AND 1 = 0";
544+
}
545+
539546
if (request.CallerIdentitySymbolIds == null || !HasTable("symbol_reference_candidates"))
540547
return string.Empty;
541548

@@ -634,7 +641,11 @@ private void BindGraphReferenceQueryPlan(SqliteCommand command, GraphReferenceQu
634641
}
635642
if (plan.BindIdentityParameter)
636643
{
637-
if (request.CallerIdentitySymbolIds != null)
644+
if (request.IdentitySymbolId != null)
645+
{
646+
SqliteCommandPolicy.Add(command, plan.Direction.IdentityParameterName, request.IdentitySymbolId.Value);
647+
}
648+
else if (request.CallerIdentitySymbolIds != null)
638649
{
639650
var symbolIdValues = request.CallerIdentitySymbolIds
640651
.Select(static symbolId => symbolId.ToString(System.Globalization.CultureInfo.InvariantCulture))
@@ -644,10 +655,6 @@ private void BindGraphReferenceQueryPlan(SqliteCommand command, GraphReferenceQu
644655
"@callerTargetSymbolIdsJson",
645656
JsonStringListCodec.Serialize(symbolIdValues));
646657
}
647-
else
648-
{
649-
SqliteCommandPolicy.Add(command, plan.Direction.IdentityParameterName, request.IdentitySymbolId!.Value);
650-
}
651658
}
652659
AddPathFilterParameters(command, request.PathPatterns, request.ExcludePathPatterns);
653660
if (plan.Shape != GraphReferenceQueryShape.TotalCount)

src/CodeIndex/Database/DbReader.ImpactDefinitions.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ private static ImpactDefinitionResolution ResolveSelectedImpactDefinition(
108108
DefinitionResult definition)
109109
{
110110
var precise = IsPreciseImpactFallbackKind(definition.Kind);
111+
var nonCallable = definition.Kind is "namespace" or "import";
111112
var symbolIds = definition.SymbolId is long symbolId
112113
? new HashSet<long> { symbolId }
113114
: [];
@@ -119,10 +120,45 @@ private static ImpactDefinitionResolution ResolveSelectedImpactDefinition(
119120
PreciseDefinitionCount: precise ? 1 : 0,
120121
PreciseLogicalDefinitionCount: precise ? 1 : 0,
121122
PreciseDefinitionFileCount: precise ? 1 : 0,
122-
NonCallableDefinitionCount: precise ? 1 : 0,
123+
NonCallableDefinitionCount: nonCallable ? 1 : 0,
123124
SinglePreciseDefinition: precise ? definition : null,
124125
PhysicalSymbolIds: symbolIds,
125126
PhysicalDefinitionPaths: new HashSet<string>(StringComparer.Ordinal) { definition.Path },
126127
PhysicalSymbolIdsTruncated: false);
127128
}
129+
130+
private static ImpactDefinitionResolution EmptyImpactDefinitionResolution()
131+
=> new(
132+
[],
133+
PhysicalCount: 0,
134+
PhysicalFileCount: 0,
135+
LogicalCount: 0,
136+
PreciseDefinitionCount: 0,
137+
PreciseLogicalDefinitionCount: 0,
138+
PreciseDefinitionFileCount: 0,
139+
NonCallableDefinitionCount: 0,
140+
SinglePreciseDefinition: null,
141+
PhysicalSymbolIds: [],
142+
PhysicalDefinitionPaths: [],
143+
PhysicalSymbolIdsTruncated: false);
144+
145+
private bool SelectedDefinitionMatchesImpactFilters(
146+
DefinitionResult definition,
147+
string? lang,
148+
IReadOnlyList<string>? pathPatterns,
149+
IReadOnlyList<string>? excludePathPatterns,
150+
bool excludeTests)
151+
{
152+
if (lang != null && !string.Equals(definition.Lang, lang, StringComparison.Ordinal))
153+
return false;
154+
155+
using var command = _conn.CreateCommand();
156+
var sql = "SELECT 1 FROM files f WHERE f.path = @selectedDefinitionPath";
157+
AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests);
158+
sql += " LIMIT 1";
159+
command.CommandText = sql;
160+
SqliteCommandPolicy.Add(command, "@selectedDefinitionPath", definition.Path);
161+
AddPathFilterParameters(command, pathPatterns, excludePathPatterns);
162+
return command.ExecuteScalar() != null;
163+
}
128164
}

src/CodeIndex/Database/DbReader.ImpactTraversal.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,9 @@ private List<CallerResult> ReadImpactCallerPage(
406406
request.ExcludePathPatterns,
407407
request.ExcludeTests,
408408
targetIds,
409-
includeAmbiguousMSource,
410-
request.IncludeMemberReads);
409+
requireAuthoritativeIdentity: request.SelectedDefinition != null,
410+
includeAmbiguousMSource: includeAmbiguousMSource,
411+
includeMemberReads: request.IncludeMemberReads);
411412
}
412413

413414
private int GetImpactGraphStateEntryBudget(int limit)

0 commit comments

Comments
 (0)