Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .claude/skills/pm-dispatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,56 @@ prompt:

一句话:**一个在缺信封的实现上无法转红的拒收用例,读起来是覆盖,实际不是。**

**过滤 / 谓词语义裁决:派发令枚举完整的编译面清单,PR 逐面申报 —— 派发令的标准
条款(#5930 裁决的流程半边)。** 适用判据:本单会**改变一条过滤 / 谓词语义**(算子
的 NULL 处理、组合子恒等、比较数形状、算子词表……)。满足时派发令**把下面那张表逐面
抄进去**,并带这一句(原话):

> 本单改的这条语义由**多个互相独立的编译器 / 求值器**各自实现。派发令列出的**每一
> 面**都必须在你的 PR 正文里有一个结论:**已改** / **本就合规**(给出证据)/
> **明确不在范围**(给出理由)。⛔ 不许静默略过 —— 评审把「没提到的面」一律读作
> 「漏掉的面」,不读作「不需要改」。

**这条防的不是「做错」,是「做对了一部分然后以为做完了」。** 一个 `FilterCondition`
语义由 **5 个互相独立的实现**承载(下表)⇒ 每条语义裁决的成本 ×5,而漏面**反复
复发**,三次都留在代码注释里:

- **#5146 → #5903**:裁决只落到面 1,面 2 是**不继承面 1 的独立编译器**,于是同一个
驱动的两种连接模式对同一条过滤给出两种答案。现场记录在
`driver-turso/src/remote-transport.ts:1731`:「LOCAL mode inherits that fix
(`TursoDriver extends SqlDriver`), this independent compiler inherited none of
it」。
- **#5326 / #5335**:面 3 与面 4 各**又花一圈**才对齐,记录在
`spec/src/data/filter.zod.ts:370`。
- **#5905**:#5298 的裁决由 PR #5962 落到 driver-sql / formula / service-analytics
与 conformance 表,**唯独漏了 HAVING 面** —— `objectql/src/having-filter.ts:37` 的
原话是「was not in that PR's inventory, which left this file as the lone
holdout」。**「inventory」这个词本身就是本条款的缺席证明**:那次派发确实有一份清
单,只是它不完整,而没有任何机制要求它完整。

三次都不是难度问题,是**没有一份清单在问「还有几面」**。

编译面清单(逐面实测 @ `main` `48f98b0`,2026-08-07):

| # | 面 | 落点(file:line) | 备注 |
| --- | --- | --- | --- |
| 1 | `driver-sql` | `packages/drivers/driver-sql/src/sql-driver.ts:7083`(`applyFilterCondition`) | `driver-sqlite-wasm`(`sqlite-wasm-driver.ts:67`)与 **local 模式**的 `driver-turso`(`turso-driver.ts:174`)都 `extends SqlDriver`,**靠继承共用这一面**,不单独算面 |
| 2 | turso RemoteTransport | `packages/drivers/driver-turso/src/remote-transport.ts:1526`(`private buildWhereSQL`) | **独立编译器,不继承面 1** —— 一个驱动的两面,由连接模式选中哪面 |
| 3 | service-analytics read-scope-sql | `packages/services/service-analytics/src/read-scope-sql.ts:259`(`compileScopedFilterToSql`) | RLS 读侧 |
| 4 | service-analytics filter-normalizer | `packages/services/service-analytics/src/strategies/filter-normalizer.ts:1235`(`lowerAnalyticsWhere`) | analytics / cube 侧 |
| 5 | `formula` | `packages/formula/src/matches-filter.ts:73`(`matchesFilterCondition`) | RLS 写侧 `check` 与公式求值;JS 两值语义的基准面 |
| 半面 | objectql `having-filter` | `packages/objectql/src/having-filter.ts:92` / `:98`(`applyHaving` / `matchesHaving`) | 聚合**后**过滤。算半面是因为词表是子集,**但申报义务不打折** —— 它是**唯一没有 conformance 表覆盖的面**(`FILTER_LOGIC_CASES` 不驱动 HAVING 路径),所以漏了它连门禁都不会红 |
| 冻结 | `driver-memory` / `driver-mongodb` | — | #5499 冻结投入:**pin-annotate,不翻转**。冻结面仍要申报,结论是「不在范围 + #5499」。现场注释见 `read-scope-sql.ts:176`、`having-filter.ts:41` |

**这张表本身由 PR 维护 —— 与域表同一纪律。** 增删一面(新驱动、新求值器、某面被合并
或退役、冻结状态变化)的那个 PR 顺手改这里,不留给下一次裁决重新数。清单**会**过期是
必然的,清单**没有维护者**才是缺陷。

⚠️ 派发前复核一遍再抄,⛔ 不要凭这张表的记忆填派发令:本仓的包路径搬过家(驱动进
`packages/drivers/`、服务进 `packages/services/`),行号更是每天在动。一条够用的复核
串:`grep -rn 'matchesFilterCondition\|buildWhereSQL\|compileScopedFilterToSql'
packages --include=*.ts | grep -v node_modules`。

**Issue 正文是线索,不是规格 —— and the dispatch wording is what makes an
honest "the premise is dead" cheap to return.** Step 1's stale-premise check
is the PM's sample; the dev's verification is the real thing, so the prompt
Expand Down
37 changes: 37 additions & 0 deletions skills/objectstack-pm-dispatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,43 @@ Non-negotiables for this dispatch:
Return ONLY the JSON report defined in the operating procedure.
```

#### One semantics, N independent implementations — enumerate them in the prompt

**Applicability:** the issue changes a *semantic rule* — how an operator, a
predicate, a comparison, or an absent/empty value is interpreted — and that rule
is implemented **more than once**, by compilers or evaluators that share no code.
Query filters, expression languages, permission predicates and serialization
formats all tend to grow this shape as a project adds backends.

When it applies, the dispatch prompt carries an **explicit inventory of every
implementing surface**, and requires the agent to give a verdict **for each one**
in its PR body: **changed** / **already conformant** (with evidence) /
**explicitly out of scope** (with a reason). A surface the PR never mentions is
reviewed as one that was *missed*, not as one that needed no change.

Why this is worth a standing clause instead of case-by-case judgment: the failure
it prevents is not "implemented it wrong", it is **"implemented part of it and
believed the work was finished"**. That failure is invisible at review time — the
diff is correct and the tests are green, while the untouched surfaces keep
answering the old way until a user hits the divergence. The cost scales with the
count: a semantics carried by N implementations makes every ruling an N-part
task, and the parts that get skipped are exactly the ones nobody wrote down.

Two disciplines keep the inventory trustworthy:

- **The inventory is maintained by PR.** Whichever change adds, retires or merges
an implementing surface updates the list in that same PR. An inventory going
stale is inevitable; an inventory with **no owner** is the defect.
- **Re-verify before pasting.** Paths move and surfaces get added between
rulings, so re-derive the list from the code at dispatch time rather than
copying the previous prompt. An inventory that was right last month and is
pasted unchecked reintroduces the very miss it exists to prevent.

Surfaces that are **deliberately frozen** (deprecated backends, formats kept only
for compatibility) stay in the inventory. Their verdict is "out of scope —
frozen", recorded rather than silently absent: a reader cannot otherwise tell a
frozen surface from a forgotten one.

#### Dispatch backends

**`mode:subagent` (default).** Sub-agents inside the PM's own session. Reports
Expand Down
Loading