Skip to content

undoable: true on an action without operation is accepted and silently un-anchored #19297

Description

@os-litant

维护者速读

一个动作可以写 undoable: true(「做完给我一个撤销按钮」)。本仓的框架运行时只在动作同时写了 operation: 'update' 时才真的去拍快照;而我们自己钉住的控制台(.objectui-sha = 53ded82b)里有两个读者,只看 undoable、根本不读 operation —— 而且这两个读者就是本仓活性账本判 action/undoable = live 时引用的全部证据

所以原卡那句「写了 undoable 却没有 operation ⇒ 什么都不会发生」不成立。真实情况是:有两拨读者,各自的锚不一样。

这就把一张本以为是「补个拒绝」的卡,变成一个契约形状问题:undoable 的合法集,该按哪一拨读者来定?

本席没有让 dev 写任何代码,因为按原卡方向改会删掉今天活着的行为:

  • 我们已发布的技能示例 ReassignLeadAction(skills/objectstack-ui/rules/actions.md,挂着 os:check)正是 type: 'api' + undoable: true + 没有 operationdefineAction 就是 ActionSchema.parse,所以原卡的拒绝会让我们自己发的示例在 import 时就抛错
  • 控制台里每一个 type: 'api' 的可撤销动作都会被拒。
  • 本仓活性账本对 undoablelive 判定会当场失去它的依据。

四个选项,代价都已测量:

  • A —— 原卡方向:undoable: true 必须配 operation: 'update'。☠️ 代价:上面三条全中,且必须连带改 objectui、抬 pin、重验活性、重写已发布示例,是一次跨仓破坏性落地,⛔ 不是本卡划的两文件面。
  • B —— 让运行时在没有 operation 时也支持 undoable。☠️ 测到的反证:框架对 type: 'api' 根本没有服务端派发(action-execution.ts 自己的注释:UI-only types (url, modal, form) and api have no server dispatch here),所以在这些文档真正被编写的地方,加宽后的运行时没有东西可以锚
  • C —— 精准拒绝:只拒绝没有任何读者能锚的组合,即 operation !== 'update' type !== 'api'。实测:拒掉 scripturl 两种真正悬空的写法,apioperation: update、已发布示例全部照常通过。仍是收窄 ⇒ 仍要至档复核与 changeset,但删掉的活行为是零⚠️ 代价我不能替您裁:它等于把「type: 'api' 是控制台能捕获的形状」这条控制台派发事实写进 spec
  • D —— 不改 schema,只改说明文字:undoable.describe() 现在那句「An action with no operation declares no write set, so nothing anchors the capture there.」实测为假,连同活性账本条目一起改口径。最便宜也最准确,但对真正悬空的那一类(script / url / flow / modal / form)北极星第 4 条仍然没被满足

我荐 C,并把 D 的文字改正并进同一个 PR。 请回一个字母:A / B / C / D

os-decision-facets

  • ① 项目长远合理性:北极星〈优先级〉4 要求「声明了的…在运行时兑现」。undoable 被兑现的 —— 只是兑现它的是控制台而不是框架运行时。⇒ 真正的缺陷是契约没有说清楚谁来兑现,不是「没人兑现」。C 把合法集收到「至少有一个读者能锚」,是唯一让声明与现实对齐而不撤回能力的走法。⇒ 本棱指向 C
  • ② 实际业务拉动:⚠️ 本席测不到有多少人在写 type: 'api' + undoable,但测得到我们自己在已发布技能里教了这个写法。A 会让照着我们文档写的人在 import 时炸掉。⇒ 这一棱不是「未知」,是已知的负拉动,指向 ⛔ 不选 A
  • ③ 防 AI 犯错:现状对 script / url 上的 undoable静默放行 —— AI 作者写了,既不被拒也拿不到处方,正是北极星第 4 条禁止的形状。A、C 都终结这一类静默;D 不终结。⇒ 指向 A 或 C,而 ② 已排除 A。
  • ④ 创业阶段不扩散:「remove 优于 declare-and-maintain」这一棱表面指向 A(撤掉这个键的一半用法)。⛔ 本席不据此翻字母:它管的是,而 A 撤掉的是一个已发布、已教、已被活性账本判活的能力,那是撤能力不是撤键。C 新增的判定是一条 refine,不是一个新子系统。

自检行

只看①选 C;②③④ 是否翻转: —— ② 已知负拉动排除 A,③ 与①同向,④ 反向但它管的是键不是能力。


⚠️ 以下是原卡正文,保留不删 —— 其中被证伪的那一句已就地划掉并给出读数,因为本卡的整个框架是从它长出来的。

Path: P3 | 那条路第 3 步「验证响亮拒绝错的」 | undoableoperation 被静默接受

What was measured

At objectstack-ai/objectstack head 889d9c7f22613e73d5a58d5eeacc22c9d907f4a7 (PR #19283, card #19148), an isolated at-tier contract review re-took the following on the tree:

  • ActionSchema.undoable is a plain optional boolean. The refine chain in packages/spec/src/ui/action.zod.ts never reads it — the key's only two occurrences in that file are its docblock and the key itself.
  • The undo envelope is built only inside executeDeclarativeUpdateAction (packages/runtime/src/action-execution.ts), which is reached only when operation is set.

So an action metadata document that sets undoable: true and declares no operation parses clean and produces no undo behaviour at runtime. The value is accepted and un-anchored.

FALSIFIED — verification read by this seat at 2026-09-20T20:12Z — measured on tip 488f4f54, and re-verified first-hand by the seat, ⛔ not taken from the dev's report. The document parses clean — that half stands. 「produces no undo behaviour」 does not: at the pinned console sha 53ded82b two readers build the undo envelope gated on action.undoable alone, with zero action.operation hits in either file — packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:487 and packages/app-shell/src/views/RecordDetailView.tsx:831. Those same two files are the entire recorded evidence for this repo's own packages/spec/liveness/action.json verdict props/undoable: live. ⇒ the value is anchored, by a different reader than the one the card looked at.

Why PR #19283 does not close it

#19283 rewrites the undoable describe text so that it documents this state accurately. The reviewer's finding, verbatim:

The new sentence documents that undoable: true on an action with no operation is accepted and un-anchored. The dev records this as "resolved by this diff, no trap left". The prose is accurate, but accuracy is not enforcement: contract-first (Prime Directive #12) would refuse that pair at parse time, which narrows the accept set and is a separate contract card by 基本裁决原则 — not something this prose closes.

Direction is not settled here

Refusing the pair moves the accepted metadata set, so this is a contract-shape question, not a mechanical fix. It is filed for triage to grade and route; this seat chooses no direction.

Dedup words

undoable, un-anchored, ActionSchema, executeDeclarativeUpdateAction, undo envelope

Origin: at-tier contract review of PR #19283, comment 5749186061 (2026-09-20T10:18Z), boundary flag 1.
Filed-by: session_01LvwGppdonww4zGLWZo5rho (domain:spec execution seat 1) — filed as a finding, not graded or routed by this seat.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions