Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help


id: ADR-016
title: Holding-crate 例外受限、有记录,并由 Core Team 授予
date: 2026-09-02
status: 提议中
relates-to:
  • ADR-007
  • crates/zeroclaw-runtime/AGENTS.md
  • docs/book/src/foundations/fnd-001-intentional-architecture.md
  • https://github.com/zeroclaw-labs/zeroclaw/pull/10557
  • https://github.com/zeroclaw-labs/zeroclaw/pull/10410
  • https://github.com/zeroclaw-labs/zeroclaw/pull/10179

ADR-016:持有型 Crate 的例外需有范围限制、有记录,并由核心团队批准

上下文

crates/zeroclaw-runtime/AGENTS.md 声明该 crate 为过渡性的暂存区,要求贡献者不要在其中新增功能,并列出了等待拆分出去的子系统。该要求是无条件的,且没有例外处理流程。

那种组合在常规情况下没有答案:已接受的工作落在一个仍留在暂存 crate 中的子系统上,而它本应迁入的 crate 尚未建成。契约禁止了唯一可用的归属位置,而目标位置又尚不存在。三个拉取请求都遇到了这种状态,并分别以三种不同的方式解决了它。

cron 前置条件门控先后经历了 #10220、一个提议中的一次性例外方案,最终在 #10557 中完成了完整抽取。抽取是正确的结果,但它是通过完整实现两种替代方案并丢弃其中一种才得出的。贡献者应当能够在将其实现两次之前,先判断是否需要抽取。

#10410 将共享配置和 agent 生命周期协调逻辑保留在运行时中,而没有在计划中的 daemon 抽离之前另行引入生命周期 crate。该做法的理由是:将代码迁移到 zeroclaw-infra 会颠倒现有的依赖关系,因为 config 已经依赖 infra;而且提前抽离会建立一个路线图并不打算建立的边界。这只是对该例外的论证,并非已定结论:该位置尚未被接受,#10410 需要在本记录所确立的流程下获得单独的明确处置。

#10179 遇到了同样的规则,但其传输层没有接收方调用者。在这种情况下,退役或明确的所有权决策,比任何例外都是更好的答案。

这三种情况在性质上就不同,而不仅仅是规模大小不同。提取对其中一种是恰当的,对另一种会产生错误的边界,而对第三种则与问题无关。单一的无条件指令无法区分它们,而对例外情况缄默,导致每位贡献者只能靠猜。

可选方案有三种:保持该指令无条件生效,要求在所有情况下都进行提取;整体放宽该要求;或者将提取作为默认做法,同时明确如何授予有范围限制的例外。

决策

Extraction 仍是默认做法。在立即提取需要进行不成比例的重构,或会建立路线图并未打算设立的 crate 边界的情况下,核心团队可以批准一项范围有限的例外。

What an exception must state

Translation: 异常必须说明的内容

已批准的例外需明确列出以下四项:

  • 允许范围。 该例外涵盖的具体路径。而不是抽象意义上的某个子系统。
  • 预期目标位置。 代码预计迁移到的 crate,使该例外描述的是延迟,而非撤销。
  • 审批授权方。 授予该权限的人。
  • 到期或复审条件。 由什么结束它,或何时重新考虑它。记录必须说明属于两者中的哪一种,因为它们的行为不同(见下文)。

过期含义

例外会过期或进入审查,记录中会注明是哪种情况。

到期会终止该权限。此后对覆盖范围的进一步新增,需要经核心团队重新批准。这并不要求移除已依据该例外合入的代码:权限只向前失效,不向后追溯,而撤销已获接受的内容属于抽离工作的职责,而不是日期到期的直接后果。

审查条件本身并不会结束任何事项。它要求核心团队重新考虑,而这次重新考虑的结果可能是续期、到期或移出。

授予方式

The record is created before the feature merges, and separately from it. A feature pull request cannot grant itself an exception, because the contract it would be waiving is the one constraining it.

Translation:

记录需在功能合并之前创建,且与功能本身分开进行。功能拉取请求无法为自身授予例外,因为它要豁免的正是约束它的那份约定。

An exception requires a concrete, supported use case. Code with no receiving caller does not qualify; retirement or an explicit ownership decision is the correct answer there.

Wait, I need to correct that: the translation should be in zh-CN.

例外需要具体且受支持的使用场景。没有接收调用方的代码不符合条件;此时正确的做法是废弃,或做出明确的归属决定。

单个例外的记录方式

例外的授予方式与仓库中的其他决策相同:通过拉取请求(Pull Request)审查,在所属 crate 的 AGENTS.md 中的活动例外表里添加条目,并由核心团队(Core Team)批准。不存在单独的机制,也不允许在常规审查规则之外进行批准。

将决策仅记录在评审讨论串中,并不算作已做出决策,因为之后阅读该契约的人无法找到它。

异常不是什么

一项例外允许在持有该 crate 中已包含的子系统上继续工作。它绝不允许在该处引入新的子系统,也不会从一个子系统推广到另一个子系统。为 cron 授予例外,并不意味着对 daemon 也有相同的许可。

采用

此记录将一项无条件禁止改为在明确条件下允许,因此它属于治理与贡献流程变更,而非普通文档编辑。FND-003 §8 规定了该途径。

采用该方案因此是核心团队需要明确做出并记录的决定,包括适用 FND-003 §8 中的哪一条路径。本拉取请求是具体的提案,而不是采用决定本身。在该决定被记录之前,无条件指令依然有效,且尚未授予任何例外。

后果

Contributors 能在动手实现之前,就先通过一条可以确定结论的决策路径来做判断,而不必等到实现之后才知道。cron 的案例就是例子:为了回答一个问题,最终完整实现了两遍,而一份简短的记录本可以先把这个问题定下来。

每一项例外都可见且标注了日期。holding-crate 合约中的活动例外表让累积的技术债务一目了然,每一条记录都注明了结束该例外所需的条件,因此一项已悄然变成永久状态的例外会显而易见,而不会被掩埋。

Core Team 会逐案判断是否相称,这是有意为之的设计。上面的三个案例表明,这种判断无法简化为一个规模阈值,因为“重构过度”和“边界划分错误”是两种不同的原因,只是恰好表现为相同的症状。

holding-crate 指令仍然有效。这并不会削弱它;它补充了该指令原本预设但从未定义的流程。

验收

ADR-016 保持提议状态,直到:

  • crates/zeroclaw-runtime/AGENTS.md 规定了例外规则,并包含一个当前生效的例外表格;
  • 至少已有一项例外申请通过此流程获得批准或被拒绝,这表明该流程切实可用,而不只是纸面上的规定。