Skip to content

Commit 461ccda

Browse files
fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键 ?? 别名读法 (#5017) (#5046)
* fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键 `??` 别名读法 (#5017) #4984#5009 同族第三轮。两条规则都以 `input: 'parsed'` 注册,看到的是 `ObjectStackSchema` 解析后的产物,所以读一个 spec 不声明的键对任何能解析的 stack 都不执行。议题点名五条,全包 grep 又找出同形的两条,一并处置为只读 canonical;`obj.security?.sharingModel` 整段删除 —— `ObjectSchema` 根本没有 `security` 键。 其中 `rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula` 不是死代码而是活着的错:canonical 排第三,同时写了 `condition` 和被拒别名 `expression` 的规则,lint 校验的是 schema 会拒绝的那个,作者声明的那个从未 被读。测试里重建旧链演示该差异。 三个 example 与平台 default permission sets 上,改动前后 findings 逐字相同。 补两层结构性 meta-guard(declared-key ⊆ schema.shape + reachability),七条 读法各自通过变异测试。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ * fix(cli,lint): 把消费被拒别名拼法的 fixture 改为 canonical (#5017) #5046 把 validate-expressions / validate-security-posture 收敛为只读 spec 声明的键之后,两处 fixture 用的正是被拒的别名拼法,规则不再读它们: - `packages/cli/test/authoring-rule-command-parity.test.ts:90` —— CI Test Core 判红点。`validations: [{ name: 'r', expression: … }]` 里 `expression` 是 `validation.zod.ts` 按名拒绝的四个别名之一,收敛后没人读,`expression-invalid` 在三命令上都不再触发。改为 canonical `condition`,并补上 `type` / `message` 让 fixture 除了那条**刻意种下的**裸引用缺陷之外完全 spec 合法 —— parity 测试 本就该跑在 spec 合法元数据上,原来的 fixture 等于种了两个缺陷。 - `packages/lint/src/runtime-gate.test.ts:125,150` —— 这两处此前是绿的,但绿得 没有意义:`validationRules` + `expression` 双重别名,使得"上下文里有一条坏 验证规则"的 fixture 实际产生 0 条 finding(实测),所以 D4 那条断言 `result.errors).toEqual([])` 通过的原因是**没有东西可减**,而不是减法正确。 改为 canonical 后上下文真的产出 1 条 `invalid CEL predicate` finding,减法 逻辑第一次被真正跑到 —— 且仍然通过。 全包 grep 过 `expression:` / `predicate:` / `formula:` 作 validation 键、 `criteria:` 作 sharing 键、`validationRules:`、`security: { sharingModel }`、 `reference_to` / `referenceTo`:其余命中要么是 #5046 里刻意 pin 住 schema 拒绝 的别名 fixture,要么属于本 PR 未改动的规则(`validate-rule-compilability.ts` 自己读 `validationRules`,已另开 #5096),要么是 objectql / driver 等包自己的 内部形状,与本次收敛无关。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 866ff16 commit 461ccda

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)