feat(lint): 判红「声明了但运行期只会跳过」的 sharing-rule condition (#4698) - #4985
Merged
Conversation
…4698) #4698 asks for a "declared but never read" check. The general form is not a lint question: "is this key read?" is only decidable from authored metadata when the consumer's decision procedure is in hand, and a repo-wide grep for a reader is famously not evidence of absence (#4604, #4914). A sharing rule's `condition` is the case where it IS decidable. Its one runtime consumer, `bootstrapDeclaredSharingRules`, does exactly one thing with the key: `compileCelToFilter(condition, { variables: {} })`. A condition that does not lower is not degraded or partially applied — the rule is SKIPPED, never reaches `sys_sharing_rule`, and grants nothing, with one boot WARN as the only trace. So the new rule does not model the consumer. It calls the consumer's own compiler, from the same package, on the same input, with the same options, and is pinned in both directions against a shared corpus. Two ids, because the two authoring mistakes have different fixes: - `sharing-rule-unlowerable-condition` — outside the pushdown subset (`has(...)`, `size(...)`, arithmetic, ternary, cross-object path). This is the issue's measured instance: `has()` is right in an object validation, which is interpreted, and wrong here, where the condition is compiled. - `sharing-rule-runtime-variable-condition` — reads `current_user.*`. Criteria rules are materialised, so there is no current user at compile time; the fix is RLS, a different mechanism, not a respelling. Both `error`, on all three commands, for the ADR-0078 reason SharingRuleSchema's own docblock states: the whole authorable surface is enforced, and this was the one field where that sentence was not true. CEL syntax stays with `expression-invalid` — one field, one complaint. Measured before shipping: every sharing-rule condition and RLS predicate declared anywhere in this repo lowers cleanly, so nothing that works today goes red. The "before" half of the proof is in the test rather than in prose — the full authoring registry runs over the offending stack and every OTHER rule must stay silent, which keeps holding as rules are added. Deliberately out of scope, filed instead of guessed: - RLS `using`/`check` (#4983) — same class, and ADR-0056 D4 explicitly asks for the gate, but its decision procedure needs `sqlPredicateToCel`, which lives in a runtime `@objectstack/lint` must not import. Hoisting the bridge into `@objectstack/formula` comes first; copying it would fork the predicate. - `validateOrgAxisRedLines` reads sharing-rule keys the spec rejects (#4984). - The tenant-scoped declared unique index (issue instance 2) is a driver-sql / objectql fix; tenancy is kernel-injected, not authored, so lint cannot judge it — the existing rule says so in its own comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
…lared-never-read-lint
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 18:42
xuyushun441-sys
enabled auto-merge
August 3, 2026 18:42
This was referenced Aug 3, 2026
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4698
范围已由 PM 收窄:只做
packages/lint内可判定的部分。tenant-scoped unique-index 那条落在 objectql/driver-sql,不在本 PR(核实结论见下)。判据:为什么是这一面,而不是「未读键」通用检查
议题给的不变式很好 —— a key that nothing reads should not validate clean —— 难的不是这句话,是谓词。「这个键被读了吗」只有在消费面的决策过程拿得到手时才是 lint 能回答的问题;多数时候拿不到:仓内 grep 找不到 reader 不构成没有消费者的证据(#4604 登记里那条方法学),消费者还可能住在另一个包、另一个仓,或者一个还没人装的插件里(#4914 的 hot-reload 案例)。
sharing rule 的
condition是判据精确的那一例,这也是它入选、邻居被推迟的全部理由:所以这条规则不建模消费者、不猜、不 grep。它调用消费者自己的决策过程,同一个包(
@objectstack/formula)、同一个输入、同一组 options。判定与 seeder 逐位一致 —— 没有会漂移的启发式,也没有「误报但不是真跳过」的可能。两条 rule id
sharing-rule-unlowerable-conditionunsupportedhas(...)/size(...))、算术、三元、跨对象路径sharing-rule-runtime-variable-conditionunresolved-variablecurrent_user.*分成两个 id 而不是一个 id 两条文案:两者成因不同、修法不同,而 allowlist 和
--json消费者是按 id 认的。第一条是议题实测的那一例:作者照着对象校验规则(那里是解释执行)正确的写法写
has(record.x),到了这里(编译执行)就成了静默失效的安全元数据。第二条是 criteria sharing rule 被物化的必然结果 —— seeder 编译出一份静态criteria_json,evaluator 据此写sys_record_share,编译时根本不存在「当前用户」。parse-error刻意不报:CEL 语法是validateStackExpressions的地盘,它已经在 gate 同一个字段,而且它的报错是专门写语法的,更好。一个字段,一句抱怨。为什么是
errorSharingRuleSchema自己的 docblock 立了这个 flag:在这条 PR 之前,这句话对这个 shape 的每一部分都成立,唯独对承载作者意图的那一个字段不成立。按
lint-flow-patterns.ts顶部写明的严重度标准 —— 没有任何一种读法下元数据按它写的那样行为,就 gate —— 这是error。它 fail closed(收件人少看见,而不是多看见),那是它能一直活着的原因,不是它可以接受的原因。双向证明
改后判红,并点名声明位置(
path: sharingRules[0].condition、where: sharing rule "x" on object "y"),报文说清楚启动时会发生什么(SKIPS the rule at boot/never written to sys_sharing_rule),而不只是「unsupported」。真实被读的必须保持绿(误伤比漏报更糟)—— 覆盖比较运算、
in、&&/||/!、== null、字符串方法,以及从examples/app-showcase与app-crm原样抄来的每一条 shipped condition。判据即消费者的判据,不是它的模型 —— 一个共享语料上双向断言 lint 干净 ⟺
compileCelToFilter({ variables: {} })成功:「改前判绿」这一半做成机械的,不是散文。议题的抱怨正是这个 stack 能过整条工具链,所以只证明新规则判红不够,还得证明此前没有任何东西判过它。测试里跑一遍完整的 authoring registry,要求除本规则外每一条都保持沉默:
这句断言会一直成立下去:哪天有别的规则长到覆盖这个 shape,这条测试就会红,逼人决定归谁管,而不是让 stack 悄悄多出一条重复诊断。
上线前实测:现存代码有没有被判红
没有。 用
isPushdownableCel/compileCelToFilter扫过examples/与packages/里每一条 sharing-rulecondition和每一条 RLSusing/check字面量(85 条),全部可下推。新 gate 不会让任何今天能工作的东西变红。三个实例在当前 main 上的复核
decision节点config.condition(单数)flow-inert-node-condition已经在判它,schemaless-node-config.zod.ts的 strict 拒收也带了逐键处方{ fields: ['name'], unique: true }lintUniqueDeclarations只在两种声明同时出现时触发,只写表级形式的对象拿不到任何诊断driver-sql的normalizeDeclaredIndex;而且租户性是 kernel 在注册时注入的、不是作者写的,该规则自己的注释就说明了 lint 判不了 —— 归 engine 车道,请 PM 移交立单has(...)刻意不做的三件事(已立单,没有猜)
isMatchAllCriteria。 seeder 的第二道闸(降下来但什么都不约束的 filter)住在plugin-sharing,而@objectstack/lint不依赖 runtime。抄一份就是把rule-criteria.ts存在的意义 —— 「什么都不约束」的唯一定义 —— fork 掉。而且这道门根本到不了:编译器能降的每一个 AST 都产出具体字段谓词。这个论断在测试里对着编译器 pin,不是写在注释里。using/check→ ADR-0056 D4 的 RLS authoring gate 从未接线:isSupportedRlsExpression 没有任何非测试消费者 #4983。 同一类、同一个编译器,ADR-0056 D4 还明确要求这道 gate —— 但 RLS 的决策过程isSupportedRlsExpression要先经sqlPredicateToCel桥接遗留 SQL-ish 子集,两个都住在plugin-security。在 lint 里判它只有两条路:import 一个 runtime(禁止),或者抄一份桥接(fork 判据 —— 正是本文件开篇拒绝做的事)。顺带一提:isSupportedRlsExpression除了自己的单测和一行一致性矩阵描述之外没有任何消费者 —— 它自己就是一个 declared-but-never-read 实例,而它恰恰是为了修这个缺陷类写的。condition。 那些是被 CEL 引擎解释执行的,不降成 filter,整个语言都在范围内,「不可下推」在那里什么都不意味着。把这条谓词复用过去会否掉能正常工作的元数据 —— 误伤方向,比缺口更糟。顺手发现,已立单不修(Prime Directive #10)
validateOrgAxisRedLines读的 sharing-rule 键是 spec 拒收的:它取rule.criteria ?? rule.filter和rule.sharedTo ?? rule.recipient,而SharingRuleSchema是.strict()、声明的是condition/sharedWith,那四个只作为被拒别名存在。实测criteria/sharedTo都REJECTED: Invalid input。所以 ADR-0105 D6 ① 在 sharing-rule criteria 这条路径上对任何 spec-valid 的 stack 都不会触发,② 同理。同一缺陷类,方向反过来:consumer 读了没人写的键。现有测试 fixture 用的正是这些废键,所以测试绿而规则死 —— 立单里建议加一条 fixture 先过SharingRuleSchema.safeParse的元测试,免得再来一次。验证
(CLI 那轮第一次跑出 31 个 collection 失败,是新 worktree 里工作区依赖没构建导致的假红 ——
Failed to resolve entry for package "@objectstack/types";pnpm --filter '@objectstack/cli^...' build之后全绿。)改动限于
packages/lint(+ 对@objectstack/formula/@objectstack/spec的只读消费),外加一个 changeset。已从合入 #4951、#4973 之后的最新 main 开分支,并在推送前 merge 了一次origin/main(带进了 #4972 对validate-expressions的改动,同包重叠,故重跑了整套 lint 测试)。Generated by Claude Code