mcpField
M
mantis-structural-index

mantis-structural-index

github.com/google开发2.7k1.5k 次安装3 天前更新
代码仓库

关于 mantis-structural-index

mantis-structural-index 是面向代码分析阶段的技能,可从源代码构建内容寻址的结构索引。它按语义单元选择后端并支持增量复用;结构化工具不可用时可回退到 grep。

主要功能

  1. 01按分区选择后端
  2. 02按内容寻址复用单元
  3. 03按快照复用索引
  4. 04查询符号调用方和函数边界
  5. 05工具不可用时回退到 grep

适用场景

  1. 01为代码发现准备源代码上下文
  2. 02查找某个符号的调用方
  3. 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 或现有清单:

  1. **检查 MODE-OFF(首先执行):**在 MODE-OFF 下(SNAPSHOT_ID 为 "unknown" 或未设置)→ 始终重新构建。不要复用之前的 "unknown" 索引,因为实时文件树会变化,而 "unknown" 是一个常量(并非新鲜度信号)。直接跳到第 2 步(内容未变的独立单元仍可能命中内容寻址缓存)。
  2. 如果 workspace/kb/structural_index/manifest.json 存在,读取其 snapshot_id 字段。
  3. 如果 snapshot_id 与当前 SNAPSHOT_ID 匹配(且 SNAPSHOT_ID 不是 "unknown")→ 立即复用索引并停止。不要执行第 2 步、探测后端或编写构建脚本。这样可以限制重试/崩溃恢复期间的开销。
  4. 如果 manifest.json 不存在,但 workspace/kb/structural_index.jsonl 存在(向后兼容),读取其来源信息头(_provenance、snapshot_id 键)。如果 snapshot_id 匹配(且不是 "unknown")→ 复用并停止。否则继续重建。
  5. 如果 SNAPSHOT_ID 不同 → 继续重建(第 2–5 步)。如需增量复用:重建语义单元之前,检查 units/ 是否有内容寻址缓存命中(参见下文的内容寻址缓存键)。命中缓存时,复用该单元的输出,无需重新提取。
第 2 步:按分区选择后端

将代码库划分为语义单元(参见下文的按语言划分的语义单元)。对于每个单元,使用基于能力、按分区决策的方式,选择此环境中可用的最精确后端,而不是使用单一的全局层级。以下仅为示例,并非穷举。辅助程序会探测每个分区对应的各层级,并选择可用的最高层级:

  1. 与快照匹配的 SCIP / LSIF / Kythe / clangd-static / 远程索引(精度:semantic)——如果存在与当前 snapshot_id 或 root_fingerprint 匹配的预构建索引。精度最高:完整的类型感知交叉引用、调用层级以及悬停/签名数据。
  2. 编译器/类型检查器支持的提取器(精度:typecheck)——如果 compile_commands.json、构建上下文或类型检查器可用。可进行类型精确的符号解析并提取调用边。
  3. 语言感知 AST 提取(精度:ast)——如果 tree-sitter、ast-grep 或特定语言的解析器可用。完整解析 AST:函数边界、调用表达式、签名。
  4. 仅符号提取(精度:symbol-only)——如果 ctags 或等效工具位于 PATH 中。仅提供符号表(函数定义、位置——没有调用图)。调用点提取会在已知函数边界内使用轻量级正则表达式扫描。
  5. 启发式回退(精度:heuristic)——使用 Python 标准库正则表达式扫描源文件。采用语言无关的启发式方法识别函数定义和调用模式。精度较低,但零依赖。
  6. 仅覆盖率清单 + 词法回退(精度: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 阶段时间。将按发现顺序截断替换为确定性优先级队列:

  1. 显式目标文件和符号(如果可用,来自 plan.json)。
  2. 已更改的单元和已知的反向依赖项(来自 Block E 差异,以及现有目录中的反向依赖边)。
  3. 包含第 1 和第 2 级单元的包/模块及其直接导入项。
  4. 按规范化路径顺序排列的其余单元。

对此队列应用确定性的 max_units(默认值:10000)或 max_source_bytes(默认值:500MB)上限。将延后处理的单元持久化到清单中,以便后续调用时继续处理。状态:complete、partial、empty、failed。 只有在相关分区完整、快照匹配、覆盖充分且语义精确时,使用者才应将“没有调用方”视为确切结论。否则,答案应为“没有已索引的调用方”,随后使用现有的词法回退。索引仅作提示,因此部分覆盖是安全的(grep 仍是权威依据)。

步骤 3:编写并运行辅助脚本

会使用两个运行时生成的辅助脚本。两者都遵循 grep 并重新生成的模式:复用前,先 grep 第一行检查版本标记;如果标记不存在或整数不同,则重新生成。

构建器:build_structural_index.py
  1. 将构建器写入 workspace/helpers/build_structural_index.py。第一行必须严格为 # MANTIS_HELPER_VERSION = 5。复用现有辅助脚本前,先 grep 其前几行,查找 MANTIS_HELPER_VERSION = 5;如果标记不存在或整数不同,则重新生成该辅助脚本。
  2. 构建器将代码库划分为语义单元(参见“按语言划分的语义单元”),计算内容寻址缓存键,检查 units/ 以复用已有内容,按分区选择后端(步骤 2),提取符号和边,并将结果写入 catalog.sqlite。
  3. 构建器必须提取:
    • 符号: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. 构建器最后写入清单(步骤 4)和兼容性指针。
  5. 构建器必须使用绝对路径并设置自身的工作目录(A 区块步骤 6)。当 snapshot_pinned 为 true 时,构建器不得在 CODE_ROOT 下写入任何内容(A 区块步骤 4)。会生成边车文件的后端(ctags tags、cscope cscope.out、clangd 缓存)必须重定向到相对于状态目录的路径:ctags -f <state>/helpers/tags、 cscope -f <state>/helpers/cscope.out、 CLANGD_INDEX_STORAGE=<state>/helpers/。对运行中服务器进行只读 LSP/SCIP 查询无需重定向。
查询辅助脚本:query_structural_index.py
  1. 将查询辅助脚本写入 workspace/helpers/query_structural_index.py。第一行必须严格为 # MANTIS_HELPER_VERSION = 5。复用现有辅助脚本前,先 grep 其前几行,查找 MANTIS_HELPER_VERSION = 5;如果标记不存在或整数不同,则重新生成。
  2. 查询辅助脚本提供针对 catalog.sqlite(或远程端点——API 相同)的有界分页操作。它就是使用约定(参见下方“查询接口”)。
  3. 如果 catalog.sqlite 不存在但 structural_index.jsonl 存在,查询辅助脚本会回退到线性扫描 JSONL 文件(速度较慢,但可正常工作)。如果两者都不存在,则返回空结果,且 coverage.partition_status = "empty"。
第 4 步:写入清单并提交
  1. 首先将清单写入 workspace/kb/structural_index/tmp/manifest.json。 然后将其原子重命名为 workspace/kb/structural_index/manifest.json。这是原子提交点——清单最后写入, 在所有单元、目录和覆盖率数据写入之后。
  2. 将兼容性指针写入 workspace/kb/structural_index.jsonl (相对于状态目录——绝不可放在 CODE_ROOT 下)。低于可配置的阈值 (默认值:10K 条记录)时,输出完整的 JSONL 导出(溯源信息头 + 所有 记录)。高于阈值时,仅输出带有 compat_pointer.full_export = false 且设置了 compat_pointer.symbol_count 的溯源信息头。 大型使用方必须使用查询接口。
  3. 清单中的 provider.backend_versions 字段记录每种语言所用的后端(例如, {"go": {"backend_name": "scip-clangd", "precision": "semantic"}, "python": {"backend_name": "tree-sitter", "precision": "ast"}}), 以便使用方了解精度级别。
  4. 构建中断会在 tmp/ 中留下未被引用的临时对象,不会损坏上一次已发布的索引。 恢复时,检查 manifest.json 的状态和 snapshot_id(第 1 步)。
第 5 步:返回结果/通知调用方
  1. 返回 manifest.json、catalog.sqlite 和查询辅助程序的路径。 附上摘要(单元数、符号数、调用边数、所用后端、覆盖率状态)。
  2. 如果索引为空(没有可用工具或未找到源文件),通知调用方:"结构索引为空——结构化上下文不可用。"
  3. 不要直接通知用户——此技能由执行框架、规划器或研究员作为子 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
RustCrateCargo.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}

来源标头字段:

字段类型描述
_provenancebool始终为 true — 标记这是来源标头行
snapshot_idstring构建索引所依据的 SNAPSHOT_ID
toolstring使用的后端(例如 "lsp-clangd"、"tree-sitter"、"ctags"、"regex"、"grep")

记录类型:

_type描述关键字段
function包含边界和签名的函数定义key、start_line、end_line、signature、calls
call_edgecaller 在文件:行号处对 callee 的调用caller、callee、file、line

快照安全

  1. 从 CODE_ROOT 构建,而非实时目录树(固定时)。 固定快照后,结构索引将从固定的 CODE_ROOT 构建,确保其反映流水线正在分析的确切字节。
  2. 匹配时复用。 如果 manifest.json 已包含当前的 SNAPSHOT_ID(且 SNAPSHOT_ID 不为 "unknown"——在 MODE-OFF 下始终重新构建),则复用,不要重新构建。SNAPSHOT_ID 不同或为 "unknown" 时重新构建。单个单元仍可从内容寻址缓存中复用。这样可限制重试和崩溃恢复期间的开销。
  3. 清单原子性。 清单通过从 tmp/ 原子重命名,最后写入。中断的构建会留下未引用的临时对象,但不会破坏上次发布的索引。恢复时,检查 manifest.json 的 status 和 snapshot_id。
  4. HALT 模式下的 STALE 标记。 当 snapshot_pinned 为 false(HALT)时,索引可能从未固定的 CODE_ROOT 构建,但会标记为可能已过期。使用方应将结构提示视为参考信息。
  5. MODE-OFF:构建,不要跳过。 当 active_snapshot 不存在(MODE-OFF)时,针对当前目录(cwd)构建,并将溯源信息中的 snapshot_id 设为 "unknown"。这是独立运行效率场景——即使未固定快照,该索引对研究人员/规划器仍然有用。不要仅因为快照不存在就返回空索引。

使用约定

以下是供调用方使用的运行时说明——它们定义了使用方如何使用结构索引。结构索引只是提示性增强;不使用它的技能,其行为与现在完全一致。

查询接口

查询辅助程序就是约定。 所有使用方都使用 query_structural_index.py(或兼容的远程端点)。JSONL 文件不是查询约定,只是兼容性导出文件。

操作:

  1. resolve_symbol(name, language?, file?, namespace?) → {results, total, ambiguous, coverage}

    • 返回所有匹配项——不会在存在歧义的符号中静默选择。
    • 调用 find_callers/find_callees 前,调用方必须消除歧义。
  2. find_callers(symbol_id, limit=100, offset=0) → {results, total, has_more, coverage}

    • 有界、分页的调用方查询。
    • 每个结果都包含 precision、backend、edge_kind。
    • 空结果包含 coverage.partition_status。
  3. find_callees(symbol_id, limit=100, offset=0) → {results, total, has_more, coverage}

    • 结构与 find_callers 相同。
  4. get_function_boundary(file, line) → {symbol_id, start_line, end_line, signature, precision, backend}

  5. 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 的发现方式(即当前行为)。结构索引仅是覆盖范围的提示——它能提高审计质量,但绝非必需。

安全性

不可妥协的不变量:

  1. 不作要求。 任何内容都不是必需的;无工具/解析失败/未调用 → 空索引 → grep 回退 = 与当前行为逐字节一致。在 Pass Lifecycle Contract 中为可选项;不符合规范的运行环境只需跳过即可。
  2. 仅作提示/取并集/绝不决定成员范围。 使用方通过 grep 对并集进行审计;索引遗漏的符号仍必须能通过穷举扫描发现。结构索引决定顺序,而非成员范围。查询契约对此作出强制规定:find_callers 返回的是提示,而非权威成员范围——使用方必须与 grep 结果取并集。
  3. 不作判定,不触碰任何发现项。 不能违反 INV-1,也不能自行丢弃发现项。
  4. 范围有限:仅限源代码交叉引用。 二进制/构建推导出的可达性(“是否编译进生产版本”)明确不在范围内——它属于开发者定制,而非此技能的一部分,因为缺少构建产物可能掩盖真实发现项(INV-2)。不要将构建推导出的可达性纳入此技能。

参考蓝图见 mantis-pipeline-adapter/references/mantis-structural-index.md。 它是一个指向此 SKILL.md 的存根文件,此文件是唯一的事实来源——不要在其中重复规范内容。

评论

支持 Markdown