fix(spec,runtime,metadata-protocol): discovery's "install this to enable" names a package that exists (#4093 follow-up) - #4204
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
marked this pull request as ready for review
July 30, 2026 15:56
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
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.
discovery 对一个缺失能力说两件事:它不在,以及该怎么办。第一件从 #2462/#4000 起被反复打磨(整条 D12 线都在做这个);第二件是从槽位名字猜出来的,而且从来没人校验过。
Install a ${slot} plugin to enable,套在 12 个槽位上plugin-redis、plugin-bullmq、job-scheduler、plugin-notifications、plugin-storage、plugin-automation、ui-plugin,以及plugin-ai/plugin-search/plugin-workflow(这三个槽位根本没有任何实现)这个值同时还作为 discovery 的
provider输出。为什么这算 bug 而不是文案瑕疵:它是这条线一直在关的那个
declared ≠ enforced,只是上移了一层 —— 不是"能力在不在",而是**"给的修复方案是不是真的"**。一个装不上的包,是在人正要修自己的栈时递过去的死胡同;而读 discovery 的 agent 无法区分"该装的包"和"从来不存在的包"。改动
CORE_SERVICE_PROVIDER+serviceUnavailableMessage()落在@objectstack/spec/system,两个 discovery builder 都读它 —— 于是两个宿主不可能对同一个槽位说出不同的包名(#4089 / #4130 为metadata、data关掉的正是这种漂移),并加了对齐测试。表里的每一条都是按"谁真的
registerService了这个槽位"核实的,不是按名字相似。这就是为什么查出notification实际由@objectstack/service-messaging填充 —— 唯一一个包名与槽位名毫无字面关系的槽位,也正是"靠名字猜"在原理上不可能对的证据。4 个槽位(
ai/search/workflow/graphql)没有任何实现,所以null是一等条目,文案直说"没有实现可装",而不是编一个看起来合理的包名。ui保留 #4146 那句更完整的说明(/ui由protocol服务提供、没有任何东西注册ui槽位)—— 只说"装 X"会被读成"X 填这个槽位",读者装完回头一看槽位还是空的。这句话是对齐测试逼出来的:两个 builder 指向同一个包但文案不同,该赢的是信息更多的那句,而不是把它抹平。让它不再腐烂
scripts/check-service-providers.mjs(接入 lint workflow,check:service-providers)对两种腐烂方式都会让 CI 红: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