Skip to content

Commit c94b95c

Browse files
committed
docs(changeset): 字段级 *When 根白名单与按槽位分档的因果句 (#6713, #6716)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
1 parent 9f6aa9c commit c94b95c

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
"@objectstack/formula": patch
3+
"@objectstack/lint": patch
4+
---
5+
6+
字段级 `*When` 的未绑定根检查:黑名单翻成白名单,并把因果句按槽位分档
7+
8+
同一段诊断上的两条**正交**分档轴,一次设计通过 —— 分开做会把这段文案写两遍,
9+
且第二遍推翻第一遍。
10+
11+
## 轴一:根集合从 3 项黑名单翻成 3 项白名单(#6713)
12+
13+
字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen` 实测只绑 `record`
14+
`previous``parent` 三个根,三处独立证据一致:服务端
15+
`rule-validator.ts` 的两处绑定(`readonlyWhen`
16+
`{ record, previous, extra: { parent } }`,`requiredWhen`
17+
`{ record, previous, ...parentScope }`);客户端 `evalFieldPredicate`
18+
`record` + `previous` + 调用方 `scope`,而 objectui 全部五个字段级调用点
19+
(`form.tsx` ×3、`WizardForm.tsx``GridField.tsx`)传的 `scope` 只可能是
20+
`undefined``{ parent }`;作者端 objectui 的
21+
`FIELD_RULE_ROOTS = ['record', 'previous', 'parent']`,注释明写 "nothing else"。
22+
23+
而检查此前是一张**黑名单** —— #6584 一项、#6711 三项
24+
(`current_user` / `user` / `ctx`)。黑名单在这个面上结构性地追不上
25+
`SCOPE_ROOTS`:每新增一个根都要有人记得抄过来(`current_user` 自己就是 #6290
26+
加进去、#6584 才被发现的)。实测有 **21 个根**落在这条缝里,它们同样未绑定、
27+
同样 fault、而且同样**静默** —— 都在 `SCOPE_ROOTS` 里,所以裸引用检查也从不
28+
报它们。其中两个是高可信度的作者笔误而非理论成员:
29+
30+
- `os.user.id` —— ADR-0068 D1 的**第四种**用户拼写(`buildScope` 把同一个
31+
`EvalUser` 挂在 `current_user` / `user` / `ctx.user` / `os.user` 下),#6711
32+
收了三种,`os` 这一支没收;
33+
- `data.status == 'x'` —— `data`**元数据表单**里同一个 `visibleWhen` 键的
34+
**合法**根(`view.zod.ts`:"Root: `record` … in runtime forms, or `data` in
35+
metadata forms"),两种表单同一个键名、不同的根。
36+
37+
判定改为 `SCOPE_ROOTS` 成员减去白名单,列表直接从 `@objectstack/formula` 取,
38+
不在消费端重述 —— 因此 `SCOPE_ROOTS` 将来新增的成员自动被覆盖。
39+
40+
处方随之**按根分档**:用户根(`current_user` / `user` / `ctx` / `os`)保留原有
41+
的选项级 `visibleWhen` 与权限集 FLS 两条用户向处方;`data` 给出元数据表单 vs
42+
运行期表单的解释;其余根给出通用的「改写成 `record` 谓词」。此前只有用户向处方,
43+
对写了 `data.type == 'select'` 的作者是答非所问。
44+
45+
## 轴二:因果句按槽位分档(#6716)
46+
47+
三个槽位此前共用一句「falls back to VISIBLE … showing for everyone」,而这句话
48+
只对其中一个精确。三格全部**实测**,每格量了两端:
49+
50+
- **`visibleWhen` —— 仅客户端、fail-OPEN,原文案正确。** 服务端根本不评估字段级
51+
`visibleWhen`(`ConditionalFieldDef` 无此成员,`fieldsNeedPrior` 只看
52+
`requiredWhen || readonlyWhen ||` 选项可见性),唯一裁决来自渲染端,
53+
`resolveFieldRuleState` 对可见性传 `fallback: true`
54+
- **`readonlyWhen` —— 两端方向相反,服务端说了算,原文案是反的。** 服务端
55+
`isReadonlyWhenLocked` 命中 `unknownVariableOf` 后返回 `true`(#4889
56+
carve-out,其触发条件正是未绑定根这一类),`stripReadonlyWhenFields` 随即把该
57+
字段从 payload 中删除;客户端 `resolveFieldRuleState``fallback: false`,
58+
表单仍渲染为可编辑。按 ADR-0057 D10(server enforces, client is courtesy)以
59+
服务端为准:作者改了字段、保存报成功、值静默不落库。原文案告诉作者「对所有人
60+
可见」—— 失败方向与排障方向都相反。
61+
- **`requiredWhen` —— 两端都 fail-OPEN,且与可见性无关。** 服务端记日志后
62+
`continue`(#4977 明确没有采用 #4889 的 carve-out),客户端 `fallback: false`,
63+
两端都不强制,记录带着空字段保存成功。原文案在这里不只是不精确,而是说错了
64+
字段的哪个属性。
65+
66+
`conditionalRequired``FieldSchema` 里是 `retiredKey`(按名字拒绝),解析后的
67+
编译路径上该分支是惰性的,因此给它一条与槽位无关的通用句,而不是编造第四格测量。
68+
69+
## `@objectstack/formula`
70+
71+
`SCOPE_ROOTS` 改为公开导出。一个绑定**封闭**根集合的面,必须能说出它****绑定
72+
的那些根,而那个补集就是 `SCOPE_ROOTS` 减去该面自己的白名单;消费端手抄的列表
73+
追不上这张表。注意它不能用 `firstUndeclaredReference` 替代:严格环境同时声明了
74+
CEL 的**类型名**,`type(record.x) == string` 里的 `string` 会被判成「能解析的根」
75+
—— 实测按可解析性判定会误杀这条合法谓词(1 例),按 `SCOPE_ROOTS` 成员判定不会。

0 commit comments

Comments
 (0)