Repository navigation
两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828
Description
Activity
补一条同族证据(来自 #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 — usename”,只在GetDiscoveryResponseSchema上.extend(),而 canonical 的name在DiscoverySchema(discovery.zod.ts:216)里是必填。
于是同一个「API 名字」概念,两个生产者分别只发
apiName和只发name,消费者两边都拿不到稳定的键——与本单featuresvscapabilities是同一种分裂,只是发生在一个 spec 已经标注退役的别名上。决策点 4 定案时建议把apiName的退役时间表一并定了(ADR-0087 路径),否则「用name」的结论落到 metadata-protocol 这条产出路径上是空的。对 #5544 的影响(记录在案):正因为两个生产者拼法相反、且集成套件跑在一台外部服务器上无法在本单证伪,#5544 的 PR 没有把
01-discovery.test.ts的apiName断言改写成name—— 只按 #5449 的约定修了possibly undefined(可选链断言,不加!也不加?? ''兜底)。改哪个键是本单的契约裁定,不是测试文件能替它决定的。
Generated by Claude Code
维护者裁定(部分)(2026-08-05,经 spec 车道 PM
session_018fxLGQdatPbBUvCgiVxg6D誊写)—— 四个子决策中三个已定,一个仍待裁:- 单一正名 + curated alias,不双正名并存 —— 已裁定。PM 执行默认(带否决窗口):正名取
capabilities(它是DiscoverySchema既有声明键;features是未声明却在发的那个),features按 ADR-0087 走退役/别名路径。若维护者要反向(features为正),回一句即改 —— 这条默认只是沿用既有 schema 声明,不引入新契约形状。 endpoints别名 —— 已裁定:按 ADR-0049 enforce-or-remove 常规退役流程处理(先测真实消费者,无消费者即删,有则声明+退役时间表)。scoping—— 已裁定:补进 schema 显式声明(REST 层真实能力协商信息,事实已存在只是没写进协议);随第 1 条取单一DiscoverySchema形状,以 optional 键落位。- 必填
name/environment/localevs 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
- 单一正名 + curated alias,不双正名并存 —— 已裁定。PM 执行默认(带否决窗口):正名取
维护者裁定(第 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
PM 认领(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05)- 分支:
claude/issue-4828-discovery-surface;工作树:../objectstack-4828-discovery(基于最新origin/main);域:domain:spec - 文件面:DiscoverySchema(spec)+ REST discovery 生产者(packages/rest)+ dispatcher 生产者(runtime/hono 侧,以实测为准)+ 别名/退役登记(ADR-0087 registries、conversions/registry.ts 如需)+ 契约测试
- 裁定依据:四子决策已全裁(见本 issue 2026-08-05 裁定评论):capabilities 正名(features 退役)/ endpoints 走 ADR-0049 流程 / scoping 进 schema / schema 为权威,REST 补齐 name/environment/locale 三键(environment 枚举内取值强制,生产者映射表写文档,语义拿不准回报)
- 同批不相交:ADR-0105 D13's "scoping field" has no metadata home — the 0105↔0117 linkage gap #4611、flow.test.ts 剩下的五类跑不通的 fixture 形状:
{节点id.字段}输出引用、assignment 无assignments包裹、legacy loop、字符串filter、brace-CEL 出边条件 #5500、ui/app.zod.ts导航项:4 条expanded别名在另外 8 个变体上把作者指向该变体同样拒绝的键(二次拒绝) #5555、HookContext.input的契约注释声明批量写携带input.ast,引擎从不设置它(AST 只在 opCtx 上);同一张表也未描述 #5038 后 after 事件的按行形状 #5273;⚠️ conversions/registry.ts 若动,ADR-0087 别名转换会把「被遮蔽的旧拼法」留在存量元数据里 —— 节点 config 收紧后它从静默丢弃变成执行期硬拒 #4923 已排下一轮串行 - 派发:os-dev(opus)。更早不同 session 认领者优先,本认领作废。
Generated by Claude Code
- 分支:
开发完成汇报(os-dev,分支
claude/issue-4828-discovery-surface,draft PR #5682)四条裁定已在一个 PR 内全部落地。几点需要维护者过目的判断:
1.
endpoints判定:删除(裁定②的分支已定)三仓消费者普查(
objectstack/objectui/cloud),零真实消费者:候选命中 复核结论 packages/client/src/index.ts走 discoveryInfo.routes,不读endpointsobjectuiuseApiDiscovery.ts的catalog.endpoints它自己硬编码的 SERVICE_ENDPOINT_CATALOG,与本载荷无关objectuiuseDiscovery.ts只读 servicescloud只把 /discovery当路径前缀用(SKIP_PATH_PREFIXES),从不读载荷阳性对照:同一套 grep 能命中
capabilities的真实消费者(packages/client的 getter)和scoping的真实消费者(client.environment-scoping.test.ts),所以「零命中」不是 grep 失灵。2.
environment映射表(裁定④要求的那张表)NODE_ENV广播值 理由 production、prodproduction正式/简写 sandboxsandbox精确 stagingsandbox预生产、类生产 development、devdevelopment正式/简写 testdevelopment临时的开发者级运行 未设置 / 其它 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 上。范围外发现(均已另开单,未认领)
- routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层) #5679 ——
routes.mcp是 REST 发出、objectui 真实消费、但ApiRoutesSchema从未声明的键(本单同族,低一层;源码里的as any强转就是证据)。有消费者,与endpoints情况相反,建议补声明。 - SDK 的 client.capabilities 声明为 WellKnownCapabilities,但两个 discovery 生产者填的是互不相交的键集 #5672 ——
client.capabilities声明为WellKnownCapabilities,但两个生产者填的键集互不相交(本 PR 把分裂减浅了但没消除,统一词汇表是新的契约决策)。 - NODE_ENV 未设置时 /discovery 广播 environment=development,而 os start 默认 NODE_ENV=production、CLI doctor 也按 production 解析 #5673 ——
NODE_ENV未设置时三处默认相反(discoverydevelopment/ CLI doctorproduction/ seed-loaderundefined)。 - packages/rest 的 14 个 getDiscovery 测试替身返回
endpoints,一个真实生产者从未发过、且已在 #4828 退役的键 #5674 ——packages/rest有 14 个getDiscovery测试替身返回endpoints(真实生产者从未发过;改它会翻转if (discovery.routes)的真假,属行为变更,故未在本 PR 顺手改)。finding。 - 「环境类别」这一个概念在 spec 里有两个枚举:DiscoverySchema.environment(3 成员)与 EnvironmentTypeSchema(7 成员) #5676 —— 「环境类别」一个概念两个枚举(
DiscoverySchema.environment3 成员 vsEnvironmentTypeSchema7 成员)。finding。
另:本 PR 是 #3877 Stage A 点名的第一个族(discovery)的落地。
conversions/registry.ts未改动,与 #4923 无重叠。
Generated by Claude Code
验收通过(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)—— PR #5682,四裁定一次落地。- 双 schema 责任拆分:
DiscoverySchema约束生产者(三包各带 conformance 门,值判parse()+ 键判「发出键集 ⊆ 允许集」双断言,允许集从GetDiscoveryResponseSchema.shape派生),spec 侧钉两 schema 键集等价。dev 用测量证明裁定验收核心(仅 parse)对退役半边不充分(zod strip 未知键)并正确补强——此类判据修正照 ACCEPT。 - ①
capabilities正名(顺带修掉client.capabilities恒 undefined 的真实缺陷);②endpoints三仓零消费者(阳性对照)→ 删除;③scoping按实测形状补声明;④ REST 补齐三键 +resolveDiscoveryEnvironment共享映射(两生产者不可能漂移)。 staging → sandbox为映射表唯一判断题,否决窗口开放:一定非 production,预生产取 sandbox;要改判仅一行。回退行安全底线正确(识别不了绝不宣称 production)。- ADR-0087 D2 不适用的架构判断(响应载荷无加载缝,走
ApiRegistry/api-registryplugin 只在packages/core/examples/里被装配,无任何真实 composition 挂载 ——ApiEndpointRegistrationSchema因此整面零执行 #4939 API 面退役先例)成立;apiName退役时间表(17 双发 / 18 移除)已写进声明处。 - changeset minor +
!记号沿 17.x rc 既有先例;生成物三件逐行核对,strictness ledger 393→394 方向诚实;conversions/registry.ts未动,ADR-0087 别名转换会把「被遮蔽的旧拼法」留在存量元数据里 —— 节点 config 收紧后它从静默丢弃变成执行期硬拒 #4923 无重叠。 - 跟进五单 + Response bodies are never checked against the schemas that declare them — staged plan, not a repo-wide sweep #3877 Stage A 首族完成回执(含 Stage D 棘轮应取双断言的建议)均已归档。
转 ready + auto-merge,队列落地。
Generated by Claude Code
- 双 schema 责任拆分:
- added a commit that references this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 6, 2026 - added 3 commits that reference this issue
on Aug 6, 2026 - added a commit that references this issue
on Sep 17, 2026 - added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
发现于 #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里根本没有声明:scoping({ enabled, resolution, scoped, environmentId })registerDiscoveryEndpoints,packages/rest/src/rest-server.tsgit grep scoping -- packages/spec/src/api/只命中events.zod.ts的注释features(顶层{ search, websockets, files, analytics, ai, notifications, i18n })HttpDispatcher.getDiscoveryInfo(),packages/runtime/src/http-dispatcher.tsDiscoverySchema的设计说明里明写「capabilities/featureswas removed because it was fully derivable fromservices[x].enabled」(discovery.zod.ts:211)endpoints(routes的重复副本,注释写 "Alias for backward compatibility with some clients")同时:
DiscoverySchema.parse()。该 schema 把name/environment/locale声明为必填,而getDiscovery()(packages/metadata-protocol/src/protocol.ts)三个都不产出,rest-server 也不补。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的退役时间表),要么从生产者删掉。建议的决策点(需要维护者定,不要猜)
featuresvscapabilities:统一到一个,还是承认两个端点各有一套?若统一,哪个是正、另一个按 ADR-0087 走退役?endpoints这个 backward-compat 别名还有真实消费者吗?没有就删;有就声明并给退役时间表。scoping是 REST 层的真实能力协商信息,应当补进 schema而非删除 —— 但补在DiscoverySchema还是一个 REST 专属的扩展 schema 里,取决于第 1 点怎么定。DiscoverySchema的必填name/environment/locale:REST 端点补上它们,还是承认DiscoverySchema描述的只是 dispatcher 形状、REST 形状另有其名?修改落点在
packages/spec/packages/rest/packages/runtime,与 #4817 的 docs 车道不同,故未并入。发现于 #4817,未认领 —— 谁开工谁按 AGENTS.md 认领。