Skip to content

fix(spec): 参考页开篇只认「不属于任何符号」的模块级 doc block (#5059) - #6134

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-5059-reference-page-descriptions
Aug 7, 2026
Merged

fix(spec): 参考页开篇只认「不属于任何符号」的模块级 doc block (#5059)#6134
os-zhuang merged 4 commits into
mainfrom
claude/issue-5059-reference-page-descriptions

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5059

前提复核(对 origin/main)

issue 正文与 08-06 08:31Z 实测评论的前提全部成立,在 #6069 分片与 #6096 之后依然如此。6 张受害页在合并基线上逐字复现:

页面 开篇首句(修改前)
data/mapping Shared history for this file (#4001).
system/translation Shared history sentence for every shape in this file (#4001).
api/contract Machine-readable semantic code (ADR-0112): a StandardErrorCode member or …
api/protocol Response for GET /api/v1/automation/actions (ADR-0018).
api/realtime Transport Protocol Enum
kernel/plugin Shared Plugin Types

按派发指示采用实测评论的方向:改生成器的取块规则,不动 packages/spec/src/**/*.zod.ts。本 PR 的 diff 一个 zod 文件都没碰。

修法:按 TSDoc 自己的规则取块

getFileDescription() 取整个文件里第一个 doc block 原样发布。这不是一条关于「描述」的规则,而是一条关于顺序的规则 —— 哪个声明碰巧排在文件最前面,它的注释就被搬上公开页。check:docs 结构上看不见:它比对生成物与源码是否一致,而生成物确实忠实复制了那个错误的块。

新规则就是把 TSDoc 的语义读回来:一个 doc block 属于它紧邻其后的那个声明 —— 那正是编辑器 hover 该符号时显示的文本。所以模块描述必须同时满足:

  1. 顶层 —— 定界符在第 0 列。缩进在声明体内的块是某个属性的文档(api/contract 发布的就是 ApiErrorSchema.code 的注释),永远不可能是文件头;
  2. 位于首个声明之前 —— import 与 re-export 不引入符号,不关闭头部区;第一个 const / export const / … 关闭;
  3. 不紧跟声明 —— 「紧跟」只跨空行。没有人会用 // ═══ 横幅把 JSDoc 和它文档的符号隔开,所以横幅(或另一个 doc block、或一条 import)出现在中间,就说明前面那块写的是模块而不是那个声明。

取不到就不输出描述。宁可缺,不要错:一段缺失是读者看得见的空白,一段自信渲染的内部注释是一张说谎的页面。

规则本身即门禁。issue 正文提议的首句模式检查(#\d{3,} / Shared history / Until #)只能覆盖 history 常量那一个子类、且只能在发布之后发现 —— 6 张受害页里它只盖得住 2 张。取块规则从结构上取不到符号注释,整个类别不可能再发生。

代码落在 packages/spec/scripts/lib/file-description.ts(从 build-docs.ts 抽出,沿用 format-type.ts #4912escape-mdx.ts #5452 的既有路子),pin 套件 packages/spec/scripts/file-description.test.ts,14 条用例含一条对真实 packages/spec/src 全树的语料门。

描述发生变化的全部 20 张页面(逐张归因)

纯删除:284 行删除 / 0 行新增,20 张页面,除此之外 content/docs/references/** 一字未动。

A. #4001 history 常量类(2 张) —— 块文档的是文件里私有的 const *_HISTORY:

页面 被误发布的块所文档的符号
data/mapping const MAPPING_HISTORY(L19)
system/translation const TRANSLATION_HISTORY(L24);另外 export const LocaleSchema(L12)已先关闭头部区

B. 靠前的内部声明注释类(18 张) —— 与 #4001 无关,只是某个符号的 JSDoc 排在了最前:

页面 块所文档的符号
api/contract ApiErrorSchema.code(缩进在对象字面量内;export const ApiErrorSchema L12 已关闭头部区)
api/protocol export const AutomationActionsResponseSchema(L76;export const AutomationTriggerRequestSchema L58 已关闭头部区)
api/realtime export const TransportProtocol(L15)
kernel/plugin export const CORE_PLUGIN_TYPES(L89;export const PluginContextSchema L8 已关闭头部区)
ai/solution-blueprint const SNAKE_CASE(L24)
ai/tool const TOOL_RETIRED_KEY_GUIDANCE(L35)
api/error-code-ledger export const ERROR_CODE_LEDGER(L38)
api/router export const RouteCategory(L14)
automation/approval export const ApproverType(L30)
cloud/template-manifest export const TemplateManifestSchema(L12)
data/driver-mysql const MYSQL_CONFIG_KEYS(L30)
data/driver-postgres const POSTGRES_CONFIG_KEYS(L28)
data/driver-sqlite const SQLITE_CONFIG_KEYS(L30)
kernel/manifest export const PluginPermissionsSchema(L33)
shared/enums export const SortDirectionEnum(L21,单行 JSDoc)
system/doc export const DocSchema(L30)
system/notification export const NotificationChannelSchema(L19)
ui/responsive export const BreakpointName(L73)

B 类里有若干块(cloud/template-manifest、三个 driver、system/docapi/error-code-ledger)读起来像模块简介 —— 但它们被紧贴着写在模块首个 schema 之上,TSDoc 就把它们判给那个符号,IDE hover 该符号显示的正是这段话。把同一段文字再当作「模块描述」发布,是生成器给一段已经有归属的文本发明了第二重含义。要把开篇找回来,作者只需在 zod 文件里补一个不文档任何符号的块(zod 侧改动,不在本座位边界内,故本 PR 不做)。

没有任何一张页面丢失了真正的文件头

Source: 行的 200 张参考页里,178 张描述逐字节不变;GAINED 与新增行数都是 0(本次不给任何页面新增描述),原本就没有描述的 2 张(data/hook-bodyqa/testing)保持不变。

规则在设计阶段被反复收窄,正是为了这一条。两个更粗的候选各自被实测否掉:

  • 「块必须排在所有 import 之前」会误删 api/websocket 的真文件头(它写在 import 与一条 re-export 之后),并把 178 张页面砍到 20 张;
  • 「块不得被 import 前后夹住」会误删 api/analytics / ai/conversation 一类的真文件头 —— scripts/lazify-schemas.tsimport { lazySchema } … 插在「注释+import 前导段」之后,而那个前导段会吞掉 doc block,于是真文件头被自动改写工具挪进了 import 列表中间。

最终规则对 api/websocketsystem/settings-manifestapi/analyticssystem/migrationui/sharing 等真文件头全部保留,并有 pin 用例逐条守住。

反向验证(方向:普通向,事前预测,实测吻合)

findModuleDocBlock 换回 #5059 之前的一行取块(source.match(/\/\*\*([\s\S]*?)\*\//)),预测「四条 rejects… + 语料门 + 六页门转红,三条 keeps a real module header 保持绿(两种取法在真文件头上本就一致)」。实测:

❯ scripts/file-description.test.ts (14 tests | 7 failed)
  × rejects a block attached to a private helper const (#4001 history constants)
  × rejects a block attached to an exported schema (`Transport Protocol Enum`)
  × rejects a block nested inside a declaration body (a property doc)
  × rejects a block that arrives after the first declaration, however good it is
  × renders nothing when the module has no description
  × never selects a block that is immediately followed by a declaration
  × drops the six openings the issue measured, and keeps the module headers beside them

三条 keeps 用例全绿 —— 这正是缺陷能存活的原因:在所有人想得到去看的输入上,新旧两种取法给出同一个答案。

验证

pnpm --filter @objectstack/spec test          → 328 files / 8385 tests passed
pnpm --filter @objectstack/spec typecheck     → tsc --noEmit + check:test-typecheck OK
pnpm --filter @objectstack/spec check:generated → ✓ All 10 generated artifacts are up to date
pnpm lint                                      → clean
node scripts/check-nul-bytes.mjs               → OK(5858 files,无裸控制字节)

ESLint job 里那批家族门禁本地逐条跑过,24 条全 OK(含 check:slot-lookup —— #6100 的止血 #6104 已在本分支基线里)。合并 origin/main(11 commits)后重跑 build + check:generated + 全量 spec 测试 + lint,均绿;合并未产生任何需要重生成的产物。

Changeset

@objectstack/spec: patch,具名。packages/spec 的 npm 导出面一字未变,但重生成的参考页是读者今天就能访问的已发布内容 —— 沿用同族先例 #5606 / PR #6058(同为 docs-gen 修复、同为 npm 面不变而参考页变,带具名 patch changeset)。body 里逐条列出了 20 张页面与「178 张未动」这条对账。

并行/邻接说明


Generated by Claude Code

claude added 4 commits August 7, 2026 02:41
`getFileDescription()` 取整个文件里第一个 `/** */` 块原样做参考页开篇。
这不是一条关于「描述」的规则,而是一条关于「顺序」的规则:哪个声明碰巧排
在文件最前面,它的注释就被搬上公开文档页。#3746 陷阱 1 早就写明这一点,
而它已经落到 main 上两次,`check:docs` 全程绿 —— 那道门比对的是生成物与
源码是否一致,而生成物确实忠实复制了那个错误的块。

改为按 TSDoc 自己的规则选块(`lib/file-description.ts`):一个 doc block
属于它紧邻其后的那个声明 —— 这正是编辑器 hover 该符号时显示的文本。所以
只有同时满足三条的块才是模块描述:顶层(不在声明体内缩进)、位于首个声明
之前(import / re-export 不算声明)、且其后不紧跟声明。取不到就不输出描述
—— 宁可缺,不要错。

规则本身即门禁:issue 正文提议的「首句模式」检查只能覆盖 history 常量那
一个子类,且只能在发布之后发现;实测的 6 张受害页里有 4 张附近根本没有
history 常量。选块规则从结构上取不到符号注释,整个类别就不可能再发生。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
纯生成物提交,`gen:schema && gen:docs` 的输出,284 行删除 / 0 行新增。
每一页移除的都是某个符号的 JSDoc 被当作模块描述发布的那一段;没有任何一页
丢失真正的模块级文件头(178 张带 Source 行的页面描述一字未动)。

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

vercel Bot commented Aug 7, 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 3:10am

Request Review

@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
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 03:24
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 52760bf Aug 7, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5059-reference-page-descriptions branch August 7, 2026 03:38
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.

两张公开参考页的正文被 #4001 的内部注释顶替(#3746 陷阱 1 已实际发生两次)

2 participants