修 #5324 / #5328 时给两个过滤面装上共用的形状 门,顺手把一批比较值种类喂给两个面做对读,发现两处语义 分叉(形状门不碰,也不该碰)。按 Prime Directive #10 单独记在这里,unassigned。
实测原文
单行数据 { id: '1', stage: 'won', score: 10 },InMemoryDriver.find vs memory-matcher.match,worktree 基于 c7406b0ec + #5324 的修复:
live(mingo) ref(matcher)
{ stage: /WON/i } ["1"] [] >>> 分叉
{ stage: ['won'] } [] ["1"] >>> 分叉
两条方向相反 ,而且都不涉及 null / 缺字段(那一族是 #5299 )。
机制
1. RegExp 比较值 —— mingo 当正则匹配,匹配器当结构相等
mingo 按 MongoDB 语义把 {field: /re/} 读作正则匹配。memory-matcher.checkCondition 的分派只豁免 Date 和 Array:
if ( typeof condition !== 'object' || condition === null ||
condition instanceof Date || Array . isArray ( condition ) ) {
return value == condition ; // ← RegExp 不在这里
}
const keys = Object . keys ( condition ) ; // RegExp 的自有可枚举键是 []
const isOperatorObject = keys . some ( k => k . startsWith ( '$' ) ) ; // false
return JSON . stringify ( value ) === JSON . stringify ( condition ) ; // 'won' vs '{}' → false
于是 RegExp 掉进结构相等分支,JSON.stringify(/WON/i) 是 {},永远不匹配 。
顺带记录:memory-driver.ts 的 normalizeFilterCondition 明确把 RegExp 排除在算子对象之外并原样传给 mingo(!(value instanceof RegExp)),所以实时路径这一侧是有意 支持的。
2. 数组比较值 —— 匹配器靠 JS 松散相等意外匹配
Array . isArray ( condition ) → return value == condition ;
'won' == ['won'] 在 JS 里是 true (数组被 toString() 成 'won')。所以匹配器把 {stage: ['won']} 读成了「等于 'won'」。mingo 按 MongoDB 语义读作「字段等于该数组,或字段是包含该元素的数组」,标量字段 'won' 都不满足 → []。
匹配器这一侧几乎肯定不是有意设计的 —— ['won','lost'] 会因为 'won' == ['won','lost']('won,lost')而不匹配,而 ['won'] 匹配;这是 toString() 的巧合,不是任何一种「数组比较值」语义。
为什么是 bug
同一个包的两个面,同一个 filter,两个相反答案。 { field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 的裁决理由原话:「a backend whose two halves disagree about what a filter MEANS is exactly the divergence the ruling closes」。driver-memory 的实时查询路径根本不支持 $not —— mingo 抛无 code 的 MingoError,CEL !expr 降下来的 RLS scope 在该驱动上直接 500 #5324 已经把形状 统一到一个门里,但语义 仍是两份实现。
匹配器是 FILTER_LOGIC_CASES held 的那一面,也是 formula 对照的那一面;实时路径是用户跑的那一面。哪一面「对」需要裁决,但两面同时对两个答案都言之凿凿是明确的缺陷。
{f: ['x']} 这种写法在 AI 生成的元数据里很容易出现(作者想写 $in 却写成了裸数组)。今天它在两个后端上得到两个答案,且都不报错。
那一单加的是形状 门(词汇表、$between 元数、组合子操作数、{field:{}}),对语义 一律不碰 —— 它的实现注释里写明了「It decides SHAPE, never MEANING」。这两条是同一个比较值在两个求值引擎里的不同读法,修它要先裁决哪个读法是 canonical,那是一次协议裁决而不是 shape gate 能顺手带走的。
建议(不代裁决)
无论走哪条,建议与 #5299 / #5298 / #5239 一起看 —— 它们是同一族「一个 filter,N 个后端 N 个答案」的裁决。
未验证的部分
我没有测 driver-sql / driver-sqlite-wasm / driver-mongodb 在这两个输入上的答案(driver-sql 对裸数组比较值会走 coerceFilterValue → knex 绑定,预期报错但没实测 );也没有清点仓内是否真有生产者产出裸 RegExp 或裸数组比较值。严重度请 PM 按 triage 定,不代表我判断它低。
关联:#5324 / #5328 (实测出这两条的那一单;形状门已统一,语义未动)、#5299 (同包两面在「字段没有值」上的三处分叉)、#5298 、#5239 (一致性表)、#5240 (两面必须对 filter 的含义一致)。
修 #5324 / #5328 时给两个过滤面装上共用的形状门,顺手把一批比较值种类喂给两个面做对读,发现两处语义分叉(形状门不碰,也不该碰)。按 Prime Directive #10 单独记在这里,unassigned。
实测原文
单行数据
{ id: '1', stage: 'won', score: 10 },InMemoryDriver.findvsmemory-matcher.match,worktree 基于c7406b0ec+ #5324 的修复:两条方向相反,而且都不涉及 null / 缺字段(那一族是 #5299)。
机制
1.
RegExp比较值 —— mingo 当正则匹配,匹配器当结构相等mingo 按 MongoDB 语义把
{field: /re/}读作正则匹配。memory-matcher.checkCondition的分派只豁免Date和Array:于是
RegExp掉进结构相等分支,JSON.stringify(/WON/i)是{},永远不匹配。顺带记录:
memory-driver.ts的normalizeFilterCondition明确把RegExp排除在算子对象之外并原样传给 mingo(!(value instanceof RegExp)),所以实时路径这一侧是有意支持的。2. 数组比较值 —— 匹配器靠 JS 松散相等意外匹配
'won' == ['won']在 JS 里是 true(数组被toString()成'won')。所以匹配器把{stage: ['won']}读成了「等于 'won'」。mingo 按 MongoDB 语义读作「字段等于该数组,或字段是包含该元素的数组」,标量字段'won'都不满足 →[]。匹配器这一侧几乎肯定不是有意设计的 ——
['won','lost']会因为'won' == ['won','lost']('won,lost')而不匹配,而['won']匹配;这是toString()的巧合,不是任何一种「数组比较值」语义。为什么是 bug
{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 的裁决理由原话:「a backend whose two halves disagree about what a filter MEANS is exactly the divergence the ruling closes」。driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 已经把形状统一到一个门里,但语义仍是两份实现。FILTER_LOGIC_CASESheld 的那一面,也是formula对照的那一面;实时路径是用户跑的那一面。哪一面「对」需要裁决,但两面同时对两个答案都言之凿凿是明确的缺陷。{f: ['x']}这种写法在 AI 生成的元数据里很容易出现(作者想写$in却写成了裸数组)。今天它在两个后端上得到两个答案,且都不报错。为什么 #5324 没有一并修
那一单加的是形状门(词汇表、
$between元数、组合子操作数、{field:{}}),对语义一律不碰 —— 它的实现注释里写明了「It decides SHAPE, never MEANING」。这两条是同一个比较值在两个求值引擎里的不同读法,修它要先裁决哪个读法是 canonical,那是一次协议裁决而不是 shape gate 能顺手带走的。建议(不代裁决)
RegExp比较值:FilterConditionSchema没有声明它($regex才是声明的写法)。可选:(a) 匹配器补上condition instanceof RegExp分支,与实时路径取齐;(b) 两个面都拒收裸RegExp,要求写{$regex: …}—— 与 driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 「未声明的一律拒收」同向,但会打断今天在实时路径上能用的写法(需先清点是否有生产者)。value == condition松散相等几乎肯定该改。可选:(a) 与 mingo 取齐(等于数组,或字段数组包含该元素);(b) 两个面都拒收裸数组、要求$in(formula的evalField已经是这个态度:if (Array.isArray(spec)) return false,即 fail-closed)。注意formula在这一条上是第三个答案。无论走哪条,建议与 #5299 / #5298 / #5239 一起看 —— 它们是同一族「一个 filter,N 个后端 N 个答案」的裁决。
未验证的部分
我没有测
driver-sql/driver-sqlite-wasm/driver-mongodb在这两个输入上的答案(driver-sql对裸数组比较值会走coerceFilterValue→ knex 绑定,预期报错但没实测);也没有清点仓内是否真有生产者产出裸RegExp或裸数组比较值。严重度请 PM 按 triage 定,不代表我判断它低。关联:#5324 / #5328(实测出这两条的那一单;形状门已统一,语义未动)、#5299(同包两面在「字段没有值」上的三处分叉)、#5298、#5239(一致性表)、#5240(两面必须对 filter 的含义一致)。