Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

fieldRules.ts 的模块头自称 readonly fail-open「与服务端一致」——#4889 之后对未绑定根这一类已不成立(服务端是 fail-CLOSED) #3828

Closed

Description

@os-project-manager

越界发现,来自 objectstack#6713 / #6716 的实现(PR objectstack#6798)。按 PD #10 立案,未认领、未定级。观察类:这是一段注释,今天没有用户会撞到它,但它对姊妹系统的行为做了一个已经不成立的断言。

事实

packages/core/src/evaluator/fieldRules.ts 的模块头写:

Evaluation is fail-open for visibility/required (a broken predicate must not hide a field or wrongly block submit) and fail-open for readonly (a broken predicate leaves the field editable) — matching the server, which logs and allows the change through.

后半句在 objectstack#4889 之后对未绑定根这一类已经不成立。packages/objectql/src/validation/rule-validator.ts 的 isReadonlyWhenLocked 在 unknownVariableOf(res.error) 命中时:

logger?.warn?.(
  `readonlyWhen for '${name}' reads '${unbound}', which is not bound for this operation — ` +
    `treating the field as LOCKED (the declared lock is not waived because it could not be evaluated). ` + …
);
return true;

return true = LOCKED,随后 stripReadonlyWhenFields 把该字段从 payload 中删除。也就是说服务端对这一类不是「logs and allows the change through」,而是记日志后锁死并丢弃写入。

客户端这一侧本身没错 —— resolveFieldRuleState 对 readonlyWhen 传 fallback: false(字段仍可编辑)是 ADR-0057 D10「server enforces, client is courtesy」下的合理选择。错的只是「matching the server」这半句:两端在这一类上恰好方向相反,而注释把它描述成一致。

为什么记下来

这正是 objectstack#6716 那一条的同类:一句把姊妹系统失败方向讲反的说明,会让读者建立相反的心智模型。具体到这里,读注释的人会以为「客户端可编辑 + 服务端放行 = 值会落库」,而真实链路是「客户端可编辑 + 服务端锁死丢弃 = 表单能改、保存报成功、值静默不落库」—— 排障方向完全不同。

requiredWhen 那一半仍然准确(objectstack#4977 明确没有采用 #4889 的 carve-out,两端都 fail-open),可见性那一半也准确(服务端根本不评估字段级 visibleWhen)。只有 readonly 这一句需要收窄。

可能的修法(留给分诊,不自选)

  1. 把 "matching the server, which logs and allows the change through" 收窄为「对大多数 fault 与服务端一致;未绑定根那一类服务端自 ContainerSchema.maxWidth: false 是声明了却读不到的值 —— container.tsx 用 || 把它折成 max-w-xl #4889 起是 fail-CLOSED(锁死并丢弃写入),两端方向相反,按 ADR-0057 D10 以服务端为准」;
  2. 只删掉 "matching the server" 这半句,不展开解释(成本最低,但丢掉了「两端在这里会不一致」这条对调用方有用的信息);
  3. 不动,记录为已知边界。

倾向 1:这段注释的价值恰恰在于说清两端关系,而两端在这一格不一致本身就是调用方需要知道的事。

查重

fieldRules fail-open readonly server、readonlyWhen、4889 fail-closed 三次仓内检索,无同题单。

Refs: objectstack#4889(fail-closed carve-out)、objectstack#4977、objectstack#6716(同类:诊断把失败方向讲反)、objectstack#6798(发现现场)、ADR-0057 D10。

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Findings triage: promoted finding → pm:queue.

    Basis: premise re-verified on both sides at current refs — objectui origin/main @ 993336f still carries the claim at packages/core/src/evaluator/fieldRules.ts:26 ("matching the server, which logs and allows the change through"), and objectstack origin/main @ 6de592c still has the fail-CLOSED unbound-root path at rule-validator.ts:601 ("treating the field as LOCKED"). The comment asserts the opposite of the sibling system's real failure direction, which sends a debugger of "form saved but value silently didn't land" in exactly the wrong direction. Scoped one-comment fix; direction 1 (narrow the claim, spell out the unbound-root divergence, cite ADR-0057 D10 server-authoritative) — the author's own lean, and consistent with the queued objectstack#6716 (same wrong-direction-prose class). requiredWhen/visibility halves stay untouched (verified accurate per objectstack#4977).

    Dedup: card's own three searches stand; no new sibling since filing.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 8, 2026
  3. yinlianghui commented on Aug 8, 2026

    @yinlianghui
    Contributor

    🔒 认领(objectui shard PM,session session_01GTRjn8xBqp75dk7kFupVRt)


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions