feat(rest): direct-mount 的 9 条路由对 RestServer 可枚举,并进入 /openapi.json (#5822) - #6303
Conversation
…5822) The two registrars that bypass RouteManager (`package-routes.ts`, `external-datasource-routes.ts`) now declare their routes once, as data, hand that array to `mountDirectRoutes` and return it; the composition step (`mountAndRecordDirectRoutes`, called by `rest-api-plugin.ts`) records it on the `RestServer`. `getRoutes()` therefore answers for the whole mounted surface — each row carrying `source: 'route-manager' | 'direct-mount'` — and the OpenAPI built-in section #5588 / PR #5821 made a projection of that table now covers the nine, eight of which are `disposition: 'sdk'` capabilities. The description and the mount are the SAME array, so there is no second source of truth. Both honesty directions are pinned: a boot without the `package` service mounts, enumerates and documents no `packages.*` route, while the five federation routes mount unconditionally (503 per request when the service is absent) and are documented accordingly. The route-ledger conformance guard's second enumeration — mock-server registration capture — is deleted: it now drives the same composition function production does and partitions one `getRoutes()` answer by `source`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wbxm29qPKnLf44AbSxizqW
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
`packages/rest` 的 TEST_DEBT ratchet re-measure 抓到 163 -> 178 (+15): the new `direct-mount-introspection.test.ts` imported its siblings without the `.js` extension, which under `moduleResolution: nodenext` does not resolve — every imported symbol degrades to `any` and each callback over it reports TS7006. One broken import per line, not fifteen independent type errors (AGENTS.md, Build & Test). Extensions added there and in `rest-route-ledger.conformance.test.ts`, plus the two `mock.calls` callbacks retyped now that `vi.fn()` resolves properly. The test layer measures 144, so the new file contributes zero and the ledger entry (163) is left alone — the ratchet only refuses upward drift, and lowering it would collide with the other PRs moving the same entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wbxm29qPKnLf44AbSxizqW
CI 记录:第二个 commit(
|
Fixes #5822
packages/rest的 9 条 direct-mount 路由现在对RestServer可枚举,并随之进入GET {apiPath}/openapi.json的 built-in 段。按分诊晋级裁定走选项 1:两个 registrar返回自己实际挂载的路由描述,由组合步骤登记给
RestServer,事实来自实际调用点。前提重验(实读 origin/main,四项全部成立)
在
origin/main@f6609e6ae(开 PR 前已 rebase 到含 #6246 的a682670b1)逐条实读:disposition: 'sdk')rest-route-ledger.ts:210-225getRoutes()只回 RouteManagerrest-server.ts:8638getRoutes() { return this.routeManager.getAll(); }IHttpServerrest-api-plugin.ts:383-418(packages 受package服务门控;federation 无条件)rest-server.ts:3294buildBuiltinPaths(this.routeManager.getAll(), basePath)一处需要修正的细节:issue 正文说两个 registrar「分别在对应服务存在时调用」。federation
那 5 条其实是无条件注册的(
external-datasource-routes.ts模块注释写明,服务缺席时按请求答503),只有 packages 那 4 条受
package服务门控。这决定了诚实性两向的正确钉法(见下)。选项 1 的同源性:为什么不是第二真相源
每个 registrar 现在只声明一次自己的路由,声明成数据,交给
mountDirectRoutes(server, routes)—— 它逐条挂载后把同一个数组原样返回:调用方登记的那份描述,就是用来挂载的那份数组本身(
direct-mount-introspection.test.ts逐条比对返回值与 mock server 上的注册调用,连 handler 的函数标识都对齐)。因此不存在
「一份用来挂载、一份用来描述」的第二清单可以漂移 —— 这正是本单点名拒绝的形状。
组合步骤
mountAndRecordDirectRoutes(direct-mount-composition.ts)从rest-api-plugin.ts抽出:它是唯一知道「哪些 registrar 绕开 RouteManager、各自在什么条件下被调用」的地方。抽出的收益不只是整洁 —— conformance 测试现在驱动同一个函数,所以将来新增第三个
绕行 registrar 会自动进入台账守卫,而不是像以前那样需要有人记得同步测试里的那份副本。
诚实性两向怎么钉的
#5821刻意选的边界是「服务器不持有该事实就不许凭空补」。本 PR 补的是事实本身,不是猜测:getRoutes()(带source: 'direct-mount')与 openapi built-in 段,按台账逐行对;package服务的 boot,4 条packages.*既没挂到 wire 上,也不在
getRoutes()、不在文档里;文档说的只是「什么被挂载了」,从不声称
external-datasource服务在场。那个判断留在每次请求里(503),boot 期不记录(AGENTS.md「启动期不得记录一个 boot 还能推翻的判决」)。
与
openapi-builtin-paths.ts对 per-operationsecurity选的方向一致。#5588的集合关系(「文档里的每条路径都是已挂载的路由」,且两侧数量相等)在 9 条加入后重跑,依然成立。
考古:当初为什么绕开 RouteManager(台账的欠账)
结论:没有找到技术理由,是作用域/归属的产物,不是有意的绕行。证据是结构性的,写进了
direct-mount.ts的模块注释:IHttpServer的自由函数,由插件在RestServer已经注册完自己的路由之后组合,并且各自带服务门控;
RouteManager是RestServer的私有字段,而在rest-api-plugin.ts里const restServer = new RestServer(...)原本声明在try块内部 —— 到了下面registrar 的调用点,连
restServer都不在作用域里,更没有 manager 可注册;package-routes.ts现存注释只解释了「为什么不是POST /packages」(POST /api/v1/packages is a publish-vs-install shape collision — REST registers first and shadows the dispatcher (#3587 finding) #3610 的first-match-wins 撞车),从未提过绕开 manager;
RouteManager.register对这些路由也没有任何拒绝条件。
本 PR 只把那个作用域障碍去掉(把
restServer提到try外),没有走选项 2 —— 选项 2 会改动9 条路由的挂载顺序与 first-match-wins 语义(#3610 的教训就在这条线上),而选项 1 已经拿到
全部收益。
conformance 测试的两套枚举收敛为一套
rest-route-ledger.conformance.test.ts原来对 direct-mount 行另起一套枚举(mock server捕获注册调用),现已删除。它现在按生产方式 boot(
registerRoutes()+mountAndRecordDirectRoutes),问服务器要一次getRoutes(),再按每行的source分区,两个来源各自双向对账。两个副作用值得记一笔:
source列仍然被逐行校验,没有因为合并而失去。未收敛的部分,以及为什么:
package-envelope.conformance.test.ts/external-datasource-envelope.conformance.test.ts仍用 mock server 捕获注册。它们捕获的目的是拿到 handler 去驱动(校验响应信封),不是枚举路由面 —— 与本单要收敛的「第二套枚举」不是
同一件事,强行合并只会让它们绕远。
反向验证(先预测方向,再跑)
预测:把两处
recorder.recordDirectMountedRoutes(...)摘掉、只保留挂载(即回到「服务器不持有该事实」的旧世界),枚举/文档类断言应当变红;而「没有无台账的 direct-mount
路由」这类方向会空集通过 —— 因为它断言的是一个变空的集合,green 是「什么都没产出」而不是
「逻辑正确」。
实测(
vitest run src/direct-mount-introspection.test.ts src/rest-route-ledger.conformance.test.ts):8 failed | 9 passed,方向与预测一致。那两条空集通过的用例是如实记录、不是遗漏:它们守的是「不许有无台账的路由」,登记消失后集合变空,
断言当然还成立。正因为这类 green 靠的是「什么都没产出」,才需要旁边那条按数量对账的
reports them through the server(expected +0 to be 9)—— 它是这次收敛里唯一能抓住「登记被摘掉」的守卫。验证后已还原并复跑全绿。
验证
packages/rest没有typecheck脚本(type-check-coverage 的 DEBT 条目),仍手工跑了tsc --noEmit -p packages/rest/tsconfig.json:改动没有新增类型错误 —— 报出的 2 条TS2345 在
origin/main上同样存在(同两个调用点,行号 178/245 → 207/279),已另单记录。影响面
getRoutes()的返回值:服务在场时多 9 条,每条多一个source字段(新导出类型MountedRoute);既有的method/path/handler/metadata读法不变。changeset 记为@objectstack/restminor —— 自省面与公开文档新增了内容,并新增了导出类型,不只是修 bug。Generated by Claude Code