Repository navigation
[metadata-protocol] SysMetadataRepository.publishDraft() 把 draft 清理的全部失败都当「并发发布者已抽走」,静默留下一条永远 pending 的 draft 行 #4981
Description
Activity
🔒 认领:PM 循环第 5 轮
会话:session_01NrmBxj8rK2uGCnh9aipjwX(账号xuyushun441-sys)
分支:claude/issue-4981-publishdraft-drain-discriminate
Worktree:objectstack-issue-4981
域:domain:engine
文件面:packages/metadata-protocol/src/sys-metadata-repository.ts(publishDraft)及同目录测试throw-vs-report 裁定(issue 单开的那个问题):只报不抛。理由沿两轴:(1) delete 在 put 提交之后,抛出会把一次已持久化成功的发布谎报成失败,且诱发调用方重试 —— 重试恰恰触发「陈旧 draft 再发布」这条真正的数据伤害路径;(2) 防 AI 错轴:响亮 + 机器可读的部分成功信号让调用方无法把缺口读成良性,#4969(今日先例,维护者已过目)同形 ——「决策成立,但绝不静默」。判别按 #4728/#4825/#4867 族:仅 ConflictError / 行已不存在静默;其余 error 级上报后果与修复动作。若要加结构化字段而它构成 spec 契约变更 —— 本轮禁区,退回 log-only 并报告。
⛔spec/**/protocol.ts零改动;releases/ 零触碰。
Generated by Claude Code
🔁 接手记录:原 dev agent 因会话额度耗尽被杀(不可恢复),worktree
objectstack-issue-4981与未提交改动完好。按 worktree 接手协议派新 agent 继续 —— 同一认领、同一分支,本条为交接记录,原认领评论继续有效。会话:session_01NrmBxj8rK2uGCnh9aipjwX。
Generated by Claude Code
✅ 验收通过 —— PR #5025(draft,CI 跑完入队);接手协议全程执行
对照实际 diff 复核(5 文件,+650/-14;⛔ spec / protocol.ts / releases 零触碰,逐一核实):
- 接手甄别做实了:原 agent 死于变异验证的「Restoring」瞬间,接手者逐 hunk 对照确认工作树里已无故意改坏的代码,而非盲信遗留状态。
- 判别按裁定落地且比裁定更细:ConflictError 的两个分支都论证了良性(actualHead===null = 并发发布者已抽走;head 不同 = 期间存了更新的 draft,更不能删);其余失败 error 级点名孤儿 artifact + 后果 + 处方,原因附队。
- 只报不抛 + 机器可读信号:
draftDrainFailed可选字段 —— 且 dev 核实了裁定第 2 条的前提:promoteDraft 是 class-only 方法,不在 MetadataRepository 接口、不在 protocol.ts、不在 spec —— 非破坏性附加成立,不是宣称。 - 裁定第 3 条的便宜半边白拿:put() 的同 hash 短路已天然自愈「active 未变时的下一次 publish」,补 pin 测试;贵的半边(内容上与真实待发布不可分)明确不猜,写进 PR 正文作跟进项 —— 这个不扩权的判断正确。
- 变异验证从零重做、双向:盖毯 → durability 门禁 + 5 测试红;删豁免 → 恰好 2 条良性用例 + 判别器测试红(静默半边同样被钉);恢复后 byte-identical。
dropPromotedDraftRow具名并入门禁台账(11 seams 全响)。
CI 绿后转 ready 入合并队列。
Generated by Claude Code
- added a commit that references this issue
on Aug 4, 2026 ✅ 返工验收通过(HEAD
92e0623)—— CI 绿即入队- 修法精确:仅 +7 行 MEASURED DEBT 基线条目(照 fix(metadata-protocol): never invent event_seq/version from a failed history read (#4867) #4980 同包同因先例),
closes指向 metadata-core 下沉路线([engine-double-contract] 四条 metadata-protocol 基线条目的closes指向一个不可能的动作:加 @objectstack/objectql devDependency 会让 turbo 直接判环 #4987),原实现零改动; - 诚实度记一笔:条目明写「成环系静态核实(objectql 的 dependencies 含 metadata-protocol),未做 turbo 实测」—— 不冒领没做过的测量,这是台账应有的纪律;
- 逃逸根因自认并修正:上轮门禁清单是手挑的、漏了 engine-double-contract;本轮从 package.json 枚举全部 28 条,25 过,3 红均诊断为与本分支无关(两条是新工作树未 build 的 §9 陷阱,build 后绿;
check:objectui-pin-fresh为 main 上既有、只挡 release PR,已挂 Release process: prevent frontend (objectui) changesets being missed when the console pin lags #3340,不重复立案); - 顺带观察(另一条目的「four siblings」散文计数已成五)刻意不顺手改热点台账 —— 正确。
Generated by Claude Code
- 修法精确:仅 +7 行 MEASURED DEBT 基线条目(照 fix(metadata-protocol): never invent event_seq/version from a failed history read (#4867) #4980 同包同因先例),
- added a commit that references this issue
on Aug 4, 2026 - added a commit that references this issue
on Aug 4, 2026 - added a commit that references this issue
on Aug 4, 2026
发现于 #4867(修同文件两个计数器的
catch { return 1 })。仅记录,未在该 PR 中修改 —— 不属于同一个编号家族,按 Prime Directive #10 单开,未指派。现象
packages/metadata-protocol/src/sys-metadata-repository.ts,publishDraft()里(origin/main 约 665–676 行,#4867 的 PR #4980 之后行号会下移):注释只点名了一个原因(并发发布者已经抽走了 draft —— 这时
delete抛ConflictError,确实良性),catch却吞掉全部失败:连接抖动、超时、权限不足、驱动错误,以及 draft 在此期间被改写导致的parentVersion不匹配。这与 #4728 / #4825 / #4867 是同一族形状 —— 一个良性原因赦免了所有原因,只是这里的数字换成了一行残留数据。后果
publishDraft()返回成功,active 行也确实是对的(这一点注释说得没错),但sys_metadata里那条state='draft'的行还在:publishDraft()会拿这条陈旧 draft 当新内容再发布一次,写出一条内容与 active 完全相同的历史事件(甚至可能把已经被覆盖的旧 body 重新推成 active);按 AGENTS.md「Degradation log levels」的那一问:降级之后系统对外看起来完全正常,而它声称已经清理的东西并没有清理 —— 属于
error那一类,不是warn。建议(与 #4728/#4825/#4867 一致的形状)
按错误类型判别,只赦免真正良性的那一个:
ConflictError(以及「行已不存在」)—— 并发发布者已抽走,静默,这正是注释里写的那个情况;error上报后果(draft 行残留、UI 会继续显示未发布改动、下一次 publish 会重复发布)与修复动作,然后决定是抛出还是仅上报。这里与 [metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867 不同,delete在put提交之后,抛出会把一次已经成功的发布报成失败,所以「抛 vs 只报」需要判断,不能照抄 [metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867 的结论 —— 这也是本条单开而不是塞进 [metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867 的原因之一。参考
nextEventSeq()/nextItemVersion(),PR fix(metadata-protocol): never invent event_seq/version from a failed history read (#4867) #4980)DatabaseLoader.nextEventSeq(),PR fix(metadata): 历史序号 event_seq 不再从一次失败的读里凭空发号 —— 只有「表还没建」可以从 1 开始 (#4825) #4872)、[metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修 #4728(ensureSchema())、[convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632(降级日志级别规则 + gate)