diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 092c693fd1..0137d9c4e0 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1706,15 +1706,29 @@ index paths to replace older physical-path target keys before identity-aware rea C# common member names are never discarded during extraction. The writer persists their receiver/type evidence in `target_qualifier`, and reference finalization records `resolution_state`. Bare-name `references` and unqualified graph discovery remain broad and -retain the stored resolution labels. On a current reference-identity contract, however, exact -qualified C# `callers` queries and confirmed `impact` traversal require a `resolved` or -`resolved_group` candidate that matches one of the selected definition IDs or its resolved -polymorphic-dispatch family; unresolved or ambiguous same-leaf evidence remains inspectable but -cannot become a confirmed caller edge. +retain the stored resolution labels. On a current C# reference-identity contract, however, +exact C# `callers` queries and confirmed C# `impact` traversal require a `resolved` or +`resolved_group` candidate that matches one of the selected definition IDs, including the +resolved polymorphic-dispatch family. Multiple C# +same-name definitions contribute their resolved ID union; unresolved or ambiguous same-leaf +evidence remains inspectable but cannot become a confirmed caller edge or transitive hop. When +no matching definition ID exists, exact callers and impact return zero confirmed rows instead +of traversing the name fallback. CLI/MCP payloads expose `identity_root_available`, +`identity_root_unavailable_reason`, `graph_evidence_confidence`, and +`identity_root_resolution_truncated`; missing roots and capped identity sets set +`authoritative_count: false`, while impact marks a missing root heuristic and strict mode fails +through the stable `no_identity_backed_root` failure code. Legacy or stale identity contracts +may keep the name-based compatibility path, but must label it `name_fallback`, degraded, and +non-authoritative. +Other graph languages retain their existing language-specific exact matching until they expose +the same persisted target-identity contract. When `--lang` is omitted, impact partitions the +same traversal step: C# rows must match resolved candidate IDs, while non-C# rows retain their +language-specific exact-name graph. A root inferred only from non-C# definitions reports +`language_graph`; persisted reference-graph incompleteness makes every impact count +non-authoritative even when the local traversal and identity resolution are otherwise complete. C# callable hotspots likewise count logical target identities instead of leaf-name aggregates. Candidate groups contribute only when they collapse to one logical target, name, and kind, and -unresolved rows do not inflate the confirmed count. Legacy or stale identity contracts keep the -name-based compatibility path. The CLI `--include-qualified-common-calls` flag and MCP +unresolved rows do not inflate the confirmed count. The CLI `--include-qualified-common-calls` flag and MCP `includeQualifiedCommonCalls` argument still bypass the query-time common-member noise filter. Keep dependency edges identity-scoped: completeness options may expose unresolved evidence but must not convert it into a same-name file dependency. @@ -5761,14 +5775,26 @@ identity-aware read を ready にする前に、full、scoped、no-op、削除 C# の一般的な member 名は extraction 時に破棄しません。writer は receiver / 型の evidence を `target_qualifier` に永続化し、reference finalization は `resolution_state` を記録します。bare-name の `references` と無修飾 graph discovery は広い検索結果と保存済み resolution label を維持します。一方、 -現行の reference-identity contract では、exact qualified C# `callers` query と confirmed `impact` -traversal は、選択した definition ID のいずれかに一致する `resolved` または `resolved_group` candidate -か、その解決済み polymorphic-dispatch family を必須とします。未解決または曖昧な same-leaf evidence -は引き続き確認できますが、confirmed caller edge にはなりません。C# callable hotspot も +現行の C# reference-identity contract では、exact C# `callers` query と confirmed C# +`impact` traversal は、選択した definition ID または解決済み polymorphic-dispatch family の +いずれかに一致する `resolved` または `resolved_group` candidate を必須とします。C# の同名 definition +が複数ある場合は解決済み ID の和集合を対象にしますが、未解決または +曖昧な same-leaf evidence は引き続き確認できても confirmed caller edge や推移 hop にはなりません。 +一致する definition ID が無い場合、exact callers と impact は name fallback を走査せず confirmed 0 件を +返します。CLI/MCP payload は `identity_root_available`、`identity_root_unavailable_reason`、 +`graph_evidence_confidence`、`identity_root_resolution_truncated` を公開し、root 不在または identity 集合の +cap 到達では `authoritative_count: false` を設定します。impact は root 不在を heuristic とし、strict mode は +安定した `no_identity_backed_root` failure code で失敗します。legacy または stale な identity contract が +名前ベースの互換経路を維持する場合も、`name_fallback`、degraded、non-authoritative と明示しなければなりません。 +他の graph 言語は、同等の永続 target-identity contract を公開するまで既存の言語固有 exact matching を維持します。 +`--lang` を省略した場合、impact は同じ traversal step を分割し、C# row には解決済み candidate ID の +一致を必須とする一方、非 C# row では言語固有の exact-name graph を維持します。非 C# definition だけから +推論した root は `language_graph` と明示し、永続 reference graph が incomplete なら、局所 traversal と +identity resolution が完了していてもすべての impact count を non-authoritative とします。 +C# callable hotspot も leaf-name aggregate ではなく logical target identity を 集計し、candidate group は logical target、name、kind が 1 つに収束するときだけ加算し、unresolved -row は confirmed count を増やしません。legacy または stale な identity contract は名前ベースの -互換経路を維持します。CLI の `--include-qualified-common-calls` と MCP の +row は confirmed count を増やしません。CLI の `--include-qualified-common-calls` と MCP の `includeQualifiedCommonCalls` は、引き続き query-time common-member noise filter を無効化します。 dependency edge は identity scope のままにし、completeness option で未解決 evidence を公開しても、 同名の file dependency へ変換してはいけません。 diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index c965b3d135..87f444bcc4 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -213,6 +213,7 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result Reusable-stat snapshot semantics keep one valid row beside two rows from the same stale extractor language plus NULL, text, integer, and invalid-timestamp stat storage, so malformed-row filtering and cached stale-language rejection remain one contract. Graph-kind regressions keep default canonical `call` / `instantiate` / `subscribe` output, raw-kind output, and explicit non-default kind access in the same seeded fixture so filtering and projection cannot drift apart. Caller-ranking coverage keeps the large count-over-path-category case, primary-recipe ties, multi-path filtering, test exclusion, and offset page reconstruction in one seeded `DbReaderTests` fixture. CLI human/JSON and MCP pagination assertions reuse compact two- or three-caller fixtures and require the structured ranking recipe to match the ordering applied before pagination (#4881). + Exact C# caller-root coverage is coordinated across `DbReaderIssue5183Tests`, `QueryCommandRunnerIssue5183Tests`, and `McpServerIssue5183Tests`. Keep one isolated fixture with an unresolved external call whose definition file is omitted, a resolved local target sharing its leaf name, two unrelated resolved same-name methods, an unresolved transitive chain, mixed C#/Python callers, a non-C#-only root colliding with an unresolved C# call, and complete, capped, plus persisted-incomplete traversal states. Assert that broad references/callers retain `name_discovery` evidence while exact callers, inspect summaries, impact, hotspots, CLI JSON/compact, and MCP confirmed counts accept only resolved C# target IDs without dropping non-C# language-graph edges; missing roots and incomplete graphs must expose stable degraded/non-authoritative metadata and strict impact failure (#5183, regression boundary from #5084). Query-only regression fixtures run representative `status`, `search`, and `files` commands against a checkpointed WAL database copied without sidecars and assert that the database, `-wal`, and `-shm` artifact set plus persistent pragmas remain unchanged. Separate fixtures prove that committed hot-WAL content, empty-to-hot transitions, checkpoint resets, cross-database dependency targets, and long-lived LSP refreshes use cleaned-up generation-validated private snapshots without touching source sidecars. Cross-database coverage also pins disposal ordering and snapshot-directory cleanup, while injected persistent copy failures pin the non-retrying `query_only_snapshot_copy_failed` taxonomy; interrupted-batch readiness demotion is covered separately through explicit `Repair` intent. `DatabasePermissionPolicyTests.cs` injects a file-mode provider so unsupported and access-denied Unix mode operations remain deterministic and cross-platform while proving both best-effort diagnostics and strict remediation failures. `DbSchemaConstraintTests.cs` also locks schema constraints to `SymbolKindCatalog` and required file foreign keys so DB readiness checks fail when code enums and SQLite CHECK clauses drift. @@ -1349,6 +1350,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" 再利用 stat snapshot の意味論は、有効な1行、同じ stale extractor 言語の2行、NULL / text / integer / 不正 timestamp の stat storage を同居させ、malformed row の除外と stale-language 判定の false cache を1つの契約として固定します。 graph kind の回帰では、既定 canonical `call` / `instantiate` / `subscribe` 出力、raw-kind 出力、明示的な非既定 kind access を同じ seed 済み fixture にまとめ、filtering と projection のずれを検出します。 caller ranking の coverage では、count と path category に大差がある場合、primary recipe の同順位、複数 path filter、test 除外、offset page の再構成を1つの seed 済み `DbReaderTests` fixture にまとめます。CLI の human / JSON と MCP pagination の assertion は2~3 caller の小さな fixture を再利用し、structured ranking recipe が pagination 前に適用した順序と一致することを必須とします (#4881)。 + exact C# caller root の coverage は `DbReaderIssue5183Tests`、`QueryCommandRunnerIssue5183Tests`、`McpServerIssue5183Tests` で連携します。definition file を省略した未解決 external call、同じ leaf 名を持つ解決済み local target、互いに無関係な同名 method 2 件、未解決 root の背後にある推移 chain、C# / Python 混合 caller、未解決 C# call と衝突する非 C# 専用 root、complete / capped / 永続 incomplete の traversal 状態を1つの隔離 fixture に維持してください。broad references/callers は `name_discovery` evidence を保つ一方、exact callers、inspect summary、impact、hotspots、CLI JSON / compact、MCP の confirmed count は解決済み C# target ID だけを受理し、非 C# の language-graph edge を落とさないことを確認します。root 不在と incomplete graph では安定した degraded / non-authoritative metadata と strict impact failure を返すことを assertion にします(#5183、#5084 からの回帰境界)。 query-only の回帰 fixture では sidecar なしで copy した checkpoint 済み WAL database に対して代表的な `status`、`search`、`files` command を実行し、database、`-wal`、`-shm` の artifact set と永続 pragma が変化しないことを検証します。別の fixture では committed hot-WAL content、empty-to-hot transition、checkpoint reset、cross-database dependency target、長時間 LSP refresh が、source sidecar に触れず cleanup される generation 検証済み private snapshot を使うことを固定します。cross-database coverage は dispose 順序と snapshot directory cleanup も固定し、注入した永続的 copy failure は retry されない `query_only_snapshot_copy_failed` taxonomy を固定します。interrupted batch の readiness demotion は明示的な `Repair` intent の別テストで固定します。 `DatabasePermissionPolicyTests.cs` は file-mode provider を注入し、unsupported / access-denied な Unix mode 操作を deterministic かつ cross-platform に保ちながら、best-effort diagnostic と strict remediation failure の両方を検証します。 `DbSchemaConstraintTests.cs` は DB readiness check が code enum と SQLite CHECK 句の drift を検出できるよう、schema constraint と `SymbolKindCatalog`、必須 file foreign key の同期も固定します。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index b0d581692e..e6ca378264 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1914,6 +1914,8 @@ When an indexed symbol has a persisted identifier column, definition-oriented LS Use `--exact-name` when you already have a precise candidate list (e.g. names returned from an earlier `search` / `inspect` / `map` call). Names are compared case-insensitively for equality instead of substring, so `Run` will not also pull in `RunAsync`, `RunImpact`, etc. `--exact-name` composes with `--name`, positional names, and all existing filters. The older `--exact` spelling still works on these commands for backward compatibility, but `--exact-name` avoids the semantic clash with `search`. For C#, pass the canonical extracted symbol name: operators are stored as `operator +` / `operator checked +`, conversion operators as `explicit operator Money` / `implicit operator decimal`, and indexers as `Item`. If your DB was created before the canonical C# operator/indexer rename landed, a normal `cdidx index .` rewrites unchanged C# rows once to upgrade them; `--rebuild` is not required for that change. `status --json` also exposes `csharp_symbol_name_ready` so you can verify that the canonical C# rename has been applied to the current DB. The fold is NFKC + Unicode CaseFold: common non-ASCII pairs such as `Ä` / `ä`, fullwidth `Run` / `Run`, ligatures, sharp-S (`Straße` / `STRASSE`), and Greek final sigma (`Σ` / `ς` / `σ`) now collapse correctly. Unicode CaseFold remains locale-invariant, so Turkish dotted `İ` still folds to `i\u0307` rather than plain `i`. DBs with stale fold metadata fall back to ASCII `COLLATE NOCASE` until the DB contains only current folded keys. Prefer `cdidx backfill-fold` to refresh stored folded keys without reparsing. A plain `cdidx index .` is also enough if the scan rewrites or purges every stale row; otherwise use `cdidx index . --rebuild`. Use `status --json` → `fold_ready` to detect which path is active. +For C# `callers --exact-name` and confirmed `impact` traversal, exact-name equality is only the discovery boundary: returned graph edges must also resolve to one of the matching definition IDs. An unresolved external C# call with the same leaf name remains available through `references`, `inspect`, or a broad `callers` query, but it is not reported as a confirmed exact caller or impact hop. When no identity-backed root exists, CLI JSON and MCP structured output return zero confirmed results with `identity_root_available: false`, `identity_root_unavailable_reason: "no_identity_backed_root"`, `graph_evidence_confidence: "no_identity_root"`, `degraded: true`, and `authoritative_count: false`; `impact` also sets `heuristic: true`, and `impact --strict` fails predictably. A stale legacy identity contract may retain its name fallback, but labels it `graph_evidence_confidence: "name_fallback"` and non-authoritative. Broad name discovery remains distinct, returns `graph_evidence_confidence: "name_discovery"`, and does not discard the unresolved evidence. Other graph languages retain their existing language-specific exact matching until an equivalent persisted target-identity contract is available. With no language filter, C# edges still require identity while non-C# exact-name edges remain visible; a non-C#-only root reports `language_graph`. An incomplete persisted reference graph always makes impact counts non-authoritative, including compact CLI and MCP projections. + For audit passes, add `--sort hotspot|references|size|complexity|path`. `--json` rows include `sort_mode`, `reference_count`, `hotspot_score`, `size_lines`, and `complexity_score` whenever an audit sort is active. @@ -5534,6 +5536,8 @@ cdidx symbols Run --format lsp # LSP locations。qf / sari `--exact-name` は、すでに解決済みの候補リスト(例: `search` / `inspect` / `map` の結果)を渡して正確にその行だけ取り返したいときに使う。部分一致ではなく大文字小文字を無視した完全一致で比較するため、`Run` を指定しても `RunAsync`、`RunImpact` 等には広がらない。`--exact-name` は `--name`、positional 名、他の全フィルタと組み合わせ可能。従来の `--exact` も後方互換で引き続き使えるが、`search` と意味がぶつからない `--exact-name` を推奨する。C# では抽出済みの canonical symbol name を渡す必要があり、演算子は `operator +` / `operator checked +`、変換演算子は `explicit operator Money` / `implicit operator decimal`、インデクサは `Item` で引く。canonical な C# operator/indexer 名へ変わる前に作った DB でも、通常の `cdidx index .` を 1 回流せば unchanged な C# 行を自動で再抽出して更新するため、この変更だけのために `--rebuild` は不要。upgrade 済みかどうかは `status --json` の `csharp_symbol_name_ready` で判定できる。fold は NFKC 正規化 + Unicode CaseFold で、`Ä` / `ä`、全角 `Run` / `Run`、合字、sharp-S(`Straße` / `STRASSE`)、Greek final sigma(`Σ` / `ς` / `σ`)などの非 ASCII 差分も正しく一致する。Unicode CaseFold は locale-invariant のため、トルコ語の dotted `İ` は依然 plain `i` ではなく `i\u0307` に fold される。stale な fold metadata を含む DB は、DB 内が current folded key のみになるまで ASCII `COLLATE NOCASE` に黙ってフォールバックする。stored folded key を再解析なしで更新したいなら `cdidx backfill-fold` を優先し、scan が stale row をすべて rewrite / purge できるなら通常の `cdidx index .` でも復帰できる。stale row が残る場合だけ `cdidx index . --rebuild` が必要。`status --json` の `fold_ready` で現在の経路を判定可能。 +C# の `callers --exact-name` と confirmed `impact` traversal では、exact-name 一致は discovery 境界にすぎず、返す graph edge は一致した definition ID のいずれかへ解決済みであることも必須です。同じ leaf 名を持つ未解決の外部 C# call は `references`、`inspect`、または broad な `callers` query で引き続き確認できますが、confirmed exact caller や impact hop には数えません。identity-backed root が無い場合、CLI JSON と MCP structured output は confirmed 0 件を返し、`identity_root_available: false`、`identity_root_unavailable_reason: "no_identity_backed_root"`、`graph_evidence_confidence: "no_identity_root"`、`degraded: true`、`authoritative_count: false` を設定します。`impact` はさらに `heuristic: true` とし、`impact --strict` は決定的に失敗します。stale な legacy identity contract では name fallback を維持する場合がありますが、`graph_evidence_confidence: "name_fallback"` と non-authoritative status を明示します。broad name discovery は `graph_evidence_confidence: "name_discovery"` を返し、未解決 evidence を破棄しません。他の graph 言語は、同等の永続 target-identity contract が利用可能になるまで既存の言語固有 exact matching を維持します。言語 filter が無い場合も C# edge は identity を必須とし、非 C# の exact-name edge は維持します。非 C# だけの root は `language_graph` を返します。永続 reference graph が incomplete なら、compact CLI と MCP projection を含む impact count は常に non-authoritative になります。 + audit では `--sort hotspot|references|size|complexity|path` を追加できます。 audit sort が有効な `--json` row には `sort_mode`、`reference_count`、 `hotspot_score`、`size_lines`、`complexity_score` が含まれます。 diff --git a/changelog.d/unreleased/5183.fixed.md b/changelog.d/unreleased/5183.fixed.md new file mode 100644 index 0000000000..ff1bd4c21a --- /dev/null +++ b/changelog.d/unreleased/5183.fixed.md @@ -0,0 +1,38 @@ +--- +category: fixed +issues: + - 5183 +affected: + - src/CodeIndex/Database/DbReader.GraphQueries.cs + - src/CodeIndex/Database/DbReader.ImpactTraversal.cs + - src/CodeIndex/Models/QueryResults.cs + - src/CodeIndex/Cli/QueryCommandRunner.Graph.cs + - src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs + - src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs + - src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs + - src/CodeIndex/Mcp/McpToolHandlers.Graph.cs + - src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs + - tests/CodeIndex.Tests/DbReaderIssue5183Tests.cs + - tests/CodeIndex.Tests/DbReaderImpactTests.cs + - tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs + - tests/CodeIndex.Tests/DbReaderTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerIssue5183Tests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerIssue5164Tests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - tests/CodeIndex.Tests/McpServerIssue5183Tests.cs + - tests/CodeIndex.Tests/golden/impact.json + - USER_GUIDE.md + - DEVELOPER_GUIDE.md + - TESTING_GUIDE.md +--- + +## English + +- **Exact C# caller and impact queries no longer promote unresolved same-name calls to confirmed graph evidence (#5183)** — exact C# callers and impact now require resolved target IDs, while broad discovery retains unresolved name evidence and labels it `name_discovery`. Language-unspecified impact keeps non-C# exact-name graph edges while identity-filtering C#, non-C#-only roots report `language_graph`, and incomplete graphs plus compact/MCP projections remain non-authoritative. Missing identity roots report zero confirmed results with stable degraded, confidence, authority, and strict-failure metadata, preserving the identity-based hotspot boundary introduced by #5084. + +## 日本語 + +- **exact C# caller / impact query が未解決の同名 call を confirmed graph evidence に昇格しないよう修正しました (#5183)** — exact C# callers と impact は解決済み target ID を必須とし、broad discovery では未解決の name evidence を維持して `name_discovery` と明示します。言語未指定の impact は C# を identity filter しつつ非 C# exact-name graph edge を維持し、非 C# 専用 root は `language_graph`、incomplete graph と compact / MCP projection は non-authoritative とします。identity root が無い場合は confirmed 0 件とし、安定した degraded、confidence、authority、strict failure metadata を返すことで、#5084 で導入した identity-based hotspot 境界も維持します。 diff --git a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs index 942675690f..7e55f5f6b6 100644 --- a/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs +++ b/src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs @@ -1473,7 +1473,28 @@ private static ResponseCount ResolveTotalCount( ? string.Equals(impactMode, "callers", StringComparison.Ordinal) : string.Equals(impactMode, "file_dependency_hints", StringComparison.Ordinal); if (!collectionIsActive) - return new ResponseCount(0, true); + { + var missingIdentityRoot = string.Equals( + extraction.SourcePayload?["identity_root_unavailable_reason"]?.GetValue(), + "no_identity_backed_root", + StringComparison.Ordinal); + var incompleteCallerGraph = extraction.PrimaryCollection == "callers" + && extraction.SourcePayload is not null + && TryReadBool(extraction.SourcePayload, "reference_graph_complete", out var graphComplete) + && !graphComplete; + var unavailableCallerRoot = extraction.PrimaryCollection == "callers" + && extraction.SourcePayload is not null + && TryReadBool(extraction.SourcePayload, "identity_root_available", out var identityRootAvailable) + && !identityRootAvailable; + var authoritative = (!missingIdentityRoot && !incompleteCallerGraph && !unavailableCallerRoot) + || extraction.SourcePayload == null + || !TryReadBool( + extraction.SourcePayload, + "authoritative_count", + out var explicitAuthority) + || explicitAuthority; + return new ResponseCount(0, authoritative); + } } if (command == "impact" && extraction.PrimaryCollection == "definitions") { diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs b/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs index 4c2af4e871..b4431037c6 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs @@ -104,6 +104,8 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions) WriteSqlGraphContractWarningIfNeeded(options.Json, sqlGraphSignal, reader, options); WriteHdlGraphContractWarningIfNeeded(options.Json, hdlGraphSignal); + if (!options.Json) + WriteImpactIdentityRootWarningIfNeeded(analysis); if (confirmedCount == 0 && !hasHeuristicHints) { @@ -447,6 +449,16 @@ private static void AddImpactDefinitionsJsonFields(JsonObject payload, ImpactAna private static void AddImpactTraversalRootJsonFields(JsonObject payload, ImpactAnalysisResult analysis) { payload["traversal_root_scope"] = analysis.TraversalRootScope; + payload["identity_root_available"] = analysis.IdentityRootAvailable; + payload["graph_evidence_confidence"] = analysis.GraphEvidenceConfidence; + payload["identity_root_resolution_truncated"] = analysis.IdentityRootResolutionTruncated; + if (analysis.IdentityRootUnavailableReason != null) + payload["identity_root_unavailable_reason"] = analysis.IdentityRootUnavailableReason; + payload["authoritative_count"] = analysis.CountIsAuthoritative; + if (!analysis.CountIsAuthoritative) + { + payload["degraded"] = true; + } if (analysis.TraversalPartialFamilyId == null) return; @@ -467,6 +479,16 @@ internal static void ApplyImpactCountAuthority(JsonObject payload, ImpactAnalysi payload["authoritative_count"] = false; } + private static void WriteImpactIdentityRootWarningIfNeeded(ImpactAnalysisResult analysis) + { + if (analysis.IdentityRootAvailable) + return; + + var reason = analysis.IdentityRootUnavailableReason ?? "unknown"; + CommandErrorWriter.WriteStderr( + $"WARN: impact traversal has no identity-backed root ({reason}); confirmed counts are not authoritative."); + } + private static List BuildImpactDefinitionJsonResults(IReadOnlyList definitions) => LogicalPartialSymbolGrouper.Group(definitions); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs index a70f53e924..ce96250348 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs @@ -201,15 +201,34 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions WriteHdlGraphContractWarningIfNeeded(options.Json, hdlGraphSignal); if (counts.Count == 0) { - WriteGraphCountResult(reader, 0, 0, options, jsonOptions, reader._hasReferencesTable, exactSignalForCount, exactZeroHintForCount, extraFields: payload => AddGraphContractJsonFields(payload, reader, jsonOptions, effectiveSqlGraphSignal, hdlGraphSignal)); + WriteGraphCountResult(reader, 0, 0, options, jsonOptions, reader._hasReferencesTable, exactSignalForCount, exactZeroHintForCount, extraFields: payload => + { + AddGraphContractJsonFields(payload, reader, jsonOptions, effectiveSqlGraphSignal, hdlGraphSignal); + AddCallerIdentityRootJsonFields(payload, counts); + if (!exact) + payload["graph_evidence_confidence"] = "name_discovery"; + }); + if (!options.Json) + WriteCallerIdentityRootWarningIfNeeded(counts); return CommandExitCodes.Success; } - WriteGraphCountResult(reader, counts.Count, counts.FileCount, options, jsonOptions, reader._hasReferencesTable, exactSignalForCount, extraFields: payload => AddGraphContractJsonFields(payload, reader, jsonOptions, effectiveSqlGraphSignal, hdlGraphSignal)); + WriteGraphCountResult(reader, counts.Count, counts.FileCount, options, jsonOptions, reader._hasReferencesTable, exactSignalForCount, extraFields: payload => + { + AddGraphContractJsonFields(payload, reader, jsonOptions, effectiveSqlGraphSignal, hdlGraphSignal); + AddCallerIdentityRootJsonFields(payload, counts); + if (!exact) + payload["graph_evidence_confidence"] = "name_discovery"; + }); + if (!options.Json) + WriteCallerIdentityRootWarningIfNeeded(counts); return CommandExitCodes.Success; } var results = reader.GetCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("callers"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads); + var callerIdentityCounts = exact + ? reader.CountCallersTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: true, options.RawKinds, options.IncludeQualifiedCommonCalls, options.IncludeMemberReads) + : (QueryCountResult?)null; if (options.IncludeBody && JsonEnvelopeWrapper.ShouldMaterializeBody("callers")) AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); ApplyBodyRecoveryCommands(results, options.DbPath, options.RedactPaths ?? true); @@ -224,12 +243,21 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions WriteExactGraphWarningIfNeeded(exact, options.Json, exactSignal, reader, options); WriteSqlGraphContractWarningIfNeeded(options.Json, sqlGraphSignal, reader, options); WriteHdlGraphContractWarningIfNeeded(options.Json, hdlGraphSignal); + if (!options.Json && callerIdentityCounts is { } humanCounts) + WriteCallerIdentityRootWarningIfNeeded(humanCounts); if (results.Count == 0) { if (options.Json && TryWriteEmptyFormattedResult(options, jsonOptions)) return ZeroResultExitCode(options); if (options.Json) - WriteGraphZeroJsonResult(reader, "callers", jsonOptions, graphAvailable: reader._hasReferencesTable, exact ? exactSignal : (ExactQuerySignal?)null, exactZeroHint, queryOptions: options, extraFields: payload => AddGraphContractJsonFields(payload, reader, jsonOptions, sqlGraphSignal, hdlGraphSignal)); + WriteGraphZeroJsonResult(reader, "callers", jsonOptions, graphAvailable: reader._hasReferencesTable, exact ? exactSignal : (ExactQuerySignal?)null, exactZeroHint, queryOptions: options, extraFields: payload => + { + AddGraphContractJsonFields(payload, reader, jsonOptions, sqlGraphSignal, hdlGraphSignal); + if (callerIdentityCounts is { } counts) + AddCallerIdentityRootJsonFields(payload, counts); + else + payload["graph_evidence_confidence"] = "name_discovery"; + }); else if (!options.Json) { CommandErrorWriter.WriteStderr(BuildZeroResultLine("No callers found", options)); @@ -269,12 +297,15 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions WriteGraphJsonResult(r, CliJsonSerializerContextFactory.Create(jsonOptions).CallerResult, exactSignal, jsonOptions, extraFields: payload => { AddGraphContractJsonFields(payload, reader, jsonOptions, sqlGraphSignal, hdlGraphSignal); + if (callerIdentityCounts is { } counts) + AddCallerIdentityRootJsonFields(payload, counts); AddReferenceRankingQueryContextJson(payload, options, jsonOptions); }); else WriteJsonResult(r, CliJsonSerializerContextFactory.Create(jsonOptions).CallerResult, jsonOptions, extraFields: payload => { AddGraphContractJsonFields(payload, reader, jsonOptions, sqlGraphSignal, hdlGraphSignal); + payload["graph_evidence_confidence"] = "name_discovery"; AddReferenceRankingQueryContextJson(payload, options, jsonOptions); }); } diff --git a/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs b/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs index 10d8b487c5..c017a57973 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs @@ -255,6 +255,51 @@ private static void WriteGraphCountResult(DbReader reader, int count, int files, Console.WriteLine(payload.ToJsonString(EnsureJsonNodeSerializerOptions(jsonOptions))); } + private static void AddCallerIdentityRootJsonFields( + JsonObject payload, + QueryCountResult counts) + { + if (counts.IdentityRootAvailable == null + && counts.GraphEvidenceConfidence == null) + return; + + payload["graph_evidence_confidence"] = counts.GraphEvidenceConfidence; + if (counts.IdentityRootAvailable == null) + return; + + payload["identity_root_available"] = counts.IdentityRootAvailable.Value; + payload["identity_root_resolution_truncated"] = counts.IdentityRootResolutionTruncated; + if (counts.IdentityRootUnavailableReason != null) + payload["identity_root_unavailable_reason"] = counts.IdentityRootUnavailableReason; + if (counts.IdentityRootAvailable == false || counts.IdentityRootResolutionTruncated) + { + payload["degraded"] = true; + payload["authoritative_count"] = false; + } + } + + private static void WriteCallerIdentityRootWarningIfNeeded(QueryCountResult counts) + { + if (counts.IdentityRootResolutionTruncated) + { + CommandErrorWriter.WriteStderr( + "WARN: exact caller identity resolution hit its safety cap; the count is partial and not authoritative."); + return; + } + if (counts.IdentityRootAvailable != false) + return; + + if (counts.GraphEvidenceConfidence == "name_fallback") + { + CommandErrorWriter.WriteStderr( + "WARN: exact callers are using the lower-confidence name fallback; the count is not authoritative."); + return; + } + + CommandErrorWriter.WriteStderr( + "WARN: no identity-backed exact caller root was found; use a broad callers query only for same-name discovery evidence."); + } + private static void WriteGraphZeroJsonResult(DbReader reader, string resultsKey, JsonSerializerOptions jsonOptions, bool graphAvailable, ExactQuerySignal? exactSignal, ExactZeroHintResult? exactZeroHint = null, GraphSupportOverride? graphSupportOverride = null, QueryCommandOptions? queryOptions = null, Action? extraFields = null) { diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index bca47b7143..b6baf04d2f 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -6,6 +6,13 @@ namespace CodeIndex.Database; public partial class DbReader { + private sealed record CallerIdentityResolution( + IReadOnlyList? SymbolIds, + bool? IdentityRootAvailable, + string? IdentityRootUnavailableReason, + string? GraphEvidenceConfidence, + bool ResolutionTruncated = false); + private static readonly string CSharpCommonQualifiedMemberCallNamesSql = string.Join( ", ", CSharpReferenceExtractor.CommonQualifiedMemberCallNames @@ -87,15 +94,13 @@ private List GetCallersCore(string query, int limit, string? lang, query = NormalizeSymbolSearchQuery(query, lang, exact) ?? query ?? string.Empty; if (!_hasReferencesTable) return new List(); - var callerIdentitySymbolIds = ResolveCallerIdentitySymbolIds( + var callerIdentity = ResolveCallerIdentity( query, lang, - pathPatterns, - excludePathPatterns, - excludeTests, exact, - includeQualifiedCommonCalls, targetSymbolId); + if (callerIdentity.SymbolIds is { Count: 0 } && lang != null) + return []; var request = CreateGraphReferenceQueryRequest( query, @@ -110,7 +115,7 @@ private List GetCallersCore(string query, int limit, string? lang, includeQualifiedCommonCalls, includeMemberReads, identitySymbolId: targetSymbolId, - callerIdentitySymbolIds, + callerIdentity.SymbolIds, excludeSelfReferences, offset); var plan = BuildGraphReferenceQueryPlan( @@ -129,15 +134,13 @@ public int CountCallers(string query, int limit = 20, string? lang = null, strin query = NormalizeSymbolSearchQuery(query, lang, exact) ?? query ?? string.Empty; if (!_hasReferencesTable) return 0; - var callerIdentitySymbolIds = ResolveCallerIdentitySymbolIds( + var callerIdentity = ResolveCallerIdentity( query, lang, - pathPatterns, - excludePathPatterns, - excludeTests, exact, - includeQualifiedCommonCalls, targetSymbolId: null); + if (callerIdentity.SymbolIds is { Count: 0 } && lang != null) + return 0; var request = CreateGraphReferenceQueryRequest( query, @@ -151,7 +154,7 @@ public int CountCallers(string query, int limit = 20, string? lang = null, strin rawKinds, includeQualifiedCommonCalls, includeMemberReads, - callerIdentitySymbolIds: callerIdentitySymbolIds); + callerIdentitySymbolIds: callerIdentity.SymbolIds); var plan = BuildGraphReferenceQueryPlan( CallerGraphReferenceDirection, request, @@ -190,15 +193,21 @@ private QueryCountResult CountCallersTotalCore( return new QueryCountResult(0, 0); lang = NormalizeQueryLanguage(lang); query = NormalizeSymbolSearchQuery(query, lang, exact) ?? query ?? string.Empty; - var callerIdentitySymbolIds = ResolveCallerIdentitySymbolIds( + var callerIdentity = ResolveCallerIdentity( query, lang, - pathPatterns, - excludePathPatterns, - excludeTests, exact, - includeQualifiedCommonCalls, targetSymbolId); + if (callerIdentity.SymbolIds is { Count: 0 } && lang != null) + { + return new QueryCountResult(0, 0) + { + IdentityRootAvailable = callerIdentity.IdentityRootAvailable, + IdentityRootUnavailableReason = callerIdentity.IdentityRootUnavailableReason, + GraphEvidenceConfidence = callerIdentity.GraphEvidenceConfidence, + IdentityRootResolutionTruncated = callerIdentity.ResolutionTruncated, + }; + } var request = CreateGraphReferenceQueryRequest( query, @@ -213,32 +222,73 @@ private QueryCountResult CountCallersTotalCore( includeQualifiedCommonCalls, includeMemberReads, identitySymbolId: targetSymbolId, - callerIdentitySymbolIds: callerIdentitySymbolIds); + callerIdentitySymbolIds: callerIdentity.SymbolIds); var plan = BuildGraphReferenceQueryPlan( CallerGraphReferenceDirection, request, GraphReferenceQueryShape.TotalCount); - return ExecuteGraphReferenceTotalCount(plan); + var result = ExecuteGraphReferenceTotalCount(plan); + return result with + { + IdentityRootAvailable = callerIdentity.IdentityRootAvailable, + IdentityRootUnavailableReason = callerIdentity.IdentityRootUnavailableReason, + GraphEvidenceConfidence = callerIdentity.GraphEvidenceConfidence, + IdentityRootResolutionTruncated = callerIdentity.ResolutionTruncated, + }; } - private IReadOnlyList? ResolveCallerIdentitySymbolIds( + private CallerIdentityResolution ResolveCallerIdentity( string query, string? lang, - IReadOnlyList? pathPatterns, - IReadOnlyList? excludePathPatterns, - bool excludeTests, bool exact, - bool includeQualifiedCommonCalls, long? targetSymbolId) { if (targetSymbolId is long candidateTargetSymbolId) - return [candidateTargetSymbolId]; - if (!HasCurrentReferenceIdentityContractForRead() - || !exact - || !SqlNameResolver.HasQualifier(query) - || lang is not (null or "csharp")) { - return null; + return new CallerIdentityResolution( + [candidateTargetSymbolId], + IdentityRootAvailable: true, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: "identity_backed"); + } + if (!exact) + { + return new CallerIdentityResolution( + SymbolIds: null, + IdentityRootAvailable: null, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: null); + } + // The persisted target-identity contract currently covers C# caller edges. + // Other graph languages retain their existing language-specific exact matching; + // treating their lack of C#-style candidates as an unresolved symbol would erase + // SQL, solution/MSBuild, stylesheet, and script graph semantics. + // 永続化された target identity 契約の対象は現時点では C# caller edge。 + // 他言語で C# 型 candidate が無いことを未解決扱いすると、SQL、solution/MSBuild、 + // stylesheet、script 固有の exact graph 意味論を消してしまうため従来経路を維持する。 + if (lang is not (null or "csharp")) + { + return new CallerIdentityResolution( + SymbolIds: null, + IdentityRootAvailable: null, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: null); + } + if (lang == null && ComputeCssScssVariableAlias(query) != null) + { + return new CallerIdentityResolution( + SymbolIds: null, + IdentityRootAvailable: null, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: null); + } + if (!HasCurrentReferenceIdentityContractForRead()) + { + return new CallerIdentityResolution( + SymbolIds: null, + IdentityRootAvailable: false, + IdentityRootUnavailableReason: "reference_identity_unavailable", + GraphEvidenceConfidence: "name_fallback"); } var resolution = ResolveImpactDefinitions( @@ -250,38 +300,60 @@ private QueryCountResult CountCallersTotalCore( excludeTests: false); if (resolution.Definitions.Count == 0) { - var leafName = SqlNameResolver.GetLeafName(query); - return includeQualifiedCommonCalls - && CSharpReferenceExtractor.CommonQualifiedMemberCallNames.Contains(leafName) - ? null - : []; + if (lang == null) + { + var languageResolution = ResolveImpactDefinitions( + query, + DefaultImpactGraphStateEntryBudget, + lang: null, + pathPatterns: null, + excludePathPatterns: null, + excludeTests: false); + if (languageResolution.Definitions.Count > 0) + { + return new CallerIdentityResolution( + SymbolIds: [], + IdentityRootAvailable: true, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: "language_graph"); + } + } + return new CallerIdentityResolution( + SymbolIds: [], + IdentityRootAvailable: false, + IdentityRootUnavailableReason: "no_identity_backed_root", + GraphEvidenceConfidence: "no_identity_root"); } var isLogicalPartialFamily = IsLogicalPartialFamilyRoot( hasResolvedIdentityGraph: true, resolution, resolution.Definitions); - var symbolIds = ResolveQualifiedCSharpRootIds( - canResolveQualifiedCSharpIdentity: true, - isLogicalPartialFamily, - resolution, - resolution.Definitions); - // A safety-budget hit means identity resolution is incomplete, not that the - // qualified target has no callers. Preserve the legacy name fallback instead - // of returning an authoritative empty identity set. - // safety budget 到達は解決途中を意味するため、0 件と断定せず旧 name fallback を維持する。 - if (resolution.PhysicalSymbolIdsTruncated) - return null; - symbolIds = ExpandCSharpPolymorphicDispatchSymbolIds( - query, - symbolIds, - pathPatterns: null, - excludePathPatterns: null, - excludeTests: false, - out var dispatchIdsTruncated); - if (dispatchIdsTruncated) - return null; - return symbolIds.Order().ToArray(); + var resolutionTruncated = resolution.PhysicalSymbolIdsTruncated + || (!isLogicalPartialFamily && resolution.LogicalCount > resolution.Definitions.Count); + + var symbolIds = resolution.PhysicalSymbolIds.ToHashSet(); + if (resolution.Definitions.All(static definition => definition.Lang == "csharp")) + { + symbolIds = ExpandCSharpPolymorphicDispatchSymbolIds( + query, + symbolIds, + pathPatterns: null, + excludePathPatterns: null, + excludeTests: false, + out var dispatchIdsTruncated); + if (dispatchIdsTruncated) + resolutionTruncated = true; + } + + return new CallerIdentityResolution( + symbolIds.Order().ToArray(), + IdentityRootAvailable: true, + IdentityRootUnavailableReason: null, + GraphEvidenceConfidence: resolutionTruncated + ? "identity_backed_partial" + : "identity_backed", + ResolutionTruncated: resolutionTruncated); } /// @@ -542,10 +614,12 @@ private List GetCallersExactCore(string symbolName, int limit, int var mutualRecursionSql = _referenceColumns.Contains("is_mutual_recursion") ? "r.is_mutual_recursion" : "0"; var sourceSymbolIdSql = _referenceColumns.Contains("source_symbol_id") ? "r.source_symbol_id" : "NULL"; var referenceSpanLengthSql = _referenceColumns.Contains("span_length") ? "r.span_length" : "NULL"; - var hasIdentityTargetScope = targetSymbolIds is { Count: > 0 } - && _referenceColumns.Contains("target_symbol_id") - && _referenceColumns.Contains("resolution_state") - && HasTable("symbol_reference_candidates"); + var hasIdentityTargetContract = targetSymbolIds != null + && _referenceColumns.Contains("target_symbol_id") + && _referenceColumns.Contains("resolution_state") + && HasTable("symbol_reference_candidates"); + var hasIdentityTargetScope = hasIdentityTargetContract + && targetSymbolIds is { Count: > 0 }; const string targetSymbolIdsSql = "SELECT CAST(value AS INTEGER) FROM json_each(@targetSymbolIdsJson)"; var targetSymbolIdSql = hasIdentityTargetScope ? $@"CASE @@ -581,33 +655,39 @@ ELSE NULL : _foldReady ? " OR (f.lang = 'csharp' AND r.symbol_name_folded IN (" + string.Join(", ", polymorphicCSharpSymbolNames.Select((_, i) => $"@polymorphicSymbolNameFolded{i}")) + "))" : " OR (f.lang = 'csharp' AND r.symbol_name COLLATE NOCASE IN (" + string.Join(", ", polymorphicCSharpSymbolNames.Select((_, i) => $"@polymorphicSymbolName{i}")) + "))"; - var nameCondition = _foldReady + var namePredicate = _foldReady ? allowSqlLeafFallback - ? @" - AND (" + BuildPersistedFoldedNameMatchSql("r.symbol_name_folded", "@symbolNameFolded") + " OR (f.lang = 'sql' AND r.symbol_name_folded = @symbolNameLeafFolded)" + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))" - : @" - AND (((f.lang = 'sql') AND sql_context_has_name_folded_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND " + BuildPersistedFoldedNameMatchSql("r.symbol_name_folded", "@symbolNameFolded") + ") OR " + BuildCSharpQualifiedContextFallbackSql(BuildQualifiedContextMatchSql(contextSql, "r.column_number", folded: true, like: false)) + " OR " + BuildQualifiedLeafFallbackSql("r.symbol_name", "r.symbol_name_folded", folded: true) + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))" + ? "(" + BuildPersistedFoldedNameMatchSql("r.symbol_name_folded", "@symbolNameFolded") + " OR (f.lang = 'sql' AND r.symbol_name_folded = @symbolNameLeafFolded)" + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))" + : "(((f.lang = 'sql') AND sql_context_has_name_folded_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND " + BuildPersistedFoldedNameMatchSql("r.symbol_name_folded", "@symbolNameFolded") + ") OR " + BuildCSharpQualifiedContextFallbackSql(BuildQualifiedContextMatchSql(contextSql, "r.column_number", folded: true, like: false)) + " OR " + BuildQualifiedLeafFallbackSql("r.symbol_name", "r.symbol_name_folded", folded: true) + polymorphicNameCondition + " OR (f.lang = 'solution' AND r.reference_kind = 'project_reference' AND r.container_name = @symbolName COLLATE NOCASE))" : allowSqlLeafFallback - ? @" - AND (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))" - : @" - AND (((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))"; + ? "(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))" + : "(((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))"; + var nameCondition = "\n AND " + namePredicate; // Identity-scoped traversal admits only references whose candidate set contains the // requested canonical target. Unresolved/ambiguous same-leaf rows remain available to // broad reference discovery, but they are not confirmed call-graph edges. // identity scope の traversal は candidate set が要求 target を含む参照だけを採用する。 // unresolved/ambiguous な同名 leaf は広い reference 探索には残すが、確定 call graph edge // としては扱わない。 - var targetCondition = hasIdentityTargetScope - ? $@" - AND EXISTS ( + var identityTargetPredicate = $@"EXISTS ( SELECT 1 FROM symbol_reference_candidates identity_candidate WHERE identity_candidate.reference_id = r.id AND identity_candidate.symbol_id IN ({targetSymbolIdsSql}) AND r.resolution_state IN ('resolved', 'resolved_group') - )" - : nameCondition; + )"; + var targetCondition = !hasIdentityTargetContract + ? nameCondition + : targetSymbolIds!.Count == 0 + ? lang == null + ? "\n AND f.lang != 'csharp'" + nameCondition + : "\n AND 1 = 0" + : lang == null + ? $@" + AND ((f.lang = 'csharp' AND {identityTargetPredicate}) + OR (f.lang != 'csharp' AND {namePredicate}))" + : $@" + AND {identityTargetPredicate}"; // impact BFS must share the call-graph contract with `callers`/`callees`/`hotspots`, // so event subscriptions (`Click += OnClick`) also participate in the transitive // caller chain. Metadata edges (`attribute`, `annotation`) stay excluded. @@ -800,7 +880,10 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i && definitions[0].PartialFamilyId != null ? definitions[0] : null; - var traversalRootScope = logicalPartialFamilyDefinition != null + var identityRootSignal = ResolveImpactIdentityRootSignal(definitionResolution, lang); + var traversalRootScope = identityRootSignal.UnavailableReason == "no_identity_backed_root" + ? "name_only" + : logicalPartialFamilyDefinition != null ? "logical_partial_family" : "symbol"; var partialFamilyMemberCount = logicalPartialFamilyDefinition != null @@ -818,6 +901,7 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i var partialFamilyMemberRootOmitted = logicalPartialFamilyDefinition != null ? Math.Max(0, definitionResolution.PhysicalCount - definitionResolution.PhysicalSymbolIds.Count) : (int?)null; + var referenceGraphComplete = IsReferenceGraphComplete(GetReferenceExtractionCapHits()); if (maxDepth <= 0) { @@ -826,7 +910,7 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i Query = symbolName, ResolvedName = resolvedName, ImpactMode = "none", - Heuristic = false, + Heuristic = !identityRootSignal.Available, MaxDepth = maxDepth, DefinitionCount = definitionResolution.PhysicalCount, DefinitionFileCount = definitionResolution.PhysicalFileCount, @@ -836,6 +920,10 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i HasMultipleDefinitions = hasMultipleDefinitions, HasMultipleDefinitionFiles = definitionResolution.PhysicalFileCount > 1, TraversalRootScope = traversalRootScope, + IdentityRootAvailable = identityRootSignal.Available, + IdentityRootUnavailableReason = identityRootSignal.UnavailableReason, + GraphEvidenceConfidence = identityRootSignal.EvidenceConfidence, + IdentityRootResolutionTruncated = identityRootSignal.ResolutionTruncated, TraversalPartialFamilyId = logicalPartialFamilyDefinition?.PartialFamilyId, PartialFamilyMemberCount = partialFamilyMemberCount, PartialFamilyMemberRootCount = partialFamilyMemberRootCount, @@ -851,9 +939,12 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i CycleDetected = false, Cycles = null, GraphTableAvailable = _hasReferencesTable, + ReferenceGraphComplete = referenceGraphComplete, ZeroResultReason = definitionResolution.PhysicalCount == 0 ? "no_matching_definition" : "depth_requested_zero", ImpactFailureChain = definitionResolution.PhysicalCount == 0 - ? ["definition_not_found", "depth_requested_zero"] + ? identityRootSignal.UnavailableReason == "no_identity_backed_root" + ? ["no_identity_backed_root", "definition_not_found", "depth_requested_zero"] + : ["definition_not_found", "depth_requested_zero"] : ["depth_requested_zero"], SuggestionType = definitionResolution.PhysicalCount == 0 ? "resolution" : "precondition", Suggestion = definitionResolution.PhysicalCount == 0 @@ -879,12 +970,16 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i List? impactFailureChain = null; string? suggestionType = null; string? suggestion = null; - var heuristic = false; + var heuristic = !identityRootSignal.Available; if (callers.Count == 0 && !callerExistsBeforeOffset) { impactMode = "none"; impactFailureChain = []; + if (identityRootSignal.UnavailableReason == "no_identity_backed_root") + { + impactFailureChain.Add(identityRootSignal.UnavailableReason); + } if (!_hasReferencesTable) { @@ -1012,6 +1107,10 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i HasMultipleDefinitions = hasMultipleDefinitions, HasMultipleDefinitionFiles = definitionResolution.PhysicalFileCount > 1, TraversalRootScope = traversalRootScope, + IdentityRootAvailable = identityRootSignal.Available, + IdentityRootUnavailableReason = identityRootSignal.UnavailableReason, + GraphEvidenceConfidence = identityRootSignal.EvidenceConfidence, + IdentityRootResolutionTruncated = identityRootSignal.ResolutionTruncated, TraversalPartialFamilyId = logicalPartialFamilyDefinition?.PartialFamilyId, PartialFamilyMemberCount = partialFamilyMemberCount, PartialFamilyMemberRootCount = partialFamilyMemberRootCount, @@ -1027,6 +1126,7 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i CycleDetected = cycles.Count > 0, Cycles = cycles.Count > 0 ? cycles : null, GraphTableAvailable = _hasReferencesTable, + ReferenceGraphComplete = referenceGraphComplete, ZeroResultReason = zeroResultReason, ImpactFailureChain = impactFailureChain is { Count: > 0 } ? impactFailureChain : null, SuggestionType = suggestionType, diff --git a/src/CodeIndex/Database/DbReader.ImpactTraversal.cs b/src/CodeIndex/Database/DbReader.ImpactTraversal.cs index 2c9fe6adf6..981c62df4b 100644 --- a/src/CodeIndex/Database/DbReader.ImpactTraversal.cs +++ b/src/CodeIndex/Database/DbReader.ImpactTraversal.cs @@ -46,6 +46,12 @@ private sealed record ImpactTraversalRoot( string NodeKey, ImpactPathNode? PathNode); + private sealed record ImpactIdentityRootSignal( + bool Available, + string? UnavailableReason, + string EvidenceConfidence, + bool ResolutionTruncated = false); + private readonly record struct ImpactTraversalFrontierNode( string Symbol, long? SymbolId, @@ -113,15 +119,19 @@ private readonly record struct ImpactBoundaryInspection( { var resolvedName = ResolveSymbolName(request.SymbolName, request.Lang); var hasResolvedIdentityGraph = HasCurrentReferenceIdentityContractForRead(); - var canResolveQualifiedCSharpIdentity = - hasResolvedIdentityGraph - && SqlNameResolver.HasQualifier(request.SymbolName) + var canResolveCSharpIdentity = hasResolvedIdentityGraph && request.Lang is null or "csharp"; - var rootDefinitionLimit = canResolveQualifiedCSharpIdentity + var rootDefinitionLimit = canResolveCSharpIdentity ? DefaultImpactGraphStateEntryBudget : request.Limit; var resolution = ResolveImpactRootDefinitions(request, resolvedName, rootDefinitionLimit); var definitions = resolution.Definitions; + var useCSharpIdentity = canResolveCSharpIdentity + && (definitions.Count == 0 + || definitions.All(static definition => definition.Lang == "csharp")); + if (useCSharpIdentity && definitions.Count == 0 && request.Lang != null) + return null; + var definitionPaths = definitions .Select(static definition => definition.Path) .ToHashSet(StringComparer.OrdinalIgnoreCase); @@ -129,38 +139,40 @@ private readonly record struct ImpactBoundaryInspection( hasResolvedIdentityGraph, resolution, definitions); - var qualifiedCSharpIds = ResolveQualifiedCSharpRootIds( - canResolveQualifiedCSharpIdentity, - isLogicalPartialFamily, - resolution, - definitions); - var traversalCSharpIds = qualifiedCSharpIds.ToHashSet(); + var identityIds = useCSharpIdentity + ? resolution.PhysicalSymbolIds.ToHashSet() + : []; + if (useCSharpIdentity && identityIds.Count == 0 && definitions.Count > 0) + return null; + + var traversalIds = identityIds.ToHashSet(); var dispatchIdsTruncated = false; - if (canResolveQualifiedCSharpIdentity && qualifiedCSharpIds.Count > 0) + if (useCSharpIdentity) { - traversalCSharpIds = ExpandCSharpPolymorphicDispatchSymbolIds( + traversalIds = ExpandCSharpPolymorphicDispatchSymbolIds( request.SymbolName, - traversalCSharpIds, + traversalIds, request.PathPatterns, request.ExcludePathPatterns, request.ExcludeTests, out dispatchIdsTruncated); } - if (hasResolvedIdentityGraph && definitionPaths.Count > 1 && qualifiedCSharpIds.Count == 0) + // Preserve the pre-existing ambiguity boundary for cross-language and other + // non-identity roots. Same-language C# definitions are safely unioned by ID. + // cross-language および identity 非対応 root の従来 ambiguity 境界を維持する。 + // 同一言語の C# definition 群だけは ID の和集合として安全に辿れる。 + if (hasResolvedIdentityGraph && definitionPaths.Count > 1 && !useCSharpIdentity) return null; var ambiguousMRootId = ResolveAmbiguousMRootId( hasResolvedIdentityGraph, request.Lang, definitions); - var identityIds = qualifiedCSharpIds.Count > 0 - ? qualifiedCSharpIds - : ambiguousMRootId is long ambiguousId - ? new HashSet { ambiguousId } - : []; - var initialTargetSymbolIds = traversalCSharpIds.Count > qualifiedCSharpIds.Count - ? traversalCSharpIds.Order().ToArray() + if (ambiguousMRootId is long ambiguousId) + identityIds.Add(ambiguousId); + var initialTargetSymbolIds = traversalIds.Count > 0 + ? traversalIds.Order().ToArray() : null; var singleIdentityId = identityIds.Count == 1 ? identityIds.Single() : (long?)null; var rootNodeKey = identityIds.Count > 1 @@ -183,9 +195,11 @@ private readonly record struct ImpactBoundaryInspection( identityIds, initialTargetSymbolIds, ambiguousMRootId != null, - !isLogicalPartialFamily - && qualifiedCSharpIds.Count > 0 - && (resolution.PhysicalSymbolIdsTruncated || dispatchIdsTruncated), + InitiallyTruncated: useCSharpIdentity + && !isLogicalPartialFamily + && (resolution.PhysicalSymbolIdsTruncated + || resolution.LogicalCount > definitions.Count + || dispatchIdsTruncated), rootNodeKey, pathNode); } @@ -216,6 +230,51 @@ private ImpactDefinitionResolution ResolveImpactRootDefinitions( return resolution; } + private ImpactIdentityRootSignal ResolveImpactIdentityRootSignal( + ImpactDefinitionResolution resolution, + string? lang) + { + if (lang is not (null or "csharp")) + { + return new ImpactIdentityRootSignal( + Available: true, + UnavailableReason: null, + EvidenceConfidence: "language_graph"); + } + if (lang == null + && resolution.Definitions.Count > 0 + && resolution.Definitions.All(static definition => definition.Lang != "csharp")) + { + return new ImpactIdentityRootSignal( + Available: true, + UnavailableReason: null, + EvidenceConfidence: "language_graph"); + } + if (!HasCurrentReferenceIdentityContractForRead()) + { + return new ImpactIdentityRootSignal( + Available: false, + UnavailableReason: "reference_identity_unavailable", + EvidenceConfidence: "name_fallback"); + } + if (resolution.PhysicalCount == 0) + { + return new ImpactIdentityRootSignal( + Available: false, + UnavailableReason: "no_identity_backed_root", + EvidenceConfidence: "no_identity_root"); + } + var resolutionTruncated = resolution.PhysicalSymbolIdsTruncated; + + return new ImpactIdentityRootSignal( + Available: true, + UnavailableReason: null, + EvidenceConfidence: resolutionTruncated + ? "identity_backed_partial" + : "identity_backed", + ResolutionTruncated: resolutionTruncated); + } + private static bool IsLogicalPartialFamilyRoot( bool hasResolvedIdentityGraph, ImpactDefinitionResolution resolution, @@ -227,19 +286,6 @@ private static bool IsLogicalPartialFamilyRoot( && definitions[0].PartialFamilyId != null && resolution.PhysicalSymbolIds.Count > 0; - private static HashSet ResolveQualifiedCSharpRootIds( - bool canResolveQualifiedCSharpIdentity, - bool isLogicalPartialFamily, - ImpactDefinitionResolution resolution, - IReadOnlyList definitions) - => (canResolveQualifiedCSharpIdentity || isLogicalPartialFamily) - && definitions.Count > 0 - && definitions.All(static definition => definition.Lang == "csharp") - && definitions.All(static definition => definition.SymbolId != null) - && (isLogicalPartialFamily || resolution.LogicalCount == definitions.Count) - ? resolution.PhysicalSymbolIds.ToHashSet() - : []; - private static long? ResolveAmbiguousMRootId( bool hasResolvedIdentityGraph, string? lang, @@ -292,7 +338,9 @@ private List ReadImpactCallerPage( ? node.TargetSymbolIds : node.SymbolId is long symbolId ? [symbolId] - : null; + : request.Lang == null && HasCurrentReferenceIdentityContractForRead() + ? [] + : null; return GetCallersExactCore( node.Symbol, pageSize, diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs index c7870a6a77..3c4e2001ba 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs @@ -168,7 +168,8 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) { if (countOnly) { - var countOnlyTotal = reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count; + var countResult = reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads); + var countOnlyTotal = countResult.Count; var histogramResults = countOnlyTotal > 0 ? reader.GetCallers(query, Math.Min(countOnlyTotal, MaxLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads) : []; @@ -190,15 +191,22 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) pathPatterns, excludePaths, excludeTests); + AddCallerIdentityRootSignal(countOnlyPayload, countResult); + if (!exact) + countOnlyPayload["graph_evidence_confidence"] = "name_discovery"; adjustments.ApplyTo(countOnlyPayload); return CreateToolResult(id, $"Counted {ConsoleUi.Counted(countOnlyTotal, "caller")}.", countOnlyPayload); } var results = reader.GetCallers(query, FetchLimitForEnvelope(limit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, offset: offset, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads); var truncated = TrimToRequestedLimit(results, limit); - var total = truncated || offset > 0 + var callerIdentityCounts = exact + ? reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact: true, rawKinds, includeQualifiedCommonCalls, includeMemberReads) + : (QueryCountResult?)null; + var total = callerIdentityCounts?.Count + ?? (truncated || offset > 0 ? reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count - : results.Count; + : results.Count); var graphSupport = ResolveGraphSupport(reader, exact, query, lang, pathPatterns, excludePaths, excludeTests); var sqlGraphSignal = QueryCommandRunner.NarrowSqlGraphContractSignalByLanguages( reader.GetSqlGraphContractSignal(lang, pathPatterns, excludePaths, excludeTests), @@ -243,6 +251,10 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) pathPatterns, excludePaths, excludeTests); + if (callerIdentityCounts is { } counts) + AddCallerIdentityRootSignal(payload, counts); + else + payload["graph_evidence_confidence"] = "name_discovery"; if (results.Count == 0) { AddExactZeroHint(payload, exactZeroHint); @@ -265,6 +277,30 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) }); } + private static void AddCallerIdentityRootSignal( + JsonObject payload, + QueryCountResult counts) + { + if (counts.IdentityRootAvailable == null + && counts.GraphEvidenceConfidence == null) + return; + + payload["graph_evidence_confidence"] = counts.GraphEvidenceConfidence; + if (counts.IdentityRootAvailable == null) + return; + + payload["identity_root_available"] = counts.IdentityRootAvailable.Value; + payload["identity_root_resolution_truncated"] = counts.IdentityRootResolutionTruncated; + if (counts.IdentityRootUnavailableReason != null) + payload["identity_root_unavailable_reason"] = counts.IdentityRootUnavailableReason; + + var degraded = counts.IdentityRootAvailable == false + || counts.IdentityRootResolutionTruncated + || payload["degraded"]?.GetValue() == true; + payload["degraded"] = degraded; + payload["authoritative_count"] = !degraded; + } + private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) { if (!TryReadRequiredStringParameter(args, "query", out var query, out var requiredError)) diff --git a/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs b/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs index f9cb18ce64..3c81e05721 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs @@ -200,6 +200,16 @@ private JsonNode ExecuteImpactAnalysis(JsonNode? id, JsonNode? args) private static void AddImpactTraversalRootFields(JsonObject payload, ImpactAnalysisResult analysis) { payload["traversal_root_scope"] = analysis.TraversalRootScope; + payload["identity_root_available"] = analysis.IdentityRootAvailable; + payload["graph_evidence_confidence"] = analysis.GraphEvidenceConfidence; + payload["identity_root_resolution_truncated"] = analysis.IdentityRootResolutionTruncated; + if (analysis.IdentityRootUnavailableReason != null) + payload["identity_root_unavailable_reason"] = analysis.IdentityRootUnavailableReason; + payload["authoritative_count"] = analysis.CountIsAuthoritative; + if (!analysis.CountIsAuthoritative) + { + payload["degraded"] = true; + } if (analysis.TraversalPartialFamilyId == null) return; diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index 0141265ca9..2869dcedd5 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -66,7 +66,11 @@ public readonly record struct QueryCountResult( double? AverageBytes = null, long? MaxBytes = null, string? MaxBytesPath = null, - bool? BytesAuthoritative = null); + bool? BytesAuthoritative = null, + [property: JsonIgnore] bool? IdentityRootAvailable = null, + [property: JsonIgnore] string? IdentityRootUnavailableReason = null, + [property: JsonIgnore] string? GraphEvidenceConfidence = null, + [property: JsonIgnore] bool IdentityRootResolutionTruncated = false); public readonly record struct UnusedCountResult( int Count, @@ -1231,6 +1235,11 @@ public class ImpactAnalysisResult public bool HasMultipleDefinitions { get; set; } public bool HasMultipleDefinitionFiles { get; set; } public string TraversalRootScope { get; set; } = "symbol"; + public bool IdentityRootAvailable { get; set; } = true; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? IdentityRootUnavailableReason { get; set; } + public string GraphEvidenceConfidence { get; set; } = "identity_backed"; + public bool IdentityRootResolutionTruncated { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? TraversalPartialFamilyId { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1244,7 +1253,11 @@ public class ImpactAnalysisResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? PartialFamilyMemberRootOmitted { get; set; } [JsonIgnore] - internal bool CountIsAuthoritative => !Truncated && PartialFamilyMemberRootTruncated != true; + internal bool CountIsAuthoritative => IdentityRootAvailable + && ReferenceGraphComplete + && !Truncated + && !IdentityRootResolutionTruncated + && PartialFamilyMemberRootTruncated != true; public List Definitions { get; set; } = []; public List Callers { get; set; } = []; public List FileImpacts { get; set; } = []; @@ -1276,6 +1289,8 @@ public class ImpactAnalysisResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? TruncatedReason { get; set; } public bool GraphTableAvailable { get; set; } = true; + [JsonIgnore] + internal bool ReferenceGraphComplete { get; set; } = true; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ZeroResultReason { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/tests/CodeIndex.Tests/DbReaderImpactTests.cs b/tests/CodeIndex.Tests/DbReaderImpactTests.cs index 1bcd291ea0..e1dbe11401 100644 --- a/tests/CodeIndex.Tests/DbReaderImpactTests.cs +++ b/tests/CodeIndex.Tests/DbReaderImpactTests.cs @@ -608,37 +608,16 @@ public void AnalyzeImpact_UserLimitTruncation_PropagatesTruncatedReason() // #1533: AnalyzeImpact は GetTransitiveCallers の truncated_reason を // そのまま伝搬して CLI/MCP 側で適切な再試行ガイダンスを出せるようにする。 const int callerCount = 6; + InsertIndexedFile( + "src/impact_limit_target.py", + "python", + "def widget_op():\n return True\n"); for (int i = 0; i < callerCount; i++) { - var callerFileId = _writer.UpsertFile(new FileRecord - { - Path = $"src/impact_limit_caller_{i:D2}.py", - Lang = "python", - Size = 96, - Lines = 2, - Modified = new DateTime(2026, 5, 15, 0, 0, 0, DateTimeKind.Utc), - }); - _writer.InsertChunks([new ChunkRecord - { - FileId = callerFileId, - ChunkIndex = 0, - StartLine = 1, - EndLine = 2, - Content = $"def impact_caller_{i:D2}():\n return widget_op()\n", - }]); - _writer.InsertReferences([ - new ReferenceRecord - { - FileId = callerFileId, - SymbolName = "widget_op", - ReferenceKind = "call", - Line = 2, - Column = 12, - Context = "return widget_op()", - ContainerKind = "function", - ContainerName = $"impact_caller_{i:D2}", - }, - ]); + InsertIndexedFile( + $"src/impact_limit_caller_{i:D2}.py", + "python", + $"def impact_caller_{i:D2}():\n return widget_op()\n"); } var analysis = _reader.AnalyzeImpact("widget_op", maxDepth: 1, limit: 2); diff --git a/tests/CodeIndex.Tests/DbReaderIssue5183Tests.cs b/tests/CodeIndex.Tests/DbReaderIssue5183Tests.cs new file mode 100644 index 0000000000..564c22155b --- /dev/null +++ b/tests/CodeIndex.Tests/DbReaderIssue5183Tests.cs @@ -0,0 +1,308 @@ +namespace CodeIndex.Tests; + +public partial class DbReaderTests +{ + [Fact] + public void ExactCallerAndImpactQueries_RequireIdentityBackedRoots_Issue5183() + { + InsertIssue5183GraphFixture(); + + var missingExact = _reader.GetCallers( + "MissingLeaf5183", + lang: "csharp", + exact: true, + pathPatterns: ["src/issue5183/*"]); + var missingCount = _reader.CountCallersTotal( + "MissingLeaf5183", + lang: "csharp", + exact: true, + pathPatterns: ["src/issue5183/*"]); + var missingBroad = _reader.GetCallers( + "MissingLeaf5183", + lang: "csharp", + exact: false, + pathPatterns: ["src/issue5183/*"]); + + Assert.Empty(missingExact); + Assert.Equal(0, missingCount.Count); + Assert.False(missingCount.IdentityRootAvailable); + Assert.Equal("no_identity_backed_root", missingCount.IdentityRootUnavailableReason); + Assert.Equal("no_identity_root", missingCount.GraphEvidenceConfidence); + Assert.Contains(missingBroad, caller => caller.CallerName == "MiddleMissing5183"); + + var resolved = _reader.GetCallers( + "IdentityLeaf5183", + lang: "csharp", + exact: true, + pathPatterns: ["src/issue5183/*"]); + var resolvedCaller = Assert.Single(resolved); + Assert.Equal("CallResolved5183", resolvedCaller.CallerName); + Assert.Equal(1, resolvedCaller.ReferenceCount); + var resolvedCount = _reader.CountCallersTotal( + "IdentityLeaf5183", + lang: "csharp", + exact: true, + pathPatterns: ["src/issue5183/*"]); + Assert.True(resolvedCount.IdentityRootAvailable); + Assert.Equal("identity_backed", resolvedCount.GraphEvidenceConfidence); + + var collisions = _reader.GetCallers( + "CollisionLeaf5183", + lang: "csharp", + exact: true, + pathPatterns: ["src/issue5183/*"]); + Assert.Equal( + ["CallFirst5183", "CallSecond5183"], + collisions.Select(caller => caller.CallerName).Order().ToArray()); + + var missingImpact = _reader.AnalyzeImpact( + "MissingLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.Empty(missingImpact.Callers); + Assert.Equal(0, missingImpact.DefinitionCount); + Assert.False(missingImpact.IdentityRootAvailable); + Assert.Equal("no_identity_backed_root", missingImpact.IdentityRootUnavailableReason); + Assert.Equal("no_identity_root", missingImpact.GraphEvidenceConfidence); + Assert.True(missingImpact.Heuristic); + Assert.False(missingImpact.CountIsAuthoritative); + Assert.Contains("no_identity_backed_root", missingImpact.ImpactFailureChain!); + + var resolvedImpact = _reader.AnalyzeImpact( + "Issue5183.ResolvedTarget5183.IdentityLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.True(resolvedImpact.IdentityRootAvailable); + Assert.Equal( + ["AboveResolved5183", "CallResolved5183"], + resolvedImpact.Callers.Select(caller => caller.CallerName).Order().ToArray()); + + var sameNameImpact = _reader.AnalyzeImpact( + "CollisionLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.True(sameNameImpact.IdentityRootAvailable); + Assert.Equal( + ["CallFirst5183", "CallSecond5183"], + sameNameImpact.Callers.Select(caller => caller.CallerName).Order().ToArray()); + + var multilingualCallers = _reader.GetCallers( + "IdentityLeaf5183", + exact: true, + pathPatterns: ["src/issue5183/*"]); + Assert.Equal( + ["CallResolved5183", "PythonCallsCSharp5183"], + multilingualCallers.Select(caller => caller.CallerName).Order().ToArray()); + + var multilingualImpact = _reader.AnalyzeImpact( + "IdentityLeaf5183", + maxDepth: 5, + limit: 20, + pathPatterns: ["src/issue5183/*"]); + Assert.Equal("identity_backed", multilingualImpact.GraphEvidenceConfidence); + Assert.Contains(multilingualImpact.Callers, caller => caller.CallerName == "CallResolved5183"); + Assert.Contains(multilingualImpact.Callers, caller => caller.CallerName == "PythonCallsCSharp5183"); + Assert.DoesNotContain(multilingualImpact.Callers, caller => caller.CallerName == "CallUnresolvedSameLeaf5183"); + + var pythonOnlyCallers = _reader.GetCallers( + "PythonOnlyLeaf5183", + exact: true, + pathPatterns: ["src/issue5183/*"]); + var pythonOnlyCaller = Assert.Single(pythonOnlyCallers); + Assert.Equal("PythonCaller5183", pythonOnlyCaller.CallerName); + var pythonOnlyCount = _reader.CountCallersTotal( + "PythonOnlyLeaf5183", + exact: true, + pathPatterns: ["src/issue5183/*"]); + Assert.True(pythonOnlyCount.IdentityRootAvailable); + Assert.Equal("language_graph", pythonOnlyCount.GraphEvidenceConfidence); + + var pythonOnlyImpact = _reader.AnalyzeImpact( + "PythonOnlyLeaf5183", + maxDepth: 5, + limit: 20, + pathPatterns: ["src/issue5183/*"]); + Assert.Equal("language_graph", pythonOnlyImpact.GraphEvidenceConfidence); + Assert.True(pythonOnlyImpact.IdentityRootAvailable); + Assert.True(pythonOnlyImpact.CountIsAuthoritative); + Assert.Equal( + ["PythonCaller5183", "PythonTop5183"], + pythonOnlyImpact.Callers.Select(caller => caller.CallerName).Order().ToArray()); + Assert.DoesNotContain(pythonOnlyImpact.Callers, caller => caller.CallerName == "CallUnresolvedPythonLeaf5183"); + + var pythonExternalImpact = _reader.AnalyzeImpact( + "ExternalOnlyLeaf5183", + maxDepth: 5, + limit: 20, + pathPatterns: ["src/issue5183/*"]); + Assert.False(pythonExternalImpact.IdentityRootAvailable); + Assert.Equal("no_identity_backed_root", pythonExternalImpact.IdentityRootUnavailableReason); + Assert.False(pythonExternalImpact.CountIsAuthoritative); + Assert.Equal( + ["PythonExternalCaller5183", "PythonExternalTop5183"], + pythonExternalImpact.Callers.Select(caller => caller.CallerName).Order().ToArray()); + Assert.DoesNotContain( + pythonExternalImpact.Callers, + caller => caller.CallerName == "CallUnresolvedExternalOnly5183"); + + _reader.ImpactGraphStateEntryBudgetForTesting = 1; + try + { + var cappedResolvedImpact = _reader.AnalyzeImpact( + "Issue5183.ResolvedTarget5183.IdentityLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.Empty(cappedResolvedImpact.Callers); + Assert.True(cappedResolvedImpact.IdentityRootAvailable); + Assert.True(cappedResolvedImpact.Truncated); + Assert.Equal( + global::CodeIndex.Database.ImpactTruncatedReasons.GraphStateBudget, + cappedResolvedImpact.TruncatedReason); + Assert.Equal( + global::CodeIndex.Database.ImpactTerminationReasons.GraphStateBudget, + cappedResolvedImpact.TerminationReason); + Assert.False(cappedResolvedImpact.CountIsAuthoritative); + } + finally + { + _reader.ImpactGraphStateEntryBudgetForTesting = null; + } + + _writer.MarkIndexIncomplete(["reference_count_exceeded"]); + using var incompleteReader = new global::CodeIndex.Database.DbReader(_db.Connection); + var incompleteImpact = incompleteReader.AnalyzeImpact( + "IdentityLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.True(incompleteImpact.IdentityRootAvailable); + Assert.False(incompleteImpact.ReferenceGraphComplete); + Assert.False(incompleteImpact.CountIsAuthoritative); + + _writer.ClearReferenceIdentityContractReady(); + using var staleReader = new global::CodeIndex.Database.DbReader(_db.Connection); + var staleImpact = staleReader.AnalyzeImpact( + "MissingLeaf5183", + maxDepth: 5, + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"]); + Assert.NotEmpty(staleImpact.Callers); + Assert.False(staleImpact.IdentityRootAvailable); + Assert.Equal("reference_identity_unavailable", staleImpact.IdentityRootUnavailableReason); + Assert.Equal("name_fallback", staleImpact.GraphEvidenceConfidence); + Assert.True(staleImpact.Heuristic); + Assert.False(staleImpact.CountIsAuthoritative); + } + + [Fact] + public void InspectAndHotspots_DoNotPromoteUnresolvedSameLeafCalls_Issue5183() + { + InsertIssue5183GraphFixture(); + + var inspect = _reader.AnalyzeSymbol( + "MissingLeaf5183", + limit: 20, + lang: "csharp", + pathPatterns: ["src/issue5183/*"], + exact: true); + + Assert.Empty(inspect.Definitions); + Assert.NotEmpty(inspect.References); + Assert.All(inspect.References, reference => Assert.Equal("unresolved", reference.ResolutionState)); + Assert.Empty(inspect.Callers); + Assert.Equal(0, inspect.GraphSections.Callers.Total); + Assert.Equal("query_fallback", inspect.GraphScope); + + var hotspots = _reader.GetSymbolHotspots( + 50, + "function", + "csharp", + ["src/issue5183/*"], + excludePathPatterns: null, + excludeTests: false); + var resolvedHotspot = Assert.Single( + hotspots, + hotspot => hotspot.Symbol.Name == "IdentityLeaf5183"); + Assert.Equal(1, resolvedHotspot.ReferenceCount); + Assert.DoesNotContain( + hotspots, + hotspot => hotspot.Symbol.Name == "MissingLeaf5183"); + } + + private void InsertIssue5183GraphFixture() + { + InsertIndexedFile( + "src/issue5183/Targets.cs", + "csharp", + """ + namespace Issue5183; + + public class ResolvedTarget5183 + { + public void IdentityLeaf5183() { } + } + + public class FirstTarget5183 + { + public void CollisionLeaf5183() { } + } + + public class SecondTarget5183 + { + public void CollisionLeaf5183() { } + } + """); + InsertIndexedFile( + "src/issue5183/Callers.cs", + "csharp", + """ + namespace Issue5183; + + public class CallerFixture5183 + { + public void CallResolved5183(ResolvedTarget5183 target) => target.IdentityLeaf5183(); + public void AboveResolved5183() => CallResolved5183(new ResolvedTarget5183()); + public void CallUnresolvedSameLeaf5183() => ExternalApi5183.IdentityLeaf5183(); + public void CallUnresolvedPythonLeaf5183() => ExternalApi5183.PythonOnlyLeaf5183(); + public void CallUnresolvedExternalOnly5183() => ExternalApi5183.ExternalOnlyLeaf5183(); + public void CallFirst5183(FirstTarget5183 target) => target.CollisionLeaf5183(); + public void CallSecond5183(SecondTarget5183 target) => target.CollisionLeaf5183(); + public void CallUnresolvedCollision5183() => ExternalApi5183.CollisionLeaf5183(); + public void TopMissing5183() => MiddleMissing5183(); + public void MiddleMissing5183() => ExternalApi5183.MissingLeaf5183(); + } + """); + InsertIndexedFile( + "src/issue5183/callers.py", + "python", + """ + def PythonOnlyLeaf5183(): + pass + + def PythonCaller5183(): + return PythonOnlyLeaf5183() + + def PythonTop5183(): + return PythonCaller5183() + + def PythonCallsCSharp5183(): + return IdentityLeaf5183() + + def PythonExternalCaller5183(): + return ExternalOnlyLeaf5183() + + def PythonExternalTop5183(): + return PythonExternalCaller5183() + """); + } +} diff --git a/tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs b/tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs index 7ed2163dd6..4aed2427f2 100644 --- a/tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs +++ b/tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs @@ -276,17 +276,17 @@ public static void InvokePrimary() Assert.Equal(2, primaryHotspot.ReferenceCount); Assert.Equal(1, secondaryHotspot.ReferenceCount); - var broadCallers = _reader.GetCallers( + var exactCallers = _reader.GetCallers( "Open5084", limit: 20, lang: "csharp", pathPatterns: ["src/identity5084/*"], exact: true, includeQualifiedCommonCalls: true); - Assert.Equal(3, broadCallers.Count); - Assert.Contains(broadCallers, caller => caller.CallerName == "InvokePrimary"); - Assert.Contains(broadCallers, caller => caller.CallerName == "InvokeSecondary"); - Assert.Contains(broadCallers, caller => caller.CallerName == "InvokeExternal"); + Assert.Equal(2, exactCallers.Count); + Assert.Contains(exactCallers, caller => caller.CallerName == "InvokePrimary"); + Assert.Contains(exactCallers, caller => caller.CallerName == "InvokeSecondary"); + Assert.DoesNotContain(exactCallers, caller => caller.CallerName == "InvokeExternal"); } [Fact] @@ -479,16 +479,24 @@ FROM symbol_reference_candidates AS candidate lang: "csharp", pathPatterns: ["src/identity/AmbiguousCaller.cs"], exact: true); - var caller = Assert.Single(callers); - Assert.Equal("Invoke", caller.CallerName); - Assert.False(caller.HasSelfReference); - Assert.False(caller.HasMutualRecursion); - Assert.Equal(1, _reader.CountCallers( + Assert.Empty(callers); + Assert.Equal(0, _reader.CountCallers( "Process", limit: 20, lang: "csharp", pathPatterns: ["src/identity/AmbiguousCaller.cs"], exact: true)); + + var discoveryCallers = _reader.GetCallers( + "Process", + limit: 20, + lang: "csharp", + pathPatterns: ["src/identity/AmbiguousCaller.cs"], + exact: false); + var caller = Assert.Single(discoveryCallers); + Assert.Equal("Invoke", caller.CallerName); + Assert.False(caller.HasSelfReference); + Assert.False(caller.HasMutualRecursion); } [Fact] diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 2c96b6e060..20ae7b333b 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -811,6 +811,10 @@ public void GetCallers_SolutionProjectReference_RequiresExplicitKind_Issue3662() [Fact] public void MemberReadEdges_AreOptInWhileLegacyCallRowsRemainReadable_Issue4894() { + InsertIndexedFile( + "src/member_read_target.cs", + "csharp", + "public class MemberReadTarget { public int CurrentValue { get; } public void LegacyValue() { } }"); InsertManualReference( "src/modern.cs", "csharp", @@ -1311,8 +1315,8 @@ public void GetCallers_DefaultWeightedRankingPrioritizesInstantiateOverNoisySubs InsertManualReferences("src/Factory.cs", "Factory", target, "instantiate", 3); InsertManualReferences("src/EventBus.cs", "EventBus", target, "subscribe", 50); - var weighted = _reader.GetCallers(target, lang: "csharp", exact: true); - var countRanked = _reader.GetCallers(target, lang: "csharp", exact: true, rankMode: ReferenceRankMode.Count); + var weighted = _reader.GetCallers(target, lang: "csharp", exact: false); + var countRanked = _reader.GetCallers(target, lang: "csharp", exact: false, rankMode: ReferenceRankMode.Count); Assert.Equal("Factory", weighted[0].CallerName); Assert.Equal(3, weighted[0].ReferenceCount); @@ -1340,7 +1344,7 @@ public void GetCallers_CountRankingIsPrimaryAndTieBreakersKeepPagesStable_Issue4 var all = _reader.GetCallers( target, lang: "csharp", - exact: true, + exact: false, rankMode: ReferenceRankMode.Count); Assert.Equal( @@ -1357,13 +1361,13 @@ public void GetCallers_CountRankingIsPrimaryAndTieBreakersKeepPagesStable_Issue4 target, limit: 2, lang: "csharp", - exact: true, + exact: false, rankMode: ReferenceRankMode.Count); var secondPage = _reader.GetCallers( target, limit: 2, lang: "csharp", - exact: true, + exact: false, rankMode: ReferenceRankMode.Count, offset: 2); Assert.Equal( @@ -1374,7 +1378,7 @@ public void GetCallers_CountRankingIsPrimaryAndTieBreakersKeepPagesStable_Issue4 target, lang: "csharp", pathPatterns: ["src/**", "tests/**"], - exact: true, + exact: false, rankMode: ReferenceRankMode.Count); Assert.Equal( ["tests/HighVolumeTests.cs", "src/ProductionCaller.cs", "tests/TiedTests.cs"], @@ -1384,7 +1388,7 @@ public void GetCallers_CountRankingIsPrimaryAndTieBreakersKeepPagesStable_Issue4 target, lang: "csharp", excludeTests: true, - exact: true, + exact: false, rankMode: ReferenceRankMode.Count); Assert.Equal( ["src/ProductionCaller.cs", "docs/DocumentedSample.cs"], @@ -2177,7 +2181,7 @@ public void GetCallers_FirstColumnComesFromFirstReferenceLocation_Issue4417() new ReferenceRecord { FileId = fileId, SymbolName = "Target", ReferenceKind = "call", Line = 3, Column = 5, Context = " Target();", ContainerKind = "function", ContainerName = "Run" }, ]); - var caller = Assert.Single(_reader.GetCallers("Target", exact: true)); + var caller = Assert.Single(_reader.GetCallers("Target", exact: false)); Assert.Equal(2, caller.FirstLine); Assert.Equal(20, caller.FirstColumn); @@ -2210,7 +2214,15 @@ void Run() Assert.Equal("Run", caller.CalleeName); Assert.Equal(1, caller.ReferenceCount); Assert.Equal(1, _reader.CountCallers("Run", lang: "csharp", exact: true, pathPatterns: ["src/*Program.cs*"])); - Assert.Equal(new QueryCountResult(1, 1), _reader.CountCallersTotal("Run", lang: "csharp", exact: true, pathPatterns: ["src/*Program.cs*"])); + Assert.Equal(new QueryCountResult( + 1, + 1, + IdentityRootAvailable: true, + GraphEvidenceConfidence: "identity_backed"), _reader.CountCallersTotal( + "Run", + lang: "csharp", + exact: true, + pathPatterns: ["src/*Program.cs*"])); } [Fact] @@ -3135,7 +3147,12 @@ private void RunEventStreamAsync() lang: "csharp", exact: true, pathPatterns: ["src/*issue2819*"])); - Assert.Equal(new QueryCountResult(1, 1, IncludesSql: false), _reader.CountCallersTotal( + Assert.Equal(new QueryCountResult( + 1, + 1, + IncludesSql: false, + IdentityRootAvailable: true, + GraphEvidenceConfidence: "identity_backed"), _reader.CountCallersTotal( "HttpMcpTransport.RunEventStreamAsync", lang: "csharp", exact: true, @@ -3800,12 +3817,10 @@ public int Parse(string value) Assert.Equal(3, Assert.Single(completePartialCallers, result => result.CallerName == "Run").ReferenceCount); Assert.Equal(1, Assert.Single(completePartialCallers, result => result.CallerName == "Noise").ReferenceCount); Assert.Equal(1, Assert.Single(completePartialCallers, result => result.CallerName == "Complex").ReferenceCount); - Assert.Equal(3, Assert.Single(completeCallers, result => result.CallerName == "Run").ReferenceCount); - Assert.Equal(1, Assert.Single(completeCallers, result => result.CallerName == "Noise").ReferenceCount); - Assert.Equal(1, Assert.Single(completeCallers, result => result.CallerName == "Complex").ReferenceCount); + Assert.Equal(2, Assert.Single(completeCallers).ReferenceCount); Assert.Equal(1, _reader.CountCallers("GetString", lang: "csharp", exact: true, pathPatterns: ["src/*common_member_graph_fixture*"])); Assert.Equal( - 3, + 1, _reader.CountCallers( "GetString", lang: "csharp", @@ -4183,7 +4198,7 @@ public void Run() "seed", lang: "csharp", referenceKind: "capture", - exact: true); + exact: false); Assert.Contains(callers, caller => caller.CallerName == "Run" @@ -5230,7 +5245,7 @@ public void GetCallers_ExposesDistinctReferenceKindsForMixedGroups() }, ]); - var caller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["src/*mixed_kind_caller*"])); + var caller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: false, pathPatterns: ["src/*mixed_kind_caller*"])); Assert.Equal("Setup", caller.CallerName); Assert.Equal("Changed", caller.CalleeName); Assert.Equal(6, caller.ReferenceCount); @@ -5256,7 +5271,7 @@ public void GetCallers_ExposesDistinctReferenceKindsForMixedGroups() Assert.Equal(new[] { "subscribe" }, callees[1].ReferenceKinds); Assert.Equal(1, callees[1].ReferenceKindCounts["subscribe"]); - var rawCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["src/*mixed_kind_caller*"], rawKinds: true)); + var rawCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: false, pathPatterns: ["src/*mixed_kind_caller*"], rawKinds: true)); Assert.Equal(new[] { "call", "subscribe" }, rawCaller.ReferenceKinds); Assert.Equal(5, rawCaller.ReferenceKindCounts["call"]); Assert.Equal(1, rawCaller.ReferenceKindCounts["subscribe"]); @@ -5321,11 +5336,11 @@ public void GraphQueries_ExplicitEventKindFiltersSeparateCanonicalAndRawKinds() }, ]); - var logicalCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"])); + var logicalCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal(new[] { "call", "subscribe" }, logicalCaller.ReferenceKinds); Assert.Equal("subscribe", logicalCaller.ReferenceKind); - var rawCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"], rawKinds: true)); + var rawCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"], rawKinds: true)); Assert.Equal(new[] { "call", "razor_event_binding", "subscribe", "unsubscribe" }, rawCaller.ReferenceKinds); Assert.Equal("subscribe", rawCaller.ReferenceKind); @@ -5333,7 +5348,7 @@ public void GraphQueries_ExplicitEventKindFiltersSeparateCanonicalAndRawKinds() "Changed", lang: "csharp", referenceKind: "subscribe", - exact: true, + exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal("subscribe", canonicalFilteredCaller.ReferenceKind); Assert.Equal(new[] { "subscribe" }, canonicalFilteredCaller.ReferenceKinds); @@ -5344,7 +5359,7 @@ public void GraphQueries_ExplicitEventKindFiltersSeparateCanonicalAndRawKinds() "Changed", lang: "csharp", referenceKind: "unsubscribe", - exact: true, + exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal("subscribe", explicitRawKindCanonicalOutput.ReferenceKind); Assert.Equal(new[] { "subscribe" }, explicitRawKindCanonicalOutput.ReferenceKinds); @@ -5353,7 +5368,7 @@ public void GraphQueries_ExplicitEventKindFiltersSeparateCanonicalAndRawKinds() "Changed", lang: "csharp", referenceKind: "subscribe", - exact: true, + exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"], rawKinds: true)); Assert.Equal(new[] { "razor_event_binding", "subscribe", "unsubscribe" }, rawFilteredCaller.ReferenceKinds); @@ -5380,8 +5395,8 @@ public void GraphQueries_ExplicitEventKindFiltersSeparateCanonicalAndRawKinds() rawKinds: true); Assert.Equal(new[] { "razor_event_binding", "subscribe", "unsubscribe" }, rawFilteredCallees.Select(callee => callee.ReferenceKind).Order().ToArray()); - Assert.Equal(1, _reader.CountCallers("Changed", lang: "csharp", referenceKind: "subscribe", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"])); - Assert.Equal(new QueryCountResult(1, 1), _reader.CountCallersTotal("Changed", lang: "csharp", referenceKind: "subscribe", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"])); + Assert.Equal(1, _reader.CountCallers("Changed", lang: "csharp", referenceKind: "subscribe", exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"])); + Assert.Equal(new QueryCountResult(1, 1), _reader.CountCallersTotal("Changed", lang: "csharp", referenceKind: "subscribe", exact: false, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal(1, _reader.CountCallees("Cleanup", lang: "csharp", referenceKind: "subscribe", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal(new QueryCountResult(1, 1), _reader.CountCalleesTotal("Cleanup", lang: "csharp", referenceKind: "subscribe", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"])); Assert.Equal(3, _reader.CountCallees("Cleanup", lang: "csharp", referenceKind: "subscribe", exact: true, pathPatterns: ["src/*unsubscribe_kind_caller*"], rawKinds: true)); @@ -5867,7 +5882,11 @@ public void GetTransitiveCallers_LimitSmallerThanCallerCount_ReportsUserLimitRea ]); } - var (results, truncated, truncatedReason, _, _) = _reader.GetTransitiveCallers("target", maxDepth: 1, limit: 3); + var (results, truncated, truncatedReason, _, _) = _reader.GetTransitiveCallers( + "target", + maxDepth: 1, + limit: 3, + lang: "python"); Assert.True(truncated); Assert.Equal(ImpactTruncatedReasons.UserLimit, truncatedReason); diff --git a/tests/CodeIndex.Tests/McpServerIssue5183Tests.cs b/tests/CodeIndex.Tests/McpServerIssue5183Tests.cs new file mode 100644 index 0000000000..658ecba4ff --- /dev/null +++ b/tests/CodeIndex.Tests/McpServerIssue5183Tests.cs @@ -0,0 +1,83 @@ +using System.Text.Json.Nodes; + +namespace CodeIndex.Tests; + +public partial class McpServerTests +{ + [Fact] + public void ExactCallersAndImpactAnalysis_KeepUnresolvedNamesOutOfConfirmedCounts_Issue5183() + { + InsertIndexedFile( + "src/issue5183/McpCaller.cs", + "csharp", + """ + namespace Issue5183; + public class McpCaller5183 + { + public void CallMissing5183() => ExternalApi5183.MissingLeaf5183(); + } + """); + + var callers = CallIssue5183Tool( + "callers", + new JsonObject + { + ["query"] = "MissingLeaf5183", + ["lang"] = "csharp", + ["exact"] = true, + ["countOnly"] = true, + }); + Assert.Equal(0, callers["count"]!.GetValue()); + Assert.False(callers["identity_root_available"]!.GetValue()); + Assert.Equal("no_identity_backed_root", callers["identity_root_unavailable_reason"]!.GetValue()); + Assert.Equal("no_identity_root", callers["graph_evidence_confidence"]!.GetValue()); + Assert.True(callers["degraded"]!.GetValue()); + Assert.False(callers["authoritative_count"]!.GetValue()); + + var broadCallers = CallIssue5183Tool( + "callers", + new JsonObject + { + ["query"] = "MissingLeaf5183", + ["lang"] = "csharp", + ["countOnly"] = true, + }); + Assert.Equal(1, broadCallers["count"]!.GetValue()); + Assert.Equal("name_discovery", broadCallers["graph_evidence_confidence"]!.GetValue()); + Assert.Null(broadCallers["identity_root_available"]); + + var impact = CallIssue5183Tool( + "impact_analysis", + new JsonObject + { + ["query"] = "MissingLeaf5183", + ["lang"] = "csharp", + ["countOnly"] = true, + }); + Assert.Equal(0, impact["confirmed_count"]!.GetValue()); + Assert.True(impact["heuristic"]!.GetValue()); + Assert.False(impact["identity_root_available"]!.GetValue()); + Assert.Equal("no_identity_backed_root", impact["identity_root_unavailable_reason"]!.GetValue()); + Assert.True(impact["degraded"]!.GetValue()); + Assert.False(impact["authoritative_count"]!.GetValue()); + Assert.Null(impact["total"]); + } + + private JsonObject CallIssue5183Tool(string name, JsonObject arguments) + { + var response = _server.HandleMessage(new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 5183, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = name, + ["arguments"] = arguments, + }, + })!; + + Assert.Null(response["error"]); + return response["result"]!["structuredContent"]!.AsObject(); + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs index 683eea093e..a67a39afc0 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs @@ -727,7 +727,7 @@ int Login(int user) } var (legacyCallerExitCode, legacyCallerStdout, legacyCallerStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - ["LegacyRun", "--db", dbPath, "--json", "--body", "--snippet-lines", "1", "--exact-name"], + ["LegacyRun", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, legacyCallerExitCode); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs index 002e902dc6..86d2cb625a 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs @@ -152,6 +152,28 @@ public void ApplyImpactCountAuthority_CappedPartialFamilyIsDegradedAndNonAuthori Assert.False(analysis.CountIsAuthoritative); } + [Fact] + public void ApplyImpactCountAuthority_IncompleteReferenceGraphIsNonAuthoritative_Issue5183() + { + var payload = new System.Text.Json.Nodes.JsonObject + { + ["degraded"] = true, + ["authoritative_count"] = true, + ["reference_graph_complete"] = false, + }; + var analysis = new ImpactAnalysisResult + { + IdentityRootAvailable = true, + ReferenceGraphComplete = false, + }; + + QueryCommandRunner.ApplyImpactCountAuthority(payload, analysis); + + Assert.True(payload["degraded"]!.GetValue()); + Assert.False(payload["authoritative_count"]!.GetValue()); + Assert.False(analysis.CountIsAuthoritative); + } + [Fact] public void RunImpact_MissingDepthValueShowsPerFlagHint_Issue1507() { @@ -195,6 +217,18 @@ public void GetTransitiveCallers_MaxDepthBoundaryProbeBudgetTerminatesStably_Iss Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), }); + writer.InsertSymbols([ + new SymbolRecord + { + FileId = fileId, + Kind = "function", + Name = "Root", + Line = 1, + StartLine = 1, + EndLine = 1, + }, + ]); + var references = new List(); for (var i = 0; i < 3; i++) { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerIssue5164Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerIssue5164Tests.cs index 442b4469fc..db90d2e80a 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerIssue5164Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerIssue5164Tests.cs @@ -146,6 +146,10 @@ public void TopLevelSelectors_RoundTripCoordinateAndCalleeIdentityWithoutCrossFi "src/Target.cs", "csharp", "public static class Target { public static void Run() { } }\n"), + new TestProjectHelper.IndexedFileFixture( + "src/ProgramRunner.cs", + "csharp", + "public static class ProgramRunner { public static int Run(string[] args) => 0; }\n"), ]); MarkCurrentContracts(dbPath); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerIssue5183Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerIssue5183Tests.cs new file mode 100644 index 0000000000..506c9985b7 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerIssue5183Tests.cs @@ -0,0 +1,155 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void ExactCallersAndImpact_ExposeUnresolvedIdentityRootAsNonAuthoritative_Issue5183() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue5183_cli"); + try + { + var dbPath = CreateIssue5183CliDatabase(projectRoot); + + var (callersExitCode, callersStdout, callersStderr) = CaptureConsole(() => RunGraphCommand( + "callers", + ["MissingLeaf5183", "--db", dbPath, "--json", "--count", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + using var callersDocument = ParseJsonOutput(callersStdout); + var callers = callersDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, callersExitCode); + Assert.Equal(string.Empty, callersStderr); + Assert.Equal(0, callers.GetProperty("count").GetInt32()); + Assert.False(callers.GetProperty("identity_root_available").GetBoolean()); + Assert.Equal("no_identity_backed_root", callers.GetProperty("identity_root_unavailable_reason").GetString()); + Assert.Equal("no_identity_root", callers.GetProperty("graph_evidence_confidence").GetString()); + Assert.True(callers.GetProperty("degraded").GetBoolean()); + Assert.False(callers.GetProperty("authoritative_count").GetBoolean()); + + var (broadExitCode, broadStdout, broadStderr) = CaptureConsole(() => RunGraphCommand( + "callers", + ["MissingLeaf5183", "--db", dbPath, "--json", "--count", "--lang", "csharp"], + _jsonOptions)); + using var broadDocument = ParseJsonOutput(broadStdout); + var broad = broadDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, broadExitCode); + Assert.Equal(string.Empty, broadStderr); + Assert.Equal(1, broad.GetProperty("count").GetInt32()); + Assert.Equal("name_discovery", broad.GetProperty("graph_evidence_confidence").GetString()); + Assert.False(broad.TryGetProperty("identity_root_available", out _)); + Assert.True(broad.GetProperty("authoritative_count").GetBoolean()); + + var (impactExitCode, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["MissingLeaf5183", "--db", dbPath, "--json", "--count", "--lang", "csharp"], + _jsonOptions)); + using var impactDocument = ParseJsonOutput(impactStdout); + var impact = impactDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, impactExitCode); + Assert.Equal(string.Empty, impactStderr); + Assert.Equal(0, impact.GetProperty("confirmed_count").GetInt32()); + Assert.True(impact.GetProperty("heuristic").GetBoolean()); + Assert.False(impact.GetProperty("identity_root_available").GetBoolean()); + Assert.Equal("no_identity_backed_root", impact.GetProperty("identity_root_unavailable_reason").GetString()); + Assert.False(impact.GetProperty("authoritative_count").GetBoolean()); + Assert.Contains( + "no_identity_backed_root", + impact.GetProperty("impact_failure_chain").EnumerateArray().Select(item => item.GetString())); + + var (compactExitCode, compactStdout, compactStderr) = CaptureConsole(() => ProgramRunner.Run( + ["impact", "MissingLeaf5183", "--db", dbPath, "--format", "compact", "--lang", "csharp"], + _jsonOptions, + "1.44.3-test")); + using var compactDocument = ParseJsonOutput(compactStdout); + var compactMetadata = compactDocument.RootElement.GetProperty("metadata"); + Assert.Equal(CommandExitCodes.Success, compactExitCode); + Assert.Equal(string.Empty, compactStderr); + Assert.False(compactMetadata.GetProperty("total_count_authoritative").GetBoolean()); + Assert.False( + compactMetadata + .GetProperty("response_context") + .GetProperty("authoritative_count") + .GetBoolean()); + + var (strictExitCode, _, strictStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["MissingLeaf5183", "--db", dbPath, "--json", "--strict", "--lang", "csharp"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.FeatureUnavailable, strictExitCode); + Assert.Equal(string.Empty, strictStderr); + + var (inspectExitCode, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["MissingLeaf5183", "--db", dbPath, "--json", "--exact", "--lang", "csharp"], + _jsonOptions)); + using var inspectDocument = ParseJsonOutput(inspectStdout); + var inspect = inspectDocument.RootElement; + Assert.Equal(CommandExitCodes.Success, inspectExitCode); + Assert.Equal(string.Empty, inspectStderr); + Assert.NotEmpty(inspect.GetProperty("references").EnumerateArray()); + Assert.Empty(inspect.GetProperty("callers").EnumerateArray()); + Assert.Equal(0, inspect.GetProperty("graph_sections").GetProperty("callers").GetProperty("total").GetInt32()); + + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/issue5183/StaleTarget.cs", + "csharp", + "public class StaleTarget5183 { public void Start() { } }\n"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/issue5183/StaleCaller.cs", + "csharp", + "public class StaleCaller5183 { public void Call(StaleTarget5183 target) => target.Start(); }\n"); + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + writer.MarkFoldReady(); + writer.ClearReferenceIdentityContractReady(); + } + + var (staleCompactExitCode, staleCompactStdout, staleCompactStderr) = CaptureConsole(() => ProgramRunner.Run( + ["impact", "StaleTarget5183", "--db", dbPath, "--format", "compact", "--fields", "callers"], + _jsonOptions, + "1.44.3-test")); + using var staleCompactDocument = ParseJsonOutput(staleCompactStdout); + var staleCompactMetadata = staleCompactDocument.RootElement.GetProperty("metadata"); + var staleCompactContext = staleCompactMetadata.GetProperty("response_context"); + Assert.Equal(CommandExitCodes.Success, staleCompactExitCode); + Assert.Equal(string.Empty, staleCompactStderr); + Assert.Equal("reference_identity_unavailable", staleCompactContext.GetProperty("identity_root_unavailable_reason").GetString()); + Assert.False(staleCompactContext.GetProperty("authoritative_count").GetBoolean()); + Assert.False(staleCompactMetadata.GetProperty("total_count_authoritative").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + private static string CreateIssue5183CliDatabase(string projectRoot) + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/issue5183/Caller.cs", + "csharp", + """ + namespace Issue5183; + public class Caller5183 + { + public void CallMissing5183() => ExternalApi5183.MissingLeaf5183(); + } + """); + using var db = new DbContext(DbOpenIntent.WriteIndex, dbPath); + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + writer.MarkIssuesReady(); + writer.MarkFoldReady(); + writer.MarkCSharpSymbolNameContractReady(); + return dbPath; + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs index a83c5ea151..1935500148 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs @@ -782,7 +782,7 @@ public void GraphCommands_AcceptExplicitCaptureAndProjectReferenceKinds() using var referencesDocument = ParseJsonOutput(referencesStdout); var (callersExitCode, callersStdout, callersStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - [query, "--db", dbPath, "--kind", kind, "--lang", lang, "--exact", "--json"], + [query, "--db", dbPath, "--kind", kind, "--lang", lang, "--json"], _jsonOptions)); using var callersDocument = ParseJsonOutput(callersStdout); @@ -1275,10 +1275,20 @@ void AssertCaller( string path, string callerKind, string callerName, - int? firstLine = null) + int? firstLine = null, + bool exact = true) { + var args = new List + { + query, + "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), + "--json", + "--lang", "csharp", + }; + if (exact) + args.Add("--exact-name"); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - [query, "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + [.. args], _jsonOptions)); using var document = ParseJsonOutput(stdout); var json = document.RootElement; @@ -1290,7 +1300,10 @@ void AssertCaller( Assert.Equal(callerName, json.GetProperty("caller_name").GetString()); Assert.Equal(query, json.GetProperty("callee_name").GetString()); Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); - Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + if (exact) + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + else + Assert.Equal("name_discovery", json.GetProperty("graph_evidence_confidence").GetString()); if (firstLine is not null) { Assert.Equal(firstLine.Value, json.GetProperty("first_line").GetInt32()); @@ -1307,7 +1320,7 @@ void AssertCaller( AssertCaller("ComputeBrace", "src/same-line-brace-property.cs", "property", "WrapBrace"); AssertCaller("ComputeCommentBlock", "src/allman-comment-property.cs", "property", "WrapCommentBlock"); AssertCaller("ComputeCommentExpression", "src/expression-comment-property.cs", "property", "WrapCommentExpression"); - AssertCaller("Trim", "src/multiline-switch-arm.cs", "function", "Read"); + AssertCaller("Trim", "src/multiline-switch-arm.cs", "function", "Read", exact: false); } finally { @@ -2050,7 +2063,7 @@ public class Factory MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - ["BaseWidget", "--db", dbPath, "--lang", "csharp", "--exact"], + ["BaseWidget", "--db", dbPath, "--lang", "csharp"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 2dd3a02984..1e1ed81413 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -10509,6 +10509,11 @@ public void Invoke() var findContent = string.Join('\n', Enumerable.Range(1, 25).Select(i => $"guard {i:D2}")) + "\n"; TestProjectHelper.InsertIndexedFile(dbPath, "src/find/Sample.cs", "csharp", findContent); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/graph/CountRunTarget.cs", + "csharp", + "public static class CountRunTarget { public static void CountRun() { } }\n"); MarkGraphAndFoldReady(dbPath); return (projectRoot, dbPath); } @@ -10520,8 +10525,8 @@ private string[] BuildTrueCountArgs(string command, string dbPath, bool useExpli "search" => new List { "needletoken", "--db", dbPath, "--json", "--count" }, "symbols" => new List { "CountTarget", "--db", dbPath, "--json", "--count" }, "definition" => new List { "CountTarget", "--db", dbPath, "--json", "--count" }, - "references" => new List { "CountRun", "--db", dbPath, "--json", "--count", "--exact" }, - "callers" => new List { "CountRun", "--db", dbPath, "--json", "--count", "--exact" }, + "references" => new List { "CountRun", "--db", dbPath, "--json", "--count", "--exact", "--path", "src/graph/CountCaller*.cs" }, + "callers" => new List { "CountRun", "--db", dbPath, "--json", "--count", "--exact", "--path", "src/graph/CountCaller*.cs" }, "callees" => new List { "Invoke", "--db", dbPath, "--json", "--count", "--exact" }, "files" => new List { "CountFile", "--db", dbPath, "--json", "--count" }, "find" => new List { "guard", "--db", dbPath, "--json", "--count", "--path", "src/find/Sample.cs" }, @@ -10596,6 +10601,26 @@ private static string CreateCallerRankingFixtureDb(string projectRoot) using var db = new DbContext(DbOpenIntent.WriteIndex, dbPath); var writer = new DbWriter(db.Connection); + var targetFileId = writer.UpsertFile(new FileRecord + { + Path = "src/RankedTarget.cs", + Lang = "csharp", + Size = 32, + Lines = 1, + Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), + }); + writer.InsertSymbols([ + new SymbolRecord + { + FileId = targetFileId, + Kind = "function", + Name = "RankedTarget", + Line = 1, + StartLine = 1, + EndLine = 1, + }, + ]); + InsertCaller("tests/HighVolumeTests.cs", "HighVolumeTests", 284); InsertCaller("src/ProductionCaller.cs", "ProductionCaller", 1); writer.MarkGraphReady(); diff --git a/tests/CodeIndex.Tests/golden/impact.json b/tests/CodeIndex.Tests/golden/impact.json index eb0b82cd98..4e857dcade 100644 --- a/tests/CodeIndex.Tests/golden/impact.json +++ b/tests/CodeIndex.Tests/golden/impact.json @@ -24,6 +24,10 @@ } ], "traversal_root_scope": "symbol", + "identity_root_available": true, + "graph_evidence_confidence": "identity_backed", + "identity_root_resolution_truncated": false, + "authoritative_count": true, "definition_count": 1, "definition_file_count": 1, "has_multiple_definitions": false,