fix(client,service-i18n): ledger the autonomous service-mount surface; two i18n SDK calls reached nothing (#3636) - #3646
Merged
Conversation
…; two i18n SDK calls reached nothing (#3636) Tranche 3 of the #3563 route audit — the last un-audited server surface. The dispatcher ledger (#3563) and the REST ledger (#3587) each stop at their own package boundary, and two services mount outside both: they reach for the `http-server` service and register straight on `IHttpServer`, so neither RouteManager nor RestServer.getRoutes() has ever seen them. That left the SDK's whole storage surface, plus all of i18n, in the pre-#3563 posture: expressed, working, guarded by nothing. Ledgers + guards, one per package (no cross-package edges): - service-storage/src/storage-route-ledger.ts — 10 routes - service-i18n/src/i18n-route-ledger.ts — 3 routes - client/src/service-route-ledger-coverage.test.ts — client half Enumeration is real on both: the registrar runs against a capturing mock IHttpServer and its registration calls ARE the route set — for i18n by driving the plugin's actual init → start → kernel:ready lifecycle, so the deferral stays exercised. Both ledgers ratchet gap and mismatch at 0. Two wire-level 404s fixed. i18n.getTranslations sent /i18n/translations?locale=xx and i18n.getFieldLabels sent /i18n/labels/:object?locale=xx, while every serving surface — the service's mounts, the dispatcher's HTTP mounts, and the plugin-rest-api.zod.ts contract — mounts only the path form. Neither call could ever be answered. Both had carried a green `sdk` row in the dispatcher ledger since tranche 1, because that guard asks whether the client METHOD exists, not whether it speaks a URL anything mounts. The client now sends the path dialect (the resolution #3611 gave meta.getView), and i18n-wire-dialect.test.ts drives the real client at a real router — asserting the abandoned dialect 404s — so a revert cannot pass quietly. One response-shape fix. service-i18n's success bodies omitted the `success` flag unwrapResponse keys on, so the SDK handed callers the raw { data: … } wrapper against that provider while returning the declared unwrapped shape against the dispatcher: one method, two shapes, decided by which plugin mounted the route. Its handlers now emit the { success: true, data } envelope the i18n route group declares; `data` did not move, so direct body readers are unaffected. Storage audited clean — 7 SDK-expressed, 3 reviewed server-only (the browser capability URL objectql stamps into file-field payloads, and the two local-driver loopbacks). The chunked-upload family, flagged for triage, turned out fully expressed. Filed, not fixed: GET {base}/_local/file/:key is built by three call sites and mounted by none (#3641); the cross-surface URL conformance guard that would have caught every bug above mechanically is the capstone (#3642). Refs #3636, #3563, #3587. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K35y3ovfWtCkBHYFCqUfAt
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 33 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 27, 2026 12:56
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.
Closes #3636 —— #3563 路由审计的第三批,也是最后一个未审计的服务端表面。
为什么这个表面此前谁都看不见
前两批的账本各自停在自己的包边界上:dispatcher 账本只看
http-dispatcher.ts,REST 账本只看RouteManager+ 两个 direct-mount 注册器。而有两个 service 是绕过两者的——它们在kernel:ready时去取http-server服务,直接往IHttpServer上挂路由,所以RouteManager和RestServer.getRoutes()都从未见过它们。结果是 SDK 的整个 storage 表面加上全部 i18n,一直停在 #3563 之前的状态:能用、在跑、但没有任何东西守着。账本与守卫
每个包一份迷你账本,放在注册器旁边,不产生任何跨包依赖边(第一批的教训原样适用):
service-storage/src/storage-route-ledger.tsservice-i18n/src/i18n-route-ledger.tsclient/src/service-route-ledger-coverage.test.ts两侧的枚举都是真枚举,不是手写清单:让注册器对着一个会记录调用的 mock
IHttpServer跑一遍,它的注册调用本身就是路由集合。i18n 这边进一步走插件真实的init → start → kernel:ready生命周期,这样那个延迟注册的行为本身也持续被覆盖到。两份账本的gap与mismatch都以 0 上棘轮。账本跨包时按相对源码路径导入(它们是纯数据、零 import),不建 service→client 的包依赖——CI 的分包测试任务只构建自己的依赖闭包,真加一条边会直接构建不出来。
顺带查出并修掉的两个线上 404
i18n.getTranslations发的是/i18n/translations?locale=xx,i18n.getFieldLabels发的是/i18n/labels/:object?locale=xx;而所有提供服务的表面——本插件的挂载、dispatcher 的 HTTP 挂载、以及plugin-rest-api.zod.ts里声明的契约——挂的都只有 path 形式。这两个调用从来就没有任何东西能应答。值得注意的是:它们从第一批起就在 dispatcher 账本里各挂着一行绿色的
sdk。因为那个守卫问的是"客户端方法在不在",而不是"它拼出来的 URL 有没有人挂"。dispatcher 的 domain handler 内部其实同时接受 query 方言,但没有任何路由会把裸/translations送到那段代码——这份宽容根本到达不了。客户端改为发 path 方言(和 #3611 给
meta.getView的处理一致)。新增的i18n-wire-dialect.test.ts把真实 client 打到真实 router 上,并断言旧方言在线上就是 404,所以回退改不掉这条守卫。三处独立佐证 path 形式才是既定契约,client 是唯一跑偏的一方:
plugin-rest-api.zod.ts的 endpoint 声明:/translations/:locale、/labels/:object/:localecontent/docs/api/plugin-endpoints.mdx、content/docs/kernel/services-checklist.mdx写的都是 path 形式(所以本 PR 无需改文档)packages/qa/http-conformance/src/adapter.test.ts早就断言/api/v1/i18n/labels/task匹配不上/labels/:object/:locale一处响应体形状修正
service-i18n的成功响应少了unwrapResponse用来判断信封的success标志,于是同一个 SDK 方法:对着这个 provider 返回的是没拆开的{ data: … },对着 dispatcher 返回的却是声明的解包形状——一个方法两种形状,取决于当时是哪个插件挂的路由。三个 handler 现在统一发i18n路由组声明的{ success: true, data }信封(plugin-rest-api.zod.ts→response_envelope)。data位置没动,直接读 body 的调用方不受影响。Storage 审计结果:干净
7 条 SDK 已表达,3 条
server-only(objectql 写进 file 字段载荷、由<img src>直接跟随的浏览器能力 URL,以及两条 local-driver 回环)。#3636 里点名需要甄别的分块上传家族,实际查下来 SDK 已完整表达(initChunkedUpload/uploadPart/completeChunkedUpload/resumeUpload),没有 gap 要补。只登记未修的两项
GET {base}/_local/file/:keyis built in three places but mounted nowhere #3641 ——GET {base}/_local/file/:key有三处在拼这个 URL,却没有任何注册器挂过它。两个 in-tree adapter 都恰好绕开了那条 fallback 分支,所以是潜伏而非已发作。账本头部显式写明这条路由的缺席,免得守卫被读成"审过并认可"。验证
turbo run test --filter=...@objectstack/service-i18n --filter=...@objectstack/service-storage—— 76/76 通过check-nul-bytes/check-doc-authoring/check-role-word/check-org-identifier均通过三个表面至此全部入账,审计文档
docs/audits/2026-07-dispatcher-client-route-coverage.md新增 §9 记录本批结论。objectui不调用 SDK 的 i18n 命名空间,本任务无需改动该仓库。