Skip to content

client SDK 的 meta.getItem 没有声明返回类型(载荷为 unknown),而并排的 meta.getItems 有 —— 同一表面上相邻两个方法的类型化不对等 #5545

Closed

Description

@baozhoutao

在 #5449 把 @objectstack/client 的测试层接进 tsc 时暴露(src/client.test.ts(106,16): error TS18046: 'result' is of type 'unknown')。基线 origin/main @ 9894a723e。

事实

packages/client/src/index.ts,meta 表面上相邻的两个方法:

getItems: async (type: string, options?: { packageId?: string }): Promise< GetMetaItemsResponse > => { … }   // :486 有声明

getItem:  async (type: string, name: string, options?: { packageId?: string }) => {                          // :502 无声明
  …
  return this.unwrapResponse(res);
},

getItem 没有返回类型注解,推断出的就是 unwrapResponse 的返回类型,调用方拿到 unknown,要读任何字段都得先自己断言。测试里那行 expect(result.name) 之所以从来没红过,只是因为该文件此前不在任何 tsc program 内(#5449 记录的那个盲区)。

影响

  • SDK 使用者调 client.meta.getItem('object', 'customer') 后无法直接读取字段,必须 as —— 而并排的 getItems 不需要。这种不对等会诱导调用方对整个 meta.* 表面统一 as any,把真实的形状错误一并盖掉。
  • 属于 AI 生成的元数据应用最容易踩的一类:消费端容忍(as)一旦成为习惯,producer 的形状漂移就没有报警面了。

处置

#5449 的 PR 只在测试里把断言从 result.name 改成 expect(result).toMatchObject({ name: 'customer' }) —— 不加 cast,不假装这个表面已被类型化,断言强度不变。真正的修法是给 getItem 声明返回类型(spec 里对应的响应 schema),那属于 client SDK 的公开签名,应单独定案。

顺带核对是否还有别的 meta.* / data.* 方法漏了返回类型注解,一并补齐。

关联:#5449、#4311。

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊:pm:queue + domain:cli。

    • 落点锚定:packages/client/src/index.ts(origin/main 实测 :502 与 :4630 两处 getItem 均无返回类型注解),packages/client 归 domain:cli 车道。
    • 性质:给公开方法补返回类型注解是从 unknown 收窄,对现有调用方非破坏;并排的 getItems 已有 GetMetaItemsResponse 先例可循。issue 建议的「顺带核对 meta.* / data.* 其余方法」一并做,验收即同一表面无漏注解方法。
    • 派发提示:注意 :4630 处还有第二个 getItem(疑似另一表面/复制体),两处一起核。
    • 查重:三仓无 meta.getItem 返回类型相关影子单;幽灵 @ts-expect-error 不止 spec:@objectstack/client 也有 1 处落在 tsconfig 排除区内(全仓横扫结果) #5449(测试层接入)已裁明不在其 PR 范围,本单是其指名的独立后续。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

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

    @baozhoutao
    ContributorAuthor

    认领(PM 会话 session_016FNvXhtSdnEGEfLEsMmvxh,cli 车道)

    方向:给 meta.getItem 声明返回类型(取 spec 对应响应 schema 实测锚定,不发明形状),并按 issue 自述顺带核对 meta.* / data.* 是否还有漏注解的方法一并补齐。属于把 unknown 收窄为已测得类型的公开签名补全,带 veto window。


    Generated by Claude Code

  5. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    开工(os-dev,会话 session_016FNvXhtSdnEGEfLEsMmvxh 派发,cli 车道)


    Generated by Claude Code

  6. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    复核结论:前提成立,但修法卡在一个尚未定案的公开契约 —— 不硬标,交回定夺(无 PR)。

    基线 origin/main @ 229d29ea4。

    1. 前提复核:成立

    packages/client/src/index.ts 两处 getItem 确实都无返回类型注解,与并排的 getItems 不对等:

    位置 方法 注解 实际推断
    :486 ObjectStackClient.meta.getItems 有 Promise< GetMetaItemsResponse > ✅
    :502 ObjectStackClient.meta.getItem 无 unwrapResponse(res) 无泛型实参 → unknown
    :4623 ScopedProjectClient.meta.getItems 有 Promise< GetMetaItemsResponse > ✅
    :4630 ScopedProjectClient.meta.getItem 无 parent._unwrap(res) 无泛型实参 → unknown(_unwrap 签名在 :1540)

    分诊评论指出的 :4630 第二处属实。

    2. 但 spec 里那个「对应的响应 schema」在默认配置下取不到

    spec 确实有单条响应 schema:plugin-rest-api.zod.ts:773 为 GET /meta/:type/:name 声明 responseSchema: 'GetMetaItemResponseSchema',即 protocol.zod.ts:193 的 { type, name, item } 信封。但生产端并不只发这一种形状,而且 spec 声明的那种恰好不是默认路径。

    实测(对着真 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":{}}}
    

    而 enableCache 默认为真(rest-server.zod.ts:285 的 z.boolean().default(true),rest-server.ts:2796 的 ?? true),所以默认走的是发裸文档的那条分支。链路:rest-server.ts:4128 发 result.data,而 metadata-protocol/src/protocol.ts:5409-5411 里 getMetaItemCached 做的正是 const item = (result as any)?.item、:5441 return { data: item, ... }。?layers=true 还是第三种形状。仓内 import-prepare.ts:18-32 的 isMetaEnvelope 与 rest-server.ts:2631-2637 的注释早已把这个分裂写在纸上,只是当运行时事实处理。

    于是两种注解都不诚实:

    • 标 Promise< GetMetaItemResponse > → 在默认服务端配置下撒谎;更糟的是它会告诉每个 SDK 调用方去读 .item,而那正好是默认响应里不存在的键。比现在的 unknown 更坏 —— unknown 至少逼调用方去看一眼。
    • 标裸文档 → 与 plugin-rest-api.zod.ts:773 的声明直接矛盾,等于在 SDK 侧单方面改契约。

    按 Prime Directive 的 contract-first:诱使消费端做宽容处理时,bug 在生产端或 spec。已按此把生产端分裂立单 #5563(未认领、无标签,待分诊)。本单被它阻塞:#5563 定案并统一形状之后,这里的注解才有唯一正确答案。

    3. 顺带核对:meta.* / data.* 全量注解清单

    data.* 两个表面基本齐全(query / find / get / create / createMany / import / batch / clone / delete / deleteMany / import job 全组 都有注解)。缺口集中在 meta.*:

    真的产出 unknown(本单口径的问题)—— 共 6 处,3 个方法 × 2 个表面:

    方法 位置 spec 响应 schema 能否直接补
    getItem :502 / :4630 GetMetaItemResponseSchema ❌ 被 #5563 阻塞(见上)
    saveItem :518 / :4637 SaveMetaItemResponseSchema = { success, message? } ⚠️ 见下,需定案
    getHistory :546 / :4650 无 —— spec 里没有任何 history 响应 schema ❌ 只能凭空发明形状,不做

    saveItem 的问题不是「撒谎」而是「截断」:metadata-protocol/src/protocol.ts:7343 与 :7441 两个 return 实际发的是 { success, message, version?, seq?, state?, projectionApplied? },是 spec schema 的超集。标成 SaveMetaItemResponse 类型上合法,但会把 version / seq / state 从公开签名里抹掉 —— 而 ADR-0008 的 OCC(If-Match)链正靠 version。今天它们只是「要 cast 才能读」,标完就变成「读了报错」。这是能力倒退,不该由 dev 私自拍板。

    产出 any 而非 unknown(另一个问题,不在本单口径内)—— 11 个方法,都是 unwrapResponse<any>:getPublished(:636)、listDrafts(:646)、migrateStored(:670)、getLegalNextStates(:688,注意它第一条 return 是有具体类型的,兜底那条是 any)、getDiagnostics(:702)、getReferences(:717)、getBookTree(:727)、getAudit(:739)、publishItem(:752)、rollbackItem(:764)、diffItem(:777)。这些路由在 spec 里同样没有响应 schema,补注解等于逐条定契约,远超本单范围 —— 若要做应另开单。

    4. 第 3 项(把 toMatchObject 升级为类型化读取)本次做不了

    client.test.ts:113 那句 expect(result).toMatchObject({ name: 'customer' }) 的前提就是注解未定;注解不落,result.name 就仍是 TS18046。注意该测试的 mock(:96-99)本身用的是裸文档 { name: 'customer', fields: [] } —— 也就是说这个权宜断言无意间站在了 #5563 的 B 侧。等 #5563 定案后连同注解一起改,才不会把某一侧偷偷写死进测试。

    5. 反向验证方向说明

    派发提示预判为「注解移除后 result.name 读取应红 TS18046」。该预判对已落注解的世界成立,但本次没有落任何注解,所以这条反向验证无对象可跑 —— 不编造。真正跑到的反向证据是上面第 2 节那对 body:同一请求、同一路由,只切一个分支开关,形状就变 —— 这才是本次的核心实测。

    结论

    needs_decision,不提 PR,分支未 push,工作树已清理。围栏全程遵守(未改 tsconfig.test.json、tests/integration/**、packages/runtime/**、packages/cli/src/commands/**、packages/rest/** —— 第 2 节的探针是一次性文件,跑完即删,工作树已确认干净)。

    建议:本单挂 Blocked-by: #5563,待 #5563 定案统一形状后再派发;届时 getItem 注解、saveItem 的超集问题、以及测试断言升级可以一次做完。


    Generated by Claude Code

  7. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    勘误(上条评论第 3 节):saveMetaItem 两个 return 的行号写错了,正确的是 packages/metadata-protocol/src/protocol.ts:7352 与 :7450(我误记成 :7343 / :7441)。实测:

    $ sed -n '7352,7353p;7450,7451p' packages/metadata-protocol/src/protocol.ts
                    return {
                        success: true,
                return {
                    success: true,
    

    其中 :7352 那个 return 发的是 { success, version, seq, projectionApplied?, state, message },:7450 那个发 { success, message }。结论不变 —— 前者是 SaveMetaItemResponseSchema({ success, message? })的超集,标该类型会把 version / seq / state 从公开签名抹掉。


    Generated by Claude Code

  8. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 5, 2026
  9. removed their assignment
    on Aug 5, 2026
  10. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    收报:needs_decision,零代码停手正确,转 pm:blocked(PM 会话 session_016FNvXhtSdnEGEfLEsMmvxh,cli 车道)

    Blocked-by: #5563

    dev 前提复核推翻了 issue 处方的关键一步:spec 确有 GetMetaItemResponseSchema,但生产端在同一路由发三种形状且 spec 声明的信封不是默认路径(缓存分支发裸文档,enableCache 默认 true;实测两条 body 已录入 #5563)。此时没有一个注解是诚实的——标信封在默认配置下撒谎,标裸文档则单方面推翻 spec 声明。按 contract-first 不硬标、立 #5563 上收件箱,是正确处置。

    留给解锁后复用的测绘结果(不必重做):

    解锁条件:#5563 维护者拍板并落地后,本单按定案形状补注解;届时重核前提(生产端应已单形状)。认领已释放,工作树已拆。


    Generated by Claude Code

  11. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 6, 2026
  12. self-assigned this
    on Aug 6, 2026
  13. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    解锁 + 认领:PM 循环第 6 轮(cli 车道)
    会话:session_01DWUR56YsttL5sTF72Q75TQ
    分支:claude/issue-5545-client-getitem-return-type
    Worktree:objectstack-issue-5545
    域:domain:cli
    文件面:packages/client/src/index.ts + src/client.test.ts + changeset(越界即停,报告说明)

    解锁依据:Blocked-by: #5563 已解除——PR #5895 已合入 origin/main(d9cac6019),GET /meta/:type/:name 全路径收敛到 {type,name,item} 信封,注解有唯一诚实答案了。且 #5745/PR #5861 已合入:SaveMetaItemResponseSchema 已补齐 version/seq/state/projectionApplied,前任 dev 卡住的「标注解会抹掉 OCC 字段」问题已消——saveItem 现在也可诚实标注。前任 needs_decision 回报的测绘结果(6 处 unknown 清单、getHistory 无 spec schema、11 处 any 另案)沿用不重做;旧围栏(#5544 tsconfig/integration)已随其合并失效。


    Generated by Claude Code

  14. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

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

    转 ready + 挂 auto-merge,两读数跟到 MERGED。


    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

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions