`Blocked-by: #4769` `Blocked-by: #4771` `Blocked-by: #4772` ## 背景 2026-08-03 一次 showcase 冷启日志暴露了三个**同形**缺陷,分别落在三个互不相干的子系统(#4769 objectql 的 ADR-0104 自证、#4771 service-automation 的 flow 节点校验、#4772 plugin-auth 的 cache 探测)。三处代码此前互不知情,由三拨人在三个时期写下。 共同形状:**启动期问注册表「有没有 X」,把得到的「没有」当成终局事实并记账下来** —— 缓存进进程内变量、打成断言式告警、或者(最坏的 #4769)写进数据库。提供方随后注册好了,记账却不会撤销。 完整的两轴分析和「是否要更进一步收紧 kernel 契约」的裁定请求在 **#4776**(`needs-user-decision`)。本 issue 是那份分析里**不需要维护者拍板的第一步**,PM 已裁定直接派发。 ## 要做什么 沿 #4632 已经建立的机制(静默降级词表 + 门禁),把词表扩到这一类。#4632 的词表已经证明这条路能自己找活:#4669 引用它落地,#4755 的 dev 实测把 `saveMetaItem` 加进词表,当场抓出 8 处全静默 catch(→ #4754)。 要抓的模式,大致是这三者的**组合**(单独出现都可能是合法的,组合起来才是违规): 1. 在插件 `start()` / 构造期 / `kernel:ready` 之前,读服务注册表或能力注册表(`getService(...)`、节点执行器表、driver 表、capability 表等); 2. 对「读不到」这个结果做出终局判断; 3. **把该判断记账** —— 赋值给实例字段 / 模块级变量(缓存)、`logger.warn` 断言式措辞、或写入持久层。 第 3 点是判定的关键,请在实现时守住:只读不记账的调用是完全合法的,不要误报。三个已知实例分别命中三种记账方式,可以拿来当测试夹具: | 实例 | 记账方式 | 修复后应当不再违规 | |:--|:--|:--| | #4772 plugin-auth | 缓存进实例字段 | ✅ 惰性解析后不再有启动期终局判断 | | #4771 service-automation | 断言式 warn(“will fail at execution time”) | ✅ 校验推迟到 `kernel:ready` | | #4769 objectql | **写进数据库**(`sys_migration` 的 `attested`) | ✅ attestation 推迟到首启 seed 之后 | ## 为什么要等三个 PR 先落地(Blocked-by) 词表要抓的正是这三处现存代码。若门禁先于修复进 main,main 会立刻变红。三个修复都已在第 3 轮派发中,合并后再落这条门禁,可以顺带**用它们验证词表**:门禁在修复前的 commit 上必须报出这三处,在修复后的 commit 上必须干净。请把这个双向验证做进 PR 的证据里 —— 一个只在当前 main 上绿的门禁,无法证明它抓得到东西(参见 #4690:`check:react-declaration-parity` 无 MANIFEST 时静默 skip 退出 0,永远不可能红)。 ## 已知的覆盖边界(请在 PR 里如实写明,不要粉饰) 这是文本模式匹配,覆盖面天然有限: - `getService('cache')` 抓得到;包了三层的 `resolveCacheOrFallback()` 抓不到。 - #4769 完全抓不到 —— 它的「注册表」是数据库里的 `sys_migration` 表,不是服务注册表。它能进上表当夹具,是因为它的**修复**可以被验证,不是因为词表能发现它。 换句话说:这条门禁能止血,不能根治。根治与否是 #4776 要裁定的事。**请不要为了让门禁看起来更强而放宽匹配** —— 误报会比漏报更快地让人关掉它。 ## 顺带 #4773(SQL driver 注册两次)尚未定性,可能是同族第四例(若第二次注册的调用方也在做「我没看到 X,所以我来注册」这类过早判断)。该 issue 独立派发,定性后如果确属同族,回到本 issue 补进夹具表。
Activity
os-zhuang commentedon Aug 3, 2026
三个
Blocked-by全部解除 —— 本条现在可派发。夹具表现在可以照着实际落地的代码写了,三种记账方式各有一个真实样本:
plugin-authcreateLazyCacheRateLimitStorage()—— 消费时才解析service-automationsealNodeTypeVocabulary()—— 词汇表封闭时才下结论objectqlapp:seeded,且先读「已放行违规值」台账双向验证按原计划做:门禁在这三个 PR 之前的 commit 上必须报出这三处,在 main 上必须干净。请把这个验证做进 PR 证据 —— 一个只在当前 main 上绿的门禁,无法证明它抓得到东西(#4690 就是这个教训:无 MANIFEST 时静默 skip 退出 0,永远不可能红)。
派发前补充:#4769 的修复给了词表一个它抓不到、但值得记录的形态
原文已写明本门禁抓不到 #4769(它的「注册表」是数据库表,不是服务注册表)。#4794 落地后又多了一个同族形态,同样抓不到,但更值得写进文档:
「拿不到答案」被当成「答案是否」缓存下来。 #4794 修的记忆化就是这个 ——
sys_migration尚未注册时的那次读根本没读到账本,却被冻结成了整个进程的结论。它与「提供方尚未注册」是同一个错的两种表述,而后者是本门禁的目标。建议:词表抓不到的部分,在门禁的文档/错误信息里明确列出已知盲区,而不是让读者以为绿就等于没有。这与 #4776 待裁定的结构性方案是互补的,不是替代。
packages/lint。@xuyushun441-sys 账号近期在该目录活动(见 #4763、#4784),派发前请先看 #4604 登记表与本 issue 的评论,确认无人认领。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
认领:PM 循环第 8 轮
会话:
session_018iARDqtrhQgz6fVHDeDkbQ分支:
claude/issue-4777-startup-registry-verdict-vocabularyWorktree:
objectstack-issue-4777车道:
domain:devx—— 维护者已指定本会话独占认领该标签,登记在 #4604。前置已全部解除 ✅
issue 头部的三条
Blocked-by现已全部合入main,逐条核过:c44dd5ed425784cfec(PR #4791)f2eb85007(PR #4788)这正是 issue 要求的派发时机 —— 门禁若先于修复进 main,main 会立刻变红。现在三处都已修好,可以落门禁,而且可以用它们做双向验证。
派发时会原样带上的三条约束(都来自 issue 自己)
main)必须干净。只在当前 main 上绿的门禁证明不了它抓得到东西 —— issue 自己就点名了反例 check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690(check:react-declaration-parity无 MANIFEST 时静默 skip 退出 0,永远不可能红)。getService('cache')抓得到,包三层的resolveCacheOrFallback()抓不到;bug(objectql): ADR-0104「空库即已迁移」自证写在首启 seed 之前 —— 部署证明了一个它同一次启动就违反的契约,第二次pnpm dev起永久 10 条 ERROR #4769 那例词表根本抓不到(它的「注册表」是 DB 里的sys_migration,不是服务注册表)—— 它能进夹具表是因为它的修复可被验证,不是因为词表能发现它。误报会比漏报更快地让人关掉这条门禁。范围只到 issue 说的「不需要维护者拍板的第一步」;是否要进一步收紧 kernel 契约由 #4776(
needs-user-decision)裁定,不在本单。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
PM 复核:通过(ACCEPT),等 CI 全绿后进合并队列
PR:#4833(草稿,base
main,body 首行Fixes objectstack-ai/objectstack#4777)文件面(6 个,
packages/**零改动 —— 车道守住了):scripts/check-startup-registry-verdict.mjs(新)、scripts/startup-registry-verdict.baseline.json(新)、.github/workflows/lint.yml、package.json、AGENTS.md、changeset。我自己核的四条
lint.yml的 Lint & Type Check job 里有pnpm check:startup-registry-verdict,而该 job 的 check run 现在是绿的,等于新门禁已在 CI 里真跑并通过。这一点在本仓格外要紧:check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 就是「脚本存在、从未接线」。"entries": []),并写明为什么空(三个实例在门禁落地前都已修好)。更值得记的是它刻意不做--fix/--update标志,理由写在文件里:「生成器会让新违规靠『跑一下更新命令』就被收编,闸门正是这样失去意义的」。package.json脚本沿用邻居的--self-test && <real run>约定,与check:durability-log-level/check:init-service-contract同形。双向验证做得比我要求的还细
我要求「修复前报出、修复后干净」。它做了四点,把「门禁能跟着修复逐个变绿」也证了:
f2eb85007^(#4771 与 #4772 缺陷都在)auth-plugin.ts:346(cached + warn)与engine.ts:3317(announces warn@3329)25784cfec^(#4772 已修、#4771 仍在)main(三处全修)c44dd5ed4^(#4769 修复前)第 4 行正是我派发时钉的那条:#4769 必须如实报成覆盖盲区,不许算作已覆盖。它不仅写了,还实测了 —— 拿修复前的 commit 跑出「干净」来证明抓不到,而不是口头声明。
失败路径也证了:扫描目录缺失 → exit 1;目录为空 → exit 1 并打印
refusing to report success(注释直接引 #4690 的教训);baseline 有陈旧条目 → exit 1。一个连「无事可扫」都当成功的门禁,就是 #4690。最该表扬的一点:它收紧了,而不是兜底
第一版在
main上报出 7 条。它把 7 条逐条读完,判定全部是误报或已被仓内契约许可,于是收紧匹配器而不是把它们塞进 baseline:dependencies/optionalDependencies/requiresServices,经全仓providesServices索引解析)使「absent」成为事实,该判断被许可;warn/error算断言,info/debug是叙述;has()/get()。我派发时的原话是「不要为了让门禁看起来更强而放宽匹配 —— 误报会比漏报更快地让人关掉它」。它面对的是相反方向的同一诱惑:把误报塞进 baseline 就能让门禁看起来立刻可用。它没有。18 条自测夹具里包含
ApprovalsServicePlugin、getUnknownNodeTypeAudit、registerNodeExecutor这些真实的仓内负例,都是第一版会误伤的。衍生 #4835
scripts/check-init-service-contract.mjs(#4471 / ADR-0116 声明覆盖门禁)只匹配getService,不匹配getServiceAsync—— 而这正是 #4772 溜过去的那个洞:修复前的AuthPlugin.init()调的就是getServiceAsync('cache'),一个本该在 #4471 下变红的点位对它完全不可见。目前不红(现有getServiceAsync调用点都在请求期),是潜伏洞。未在本 PR 修:不同规则、不同语义,应当独立成 PR。已未认领归档。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
结案:PR #4833 已合入
main,issue 由Fixes自动关闭「启动期对注册表的终局判断」这一类缺陷从今天起有了机械防线:
scripts/check-startup-registry-verdict.mjs(AST 门禁,pnpm check:startup-registry-verdict)已接进lint.yml的 Lint & Type Check job,沿 #4632 的既有机制(词表 + shrink-only baseline +--self-test+--list)扩展,未另起炉灶。复核记录的要点:
f2eb85007^(两缺陷都在)→ 2 处违规;25784cfec^(修好一个)→ 恰好 1 处,门禁跟着修复逐个变绿;当前main→ 干净;c44dd5ed4^(bug(objectql): ADR-0104「空库即已迁移」自证写在首启 seed 之前 —— 部署证明了一个它同一次启动就违反的契约,第二次pnpm dev起永久 10 条 ERROR #4769 修复前)→ 干净,即实测证明 bug(objectql): ADR-0104「空库即已迁移」自证写在首启 seed 之前 —— 部署证明了一个它同一次启动就违反的契约,第二次pnpm dev起永久 10 条 ERROR #4769 是词表盲区(它的「注册表」是 DB 里的sys_migration),如实入档而非算作覆盖。--fix/--update—— 理由写在文件里:生成器会让新违规靠「跑一下更新命令」被收编。refusing to report success)、陈旧 baseline 条目 exit 1 —— 一个连「无事可扫」都当成功的门禁,就是 check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690。遗留:
needs-user-decision,domain:engine)—— 是否进一步收紧 kernel 注册表契约,本门禁止血不根治,根治待维护者裁定;getService,不认getServiceAsync—— #4772 就是从这个洞里溜过去的 #4835(domain:devx)—— 本单衍生:check-init-service-contract.mjs只认getService不认getServiceAsync,正是 bug(plugin-auth):[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772 溜过去的洞,待派;Driver already registered, skipping {"driverName":"com.objectstack.driver.sql"}—— SQL driver 被注册了两次 #4773(SQL driver 注册两次)定性后若属同族,回本门禁补夹具。Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
同一块地的第二个入口:#4825 的 dev 从读侧撞上了同一个问题,且给出了很强的论证
#4825(PR #4872,已复核通过入队)修的是
nextEventSeq()把驱动读失败当成「表还没建」、静默从 1 重新发号。issue 末尾留了个问题:_find这类调用该不该进DURABILITY_CRITICAL_CALLEES?dev 按指示只报告未动手,结论值得原样记下来。为什么不能直接把
_find加进词表这一条我完全同意,而且它对本单(#4777,启动期注册表判断)是同样的约束:词表扩张的边际代价不是线性的,每加一个高频 callee,例外清单就会淹没信号,然后所有人学会「红了就加例外」。
有害形状需要三个部分同时出现
dev 把它拆得比"读失败"精确得多:
nextEventSeq三条全中:_find失败 →return 1→event_seq: 1被插入;catch { return [] }只用来渲染一个空列表:中前两条,完全正常。这个三部分判据是关键 —— 它把「读失败」这个太宽的类,收窄成一个真正有害的类。而 #4777 关心的启动期注册表判断,恰好也是这个形状:读失败 → 顶替成「没有 provider」→ 把这个结论记下来(缓存 / 断言 / 写库)。
两条单是同一个规则的两个实例。
建议的处理顺序:先立规则,后立门禁,门禁上线前先量准确率
dev 推荐的是「在 AGENTS.md 写一条规则(形状照抄它下面那条已有的 Startup registry reads 规则),机械门禁另立窄单、先测命中率再决定」。理由:
我接受这个判断。补充一点本单的角度:#4632 真正起作用的部分本来就是那条写下来的规则,机械检查是它的放大器而非本体 —— 今天 #4728 / #4825 / #4754 三处都是人按规则找出来的,不是 gate 报出来的。
因此本单的范围建议收敛为
DURABILITY_CRITICAL_CALLEES保持现状(五个写/预置 callee 不动)—— 这与本单原本"扩词表"的方向相反,是根据新证据的修正;catch { return <literal> }且该值随后被持久化的形状,并在信任它之前先量命中率与误报率。scripts/check-durability-degradation-log-level.mjs不在本会话(session_015Br2xsJsczFsTR9bvbh2Ny)认领的 engine / services / identity / cli 范围内,偏 devx。本会话不派发本单,仅把这份论证归档在此供接手方使用。跨车道协调按登记表走 #4604。关联:#4825(PR #4872,读侧实例)、#4632(写侧规则与门禁)、#4867(canonical 路径上的同一缺陷)、#4776。
Generated by Claude Code
feat(devx): gate the "startup registry verdict" class — read the regi…
fix(tooling): derive the docs-audit scope from the filesystem instead…