diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 389f38926..147a60726 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1455,7 +1455,7 @@ Current stable codes and triggers: | 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. | | Vacuum | `cdidx vacuum` runs `PRAGMA incremental_vacuum` against writable incremental-auto-vacuum DBs, and performs a one-time `PRAGMA auto_vacuum=INCREMENTAL` plus full `VACUUM` conversion for legacy no-autovacuum DBs. `cdidx vacuum --dry-run --json` estimates reclaimable pages/bytes without executing vacuum pragmas: logical metrics come from the artifact-preserving query snapshot, while physical before/after fields share one stable observation of the requested source DB/WAL/SHM set from the matching DB/WAL generation. Real `cdidx vacuum --json` reports logical database bytes (`page_count * page_size`) and requested-source main/WAL/SHM file lengths before and after maintenance, plus their physical file-set sum. Each logical before/after PRAGMA group is read from one SQLite read snapshot. Real after fields and maintenance guidance are finalized after a cancellation-aware best-effort truncate checkpoint and closure of the command-owned vacuum connection. An unchanged `data_version` across the logical-after snapshot, command-owned checkpoint, and stable source-state capture establishes the pre-close witness. The post-close query-only observer binds its logical snapshot to one stable requested-source file set. It accepts either the unchanged raw source generation or the specific checkpoint representation in which the witness has a non-empty WAL, the observer has a zero-length WAL, and both expose the same logical size signature (`page_count`, `freelist_count`, `page_size`, and `auto_vacuum`); this second case preserves coherent public size metrics but does not assert that application data stayed on the same generation. If an external writer changes that signature, or another raw generation does not fit that hot-WAL-to-zero-WAL checkpoint shape, all physical after fields and the `db_size_bytes_after` / `wal_size_bytes_after` aliases are unavailable (null in the result DTO and omitted from normal CLI JSON), and WAL guidance is `unknown`. A physical observation is accepted only when two complete file-identity reads agree and bracket the same DB/WAL generation; capture retries at most three times, and persistent instability or metadata-read failure makes the entire physical tuple and its aliases unavailable while WAL guidance becomes `unknown`. A confirmed absent sidecar is zero. `db_size_bytes_*` and `wal_size_bytes_*` remain compatibility aliases for `main_file_bytes_*` and `wal_file_bytes_*`; `pages_reclaimed` and `bytes_reclaimed` remain logical page reclamation rather than a physical file-size delta. Another active connection can legitimately retain WAL/SHM, so main-file or physical-file-set bytes need not equal logical database bytes. `physical_file_set_bytes_*` is the sum of logical file lengths, not filesystem allocated-block usage, and `wal_checkpoint_timing_note` documents the stable post-close observation point. | -| FTS optimize preview | `cdidx optimize --dry-run` and its `cdidx index --optimize --dry-run` alias open a `QueryOnly` snapshot, probe an existing lockfile without creating or acquiring it, and never run write PRAGMAs, schema setup, FTS control inserts, or metadata writes against the source DB/WAL/SHM set. Both entry points preserve an explicitly supplied `file:` URI, including `immutable=1`, for the query connection while filesystem probes use its normalized local path, so status and either preview apply identical stale-snapshot semantics even with a hot WAL. JSON reports size/freelist/readiness indicators, planned operations, and the same `fts_optimization` recommendation object as status, including the exact threshold and observed writes. A stale batch or unavailable legacy counter/page snapshot suppresses recommendation with a stable reason and state. Real execution reports the same object before and after optimize, including the reset counter, and performs the repair-mode schema initialization or migration check. Object sizes use `dbstat` page bytes when available and a labeled logical-payload fallback otherwise. A real optimize records its elapsed milliseconds so later previews can expose `estimated_duration_ms`. | +| FTS optimize preview | `cdidx optimize --dry-run` and its `cdidx index --optimize --dry-run` alias open a `QueryOnly` snapshot, probe an existing lockfile without creating or acquiring it, and never run write PRAGMAs, schema setup, FTS control inserts, or metadata writes against the source DB/WAL/SHM set. Both entry points preserve an explicitly supplied `file:` URI, including `immutable=1`, for the query connection while filesystem probes use its normalized local path, so status and either preview apply identical stale-snapshot semantics even with a hot WAL. Human and JSON path display uses the shared maintenance policy: caller-relative spelling remains visible by default, absolute inputs become ``, and `--show-paths` opts into the resolved absolute database path; internal snapshot and filesystem operations always retain the real path. JSON reports size/freelist/readiness indicators, planned operations, and the same `fts_optimization` recommendation object as status, including the exact threshold and observed writes. A stale batch or unavailable legacy counter/page snapshot suppresses recommendation with a stable reason and state. Real execution reports the same object before and after optimize, including the reset counter, and performs the repair-mode schema initialization or migration check. Object sizes use `dbstat` page bytes when available and a labeled logical-payload fallback otherwise. A real optimize records its elapsed milliseconds so later previews can expose `estimated_duration_ms`. | | Size and process diagnostics | `status --json` also reports `db_size_bytes`, `wal_size_bytes`, capped `symbol_kinds` / `symbols_by_language` kind maps with `symbol_kind_*` and `symbols_by_language_kind_*` overflow metadata when caps apply, current `process` heap/GC/working-set metrics, `last_index_run` metadata from successful CLI and MCP index runs, and `last_workspace_freshened_at` as the latest successful index/update timestamp. `last_index_run.bytes_read_skipped_file_count` and `bytes_read_incomplete` report whether unreadable files were omitted from the `bytes_read` total, while `last_index_run.diagnostics`, `diagnostic_count`, and `diagnostics_truncated` carry bounded warnings for best-effort index metadata writes that failed after the index data itself was successfully written. `indexed_at` still comes from indexed file rows, so partial or no-op updates can freshen the workspace without moving `indexed_at`. | | Memory tracing | `index --json --memory-trace` adds a `memory_timeline` block to the CLI index result and persists peak working-set MB into `last_index_run`; dry-run results also emit live `start`, `snapshot`, `scan`, and `finalize` samples but never persist run metadata. `index --dry-run --rebuild` bypasses destructive confirmation because it does not delete or rewrite the index. `CDIDX_MEM_WARN_MB=` prints a warning when the sampled working set crosses that threshold. | | Newer schema protection | Writable opens reject databases whose `PRAGMA user_version` contains readiness bits outside the current binary's `CurrentSchemaVersion` mask. Read-only status/query paths may still surface `index_newer_than_reader=true` as a degraded audit signal, but write-capable paths must fail with `E003_SCHEMA_TOO_NEW` so an older cdidx cannot silently rewrite a DB stamped by a newer one. | @@ -5572,7 +5572,7 @@ apply 時は `PRAGMA optimize` を実行します。 | 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 値を返し、ゼロとして偽装しません。 | | vacuum | `cdidx vacuum` は incremental-auto-vacuum DB では `PRAGMA incremental_vacuum` を実行し、legacy no-autovacuum DB では初回のみ `PRAGMA auto_vacuum=INCREMENTAL` と full `VACUUM` で変換します。`cdidx vacuum --dry-run --json` は vacuum pragma を実行せずに回収可能 page/byte を推定します。論理 metrics は artifact-preserving query snapshot から取得し、物理 before/after field は対応する DB/WAL 世代にある指定元 source DB/WAL/SHM set の1つの安定 observation を共有します。実行系 `cdidx vacuum --json` は、論理 database byte(`page_count * page_size`)、maintenance 前後の指定元 source main/WAL/SHM file length、およびその物理 file-set 合計を返します。論理 before / after の各 PRAGMA 群は、それぞれ1つの SQLite read snapshot から取得します。実行後の field と maintenance guidance は、cancellation 対応の best-effort truncate checkpoint と command 所有 vacuum connection の close が完了した後に確定します。logical-after snapshot、command 所有 checkpoint、安定した source-state capture の間で `data_version` が変わらない場合に、close 前の witness を確立します。post-close の query-only observer は、その論理 snapshot を指定元 source の1つの安定 file set に結合します。raw source 世代が不変な場合、または witness の WAL が non-empty、observer の WAL が zero-length で、両者の logical size signature(`page_count`、`freelist_count`、`page_size`、`auto_vacuum`)が一致する checkpoint 表現の場合だけ採用します。後者は公開 size metrics の一貫性を保ちますが、application data が同一世代に留まったことまでは保証しません。外部 writer がその signature を変更した場合、または別の raw 世代がこの hot-WAL から zero-WAL への checkpoint 形状に当てはまらない場合は、すべての物理 after field と `db_size_bytes_after` / `wal_size_bytes_after` alias を unavailable(result DTO では null、通常の CLI JSON では省略)にし、WAL guidance を `unknown` にします。物理 observation は、file identity 一式の2回の取得結果が一致し、かつ同じ DB/WAL 世代を挟んでいる場合だけ採用します。取得は最大3回まで再試行し、継続的な不安定状態または metadata read failure では物理 tuple と互換 alias をすべて unavailable にし、WAL guidance を `unknown` にします。sidecar が存在しないことを安定して確認できた場合だけゼロを返します。`db_size_bytes_*` と `wal_size_bytes_*` はそれぞれ `main_file_bytes_*` と `wal_file_bytes_*` の互換 alias として維持し、`pages_reclaimed` / `bytes_reclaimed` は物理 file-size 差ではなく論理 page 回収量を表します。別の active connection が WAL/SHM を保持することは正当なため、main-file byte や physical-file-set byte は logical database byte と一致するとは限りません。`physical_file_set_bytes_*` は filesystem allocated-block usage ではなく論理 file length の合計であり、`wal_checkpoint_timing_note` はこの安定した post-close observation point を説明します。 | -| FTS optimize preview | `cdidx optimize --dry-run` とその alias である `cdidx index --optimize --dry-run` は `QueryOnly` snapshot を開き、既存 lockfile を作成も取得もせずに probe し、source DB/WAL/SHM set に対する write PRAGMA、schema setup、FTS control insert、metadata write を一切実行しません。どちらの entry point も query connection では明示的に指定された `file:` URI と `immutable=1` を保持し、filesystem probe だけが正規化済み local path を使うため、hot WAL がある場合も status と両方の preview は同じ stale-snapshot semantics を適用します。JSON は size/freelist/readiness 指標、planned operation、および正確な threshold と observed write を含む status と同じ `fts_optimization` recommendation object を返します。stale batch または legacy counter / page snapshot が利用できない場合は、安定した reason と state で recommendation を抑止します。実行系は optimize 前後で同じ object と reset 後の counter を返し、repair mode の schema 初期化または migration 確認を実行します。object size は利用可能なら `dbstat` page byte を使い、利用できない場合は明示した logical-payload fallback を使います。実際の optimize は所要 millisecond を記録し、後続 preview が `estimated_duration_ms` として返せるようにします。 | +| FTS optimize preview | `cdidx optimize --dry-run` とその alias である `cdidx index --optimize --dry-run` は `QueryOnly` snapshot を開き、既存 lockfile を作成も取得もせずに probe し、source DB/WAL/SHM set に対する write PRAGMA、schema setup、FTS control insert、metadata write を一切実行しません。どちらの entry point も query connection では明示的に指定された `file:` URI と `immutable=1` を保持し、filesystem probe だけが正規化済み local path を使うため、hot WAL がある場合も status と両方の preview は同じ stale-snapshot semantics を適用します。human / JSON の path 表示は共有 maintenance policy を使用し、既定では呼び出し元の相対表記を維持しつつ absolute input を `` にし、`--show-paths` 指定時だけ解決済み absolute database path を表示します。内部の snapshot / filesystem 操作は常に実 path を保持します。JSON は size/freelist/readiness 指標、planned operation、および正確な threshold と observed write を含む status と同じ `fts_optimization` recommendation object を返します。stale batch または legacy counter / page snapshot が利用できない場合は、安定した reason と state で recommendation を抑止します。実行系は optimize 前後で同じ object と reset 後の counter を返し、repair mode の schema 初期化または migration 確認を実行します。object size は利用可能なら `dbstat` page byte を使い、利用できない場合は明示した logical-payload fallback を使います。実際の optimize は所要 millisecond を記録し、後続 preview が `estimated_duration_ms` として返せるようにします。 | | size / process diagnostics | `status --json` は `db_size_bytes`、`wal_size_bytes`、上限付きの `symbol_kinds` / `symbols_by_language` kind map と、上限適用時の `symbol_kind_*` / `symbols_by_language_kind_*` overflow metadata、現在の `process` heap / GC / working-set metrics、成功した CLI / MCP index 実行由来の `last_index_run` metadata、最新の成功 index/update 時刻を示す `last_workspace_freshened_at` も公開します。`last_index_run.bytes_read_skipped_file_count` と `bytes_read_incomplete` は、読み取り不能な file が `bytes_read` 合計から除外されたかどうかを報告します。`last_index_run.diagnostics`、`diagnostic_count`、`diagnostics_truncated` は、index data 自体の書き込みが成功した後に best-effort index metadata write が失敗した場合の上限付き warning を保持します。`indexed_at` は引き続き indexed file row 由来なので、partial / no-op update は `indexed_at` を動かさずに workspace 鮮度だけを更新することがあります。 | | memory tracing | `index --json --memory-trace` は CLI index 結果に `memory_timeline` block を追加し、peak working-set MB を `last_index_run` に保存します。dry-run 結果も live な `start`、`snapshot`、`scan`、`finalize` sample を返しますが、run metadata は保存しません。`index --dry-run --rebuild` は index を削除も rewrite もしないため destructive confirmation を bypass します。`CDIDX_MEM_WARN_MB=` は sampled working set がしきい値を超えたときに warning を出します。 | | newer schema protection | writable open は、`PRAGMA user_version` に current binary の `CurrentSchemaVersion` mask 外の readiness bit が含まれる database も拒否します。read-only status/query path は degraded audit signal として `index_newer_than_reader=true` を表示できますが、write-capable path は古い cdidx が新しい binary で stamp された DB を黙って rewrite しないよう `E003_SCHEMA_TOO_NEW` で失敗しなければなりません。 | diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 5bf45e37e..dda002677 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -444,8 +444,8 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result owns the import-staging confidentiality regression. Keep the non-dry-run parent at mode `0755`, set process umask `022` only inside the non-parallel SQLite-sensitive collection, observe the empty staging DB before extraction can write its first byte, and verify owner-only DB/WAL/SHM modes through validation, trust rewriting, `--prune-paths`, rollback-backup creation, cancellation cleanup, collision rejection, and final replacement. POSIX mode assertions must remain platform-conditional while success and `CreateNew` collision behavior continue to run on Windows. - `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`, `Run_CancelDuringDryRunScan_ReturnsInterruptedJson`, and `Run_CancelBeforeFreshScan_ReturnsInterruptedJson` exercise the same in-process cancellation paths used after Ctrl-C/SIGINT wiring, including scan-time cancellation, so interrupted index runs keep returning the canonical JSON error contract. -- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`, `RunOptimizeFts_LockHeld_ReportsDbLocked`, and `RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable` - share seeded databases to verify that optimize dry-run reports sizes, readiness, lock state, recommendations, and planned work while preserving the source bytes and creating no lock artifacts; the same fixtures retain the writable optimize and lock/read-only URI mutation guards. +- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`, `RunOptimizeFts_DryRunPathDisclosureRequiresShowPaths_Issue5193`, `RunOptimizeFts_LockHeld_ReportsDbLocked`, and `RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable` + share seeded databases to verify that optimize dry-run reports sizes, readiness, lock state, recommendations, and planned work while preserving the source bytes and creating no lock artifacts; the same fixtures retain the writable optimize and lock/read-only URI mutation guards. The Issue #5193 matrix keeps the standalone command and index-command alias aligned in human/JSON output for relative and absolute inputs with and without `--show-paths`, rejects default resolved-prefix disclosure, and rechecks both database bytes and the optimize write counter. `ConsoleUiTests.PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags` keeps the optimize dry-run flag visible in full CLI usage. - Issue #4889 backfill-checkpoint coverage keeps the mutation preflight, folded-value validation, index lock, and artifact decision in one contract. Reuse the completed fixture for default no-op, forced `--checkpoint`, `--no-checkpoint` mutation, human output, a non-NULL drifted fold that requires a protected full repair, and a zero-row pending graph refresh; keep WAL/SHM open during the forced snapshot so Windows sharing semantics and the complete DB/WAL/SHM/manifest file set are exercised. Separate dry-run, invalid-DB, and held-lock assertions must prove that no checkpoint directory is created, while a real pending rewrite or pending graph refresh must retain the automatic checkpoint. - Issue #4946 fold-backfill coverage keeps CLI and MCP preflight aligned for a database that simultaneously contains NULL folded values and non-NULL drifted values. Both surfaces must promote that mixed state to one full rewrite and verify successfully in the first invocation. If that promoted rewrite is interrupted, its checkpoint must keep the retry in full-rewrite mode until completion and then be cleared; even an end-of-table checkpoint must be consumed and exactly verified before a later scoped NULL update can use targeted repair. Complete databases without a checkpoint must not pay for an additional present-value scan before their existing exact verification. @@ -1588,8 +1588,8 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" import staging の confidentiality 回帰を担当します。non-dry-run の parent を mode `0755` にし、process umask `022` は非並列の SQLite-sensitive collection 内だけで設定し、extraction が最初の byte を書く前の空 staging DB を観測してください。validation、trust rewrite、`--prune-paths`、rollback backup 作成、cancellation cleanup、衝突拒否、最終 replacement を通して DB/WAL/SHM が owner-only であることを検証します。POSIX mode assertion は platform 条件付きに保ちつつ、success と `CreateNew` の衝突挙動は Windows でも実行してください。 - `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`、`Run_CancelDuringDryRunScan_ReturnsInterruptedJson`、`Run_CancelBeforeFreshScan_ReturnsInterruptedJson` Ctrl-C/SIGINT 配線後に使われる in-process cancellation 経路を、scan 中のキャンセルも含めて検証し、interrupted index run が標準の JSON error contract を返し続けることを固定する。 -- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`、`RunOptimizeFts_LockHeld_ReportsDbLocked`、`RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable` - seed 済み database を共有し、optimize dry-run が size、readiness、lock state、推奨、planned work を報告しつつ source byte を保持し、lock artifact を作成しないことを検証する。同じ fixture で、書き込み版 optimize と lock/read-only URI の mutation guard も維持する。 +- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`、`RunOptimizeFts_DryRunPathDisclosureRequiresShowPaths_Issue5193`、`RunOptimizeFts_LockHeld_ReportsDbLocked`、`RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable` + seed 済み database を共有し、optimize dry-run が size、readiness、lock state、推奨、planned work を報告しつつ source byte を保持し、lock artifact を作成しないことを検証する。同じ fixture で、書き込み版 optimize と lock/read-only URI の mutation guard も維持する。Issue #5193 の matrix は、standalone command と index-command alias について relative / absolute input と `--show-paths` の有無における human / JSON 出力を同期させ、既定で解決済み prefix が漏れないことを確認し、database byte と optimize write counter の両方を再検証する。 `ConsoleUiTests.PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags` は、full CLI usage に optimize dry-run flag が表示され続けることを固定します。 - Issue #4889 の backfill-checkpoint coverage は、mutation preflight、folded値の検証、index lock、artifact 作成判断を1つの契約として固定します。完了済み fixture を既定no-op、明示 `--checkpoint`、`--no-checkpoint` mutation、human output、保護された全行修復を必要とする非NULLのfold drift、zero-rowのpending graph refreshで再利用し、明示snapshot中は WAL/SHM をopenしたままにしてWindowsの共有 semanticsとDB/WAL/SHM/manifest一式を検証してください。dry-run、無効DB、lock保持中のassertionではcheckpoint directoryが作られないことを個別に証明し、実際にpending rewriteまたはpending graph refreshがある場合はautomatic checkpointを維持してください。 - Issue #4946 の fold-backfill coverage は、NULL の folded 値と非 NULL だが drift した値が同時に存在する database で CLI と MCP の preflight を同期させます。両 surface はこの混在状態を1回の全行 rewrite に昇格し、最初の invocation で検証成功しなければなりません。昇格した rewrite が中断された場合は、完了まで checkpoint に従って全行 rewrite mode を維持してから checkpoint を消去します。table 末尾に到達した checkpoint も、後続の scoped NULL update が対象修復を使う前に消費して厳密検証してください。checkpoint のない complete database では、既存の厳密検証より前に追加の present-value scan を実行しないでください。 diff --git a/changelog.d/unreleased/5193.security.md b/changelog.d/unreleased/5193.security.md new file mode 100644 index 000000000..eebaef4c6 --- /dev/null +++ b/changelog.d/unreleased/5193.security.md @@ -0,0 +1,22 @@ +--- +category: security +issues: + - 5193 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs + - tests/CodeIndex.Tests/IndexCommandRunnerIssue5193Tests.cs + - tests/CodeIndex.Tests/CommandErrorWriterTests.cs + - DEVELOPER_GUIDE.md + - TESTING_GUIDE.md +--- + +## English + +- **Optimize previews now honor `--show-paths` (#5193)** — standalone and index-command-alias human/JSON dry-run output preserve safe relative database spelling or redact absolute paths by default, while `--show-paths` explicitly reveals the resolved absolute path without changing internal database operations. + +## 日本語 + +- **optimize preview が `--show-paths` を遵守するようになりました (#5193)** — standalone と index-command alias の human / JSON dry-run 出力は既定で安全な相対 database 表記を維持するか absolute path を伏せ、`--show-paths` 指定時だけ解決済み absolute path を明示的に表示し、内部 database 操作は変更しません。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs b/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs index 2f2676f9e..2ebdbd142 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs @@ -274,12 +274,15 @@ private static int RunOptimizeFtsPreviewForDb( var ftsOptimization = status.MaintenanceGuidance.FtsOptimization; var writesSinceOptimize = ftsOptimization.ObservedWrites; stopwatch.Stop(); + var displayDbPath = showPaths + ? dbPath + : MaintenanceDatabaseErrorClassifier.FormatPathForOutput(errorDbPath, showPaths: false); var result = new OptimizeFtsPreviewJsonResult { Status = "dry_run", DryRun = true, - DbPath = dbPath, + DbPath = displayDbPath, WritesSinceOptimizeBefore = checked((int)Math.Min(writesSinceOptimize, int.MaxValue)), WritesSinceOptimizeAfter = checked((int)Math.Min(writesSinceOptimize, int.MaxValue)), ElapsedMs = stopwatch.ElapsedMilliseconds, @@ -336,7 +339,7 @@ private static int RunOptimizeFtsPreviewForDb( else { CommandOutputWriter.WriteLine("FTS5 optimize preview (read-only; no changes made)."); - CommandOutputWriter.WriteLine(ConsoleUi.FormatSummaryLine("DB", dbPath, indent: " ")); + CommandOutputWriter.WriteLine(ConsoleUi.FormatSummaryLine("DB", displayDbPath, indent: " ")); CommandOutputWriter.WriteLine(ConsoleUi.FormatSummaryLine("DB size", ConsoleUi.FormatBytes(result.DbSizeBytes ?? 0), indent: " ")); CommandOutputWriter.WriteLine(ConsoleUi.FormatSummaryLine("Core size", ConsoleUi.FormatBytes(result.CoreTableSizeBytes ?? 0), indent: " ")); CommandOutputWriter.WriteLine(ConsoleUi.FormatSummaryLine("FTS size", ConsoleUi.FormatBytes(result.FtsSizeBytes ?? 0), indent: " ")); diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs index 6b4b66a2f..ee6f2e328 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs @@ -371,6 +371,7 @@ public static IndexCommandOptions ParseArgs(string[] args) // オプション解析の境界で絶対化し、以降の cwd 変化で相対パス計算が崩れないようにする。 ProjectPath = AbsolutizePathOption(projectPath), DbPath = AbsolutizeDbPathOption(dbPath), + DbPathInput = dbPath, DataDir = AbsolutizePathOption(dataDir), Rebuild = rebuild, Verbose = verbose, diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index d44ed6381..a1d8f59e1 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -205,6 +205,9 @@ internal static int Run( var requestedDbPath = dbPath; dbPath = DbPathResolver.NormalizeDbPath(dbPath); var resolvedDbPath = Path.GetFullPath(dbPath); + var diagnosticDbPath = string.IsNullOrWhiteSpace(options.DbPathInput) + ? resolvedDbPath + : options.DbPathInput; var databaseExistedBeforeIndex = File.Exists(LongPath.EnsureWindowsPrefix(resolvedDbPath)); if (!options.Json && !options.Quiet) @@ -215,9 +218,11 @@ internal static int Run( options.ShowPaths) : Path.GetFullPath(options.ProjectPath!); var databaseDisplayPath = options.OptimizeOnly - ? MaintenanceDatabaseErrorClassifier.FormatPathForOutput( - resolvedDbPath, - options.ShowPaths) + ? options.ShowPaths + ? resolvedDbPath + : MaintenanceDatabaseErrorClassifier.FormatPathForOutput( + diagnosticDbPath, + showPaths: false) : resolvedDbPath; ConsoleUi.PrintBanner(); CommandOutputWriter.WriteLine(); @@ -235,6 +240,7 @@ internal static int Run( options.ProjectPath, options.DryRun, showPaths: options.ShowPaths, + diagnosticDbPath: diagnosticDbPath, queryOnlyDbPath: options.DryRun ? requestedDbPath : null); bool ignoreCase; @@ -679,6 +685,9 @@ public sealed class IndexCommandOptions public bool ShowHelp { get; init; } public string? ProjectPath { get; init; } public string? DbPath { get; init; } + // Retain the exact --db token only for maintenance display; DbPath remains the stable I/O path. + // maintenance 表示専用に --db の元表記を保持し、I/O には引き続き安定した DbPath を使う。 + internal string? DbPathInput { get; init; } public string? DataDir { get; init; } public bool Rebuild { get; init; } public bool Verbose { get; init; } diff --git a/tests/CodeIndex.Tests/CommandErrorWriterTests.cs b/tests/CodeIndex.Tests/CommandErrorWriterTests.cs index 1a318fbdb..d34b87642 100644 --- a/tests/CodeIndex.Tests/CommandErrorWriterTests.cs +++ b/tests/CodeIndex.Tests/CommandErrorWriterTests.cs @@ -71,6 +71,7 @@ public void MaintenanceClassifier_PreservesStructuredDatabaseFailures_Issue4856( [InlineData("/Users/alice/private/codeindex.db")] [InlineData("/Users/alice/private/review-missing.db")] [InlineData(@"C:\Users\alice\private\codeindex.db")] + [InlineData(@"\\server\share\private\codeindex.db")] [InlineData("file:///Users/alice/private/codeindex.db?immutable=1")] public void MaintenanceClassifier_RedactsPlatformAbsolutePathsUnlessExplicitlyEnabled_Issue4856( string dbPath) diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerIssue5193Tests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerIssue5193Tests.cs new file mode 100644 index 000000000..c6442a510 --- /dev/null +++ b/tests/CodeIndex.Tests/IndexCommandRunnerIssue5193Tests.cs @@ -0,0 +1,187 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class IndexCommandRunnerTests +{ + [Fact] + public void RunOptimizeFts_DryRunPathDisclosureRequiresShowPaths_Issue5193() + { + var relativeDbPath = $"cdidx_optimize_preview_paths_{Guid.NewGuid():N}.db"; + var dbPath = Path.GetFullPath(relativeDbPath); + try + { + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + db.InitializeSchema(); + + SqliteConnection.ClearAllPools(); + var bytesBefore = File.ReadAllBytes(dbPath); + string? writesBefore; + using (var db = new DbContext(DbOpenIntent.QueryOnly, dbPath)) + writesBefore = db.GetMetaString(DbWriter.FtsIncrementalWritesSinceOptimizeMetaKey); + + var cases = new[] + { + new { InputPath = dbPath, ShowPaths = false, ExpectedPath = "" }, + new { InputPath = dbPath, ShowPaths = true, ExpectedPath = dbPath }, + new { InputPath = relativeDbPath, ShowPaths = false, ExpectedPath = relativeDbPath }, + new { InputPath = relativeDbPath, ShowPaths = true, ExpectedPath = dbPath }, + }; + + foreach (var testCase in cases) + { + foreach (var useIndexAlias in new[] { false, true }) + { + foreach (var json in new[] { false, true }) + { + var args = new List + { + "--db", + testCase.InputPath, + "--dry-run", + }; + if (json) + args.Add("--json"); + if (testCase.ShowPaths) + args.Add("--show-paths"); + + var (exitCode, output) = RunOptimizePreviewAndCapture( + args.ToArray(), + useIndexAlias); + + Assert.Equal(CommandExitCodes.Success, exitCode); + if (json) + { + using var document = JsonDocument.Parse(output); + Assert.Equal( + testCase.ExpectedPath, + document.RootElement.GetProperty("db_path").GetString()); + } + else + { + Assert.Contains(testCase.ExpectedPath, output, StringComparison.Ordinal); + } + + if (!testCase.ShowPaths) + Assert.DoesNotContain(dbPath, output, StringComparison.Ordinal); + } + } + } + + SqliteConnection.ClearAllPools(); + Assert.Equal(bytesBefore, File.ReadAllBytes(dbPath)); + using (var db = new DbContext(DbOpenIntent.QueryOnly, dbPath)) + { + Assert.Equal( + writesBefore, + db.GetMetaString(DbWriter.FtsIncrementalWritesSinceOptimizeMetaKey)); + } + } + finally + { + SqliteConnection.ClearAllPools(); + DeleteFile(dbPath + "-shm"); + DeleteFile(dbPath + "-wal"); + DeleteFile(dbPath); + } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void RunIndexOptimize_DryRunBlankDbInputReportsResolvedFallback_Issue5193( + string blankDbPath) + { + var projectPath = CreateTempProject(); + var dbPath = Path.Combine(projectPath, ".cdidx", "codeindex.db"); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + db.InitializeSchema(); + + SqliteConnection.ClearAllPools(); + var bytesBefore = File.ReadAllBytes(dbPath); + foreach (var showPaths in new[] { false, true }) + { + foreach (var json in new[] { false, true }) + { + var args = new List + { + "--db", + blankDbPath, + "--dry-run", + }; + if (json) + args.Add("--json"); + if (showPaths) + args.Add("--show-paths"); + + var (exitCode, output) = RunOptimizePreviewAndCapture( + args.ToArray(), + useIndexAlias: true, + projectPath: projectPath); + + Assert.Equal(CommandExitCodes.Success, exitCode); + var expectedPath = showPaths ? dbPath : ""; + if (json) + { + using var document = JsonDocument.Parse(output); + Assert.Equal( + expectedPath, + document.RootElement.GetProperty("db_path").GetString()); + } + else + { + Assert.Contains(expectedPath, output, StringComparison.Ordinal); + } + + if (!showPaths) + Assert.DoesNotContain(dbPath, output, StringComparison.Ordinal); + } + } + + SqliteConnection.ClearAllPools(); + Assert.Equal(bytesBefore, File.ReadAllBytes(dbPath)); + } + finally + { + SqliteConnection.ClearAllPools(); + DeleteDirectory(projectPath); + } + } + + private (int ExitCode, string Output) RunOptimizePreviewAndCapture( + string[] args, + bool useIndexAlias, + string projectPath = ".") + { + lock (TestConsoleLock.Gate) + { + var originalOut = Console.Out; + try + { + using var stdout = new StringWriter(); + Console.SetOut(stdout); + var exitCode = useIndexAlias + ? IndexCommandRunner.Run( + [projectPath, "--optimize", .. args], + _jsonOptions, + cancellationForTesting: null, + output: null) + : IndexCommandRunner.RunOptimizeFts( + args, + _jsonOptions, + forceLogicalObjectSizeFallbackForTesting: true); + return (exitCode, stdout.ToString()); + } + finally + { + Console.SetOut(originalOut); + } + } + } +} diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 7e6249592..12fd15f65 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -6852,7 +6852,7 @@ public void RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue Assert.Equal(CommandExitCodes.Success, previewExitCode); Assert.Equal("dry_run", previewJson.GetProperty("status").GetString()); Assert.True(previewJson.GetProperty("dry_run").GetBoolean()); - Assert.Equal(dbPath, previewJson.GetProperty("db_path").GetString()); + Assert.Equal("", previewJson.GetProperty("db_path").GetString()); Assert.Equal( DbWriter.DefaultFtsOptimizeIncrementalWriteThreshold, previewJson.GetProperty("writes_since_optimize_before").GetInt32()); @@ -6935,6 +6935,8 @@ public void RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue Assert.Contains("state=current", humanPreviewOutput, StringComparison.Ordinal); Assert.Contains("Planned operations", humanPreviewOutput, StringComparison.Ordinal); Assert.Contains("initialize_or_migrate_schema", humanPreviewOutput, StringComparison.Ordinal); + Assert.Contains("", humanPreviewOutput, StringComparison.Ordinal); + Assert.DoesNotContain(dbPath, humanPreviewOutput, StringComparison.Ordinal); Assert.Equal(dbBytesBeforePreview, File.ReadAllBytes(dbPath)); int exitCode; @@ -7206,6 +7208,8 @@ public void RunOptimizeFts_LockHeld_ReportsDbLocked() Assert.Equal(CommandExitCodes.Success, previewExitCode); Assert.Equal("locked", previewJson.GetProperty("lock_state").GetString()); + Assert.Equal("", previewJson.GetProperty("db_path").GetString()); + Assert.DoesNotContain(dbPath, previewJson.ToString(), StringComparison.Ordinal); Assert.True(previewJson.GetProperty("source_database_unchanged").GetBoolean()); int exitCode;