You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(spec): declare $icontains (ASCII fold), pin the $contains family case-sensitive, retire $regex (#5701)
The contract half of #4706's maintainer ruling (B). Declaration only: no live
rejection, no driver code touched, every backend answers exactly what it
answered before.
- `$icontains` on StringOperatorSchema / FieldOperatorsSchema / Filter: contains,
ignoring ASCII case (A-Z against a-z) and nothing else. The boundary is stated
for authors — `café` does NOT match `CAFÉ` — because ASCII is the one fold all
five backends can deliver: SQLite without ICU (so turso and sqlite-wasm too)
folds ASCII only, and promising Unicode would repeat the defect this retires.
- `$contains` / `$notContains` / `$startsWith` / `$endsWith` are declared
CASE-SENSITIVE, superseding the recorded "Case sensitivity should be handled at
backend level" (Prime Directive #13 — the old sentence is quoted in place so
the reversal is findable from the sentence a reader remembers). What that
non-guarantee bought, measured: three different answers across five backends,
two of them inside driver-memory alone (query path folds full Unicode, the
reference matcher is case-sensitive).
- `RETIRED_FILTER_OPERATORS`: pure data, no behaviour. `$regex` and `$options`
with prescriptions naming `$icontains`. Nothing rejects them here — the five
existing refusal sites are wired to it by #5702, and #5710 must flip the one
live producer (plugin-auth's ObjectQL adapter, on the authentication path)
first or sign-in breaks.
- `FILTER_TEXT_CASES` (`data/filter-text-conformance.ts`): the shared standard
for case folding, literal comparands and the `$regex` refusal, with the
`expectRejection` discriminant `filter-logic-conformance.ts` deliberately never
grew. A sibling table rather than rows in that one, whose charter excludes both
axes and warns against red rows for work nobody is dispatched to do. Registered
in check:driver-conformance with one MEASURED DEBT row per driver pointing at
#5702 — 25 covered / 5 DEBT / 0 exempt, main stays green.
`$icontains` is deliberately NOT in `FILTER_OPERATORS` yet. That array is a
runtime allowlist, not a word list: driver-memory's shape gate derives its
ACCEPTED set from it while its matcher's `default:` arm breaks. Measured by
adding it early and rebuilding — the gate stopped refusing and
`match({name:'zzz'}, {name:{$icontains:'acme'}})` returned true, i.e. the
predicate was silently dropped and every row matched. A dropped predicate widens
rather than narrows, which on an RLS read scope is a permission bypass (#3948).
It joins the array in the PR that implements it; `filter-operator-vocabulary.test.ts`
pins the difference at exactly {$icontains} so neither adding a second staged
operator nor clearing this one can happen silently.
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
Co-authored-by: Claude <noreply@anthropic.com>
* chore(spec): regenerate api-surface + authorable-surface after merging main
The merge driver (`merge=os-regen`, AGENTS.md §11) deferred both artifacts
rather than text-merging them, and `pre-commit` collects that debt. Regenerated
from the MERGED source, so the union includes #5721's ActionSession entries
alongside this branch's $icontains / FILTER_TEXT additions.
`authorable-surface.base.json` is deliberately NOT bumped: it is the deletion
gate's anchor, main only moves it when a key is REMOVED, and this branch only
adds. `check:authorable-surface` confirms it — 'trails the merge base by 5
key(s) — expected right after a surface change lands'.
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
|**$icontains**|`string`| optional | Contains substring, ignoring case — but ONLY ASCII case (A-Z against a-z). Every other character compares literally, so "café" does NOT match "CAFÉ" and "москва" does not match "МОСКВА". The domain is ASCII because that is the one fold all five backends can deliver: SQLite (and therefore turso and sqlite-wasm) folds ASCII only, so a Unicode promise here would be a guarantee three of the five could not keep. The comparand is matched LITERALLY — "%", "_" and regex metacharacters are ordinary characters, not wildcards. Case-SENSITIVE containment is $contains. [#5701: declared by the protocol; the driver lowerings land with #5702.]|
0 commit comments