关于 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)立即运行,早于第一个代码阅读分析阶段(summarize/architecture)。它只需要 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/legacy 模式(从当前目录读取源代码,将索引写入./workspace/kb/structural_index/)。
输入/输出约定
- 读取:
workspace/.mantis_state.json(读取active_snapshot,用于溯源检查和快照感知重建逻辑)。workspace/kb/structural_index/manifest.json(检查snapshot_id,以实现匹配时复用的幂等性——主要检查)。workspace/kb/structural_index.jsonl(清单不存在时,用于向后兼容的溯源检查)。workspace/kb/structural_index/units/(用于增量单元复用的内容寻址缓存)。workspace/kb/structural_index/native/和旁置的provenance.json文件(预构建索引附件和元数据清单)。- CODE_ROOT 源文件(通过生成的辅助脚本读取——该脚本会解析
CODE_ROOT下的所有源文件)。
- 写入:
workspace/kb/structural_index/manifest.json(相对于状态目录——原子提交点,最后写入)。workspace/kb/structural_index/catalog.sqlite(相对于状态目录——针对查询优化的服务存储)。workspace/kb/structural_index/units/(相对于状态目录——内容寻址的不可变单元输出)。workspace/kb/structural_index/shards/(相对于状态目录——大型语料库的分区服务数据)。workspace/kb/structural_index/native/(相对于状态目录——预构建索引附件:SCIP、Kythe、LSIF)。workspace/kb/structural_index/tmp/(相对于状态目录——构建期间的临时对象)。workspace/kb/structural_index.jsonl(相对于状态目录——兼容性指针;低于阈值时导出完整数据)。workspace/helpers/build_structural_index.py(相对于状态目录——构建器辅助脚本)。workspace/helpers/query_structural_index.py(相对于状态目录——查询辅助脚本)。
- 前提条件:
CODE_ROOT下必须存在源文件。如果尚未解析出CODE_ROOT(MODE-OFF 且没有可读取的active_snapshot),则针对当前目录构建,并将snapshot_id设为"unknown"。不要跳过——这是独立效率场景。
- **接线前不生效:**调用方(harness、
mantis-plan或mantis-researcher)调用此技能之前,它会返回空索引。如果工具不可用或无法解析源文件,它不会失败,而只会返回空索引。 - 幂等性保证:
- 对
CODE_ROOT只读。仅写入相对于状态目录的路径。使用相同的CODE_ROOT和SNAPSHOT_ID重新运行时,会复用现有索引(清单中的snapshot_id匹配),而不是重新构建;但在 MODE-OFF 下始终会重新构建。内容寻址缓存键匹配时,不同快照间会复用单个语义单元(增量重建)。
- 对
说明
步骤 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 步骤 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行;所有文件均回退到较低层级)。 - 记录并延后摄取规则:在不依赖任何依赖项的纯 Python 中解析原始二进制原生索引(例如 SCIP protobuf)成本高且复杂。Option A 构建脚本必须检测匹配的预构建原生索引,将其条目记录在
manifest.native_indexes中,并为溯源清单中列出的所有文件设置coverage表的backend(例如"scip"或"scip-clangd")。如果catalog.sqlite未填入原生索引中的符号(原始二进制数据延后由测试框架/MCP 读取器处理),则设置coverage.status = "deferred"和precision = "deferred"(且indexed_files = 0)。这样可防止查询辅助程序以semantic精度将未摄取的分区声称为“权威空”,并确保使用方运行强制的 grep 回退。如果catalog.sqlite已填入数据(例如通过 Option B 预摄取或scip-to-sqlite),则设置precision = "semantic"和status = "indexed"。
LSP 不等同于 SCIP / LSIF。 LSP 是一种交互式协议,其工作区状态可能不完整或可变。只有当服务端能够证明快照标识和完整的工作区覆盖范围时,才使用 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:编写并运行辅助脚本
会使用两个运行时生成的辅助脚本。两者都遵循 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)。会生成边车文件的后端(ctagstags、cscopecscope.out、clangd 缓存)必须重定向到相对于状态目录的路径: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(相对于状态目录——绝不可放在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和查询辅助程序的路径。 附上摘要(单元数、符号数、调用边数、所用后端、覆盖率状态)。 - 如果索引为空(没有可用工具或未找到源文件),通知调用方:"结构索引为空——结构化上下文不可用。"
- 不要直接通知用户——此技能由执行框架、规划器或研究员作为子 Agent 调用。
索引架构和磁盘契约
磁盘布局
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 条目 | 传递性头文件接口的 sha256 |
| Go | 包 | go.mod + 构建标签 | 导入包导出 API 的 sha256 |
| Rust | Crate | Cargo.toml + 功能 | 外部 crate 签名的 sha256 |
| Java/Kotlin | 编译器批次 | 模块路径 + 类路径 | 导入类签名的 sha256 |
| TypeScript | 项目 | tsconfig.json | 导入模块类型声明的 sha256 |
| 回退 | 单个文件 | 无 | 空字符串 |
回退粒度规则: 对于回退层级(正则表达式、AST 解析器或不带语言构建系统的轻量级爬虫),每个源文件都必须是独立的语义单元。切勿将多个源文件打包到单个回退单元中。按文件隔离单元对于内容寻址缓存效率至关重要——未来快照中某个文件发生变化时,只有该文件的单元会失效,而所有未更改的文件都会命中缓存(reused = N-1)。
基线 + 增量覆盖层
- 静态基线:在 CI 中为完整快照构建一次。
- 增量缓存:跨快照复用不可变的单元输出。
- 增量覆盖层:在本地索引已更改/目标文件,并叠加在基线上。
- 远程服务:当全局索引在本地的成本过高时可选使用。
查询时,查询辅助工具会合并基线 + 覆盖层的结果。对于同一文件,覆盖层中的符号会取代基线中的符号。清单会记录 provider.kind = "baseline+overlay",并包含基线清单引用和覆盖层单元数量。
兼容性 JSONL 导出
旧版 structural_index.jsonl 格式仅作为兼容性导出保留——它不是查询契约。低于阈值(默认 10K 条记录)时,会导出完整的 JSONL。超过阈值时,只会写入来源指针。大型使用方必须使用查询接口。
第 1 行 — 来源标头:
{"_provenance": true, "snapshot_id": "abc123", "tool": "tree-sitter"}
第 2 行及之后 — 结构化记录(每行一条,使用 _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 的调用 | 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}- 返回所有匹配项——不会在存在歧义的符号中静默选择。
- 调用
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}
关键属性:
- 结果有界且支持分页(防止将整个索引加载到内存中)。
- 遍历图前显式解析名称(不会在存在歧义的符号中静默选择)。
- 每个结果都附带精度和后端信息。
- 空结果也附带覆盖情况。
- 本地和远程提供方的操作完全相同(远程方式通过
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
- 波次 1(快速分诊): 对整个仓库执行 grep,搜索函数名,建立完整的候选调用点集合——这是强制底线。然后使用结构索引查询助手(先调用
resolve_symbol,再调用find_callers),对优先审计的调用点进行排序(索引能够区分实际调用与注释/字符串/变量名)。审计两组结果的并集——结构索引可能遗漏基于宏的调用、函数指针和动态分派,因此 grep 仍是底线。 - 波次 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 的存根文件,此文件是唯一的事实来源——不要在其中重复规范内容。