diff --git a/packages/cloud-connection/src/marketplace-install-local-seed-lookup.test.ts b/packages/cloud-connection/src/marketplace-install-local-seed-lookup.test.ts index 8030616180..2a9bbf94f7 100644 --- a/packages/cloud-connection/src/marketplace-install-local-seed-lookup.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-seed-lookup.test.ts @@ -83,7 +83,7 @@ function makeEngine() { let records = store[objectName] || []; if (query?.where) { records = records.filter((r) => - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/cloud-connection/src/marketplace-install-local-state-machine-exempt.test.ts b/packages/cloud-connection/src/marketplace-install-local-state-machine-exempt.test.ts index f31d232e7d..463a68f2ee 100644 --- a/packages/cloud-connection/src/marketplace-install-local-state-machine-exempt.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-state-machine-exempt.test.ts @@ -102,7 +102,7 @@ function makeEngine() { let records = store[objectName] || []; if (query?.where) { records = records.filter((r) => - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/core/src/security/api-key.test.ts b/packages/core/src/security/api-key.test.ts index 2689e2a31f..83063853be 100644 --- a/packages/core/src/security/api-key.test.ts +++ b/packages/core/src/security/api-key.test.ts @@ -17,7 +17,7 @@ function makeQl(rows: any[]) { find: async (object: string, opts: any) => { if (object !== 'sys_api_key') return []; const where = opts?.where ?? {}; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, }; } diff --git a/packages/core/src/security/resolve-authz-context.test.ts b/packages/core/src/security/resolve-authz-context.test.ts index ffee6d90e3..82f672c585 100644 --- a/packages/core/src/security/resolve-authz-context.test.ts +++ b/packages/core/src/security/resolve-authz-context.test.ts @@ -19,7 +19,7 @@ function makeQl(tables: Record) { const rows = tables[object] ?? []; const where = opts?.where ?? {}; return rows.filter((r) => - Object.entries(where).every(([k, v]) => { + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); if (v && typeof v === 'object' && '$in' in (v as any)) return (v as any).$in.includes(r[k]); return r[k] === v; }), @@ -123,7 +123,7 @@ function makeCountingQl(tables: Record) { const rows = tables[object] ?? []; const where = opts?.where ?? {}; return rows.filter((r) => - Object.entries(where).every(([k, v]) => { + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); if (v && typeof v === 'object' && '$in' in (v as any)) return (v as any).$in.includes(r[k]); return r[k] === v; }), diff --git a/packages/core/src/utils/migration-journal.test.ts b/packages/core/src/utils/migration-journal.test.ts index 3302fe8315..bfc0839703 100644 --- a/packages/core/src/utils/migration-journal.test.ts +++ b/packages/core/src/utils/migration-journal.test.ts @@ -81,7 +81,7 @@ class FakeEngine { async find(objectName: string, query?: { where?: Record }): Promise { const where = query?.where ?? {}; - return this.rows(objectName).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return this.rows(objectName).filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); } async findOne(objectName: string, query?: { where?: Record }): Promise { diff --git a/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts b/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts index ed2839058e..cf042f02db 100644 --- a/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts +++ b/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts @@ -73,7 +73,7 @@ class FakeEngine { async find(objectName: string, query?: { where?: Record }): Promise { const where = query?.where ?? {}; - return this.rows(objectName).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return this.rows(objectName).filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); } async findOne(objectName: string, query?: { where?: Record }): Promise { diff --git a/packages/metadata-protocol/src/seed-loader-composite-external-id.test.ts b/packages/metadata-protocol/src/seed-loader-composite-external-id.test.ts index a93a85e32e..97bdc93b96 100644 --- a/packages/metadata-protocol/src/seed-loader-composite-external-id.test.ts +++ b/packages/metadata-protocol/src/seed-loader-composite-external-id.test.ts @@ -35,7 +35,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-deferred-dropped.test.ts b/packages/metadata-protocol/src/seed-loader-deferred-dropped.test.ts index eab51637cf..c9fe2e1fe9 100644 --- a/packages/metadata-protocol/src/seed-loader-deferred-dropped.test.ts +++ b/packages/metadata-protocol/src/seed-loader-deferred-dropped.test.ts @@ -44,7 +44,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts b/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts index 8e03efcd2b..15c3cd3921 100644 --- a/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts +++ b/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts @@ -29,7 +29,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-driver-text.test.ts b/packages/metadata-protocol/src/seed-loader-driver-text.test.ts index a684437fc9..8bfc33cae6 100644 --- a/packages/metadata-protocol/src/seed-loader-driver-text.test.ts +++ b/packages/metadata-protocol/src/seed-loader-driver-text.test.ts @@ -210,7 +210,7 @@ describe('[#8442] raw driver text is withheld from the seed `errors[].message`', find: vi.fn(async (o: string, q?: any) => { const rows = store[o] || []; return q?.where - ? rows.filter((r) => Object.entries(q.where).every(([k, v]) => r[k] === v)) + ? rows.filter((r) => Object.entries(q.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : rows; }), findOne: vi.fn(async (o: string, q?: any) => { diff --git a/packages/metadata-protocol/src/seed-loader-engine-schema-fallback.test.ts b/packages/metadata-protocol/src/seed-loader-engine-schema-fallback.test.ts index 88ef318475..24d8055685 100644 --- a/packages/metadata-protocol/src/seed-loader-engine-schema-fallback.test.ts +++ b/packages/metadata-protocol/src/seed-loader-engine-schema-fallback.test.ts @@ -39,7 +39,7 @@ function createFaithfulEngine(schemas: Record) { let records = store[objectName] || []; if (query?.where) { records = records.filter((r) => - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') { diff --git a/packages/metadata-protocol/src/seed-loader-env-scope.test.ts b/packages/metadata-protocol/src/seed-loader-env-scope.test.ts index 5856c7a2dd..de107db946 100644 --- a/packages/metadata-protocol/src/seed-loader-env-scope.test.ts +++ b/packages/metadata-protocol/src/seed-loader-env-scope.test.ts @@ -43,7 +43,7 @@ function createEngine() { let records = store[objectName] || []; if (query?.where) { records = records.filter((r) => - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts b/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts index 8dd853789e..3f1690403a 100644 --- a/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts +++ b/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts @@ -36,7 +36,7 @@ function createEngine(schemas: Record) { let records = store[objectName] || []; if (query?.where) { records = records.filter((r) => - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-replay.test.ts b/packages/metadata-protocol/src/seed-loader-replay.test.ts index d1b5598358..4b72352e97 100644 --- a/packages/metadata-protocol/src/seed-loader-replay.test.ts +++ b/packages/metadata-protocol/src/seed-loader-replay.test.ts @@ -30,7 +30,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') { diff --git a/packages/metadata-protocol/src/seed-loader-retry.test.ts b/packages/metadata-protocol/src/seed-loader-retry.test.ts index d57670755a..0a0241cd1c 100644 --- a/packages/metadata-protocol/src/seed-loader-retry.test.ts +++ b/packages/metadata-protocol/src/seed-loader-retry.test.ts @@ -25,7 +25,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-state-machine-exempt.test.ts b/packages/metadata-protocol/src/seed-loader-state-machine-exempt.test.ts index ab6d161c19..bec578f618 100644 --- a/packages/metadata-protocol/src/seed-loader-state-machine-exempt.test.ts +++ b/packages/metadata-protocol/src/seed-loader-state-machine-exempt.test.ts @@ -68,7 +68,7 @@ function createEnforcingEngine(): { engine: IDataEngine; store: Record { let rows = store[objectName] || []; if (query?.where) { - rows = rows.filter((r) => Object.entries(query.where).every(([k, v]) => r[k] === v)); + rows = rows.filter((r) => Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); } if (typeof query?.limit === 'number') rows = rows.slice(0, query.limit); return rows; diff --git a/packages/metadata-protocol/src/seed-loader-summary-stale.test.ts b/packages/metadata-protocol/src/seed-loader-summary-stale.test.ts index 3f714dabc5..52c00f830a 100644 --- a/packages/metadata-protocol/src/seed-loader-summary-stale.test.ts +++ b/packages/metadata-protocol/src/seed-loader-summary-stale.test.ts @@ -37,7 +37,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/metadata-protocol/src/seed-loader-unresolved-drop.test.ts b/packages/metadata-protocol/src/seed-loader-unresolved-drop.test.ts index 363a02531b..0854b0367a 100644 --- a/packages/metadata-protocol/src/seed-loader-unresolved-drop.test.ts +++ b/packages/metadata-protocol/src/seed-loader-unresolved-drop.test.ts @@ -48,7 +48,7 @@ function createFaithfulEngine(): { engine: IDataEngine; store: Record - Object.entries(query.where).every(([k, v]) => r[k] === v), + Object.entries(query.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); } if (typeof query?.limit === 'number') records = records.slice(0, query.limit); diff --git a/packages/plugins/plugin-audit/src/comment-access-hooks.test.ts b/packages/plugins/plugin-audit/src/comment-access-hooks.test.ts index 0cd47d87c3..a62076fcff 100644 --- a/packages/plugins/plugin-audit/src/comment-access-hooks.test.ts +++ b/packages/plugins/plugin-audit/src/comment-access-hooks.test.ts @@ -22,13 +22,13 @@ function install(opts: { }, find: async (_object, options: any) => { const rows = (opts.comments ?? []).filter((r) => - Object.entries(options?.where ?? {}).every(([k, v]) => r[k] === v), + Object.entries(options?.where ?? {}).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); return typeof options?.limit === 'number' ? rows.slice(0, options.limit) : rows; }, findOne: async (_object, options: any) => (opts.comments ?? []).find((r) => - Object.entries(options?.where ?? {}).every(([k, v]) => r[k] === v), + Object.entries(options?.where ?? {}).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ) ?? null, }; installCommentAccessHooks(engine, () => opts.sharing, silentLogger()); diff --git a/packages/plugins/plugin-auth/src/account-issuer-parity.test.ts b/packages/plugins/plugin-auth/src/account-issuer-parity.test.ts index 788cf066e6..bf14392cb8 100644 --- a/packages/plugins/plugin-auth/src/account-issuer-parity.test.ts +++ b/packages/plugins/plugin-auth/src/account-issuer-parity.test.ts @@ -81,7 +81,7 @@ function makeQl(row: Record) { const where = query?.where ?? {}; return (tables[object] ?? []).filter((r) => Object.entries(where).every(([field, value]) => - value === null ? r[field] == null : r[field] === value, + { if (field.startsWith('$')) throw new Error(`fake driver: unsupported operator ${field}`); return value === null ? r[field] == null : r[field] === value; }, ), ); }, diff --git a/packages/plugins/plugin-auth/src/admin-import-users.test.ts b/packages/plugins/plugin-auth/src/admin-import-users.test.ts index 580d5e9369..74d50286e9 100644 --- a/packages/plugins/plugin-auth/src/admin-import-users.test.ts +++ b/packages/plugins/plugin-auth/src/admin-import-users.test.ts @@ -33,7 +33,7 @@ function makeDeps(opts: { }); const find = vi.fn(async (_obj: string, q: any) => { const where = q?.where ?? {}; - return existing.filter((u) => Object.entries(where).every(([k, v]) => u[k] === v)); + return existing.filter((u) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return u[k] === v; })); }); const update = vi.fn(async () => ({})); const insert = vi.fn(async () => ({})); diff --git a/packages/plugins/plugin-auth/src/auth-manager.test.ts b/packages/plugins/plugin-auth/src/auth-manager.test.ts index c8b090952c..3f6db9b0ae 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.test.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.test.ts @@ -3862,7 +3862,7 @@ describe('isOrgOrPlatformAdmin – one grade ruler for "is this membership an ad if (object === 'sys_member') { const where = query?.where ?? {}; return (opts.members ?? []).filter((row) => - Object.entries(where).every(([k, v]) => row[k] === v), + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return row[k] === v; }), ); } return []; diff --git a/packages/plugins/plugin-auth/src/backfill-account-issuer.test.ts b/packages/plugins/plugin-auth/src/backfill-account-issuer.test.ts index fe8504fb27..7679bc9af0 100644 --- a/packages/plugins/plugin-auth/src/backfill-account-issuer.test.ts +++ b/packages/plugins/plugin-auth/src/backfill-account-issuer.test.ts @@ -26,7 +26,7 @@ function makeQl(tables: Record) { const where = query?.where ?? {}; return rows.filter((row) => Object.entries(where).every(([field, value]) => - value === null ? row[field] == null : row[field] === value, + { if (field.startsWith('$')) throw new Error(`fake driver: unsupported operator ${field}`); return value === null ? row[field] == null : row[field] === value; }, ), ); }), diff --git a/packages/plugins/plugin-email/src/attachment-reclaim.test.ts b/packages/plugins/plugin-email/src/attachment-reclaim.test.ts index 10803bb811..a2408df0c8 100644 --- a/packages/plugins/plugin-email/src/attachment-reclaim.test.ts +++ b/packages/plugins/plugin-email/src/attachment-reclaim.test.ts @@ -55,7 +55,7 @@ function fakeEngine(rows: Array>) { updates: [] as Array>, async find(table: string, o: any = {}) { const where = o?.where ?? {}; - let out = rowsOf(table).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + let out = rowsOf(table).filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); if (o.limit) out = out.slice(0, o.limit); return out; }, @@ -73,7 +73,11 @@ function fakeEngine(rows: Array>) { const dispatch = assertEngineDeleteDispatch(o); if (dispatch.kind === 'multi') { const survivors = rowsOf(table).filter( - (r) => !Object.entries(o?.where ?? {}).every(([k, v]) => r[k] === v), + (r) => + !Object.entries(o?.where ?? {}).every(([k, v]) => { + if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); + return r[k] === v; + }), ); const deleted = rowsOf(table).length - survivors.length; tables.set(table, survivors); diff --git a/packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts b/packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts index e863ec64d0..8b90c57fbe 100644 --- a/packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts @@ -30,7 +30,7 @@ function makeQl(declared: any[] = []) { // `organization_id: null`, which strict `===` would make unsatisfiable // here while it works in production. return rows.filter((r) => - Object.entries(where).every(([k, v]) => (v === null ? r[k] == null : r[k] === v)), + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return (v === null ? r[k] == null : r[k] === v); }), ); }, async insert(object: string, data: any) { diff --git a/packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts b/packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts index 4b94c6fbdf..2ae1b613cd 100644 --- a/packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts @@ -15,7 +15,7 @@ function makeQl(declared: any[] = []) { async find(object: string, q: any) { if (object !== 'sys_permission_set') return []; const where = q?.where ?? {}; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async insert(object: string, data: any) { if (object !== 'sys_permission_set') return null; diff --git a/packages/plugins/plugin-security/src/bootstrap-declared-positions.test.ts b/packages/plugins/plugin-security/src/bootstrap-declared-positions.test.ts index d5c39469eb..2bd7bb626b 100644 --- a/packages/plugins/plugin-security/src/bootstrap-declared-positions.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-declared-positions.test.ts @@ -26,7 +26,7 @@ function makeQl(declared: any[] = []) { async find(object: string, q: any) { if (object !== 'sys_position') return []; const where = q?.where ?? {}; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async insert(object: string, data: any) { if (object !== 'sys_position') return null; diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin.test.ts index 1905575d86..9701a38b88 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin.test.ts @@ -27,7 +27,7 @@ function makeQl(seedRows: any[] = []) { async find(object: string, q: any) { if (object !== 'sys_permission_set') return []; const where = q?.where ?? {}; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async insert(object: string, data: any) { if (object !== 'sys_permission_set') return null; diff --git a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts index c8f8c36a35..e4563a5b8a 100644 --- a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts @@ -47,7 +47,7 @@ function makeQl() { const where = q?.where ?? {}; const matched = rows.filter((r) => // (2) `null` is IS NULL, not `=== null`. - Object.entries(where).every(([k, v]) => (v === null ? r[k] == null : r[k] === v)), + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return (v === null ? r[k] == null : r[k] === v); }), ); if (q?.limit === undefined) return matched; // (1) The #4363 tie-breaker. BINARY collation, as SQLite compares ids. diff --git a/packages/plugins/plugin-security/src/explain-engine.test.ts b/packages/plugins/plugin-security/src/explain-engine.test.ts index 306b7436cb..5dd3b982b7 100644 --- a/packages/plugins/plugin-security/src/explain-engine.test.ts +++ b/packages/plugins/plugin-security/src/explain-engine.test.ts @@ -444,7 +444,7 @@ function makeGrantQl(tables: Rows) { async find(object: string, opts: any) { const where = opts?.where ?? {}; return (tables[object] ?? []).filter((row) => - Object.entries(where).every(([key, cond]) => { + Object.entries(where).every(([key, cond]) => { if (key.startsWith('$')) throw new Error(`fake driver: unsupported operator ${key}`); const cell = row[key]; if (cond && typeof cond === 'object' && '$in' in (cond as any)) { return ((cond as any).$in as unknown[]).includes(cell); diff --git a/packages/plugins/plugin-security/src/invitation-placement.test.ts b/packages/plugins/plugin-security/src/invitation-placement.test.ts index b9119aa464..c38c21dc37 100644 --- a/packages/plugins/plugin-security/src/invitation-placement.test.ts +++ b/packages/plugins/plugin-security/src/invitation-placement.test.ts @@ -32,9 +32,9 @@ function makeQl(tables: Record = {}) { const where = opts?.where ?? {}; return rows.filter((row) => Object.entries(where).every(([k, v]) => - v && typeof v === 'object' && '$in' in (v as any) + { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return v && typeof v === 'object' && '$in' in (v as any) ? (v as any).$in.includes(row[k]) - : row[k] === v, + : row[k] === v; }, ), ); }), diff --git a/packages/plugins/plugin-security/src/normalize-managed-by.test.ts b/packages/plugins/plugin-security/src/normalize-managed-by.test.ts index 17a86b3949..502b878cc2 100644 --- a/packages/plugins/plugin-security/src/normalize-managed-by.test.ts +++ b/packages/plugins/plugin-security/src/normalize-managed-by.test.ts @@ -17,7 +17,7 @@ function makeQl() { async find(object: string, opts: any) { const where = opts?.where ?? {}; return (tables[object] ?? []).filter((r) => - Object.entries(where).every(([k, v]) => r[k] === v), + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); }, async update(object: string, data: any) { diff --git a/packages/plugins/plugin-webhooks/src/auto-enqueuer.test.ts b/packages/plugins/plugin-webhooks/src/auto-enqueuer.test.ts index fdfb10df21..2f005fb206 100644 --- a/packages/plugins/plugin-webhooks/src/auto-enqueuer.test.ts +++ b/packages/plugins/plugin-webhooks/src/auto-enqueuer.test.ts @@ -81,7 +81,7 @@ class FakeEngine implements IDataEngine { async find(name: string, q?: any): Promise { const all = this.rows[name] ?? []; if (!q?.where) return all; - return all.filter((r) => Object.entries(q.where).every(([k, v]) => r[k] === v)); + return all.filter((r) => Object.entries(q.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); } async findOne(name: string, q?: any): Promise { return (await this.find(name, q))[0] ?? null; @@ -104,7 +104,14 @@ class FakeEngine implements IDataEngine { const arr = this.rows[name] ?? []; const before = arr.length; this.rows[name] = arr.filter( - (r) => !(opts?.where && Object.entries(opts.where).every(([k, v]) => r[k] === v)), + (r) => + !( + opts?.where && + Object.entries(opts.where).every(([k, v]) => { + if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); + return r[k] === v; + }) + ), ); return { affected: before - this.rows[name].length }; } diff --git a/packages/rest/src/import-runner-idempotency.test.ts b/packages/rest/src/import-runner-idempotency.test.ts index 37935807c5..000414bd9c 100644 --- a/packages/rest/src/import-runner-idempotency.test.ts +++ b/packages/rest/src/import-runner-idempotency.test.ts @@ -53,7 +53,7 @@ function makeProtocol(opts: { firstCall?: 'throw' | 'shortReturn' } = {}) { const filter = args.query?.$filter ?? {}; // Supports equality and { $in: [...] } — the id recheck (framework#3173) // queries by pre-assigned id $in, like the real SQL driver does. - return store.filter((row) => Object.entries(filter).every(([k, v]) => { + return store.filter((row) => Object.entries(filter).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); if (v && typeof v === 'object' && Array.isArray((v as any).$in)) return (v as any).$in.includes(row[k]); return row[k] === v; })); diff --git a/packages/rest/src/import-runner-selfref.test.ts b/packages/rest/src/import-runner-selfref.test.ts index 710f6249d1..1ac369e2ae 100644 --- a/packages/rest/src/import-runner-selfref.test.ts +++ b/packages/rest/src/import-runner-selfref.test.ts @@ -44,7 +44,7 @@ function makeProtocol(seed: Array> = []) { })); const findData = vi.fn(async (args: { query?: { $filter?: Record } }) => { const filter = args.query?.$filter ?? {}; - return store.filter((row) => Object.entries(filter).every(([k, v]) => row[k] === v)); + return store.filter((row) => Object.entries(filter).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return row[k] === v; })); }); const p: ImportProtocolLike = { findData, createData: vi.fn(), updateData: vi.fn(), createManyData }; return { p, store, createManyData }; diff --git a/packages/runtime/src/http-dispatcher.keys.test.ts b/packages/runtime/src/http-dispatcher.keys.test.ts index 5bdb21767b..3fe150b6be 100644 --- a/packages/runtime/src/http-dispatcher.keys.test.ts +++ b/packages/runtime/src/http-dispatcher.keys.test.ts @@ -25,7 +25,7 @@ function makeKernel() { find: async (obj: string, opts: any) => { const where = opts?.where ?? {}; if (obj !== 'sys_api_key') return []; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, update: async () => ({}), delete: async () => ({}), diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index d49e7e829e..d43c662d0d 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -3944,7 +3944,7 @@ describe('HttpDispatcher — action body ctx.user identity (#2701)', () => { find: async (obj: string, opts: any) => { const where = opts?.where ?? {}; if (obj !== 'sys_api_key') return []; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, update: async () => ({}), delete: async () => ({}), }; diff --git a/packages/runtime/src/migration-recovery-plugin.test.ts b/packages/runtime/src/migration-recovery-plugin.test.ts index 7dec67e93c..a0d2139217 100644 --- a/packages/runtime/src/migration-recovery-plugin.test.ts +++ b/packages/runtime/src/migration-recovery-plugin.test.ts @@ -45,7 +45,7 @@ function engineWith(rows: any[], opts: { failFind?: boolean; noJournalObject?: b if (opts.failFind) throw new Error('table is gone'); if (obj !== JOURNAL) return []; const where = q?.where ?? {}; - return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, insert: async () => ({}), }; diff --git a/packages/runtime/src/security/resolve-execution-context.test.ts b/packages/runtime/src/security/resolve-execution-context.test.ts index 8b1aac439c..521bd92919 100644 --- a/packages/runtime/src/security/resolve-execution-context.test.ts +++ b/packages/runtime/src/security/resolve-execution-context.test.ts @@ -17,6 +17,7 @@ function makeQl(apiKeyRows: any[]) { if (object !== 'sys_api_key') return []; return apiKeyRows.filter((row) => { for (const [k, v] of Object.entries(where)) { + if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); if (row[k] !== v) return false; } return true; diff --git a/packages/runtime/src/seed-loader.test.ts b/packages/runtime/src/seed-loader.test.ts index 54159054a2..54d5a7b2ab 100644 --- a/packages/runtime/src/seed-loader.test.ts +++ b/packages/runtime/src/seed-loader.test.ts @@ -31,6 +31,7 @@ function createMockEngine(data: Record = {}): IDataEngine { if (query?.filter) { return records.filter(r => { for (const [k, v] of Object.entries(query.filter)) { + if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); if (r[k] !== v) return false; } return true; diff --git a/packages/services/service-automation/src/builtin/wait-node-degraded-run.test.ts b/packages/services/service-automation/src/builtin/wait-node-degraded-run.test.ts index af7d35b2bc..b06699f921 100644 --- a/packages/services/service-automation/src/builtin/wait-node-degraded-run.test.ts +++ b/packages/services/service-automation/src/builtin/wait-node-degraded-run.test.ts @@ -57,7 +57,7 @@ function makeFakeEngine() { async find(table: string, opts: any = {}) { const t = tables.get(table) ?? []; const out = opts.where - ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)) + ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : [...t]; return opts.limit ? out.slice(0, opts.limit) : out; }, diff --git a/packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts b/packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts index 78a8a88bf4..a42c3f7626 100644 --- a/packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts +++ b/packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts @@ -32,7 +32,7 @@ function fakeDataEngine(opts: { failReads?: string } = {}) { if (opts.failReads) throw new Error(opts.failReads); const where = options?.where ?? {}; return [...rows.values()].filter(r => - Object.entries(where).every(([k, v]) => r[k] === v)); + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async insert(_object, data) { rows.set(String(data.id), { ...data }); return data; }, async update(_object, data, options) { diff --git a/packages/services/service-automation/src/runas-grant-resolution.integration.test.ts b/packages/services/service-automation/src/runas-grant-resolution.integration.test.ts index e26a3490f2..fc55d8fda2 100644 --- a/packages/services/service-automation/src/runas-grant-resolution.integration.test.ts +++ b/packages/services/service-automation/src/runas-grant-resolution.integration.test.ts @@ -30,7 +30,7 @@ function fakeObjectQl(tables: Record) { const match = (object: string, where: any): any[] => (tables[object] ?? []).filter((r) => Object.entries(where ?? {}).every(([k, v]) => - v && typeof v === 'object' && '$in' in (v as any) ? (v as any).$in.includes(r[k]) : r[k] === v, + { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return v && typeof v === 'object' && '$in' in (v as any) ? (v as any).$in.includes(r[k]) : r[k] === v; }, ), ); const engine: any = { diff --git a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts index 49c2f937cc..ba09d6ec74 100644 --- a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts +++ b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts @@ -248,11 +248,11 @@ describe('DatasourceAdminServicePlugin: runtime datasource durability', () => { getDriverByName() { return undefined; }, findOne: async (_o: string, q: { where?: Record }) => { const w = q.where ?? {}; - return rows.find((r) => Object.entries(w).every(([k, v]) => r[k] === v)); + return rows.find((r) => Object.entries(w).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, find: async (_o: string, q: { where?: Record }) => { const w = q.where ?? {}; - return rows.filter((r) => Object.entries(w).every(([k, v]) => r[k] === v)); + return rows.filter((r) => Object.entries(w).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, insert: async (_o: string, row: Record) => { rows.push({ ...row }); }, update: async (_o: string, row: Record, opts: { where: Record }) => { diff --git a/packages/services/service-job/src/db-job-adapter.degraded-outcome.test.ts b/packages/services/service-job/src/db-job-adapter.degraded-outcome.test.ts index 7c00e75384..1908a7cf2b 100644 --- a/packages/services/service-job/src/db-job-adapter.degraded-outcome.test.ts +++ b/packages/services/service-job/src/db-job-adapter.degraded-outcome.test.ts @@ -29,7 +29,7 @@ function makeFakeEngine() { async find(table: string, opts: any = {}) { const t = tables.get(table) ?? []; let out = opts.where - ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)) + ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : [...t]; if (opts.limit) out = out.slice(0, opts.limit); return out; diff --git a/packages/services/service-job/src/db-job-adapter.test.ts b/packages/services/service-job/src/db-job-adapter.test.ts index 1cd0b9f6c1..58a478b7de 100644 --- a/packages/services/service-job/src/db-job-adapter.test.ts +++ b/packages/services/service-job/src/db-job-adapter.test.ts @@ -12,7 +12,7 @@ function makeFakeEngine() { async find(table: string, opts: any = {}) { const t = tables.get(table) ?? []; let out = opts.where - ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)) + ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : [...t]; if (opts.orderBy) { for (const ord of [...opts.orderBy].reverse()) { diff --git a/packages/services/service-job/src/db-job-adapter.timeout.test.ts b/packages/services/service-job/src/db-job-adapter.timeout.test.ts index 955452c217..ffe5cf9ecb 100644 --- a/packages/services/service-job/src/db-job-adapter.timeout.test.ts +++ b/packages/services/service-job/src/db-job-adapter.timeout.test.ts @@ -23,7 +23,7 @@ function makeFakeEngine() { async find(table: string, opts: any = {}) { const t = tables.get(table) ?? []; let out = opts.where - ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)) + ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : [...t]; if (opts.limit) out = out.slice(0, opts.limit); return out; diff --git a/packages/services/service-job/src/job-service-plugin.test.ts b/packages/services/service-job/src/job-service-plugin.test.ts index f44a1ea395..7bf8f1e057 100644 --- a/packages/services/service-job/src/job-service-plugin.test.ts +++ b/packages/services/service-job/src/job-service-plugin.test.ts @@ -24,7 +24,7 @@ function makeFakeEngine() { async find(table: string, opts: any = {}) { const t = tables.get(table) ?? []; return opts.where - ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)) + ? t.filter((r) => Object.entries(opts.where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : [...t]; }, async insert(table: string, data: any) { diff --git a/packages/services/service-storage/src/attachment-access-hooks.test.ts b/packages/services/service-storage/src/attachment-access-hooks.test.ts index c0f6b6176a..ce8d3509ce 100644 --- a/packages/services/service-storage/src/attachment-access-hooks.test.ts +++ b/packages/services/service-storage/src/attachment-access-hooks.test.ts @@ -18,13 +18,13 @@ function install(opts: { }, find: async (_object, options: any) => { const rows = (opts.attachments ?? []).filter((r) => - Object.entries(options?.where ?? {}).every(([k, v]) => r[k] === v), + Object.entries(options?.where ?? {}).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); return typeof options?.limit === 'number' ? rows.slice(0, options.limit) : rows; }, findOne: async (_object, options: any) => (opts.attachments ?? []).find((r) => - Object.entries(options?.where ?? {}).every(([k, v]) => r[k] === v), + Object.entries(options?.where ?? {}).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ) ?? null, update: async () => ({}), }; diff --git a/packages/services/service-storage/src/file-reference-lifecycle.test.ts b/packages/services/service-storage/src/file-reference-lifecycle.test.ts index b71ba779ce..e2526bf9ab 100644 --- a/packages/services/service-storage/src/file-reference-lifecycle.test.ts +++ b/packages/services/service-storage/src/file-reference-lifecycle.test.ts @@ -173,7 +173,7 @@ async function driveMultiUpdate( data: Record, ) { const rows = (engine.tables[object] ?? []).filter((r) => - Object.entries(where).every(([k, v]) => r[k] === v), + Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; }), ); const options = { multi: true, where }; const scope: Record = {}; @@ -234,7 +234,7 @@ async function driveDelete(engine: Engine, object: string, input: any) { return (engine.tables[object] ?? []).filter((r) => ids.some((i: unknown) => String(i) === String(r.id))); })() : where - ? (engine.tables[object] ?? []).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)) + ? (engine.tables[object] ?? []).filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) : []; const drop = () => { diff --git a/packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts b/packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts index 712ddc8873..a023a9457d 100644 --- a/packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts +++ b/packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts @@ -58,7 +58,7 @@ function ledgerEngine(rows: Array>) { getObject: (name: string) => (name in tables ? { name } : undefined), async find(object, options: any) { const where = options?.where ?? {}; - return tables[object].filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return tables[object].filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async insert(object, data) { tables[object].push({ ...data }); @@ -85,11 +85,11 @@ function reapEngine() { registerHook() {}, async find(object: string, options: any) { const where = options?.where ?? {}; - return tables[object].filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); + return tables[object].filter((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })); }, async findOne(object: string, options: any) { const where = options?.where ?? {}; - return tables[object].find((r) => Object.entries(where).every(([k, v]) => r[k] === v)) ?? null; + return tables[object].find((r) => Object.entries(where).every(([k, v]) => { if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`); return r[k] === v; })) ?? null; }, async update(object: string, data: any, options?: any) { assertEngineUpdateDispatch(data, options); diff --git a/scripts/check-where-matcher-conformance.mjs b/scripts/check-where-matcher-conformance.mjs index 9e42934138..f537429e41 100644 --- a/scripts/check-where-matcher-conformance.mjs +++ b/scripts/check-where-matcher-conformance.mjs @@ -81,11 +81,69 @@ // // ## How a candidate is discovered -- and why the control probe IS the filter // -// Discovery is structural: a function in `packages/**/*.test.ts` with two or -// more identifier parameters `(R, W, …)` whose body both indexes R by a -// computed key (`R[k]` -- it reads fields off a row generically) and treats W -// as a filter object (`Object.entries(W)` / `Object.keys(W)`, or a read of -// `W.$or` / `W.$and`). +// Discovery is structural: a function in `packages/**/*.test.ts` whose body +// both indexes its FIRST parameter R by a computed key (`R[k]` -- it reads +// fields off a row generically) and treats some other identifier W as a filter +// object (`Object.entries(W)` / `Object.keys(W)`, or a read of `W.$or` / +// `W.$and`). +// +// W is looked for in two places, in this order (#8615): +// +// 1. the function's OWN second parameter -- the `(row, where)` spelling, and +// the only one this gate could see before #8615. +// 2. FAILING THAT, an ACCESS PATH the body reads as a filter, whose root is +// not one of this function's own parameters and RESOLVES in an enclosing +// scope -- either a same-file declaration (the `visibleDeclarations` walk +// already built for extraction) or a parameter bound by an enclosing +// function. This is the single-param `.filter()` callback that CLOSES OVER +// its `where`: +// +// const where = query?.where ?? {}; +// return rows.filter((r) => Object.entries(where).every(([k,v]) => r[k]===v)); +// +// Byte-for-byte the shape (b) body this gate already grades everywhere it +// appears as a second parameter -- the arity was never the defect, it was +// only what discovery happened to key on. "Not in the ledger" read as +// "conforming" for every one of them. +// +// A PATH rather than a bare identifier, because the capture is written both +// ways and roughly half the corpus uses the indirect spelling: `where` (a local +// `const`), but equally `opts.where`, `query?.where`, `q.where`, +// `options.where`, `query.filter`. Keying on the identifier alone read the ROOT +// of those (`opts`) as the filter, which is not a widening but a MIS-binding -- +// it made 21 real matchers UNJUDGED with a `TypeError` from the control probe, +// i.e. it manufactured exactly the "could not run" failures this gate treats as +// errors. Paths are reduced only through plain dotted access (optional chaining +// and a `?? {}` / `|| {}` tail unwrapped); a call or a computed key yields no +// path and no candidate, because the battery could not synthesise a binding. +// +// Requiring the root to RESOLVE is the load-bearing half of arm 2, and it is a +// real scope walk rather than a name test on purpose: a root that binds nowhere +// same-file is an import or a global, and grading those would have this gate +// judge code it cannot see. What it is NOT is a tightening substitute for the +// control probe -- arm 2 deliberately admits more structural candidates than it +// admits matchers, and the probe below is what decides membership. Measured on +// the corpus at #8615: arm 2 proposed 64 structural candidates, of which the +// control probe seated 62 and dropped 2. +// +// The 2 it dropped are worth naming, because they are a REAL residual blind +// spot rather than noise: both are inverted survivor filters inside a `delete` +// double -- `(r) => !Object.entries(opts.where).every(…)`. They carry the same +// shape (b) defect (a `$or` matches nothing, so the row is not deleted), but +// they answer the control probe `false`/`true` instead of `true`/`false` and so +// are correctly not row-SELECTING predicates by this gate's definition. Left +// ungraded on purpose: teaching the probe to recognise a negated predicate +// means guessing at intent, which is the naming-based reasoning this gate +// exists to avoid. Tracked separately. +// +// For extraction, a captured W is RE-BOUND as a synthetic second parameter, so +// the battery drives `(row, where)` uniformly whatever the source arity was -- +// the path is rebuilt outwards, so `opts.where` binds `opts` to +// `{ where: }`. Re-binding rather than inlining W's declaration is +// deliberate: the local-`const` spellings all initialise W from an enclosing +// parameter (`query?.where ?? {}`, `opts?.where ?? {}`, `args.query?.$filter ?? +// {}`) that no standalone lift can supply, so inlining would have made every +// one of them UNJUDGED -- a different way of not grading them. // // That heuristic alone over-matches. Rather than tighten it by guessing at // PARAMETER NAMES -- the failure `check-engine-double-contract` documents, @@ -234,6 +292,93 @@ const identifiersIn = (src) => { return out; }; +/** + * Does `body` read `id` as a filter object? The two spellings are exactly the + * ones the two-parameter rule has always tested for — kept as one helper so the + * own-parameter arm and the captured arm cannot drift apart (the drift that let + * one spelling get fixed and the other not, in `check-engine-double-contract`). + */ +const readsAsFilter = (body, id) => + new RegExp(`Object\\.(entries|keys)\\(\\s*${id}`).test(body) || + new RegExp(`\\b${id}\\s*\\??\\.\\s*\\$(or|and)`).test(body); + +/** `where ?? {}`, `(where)`, `where!`, `where as any` -> `where`. */ +function unwrapExpr(e) { + for (;;) { + if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAsExpression(e)) { + e = e.expression; + } else if ( + ts.isBinaryExpression(e) && + (e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || + e.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) { + e = e.left; + } else { + return e; + } + } +} + +/** + * Reduce an expression to a plain ACCESS PATH: a root identifier plus dotted + * property names (`where` -> [where]; `opts?.where` -> [opts, where]; + * `args.query.$filter` -> [args, query, $filter]). Anything else — a call, an + * element access, a computed key — returns null, because the battery cannot + * synthesise a binding for it. + */ +function accessPath(e) { + const parts = []; + for (let cur = unwrapExpr(e); ; cur = unwrapExpr(cur.expression)) { + if (ts.isIdentifier(cur)) return [cur.text, ...parts]; + if (!ts.isPropertyAccessExpression(cur) || !ts.isIdentifier(cur.name)) return null; + parts.unshift(cur.name.text); + } +} + +/** + * The two filter spellings, harvested as access paths instead of asserted + * against a known parameter name. Source order, deduplicated. + */ +function filterPathsIn(fnNode) { + const out = []; + const seen = new Set(); + const add = (expr) => { + const path = accessPath(expr); + if (!path) return; + const k = path.join('.'); + if (!seen.has(k)) { seen.add(k); out.push(path); } + }; + const visit = (n) => { + if ( + ts.isCallExpression(n) && + ts.isPropertyAccessExpression(n.expression) && + ts.isIdentifier(n.expression.expression) && + n.expression.expression.text === 'Object' && + (n.expression.name.text === 'entries' || n.expression.name.text === 'keys') && + n.arguments.length >= 1 + ) { + add(n.arguments[0]); + } + if (ts.isPropertyAccessExpression(n) && (n.name.text === '$or' || n.name.text === '$and')) { + add(n.expression); + } + ts.forEachChild(n, visit); + }; + visit(fnNode.body); + return out; +} + +/** Parameter names bound by the functions/methods enclosing `node`. */ +function enclosingParameters(node) { + const out = new Set(); + for (let cur = node.parent; cur; cur = cur.parent) { + if (Array.isArray(cur.parameters)) { + for (const p of cur.parameters) if (ts.isIdentifier(p.name)) out.add(p.name.text); + } + } + return out; +} + /** * Structural candidates in one source text. Behavioural admission (the control * probe) happens later, in `judge` — this stage only proposes. @@ -245,21 +390,39 @@ export function discoverInSource(text, label) { const visit = (node) => { if (isFn(node) && node.body) { const params = node.parameters.map((p) => (ts.isIdentifier(p.name) ? p.name.text : null)); - if (params.length >= 2 && params[0] && params[1]) { - const body = node.body.getText(sf); - const [r, w] = params; - const indexesRow = new RegExp(`\\b${r}\\s*\\??\\.?\\[`).test(body); - const readsFilter = - new RegExp(`Object\\.(entries|keys)\\(\\s*${w}`).test(body) || - new RegExp(`\\b${w}\\s*\\??\\.\\s*\\$(or|and)`).test(body); - if (indexesRow && readsFilter) { + const r = params[0]; + const body = r ? node.body.getText(sf) : ''; + if (r && new RegExp(`\\b${r}\\s*\\??\\.?\\[`).test(body)) { + // Arm 1 — the filter is this function's own second parameter. + let w = params.length >= 2 && params[1] && readsAsFilter(body, params[1]) ? params[1] : null; + let captured = false; + let declarations = null; + // Arm 2 (#8615) — no own filter parameter, so look for one CAPTURED + // from an enclosing scope. Resolution of the path's ROOT is required; a + // root binding nowhere in this file is an import or a global, and this + // gate does not grade code it cannot see. + if (!w) { + const own = new Set(params.filter(Boolean)); + declarations = visibleDeclarations(node, sf); + const enclosing = enclosingParameters(node); + for (const path of filterPathsIn(node)) { + const root = path[0]; + if (own.has(root)) continue; + if (!declarations.has(root) && !enclosing.has(root)) continue; + w = path; + captured = true; + break; + } + } + if (w) { const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf)); out.push({ file: label, line: line + 1, name: declaredName(node) ?? '(anonymous)', source: node.getText(sf), - declarations: visibleDeclarations(node, sf), + declarations: declarations ?? visibleDeclarations(node, sf), + capturedPath: captured ? w : null, }); } } @@ -275,12 +438,18 @@ export function discoverInSource(text, label) { // --------------------------------------------------------------------------- function buildCallable(candidate, dropped = new Set()) { const self = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate.name) ? candidate.name : '__matcher'; + const captured = candidate.capturedPath ?? null; + // A captured filter is supplied by the battery, so the path's ROOT must NOT + // be lifted in: its declaration would shadow the synthetic binding below, and + // every real instance initialises it from an enclosing parameter this lift + // cannot supply anyway (`query?.where ?? {}`). + const skip = captured ? new Set([...dropped, captured[0]]) : dropped; const included = new Map(); let frontier = identifiersIn(candidate.source); for (let depth = 0; depth < 6; depth++) { const next = new Set(); for (const id of frontier) { - if (included.has(id) || id === self || dropped.has(id)) continue; + if (included.has(id) || id === self || skip.has(id)) continue; const decl = candidate.declarations.get(id); if (!decl) continue; included.set(id, decl); @@ -293,9 +462,21 @@ function buildCallable(candidate, dropped = new Set()) { /^(export\s+)?(default\s+)?(async\s+)?function\s+[A-Za-z_$][A-Za-z0-9_$]*/, 'function', ); - const code = - `${[...included.values()].join('\n')}\n` + - `const ${self} = ${normalised};\nreturn ${self};`; + // Re-bind a captured filter as a synthetic second parameter so the battery + // drives `(row, where)` uniformly, whatever arity the source was written at. + // The path is rebuilt outwards, so `opts.where` becomes `{ where: }` + // and `args.query.$filter` becomes `{ query: { $filter: } }`. + let definition = `const ${self} = ${normalised};`; + if (captured) { + let bound = '__os_where'; + for (const prop of captured.slice(1).reverse()) bound = `{ ${JSON.stringify(prop)}: ${bound} }`; + definition = + `const ${self} = (__os_row, __os_where) => {\n` + + ` const ${captured[0]} = ${bound};\n` + + ` return (${normalised})(__os_row);\n` + + `};`; + } + const code = `${[...included.values()].join('\n')}\n${definition}\nreturn ${self};`; const js = ts.transpileModule(code, { compilerOptions: { target: ts.ScriptTarget.ES2022, isolatedModules: true }, }).outputText; @@ -524,6 +705,65 @@ const matches = (row: any, where: any): boolean => { return Object.entries(where).every(([k, v]) => k.startsWith('$') || eq(row[k], v)); };`; +// --- #8615: the captured-filter arm ------------------------------------- +// Every fixture below is a SINGLE-parameter callback. None of them has a second +// parameter to key on, which is the whole point: before #8615 discovery could +// not see one of these, so the gate could grade them neither green nor red. + +// Captured from a same-file `const`, combinator-blind. The shape the 17 sites +// on the card were written in. +const FIXTURE_CAPTURED_BLIND = ` +const run = (rows: any[], query: any) => { + const where = query?.where ?? {}; + return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); +};`; + +// Captured through an ACCESS PATH rooted at an ENCLOSING PARAMETER. Roughly +// half the corpus is spelled this way (`opts.where`, `q.where`, `query.filter`) +// and keying on the root identifier alone mis-binds it into a TypeError. +const FIXTURE_CAPTURED_PATH = ` +function makeDriver(rows: any[]) { + return { + find: (opts: any) => rows.filter((r) => Object.entries(opts.where).every(([k, v]) => r[k] === v)), + }; +}`; + +// Same shape, but the filter root binds NOWHERE in the file — an import or a +// global. Discovery must decline it: this gate does not grade code it cannot +// see. This is the fixture that holds the resolution requirement in place. +const FIXTURE_CAPTURED_UNRESOLVED = ` +const run = (rows: any[]) => + rows.filter((r) => Object.entries(IMPORTED_FILTER).every(([k, v]) => r[k] === v));`; + +// The filter expression is a CALL, so it reduces to no access path and the +// battery could not synthesise a binding for it. Declined, not guessed at. +const FIXTURE_CAPTURED_CALL = ` +const run = (rows: any[], get: any) => + rows.filter((r) => Object.entries(get()).every(([k, v]) => r[k] === v));`; + +// A captured matcher that conjoins correctly — discovery must not assume that +// "single-param callback" implies "defective". +const FIXTURE_CAPTURED_CORRECT = ` +const run = (rows: any[], query: any) => { + const where = query?.where ?? {}; + return rows.filter((r) => + Object.entries(where).every(([k, v]) => + k === '$or' + ? (v as any[]).some((b) => Object.entries(b).every(([bk, bv]) => r[bk] === bv)) + : k === '$and' + ? (v as any[]).every((b) => Object.entries(b).every(([bk, bv]) => r[bk] === bv)) + : r[k] === v)); +};`; + +// An INVERTED survivor filter inside a delete double. Structurally a candidate, +// and it carries the same shape (b) defect — but it answers the control probe +// backwards, so it is not a row-SELECTING predicate and the probe drops it. +// Pinned as a fixture so this residual stays a KNOWN limit rather than drifting +// into an accidental one. +const FIXTURE_CAPTURED_NEGATED = ` +const run = (rows: any[], opts: any) => + rows.filter((r) => !Object.entries(opts.where).every(([k, v]) => r[k] === v));`; + function judgeFixture(src) { const found = discoverInSource(src, 'fixture.test.ts'); return { found, results: found.map(judge) }; @@ -576,6 +816,40 @@ function selfTest() { const closure = judgeFixture(FIXTURE_HELPER_CLOSURE); expect('a matcher using a same-file helper is lifted', closure.results[0]?.verdict === 'CONFORMING'); + // --- #8615: the captured-filter arm ------------------------------------ + const capBlind = judgeFixture(FIXTURE_CAPTURED_BLIND); + expect('a single-param callback capturing a same-file `where` is discovered', capBlind.found.length === 1); + expect('its captured path is recorded', capBlind.found[0]?.capturedPath?.join('.') === 'where'); + expect('a captured combinator-blind matcher is SILENT', capBlind.results[0]?.verdict === 'SILENT'); + + const capPath = judgeFixture(FIXTURE_CAPTURED_PATH); + expect('a filter captured through opts.where is discovered', capPath.found.length === 1); + expect('the whole access path is recorded', capPath.found[0]?.capturedPath?.join('.') === 'opts.where'); + expect( + 'a path-captured matcher is JUDGED, not left unrunnable', + capPath.results[0]?.verdict === 'SILENT', + ); + + const capUnresolved = judgeFixture(FIXTURE_CAPTURED_UNRESOLVED); + expect( + 'a filter root that resolves nowhere same-file is NOT discovered', + capUnresolved.found.length === 0, + ); + + const capCall = judgeFixture(FIXTURE_CAPTURED_CALL); + expect('a filter expression that is a call yields no candidate', capCall.found.length === 0); + + const capCorrect = judgeFixture(FIXTURE_CAPTURED_CORRECT); + expect('a conjoining captured matcher is discovered', capCorrect.found.length === 1); + expect('a conjoining captured matcher is CONFORMING', capCorrect.results[0]?.verdict === 'CONFORMING'); + + const capNegated = judgeFixture(FIXTURE_CAPTURED_NEGATED); + expect('an inverted survivor filter is a structural candidate', capNegated.found.length === 1); + expect( + 'the control probe drops the inverted survivor filter (documented residual)', + capNegated.results[0]?.verdict === 'OUT_OF_SCOPE', + ); + // Reconciliation, both directions. const fakeMeasured = new Map([['a.test.ts', { silent: 1, unjudged: 0, details: [{ line: 1, name: 'm', result: { shapes: ['x'] } }] }]]); expect('an unbaselined silent matcher is an error', reconcile(fakeMeasured, {}).length === 1); @@ -592,7 +866,10 @@ function selfTest() { } console.log( 'OK self-test: separates conjoining, early-returning, combinator-blind and refusing\n' + - ' matchers on synthetic fixtures; the control probe drops a non-predicate; the\n' + + ' matchers on synthetic fixtures; the control probe drops a non-predicate; single-\n' + + ' parameter callbacks capturing their filter — by name and by access path — are\n' + + ' discovered and judged, while an unresolvable root, a computed filter and an\n' + + ' inverted survivor filter are each declined for their own recorded reason; the\n' + ' ledger reconciles in both directions.', ); }