Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/docs-gen-bare-path-in-parens.md
Original file line number Diff line number Diff line change
@@ -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。
2 changes: 1 addition & 1 deletion content/docs/references/automation/etl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/shared/mapping.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions packages/spec/scripts/file-description.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, `(?<!\()` … `(?!\))`, from before the
* tokenizer existed. Its job was "do not touch a path that is already a link's
* destination", because `](route)` puts that path between exactly those two
* characters. It never could state that (lookaround cannot say "not nested
* inside a link" — the module comment is explicit), and since #6136 it has had
* nothing left to state: a formed link is a `link` run and this step is only
* ever shown `text` runs. What the pair still did was refuse every path an
* author had put in ordinary parentheses, which is neither a link nor code —
* so those paths rendered as bare text on three published pages:
*
* automation/etl.mdx:16 `- **Enterprise Connector** (integration/connector.zod.ts) - …`
* integration/connector.mdx:17 `- **ETL Pipeline** (automation/etl.zod.ts) - …`
* shared/mapping.mdx:16-17 `- Integration connectors (integration/connector.zod.ts)` (+ external-lookup)
*
* MEASURED (reverse verification), the ordinary direction: putting either
* guard back turns the four parenthesised cases below red — the two-sided pair
* and each half on its own, since a path in `(…)` trips both — while the
* unparenthesised cases and the whole #6229 block above stay green. Restoring
* them also leaves `keeps a formed link's destination out of reach` green,
* which is the point of that case: it is the tokenizer that holds the
* invariant now, so removing the guards cannot re-open #6136.
*
* Corpus-wide the widening is exactly those four positions and nothing else
* (`gen:docs` on the fixed generator: 231 files, 3 changed, 4 lines), and all
* three routes it newly emits resolve to a real page.
*/
describe('renderFileDescription — #6420: a bare path in parentheses still links', () => {
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
Expand Down Expand Up @@ -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, `(?:\.\./)*<dir>/<file>.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
Expand Down
15 changes: 14 additions & 1 deletion packages/spec/scripts/lib/file-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `(?<!\()`
// … `(?!\))` was this step's ORIGINAL, pre-tokenizer attempt at "do not touch
// a path that is already a link's destination" — `](route)` puts that path
// between exactly those two characters. It never could express that (the
// module comment above says why: lookaround cannot say "not nested inside a
// link"), and since #6136 it has had nothing left to do — a formed link is a
// `link` run and this step is only shown `text` runs. What the pair still did
// was refuse every path an AUTHOR wrote in parentheses, which is ordinary
// prose and not a link at all: `- **Enterprise Connector**
// (integration/connector.zod.ts) - …` rendered as neither a link nor code,
// just plain text, on three published pages. So the guards go and the
// tokenizer keeps the invariant they were reaching for.
out = mapProse(out, ['text'], s =>
s.replace(/(?<!\()((?:\.\.\/)*\b[\w-]+\/[\w.-]+\.zod\.ts)\b(?!\))/g, (_m, p: string) => {
s.replace(/((?:\.\.\/)*\b[\w-]+\/[\w.-]+\.zod\.ts)\b/g, (_m, p: string) => {
const route = sourcePathToDocsRoute(p);
return route ? `[${p}](${route})` : `\`${p}\``;
}));
Expand Down
Loading