Repository navigation
$null 的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 5, 2026 PM 裁定 A(两边都拒收非布尔比较值) + 认领,与 #5348 合并为一次派发(会话
session_01Pbu27iNUfQCHeuS551Rqo7)分支
claude/issue-5347-refuse-out-of-contract-filter-input,worktree/home/user/objectstack-issue-5347,域domain:engine,标target:v17(收紧类,窗口在 GA 前)。裁定:A,否决窗口开放
正文说「这需要一次裁决」,对 —— 但它不需要一次新裁决,今天已经立过了:
{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240(维护者拍板):{field: {}}四后端一律响亮拒收,不取 TRUE 不取 FALSE,因为「它几乎必然是编写事故,而两种静默读法都会用一个作者没要求的行数回答它」。- driver-memory 的实时查询路径根本不支持
$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 / driver-memory 对形状错误的$between静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328(PR fix(driver-memory): refuse the filters the live query path cannot evaluate, compile the one it must (#5324, #5328) #5349,已合入60a7a2d37):「拒收你无法求值的东西」,并带一条配套子句「契约声明了的,你就得求值」。
$null: 'yes'落在这条规则的第一半:spec 的FieldOperatorsSchema声明$null: z.boolean(),非布尔是越界输入,不是一种需要解释的写法。B / C 都要在两个同样难自圆其说的巧合之间挑一个,而且在 B 下$null: 0读作 IS NULL —— 几乎肯定不是作者的意思。A 是唯一一条不必猜的路。正文自己也倾向 A,理由与此一致,采纳。否决窗口:维护者若认为收紧代价过大,回一句我改判 B 并重派。
为什么与 #5348 合并
两条都是 driver-sql 的「拒收越界输入」,落在同一文件的同一区域(
sql-driver.ts的过滤编译/校验遍历)。sql-driver.ts今天已被改了四次(#5243 归约、#5296 NULL-safe、#5327{field:{}}闸门、#5329 数组方言删除),分两单派意味着第二个 agent 去改第一个刚重写过的代码。一个 PR 同时Fixes #5347与Fixes #5348。两条也是同一形状的两面:#5348 是节点位置的越界
$op被当成列名编译、静默返回空集;本单是字段位置的越界比较值被两个后端读成相反的意思。 都是「越界输入没有在门口被拦下」。已定范围
- driver-sql:
$null非布尔比较值拒收;$null的守卫表(fix(driver-sql):$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296 的nullValueSatisfiesOperator里case '$null': return value !== false)随之调整 —— 拒收之后那条!== false的宽松读法不再需要,请一并核对是否可收紧为「只可能是 true/false」。 - driver-memory:
normalizeFieldOperators的$nullarm 同样拒收,两个过滤面都要(fix(driver-memory): refuse the filters the live query path cannot evaluate, compile the one it must (#5324, #5328) #5349 刚落的assertFilterConditionShape是唯一的门,加在那里)。 - 信封:复用
filter-refusal.ts的unsupportedFilterError(INVALID_FILTER/ 400),不要另造第三套。
未验证、需实现方补测
正文明说没测
driver-sqlite-wasm(继承SqlDriver,预期随基类)与driver-mongodb($nullarm 写法与 memory 相同,预期与 memory 一致)。请实测这两个,并把结果写进 PR —— 如果 mongodb 侧也需要改,在本单范围内一并改(它是同一条规则的第四个后端);如果只是随基类自动生效,给出证据。明确不在范围
- ⛔ regex 族(
$contains/$startsWith/$endsWith/$notContains)的非字符串比较值 —— 正文已实测两边方向一致、都 fail-closed,不构成分叉,driver-sql 的 [spec]$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041 注释也明确把这族排除在守卫外。不要顺手收紧它。 - ⛔
packages/spec/**。 - ⛔
packages/services/service-analytics/**(analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352 在飞)、packages/plugins/driver-memory/src/memory-analytics.ts(driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not整条丢,$between/$startsWith/$null/$regex因无 cube 映射而丢 —— 聚合结果被放大 #5345 待派,见下)。 - ⛔
content/docs/releases/**。
Generated by Claude Code
- added a commit that references this issue
on Aug 5, 2026 - added 3 commits that reference this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 8, 2026 - added a commit that references this issue
on Aug 17, 2026
修 #5324 / #5328 时按 #5328 的建议「顺带查同一
switch里其他有条件写入的 arm 是否同款($null、regex 族)」清点,发现$null这一条不是「静默丢弃」而是静默取反。按 Prime Directive #10 单独记在这里,unassigned。实测原文
同一个过滤器
{ stage: { $null: 'yes' } },同一份数据(单行{ id: '1', stage: 'won' }):对照组,证明分叉只出现在非布尔值上:
机制:两边的默认分支挂在相反的一侧
driver-sql(sql-driver.ts:6363)按=== false判定,即「不是false就当 true」:driver-memory(memory-driver.tsnormalizeFieldOperators)按=== true判定,即「不是true就当 false」:两个默认分支方向相反,于是每一个既不是
true也不是false的比较值 ——'yes'、1、null、undefined、{}—— 都会拿到两个互相矛盾的答案。driver-sql内部是自洽的(#5146 的nullValueSatisfiesOperator也写着case '$null': return value !== false),driver-memory内部也是自洽的(两个过滤面一致);分叉严格发生在两个后端之间。为什么是 bug
$between静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328 那类静默是结果集变空或变全,还看得出不对;这一条返回的是一个语义上恰好相反的行结果集,两边都言之凿凿。FieldOperatorsSchema里$null: z.boolean().optional(),所以非布尔值本就是越界输入 —— 但where走到驱动时没有任何一层按FieldOperatorsSchema校验过,所以越界输入是真的能到这里的。AI 生成的元数据把$null: "true"(字符串)写出来是完全可能的,而字符串"false"是 truthy,于是 SQL 说 IS NULL、memory 说 IS NOT NULL。$not同级:$null是 RLS scope 里判断「字段没值」的常规写法。建议(不代裁决)—— 这需要一次裁决,不能各改各的
三条路,方向不同,必须先定哪个是 canonical,否则改一边只是把分叉换个位置:
INVALID_FILTER/ 400)。与{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 / driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 / driver-memory 对形状错误的$between静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328 已确立的「无法求值就响亮拒收」同向,也与「declared = enforced」同向:spec 声明z.boolean(),那就在驱动门口按声明拒收。代价是这是收紧,会让今天靠 truthy 巧合工作的调用方拿到 400。=== false判定(采 driver-sql 的现状为准)。改动最小,但等于把「除false外都算 IS NULL」这条不成文规则固化成契约 —— 而$null: 0之类的输入在这条规则下读作 IS NULL,几乎肯定不是作者的意思。=== true判定(采 driver-memory 的现状为准)。对称的问题。我个人倾向 A:它是唯一一条不需要在两个都很难自圆其说的巧合之间挑一个的路,也是唯一一条让「AI 写错的元数据在写入时就被拒」而不是「在两个后端上各跑出一个答案」的路。但这条会改变今天不报错的输入的行为,所以留给维护者拍。
顺带记录:同一轮清点里,regex 族(
$contains/$startsWith/$endsWith/$notContains)的比较值不是字符串时,两边都做String(val)字符串化([object Object]),方向一致、都是 fail-closed,不构成分叉 —— driver-sql 的 #5041 注释也明确把这一族排除在守卫外。这条不需要动。未验证的部分
我没有实测
driver-mongodb/driver-sqlite-wasm在同一输入下的行为(sqlite-wasm 继承SqlDriver,预期与 SQL 一致但没实测;mongodb 的$nullarm 写法与 memory 相同,预期与 memory 一致但没实测),也没有量化真实元数据里出现非布尔$null的频率。严重度请 PM 按 triage 定,不代表我判断它低。关联:#5324 / #5328(实测出这一条的那一单)、#5146(
$not的 NULL 语义裁决,同款「先定 canonical 再改」)、#5240(一个条件一种措辞)、#5298(非否定路径上的 NULL 语义分叉)。