在 #4777 建门禁时顺带发现,**不在该 issue 范围内**,按 Prime Directive #10 单独记录。 ## 发现 `scripts/check-init-service-contract.mjs`(#4471 / ADR-0116 的声明覆盖门禁)只匹配一个 callee: ```js // scripts/check-init-service-contract.mjs if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'getService') { ``` 而 kernel 的服务解析有两个入口 —— `getService` 和 `getServiceAsync` (`packages/core/src/kernel.ts:505`)。**两者的排序风险完全相同**:插件 `init()` 的执行顺序 决定能不能解析到,ADR-0116 的 `dependencies` / `optionalDependencies` / `requiresServices` 对两者一视同仁地生效。但门禁只看得见前者。 ## 这不是理论问题 —— #4772 正是这么溜过去的 `AuthPlugin.init()` 里(修复前,`f2eb85007^`): ```ts cache = await (ctx as { getServiceAsync?: (n: string) => Promise< unknown > }).getServiceAsync?.('cache'); ``` - `cache` 由 `CacheServicePlugin` 通过 `providesServices = ['cache']` 提供 —— 是一个 workspace 提供的服务,正是该门禁的判定对象; - 修复前的 `AuthPlugin` 声明里没有任何一条覆盖它(`requiresServices = ['data', 'manifest']`, 依赖里也没有 `com.objectstack.service.cache`); - 换句话说,这**本该**在 #4471 门禁下判红,却因为 callee 名字是 `getServiceAsync` 而完全不可见。 代价见 #4772:showcase 冷启里 21ms 的顺序差把 `undefined` 冻进了 better-auth 配置, 多节点部署的限流计数永远到不了共享存储(ADR-0069 D2 宣称了运行时没交付的能力)。 ## 当前是否已经红? 否。今天 `packages/**` 里的 `getServiceAsync` 调用点(`rest/src/rest-server.ts`、 `runtime/src/http-dispatcher.ts`、`runtime/src/dispatcher-plugin.ts`)都在请求期路径上, 不在任何插件的 `init()` 里 —— 所以这是一个**潜伏的洞**,不是现存的红。 补上词表大概率仍然是绿的,应该顺手做掉。 ## 建议 把 `getServiceAsync`(以及 `hasService` 之类真正读注册表的入口,如果确认存在)加进该脚本的 lookup 词表,并补一条 self-test:`init()` 里未声明的 `getServiceAsync('X')` 必须判红。 需要顺带核对 `--list` 的边名输出(现在硬编码写作 `getService('X')`)。 ## 为什么不在 #4777 里一并做 #4777 建立的是另一条规则(「读注册表可以,**记账**不行」),两条门禁互补: **#4471 说「你得声明」,#4777 说「你没声明就不许记账」。** 扩 #4471 的词表是它自己的 scope,而且改动会波及一条与本次派发不同的判定语义,单独一 PR 更干净。 发现于:PR #4833(#4777)。
Activity
xuyushun441-sys commentedon Aug 3, 2026
认领(PM 分派)
session_018iARDqtrhQgz6fVHDeDkbQclaude/issue-4835-init-contract-getserviceasyncdomain:devx(落点scripts/check-init-service-contract.mjs)本会话独占
domain:devx,同批另有 #4843(scripts/objectui-range.mjs)与 #4801(packages/cli),落点互斥。派发要求里特别强调:fixture 必须包含修复前
AuthPlugin.init()的那个形状(getServiceAsync?.('cache'),即 #4772 真正溜过去的写法),并证明它在补词表前判绿、补之后判红。只观察到绿的门禁,与一个什么都匹配不上的门禁,从外部无法区分。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
审核通过 — PR #4902 已取消草稿并挂上 auto-merge(SQUASH)
核的是实物,不是报告措辞。
双向证明成立,而且比我要求的更有说服力。 反向验证用的是
f2eb85007^的AuthPlugin.init()逐字形状(可选调用 + 强转ctx+ best-effort try/catch),不是一个"意思差不多"的复刻。改前审计输出✓ ... (75 plugin unit(s) scanned)且 exit 0 —— 单元数从 74 涨到 75 这个细节值得记下来:文件被扫到了、类被识别成插件单元了,唯独那次调用不可见。这正是 #4772 当时的处境,也正是"只观察到绿"为什么什么都证明不了 —— 门禁当时不是没运行,是运行了而看不见判据。扩词表的成员资格是读代码定的,不是照议题抄的。 加
getServiceScoped的理由是它的 kernel 实现体与getServiceAsync逐字相同(同一个pluginLoader.getService(name, scopeId)),同一注册表、同一排序风险;议题提示的hasService没有加,因为ObjectKernel.hasAnyService是 private、PluginLoader.hasService的 loader 实例插件拿不到 —— 加了只会误伤别的对象上的同名方法。不臆造、也不因为议题提了就照收,这是对的;self-test case 19 把这个"不加"的决定也钉住了,以后想加的人得先推翻一条测试。预过滤那处是这个 PR 里最有价值的一笔,而且不在议题范围内。 旧代码
if (!text.includes('getService'))今天恰好能用 —— 因为'getService'是三个 accessor 的公共子串;但下一个不含该子串的入口会在 AST 看到它之前就被过滤掉,门禁照样报绿。改成从词表派生之后,这个二次隐患没了。这与本单要修的是同一形状的洞,dev 自己看出来并顺手堵上,没有扩到无关面。--list与报错文案改为按原文引用 accessor:一个把getServiceAsync打印成getService的机器可读输出是会撒谎的输出,读者照着它去搜代码会搜不到。落点 2 个文件(
scripts/+ changeset),零越界;验证用的临时 fixture 已删除,不在 diff 里。空 frontmatter changeset 符合仓内惯例(pr-automation.yml明确写了它与skip-changeset标签等价)。扩词表后仓库审计仍全绿、边数与 main 一致(41/1/3),没有把任何现存代码判红。Generated by Claude Code
fix(devx): init-service guard reads every service accessor, not just …
fix(scripts): check:doc-authoring 覆盖 .claude 语料,worktrees 进 SKIP_PATHS (
fix(tooling): derive the docs-audit scope from the filesystem instead…
fix(tooling): fail check-doc-authoring by name when a declared ROOT i…