diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 8fdd422981..ff387477e7 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -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 diff --git a/skills/objectstack-pm-dispatch/SKILL.md b/skills/objectstack-pm-dispatch/SKILL.md index c6a7fc2949..62f6c3dd8d 100644 --- a/skills/objectstack-pm-dispatch/SKILL.md +++ b/skills/objectstack-pm-dispatch/SKILL.md @@ -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