feat(spec,runtime,metadata-protocol,client)!: 统一能力词表 —— 封闭声明,两个 discovery 生产者全量发 (#5672) - #5857
Merged
Merged
Conversation
…ocabulary, emitted whole by every discovery producer (#5672) #4828 收敛了两个 discovery 生产者的「拼写」分裂(都发 `capabilities`), 但更深的一层没动:两者填的键集互不相交,只有 `search` 重合。 `DiscoverySchema.capabilities` 是开放 record,所以两种形状都合法、没有闸门 看得见分裂;而 SDK 的 getter 把结果断言成 `WellKnownCapabilities` —— 面对 dispatcher 宿主,`client.capabilities.transactionalBatch` 静态是 `boolean`、运行时是 `undefined`,类型在撒谎。 按维护者 2026-08-06 裁定 A 统一能力词表: - spec:`WellKnownCapabilitiesSchema` 成为唯一词表,收编原属 dispatcher 的 六个键(websockets/files/analytics/ai/notifications/i18n —— 都是早已在线 上的真实答案,这里是补声明而非发明);`DiscoverySchema.capabilities` 由「可选开放 record」改为「必填封闭对象」,键集从词表推导。新导出 `WELL_KNOWN_CAPABILITY_KEYS` 与 `CapabilityDescriptorSchema`。 - 两个生产者全量发,每个键的作答依据写进代码注释。dispatcher 的 `comments` 改为从它本就为 `/data` 域解析的 registry 实测 `sys_comment`;唯一诚实的 `false` 是 `transactionalBatch` —— 原子 `/batch` 由 @objectstack/rest 挂载, 该 dispatcher 完全没有 batch 分支。 - client:getter 不再断言,改为遍历 spec 的键表构造,类型因此成真; 旧服务器缺键读作 `false`(fail-closed),非布尔值不做强制转换。 - 三包 conformance 门顺延加全量性判据(键全在、值全为 boolean、无词表外键), allowance 从 schema 推导。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
…edger) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
同一 slot(file-storage)的两个词表键此前判据不同:files 走可服务性、 chunkedUpload 只看注册存在。后果有二:自声明 stub 的存储实现会被广告出 chunkedUpload: true —— 而该 builder 本就不给它广告 routes.storage;更要紧的 是 runtime dispatcher 对同一键答的是 hasFiles(可服务性),两个生产者会对同 一宿主的同一键给出相反答案 —— 正是本单要消除的方言。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 119 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
August 6, 2026 09:17
This was referenced Aug 6, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Aug 6, 2026
…ble-surface) #5861 被合并队列踢出,签名是 `strictness-ledger-doc.test.ts > is checked in current`。 归因:#5849 / #5857 在入队前合入 main(#5857 给 `api/` 新增了一个 z.object 站点), 本分支的 counts.md 是在旧树上渲染的,合并树上不再等于现渲染 —— 单体生成物的串行税 (#5837 正在治的病),不是实现回归。 在合并树上重跑 os-regen:`api/` 站点数 395 → 396(main 的 +1 与本单 `projectionApplied` 嵌套对象的 +1),docs 与 authorable-surface 一并重渲。相对 origin/main 的净增量仍只有 本单四个键。`authorable-surface.base.json` 本轮无重锚漂移(已核对与 main 一致)。 Co-Authored-By: Claude <noreply@anthropic.com>
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.
Fixes #5672
按维护者 2026-08-06 裁定 A 实施:spec 声明封闭词表,两个 discovery 生产者必须全量发,缺能力 =
enabled: false而非缺键,WellKnownCapabilities类型随之成真,闸门沿用 #5682 建、#5743 扩到 routes 层的三包 conformance 门并顺延加全量性判据。前提复核(基于
origin/main,含 #5743)issue 的前提成立且未过期。在
fc5f536a1上实测两个生产者:capabilities的键getDiscovery()(packages/metadata-protocol/src/protocol.ts)commentsautomationcronsearchexportchunkedUploadtransactionalBatchgetDiscoveryInfo()(packages/runtime/src/http-dispatcher.ts)searchwebsocketsfilesanalyticsainotificationsi18n只有
search重合;DiscoverySchema.capabilities是开放z.record,两种形状都合法。第三处生产点(packages/rest/src/rest-server.ts:3100)先扫后改:它不是独立生产者,而是在getDiscovery()之上合成 —— 只重写capabilities.transactionalBatch一个条目(与自身api.enableBatch相与),因此词表经它到达浏览器,同规则自动覆盖,无需第四套逻辑。逐键消费者测量表
跨三仓实测(
objectstack/objectui/cloud,均为当日 HEAD)。「消费者」指真的读这个 discovery 能力位的代码,不含同名的 widget / permission / datasource / hook-body capabilities。transactionalBatchpackages/data-objectstack/src/index.tsreadTransactionalBatchCapability(),connect 时读、决定是否放弃非原子回退packages/clientgettersearchcommentsautomationcronexportchunkedUploadpackages/client测试websocketsfilesanalyticsainotificationsi18n取舍与证据:零消费者的键一律保留并集,不做删减。理由三条 ——(1)裁定只授权统一词表,删键属 ADR-0049 另一单;(2)没有一个键是「刚出生、任何面都没兑现过」的:dispatcher 那六个自 #4828 起就是线上真实答案,protocol 那六个由
protocol-discovery.test.ts逐键钉着;(3)transactionalBatch恰好证明「今天零消费者」不等于「没用」—— 它在 objectui 有真实读者,而其余键的零读数只说明 objectui 尚未做能力自适应 UI。一处值得单独记的语义重叠:
files与chunkedUpload都落在file-storageslot 上,在今天的宿主上答案恒等(唯一发货的存储面两者都提供)。这不是复制粘贴而是宿主事实,已在两处 emit 点写明;若日后要合并成一个键,那是 ADR-0049 的另一单。两生产者作答依据表
每个键的依据都写进了代码注释。裁定第 3 条的
false只用在确实不交付的面上,能算出来的一律实测。getDiscovery()getDiscoveryInfo()(dispatcher)commentssys_comment(ADR-0052 §5)getObjectQLService(kernel)读同一 registry —— 它本就为自己的/data域解析这个引擎,而/data/sys_comment正是 comments 的服务方式automationregisteredServices.has('automation')hasAutomation(可服务性,与routes.automation同一判据)cronhas('job')hasJob(存在即能力:job 是无 HTTP 面的内核内契约,#4318)searchhas('search')hasSearchexporthas('automation') || has('queue')hasAutomation || hasQueue(同一析取)chunkedUploadcapabilityServed('file-storage')(本 PR 收紧,见下)hasFilestransactionalBatchtypeof engine.transaction === 'function'false—— 该面不交付。原子/batch由@objectstack/rest的registerBatchEndpoints挂载;该 dispatcher 完全没有 batch 分支(domains/data.ts只把query路由为自定义 action)。改答engine.transaction会替一个本宿主不服务的端点广告原子性 —— 正是该标志位当初(#3298/#1604)要消除的谎websocketsfalsefalse—— 两边同因:realtime 是进程内 pub/sub 总线,无人挂载 WS/SSE 面(ADR-0076 D12, #2462),这也正是routes.realtime永不广告的原因filescapabilityServed('file-storage')hasFilesanalyticsainotificationsi18ncapabilityServed(...)hasAnalytics/hasAi/hasNotification/hasI18n其中
capabilityServed(name)=registeredServices.has(name) && !unserveable(name),复用该 builder 本就用于决定是否广告路由的判据;dispatcher 侧的isServiceServeable语义等价。广告什么与声称什么因此不可能不一致。一处越出「只加键」的收紧,请着重看
getDiscovery()的chunkedUpload由registeredServices.has('file-storage')改为capabilityServed('file-storage')(单独一个 commit)。不这么改会有两个后果,第二个是决定性的:chunkedUpload: true—— 而同一 builder 本就不给它广告routes.storage;若维护者认为这仍属越界,单独 revert 该 commit 即可,其余不受影响。
契约改动
packages/spec/src/api/discovery.zod.ts:WellKnownCapabilitiesSchema成为唯一词表,收编原属 dispatcher 的六键。为让DiscoverySchema能从它派生,该 schema 连同新增的CapabilityDescriptorSchema一起上移到DiscoverySchema之前 —— 这不是排版:lazySchema在OS_EAGER_SCHEMAS=1(文档记载的应急回滚开关)下会在模块加载期立即求值,前向引用会踩 TDZ。DiscoverySchema.capabilities:开放 record → 封闭对象,且 optional → required,键集由WellKnownCapabilitiesSchema.shape派生(不是第二份手写清单),每个键的.describe()继承自对应标志位。scoping先例反过来读:scoping之所以 optional,是因为只有 REST 一个生产者能诚实回答;capabilities三个生产者都能答,而 optional 会把消费者退回「每个标志位都是undefined」—— 恰是裁定要消除的 两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828 前状态。WELL_KNOWN_CAPABILITY_KEYS(键表,派生自 schema)、CapabilityDescriptorSchema/CapabilityDescriptor。packages/client/src/index.ts—— 断言删除:getter 不再复制服务器的键集再断言,而是遍历WELL_KNOWN_CAPABILITY_KEYS构造,返回值因此按构造就是完整的WellKnownCapabilities,as unknown as没有东西可断言了。两条明写的读取规则:服务器缺键读作false(fail-closed,与线上规则同义);非布尔值不做强制转换(off-spec 值在机器可读面上不该被消费者容忍成能力主张,PD #12)。先证红
预测在先:metadata-protocol 缺 dispatcher 的 6 键、dispatcher 缺 WellKnown 的 6 键,两边都应红;方向为「加判据 → 红 → 修生产者 → 绿」。实测两处都如预测,其中第一处比预期更强 —— 红在编译期而非测试期:
即
wellKnown上的WellKnownCapabilities标注让「词表加键 ⇒ 该生产者不答就编译不过」成为结构性事实,而非只靠一条测试。dispatcher 侧的 capabilities 字面量位于无标注的返回对象里,tsc看不见,由闸门捕获:修生产者后:runtime 15/15、metadata-protocol 11/11、rest 14/14 全绿。
SDK 类型谎言的编译期探针
新增
packages/client/src/capabilities-vocabulary.test.ts。这里要如实说明模板的一个预设不成立:谎言藏在类型断言里,所以pnpm typecheck修前修后都是绿的 —— 编译器无法证伪一个 cast。因此探针写成「静态承诺 + 运行时事实」的配对:const promisedBoolean: boolean = caps.transactionalBatch;这行只因声明类型承诺 boolean 才编译得过,紧跟着的expect(typeof promisedBoolean).toBe('boolean')才是证伪者。把 getter 还原成旧实现、拿修前 dispatcher 的真实载荷喂进去:expected 'undefined' to be 'boolean'就是 issue 描述的那句谎,逐字落在断言里。方向与预测一致(红)。闸门:全量性判据
三包 conformance 门各加一组,allowance 由
WELL_KNOWN_CAPABILITY_KEYS推导、绝不手写(与 #5743 的declaredRouteKeys()同一纪律)。三条判据是故意分开的三个问题:enabled都是真布尔;safeParse答不了,因为 zod object 默认 strip 未知键(routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层) #5679 的教训),只有键集检查看得见。反空洞各配一条:metadata-protocol 断言词表真的跨了两个历史半区;dispatcher 断言那六个它从不发的键真的有答案 —— 其中
comments在该 suite 的 kernel 下应为true(其 registry stub 对任何名字都返回对象),这一条正是区分「实测」与「盖章false」的关键,并另配一个不含sys_comment的 registry 断言false,两个方向都钉住。夹具三态处置
packages/spec/src/api/discovery.test.ts17 处夹具补capabilities: allCapabilitiesOff(由词表派生),含拒收类夹具 —— 让它们只因自己那处植入的缺陷而红,而不是顺带多一个缺键。should allow capabilities to be omitted钉的正是被删掉的那条腿。它若只被改写会为空洞的理由继续绿(对着无人生产的键断言toBeUndefined(),契约说 optional 还是说「忘了发」都一样绿),故换成四条:拒收整块缺失、拒收半个词表(直接用两个生产者修前的真实半区当输入)、词表与WellKnownCapabilities同集、每条目符合CapabilityDescriptor。packages/spec/src/api/protocol.test.ts那条夹具以feed: { enabled: true }打头并断言它往返 —— 开放 record 下,一个无人生产、无人消费的键长得和真键一模一样。改为显式钉住封闭后的两个事实(feed不会进入解析结果;缺半个词表被拒),而不是静默删掉。WellKnownCapabilitiesSchema的「缺字段即拒」由钉死单键改为遍历词表逐键剔除,新键无法漏测。按规则的消费半径扫查
不按「改了哪个包」扫,按
DiscoverySchema/WellKnownCapabilities的调用方扫,列全为:packages/{spec,client,metadata-protocol,runtime,rest}、packages/objectql/src/protocol-discovery.test.ts,以及packages/spec/json-schema/api/{Discovery,GetDiscoveryResponse}.json(生成物)。objectui / cloud 侧逐键 grep 见上表。验证
合入当日
origin/main(9a1544677)后重跑,全部真实输出:生成物走 os-regen 四步(build → check → --fix → 复检),重生成
api-surface.json/content/docs/references/**/ strictness ledger。authorable-surface.base.json的重锚漂移照 #5358 剔除 —— 该 diff 全是别人的键(ApiRoutes:mcp、Discovery:scoping、$icontains…)被锚点前移顺带捎上的,与本单无关;剔除后闸门仍绿并给出trails the merge base by 8 key(s) — expected的提示。api-surface.json相对 main 的净 delta 恰为本单三个新导出,无多无少。changeset
.changeset/unified-capability-vocabulary.md——@objectstack/specminor(词表封闭 + 六个新键)、@objectstack/runtime与@objectstack/metadata-protocolminor(两者的线上可见键集都实增六个)、@objectstack/clientpatch(类型面诚实化,无行为承诺变化)。正文按消费者视角写:此前有哪些标志位取决于宿主类型、且可能整个缺席;此后恒有布尔。界外发现
gen:schemarmSync 整个json-schema/会顺手抹掉gen:openapi的产物,rest 的 openapi 路由测试随后 503 假红——check:generated原地跑 build-schemas 也触发 #5371(已存在,查重命中):gen:schema清空整个json-schema/会抹掉gen:openapi的产物,packages/rest随后 8 条假红(expected 503 to be 200)。本轮完整复现,已在原单追加评论与现场输出,未另开新单。callData的action === 'batch'分支静默返回results: []—— 当前不可达,一旦接线就是「成功地什么也没做」 #5856(新开,finding,未认领):callData的action === 'batch'分支静默返回results: []。今天不可达(data 域只路由query),故为观察类;但它是「200 + 空结果」这种最难查的 declared ≠ enforced 形状,且不可达靠的是上游路由表恰好没列它。附带说明:该分支反而支持本 PR 让 dispatcher 答transactionalBatch: false的结论 —— 即便可达,它也不开事务。Generated by Claude Code