Skip to content

pm-dispatch skill 缺六条实测规程:阻塞解除后的重新定价、带前提的裁决、收益是否穿过下游边界、多面组件的测试落点、跨仓 pin 滞后、死代码删除的复核 #5513

Description

@os-zhuang

2026-08-05 用 /pm-dispatch 跑完一整条 filter 缺陷链(objectstack #5363 / #5366 / #5368 / #5375 / #5431 / #5445,cloud #1117)后回看,有六处在这一轮真实咬过人或真实救过场的规程,.claude/skills/pm-dispatch/SKILL.md(908 行)里没有对应条目。逐条 grep 核过现有文本,不是重复条目 —— 最接近的 Stale-premise check(:426)覆盖的是「issue 放久了与 main 脱节」,与下面第 1 条是不同的东西。

unassigned,只是记录。


1. 阻塞解除后要给延后的 issue 重新定价(step 3,:508 附近)

现有文本管到哪

### 3. Select the batch 的「Same-file issues serialize strictly across rounds — and deferring is not shelving」已经要求:延后一条 issue 时,把在飞那单学到的坑当轮记到被延后的 issue 上(#4820/#4821 的教训)。

缺的是后半段

前一单合入之后,后一单的成本模型会变,而且方向不止一个。本轮四种结果各出现过:

方向 实例
变便宜 #5375(#5345)去掉了「cube 风格数组也可作为输入」这条腿,{member, operator, values} 三元组自此纯属私有中间表示 —— #5373 的 B 路线因此从 issue 正文写的「工作量最大」变成不跨 spec 的内部改动
没变 #5431(#5373)对 #5374:dev 明确回报「没有让它变简单,也没有顺带修好它」—— 调用点现在收到真值而非字符串化的值,但「{$not: 'x'} 约束不了任何东西」在算子层,与比较数编码正交
issue 正文的成本估计过期 同上,#5373 正文的「我倾向 B(工作量最大)」在派发时已不成立

默认假设(「前一单大概让它变简单了」)本轮错了两次、对了一次。 而且两个方向的代价不对称:误以为变简单 → dev 按缩小的范围做,漏修;误以为没变 → 走了一条已经没必要的贵路线。

建议

step 3 那段之后补一条:阻塞解除、准备派发被延后的那一单时,重新读一遍它的选项与成本估计,并把下面这句写进派发令作为必答项:

你的改动是否让 #X 变简单、变难、变得不必要,或完全无影响?明确回答,不要假设。

本轮正是这个必答项的否定回答,直接决定了 #5374 不能缩范围(见 #5445 的「范围之外」段)。


2. 带前提的裁决 —— step 8 目前只有两档(:785)

现有文本管到哪

### 8. Escalate uncertainties to the maintainer 给了一条清晰的升级门槛(「明显的问题直接修」),并把结果二分成:PM 自己裁(多数)或 升级给维护者(仅公开契约/产品语义分歧,或破坏性动作)。

缺的第三档

#5373 给了 A/B/C 三条路并在正文写「这是本面数据表示的公开形状,请 PM/维护者裁」。按现有门槛读,它像是要升级的那一类。

实际做法是第三档:裁了 B,但把裁决挂在一个可证伪的前提上 —— 「这个三元组现在是纯内部表示」—— 并在派发令里要求 dev 先验证前提再动手,且明确写「前提不成立就报 fork,不许硬做,也不许悄悄退回 A 或 C」。

dev 用五项检查验证了它,其中一条是反向证据:spec/src/api/analytics.test.tsruntime/src/http-dispatcher.test.ts 各有一条测试断言 cube 风格的 filters 数组会被拒收 —— 直接证明该三元组没有线上格式。最终 PR 未触碰一个 spec 字节。

为什么值得单列

它让 PM 在信息不全时能做决定而不靠猜:决定自带检验。既不是「自己拍了算」(那要赌前提),也不是「升级」(那要占用维护者时间去回答一个可以被代码回答的问题)。

建议

在 step 8 的升级门槛之后补一小节,大意:当分歧的关键是一个可以被代码证伪的事实(而不是产品口味或契约取向)时,裁决 + 前提验证要求 + 「前提不成立就报 fork」的显式禁令,是比升级更合适的动作。三件缺一不可 —— 尤其是最后那条禁令,否则 dev 会在前提不成立时自行改选,而那正是无人裁决的状态。


3. review 清单缺一条:用户可见的收益,真的到得了用户吗(step 7,:692)

现象

本轮整条链的价值主张是「拒收要说清楚作者错在哪」。#5423 揭示:packages/rest/src/rest-server.ts 有两处 4xx 直通(mapDataError :395 与 sendError :604),对 message.length >= 500 的错误整条替换"Request failed" —— 不是截断。

实测 driver-sql$null 拒收(#5347/#5368 刚写的那条)运行时 606 字符,越线。REST 客户端拿到的是:

{ "code": "INVALID_FILTER", "error": "Request failed" }

即:code 到了,正文一个字也没到。而写那条 message 的唯一目的就是告诉作者错在哪。

反直觉的一层:不带 status 时原文完整直通,带上 status: 400 反而被这道闸门吞掉 —— 而 status: 400 正是 #4436 为了进 ADR-0112 信封特意加的。sql-driver.ts:456 的注释把这个因果讲反了。

为什么是规程缺口

step 7 的清单检查了:PR 存在且是 draft、范围、changeset、测试证据是真实命令输出、diff 是否满足验收标准、dev 是否验证了前提、+0/-0 的 NUL 陷阱。唯独没有一条问「这个收益穿过它必经的那道边界之后还在吗」。

四个 PR 合入、没有任何人查过这件事,直到一个 dev 在做别的单时顺手撞上。

建议

step 7 清单加一条:当一批工作的价值是通过某个边界交付的(HTTP 错误信封、序列化、日志汇聚、跨进程传输),至少端到端验一次收益在边界之后仍然存在。不必每单都做 —— 判据是「这批工作的价值主张是否依赖某个下游组件如实转发」。


4. 多面组件:测试必须落在共享一致性表,不许独立文件(step 5 派发令,:576)

证据

driver-memory 有三个过滤面(find 的 live path、参考匹配器、analytics/cube 面)。让这些缺陷活下来的不是难度,是没有一条断言在问 —— #5345 之前,共享一致性表 FILTER_LOGIC_CASES 只盯住其中两个。

本轮三次派发都写了这条要求,三次都兑现,并且形成了三条正交的轴,共用同一条不变量:

PR
#5375(#5345) 过滤器形状(组合子、算子词表)
#5431(#5373) 比较数类型(布尔 / null / 数字样字符串)
#5445(#5374) 算子(每个声明的算子编译出的谓词是否真的排除行)

不变量:find() 给出相同的行集,或者以 INVALID_FILTER 拒收 —— 不允许有第三种、更安静的答案。

第三条轴还带了「declared = enforced」的另一半:ANALYTICS_FILTER_CAPABILITIES 声明的每一个算子都被驱动着走两条路并必须一致,且探针必须至少排除一行(否则「一致」什么也证明不了 —— 那正是 #5374 的形状)。

建议

派发令加一条标准条款,原话可直接用:

测试放在未来的分叉会被抓住的地方,不是放在一个独立测试文件里。若本组件对同一契约有多个实现面,新用例进共享一致性覆盖。

适用判据:该组件对同一个契约有 ≥2 个实现面。


5. 跨仓:pin 滞后要在派发令里点明,不许当 rider 顺手 bump(Multi-repo 段,:191)

现象

cloud#1116 的裁决来自 framework 的 #5347,落地于 framework 的 #5368(9c5abf4e9)。但 cloud 的 .objectstack-sha586d6f701a16,9c5abf4e9 不是它的祖先 —— framework main 领先 pin 87 个 commit

所以 cloud#1117 合入后到下一次 pin bump 之前,同一个 TursoDriver 仍有短暂分叉,只是方向反了:remote 抛 400,local(继承 SqlDriver)仍编译 IS NULL。这是 fail-closed 的一侧先到,不是新的洞,pin 前移后自动收敛。

dev 没有动 pin,并把这件事写进 PR 正文留档 —— 这是对的:.objectstack-sha 是共享文件,且要走 scripts/bump-objectstack.sh(连带 hono override 与 lockfile 重生),塞进这一单会把一个独立的、会冲突的改动变成 rider。

建议

Multi-repo 段补一条:当一单的裁决来自另一个仓已合入的 PR 时,派发前核 pin 是否已覆盖那个 commit(git log --oneline <pin_sha>..<target_sha> 或直接判祖先关系)。未覆盖则要求 dev 在 PR 正文留档说明分叉窗口与方向,不要把 pin bump 塞进这一单。

顺带值得单独想清楚的(不在本单范围,若认为值得可另立):跨仓一致性目前完全靠 pin bump 的节奏兜着,没有任何闸门在量这个滞后。 本轮它恰好是安全方向,不保证下次也是。


6. 声明为死代码的删除,PM 要自己 grep 一次(step 7 清单,:692)

现象

#5445 删除了 memory-analytics.ts 里两条被判定为死代码的映射:'inDateRange': '$gte'(注释自称 "Will need special handling" 但无任何调用点实现)与 'notSet': '$exists'(方向还是反的)。

放行前自己核了一次:

git grep -n "inDateRange\|'notSet'" origin/main -- 'packages/**/*.ts'

结果确认 driver-memory 内只有被删的那两行引用,其余命中全部落在 service-analytics —— 另一个包、另一套 strategy,不受影响。dev 的判断成立。

为什么值得单列

step 7 的清单是围绕「改动是否正确」构造的,删除是另一回事:它比修改难回滚,而且「这是死代码」是一个断言,不是一个可从 diff 读出的事实。dev 给了推理(无条目降级到该名、timeDimensions 走 Stage 2、两个出口都只消费 normalizeFilters 的输出),但推理是可以错的,而 grep 只花十秒。

注意与 Operational notes 6 的关系:那条讲的是退役核验要用带引号的精确名 + 查声明式而非查提及。本条是它在 review 侧的对应动作 —— notes 6 说「怎么查才不会假阴性」,本条说「什么时候必须查」。

建议

step 7 清单加一条:diff 里有以「死代码 / 不可达」为由的删除时,PM 在 origin/main 上独立核一次引用面(按 Operational notes 6 的方法),再决定 ACCEPT。


未验证的部分

  • 六条都出自同一轮(2026-08-05 的 filter 链),样本集中在「一个组件的多个实现面逐层收口」这一类工作上。第 1、4 条可能对形态很不同的批次(如纯 UI、纯文档)不那么适用,写入时值得斟酌措辞的普适性。
  • 没有回溯统计前几轮(08-03 / 08-04)是否也命中过这六条中的任何一条 —— 若命中过,证据会更硬,值得实施时顺带查一下 issue 时间线。
  • 第 3 条的边界检查该做到多细(每单一次?每批一次?只在价值主张依赖下游转发时?)没有定论,建议里给的是最后一种,但没有实测支撑哪一种成本收益最好。

关联

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions