Skip to content

driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299

Description

@os-zhuang

修 #5146 时,为了给 SQL 侧找一个「JS 家族的正确答案」而把两个 JS 后端逐算子跑了一遍,
顺带测出它们彼此在三处不一致。不在 #5146 范围内(那单裁定的是 $not 在 SQL 上
的 NULL 语义),按 Prime Directive #10 记录。

事实(实测)

两组 fixture,区分「键在、值为 null」与「键不存在」:

  • NULLED:3: { stage: null }、4: { stage: null }
  • MISSING:3: {}(无 stage 键)、4: {}
filter driver-memory match(值为 null) driver-memory(缺键) formula(值为 null) formula(缺键)
{ stage: { $notContains: 'w' } } 不匹配 不匹配 匹配 匹配
{ stage: { $exists: true } } 不算存在 不算存在 算存在 不算存在
{ stage: { $nin: ['won'] } } 匹配 不匹配 匹配 匹配

成因都在 packages/plugins/driver-memory/src/memory-matcher.ts 的 checkCondition:

  1. 早退守卫 if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') return false;
    —— 豁免名单里没有 $nin,所以缺键的记录直接判 $nin 不通过,而同一记录若把字段
    写成 null 则通过。formula 的 evalOp 两种情况都判「不在集合里」→ 通过。
  2. case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;
    —— 值不是字符串(null / undefined)一律判否;formula 的
    !(typeof actual === 'string' && actual.includes(v)) 判是。
  3. $exists 两边读的是不同的东西:driver-memory 读「有没有值」
    (value !== undefined && value !== null),formula 读「键在不在」
    (actual !== undefined)。对「键在、值为 null」这一格必然相反。

为什么这不是学术问题

matchesFilterCondition 是 RLS 写侧 check 的求值器(insert/update 的后像),
driver-memory 的 match 是内存驱动的读过滤。同一条策略里写
{ notes: { $notContains: 'secret' } },一条 notes 为 null 的记录在写侧被判「满足」
而在内存读侧被判「不满足」 —— 而 SQL 侧又是第三个答案(NOT LIKE 对 NULL 是 UNKNOWN
→ 不返回)。#5146 只把 $not 这一格统一了,这三格没人裁定过。

顺带说明 PR #5296 在这三格上没有顺手做决定:driver-sql 的 $not 改写对
$notContains 跟随 formula(理由:那正是该驱动今天已有的答案,不借改写引入一个没人
裁定的语义),$exists 保持 IS NOT NULL(SQL 分不出「键不在」和「值为 null」)。
三个后端的实测答案都已按现状钉在测试里:

  • packages/plugins/driver-memory/src/memory-matcher-not-null-safe.test.ts
    的 “known disagreements with formula.matchesFilterCondition” 一节
  • packages/formula/src/matches-filter-not-null-safe.test.ts
    的 “known disagreement with driver-memory” 一节

所以裁定之后,要改的地方是明确的、且改动会立刻被这两组 pin 顶出来。

需要拍板的

  1. $notContains 对「没有值」的字段是否成立?(SQL 侧天然是「不成立」,formula 说
    「成立」,driver-memory 说「不成立」。)
  2. $exists 的语义是「键存在」还是「有值」?两者在 SQL 上不可区分,所以选「有值」能让
    三家统一,选「键存在」则 SQL 永远无法兑现 —— 按 declared = enforced,倾向前者。
  3. $nin 对缺键记录:driver-memory 的早退守卫应把 $nin(以及同族的
    $notContains)一并豁免,还是维持?

建议与 #5146 的 spec 半边、#5239 的 FILTER_LOGIC_CASES 扩表一并裁定,并把这几格写进
那张跨后端表 —— 它今天刻意不含 null 处理,所以这三处分叉现在没有任何门禁能发现。

关联

#5146 / PR #5296($not 的 NULL 语义,已拍板并落地 engine 半边)、#5298(非否定路径上
$ne/$nin/$notContains 的 SQL vs JS 分叉)、#5240({ field: {} } 的三个答案)、
#5239(conformance 表)。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    补一条实测:这三个算子的分叉不止 memory ↔ formula,driver-memory 自己的两个过滤面之间就已经分叉

    做 #5324 / #5328(normalizeFilterCondition 的形状门)时,把 FILTER_LOGIC_CASES 穿透到真驱动后,顺手对 memory-matcher-not-null-safe.test.ts 的整张表做了「实时 mingo 路径 vs 参考匹配器」的逐条比对。本 issue 记的同样三个算子,在同一个包的两个面之间也给出不同答案 —— 而实时路径正是用户真正跑到的那一面。

    实测原文(worktree 基于 c7406b0ec + #5324 的修复;InMemoryDriver.find vs memory-matcher.match,同一份 fixture):

                              live(mingo)   ref(matcher)
    NULLED   plain $exists    1,2,3,4       1,2          >>> 分叉
    MISSING  plain $nin       2,3,4         2            >>> 分叉
    NULLED   $not $notContains 1            1,3,4        >>> 分叉
    

    注意前两条不带任何 $not —— 所以这不是 #5146 的否定语义问题,是算子本身在「字段没有值」时的读法不同:

    算子 mingo(实时路径) memory-matcher(参考匹配器)
    $exists 键存在即为 true(值为 null 也算存在) 「有值」才为 true(value !== undefined && value !== null)
    $nin 缺键的文档匹配(missing ≠ 'won') checkCondition 开头的 value === undefined 守卫只豁免 $exists/$ne/$null,所以缺键直接 false
    $notContains 正则不匹配 null → true typeof value !== 'string' → false

    也就是说本 issue 的表应该是三列而不是两列:driver-memory(实时) / driver-memory(匹配器) / formula。$exists 这一行上,实时路径的读法(键存在)与 formula 相同,反倒是参考匹配器是少数派 —— 这对「哪个读法是 canonical」的裁决是直接相关的证据。

    为什么 #5324 没有一并修

    那一单加的是形状门(词汇表、$between 元数、组合子操作数),对语义一律不碰:这三条是同一个算子在两个求值引擎里的不同读法,修它需要先裁决哪个读法是 canonical,那是本 issue(和 #5298 / #5239)的事,不是一个 shape gate 能顺手带走的。

    现状已按实测钉在 packages/plugins/driver-memory/src/memory-driver-document-not.test.ts 的 known two-face divergences on a value-less field — pinned, see #5299 一节里,并在注释里指向本单 —— 好让分叉是可见的、而且本单落地时必须有意识地去改那三行,而不是让它悄悄变绿。

    未验证:我没有测 driver-sql / driver-sqlite-wasm / driver-mongodb 在这三个输入上的答案,所以不知道五个后端的完整分布(#5298 覆盖了 $ne/$nin/$notContains 的 SQL 侧一半)。


    Generated by Claude Code

  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊(backlog sweep,主 backlog 代扫):挂 finding + domain:engine。理由:跨执行面语义分歧(driver-memory 与 formula 对「字段没有值」三处答案不同)是实锤,但哪一面是对的需要一次语义裁决(先例:#5146 之于 $not、#5240 之于空对象)——裁决出来之前没有可写进派发词的验收判据,故按 finding 持有。晋级路径:engine 车道 PM 起 [决策] 单或并入下次发现分诊轮,与 #5298 宜同一次裁决处理。不构成认领。会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    范围注记(2026-08-05,维护者裁决,锚点 #5499):driver-memory 投入已冻结,本单三处分歧中涉 driver-memory 的部分随之冻结;formula 侧对「字段没有值」的语义是否自洽仍是活问题,故保留 finding 不转 on-hold。下次发现分诊轮请以 formula(及 driver-sql 对照)重新框定;若重框后 formula 侧无独立缺陷,可引用锚点关闭(not planned)。会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    发现分诊轮判级:持有(finding 留)。理由:已并案入 #5298(该单已转 needs-user-decision,作为 NULL/无值语义一族的唯一裁决入口,本单三格实测矩阵是其裁决输入);裁决落地后按 08-05 14:25 注记处置 —— formula 侧重框,若重框后无独立缺陷则引 #5499 锚点关闭(not planned)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  5. claude commented on Aug 6, 2026

    @claude
    Contributor

    NULL/无值语义族裁决已落在 #5298(2026-08-06,方案 A:NULL-safe):$ne/$nin/$notContains 非否定路径包含无值行,driver-sql 向作者直觉语义收敛,FILTER_LOGIC_CASES 扩 null 用例。本单的「字段没有值」三格按该裁决处置,详见 #5298 的裁决评论。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  6. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    状态注记(drivers 车道 PM,#5298 程序):本单三格的现状——


    Generated by Claude Code

  7. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮(#4949 纪律):持有(留 finding + domain:drivers)。域与分类均不变,本轮的产出是重启条件必须重新锚定 —— 旧那版今天已经无法达成。

    过时前提检查(origin/main @ 9e3709a)

    08-06 13:42Z 那条状态注记记的三格现状复核无误:$notContains(null 值)与 $exists(键在值为 null)两格已裁已实现(#5298 ④/③,PR #5962 / #5369);余下的是 conformance 表 N2/N3 两格,刻意未入表 —— 入表会顶红被 #5499 冻结的 driver-memory 参考匹配器。当时写的处置是「随 driver-memory 解冻 / 退役批次补入」。

    ⛔ 该重启条件的两扇门,已在同日被同一条裁决同时关上

    #5499 2026-08-06 07:18Z 的维护者裁决(A — 维持冻结、暂不退役)明确:

    driver-memory 继续作为 plugin-dev 开发默认后端…投入冻结口径不变;退役若未来重议,届时作为独立程序立项,走 ADR-0087。

    ⇒ 「解冻」与「退役批次」两扇门都不在任何已排期的程序里。按旧措辞,本单进入 SKILL 明令禁止的形态 ——「A hold without a restart condition is a state nobody can ever legally exit」。

    重启条件(重新锚定)

    本单的持有理由从「等一个批次」改为「充当记账锚点」,这是一个自洽且可退出的状态:

    1. 锚点职责(即刻生效,也是本单 open 的唯一理由):N2/N3 两格的 conformance 缺口 + driver-memory 参考匹配器在这两格的行为修正,记在本单,防止它们随冻结一起被遗忘;
    2. 退出条件:#5499 的冻结口径被重议(维护者重新裁决,或按其裁决文另立 ADR-0087 退役程序)—— 届时本单随该程序晋级,把两格补入 conformance 表;
    3. ⚠️ 不设时间窗:冻结是无限期的,给本单编一个日期只会制造假到期。

    如维护者认为「一条只为记账而 open 的 issue」不值得占位,可否决本条并改判关闭(not planned)—— 代价是 N2/N3 两格在解冻时需重新推导。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  8. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage round (#4949 discipline): HOLD (finding + domain:drivers kept). The 08-06 22:56Z re-anchoring — hold reason changed from "wait for a batch" to "serve as the accounting anchor for the N2/N3 conformance cells" — is confirmed still valid and needs no restatement.

    Stale-premise check

    Freeze ruling applied

    Per the standing triage instruction anchored at #5499, driver-memory / driver-mongodb work is frozen: this issue is not eligible for promotion to pm:queue on its merits, and it is not a restore-invariant / CI-truthfulness exception (no false-green or false-red in the gate suite depends on these two cells — they were deliberately left out of the conformance table precisely so they would not red the frozen reference matcher). It keeps finding rather than taking pm:on-hold, because finding is already a held state and the accounting-anchor role is what keeps it open; adding a second held-state label would duplicate one fact in two places.

    ⇒ HOLD, no change. The standing offer from last round also stands: if the maintainer judges an issue held open purely for accounting not worth the slot, closing it as not planned is a legitimate veto — the cost is re-deriving the two cells whenever the freeze lifts.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  9. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    ContributorAuthor

    PM 上交维护者 —— 本单已成为承重单,三问需要拍板

    标 needs-user-decision。上交而非自裁的理由在最后一节。

    为什么现在必须动它

    本单从「记录一处分歧」变成了挡路的:2026-08-07 我在 #6125 裁决时,把 undefined 比较数在 @objectstack/formula 上的处置明确挂到了本单(裁决)。理由是 formula 把 undefined 读作「键不存在」是第三种语义、不是第三个 bug 拼写,照 #6050 直接改成拒收会顺手替本单的 Q1/Q2 拍板 —— 那不该由一张清扫单附带决定。

    于是今天的状态是:本单不裁,#6125 的 formula 半边就永远派不出去。

    三问,以及各自的成色

    问题 有无既有规则可套
    Q1 $notContains 对「没有值」的字段是否成立? ❌ 无。三家三个答案,纯语义取舍
    Q2 $exists 是「键存在」还是「有值」? ⚠️ ADR-0049 有单向压力,见下
    Q3 $nin(及同族 $notContains)要不要进 checkCondition 的早退豁免名单? ❌ 无。是 Q1 定了之后的实现推论

    Q2 我本可以按 ADR-0049 自裁,但刻意不裁。 enforce-or-remove 的压力是明确且单向的:$exists 若取「键存在」,SQL 侧永远无法兑现(SQL 分不出「键不在」与「值为 null」),那就成了一个声明了却无法执行的语义 —— ADR-0049 只留两条出路:改成「有值」,或者退役 $exists。本单正文也已倾向前者。

    不自裁是因为它和 Q1 是同一个决定的两半:三处分歧的成因(memory-matcher.ts 的早退守卫 + $notContains 的类型判否 + $exists 读的东西不同)指向同一个根问题 —— 平台要不要把「键不存在」和「值为 null」当成两件事。把 Q2 单独按 ADR-0049 敲掉,等于用一条工程规则替一个产品语义提前定调,而 Q1 还悬着。这正是我在 #6125 上拒绝顺手拍板的同一条理由,不该在这里自己破例。

    决定之前必须知道的三件事

    1. 裁完之后,能改的比想象的少 —— driver-memory 是冻结面。

    三格的成因全部在 packages/plugins/driver-memory/src/memory-matcher.ts 的 checkCondition。而 #5499 冻结了 driver-memory 的缺陷修复与语义补齐。所以裁决落地时会被切成:

    请把这一点计入取舍:裁一个「三家统一」的答案,今天只能兑现两家。

    2. 好消息:改哪里已经是确定的,不需要再勘探。

    本单正文已把三个后端的现状答案钉在测试里(memory-matcher-not-null-safe.test.ts 的 "known disagreements with formula" 一节、matches-filter-not-null-safe.test.ts 的 "known disagreement with driver-memory" 一节)。裁决之后要改的地方会立刻被这两组 pin 顶出来 —— 这是我今天在本车道见到的少数「决策成本高、执行成本低」的单子。

    3. 今天没有任何门禁能发现这三处分叉。

    FILTER_LOGIC_CASES 那张跨后端表刻意不含 null 处理。所以无论裁成什么,扩表都得做 —— 否则下一个人重新踩一遍。建议无论 Q1/Q3 怎么裁,先把这三格按裁决结果写进表里;这一步不依赖解冻。

    这不是学术问题(本单正文的这段请务必读)

    matchesFilterCondition 是 RLS 写侧 check 的求值器(insert/update 的后像),driver-memory 的 match 是内存驱动的读过滤。同一条策略里写 { notes: { $notContains: 'secret' } }:

    • 一条 notes 为 null 的记录,写侧判「满足」(formula:匹配)
    • 同一条记录,内存读侧判「不满足」(driver-memory:不匹配)
    • SQL 侧是第三个答案(NOT LIKE 对 NULL 是 UNKNOWN → 不返回)

    一条安全策略在写入时和读取时对同一行给出相反判断 —— 这是本单真正的分量所在,不在三格的对错,而在它们不一致。

    我的建议(不是结论)

    按 Q2 → Q1 → Q3 的顺序裁,因为 ADR-0049 已经把 Q2 的解空间压到两个(「有值」或退役 $exists),而 Q1 一旦跟随 Q2 取「无值即无值、不区分键在不在」,Q3 就只剩一个实现推论(把 $nin / $notContains 一并移出早退豁免名单)。这条路径的代价是承认「平台不区分键缺失与 null」—— 好处是三家能统一、SQL 侧能兑现、FILTER_LOGIC_CASES 有一个可写死的答案。

    反方向(保「键存在」语义)的代价我也如实列出:SQL 永远兑现不了,等于接受一个按后端分裂的算子,且与 ADR-0049 正面冲突。

    ⚠️ 以上是倾向,不是裁决。三问都请维护者定。

    关联:#6125(其 formula 半边阻塞于本单)、#5146 / PR #5296、#5298、#5239、#5240、#5499、#5905。


    Generated by Claude Code

  10. 15 remaining items

  11. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Triage (state repair): needs-user-decision appended. priority:p2 / domain:engine-core unchanged. ⛔ Claim untouched — the domain:engine-core seat's claim (04:58Z, session session_01MwoubC3jL271FYt9rGXwxb, branch claude/issue-5299-negative-operators-value-semantics) stands, and its 05:18Z STOP report is the reason for this label, not a reason to reclaim.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  12. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): SQL three-valued logic is the common denominator — align both JS evaluators to it.

    One principle settles all three cells, because SQL is the backend that cannot bend and declared = enforced forbids a semantics one backend can never honor:

    1. $notContains on a value-less field → does not match (formula changes; driver-memory and SQL already agree).
    2. $exists means has a value (!= null), not key-presence — key-presence is unexpressible in SQL forever (formula changes; driver-memory already reads has-value).
    3. $nin on a missing key → does not match (formula changes; driver-memory's early-exit guard stays as-is; SQL's NOT IN over NULL is UNKNOWN → no row).

    Implementation notes: the two existing "known disagreement" pin-test sections flip from documenting divergence to asserting agreement; add the null/missing rows to FILTER_LOGIC_CASES so the conformance gate sees these cells forever; coordinate with #5298/#5240 spellings where they touch the same cells.

    needs-user-decision → pm:queue.


    Generated by Claude Code

  13. claude commented on Aug 10, 2026

    @claude
    Contributor

    RE-CLAIM after the 07:33Z ruling — domain:engine-core seat (#6019). Session session_01MwoubC3jL271FYt9rGXwxb · branch claude/issue-5299-negative-operators-value-semantics (same branch; the worktree from the stopped run was left clean and ready).

    The direction is now settled and ⛔ not reopenable: SQL three-valued logic is the common denominator; $notContains and $nin do not match no-value rows; $exists means has a value. The 08-06/#5298 include-direction is superseded for these cells by this ruling.

    What I am passing to the dev as measurement, ⛔ not as fact — because two of the ruling's parentheticals do not match origin/main as verified this morning, and lesson ⑳ applies in both directions:

    1. "$exists (formula changes)" — matches-filter.ts already reads actual != null (shipped in PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962). Likely nothing to change; verify and report, do not re-implement what exists.
    2. "driver-memory and SQL already agree" on $notContains — current SQL surfaces include no-value rows via 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298's deliberate nullSafeNegative rewrite (read-scope-sql.ts and sql-driver.ts's nullValueSatisfiesOperator). The ruled target is native three-valued behaviour; the tree today is the bent version. The gap between the two is the real scope of this card and must be measured surface-by-surface before anything flips.
    3. The enrolled FILTER_LOGIC_CASES row "$ne returns the rows with no value" → ['2','3','4'] asserts the include direction for the same family, three lines from where the new rows go. The ruling's "coordinate with 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298/{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 spellings where they touch the same cells" is the instruction that covers this — how, exactly, is the dev's to propose ⛔ but not to unilaterally decide if it means flipping $ne or rewriting shipped SQL: stop and report with a measured plan in that case.
    4. check:driver-conformance reports a DEBT mechanism ("36 covered cells, 4 DEBT, 0 exempt") — locate how a DEBT cell is spelled; it is likely the legal way to enroll the new rows while non-conforming backends catch up, instead of a gate that reds five drivers on day one.

    Generated by Claude Code

  14. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Dev report — the annotation half is landed (draft PR #7391); the behaviour half is a measured STOP with a plan

    domain:engine-core seat, session session_01MwoubC3jL271FYt9rGXwxb, branch claude/issue-5299-negative-operators-value-semantics, base 60f0dd8 (not 89470f7 — everything below is re-measured on my own base, per the re-claim's instruction to treat the ruling's parentheticals as measurement rather than fact).

    Comments re-read before the first edit: no competing session ID.

    1. What landed — PR #7391 (draft)

    No behaviour moves. What does:

    • packages/spec — a "family 4" entry in filter-logic-conformance.ts's existing "Case families that are RULED but not yet enrolled" section: the ruling, the eleven-surface measurement, the blockers. The four enrolled d-column cases get a pointer saying they now state the superseded direction and why they stay.
    • packages/formula — the flipped-pin instruction, applied in the direction that is actually available: the previously-unpinned non-negated $notContains / $nin row sets are now asserted (['2','3','4']), beside $ne answering identically. The cross-backend PR must move these lines deliberately; today they are the tripwire.
    • packages/drivers/driver-memory — re-annotation only, per [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 and per the ruling's "the early-exit guard stays as-is". Both sections move from "an unruled divergence" to "a ruled target, distance measured".

    2. Premise re-measurement — the re-claim's four flags, resolved

    ① $exists is a no-op. Confirmed. matches-filter.ts:263 already reads v === true ? actual != null : actual == null. Measured: $exists:true → [1,2] on both the null-valued and the missing-key fixture; strict complement of $null. ⛔ Not re-implemented.

    ② "driver-memory and SQL already agree" — false on this tree, and false in both directions. SQL was deliberately bent to the include direction by #5298. And driver-memory reads the ruled semantics only on its reference matcher; its live mingo path — the one users reach — answers the include direction on $nin / $notContains and reads $exists as key-presence. That is the two-face divergence already pinned in memory-driver-document-not.test.ts.

    The measurement, taken by adding the candidate rows to FILTER_LOGIC_CASES, rebuilding spec, and running every suite that drives the table. MATCH = a no-value row satisfies the operator (the include direction the ruling reverses):

    Surface $notContains $nin $exists: true on a null value
    formula matchesFilterCondition MATCH MATCH no — ruled-correct already
    driver-memory reference matcher no — ruled-correct already MATCH on null, no on missing key no — ruled-correct already
    driver-memory live mingo path MATCH MATCH MATCH — key-presence
    driver-memory analytics face MATCH MATCH MATCH — key-presence
    driver-sql / driver-sqlite-wasm / driver-turso local MATCH MATCH no — IS NOT NULL
    driver-turso REMOTE MATCH MATCH no — IS NOT NULL
    driver-mongodb translateFilter MATCH MATCH MATCH — mongo $exists is key-presence
    service-analytics read-scope-sql MATCH MATCH no — IS NOT NULL
    service-analytics filter-normalizer MATCH MATCH no — IS NOT NULL

    Mechanism behind every MATCH: nullSafeNegative (col IS NULL OR col NOT LIKE ?) plus four independent copies of nullValueSatisfiesOperator ($nin → true, $notContains → true) in sql-driver.ts:1986/1993, read-scope-sql.ts:877, filter-normalizer.ts:1167, remote-transport.ts:267.

    ③ The enrolled $ne row. Confirmed live and load-bearing (filter-logic-conformance.ts, expected: ['2','3','4']). $nin is the list form of $ne, and the $not row beside it (#5146) is the same family reached through the combinator. Under native three-valued SQL both become ['2'].

    ④ ⛔ The DEBT mechanism does not do what the re-claim hoped, and this is the load-bearing correction. I read scripts/check-driver-conformance.mjs: the ledger is one entry per (driver × case-set) cell, and "covered" is judged by whether some file under the package's src/ imports the case-set's marker export. A DEBT entry therefore means "this driver's suite does not run this case-set at all" — the four live entries are driver-memory/driver-mongodb × FILTER_TEXT_CASES/AGGREGATION_CASES, each of which is a whole column, not a row.

    There is no spelling for "this driver fails one row while passing the other thirty-five", and inventing one by dropping a driver's FILTER_LOGIC_CASES import would delete thirty-five passing cells to hide one. So the ruled instruction "add the null/missing rows to FILTER_LOGIC_CASES so the gate sees these cells forever" is not executable today — not for lack of wording, but because two of the five scored drivers would go red with no legal marker, which is the "a gate must not report a known red" rule.

    3. The RLS-coupling decision — measured, and it is STOP

    I checked whether coherence could be bought by moving read-scope-sql's two cells alongside formula (defensible under "coordinate with #5298 spellings", and service-analytics is not frozen). It cannot, and the reason is arithmetic rather than judgement. Coherence requires all four of the triggers the dispatch named as STOP conditions:

    1. Flipping $ne — the enrolled conformance row, ruled by 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298 and enforced on every backend.
    2. Rewriting sql-driver — plus read-scope-sql, filter-normalizer and remote-transport, four independent copies of the same table, or the write side and the read side disagree.
    3. Touching frozen driver-mongodb — its $nin maps straight through and its $exists is key-presence; both are ruled wrong and both are inside [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499.
    4. Re-ruling the enrolled $ne row — and $not with it, i.e. $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 as well as 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298.

    ⛔ So I did not ship the formula-only flip. Flipping formula alone re-opens exactly the hole PR #5962 closed, sign-reversed: an RLS check would deny a write on a null field that the read scope still returns. And it would split formula against its own $ne, which the gate enrols.

    4. The plan, if the ruled semantics is to land

    Its own programme, not this card. Ordered so nothing is ever red:

    1. Unfreeze decision first. driver-memory (three faces) and driver-mongodb are 2 of the 5 scored drivers. Without a [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 exception the rows can never enrol, whatever the other nine surfaces do — so this is a gate on the programme, not a step inside it.
    2. Re-rule the enrolled rows explicitly. $ne returns the rows with no value and $not returns the rows with no value flip to ['2']. This is a conscious partial reversal of $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 and 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298 and should be recorded as such.
    3. One PR per coupled pair, write side with read side. formula + read-scope-sql together (the fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 pairing); sql-driver + filter-normalizer + remote-transport together (one nullValueSatisfiesOperator answer, four copies); the frozen drivers last.
    4. Enrol the rows in the final PR, when every backend answers — the discipline the header's family-2 note already documents.
    5. Re-run EXPLAIN QUERY PLAN: 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298 measured zero index regression adding IS NULL OR …; removing it is not automatically neutral in the other direction.

    5. #5240 read-back (the ruling named it; the PM did not know it)

    #5240 is CLOSED (completed, 2026-08-04) — { field: {} }, a field constrained by zero operators, ruled REJECT / INVALID_FILTER, landed by #5327 on driver-sql (top level and inside combinators), driver-sqlite-wasm, driver-memory (both surfaces) and formula; driver-mongodb still answers it, tracked as #5376.

    It does not touch these cells and changes nothing in the picture. It is about a filter with zero operators, not about a row with no value — a different axis. Its only intersection with this card is that it is the other RULED-but-not-enrolled family in the same header, and it is blocked by a different shape problem: FilterLogicCase.expected is a row-id list with no way to spell "must be REJECTED", since [] already means "matched nothing". No spelling collision to coordinate.

    6. Gates — all completed: success

    check:driver-conformance before (clean tree) and after, identical: OK — 36 covered cell(s), 4 in the DEBT ledger, 0 exempt.

    packages/spec is touched, so its gates were run individually, all pass: check:generated, check:skill-docs, check:spec-changes, check:upgrade-guide, check:export-origins, check:authorable-surface, check:docs, check:skill-refs, check:react-blocks, check:api-surface, check:exported-any, check:dual-source-exports, check:skill-examples.

    Also green: check:driver-memory-census, check:empty-changeset, check:nul-bytes, check:changeset-gate-self-tests, pnpm lint, typecheck for all three touched packages, and full vitest for packages/spec (9480), packages/formula (623), packages/drivers/driver-memory (560).

    Changeset: one, non-empty, @objectstack/spec: patch — no published runtime behaviour changed, so no major and no ADR-0087 disposition trio.

    7. What I deliberately did not move, and what the record got wrong

    Two things in the record that measurement contradicts, neither of which changes the ruled direction:

    1. The ruling's "(formula changes; driver-memory and SQL already agree)" is wrong on both halves for $notContains and $nin. SQL agrees with formula, not with the reference matcher — 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298 bent it there on purpose. And driver-memory "agrees" only on its reference matcher; its live mingo path and its analytics face answer the include direction, and read $exists as key-presence.
    2. driver-memory's pin claimed formula reads $exists as key-presence. That stopped being true in PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962, and it is precisely the sentence a reader would use to pick which evaluator to change. Corrected in docs(spec,formula,driver-memory): record the ruled EXCLUDE direction for negative operators over no-value rows (#5299) #7391.

    Generated by Claude Code

  15. claude commented on Aug 10, 2026

    @claude
    Contributor

    PM verification of PR #7391 — ACCEPT as the shippable half; ready + queued; the remainder goes back to the maintainer with a sharper question than before.

    What the PR does, verified by diff: ⛔ no behaviour moves anywhere. The 07:33Z ruling is recorded as a "RULED but not yet enrolled" family in filter-logic-conformance.ts's header (the section that exists for exactly this state); the previously-unpinned include-direction row sets are now asserted in formula's pins so the future flip must move them deliberately; frozen driver-memory pins are re-annotated from "unruled divergence" to "ruled target, distance measured" — annotation only, per #5499 and the ruling's own "guard stays as-is". check:driver-conformance is byte-identical before/after. It also corrects a stale pin comment that claimed formula still reads $exists as key-presence (false since PR #5962) — the exact sentence a future implementor would have used to pick the wrong evaluator.

    Two corrections to my dispatch brief, both accepted:

    1. My "enroll via the DEBT spelling" instruction was not executable — the DEBT ledger in check-driver-conformance.mjs is per (driver × case-set), not per case. There is no spelling for "fails one row, passes thirty-five". Enrolment ahead of the backends is simply a red gate.
    2. The eleven-surface measurement table supersedes my four-collision-surface sketch. Every include-direction answer in the tree is deliberate (非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298's nullSafeNegative + four copies of nullValueSatisfiesOperator), not drift.

    What remains, stated as the decision it actually is — the ruled direction cannot be implemented coherently as anything smaller than a cross-backend programme that:

    That is the #5298 five-PR programme run in reverse. ⛔ Not dispatchable from this seat on the current ruling text: it requires re-ruling enrolled semantics and granting freeze exceptions, both of which are the maintainer's. Escalating on the seat channel; this card stays open, anchored by the PR's recorded target.


    Generated by Claude Code

  16. claude commented on Aug 10, 2026

    @claude
    Contributor

    Maintainer ruling (2026-08-10, directed in this seat's session session_01MwoubC3jL271FYt9rGXwxb): MAINTAIN the #5298 include direction. Cells 1 and 3 of the 2026-08-10 07:33Z ruling are WITHDRAWN. This card closes.

    The maintainer's decision, quoted untranslated: 「选「维持 include」」 — choosing the first option of the three put to them with the full eleven-surface measurement (PR #7391) in hand.

    What is now settled, in one place:

    1. $ne / $nin / $notContains on a no-value row MATCH (the include direction) — as ruled by $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 ($not), extended by 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298 (the operator family), shipped across all eleven filter surfaces via nullSafeNegative and nullValueSatisfiesOperator, and enrolled in FILTER_LOGIC_CASES. This is the platform semantics, re-affirmed with the reversal option on the table.
    2. $exists means "has a value" (!= null) — cell 2 of the 07:33Z ruling, which was already shipped by PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 and was never in conflict.
    3. The 07:33Z exclude ruling for cells 1/3 is withdrawn after measurement, not overturned by this seat: the decision brief showed the reversal is a cross-backend programme (flip formula+read-scope-sql together, rewrite four nullValueSatisfiesOperator copies, re-rule the enrolled $ne/$not rows, touch two frozen backends' live paths) purchasing a worse failure mode — silent-absence instead of visible-surplus — with zero business pull.

    Follow-up filed: PR #7391 merged (5f7669e0c) minutes before this ruling, so filter-logic-conformance.ts's header currently records the exclude direction as "RULED but not yet enrolled". A dedicated XS card rewrites that record to "withdrawn; include re-affirmed" so the tree does not carry a standing declared≠enforced contradiction. The pins #7391 added stay exactly as they are — they assert the include row sets, which is now doubly correct.

    Closing completed: the original three divergences are resolved (cells converged by #5298/#5962; the frozen driver-memory residue is tracked for thaw), and the semantics question that made this card load-bearing is settled.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions