diff --git a/.agents/skills/dsh-create-upgrade-guide/SKILL.md b/.agents/skills/dsh-create-upgrade-guide/SKILL.md index cf3b46bfdd..9723537e22 100644 --- a/.agents/skills/dsh-create-upgrade-guide/SKILL.md +++ b/.agents/skills/dsh-create-upgrade-guide/SKILL.md @@ -25,7 +25,7 @@ Pre-stable internal APIs whose consumers are all updated in the same change need - `` is the `version` field of the root `package.json` when you write the guide, for example `v0.1.7-rc.2`. The guide describes the upgrade from that release to the next one. - `` is a kebab-case name of the changed surface, for example `profile-flag-rename`. -- No other files belong in the tree: no index, no Chinese counterpart, no attachments. +- Each item directory holds exactly `guide.md`, its Chinese counterpart `guide.zh.md`, and the pairing record `guide.i18n.yaml`: no index, no attachments. ## Maintenance @@ -36,18 +36,19 @@ While the root version is unchanged, the current version directory describes the Start from the [upgrade-guide template](../dsh-doc/templates/upgrade-guide.md). `verify-upgrade-guides` enforces: - frontmatter with exactly `kind: upgrade-guide` and a one-sentence `description` naming what breaks; add a key only together with its gate check, then backfill every guide; -- one `#` title, then exactly the `## Change` and `## Migration` sections, in that order; -- at most 500 words, frontmatter included. +- one `#` title, then exactly the `## Change` and `## Migration` sections in `guide.md`, and `## 变更` and `## 迁移` in `guide.zh.md`, in that order; +- at most 500 words in `guide.md`, frontmatter included. `Change` states the old and new behavior of the surface and who is affected. `Migration` lists ordered steps that name exact files, keys, commands, or symbols, and states how to confirm the migration worked. When a guide needs more than 500 words, the extra text belongs elsewhere: link source files or symbols instead of restating code, move rationale into an [Agent Note](../../notes/README.md), and turn repeated mechanical steps into a script or `dsh` command that the guide invokes. -Write English only, following [dsh-prose-standard](../dsh-prose-standard/SKILL.md). +Follow [dsh-prose-standard](../dsh-prose-standard/SKILL.md). Write the Chinese counterpart in the same change under the [bilingual pairing rules](../../../docs/AGENTS.md), then record the pair with `pnpm run verify-translation-pairing --write docs/upgrade-guide/v//guide.md`. ## Verify ```sh pnpm run verify-upgrade-guides +pnpm run verify-translation-pairing docs/upgrade-guide/v//guide.md pnpm run verify-md-links ``` diff --git a/.agents/skills/dsh-doc/SKILL.md b/.agents/skills/dsh-doc/SKILL.md index 0c97714672..5e32d4c07e 100644 --- a/.agents/skills/dsh-doc/SKILL.md +++ b/.agents/skills/dsh-doc/SKILL.md @@ -57,7 +57,7 @@ The `kind` frontmatter field selects exactly one README template. Every kind in - `persistence-change` → [templates/persistence-change.md](templates/persistence-change.md): a dated record in `docs/persistence-changes/` — acknowledge a detected type transition with per-root predecessors and generated after schemas. - `persistence-release` → [templates/persistence-release.md](templates/persistence-release.md): a pinned tag in `docs/persistence-changes/releases/` — compare reconstructed historical types without claiming compatibility acknowledgement. - `persistence-format` → [templates/persistence-format.md](templates/persistence-format.md): a historical Session format checkpoint in `docs/persistence-changes/historical-formats/`, with complete schemas; the current writer uses the existing catalog. -- `upgrade-guide` → [templates/upgrade-guide.md](templates/upgrade-guide.md): an English-only `docs/upgrade-guide/v//guide.md` — migration steps for one externally perceptible break, owned by [dsh-create-upgrade-guide](../dsh-create-upgrade-guide/SKILL.md). +- `upgrade-guide` → [templates/upgrade-guide.md](templates/upgrade-guide.md): `docs/upgrade-guide/v//guide.md` and its Chinese sibling — migration steps for one externally perceptible break, owned by [dsh-create-upgrade-guide](../dsh-create-upgrade-guide/SKILL.md). Open the template before writing and follow its skeleton and rules; it states what the kind is, how the page is structured, and the fact checks each section owes. Add a new kind only together with a distinct template file, a documented repository position or declared owner, and a focused check that maps documents to it. diff --git a/.agents/skills/dsh-doc/references/metadata-links-i18n.md b/.agents/skills/dsh-doc/references/metadata-links-i18n.md index 34e4cc55bb..4ea476085c 100644 --- a/.agents/skills/dsh-doc/references/metadata-links-i18n.md +++ b/.agents/skills/dsh-doc/references/metadata-links-i18n.md @@ -29,7 +29,7 @@ kind: "package-reference" ## The kind system -`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Files named `docs/upgrade-guide/v//guide.md` use `upgrade-guide` and have no Chinese sibling. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: +`kind` selects the document template directly; every kind maps to exactly one template that exists in this skill, and no template exists without a kind. Files named `docs/persistence-changes/YYYY-MM-DD-slug.md` use `persistence-change`; files named `docs/persistence-changes/releases/dsh-v*.md` use `persistence-release`. Files named `docs/persistence-changes/historical-formats/vN.md` use `persistence-format`. Their Chinese siblings use the same kind. Files named `docs/upgrade-guide/v//guide.md` and their `guide.zh.md` siblings use `upgrade-guide`. Folder READMEs are indexes, not records. Derive package README kinds mechanically, in this order: 1. The README is `packages/README.md` or `packages//README.md` → `package-group`. 2. The package manifest declares `dsh.bundle.patch` → `package-bundle`. @@ -45,7 +45,7 @@ kind: "package-reference" | `persistence-change` | `docs/persistence-changes/YYYY-MM-DD-slug.md` and its Chinese sibling | [persistence-change.md](../templates/persistence-change.md) | Historical acknowledgement: mechanically detected type changes, per-root predecessor references, compatibility decision, and generated after schemas. | | `persistence-release` | `docs/persistence-changes/releases/dsh-v*.md` and its Chinese sibling | [persistence-release.md](../templates/persistence-release.md) | Retrospective observation: pinned tag, adjacent before/after digests, actual version constants, and changed after schemas; no compatibility acknowledgement. | | `persistence-format` | `docs/persistence-changes/historical-formats/vN.md` and its Chinese sibling | [persistence-format.md](../templates/persistence-format.md) | Selected historical format: source evidence, complete root schemas and reachable types; the current writer uses the generated catalog. | -| `upgrade-guide` | `docs/upgrade-guide/v//guide.md`, English only | [upgrade-guide.md](../templates/upgrade-guide.md) | Upgrade path: one externally perceptible break from the directory's release to the next, with `Change` and `Migration` sections in at most 500 words. | +| `upgrade-guide` | `docs/upgrade-guide/v//guide.md` and its Chinese sibling | [upgrade-guide.md](../templates/upgrade-guide.md) | Upgrade path: one externally perceptible break from the directory's release to the next, with `Change` and `Migration` sections in at most 500 English words. | Before assigning `package-library` or `package-bundle`, inspect the facts: read `package.json` for `dsh.bundle.patch` and `src/index.ts` for the entry shape (`apply` export or a default service export is a plugin; a plain module API is a library). `dsh plugin --profile add ` installs any npm dependency, but the profile reconcile activates a layer only for a package that declares `dsh.bundle`; never present that command as an install path for a library or a plain plugin. The documentation check derives the expected kind from these same facts, rejects another value, and rejects `name`, `audience`, `tags`, and README-local `i18n` metadata. Add a new kind only with a distinct template, an unambiguous repository position or declared owner, and a focused check that maps documents to it. diff --git a/.agents/skills/dsh-doc/templates/upgrade-guide.md b/.agents/skills/dsh-doc/templates/upgrade-guide.md index 091495b81d..d47f7dc4ae 100644 --- a/.agents/skills/dsh-doc/templates/upgrade-guide.md +++ b/.agents/skills/dsh-doc/templates/upgrade-guide.md @@ -1,6 +1,6 @@ # Template: upgrade-guide -Use this kind for `docs/upgrade-guide/v//guide.md`. The guides are English-only and have no folder index. [dsh-create-upgrade-guide](../../dsh-create-upgrade-guide/SKILL.md) owns scope, placement, maintenance, and length rules; `pnpm run verify-upgrade-guides` enforces them. +Use this kind for `docs/upgrade-guide/v//guide.md` and its Chinese sibling `guide.zh.md`, whose sections are `## 变更` and `## 迁移`. The tree has no folder index. [dsh-create-upgrade-guide](../../dsh-create-upgrade-guide/SKILL.md) owns scope, placement, maintenance, and length rules; `pnpm run verify-upgrade-guides` enforces them. ## Frontmatter @@ -16,6 +16,8 @@ description: "The externally perceptible surface that breaks and what replaces i ````markdown # +English | [中文](guide.zh.md) + ## Change State the old and new behavior of the surface and who observes it. diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml index ddb3521a91..4c794f49bd 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.i18n.yaml @@ -12,8 +12,8 @@ en: 08386e96eebf06d9 zh: c6d2b41199b0c466 /bilingual-documentation/scope-and-exclusions: - en: 093bd091c9d46eda - zh: ab6cd07f94c740e4 + en: db70e758813d339b + zh: 3dcea283a08c47cb /bilingual-documentation/division-of-labor: en: 88097c97cb72f3ba zh: 78a18000626cc818 diff --git a/docs/i18n/README.md b/docs/i18n/README.md index ca970d6b2d..4c23fac67e 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -60,7 +60,6 @@ Generated English sources omit the language switcher that ordinary authored sour - `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction. - [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior. - [review-ownership/README.md](../../.github/review-ownership/README.md) — repository-internal approval policy maintained in English only. -- `docs/upgrade-guide/` — agent-facing upgrade guides maintained in English only. - `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them. **Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class. diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index edc6f972fc..a905a0bdae 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -62,7 +62,6 @@ - `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。 - [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。 - [review-ownership/README.md](../../.github/review-ownership/README.md):仓库内部审批策略,只以英文维护。 -- `docs/upgrade-guide/`:面向 agent 的升级指南,只以英文维护。 - `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。 **统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml new file mode 100644 index 0000000000..f0046f5fc9 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md +/: + en: 224d57a22f38ba26 + zh: 1472851dde2bf9c3 +/schedule-moves-into-the-automation-tasks-optional-bundle: + en: 138f715e3a260eb6 + zh: eff22703b2c79915 +/schedule-moves-into-the-automation-tasks-optional-bundle/change: + en: b1b250e677c23b10 + zh: b71c1591ea4f9ce5 +/schedule-moves-into-the-automation-tasks-optional-bundle/migration: + en: 656fea5dc45f8310 + zh: 8dfe16d2fdacf71a diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md index ccd08f683d..72ffb9f689 100644 --- a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.md @@ -5,6 +5,8 @@ description: "The Web composition no longer carries the Schedule rows; the Autom # Schedule moves into the Automation tasks optional bundle +English | [中文](guide.zh.md) + ## Change In v0.1.7-rc.2, `@deepseek-ai/dsh-web-app` carried the `time-context`, `schedule`, and `ui-schedule` rows with `disabled: true`. Turning Schedule on in the Plugins page, or by hand, wrote id-targeted overrides such as `- id: schedule` with `disabled: false` into `$DSH_HOME/profiles//cordis.patch.yml` or a `--patch` overlay. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md new file mode 100644 index 0000000000..46483a1175 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/schedule-optional-bundle/guide.zh.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "Web 组合不再包含 Schedule 的三行;改由“自动化任务”可选 bundle 插入。" +--- + +# Schedule 移入“自动化任务”可选 bundle + +[English](guide.md) | 中文 + +## 变更 + +在 v0.1.7-rc.2 中,`@deepseek-ai/dsh-web-app` 以 `disabled: true` 包含 `time-context`、`schedule` 和 `ui-schedule` 三行。在插件管理页或手动开启 Schedule,会向 `$DSH_HOME/profiles//cordis.patch.yml` 或 `--patch` overlay 写入按 id 定位的覆盖项,例如 `- id: schedule` 加 `disabled: false`。 + +下一版本从 Web 组合中移除这三行。插件管理页“官方”分组中的“自动化任务”,即 `@deepseek-ai/dsh-experimental-schedule-bundle`,负责插入它们。每个安装都自带该 bundle,默认关闭。 + +按 id 开启 Schedule 的 profile 升级后会失去 Schedule:loader 警告 `patch: entry schedule not found`(`time-context` 和 `ui-schedule` 同理),`schedule_*` 工具和自动化任务页面消失,已存储的提醒不再投递。已存储的任务和投递记录仍保留在磁盘上。 + +## 迁移 + +1. 打开插件管理页,在“官方”分组中找到“自动化任务”并开启。开关会把 `@deepseek-ai/dsh-experimental-schedule-bundle` 追加到 `$DSH_HOME/profiles//package.json` 的 `dsh.profile.bundles`;手动维护的 profile 需自行添加该条目。 +2. 保留为 `schedule`、`time-context` 或 `ui-schedule` 设置其他字段的覆盖项,例如 `deliveryHistoryDays`;bundle 插入这些行后它们重新生效。只设置 `disabled: false` 的覆盖项已无作用,可以删除。 +3. 确认:重启 `dsh web`,检查启动日志中这三个 id 不再出现 `patch: entry ... not found` 警告,且侧栏显示“自动化任务”并列出之前存储的提醒。 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md deleted file mode 100644 index e96eaf087f..0000000000 --- a/docs/upgrade-guide/v0.1.7-rc.2/session-log-otel-collector/guide.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -kind: upgrade-guide -description: "Feedback Session-log uploads go to a new collector host and require the @deepseek-ai/dsh-otel service." ---- - -# Session-log uploads use the shared OTel service and a new collector - -## Change - -In v0.1.7-rc.2, `@deepseek-ai/dsh-session-telemetry-otel` owned its own OTLP transport and sent feedback-authorized Session logs to `https://harness-telemetry.deepseeksvc.com/v1/logs`. - -From the next release: - -- The plugin injects the `otel` service from the new `@deepseek-ai/dsh-otel` plugin. `@deepseek-ai/dsh-base` mounts it as the `otel` row; a composition that mounts `session-telemetry-otel` without that row leaves the plugin inactive, and feedback uploads stop. -- The default collector is `https://dsh-otel-collector.deepseeksvc.com/v1/logs`. `DSH_TELEMETRY_OTLP_URL` still overrides it. -- Uploads are split into requests of at most 4,000,000 uncompressed bytes (`maxRequestBytes`). - -Profiles that extend `@deepseek-ai/dsh-base` and networks with unrestricted egress need no action. Persisted Session formats are unchanged. - -## Migration - -1. If a custom composition lists `session-telemetry-otel` without `@deepseek-ai/dsh-base`, add the row before it: - - ```yaml - - id: otel - name: '@deepseek-ai/dsh-otel' - ``` - -2. If an egress allowlist or proxy rule names `harness-telemetry.deepseeksvc.com`, add `dsh-otel-collector.deepseeksvc.com`. A deployment that points `DSH_TELEMETRY_OTLP_URL` at its own collector keeps its setting. -3. Confirm: send feedback with Session-log upload enabled and check that the configured collector receives the request; for a self-hosted collector, check its receive log. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml new file mode 100644 index 0000000000..89d7925446 --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md +/: + en: 7d7e527ed3c87db1 + zh: 4654dab85b66bcb0 +/legacy-normal-work-details-display-as-detailed: + en: 3c88b0b06094c162 + zh: 9ae8ff49975824a9 +/legacy-normal-work-details-display-as-detailed/change: + en: 0e2287aa768141f3 + zh: 035992a94129fd9e +/legacy-normal-work-details-display-as-detailed/migration: + en: 6078ccce1ceacfd6 + zh: 6a394cf5259c9965 diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md index b0af748cad..cfb2be2793 100644 --- a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.md @@ -5,6 +5,8 @@ description: "The legacy ui-chat.transcriptView value normal, and an unset value # Legacy `normal` work details display as Detailed +English | [中文](guide.zh.md) + ## Change The Host setting `ui-chat.transcriptView` selects Settings → General → Work details. In v0.1.7-rc.2 the client displayed the legacy saved value `normal` as `standard`, and every client used `standard` when the setting was missing, `null`, or invalid. diff --git a/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md new file mode 100644 index 0000000000..4bcc77d18e --- /dev/null +++ b/docs/upgrade-guide/v0.1.7-rc.2/transcript-view-legacy-normal/guide.zh.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "旧值 ui-chat.transcriptView: normal,以及非 Desktop Web 中未设置的值,改为按“详细”而非“标准”展示。" +--- + +# 旧值 `normal` 的工作步骤展示改为“详细” + +[English](guide.md) | 中文 + +## 变更 + +Host 设置 `ui-chat.transcriptView` 对应设置 → 通用设置 → 工作步骤展示。在 v0.1.7-rc.2 中,客户端把旧保存值 `normal` 按 `standard` 展示;设置缺失、为 `null` 或无效时,所有客户端都使用 `standard`。 + +从下一版本起: + +- 保存的 `normal` 在 Desktop 和 Web 中均按 `detailed` 展示,磁盘中的保存值不会被改写。 +- 设置缺失、为 `null` 或无效时,非 Desktop Web(npm 安装的 `dsh web`)按 `detailed` 展示;Desktop 仍使用 `standard`。 +- 已保存的 `compact`、`standard`、`detailed` 和 `verbose` 保持不变。 + +依赖旧读取方式的用户会看到运行中轮次的过程组正文直接展开,而不是收起的摘要。 + +## 迁移 + +1. 如需保留原有展示,打开设置 → 通用设置 → 工作步骤展示并选择“标准”。客户端会把 `standard` 保存到 `ui-chat.transcriptView`,此后的默认值变化不影响该值。 +2. 确认:发起一个调用工具的轮次,检查其过程组显示收起的摘要和实时任务详情。 diff --git a/scripts/translation-pairing.manifest.json b/scripts/translation-pairing.manifest.json index fe517aa995..f9bb1549a2 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -8,7 +8,6 @@ "docs/cordis-api/inherited.md", "docs/i18n/style-samples.md", "docs/i18n/terminology.md", - "docs/i18n/translation-prompt.md", - "docs/upgrade-guide/" + "docs/i18n/translation-prompt.md" ] } diff --git a/scripts/verify-upgrade-guides.spec.ts b/scripts/verify-upgrade-guides.spec.ts index 07b1e516aa..6d92157e8a 100644 --- a/scripts/verify-upgrade-guides.spec.ts +++ b/scripts/verify-upgrade-guides.spec.ts @@ -34,6 +34,11 @@ const VALID = [ '', ].join('\n') +const VALID_ZH = VALID + .replace('description: "The `--preset` flag is removed."', 'description: "移除 `--preset` 参数。"') + .replace('## Change', '## 变更') + .replace('## Migration', '## 迁移') + function fixtureRoot(files: Record): string { const root = mkdtempSync(join(tmpdir(), 'dsh-upgrade-guides-')) roots.push(root) @@ -46,30 +51,32 @@ function fixtureRoot(files: Record): string { } describe('upgrade guide gate', () => { - it('accepts an absent tree and conforming guides, including prerelease versions and CRLF files', () => { + it('accepts an absent tree and conforming guide pairs, including prerelease versions and CRLF files', () => { expect(collectUpgradeGuideViolations(fixtureRoot({}))).toEqual([]) expect(collectUpgradeGuideViolations(fixtureRoot({ 'v0.1.7-rc.2/profile-flag/guide.md': VALID, + 'v0.1.7-rc.2/profile-flag/guide.zh.md': VALID_ZH, + 'v0.1.7-rc.2/profile-flag/guide.i18n.yaml': 'not parsed by this gate\n', 'v1.0.0/crlf-item/guide.md': VALID.replaceAll('\n', '\r\n'), }))).toEqual([]) }) - it('rejects files outside the version/item/guide.md layout', () => { + it('rejects files outside the version/item/guide pair layout', () => { expect(collectUpgradeGuideViolations(fixtureRoot({ 'README.md': VALID, '0.1.7/item/guide.md': VALID, 'v0.1/item/guide.md': VALID, 'v0.1.7/Item_Name/guide.md': VALID, - 'v0.1.7/item/guide.zh.md': VALID, + 'v0.1.7/item/notes.md': VALID, 'v0.1.7/item/extra/guide.md': VALID, }))).toEqual([ 'docs/upgrade-guide/0.1.7/item/guide.md', 'docs/upgrade-guide/README.md', 'docs/upgrade-guide/v0.1.7/Item_Name/guide.md', 'docs/upgrade-guide/v0.1.7/item/extra/guide.md', - 'docs/upgrade-guide/v0.1.7/item/guide.zh.md', + 'docs/upgrade-guide/v0.1.7/item/notes.md', 'docs/upgrade-guide/v0.1/item/guide.md', - ].map(path => `${path}: only v//guide.md files belong in docs/upgrade-guide`)) + ].map(path => `${path}: only v//guide.{md,zh.md,i18n.yaml} files belong in docs/upgrade-guide`)) }) it('rejects missing, extra, or invalid frontmatter fields', () => { @@ -102,11 +109,22 @@ describe('upgrade guide gate', () => { expect(violations[0]).toBe('docs/upgrade-guide/v0.1.7/extra/guide.md: headings must be one "#" title followed by "## Change", "## Migration"; got "# `--profile` replaces `--preset`", "## Change", "## Migration", "## References"') }) - it('rejects a guide over 500 words, counting frontmatter', () => { + it('checks the Chinese sibling against its own section names and metadata', () => { + expect(collectUpgradeGuideViolations(fixtureRoot({ + 'v0.1.7/english-headings/guide.zh.md': VALID, + 'v0.1.7/wrong-kind/guide.zh.md': VALID_ZH.replace('kind: upgrade-guide', 'kind: guide'), + }))).toEqual([ + 'docs/upgrade-guide/v0.1.7/english-headings/guide.zh.md: headings must be one "#" title followed by "## 变更", "## 迁移"; got "# `--profile` replaces `--preset`", "## Change", "## Migration"', + 'docs/upgrade-guide/v0.1.7/wrong-kind/guide.zh.md: kind must be upgrade-guide', + ]) + }) + + it('rejects an English guide over 500 words, counting frontmatter, and leaves the Chinese sibling uncounted', () => { const words = VALID.split(/\s+/u).filter(Boolean).length expect(collectUpgradeGuideViolations(fixtureRoot({ 'v0.1.7/at-limit/guide.md': `${VALID}${'word '.repeat(500 - words)}\n`, 'v0.1.7/over-limit/guide.md': `${VALID}${'word '.repeat(501 - words)}\n`, + 'v0.1.7/over-limit/guide.zh.md': `${VALID_ZH}${'词 '.repeat(600)}\n`, }))).toEqual(['docs/upgrade-guide/v0.1.7/over-limit/guide.md: 501 words exceeds the 500-word ceiling']) }) }) diff --git a/scripts/verify-upgrade-guides.ts b/scripts/verify-upgrade-guides.ts index 9a776013af..bb067cb41f 100644 --- a/scripts/verify-upgrade-guides.ts +++ b/scripts/verify-upgrade-guides.ts @@ -1,6 +1,7 @@ /** * Enforce the `docs/upgrade-guide/v//guide.md` layout, metadata, * sections, and word ceiling defined by the `dsh-create-upgrade-guide` skill. + * `verify-translation-pairing` owns the Chinese sibling's pairing record. * @module scripts/verify-upgrade-guides */ @@ -11,17 +12,20 @@ import { fromMarkdown } from 'mdast-util-from-markdown' const ROOT = resolve(import.meta.dirname, '..') const GUIDE_ROOT = 'docs/upgrade-guide' -const MAX_WORDS = 500 const FRONTMATTER_KEYS = ['description', 'kind'] -const SECTIONS = ['Change', 'Migration'] +/** Required `##` sections for each guide language; the word ceiling applies to the English source. */ +const LANGUAGES = { + 'guide.md': { sections: ['Change', 'Migration'], maxWords: 500 }, + 'guide.zh.md': { sections: ['变更', '迁移'], maxWords: undefined }, +} as const const SEMVER = String.raw`(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?` -const GUIDE_PATH = new RegExp(`^v${SEMVER}/[a-z0-9]+(?:-[a-z0-9]+)*/guide\\.md$`, 'u') +const GUIDE_PATH = new RegExp(`^v${SEMVER}/[a-z0-9]+(?:-[a-z0-9]+)*/(guide\\.md|guide\\.zh\\.md|guide\\.i18n\\.yaml)$`, 'u') /** Return one guide's frontmatter and structure violations. */ -function guideViolations(source: string): string[] { +function guideViolations(source: string, language: typeof LANGUAGES[keyof typeof LANGUAGES]): string[] { const violations: string[] = [] const words = source.split(/\s+/u).filter(Boolean).length - if (words > MAX_WORDS) violations.push(`${String(words)} words exceeds the ${String(MAX_WORDS)}-word ceiling`) + if (language.maxWords !== undefined && words > language.maxWords) violations.push(`${String(words)} words exceeds the ${String(language.maxWords)}-word ceiling`) const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/u.exec(source) if (frontmatter === null) return [...violations, 'must start with YAML frontmatter'] @@ -36,7 +40,7 @@ function guideViolations(source: string): string[] { const body = source.slice(frontmatter[0].length) const outline = fromMarkdown(body).children .flatMap(node => node.type === 'heading' && node.depth <= 2 ? [body.slice(node.position?.start.offset, node.position?.end.offset)] : []) - const expected = SECTIONS.map(section => `## ${section}`) + const expected = language.sections.map(section => `## ${section}`) if (outline.length !== expected.length + 1 || !outline[0]?.startsWith('# ') || outline.slice(1).join('\n') !== expected.join('\n')) { violations.push(`headings must be one "#" title followed by ${expected.map(heading => `"${heading}"`).join(', ')}; got ${outline.map(heading => `"${heading}"`).join(', ') || 'none'}`) } @@ -58,11 +62,14 @@ export function collectUpgradeGuideViolations(root: string): string[] { .sort() for (const file of files) { const path = `${GUIDE_ROOT}/${file}` - if (!GUIDE_PATH.test(file)) { - violations.push(`${path}: only v//guide.md files belong in ${GUIDE_ROOT}`) + const name = GUIDE_PATH.exec(file)?.[1] + if (name === undefined) { + violations.push(`${path}: only v//guide.{md,zh.md,i18n.yaml} files belong in ${GUIDE_ROOT}`) continue } - for (const violation of guideViolations(readFileSync(join(directory, file), 'utf8'))) violations.push(`${path}: ${violation}`) + if (name === 'guide.md' || name === 'guide.zh.md') { + for (const violation of guideViolations(readFileSync(join(directory, file), 'utf8'), LANGUAGES[name])) violations.push(`${path}: ${violation}`) + } } return violations }