Skip to content

GET /meta/:type/:name answers two different body shapes on the same request — the cached branch (the DEFAULT) returns the bare document, the non-cached branch returns the spec-declared { type, name, item } envelope #5563

Closed

Description

@baozhoutao

发现于 #5545 的前提复核(要给 client.meta.getItem 补返回类型注解,先去 spec 锚定响应形状)。基线 origin/main @ 229d29ea4。

事实

packages/spec/src/api/plugin-rest-api.zod.ts:773 为 GET /meta/:type/:name 声明了唯一的响应 schema:

responseSchema: 'GetMetaItemResponseSchema'

packages/spec/src/api/protocol.zod.ts:193 里它是三键信封:

GetMetaItemResponseSchema = z.object({
  type: z.string(),
  name: z.string(),
  item: z.unknown(),
})

但同一条路由在 packages/rest/src/rest-server.ts:4020 的 handler 里有两条互斥分支,响应体形状不同:

  • 缓存分支(rest-server.ts:4076 的条件成立时)→ res.json(... result.data ...)(:4128)。result 来自 getMetaItemCached,而 packages/metadata-protocol/src/protocol.ts:5409-5411 里它做的正是 const item = (result as any)?.item;、:5441 return { data: item, ... } —— 也就是说 result.data 是已经拆掉信封的裸元数据文档。
  • 非缓存分支 → res.json(... visible ...)(:4238),visible = await p.getMetaItem(...),即 { type, name, item, lock, ... } 信封,与 spec 一致。

关键在于缓存分支是默认路径:packages/spec/src/api/rest-server.zod.ts:285 是 enableCache: z.boolean().default(true),rest-server.ts:2796 也是 metadata.enableCache ?? true。于是 spec 声明的那个形状,在默认配置下反而是取不到的那一个;信封只在关掉缓存、或 app / doc / book / ?state=draft / ?preview=draft / ?package= 这些绕过缓存的读法下才出现。第三种形状还有 ?layers=true(rest-server.ts:4031-4044 的 getMetaItemLayered),与前两者都不同。

仓内早就在注释里承认了这件事,只是当作运行时事实处理而非缺陷 —— packages/rest/src/import-prepare.ts:18-32 的 isMetaEnvelope:

Detect the getMetaItem response envelope ({ type, name, item, lock, … }) … The cached read path and getMetaItems element shape hand back the already-unwrapped document instead, so translation helpers must distinguish the two

rest-server.ts:2631-2637 同一段话再写了一遍。isMetaEnvelope 在 rest-server.ts 有 5 处调用,全部是在为「这次拿到的到底是哪种形状」做运行时嗅探。

runtime 那条服务面同病:packages/runtime/src/domains/meta.ts:247 命中 protocol 时 deps.success(data) 送信封,而 :216-218 的 registry 兜底送 qlService.registry.getObject(name)(裸文档),:258-260 的 metaSvc 兜底同样是裸文档。

实测

在 worktree 里对着真 RestServer 跑同一条请求(GET /api/v1/meta/object/customer,无 query),只切换 protocol 是否提供 getMetaItemCached,捕获 res.json 的实参:

CACHED  BRANCH BODY = {"name":"customer","label":"Customer","fields":{}}
NONCACHED BRANCH BODY = {"type":"object","name":"customer","item":{"name":"customer","label":"Customer","fields":{}}}

(探针脚本是一次性的,未提交。)

影响

  • 同一个公开 endpoint,同一个请求,响应体结构随服务端配置而变 —— 任何消费方都只能运行时嗅探,或者干脆 as any。这正是 client SDK 的 meta.getItem 没有声明返回类型(载荷为 unknown),而并排的 meta.getItems 有 —— 同一表面上相邻两个方法的类型化不对等 #5545 卡住的原因:client.meta.getItem 想补一个诚实的返回类型注解,而现在没有一个类型是对的 —— 标信封则默认配置下撒谎,标裸文档则与 spec 声明矛盾。
  • 已有消费方按裸文档理解这条路由,与 spec 相反:packages/client-react/src/metadata-hooks.tsx:113-140 把 meta.getItem(...) 的结果和 getCached().data(裸文档)喂进同一个 data state,而 useObject 自己的 docstring(:52-64)读的是 schema.label / schema.fields。若服务端走到非缓存分支,这些读取全是 undefined,且不报错。packages/client/src/client.test.ts:96-99 的 mock 同样是裸文档。
  • 属于「声明了却没有强制」的一类:spec 写了唯一响应 schema,生产端有三种形状,没有任何 gate 会发现。AI 生成的消费代码在这种表面上一定会长出 ?? 与 as any。

处置(需定案,勿直接猜)

统一到一种形状是不可避免的,但选哪一种是公开契约决定,应由 maintainer 拍板:

  • A 全部收敛到 spec 的 { type, name, item } 信封 —— 契约优先,plugin-rest-api.zod.ts:773 本来就这么声明。代价:对今天默认配置(缓存分支)的所有读者是破坏性变更。
  • B 全部收敛到裸文档,并改 spec —— 贴合 client-react / CLI / client 自测的既有预期。代价:丢掉 type / name / lock,而 lock 是 ADR-0008 OCC(If-Match)那条链在用的,得另找承载;且「裸元数据文档」本身是个很弱的类型,收窄价值有限。

无论哪条,都应在同一次里把 isMetaEnvelope 的运行时嗅探删掉 —— 它存在本身就是这个分裂的度量。

关联:#5545(被本单阻塞)、#3847(同族:声明了富形状、两个 surface 都发裸值)。

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    进维护者决策收件箱(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,已打 needs-user-decision;domain 标签留分诊席)

    待拍板(公开契约定案,不代拍;完整证据链在 issue 正文与 #5545 的 needs_decision 回报):

    主问题:GET /meta/:type/:name 唯一响应形状定哪种?

    附带问题(同链路,建议一并拍):SaveMetaItemResponseSchema 是实际返回的真子集(实现多发 version/seq/state/projectionApplied,version 正是 OCC If-Match 链在用的)。dev 推荐先把这些字段补进 spec 再给 meta.saveItem 标注解;若只想小步走,另开单优于直接标子集类型。

    下游挂账(拍板后随实施吸收,均已记录暂不立单):#5545(blocked,注解随 A/B 落地跟进)、11 个 unwrapResponse<any> 的 meta.* 方法(spec 无响应 schema,逐条定契约与本单定案耦合)、client-react hooks 的裸文档预期。


    Generated by Claude Code

  2. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    维护者裁定(2026-08-06,经 cli 车道 PM session_01DWUR56YsttL5sTF72Q75TQ 誊写):A —— 全部收敛到 spec 已声明的 { type, name, item } 信封,同一次删掉 isMetaEnvelope 运行时嗅探(5 处调用 + import-prepare.ts 定义)。理由锚点:spec 为权威(与 #4828 子决策 4 同一原则);lock 保住 ADR-0008 OCC(If-Match)链的承载;嗅探的存在本身就是分裂的度量,须随收敛一并消灭。

    附带问题一并裁定:SaveMetaItemResponseSchema 按「先补齐 spec 字段」处置 —— version / seq / state / projectionApplied 补进 spec 声明(version 正是 OCC 在用),之后 meta.saveItem 才标注解;spec 半边另立单转 spec 座位(contract-first,不阻塞本单主问题)。

    实施口径(裁决翻公开语义 ⇒ 全仓一轮翻完,SKILL step 5):

    • 缓存分支(默认路径)改发信封;?layers=true 第三形状一并对齐或显式另立 schema,实施时如实报告;
    • 同一轮翻正全部旧形状读者:rest-server.ts 5 处嗅探、runtime domains/meta.ts 两处裸文档兜底、client-react/metadata-hooks.tsx:113-140 及其 docstring、client/src/client.test.ts 替身 —— 翻转后的断言须承载新语义(信封字段真被读到),不是删旧断言;
    • 破坏性变更:changeset 按 major 语义写迁移处方。

    下游:#5545 保持 pm:blocked 至本单实施合入(注解要对着真实收敛后的线上形状标,先标即撒谎)。

    标签:摘 needs-user-decision → pm:queue,归 cli 车道排程(v17 相关;文件面横跨 rest/runtime/client/client-react,与在飞 #5085/#5674 及 #5588 第一棒同包,严格串行,排 #5588 第一棒之后)。


    Generated by Claude Code

  3. self-assigned this
    on Aug 6, 2026
  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    认领:PM 循环第 5 轮(cli 车道,v17 相关,独占窗口)
    会话:session_01DWUR56YsttL5sTF72Q75TQ
    分支:claude/issue-5563-meta-getitem-envelope-convergence
    Worktree:objectstack-issue-5563
    域:domain:cli
    文件面(四包,窗口内本车道不派任何其他单):packages/rest(rest-server.ts 5 处嗅探与 meta handler、import-prepare.ts、相关测试)+ packages/runtime(domains/meta.ts 两处裸文档兜底)+ packages/client(client.test.ts 替身)+ packages/client-react(metadata-hooks.tsx:113-140 及 docstring)+ changeset(major);⛔ 不触 packages/spec(信封已声明;SaveMetaItemResponseSchema 补字段属 #5745,spec 座位)

    执行依据:维护者 2026-08-06 已拍 A(裁决评论在本 thread 03:40Z):全收敛 spec 信封 + 同一轮删 isMetaEnvelope 嗅探;?layers=true 第三形状对齐或如实报告;翻转后的断言须承载新语义。churn:rest-server.ts 今日已被 #5821(openapi 段)动过、meta.ts 所在 runtime 被 #5774 动过,以最新 origin/main 为准;#5545 保持 blocked 至本单合入。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    验收:ACCEPT(cli 车道 PM,session_01DWUR56YsttL5sTF72Q75TQ,2026-08-06 12:0xZ)→ PR #5895

    转 ready + 挂 auto-merge;MERGED 后在 #5545 留解锁评论(注解可对真实收敛形状标),独占窗口关闭、接续派发 #5673。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions