GrooveSeek

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

View the Project on GitHub alphabet-h/grooveseek

安定性ポリシー

GrooveSeek が壊さないと約束するもの、および意図的に約束しないもの。 本ポリシーは 1.0.0 から効力を持つ。それ以前のリリースは beta であり、 互換性の保証はない。

English version: stability.md

GrooveSeek は Semantic Versioning に従う。 本書における意味:

この文書が存在する理由

これが無いと、「1.0.0」は観測できるものすべてが 2.0.0 まで固定される、という 約束になる。それは本プロジェクトに守れる約束ではない。観測できる面は、public な Rust API 全体、すべてのコマンドラインフラグ、6 個の MCP toolgroove.toml の 全セクション、そして SQLite スキーマに及ぶ。全部を凍らせれば、誰も依存していない 部分を永久に改善できないか、 誰も気付かない変更のたびに major を出すか、どちらかになる。

だから約束は意図的に狭めてある。そして狭めた事実を、推測に委ねず書き下す。

どこで動く想定か

GrooveSeek は認証を持たない。そして持たせる予定も無い。 これは欠落ではなく 設計上の立場なので、ロードマップではなく本書に書く。

HTTP transport は同一ホストから使われる想定である。ネットワーク境界は別の何かが 担う — コンテナのネットワーク分離、reverse proxy、あるいはナレッジベースに顔をつける アプリケーション。それが何であれ、外からの接続を終端し、認証を済ませ、 GrooveSeek とは loopback で話す。

非 loopback bind は引き続き許される。コンテナはそうしないと publish した ポートが届かないからである。ただしそれは、その境界を自分で引き受けたという意味に なる。/mcp に掛かっているのは Host 検証と session 数の上限だけで、 どちらも認証ではない — ポートに到達できるものは、ナレッジベース全文を読める。 起動時の警告はそのままの意味である。

/ui/api/admin/status は loopback でない peer を必ず拒否する。 これは設定で変えられないので、proxy は同一ホストに置く必要がある。

Origin 検証

MCP 仕様は、Streamable HTTP サーバが DNS rebinding 対策として Origin ヘッダを 検証することを要求している。GrooveSeek はこれを行い、既定値は bind した port の loopback origin である。

Origin を持たない要求 — 通常の MCP クライアント / tray / curl — は素通りする。 これは RFC 6454 と仕様のとおりで、この検査が止めるのは「利用者自身のブラウザに 開かれた web ページがこのポートへ到達すること」だけであり、2 つ目のアクセス制御では ない。proxy 越しに公開するなら、[transport.http].allowed_origins公開 origin を 明示する必要がある。ブラウザ上のクライアントが送るのは loopback origin ではなく そちらだからである。なおこのキーは既定リストを拡張ではなく置換するので、 ブラウザ上のクライアントが loopback 経由でも来るなら loopback の分も併記すること。

掛かるのは /mcp である。そして /ui/mcp 経由で検索する。 したがってこのリストは組み込みページが検索できるかどうかを決める — 既定を 公開 origin だけで置き換えると、/ui は表示されるのに検索できない状態になる。

検査する route については、1 つの検査が答える/mcp を MCP ライブラリに 任せず GrooveSeek 自身が行うので、2 つの面が同じ値を 2 通りに読むことはもう 起きない。以前は 5 つの Host 綴りで実際に起きていた — その記録は ADR-0009

ただし「何と照合するか」は route ごとに違い、このキーは全部には届かない:

Route Host Origin
/mcp allowed_hosts allowed_origins
/ui / /api/admin/status loopback + bind アドレス (設定不可) allowed_origins
/healthz allowed_hosts。ただし healthz_public = false の時だけ 検証しない

Origin を持たないリクエストは、検査が走る面ではどこでも通る。だから tray と curl は影響を受けない。

allowed_hosts でも同じことが 1 段手前で起きる。 Host 検証は Origin 検証より 先に走るので、公開ホスト名だけを並べたリストは、ローカルで開いた /ui が送る Host: localhost を拒否する。どちらのキーも既定を拡張ではなく置換するので、 実際にブラウザで使う名前と origin をそのまま列挙することallowed_hosts = ["127.0.0.1"] でも、localhost で開いたページは拒否される。

サーバが起動時に警告するのは「そのリストに loopback のエントリが 1 つも無い」 場合だけである。「1 つはあるが使っているアドレスと違う」場合は警告しないし、 ブラウザからは 403 しか見えない — そのため /ui は検索が拒否された時に、 必要な host と origin を画面上に表示する

安定 (Stable)

以下を壊すには major が要る。

コマンドライン

機械可読な出力

--format を取るサブコマンドはすべてどちらかの組に入れてある。 沈黙が約束と読まれないようにするため。

安定 — 人間以外に読ませるために存在するもの:

安定ではない非安定を参照:

search が返すもの

上の節は「文書化されたフィールドは名前・型・意味を保つ」と約束している。 その集合がこれである — search MCP tool と groove search --format json の 両方について書き下す (wrapper は同一、1 フィールドだけ違う。後述)。 この表に無いフィールドは約束の対象ではない。

topic が 2 箇所に出て意味が違うので、名前はパスで書く。

フィールド 存在
results array 常に
results[].score number 常に
results[].path string 常に。KB からの相対パス
results[].title string または null 常に。title が無い文書では null
results[].heading string または null 常に。見出しの無い chunk では null
results[].topic string または null 常に
results[].date string または null 常に
results[].tags string の array 常に。無ければ []
results[].content string 常に
results[].match_spans array 計算されなかった場合はキーごと不在。後述
results[].match_spans[].start integer content へのバイトオフセット。開始 (含む)
results[].match_spans[].end integer content へのバイトオフセット。終了 (含まない)
results[].expanded_from object parent retriever が扱った場合のみ。他は不在あること = 拡張された証拠ではない — 後述
results[].expanded_from.kind string "adjacent" または "whole_document"どちらかで下の 3 行の有無が決まる
results[].expanded_from.from_index integer adjacent のみ。merge した最初の chunk index (含む)
results[].expanded_from.to_index integer adjacent のみ。merge した最後の chunk index (含む)
results[].expanded_from.total_chunks integer whole_document のみ。その文書の chunk 総数
results[].start_line integer chunk がソースファイル由来のときのみ。他は不在。1 始まりで、由来した定義ではなく chunk 自身の範囲を指す
results[].end_line integer 同上。1 始まりで終端を含む
results[].symbol_kind string chunk が定義のときのみ。他は不在。grammar 自身の語 (function / class / method / constant …) で、対応言語が増えると値も増える
results[].uri string サーバが渡す文書のときのみ。CLI では常に不在
low_confidence boolean 常に。助言 — 後述
filter_applied object 常に。{} の意味は見た目より狭い — 後述
filter_applied.category string 指定時のみ
filter_applied.topic string 指定時のみ
filter_applied.path_globs string の array 指定時のみ
filter_applied.tags_any string の array 指定時のみ
filter_applied.tags_all string の array 指定時のみ
filter_applied.date_from string 指定時のみ
filter_applied.date_to string 指定時のみ
filter_applied.min_confidence_ratio number 指定時のみ
filter_applied.excluded_terms string の array query に除外があったときのみ
error string 上の全体の代わりにこれだけが返る。MCP tool が拒否・失敗したとき。後述

search の応答は 2 つの形のうちどちらか。 最終行より上が成功時。MCP tool が 呼び出しを拒否する / 検索が失敗する場合 — mmr_lambda が範囲外、query が 1 KiB を 超える、glob が壊れている、除外だけの query、embedding や DB の失敗 — は {"error": "…"} だけが 返り、results キーは存在しない。呼び出し側は results を無条件に読まず、 どちらが来たかで分岐すること。CLI にこの封筒は無い — 失敗を stderr に出して 非ゼロ終了する (責務分離の節のとおり)。

filter_applied が echo するのは、上表の行が「効果を持って届いたとき」だけ。 そこから 4 つのことが follow する。どれも空オブジェクトの見た目とは違う:

{} は「上表の行のうち、報告すべき効果を持って届いたものが 1 つも無かった」と 読む。「filter が指定されなかった」でも「filter が走らなかった」でもない。 後から quality 入力を echo に足すのは「フィールドの追加」規則により minor である。

「不在」は null ではなくキーごと無いこと。 上表で「のみ」と書いた行は、 条件を満たさなければオブジェクトからキーが消える。消費側は キーの不在と明示的な null を区別してはならない — どちらも「与えられていない」 と読む。

expanded_from が言うのは「parent retriever が走った」であって 「content が増えた」ではない。 adjacent の 2 つの index が等しい場合が degrade したケース — 隣接 chunk を足すと max_expanded_tokens を超えるので、 hit を自分の chunk のまま残し、そうなった事実をこのフィールドが記録する。 chunk が 1 つしかない文書も同じ理由で同じ形になる。 from_index == to_index は「扱ったが拡張していない」と読む — どちらにせよ content は元の chunk である。

match_spans は 3 状態を持ち、それぞれ意味が違う: 不在 = 計算していない (query の分割語に非 ASCII 文字が含まれる / query が空または空白のみ / chunk の content が 256 KiB を超える)、[] = 計算したが一致なし、 非空 = docs/citations.ja.md の契約 (同じ 3 ケースを列挙している)。

results[].uri が「成功時の wrapper」における唯一の差。MCP tool は渡せる 文書に付け、groove search は決して出さない。後から CLI 側に足すのは、上の 「フィールドの追加」規則により minor である。

2 つの面が本当に分かれるのは失敗時。MCP tool は上の error 封筒で答え、 tool call に exit status は無い。CLI は理由を stderr に書いて非ゼロ終了し、 JSON は一切出さない。つまり wrapper は uri を除いて一致するが、 失敗時の契約は対応していない — 片方の面に対して書いたコードは、 もう片方が同じ形で異常を報告するとは仮定できない。

low_confidence は「フィールドとして」凍結する。「判定として」ではない。 約束するのはキーが存在し boolean であることだけ。その裏の式・既定の閾値・ どのクエリで立つかは明示的に凍結せず、どのリリースでも変わり得る。 これはヒューリスティックであり、実測が示すのは「答が正しいか」ではなく 「融合後スコア分布の形」に反応しているということなので、 注意の合図として読み、判定として読まないこと。何を検出し何を検出しないかは docs/filters.ja.md に記録してある。

MCP の面

同じものを二つの面がどう名付けるか

コマンドラインと MCP ツールは別々に凍る 2 つの名前空間である。パラメータが 変わったからフラグが変わる、あるいはその逆、ということはない。

両方が同じ概念を出しているところでは同じ名詞を使い、それ以外は各面の流儀に従う。 コマンドラインは kebab-case で、繰り返し可能なフラグは単数形。ツールのパラメータは snake_case で、配列は複数形。よって --path-globpath_globs は同じフィルタであり、 --tag-any / tags_any--tag-all / tags_all も同様。対応は一致してはいないが 予測可能で、フラグごとの対応は usage.ja.md に書いてある。

意図的に対応させていないものが 2 つある:

は名前より厳しい規則に従う。名前の違いは調べ直せば済むが、値の違いは呼び出しが 失敗するため。二面で綴りが分かれる enum 値は、どちらの綴りも両側で受け付ける—— seed_strategyall_chunksall-chunks もどちらの面でも通る。

HTTP

設定

既定の embedding モデル

既定のモデルを変えることは major である。モデル識別子は索引に記録され、 起動時に完全一致が要求されるので、既定を変えると既存の全インストールが 再索引するまで起動しなくなる

ユーザのマシンに残る名前

1.0.0 のではなくに改名した理由がこれである:

   
索引 DB .groove.dbkb_path親ディレクトリ内
設定 groove.toml
除外ファイル .grooveignore
評価セット / 履歴 .groove-eval.yml / .groove-eval-history.json
サービス成果物 サービス名から導かれる task / unit / launch agent の名前、および config home の構成
環境変数 GROOVE_CONFIG_HOME / GROOVE_TRAY_LOG / GROOVE_GRAMMAR_DIR (v1.3.0+、絶対パス必須) (FASTEMBED_CACHE_DIR は fastembed の名前なので尊重はするが凍結対象にはしない。GROOVE_BIN は同梱サンプル hook の変数で、バイナリは読まない)
grammar plugin の配置 1 言語 1 ライブラリ。名前は groove_grammar_<言語> にプラットフォーム固有の接頭辞・拡張子を付けたもので、grammar ディレクトリに置く。export する C symbol と宣言する ABI 版も凍結対象 — ある 1.x 向けにビルドした plugin は次の 1.x でもロードできる

非安定 (Unstable)

以下は目に見えるが、patch を含むどのリリースでも変わりうる。本書がその告知である。

管理用の web 面

/ui/api/admin/status は安定ではない。 これらはサーバを動かしている 本人が自分のサーバを見るためのもので、仕様として loopback 限定であり (GrooveSeek は認証を持たない)、画面は変わり続ける前提である。 HTML も JSON も内部実装として扱うこと。

kb.path は既に削除した。 status 帯にも出さない。これはナレッジベースの 絶対パスを持っており、Windows では C:\Users\<名前>\... — つまり利用者の アカウント名が、JSON の本体と、あのページのスクリーンショット全部に載っていた。 必要としている読み手はいなかった (tray が読むのは daemon.pidindexing.active だけ)。見ている本人にとってどの KB かを示すのは kb.documents / kb.chunks / kb.model で、こちらは残っている。 本段落がその告知であり、非安定な面だからこそ 1.0 より前に消す

両 route は Content-Security-PolicyX-Content-Type-Options: nosniff を 付けて配信する。ポリシーは default-src 'none'このページが実際に使うものだけ を戻した形なので、/ui に外部 script / stylesheet を足すと、ブラウザの console ではなくそこで落ちる。

/api/search は既に削除した — 1.x の途中ではなく 1.0.0 より前に。 search tool が取る 17 パラメータのうち 2 つしか受け取っておらず、プロセスの外から 使う口としては /mcp の方が既に優れていたためである。そして /ui 自身が /mcp を 使うようになったので、あのページは「MCP クライアントの最小の実例」になっている。

/ui も無くす予定である。ナレッジベースを閲覧するのは /mcp を話す クライアントの仕事で、そちらなら全 tool と全パラメータに手が届き、面としても既に 安定している。したがって組み込みページは 1.x のうちに、そうしたクライアントが 揃った時点で退役させる意図がある。/api/admin/status はその対象ではない — 返しているのは version / pid / 索引進捗といった運用状態であり、言語モデル向けに 設計された tool 面に置くべきものではない。

これは予告であって予定表ではない。これらは非安定な面なので、 上に書いたことはどちらの向きにも約束ではない。

人間向けの出力と、2 つの計測ツール

どのサブコマンドであれ text 出力は、いつでも書き換え・並べ替え・追記されうる (search --format text / graph --format text を含む)。parse しないこと。 graph --format dot / --format svg も同じ — 図であり、図の描き方は見せ方である。

eval --format jsontune --format json も、JSON だが安定ではない。 検索の計測値を報告するもので、計測の定義そのものが良くなっていく前提だから。 eval は履歴ファイルに metric_version を記録していて、定義の違う実行同士を 比較しないようにしている。JSON を凍らせると指標が凍る。

機械可読な形が必要で存在しないなら、それは機能の欠落なので、 画面スクレイパを書く前に issue を立ててほしい。

索引データベース

.groove.db は SQLite ファイルだが、内部スキーマは契約ではない。テーブルは 追加・削除・再構成されうるし、アップグレードが索引を作り直すこともある。 約束するのはファイル名と置き場所だけで、これは「何をバックアップし、何を バージョン管理から外すか」を道具が判断できるようにするためである。 索引は SQLite からではなく GrooveSeek 経由で読むこと。

Rust ライブラリ API

本ポリシーの対象外。 public なアイテムが露出しているのは、バイナリとテストが そこから組み上がっているからであって、再利用のために提供しているからではない。 grooveseekpublish = false であり、cargo packageそもそも通らない (workspace が version 指定の無い path 依存を使っている)。crates.io 上にこれらの型へ 依存するものは 1 つも無いので、自由に変えてよい。

将来 GrooveSeek をライブラリとして公開するなら、それは独立した判断であり、 それ自身の安定性宣言を伴う。

診断とログ

ログ行・レベル・警告の文面は安定ではない。詳細度は RUST_LOG で決まる (RUST_LOG=grooveseek=debug で詳細、未設定なら info)。そこに出る内容は インタフェースではなくデバッグ補助である。

非推奨化 (Deprecation)

安定な面は、予告なく削除しない:

  1. minor で非推奨とマークする。使用は引き続き動作し、代替を名指しする警告を stderr に出す
  2. 削除は次の major 以降

例外は、旧挙動を保ったままでは直せないセキュリティ欠陥のみ。その場合は CHANGELOG エントリの冒頭で明示する。

GrooveSeek に安全に依存する

根拠

この範囲をどう決めたか(何を実測し、なぜ Rust API を外したか)は ADR-0008 にある。