发现于 #4825 (修 DatabaseLoader.nextEventSeq())。仅记录,未在该 PR 中修改 —— 落点在另一个包(packages/metadata-protocol),按 Prime Directive #10 单开。
现象
packages/metadata-protocol/src/sys-metadata-repository.ts 有两个同形的 catch:
// nextEventSeq(ctx) — 约 1051 行
return max + 1 ;
} catch {
// Table not provisioned yet (fresh DB) — start at 1.
return 1 ;
}
// nextItemVersion(ref, ctx) — 紧随其后
return max + 1 ;
} catch {
return 1 ;
}
两者都读 sys_metadata_history,都把全部 读失败折成 return 1。
为什么值得单开(比 #4825 更该修,不是更不该)
#4825 修的是 DatabaseLoader —— TSDoc 自称 legacy、非事务的那条路径。这里是 canonical 路径 :#4825 的正文与分诊都把 SysMetadataRepository 称作「历史写入应当收敛过去的地方」。同一个缺陷在目的地上原样存在。
而且这里有两个数字,不是一个:
event_seq —— 历史排序 / 回滚定位的依据。已有 N 行时一次瞬时读失败让下一条拿到 1,与既有行撞号;
version —— nextItemVersion() 的 TSDoc 明说它刻意从 history 取 MAX「so delete + recreate continues incrementing instead of restarting at 1」。一次读失败正好把它恢复成它明确要避免的那个行为 :lineage 从 1 重启,与既有 lineage 行撞号。而 MetadataManager.rollback(type, name, version) 与 POST /api/v1/meta/:type/:name/rollback 正是按 version 定位快照的 —— 撞号之后回滚可能指向另一条记录的同号版本。
关键危害与 #4825 相同,是「落盘的字节是错的 」而不是「字节没落盘」:insert 成功、日志一行没有、系统对外完全正常,错误只在版本顺序里,重试不修、重启也不修。
有一点值得注意但不改变结论:这两个调用都在事务里(put/delete 的 txn body)。事务解决的是并发 撞号,解决不了读失败被折成 1 ——一个成功提交的事务照样可以提交一个错号。
建议
与 #4825 落地的方案一致,直接复用它的判别器,不要另起一套:
⚠️ 跨包复用需要先定一件事:该判别器目前是 @objectstack/metadata 的内部 工具(未从包入口导出),而 @objectstack/metadata-protocol 是另一个包。三个选项,值得在动手前定:
从 @objectstack/metadata 导出(最小改动,但把一个内部工具变成公共 API 面);
下沉到两个包共同的依赖里(@objectstack/types 或 @objectstack/spec/shared)—— 注意 Prime Directive ✨ Set up Copilot instructions #2 :packages/spec 不放业务逻辑,错误分类算不算,需要判断;
在 metadata-protocol 里复制一份 —— 不建议 ,正是 [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825 刻意避免的「同一问题两套判别」。
倾向 2(若落点合法)或 1,由维护者定。
参考
发现于 #4825(修
DatabaseLoader.nextEventSeq())。仅记录,未在该 PR 中修改 —— 落点在另一个包(packages/metadata-protocol),按 Prime Directive #10 单开。现象
packages/metadata-protocol/src/sys-metadata-repository.ts有两个同形的catch:两者都读
sys_metadata_history,都把全部读失败折成return 1。为什么值得单开(比 #4825 更该修,不是更不该)
#4825 修的是
DatabaseLoader—— TSDoc 自称 legacy、非事务的那条路径。这里是 canonical 路径:#4825 的正文与分诊都把SysMetadataRepository称作「历史写入应当收敛过去的地方」。同一个缺陷在目的地上原样存在。而且这里有两个数字,不是一个:
event_seq—— 历史排序 / 回滚定位的依据。已有 N 行时一次瞬时读失败让下一条拿到1,与既有行撞号;version——nextItemVersion()的 TSDoc 明说它刻意从 history 取 MAX「so delete + recreate continues incrementing instead of restarting at 1」。一次读失败正好把它恢复成它明确要避免的那个行为:lineage 从 1 重启,与既有 lineage 行撞号。而MetadataManager.rollback(type, name, version)与POST /api/v1/meta/:type/:name/rollback正是按version定位快照的 —— 撞号之后回滚可能指向另一条记录的同号版本。关键危害与 #4825 相同,是「落盘的字节是错的」而不是「字节没落盘」:insert 成功、日志一行没有、系统对外完全正常,错误只在版本顺序里,重试不修、重启也不修。
有一点值得注意但不改变结论:这两个调用都在事务里(
put/delete的 txn body)。事务解决的是并发撞号,解决不了读失败被折成 1——一个成功提交的事务照样可以提交一个错号。建议
与 #4825 落地的方案一致,直接复用它的判别器,不要另起一套:
packages/metadata/src/utils/schema-sync-errors.ts里的isMissingTableError()([metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825 新增,与 [metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修 #4728 的isSchemaAlreadyExistsError()共用同一个 code / errno / message +cause链匹配器);error上报后果(序号将从 1 重新发号、与既有行撞号、版本顺序与回滚目标不可信)并抛出,让调用方决定。@objectstack/metadata的内部工具(未从包入口导出),而@objectstack/metadata-protocol是另一个包。三个选项,值得在动手前定:@objectstack/metadata导出(最小改动,但把一个内部工具变成公共 API 面);@objectstack/types或@objectstack/spec/shared)—— 注意 Prime Directive ✨ Set up Copilot instructions #2:packages/spec不放业务逻辑,错误分类算不算,需要判断;metadata-protocol里复制一份 —— 不建议,正是 [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825 刻意避免的「同一问题两套判别」。倾向 2(若落点合法)或 1,由维护者定。
参考
DatabaseLoader.nextEventSeq(),同形,已修)ensureSchema()的同形缺陷,已修)、[convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632(降级日志级别规则 + gate)、[automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420(事故)、[core/ADR-0116] init 阶段取服务的顺序契约是自愿声明的——不声明就没人拦,#4085 与 #4420 都是这么发生的 #4471(根因模式)