docs: make upgrade guides bilingual and drop the OTel guide

Upgrade guides now pair guide.md with guide.zh.md and guide.i18n.yaml under
the ordinary translation-pairing gate. verify-upgrade-guides checks the
Chinese sibling's metadata and its 变更/迁移 sections; the 500-word ceiling
applies to the English source. Remove the session-log-otel-collector guide.
This commit is contained in:
turtle1999 committed 2026-09-29 10:34:29 +08:00
1 parent 9126818b7f
commit 61d4ab0b6c
17 files changed
+137 -59

No files matched your search

@@ -25,7 +25,7 @@ Pre-stable internal APIs whose consumers are all updated in the same change need
- `<version>` 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.
- `<item>` 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<version>/<item>/guide.md`.
## Verify
```sh
pnpm run verify-upgrade-guides
pnpm run verify-translation-pairing docs/upgrade-guide/v<version>/<item>/guide.md
pnpm run verify-md-links
```
+1 -1
View File
@@ -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<version>/<item>/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<version>/<item>/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.
@@ -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<version>/<item>/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<version>/<item>/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/<group>/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<version>/<item>/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<version>/<item>/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 <name> add <package>` 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.
@@ -1,6 +1,6 @@
# Template: upgrade-guide
Use this kind for `docs/upgrade-guide/v<version>/<item>/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<version>/<item>/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
# <Specific break, for example: `--profile` replaces `--preset`>
English | [中文](guide.zh.md)
## Change
State the old and new behavior of the surface and who observes it.
+2 -2
View File
@@ -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
-1
View File
@@ -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.
-1
View File
@@ -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 专用政策类别。
@@ -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
@@ -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/<name>/cordis.patch.yml` or a `--patch` overlay.
@@ -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/<name>/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/<name>/package.json` 的 `dsh.profile.bundles`;手动维护的 profile 需自行添加该条目。
2. 保留为 `schedule`、`time-context` 或 `ui-schedule` 设置其他字段的覆盖项,例如 `deliveryHistoryDays`;bundle 插入这些行后它们重新生效。只设置 `disabled: false` 的覆盖项已无作用,可以删除。
3. 确认:重启 `dsh web`,检查启动日志中这三个 id 不再出现 `patch: entry ... not found` 警告,且侧栏显示“自动化任务”并列出之前存储的提醒。
@@ -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.
@@ -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
@@ -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.
@@ -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. 确认:发起一个调用工具的轮次,检查其过程组显示收起的摘要和实时任务详情。
+1 -2
View File
@@ -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"
]
}
+24 -6
View File
@@ -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, string>): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-upgrade-guides-'))
roots.push(root)
@@ -46,30 +51,32 @@ function fixtureRoot(files: Record<string, string>): 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<semver>/<kebab-case-item>/guide.md files belong in docs/upgrade-guide`))
].map(path => `${path}: only v<semver>/<kebab-case-item>/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'])
})
})
+16 -9
View File
@@ -1,6 +1,7 @@
/**
* Enforce the `docs/upgrade-guide/v<version>/<item>/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<semver>/<kebab-case-item>/guide.md files belong in ${GUIDE_ROOT}`)
const name = GUIDE_PATH.exec(file)?.[1]
if (name === undefined) {
violations.push(`${path}: only v<semver>/<kebab-case-item>/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
}