Skip to content

Commit d4df105

Browse files
fix(spec): 同目录裸源码路径通过 fromCategory 解析 —— 9 处纯文本变成链接或代码段 (#6534)
* fix(spec): 同目录裸源码路径通过 fromCategory 解析,不再以纯文本落地 (#6484) `file-description.ts` 的 bare-path 改写步骤与 `build-docs.ts` 的 `sourcePathToDocsRoute()` 两侧都要求路径里至少有一个目录段,于是作者写 `auth.zod.ts`(与自己同目录)时两侧都匹配不上 —— 既不成链接,也不回退成 代码段,以纯文本落在页面上。 缺的不是正则而是上下文:`build-docs.ts` 按分类遍历、自己知道正在渲染哪个 目录,却只把 `sourcePathToDocsRoute` 一个成员交给渲染方。 - `FileDescriptionContext` 增加 `fromCategory`(必填),由 `build-docs.ts` 传入;裸文件名在渲染方补全为 `<分类>/<文件>` 后再交给解析器。补全放在 调用方一侧是有意的 —— 裸名不是身份(#4696),全分类搜同名文件会撞。 三个引用位置(两种 `{@link}` 与裸散文)共用同一条补全规则。 - 改写正则的目录段变为可选:`(?:[\w-]+\/)?`。用 `?` 而非 `*`,放宽严格可加 —— 嵌套源码仍从最后两段开始匹配,形状不变。 - `sourcePathToDocsRoute()` 补上它文档里一直声明、实现却没做的那一半: 分类为真不等于页面存在。放宽后有 4 个邻居根本不存在,旧实现会各发一条 404 链接;现在按本次运行真正发出的页面清单判断。为此把「schema 归页」的 分组提前到 `PAGES_BY_CATEGORY` 统一算一次,§2 与解析器读同一份,不做第二次枚举。 测试:`file-description.test.ts` 新增 10 条单测 + 1 条语料断言,含反空过守卫 (同一路径带分类段仍成链接)与 `../` 组合方向。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o * chore(docs): regenerate references —— 9 处同目录裸路径不再是纯文本 (#6484) `pnpm --filter @objectstack/spec gen:docs` 的纯输出,未手改一个字节。 231 个产物里 4 个文件、9 行变化,与 issue 点名的 9 处完全重合,零附带: - 5 处成为站内链接:api/realtime、api/websocket、cloud/package-version、 cloud/environment-package、system/encryption - 4 处回退成代码段:auth、audit、compliance、masking —— 这四个邻居本就不存在, 按 #6229「目标没有页面就不发链接」 全语料 216 条站内路由 / 437 个位置逐条核过,无死链。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4afdd3e commit d4df105

8 files changed

Lines changed: 466 additions & 51 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): 参考页里写在同目录的裸源码路径不再以纯文本落地 (#6484)
6+
7+
参考页开篇那段模块描述由 `packages/spec/scripts/lib/file-description.ts` 渲染。它把
8+
JSDoc 里裸写的 `*.zod.ts` 路径改写成站内链接,而这条机制的**两侧**过去都要求路径里
9+
至少有一个目录段:改写正则的 `[\w-]+/` 分组是必需的,`build-docs.ts`
10+
`sourcePathToDocsRoute()` 也要求那个斜杠、并把第一段读作分类名。
11+
12+
于是作者按最自然的方式引用邻居 —— 写 `auth.zod.ts` 而不是 `identity/auth.zod.ts` ——
13+
两侧都匹配不上,既没成链接,也没回退成代码段,以**纯文本**发布在四张参考页上,共 9 处:
14+
`api/realtime-shared``cloud/package``identity/identity``system/security-context`
15+
16+
缺的从来不是正则,而是**上下文**:`build-docs.ts` 按分类遍历,自己知道正在渲染哪个目录,
17+
却只把一个成员交给渲染方。现在 `FileDescriptionContext` 增加 `fromCategory`,由
18+
`build-docs.ts` 传入,裸文件名在渲染方补全成 `<分类>/<文件>` 后再去解析 —— 与
19+
`schemaHrefFrom(fromCategory)` 是同一道缝。补全放在调用方一侧是有意的:裸名不是身份
20+
(#4696),`auth.zod.ts` 在多个分类下都存在,让解析器自己去全分类搜同名文件只会答出
21+
目录遍历最后到达的那一个。
22+
23+
读者可见的变化是这 9 处:**5 处成为可点链接**(`api/realtime``api/websocket`
24+
`cloud/package-version``cloud/environment-package``system/encryption`),**4 处回退成
25+
代码段**(`auth``audit``compliance``masking` —— 这四个邻居本就不存在,按 #6229
26+
的规矩「目标没有页面就不发链接」)。纯文本是三种结果里唯一错的那种,现在一处不剩。
27+
28+
`sourcePathToDocsRoute()` 同时补上了它文档里一直声明、实现却没做的那一半:分类是真的
29+
不等于页面存在。旧实现只校验分类,这在放宽之前侥幸成立(能匹配上的路径恰好都有页面);
30+
放宽后那 4 个不存在的邻居会各产出一条 404 链接。现在按本次运行真正发出的页面清单判断,
31+
全语料 216 条站内路由、437 个位置,无死链。

content/docs/references/api/realtime-shared.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ realtime protocol (SSE/Polling/WebSocket) and the WebSocket collaboration protoc
1616
- `realtime.zod.ts` — Transport-layer protocol (Channel, Subscription, Transport selection)
1717
- `websocket.zod.ts` — Collaboration protocol (Cursor, OT editing, Advanced presence)
1818

19-
See also: realtime.zod.ts for transport-layer configuration
19+
See also: [realtime.zod.ts](/docs/references/api/realtime) for transport-layer configuration
2020

21-
See also: websocket.zod.ts for collaborative editing protocol
21+
See also: [websocket.zod.ts](/docs/references/api/websocket) for collaborative editing protocol
2222

2323
<Callout type="info">
2424
**Source:** `packages/spec/src/api/realtime-shared.zod.ts`

content/docs/references/cloud/package.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ flows, translations, agents — into a named, versioned artifact.
1414

1515
Architecture:
1616
- `sys_package` — identity (one row per logical package)
17-
- `sys_package_version` — immutable release snapshots (see package-version.zod.ts)
18-
- `sys_package_installation` — env ↔ version pairing (see environment-package.zod.ts)
17+
- `sys_package_version` — immutable release snapshots (see [package-version.zod.ts](/docs/references/cloud/package-version))
18+
- `sys_package_installation` — env ↔ version pairing (see [environment-package.zod.ts](/docs/references/cloud/environment-package))
1919

2020
See `docs/adr/0003-package-as-first-class-citizen.md` for the full rationale.
2121

content/docs/references/identity/identity.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Identity & User Model Specification
1010
Defines the standard user, account, and session data models for ObjectStack.
1111
These schemas represent "who is logged in" and their associated data.
1212

13-
This is separate from authentication configuration (auth.zod.ts) which
13+
This is separate from authentication configuration (`auth.zod.ts`) which
1414
defines "how to login".
1515

1616
<Callout type="info">

content/docs/references/system/security-context.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ Provides a central governance layer that correlates and unifies
1111
the four independent security subsystems it was designed against. Three of
1212
the four have since been REMOVED per ADR-0056 D8 (declared-but-never-enforced;
1313
see system/index.ts notes) — only encryption survives, marked experimental:
14-
- **Audit** (audit.zod.ts — REMOVED): the live audit path is plugin-audit's
14+
- **Audit** (`audit.zod.ts` — REMOVED): the live audit path is plugin-audit's
1515
always-on capture + object/field `trackHistory` + lifecycle `audit` retention
16-
- **Encryption** (encryption.zod.ts): Field-level encryption and key management
17-
- **Compliance** (compliance.zod.ts — REMOVED): GDPR/HIPAA/SOX/PCI-DSS configs
18-
- **Masking** (masking.zod.ts — REMOVED): PII data masking and tokenization
16+
- **Encryption** ([encryption.zod.ts](/docs/references/system/encryption)): Field-level encryption and key management
17+
- **Compliance** (`compliance.zod.ts` — REMOVED): GDPR/HIPAA/SOX/PCI-DSS configs
18+
- **Masking** (`masking.zod.ts` — REMOVED): PII data masking and tokenization
1919

2020
This schema enforces cross-cutting security policies, ensuring compliance
2121
frameworks drive encryption requirements, masking rules respect role-based

packages/spec/scripts/build-docs.ts

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,77 @@ function schemaHrefFrom(fromCategory: string): (name: string) => string | null {
280280
}
281281

282282

283+
/**
284+
* Every page this run publishes: `category` -> page slug -> the schemas that
285+
* page documents.
286+
*
287+
* Grouped ONCE and read twice — by §2 below, which emits the pages, and by
288+
* `sourcePathToDocsRoute`, which has to answer "is there a page for this file?"
289+
* while §2 is still part-way through the categories. Neither of the two obvious
290+
* shortcuts can answer it: asking the sink (`wasEmitted`) makes the reply depend
291+
* on which category the walk reached first, and asking the disk makes a run's
292+
* output depend on the previous run's, so a deleted page would keep resolving
293+
* until someone regenerated twice.
294+
*/
295+
function groupSchemasByPage(): Map<string, Map<string, Array<{ name: string; content: any }>>> {
296+
const byCategory = new Map<string, Map<string, Array<{ name: string; content: any }>>>();
297+
298+
for (const category of Object.keys(CATEGORIES)) {
299+
const categorySchemaDir = path.join(SCHEMA_DIR, category);
300+
if (!fs.existsSync(categorySchemaDir)) {
301+
console.log(`Warning: Schema directory ${categorySchemaDir} does not exist`);
302+
continue;
303+
}
304+
305+
const pages = new Map<string, Array<{ name: string; content: any }>>();
306+
for (const file of fs.readdirSync(categorySchemaDir).filter(f => f.endsWith('.json'))) {
307+
const schemaName = file.replace('.json', '');
308+
const content = JSON.parse(fs.readFileSync(path.join(categorySchemaDir, file), 'utf-8'));
309+
// Category-scoped: the page is owned by the file in THIS category that puts
310+
// the name on its export surface — declaration or re-export. `misc` stays
311+
// the catch-all for a published schema no `.zod.ts` here accounts for
312+
// (`security/*` declares two in plain `.ts` files), and it is honest about
313+
// it: `sourcePathFor` finds no file, so the page prints no "Source:" line.
314+
const zodFile = schemaIndex.pageFor(category, schemaName) || 'misc';
315+
316+
if (!pages.has(zodFile)) pages.set(zodFile, []);
317+
pages.get(zodFile)!.push({ name: schemaName, content });
318+
}
319+
320+
byCategory.set(category, pages);
321+
}
322+
323+
return byCategory;
324+
}
325+
283326
/**
284327
* Rewrite a source path referenced from JSDoc (`../automation/sync.zod.ts`) to
285328
* the docs route that renders it. Without this the generated page links to a
286329
* path that only exists in the repo, i.e. a 404 on the site.
330+
*
331+
* Always given a path WITH a category segment: `lib/file-description.ts`
332+
* completes a same-directory spelling from its `fromCategory` before calling in,
333+
* precisely so this stays the `<category>/<file>` lookup #4696 settled on and
334+
* never has to guess which `auth.zod.ts` an author meant.
287335
*/
288336
function sourcePathToDocsRoute(target: string): string | null {
289337
const m = target.match(/(?:^|\/)([\w-]+)\/([\w.-]+)\.zod\.ts$/);
290338
if (!m) return null;
291339
const [, category, zodFile] = m;
292340
if (!CATEGORIES[category]) return null;
341+
// A real category is not yet a page. This used to be the whole test, which
342+
// was survivable only because every path the old regex could match happened
343+
// to name a file with a page behind it. #6484 widened what reaches here to
344+
// include same-directory spellings, and FOUR of the nine name a neighbour
345+
// that does not exist at all — `identity/auth`, `system/audit`,
346+
// `system/compliance`, `system/masking`, all four long since removed. Under
347+
// the old test each would have become a confident link to a 404 (measured:
348+
// deleting this line puts exactly those four dead routes into the artifact).
349+
//
350+
// File existence is not the test either: seven `.zod.ts` sources publish no
351+
// page at all, their schemas being unrepresentable in JSON Schema. The test
352+
// is whether THIS run emits the page, which is what the map knows.
353+
if (!PAGES_BY_CATEGORY.get(category)?.has(zodFile)) return null;
293354
return `/docs/references/${category}/${zodFile}`;
294355
}
295356

@@ -420,7 +481,14 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string,
420481
const sourcePath = sourceRel ? path.join(REPO_ROOT, sourceRel) : undefined;
421482
let fileDesc = '';
422483
if (sourcePath && fs.existsSync(sourcePath)) {
423-
fileDesc = renderFileDescription(fs.readFileSync(sourcePath, 'utf-8'), { sourcePathToDocsRoute });
484+
// `category` is what a path written relative to the module's own
485+
// directory is relative TO — without it the renderer cannot tell which
486+
// `auth.zod.ts` a neighbour reference means, and until #6484 it was never
487+
// told, so those references shipped as plain prose.
488+
fileDesc = renderFileDescription(fs.readFileSync(sourcePath, 'utf-8'), {
489+
fromCategory: category,
490+
sourcePathToDocsRoute,
491+
});
424492
}
425493

426494
let md = `---\n`;
@@ -699,6 +767,15 @@ function deadDocLinks(mdx: string): string[] {
699767

700768
console.log('Building documentation...');
701769

770+
/**
771+
* The page inventory, built before anything is rendered.
772+
*
773+
* It has to exist before the first `renderFileDescription` call, because that
774+
* is where `sourcePathToDocsRoute` is asked whether a referenced neighbour has
775+
* a page — an answer no partially-filled sink could give.
776+
*/
777+
const PAGES_BY_CATEGORY = groupSchemasByPage();
778+
702779
/** Categories that had schemas to regenerate from — drives the flush() guard. */
703780
let managedCount = 0;
704781

@@ -734,34 +811,11 @@ if (fs.existsSync(DOCS_ROOT)) {
734811
// But verify we don't kill the manual files.
735812
}
736813

737-
Object.keys(CATEGORIES).forEach(category => {
738-
const categorySchemaDir = path.join(SCHEMA_DIR, category);
739-
740-
if (!fs.existsSync(categorySchemaDir)) {
741-
console.log(`Warning: Schema directory ${categorySchemaDir} does not exist`);
742-
return;
743-
}
744-
745-
const files = fs.readdirSync(categorySchemaDir).filter(f => f.endsWith('.json'));
746-
const zodFileSchemas = new Map<string, Array<{name: string, content: any}>>();
747-
748-
files.forEach(file => {
749-
const schemaName = file.replace('.json', '');
750-
const schemaPath = path.join(categorySchemaDir, file);
751-
const content = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
752-
// Category-scoped: the page is owned by the file in THIS category that puts
753-
// the name on its export surface — declaration or re-export. `misc` stays
754-
// the catch-all for a published schema no `.zod.ts` here accounts for
755-
// (`security/*` declares two in plain `.ts` files), and it is honest about
756-
// it: `sourcePathFor` finds no file, so the page prints no "Source:" line.
757-
const zodFile = schemaIndex.pageFor(category, schemaName) || 'misc';
758-
759-
if (!zodFileSchemas.has(zodFile)) {
760-
zodFileSchemas.set(zodFile, []);
761-
}
762-
zodFileSchemas.get(zodFile)!.push({ name: schemaName, content });
763-
});
764-
814+
// The grouping is `PAGES_BY_CATEGORY`'s, not a second one computed here: the
815+
// page a schema lands on decides both what this loop writes and what
816+
// `sourcePathToDocsRoute` calls a live route, and two enumerations of that could
817+
// disagree — the same discipline §2.6 already applies to the root index.
818+
PAGES_BY_CATEGORY.forEach((zodFileSchemas, category) => {
765819
const categoryDir = path.join(DOCS_ROOT, category);
766820

767821
// Generate file

0 commit comments

Comments
 (0)