Skip to content

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

Closed

Description

@os-zhuang

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

现象

#4754 把 saveMetaItem 加进 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.ts — catch → deps.errorFromThrown(e, 400),调用方拿到带 issues 的 4xx/422;
    • packages/metadata-protocol/src/protocol.ts ×2 — migrateStoredItems 的 report.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 和词表里其他条目不是一类

现有词表条目(syncSchema、writeRecord、writeDeferredReference、rearmSuspendedWaitTimers、dropPromotedDraftRow、deliverPersistedRow…)清一色是背景副作用:调用方并不在等这一次写的结果,某个更外层的操作会照常报成功 —— 所以「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」。

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 4, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    发现分诊轮判级(session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):晋级 pm:queue。前提核验见 #4604 01:57Z(对 b4ad98435 成立:两条给正确代码写的条目仍在,closes 自署 #5241)。理由:shrink-only 账本里有了永不会缩的行,账本可信度受损;改动机械、条目自带 closer。维护者可否决。


    Generated by Claude Code

  4. self-assigned this
    on Aug 5, 2026
  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 1 轮追加批(engine-core 车道,维护者要求提速,batch 提至 5)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-5241-durability-gate-propagation
    Worktree:objectstack-issue-5241
    域:domain:engine-core
    文件面:scripts/check-durability-degradation-log-level.mjs、scripts/durability-degradation.baseline.json(删两条 #5241 条目)+ 闸门自测,.changeset/*.md 视先例取舍。

    方向裁决(PM 拍板,维护者可否决——不升级的依据:这是验证策略/闸门形状,非产品语义):取方向 1。理由:两条基线条目的 closes 字段自己写明的关闭条件就是「teach the checker a DECLARED failure-propagation vocabulary」,方向 1 与闸门「显式声明、不猜」的既有哲学一致;方向 2 稀释 shrink-only 账本语义,方向 3 留着永不缩的行。对 issue 作者存疑的第二形状(report.failed++ / failed.push 结构化逐项报告):若确实无法在不引入脚本注释所拒绝的启发式的前提下机械判定,允许退到站点级显式声明(如 file::callee 粒度的 declared error-propagation 清单,每条带理由)——它仍是「显式声明」,只是粒度更细;⛔ 不接受按名字/形状猜的启发式。


    Generated by Claude Code

  6. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 1 轮追加批):PR #5616,当前 draft 停放,唯一红是 base 分支的 #5604 签名(dev 已用单文件隔离实验在 #5604 钉死因果,并证明本 PR 自己的 durability 门在同一次 CI 里是绿的);#5604 落地后同步 main → 红清零 → 转 ready 入队。

    交付按方向 1 落地且形状比裁决更硬:声明只给名字(FAILURE_PROPAGATION_CALLEES 按 via: return/effect 分交付方式;站点级声明键取「文件+函数名」而非 file::callee,避免给 9000 行文件发全文件许可证),结构仍由 catchDeliversFailure() 证明每条出口路径都交付了故障,证不出来判「没交付」。两条基线条目删除、账本清空为稳态;AGENTS.md 补第三种合法应答。自测 19→35 条,六项反向验证全部命中预测(B 最关键:packages.ts 把 unhideError 写进响应体这种「像传播的赋值」被拒绝放行 —— 新词汇没把 #4754 的真丢失重新弄绿)。模板与实况的一处偏差(基线已空无真降级条目可移除)dev 如实报告,等价证明(实验 A/B)成立,接受。

    skip-changeset 已挂(scripts + AGENTS.md,发布产物不变)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchtoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions