Skip to content

check:init-service-contract 只认 getService,不认 getServiceAsync —— #4772 就是从这个洞里溜过去的 #4835

Closed

Description

@xuyushun441-sys

在 #4777 建门禁时顺带发现,不在该 issue 范围内,按 Prime Directive #10 单独记录。

发现

scripts/check-init-service-contract.mjs(#4471 / ADR-0116 的声明覆盖门禁)只匹配一个 callee:

// 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^):

cache = await (ctx as { getServiceAsync?: (n: string) => Promise< unknown > }).getServiceAsync?.('cache');

当前是否已经红?

否。今天 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

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    认领(PM 分派)

    • session:session_018iARDqtrhQgz6fVHDeDkbQ
    • 分支:claude/issue-4835-init-contract-getserviceasync
    • 车道:domain: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

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    审核通过 — 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions