首页
/ Memos 的架构决策记录(ADR)体系:命名规范、状态生命周期与三份核心 ADR 的工程落点

Memos 的架构决策记录(ADR)体系:命名规范、状态生命周期与三份核心 ADR 的工程落点

2026-09-05 16:56:42作者:宗隆裙

本文以 Memos 仓库的 ADR 总览文档 为主体,完整解析 Memos 如何管理架构决策记录:ADR 的索引机制、文件命名约定、状态机(Proposed / Accepted / Rejected / Superseded)、建议的文档结构与决策流程;并结合当前三份 Accepted 状态 ADR(标签语法、用户名格式、Space UID)对应的仓库源码实现,说明决策如何落到可验证的代码事实上。读完本文,你将掌握在开源项目中查阅、撰写和维护 ADR 的完整方法,并能快速定位每个决策在 Memos 源码中的落点。

什么是架构决策记录,以及 Memos 为什么需要它

ADR(Architecture Decision Records)用于记录那些在相关实现工作完成后、其决策理由(rationale)仍需要保持可查阅的重大技术与产品决策。这一动机在 Memos 中体现得很直接:标签识别、用户名校验、Space 标识分配这类规则,如果只散落在代码和 Issue 讨论里,后续维护者很难回答“当年为什么这样设计”。Memos 的 ADR 目录 就是这类决策的集中存放地,配合 领域术语表 形成设计文档体系:术语表定义跨文档共享的产品与领域语言,而协议、Unicode、解析器层面的专有术语保留在使用它们的 ADR 中。

ADR 索引:当前仓库中的三份决策记录

ADR 总览 维护一张索引表,记录每份 ADR 的编号、标题、状态与日期。当前索引如下:

ADR 标题 状态 日期
0001 Tag Syntax and Recognition Accepted 2026-08-01
0002 Username Format and References Accepted 2026-08-02
0003 Space UID Allocation and Format Accepted 2026-08-27

三份 ADR 分别定义了 Memos 三种核心“标识语言”:

  1. ADR 0001:标签语法与识别。规定 # 引导的标签在 Memos Markdown 中的词法形式(基于 Unicode UAX #31 与 Emoji 17.0 数据,钉住在 Unicode 17.0)、/ 层级分隔语义、Markdown 上下文资格判定(哪些节点透明、哪些不透明)、精确相等(不做大小写折叠或 Unicode 规范化)的标签身份,以及层级展开产生的“隐含祖先标签”语义。
  2. ADR 0002:用户名格式与引用。规定可写用户名的规范文法(1 到 36 个 ASCII 字符,字母/数字/连字符,首尾必须为字母或数字,保留大小写),用户名到用户 ID 的解析规则(精确 ASCII 字节相等),以及 Markdown @mention 作为用户名引用的第一种源形式。
  3. ADR 0003:Space UID 分配与格式。规定由第一方客户端生成小写 UUID v4 作为 Space 的公开 UID(也可自定义),API 字段保持可选以兼容旧客户端,以及 UI 上何时需要展示 UID 以区分同名 Space 的规则。

每份 ADR 都遵循统一的上下文:开头声明 StatusDate,并链接到 领域术语表 作为共享词源。

命名与编号约定

ADR 总览 明确了以下硬性约定:

  • 文件命名为 NNNN-short-kebab-case-title.md,使用四位数编号,且编号永不复用
  • 在 ADR 立项(opened)时即分配下一个编号——即使更早的 ADR 后来被拒绝或被取代。
  • 文档标题使用 # ADR NNNN: Title 形式,日期记录为 YYYY-MM-DD
  • 已接受的 ADR 作为历史记录保留。决策变更时用新 ADR 取代,而不是改写原始理由。
  • 当一份 ADR 取代另一份时,在新 ADR 中添加 Supersedes: ADR NNNN,在旧 ADR 中添加 Superseded by: ADR NNNN,形成双向交叉链接。

“编号永不复用”是关键设计:它保证了任何时刻仓库历史中引用的 ADR 编号都能稳定定位到一份文档,即使那份文档最终状态是 Rejected 或 Superseded。

状态定义

ADR 的生命周期由四个状态描述:

  • Proposed:讨论中,可能仍有未解决的问题。
  • Accepted:已批准作为要实施和长期维护的决策。
  • Rejected:经过考虑但未被选中。
  • Superseded:被后续 ADR 取代。

当前仓库的三份 ADR 均处于 Accepted 状态,因此它们同时是规范性文档——描述的规则是正在实施和维持的决策,而不是历史快照。

建议结构:无需重开讨论即可理解决策

每份 ADR 应包含足够的信息,使读者不必重建当时的讨论就能理解决策。建议结构如下(可选小节在不提供有用上下文时可以不写):

# ADR NNNN: Title

Status: Proposed

Date: YYYY-MM-DD

## Context

## Decision drivers

## Decision

## Consequences

## Alternatives considered

## Open questions before acceptance

## References

对照 ADR 0001 可以发现这套结构在真实文档中的落地方式:

  • Context:说明现状问题。ADR 0001 指出 Memos 当时存在四个行为不一致的标签实现(Go 解析器、前端 remark 插件、编辑器装饰、编辑器补全),并明确“这些差异暴露了实现漂移,但不定义目标语言,当前行为非规范性”。
  • Decision drivers:列出决策驱动力,如“对多语言个人笔记自然工作”“让后端、渲染器、编辑器装饰与补全获得相同的值和源跨度”“保持解析在 Go 与 JavaScript 运行时间确定性一致”。
  • Decision:ADR 0001 在此给出了完整的形式文法(TagCandidate := Introducer TagSourceSpelling 等)、15 条编号规则、最大前缀扫描算法与 Markdown 上下文判定;ADR 0002 给出了用户名文法与 mention 候选识别规则。
  • Consequences:分 Positive / Negative 两列。例如 ADR 0001 的负面后果包括“全限定 emoji 匹配需要序列感知数据而非简单字符类”“钉住 Unicode 数据意味着数据更新时需要维护”。
  • Alternatives considered:每个被否决的备选方案都有明确理由,如“保留 100 code point 上限”被否决的理由是“该值武断、统计的是 code point 而非用户感知字符,且当前存在三种不同的溢出行为”。

决策流程

ADR 总览 规定四步流程:

  1. 用下一个可用编号创建 Proposed 状态的 ADR,并加入索引。
  2. 与维护者讨论提案,随决策演进更新 ADR。
  3. 结论明确后将状态改为 AcceptedRejected
  4. 若已接受的决策发生实质性变更,创建新 ADR 并用交叉链接关联被取代的记录。

流程与“编号永不复用”“不改写原始理由”两条约定共同保证:ADR 目录是只增不减的决策历史,任何规则演进的因果链都可追溯。

决策的源码落点:从 ADR 到可验证的实现

ADR 的价值在于决策可以被代码印证。当前仓库中三份 ADR 的核心规则都能在源码中找到对应实现。

用户名格式(ADR 0002)

internal/base/username.go 实现了 ADR 0002 的规范文法:MaxUsernameLength = 36IsValidUsername 要求首尾为 ASCII 字母或数字、中间字符由 IsUsernameCharacter(字母、数字、-)构成。ADR 中“完整消费 @ 后连续串再整体校验、不截短为合法前缀”的 mention 识别规则,对应 internal/markdown/parser/mention.goFindMentionMatches:先扫描完整字符串,再用 IsValidUsername 整体判定,并用 IsUsernameCharacter 判定左边界。

ADR 0002 还规定用户名相等是精确 ASCII 字节相等,且这一规则由 schema 而非数据库默认排序保证。仓库中的迁移文件提供了直接证据:

三种后端的显式大小写敏感二进制排序,与 ADR 中“Alicealice 是不同用户名”的结论逐字对应。

标签识别(ADR 0001)

ADR 0001 的“钉住 Unicode 17.0 数据”在 Go 侧有明确落点:internal/markdown/parser/tag.go 通过 go:generate 指令从 tagdata 生成 tag_unicode_tables.gounicode17XIDContinueunicode17DefaultIgnorableunicode17CombiningMarks 等表),isSegmentStarterisSegmentContinuationisApostropheJoiner 三个判定函数正是文法中 SegmentStarter / ValueUnit / ApostropheJoiner 三条产生式的实现;matchFullyQualifiedEmoji 实现了“最长全限定 emoji 优先”的原子匹配,FindTagMatches 则按“emoji 优先 → 字符引用跳过 → 转义跳过 → # 引导扫描”的候选枚举顺序工作。ADR 中 #C++#R&D#foo/́bar 等大量规范性示例表,即为该实现的验收基准。

前端侧对应 web/src/lib/tag.ts 中的标签元数据匹配逻辑:findTagMetadata 先做精确键匹配,再把每个键作为锚定正则 ^pattern$ 测试,这印证了 ADR 0001 中“标签元数据规则可选择性匹配多个派生值,但不改变其身份”的领域语义——元数据只是装饰层,标签身份仍由源文本派生。

Space UID(ADR 0003)

ADR 0003 规定 Space UID 复用既有的公开资源 UID 文法。该文法在 internal/base/resource_name.go 中以 UIDMatcher 正则落地:^a-zA-Z0-9?$,与 ADR 的 BNF 产生式(Alphanumeric UIDCharacter{0,34} Alphanumeric,1 到 36 字符)完全一致。值得注意的是 ADR 0002 特意提醒“通用的资源名规则不是用户名契约,不能仅因正则相似就复用作替代”——仓库中 UIDMatcherIsValidUsername 正是两套独立实现的直接例证:前者允许首尾连字符形态,后者严格排除。

如何参与 ADR 演进

基于上述约定,对 Memos 的 ADR 体系可以归纳出对贡献者的清晰路径:查阅现有决策先看 docs/adr/README.md 的索引表;理解术语查 docs/glossary.md;若认为某个已接受决策需要变更,正确做法是按 ADR 总览 的流程新建一份 Proposed ADR、取下一个未复用编号、在索引中登记,并在结论明确后标注 Supersedes / Superseded by 交叉链接——而不是修改旧 ADR 的理由章节。这一机制使 ADR 目录既是决策的“当前事实来源”,也是可审计的决策演化史。

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