06 · 架构防线
所有权、注册与单一事实源
关键文件
docs/defensive-patterns/README.mddocs/defensive-patterns/general.mdpackages/AGENTS.mddocs/architecture.md.agents/skills/cordis-styling-nitpicking/docs/defensive-patterns/ 里每条规则都写明了必须成立的条件、评审者据以判断的标准、适用边界和谁能批准例外,并附正反两个例子。README 总结了贯穿所有规则的五个习惯:
| 习惯 | 含义 |
|---|---|
| 正确性写进结构 | 用具名调用点、显式生命周期、显式条件和类型保证正确,不靠时序事后补救 |
| 单一事实源 | 一份数据只有一条读取路径,一个事实只有一个家,共享状态只有一个 owner |
| 通过注册扩展 | 新能力自己注册;已有的承载层不再接收新逻辑 |
| 改动面等于评审面 | 改动只包含行为变化需要的行;迁移是移动代码而不是重写;性能敏感的改动附测量数据 |
| 要求证据 | 断言必须可证伪、可重算:前后实测,或由门禁检查,不靠手工同步 |
packages/AGENTS.md 里的设计规则
- 要有当前的 owner 和需求:每个抽象、状态机、选项、防御性拷贝和兼容路径都对应一个当前契约或生产消费者。
- 公共选择要有证据:"可以配置"不能成为不受支持的默认值或公共操作集的理由。没有当前消费者的证据或先例,就要求显式值,或推迟决定。
- 服务契约为所有消费者设计:反过来,一个只有一个内部调用方的公共服务方法属于不必要的 API 扩张,应改为构造时传入的私有闭包。
- 共享状态要有 owner:挂在服务或实例上。模块级缓存、单例、全局
WeakMap、Symbol.for/globalThis都会隐藏 owner,不再新增全局的current/active/selection。 - 在做决定的操作里强制执行:schema 省略、prompt 过滤、外观层、包装器和监听器顺序都可能被直接调用者绕过,不算强制执行。拒绝路径要通过执行器测试。
- 只在提交点发布状态:操作成功后才发通知、更新派生状态;缓存、prompt、UI 回显、回放和查询视图都从同一个权威源派生。
- 边界作用于完整结果:字节、token、条数、时间限制在完整的输出值(含包装和元数据)已知的位置执行,测试极小值、恰好等于上限、超大单块和多字节文本。
- 一个异步操作只有一个生命周期控制器:就绪、取消、销毁、预留各自需要独立的 owner;否则合并到一个控制器。
Cordis 插件的三条 P0
插件框架里最容易出错的是生命周期。根 AGENTS.md 要求所有注册都经过 ctx.effect() / ctx.on(),register() 返回销毁函数;waterfall 监听器必须调用 next()。cordis-styling-nitpicking 在评审时检查三类 P0 问题:单例状态、没有撤销的副作用、清理依赖先于消费者完成就被释放。每个注册表贡献都要有"销毁后条目被移除"的测试。
其他 general.md 规则(节选)
- 时序不承载正确性:不靠
queueMicrotask、setTimeout(…, 0)或 React effect 的时机保证正确,因为这类代码轻负载下成立、重负载下静默失败。 - 正交结果分别报告:进程可能既超时又以 0 退出。
timedOut、signal、exitCode各自独立暴露。 - 迁移保持行为不变:移动代码优于重写;迁移中发现的缺陷单独修。
- 销毁要等到静止:kill 或 abort 之后要等子进程真正退出。
- 改动附性能证据:同一环境、同一输入下,测运行时间、峰值内存、稳态内存。"整体没变慢"不算证据。