feat(lint): view/page 谓词裸标识符构建期闸门 —— 坏谓词发不出去 (#6128) - #6248
Merged
Conversation
新增 error 级规则 `visibility-bare-identifier`:view/page 可见性谓词 (`visibleWhen` + 两个已弃用别名 `visibleOn` / `visibility`)引用了任何绑定根都 解析不到的顶层标识符时,三条 authoring 命令一律拒收。#5149 维护者 2026-08-06 裁决的构建期半边(运行时 warn-once 半边已由 objectui#3541 合入)。 两道现有闸都放行的机制已写进规则注释防误并:ADR-0032 标识符闸的遍历从不走 views/pages;ADR-0089 D3b 只判有根谓词的根错层,无根谓词两边都不匹配。 判定由两个既有 oracle 合成,本包不自建 CEL 环境(#4812):声明性取 formula 的 `firstUndeclaredReference`,AST 取规范入口 `parseCelToAst`;AST 先声明所有接收者 位置的标识符,于是只剩当作裸值引用的会被判,未知根交还 D3b。 与 #4953 的边界按构造成立:本规则从不追问 KEY 在已绑定根上是否存在,只问标识符 有没有根 —— 无根标识符在全量与稀疏绑定下都解析不到,故 `has(record.x)` / `record.x != null` 两种守卫写法一律绿,已加测试钉住。 遍历按实测修正,否则规则生来即死:`os build` 跑 app-showcase,唯一一条 view 表单 谓词落在 `views[0].formViews.edit.sections[0].fields[6]` —— 运行时形状下 `views[]` 是视图容器。现覆盖容器的 `form` 与每个 `formViews.<key>`,pages 改走共享的 `walkPageComponents`;`objects[].views` 明确不读(schema 已立碑拒绝)。 注册表 tier advisory → gating(#5762 先例)。app-todo / app-crm / app-showcase 三例 `os validate` 全绿、零 visibility finding,示例零改动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 7, 2026
hotlong
marked this pull request as ready for review
August 7, 2026 12:50
hotlong
enabled auto-merge
August 7, 2026 12:50
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6128
按 #5149 维护者 2026-08-06 裁决落地构建期半边(运行时 warn-once 半边已由 objectui#3541 合入)。新增 error 级 规则
visibility-bare-identifier:view/page 可见性谓词里引用了任何绑定根都解析不到的顶层标识符时,os validate/os build/os lint一律拒收 —— 坏谓词根本发不出去。一、为什么现有两道闸都放行(机制,已写进规则注释防后人误并)
#5149 Repro 1 实测:
status == 'active'这类写法通过了平台所有闸门,然后在控制台 fail-open —— 标识符解析不到,evalFieldPredicate返回 fallback,而可见性的 fallback 是true,于是「永远不生效的谓词」与「根本没写谓词」在屏幕上一模一样。两道闸各自漏掉它的原因是结构性的,两条都必须写下来:validate-expressions.ts)确实解析 record 作用域的裸引用,但它的遍历面是 objects / flows / actions / sharingRules / hooks —— 从不走views与pages,view 表单字段的visibleWhen完全在射程外。data.、metadata 面写了record.)。无根谓词两个方向都不匹配,干净通过。维护者裁决维持 fail-open(已发货 app 行为不变),但本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,静默不可以。运行时那半让坏谓词显形,这半让它进不了产物。
二、绑定约定的出处(读 schema / 运行时,不猜)
规则族按 schema 枚举,没有第四种拼写:
visibleWhen是三个载体的规范键(FormFieldBaseSchemaview.zod.ts:1416、FormSectionSchemaview.zod.ts:1510、PageComponentSchemapage.zod.ts:143),visibleOn是 view 侧已弃用别名(view.zod.ts:1418 / :1512),visibility是 page 侧的(page.zod.ts:145)。绑定根同样是读出来的:view.zod.ts:1416 / :1510 写明 runtime 表单绑
record+current_user、metadata 编辑表单绑data;page.zod.ts:143 再加页面状态(page前缀)。运行时侧 objectuipackages/core/src/evaluator/fieldRules.ts的evalFieldPredicate绑record+previous+ 调用方给的 extra scope(如parent)。取并集而不是任一份 —— 这是 error 级闸门唯一安全的方向(少一个根是误红,多一个根只是漏判)。current_user照样声明为合法根,尽管 #6146 实测它「文档有、两端都没实现」:那个根到底解不解析是 #6146 的裁决,本规则不能替它先斩。三、判定怎么做的:两个既有 oracle,本包不自建 CEL 环境
@objectstack/lint不允许自建Environment—— 那正是 #4812 从本包 null-guard pass 手里拿掉的私有前端。所以:parseCelToAst(packages/lint 绕过 @objectstack/formula 直接 parse CEL —— 两个解析入口对「什么能解析」会给出不同答案 #4812 / refactor(formula,lint): parseCelToAst 成为唯一的 CEL 解析入口 (#4812) #6130);a.b/a?.b/a['b']/a.exists(…))—— 作者把它们当命名空间用,未知的那些是根错误,归 ADR-0089 D3b 管;@objectstack/formula的firstUndeclaredReference—— 即validateExpression给 record 作用域裸引用定罪的同一个严格环境。剩下的就只有「当作裸值引用」的标识符。这样拆的必要性是实测出来的:comprehension macro 变量(
record.tags.all(t, t != '')里的t)在 AST 里就是一个顶层 id,只有 cel-js 的 checker 知道它是宏内绑定;反过来 checker 会把my_record.x的未知根也报出来,而那不是本规则的射程。任一个 oracle 单用都会误红。@objectstack/formula侧因此新增一个公开导出firstUndeclaredReference,理由与既有的collectCelRootIdentifiers完全一致:绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方重建严格环境。四、与 #4953 的边界(要求的稀疏绑定不误杀)
#4953 实测同一求值器在全量 vs 稀疏绑定下语义恰好相反:
has(record.a)全量true/ 稀疏false;record.a != null全量false/ 稀疏 FAULT。本规则按构造与这条分叉无关:它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根 —— 而无根标识符在两种绑定下都解析不到。所以无论 #4953 最终怎么裁,
has(record.x)/record.x != null/has(a) && has(b) && a < b/ 其全量绑定对应写法,在本闸门下一律绿。这条边界不是叙述,是钉住的:五个形状逐一 pin,连同一句注释说明「若日后有人把规则扩成 key 级推理,这几条会红,而不是边界被悄悄丢掉」。
五、遍历按实测修正 —— 否则规则生来即死
写完规则后端到端注入验证,
os validate没有红。查因:os build跑examples/app-showcase,全仓唯一一条 view 表单谓词落在运行时 app 形状下
views[]条目是视图容器 ——ViewSchema的自有键就是list/form/listViews/formViews(view.zod.ts:1890-1903,严格错误映射里把这句话原文写了出来),sections在下一层。原遍历只读views[].sections,在这份 stack 上报「干净」。也就是说两条既有 ADR-0089 D3b advisory 在真实形状上一直基本不可达 —— 这是 #4984 / #5009 那一族。所以遍历改成:
form、每个formViews具名项,以及仍然直接携带sections的defineForm形状(metadata 编辑表单走的就是它);list/listViews是ObjectListViewSchema,不带sections,不走。walkPageComponents(lint: no reference-integrity or option-key validation for app metadata #3583)—— regions、slotted 页的 slots 映射、以及藏在无类型properties袋里的page:tabs/page:accordion/page:card子树都随之覆盖,source-authored 页按其既有语义跳过(那里的regions是派生缓存,对它报 error 等于对作者没写过的元数据拒收)。手写一份regions[].components[]循环正是page-walk.ts自己头注里说的「已经造出过一条死规则」的那份拷贝。objects[].views明确不读:object.zod.ts:1833 已把该键立碑拒绝(「viewsis not an ObjectSchema field」),读它只会造出一条对任何能过 schema 的 stack 都不触发的幽灵检查。已加反向测试钉住「不读」。fields[].fields[](composite / repeater 子字段):那里的绑定我引不出一句 spec 原文,而 error 级闸门不能去判一个自己叫不出名字的约定。已写进注释。修正后同一次注入端到端复现:
顺带把
where补成能区分同一容器下两个表单(容器在产物里可能既无name也无object),并让 name-keyed 集合报键而不是合成下标。六、逆向验证(方向先声明,再跑)
声明的方向:红。 新检查是纯增量的 error 诊断,新测试里的正向断言只有它能满足;所有「必须保持绿」的 pin(稀疏绑定 5 条、macro 4 条、合法根 5 条、未知根、语法不通过、CEL 类型名盲点、注册表 tier)断言的是不产生 finding,应当不受影响。
只把
firstBareIdentifier短接为return null(测试一行不动)后实测:恰好 11 条,且恰好是事先枚举的那 11 条正向断言;所有绿 pin 与既有 ADR-0089 D3b 全套保持绿。恢复后全绿。
七、示例应用扫描(新增 gating 规则的爆炸半径)
三个示例逐个
os validate(遍历修正之后跑的,即用最宽的那一版):examples/app-todoexamples/app-crmexamples/app-showcase三例全部通过,零 visibility 类 finding —— 遍历放宽也没有新增 advisory。示例内容零改动,不需要 rollout 决策。
八、changeset 定级理由
@objectstack/lint: minor,@objectstack/formula: minor。lint-never-fire-family-gates.md):lint 规则由 warning 升 / 新增为 error 级 gating 诊断,该 PR 定的是 minor。本 PR 是新增一条 error 级 gating 规则 + 注册表 tieradvisory → gating,同一类。formula-canonical-parse-entry.md:新增一个公开导出,定 minor。注册表 tier 的改动不是自述:
authoring-rule-wiring.test.ts会读规则源码核对 advisory 声明的真伪,留 advisory 必红。九、已知盲点(方向安全,已钉测试)
type/int/string/list/map/timestamp…)不判:CEL 自身声明这些标识符,type == 'grid'到 checker 那里是类型 overload 错误而非未知变量;改读 overload 消息会误杀合法的type(record.x) == string。DEFAULT_LIMITS)的谓词不判,交还给拥有该判定的闸门 —— 与validate-null-guards.ts同一条政策。附带记录一个真实缺口:view/page 谓词今天没有任何语法校验,===这类写法目前无人报告(见 out-of-scope findings)。两者都是漏判,永远不会变成误红,各有一条测试把它钉成「决定」而不是「洞」。
验证
Generated by Claude Code