fix(spec,runtime,service-automation): IAutomationService 声明它早已在服务的 connector 注册表 (#4127) - #4150
Merged
os-zhuang merged 2 commits intoJul 30, 2026
Merged
Conversation
… connector registry it already serves (#4127) The fourth and last dispatcher call site from #4127 calling a method its contract never declared. The first three shipped in #4143; this one was held back because the fix is a type MOVE, not a type addition — `ConnectorDescriptor` was declared in service-automation's engine, one IMPLEMENTATION of `IAutomationService`. A contract cannot name a type living inside its own implementation, so `getConnectorDescriptors` could not be declared until the type had a home in the spec. `IAutomationService` += `getConnectorDescriptors?()`. It is the sibling of `getActionDescriptors`, declared since ADR-0018: the two fill the flow designer's `connector_action` node together — node vocabulary from one, the connector / action / input pickers from the other. Only one was written down. `GET /automation/connectors` has served the other since ADR-0022 by probing for the method and re-typing its own result as `any` to filter on `?type=` — a filter on a field the type system did not know existed, one typo from silently matching nothing and answering an empty registry, which is also what the route legitimately returns when the method is absent, so the failure had no distinguishable symptom. Optional for the same reason `getActionDescriptors` is: a connector registry is a capability of the flow-engine implementation, not a property of every automation slot. A script-runner filling the slot has no connectors to describe and answers an empty registry rather than 404 — `handlerReady` does not apply, since the slot is serveable and only this capability is absent. `ConnectorDescriptor` / `ConnectorActionDescriptor` / `ConnectorOrigin` / `ConnectorState` move to `@objectstack/spec/integration`, beside the ADR-0097 provider contract, for the reason that file already states about itself: they are pure types, so a connector plugin — or a designer client, or the dispatcher — speaks about registered connectors depending only on the spec, with no runtime coupling to the engine. `ConnectorOrigin` is ADR-0097 §4 vocabulary and `ConnectorState` is #3017 vocabulary; neither was ever engine-private in meaning, only in location. Nothing is renamed and no shape changes. service-automation imports the four back and re-exports them from its index — same names, same entry point — so every existing importer compiles unchanged. `ConnectorState` joins that re-export, which it should have been in all along: it is a required field of the descriptor the index has always exported. The dispatcher's connector fixture had already drifted, which is the concrete cost: it declared `{ name, label, type, actions }` and omitted `origin` and `state` — both REQUIRED, and both the fields a designer reads to tell a live declarative instance from a plugin one (ADR-0097 §4), or a dispatchable connector from a degraded one listed honestly rather than hidden (#3017). Nothing caught it, because an undeclared return type cannot be checked against. The fixture is typed now, and a new test pins that origin / state / degradedReason survive the route rather than only name and type. Refs #4127 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 115 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…rface snapshot `check:api-surface` gates `@objectstack/spec`'s public exports against a committed snapshot. Moving `ConnectorDescriptor` / `ConnectorActionDescriptor` / `ConnectorOrigin` / `ConnectorState` into `spec/integration` adds four exports to it. The gate's own verdict: 0 breaking (removed/narrowed), 4 added — additions only, which is what a type moving INTO the spec should be. Regenerated with `gen:api-surface`; the diff is exactly those four lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
os-zhuang
marked this pull request as ready for review
July 30, 2026 12:41
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
… not `any` (#4127) (#4168) #4127's most valuable item was the one it did not do: add a gate for the class. Its four contract gaps were found by a human sweeping the dispatcher by hand. A sweep is not repeatable, and this one was not complete. The root was one line — `getService(name: string): any` in domain-handler-registry.ts. Against `any`, a domain calling a method its contract declares and a domain calling a method nobody declares typecheck identically. That is what let #4087 ship a `/storage` handler passing two arguments no implementation takes, and what hid #4127's four. `CoreServiceContracts` is the slot -> contract ledger. `CoreServiceName` named the slots and `contracts/*` described them; nothing connected the two. It does now, and `getService<K>(name: K)` resolves through it, so a call outside the contract is a compile error at the call site. An entry is a claim, so entries are made only where the binding is evidenced: by the provider that registers the slot (service-storage -> file-storage; objectql -> data, whose own comment reads "ObjectQL implements IDataEngine"), or by dispatcher work that proved it (#4143/#4150 for automation, notification, i18n). `ui` is deliberately unmapped — the slot exists and domains/ui.ts serves it, but no IUiService was ever written. An unmapped slot resolves to `unknown`, not `any`, so it must be cast deliberately and the gap stays legible. Two findings within minutes of turning it on: - `/auth` called a method that does not exist. domains/auth.ts probed `authService.handler(request, response)`; `IAuthService` declares `handleRequest(request)` and `AuthManager` implements exactly that, with no `handler`. False on every deployment — #4143's dead `automation.trigger` again. #4127's sweep never mentions `/auth` in either its gap list or its "clean" list: the file the compiler flagged first is the one the human pass skipped. Not a live hole — the Hono adapter calls `handleRequest` itself and only falls through when no usable auth service answered — but reading the contract makes the branch reachable for the first time, so a host calling `handleAuth` directly WITH an auth service now gets it instead of mockAuthFallback's `mock_<uuid>` session. - `POST /analytics/sql` invoked an optional method unguarded. `generateSql?` is optional on IAnalyticsService — unlike `query`/`getMeta` beside it — so a provider without it answered a 500 from TypeError instead of saying the capability is absent. Answers `handled: false` now, the same 404 the entry gate already gives for absent analytics capability. `isServiceServeable` becomes a type guard (`svc is NonNullable<T>`). Every domain already calls it first on a resolved slot, so one predicate narrows away the `undefined` for the whole body — the null check and the capability check were always the same check. The test-side hole #4127 predicted, closed for this batch: THREE tests across two files mocked `{ handler }` for auth, including one whose subject was the resolution path, so it proved the lookup worked and nothing about the call. `ContractMock<T>` guards mock keys against the contract; signatures stay `unknown` so vi.fn() does not force everything back to `as any`. The automation mock's `trigger` stays as a labelled negative control outside the checked literal — a test asserting the route never calls it is the point. The 12 domains not calling `getService` are untouched. `resolveService`, which also takes non-CoreServiceName names like `protocol` and `objectql`, is left for a later batch rather than widened here. Refs #4127
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.
Refs #4127。#4143 的收尾——那个 issue 里的第四处、也是最后一处 dispatcher 调用了契约从未声明的方法。
前三处已随 #4143 合入;这一处当时被单独拎出来,是因为它的修法是类型搬家而非新增类型:
ConnectorDescriptor声明在@objectstack/service-automation的 engine 里,而 engine 是IAutomationService的一个实现。契约无法引用一个住在自己实现内部的类型,所以在把类型挪进 spec 之前,getConnectorDescriptors根本没法被声明。1.
IAutomationService+=getConnectorDescriptors?()它是
getActionDescriptors的孪生兄弟——后者从 ADR-0018 起就在契约里了。两者共同填满流程设计器connector_action节点的选择器:节点词汇表来自前者,connector → action → input 三级选择器来自后者。只有一个被写下来了。GET /api/v1/automation/connectors从 ADR-0022 起就在服务后者,做法是先探测方法存在、再把自己的返回值重新标成any才能按?type=过滤:这个过滤离"打错字就静默匹配不到任何东西"只有一步之遥,而匹配不到时返回空注册表,恰好也是该路由在方法缺席时的合法返回——所以这个故障没有任何可区分的症状。
保持 optional,理由和
getActionDescriptors一样:connector 注册表是 flow-engine 这个实现的能力,不是每个 automation 槽的固有属性。一个 script-runner 填这个槽时没有 connector 可描述,路由返回空注册表而非 404 ——handlerReady表达不了这件事,槽是可服务的,缺的只是这一项能力。2. 四个类型迁入
@objectstack/spec/integrationConnectorDescriptor/ConnectorActionDescriptor/ConnectorOrigin/ConnectorState搬到 ADR-0097 provider 契约旁边,理由就是那个文件已经写在自己头上的那条:它们是纯类型(Prime Directive #2),所以 connector 插件——或设计器客户端,或 dispatcher——谈论"已注册的 connector"时只需依赖 spec,不必和 engine 产生运行时耦合。ConnectorOrigin是 ADR-0097 §4 的词汇,ConnectorState是 #3017 的词汇;两者在语义上从来就不是 engine 私有的,只是位置上是。没有重命名,形状零变化。 service-automation 把这四个 import 回来并从自己的 index 再导出——同样的名字、同样的入口——所有既有 importer 无改动编译通过。
ConnectorState也加进了这个再导出列表,它本就该在里面:它是 index 一直在导出的那个 descriptor 的必填字段。3. 测试 fixture 早就漂移了——这是具体代价
dispatcher 的 connector mock 写的是
{ name, label, type, actions },漏掉了origin和state——两个都是ConnectorDescriptor的必填字段,也正是设计器用来区分"活的 declarative 实例 vs plugin connector"(ADR-0097 §4)、以及"可派发 vs 已降级但仍诚实列出而非静默消失"(#3017)的字段。没有任何东西发现它,因为一个未声明的返回类型无从校验。现在 fixture 带上了类型,新增测试钉住
origin/state/degradedReason确实穿过路由存活,而不是只有name和type。验证
@objectstack/spec@objectstack/service-automation@objectstack/runtimehttp-dispatchertsc --noEmit(spec)pnpm lint🤖 Generated with Claude Code
https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
Generated by Claude Code