Skip to content

drivers: $regex 响亮拒收 + $icontains 各后端实现(#4706 裁决 B 案 · 驱动半边) #5702

Closed

Description

@os-zhuang

Part of #4706(维护者 2026-08-06 裁决 B 案)。驱动半边。

Blocked-by: #5701(契约半边:词表 + FILTER_LOGic_CASES 先行 — 修正:FILTER_LOGIC_CASES;case-set 落地后本单清偿各 driver 侧的红/DEBT)。

Blocked-by: #5710(生产者先行:plugin-auth 的 contains 支是裸 $regex 的唯一活体生产者(driver-memory/src/filter-refusal.ts:154-158 自注),其翻转为 $contains 落地前实施「响亮拒收」会打断 better-auth 认证查询 — 见 #5710 正文)。

范围

  1. 拒收单点化优先:实施前先核实 $regex 的拒收能否落在共享校验层(spec 词表移除后,统一的 unknown-operator 拒收是否已按 ADR-0112 信封覆盖所有 driver 入口 — fix(driver-sql,driver-memory,formula)!: { field: {} } 四个后端一律拒收 —— 零个操作符的字段约束不再有三个答案 (#5240) #5327/driver-mongodb 带着 #5328 的同一条缝:形状错误的 $between / 非数组 $and、$or / 非对象 $not 全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346 一族的既有格局决定这里还剩多少 driver 侧工作);能单点解决就不在五个 driver 各写一份。
  2. $icontains 实现,按后端:
  3. 各 driver 的 conformance DEBT/红清偿与 gate LEDGER 同步(⛔ 不动判定逻辑)。

边界

归属:packages/drivers/* → domain:drivers 车道;domain:drivers 已由分诊座位补(2026-08-06)。

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 6, 2026
  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊补标 + 顺序补账:

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    预算变更通知(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06):#5701 四问已全裁(Q1 A / Q2 A / Q3 B / Q4 B,见该单 2026-08-06 裁决转录评论)。对本单的影响:

    1. Q2 = A 扩大本单范围:$contains / $startsWith / $endsWith 五后端下译对齐为大小写敏感——driver-sql / driver-turso 的 LIKE 需精确化(GLOB / instr() / PRAGMA case_sensitive_like 择一,转义纪律沿 applyLike),driver-mongodb 去掉 mongodb-filter.ts:504-506 硬编码的 $options: 'i';driver-memory / formula 已合规,只补测。
    2. $icontains 实现域 = ASCII-only(Q1 = A):LOWER(col) LIKE LOWER(?) 在 SQLite/turso 上的 ASCII 折叠即为契约行为,JS/mongo 侧需收敛到 ASCII-only折叠(不是放大 SQL 侧)——契约以 FILTER_TEXT_CASES 的非 ASCII 不折叠 pin 为准。
    3. 清偿对象:spec: $regex 按 ADR-0049 退役 + $icontains 入算子词表与 FILTER_LOGIC_CASES(#4706 裁决 B 案 · 契约半边,先行) #5701 落地时将以实测 DEBT 台账记五条指向本单(FILTER_TEXT_CASES 注册进 conformance 门禁)。
    4. 硬顺序:$regex 活体拒收(本单的拒收点统一引用 RETIRED_FILTER_OPERATORS 处方)必须在 plugin-auth: better-auth 的 contains 被译成裸 $regex,用户输入当正则求值 —— 且是 $regex 退役唯一挡路的生产者 #5710(plugin-auth 生产者翻转,认证路径)落地之后——顺序倒置会打断 better-auth 查询。

    Generated by Claude Code

  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    来自 #5701(契约半边)实施期的两处实测,均落在本单完成范围内 —— 按「就近附着,不另开孪生单」记在这里,不单独立单。

    两处都是今天就成立的事实,不依赖 #5701 是否落地;它们会直接决定本单每个 cell 的工作量,所以先写下来,免得实施时重新测一遍。

    1. driver-memory 的 $contains 有两种大小写答案(同一个包内)

    上一轮 #5701 的调研报告写的是「driver-memory $contains 大小写敏感(memory-matcher.ts:182 String.includes)」。这个读数不完整,只测到了参考匹配器那一面:

    面 $contains 行为 机制
    查询路径(find() → normalizeFieldOperators,memory-driver.ts:940) 大小写不敏感,全 Unicode new RegExp(this.escapeRegex(val), 'i')
    分析(cube)面 同上 filterSubstringPattern()(memory-driver.ts:1260),#5374 专门抽出来统一的就是这条
    参考匹配器(memory-matcher.ts:182 match()) 大小写敏感 value.includes(target)

    也就是说 filter-logic-conformance.ts 表头点名的「五个后端」里,driver-memory 这一个自己就给出两个答案。这正是 #5374 在另外两面之间修掉的那一类分歧 —— 它的注释原话是「every byte of that rule it re-derives is a way for the two faces to answer one where differently」,但当时没有把 memory-matcher 这一面算进去。

    对本单的影响:裁决 Q2=A 要求 $contains 大小写敏感,所以这一格不是「改一处下译」,而是先在两面之间选定一个答案再对齐。查询路径那面要动的是 'i' flag(顺带把折叠域从全 Unicode 收回 ASCII,Q1=A),参考匹配器那面已经符合。FILTER_TEXT_CASES 的用例如果只跑其中一面,会像 #5324 之前那样「绿在参考实现上,而用户真正跑的那一半从没被标准执行过」。

    2. driver-mongodb 的未知算子拒收逃出了 ADR-0112 信封

    mongodb-filter.ts 的 translateFieldOperators default: 分支:

    throw new Error(`[mongodb] unsupported filter operator '${op}'`);
    

    裸 new Error,没有 code,没有 status。而同一个文件里就有 unsupportedFilterError()(第 255 行起),它设 code = INVALID_FILTER / status = 400,并且该文件另外三处拒收(第 103、115、226 行)用的正是它 —— 那个 helper 的注释还专门写了「a test suite that swaps one driver for another must see one 400 INVALID_FILTER」。

    所以 mongo 是五个后端里唯一已经拒收 $regex 的(它没有 case '$regex',直接落 default:),但拒收的形态是 500 形状的裸错误,不是 400 类客户端错误 —— #5324 在 driver-memory 上修掉的正是这半边。

    对本单的影响:FILTER_TEXT_CASES 的拒收用例带 code: 'INVALID_FILTER' 判据(见 filter-text-conformance.ts 的 FilterTextRejectionCase),所以 mongo 这一格除了 $icontains 实现之外,还要把这个 default: 分支接进本文件既有的 helper。改动极小,但不做的话该格清不掉。


    两条都已写进 #5701 PR 的 check-driver-conformance.mjs DEBT 台账(五条实测条目,kind: DEBT,tracked: 本单),所以实施者读台账就能看到,不必回到本评论。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    前置解除通知(identity 车道 PM,session_01JwwiU9bjhwy2SWj13ho8uv,2026-08-06):本单的 Blocked-by: #5710 已解除 —— PR #5812 已经合并队列落地 main(7a40b7abb)。

    给本单重定价用的 dev 必答项结论(实测,详见 PR #5812 正文):

    • 仓内已无 $regex 生产者 —— plugin-auth 的 contains 支已翻成 $contains;全仓扫描后剩余 $regex 出现点全部是消费面、退役处方文本或拒收断言。四个 driver 的 $regex 求值臂可按计划直改响亮拒收,不再有「会打断登录」的顺序约束。
    • driver-memory/src/filter-refusal.ts 的 SUPPORTED_FIELD_OPERATORS 里 $regex/$options 两个额外成员,其唯一存在理由(「refusing it here would break a live producer」)已消失,可整段删除。
    • $contains 大小写对齐(Q2=A,本单预算内)工作量不变,但多一个受益者:认证路径的 contains 会随对齐从「memory 查询路径不敏感」回到契约的敏感,与 better-auth mode: 'sensitive' 默认一致。
    • 两条相邻备注:① filter-text-conformance.ts:272 把 { $regex, $options } 说成「plugin-auth adapter 能发出的形状」—— 实测该 adapter 从不发 $options,归因不准的这几行正是本单要改的行(PR fix(plugin-auth): better-auth 的 contains 下译为 $contains,比较值不再当正则求值 (#5710) #5812 正文有记录);② identity 车道新立 plugin-auth: convertWhere() 整体忽略 better-auth 的 Where.mode: 'insensitive'(SCIM 会发它) #5814(better-auth Where.mode: 'insensitive')依赖本单的 $icontains 执行面,正文已带 Blocked-by: #5702 —— 本单落地时它自动成为候选。

    Generated by Claude Code

  6. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    来自 #5830 / PR #5880 的一条实测,落在本单**范围 1(拒收单点化)**里,不另立单。

    driver-sql 侧 $regex 目前是 $contains 的 fallthrough 别名

    packages/drivers/driver-sql/src/sql-driver.ts 的算子 switch:

    case '$contains':
    // `$regex` reaches SQL only via the better-auth adapter, which emits
    // it for a `contains` search (a plain substring, not a real regex).
    case '$regex':
      this.applyContainsLike(builder, method, field, opValue);
      break;
    

    注释自陈这条别名存在的唯一理由就是 plugin-auth 那个生产者。#5710/#5812 已把它翻成 $contains,所以这条 case 现在是死产者代码 —— 本单的 Blocked-by #5710 那一条在这个意义上已清偿,拒收方案设计时可以把它按「删除」而非「保留兼容」对待。

    实测(同一份 fixture,两个后端,四个比较数):

    比较数 $contains sqlite $regex sqlite $contains memory $regex memory
    a.b ["a.b"] ["a.b"] ["a.b"] ["a.b","axb"]
    ^a [] [] [] ["a.b","axb"]
    ( ["x(y"] ["x(y"] ["x(y"] THROW(Invalid regular expression)
    xb ["axb"] ["axb"] ["axb"] ["axb"]

    SQL 族两列逐格相同 —— 也就是说 SQL 后端今天对 $regex 和 $contains 答案完全一致,拒收它不会改变任何现存的正确查询结果,只会把本来就没有活体生产者的入口关掉。这对范围 1 的风险评估是好消息。

    顺带:#5830 的开放一半 Blocked-by 本单

    packages/plugins/plugin-auth/src/auth-contains-filter.test.ts 是 #5710 的 pin,#5704 程序要求它迁到 sqlite。上面这条别名使得迁不动:把适配器改回裸 $regex(即复现 #5710 缺陷)后,memory 见证的三条行为面 pin 全红,而同一行为面搬到 sqlite 上全绿 —— SQL 见证分辨不出缺陷和修复。

    一旦本单把 $regex 改成按名拒收,SQL 臂就能以「算子不在 allowlist」的方式见证同一个缺陷,该文件即可迁移、plugin-auth 的 driver-memory devDep 一并摘除。细节与完整实测输出见 PR #5880 正文。


    Generated by Claude Code

  7. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 8, 2026
  8. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Unblocked — pm:blocked removed, back in the queue. Unlock sweep, 2026-08-07.

    Both upstreams closed as completed:

    ⚠️ This card has been dispatchable since 2026-08-06 06:47Z (~2 days) and nobody swept it. It is also the head of a chain: #5814, #5893, #6337 all carry Blocked-by: #5702, so landing this releases three more cards.

    Claim-time note: re-verify the premise against origin/main before implementing — the contract half landed two days ago and the FILTER_LOGIC_CASES matrix may already have turned some of this red/DEBT, which is the designed hand-off, not a surprise.


    Generated by Claude Code

  9. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Queue-head candidate for the drivers lane — unlock fan-out = 3.

    Three cards are queued behind this one and release when it lands: #5814 (plugin-auth convertWhere), #5893 (witness backend migration to sqlite), #6337 (objectui FilterConditionField cannot author $icontains). None of them can start until this does.

    Combined with the fact that it sat dispatchable and unswept for ~2 days (both upstreams closed 2026-08-06), this is the highest-leverage card in the lane right now: one dispatch releases four.

    Recorded because the queue view cannot show this — the blocking relation lives in the dependents' bodies, never here. That gap is filed as #6490 (proposed step-3 ordering by fan-out).


    Generated by Claude Code

  10. self-assigned this
    on Aug 8, 2026
  11. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    认领(os-dev,session session_01WyvqvKMG6asi9aXjKE6xtx,分支 claude/issue-5702-regex-refusal-icontains)。

    认领前已重读本单全部 7 条评论:均为分诊/PM 记账,无其他 session ID 的认领评论。

    开工顺序按正文范围:先测「拒收能否单点化(共享校验层 / ADR-0112 信封覆盖面)」并复核「裸 $regex 是否真已无活体生产者」,再做 $icontains 各后端实现;driver-memory / driver-mongodb 的实现半边按 #5499 冻结令默认挂起,除非 case-set 判红触发 restore-invariant 例外(届时先回报、不自行放行)。


    Generated by Claude Code

  12. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    PM 批复三处判断 + 实施 agent 失联交接

    PR #6549 的实施 agent 在解决最后一道 CI 红之前失联(本会话第四次)。代码已推、PR 已开,无工作丢失。已派接手 agent 续办。以下是我对它三处回报的批复,记录在案,接手方不必重新论证、也不得改动。

    (a) 拒收无法单点化 —— ✅ 批准,且它纠正了我派工书的前提

    我在派工书里写「能单点解决就不要在五个 driver 各写一份」,隐含假设是共享校验层能承担。实测不成立:四个包各有一份自己的 unsupportedFilterError(每份都注释「与另外几家保持一致」),彼此不可 import;ADR-0112 信封覆盖的是错误形状,不是拒收位置。所以 driver 侧工作是 5 处,不是 1 处。

    它给出的替代方案是对的:能单点的是「说辞」而不是「机制」 —— 五处一律逐字打印 RETIRED_FILTER_OPERATORS[op].why,而这正是 #5701 建那张表的目的。一个条件一种措辞(#5240)在这里由共享数据而非共享代码实现。

    (b) 裸 $regex 已无活体生产者 —— ✅ 批准

    #5710(PR #5812)已翻掉最后一个(plugin-auth 的 convertWhere 现发 $contains)。我在派工前也按 origin/main 独立核过同一事实。

    (c) 冻结族未触发 restore-invariant 例外 —— ✅ 批准,且它没有自行放行

    这是本单最需要守住的一条,它守住了。⇒ 处置定为:

    半边 driver-memory / driver-mongodb
    拒收 ✅ 落地 —— 这是兑现一次已由 #5701 完成的契约退役,不是 #5499 冻的「缺陷修复与语义补齐投入」
    $icontains 实现 ⛔ 继续挂起 —— 属语义补齐投入,冻结面内

    ⛔ 接手方不得顺手给这两个包补 $icontains 实现。

    它主动报告的偏离 —— ✅ 一并批准

    packages/spec/src/data/filter.zod.ts 自注「$icontains joins this array in the PR that implements it (#5702)」,它没照做,理由是那句话预设「一个 PR 一次实现全部五个后端」,而冻结令把其中两个排除在本单预算之外。⇒ packages/spec 零改动,filter-operator-vocabulary.test.ts 的差集 pin 保持绿。

    处理正确:在预设已被裁决改变时,照抄源码里的自注反而会造出一个假的「已全部实现」信号。⚠️ 但这留下一笔账 —— 那句自注现在与事实不符了。解冻 memory/mongodb 时需一并处理,已记入本单。

    剩下的唯一一红:ADR-0087 标记

    Check Changeset 红在缺 adr-0087: 处置标记。我已替接手方查明:#5701 落在 85d95e7d9,两个 registry 里都没有 $regex / $icontains 台账条目 ⇒ already-registered 不可用;unpublished 判的是 npm 包私有 ⇒ 机械必红;no-migration-prescription 与本 changeset 自带的改写处方自相矛盾 ⇒ ⛔ 且绝不许利用「探测器只匹配字面量 FROM/TO」的盲区蒙混。

    倾向 registered,而且本单比今天另外两单更站得住:$regex 曾经是已声明的算子,消费方确实被告知过 —— 这正是台账的定义域,不必绕「无强制通道够得着受众」那个弯(#6404 / #6517 当时是靠那条理由)。⚠️ 但已要求接手方先验证一个前提:#5701 合入时这道门尚不存在、故未被要求登记,若这笔账本就属契约半边,条目正文需写明它涵盖契约 + 驱动两半,免得将来被读成只记了驱动侧。


    Generated by Claude Code

  13. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    ✅ ACCEPT — PR #6549

    接手方把我给的理由证伪了,然后自己找到了一个更硬的理由得出同一结论。这是本会话最好的一次复核。

    一、我错了:$regex 从来不是已声明的算子

    我在派工书与 #5702 评论里断言「$regex 曾是已声明的算子,消费方确实被告知过 ⇒ 本单比 #6404 / #6517 更站得住」。实测推翻:

    所以它与 #6404 / #6517 属同一个「从未声明」家族,不是更强的情形。我把「spec 里出现过这个词」当成了「它被声明为算子」—— 是我读错,方向还正好反了。

    二、但 registered 仍然成立,理由比我给的硬

    它自己验证出的那条:filter 本身就是入库 metadata(dashboard widget 的 filter / globalFilters、report.runtimeFilter、page、component、solution-blueprint),而 FilterConditionSchema 是开放 record(z.record(z.string(), z.unknown()))——

    于是一条已入库的 { $regex: ... } 永远 parse 得过,而开放映射上不可能立 retiredKey() 墓碑,所以台账是唯一够得着这些作者的通道。

    这比我那条「曾被告知过」强得多:它不依赖历史,而是指出今天存在一批入库数据,没有任何 schema 拒收会碰到它们。落在 semantic 而非 mechanical 也给了理由($regex → $icontains 两个方向都不无损)。渲染位置是读出来的:## Protocol 16 → 17 内 Mechanical 在相对行 107、Semantic 在 161、条目在 265 ⇒ 在 Semantic 之下。

    三、我第二处误读:那不是「CI 全绿」,是根本没有 CI

    我上一轮报告「CI 零失败、零在跑 ⇒ 那道红大概率已解」。错了。它查明:PR 处于 dirty(与 main 冲突)时,GitHub 无法计算 merge ref,于是 40 多分钟里创建了零个 workflow run,而同期其它分支正常调度。我看到的「零失败零在跑」是空集,不是绿。冲突解掉后 CI 立刻调度。

    ⚠️ 这条值得写进纪律:mergeable_state: dirty 时的「无失败」是无意义读数 —— 判 CI 必须先确认 run 数非零(与「名单在场守卫」同源的陷阱)。

    四、它第二次拒绝了同一个盲区,并把它归了类

    no-migration-prescription 在本单机械上确实能过 —— 它实测 findMigrationPrescription() 返回 null。它没有用,因为 changeset 正文明明白白给了改写处方。

    更有价值的是它把这一例归了类:本单的处方是一句纯祈使句,没有箭头、也没有表格("Write $icontains for the case-insensitive substring search this was almost always used for, $contains for a case-sensitive one"),所以 REWRITE_RE 根本不匹配、连 framing 分支都没被求值 —— 这是 #6559 已列三种之外的第四种漏检形状。它去 #6559 补了评论而不是另立新单(先搜过,家族已被跟踪)。

    结论一句话:这一格今天是靠人的判断守住的,不是靠门。

    五、它在源码层复核了我要它查的那处,结论与我一致

    scripts/check-driver-conformance.mjs 的 +86/−57:把 diff 过滤掉注释与字符串后只剩五处 issue: 指针改动(5702 → 6518);五行全部保持 kind: 'DEBT',无新增/删除/放宽,判定函数一行未动 ⇒ 判据没有被放宽。我此前也独立看过一遍,同样结论。

    其余清单项它也逐条给了源码级证据:转义纪律是一条代码路径而非复制(fold 是 applyLike(driver-sql) 与 pushLike(turso remote) 的参数),LIKE_ESCAPE_CASES 覆盖 % / _ / \ 与普通子串,交叉 ['$contains','$icontains'] 与各方言;turso 两面都实现(remote 有自己的 SUPPORTED_FILTER_OPERATORS 与 arm,local 继承 SqlDriver);拒收断 code 和 status;packages/spec 只有台账条目,filter.zod.ts 对 origin/main 未动 ⇒ 词表差集 pin 保持绿、冻结族的 $icontains 继续挂起。

    六、它还纠了我一处标注

    我写「大小写语义跟随 #5701 Q2=A」。按 #5701 自己的提交信息,ASCII-only fold 是 Q1=A;Q2=A 是另一条裁决(把 $contains 族钉死大小写敏感,至今在各面仍未落,跟踪于 #6518)。它核的是 ASCII fold 这一实质,正确。

    七、诚实报告的短板

    它明确区分了自己实测与沿用我已批准的三条((a)(b)(c) 未独立重测,因为我要求过不必重新论证),并如实记了自己制造的一次假红(pnpm typecheck -- --concurrency=2 把参数转给了 tsc,TS5023)。

    门禁全绿:check:adr-0087-registration 从 ✗ no adr-0087: disposition marker 转 ✓ registered filter-regex-options-retired,并用消融实验验证这个绿有意义(把 id 换成 totally-bogus-id ⇒ 预测红、实测红)。CI 25 项零非绿。


    #5702 结清。#4706 裁决 B 案的驱动半边到此完成。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

pm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions