Uh oh!
There was an error while loading. Please reload this page.
feat(spec): icontains joins the view and infix filter vocabularies (#8934) - #9020
Conversation
…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
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…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
…ntains-dialect-parity
…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
Uh oh!
There was an error while loading. Please reload this page.
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.ts—VIEW_FILTER_OPERATORSgainsicontains. No alias rows, deliberately: the alias-table convention, measured, is a migration bridge for spellings already living in stored metadata (historical shorthand likeeq, camelCase likenotEquals); 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.ts—AST_OPERATOR_MAPgainsicontains→$icontains(soVALID_AST_OPERATORS,isFilterASTandparseFilterASTall 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:likeis folded to$containsat the wire — wildcards bind as literals and driver-sql'slike/ilikearm is unreachable #7536 boundary prose in place rather than duplicating it:$icontainsLIKE-escapes the comparand (a caller's%is a LITERAL),$ilikepasses a raw pattern — a semantic boundary, so neither spelling is aliased onto the other in either vocabulary. A matchingCANONICAL_INFIXrow makescanonicalAstOperatorround-trip it through the generic path (same shape as$like/$ilike) instead of the not-a-known-operator fallback.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—$icontainsjoins the real-lowering KNOWN set; new$icontainshas no counterpart inVIEW_FILTER_OPERATORSorVALID_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 ontoilikeorcontainsin either direction, generic canonicalisation round-trip, and the absence of a negative form.filter-text-conformance.ts— one new case computed THROUGHparseFilterAST(['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 of100%also matches the100X matchfixture 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$icontainspost-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$icontainshas no counterpart inVIEW_FILTER_OPERATORSorVALID_AST_OPERATORS, so it is authorable only in the MongoDB-style dialect #8934 pointer.$icontainshas been aFieldOperatorsSchemakey since spec:$regex按 ADR-0049 退役 +$icontains入算子词表与 FILTER_LOGIC_CASES(#4706 裁决 B 案 · 契约半边,先行) #5701, soSCALAR_COMPARAND_OPERATORSand the reconciliation test infilter-comparand-type.test.tsalready cover it; the comparand-type conformance table judges TYPE policy on positions, not per-operator spellings (per-operator value rules like non-string$icontainsareFILTER_TEXT_CASES' subject and already pinned there).content/docs/references/ui/*.mdxregenerated viacheck:generated --fix(gen:docswas the one proved-stale artifact; the authorable-surface key set is unchanged — the widening is enum values, not keys).@objectstack/specminor (acceptance-set widening; matches the recent filter-vocabulary changeset convention).Per-face impact (the compile/evaluation faces the two vocabularies feed)
metadata-protocol:isFilterAST→parseFilterAST)icontains, lowers to$icontains; error messages listing the vocabulary are rendered fromVALID_AST_OPERATORSdynamicallyobjectql)$icontainsexecution already landed (#6520)ViewFilterRuleSchema)icontainsparses; value-shape refinement treats it as an unshaped scalar operator (correct — its comparand is a scalar);graftNormalizedOperatorsis identity on a canonical spelling$icontainssince #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 theparseFilterASTroutedriver-memorylegacy comparison-node face (direct-caller shape{ type: 'comparison', operator }, not reachable through any platform door)icontainshits the loudunsupportedFilterErrordefault 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 surfacelintvalidate-preset-comparandscanonicalAstOperator; judges ordering operators only —icontainsis not one; unaffectedVerification — union run at
ab81100(git rev-parse --short HEADfrom the run itself; working tree clean)pnpm --filter @objectstack/spec test— 406 files / 10753 tests passed (full suite; spec content identical to head).@objectstack/spec✓ (test layer undertsconfig.test.json, debt ledger untouched),@objectstack/driver-memory✓.check:generated— all 13 artifacts up to date after--fix.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 atab81100.'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$icontainshas no counterpart inVIEW_FILTER_OPERATORSorVALID_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 becauseconvertComparison's deliberate$${op}fallback coincidentally produces$icontains; the enforcing guard for the row is theisFilterASTvocabulary 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