Skip to content

dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127

Description

@os-zhuang

按 Prime Directive #10 记录,#4087(PR #4112)的根因排查时顺带扫出来的。

为什么要单独开一条

#4087 的根因不是"忘了门控",而是 dispatcher domain 的调用点和 packages/spec/src/contracts/* 对不上——而现有的所有 gate 都不查这件事:route-ledger.conformance.test.ts 只查"域有没有 ledger 行",client-url-conformance 只查"SDK 拼的 URL 有没有人挂",check-route-envelope 只查响应信封。没有任何一条查"这个 domain 调服务的方式,契约认不认"。

所以修完 #4087 我把其余 domain 按同一把尺子扫了一遍。结论:同类问题还有,但方向是反的#4087 是调用点编了一个没人实现的形状;下面这些是调用点和实现是对的,契约没声明。危害小得多(这些路由能正常工作),但它让契约不再是契约——packages/spec 是 Prime Directive #12 里"metadata 生产者和运行时之间那唯一一份合同",一份漏了半个面的合同挡不住下一个 #4087

扫描结果

1. /notifications —— 三条 SDK 路由,三个方法契约里都没有

INotificationServicepackages/spec/src/contracts/notification-service.ts)声明的全部是:

send(message): Promise<NotificationResult>;
sendBatch?(messages): Promise<NotificationResult[]>;
getChannels?(): NotificationChannel[];

domains/notifications.ts 调的是:

调用 路由 ledger disposition
72 service.listInbox(userId, { read, type, limit }) GET /notifications sdknotifications.list
79 service.markRead(userId, ids) POST /notifications/read sdknotifications.markRead
85 service.markAllRead(userId) POST /notifications/read/all sdknotifications.markAllRead

实现在 packages/services/service-messaging/src/messaging-service.ts:284 / :365,工作正常。

值得注意的是 domain 自己已经在 duck-type 了:

if (!isServiceServeable(service) || typeof service.listInbox !== 'function') return { handled: false };

#4058 给这行加的注释写得很清楚——typeof service.listInbox !== 'function' 这个判断"顺带把 dev stub 挡在外面了(那个只实现 send / sendBatch)"。dev stub 只实现 send/sendBatch,恰恰是因为它照着契约实现的。 契约漏了收件箱面,于是唯一按契约写的实现反而是"不完整"的那个,靠 duck-type 兜住。

2. /automation —— 三个方法契约里没有

IAutomationService 没有声明:

  • trigger(name, payload, { request }) —— domains/automation.ts:61,路由 POST /automation/trigger/:name
  • getConnectorDescriptors() —— :120,实现在 service-automation/src/engine.ts:1404
  • getFlowRuntimeStates() —— :140,实现在 engine.ts:1555

另外 :66automationService.execute(triggerName, body) 把 HTTP body 直接塞进契约声明为 AutomationContext 的第二个形参。同一文件 :201 传的是真正构造过的 automationContext,两处不一致——这个更接近 #4087 的形状,建议单独确认一下。

3. /i18n —— getFieldLabels 契约里没有

II18nService 声明了 t / getTranslations / loadTranslations / getLocales / getDefaultLocale? / setDefaultLocale? / getCoverage? / suggestTranslations?,没有 getFieldLabelsdomains/i18n.ts:105 同样在 duck-type:

if (typeof i18nService.getFieldLabels === 'function') { ... }

路由 GET /i18n/labels/:object/:locale 在 ledger 里是 sdki18n.getFieldLabels

扫干净的

/analytics/ui/security/keys/share-links/packages/meta/data 的调用点都在契约内。/ai 走的是自己的 route table(真实表在 cloud 的 service-ai/src/ai-route-ledger.ts),不在本次尺子范围内。

建议

按 Prime Directive #12,修契约,不是修调用点。 这三处的调用点和实现是一致的、能用的,错的是那份漏声明的合同:

  1. INotificationServicelistInbox / markRead / markAllRead(可选方法,因为收件箱是 service-messaging 才有的能力),然后 domains/notifications.ts 的 duck-type 可以退成 isServiceServeable 一个判断。
  2. IAutomationServicetrigger? / getConnectorDescriptors? / getFlowRuntimeStates?;顺便确认 execute(triggerName, body) 那处是不是应该和 :201 一样构造 AutomationContext
  3. II18nServicegetFieldLabels?domains/i18n.ts 的 duck-type 同样可以退化。

外加一条更值钱的:给这个类别加个 gate。 三处 duck-type(typeof x.foo === 'function')是同一个信号——每一处都是有人当时发现"契约里没有",然后选择绕过而不是补。可行的做法是让 domain 拿到的服务句柄带上契约类型(而不是 as any),调用不上就是编译错误。domains/notifications.ts:44 现在写的就是 as any

顺带:测试侧的同一个洞

#4087 能活这么久,直接原因是覆盖它的 5 个用例全都 mock 了 handler 想要的形状而不是契约声明的形状upload 只断言"被调用过"、download mock 成 { data, mimeType },而契约是 Promise<void>Promise<Buffer>)——包括 #4086 上周才加的那两个。如果 domain 测试的 mock 用 satisfies IStorageService 之类约束住,这类 mock 会直接编译不过。这比再加一条运行时 gate 便宜,也更早失败。

关联:#4087 / PR #4112#4058#4000、ADR-0076 D12、Prime Directive #10 / #12

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions