Skip to content

fix(spec): key the reference-docs index by ${category}/${name} — 9 ghost pages retired, 26 schemas re-homed (#4696) - #5831

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-4696-docs-category-index
Aug 6, 2026
Merged

fix(spec): key the reference-docs index by ${category}/${name} — 9 ghost pages retired, 26 schemas re-homed (#4696)#5831
baozhoutao merged 1 commit into
mainfrom
claude/issue-4696-docs-category-index

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #4696

结论先说

前提成立,且在当前 origin/main 上有活体证据。但立案时给出的成因只占 26 例中的 1 例:真正的大头是「re-export 索引根本看不见」。两者是同一个缺陷的两面(索引 key 不带 category),修在同一处。

一、真实存量(PM 前提要求,按当前 origin/main 实测)

立案时点名的候选(ConflictResolution / DataSyncConfig / FieldMapping / EnvironmentArtifact / PackageDependency / TenantPlan已全部清零 —— #4535 双源清账把 dual-source-exports.baseline.json 打到了 entries: []。逐项枚举后的实际存量:

类别 数量 明细
跨 category 同名的两个独立声明 1 ServiceStatusapi/discovery.zod.ts 的 enum + system/core-services.zod.ts 的 object
同 category 内两个声明同名(旧版会覆盖) 0 ——
同 category 内「声明 + 若干 re-export」 3 api/{BasePresence,PresenceStatus,RealtimeRecordAction},声明在 realtime-shared.zod.ts
re-export 造成的错归页 25 见下表
合计错归页 schema 26 ——

第 4 行是本单真正的体量来源,而立案时没有察觉:scanCategories() 只匹配 export const X,所以一个名字如果是通过 export { XSchema } from '../other/y.zod'(或 import 后再 export { XSchema })进入 @objectstack/spec/{某 category} 的,它在自己 category 下压根没有索引条目,于是必然掉进裸名字全局表 —— 也就是拿声明方 category 的 slug 去给消费方 category 排页。

25 例的来源全部是显式的 re-export 语句,一条不漏:

  • api/protocol.zod.ts:617 re-export 12 个 package RPC 信封(声明在 kernel/package-registry.zod.ts
  • api/router.zod.ts:8 export { HttpMethod }(声明在 shared/http.zod.ts
  • ui/view.zod.ts:37 export { HttpMethodSchema, HttpRequestSchema }
  • integration/connector.zod.ts:821 re-export 5 个 ConnectorInstance*Auth(声明在 shared/connector-auth.zod.ts
  • system/job.zod.ts:82 / automation/control-flow.zod.ts:288 re-export RetryPolicySchema(声明在 shared/retry-policy.zod.ts
  • system/metadata-persistence.zod.ts:192,350 re-export MetadataFormatSchema 与 metadata-loader 两个 schema

二、改了什么

索引 key 从 name 收敛为 ${category}/${name}(与 build-schemas.ts 的 def key、与 json-schema/{category}/{Name}.json 完全同口径)。新模块 packages/spec/scripts/lib/schema-index.ts 承担三件事:

  1. 索引的输入改成 category 的「运行时导出面」,也就是 build-schemas.ts 看到的东西:声明与 re-export 都算数;纯类型导出(export type { X }export { type X })不算 —— 它们不产出 z.ZodType,永远不会有 JSON Schema,就不该占页。
  2. 归页规则:本 category 内声明优先于任意条数的 re-export(api/realtime-shared.zod.ts 那 3 例就是这个形状);没有声明、只有唯一一条 re-export 时归该 re-export 文件。
  3. 同 category 内无法定夺 = 硬报错,绝不覆盖。两种形状都报:两个文件都声明spec 双源清账 C9:RateLimitConfig / RateLimitConfigSchema(./integration ≠ ./shared)—— 2 条 #4684 RateLimitConfig 的形状搬进单 category)、或没有声明而两条以上 re-export。报错信息点名两个文件与各自的导出方式,并给出改法。当前 main 上此错误为 0 条。

交叉引用链接($ref 渲染)同口径:先看读者自己所在的 category(该 entry point 确实导出这个名字时),否则退到唯一的声明 category;两个 category 都声明时不出链接,宁可渲染成纯文本,也不给一个自信的错链接。

顺带:export const 的匹配加了行首锚定。export 只在模块顶层合法,不锚定就会把 TSDoc 代码示例里的 export const 当成真导出 —— 旧索引里 leadSeed / SETUP_APP / nightlySync / reportForm 四条就是这么来的,描述的是不存在的导出。另外删掉了 scanCategories() 里只写不读的 zodFileCounts

三、页面变动(PM 要求逐条列出)

9 个页面被删除,原因一律相同:这些 slug 在自己的 category 下没有对应的 .zod.ts 文件(所以它们本来就不打印 Source: 行),是裸名字索引借用别的 category 的 slug 凭空造出来的。它们的 section 迁到了真正导出这些名字的那个文件的页面上 —— 那个页面的 Source: 行与 import … from '@objectstack/spec/{category}' 示例本来就是真的。

删除的页面 section 迁往 迁移的 schema
api/core-services.mdx api/discovery.mdx ServiceStatus(唯一一例真·同名冲突)
api/http.mdx api/router.mdx HttpMethod
api/package-registry.mdx api/protocol.mdx 12 个 package RPC 信封
automation/retry-policy.mdx automation/control-flow.mdx RetryPolicy
integration/connector-auth.mdx integration/connector.mdx 5 个 ConnectorInstance*Auth
system/metadata-loader.mdx system/metadata-persistence.mdx MetadataFallbackStrategyMetadataManagerConfig
system/metadata-types.mdx system/metadata-persistence.mdx MetadataFormat
system/retry-policy.mdx system/job.mdx RetryPolicy
ui/http.mdx ui/view.mdx HttpMethodHttpRequest

5 个 meta.json 变动(只删条目,不新增):api(-3)、automation(-1)、integration(-1)、system(-3)、ui(-1)。

SECTION_GROUPS 同步删掉 6 个名字(api 的 http / core-services / package-registry、integration 的 connector-auth、system 的 metadata-loader、ui 的 http)。buildCategoryPages 会按「本次实际产出的页面」过滤,所以留着它们输出完全一样 —— 正因为「留着也无害」,才要像 #4988 那样显式删掉,而不是把已删页面的名字留在配置里误导下一个读者。已实测:删前删后 meta.json 逐字节一致。

没有任何 inbound 链接因此失效:改动前对这 9 个页面的站内链接数为 0(旧的 schemaHref 走全局表,永远解析到声明方 category,从来指不到这些幽灵页);改动后 content/docs/references/** 内也没有指向它们的链接残留。

没有页面被误删kernel/metadata-loader.mdxkernel/package-registry.mdxshared/http.mdxshared/metadata-types.mdxsystem/core-services.mdx 均原样保留 —— 它们才是有真实源文件的那一侧。

一个「没有变化」也值得记一笔cloud/Sha256Digest#4740 维护者裁定的 route A′ re-export)归页不变,仍在 cloud/environment-artifact.mdx。旧代码是碰巧对的(声明方 system/environment-artifact.zod.ts 与 cloud 的 re-export 文件恰好同名同 slug)。如果只做「本 category 的 export const 才算数」的朴素改法,这一例会掉进 cloud/misc.mdx —— 把维护者已经裁过的用例改坏。这正是本 PR 必须把 re-export 纳入索引、而不是只换 key 的原因。

四、为什么不需要 needs_decision

PM 令写明:「若出现单一声明须由两个 category 共享、归属真歧义的实例,停下来报 needs_decision」。逐条查过 25 例 re-export,没有一例是真歧义:每一个名字在消费方 category 里都有一个显式的、唯一的导出语句,那个文件就是归属方。shared/connector-auth.zod.tsshared/retry-policy.zod.ts 甚至根本不在 src/shared/index.ts 的导出里(@objectstack/spec/shared 不发布它们,json-schema/shared/ 下也没有对应文件),所以它们只可能归在消费方那一侧 —— 旧代码把它们排到 integration/connector-auth.mdx / automation/retry-policy.mdx / system/retry-policy.mdx 这种「名字来自另一个 category、且那边根本没发布」的页面上,是纯粹的错误,不是一种可选 IA。

五、必答项:本单对 #4759 的影响 = 更简单(既非不必要,也非更难)

#4759 的方向 1 是「把根 content/docs/references/index.mdx 的表纳入生成」。该文件 421 行,主体是每个 category 一张 文件 | Schema | 用途 表 —— 而这张表的前两列,正是本 PR 刚刚建立并校正过的映射:

  • schemaIndex.pageFor(category, name) 给出「某 category 下每个已发布 schema 归哪个文件」;
  • zodFileSourceRel${category}/${slug} → 真实 rel 路径)把 slug 还原成源文件路径。

关键在于顺序:如果先做 #4759、后做本单,生成出来的根索引会把 api/http.zod.tsapi/core-services.zod.tsintegration/connector-auth.zod.ts 这些不存在的文件写进根页面 —— 用一个错的索引去生成一张「文件→Schema」表,等于把错误从侧边栏放大到首页。本单先落地,#4759 的方向 1 就是在一个已经为真的映射上做渲染。

不是「不必要」:根索引仍然是 preserve-not-regenerate,计数仍然是烂的(头部写 133,导航表合计 169,Data 一行自称 19 而分节标题写 18),本 PR 一个字都没动它。也不是「更难」:根索引引用的是 .zod.ts 文件名与 schema const,不是页面 slug,本 PR 删掉的 9 个页面在其中一处都没出现metadata-loader.zod.ts / package-registry.zod.ts 两行在 kernel 段、connector-auth.zod.ts 一行在 shared 段,指的都是真实文件),所以本 PR 没有给它增加任何新的烂账。顺带为 #4759 记一条已有的烂账:connector-auth.zod.ts 那行所在的 Shared 段,其实没有 shared/connector-auth 参考页(该文件不在 src/shared/index.ts 导出里)。

六、测试与反向验证

新增 packages/spec/scripts/schema-index.test.ts(19 例)。抽成 lib/ 模块的理由与 format-type#4912)、schema-name#4592)一致:生成器是有副作用的顶层脚本,不抽出来就只能「跑一遍再 grep 产出的 .mdx」,而这正是裸名字索引能潜伏这么久的原因。

反向验证先定方向再跑,四次全部命中预期的 RED

注入的缺陷 预期 实测
索引 key 退回裸 name 10 failed / 9 passed
exportedValueNames 只认声明、不认 re-export 7 failed / 12 passed
冲突改为「后写覆盖」而不报错 3 failed / 16 passed
生成器归页退回裸名字查表 --check 退出码 1,报 22 个文件漂移 + 7 个新幽灵页

命令与结果:

  • pnpm --filter @objectstack/spec check:docsEXIT=0231 generated files in sync(改前基线 240,差值即删掉的 9 页;生成器自身达到不动点)
  • pnpm --filter @objectstack/spec testTest Files 320 passed / Tests 8175 passed
  • pnpm --filter @objectstack/spec typecheckEXIT=0tsc --noEmitcheck:test-typecheck 均通过)
  • node scripts/check-nul-bytes.mjs → OK(5677 个文件,无裸控制字节);改动文件另做了 grep -naP 控制字符自扫,clean
  • npx eslint(三个改动脚本)→ 0

补充一步:packages/spec/scripts/** 不在任何 tsconfig 的 include 里(已立案 #5475),所以新文件不被 pnpm typecheck 覆盖。为免漏检,对两个新文件单独跑了一次 tsc --noEmit --ignoreConfig --strict,退出码 0。

七、纪律性说明

八、越界发现(不在本 PR 修,另开 issue)

packages/spec/src/shared/http.zod.ts同一个文件里声明了两个不同的 enum,而它们发布成同一个 JSON Schema 名 HttpMethodexport const HttpMethod(7 个方法,含 HEAD/OPTIONS,api/* 的线上契约)与 export const HttpMethodSchema(5 个方法的 UI 子集,类型别名叫 HttpMethodType)。schemaNameFromExportKey 剥掉 Schema 后缀后两者同名,build-schemas.ts 后写覆盖前写,于是 json-schema/shared/HttpMethod.jsonshared/http.mdx#httpmethod 只描述了 5 个值 —— 一个照着参考页写 HEAD 路由的作者会以为它非法。check:dual-source-exports 看不见:它比对的是导出名HttpMethod vs HttpMethodSchema 不同名),而碰撞发生在剥后缀之后的 schema 名上,与 #4592 同族。本 PR 让它更显眼(7 值那份现在落在 api/router.mdx、5 值那份落在 ui/view.mdx,两个页面上出现两个不同的「HttpMethod」),但成因在 src/**,属本单红线之外。

另记一条不构成缺陷、但本 PR 未改变的既有降级:security/misc.mdx 上的 CapabilityDeclaration / TenancyPosture 声明在 security/capabilities.tssecurity/tenancy-posture.ts(不是 .zod.ts),扫描器看不到,仍落在 misc 兜底桶里 —— 这是 #4410 就设计好的诚实降级(页面不打印 Source:),归页结果改动前后一致。

`build-docs.ts` kept schema-name -> category and schema-name -> page as two
GLOBAL maps keyed by the bare schema name, so the same name under two
categories overwrote itself and the winner's page slug placed the loser's
schema. `build-schemas.ts` already publishes `json-schema/<category>/<Name>.json`
— the docs index now uses that same key instead of inventing a second identity
for the same schema.

Measured on origin/main, the defect had two inputs and the second was the
bigger one:

  - one genuine cross-category collision (`ServiceStatus`: an api enum and a
    system object), which put the api enum on `api/core-services.mdx`;
  - 25 RE-EXPORTS the scanner could not see at all, because it matched
    `export const X` only. A name arriving via `export { XSchema } from '…'` or
    a bare `export { XSchema }` had no entry for its own category and fell
    through to the collision path by construction.

The index now records every value export a `.zod.ts` names, declaration and
re-export alike (type-only exports excluded — they publish no `z.ZodType`), a
declaration owns the page over any number of re-exports, and a name two files
in one category both claim is a build ERROR naming both files rather than an
overwrite. Cross-reference links follow the same key: own category first, then
the single declaring category, and no link at all when two categories declare
the name.

Nine pages that named no real file are deleted; their sections moved to the
page of the file that genuinely exports them, whose `Source:` line and import
example were already true.

Extracted to `scripts/lib/schema-index.ts` and pinned by
`scripts/schema-index.test.ts` — same reason `format-type` (#4912) and
`schema-name` (#4592) were extracted: the generator is a side-effecting
top-level script, so page placement was otherwise only assertable by running it
and reading the emitted `.mdx`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
@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 7:08am

Request Review

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels 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). ✅

@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 07:29
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 4615a18 Aug 6, 2026
26 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-4696-docs-category-index branch August 6, 2026 07:41
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build-docs.ts 的 schema→页面索引按「裸名字」全局建表,同名跨 category 的 schema 会被归到错误的页面

2 participants