diff --git a/.changeset/docs-gen-bare-path-in-parens.md b/.changeset/docs-gen-bare-path-in-parens.md new file mode 100644 index 0000000000..5eb6b056ca --- /dev/null +++ b/.changeset/docs-gen-bare-path-in-parens.md @@ -0,0 +1,28 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): 参考页里写在括号中的裸源码路径重新成链接 (#6420) + +参考页开篇那段模块描述由 `packages/spec/scripts/lib/file-description.ts` 渲染。其中 +把 JSDoc 里裸写的 `*.zod.ts` 路径改写成站内链接的那一步,正则两端各挂着一个前后瞻 +——「前面不是 `(`」和「后面不是 `)`」。这对前后瞻是 tokenizer 出现**之前**的产物, +本意是「别去动已经是链接目标的路径」:`](route)` 恰好把那个路径夹在这两个字符中间。 +它从来表达不了这件事(前后瞻说不出「不在链接内部」,模块注释里写着),而 #6136 之后 +它更是无事可做了 —— 成形的链接是独立的 `link` token,这一步只会看到 `text` token。 + +它**仍在**做的,是把作者自己写在普通括号里的每一个路径一并拒掉。那是散文,不是链接, +于是这些路径既没成链接也没成代码,以纯文本发布在三张参考页上: + +- `references/automation/etl` —— `- **Enterprise Connector** (integration/connector.zod.ts) - …` +- `references/integration/connector` —— `- **ETL Pipeline** (automation/etl.zod.ts) - …` +- `references/shared/mapping` —— `- Integration connectors (integration/connector.zod.ts)` 与 `- External lookups (data/external-lookup.zod.ts)` + +现在删掉这对前后瞻,它们原本想守的不变量交还给 tokenizer 守。读者可见的变化就是上面 +四处从纯文本变成可点的站内链接,路由分别指向 `/docs/references/integration/connector`、 +`/docs/references/automation/etl`、`/docs/references/data/external-lookup` —— 三条都 +对应真实存在的页面。 + +放宽的**实测**半径就是这四处,别无其他:在修好的生成器上重跑 `gen:docs`,231 个产物 +里 3 个文件、4 行发生变化。渲染成链接的前提没有放宽 —— 目标没有页面的路径照旧回退成 +代码段,所以括号位置永远不会产出 404。 diff --git a/content/docs/references/automation/etl.mdx b/content/docs/references/automation/etl.mdx index 4bfd3a8429..0275a7ee94 100644 --- a/content/docs/references/automation/etl.mdx +++ b/content/docs/references/automation/etl.mdx @@ -13,7 +13,7 @@ Inspired by modern data integration platforms like Airbyte, Fivetran, and Apache retired in #4738 — narrative-only, zero consumers; see `packages/spec/docs/SYNC_ARCHITECTURE.md`): - **ETL Pipeline** (THIS FILE) - Data engineers - Aggregate 10 sources to warehouse -- **Enterprise Connector** (integration/connector.zod.ts) - System integrators - Full SAP integration; connector-attached sync via `syncConfig` +- **Enterprise Connector** ([integration/connector.zod.ts](/docs/references/integration/connector)) - System integrators - Full SAP integration; connector-attached sync via `syncConfig` ETL pipelines enable automated data synchronization between systems, transforming data as it moves from source to destination. diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 23f28ce36b..c109cc96fa 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -14,7 +14,7 @@ and message queues through a unified protocol. **Positioning in the sync/integration layering** (L1 "Simple Sync" was retired in #4738 — narrative-only, zero consumers; see `packages/spec/docs/SYNC_ARCHITECTURE.md`): -- **ETL Pipeline** (automation/etl.zod.ts) - Data engineers - Aggregate 10 sources to warehouse +- **ETL Pipeline** ([automation/etl.zod.ts](/docs/references/automation/etl)) - Data engineers - Aggregate 10 sources to warehouse - **Enterprise Connector** (THIS FILE) - System integrators - Full SAP integration; connector-attached sync via `syncConfig` **SCOPE: Most comprehensive integration layer.** diff --git a/content/docs/references/shared/mapping.mdx b/content/docs/references/shared/mapping.mdx index 88fb9ca0bf..e4873d23e8 100644 --- a/content/docs/references/shared/mapping.mdx +++ b/content/docs/references/shared/mapping.mdx @@ -13,8 +13,8 @@ This module provides the canonical field mapping schema used across ObjectStack for data synchronization. **Use Cases:** -- Integration connectors (integration/connector.zod.ts) -- External lookups (data/external-lookup.zod.ts) +- Integration connectors ([integration/connector.zod.ts](/docs/references/integration/connector)) +- External lookups ([data/external-lookup.zod.ts](/docs/references/data/external-lookup)) @example Basic field mapping ```typescript diff --git a/packages/spec/scripts/file-description.test.ts b/packages/spec/scripts/file-description.test.ts index 561da8e47c..09a5b9c646 100644 --- a/packages/spec/scripts/file-description.test.ts +++ b/packages/spec/scripts/file-description.test.ts @@ -553,6 +553,112 @@ describe('renderFileDescription — #6229: a bare path keeps its `../` prefix in }); }); +/** + * #6420 — a path an author wrote in PARENTHESES is prose, not a link. + * + * The rewriter carried a lookaround pair, `(? { + const ctx = { + // Mirrors `build-docs.ts`'s `sourcePathToDocsRoute`, restricted to the two + // categories these cases name, so an unroutable path is genuinely + // unroutable rather than a stand-in that resolves everything. + sourcePathToDocsRoute: (t: string) => { + const m = /(?:^|\/)(integration|automation)\/([\w-]+)\.zod\.ts$/.exec(t); + return m ? `/docs/references/${m[1]}/${m[2]}` : null; + }, + }; + + const describedBy = (line: string) => + renderFileDescription(['/**', ` * ${line}`, ' */', '', "import { z } from 'zod';", ''].join('\n'), ctx); + + it('links a parenthesised path — the published `automation/etl` line', () => { + // `packages/spec/src/automation/etl.zod.ts` verbatim — the exact input + // behind `content/docs/references/automation/etl.mdx:16`. + expect( + describedBy('- **Enterprise Connector** (integration/connector.zod.ts) - System integrators'), + ).toBe( + '- **Enterprise Connector** ([integration/connector.zod.ts](/docs/references/integration/connector)) - System integrators', + ); + }); + + it('links a parenthesised path that closes the line — the `shared/mapping` shape', () => { + // `content/docs/references/shared/mapping.mdx:16`. Distinct from the case + // above on purpose: there the `)` is followed by more prose, here it ends + // the line, and the trailing guard `(?!\))` refused both. + expect(describedBy('- Integration connectors (integration/connector.zod.ts)')).toBe( + '- Integration connectors ([integration/connector.zod.ts](/docs/references/integration/connector))', + ); + }); + + it('keeps a `../` prefix inside the link when the path is parenthesised', () => { + // #6229 and this fix compose: the prefix belongs inside the link, and the + // parentheses stay outside it. Neither fix implies the other. + expect(describedBy('The layer (../integration/connector.zod.ts) is the widest.')).toBe( + 'The layer ([../integration/connector.zod.ts](/docs/references/integration/connector)) is the widest.', + ); + }); + + it('prints an unroutable parenthesised path as code, never as a dead link', () => { + // Widening the rewriter must not widen what it is willing to LINK. A path + // with no page still falls back to a code span, so the parentheses can + // never produce a 404 on the site. + expect(describedBy('Nothing here (nowhere/absent.zod.ts) resolves.')).toBe( + 'Nothing here (`nowhere/absent.zod.ts`) resolves.', + ); + }); + + it('still links the same path outside parentheses — the fix widens, it does not move', () => { + // The vacuity guard for the four cases above. Each of them asserts an + // OUTPUT for a path in parentheses; if this `ctx` had stopped resolving + // that path, the parenthesised cases could have been written around a + // code-span fallback and passed while proving nothing. Pinning the same + // path unparenthesised fixes the only variable to the parentheses. + expect(describedBy('The layer integration/connector.zod.ts is the widest.')).toBe( + 'The layer [integration/connector.zod.ts](/docs/references/integration/connector) is the widest.', + ); + }); + + it('keeps a formed link destination out of reach — the tokenizer, not the guards', () => { + // The case the deleted lookaround was actually written for, and the reason + // deleting it is safe. A titled `{@link}` whose target has no page emits + // `[label](../nowhere/absent.zod.ts)`: the raw path is now a link + // DESTINATION, sitting between the very `(` and `)` the guards tested for. + // With them gone the only thing standing between that path and a second + // rewrite is #6136's tokenizer, which classifies the whole construct as a + // `link` run this step is never shown. Were that protection to regress, + // this case reports `[the fallback](\`../nowhere/absent.zod.ts\`)` — the + // #6136 shape — while every other case here stays green. + expect(describedBy('See {@link ../nowhere/absent.zod.ts|the fallback} for now.')).toBe( + 'See [the fallback](../nowhere/absent.zod.ts) for now.', + ); + }); +}); + /** * The corpus half: re-derive the verdict from the real sources, so the six * pages the issue measured cannot silently re-acquire a wrong opening, and so a @@ -790,6 +896,32 @@ describe('corpus — every rendered description is well-formed markdown', () => expect(offenders).toEqual([]); }); + it('never leaves a bare source path sitting in parentheses (#6420)', () => { + // The corpus half of the unit block above. A path this step CAN match — + // one with a category segment, `(?:\.\./)*/.zod.ts` — must never + // reach a page still bare: it is a link when a page renders it and a code + // span when none does, and "plain text between parentheses" is the one + // outcome the lookaround pair used to force. Scanned on the rendered + // fragment rather than on the emitted `.mdx` for the same reason the rest + // of this file is: `check:docs` reproduces the artifact faithfully and so + // stayed green through all three published symptoms. + // + // Only the head character is examined, not a full `(…)` pair: `- Integration + // connectors (integration/connector.zod.ts)` and `(…) - System integrators` + // are different closers and both were victims, so what identifies the class + // is a `(` immediately before the path. A path that follows `](` is a link + // destination and belongs there — the tokenizer put it there. + const offenders: string[] = []; + for (const { rel, out } of described) { + for (const line of withoutFences(out).split('\n')) { + const bare = line.replace(/`[^`]*`/g, ''); // a code span is the null-route fallback + const hit = /(^|[^\]])\((?:\.\.\/)*[\w-]+\/[\w.-]+\.zod\.ts/.exec(bare); + if (hit) offenders.push(`${rel}: ${hit[0].trim()}`); + } + } + expect(offenders).toEqual([]); + }); + it('keeps a description for every source that had one — #6134 selection is untouched', () => { // The rendering fix must not remove a page's opening paragraph; that is // #5059's acceptance criterion and it still binds. 185 sources carry a diff --git a/packages/spec/scripts/lib/file-description.ts b/packages/spec/scripts/lib/file-description.ts index c3604e8981..5c36978899 100644 --- a/packages/spec/scripts/lib/file-description.ts +++ b/packages/spec/scripts/lib/file-description.ts @@ -386,8 +386,21 @@ function renderProse(text: string, ctx: FileDescriptionContext): string { // `*` without moving the `\b` changes nothing, since a group that is never // reached repeats zero times either way. Both halves are load-bearing — // moving the `\b` alone would still strand the outer level of a `../../`. + // + // No lookaround guards the parentheses any more (#6420). The pair `(? - s.replace(/(? { + s.replace(/((?:\.\.\/)*\b[\w-]+\/[\w.-]+\.zod\.ts)\b/g, (_m, p: string) => { const route = sourcePathToDocsRoute(p); return route ? `[${p}](${route})` : `\`${p}\``; }));