diff --git a/.changeset/filter-text-status-claims-remeasured.md b/.changeset/filter-text-status-claims-remeasured.md new file mode 100644 index 0000000000..8d976fc369 --- /dev/null +++ b/.changeset/filter-text-status-claims-remeasured.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): re-measure and rewrite four expired post-#5702/#5710 status claims in `src/data` (#6993) + +Four status sentences in `filter-text-conformance.ts`, `filter-text-conformance.test.ts` +and `filter.zod.ts` still described the pre-#5702/#5710 world as current: "`$icontains` +… implemented by nobody", "a standard no backend answers yet", the `$regex` retirement +block's "hard order: #5710 flips the producer, then #5702 turns these strings into +refusals" (both gates fired since), and "`$options: 'i'` are #5702's work" (the fold is +still there; its owner is #6682 now). Each was re-measured by executing every face — +the five drivers (both turso transports), `formula`, objectql `having`, the analytics +read-scope compiler — plus a fresh run of `scripts/check-driver-conformance.mjs`, and +rewritten to state the shipped reality with dated re-verification markers, pointing at +the gate-maintained conformance ledger instead of hand counts where one exists. + +No behaviour change: no operator added to `FILTER_OPERATORS`, no refusal or assertion +touched, generated artifacts byte-identical. The one measured gap the census surfaced +(objectql `having` refuses retired operators outside the ADR-0112 envelope) is filed +as #7047, not fixed here. diff --git a/packages/spec/src/data/filter-text-conformance.test.ts b/packages/spec/src/data/filter-text-conformance.test.ts index 52740c8312..3997686cc5 100644 --- a/packages/spec/src/data/filter-text-conformance.test.ts +++ b/packages/spec/src/data/filter-text-conformance.test.ts @@ -1,11 +1,16 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * `FILTER_TEXT_CASES` is a standard no backend answers yet (#5701 is the - * contract half of the #4706 ruling; #5702 writes the lowerings). That makes it - * unusually easy for the table to be quietly WRONG — nothing executes it, so a - * miscounted `expected` list would sit there until a driver author trusted it - * and chased their own correct implementation. + * `FILTER_TEXT_CASES` landed as a standard no backend answered yet (#5701, the + * contract half of the #4706 ruling). That premise was true when this file was + * written and has since been falsified: the SQL-family drivers import and + * execute the whole table — that import is what + * `scripts/check-driver-conformance.mjs` counts as coverage, and its ledger's + * DEBT rows are the open remainder (re-verified 2026-08, #6993). What has NOT + * expired is this file's reason to exist: in the window where nothing executed + * the table, a miscounted `expected` list would have sat quietly wrong until a + * driver author trusted it — and between driver runs, the same oracle is still + * what keeps the table honest. * * So this file executes it, against a reference evaluator written from the * declared semantics: ASCII-only case folding, literal comparands. If the table diff --git a/packages/spec/src/data/filter-text-conformance.ts b/packages/spec/src/data/filter-text-conformance.ts index 2d20153985..1e89bd1b5f 100644 --- a/packages/spec/src/data/filter-text-conformance.ts +++ b/packages/spec/src/data/filter-text-conformance.ts @@ -34,22 +34,40 @@ * discriminant. #5240's `{ field: {} }` family can adopt the shape from here * without reopening the logic table. * - * ## Status: NO backend answers this table yet — that is the design + * ## Status: the SQL family answers this table; the ledger counts the rest * - * This is the contract half of the #4706 ruling (#5701). `$icontains` is - * declared by `StringOperatorSchema` (`filter.zod.ts`) and implemented by nobody; the - * `$contains` family's case-sensitivity is declared and honoured by two of five - * backends. Every driver therefore carries a measured DEBT row in - * `scripts/check-driver-conformance.mjs`, pointing at **#5702**, which is the - * issue that writes the lowerings and deletes the rows. A suite arriving here - * before then would be red about work nobody has been dispatched to do, which - * is the failure mode rule 2 above names. + * This is the contract half of the #4706 ruling (#5701). What this section + * said when the table landed — `$icontains` "implemented by nobody", the + * `$contains` family's case-sensitivity "honoured by two of five backends", + * "every driver carries a measured DEBT row" pointing at #5702 — was true in + * the #5701 era and has since been falsified by #5702 (closed 2026-08-08) and + * #6518 (re-verified 2026-08 by EXECUTING every face, #6993 — a text scan + * undercounts here, because `driver-sqlite-wasm` inherits its compiler and + * carries no case arm of its own): * - * The `$regex` rejection cases carry a further ordering constraint: `$regex` - * still has one LIVE producer — `plugin-auth`'s ObjectQL adapter emits - * `{ field: { $regex: value } }` for better-auth's `contains` search, on the - * authentication path. **#5710 flips that producer first.** A backend that - * enrols these cases before #5710 lands breaks sign-in. + * - `driver-sql`, `driver-sqlite-wasm` (inherited, on the sql.js engine) and + * `driver-turso` (both transports) answer `$icontains` and answer the + * `$contains` family case-exactly; their suites import this whole table, + * which is what `scripts/check-driver-conformance.mjs` counts as coverage. + * - `driver-memory` and `driver-mongodb` refuse `$icontains` with + * `INVALID_FILTER` / 400 (#6520) and still fold the `$contains` family over + * the whole Unicode range (#6682). Each carries a measured DEBT row in that + * same gate's ledger — the ledger is RECONCILED against the imports on + * every run, so read the open set THERE rather than trusting a count + * written in prose here. + * + * Rule 2 above still governs the open cells: the rows join a driver's suite + * in the PR that closes its gap, not before. + * + * The `$regex` rejection cases carried a further ordering constraint when + * this table landed: `plugin-auth`'s ObjectQL adapter still emitted + * `{ field: { $regex: value } }` on the authentication path, so a backend + * enrolling them early would have broken sign-in. That constraint is history + * rather than advice now — #5710 flipped the producer to `$contains` (the + * whole-repo re-scan finding no other live producer is recorded in + * `driver-memory/src/filter-refusal.ts`), and the refusal sites print + * `RETIRED_FILTER_OPERATORS`' prescriptions (re-verified 2026-08, #6993; the + * per-face envelope census lives on that table's own docblock). * * ## What belongs here * @@ -61,7 +79,9 @@ * @see FILTER_LOGIC_CASES — combinator semantics, the sibling standard. * @see https://github.com/objectstack-ai/objectstack/issues/4706 (the ruling) * @see https://github.com/objectstack-ai/objectstack/issues/5701 (this table) - * @see https://github.com/objectstack-ai/objectstack/issues/5702 (the backends) + * @see https://github.com/objectstack-ai/objectstack/issues/5702 (the SQL family — landed) + * @see https://github.com/objectstack-ai/objectstack/issues/6520 ($icontains on the JS faces — open) + * @see https://github.com/objectstack-ai/objectstack/issues/6682 (the $contains family on memory + mongodb — open) */ import type { FilterCondition } from './filter.zod'; @@ -222,14 +242,20 @@ export const FILTER_TEXT_CASES: readonly FilterTextCase[] = [ // ── The `$contains` family is CASE-SENSITIVE (#4706 Q2 = A) ──────────────── // // Supersedes `filter.zod.ts`\'s former "Case sensitivity should be handled at - // backend level". Two of five backends already answer this way - // (driver-memory, formula); the SQL family\'s LIKE and mongo\'s hardcoded - // `$options: 'i'` are #5702\'s work. + // backend level". "The SQL family\'s LIKE and mongo\'s hardcoded `$options: 'i'` + // are #5702\'s work" was the score when these rows landed and has since split + // (re-measured 2026-08, #6993, by executing each face): #6518 made the SQL + // family case-exact (GLOB on the SQLite dialects), so those three drivers + // answer these rows today, while mongo\'s `$options: 'i'` is STILL hardcoded + // (`translateFilter` lowers `$contains` to `$regex` + `$options: 'i'`) and + // driver-memory\'s query path still folds Unicode — that remainder is #6682\'s + // work now, not #5702\'s. (`formula` and driver-memory\'s reference matcher + // measured case-exact both then and now.) { name: '$contains is case-SENSITIVE — a lower-case comparand misses the upper-case row', filter: { name: { $contains: 'acme' } }, expected: ['2'], - note: 'Row 1 (ACME Corp) must NOT match. On SQLite/turso today LIKE folds ASCII and returns both.', + note: 'Row 1 (ACME Corp) must NOT match. SQLite\'s LIKE folds ASCII — the defect #6518 replaced with GLOB on the SQLite dialects; a backend returning both here has regressed to it (driver-memory / driver-mongodb still fold — #6682).', }, { name: '$contains is case-SENSITIVE — an upper-case comparand misses the lower-case row', @@ -269,7 +295,7 @@ export const FILTER_TEXT_CASES: readonly FilterTextCase[] = [ expectRejection: true, code: 'INVALID_FILTER', mustMention: ['$regex', '$options', '$icontains'], - note: 'The exact shape plugin-auth\'s adapter can emit, and the one `$icontains` replaces one-for-one. #5710 flips that producer BEFORE any backend enrols this case. "One mistake" is about the AUTHOR\'s fix being single (write $icontains), not about the message naming one key: it must name BOTH retired spellings, or an author who fixes only $regex trips the dangling-$options refusal on the next attempt.', + note: 'The exact shape plugin-auth\'s adapter used to emit, and the one `$icontains` replaces one-for-one. #5710 flipped that producer before any backend enrolled this case (re-verified 2026-08, #6993). "One mistake" is about the AUTHOR\'s fix being single (write $icontains), not about the message naming one key: it must name BOTH retired spellings, or an author who fixes only $regex trips the dangling-$options refusal on the next attempt.', }, { name: 'a dangling $options with no $regex is REFUSED', diff --git a/packages/spec/src/data/filter.zod.ts b/packages/spec/src/data/filter.zod.ts index 0740bcfec4..d990301e3e 100644 --- a/packages/spec/src/data/filter.zod.ts +++ b/packages/spec/src/data/filter.zod.ts @@ -1302,15 +1302,23 @@ export interface RetiredFilterOperatorGuidance { * took that trade explicitly: the spec declares, the existing refusal sites * enforce. * - * Those sites are the five that already refuse unknown operators today — + * Those sites are the five that refuse unknown operators — * `driver-sql`'s `default:` arm, `driver-turso`'s remote transport, * `driver-memory`'s `filter-refusal.ts`, `driver-mongodb`'s * `translateFieldOperators`, and `objectql`'s `having` — and the point of one - * table is that they stop each writing their own sentence. Wiring them to it is - * **#5702**, deliberately not this PR: `$regex` still has one live producer - * (`plugin-auth`'s ObjectQL adapter, on the authentication path), so a refusal - * landing before #5710 flips that producer would break sign-in. Hard order: - * **#5710 flips the producer, then #5702 turns these strings into refusals.** + * table is that they stop each writing their own sentence. When this block was + * written, wiring them was deliberately deferred behind a hard order ("#5710 + * flips the producer, then #5702 turns these strings into refusals"), because + * `$regex` still had one live producer on the authentication path. Both gates + * have fired since — #5710 flipped `plugin-auth`'s adapter to `$contains`, + * #5702 wired all five sites — so that ordering is shipped history, not + * pending advice. Census per face, by executing `{ $regex }` and a dangling + * `{ $options }` against each (re-verified 2026-08, #6993): all five print + * this table's `why` verbatim; the four driver faces throw it in the ADR-0112 + * envelope (`INVALID_FILTER` / 400 — `driver-sqlite-wasm` and both turso + * transports inherit or mirror it), while `having`'s refusal is still a bare + * `Error` carrying the sentence without `code`/`status` — the one open half, + * tracked as #7047. * * ## Why `$regex` was retired rather than implemented (#4706) *