Skip to content

fix(spec): docs-gen 把 retiredKey() 墓碑渲染成 never,而不是 any (#5606) - #6058

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5606-format-type-never-tombstone
Aug 7, 2026
Merged

fix(spec): docs-gen 把 retiredKey() 墓碑渲染成 never,而不是 any (#5606)#6058
os-zhuang merged 3 commits into
mainfrom
claude/issue-5606-format-type-never-tombstone

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5606

状态(2026-08-07,解除停放):停放条件已满足 —— #5837 分片随 #6069 落地,spec 座位独占窗口解除。本分支已按解除序列走完:merge origin/main(merge 先单独 commit)→ gen:schema && gen:docs 整体重生成 → 生成页作为独立 commit 提交。下表那条唯一的预期红 check:docs 已清零(本地实测 ✅ 232 generated files in sync with packages/spec)。⛔ 仍保持 draft,转 ready 由 PM 执行。

前提复核(对 origin/main,非引用 issue 正文)

三条都自己量过,前提成立:

  1. packages/spec/scripts/lib/format-type.tsformatType() 确实没有 z.never() 分支。

  2. 用真实转换器探过墓碑节点,输出向与 io: 'input' 回退向都是同一个形状:

    "heading": { "description": "[REMOVED] …", "not": {} }
    

    type / 无 $ref / 无 enum ⇒ 一路落到 return prop.type || 'any'

  3. 页面实例仍在 origin/main:

修法

两半都落在 scripts/lib/format-type.ts 内,没有碰 build-docs.ts —— INLINE_KEY_LIMIT 与内联摘要装配本来就住在 format-type.ts 里,所以派单里「可选第二半仅当落点仍在 format-type.ts 时才做」的条件成立(#5837 的 build-docs.ts 读点面未被触及)。

  1. { not: {} }never,置于 formatType() 最前:该节点什么都不接受,后面没有任何分支能比它更具体。准确的 TypeScript(该键 z.input 类型本就是 never),而且不像 any 需要旁边的散文兜底。非空的 not({ not: { type: 'string' } })是普通的否定约束,匹配,保持原渲染。
  2. 内联摘要在计入 INLINE_KEY_LIMIT 之前先剔除墓碑。 摘要格只印前 4 个声明键的 k?: type,压根没有描述列 —— 嵌套的墓碑无处安放处方。退役键已不是可写面,不该占四个槽位之一,更不该把作者必须写的键挤到 后面。[spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 的「把墓碑挪到 shape 底部」规避办法覆盖不了这一类:ADR-0049 enforce-or-remove:IndexSchema.typeIndexSchema.partial 没有任何 DDL 消费者 #5248IndexSchema 退役到只剩 3 个活键,上限为 4 时第一个墓碑在数学上必然进摘要。

渲染变化(单测里钉住的,且已由本轮重生成逐字兑现):

位置 之前 之后
Typography.fontFamily { base?: string; heading?: any; mono?: any } { base?: string }
ObjectSchema.indexes { name?: string; fields: string[]; unique?: …; type?: any; … }[] { name?: string; fields: string[]; unique?: … }[](无 ,摘要已完整)
Theme.animation 等自有表行的墓碑 类型格 any 类型格 never(描述列 [REMOVED] 处方原样保留)

测试与反向验证

新增 packages/spec/scripts/format-type.test.ts 两个 describe 块,共 8 个 case。

  • 全量:pnpm --filter @objectstack/spec test325 files / 8316 tests passed
  • 定向:npx vitest run scripts/format-type.test.ts1 passed / 27 tests passed
  • pnpm --filter @objectstack/spec typecheck → 绿(check:test-typecheck OK;79 file / 691 error 的既有 debt 账本未增未减)。
  • npx eslint 两个改动文件 → 退出码 0。
  • node scripts/check-nul-bytes.mjs → OK(5779 个 tracked 文本文件);另按扩面自扫 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' 三个改动文件,无命中。

反向验证(实测,两半分开做;方向都是常规的「还原缺陷 → 新钉子变红」,因为这些断言的是修复产出的肯定渲染,不是「某个发现消失」):

  • 注释掉 isNeverNode 前置返回 → 3 failed | 24 passed,三条红分别报 expected 'any' to be 'never'expected 'any[]' to be 'never[]'。第一块里第 4 个 case 故意保持绿:它断言的是非 never 的渲染(非空 not 的越界护栏),所以是护栏不是死钉。第二块整体保持绿 —— 这是两半彼此独立的诚实信号:把墓碑从摘要里滤掉,与它本来会渲染成什么无关。
  • 还原该返回、把摘要改回不过滤的 Object.keys(prop.properties)4 failed | 23 passed,四条红全在第二块,报出只做第一半会发布的中间态:{ base?: string; heading?: never; mono?: never }{ dead1?: never; a: string; dead2?: never; b?: string; … }{ x?: never; y?: never }。比 any 安全,但仍在拿读者的四个槽位养没人能写的键。

两次实测的数字与红行签名都写进了测试文件的块注释,不用重跑就能读到。

✅ 解除停放:content/docs/references/** 整体重生成(已完成,2026-08-07)

停放期间挂着的那半件事,本轮补齐。序列严格按解除条件执行:

  1. git merge origin/main —— 无冲突(本分支只碰 format-type.ts + 其单测 + changeset),merge 单独 commit 在先(fix(spec): --update-base 只许锚点前移,MERGE 态与倒退一律拒绝 (#5370) #5851 MERGE-state 守卫 / os-regen 驱动指示的 gen:schema 在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 anchor 回归)。
  2. pnpm --filter @objectstack/spec gen:schema —— 本座位未被权限系统拒绝,正常跑完(✅ Successfully generated 1622 schemas)。它同时物化了 gitignore 掉的 json-schema/ 树(1611 个文件)与分片产物。
  3. pnpm --filter @objectstack/spec gen:docs —— ✅ Generated 232 files
  4. 生成页 content/docs/references/** 作为独立 commit 提交(⛔ 全程没有对生成页做任何文本合并 / 手改)。

重生成 diff 的量与形(逐条实测):

实测
变更页数 30 个 reference 页
变更行数 117 行(117 insertions / 117 deletions,全是逐行替换;无增删文件)
逐键墓碑表行 101 行,类型格 anynever;其中带 [REMOVED] 处方的 = 101,即处方无一丢失(反向核对:never 行里不带 [REMOVED] 的 = 0)
内联摘要格 16 行,墓碑在计入 INLINE_KEY_LIMIT 前被剔除
落在 content/docs/references/ 之外的改动 0
三个热点分片产物(authorable-surface/json-schema.manifest/api-surface/) byte-identical —— gen:schema 跑完后 git status 对这三个目录全空,post-#6069 分片写入器无一片 CHANGED

派单里最要紧的那条「分片产物必须干净」是独立核过的:gen:schema 之后整棵树 git status --porcelain 返回 0 行,gen:docs 之后也只有 30 个 references 页。#6069 的分片与本单零交集,这点从实测面而不只是推理面成立。

归因探针(先声明预期方向,再跑): 30 页里有多少是本单渲染器改动、有多少是 main 上本来就漂的?预期 —— 若 origin/main 的生成页本就同步,则把 format-type.ts 换回 origin/main 版本再跑一次 gen:docs,content/docs/references/** 应当逐字节回到 origin/main(diff 全空)。实测正是如此:

$ git checkout origin/main -- packages/spec/scripts/lib/format-type.ts
$ pnpm --filter @objectstack/spec gen:docs      # ✅ Generated 232 files
$ git diff --stat origin/main -- content/docs/references/
(空)

main 侧零漂移,30 页 / 117 行 100% 归因于本单的渲染器改动。探针跑完已把两边都还原回本分支状态(git status 干净)。

兑现 PR 上文那张渲染变化表(从真实 diff 里摘的):

- | **typography** | `{ fontFamily?: object; fontSize?: any; fontWeight?: any; lineHeight?: any; … }` | optional | Typography settings |
+ | **typography** | `{ fontFamily?: object }` | optional | Typography settings |
- | **fontFamily** | `{ base?: string; heading?: any; mono?: any }` | optional |  |
+ | **fontFamily** | `{ base?: string }` | optional |  |
- | **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization'; type?: any; … }[]` | optional | Database performance indexes |
+ | **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes |

逐键表行的处方原样保留,例:

+ | **tools** | `never` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 (#3894) — use `skills`. … |

预期红(已清零 ✅)

检查 原签名 现状
TypeScript Type Check 作业内的 Check generated reference docs are in sync with the spec(pnpm --filter @objectstack/spec check:docs,.github/workflows/lint.yml:616) content/docs/references/** 与新渲染器的产出有差异 已清零 —— 生成页已随本轮 commit 补齐。本地实测:✅ 232 generated files in sync with packages/spec,退出码 0

停放期间这条红的成因(存档):CI 里 check:authorable-surface 会先生成 json-schema 树,check:docs 再拿它重渲染并逐字节比对;渲染器变了而生成页没跟着变,必红。初版正文把它挂在 ESLint 作业下是错的,check:docs 落在 typecheck: job 里(lint.yml 第 616 行),已于停放期间自我更正。

至此本 PR 不再有任何已声明的预期红。 除下节平台签名外的任何红都应当当成真问题处理。

本轮解除停放的完整验证(全部前台阻塞跑完,真实输出)

命令 结果
pnpm --filter @objectstack/spec check:docs 232 generated files in sync with packages/spec(退出码 0)—— 预期红清零点
pnpm --filter @objectstack/spec check:generated All 10 generated artifacts are up to date.(10/10 全绿,含 check:api-surfacecheck:docs)
npx vitest run scripts/format-type.test.ts Test Files 1 passed (1) / Tests 27 passed (27)
pnpm --filter @objectstack/spec test(merge 后全量复跑) Test Files 326 passed (326) / Tests 8340 passed (8340)(较停放前 325/8316 增加的 1 file / 24 tests 来自 merge 进来的 #6069 sharded-artifacts.test.ts)
pnpm --filter @objectstack/spec typecheck ✅ 绿;check:test-typecheck OK,debt 账本 79 file / 691 error 未增未减
npx eslint 两个改动源文件 ✅ 退出码 0
node scripts/check-nul-bytes.mjs ✅ OK(5835 个 tracked 文本文件,无裸控制字节);另对 33 个改动文件扩面自扫 [\x00-\x08\x0b\x0c\x0e-\x1f\x7f],无命中

一个值得记下的过程坑(非缺陷):新 worktree 里首次跑 check:generated,api-surface/ 报 stale;实际是 gen:api-surfacepackages/spec/dist/index.d.ts,而新 worktree 从未 build 过(Could not resolve module symbol for . … Is the package built?)。这正是 AGENTS.md §9 的陈旧产物陷阱。pnpm --filter @objectstack/spec build 之后复跑即 10/10 全绿,不是 main 上的真漂移,也未产生任何 api-surface 改动

平台故障(停放期间的红,非本 PR;存档)

GitHub Actions 自 2026-08-06 15:42Z 起平台级故障(runner 引导 503)。停放期间本 PR 上除 check:docs 外的全部红/取消都是这一签名,已由 PM 验签:

  • Failed to resolve action download info. / Service Unavailable(annotation 逐字相同):Auto LabelBuild CoreCheck PR SizeConsole Pin GateFlag docs affected by code changes
  • 作业零步骤即被取消(同一故障,连步骤都没起来):ESLintConsole Pin FreshnessSpec property livenessfilter
  • 绿:Check ChangesetNo other open PR may claim the same issueDogfood Regression Gate (1/3)(3/3)Vercel Preview Comments

Auto Label 被这场风暴打掉,导致本 PR 停放期间零标签;本轮 push 会重新触发。

停放(已解除 ✅)

关于 changeset

写了真 changeset(.changeset/docs-gen-retired-key-never.md,@objectstack/spec patch),没有走 skip-changeset。理由:本 PR 改的是读者可见的产物。reference 页是升级作者(尤其 AI 作者,ADR-0033)的主要输入面,heading?: any → 键从页面上消失、type?: anytype 从摘要里消失,是升级者会直接撞上的表述变化;而 skip-changeset 的适用面是「test-only / workflow-only / .claude/-only,什么都不发布」。本 PR 不属于那一类。改动虽然只落在 scripts/,但它是生成器,产物是发布面的一部分 —— 本轮重生成把这 30 个页面实际提交进来,这条判断被进一步坐实。Check Changeset 已绿。

#5729 的定价(派单要求的回答)

答:变简单(easier),而且是同文件同函数的相邻分支。

字面量加引号发生在 format-type.ts,不在 build-docs.ts:

if (prop.enum) {
  return `Enum<${prop.enum.map((e: any) => `'${e}'`).join(' | ')}>`;
}

if (prop.const !== undefined) {
  return `'${prop.const}'`;
}

两处都无条件套单引号,不看 typeof e / typeof prop.const,所以 z.literal(2) 印成 '2'、数值字面量联合印成字符串型 —— 这就是 #5729build-docs.ts 里唯一用到 const 的地方是 union options 段的 **Type:** \${variant.properties.type.const}``(反引号,不加单引号),不参与本类缺陷。

#5729 的具体影响:

⛔ 本 PR 不顺手修 #5729 —— 它是独立单,且正文明确要求墓碑这一单不搭车。

`retiredKey()` 是 `z.never()`,`z.toJSONSchema` 把它发成 `{ "not": {} }` ——
没有 `type`、没有 `$ref`、没有 `enum`。`formatType()` 没有对应分支,于是全仓
约 28 处墓碑一路落到函数末尾的 `return prop.type || 'any'`,reference 页把一个
**已删除**的键印成了 **`any`**。

这是退役能得到的最差渲染。这些页面是升级作者(很常是 AI 作者,ADR-0033)的主要
输入,`heading?: any` 读起来不是「这个键被删了」,而是「这个槽存在,而且不校验」
—— 比它替换掉的 `heading?: string` **更**鼓励去写。写了之后 parse 会带着
`[REMOVED]` 处方硬拒,但那已经是在一份错元数据产出之后了。

两处改动,都落在 `scripts/lib/format-type.ts`:

- `{ not: {} }` 现在渲染成 `never`。这既是准确的 TypeScript(该键的 `z.input`
  类型本就是 `never`),也不像 `any` 那样需要旁边的散文来兜底。
- 内联 shape 摘要在计入 `INLINE_KEY_LIMIT` **之前**先剔除墓碑。摘要格只印前 4 个
  声明键的 `k?: type`,根本没有描述列,所以嵌套的墓碑无处安放处方:
  `ui/theme.mdx` 宣传着 `{ base?: string; heading?: any; mono?: any }`,而这两条
  处方在整页**任何地方都不出现**。退役键已不再是可写面,因此不再占用四个槽位之
  一,也不再把作者**必须**写的键挤到 `…` 后面。已知的「把墓碑挪到 shape 底部」
  规避办法覆盖不了这一类:#5248 把 `IndexSchema` 退役到只剩 3 个活键,在上限为 4
  时第一个墓碑**在数学上**必然进入摘要。

逐键表行不受影响,描述列仍然完整携带 `[REMOVED]` 处方,只是类型格从 `any`
改成了 `never`。

反向验证(实测,两半分别做,方向都是常规的「还原缺陷 → 新钉子变红」):
注释掉 `isNeverNode` 前置返回 → 3 failed | 24 passed,三条红全部报
`expected 'any' to be 'never'`;还原该返回、把摘要改回不过滤的
`Object.keys(prop.properties)` → 4 failed | 23 passed,四条红报出「只做第一半」
会发布的中间态(`{ base?: string; heading?: never; mono?: never }`)。

⚠️ `content/docs/references/**` 的整体重生成不在本 commit 内:该步需要先
`gen:schema` 物化 gitignore 掉的 `packages/spec/json-schema/` 树,而本座位的
权限系统拒绝执行 `gen:schema`。详见 PR 正文。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
@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 7, 2026 12:52am

Request Review

claude added 2 commits August 7, 2026 00:33
…stone renderer (#5606)

Generated by `pnpm --filter @objectstack/spec gen:schema && gen:docs`.
Do not hand-edit — regenerate instead.

30 reference pages, 117 lines:
- 101 per-key tombstone rows: type cell `any` -> `never`, [REMOVED]
  prescriptions unchanged.
- 16 inline summary cells: tombstones dropped before INLINE_KEY_LIMIT,
  e.g. Theme.typography.fontFamily `{ base?: string; heading?: any;
  mono?: any }` -> `{ base?: string }`, ObjectSchema.indexes drops
  `type?: any` and its trailing ellipsis.

The three sharded artifact dirs (authorable-surface/, json-schema.manifest/,
api-surface/) are byte-identical after gen:schema — this change does not
reach them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
@github-actions github-actions Bot added the size/l label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

CI 红分诊(PM 座位,会话 session_014wsZeReNTqiceBfLb5Pyf5,01:0xZ):解除停放推送(1c8c497)后 Test Core (2/3) 唯一失败 = datasource-pool-support.test.ts「sqlite WITHOUT a pool still builds」——在案 flaky #6044 第 4 例(与本 PR 渲染器改动零交集,同 shard 其余 12 文件全过)。按台账原样重投(run 收尾后执行),⛔ 不改代码。预期红(check:docs)是否已随重生成清零以本轮全量 CI 结论为准。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 01:14
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 6e82972 Aug 7, 2026
38 of 40 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5606-format-type-never-tombstone branch August 7, 2026 01:35
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

Development

Successfully merging this pull request may close these issues.

[docs-gen] 生成的 reference 把 retiredKey() 墓碑渲染成 any —— 嵌套两层时连 [REMOVED] 处方都没有,退役键读起来像自由槽

2 participants