Skip to content

validateOrgAxisRedLines 读的 sharing-rule 键是 spec 拒收的:ADR-0105 D6 ① 在 criteria 路径上从不触发 #4984

Closed

Description

@xuyushun441-sys

发现于 #4698 的实现过程。同一缺陷类,只是方向反过来:不是 producer 写了没人读的键,而是 consumer 读了没人写的键。

事实

packages/lint/src/validate-org-axis-red-lines.ts 判 ADR-0105 D6 ①(禁止沿 org 树做权限继承)时,对 sharing rule 这样取值:

asArray(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
  const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? '');
  const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? '');
  if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) { /* error */ }
});

但 SharingRuleSchema(packages/spec/src/security/sharing.zod.ts)是 .strict(),声明的键集是:

name, label, description, object, active, accessLevel, sharedWith, type, condition

criteria / filter / sharedTo / recipient 全部只作为被拒别名出现在 sharingRuleUnknownKeyError 的 aliases 映射里 —— 那是给报错信息用的处方,不是被接受的键。

实测(build 后的 spec):

condition => ACCEPTED keys=name,object,active,accessLevel,sharedWith,type,condition
criteria  => REJECTED: Invalid input
sharedTo  => REJECTED: Invalid input

后果

validateOrgAxisRedLines 在 registry 里是 input: 'parsed',即跑在 Zod parse 之后。对任何 spec-valid 的 stack,rule.criteria / rule.filter / rule.sharedTo / rule.recipient 恒为 undefined,JSON.stringify(undefined ?? '') 得 '""',includes('parent_organization_id') 恒为 false。

于是:一条声明为 error、以 ADR-0105 D6 红线为依据的 gate,在 sharing-rule criteria 这条路径上永远不会触发。 作者写

defineSharingRule({
  name: 'hq_sees_children',
  type: 'criteria',
  object: 'work_order',
  sharedWith: { type: 'team', value: 'hq' },
  condition: "record.parent_organization_id == 'org_hq'",   // ← 正是 D6 ① 禁的形状
})

会一路绿灯通过 os validate / os build / os lint。

同一文件里 RLS 那两段(permissions[].rowLevelSecurity[].using/check、objects[].rowLevelSecurity[])读的键是对的,只有 sharing-rule 这段错。ORG_AXIS_CROSS_ORG_BU_GRANT(②)也读 rule.sharedTo ?? rule.recipient,同样恒空 —— 那条同样从不触发。

修法

把两处取值改到 spec 的键上:rule.condition(而不是 criteria/filter)、rule.sharedWith(而不是 sharedTo/recipient)。

两点值得在修的时候一起想清楚,不要机械替换:

  1. 是否保留别名读法。 这条规则也会被 os lint 在 normalized(pre-parse)层跑一次,那一层理论上还能看见作者写的别名。但按 Prime Directive Add comprehensive test suite for Zod schema validation #12,别名不该在 consumer 侧用 ?? 容忍 —— 别名已经在 strictUnknownKeyError 里有明确处方并被 reject,parse 就是那道门。建议只读 canonical 键,别名交给 spec 的拒收信息。
  2. condition 是 ExpressionInputSchema。 parse 之后它是 { dialect: 'cel', source } 信封,pre-parse 时可能是裸字符串。两种形状都要能取到 source(可参照 validate/lint have no check for "declared but never read" metadata — three instances found in one app in a day #4698 那条新规则里的 toCompilerInput)。

回归测试

现有的 validate-org-axis-red-lines.test.ts 里 sharing-rule 那几例用的正是 criteria / sharedTo,所以测试是绿的而规则是死的 —— 测试 fixture 和 schema 一起漂移了。修的时候必须把 fixture 也改成 spec-valid 的形状,否则换个键名照样测不到真东西。建议加一条元测试:每个 sharing-rule fixture 先过一遍 SharingRuleSchema.safeParse,不通过就 fail —— 这类 fixture-schema 漂移只有这样才不会再来一次。

相关:#4698(母议题)、ADR-0105 D6、ADR-0057 D5。

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    domain:devxDomain lane — seat card indexed by label:pm:seat
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    on Aug 3, 2026
  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    认领(PM 分派)

    • session:session_018iARDqtrhQgz6fVHDeDkbQ
    • 分支:claude/issue-4984-org-axis-redlines-dead-keys
    • 车道:domain:devx(落点 packages/lint)

    ⚠️ 与刚过审的 PR #4985(同包,新增文件为主)基本不相交,但 #4985 尚在队列 —— 从最新 main 开分支,若它先落地无需处理;若碰 authoring-rules.ts 注意它加过一条注册。

    本单要害:error 级红线门禁读的四个键(criteria/filter/sharedTo/recipient)在 .strict() schema 下只作为被拒绝的别名存在 —— 规则对任何 spec 合法的 stack 永不触发,而它自己的测试 fixture 用的正是死键,测试全绿、规则全死。修复除了改键名,必须带上 #4984 正文建议的 meta-test(fixture 必须先过 SharingRuleSchema.safeParse,fixture/schema 漂移即红),否则同样的漂移还会再来一次。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    审核通过 — PR #4992 已取消草稿并挂上 auto-merge(SQUASH)

    死态复现是双层的,比议题写的更深:fixture 不止外层键漂移(criteria/sharedTo 被拒),sharedTo 内层的 id 相对 canonical 的 value 也是被拒别名 —— 只改外层键名仍过不了 schema。D6 ① ② 两条红线在 parsed 与 raw 两层都实测判绿(死),不是推断。

    语义搬迁不是换名字,这正是派发文里要求"逐条核对 D6 原意"的原因:condition 是 ExpressionInputSchema 的三形态(裸串 / {dialect,source} / {dialect,ast}),新 expressionText() 三种都取;并且刻意不读 meta.rationale —— 若用 JSON.stringify(整个 condition),一条"本规则刻意不使用 parent_organization_id"的正当注释就会触发 error 级误报。error 门禁被散文误伤,比漏报更糟。别名一律不做 ?? 兜底(PD#12),且有测试钉住"写别名不判红,交给 spec parse 拒收"。

    meta-test 是结构性修复,且证明了守卫本身不死:每个 fixture 先过真的 SharingRuleSchema.safeParse,把 #4984 之前的两个死 fixture 原样喂进去断言必须抛错。反向验证把规则改回读死键:18 测试 7 红 —— 断言承重。

    实测无新红:两个 example app 与 default-permission-sets 全扫,没有"一直没被拦的真实违规",也没有误伤。

    两个衍生发现,处置各得其所:

    落点 3 文件全在 packages/lint,零共享文件。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions