fix(core)!: retire the four lowercase $-dialect aliases from convertFiltersToAST (objectui#8568) - #8988
Conversation
`convertOperatorToAST`'s `operatorMap` carried `$notin`, `$notcontains`, `$startswith` and `$endswith` "for tolerance" while `ValueDataSource` refused the same four by design (objectui#8447: alias arms there "would fossilise a second dialect"). One authored filter therefore had two fates depending on which data source was behind the view. The maintainer ruled the tolerant side out: the `$` dialect follows `@objectstack/spec`'s spellings, with no deprecation window, and the documentation follows the implementation. The four are refused BY NAME and the refusal names the canonical spelling, so the repair is a key rename rather than a diff of two operator lists. A generic unknown operator still gets the generic message; that asymmetry is pinned. `packages/data-objectstack/README.md`'s tables and PR #8565's reconciliation pin follow the code: the pin reddened on exactly the four rows that had to change, which is the signal it was built to give. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
…568) Ablating the named arm left `names the canonical spelling` GREEN: the generic unknown-operator message's own "Supported operators:" list already spells `$nin`, `$notContains`, `$startsWith` and `$endsWith`, so `toContain(canonical)` was a case that passed while witnessing nothing. The prescription phrase is what only the named arm can produce, so that is what is asserted now, with the measurement recorded beside it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
…ire-lowercase-aliases
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…empty (objectui#8568) PR #8977's live-tree case asserted `aliases.length > 0`, `rows.length > 0` and `selfCarved > 0` unconditionally. Retiring the four aliases makes all three false at once, by construction: the census derives its subject set as operatorMap-minus-spec, and that set is now empty. The census script already models the empty tree and a synthetic unit case already pins it; only the live-tree case had the pre-ruling shape baked in. The blindness guard those three carried is real, so it is re-expressed rather than deleted. On an empty tree the instrument's reach is proved by the `authored-reach` control, which counts canonical `$`-operator payloads in non-test authored files and cannot be satisfied by a scanner that never opened the authored corpus; the empty set is then held to being DERIVED rather than the residue of a failed read on either side. The pre-retirement branch is kept intact, so the case still holds on any tree that carries an alias again. The subset check is recomputed in the test rather than by calling `deriveAliases` again: measured under ablation, a stubbed `deriveAliases` returning `[]` satisfies its own output, so reusing it would have made this branch pass while witnessing nothing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
ACCEPT + 席内 Clause-② 复核 PASS — 派发席
|
| 键数 | 小写别名 | |
|---|---|---|
origin/main |
17 | $notin · $notcontains · $startswith · $endswith 全在 |
| 本分支 | 13 | 一个不剩,13 个全部是规范拼写 |
四个退役别名移入 RETIRED_OPERATOR_ALIASES,各自映射到规范写法:
const RETIRED_OPERATOR_ALIASES: Record<string, string> = {
'$notin': '$nin',
'$notcontains': '$notContains',
'$startswith': '$startsWith',
'$endswith': '$endsWith',
};⇒ 这正是裁决要的形状:按名拒绝,并点名规范拼写。⭐ 而且在「无宽限窗口」的前提下,那条 prescription 就是全部的迁移援助 —— 一个只说「unknown operator」的拒绝会把这次退役的成本整个推给作者。ValueDataSource 未动,objectui#8447 的方向维持。
⭐ 两处「绿着却什么也没见证」被 dev 自己抓出来,方向按实测报告
这是本轮最值得记的一条,因为两处都是通过而不见证的形状:
- 消融 leg B(删掉具名拒绝臂)最初只红 4 个。那条断言「拒绝信息里出现规范拼写」的用例保持绿色 —— 因为通用 unknown-operator 信息自带的
Supported operators:列表里本来就印着$nin/$notContains/$startsWith/$endsWith,于是toContain(canonical)恒真。改钉 prescription 短语(只有具名臂能产出)后,同一次消融从 4 红变 8 红。 - 消融 leg C 没能让它瞄准的普查守卫变红:那条用例先调用
deriveAliases去取自己的被测集合,而一个返回空数组的桩满足它自己的输出。改为独立重算子集关系后才有区分力。
⇒
pin 的三步测量
PR #8565 的对账 pin,按顺序实测:改代码前绿(exit 0,10 passed)→ 改代码后红(exit 1,只点名 README 第 143/146/147/148 行,别无其他)→ 改 README 后绿(exit 0,10 passed)。⇒ 该 pin 变红是设计信号,⛔ 不是缺陷,且它精确指出了要改的行。
changeset
minor + 正文写明破坏。本仓禁 major,这是规定形态。
⛔ needs:contract-review 本席不清,理由在案
dev 把该标签同笔挂在卡与 PR 两个载体上并回读了两侧。scripts/pm/check-half-states.mjs 的 h31ContractReviewCarrierSplit 只在劈叉时报红,两侧一致(都挂或都不挂)是它的静默态。
⇒ 只清 PR 一侧会制造劈叉;两侧同清则是一次「清闸动作」,而本席复核的正是本车道自己的活,自清等于自我背书。⛔ 本席两者都不做,标签保持挂起,清闸留给维护者或总监席。
⇒ 放行:翻 ready、挂 SQUASH,按队列 ref 阳性命中确认,再按内容带亮/死对照核落地。
Generated by Claude Code
Fixes #8568
Clause-②: yes— this lane's in-seat default-tier review plus the gate table below is the review of record.The ruling this implements
The director seat ruled option 1 on this card (comment 5617467534), from the maintainer verbatim: 「我们的项目以objectstack 协议为准,文档应该以实际实现为准。」 The
$dialect follows@objectstack/spec's canonical spellings; the four lowercase aliases retire with no deprecation window (2026-08-27 「不渐进」);ValueDataSourceis not touched (objectui#8447's direction stands); README rows and this repo's reconciliation pin follow the implementation.What changed
convertOperatorToAST'soperatorMapno longer carries$notin,$notcontains,$startswith,$endswith. All four are now refused by name, and the refusal prescribes the canonical spelling:A named arm rather than the generic unknown-operator paragraph, because with no deprecation window that message is the whole migration aid: "unknown operator, here is the supported list" makes an author diff two lists to find which entry they meant, while "write
$startsWith" is the repair.The prescriptions are derived, not restated.
RETIRED_OPERATOR_ALIASESis read out of the source by the new pin and both directions are executed against@objectstack/spec: every KEY must be absent fromFILTER_OPERATORS, every VALUE present in it and lowerable here. A fifth alias added later, or a prescription naming a spelling the spec does not declare, reddens.There is also a spec-derived invariant that outlives this card: for every camelCase member of
FILTER_OPERATORS, its all-lowercase spelling must not be accepted. That is commandment #0.1 itself, read off the spec rather than off the four names this card happened to retire.The pin reddened exactly as designed
PR #8565's reconciliation pin reads
operatorMapout offilter-converter.ts's source. After the code change and before the README edit, it named the four rows that had to move and nothing else:Before the code change: exit 0, 10 passed. After the code change, before the README: exit 1, 1 failed. After the README: exit 0, 10 passed. The mechanism is worth naming, because it is not the one you would guess: the "row for every key of the operator map" case gets weaker when keys are removed. What reddened is the supported table's first column naming a spelling the code now refuses. The four alias spellings moved into the Refused at lowering time table, where the same pin now executes their refusal on every run.
The second pin the retirement moved: PR #8977's census
PR #8977 merged into
mainwhile this branch was in flight, and merging it in turned one of its cases red — a genuine cross-PR interaction on the same card, not a conflict git could see. Its live-tree case assertedaliases.length > 0,rows.length > 0andselfCarved > 0unconditionally. The census derives its subject set asoperatorMapminus spec, so retiring the four makes all three false at once, by construction. The census SCRIPT already models the empty tree ("no aliases derived, so there is no twin to light") and a synthetic unit case already pins it; only the live-tree case carried the pre-ruling shape.Repaired rather than deleted, because the blindness guard those three assertions carried is real. On an empty tree the instrument's reach is proved by the
authored-reachcontrol — it counts CANONICAL$-operator payloads in non-test authored files, so it cannot be satisfied by a scanner that never opened the authored corpus — and the empty set is then held to being DERIVED rather than the residue of a failed read on either side. The pre-retirement branch is kept intact, so the case still holds on any tree that carries an alias again, including a revert.The census now reads, on this head, exactly what the ruling predicted:
Ablation — three legs, each on the committed tree
Every mutation was proven on disk before any result was read (anchor counts plus a
git hash-objectdiffering from the HEAD blob), restored withgit checkout HEAD -- ABSOLUTE_PATH, and the restore proven by an EMPTYgit diff HEADplus blob-hash equality, undertrap ... EXIT INT TERMon absolute paths. No build leg applies: the root vitest config aliases@object-ui/coretopackages/core/src, and both pins import their subject by relative source path, so there is nodistindirection to stale.operatorMapderiveAliasesstubbed to return[]git diff HEADempty, blob equals the HEAD blobLeg A reddens across three files, including PR #8565's pin, so the retirement is enforced from more than one direction. Leg B leaves the envelope assertions green and moves only the naming cases — which is the point: a plain refusal is not what this card bought.
Two defects the ablation found in the instruments, with the direction reported as observed
Leg B first ran 4 red, not 8. The case asserting the refusal
names the canonical spellingstayed GREEN with the named arm deleted, because the generic unknown-operator message's ownSupported operators:list already spells$nin,$notContains,$startsWithand$endsWith.toContain(canonical)was a case that passed while witnessing nothing. The assertion now pins the PRESCRIPTION phrase, which only the named arm can produce; re-running the same ablation moved leg B from 4 red to 8 red.Leg C did not redden the census guard it was aimed at. The repaired live-tree case first re-used
deriveAliasesto check its own subject set, and a stub returning[]satisfies its own output — so that branch would have passed while witnessing nothing too. The subset question is now recomputed in the test (every accepted key must be a canonical one), which is what makes an alias re-added to the map red there even when the census's own derivation is what broke. Leg C's 2 reds are the unit-levelderiveAliasescases, i.e. the file as a whole always caught that mutation; the point is that the new branch now carries its own share.Census: the in-repo repair step is a measured no-op
The ruling's execution step 1 — census the in-repo authored corpus — landed as PR #8977: 24 alias occurrences tree-wide, 7 in operator-key position, ZERO in the authored corpus. Every tree-wide hit was the map that defined the aliases or something pointing at it. So step 2, "in-repo hits get corrected to canonical spellings first", had nothing to correct, and this PR is the map plus its ledger.
That zero is consumer-local, not seam-wide (objectui#6839). Stored view / list / sharing-rule criteria, producer-side metadata and published consumers of
@object-ui/coreare all invisible from here. One thing about that population is measurable and it lowers the marginal cost:kvToCondition— the reader that loads stored$-criteria back into the filter builder — has arms for 15 spellings and none of these four, so a stored lowercase criterion already failed to round-trip and dropped the admin into the raw-JSON editor. Re-verified here against the current source, not carried forward from the census.Gates — all on the final head, after
origin/mainwas merged inorigin/mainadvanced from6109e5daatoca67d42f0mid-task (PR #8977 and PR #8967 landed) and is now an ancestor of this head. The merge was clean — no conflicts — and every gate below was run after it. Exit codes captured to disk before being read, never through a pipe.turbo run type-check lint(core, data-objectstack)type-check:scriptsturbo run lint --concurrency=2— the whole graph, as CI runs itvitest run packages/core/ packages/data-objectstack/ scripts/vitest run packages/fields/ packages/plugin-grid/(the otherconvertFiltersToASTreaders)turbo run build --concurrency=2check:control-bytesgrep -naPover every file in the diff, no hitscheck:changeset-presencecheck-governed-queue-guard.mjs --test(8 paths)check:doc-snippetscheck:doc-typescheck:doc-examplescheck:new-line-citationscheck:readme-exportscheck:doc-fences·check:spec-symbols·check:unreferenced-sources·check:vi-mock-*·check:comment-mask-corpuscensus:dollar-dialect-aliasesChangeset
minorwith the break spelled out in the body: this repo forbidsmajor, and this is breaking for anyone spelling those four in lowercase. The repair is a key rename; the lowered node is unchanged and no result set moves for a filter that was already spelled canonically.Acceptance notes — out of scope, noted, not filed
check:doc-examplesledger is NOT keyed by line number. The dispatch brief (and its own correction) both state thatUNGATED_EXAMPLESis keyedpath:LINE symbol, so any line added above a ledgered@examplesilently invalidates that row and CI will not say so. Measured false: the key ispath symbol #ordinal(scripts/check-doc-example-types.mjs:172, and every row from:500), so line displacement does not move a row — only a renamed symbol or a changed@exampleordinal does. This diff pushedfilter-converter.ts's@exampleblocks down by roughly 34 lines and the gate stayed at 89/89. It was moot here anyway: nofilter-converter.tsrow exists in that ledger. Successor: the dispatching seat, which will otherwise keep repeating the arithmetic warning.$ncontainshas no prescription.packages/core/src/adapters/README.md:154groups it with the four retired aliases as a "non-canonical spelling", andkvToConditionstill reads it back — deliberately, it is the spellingFilterConditionFieldused to emit.convertFiltersToASThas never accepted it, so it is outside this retirement: it falls to the generic unknown-operator arm, loudly and correctly. Recorded as the boundary of this change rather than widened into it. Successor: whoever revisitsFilterConditionField's legacy read arms.$-dialect lowercase aliases before objectui#8568 is ruled #8977 and feat(types)!: retirecancelLabel/confirmLabel/confirmVariantfromAlertDialogSchema#8967 merged and four new ones opened. No open PR touched any file in this diff at either reading. PR feat(core):isEmptyValue— the shared emptiness floor, and the surfaces that state their answer against it #8981 is the nearest —packages/core/src/utils/emptiness.tsandpackages/core/src/index.ts, neither of which this PR reads or writes. Treat that as a timestamp, not a fact.Serial adjacency and the open-PR set were re-derived here against currently-open PRs via repo-scoped REST, not carried from any snapshot.
Session, written as prose and as a code span so it survives a body edit:
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB