Skip to content

Commit bf5a339

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4779-sharing-bulk-recompute
2 parents 40db293 + 461ccda commit bf5a339

7 files changed

Lines changed: 1175 additions & 26 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 收敛 `validateStackExpressions` / `validateSecurityPosture` 里读 spec 不声明键的 `??` 别名链 (#5017)
6+
7+
两条规则都以 `input: 'parsed'` 注册,看到的是 `ObjectStackSchema` 解析后的产物。
8+
#4984#5009 清掉了 sharing rule 字段层和 org-axis 规则里的同形读法;这一轮是同族
9+
第三轮,落在另外两个文件。议题点名五条,全包 grep 又找出同形的两条,一并处置:
10+
11+
| 原读法 | spec 事实 | 处置 |
12+
|:--|:--|:--|
13+
| `rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula`(两处) | 四个别名全是 `validation.zod.ts``aliases: { …: 'condition' }` **按名拒绝**的键;canonical 排第三 | 收敛为 `rule.condition` |
14+
| `obj.validations ?? obj.validationRules` | `ObjectSchema` 只声明 `validations`,strict 按名拒绝 | 收敛为 `obj.validations` |
15+
| `rule.condition ?? rule.criteria ?? rule.predicate` | `criteria` 是运行时编译产物 `criteria_json` 的拼法(#3896),`predicate` 直接拒绝 | 收敛为 `sharingRule.condition` |
16+
| `def.reference ?? def.referenceTo` | `field.zod.ts:331``referenceTo` 映射为 `reference` | 收敛为 `def.reference` |
17+
| `action.objectName ?? action.object` | canonical 是 `objectName`;`object` 按名拒绝 | 收敛为 `action.objectName` |
18+
| `obj.sharingModel ?? (obj.security)?.sharingModel` | **`ObjectSchema` 根本没有 `security`** —— OWD 三个拨盘是平铺的,且 strict:嵌套写法被整包拒绝 | **删除整个 fallback** |
19+
| `def.reference ?? def.reference_to` |`referenceTo` | 收敛为 `def.reference` |
20+
21+
对任何能解析的 stack,判定结果不变 —— 三个 example(crm / showcase / todo)与平台
22+
default permission sets 上,改动前后两条规则的 findings 逐字相同。
23+
24+
**其中一条不是死代码,是活着的错。** `rule.expression ?? … ?? rule.condition ?? …`
25+
把 canonical 的 `condition` 排在两个被拒别名之后,所以一条同时写了 `condition`
26+
`expression` 的规则,lint 校验的是 schema 会拒绝的那个,而作者声明的那个**从头到尾
27+
没被看过**:producer 和 consumer 对同一份元数据给出两套说法。测试里重建了旧链来演示
28+
这个差异,而不是只描述它。
29+
30+
真正的代价从来不是漏报,而是误导 —— `object.security.sharingModel` 出现在**安全
31+
linter**里,足以让下一位作者(人或 AI)相信对象级 `security` 信封是真实的授权面。
32+
33+
同时补上两层结构性 meta-guard(#4992 模式,#5018 形状),让下一条死读法在 review
34+
前就红:
35+
36+
- **declared-key guard** —— 规则源码里从每个 surface 上读的键,必须出现在该 surface
37+
自己的 Zod `.shape` 里。扫源码不是扫行为是刻意的:不可达分支没有行为可断言。
38+
- **reachability guard** —— `validateSecurityPosture` 全部 15 个 `findings.push`
39+
落点都必须被一条 schema **不报 `unrecognized_keys`** 的 fixture 触达。判据不是
40+
#5018`safeParse` 全绿,而这正是这条规则的特点:它被文档明确设计为也跑在
41+
parse 前,好让 `os lint` 对 zod 会拒绝的****(`sharingModel: 'read'`)给出更
42+
好的信息。被拒的****和被拒的****是两回事 —— 后者在 parsed 路径上压根到不了。
43+
44+
七条读法各自做过变异测试:任意一条加回去,都至少有一条测试转红。

packages/cli/test/authoring-rule-command-parity.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ const CASES: ReadonlyArray<{ rule: string; blindTo: readonly AuthoringCommand[];
8787
rule: 'expression-invalid',
8888
blindTo: ['lint'],
8989
stack: withBaseline({
90-
objects: [{ name: 'parity_lead', label: 'Lead', sharingModel: 'private', fields: { lead_score: { type: 'number', label: 'Score' } }, validations: [{ name: 'r', expression: 'lead_score > 100' }] }],
90+
// The planted defect is the BARE `lead_score` (a record-scoped predicate
91+
// binds fields under `record`, so this silently evaluates to null) — not
92+
// the key it is written under. Spelled `condition`, which is the only key
93+
// `validation.zod.ts` declares: `expression` is one of the four names it
94+
// rejects outright, so a fixture using it planted TWO defects and let the
95+
// rule under test read a stack `os validate` would never accept (#5017).
96+
objects: [{ name: 'parity_lead', label: 'Lead', sharingModel: 'private', fields: { lead_score: { type: 'number', label: 'Score' } }, validations: [{ type: 'script', name: 'r', message: 'Score out of range', condition: 'lead_score > 100' }] }],
9197
}),
9298
},
9399
{

packages/lint/src/runtime-gate.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ describe('runtime publish gate (#4463)', () => {
122122
{
123123
name: 'leave_request',
124124
fields: { owner: { type: 'text' } },
125-
validationRules: [{ name: 'bad', expression: 'record.owner ==', message: 'x' }],
125+
// Spelled with the two keys the spec declares (`validations` /
126+
// `condition`). Written as `validationRules` / `expression` — both
127+
// rejected aliases — the broken CEL was not reachable by the rule at
128+
// all, so this fixture proved the subtraction worked by having nothing
129+
// to subtract (#5017).
130+
validations: [{ type: 'script', name: 'bad', message: 'x', condition: 'record.owner ==' }],
126131
},
127132
];
128133

@@ -147,7 +152,12 @@ describe('runtime publish gate (#4463)', () => {
147152
{
148153
name: 'leave_request',
149154
fields: { owner: { type: 'text' } },
150-
validationRules: [{ name: 'bad', expression: 'record.owner ==', message: 'x' }],
155+
// Spelled with the two keys the spec declares (`validations` /
156+
// `condition`). Written as `validationRules` / `expression` — both
157+
// rejected aliases — the broken CEL was not reachable by the rule at
158+
// all, so this fixture proved the subtraction worked by having nothing
159+
// to subtract (#5017).
160+
validations: [{ type: 'script', name: 'bad', message: 'x', condition: 'record.owner ==' }],
151161
},
152162
];
153163
const result = runRuntimeAuthoringRules({

0 commit comments

Comments
 (0)