GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

3. .kb-mcpignore は索引の境界であってアクセスの境界ではない / ignore はマッチャとしてのみ使う

背景と課題

v0.21.0 より前、索引から何かを外す手段は exclude_dirs だけだった。これは ディレクトリ basename の完全一致リストなので、「drafts/*.md だけ外す」 「*.tmp.md を外す」「archive/2024/** を外す」が書けない。素直な解は KB に gitignore 構文のファイルを置くことで、Cursor も ripgrep も大半の開発者向け ツールもそうしている。

実装に入る前に答えを出す必要がある問いが 2 つあり、どちらも好みで決められない。

1 つ目: このファイルは何を境界にするのか。 kb-mcp には exclude_dirs に ついて既に意図的な答えがあり、テストで pin までされている — 「索引しない」 であって「読ませない」ではない。除外ディレクトリ配下のファイルは search に 出ないが、パスを知っている呼び出し元には get_document が返す (validate_get_document_path は除外引数を受け取らず、 document_in_excluded_dir_is_still_readable がそれを pin している)。新しい ignore ファイルはこの契約を踏襲することも破ることもできる。業界も割れており、 Cursor はまさにこの区別のために 2 枚のファイルを持っている — 索引用の .cursorindexingignore とアクセス用の .cursorignore

2 つ目: ignore crate をどこまで使うか。 この crate はマッチャ (Gitignore) だけでなくディレクトリ walker (WalkBuilder) も提供する。 kb-mcp は既に walkdir で歩いており、しかもその判断が 3 箇所に分かれていて、 過去に 2 度食い違っている。

決定要因

検討した選択肢

ファイルの効果範囲

  1. 索引のみ。既存の exclude_dirs 契約を踏襲する
  2. 索引 + アクセス。get_document / get_best_practice も拒否する
  3. 2 枚に分ける (Cursor 方式)

実装

  1. ignore::WalkBuilder に乗り換え、walkdir を置き換える
  2. ignore::gitignore::Gitignoreマッチャとしてのみ使い、walkdir は残す
  3. 既に入っている globset の上に gitignore セマンティクスを自作する

決定

索引のみ (選択肢 1)、ignore はマッチャとしてのみ (選択肢 2)。

効果範囲は「このファイルが実際に保証できること」から決まる。KB に書ける者は .kb-mcpignore を消すこともできる以上、木の中に置いたルールがその木を守る 境界にはなり得ない。ignored なパスに対して get_document を拒否すると、 「任意の書き手が消せるファイル」の上に立ちながら見た目はアクセス制御になる — BU-20 が訂正させられたのと同じ形である。2 つの除外機構で契約が 1 つに揃うのは 説明としても単純だ: 除外されたものは決して索引されない。そして索引されるか どうかは読めるかどうかの境界ではない。 読ませたくないものは kb_path の外 — README がずっとそう書いている。

選択肢 3 は「見返りの無い概念の増加」として却下した。ファイル・ドキュメント・ テストがすべて倍になるが、表現しようとしている区別の強い方の半分は、いま 提供しないと決めたばかりのものである。

実装側の決定は実測が出した結論だ。WalkBuilder の既定は、既存 KB の挙動を 目に見えない形で変える: hidden() が既定 true で、しかも Windows の 「hidden」は「dot 始まり または FILE_ATTRIBUTE_HIDDEN を持つ」なので、 ユーザがエクスプローラで隠したノートが黙って index から消える。add_ignore は walk root ではなくプロセスのカレントディレクトリ基準で解決する (サービスと して入れた daemon の cwd は任意)。require_git / parents / git_ignore / git_global はすべて既定 on なので、KB がたまたま git リポジトリかどうかで挙動が 変わる。そして filter_entry は述語を 1 個しか取れず、ignore 判定との評価順序が docs に書かれていない — kb-mcp は既にそこで hardcoded denylist・Office lock ファイル・symlink・hardlink を判定している。

マッチャだけを取れば既存の walk は無傷で、より重要なことに、3 面すべての 除外判定を 1 つの関数にできる。この失敗形態には既に 2 回の代償を払っている。

選択肢 3 (globset で自作) は、独立実装同士が「誰もテストしない edge case」で 実際に食い違っているという証拠と、内部の先行知見 — 「手書きのマッチャにレビューが edge case を当て続けるのは library に委譲する サイン」— の 2 点で却下した。

帰結