Skip to content

Commit 2e35e25

Browse files
committed
chore(#5322): 收官同步 —— filter.zod 空组合子宣告转正、#5366 refusal 表随裁定翻向
- filter.zod.ts:按 #5323 同步散文预留的交接("The declaration flips to stated contract with that PR"),空组合子单位元从「Deliberately NOT declared」段转为正式契约段;{field:{}} 半边保持未宣告(#5376 仍开)。 - filter-refusal-envelope.test.ts(#5366 新到):空数组两行从 REFUSALS 翻入 ACCEPTED(单位元树断言),同一守卫点的非数组拼写补位 REFUSALS,信封不变。 - filter-logic-conformance.ts:族 1 段落按分工删除(四行已进表),族 2/3 原样。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
1 parent b346089 commit 2e35e25

2 files changed

Lines changed: 51 additions & 20 deletions

File tree

packages/services/service-analytics/src/__tests__/filter-refusal-envelope.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,22 @@ const REFUSALS: Array<{ name: string; where: unknown; message: RegExp; issueBull
8585
message: /needs a two-element \[min, max\] array/,
8686
issueBullet: true,
8787
},
88+
// FLIPPED with the #5322 ruling: these two entries were "$and/$or with an
89+
// empty array". The refusing SITE is unchanged (the combinator guard #5352's
90+
// body bulleted, hence issueBullet stays true) but its empty-array half
91+
// graduated to a boolean identity — `{$and: []}` = TRUE, `{$or: []}` = FALSE,
92+
// asserted in ACCEPTED below — so the site's remaining refusal is the
93+
// non-array spelling, still carrying the same envelope.
8894
{
89-
name: '$and with an empty array',
90-
where: { $and: [] },
91-
message: /"\$and" requires a non-empty array/,
95+
name: '$and that is not an array',
96+
where: { $and: 'won' },
97+
message: /"\$and" requires an array of filter objects/,
9298
issueBullet: true,
9399
},
94100
{
95-
name: '$or with an empty array',
96-
where: { $or: [] },
97-
message: /"\$or" requires a non-empty array/,
101+
name: '$or that is not an array',
102+
where: { $or: { stage: 'won' } },
103+
message: /"\$or" requires an array of filter objects/,
98104
issueBullet: true,
99105
},
100106
{
@@ -176,6 +182,20 @@ const ACCEPTED: Array<{ name: string; where: unknown; tree: unknown }> = [
176182
where: {},
177183
tree: null,
178184
},
185+
{
186+
// #5322: the AND identity — a conjunction of zero conditions constrains
187+
// nothing. Was in REFUSALS ("requires a non-empty array") until the ruling.
188+
name: 'an empty $and as TRUE (#5322)',
189+
where: { $and: [] },
190+
tree: null,
191+
},
192+
{
193+
// #5322: the OR identity — a disjunction of zero conditions matches
194+
// nothing. Fail-closed for a scope whose disjunct list looped to zero items.
195+
name: 'an empty $or as the FALSE constant (#5322)',
196+
where: { $or: [] },
197+
tree: { kind: 'const', value: false },
198+
},
179199
{
180200
// #5334: `[]` is "no filter", not a failed filter.
181201
name: 'an empty `where` array as "no filter" (#5334)',

packages/spec/src/data/filter.zod.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -289,22 +289,33 @@ export type FilterCondition = {
289289
* Directive #10, and this sentence is kept as the record that the tracking
290290
* worked.
291291
*
292+
* ## Empty combinators are boolean identities (#5322, maintainer ruling 2026-08-04)
293+
*
294+
* `{ $and: [] }` is TRUE — the AND identity, no constraint. `{ $or: [] }` is
295+
* FALSE — the OR identity, zero rows. A `{}` disjunct is TRUE and ABSORBS its
296+
* `$or`; `{ $not: {} }` is `NOT TRUE` — FALSE. The ruling took the reduction
297+
* over the analytics compilers' fail-closed throw for two reasons: only a
298+
* reduction can evaluate a NESTED tree (a rejection must first reduce to
299+
* judge an empty combinator sitting inside a `$or` branch, which concedes the
300+
* point), and `{ $or: [] }` = zero rows is fail-closed exactly where it
301+
* matters — an RLS scope whose disjunct list loops to zero items hides every
302+
* row instead of exposing the table (#5134). An earlier revision of this
303+
* paragraph kept the identities OUT of the contract because two compilers
304+
* still refused them; that gap closed with PR #5365 (both
305+
* `service-analytics` compilers reduce, and the four cases are enrolled in
306+
* `filter-logic-conformance.ts` against every backend — the five drivers
307+
* already reduced: `driver-sql` #5243, `driver-mongodb` #5323). Loud
308+
* AUTHORING-time rejection of the literal spellings is a separate, optional
309+
* lint concern (#5330), not a runtime semantic.
310+
*
292311
* ## Deliberately NOT declared here
293312
*
294-
* The boolean identities of the EMPTY combinators (`{ $and: [] }` = TRUE,
295-
* `{ $or: [] }` = FALSE, `{ $not: {} }` = FALSE) are RULED — #5322
296-
* (maintainer, 2026-08-04) took the identity over the analytics compilers'
297-
* fail-closed throw — but not yet stated here as contract: on main today
298-
* `read-scope-sql` and `filter-normalizer` still refuse an empty `$and`/`$or`,
299-
* and the ruling's implementation PR #5365 (aligns both compilers, enrolls the
300-
* four cases in `FILTER_LOGIC_CASES`) is sequenced to land after this one. The
301-
* declaration flips to stated contract with that PR, not here — declaring it
302-
* first would out-run enforcement. Likewise `{ field: {} }` (a field
303-
* constrained by zero operators): #5240 ruled it REJECTED and #5327 gated
304-
* driver-sql / driver-sqlite-wasm / driver-memory / formula; `driver-mongodb`
305-
* still answers it (tracked by #5376), and the schema-side narrowing stays
306-
* with the spec lane. Declaring either before it is enforced everywhere would
307-
* be exactly the `declared ≠ enforced` shape this file exists to prevent.
313+
* `{ field: {} }` (a field constrained by zero operators): #5240 ruled it
314+
* REJECTED and #5327 gated driver-sql / driver-sqlite-wasm / driver-memory /
315+
* formula; `driver-mongodb` still answers it (tracked by #5376), and the
316+
* schema-side narrowing stays with the spec lane. Declaring it before it is
317+
* enforced everywhere would be exactly the `declared ≠ enforced` shape this
318+
* file exists to prevent.
308319
*/
309320
export const FilterConditionSchema: z.ZodType<FilterCondition, FilterCondition> = z.lazy(() =>
310321
z.record(z.string(), z.unknown()).and(

0 commit comments

Comments
 (0)