From a8582eaf859fa21b05113d6a7aba518a69ab4def Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 10:28:04 +0000 Subject: [PATCH] =?UTF-8?q?docs(spec,formula,driver-memory):=20the=202026-?= =?UTF-8?q?08-10=20EXCLUDE=20ruling=20was=20withdrawn=20=E2=80=94=20record?= =?UTF-8?q?=20include=20as=20re-affirmed=20(#7406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #7391 (`5f7669e0c`) landed minutes before the maintainer withdrew cells 1 and 3 of the 2026-08-10 07:33Z ruling on #5299 and re-affirmed #5298's include direction as the platform semantics. The tree was left proclaiming a withdrawn target in `filter-logic-conformance.ts` — the one file whose job is to be the single source of truth — plus matching "ruled target / superseded direction" annotations in the formula and driver-memory pins. This rewrites that record. Nothing executes differently: only comments and `describe`/`it` titles move, and every `expect(...)` line is byte-identical. - The family-4 header note now reads as what it is: a reversal that was ruled, measured, and WITHDRAWN the same day. It moves out from under "RULED but not yet enrolled" — it is not a pending family and nothing waits on a backend — into its own section, and the singular wording of that heading is restored. The eleven-surface measurement table stays: it is the record of WHY the reversal was declined (a cross-backend programme buying silent absence in place of visible surplus, with no business pull), and the reason the null semantics are settled rather than merely unchallenged. - `$exists` = has-value (cell 2) was never in conflict and had already shipped in PR #5962. It stands, and is called out as the leg that survived. - The enrolled `$ne` / `$not` `d`-column rows state the affirmed direction instead of a superseded one. - The formula pin's section becomes "the affirmed direction"; the security coupling it documents (`formula` is the RLS write-side `check`, `read-scope-sql` the read-side lowering, converged by PR #5962) is kept as the tripwire for anyone re-proposing a reversal. - The two driver-memory pins are re-annotated against the affirmed direction: the matcher's `$notContains` answer is DIVERGENT again rather than canonical, `$nin` is half-converged, and on the live-vs-reference pin neither column is wholly right — reference is correct on `$exists`, live is correct on `$nin` and `$notContains`. Behaviour untouched per the #5499 freeze. One measurement-table cell that judged `driver-memory`'s reference matcher "already ruled-correct" on `$notContains` is corrected to DIVERGENT for the same reason. `.changeset/filter-null-semantics-ruled-target.md` is left alone: it describes what PR #7391 did at the time, and is historical. Refs #7406 #5299 #5298 #5146 #5962 #5499, PR #7391 (`5f7669e0c`) Co-Authored-By: Claude --- .../src/memory-driver-document-not.test.ts | 36 +++-- .../src/memory-matcher-not-null-safe.test.ts | 64 ++++---- .../src/matches-filter-not-null-safe.test.ts | 67 ++++---- .../spec/src/data/filter-logic-conformance.ts | 152 +++++++++++------- 4 files changed, 184 insertions(+), 135 deletions(-) diff --git a/packages/drivers/driver-memory/src/memory-driver-document-not.test.ts b/packages/drivers/driver-memory/src/memory-driver-document-not.test.ts index de3f87d469..c9e586ed24 100644 --- a/packages/drivers/driver-memory/src/memory-driver-document-not.test.ts +++ b/packages/drivers/driver-memory/src/memory-driver-document-not.test.ts @@ -203,22 +203,30 @@ describe('[#5324] InMemoryDriver.find compiles a document-level $not', () => { * where this measurement is recorded. Pinned as measured so the fix that lands * there has to move these lines deliberately. * - * ⚠️ [#5299, ruled 2026-08-10] The ruling is in, and it says the REFERENCE - * column below is the target on all three rows: SQL three-valued logic is the - * common denominator, so **negative operators never match no-value rows; the - * only ways to select "no value" are `$exists: false` / `$null: true`.** The - * `live` column is therefore the side that is wrong on every row here — - * mingo's `$exists` is key-presence, and its `$nin` / `$notContains` match a - * value that is not there. + * ⚠️ [#5299, settled 2026-08-10] The semantics are settled, and neither column + * below is wholly right — they are correct on complementary rows: * - * ⛔ Still not flipped, and by decision rather than by difficulty: this package - * is inside the #5499 investment freeze. Note also what the ruling assumed and - * this file disproves — it says "driver-memory already reads has-value" and - * "driver-memory and SQL already agree", which is true of the reference - * matcher and FALSE of the live query path users actually reach. That is the - * reason this pin exists. + * `$exists` REFERENCE is correct. `$exists` means "has a value" + * (#5298 ③ / #5369, PR #5962), so mingo's key-presence + * reading is the divergent one. + * `$nin` LIVE is correct. Negative operators MATCH no-value rows — + * #5146, extended by #5298, re-affirmed 2026-08-10 — so a + * missing key satisfying `$nin` is the affirmed answer, and + * the reference matcher's early-exit guard is the divergence. + * `$notContains` LIVE is correct, for the same reason. + * + * A ruling that morning (07:33Z) would have made the REFERENCE column the + * target on all three rows. Cells 1 and 3 of it were WITHDRAWN the same day, + * once the reversal's cross-backend cost had been measured, and the include + * direction was re-affirmed — which leaves the split above. + * + * ⛔ Nothing is flipped in either direction: this package is inside the #5499 + * investment freeze. What the round trip confirmed is exactly why this pin + * exists — this package answers with two different faces, so a statement like + * "driver-memory already reads has-value" is true of the reference matcher and + * FALSE of the live query path users actually reach. */ - describe('[#5299] the ruled cells, live vs reference — behaviour frozen (#5499)', () => { + describe('[#5299] the settled cells, live vs reference — behaviour frozen (#5499)', () => { const liveVsReference = async (where: unknown) => ({ live: await idsFrom(nulled, where), reference: NULLED.filter((r) => match(r, where)).map((r) => r.id), diff --git a/packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts b/packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts index 3d319fb11b..e893037b9b 100644 --- a/packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts +++ b/packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts @@ -128,25 +128,32 @@ describe('[#5146] memory-matcher — $not over records with no value', () => { }); }); - // ── The three cells #5299 ruled on — behaviour FROZEN, annotation current ── + // ── The three #5299 cells — behaviour FROZEN, annotation current ─────────── /** - * [#5299, ruled 2026-08-10] These three cells were filed as "known - * disagreements with `formula`, not ruled on by #5146". They are ruled now: - * SQL's native three-valued logic is the common denominator, so **negative - * operators never match no-value rows; the only ways to select "no value" are - * `$exists: false` / `$null: true`.** + * [#5299, settled 2026-08-10] These three cells were filed as "known + * disagreements with `formula`, not ruled on by #5146". They are settled now, + * and the settled direction is INCLUDE: `$ne` / `$nin` / `$notContains` MATCH + * a no-value row (#5146, extended by #5298, shipped across eleven surfaces), + * and `$exists` means "has a value" (#5298 ③ / #5369, PR #5962). * - * ⛔ Nothing below is flipped, and the reason is not inertia. This package is - * inside the #5499 investment freeze, and the ruling itself says - * `checkCondition`'s early-exit guard STAYS AS IT IS. What the ruling changed - * is the annotation: the section is no longer "a divergence nobody has ruled - * on", it is "a ruled target, with this matcher's distance from it measured". + * A ruling on 2026-08-10 07:33Z briefly went the other way — SQL's native + * three-valued logic as the common denominator, negative operators never + * matching no-value rows — which would have made this matcher's answers below + * the canonical ones. Cells 1 and 3 of it were WITHDRAWN the same day, once + * the reversal's cross-backend cost had been measured, and include was + * re-affirmed. `$exists` = has-value is the leg that stands. + * + * ⛔ Nothing below is flipped, and the reason is not inertia: this package is + * inside the #5499 investment freeze, and both rulings leave `checkCondition`'s + * early-exit guard exactly as it is. So this section is what it always was — a + * measured divergence from the platform answer, pinned rather than harmonised + * — with its wording brought current. * * Re-measured on `60f0dd8`, because the old wording had gone stale in a way * that mattered — it named `formula` as the key-presence reader on `$exists`, - * and `formula` stopped being that in PR #5962 (#5298 ③ / #5369). Where each - * cell actually stands: + * and `formula` stopped being that in PR #5962. Where each cell stands against + * the affirmed include direction: * * `$exists` CONVERGED, and this matcher was already right. Both * evaluators read "has a value"; the formula-side assertion @@ -155,29 +162,30 @@ describe('[#5146] memory-matcher — $not over records with no value', () => { * mingo query path and the analytics face — still read * key-presence, so the package disagrees with itself and this * test is the face that is correct. - * `$notContains` This matcher ALREADY answers the ruled semantics; every - * other surface in the repo (including all four SQL - * compilers, deliberately, via #5298's `nullSafeNegative`) - * answers the opposite. Here the gap is theirs, not ours. - * `$nin` HALF right: a missing key already does not match, a - * present-but-null value still does. The ruled answer is "no" - * for both. Frozen at this state. + * `$notContains` DIVERGES: a value-less field does not satisfy it here, + * while `formula` and all four SQL compilers say it does — + * deliberately, via #5298's `nullSafeNegative`. This matcher + * is the odd one out among the eleven surfaces, and frozen. + * `$nin` HALF converged: a present-but-null value already matches, + * as the affirmed direction requires; a MISSING key still + * does not, because the early-exit guard does not exempt + * `$nin`. Frozen at this state. */ - describe('[#5299] the ruled no-value cells — target recorded, behaviour frozen (#5499)', () => { + describe('[#5299] the settled no-value cells — divergence measured, behaviour frozen (#5499)', () => { it('$nin: an ABSENT field is treated differently from a null one', () => { // The early `value === undefined` guard in `checkCondition` exempts only // `$exists` / `$ne` / `$null`, so an absent field fails `$nin` outright - // while a null field passes it. The ruling keeps this guard; the NULL half - // is the part still short of the ruled answer, and it is frozen. + // while a null field passes it. The guard stays; the MISSING half is the + // part still short of the affirmed include answer, and it is frozen. expect(ids(NULLED, { $not: { stage: { $nin: ['won'] } } })).toEqual(['1']); expect(ids(MISSING, { $not: { stage: { $nin: ['won'] } } })).toEqual(['1', '3', '4']); }); - it('$notContains: a value-less field does NOT satisfy it here — the RULED answer', () => { - // `typeof null !== 'string'` → false, so the negation matches. This is - // what #5299 ruled canonical. `formula` and all four SQL compilers answer - // the opposite today; moving them is a cross-backend programme, not a - // change to this file. + it('$notContains: a value-less field does NOT satisfy it here — DIVERGENT', () => { + // `typeof null !== 'string'` → false, so the negation matches. The + // affirmed direction is the opposite: `formula` and all four SQL compilers + // say a value-less field DOES satisfy `$notContains`, and #5299 re-affirmed + // that on 2026-08-10. This matcher stays where it is under #5499. expect(matched({ $not: { stage: { $notContains: 'w' } } })).toEqual(['1', '3', '4']); }); diff --git a/packages/formula/src/matches-filter-not-null-safe.test.ts b/packages/formula/src/matches-filter-not-null-safe.test.ts index b898a9b194..7213d924c4 100644 --- a/packages/formula/src/matches-filter-not-null-safe.test.ts +++ b/packages/formula/src/matches-filter-not-null-safe.test.ts @@ -119,9 +119,10 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', ( // rejects it. `driver-sql` follows this answer; `driver-memory`'s // REFERENCE matcher answers the opposite for a null-valued field. // - // ⚠️ [#5299, ruled 2026-08-10] This is the SUPERSEDED direction — see the - // block at the bottom of this file for the ruled target and the measured - // reason nothing has moved yet. + // ⚠️ [#5299, 2026-08-10] A ruling that morning would have reversed this + // direction; it was WITHDRAWN the same day and include re-affirmed. See + // the block at the bottom of this file for the affirmed direction and the + // measurement that settled it. expect(matched({ $not: { stage: { $notContains: 'w' } } })).toEqual(['1']); }); @@ -187,41 +188,44 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', ( }); }); - // ── The NON-negated negatives — pinned against a ruling that has not landed ─ + // ── The NON-negated negatives — the affirmed direction, pinned ───────────── /** - * [#5299, ruled 2026-08-10] The maintainer took SQL's native three-valued - * logic as the common denominator: **negative operators never match no-value - * rows; the only ways to select "no value" are `$exists: false` / - * `$null: true`.** Under that rule this evaluator answers `['2']` below. + * [#5299, re-affirmed 2026-08-10] `$ne` / `$nin` / `$notContains` MATCH a + * no-value row. That is the platform semantics: ruled by #5146 for `$not`, + * extended to this operator family by #5298, and shipped across all eleven + * filter surfaces. This evaluator answers `['2','3','4']` below — the affirmed + * answer, not a lag behind a target. * - * It answers `['2','3','4']`, and that is pinned here rather than fixed, - * because flipping it ALONE would re-open the exact hole PR #5962 closed. That - * PR converged `formula` (the RLS write-side `check`) and `read-scope-sql` - * (the read-side lowering) in ONE change precisely because they are - * security-coupled: one policy string must not admit two row sets. Every SQL - * face still emits `nullSafeNegative` for these two operators + * It was challenged and it held. A ruling on 2026-08-10 07:33Z would have + * taken SQL's native three-valued logic as the common denominator — negative + * operators never matching no-value rows, `['2']` below — and cells 1 and 3 of + * it were WITHDRAWN the same day once the reversal's cost had been measured + * across every surface. + * + * These assertions are load-bearing under either direction, and the coupling + * that makes them so is a large part of why the reversal was declined: this + * evaluator is the RLS write-side `check` and `read-scope-sql` is the + * read-side lowering, and PR #5962 converged them in ONE change precisely + * because they are security-coupled — one policy string must not admit two row + * sets. Every SQL face emits `nullSafeNegative` for these two operators * (`col IS NULL OR col NOT IN (…)`), so a formula-only flip would make an RLS * `check` DENY a write on a null field that the read scope still RETURNS — - * #5962's defect with the sign reversed. - * - * So these assertions are load-bearing in both directions. They say what this - * evaluator does today, and they are the tripwire the cross-backend PR must - * step on: whoever lands the ruled semantics changes these lines DELIBERATELY, - * in the same PR that moves `driver-sql`, `read-scope-sql`, `filter-normalizer` - * and `driver-turso`'s remote transport — not one evaluator at a time. + * #5962's defect with the sign reversed. Anyone re-proposing a reversal steps + * on these lines DELIBERATELY, in the same PR that moves `driver-sql`, + * `read-scope-sql`, `filter-normalizer` and `driver-turso`'s remote transport + * — not one evaluator at a time. * - * The full eleven-surface measurement and the enrolment blocker (the - * conformance ledger has no per-row DEBT, and two of the five scored drivers - * are inside the #5499 freeze) are recorded on family 4 in - * `@objectstack/spec`'s `filter-logic-conformance.ts` header. + * The full eleven-surface measurement, and why the reversal was declined, are + * recorded on family 4 in `@objectstack/spec`'s + * `filter-logic-conformance.ts` header. */ - describe('[#5299] $notContains / $nin over a value-less field — the pre-ruling answer', () => { - it('$notContains MATCHES a value-less field — ruled target is that it must NOT', () => { + describe('[#5299] $notContains / $nin over a value-less field — the affirmed direction', () => { + it('$notContains MATCHES a value-less field — re-affirmed 2026-08-10', () => { expect(matched({ stage: { $notContains: 'w' } })).toEqual(['2', '3', '4']); }); - it('$nin MATCHES a value-less field — ruled target is that it must NOT', () => { + it('$nin MATCHES a value-less field — re-affirmed 2026-08-10', () => { expect(matched({ stage: { $nin: ['won'] } })).toEqual(['2', '3', '4']); }); @@ -233,9 +237,10 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', ( expect(matched({ stage: { $nin: ['won'] } })).toEqual(matched({ stage: { $ne: 'won' } })); }); - it('the ruled ESCAPE HATCH already works, in both directions', () => { - // Whatever happens to the three cells above, the rule's second half is - // already true here: "no value" is selectable, precisely, today. + it('the ESCAPE HATCH works, in both directions', () => { + // The half of the 07:33Z rule that was never in dispute, and that its + // withdrawal did not touch: "no value" is selectable, precisely, today — + // and `$exists` reads has-value (cell 2, shipped in PR #5962, stands). expect(matched({ stage: { $exists: false } })).toEqual(['3', '4']); expect(matched({ stage: { $null: true } })).toEqual(['3', '4']); expect(matched({ stage: { $exists: true } })).toEqual(['1', '2']); diff --git a/packages/spec/src/data/filter-logic-conformance.ts b/packages/spec/src/data/filter-logic-conformance.ts index ed1891774a..c68babb4dd 100644 --- a/packages/spec/src/data/filter-logic-conformance.ts +++ b/packages/spec/src/data/filter-logic-conformance.ts @@ -73,18 +73,19 @@ * {@link FilterLogicRow.d} column carries it, and the four `d`-column cases * below enforce it on every backend. * - * ⚠️ That answer is the INCLUDE direction, and a later ruling has replaced it as - * the TARGET without any backend having moved yet. The four `d`-column cases - * below still state what the tree does, which is what a conformance table is - * for — but do not read them as the settled semantics. Family 4 below is the - * ruling that supersedes them and the measurement of what it costs. + * ⚠️ That answer — the INCLUDE direction — was reversed by a ruling on + * 2026-08-10 and RE-AFFIRMED the same day, once the reversal's full cost had + * been measured. So the four `d`-column cases below ARE the settled semantics, + * not a placeholder for a pending target. The record of that round trip is + * family 4 at the bottom of this note, kept because the measurement in it is + * what makes this answer settled rather than merely unchallenged. * * ## Case families that are RULED but not yet enrolled * - * Both families left were ruled by the maintainer and are implemented in some - * backends. They are not in the table yet — a red row here does not enforce a + * The one family left was ruled by the maintainer and is implemented in some + * backends. It is not in the table yet — a red row here does not enforce a * ruling, it just turns another lane's unfinished work into this table's - * failure, and each family still has a blocker standing, named below. Add the + * failure, and the family still has a blocker standing, named below. Add the * rows in the PR that closes the gap, not before. * * Two families have GRADUATED out of this note, and how they did is the note's @@ -113,7 +114,7 @@ * remains" written from a reading of the backend LIST rather than from a * measurement is the sentence that hides the next one. * - * The family numbering of the ones that remain is kept as their historical id. + * The family numbering of the one that remains is kept as its historical id. * * ### 3. `{ field: {} }` — a field constrained by zero operators (#5240) * @@ -131,41 +132,51 @@ * invented here. The case lands with that extension, alongside the * schema-side narrowing that stays with the spec lane. * - * ### 4. Negative operators over a no-value row — the EXCLUDE direction (#5299) - * - * Ruled by the maintainer on **2026-08-10** (#5299): *SQL three-valued logic is - * the common denominator; align both JS evaluators to it.* Concretely — - * `$notContains` on a value-less field does NOT match; `$exists` means "has a - * value" (`!= null`), never key-presence; `$nin` on a no-value field does NOT - * match. Stated for authors as one rule: **negative operators never match - * no-value rows; select "no value" with `$exists: false` / `$null: true`.** - * - * This REVERSES the include direction #5298 ruled on 2026-08-06 and #5146 ruled - * for `$not` before it — the same direction the four `d`-column cases above - * currently enforce. So the two enrolled rows `$ne returns the rows with no - * value` and `$not returns the rows with no value` are not neighbours of this - * family, they are the SAME family stating the opposite answer: under native - * three-valued SQL, `d <> 'v1'` and `NOT (d = 'v1')` are UNKNOWN for a NULL `d` - * and return `['2']`, not `['2','3','4']`. Enrolling family 4 therefore means - * re-ruling those two rows in the same PR, not adding rows beside them. - * - * ⛔ What blocks enrolment is NOT a missing wording, and it is worth stating - * because the obvious workaround does not exist: **the DEBT ledger in - * `scripts/check-driver-conformance.mjs` is per (driver × case-set), not per - * case.** An entry says "this driver's suite does not import this marker at - * all". There is no spelling for "this driver fails one row while passing the - * other thirty-five", so a row added ahead of a backend is simply a red gate — - * the thing #5903's note calls "a gate that reports a known red", which teaches - * every agent reading CI to discount the colour. + * ## A reversal that was ruled, measured, and WITHDRAWN — the record (#5299) + * + * Not a pending family, and nothing here is waiting on a backend. It is kept + * because the measurement below is the reason the null semantics above are + * settled, and because a record of a reversal that was costed and declined is + * what stops the same proposal being re-derived from first principles. The + * family number is its historical id. + * + * ### 4. Negative operators over a no-value row — EXCLUDE ruled, then withdrawn + * + * Ruled by the maintainer on **2026-08-10 07:33Z** (#5299): *SQL three-valued + * logic is the common denominator; align both JS evaluators to it* — concretely + * `$notContains` and `$nin` would stop matching no-value rows, and `$exists` + * would mean "has a value" (`!= null`) rather than key-presence. + * + * **Cells 1 and 3 of that ruling were WITHDRAWN the same day**, hours later, + * with the eleven-surface measurement below in hand: 「选「维持 include」」 — + * maintain include. So the settled platform semantics are the ones this table + * already enforces: + * + * - **`$ne` / `$nin` / `$notContains` on a no-value row MATCH** — the include + * direction, ruled by #5146 for `$not`, extended to the operator family by + * #5298, shipped across all eleven surfaces below via `nullSafeNegative` and + * `nullValueSatisfiesOperator`, and enrolled in {@link FILTER_LOGIC_CASES}. + * Not merely unchallenged — challenged, measured, and re-affirmed with the + * reversal option on the table. + * - **`$exists` means "has a value"** (`!= null`), never key-presence — cell 2, + * the leg of the 07:33Z ruling that was never in conflict with #5298 and had + * already shipped in PR #5962. It stands. * - * The measurement, taken on `60f0dd8` by adding the candidate rows to this - * table and running every suite that drives it. `MATCH` = a no-value row - * satisfies the operator, i.e. the include direction the ruling reverses: + * So the two enrolled rows `$ne returns the rows with no value` and `$not + * returns the rows with no value` state the affirmed answer, and the `['2']` + * that native three-valued SQL would give for `d <> 'v1'` is the answer this + * platform deliberately does NOT take. + * + * ### Why the reversal was declined — the measurement + * + * Taken on `60f0dd8` by adding the candidate rows to this table and running + * every suite that drives it. `MATCH` = a no-value row satisfies the operator, + * i.e. the include direction — the affirmed one: * * | Surface | `$notContains` | `$nin` | `$exists: true` on a null value | * |---|---|---|---| * | `formula` `matchesFilterCondition` | MATCH | MATCH | no — already ruled-correct (#5962) | - * | `driver-memory` reference matcher | no — already ruled-correct | MATCH on a null value, no on a missing key | no — already ruled-correct | + * | `driver-memory` reference matcher | no — DIVERGENT, frozen (#5499) | MATCH on a null value, no on a missing key | no — ruled-correct (#5962) | * | `driver-memory` live mingo path | MATCH | MATCH | MATCH — reads KEY-PRESENCE | * | `driver-memory` analytics face | MATCH | MATCH | MATCH — reads KEY-PRESENCE | * | `driver-sql` / `driver-sqlite-wasm` / `driver-turso` local | MATCH | MATCH | no — `IS NOT NULL` | @@ -179,21 +190,37 @@ * and the four independent copies of `nullValueSatisfiesOperator` * (`$nin` → true, `$notContains` → true) in `sql-driver.ts`, * `read-scope-sql.ts`, `filter-normalizer.ts` and `remote-transport.ts`. So - * "SQL already agrees" is false of this tree: SQL was bent TOWARDS the JS - * answer, and the ruling asks for it to be bent back. - * - * Two of the eleven surfaces — every `driver-memory` face and `driver-mongodb` - * — are inside the #5499 investment freeze, and they are two of the FIVE - * drivers this gate scores. Since the ledger cannot carry a per-row exemption, - * enrolment cannot happen while the freeze stands, whatever the other nine do. - * - * ⚠️ `$exists` is the one cell of the three that is NOT open on the JS - * evaluators: `formula` and `driver-memory`'s reference matcher both already - * read "has a value" (#5298 ③ / #5369, landed in #5962). What is still open on - * that cell is `driver-memory`'s live mingo path and `driver-mongodb`, both of - * which read key-presence — both frozen. So the ruling's `$exists` leg needs no - * work on the surface it names, and cannot be enrolled because of two it does - * not. + * "SQL already agrees" was false of this tree: SQL had been bent TOWARDS the JS + * answer on purpose, and the reversal asked for it to be bent back. + * + * That is what the table decided. The reversal was not a wording change but a + * cross-backend programme: flip `formula` and `service-analytics`'s + * `read-scope-sql` in ONE change (they are security-coupled — the RLS write-side + * `check` and the read-side lowering; splitting them re-opens the hole PR #5962 + * closed, with the sign reversed), rewrite four independent copies of + * `nullValueSatisfiesOperator`, re-rule the two enrolled `$ne` / `$not` rows, + * and touch the live query paths of two backends inside the #5499 investment + * freeze. What it buys is a WORSE failure mode: a filter that silently drops + * rows the author expected, in place of one that returns rows they can see and + * narrow — silent absence for visible surplus. With no business pull behind it, + * the maintainer kept include. + * + * ⚠️ One cell of the three is still short of its ruling, and it is the cell that + * SURVIVED. `$exists` = has-value is settled and shipped on the surfaces the + * ruling named — `formula` and `driver-memory`'s reference matcher both read it + * that way (#5298 ③ / #5369, landed in #5962). Still reading key-presence: + * `driver-memory`'s live mingo path and `driver-mongodb`, both frozen by #5499, + * and both among the FIVE drivers this gate scores. So a `$exists` row cannot be + * enrolled here yet. + * + * ⛔ And the blocker on that row is NOT a missing wording, which is worth + * stating because the obvious workaround does not exist: **the DEBT ledger in + * `scripts/check-driver-conformance.mjs` is per (driver × case-set), not per + * case.** An entry says "this driver's suite does not import this marker at + * all". There is no spelling for "this driver fails one row while passing the + * other thirty-five", so a row added ahead of a backend is simply a red gate — + * the thing #5903's note calls "a gate that reports a known red", which teaches + * every agent reading CI to discount the colour. */ import type { FilterCondition } from './filter.zod'; @@ -385,14 +412,15 @@ export const FILTER_LOGIC_CASES: readonly FilterLogicCase[] = [ // until it did, enrolling these two rows would have been a gate that reports a // known red — which teaches every agent reading CI to discount the colour. // - // ⚠️ [#5299, ruled 2026-08-10] These two rows now state the SUPERSEDED - // direction. The ruling took SQL's native three-valued logic as the common - // denominator, under which `d <> 'v1'` and `NOT (d = 'v1')` are UNKNOWN for a - // NULL `d` and both rows become `['2']`. They are left as they are on purpose: - // they are what all eleven surfaces answer today, and a conformance table that - // states the target instead of the tree is a gate reporting a known red. They - // move in the PR that moves the backends — see family 4 in this file's header - // for the measured blocker list. + // ⚠️ [#5299, 2026-08-10] These two rows were RE-AFFIRMED that day, and the + // round trip is worth knowing before anyone proposes it again. A morning + // ruling took SQL's native three-valued logic as the common denominator — + // `d <> 'v1'` and `NOT (d = 'v1')` are UNKNOWN for a NULL `d`, so both rows + // would become `['2']`. Hours later, with the cross-backend cost measured on + // all eleven surfaces, the maintainer WITHDREW that direction and kept + // include. So these rows are the settled semantics, not a tree lagging its + // target — see family 4 in this file's header for the measurement and why the + // reversal was declined. { name: '$ne returns the rows with no value', filter: { d: { $ne: 'v1' } },