Skip to content

validate/lint have no check for "declared but never read" metadata — three instances found in one app in a day #4698

Closed

Description

@os-zhuang

Reported from HotCRM, where a backlog sweep turned up the same failure shape five times in one day. Each instance passed os validate, os lint, tsc, and a full test suite. Filing the pattern rather than the instances, because the instances are app-side and the blind spot is not.

The shape

A key is authored, is schema-valid, reads as meaningful to anyone opening the file — and is never consumed by the runtime. The declaration and the behaviour disagree, and nothing in the toolchain notices, because every check verifies that what is declared is well-formed, never that it is read.

Three measured instances

1. decision node config.condition (singular). Every one of HotCRM's 24 flows spells a decision node's test as config.condition. The builtin decision executor reads config.conditions — plural array — and the edges. So the singular key is inert on all of them; branching happens to be correct only because the same expression is duplicated on the outgoing edges. Passes validate and lint. (hotcrm#642)

2. Table-level { fields: ['name'], unique: true } on a tenant-scoped object. normalizeDeclaredIndex() keeps declared-index columns verbatim, so this emits a platform-wide UNIQUE (name); only field-level unique: true gets the (organization_id, …) composite. The result is that two organizations cannot both have an account named "Acme Corp" — a multi-tenancy defect authored in one line that looks identical to the correct spelling. The existing unique/double-declaration lint fires only when both forms are present, so an object carrying only the table-level form gets no diagnostic at all. (hotcrm#625)

3. has(...) inside a sharing-rule condition. Sharing conditions are compiled to pushdown filters via compileCelToFilter; function calls are outside the translatable subset, so a guarded rule becomes untranslatable and the seeder skips it — the rule exists in metadata, is absent from sys_sharing_rule, and the only signal is one WARN line at boot. An author following the guidance that is correct for object validations produces silently inert security metadata. (hotcrm#621, #633)

Two more from the same sweep, same shape, different mechanism: a validation predicate that aborts is skipped rather than failed (objectstack#4649), and a flow variable that is declared is not thereby bound (objectstack#4697).

Why this is a platform concern

The app-side fixes are all in. What is not fixed is that the next app — or the next agent writing metadata for this one — will author the same shapes and get the same silence. These are exactly the errors generated metadata is most prone to: each is a plausible-looking key that a model would produce by analogy from a neighbouring declaration.

Suggested direction

A conservative "unread key" diagnostic would have caught instances 1 and 3 outright: for each metadata node type, the set of keys the runtime actually consumes is knowable from the executors, and a key outside that set is either a typo or a misconception. Instance 2 wants a narrower rule — a declared unique index on a tenant-scoped object whose columns do not lead with the tenant field is almost certainly a mistake, and the existing lint already knows the concept, it just only fires in the two-declaration case.

Whatever the mechanism, the useful invariant is the one the ledger discipline already applies to spec properties: a key that nothing reads should not validate clean.

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Aug 3, 2026
  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    认领(PM 分派)—— 范围收窄

    范围:只做 packages/lint 内可判定的部分。三个实例里 tenant-scoped unique-index 那条的修复落在 objectql,归 domain:engine 车道,须另立单移交,不进本 PR。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

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

    三例复核的结果:一例已过期、一例移交、一例落地 —— 每个处置都有实据。

    • 实例 1(decision 节点 condition 单数键):已被 A decision node has three declared ways to route a branch and two of them do nothing — app-crm's convert-lead guard runs both branches #4414 修掉,flow-inert-node-condition 规则 + per-key 严格拒绝都在 main 上。报告而非重修,对。
    • 实例 2(tenant-scoped unique index):仍活着但不属 lint 层 —— 判定所需的事实("这个对象是否 tenant-scoped")不在作者写的元数据里,organization_id 是注册期 kernel 注入的,lintUniqueDeclarations 自己的注释就写着拒绝猜测的理由。修复点在 driver-sql 的 normalizeDeclaredIndex,我已另立 engine 车道单移交。
    • 实例 3(sharing rule 的 has(...) 条件被 seeder 静默跳过):本 PR 落地。

    谓词的构造方式是这单最值得记的:lint 调用的是消费者本人的那次调用 —— 同一个包(@objectstack/formula)、同一个函数(compileCelToFilter)、同一个入参形状。不是消费者的模型,不是 grep;判决与 seeder 按构造逐位一致,没有可漂移的启发式,也不存在"不是真 skip 的误报"。还有一条 10 源语料的双向一致性断言(lintIsClean === compileCelToFilter(...).ok)钉住这个性质。与 #4948 的共享 dispatch 谓词、#4934/#4941 的死根断言同族:判据只有一份,消费与检查读同一份。

    "before"不是一句话而是常驻测试:全量 authoring registry(25+ 规则)跑一个唯一缺陷是坏 sharing 条件的完整 stack,断言 finding 列表恰好等于新规则那一条 —— 改动前这个列表是空的,正是本单的指控,现在它是回归测试。还带非空断言防 registry 静默停摆。

    边界五条全部有记录(RLS 面、isMatchAllCriteria、tenant unique、flow/hook 条件、runtime-publish 面),其中 flow/hook 那条有实测支撑:showcase 里 16 条不可下推的条件全部正确(读 previous.*,解释执行不下推)—— 把它们纳入就是误伤。全仓 85 条相关字面量扫过,main 零红,是测出来的不是假设的。

    两个衍生发现都够硬,已分别立单:

    落点 5 文件全在 packages/lint,零共享文件;55 文件 / 1007 测试全绿。


    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

domain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions