一次落地通道的设计复盘:该证明什么,与该替换什么,是两个问题。

1. 改一行提示词,也要重启整个 Agent?⌗

自修改 Agent 有个很容易写出来、也很容易把自己折腾累的流水线:

修改 → 合并 → 全量测试 → 类型检查 → 新版本 → 重启

整齐,统一,而且不问改了什么。

但文档、热加载工具、前端静态文件和核心运行时,并不是同一种东西。让它们走同一套流程,相当于修改菜单上的错别字,也要求厨房重新点火。

这次落地改动把判断拆成了两张表:

  • 验证计划:这个合并候选需要运行哪些检查?
  • 部署计划:运行中的系统需要怎样才能用到它?

关键不是给变更贴上“小、中、大”标签,而是承认:验证成本和生效方式不是同一个维度。

最有意思的结果是:界面变更可以只做构建验证,却仍然需要重启。这个看似不对称的决定,恰恰说明实现没有被漂亮的分级表骗走。

2. 第一张表:按合并后的实际路径决定验证⌗

verification-plan.ts 将变更分成三级。下面是该提交的实际规则,不是通用 CI 建议:

级别路径条件落地验证动作
1仅 skills/、prompts/、docs/ 或仓库根目录 Markdown;空变更也在此级不运行测试、类型检查或界面构建
2包含 ui/,其余路径只能属于第一级运行 ui:build,即 Vite 构建
3出现任何不属于以上范围的路径选择受影响测试,并执行类型检查;满足选择器规则时改跑全量测试

这是一个有明确默认方向的分类器:未知路径进入第三级,不会因为认不出来就按轻量变更处理。

例如 skills-other/ 不属于 skills/,src/README.md 也不是根目录 Markdown。测试专门覆盖了这两种看起来“差不多”的路径。

级别只会被更严格的成员抬高,不受路径排列顺序影响。一个批次同时修改核心、界面和文档,就进入第三级,不会因为最后合入的是文档而变轻。

落地通道取的也不是分支作者口头描述的改动范围,而是临时合并工作区中,基线到合并候选的 Git 差异。批量落地时,比较覆盖整个合并批次。

合并候选与基线的差异
          │
          ▼
     变更路径集合
          │
          ├─ 有非热层、非 UI 路径 → 第三级
          ├─ 否则有 UI 路径      → 第二级
          └─ 否则               → 第一级

还有个朴素但重要的细节:差异读取使用 --no-renames。一次移动被视为旧路径删除、新路径增加,两端都参加分类。把文件从核心目录移到热加载目录,不能顺便把核心删除动作藏掉。

3. 第三级也不再自动等于全量测试⌗

核心改动进入第三级后,生产验证接入已有的 selectAffectedTests,没有另造一套选择逻辑。

它主要做三件事:

  1. 通过反向依赖关系,寻找会导入已变更文件的测试;变更的测试文件本身也参与。
  2. 代码发生变化时,纳入扫描全仓库的守卫类测试;部分非代码资产则通过文件引用关系选择测试。
  3. 对测试公共地基的修改,直接选择全量测试,例如依赖清单、锁文件、测试基础设施及特定数据库迁移相关路径。

这里的“受影响”是选择器能识别的依赖关系,不是对所有运行时行为的数学证明。静态图可以缩小范围,但不能凭空理解任意动态加载或提示词语义。

类型检查没有随测试数量一起消失。测试夹具中,一个普通模块只关联到一个测试文件,断言是:测试运行器收到这一个文件,类型检查仍执行一次。即使没有选中任何测试,第三级仍保留类型检查。

基础设施失败也不会被洗成成功。测试执行端不可用时,结果保留“没有判决”的状态,而不是拿“没跑出失败”冒充通过。

但要把另一条边界说透:第一级根本不会进入这套受影响测试选择流程。 它不是“选出来零个测试”,而是分类策略明确决定不在此处运行测试。

两者的结果可能一样,责任却完全不同:前者是政策,后者是选择结果。

4. 第二张表:构建成功,不代表正在提供新界面⌗

验证计划出来后,如果直接写成“一级不重启,二级也不重启,三级重启”,看上去很顺。

源码没有这么做。

变更类别验证动作是否进入新 release 部署
仅热层或文档跳过上述验证命令否
UI 与可选热层、文档Vite 构建是
含核心等其他路径受影响测试或全量测试,加类型检查是

原因不是前端代码比提示词更危险,而是当前系统从运行中的只读 release 的 ui/dist 提供静态文件。

临时合并工作区里的构建,证明的是候选可以构建;它没有替换正在服务用户的产物。

合并工作区中的 UI 源码
          │
          ▼
      Vite 构建通过
          │
          └─ 这是验证结果,不是线上文件切换

运行中的 release/ui/dist
          │
          ▼
      实际对外提供的界面

所以,这个版本的 UI 变更仍然沿用 release 部署和重启路径。未来若建立独立的静态产物发布机制,可以再改变部署计划;仅仅把验证变轻,并不会自动创造那条发布路径。

相对“一种变更等级决定所有动作”的方案,这里的进步很具体:没有拿检查通过的证据,替代产物已经生效的证据。

5. 不部署,也必须完成状态记账⌗

热层和文档不需要新 release,但不能永远停在“等待部署”。

新的部署逻辑会为这些落地记录写入 deploy-skipped,给出无需部署的理由,然后标记为 running;它不生成一份假部署,也不填写一个并不存在的新 release 树标识。

因此,running 的含义被明确扩展:系统通过 release 或源码树热层使用该变更。它不再等价于“进程版本号已经包含这次提交”。

对文档而言,这更接近“无需部署的落地已经闭环”,不能进一步解释成某个模型已经读过、理解并采用了它。

测试把这个区别落到了状态上:纯热层批次结束后,没有新部署历史,没有重启,也没有进入试用期;但每条落地记录都完成状态推进。一个已经持久化为等待态的纯热层部署,也能在恢复处理时按新规则结束,而不再请求重启。

这让“跳过”成为有理由、有记录的完成方式,而不是流水线中的失踪。

部署分类还有一层与验证不同的取数逻辑:它汇总每条落地自己的改动,再加上从批次起点到实际部署目标的差异。

这样,批次中前一次核心修改即使被后一次撤回,也不会仅凭最终净差异就被认作纯热层;部署目标中不在当前批次内的额外改动,也会参与判断。这是部署范围的计算,不只是重复调用一个分类函数。

6. 测试证明了什么,没有证明什么⌗

本次阅读的证据来自该落地提交的四个测试文件;以下是测试断言,不是本文重新执行所得的运行报告。

测试层关键断言
分类器未知路径升到第三级;混合路径取严格级别;移动两端都参加分类
落地通道整个合并批次决定级别;从核心移到热层仍保留核心验证
生产验证一级不执行检查命令;二级只构建;构建失败阻止通过;三级选择测试并保留类型检查
部署器纯热层跳过部署并闭环;热层与核心混合批次一起部署;UI 仍走 release;等待中的热层部署可直接结束

其中,生产验证测试用替代的测试运行器、类型检查函数和构建可执行文件,检查调用选择、次数及失败传播。这能证明分流机制,不能证明真实前端一定正确,也不能替代完整生产部署的验收。

还有两个不该省略的代价。

第一,路径分类是仓库架构契约,不是风险检测器。 skills/ 下的工具代码也属于第一级,而不只是说明文档。它可能写错、改变副作用,甚至影响很多调用;“可热加载”不等于“不会出错”。这里选择降低落地验证成本,不能改写成风险已经消失。

第二,分级不是把所有检查做并集。 当前实现中,核心与 UI 混合变更进入第三级,不再单独执行第二级的 Vite 构建。生产验证测试明确断言了这一行为。因此,准确表述是“混合变更采用第三级流程”,不是“自动包含所有较低级别的检查”。

这两条是当前策略的边界。是否接受,取决于仓库的实际加载机制与验证要求;不能靠把级别数字写大来消除取舍。

本次材料没有提供改动前后的耗时对照,所以也不该声称节省了多少分钟或多少比例。能够从代码直接确认的收益是:某些路径不再触发相应检查命令,纯热层批次不再触发 release 部署。

7. 真正该统一的是解释,不是动作⌗

自修改系统需要约束,但约束不一定表现为“每次都做最多的事”。

这次改动留下的可复用思路是:

  • 用实际合并差异,而不是任务标题,决定验证范围。
  • 用运行时的产物来源,决定是否需要部署。
  • 对跳过的动作同样记录理由,并完成状态推进。
  • 承认路径策略、静态依赖选择与真实行为之间仍有边界。

全量测试不是每次变更都必须支付的固定税,热加载也不是免检证明。少做一步,应该因为那一步与当前变更不相干;保留一步,应该因为没有它,新产物就到不了正在运行的系统。

流水线可以不整齐,因果关系必须整齐。


源码依据:落地提交 bed9c88f 的 src/landing/verification-plan.ts、lane.ts、production.ts、deploy.ts,以及同一版本的 src/testing/affected-tests.ts。测试依据:src/landing/verification-plan.test.ts、lane.test.ts、production-verify.test.ts 与 deploy.test.ts。