anatta-rs/ast-to-mermaid

GitHub: anatta-rs/ast-to-mermaid

一个基于 Tree-sitter 的 Rust 命令行工具,将代码 AST 转换为多层级 Mermaid 图表,支持 Git 感知的结构差异对比和函数调用时序可视化。

Stars: 3 | Forks: 1

# ast-to-mermaid 基于 Tree-sitter 的代码图构建器,可生成五个缩放级别的 [Mermaid](https://mermaid.js.org/) 图表(project / overview / module / function / impact),以及针对每个函数的 `sequenceDiagram` 视图和适用于下游图存储的 JSON artifact bundle。**Git 感知**:可在任意 ref 处渲染图表,生成按 commit 的 bundle,并对比分支间的结构性变更。 独立的 Rust crate。**没有数据库,没有图后端,没有 async runtime,没有内部框架耦合** —— 只有 tree-sitter、serde、clap 以及常用的错误/日志辅助工具。输入一个路径,即可输出 Mermaid 字符串(或包含 `.mmd` + `.meta.json` artifacts 的目录)。 以 git blob SHA-1 为键的内容寻址缓存使得分支切换成本极低:95% 以上的文件未更改 → 跳过解析,在 rust-analyzer(1500 个文件)上实现了 **38 倍热路径加速**。 ## 在真实代码中查看 下方的每张图表都是此 crate 在其自身的 `src/` 上输出的真实 CLI 结果。长实体 ID(CLI 输出为 `code_src_some_module_rs__function__foo`)在某些标签中被缩短为尾段,以便在视觉上留出空间 —— 结构、边和计数均未修改。 ### 全局视角:`a2m project ./src` 整个 crate 的概览 —— 每个顶层模块 + 跨模块调用计数: ``` graph TD artifacts["artifacts — 1 mod, 9 fn, 2 struct"] bin["bin — 1 mod, 2 fn, 1 struct"] cache_rs["cache.rs — 1 mod, 5 fn, 5 struct"] cli_support_rs["cli_support.rs — 1 mod, 12 fn, 7 struct"] diff_rs["diff.rs — 1 mod, 4 fn, 7 struct"] error_rs["error.rs — 1 mod, 0 fn, 0 struct"] git_source_rs["git_source.rs — 1 mod, 5 fn, 1 struct"] graph_["graph — 2 mod, 0 fn, 2 struct"] lib_rs["lib.rs — 1 mod, 0 fn, 0 struct"] model_rs["model.rs — 1 mod, 0 fn, 3 struct"] parser["parser — 1 mod, 9 fn, 2 struct"] pipeline_rs["pipeline.rs — 1 mod, 14 fn, 3 struct"] render["render — 8 mod, 14 fn, 2 struct"] resolve_rs["resolve.rs — 1 mod, 4 fn, 0 struct"] artifacts -->|"1 calls"| render bin -->|"6 calls"| cli_support_rs cli_support_rs -->|"2 calls"| artifacts cli_support_rs -->|"1 calls"| cache_rs cli_support_rs -->|"2 calls"| diff_rs cli_support_rs -->|"3 calls"| git_source_rs cli_support_rs -->|"7 calls"| pipeline_rs pipeline_rs -->|"1 calls"| artifacts pipeline_rs -->|"4 calls"| git_source_rs pipeline_rs -->|"2 calls"| parser pipeline_rs -->|"1 calls"| render pipeline_rs -->|"1 calls"| resolve_rs ``` (注意 `graph_` 节点 ID:`graph/` 模块的名称与 Mermaid 的 `graph TD` 关键字冲突,因此 `sanitize_id` 为其添加了 `_` 后缀。如果没有这种转义,图表将无法解析 —— 几乎每个输出目标语言的 codegen 工具都必须处理这种保留字防护。) ### 收敛性:`a2m impact ./src --target parse_phase` 对 `pipeline::parse_phase` 的更改会如何向外扩散?它是一个内部 helper,被 `analyze` 和 `bundle` 同时调用,这意味着 **每个**经过解析循环的子命令都会触及它 —— 通过五个不同的 cli_support 处理程序,其中 `run_diff` 通过 `ensure_indexed` 绕了两跳: ``` graph BT parse_phase(("fn parse_phase (impacted)")) main["main"] run_analyze["run_analyze"] run_bundle["run_bundle"] run_index["run_index"] run_diff["run_diff"] ensure_indexed["ensure_indexed"] analyze["analyze"] bundle["bundle"] main --> run_analyze main --> run_bundle main --> run_index run_analyze --> analyze run_bundle --> bundle run_index --> bundle run_diff --> ensure_indexed ensure_indexed --> bundle analyze --> parse_phase bundle --> parse_phase ``` 五个入口点,两个私有 helper,全部收敛于一个受影响的函数。这种影响范围可以将“重构这个安全吗?”变成一个 10 秒钟就能得出答案的问题。 ### 分发器:`a2m module ./src --target render/mod.rs` `render::render` 是一个被 **六个**模块共享的函数名。解析器通过 `use` 导入 + 限定调用路径进行消歧,因此分发扇出和两个真正的调用者都落在了正确的节点上。`impl` 块中的方法也是一等公民 —— 可以通过 `--target Type::method` 寻址(例如 `--target HnswBuilder::build`),无需拼写出泛型参数: ``` graph TD subgraph render_mod["render/mod.rs"] level(enum Level) render["fn render"] require_target["fn require_target"] end function_render(["render"]) impact_render(["render"]) module_render(["render"]) overview_render(["render"]) project_render(["render"]) emit_artifacts(["emit_artifacts"]) analyze(["analyze"]) render --> function_render render --> impact_render render --> module_render render --> overview_render render --> project_render emit_artifacts --> render analyze --> render ``` 五个缩放级别,一个工具 —— `a2m overview` 和 `a2m function` 上面没有展示,但遵循相同的结构。 ### 差异对比:`a2m diff ..` 两个已缓存 bundle 之间的集合差异,按更改类型着色,**在更改的实体之间绘制了边**,以便您查看影响范围 —— 不仅是 *什么* 发生了改变,还有 *这些更改是如何连接在一起的*。这是在本仓库上执行 `a2m diff 36f1585~..36f1585` 的真实输出(该 commit 教会了解析器通过 `use` 导入 + 限定路径来消除跨模块调用的歧义): ``` graph TD %% diff: 36f1585~ → 36f1585 classDef added fill:#9f9,stroke:#0a0,color:#000 classDef removed fill:#f99,stroke:#a00,color:#000 classDef modified fill:#fb8,stroke:#d60,color:#000 classDef renamed fill:#9ff,stroke:#0aa,color:#000 %% added (11) n0["parser/mod.rs::function::collect_use_paths"]:::added n1["parser/mod.rs::function::extract_use_imports"]:::added n2["resolve.rs::function::file_module_name"]:::added n3["resolve.rs::function::split_call_name"]:::added n4["tests/cross_module_resolution.rs"]:::added n5["tests/…::bare_call_to_unique_name_still_resolves"]:::added n6["tests/…::build_store"]:::added n7["tests/…::qualified_inline_calls_dispatch_to_correct_sibling_module"]:::added n8["tests/…::use_import_resolves_to_mod_dot_rs_when_name_is_ambiguous"]:::added n9["extern:fs::read"]:::added n10["extern:tempfile::tempdir"]:::added %% modified (7) n11["parser/mod.rs"]:::modified n12["parser/mod.rs::function::CodeParser::parse_into"]:::modified n13["parser/mod.rs::function::extract_calls"]:::modified n14["parser/mod.rs::function::extract_item"]:::modified n15["parser/mod.rs::impl::CodeParser"]:::modified n16["resolve.rs"]:::modified n17["resolve.rs::function::resolve_cross_module_calls"]:::modified %% blast-radius edges (both endpoints in changeset) n12 --> n1 n12 --> n14 n14 --> n13 n1 --> n0 n17 --> n3 n17 --> n2 n5 --> n6 n5 --> n10 n6 --> n9 n6 --> n17 n7 --> n6 n7 --> n10 n8 --> n6 n8 --> n10 ``` `+11 -0 ~7 ↪0`。两个连接在一起的视觉聚类 —— 正是“提取 helper + 添加测试文件”重构的形态: - **上半部分**是生产环境的重构:`parse_into`(已修改)现在调用 `extract_use_imports`(新增),后者又调用 `collect_use_paths`(新增)。`resolve_cross_module_calls`(已修改)增加了两个新的 helper(`split_call_name`、`file_module_name`)。八个橙色节点,四个绿色叶子节点。 - **下半部分**是测试层:`cross_module_resolution.rs` 是一个全新的测试文件。其中的三个测试共享了一个 `build_store` helper,该 helper 调用 `resolve_cross_module_calls` —— 这条单一的边是两个聚类之间的桥梁,告诉您 **新测试文件实际上测试了新的解析器代码**,而不是一些无关的路径。 - **出现了两个外部原子**(`fs::read`、`tempfile::tempdir`),因为新的测试文件引入了两个以前在任何地方都没有引用过的 stdlib + dev-dep 符号。 图的形状就是更改的形状。一个纯粹的 bug 修复会有一个橙色节点和一条连接红绿节点对的边。一个干净的重构将全是橙色,没有绿色/红色。一个带有测试的功能添加看起来正是这样 —— 一个紧密的生产环境聚类通过一两条边连接到测试聚类。 `--format json` 为下游工具返回结构化的 `BundleDiff`。重命名启发式算法将具有相同 `content_hash` 的(已移除、已添加)条目进行配对。对于任何尚未缓存的 ref,会自动运行 `a2m index`。 ### 操作顺序:`a2m sequence ./src --target ` 其他五个视图是无序的调用图 —— 它们告诉您 *谁调用了谁*,而不是 *按照什么顺序*。`a2m sequence` 按源码顺序遍历一个函数体,并生成 Mermaid `sequenceDiagram`:每个接收者一条生命线,每次调用一个箭头,控制流被提升到 `alt` / `loop` 块中。它适用于 Rust 和 Python —— `for`/`while`/`if`/`match` 提升为 `loop`/`alt`,Rust 的后缀 `.await` 和 Python 的前缀 `await` 都会标记为异步箭头。以本仓库缓存模块中的 `dir_size_recursive` 为例 —— 12 行 Rust 代码,一次树遍历: ``` fn dir_size_recursive(dir: &Path) -> Result { let mut total = 0; for entry in std::fs::read_dir(dir)? { let entry = entry?; let meta = entry.metadata()?; if meta.is_dir() { total += dir_size_recursive(&entry.path())?; } else { total += meta.len(); } } Ok(total) } ``` `a2m sequence ./src --target dir_size_recursive` → ``` sequenceDiagram autonumber %% fn dir_size_recursive(dir: &Path) -> Result participant self as self participant entry as entry participant meta as meta loop for std::fs::read_dir(dir)? self->>entry: metadata alt if meta.is_dir() self->>self: dir_size_recursive self->>entry: path else self->>meta: len end end ``` 一目了然整个算法:一个遍历目录条目的 `for` 循环,根据 `is_dir()` 进行分支 —— 递归调用(第 3 步的 `self` 循环)和叶子文件路径(`meta.len()`)。递归在视觉上是 `self` 生命线上的一条自指箭头;不同生命线(`meta`)上的 `else` 分支使得目录/文件的区分一目了然,无需阅读源码。 接收者的分类是基于语法的:`obj.method()` → `obj`,`Type::method()` → `Type`,纯标识符 → `self`。`.await` 对箭头进行标注。测试/panic 的底层逻辑(`assert!`、`Some`/`Ok` 构造函数等)会作为噪音被过滤掉。传入 `--all --out ` 可以为树中每个非空函数输出一个 `.mmd` 文件。 ## 安装 ``` cargo install ast-to-mermaid ``` 这将安装一个名为 `a2m` 的二进制文件,包含 11 个子命令。从源码构建的方式也是如此: ``` cargo build --release ./target/release/a2m --help ``` ## 快速开始 ``` # 鸟瞰图:每个 crate/module + 跨 module 调用边(工作树) a2m project ./my-repo # 特定 git ref 下的相同图表 — 通过 `git ls-tree` / `cat-file` 读取, # 无需 checkout。 a2m project ./my-repo --ref main a2m project ./my-repo --ref v0.1.0 a2m project ./my-repo --ref HEAD~3 # 单个 module 的 items + 内部/跨 module 调用 a2m module ./my-repo --target src/server/handlers.rs # 进入某个函数的逆向调用链(谁调用了它?) a2m function ./my-repo --target parse_config # 某个类型上的方法 — `Type::method` 简写会自动为你处理 generics a2m function ./my-repo --target HnswBuilder::build # 作为 Mermaid sequenceDiagram 的单个函数体(语句顺序) a2m sequence ./my-repo --target run_diff # 或者工作树中每个非空函数,每个 fn 生成一个 .mmd a2m sequence ./my-repo --all --out ./diagrams # 正向 + 反向影响(默认 3 跳) a2m impact ./my-repo --target execute # 写入文件而不是 stdout a2m project ./my-repo --out graph.mmd # 输出 Graphviz DOT 而不是 Mermaid — 适用于对浏览器渲染器来说过大的图 # (GitHub 限制为 500 条边,mermaid.live 在差不多的位置会卡死)。 # 直接 pipe 到 dot/sfdp/twopi/circo: a2m project ./my-repo --format dot | dot -Tsvg > graph.svg # 将某个 ref 的完整 bundle 物化到缓存中(幂等,重复运行 # 会打印缓存的路径并立即退出) a2m index ./my-repo --ref main # 两个 ref 之间的结构性 diff — 带颜色区分的 Mermaid 输出 a2m diff main..feature # 在内置规则(target, node_modules, .git, dotfiles)的基础上跳过额外目录 a2m project ./my-repo --exclude vendor,generated # 查看 parse / resolve 阶段的耗时 + cache 命中率 a2m project ./my-repo --trace=info ``` ## 十一个子命令 | 子命令 | 输出 | 需要 `--target` | |---|---|---| | `a2m project` | 所有 crate + 跨 crate 调用计数 | 否 | | `a2m overview` | 顶层模块 + 计数 (fn / struct / trait) + 跨模块边 | 否 | | `a2m module` | 一个模块的条目 + 它们的调用者/被调用者,包括模块内和跨模块 | 是 — 模块路径或文件名 | | `a2m function` | 单个函数及其调用者,回溯 N 跳 | 是 — 函数名 | | `a2m impact` | 函数的前向 + 后向调用链(默认 3 跳) | 是 — 函数名 | | `a2m sequence` | 将函数体作为 Mermaid `sequenceDiagram`(语句顺序,每个接收者一条生命线) | 是 — 函数名,或 `--all` | | `a2m walk` | 列出路径下的源文件(无解析) —— 适用于 shell pipeline | 否 | | `a2m bundle` | 完整的 4 层 artifact bundle(使用 `--with-sequences` 时包含 `+ sequences/`,见下文) | 否 — 需要 `--out` | | `a2m index` | 将 git ref 的 bundle 物化到缓存中 (`./.a2m/cache/refs//`) | 否 — 默认为工作区 | | `a2m diff` | 两个已缓存 bundle 之间的集合差异,带有颜色编码的 Mermaid 或 JSON | 是 — `..` | | `a2m gc` | 根据 mtime + 软大小上限驱逐旧 / 超大的缓存条目 | 否 | 前八个命令接受 `--ref ` 以从任意 ref 读取,而不是工作区。最后三个命令接受 `--cache-dir ` 以重新定位缓存,并接受 `--no-cache` 以绕过缓存(临时的 tempdir)。 五个分析类子命令(`project`、`overview`、`module`、`function`、`impact`)也接受 `--format ` —— 参见 [当图对于 Mermaid 过大时](#when-the-graph-is-too-big-for-mermaid)。 ## 当图对于 Mermaid 过大时 Mermaid 通过 dagre 在客户端渲染。浏览器 —— 以及 GitHub 的 markdown 渲染器 —— 将输入限制在 **500 条边 / 50 KB** 左右。超过这个限制,图表在结构上仍然是正确的,但无法查看:GitHub 会显示 `Edge limit exceeded`,mermaid.live 会卡死,SVG 画布会返回空白。 Graphviz 可以轻松处理 10k+ 的节点。`--format dot` 标志会输出 DOT 而不是 Mermaid,因此您可以直接将其 pipe 到您选择的布局引擎中: ``` # 分层布局(默认 — 最适合阅读依赖链) a2m project ./my-repo --format dot | dot -Tsvg > graph.svg # 力导向布局(最适合在大图中发现集群) a2m project ./my-repo --format dot | sfdp -Tsvg > graph.svg # 径向布局(一个中心节点向外发散) a2m project ./my-repo --format dot | twopi -Tsvg > graph.svg ``` 请先安装 graphviz(`brew install graphviz` / `apt install graphviz` / `choco install graphviz`)。 保留的内容:节点、ID、边、边标签、`subgraph` 边界(作为 DOT cluster)。不保留的内容:Mermaid 特定的节点形状(trait 为六边形,常量为圆柱体等)会折叠为 DOT 默认的矩形。连接性得以保留;排版提示会丢失。这就是针对 *“否则图将无法查看”* 所做的权衡。 ## Artifact bundle `a2m bundle` 写入的是一个结构化的目录,而不是单一的 Mermaid 字符串 —— 每个实体都有自己的 `.mmd` 和 `.meta.json`,外加一个主 `index.json`: ``` a2m bundle ./src --out ./.artifacts ``` ``` .artifacts/ ├── overview.mmd # project-level diagram ├── index.json # schema=2, every entity (id, kind, content_hash, edges) └── entities/ ├── code_src_pipeline.rs.mmd # the module ├── code_src_pipeline.rs.meta.json # ↳ children, hash, ... ├── code_src_pipeline.rs__function__analyze.mmd # one function └── code_src_pipeline.rs__function__analyze.meta.json # ↳ callers, callees, line range, signature, doc ``` 每个 `.meta.json` 都包含实体的 ID、种类、文件/行范围、签名、文档、SHA-256 内容哈希,以及完整的边结构 —— `callers`、`callees`,以及用于 impl/trait 对的 `implements` / `implemented_by`。调用所分析的树之外的 crate(例如 `serde_json::to_string`、`divan::main`)在 bundle 中会变成合成的 `extern:` 原子,这样外部依赖性就会在边界处可见,而不是悄悄消失。 该 bundle 是纯 JSON + Mermaid —— 无需重新解析即可将其加载到任何图存储(Neo4j、DuckDB、内存中)。 ### 可选:按函数划分的序列图 传入 `--with-sequences`,bundle 就会获得第五层 —— 为每个函数体至少包含一次调用的 Rust 函数生成一个 Mermaid `sequenceDiagram`: ``` a2m bundle ./src --out ./.artifacts --with-sequences ``` ``` .artifacts/ ├── overview.mmd ├── index.json # function entries gain `sequence_path: "sequences/.mmd"` ├── entities/... └── sequences/ └── code_src_pipeline.rs__function__analyze.mmd # statement-order body view, lifelines per receiver ``` 默认关闭 —— 提取序列会使用 tree-sitter 访问者重新解析每个 Rust 文件,并使 bundle 的实际运行耗时大致增加一倍。具有空函数体的函数(getter、`unimplemented!()`、仅包含文档的存根)将被跳过,因此该层在真实的代码库中会保持稀疏。 `content_hash` 是实体源码片段的 **git blob SHA-1** —— 与 `git hash-object` 产生的值相同。缓存键、跨分支去重以及 `a2m diff` 重命名启发式算法都依赖于这个身份标识。 ## Git 感知模式 + 缓存 `a2m` 在 `/.a2m/cache/` 维护着一个内容寻址缓存。该目录在首次运行时创建,并自动添加到 gitignore(如果不存在,则写入单行的 `.a2m/.gitignore`),其结构如下: ``` .a2m/cache/ ├── version # schema + grammar + a2m versions; mismatch wipes ├── blobs/ │ └── .cbor # parse output for one file blob └── refs/ └── / # one materialized bundle per ref ├── overview.mmd ├── index.json └── entities/... ``` 两层结构,两个收益: - **`blobs/.cbor`** —— 按文件原子去重。切换分支 → 仅需重新解析更改的 blob。在 rust-analyzer 热路径上测得 38倍的解析阶段加速。 - **`refs//`** —— 在相同的 ref 上复用整个 bundle。在已缓存的 commit 上重新运行 `a2m index` 会打印路径并在约 50 毫秒内退出。 ### 工作流示例 ``` # CI:每次 commit 物化一次,下游 job 直接读取 bundle a2m index ./repo --ref "$GITHUB_SHA" cp -r .a2m/cache/refs/"$GITHUB_SHA"/ ./pr-graph/ # Dev loop:查看两个 PR ref 之间在结构上的变化 a2m diff main..feature/cache-rewrite # 清理缓存(默认 1 GB 软限制) a2m gc --max-size 500M --dry-run # plan a2m gc --max-size 500M # execute # 冷路径 benchmark(无持久化 cache) a2m project ./repo --no-cache --trace=info ``` ### 追踪 `--trace=info` 输出结构化的按阶段耗时: ``` INFO parse_phase{files=1464}: parse_phase done parsed=1464 atoms=15867 hits=1464 misses=0 elapsed_ms=42 INFO resolve_phase{atoms=15867}: resolve_phase done edges=5924 elapsed_ms=98 ``` `hits` / `misses` 计数器会准确告诉您原子缓存节省了多少工作。 ## 语言 - **Rust** — `tree-sitter-rust` - **Python** — `tree-sitter-python` 其他任何语言在遍历期间都会被静默跳过。解析器是 **查询驱动** 的:每种受支持的语言在 `src/parser/queries//` 中都会有一小套 `.scm` tree-sitter 查询文件(items、calls、uses、impl-methods)。添加一种语言大致上是:添加 `tree-sitter-` 依赖,放入查询文件,添加一个 `Language` 枚举变体。不需要为每种语言编写新的遍历代码。 ## 作为库使用 添加到您的 `Cargo.toml`: ``` [dependencies] ast-to-mermaid = "0.2" ``` ``` use ast_to_mermaid::artifacts::write_artifacts; use ast_to_mermaid::pipeline::{AnalyzeOptions, analyze, bundle}; use ast_to_mermaid::render::Level; use std::path::Path; fn main() -> Result<(), Box> { // Render a single Mermaid string at a given level. let mut opts = AnalyzeOptions::default(); opts.level = Level::Overview; let report = analyze(Path::new("./my-repo"), &opts)?; println!("{}", report.mermaid); // Or build the full artifact bundle. let (artifacts, _report) = bundle(Path::new("./my-repo"), &AnalyzeOptions::default())?; write_artifacts(&artifacts, Path::new("./.artifacts"))?; Ok(()) } ``` 较低级别的组件是公开的,供希望手动驱动 pipeline 的嵌入者使用:`parser::{CodeParser, Language}`、`graph::Store`、`resolve::{resolve_cross_module_calls, resolve_implements_edges, EXTERN_KIND}`、`render::{render, Level}`、`pipeline::{bundle, DEFAULT_EXCLUDED_DIRS}`。 ## 工作原理 ``` collect inputs (FS walk OR `git ls-tree --full-tree `) └─ src/git_source shell-out to git rev-parse / ls-tree / cat-file └─ src/cache per-blob: blobs/.cbor (cbor) per-ref: refs// (full bundle) └─ src/parser tree-sitter → ParseUnit { atoms, edges } (intra-file Calls + Contains edges) └─ src/graph in-memory Store └─ src/resolve cross-module Calls edges (file-scope `use` imports + qualified call paths to disambiguate same-named functions across modules) └─ src/render Mermaid string per zoom level └─ src/artifacts emit_artifacts → ArtifactSet → write_artifacts └─ src/diff compute_diff(BundleA, BundleB) → BundleDiff (added/removed/modified/renamed) + render_mermaid (colour-coded) ``` 没有 async,没有持久层,没有图后端。缓存是磁盘上纯 CBOR + JSON 文件。内存中的 `Store` 是一个 `RwLock`,并且只在一次 CLI 调用期间存在。 ## 质量检测 ``` make check # fmt + clippy (pedantic) + test make ci # check + coverage-gate make coverage-gate # fail if line coverage < 95 % make hooks # install pre-commit + pre-push hooks (.githooks/) ``` CI 会在每个 PR 上运行 `make ci`。覆盖率检测会忽略 `bin/*.rs`(这些是简单的包装器 —— 被测试的是库本身)。 ## 状态 `v0.2` —— Git 感知。十个子命令(七个渲染级别 + `index` / `diff` / `gc`),库 API,artifact bundle,以 git blob SHA-1 为键的两层内容寻址缓存。`index.json` 存在破坏性更改(`schema: 2`,新增 `content_hash` 字段,值格式从 `sha256:` 更改为原始的 git blob SHA-1)。已在 6 到 1463 个文件(rust-analyzer)的 Rust crate 上进行了测试;基准测试请参见 [`docs/perf/2026-05-01-resolve-cost-baseline.md`](./docs/perf/2026-05-01-resolve-cost-baseline.md)。 未来工作:针对大型 monorepo 的冷路径引入并行解析循环(`rayon`),如果 `--trace=info` 显示在实际工作负载中解析阶段耗时超过了总耗时的 30%,则考虑可选的 V2 级别缓存(目前即使在 rust-analyzer 规模下也 ≤ 7%)。 ## 示例(本仓库的真实输出) ### Atom cache:冷启动 → 热启动 → 单文件编辑 在此 crate 上连续执行三次 `a2m project ./src --trace=info`。缓存状态可通过 `hits` / `misses` 查看: ``` # Run 1 — 冷缓存,每个文件均为未命中 INFO parse_phase{files=22}: parse_phase done parsed=22 atoms=173 hits=0 misses=22 elapsed_ms=23 INFO resolve_phase{atoms=173}: resolve_phase done edges=50 elapsed_ms=0 # Run 2 — 热缓存,每个文件从磁盘重放,完全跳过 parser INFO parse_phase{files=22}: parse_phase done parsed=22 atoms=173 hits=22 misses=0 elapsed_ms=0 INFO resolve_phase{atoms=173}: resolve_phase done edges=50 elapsed_ms=0 # Run 3 — 修改了一个文件;仅该 blob 被重新 parse,其他 21 个被重用 INFO parse_phase{files=22}: parse_phase done parsed=22 atoms=173 hits=21 misses=1 elapsed_ms=1 INFO resolve_phase{atoms=173}: resolve_phase done edges=50 elapsed_ms=0 ``` 该模式可扩展:在 rust-analyzer 上(1464 个文件 / 570 k LOC),热解析阶段的时间从 1432 毫秒降至 42 毫秒 —— **38 倍加速** —— 参见 [`docs/perf/2026-05-01-resolve-cost-baseline.md`](./docs/perf/2026-05-01-resolve-cost-baseline.md)。 ### `a2m diff 0ee4cae..0ddc266` —— 原子写入 commit 构建此 README 的分支上两个 commit 之间的真实差异对比。原子写入 commit 添加了 `atomic_write` / `atomic_rename` helper 和 `write_bundle_atomic` CLI helper,然后修改了 `ensure_indexed` 和 `run_index` 以调用它们: ``` graph TD %% diff: 0ee4cae → 0ddc266 classDef added fill:#9f9,stroke:#0a0,color:#000 classDef removed fill:#f99,stroke:#a00,color:#000 classDef modified fill:#fb8,stroke:#d60,color:#000 classDef renamed fill:#9ff,stroke:#0aa,color:#000 n0["cache.rs::const::BLOB_ENVELOPE_VERSION"]:::added n1["cache.rs::const::BLOB_MAGIC"]:::added n2["cache.rs::function::atomic_rename"]:::added n3["cache.rs::function::atomic_write"]:::added n4["cache.rs::struct::BlobEnvelope"]:::added n5["cli_support.rs::function::write_bundle_atomic"]:::added n6["cache.rs"]:::modified n7["cache.rs::impl::Cache"]:::modified n8["cli_support.rs"]:::modified n9["cli_support.rs::function::ensure_indexed"]:::modified n10["cli_support.rs::function::run_index"]:::modified %% blast-radius edges (both endpoints in changeset) n9 --> n5 n10 --> n5 n5 --> n2 ``` 这些箭头直接说明了情况:两个 **已修改** 的函数(橙色)都新增了对新的 `write_bundle_atomic` helper(绿色)的调用,而后者又调用了新的 `atomic_rename` helper(绿色)。如果没有这些边,同样的差异对比仅仅是一个带颜色编码的列表 —— 有用但却无法体现调用关系。有了它们,您就能看出为什么这些修改是必要的。`+6 -0 ~5 ↪0`。 ### `a2m diff v0.1.0..HEAD` —— 整个 git 感知的历程 最后一个版本与此分支 HEAD 之间累积差异的统计数据: ``` diff v0.1.0 → HEAD: +63 -24 ~45 ↪0 ``` 新增了 63 个实体(新的 `cache`、`diff`、`git_source` 模块 + 它们的公开 API + 新的子命令处理程序),移除了 24 个(FNV-1a `hex_sha256`,被合并的七个独立二进制入口点),修改了 45 个(每个现有模块都增加了 `--ref` 管道,解析器被重构以公开 `ParseUnit`,artifact 发射器现在会写入 `content_hash`)。 将完整的 Mermaid 输出粘贴到 [mermaid.live](https://mermaid.live) 中,以直观地滚动浏览颜色编码的实体列表。 ### `a2m index --ref` 和重新运行 ``` $ a2m index ./repo --ref main indexed 8209bc8315459f3534c501b0d1607d2b84470fcd → /repo/.a2m/cache/refs/8209bc8.../bundle (22 files, 153 atoms, 47 edges) $ a2m index ./repo --ref main cached 8209bc8315459f3534c501b0d1607d2b84470fcd → /repo/.a2m/cache/refs/8209bc8.../bundle $ a2m index ./repo --ref main --force indexed 8209bc8315459f3534c501b0d1607d2b84470fcd → /repo/.a2m/cache/refs/8209bc8.../bundle (22 files, 153 atoms, 47 edges) ``` 默认具有幂等性:在同一个 commit 上重新运行会打印出缓存的路径,并在几毫秒内退出。`--force` 会重新物化(例如在解析器版本升级之后)。 ### `a2m gc` —— 修剪缓存 ``` $ a2m gc --max-size 100K --dry-run would remove 1 entries (235830 bytes) from /repo/.a2m/cache (had 1 entries, 235830 bytes) $ a2m gc --max-size 100K removed 1 entries (235830 bytes) from /repo/.a2m/cache (had 1 entries, 235830 bytes) ``` 驱逐是按照 mtime 升序进行的,直到总大小低于上限。`--older-than 30d` 会在此之上增加一个时间过滤条件。 ## 许可证 [Apache-2.0](./LICENSE)
标签:Mermaid, Rust, Tree-sitter, 云安全监控, 代码可视化, 可视化界面, 网络流量审计, 通知系统, 静态分析