GrooveSeek

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

View the Project on GitHub alphabet-h/grooveseek

18. 同じ範囲は 1 つの定義として扱う

背景と課題

ADR-0014 は、chunker が定義単位の chunk 化をやめるまでに定義が入れ子で居てよい深さの上限を置いた。定義のスコープを集める walk が自分の歩数を数え、構文木の祖先を 64 個超えた最初の定義でそのファイルの定義単位 chunk 化は終わる。この上限は同時に、chunker が持つ唯一の再帰関数が暴走しない根拠でもあった。 emit_def は包含関係の木を降りるが、包含する定義は構文木の祖先でもあるので、祖先の 上限は再帰の上限でもある、と考えられていた。

破綻するのはこの「でもある」の部分である。包含関係は構文木ではなくバイト範囲から計算 される。ある定義が別の定義を包含するのは、前者の範囲が後者の開始より先に終わらないとき。 範囲が自分自身より先に終わることはないので、同じバイトを覆う 2 つの定義は「一方が 他方を包含する」と記録される。こうしてできる鎖の長さは重複の個数そのものであり、その環は 1 つも構文木の祖先を増やさないので、スコープの上限には永久に見えない。

焼き込みの Rust grammar はこの形を作らない。3 リリースのあいだ気付かれなかったのはそのため である。しかし本プロジェクトが配布している PHP grammar は作る。その tags query は @definition.field を property_declaration に、@name をその下の property_element ごとに 付ける。tree-sitter-tags はタグをノード単位ではなく名前単位で保持するので、 public $a, $b, $c; は 1 つの範囲を覆う 3 つの定義として届く。1 つの文で 3,000 個の プロパティを宣言するクラスは 22,923 バイトのファイル — ここでの防御であるはずの 1 MiB の 生バイト上限の 2% — であり、これを索引すると実行は STATUS_STACK_OVERFLOW で終わった 。 スタックオーバーフローは panic ではない。 読めない 1 ファイルを実行全体の中止ではなく skip で済ませているガードはこれを見ることができず、何も書かれず、MCP transport 経由では プロセスもろともサーバが落ちる。

本決定が答える問い: 2 つの定義が同じバイトを覆うとき、定義とは何か。

判断の軸

検討した選択肢

  1. 同じバイトを覆う定義を 1 つに畳む
  2. スコープと chunk の上限の隣に、包含深さの第 3 の上限を足し、超過したら行単位 chunk へ degrade する
  3. 両方: 重複を畳んだうえで、その後ろに上限も残す

決定

選択肢 1。 既に記録された範囲と同じ範囲を覆う定義は、包含関係の木を作る前に落とす。 最も左の名前がその宣言を代表する。

選択肢 2 を採らない理由。 包含深さの上限はクラッシュを止めるが、何が誤りなのかを名指さ ない。同じバイトを覆う定義は深く入れ子になっているのではなく、複数の名前の下に見えている 1 つの定義であり、その重複を入れ子として記録していること自体が欠陥である。しかもこの上限 は、ファイル内のたった 1 つの宣言がたまたま横に長かったという理由で、そのファイルの定義 メタデータを全部失わせる。カンマ区切りのプロパティ列は病的な入力ではなくありふれた PHP である。degrade は「その形が定義単位 chunk 化を破るファイル」への正しい答えであって、 「chunker が単に読み違えたファイル」への答えではない。

選択肢 3 を採らない理由。 重複が消えれば包含関係は構文木より深く入れ子にならず、 スコープの上限が既に自分の限界を超えたものを拒否している。第 3 の上限にはどの入力も到達 しないので、それを出荷することは 3 番目の判断軸の失敗を意図的に出荷することになる。代わり にこの不変条件は debug_assert! として書いた。発火し得ない場所で強制を主張することなく、 同じことを述べる。

畳み込みはファイルだけから決まるので、同じファイルはどこでも同じように chunk 化され続ける。 宣言全体が chunk の予算に収まる場合、出力は一切変わらない — 鎖の最も外側の定義が既に その宣言を代表しており、最も外側の定義は元から最初の名前だったからである。

結果と代償

参考