Skip to content

fix(core): lower $icontains, the canonical operator convertFiltersToAST refused (objectui#8976) - #8996

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-8976-icontains-converter
Sep 10, 2026
Merged

fix(core): lower $icontains, the canonical operator convertFiltersToAST refused (objectui#8976)#8996
os-steve merged 1 commit into
mainfrom
claude/issue-8976-icontains-converter

Conversation

@os-steve

@os-steve os-steve commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8976

What was wrong

$icontains is a canonical member of @objectstack/spec's FILTER_OPERATORS, ValueDataSource executes it, FilterConditionField emits it for its "contains (ignore case)" builder row, and packages/core/src/adapters/README.md prescribes it as the repair when $like / $ilike / $regex are refused.

convertOperatorToAST's operatorMap had no row for it, so convertFiltersToAST refused it with the generic unknown-operator paragraph — INVALID_FILTER / 400. The one spelling this repo tells an author to write was the one spelling it rejected.

Census, re-taken on today's tree

Not the card's readings. Re-measured on 4d65991c5 (the tip this branch was cut from), spec 17.4.0, both legs in one process, with a control operator both sides agree on:

spec FILTER_OPERATORS includes $icontains: true
spec VALID_AST_OPERATORS has icontains: true
spec VIEW_FILTER_OPERATORS has icontains: true
LOWERING $icontains        => THREW FilterOperatorError / INVALID_FILTER / 400
LOWERING CONTROL $contains => ["name","contains","AC"]
MATCHER  $icontains        => rows ["ACME"]
MATCHER  CONTROL $contains => rows ["ACME"]

The defect reproduces unchanged after PR #8974, #8977 and #8988.

What changed

packages/core/src/utils/filter-converter.ts

  • operatorMap gains '$icontains': 'icontains', placed after $endsWith so the map follows the spec's own declaration order. The value is an identity and that is not a typo: icontains is itself a member of VALID_AST_OPERATORS, so unlike $startsWith to startswith there is no case to squash.
  • The unknown-operator message now enumerates $icontains among the supported operators.
  • The $regex refusal now prescribes it by name. @objectstack/spec's own FILTER_TEXT_CASES carries mustMention: ['$regex', '$icontains'] on that refusal, and the converter could not satisfy it while it did not accept $icontains.

packages/data-objectstack/README.md gains the $icontains row it never had, plus prose explaining the identity lowering. The $regex refused row's prescription is brought back in step with the code.

packages/data-objectstack/src/readme-filter-operator-table.test.ts — the pin's blind spot, closed.

Why the pin could not see this

PR #8565's reconciliation pin held the README tables against two populations, and both were the code's own: operatorMap's keys, and the operators the unknown-operator error calls supported. That makes it exact about drift between code and page — and structurally unable to see an operator missing from both sides at once. $icontains was exactly that operator, so the page could carry no row for it in either table and the suite stayed green. A pin that reads only the code cannot report the code being wrong.

A third population is now read, and it is the one the code answers to: the spec's FILTER_OPERATORS. Every canonical member must be accounted for by the two tables — documented as supported, or documented as refused. Which of the two is not asserted (the existing rows already hold the tables to the code); what is asserted is that no canonical operator is missing from both.

Deliberately one-directional. The refused table legitimately carries spellings the spec does not declare ($regex, and the four lowercase aliases objectui#8568 retired); asserting the converse would delete it.

Ablation — proof the closed blind spot would now catch a future omission

Both arms mutate on disk, prove the mutation landed by counting the target text before and after, run, then restore under a trap on absolute paths. Each restore is proven by git diff HEAD plus a blob-hash comparison against HEAD, never by an exit code. Expectations were written down before each run. Tests resolve @object-ui/core through the root vitest alias to packages/core/src, so the arms act on source and no rebuild step can hide a mutation.

Arm A — the blind-spot demonstration. Reproduce the exact pre-PR state of the two files the old pin read (converter and README restored to 4d65991c5), keep only the pin at HEAD.

on-disk proof: converter icontains-hits 5 -> 1 ; README icontains-hits 4 -> 0
ARM A vitest exit: 1
  × no member of the spec FILTER_OPERATORS is missing from BOTH tables
  Tests  1 failed | 11 passed (12)
restored: git-diff-HEAD-clean=yes  blob 11f41b551 == HEAD blob 11f41b551

One failure, and it is the new case. The two OLD completeness cases — "carries a row for every key of the operator map" and "carries a row for every operator the unknown-operator error calls supported" — are among the 11 that passed, on the exact tree state that shipped the defect. That is the blind spot, executed rather than argued.

Arm B — the fix is load-bearing. From HEAD, delete the operatorMap row only; README, pin and acceptance test intact.

on-disk proof: operatorMap row occurrences 1 -> 0
ARM B vitest exit: 1
  × $icontains is accepted by convertFiltersToAST
  × $icontains lowers to an operator the AST gate accepts
  × the converter lowers $icontains to `icontains`, unsquashed
  × supported and combinator rows lower to exactly the documented output
  × every spelling in the first column lowers to exactly the operator(s) the second column names
  Tests  5 failed | 23 passed (28)
restored: git-diff-HEAD-clean=yes  blob 11f41b551 == HEAD blob 11f41b551

Acceptance assertion

packages/core/src/utils/__tests__/filter-icontains-alignment-8976.test.ts asserts the harder claim: the spelling the adapters README prescribes must actually work. The prescriptions are read out of that page's "write instead" column and intersected with the spec's FILTER_OPERATORS, so the population comes from hand-written prose and from the contract — never from the function under test. Three controls guard it against passing while witnessing nothing: the reader must find at least three prescriptions and must find $icontains; $icontains must still be canonical in both spec vocabularies; and a case-sensitive probe must select a strictly smaller row set than the case-insensitive one, so "both faces select rows" cannot pass on a matcher that ignores the fold.

One control in the pin was wrong on first write and caught by its own run: it asserted the spec list differs from the unknown-operator message's enumeration. After this fix those two agree exactly, so the claim was false. It now rests on a structural fact instead — operatorMap cannot carry $null / $exists, which pick their AST operator from the value — and the file records why the equality must not be pinned in either direction.

Verification

gate exit reading
affected suites, packages/core + packages/data-objectstack, from repo ROOT 0 202 files, 3845 tests passed
the four test files elsewhere that touch icontains (scripts, plugin-list, fields, components) 0 4 files, 169 tests passed
type-check (turbo, both packages) 0 5 tasks successful
lint (turbo, both packages) 0 3 tasks; 0 errors, 530 pre-existing no-explicit-any warnings
check:control-bytes 0 7202 tracked text files scanned
check:changeset-presence 0 2 published-source files, 1 changeset declared
check-governed-queue-guard --test (my 5 paths) 0 NOT GOVERNED — ordinary review route
check:doc-snippets 0 638 of 638 blocks judged, 0 failed
check:doc-types 0 188 docs, every documented type registered
check:doc-examples 0 124 blocks; 35 compile, 89 fail exactly as their ledger rows declare
check:doc-fences 0 227 documents
check:readme-exports 0 43 READMEs, 0 fabricated / 0 stale
check:new-line-citations 0 0 new cross-file line citations added
census:dollar-dialect-aliases 0 authored-corpus alias occurrences 0, both positive controls fired

check:doc-snippets and check:doc-examples first returned exit 2 — PREREQUISITE NOT MET, not a pass. Re-run green after the scoped build they name (turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2, 34 packages, 35 tasks successful), taken through the shared verify lock.

NOT MEASURED: the full suites of packages/plugin-list, packages/fields and packages/components. A whole-package run of those four was started, exceeded the foreground budget, and I stopped it by recorded PID (exit 144 = my SIGTERM, not a test verdict). Narrowed to the four test files in them that actually reference icontains, which passed. My diff touches no file in those packages; CI runs them in full.

Scope

  • ⛔ Not folded with objectui#8568. Opposite legs of one class: there the converter was more tolerant than the contract (four lowercase aliases), here less tolerant on a canonical member. A single "make them agree" change would have widened the matcher.
  • objectui#8770 (unlowered TRUE-identity groups) also lands in this file. It is pm:queue and unassigned; its shape is untouched here and it queues behind this. Serial adjacency was re-derived against all 7 currently-open PRs by diffing each head against its merge-base: none touches packages/core/src/utils/filter-converter.ts, packages/data-objectstack/README.md or the pin.
  • ⛔ Not claimed: end-to-end reach from a stored criteria_json into this converter. The criteria store is not in this tree — the consumer-local caveat objectui#6839 established. What is proven is the acceptance-set disagreement and the producer arm.

Assumptions falsified

Two premises in the card did not survive re-measurement on today's tree. Neither changes the direction.

  1. The card says icontains "is already an identity row in FILTER_OPERATOR_ALIASES". There is no FILTER_OPERATOR_ALIASES in @objectstack/spec — only VIEW_FILTER_OPERATOR_ALIASES, and icontains deliberately has no row there. The spec's own comment: "icontains (record:chatter.feed / record:discussion.feed 宣称「same shape as record:activity」,但其中的过滤成员(types / limit / showCompleted / unifiedTimeline)在这条路径上被静默丢弃 #8934) deliberately has NO rows here". The conclusion survives by a different mechanism: icontains is canonical, so normalizeFilterOperator returns it unchanged and there is nothing to bridge. The identity row the card meant is this repo's, in packages/data-objectstack/src/index.ts.
  2. The card's measurement git grep -n icontains -- packages/data-objectstack/ "returns nothing" is now falsesrc/index.ts:126 carries icontains: 'icontains'. The narrow claim that matters, that README.md has no $icontains row, held.

Out of scope, filed as objectui#9001

convertFiltersToAST has no text-comparand guard, so { name: { $icontains: '' } } lowers to ['name', 'icontains', ''] while ValueDataSource refuses it and the spec's FILTER_TEXT_CASES declares it INVALID_FILTER. ⚠️ Stated plainly because the timing invites the wrong reading: this PR did not open that door. Measured on the same tree, the object-form dialect already reached it — FILTER_OPERATOR_ALIASES['icontains'] in packages/data-objectstack is an identity row, so { field, operator: 'icontains', value: '' } lowered to the wire with no converter involved. The sibling positive operators have the same gap ({ name: { $contains: '' } } lowers today and matches every row). What this PR adds is a second entrance. Whether the guard belongs in the producer, and whether it extends to the siblings, is a ruling this card did not make — ValueDataSource deliberately declined to widen by analogy, and objectui#8600 recorded the related enrolment question. Filed rather than folded.


🤖 Generated with Claude Code

https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB

…used

`$icontains` is a member of `@objectstack/spec`'s `FILTER_OPERATORS`,
`ValueDataSource` executes it, `FilterConditionField` emits it for its
"contains (ignore case)" builder row, and `packages/core/src/adapters/README.md`
prescribes it as the repair when `$like` / `$ilike` / `$regex` are refused.
`convertOperatorToAST` had no row for it, so `convertFiltersToAST` answered the
generic unknown-operator refusal (`INVALID_FILTER` / 400) — the one spelling
this repo tells an author to write was the one spelling it rejected.

Aligning the converter with the contract, per AGENTS.md #0. The lowered value
is an identity: `icontains` is itself a member of `VALID_AST_OPERATORS`, so
unlike `$startsWith` -> `startswith` there is no case to squash.

Two consequences of the same gap ride along: the unknown-operator message now
enumerates `$icontains`, and the `$regex` refusal prescribes it by name — the
spec's own `FILTER_TEXT_CASES` requires that refusal to mention `$icontains`,
and it could not while the converter did not accept it.

`packages/data-objectstack/README.md` gains the row it never had, and the
reconciliation pin's blind spot is closed in the same change: it held the
tables against the code's own two populations (`operatorMap`'s keys and the
operators the error calls supported), and `$icontains` was in NEITHER, so the
omission was invisible to it by construction. The tables are now also held
complete against the spec's `FILTER_OPERATORS`.

This is the opposite leg of objectui#8568 and deliberately not folded with it:
there the converter was more tolerant than the contract, here less tolerant.
A single "make them agree" change would have widened the matcher instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@github-actions github-actions Bot added documentation Improvements or additions to documentation data-adapter package: core tests labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.4 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-Cor52x6j.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 207.32KB 57.37KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.71KB 65.78KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT + 席内 Clause-② 复核 PASS — 派发席 domain:ui @ objectui

session_01MPaVWWMuWeT5LgB1qoXjVB(os-steve),复核时刻 2026-09-10T17:0xZ。⛔ 本复核对 GitHub 与 diff 本身核验,⛔ 不对终报自述核验。

门禁

34 个 check 全部收敛:31 success · 3 skipped · 0 红 · 0 悬。 逐个 job 结论读取,⛔ 未用 get_status、⛔ 未认 required 子集。Governed Surface Queue Guard = success。四个 shard 16:27–16:33Z 全绿,Type Check 16:24:58Z 绿。

裁决落实,本席按内容亲核

依据分诊裁定(评论 5619923481:aligning the converter is 恢复不变量 … the direction is settled by the contract(协议为基准)):

主体 main 分支
operatorMap'$icontains' 0 1
data-objectstack/README.mdicontains 0 4

且那一行是恒等映射,并自带解释:

// The VALUE is an IDENTITY, and that is not a typo: `icontains` is itself a
'$icontains': 'icontains',

⇒ 因为 icontains 本身就是 spec 的 VALID_AST_OPERATORS 成员 —— 下游无需发明任何东西,这正是分诊说「协议为基准」的含义。

⭐ 本轮最有价值的产出:ARM A 是对「pin 盲区」的直接证明

把转换器与 README 还原到 4d65991c5(即发布了该缺陷的那棵树的确切状态),pin 留在 HEAD:

结果:1 failed / 11 passed
唯一的红 = 新增的那条(读 spec 的 FILTER_OPERATORS)
两条 OLD 的「完整性」用例 —— 全部 PASSED

旧 pin 在恰恰发布了这个缺陷的树上是绿的。 这比任何论证都更硬地证明了它结构性看不见 $icontains:它只对着代码自己的两个集合(operatorMap 的键、unknown-operator 报错自称支持的集合)校验,而 $icontains 两个都不在。新 pin 引入第三个、代码推导不掉的人口 —— spec 的 FILTER_OPERATORS —— 且单向断言,好让 refused 表仍可记录非 spec 拼写。

⭐ 一处本席与分诊都没预见到的发现:spec 自己早就要求了这件事

@objectstack/specFILTER_TEXT_CASES$regex 的拒绝上带着 mustMention ['$regex','$icontains']转换器在不接受 $icontains 的前提下根本无法满足它自己的 spec。本 PR 让 $regex 的拒绝按名开方 $icontains,这不是新增的礼貌,是补上一条既有义务。

⚠️ 一条声明的收窄,本席接受

plugin-list / fields / components 的整包套件 NOT MEASURED —— 整包跑超出前台预算,按记录的 PID 终止,exit 144 是 SIGTERM 而不是测试判决,终报如实这么写了。收窄到四个碰 icontains 的文件(169 tests,exit 0),且 diff 不碰那三个包的任何文件。⇒ 这是合规的声明式收窄,⛔ 不是跳过;CI 的分片农场覆盖其余。

⚠️ 另记:dev 自己写的一条对照被它自己的修复证伪 —— 它断言 spec 列表与 unknown-operator 枚举不同,而本修复让两者恰好一致。它改为立在结构事实上(operatorMap 不可能承载 $null / $exists),并在文件里写明为何这条相等不得被双向钉死。⇒ 与本仓 ⑭「通过 ≠ 见证」同族。

卡的两处引用被证伪(结论不受影响)

  1. 卡称 icontains「已是 FILTER_OPERATOR_ALIASES 的恒等行」—— @objectstack/spec 无此导出,只有 VIEW_FILTER_OPERATOR_ALIASES,而 icontains 在那里刻意没有行(spec 自己的注释说明了)。卡混淆了本仓 data-objectstack/src/index.ts 里的同名表。结论经由规范成员身份成立。
  2. 卡的 git grep -n icontains -- packages/data-objectstack/ 无输出已过期(src/index.ts:126 现在带它)。真正要紧的那条窄断言 —— README 无 $icontains 行 —— 成立,并由本 PR 修复。

Clause-②

公开面扩大(转换器接受集新增一个规范成员)⇒ Clause-②: yes,按机制申报。依 objectstack-ai/objectstack#17285,本车道默认档自审 + 门禁即 review of record。⇒ PASS

放行:翻 ready、挂 SQUASH,按队列 ref 阳性命中确认,再按内容带亮/死对照核落地。落地后 objectui#8770 解封 —— ⚠️ 其方向未裁,承接前先读它的评论串。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 10, 2026 16:44
@os-steve
os-steve added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit b79aac2 Sep 10, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-8976-icontains-converter branch September 10, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data-adapter documentation Improvements or additions to documentation package: core tests

Projects

None yet

2 participants