出自 #6050 的实施(PR 见下)。#6050 的裁决(2026-08-07,B 案)把「比较数位置的 undefined」定为一律拒收(INVALID_FILTER / 400),实施面按裁决只覆盖 driver-sql + driver-turso 的 remote transport;issue 正文「落点」节要求裁决后按 #5298 的方式逐格实测其余无值语义面。实测做完了,记录在此 ,按 Prime Directive #10 不指派、不扩大那张 PR 的 diff。
实测(origin/main @ cba7454df,PR 落地前;四行 fixture:d 在 1-2 有值、3 为 SQL NULL、4 为「键不存在」)
同一个形状 { d: undefined },五个求值面五种读法 :
求值面
{ d: undefined }
读法
driver-sql / turso LOCAL
抛裸 knex Undefined binding(s)(无 code/status)
崩
turso REMOTE(buildWhereSQL)
['3','4']
undefined ≡ null → IS NULL
driver-memory(mingo 活路径)
['3','4']
undefined ≡ null
@objectstack/formula matchesFilterCondition
['4']
undefined ≢ null,只匹配键缺失 的行
service-analytics read-scope-sql.ts compileScopedFilterToSql
"t"."d" = ? / params [null]
d = NULL → UNKNOWN → 匹配零行
对照组(null 比较数)五面一致,{d: null} / {$eq: null} → IS NULL 族,{$ne: null} → IS NOT NULL 族。
其余格(实测同表):
read-scope-sql:{$gt: undefined} → "t"."d" > ? bound null;{$in:[undefined]} → IN (?) bound null;{$not:{d: undefined}} → NOT (("t"."d" IS NOT NULL AND "t"."d" = ?)) bound null。全部是「合法 SQL、绑了个 NULL、答一个没人问的问题」,且一条日志都不报 。
formula:{$ne: undefined} → ['1','2','3'](含 SQL NULL 行),{$in:[undefined]} → ['4'],{$gt: undefined} → []。
driver-memory:{$ne: undefined} → ['1','2'],{$in:[undefined]} → [],{$not:{d:{$ne: undefined}}} → ['3','4']。
为什么单独立单而不是搭 #6050 的车
三条,都是语义争议 而非实现细节,#6050 的裁决没有覆盖它们,其派工单也明确要求「有语义争议只记录留后续单」:
formula 的读法是第三种语义,不是第三个 bug 拼写。 它把 undefined 读成「这个键在记录里不存在」——{d: undefined} 只匹配 row 4,而 {d: null} 匹配 3 和 4。这是有意义的区分 (driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 正在争的就是「键缺失」vs「值为 null」该不该分家),直接照 drivers: undefined 比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 改成拒收会顺手替 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 拍板。
read-scope-sql 的信封不同。 它只会抛 READ_SCOPE_COMPILE_FAILED / 500 (模块自述「⛔ The only way this module refuses」),而 drivers: undefined 比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 判的是 INVALID_FILTER / 400 。read scope 是平台自己编译出来的,不是调用方输入,所以「这是谁的错、该报哪个码」是一次独立裁决,不能默认沿用。
driver-memory / driver-mongodb 是 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 的冻结面 ,drivers: undefined 比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 的裁决明确写了「照 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 只 pin 不改」。但请注意后果:PR 落地后 driver-memory 与 driver-sql 在这一格上开始不一致 (memory 答 ['3','4'],sql 拒收)。这是裁决接受的代价,不是回归,但它是冻结解冻时要还的账。
触达性(判严重度需要的那一半)
undefined 过不了 JSON,所以只能从进程内 代码来。三个面的进料口:
⚠️ 严重度请分诊自己判:立单时判的严重度两个方向都不可靠(#5347 的先例)。这里如实记的是「未证实触达」,不是「不会发生」——read-scope-sql 那格若真被喂到,方向是匹配零行 (fail-closed,不是越权),而 formula 那格方向是「少匹配」,两者都不是 #6050 那种越权读。
建议的处置(供裁决方,不是既定结论)
把 drivers: undefined 比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 的 B 案推平到 formula + read-scope-sql,各自用自己 的信封(formula 无信封,需先定;read-scope 用 READ_SCOPE_COMPILE_FAILED/500);同时给 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 的「键缺失 vs null」一并拍板,因为改 formula 绕不过它;或
只补 read-scope-sql(信封已有、方向单一),formula 挂到 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 上一起裁;或
全部并入 [finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 提议的「谓词编译收敛」调查程序 —— 这已经是同一形状第 N 次要 5 份裁决,[finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 的成本清单说的正是这件事。
关联
Blocked-by: #6050 (其 PR 落地后本单的对照表才是当前状态)。同族:#5298 (无值语义族的裁决入口 + 「守卫匹配发射器」不变量)、#5299 (driver-memory 与 formula 对「字段没有值」的三处分歧)、#5905 (objectql having-filter.ts 是第五个求值面)、#5930 ([finding] 仓内 5 个独立过滤器→谓词编译器,每次裁决 ×5)、#5499 (memory/mongodb 投入冻结)、#5347 / #5369 (比较数按声明拒收的先例)。
会话:session_01WyvqvKMG6asi9aXjKE6xtx
出自 #6050 的实施(PR 见下)。#6050 的裁决(2026-08-07,B 案)把「比较数位置的
undefined」定为一律拒收(INVALID_FILTER/ 400),实施面按裁决只覆盖driver-sql+driver-turso的 remote transport;issue 正文「落点」节要求裁决后按 #5298 的方式逐格实测其余无值语义面。实测做完了,记录在此,按 Prime Directive #10 不指派、不扩大那张 PR 的 diff。实测(
origin/main@cba7454df,PR 落地前;四行 fixture:d在 1-2 有值、3 为 SQL NULL、4 为「键不存在」)同一个形状
{ d: undefined },五个求值面五种读法:{ d: undefined }driver-sql/ turso LOCALUndefined binding(s)(无code/status)buildWhereSQL)['3','4']undefined ≡ null→IS NULLdriver-memory(mingo 活路径)['3','4']undefined ≡ null@objectstack/formulamatchesFilterCondition['4']undefined ≢ null,只匹配键缺失的行service-analyticsread-scope-sql.tscompileScopedFilterToSql"t"."d" = ?/ params[null]d = NULL→ UNKNOWN → 匹配零行对照组(
null比较数)五面一致,{d: null}/{$eq: null}→ IS NULL 族,{$ne: null}→ IS NOT NULL 族。其余格(实测同表):
read-scope-sql:{$gt: undefined}→"t"."d" > ?boundnull;{$in:[undefined]}→IN (?)boundnull;{$not:{d: undefined}}→NOT (("t"."d" IS NOT NULL AND "t"."d" = ?))boundnull。全部是「合法 SQL、绑了个 NULL、答一个没人问的问题」,且一条日志都不报。formula:{$ne: undefined}→['1','2','3'](含 SQL NULL 行),{$in:[undefined]}→['4'],{$gt: undefined}→[]。driver-memory:{$ne: undefined}→['1','2'],{$in:[undefined]}→[],{$not:{d:{$ne: undefined}}}→['3','4']。为什么单独立单而不是搭 #6050 的车
三条,都是语义争议而非实现细节,#6050 的裁决没有覆盖它们,其派工单也明确要求「有语义争议只记录留后续单」:
formula的读法是第三种语义,不是第三个 bug 拼写。 它把undefined读成「这个键在记录里不存在」——{d: undefined}只匹配 row 4,而{d: null}匹配 3 和 4。这是有意义的区分(driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 正在争的就是「键缺失」vs「值为 null」该不该分家),直接照 drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 改成拒收会顺手替 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 拍板。read-scope-sql的信封不同。 它只会抛READ_SCOPE_COMPILE_FAILED/ 500(模块自述「⛔ The only way this module refuses」),而 drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 判的是INVALID_FILTER/ 400。read scope 是平台自己编译出来的,不是调用方输入,所以「这是谁的错、该报哪个码」是一次独立裁决,不能默认沿用。driver-memory/driver-mongodb是 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 的冻结面,drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 的裁决明确写了「照 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 只 pin 不改」。但请注意后果:PR 落地后 driver-memory 与 driver-sql 在这一格上开始不一致(memory 答['3','4'],sql 拒收)。这是裁决接受的代价,不是回归,但它是冻结解冻时要还的账。触达性(判严重度需要的那一半)
undefined过不了 JSON,所以只能从进程内代码来。三个面的进料口:read-scope-sql的filter来自 CEL 编译(cel-to-filter.ts)与存库的 metadata(JSON,不可能带 undefined)——未证实今天有生产调用方能喂进 undefined,所以按 observation 立单;formula同理;driver-sql/turso 那两格是已证实触达的({ owner_id: ctx.user?.id }),所以才有 drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050。read-scope-sql那格若真被喂到,方向是匹配零行(fail-closed,不是越权),而formula那格方向是「少匹配」,两者都不是 #6050 那种越权读。建议的处置(供裁决方,不是既定结论)
undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 的 B 案推平到formula+read-scope-sql,各自用自己的信封(formula 无信封,需先定;read-scope 用READ_SCOPE_COMPILE_FAILED/500);同时给 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 的「键缺失 vs null」一并拍板,因为改 formula 绕不过它;或read-scope-sql(信封已有、方向单一),formula挂到 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 上一起裁;或关联
Blocked-by: #6050(其 PR 落地后本单的对照表才是当前状态)。同族:#5298(无值语义族的裁决入口 + 「守卫匹配发射器」不变量)、#5299(driver-memory 与 formula 对「字段没有值」的三处分歧)、#5905(objectql
having-filter.ts是第五个求值面)、#5930([finding] 仓内 5 个独立过滤器→谓词编译器,每次裁决 ×5)、#5499(memory/mongodb 投入冻结)、#5347 / #5369(比较数按声明拒收的先例)。会话:
session_01WyvqvKMG6asi9aXjKE6xtx