Skip to content

Commit 3510e4a

Browse files
qq9340100claude
andauthored
refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) (#6528)
维护者 2026-08-06 裁决方案 1。布尔身份归约(`$and: []` 为 TRUE、`$or: []` 为 FALSE、`{}` 是吸收 `$or` 的 TRUE 析取项、`$not: {}` 为 FALSE)是 #5322/#5134 的裁 决,由 `FILTER_LOGIC_CASES` 的四条恒等元 case 钉住 —— 却被实现了四遍:driver-sql 的 `reduceFilterNode`、driver-mongodb 里同名的一份、driver-memory matcher 里 `every`/`some`/真值性涌现出的第三份,以及 lint 里差点手抄的第四份( `lint-flow-patterns.ts` 拒绝手抄并开了本 issue)。 ## 收编 新增 `packages/spec/src/data/filter-verdict.ts` —— 与证明它的 case 表同处一地: - `reduceFilterVerdict(node, options?) -> 'true' | 'false' | 'clause'`,纯归约, 自身从不抛错; - `reduceFilterKeyVerdict(key, value, options?)`,SQL / MongoDB 两个 emitter 在 已进入某节点时只问一个 key 的判定,需要这个入口; - `FilterVerdictHooks` —— 各 backend 自己的拒绝(driver-sql 的未声明 `$` 组合子、 `undefined` 比较数;driver-mongodb 的 query-level key、`$null` 比较数)作为钩子 传入,并在它们原先被调用的**同一位置**调用。所以没有任何 message / code / status 发生位移。 - 无钩子时(lint 的用法)每个无法归约的形状一律答 `'clause'`(保守方向);把不可 归约的东西答成 `'true'` 才是把垃圾提升成 match-all —— #5239 正是为此把非节点 拒绝放在 MongoDB 身份归约之前。 四个消费者收编: 1. `driver-sql/src/sql-driver.ts` —— 机械替换,拒绝留在 `classifyFilterKey`; 2. `driver-mongodb/src/mongodb-filter.ts` —— 同; 3. `driver-memory/src/memory-matcher.ts` 的 `evaluate` —— 前置一次记录无关的判定 (`'true'`/`'false'` 直接返回,`'clause'` 原样落到未改动的求值循环)。恒等元答 案不再是 `Array.prototype` 的涌现副产物,而是与另两个 driver 同一个对象; 4. `packages/lint/src/lint-flow-patterns.ts` 的 `filterCarriesNoCondition`。 ## lint 补回两个漏判形状 `multi: true` 无界整表写规则(#5482)此前问的是"filter 有没有零个 key",于是 `filter: { $and: [] }`(AND 恒等元,整表写)与 `filter: { $or: [{}] }`(TRUE 析取 项吸收整个 `$or`)两种形状静默通过。现在改问归约,两者都告警;而 `{ $or: [] }` 与 `{ $not: {} }` 归约为 FALSE(匹配零行),继续保持沉默 —— 手写"是不是空组合子" 的判据会把这两个也误报,共享判定不会,因为它就是 driver 执行的那个判定。 message 按实际形状命名(``a `filter` that REDUCES TO TRUE (`{"$and":[]}`)``), 不再把非空 filter 说成 "EMPTY"。lint 测试正反两向断言,并直接断言 `reduceFilterVerdict(...) === 'true'` 与规则告警一一对应。 ## 验证 - 反向验证(方向先写后跑):把共享谓词的 `$and: []` 判定临时改为 FALSE,预判 spec pin + case-set 一致性检查、lint 新告警、以及**三个 driver 的 conformance case-set** 同向转红。实测 spec 5 红 / lint 3 红 / driver-memory 1 红 / driver-sql 3 红,复原后全绿。driver 侧转红是关键一环:它证明这次耦合是真的, 不是装饰 —— 收编前动 spec 侧谓词碰不到任何 driver。 - 五包全绿:spec 341 files / 8758 tests,lint 62 / 1578,driver-sql 69 / 959, driver-mongodb 10 / 206,driver-memory 18 / 532;五包 typecheck 全 Done。 - 继承 SqlDriver 的 driver-sqlite-wasm(18 / 254)与 driver-turso(27 / 828)同样 全绿。 - `pnpm check:type-check-debt` OK(34 条 ledger 重新测量,无一超出记录值 —— 首跑 报 root +5 是 AGENTS.md §9 的陈旧构建陷阱,`pnpm build` 全量后消失)。 - 门禁族全绿:eslint、check:nul-bytes、check:engine-double-contract、 check:error-code-casing、check:route-envelope、check:driver-conformance、 check:spec-parsed-alias、check:query-options-erasure、check:merge-driver。 - spec 生成物:新增 5 个公开导出,`check:generated --fix` 只重生成被证明陈旧的 `api-surface/`(data.json +5 行);其余 9 项本就最新,复跑后 10/10 up to date。 `gen:openapi` 补跑,产物在 gitignore 的 `json-schema/` 树内,无 tracked 变更。 ADR-0122 不适用:新导出是普通 TS 函数与类型,不是 Zod schema 别名, `type-alias-convention.pin.test.ts` 的 754 计数无需变动(spec 全量测试已覆盖)。 ## 边界(按认领申报严守) 未触碰 `packages/lint/src/index.ts`(在飞 PR #6472 正在改该 barrel;本改动不产生 新规则 id,实测确不需要动它)、`data-model-rules.ts`、 `validate-semantic-roles.ts`、`validate-visibility-predicates.ts`。 未触碰 `content/docs/releases/`。 ## 交付 本会话无 GitHub 工具,按指示只推分支、不开 PR。 Claude-Session: https://claude.ai/code/session_01TQ1rGJx1JZ1dYGP4r9Mmov Co-authored-by: Claude <noreply@anthropic.com>
1 parent 259459d commit 3510e4a

10 files changed

Lines changed: 809 additions & 112 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/driver-sql": patch
4+
"@objectstack/driver-mongodb": patch
5+
"@objectstack/driver-memory": patch
6+
"@objectstack/lint": patch
7+
---
8+
9+
refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659)
10+
11+
`{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE
12+
disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling
13+
(#5322/#5134) pinned for every backend by the four identity cases in
14+
`FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode`
15+
in `driver-sql`, the same function again in `driver-mongodb`, the
16+
`every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a
17+
fifth hand-written copy inside `@objectstack/lint`, which declined to write one
18+
and filed this issue instead.
19+
20+
**New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**,
21+
beside the case table that proves it. It answers `'true' | 'false' | 'clause'`
22+
for a filter node and never throws on its own; each backend's own refusals — the
23+
undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the
24+
query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as
25+
`FilterVerdictHooks` and are invoked from exactly the positions they were invoked
26+
from before. `reduceFilterKeyVerdict` answers the same question for one key, which
27+
is what both SQL and MongoDB emitters consult while walking a node.
28+
29+
**No behaviour changes in the three drivers.** The move is mechanical: the shared
30+
algebra replaces each private copy, the refusals stay where they were, and the
31+
`FILTER_LOGIC_CASES` conformance suites are green on both sides of the change —
32+
including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`.
33+
34+
**`@objectstack/lint` gains two warnings it was structurally blind to.** The
35+
`multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero
36+
keys", so a `delete_record` bounded by `filter: { $and: [] }` or
37+
`filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes
38+
— passed silently. It now asks the reduction, and it warns about both while
39+
staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The
40+
message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`)
41+
rather than calling a non-empty filter "empty".
42+
43+
If you have a flow declaring a bulk write bounded by one of those two shapes, the
44+
lint will now tell you so — the write was already unbounded at run time; only the
45+
feedback is new.

packages/drivers/driver-memory/src/memory-matcher.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
* comparand is not `[min, max]` (#5328).
1717
*/
1818

19+
// [#5659] The Filter Protocol's boolean identity reduction, shared with
20+
// driver-sql, driver-mongodb and the flow linter. See `evaluate` for why a
21+
// record-at-a-time matcher consults a record-INDEPENDENT verdict first.
22+
import { reduceFilterVerdict } from '@objectstack/spec/data';
23+
1924
import { assertFilterConditionShape } from './filter-refusal.js';
2025

2126
type RecordType = Record<string, any>;
@@ -41,9 +46,34 @@ export function match(record: RecordType, filter: any): boolean {
4146
return evaluate(record, filter);
4247
}
4348

49+
/**
50+
* [#5659] The boolean identities come from the SHARED reduction; everything
51+
* record-dependent is still decided here.
52+
*
53+
* This matcher used to answer `{ $and: [] }`, `{ $or: [] }`, `{}` and
54+
* `{ $not: {} }` as a by-product of the JS it happens to be written in —
55+
* `every` over an empty array is `true`, `some` is `false`, an empty node falls
56+
* through to the trailing `return true`. Those four answers are not an accident
57+
* of `Array.prototype`, they are a ruling (#5322/#5134) that `driver-sql` and
58+
* `driver-mongodb` each spell out in a `reduceFilterNode` of their own, and
59+
* "emergent from the evaluator" is not a place a ruling can be read from or
60+
* held to. Asking the shared predicate makes this backend's identity answers
61+
* the same OBJECT as theirs rather than a third agreeing coincidence.
62+
*
63+
* It is a pre-pass and not a rewrite of the loops below, because the verdict is
64+
* record-independent by construction and the evaluation is not: `'clause'` —
65+
* the answer for every filter that carries a real predicate — falls straight
66+
* through to the untouched code. And a `'true'` verdict can only be reached by
67+
* a filter whose keys are ALL combinators that resolved (a field key always
68+
* contributes `'clause'`), so returning early on it skips no field constraint.
69+
*/
4470
function evaluate(record: RecordType, filter: any): boolean {
4571
if (!filter || Object.keys(filter).length === 0) return true;
46-
72+
73+
const verdict = reduceFilterVerdict(filter);
74+
if (verdict === 'true') return true;
75+
if (verdict === 'false') return false;
76+
4777
// 1. Handle Top-Level Logical Operators ($and, $or, $not)
4878
// These usually appear at the root or nested.
4979

packages/drivers/driver-mongodb/src/mongodb-filter.ts

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
import type { Filter } from 'mongodb';
2727
import { nextUtcCalendarDay } from '@objectstack/core';
2828
import { StandardErrorCode } from '@objectstack/spec/api';
29+
// [#5659] The Filter Protocol's boolean identity reduction, shared with
30+
// driver-sql, driver-memory and the flow linter and proven against the same
31+
// `FILTER_LOGIC_CASES` table this driver's conformance suite runs. This file
32+
// supplies only its own refusals — see `reduceFilterNode` below.
33+
import {
34+
reduceFilterVerdict,
35+
reduceFilterKeyVerdict,
36+
type FilterVerdict as SharedFilterVerdict,
37+
type FilterVerdictHooks,
38+
} from '@objectstack/spec/data';
2939
import {
3040
coerceTemporalValue,
3141
type TemporalFieldKind,
@@ -64,8 +74,12 @@ function matchNothing(): Filter<any> {
6474
* - `'true'` — matches every document; the translator emits no condition.
6575
* - `'false'` — matches no document; the translator emits {@link matchNothing}.
6676
* - `'clause'` — carries at least one real predicate; translate it normally.
77+
*
78+
* [#5659] The vocabulary is `@objectstack/spec`'s now, because the REDUCTION
79+
* that produces it is — see {@link reduceFilterNode}. Kept as a local alias so
80+
* every use site below still reads `FilterVerdict`.
6781
*/
68-
type FilterVerdict = 'true' | 'false' | 'clause';
82+
type FilterVerdict = SharedFilterVerdict;
6983

7084
/**
7185
* [#5239] Is `value` a Filter Protocol NODE — the shape `FilterConditionSchema`
@@ -135,51 +149,56 @@ function assertFilterNodeList(value: unknown, key: string, path: string): assert
135149
* emitted document came out empty, is the point. "Nothing was emitted" cannot
136150
* distinguish "the author wrote an empty group" from "something failed to
137151
* translate"; a structural verdict has no such blind spot.
152+
*
153+
* ## [#5659] The algebra is `@objectstack/spec`'s; the REFUSALS are this driver's
154+
*
155+
* Every paragraph above describes a ruling four consumers had to agree on and
156+
* implemented four times — here, in `driver-sql` (whose copy this one was
157+
* written to mirror, down to the variable names), in `driver-memory`'s matcher,
158+
* and nearly a fifth time inside `@objectstack/lint`. It lives once now, in
159+
* {@link reduceFilterVerdict}, proven against the same `FILTER_LOGIC_CASES`
160+
* table this driver's conformance suite runs. What stays here is WHICH shapes
161+
* this translator refuses and how it words them, handed over as
162+
* {@link MONGO_FILTER_VERDICT_HOOKS} and invoked from exactly the positions
163+
* they were invoked from before.
138164
*/
139165
function reduceFilterNode(node: Record<string, unknown>, path: string): FilterVerdict {
140-
let sawFalse = false;
141-
let sawClause = false;
142-
for (const [key, value] of Object.entries(node)) {
143-
const verdict = reduceFilterKey(key, value, path);
144-
if (verdict === 'false') sawFalse = true;
145-
else if (verdict === 'clause') sawClause = true;
146-
}
147-
return sawFalse ? 'false' : sawClause ? 'clause' : 'true';
166+
return reduceFilterVerdict(node, { ...MONGO_FILTER_VERDICT_HOOKS, path });
148167
}
149168

150169
/** [#5239] The verdict of ONE key of a filter node. */
151170
function reduceFilterKey(key: string, value: unknown, path: string): FilterVerdict {
152-
const here = path ? `${path}.${key}` : key;
153-
154-
if (key === '$and' || key === '$or') {
155-
assertFilterNodeList(value, key, here);
156-
let sawTrue = false;
157-
let sawFalse = false;
158-
let sawClause = false;
159-
value.forEach((element, index) => {
160-
const elementPath = `${here}[${index}]`;
161-
assertFilterNode(element, elementPath);
162-
const verdict = reduceFilterNode(element, elementPath);
163-
if (verdict === 'true') sawTrue = true;
164-
else if (verdict === 'false') sawFalse = true;
165-
else sawClause = true;
166-
});
167-
// `$and: []` → no FALSE, no clause → TRUE (the AND identity).
168-
if (key === '$and') return sawFalse ? 'false' : sawClause ? 'clause' : 'true';
169-
// `$or: []` → no TRUE, no clause → FALSE (the OR identity). MongoDB itself
170-
// answers neither: it rejects the empty array outright
171-
// (`$and/$or/$nor must be a nonempty array`), so this filter used to be a
172-
// 500-shaped throw rather than a verdict.
173-
return sawTrue ? 'true' : sawClause ? 'clause' : 'false';
174-
}
171+
return reduceFilterKeyVerdict(key, value, { ...MONGO_FILTER_VERDICT_HOOKS, path });
172+
}
175173

176-
if (key === '$not') {
177-
assertFilterNode(value, here);
178-
const inner = reduceFilterNode(value, here);
179-
// NOT TRUE ≡ FALSE — so `{ $not: {} }` matches nothing.
180-
return inner === 'true' ? 'false' : inner === 'false' ? 'true' : 'clause';
181-
}
174+
/**
175+
* [#5659] This translator's half of the reduction: the shape refusals, at the
176+
* positions the shared walk visits them.
177+
*
178+
* The two `assert*` functions are wrapped in arrows rather than passed by
179+
* reference because they are TypeScript assertion functions, whose narrowing is
180+
* meaningless through a property reference. Nothing else about the call changes.
181+
*/
182+
const MONGO_FILTER_VERDICT_HOOKS: FilterVerdictHooks = {
183+
assertNodeList: (value, key, path) => assertFilterNodeList(value, key, path),
184+
assertNode: (value, path) => assertFilterNode(value, path),
185+
classifyKey: (key, value, here) => classifyFilterKey(key, value, here),
186+
};
182187

188+
/**
189+
* [#5239] The verdict of ONE **non-combinator** key — and this translator's gate
190+
* on what a field constraint may not be.
191+
*
192+
* `here` is the already-joined path of the key, exactly as the reduction hands
193+
* it over; the three combinator arms this used to open with are the shared
194+
* walk's now.
195+
*
196+
* MongoDB's own answer to an empty combinator is worth keeping written down,
197+
* because the reduction is what spares this driver from it: `$and`/`$or` with an
198+
* empty array is rejected outright (`$and/$or/$nor must be a nonempty array`),
199+
* so `{ $or: [] }` used to be a 500-shaped throw rather than a verdict.
200+
*/
201+
function classifyFilterKey(key: string, value: unknown, here: string): FilterVerdict {
183202
// Query-level keys carry no predicate; the emitter skips them and so does the
184203
// verdict, so the two never disagree about what this node is worth.
185204
if (QUERY_LEVEL_KEYS.has(key)) return 'true';

packages/drivers/driver-sql/src/sql-driver.ts

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ import { parseAutonumberFormat, renderAutonumber, missingFieldValues, isTenancyD
1414
// `AggregationNodeSchema.function` actually admits.
1515
import { AggregationFunction } from '@objectstack/spec/data';
1616
import { STRUCTURED_JSON_TYPES, FILE_REFERENCE_TYPES, MULTI_OPTION_TYPES, NUMERIC_VALUE_TYPES } from '@objectstack/spec/data';
17+
// [#5659] The Filter Protocol's boolean identity reduction — `$and: []` is TRUE,
18+
// `$or: []` is FALSE, `{}` is a TRUE disjunct, `$not: {}` is FALSE. One
19+
// implementation for all four consumers, proven against the same
20+
// `FILTER_LOGIC_CASES` table this driver's conformance suite runs; this file
21+
// supplies only its own refusals. See `reduceFilterNode` below.
22+
import {
23+
reduceFilterVerdict,
24+
reduceFilterKeyVerdict,
25+
type FilterVerdict as SharedFilterVerdict,
26+
type FilterVerdictHooks,
27+
} from '@objectstack/spec/data';
1728
// `defaultValue` runtime tokens (#4560). The DDL below asks the SPEC — not a
1829
// list of its own — which `defaultValue`s are instructions rather than literals,
1930
// so the engine and this driver can never disagree about what may become a
@@ -1000,8 +1011,12 @@ function safeShapePreview(value: unknown): string {
10001011
* - `'true'` — matches every row; the compiler emits NO clause for it.
10011012
* - `'false'` — matches no row; the compiler emits the dialect FALSE constant.
10021013
* - `'clause'` — carries at least one real predicate; compile it normally.
1014+
*
1015+
* [#5659] The vocabulary is `@objectstack/spec`'s now, because the REDUCTION
1016+
* that produces it is — see {@link reduceFilterNode}. Kept as a local alias so
1017+
* every use site below still reads `FilterVerdict`.
10031018
*/
1004-
type FilterVerdict = 'true' | 'false' | 'clause';
1019+
type FilterVerdict = SharedFilterVerdict;
10051020

10061021
/**
10071022
* [#5134] Is `value` a Filter Protocol NODE — the shape `FilterConditionSchema`
@@ -1359,52 +1374,59 @@ function assertFilterNodeList(value: unknown, key: string, path: string): assert
13591374
* "empty because the author wrote nothing" from "empty because something failed
13601375
* to compile". A structural verdict has no such blind spot, and it lets the
13611376
* emitter guarantee that every group it opens receives at least one clause.
1377+
*
1378+
* ## [#5659] The algebra is `@objectstack/spec`'s; the REFUSALS are this driver's
1379+
*
1380+
* Everything above describes a ruling (#5322/#5134) that four consumers had to
1381+
* agree on and implemented four times — here, in `driver-mongodb`, in
1382+
* `driver-memory`'s matcher, and nearly a fifth time inside `@objectstack/lint`,
1383+
* which declined to hand-write it and filed #5659 instead. The reduction now
1384+
* lives once, in {@link reduceFilterVerdict}, proven against the same
1385+
* `FILTER_LOGIC_CASES` table this driver's conformance suite runs.
1386+
*
1387+
* What stays here is what is genuinely this driver's: WHICH shapes it refuses
1388+
* and with which message. They are handed to the shared walk as
1389+
* {@link SQL_FILTER_VERDICT_HOOKS} and are invoked from exactly the positions
1390+
* they were invoked from before, so no wording, code or status moved — the
1391+
* conformance case-set is green on both sides of the change.
13621392
*/
13631393
function reduceFilterNode(node: Record<string, unknown>, path: string): FilterVerdict {
1364-
let sawFalse = false;
1365-
let sawClause = false;
1366-
for (const [key, value] of Object.entries(node)) {
1367-
const verdict = reduceFilterKey(key, value, path);
1368-
if (verdict === 'false') sawFalse = true;
1369-
else if (verdict === 'clause') sawClause = true;
1370-
}
1371-
// AND over the node's keys: FALSE dominates, then a real predicate, else TRUE.
1372-
return sawFalse ? 'false' : sawClause ? 'clause' : 'true';
1394+
return reduceFilterVerdict(node, { ...SQL_FILTER_VERDICT_HOOKS, path });
13731395
}
13741396

13751397
/** [#5134] The verdict of ONE key of a filter node. */
13761398
function reduceFilterKey(key: string, value: unknown, path: string): FilterVerdict {
1377-
const here = path ? `${path}.${key}` : key;
1378-
1379-
if (key === '$and' || key === '$or') {
1380-
assertFilterNodeList(value, key, here);
1381-
let sawTrue = false;
1382-
let sawFalse = false;
1383-
let sawClause = false;
1384-
value.forEach((element, index) => {
1385-
const elementPath = `${here}[${index}]`;
1386-
assertFilterNode(element, elementPath);
1387-
const verdict = reduceFilterNode(element, elementPath);
1388-
if (verdict === 'true') sawTrue = true;
1389-
else if (verdict === 'false') sawFalse = true;
1390-
else sawClause = true;
1391-
});
1392-
// `$and: []` → no FALSE, no clause → TRUE (the AND identity).
1393-
if (key === '$and') return sawFalse ? 'false' : sawClause ? 'clause' : 'true';
1394-
// `$or: []` → no TRUE, no clause → FALSE (the OR identity). This is the
1395-
// half the old compile got backwards: it answered the whole table.
1396-
return sawTrue ? 'true' : sawClause ? 'clause' : 'false';
1397-
}
1399+
return reduceFilterKeyVerdict(key, value, { ...SQL_FILTER_VERDICT_HOOKS, path });
1400+
}
13981401

1399-
if (key === '$not') {
1400-
assertFilterNode(value, here);
1401-
const inner = reduceFilterNode(value, here);
1402-
// NOT TRUE ≡ FALSE — so `{ $not: {} }` matches nothing.
1403-
return inner === 'true' ? 'false' : inner === 'false' ? 'true' : 'clause';
1404-
}
1402+
/**
1403+
* [#5659] This driver's half of the reduction: the shape refusals, at the
1404+
* positions the shared walk visits them.
1405+
*
1406+
* `assertFilterNodeList` / `assertFilterNode` are wrapped in arrows rather than
1407+
* passed by reference because they are TypeScript assertion functions, whose
1408+
* narrowing is meaningless — and whose declaration requirements are a nuisance
1409+
* — through a property reference. Nothing else about the call changes.
1410+
*/
1411+
const SQL_FILTER_VERDICT_HOOKS: FilterVerdictHooks = {
1412+
assertNodeList: (value, key, path) => assertFilterNodeList(value, key, path),
1413+
assertNode: (value, path) => assertFilterNode(value, path),
1414+
classifyKey: (key, value, here) => classifyFilterKey(key, value, here),
1415+
};
14051416

1417+
/**
1418+
* [#5134] The verdict of ONE **non-combinator** key — and this driver's gate on
1419+
* everything a field constraint may not be.
1420+
*
1421+
* `here` is the already-joined path of the key, exactly as the reduction hands
1422+
* it over; the three combinator arms this used to open with are the shared
1423+
* walk's now, and the refusals below are unchanged from when they sat under
1424+
* them.
1425+
*/
1426+
function classifyFilterKey(key: string, value: unknown, here: string): FilterVerdict {
14061427
// [#5348] Everything still `$`-prefixed at this point is an UNDECLARED
1407-
// combinator — the three declared ones each returned above. Refused here and
1428+
// combinator — the shared walk resolved the three declared ones before this
1429+
// key ever reached the hook (#5659). Refused here and
14081430
// not in the emitter for exactly the reason the two lines below are here, and
14091431
// the reason #5327 gave for `{ field: {} }`: this walk is exhaustive and does
14101432
// not short-circuit, while the emitter is skipped wholesale by a boolean

0 commit comments

Comments
 (0)