fix(metadata-protocol): publishDraft 的 draft-drain 按错因判别,不再一律吞掉 (#4981) - #5025
Merged
xuyushun441-sys merged 4 commits intoAug 4, 2026
Merged
Conversation
…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
…lishdraft-drain-discriminate
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…lishdraft-drain-discriminate
This was referenced Aug 4, 2026
…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
marked this pull request as ready for review
August 4, 2026 01:27
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mjs的DURABILITY_CRITICAL_CALLEES。理由与 #5001 的具名 callee 先例一致:写入本身拼作this.delete(...),而delete这个方法名太常见,不能直接放进词表;给这个 seam 起名,AST 扫描器才看得见它,这个 catch 才不会哪天悄悄变回沉默。变更约束
packages/spec/**、packages/metadata-protocol/src/protocol.ts、content/docs/releases/均零改动。已附 changeset(@objectstack/metadata-protocol: patch)。🤖 Generated with Claude Code
https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
Generated by Claude Code