Skip to content

fix(spec,runtime,service-automation): IAutomationService 声明它早已在服务的 connector 注册表 (#4127) - #4150

Merged
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb
Jul 30, 2026
Merged

fix(spec,runtime,service-automation): IAutomationService 声明它早已在服务的 connector 注册表 (#4127)#4150
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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= 过滤:

if (typeof automationService.getConnectorDescriptors === 'function') {
    let connectors = automationService.getConnectorDescriptors() ?? [];
    if (query?.type) {
        connectors = connectors.filter((c: any) => c?.type === query.type);  // ← 在类型系统不知道存在的字段上过滤
    }

这个过滤离"打错字就静默匹配不到任何东西"只有一步之遥,而匹配不到时返回空注册表,恰好也是该路由在方法缺席时的合法返回——所以这个故障没有任何可区分的症状。

保持 optional,理由和 getActionDescriptors 一样:connector 注册表是 flow-engine 这个实现的能力,不是每个 automation 槽的固有属性。一个 script-runner 填这个槽时没有 connector 可描述,路由返回空注册表而非 404 —— handlerReady 表达不了这件事,槽是可服务的,缺的只是这一项能力。

2. 四个类型迁入 @objectstack/spec/integration

ConnectorDescriptor / 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 }漏掉了 originstate——两个都是 ConnectorDescriptor必填字段,也正是设计器用来区分"活的 declarative 实例 vs plugin connector"(ADR-0097 §4)、以及"可派发 vs 已降级但仍诚实列出而非静默消失"(#3017)的字段。

没有任何东西发现它,因为一个未声明的返回类型无从校验。现在 fixture 带上了类型,新增测试钉住 origin / state / degradedReason 确实穿过路由存活,而不是只有 nametype

验证

结果
@objectstack/spec 7089 tests / 272 files(新增 2 个契约测试)
@objectstack/service-automation 457 / 41
@objectstack/runtime http-dispatcher 218 tests(新增 1 个)
tsc --noEmit(spec) clean
pnpm lint clean
liveness + empty-state gates clean
5 个 generated-artifact gates clean

排查记录:本地首轮 http-dispatcher.test.ts 有 1 个失败(discovery honest capabilities (D12) 的 data 槽),stash 掉本 PR 全部改动后在干净 main 上同样失败,全量 pnpm build 后消失——是 worktree 里的 stale dist,与本改动无关。

🤖 Generated with Claude Code

https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw


Generated by Claude Code

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

vercel Bot commented Jul 30, 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 Jul 30, 2026 12:13pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/runtime, packages/services, @objectstack/spec.

115 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang marked this pull request as ready for review July 30, 2026 12:41
@os-zhuang
os-zhuang merged commit 41dcda3 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-storage-upload-typeerror-0exbcb branch July 30, 2026 12:42
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants