Skip to content

feat(devx): 把静默降级词表扩到「启动期对注册表的终局判断」—— 读可以,记账不行 (#4777) - #4833

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4777-startup-registry-verdict-vocabulary
Aug 3, 2026
Merged

feat(devx): 把静默降级词表扩到「启动期对注册表的终局判断」—— 读可以,记账不行 (#4777)#4833
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4777-startup-registry-verdict-vocabulary

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4777

这条门禁抓什么

一次 showcase 冷启(2026-08-03)在三个互不相干的子系统里抓出同一个形状 ——
启动期问注册表「有没有 X」,把「没有」当成终局事实并记账,提供方随后注册好了,
记账却不会撤销。三处代码由三拨人在三个时期写下,此前互不知情。

判定必须三条同时成立,少一条就不是发现:

  1. 读一个还在填充中的注册表 —— init()/constructor 期的服务注册表,或
    ADR-0018 那种「封闭(seal)之前一直可以长大」的能力注册表;
  2. 从「读不到」得出终局结论;
  3. 该结论被记账 —— 缓存进实例字段 / 模块级变量、写成断言式 warn、或落库。

第 3 条是这条规则和噪音的分界线。 只读不记账完全合法,门禁必须放过 ——
AutomationEngine.getUnknownNodeTypeAudit() 每次调用都枚举执行器注册表、什么都不记,
它是对的。一个见到启动期 getService(...) 就报的门禁,一周之内就会被人关掉,而被关掉的
门禁比没有门禁更糟,因为它还会报告成功

双向验证(这是交付物,不是附赠)

只在当前 main 上绿的门禁,和一个什么都匹配不到的门禁无法区分(见 #4690:
check:react-declaration-parity 无 MANIFEST 时静默 skip 退出 0,永远不可能红)。
所以下面三个点都跑了,输出照抄:

f2eb85007^ —— #4771#4772 两处缺陷都还在:

$ node scripts/check-startup-registry-verdict.mjs --packages-dir /tmp/os4777-pre4772/packages

✗ 2 startup registry read(s) record a verdict the boot can still contradict (#4777):

  packages/plugins/plugin-auth/src/auth-plugin.ts:346  AuthPlugin.init()
    reads   : getServiceAsync('cache')
    why     : resolves the kernel service registry (async accessor), which other plugins are still filling; ADR-0116: another plugin's init() may not have run yet, so an absent service may simply be a later one
    records : asserted — warn log (line 357); cached — < obj >.secondaryStorage (line 352)

  packages/services/service-automation/src/engine.ts:3317  validateNodeTypes
    reads   : nodeExecutors, actionDescriptors (open capability registry)
    why     : ADR-0018 makes the flow node-type vocabulary open and runtime-extensible …
    records : announces the verdict — warn@3329

25784cfec^ —— #4772 已修、#4771 还在(修一个就少一个,证明报的确实是那两处):

$ node scripts/check-startup-registry-verdict.mjs --packages-dir /tmp/os4777-pre4771/packages

✗ 1 startup registry read(s) record a verdict the boot can still contradict (#4777):

  packages/services/service-automation/src/engine.ts:3317  validateNodeTypes
    records : announces the verdict — warn@3329

③ 当前 main —— 干净:

$ pnpm check:startup-registry-verdict
  ✓ 18 self-test case(s) passed
✓ startup registry verdicts: 47 startup/open-registry seam(s) across 1398 file(s),
  41 read-only (legal), none recording a verdict the boot can contradict.

47 个 seam / 41 个只读,说明它确实在看东西,不是空转。--list 里那些合法的读长这样:

[B] …/engine.ts:3396  getUnknownNodeTypeAudit reads nodeExecutors+actionDescriptors → read-only (legal)
[B] …/engine.ts:3357  knownNodeTypes         reads nodeExecutors+actionDescriptors → read-only (legal)
[B] …/engine.ts:3437  sealNodeTypeVocabulary reads … → announces warn@3449 via warnUnknownNodeTypes() [sealed by nodeTypeVocabularySealed]
[A] …/knowledge-service-plugin.ts:78  KnowledgeServicePlugin.init() probes 'objectql' → cached:this.service@100
    [final: declared 'com.objectstack.engine.objectql' in dependencies/optionalDependencies — the kernel orders it ahead]

覆盖边界 —— 如实写明,不粉饰

这是声明式词表 + AST,覆盖面天生有限,宁可漏报也不误报:

这条门禁能止血,不能根治。 是否要进一步收紧 kernel 契约是 #4776 要裁定的事,不在本 PR。

为什么不误报 —— 三条「治好了的形状」必须原样通过

跑第一版时它在 main 上报了 7 处,逐一读下来全是误报或已被仓库自有契约许可,于是按下面三条收窄
(没有为了让门禁显得更强而放宽匹配):

  1. ADR-0116 的声明就是许可。 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
    「你没声明就不许记账」。
  2. 只有 warn/error 才叫「断言」。 info/debug 是叙述。
  3. Rule B 只认「枚举」。 [...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

…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
@vercel

vercel Bot commented Aug 3, 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 3, 2026 9:32am

Request Review

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling and removed size/xl labels Aug 3, 2026
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 09:49
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 799d098 Aug 3, 2026
21 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4777-startup-registry-verdict-vocabulary branch August 3, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

把 #4632 词表扩到「启动期对注册表的终局判断」—— 一次冷启抓出 3 例,当前零防线

2 participants