首页
/ ruflo-adr 的 adr-index 技能:把 ADR 决策档案转化为可查询的依赖知识图谱

ruflo-adr 的 adr-index 技能:把 ADR 决策档案转化为可查询的依赖知识图谱

2026-09-08 12:14:12作者:翟江哲Frasier

在 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/climemory store 命令,批量写入同名数据,同时规避 ADR 标题中的特殊字符对 shell 引号造成的干扰。

adr-index 需要完成三类工作:

  1. 扫描:遍历仓库,收集所有位于 */docs/adr/*/docs/adrs/ 路径下的 Markdown 文件;
  2. 解析:识别两种 ADR 编写格式并抽取字段;
  3. 持久化:写入 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 中:

  1. v3-style(v3 风格):以 # ADR-097: Title 标题 + **Status**: Proposed 状态行表示,这也是经典 Nygard / MADR 风格的延续;
  2. plugin-style(插件风格):在文件头部使用 YAML frontmatter,形如:
    ---
    id: ADR-NNNN
    status: Proposed
    ---
    

parseAdr() 依次从文件中抽取 idtitlestatusdatetagscontext(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_DIRSnode_modules.gitdistv2.next.turbobuild.claude.brainfindAdrs() 遍历时只保留以 .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 对象(含 byStatusbyRelationbySourcedanglingRefsstatusMismatcheserrors),方便接入 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-BADR-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-patternsadr + patterns。插件同时声明不得遮蔽保留命名空间(patternclaude-memoriesdefault)。

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 与语义去重

技能文档特别强调两条数据完整性约束:

  1. 显式 --upsertmemoryStoreArgs() 固定生成 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 的第二条记录;
  2. 语义去重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 引用,如 #1697commit abc123PR 1234。若直接用 ADR-(\d+) 正则抽取,会把这些 4 位数字误认成 ADR-1697 之类根本不存在的记录,进而产生成批悬空边。

技能文档明确要求:这些引用必须在正则抽取前被剥离。该逻辑位于 parse-adrs.mjsextractAdrRefs()

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-index only 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-indexadr-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

生命周期关系统一建模为因果边 supersedesamendsdepends-onrelatedadr-create 技能在创建文件时会以标题为查询词在 adr-patterns 中做语义搜索找出相关决策,写入 Links 并调用 agentdb_causal-edge 注册 depends-on 边;adr-review 则按变更文件反查相关 ADR、加载 Decision/Status/Consequences,并借助因果查询确认被引用 ADR 是否已被 supersede,最终输出含 Violations / Warnings / Compliant / Unlinked Changes 的合规报告。adr-index 正是这张图谱的批量"注入器",位于整条链路的入口处。

九、验证契约与落地建议

仓库为插件提供了两重验证手段:

  1. 自动化测试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 即可守住持久化契约;
  2. 冒烟脚本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 做一次全量调和,再回到索引-验证的常规节奏。这样既能享受批量导入的高吞吐,又能保住图谱与磁盘真相的长期一致。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
918
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.6 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
517
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389