Surfaced by the os-dev seat delivering objectui#6939 (PR #9558) as its fence-line-2 observation,
and re-measured in this seat before filing — ⛔ not adopted from the report.
objectui origin/main + installed @objectstack/spec@17.4.0, 2026-09-15T03:50Z.
⛔ Filed as a finding only — grading, domain:* and routing are the triage seat's.
The defect
A user picks an operator from the filter-builder dropdown; the resulting filter is refused by the
mirror that validates it. objectui#6939 fixed the three catalog fixtures that had the same
problem; this is the same defect on the path a real user takes, and no card owns it.
Measured
defaultOperators in packages/components/src/custom/filter-builder.tsx offers 22 ids.
FilterOperatorSchema in packages/types/src/zod/complex.zod.ts declares 14 members.
|
count |
ids |
| offered by the dropdown |
22 |
equals notEquals contains containsCaseInsensitive notContains isEmpty isNotEmpty greaterThan lessThan greaterOrEqual lessOrEqual before after between in notIn startsWith endsWith isNull isNotNull exists notExists |
| accepted as authored by the mirror |
3 |
equals contains in |
| refused as authored |
19 |
the rest |
Folding every offered id through the spec's own normalizeFilterOperator and testing the result
against the declared set:
|
count |
detail |
| fold onto a declared member |
14 |
e.g. lessThan → less_than — a pure spelling difference |
| do not fold |
8 |
splits again, and the split is the whole decision |
The 8 that do not fold are two different problems:
- 2 fold to a name the enum does not declare:
isEmpty → is_empty, isNotEmpty → is_not_empty.
The spec's alias table knows these names; this mirror's enum does not list them.
- 6 fold to themselves:
containsCaseInsensitive, before, after, between, exists,
notExists. ⭐ normalizeFilterOperator returns its input unchanged for an id it does not know,
so "folds to itself" is exactly "the protocol has no member for this".
Controls in the same run: normalizeFilterOperator('lessThan') → less_than (fires);
normalizeFilterOperator('qqzzAbsent') → qqzzAbsent (the identity behaviour that makes the
6-row reading meaningful); declared set size 14.
⚠️ My first instrument returned 0 dropdown ids — a regex that assumed single quotes where the
file uses double. ⛔ A zero from a broken parser is not a reading; it was thrown away and the block
was re-read by offset. The numbers above come from the second instrument, with both controls firing.
Why a spelling fix is not the whole answer
The tempting repair — fold at the storage boundary — closes 14 of the 19. It leaves 8 ids a
user can pick from a dropdown and cannot store, and those 8 are not one group:
isEmpty / isNotEmpty need the mirror to declare two names the protocol already publishes.
- The other six need the protocol to gain a member, or the dropdown to stop offering them.
⇒ under the maintainer's standing principle 「以 objectstack 协议为准…协议不正确的应该先修改协议」,
group 2 is a spec card first, ⛔ not an objectui change.
⚠️ filter-builder.tsx already records part of this: OPT_IN_OPERATORS holds
containsCaseInsensitive, exists and notExists, with an upstream blocker named for the last two
— so three of the six are known-unsupported by design, and the card that rules this should start
from that comment rather than re-deriving it.
Already pinned, ⛔ so this is a recorded decision rather than a silence
PR #9558 pins the refusal in both
examples/schema-catalog/test/filter-builder-operator-vocabulary-6939.test.tsx and
packages/types/src/__tests__/filter-builder-mirror-6939.test.ts. ⇒ whichever way this is ruled,
the pin names the key that moves and a repair cannot land silently.
Siblings — the same shape, one key over
⭐ Three cards now say the same thing about three different keys: objectui's UI vocabularies and
the protocol's declared sets are drifting apart in more than one place, and each was found
separately. Whoever grades these may want to ask whether the right artefact is a per-key ruling or
one gate that compares a component's offered vocabulary against the protocol's declared set.
Dedup: searched before filing; #8961 / #9554 / #6939 are the nearest and none is about the
filter-builder's operator dropdown.
os-decision-facets
维护者速读
筛选器的下拉菜单给作者 22 个操作符;objectui 自己那份校验用的枚举 FilterOperatorSchema 只声明 14 个,按作者原样接受的只有 3 个。
⭐ 但实测把方向反过来了:协议(@objectstack/spec 自己的 ViewFilterRuleSchema)按原样接受 22 个里的 19 个,并在解析时归一化成规范拼法。两边的差集只朝一个方向:协议有而 objectui 没有的 6 个(icontains is_empty is_not_empty before after between);objectui 有而协议没有的 —— 一个都没有。
⇒ 19 个拒绝里有 16 个,是 objectui 自己的镜子在拒绝协议本身接受的东西。 下拉菜单几乎是对的,⛔ 不该动它。
⚠️ 同时要修正卡面的口径:那句「用户挑一个操作符、结果被拒」在任何运行时路径上都测不出来。真正被拒的是作文面(CLI 的 check/validate,以及只在 __DEV__ 下 console.warn 的结构检查)。缺陷是真的——作者写不进 before/after/between/isEmpty/isNotEmpty,而协议五个全收——但它是作文面缺陷,不是终端用户点击的缺陷。
问:那份 14 个成员的窄枚举,是有意收窄还是落后于协议的陈旧子集**?**
| 选项 |
做什么 |
代价 |
| A 恢复保真 |
把 FilterOperatorSchema 加宽到协议的 20 个规范成员 |
关掉 6 个;14 个别名拼法仍按原样被拒(可辩护:作文元数据本就该写规范拼法) |
| B 采用协议自己的行为 |
镜子不再手工维护第二份枚举,改为从 spec 派生并像协议一样在解析时归一化 |
一次关掉 19 个里的全部,并且消掉整个漂移类;⚠️ 会改变已存作文文档归一化后的样子 ⇒ 需要一条往返 pin |
| C 裁定这 14 个是有意的 |
把「为什么窄」写在枚举旁边(今天那里除了「Filter Operator Enum」没有任何文档) |
那么下拉菜单里的 before/after/between/isEmpty/isNotEmpty 就是在提供「objectui 决定作者不许写」的操作符,而这五个今天在真实视图路径上存得下去 |
| D 什么都不改 |
只把 3 个真正的协议缺口(containsCaseInsensitive/icontains 别名行、exists、notExists)上报 objectstack |
16 个的镜子/协议分歧原样留着 |
四棱
- ① 项目长远合理性:B 把「手工维护第二份枚举」这件事本身去掉——漂移类消失,而不是这一例被修好;A 只是把今天的差补上,下次协议加成员还会再漂一次;C 把特例定型;D 留着分歧。按①:B。
- ② 实际业务拉动:⚠️ 实测后比卡面小:作文面而非运行时。但它是真实的——五个协议接受的操作符作者写不进去,而 objectui#6939 已经因为同一缺陷改过目录 fixture。
- ③ 防 AI 犯错:失败方向是响亮的(校验拒绝),这是好的;问题是拒得没道理——AI 从下拉里挑一个合法操作符,写出来的文档被自家 gate 拒,而协议会收。B 让 gate 与运行时、与协议三者一致(本仓的
viewFilterFold 与 ListView 存盘前都已经过 normalizeFilterOperator)。
- ④ 创业阶段不扩散:B 少一份永久义务(那份手工枚举);A 保留它;C 新增一条要维护的说明;D 把 3 个缺口推给上游。
Prior rulings read (THIS repo, objectui — AGENTS.md + docs/adr/**, which holds 0001/0034/0036/0053–0059): filter / between / incomplete → 73 hits across AGENTS.md and docs/adr/0001, 0053, 0054, 0055, 0057;⛔ 无一条把操作符词表当作已裁事项。
Prior rulings read (objectstack 的语料,共享协议自己的裁决 —— ⛔ 不是本仓编号): filter,between,incomplete,builder → 18 hits; ADR-0021 D2, ADR-0021 D3, ADR-0029 D9.3, ADR-0057 D10, ADR-0057 D12, ADR-0058 D1, ADR-0058 D3, ADR-0061 D1, ADR-0061 D6。
推荐:B,回退项 A。自检:「只看①选 B;②③④ 是否翻转:否——②把规模调小但不改方向,③④ 同向。」
置信缺口:① 协议侧的那些数(20 规范 / 36 别名 / 22 里接受 19)是 dev 的测量,本席未复算——共享检出没有 node_modules;本席自测的只有 objectui 那 14 个成员。② B 会改变已存文档归一化后的样子,影响面未量。③ ⚠️ 四个选项都要改 packages/types/src/zod/complex.zod.ts,而它被开着的 PR objectui#9895 持有 ⇒ 裁完也要等那个 PR,这是排期事实,⛔ 不是需要裁决的理由。
裁后执行:裁 B ⇒ 一张卡,镜子改为从 spec 派生 + 一条往返 pin;裁 A ⇒ 一张卡,加宽到 20 并 pin 住「本仓不得声明协议没有的成员」;裁 C ⇒ 一张卡,把理由写在枚举旁,并同笔决定下拉菜单是否要撤掉那五个;裁 D ⇒ 本仓关卡,3 个缺口在 objectstack 立卡。⚠️ 任一选项落地前先等 PR objectui#9895。
四棱块由 domain:ui#2 执行席在本卡因停手条件回到决策箱时补齐;⛔ 本席不改级别、不改路由,⛔ 也不代裁。dev 的完整测量见 5735810227,本席的复核见 5735889585。
Generated by Claude Code
Surfaced by the
os-devseat delivering objectui#6939 (PR #9558) as its fence-line-2 observation,and re-measured in this seat before filing — ⛔ not adopted from the report.
objectui
origin/main+ installed@objectstack/spec@17.4.0,2026-09-15T03:50Z.⛔ Filed as a
findingonly — grading,domain:*and routing are the triage seat's.The defect
A user picks an operator from the filter-builder dropdown; the resulting filter is refused by the
mirror that validates it. objectui#6939 fixed the three catalog fixtures that had the same
problem; this is the same defect on the path a real user takes, and no card owns it.
Measured
defaultOperatorsinpackages/components/src/custom/filter-builder.tsxoffers 22 ids.FilterOperatorSchemainpackages/types/src/zod/complex.zod.tsdeclares 14 members.equalsnotEqualscontainscontainsCaseInsensitivenotContainsisEmptyisNotEmptygreaterThanlessThangreaterOrEquallessOrEqualbeforeafterbetweeninnotInstartsWithendsWithisNullisNotNullexistsnotExistsequalscontainsinFolding every offered id through the spec's own
normalizeFilterOperatorand testing the resultagainst the declared set:
lessThan→less_than— a pure spelling differenceThe 8 that do not fold are two different problems:
isEmpty→is_empty,isNotEmpty→is_not_empty.The spec's alias table knows these names; this mirror's enum does not list them.
containsCaseInsensitive,before,after,between,exists,notExists. ⭐normalizeFilterOperatorreturns its input unchanged for an id it does not know,so "folds to itself" is exactly "the protocol has no member for this".
Controls in the same run:
normalizeFilterOperator('lessThan')→less_than(fires);normalizeFilterOperator('qqzzAbsent')→qqzzAbsent(the identity behaviour that makes the6-row reading meaningful); declared set size 14.
file uses double. ⛔ A zero from a broken parser is not a reading; it was thrown away and the block
was re-read by offset. The numbers above come from the second instrument, with both controls firing.
Why a spelling fix is not the whole answer
The tempting repair — fold at the storage boundary — closes 14 of the 19. It leaves 8 ids a
user can pick from a dropdown and cannot store, and those 8 are not one group:
isEmpty/isNotEmptyneed the mirror to declare two names the protocol already publishes.⇒ under the maintainer's standing principle 「以 objectstack 协议为准…协议不正确的应该先修改协议」,
group 2 is a spec card first, ⛔ not an objectui change.
filter-builder.tsxalready records part of this:OPT_IN_OPERATORSholdscontainsCaseInsensitive,existsandnotExists, with an upstream blocker named for the last two— so three of the six are known-unsupported by design, and the card that rules this should start
from that comment rather than re-deriving it.
Already pinned, ⛔ so this is a recorded decision rather than a silence
PR #9558 pins the refusal in both
examples/schema-catalog/test/filter-builder-operator-vocabulary-6939.test.tsxandpackages/types/src/__tests__/filter-builder-mirror-6939.test.ts. ⇒ whichever way this is ruled,the pin names the key that moves and a repair cannot land silently.
Siblings — the same shape, one key over
needs-user-decision) — the sort vocabulary: three$orderbyshapes, ofwhich the protocol declares one. Same family, same question, already in the decision inbox.
object-gridsends$orderbyas an object array the protocol does not declare.⭐ Three cards now say the same thing about three different keys: objectui's UI vocabularies and
the protocol's declared sets are drifting apart in more than one place, and each was found
separately. Whoever grades these may want to ask whether the right artefact is a per-key ruling or
one gate that compares a component's offered vocabulary against the protocol's declared set.
Dedup: searched before filing; #8961 / #9554 / #6939 are the nearest and none is about the
filter-builder's operator dropdown.
os-decision-facets
维护者速读
筛选器的下拉菜单给作者 22 个操作符;objectui 自己那份校验用的枚举
FilterOperatorSchema只声明 14 个,按作者原样接受的只有 3 个。⭐ 但实测把方向反过来了:协议(
@objectstack/spec自己的ViewFilterRuleSchema)按原样接受 22 个里的 19 个,并在解析时归一化成规范拼法。两边的差集只朝一个方向:协议有而 objectui 没有的 6 个(icontainsis_emptyis_not_emptybeforeafterbetween);objectui 有而协议没有的 —— 一个都没有。⇒ 19 个拒绝里有 16 个,是 objectui 自己的镜子在拒绝协议本身接受的东西。 下拉菜单几乎是对的,⛔ 不该动它。
__DEV__下console.warn的结构检查)。缺陷是真的——作者写不进before/after/between/isEmpty/isNotEmpty,而协议五个全收——但它是作文面缺陷,不是终端用户点击的缺陷。问:那份 14 个成员的窄枚举,是有意收窄还是落后于协议的陈旧子集**?**
FilterOperatorSchema加宽到协议的 20 个规范成员before/after/between/isEmpty/isNotEmpty就是在提供「objectui 决定作者不许写」的操作符,而这五个今天在真实视图路径上存得下去containsCaseInsensitive/icontains别名行、exists、notExists)上报 objectstack四棱
viewFilterFold与ListView存盘前都已经过normalizeFilterOperator)。Prior rulings read (THIS repo, objectui —
AGENTS.md+docs/adr/**, which holds 0001/0034/0036/0053–0059):filter/between/incomplete→ 73 hits acrossAGENTS.mdanddocs/adr/0001,0053,0054,0055,0057;⛔ 无一条把操作符词表当作已裁事项。Prior rulings read (objectstack 的语料,共享协议自己的裁决 —— ⛔ 不是本仓编号):
filter,between,incomplete,builder→ 18 hits; ADR-0021 D2, ADR-0021 D3, ADR-0029 D9.3, ADR-0057 D10, ADR-0057 D12, ADR-0058 D1, ADR-0058 D3, ADR-0061 D1, ADR-0061 D6。推荐:B,回退项 A。自检:「只看①选 B;②③④ 是否翻转:否——②把规模调小但不改方向,③④ 同向。」
置信缺口:① 协议侧的那些数(20 规范 / 36 别名 / 22 里接受 19)是 dev 的测量,本席未复算——共享检出没有⚠️ 四个选项都要改
node_modules;本席自测的只有 objectui 那 14 个成员。② B 会改变已存文档归一化后的样子,影响面未量。③packages/types/src/zod/complex.zod.ts,而它被开着的 PR objectui#9895 持有 ⇒ 裁完也要等那个 PR,这是排期事实,⛔ 不是需要裁决的理由。裁后执行:裁 B ⇒ 一张卡,镜子改为从 spec 派生 + 一条往返 pin;裁 A ⇒ 一张卡,加宽到 20 并 pin 住「本仓不得声明协议没有的成员」;裁 C ⇒ 一张卡,把理由写在枚举旁,并同笔决定下拉菜单是否要撤掉那五个;裁 D ⇒ 本仓关卡,3 个缺口在 objectstack 立卡。⚠️ 任一选项落地前先等 PR objectui#9895。
四棱块由
domain:ui#2执行席在本卡因停手条件回到决策箱时补齐;⛔ 本席不改级别、不改路由,⛔ 也不代裁。dev 的完整测量见5735810227,本席的复核见5735889585。Generated by Claude Code