Repository navigation
Commit 5c083e8
fix(ui-core): evaluatePlainCondition reads a bare-string predicate as CEL, as evaluateCondition does (#27773)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_
Fixes #27605
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no skill or doc names `evaluatePlainCondition` (repo
grep over `skills/`, `content/`, `docs/`: 0 hits); the CEL teaching for
bare predicates is #27587's
## 维护者速读
- 改了什么:`@object-ui/core` 对外导出的 `evaluatePlainCondition(condition,
record)` 把「裸字符串」条件按 CEL 求值,与 `evaluateCondition` 走同一个 CEL 引擎;只有 `${…}`
模板仍走旧 JS 求值器。
- 为什么改:spec 把裸字符串定义为 CEL 简写,服务端和控制台其余谓词路径(#27584 之后)都按 CEL 读;这个导出却还按旧 JS
语法读,调用它的宿主拿到的是另一种方言。
- 风险与回滚:只依赖 JS 语法的裸字符串(`===`、`.includes()`、`.length` 等)现在返回 false,原来能算出
true;仓内除自身测试外零调用方(实测见下)。回滚即 revert 本 PR。
-
先看:`ui/packages/core/src/evaluator/ExpressionEvaluator.ts`(`evaluatePlainCondition`
函数体)、`ui/packages/core/src/evaluator/__tests__/ExpressionEvaluator.test.ts`(`describe('evaluatePlainCondition')`)、`.changeset/27605-plain-condition-cel.md`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/core/src/evaluator/__tests__/ExpressionEvaluator.test.ts`,预期 88
个测试全绿。
- 需要你做的:无,席位落地。
## What changes
- `evaluatePlainCondition` (`@object-ui/core`, published through
`evaluator/index.ts`): a bare string now goes to
`ExpressionEvaluator.evaluateCondition(condition, { throwOnError: true
})`, which since #27584 evaluates a bare string on the canonical
`@objectstack/formula` CEL engine (`evaluateCelCondition`). The
evaluation context is unchanged (`{ ...record, data: record }`), so both
`status` and `data.status` resolve in CEL.
- The function's fault policy is kept: `throwOnError: true` turns a CEL
fault into a throw, which the existing `catch` answers with `false`; a
non-boolean result answers `false` (the engine seam already compares
`=== true`). A blank condition answers `false` as it did before, by an
explicit guard, because `evaluateCondition` answers a blank gate `true`
("no gate").
- A `${…}` template is unchanged: `evaluate(condition, { throwOnError:
true }) === true` on the legacy engine.
- Changeset `.changeset/27605-plain-condition-cel.md`:
`'@object-ui/core': minor`, both declaration lines, a BREAKING note that
teaches the CEL spelling with no FROM-TO list and no console-only escape
hatch, ADR-0087 disposition `not-required (no-migration-prescription)`
as in the precedent's changeset.
## Why
`@objectstack/spec` defines a bare string in a predicate slot as `{
dialect: 'cel', source }` (the string arm of `ExpressionInputSchema` in
`packages/spec/src/shared/expression.zod.ts`), the server reads it on
CEL, and #27584 moved every console predicate path to CEL. This export
was the one public reader left on the legacy JavaScript grammar
(contract review of #27584, should-fix 3; triage graded it align, not
retire).
## Tests
All in `ExpressionEvaluator.test.ts`,
`describe('evaluatePlainCondition')`:
- Rewritten pin: the compound case now uses the CEL spelling (`amount >
1000 && status == 'urgent'`), true and false legs.
- New: CEL-only constructs evaluate (`status in [...]`, `size(tags) >
0`), each with a true and a false leg.
- New: a JS-only spelling is refused with `false` where the JS grammar
would have answered `true` (`===`, `.includes(`), each beside its CEL
control (`==`, `.contains(`) answering `true`, and no console line.
- New: `data.amount > 1000` as a bare CEL predicate.
- New: parity, six source and record pairs, `evaluatePlainCondition`
equals `new ExpressionEvaluator({ ...record, data: record
}).evaluateCondition(source)`.
- New: a `${…}` template still reads `===` on the legacy engine.
- New: a blank and a whitespace-only condition answer `false`.
- Kept: field reference, numeric comparison, template `data.` reference,
invalid syntax, non-boolean result.
## Acceptance notes
- Narrowing, declared `Clause-②: no (narrowing)` / `Metadata-protocol:
narrowed` as the claim and the precedent (#27584) do: a bare string that
only the JS grammar reads now answers `false` from this export.
- Caller sweep at `d93d9be5` (`git grep evaluatePlainCondition` over the
whole tree, `ui/` and `packages/` included): 0 callers outside its own
file and its test file. Sibling repositories (HotCRM, cloud) were not
measured: NOT MEASURED, out of this card's scope.
- Blank condition: kept at `false` (unchanged), so it deliberately
differs from `evaluateCondition`'s diagnosed "no gate" `true`; this
export answers "does the condition hold", and a blank one never did.
Pinned.
- No console line on a fault, as before: the `throwOnError` route passes
`warn: false` to the engine seam, so the answer `false` is the whole
report. Pinned.
## Verification
<details><summary>Ablation (scripts/ablation-replace.mjs under the
verify lock, restored to HEAD after)</summary>
- Anchor `return evaluator.evaluateCondition(condition, { throwOnError:
true });` replaced with `return evaluator.evaluateExpression(condition)
=== true;` (the legacy route): 3 failed of 88, the CEL-only,
JS-only-refused and parity tests. Restore: blob equal to HEAD, `git diff
HEAD` empty.
</details>
<details><summary>Local runs</summary>
- `pnpm --dir ui exec vitest run packages/core/`: 218 files passed, 4141
tests passed, 27 skipped.
- `pnpm --filter @object-ui/core type-check` (`tsc --noEmit && tsc -p
tsconfig.test.json`): exit 0.
- eslint on both changed source files: 0 errors (warnings are the file's
existing `any`s).
- `node ui/scripts/check-new-cross-file-line-citations.mjs`: 0 new
citations.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 34 commands on this diff; each was
run with its exit code recorded, and all 34 exit 0 on `873fd637`.
`--ran` reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
- `pnpm --dir ui exec vitest run scripts/` first exited 1 with 2
failures, both from this container's build state and not from the diff:
`unit-registry-absence-collision` could not import 31 modules because
`@objectstack/client` had no `dist/` here, and `check-vi-mock-inherit`
timed out at 15 s on the shared box. After `pnpm --filter
'@objectstack/client...' build`, the full command re-ran: 169 files
passed, 2 skipped, 4902 tests passed, exit 0. That re-run is the
recorded line.
</details>
<details><summary>Dogfood battery</summary>
Run once on the final head `873fd637`, after building the closure (`pnpm
--filter '@objectstack/dogfood^...' build`, exit 0), under the verify
lock: `pnpm --filter @objectstack/dogfood test`, `VERDICT command-exit
0`; Test Files 354 passed, 3 skipped (357); Tests 2859 passed, 20
skipped (2879); 0 failures.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYKdX5M2WLtcLDH7qF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 561b764 commit 5c083e8
3 files changed
Lines changed: 92 additions & 8 deletions
File tree
- .changeset
- ui/packages/core/src/evaluator
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
710 | 710 | | |
711 | 711 | | |
712 | 712 | | |
713 | | - | |
| 713 | + | |
714 | 714 | | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
715 | 724 | | |
716 | 725 | | |
717 | 726 | | |
718 | 727 | | |
719 | 728 | | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
720 | 732 | | |
721 | 733 | | |
722 | | - | |
723 | | - | |
724 | | - | |
725 | | - | |
726 | | - | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
727 | 738 | | |
728 | 739 | | |
729 | 740 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
178 | | - | |
179 | | - | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
180 | 224 | | |
181 | 225 | | |
182 | 226 | | |
183 | 227 | | |
184 | 228 | | |
185 | 229 | | |
186 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
187 | 237 | | |
188 | 238 | | |
189 | 239 | | |
190 | 240 | | |
191 | 241 | | |
192 | 242 | | |
193 | 243 | | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
194 | 249 | | |
195 | 250 | | |
196 | 251 | | |
| |||
0 commit comments