Skip to content

feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 (#6253) - #6472

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-6253-view-predicate-cel-syntax
Aug 8, 2026
Merged

feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 (#6253)#6472
hotlong merged 3 commits into
mainfrom
claude/issue-6253-view-predicate-cel-syntax

Conversation

@hotlong

@hotlong hotlong commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6253

view/page 的可见性谓词此前在构建期无人判语法。本 PR 新增 error 级规则
visibility-predicate-syntax,落在 PR #6248 已经注释过的那个 parseCelToAst 返回
null 的分支上。

裁定与路线

按维护者 2026-08-07 的裁定执行:判 blocking error,与 ADR-0032 下其它谓词面
(validation rule / flow / action)同级;不设 warning 档,不为本面写豁免
实施方在这一点上没有裁量,本 PR 也不提替代方案、不加软化开关。

为什么这一面此前没人报

validate-expressions.ts(ADR-0032)对它遍历到的每条谓词都跑 validateExpression,
语法错报 blocking error —— 但它的遍历面是 objects / flows / actions / sharingRules /
hooks,从不走 viewspages。走这一面的三条规则都明确不判语法,理由是
「不发明第二个语法判定」。那条政策在它自己的调用点上成立(validateExpression 就在
同一批调用点上跑),在 view/page 面上不成立:那里没有第二个判定,沉默就是没人报

后果是 #5149 同型的 fail-open:谓词求值失败 → evalFieldPredicate 返回 fallback →
可见性 fallback 是 true → 元素无条件渲染,与「没写谓词」在屏幕上一模一样。

判定仍然不是本包给的(这正是旧政策要保护的东西)

旧政策的真实内容是「对『什么能解析』只有一个答案」,这一点完整保留:判定取
parseCelToAst(规范前端,带 #3306 改写与 DEFAULT_LIMITS,#4812),本规则
不自建 Environment、不手写 tokenizer。新增的是对既有判定的上报,外加原始
报错缺的自纠措辞 —— cel-js 只说 Unexpected character: = 并画一个 caret,既没点名
作者写的运算符,也没给出 CEL 的写法。

明确不走 validateExpression / celEngine.compile

尽管那才是 ADR-0032 的入口。compile() 是 parse + 类型检查,差别不是理论上的 ——
本 PR 实测它会以 no such overload: type == string 拒掉 type == 'grid',而那正是
本文件已钉测试的既有盲点(字段名与 CEL 类型名相同时不判,因为改读 overload 消息会
误杀合法的 type(record.x) == string)。从语法分支绕过去会把那条决定悄悄推翻,并把一条
error 级闸门从「解析不了」扩张成「类型检查不过」—— 而这一面的谓词绝大多数是 dyn
裁定说的是语法,parse 判定恰好就是语法。已加测试钉住这条边界。

消息自纠

实测过的非 CEL 拼法各自点名并给出 CEL 写法:=====!==!=<>!=
and&&or||not!、单个 ===。扫描前先把字符串字面量抹平,
所以 record.msg == 'a === b' and record.n > 1 归咎于 and 而不是字面量里的 ===;
record.msg == 'a === b' 本身能解析,压根不报。

?? 与 SQL 的 IN (…) 故意不进表:两者都会解析失败、都照报(带前端原话),但都没有
「换一个 token」就能修好的等价写法,给半个修法只会让作者多跑一趟。

真实 CLI 门后的输出(下面「端到端」一节的注入实验):

✗ Author-time rules failed (1 issue)
• view "views[0]" · formViews.edit: visibility predicate is not valid CEL — Unexpected character: = (predicate: `record.priority === 'urgent'`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).
    `===` is not a CEL operator — CEL spells it `==`. Replace `===` with `==`, e.g. `record.country == 'USA'`.
    rule: visibility-predicate-syntax  at views[0].formViews.edit.sections[0].fields[6]

边界(均已钉测试)

  • 空/纯空白谓词不是语法错parseCelToAst 对空源也返回 null,没有这道 guard
    会把「没写谓词」报成坏 CEL。
  • DEFAULT_LIMITS 超限属于边界错而非语法错,照报但引用前端原话、不假装找到了
    typo(与 ADR-0032 把两者一并归入「invalid CEL predicate」的既有做法一致);
    超长谓词在消息里省略,单条 runaway 表达式刷不满控制台。
  • 一条坏谓词只出一个 finding。源码解析不出 AST 就没有标识符可判,裸标识符闸自动
    让位。该互斥性由「断言整个上报集合」钉住,而不是靠调用方内部实现。

反向验证(先声明,后运行)

声明的方向:RED(常规方向)—— 把生产改动摘掉、测试全留,新增的钉子测试应当变红。
预测 21 红 / 65 绿(共 86),并逐条列出了哪些断言会红。

实测结果:21 failed | 65 passed (86),且逐条命中,与声明完全一致

诚实记录的一点:纯「不报」型断言在功能被删掉时是空绿的。因此凡是有意义的地方,
都把否定断言与同一测试内的肯定断言配对(如「CEL 写法是干净的 —— 配对以防空绿」、
「空谓词不是语法错」、「一条坏谓词只出一个 finding」),删掉规则时红的是配对里的
肯定那一半。仍然空绿的几条(如「字符串字面量里的 === 不是错」「不扩张到类型
检查」「若干合法谓词不报」)在报告里按空绿如实标注,不当作证据。

另有一条既有测试原本断言整条规则沉默('country === "USA"'[]),它钉的正是
本 PR 删掉的那个分支 —— 按「整条替换」处置,重写为它幸存的那半个事实:解析不出 AST
的源不产生裸标识符判定

验证证据

闸门 结果
pnpm lint(ESLint,家族闸在此 job 内) pass,无输出
pnpm --filter @objectstack/lint typecheck pass
pnpm --filter @objectstack/cli typecheck pass(消费方编译面)
pnpm --filter @objectstack/lint test 62 files / 1573 passed
pnpm --filter @objectstack/cli test 91 files / 928 passed
pnpm --filter @objectstack/metadata-protocol test 53 files / 552 passed
pnpm check:nul-bytes OK(6093 文件,无裸控制字节)
pnpm check:empty-changeset OK(本 diff 新增 1 份声明式 changeset)
pnpm check:adr-anchors OK
pnpm --filter @objectstack/lint check:doc-formula-expressions OK

新增 32 条测试。另按字节纪律对改动文件做了闸门之外的自扫描
(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'),干净。

消费半径清扫

规则只有注册表一个调用方,但改动会把原本沉默的谓词变成阻断,所以全仓清扫了会喂到
这条规则的 fixture:全仓 examples / apps / packages 的 view/page 可见性谓词都能通过
规范前端解析。唯一的 === 出现在 packages/spec/src/ui/view.test.ts(:1126 / :1240 /
:1245 / :1291 / :1373)—— 那是纯 schema 测试样本,只跑 FormFieldSchema.parse,
不经 lint,是本单援引的证据而非待修点,按要求只读未改。CLI 的模板与脚手架里没有
任何可见性谓词。

端到端(证明真的会拦)

三个示例 app os validate 全部通过、零 visibility finding、exit 0。
随后往 app-showcase 的表单谓词临时注入 ===,os validate exit 1
✗ Author-time rules failed (1 issue)(见上面的输出),随即已还原并复验通过 ——
「坏谓词发不出去」是实测的,不是推断的。

明确没有做的事

  • 没碰 validate-expressions.ts —— 本单不改 ADR-0032 的遍历面。
  • 没碰 packages/spec/src/ui/view.test.ts —— 那里的 === 是样本,不是待修点。
  • 没碰 validate-null-guards.ts —— 同一条沉默政策的另一条规则,不在本单射程内。
    顺带说明:它的沉默在它自己的调用点上仍然成立(validateExpression 就在旁边跑),
    本 PR 的论据不构成对它的改动理由。
  • 没碰 content/docs/releases/
  • 没改注册表:validateVisibilityPredicates 的 tier 在 lint(devx): view/page 谓词(visibleWhen/visibleOn 等)的裸标识符构建期静态校验 —— #5149 裁决拆单 #6128 已是 gating
    commands 已是 build/lint/validate,本规则的 error 直接沿用(已加测试复核该前提仍然成立)。
  • 没加 skip-changeset 标签(本 PR 有 changeset)。
  • 未翻 ready-for-review、未开启 auto-merge。

Changeset

.changeset/lint-visibility-predicate-syntax-gate.md,@objectstack/lint: minor
取 minor 的理由是照搬同族先例:同一文件、同样「新增一条 error 级规则」的
visibility-bare-identifier(#6128)用的就是 minor。这是新增能力而非破坏性 API 变更 ——
公开签名只是追加了一个规则 id 常量,既有类型与函数签名一律未变。

规则 id 常量已按 rule-id-barrel-exports.test.ts 的要求加入已发布 barrel
(packages/lint/src/index.ts)—— 这是本 PR 唯一一处在 issue 指定文件面之外的改动,
纯机械要求:少了这一行,该闸门会红。


Generated by Claude Code

claude added 2 commits August 8, 2026 00:33
新增 error 级规则 `visibility-predicate-syntax`:view/page 的可见性谓词
(`visibleWhen` 及两个已弃用别名 `visibleOn` / `visibility`)如果规范 CEL
前端解析不了,构建期直接拒收,不再零诊断放行到运行时 fail-open。

按维护者 2026-08-07 对 #6253 的裁定:判 blocking error,与 ADR-0032 下
其它谓词面同级,不设 warning 档、不写本面豁免。

判定仍取 `parseCelToAst`(规范前端),本规则不自建 Environment、不手写
tokenizer;新增的是对既有判定的上报与自纠措辞。明确不走 `compile()` /
`validateExpression`——那是 parse + 类型检查,实测会拒掉 `type == 'grid'`,
从语法分支推翻本文件已钉测试的既有盲点,并把闸门从「解析不了」扩张成
「类型检查不过」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
`quoteSource(source!)` 改为在 `if (source && syntaxFault)` 内由编译器收窄,
去掉本文件注释自己反对的 `!` 断言。行为不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 1:30am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`check:type-check-debt`(TypeScript Type Check job 的最后一步)红:
@objectstack/lint 的 TEST_DEBT 实测 42 → 47(+5)。

根因是一处**既有**缺陷,被本单新增的测试放大:测试文件第 10 行
`from './validate-visibility-predicates'` 缺 `.js` 后缀。在 NodeNext 下这是
TS2835,且该模块因此解析为 `any`,于是文件里每一个 `.map((f) => …)` /
`.filter((f) => …)` 回调都级联出 TS7006(`f` 隐式 any)。本单新增的断言带来
更多这类回调,把既有级联乘大了。

补上 `.js` 后(与同包所有测试文件、以及本文件自己的第二条 import
`./authoring-rules.js` 一致),该文件的测试层错误 28 → **0**,
整包 47 → **19**,低于台账记录的 42。

**台账未抬**(棘轮只减不增,#5278):entry 仍是 42,门现在把它报成
改进(`ℹ TEST_DEBT records 42, tsc now reports 19 (-23)`)。
**测试未削弱**:本次改动是 1 增 1 删的单行 import,断言一条未动 ——
118 处 expect、86 条用例(其中本单新增 32 条)全绿。

本地未复现是因为检查面不同:@objectstack/lint 的 tsconfig 把 `*.test.ts`
排除在外,所以 `pnpm --filter @objectstack/lint typecheck` 根本不读测试文件,
而该门会解除排除后重测。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

view/page 可见性谓词的 CEL **语法**在构建期无人校验 —— country === "USA" 这类写法零诊断、运行时静默 fail-open

2 participants