Skip to content

fix(spec,runtime,metadata-protocol): discovery's "install this to enable" names a package that exists (#4093 follow-up) - #4204

Merged
os-zhuang merged 4 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28
Jul 30, 2026
Merged

fix(spec,runtime,metadata-protocol): discovery's "install this to enable" names a package that exists (#4093 follow-up)#4204
os-zhuang merged 4 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

discovery 对一个缺失能力说两件事:它不在,以及该怎么办。第一件从 #2462/#4000 起被反复打磨(整条 D12 线都在做这个);第二件是从槽位名字猜出来的,而且从来没人校验过。

  • dispatcher:Install a ${slot} plugin to enable,套在 12 个槽位
  • metadata-protocol:手写表,15 个里有 10 个指向不存在的包 —— plugin-redisplugin-bullmqjob-schedulerplugin-notificationsplugin-storageplugin-automationui-plugin,以及 plugin-ai / plugin-search / plugin-workflow(这三个槽位根本没有任何实现)

这个值同时还作为 discovery 的 provider 输出。

为什么这算 bug 而不是文案瑕疵:它是这条线一直在关的那个 declared ≠ enforced,只是上移了一层 —— 不是"能力在不在",而是**"给的修复方案是不是真的"**。一个装不上的包,是在人正要修自己的栈时递过去的死胡同;而读 discovery 的 agent 无法区分"该装的包"和"从来不存在的包"

改动

CORE_SERVICE_PROVIDER + serviceUnavailableMessage() 落在 @objectstack/spec/system,两个 discovery builder 都读它 —— 于是两个宿主不可能对同一个槽位说出不同的包名(#4089 / #4130metadatadata 关掉的正是这种漂移),并加了对齐测试。

表里的每一条都是按"谁真的 registerService 了这个槽位"核实的,不是按名字相似。这就是为什么查出 notification 实际由 @objectstack/service-messaging 填充 —— 唯一一个包名与槽位名毫无字面关系的槽位,也正是"靠名字猜"在原理上不可能对的证据。

4 个槽位(ai/search/workflow/graphql)没有任何实现,所以 null 是一等条目,文案直说"没有实现可装",而不是编一个看起来合理的包名。

ui 保留 #4146 那句更完整的说明(/uiprotocol 服务提供、没有任何东西注册 ui 槽位)—— 只说"装 X"会被读成"X 填这个槽位",读者装完回头一看槽位还是空的。这句话是对齐测试逼出来的:两个 builder 指向同一个包但文案不同,该赢的是信息更多的那句,而不是把它抹平。

让它不再腐烂

scripts/check-service-providers.mjs(接入 lint workflow,check:service-providers)对两种腐烂方式都会让 CI 红:

  1. 名字不是真实的 workspace 包(改名/删包/手滑)
  2. CoreServiceName 里的槽位在表里没有条目(会退化成"补救指引指向 undefined")

两种都实测破坏过一次确认能抓到。和今天早些时候抓到我自己那条陈旧豁免的 check-route-envelope 同一个精神 —— 守卫必须比修复活得久,否则下一次改名就把它带回来了。

验证

spec 7143 / runtime 947 / metadata-protocol 110 全过;build 71/71;pnpm lint全部 11 个 check:* 干净。

FROM → TO

多数 unavailable 槽位的 services.<slot>.message.provider 文本变化。任何在匹配旧文案 Install a <slot> plugin to enable 的地方,应改为匹配 status: 'unavailable' —— status 才是契约,message 是给人和 agent 读的散文

🤖 Generated with Claude Code

https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B


Generated by Claude Code

…ble" names a package that exists (#4093 follow-up)

Discovery tells a consumer two things about an absent capability: that it is
absent, and what to do about it. The first has been carefully honest since
#2462/#4000 — the whole D12 lineage is about it. The second was invented from
the slot name, and nothing ever checked it.

The dispatcher templated `Install a ${slot} plugin to enable` across twelve
slots. metadata-protocol hand-wrote a table where TEN OF FIFTEEN entries named
a package that does not exist: plugin-redis, plugin-bullmq, job-scheduler,
plugin-notifications, plugin-storage, plugin-automation, ui-plugin, plus
plugin-ai / plugin-search / plugin-workflow for slots nothing implements at
all. The same value is surfaced as discovery's `provider`.

That is the same `declared ≠ enforced` failure this lineage has been closing,
one level over: not "does the capability exist" but "is the fix real". A
package that cannot be installed is a dead end handed to someone at the exact
moment they are trying to fix their stack, and an agent reading discovery
cannot tell it apart from a package it should install.

CORE_SERVICE_PROVIDER + serviceUnavailableMessage() in @objectstack/spec/system
are now the one place that sentence is written, read by both builders — so the
two hosts cannot name different packages for the same slot, the drift #4089 and
#4130 closed for the `metadata` and `data` entries. A parity test asserts it.

Entries were verified against what actually calls `registerService` for each
slot, not against name similarity. That is how `notification` turned out to be
filled by @objectstack/service-messaging — the one slot whose package shares no
word with its name, and the proof that a name-derived guess cannot be right in
general.

Four slots (ai, search, workflow, graphql) have no implementation anywhere, so
`null` is a first-class entry and the message says nothing ships rather than
naming a plausible package. `ui` keeps the fuller sentence from #4146 — it is
served by the `protocol` service and nothing registers the `ui` slot, so
"install X" alone would read as "X fills this slot" and leave the reader
staring at an empty slot afterwards. Moving it into the shared table is what
the parity test surfaced: the two builders agreed on the package but not on
the sentence, and the richer one deserved to win rather than be flattened.

scripts/check-service-providers.mjs (wired into the lint workflow as
check:service-providers) fails CI on either way this table rots: a name that is
not a real workspace package, or a CoreServiceName slot with no entry at all.
Both modes verified by temporarily breaking the table. Same spirit as
check-route-envelope, which caught a stale exemption of mine earlier today —
the guard has to outlive the fix, or the next rename re-introduces it.

Verified: spec 7143, runtime 947, metadata-protocol 110 pass; build 71/71;
pnpm lint and all eleven check:* scripts clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@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 3:42pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file protocol:system tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec.

113 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 @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 @objectstack/metadata-protocol, 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/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • 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, @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 @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.

claude added 3 commits July 30, 2026 15:15
…t ships now

The drift check flagged 113 docs — the blast radius of touching
@objectstack/spec — but only two of them state a remedy this PR changes, and
both do it by showing real discovery output:

- `protocol/kernel/http-protocol.mdx` printed `Install plugin-workflow to
  enable` and `Install plugin-ai to enable`. Wrong twice over now: neither
  package exists, and both slots have no implementation at all, so the message
  says so instead. Its `provider` for auth is scoped too, matching the field.
- `kernel/services-checklist.mdx` carried the old template verbatim, down to
  its grammar bug: `"Install a auth plugin to enable"`.

Deliberately not touched: `references/api/discovery.mdx` is auto-generated;
`releases/v9.mdx` records what v9 said and should keep saying it;
`services-checklist.mdx`'s Phase 1/2/3 table is a roadmap of plugins to build,
not an install instruction; `plugins/anatomy.mdx` names `ui-plugin` as a kind
of plugin, not a package to fetch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
The `TypeScript Type Check` job's last step is spec's API-surface guard, and it
reported exactly what this PR intends: `0 breaking (removed/narrowed), 2 added`
— `CORE_SERVICE_PROVIDER (const)` and `serviceUnavailableMessage (function)`
under `./system`. Regenerated per its own instruction; the snapshot diff is
those two lines and nothing else.

Worth the ceremony: spec is the constitution, and this PR puts a table in it
that both discovery builders now depend on. An addition nobody had to
acknowledge is how a "shared" source of truth quietly becomes a second one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
…ackage name (#4093 follow-up)

`protocol-discovery.test.ts` pinned `services.auth.provider` to the bare label
`plugin-auth`. It now comes from CORE_SERVICE_PROVIDER, which names the package
you can actually install — the FROM → TO the changeset already declares for
this field.

Scoped is the point, not incidental: `provider` is what a consumer (or an
agent) reads to decide what to fetch, and `@objectstack/plugin-auth` is
copy-pasteable into an install command where `plugin-auth` is a guess at best
and, for the ten slots this PR corrected, wrong outright.

Caught by Test Core, not by my filtered local runs — a `provider`/`message`
assertion can live in any package that exercises a discovery builder, and this
one is in objectql.

Deliberately NOT touched: the `provider: 'plugin-search'` and `Install
plugin-workflow to enable` strings in spec's discovery/dispatcher schema tests.
Those are arbitrary payloads proving ServiceInfoSchema accepts a shape, not
claims the platform makes — and "fixing" them means inventing a slot→package
pairing for slots that have none, which is the very thing this PR removes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 15:56
@os-zhuang
os-zhuang merged commit 86a71d1 into main Jul 30, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-handler-ready-1odd28 branch July 30, 2026 15:57
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…ot a 404 that blames the route (#4093 follow-up) (#4222)

Two different facts were being answered with whatever each domain happened to
reach for, and only `mcp` told them apart:

  - The route is not there. `/mcp` when the server is disabled for the
    environment; `/analytics` when the service is unserveable, because
    dispatcher-plugin gates the MOUNT and never registers those paths (#4000).
    A path the server does not expose is a 404 from the host's own router.
    Unchanged — that half was already right.

  - The route is there; the implementation is not. Every unconditionally
    mounted domain. The request reached a handler with nothing to delegate to.
    That is 501, and it is what changes here.

`/automation` and `/notifications` returned `{ handled: false }`. That looks
neutral, but it is not a fall-through: the dispatcher plugin's single exit
turns it into `404 ROUTE_NOT_FOUND` with the hint "No handler matched this
request. Check the API discovery endpoint for available routes." Both halves
are false. A handler DID match — it just had nothing behind it — and discovery
correctly does not list the route, so the hint points at a page that will never
mention it. An operator reads that as a routing bug and goes looking for one
that does not exist.

`/ui` answered 503, which claims the condition is temporary. An uninstalled
MetadataPlugin does not become installed by retrying. `/ai` answered 404 for
the same mounted-but-unimplemented case.

The refusal is written once (`domains/unavailable.ts`) and carries the same
remedy sentence discovery reports for the slot, via `serviceUnavailableMessage`
from the table #4204 added — so the wall and the discovery entry cannot drift
into naming different fixes. `POST /api/v1/automation` on a stack without the
service now answers `501 Install @objectstack/service-automation to enable`.

`/ai` keeps a local message deliberately: its real provider ships outside this
workspace as a Cloud/EE package, so CORE_SERVICE_PROVIDER — verified against
workspace packages by check:service-providers — records `null` for that slot
and the shared sentence would say "nothing ships", which is wrong. That gap
between "no workspace package" and "no package" is worth knowing about; the
override keeps this change honest without widening the guard's contract here.

Deliberately unchanged: analytics's route-mount gate (a genuinely absent path);
mcp's 404-vs-501 pair, which is the model this generalizes; the `handled: false`
at the END of each domain, which means "no sub-route matched" and is a true
404; `GET /ai/agents`'s empty-list 200 (a deliberate courtesy for the console's
per-navigation poll); and `/ai`'s `503 routes not yet initialized`, a different
condition — service present, internal state unready — that may genuinely be
transient.

This was already the house convention for one surface: the API docs described
`/graphql` as "route is wired but bring-your-own service — returns 501 unless
an implementation is registered". The rule now holds everywhere, and those docs
state it as a rule rather than a GraphQL quirk, including that nothing answers
503 for a missing capability (that status is for transient states like a
still-booting kernel on `/ready`).

Tests: nine pinned the old answers and are updated to the new one, with the
part that actually mattered preserved in each — the automation stub is still
asserted NEVER CALLED, the notifications stub's `listInbox` likewise, and
`/ai/agents` still returns its empty list. route-parity's integration assertion
keeps its invariant (not advertised ⟹ not served, #3369) and now states why 501
expresses it better than the 404 it used to assert.

Verified: full `pnpm test` 132/132 tasks; runtime 955; build 71/71; pnpm lint
and every check:* gate clean (route-envelope needed the new domains/ file
declared as handBuilt: 0 — it answers only through deps.error, which is that
check's own thesis applied to the refusal itself).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Jul 31, 2026
…ted sentence

The drift check flagged it and a targeted grep found it: http-protocol.mdx's
discovery sample printed the `ai` slot's message as "No implementation ships
for the 'ai' slot" — the line I wrote in #4204, made stale by this PR
correcting the very claim it quotes.

The `workflow` entry beside it is left alone: nothing registers that slot in
either repository, so its sentence is still true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants