feat(spec)!: 声明 routes.mcp,并把 discovery 一致性闸门下沉到 routes 一层 (#5679) - #5743
Conversation
…gate to routes keys (#5679) `/discovery` advertises `routes.mcp`, objectui reads it, and `ApiRoutesSchema` never declared it — #4828's defect one level down, with the opposite disposition: `endpoints` was retired for having no reader, `mcp` has two real ones and is in fact the only `routes.*` key anything in objectui reads. `ApiRoutesSchema` is a plain z.object, which strips unknown keys, so any consumer parsing /discovery through the spec dropped `routes.mcp` silently. - spec: declare `mcp: z.string().optional()` as MEASURED off both producers — a path string, always the unscoped base (/mcp is mounted bare), optional rather than nullable (the key is absent, never null, when MCP is off or unserveable). - rest: drop the two `as any` casts at the emit site. Type-only; the emitted body is byte-identical. With the key undeclared, removing them produced two TS2339; with it declared, tsc returns to its ratcheted baseline of 2. - gates: the #4828 conformance tests now check `routes` keys as well as top-level ones in all three producer packages, deriving the allowance from ApiRoutesSchema. Extended one level, not recursed. Corrects the issue's premise on one point: the runtime dispatcher's getDiscoveryInfo() also emits `routes.mcp` (its routes literal always carries the key, holding the path or undefined), so both producers were affected and the new gate went red on both before the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…5679) CI 上两条不同签名的红,都是合并 origin/main 之后才出现的: 1. `@objectstack/client` DTS build — TS2741: `getRoute()` 的约定回退表 `Record<ApiRouteType, string>` 按设计对 `keyof ApiRoutes` **全键必填**, 所以给 ApiRoutesSchema 新增一个已声明键就必须给出它的约定路径。这是我 消费半径漏掉的包。补 `mcp: '/api/v1/mcp'` —— 不是猜的,正是两个 discovery 生产者实测发出的值,所以回退值与被发现值一致而非互相打架;而且该表本就 是 unscoped 约定表(每行都是 /api/v1/...),恰好符合 /mcp 裸挂载的事实。 全仓复扫 `keyof ApiRoutes` / ApiRoutes 型字面量,确认没有第三处:其余站点 都是 `Partial<ApiRoutes>` 或只填两个必填键,不受影响。 2. `check:authorable-surface` — 合并带进了 #5721 的 `ui/ActionSession` 源, 但 authorable-surface.json 未在合并树上重生成。跑 gen:schema 重出,该文件 现同时含本单的 `api/ApiRoutes:mcp` 与 main 的 ActionSession 三键。 `authorable-surface.base.json` 的 baseRev 重锚照 #5358 再次剔除 —— 那是 main 前移导致的机械重锚,不属本单。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
修复提交
|
| 检查 | 结果 |
|---|---|
check:generated |
10/10 ✓(含 check:docs、check:authorable-surface、check:api-surface) |
packages/spec typecheck |
OK |
packages/client build(原 TS2741) |
ESM/CJS/DTS Build success |
packages/rest tsc --noEmit |
2 = ratchet 基线 |
check:nul-bytes |
OK(5642 files,无裸控制字节) |
一条过程教训(记给后来者)
中途我一度把另一个并行 agent 的 check:generated 输出当成了自己的:本容器里多个 agent 共享同一个 scratchpad 目录,我用的 checkgen.log 与别人重名了 —— 那份日志里 0 处提到我的工作树、4 处指向 objectstack-5315-tenantfield。改用带 issue 号的唯一文件名后重跑才拿到自己的结果。并行跑 agent 时,日志文件名必须带任务标识,否则会拿别人的绿/红当自己的证据。
Generated by Claude Code
Fixes #5679
结论先说
前提基本成立,但有一处需要更正:
routes.mcp确实被发出、被 objectui 真实消费、却从未在ApiRoutesSchema里声明。但 issue 正文那句「dispatcher 侧的getDiscoveryInfo()不发routes.mcp,只有 REST 这条路径发」是错的 —— 两个生产者都发。详见下面「premise 更正」一节。这是 #4828 的同族缺陷低一层,而处置方向与
endpoints相反:endpoints因零消费者被删,mcp有两个真实消费者(而且它是 objectui 里唯一被读的routes.*键),所以是补声明,不是删。为什么是缺陷,不是洁癖
ApiRoutesSchema是普通z.object,默认 strip 未知键。所以任何按 spec 严格解析/discovery的消费者都会把routes.mcp悄悄丢掉 —— Integrations 的 MCP 连接卡直接空掉,且无任何报错。今天没炸,只是因为 objectui 那两处读的是原始 JSON,没走 schema。同一次测试运行里能直接看到这个盲区:
DiscoverySchema.safeParse(body)在mcp未声明时依然是绿的(被 strip 掉了),只有键集检查能看见它。测得的形状(不是猜的)
先测后声明。两个生产者的实测结果:
routes.mcp"/api/v1/mcp"env_alpha)"/api/v1/mcp"—— 仍是无环境段的路径,而同一响应里routes.data是/api/v1/environments/env_alpha/dataOS_MCP_SERVER_ENABLED=falsedelete掉)"/api/v1/mcp"undefined(JSON.stringify在传输时丢弃)据此声明为
mcp: z.string().optional()—— optional 而非 nullable:两个生产者都不会发null。/mcp是裸挂载的,所以永远是 unscoped base。改动
packages/spec:ApiRoutesSchema声明mcp,注释里写清测得形状、为何 optional、以及为何与endpoints处置相反。packages/rest:去掉发出点的两处as any。纯类型改动,发出的响应体逐字节不变 —— 但那个 cast 的消失本身就是结构性证明(见下)。discovery-schema-conformance.test.ts现在除顶层键集外,也检查routes一层的键集,allowance 同样从ApiRoutesSchema推导(而不是手写数组),与顶层从协议 schema 推导的做法一致。capabilities/services是z.record,键本来就是开放的。先证红
两个方向各一条,方向都是事先定好的:
红证 1 —— 类型(结构性证明)。
@objectstack/rest的 typecheck ratchet 把该包钉在恰好 2 个错误。基线确认为 2(两条package-routes.ts的 TS2345)。在未声明mcp的前提下去掉 cast:2 → 4,
check:type-check-coverage会红。补上声明后回到恰好 2,cast 已删除且通过类型检查。红证 2 —— 闸门。 在未声明的 schema 上,新的 routes 键集断言在两个生产者上都红:
同一次运行里
satisfies the canonical DiscoverySchema保持绿 —— 这正是 strip 盲区本身。premise 更正
issue 正文的括号注写着 dispatcher 不发
routes.mcp。实测不成立:packages/runtime/src/http-dispatcher.ts的 routes 字面量里始终带着这个键键恒在,值是路径或
undefined。之所以看起来「不发」,是因为JSON.stringify会丢弃undefined值 —— 但Object.keys()看得见,新闸门也就在这个生产者上照样红了。结论:受影响的是两个生产者,不是一个,补一次声明同时覆盖两者。metadata-protocol的getDiscovery()则确实不发:它的 routes 标注了const routes: ApiRoutes,编译器把它按住在已声明键集内。那一侧的闸门改动前后都是绿的(诚实说明:它不是红证,是回归闸门)。验证
packages/rest全量testpackages/spec全量testpackages/metadata-protocol全量testpackages/runtimeconformancepackages/resttsc --noEmitpackages/spectypecheckcheck:nul-bytes+ 控制字符自查中途
packages/rest曾出现 8 条 OpenAPI 用例红 —— 定位为我本地验证顺序的副作用:check:authorable-surface会重跑build-schemas.ts但不跑gen:openapi,把packages/spec/json-schema/openapi.json清掉了,而该路由在运行时从磁盘读它,于是返回 503。重跑gen:openapi后全绿。该目录是 gitignore 的,不进提交,CI 的build本身就是gen:schema && gen:openapi && tsup,顺序正确。生成物
packages/spec/authorable-surface.json:+1 行api/ApiRoutes:mcpcontent/docs/references/api/discovery.mdx:+1 行表格项(该文件头部标注 AUTO-GENERATED,经gen:docs重生成)authorable-surface.base.json的 baseRev 重锚(带进 feat(spec,runtime,metadata-protocol)!: discovery 两个生产者统一到一个 schema —— capabilities 正名、features/endpoints 退役、scoping 声明 (#4828) #5682 的两个scoping键)已 revert —— 闸门对此只报 informational(「trails the merge base by 2 key(s)」),不属本单。Changeset
@objectstack/specminor(新声明键)+@objectstack/restpatch。rest 侧是纯类型改动、响应体不变,按仓库惯例仍给 patch 以记录 cast 的移除。已合入最新
origin/main(#5721/#5722/#5731 等),无冲突。Generated by Claude Code