From 25bad4f66cb632e5f010356f2bd05328fcfcc314 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 01:40:40 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(spec):=20=E6=8B=AC=E5=8F=B7=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E8=A3=B8=E6=BA=90=E7=A0=81=E8=B7=AF=E5=BE=84=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=88=90=E9=93=BE=E6=8E=A5=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E5=88=A0=E6=8E=89=20tokenizer=20=E4=B9=8B=E5=90=8E=E5=B7=B2?= =?UTF-8?q?=E6=97=A0=E4=BA=8B=E5=8F=AF=E5=81=9A=E7=9A=84=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=9E=BB=E5=AF=B9=20(#6420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `file-description.ts` 的 bare-path 改写步骤两端各挂一个前后瞻:`(? Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o --- .changeset/docs-gen-bare-path-in-parens.md | 28 ++++ .../spec/scripts/file-description.test.ts | 132 ++++++++++++++++++ packages/spec/scripts/lib/file-description.ts | 15 +- 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 .changeset/docs-gen-bare-path-in-parens.md 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/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}\``; })); From 9a90d48d446fbfb603a556c200e3a2937abc004d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 01:40:40 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore(docs):=20=E9=87=8D=E6=96=B0=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=8F=82=E8=80=83=E6=96=87=E6=A1=A3=20=E2=80=94?= =?UTF-8?q?=E2=80=94=204=20=E5=A4=84=E6=8B=AC=E5=8F=B7=E5=86=85=E8=A3=B8?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=88=90=E9=93=BE=E6=8E=A5=20(#6420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o --- content/docs/references/automation/etl.mdx | 2 +- content/docs/references/integration/connector.mdx | 2 +- content/docs/references/shared/mapping.mdx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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 665856b7f7..9bfae000ce 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 From 8396c888190d11d419350de92ad3948bb8e35a8a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 01:41:27 +0000 Subject: [PATCH 3/3] =?UTF-8?q?chore(docs):=20=E5=9C=A8=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=A0=91=E4=B8=8A=E6=95=B4=E4=BD=93=E9=87=8D=E8=B7=91=20gen:do?= =?UTF-8?q?cs=20=E2=80=94=E2=80=94=20connector.mdx=20=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=B8=A6=E4=B8=8A=20#6473=20=E4=B8=8E=E6=9C=AC=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E6=95=88=E6=9E=9C=20(#6420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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 Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o --- .../docs/references/integration/connector.mdx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 9bfae000ce..c109cc96fa 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -18,11 +18,43 @@ retired in #4738 — narrative-only, zero consumers; see - **Enterprise Connector** (THIS FILE) - System integrators - Full SAP integration; connector-attached sync via `syncConfig` **SCOPE: Most comprehensive integration layer.** -Includes authentication, webhooks, rate limiting, field mapping, bidirectional sync, +Includes authentication, webhooks, field mapping, bidirectional sync, retry policies, and complete lifecycle management. This protocol supports multiple authentication strategies, bidirectional sync, -field mapping, webhooks, and comprehensive rate limiting. +field mapping, webhooks, and comprehensive retry and resilience policies. + +## What this layer does NOT provide + +**There is no outbound rate limiting.** This header used to advertise "rate +limiting" twice — once in the SCOPE line, once as "comprehensive rate limiting" — +and no engine ever backed either. `connector.rateLimitConfig`, and the entire +`ConnectorRateLimitConfig` / `RateLimitStrategy` shape behind it, was removed in +`@objectstack/spec` 17.0.0 (#4911, ADR-0049 D2), because **no outbound +rate-limiting engine ever existed**. The platform's only token bucket (runtime +`security/rate-limit.ts`) throttles **INBOUND** requests *to* us; nothing throttles +the calls a connector makes *out*. Do **not** substitute `shared`'s +`RateLimitConfig` — that is the inbound limiter and would cap the wrong direction. +**Until an outbound throttle exists, rate-limit at the connector provider or +upstream gateway.** What L3 does declare for a rate-limited upstream is +`retryConfig` — whose `retryableStatusCodes` default `[408, 429, 500, 502, 503, +504]` includes `429` — and `health.circuitBreaker`. The full removal reasoning is +recorded at the removal site: the "REMOVED: outbound rate limiting" block in +`integration/connector.zod.ts`, and `packages/spec/docs/SYNC_ARCHITECTURE.md`. + +**Field mapping does not transform values.** This header used to offer "field +mapping and transformations"; only the first half was ever true. +`ConnectorFieldMappingSchema` extends the base mapping with exactly three keys — +`dataType`, `required` and `syncMode`. `FieldMapping.transform` was removed in +`@objectstack/spec` 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` +union went with it (`constant` / `cast` / `lookup` / `javascript` / `map`) — **no +runtime ever executed any of the five**. An L3 connector mapping moves a value from +`source` to `target`; it does not compute one. **Value conversion belongs on a +surface that runs it:** the import mapping's own `mapping.fieldMapping[].transform` +(`data/mapping.zod.ts` — a string enum, +`none`/`constant`/`map`/`split`/`join`/`lookup`, with its settings in `params`), +applied row by row by the REST import path — or an ETL transformation step +(L2 above). Already authored the retired key? `os migrate meta --from 16` rewrites it. ## Runtime contract — descriptor vs. registered connector (#2612) @@ -52,8 +84,8 @@ Authentication is now imported from the canonical `auth/config.zod.ts`. **Use Enterprise Connector when:** - Building enterprise-grade connectors (e.g., Salesforce, SAP, Oracle) - Complex OAuth2/SAML authentication required -- Bidirectional sync with field mapping and transformations -- Webhook management and rate limiting required +- Bidirectional sync with field mapping (`dataType` / `syncMode` per field — it moves values, it does not transform them) +- Webhook management required - Full CRUD operations and data synchronization - Need comprehensive retry strategies and error handling