Skip to content

fix(driver-memory): refuse the filters the live query path cannot evaluate, compile the one it must (#5324, #5328) - #5349

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-5324-memory-filter-refuse-what-it-cannot-evaluate
Aug 5, 2026
Merged

fix(driver-memory): refuse the filters the live query path cannot evaluate, compile the one it must (#5324, #5328)#5349
os-zhuang merged 6 commits into
mainfrom
claude/issue-5324-memory-filter-refuse-what-it-cannot-evaluate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5324
Fixes #5328

两条缺陷是同一个形状

normalizeFilterCondition(memory-driver.ts)对它无法求值的东西不拒收,而是二选一地静默处理 —— 两条 arm 在同一个 switch 里:

修复前 driver-sql 同输入
未知 $op(default: result[op] = val 通用透传) 原样交给 mingo → 抛无 code / 无 statusMingoError,逃出 ADR-0112 信封(500 形状) INVALID_FILTER / 400
形状错误的 $between(arm 写成有条件的) 整条约束消失,find 返回 [] INVALID_FILTER / 400

一个静默变 500、一个静默变空结果集 —— 方向相反,病因相同:该拒收的没拒收#4436 / #3948 已确立「无法编译的过滤器必须响亮拒收,不能被跳过或改写」,#5240 又统一了拒收的信封;本 PR 把这条补进 driver-memory 的实时查询路径,复用同包已有的 filter-refusal.ts(没有另造第三套)。

未知 $op 通用来写,不是加一条 $not 分支 —— 透传是通用的,只补 $not 会把 {name:{$sounds_like:'x'}} 那一半留在 500 形状里。

同批一并收进来的、同一形状的两条:

$not:方向相反的那一条 —— 实现,不是拒收

#5324 正文列了两条路且明确不代裁决。证据把路选了:$notspec 声明的组合子(LOGICAL_OPERATORS),cel-to-filter 把每一条 CEL !expr 的 RLS read scope 编译成 {$not:{…}},driver-sql 编译它、driver-mongodb 翻译它、本包的参考匹配器求值它,而门禁表 FILTER_LOGIC_CASES 里有一条用例要求它。拒收会让 driver-memory 成为唯一跑不了一个已声明算子的后端,并且让一致性表带上一条永远不可能通过的用例。

「拒收你无法求值的东西」有一条配套子句:契约声明了的,你就得求值。 所以未声明的一律拒收,已声明的这一条补上实现。

改法是 $nor 单操作数 —— MongoDB(因而 mingo)没有文档级 $not,$nor: [X] 就是它的文档级否定,与 driver-mongodb 因同一原因做的改写(#4405)一致。它天然 NULL-safe:内层条件对 null / 缺字段的行不成立,$nor 因而收下该行 —— 正是 #5146 裁定为 canonical、并据此重写了 driver-sql 的那套语义。这一点不是推理,是逐条实测的(见下表)。

⚠️ 门禁的洞:一致性表此前没穿过真驱动

这是本 PR 比修两条 arm 更要紧的部分。

FILTER_LOGIC_CASES 是五个过滤后端共用的唯一标准(#3774)。其中四家是穿过真正执行查询的代码跑的:driver-sql 编译成 SQL、driver-sqlite-wasm 在 sql.js 上执行、driver-mongodb 翻译并执行、service-analytics 下沉成 read-scope SQL。

driver-memory 只经 memory-matcher 跑(memory-matcher-or-semantics.test.ts)。而驱动根本不调用 match() —— 它只从那个模块 import 了 getValueByPath,过滤交给 mingo。所以这个后端在一致性表上的那一半,量的是参考实现,用户真正跑的那一半一次都没对着标准执行过

代价不是假设的:表里 $not ANDs with its sibling keys inside a branch 这条自存在起一直是绿的,而同一个 filter 走 InMemoryDriver.find 会抛 unknown top level operator: $not一个给不能跑的算子发绿灯的门禁,比没有门禁更糟 —— 它报告了自己并不具备的覆盖,正是 Prime Directive #10 说的 declared ≠ enforced。

本 PR 按其余三家的做法补上:新增 memory-driver-filter-logic-conformance.test.ts,把整张表穿过 InMemoryDriver.findmemory-matcher-or-semantics.test.ts 保留 —— 匹配器仍是参考求值器,两个面都被同一张表约束,才让「这个包有两个过滤面」成为一句可以核查的话。表内另加一条断言:两个面对整张表逐条给出相同答案

两个过滤面:一个门,不是两份拷贝

#5240 让两个面拒收同一形状的办法是把同一个检查写了两遍。那仍是一条规则的两个实现,而本单实测的输入证明了两份实现能飘多远:同一个形状错误的 $between,实时路径答「没有行」,参考匹配器答「所有行」 —— 一个包、一个 filter、两个互相矛盾的静默答案,而且都不是区间。

所以规则现在只在一个函数里:filter-refusal.tsassertFilterConditionShape,两个面在求值前都调用它。它先走完整棵树再求值,理由与 #5240 写下的两条一样(且现在承担得更多):

  1. 两个面都短路(every/some、mingo 自己的谓词组合),求值中途抛出的拒收会取决于当时那条记录触发或不触发 —— 一条写坏的权限规则必须对每条记录都被拒,不能看数据的运气;
  2. 它对布尔单位元也不短路:{$or:[{a:{}},{}]} 里那个 TRUE 析取项会让发射器提前返回,永远走不到畸形的那一支 —— driver-sql 的 reduceFilterNode 把这叫「a gate conditional on evaluation order」。

现场核对(STALE-PREMISE)

worktree 基于 origin/main = c7406b0ec(已含 #5158 / PR #5329 的数组方言删除)。逐条核对两个 issue 引用的现场:

issue 的说法 现场 处理
#5324:default: 是通用透传,$not 只是其一 ✅ 成立 按通用写
#5324:mingo 对文档级 $not 抛无 code/statusMingoError ✅ 实测复现 unknown top level operator: $not | code=undefined status=undefined
#5328:$between arm 有条件写入,find 返回 [] ✅ 成立 补 else + 上移到门
#5328:数组路径的守卫已被 #5158 删除,对象路径从未有守卫 ✅ 成立
#5328:用例被钉在 resolves.toEqual([]) 并留注释指向本单 ✅ 找到 已翻回 rejects + 断言信封,注释一并移除
#5324 评论:memory-filter-ast-vocabulary.test.ts 的用例按现状放宽为 .rejects.toThrow() ✅ 找到 已收紧回 code/status 断言
#5240{$not:{}} 用例钉着 unknown top level operator ⚠️ issue 未提 该 pin 正是 #5324 的症状。现改为断言 [](#5134 的 NOT TRUE ≡ FALSE),两个面都断言,与 driver-sql 一致
#5324:$notFILTER_LOGIC_CASES 里(filter-logic-conformance.ts:172) ✅ 成立 这条决定了 $not 必须实现而非拒收

一处与 issue 表述不同:#5328 说匹配器侧此前也是「匹配 0 行」。实测不是 —— 匹配器的 Array.isArray(target) && … 守卫跳过比较,答的是匹配全部行。两个面的静默答案是相反的,这比 issue 记的更糟,也是把门收成一份实现的直接理由。

词汇表:来自 spec,不是手抄

SUPPORTED_FIELD_OPERATORS = spec 的 FILTER_OPERATORS + $regex + $options。不在这里手写一份列表 —— 「A private alias list here is what let this driver and driver-sql accept different vocabularies」(#3948,同一文件 convertConditionToMongo 上方的注释)。两个额外项都是既有行为而非新能力:

  • $regex —— 不在 spec 列表里但真的有生产者:plugin-auth 的 ObjectQL adapter 为 contains 搜索产出 {field:{$regex:value}};driver-sql 编译它,objectql 的 having 允许它,本包匹配器实现它。拒收会打断一个活着的生产者。
  • $options —— $regex 的旗标伴随键,匹配器读它(new RegExp(target, condition.$options)),objectql 的 having 同理跳过它。

其余一律拒收,包括此前意外放行的 mingo 算子($elemMatch$size$type$mod$where$expr、字段级 $not)—— 都不在 Filter Protocol 里,匹配器都不实现,driver-sql 全部拒收。

刻意不收紧的三处(收紧会让本驱动发明一个比它必须与之一致的后端更严的契约):无 $ 键的字段规格({author:{name:'x'}},读作深比较 —— 与 driver-mongodb 一致)、$between 数组的成员类型(driver-sql 只查元数,#5041 实测过成员情形并有意留下)、LIKE 族的字符串化比较值(同上,且方向 fail-closed)。

可观察的行为变更

变更集已写明。原本静默返回空结果集 / 原本抛 500 形状的输入,现在一律 INVALID_FILTER / 400,消息带算子名、字段名与路径(filter.$or[1].$and[0].stage)。一个因此开始收到 400 的查询,此前就已经是坏的 —— 它对同一输入要么返回空集要么抛无信封的 500,而 driver-sql 一直在拒收它。反方向:带否定 scope 的查询从抛异常变为正常返回

测试

测试 覆盖 结果
memory-driver-filter-logic-conformance.test.ts(新) 门禁洞:FILTER_LOGIC_CASES 全表穿透 InMemoryDriver.find + fixture 完整性 + 两面逐条同答案 19 passed
memory-filter-vocabulary-refusal.test.ts(新) 8 个伪算子($sounds_like/$elemMatch/$size/$type/$mod/$where/$expr/字段级 $not)、4 个节点级未声明 $ 键、6 个畸形 $between(非数组/一元/三元/对象/字符串/null)、7 个畸形组合子操作数、21 条合法形状不变、位置信息、拒收与记录无关 —— 每条都跑两个面并断言两面消息逐字相同 41 passed
memory-driver-document-not.test.ts(新) $not 在每个位置(顶层/$or 内/$and 内/带兄弟键/两层嵌套/CEL RLS 形状)+ #5146 整张 canon(NULLED 与 MISSING 两种读法)+ #5134 布尔单位元 + 三条已知两面语义分叉(钉住,指向 #5299) 20 passed
memory-filter-ast-vocabulary.test.ts(改) 两条按现状放宽的 pin 收紧回信封断言 passed
memory-empty-field-constraint.test.ts(改) {$not:{}} 由「抛 MingoError」改为「两个面都答 [] passed

信封与 driver-sql 的一致性:code === 'INVALID_FILTER'status === 400 逐字断言;消息首句与 driver-sql 逐字一致(Unsupported filter operator "$x" on field "y". / Operator "$between" on field "y" requires a [min, max] value array.),作为字面量钉在测试里(driver-memory 不依赖 driver-sql,也不该依赖 —— 与 memory-filter-refusal-envelope.test.ts 钉信封的做法相同)。supported 列表由词汇表 Set 生成,故顺序与 driver-sql 略异、内容随实现自动同步。

反向验证(把改动 stash 掉)

只 stash 三个源文件(filter-refusal.ts / memory-driver.ts / memory-matcher.ts),测试原样保留:

Test Files  5 failed (5)
     Tests  43 failed | 116 passed (159)

失败原文(节选):

FAIL  memory-driver-document-not.test.ts > … > at the top level
Error: unknown top level operator: $not

FAIL  memory-driver-filter-logic-conformance.test.ts > … > $not ANDs with its sibling keys inside a branch
FAIL  memory-driver-filter-logic-conformance.test.ts > … > both filter faces answer the whole table identically
Error: unknown top level operator: $not

FAIL  memory-filter-vocabulary-refusal.test.ts > … > refuses "$where" in a field constraint, on both faces
AssertionError: expected undefined to be 'INVALID_FILTER' // Object.is equality

FAIL  memory-filter-vocabulary-refusal.test.ts > … > the two faces used to answer a malformed $between OPPOSITELY
Error: expected the live query path to refuse this filter, but it answered

FAIL  memory-filter-ast-vocabulary.test.ts > … > refuses a malformed between instead of emitting no predicate — #5328
Error: expected the driver to refuse this filter, but it resolved

expected undefined to be 'INVALID_FILTER' 就是丢失的信封本身。

连带面回归(driver-memory 是测试与 dev 的默认驱动)

结果
@objectstack/driver-memory 15 files / 404 passed
@objectstack/objectql 118 files / 1908 passed
@objectstack/plugin-security 34 files / 731 passed
@objectstack/runtime 90 files / 1333 passed
@objectstack/service-datasource 11 files / 204 passed
@objectstack/cli 70 files / 633 passed
@objectstack/plugin-dev 2 files / 30 passed

合计 5243 passed, 0 failed, 0 skipped@objectstack/driver-memory typecheck(tsc --noEmit)干净,eslint --max-warnings=0 干净。

本地跳过 / CI 才跑的盲区:上述七个包没有任何 skipped 用例(vitest 未报告 skip 计数)。依赖清点:仓内声明依赖 @objectstack/driver-memory 的包共 7 个(runtimeservice-datasourceqa/dogfoodcliplugin-devexamples/embed-objectql、自身),上表覆盖了其中有测试脚本的全部。driver-mongodb 的一致性套件需要真实 mongod,本地未跑 —— 本 PR 不改该包,故不主张覆盖它;但下面第 2 条范围外发现正是关于它的,实现方须先实测。

一处环境注意:首轮 typecheck / plugin-security test 因依赖包 dist 未构建而失败(Cannot find module '@objectstack/spec/data'Failed to resolve entry for package "@objectstack/formula")。这是 AGENTS.md §9 记录的陈旧构建态陷阱,不是本改动引起 —— 未改动的文件同样报错;pnpm --filter "…" build 后全部转绿。

范围外发现(均已立单,unassigned,未在本 PR 修)

按 PD #10。四条都先搜过既有 open issue 再落单;第五条命中既有单,按「attach, don't scatter」改为评论。

  1. driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not 整条丢,$between/$startsWith/$null/$regex 因无 cube 映射而丢 —— 聚合结果被放大 #5345 — 同包第三个过滤面 memory-analytics.tsflattenFilterCondition 静默丢弃大半个 filter:$or / $not 整条 continue,$between / $startsWith / $endsWith / $null / $regex 因 cube 无映射而丢。方向是放大(少过滤 = 多返回行),且 $not 正是 RLS scope 的形状。本 PR 的门禁穿透只覆盖 find 面。
  2. driver-mongodb 带着 #5328 的同一条缝:形状错误的 $between / 非数组 $and$or / 非对象 $not 全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346 — driver-mongodb 带着 driver-memory 对形状错误的 $between 静默不发谓词(匹配 0 行),driver-sql 同一过滤器抛错 —— 一个过滤器两个答案 #5328 的孪生代码:$between 同款有条件写入、$and/$or 非数组与 $not 非对象被静默吞掉;其未知算子拒收无 ADR-0112 信封且消息带 [mongodb] 前缀(analytics /query 未做 cube 存在性校验,未注册名直达驱动当表名;且错误路径原样回显驱动 SQL(#3770 同类,另一子系统) #3867 已在 SQL 侧删除同类前缀)。逐行代码对读,未实测(需 mongod),已在单里写明。
  3. $null 的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347$null 比较值非布尔时,driver-sql 与 driver-memory 给出完全相反的答案(实测:{stage:{$null:'yes'}} → SQL [](IS NULL)、memory ["1"](IS NOT NULL))。两边默认分支挂在相反一侧(=== false vs === true)。这需要先裁决 canonical 才能改,单里列了三条路与推荐。本 PR 刻意未收紧 $null 的比较值类型,以免把分叉换个位置。
  4. driver-sql 把**文档级**未声明 $op({$where:…}{$nor:[…]})当成列名编译,静默返回空结果集 —— #5324 的文档级一半在 SQL 侧还在 #5348 — driver-sql 把文档级未声明 $op({$where:…}{$nor:[…]})当成列名编译,静默返回空结果集(实测);而它的字段级同类输入一直是正确的 INVALID_FILTER / 400。四家后端里现在只剩它。
  5. driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 的补充评论 — 本单已记 driver-memory ↔ formula 在 $notContains/$exists/$nin 上的三处分叉;实测补充:同样这三个算子在 driver-memory 自己的两个面之间也已分叉($exists$nin 甚至不带任何 $not)。属语义而非形状,本 PR 的门不碰,现状已按实测钉在 memory-driver-document-not.test.ts 并注释指向 driver-memory 与 formula 对「字段没有值」给出三处不同答案:$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299

未做的清点:$in/$nin 收到非数组比较值时两个后端的行为、$regex 族形状错误时的行为 —— 都没实测,故未立单。

硬禁区遵守

packages/spec/** 未改一行(FILTER_LOGIC_CASES内容#5239,本 PR 只补 driver-memory 侧的穿透);packages/services/service-analytics/**(#5325 在飞)、packages/plugins/driver-sql/**packages/formula/** 均只读作参照;content/docs/releases/** 未改,变更记录只走 .changeset/*.md;无生成物手改。


🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7


Generated by Claude Code

…luate, compile the one it must (#5324, #5328)

`normalizeFilterCondition` had two ways of not refusing a filter it could not
compile, in one `switch`:

- `default: result[op] = val` handed every unrecognised `$op` to mingo, which
  answered with a `MingoError` carrying no `code` and no `status` — outside the
  ADR-0112 envelope, so a client mistake was served as a 500-shaped body (#5324);
- the `$between` arm was written conditionally, so a comparand that was not a
  two-element array skipped it, the constraint vanished, and `find` returned `[]`
  (#5328).

Opposite symptoms, one cause: the shape that could not be evaluated was not
refused. #3948 and #4436 settled that an uncompilable filter is a loud refusal
rather than a silent answer; this brings that rule to driver-memory's live
query path, reusing `filter-refusal.ts`'s existing `INVALID_FILTER` / 400.

`$not` goes the other way. It is a declared combinator (`LOGICAL_OPERATORS`),
`cel-to-filter` emits it for a CEL `!expr` RLS scope, and driver-sql,
driver-mongodb and this package's own matcher all implement it — but MongoDB
has no document-level `$not`, so passing it through meant every negated scope
threw. It is compiled to `$nor` with one operand (driver-mongodb's rewrite,
#4405), which is NULL-safe by construction and so lands on the #5146 canon.

Both filter faces now share ONE shape gate rather than a copy of one check.
They had drifted: a malformed `$between` returned NO rows from the live path
and EVERY row from the reference matcher.

Closes the conformance gap that hid this: `FILTER_LOGIC_CASES` reached this
backend through the reference matcher only, which the driver does not call, so
the table's `$not` case was green while the same filter through
`InMemoryDriver.find` threw. It now runs through the real driver too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 12:25am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-memory.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory)
  • content/docs/deployment/vercel.mdx (via @objectstack/driver-memory)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-memory)
  • content/docs/permissions/authentication.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/index.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-memory)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-memory)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-memory)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 4 commits August 5, 2026 00:08
`assertFieldConstraintShape` had inlined `keys.length === 0`, which left
`isEmptyFieldConstraint` — and the #5240 reasoning in its doc comment about what
does NOT count as one (a `Date` enumerates to nothing but is a comparand) —
attached to nothing. Behaviour-identical: the caller has already established the
spec is a filter node, which is the predicate's other half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
…'s wording

A malformed `$and`/`$or` operand and a malformed `$not` operand each had two
messages: the gate's `filterNodeListExpectedError` / `filterNodeExpectedError`,
and a second, differently-worded `unknownLogicalOperatorError` in the
translator's unreachable floor. #5240's rule is one condition, one wording — and
a floor that says something different from the gate above it is exactly what a
reader would use to conclude they had hit a different problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
…one operator the allowlist would have leaked

Measured after the vocabulary landed: `{ field: { $options: 'i' } }` still
escaped the ADR-0112 envelope on the live path (`unknown query operator
$options`, no code, no status) while the reference matcher ignored it and
matched EVERY row. #5324's exact shape, surviving for a single operator —
because `$options` is in the vocabulary as a MODIFIER of `$regex`, not a
predicate, so allowlisting the key without requiring its partner left the hole
open for it alone.

Refused when no `$regex` accompanies it, on both faces, and pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7

Copy link
Copy Markdown
Contributor Author

开 PR 后的三个追加提交 —— 其中一个补掉了我自己开的一个洞

1. ab2425183$options 没有 $regex 时也拒收(这一条是缺陷修复,不是清理)

装好词汇表之后我又跑了一轮比较值种类的对读,发现新词汇表自己漏了一个:

{ stage: { $options: 'i' } }
  live(mingo)  =>  THROW  'unknown query operator $options'  code=undefined  status=undefined
  ref(matcher) =>  OK ["1"]        ← 忽略它 = 匹配全部行

也就是 #5324 的形状为单独一个算子存活了下来,而且是我把它放进白名单才存活的:$options 在词汇表里是 $regex修饰符而不是谓词,只放行键、不要求它的搭档,就正好给它留了门。两个面还照例给出相反答案(一个抛无信封的错、一个匹配全表)。

现在:没有 $regex 陪同的 $options 在两个面上都拒收,并有 pin。这条不改的话,本 PR 会带着一个自己造的、与它要修的完全同形的洞合入。

{ stage: { $regex: 'W', $options: 'i' } } 不受影响(已在合法形状表里)。

2. 05a27ec7e — 转换器的兜底 throw 改用门的措辞

畸形 $and/$or 操作数与畸形 $not 操作数原本有两套措辞:门里的 filterNodeListExpectedError / filterNodeExpectedError,和转换器不可达兜底里的另一句 unknownLogicalOperatorError#5240 的规矩是一个条件一种措辞;兜底说得和它上面的门不一样,正是读到两条消息的人会据以认为自己撞上了两个不同问题的东西。

3. c49907cbd — 零算子检查用回自己的谓词

assertFieldConstraintShape 里内联了 keys.length === 0,把 isEmptyFieldConstraint 连同它 doc 注释里 #5240 的推理(Date 枚举为空但它是比较值)一起变成了没人调用的死代码。行为完全相同 —— 调用方已经确认过 spec 是 filter node,那正是这个谓词的另一半。


三条之后:typecheck 干净、eslint --max-warnings=0 干净、@objectstack/driver-memory 405 passed(比 PR 正文表里的 404 多的一条就是 $options 的 pin)。

另外新立一单(范围外发现,第 6 条)

#5354 — 同一轮对读还测出两个面对比较值的种类给出相反答案,两条方向相反,且都与 null 无关(那一族是 #5299):

{ stage: /WON/i }    live=["1"]  ref=[]      // mingo 当正则;匹配器把 RegExp 掉进结构相等分支,JSON.stringify(/re/) 是 {}
{ stage: ['won'] }   live=[]     ref=["1"]   // 匹配器 `'won' == ['won']` 靠 toString() 意外为真

语义分叉,本 PR 的门明确「decides SHAPE, never MEANING」,故不碰;已按实测立单。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants