Memos 的架构决策记录(ADR)体系:命名规范、状态生命周期与三份核心 ADR 的工程落点
本文以 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 三种核心“标识语言”:
- ADR 0001:标签语法与识别。规定
#引导的标签在 Memos Markdown 中的词法形式(基于 Unicode UAX #31 与 Emoji 17.0 数据,钉住在 Unicode 17.0)、/层级分隔语义、Markdown 上下文资格判定(哪些节点透明、哪些不透明)、精确相等(不做大小写折叠或 Unicode 规范化)的标签身份,以及层级展开产生的“隐含祖先标签”语义。 - ADR 0002:用户名格式与引用。规定可写用户名的规范文法(1 到 36 个 ASCII 字符,字母/数字/连字符,首尾必须为字母或数字,保留大小写),用户名到用户 ID 的解析规则(精确 ASCII 字节相等),以及 Markdown
@mention作为用户名引用的第一种源形式。 - ADR 0003:Space UID 分配与格式。规定由第一方客户端生成小写 UUID v4 作为 Space 的公开 UID(也可自定义),API 字段保持可选以兼容旧客户端,以及 UI 上何时需要展示 UID 以区分同名 Space 的规则。
每份 ADR 都遵循统一的上下文:开头声明 Status 与 Date,并链接到 领域术语表 作为共享词源。
命名与编号约定
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 总览 规定四步流程:
- 用下一个可用编号创建
Proposed状态的 ADR,并加入索引。 - 与维护者讨论提案,随决策演进更新 ADR。
- 结论明确后将状态改为
Accepted或Rejected。 - 若已接受的决策发生实质性变更,创建新 ADR 并用交叉链接关联被取代的记录。
流程与“编号永不复用”“不改写原始理由”两条约定共同保证:ADR 目录是只增不减的决策历史,任何规则演进的因果链都可追溯。
决策的源码落点:从 ADR 到可验证的实现
ADR 的价值在于决策可以被代码印证。当前仓库中三份 ADR 的核心规则都能在源码中找到对应实现。
用户名格式(ADR 0002)
internal/base/username.go 实现了 ADR 0002 的规范文法:MaxUsernameLength = 36,IsValidUsername 要求首尾为 ASCII 字母或数字、中间字符由 IsUsernameCharacter(字母、数字、-)构成。ADR 中“完整消费 @ 后连续串再整体校验、不截短为合法前缀”的 mention 识别规则,对应 internal/markdown/parser/mention.go 的 FindMentionMatches:先扫描完整字符串,再用 IsValidUsername 整体判定,并用 IsUsernameCharacter 判定左边界。
ADR 0002 还规定用户名相等是精确 ASCII 字节相等,且这一规则由 schema 而非数据库默认排序保证。仓库中的迁移文件提供了直接证据:
- MySQL:store/migration/mysql/0.31/05__case_sensitive_username.sql 将
username列改为COLLATE utf8mb4_bin,LATEST.sql 中该列声明为VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL UNIQUE; - PostgreSQL:store/migration/postgres/0.31/05__case_sensitive_username.sql 将其改为
TEXT COLLATE "C"; - SQLite:store/migration/sqlite/LATEST.sql 中声明
username TEXT COLLATE BINARY NOT NULL UNIQUE。
三种后端的显式大小写敏感二进制排序,与 ADR 中“Alice 和 alice 是不同用户名”的结论逐字对应。
标签识别(ADR 0001)
ADR 0001 的“钉住 Unicode 17.0 数据”在 Go 侧有明确落点:internal/markdown/parser/tag.go 通过 go:generate 指令从 tagdata 生成 tag_unicode_tables.go(unicode17XIDContinue、unicode17DefaultIgnorable、unicode17CombiningMarks 等表),isSegmentStarter、isSegmentContinuation、isApostropheJoiner 三个判定函数正是文法中 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 特意提醒“通用的资源名规则不是用户名契约,不能仅因正则相似就复用作替代”——仓库中 UIDMatcher 与 IsValidUsername 正是两套独立实现的直接例证:前者允许首尾连字符形态,后者严格排除。
如何参与 ADR 演进
基于上述约定,对 Memos 的 ADR 体系可以归纳出对贡献者的清晰路径:查阅现有决策先看 docs/adr/README.md 的索引表;理解术语查 docs/glossary.md;若认为某个已接受决策需要变更,正确做法是按 ADR 总览 的流程新建一份 Proposed ADR、取下一个未复用编号、在索引中登记,并在结论明确后标注 Supersedes / Superseded by 交叉链接——而不是修改旧 ADR 的理由章节。这一机制使 ADR 目录既是决策的“当前事实来源”,也是可审计的决策演化史。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00