ruflo-adr 的 adr-index 技能:把 ADR 决策档案转化为可查询的依赖知识图谱
在 Ruflo 仓库中,架构决策记录(Architecture Decision Record,ADR)散落在各个模块的 docs/adr/ 或 docs/adrs/ 目录下,数量可达上百份。当 Agent 需要在一次会话内快速回答"哪个决策被替换了、ADR-097 依赖谁、历史决策文件与 AgentDB 图谱是否同步"这类问题时,逐份调用 MCP 工具读取显然低效。本指南围绕 SKILL.md 中定义的 adr-index 技能展开,说明它如何通过 scripts/import.mjs 一次批量导入全部 ADR,将记录持久化到 adr-patterns 命名空间、将因果关系(supersedes / amends / related / depends-on)写入 adr-edges 命名空间,并揭示其幂等写入、防误报抽取与只增不改的边界约束。读完本文,你将掌握 ADR 全量索引、图谱一致性验证与语义检索的完整实操链路,并能判断何时应改用 adr-reindex 这类"收敛"工具而不是继续追加索引。
一、技能定位:一次 Bash 调用替代数百次 MCP 往返
adr-index 是 ruflo-adr 插件中的核心技能之一,其定位非常明确——把磁盘上所有 ADR 文件同步为 AgentDB 中的结构化记录与因果边。技能 frontmatter 中的 description 直接说明了设计动机:one Bash call vs hundreds of MCP round-trips。
在 Ruflo monorepo 中,ADR 文件可能数以百计(仅 v3/docs/adr/ 下就存在 177 份决策记录),如果每个 ADR 都通过 memory_store 等 MCP 工具单独写入,将是数百次网络/进程往返。而 import.mjs 的注释给出了量化依据:
70+ ADRs × multiple memory_store calls each is hundreds of MCP round-trips. spawnSync over the CLI is materially faster and avoids shell-quoting pitfalls in the ADR titles.
即:通过 spawnSync('npx', ...) 以子进程方式调用 @claude-flow/cli 的 memory store 命令,批量写入同名数据,同时规避 ADR 标题中的特殊字符对 shell 引号造成的干扰。
adr-index 需要完成三类工作:
- 扫描:遍历仓库,收集所有位于
*/docs/adr/或*/docs/adrs/路径下的 Markdown 文件; - 解析:识别两种 ADR 编写格式并抽取字段;
- 持久化:写入
adr-patterns(记录)与adr-edges(关系)两个命名空间,并输出统计摘要。
二、何时使用 adr-index
依据技能文档中的 "When to use",以下三种场景适合调用 /adr-index:
| 场景 | 说明 |
|---|---|
| 从其他项目导入 ADR 后 | 迁移代码库或合并分支后,新落地的 ADR 文件需要一次性灌入 AgentDB |
| AgentDB 图谱与磁盘文件失步 | 新增、修改了 ADR 内容但图谱未更新,重跑索引做收敛 |
| 在存量代码库上启动 ADR 追踪 | 首次为历史悠久的项目建立 ADR 可检索基线 |
其对应的命令入口在插件的命令列表中也以 /adr-index 暴露,参见 README.md 的技能表。
三、两种 ADR 格式:双格式感知的解析器
技能文档强调,导入器必须同时兼容 Ruflo monorepo 中存在的两种 ADR 格式,这一能力实现在 parse-adrs.mjs 中:
- v3-style(v3 风格):以
# ADR-097: Title标题 +**Status**: Proposed状态行表示,这也是经典 Nygard / MADR 风格的延续; - plugin-style(插件风格):在文件头部使用 YAML frontmatter,形如:
--- id: ADR-NNNN status: Proposed ---
parseAdr() 依次从文件中抽取 id、title、status、date、tags、context(Context 首段)与 links(关系边)七个字段。值得注意的是解析器对格式细节做了高度容错,例如状态行的冒号允许出现在加粗内或加粗外(**Status:** 与 **Status**: 都接受),也接受可选的列表项前缀(- **Status**: proposed)以及 Proposed (v3.6.x) 这样的括号限定词——这些细节正是 adr-create 技能自身模板所产出的写法,若解析过严会导致新创建的 ADR 全部落入 Unknown 状态。
3.1 目录跳过规则:避免把外部仓库的 ADR 误索引
一个容易踩的坑是:从项目根目录递归遍历时,会把 .claude/worktrees/*(仓库镜像)与 .brain(ruvnet-brain 树,内含约 50 个外部仓库的浅克隆)下的 docs/adr/ 一并扫进来。实测数据显示,不跳过时一次遍历会捕获 1,415 份外来 ADR vs 项目自有 19 份,而且因为 .brain 按字典序排在 docs 之前,项目自身的 ADR 会被挤到遍历顺序的最后,一旦运行中断就会出现"导入了几百份外部 ADR、自己的却一份没导"的惨状。
因此 parse-adrs.mjs 定义了 SKIP_DIRS:node_modules、.git、dist、v2、.next、.turbo、build、.claude、.brain。findAdrs() 遍历时只保留以 .md 结尾且路径包含 /docs/adr/ 或 /docs/adrs/ 的文件。
四、执行步骤与运行参数
技能给出了标准执行流程,下面逐条展开:
4.1 运行导入器
node plugins/ruflo-adr/scripts/import.mjs
可选环境变量(import.mjs 中读取):
| 环境变量 | 作用 | 默认值 |
|---|---|---|
IMPORT_FORMAT=json |
摘要以 JSON 而非 Markdown 输出,便于后续程序化消费 | markdown |
IMPORT_DRY_RUN=1 |
只解析并输出摘要,跳过对 AgentDB 的持久化写入 | 未设置(执行写入) |
ADR_ROOT=/path |
指定扫描根目录 | process.cwd()(当前工作目录) |
其中 ADR_ROOT 尤为重要:它同时决定了扫描根目录和底层 memory 子进程的运行目录(cwd)。因为在 import.mjs 的实现中,每一个 npx ... memory store 子进程都必须传入 cwd: ROOT,否则 CLI 解析出的 .swarm/memory.db 会落在当前进程的 cwd 下——从仓库 A 运行却把数据写进仓库 B 的数据库(issue #2666)。
4.2 检查摘要输出
非 dry-run 成功完成后,脚本会在 stdout 打印 Markdown 格式的 ## ADR Index Summary,包含以下关键统计(对应 import.mjs):
- Total ADRs:扫描到的 ADR 总数及来源目录数量(source dirs),附扫描根目录;
- Records stored:成功写入
adr-patterns的记录数 / 总数(dry-run 模式下标注(dry-run, skipped)); - Edges stored:成功写入
adr-edges的关系边数 / 总数; - By status:按状态(Proposed / Accepted / Superseded 等)分组的数量;
- Relationships:按关系类型分组的边数;
- Issues found:三类问题清单——
- Dangling refs(悬空引用):边指向不存在的 ADR;
- Status mismatches(状态不一致):某 ADR 作为
supersedes边的源,但其自身状态不是Superseded,通常是后继 ADR 晋升时漏改了状态; - Storage errors:个别 key 写入失败的报错摘要;
- Source breakdown:各来源目录(按
/docs/前缀切分)的 ADR 数量,Top 12。
使用 IMPORT_FORMAT=json 时输出同一结构的 result 对象(含 byStatus、byRelation、bySource、danglingRefs、statusMismatches、errors),方便接入 CI 或其他脚本做断言。
4.3 验证图谱完整性(推荐)
索引完成后,技能文档推荐通过同级技能 adr-verify 做只读回读校验,其实现为 verify.mjs:
node plugins/ruflo-adr/scripts/verify.mjs
可选参数包括 VERIFY_FORMAT=json(JSON 报告)与 VERIFY_STRICT=1(只要发现任何问题就退出码 1;默认仅在发现 supersede 环时退出 1)。verify 主要检查三类缺陷:
- 悬空引用:边的
to(或from)指向adr-patterns中不存在的 ADR ID,常见原因是引用了兄弟仓库的 ADR,或目标文件已被删除; - Supersede 环:
ADR-A supersedes ADR-B且ADR-B supersedes ADR-A(或更长的环),在 verify.mjs 中通过 DFS 检测,一旦发现即视为数据损坏并以退出码 1 失败; - 状态不一致:发出
supersedes边的 ADR 自身状态不是Superseded。
退出码语义为:0 表示图谱健康(或非严格模式下仅有悬空引用/状态不一致);1 表示检测到环,或在严格模式下检测到任意问题。这一能力非常适合作为 CI 中 fail-closed 的闸门。
4.4 语义检索已灌入的记录
图谱灌入后,即可通过 ruflo 的 memory 语义搜索直接消费:
memory_search --query "federation budget" --namespace adr-patterns
由于技能 frontmatter 声明了 allowed-tools: Bash mcp__plugin_ruflo-core_ruflo__memory_list mcp__plugin_ruflo-core_ruflo__memory_search,Agent 在执行索引与查询时无需额外权限审批。查询命名空间必须是导入写入的 adr-patterns,否则会命中空的默认命名空间(这也是 #2781 所修复的读写不一致陷阱:若曾通过 CLI_CORE=1 把写入路由到 @claude-flow/cli-core@alpha 的 JsonMemoryBackend,默认读者 ruflo memory search 走的是 @claude-flow/cli@latest 的 SQLite 存储,会出现"147/147 stored 但搜索零命中"的假成功)。因此 import.mjs 现在遇到 CLI_CORE=1 只打印警告并忽略该变量,保证写入端与读取端永远一致。
五、存储结构:两个命名空间的确定性键设计
技能文档给出了明确的存储契约,其底层实现在 index-records.mjs 中,是可独立单测的纯函数。
5.1 adr-patterns:ADR 记录
- Key:
<ADR-id>::<basename>,例如ADR-097::ADR-097-some-title,由adrRecordKey()生成,id与文件名去扩展名后拼接; - Value(纯文本,见
adrRecordValue()):
<title> — <first paragraph of Context>
file: <relative path>
status: <Proposed|Accepted|Superseded|...>
date: <ISO date>
tags: <comma-separated>
Context 段落取自 ADR 文件中 ## Context 小节的第一段,截断为 400 字符并压缩空白,作为语义搜索的正文素材;file 为相对仓库根的路径,便于日后回溯原文。
该命名空间的命名遵循 ruflo-agentdb 的 kebab-case <plugin-stem>-<intent> 约定(详见 ruflo-agentdb ADR-0001 的 "Namespace convention" 一节),adr-patterns 即 adr + patterns。插件同时声明不得遮蔽保留命名空间(pattern、claude-memories、default)。
5.2 adr-edges:因果边
- Key:确定性三元组
<relation>:<FROM>-><TO>,例如supersedes:ADR-097->ADR-086,由edgeKey()生成; - Value:
{ "from": "ADR-097", "to": "ADR-086", "relation": "related", "capturedAt": "<ISO>" }
capturedAt 只是观测时间戳,不参与键的构成——正如 index-records.mjs 注释所言,时间戳若进入身份定义,每次索引运行都会再造一条逻辑上相同的边。parseEdgeKey() 还兼容旧的 relation:FROM->TO:timestamp-rand 后缀形态,使升级前的既有安装仍可被验证(issue #2660)。
5.3 幂等性:显式 upsert 与语义去重
技能文档特别强调两条数据完整性约束:
- 显式
--upsert:memoryStoreArgs()固定生成npx @claude-flow/cli@latest memory store --namespace=... --key=... --upsert --value=...参数序列(index-records.mjs),并刻意不依赖 CLI 解析器默认值——--flag=value单 token 形式同时规避了 npm 对以非 ASCII 破折号开头参数的 argv 校验问题(issue #2474,典型如标题中的 U+2014 em-dash)。重复运行adr-index会就地刷新变更的元数据(如状态从 proposed 升为 accepted),而不会产生同 key 的第二条记录; - 语义去重:
uniqueEdges()以关系三元组为身份做去重,{relation, from, to}完全相同的多条边只保留一条(index-records.mjs)。这样重复运行时不会为一条未变化的关系制造重复副本。
adr-patterns 记录的值变更会反映为不同内容、相同 key——测试 index-idempotency-2660.test.mjs 中正是断言:proposed 与 accepted 两种状态的 adrRecordKey 相等而 adrRecordValue 不等,且生成的 argv 恰好包含一次 --upsert。这就是"幂等索引"这一数据完整性契约的可测试化表达。
六、防误报:issue 号不会被误读为 ADR 编号
ADR 正文中经常出现 GitHub issue、PR 与 commit 引用,如 #1697、commit abc123、PR 1234。若直接用 ADR-(\d+) 正则抽取,会把这些 4 位数字误认成 ADR-1697 之类根本不存在的记录,进而产生成批悬空边。
技能文档明确要求:这些引用必须在正则抽取前被剥离。该逻辑位于 parse-adrs.mjs 的 extractAdrRefs():
const cleaned = s
.replace(/#\d+/g, '') // #1697
.replace(/issue[s]?\s*\d+/gi, '') // issue 1697
.replace(/PR\s*\d+/gi, '') // PR 1234
.replace(/commit\s*[`a-f0-9]+/gi, '') // commit `abc123`
.replace(/`[^`]*`/g, ''); // any backtick-quoted span
const re = /\bADR-?(\d+)\b/gi;
先剥离 issue/PR/commit 与反引号包裹片段,再执行 \bADR-?(\d+)\b 抽取。
另一个隐蔽的一致性问题涉及编号补零:文件名 0001-foo.md 解析出 ADR-0001,而正文 Supersedes: ADR-0001 若经过"补零再截断"流水线可能得到 ADR-001,导致每条边都因 ID 不匹配而变成悬空引用。修复方式是让文件名解析与正文引用解析共用同一个规范化函数 normalizeAdrId():≤3 位数字统一补零到 3 位,≥4 位保持原样,保证同一数值无论来自文件名还是正文,最终都落到同一个节点 key(见 parse-adrs.mjs)。
关系抽取还支持两类来源:frontmatter 中的关系字段(supersedes / amended-by / amends / related / depends-on)与正文中的 **Supersedes:**、**Amended by:**、**Related:**、**Depends on:** 标签行。特别地,supersedes 边的方向会被反转存储({ from: ref, to: selfId }),即"本文件被谁取代",从而在图上表达"新决策替换旧决策"的方向。
七、边界与限制:adr-index 不能做什么
技能文档最后严肃划清了 adr-index 的能力边界,这是最容易误用的部分:
adr-indexonly ever adds/upserts.
如果某个 ADR 文件被删除(或幸存文件中某条关系行被移除),adr-index 之前写入的那行数据会永久残留——它没有删除原语。更隐蔽的是,adr-verify 也无法发现这种残留:一个零入边零出边的孤儿行既没有悬空引用,也构不成环,图谱校验会照常判定"健康"(issue #2666)。
这构成两种不同的失效模式:
| 失效类型 | 含义 | 对应工具 |
|---|---|---|
| staleness(陈旧) | ADR 内容变了但存储记录未更新 | adr-index(收敛,add/upsert) |
| reaping(收割) | 源文件(truth)已删除,派生缓存行必须跟着消失 | adr-reindex(drop-and-rebuild) |
判别信号很简单:如果 adr-verify 报告的 ADR 数明显高于 find docs/adr -name '*.md' | wc -l 的磁盘真实文件数,就应运行 reindex.mjs 而不是再跑一次 adr-index。adr-reindex 的做法是先通过 memory purge --namespace <ns> --force 硬删除两个命名空间的全部行(真实 DELETE FROM memory_entries,而非 memory delete 的软删除墓碑——软删除仍占用 UNIQUE(namespace, key) 槽位,会阻塞同 key 的重新写入),再从当前磁盘状态全量重建,最后回读计数断言与磁盘文件数一致(技能见 adr-reindex/SKILL.md,其对应的决策记录为 ADR-0002)。该操作不可逆且会 purge 整个命名空间,因此日常收敛请留在 adr-index,仅在需要"收割"删除时升级到 adr-reindex。
八、技能协同:adr 家族的完整生命周期
adr-index 并非孤立技能,它与 ruflo-adr 插件的其他技能构成完整的 ADR 生命周期:
| 技能 | 职责 | 与 adr-index 的关系 |
|---|---|---|
adr-create |
顺序编号创建新 ADR,并在 AgentDB 中注册 | 生产 adr-index 消费的文件 |
adr-index |
全量导入,建索引 + 依赖图(只增不改) | 本文主角 |
adr-review |
基于 git diff 对照 adr-patterns 做合规审查 |
消费索引做语义匹配 |
adr-verify |
回读两个命名空间,暴露悬空引用/环/状态不一致 | 索引后的只读校验 |
adr-reindex |
针对删除的 ADR 做 drop-and-rebuild 调和 | 补足 adr-index 无法"删行"的缺口 |
从 README.md 可看到 ADR 状态机的全貌:
proposed --> accepted --> deprecated
\--> superseded by ADR-XXX
生命周期关系统一建模为因果边 supersedes、amends、depends-on、related。adr-create 技能在创建文件时会以标题为查询词在 adr-patterns 中做语义搜索找出相关决策,写入 Links 并调用 agentdb_causal-edge 注册 depends-on 边;adr-review 则按变更文件反查相关 ADR、加载 Decision/Status/Consequences,并借助因果查询确认被引用 ADR 是否已被 supersede,最终输出含 Violations / Warnings / Compliant / Unlinked Changes 的合规报告。adr-index 正是这张图谱的批量"注入器",位于整条链路的入口处。
九、验证契约与落地建议
仓库为插件提供了两重验证手段:
- 自动化测试:scripts/tests 下的用例直接针对纯函数层做断言,例如
index-idempotency-2660.test.mjs验证 key 稳定性与去重,skip-brain-dir-2911.test.mjs验证.brain跳过逻辑,parser-bullets-2659.test.mjs验证列表项前缀解析,adr-create-schema-2651.test.mjs覆盖创建模板——这样无需真实memory.db即可守住持久化契约; - 冒烟脚本:
bash plugins/ruflo-adr/scripts/smoke.sh,预期输出"22 passed, 0 failed",被视为插件与 CLI 之间的兼容性契约(详见 README.md 的 Verification 节)。
实操落地建议:日常开发中把 adr-index(收敛)与 adr-verify(校验)组合成固定检查动作,先全量导入、后只读验证;把 VERIFY_STRICT=1 接进 CI 作为 fail-closed 闸门;一旦出现"图谱 ADR 数 > 磁盘文件数"的信号立即切换到 adr-reindex 做一次全量调和,再回到索引-验证的常规节奏。这样既能享受批量导入的高吞吐,又能保住图谱与磁盘真相的长期一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00