Skip to content

fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键 ?? 别名读法 (#5017) - #5046

Merged
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5017-lint-alias-fallback-sweep
Aug 4, 2026
Merged

fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键 ?? 别名读法 (#5017)#5046
xuyushun441-sys merged 3 commits into
mainfrom
claude/issue-5017-lint-alias-fallback-sweep

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #5017

#4984#5009 同族第三轮。两条规则都以 input: 'parsed' 注册(authoring-rules.ts),看到的是 ObjectStackSchema 解析后的产物 —— 未声明的键要么被 strip,要么被 strict 子 schema 按名整包拒绝,所以读它的分支对任何能解析的 stack 都不执行。

议题点名五条,按派发要求全包 grep 同形 ?? 别名链,又找出两条,一并处置。

七条链,逐条核实

每条都对着 live .shape + safeParse 实测过。

位置 原读法 spec 事实(实测) 处置
validate-expressions.ts 205/421 rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula 四个别名全在 validation.zod.tsaliases: { formula/expression/predicate/rule: 'condition' } 里按名拒绝;canonical 排第三 收敛为 rule.condition
validate-expressions.ts 414 obj.validations ?? obj.validationRules ObjectSchema.shape 只有 validations;拒绝信息 "Did you mean validationRulesvalidations?" 收敛
validate-expressions.ts 595 rule.condition ?? rule.criteria ?? rule.predicate SharingRuleSchema.shape = accessLevel, active, condition, description, label, name, object, sharedWith, type;criteria 是运行时 criteria_json 的拼法(#3896),predicate 直接拒绝 收敛
validate-security-posture.ts 94 obj.sharingModel ?? (obj.security)?.sharingModel ObjectSchema 没有 security —— sharingModel / externalSharingModel / publicSharing 是平铺的,且 strict:嵌套写法被整包拒绝 删除整个 fallback
validate-security-posture.ts 118 def.reference ?? def.reference_to field.zod.ts:331reference_to 映射为 reference 收敛
额外找到 validate-expressions.ts 178 def.reference ?? def.referenceTo 同上一行的别名表 收敛
额外找到 validate-expressions.ts 555 action.objectName ?? action.object canonical 是 objectName;"Did you mean objectobjectName?" 收敛

关键差异:第一条不是死代码,是活着的错

其余六条 canonical 都排首位,别名 limb 纯属不可达。这一条 canonical 排第三,所以别名会短路掉 canonical。改动前实测:

A) condition 干净 + expression 有错  => 报的是 expression 里的错
B) condition 有错   + expression 干净 => 报的是 expression 的 null-guard,
                                        condition 里的 `no_such_field` 从头到尾没被看过

也就是说:一条同时写了 condition 和被拒别名的规则,lint 校验的是 schema 会拒绝的那个,而作者声明的那个从未被读。producer 和 consumer 对同一份元数据给出两套说法。

分两层说清:

  • parsed 层(os compile / build / validate):三个别名都让整包 stack 被拒,所以恒为 undefined,链必然落到 condition —— 与只读 canonical 完全等价,零行为差异
  • normalized 层(os lint 不 parse,两个 tier 都跑在 normalized 上):别名可达,上面的短路就在这里发生。改动后 lint 读 canonical,真实缺陷浮出来。

测试里重建了旧链(OLD_CHAIN)来演示这个差异,而不是只描述它。

反向验证:两个方向都测了

#5018 的教训是方向会反过来。这里两种都有,各自 pin 住:

  • canonical 排首位的六条:改动前对非法拼法越权判红 → 改动后让位给 schema 指名道姓的拒绝。七条别名拼法逐条断言 schema 的拒绝文案 + lint 之后的输出。
  • def.reference ?? def.referenceTo(178):这条喂的是计数不是谓词,所以删掉别名 limb 反而会让下游 parent-scope gate 新增一条诊断(master 数从 1 变 0)。只发生在 pre-parse 层、且该 stack 已因这个键被 schema 按名拒绝 —— 是无效 stack 上的噪音,不是有效 stack 上的新判定。诚实 pin 住而不是留给后人发现。
  • action.objectName ?? action.object:删掉别名丢的是 action 的对象绑定而非谓词,所以对象无关的那半(裸引用作用域)照旧判红,字段存在性那半不再可能。lintAfter 逐条写明,没有统一假设。

真实元数据零新红

app-crm / app-showcase / app-todo + plugin-security 的 default permission sets,改动前后两条规则的 findings 逐字 diff 为空(1 + 5 + 0 + 0 条安全 finding,0 条表达式 finding,全部不变)。

两层 meta-guard(#4992 模式,#5018 形状)

  • declared-key guard —— 源码里从每个 receiver 上读的键 ⊆ 该 surface 自己的 .shape,且 expected 精确匹配(改名 loop 变量会静默解除扫描,所以强制回访表格)。另加一条 "covers every receiver" 元测试,防止 receiver 整个溜出表格。
  • reachability guard —— validateSecurityPosture 全部 15 个 findings.push 落点都被 fixture 触达。

判据上有一处刻意不照抄 #5018:这里不要求 fixture safeParse 全绿,而是要求 schema 不报 unrecognized_keys。因为这条规则被文档明确设计成也跑在 parse 前,好让 os lint 对 zod 会拒绝的(sharingModel: 'read'invalid_value)给出更好的信息 —— 强求 fixture 全绿会直接删掉四条正当规则。被拒的和被拒的是两回事:后者在 parsed 路径上压根到不了,而它出现在源码里就等于宣称"存在这么一个 authoring 面"。

覆盖范围写明:validate-security-posture.ts 两层 guard 全覆盖(15 个落点、除两个有据可查的非 schema receiver 外的全部 receiver)。validate-expressions.ts 的 declared-key guard 覆盖除 flow node config 外的全部 receiver(cfg / startCfg 排除的理由本身是 schema 事实:node config 按 type 判别、表达式槽走 resolveFlowNodeExpressions 描述符注册表;而 functionNameschemaless-node-config.zod.ts 声明的键、由 ADR-0087 D2 转换 flow-node-script-config-aliases(#3796)在 load 时改写,退役五键是刻意在 pre-parse 层识别以给出具名替代 —— 读一个键是为了拒绝它,与本单的缺陷正好相反)。reachability 那半限定在本单改动的读法及其所在 surface:该规则 660 行、issues.push 落点不带 rule id / path 模板(finding 形状早于 { rule, path, hint }),#5018 式的落点扫描不能直接搬;完整落点清单属于议题自己的建议 3(所有 input:'parsed' 规则共用一条 guard)。

七条读法各自做过变异测试:任意一条加回去,都至少有一条测试转红(M1–M8,分别 2/5/2/2/2/1/3/2 条红)。

顺带发现,已另开单不在本 PR 修

#5026 —— 同文件的字段公式校验读 f.formula,而 FieldSchema 声明的是 expression(formula 正是它按名拒绝的别名)。形状同族但不是 ??:代码里根本没有 canonical 读法,所以这段对任何 spec 合法 stack 从未跑过(真实元数据全部用 expression: 拼法)。收敛过去等于启用一条从未跑过的检查 —— 是覆盖面扩大而非删死代码,可能对现有元数据判红,不该由本 PR 顺手带上。

本 PR 里它是 declared-key guard 中唯一一条显式记账的欠债(TRACKED_UNDECLARED_READS,带 issue 号),并注明"这个列表只能缩短,不能变长"—— 而不是把 f 整个排除出表格。

验证

pnpm --filter @objectstack/lint test        → 56 files, 1127 tests passed
pnpm --filter @objectstack/lint typecheck   → tsc --noEmit, clean
eslint (四个改动文件)                        → exit 0
真实元数据 before/after diff                 → 空
变异测试 M1–M8                               → 全部转红

改动限于 packages/lint。未触碰 content/docs/releases/


Generated by Claude Code

… 不声明键 `??` 别名读法 (#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
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 4:10am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

#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

Copy link
Copy Markdown
Contributor Author

CI Test Core (3/3) 修复 —— 跨包 fixture 消费被拒别名拼法

22687eb 已推。合并了最新 main

判红点

packages/cli/test/authoring-rule-command-parity.test.ts:90 的 fixture 写的是

validations: [{ name: 'r', expression: 'lead_score > 100' }]

expression 正是 validation.zod.ts 按名拒绝的四个别名之一。规则收敛为只读 condition 后没人读它,expression-invalid gates on all three commands 于是 expected [] to include 'expression-invalid'

改为 canonical,并补上 type / message,让 fixture 除了那条刻意种下的裸引用缺陷之外完全 spec 合法:

validations: [{ type: 'script', name: 'r', message: 'Score out of range', condition: 'lead_score > 100' }]

实测收敛后的规则仍然读到并判红:

parity fixture => 1 ["error | bare reference `lead_score` — a formula/validation expression binds the record a…"]

blindTo 在这张表里是文档不是断言(文件头注明),断言是"三个命令都判红",所以没有静默改断言 —— 与 #4409 的意图一致:原来的 fixture 等于种了两个缺陷(别名键 + 裸引用),而 parity 测试本就该跑在 spec 合法元数据上。

顺手 grep 又抓到一处「绿得没有意义」

packages/lint/src/runtime-gate.test.ts:125,150 此前是绿的,但绿的原因不对:

validationRules: [{ name: 'bad', expression: 'record.owner ==', message: 'x' }]

双重别名(集合键 + 谓词键)。实测:

canonical spelling  => 1 finding(s): ["invalid CEL predicate: Unexpected token: EOF …"]
alias spelling      => 0 finding(s)   <- 此前如此

也就是说 D4 那条「上下文里的既有违规不该赖到本次写入头上」的用例,断言 result.errors).toEqual([]) 之所以通过,是因为没有东西可减,而不是减法正确。改为 canonical 后上下文真的产出一条 finding,减法逻辑第一次被真正跑到 —— 且仍然通过(11/11)。这是本次修复顺带恢复的覆盖,不是新增的宽容。

全包 sweep 结果

grep 过 expression: / predicate: / formula: 作 validation 键、criteria: 作 sharing 键、validationRules:security: { sharingModel }reference_to / referenceTo,以及本 PR 改动的两条规则在 packages/lint 之外的全部调用方(cli 的 validate / lint / compile、metadata-protocol 的 runtime gate、plugin-security)。其余命中分三类,均无需改动:

  • 本 PR 里刻意保留的别名 fixture(REJECTED_ALIASES、security 的 security:{} / reference_to pin)—— 它们的作用就是断言 schema 的具名拒绝;
  • 属于本 PR 未改动的规则(validate-rule-compilability.ts 自己读 obj.validations ?? obj.validationRules,其 fixture 与之自洽);
  • objectql / client / driver-* 等包自己的内部形状,不经过这两条规则。

另开的单

#5096 —— 全包 grep 的第八处,落在第三个文件 validate-rule-compilability.ts:239,同形 obj.validations ?? obj.validationRules,同样 input: 'parsed'。没有一并带进本 PR:它不在 #5017 点名范围内,本 PR 的真实元数据零新红 / 反向验证 / 变异测试都是围绕那两个文件做的,顺手带上等于把没做过同等验证的改动混进已经绿了的 PR。#5096 里写了照本 PR 模式的处置步骤,含"改规则时必须同步改 validate-rule-compilability.test.ts:295 的别名 fixture"这条 —— 正是上面 runtime-gate 踩到的坑。

顺带确认了一件可能推翻本 PR 前提的事

packages/spec/src/conversions/registry.ts:264owd-legacy-read-aliases 是仓库里唯一承认 object.security.sharingModel 存在的地方,乍看像是"security 信封是真实的 pre-parse 形状"。核过了,不影响本 PR 的删除:该转换 retiredFromLoadPath: true(已退出 load 路径),而且它只把嵌套值 read/read_write 规范化,从不把 security.sharingModel 上提到 sharingModel —— 即便它还在跑,对象仍然带着 ObjectSchema 会整包拒绝的 security 键。换句话说那段转换自己也是死的。

验证

pnpm --filter @objectstack/cli test    → 68 files, 600 tests passed
pnpm --filter @objectstack/lint test   → 57 files, 1160 tests passed
typecheck (cli + lint)                 → tsc --noEmit, clean
eslint (两个改动文件)                   → exit 0

Generated by Claude Code

@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 461ccda Aug 4, 2026
24 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-5017-lint-alias-fallback-sweep branch August 4, 2026 04:27
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…ctstack-ai#5096) (objectstack-ai#5121)

objectstack-ai#4984objectstack-ai#5009objectstack-ai#5017/PR objectstack-ai#5046 同族第八处,落在第三个文件。
`validate-rule-compilability.ts:239` 读 `obj.validations ?? obj.validationRules`,
而 `ObjectSchema.shape` 只声明 `validations` 且 strict —— `validationRules` 被按名
拒绝("Did you mean `validationRules` → `validations`?",objectstack-ai#4001)。该规则以
`input: 'parsed'` 注册,canonical 排首位,别名 limb 对任何能解析的 stack 不可达。

三个 example(crm / showcase / todo,28 个对象、17 条验证规则)上改动前后 findings
逐字相同,两侧均 0 条。代价从来不是漏报而是误导:consumer 里的别名 fallback 等于
向后来的读者和照着写的 AI 宣称 `objects[].validationRules` 是真实 authoring 面。

补两层结构性 meta-guard:declared-key(含 `rule[branch]` 计算属性读法的专项断言)
+ reachability(判据是 `safeParse` 全绿,比 objectstack-ai#5046 严一档 —— 编不过的 regex/schema
在 spec 眼里依然完全合法)。原测试 `reads `validationRules` too` 断言的正是被删掉
的 limb(实测产出 1 条 finding,非空转),故替换而非改拼写。变异测试:别名 limb
加回 → 2 条红;改为纯别名读 → 11 条红。


Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果)

2 participants