feat(devx): 把静默降级词表扩到「启动期对注册表的终局判断」—— 读可以,记账不行 (#4777) - #4833
Merged
xuyushun441-sys merged 1 commit intoAug 3, 2026
Merged
Conversation
…stry all you like, never record the "no" (#4777) One showcase cold start on 2026-08-03 produced three instances of one shape in three unrelated subsystems, written by three people at three times: ask a registry "is X there?" while the boot is still filling it, treat the "no" as final, and RECORD it — cached on the instance (#4772 plugin-auth), asserted in a warn (#4771 service-automation), or written to the database (#4769 objectql). The provider registers a moment later; nothing undoes the record. All three are fixed. This is what stops the class from coming back, built on the #4632 machinery (declared vocabulary + AST + shrink-only baseline + --self-test) rather than a parallel one. The gate matches the three-part shape, and part 3 is what makes it a rule and not noise: 1. a read of a registry still filling — the service registry during init()/constructor, or an ADR-0018 open capability registry before its seal; 2. a terminal conclusion drawn from "absent"; 3. that conclusion recorded — cached in an instance field / module binding, asserted in a warn, or persisted. A read-only probe stays completely legal, and every cure passes untouched: a probe deferred into a lazy accessor or a kernel:ready hook (nested bodies are not descended into), a probe whose ordering an ADR-0116 declaration already made final (dependencies / optionalDependencies / requiresServices — tolerance lives in the plugin, never in a checker ledger), and a verdict drawn at a declared seal (sealNodeTypeVocabulary). Rule B counts only registry ENUMERATION, so a keyed has()/get() on a runtime path is not mistaken for a boot-time membership verdict. Proven in both directions, because a gate that has only ever been green cannot be told apart from one that matches nothing (#4690): f2eb850^ (both defects live) → 2 violations (auth-plugin, engine.ts) 25784cf^ (#4772 fixed only) → 1 violation (engine.ts) main → clean, 47 seams seen, 41 read-only Coverage is stated, not implied: it under-matches on purpose. #4769 is a fixture, not a catch — its "registry" is the sys_migration table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
This was referenced Aug 3, 2026
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 09:49
xuyushun441-sys
deleted the
claude/issue-4777-startup-registry-verdict-vocabulary
branch
August 3, 2026 09:58
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 #4777
这条门禁抓什么
一次 showcase 冷启(2026-08-03)在三个互不相干的子系统里抓出同一个形状 ——
启动期问注册表「有没有 X」,把「没有」当成终局事实并记账,提供方随后注册好了,
记账却不会撤销。三处代码由三拨人在三个时期写下,此前互不知情。
判定必须三条同时成立,少一条就不是发现:
init()/constructor期的服务注册表,或ADR-0018 那种「封闭(seal)之前一直可以长大」的能力注册表;
warn、或落库。第 3 条是这条规则和噪音的分界线。 只读不记账完全合法,门禁必须放过 ——
AutomationEngine.getUnknownNodeTypeAudit()每次调用都枚举执行器注册表、什么都不记,它是对的。一个见到启动期
getService(...)就报的门禁,一周之内就会被人关掉,而被关掉的门禁比没有门禁更糟,因为它还会报告成功。
双向验证(这是交付物,不是附赠)
只在当前 main 上绿的门禁,和一个什么都匹配不到的门禁无法区分(见 #4690:
check:react-declaration-parity无 MANIFEST 时静默 skip 退出 0,永远不可能红)。所以下面三个点都跑了,输出照抄:
①
f2eb85007^—— #4771 与 #4772 两处缺陷都还在:②
25784cfec^—— #4772 已修、#4771 还在(修一个就少一个,证明报的确实是那两处):③ 当前
main—— 干净:47 个 seam / 41 个只读,说明它确实在看东西,不是空转。
--list里那些合法的读长这样:覆盖边界 —— 如实写明,不粉饰
这是声明式词表 + AST,覆盖面天生有限,宁可漏报也不误报:
getService('cache')抓得到;包了三层、还在另一个包里的resolveCacheOrFallback()抓不到 —— 调用图只跟到同文件。
pnpm dev起永久 10 条 ERROR #4769 完全抓不到。 它的「注册表」是数据库里的sys_migration表,经 ObjectQL 读出,语法上和任何一次取行没有区别。它进 fixture 表是因为它的修复可以被验证
(
c44dd5ed4^上本门禁同样是绿的 —— 已实测,如实计为「未覆盖」而不是「已覆盖」),不是因为词表能发现它。
ctx.getService(cfg.name))一律跳过,不猜。这条门禁能止血,不能根治。 是否要进一步收紧 kernel 契约是 #4776 要裁定的事,不在本 PR。
为什么不误报 —— 三条「治好了的形状」必须原样通过
跑第一版时它在 main 上报了 7 处,逐一读下来全是误报或已被仓库自有契约许可,于是按下面三条收窄
(没有为了让门禁显得更强而放宽匹配):
dependencies/optionalDependencies/requiresServices一旦声明,kernel 就会把提供方提前(或断言其已注册),此时「没有」就是事实而非猜测。这正是 bug(plugin-auth):
[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772(cache一处未声明)与KnowledgeServicePlugin(声明了
optionalDependencies,缓存答案完全正确)之间的分界。容忍度写在插件自己的声明里、由 kernel 执行,永远不写进 checker 的 ledger —— 与
check-init-service-contract.mjs同一条线。这条门禁因此与 [core/ADR-0116] init 阶段取服务的顺序契约是自愿声明的——不声明就没人拦,#4085 与 #4420 都是这么发生的 #4471 互补而非重复:[core/ADR-0116] init 阶段取服务的顺序契约是自愿声明的——不声明就没人拦,#4085 与 #4420 都是这么发生的 #4471 说「你得声明」,把 #4632 词表扩到「启动期对注册表的终局判断」—— 一次冷启抓出 3 例,当前零防线 #4777 说
「你没声明就不许记账」。
warn/error才叫「断言」。info/debug是叙述。[...registry.keys()]/for…of/.size是对整个世界此刻的快照,才可能产生 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 那种「没有插件提供这个类型」的终局判断;
registry.has(x)/.get(x)问的是一个具体条目,是运行时正常派发 ——registerNodeExecutor对找到的重复项 warn、refuseGatedResume在 resume 时解析一个 descriptor,两者都读注册表也都 warn,都不是启动期判断。
start()也刻意不在扫描范围内:到start()时每个init()都已跑完,ApprovalsServicePlugin.start()探测automation并 warn 与 #4772 的缺陷结构完全相同,只有阶段能区分二者 —— 报它就等于给一个正确的修复判红。这两种形状都各有一条 self-test 钉住。
机制沿用 #4632,不另起炉灶
scripts/check-durability-degradation-log-level.mjs的那套(显式词表 + AST + 只减不增的baseline +
--self-test+--list)原样复用。没有塞进同一个脚本,因为两者问的是不同问题(catch 块的日志级别 vs 生命周期体里的记账),合并会让词表语义含混。
新增
--packages-dir 路径:上面的 before/after 就是这么产出的,谁都可以复跑。另有一条「响亮的缺席」保护:扫不到任何源文件时报错退出 1,而不是绿着退出 0(#4690 的教训)。
变更清单
scripts/check-startup-registry-verdict.mjs—— 门禁 + 18 条 self-test(每条都引 把 #4632 词表扩到「启动期对注册表的终局判断」—— 一次冷启抓出 3 例,当前零防线 #4777,正反双向:三种记账方式必须报,三种治好的形状 + 只读探测 + 非字面量服务名必须放过)
scripts/startup-registry-verdict.baseline.json—— 只减不增,落地时为空(三处实例都已修完,无需 baseline 任何东西)
package.json——check:startup-registry-verdict(先跑--self-test再审计,同邻居).github/workflows/lint.yml——Lint & Type Check里加一步AGENTS.md—— 新增「Startup registry reads」一节:判定问题、三部分形状、三种解法(按优先级:用时解析 → 声明顺序 → 先封闭再判断)、以及门禁的边界
.changeset/startup-registry-verdict-guard.md—— tooling-only,不发布任何包未改动
packages/**任何文件。🤖 Generated with Claude Code
https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
Generated by Claude Code