From 74b4d47f5fa87a982768faf23cc6fcd514c1b108 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 14:07:27 +0900 Subject: [PATCH 1/3] Deduplicate status repair commands (#4915) --- AGENT_GUIDE.md | 1 + DEVELOPER_GUIDE.md | 4 +- README.md | 19 +++ USER_GUIDE.md | 4 +- changelog.d/unreleased/4915.fixed.md | 22 +++ .../Cli/QueryCommandRunner.RepairCommands.cs | 17 +- .../Cli/QueryCommandRunner.Status.cs | 72 +++++++- src/CodeIndex/Models/QueryResults.cs | 6 + .../QueryCommandRunnerFilesTests.cs | 160 ++++++++++++++++-- 9 files changed, 287 insertions(+), 18 deletions(-) create mode 100644 changelog.d/unreleased/4915.fixed.md diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index a369586a8..2a1dcd311 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -136,6 +136,7 @@ CI watching must be bounded. Do not loop indefinitely. ## Status Contract - `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `graph_data_current`, `index_complete`, `index_incomplete_reasons`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `language_readiness`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `head_freshness`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `unknown_extension_files`, `unknown_extension_files_truncated`, `unknown_extension_file_path_limit`, `unknown_extension_extension_counts`, `unknown_extension_category_counts`, `unknown_extension_groups`, `extractors`, `git_executable`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `db_file_mode`, `database_permission_policy`, `database_permission_diagnostics`, `mac_profile`, `mac_profile_diagnostics`, `db_size_bytes`, `wal_size_bytes`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`), `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`), `maintenance_guidance`, WAL checkpoint diagnostics (`read_only_fallback`, `wal_checkpoint_attempted`, `wal_checkpoint_succeeded`, `wal_checkpoint_skipped_reason`, `wal_checkpoint_failure_reason`, `wal_checkpoint_busy`, `wal_checkpoint_log_page_count`, `wal_checkpoint_checkpointed_page_count`, `wal_checkpoint_remaining_page_count`, `read_only_immutable_fallback`, `wal_stale_snapshot_risk`, `wal_stale_snapshot_reason`), `symbol_kinds`, `symbols_by_language`, status kind cap metadata (`symbol_kind_limit`, `symbol_kind_name_limit`, `symbol_kind_total_count`, `symbol_kind_omitted_count`, `symbol_kind_names_truncated`, `symbols_by_language_kind_total_counts`, `symbols_by_language_kind_omitted_counts`, `symbols_by_language_kind_names_truncated`), `process`, `last_index_run`, `last_failed_or_partial_index_run`, `last_failed_or_partial_index_run.progress_persisted`, `last_failed_or_partial_index_run.recovery_hint`, `last_failed_or_partial_index_run.file_errors`, `last_workspace_freshened_at`, `hooks`, `hook_diagnostics`, `trust_overrides`, MCP-only `mcp_session`, `mcp.rate_limit.bucket_limit`, `mcp.rate_limit.bucket_limit_rejection_count`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields and `repair_commands`. +- `status --check` repair actions are structured by `name`, `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`. Preserve compatibility `reason` as the first trigger and ordered `reasons` as the complete trigger set. Deduplicate only exact structured identities; different targets, options, actions, mutation classes, or safety semantics must remain distinct. JSON and human output must use the same deduplicated order, and human command-token quoting must remain consistent. - `maintenance_guidance.fts_optimization` is the shared, read-only recommendation contract for status, explain, optimize preview, and optimize execution. Keep `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` synchronized; stale or unavailable snapshots must not recommend mutation. - A valid CLI `status --stale-after ` implies the workspace check. Check-mode JSON includes `query_context.check_mode` (`explicit` or `implied_by_stale_after`) and `query_context.stale_after_seconds`; ordinary status JSON omits `query_context`. - `status --explain` derives accepted top-level keys from the same source-generated `StatusResult` serializer metadata as `status --json`, excludes ignored properties, and supports bounded dot-separated member paths without reading runtime values. Major readiness, trust, extension, maintenance, and cap-hit sections return structured meaning, source, dependencies, interpretation, and repair guidance; unknown input is sanitized and returns bounded valid candidates. Bounded status explain envelopes also omit database paths, timings, indexed HEADs, and stable-at timestamps. diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 70ea753b8..893a3b8d9 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -991,7 +991,7 @@ Current stable codes and triggers: | Schema discovery cache | `DbReader` schema discovery uses a process-level cache keyed by the normalized DB path. Column and index results are stored and returned as immutable `FrozenSet` snapshots, so callers cannot mutate schema decisions shared by other readers. Path states are reference-counted by live `DbSchemaCache` owners, are removed when the final owning `DbContext` is disposed, and are never evicted while an owner remains active. The cache checks `PRAGMA schema_version` before serving a lookup so SQLite DDL performed by cdidx or an external `sqlite3` session invalidates stale snapshots. Manual schema edits outside cdidx are still unsupported operationally; run `cdidx validate` after such edits before trusting query output. | | Batch trust marker | Index write batches stamp `codeindex_meta.batch_in_progress=true` before starting a mutation transaction and clear it inside the transaction that commits the matching rows and readiness metadata. If the indexer crashes after the marker is written but before the commit clears it, every later open reports `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` without changing readiness metadata. The explicit `index --rebuild` repair path alone demotes readiness before rebuilding. Gracefully handled per-file errors clear the marker after rollback; orphaned markers are reserved for interrupted or crashed batches whose trust metadata should not be treated as clean. | | Read-only opens and fallback | Query-only commands open with SQLite `Mode=ReadOnly` from the first attempt, retain WAL visibility, and never use writable setup or opportunistic migrations. A write-capable intent may still fall back to read-only when writable journal/WAL setup fails; an explicitly supplied `immutable=1` URI is the opt-in stale-snapshot escape hatch. If a WAL is present and must be observed from storage that cannot expose its sidecars, copy `.db`, `.db-wal`, and `.db-shm` together to a readable location or use a SQLite backup from an environment that can open the full WAL set. | -| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds `repair_commands[]` entries with `name`, `args`, `reason`, and `safety_notes` so clients do not parse prose remediation strings. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | +| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds structured `repair_commands[]` entries with `name`, `action`, `args`, `mutation_class`, `safety_class`, `safety_notes`, compatibility `reason`, and ordered `reasons`. Exact structured identities are deduplicated and aggregate reasons in check priority order; differences in target, options, action, mutation class, or safety semantics prevent merging. Human check output uses the same command set and preserves command-token quoting for each `[repair]` action. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | | Maintenance thresholds | WAL guidance flips to `checkpoint_recommended` at `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (default 64 MiB). Freelist guidance flips to `vacuum_recommended` at `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (default `0.20`). Invalid or out-of-range env values fall back to defaults. | | Maintenance command precedence | `maintenance_guidance.recommended_command` preserves the existing vacuum-then-checkpoint precedence. It returns `cdidx optimize --db ` only when WAL and freelist states are both exactly `ok` and the trusted FTS write snapshot reaches its threshold; an `unknown` higher-priority state or a stale/unavailable FTS snapshot never selects an optimize command. | | Page attribution | `status --json` reads SQLite page ownership without mutating the source. It prefers `dbstat` page bytes and otherwise traverses a bounded b-tree/WAL snapshot (at most 1,000,000 pages and 100,000 schema objects); when a live WAL connection is not already backed by a stable detached file set, the fallback first makes a cancellation-aware private backup of that connection's active read snapshot so a concurrent commit cannot mix generations. `allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` equals `logical_database_bytes`; table/index and internal/leaf/overflow/other page subtotals each reconcile to `allocated_object_bytes`. Payload, unused space, and structural overhead form a second reconciliation. Physical main/WAL/SHM bytes are reported separately. Output is capped at 20 object names, each support-sanitized to at most 128 characters. A failed or inconsistent probe returns `available=false`, a stable `unavailable_reason`, and null/omitted attribution values rather than zeros. | @@ -4431,7 +4431,7 @@ apply 時は `PRAGMA optimize` を実行します。 | schema discovery cache | `DbReader` の schema discovery は正規化済み DB path を key にした process-level cache を使います。column / index 結果は immutable な `FrozenSet` snapshot として保存・返却されるため、caller が他の reader と共有する schema 判定を変更することはできません。path state は有効な `DbSchemaCache` owner により参照カウントされ、最後の owner `DbContext` が dispose されると削除され、owner が active な間は退避されません。lookup 前に `PRAGMA schema_version` を確認するため、cdidx や外部 `sqlite3` session による SQLite DDL は stale snapshot を invalidate します。cdidx 外での手動 schema edit は運用上 unsupported であり、その後は query output を信頼する前に `cdidx validate` を実行してください。 | | batch trust marker | index write batch は mutation transaction を始める前に `codeindex_meta.batch_in_progress=true` を stamp し、対応する row と readiness metadata を commit する transaction 内で clear します。marker が書かれた後、clear される前に indexer が crash した場合、その後のすべての open は readiness metadata を変更せずに `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` と警告します。readiness を degrade するのは、明示的な `index --rebuild` repair path だけです。file ごとの error が graceful に処理された場合は rollback 後に marker を clear するため、orphaned marker は interrupted / crashed batch の trust metadata を clean と扱わないための signal です。 | | read-only open / fallback | query-only command は最初の試行から SQLite `Mode=ReadOnly` で開き、WAL の可視性を保ちながら writable setup と opportunistic migration を実行しません。write-capable intent は journal/WAL setup に失敗した場合に read-only へ fallback することがあります。明示的な `immutable=1` URI は stale snapshot を許容する opt-in escape hatch です。sidecar を公開できない storage 上の WAL を観測する必要がある場合は、`.db` / `.db-wal` / `.db-shm` をまとめて readable location に copy するか、full WAL set を open できる環境で SQLite backup を使います。 | -| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`args`、`reason`、`safety_notes` を返し、client が prose remediation を parse しなくてよいようにします。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | +| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`action`、`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason`、順序付きの `reasons` を返します。完全に同一の構造化 identity は deduplicate して check の優先順に reason を集約し、target、option、action、mutation class、安全性 semantics が異なる場合は merge しません。human check output も同じ command set を使い、各 `[repair]` action で command token の quote を維持します。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | | maintenance threshold | WAL guidance は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (既定 64 MiB) 以上で `checkpoint_recommended` になります。freelist guidance は `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (既定 `0.20`) 以上で `vacuum_recommended` になります。不正・範囲外の環境変数値は既定値へ戻します。 | | maintenance command の優先順位 | `maintenance_guidance.recommended_command` は既存の vacuum、checkpoint の順序を維持します。WAL と freelist の state が両方とも厳密に `ok` で、信頼できる FTS write snapshot が threshold に達した場合だけ `cdidx optimize --db ` を返します。上位 state が `unknown` の場合や FTS snapshot が stale / unavailable の場合は optimize command を選択しません。 | | page attribution | `status --json` は source を変更せずに SQLite page ownership を読み取ります。`dbstat` page byte を優先し、利用できない場合は件数上限付きの b-tree / WAL snapshot traversal(最大1,000,000 page、100,000 schema object)へ fallback します。live WAL connection が安定した detached file set に基づいていない場合、fallback は先にその connection の active read snapshot を cancellation 対応の private backup に固定し、並行 commit による世代混在を防ぎます。`allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` は `logical_database_bytes` と一致し、table/index と internal/leaf/overflow/other page の小計はそれぞれ `allocated_object_bytes` と一致します。payload、unused space、structural overhead も別に再照合されます。物理 main/WAL/SHM byte は分離して報告します。出力する object 名は最大20件で、各名称は support-safe sanitizer により最大128文字になります。probe の失敗・不整合時は `available=false`、安定した `unavailable_reason`、null / 省略された attribution 値を返し、ゼロとして偽装しません。 | diff --git a/README.md b/README.md index 699f00903..210af505f 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,16 @@ optimization recommendation used by status and optimize. | Remediation fields | `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`, `repair_commands`. | | MCP-only session diagnostics | `mcp_session`, `mcp_session.metrics`, `mcp_session.audit_log`, `mcp.rate_limit.bucket_limit`, `mcp.rate_limit.bucket_limit_rejection_count`. | +When check mode fails, each `repair_commands[]` entry includes `name`, `action`, +`args`, `mutation_class`, `safety_class`, `safety_notes`, the compatibility +`reason` (the first trigger), and the complete ordered `reasons` list. Commands +with the same structured action, arguments, mutation class, and safety semantics +are emitted once with their reasons aggregated in check priority order. Different +targets, options, actions, or safety semantics remain separate even if their +rendered command text looks similar. Human check output follows the same +deduplication and preserves command-token quoting in one `[repair]` line per +structured action. + Supplying `status --stale-after ` implies the workspace freshness check. Check-mode JSON includes `query_context.check_mode` (`explicit` or `implied_by_stale_after`) and the effective `query_context.stale_after_seconds`; @@ -811,6 +821,15 @@ FTS optimization recommendation を説明します。 | remediation fields | `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`, `repair_commands`。 | | MCP-only session diagnostics | `mcp_session`, `mcp_session.metrics`, `mcp_session.audit_log`, `mcp.rate_limit.bucket_limit`, `mcp.rate_limit.bucket_limit_rejection_count`。 | +check mode が失敗した場合、各 `repair_commands[]` entry は `name`、`action`、 +`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason` +(最初の trigger)、および順序付きの全 `reasons` list を含みます。同じ構造化 +action、argument、mutation class、安全性 semantics を持つ command は1件だけ返し、 +reason は check の優先順で集約します。target、option、action、安全性 semantics が +異なる command は、表示上の command text が似ていても別々に維持します。human +check output も同じ deduplication を使い、command token の quote を維持した +`[repair]` line を構造化 action ごとに1件だけ表示します。 + `status --stale-after ` を指定すると workspace freshness check を暗黙に有効化します。 check mode の JSON は `query_context.check_mode`(`explicit` または `implied_by_stale_after`)と有効な `query_context.stale_after_seconds` を含み、 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index bbe29ccd2..5966fadb2 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1299,6 +1299,8 @@ Here, drift refusal means drift detected through the final pre-publication valid `cdidx optimize --dry-run --json` previews FTS5 maintenance without acquiring the index lock or changing the source DB/WAL/SHM files. The result includes DB/core-table/FTS sizes, page and freelist indicators, the incremental-write recommendation, current lock and readiness state, a previous-duration estimate when available, and the operations a real optimize would perform, including its repair-mode schema initialization or migration check. `object_sizes_measurement` distinguishes exact `dbstat` page bytes from the logical-payload fallback used when SQLite does not provide `dbstat`. +`status --check --json` returns structured `repair_commands` for failed checks. Each entry identifies its `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`; `reason` remains the first trigger for compatibility, while `reasons` contains every trigger in deterministic check order. Identical structured actions are emitted once, but commands with different targets, options, actions, or safety semantics stay separate. Human check output applies the same rule and preserves command-token quoting for each emitted `[repair]` command. Writable repair arguments use normalized local paths rather than preserving read-only `file:` URI options. + ### Search code ```bash @@ -4684,7 +4686,7 @@ DB を read-only で開いて SQLite の `PRAGMA integrity_check` を実行し DB / WAL の肥大や空き page を確認したい場合は `status --json` の `maintenance_guidance` を見ます。既定では WAL が 64 MiB 以上で `checkpoint_recommended`、`freelist_count / page_count` が 0.20 以上で `vacuum_recommended` になり、`recommended_command` と `post_maintenance_follow_up` が返ります。しきい値は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` と `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` で調整できます。 -`status --check --json` は failed check ごとに `repair_commands` を返します。各 entry は `name`、`args`、`reason`、`safety_notes` を持つため、自動化は `recommended_action` の文章を分解せずに修復コマンドを組み立てられます。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 +`status --check --json` は failed check に対する構造化 `repair_commands` を返します。各 entry は `action`、`args`、`mutation_class`、`safety_class`、`safety_notes` を持ち、互換用の `reason` は最初の trigger、`reasons` は deterministic な check 順序ですべての trigger を保持します。同一の構造化 action は1件だけ返しますが、target、option、action、安全性 semantics が異なる command は別々に維持します。human check output にも同じ規則を適用し、各 `[repair]` command で command token の quote を維持します。書き込み用の修復 argument には read-only の `file:` URI option を残さず、正規化済み local path を使います。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 ```bash cdidx vacuum --dry-run --json # 回収見積もりと maintenance guidance だけを確認 diff --git a/changelog.d/unreleased/4915.fixed.md b/changelog.d/unreleased/4915.fixed.md new file mode 100644 index 000000000..b0239e3e8 --- /dev/null +++ b/changelog.d/unreleased/4915.fixed.md @@ -0,0 +1,22 @@ +--- +category: fixed +issues: + - 4915 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.Status.cs + - src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs + - src/CodeIndex/Models/QueryResults.cs + - tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs + - README.md + - USER_GUIDE.md + - DEVELOPER_GUIDE.md + - AGENT_GUIDE.md +--- + +## English + +- **Status repair actions are deduplicated with every trigger retained (#4915)** — `status --check` now emits each identical structured repair action once, preserves all triggering checks in deterministic order, and keeps actions with different targets, options, mutation classes, or safety semantics separate in JSON and human output. + +## 日本語 + +- **status の修復 action を重複排除し、すべての trigger を保持 (#4915)** — `status --check` は同一の構造化 repair action を1件だけ出力し、trigger になった check を deterministic な順序ですべて保持します。target、option、mutation class、安全性 semantics が異なる action は JSON / human output のどちらでも別々に維持します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs index 5f9f3e257..0af96650e 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs @@ -65,8 +65,19 @@ private static string QuoteCommandArgument(string value) if (!fullPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) fullPath = Path.GetFullPath(fullPath); - return fullPath.IndexOfAny([' ', '\t', '"']) >= 0 - ? $"\"{fullPath.Replace("\"", "\\\"", StringComparison.Ordinal)}\"" - : fullPath; + return QuoteCommandToken(fullPath); + } + + private static string RenderStatusRepairCommand(StatusRepairCommand command) + => string.Join(' ', new[] { command.Name }.Concat(command.Args).Select(QuoteCommandToken)); + + private static string QuoteCommandToken(string value) + { + if (value.Length >= 2 && value[0] == '<' && value[^1] == '>') + return value; + + return value.IndexOfAny([' ', '\t', '"']) >= 0 + ? $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"" + : value; } } diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Status.cs b/src/CodeIndex/Cli/QueryCommandRunner.Status.cs index bc96c3bb5..5d41b7eda 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Status.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Status.cs @@ -209,7 +209,10 @@ public static int RunStatus( if (options.StaleAfter.HasValue) WriteStatusAge(status, staleAfter.Value); if (checkFailures.Count > 0) + { WriteStatusCheckDiagnostics(checkFailures); + WriteStatusRepairCommands(status.RepairCommands); + } } else { @@ -1037,7 +1040,7 @@ private static IReadOnlyList BuildStatusCheckFailures(Status if (failures.Count == 0) return null; - var commands = new List(); + var candidates = new List(); foreach (var failure in failures) { var command = failure.Name switch @@ -1047,18 +1050,21 @@ private static IReadOnlyList BuildStatusCheckFailures(Status options, failure.Name, rebuild: false, + safetyClass: "workspace_refresh", "Re-runs indexing for the current workspace snapshot."), "index_complete" => BuildIndexRepairCommand( status, options, failure.Name, rebuild: false, + safetyClass: "source_error_recovery", "Fix the reported file/extractor error first; successful rows remain persisted and a rebuild is not required."), "reference_graph_complete" => BuildIndexRepairCommand( status, options, failure.Name, rebuild: false, + safetyClass: "reference_graph_refresh", GetReferenceGraphRepairSafetyNote(status)), "graph_table_available" or "issues_table_available" or "file_issues_data_current" or "sql_graph_contract_ready" or "csharp_symbol_name_ready" or "csharp_metadata_target_ready" @@ -1067,21 +1073,24 @@ private static IReadOnlyList BuildStatusCheckFailures(Status options, failure.Name, rebuild: false, + safetyClass: "metadata_refresh", "Rewrites stale or missing index metadata before query results are trusted."), "hotspot_family_ready" or "index_newer_than_reader" => BuildIndexRepairCommand( status, options, failure.Name, rebuild: true, + safetyClass: "full_rebuild", "Performs a full rebuild because partial updates cannot prove every indexed row was restamped."), "fold_ready" => BuildBackfillFoldRepairCommand(options, failure.Name), "migration_in_progress" => BuildStatusCheckRepairCommand(options, failure.Name), _ => null, }; if (command != null) - commands.Add(command); + candidates.Add(command); } + var commands = DeduplicateStatusRepairCommands(candidates); return commands.Count == 0 ? null : commands; } @@ -1107,6 +1116,7 @@ private static StatusRepairCommand BuildIndexRepairCommand( QueryCommandOptions options, string reason, bool rebuild, + string safetyClass, string safetyNote) { var args = new List @@ -1131,8 +1141,12 @@ private static StatusRepairCommand BuildIndexRepairCommand( return new StatusRepairCommand { Name = "cdidx", + Action = "index", Args = args, Reason = reason, + Reasons = [reason], + MutationClass = "index_write", + SafetyClass = safetyClass, SafetyNotes = [ safetyNote, @@ -1166,8 +1180,12 @@ private static StatusRepairCommand BuildBackfillFoldRepairCommand(QueryCommandOp return new StatusRepairCommand { Name = "cdidx", + Action = "backfill_fold", Args = args, Reason = reason, + Reasons = [reason], + MutationClass = "database_write", + SafetyClass = "fold_backfill", SafetyNotes = [ "Restamps folded-name columns in place without reparsing source files.", @@ -1188,8 +1206,12 @@ private static StatusRepairCommand BuildStatusCheckRepairCommand(QueryCommandOpt return new StatusRepairCommand { Name = "cdidx", + Action = "status_check", Args = args, Reason = reason, + Reasons = [reason], + MutationClass = "read_only", + SafetyClass = "wait_for_writer", SafetyNotes = [ "Wait for the active index or migration writer to finish before rerunning status.", @@ -1198,6 +1220,52 @@ private static StatusRepairCommand BuildStatusCheckRepairCommand(QueryCommandOpt }; } + internal static List DeduplicateStatusRepairCommands( + IEnumerable candidates) + { + var commands = new List(); + foreach (var candidate in candidates) + { + if (candidate.Reasons.Count == 0 && !string.IsNullOrWhiteSpace(candidate.Reason)) + candidate.Reasons.Add(candidate.Reason); + + var existing = commands.FirstOrDefault(command => + string.Equals(command.Name, candidate.Name, StringComparison.Ordinal) + && string.Equals(command.Action, candidate.Action, StringComparison.Ordinal) + && string.Equals(command.MutationClass, candidate.MutationClass, StringComparison.Ordinal) + && string.Equals(command.SafetyClass, candidate.SafetyClass, StringComparison.Ordinal) + && command.Args.SequenceEqual(candidate.Args, StringComparer.Ordinal) + && command.SafetyNotes.SequenceEqual(candidate.SafetyNotes, StringComparer.Ordinal)); + if (existing == null) + { + commands.Add(candidate); + continue; + } + + foreach (var reason in candidate.Reasons) + { + if (!existing.Reasons.Contains(reason, StringComparer.Ordinal)) + existing.Reasons.Add(reason); + } + } + + return commands; + } + + private static void WriteStatusRepairCommands(IReadOnlyList? commands) + { + if (commands == null) + return; + + foreach (var command in commands) + { + CommandErrorWriter.WriteStderr( + $"[repair] {RenderStatusRepairCommand(command)} " + + $"(reasons={string.Join(',', command.Reasons)}; action={command.Action}; " + + $"mutation={command.MutationClass}; safety={command.SafetyClass})"); + } + } + private static void WriteStatusCheckDiagnostics(IReadOnlyList failures) { foreach (var failure in failures) diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index 311816881..b557824c3 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -2027,8 +2027,14 @@ internal sealed partial class StatusMetadataJsonContext : JsonSerializerContext public sealed class StatusRepairCommand { public string Name { get; set; } = string.Empty; + public string Action { get; set; } = string.Empty; public List Args { get; set; } = []; public string Reason { get; set; } = string.Empty; + public List Reasons { get; set; } = []; + [JsonPropertyName("mutation_class")] + public string MutationClass { get; set; } = string.Empty; + [JsonPropertyName("safety_class")] + public string SafetyClass { get; set; } = string.Empty; [JsonPropertyName("safety_notes")] public List SafetyNotes { get; set; } = []; } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs index 278512f28..2da352e05 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs @@ -1358,13 +1358,13 @@ public void RunStatus_Json_RelativeReadOnlyUriFoldRemediationUsesWorkingDirector SqliteConnection.ClearAllPools(); var (exitCode, stdout, stderr) = RunBuiltCli( - ["status", "--db", "file:codeindex.db?immutable=1", "--json"], + ["status", "--db", "file:codeindex.db?immutable=1", "--check=fold", "--json"], dbDirectory); using var document = ParseJsonOutput(stdout); var json = document.RootElement; - Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(2, exitCode); Assert.Equal(string.Empty, stderr); Assert.Equal("stale_fold_key_version", json.GetProperty("fold_ready_reason").GetString()); Assert.Contains(dbPath, json.GetProperty("recommended_action").GetString()); @@ -1373,6 +1373,20 @@ public void RunStatus_Json_RelativeReadOnlyUriFoldRemediationUsesWorkingDirector Assert.DoesNotContain("", json.GetProperty("alternative_action").GetString()); Assert.DoesNotContain("file:", json.GetProperty("recommended_action").GetString()); Assert.DoesNotContain("file:", json.GetProperty("alternative_action").GetString()); + var repairCommand = Assert.Single(json.GetProperty("repair_commands").EnumerateArray()); + Assert.Equal("backfill_fold", repairCommand.GetProperty("action").GetString()); + Assert.Equal("fold_ready", repairCommand.GetProperty("reason").GetString()); + Assert.Equal( + ["fold_ready"], + repairCommand.GetProperty("reasons").EnumerateArray().Select(value => value.GetString()).ToArray()); + Assert.Equal("database_write", repairCommand.GetProperty("mutation_class").GetString()); + Assert.Equal("fold_backfill", repairCommand.GetProperty("safety_class").GetString()); + var repairArgs = repairCommand.GetProperty("args").EnumerateArray().Select(value => value.GetString()).ToArray(); + Assert.Equal("backfill-fold", repairArgs[0]); + Assert.Equal("--db", repairArgs[1]); + Assert.EndsWith(Path.Combine(".cdidx", "codeindex.db"), repairArgs[2], StringComparison.Ordinal); + Assert.DoesNotContain(repairArgs, value => value?.Contains("file:", StringComparison.Ordinal) == true); + Assert.DoesNotContain(repairArgs, value => value?.Contains("immutable=1", StringComparison.Ordinal) == true); } finally { @@ -1407,16 +1421,20 @@ public void RunStatus_HumanOutput_RelativeReadOnlyUriUsesWorkingDirectoryDbPath( SqliteConnection.ClearAllPools(); var (exitCode, stdout, stderr) = RunBuiltCli( - ["status", "--db", "file:codeindex.db?mode=ro"], + ["status", "--db", "file:codeindex.db?mode=ro", "--check=fold"], dbDirectory); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - Assert.Contains(dbPath, stdout); - Assert.Contains("cdidx backfill-fold --db", stdout); - Assert.Contains("cdidx index", stdout); - Assert.DoesNotContain("", stdout); - Assert.DoesNotContain("file:codeindex.db", stdout); + Assert.Equal(2, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("[repair] cdidx backfill-fold --db ", stderr, StringComparison.Ordinal); + Assert.Contains("codeindex.db", stderr, StringComparison.Ordinal); + Assert.Contains("reasons=fold_ready", stderr, StringComparison.Ordinal); + Assert.Contains("action=backfill_fold", stderr, StringComparison.Ordinal); + Assert.Contains("mutation=database_write", stderr, StringComparison.Ordinal); + Assert.Contains("safety=fold_backfill", stderr, StringComparison.Ordinal); + Assert.DoesNotContain("", stderr, StringComparison.Ordinal); + Assert.DoesNotContain("file:codeindex.db", stderr, StringComparison.Ordinal); + Assert.DoesNotContain("mode=ro", stderr, StringComparison.Ordinal); } finally { @@ -2396,6 +2414,7 @@ public void RunStatus_CheckJson_ReturnsSuccessWhenIndexMatchesWorkspace() Assert.True(headFreshness.GetProperty("workspace_matches_index").GetBoolean()); Assert.Equal(1, check.GetProperty("matched_file_count").GetInt32()); Assert.Contains("index fresh", json.GetProperty("summary").GetString()); + Assert.False(json.TryGetProperty("repair_commands", out _)); var queryContext = json.GetProperty("query_context"); Assert.Equal(QueryCommandRunner.StatusCheckModeExplicit, queryContext.GetProperty("check_mode").GetString()); Assert.Equal(json.GetProperty("stale_after_seconds").GetInt64(), queryContext.GetProperty("stale_after_seconds").GetInt64()); @@ -2577,7 +2596,13 @@ public void RunStatus_CheckJson_ReturnsStaleIndexWhenContentChecksumDiffers() Assert.Equal("src/app.cs", check.GetProperty("changed_files")[0].GetString()); var repairCommand = Assert.Single(json.GetProperty("repair_commands").EnumerateArray()); Assert.Equal("cdidx", repairCommand.GetProperty("name").GetString()); + Assert.Equal("index", repairCommand.GetProperty("action").GetString()); Assert.Equal("workspace_stale", repairCommand.GetProperty("reason").GetString()); + Assert.Equal( + ["workspace_stale"], + repairCommand.GetProperty("reasons").EnumerateArray().Select(value => value.GetString()).ToArray()); + Assert.Equal("index_write", repairCommand.GetProperty("mutation_class").GetString()); + Assert.Equal("workspace_refresh", repairCommand.GetProperty("safety_class").GetString()); var repairArgs = repairCommand.GetProperty("args").EnumerateArray().Select(arg => arg.GetString()).ToArray(); Assert.Contains("index", repairArgs); Assert.Contains(projectRoot, repairArgs); @@ -2592,6 +2617,118 @@ public void RunStatus_CheckJson_ReturnsStaleIndexWhenContentChecksumDiffers() } } + [Fact] + public void DeduplicateStatusRepairCommands_UsesStructuredIdentityAndStableReasonOrder_Issue4915() + { + static StatusRepairCommand Command( + string reason, + string target = "workspace-a", + string action = "index", + string mutationClass = "index_write", + string safetyClass = "metadata_refresh", + string safetyNote = "Refresh metadata.", + bool rebuild = false) + => new() + { + Name = "cdidx", + Action = action, + Args = rebuild ? ["index", target, "--rebuild"] : ["index", target], + Reason = reason, + Reasons = [reason], + MutationClass = mutationClass, + SafetyClass = safetyClass, + SafetyNotes = [safetyNote], + }; + + var commands = QueryCommandRunner.DeduplicateStatusRepairCommands( + [ + Command("graph_table_available"), + Command("issues_table_available"), + Command("other_target", target: "workspace-b"), + Command("other_options", rebuild: true), + Command("other_safety_class", safetyClass: "reference_graph_refresh"), + Command("other_safety_note", safetyNote: "Inspect reference caps."), + Command("other_mutation", mutationClass: "database_write"), + Command("other_action", action: "backfill_fold"), + ]); + + Assert.Equal(7, commands.Count); + Assert.Equal("graph_table_available", commands[0].Reason); + Assert.Equal( + ["graph_table_available", "issues_table_available"], + commands[0].Reasons); + Assert.Equal("workspace-b", commands[1].Args[1]); + Assert.Equal("--rebuild", commands[2].Args[2]); + Assert.Equal("reference_graph_refresh", commands[3].SafetyClass); + Assert.Equal("Inspect reference caps.", commands[4].SafetyNotes[0]); + Assert.Equal("database_write", commands[5].MutationClass); + Assert.Equal("backfill_fold", commands[6].Action); + Assert.Empty(QueryCommandRunner.DeduplicateStatusRepairCommands([])); + } + + [Fact] + public void RunStatus_Check_DeduplicatesRepairCommandsForJsonAndHumanOutput_Issue4915() + { + var containerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_repair_dedup"); + var projectRoot = Path.Combine(containerRoot, "workspace member with spaces"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + const string content = "class App {}\n"; + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), content); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", content); + + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(jsonStdout); + var repairCommands = document.RootElement.GetProperty("repair_commands").EnumerateArray().ToArray(); + var metadataRefresh = repairCommands[0]; + + Assert.Equal(2, jsonExitCode); + Assert.Equal(string.Empty, jsonStderr); + Assert.Equal(4, repairCommands.Length); + Assert.Equal( + ["metadata_refresh", "reference_graph_refresh", "full_rebuild", "fold_backfill"], + repairCommands.Select(command => command.GetProperty("safety_class").GetString()).ToArray()); + Assert.Equal("graph_table_available", metadataRefresh.GetProperty("reason").GetString()); + Assert.Equal( + [ + "graph_table_available", + "file_issues_data_current", + "csharp_symbol_name_ready", + "csharp_metadata_target_ready", + ], + metadataRefresh.GetProperty("reasons").EnumerateArray().Select(value => value.GetString()).ToArray()); + Assert.Equal("index", metadataRefresh.GetProperty("action").GetString()); + Assert.Equal("index_write", metadataRefresh.GetProperty("mutation_class").GetString()); + + var (humanExitCode, humanStdout, humanStderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check"], + _jsonOptions)); + var repairLines = humanStderr + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Where(line => line.StartsWith("[repair] ", StringComparison.Ordinal)) + .ToArray(); + + Assert.Equal(2, humanExitCode); + Assert.Equal(string.Empty, humanStdout); + Assert.Equal(4, repairLines.Length); + Assert.Contains($"cdidx index \"{projectRoot}\" --db \"{dbPath}\"", repairLines[0], StringComparison.Ordinal); + Assert.Contains( + "reasons=graph_table_available,file_issues_data_current,csharp_symbol_name_ready,csharp_metadata_target_ready", + repairLines[0], + StringComparison.Ordinal); + Assert.Equal(1, repairLines.Count(line => line.Contains("safety=metadata_refresh", StringComparison.Ordinal))); + } + finally + { + TestProjectHelper.DeleteDirectory(containerRoot); + } + } + [Fact] public void RunStatus_CheckJson_ReferenceGraphUnavailableUsesRefreshGuidance_Issue4620() { @@ -2699,7 +2836,10 @@ public void RunStatus_CheckJson_ReportsBackfillRepairCommandForFoldDegradation_I Assert.Equal(string.Empty, stderr); Assert.Equal("fold_ready", json.GetProperty("failed_checks")[0].GetString()); Assert.Equal("cdidx", repairCommand.GetProperty("name").GetString()); + Assert.Equal("backfill_fold", repairCommand.GetProperty("action").GetString()); Assert.Equal("fold_ready", repairCommand.GetProperty("reason").GetString()); + Assert.Equal("database_write", repairCommand.GetProperty("mutation_class").GetString()); + Assert.Equal("fold_backfill", repairCommand.GetProperty("safety_class").GetString()); Assert.Equal("backfill-fold", repairArgs[0]); Assert.Contains("--db", repairArgs); Assert.Contains(dbPath, repairArgs); From ddac4cb66d486ebbd684ed1a6754007487a40ea0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 14:57:26 +0900 Subject: [PATCH 2/3] Harden status repair command quoting (#4915) --- AGENT_GUIDE.md | 2 +- DEVELOPER_GUIDE.md | 4 ++-- README.md | 4 ++-- USER_GUIDE.md | 4 ++-- .../Cli/QueryCommandRunner.RepairCommands.cs | 4 +++- tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs | 11 +++++++++-- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index 2a1dcd311..89b9dbbfb 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -136,7 +136,7 @@ CI watching must be bounded. Do not loop indefinitely. ## Status Contract - `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `graph_data_current`, `index_complete`, `index_incomplete_reasons`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `language_readiness`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `head_freshness`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `unknown_extension_files`, `unknown_extension_files_truncated`, `unknown_extension_file_path_limit`, `unknown_extension_extension_counts`, `unknown_extension_category_counts`, `unknown_extension_groups`, `extractors`, `git_executable`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `db_file_mode`, `database_permission_policy`, `database_permission_diagnostics`, `mac_profile`, `mac_profile_diagnostics`, `db_size_bytes`, `wal_size_bytes`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`), `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`), `maintenance_guidance`, WAL checkpoint diagnostics (`read_only_fallback`, `wal_checkpoint_attempted`, `wal_checkpoint_succeeded`, `wal_checkpoint_skipped_reason`, `wal_checkpoint_failure_reason`, `wal_checkpoint_busy`, `wal_checkpoint_log_page_count`, `wal_checkpoint_checkpointed_page_count`, `wal_checkpoint_remaining_page_count`, `read_only_immutable_fallback`, `wal_stale_snapshot_risk`, `wal_stale_snapshot_reason`), `symbol_kinds`, `symbols_by_language`, status kind cap metadata (`symbol_kind_limit`, `symbol_kind_name_limit`, `symbol_kind_total_count`, `symbol_kind_omitted_count`, `symbol_kind_names_truncated`, `symbols_by_language_kind_total_counts`, `symbols_by_language_kind_omitted_counts`, `symbols_by_language_kind_names_truncated`), `process`, `last_index_run`, `last_failed_or_partial_index_run`, `last_failed_or_partial_index_run.progress_persisted`, `last_failed_or_partial_index_run.recovery_hint`, `last_failed_or_partial_index_run.file_errors`, `last_workspace_freshened_at`, `hooks`, `hook_diagnostics`, `trust_overrides`, MCP-only `mcp_session`, `mcp.rate_limit.bucket_limit`, `mcp.rate_limit.bucket_limit_rejection_count`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields and `repair_commands`. -- `status --check` repair actions are structured by `name`, `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`. Preserve compatibility `reason` as the first trigger and ordered `reasons` as the complete trigger set. Deduplicate only exact structured identities; different targets, options, actions, mutation classes, or safety semantics must remain distinct. JSON and human output must use the same deduplicated order, and human command-token quoting must remain consistent. +- `status --check` repair actions are structured by `name`, `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`. Preserve compatibility `reason` as the first trigger and ordered `reasons` as the complete trigger set. Deduplicate only exact structured identities; different targets, options, actions, mutation classes, or safety semantics must remain distinct. JSON and human output must use the same deduplicated order, and human platform-aware shell quoting must remain consistent. - `maintenance_guidance.fts_optimization` is the shared, read-only recommendation contract for status, explain, optimize preview, and optimize execution. Keep `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` synchronized; stale or unavailable snapshots must not recommend mutation. - A valid CLI `status --stale-after ` implies the workspace check. Check-mode JSON includes `query_context.check_mode` (`explicit` or `implied_by_stale_after`) and `query_context.stale_after_seconds`; ordinary status JSON omits `query_context`. - `status --explain` derives accepted top-level keys from the same source-generated `StatusResult` serializer metadata as `status --json`, excludes ignored properties, and supports bounded dot-separated member paths without reading runtime values. Major readiness, trust, extension, maintenance, and cap-hit sections return structured meaning, source, dependencies, interpretation, and repair guidance; unknown input is sanitized and returns bounded valid candidates. Bounded status explain envelopes also omit database paths, timings, indexed HEADs, and stable-at timestamps. diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 893a3b8d9..18b077caf 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -991,7 +991,7 @@ Current stable codes and triggers: | Schema discovery cache | `DbReader` schema discovery uses a process-level cache keyed by the normalized DB path. Column and index results are stored and returned as immutable `FrozenSet` snapshots, so callers cannot mutate schema decisions shared by other readers. Path states are reference-counted by live `DbSchemaCache` owners, are removed when the final owning `DbContext` is disposed, and are never evicted while an owner remains active. The cache checks `PRAGMA schema_version` before serving a lookup so SQLite DDL performed by cdidx or an external `sqlite3` session invalidates stale snapshots. Manual schema edits outside cdidx are still unsupported operationally; run `cdidx validate` after such edits before trusting query output. | | Batch trust marker | Index write batches stamp `codeindex_meta.batch_in_progress=true` before starting a mutation transaction and clear it inside the transaction that commits the matching rows and readiness metadata. If the indexer crashes after the marker is written but before the commit clears it, every later open reports `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` without changing readiness metadata. The explicit `index --rebuild` repair path alone demotes readiness before rebuilding. Gracefully handled per-file errors clear the marker after rollback; orphaned markers are reserved for interrupted or crashed batches whose trust metadata should not be treated as clean. | | Read-only opens and fallback | Query-only commands open with SQLite `Mode=ReadOnly` from the first attempt, retain WAL visibility, and never use writable setup or opportunistic migrations. A write-capable intent may still fall back to read-only when writable journal/WAL setup fails; an explicitly supplied `immutable=1` URI is the opt-in stale-snapshot escape hatch. If a WAL is present and must be observed from storage that cannot expose its sidecars, copy `.db`, `.db-wal`, and `.db-shm` together to a readable location or use a SQLite backup from an environment that can open the full WAL set. | -| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds structured `repair_commands[]` entries with `name`, `action`, `args`, `mutation_class`, `safety_class`, `safety_notes`, compatibility `reason`, and ordered `reasons`. Exact structured identities are deduplicated and aggregate reasons in check priority order; differences in target, options, action, mutation class, or safety semantics prevent merging. Human check output uses the same command set and preserves command-token quoting for each `[repair]` action. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | +| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds structured `repair_commands[]` entries with `name`, `action`, `args`, `mutation_class`, `safety_class`, `safety_notes`, compatibility `reason`, and ordered `reasons`. Exact structured identities are deduplicated and aggregate reasons in check priority order; differences in target, options, action, mutation class, or safety semantics prevent merging. Human check output uses the same command set and preserves platform-aware shell quoting for each `[repair]` action. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | | Maintenance thresholds | WAL guidance flips to `checkpoint_recommended` at `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (default 64 MiB). Freelist guidance flips to `vacuum_recommended` at `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (default `0.20`). Invalid or out-of-range env values fall back to defaults. | | Maintenance command precedence | `maintenance_guidance.recommended_command` preserves the existing vacuum-then-checkpoint precedence. It returns `cdidx optimize --db ` only when WAL and freelist states are both exactly `ok` and the trusted FTS write snapshot reaches its threshold; an `unknown` higher-priority state or a stale/unavailable FTS snapshot never selects an optimize command. | | Page attribution | `status --json` reads SQLite page ownership without mutating the source. It prefers `dbstat` page bytes and otherwise traverses a bounded b-tree/WAL snapshot (at most 1,000,000 pages and 100,000 schema objects); when a live WAL connection is not already backed by a stable detached file set, the fallback first makes a cancellation-aware private backup of that connection's active read snapshot so a concurrent commit cannot mix generations. `allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` equals `logical_database_bytes`; table/index and internal/leaf/overflow/other page subtotals each reconcile to `allocated_object_bytes`. Payload, unused space, and structural overhead form a second reconciliation. Physical main/WAL/SHM bytes are reported separately. Output is capped at 20 object names, each support-sanitized to at most 128 characters. A failed or inconsistent probe returns `available=false`, a stable `unavailable_reason`, and null/omitted attribution values rather than zeros. | @@ -4431,7 +4431,7 @@ apply 時は `PRAGMA optimize` を実行します。 | schema discovery cache | `DbReader` の schema discovery は正規化済み DB path を key にした process-level cache を使います。column / index 結果は immutable な `FrozenSet` snapshot として保存・返却されるため、caller が他の reader と共有する schema 判定を変更することはできません。path state は有効な `DbSchemaCache` owner により参照カウントされ、最後の owner `DbContext` が dispose されると削除され、owner が active な間は退避されません。lookup 前に `PRAGMA schema_version` を確認するため、cdidx や外部 `sqlite3` session による SQLite DDL は stale snapshot を invalidate します。cdidx 外での手動 schema edit は運用上 unsupported であり、その後は query output を信頼する前に `cdidx validate` を実行してください。 | | batch trust marker | index write batch は mutation transaction を始める前に `codeindex_meta.batch_in_progress=true` を stamp し、対応する row と readiness metadata を commit する transaction 内で clear します。marker が書かれた後、clear される前に indexer が crash した場合、その後のすべての open は readiness metadata を変更せずに `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` と警告します。readiness を degrade するのは、明示的な `index --rebuild` repair path だけです。file ごとの error が graceful に処理された場合は rollback 後に marker を clear するため、orphaned marker は interrupted / crashed batch の trust metadata を clean と扱わないための signal です。 | | read-only open / fallback | query-only command は最初の試行から SQLite `Mode=ReadOnly` で開き、WAL の可視性を保ちながら writable setup と opportunistic migration を実行しません。write-capable intent は journal/WAL setup に失敗した場合に read-only へ fallback することがあります。明示的な `immutable=1` URI は stale snapshot を許容する opt-in escape hatch です。sidecar を公開できない storage 上の WAL を観測する必要がある場合は、`.db` / `.db-wal` / `.db-shm` をまとめて readable location に copy するか、full WAL set を open できる環境で SQLite backup を使います。 | -| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`action`、`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason`、順序付きの `reasons` を返します。完全に同一の構造化 identity は deduplicate して check の優先順に reason を集約し、target、option、action、mutation class、安全性 semantics が異なる場合は merge しません。human check output も同じ command set を使い、各 `[repair]` action で command token の quote を維持します。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | +| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`action`、`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason`、順序付きの `reasons` を返します。完全に同一の構造化 identity は deduplicate して check の優先順に reason を集約し、target、option、action、mutation class、安全性 semantics が異なる場合は merge しません。human check output も同じ command set を使い、各 `[repair]` action で platform-aware な shell quote を維持します。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | | maintenance threshold | WAL guidance は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (既定 64 MiB) 以上で `checkpoint_recommended` になります。freelist guidance は `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (既定 `0.20`) 以上で `vacuum_recommended` になります。不正・範囲外の環境変数値は既定値へ戻します。 | | maintenance command の優先順位 | `maintenance_guidance.recommended_command` は既存の vacuum、checkpoint の順序を維持します。WAL と freelist の state が両方とも厳密に `ok` で、信頼できる FTS write snapshot が threshold に達した場合だけ `cdidx optimize --db ` を返します。上位 state が `unknown` の場合や FTS snapshot が stale / unavailable の場合は optimize command を選択しません。 | | page attribution | `status --json` は source を変更せずに SQLite page ownership を読み取ります。`dbstat` page byte を優先し、利用できない場合は件数上限付きの b-tree / WAL snapshot traversal(最大1,000,000 page、100,000 schema object)へ fallback します。live WAL connection が安定した detached file set に基づいていない場合、fallback は先にその connection の active read snapshot を cancellation 対応の private backup に固定し、並行 commit による世代混在を防ぎます。`allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` は `logical_database_bytes` と一致し、table/index と internal/leaf/overflow/other page の小計はそれぞれ `allocated_object_bytes` と一致します。payload、unused space、structural overhead も別に再照合されます。物理 main/WAL/SHM byte は分離して報告します。出力する object 名は最大20件で、各名称は support-safe sanitizer により最大128文字になります。probe の失敗・不整合時は `available=false`、安定した `unavailable_reason`、null / 省略された attribution 値を返し、ゼロとして偽装しません。 | diff --git a/README.md b/README.md index 210af505f..e8ba2c4f3 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ with the same structured action, arguments, mutation class, and safety semantics are emitted once with their reasons aggregated in check priority order. Different targets, options, actions, or safety semantics remain separate even if their rendered command text looks similar. Human check output follows the same -deduplication and preserves command-token quoting in one `[repair]` line per +deduplication and preserves platform-aware shell quoting in one `[repair]` line per structured action. Supplying `status --stale-after ` implies the workspace freshness check. @@ -827,7 +827,7 @@ check mode が失敗した場合、各 `repair_commands[]` entry は `name`、`a action、argument、mutation class、安全性 semantics を持つ command は1件だけ返し、 reason は check の優先順で集約します。target、option、action、安全性 semantics が 異なる command は、表示上の command text が似ていても別々に維持します。human -check output も同じ deduplication を使い、command token の quote を維持した +check output も同じ deduplication を使い、platform-aware な shell quote を維持した `[repair]` line を構造化 action ごとに1件だけ表示します。 `status --stale-after ` を指定すると workspace freshness check を暗黙に有効化します。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 5966fadb2..331083dec 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1299,7 +1299,7 @@ Here, drift refusal means drift detected through the final pre-publication valid `cdidx optimize --dry-run --json` previews FTS5 maintenance without acquiring the index lock or changing the source DB/WAL/SHM files. The result includes DB/core-table/FTS sizes, page and freelist indicators, the incremental-write recommendation, current lock and readiness state, a previous-duration estimate when available, and the operations a real optimize would perform, including its repair-mode schema initialization or migration check. `object_sizes_measurement` distinguishes exact `dbstat` page bytes from the logical-payload fallback used when SQLite does not provide `dbstat`. -`status --check --json` returns structured `repair_commands` for failed checks. Each entry identifies its `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`; `reason` remains the first trigger for compatibility, while `reasons` contains every trigger in deterministic check order. Identical structured actions are emitted once, but commands with different targets, options, actions, or safety semantics stay separate. Human check output applies the same rule and preserves command-token quoting for each emitted `[repair]` command. Writable repair arguments use normalized local paths rather than preserving read-only `file:` URI options. +`status --check --json` returns structured `repair_commands` for failed checks. Each entry identifies its `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`; `reason` remains the first trigger for compatibility, while `reasons` contains every trigger in deterministic check order. Identical structured actions are emitted once, but commands with different targets, options, actions, or safety semantics stay separate. Human check output applies the same rule and preserves platform-aware shell quoting for each emitted `[repair]` command. Writable repair arguments use normalized local paths rather than preserving read-only `file:` URI options. ### Search code @@ -4686,7 +4686,7 @@ DB を read-only で開いて SQLite の `PRAGMA integrity_check` を実行し DB / WAL の肥大や空き page を確認したい場合は `status --json` の `maintenance_guidance` を見ます。既定では WAL が 64 MiB 以上で `checkpoint_recommended`、`freelist_count / page_count` が 0.20 以上で `vacuum_recommended` になり、`recommended_command` と `post_maintenance_follow_up` が返ります。しきい値は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` と `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` で調整できます。 -`status --check --json` は failed check に対する構造化 `repair_commands` を返します。各 entry は `action`、`args`、`mutation_class`、`safety_class`、`safety_notes` を持ち、互換用の `reason` は最初の trigger、`reasons` は deterministic な check 順序ですべての trigger を保持します。同一の構造化 action は1件だけ返しますが、target、option、action、安全性 semantics が異なる command は別々に維持します。human check output にも同じ規則を適用し、各 `[repair]` command で command token の quote を維持します。書き込み用の修復 argument には read-only の `file:` URI option を残さず、正規化済み local path を使います。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 +`status --check --json` は failed check に対する構造化 `repair_commands` を返します。各 entry は `action`、`args`、`mutation_class`、`safety_class`、`safety_notes` を持ち、互換用の `reason` は最初の trigger、`reasons` は deterministic な check 順序ですべての trigger を保持します。同一の構造化 action は1件だけ返しますが、target、option、action、安全性 semantics が異なる command は別々に維持します。human check output にも同じ規則を適用し、各 `[repair]` command で platform-aware な shell quote を維持します。書き込み用の修復 argument には read-only の `file:` URI option を残さず、正規化済み local path を使います。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 ```bash cdidx vacuum --dry-run --json # 回収見積もりと maintenance guidance だけを確認 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs index 0af96650e..7512fefb0 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs @@ -69,7 +69,9 @@ private static string QuoteCommandArgument(string value) } private static string RenderStatusRepairCommand(StatusRepairCommand command) - => string.Join(' ', new[] { command.Name }.Concat(command.Args).Select(QuoteCommandToken)); + => ExcerptRecoveryCommandFormatter.RenderDisplayCommand( + new[] { command.Name }.Concat(command.Args).ToArray(), + OperatingSystem.IsWindows() ? RecoveryCommandShell.PowerShell : RecoveryCommandShell.PosixSh); private static string QuoteCommandToken(string value) { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs index 2da352e05..6555dcff0 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs @@ -2670,7 +2670,7 @@ static StatusRepairCommand Command( public void RunStatus_Check_DeduplicatesRepairCommandsForJsonAndHumanOutput_Issue4915() { var containerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_repair_dedup"); - var projectRoot = Path.Combine(containerRoot, "workspace member with spaces"); + var projectRoot = Path.Combine(containerRoot, "workspace;printf_PWNED'$value"); try { Directory.CreateDirectory(Path.Combine(projectRoot, "src")); @@ -2716,7 +2716,14 @@ public void RunStatus_Check_DeduplicatesRepairCommandsForJsonAndHumanOutput_Issu Assert.Equal(2, humanExitCode); Assert.Equal(string.Empty, humanStdout); Assert.Equal(4, repairLines.Length); - Assert.Contains($"cdidx index \"{projectRoot}\" --db \"{dbPath}\"", repairLines[0], StringComparison.Ordinal); + var quotedProjectRoot = OperatingSystem.IsWindows() + ? $"'{projectRoot.Replace("'", "''", StringComparison.Ordinal)}'" + : $"'{projectRoot.Replace("'", "'\\''", StringComparison.Ordinal)}'"; + var quotedDbPath = OperatingSystem.IsWindows() + ? $"'{dbPath.Replace("'", "''", StringComparison.Ordinal)}'" + : $"'{dbPath.Replace("'", "'\\''", StringComparison.Ordinal)}'"; + Assert.Contains($"cdidx index {quotedProjectRoot} --db {quotedDbPath}", repairLines[0], StringComparison.Ordinal); + Assert.DoesNotContain($"index {projectRoot}", repairLines[0], StringComparison.Ordinal); Assert.Contains( "reasons=graph_table_available,file_issues_data_current,csharp_symbol_name_ready,csharp_metadata_target_ready", repairLines[0], From 9bdb8a32fd2ba3d64b408cce54947766889800a0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 15:42:07 +0900 Subject: [PATCH 3/3] Keep status repair diagnostics single-line (#4915) --- AGENT_GUIDE.md | 2 +- DEVELOPER_GUIDE.md | 4 +-- README.md | 6 +++-- USER_GUIDE.md | 4 +-- .../Cli/QueryCommandRunner.RepairCommands.cs | 25 +++++++++++++++++-- .../QueryCommandRunnerFilesTests.cs | 23 +++++++++++++++++ 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index 89b9dbbfb..178ce2b8f 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -136,7 +136,7 @@ CI watching must be bounded. Do not loop indefinitely. ## Status Contract - `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `graph_data_current`, `index_complete`, `index_incomplete_reasons`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `language_readiness`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `head_freshness`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `unknown_extension_files`, `unknown_extension_files_truncated`, `unknown_extension_file_path_limit`, `unknown_extension_extension_counts`, `unknown_extension_category_counts`, `unknown_extension_groups`, `extractors`, `git_executable`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `db_file_mode`, `database_permission_policy`, `database_permission_diagnostics`, `mac_profile`, `mac_profile_diagnostics`, `db_size_bytes`, `wal_size_bytes`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`), `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`), `maintenance_guidance`, WAL checkpoint diagnostics (`read_only_fallback`, `wal_checkpoint_attempted`, `wal_checkpoint_succeeded`, `wal_checkpoint_skipped_reason`, `wal_checkpoint_failure_reason`, `wal_checkpoint_busy`, `wal_checkpoint_log_page_count`, `wal_checkpoint_checkpointed_page_count`, `wal_checkpoint_remaining_page_count`, `read_only_immutable_fallback`, `wal_stale_snapshot_risk`, `wal_stale_snapshot_reason`), `symbol_kinds`, `symbols_by_language`, status kind cap metadata (`symbol_kind_limit`, `symbol_kind_name_limit`, `symbol_kind_total_count`, `symbol_kind_omitted_count`, `symbol_kind_names_truncated`, `symbols_by_language_kind_total_counts`, `symbols_by_language_kind_omitted_counts`, `symbols_by_language_kind_names_truncated`), `process`, `last_index_run`, `last_failed_or_partial_index_run`, `last_failed_or_partial_index_run.progress_persisted`, `last_failed_or_partial_index_run.recovery_hint`, `last_failed_or_partial_index_run.file_errors`, `last_workspace_freshened_at`, `hooks`, `hook_diagnostics`, `trust_overrides`, MCP-only `mcp_session`, `mcp.rate_limit.bucket_limit`, `mcp.rate_limit.bucket_limit_rejection_count`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields and `repair_commands`. -- `status --check` repair actions are structured by `name`, `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`. Preserve compatibility `reason` as the first trigger and ordered `reasons` as the complete trigger set. Deduplicate only exact structured identities; different targets, options, actions, mutation classes, or safety semantics must remain distinct. JSON and human output must use the same deduplicated order, and human platform-aware shell quoting must remain consistent. +- `status --check` repair actions are structured by `name`, `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`. Preserve compatibility `reason` as the first trigger and ordered `reasons` as the complete trigger set. Deduplicate only exact structured identities; different targets, options, actions, mutation classes, or safety semantics must remain distinct. JSON and human output must use the same deduplicated order. Human output must preserve platform-aware shell quoting, visibly escape control characters to keep each repair action on one diagnostic line, and leave structured JSON `args` unchanged. - `maintenance_guidance.fts_optimization` is the shared, read-only recommendation contract for status, explain, optimize preview, and optimize execution. Keep `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` synchronized; stale or unavailable snapshots must not recommend mutation. - A valid CLI `status --stale-after ` implies the workspace check. Check-mode JSON includes `query_context.check_mode` (`explicit` or `implied_by_stale_after`) and `query_context.stale_after_seconds`; ordinary status JSON omits `query_context`. - `status --explain` derives accepted top-level keys from the same source-generated `StatusResult` serializer metadata as `status --json`, excludes ignored properties, and supports bounded dot-separated member paths without reading runtime values. Major readiness, trust, extension, maintenance, and cap-hit sections return structured meaning, source, dependencies, interpretation, and repair guidance; unknown input is sanitized and returns bounded valid candidates. Bounded status explain envelopes also omit database paths, timings, indexed HEADs, and stable-at timestamps. diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 18b077caf..3bff3b8c5 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -991,7 +991,7 @@ Current stable codes and triggers: | Schema discovery cache | `DbReader` schema discovery uses a process-level cache keyed by the normalized DB path. Column and index results are stored and returned as immutable `FrozenSet` snapshots, so callers cannot mutate schema decisions shared by other readers. Path states are reference-counted by live `DbSchemaCache` owners, are removed when the final owning `DbContext` is disposed, and are never evicted while an owner remains active. The cache checks `PRAGMA schema_version` before serving a lookup so SQLite DDL performed by cdidx or an external `sqlite3` session invalidates stale snapshots. Manual schema edits outside cdidx are still unsupported operationally; run `cdidx validate` after such edits before trusting query output. | | Batch trust marker | Index write batches stamp `codeindex_meta.batch_in_progress=true` before starting a mutation transaction and clear it inside the transaction that commits the matching rows and readiness metadata. If the indexer crashes after the marker is written but before the commit clears it, every later open reports `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` without changing readiness metadata. The explicit `index --rebuild` repair path alone demotes readiness before rebuilding. Gracefully handled per-file errors clear the marker after rollback; orphaned markers are reserved for interrupted or crashed batches whose trust metadata should not be treated as clean. | | Read-only opens and fallback | Query-only commands open with SQLite `Mode=ReadOnly` from the first attempt, retain WAL visibility, and never use writable setup or opportunistic migrations. A write-capable intent may still fall back to read-only when writable journal/WAL setup fails; an explicitly supplied `immutable=1` URI is the opt-in stale-snapshot escape hatch. If a WAL is present and must be observed from storage that cannot expose its sidecars, copy `.db`, `.db-wal`, and `.db-shm` together to a readable location or use a SQLite backup from an environment that can open the full WAL set. | -| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds structured `repair_commands[]` entries with `name`, `action`, `args`, `mutation_class`, `safety_class`, `safety_notes`, compatibility `reason`, and ordered `reasons`. Exact structured identities are deduplicated and aggregate reasons in check priority order; differences in target, options, action, mutation class, or safety semantics prevent merging. Human check output uses the same command set and preserves platform-aware shell quoting for each `[repair]` action. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | +| Status pragma diagnostics | `status --json` exposes the selected read-only connection under `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) and resolved connection values under `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`). It also exposes prepared-command cache counters under `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) for automation and support diagnostics. `maintenance_guidance` derives `wal_state`, `freelist_ratio`, `freelist_state`, `estimated_*_reclaimable`, `auto_vacuum_mode(_name)`, `recommended_command`, and `post_maintenance_follow_up` from those raw metrics without changing the raw values. Its nested `fts_optimization` uses the same pure evaluator as optimize preview and execution, exposing `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state` without writing to the database. `status --check --json` adds structured `repair_commands[]` entries with `name`, `action`, `args`, `mutation_class`, `safety_class`, `safety_notes`, compatibility `reason`, and ordered `reasons`. Exact structured identities are deduplicated and aggregate reasons in check priority order; differences in target, options, action, mutation class, or safety semantics prevent merging. Human check output uses the same command set, preserves platform-aware shell quoting, visibly escapes control characters to keep each `[repair]` action on one diagnostic line, and leaves structured JSON `args` unchanged. `last_failed_or_partial_index_run` exposes bounded failed/partial index context (`status`, `mode`, timings, counts, stable error code, reason, `progress_persisted`, and bounded `recovery_hint`) and must not include raw exception text or file paths. | | Maintenance thresholds | WAL guidance flips to `checkpoint_recommended` at `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (default 64 MiB). Freelist guidance flips to `vacuum_recommended` at `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (default `0.20`). Invalid or out-of-range env values fall back to defaults. | | Maintenance command precedence | `maintenance_guidance.recommended_command` preserves the existing vacuum-then-checkpoint precedence. It returns `cdidx optimize --db ` only when WAL and freelist states are both exactly `ok` and the trusted FTS write snapshot reaches its threshold; an `unknown` higher-priority state or a stale/unavailable FTS snapshot never selects an optimize command. | | Page attribution | `status --json` reads SQLite page ownership without mutating the source. It prefers `dbstat` page bytes and otherwise traverses a bounded b-tree/WAL snapshot (at most 1,000,000 pages and 100,000 schema objects); when a live WAL connection is not already backed by a stable detached file set, the fallback first makes a cancellation-aware private backup of that connection's active read snapshot so a concurrent commit cannot mix generations. `allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` equals `logical_database_bytes`; table/index and internal/leaf/overflow/other page subtotals each reconcile to `allocated_object_bytes`. Payload, unused space, and structural overhead form a second reconciliation. Physical main/WAL/SHM bytes are reported separately. Output is capped at 20 object names, each support-sanitized to at most 128 characters. A failed or inconsistent probe returns `available=false`, a stable `unavailable_reason`, and null/omitted attribution values rather than zeros. | @@ -4431,7 +4431,7 @@ apply 時は `PRAGMA optimize` を実行します。 | schema discovery cache | `DbReader` の schema discovery は正規化済み DB path を key にした process-level cache を使います。column / index 結果は immutable な `FrozenSet` snapshot として保存・返却されるため、caller が他の reader と共有する schema 判定を変更することはできません。path state は有効な `DbSchemaCache` owner により参照カウントされ、最後の owner `DbContext` が dispose されると削除され、owner が active な間は退避されません。lookup 前に `PRAGMA schema_version` を確認するため、cdidx や外部 `sqlite3` session による SQLite DDL は stale snapshot を invalidate します。cdidx 外での手動 schema edit は運用上 unsupported であり、その後は query output を信頼する前に `cdidx validate` を実行してください。 | | batch trust marker | index write batch は mutation transaction を始める前に `codeindex_meta.batch_in_progress=true` を stamp し、対応する row と readiness metadata を commit する transaction 内で clear します。marker が書かれた後、clear される前に indexer が crash した場合、その後のすべての open は readiness metadata を変更せずに `Last batch did not complete; run cdidx index --rebuild to re-index from a known clean state.` と警告します。readiness を degrade するのは、明示的な `index --rebuild` repair path だけです。file ごとの error が graceful に処理された場合は rollback 後に marker を clear するため、orphaned marker は interrupted / crashed batch の trust metadata を clean と扱わないための signal です。 | | read-only open / fallback | query-only command は最初の試行から SQLite `Mode=ReadOnly` で開き、WAL の可視性を保ちながら writable setup と opportunistic migration を実行しません。write-capable intent は journal/WAL setup に失敗した場合に read-only へ fallback することがあります。明示的な `immutable=1` URI は stale snapshot を許容する opt-in escape hatch です。sidecar を公開できない storage 上の WAL を観測する必要がある場合は、`.db` / `.db-wal` / `.db-shm` をまとめて readable location に copy するか、full WAL set を open できる環境で SQLite backup を使います。 | -| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`action`、`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason`、順序付きの `reasons` を返します。完全に同一の構造化 identity は deduplicate して check の優先順に reason を集約し、target、option、action、mutation class、安全性 semantics が異なる場合は merge しません。human check output も同じ command set を使い、各 `[repair]` action で platform-aware な shell quote を維持します。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | +| status pragma diagnostics | `status --json` は選択された read-only connection を `sqlite_connection_policy` (`active_mode=read_only`, `open_mode=read_only`) で、解決済みの接続値を `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `busy_timeout_ms`, `page_count`, `freelist_count`, `page_size`, `auto_vacuum`) で公開します。また、prepared command cache counter を `prepared_command_cache` (`count`, `capacity`, `hit_count`, `miss_count`, `eviction_count`) で公開します。`maintenance_guidance` は raw 値を変えずに `wal_state`、`freelist_ratio`、`freelist_state`、`estimated_*_reclaimable`、`auto_vacuum_mode(_name)`、`recommended_command`、`post_maintenance_follow_up` を派生します。nested な `fts_optimization` は optimize preview / execution と同じ純粋 evaluator を使い、database に書き込まず `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開します。`status --check --json` は `repair_commands[]` に `name`、`action`、`args`、`mutation_class`、`safety_class`、`safety_notes`、互換用の `reason`、順序付きの `reasons` を返します。完全に同一の構造化 identity は deduplicate して check の優先順に reason を集約し、target、option、action、mutation class、安全性 semantics が異なる場合は merge しません。human check output も同じ command set を使い、platform-aware な shell quote を維持し、control character を可視 escape して各 `[repair]` action を1行に保ちます。構造化 JSON の `args` は変更しません。`last_failed_or_partial_index_run` は bounded な failed / partial index context (`status`、`mode`、timing、count、stable error code、reason、`progress_persisted`、bounded な `recovery_hint`) のみを公開し、raw exception text や file path を含めてはいけません。 | | maintenance threshold | WAL guidance は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` (既定 64 MiB) 以上で `checkpoint_recommended` になります。freelist guidance は `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` (既定 `0.20`) 以上で `vacuum_recommended` になります。不正・範囲外の環境変数値は既定値へ戻します。 | | maintenance command の優先順位 | `maintenance_guidance.recommended_command` は既存の vacuum、checkpoint の順序を維持します。WAL と freelist の state が両方とも厳密に `ok` で、信頼できる FTS write snapshot が threshold に達した場合だけ `cdidx optimize --db ` を返します。上位 state が `unknown` の場合や FTS snapshot が stale / unavailable の場合は optimize command を選択しません。 | | page attribution | `status --json` は source を変更せずに SQLite page ownership を読み取ります。`dbstat` page byte を優先し、利用できない場合は件数上限付きの b-tree / WAL snapshot traversal(最大1,000,000 page、100,000 schema object)へ fallback します。live WAL connection が安定した detached file set に基づいていない場合、fallback は先にその connection の active read snapshot を cancellation 対応の private backup に固定し、並行 commit による世代混在を防ぎます。`allocated_object_bytes + freelist_bytes + unexplained_residual_bytes` は `logical_database_bytes` と一致し、table/index と internal/leaf/overflow/other page の小計はそれぞれ `allocated_object_bytes` と一致します。payload、unused space、structural overhead も別に再照合されます。物理 main/WAL/SHM byte は分離して報告します。出力する object 名は最大20件で、各名称は support-safe sanitizer により最大128文字になります。probe の失敗・不整合時は `available=false`、安定した `unavailable_reason`、null / 省略された attribution 値を返し、ゼロとして偽装しません。 | diff --git a/README.md b/README.md index e8ba2c4f3..37e7ec9f9 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,8 @@ are emitted once with their reasons aggregated in check priority order. Differen targets, options, actions, or safety semantics remain separate even if their rendered command text looks similar. Human check output follows the same deduplication and preserves platform-aware shell quoting in one `[repair]` line per -structured action. +structured action. Control characters are visibly escaped in human output so they +cannot forge additional lines; structured JSON `args` retain their original values. Supplying `status --stale-after ` implies the workspace freshness check. Check-mode JSON includes `query_context.check_mode` (`explicit` or @@ -828,7 +829,8 @@ action、argument、mutation class、安全性 semantics を持つ command は1 reason は check の優先順で集約します。target、option、action、安全性 semantics が 異なる command は、表示上の command text が似ていても別々に維持します。human check output も同じ deduplication を使い、platform-aware な shell quote を維持した -`[repair]` line を構造化 action ごとに1件だけ表示します。 +`[repair]` line を構造化 action ごとに1件だけ表示します。human output では control +character を可視 escape して偽の行を防ぎ、構造化 JSON の `args` は原値を維持します。 `status --stale-after ` を指定すると workspace freshness check を暗黙に有効化します。 check mode の JSON は `query_context.check_mode`(`explicit` または diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 331083dec..79f6a18a6 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1299,7 +1299,7 @@ Here, drift refusal means drift detected through the final pre-publication valid `cdidx optimize --dry-run --json` previews FTS5 maintenance without acquiring the index lock or changing the source DB/WAL/SHM files. The result includes DB/core-table/FTS sizes, page and freelist indicators, the incremental-write recommendation, current lock and readiness state, a previous-duration estimate when available, and the operations a real optimize would perform, including its repair-mode schema initialization or migration check. `object_sizes_measurement` distinguishes exact `dbstat` page bytes from the logical-payload fallback used when SQLite does not provide `dbstat`. -`status --check --json` returns structured `repair_commands` for failed checks. Each entry identifies its `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`; `reason` remains the first trigger for compatibility, while `reasons` contains every trigger in deterministic check order. Identical structured actions are emitted once, but commands with different targets, options, actions, or safety semantics stay separate. Human check output applies the same rule and preserves platform-aware shell quoting for each emitted `[repair]` command. Writable repair arguments use normalized local paths rather than preserving read-only `file:` URI options. +`status --check --json` returns structured `repair_commands` for failed checks. Each entry identifies its `action`, `args`, `mutation_class`, `safety_class`, and `safety_notes`; `reason` remains the first trigger for compatibility, while `reasons` contains every trigger in deterministic check order. Identical structured actions are emitted once, but commands with different targets, options, actions, or safety semantics stay separate. Human check output applies the same rule, preserves platform-aware shell quoting, and visibly escapes control characters so each `[repair]` command remains one diagnostic line; structured JSON `args` retain their original values. Writable repair arguments use normalized local paths rather than preserving read-only `file:` URI options. ### Search code @@ -4686,7 +4686,7 @@ DB を read-only で開いて SQLite の `PRAGMA integrity_check` を実行し DB / WAL の肥大や空き page を確認したい場合は `status --json` の `maintenance_guidance` を見ます。既定では WAL が 64 MiB 以上で `checkpoint_recommended`、`freelist_count / page_count` が 0.20 以上で `vacuum_recommended` になり、`recommended_command` と `post_maintenance_follow_up` が返ります。しきい値は `CDIDX_MAINTENANCE_WAL_WARN_BYTES` と `CDIDX_MAINTENANCE_FREELIST_WARN_RATIO` で調整できます。 -`status --check --json` は failed check に対する構造化 `repair_commands` を返します。各 entry は `action`、`args`、`mutation_class`、`safety_class`、`safety_notes` を持ち、互換用の `reason` は最初の trigger、`reasons` は deterministic な check 順序ですべての trigger を保持します。同一の構造化 action は1件だけ返しますが、target、option、action、安全性 semantics が異なる command は別々に維持します。human check output にも同じ規則を適用し、各 `[repair]` command で platform-aware な shell quote を維持します。書き込み用の修復 argument には read-only の `file:` URI option を残さず、正規化済み local path を使います。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 +`status --check --json` は failed check に対する構造化 `repair_commands` を返します。各 entry は `action`、`args`、`mutation_class`、`safety_class`、`safety_notes` を持ち、互換用の `reason` は最初の trigger、`reasons` は deterministic な check 順序ですべての trigger を保持します。同一の構造化 action は1件だけ返しますが、target、option、action、安全性 semantics が異なる command は別々に維持します。human check output にも同じ規則を適用し、platform-aware な shell quote と control character の可視 escape により、各 `[repair]` command を1行に維持します。構造化 JSON の `args` は原値を保持します。書き込み用の修復 argument には read-only の `file:` URI option を残さず、正規化済み local path を使います。前回の index が中断・失敗した情報が DB に残っている場合は、`last_failed_or_partial_index_run` に bounded metadata だけを返し、例外本文や file path は含めません。 ```bash cdidx vacuum --dry-run --json # 回収見積もりと maintenance guidance だけを確認 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs index 7512fefb0..e640bff1c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.RepairCommands.cs @@ -68,11 +68,32 @@ private static string QuoteCommandArgument(string value) return QuoteCommandToken(fullPath); } - private static string RenderStatusRepairCommand(StatusRepairCommand command) + internal static string RenderStatusRepairCommand(StatusRepairCommand command) => ExcerptRecoveryCommandFormatter.RenderDisplayCommand( - new[] { command.Name }.Concat(command.Args).ToArray(), + new[] { command.Name }.Concat(command.Args).Select(EscapeStatusRepairControlCharacters).ToArray(), OperatingSystem.IsWindows() ? RecoveryCommandShell.PowerShell : RecoveryCommandShell.PosixSh); + private static string EscapeStatusRepairControlCharacters(string value) + { + if (!value.Any(char.IsControl)) + return value; + + var escaped = new System.Text.StringBuilder(value.Length); + foreach (var character in value) + { + escaped.Append(character switch + { + '\r' => "\\r", + '\n' => "\\n", + '\t' => "\\t", + _ when char.IsControl(character) => $"\\u{(int)character:X4}", + _ => character.ToString(), + }); + } + + return escaped.ToString(); + } + private static string QuoteCommandToken(string value) { if (value.Length >= 2 && value[0] == '<' && value[^1] == '>') diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs index 6555dcff0..1c69f81b4 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs @@ -2666,6 +2666,29 @@ static StatusRepairCommand Command( Assert.Empty(QueryCommandRunner.DeduplicateStatusRepairCommands([])); } + [Fact] + public void RenderStatusRepairCommand_EscapesControlCharactersOnOneLine_Issue4915() + { + var command = new StatusRepairCommand + { + Name = "cdidx", + Action = "index", + Args = ["index", "workspace\n[repair] forged\u001b"], + Reason = "workspace_stale", + Reasons = ["workspace_stale"], + MutationClass = "index_write", + SafetyClass = "workspace_refresh", + }; + + var rendered = QueryCommandRunner.RenderStatusRepairCommand(command); + + Assert.Equal("cdidx index 'workspace\\n[repair] forged\\u001B'", rendered); + Assert.DoesNotContain('\n', rendered); + Assert.DoesNotContain('\r', rendered); + Assert.DoesNotContain('\u001b', rendered); + Assert.Equal("workspace\n[repair] forged\u001b", command.Args[1]); + } + [Fact] public void RunStatus_Check_DeduplicatesRepairCommandsForJsonAndHumanOutput_Issue4915() {