Skip to content

gen:schema rmSync 整个 json-schema/ 会顺手抹掉 gen:openapi 的产物,rest 的 openapi 路由测试随后 503 假红——check:generated 原地跑 build-schemas 也触发 #5371

Closed

Description

@os-zhuang

现象(PR #5312 同步接力实测)

packages/spec/scripts/build-schemas.ts(gen:schema)开场 fs.rmSync(OUT_DIR) 清空 packages/spec/json-schema/,而 openapi.json 只由另一条生成链 gen:openapi(build-openapi.ts)写入,且 gitignored、无门(AGENTS.md 已记录 gen:openapi/gen:sbom 无门)。

于是任何「gen:schema(或原地跑 build-schemas 的 check:generated)之后、没有再跑 full spec build/gen:openapi」的窗口里,openapi.json 处于被抹掉状态。此时跑 @objectstack/rest 测试:

FAIL src/rest-openapi-route.test.ts — 5 条
AssertionError: expected 503 to be 200
(loadOpenApiSpec() 找不到 @objectstack/spec/json-schema/openapi.json → OPENAPI_UNAVAILABLE)

失败读起来像 rest 路由坏了,实际是 spec 侧生成物被兄弟生成器清场。归因成本一整圈(§9 陈旧产物陷阱的又一变体:这次不是「陈旧」,是「被抹掉」)。

为什么今天没人在 CI 撞到

CI 的 test job 干净检出后走完整 pnpm build(spec build = gen:schema && gen:openapi && tsup,顺序保证 openapi.json 最后在场);只有本地/agent 工作流会在 build 之后再跑独立 gen:schema 或 check:generated,然后再跑 rest 测试。多 agent 接力(合并后重生成基线)恰好就是这个顺序。

候选修法(供 triage)

  • build-schemas.ts 清场时保留 openapi.json(它不属于该生成器的输出集,删别人的产物本身就是越界);
  • 或 gen:schema 末尾顺带触发 gen:openapi(两者本来在 build 里就是连体);
  • 或 rest 测试对缺失产物给出指向性 fail(「run gen:openapi」),把一圈归因变成一行提示。

第一条最小:各生成器只拥有并清理自己的输出。

观察级:CI 与发布产物不受影响,只咬本地/agent 的验证圈。发现于 #5312 同步接力,当场以 pnpm --filter @objectstack/spec gen:openapi 恢复后 rest 40 files / 608 tests 全绿。

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    又一次实地复现(2026-08-05,#5126 / PR #5610 的开发过程中),与本 issue 描述完全一致,补一组现场数据:

    在一个全新工作树里,为核对生成物跑了 pnpm --filter @objectstack/spec check:docs(其脚本是 pnpm gen:schema && build-docs --check)。随后跑 @objectstack/rest 测试,rest-openapi-route.test.ts 与 rest-endpoint-surfaces-served-only.test.ts 共 8 例转红,stderr 全是同一条:

    [REST] Failed to load OpenAPI spec: ENOENT: no such file or directory,
    open '.../packages/spec/json-schema/openapi.json'
    

    确认为假红:重新跑 pnpm --filter '@objectstack/rest^...' build(其中 spec 的 build = gen:schema && gen:openapi && tsup,把 openapi.json 补了回来)后,同样的三个包全绿 —— rest 48 files / 719 tests、metadata-protocol 42 / 388、types 7 / 109,零改动。

    两点也许对定位有用:

    1. 触发路径不止 gen:schema 本身。任何以 gen:schema 开头的复合脚本都会踩到,check:docs 就是一个 —— 它读起来像个只读的检查,却会把 json-schema/ 整个抹掉重建,只重建 schema 那一半。一个名字里带 check: 的命令产生破坏性副作用,是这个坑特别难联想到的原因。
    2. packages/spec check:generated 的输出里那行 Generated but ungated (2): gen:openapi, gen:sbom 正好解释了为什么没有闸门拦住它:openapi.json 被抹掉后没有任何 check 会发现它不见了,红只会在下游 rest 的测试里出现,而且报的是 rest 的错。

    对并行开发 agent 的影响是实打实的:这 8 条红看起来完全像「你的改动弄坏了 openapi 路由」,而实际改动只是往 ERROR_CODE_LEDGER 加了一个错误码。


    Generated by Claude Code

  3. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 6, 2026
  4. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮判级(#4949 纪律):晋级 pm:queue,并补域标签 domain:spec(原单无 domain:*;落点 packages/spec/scripts/build-schemas.ts)。

    过时前提检查:origin/main 889ae47 上 build-schemas.ts:137 仍是 fs.rmSync(OUT_DIR, { recursive: true, force: true, … }) 整目录清场,openapi.json 仍只由 build-openapi.ts(gen:openapi)写入;前提成立。

    晋级理由(为什么不按观察类持有):本单产出的是假红,而且专挑本仓的主力工作流下手 —— 正文说明 CI 干净检出走完整 pnpm build 不会撞到,撞到的是「build 之后再单独跑 gen:schema / check:generated,然后跑 rest 测试」这个顺序,而多 agent 接力(合并后重生成基线)恰好就是这个顺序。症状(rest-openapi-route.test.ts 五条 503→期望 200)读起来像 rest 路由坏了,归因成本实测"一整圈"。一条会把无关车道的 dev 引向错误诊断的假红,属队列健康的共享损伤(SKILL「The merge queue is still one shared serial resource」同源),不是观察类。

    修法已由原单给出两条候选,均可机械验收:① 清场时保留 openapi.json(它不属 gen:schema 的输出集,删别人的产物本身越界);② gen:schema 末尾顺带触发 gen:openapi。⚠️ 按 Operational note 8(共享基础设施先按症状复查 main),派发前再跑一次 git log origin/main -- packages/spec/scripts/build-schemas.ts,确认在飞期间没有别人已经改掉这段清场逻辑。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  5. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊交叉链接(不改本单分类,不构成认领):本轮把 #5757 判为与本单同一现场的另一半,并按收敛规则把派发入口保留在本单。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  6. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    今天(2026-08-06)在 #5588 第一棒的 worktree 里原样复现一次,补一条独立佐证 —— 不另立单。

    顺序完全是本单描述的那条:

    1. pnpm --filter @objectstack/rest test → 57 files / 811 tests 全绿(此时 packages/spec/json-schema/openapi.json 在);
    2. pnpm --filter @objectstack/spec check:generated → 10/10 绿(它内部的 check:docs = pnpm gen:schema && build-docs --check,build-schemas.ts:179 对 OUT_DIR 做 fs.rmSync(recursive));
    3. 同一个 worktree 再跑同一批 rest 测试 → rest-openapi-route.test.ts 12 条全红,expected 503 to be 200,因为 loadOpenApiSpec() 找不到文件;
    4. pnpm --filter @objectstack/spec gen:openapi → 立刻回绿(32/32、811/811)。

    ls packages/spec/json-schema/ 在第 2 步之后是 16 个子目录 + objectstack.json,唯独少 openapi.json —— 因为那是 gen:openapi 单独写的,gen:schema 只负责重建自己那部分。

    补一句本单没写、但值得记的:check:generated 自己的收尾台账明确打印「Generated but ungated (2): gen:openapi, gen:sbom」—— 它知道 gen:openapi 无门禁,但不知道自己的兄弟步骤会删掉它的产物。这个假红方向对 rest 会变得更显眼:#5588 落地后(PR #5821),GET /openapi.json 的 built-in 段虽然由 rest 自己产出,components.schemas / info / securitySchemes 仍从这份产物读,产物缺失时整条路由仍然 503。


    Generated by Claude Code


    Generated by Claude Code

  7. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    疑似域误标:本单落点在 gen:schema / gen:openapi 脚本(packages/spec/scripts/**,C 包 #5163 文件面),且与 spec-tooling 座位在飞的锚点族(#5370 起)同根,建议改标 domain:spec-tooling ——交分诊座位定夺。背景:维护者 2026-08-06 拍板 spec 车道缩盘方案,见 #5837。


    Generated by Claude Code

  8. added and removed
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 6, 2026
  9. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    标签落实(维护者 2026-08-06 指示,session_01LeEfA7CFwbJb7JJmXm2KM3):按 08-06 转席建议执行 domain:spec → domain:spec-tooling(落点 gen 脚本,与该座位在飞锚点族 #5370 同根)。注意 #5757 的派发入口收敛在本单——落地时按其 08-06 评论一并复看 openapi 门禁归宿。


    Generated by Claude Code

  10. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    又一次复现(#5672 实施途中,2026-08-06,claude/issue-5672-unified-capabilities)。查重命中本单,故不另开。

    按 AGENTS.md 记载的标准流程走(改了 packages/spec → pnpm --filter @objectstack/spec build && check:generated && check:generated --fix),随后 packages/rest 8 条测试假红:

    Test Files  2 failed | 53 passed (55)
         Tests  8 failed | 778 passed (786)
    
    [REST] Failed to load OpenAPI spec: ENOENT: no such file or directory,
    open '.../packages/spec/json-schema/openapi.json'
    
    FAIL src/rest-openapi-route.test.ts > serves a document identical to the pre-#5093 one …
    AssertionError: expected 503 to be 200
    FAIL src/rest-endpoint-surfaces-served-only.test.ts > drops the runtime-written path …
    AssertionError: expected [] to deeply equal [ '/api/v1/apps/showcase/tasks' ]
    

    触发点与本单描述一致:check:generated 里的 check:authorable-surface 会原地跑 build-schemas,日志打出 Cleaning output directory: packages/spec/json-schema,openapi.json 随目录一起没了。补一次 pnpm --filter @objectstack/spec gen:openapi 后 rest 立刻 58 files / 824 tests 全绿。

    两点补充观察,或许对定 fix 形状有用:

    1. check:generated 自己的输出里就写着 Generated but ungated (2): gen:openapi, gen:sbom — nothing verifies these are current。也就是说这个产物既会被别的 gen 步骤删掉,又没有任何 gate 发现它不见了 —— 两个性质叠加,失败才会以「别的包里 8 条业务测试红」这种完全不指向成因的形态出现。
    2. 假红的读感极具误导性:expected 503 to be 200 / expected [] to deeply equal [...] 看上去像本次改动打断了路由或枚举,没有一条指向缺文件;只有 stderr 里那行 ENOENT 是线索,而它在 vitest 默认输出里被淹在测试名之间。本轮我是先怀疑自己的 discovery 改动、翻了 .gitignore 确认 packages/spec/json-schema/ 整目录未入库之后才定位到的。

    Generated by Claude Code

  11. self-assigned this
    on Aug 7, 2026
  12. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 5 轮(spec-tooling 车道,座位登记 #6018;rc.5 已发布,维护者 2026-08-07 指示恢复派发)
    会话:session_014wsZeReNTqiceBfLb5Pyf5
    分支:claude/issue-5371-gen-schema-clears-only-its-own
    Worktree:objectstack-issue-5371
    域:domain:spec-tooling
    文件面:packages/spec/scripts/build-schemas.ts(清场范围收窄到自有产物)+ 必要的 pin 测试 + .changeset/*.md。⛔ 不碰 build-docs.ts / lib/file-description.ts / lib/format-type.ts / lib/zod-graph.ts(本轮并行四单各占其一)、不碰 zod 文件。(越界即停,报告说明)

    重新定价(⛔ 不沿用正文的 pre-#6069 假设):#5837 分片(PR #6069)已改写 build-schemas 的写盘布局,清场逻辑与 OUT_DIR 语义可能已变(main 现在 rmSync(OUT_DIR) 在 :285);先对 origin/main 复核现状再定修法。正文给的三条候选里第一条(各生成器只清自己的输出)仍是最小修法,但要按分片后的实际布局落地。

    同文件让行:#5898 / #5847 排本单之后。


    Generated by Claude Code

  13. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    ContributorAuthor

    验收:ACCEPT → PR #6200(转 ready 入队)

    亲核:36 检查零非绿(ESLint / TypeScript Type Check 逐个 conclusion=success);5 文件,全在申报面内。

    三点设计判断值得记:

    1. 所有权注册表是双向绑定的:lib/json-schema-out-dir.ts 声明 openapi.json → gen:openapi,而 build-openapi.ts 从注册表导入自己的产物名——声明因此不可能与它保护的那个生成器漂移(否则就是「声明一份、实现一份」的老毛病,本车道今天刚在 [finding] TEST_DEBT 的 tests 字段是闸门自己已经算出来的数字,却手写在台账里 —— 19 条中 12 条已漂(runtime 记 66,实测 101) #5826 处理过同型)。
    2. deny-list 而非 allow-list 有论证,不是省事:category 目录派生自 build-schemas.ts 自己的 Protocol map,allow-list 会让退役 namespace 的陈旧树永久幸存,而 json-schema 在 spec 的 files 白名单里——那意味着陈旧产物会随包发布。理由已写进 lib 头注释。
    3. 验收严格按 issue 描述的现场序列复现:gen:schema 单跑后 openapi.json md5 逐字节不变(而植入的陈旧探针照常被扫掉,清场仍是彻底的)→ check:generated 全绿 → @objectstack/rest 62 files / 854 tests 全绿,幻影 503 不再出现。反向验证恢复被删的那条 rmSync 后,两个新 pin 报出与现场报告逐字一致的 ENOENT。

    边界守得也对:没有顺手关 #5757(json-schema 树的新鲜度/存在性门缺口),该单是候选 (1),交叉链接留在 PR body。

    附带收益:本单落地后,check:generated 不再删 openapi.json——今天有四个 agent 各为此付过一圈归因成本,#5898 / #5847 两个后续单的验证圈从此不再交这笔税。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions