Skip to content

两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828

Description

@xuyushun441-sys

发现于 #4817(把 content/docs/protocol/kernel/http-protocol.mdx 的 discovery 一节改成两段式时逐字段核对实现),不在该 docs-only PR 范围内修,未认领。

现象

/discovery 属于「机器可读表面」——SDK、codegen、AI 客户端直接读它(AGENTS.md「Route & surface ownership」第 4 条:machine-readable surfaces must not lie)。但两个生产者返回的顶层键,有三个在 packages/spec/src/api/discovery.zod.ts 里根本没有声明:

线上字段 谁发出 spec 里的声明
scoping({ enabled, resolution, scoped, environmentId }) registerDiscoveryEndpoints,packages/rest/src/rest-server.ts 无。git grep scoping -- packages/spec/src/api/ 只命中 events.zod.ts 的注释
features(顶层 { search, websockets, files, analytics, ai, notifications, i18n }) HttpDispatcher.getDiscoveryInfo(),packages/runtime/src/http-dispatcher.ts 无 —— 而且 DiscoverySchema 的设计说明里明写「capabilities/features was removed because it was fully derivable from services[x].enabled」(discovery.zod.ts:211)
endpoints(routes 的重复副本,注释写 "Alias for backward compatibility with some clients") 同上 无

同时:

  • REST 形状永远无法通过 DiscoverySchema.parse()。该 schema 把 name / environment / locale 声明为必填,而 getDiscovery()(packages/metadata-protocol/src/protocol.ts)三个都不产出,rest-server 也不补。
  • dispatcher 形状不产出 capabilities(schema 里是可选,所以不报错),于是同一个协议概念被两个生产者用两套互不相交的字段表达:REST 说 capabilities,dispatcher 说 features。
  • environment 在 schema 里是 z.enum(['production','sandbox','development']),dispatcher 直接塞 getEnv('NODE_ENV', 'development') 的原始值,NODE_ENV=test / staging 都会落在枚举外。

之所以没人发现:唯一在协议层实际引用的是 GetDiscoveryResponseSchema(packages/spec/src/api/protocol.zod.ts:122),它是 DiscoverySchema.partial().required({version:true}).extend({apiName}),而 zod object 默认 strip 未知键 —— 于是「未声明的字段」和「缺失的必填字段」两类问题都被这层宽松包装吃掉了,没有任何 gate 在两端比对。这正是 Prime Directive #10 的 declared ≠ enforced 形状,只是方向反过来:enforced 的比 declared 的多。

为什么值得单开一条

这不是文档问题(#4817 已按实现实际返回的形状把两份示例写对了,包括 scoping / features / endpoints),而是契约问题:文档现在忠实描述了一个 spec 没有声明的线上形状。要么把这三个键补进 schema(并说清 features 与 capabilities 谁是正,endpoints 的退役时间表),要么从生产者删掉。

建议的决策点(需要维护者定,不要猜)

  1. features vs capabilities:统一到一个,还是承认两个端点各有一套?若统一,哪个是正、另一个按 ADR-0087 走退役?
  2. endpoints 这个 backward-compat 别名还有真实消费者吗?没有就删;有就声明并给退役时间表。
  3. scoping 是 REST 层的真实能力协商信息,应当补进 schema而非删除 —— 但补在 DiscoverySchema 还是一个 REST 专属的扩展 schema 里,取决于第 1 点怎么定。
  4. DiscoverySchema 的必填 name / environment / locale:REST 端点补上它们,还是承认 DiscoverySchema 描述的只是 dispatcher 形状、REST 形状另有其名?

修改落点在 packages/spec / packages/rest / packages/runtime,与 #4817 的 docs 车道不同,故未并入。

发现于 #4817,未认领 —— 谁开工谁按 AGENTS.md 认领。

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    补一条同族证据(来自 #5544 的实施,界外发现,查重命中本单故不另开单):决策点 4 的 name 缺失,在 metadata-protocol 侧不只是「不产出」,而是产出了 spec 标注为 deprecated 的别名 apiName。

    • packages/metadata-protocol/src/protocol.ts:2509(getDiscovery() 的返回体):apiName: 'ObjectStack API',没有 name。
    • packages/runtime/src/http-dispatcher.ts 的 getDiscoveryInfo() 走的是另一套:同仓 packages/runtime/src/http-dispatcher.root.test.ts:38 的注释原话就是 “getDiscoveryInfo returns 'name' not 'apiName'”。
    • spec 侧 packages/spec/src/api/protocol.zod.ts:118-127 把 apiName 明写为 “deprecated — use name”,只在 GetDiscoveryResponseSchema 上 .extend(),而 canonical 的 name 在 DiscoverySchema(discovery.zod.ts:216)里是必填。

    于是同一个「API 名字」概念,两个生产者分别只发 apiName 和只发 name,消费者两边都拿不到稳定的键——与本单 features vs capabilities 是同一种分裂,只是发生在一个 spec 已经标注退役的别名上。决策点 4 定案时建议把 apiName 的退役时间表一并定了(ADR-0087 路径),否则「用 name」的结论落到 metadata-protocol 这条产出路径上是空的。

    对 #5544 的影响(记录在案):正因为两个生产者拼法相反、且集成套件跑在一台外部服务器上无法在本单证伪,#5544 的 PR 没有把 01-discovery.test.ts 的 apiName 断言改写成 name —— 只按 #5449 的约定修了 possibly undefined(可选链断言,不加 ! 也不加 ?? '' 兜底)。改哪个键是本单的契约裁定,不是测试文件能替它决定的。


    Generated by Claude Code

  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    维护者裁定(部分)(2026-08-05,经 spec 车道 PM session_018fxLGQdatPbBUvCgiVxg6D 誊写)—— 四个子决策中三个已定,一个仍待裁:

    1. 单一正名 + curated alias,不双正名并存 —— 已裁定。PM 执行默认(带否决窗口):正名取 capabilities(它是 DiscoverySchema 既有声明键;features 是未声明却在发的那个),features 按 ADR-0087 走退役/别名路径。若维护者要反向(features 为正),回一句即改 —— 这条默认只是沿用既有 schema 声明,不引入新契约形状。
    2. endpoints 别名 —— 已裁定:按 ADR-0049 enforce-or-remove 常规退役流程处理(先测真实消费者,无消费者即删,有则声明+退役时间表)。
    3. scoping —— 已裁定:补进 schema 显式声明(REST 层真实能力协商信息,事实已存在只是没写进协议);随第 1 条取单一 DiscoverySchema 形状,以 optional 键落位。
    4. 必填 name / environment / locale vs REST 实际形状 —— 仍待维护者定权威侧:是 REST 端点补齐三键(schema 为权威),还是承认 DiscoverySchema 只描述 dispatcher 形状、REST 另立(实现为权威)。这是唯一真正的形状分歧,environment 枚举外取值(NODE_ENV=test/staging)也随这条一并定。

    needs-user-decision 标签保留,仅剩第 4 条。第 4 条裁定后本单整体转队(1–3 可与 4 同一 PR 落地,避免 discovery 面两次动刀)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    维护者裁定(第 4 条,收口)(2026-08-05,经 spec 车道 PM session_018fxLGQdatPbBUvCgiVxg6D 誊写):schema 为权威,REST 补齐三键。

    • DiscoverySchema 的必填 name / environment / locale 维持必填;packages/rest 的 discovery 端点(及其上游 getDiscovery())补齐产出这三个键,而不是给 schema 降级或另立 REST 专属形状。
    • 随之而来的强制推论:所有生产者的 environment 取值必须落在声明枚举内(production | sandbox | development)。dispatcher 直塞 getEnv('NODE_ENV') 的做法要改为映射到枚举(test/staging 等枚举外取值如何归置属实现层选择,dev 在 PR 里给出映射表并文档化;语义拿不准就回报,不要猜)。
    • 至此四条子决策全部已裁,汇总:① 正名 capabilities,features 按 ADR-0087 退役/别名(默认带否决窗口,未被否决);② endpoints 走 ADR-0049 退役流程(先测真实消费者);③ scoping 以 optional 键补进 DiscoverySchema;④ schema 为权威,REST 补齐三键。
    • 落地形态:四条一个 PR(避免 discovery 机器可读面两次动刀),验收核心是两个生产者的线上形状都能通过 DiscoverySchema.parse() 严格校验——把「宽松包装吃掉两类问题」的 GetDiscoveryResponseSchema 侧也接上比对闸门,防回潮。

    摘除 needs-user-decision,转 pm:queue。当前批次 5/5 满员,排在 #4611 之后的下一个空槽。


    Generated by Claude Code

  4. self-assigned this
    on Aug 5, 2026
  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    PM 认领(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05)


    Generated by Claude Code

  6. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    开发完成汇报(os-dev,分支 claude/issue-4828-discovery-surface,draft PR #5682)

    四条裁定已在一个 PR 内全部落地。几点需要维护者过目的判断:

    1. endpoints 判定:删除(裁定②的分支已定)

    三仓消费者普查(objectstack / objectui / cloud),零真实消费者:

    候选命中 复核结论
    packages/client/src/index.ts 走 discoveryInfo.routes,不读 endpoints
    objectui useApiDiscovery.ts 的 catalog.endpoints 它自己硬编码的 SERVICE_ENDPOINT_CATALOG,与本载荷无关
    objectui useDiscovery.ts 只读 services
    cloud 只把 /discovery 当路径前缀用(SKIP_PATH_PREFIXES),从不读载荷

    阳性对照:同一套 grep 能命中 capabilities 的真实消费者(packages/client 的 getter)和 scoping 的真实消费者(client.environment-scoping.test.ts),所以「零命中」不是 grep 失灵。

    2. environment 映射表(裁定④要求的那张表)

    NODE_ENV 广播值 理由
    production、prod production 正式/简写
    sandbox sandbox 精确
    staging sandbox 预生产、类生产
    development、dev development 正式/简写
    test development 临时的开发者级运行
    未设置 / 其它 development 保持原默认,且绝不在猜测时宣称 production

    prod/dev 简写被接受,与 seed-loader.ts 既有的 NODE_ENV_TO_SEED_ENV 同理由:NODE_ENV 是操作员提供的第三方边界变量(PD #9 明列),归一化它不是 PD #12 禁止的消费者侧容忍。

    staging → sandbox 是表里唯一的判断题,单独标出请裁:它一定不是 production;在 sandbox 与 development 之间,staging 是类生产的,故取 sandbox。要改判只需改这一行。

    3. 一处与派发预设不同的结论:ADR-0087 D2 转换表不适用

    派发单提到「features 按 ADR-0087 走 curated alias/tombstone」。实施时核对了 D2 的适用面:转换层作用于加载期的被授权元数据(normalizeStackInput 那个缝,flow.node.type / page.kind 这类键)。响应载荷没有加载缝 —— discovery 每次请求现算,不存在需要在加载时改写的存量形状,登记 D2 条目不会有任何代码去应用它。

    而且 features / endpoints 从未被声明过,所以也没有「从 schema 删除 + 立 tombstone」这一步(retiredKey() 的前提是键在 schema 的 shape 里)。

    故走的是本仓既有的 API 面退役先例(#4939 registry-retirement.test.ts):停发 + pin 测试钉死不许回潮;apiName 因为确实被声明过,给了明确时间表(协议 18)写在声明处。packages/spec/liveness/ 同样不适用(它以注册表为根、走可授权元数据类型,discovery 不在其中)。

    如果维护者认为仍应在某处登记一条机器可读的退役记录,请指个位置,我另开 PR 补。

    4. 验收核心的一个补充(重要)

    裁定写的验收核心是「两个生产者的线上形状都能通过 DiscoverySchema.parse() 严格校验」。实施时发现这条对必填键那一半充分,对退役那一半不充分:zod object 默认 strip 未知键,所以 parse() 根本看不见 features/endpoints 这类多余键 —— 光有它,退役的那一半是没有闸门的。

    所以每个生产者的闸门是两条断言:

    • DiscoverySchema.parse() 判值(必填键、environment 落枚举、scoping 形状);
    • 发出的键集 ⊆ GetDiscoveryResponseSchema 的 shape,判键(不许发协议没声明的东西)。允许集直接从 schema 推导,而不是手写数组,免得闸门自己变成第三种方言。

    先证红也印证了这个分工:getDiscovery() 未改时第一条红(name: invalid_type / environment: invalid_value / locale: invalid_type),第二条绿 —— 它的病是少键不是多键;多键那一半在 dispatcher 上。

    范围外发现(均已另开单,未认领)

    另:本 PR 是 #3877 Stage A 点名的第一个族(discovery)的落地。

    conversions/registry.ts 未改动,与 #4923 无重叠。


    Generated by Claude Code

  7. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    验收通过(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)—— PR #5682,四裁定一次落地。

    转 ready + auto-merge,队列落地。


    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