mantis-structural-index について
mantis-structural-indexは、コード分析段階でソースコードからコンテンツアドレス方式の構造インデックスを構築するスキルです。意味単位ごとにバックエンドを選び、差分再利用に対応し、構造解析ツールが使えない場合はgrepにフォールバックします。
主な機能
- 01パーティションごとのバックエンド選択
- 02コンテンツアドレス方式の単位再利用
- 03スナップショット単位のインデックス再利用
- 04呼び出し元と関数境界のクエリ
- 05ツール不在時のgrepへのフォールバック
活用シーン
- 01コード探索向けのソース情報の準備
- 02シンボルの呼び出し元の特定
- 03ファイルと行番号から関数境界を特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ google/ mantis --skill mantis-structural-indexインストール後、Claude Code で/mantis-structural-indexを入力して読み込まれたか確認します。
詳細
構造コードインデックスビルダー
これは Pass Lifecycle Contract におけるオプションの第一級ステージです。
スナップショットが固定された直後(Block D)に実行され、最初のコード読解分析ステージ(要約/アーキテクチャ)の前に実行されます。必要なのは CODE_ROOT
+ SNAPSHOT_ID のみで、アーキテクチャ/KB に依存してはなりません。
システムの目標
構造コードインデックスビルダー。機能ベースのパーティションごとのバックエンド選択を使用し、ソースコードからコンテンツアドレス可能なセマンティックユニットインデックスを構築します。grep に段階的にフォールバックできる設計です。find_callers(symbol)、
get_function_boundary(file, line)、および呼び出しサイトの認識機能を提供し、検出時の LLM 推論品質を向上させます。構造データで grep ベースの呼び出しサイト検出を補完しますが、置き換えることはありません。インデックスは独立したセマンティックコンパイル単位から構成され、マニフェスト+SQLite カタログとして永続化され、範囲が制限されたクエリヘルパーを通じて公開されます。
コマンド定義
- コマンド:
/mantis-structural-index - 説明:
CODE_ROOT配下のソースコードから、コンテンツアドレス可能なセマンティックユニットインデックスを構築します。 - 引数(任意。オーケストレーターから渡され、Block A が使用します):
--snapshot_root/--snapshot_id/--state_root。すべて指定されていない場合は、MODE-OFF/レガシーモードとなります(カレントディレクトリからソースを読み取り、インデックスを./workspace/kb/structural_index/に書き込みます)。
入出力コントラクト
- 読み取り対象:
workspace/.mantis_state.json(来歴の確認とスナップショットを考慮した再ビルドロジックのために、active_snapshotを読み取ります)。workspace/kb/structural_index/manifest.json(一致時の再利用に関する冪等性を確認するために、snapshot_idを確認します。主な確認対象です)。workspace/kb/structural_index.jsonl(manifestがない場合の後方互換性のための来歴確認)。workspace/kb/structural_index/units/(増分ユニット再利用のためのコンテンツアドレス指定キャッシュ)。workspace/kb/structural_index/native/およびサイドカーのprovenance.jsonファイル(事前構築済みインデックスの添付ファイルとメタデータマニフェスト)。- CODE_ROOTのソースファイル(生成されたヘルパースクリプト経由。スクリプトはCODE_ROOT配下のすべてのソースファイルを解析します)。
- 書き込み対象:
workspace/kb/structural_index/manifest.json(STATE-RELATIVE — アトミックなコミットポイント。最後に書き込みます)。workspace/kb/structural_index/catalog.sqlite(STATE-RELATIVE — クエリに最適化されたサービングストア)。workspace/kb/structural_index/units/(STATE-RELATIVE — コンテンツアドレス指定による不変のユニット出力)。workspace/kb/structural_index/shards/(STATE-RELATIVE — 大規模コーパス向けの分割されたサービングデータ)。workspace/kb/structural_index/native/(STATE-RELATIVE — 事前構築済みインデックスの添付ファイル:SCIP、Kythe、LSIF)。workspace/kb/structural_index/tmp/(STATE-RELATIVE — ビルド中の一時オブジェクト)。workspace/kb/structural_index.jsonl(STATE-RELATIVE — 互換性のためのポインター。しきい値未満の場合は全件をエクスポートします)。workspace/helpers/build_structural_index.py(STATE-RELATIVE — ビルダーのヘルパースクリプト)。workspace/helpers/query_structural_index.py(STATE-RELATIVE — クエリのヘルパースクリプト)。
- 前提条件:
- CODE_ROOT配下にソースファイルが存在する必要があります。CODE_ROOTを解決できない場合(MODE-OFFで、読み取り可能な
active_snapshotがない場合)は、snapshot_idを"unknown"に設定して、現在のディレクトリを対象にビルドします。スキップしてはいけません。これはスタンドアロンでの効率性を確保するケースです。
- CODE_ROOT配下にソースファイルが存在する必要があります。CODE_ROOTを解決できない場合(MODE-OFFで、読み取り可能な
- **接続されるまで非アクティブ:**呼び出し元(ハーネス、
mantis-plan、またはmantis-researcher)がこのスキルを呼び出すまで、このスキルは空のインデックスを返します。失敗することはありません。ツールが利用できない場合やソースを解析できない場合は、空のインデックスを返します。 - 冪等性の保証:
- CODE_ROOTは読み取り専用です。書き込み先はSTATE-RELATIVEパスのみです。同じ
CODE_ROOTとSNAPSHOT_IDで再実行すると、再ビルドする代わりに既存のインデックスを再利用します(manifestのsnapshot_idが一致する場合)。ただし、MODE-OFFでは常に再ビルドします。コンテンツアドレス指定キャッシュのキーが一致する場合、個々のセマンティックユニットはスナップショット間で再利用されます(増分再ビルド)。
- CODE_ROOTは読み取り専用です。書き込み先はSTATE-RELATIVEパスのみです。同じ
手順
Step 0:ロケーターの解決(最初に実行)
LOCATOR RESOLUTION (before reading ANY target code or artifact):
0. ROLE: If this skill NEVER reads target source (report, calibrate, reflect),
you are a FINDINGS-ONLY stage: skip steps 2-6; still read active_snapshot from
state for provenance/annotation; NEVER stop merely because a code root is unset.
1. Determine CODE_ROOT, in this priority order:
a. If --target_root is passed on THIS invocation, CODE_ROOT = --target_root.
It is AUTHORITATIVE and OVERRIDES SNAPSHOT_ROOT and the state fallback
(used when a caller hands you a prepared tree, e.g. a patched shadow).
b. Else if --snapshot_root (or SNAPSHOT_ROOT) is passed, use it.
c. Else read state_root/workspace/.mantis_state.json (state_root from
--state_root if passed, else ./workspace/... relative to the current dir)
-> active_snapshot.root / .snapshot_id / .snapshot_pinned.
d. Else (no arg AND no readable active_snapshot): CODE_ROOT = current directory,
treat snapshot_pinned = false (MODE-OFF). Do NOT stop.
2. SENTINEL CHECK (only if snapshot_pinned is true AND you did NOT take path 1a):
verify CODE_ROOT/.mantis_snapshot_id exists and equals SNAPSHOT_ID. If missing
or different -> STOP "snapshot sentinel mismatch". (A --target_root tree (1a) is
deliberately mutated and is sentinel-EXEMPT.)
3. PATH FIELDS:
- SNAPSHOT-RELATIVE (read under CODE_ROOT): code_paths entries; plan target_files
that are file paths. Strip ONLY a trailing ":<digits>". A code_paths entry
containing "://" is a URL/endpoint, NOT a file read. A code_paths entry that is
NOT of the form <existing-path>:<integer> is a non-source LOCATOR
(symbol/offset/endpoint): only check that the artifact/symbol exists; skip ALL
line-range and line-existence logic.
- STATE-RELATIVE (read/write under state_root/workspace, NEVER prefix CODE_ROOT):
kb_references, repro_file_path, reattack_file_path, helper scripts, report
files, and all state/findings JSON.
4. Never WRITE under CODE_ROOT when snapshot_pinned is true. Any command that
compiles, generates, or writes artifacts MUST run in a PRIVATE SHADOW copy
(mktemp -d from CODE_ROOT), never with cwd=CODE_ROOT. Read-only inspection may
cd into CODE_ROOT.
5. VCS-METADATA CARVE-OUT: history-log extraction and any VCS diff/blame command
run in the LIVE repository root (which still has .git/.hg/.repo), NOT CODE_ROOT
(the snapshot copy strips VCS metadata). Do NOT stop merely because CODE_ROOT
lacks .git/.hg/.repo.
6. Every shell command uses ABSOLUTE paths and sets its own working directory on
that call. Do NOT assume the working directory persists between calls.
これはソースコードを読み取る段階です。ヘルパースクリプト経由でCODE_ROOT配下の対象ソースファイルを読み取ります。Block Aのstep 0にある「結果のみを出力する場合はスキップ」という指示は適用されません。
ステップ 1:冪等性/鮮度チェック(必ず最初に実施)
必ず最初に行うステップ: ビルドスクリプトの作成、バックエンドの調査、シンボルの抽出を始める前に、MODE-OFF または既存のマニフェストを確認します。
- MODE-OFF の確認(最初に実施): MODE-OFF(
SNAPSHOT_IDが"unknown"または未設定)の場合 → 常に再ビルドします。以前の"unknown"インデックスは再利用しないでください。ライブツリーは変更される可能性があり、"unknown"は定数であって(鮮度を示すものではないため)です。ステップ 2 に直接進みます(内容が変更されていない個々のユニットは、引き続きコンテンツアドレス指定キャッシュを利用できる場合があります)。 workspace/kb/structural_index/manifest.jsonが存在する場合は、そのsnapshot_idフィールドを読み取ります。snapshot_idが現在のSNAPSHOT_IDと一致する場合(かつSNAPSHOT_IDが"unknown"ではない場合)→ インデックスを直ちに再利用して終了します。ステップ 2 の実行、バックエンドの調査、ビルドスクリプトの作成は行わないでください。これにより、再試行やクラッシュ後の再開に伴うコストを抑えられます。manifest.jsonが存在せず、workspace/kb/structural_index.jsonlが存在する場合(後方互換性のため)は、そのプロベナンスヘッダー(_provenance、snapshot_idキー)を読み取ります。snapshot_idが一致する場合(かつ"unknown"ではない場合)→ 再利用して終了します。それ以外の場合は再ビルドに進みます。SNAPSHOT_IDが異なる場合は、再ビルドに進みます(ステップ 2~5)。インクリメンタルな再利用では、意味単位を再ビルドする前に、units/にコンテンツアドレス指定キャッシュのヒットがあるか確認します(下記の「コンテンツアドレス指定キャッシュキー」を参照)。キャッシュにヒットした場合は、再抽出せずにユニットの出力を再利用します。
ステップ 2:パーティションごとにバックエンドを選択
コードベースを意味単位に分割します(下記の「言語ごとの意味単位」を参照)。各ユニットについて、単一の全体的な段階的選択ではなく、パーティションごとの機能ベースの判断を用いて、この環境で利用可能な最も精度の高いバックエンドを選択します。以下は例であり、網羅的な列挙ではありません。ヘルパーはパーティションごとに各層を調査し、利用可能な最上位の層を選択します。
- スナップショットに一致する SCIP/LSIF/Kythe/clangd-static/リモートインデックス
(精度:
semantic)— 現在のsnapshot_idまたはroot_fingerprintに一致する事前構築済みインデックスが利用可能な場合。最も高精度で、型を考慮した完全な相互参照、呼び出し階層、ホバー/シグネチャーデータを提供します。 - コンパイラー/型チェッカーベースの抽出器(精度:
typecheck)—compile_commands.json、ビルドコンテキスト、または型チェッカーが利用可能な場合。型に基づいて正確にシンボルを解決し、呼び出しエッジを抽出します。 - 言語対応 AST 抽出(精度:
ast)—tree-sitter、ast-grep、または言語固有のパーサーが利用可能な場合。関数の境界、呼び出し式、シグネチャーを含む完全な AST 解析を行います。 - シンボルのみの抽出(精度:
symbol-only)—ctagsまたは同等のツールがPATH上にある場合。シンボルテーブルのみを対象とします(関数定義と位置情報。呼び出しグラフは含みません)。呼び出し箇所の抽出には、既知の関数境界内で軽量な正規表現パスを使用します。 - ヒューリスティックによるフォールバック(精度:
heuristic)— ソースファイルに対する Python 標準ライブラリの正規表現パスを使用します。言語に依存しないヒューリスティックにより、関数定義と呼び出しパターンを特定します。精度は低くなりますが、依存関係はありません。 - カバレッジのみのマニフェスト+字句解析によるフォールバック(精度:
coverage-only)— grep を使用し、構造インデックスは書き込みません。マニフェストにはstatus: "empty"が記録されます。利用側は grep ベースの探索(現在の動作とバイト単位で同一)にフォールバックします。
ネイティブインデックスのプロービングと解決ルール:
- プローブ手順:パーティションごとのバックエンドを評価する前に、
workspace/kb/structural_index/native/とそのサブディレクトリnative/{scip,lsif,kythe}/を調べ、事前構築済みインデックスファイルを確認します。 - スナップショット宣言の規約:ネイティブ形式(SCIP、LSIF、
Kythe)はバイナリペイロードにスナップショットの識別情報を直接埋め込まないため、
事前構築済みインデックスでは、対象スナップショットを宣言するサイドカー
provenance.jsonマニフェストをworkspace/kb/structural_index/native/provenance.jsonまたはnative/<kind>/provenance.jsonに配置する必要があります。マニフェストには次の配列が含まれます。 アタッチメント:[{"kind": "scip|lsif|kythe", "path": "...", "snapshot_id": "...", "root_fingerprint": "...", "language": "...", "indexer": "...", "precision": "semantic", "files": [...]}]。 ネイティブインデックスは、宣言されたsnapshot_idがSNAPSHOT_IDと一致する場合(SNAPSHOT_ID != "unknown"のとき)、または宣言されたroot_fingerprintがワークスペースで計算されたルートフィンガープリントと一致する場合に、一致と判定されます。files配列がない、または空の場合、そのネイティブインデックスは個別ファイルを直接カバーしていないものとして扱います(manifest.native_indexesに記録しますが、coverage行は更新せず、すべてのファイルについて下位ティアにフォールスルーします)。 - 取り込みの記録・保留ルール:生のバイナリネイティブインデックス(例:
SCIP protobuf)を依存関係なしの純粋な Python で解析するのは、コストが高く複雑です。
オプション A のビルダースクリプトでは、一致する事前構築済みネイティブインデックスを必ず検出し、そのエントリを
manifest.native_indexesに記録するとともに、プロヴェナンスマニフェストに記載されたすべてのファイルについてcoverageテーブルのbackend(例:"scip"または"scip-clangd")を設定します。catalog.sqliteにネイティブインデックスのシンボルが登録されていない場合(生のバイナリはハーネス/MCP リーダーでの処理に保留)、coverage.status = "deferred"とprecision = "deferred"を設定します(indexed_files = 0)。これにより、クエリーヘルパーが未取り込みのパーティションをsemantic精度の「権威ある空」と判定するのを防ぎ、コンシューマーが必須の grep フォールバックを実行するようにします。catalog.sqliteに登録済みの場合(例:オプション B による事前取り込み、またはscip-to-sqlite経由)、precision = "semantic"とstatus = "indexed"を設定します。
LSP は SCIP/LSIF と同等ではありません。 LSP はインタラクティブなプロトコルであり、ワークスペースの状態が部分的または可変である可能性があります。サーバーがスナップショットの識別情報とワークスペース全体の網羅性を証明できる場合に限り、使用してください。言語サーバーが実行中であるだけでは、自動的に semantic バックエンドとして認められるわけではありません。
SCIP のマージ。 SCIP では、精度レベルが異なるインデクサーからの相補的な情報のマージが明示的に認められています。その形式には、インデクサーのバージョンとドキュメントごとの言語メタデータが記録されます。ビルダーは、単一のカタログ内で複数のインデクサー(例:Go 用 SCIP インデックスと Python 用 tree-sitter)の結果をマージしてもかまいません。
決定性は、実行時に生成されるバージョン管理付きヘルパー(
build_structural_index.py、# MANTIS_HELPER_VERSION = 5、再利用時には grep して再生成)にあり、パーティションごとにバックエンドをプローブして選択します。バイナリは同梱しません。エアギャップ環境でも安全です。
決定論的な部分カバレッジ。 非常に大規模なソースツリーでは、全体の再構築によりステージ 0 の実行時間の大部分を費やすことがあります。検出順による打ち切りを、決定論的な優先度付きキューに置き換えます。
- 明示的な対象ファイルとシンボル(利用可能な場合は
plan.jsonから)。 - 変更されたユニットと既知の逆依存関係(Block E の差分と、既存カタログ内の逆依存関係エッジから)。
- ティア 1 および 2 のユニットを含むパッケージ/モジュールと、それらの直接インポート。
- 正規化されたパス順の残りのユニット。
このキューに、決定論的な max_units(デフォルト:10000)または
max_source_bytes(デフォルト:
500MB)の上限を適用します。後の呼び出しで再開できるよう、保留されたユニットをマニフェストに保存します。ステータス:complete、partial、empty、failed。
関連するパーティションが完全で、スナップショットに一致し、網羅され、意味的に正確な場合に限り、コンシューマーは「呼び出し元なし」という結果を確かなものとして解釈してください。それ以外の場合は、「インデックス化された呼び出し元なし」と返し、既存の字句検索フォールバックを実行します。インデックスはヒント専用であるため、部分的な網羅状況でも安全です(grep が引き続き正となります)。
ステップ 3:ヘルパースクリプトの作成と実行
実行時に生成するヘルパーを 2 つ使用します。どちらも grep して再生成する方式に従います。再利用する前に、先頭行を grep してバージョンマーカーを確認してください。マーカーがない場合、または異なる整数の場合は再生成します。
ビルダー:build_structural_index.py
- ビルダーを
workspace/helpers/build_structural_index.pyに作成します。先頭行は必ず# MANTIS_HELPER_VERSION = 5と完全に一致させてください。既存のヘルパーを再利用する前に、先頭行を grep してMANTIS_HELPER_VERSION = 5があることを確認します。マーカーがない場合、または異なる整数の場合はヘルパーを再生成します。 - ビルダーは、コードベースを意味単位に分割し(「言語ごとの意味単位」を参照)、コンテンツアドレス指定によるキャッシュキーを計算し、再利用のために
units/を確認し、パーティションごとにバックエンドを選択し(ステップ 2)、シンボルとエッジを抽出して、結果をcatalog.sqliteに書き込みます。 - ビルダーは、以下を抽出する必要があります。
- シンボル:
symbol_id、name、qualified_name、namespace、language、file_path、start_line、end_line、kind、signature、backend、precision。 - 呼び出しエッジ:
caller_id、callee_id、callee_name、file_path、line、edge_kind。 - 関数境界:
symbol_id、file_path、start_line、end_line、signature、language。 - 網羅状況:
file_path、indexed、backend、precision、unit_cache_key、status。
- シンボル:
- ビルダーはマニフェストを最後に書き込み(ステップ 4)、互換性ポインターも書き込みます。
- ビルダーは絶対パスを使用し、独自の作業ディレクトリを設定する必要があります(ブロック A のステップ 6)。
snapshot_pinnedが true の場合、CODE_ROOT配下に何も書き込んではいけません(ブロック A のステップ 4)。サイドカーファイルを生成するバックエンド(ctags のtags、cscope のcscope.out、clangd のキャッシュ)は、STATE 相対パスにリダイレクトする必要があります:ctags -f <state>/helpers/tags、cscope -f <state>/helpers/cscope.out、CLANGD_INDEX_STORAGE=<state>/helpers/。実行中のサーバーへの読み取り専用 LSP/SCIP クエリにはリダイレクトは不要です。
クエリヘルパー:query_structural_index.py
- クエリヘルパーを
workspace/helpers/query_structural_index.pyに作成します。先頭行は必ず# MANTIS_HELPER_VERSION = 5と完全に一致させてください。既存のヘルパーを再利用する前に、先頭行を grep してMANTIS_HELPER_VERSION = 5があることを確認します。マーカーがない場合、または異なる整数の場合はヘルパーを再生成します。 - クエリヘルパーは、
catalog.sqlite(またはリモートエンドポイント。同一の API)に対する件数制限付きのページネーション対応操作を提供します。これが利用時の契約となります(以下の「クエリインターフェース」を参照)。 catalog.sqliteが存在せず、structural_index.jsonlが存在する場合、クエリヘルパーは JSONL ファイルを線形スキャンするようフォールバックします(低速ですが機能します)。両方とも存在しない場合は、coverage.partition_status = "empty"として空の結果を返します。
ステップ 4:マニフェストの作成とコミット
- まずマニフェストを
workspace/kb/structural_index/tmp/manifest.jsonに書き込みます。その後、workspace/kb/structural_index/manifest.jsonにアトミックに名前を変更します。これがアトミックコミットのポイントです。すべてのユニット、カタログ、カバレッジデータを書き込んだ後、最後にマニフェストを書き込みます。 workspace/kb/structural_index.jsonlに互換性ポインターを書き込みます(STATE-RELATIVE —CODE_ROOT配下には絶対に置かないでください)。設定可能なしきい値(デフォルト:10K レコード)未満の場合は、完全な JSONL エクスポート(プロベナンスヘッダー+すべてのレコード)を出力します。しきい値を超える場合は、compat_pointer.full_export = falseおよびcompat_pointer.symbol_countを設定したプロベナンスヘッダーのみを出力します。大規模なコンシューマーはクエリーインターフェースを使用する必要があります。- マニフェストの
provider.backend_versionsフィールドには、言語ごとに使用したバックエンドを記録します(例:{"go": {"backend_name": "scip-clangd", "precision": "semantic"}, "python": {"backend_name": "tree-sitter", "precision": "ast"}})。 これにより、コンシューマーは精度レベルを把握できます。 - ビルドが中断されても、参照されていない一時オブジェクトが
tmp/に残るだけで、最後に公開されたインデックスが破損することはありません。再開時には、manifest.jsonのステータスとsnapshot_idを確認します(ステップ 1)。
ステップ 5:結果の返却/呼び出し元への通知
manifest.json、catalog.sqlite、クエリーヘルパーのパスを返します。概要(ユニット数、シンボル数、呼び出しエッジ数、使用したバックエンド、カバレッジのステータス)を含めます。- インデックスが空(利用可能なツールがない、またはソースファイルが見つからない)場合は、呼び出し元に次のように通知します:「構造インデックスが空です — 構造コンテキストを利用できません。」
- ユーザーに直接通知しないでください。このスキルは、ハーネス、プランナー、またはリサーチャーによってサブエージェントとして呼び出されます。
インデックスのスキーマとディスク上の仕様
ディスク上のレイアウト
workspace/kb/structural_index/
├── manifest.json # Atomic commit point — written LAST
├── catalog.sqlite # Query-optimized serving store (both directions indexed)
├── units/ # Content-addressed immutable unit outputs
│ └── ab/cd/abcdef... # sha256 prefix sharding (2+2 hex dirs)
├── shards/ # Partitioned serving data (large corpora)
│ └── shard_0000.sqlite
├── native/ # Prebuilt index attachments (SCIP, Kythe, LSIF)
│ ├── provenance.json # Prebuilt index provenance manifest
│ ├── scip/
│ └── kythe/
└── tmp/ # Temporary objects during build
workspace/helpers/
├── build_structural_index.py # Builder (MANTIS_HELPER_VERSION = 5)
└── query_structural_index.py # Query helper (MANTIS_HELPER_VERSION = 5)
workspace/kb/structural_index.jsonl # Compatibility pointer
マニフェストのスキーマ(manifest.json)
{
"schema_version": 1,
"snapshot_id": "<SNAPSHOT_ID or 'unknown'>",
"root_fingerprint": "<sha256 of sorted (path, content_sha256) for all source files>",
"status": "complete|partial|empty|failed",
"provider": {
"kind": "local-build|baseline+overlay|remote",
"catalog": "catalog.sqlite",
"backend_versions": {
"cpp": {"backend_name": "tree-sitter", "backend_version": "0.20.8", "precision": "ast"},
"go": {"backend_name": "scip-clangd", "backend_version": "0.2.3", "precision": "semantic"}
}
},
"units": {"total": 0, "reused": 0, "rebuild": 0, "failed": 0},
"coverage": {"total_files": 0, "indexed_files": 0, "failed_files": 0, "deferred_files": 0},
"shards": [{"id": "", "path": "", "checksum": "", "partition_key": "", "symbol_count": 0, "edge_count": 0}],
"deferred_units": [{"unit_id": "", "language": "", "files": [], "priority": 4, "reason": ""}],
"native_indexes": [{"kind": "scip", "path": "", "snapshot_id": "", "root_fingerprint": "", "language": "", "indexer": "", "precision": ""}],
"baseline": {"source": "ci|local|none", "snapshot_id": "", "manifest_path": ""},
"overlay": {"units_added": 0, "units_modified": 0, "files": []},
"compat_pointer": {"path": "structural_index.jsonl", "full_export": true, "symbol_count": 0},
"created_at": "<ISO 8601>",
"build_duration_ms": 0
}
アトミックコミット:マニフェストは最後に書き込まれます(tmp/ からアトミックに名前を変更します)。ビルドが中断されても、参照されていない一時オブジェクトが残るだけで、最後に公開されたインデックスが破損することはありません。
SQLite カタログのスキーマ(catalog.sqlite)
CREATE TABLE IF NOT EXISTS schema_meta (
key TEXT PRIMARY KEY, value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS symbols (
symbol_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
qualified_name TEXT NOT NULL,
namespace TEXT,
language TEXT NOT NULL,
file_path TEXT NOT NULL,
start_line INTEGER NOT NULL,
end_line INTEGER,
kind TEXT NOT NULL,
signature TEXT,
backend TEXT NOT NULL,
precision TEXT NOT NULL CHECK (precision IN ('semantic','typecheck','ast','symbol-only','heuristic','deferred','coverage-only')),
corpus TEXT DEFAULT 'default',
partition_key TEXT,
unit_cache_key TEXT,
source_layer TEXT NOT NULL DEFAULT 'baseline'
);
CREATE TABLE IF NOT EXISTS call_edges (
edge_id INTEGER PRIMARY KEY AUTOINCREMENT,
caller_id TEXT NOT NULL,
callee_id TEXT,
callee_name TEXT NOT NULL,
file_path TEXT NOT NULL,
line INTEGER NOT NULL,
edge_kind TEXT NOT NULL CHECK (edge_kind IN ('direct','indirect','virtual','macro','unresolved')),
corpus TEXT DEFAULT 'default',
partition_key TEXT,
source_layer TEXT NOT NULL DEFAULT 'baseline',
FOREIGN KEY (caller_id) REFERENCES symbols(symbol_id),
FOREIGN KEY (callee_id) REFERENCES symbols(symbol_id)
);
CREATE TABLE IF NOT EXISTS function_boundaries (
symbol_id TEXT PRIMARY KEY,
file_path TEXT NOT NULL,
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
signature TEXT,
language TEXT NOT NULL,
FOREIGN KEY (symbol_id) REFERENCES symbols(symbol_id)
);
CREATE TABLE IF NOT EXISTS coverage (
file_path TEXT NOT NULL,
indexed INTEGER NOT NULL DEFAULT 0,
backend TEXT,
precision TEXT,
unit_cache_key TEXT,
status TEXT NOT NULL DEFAULT 'pending',
corpus TEXT DEFAULT 'default',
partition_key TEXT,
PRIMARY KEY (file_path, corpus)
);
CREATE TABLE IF NOT EXISTS unit_cache (
cache_key TEXT PRIMARY KEY,
unit_id TEXT NOT NULL,
language TEXT NOT NULL,
extractor_name TEXT NOT NULL,
extractor_version TEXT NOT NULL,
file_count INTEGER NOT NULL,
symbol_count INTEGER NOT NULL,
edge_count INTEGER NOT NULL,
source_bytes INTEGER NOT NULL,
created_at TEXT NOT NULL,
snapshot_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS deferred_units (
unit_id TEXT PRIMARY KEY,
language TEXT NOT NULL,
files TEXT NOT NULL,
priority INTEGER NOT NULL,
reason TEXT NOT NULL,
cache_key TEXT,
created_at TEXT NOT NULL
);
-- Indexes: both directions!
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_qualified ON symbols(qualified_name);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path, start_line);
CREATE INDEX IF NOT EXISTS idx_symbols_lang ON symbols(language);
CREATE INDEX IF NOT EXISTS idx_edges_caller ON call_edges(caller_id);
CREATE INDEX IF NOT EXISTS idx_edges_callee ON call_edges(callee_id);
CREATE INDEX IF NOT EXISTS idx_edges_callee_name ON call_edges(callee_name);
CREATE INDEX IF NOT EXISTS idx_edges_file ON call_edges(file_path, line);
CREATE INDEX IF NOT EXISTS idx_boundaries_file ON function_boundaries(file_path, start_line, end_line);
CREATE INDEX IF NOT EXISTS idx_coverage_status ON coverage(status);
CREATE INDEX IF NOT EXISTS idx_deferred_priority ON deferred_units(priority);
パーティショニング:シンボル数が 500K を超えるか、エッジ数が 2M を超える場合は、lang:{language}:bucket:{NN}(安定したハッシュ)で shards/ 配下のシャード SQLite ファイルに分割します。各シャードのスキーマは同一です。クエリーヘルパーは、マニフェストに基づいてクエリーを適切なシャードにルーティングします。
コンテンツアドレス方式のキャッシュキー
cache_key = sha256(
schema_version +
extractor_name@version +
language +
compile_context_digest +
ordered_input_content_digests +
dependency_or_interface_digest
)
重要:snapshot_id はキャッシュキーに含めません。プロベナンスにのみ含めます。これにより、ユニットが同一であれば、コミット、ブランチ、スナップショット間で再利用できます。
正規シンボル ID
ネイティブ ID(セマンティックバックエンドが利用可能な場合):
- SCIP:
scip:{symbol} - Kythe:
kythe:{uri} - clangd:
clangd:{usr}
フォールバック ID(セマンティックバックエンドがない場合):
fallback:{language}:{file_path}:{sha256(qualified_name|start_line|signature)[:16]}
これにより、名前空間、オーバーロード、メソッド、ローカル関数、言語やリポジトリをまたいだ重複名を区別できます。
エッジの種類
エッジの種類は明確に分けて保持します。暗黙的に統合してはいけません。
direct— 既知のシンボルへの静的に解決された呼び出しindirect— 関数ポインター、クロージャー、コールバックvirtual— 仮想メソッドのディスパッチ(実行時に解決)macro— マクロ展開によって導入された呼び出しunresolved— 呼び出し先の名前は見つかったが、symbol_id を解決できなかったもの
言語ごとのセマンティック単位
| 言語 | 単位の種類 | コンパイルコンテキスト | 依存関係ダイジェスト |
|---|---|---|---|
| C/C++ | コンパイル単位 | compile_commands.json entry | sha256 of transitive header interfaces |
| Go | パッケージ | go.mod + build tags | sha256 of imported packages' exported API |
| Rust | Crate | Cargo.toml + features | sha256 of extern crate signatures |
| Java/Kotlin | コンパイラバッチ | module path + classpath | sha256 of imported class signatures |
| TypeScript | プロジェクト | tsconfig.json | sha256 of imported module type declarations |
| Fallback | 個別ファイル | なし | 空文字列 |
フォールバックの粒度ルール: フォールバック層(正規表現、AST パーサー、または言語のビルドシステムを使わない軽量クローラー)では、各ソースファイルをそれぞれ独立したセマンティック単位にする必要があります。複数のソースファイルを単一のフォールバック単位にまとめないでください。ファイルごとの単位分離は、コンテンツアドレス方式のキャッシュ効率に不可欠です。将来のスナップショットで1つのファイルが変更された場合、そのファイルの単位だけが無効になり、変更されていないファイルはすべてキャッシュにヒットします(reused = N-1)。
ベースライン + 差分オーバーレイ
- 静的ベースライン:スナップショット全体を対象に、CI で一度だけ構築します。
- 増分キャッシュ:不変の単位出力をスナップショット間で再利用します。
- 差分オーバーレイ:変更されたファイル/対象ファイルをローカルでインデックス化し、ベースラインに重ねます。
- リモートでの提供:グローバルインデックスのローカル処理にコストがかかりすぎる場合は、任意で利用できます。
クエリ時に、クエリヘルパーがベースラインとオーバーレイの結果をマージします。同じファイルについては、オーバーレイのシンボルがベースラインのシンボルより優先されます。マニフェストには、ベースラインのマニフェスト参照とオーバーレイの単位数を含む provider.kind = "baseline+overlay" が記録されます。
互換性 JSONL エクスポート
旧形式の structural_index.jsonl は、互換性のためのエクスポートとしてのみ保持されます。クエリの契約ではありません。しきい値(デフォルトでは10Kレコード)以下の場合、JSONL 全体がエクスポートされます。しきい値を超える場合は、出所を示すポインターのみが書き込まれます。大規模なコンシューマーは、必ずクエリインターフェースを使用してください。
1行目 — 出所ヘッダー:
{"_provenance": true, "snapshot_id": "abc123", "tool": "tree-sitter"}
2行目以降 — 構造レコード(各行に1件、_type 識別子付き):
{"_type": "function", "key": "src/parser.c:parse_input", "start_line": 45, "end_line": 120, "signature": "int parse_input(char *buf, size_t len)", "calls": ["malloc", "validate_input", "memcpy"]}
{"_type": "call_edge", "caller": "parse_input", "callee": "malloc", "file": "src/parser.c", "line": 78}
{"_type": "call_edge", "caller": "main", "callee": "parse_input", "file": "src/main.c", "line": 203}
出所ヘッダーのフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
_provenance | bool | 常に true — この行が出所ヘッダーであることを示します |
snapshot_id | string | インデックスの構築対象となった SNAPSHOT_ID |
tool | string | 使用したバックエンド(例:"lsp-clangd"、"tree-sitter"、"ctags"、"regex"、"grep") |
レコードの種類:
_type | 説明 | 主なフィールド |
|---|---|---|
function | 境界とシグネチャを含む関数定義 | key、start_line、end_line、signature、calls |
call_edge | caller から callee への file:line 上の呼び出し | caller、callee、file、line |
スナップショットの安全性
- ピン留めされている場合は、ライブツリーではなく
CODE_ROOTからビルドします。 スナップショットがピン留めされている場合、構造インデックスはピン留めされたCODE_ROOTからビルドされ、パイプラインが解析している正確なバイト列を反映します。 - 一致する場合は再利用します。
manifest.jsonに現在のSNAPSHOT_IDがすでに記録されている場合(かつSNAPSHOT_IDが"unknown"ではない場合。MODE-OFFでは常に再ビルドします)、それを再利用し、再ビルドしません。SNAPSHOT_IDが異なる場合、または"unknown"の場合は再ビルドします。個々のユニットは、コンテンツアドレス方式のキャッシュから引き続き再利用できます。これにより、再試行やクラッシュ後の再開にかかるコストを抑えられます。 - マニフェストのアトミック性。 マニフェストは
tmp/からアトミックな名前変更を行い、最後に書き込みます。ビルドが中断されても、参照されない一時オブジェクトが残るだけで、最後に公開されたインデックスが破損することはありません。再開時には、manifest.jsonのstatusとsnapshot_idを確認してください。 - HALTモードのSTALEフラグ。
snapshot_pinnedがfalse(HALT)の場合、インデックスはピン留めされていないCODE_ROOTからビルドされることがありますが、古い可能性があることを示すフラグが付けられます。コンシューマーは構造に関するヒントを参考情報として扱います。 - MODE-OFF:スキップせずにビルドします。
active_snapshotが存在しない場合(MODE-OFF)、現在のディレクトリ(cwd)を対象にビルドし、プロビナンスのsnapshot_idを"unknown"に設定します。これはスタンドアロンでの効率性を重視するケースです。スナップショットがピン留めされていなくても、インデックスは調査担当者やプランナーにとって有用です。スナップショットがないことだけを理由に、空のインデックスを返してはいけません。
利用規約
以下は呼び出し側向けの実行時の指示で、コンシューマーが構造インデックスをどのように使用するかを定めています。構造インデックスはヒント専用の拡張機能です。これを使用しないスキルの動作は、これまでとまったく同じです。
クエリーインターフェース
クエリーヘルパーが利用規約です。 すべてのコンシューマーはquery_structural_index.py(または互換性のあるリモートエンドポイント)を使用します。JSONLファイルはクエリーの利用規約ではなく、互換性のためのエクスポートにすぎません。
操作:
-
resolve_symbol(name, language?, file?, namespace?)→{results, total, ambiguous, coverage}- すべての一致を返します。あいまいなシンボルから、暗黙的に1つを選ぶことはありません。
- 呼び出し側は、
find_callers/find_calleesを呼び出す前に、必ず候補を特定しなければなりません。
-
find_callers(symbol_id, limit=100, offset=0)→{results, total, has_more, coverage}- 件数を制限し、ページネーションに対応した呼び出し元の検索です。
- 各結果には
precision、backend、edge_kindが含まれます。 - 結果が空の場合は、
coverage.partition_statusが含まれます。
-
find_callees(symbol_id, limit=100, offset=0)→{results, total, has_more, coverage}find_callersと同じ形式です。
-
get_function_boundary(file, line)→{symbol_id, start_line, end_line, signature, precision, backend} -
get_coverage(file?)→{total_files, indexed_files, failed, deferred, partition_status, backends_used}
主な特性:
- 結果の件数制限とページネーションに対応しているため、インデックス全体をメモリに読み込まずに済みます。
- グラフをたどる前に名前を明示的に解決するため、あいまいなシンボルから暗黙的に1つを選ぶことはありません。
- 各結果に精度とバックエンドが付与されます。
- 結果が空の場合もカバレッジが付与されます。
- ローカルプロバイダーとリモートプロバイダーで同じ操作を使用します(リモートの場合は環境変数
MANTIS_STRUCTURAL_INDEX_URLまたはマニフェストのprovider.kind = "remote"を使用)。
結果が空の場合のカバレッジ:
partition_status | 意味 | コンシューマーのアクション |
|---|---|---|
complete | すべてのファイルがインデックス化済み | 「インデックス化された呼び出し元なし」(ヒント専用ルールに従い、grepも実行します) |
partial | 一部のファイルが保留または失敗 | 「完全にはインデックス化されていません」— grepへのフォールバックを必ず実行します |
empty | 利用可能なバックエンドなし | 「インデックス化されていません」— grepへのフォールバックを必ず実行します |
failed | バックエンドの実行に失敗 | 「インデックス作成に失敗しました」— grepへのフォールバックを必ず実行します |
mantis-plan
- 関数レベルの依存関係の広がりを調べるには、構造インデックスのクエリーヘルパーを使用します。
調査を計画する際は、
resolve_symbol()の後にfind_callers()を呼び出して、 対象を呼び出すすべての関数を特定します。これにより、監査対象が単一ファイルの分析を 超えて広がります。 - 構造インデックスが決めるのは調査の順序(コールグラフの中心性に基づき、どの関数を 先に監査するか)であり、対象の包含ではありません。監査対象を広げることはできます (安全側に多めに報告)が、ファイルを削除したり除外したりしてはなりません。 対象を決める下限には、プランナーの既存ロジックを維持します。
mantis-researcher
- Wave 1(迅速なトリアージ): 関数名でリポジトリ全体を grep し、候補となる呼び出し箇所を
網羅的に洗い出します。これが必須の下限です。次に、構造インデックスのクエリーヘルパー
(
resolve_symbolの後にfind_callers)を使用して、最初に監査する呼び出し箇所の 順位付けと優先順位付けを行います(インデックスは実際の呼び出しと、コメント/文字列/ 変数名を区別します)。両方の結果セットの和集合を監査します。構造インデックスでは マクロによる呼び出し、関数ポインター、動的ディスパッチを見落とす可能性があるため、 grep が引き続き下限となります。 - Wave 2(詳細監査):
get_function_boundary(file, line)を使用して、まず外側の 関数から調査を始めます。必要に応じて呼び出し元/呼び出し先/ファイルへと範囲を広げ、 関数をまたぐコンテキストを確認します。これにより、網羅性を保ちながらコンテキストを 節約できます。 - 構造インデックスが存在しないか空の場合は、grep に基づく検出(現行の動作)に フォールバックします。構造インデックスは網羅性のためのヒントにすぎず、監査の品質を 高めますが、必須ではありません。
安全性
交渉の余地のない不変条件:
- 非依存。 必須のものはありません。ツールなし/解析失敗/未呼び出しの場合は、 空のインデックスから grep にフォールバックし、バイト単位で現行の動作と同じになります。 Pass Lifecycle Contract ではオプションです。準拠していないハーネスは単にスキップします。
- ヒントのみ/和集合/対象の包含には使用しない。 利用側は grep の結果との和集合を
監査します。インデックスが見落としたシンボルも、網羅的な検索で必ず見つけられる状態を
保ちます。構造インデックスが決めるのは順序であり、対象の包含ではありません。クエリー
コントラクトでは、
find_callersが正式な対象一覧ではなくヒントを返すことを規定します。 利用側は grep の結果と必ず統合しなければなりません。 - 判定を行わず、検出結果には触れない。 INV-1 に違反することはできず、それ自体で 検出結果を削除することもできません。
- 範囲は狭く、ソースの相互参照のみ。 バイナリ/ビルドから導かれる到達可能性 (「本番環境向けにコンパイルされているか」)は明確に対象外です。これは開発者向けの カスタマイズであり、このスキルには含まれません。ビルドに含まれないことによって 実際の検出結果が隠される可能性があるためです(INV-2)。ビルドから導かれる到達可能性を このスキルに組み込まないでください。
リファレンス用のブループリントは mantis-pipeline-adapter/references/mantis-structural-index.md にあります。これは、この SKILL.md を唯一の信頼できる情報源として示すスタブです。 仕様の内容をここに重複して記載しないでください。