fix(spec): key the reference-docs index by ${category}/${name} — 9 ghost pages retired, 26 schemas re-homed (#4696) - #5831
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
baozhoutao
marked this pull request as ready for review
August 6, 2026 07:29
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: []。逐项枚举后的实际存量:ServiceStatus:api/discovery.zod.ts的 enum +system/core-services.zod.ts的 objectapi/{BasePresence,PresenceStatus,RealtimeRecordAction},声明在realtime-shared.zod.ts第 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:617re-export 12 个 package RPC 信封(声明在kernel/package-registry.zod.ts)api/router.zod.ts:8export { HttpMethod }(声明在shared/http.zod.ts)ui/view.zod.ts:37export { HttpMethodSchema, HttpRequestSchema }integration/connector.zod.ts:821re-export 5 个ConnectorInstance*Auth(声明在shared/connector-auth.zod.ts)system/job.zod.ts:82/automation/control-flow.zod.ts:288re-exportRetryPolicySchema(声明在shared/retry-policy.zod.ts)system/metadata-persistence.zod.ts:192,350re-exportMetadataFormatSchema与 metadata-loader 两个 schema二、改了什么
索引 key 从
name收敛为${category}/${name}(与build-schemas.ts的 def key、与json-schema/{category}/{Name}.json完全同口径)。新模块packages/spec/scripts/lib/schema-index.ts承担三件事:build-schemas.ts看到的东西:声明与值 re-export 都算数;纯类型导出(export type { X }、export { type X })不算 —— 它们不产出z.ZodType,永远不会有 JSON Schema,就不该占页。api/realtime-shared.zod.ts那 3 例就是这个形状);没有声明、只有唯一一条 re-export 时归该 re-export 文件。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}'示例本来就是真的。api/core-services.mdxapi/discovery.mdxServiceStatus(唯一一例真·同名冲突)api/http.mdxapi/router.mdxHttpMethodapi/package-registry.mdxapi/protocol.mdxautomation/retry-policy.mdxautomation/control-flow.mdxRetryPolicyintegration/connector-auth.mdxintegration/connector.mdxConnectorInstance*Authsystem/metadata-loader.mdxsystem/metadata-persistence.mdxMetadataFallbackStrategy、MetadataManagerConfigsystem/metadata-types.mdxsystem/metadata-persistence.mdxMetadataFormatsystem/retry-policy.mdxsystem/job.mdxRetryPolicyui/http.mdxui/view.mdxHttpMethod、HttpRequest5 个
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.mdx、kernel/package-registry.mdx、shared/http.mdx、shared/metadata-types.mdx、system/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.ts和shared/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.ts、api/core-services.zod.ts、integration/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:
name10 failed / 9 passedexportedValueNames只认声明、不认 re-export7 failed / 12 passed3 failed / 16 passed--check红命令与结果:
pnpm --filter @objectstack/spec check:docs→EXIT=0,231 generated files in sync(改前基线 240,差值即删掉的 9 页;生成器自身达到不动点)pnpm --filter @objectstack/spec test→Test Files 320 passed / Tests 8175 passedpnpm --filter @objectstack/spec typecheck→EXIT=0(tsc --noEmit+check:test-typecheck均通过)node scripts/check-nul-bytes.mjs→ OK(5677 个文件,无裸控制字节);改动文件另做了grep -naP控制字符自扫,cleannpx eslint(三个改动脚本)→ 0补充一步:
packages/spec/scripts/**不在任何 tsconfig 的 include 里(已立案 #5475),所以新文件不被pnpm typecheck覆盖。为免漏检,对两个新文件单独跑了一次tsc --noEmit --ignoreConfig --strict,退出码 0。七、纪律性说明
packages/spec/src/**一个字没动(PM 令的红线)。packages/spec/authorable-surface.base.json不在本 PR 里:gen:schema每次都会重写它(check:authorable-surface在--check模式下也会重写authorable-surface.base.json—— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358,修复 PR fix(spec): 重锚 authorable-surface.base.json 改为显式动作 —— 构建不再顺手推进删除门的锚点 #5807 尚未合入),已逐次git checkout --还原并在git status上逐文件核对。content/docs/references/**是整体重生成的(gen:docs),删除项已逐条列在第三节并注明成因。八、越界发现(不在本 PR 修,另开 issue)
packages/spec/src/shared/http.zod.ts在同一个文件里声明了两个不同的 enum,而它们发布成同一个 JSON Schema 名HttpMethod:export const HttpMethod(7 个方法,含 HEAD/OPTIONS,api/*的线上契约)与export const HttpMethodSchema(5 个方法的 UI 子集,类型别名叫HttpMethodType)。schemaNameFromExportKey剥掉Schema后缀后两者同名,build-schemas.ts后写覆盖前写,于是json-schema/shared/HttpMethod.json与shared/http.mdx#httpmethod只描述了 5 个值 —— 一个照着参考页写HEAD路由的作者会以为它非法。check:dual-source-exports看不见:它比对的是导出名(HttpMethodvsHttpMethodSchema不同名),而碰撞发生在剥后缀之后的 schema 名上,与 #4592 同族。本 PR 让它更显眼(7 值那份现在落在api/router.mdx、5 值那份落在ui/view.mdx,两个页面上出现两个不同的「HttpMethod」),但成因在src/**,属本单红线之外。另记一条不构成缺陷、但本 PR 未改变的既有降级:
security/misc.mdx上的CapabilityDeclaration/TenancyPosture声明在security/capabilities.ts、security/tenancy-posture.ts(不是.zod.ts),扫描器看不到,仍落在misc兜底桶里 —— 这是 #4410 就设计好的诚实降级(页面不打印Source:),归页结果改动前后一致。