pstack 拆解 28 · 构建:加行为、改结构、修缺陷
构建里最常见的三类任务,各有一条纪律,但都收在同一个问题上:你对自己在做什么有多清醒。
feature
你拥有设计,委派实现。设计和实现是两种不同的工作:设计错了,越早发现越便宜;实现是体力活,可以安全地交出去。把它们分开,既能让设计错在写大量代码之前被抓住,也让两份工作各自有清晰的审查标准。
feature 做新行为或改过的行为,从一个命名好的数据形状建起。
它的步骤是固定顺序。
- 先跑
how,摸清受影响的子系统。 - 跑
architect,做并行设计探索。 - 写 throughput checkpoint,把它铺成四个 todo:
- 阻塞性第一步。 哪些关卡必须在 fan-out 之前跑。
- 独立工作流。 哪些是互不相干的文件、服务或层,可以并行;共享写要串行。
- 共享可变状态。 默认拆开写目标;只有真实不变式才串行化。
- 最小安全分解。 如果一个 worker 最好,说明为什么。
- 把写代码的任务委派给一个 subagent,用配置里的 feature 模型,给一个具体范围:文件路径、命名好的数据形状和它的组织方式、成功标准。当实现有多个合法形状时(错误处理、抽象层、测试结构),改用
arena委派,让多个 runner 把备选摆出来,让 cross-judge 守住挑选。这一步是强制的,没有"跳过并说明"的出口。一个被禁止再派生 subagent 的 subagent,自己直接拥有这个 diff,但保持同样的审查分离。也不接受"我在等着内层 agent"这种回复。还有几条细节:注释按 Comments 的规矩,外科手术式地编辑,对从上游继承来的文件重新 grounding,把共享原语的改进移植到所有消费者并逐个验证。 - 在匹配的 surface 上验证。
inconclusive或错的 surface 不算通过,要标出来。 - rebase 成小而有顺序的 commit,后续栈起来。
- 设计有争议的话,发布前先
interrogate。 - 走 opening-a-pr。
它有一条关于 fan-out 的结构判断:代码耦合的工作(一个功能、一次迁移)交给一个 owner,把 checkpoint 内联,那个 owner 在阻塞阶段之后再内部 fan out。父层的 fan out 留给那些产出独立产物的切片:审计、跨子系统的调查、互相竞争的实验。
它的回复要点名:你建了什么、你选了什么以及为什么、throughput checkpoint、还开着的决定,设计备选用表格。
refactoring
refactoring 是行为不变的结构改动。它有一句总纲:你拥有契约,结构会变,行为不会。
它和 feature 的区别是加行为,和 bug-fix 的区别是修行为。如果清理过程中暴露出一个缺失功能或一个真的 bug,拆出去,先把这次结构改完、发出去。一次重画是允许的,但要命名它、路由到 feature。大的、跨切面的结构工作,属于 figure-it-out。
它的步骤。
- 先钉住行为契约。 跑
how学清楚现在的契约,然后写一个 characterization test、一个 snapshot,或者一个等价 harness,在任何结构移动之前把当前行为捕获下来。如果这块区域没有覆盖,先写 pin 再碰结构。它明确说:类型检查和 lint 不算 pin。 - 命名代码缺失的那个结构。如果当前形状已经清楚、局部、不太会长大,无聊的代码留下。重塑造必须删掉分支或非法状态,不能只增加间接。
- 命名目标形状:如果今天建,模块布局、类型、调用图应该是什么样。跨函数边界时先跑
architect。 - 先减后加:删死代码、折叠单调用者包装、丢掉多余校验、清掉孤儿引用,再引入新形状。最小的、能到达目标形状的改动才发。一个"可能有用"的推测性清理,回退。
- 小步移动,每一步保持 pin 绿。API 重塑的同一波里,迁移所有调用方、删掉旧 API,不要兼容垫片,不要新旧并存的路径。它还点了一个容易漏的地方:重命名会静默地在字符串、prose、反向引用里漏掉用法,所以每一次重命名都要对着真实文件抽查。
- 在真实产物上证明行为没变,不是"能编译"。大一些的重塑跑一次等价检查:一个 diff 新旧输出的脚本,一个把录下来的基线在新代码上重放,或者一次在匹配 surface 上的冒烟运行。
- 确认这次改动值得保留。成功的标准是 reader load 降低了。如果这次 diff 没有在某处降低 reader load,回退它。
- rebase 成有序 commit:一个减法的 commit,然后重塑,然后后续清理。
bug-fix
用科学方法修 bug。修 bug 最容易犯的错是凭直觉改一个"看起来像"的地方,然后因为现象偶尔不复现而以为好了。所以它要求每一行上线的代码都能追溯到运行时证据,"可能有用"的双保险不算,因为那是一个假设,不是修复。
bug-fix 的原则说得最直白:你是这个任务的负责人。计划、审查、验证。
它要求你用科学方法。每一行上线的代码都能追溯到运行时证据。"可能有用"的双保险,只是一个假设,它不发。证据推翻了假设,就把促使它的那个改动一起回退。最小的、被证据支持的那个改动才上线。
它的步骤。
- 自己复现。 在匹配的 surface 上,通过对应的 control skill,即使某个调试协议让你去请用户复现。只有在你能说出一个具体理由、说明 control surface 够不着目标,而且你已经驱动到它能到的极限之后,才去问用户。如果它不直接复现,就合成触发条件、收紧条件、或者插桩直到它触发。
- 二分查找原因。 形成候选假设,一个个排除,直到只剩一个。种子来自
how(受影响的子系统)和why(回归史)。每一轮取那个能砍掉最多剩余问题空间的切分,拿运行时证据,排除掉。程序状态不清楚时就插桩,读代码运行时发生的实况。不要猜。 一个又长又顽固的排查用/loop驱动。在进入第 3 步之前,用运行时证据确认存活下来的机制。 - 规划修复。跨函数边界先
architect。把实现委派给一个 subagent,用配置里的 bug-fix 模型,范围具体。 - 在同一个 surface 上验证。 原始 repro 现在通过。
inconclusive或者错的 surface 不算通过,要标出来。它还补了一句:单元测试显示的是分支行为,不是 bug 消失。 - 让失败的 repro 先于修复进 git 历史。 当 bug 有廉价本地测试路径时,用
tdd的失败测试优先节奏;如果测试昂贵、集成重、或不清楚,跳过它。 - 走 opening-a-pr。
它的回复要求:哪里坏了、根因、修复、你怎么验证的,并且逐字粘贴"先失败、后通过"的 repro 输出。
参考
原文都在 pstack 上游仓库(cursor/plugins):