范围外发现,出自 #4812 / PR #6130 的对拍测试(一条 fixture 断言 kind === 'parse' 意外翻红)。按 Prime Directive #10 记录,不指派。
事实(origin/main bc67c28e2 基线,实测)
packages/formula/src/cel-engine.ts 的 classifyError 靠错误文案关键词分类:
function classifyError(err: unknown): EvalResult<never> {
const message = err instanceof Error ? err.message : String(err);
let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';
if (/Exceeded max/i.test(message)) kind = 'bounds';
else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';
else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';
return { ok: false, error: { kind, message } };
}
而 cel-js 8.0.0 对语法错并不只有一种措辞。实测五条:
| 源码 |
cel-js 首行错误 |
命中分支 |
得到的 kind |
record.budget > |
Unexpected token: EOF |
unexpected |
parse ✅ |
record.a $$ 1 |
Unexpected character: $ |
unexpected |
parse ✅ |
record.a ?? 3 |
Unexpected token: QUESTION |
unexpected |
parse ✅ |
((record.a) |
Expected RPAREN, got EOF |
无 |
runtime ❌ |
[1,2 |
Expected RBRACKET, got EOF |
无 |
runtime ❌ |
Expected RPAREN, got EOF 里既没有 parse / unexpected / syntax,也没有 type / unknown variable / undeclared,于是落到默认值 runtime。括号、方括号、花括号不配对是最常见的手写语法错之一,恰好整类都落在这个洞里。
为什么这条是用户可见的(不是纯内部字段)
kind 不止用于内部分支,它被原样拼进作者读到的文案和 API 响应体:
packages/objectql/src/validation/rule-validator.ts:1262
`Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected (#4649)`
- 同文件
:1417,when-predicate 版本同构
packages/objectql/src/cel-fault.ts:75
return `${error.kind}: ${first || 'unknown error'}`;
packages/rest/src/rest-server.ts:571
...(error?.kind ? { reason: error.kind } : {}) —— 进 HTTP 错误响应的 reason 字段
所以一条少写了一个右括号的校验规则,作者拿到的是 (runtime: Expected RPAREN, got EOF),REST 消费方拿到的是 reason: "runtime"。message 本身是对的,分类是错的 —— 而分类正是用来告诉作者"这该去哪儿修"的:runtime 指向数据/求值期,parse 指向"你的表达式写错了"。ADR-0032 D1d 要求消息面向自纠,这一格与之相悖。
影响面
classifyError 同时服务 celEngine.compile 与 celEngine.evaluate,所以 build 期(os build / os validate / os lint)与运行期(写入拒绝、REST)两侧都受影响。
未量化 / 未主张
- 没有统计真实 metadata 里不配对分隔符的出现频率。
- 没有排查 cel-js 是否还有别的语法错措辞同样漏网(上表只穷举了我构造的五条);这个洞的成因是"靠文案关键词分类"这个做法本身,补关键词只是补当前已知的洞。
建议方向(供 triage,不代裁)
关键词匹配是脆的:cel-js 换一次措辞就再破一次。可考虑改读 cel-js 抛出的错误对象上的结构化信息(ErrorOptions 带 code,见 lib/index.d.ts:82-86),把分类建立在契约上而不是文案上;若结构化信息不足以区分,则至少把关键词表补全并加一组 fixture 钉住每一类措辞 —— 后者是止血,前者是根治。
关联
#4812 / PR #6130(发现出处;该 PR 的对拍测试里刻意没有把这条错误分类断言进去,并在注释里写明了原因 —— 断言它等于把这个 bug 钉成契约)。ADR-0032 D1d(自纠消息)。
范围外发现,出自 #4812 / PR #6130 的对拍测试(一条 fixture 断言
kind === 'parse'意外翻红)。按 Prime Directive #10 记录,不指派。事实(
origin/mainbc67c28e2基线,实测)packages/formula/src/cel-engine.ts的classifyError靠错误文案关键词分类:而 cel-js 8.0.0 对语法错并不只有一种措辞。实测五条:
record.budget >Unexpected token: EOFunexpectedparse✅record.a $$ 1Unexpected character: $unexpectedparse✅record.a ?? 3Unexpected token: QUESTIONunexpectedparse✅((record.a)Expected RPAREN, got EOFruntime❌[1,2Expected RBRACKET, got EOFruntime❌Expected RPAREN, got EOF里既没有parse/unexpected/syntax,也没有type/unknown variable/undeclared,于是落到默认值runtime。括号、方括号、花括号不配对是最常见的手写语法错之一,恰好整类都落在这个洞里。为什么这条是用户可见的(不是纯内部字段)
kind不止用于内部分支,它被原样拼进作者读到的文案和 API 响应体:packages/objectql/src/validation/rule-validator.ts:1262`Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected (#4649)`:1417,when-predicate 版本同构packages/objectql/src/cel-fault.ts:75return `${error.kind}: ${first || 'unknown error'}`;packages/rest/src/rest-server.ts:571...(error?.kind ? { reason: error.kind } : {})—— 进 HTTP 错误响应的reason字段所以一条少写了一个右括号的校验规则,作者拿到的是
(runtime: Expected RPAREN, got EOF),REST 消费方拿到的是reason: "runtime"。message 本身是对的,分类是错的 —— 而分类正是用来告诉作者"这该去哪儿修"的:runtime指向数据/求值期,parse指向"你的表达式写错了"。ADR-0032 D1d 要求消息面向自纠,这一格与之相悖。影响面
classifyError同时服务celEngine.compile与celEngine.evaluate,所以 build 期(os build/os validate/os lint)与运行期(写入拒绝、REST)两侧都受影响。未量化 / 未主张
建议方向(供 triage,不代裁)
关键词匹配是脆的:cel-js 换一次措辞就再破一次。可考虑改读 cel-js 抛出的错误对象上的结构化信息(
ErrorOptions带code,见lib/index.d.ts:82-86),把分类建立在契约上而不是文案上;若结构化信息不足以区分,则至少把关键词表补全并加一组 fixture 钉住每一类措辞 —— 后者是止血,前者是根治。关联
#4812 / PR #6130(发现出处;该 PR 的对拍测试里刻意没有把这条错误分类断言进去,并在注释里写明了原因 —— 断言它等于把这个 bug 钉成契约)。ADR-0032 D1d(自纠消息)。