fix(formula,lint): current_user 收进 SCOPE_ROOTS,字段级拒绝改为按面的真规则 (#6290) - #6584
Merged
Conversation
`@objectstack/formula` 对同一个根说了两套话。`introspectScope` 把 `current_user` 作为合法命名空间交给作者,`checkRoleCatalog` 的四条 position 成员判定正则也全以它打头 —— 两处都对:ADR-0068 D1 把 `current_user` 定为**规范**拼写,`buildScope` 也确实把同一个 `EvalUser` 挂在它下面。只有 `cel-engine.ts` 的 `SCOPE_ROOTS` 不认,于是严格环境把 这个被祝福的拼写读成**裸字段引用**,而它的两个别名(`user`、`ctx`) 一路放行。 三处改动: 1. `SCOPE_ROOTS` 收下 `current_user`。该表是「永不 fault」的基线,不是 逐面契约,现在它宣告的与本包别处宣告的一致。新增行为钉: `introspectScope` 报出的每个根都必须能在严格环境里解析。 2. 删掉错误修法提示。旧拒绝是基线遗漏的副产物,作者拿到的是**通用** 裸字段诊断 ——「Write `record.current_user`」。这个形状在平台的任何 一层都不绑定,照做的作者得到的东西比原来更糟,而且照样静默。字段级 判定现在由 `@objectstack/lint` 里一条自己的规则给出,写明真实失败链 (未绑定 ⇒ fault ⇒ 可见性 fallback 为 `true` ⇒ 本想藏起来的字段对所有 人恒可见,#6146),并给出**真实存在**的处方:把谓词移到选项自己的 `visibleWhen`、在权限集上声明字段级安全 (`fields: { '<object>.<field>': { readable: false } }`)、或改写成 `record` 谓词。覆盖共用同一求值器的 `visibleWhen` / `readonlyWhen` / `requiredWhen`。 3. option 级 `visibleWhen` 首次被校验。`validate-expressions.ts` 走完 字段级条件规则就停了,于是 `SelectOption.visibleWhen` —— 一个客户端 过滤、服务端强制的可授权 CEL 槽 —— 穿过 compile / validate / 运行期 无人校验。裸字段引用、指向不存在字段、语法错误、误用 template 方言 全部静默通过,选项只是从此不再出现。现在按 option value 定位逐条走查, 与宿主字段同为 `record` scope。 两个面**故意**对 `current_user` 给出相反判定,因为求值器不同:字段级走 `evalFieldPredicate`(`record` + `previous` + `parent`,从不绑用户), option 级走 `resolveCascadingOptions`,对宿主 predicate scope 求值,确实 绑定它(ADR-0068 / objectui#2284)。showcase 的角色门控选项 (`'admin' in current_user.positions`)此前从未撞上本规则,现在作为合法 用法被钉住。 Sweep:option 遍历生效后,三个示例应用(`app-showcase` / `app-crm` / `app-todo`)的 `objectstack validate` 全部通过 —— 零新增 finding,包括 同时携带 record 级联与角色门控选项的那个 showcase 对象。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…rent-user-scope-roots
|
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:
|
baozhoutao
marked this pull request as ready for review
August 8, 2026 06:07
This was referenced Aug 8, 2026
This was referenced Aug 8, 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 #6290
packages/formula对同一个根说了两套话:introspectScope把current_user当合法命名空间交给作者、checkRoleCatalog四条正则全以它打头(两处都对 —— ADR-0068 D1 把它定为规范拼写,buildScope也确实挂了它),只有cel-engine.ts的SCOPE_ROOTS不认。于是严格环境把被祝福的拼写读成裸字段引用,而它的两个别名(user、ctx)一路放行;拒绝时给出的还是通用裸字段处方 ——「Writerecord.current_user」,一个在平台任何一层都不绑定的形状。按 PM 裁决落地:收下进
SCOPE_ROOTS,字段级判定改为按面的真规则,并补齐 option 级遍历。前提复核表(动手前逐条实测)
cel-engine.ts:54-68的SCOPE_ROOTS有user无current_user;validate.ts:427的introspectScope返回['record','previous','input','os','current_user','user','vars'];validate.ts:261-269四条ROLE_*_RE全写 `(?:current_uservisibleWhen的current_user定为合法面visiblegate 求值结果相同」,规范变量名current_user;spec 侧写得更死 ——field.zod.ts:129-143的SelectOptionSchema.visibleWhenJSDoc:「options resolve throughresolveCascadingOptionsagainst the predicate scope (ADR-0068 / objectui#2284)… Per-option is the one*Whensurface where acurrent_usertest actually resolves」8a88885cb,只动packages/spec/src/{data/field.zod.ts,ui/view.zod.ts}+content/docs/**+ changeset;本单动packages/formula/src/cel-engine.ts与packages/lint/src/validate-expressions.ts,交集为空。推前已 mergeorigin/main(至a36db28b7),复核 #6146 的口径原样保留P3 —— 裁决前提的实测细节
耦合方向确认(裁决前提所担心的那半边,确实存在):字段级
visibleWhen对current_user的拒绝,今天完全来自validateExpression(…, {scope:'record'})→firstUndeclaredReference→buildScopedEnv注册SCOPE_ROOTS。收下current_user后该路径不再报:但两者可调和 —— 裁决成立,不作废。 判据是同包另一个 helper 的结构性独立:
collectCelRootIdentifiers走buildEnv(unlistedVariablesAreDyn: true)读 AST,完全不读SCOPE_ROOTS,收下前后同答案:所以「按面区分」不是勉强绕开,而是本仓已有的既定机制:#3447 P2 的审批节点 approver 就用
collectCelRootIdentifiers表达闭合根集,validate-visibility-predicates.ts:418用逐面VIEW_PAGE_EXTRA_ROOTS表达同一件事,而validate-expressions.ts:81早已 import 了这个 helper。结论:
SCOPE_ROOTS是「永不 fault」的基线(其自身 doc-comment 就是这么写的:「an unknown root is a missed catch, a missing root is a false positive that would break the build」),不是逐面契约;逐面闭合由面自己声明。收下current_user之后,字段级判定不但仍可维持,而且比原来更正:原来它是基线遗漏的副产物,所以只能借通用文案说话 —— 那正是错误处方的成因。三个半边
1.
SCOPE_ROOTS收下current_user(packages/formula/src/cel-engine.ts)一词之改 + 成因注释。新增行为钉(不是列表相等断言,
SCOPE_ROOTS作为基线本就可以多于它对外宣告的):introspectScope报出的每个根,都必须能在严格环境里解析。2. 删错误修法提示 —— 判定搬到面上(
packages/lint/src/validate-expressions.ts)新增
checkFieldRuleUserRoot,只作用于字段级visibleWhen/readonlyWhen/requiredWhen(三者共用一个求值器)。新处方原文:三条处方逐条落到实际存在的面上,而非文案改写:option 级
visibleWhen=SelectOptionSchema.visibleWhen(field.zod.ts:143);字段级安全 =PermissionSetSchema.fields(permission.zod.ts:455,FieldPermissionSchema.readable);record改写 = #6146 收窄后的字段级口径。测试用一条否定断言 + 三条肯定断言钉住(「文案变了」不是要保的性质,「文案指向真的绑定的形状」才是)。3. option 级遍历补齐(同文件)
f.options[].visibleWhen首次被走查,按 optionvalue定位。与宿主字段同为recordscope —— 收下current_user之后,两个面的全部差别就落在checkFieldRuleUserRoot这一条上,而这正是它们该有的差别(求值器不同:evalFieldPredicatevsresolveCascadingOptions)。读的是字面
f.options而非(f as AnyRec).options:后者会让 #5017 的 meta-guard 源码扫描看不见这个新面(该文件自己就写着这条纪律)。相应更新了 meta 表:f的期望读集加入options,新增opt→SelectOptionSchema行。反向验证表(方向先写死,再实测)
预测在动手前落笔,与实测逐条比对。
回退 A —— 从
SCOPE_ROOTS删掉'current_user'(其余保留)every root introspectScope advertises really resolvesrole-catalog verdict reachable through every user spellingstill REJECTS a field-level visibleWhenexpected [ { …(4) }, { …(4) } ] to have a length of 1 but got 2prescribes surfaces that exist — never record.current_user[0]就是那条字面写着Write \record.current_user`` 的通用文案expected 'bare reference \current_user` — a for…' not to contain 'record.current_user'`ACCEPTS the showcase role-gated OPTIONexpected [ { …(4) } ] to have a length of +0 but got 1未列入预测表、同向连带转红的一条:
rejects it on readonlyWhen / requiredWhen too—— 同一「翻倍」成因(4 条而非 2 条)。如实记录,非预测命中。回退 B —— 删掉 option 级遍历循环(保留
SCOPE_ROOTS改动)expected [] to have a length of 1)ACCEPTS the showcase role-gated OPTIONtoHaveLength(0)通过是因为什么都没走,不是因为判定对(#5046 的空绿陷阱)未列入预测表、额外转红的两条:#5017 meta-guard 的
every key read off 'opt'与the field receiver reads only declared keys—— 源码扫描独立于行为发现了「读没了」。属于额外收益,如实记录。这就是 ② 必须与 ③ 并存的理由:② 单独看,在回退 B 下是空绿的;真正证明遍历跑起来的是 ③ 产出的 finding。
命令输出
真实 metadata sweep(半边 3 是激活一条检查,同 #5026,必须扫真栈):
零新增 finding —— 包括同时携带 record 级联选项与角色门控
current_user选项的showcase_cascading_select。消费半径外扩(
SCOPE_ROOTS加宽会改动所有recordscope 面的判定,故不止本两包):说明
user/ctx.user别名在字段级仍静默放行,本 PR 不动 —— 那是收下current_user之前就存在的洞(两者一直在SCOPE_ROOTS里),不是本次引入的回归,且ctx还是 ActionEngine 的根,爆炸半径另算。已按纪律另立 finding,不在本 PR 修。新文案指的是面而非拼写,所以不会把作者推去写user.positions。check:engine-double-contract一类 objectql 面门禁不适用。@objectstack/lintTEST_DEBT 记 42、实测 19,该盈余为改动前既有,不属本单)。Generated by Claude Code