Repository navigation
Commit c946cb9
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XKy1Tn6rK9yK1erywxCx5Bk)_
Fixes #27904
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:公式引擎拒绝一条写错的 CEL
表达式时,除了原来那句英文,现在还带上引擎自己的错误类别(比如「缺一个右括号」「多了一个不认识的字符」)和出错的字符位置。
- **为什么改**:编辑器之前只能显示一句笼统的「语法错误」加原始报错,没法用用户的语言说「第 17
个字符处缺一个右括号」,也没法把光标指到出错处。
- **风险与回滚**:只新增两个可选字段,原有英文句子一字不变;回滚即还原这一个
commit。引擎读的是平台改写过的表达式时不给位置(宁缺不错)。
- **先看**:`packages/formula/src/cel-engine.ts`(`compileCel` 与
`celFaultRange`
的「只在读的是作者原文时给位置」判断)、`packages/formula/src/validate.ts`(`invalid-cel`
分支)、`packages/formula/src/expression-refusal.ts`(新字段的契约说明)。
- **两分钟验证**:`pnpm --filter @objectstack/formula exec vitest run
src/cel-fault-kind-position.test.ts`,预期 15 个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
- `invalid-cel` params (`ExpressionRefusalParams`) gain two **optional**
members beside `role` and `detail`:
- `celCode`: cel-js's own structured code for the fault, read off the
error object (`ParseError#code` / `TypeError#code`), never out of its
prose. Parser codes for a syntax fault (`unexpected_token`,
`expected_token`, `unexpected_character`, `unterminated_string`,
`invalid_exponent`, …), checker codes for a type fault
(`no_such_overload`, …).
- `pos`: `{ start, end }`, the range cel-js reports, as UTF-16 offsets
into `source` with `end` exclusive. A missing `)` reads `start === end
=== source.length`.
- `EvalError.pos`, declared and never filled, is now filled by
`celEngine.compile` and `celEngine.evaluate` for a cel-js parse, check
or evaluation fault that carries a range.
- Neither member is ever guessed. Both are absent when the engine
reported none (`expression_must_be_string`, a native throw such as
`matches()`'s regex `SyntaxError`). `pos` is also absent when cel-js
read a source the platform rewrote first (the #3306 null-guard ternary
lowering, the #25267 indexed `has()` lowering, the evaluate-time
temporal-equality rewrite): a range into reserialized text would point
at characters the author never wrote.
- The code reaches `validateExpression` through a package-internal
`compileCel(source)`, which `celEngine.compile` now delegates to. It is
not re-exported from the package entry, and the code is kept off
`EvalError` on purpose: `EvalError` reaches logs and error bodies, where
a second `code` beside an ADR-0112 `error.code` would read as one.
- `expression-refusal.ts`'s contract gains one clause: a LOCATOR param
is a structured reading of the fault `detail` renders, the one exception
to "params are the interpolated values".
The refusal's `message` is byte-identical, and every other code
(`cel-too-large`, `cel-method-call`, `cel-unknown-function`,
`cel-template-brace`, …) keeps its params unchanged. Adding `pos` to
those four from the same read would be one line each; it is left out
because no reader asks for it yet.
## Tests
- New `packages/formula/src/cel-fault-kind-position.test.ts`, 15 cases:
- five parse-fault kinds through `validateExpression`: unexpected end
(`record.rating >=`), missing `)` (`(record.a > 1 && record.b < 2`),
unexpected character mid-source (`record.a # 1`), stray `)` (`record.a >
1 )`), unterminated string (`record.a == 'abc`). Each pins `celCode`,
`pos`, the text `source.slice(start, end)` points at (`#`, `)`, `'abc`,
or empty at `source.length`), and the message byte for byte as our words
around the engine's pass-through `detail`;
- a type fault (`1 + 'a'` → `no_such_overload`, `{0, 7}`);
- absence over a rewritten source, with a positive control that the
source really is rewritten;
- `EvalError.pos` on `compile` (parse, missing `)`, check, rewritten →
absent) and on `evaluate` (parse, runtime `No such key` pinned to
`missing`, rewritten → absent, native throw → absent).
- **Red on base** `9da2f4b0`: 11 failed, 2 passed (the two absence
controls hold on base by construction).
- `expression-refusal-codes.test.ts`: the two `invalid-cel` pins now
include the new members (that table pins params exactly), and the sweep
judges the locator params by what they are (`invalid-cel` only, a
snake_case cel-js code, a range inside the source) instead of by message
containment.
<details><summary>Ablation (4 legs, each red, each restored to the HEAD
blob with an empty diff against HEAD)</summary>
All legs through `node scripts/ablation-replace.mjs` (anchor hit 1 → 0
on disk); formula tests import `./validate` / `./cel-engine` from
`src/`, so no rebuild applies.
| Leg | Mutation | Result |
|---|---|---|
| 1 | `invalid-cel` params back to `{ role: celRole, detail }` | 8
failed (7 new + `invalid-cel` pin) |
| 2 | `classifyError` never fills `pos` | 9 failed |
| 3 | compile check path drops the `parsed === source` guard | 2 failed
(both rewritten-source absence cases) |
| 4 | evaluate drops the `readSource === source` guard | 1 failed
(evaluate rewritten-source case) |
</details>
<details><summary>Suites on head ddf2020</summary>
- `@objectstack/formula` full suite: Test Files 61 passed, Tests 1578
passed; `pnpm typecheck` (incl. `check:test-typecheck`) exit 0; `pnpm
build` exit 0.
- `ExpressionRefusalParams` importers outside `packages/formula`: none
(`git grep`, only a CHANGELOG line).
- `EvalError` importer `@objectstack/objectql` (downstream): Test Files
513 passed, Tests 9903 passed.
- The ui reader that acts on `invalid-cel` (`celAuthoring.ts` key map)
and every ui test naming `celAuthoring` / `invalidCel`: Test Files 51
passed, Tests 470 passed, against the rebuilt formula `dist/`.
- `eslint --no-inline-config packages/formula`: exit 0.
</details>
## Acceptance notes
- **Changeset level.** The dispatch order asked for `minor`; the
changeset is `patch`. AGENTS.md ties the level to the
`Metadata-protocol` kind alone, and `check-changeset-no-major.mjs`
refuses `none` beside a `minor`. Nothing a metadata author writes
changes here, so `none` + `patch`.
- **The ui's plain-word mapping and caret** are the reader that acts on
these members (Studio/Setup CEL editors, `celAuthoring.ts`); that is
ui-lane work and out of this diff.
- cel-js names which token it expected (`Expected RPAREN, got EOF`) only
in prose; the structured `celCode` is `expected_token` without the
token. An editor that wants to say "a closing bracket" specifically has
`pos` (end of source) and `detail`; a structured expected token would
need cel-js upstream.
## Verification
Gate list from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on head `ddf2020b` (60 commands), each run
with its exit code captured before any pipe, then reconciled with
`--ran`: **60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN**, every one exit
0.
Three families first answered exit 3 (prerequisite not met, nothing
measured) and were re-run after their prerequisite:
`check-plugin-teardown-shape.mjs --self-test` (fixture commit fetched),
`check:lean-entry-closure` and `check:dual-build-cjs-loads` (dists
built). All three exit 0 on the re-run. `pnpm lint` scoped to
`packages/formula`: exit 0. The repo-wide farm and `test:repo` belong to
CI; read the PR's check-runs.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nkzMdFu8KxmmxsudB)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5a400a commit c946cb9
7 files changed
Lines changed: 408 additions & 41 deletions
File tree
- .changeset
- packages/formula/src
| 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 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
1969 | 1969 | | |
1970 | 1970 | | |
1971 | 1971 | | |
1972 | | - | |
| 1972 | + | |
1973 | 1973 | | |
1974 | 1974 | | |
1975 | | - | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
| 1992 | + | |
| 1993 | + | |
| 1994 | + | |
| 1995 | + | |
| 1996 | + | |
| 1997 | + | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
| 2004 | + | |
| 2005 | + | |
| 2006 | + | |
| 2007 | + | |
| 2008 | + | |
| 2009 | + | |
| 2010 | + | |
| 2011 | + | |
| 2012 | + | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
| 2023 | + | |
| 2024 | + | |
| 2025 | + | |
| 2026 | + | |
| 2027 | + | |
| 2028 | + | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
| 2037 | + | |
| 2038 | + | |
| 2039 | + | |
| 2040 | + | |
| 2041 | + | |
| 2042 | + | |
| 2043 | + | |
| 2044 | + | |
| 2045 | + | |
| 2046 | + | |
| 2047 | + | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
1976 | 2071 | | |
1977 | 2072 | | |
1978 | 2073 | | |
1979 | 2074 | | |
1980 | 2075 | | |
1981 | 2076 | | |
1982 | | - | |
1983 | | - | |
1984 | | - | |
1985 | | - | |
1986 | | - | |
1987 | | - | |
1988 | | - | |
1989 | | - | |
1990 | | - | |
1991 | | - | |
1992 | | - | |
1993 | | - | |
1994 | | - | |
1995 | | - | |
1996 | | - | |
1997 | | - | |
1998 | | - | |
1999 | | - | |
2000 | | - | |
2001 | | - | |
2002 | | - | |
2003 | | - | |
2004 | | - | |
2005 | | - | |
2006 | | - | |
2007 | | - | |
2008 | | - | |
2009 | | - | |
| 2077 | + | |
2010 | 2078 | | |
2011 | 2079 | | |
2012 | 2080 | | |
| |||
2030 | 2098 | | |
2031 | 2099 | | |
2032 | 2100 | | |
| 2101 | + | |
| 2102 | + | |
| 2103 | + | |
2033 | 2104 | | |
2034 | 2105 | | |
2035 | 2106 | | |
| |||
2064 | 2135 | | |
2065 | 2136 | | |
2066 | 2137 | | |
| 2138 | + | |
2067 | 2139 | | |
2068 | 2140 | | |
2069 | 2141 | | |
| |||
2096 | 2168 | | |
2097 | 2169 | | |
2098 | 2170 | | |
2099 | | - | |
| 2171 | + | |
2100 | 2172 | | |
2101 | 2173 | | |
2102 | 2174 | | |
0 commit comments