diff --git a/.changeset/filter-logic-conformance-mongodb-wasm.md b/.changeset/filter-logic-conformance-mongodb-wasm.md new file mode 100644 index 0000000000..96ea79fb48 --- /dev/null +++ b/.changeset/filter-logic-conformance-mongodb-wasm.md @@ -0,0 +1,53 @@ +--- +"@objectstack/driver-mongodb": patch +"@objectstack/driver-sqlite-wasm": patch +--- + +test(drivers): the filter-logic standard now covers the backend it was counted without (#4405) + +`FILTER_LOGIC_CASES` (#3774) opens by calling itself the standard "the four +independent FilterCondition backends are each checked against". Five backends +exist. `driver-mongodb`'s `translateFilter` was missed, not excluded — an +independent implementation whose `$and`/`$or`/`$not` translation shares no line +of code with the SQL compiler or the in-memory matcher, and the only one whose +target language cannot spell the standard directly: MongoDB has no +document-level `$not` at all (the server answers `unknown top level operator: +$not`), so a negation has to leave as `$nor`, and a branch's own keys have to +stay in one document while `$and`/`$or` clauses are lifted beside them. That +route was never checked against the shared cases. Both DEBT rows the #4363 gate +recorded are now cleared, and `scripts/check-driver-conformance.mjs` reports +`ok` for every cell of the matrix. + +**`driver-mongodb` runs the table twice, and the split is deliberate.** +`mongodb-filter-logic-translation.test.ts` drives every shared case through +`translateFilter` and evaluates the emitted MongoDB *document* over the shared +fixture — a pure function, no server, so it always runs. That matters here more +than anywhere: `mongodb-memory-server` downloads a ~123 MB binary from +fastdl.mongodb.org, and a defect only a downloadable binary can catch is a +defect nobody catches on a restricted network. Its in-process reader is strict +by construction — every shape it does not model throws instead of evaluating to +true, a document-level `$not` included — and its own discrimination is pinned by +cases that require a widened document to FAIL the case it widens, so "all green" +cannot mean "the reader says yes to everything". +`mongodb-filter-logic-conformance.test.ts` runs the same table against a real +mongod and answers the one question the first half cannot — does MongoDB agree? +— skipping cleanly (never silently) when the binary is unreachable. + +**`driver-sqlite-wasm` runs the table through its own engine.** It inherits +`SqlDriver`'s filter compiler, so nothing is re-implemented; what the suite pins +is that a nested `(… AND …) OR (… AND …)` survives the custom sql.js dialect +that compiles, binds and marshals it — the same seam its temporal and pagination +suites cover for their clauses. Tracked as DEBT rather than EXEMPT because +"inherits, therefore fine" is the assumption those suites exist to disprove; the +suite is what disproves it. + +**No divergence was found.** `translateFilter` answers all seventeen shared +cases correctly today, `$not`-inside-a-branch and nested `$and`-inside-`$or` +included, so no translation change ships here — what changes is that the next +edit to it cannot quietly widen a filter. Both suites were verified to be +discriminating rather than decorative by reintroducing the #3774 miscompile +(propagating `or` into a branch's own contents): 15 of the mongodb translation +suite's 26 tests fail, and 13 of the wasm suite's 18. + +`packages/spec`'s `filter-logic-conformance.ts` header now says five and names +the fifth — a code comment; no schema, export or generated artifact moved. diff --git a/packages/plugins/driver-mongodb/src/mongodb-filter-logic-conformance.test.ts b/packages/plugins/driver-mongodb/src/mongodb-filter-logic-conformance.test.ts new file mode 100644 index 0000000000..ce49b657ae --- /dev/null +++ b/packages/plugins/driver-mongodb/src/mongodb-filter-logic-conformance.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Filter logical-combinator conformance for the MongoDB driver, against a REAL + * mongod (#4405) — the half that answers whether MongoDB agrees. + * + * The shared cases come from `@objectstack/spec/data`, so this backend now + * stands beside `driver-sql`, `driver-memory`, `formula`'s + * `matchesFilterCondition` and `read-scope-sql` under one standard (#3774). + * `mongodb-filter.ts` reaches that standard by a completely separate route: + * MongoDB has no document-level `$not`, so a negation is emitted as `$nor`, and + * a branch's own keys have to stay inside one document while `$and`/`$or` + * clauses are lifted beside them. Whether that route arrives at the same rows + * is not a question a translator test can close — it is a question about the + * server's evaluation of the document, and this file is where it is asked. + * + * The same table is driven server-free by + * `mongodb-filter-logic-translation.test.ts`, which is the half that always + * runs. This one skips when the mongod binary cannot be fetched (the + * `createTestMongod` convention every suite in this package uses — a blocked or + * hanging download costs a skipped suite, not a stalled test job). **A skip is + * not a pass**: on a machine without the binary, the translation suite is the + * whole proof, which is exactly why it carries the priority half. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { MongoMemoryServer } from 'mongodb-memory-server'; +import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data'; +import { MongoDBDriver } from './mongodb-driver.js'; +import { createTestMongod } from './test-mongod.js'; + +const sharedMongod: MongoMemoryServer | undefined = await createTestMongod('filter logic conformance'); + +describe.skipIf(!sharedMongod)('driver-mongodb — filter logic conformance', () => { + const mongod = sharedMongod as MongoMemoryServer; + let driver: MongoDBDriver; + + beforeAll(async () => { + driver = new MongoDBDriver({ url: mongod.getUri(), database: 'filter_logic_conformance' }); + await driver.connect(); + // Every fixture column is a plain string — the shared table keeps its + // predicates boring on purpose, so nothing here is about coercion. The + // declaration is still made, because that is how a real object reaches the + // driver and how its field kinds are resolved (#4047). + await driver.syncSchema('conformance', { + name: 'conformance', + fields: { + a: { type: 'string' }, + b: { type: 'string' }, + c: { type: 'string' }, + owner: { type: 'string' }, + status: { type: 'string' }, + parent_object: { type: 'string' }, + parent_id: { type: 'string' }, + }, + }); + for (const row of FILTER_LOGIC_ROWS) { + await driver.create('conformance', { ...row }); + } + }, 90_000); + + afterAll(async () => { + if (driver) await driver.disconnect(); + if (sharedMongod) await sharedMongod.stop(); + }); + + for (const c of FILTER_LOGIC_CASES) { + it(c.name, async () => { + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter } as any); + const got = (rows as any[]) + .map((r) => String(r.id)) + .sort((x, y) => x.localeCompare(y)); + expect(got, c.note).toEqual([...c.expected]); + }); + } + + /** + * The fixture as a whole, so a case that returns nothing because the seed + * failed cannot read as a case that correctly excluded everything. + */ + it('the fixture really is all four rows', async () => { + const rows = await driver.find('conformance', { object: 'conformance' } as any); + expect((rows as any[]).map((r) => String(r.id)).sort()).toEqual(['1', '2', '3', '4']); + }); +}); diff --git a/packages/plugins/driver-mongodb/src/mongodb-filter-logic-translation.test.ts b/packages/plugins/driver-mongodb/src/mongodb-filter-logic-translation.test.ts new file mode 100644 index 0000000000..d99bfb62f7 --- /dev/null +++ b/packages/plugins/driver-mongodb/src/mongodb-filter-logic-translation.test.ts @@ -0,0 +1,269 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Filter logical-combinator conformance for `translateFilter` — the MongoDB + * query documents it EMITS, asserted without a server (#4405). + * + * `mongodb-filter.ts` is an independent FilterCondition backend: the fifth one, + * and the one #3774 never enrolled when it named "the four". Its `$and` / `$or` + * / `$not` translation shares no line of code with the SQL compiler or the + * in-memory matcher, so nothing held it to the shared standard in + * `@objectstack/spec/data` — the standard that exists because a backend can + * widen a filter and still look like it is filtering (#3774 compiled + * `{$or:[{a,b}]}` to `a = ? OR b = ?`, and every read-visibility filter written + * that way returned rows the scope excluded). + * + * This is the half that must ALWAYS run. `translateFilter` is a pure function, + * and a driver defect only a downloadable 123 MB binary can catch is a defect + * nobody catches on a restricted network — the reason `test-mongod.ts` exists + * and the reason the #4419 suites split the same way. `mongodb-filter-logic- + * conformance.test.ts` runs the same shared cases against a real mongod and + * answers the question this file cannot: does MongoDB agree? + * + * ## Why there is a matcher in here at all + * + * A translator's output is a document, and the shared cases are stated in row + * ids, so something has to bridge the two. Pinning the emitted document + * literally for all eighteen cases would pin today's spelling rather than the + * semantics — the standard is "these ids, and no others", not "this JSON". + * + * So {@link matchDoc} evaluates the emitted document over + * {@link FILTER_LOGIC_ROWS} by MongoDB's documented semantics, and is + * deliberately **strict**: every shape it does not model is a thrown error, not + * a silently-true predicate. A stand-in more permissive than the real engine + * turns a suite into a green light for broken code, which is the hazard #4419 + * called out. Its own discrimination is proved below (a widened document must + * FAIL the case it widens), so "all green" cannot mean "the matcher says yes to + * everything". + * + * The named risk areas from #4405 get literal wire-shape pins on top of the + * sweep, because they are where the two engines' vocabularies differ rather + * than where a row count differs: MongoDB has no document-level `$not` at all + * (the server answers `unknown top level operator: $not`), so a negation has to + * leave here as `$nor` or as a per-field `$not`, and a nested `$and` inside a + * `$or` branch must stay a nested document rather than being flattened into its + * parent. + */ + +import { describe, it, expect } from 'vitest'; +import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS, type FilterLogicRow } from '@objectstack/spec/data'; +import { translateFilter } from './mongodb-filter.js'; + +// ── A deliberately strict reader of the emitted document ──────────────────── + +/** Thrown for any shape this matcher does not model — never swallowed. */ +class UnsupportedShape extends Error {} + +/** + * MongoDB orders values only WITHIN a BSON type bracket; across brackets a + * range predicate simply does not match. The shared fixture is all strings, so + * a cross-type comparison here means the translator produced a comparand the + * case never asked for. + */ +function compare(a: unknown, b: unknown): number | undefined { + if (typeof a !== typeof b) return undefined; // different bracket → no order + if (typeof a === 'string' || typeof a === 'number') { + return a === b ? 0 : (a as any) < (b as any) ? -1 : 1; + } + throw new UnsupportedShape(`unsupported comparand type: ${typeof a}`); +} + +/** Operators applied to one field's value. */ +function matchOps(value: unknown, ops: Record): boolean { + for (const [op, arg] of Object.entries(ops)) { + switch (op) { + case '$eq': + if (value !== arg) return false; + break; + case '$ne': + if (value === arg) return false; + break; + case '$gt': + if (!((compare(value, arg) ?? 0) > 0)) return false; + break; + case '$gte': + if (!((compare(value, arg) ?? -1) >= 0)) return false; + break; + case '$lt': + if (!((compare(value, arg) ?? 0) < 0)) return false; + break; + case '$lte': + if (!((compare(value, arg) ?? 1) <= 0)) return false; + break; + case '$in': + if (!Array.isArray(arg)) throw new UnsupportedShape('$in without an array'); + if (!arg.includes(value)) return false; + break; + case '$nin': + if (!Array.isArray(arg)) throw new UnsupportedShape('$nin without an array'); + if (arg.includes(value)) return false; + break; + case '$exists': + if ((value !== undefined) !== arg) return false; + break; + case '$not': { + // The per-field negation — the only `$not` MongoDB accepts, and it + // takes an operator document, never a plain value. + if (!arg || typeof arg !== 'object' || Array.isArray(arg)) { + throw new UnsupportedShape('per-field $not takes an operator document'); + } + if (matchOps(value, arg as Record)) return false; + break; + } + default: + throw new UnsupportedShape(`unsupported field operator '${op}'`); + } + } + return true; +} + +/** One field key of a query document: a literal, or an operator document. */ +function matchField(value: unknown, cond: unknown): boolean { + if (cond !== null && typeof cond === 'object' && !Array.isArray(cond) && !(cond instanceof Date)) { + const keys = Object.keys(cond as Record); + const ops = keys.filter((k) => k.startsWith('$')); + if (ops.length === keys.length && keys.length > 0) { + return matchOps(value, cond as Record); + } + if (ops.length > 0) { + throw new UnsupportedShape(`mixed operator/literal keys on one field: ${keys.join(', ')}`); + } + } + return value === cond; +} + +/** + * Evaluate an emitted MongoDB query document against one fixture row. + * + * Models exactly what MongoDB does for the vocabulary these cases can produce, + * and throws for everything else — including a document-level `$not`, which the + * server itself rejects. + */ +export function matchDoc(row: FilterLogicRow, doc: Record): boolean { + for (const [key, value] of Object.entries(doc)) { + switch (key) { + case '$and': + if (!Array.isArray(value)) throw new UnsupportedShape('$and without an array'); + if (!value.every((sub) => matchDoc(row, sub as Record))) return false; + break; + case '$or': + if (!Array.isArray(value)) throw new UnsupportedShape('$or without an array'); + if (!value.some((sub) => matchDoc(row, sub as Record))) return false; + break; + case '$nor': + if (!Array.isArray(value)) throw new UnsupportedShape('$nor without an array'); + if (value.some((sub) => matchDoc(row, sub as Record))) return false; + break; + case '$not': + throw new UnsupportedShape( + 'document-level $not — MongoDB has no such operator and answers ' + + '`unknown top level operator: $not`; a negation must be emitted as $nor ' + + 'or as a per-field $not', + ); + default: + if (key.startsWith('$')) throw new UnsupportedShape(`unsupported document operator '${key}'`); + if (!matchField((row as any)[key], value)) return false; + } + } + return true; +} + +/** Ids the emitted document selects from the shared fixture, ascending. */ +function select(doc: Record): string[] { + return FILTER_LOGIC_ROWS.filter((row) => matchDoc(row, doc)) + .map((row) => row.id) + .sort((x, y) => x.localeCompare(y)); +} + +/** Walk an emitted document, reporting every document-level `$not`. */ +function documentLevelNots(doc: unknown, path = '$'): string[] { + if (Array.isArray(doc)) return doc.flatMap((d, i) => documentLevelNots(d, `${path}[${i}]`)); + if (!doc || typeof doc !== 'object') return []; + const found: string[] = []; + for (const [key, value] of Object.entries(doc as Record)) { + if (key === '$not') found.push(path); + // Only logical operators carry further query DOCUMENTS; anything under a + // field key is an operator document, where `$not` is legal. + if (key === '$and' || key === '$or' || key === '$nor') { + found.push(...documentLevelNots(value, `${path}.${key}`)); + } + } + return found; +} + +// ── The shared standard ───────────────────────────────────────────────────── + +describe('translateFilter — filter logic conformance, without a server (#4405)', () => { + for (const c of FILTER_LOGIC_CASES) { + it(c.name, () => { + const doc = translateFilter(c.filter) as Record; + expect(select(doc), `${c.note ?? ''}\nemitted: ${JSON.stringify(doc)}`).toEqual([ + ...c.expected, + ]); + }); + } +}); + +// ── The vocabulary gap the row counts cannot show ─────────────────────────── + +describe('translateFilter — the shapes MongoDB spells differently', () => { + it('never emits a document-level $not: the server rejects it outright', () => { + for (const c of FILTER_LOGIC_CASES) { + const doc = translateFilter(c.filter) as Record; + expect(documentLevelNots(doc), `${c.name} emitted ${JSON.stringify(doc)}`).toEqual([]); + } + }); + + it('$not becomes a $nor that AND-s with the sibling keys of its own branch', () => { + // The #4405 risk area, stated literally: the negation is a `$nor`, and it + // joins `b: 'zz'` with `$and` rather than replacing it. + expect(translateFilter({ $or: [{ c: 'nope' }, { $not: { a: 'x' }, b: 'zz' }] })).toEqual({ + $or: [{ c: 'nope' }, { $and: [{ b: 'zz' }, { $nor: [{ a: 'x' }] }] }], + }); + }); + + it('a nested $and inside a $or branch stays nested, and keeps the branch key beside it', () => { + expect(translateFilter({ $or: [{ c: 'nope' }, { $and: [{ a: 'qq' }], b: 'y' }] })).toEqual({ + $or: [{ c: 'nope' }, { $and: [{ b: 'y' }, { $and: [{ a: 'qq' }] }] }], + }); + }); + + it('a multi-key $or branch stays ONE document — the shape #3774 flattened', () => { + expect(translateFilter({ $or: [{ a: 'x', b: 'y' }] })).toEqual({ + $or: [{ a: 'x', b: 'y' }], + }); + }); +}); + +// ── The matcher is not the thing being tested, so it gets tested ──────────── + +describe('the in-process matcher discriminates', () => { + it('a widened document FAILS the case it widens (the #3774 miscompile)', () => { + // `{$or:[{a:'x',b:'y'}]}` must select row 1 alone. This is what OR-ing the + // branch's own keys would have emitted instead — if the sweep above can + // pass with this, it is proving nothing. + expect(select({ $or: [{ a: 'x' }, { b: 'y' }] })).toEqual(['1', '2', '3']); + expect(select({ $or: [{ a: 'x', b: 'y' }] })).toEqual(['1']); + }); + + it('a widened operator window FAILS too', () => { + expect(select({ b: { $gte: 'y', $lt: 'z' } })).toEqual(['1', '3']); + expect(select({ $or: [{ b: { $gte: 'y' } }, { b: { $lt: 'z' } }] })).toEqual(['1', '2', '3', '4']); + }); + + it('refuses a document-level $not instead of quietly evaluating one', () => { + expect(() => matchDoc(FILTER_LOGIC_ROWS[0], { $not: { a: 'x' } })).toThrow( + /document-level \$not/, + ); + }); + + it('refuses any operator it does not model, rather than treating it as true', () => { + expect(() => matchDoc(FILTER_LOGIC_ROWS[0], { a: { $regex: 'x' } })).toThrow(/unsupported/); + expect(() => matchDoc(FILTER_LOGIC_ROWS[0], { $expr: {} })).toThrow(/unsupported/); + }); + + it('honours $nor and the per-field $not the translator is allowed to emit', () => { + expect(select({ $nor: [{ a: 'x' }] })).toEqual(['3', '4']); + expect(select({ a: { $not: { $eq: 'x' } } })).toEqual(['3', '4']); + }); +}); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-filter-logic-conformance.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-filter-logic-conformance.test.ts new file mode 100644 index 0000000000..3288e8551c --- /dev/null +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-filter-logic-conformance.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Filter logical-combinator conformance for the wasm driver (#3774, #4405) — + * the shared `@objectstack/spec/data` cases, run through this driver's own + * pipeline. + * + * `SqliteWasmDriver extends SqlDriver`, so the `$and` / `$or` / `$not` + * compilation is inherited and nothing here re-implements it. What this pins is + * the other half, the same half its temporal and pagination suites pin: the + * compiled predicate has to survive a different **engine**. This driver swaps + * knex's transport for a custom sql.js dialect (`Client_WasmSqlite`) that + * compiles the statement, binds its parameters and marshals the rows back + * through its own path. A dialect that mis-bound the parameters of a nested + * `(… AND …) OR (… AND …)` would produce precisely the failure the shared + * standard exists to rule out — a filter that looks applied and selects the + * wrong rows — and it would fail in no other suite in the repo. + * + * "It inherits the compiler, therefore it is fine" is the assumption those two + * suites exist to disprove; #4405 recorded this cell as DEBT rather than EXEMPT + * for exactly that reason. This file clears it. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data'; +import { SqliteWasmDriver } from './index.js'; + +describe('driver-sqlite-wasm — filter logic conformance', () => { + let driver: SqliteWasmDriver; + + beforeAll(async () => { + driver = new SqliteWasmDriver({ filename: ':memory:' }); + await driver.initObjects([ + { + name: 'conformance', + fields: { + a: { type: 'string' }, + b: { type: 'string' }, + c: { type: 'string' }, + owner: { type: 'string' }, + status: { type: 'string' }, + parent_object: { type: 'string' }, + parent_id: { type: 'string' }, + }, + }, + ]); + for (const row of FILTER_LOGIC_ROWS) { + await driver.create('conformance', { ...row }, { bypassTenantAudit: true } as any); + } + }); + + afterAll(async () => { + await driver.disconnect(); + }); + + for (const c of FILTER_LOGIC_CASES) { + it(c.name, async () => { + const rows = await driver.find( + 'conformance', + { object: 'conformance', where: c.filter } as any, + { bypassTenantAudit: true } as any, + ); + const got = (rows as any[]) + .map((r) => String(r.id)) + .sort((x, y) => x.localeCompare(y)); + expect(got, c.note).toEqual([...c.expected]); + }); + } + + /** + * The fixture as a whole, so a case that returns nothing because the seed + * failed cannot read as a case that correctly excluded everything. + */ + it('the fixture really is all four rows', async () => { + const rows = await driver.find('conformance', {} as any, { bypassTenantAudit: true } as any); + expect((rows as any[]).map((r) => String(r.id)).sort()).toEqual(['1', '2', '3', '4']); + }); +}); diff --git a/packages/spec/src/data/filter-logic-conformance.ts b/packages/spec/src/data/filter-logic-conformance.ts index 8c11c447d0..a0577aadf5 100644 --- a/packages/spec/src/data/filter-logic-conformance.ts +++ b/packages/spec/src/data/filter-logic-conformance.ts @@ -6,7 +6,7 @@ * * ## Why this exists * - * `FilterCondition` is evaluated by four independent implementations, and they + * `FilterCondition` is evaluated by five independent implementations, and they * had drifted: * * | Backend | Where | @@ -15,6 +15,14 @@ * | In-memory matcher | `driver-memory` `memory-matcher` | * | Record-at-a-time evaluator | `formula` `matchesFilterCondition` (RLS write-side `check`) | * | Read-scope SQL lowering | `service-analytics` `read-scope-sql` | + * | MongoDB query translator | `driver-mongodb` `translateFilter` | + * + * #3774 said "four" and enrolled four: `translateFilter` was missed, not + * excluded, and ran unchecked against this standard until #4405 — the one + * backend whose target language has no document-level `$not` at all, so a + * negation leaves it as `$nor`. `driver-sqlite-wasm` runs the table too; it + * *inherits* the SQL compiler, so what its suite adds is the sql.js engine + * executing the compiled predicate rather than a sixth way of building one. * * In #3774 the SQL compiler OR-ed the contents *within* a `$or` branch instead * of AND-ing them, so every `$or` filter matched more rows than it should — diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts index d4b12dae6c..4571e0b611 100644 --- a/packages/spec/src/data/index.ts +++ b/packages/spec/src/data/index.ts @@ -3,8 +3,9 @@ export * from './query.zod'; export * from './filter.zod'; // Canonical conformance cases for the filter logical combinators — the shared -// standard the four independent FilterCondition backends are each checked -// against, so they cannot drift apart again (#3774). +// standard the five independent FilterCondition backends are each checked +// against, so they cannot drift apart again (#3774; the fifth — MongoDB's +// `translateFilter` — was enrolled by #4405). export * from './filter-logic-conformance'; export * from './temporal-conformance'; // Canonical conformance cases for deterministic paged reads — the standard diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index db58e49acd..8d3b0d4367 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -109,31 +109,29 @@ const CASE_SETS = [ // One entry per uncovered (driver x case-set) cell. `kind` is DEBT (should be // covered, is not yet) or EXEMPT (cannot meaningfully apply). Both are measured // claims; neither is a default. +// +// EMPTY, as of #4405 — every cell of the matrix is covered by a suite. The two +// FILTER_LOGIC_CASES rows this ledger opened with are both cleared: +// +// driver-mongodb `translateFilter` was the independent fifth backend +// #3774 never enrolled when it named "the four". It now +// drives the shared cases twice: server-free over the +// MongoDB documents it emits (the half that always runs, +// because the mongod binary is not always fetchable), and +// against a real mongod. +// driver-sqlite-wasm Inherits SqlDriver's filter compiler, so what its suite +// pins is the sql.js dialect executing the compiled +// predicate — the same seam its temporal and pagination +// suites cover for their clauses. It was tracked as DEBT +// rather than EXEMPT because "inherits, therefore fine" is +// the assumption those suites exist to disprove; the suite +// is what disproves it, not the entry. +// +// An empty ledger is the intended steady state, not a reason to delete the +// mechanism: the next driver that arrives uncovered fails CONSUMED and lands +// its measured entry here. -const LEDGER = [ - { - driver: 'driver-mongodb', - marker: 'FILTER_LOGIC_CASES', - kind: 'DEBT', - issue: 'https://github.com/objectstack-ai/objectstack/issues/4405', - why: - "`mongodb-filter.ts`'s `translateFilter` is an independent FilterCondition " - + 'backend — the fifth, and the one #3774 never enrolled when it named "the four". ' - + 'Its $and/$or/$not translation shares no code with the SQL or in-memory paths.', - }, - { - driver: 'driver-sqlite-wasm', - marker: 'FILTER_LOGIC_CASES', - kind: 'DEBT', - issue: 'https://github.com/objectstack-ai/objectstack/issues/4405', - why: - 'Inherits SqlDriver\'s filter compiler, so the risk is the sql.js dialect executing ' - + 'the compiled predicate, not the predicate being built wrong — the same risk its ' - + 'temporal and pagination suites already cover for their clauses. Lower value than ' - + 'the mongodb row above, tracked with it rather than exempted, because "inherits, ' - + 'therefore fine" is exactly the assumption those two suites exist to disprove.', - }, -]; +const LEDGER = []; // ── Discovery ───────────────────────────────────────────────────────────────