Semantic search over a Markdown knowledge base, served over MCP.
GrooveSeek が壊さないと約束するもの、および意図的に約束しないもの。 本ポリシーは 1.0.0 から効力を持つ。それ以前のリリースは beta であり、 互換性の保証はない。
English version: stability.md
GrooveSeek は Semantic Versioning に従う。 本書における意味:
これが無いと、「1.0.0」は観測できるものすべてが 2.0.0 まで固定される、という
約束になる。それは本プロジェクトに守れる約束ではない。観測できる面は、public な
Rust API 全体、すべてのコマンドラインフラグ、6 個の MCP tool、groove.toml の
全セクション、そして SQLite スキーマに及ぶ。全部を凍らせれば、誰も依存していない
部分を永久に改善できないか、
誰も気付かない変更のたびに major を出すか、どちらかになる。
だから約束は意図的に狭めてある。そして狭めた事実を、推測に委ねず書き下す。
GrooveSeek は認証を持たない。そして持たせる予定も無い。 これは欠落ではなく 設計上の立場なので、ロードマップではなく本書に書く。
HTTP transport は同一ホストから使われる想定である。ネットワーク境界は別の何かが 担う — コンテナのネットワーク分離、reverse proxy、あるいはナレッジベースに顔をつける アプリケーション。それが何であれ、外からの接続を終端し、認証を済ませ、 GrooveSeek とは loopback で話す。
非 loopback bind は引き続き許される。コンテナはそうしないと publish した
ポートが届かないからである。ただしそれは、その境界を自分で引き受けたという意味に
なる。/mcp に掛かっているのは Host 検証と session 数の上限だけで、
どちらも認証ではない — ポートに到達できるものは、ナレッジベース全文を読める。
起動時の警告はそのままの意味である。
/ui と /api/admin/status は loopback でない peer を必ず拒否する。
これは設定で変えられないので、proxy は同一ホストに置く必要がある。
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 を画面上に表示する。
以下を壊すには major が要る。
この文書が説明している groove バイナリの long フラグ
(--kb-path / --config 等)。short 形式は文書化されている範囲で安定。
意図的に含めないものが 2 つある: 例の中に出てくる他プログラムのフラグ
(systemctl --user / cargo --release / huggingface-cli --include) と、
groove-tray のフラグ (この方針が対象とするコマンドラインではなく、
companion バイナリのため)。
「文書化された」は仮定ではなく検査している: groove の全サブコマンドを
歩いて、受け付ける long フラグが docs/ にも README にも出てこなければ
落ちるテストがある。凍る集合が「たまたま書かれたかどうか」で決まらないようにするため
stdout と stderr の責務分離: コマンドが結果を出すなら、結果は stdout、 診断は stderr。パイプラインが依存するので凍結する。
その意味で結果を出すのは search / graph / doctor / validate /
eval / tune / status / service status / service list。
これらについては出力先を凍結する。
serve が最も厳しい。 既定の stdio transport では、その stdout が
MCP 接続そのものであり、他のものを書いてはならない — 1 行混ざるだけで
全クライアントのセッションが壊れる。これも凍結対象で、形式ではなく禁止として。
それ以外はすべて診断であり stderr に留まる。index の進捗、
service install / uninstall / tray-install / tray-uninstall の確認、
そして status の “No index found” — これは答えではなく答えられないことの
報告なので、その場合 stdout は空になる。
status / service status / service list はかつて結果を stderr に
書いていた。それが ADR-0008
の残した未決事項であり、
ADR-0010
が決着させた (移したリリースは CHANGELOG が示す)。2>&1 で受けている側は
影響を受けない。stderr だけを捕まえていた側には何も届かなくなり、
groove status | … には、ずっとそう見えていたとおりに件数が流れる。
--format を取るサブコマンドはすべてどちらかの組に入れてある。
沈黙が約束と読まれないようにするため。
安定 — 人間以外に読ませるために存在するもの:
search / graph / doctor / validate が出す JSON: 現在文書化されている
各フィールドは、名前・型・意味を保つvalidate --format github (GitHub Actions の annotation 形式
::error file=…::message)。形を決めているのは GitHub であってこちらではない。
ここで約束しているのは「このフラグがその形式を出し続けること」安定ではない — 非安定を参照:
--format text 出力と、status / service status /
service list が印字するもの。人間が読むために書かれており、より良い言い回しが
見つかれば書き換える。出力先は凍結する (上記) が、文面は凍結しない — 件数が
stdout に届くことには依存してよいが、それを載せる行の形には依存できない。
status が最初に出す 2 つの数 (documents / chunks) を機械可読に取る
安定した経路は groove doctor --format json であるgraph --format dot と --format svg。これは図である: DOT は妥当な DOT、
SVG は妥当な SVG だが、レイアウト・ラベル・配色は見せ方であって変わるeval と tune が出す JSON。どちらも指標そのものが良くなっていく前提の
power-user 向け計測ツールで、eval は既にその理由で履歴に metric_version を
刻んでいる。形を凍らせると指標まで凍る上の節は「文書化されたフィールドは名前・型・意味を保つ」と約束している。
その集合がこれである — 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 する。どれも空オブジェクトの見た目とは違う:
min_quality と include_low_quality は適用されるが決して echo されない。
品質フィルタは既定で有効なので、{} は「結果に filter が掛かっていない」を
意味しないtags_any / tags_all は受理されるが echo からは落ちる
(空リストは何も除外しないため)。path_globs だけは例外で、空だと受理されず
error 封筒が返る — 何にも一致しない glob リストは「全部よこせ」の要求より
書き間違いである可能性が高いため。無効にしたいなら null を渡すmin_confidence_ratio は echo されるが何も絞らない — low_confidence を
比べる閾値を決めるだけ。echo は「結果を絞ったものの一覧」でもないfilter_applied は空にならない: query が何かを除外して
いれば、他の filter が無くても excluded_terms が echo される{} は「上表の行のうち、報告すべき効果を持って届いたものが 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 に記録してある。
list_topics に children が付いたのがその形——全 entry に
新しいキーが 1 つ増え、既存のものは名前も型も変わらないdeep_dive / find_gaps / summarize_topic / whats_new。
展開される文面は安定ではないkb:// の resource URI スキーム(kb://doc/{path} と kb://topic/{prefix})、
および「サーバが提示した URI は読み返せる」という規則
(ADR-0004)コマンドラインと MCP ツールは別々に凍る 2 つの名前空間である。パラメータが 変わったからフラグが変わる、あるいはその逆、ということはない。
両方が同じ概念を出しているところでは同じ名詞を使い、それ以外は各面の流儀に従う。
コマンドラインは kebab-case で、繰り返し可能なフラグは単数形。ツールのパラメータは
snake_case で、配列は複数形。よって --path-glob と path_globs は同じフィルタであり、
--tag-any / tags_any と --tag-all / tags_all も同様。対応は一致してはいないが
予測可能で、フラグごとの対応は usage.ja.md に書いてある。
意図的に対応させていないものが 2 つある:
get_connection_graph は groove graph、
rebuild_index は groove index。それぞれの集合は内部で一貫している——
モデルが選ぶ対象だからツールは「対象に対する動詞」として読め、人が打つものだから
サブコマンドは短い——そして、片方をもう片方に読み替えねばならない呼び出し手はいないrerank と --reranker。ツールのパラメータは呼び出しごとの真偽値、
フラグはモデルの選択 — そしてコマンドラインでは、モデルを明示することが
呼び出しごとの上書きそのもので、--reranker none はそのクエリだけ切る。
両者の背後にある既定値は rerank_by_default キーで、これは groove search も
groove serve も読む。対応するフラグは groove serve --rerank-by-default の方値は名前より厳しい規則に従う。名前の違いは調べ直せば済むが、値の違いは呼び出しが
失敗するため。二面で綴りが分かれる enum 値は、どちらの綴りも両側で受け付ける——
seed_strategy は all_chunks も all-chunks もどちらの面でも通る。
/mcp — Streamable HTTP transport のエンドポイント/healthz — そのパスと、健全なサーバが 200 を返すことmodle = "bge-m3" のような打ち間違いがそのまま通り、
違うモデルでナレッジベースが索引される。起動しない方が安全な失敗の向きである。
設定ファイルは、それを読むバイナリと組にして管理するもので、版をまたいで
共有するものではない既定のモデルを変えることは major である。モデル識別子は索引に記録され、 起動時に完全一致が要求されるので、既定を変えると既存の全インストールが 再索引するまで起動しなくなる。
1.0.0 の後ではなく前に改名した理由がこれである:
| 索引 DB | .groove.db(kb_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 でもロードできる |
以下は目に見えるが、patch を含むどのリリースでも変わりうる。本書がその告知である。
/ui と /api/admin/status は安定ではない。 これらはサーバを動かしている
本人が自分のサーバを見るためのもので、仕様として loopback 限定であり
(GrooveSeek は認証を持たない)、画面は変わり続ける前提である。
HTML も JSON も内部実装として扱うこと。
kb.path は既に削除した。 status 帯にも出さない。これはナレッジベースの
絶対パスを持っており、Windows では C:\Users\<名前>\... — つまり利用者の
アカウント名が、JSON の本体と、あのページのスクリーンショット全部に載っていた。
必要としている読み手はいなかった (tray が読むのは daemon.pid と
indexing.active だけ)。見ている本人にとってどの KB かを示すのは
kb.documents / kb.chunks / kb.model で、こちらは残っている。
本段落がその告知であり、非安定な面だからこそ 1.0 より前に消す。
両 route は Content-Security-Policy と X-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 面に置くべきものではない。
これは予告であって予定表ではない。これらは非安定な面なので、 上に書いたことはどちらの向きにも約束ではない。
どのサブコマンドであれ text 出力は、いつでも書き換え・並べ替え・追記されうる
(search --format text / graph --format text を含む)。parse しないこと。
graph --format dot / --format svg も同じ — 図であり、図の描き方は見せ方である。
eval --format json と tune --format json も、JSON だが安定ではない。
検索の計測値を報告するもので、計測の定義そのものが良くなっていく前提だから。
eval は履歴ファイルに metric_version を記録していて、定義の違う実行同士を
比較しないようにしている。JSON を凍らせると指標が凍る。
機械可読な形が必要で存在しないなら、それは機能の欠落なので、 画面スクレイパを書く前に issue を立ててほしい。
.groove.db は SQLite ファイルだが、内部スキーマは契約ではない。テーブルは
追加・削除・再構成されうるし、アップグレードが索引を作り直すこともある。
約束するのはファイル名と置き場所だけで、これは「何をバックアップし、何を
バージョン管理から外すか」を道具が判断できるようにするためである。
索引は SQLite からではなく GrooveSeek 経由で読むこと。
本ポリシーの対象外。 public なアイテムが露出しているのは、バイナリとテストが
そこから組み上がっているからであって、再利用のために提供しているからではない。
grooveseek は publish = false であり、cargo package はそもそも通らない
(workspace が version 指定の無い path 依存を使っている)。crates.io 上にこれらの型へ
依存するものは 1 つも無いので、自由に変えてよい。
将来 GrooveSeek をライブラリとして公開するなら、それは独立した判断であり、 それ自身の安定性宣言を伴う。
ログ行・レベル・警告の文面は安定ではない。詳細度は RUST_LOG で決まる
(RUST_LOG=grooveseek=debug で詳細、未設定なら info)。そこに出る内容は
インタフェースではなくデバッグ補助である。
安定な面は、予告なく削除しない:
例外は、旧挙動を保ったままでは直せないセキュリティ欠陥のみ。その場合は CHANGELOG エントリの冒頭で明示する。
search と graph の JSON だけにし、それ以外の text は parse しない.groove.db を直接読まないこの範囲をどう決めたか(何を実測し、なぜ Rust API を外したか)は ADR-0008 にある。