Repository navigation
driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299
Description
Activity
补一条实测:这三个算子的分叉不止 memory ↔ formula,driver-memory 自己的两个过滤面之间就已经分叉
做 #5324 / #5328(
normalizeFilterCondition的形状门)时,把FILTER_LOGIC_CASES穿透到真驱动后,顺手对memory-matcher-not-null-safe.test.ts的整张表做了「实时 mingo 路径 vs 参考匹配器」的逐条比对。本 issue 记的同样三个算子,在同一个包的两个面之间也给出不同答案 —— 而实时路径正是用户真正跑到的那一面。实测原文(worktree 基于
c7406b0ec+ #5324 的修复;InMemoryDriver.findvsmemory-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
分诊(backlog sweep,主 backlog 代扫):挂
finding+domain:engine。理由:跨执行面语义分歧(driver-memory 与 formula 对「字段没有值」三处答案不同)是实锤,但哪一面是对的需要一次语义裁决(先例:#5146 之于$not、#5240 之于空对象)——裁决出来之前没有可写进派发词的验收判据,故按 finding 持有。晋级路径:engine 车道 PM 起[决策]单或并入下次发现分诊轮,与 #5298 宜同一次裁决处理。不构成认领。会话:session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
范围注记(2026-08-05,维护者裁决,锚点 #5499):driver-memory 投入已冻结,本单三处分歧中涉 driver-memory 的部分随之冻结;formula 侧对「字段没有值」的语义是否自洽仍是活问题,故保留
finding不转 on-hold。下次发现分诊轮请以 formula(及 driver-sql 对照)重新框定;若重框后 formula 侧无独立缺陷,可引用锚点关闭(not planned)。会话:session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
发现分诊轮判级:持有(
finding留)。理由:已并案入 #5298(该单已转needs-user-decision,作为 NULL/无值语义一族的唯一裁决入口,本单三格实测矩阵是其裁决输入);裁决落地后按 08-05 14:25 注记处置 —— formula 侧重框,若重框后无独立缺陷则引 #5499 锚点关闭(not planned)。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
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
状态注记(drivers 车道 PM,#5298 程序):本单三格的现状——
$notContains(null 值):已裁已实现(非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 ④「包含 NULL 行」,PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 落地 driver-sql/read-scope-sql/formula);$exists(键在值为 null):已裁已实现(非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 ③「取有值」,formula 已收敛,$exists的比较值不是布尔时,三个后端面给出三个答案,driver-memory 自己的两个面在'yes'上就已分叉 —— 实测,$null(#5347)的同族另一轴 #5369 已闭环);$nin(缺键):方向已裁(包含),但 conformance 表的 N2/N3 格位刻意未入表——它们会顶红被 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 冻结的 driver-memory 参考匹配器,裁定(非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 PM 裁定评论)是随 driver-memory 解冻/退役批次补入。本单保持 open,作为 N2/N3 两格的记账锚点;driver-memory 参考匹配器在这两格的行为修正也记在此(解冻时一并)。
Generated by Claude Code
发现分诊轮(#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」。
重启条件(重新锚定)
本单的持有理由从「等一个批次」改为「充当记账锚点」,这是一个自洽且可退出的状态:
- 锚点职责(即刻生效,也是本单 open 的唯一理由):N2/N3 两格的 conformance 缺口 + driver-memory 参考匹配器在这两格的行为修正,记在本单,防止它们随冻结一起被遗忘;
- 退出条件:
#5499的冻结口径被重议(维护者重新裁决,或按其裁决文另立 ADR-0087 退役程序)—— 届时本单随该程序晋级,把两格补入 conformance 表; ⚠️ 不设时间窗:冻结是无限期的,给本单编一个日期只会制造假到期。
如维护者认为「一条只为记账而 open 的 issue」不值得占位,可否决本条并改判关闭(not planned)—— 代价是 N2/N3 两格在解冻时需重新推导。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings triage round (#4949 discipline): HOLD (
finding+domain:driverskept). 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
- [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 unchanged: still open, still
priority:p0+tracking+domain:drivers, last updated 2026-08-06T07:18:45Z. The maintainer's ruling A (keep the freeze, do not retire; any future retirement becomes its own ADR-0087 programme) is the live standing instruction, so the freeze ondriver-memory/driver-mongodbinvestment still applies to this issue. - Consequently the exit condition written last round — "the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze is re-litigated" — is neither satisfied nor expired. It remains the only legal exit and it is externally checkable, which is what the "a hold without a restart condition is a state nobody can ever legally exit" rule requires.
- The two already-adjudicated cells (
$notContainson null values,$existson key-present-value-null) remain closed out via 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 ④/③ (PRs fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 /$exists的比较值不是布尔时,三个后端面给出三个答案,driver-memory 自己的两个面在'yes'上就已分叉 —— 实测,$null(#5347)的同族另一轴 #5369); the open remainder is still just the N2/N3 conformance cells plus the reference matcher's behaviour in them.
Freeze ruling applied
Per the standing triage instruction anchored at #5499,
driver-memory/driver-mongodbwork is frozen: this issue is not eligible for promotion topm:queueon 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 keepsfindingrather than takingpm:on-hold, becausefindingis 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
- [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 unchanged: still open, still
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 的缺陷修复与语义补齐。所以裁决落地时会被切成:- ✅
formula半边 —— 可改,且能解开 [finding]undefined比较数在仓内有五种读法 —— #6050 只在 driver-sql/turso 落了拒收,formula 读作「键缺失」、read-scope-sql 编成= NULL、driver-memory 读作 null #6125; - ⛔
driver-memory半边 —— 除非同时解冻,否则只能记账,分歧会继续存在; - ✅
FILTER_LOGIC_CASES([spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239)扩表 —— 可做,且是本单最该先做的一步(见下)。
请把这一点计入取舍:裁一个「三家统一」的答案,今天只能兑现两家。
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
- ✅
- addedpriority:p2Medium: important, M3Medium: important, M3
on Aug 7, 2026 15 remaining items
Triage (state repair):
needs-user-decisionappended.priority:p2/domain:engine-coreunchanged. ⛔ Claim untouched — thedomain:engine-coreseat's claim (04:58Z, sessionsession_01MwoubC3jL271FYt9rGXwxb, branchclaude/issue-5299-negative-operators-value-semantics) stands, and its 05:18Z STOP report is the reason for this label, not a reason to reclaim.- Why the decision box, again: the dev's stop-and-report (05:18Z) establishes with merge-level evidence that the 2026-08-07 ruling's cells 1 and 3 reverse the completed 非否定路径上的
$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 programme (五 PR chain, include-direction shipped acrossformula/read-scope-sql/sql-driver,$nerow already enrolled inFILTER_LOGic_CASES), and cell 2 is already delivered by PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962. The report ends with a genuine A/B fork — (A) 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 governs, cells 1/3 withdrawn as written against a stale premise, nothing dispatchable today; (B) the 08-07 ruling supersedes and becomes a cross-driver reversal programme, much of it inside the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze. Two maintainer rulings in conflict can only be resolved by the maintainer; the state machine has to say a decision is pending or the card sits invisible to the inbox while the dev waits. - Worth weighing for the ruling: the dev's observation that the 08-07 ruling's wording matches the direction the maintainer already flagged as a typo on 非否定路径上的
$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 (2026-08-06 12:16 「笔误」 correction) is on record and checkable — it makes (A) the likelier intent, and (A) costs nothing today. - State honesty: this card has now been through
finding→needs-user-decision→ ruled →pm:queue→ claimed → stopped-on-conflict. The label follows the live state (a decision is pending); when ruled, whoever records it should restore the dispatch state and the claim can resume in place — the worktree is left clean per the report.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Why the decision box, again: the dev's stop-and-report (05:18Z) establishes with merge-level evidence that the 2026-08-07 ruling's cells 1 and 3 reverse the completed 非否定路径上的
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:
$notContainson a value-less field → does not match (formula changes; driver-memory and SQL already agree).$existsmeans has a value (!= null), not key-presence — key-presence is unexpressible in SQL forever (formula changes; driver-memory already reads has-value).$ninon a missing key → does not match (formula changes; driver-memory's early-exit guard stays as-is; SQL'sNOT INover 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_CASESso 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
RE-CLAIM after the 07:33Z ruling —
domain:engine-coreseat (#6019). Sessionsession_01MwoubC3jL271FYt9rGXwxb· branchclaude/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;
$notContainsand$nindo not match no-value rows;$existsmeans 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/mainas verified this morning, and lesson ⑳ applies in both directions:- "$exists (formula changes)" —
matches-filter.tsalready readsactual != 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. - "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 deliberatenullSafeNegativerewrite (read-scope-sql.tsandsql-driver.ts'snullValueSatisfiesOperator). 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. - The enrolled
FILTER_LOGIC_CASESrow "$nereturns 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$neor rewriting shipped SQL: stop and report with a measured plan in that case. check:driver-conformancereports 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
- "$exists (formula changes)" —
Dev report — the annotation half is landed (draft PR #7391); the behaviour half is a measured STOP with a plan
domain:engine-coreseat, sessionsession_01MwoubC3jL271FYt9rGXwxb, branchclaude/issue-5299-negative-operators-value-semantics, base60f0dd8(not89470f7— 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 infilter-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 enrolledd-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/$ninrow sets are now asserted (['2','3','4']), beside$neanswering 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
①
$existsis a no-op. Confirmed.matches-filter.ts:263already readsv === 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-memoryreads the ruled semantics only on its reference matcher; its live mingo path — the one users reach — answers the include direction on$nin/$notContainsand reads$existsas key-presence. That is the two-face divergence already pinned inmemory-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: trueon a null valueformulamatchesFilterConditionMATCH MATCH no — ruled-correct already driver-memoryreference matcherno — ruled-correct already MATCH on null, no on missing key no — ruled-correct already driver-memorylive mingo pathMATCH MATCH MATCH — key-presence driver-memoryanalytics faceMATCH MATCH MATCH — key-presence driver-sql/driver-sqlite-wasm/driver-tursolocalMATCH MATCH no — IS NOT NULLdriver-tursoREMOTEMATCH MATCH no — IS NOT NULLdriver-mongodbtranslateFilterMATCH MATCH MATCH — mongo $existsis key-presenceservice-analyticsread-scope-sqlMATCH MATCH no — IS NOT NULLservice-analyticsfilter-normalizerMATCH MATCH no — IS NOT NULLMechanism behind every
MATCH:nullSafeNegative(col IS NULL OR col NOT LIKE ?) plus four independent copies ofnullValueSatisfiesOperator($nin→ true,$notContains→ true) insql-driver.ts:1986/1993,read-scope-sql.ts:877,filter-normalizer.ts:1167,remote-transport.ts:267.③ The enrolled
$nerow. Confirmed live and load-bearing (filter-logic-conformance.ts,expected: ['2','3','4']).$ninis the list form of$ne, and the$notrow 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'ssrc/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 aredriver-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_CASESimport would delete thirty-five passing cells to hide one. So the ruled instruction "add the null/missing rows toFILTER_LOGIC_CASESso 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 alongsideformula(defensible under "coordinate with #5298 spellings", andservice-analyticsis 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:- 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. - Rewriting
sql-driver— plusread-scope-sql,filter-normalizerandremote-transport, four independent copies of the same table, or the write side and the read side disagree. - Touching frozen
driver-mongodb— its$ninmaps straight through and its$existsis key-presence; both are ruled wrong and both are inside [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499. - Re-ruling the enrolled
$nerow — and$notwith 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
formulaalone re-opens exactly the hole PR #5962 closed, sign-reversed: an RLScheckwould deny a write on a null field that the read scope still returns. And it would splitformulaagainst 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:
- Unfreeze decision first.
driver-memory(three faces) anddriver-mongodbare 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. - Re-rule the enrolled rows explicitly.
$ne returns the rows with no valueand$not returns the rows with no valueflip 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. - One PR per coupled pair, write side with read side.
formula+read-scope-sqltogether (the fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 pairing);sql-driver+filter-normalizer+remote-transporttogether (onenullValueSatisfiesOperatoranswer, four copies); the frozen drivers last. - Enrol the rows in the final PR, when every backend answers — the discipline the header's family-2 note already documents.
- Re-run
EXPLAIN QUERY PLAN: 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 measured zero index regression addingIS 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 ondriver-sql(top level and inside combinators),driver-sqlite-wasm,driver-memory(both surfaces) andformula;driver-mongodbstill 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.expectedis 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: successcheck:driver-conformancebefore (clean tree) and after, identical:OK — 36 covered cell(s), 4 in the DEBT ledger, 0 exempt.packages/specis 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,typecheckfor all three touched packages, and fullvitestforpackages/spec(9480),packages/formula(623),packages/drivers/driver-memory(560).Changeset: one, non-empty,
@objectstack/spec: patch— no published runtime behaviour changed, so nomajorand no ADR-0087 disposition trio.7. What I deliberately did not move, and what the record got wrong
- ⛔
formula's$notContains/$ninarms — §3. - ⛔ New
FILTER_LOGIC_CASESrows — §2④; no legal DEBT spelling exists. - ⛔ Any
driver-memory/driver-mongodbbehaviour — [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499, and the ruling's own instruction. - ⛔
$exists— already delivered by PR fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962; verified, not re-implemented.
Two things in the record that measurement contradicts, neither of which changes the ruled direction:
- The ruling's "(formula changes; driver-memory and SQL already agree)" is wrong on both halves for
$notContainsand$nin. SQL agrees withformula, not with the reference matcher — 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 bent it there on purpose. Anddriver-memory"agrees" only on its reference matcher; its live mingo path and its analytics face answer the include direction, and read$existsas key-presence. driver-memory's pin claimedformulareads$existsas 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
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-conformanceis byte-identical before/after. It also corrects a stale pin comment that claimed formula still reads$existsas 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:
- My "enroll via the DEBT spelling" instruction was not executable — the DEBT ledger in
check-driver-conformance.mjsis 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. - 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'snullSafeNegative+ four copies ofnullValueSatisfiesOperator), 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:
- flips
formulaandread-scope-sqltogether (the fix(drivers,analytics,formula): $ne / $nin / $notContains 在 $not 之外也 NULL-safe (#5298) #5962 security coupling; formula alone re-opens the RLS split sign-reversed), - rewrites the four
nullValueSatisfiesOperatorcopies +nullSafeNegative(driver-sql,driver-tursoremote, bothservice-analyticsfaces), - re-rules the enrolled
$neand$notconformance rows (['2','3','4']→['2']) — reversing what$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 and 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 shipped, since$ninis the list form of$ne, - and touches the live query paths of two frozen backends (driver-memory mingo path, driver-mongodb — both currently include, both inside [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499).
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
- My "enroll via the DEBT spelling" instruction was not executable — the DEBT ledger in
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:
$ne/$nin/$notContainson 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 vianullSafeNegativeandnullValueSatisfiesOperator, and enrolled inFILTER_LOGIC_CASES. This is the platform semantics, re-affirmed with the reversal option on the table.$existsmeans "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.- 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
nullValueSatisfiesOperatorcopies, re-rule the enrolled$ne/$notrows, 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, sofilter-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
- added a commit that references this issue
on Oct 7, 2026
修 #5146 时,为了给 SQL 侧找一个「JS 家族的正确答案」而把两个 JS 后端逐算子跑了一遍,
顺带测出它们彼此在三处不一致。不在 #5146 范围内(那单裁定的是
$not在 SQL 上的 NULL 语义),按 Prime Directive #10 记录。
事实(实测)
两组 fixture,区分「键在、值为 null」与「键不存在」:
NULLED:3: { stage: null }、4: { stage: null }MISSING:3: {}(无 stage 键)、4: {}match(值为 null){ stage: { $notContains: 'w' } }{ stage: { $exists: true } }{ stage: { $nin: ['won'] } }成因都在
packages/plugins/driver-memory/src/memory-matcher.ts的checkCondition:if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') return false;—— 豁免名单里没有
$nin,所以缺键的记录直接判$nin不通过,而同一记录若把字段写成
null则通过。formula 的evalOp两种情况都判「不在集合里」→ 通过。case '$notContains': if (typeof value !== 'string' || value.includes(target)) return false;—— 值不是字符串(null / undefined)一律判否;formula 的
!(typeof actual === 'string' && actual.includes(v))判是。$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 顶出来。
需要拍板的
$notContains对「没有值」的字段是否成立?(SQL 侧天然是「不成立」,formula 说「成立」,driver-memory 说「不成立」。)
$exists的语义是「键存在」还是「有值」?两者在 SQL 上不可区分,所以选「有值」能让三家统一,选「键存在」则 SQL 永远无法兑现 —— 按 declared = enforced,倾向前者。
$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 表)。