Skip to content

fix(scripts): check:durability-log-level 学会「故障已答给调用方」——显式声明的传播词汇,清空基线 (#5241) - #5616

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5241-durability-gate-propagation
Aug 5, 2026
Merged

fix(scripts): check:durability-log-level 学会「故障已答给调用方」——显式声明的传播词汇,清空基线 (#5241)#5616
os-zhuang merged 2 commits into
mainfrom
claude/issue-5241-durability-gate-propagation

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5241

⏸️ 停放状态(PM 维护,2026-08-05)

复核已通过(ACCEPT),draft 停放中,等待 base 恢复。

预期红清单(签名级,逐条比对,出现不在此列的签名才是新问题):

  1. ESLint job → check-engine-double-contract: 1 problem(s)x PINNED: packages/runtime/src/action-execution-calldata-not-found.test.ts declares 2 engine double(s) whose delete() does not route through assertEngineDeleteDispatch (lines 69, 102) —— base 分支自身的红(main 全仓红:check:engine-double-contract 挂在 #5584 刚落地的 action-execution-calldata-not-found.test.ts(2 个 double 未接 assertEngineDeleteDispatch,基线无条目)——所有新 PR 的 ESLint job 都过不去 #5604 立案,cli 车道在修);dev 已用单文件隔离实验钉死因果,且本 PR 自己的 durability 门在同一次 CI 里是绿的(20:32:26 ✓ self-test: 35 case(s)、20:32:28 ✓ ... (3 propagating, declared))。

解除条件:#5604 的修复合入 main → 本分支 git merge origin/main → CI 重跑红清零(并确认 durability 门在重跑里仍绿)→ 转 ready → 挂 auto-merge 入队。

方向 1(PM 裁决):给闸门加一份显式声明的故障传播词汇,而不是放宽基线语义、也不是维持现状。验收标准直接来自两条基线条目 closes 字段自己写的关闭条件。

问题

闸门只认两种合法应答 —— rethrow 或 error 日志 —— 表达不了第三种:故障已经答给了调用方。按 AGENTS.md 自己的判据问「降级之后系统从外面看还正常吗?」,这种形状答案是:请求方被明确告知这次写入没有落盘。它根本不是降级。

#4754saveMetaItem 加进词表后立刻撞上:4 处命中里 3 处是传播而非降级,只能进 durability-degradation.baseline.json —— 给正确代码在一份「每行都是待还的债」的 shrink-only 账本里写条目。更糟的是 meta.ts 那处最省事的「修法」是补一句 logger.error,而那条路径最常见的情况是作者提交了不合 spec 的 body:每一次校验拒绝打一条持久性 error,正是 AGENTS.md 点名的镜像错误。

做法:声明只给名字,结构仍由闸门证明

这是「词汇」和「改名后的基线条目」的分界线,而且是机械的、不是编辑口径上的:

  • FAILURE_PROPAGATION_CALLEES —— 全仓专名(errorFromThrown),按交付方式分两类:
    • via: 'return' —— 调用构造答复,值本身才是交付,所以只在 return 里算数(const env = errorFromThrown(e) 之后 warn 一句,什么都没交付);
    • via: 'effect' —— 调用本身就是交付(把故障写进调用方会收到的报告),裸表达式语句即可。
  • FAILURE_PROPAGATION_SITES —— issue 作者存疑的第二形状(结构化逐项结果报告)的答案。record(...) / failed.push(...) 这种名字全仓无意义,声明全仓等于按名字猜 —— 所以按函数作用域声明,键是「文件 + 所在函数名」,不是行号(行号每次无关改动都churn)、也不是整文件(protocol.ts 九千行,给 saveMetaItem 发全文件许可证的盲区大到能藏下一个 permission-set backfill (ADR-0094 D4) 现在 100% 失败:行里的 active 存储列喂进了 #4001 之后严格化的 permission spec #4669)。每条带理由,并做失效检测

关键在于:声明只提供名字,catchDeliversFailure() 仍要证明 catch 的每一条出口路径都交付了故障。它按结构建模(顺序、块、if/elsethrowreturnbreak/continue),对未建模的形状保守回退 —— 只能把已有交付向前带,绝不凭空造一个;凡是证明不了已交付的出口都算逃逸。也就是「证不出来」读作「没交付」,判它而不是放它,这是闸门唯一安全的方向。

闸门没有因新词汇变瞎(反向验证,方向均为事先预测)

实验 预测 实际
A 把真降级站点 service-automation/plugin.tserror 降成 warn 红,quiet-log @931
B 把 #4754 认定为真丢失packages.ts ADR-0045 可见性翻转的 error 降成 debug 红,packages.ts:243
C 从全局词表删掉 errorFromThrown 红,meta.ts:161
D 删掉两条 FAILURE_PROPAGATION_SITES 红,protocol.ts:7741 + :9045
E 把一条站点声明的键改成不存在的函数名 红,失效声明 + 该站点重新被判
F 只把脚本回退到 origin/main、保留清空后的基线 红,3 条 红,3 条

B 是其中最要紧的一条:packages.ts 那处把 unhideError 写进了响应体 —— 表面上很像「已答给调用方」,但那是一次赋值,不是声明过的传播调用,闸门拒绝放它过。新词汇没有把 #4754 刚修好的真丢失重新弄绿。

> 说明一处与派单模板不符的地方:模板要求「把 baseline 里任一真降级条目临时移除时闸门必须红」。本 PR 之后基线已空,不存在真降级条目可移除 —— 空账本正是它应有的稳态。实验 A 与 B 是诚实的等价证明:直接把真降级站点弄哑,闸门照样红。

自测

--self-test 从 19 条扩到 35 条,两个方向都钉住。新增负例包括:部分路径传播(一支答错误信封、另一支答成功值 / 另一支直接落到块尾)、信封构造了但没 return、交付写在稍后才跑的回调里、声明过的报告只在一支分支写、同样的报告形状但没有声明、站点声明键在另一个函数上、以及一个「证明不到」的保守回退样例。另加 expectSitesUsed 断言,钉住失效检测所依赖的记账。

文件面

未改动 packages/** 任何生产代码 —— 那 3 处站点一直是对的,不是修复对象。

Changeset

不需要:scripts-only + AGENTS.md,发布产物不变。也没有加空 frontmatter 的占位 changeset(Check Changeset 门的日志明说那是 last resort 且有 #4898 风险)。skip-changeset 标签已由 PM 挂上,门已过。


🤖 Generated with Claude Code

https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

…pagation vocabulary (#5241)

The gate had two legal answers for a catch guarding a durability-critical
callee — rethrow, or log at `error` — and no way to express the third:
the failure was handed to the CALLER. AGENTS.md's own judgment question
("does the system still look normal from the outside?") answers NO for
that shape, so it is not a degradation at all.

#4754 hit this the moment `saveMetaItem` entered the vocabulary: three of
its four hits were propagation, not degradation, and had to be parked in
`durability-degradation.baseline.json` — entries for CORRECT code, in a
shrink-only ledger whose header calls every line debt. Worse, the cheapest
way out at `meta.ts` was to bolt on a `logger.error` on a path whose common
case is a rejected request: one durability `error` per bad keystroke, the
mirror-image failure AGENTS.md names.

The vocabulary is DECLARED, never guessed, and the declaration only supplies
the NAME — the checker still proves the STRUCTURE (every path out of the
catch delivers the failure), so a declaration cannot become a baseline entry
wearing a friendlier name:

  - FAILURE_PROPAGATION_CALLEES — repo-wide names (`errorFromThrown`), split
    by how they deliver: `via: 'return'` (the value IS the answer, so it only
    counts inside a `return`) vs `via: 'effect'` (the call IS the delivery).
  - FAILURE_PROPAGATION_SITES — function-scoped names for the second shape:
    a batch whose contract is a per-item outcome report, delivering through
    `record(...)` / `failed.push(...)`. Keyed by file plus enclosing FUNCTION,
    not by file: `protocol.ts` is nine thousand lines and a file-wide licence
    for `saveMetaItem` would hide the next #4669. Entries are staleness-checked.

`catchDeliversFailure()` is conservative by construction: it can carry a
delivery forward but never invent one, and any exit it cannot prove delivered
counts as an escape — "cannot prove" reads as "does not deliver", which judges
the seam instead of excusing it.

Both #5241 baseline entries went stale and are deleted; the ledger is now
empty, which is its intended steady state. No production code changed — the
three sites were correct all along.

Self-test grows 19 -> 35 cases, pinning both halves of the rule: partial
propagation, an envelope built but never returned, a delivery in a later
callback, a report written on one branch only, an undeclared report shape,
and a site declaration keyed to a different function all still flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 9:06pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation size/l labels Aug 5, 2026
@os-zhuang os-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Aug 5, 2026 — with Claude
@github-actions github-actions Bot added the size/l label Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

CI 红说明(engine-core 车道 PM):ESLint job 的失败是 check:engine-double-contract 挂在 packages/runtime/src/action-execution-calldata-not-found.test.ts(lines 69/102),与 #5596 上的签名一致 —— base 分支自身的红,#5604 立案、cli 车道在修,非本 PR diff 所致(本 PR 改的是另一个门 check:durability-log-level)。不追此签名;#5604 落地后同步 main 重跑,届时须确认本 PR 自己的 durability 门在 CI 上真正跑绿(而不只是 engine-double 门恢复)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 21:11
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit c03fd9a Aug 5, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5241-durability-gate-propagation branch August 5, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants