Repository navigation
发布出去的 /api/v1/openapi.json 描述的 built-in 路由一条都不存在 —— 10 个 operation 真实 boot 全部 404(证伪 #5456 的「当前未漂移」) #5588
Description
发布出去的 GET /api/v1/openapi.json 里,built-in 路由那一段描述的每一条路径都不存在。真实 boot 逐条探测,文档里的路径全部 404;真实路由在另一个前缀上。任何人拿这份文档生成客户端,生成出来的客户端每一个数据调用都会 404。
基线:origin/main @ 5acb93add。在 #5456 的前提复核中量到(#5456 的 body 断言「当前未漂移,7 条与今天的路由面一致」——该断言被本单证伪),按 Prime Directive #10 单独立单。
真实 boot 实测
pnpm dev:crm -- --fresh -p 39177
GET /api/v1/openapi.json → 200, 151 paths({object} 已按 CRM 对象展开)
逐条探测文档描述的路径 vs 真实路由:
文档里的(展开后)路径 真实结果
/api/crm_contact 404 {"error":"Not found"}
/api/meta 404 {"error":"Not found"}
/api/meta/types 404 {"error":"Not found"}
/api/.well-known/objectstack 404 {"error":"Not found"}
真实路由 结果(401 = 路由在,鉴权挡住)
/api/v1/data/crm_contact 401 {"error":"UNAUTHENTICATED",...}
/api/v1/meta 401
/api/v1/discovery 200
/.well-known/objectstack 200 ← dispatcher 在根上服务,不在 /api 下
动词也错。文档写 PUT {object}/{id},真实是 PATCH——服务器对 PUT 明确回 405:
PUT /api/v1/data/crm_contact/xyz → 405
PATCH /api/v1/data/crm_contact/xyz → 401
逐条对照
packages/spec/scripts/build-openapi.ts 的 generateCrudPaths / generateMetadataPaths / generateDiscoveryPaths 用 basePath = '/api' 手写出 7 条 path / 10 个 operation:
| base spec operation | 真实 rest 路由 | 判定 |
|---|---|---|
GET /api/{object} |
GET /api/v1/data/:object |
路径错(缺 /v1、缺 /data) |
POST /api/{object} |
POST /api/v1/data/:object |
路径错 |
GET /api/{object}/{id} |
GET /api/v1/data/:object/:id |
路径错 |
PUT /api/{object}/{id} |
PATCH /api/v1/data/:object/:id |
路径错 且动词错(PUT 回 405) |
DELETE /api/{object}/{id} |
DELETE /api/v1/data/:object/:id |
路径错 |
GET /api/meta |
GET /api/v1/meta |
路径错(缺 /v1) |
GET /api/meta/types |
全仓没有这条路由 | 描述了一条谁都不服务的路由(最接近的是 GET /api/v1/meta,它返回 types) |
GET /api/meta/{type} |
GET /api/v1/meta/:type |
路径错(缺 /v1) |
GET /api/meta/{type}/{name} |
GET /api/v1/meta/:type/:name |
路径错(缺 /v1) |
GET /api/.well-known/objectstack |
rest 没有这条路由 | dispatcher(packages/runtime/src/dispatcher-plugin.ts:658)在根路径 /.well-known/objectstack 上服务;rest 的 discovery 是 GET /api/v1 + GET /api/v1/discovery |
字面比对:0/10 命中。补 /v1 和 /data 之后仍有 3/10 对不上。
反方向同样不等:RestServer.getRoutes() + 两个 direct-mount registrar 枚举出 91 条真实路由;即使只看 base spec 自称覆盖的三个 ledger family(crud 6 + metadata 17 + discovery 2 = 25 条),文档也只描述了 10 个 operation。
为什么没被发现
serve 期的 enrichment(rest-server.ts registerOpenApiEndpoints)只做四件事:覆写 servers[0](只写 origin,不含 basePath)、展开 {object}、合并声明式端点、覆写 info.version。没有任何一步重写 path 前缀。而 check:generated 的收尾台账把 gen:openapi 记为无门禁({ gen: 'gen:openapi', why: 'the OpenAPI document is generated but no check gate compares it to the routes' }),#5168 补的是产物自洽门($ref 能解析、schema 不降级),不看路由。所以两边各自「正确」,合起来全错,全绿。
现有测试还把错误形状钉住了:packages/rest/src/rest-openapi-route.test.ts:125 断言 body.paths['/api/{object}']['x-template'] === true —— 它证明了服务出去的文档字面上就带着 /api/{object}。
处置需要一次契约裁决(本单不预设)
注意 apiPath 是可配置的(api.apiPath ?? api.basePath + '/' + api.version,rest-server.ts:2830),所以 packages/spec 里的静态 JSON 原则上无法对所有部署写对前缀——今天写 /api 只是错得更彻底,写死 /api/v1 也只是对默认部署正确。
- A 把模板改成具体的
/api/v1/data/{object}等 —— 最便宜,但把一个可配置值焊进发布产物,是新的漂移源。 - B 模板相对化(只写
/data/{object}),由 rest 在 serve 期用自己的getApiBasePath()拼前缀 —— 和它已经在做的{object}展开同一条流水线。spec 保持与部署无关。 - C built-in 路由这一段整体由 rest 产出(它已经在
openapi-endpoints.ts里产出声明式端点那一段了),spec 只保留它真正拥有的components.schemas/info/securitySchemes。ADR-0076 意义上的唯一属主本来就是 rest(GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 已用真实 boot 确权),这样结构上不可能漂移,也就不再需要gen:openapi的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456 那道对账门。
关联
- 阻塞
gen:openapi的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456(对账门):门要落地,先得决定 base spec 该描述什么;在此之前「模板集合 ↔ ledger 双向相等」在两个方向上都不成立。 - 发布出去的 OpenAPI 文档
components.schemas是空的,而 6 个$ref全部悬空 ——lazySchemaProxy 撞上typeof === 'object'判据 #5168(自洽门,已修)、GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078(属主确权)、gen:schemarmSync 整个json-schema/会顺手抹掉gen:openapi的产物,rest 的 openapi 路由测试随后 503 假红——check:generated原地跑 build-schemas 也触发 #5371(产物生命周期)、fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 / 声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224(faces announce only what the matcher serves —— 同一类「面在说谎」,那单管的是声明式端点,本单管的是 built-in 路由)。 - AGENTS.md Prime Directive chore: version packages #10(绝不宣传运行时不提供的能力)。
Activity
baozhoutao commentedon Aug 5, 2026
进维护者决策收件箱(cli 车道 PM,
session_016FNvXhtSdnEGEfLEsMmvxh,已打needs-user-decision;domain 标签留分诊席)v17 相关性提请注意:这份
/api/v1/openapi.json是随包发布的对外契约文档,consumer 照它生成客户端会全线 404——若 v17 发布窗口临近,可能需要一个止血决定。待拍板三问(dev 的完整三轴论证在 #5456 的 needs_decision 回报,正文有逐条对照表):
apiPath焊进静态发布产物——修掉今天这一份,修不掉这一类)/ B 模板相对化 + rest serve 期拼前缀(spec 保持部署无关,仍需gen:openapi的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456 的门)/ C built-in 段整体移交 rest 产出(rest 已在产出声明式端点段;ADR-0076 属主GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 已确权;结构上消灭第二真相源,gen:openapi的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456 的门随之不需要存在)。dev 推荐 C 其次 B 反对 A,本席同意——与 fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 刚立的「faces announce only what the matcher serves」同一原则;若需止血热修可先 A 后 C,但请明示。rest-route-ledger现有 disposition 惯例,每条内建路由 documented 或带 review 过的 undocumented 理由,新路由只有一处要填。拍板后执行路径:裁 C → #5588 按 C 实施、#5456 关闭(门无对象);裁 A/B → #5588 先修、#5456 解锁按台账形状落门。cli 车道随时可派。
Generated by Claude Code
baozhoutao commentedon Aug 6, 2026
维护者裁定(2026-08-06,经 cli 车道 PM
session_01DWUR56YsttL5sTF72Q75TQ誊写):C —— built-in 路由段整体移交 rest 产出。spec 只保留它真正拥有的components.schemas/info/securitySchemes;ADR-0076 属主 rest(#5078 确权),结构上消灭第二真相源。v17 窗口若临近可先 A 止血,但默认直接实施 C。执行拆分(两棒,先 rest 后 spec,保证服务出的文档任何时刻不缺 built-in 段):
packages/rest在registerOpenApiEndpoints的 serve 期流水线里丢弃静态产物中过时的 built-in 段、改由自身路由事实(rest-route-ledger/getRoutes())产出正确的 built-in operations(路径含真实前缀、动词 PATCH、不再描述幽灵路由);rest-openapi-route.test.ts:125等钉住旧形状的 pin 同步翻正为断言新语义(承重翻转,不是删断言)。packages/spec/scripts/build-openapi.ts摘除generateCrudPaths/generateMetadataPaths/generateDiscoveryPaths,发布的静态产物不再携带任何路由描述;发布出去的 OpenAPI 文档components.schemas是空的,而 6 个$ref全部悬空 ——lazySchemaProxy 撞上typeof === 'object'判据 #5168 自洽门随之调整覆盖面。连带处置:#5456(对账门)关闭,not planned —— 裁 C 后门无对象(rest 既是路由事实又是文档产出方,无两方可对账);若 C 实施受阻改走 A/B,重开它按台账形状落门。
标签:摘
needs-user-decision→pm:queue,归 cli 车道排程(v17 相关,rest 包空出后优先)。Generated by Claude Code
baozhoutao commentedon Aug 6, 2026
认领:PM 循环第 3 轮(cli 车道,v17 优先)
会话:
session_01DWUR56YsttL5sTF72Q75TQ分支:
claude/issue-5588-openapi-builtin-rest-ownedWorktree:
objectstack-issue-5588域:
domain:cli(裁决 C 的第一棒,rest 半边)文件面:
packages/rest(openapi-endpoints.ts/rest-server.ts的registerOpenApiEndpoints流水线 +rest-openapi-route.test.ts等 pin)+ changeset;⛔ 不触packages/spec(第二棒 #5744 归 spec 座位,Blocked-by 本单)执行注记:①serve 期丢弃静态产物中过时的 built-in 段、由 rest 自身路由事实产出正确段(真实前缀经
getApiBasePath()、PATCH 非 PUT、无幽灵路由);②rest-openapi-route.test.ts:125等钉旧形状的 pin 承重翻转;③在飞注意:PR #5788(纯 rest 测试替身,已在合并队列)与本单文件面不相交,落地后 merge origin/main 照常;④验收对照 issue 正文的探测表,PR 正文给真实 boot 的前后对比。Generated by Claude Code
baozhoutao commentedon Aug 6, 2026
验收:ACCEPT(第一棒)(cli 车道 PM,
session_01DWUR56YsttL5sTF72Q75TQ,2026-08-06 07:0xZ)→ PR #5821routeManager.getAll()(路由器匹配请求的同一张表)serve 期产出,静态旧段丢弃不合并——前缀跟getApiBasePath()配置、动词即注册动词、幽灵行结构上不可能;{object}展开与声明式端点合并原样保留;components.schemas/info/securitySchemes仍归 spec。RestServer不可枚举 —— 因此进不了/openapi.json,也进不了任何运行时自省 #5822 记账(它们对 RestServer 不可枚举,凭空补上正是本单要修的缺陷类);不编造 schema/状态码,匿名路由的 security 取「故意少说」的安全方向。success;changesetrest: minor档正确;文档体积 151→1078 paths 是修复的直接结果(真实路由终于全部被描述),不是膨胀。RestServer不可枚举 —— 因此进不了/openapi.json,也进不了任何运行时自省 #5822 已立(finding);gen:schemarmSync 整个json-schema/会顺手抹掉gen:openapi的产物,rest 的 openapi 路由测试随后 503 假红——check:generated原地跑 build-schemas 也触发 #5371 未重复立单、改为追加实测佐证评论——查重纪律执行到位。转 ready + 挂 auto-merge;MERGED 后:①在 #5744 留解锁评论通知 spec 座位(第二棒可动);②rest 线排 #5563(独占窗口,须待 #5673 落地、四包全空)。
Generated by Claude Code
fix(rest): `/openapi.json` 的 built-in 路由段改由 rest 按自身路由事实产出(objectstac…