feat(scripts): durability 闸门新增「读接缝编造空值」规则 —— #4728/#4825/#5108 这一族终于有闸门了 (#5186) - #5986
Merged
Merged
Conversation
`check:durability-log-level` graded ONE axis — how loud is the catch — which is structurally unable to see the other half of the #4632 family. A read seam does not fail by logging too quietly; it fails by `catch { return []; }`: no log to grade, and an answer invented for a read that never happened. The vocabulary model comes apart there too, because a read's callee is `find`/`findOne`/`count`, names too generic to declare repo-wide. The same shape has recurred three times in one package (#4728 -> #4825 -> #5108), every one found by a human and none by the gate. Second rule, in the same script and the same CI step, sharing no vocabulary, no baseline and no verdict with the first: try block performs a READ (IDataDriver find/findOne/count, or a same-file wrapper over one) + catch logs NOTHING + some path returns an empty/zero value ([] / false / null / undefined / {} / '' / 0 / 1) that was NOT reached by discriminating the error TYPE -> red. The exemption is the shape #4825 and #5108 left behind: `isMissingTableError` (packages/metadata/src/errors.ts), inline or through a rethrowing guard such as `rethrowUnlessTableUnprovisioned`, with polarity handled both ways. It is declared, never inferred, and still structurally proved: a hand-rolled `e.code === '42P01'` does not exempt, and a discrimination that returns the empty value on both branches does not either. Scan scope is narrowed to packages/metadata, packages/metadata-protocol and packages/objectql per the maintainer's 2026-08-06 ruling; the narrowing is what makes the generic read names mean "a storage seam" at all. Measured: 64 read seams in scope, 3 findings, all baselined in the new shrink-only scripts/durability-read-invention.baseline.json - two real degradations tracked as #5980 and #5979, one reviewed as legitimate (a declared tri-state `null`). Reverse-verified: reverting the #4825 and #5108 fixes turns the rule red on 5 of their 6 seams; the sixth returns an envelope rather than a bare empty value and is documented as a limitation. No CI wiring change: `check:durability-log-level` already runs the self-test and the audit, and both now cover both rules. Fixes #5186 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 6, 2026 14:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5186
按维护者 2026-08-06 10:44Z 的「裁 3——收窄先行」落地。
前提复核(先证再做)
三条事实全部在
origin/main上核过,issue 的判断成立:DURABILITY_CRITICAL_CALLEES现有 13 项(issue 写的 9 项之后又长了 4 个),syncSchema/initObjects/writeRecord/dropPromotedDraftRow/saveMetaItem/persistAuditTrailRow…,无一读语义analyzeSourceFile判loud/quiet/rethrow/propagates,读接缝的catch { return [] }在这四个维度上全是空的DatabaseLoader五处读现在都走rethrowUnlessTableUnprovisioned(error),nextEventSeq走if (isMissingTableError(error)) return 1; throw error;这一版做了什么
在同一个脚本、同一个 CI step 里加了第二条规则,与第一条不共享词表、不共享 baseline、不共享判决:
判据的关键不在「调了什么」,而在「没读到,却编了一个答案出来」。
豁免就是 #4825 / #5108 修完留下的形状:
isMissingTableError(packages/metadata/src/errors.ts)。行内写或经rethrowUnlessTableUnprovisioned这类「重抛式 guard」都认,两种极性都认:豁免是声明的、且仍要结构证明:
e.code === '42P01'//no such table/i不算——那正是 loadMetaFromDb 用 /no such table/i 正则判「良性首启」,其余 sys_metadata 读失败吞成 console.warn + loaded:0 —— isMissingTableError 的手抄第二份 #5841 在loadMetaFromDb刚退休掉的第二套词表,判红是本意;isMissingTableError」式的存在性检查会直接放行它);indexFunctionBodies按裸名索引,不设这道门就会被同名方法蹭到许可。扫描面收窄到
packages/metadata/packages/metadata-protocol/packages/objectql。收窄不只是成本考虑:find/findOne/count是彻底通用的名字,是 SCOPE 让它们意味着「存储缝」而不是「任意数据读取」。度量与 baseline
扫描面内 64 处读接缝,3 处命中,全部进新的 shrink-only
scripts/durability-read-invention.baseline.json(本 PR 是闸门单,文件面scripts/+.changeset/,不改被扫的包):protocol.ts的listCommitsunfixed-degradation[],JSDoc 自己写着这是设计 → 已立 #5980engine.ts的seedAutonumberunfixed-degradation0起号,与既有行撞号,零日志 → 已立 #5979engine.ts的referenceExistsreviewed-legitimatenull不是编造:签名是Promise< boolean or null >,JSDoc 明写「false只在探测跑成功且没找到时,null是根本没跑成」,故障是经返回值交给调用方的。规则读语法不读返回类型,故记录复核结论而不是放宽规则baseline 的 key 是
file::enclosing function,不是file::callee——callee 永远是find,而protocol.ts九千行、engine.ts五千行,文件级许可的盲区大到能藏下一个 #5108。这与FAILURE_PROPAGATION_SITES的粒度取法同源。反向验证(方向事先预判为「红」,结果符合)
把 #4825 + #5108 的修复全部还原(去掉 5 处 guard 调用、把
nextEventSeq改回catch { return 1 }),闸门在它们当年修的那 6 处里红了 5 处:第 6 处
load()看不见,这是如实记录的局限,不是漏测:它返回的是{ data: null, loadTime: … }—— 空答案被包在信封里,而规则读的是被return的表达式。放宽成「对象字面量里有一个空值属性就判」会把三个包里每一个 result envelope 都按其中一个属性判掉,那个误报率是会让闸门被关掉的量级(而被关掉的闸门比没有闸门更糟,因为它还报成功)。信封形状的空答案需要另一条判据(声明的返回类型),不是这一条的松版。脚本头部把这条与另外两条局限一起写在「Honest limitations」里。baseline 的 shrink-only 也双向验过:删掉一条 → 红(该 seam 重新报出);加一条对不上的 → 红(stale entry 报出)。
为什么放在同一个脚本里
「独立规则」按裁决是指不走
DURABILITY_CRITICAL_CALLEES词表(判据独立),不是指独立文件。放在同一脚本换来三件事:check:durability-log-level本来就是「self-test && audit」,两条规则都被覆盖,package.json与lint.yml一行不动(lint.yml 属 spec-tooling 座位在飞面,本单 ⛔ 不碰);walkSameTick/loggerLevel/indexFunctionBodies等工具;collectLoggedLevels()给两条规则共用——两份拷贝就是两套「这个 catch 说过话」的事实来源,会照着@objectstack/metadata/errors存在要防的那种方式漂移。两条规则的报告、baseline、staleness 检查各自独立,任一条红不遮蔽另一条。
自检
脚本自带 fixture 从 35 → 61 例(原 35 例全部保持绿,验证
collectResponse抽取无回归),新增 26 例全部来自仓库真实代码而非想象:通过样例是 #4825 / #5108 修完的代码,命中样例是它们修之前的形状。一个已经复发三次的族,闸门必须双向钉在这三次实例上,否则第四次照样绿灯过。关于 changeset
本 PR 只动
scripts/(根 package 是private: true,scripts/不进任何发布产物),不发布任何东西,故按pr-automation.yml明示的路线 2 走skip-changeset标签,而不是写一个不指名任何包的空 changeset(路线 3 是 last resort,#4898 那次静默停发的原因)。越界说明
AGENTS.md:该文件不在本单申报面内。但「Degradation log levels」末尾的 It has teeth 段现在只描述了check:durability-log-level的一半(且「baseline 目前是空的」这句容易被读成整条命令的 baseline 都是空的)。已作为观察类发现立卡 [finding] AGENTS.md 的「It has teeth」段只描述check:durability-log-level的日志级别那半 —— #5186 之后同一条命令还跑读接缝规则,散文没跟上 #5981,建议补一段指路牌。listCommits/seedAutonumber两处真实缺陷已分别立卡MetadataProtocol.listCommits把 commit store 读不到答成[]—— ADR-0067 时间线上「无历史」与「读不到」不可分辨(零日志,JSDoc 里写着这是设计) #5980 / ObjectQLseedAutonumber把读故障答成return 0—— 对已有 N 行的表重新从 1 发号,自增号与既有行相撞且零日志(#4825 同族,活体) #5979,并在 baseline 条目里用tracked_by指了回去。🤖 Generated with Claude Code
https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
Generated by Claude Code