Merge pull request #5389 from deepseek-harness/docs/upgrade-guide-structure
docs: add upgrade guide structure, skill, and gate
This commit is contained in:
commit
17da46e4f3
18 files changed
+435
-4
No files matched your search
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: dsh-create-upgrade-guide
|
||||
description: Use when a deepseek-harness change breaks an externally perceptible surface (CLI, profiles, cordis.yml or settings keys, persisted user data, SDK or wire APIs, published package entry points), or when changing a surface that an unreleased upgrade guide already covers, to write or update docs/upgrade-guide/v<version>/<item>/guide.md.
|
||||
---
|
||||
|
||||
# DSH Create Upgrade Guide
|
||||
|
||||
An upgrade guide tells a reader who runs release vT what to change to run the next release. Write it in the change that introduces the break, as soon as the break is recognized; do not defer it to release preparation.
|
||||
|
||||
## Scope
|
||||
|
||||
Write a guide only for breaks that someone outside the repository can observe after upgrading:
|
||||
|
||||
- `dsh` commands, flags, and profile names;
|
||||
- `cordis.yml`, patch, overlay, and settings keys or their accepted values;
|
||||
- persisted user data: Session logs, SQLite stores, and user data directories;
|
||||
- TypeScript and Python SDK exports, JSON-RPC, HTTP, and ACP messages;
|
||||
- published package names and entry points.
|
||||
|
||||
Pre-stable internal APIs whose consumers are all updated in the same change need no guide. A Session-format change still follows [type acknowledgements](../../../docs/cookbook/reviewing-persistence-type-changes.md); its guide states only the reader's actions and links that record.
|
||||
|
||||
## Location
|
||||
|
||||
`docs/upgrade-guide/v<version>/<item>/guide.md`
|
||||
|
||||
- `<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`.
|
||||
- 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
|
||||
|
||||
While the root version is unchanged, the current version directory describes the delta from vT to current `master`. When a later change touches a surface a guide there covers, update that guide in the same change; delete the guide when the break is reverted. After the version bump, guides in older version directories are frozen; a new break goes into the new version directory. Never write a guide spanning more than one release.
|
||||
|
||||
## Format
|
||||
|
||||
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 `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.
|
||||
|
||||
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
|
||||
```
|
||||
@@ -57,6 +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): `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.
|
||||
|
||||
@@ -67,7 +68,7 @@ These rules decide what a section may say. They apply to every authored human-fa
|
||||
- **Summary says what the subject does.** The opening `Summary` and the user-facing sections describe what a user or agent can DO with the subject — outcomes, benefits, when to choose it, main cost — never its role, type, or internal identity. In a package Summary, “what it is” means only its reader-visible capability, not its Cordis role, registrations, or internal components. Omit source identifiers unless the reader directly uses them in configuration, a command, or a public API. "The seam registers `ctx.x` and appends `x/event` records" is identity narration; "you can save a note per message and it survives restarts" is what it does.
|
||||
- **Developer sections explain, never enumerate.** Folded implementation content covers the overall design concept, architecture, and hand-waving dataflow — enough to understand how the package works — and links code for exact detail. No full API catalogs, exhaustive column lists, event-payload enumerations, or JSDoc restatement inside the folds.
|
||||
- **Dev Note is the only slop zone.** Partial ideas, scratches, undecided directions, measured artifacts, and working hypotheses live only in the final Dev Note, marked explicitly non-authoritative. Every other section is polished, current-state prose.
|
||||
- **Current state only.** Ordinary documentation describes the current codebase. Dedicated `persistence-change`, `persistence-release`, and `persistence-format` records retain historical type evidence; release comparisons and format checkpoints describe observed history without asserting compatibility. Agent Notes and postmortems retain their own historical scope.
|
||||
- **Current state only.** Ordinary documentation describes the current codebase. Dedicated `persistence-change`, `persistence-release`, and `persistence-format` records retain historical type evidence; release comparisons and format checkpoints describe observed history without asserting compatibility. An `upgrade-guide` describes the change from its version directory's release to the next one. Agent Notes and postmortems retain their own historical scope.
|
||||
- **Use controlled technical English.** Give each sentence an explicit actor and one main action when ambiguity can change behavior. Reuse one term per concept, prefer direct verbs, split stacked instructions and conditions, and preserve modality and exceptions. Apply the non-certified, ASD-STE100-inspired discipline in [the page-style reference](references/style.md#controlled-technical-english). Do not force a shorter sentence when precision would fall.
|
||||
|
||||
## Quality criteria
|
||||
|
||||
@@ -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. 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,6 +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` 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Template: upgrade-guide
|
||||
|
||||
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
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The externally perceptible surface that breaks and what replaces it."
|
||||
---
|
||||
```
|
||||
|
||||
## Skeleton
|
||||
|
||||
````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.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Name the exact file, key, command, or symbol to change, with a before/after snippet when it is shorter than prose.
|
||||
2. State how to confirm the migration worked.
|
||||
````
|
||||
@@ -8,6 +8,8 @@ Public APIs are pre-stable; update every consumer. Follow [version/status](docs/
|
||||
|
||||
Acknowledge [declared persistence-type changes](docs/cookbook/reviewing-persistence-type-changes.md).
|
||||
|
||||
Record each externally perceptible breaking change immediately in an [upgrade guide](.agents/skills/dsh-create-upgrade-guide/SKILL.md).
|
||||
|
||||
**Application launch.** Only `dsh` profiles launch supported Node apps; package bins, demos, and public SDK argv escapes are forbidden ([rule](docs/architecture.md#application-launch)).
|
||||
|
||||
## Repository layout
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ When the gate goes red:
|
||||
2. **Condense** content that belongs here but can be shorter.
|
||||
3. **Raise** the ceiling only when the words need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug.
|
||||
|
||||
Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,950; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 750 and this file ≤ 1,320; `packages/README.md` ≤ 994; plus `cordis-primer.md` 600, `defensive-patterns.md` 550, `testing.md` 1,300, `examples/AGENTS.md` 310. Review governs unbudgeted tiers.
|
||||
Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room. Targets: root `AGENTS.md` ≤ 1,960; `architecture.md` ≤ 2,400; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 750 and this file ≤ 1,320; `packages/README.md` ≤ 994; plus `cordis-primer.md` 600, `defensive-patterns.md` 550, `testing.md` 1,300, `examples/AGENTS.md` 310. Review governs unbudgeted tiers.
|
||||
|
||||
## The slop checklist
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The Web composition no longer carries the Schedule rows; the Automation tasks optional bundle inserts them."
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
The next release removes the three rows from the Web composition. `@deepseek-ai/dsh-experimental-schedule-bundle`, shown as Automation tasks in the Plugins page's Official group, inserts them. Every installation ships the bundle switched off.
|
||||
|
||||
Profiles that enabled Schedule by id lose it after upgrading: the loader warns `patch: entry schedule not found` (and the same for `time-context` and `ui-schedule`), the `schedule_*` tools and the Automation tasks page disappear, and stored reminders stop being delivered. Stored tasks and delivery records remain on disk.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Open Plugins, find Automation tasks in the Official group, and switch it on. The switch appends `@deepseek-ai/dsh-experimental-schedule-bundle` to `dsh.profile.bundles` in `$DSH_HOME/profiles/<name>/package.json`; for a profile edited by hand, add that entry yourself.
|
||||
2. Keep existing overrides that set other fields on `schedule`, `time-context`, or `ui-schedule`, such as `deliveryHistoryDays`; they apply again once the bundle inserts the rows. Overrides that only set `disabled: false` are redundant and can be deleted.
|
||||
3. Confirm: restart `dsh web`, check that startup logs no `patch: entry ... not found` warning for the three ids, and that the sidebar shows Automation tasks with the previously stored reminders.
|
||||
@@ -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` 警告,且侧栏显示“自动化任务”并列出之前存储的提醒。
|
||||
@@ -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
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The legacy ui-chat.transcriptView value normal, and an unset value in non-Desktop Web, now display Detailed instead of Standard."
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
From the next release:
|
||||
|
||||
- A saved `normal` displays as `detailed` on Desktop and Web. The saved value on disk is not rewritten.
|
||||
- A missing, `null`, or invalid value displays as `detailed` in non-Desktop Web (the npm `dsh web` install). Desktop still uses `standard`.
|
||||
- Saved `compact`, `standard`, `detailed`, and `verbose` values are unchanged.
|
||||
|
||||
Users who relied on the old reading see running Turns with process-group bodies expanded instead of collapsed summaries.
|
||||
|
||||
## Migration
|
||||
|
||||
1. To keep the previous presentation, open Settings → General → Work details and choose Standard. The client saves `standard` to `ui-chat.transcriptView`, which every later default change leaves untouched.
|
||||
2. Confirm: start a Turn that calls tools and check that its process group shows a collapsed summary with live task detail.
|
||||
@@ -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. 确认:发起一个调用工具的轮次,检查其过程组显示收起的摘要和实时任务详情。
|
||||
@@ -118,6 +118,7 @@
|
||||
"verify-archived-agent-notes": "tsx scripts/verify-archived-agent-notes.ts",
|
||||
"verify-type-equiv": "tsx scripts/verify-type-equiv.ts",
|
||||
"verify-skill-invocation-metadata": "tsx scripts/verify-skill-invocation-metadata.ts",
|
||||
"verify-upgrade-guides": "tsx scripts/verify-upgrade-guides.ts",
|
||||
"verify-translation-prompt": "tsx scripts/verify-translation-prompt.ts",
|
||||
"verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts",
|
||||
"test:docs": "tsx scripts/run-gates.ts doc-quick",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1950,
|
||||
"AGENTS.md": 1960,
|
||||
"docs/AGENTS.md": 1320,
|
||||
"docs/architecture.md": 2410,
|
||||
"docs/cordis-primer.md": 600,
|
||||
|
||||
@@ -45,6 +45,7 @@ const KIND_TEMPLATES: Readonly<Record<string, string>> = {
|
||||
'persistence-change': '.agents/skills/dsh-doc/templates/persistence-change.md',
|
||||
'persistence-release': '.agents/skills/dsh-doc/templates/persistence-release.md',
|
||||
'persistence-format': '.agents/skills/dsh-doc/templates/persistence-format.md',
|
||||
'upgrade-guide': '.agents/skills/dsh-doc/templates/upgrade-guide.md',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -837,6 +837,7 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('skill-invocation-metadata', 'verify-skill-invocation-metadata', { label: 'skill invocation metadata', quick: true }),
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt', quick: true }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets', quick: true }),
|
||||
pnpmScript('upgrade-guides', 'verify-upgrade-guides', { label: 'upgrade guides', quick: true }),
|
||||
pnpmExec('doc-standard-tests', ['vitest', 'run', 'scripts/doc-standard.spec.ts'], {
|
||||
label: 'documentation standard tests',
|
||||
quick: true,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectUpgradeGuideViolations } from './verify-upgrade-guides.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const VALID = [
|
||||
'---',
|
||||
'kind: upgrade-guide',
|
||||
'description: "The `--preset` flag is removed."',
|
||||
'---',
|
||||
'',
|
||||
'# `--profile` replaces `--preset`',
|
||||
'',
|
||||
'## Change',
|
||||
'',
|
||||
'Old and new behavior.',
|
||||
'',
|
||||
'### Detail',
|
||||
'',
|
||||
'```md',
|
||||
'# not a heading',
|
||||
'```',
|
||||
'',
|
||||
'## Migration',
|
||||
'',
|
||||
'1. Rename the flag.',
|
||||
'',
|
||||
].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)
|
||||
for (const [file, source] of Object.entries(files)) {
|
||||
const path = join(root, 'docs/upgrade-guide', file)
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, source)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
describe('upgrade guide gate', () => {
|
||||
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 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/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/notes.md',
|
||||
'docs/upgrade-guide/v0.1/item/guide.md',
|
||||
].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', () => {
|
||||
const body = VALID.slice(VALID.indexOf('# '))
|
||||
expect(collectUpgradeGuideViolations(fixtureRoot({
|
||||
'v0.1.7/none/guide.md': body,
|
||||
'v0.1.7/list/guide.md': `---\n- a\n---\n${body}`,
|
||||
'v0.1.7/fields/guide.md': `---\nkind: persistence-change\ndescription: ""\nversion: v0.1.7\n---\n${body}`,
|
||||
}))).toEqual([
|
||||
'docs/upgrade-guide/v0.1.7/fields/guide.md: frontmatter keys must be exactly description, kind; got description, kind, version',
|
||||
'docs/upgrade-guide/v0.1.7/fields/guide.md: kind must be upgrade-guide',
|
||||
'docs/upgrade-guide/v0.1.7/fields/guide.md: description must be a non-empty string',
|
||||
'docs/upgrade-guide/v0.1.7/list/guide.md: frontmatter must be a YAML object',
|
||||
'docs/upgrade-guide/v0.1.7/none/guide.md: must start with YAML frontmatter',
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects a missing title and missing, renamed, extra, or reordered sections', () => {
|
||||
const cases = {
|
||||
'no-title': VALID.replace('# `--profile` replaces `--preset`\n', ''),
|
||||
'two-titles': VALID.replace('## Migration', '# Migration'),
|
||||
'renamed': VALID.replace('## Change', '## Changes'),
|
||||
'extra': `${VALID}\n## References\n`,
|
||||
'reordered': VALID.replace('## Change', '## Placeholder').replace('## Migration', '## Change').replace('## Placeholder', '## Migration'),
|
||||
}
|
||||
const violations = collectUpgradeGuideViolations(fixtureRoot(Object.fromEntries(
|
||||
Object.entries(cases).map(([item, source]) => [`v0.1.7/${item}/guide.md`, source]),
|
||||
)))
|
||||
expect(violations.map(violation => violation.split('/')[3])).toEqual(['extra', 'no-title', 'renamed', 'reordered', 'two-titles'])
|
||||
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('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'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { JSON_SCHEMA, load } from 'js-yaml'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, '..')
|
||||
const GUIDE_ROOT = 'docs/upgrade-guide'
|
||||
const FRONTMATTER_KEYS = ['description', 'kind']
|
||||
/** 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|guide\\.zh\\.md|guide\\.i18n\\.yaml)$`, 'u')
|
||||
|
||||
/** Return one guide's frontmatter and structure violations. */
|
||||
function guideViolations(source: string, language: typeof LANGUAGES[keyof typeof LANGUAGES]): string[] {
|
||||
const violations: string[] = []
|
||||
const words = source.split(/\s+/u).filter(Boolean).length
|
||||
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']
|
||||
const metadata = load(frontmatter[1] as string, { schema: JSON_SCHEMA })
|
||||
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) return [...violations, 'frontmatter must be a YAML object']
|
||||
const fields = metadata as Record<string, unknown>
|
||||
const keys = Object.keys(fields).sort()
|
||||
if (keys.join() !== FRONTMATTER_KEYS.join()) violations.push(`frontmatter keys must be exactly ${FRONTMATTER_KEYS.join(', ')}; got ${keys.join(', ') || 'none'}`)
|
||||
if (fields.kind !== 'upgrade-guide') violations.push('kind must be upgrade-guide')
|
||||
if (typeof fields.description !== 'string' || fields.description.trim() === '') violations.push('description must be a non-empty 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 = 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'}`)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
/**
|
||||
* Report upgrade-guide tree violations.
|
||||
* @param root - Repository root containing `docs/upgrade-guide`.
|
||||
* @returns diagnostics prefixed by the repository-relative file path; empty when the tree is absent.
|
||||
*/
|
||||
export function collectUpgradeGuideViolations(root: string): string[] {
|
||||
const directory = join(root, GUIDE_ROOT)
|
||||
if (!existsSync(directory)) return []
|
||||
const violations: string[] = []
|
||||
const files = readdirSync(directory, { recursive: true, withFileTypes: true })
|
||||
.filter(entry => !entry.isDirectory())
|
||||
.map(entry => join(entry.parentPath, entry.name).slice(directory.length + 1).replaceAll('\\', '/'))
|
||||
.sort()
|
||||
for (const file of files) {
|
||||
const path = `${GUIDE_ROOT}/${file}`
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
const violations = collectUpgradeGuideViolations(ROOT)
|
||||
if (violations.length > 0) {
|
||||
process.stderr.write('verify-upgrade-guides: violations found (see .agents/skills/dsh-create-upgrade-guide/SKILL.md):\n')
|
||||
for (const violation of violations) process.stderr.write(` ${violation}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
process.stdout.write('verify-upgrade-guides: upgrade guides conform.\n')
|
||||
}
|
||||
Reference in new issue
Block a user