Skip to content

feat(spec): icontains joins the view and infix filter vocabularies (#8934) - #9020

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-8934-icontains-dialect-parity
Aug 16, 2026
Merged

feat(spec): icontains joins the view and infix filter vocabularies (#8934)#9020
os-zhuang merged 5 commits into
mainfrom
claude/issue-8934-icontains-dialect-parity

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Fixes#8934

Implements the maintainer's Option A ruling (issue comment, 2026-08-16T04:25Z): the case-insensitive contains that every driver and evaluation face already executes as $icontains (#5702/#6520) becomes authorable from all three filter dialects instead of exactly one.

What changed

  • packages/spec/src/ui/view.zod.tsVIEW_FILTER_OPERATORS gains icontains. No alias rows, deliberately: the alias-table convention, measured, is a migration bridge for spellings already living in stored metadata (historical shorthand like eq, camelCase like notEquals); a canonical operator born after the table has no legacy spellings to bridge, and single-token canonicals (contains, in, between) carry no folds either. Inventing synonyms would widen the authoring surface, not bridge a legacy one. The curation decision is recorded on the table's docblock.
  • packages/spec/src/data/filter.zod.tsAST_OPERATOR_MAP gains icontains$icontains (so VALID_AST_OPERATORS, isFilterAST and parseFilterAST all pick it up — the set is derived from the map's keys, verified: no second hand-maintained set exists). The new row extends the Filter AST: like is folded to $contains at the wire — wildcards bind as literals and driver-sql's like/ilike arm is unreachable #7536 boundary prose in place rather than duplicating it: $icontains LIKE-escapes the comparand (a caller's % is a LITERAL), $ilike passes a raw pattern — a semantic boundary, so neither spelling is aliased onto the other in either vocabulary. A matching CANONICAL_INFIX row makes canonicalAstOperator round-trip it through the generic path (same shape as $like/$ilike) instead of the not-a-known-operator fallback.
  • No not_icontains — ruled out by name: the $ dialect has no $notIcontains, and the authoring vocabularies mirror the executed set rather than widening it. Pinned by test.
  • filter-view-operator-parity.test.ts$icontains joins the real-lowering KNOWN set; new $icontains has no counterpart in VIEW_FILTER_OPERATORS or VALID_AST_OPERATORS, so it is authorable only in the MongoDB-style dialect #8934 block pins: the lowering (['name','icontains','acme']{ name: { $icontains: 'acme' } }), case-insensitive spelling fold, the %-literal comparand case the ruling required, no collapse onto ilike or contains in either direction, generic canonicalisation round-trip, and the absence of a negative form.
  • filter-text-conformance.ts — one new case computed THROUGH parseFilterAST(['name','icontains','100%']). Today it is byte-equal at runtime to the existing $icontains%-literal case, on purpose: if the infix spelling is ever folded onto $ilike, the raw-pattern reading of 100% also matches the 100X match fixture row and the case goes red on every backend that imports the table (all five drivers plus objectql's having face) — the boundary is enforced where it executes.
  • driver-memory/src/memory-filter-text-conformance.test.ts — the analytics-face case-count pin moves 11 → 12: the new table case is $icontains post-lowering, so it joins that face's expressible subset automatically (the exact mechanism the test's own selection note promises). Caught by CI on the first push; count and prose updated with a $icontains has no counterpart in VIEW_FILTER_OPERATORS or VALID_AST_OPERATORS, so it is authorable only in the MongoDB-style dialect #8934 pointer.
  • Comparand-type door: no change needed$icontains has been a FieldOperatorsSchema key since spec: $regex 按 ADR-0049 退役 + $icontains 入算子词表与 FILTER_LOGIC_CASES(#4706 裁决 B 案 · 契约半边,先行) #5701, so SCALAR_COMPARAND_OPERATORS and the reconciliation test in filter-comparand-type.test.ts already cover it; the comparand-type conformance table judges TYPE policy on positions, not per-operator spellings (per-operator value rules like non-string $icontains are FILTER_TEXT_CASES' subject and already pinned there).
  • content/docs/references/ui/*.mdx regenerated via check:generated --fix (gen:docs was the one proved-stale artifact; the authorable-surface key set is unchanged — the widening is enum values, not keys).
  • Changeset: @objectstack/spec minor (acceptance-set widening; matches the recent filter-vocabulary changeset convention).

Per-face impact (the compile/evaluation faces the two vocabularies feed)

faceimpact
wire door (metadata-protocol: isFilterASTparseFilterAST)accepts infix icontains, lowers to $icontains; error messages listing the vocabulary are rendered from VALID_AST_OPERATORS dynamically
engine door (objectql)same pair, same result; $icontains execution already landed (#6520)
view rules (ViewFilterRuleSchema)icontains parses; value-shape refinement treats it as an unshaped scalar operator (correct — its comparand is a scalar); graftNormalizedOperators is identity on a canonical spelling
all five driversexecute $icontains since #5702/#6520 — no driver change needed; driver-memory's vocabulary-parity suite (it.each(VALID_AST_OPERATORS)) picks the new spelling up automatically and passes through the parseFilterAST route
driver-memory legacy comparison-node face (direct-caller shape { type: 'comparison', operator }, not reachable through any platform door)infix icontains hits the loud unsupportedFilterError default arm — a loud refusal, not a silent drop; the platform doors lower the spelling before any driver. Left as-is: extending that legacy face is a driver-scope decision outside this card's surface
lintvalidate-preset-comparandsfolds via canonicalAstOperator; judges ordering operators only — icontains is not one; unaffected

Verification — union run at ab81100 (git rev-parse --short HEAD from the run itself; working tree clean)

  • pnpm --filter @objectstack/spec test406 files / 10753 tests passed (full suite; spec content identical to head).
  • Consumer suites (downstream direction — packages whose code reads these vocabularies): driver-memory 756 ✓, driver-sql 1726 ✓ / 56 skipped, driver-turso 1003 ✓, driver-mongodb 385 ✓ / 143 skipped, driver-sqlite-wasm 395 ✓, objectql 3722 ✓, metadata-protocol 1524 ✓, lint 2065 ✓, service-analytics 1722 ✓.
  • Typecheck: @objectstack/spec ✓ (test layer under tsconfig.test.json, debt ledger untouched), @objectstack/driver-memory ✓.
  • check:generated — all 13 artifacts up to date after --fix.
  • Diff-derived gate union (node scripts/pm/dispatch-gates.mjs, re-derived against the actual diff — it added changeset/docs gates beyond the dispatch list): check:changeset-gate-self-tests, check:cross-package-test-inputs (both spellings), check:docs-audit-scope, check:merge-driver, check:objectui-changeset, check:quick-reference-counts, check:role-word, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-dev-prereqs, check:doc-formula-expressions, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check:type-check-debt (ratchet re-measure on the built closure: 33 entries, none above recorded) — all green at ab81100.
  • Reverse verification (fix committed first; direction decided up front as RED): ablating only the 'icontains': '$icontains' map row turned 3 assertions red — VALID_AST_OPERATORS covers every canonical view operator, icontains survives isFilterAST as a bare triple, and the $icontains has no counterpart in VIEW_FILTER_OPERATORS or VALID_AST_OPERATORS, so it is authorable only in the MongoDB-style dialect #8934 block's vocabulary-membership pin — then the row was restored from the commit (tree byte-clean). Honest nuance: the lowering assertions themselves stay green under that ablation because convertComparison's deliberate $${op} fallback coincidentally produces $icontains; the enforcing guard for the row is the isFilterAST vocabulary gate, and the guard against the dangerous direction (mis-aliasing onto $ilike) is the computed conformance case plus the explicit non-aliasing pins.

Generated by Claude Code

…8934)
Maintainer ruling (Option A on #8934): the case-insensitive contains every
driver executes as $icontains (#5702/#6520) becomes authorable from all
three filter dialects instead of exactly one.
- ui/view.zod.ts: VIEW_FILTER_OPERATORS gains 'icontains'; no alias rows —
the alias table bridges spellings already in stored metadata, and a new
canonical operator has none.
- data/filter.zod.ts: AST_OPERATOR_MAP gains 'icontains' -> '$icontains'
(extending the #7536 boundary prose: escaped substring, never a raw
pattern, never aliased onto ilike/$ilike in either direction); matching
CANONICAL_INFIX row so canonicalAstOperator round-trips generically.
- filter-view-operator-parity.test.ts: #8934 block — lowering, %-literal
comparand, no ilike/contains collapse, no negative form.
- filter-text-conformance.ts: one case computed THROUGH the lowering, so a
future icontains->$ilike fold goes red on every backend running the table.
- No not_icontains: the $ dialect has no $notIcontains; the authoring
vocabularies mirror the executed set.
- content/docs/references regenerated via check:generated --fix (gen:docs
was the one proved-stale artifact; authorable-surface key set unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
@vercel

vercelBot commented Aug 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 16, 2026 7:12am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

…s after #8934
The new FILTER_TEXT_CASES entry (infix icontains, %-literal) lands as
$icontains post-lowering, so it joins the analytics face's expressible
subset automatically — the count pin moves 11 -> 12, exactly the mechanism
the selection note promises.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
…8934)
os-regen relay: merge of origin/main (6468df1) committed first with main's
side of the generated artifacts; gen:docs re-run on the merged tree restores
the icontains rows into main's current references. gen:openapi re-run after
(the gen:schema-cleanup sister trap). check:generated: all 13 up to date.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:dataprotocol:uisize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$icontains has no counterpart in VIEW_FILTER_OPERATORS or VALID_AST_OPERATORS, so it is authorable only in the MongoDB-style dialect

2 participants

@os-zhuang@claude