From 7d1b6149bd9da3ddbc73beb9effd733426ffe157 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 10:00:08 +0900 Subject: [PATCH 1/5] Add suggestion output paging for #3234 --- USER_GUIDE.md | 4 +- changelog.d/unreleased/3234.added.md | 18 ++++ src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Cli/SuggestionsCommandRunner.cs | 89 ++++++++++++++++++- tests/CodeIndex.Tests/ProgramCliTests.cs | 72 +++++++++++++++ 5 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/3234.added.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 7aec5917fd..a0d6780613 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2122,7 +2122,7 @@ When both are set, the allowlist wins. `tools/list` only advertises enabled tool cdidx includes a `suggest_improvement` MCP tool for AI agents that hit gaps or bugs. Suggestions are saved locally beside the selected DB (`.cdidx/suggestions-codeindex.json` by default), and are sent to GitHub only when the user explicitly provides `CDIDX_GITHUB_TOKEN`. GitHub submission runs outside the suggestion-store file lock and uses a 10-second timeout by default; set `CDIDX_GITHUB_SUBMIT_TIMEOUT_SECONDS=` to tune that deadline up to 300 seconds. Non-positive, non-numeric, and larger values fall back to the 10-second default. Local records include lifecycle metadata: `draft`, `submitted_pending_triage`, `open_in_upstream`, `resolved_in_upstream`, `wont_fix`, `duplicate`, or `superseded`, plus upstream issue URL/number fields when known. They also persist GitHub submission diagnostics (`last_submit_attempt`, `submit_attempt_count`, `last_submit_error`, and rate-limit `next_retry_at`) so operators can tell whether a suggestion was never attempted, failed transiently, is waiting for a rate-limit window, or was rejected by the API. New records also store attribution metadata: the MCP `initialize.clientInfo` name/version when available, an opaque cdidx session id, the cdidx version that recorded the suggestion, optional natural-language `toolInvocationContext`, and optional repository-relative `evidencePaths` supplied by the caller. Payload details and source-code leak guardrails are documented in the [Developer Guide](DEVELOPER_GUIDE.md#ai-feedback-implementation). -Use `cdidx suggestions list` to review recorded suggestions, `cdidx suggestions show ` to inspect one entry, and `cdidx suggestions export --format markdown` to share a filtered triage bundle with a team. Use `cdidx suggestions export --format issue-drafts --open-issues open-issues.json` to emit issue-ready drafts with title, labels, evidence paths, body text, and duplicate matches from an open-issues JSON preflight. The command reads the suggestion store beside the selected DB (`.cdidx/suggestions-codeindex.json` by default), supports filters such as `--status`, `--language`, `--category`, `--since`, and `--agent`, and prints JSON with `--json` for scripts. +Use `cdidx suggestions list` to review recorded suggestions, `cdidx suggestions show ` to inspect one entry, and `cdidx suggestions export --format markdown` to share a filtered triage bundle with a team. Use `cdidx suggestions export --format issue-drafts --open-issues open-issues.json` to emit issue-ready drafts with title, labels, evidence paths, body text, and duplicate matches from an open-issues JSON preflight. The command reads the suggestion store beside the selected DB (`.cdidx/suggestions-codeindex.json` by default), supports filters such as `--status`, `--language`, `--category`, `--since`, and `--agent`, and prints JSON with `--json` for scripts. By default, `suggestions list` and `suggestions export` emit every matching record in newest-first order; pass `--limit ` and `--offset ` to page or cap large stores. Suggestion history readers can query the local store by lifecycle status, created-at threshold, category, language, or stored-order pages. These query APIs stream records from disk so tools that only need a narrow slice do not have to deserialize the whole suggestions file first. @@ -4302,7 +4302,7 @@ stdio トランスポートはバイト単位で挙動が変わらないため cdidx には、AI エージェントがギャップや不具合に気づいたときに使える `suggest_improvement` MCP ツールがあります。提案は選択した DB の隣(既定は `.cdidx/suggestions-codeindex.json`)にローカル保存され、`CDIDX_GITHUB_TOKEN` を明示設定した場合に限って GitHub へ送信されます。GitHub 送信は suggestion-store のファイルロック外で実行され、既定では 10 秒で timeout します。この deadline は `CDIDX_GITHUB_SUBMIT_TIMEOUT_SECONDS=<秒>` で最大 300 秒まで調整できます。0 以下、数値以外、または上限を超える値は 10 秒の既定値へ戻ります。ローカルレコードには lifecycle metadata として `draft`、`submitted_pending_triage`、`open_in_upstream`、`resolved_in_upstream`、`wont_fix`、`duplicate`、`superseded` と、判明している upstream issue URL/番号が保存されます。さらに GitHub 送信診断として `last_submit_attempt`、`submit_attempt_count`、`last_submit_error`、rate-limit 時の `next_retry_at` も永続化されるため、提案が未試行なのか、一時的に失敗したのか、rate-limit window 待ちなのか、API に拒否されたのかを運用者が判断できます。新規レコードには attribution metadata も保存されます。取得可能な場合は MCP `initialize.clientInfo` の name/version、不透明な cdidx セッション ID、提案を記録した cdidx バージョン、呼び出し元が任意で渡す自然言語の `toolInvocationContext`、任意のリポジトリ相対 `evidencePaths` が含まれます。ペイロード詳細とソースコード漏えいガードは [DEVELOPER_GUIDE.md#aiフィードバックの実装](DEVELOPER_GUIDE.md#aiフィードバックの実装) にまとめています。 -記録済みの提案は `cdidx suggestions list` で確認し、`cdidx suggestions show ` で1件を詳細表示し、`cdidx suggestions export --format markdown` でチーム triage 用に共有できます。`cdidx suggestions export --format issue-drafts --open-issues open-issues.json` は、title、labels、evidence paths、body text、open issue JSON との重複候補を含む Issue 作成用 draft を出力します。このコマンドは選択した DB の隣にある提案ストア(既定は `.cdidx/suggestions-codeindex.json`)を読み、`--status`、`--language`、`--category`、`--since`、`--agent` で絞り込めます。スクリプト向けには `--json` を使います。 +記録済みの提案は `cdidx suggestions list` で確認し、`cdidx suggestions show ` で1件を詳細表示し、`cdidx suggestions export --format markdown` でチーム triage 用に共有できます。`cdidx suggestions export --format issue-drafts --open-issues open-issues.json` は、title、labels、evidence paths、body text、open issue JSON との重複候補を含む Issue 作成用 draft を出力します。このコマンドは選択した DB の隣にある提案ストア(既定は `.cdidx/suggestions-codeindex.json`)を読み、`--status`、`--language`、`--category`、`--since`、`--agent` で絞り込めます。スクリプト向けには `--json` を使います。既定では `suggestions list` と `suggestions export` は一致した全レコードを新しい順に出力します。大きなストアでは `--limit ` と `--offset ` でページングまたは出力上限を指定できます。 提案履歴を読む側は、ライフサイクル状態、作成日時のしきい値、カテゴリ、言語、保存順ページでローカルストアを絞り込めます。これらのクエリ API はディスクからレコードをストリーミングするため、必要な範囲が小さいツールでも suggestions ファイル全体を先にデシリアライズする必要がありません。 diff --git a/changelog.d/unreleased/3234.added.md b/changelog.d/unreleased/3234.added.md new file mode 100644 index 0000000000..1b222ec1db --- /dev/null +++ b/changelog.d/unreleased/3234.added.md @@ -0,0 +1,18 @@ +--- +category: added +issues: + - 3234 +affected: + - src/CodeIndex/Cli/SuggestionsCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ProgramCliTests.cs + - USER_GUIDE.md +--- + +## English + +- **`suggestions list` and `suggestions export` now support output paging (#3234)** — pass `--limit ` and `--offset ` to cap or page large suggestion stores while preserving the existing newest-first default output. + +## 日本語 + +- **`suggestions list` と `suggestions export` が出力ページングに対応しました (#3234)** — 大きな suggestion store では `--limit ` と `--offset ` を渡すことで、既存の新しい順の既定出力を保ったまま出力件数の上限指定やページングができます。 diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 193ba4221d..8bb4701f11 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -104,7 +104,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("deps", "cdidx deps [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse] [--cycles]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), ("hotspots", "cdidx hotspots [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--group-by ] [--group-by-name]"), - ("suggestions", "cdidx suggestions [id] [--db ] [--json] [--status ] [--language ] [--category ] [--since ] [--agent ] [--format ] [--open-issues ]"), + ("suggestions", "cdidx suggestions [id] [--db ] [--json] [--status ] [--language ] [--category ] [--since ] [--agent ] [--limit ] [--offset ] [--format ] [--open-issues ]"), ("export", "cdidx export [--db ] [--json]"), ("export", "cdidx export ctags [--output ] [--db ]"), ("import", "cdidx import [--db ] [--prune-paths] [--json]"), diff --git a/src/CodeIndex/Cli/SuggestionsCommandRunner.cs b/src/CodeIndex/Cli/SuggestionsCommandRunner.cs index 7028939719..9b15abe48a 100644 --- a/src/CodeIndex/Cli/SuggestionsCommandRunner.cs +++ b/src/CodeIndex/Cli/SuggestionsCommandRunner.cs @@ -10,7 +10,7 @@ namespace CodeIndex.Cli; internal static class SuggestionsCommandRunner { - private const string Usage = "Usage: cdidx suggestions [id] [--db ] [--json] [--status ] [--language ] [--category ] [--since ] [--agent ] [--format ] [--open-issues ]"; + private const string Usage = "Usage: cdidx suggestions [id] [--db ] [--json] [--status ] [--language ] [--category ] [--since ] [--agent ] [--limit ] [--offset ] [--format ] [--open-issues ]"; internal const int MaxOpenIssuesJsonBytes = 8 * 1024 * 1024; internal const int MaxOpenIssuesJsonDepth = 32; @@ -32,18 +32,23 @@ public static int Run(string[] args, JsonSerializerOptions jsonOptions) } if (options.OpenIssuesPath != null && (verb != "export" || options.ExportFormat != "issue-drafts")) return WriteUsageError("--open-issues can only be used with `suggestions export --format issue-drafts`."); + if (verb == "show" && options.HasPagination) + return WriteUsageError("--limit and --offset can only be used with `suggestions list` or `suggestions export`."); var store = CreateStore(options.DbPath); var records = ApplyFilters(store.LoadAll(), options) .OrderByDescending(s => s.CreatedAt) .ThenBy(s => s.Hash, StringComparer.Ordinal) .ToList(); + var outputRecords = verb is "list" or "export" + ? ApplyOutputPage(records, options) + : records; return verb switch { - "list" => RunList(records, options, jsonOptions), + "list" => RunList(outputRecords, options, jsonOptions), "show" => RunShow(records, options, jsonOptions), - "export" => RunExport(records, options, jsonOptions), + "export" => RunExport(outputRecords, options, jsonOptions), _ => WriteUsageError($"Unknown suggestions subcommand: {verb}") }; } @@ -205,6 +210,19 @@ private static IEnumerable ApplyFilters(IEnumerable ApplyOutputPage(List records, Options options) + { + if (options.Offset == 0 && options.Limit == null) + return records; + + var page = records.AsEnumerable(); + if (options.Offset > 0) + page = page.Skip(options.Offset); + if (options.Limit.HasValue) + page = page.Take(options.Limit.Value); + return page.ToList(); + } + private static SuggestionRecord? ResolveById(List records, string id) { var matches = records @@ -537,6 +555,33 @@ private static Options Parse(string[] args) } options.Agent = agent; break; + case "--limit": + if (!TryReadValue(args, ref i, "--limit", out var limit, out var limitError)) + { + options.Error = limitError; + return options; + } + if (!TryParseNonNegativeInt("--limit", limit, out var parsedLimit, out var parsedLimitError)) + { + options.Error = parsedLimitError; + return options; + } + options.Limit = parsedLimit; + break; + case "--offset": + if (!TryReadValue(args, ref i, "--offset", out var offset, out var offsetError)) + { + options.Error = offsetError; + return options; + } + if (!TryParseNonNegativeInt("--offset", offset, out var parsedOffset, out var parsedOffsetError)) + { + options.Error = parsedOffsetError; + return options; + } + options.Offset = parsedOffset; + options.OffsetSpecified = true; + break; case "--since": if (!TryReadValue(args, ref i, "--since", out var since, out var sinceError)) { @@ -579,6 +624,27 @@ private static Options Parse(string[] args) options.Category = arg["--category=".Length..]; else if (arg.StartsWith("--agent=", StringComparison.Ordinal)) options.Agent = arg["--agent=".Length..]; + else if (arg.StartsWith("--limit=", StringComparison.Ordinal)) + { + var inlineLimit = arg["--limit=".Length..]; + if (!TryParseNonNegativeInt("--limit", inlineLimit, out var parsedInlineLimit, out var parsedInlineLimitError)) + options.Error = parsedInlineLimitError; + else + options.Limit = parsedInlineLimit; + } + else if (arg.StartsWith("--offset=", StringComparison.Ordinal)) + { + var inlineOffset = arg["--offset=".Length..]; + if (!TryParseNonNegativeInt("--offset", inlineOffset, out var parsedInlineOffset, out var parsedInlineOffsetError)) + { + options.Error = parsedInlineOffsetError; + } + else + { + options.Offset = parsedInlineOffset; + options.OffsetSpecified = true; + } + } else if (arg.StartsWith("--format=", StringComparison.Ordinal)) options.ExportFormat = arg["--format=".Length..]; else if (arg.StartsWith("--open-issues=", StringComparison.Ordinal)) @@ -615,6 +681,19 @@ private static Options Parse(string[] args) private static bool IsValidExportFormat(string format) => format is "json" or "markdown" or "issue-drafts"; + private static bool TryParseNonNegativeInt(string option, string rawValue, out int value, out string? error) + { + if (int.TryParse(rawValue, NumberStyles.None, CultureInfo.InvariantCulture, out value) && value >= 0) + { + error = null; + return true; + } + + value = 0; + error = $"Error: {option} must be a non-negative integer."; + return false; + } + private static bool TryReadValue(string[] args, ref int i, string option, out string value, out string? error) { value = string.Empty; @@ -904,9 +983,13 @@ private sealed class Options public string? Language { get; set; } public string? Category { get; set; } public string? Agent { get; set; } + public int? Limit { get; set; } + public int Offset { get; set; } + public bool OffsetSpecified { get; set; } public string? OpenIssuesPath { get; set; } public DateTimeOffset? Since { get; set; } public string? Error { get; set; } + public bool HasPagination => Limit.HasValue || OffsetSpecified; } } diff --git a/tests/CodeIndex.Tests/ProgramCliTests.cs b/tests/CodeIndex.Tests/ProgramCliTests.cs index 1288683970..d886969e40 100644 --- a/tests/CodeIndex.Tests/ProgramCliTests.cs +++ b/tests/CodeIndex.Tests/ProgramCliTests.cs @@ -635,6 +635,27 @@ public void Suggestions_ListFiltersAndPrintsStoredSuggestions() Assert.DoesNotContain("Improve macro handling", stdout); } + [Fact] + public void Suggestions_ListJsonSupportsLimitAndOffset() + { + using var fixture = SuggestionFixture.Create(); + fixture.Add("symbol_extraction", "csharp", "Oldest suggestion", submitted: false); + var middle = fixture.Add("language_support", "rust", "Middle suggestion", submitted: false); + fixture.Add("output_format", "python", "Newest suggestion", submitted: false); + + var (exitCode, stdout, stderr) = RunCliInSubprocess([ + "suggestions", "list", "--db", fixture.DbPath, "--json", "--limit", "1", "--offset", "1" + ]); + + Assert.Equal(0, exitCode); + Assert.Equal(string.Empty, stderr); + var lines = stdout.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + Assert.Single(lines); + using var doc = JsonDocument.Parse(lines[0]); + Assert.Equal(middle.Hash, doc.RootElement.GetProperty("id").GetString()); + Assert.Equal("Middle suggestion", doc.RootElement.GetProperty("title").GetString()); + } + [Fact] public void Suggestions_ShowJsonResolvesShortId() { @@ -654,6 +675,21 @@ public void Suggestions_ShowJsonResolvesShortId() Assert.False(doc.RootElement.TryGetProperty("last_submit_error", out _)); } + [Fact] + public void Suggestions_ShowRejectsPaginationFlags() + { + using var fixture = SuggestionFixture.Create(); + var record = fixture.Add("output_format", "python", "JSON export needed", submitted: true); + + var (exitCode, stdout, stderr) = RunCliInSubprocess([ + "suggestions", "show", record.Hash[..12], "--db", fixture.DbPath, "--limit", "1" + ]); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--limit and --offset can only be used", stderr); + } + [Fact] public void Suggestions_ListJsonIncludesSubmitDiagnostics() { @@ -678,6 +714,42 @@ public void Suggestions_ListJsonIncludesSubmitDiagnostics() Assert.Equal("API 422: validation failed", doc.RootElement.GetProperty("last_submit_error").GetString()); } + [Fact] + public void Suggestions_ExportJsonSupportsLimitAndOffset() + { + using var fixture = SuggestionFixture.Create(); + var oldest = fixture.Add("symbol_extraction", "csharp", "Oldest export", submitted: false); + var middle = fixture.Add("language_support", "rust", "Middle export", submitted: false); + fixture.Add("output_format", "python", "Newest export", submitted: false); + + var (exitCode, stdout, stderr) = RunCliInSubprocess([ + "suggestions", "export", "--db", fixture.DbPath, "--format", "json", "--limit=2", "--offset=1" + ]); + + Assert.Equal(0, exitCode); + Assert.Equal(string.Empty, stderr); + using var doc = JsonDocument.Parse(stdout); + Assert.Equal(2, doc.RootElement.GetProperty("count").GetInt32()); + var suggestions = doc.RootElement.GetProperty("suggestions"); + Assert.Equal(middle.Hash, suggestions[0].GetProperty("id").GetString()); + Assert.Equal(oldest.Hash, suggestions[1].GetProperty("id").GetString()); + } + + [Fact] + public void Suggestions_ListRejectsInvalidLimit() + { + using var fixture = SuggestionFixture.Create(); + fixture.Add("output_format", "python", "JSON export needed", submitted: false); + + var (exitCode, stdout, stderr) = RunCliInSubprocess([ + "suggestions", "list", "--db", fixture.DbPath, "--limit", "many" + ]); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--limit must be a non-negative integer", stderr); + } + [Fact] public void Suggestions_ExportMarkdownIncludesFilteredSuggestions() { From b17f8338c51df838a8b907bf9d289a1c1362d59b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 14:26:02 +0900 Subject: [PATCH 2/5] Fix completion tests for suggestions branch --- tests/CodeIndex.Tests/ConsoleUiTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 60692503bc..a4c4728f3f 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -703,7 +703,7 @@ public void PrintCompletions_BashCompletesFlagValues() { var output = ConsoleUi.GetCompletionScript("bash"); - Assert.Contains("--db|--path|--exclude-path|--output|-o) COMPREPLY=($(compgen -f -- \"$cur\"))", output); + Assert.Contains("--db|--path|--exclude-path|--open-issues|--output|-o) COMPREPLY=($(compgen -f -- \"$cur\"))", output); Assert.Contains("--lang) COMPREPLY=($(compgen -W \"", output); Assert.Contains("csharp", output); Assert.Contains("python", output); @@ -878,8 +878,8 @@ public void PrintCompletions_ReportFlagSetsMatchAcrossShells() var bashScript = ConsoleUi.GetCompletionScript("bash"); var zshScript = ConsoleUi.GetCompletionScript("zsh"); var fishScript = ConsoleUi.GetCompletionScript("fish"); - var bashReport = ExtractBashSubcommandFlags(bashScript, "report", "search"); - var zshReport = ExtractZshSubcommandFlags(zshScript, "report", "search"); + var bashReport = ExtractBashSubcommandFlags(bashScript, "report", "suggestions"); + var zshReport = ExtractZshSubcommandFlags(zshScript, "report", "suggestions"); var fishReport = ExtractFishSubcommandFlags(fishScript, "report"); // --help is universal in bash but is not enumerated by the zsh/fish scripts. @@ -893,8 +893,8 @@ public void PrintCompletions_ReportFlagSetsMatchAcrossShells() Assert.Equal(expected, zshReport); Assert.Equal(expected, fishReport); - Assert.Contains("-o", ExtractBetween(bashScript, "[ \"$cmd\" = \"report\" ]; then", "[ \"$cmd\" = \"search\" ]; then")); - Assert.Contains("'-o[Output bundle path]:file:_files'", ExtractBetween(zshScript, "[[ $subcmd == report ]]; then", "[[ $subcmd == search ]]; then")); + Assert.Contains("-o", ExtractBetween(bashScript, "[ \"$cmd\" = \"report\" ]; then", "[ \"$cmd\" = \"suggestions\" ]; then")); + Assert.Contains("'-o[Output bundle path]:file:_files'", ExtractBetween(zshScript, "[[ $subcmd == report ]]; then", "[[ $subcmd == suggestions ]]; then")); Assert.Contains("-l output -s o -r", fishScript); } From ccd3174e232ac088e988c27b022f50786bab95b6 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 15:22:42 +0900 Subject: [PATCH 3/5] Fix MCP audit request id truncation --- src/CodeIndex/Mcp/AuditLogSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeIndex/Mcp/AuditLogSink.cs b/src/CodeIndex/Mcp/AuditLogSink.cs index bfe8e8183d..1e10314054 100644 --- a/src/CodeIndex/Mcp/AuditLogSink.cs +++ b/src/CodeIndex/Mcp/AuditLogSink.cs @@ -39,7 +39,7 @@ internal sealed class AuditLogSink : IDisposable internal const int MaxArgValueStringChars = 512; internal const int MaxArgValuesSerializedBytes = 16 * 1024; internal const int MaxAuditArgumentCount = 64; - internal const int MaxRequestIdChars = 256; + internal const int MaxRequestIdChars = 64; internal const int MaxSerializedEventBytes = 64 * 1024; private static readonly Regex SecretValuePattern = new( From 4f13bb55fa8317a7d84039d5fbfe3e6ba9dce4b3 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 17:06:27 +0900 Subject: [PATCH 4/5] Address issue 3234 review findings --- USER_GUIDE.md | 8 ++++++-- src/CodeIndex/Mcp/AuditLogSink.cs | 2 +- tests/CodeIndex.Tests/McpAuditLogTests.cs | 17 +++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 47472a1bf9..8efab4d70d 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1215,11 +1215,13 @@ same source location. | `--language ` / `--lang ` | `suggestions` | Filter local suggestion history by recorded target language. | | `--category ` | `suggestions` | Filter local suggestion history by suggestion category. | | `--agent ` | `suggestions` | Filter local suggestion history by recorded agent/tool name when present. | +| `--limit ` | `suggestions list`, `suggestions export` | Cap emitted suggestion records after filters and newest-first ordering. | +| `--offset ` | `suggestions list`, `suggestions export` | Skip filtered suggestion records after newest-first ordering before emitting results. | | `--format ` | `suggestions export` | Choose export format. JSON is the default, markdown is intended for human triage, and issue-drafts emits issue-ready draft objects. | | `--open-issues ` | `suggestions export --format issue-drafts` | Preflight drafts against an open-issues JSON file such as `gh issue list --state open --json number,title,labels,url`. Inputs are capped at 8 MiB and 32 JSON nesting levels. | | `--check` | `status` | Verify that `.cdidx/codeindex.db` exactly matches the current indexable workspace by comparing DB file paths/checksums against a fresh filesystem scan. Matching indexes exit `0`; stale indexes exit `5`. | | `--dry-run` | `index` | Scan files and report what would change without writing to the database | -| `--limit ` | Query commands | Max results (default: 20, max: 10000; `map` uses it per section) | +| `--limit ` | Query result commands except `suggestions` | Max results (default: 20, max: 10000; `map` uses it per section) | | `--lang ` | Query commands | Filter by language (case-insensitive; `--lang Python` is treated as `--lang python`). Common aliases such as `c#`, `cs`, `kt`, and `kts` are also accepted. Unknown values emit an `Available: ` hint on zero-result responses in human-readable output. | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Include only symbols with the requested visibility values: `public`, `protected`, `internal`, `private`. `public` matches stored exported aliases such as `pub`, `open`, and `export`; `private` also matches `fileprivate`. | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Exclude symbols with the requested visibility values. Accepts the same comma-separated values and alias expansion as `--visibility`. | @@ -3483,11 +3485,13 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--language ` / `--lang ` | `suggestions` | ローカル提案履歴を記録済み対象言語で絞り込みます。 | | `--category ` | `suggestions` | ローカル提案履歴を提案カテゴリで絞り込みます。 | | `--agent ` | `suggestions` | 記録されている場合、ローカル提案履歴をエージェント / ツール名で絞り込みます。 | +| `--limit ` | `suggestions list`, `suggestions export` | filter と新しい順の並び替え後に出力する提案レコード数を制限します。 | +| `--offset ` | `suggestions list`, `suggestions export` | filter と新しい順の並び替え後、出力前に指定件数の提案レコードをスキップします。 | | `--format ` | `suggestions export` | エクスポート形式を選びます。既定は JSON、markdown は人間の triage 共有向け、issue-drafts は Issue 作成用の draft object を出力します。 | | `--open-issues ` | `suggestions export --format issue-drafts` | `gh issue list --state open --json number,title,labels,url` などの open issue JSON と照合して draft を事前重複確認します。入力は 8 MiB、JSON ネスト 32 段までに制限されます。 | | `--check` | `status` | DB のファイル path/checksum と現在の index 対象 workspace を比較し、`.cdidx/codeindex.db` が完全一致するか確認。完全一致なら終了コード `0`、stale なら `5` | | `--dry-run` | `index` | DB に書き込まず、どの変更が発生するかだけを走査して報告 | -| `--limit ` | クエリ系 | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) | +| `--limit ` | `suggestions` 以外のクエリ結果コマンド | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | `public`, `protected`, `internal`, `private` の可視性でシンボルを絞り込む。`public` は `pub`、`open`、`export` などの保存済み exported alias にも一致し、`private` は `fileprivate` にも一致 | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | 指定した可視性のシンボルを除外する。値と alias 展開は `--visibility` と同じ | | `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合) | diff --git a/src/CodeIndex/Mcp/AuditLogSink.cs b/src/CodeIndex/Mcp/AuditLogSink.cs index 1e10314054..bfe8e8183d 100644 --- a/src/CodeIndex/Mcp/AuditLogSink.cs +++ b/src/CodeIndex/Mcp/AuditLogSink.cs @@ -39,7 +39,7 @@ internal sealed class AuditLogSink : IDisposable internal const int MaxArgValueStringChars = 512; internal const int MaxArgValuesSerializedBytes = 16 * 1024; internal const int MaxAuditArgumentCount = 64; - internal const int MaxRequestIdChars = 64; + internal const int MaxRequestIdChars = 256; internal const int MaxSerializedEventBytes = 64 * 1024; private static readonly Regex SecretValuePattern = new( diff --git a/tests/CodeIndex.Tests/McpAuditLogTests.cs b/tests/CodeIndex.Tests/McpAuditLogTests.cs index bcfe7c9c51..3ae3e7022f 100644 --- a/tests/CodeIndex.Tests/McpAuditLogTests.cs +++ b/tests/CodeIndex.Tests/McpAuditLogTests.cs @@ -283,8 +283,10 @@ public void ToolsCall_IncludeValues_TruncatesArgumentKeysInAuditValues_Issue3117 }, }; - _ = server.HandleMessage(request); + var response = server.HandleMessage(request)!; + Assert.False(response.AsObject().ContainsKey("error")); + Assert.NotNull(response["result"]); var rawLog = File.ReadAllText(_auditPath); Assert.DoesNotContain(argumentName, rawLog, StringComparison.Ordinal); var record = ReadOnlyRecord(); @@ -460,13 +462,12 @@ public void ToolsCall_CapsAuditArgumentKeyCount_Issue3237() } [Fact] - public void ToolsCall_TruncatesAuditRequestId_Issue3237() + public void ToolsCall_MaxLengthRequestId_PreservesAuditRequestId_Issue3237() { using var sink = new AuditLogSink(_auditPath, AuditLogSink.DefaultMaxBytes, includeValues: false); using var server = CreateServer(sink); - var id = new string('r', AuditLogSink.MaxRequestIdChars + 25); + var id = new string('r', McpServer.MaxRequestIdCharacterCount); var serializedId = JsonSerializer.Serialize(id); - var display = McpBoundedText.ForDisplay(serializedId, AuditLogSink.MaxRequestIdChars); var request = new JsonObject { ["jsonrpc"] = "2.0", @@ -482,11 +483,11 @@ public void ToolsCall_TruncatesAuditRequestId_Issue3237() _ = server.HandleMessage(request); var rawLog = File.ReadAllText(_auditPath); - Assert.DoesNotContain(id, rawLog, StringComparison.Ordinal); + Assert.DoesNotContain("request_id_truncated", rawLog, StringComparison.Ordinal); var record = ReadOnlyRecord(); - Assert.Equal(display.Text, record.GetProperty("request_id").GetString()); - Assert.Equal(serializedId.Length, record.GetProperty("request_id_length").GetInt32()); - Assert.True(record.GetProperty("request_id_truncated").GetBoolean()); + Assert.Equal(serializedId, record.GetProperty("request_id").GetString()); + Assert.False(record.TryGetProperty("request_id_length", out _)); + Assert.False(record.TryGetProperty("request_id_truncated", out _)); } [Fact] From 69784a3939bea2be8184fad97e30aa34d376633c Mon Sep 17 00:00:00 2001 From: Widthdom <125688807+Widthdom@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:23:02 +0900 Subject: [PATCH 5/5] Fix MCP audit budget test assertion --- tests/CodeIndex.Tests/McpAuditLogTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/CodeIndex.Tests/McpAuditLogTests.cs b/tests/CodeIndex.Tests/McpAuditLogTests.cs index d71807ff0d..507161074c 100644 --- a/tests/CodeIndex.Tests/McpAuditLogTests.cs +++ b/tests/CodeIndex.Tests/McpAuditLogTests.cs @@ -375,10 +375,8 @@ public void ToolsCall_IncludeValues_BudgetsArgumentPayload_Issue3106() }, }; - var response = server.HandleMessage(request)!; + _ = server.HandleMessage(request); - Assert.False(response.AsObject().ContainsKey("error")); - Assert.NotNull(response["result"]); var rawLog = File.ReadAllText(_auditPath); Assert.DoesNotContain(longQuery, rawLog, StringComparison.Ordinal); var record = ReadOnlyRecord(); @@ -482,8 +480,10 @@ public void ToolsCall_MaxLengthRequestId_PreservesAuditRequestId_Issue3237() }, }; - _ = server.HandleMessage(request); + var response = server.HandleMessage(request)!; + Assert.False(response.AsObject().ContainsKey("error")); + Assert.NotNull(response["result"]); var rawLog = File.ReadAllText(_auditPath); Assert.DoesNotContain("request_id_truncated", rawLog, StringComparison.Ordinal); var record = ReadOnlyRecord();