diff --git a/README.md b/README.md index 25ec7026a9..47ee3b6cb7 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ After the first command, use these cues and follow-up commands: | Edits or branch switches | Refresh incrementally with `--files`, `--commits`, or `--changed-between ` instead of rebuilding. See [Quick Start](USER_GUIDE.md#quick-start) and [Incremental update reliability](USER_GUIDE.md#incremental-update-reliability). | | Intentional rebuilds | Interactive terminals ask before deleting the DB. Scripts and CI must pass `--yes` or `--force`. | | Long-lived DB compaction | Run `cdidx optimize` or `cdidx index --optimize` to compact FTS5 segments immediately. Incremental refreshes also optimize opportunistically. | -| Pathological generated files | `--max-symbols-per-file ` skips indexing file content, symbols, and references when one file emits too many symbols, leaving a `symbol_count_exceeded` issue for audit. | -| Maintenance rollback | Run `cdidx db checkpoint ` before risky DB maintenance and `cdidx db restore ` to roll back. `backfill-fold` creates an automatic checkpoint unless `--no-checkpoint` is passed, and interrupted folded-key rewrites resume from remaining rows. | +| Pathological generated files | `--max-symbols-per-file ` (max 50000) skips indexing file content, symbols, and references when one file emits too many symbols, leaving a `symbol_count_exceeded` issue for audit. | +| Maintenance rollback | Run `cdidx db checkpoint ` (name max 128 chars) before risky DB maintenance and `cdidx db restore ` to roll back. `backfill-fold` creates an automatic checkpoint unless `--no-checkpoint` is passed, and interrupted folded-key rewrites resume from remaining rows. | | Permission or I/O scan errors | `cdidx` records the scan error, continues other directories, and writes `.cdidx/scan-checkpoint.json` so same-HEAD retries can skip completed directories. | Output controls: @@ -173,7 +173,7 @@ downgrading `cdidx`. | Ranking and filters | Public/exported symbol matches rank ahead of protected, internal, and private matches. Use `--no-visibility-rank` for legacy order, and `--visibility` / `--exclude-visibility` with `symbols`, `definition`, `unused`, and `hotspots`. Query defaults can be adjusted with `CDIDX_DEFAULT_LIMIT`, `CDIDX_DEFAULT_SNIPPET_LINES`, and `CDIDX_DEFAULT_MAX_LINE_WIDTH`; explicit CLI flags still win. | | Project scoping | `.sln` / `.csproj`-aware --project <name|path> filters for indexing and queries, plus `--solution ` when a workspace has multiple solution files. | | MCP/LSP integration | MCP server support for AI clients such as Claude Code, Cursor, and Windsurf, including tools, indexed-file resources, starter prompts, schema constraints for local argument validation, `mimeType` on text content blocks, logging, a structured `ping` health result, HTTP `GET /healthz`, opt-in HTTP `/events` keep-alive notifications, a compatibility server-side `notifications/initialized` ready signal on stdio or HTTP `/events` streams, and `Language support:` descriptions sourced from the same registries as `cdidx languages`. Tool schemas reject unknown arguments with `-32602`, advertise `x-stability`, and use snake_case structured JSON keys to match the CLI JSON contract. LSP mode exposes `initialize`, `workspace/symbol`, `textDocument/documentSymbol`, `textDocument/definition`, and `textDocument/references` over stdio for editors that do not speak MCP. | -| Freshness | Parallel full-scan extraction with `--parallelism`, incremental refreshes with `--files` and `--commits`, continuous `--watch`, exact `status --check`, and configurable stale thresholds via `--stale-after` / `CDIDX_STALE_AFTER`. | +| Freshness | Parallel full-scan extraction with `--parallelism`, incremental refreshes with `--files` and `--commits`, continuous `--watch`, exact `status --check`, and configurable stale thresholds via `--stale-after` / `CDIDX_STALE_AFTER` up to `30d`. | | Storage | Local-first `.cdidx/codeindex.db` storage. Query commands run from nested directories prefer the outermost ancestor `.cdidx/codeindex.db` before falling back to the current directory. `--data-dir `, `CDIDX_DATA_DIR`, or `XDG_DATA_HOME` can move default SQLite storage outside the workspace; explicit `--db ` still wins. | | DB maintenance | New indexes use SQLite incremental auto-vacuum. Successful writer runs truncate-checkpoint the WAL. `cdidx vacuum` reclaims free pages from existing DBs, including a one-time full `VACUUM` conversion for legacy no-autovacuum DBs. `cdidx db schema` reports the on-disk schema, `cdidx db prune --dry-run\|--apply` audits or removes orphaned DB rows, and `status --json` reports metrics under `db_pragma_settings`. | | Security defaults | On POSIX systems, `.cdidx` is created with `0700` permissions, lifecycle, metrics, MCP audit, and query trace logs are created owner-read/write from the start, metrics and audit logs rotate to bounded slots, query trace logs are pruned to a bounded retained set, and `status --json` reports the effective `data_dir_mode` when available. | @@ -401,8 +401,8 @@ regex match には timeout が付きます。詳細は | 編集後やブランチ切り替え後 | 再構築ではなく `--files`、`--commits`、`--changed-between ` で差分更新します。詳細は [クイックスタート](USER_GUIDE.md#クイックスタート) と [インクリメンタル更新の信頼性](USER_GUIDE.md#インクリメンタル更新の信頼性) を参照してください。 | | 意図的な再構築 | interactive terminal では既存 DB 削除前に確認を求めます。script / CI では `--yes` または `--force` が必要です。 | | 長期間使っている DB の compact | `cdidx optimize` または `cdidx index --optimize` で FTS5 segment をすぐに compact できます。差分更新中も必要に応じて自動 optimize します。 | -| 病的な generated file | 1 ファイルが過剰な symbol を出す場合、`--max-symbols-per-file ` は file content / symbols / references を保存せず、監査用の `symbol_count_exceeded` issue を残します。 | -| 保守作業の rollback | risky な DB 保守の前に `cdidx db checkpoint `、戻す場合は `cdidx db restore ` を使います。`backfill-fold` は `--no-checkpoint` を渡さない限り自動 checkpoint を作成し、中断された folded-key rewrite は残り行から再開します。 | +| 病的な generated file | 1 ファイルが過剰な symbol を出す場合、`--max-symbols-per-file `(最大 50000)は file content / symbols / references を保存せず、監査用の `symbol_count_exceeded` issue を残します。 | +| 保守作業の rollback | risky な DB 保守の前に `cdidx db checkpoint `(name は最大 128 文字)、戻す場合は `cdidx db restore ` を使います。`backfill-fold` は `--no-checkpoint` を渡さない限り自動 checkpoint を作成し、中断された folded-key rewrite は残り行から再開します。 | | 権限や I/O の scan error | `cdidx` は scan error を記録し、他のディレクトリの走査を続けます。同じ HEAD の再実行では `.cdidx/scan-checkpoint.json` により成功済みディレクトリを読み飛ばせます。 | 出力を整える option: @@ -469,7 +469,7 @@ upgrade / downgrade 後はインストール済み補完 script を再生成し | 順位と filter | public/exported なシンボル一致を protected、internal、private より優先します。従来順は `--no-visibility-rank`、可視性の include / exclude は `symbols`、`definition`、`unused`、`hotspots` の `--visibility` / `--exclude-visibility` で指定できます。query 既定値は `CDIDX_DEFAULT_LIMIT`、`CDIDX_DEFAULT_SNIPPET_LINES`、`CDIDX_DEFAULT_MAX_LINE_WIDTH` で調整でき、明示 CLI flag が常に優先されます。 | | project scope | `.sln` / `.csproj` を使った --project <name|path> filter で index と query を .NET project 配下へ絞り込めます。workspace に solution が複数ある場合は `--solution ` を指定します。 | | MCP/LSP 連携 | Claude Code、Cursor、Windsurf などの AI クライアント向け MCP server。tools、インデックス済みファイル resources、starter prompts、ローカル引数検証用の schema constraints、text content block の `mimeType`、logging、構造化された `ping` health result、HTTP `GET /healthz`、opt-in の HTTP `/events` keep-alive notification、stdio または HTTP `/events` stream 上の互換性用 server-side `notifications/initialized` ready signal、`cdidx languages` と同じ言語レジストリ由来の `Language support:` 説明を提供します。Tool schema は未知の引数を `-32602` で拒否し、`x-stability` を公開し、CLI JSON contract と一致する snake_case の structured JSON key を使います。LSP mode は MCP 非対応 editor 向けに `initialize`、`workspace/symbol`、`textDocument/documentSymbol`、`textDocument/definition`、`textDocument/references` を stdio で公開します。 | -| freshness | `--parallelism` による parallel full-scan、`--files` / `--commits` による差分更新、`--watch` による継続更新、`status --check` による完全一致確認、`--stale-after` / `CDIDX_STALE_AFTER` による age threshold 上書きに対応します。 | +| freshness | `--parallelism` による parallel full-scan、`--files` / `--commits` による差分更新、`--watch` による継続更新、`status --check` による完全一致確認、`--stale-after` / `CDIDX_STALE_AFTER` による最大 `30d` までの age threshold 上書きに対応します。 | | storage | `.cdidx/codeindex.db` に保存する local-first 設計。ネストしたディレクトリからの query コマンドは、current directory にフォールバックする前に最上位祖先の `.cdidx/codeindex.db` を優先します。既定の SQLite 保存先は `--data-dir `、`CDIDX_DATA_DIR`、`XDG_DATA_HOME` で workspace 外へ移せます。明示的な `--db ` は引き続き最優先です。 | | DB maintenance | 新規 index DB は SQLite incremental auto-vacuum を使います。成功した writer 実行は WAL を `TRUNCATE` checkpoint します。既存 DB は `cdidx vacuum` で free page を回収でき、legacy no-autovacuum DB は初回だけ full `VACUUM` で変換します。`cdidx db schema` は on-disk schema を出力し、`cdidx db prune --dry-run\|--apply` は orphaned DB rows を検査・削除します。`status --json` は `db_pragma_settings` 配下に metrics を出力します。 | | security defaults | POSIX では `.cdidx` を `0700` 権限で作成し、lifecycle log、metrics log、MCP audit log、query trace log は作成時点から owner read/write のみで作成します。metrics log と audit log は bounded slot へ rotation し、query trace log は bounded な保持件数へ pruning します。`status --json` は利用可能な場合に実効 POSIX mode を `data_dir_mode` として報告します。 | diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 04e381cb24..46a4c07b93 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -393,12 +393,14 @@ be audited whenever the matching help text changes. | Setting | Current default | Source of truth | |---|---|---| -| Query result limit | `20` (`--limit`, alias `--top`) | CLI help and query runners | +| Query/diff result limit | `20` (`--limit`, alias `--top` for query commands), max `10000` | CLI help and query runners | | Search snippet lines | `8` (`--snippet-lines`, max `20`) | CLI help and search runner | | Max line width | `512` (`--max-line-width`, `0` disables) | `LineWidthFormatter.DefaultMaxLineWidth` | | Index max file size | `4MiB` unless `CDIDX_MAX_FILE_BYTES` is set | index runner help | -| Watch debounce | `500` ms (`--debounce`) | index watch runner | -| Status stale-after hint | `24h`, overridden by `--stale-after`, `CDIDX_STALE_AFTER`, or `.cdidxrc.json` | status runner | +| Index max symbols per file | `5000` (`--max-symbols-per-file`), max `50000` | index runner help | +| DB checkpoint name length | max `128` characters | db runner help | +| Watch debounce | `500` ms (`--debounce`), max `60000` ms | index watch runner | +| Status stale-after hint | `24h`, overridden by `--stale-after`, `CDIDX_STALE_AFTER`, or `.cdidxrc.json`; max `30d` | status runner | | Color mode | `auto`, overridden by `--color`, `CLICOLOR_FORCE`, `NO_COLOR`, or `CLICOLOR=0` | `ConsoleUi` | | ANSI palette | `basic` fallback, auto-upgraded from terminal hints unless overridden | `ConsoleUi` | | Report log tail | `200` lines (`--log-lines`), clamped to `2000` | report runner help | @@ -1265,6 +1267,7 @@ same source location. | `--force` | `index` | Bypass the per-database index lock. Only use when you are sure no other `cdidx index` is active against the same DB; concurrent runs may corrupt the schema. | | `--duration-format ` | `index` | Choose human elapsed-time display for index summaries. `auto` (default) uses unit labels; `seconds` emits decimal seconds; `hms` keeps `HH:MM:SS`. JSON always keeps raw `elapsed_ms`. | | `--max-file-bytes ` | `index` | Override the per-file indexing limit for this run. Defaults to 4MiB, or `CDIDX_MAX_FILE_BYTES` when set. Values accept raw bytes or `K` / `M` / `G` suffixes such as `50M`. | +| `--max-symbols-per-file ` | `index` | Skip file content, symbols, and references when one file emits too many symbols. Defaults to `5000`; values above `50000` are rejected. | | `--parallelism ` | `index` | Set full-scan extraction worker count. Defaults to CPU count capped at 16, or `CDIDX_INDEX_PARALLELISM` when set. SQLite writes stay single-consumer. | | `--watch` | `index` | After the initial scan completes, stay running and reindex incrementally as files change (FileSystemWatcher / inotify / FSEvents). Rejects `--commits`, `--changed-between`, `--files`, and `--dry-run` because the loop already drives continuous incremental updates. | | `--debounce ` | `index` (watch only) | Coalesce bursts of file events into a single update after `` of quiet (non-negative integer; default: 500). Invalid values emit a warning and are ignored. | @@ -1560,7 +1563,7 @@ Supported schema (top-level keys are snake_case; nested indexing kind keys keep } ``` -JSON5-style line comments (`//`) and trailing commas are accepted so the file stays human-editable. The optional `$schema` key is ignored at runtime; it is honored only so editors that recognize JSON Schema references can offer completion. Setting `disable_persistent_log` to `false` is a no-op (absence already means "logging enabled") — only `true` exports `CDIDX_DISABLE_PERSISTENT_LOG=1`. Config-sourced `metrics_path` and `global_tool_log_dir` values are resolved from the config workspace root and must stay inside that workspace; use the CLI flag or a real environment variable when you intentionally need an outside destination. `stale_after` uses the same compact duration format as `status --check --stale-after`: `30m`, `2h`, or `7d`. `suggestion_dedup_threshold` sets the MCP suggestion fuzzy-deduplication cutoff as a number from `0` to `1`; the built-in default is `0.85`, and `cdidx mcp --suggestion-dedup-threshold <0..1>` overrides it for one MCP session. `suggestion_max_age_days` and `suggestion_max_count` bound the live `.cdidx/suggestions-*.json` store; pruned records are appended to `.cdidx/suggestions-*.archive.jsonl`. Defaults are 365 days and 5000 records, and config-file values may not exceed 3650 days or 100000 records. Matching environment variables above those caps fall back to the defaults. String-array settings such as `indexing.includeKinds`, `indexing.excludeKinds`, `mcp.tools.allow`, and `mcp.tools.deny` are capped at 128 entries and 256 characters per item before they are joined into environment variables. `indexing.includeKinds` and `indexing.excludeKinds` set the default symbol-kind filter for `cdidx index`; CLI flags `--include-symbol-kind [,]` and `--exclude-symbol-kind [,]` override those env-backed defaults for a single run. +JSON5-style line comments (`//`) and trailing commas are accepted so the file stays human-editable. The optional `$schema` key is ignored at runtime; it is honored only so editors that recognize JSON Schema references can offer completion. Setting `disable_persistent_log` to `false` is a no-op (absence already means "logging enabled") — only `true` exports `CDIDX_DISABLE_PERSISTENT_LOG=1`. Config-sourced `metrics_path` and `global_tool_log_dir` values are resolved from the config workspace root and must stay inside that workspace; use the CLI flag or a real environment variable when you intentionally need an outside destination. `stale_after` uses the same compact duration format as `status --check --stale-after`: `30m`, `2h`, or `7d`, up to `30d`. `suggestion_dedup_threshold` sets the MCP suggestion fuzzy-deduplication cutoff as a number from `0` to `1`; the built-in default is `0.85`, and `cdidx mcp --suggestion-dedup-threshold <0..1>` overrides it for one MCP session. `suggestion_max_age_days` and `suggestion_max_count` bound the live `.cdidx/suggestions-*.json` store; pruned records are appended to `.cdidx/suggestions-*.archive.jsonl`. Defaults are 365 days and 5000 records, and config-file values may not exceed 3650 days or 100000 records. Matching environment variables above those caps fall back to the defaults. String-array settings such as `indexing.includeKinds`, `indexing.excludeKinds`, `mcp.tools.allow`, and `mcp.tools.deny` are capped at 128 entries and 256 characters per item before they are joined into environment variables. `indexing.includeKinds` and `indexing.excludeKinds` set the default symbol-kind filter for `cdidx index`; CLI flags `--include-symbol-kind [,]` and `--exclude-symbol-kind [,]` override those env-backed defaults for a single run. ## How it works @@ -1838,7 +1841,7 @@ If it exits `0` with `index_matches_workspace: true`, skip reindexing. Otherwise cdidx . # incremental update (skips unchanged files) ``` -`status --check` uses a 24-hour index-age threshold by default when explaining stale-index hints. Override it per invocation with `--stale-after ` (`30m`, `2h`, `7d`), for a process or CI job with `CDIDX_STALE_AFTER`, or per repository with `.cdidxrc.json` (`"stale_after": "2h"`). The effective threshold is shown in human output and as `stale_after_seconds` in JSON. +`status --check` uses a 24-hour index-age threshold by default when explaining stale-index hints. Override it per invocation with `--stale-after ` (`30m`, `2h`, `7d`, max `30d`), for a process or CI job with `CDIDX_STALE_AFTER`, or per repository with `.cdidxrc.json` (`"stale_after": "2h"`). The effective threshold is shown in human output and as `stale_after_seconds` in JSON. ## Keeping the index up to date (requires cdidx) @@ -2657,13 +2660,15 @@ render できます。 | 設定 | 現在の既定値 | Source of truth | |---|---|---| -| Query result limit | `20`(`--limit`、alias `--top`) | CLI help と query runners | +| Query/diff result limit | `20`(`--limit`、query command では alias `--top`)、最大 `10000` | CLI help と query runners | | Search snippet lines | `8`(`--snippet-lines`、最大 `20`) | CLI help と search runner | | Max line width | `512`(`--max-line-width`、`0` で無効) | `LineWidthFormatter.DefaultMaxLineWidth` | | Index max file size | `CDIDX_MAX_FILE_BYTES` 未設定時は `4MiB` | index runner help | +| Index max symbols per file | `5000`(`--max-symbols-per-file`)、最大 `50000` | index runner help | +| DB checkpoint name length | 最大 `128` 文字 | db runner help | | Index completion notification | `auto`(interactive terminal は bell、redirected output は none)。`--notify` / `CDIDX_NOTIFY` で上書き | index runner help | -| Watch debounce | `500` ms(`--debounce`) | index watch runner | -| Status stale-after hint | `24h`。`--stale-after` / `CDIDX_STALE_AFTER` / `.cdidxrc.json` で上書き | status runner | +| Watch debounce | `500` ms(`--debounce`)、最大 `60000` ms | index watch runner | +| Status stale-after hint | `24h`。`--stale-after` / `CDIDX_STALE_AFTER` / `.cdidxrc.json` で上書き、最大 `30d` | status runner | | Color mode | `auto`。`--color` / `CLICOLOR_FORCE` / `NO_COLOR` / `CLICOLOR=0` で上書き | `ConsoleUi` | | ANSI palette | `basic` fallback。terminal hints で自動昇格、または明示上書き | `ConsoleUi` | | Report log tail | `200` lines(`--log-lines`)、最大 `2000` に clamp | report runner help | @@ -3384,7 +3389,7 @@ Languages: - `index_matches_workspace` と `workspace_check.changed_files`、`missing_files`、`outside_sparse_cone_files`、`unindexed_files`、`unverifiable_files`、`scan_errors`、`head_changed` を返します(前回 full scan 時から worktree の HEAD が動いている場合は `indexed_head_commit` と `workspace_head_commit` も併記します)。git index で skip-worktree ビットが立っているパス (sparse-checkout cone/non-cone、partial clone、`git update-index --skip-worktree`) は `outside_sparse_cone_files` に分類され、freshness の判定を失敗させません。 - DB が現在の workspace と完全一致するときだけ終了コード `0`、stale な index では終了コード `5` です。 -`status --check` は既定で 24 時間の index-age しきい値を使って stale-index hint を説明します。呼び出しごとに `--stale-after `(`30m` / `2h` / `7d`)、プロセスや CI 単位で `CDIDX_STALE_AFTER`、リポジトリ単位で `.cdidxrc.json` の `"stale_after": "2h"` により上書きできます。有効なしきい値は human 出力に表示され、JSON では `stale_after_seconds` として返ります。 +`status --check` は既定で 24 時間の index-age しきい値を使って stale-index hint を説明します。呼び出しごとに `--stale-after `(`30m` / `2h` / `7d`、最大 `30d`)、プロセスや CI 単位で `CDIDX_STALE_AFTER`、リポジトリ単位で `.cdidxrc.json` の `"stale_after": "2h"` により上書きできます。有効なしきい値は human 出力に表示され、JSON では `stale_after_seconds` として返ります。 `cdidx index ` も incremental 実行時に同じ HEAD 変化を検知します。記録済み HEAD と worktree の HEAD が異なる場合は `head_changed` 警告を表示し、`--json` 出力には `head_changed`、`prior_indexed_head_commit`、`current_head_commit`、`head_change_notice` を含めます。ブランチ切り替え workflow が切り替え前後の ref を持っているなら、プロジェクト全体を再構築せず `cdidx index --changed-between ` で更新してください。2つの ref 間で変更されたファイルだけを更新し、rename/delete の旧 path も purge 対象に含めます。ref が分からない場合、履歴を動かす操作の後、または checkout 全体の stale path purge が必要な場合は、`cdidx index --rebuild` または `cdidx --json` の full refresh を使います。 @@ -3831,7 +3836,7 @@ MCP のレスポンスサイズ上限は、環境変数 override で guard が } ``` -人手で編集しやすいよう JSON5 形式の行コメント(`//`)と末尾カンマを許容します。任意の `$schema` キーはランタイムでは無視され、JSON Schema 参照をサポートするエディタが補完を提供するためだけに認識されます。`disable_persistent_log` を `false` に設定しても何も起きません(不在のままで "ログ有効" が既定)— `true` の場合のみ `CDIDX_DISABLE_PERSISTENT_LOG=1` を export します。config 由来の `metrics_path` と `global_tool_log_dir` は設定ファイルの workspace root から解決され、その workspace 内に収まる必要があります。意図的に外部の出力先を使う場合は CLI フラグまたは実際の環境変数を使ってください。`stale_after` は `status --check --stale-after` と同じ compact duration 形式(`30m` / `2h` / `7d`)です。`suggestion_dedup_threshold` は MCP suggestion の fuzzy deduplication しきい値を `0` から `1` の数値で設定します。組み込み既定値は `0.85` で、`cdidx mcp --suggestion-dedup-threshold <0..1>` は 1 回の MCP session だけこの値を上書きします。`suggestion_max_age_days` と `suggestion_max_count` は live の `.cdidx/suggestions-*.json` store の上限を設定し、prune された record は `.cdidx/suggestions-*.archive.jsonl` に追記されます。既定値は 365 日と 5000 件で、config-file 値は 3650 日または 100000 件を超えられません。同じ環境変数がこの上限を超えた場合は既定値へ戻ります。`indexing.includeKinds`、`indexing.excludeKinds`、`mcp.tools.allow`、`mcp.tools.deny` のような string array 設定は、環境変数へ join される前に 128 件、1 要素 256 文字までに制限されます。`indexing.includeKinds` と `indexing.excludeKinds` は `cdidx index` の symbol-kind filter 既定値を設定し、CLI フラグ `--include-symbol-kind [,]` / `--exclude-symbol-kind [,]` はその env 経由の既定値を 1 回の実行だけ上書きします。 +人手で編集しやすいよう JSON5 形式の行コメント(`//`)と末尾カンマを許容します。任意の `$schema` キーはランタイムでは無視され、JSON Schema 参照をサポートするエディタが補完を提供するためだけに認識されます。`disable_persistent_log` を `false` に設定しても何も起きません(不在のままで "ログ有効" が既定)— `true` の場合のみ `CDIDX_DISABLE_PERSISTENT_LOG=1` を export します。config 由来の `metrics_path` と `global_tool_log_dir` は設定ファイルの workspace root から解決され、その workspace 内に収まる必要があります。意図的に外部の出力先を使う場合は CLI フラグまたは実際の環境変数を使ってください。`stale_after` は `status --check --stale-after` と同じ compact duration 形式(`30m` / `2h` / `7d`、最大 `30d`)です。`suggestion_dedup_threshold` は MCP suggestion の fuzzy deduplication しきい値を `0` から `1` の数値で設定します。組み込み既定値は `0.85` で、`cdidx mcp --suggestion-dedup-threshold <0..1>` は 1 回の MCP session だけこの値を上書きします。`suggestion_max_age_days` と `suggestion_max_count` は live の `.cdidx/suggestions-*.json` store の上限を設定し、prune された record は `.cdidx/suggestions-*.archive.jsonl` に追記されます。既定値は 365 日と 5000 件で、config-file 値は 3650 日または 100000 件を超えられません。同じ環境変数がこの上限を超えた場合は既定値へ戻ります。`indexing.includeKinds`、`indexing.excludeKinds`、`mcp.tools.allow`、`mcp.tools.deny` のような string array 設定は、環境変数へ join される前に 128 件、1 要素 256 文字までに制限されます。`indexing.includeKinds` と `indexing.excludeKinds` は `cdidx index` の symbol-kind filter 既定値を設定し、CLI フラグ `--include-symbol-kind [,]` / `--exclude-symbol-kind [,]` はその env 経由の既定値を 1 回の実行だけ上書きします。 ## 動作の仕組み diff --git a/changelog.d/unreleased/3124.fixed.md b/changelog.d/unreleased/3124.fixed.md new file mode 100644 index 0000000000..f5c323a2d5 --- /dev/null +++ b/changelog.d/unreleased/3124.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3124 +affected: + - src/CodeIndex/Cli/DbCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/DbCommandRunnerTests.cs + - README.md + - USER_GUIDE.md +--- + +## English + +- **Database checkpoint names now have a predictable length cap (#3124)** — checkpoint and restore reject names above `128` characters before constructing checkpoint paths, and diagnostics truncate echoed names. + +## 日本語 + +- **Database checkpoint name に予測可能な長さ上限を設けました (#3124)** — checkpoint / restore は `128` 文字を超える名前を checkpoint path 作成前に拒否し、診断に出す名前は切り詰めます。 diff --git a/changelog.d/unreleased/3162.fixed.md b/changelog.d/unreleased/3162.fixed.md new file mode 100644 index 0000000000..42a567d8a3 --- /dev/null +++ b/changelog.d/unreleased/3162.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 3162 +affected: + - src/CodeIndex/Cli/DiffCommandRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/DiffCommandRunnerTests.cs + - USER_GUIDE.md +--- + +## English + +- **`diff --limit` now enforces the shared result maximum (#3162)** — values above `10000` are rejected before result lists are allocated. + +## 日本語 + +- **`diff --limit` が共有 result maximum を適用するようになりました (#3162)** — `10000` を超える値は結果リストを確保する前に拒否されます。 diff --git a/changelog.d/unreleased/3172.fixed.md b/changelog.d/unreleased/3172.fixed.md new file mode 100644 index 0000000000..9a3a0b0975 --- /dev/null +++ b/changelog.d/unreleased/3172.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 3172 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs + - README.md + - USER_GUIDE.md +--- + +## English + +- **`index --max-symbols-per-file` now enforces a hard maximum (#3172)** — values above `50000` are rejected instead of allowing callers to disable the per-file symbol safety guard. + +## 日本語 + +- **`index --max-symbols-per-file` に hard maximum を設けました (#3172)** — `50000` を超える値は拒否し、1 ファイルあたりの symbol safety guard を実質的に無効化できないようにしました。 diff --git a/changelog.d/unreleased/3173.fixed.md b/changelog.d/unreleased/3173.fixed.md new file mode 100644 index 0000000000..e1a98f66af --- /dev/null +++ b/changelog.d/unreleased/3173.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3173 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - src/CodeIndex/Cli/IndexWatchRunner.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs + - USER_GUIDE.md +--- + +## English + +- **`index --watch --debounce` now rejects impractical intervals (#3173)** — debounce values above `60000` ms fail as command-line errors instead of making watch mode look alive while deferring updates indefinitely. + +## 日本語 + +- **`index --watch --debounce` が実用的でない間隔を拒否するようになりました (#3173)** — `60000` ms を超える debounce 値は command-line error になり、watch mode が動いているように見えながら更新を実質的に先送りする状態を防ぎます。 diff --git a/changelog.d/unreleased/3176.fixed.md b/changelog.d/unreleased/3176.fixed.md new file mode 100644 index 0000000000..9db6353cd6 --- /dev/null +++ b/changelog.d/unreleased/3176.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 3176 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - README.md + - USER_GUIDE.md +--- + +## English + +- **`status --stale-after` now has a practical maximum (#3176)** — CLI, environment, and config stale thresholds above `30d` are rejected instead of making stale indexes look fresh indefinitely. + +## 日本語 + +- **`status --stale-after` に実用上限を設けました (#3176)** — CLI / 環境変数 / config の stale threshold が `30d` を超える場合は拒否し、stale な index が実質無期限に fresh と見なされることを防ぎます。 diff --git a/changelog.d/unreleased/3177.fixed.md b/changelog.d/unreleased/3177.fixed.md new file mode 100644 index 0000000000..5209d2b614 --- /dev/null +++ b/changelog.d/unreleased/3177.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 3177 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **`index --commits` now bounds commit-ref inputs (#3177)** — `--commits` rejects excessive ref counts and oversized ref tokens before invoking git, keeping validation work and diagnostics bounded. + +## 日本語 + +- **`index --commits` の commit ref 入力に上限を設けました (#3177)** — `--commits` は git を呼び出す前に過剰な ref 数と長すぎる ref token を拒否し、検証処理と診断出力を bounded に保ちます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 1d6553fc6e..ebc6104f85 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -334,7 +334,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--force", Description = "Bypass the per-database index lock", Commands = Set("index") }, new() { Name = "--duration-format", ValuePlaceholder = "", Description = "Index elapsed time display format", Commands = Set("index") }, new() { Name = "--max-file-bytes", ValuePlaceholder = "", Description = "Override the per-file indexing size limit", Commands = Set("index") }, - new() { Name = "--max-symbols-per-file", ValuePlaceholder = "", Description = "Skip file content, symbols, and references when one file emits too many symbols", Commands = Set("index") }, + new() { Name = "--max-symbols-per-file", ValuePlaceholder = "", Description = "Skip file content, symbols, and references when one file emits too many symbols (max 50000)", Commands = Set("index") }, new() { Name = "--parallelism", ValuePlaceholder = "", Description = "Full-scan extraction worker count (default: CPU count capped at 16; also honors CDIDX_INDEX_PARALLELISM)", Commands = Set("index") }, new() { Name = "--memory-trace", Description = "Include phase memory samples in index JSON output", Commands = Set("index") }, new() { Name = "--commits", ValuePlaceholder = "", Description = "Update files changed in given git commits", Commands = Set("index") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 6237debfae..ff96ea5933 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -96,8 +96,8 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("config", "cdidx config show [--json]"), ("validate-config", "cdidx validate-config"), ("doctor", "cdidx doctor"), - ("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"), - ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), + ("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db ] [--json] | cdidx db checkpoint [name<=128] [--db ] [--json] | cdidx db checkpoints --list [--db ] [--json] | cdidx db restore [--db ] [--json]"), + ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), ("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"), ("validate", "cdidx validate [--db ] [--json[=array]] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]"), ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), @@ -937,18 +937,18 @@ private static void PrintFlagReference(Action WriteHelpLine) Console.WriteLine(" --duration-format Index elapsed time format: `auto` (default), `seconds`, or `hms`; JSON keeps raw elapsed_ms"); WriteHelpLine(" --notify Long index completion signal: auto, bell, osc9, desktop, or none (also honors CDIDX_NOTIFY; quiet/json suppress it)"); WriteHelpLine(" --max-file-bytes Index only files up to this size (default: 4MiB; also honors CDIDX_MAX_FILE_BYTES; accepts K/M/G suffixes)"); - WriteHelpLine(" --max-symbols-per-file Skip file content, symbols, and references when one file emits too many symbols (default: 5000)"); + WriteHelpLine(" --max-symbols-per-file Skip file content, symbols, and references when one file emits too many symbols (default: 5000; max: 50000)"); WriteHelpLine(" --parallelism Full-scan extraction workers (default: CPU count capped at 16; also honors CDIDX_INDEX_PARALLELISM)"); WriteHelpLine(" --follow-symlinks Directory symlink policy: none (default), internal, or all"); WriteHelpLine(" --include-symbol-kind [,] Keep only matching symbol kinds during indexing"); WriteHelpLine(" --exclude-symbol-kind [,] Drop matching symbol kinds during indexing"); Console.WriteLine(" --commits [commit-ref ...]"); - Console.WriteLine(" Update only files changed in the specified git commits (preferred after commits)"); + Console.WriteLine($" Update only files changed in the specified git commits (preferred after commits; max {IndexCommandRunner.MaxCommitRefCount} refs, {IndexCommandRunner.MaxCommitRefLength} chars each)"); Console.WriteLine(" --changed-between "); Console.WriteLine(" Update only files changed between two git refs (useful after branch switches)"); Console.WriteLine(" --files [path ...] Update only the specified files; old rename/delete paths are not purged unless also listed"); WriteHelpLine(" --watch After the initial scan, stay running and reindex on file changes (FileSystemWatcher / inotify / FSEvents); rejects --commits / --changed-between / --files / --dry-run"); - Console.WriteLine(" --debounce Watch only: coalesce bursts of file events into one update after of quiet (default: 500)"); + Console.WriteLine($" --debounce Watch only: coalesce bursts of file events into one update after of quiet (default: {IndexWatchRunner.DefaultDebounceMs}, max {IndexWatchRunner.MaxDebounceMs})"); Console.WriteLine(" --optimize index only: optimize the existing FTS5 table for this project's DB without scanning files"); WriteHelpLine(" --color Color output: `auto` (default), `always`, or `never`; flag wins over `CLICOLOR_FORCE` / `NO_COLOR` / `CLICOLOR` env vars, which win over TTY auto-detect"); WriteHelpLine(" --palette ANSI palette: `basic` (8-color, default fallback), `256`, or `truecolor`; flag wins over `CDIDX_COLOR_PALETTE` env var, which wins over `COLORTERM` / `TERM` auto-detect"); diff --git a/src/CodeIndex/Cli/DbCommandRunner.cs b/src/CodeIndex/Cli/DbCommandRunner.cs index 05770a77f0..68ccb5f537 100644 --- a/src/CodeIndex/Cli/DbCommandRunner.cs +++ b/src/CodeIndex/Cli/DbCommandRunner.cs @@ -13,6 +13,8 @@ public static class DbCommandRunner { private const string CheckpointsDirectorySuffix = ".checkpoints"; private const string AutoCheckpointPrefix = "auto-"; + internal const int MaxCheckpointNameLength = 128; + private const int CheckpointNameDiagnosticTextLimit = 80; internal const int CheckpointListEntryLimit = 100; internal const int CheckpointFileInspectLimit = 32; internal const int IntegrityCheckRowLimit = 100; @@ -389,7 +391,7 @@ private static int RunRestore(DbCommandOptions options, JsonSerializerOptions js { var checkpointPath = GetCheckpointPath(fullDbPath, options.Name); if (!Directory.Exists(checkpointPath)) - return WriteCommandError(options.Json, jsonOptions, $"checkpoint not found: {options.Name}", CommandExitCodes.NotFound, "Run `cdidx db checkpoints --list` to see available checkpoints.", CommandErrorCodes.DbNotFound); + return WriteCommandError(options.Json, jsonOptions, $"checkpoint not found: {FormatCheckpointNameForDiagnostic(options.Name)}", CommandExitCodes.NotFound, "Run `cdidx db checkpoints --list` to see available checkpoints.", CommandErrorCodes.DbNotFound); var backupPath = RestoreCheckpoint(fullDbPath, options.Name, checkpointPath); if (options.Json) @@ -672,7 +674,7 @@ private static DbCheckpointOperationResult CreateCheckpoint(string fullDbPath, s var root = GetCheckpointRoot(fullDbPath); var checkpointPath = GetCheckpointPath(fullDbPath, name); if (Directory.Exists(checkpointPath)) - throw new InvalidOperationException($"checkpoint already exists: {name}"); + throw new InvalidOperationException($"checkpoint already exists: {FormatCheckpointNameForDiagnostic(name)}"); DataDirectorySecurity.CreateSensitiveDirectory(root); var tempPath = Path.Combine(root, ".tmp-" + name + "-" + Guid.NewGuid().ToString("N")); @@ -777,7 +779,7 @@ private static string RestoreCheckpoint(string fullDbPath, string name, string c SqliteConnection.ClearAllPools(); var checkpointDbPath = Path.Combine(checkpointPath, Path.GetFileName(fullDbPath)); if (!File.Exists(LongPath.EnsureWindowsPrefix(checkpointDbPath))) - throw new InvalidOperationException($"checkpoint is incomplete: {name}"); + throw new InvalidOperationException($"checkpoint is incomplete: {FormatCheckpointNameForDiagnostic(name)}"); var restoreTempPath = fullDbPath + ".restore-tmp-" + DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture); var backupPath = fullDbPath + ".restore-backup-" + DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture); @@ -788,7 +790,7 @@ private static string RestoreCheckpoint(string fullDbPath, string name, string c CopyIfExists(Path.Combine(checkpointPath, Path.GetFileName(fullDbPath) + "-wal"), Path.Combine(restoreTempPath, Path.GetFileName(fullDbPath) + "-wal"), privateDestination: true); CopyIfExists(Path.Combine(checkpointPath, Path.GetFileName(fullDbPath) + "-shm"), Path.Combine(restoreTempPath, Path.GetFileName(fullDbPath) + "-shm"), privateDestination: true); if (!File.Exists(LongPath.EnsureWindowsPrefix(Path.Combine(restoreTempPath, Path.GetFileName(fullDbPath))))) - throw new InvalidOperationException($"checkpoint staging failed: {name}"); + throw new InvalidOperationException($"checkpoint staging failed: {FormatCheckpointNameForDiagnostic(name)}"); DataDirectorySecurity.CreateSensitiveDirectory(backupPath); MoveIfExists(fullDbPath, Path.Combine(backupPath, Path.GetFileName(fullDbPath)), privateDestination: true); @@ -822,9 +824,15 @@ private static void ValidateCheckpointName(string name) || name.IndexOfAny(InvalidCheckpointNameChars) >= 0 || name.Contains(Path.DirectorySeparatorChar) || (Path.AltDirectorySeparatorChar != '\0' && name.Contains(Path.AltDirectorySeparatorChar))) - throw new ArgumentException($"invalid checkpoint name: {name}"); + throw new ArgumentException($"invalid checkpoint name: {FormatCheckpointNameForDiagnostic(name)}"); + + if (name.Length > MaxCheckpointNameLength) + throw new ArgumentException($"checkpoint name is too long ({name.Length} characters; max {MaxCheckpointNameLength}): {FormatCheckpointNameForDiagnostic(name)}"); } + private static string FormatCheckpointNameForDiagnostic(string name) + => ConsoleUi.FormatBoundedValue(name, CheckpointNameDiagnosticTextLimit); + private static string MakeTimestampCheckpointName() => DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture); diff --git a/src/CodeIndex/Cli/DiffCommandRunner.cs b/src/CodeIndex/Cli/DiffCommandRunner.cs index f385411d0e..7c13574cfd 100644 --- a/src/CodeIndex/Cli/DiffCommandRunner.cs +++ b/src/CodeIndex/Cli/DiffCommandRunner.cs @@ -8,6 +8,7 @@ namespace CodeIndex.Cli; public static class DiffCommandRunner { private const int DefaultDiffLimit = 20; + internal static int MaxDiffLimit => QueryCommandRunner.NumericFlagUpperBounds["--limit"]; private const int DriftExitCode = 1; private const int SchemaMismatchExitCode = 2; private const int UnreadableExitCode = 3; @@ -110,6 +111,11 @@ internal static DiffCommandOptions ParseArgs(string[] args) case "--limit" when i + 1 < args.Length: if (!int.TryParse(args[++i], out limit) || limit < 0) parseError = "--limit requires a non-negative integer"; + else if (limit > MaxDiffLimit) + { + parseError = $"--limit must be less than or equal to {MaxDiffLimit}"; + limit = DefaultDiffLimit; + } break; case "--limit": parseError = "--limit requires a value"; diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs index a2f5f39cfc..1e7e35da5f 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs @@ -128,7 +128,10 @@ public static IndexCommandOptions ParseArgs(string[] args) case "--debounce" when i + 1 < args.Length: if (int.TryParse(args[i + 1], System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsedDebounce) && parsedDebounce >= 0) { - watchDebounceMs = parsedDebounce; + if (parsedDebounce <= IndexWatchRunner.MaxDebounceMs) + watchDebounceMs = parsedDebounce; + else + parseError ??= $"--debounce must be less than or equal to {IndexWatchRunner.MaxDebounceMs} ms, got '{args[i + 1]}'"; i++; } else @@ -156,10 +159,10 @@ public static IndexCommandOptions ParseArgs(string[] args) maxFileSizeBytes = ParseMaxFileBytes(option["--max-file-bytes=".Length..], maxFileSizeBytes); break; case "--max-symbols-per-file" when i + 1 < args.Length: - maxSymbolsPerFile = ParseMaxSymbolsPerFile(args[++i], maxSymbolsPerFile, "--max-symbols-per-file"); + maxSymbolsPerFile = ParseMaxSymbolsPerFile(args[++i], maxSymbolsPerFile, "--max-symbols-per-file", ref parseError); break; case var option when option.StartsWith("--max-symbols-per-file=", StringComparison.Ordinal): - maxSymbolsPerFile = ParseMaxSymbolsPerFile(option["--max-symbols-per-file=".Length..], maxSymbolsPerFile, "--max-symbols-per-file"); + maxSymbolsPerFile = ParseMaxSymbolsPerFile(option["--max-symbols-per-file=".Length..], maxSymbolsPerFile, "--max-symbols-per-file", ref parseError); break; case "--parallelism" when i + 1 < args.Length: parallelism = ParseIndexParallelism(args[++i], parallelism, "--parallelism"); @@ -177,7 +180,7 @@ public static IndexCommandOptions ParseArgs(string[] args) while (i + 1 < args.Length && !args[i + 1].StartsWith('-')) { var commit = args[++i]; - commits.Add(commit); + AddCommitRef(commit, commits, ref parseError); } if (commits.Count == 0) Console.Error.WriteLine("Warning: --commits specified but no commit refs provided / --commits が指定されましたがコミットrefがありません"); @@ -412,6 +415,23 @@ private static int CountCsvEntries(string value) return count; } + private static void AddCommitRef(string commit, List commits, ref string? parseError) + { + if (commits.Count >= MaxCommitRefCount) + { + parseError ??= $"--commits accepts at most {MaxCommitRefCount} commit refs"; + return; + } + + if (commit.Length > MaxCommitRefLength) + { + parseError ??= $"--commits commit ref is too long ({commit.Length} characters; max {MaxCommitRefLength})"; + return; + } + + commits.Add(commit); + } + internal static int DefaultIndexParallelism() => Math.Clamp(Environment.ProcessorCount, 1, MaxIndexParallelism); @@ -462,10 +482,16 @@ private static int ParseIndexParallelism(string value, int fallback, string sour return fallback; } - private static int ParseMaxSymbolsPerFile(string value, int fallback, string source) + private static int ParseMaxSymbolsPerFile(string value, int fallback, string source, ref string? parseError) { if (int.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed) && parsed > 0) - return parsed; + { + if (parsed <= MaxSymbolsPerFileLimit) + return parsed; + + parseError ??= $"{source} must be less than or equal to {MaxSymbolsPerFileLimit}"; + return fallback; + } Console.Error.WriteLine($"Warning: invalid {source} value '{value}' (ignored; use a positive integer) / 不正な {source} 値 '{value}'(無視。正の整数を指定)"); return fallback; diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 5a68416277..b458d70923 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -20,6 +20,9 @@ public static partial class IndexCommandRunner internal const string IncludeSymbolKindsEnvironmentVariable = "CDIDX_INDEX_INCLUDE_SYMBOL_KINDS"; internal const string ExcludeSymbolKindsEnvironmentVariable = "CDIDX_INDEX_EXCLUDE_SYMBOL_KINDS"; internal const int DefaultMaxSymbolsPerFile = 5000; + internal const int MaxSymbolsPerFileLimit = 50_000; + internal const int MaxCommitRefCount = 64; + internal const int MaxCommitRefLength = 256; internal const int MaxGitExcludeBytes = 256 * 1024; private const string SymbolKindFilterMetaKey = "index_symbol_kind_filter"; private const int ScanCheckpointVersion = 1; diff --git a/src/CodeIndex/Cli/IndexWatchRunner.cs b/src/CodeIndex/Cli/IndexWatchRunner.cs index 9e9f7b39ed..b2f662d55d 100644 --- a/src/CodeIndex/Cli/IndexWatchRunner.cs +++ b/src/CodeIndex/Cli/IndexWatchRunner.cs @@ -15,6 +15,7 @@ namespace CodeIndex.Cli; internal static class IndexWatchRunner { internal const int DefaultDebounceMs = 500; + internal const int MaxDebounceMs = 60_000; private const int InternalBufferSize = 64 * 1024; private const int PollIntervalMs = 50; diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 06dc8734e9..9c9ba40d62 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -34,6 +34,8 @@ public static class QueryCommandRunner private const string LanguageCapabilityReferences = "references"; private const string LanguageCapabilitySymbols = "symbols"; internal static readonly TimeSpan DefaultStaleAfter = TimeSpan.FromHours(24); + internal static readonly TimeSpan MaxStaleAfter = TimeSpan.FromDays(30); + internal const string MaxStaleAfterDisplay = "30d"; internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System; private static readonly JsonDocumentOptions BatchJsonDocumentOptions = new() { @@ -7145,6 +7147,12 @@ internal static bool TryParseStaleAfter(string value, out TimeSpan staleAfter, o return false; } + if (ticks > MaxStaleAfter.Ticks) + { + error = $"Error: stale-after value '{value}' exceeds the maximum {MaxStaleAfterDisplay}."; + return false; + } + staleAfter = TimeSpan.FromTicks((long)Math.Round(ticks, MidpointRounding.AwayFromZero)); return true; } diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 94969f9e0f..4f5d213927 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -141,7 +141,8 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("--count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts", output); Assert.Contains("--no-dedup search only: return every raw overlapping chunk hit (debug/density)", output); Assert.Contains("--commits [commit-ref ...]", output); - Assert.Contains("Update only files changed in the specified git commits (preferred after commits)", output); + Assert.Contains("Update only files changed in the specified git", output); + Assert.Contains("commits (preferred after commits; max 64 refs, 256 chars each)", output); Assert.Contains("--files [path ...] Update only the specified files; old rename/delete paths are not purged unless also listed", output); Assert.Contains("--optimize index only: optimize the existing FTS5 table for this project's DB without scanning files", output); Assert.Contains("--duration-format Index elapsed time format: `auto` (default), `seconds`, or `hms`; JSON keeps raw elapsed_ms", output); diff --git a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs index 6afb82d8e6..7a0daf9d80 100644 --- a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs @@ -326,6 +326,33 @@ public void Run_CheckpointAndRestore_RestoresDatabaseBytes() } } + [Fact] + public void Run_CheckpointRejectsOversizedNameBeforePathConstruction_Issue3124() + { + var root = Path.Combine(Path.GetTempPath(), $"cdidx_db_checkpoint_name_cap_{Guid.NewGuid():N}"); + var dbPath = Path.Combine(root, "codeindex.db"); + var name = new string('a', DbCommandRunner.MaxCheckpointNameLength + 1); + try + { + Directory.CreateDirectory(root); + File.WriteAllText(dbPath, "db"); + + var (exitCode, _, stderr) = RunAndCaptureStreams(["checkpoint", name, "--db", dbPath]); + + Assert.Equal(CommandExitCodes.DatabaseError, exitCode); + Assert.Contains($"checkpoint name is too long ({name.Length} characters; max {DbCommandRunner.MaxCheckpointNameLength})", stderr); + Assert.Contains("truncated; original length", stderr); + Assert.DoesNotContain(name, stderr); + Assert.False(Directory.Exists(dbPath + ".checkpoints")); + } + finally + { + SqliteConnection.ClearAllPools(); + if (Directory.Exists(root)) + Directory.Delete(root, recursive: true); + } + } + [Fact] public void Run_Checkpoint_OnPosix_WritesPrivateSnapshotPermissions() { diff --git a/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs b/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs index 5dbe2ba2cc..867d2b335c 100644 --- a/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs @@ -86,6 +86,25 @@ public void Run_LimitZeroStillDetectsDatabaseDrift_Issue2885() } } + [Fact] + public void ParseArgs_LimitAcceptsMaximum_Issue3162() + { + var options = DiffCommandRunner.ParseArgs(["left.db", "right.db", "--limit", $"{DiffCommandRunner.MaxDiffLimit}"]); + + Assert.Equal(DiffCommandRunner.MaxDiffLimit, options.Limit); + Assert.Null(options.ParseError); + } + + [Fact] + public void ParseArgs_LimitRejectsValueAboveMaximum_Issue3162() + { + var aboveMaximum = $"{DiffCommandRunner.MaxDiffLimit + 1}"; + var options = DiffCommandRunner.ParseArgs(["left.db", "right.db", "--limit", aboveMaximum]); + + Assert.InRange(options.Limit, 0, DiffCommandRunner.MaxDiffLimit); + Assert.Equal("--limit must be less than or equal to 10000", options.ParseError); + } + [Fact] public void Run_DetailedJsonReportsLimitedSymbolRows_Issue2885() { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 57432cc515..a21e19137a 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -1423,6 +1423,19 @@ public void ParseArgs_DebounceFlag_ParsesValue() Assert.Equal(250, options.WatchDebounceMs); } + [Fact] + public void ParseArgs_DebounceFlag_RejectsValueAboveMaximum_Issue3173() + { + var oversized = $"{IndexWatchRunner.MaxDebounceMs + 1}"; + + var options = IndexCommandRunner.ParseArgs([".", "--watch", "--debounce", oversized]); + + Assert.True(options.Watch); + Assert.Null(options.WatchDebounceMs); + Assert.Contains("--debounce", options.ParseError); + Assert.Contains($"{IndexWatchRunner.MaxDebounceMs}", options.ParseError); + } + [Fact] public void ParseArgs_DebounceFlag_InvalidValue_IsIgnored() { @@ -1474,6 +1487,25 @@ public void ParseArgs_MaxSymbolsPerFileInlineFlag_ParsesPositiveValue() Assert.Equal(43, options.MaxSymbolsPerFile); } + [Fact] + public void ParseArgs_MaxSymbolsPerFileFlag_AcceptsMaximum_Issue3172() + { + var options = IndexCommandRunner.ParseArgs([".", "--max-symbols-per-file", $"{IndexCommandRunner.MaxSymbolsPerFileLimit}"]); + + Assert.Equal(IndexCommandRunner.MaxSymbolsPerFileLimit, options.MaxSymbolsPerFile); + Assert.Null(options.ParseError); + } + + [Fact] + public void ParseArgs_MaxSymbolsPerFileFlag_RejectsValueAboveMaximum_Issue3172() + { + var aboveMaximum = $"{IndexCommandRunner.MaxSymbolsPerFileLimit + 1}"; + var options = IndexCommandRunner.ParseArgs([".", $"--max-symbols-per-file={aboveMaximum}"]); + + Assert.Equal(IndexCommandRunner.DefaultMaxSymbolsPerFile, options.MaxSymbolsPerFile); + Assert.Contains("--max-symbols-per-file must be less than or equal to 50000", options.ParseError); + } + [Fact] public void ParseArgs_MaxFileBytesInvalidValue_IsIgnored() { @@ -1518,6 +1550,33 @@ public void ParseArgs_CommitsAcceptsHexCommitId() Assert.Null(options.ParseError); } + [Fact] + public void ParseArgs_CommitsRejectsTooManyRefs_Issue3177() + { + var refs = Enumerable + .Range(0, IndexCommandRunner.MaxCommitRefCount + 1) + .Select(i => $"HEAD~{i}") + .ToArray(); + var args = new[] { ".", "--commits" }.Concat(refs).ToArray(); + + var options = IndexCommandRunner.ParseArgs(args); + + Assert.Equal(IndexCommandRunner.MaxCommitRefCount, options.Commits.Count); + Assert.Contains($"at most {IndexCommandRunner.MaxCommitRefCount}", options.ParseError); + } + + [Fact] + public void ParseArgs_CommitsRejectsOversizedRef_Issue3177() + { + var oversizedRef = new string('a', IndexCommandRunner.MaxCommitRefLength + 1); + + var options = IndexCommandRunner.ParseArgs([".", "--commits", oversizedRef]); + + Assert.Empty(options.Commits); + Assert.Contains("commit ref is too long", options.ParseError); + Assert.Contains($"max {IndexCommandRunner.MaxCommitRefLength}", options.ParseError); + } + [Fact] public void ParseArgs_ParallelismFlag_ParsesPositiveValue() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 1f80a51e2b..6fdf5defc9 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -287,6 +287,7 @@ public void ParseArgs_ProjectFilterExpandsSolutionProjectToPathGlob_Issue1707() [InlineData("30m", 30 * 60)] [InlineData("2h", 2 * 60 * 60)] [InlineData("7d", 7 * 24 * 60 * 60)] + [InlineData(QueryCommandRunner.MaxStaleAfterDisplay, 30 * 24 * 60 * 60)] public void TryParseStaleAfter_AcceptsCompactDurations(string value, int expectedSeconds) { Assert.True(QueryCommandRunner.TryParseStaleAfter(value, out var staleAfter, out var error)); @@ -306,6 +307,15 @@ public void TryParseStaleAfter_RejectsInvalidDurations(string value) Assert.Contains("stale-after", error); } + [Theory] + [InlineData("31d")] + [InlineData("721h")] + public void TryParseStaleAfter_RejectsDurationsAboveMaximum_Issue3176(string value) + { + Assert.False(QueryCommandRunner.TryParseStaleAfter(value, out _, out var error)); + Assert.Contains(QueryCommandRunner.MaxStaleAfterDisplay, error); + } + [Fact] public void ParseArgs_StatusStaleAfterStoresDuration() {