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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Use the full suite by default. Use targeted filters only while iterating locally

## Test Stack

- Issue #5226 impact-count coverage belongs in `DbReaderIssue5226Tests.cs`, `QueryCommandRunnerIssue5226Tests.cs`, `QueryCommandRunnerImpactTests.cs`, and `McpServerIssue5226Tests.cs`. Keep zero, one, above-default, multi-hop, include/exclude-filter, incomplete-graph, dedicated safety-cap, heuristic-fallback, cross-language SQL-readiness, and ordinary row-limited cases together. Human, JSON, compact, and MCP count modes must ignore the presentation limit while preserving authoritative/lower-bound metadata and omitting count-only result rows; capped human output must retain numeric stdout and warn with the reason on stderr.
- Issue #5225 scoped C# dry-run coverage belongs in `IndexCommandRunnerDryRunTests.cs`. Keep paired preview/execution fixtures for `--files`, `--commits`, and `--changed-between`, plus static-interface and member-read expansion identities. Preserve non-C# no-expansion, candidate-cap lower-bound metadata, preflight-error and cancellation controls, human/JSON count and truncation output, and database/source non-mutation assertions across both target frameworks.
- Issue #5197 dependency-cycle coverage belongs in `QueryCommandRunnerIssue5197Tests.cs`, `McpServerIssue5197Tests.cs`, and the `deps-cycles-summary.json` golden in `JsonOutputSnapshotTests.cs`. Keep a cycle larger than the 50-node presentation limit to verify complete analysis, authoritative counts, bounded default materialization in human, JSON, DOT, GraphML, and JSON graph output, compact summaries, explicit raw expansion, expansion-aware recovery guidance, largest-component/grouping metadata, and CLI/MCP parity. A later JSON graph cursor page must report its own materialized and omitted-node totals even when they differ from the global largest component. Verify that SCC summaries aggregate every advertised evidence dimension and that actual MCP summaries and expanded results validate against the advertised `deps` output schema, including node arrays beyond the ordinary 10,000-item row bound. MCP must reject summary mode combined with `format=json-graph` just as the CLI does. Keep the C# suppression fixture mixed: unresolved qualified calls and resolved same-name decoys are removable evidence only with a current identity contract, stale and absent contracts retain qualified calls with unavailable resolution evidence, and confirmed resolved targets must remain and drive the surviving SCC even when their target file contains same-name overloads.
- Issue #5198 CODEOWNERS coverage belongs in `FileIndexerTests.cs`, `SymbolExtractorRepositoryMetadataTests.cs`, `QueryCommandRunnerFilesTests.cs`, `IndexCommandRunnerUpdateTests.cs`, `QueryCommandRunnerTests.cs`, and `McpServerToolsCallTests.cs`. Preserve the three case-sensitive Git-worktree-relative locations and the arbitrary-nested/case-variant/POSIX-literal-backslash negative cases, including scans rooted below the enclosing worktree; parser cases for full-line and inline comments, blank lines, whitespace, CRLF, rejection of unsupported escaped leading `#`, ownerless rules, user/team/email owners, malformed mentions, duplicate and overlapping patterns, invalid input, and persistable bounded diagnostics; ordered rule and owner child symbols; symbol-only capability guidance; full/scoped/delete indexing; branch-switch reconciliation; and removal from unknown-extension status diagnostics.
Expand Down Expand Up @@ -1172,6 +1173,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"

## テストスタック

- Issue #5226 の impact count coverage は `DbReaderIssue5226Tests.cs`、`QueryCommandRunnerIssue5226Tests.cs`、`QueryCommandRunnerImpactTests.cs`、`McpServerIssue5226Tests.cs` が担当します。0件、1件、既定上限超過、multi-hop、include / exclude filter、不完全 graph、専用 safety cap、heuristic fallback、cross-language の SQL readiness、通常の row 上限制限を一緒に維持してください。human、JSON、compact、MCP の count mode は表示用 limit を無視し、authoritative / lower-bound metadata を保持しながら count-only の result row を生成しないことを検証します。cap 到達時の human 出力は stdout を数値のみのまま保ち、stderr に理由付き warning を出します。
- Issue #5225 の scoped C# dry-run coverage は `IndexCommandRunnerDryRunTests.cs` が担当します。`--files`、`--commits`、`--changed-between` の preview / 実行を対にした fixture と、static-interface / member-read 展開の target identity を維持してください。両 target framework で、非 C# の非展開、candidate cap の lower-bound metadata、preflight error / cancellation control、human / JSON の件数・truncation 出力、database / source の非変更 assertion も保ちます。
- Issue #5197 の dependency-cycle coverage は `QueryCommandRunnerIssue5197Tests.cs`、`McpServerIssue5197Tests.cs`、`JsonOutputSnapshotTests.cs` の `deps-cycles-summary.json` golden が担当します。50 node の表示上限を超える cycle を維持し、完全な解析、authoritative な件数、human、JSON、DOT、GraphML、JSON graph 出力における既定の上限付き materialization、compact summary、明示的な raw 展開、展開状態を考慮した recovery guidance、最大 component / grouping metadata、CLI / MCP parity を検証してください。後続の JSON graph cursor page は global な最大 component と異なる場合も、その page 自身の materialize 済み node 数と省略 node 数を報告します。SCC summary が公開するすべての evidence dimension を集計し、実際の MCP summary と展開結果が通常の 10,000 item の row 上限を超える node array も含めて公開 `deps` output schema に適合することを検証します。また MCP は CLI と同様に summary mode と `format=json-graph` の併用を拒否します。C# 抑制 fixture は、current な identity contract がある場合だけ未解決の修飾 call と解決済みcallの同名decoyを除外可能とし、stale / absent contract では unavailable な resolution evidence として修飾 call を保持し、target file に同名 overload がある場合も確認済みの解決済みtargetが残る SCC を構成する混在状態を維持します。
- Issue #5198 の CODEOWNERS coverage は `FileIndexerTests.cs`、`SymbolExtractorRepositoryMetadataTests.cs`、`QueryCommandRunnerFilesTests.cs`、`IndexCommandRunnerUpdateTests.cs`、`QueryCommandRunnerTests.cs`、`McpServerToolsCallTests.cs` が担当します。case-sensitive な3つの Git worktree-relative location と、enclosing worktree より下を scan root にした場合を含む任意の nested file・大小文字違い file・POSIX の literal backslash file の negative case、full-line / inline comment・blank line・whitespace・CRLF・未対応である先頭 `#` の escape の拒否・ownerless rule・user/team/email owner・malformed mention・duplicate / overlapping pattern・不正 input・永続化可能な上限付き diagnostic の parser case、順序付き rule と owner child symbol、symbol-only capability guidance、full / scoped / delete indexing、branch-switch reconciliation、unknown-extension status diagnostic からの除外を維持してください。
Expand Down
21 changes: 21 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ cdidx impact Run --max-hops 2 --exclude-tests
cdidx impact Run --max-hops 0 --json
cdidx impact FolderDiffService --with-paths --json
cdidx impact CurrentValue --include-member-reads --json
cdidx impact Run --count --limit 1 --json
```

`impact` resolves a symbol and walks transitive callers through call-graph edges.
Expand All @@ -899,6 +900,16 @@ dependencies are intentionally part of the graph. Legacy indexes stored those
reads as `call`; they remain readable and keep their historical inclusive behavior
until re-indexed.

`--count` (MCP: `countOnly`) treats `--limit` as a row-presentation setting and
therefore ignores it while computing the total. Human, JSON, compact, and MCP
count outputs report the same filtered, multi-hop total without materializing caller
or file-impact rows. The traversal still has dedicated safety budgets. If one is
reached, `truncated` and its safety-cap reason remain visible, CLI JSON reports
`authoritative_count: false`, and MCP reports `total: null`; the returned `count` is
a lower bound. Human count output keeps stdout numeric and writes the cap reason and
lower-bound warning to stderr. Without `--count`, `--limit` continues to cap displayed
rows and a `user_limit` truncation can be retried with a larger value.

When the reference-identity contract is current and every matching C# declaration belongs to one logical partial family, `impact`
uses the family's stable `partial_family_id` as one traversal root and walks the
union of all physical member identities. Callers, file hints, and shortest paths
Expand Down Expand Up @@ -4589,6 +4600,7 @@ cdidx impact Run --max-hops 2 --exclude-tests
cdidx impact Run --max-hops 0 --json
cdidx impact FolderDiffService --with-paths --json
cdidx impact CurrentValue --include-member-reads --json
cdidx impact Run --count --limit 1 --json
```

`impact` は symbol を解決し、call-graph edges を通じて transitive callers を探索します。
Expand All @@ -4603,6 +4615,15 @@ member / value read を `member_read` として保存し、callers / callees / i
`call` として保存しているため、引き続き読み取り可能で、再 index するまでは従来の
inclusive な挙動を維持します。

`--count`(MCP は `countOnly`)では `--limit` を row 表示用の設定として扱い、総数の
算出時には無視します。human、JSON、compact、MCP の count 出力は caller / file-impact
row を materialize せず、同じ filter・multi-hop 条件の総数を一致して返します。ただし
traversal 専用の safety budget は引き続き適用されます。到達時は `truncated` と safety-cap
理由を保持し、CLI JSON は `authoritative_count: false`、MCP は `total: null` を返すため、
`count` は lower bound です。human count 出力は stdout を数値のみのまま維持し、cap 理由と
lower-bound warning を stderr に出力します。`--count` がない通常表示では、従来どおり
`--limit` が表示 row を制限し、`user_limit` なら値を増やして再試行できます。

reference-identity contract が current で、一致する C# declaration がすべて 1 つの論理 partial family に属する場合、`impact` は
安定した `partial_family_id` を 1 つの traversal root として使い、全物理 member identity
の和集合を辿ります。複数 member から到達する caller、file hint、shortest path は重複排除し、
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/5226.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 5226
affected:
- src/CodeIndex/Database/DbReader.ImpactTraversal.cs
- src/CodeIndex/Database/DbReader.ImpactTraversal.Engine.cs
- src/CodeIndex/Database/DbReader.GraphQueries.cs
- src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs
- src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs
- USER_GUIDE.md
---

## English

- **Impact count modes now return limit-independent totals (#5226)** — `impact --count` and MCP `impact_analysis` with `countOnly` no longer let the presentation `limit` change the reported total, restoring the count contract introduced in #159. Filtered and multi-hop counts agree across human, JSON, compact, and MCP output; bounded identity-target batches keep multi-hop MCP counts within the request deadline, supplied MCP limits are reported as ignored, dedicated safety caps still report non-authoritative lower bounds without materializing normal result rows, human output warns on stderr, and count-only summaries retain graph-readiness evidence.

## 日本語

- **impact の count mode が表示上限に依存しない総数を返すようになりました (#5226)** — `impact --count` と MCP `impact_analysis` の `countOnly` では、表示用 `limit` が報告総数を変えなくなり、#159 で導入した count 契約を復元しました。filter・multi-hop 条件の件数は human、JSON、compact、MCP で一致し、bounded な identity-target batch により multi-hop MCP count を request deadline 内に保ち、指定された MCP `limit` は ignored として通知します。専用 safety cap 到達時は通常の result row を生成せず、authoritative でない lower bound として stderr warning も含めて通知し、count-only summary でも graph readiness の evidence を保持します。
37 changes: 28 additions & 9 deletions src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
JsonEnvelopeWrapper.GetBoundedResponseOffset("impact"),
JsonEnvelopeWrapper.GetBoundedImpactCollection(),
options.IncludeMemberReads,
selectedDefinition);
selectedDefinition,
countOnly: options.CountOnly);
if (options.IncludeBody
&& !options.CountOnly
&& options.OutputFormat is (OutputFormatText or OutputFormatJson)
Expand All @@ -113,12 +114,13 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
DbReader.IsSqlLanguage(options.Lang)
|| DbReader.ContainsSqlLanguage(analysis.Definitions.Select(definition => definition.Lang))
|| DbReader.ContainsSqlLanguage(analysis.Callers.Select(caller => caller.Lang))
|| reader.AnyFilePathHasLanguage(analysis.FileImpacts.SelectMany(impact => new[] { impact.SourcePath, impact.TargetPath }), "sql"));
|| reader.AnyFilePathHasLanguage(analysis.FileImpacts.SelectMany(impact => new[] { impact.SourcePath, impact.TargetPath }), "sql")
|| reader.AnyFilePathHasLanguage(analysis.CountFileHistogram.Keys, "sql"));
var hdlGraphSignal = reader.GetHdlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests);
var confirmedCount = analysis.Callers.Count;
var confirmedFileCount = analysis.Callers.Select(r => r.Path).Distinct().Count();
var hintCount = analysis.FileImpacts.Count;
var hintFileCount = analysis.FileImpacts.Select(r => r.SourcePath).Distinct().Count();
var confirmedCount = analysis.ConfirmedCount;
var confirmedFileCount = analysis.ConfirmedFileCount;
var hintCount = analysis.HintCount;
var hintFileCount = analysis.HintFileCount;
var hasHeuristicHints = analysis.ImpactMode == "file_dependency_hints";
var visibleCount = hasHeuristicHints ? hintCount : confirmedCount;
var visibleFileCount = hasHeuristicHints ? hintFileCount : confirmedFileCount;
Expand Down Expand Up @@ -206,6 +208,9 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
["heuristic"] = analysis.Heuristic,
["hint_count"] = analysis.HintCount,
["hint_file_count"] = 0,
["max_hops"] = maxDepth,
["max_depth"] = maxDepth,
["actual_depth"] = analysis.ActualDepth,
["definition_count"] = analysis.DefinitionCount,
["definition_file_count"] = analysis.DefinitionFileCount,
["has_multiple_definitions"] = analysis.HasMultipleDefinitions,
Expand Down Expand Up @@ -240,7 +245,7 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
}
else
{
Console.WriteLine("0");
WriteImpactHumanCount(0, analysis);
if (!analysis.GraphTableAvailable)
CommandErrorWriter.WriteStderr("WARN: symbol_references table missing — this count result is degraded, not authoritative.");
}
Expand Down Expand Up @@ -321,6 +326,9 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
["heuristic"] = analysis.Heuristic,
["hint_count"] = hintCount,
["hint_file_count"] = hintFileCount,
["max_hops"] = maxDepth,
["max_depth"] = maxDepth,
["actual_depth"] = analysis.ActualDepth,
["truncated"] = analysis.Truncated,
};
AddImpactTraversalRootJsonFields(payload, analysis);
Expand All @@ -337,7 +345,7 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
}
else
{
Console.WriteLine($"{visibleCount}");
WriteImpactHumanCount(visibleCount, analysis);
}
return CommandExitCodes.Success;
}
Expand All @@ -356,7 +364,7 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
["hint_file_count"] = hintFileCount,
["max_hops"] = maxDepth,
["max_depth"] = maxDepth,
["actual_depth"] = analysis.Callers.Count > 0 ? analysis.Callers.Max(r => r.Depth) : 0,
["actual_depth"] = analysis.ActualDepth,
["truncated"] = analysis.Truncated,
["impact_mode"] = analysis.ImpactMode,
["heuristic"] = analysis.Heuristic,
Expand Down Expand Up @@ -516,6 +524,17 @@ private static void WriteImpactIdentityRootWarningIfNeeded(ImpactAnalysisResult
$"WARN: impact traversal has no identity-backed root ({reason}); confirmed counts are not authoritative.");
}

internal static void WriteImpactHumanCount(int count, ImpactAnalysisResult analysis)
{
Console.WriteLine(count.ToString(CultureInfo.InvariantCulture));
if (!analysis.Truncated)
return;

var reason = analysis.TruncatedReason ?? analysis.TerminationReason;
CommandErrorWriter.WriteStderr(
$"WARN: impact count truncated ({reason}); {count.ToString(CultureInfo.InvariantCulture)} is a lower bound, not authoritative.");
}

private static List<SymbolResult> BuildImpactDefinitionJsonResults(IReadOnlyList<SymbolResult> definitions)
=> LogicalPartialSymbolGrouper.Group(definitions);

Expand Down
Loading
Loading