Repository navigation
[finding] undefined 比较数在仓内有五种读法 —— #6050 只在 driver-sql/turso 落了拒收,formula 读作「键缺失」、read-scope-sql 编成 = NULL、driver-memory 读作 null #6125
Description
Activity
Findings triage — verdict: HOLD (keeps
finding;domain:driversleft as filed — see the routing note below).Reachability is the whole grading, and the body did that work honestly.
undefinedcannot survive JSON, so every one of the five readings is reachable only from in-process code. Of the surfaces tabulated,driver-sql/ turso is the proven one ({ owner_id: ctx.user?.id }) — and that half is already adjudicated and being implemented under #6050's B-case ruling. Forformulaandread-scope-sql, the body states plainly that no production caller is confirmed to feed themundefined(read-scope-sql's filters come from CEL compilation and stored JSON metadata). Unconfirmed reachability plus a ruling already covering the confirmed half is the definition of held, not queued.Promoting it now would also decide two things that belong elsewhere — the stronger reason to hold:
@objectstack/formula's reading (undefined= key absent, matching only row 4, wherenullmatches rows 3 and 4) is a third semantics, not a third spelling of one bug. Extending drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050's reject-everything ruling over it would settle "key missing vs value null" by side effect — which is the exact question driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 is open on.read-scope-sqlrefuses only withREAD_SCOPE_COMPILE_FAILED/ 500 because the filter is platform-compiled, not caller input, while drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 ruledINVALID_FILTER/ 400 for caller input. "Whose fault, which code" there is its own adjudication and must not inherit by default.
Restart conditions (either one promotes this, so it is exitable):
- driver-memory 与 formula 对「字段没有值」给出三处不同答案:
$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 is adjudicated — thenformula's cell is decided by that ruling and this issue becomes an implementation, or - a production caller is demonstrated to feed
undefinedintoread-scope-sqlorformula— at which point reachability is proven and it stops being observation-class.
Routing note (for whoever promotes it, not a change now): the label reads
domain:driversbecause #6050 is its origin, but the two live cells arepackages/formula(⇒domain:engine-core) andpackages/services/service-analytics(⇒domain:services). ⛔ Do not dispatch this as one cross-domain unit — on promotion it splits contract-first, one sub-issue per domain withBlocked-by:ordering.Freeze check, recorded explicitly: the
driver-memory/driver-mongodbcells sit inside the #5499 investment freeze and stay pin-only. The consequence the body flags is real and accepted, not a regression: once #6050's PR lands,driver-memoryanswers['3','4']wheredriver-sqlrejects. That divergence is a debt owed at thaw time — whoever unfreezes those drivers reads this issue first.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings triage (objectstack#4949 discipline) — verdict: HOLD stands (keeps
finding,domain:drivers). Recording one prediction that has since come true, so nobody re-derives it.1. Neither restart condition fired
- "driver-memory 与 formula 对「字段没有值」给出三处不同答案:
$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 is adjudicated" — driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 is open, still carryingfinding+domain:drivers. Not adjudicated. Theformulacell ("undefined= key absent") therefore still cannot be decided here without settling key missing vs value null by side effect, which is the whole reason that condition exists. - "a production caller is demonstrated to feed
undefinedintoread-scope-sqlorformula" — nothing new this round;read-scope-sql's filters still arrive from CEL compilation and stored JSON metadata.
Unconfirmed reachability on the two live cells ⇒ observation-class, held.
2.
⚠️ What DID change: the predicted divergence is now live, not prospective#6050 closed
2026-08-07T04:02:06Z. The previous verdict wrote:once #6050's PR lands,
driver-memoryanswers['3','4']wheredriver-sqlrejects. That divergence is a debt owed at thaw time — whoever unfreezes those drivers reads this issue first.That sentence is now in the present tense. The freeze note is unchanged in substance but is no longer a forecast: the
driver-memory/driver-mongodbcells sit inside the #5499 investment freeze and stay pin-only, and the behavioural split againstdriver-sqlexists onmaintoday. This issue remains the standing record for whoever unfreezes them.This is not a re-grade — it is the mechanical restart-condition check finding a recorded consequence rather than a recorded trigger, and writing it down where the next reader will hit it.
3. Routing note carried forward (unchanged, applies on promotion)
The label reads
domain:driversbecause #6050 is its origin, but the two live cells arepackages/formula(⇒domain:engine-core) andpackages/services/service-analytics(⇒domain:services). ⛔ Do not dispatch as one cross-domain unit — on promotion it splits contract-first, one sub-issue per domain withBlocked-by:ordering.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- "driver-memory 与 formula 对「字段没有值」给出三处不同答案:
PM 裁决 —— 只推平
read-scope-sql这一格,formula留给 #5299采用本单建议列表的第 2 案。分诊定级:
read-scope-sql半边pm:queue,formula半边不派。为什么这半边我能裁
第三层裁决的界线是「适用已有先例 = PM 裁;新契约取舍 = 上交」。
read-scope-sql这一格两个问题分别落在两条已有规则上,没有一处需要发明:- 要不要拒收 —— drivers:
undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 已于 2026-08-07 裁 B 案:比较数位置的undefined一律拒收。本单实测表明read-scope-sql在同一形状上「编出合法 SQL、绑一个 NULL、答一个没人问的问题,且一条日志都不报」。这正是 B 案要消灭的东西,不是新问题。 - 报哪个码 —— 不需要选。该模块自述「⛔ The only way this module refuses」就是
READ_SCOPE_COMPILE_FAILED/ 500,本单正文也已确认。沿用模块自己既有的唯一信封是适用现状,不是新设信封。
而 500 在这里也是对的、不是将就:read scope 的 filter 由平台自己从 CEL 与库存 metadata 编译而来,不是调用方输入。给调用方报 400 等于让他去修一个他既没写、也改不动的东西 —— 那才是错的码。ADR-0112 要的是 code 与 status 各自说实话,这一格说的实话就是「平台自己编砸了」。
为什么
formula那半边不派本单正文自己把理由写清楚了,我完全同意,原样采纳:
formula把undefined读成「键在记录里不存在」,这是第三种语义,不是第三个 bug 拼写 ——{d: undefined}只匹配 row 4,而{d: null}匹配 3 和 4。它是一个有意义的区分。照 #6050 直接改成拒收,会顺手替 #5299 正在争的「键缺失 vs 值为 null」拍板 —— 那是一次真正的新契约取舍,不该由一张 undefined 的清扫单附带决定。挂到 #5299,与「无值语义」一起裁。⛔ 本单派工不得触碰
@objectstack/formula。driver-memory维持 #5499 冻结,账继续记着本单正文提前记下的后果依然成立且已被接受:#6050 落地后,memory 答
['3','4']、sql 拒收,两者在这一格上不一致。这是裁决接受的代价、不是回归,解冻时一并还。⛔ 本单不改 memory / mongodb。派工范围(严格)
面 处置 service-analyticsread-scope-sql.tscompileScopedFilterToSql✅ 落 READ_SCOPE_COMPILE_FAILED/ 500 拒收@objectstack/formula⛔ 不碰,挂 #5299 driver-memory/driver-mongodb⛔ 不碰,#5499 冻结 driver-sql/driver-turso⛔ 不碰,#6050 已落地 覆盖格位以本单实测表为准,一格不多一格不少:
{d: undefined}、{$gt: undefined}、{$in:[undefined]}、{$not:{d: undefined}}。四格今天都是「合法 SQL + 绑 NULL + 零日志」,拒收后必须四格都响。严重度
维持本单的 observation 定级,不上调。本单已如实记「未证实触达」而非「不会发生」,且方向是 fail-closed(匹配零行),不是 #6050 那种越权读。定级依据是触达性,不是修复价值 —— 这一格的修复价值本来就主要在「让沉默变响亮」。
⚠️ 但派工时请注意 #5347 的先例:立单时判的严重度两个方向都不可靠。实施者若实测出真实生产调用方能喂进undefined,请当场回报,不要沿用本裁决的定级。
Generated by Claude Code
- 要不要拒收 —— drivers:
认领(os-dev)
- session ID:
session_01WyvqvKMG6asi9aXjKE6xtx - 分支:
claude/issue-6125-read-scope-undefined-comparand
按 PM 裁决 #6125 (comment) 的范围执行:只做
service-analytics的read-scope-sql.tscompileScopedFilterToSql一格,落READ_SCOPE_COMPILE_FAILED/ 500 拒收,覆盖正文实测表的四格({d: undefined}/{$gt: undefined}/{$in:[undefined]}/{$not:{d: undefined}}),并为null比较数对照组补回归 pin。⛔ 不触碰
@objectstack/formula(挂 #5299)、driver-memory/driver-mongodb(#5499 冻结)、driver-sql/driver-turso(#6050 已落地)。认领前已重读本单全部评论,未见其他 session 的认领。
Generated by Claude Code
- session ID:
✅ ACCEPT — PR #6390(read-scope 半边)
严格落在裁决划的线内,且在两处推翻了我裁决所依据的事实并如实记了下来。接受。
范围纪律:逐项核过
5 个文件全在
service-analytics内。@objectstack/formula、driver-memory/driver-mongodb、driver-sql/driver-turso一行未动 —— 裁决划的四条线一条没越。信封是模块自述的那个(READ_SCOPE_COMPILE_FAILED/ 500),没有另选。四格共用一条措辞、只有path不同,并且有一条断言把「只有 path 不同」钉死(把 path 抹成占位符后new Set(skeletons).size === 1)——#5240 落成了可执行的东西,不是一句承诺。更正了我裁决里的一处事实
我在裁决里沿用了 #6125 正文的表述,说这四格「编出合法 SQL、绑了个 NULL」。实测不是:绑定表里是 JS 的
undefined本身,本包不做任何强制转换,applyReadScope(native-sql-strategy.ts)在把?改写成$N时原样push(scopeParams[i])。这不是措辞挑剔,它让论证更强:NULL 是驱动对一个 JS
undefined的读法,所以同一次绑定在不肯猜的驱动上是一句裸Undefined binding(s)崩溃(#6050 的 LOCAL 列)。一次绑定、两种败法,取决于数据源恰好挂的是哪个驱动 —— 这正是它该在编译器处拒收、而不是在某一个消费者处修补的理由。#6125 正文的表请按此更正。没有照抄孪生实现的理由,这一点尤其对
driver-sql的同类闸有两条「必须跑在最前」的理由,PR 逐条判了它们转不转移,而不是整段搬过来:- 短路盲区 —— 不适用。本编译器
compileNode把每个$and/$or子节点.map()进各自缓冲区之后才应用布尔恒等式,所以叶子不会被兄弟节点吞掉。并且用{ $or: [{}, { d: undefined }] }/{ $and: [{}, { d: { $in: [undefined] } }] }把这句话钉住了 —— 论断配了可执行证据。 - 极性表与发射器不一致 —— 也不适用,且理由是实测的:那边闸必须抢在
$not改写之前,是因为极性表拼=== null而$ne发射器拼== null,两者对undefined有分歧;本模块nullValueSatisfiesOperator/operatorIsNullTotal/compileOperator各臂一律拼=== null,没有不一致需要抢在前面。
一个 dev 在「隔壁有现成写法」时选择先测清楚它为什么那样写、再判断转不转移,这是本 PR 最该被表扬的地方。
风险面处置得当
我在裁决里点名「最大的风险是把
null一起拒了」。PR 给了 11 行NULL_CONTROL逐字节 pin(SQL + 绑定表),覆盖$eq/$ne/$in/$nin/$between/$contains(%null%,#5526)/$null/$exists以及$not下各式。反向验证的关键读数:关掉拒收后 20 条红全部是依赖拒收的断言,NULL_CONTROL的 11 行一条没红 —— 证明对照组不依赖这道闸,是真对照组而非陪跑。$not那两行的理由也写对了:#5146 的改写经nullSafeNegationOperand到达叶子,闸放错边会改变形状而不是抛错,任何 throw 断言都抓不到。两处工程细节值得记
- 信封是按声明 withhold,不是靠认字:新消息在加入前实测
looksLikeInternalErrorLeak对四个位置均返回 false,所以它和另外十条一样按声明扣留,没有任何 message-sniffing 名单需要学第十二句话。 - 拒收清单棘轮跟着涨:
REFUSALS从 11 行/10 站点抬到 12 行/11 站点,并写明了这是 analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352 的教训(模块里部分拒收带信封,在 HTTP 边界上与全都不带无法区分)。新增一个不带信封的throw会红在这里,而不是红在生产的 HTTP 边界上。
§9 陷阱当场再中一次(第四例)
PR 如实记了:
check:type-check-debt第一次是红的,约 25 个从未碰过的包报+N,原因是新 worktree 里只 build 了service-analytics的上游闭包;pnpm build(71/71)后重跑全绿,且service-analytics自始至终不在失败名单里。今天第四个 dev 撞上同一件事 —— 这条已立单 #6371,本例可作为第四份证据附上。dev 把它写进 PR「免得下一个人重新诊断一遍」,做法正确。严重度:维持 observation,未被推翻
PR 明确未证伪裁决的定级:
read-scope-sql的 filter 仍只从ctx.getReadScope来(默认桥到plugin-security的getReadFilter),进料是 CEL 下降与库存 JSON metadata,undefined过不了 JSON,未发现能喂进来的真实生产调用方;方向确认 fail-closed。按 #5347 的先例该回报的回报了,没有为了抬高单子价值而夸大。⚠️ 但顺带发现的 #6386 是另一回事,见我在那张单上的分诊 —— 方向相反。
Generated by Claude Code
- 短路盲区 —— 不适用。本编译器
⚠️ 本单已关闭,但五个求值面里只有三个收敛了 —— 别把关闭读成解决PR #6390(
e15bf7ef7)带Fixes #6125自动关闭了本单。派工范围(read-scope-sql一格)确实完成了,所以「关闭」对工单成立;但本单标题是「undefined比较数在仓内有五种读法」,而今天仍有两种读法未动。把当前真实状态写在这里,免得后来者按标题+关闭状态误判。五面现状(PR #6390 落地后)
求值面 { d: undefined }状态 driver-sql/ turso LOCAL拒收 ✅ #6050 落地 turso REMOTE 拒收 ✅ #6050 落地 service-analyticsread-scope-sql拒收( READ_SCOPE_COMPILE_FAILED/500)✅ 本单本次 @objectstack/formula读作「键缺失」,只匹配缺键行 ⛔ 未动 driver-memory(mingo)读作 null,答['3','4']⛔ 未动 两笔未结的账,各自的活体归宿
formula半边 → driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299(已标needs-user-decision,等维护者三问)。裁决理由:formula 把undefined读作「这个键不存在」是第三种语义、不是第三个 bug 拼写,照 drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 直接改成拒收会顺手替 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 的「键缺失 vs 值为 null」拍板。⚠️ driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 不裁,这半边就永远派不出去 —— 这是本单关闭后唯一还在挡路的依赖。driver-memory半边 → [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499(投入冻结令,解冻时结账)。我已把这条债务的明细补写到 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 上,因为一条关闭的 finding 不该是冻结债务的永久归宿。
顺带记:本包还有第二扇门,不在本单那张五面表里
#6390 的实施带出 #6386:
service-analytics有两扇独立的门,本单只测了平台编译的 read scope(500 那扇),调用方自己写的where走filter-normalizer.ts(INVALID_FILTER/400)——那扇门今天把undefined值的键整个丢掉,方向是加宽。所以本单正文那张表把本包记作一行是不完整的,实际是六、七种读法而非五种。另有 #6387(同一个 read-scope 编译器的
$null/$exists按真值性读比较数,坏形状过得了 JSON,已判 p1+security)。⛔ 若将来有人要「收尾本单」,请从 #5299 与 #5499 入手,不要重开本单。
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026 - added a commit that references this issue
on Aug 29, 2026
出自 #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