Skip to content

Commit a2e157c

Browse files
fix(spec): 参考页里写在括号中的裸源码路径重新成链接 —— 删掉 tokenizer 之后已无事可做的前后瞻对 (#6491)
* fix(spec): 括号里的裸源码路径重新成链接 —— 删掉 tokenizer 之后已无事可做的前后瞻对 (#6420) `file-description.ts` 的 bare-path 改写步骤两端各挂一个前后瞻:`(?<!\()` 与 `(?!\))`。这是 tokenizer 出现之前的产物,本意是「别动已经是链接目标的路径」—— `](route)` 恰好把路径夹在这两个字符中间。前后瞻从来说不出这件事(说不出「不在 链接内部」,模块注释里写着),而 #6136 之后它更是无事可做:成形的链接是独立的 `link` token,这一步只会拿到 `text` token。 它仍在做的,是把作者写在**普通括号**里的路径一并拒掉 —— 那是散文,不是链接。 于是这些路径既没成链接也没成代码,以纯文本发布在三张参考页上(4 处)。 删掉这对前后瞻,原本想守的不变量交还给 tokenizer 守。目标没有页面的路径照旧 回退成代码段,所以括号位置不会产出 404。 单测 6 条与 #6229 的 7 条并列;语料级一条断言「能被本步匹配的路径不会以裸文本 留在括号里」。反向验证(实测):把前后瞻整对放回、或只放回任一半,都让 4 条括号 用例 + 语料条转红,其余全绿 —— 包括「成形链接的目标不被二次改写」那条,它证明 守不变量的现在是 tokenizer。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o * chore(docs): 重新生成参考文档 —— 4 处括号内裸路径成链接 (#6420) `pnpm --filter @objectstack/spec gen:docs` 的纯产物,未手改一个字节。 231 个产物里 3 个文件、4 行变化,即本次放宽的全部实测半径: - references/automation/etl:16 (integration/connector.zod.ts) - references/integration/connector:17 (automation/etl.zod.ts) - references/shared/mapping:16-17 (integration/connector.zod.ts) 与 (data/external-lookup.zod.ts) 三条新路由 /docs/references/{integration/connector,automation/etl,data/external-lookup} 均对应真实存在的页面。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o * chore(docs): 在合并树上整体重跑 gen:docs —— connector.mdx 同时带上 #6473 与本单的效果 (#6420) #6473(#6383)在飞行中落地,与本单同页相撞。按本仓成规**不做文本合并**: 合 main 后在合并树上整体重跑 `gen:schema && gen:docs`(#4675 第四步),产物取 生成器输出,未手改一个字节。仓库自带的 regen merge driver 也正是这样拒绝 文本合并并给出这条指令的。 合并后同页实测两侧效果俱在: - #6473:第 27-43 行「What this layer does NOT provide」一节在; - 本单:第 17 行 `- **ETL Pipeline** (automation/etl.zod.ts)` 已成链接。 并复核 #6473 新增散文里的路径:第 43/54/80/106/113 行均在反引号内,是 `code` token,本单放宽够不着 —— 与派单预判一致,此处为实测而非假定。 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 1fe436d commit a2e157c

6 files changed

Lines changed: 178 additions & 5 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): 参考页里写在括号中的裸源码路径重新成链接 (#6420)
6+
7+
参考页开篇那段模块描述由 `packages/spec/scripts/lib/file-description.ts` 渲染。其中
8+
把 JSDoc 里裸写的 `*.zod.ts` 路径改写成站内链接的那一步,正则两端各挂着一个前后瞻
9+
——「前面不是 `(`」和「后面不是 `)`」。这对前后瞻是 tokenizer 出现**之前**的产物,
10+
本意是「别去动已经是链接目标的路径」:`](route)` 恰好把那个路径夹在这两个字符中间。
11+
它从来表达不了这件事(前后瞻说不出「不在链接内部」,模块注释里写着),而 #6136 之后
12+
它更是无事可做了 —— 成形的链接是独立的 `link` token,这一步只会看到 `text` token。
13+
14+
**仍在**做的,是把作者自己写在普通括号里的每一个路径一并拒掉。那是散文,不是链接,
15+
于是这些路径既没成链接也没成代码,以纯文本发布在三张参考页上:
16+
17+
- `references/automation/etl` —— `- **Enterprise Connector** (integration/connector.zod.ts) - …`
18+
- `references/integration/connector` —— `- **ETL Pipeline** (automation/etl.zod.ts) - …`
19+
- `references/shared/mapping` —— `- Integration connectors (integration/connector.zod.ts)``- External lookups (data/external-lookup.zod.ts)`
20+
21+
现在删掉这对前后瞻,它们原本想守的不变量交还给 tokenizer 守。读者可见的变化就是上面
22+
四处从纯文本变成可点的站内链接,路由分别指向 `/docs/references/integration/connector`
23+
`/docs/references/automation/etl``/docs/references/data/external-lookup` —— 三条都
24+
对应真实存在的页面。
25+
26+
放宽的**实测**半径就是这四处,别无其他:在修好的生成器上重跑 `gen:docs`,231 个产物
27+
里 3 个文件、4 行发生变化。渲染成链接的前提没有放宽 —— 目标没有页面的路径照旧回退成
28+
代码段,所以括号位置永远不会产出 404。

content/docs/references/automation/etl.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Inspired by modern data integration platforms like Airbyte, Fivetran, and Apache
1313
retired in #4738 — narrative-only, zero consumers; see
1414
`packages/spec/docs/SYNC_ARCHITECTURE.md`):
1515
- **ETL Pipeline** (THIS FILE) - Data engineers - Aggregate 10 sources to warehouse
16-
- **Enterprise Connector** (integration/connector.zod.ts) - System integrators - Full SAP integration; connector-attached sync via `syncConfig`
16+
- **Enterprise Connector** ([integration/connector.zod.ts](/docs/references/integration/connector)) - System integrators - Full SAP integration; connector-attached sync via `syncConfig`
1717

1818
ETL pipelines enable automated data synchronization between systems, transforming
1919
data as it moves from source to destination.

content/docs/references/integration/connector.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and message queues through a unified protocol.
1414
**Positioning in the sync/integration layering** (L1 "Simple Sync" was
1515
retired in #4738 — narrative-only, zero consumers; see
1616
`packages/spec/docs/SYNC_ARCHITECTURE.md`):
17-
- **ETL Pipeline** (automation/etl.zod.ts) - Data engineers - Aggregate 10 sources to warehouse
17+
- **ETL Pipeline** ([automation/etl.zod.ts](/docs/references/automation/etl)) - Data engineers - Aggregate 10 sources to warehouse
1818
- **Enterprise Connector** (THIS FILE) - System integrators - Full SAP integration; connector-attached sync via `syncConfig`
1919

2020
**SCOPE: Most comprehensive integration layer.**

content/docs/references/shared/mapping.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ This module provides the canonical field mapping schema used across
1313
ObjectStack for data synchronization.
1414

1515
**Use Cases:**
16-
- Integration connectors (integration/connector.zod.ts)
17-
- External lookups (data/external-lookup.zod.ts)
16+
- Integration connectors ([integration/connector.zod.ts](/docs/references/integration/connector))
17+
- External lookups ([data/external-lookup.zod.ts](/docs/references/data/external-lookup))
1818

1919
@example Basic field mapping
2020
```typescript

packages/spec/scripts/file-description.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,112 @@ describe('renderFileDescription — #6229: a bare path keeps its `../` prefix in
553553
});
554554
});
555555

556+
/**
557+
* #6420 — a path an author wrote in PARENTHESES is prose, not a link.
558+
*
559+
* The rewriter carried a lookaround pair, `(?<!\()` … `(?!\))`, from before the
560+
* tokenizer existed. Its job was "do not touch a path that is already a link's
561+
* destination", because `](route)` puts that path between exactly those two
562+
* characters. It never could state that (lookaround cannot say "not nested
563+
* inside a link" — the module comment is explicit), and since #6136 it has had
564+
* nothing left to state: a formed link is a `link` run and this step is only
565+
* ever shown `text` runs. What the pair still did was refuse every path an
566+
* author had put in ordinary parentheses, which is neither a link nor code —
567+
* so those paths rendered as bare text on three published pages:
568+
*
569+
* automation/etl.mdx:16 `- **Enterprise Connector** (integration/connector.zod.ts) - …`
570+
* integration/connector.mdx:17 `- **ETL Pipeline** (automation/etl.zod.ts) - …`
571+
* shared/mapping.mdx:16-17 `- Integration connectors (integration/connector.zod.ts)` (+ external-lookup)
572+
*
573+
* MEASURED (reverse verification), the ordinary direction: putting either
574+
* guard back turns the four parenthesised cases below red — the two-sided pair
575+
* and each half on its own, since a path in `(…)` trips both — while the
576+
* unparenthesised cases and the whole #6229 block above stay green. Restoring
577+
* them also leaves `keeps a formed link's destination out of reach` green,
578+
* which is the point of that case: it is the tokenizer that holds the
579+
* invariant now, so removing the guards cannot re-open #6136.
580+
*
581+
* Corpus-wide the widening is exactly those four positions and nothing else
582+
* (`gen:docs` on the fixed generator: 231 files, 3 changed, 4 lines), and all
583+
* three routes it newly emits resolve to a real page.
584+
*/
585+
describe('renderFileDescription — #6420: a bare path in parentheses still links', () => {
586+
const ctx = {
587+
// Mirrors `build-docs.ts`'s `sourcePathToDocsRoute`, restricted to the two
588+
// categories these cases name, so an unroutable path is genuinely
589+
// unroutable rather than a stand-in that resolves everything.
590+
sourcePathToDocsRoute: (t: string) => {
591+
const m = /(?:^|\/)(integration|automation)\/([\w-]+)\.zod\.ts$/.exec(t);
592+
return m ? `/docs/references/${m[1]}/${m[2]}` : null;
593+
},
594+
};
595+
596+
const describedBy = (line: string) =>
597+
renderFileDescription(['/**', ` * ${line}`, ' */', '', "import { z } from 'zod';", ''].join('\n'), ctx);
598+
599+
it('links a parenthesised path — the published `automation/etl` line', () => {
600+
// `packages/spec/src/automation/etl.zod.ts` verbatim — the exact input
601+
// behind `content/docs/references/automation/etl.mdx:16`.
602+
expect(
603+
describedBy('- **Enterprise Connector** (integration/connector.zod.ts) - System integrators'),
604+
).toBe(
605+
'- **Enterprise Connector** ([integration/connector.zod.ts](/docs/references/integration/connector)) - System integrators',
606+
);
607+
});
608+
609+
it('links a parenthesised path that closes the line — the `shared/mapping` shape', () => {
610+
// `content/docs/references/shared/mapping.mdx:16`. Distinct from the case
611+
// above on purpose: there the `)` is followed by more prose, here it ends
612+
// the line, and the trailing guard `(?!\))` refused both.
613+
expect(describedBy('- Integration connectors (integration/connector.zod.ts)')).toBe(
614+
'- Integration connectors ([integration/connector.zod.ts](/docs/references/integration/connector))',
615+
);
616+
});
617+
618+
it('keeps a `../` prefix inside the link when the path is parenthesised', () => {
619+
// #6229 and this fix compose: the prefix belongs inside the link, and the
620+
// parentheses stay outside it. Neither fix implies the other.
621+
expect(describedBy('The layer (../integration/connector.zod.ts) is the widest.')).toBe(
622+
'The layer ([../integration/connector.zod.ts](/docs/references/integration/connector)) is the widest.',
623+
);
624+
});
625+
626+
it('prints an unroutable parenthesised path as code, never as a dead link', () => {
627+
// Widening the rewriter must not widen what it is willing to LINK. A path
628+
// with no page still falls back to a code span, so the parentheses can
629+
// never produce a 404 on the site.
630+
expect(describedBy('Nothing here (nowhere/absent.zod.ts) resolves.')).toBe(
631+
'Nothing here (`nowhere/absent.zod.ts`) resolves.',
632+
);
633+
});
634+
635+
it('still links the same path outside parentheses — the fix widens, it does not move', () => {
636+
// The vacuity guard for the four cases above. Each of them asserts an
637+
// OUTPUT for a path in parentheses; if this `ctx` had stopped resolving
638+
// that path, the parenthesised cases could have been written around a
639+
// code-span fallback and passed while proving nothing. Pinning the same
640+
// path unparenthesised fixes the only variable to the parentheses.
641+
expect(describedBy('The layer integration/connector.zod.ts is the widest.')).toBe(
642+
'The layer [integration/connector.zod.ts](/docs/references/integration/connector) is the widest.',
643+
);
644+
});
645+
646+
it('keeps a formed link destination out of reach — the tokenizer, not the guards', () => {
647+
// The case the deleted lookaround was actually written for, and the reason
648+
// deleting it is safe. A titled `{@link}` whose target has no page emits
649+
// `[label](../nowhere/absent.zod.ts)`: the raw path is now a link
650+
// DESTINATION, sitting between the very `(` and `)` the guards tested for.
651+
// With them gone the only thing standing between that path and a second
652+
// rewrite is #6136's tokenizer, which classifies the whole construct as a
653+
// `link` run this step is never shown. Were that protection to regress,
654+
// this case reports `[the fallback](\`../nowhere/absent.zod.ts\`)` — the
655+
// #6136 shape — while every other case here stays green.
656+
expect(describedBy('See {@link ../nowhere/absent.zod.ts|the fallback} for now.')).toBe(
657+
'See [the fallback](../nowhere/absent.zod.ts) for now.',
658+
);
659+
});
660+
});
661+
556662
/**
557663
* The corpus half: re-derive the verdict from the real sources, so the six
558664
* 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', () =>
790896
expect(offenders).toEqual([]);
791897
});
792898

899+
it('never leaves a bare source path sitting in parentheses (#6420)', () => {
900+
// The corpus half of the unit block above. A path this step CAN match —
901+
// one with a category segment, `(?:\.\./)*<dir>/<file>.zod.ts` — must never
902+
// reach a page still bare: it is a link when a page renders it and a code
903+
// span when none does, and "plain text between parentheses" is the one
904+
// outcome the lookaround pair used to force. Scanned on the rendered
905+
// fragment rather than on the emitted `.mdx` for the same reason the rest
906+
// of this file is: `check:docs` reproduces the artifact faithfully and so
907+
// stayed green through all three published symptoms.
908+
//
909+
// Only the head character is examined, not a full `(…)` pair: `- Integration
910+
// connectors (integration/connector.zod.ts)` and `(…) - System integrators`
911+
// are different closers and both were victims, so what identifies the class
912+
// is a `(` immediately before the path. A path that follows `](` is a link
913+
// destination and belongs there — the tokenizer put it there.
914+
const offenders: string[] = [];
915+
for (const { rel, out } of described) {
916+
for (const line of withoutFences(out).split('\n')) {
917+
const bare = line.replace(/`[^`]*`/g, ''); // a code span is the null-route fallback
918+
const hit = /(^|[^\]])\((?:\.\.\/)*[\w-]+\/[\w.-]+\.zod\.ts/.exec(bare);
919+
if (hit) offenders.push(`${rel}: ${hit[0].trim()}`);
920+
}
921+
}
922+
expect(offenders).toEqual([]);
923+
});
924+
793925
it('keeps a description for every source that had one — #6134 selection is untouched', () => {
794926
// The rendering fix must not remove a page's opening paragraph; that is
795927
// #5059's acceptance criterion and it still binds. 185 sources carry a

packages/spec/scripts/lib/file-description.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,21 @@ function renderProse(text: string, ctx: FileDescriptionContext): string {
386386
// `*` without moving the `\b` changes nothing, since a group that is never
387387
// reached repeats zero times either way. Both halves are load-bearing —
388388
// moving the `\b` alone would still strand the outer level of a `../../`.
389+
//
390+
// No lookaround guards the parentheses any more (#6420). The pair `(?<!\()`
391+
// … `(?!\))` was this step's ORIGINAL, pre-tokenizer attempt at "do not touch
392+
// a path that is already a link's destination" — `](route)` puts that path
393+
// between exactly those two characters. It never could express that (the
394+
// module comment above says why: lookaround cannot say "not nested inside a
395+
// link"), and since #6136 it has had nothing left to do — a formed link is a
396+
// `link` run and this step is only shown `text` runs. What the pair still did
397+
// was refuse every path an AUTHOR wrote in parentheses, which is ordinary
398+
// prose and not a link at all: `- **Enterprise Connector**
399+
// (integration/connector.zod.ts) - …` rendered as neither a link nor code,
400+
// just plain text, on three published pages. So the guards go and the
401+
// tokenizer keeps the invariant they were reaching for.
389402
out = mapProse(out, ['text'], s =>
390-
s.replace(/(?<!\()((?:\.\.\/)*\b[\w-]+\/[\w.-]+\.zod\.ts)\b(?!\))/g, (_m, p: string) => {
403+
s.replace(/((?:\.\.\/)*\b[\w-]+\/[\w.-]+\.zod\.ts)\b/g, (_m, p: string) => {
391404
const route = sourcePathToDocsRoute(p);
392405
return route ? `[${p}](${route})` : `\`${p}\``;
393406
}));

0 commit comments

Comments
 (0)