feat(rest): 端点条目进 OpenAPI enrichment;摘除 dispatcher 死分支(#5040 E6) - #5171
Merged
Conversation
…5040 E6) `GET {basePath}/openapi.json` has one owner and a real boot proved it is `packages/rest` (#5078: a 355KB OpenAPI 3.1 document with a Host-injected `servers[0]`, 199 paths after `{object}` expansion and two `x-template` markers — every one a rest-server fingerprint). So the documentation face for `apis:` endpoints joins that pipeline instead of a `generateOpenApi` on a metadata service, which would have been the second owner ADR-0076 forbids. Each declaration contributes its literal `path`, the lower-cased method as the Operation key, `operationId` = `name`, and the only two documentation fields the frozen vocabulary carries (`summary` / `description`, omitted when absent rather than replaced by a generated stand-in). Everything else emitted is a fact about how the executor treats the declaration, each cited to its authority: the `query.id` parameter for object_operation get/update/delete, 201 for create and 200 otherwise, and 501 for script / proxy / an object_operation missing its `objectParams`. No invented request or response schemas — the shipped document's `components.schemas` is empty (#5168), so an emitted `$ref` would dangle like the six built-in ones already do. `authRequired` is materialised by the parse, so authenticated operations point at a security scheme read off the document itself (not a scheme name hard-coded in rest, which would be a second place to keep right) and `authRequired: false` emits an explicit `security: []`. Items failing `ApiEndpointSchema` are skipped loudly and named; a duplicate method+path is resolved by lexicographically-first `name`, the endpoint matcher's own rule, so the document cannot name an endpoint the runtime does not run; a declaration never displaces a built-in path+method. Live behaviour is unchanged: publish still rejects a non-empty `apis:` until the E7 flip, so the enumeration is empty and the enricher returns its input document by reference — pinned by tests rather than argued. Refs #5093, #5040 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
…s ledger row (#5078) The dispatcher carried a `GET /openapi.json` branch that duck-typed a `generateOpenApi` method on the metadata service. No implementation has ever provided it — not `MetadataManager`, not `NodeMetadataManager`, not a plugin, not either sibling repo; the only other repo-wide hits for the name are an unrelated boolean config key and its tests. The `if` was constant-false on every request ever served. It was dead a second way, which is what settles the disposition rather than merely arguing it: the real boot in #5078 showed nothing routes `/openapi.json` into `dispatch()` at all. `packages/rest` owns and answers the path end to end, and there is no ownership contest to resolve — only a shadow to remove (ADR-0076 "one route, one owner": a second implementation of a path another package already serves is code `grep` finds and the runtime never runs). The ledger moves with it. The row's note said the path "falls through when metadata service lacks a generator", which reads as sometimes-yes; it was always, so the note was the machine-readable surface ADR-0076 §4 forbids — that inaccuracy is exactly what #5078 was filed about. The row is removed rather than reworded: this ledger enumerates the routes THIS package's dispatcher serves, and it now serves none there. `/openapi.json` leaves `LEGACY_CHAIN_PREFIXES` for the same reason — that list means "branches of the `dispatch()` if-chain", and keeping a prefix in it for a branch that no longer exists would tell the same class of lie in the very change that stops one. Cited comments stay at both sites pointing at the real owner and its truthful row in `packages/rest/src/rest-route-ledger.ts`. Refs #5093, #5040 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 26 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 4, 2026 07:30
os-zhuang
enabled auto-merge
August 4, 2026 07:30
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 #5093
Fixes #5078
Part-of #5040(E 系列第 6 单,按 2026-08-04 的 E6 设计修正执行,替代原设计 §3.5)
一句话
apis:端点的文档面加入 rest-server 既有的 OpenAPI enrichment 管线(与{object}展开同根),同时摘除http-dispatcher里那条双重死的generateOpenApi探测分支、连同route-ledger里对应的失准行。现网行为零变更。为什么是 rest-server,而不是
MetadataManager.generateOpenApi原设计 §3.5 推荐在 metadata service 上实现
generateOpenApi。#5078 的真实 boot 把这条路否掉了:GET /api/v1/openapi.json已经由packages/rest完整拥有并应答 —— 355KB 的 OpenAPI 3.1 文档,三个指纹全部指向 rest-server(servers[0]按 Host 头注入、{object}展开出 199 条 paths、恰好 2 条x-template)。在 metadata service 上再实现一个,就是 ADR-0076 第 1 条明令禁止的第二属主——也正是本单存在的意义。E1 的契约成员因此已剔除。所以本 PR 把文档面送进已有属主自己的管线,而不是新造一个生成器。
改了什么
1.
packages/rest/src/openapi-endpoints.ts(新,纯函数)每条声明贡献一个 path 条目:
path(原样)、method小写作 Operation 键、operationId=namesummary/description—— 缺省即缺省,不生成替身(生成的句子和作者写的句子事后分不出来)object_operation的get/update/deletedocumented 一个必填的idquery 参数(词表无路径模板语法,requireRecordId就是这么取的);create答 201、其余 200;script/proxy与缺objectParams的object_operation答 501不编造任何 request / response schema。 出厂文档的
components.schemas是空的(见下面越范围发现 #5168),此处再写$ref只会多六个悬空引用。请求体一律type: object(「一个 JSON 对象,形状此处不描述」——这是真话),且只在POST/PUT/PATCH上出现。authRequired→ security:默认true由 schema parse 物化,为 true 的条目引用从文档自身读出的 security 方案(doc.security→ 否则第一个securitySchemes);在 rest 里硬写bearerAuth会造出第二处需要保持正确的地方,而且它失效时文档照样能解析、只是指向一个不存在的方案。为 false 的条目写显式security: []—— review 时一眼能看见的那个形状。响亮跳过(与端点匹配器装载门同一姿态):不满足
ApiEndpointSchema的条目点名跳过;同method+path撞车按「name字典序在前者胜」裁决 —— 这是buildEndpointIndex的同一条规则,不这样文档就会指认一个运行时并不执行的端点;撞上内建 path+method 时内建保留、声明略过并报错(一路一主,在文档里同样成立)。2.
rest-server.ts:接进管线{object}展开与端点条目共用一次 protocol 解析,但各自一个try—— 两者描述不同的面,一边枚举失败不该把另一边悄悄清空。3.
http-dispatcher.ts:摘除死分支generateOpenApi作为方法在本仓与两个兄弟仓零实现(同名命中只剩一个无关的 config 布尔键),且 boot 实测没有任何路由把/openapi.json送进dispatch()—— 双重死。删除,原地留注明出处的注释。4.
route-ledger.ts:台账修真GET /openapi.json行的注记」,本 PR 删除了该行(并从LEGACY_CHAIN_PREFIXES移除/openapi.json),两处都留了注明出处的注释。理由:LEGACY_CHAIN_PREFIXES说谎 —— 该常量的 docstring 自述为「dispatch()if-chain 尚未提升进 registry 的分支」,分支删了它就不是了。而这条 docstring 恰恰是 E3(#5040 执行器):挂载 seam ——IHttpServer.setFallbackHandler的 Hono 实现 + dispatcher 端点派发步 + 路由台账登记 #5090 引用GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 写下的:「一张名字不再描述其内容的 pin 清单,正是台账注记悄悄变假的方式」。在终结一次失准的同一个 PR 里再造一次同类失准,不可接受;route-ledger.conformance.test.ts会判定该行 stale 而红(它要求每个 ledger domain 必须是活 registry 前缀 / pinned legacy 分支 / pinned non-dispatch mount 三者之一)。为一条纯交叉引用行新造第四类,结构成本高于收益;packages/rest/src/rest-route-ledger.ts(GET /api/v1/openapi.json,一直是准的)。防漂移价值(「别再往 dispatcher 加 openapi 分支」)由两处注释承载,与 声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 摘除/__api-endpoint时的处理方式一致。原注记「falls through when metadata service lacks a generator」读起来像有时有有时没有;实际从来没有,是 100% fall-through 穿了件条件句的外衣 —— 这就是 #5078 立单的那个点。
空集不变量(本单最吃劲的测试)
publish/validate 对非空
apis:仍然硬拒(E7 前不撤),所以今天枚举出的是空集,本单必须在这个状态下完全不可见。三层钉住:enrichOpenApiWithEndpoints(doc, [])返回同一个对象(toBe),不是等价对象 —— 字节相同由构造保证;api类型但返回空集」与「协议根本不支持api枚举」(= 本改动前的世界)两份响应JSON.stringify逐字节相等。外加「加了端点时不得改动 base spec」(base spec 跨请求缓存,一次污染就会漏进之后每个响应)。
验证
越范围发现(已立单,未在本 PR 修)
#5168 —— 发布出去的 OpenAPI 文档
components.schemas恒为空,而paths里 6 个$ref全部悬空(覆盖 CRUD 全部请求/响应体)。根因已用对照实验坐实:build-openapi.ts的收集判据typeof schema === 'object',而这些 schema 经lazySchema()包装后 Proxy target 是函数,typeof为'function'——OS_EAGER_SCHEMAS=1下同一条命令输出Components: 9,默认输出Components: 0。gen:openapi是全仓两个完全无门禁的生成器之一,所以三层不自洽(空 components、悬空 ref、日志里明晃晃的Components: 0)一处都没红。本单因此不$ref任何 components。未做
generateOpenApi(见开头);api-endpoint-step.ts/api-mapping.ts/endpoint-policy.ts/endpoint-executor.ts/dispatcher-plugin.ts(E5 遗漏面:端点 inputMapping / outputMapping 未实现(设计 §3.4 的 api-mapping.ts),E7 翻转前必须补 #5137 的文件面)与packages/spec;content/docs/releases/。🤖 Generated with Claude Code
https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
Generated by Claude Code