Skip to content

check:durability-log-level 分不清「故障已答给调用方」和「故障被吞掉」—— 词表加 saveMetaItem 后逼出 2 条基线,记的全是正确代码 #5241

Description

@os-zhuang

发现于 #4754 的实现期(PR 分支 claude/issue-4754-savemetaitem-durability-wordlist)。未认领 —— 只是记录。观察类:今天没有用户会撞到,代价是一份本不该存在的基线。

现象

#4754saveMetaItem 加进 DURABILITY_CRITICAL_CALLEES。修掉闸门自身两个精度缺陷后,命中从 8 降到 4,其中:

  • 1 处是真丢失(packages/runtime/src/domains/packages.ts 的 ADR-0045 可见性翻转:HTTP 200 + unhideError 埋在 body 里,零日志)—— 已按范本改成 error;
  • 3 处不是降级,它们把故障原样答给了调用方:
    • packages/runtime/src/domains/meta.tscatchdeps.errorFromThrown(e, 400),调用方拿到带 issues 的 4xx/422;
    • packages/metadata-protocol/src/protocol.ts ×2 — migrateStoredItemsreport.failed++ + 逐项 outcome:'failed';复制入包的 failed.push() + 聚合 success:false / failedCount

按 AGENTS.md 的判据原问 ——「降级之后系统从外面看还正常吗?」—— 这 3 处答案都是:请求方被明确告知这次写入没有落盘。它们根本不是降级,而是错误传播。但闸门的模型只认「loud 日志 or rethrow」,表达不了「已答给调用方」,于是这 3 处只能进 scripts/durability-degradation.baseline.json

为什么这是个问题,而不是「基线就是干这个的」

基线文件自己的表头写着「Every entry names WHY it is still here and WHAT closes it」,而且是 shrink-only。给正确代码写基线条目有两个后果:

  1. 这些条目永远关不掉(代码没毛病),shrink-only 的账本从此有了一批不会缩的行,「基线 = 待还的债」这个语义被稀释;
  2. 更糟的是它给下一个作者立了个范例:这个闸门会误报,遇红先加基线。而闸门脚本自己的注释恰恰写着,误报率高到让人绕过的闸门「worth less than no gate, because it also reports success」。

还有第三个后果,是这次差点踩到的:面对 meta.ts 那处误报,最省事的「修法」是补一句 logger.error —— 而那条路径最常见的情况是作者提交了不合 spec 的 body,于是每一次校验拒绝都会打一条持久性 error。这正是 AGENTS.md 点名的镜像错误(「trains everyone to skim error」),也正是 #4420 那条 warn 当初没人读的成因。一个闸门,最省力的满足方式是有害的,那闸门的形状就有问题。

根因:saveMetaItem 和词表里其他条目不是一类

现有词表条目(syncSchemawriteRecordwriteDeferredReferencerearmSuspendedWaitTimersdropPromotedDraftRowdeliverPersistedRow…)清一色是背景副作用:调用方并不在等这一次写的结果,某个更外层的操作会照常报成功 —— 所以「catch 必须响」对它们无一例外地成立。

saveMetaItem 不同:8 处命中里 7 处是调用方直面的主操作(HTTP 写元数据、批量迁移/复制),只有 1 处是搭别人便车的副作用。#4669 那个真事故也正是副作用型。词表按 callee 名字匹配,而这个 callee 横跨两类,精度就掉了。

建议方向(裁决留给维护者,本卡不预设)

  1. 给闸门加一份「故障传播词汇」(和 DURABILITY_CRITICAL_CALLEES 一样是显式声明的,不猜):一个 catch 若每条路径都把故障交了出去,就等价于 rethrow。HTTP 形状好办 —— errorFromThrown / sendError 是专名;难的是 protocol.ts 那种结构化逐项结果报告(report.failed++failed.push({...})),按名字识别就退回成脚本注释明确拒绝的启发式了;
  2. 收窄词表:承认这一族要区分「副作用型写」和「主操作型写」,而 callee 名字做不到这个区分 —— 那就接受 saveMetaItem 只能靠基线管住,并把基线表头的语义从「待还的债」放宽成「已复核的例外」,明说这个取舍;
  3. 维持现状(3 处基线),只把这份取舍记在案。

我倾向 1,但它对第二种形状(结构化结果报告)是否可机械判,我没有把握 —— 这正是本卡不自作主张的原因。

现状

#4754 的 PR 已经落了这些,本卡不阻塞它:

本卡关闭时,应能删掉那 2 条基线条目(键是 file::callee,3 处站点合成 2 个键)。

关联

#4632(立规矩)、#4669(副作用型的那次真事故)、#4754(加词表 + 清账)、#5186(同一闸门的另一个盲区:读接缝的漏报;本卡是反方向的误报)、AGENTS.md「Degradation log levels — warn vs error」。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions