feat(spec): $icontains(ASCII 折叠域)+ $contains 族钉死大小写敏感 + $regex 退役指引表 (#5701) - #5752
Merged
Merged
Conversation
… case-sensitive, retire $regex (#5701) The contract half of #4706's maintainer ruling (B). Declaration only: no live rejection, no driver code touched, every backend answers exactly what it answered before. - `$icontains` on StringOperatorSchema / FieldOperatorsSchema / Filter: contains, ignoring ASCII case (A-Z against a-z) and nothing else. The boundary is stated for authors — `café` does NOT match `CAFÉ` — because ASCII is the one fold all five backends can deliver: SQLite without ICU (so turso and sqlite-wasm too) folds ASCII only, and promising Unicode would repeat the defect this retires. - `$contains` / `$notContains` / `$startsWith` / `$endsWith` are declared CASE-SENSITIVE, superseding the recorded "Case sensitivity should be handled at backend level" (Prime Directive #13 — the old sentence is quoted in place so the reversal is findable from the sentence a reader remembers). What that non-guarantee bought, measured: three different answers across five backends, two of them inside driver-memory alone (query path folds full Unicode, the reference matcher is case-sensitive). - `RETIRED_FILTER_OPERATORS`: pure data, no behaviour. `$regex` and `$options` with prescriptions naming `$icontains`. Nothing rejects them here — the five existing refusal sites are wired to it by #5702, and #5710 must flip the one live producer (plugin-auth's ObjectQL adapter, on the authentication path) first or sign-in breaks. - `FILTER_TEXT_CASES` (`data/filter-text-conformance.ts`): the shared standard for case folding, literal comparands and the `$regex` refusal, with the `expectRejection` discriminant `filter-logic-conformance.ts` deliberately never grew. A sibling table rather than rows in that one, whose charter excludes both axes and warns against red rows for work nobody is dispatched to do. Registered in check:driver-conformance with one MEASURED DEBT row per driver pointing at #5702 — 25 covered / 5 DEBT / 0 exempt, main stays green. `$icontains` is deliberately NOT in `FILTER_OPERATORS` yet. That array is a runtime allowlist, not a word list: driver-memory's shape gate derives its ACCEPTED set from it while its matcher's `default:` arm breaks. Measured by adding it early and rebuilding — the gate stopped refusing and `match({name:'zzz'}, {name:{$icontains:'acme'}})` returned true, i.e. the predicate was silently dropped and every row matched. A dropped predicate widens rather than narrows, which on an RLS read scope is a permission bypass (#3948). It joins the array in the PR that implements it; `filter-operator-vocabulary.test.ts` pins the difference at exactly {$icontains} so neither adding a second staged operator nor clearing this one can happen silently. Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D Co-authored-by: Claude <noreply@anthropic.com>
…ex-retire-icontains
…g main The merge driver (`merge=os-regen`, AGENTS.md §11) deferred both artifacts rather than text-merging them, and `pre-commit` collects that debt. Regenerated from the MERGED source, so the union includes #5721's ActionSession entries alongside this branch's $icontains / FILTER_TEXT additions. `authorable-surface.base.json` is deliberately NOT bumped: it is the deletion gate's anchor, main only moves it when a key is REMOVED, and this branch only adds. `check:authorable-surface` confirms it — 'trails the merge base by 5 key(s) — expected right after a surface change lands'. Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 04:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5701
#4706 维护者裁决 B 案的契约半边。纯声明 + case-set + DEBT 台账,不引入任何活体拒收,不改任何驱动代码 —— 五个后端今天怎么答,落地后还怎么答。
裁定摘录(#5701 的 2026-08-06「维护者裁决转录」)
$icontains折叠域 = ASCII-only(A-Z/a-z),明写进 zod.describe()与文档,边界显著标注StringOperatorSchema.$icontains的.describe()+ 文档 Callout(café不匹配CAFÉ)$contains/$startsWith/$endsWith同批钉死为大小写敏感;filter.zod.ts:139那句「backend level」按 PD #13 留明写取代记录StringOperatorSchema文档块「This SUPERSEDES a recorded decision」:原句原样引用 + 五面实测分歧表RETIRED_FILTER_OPERATORS,不收窄FilterConditionSchema,不引入活体拒收filter.zod.ts新增纯数据表($regex/$options),Object.freeze,处方点名$icontainsexpectRejection判别式在新文件定义,五 driver 记实测 DEBT,main 不红packages/spec/src/data/filter-text-conformance.ts+ 门禁 5 条 DEBT$istartsWith/$iendsWith本批不加用例面对照表(派发要求 6 条 →
FILTER_TEXT_CASES)$icontainsASCII 双向折叠命中...matches an upper-case row from a lower-case comparand/...from an upper-case comparand['1','2'],两向对称ASCII-only: a lower-case non-ASCII comparand does NOT match its upper-case row+ 镜像['4']/['3']—— 全 Unicode 折叠两向都返回['3','4'],两向都错%/_/ 正则元字符作字面量$icontains treats % / _ / . as a literal ...+$contains treats _ as a literal character too['5']/['7']/['9']/['7']an empty $icontains comparand is REFUSED/a non-string ... is REFUSEDexpectRejection,code: 'INVALID_FILTER'$contains大小写敏感$contains is case-SENSITIVE×2、$startsWith、$endsWith、$notContains['2']/['1']/['1']/['2']/ 8 行$regex拒收 pin,信息含$icontains$regex is REFUSED, and the refusal names $icontains、$regex with $options、悬空$optionsexpectRejection+mustMention含$icontains九行 fixture 成对设计(ASCII 大小写 / 非 ASCII 大小写 /
%/_·.),每对只差一个受测字符,所以错答返回的是看得见的错 id,而不是碰巧相同的行数。filter-text-conformance.test.ts用一个按声明语义写的参考求值器把整张表跑了一遍 —— 表里每个expected都由 ASCII 折叠语义复算出来,不是手数的;另外两条断言证明这张表非空洞:全 Unicode 折叠必须恰好在那两条 ASCII-only 用例上给出不同答案。$icontains暂不进FILTER_OPERATORS(请重点审这段)派发口径是「
$icontains入词表」。实施时实测发现FILTER_OPERATORS不是词表,是运行时白名单,有两个消费者从它派生而非重述:driver-memory的SUPPORTED_FIELD_OPERATORS=new Set([...FILTER_OPERATORS, '$regex', '$options']),是它形状门禁的接受集合;而memory-matcher的default:分支是break(注释写明「Unreachable through match: the shape gate refuses an operator this driver does not evaluate」)。service-analytics的objectql-echo-operator-coverage.test.ts断言其编译器对每个成员都渲染出谓词。实测(先写预测,再跑):把
$icontains加进FILTER_OPERATORS、重建 spec dist 后,在 driver-memory 内探针得到:一行不该命中的记录命中了。谓词被丢不是收窄而是放大,在 RLS 读作用域上是越权读(#3948),且发生在任何后端能回答该算子之前。这与派发的三条硬约束(不动驱动代码 / 不引入活体行为 / main 不红)直接冲突。
因此本 PR 的落点:
$icontains声明在StringOperatorSchema/FieldOperatorsSchema/Filter< T >(已核实这三处没有任何运行时白名单消费者 ——NormalizedFilterSchema是它们唯一的消费者,而运行时无人用它 parse filter,故此处声明是惰性的),FILTER_OPERATORS保持不变,并在其文档块写明这条过渡与解除条件。这个差异不是无声的:
filter-operator-vocabulary.test.ts把「声明面 − 执行面」钉死为恰好{ $icontains },双向断言 ——$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702 实现完把$icontains补进FILTER_OPERATORS→ 也红,失败信息直接指示「删掉FILTER_OPERATORS上那段过渡说明」。若维护者认为应当强行入表并同批修 driver-memory,请驳回本段,我按新口径重做(那会把本单从「零驱动改动」变成跨 #5702 领地)。
反向验证:预测 vs 实测
$icontainshas()= true,门禁无拒收default: break使谓词被丢 → 放大trueservice-analytics算子覆盖门禁变红FILTER_TEXT列全判 DEBT(非红)全仓
$regex清点 —— 无一处需要翻转,逐处说明派发要求「examples / 测试 / 文档中现存
$regex用法随本 PR 翻转」。实测结论:这三类里没有一处「使用」 —— 全是拒收 pin、历史注释,或明确划归他单的活体。逐处:examples/**、skills/**content/docs/**$regex过滤用法;唯一相关是 search 展开那句「in-memory driver matches with a case-insensitive regex」service-analytics的read-scope-sql/ 两处*-not-null-safe测试$regex作「编译器必须拒收的算子」,本身就是拒收 pin$icontains会毁掉断言意图)service-analytics两处objectql-*operator*.test.ts$regex分支的替身引擎packages/rest/analytics-filter-refusal-envelope.test.ts:292$regex的拒收报文packages/formula/matches-filter.test.ts:95$regex作 fail-closed 的「未知算子」样本plugin-auth:1 生产者 + 3 测试替身objectql/having-filter.ts(+test)、四个驱动.changeset/*.md四份不触清单(派发 ⛔ 项,已逐条核对)
plugin-auth/objectql-adapter.ts(#5710)、driver-sql / driver-turso / driver-memory / objectql having-filter(#5702)、component.zod / hook.zod / api-routes / kernel-contracts、content/docs/releases/。conversions/registry.ts未触发 —— 退役的不是 authorable key(spec 从未声明$regex),ADR-0087 D2 无对象可登记,与上轮结论一致。#4923 无冲突(其 #5731 已合入,本分支已 merge)。一处需要更正上轮口径:
StringOperatorSchema确实在 authorable-surface 轨道上,gen:schema因此新增了恰好一个键data/StringOperator:$icontains(纯新增,删除门禁不涉及),已随本 PR 提交。FieldOperatorsSchema不在该轨道(全文件零命中),所以那一处声明不产生生成物变化。顺带记录的两处实测(不在本 PR 修,已附着到 #5702)
$contains有两种大小写答案:查询路径 / 分析面走new RegExp(escapeRegex(v), 'i')(不敏感、全 Unicode),参考匹配器memory-matcher.match()走includes(敏感)。上轮调研只读到后者,本 PR 的 DEBT 条目已按实测改写。default:抛裸new Error,而同文件的unsupportedFilterError(另三处在用)才带INVALID_FILTER/ 400。两条都写进了 DEBT 台账条目正文(实施者读台账即可看到),并在 #5702 留了评论。
验证输出(均在合入最新
origin/main之后重跑)P3 反向验证(重测,红→绿闭环)
比预测更强:我只预测了词表等值断言变红(第 1 条)。实测多出第 2 条 ——
$icontains echoed no WHERE — the predicate was dropped,即 analytics 编译器也把这个谓词静默丢掉了。这是在第二个消费者身上独立复现了与 driver-memory 同一个「放大」缺陷,而不是同一处的两种说法。过程更正(如实记录):这个测量的前两轮都作废过。第一轮两臂同红(模块收集期失败,不是断言失败),原因是新工作树没预构建 service-analytics 依赖链 —— 失败形态与「你的改动弄坏了 import」一模一样;若没有事先设好对照臂,这轮会被误读成 P3 已验证。第二轮反事实根本没被应用:
perl -0pi -e "s/\Q$WITHOUT\E/.../"里\Q只关元字符、不关插值,$contains/$notContains被 perl 当成自己的变量插成空串,两臂又都成了对照。第三轮改用 node 做替换,并把「已应用 / 已还原」从假设改成断言(出现次数必须恰为 1,还原后必须与备份字节相同),才拿到上面的红绿闭环。Generated by Claude Code