Repository navigation
fix(spec): refuse undoable: true where no runtime fulfils it - #19635
Conversation
… console The `undoable` describe sentence and the comment above it both claimed that an action with no `operation` has nothing anchoring the capture. Measured false: the pinned console builds the undo envelope for a `type: 'api'` action from `undoable` alone, and those readers are the whole recorded evidence for this key's `live` liveness verdict. State the closed fulfillable set instead. Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
`undoable` was accepted on every action shape, and on most of them nothing ever built an Undo. Close the accepted set to the two shapes some runtime fulfils — `operation: 'update'`, snapshotted by the framework runtime, and `type: 'api'`, snapshotted by the pinned console — and refuse the rest at parse time with a remedy naming both. A blanket requirement of `operation: 'update'` is deliberately not the rule: it would refuse the published `ReassignLeadAction` example at import time and every console api action with undo. Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
For the `type: 'api'` shape the snapshot is the console's, not the framework runtime's. Name both fulfilling shapes on the same bullet, so an author reads the closed set the schema now enforces. Net 0 lines. Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
…escribe Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
…iling The published catalog's token ratchet had zero headroom on this file (8432 bytes = ceiling 2108 exactly), so the longer phrasing red it at +35 tokens. Tighten the same correction to 2107 tokens: net 0 lines, -4 bytes, -1 token, and no surrounding prose touched. Claude-Session: https://claude.ai/code/session_01GBPc6CYjy3tNTYhaKCmqg4 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3429a0eb8f06b00b9e0a70d69410d3520eb45984 && git checkout 3429a0eb8f06b00b9e0a70d69410d3520eb45984
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6ffccc51e2f36c24cbabea0a5242d0001a8d00f3 c8d4d651309b58bb63496bcf1a6656b77f5cf823 && git checkout -B drift-repro 6ffccc51e2f36c24cbabea0a5242d0001a8d00f3 && git merge --no-ff c8d4d651309b58bb63496bcf1a6656b77f5cf823
node scripts/docs-audit/affected-docs.mjs --json 6ffccc51e2f36c24cbabea0a5242d0001a8d00f3
|
Contract reviewServed-tier: In-seat at tier ( ① Derived judgmentsThe narrowing is exactly letter C. Branch if (data.undoable === true && data.operation !== 'update' && data.type !== 'api') {⇒ refused iff What it does NOT withdraw — the thing a narrowing must prove: The remedy names both fulfilling shapes, as the ruling requires — read at A defaulting subtlety the dev handled rather than tripped on ( The falsified describe is repaired at the source of the falsification. The old sentence 「An action with no The
The diff is one bullet line replaced — ⛔ no surrounding prose rewritten — and the replacement is shorter and more precise than what it replaced (it names both snapshot owners and the refusal). ⇒ no ratchet was raised. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)—— 本 PR 需要您的一个动作席位已对照自己读的 diff 校正了草稿并填入席位意见。头 改了什么 —— 「做完给我一个撤销按钮」( 为什么改 —— 声明了却没人兑现,是北极星第 4 条禁止的形状:作者写了,既不被拒、也拿不到处方。而最直觉的修法(强制配 风险与代价(含回滚) —— 风险低,且是量过的:四份真实写法无一被拒;CI 35 条检查全完成,33 绿 2 跳过,零非绿;消费包套件(runtime / lint / objectql / metadata-protocol / 两个示例应用)全绿。代价是把「 席位意见 —— 建议批准。 三个理由:① 这是一次收窄,而收窄唯一要证明的事(没删掉活行为)是量出来的,不是推断的;② 被证伪的那句 describe 是在它自己所在的那一行修好的,不是在别处加个补丁绕过;③ 消融实验方向预测在前、结果在后,而且 dev 顶回了我两处措辞错误(见下),说明它在读门禁而不是读我的散文。
⭐ 顺带一件关于我自己的事 —— 我给 dev 的预算写错了单位:我写「 你要做的 —— 一个动作:本 PR 触到 Generated by Claude Code |
Fixes #19297
Clause-②: yes
Ruling
5754211444(batch #203 item 5 · letter C · 「203 同意」), letter C plus D's two text corrections in one PR.What this does
undoable: trueon a registeredactionis now legal only on a shape some runtime actually fulfils, and refused at parse time everywhere else. One.refine()onActionSchema, readingoperationandtypetogether, with a remedy naming both fulfilling shapes.operation: 'update'type: 'api'script/url, and the dormantflow/modal/form, withoutoperation: 'update'D's two corrections ride along: the
undoable.describe()sentence (and the code comment above it carrying the same claim) said an action with nooperationhas nothing anchoring the capture — measured false against the pinned console; andskills/objectstack-ui/rules/actions.mdattributed the snapshot to 「the runtime」 on a bullet whose own example is the api shape.The ablation the ruling names
Three legs, each a real on-disk mutation of the refine through
scripts/ablation-replace.mjs(anchor hit proven, blob hash before/after printed, restore provengit diff HEADempty). Two populations, each named so a count never travels without its definition:ActionTypemember withundoable: trueand nooperation, plus theoperation: 'update'shape.undoable: true: the publishedReassignLeadActionskill example,packages/spec/src/ui/action-row-update.test.ts:119, andpackages/runtime/src/action-declarative-update.test.tsat :75 and :533.operation: 'update')ReassignLeadActionRefused at L1, by name:
script,url,flow,modal,form. Accepted:api, andoperation: 'update'. The ruling's expectation holds exactly — the published example and api actions stay accepted, and the refused set is the one the ruling's own binding text enumerates (「measured asscript/urland the dormantflow/modal/form」). L2 reproduces, first-hand, the breakage the ruling cites as its reason for refusing A.One note on the dispatch's shorthand 「blanket refusal 1 → 5」: it does not reproduce as a single count moving, because its two numbers belong to two different populations. Both numbers are real and both are above — 5 is the precise refine's refused type-shapes, 1 is the live document a blanket refusal would break. No fork: the ruling's stated, testable expectation is met.
Risk, and what was measured rather than assumed
packages/spec/dropped-refinements.baseline.jsonis unchanged —ui/Actionalready carries a root site (in) from the existing refine chain, so a second refinement at the same position adds no new site;check:authorable-surfaceand the spec build are green without touching it.packages/spec/liveness/state-counts.mdis unchanged too, andcheck:livenessis green: theprops/undoablerow's status (live) and its objectui-side evidence are untouched by this diff, so no re-citation is owed here.../objectuiis not checked out in this container, so the two console readers at the pinned sha were not read first-hand. Every statement about them in this PR is carried from the card's recorded evidence and frompackages/spec/liveness/action.json, not re-measured. Stated rather than implied.ActionSchema's chain plus its pin test. The prose corrections are independent of it and correct on their own.Skills line budget — both readings
The dispatch's ceiling was +2 net lines. The published catalog also carries a token ratchet with zero headroom on this file, which the first phrasing red at +35 tokens; the correction was tightened to fit rather than the ceiling raised.
skills/objectstack-ui/rules/actions.md— linesskills/objectstack-ui/rules/actions.md— tokens (ceil(utf8 bytes / 4), ceiling 2108)skills/objectstack-ui/rules/actions.md— bytesskills/objectstack-uipackage — linesSKILL.md— linesskills/tree — linesNo surrounding prose was rewritten and no ceiling was moved.
check-skills-token-ratchetandcheck-skill-line-ratchetare both green.Verification
All readings at
c8d4d6513, the final commit.Gates — derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, every derived command run with its exit code captured before any pipe, reconciled with--ran:All 115 exit 0. Nine first returned exit 3 (
PREREQUISITE NOT MET— a gate that reads built output, not a finding) and were re-run to a real reading after the dependency closure and then the full build.Suites — the spec lane charter's consumer requirement, each package named with a real reading:
@objectstack/spec(own)typecheckexit 0@objectstack/runtime@objectstack/lint@objectstack/objectql@objectstack/metadata-protocol@objectstack/example-crm@objectstack/example-showcase@objectstack/example-todo@objectstack/clibin/entry and no spawn helperpnpm lint(eslint . --no-inline-config, the whole repo) exit 0 atc8d4d6513.New pin:
packages/spec/src/ui/action-undoable-fulfillment.test.ts, 20 cases — every unfulfilled shape refused at theundoablepath, the remedy naming both fulfilling shapes and the runtime that fulfils each, refusal through the registeredactionmetadata door, both fulfilled shapes accepted, andundoableabsent orfalseuntouched on every type.Acceptance notes
undoableprose line inaction.zod.ts(「undoablecaptures the prior values of exactly the fields written.」) was read and left as written: it sits inside theoperationkey's own EXECUTOR CONTRACT block, scoped to theoperation: 'update'shape, where it is true. It is not the sentence ruling D calls measured-false.claude/issue-19297-undoable-requires-operationwas inspected read-only and carries no commits of its own — its tip488f4f54is an ordinarymaincommit. Nothing of direction A exists on it to inherit. Not branched from, not pushed to.ActionSchema's chain alone, matching the existing scoping precedent beside it; an inline action is not a registered action and has no console reader to speak for. The.describe()renders into the InlineAction reference table too, so its refusal sentence is qualified 「on a registered action」, the same way theconfirmTextdescribe is.维护者速读(草稿)
改了什么
「做完给我一个撤销按钮」这个开关,以前写在哪种动作上都收,可真正会去拍快照的只有两种。现在把合法范围收到这两种——一种由框架运行时兑现,一种由我们自己钉住的控制台兑现——其余写法在解析时就被拒,拒绝话术里同时点名这两条出路。顺带改正两处说明文字:schema 里那句「没有 operation 就没有东西可锚」实测为假,技能文档里把快照归给「运行时」的那句,对 api 那种形状其实是控制台干的。
为什么改
声明了却没人兑现,是北极星第 4 条禁止的形状:作者写了,既不被拒也拿不到处方。但最直觉的修法——强制配
operation: 'update'——实测会当场炸掉我们自己已发布的示例和控制台里每一个带撤销的 api 动作,所以没有采用。这次收的是「一个读者都没有」的那部分,删掉的活行为是零。风险与代价(含回滚)
风险低:全仓四份真实写法无一被拒,例子应用、runtime、lint、objectql、metadata-protocol 的套件全绿。代价是把「api 形状由控制台兑现」这句话写进了 spec——裁决已认定这是契约陈述而非越界。回滚是一次 revert:那条规则是独立的一段,两处文字改正即使单独留下也是对的。派发词担心的两个生成账本(
dropped-refinements、state-counts)实测都没被动到,不存在和其它 PR 抢同一个文件的问题。一处如实说明:姊妹仓 objectui 没有检出到这个容器里,控制台那两个读者本次未能亲自读到,相关陈述沿用卡片已记录的证据。席位意见
你要做的
回一句可落地即可。本 PR 触到
skills/**,按 Tier H 规则保持 draft,不合并、不排队、不开自动合并;落地等的是至档合约复核加上您的批准。Generated by Claude Code