Skip to content

fix(metadata-protocol): publishDraft 的 draft-drain 按错因判别,不再一律吞掉 (#4981) - #5025

Merged
xuyushun441-sys merged 4 commits into
mainfrom
claude/issue-4981-publishdraft-drain-discriminate
Aug 4, 2026
Merged

fix(metadata-protocol): publishDraft 的 draft-drain 按错因判别,不再一律吞掉 (#4981)#5025
xuyushun441-sys merged 4 commits into
mainfrom
claude/issue-4981-publishdraft-drain-discriminate

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4981

问题

发布一份 draft 是两次写:先由事务化的 put 把 body 提升到 active 行,再用 delete 排干(drain)已经冗余的 state='draft' 行。这第二次写被一个裸 catch {} 包着,而它的注释只点名了一种成因——「可能有并发发布者已经排干了 draft」——行为上却赦免了全部成因:连接中断、语句超时、权限不足、驱动故障、parentVersion 不匹配。

后果是一种静默且会自我延续的不一致:publishDraft 返回成功,active 行正确且已持久化,但一条陈旧的 state='draft' 行留在 sys_metadata 里,装着刚刚才发布过的那份 body。没有日志、没有重试,于是 Studio/Setup 会对一个根本没有待发布改动的构件持续显示「有未发布改动」,而下一次发布该构件时又会把同一份已发布的 body 再提升一遍——只要中间有任何人发布过或回滚过,这一次就会覆盖 active 行。

同族问题:#4728 / #4825 / #4867(以一个良性成因赦免所有成因);日志级别规则见 #4632

改动

排干失败现在按错因判别。 ConflictError 保持静默,因为它的两条分支都确实良性,而且它是 delete() 在碰驱动之前用自己的行查找抛出的唯一错误——不是驱动相关的信号:

  • actualHead === null:行已经不在了,正是旧注释描述的并发发布者竞态;
  • actualHead !== draftHash:发布在途中有人又存了一份更新的 draft。活下来的那行不是陈旧幽灵,而是真实的待发布工作,删掉它会毁掉管理员的编辑。此时「有未发布改动」是正确的,所以在这里报后果反而是误报——AGENTS.md 明确写了把非降级升成 error 是隐藏降级的镜像错误。

其余一切失败按 error 级上报,点名孤立的构件、后果与修复动作,并原样附上原始 cause。按 AGENTS.md 的判据这是持久性/一致性降级——系统看上去一切正常,而它声称已经清理掉的东西其实还在——所以是 error 而不是 warn

promoteDraft 依然返回成功,这是刻意的。 排干发生在 put 提交之后,抛出会把一次已持久化成功的发布谎报成失败,并诱使调用方重试——而重试恰恰是那条真正有害的路径,因为它会把陈旧 draft 再提升一次。所以改为在不对发布结果撒谎的前提下把失败暴露出来:除日志外,结果对象上新增可选字段 draftDrainFailed{ ref, draftHash, cause },类型 DraftDrainFailure 已导出),让调用方无需解析日志即可反应。

关于 PM 裁定第 2 条(机器可读信号只能放进现有结果类型的非破坏性槽位):promoteDraft 并未出现在 MetadataRepository 接口里,也不在 protocol.ts 中,它是类自有方法,其返回类型是就地声明的字面量类型。因此新增一个可选字段是纯增量的,干净发布时该字段根本不存在,现有调用方零改动,没有触碰任何 spec 契约形状

关于「下一次发布能否自愈」(裁定第 3 条):便宜的那一半已经成立,并已被测试钉住。 put() 的同哈希短路意味着重新发布一份陈旧 draft 不会写出第二条 history 事件,随后排干成功、行被移除。昂贵的那一半——active 行在此期间被别的发布/回滚改过、以致陈旧 draft 的 body 已不再等于 active——仅凭内容无法与真实待发布工作区分,本 PR 不做猜测,留作后续跟进。

门禁

排干这处写入被提取成具名方法 dropPromotedDraftRow,并登记进 scripts/check-durability-degradation-log-level.mjsDURABILITY_CRITICAL_CALLEES。理由与 #5001 的具名 callee 先例一致:写入本身拼作 this.delete(...),而 delete 这个方法名太常见,不能直接放进词表;给这个 seam 起名,AST 扫描器才看得见它,这个 catch 才不会哪天悄悄变回沉默。

变更约束

packages/spec/**packages/metadata-protocol/src/protocol.tscontent/docs/releases/零改动。已附 changeset(@objectstack/metadata-protocol: patch)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX


Generated by Claude Code

claude added 2 commits August 4, 2026 00:01
…res instead of swallowing all of them (#4981)

The post-promotion drain `delete` was guarded by a bare `catch {}` whose comment
named only the benign concurrent-publisher race while its behaviour amnestied
every cause. A connection blip, timeout or privilege error therefore left a stale
`state='draft'` row behind with no log and no retry: Studio/Setup kept showing
"unpublished changes" for an artifact that had none, and the next publish
re-promoted the already-published body.

The drain now discriminates: ConflictError (the only error `delete()` raises from
its own pre-driver lookup, covering both "row already gone" and "a newer draft was
saved") stays silent; every other failure is reported at `error` level with the
consequence, the remedy and the original cause.

`promoteDraft` still returns success — the drain runs after the `put` committed, so
throwing would misreport a durable publish and invite the retry that re-promotes the
stale draft. The failure is surfaced machine-readably instead, via a new optional
`draftDrainFailed` field on the existing result object.

The write is extracted as a named `dropPromotedDraftRow` callee so
`check:durability-log-level` can see a seam otherwise spelled `this.delete(...)`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 12:46am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…ine-double-contract ledger

`check-engine-double-contract` flagged the new draft-drain suite's fake engine:
its `delete()` does not route through `assertEngineDeleteDispatch`, so the double
is structurally looser than the real `ObjectQL.delete` — the #4434 shape.

The gate's preferred remedy (add @objectstack/objectql to devDependencies) is not
available to this package: @objectstack/objectql depends on
@objectstack/metadata-protocol in `dependencies`, so the edge is CYCLIC and turbo
refuses the graph. That was measured in #4867 by adding the edge and reverting it;
the dependency direction is re-verified statically here rather than re-run.

So this takes the gate's other sanctioned route — a MEASURED DEBT entry, modeled
on the sibling entry #4980 added for sys-metadata-repository.history-counters.ts,
with the same `closes` route: sink assertEngineDeleteDispatch into a package both
sides already depend on (@objectstack/metadata-core), tracked as #4987.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 4, 2026 01:27
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 79822b5 Aug 4, 2026
24 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4981-publishdraft-drain-discriminate branch August 4, 2026 01:34
xuyushun441-sys pushed a commit that referenced this pull request Aug 4, 2026
…d-summary-stale-loud

Conflict: scripts/check-durability-degradation-log-level.mjs — both sides
appended a DURABILITY_CRITICAL_CALLEES entry at the same list position.
Resolution keeps BOTH: this branch's `performSeedWrite` (#4998) and #5025's
`dropPromotedDraftRow` (#4981), with the seed-loader callees kept adjacent.

Verified on the merged state: #5025's seam is unaffected by this branch's
tightening of the rethrow rule. Its catch contains no `throw` at all, so
`rethrows` is false and the "only an unconditional rethrow excuses the seam"
change cannot apply to it — it is judged on log level exactly as before, and
reports loud via console.error in draftDrainVerdict(). Gate: 12 seams, all loud
or rethrowing, exit 0; self-test 13/13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants