From 275cbab70a9ffc88dc60896ab32cbfa4166cef91 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 18:28:00 +0000 Subject: [PATCH] docs(spec): the $search expansion compiles to $icontains, not $contains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `DriverCapabilities.fullTextSearch` tombstone prescription and five hand-written passages still described the ADR-0061 search expansion as an `$or` of `$contains`. `expandSearchToFilter` has emitted `$icontains` since #7641; `$contains` is contractually case-sensitive (#4706 Q2 = A). One callout went further than a stale operator name: it told the reader the case-insensitivity question "remains a separate open question, and one that can now actually be answered" — inviting a reader acting in good faith to re-open an adjudicated ruling. It now states the settled answer. Site C lands in the `.describe()` source, never in the generated `.mdx`; the four reference rows are regenerated by `check:generated --fix`. Every other `$contains` mention on the three pages was classified individually — the operator's own rows, examples and case-sensitivity rulings are correct and are untouched. `packages/objectql/src/search-filter.ts` is not in this diff: its `__search` companion clause is deliberately `$contains` and must not be aligned. `content/docs/releases/` is not touched; it was already correct. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i --- .changeset/olive-pugs-shave.md | 18 ++++++++++++ content/docs/data-modeling/queries.mdx | 4 +-- content/docs/data-modeling/schema-design.mdx | 8 +++--- .../docs/protocol/objectql/query-syntax.mdx | 28 +++++++++++-------- content/docs/references/data/driver-nosql.mdx | 2 +- content/docs/references/data/driver-sql.mdx | 2 +- content/docs/references/data/driver.mdx | 4 +-- packages/spec/src/data/driver.zod.ts | 5 ++-- 8 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 .changeset/olive-pugs-shave.md diff --git a/.changeset/olive-pugs-shave.md b/.changeset/olive-pugs-shave.md new file mode 100644 index 0000000000..3e94a011c0 --- /dev/null +++ b/.changeset/olive-pugs-shave.md @@ -0,0 +1,18 @@ +--- +'@objectstack/spec': patch +--- + +Correct the `$search` expansion description: it compiles to `$icontains`, not `$contains` + +The `DriverCapabilities.fullTextSearch` tombstone prescription in `driver.zod.ts` said +`$search` is compiled into "an `$or` of `$contains` predicates". It has compiled to +`$icontains` since #7641 — textual search is case-insensitive by ruling, and `$contains` +is contractually case-**sensitive** (#4706 Q2 = A). The prescription now says so, and the +four generated driver reference rows it feeds regenerate with it. + +The same false sentence is corrected on the three hand-written pages that carried it +(`protocol/objectql/query-syntax.mdx`, `data-modeling/queries.mdx`, +`data-modeling/schema-design.mdx`), including a callout that told readers the +case-insensitivity question "remains a separate open question" when #7641 closed it. + +Documentation only — no accept/reject, emitted-shape or runtime behaviour change. diff --git a/content/docs/data-modeling/queries.mdx b/content/docs/data-modeling/queries.mdx index 4cd657c5f6..22782bd88f 100644 --- a/content/docs/data-modeling/queries.mdx +++ b/content/docs/data-modeling/queries.mdx @@ -493,7 +493,7 @@ The structured form is equivalent and carries the experimental knobs below — ` Only `query` and `fields` are implemented. The engine expands `search` into a driver-agnostic -`$and`-of-`$or`-of-`$contains` filter (ADR-0061) — `fuzzy`, `operator`, `boost`, `minScore`, +`$and`-of-`$or`-of-`$icontains` filter (ADR-0061) — `fuzzy`, `operator`, `boost`, `minScore`, `language`, and `highlight` are accepted by `QuerySchema` but read nowhere in `expandSearchToFilter()` / `normalizeSearch()`, so they have no effect; since #4286 their `.describe()` markers say so. Multiple search terms are always AND-ed regardless of @@ -530,7 +530,7 @@ The answer is a **mirror field**: copy the related record's title into a stored field on this object and declare *that* field searchable. A task list searched by project name gets a `project_name` text column on `task`, maintained on write and listed in `task.searchableFields`. It has to be a **stored** field — a `formula` -field is virtual, so no driver has a column for `$contains` to scan. Cross-object +field is virtual, so no driver has a column for `$icontains` to scan. Cross-object search paths are rejected by design, not pending. Full recipe (the hooks that keep the mirror fresh, and the lint wording) in [Schema Design → Searching by a related record's title](/docs/data-modeling/schema-design#searching-by-a-related-records-title--mirror-the-value). diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index 09f9c3073f..979d19cac9 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -129,15 +129,15 @@ To let users search a task list by project name: } ``` -`?search=apollo` now expands to `name $contains 'apollo' OR project_name -$contains 'apollo'` — one single-table scan, on every driver, with no traversal. +`?search=apollo` now expands to `name $icontains 'apollo' OR project_name +$icontains 'apollo'` — one single-table scan, on every driver, with no traversal. If the object declares no `searchableFields` at all, a `text` mirror is picked up by the auto-default anyway; declare the set explicitly when you want to pin it. **The mirror must be a stored field — a `formula` field does not work.** A -`formula` field is *virtual*: no driver materializes a column for it, so a -`$contains` predicate against one has nothing to scan (the SQL driver would emit +`formula` field is *virtual*: no driver materializes a column for it, so an +`$icontains` predicate against one has nothing to scan (the SQL driver would emit a `WHERE` over a column that does not exist). A CEL formula also only reads this record's own fields (`record.`), so it cannot fetch the related title in the first place. diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index d627e567f7..5321bd5e14 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -969,9 +969,9 @@ disclose every other tenant's values for that column. ### Full-Text Search The `search` parameter does **not** reach a full-text index. The engine expands it into -an `$or` of `$contains` predicates across the object's server-resolved searchable fields +an `$or` of `$icontains` predicates across the object's server-resolved searchable fields (ADR-0061) and deletes `search` from the AST before the driver sees it — every driver -already runs `$or`/`$contains`, so no driver support is needed (which is also why the +already runs `$or`/`$icontains`, so no driver support is needed (which is also why the old `supports.fullTextSearch` capability bit had no reader and was retired in 17.0.0, #4634). @@ -1006,13 +1006,15 @@ because the engine-side intersection alone used to drop the unknown name and fal scanning the full searchable set. Internal callers reaching `engine.find()` directly keep the tolerant intersection. Multiple whitespace-separated terms are AND-ed and fields are OR-ed. Case sensitivity comes from the operator the expansion emits, not -from the expansion: it emits a plain `$contains`, which is **case-sensitive** by the -rule in [Case Sensitivity](#case-sensitivity) above. Note what that means for search — -a user typing `acme` does not find `ACME Corp`. Only `select` / `status` option -*labels* are matched case-insensitively by the expansion itself. +from the expansion: it emits `$icontains`, which ignores **ASCII case** by the +rule in [Case Sensitivity](#case-sensitivity) above. So a user typing `acme` does +find `ACME Corp` — search is case-insensitive by ruling +([#7641](https://github.com/objectstack-ai/objectstack/issues/7641)). `select` / +`status` option *labels* are matched case-insensitively too, by a separate +label→value mapping in the expansion rather than by the operator. - **Measured today: every driver matches that rule.** The `$contains` alignment + **Measured today: both operators mean one thing on every driver.** The `$contains` alignment landed in three steps — [#6518](https://github.com/objectstack-ai/objectstack/issues/6518) made `SqlDriver` case-exact per dialect (`GLOB` on the SQLite dialects, `LIKE` unchanged on @@ -1024,8 +1026,12 @@ a user typing `acme` does not find `ACME Corp`. Only `select` / `status` option the divergence this callout warned about is closed, and `FILTER_TEXT_CASES` holds all five drivers to it. Whether the expansion should emit `$icontains` instead of `$contains` — i.e. whether search is case-insensitive by - definition — remains a separate open question, and one that can now actually be - answered, since both operators mean one thing everywhere. + definition — is **settled, not open**: + [#7641](https://github.com/objectstack-ai/objectstack/issues/7641) ruled that it should, + and `expandSearchToFilter` has compiled `$search` to `$icontains` ever since. + `$contains` stays case-**sensitive** by contract + ([#4706](https://github.com/objectstack-ai/objectstack/issues/4706) Q2 = A) — the two + are different operators, not two spellings of one. `fuzzy`, `boost`, `operator`, `minScore`, `language`, and `highlight` carry `[EXPERIMENTAL — not enforced]` markers (#4286): the schema accepts them, the @@ -1050,8 +1056,8 @@ The declarative answer is a **mirror field**: copy the related record's title into a stored field on this object and declare *that* field searchable — a task list searched by project name carries a `project_name` text column on `task`, maintained on write and listed in `task.searchableFields`, so the expansion stays -a single-table `$or` of `$contains`. The mirror must be **stored**: a `formula` -field is virtual, no driver materializes a column for it, and a `$contains` +a single-table `$or` of `$icontains`. The mirror must be **stored**: a `formula` +field is virtual, no driver materializes a column for it, and an `$icontains` against one has nothing to scan. Cross-object search paths are rejected by design, not pending — see [Schema Design → Searching by a related record's title](/docs/data-modeling/schema-design#searching-by-a-related-records-title--mirror-the-value) diff --git a/content/docs/references/data/driver-nosql.mdx b/content/docs/references/data/driver-nosql.mdx index 7c8a2d349d..5e79c0aa0e 100644 --- a/content/docs/references/data/driver-nosql.mdx +++ b/content/docs/references/data/driver-nosql.mdx @@ -180,7 +180,7 @@ const result = AggregationPipelineSchema.parse(data); | **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | | **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | | **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | -| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. | | **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | | **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | | **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | diff --git a/content/docs/references/data/driver-sql.mdx b/content/docs/references/data/driver-sql.mdx index 41d220d345..8951ce2269 100644 --- a/content/docs/references/data/driver-sql.mdx +++ b/content/docs/references/data/driver-sql.mdx @@ -94,7 +94,7 @@ const result = DataTypeMappingSchema.parse(data); | **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | | **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | | **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | -| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. | | **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | | **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | | **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | diff --git a/content/docs/references/data/driver.mdx b/content/docs/references/data/driver.mdx index b4b8d5f56c..886d33b580 100644 --- a/content/docs/references/data/driver.mdx +++ b/content/docs/references/data/driver.mdx @@ -48,7 +48,7 @@ const result = DriverCapabilitiesSchema.parse(data); | **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | | **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | | **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | -| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. | | **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | | **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | | **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | @@ -102,7 +102,7 @@ const result = DriverCapabilitiesSchema.parse(data); | **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | | **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | | **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | -| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. | | **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | | **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | | **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | diff --git a/packages/spec/src/data/driver.zod.ts b/packages/spec/src/data/driver.zod.ts index 1e452f531b..4c4f935ee1 100644 --- a/packages/spec/src/data/driver.zod.ts +++ b/packages/spec/src/data/driver.zod.ts @@ -294,9 +294,10 @@ export const DriverCapabilitiesSchema = lazySchema(() => z.object({ + 'driver-side JOIN planning — no code consulted the bit.')), fullTextSearch: retiredKey(capRemoved('fullTextSearch', - '`$search` is compiled by the engine into an `$or` of `$contains` predicates over the ' + '`$search` is compiled by the engine into an `$or` of `$icontains` predicates over the ' + 'searchable fields (ADR-0061) and removed from the AST before the driver sees it — no ' - + 'driver-side full-text path exists.')), + + 'driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: ' + + 'textual search is case-insensitive by ruling (#7641).')), jsonQuery: retiredKey(capRemoved('jsonQuery', 'No engine path ever branched on driver-side JSON querying.')), geospatialQuery: retiredKey(capRemoved('geospatialQuery',