发布出去的 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 也只是对默认部署正确。
关联
发布出去的
GET /api/v1/openapi.json里,built-in 路由那一段描述的每一条路径都不存在。真实 boot 逐条探测,文档里的路径全部 404;真实路由在另一个前缀上。任何人拿这份文档生成客户端,生成出来的客户端每一个数据调用都会 404。基线:
origin/main@5acb93add。在 #5456 的前提复核中量到(#5456 的 body 断言「当前未漂移,7 条与今天的路由面一致」——该断言被本单证伪),按 Prime Directive #10 单独立单。真实 boot 实测
逐条探测文档描述的路径 vs 真实路由:
动词也错。文档写
PUT {object}/{id},真实是PATCH——服务器对PUT明确回 405:逐条对照
packages/spec/scripts/build-openapi.ts的generateCrudPaths/generateMetadataPaths/generateDiscoveryPaths用basePath = '/api'手写出 7 条 path / 10 个 operation:GET /api/{object}GET /api/v1/data/:object/v1、缺/data)POST /api/{object}POST /api/v1/data/:objectGET /api/{object}/{id}GET /api/v1/data/:object/:idPUT /api/{object}/{id}PATCH /api/v1/data/:object/:idDELETE /api/{object}/{id}DELETE /api/v1/data/:object/:idGET /api/metaGET /api/v1/meta/v1)GET /api/meta/typesGET /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/objectstackpackages/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(crud6 +metadata17 +discovery2 = 25 条),文档也只描述了 10 个 operation。为什么没被发现
serve 期的 enrichment(
rest-server.tsregisterOpenApiEndpoints)只做四件事:覆写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也只是对默认部署正确。/api/v1/data/{object}等 —— 最便宜,但把一个可配置值焊进发布产物,是新的漂移源。/data/{object}),由 rest 在 serve 期用自己的getApiBasePath()拼前缀 —— 和它已经在做的{object}展开同一条流水线。spec 保持与部署无关。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 双向相等」在两个方向上都不成立。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 路由)。