Skip to content

fix(spec): openapi 静态产物摘除 built-in 路由段,只保留契约半边 (#5744) - #5967

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-5744-openapi-drop-static-paths
Aug 6, 2026
Merged

fix(spec): openapi 静态产物摘除 built-in 路由段,只保留契约半边 (#5744)#5967
qq9340100 merged 1 commit into
mainfrom
claude/issue-5744-openapi-drop-static-paths

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #5744

#5588 维护者裁定 C第二棒(spec 半边)。第一棒 PR #5821 已合入 origin/main(75f82f3ac),registerOpenApiEndpoints 流水线里 enriched.paths = builtin.paths —— serve 期已由 rest 按自身路由表自产 built-in 段并整体丢弃静态产物的 paths。因此本单是零行为变化的清理

前置复核(在 origin/main 上)

事实 证据
第一棒已合入 git merge-base --is-ancestor 75f82f3ac origin/main → YES
serve 期整体丢弃静态段 packages/rest/src/rest-server.ts:3271 enriched.paths = builtin.paths;(注释显式写 DISCARDED rather than merged)
静态产物旧段确实还在发 摘除前 pnpm --filter @objectstack/spec gen:openapiPaths: 7,9 个 $ref 全部在 paths 段内、components 外 0 个

改了什么

packages/spec/scripts/build-openapi.ts

  • 摘除 generateCrudPaths / generateMetadataPaths / generateDiscoveryPaths 三个函数、interface OpenApiPathconst basePath = '/api' 及其调用;
  • 顺带摘除 tags: [CRUD, Metadata, Discovery] —— 这三个 tag 只为命名被摘掉的三段而存在,任何文档里都没有 operation 携带它们,serve 期的 tag 列表由 rest 与路由段一起产出(enriched.tags = builtin.tags),所以移除同样是零行为变化;
  • 保留 servers(rest 把它作为兜底项接在实时请求源之后,删了会改变服务出的文档)、securityinfocomponents;
  • 控制台摘要去掉 Paths: 行(Paths: 0 会被读成「收集器什么都没产出」而不是「这份产物不描述路由」),换成一行明说路由段归属。

paths 是「键不存在」而不是 paths: {} —— 这是本单唯一需要拍板的形状细节,理由写在文件头注释里:OpenAPI 3.1 里 paths 可省(paths/components/webhooks 三者有其一即合法),而两种写法说的不是一件事。paths: {} 断言「这个 API 什么都不服务」,是假的;键不存在则对路由不作任何断言,这才是这份产物有资格作出的声明。消费侧唯一的读取点是 rest 的 spec.paths ?? {}enriched.paths = builtin.paths,两种写法它都吃。

产物新形状(顶层 5 键):openapi / info / servers / components(schemas 9 条 + securitySchemes)/ security

#5168 自洽门:按新形状调整,不留活口

摘段后 9 个 $ref 一个不剩 —— 它们全住在被摘掉的 operation 请求/响应体里。这正是「断言还绿着,是因为什么都没产出」那一类,所以按三条处理,而不是为了让门「有东西可断言」保留任何静态段:

  1. 如实标注空断言:新增 emits a document with no $ref at all — so the ref gate is vacuous TODAY,把这个事实写在门旁边,而不是让下一位读者以为 findDanglingRefs(doc) === [] 是覆盖。

  2. 反向验证改指向幸存面:旧的两条变异改写 generateCrudPaths 里的 #/components/schemas/ApiError' 字面量 —— 该字面量已随段消失,旧变异会撞上 runGenerator 的「mutation must actually change the source」守卫,变成报告测试坏了而不是报告门有效。新变异注入 components:

    • Broken: { $ref: '#/components/schemas/ApiErrorTypo' }(组件引用被改名的兄弟);
    • Recursive: { ... $ref: '#/$defs/Recursive' } —— 门保留下来真正要防的活风险:z.toJSONSchema 把复用/递归子 schema 放进它返回值根部的 $defs,用根相对 #/$defs/… 指过去;九条各自独立转换后停在 components.schemas[Name] 下,该指针于是指向整份 OpenAPI 文档的根 —— 那里没有 $defs。九条今天都不是递归形状,这条变异就是「哪天有一条变成递归」的预演。

    三条反向验证的预期方向都是 RED(非零退出),实测三条全 RED,并各自匹配到具体的报错文本。

  3. 补两条新钉子:产物不含路由段(七条幽灵路径 + 三个 tag 逐条断言不存在,paths/tags 键为 undefined),以及产物仍完整保留 spec 拥有的五个顶层键 —— 后者防的是把这次收缩做过头、连 rest serve 期要读的东西一起删掉。

lib/openapi-self-consistency.ts 的模块 TSDoc 同步补上「规则 1 今天为空断言、保留它是为了 $defs」这一节。

check:generated 台账

gen:openapi 那条 why 原文是「no check gate compares it to the routes」。裁定 C 之下产物里没有路由,这句话名指的对账没有第二方。改写为真正剩下的缺口:没有任何东西把产物的 components.schemassrc/api 对账,产物过期不会让任何东西变红;自恰性由 #5168 在写盘前自检覆盖,时效性没有。(#5456 已依此关闭 not planned。)

AGENTS.md 里「Two generators have no gate at all — nothing verifies their output is current」未提路由,新现实下仍然成立,故未改。

验收对照

验收项 结果
gen:openapi 绿、产物 paths 不含手写 built-in 段 Components: 9 / Route sections: none;"paths" in doc === false,七条幽灵路径与三个 tag 在整份 JSON 里 0 命中
服务出的 built-in 段仍在且正确 packages/rest 全量 60 files / 833 tests 绿,含 #5588 那一组 7 条(publishes no path the server does not mountdocuments the real CRUD surface, with the real verbs 等)
#5168 门与台账措辞与新现实一致 ✅ 见上

关于 #5821 那条 serve 期钉子(discards the static artifact section even though spec still emits it)

按派单要求如实报告:该钉子直接读本进程加载的静态产物做比对,摘段后它的输入集合 Object.keys(artifact.paths ?? {}) 变成空集,那个 for 循环因此成为空断言。从 rest 的解析路径实测:

resolved artifact: packages/spec/json-schema/openapi.json
artifact top-level keys: [ 'openapi', 'info', 'servers', 'components', 'security' ]
#5821 pin input set (stale paths): [] -> size 0

它仍然是绿的,而且没有整体退化为空断言 —— 同一条 test 里循环之后的三条断言仍然咬合:components.schemas 键集与产物逐字相等、securitySchemes 深等、info.title 相等。也就是说它现在钉的是「spec 拥有的那半边原样穿过 serve 期」,这半边我这次特意没动。⛔ 没有为了让它「有东西可断言」而保留任何静态段。该 test 的标题与注释(「even though spec still emits it」)现在措辞过期了 —— 属 packages/rest,第一棒收官后本单不碰,已作为观察项记在报告里交 PM 决定。

变更集

.changeset/openapi-static-artifact-contract-only.md,@objectstack/spec: major。判 major 而非 minor:@objectstack/spec/openapi.jsonexports + files 里的已发布导出,pathstags 两个顶层键消失(不是变空),直接 doc.paths 取值的外部消费者会拿到 undefined。被删内容本身 0/10 命中、照它生成的客户端每个数据调用都 404,但「消费者会看到破坏」这一事实与「被删的东西本来就是错的」是两回事,按前者定档。要一份带路由的文档,唯一正确来源是运行中的服务:GET {apiPath}/openapi.json

验证记录(全部前台阻塞执行)

pnpm --filter @objectstack/spec test scripts/openapi-self-consistency.test.ts --reporter=verbose
  → Test Files 1 passed (1) / Tests 21 passed (21)
    含 3 条反向验证全部 RED-as-predicted

pnpm --filter @objectstack/spec test          → Test Files 323 passed / Tests 8271 passed
pnpm --filter @objectstack/spec typecheck     → tsc --noEmit 绿;check:test-typecheck OK
pnpm --filter @objectstack/spec check:generated --reconcile-only
  → 18 check: + 13 gen: scripts, all classified (…2 ungated generators…)
pnpm --filter @objectstack/rest test          → Test Files 60 passed / Tests 833 passed
npx eslint (4 个改动文件) --no-inline-config   → exit 0
pnpm check:nul-bytes / doc-authoring / docs-audit-scope / role-word / adr-anchors /
     org-identifier / route-envelope / error-code-casing / published-files /
     engine-double-contract / release-notes   → 全绿
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' (4 个改动文件 + changeset) → clean

生成物纪律:packages/spec/json-schema/ 已被根 .gitignore 忽略,产物不入库;gen:schema(在 pnpm --filter '@objectstack/rest^...' build 里)跑完后已复核 openapi.json 仍在且为新形状(#5371 的 rmSync 陷阱)。


🤖 Generated with Claude Code

https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY


Generated by Claude Code

#5588 裁定 C 第二棒。`build-openapi.ts` 手写的 `generateCrudPaths` /
`generateMetadataPaths` / `generateDiscoveryPaths`(7 path / 10 operation,真实
boot 0/10 命中)整体摘除;`paths` 与三个只为命名它们而存在的 tag 一并消失。第
一棒 #5821 已让 rest 在 serve 期自产该段并丢弃静态旧段,故本次为零行为变化。

#5168 自恰门按新形状调整:如实标注 `assertRefsResolve` 今天空断言,保留它防的是
`z.toJSONSchema` 的 `#/$defs/…` 指针;反向验证的变异改注入 components 侧。新增
「产物不含路由段」「产物仍保留 spec 拥有的五个顶层键」两条钉子。`check:generated`
台账 `gen:openapi` 的 `why` 改写为真正剩下的缺口(时效性无门禁)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 1:26pm

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants