概要
複数のリポジトリにまたがる横断検索機能を実装する。チームが複数リポジトリを運用している場合に、全リポジトリの知識を一括検索できるようにする。
背景・動機
少人数チームでは複数のリポジトリ(フロントエンド、バックエンド、ドキュメント等)を運用することが多い。「認証」に関する情報を探すとき、全リポジトリを横断して検索できると、知識の発見効率が大幅に向上する。
提案する解決策
実装前提条件(リファクタリング)
本機能の実装にあたり、以下の既存コードのリファクタリングが必要となる。
M1: 検索関数の base_path パラメータ追加
現在、全検索関数が Path::new(".") をハードコードしており、任意のリポジトリパスを受け取れない。全てのPath::new(".")ハードコード箇所に base_path: &Path パラメータを追加し、カレントディレクトリ依存を排除する。
対象箇所:
src/cli/search.rs 内の各検索関数(index_dir, symbol_db_path, load_config の呼び出し箇所)
src/cli/context.rs 内の index_dir, symbol_db_path 呼び出し
src/cli/config.rs 内の load_config 呼び出し
src/main.rs 内の load_config 呼び出し
注意: 対象箇所数は固定値で管理せず、Path::new(".")の全ハードコード箇所を網羅的に置換する。新たなハードコード箇所が追加された場合も同様に対応すること。
注意(Stage 7 M3): try_hybrid_search 内の Path::new(".") は SearchContext 導入時に修正漏れリスクが高い。明示的にリファクタリング対象箇所として管理し、SearchContext 移行時に確実に置換すること。
既存の単一リポジトリ利用時は base_path にカレントディレクトリ(.)をデフォルト値として渡すことで後方互換性を維持する。
M2: SearchResult にリポジトリ識別子を付与
現在の SearchResult.path はリポジトリ相対パスのため、複数リポジトリの結果をマージする際にパス衝突が発生する。以下の変更を行う。
SearchResult 構造体に repository: Option<String> フィールドを追加(単一リポジトリ利用時は None)
- ワークスペース検索時は各結果に
repository としてエイリアスを設定
- Human出力では
[alias] path 形式でプレフィックス表示
- JSON出力では
"repository": "alias" フィールドを追加
注意(Stage 7 M4): SearchResult 構築箇所として enrich_semantic_to_search_results および doc_to_search_result もプロダクションコード側の修正対象に含める。テスト側の make_result だけでなく、これらの関数内での SearchResult 初期化箇所にも repository フィールドを追加すること。
ワークスペース設定
M3: TOMLスキーマ定義
# ~/commandindex-workspace.toml(ユーザーホーム or 任意の場所)
[workspace]
name = "my-team"
[[workspace.repositories]]
path = "~/projects/frontend" # 絶対パスまたはワークスペースファイルからの相対パス
alias = "frontend" # 省略時はディレクトリ名を使用
[[workspace.repositories]]
path = "~/projects/backend"
alias = "backend"
[[workspace.repositories]]
path = "~/projects/docs"
alias = "docs"
パス解決ルール:
- 絶対パス(
/home/user/projects/frontend や ~/projects/frontend): そのまま使用。~ はホームディレクトリに展開する。
- 相対パス(
../frontend): ワークスペース設定ファイルの親ディレクトリを基準に解決する。
- パス正規化: パス解決後に
canonicalize() で正規化し、シンボリックリンクや .. を含むパスを実体パスに変換する。これにより、異なる表記で同一ディレクトリを指すパスの重複登録を検出できる。
- チルダ展開(Stage 7 S6):
~ の展開には dirs::home_dir() を使用する。HOME 環境変数が未設定の環境(一部のCI環境やコンテナ等)では dirs::home_dir() が None を返すため、その場合はエラーメッセージ(例: Cannot expand '~': home directory not found)を表示して該当リポジトリをスキップする。
- パス重複チェック:
canonicalize() 後のパスが重複している場合、設定ファイルロード時にエラーとして報告する(エイリアス重複チェックと同様)。
エイリアスのデフォルト値:
alias を省略した場合、path の末尾ディレクトリ名を自動的にエイリアスとして使用する。
- 例:
path = "~/projects/frontend" → alias = "frontend"
エラーハンドリング:
- 指定パスが存在しない場合: エラーメッセージを表示し、該当リポジトリをスキップして他のリポジトリの検索は続行する。
- 指定パスに
.commandindex/ インデックスが存在しない場合: 警告を表示し、該当リポジトリをスキップする。
- エイリアスが重複する場合: 設定ファイルロード時にエラーとして報告する。
- 正規化後のパスが重複する場合: 設定ファイルロード時にエラーとして報告する。
- ワークスペースファイル自体が不正なTOMLの場合: エラーメッセージを表示して終了する。
セキュリティ対策
M2-security: シンボリックリンク経由のディレクトリトラバーサル防止(Stage 7 M2)
canonicalize() によるシンボリックリンク解決時に、意図しない機密ディレクトリ(例: /etc, /root 等)への参照が可能になるリスクがある。以下の対策を実施する。
canonicalize() 後のパスに対して、.commandindex/ ディレクトリの存在チェックを行い、CommandIndexプロジェクトとして有効なリポジトリのみを検索対象とする
.commandindex/ ディレクトリが存在しないパスは「未インデックス」として警告・スキップする(既存のエラーハンドリングと統合)
- これにより、シンボリックリンクが機密ディレクトリを指していても、
.commandindex/ が存在しない限りインデックス読み込みは発生しない
S6-security: チルダ展開時のHOME未設定環境への対応(Stage 7 S6)
~ を含むパスの展開において、HOME 環境変数が未設定の環境では予期しない動作やパニックが発生する可能性がある。
- チルダ展開には
dirs::home_dir() を使用し、None が返された場合はパニックせずエラーメッセージを返す
- エラーメッセージ例:
Cannot expand '~': home directory not found
- 該当リポジトリはスキップし、他のリポジトリの処理は続行する
S1: 設定の責務分離
ワークスペース設定ファイル(commandindex-workspace.toml)は 「どのリポジトリを横断検索対象とするか」のみを管理 する。各リポジトリ固有の設定(除外パターン、インデックス設定等)は、各リポジトリ内の commandindex.toml(#76 で導入)に委譲する。ワークスペース設定ファイルにリポジトリ固有の設定項目を持たせない。
CLIインターフェース
# ワークスペース内の全リポジトリを横断検索
commandindex search "認証" --workspace ~/commandindex-workspace.toml
# 特定リポジトリに絞り込み(検索前フィルタ:対象リポのみ検索実行)
commandindex search "認証" --workspace ~/commandindex-workspace.toml --repo backend
# ワークスペースのインデックス状況を確認
commandindex status --workspace ~/commandindex-workspace.toml
# ワークスペース内の全リポジトリをインデックス更新
commandindex update --workspace ~/commandindex-workspace.toml
Phase 1 非対応サブコマンド
以下のサブコマンドはPhase 1では --workspace オプションに非対応とする。--workspace を付けて実行した場合は明示的なエラーメッセージを返す。
| サブコマンド |
エラーメッセージ |
理由 |
embed |
embed does not support --workspace yet |
Embeddingストアのリポジトリ横断スコア正規化が未対応 |
context |
context does not support --workspace yet |
コンテキスト生成のマルチリポ対応は設計検討が必要 |
config show |
config show does not support --workspace yet |
ワークスペース設定と個別リポジトリ設定の統合表示は未対応(Stage 7 S4) |
config path |
config path does not support --workspace yet |
ワークスペース設定と個別リポジトリ設定パスの出力は未対応(Stage 7 S4) |
clean |
clean does not support --workspace yet |
マルチリポジトリ一括クリーンの安全性確認が未対応(Stage 7 S4) |
S5: --repo フィルタの動作仕様
--repo オプションは 検索前フィルタ として実装する。指定されたエイリアスに一致するリポジトリのみを検索対象とし、他のリポジトリのインデックスは開かない(検索後フィルタではない)。これにより、不要なリポジトリのインデックス読み込みを回避し、パフォーマンスを最適化する。
検索結果の出力
- 各結果にリポジトリ名(alias)をプレフィックスとして表示(例:
[backend] src/auth.rs)
- JSON出力には
repository フィールドを追加
- スコアはリポジトリ間で正規化(Phase 1ではRRFスタイルのランク順位ベースマージ。下記「スコアマージ方式」を参照)
スコアマージ方式
Phase 1では、リポジトリごとに独立してBM25検索を実行し、結果をRRF(Reciprocal Rank Fusion)スタイルでマージする。
方式:
- 各リポジトリで独立にBM25検索を実行し、リポジトリ内でのランク順位を取得する
- 各結果に対してRRFスコアを算出:
score = 1 / (k + rank)(kは定数、既存のRRF実装に合わせる)
- 全リポジトリの結果をRRFスコアで降順ソートしてインターリーブする
- リポ間マージ用のRRF関数(Stage 7 M1): 既存の
rrf_merge はキーが (path, heading) のため、異なるリポジトリで同一パスのドキュメントが存在する場合にキー衝突が発生する。リポジトリ間マージ専用の rrf_merge_cross_repo 関数を新設し、キーを (repository, path, heading) とすることで衝突を防止する。単一リポジトリ内のマージには既存の rrf_merge をそのまま使用する。
理由:
- BM25スコアはインデックスごとのIDF値に依存するため、異なるリポジトリ間でスコアを直接比較することは統計的に不正確
- ランク順位ベースのマージにより、リポジトリ間の公平な結果統合を実現する
S3: status --workspace 出力仕様
status --workspace は以下の情報をリポジトリごとに出力する。--format オプションは既存の StatusFormat 型を再利用する。
Human出力(デフォルト):
Workspace: my-team (3 repositories)
frontend ~/projects/frontend 1,234 files 2026-03-20 15:30
backend ~/projects/backend 567 files 2026-03-21 10:00
docs ~/projects/docs 89 files 2026-03-19 08:45
JSON出力(--format json):
{
"workspace": "my-team",
"repositories": [
{
"alias": "frontend",
"path": "/home/user/projects/frontend",
"indexed_files": 1234,
"last_updated": "2026-03-20T15:30:00Z",
"status": "ok"
},
{
"alias": "backend",
"path": "/home/user/projects/backend",
"indexed_files": 567,
"last_updated": "2026-03-21T10:00:00Z",
"status": "ok"
},
{
"alias": "docs",
"path": "/home/user/projects/docs",
"indexed_files": 0,
"last_updated": null,
"status": "not_indexed"
}
]
}
status フィールドの値: ok(正常)、not_indexed(インデックス未作成)、not_found(パスが存在しない)
実装方針
- 各リポジトリのインデックスは独立(既存の
.commandindex/ を利用)
- ワークスペース検索は各リポジトリの検索結果をマージ
- 並列検索(rayon等)でパフォーマンスを確保
リポジトリ数上限とリソース制御(Stage 7 S1)
10以上のリポジトリをワークスペースに登録した場合、tantivy の IndexReader が使用する mmap ファイルハンドルがOSのプロセスごとの上限に達するリスクがある。以下の対策を実施する。
- ワークスペースに登録可能なリポジトリ数の上限を 50 とする。超過した場合は設定ファイルロード時にエラーとして報告する
- Phase 1 では逐次検索(1リポジトリずつ open → search → close)で実装し、同時に開くインデックスを1つに制限する
- 将来的に並列検索を導入する場合は、セマフォ(
tokio::sync::Semaphore 等)で同時オープン数を制御する
SearchContext 構造体のフィールド定義
SearchContext 構造体には以下のフィールドを含める。
必須フィールド:
base_path: PathBuf — リポジトリのルートパス
候補フィールド(リファクタリング時に検討):
config: Config — commandindex.toml から読み込んだ設定
index_dir: PathBuf — tantivy インデックスディレクトリパス
symbol_db_path: PathBuf — tree-sitter シンボルDBパス
含めないもの:
OutputFormat / StatusFormat — 出力形式は検索コンテキストではなくCLI層の責務
limit — 表示件数制限もCLI層の責務
workspace 関連 — ワークスペースは SearchContext の外側で管理し、リポジトリごとに SearchContext を生成する
S2: 横断検索のスコープ
Phase 1 では BM25(tantivy全文検索)のみ を横断検索の対象とする。ハイブリッド検索(Embedding + BM25)の横断対応は将来のPhaseで対応する。理由: Embeddingストアはリポジトリごとに独立しており、横断時のスコア正規化が複雑になるため。
S4: rayon依存に関する注意事項
並列検索のために rayon クレートを追加する際、既存の tantivy が内部で rayon を使用しているため、バージョン競合が発生しないことを確認する。cargo tree -d でバージョン重複をチェックし、必要に応じて tantivy が使用する rayon バージョンに合わせる。
update --workspace の動作仕様
エラーハンドリング:
update --workspace 実行時、各リポジトリのインデックス更新でエラーが発生した場合のハンドリング方針:
- エラーが発生したリポジトリについては、エラーメッセージを表示してスキップする
- 他のリポジトリのインデックス更新は続行する(
status --workspace と同様のスキップ・続行方式)
- 全リポジトリの処理完了後、エラーが発生したリポジトリの一覧をサマリとして出力する
- 1つ以上のリポジトリでエラーが発生した場合、コマンド全体の終了コードは非ゼロとする
進捗表示(Stage 7 S5):
update --workspace の逐次実行時に、ユーザーが進捗を把握できるよう以下の形式で進捗メッセージを出力する:
[1/3] Updating frontend...
[2/3] Updating backend...
[3/3] Updating docs...
影響範囲分析
Stage 3 影響範囲レビュー指摘事項
Must Fix
| ID |
指摘内容 |
影響ファイル |
対応方針 |
| M1 |
SearchResultにrepositoryフィールド追加時、Option<String>型でデフォルトNoneにして後方互換維持 |
src/search/hybrid.rs, src/output/ 配下 |
repository: Option<String>をフィールドに追加。既存コードではNoneをセットし、ワークスペース検索時のみSome(alias)を設定。シリアライズ時は#[serde(skip_serializing_if = "Option::is_none")]で後方互換を維持 |
| M2 |
全てのPath::new(".")ハードコード箇所を除去、SearchContext構造体導入で引数爆発を防ぐ |
src/cli/search.rs, src/cli/context.rs, src/cli/config.rs, src/main.rs |
SearchContext構造体を新設しbase_path(必須), config, index_dir, symbol_db_path(候補)をまとめる。各関数は&SearchContextを受け取る形にリファクタリング。デフォルトコンストラクタでbase_path = Path::new(".")を設定し後方互換維持。OutputFormatやlimit等のCLI層の責務は含めない |
| M3 |
既存テスト(output_format.rs, hybrid.rsのmake_result)がコンパイルエラーになるため機械的修正が必要 |
tests/output_format.rs, tests/ 配下のテストファイル |
SearchResultへのフィールド追加に伴い、テスト内のmake_resultヘルパーにrepository: Noneを追加。構造体初期化箇所を網羅的に修正 |
| M4 |
--workspace/--repoオプションの既存conflicts_with_allとの整合性確認 |
src/main.rs(clap定義部分) |
既存のconflicts_with_all設定を確認し、--workspaceが他オプション(--format等)と競合しないことを検証。--repoは--workspace必須の依存関係をrequiresで定義 |
| M5 |
WorkspaceConfig構造体とload_workspace_config関数の新規実装 |
src/cli/workspace.rs(新規), src/cli/mod.rs |
WorkspaceConfig, RepositoryEntry構造体をserde対応で定義。load_workspace_configでTOMLパース・パス解決・canonicalize()によるパス正規化・エイリアス重複チェック・パス重複チェック・存在確認を実装 |
Should Fix
| ID |
指摘内容 |
影響ファイル |
対応方針 |
| S1 |
JSON出力のrepositoryフィールド追加でe2eテスト影響確認 |
tests/ 配下のe2eテスト |
repositoryフィールドがNoneの場合はJSON出力に含まれないことを確認するテストを追加。ワークスペース検索時の出力検証テストも新規作成 |
| S2 |
マルチリポ時のIndexReader openのI/Oコスト・メモリ懸念 |
src/cli/search.rs, src/search/hybrid.rs |
Phase 1では逐次open/closeで実装。リポジトリ数が多い場合の性能劣化はログ警告で通知。将来的にはIndexReaderプール化を検討 |
| S3 |
rayon依存追加時のtantivyバージョン確認 |
Cargo.toml |
cargo tree -dでバージョン重複を確認し、tantivy内部のrayonバージョンに合わせる。CI上でも重複チェックを実施 |
| S4 |
SearchContext構造体導入推奨 |
src/cli/search.rs, src/cli/context.rs |
M2と統合して対応。SearchContextにbase_path(必須), config, index_dir, symbol_db_pathを集約し、関数シグネチャを簡素化。OutputFormatやlimit等のCLI層の責務は含めない |
| S5 |
status/updateのワークスペース対応ラッパー関数 |
src/cli/status.rs, src/cli/update.rs(新規または既存) |
各リポジトリに対して既存のstatus/updateを呼び出すラッパー関数を実装。エラーは個別リポジトリ単位でハンドリングし、全体の処理は続行。status --workspaceの--formatは既存のStatusFormat型を再利用 |
| S6 |
新CLIオプションのテスト追加 |
tests/cli_args.rs |
--workspace, --repoオプションのパーステスト、--repo単独指定時のエラーテスト、--workspaceと他オプション組み合わせテストを追加 |
| S7 |
Phase 1ではembed/contextに--workspace非対応を明示 |
src/main.rs(clap定義部分), ドキュメント |
embed/contextサブコマンドには--workspaceオプションを追加しない。--workspaceを付けた場合は明示的なエラーメッセージ(例: embed does not support --workspace yet、context does not support --workspace yet)を返す |
Stage 5 通常レビュー指摘事項(反映済み)
Must Fix
| ID |
指摘内容 |
対応箇所 |
| M1 |
Path::new(".")のハードコード箇所数を固定値で管理しない |
M1セクション: 「13箇所」→「全てのハードコード箇所」に修正。注意書き追加 |
| M2 |
ワークスペース検索時のスコアマージ方式を定義 |
「スコアマージ方式」セクションを新設。RRFスタイルのランク順位ベースマージを明記 |
| M3 |
影響ファイル一覧にsrc/cli/context.rsを追加。Phase 1ではcontext --workspaceを非対応とする |
影響ファイル一覧に追加済み。Phase 1非対応サブコマンド一覧にcontextを追加 |
Should Fix
| ID |
指摘内容 |
対応箇所 |
| S1 |
パス解決後にcanonicalize()で正規化、パス重複チェック追加 |
M3 TOMLスキーマ定義のパス解決ルール・エラーハンドリングに追記。M5対応方針にも反映 |
| S2 |
SearchContextに含めるフィールドを明確にリストアップ |
「SearchContext構造体のフィールド定義」セクションを新設。必須/候補/含めないものを分類 |
| S3 |
Phase 1ではリポごとにBM25スコアをランク順位ベースでマージ(RRFスタイル) |
「スコアマージ方式」セクションに詳細手順を記載。スコア直接比較を行わない理由も明記 |
| S4 |
update --workspaceのエラーハンドリングを明記 |
「update --workspaceのエラーハンドリング」セクションを新設。statusと同様のスキップ・続行方式 |
| S5 |
status --workspaceの--formatがStatusFormat型を再利用 |
S3 status --workspace出力仕様に明記。S5対応方針にも反映 |
Stage 7 影響範囲レビュー指摘事項
Must Fix
| ID |
指摘内容 |
影響ファイル |
対応方針 |
| M1 |
rrf_mergeのキーが(path, heading)のためリポ間マージ時にパス衝突を引き起こす |
src/search/hybrid.rs |
リポ間マージ専用のrrf_merge_cross_repo関数を新設。キーを(repository, path, heading)とする。単一リポ内マージには既存rrf_mergeを使用 |
| M2 |
canonicalize()でシンボリックリンク解決時に機密ディレクトリへの参照が可能 |
src/cli/workspace.rs(新規) |
canonicalize()後のパスに対して.commandindex/ディレクトリの存在チェックを実施。存在しないパスは未インデックスとして警告・スキップ |
| M3 |
try_hybrid_search内のPath::new(".")がSearchContext導入時に修正漏れリスク |
src/cli/search.rs, src/search/hybrid.rs |
明示的にリファクタリング対象箇所として管理。SearchContext移行時に確実に置換 |
| M4 |
enrich_semantic_to_search_results、doc_to_search_result内のSearchResult構築箇所がテスト修正対象に含まれていない |
src/search/hybrid.rs, src/cli/search.rs |
プロダクションコード側のSearchResult構築箇所(enrich_semantic_to_search_results, doc_to_search_result)も修正対象に明記 |
Should Fix
| ID |
指摘内容 |
影響ファイル |
対応方針 |
| S1 |
10+リポでmmapファイルハンドル上限リスク |
src/cli/workspace.rs(新規), src/cli/search.rs |
リポ数上限を50に設定。Phase 1では逐次検索で同時オープンを1つに制限。将来的にセマフォ制御 |
| S2 |
既存e2eテストに--workspace未指定時のリグレッション検証追加 |
tests/ 配下のe2eテスト |
--workspace未指定時の既存動作が変化しないことを確認するリグレッションテストを追加 |
| S3 |
WorkspaceConfig用エラー型をConfigErrorと分離して新設 |
src/cli/workspace.rs(新規) |
WorkspaceConfigError列挙型を新設し、ワークスペース設定固有のエラー(パス重複、エイリアス重複、リポ数上限超過等)を構造化 |
| S4 |
config show/config pathをPhase 1非対応サブコマンドリストに追加 |
src/main.rs(clap定義部分), ドキュメント |
config show, config path, cleanをPhase 1非対応サブコマンドテーブルに追加 |
| S5 |
update --workspaceの逐次実行で進捗メッセージ出力 |
src/cli/update.rs |
[1/3] Updating frontend...形式の進捗メッセージを出力 |
| S6 |
チルダ展開時のHOME未設定環境への対応 |
src/cli/workspace.rs(新規) |
dirs::home_dir()を使用。Noneの場合はエラーメッセージを返して該当リポジトリをスキップ |
影響を受けるファイル一覧
| ファイル |
変更種別 |
影響する指摘 |
src/search/hybrid.rs |
変更(SearchResult構造体修正、rrf_merge_cross_repo新設) |
M1, M3, Stage7-M1, Stage7-M4 |
src/cli/search.rs |
変更(base_pathパラメータ追加、SearchContext導入、doc_to_search_result修正) |
M2, S2, S4, Stage7-M3, Stage7-M4 |
src/cli/context.rs |
変更(base_pathパラメータ追加、Phase 1では--workspace非対応) |
M2 |
src/cli/config.rs |
変更(base_pathパラメータ追加) |
M2 |
src/main.rs |
変更(clap定義、load_config呼び出し修正、Phase 1非対応サブコマンド追加) |
M2, M4, S7, Stage7-S4 |
src/output/ 配下 |
変更(repositoryフィールド出力対応) |
M1 |
src/cli/workspace.rs |
新規(WorkspaceConfig, WorkspaceConfigError, load_workspace_config、セキュリティチェック) |
M5, Stage7-M2, Stage7-S1, Stage7-S3, Stage7-S6 |
src/cli/mod.rs |
変更(workspaceモジュール追加) |
M5 |
src/cli/status.rs |
変更(ワークスペース対応ラッパー、StatusFormat再利用) |
S5 |
src/cli/update.rs |
変更(ワークスペース対応ラッパー、エラーハンドリング、進捗表示) |
S5, Stage7-S5 |
tests/output_format.rs |
変更(make_result修正) |
M3, S1 |
tests/cli_args.rs |
変更(新CLIオプションテスト追加) |
S6 |
tests/ 配下のe2eテスト |
変更(repositoryフィールド検証追加、リグレッションテスト追加) |
M3, S1, Stage7-S2 |
Cargo.toml |
変更(rayon依存追加、dirs依存追加) |
S3, Stage7-S6 |
受け入れ基準
基本機能
リファクタリング(M1)
検索結果の識別(M2)
スコアマージ(Stage 5 M2追加)
設定ファイルの堅牢性(M3)
セキュリティ(Stage 7 新設)
設計原則(S1)
検索スコープ(S2)
ステータス出力(S3)
依存管理(S4)
Phase 1 非対応(Stage 5 M3追加, Stage 7 S4追加)
SearchContext 構造体(Stage 5 S2追加)
update --workspace エラーハンドリング(Stage 5 S4追加)
影響範囲対応(Stage 3 レビュー反映)
影響範囲対応(Stage 7 レビュー反映)
品質
依存 Issue
概要
複数のリポジトリにまたがる横断検索機能を実装する。チームが複数リポジトリを運用している場合に、全リポジトリの知識を一括検索できるようにする。
背景・動機
少人数チームでは複数のリポジトリ(フロントエンド、バックエンド、ドキュメント等)を運用することが多い。「認証」に関する情報を探すとき、全リポジトリを横断して検索できると、知識の発見効率が大幅に向上する。
提案する解決策
実装前提条件(リファクタリング)
本機能の実装にあたり、以下の既存コードのリファクタリングが必要となる。
M1: 検索関数の
base_pathパラメータ追加現在、全検索関数が
Path::new(".")をハードコードしており、任意のリポジトリパスを受け取れない。全てのPath::new(".")ハードコード箇所にbase_path: &Pathパラメータを追加し、カレントディレクトリ依存を排除する。対象箇所:
src/cli/search.rs内の各検索関数(index_dir,symbol_db_path,load_configの呼び出し箇所)src/cli/context.rs内のindex_dir,symbol_db_path呼び出しsrc/cli/config.rs内のload_config呼び出しsrc/main.rs内のload_config呼び出し既存の単一リポジトリ利用時は
base_pathにカレントディレクトリ(.)をデフォルト値として渡すことで後方互換性を維持する。M2:
SearchResultにリポジトリ識別子を付与現在の
SearchResult.pathはリポジトリ相対パスのため、複数リポジトリの結果をマージする際にパス衝突が発生する。以下の変更を行う。SearchResult構造体にrepository: Option<String>フィールドを追加(単一リポジトリ利用時はNone)repositoryとしてエイリアスを設定[alias] path形式でプレフィックス表示"repository": "alias"フィールドを追加ワークスペース設定
M3: TOMLスキーマ定義
パス解決ルール:
/home/user/projects/frontendや~/projects/frontend): そのまま使用。~はホームディレクトリに展開する。../frontend): ワークスペース設定ファイルの親ディレクトリを基準に解決する。canonicalize()で正規化し、シンボリックリンクや..を含むパスを実体パスに変換する。これにより、異なる表記で同一ディレクトリを指すパスの重複登録を検出できる。~の展開にはdirs::home_dir()を使用する。HOME環境変数が未設定の環境(一部のCI環境やコンテナ等)ではdirs::home_dir()がNoneを返すため、その場合はエラーメッセージ(例:Cannot expand '~': home directory not found)を表示して該当リポジトリをスキップする。canonicalize()後のパスが重複している場合、設定ファイルロード時にエラーとして報告する(エイリアス重複チェックと同様)。エイリアスのデフォルト値:
aliasを省略した場合、pathの末尾ディレクトリ名を自動的にエイリアスとして使用する。path = "~/projects/frontend"→alias = "frontend"エラーハンドリング:
.commandindex/インデックスが存在しない場合: 警告を表示し、該当リポジトリをスキップする。セキュリティ対策
M2-security: シンボリックリンク経由のディレクトリトラバーサル防止(Stage 7 M2)
canonicalize()によるシンボリックリンク解決時に、意図しない機密ディレクトリ(例:/etc,/root等)への参照が可能になるリスクがある。以下の対策を実施する。canonicalize()後のパスに対して、.commandindex/ディレクトリの存在チェックを行い、CommandIndexプロジェクトとして有効なリポジトリのみを検索対象とする.commandindex/ディレクトリが存在しないパスは「未インデックス」として警告・スキップする(既存のエラーハンドリングと統合).commandindex/が存在しない限りインデックス読み込みは発生しないS6-security: チルダ展開時のHOME未設定環境への対応(Stage 7 S6)
~を含むパスの展開において、HOME環境変数が未設定の環境では予期しない動作やパニックが発生する可能性がある。dirs::home_dir()を使用し、Noneが返された場合はパニックせずエラーメッセージを返すCannot expand '~': home directory not foundS1: 設定の責務分離
ワークスペース設定ファイル(
commandindex-workspace.toml)は 「どのリポジトリを横断検索対象とするか」のみを管理 する。各リポジトリ固有の設定(除外パターン、インデックス設定等)は、各リポジトリ内のcommandindex.toml(#76 で導入)に委譲する。ワークスペース設定ファイルにリポジトリ固有の設定項目を持たせない。CLIインターフェース
Phase 1 非対応サブコマンド
以下のサブコマンドはPhase 1では
--workspaceオプションに非対応とする。--workspaceを付けて実行した場合は明示的なエラーメッセージを返す。embedembed does not support --workspace yetcontextcontext does not support --workspace yetconfig showconfig show does not support --workspace yetconfig pathconfig path does not support --workspace yetcleanclean does not support --workspace yetS5:
--repoフィルタの動作仕様--repoオプションは 検索前フィルタ として実装する。指定されたエイリアスに一致するリポジトリのみを検索対象とし、他のリポジトリのインデックスは開かない(検索後フィルタではない)。これにより、不要なリポジトリのインデックス読み込みを回避し、パフォーマンスを最適化する。検索結果の出力
[backend] src/auth.rs)repositoryフィールドを追加スコアマージ方式
Phase 1では、リポジトリごとに独立してBM25検索を実行し、結果をRRF(Reciprocal Rank Fusion)スタイルでマージする。
方式:
score = 1 / (k + rank)(kは定数、既存のRRF実装に合わせる)rrf_mergeはキーが(path, heading)のため、異なるリポジトリで同一パスのドキュメントが存在する場合にキー衝突が発生する。リポジトリ間マージ専用のrrf_merge_cross_repo関数を新設し、キーを(repository, path, heading)とすることで衝突を防止する。単一リポジトリ内のマージには既存のrrf_mergeをそのまま使用する。理由:
S3:
status --workspace出力仕様status --workspaceは以下の情報をリポジトリごとに出力する。--formatオプションは既存のStatusFormat型を再利用する。Human出力(デフォルト):
JSON出力(
--format json):{ "workspace": "my-team", "repositories": [ { "alias": "frontend", "path": "/home/user/projects/frontend", "indexed_files": 1234, "last_updated": "2026-03-20T15:30:00Z", "status": "ok" }, { "alias": "backend", "path": "/home/user/projects/backend", "indexed_files": 567, "last_updated": "2026-03-21T10:00:00Z", "status": "ok" }, { "alias": "docs", "path": "/home/user/projects/docs", "indexed_files": 0, "last_updated": null, "status": "not_indexed" } ] }statusフィールドの値:ok(正常)、not_indexed(インデックス未作成)、not_found(パスが存在しない)実装方針
.commandindex/を利用)リポジトリ数上限とリソース制御(Stage 7 S1)
10以上のリポジトリをワークスペースに登録した場合、tantivy の
IndexReaderが使用する mmap ファイルハンドルがOSのプロセスごとの上限に達するリスクがある。以下の対策を実施する。tokio::sync::Semaphore等)で同時オープン数を制御するSearchContext構造体のフィールド定義SearchContext構造体には以下のフィールドを含める。必須フィールド:
base_path: PathBuf— リポジトリのルートパス候補フィールド(リファクタリング時に検討):
config: Config—commandindex.tomlから読み込んだ設定index_dir: PathBuf— tantivy インデックスディレクトリパスsymbol_db_path: PathBuf— tree-sitter シンボルDBパス含めないもの:
OutputFormat/StatusFormat— 出力形式は検索コンテキストではなくCLI層の責務limit— 表示件数制限もCLI層の責務workspace関連 — ワークスペースはSearchContextの外側で管理し、リポジトリごとにSearchContextを生成するS2: 横断検索のスコープ
Phase 1 では BM25(tantivy全文検索)のみ を横断検索の対象とする。ハイブリッド検索(Embedding + BM25)の横断対応は将来のPhaseで対応する。理由: Embeddingストアはリポジトリごとに独立しており、横断時のスコア正規化が複雑になるため。
S4: rayon依存に関する注意事項
並列検索のために
rayonクレートを追加する際、既存のtantivyが内部でrayonを使用しているため、バージョン競合が発生しないことを確認する。cargo tree -dでバージョン重複をチェックし、必要に応じてtantivyが使用するrayonバージョンに合わせる。update --workspaceの動作仕様エラーハンドリング:
update --workspace実行時、各リポジトリのインデックス更新でエラーが発生した場合のハンドリング方針:status --workspaceと同様のスキップ・続行方式)進捗表示(Stage 7 S5):
update --workspaceの逐次実行時に、ユーザーが進捗を把握できるよう以下の形式で進捗メッセージを出力する:影響範囲分析
Stage 3 影響範囲レビュー指摘事項
Must Fix
SearchResultにrepositoryフィールド追加時、Option<String>型でデフォルトNoneにして後方互換維持src/search/hybrid.rs,src/output/配下repository: Option<String>をフィールドに追加。既存コードではNoneをセットし、ワークスペース検索時のみSome(alias)を設定。シリアライズ時は#[serde(skip_serializing_if = "Option::is_none")]で後方互換を維持Path::new(".")ハードコード箇所を除去、SearchContext構造体導入で引数爆発を防ぐsrc/cli/search.rs,src/cli/context.rs,src/cli/config.rs,src/main.rsSearchContext構造体を新設しbase_path(必須),config,index_dir,symbol_db_path(候補)をまとめる。各関数は&SearchContextを受け取る形にリファクタリング。デフォルトコンストラクタでbase_path = Path::new(".")を設定し後方互換維持。OutputFormatやlimit等のCLI層の責務は含めないoutput_format.rs,hybrid.rsのmake_result)がコンパイルエラーになるため機械的修正が必要tests/output_format.rs,tests/配下のテストファイルSearchResultへのフィールド追加に伴い、テスト内のmake_resultヘルパーにrepository: Noneを追加。構造体初期化箇所を網羅的に修正--workspace/--repoオプションの既存conflicts_with_allとの整合性確認src/main.rs(clap定義部分)conflicts_with_all設定を確認し、--workspaceが他オプション(--format等)と競合しないことを検証。--repoは--workspace必須の依存関係をrequiresで定義WorkspaceConfig構造体とload_workspace_config関数の新規実装src/cli/workspace.rs(新規),src/cli/mod.rsWorkspaceConfig,RepositoryEntry構造体をserde対応で定義。load_workspace_configでTOMLパース・パス解決・canonicalize()によるパス正規化・エイリアス重複チェック・パス重複チェック・存在確認を実装Should Fix
repositoryフィールド追加でe2eテスト影響確認tests/配下のe2eテストrepositoryフィールドがNoneの場合はJSON出力に含まれないことを確認するテストを追加。ワークスペース検索時の出力検証テストも新規作成IndexReaderopenのI/Oコスト・メモリ懸念src/cli/search.rs,src/search/hybrid.rsCargo.tomlcargo tree -dでバージョン重複を確認し、tantivy内部のrayonバージョンに合わせる。CI上でも重複チェックを実施SearchContext構造体導入推奨src/cli/search.rs,src/cli/context.rsSearchContextにbase_path(必須),config,index_dir,symbol_db_pathを集約し、関数シグネチャを簡素化。OutputFormatやlimit等のCLI層の責務は含めないstatus/updateのワークスペース対応ラッパー関数src/cli/status.rs,src/cli/update.rs(新規または既存)status/updateを呼び出すラッパー関数を実装。エラーは個別リポジトリ単位でハンドリングし、全体の処理は続行。status --workspaceの--formatは既存のStatusFormat型を再利用tests/cli_args.rs--workspace,--repoオプションのパーステスト、--repo単独指定時のエラーテスト、--workspaceと他オプション組み合わせテストを追加embed/contextに--workspace非対応を明示src/main.rs(clap定義部分), ドキュメントembed/contextサブコマンドには--workspaceオプションを追加しない。--workspaceを付けた場合は明示的なエラーメッセージ(例:embed does not support --workspace yet、context does not support --workspace yet)を返すStage 5 通常レビュー指摘事項(反映済み)
Must Fix
Path::new(".")のハードコード箇所数を固定値で管理しないsrc/cli/context.rsを追加。Phase 1ではcontext --workspaceを非対応とするcontextを追加Should Fix
canonicalize()で正規化、パス重複チェック追加SearchContextに含めるフィールドを明確にリストアップSearchContext構造体のフィールド定義」セクションを新設。必須/候補/含めないものを分類update --workspaceのエラーハンドリングを明記update --workspaceのエラーハンドリング」セクションを新設。statusと同様のスキップ・続行方式status --workspaceの--formatがStatusFormat型を再利用status --workspace出力仕様に明記。S5対応方針にも反映Stage 7 影響範囲レビュー指摘事項
Must Fix
rrf_mergeのキーが(path, heading)のためリポ間マージ時にパス衝突を引き起こすsrc/search/hybrid.rsrrf_merge_cross_repo関数を新設。キーを(repository, path, heading)とする。単一リポ内マージには既存rrf_mergeを使用canonicalize()でシンボリックリンク解決時に機密ディレクトリへの参照が可能src/cli/workspace.rs(新規)canonicalize()後のパスに対して.commandindex/ディレクトリの存在チェックを実施。存在しないパスは未インデックスとして警告・スキップtry_hybrid_search内のPath::new(".")がSearchContext導入時に修正漏れリスクsrc/cli/search.rs,src/search/hybrid.rsSearchContext移行時に確実に置換enrich_semantic_to_search_results、doc_to_search_result内のSearchResult構築箇所がテスト修正対象に含まれていないsrc/search/hybrid.rs,src/cli/search.rsSearchResult構築箇所(enrich_semantic_to_search_results,doc_to_search_result)も修正対象に明記Should Fix
src/cli/workspace.rs(新規),src/cli/search.rs--workspace未指定時のリグレッション検証追加tests/配下のe2eテスト--workspace未指定時の既存動作が変化しないことを確認するリグレッションテストを追加WorkspaceConfig用エラー型をConfigErrorと分離して新設src/cli/workspace.rs(新規)WorkspaceConfigError列挙型を新設し、ワークスペース設定固有のエラー(パス重複、エイリアス重複、リポ数上限超過等)を構造化config show/config pathをPhase 1非対応サブコマンドリストに追加src/main.rs(clap定義部分), ドキュメントconfig show,config path,cleanをPhase 1非対応サブコマンドテーブルに追加update --workspaceの逐次実行で進捗メッセージ出力src/cli/update.rs[1/3] Updating frontend...形式の進捗メッセージを出力src/cli/workspace.rs(新規)dirs::home_dir()を使用。Noneの場合はエラーメッセージを返して該当リポジトリをスキップ影響を受けるファイル一覧
src/search/hybrid.rsSearchResult構造体修正、rrf_merge_cross_repo新設)src/cli/search.rsbase_pathパラメータ追加、SearchContext導入、doc_to_search_result修正)src/cli/context.rsbase_pathパラメータ追加、Phase 1では--workspace非対応)src/cli/config.rsbase_pathパラメータ追加)src/main.rsload_config呼び出し修正、Phase 1非対応サブコマンド追加)src/output/配下repositoryフィールド出力対応)src/cli/workspace.rsWorkspaceConfig,WorkspaceConfigError,load_workspace_config、セキュリティチェック)src/cli/mod.rsworkspaceモジュール追加)src/cli/status.rsStatusFormat再利用)src/cli/update.rstests/output_format.rsmake_result修正)tests/cli_args.rstests/配下のe2eテストrepositoryフィールド検証追加、リグレッションテスト追加)Cargo.toml受け入れ基準
基本機能
--workspaceで全リポジトリを横断検索できる--repoで特定リポジトリに絞り込める(検索前フィルタとして動作)status --workspaceでワークスペース全体の状態を確認できるupdate --workspaceでワークスペース全体を更新できるリファクタリング(M1)
base_pathパラメータを受け取り、任意のリポジトリパスで動作するPath::new(".")ハードコード箇所がSearchContext経由に置換されているtry_hybrid_search内のPath::new(".")も確実にSearchContext経由に置換されている(Stage 7 M3)検索結果の識別(M2)
SearchResultにリポジトリ識別子(repositoryフィールド)が含まれるenrich_semantic_to_search_resultsおよびdoc_to_search_result内のSearchResult構築箇所にもrepositoryフィールドが追加されている(Stage 7 M4)スコアマージ(Stage 5 M2追加)
rrf_merge_cross_repo関数がキー(repository, path, heading)で実装されている(Stage 7 M1)設定ファイルの堅牢性(M3)
~展開が正しく動作するcanonicalize()で正規化されるセキュリティ(Stage 7 新設)
canonicalize()後のパスに対して.commandindex/ディレクトリの存在チェックが実施されている(Stage 7 M2).commandindex/が存在しない限りインデックス読み込みは発生しない(Stage 7 M2)dirs::home_dir()を使用し、Noneの場合はパニックせずエラーメッセージを返す(Stage 7 S6)設計原則(S1)
検索スコープ(S2)
ステータス出力(S3)
status --workspaceがリポ名・パス・ファイル数・最終更新日・ステータスを出力する--format jsonで構造化JSON出力が可能--formatオプションは既存のStatusFormat型を再利用する依存管理(S4)
rayon追加時にtantivyとのバージョン競合がないことを確認済みPhase 1 非対応(Stage 5 M3追加, Stage 7 S4追加)
embed --workspaceが明示的エラーを返すcontext --workspaceが明示的エラーを返すconfig show --workspaceが明示的エラーを返す(Stage 7 S4)config path --workspaceが明示的エラーを返す(Stage 7 S4)clean --workspaceが明示的エラーを返す(Stage 7 S4)SearchContext構造体(Stage 5 S2追加)base_pathが必須フィールドとして含まれているOutputFormatやlimit等のCLI層の責務が含まれていないupdate --workspaceエラーハンドリング(Stage 5 S4追加)[1/3] Updating frontend...形式)が出力される(Stage 7 S5)影響範囲対応(Stage 3 レビュー反映)
SearchResult.repositoryがOption<String>型で後方互換を維持している(M1)Path::new(".")ハードコード箇所がSearchContext経由に置換されている(M2)output_format.rs,hybrid.rs)がrepositoryフィールド追加後もコンパイル・パスする(M3)--workspace/--repoオプションが既存のconflicts_with_allと競合しない(M4)WorkspaceConfig構造体とload_workspace_config関数が実装されている(M5)SearchContext構造体で引数爆発が防止されている(S4)status/updateのワークスペース対応ラッパーが実装されている(S5)--workspace,--repoのCLIパーステストが追加されている(S6)embed --workspaceが明示的エラーを返す(S7)context --workspaceが明示的エラーを返す(S7)影響範囲対応(Stage 7 レビュー反映)
rrf_merge_cross_repo関数がキー(repository, path, heading)で新設されている(M1)canonicalize()後のセキュリティチェック(.commandindex/存在確認)が実装されている(M2)try_hybrid_search内のPath::new(".")が明示的にリファクタリング対象として管理されている(M3)enrich_semantic_to_search_results、doc_to_search_resultのSearchResult構築箇所が修正対象に含まれている(M4)--workspace未指定時のリグレッションテストが追加されている(S2)WorkspaceConfigError列挙型がConfigErrorと分離して新設されている(S3)config show/config path/cleanがPhase 1非対応サブコマンドに含まれている(S4)update --workspaceで進捗メッセージが出力される(S5)dirs::home_dir()を使用し、HOME未設定時にエラーメッセージを返す(S6)品質
依存 Issue