docs: deduplicate paired code-block checks

This commit is contained in:
Tianyi Cui committed 2026-07-26 02:52:48 +08:00
1 parent 633ceaf41b
commit f2cf29bd04
18 files changed
+175 -971

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-20-core-data-structures-catalog.md: d7e9d3d9b14fe723e3396c8167fe714b7613e2b5
2026-06-20-core-data-structures-catalog.zh.md: 8d2f16a46216cba8df0539be0abd2f1ad840eec3
2026-06-20-core-data-structures-catalog.md: ef100f96b06c454cfd1ec092cc7fd23e712bdf7a
2026-06-20-core-data-structures-catalog.zh.md: 4ace2b8c8a6b08e7721c1df8003ccfbdb128daf1
@@ -33,7 +33,7 @@ The durability requirement was specific: the doc shows the **literal** current t
- Complete type declarations and their JSDoc are pasted verbatim into a dedicated ` ```ts type-equiv ` fence. A concise ` ```ts public-api ` fence carries the source-equivalent ambient projection for a class whose implementation bodies do not belong in the catalog. `doc-typecheck` recognizes both and skips them (the bare declarations are not standalone-compilable), and **excludes them from the opt-out ratio** — they are a separately-checked category, not unchecked sketches.
- A new `scripts/verify-type-equiv.ts` extracts each block via the TypeScript parser and asserts that its declaration structure and every JSDoc comment match the declared symbol, ignoring only formatting whitespace and non-JSDoc comments. Ordinary blocks retain the complete declaration. A `public-api` projection retains a class's public fields, constructor, accessors, and methods with their original JSDoc while removing implementation bodies and private or protected members. This is chosen over a compiled `_Check` assertion because source names and documentation identity, not assignability, are the properties the catalog preserves.
- Provenance lives in a central `scripts/type-equiv.manifest.json` (`{ doc, symbol, source }` entries), **not** in directive comments in the prose. The script enforces a **1:1 correspondence**: every type-equiv block has exactly one manifest entry and vice versa, so a block can never be silently unchecked and an entry can never rot.
- Provenance lives in a central `scripts/type-equiv.manifest.json` (`{ doc, symbol, source }` entries), **not** in directive comments in the prose. The script enforces a **1:1 correspondence** between each primary type-equiv block and one manifest entry, so a block can never be silently unchecked and an entry can never rot. A paired `.zh.md` block reuses the unsuffixed sibling's entry only when the complete tracked fence sequence matches in order, kind, and byte-exact body; otherwise the gate checks it independently, finds no manifest entry, and fails.
- Wired into `doc-sync`, so relevant documentation changes run it locally and CI runs it with the other documentation checks.
### Maintenance is the author's job, with a gate backstop
@@ -33,7 +33,7 @@ Status: implemented
- 完整的类型声明及其 JSDoc 会逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。简洁的 ` ```ts public-api ` 围栏承载与源码等价的类环境投影,用于实现体不应进入目录的类。`doc-typecheck` 会识别并跳过这两种围栏(裸声明无法独立编译),并且**将它们排除在 opt-out 比例之外**——它们是单独受检的类别,而不是未经检查的草图。
- 新增的 `scripts/verify-type-equiv.ts` 通过 TypeScript 解析器提取每个块,并断言其声明结构和每条 JSDoc 注释都与所声明的符号匹配,只忽略格式空白和非 JSDoc 注释。普通块保留完整声明。`public-api` 投影保留类的公共字段、构造函数、访问器和方法及其原始 JSDoc,同时移除实现体以及私有或受保护成员。之所以选择它而非编译式 `_Check` 断言,是因为目录所保留的是源码名称与文档一致性,而不是可赋值性。
- 来源信息存放在集中的 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本强制执行 **1:1 对应**:每个 type-equiv 块恰好有一条 manifest 条目,反之亦然;因此一个块永远不会被静默漏检,一条条目也永远不会腐烂。
- 来源信息存放在集中的 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本在每个主 type-equiv 块与一条 manifest 条目之间强制执行 **1:1 对应**,因此一个块永远不会被静默漏检,一条条目也永远不会腐烂。只有当配对 `.zh.md` 块的完整受跟踪围栏序列在顺序、类型和按字节精确的正文上均与无后缀兄弟文件匹配时,才会复用后者的条目;否则门禁会独立检查该块,发现没有 manifest 条目后失败。
- 接入 `doc-sync`,因此相关文档变更会在本地运行它,CI 也会与其他文档检查一起运行它。
### 维护是作者的职责,门禁作为兜底
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-02-bilingual-docs-and-pairing-gate.md: 08e149ccc2342695d6dae4f1845896def6bf4388
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 91f25f33c20337ea688257829552795085e0a8c0
2026-07-02-bilingual-docs-and-pairing-gate.md: ece3ccc183893a85335a36eb9b00cb42d32a1a37
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: dc8c648cc4ce9dd7739383c620ee2b1c3794f2ee
@@ -12,7 +12,7 @@ This repo's README and docs tree are read by people and agents inside and outsid
- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).
- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: sources selected explicitly, by document class, or by the manifest's `requiredSince` cutoff have complete pairs; every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. The `requiredClasses` set in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) closes a translated class for all present and future files instead of relying on an enumerated snapshot.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: sources selected explicitly, by document class, or by the manifest's `requiredSince` cutoff have complete pairs; every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. The `requiredClasses` set in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) closes a translated class for all present and future files instead of relying on an enumerated snapshot. Source-oriented code gates consume a `.zh.md` fence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.
- **The enforcement frontier advances in coherent review batches, then closes by class.** Explicit `required` entries admit related files while their back-catalog is still being reviewed; after that catalog is complete, its `non-readme` or `readme` class enters `requiredClasses` and can no longer grow a backlog. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.
- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.
- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent.
@@ -12,7 +12,7 @@ Status: implemented
- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。
- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:通过显式指定、文档类别或 manifest(元数据清单)的 `requiredSince` 分界日期选中的源文档必须有完整配对;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `requiredClasses` 集合会将已完成翻译的类别纳入强制范围,对其当前及今后所有文件强制执行契约,而不再依赖一份枚举式快照。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:通过显式指定、文档类别或 manifest(元数据清单)的 `requiredSince` 分界日期选中的源文档必须有完整配对;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `requiredClasses` 集合会将已完成翻译的类别纳入强制范围,对其当前及今后所有文件强制执行契约,而不再依赖一份枚举式快照。只有当 `.zh.md` 围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。
- **执行红线按连贯的评审批次推进,再以类别为单位完成强制覆盖。** 在存量文档仍处于评审阶段时,显式 `required` 条目会纳入相关文件;存量文档全部完成后,其 `non-readme` 或 `readme` 类别进入 `requiredClasses`,不再产生新的 backlog。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。
- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
development.md: f706d54764bbf79d1f13ccb4c412e7b5717b1edb
development.zh.md: 15f5ae0c514ac412c302a99cb0a662acce510c44
development.md: c46d84740e6f0a1f67158f39f9ea421cb57165d4
development.zh.md: 9e13ab258e1db5406f84ece61959a995110578ae
+1 -1
View File
@@ -166,7 +166,7 @@ The [core data structures](core-data-structures/core.md) docs paste source-equiv
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate also enforces a 1:1 correspondence by document, symbol, and projection, so a block can't go silently unchecked and a stale entry can't linger. `doc-typecheck` skips both fence kinds (they aren't standalone-compilable) and excludes them from its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a block, update the manifest in the same change.
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
## Architecture context
+1 -1
View File
@@ -166,7 +166,7 @@ pnpm run demo:acp
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码契约和确切形状。该门禁还按文档、符号和投影强制 1:1 对应,因此不会有块被静默漏检,也不会有陈旧条目滞留。`doc-typecheck` 跳过两种围栏(它们不能独立编译),并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个块时,请在同一个变更里更新 manifest。
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码契约和确切形状。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
## 架构上下文
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: a60572b0691702c949b44b82a7b1d732a888ed93
README.zh.md: 04ae032233dfa01c145b5d6bdbe7353a366e11f1
README.md: 053453bc622f58083a5e0e2992f1b8a820e3f3f9
README.zh.md: 7f6242e8ff2a3ec69e2b84402dab447297047991
+2
View File
@@ -28,6 +28,8 @@ This repo's documentation is read by people and agents both inside and outside t
3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all.
4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named Agent Notes merge bilingual from birth.
Source-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.
`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok — and is the work list for translation batches. It never fails; it reports.
The practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write`), exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.
+2
View File
@@ -28,6 +28,8 @@
3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。
4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对;新建的日期命名 Agent Note 从创建起便须配齐中英文。
面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest 中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。
`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok),是翻译批次的工作清单。它从不失败;它只报告。
这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write`),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。
+10 -3
View File
@@ -1,7 +1,8 @@
/**
* Typecheck Markdown `ts` fences against the workspace API. `ignore-check` fences are reported as
* opt-outs; generated catalog fragments and source-equivalence blocks are skipped here because their
* owning gates verify them. A build-coordinated mode consumes existing declarations without emit.
* owning gates verify them. Byte-identical `.zh.md` copies reuse their unsuffixed sibling's check. A
* build-coordinated mode consumes existing declarations without emit.
*/
import { execFileSync } from 'node:child_process'
@@ -10,6 +11,7 @@ import { join, relative, resolve } from 'node:path'
import ts from 'typescript'
import { builtDeclarationPath } from './doc-typecheck-paths.ts'
import { extractFences } from './md-fences.ts'
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
const root = resolve(import.meta.dirname, '..')
@@ -206,7 +208,12 @@ for (const pattern of markdownGlobs) {
}
files.sort()
const all = files.flatMap(extractBlocks)
const extracted = files.flatMap(extractBlocks)
const { primary: all, derivatives } = partitionPairedMarkdownDerivatives(
extracted,
block => block.file,
block => `${block.kind}\0${block.code}`,
)
const checked = all.filter(b => b.kind === 'check')
const ignored = all.filter(b => b.kind === 'ignore')
// Only compile-eligible fences belong in the opt-out ratio; every other skipped
@@ -233,7 +240,7 @@ if (compilationError !== undefined) {
const ratio = ignored.length / ratioDenominator
const skipped = all.length - ratioDenominator
console.log(`doc-typecheck: ${checked.length} block(s) compiled, ${ignored.length} ignored (${(ratio * 100).toFixed(0)}% opt-out), ${skipped} type-equiv/catalog (checked elsewhere).`)
console.log(`doc-typecheck: ${checked.length} block(s) compiled, ${ignored.length} ignored (${(ratio * 100).toFixed(0)}% opt-out), ${skipped} type-equiv/catalog (checked elsewhere), ${derivatives.length} paired derivative(s).`)
// Guard against the escape hatch becoming the norm.
if (ratioDenominator >= 4 && ratio > 0.5) {
console.error(`doc-typecheck: too many blocks opt out of checking (${ignored.length}/${ratioDenominator}). Make them compile or delete them.`)
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
interface Block {
doc: string
kind: string
code: string
}
const partition = (blocks: Block[]) => partitionPairedMarkdownDerivatives(
blocks,
block => block.doc,
block => `${block.kind}\0${block.code}`,
)
describe('partitionPairedMarkdownDerivatives', () => {
it('treats a complete byte-identical Chinese sequence as derivative', () => {
const english = [
{ doc: 'docs/example.md', kind: 'ts', code: 'const one = 1' },
{ doc: 'docs/example.md', kind: 'type-equiv', code: 'interface Example {}' },
]
const chinese = english.map(block => ({ ...block, doc: 'docs/example.zh.md' }))
const unrelated = { doc: 'docs/other.md', kind: 'ts', code: 'const other = 2' }
expect(partition([...english, ...chinese, unrelated])).toEqual({
primary: [...english, unrelated],
derivatives: chinese,
})
})
it('keeps reordered, changed, partial, and orphan Chinese sequences primary', () => {
const sequence = (doc: string) => [
{ doc, kind: 'ts', code: 'const one = 1' },
{ doc, kind: 'ts', code: 'const two = 2' },
]
const english = sequence('docs/example.md')
const changed = english.map((block, index) => ({
...block,
doc: 'docs/example.zh.md',
code: index === 0 ? 'const one = 0' : block.code,
}))
const reorderedEnglish = sequence('docs/reordered.md')
const reordered = [...reorderedEnglish].reverse().map(block => ({ ...block, doc: 'docs/reordered.zh.md' }))
const partialEnglish = sequence('docs/partial.md')
const partial = [{ ...partialEnglish[0]!, doc: 'docs/partial.zh.md' }]
const orphan = [{ doc: 'docs/orphan.zh.md', kind: 'ts', code: 'const orphan = true' }]
const blocks = [
...english,
...changed,
...reorderedEnglish,
...reordered,
...partialEnglish,
...partial,
...orphan,
]
expect(partition(blocks)).toEqual({ primary: blocks, derivatives: [] })
})
it('requires the fence kind to match as well as the body', () => {
const english = { doc: 'docs/example.md', kind: 'type-equiv', code: 'interface Example {}' }
const chinese = { ...english, doc: 'docs/example.zh.md', kind: 'public-api' }
expect(partition([english, chinese])).toEqual({ primary: [english, chinese], derivatives: [] })
})
})
+63
View File
@@ -0,0 +1,63 @@
/**
* Separate byte-identical Chinese Markdown code blocks from the primary checks
* performed on their unsuffixed English siblings. The bilingual pairing gate
* owns cross-language identity; source-oriented gates consume one copy.
*/
/** The result of separating canonical blocks from paired Chinese derivatives. */
export interface MarkdownDerivativePartition<T> {
/** Blocks that still require the caller's owning check. */
primary: T[]
/** Chinese blocks covered by the byte-identical unsuffixed sequence. */
derivatives: T[]
}
/** Return the unsuffixed sibling of a Chinese Markdown path. */
function unsuffixedSibling(doc: string): string | null {
return doc.endsWith('.zh.md') ? `${doc.slice(0, -'.zh.md'.length)}.md` : null
}
/**
* Partition complete byte-identical `.zh.md` block sequences from primary
* blocks. A partial or reordered match stays primary so the caller fails
* closed; the translation-pairing gate reports the cross-language mismatch.
*
* @param blocks - Blocks in repository scan order.
* @param docOf - Repository-relative Markdown path owning a block.
* @param fingerprintOf - Block kind/info string plus byte-exact body.
* @returns Primary blocks and paired Chinese derivatives, preserving order.
*/
export function partitionPairedMarkdownDerivatives<T>(
blocks: readonly T[],
docOf: (block: T) => string,
fingerprintOf: (block: T) => string,
): MarkdownDerivativePartition<T> {
const byDoc = new Map<string, T[]>()
for (const block of blocks) {
const doc = docOf(block)
const group = byDoc.get(doc)
if (group) group.push(block)
else byDoc.set(doc, [block])
}
const derivativeDocs = new Set<string>()
for (const [doc, candidates] of byDoc) {
const sibling = unsuffixedSibling(doc)
if (sibling === null) continue
const originals = byDoc.get(sibling)
if (originals === undefined || originals.length !== candidates.length) continue
if (candidates.every((candidate, index) => {
const original = originals[index]
return original !== undefined && fingerprintOf(candidate) === fingerprintOf(original)
})) {
derivativeDocs.add(doc)
}
}
const primary: T[] = []
const derivatives: T[] = []
for (const block of blocks) {
(derivativeDocs.has(docOf(block)) ? derivatives : primary).push(block)
}
return { primary, derivatives }
}
@@ -16,19 +16,19 @@
},
{
"role": "user",
"content": "# Development guide\n\nEnglish | [中文](development.zh.md)\n\nThis onboarding guide helps project contributors get started with the local environment, daily workflow, and CI flow; see the Agent Notes for design rationale and technical trade-offs.\n\n## Prerequisites\n\n- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).\n- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.\n- Git.\n- Optional: a DeepSeek API key for the TUI, headless, and ACP automation demos and real-API e2e tests.\n\n## First-time setup\n\nInstall dependencies from the repo root:\n\n```sh\npnpm install\n```\n\nThe install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency through `scripts/install-lefthook.mjs`; the wrapper script uses lefthook's reviewed `--force` mode so linked worktrees with an existing `core.hooksPath` do not fail normal `pnpm run …` commands.\n\nIf hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:\n\n```sh\npnpm exec lefthook install --force\n```\n\nRun typecheck once after a fresh clone:\n\n```sh\npnpm run typecheck\n```\n\nThat first typecheck runs the whole-repo `tsc -b` graph: it emits every package/vendor `lib/types` and checks examples, tests, and scripts through the two no-emit aggregates described below.\n\n## TypeScript project layout\n\nThe repository's TypeScript configuration has exactly three roles; every tsconfig file plays one of them.\n\n| File | Role | Forms a program? |\n|---|---|---|\n| `tsconfig.json` | Solution root: `extends` base, `files: []`, references to the two aggregates. The whole-repo `tsc -b tsconfig.json` graph, the tsserver discovery entry, and — through the inherited `paths` — the resolution config for tsx running `examples/` and `scripts/` (their nearest tsconfig is this file). | No |\n| `tsconfig.host.json` | Host aggregate: host-side packages (via references), examples, tests, scripts, website. Excludes `packages/client`. | Yes |\n| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`. | Yes |\n| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |\n| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the client aggregate and every `packages/client/*` package. | No |\n\nHost and client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Two disciplines follow:\n\n- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope.\n- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges. Program-backed generators and gates (`scripts/ts-project.ts` consumers, doc-typecheck standalone mode) are host-only by decision; the client side gains program-backed tooling only with a concrete need.\n\nStatic analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Decision record: [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md); the tsc-first emit pipeline is the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md).\n\nIf a relevant local check consumes built package output, build once first:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs; ordinary commits and pushes do not require that build unless their selected checks consume it.\n\n## Environment variables\n\nThe real DeepSeek adapter and key-backed agent demos read credentials from the environment or from a gitignored `.env` at the repo root:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` is optional and defauLine truncated
"content": "# Development guide\n\nEnglish | [中文](development.zh.md)\n\nThis onboarding guide helps project contributors get started with the local environment, daily workflow, and CI flow; see the Agent Notes for design rationale and technical trade-offs.\n\n## Prerequisites\n\n- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).\n- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.\n- Git.\n- Optional: a DeepSeek API key for the TUI, headless, and ACP automation demos and real-API e2e tests.\n\n## First-time setup\n\nInstall dependencies from the repo root:\n\n```sh\npnpm install\n```\n\nThe install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency through `scripts/install-lefthook.mjs`; the wrapper script uses lefthook's reviewed `--force` mode so linked worktrees with an existing `core.hooksPath` do not fail normal `pnpm run …` commands.\n\nIf hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:\n\n```sh\npnpm exec lefthook install --force\n```\n\nRun typecheck once after a fresh clone:\n\n```sh\npnpm run typecheck\n```\n\nThat first typecheck runs the whole-repo `tsc -b` graph: it emits every package/vendor `lib/types` and checks examples, tests, and scripts through the two no-emit aggregates described below.\n\n## TypeScript project layout\n\nThe repository's TypeScript configuration has exactly three roles; every tsconfig file plays one of them.\n\n| File | Role | Forms a program? |\n|---|---|---|\n| `tsconfig.json` | Solution root: `extends` base, `files: []`, references to the two aggregates. The whole-repo `tsc -b tsconfig.json` graph, the tsserver discovery entry, and — through the inherited `paths` — the resolution config for tsx running `examples/` and `scripts/` (their nearest tsconfig is this file). | No |\n| `tsconfig.host.json` | Host aggregate: host-side packages (via references), examples, tests, scripts, website. Excludes `packages/client`. | Yes |\n| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`. | Yes |\n| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |\n| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the client aggregate and every `packages/client/*` package. | No |\n\nHost and client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Two disciplines follow:\n\n- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope.\n- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges. Program-backed generators and gates (`scripts/ts-project.ts` consumers, doc-typecheck standalone mode) are host-only by decision; the client side gains program-backed tooling only with a concrete need.\n\nStatic analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Decision record: [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md); the tsc-first emit pipeline is the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md).\n\nIf a relevant local check consumes built package output, build once first:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs; ordinary commits and pushes do not require that build unless their selected checks consume it.\n\n## Environment variables\n\nThe real DeepSeek adapter and key-backed agent demos read credentials from the environment or from a gitignored `.env` at the repo root:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` is optional and defauLine truncated
},
{
"role": "assistant",
"content": "# 开发指南\n\n[English](development.md) | 中文\n\n本指南覆盖参与 DeepSeek Harness 开发所需的本地环境搭建、日常工作流与 CI 流程;设计动机与技术权衡请查阅相应 Agent Note。\n\n## 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git。\n- 可选:一个 DeepSeek API key,用于 TUI、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n## 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程同时会运行根目录的 `postinstall` 脚本,该脚本通过 `scripts/install-lefthook.mjs` 从仓库 dev 依赖安装 lefthook。包装脚本使用 lefthook 经过评审的 `--force` 模式,确保已存在 `core.hooksPath` 的关联 worktree 不会导致正常的 `pnpm run …` 命令失败。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致缺少钩子,请手动安装:\n\n```sh\npnpm exec lefthook install --force\n```\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n首次类型检查会执行全仓 `tsc -b tsconfig.json` 图:发射每个 package/vendor 的 `lib/types`,并通过下述两个 no-emit 聚合检查示例、测试和脚本。\n\n## TypeScript 项目布局\n\n仓库的 TypeScript 配置只有三种角色;每个 tsconfig 文件恰好扮演其中一种。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个聚合。全仓 `tsc -b tsconfig.json` 图、tsserver 发现入口,并经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置(它们最近的 tsconfig 就是此文件)。 | 否 |\n| `tsconfig.host.json` | host 聚合:host 侧各包(经 references)、示例、测试、脚本、website。排除 `packages/client`。 | 是 |\n| `tsconfig.client.json` | client 聚合:`packages/client/*` 各包及其测试、`apps/web`。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 client 聚合和每个 `packages/client/*` 包 extends。 | 否 |\n\nhost 与 client 保持两个聚合 program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个聚合,一个 paths 门面也可以横跨两侧。由此推出两条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个聚合展平进一个 program 会撞上 `Context` 合并冲突。基于 program 的生成器与门禁(`scripts/ts-project.ts` 的消费者、doc-typecheck standalone 模式)按决策仅覆盖 host 侧;client 侧只在出现真实需求时再获得基于 program 的工具。\n\n静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。决策记录:[solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md);tsc-first 发射管线见 [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验 package 入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n## 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时Line truncated
"content": "# 开发指南\n\n[English](development.md) | 中文\n\n本指南覆盖参与 DeepSeek Harness 开发所需的本地环境搭建、日常工作流与 CI 流程;设计动机与技术权衡请查阅相应 Agent Note。\n\n## 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git。\n- 可选:一个 DeepSeek API key,用于 TUI、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n## 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程同时会运行根目录的 `postinstall` 脚本,该脚本通过 `scripts/install-lefthook.mjs` 从仓库 dev 依赖安装 lefthook。包装脚本使用 lefthook 经过评审的 `--force` 模式,确保已存在 `core.hooksPath` 的关联 worktree 不会导致正常的 `pnpm run …` 命令失败。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致缺少钩子,请手动安装:\n\n```sh\npnpm exec lefthook install --force\n```\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n首次类型检查会执行全仓 `tsc -b tsconfig.json` 图:发射每个 package/vendor 的 `lib/types`,并通过下述两个 no-emit 聚合检查示例、测试和脚本。\n\n## TypeScript 项目布局\n\n仓库的 TypeScript 配置只有三种角色;每个 tsconfig 文件恰好扮演其中一种。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个聚合。全仓 `tsc -b tsconfig.json` 图、tsserver 发现入口,并经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置(它们最近的 tsconfig 就是此文件)。 | 否 |\n| `tsconfig.host.json` | host 聚合:host 侧各包(经 references)、示例、测试、脚本、website。排除 `packages/client`。 | 是 |\n| `tsconfig.client.json` | client 聚合:`packages/client/*` 各包及其测试、`apps/web`。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 client 聚合和每个 `packages/client/*` 包 extends。 | 否 |\n\nhost 与 client 保持两个聚合 program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个聚合,一个 paths 门面也可以横跨两侧。由此推出两条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个聚合展平进一个 program 会撞上 `Context` 合并冲突。基于 program 的生成器与门禁(`scripts/ts-project.ts` 的消费者、doc-typecheck standalone 模式)按决策仅覆盖 host 侧;client 侧只在出现真实需求时再获得基于 program 的工具。\n\n静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。决策记录:[solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md);tsc-first 发射管线见 [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验 package 入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n## 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时Line truncated
},
{
"role": "user",
"content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis repo's documentation is read by people and agents both inside and outside the company, so the README, Agent Notes, and docs tree are maintained in English and Simplified Chinese. This page defines the pairing contract, the enforcement gate, and the rollout policy; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md).\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. The recorded hash also recovers the exact last-confirmed text of either side (`git cat-file -p <hash>`), so an out-of-sync pair is updated by diffing the edited side against its last-confirmed state and patching the counterpart minimally — never by re-translating whole files. After bringing the pair back in line, `pnpm run verify-translation-pairing --write` re-records both hashes; that yaml diff is the reviewable act of confirming consistency.\n- **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every file listed as `required`, and every document whose class appears in `requiredClasses`, in [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) has a complete pair. The classes are `non-readme` and `readme`; class matching is case-insensitive on the basename, so `missions/readme.md` is a README.\n2. Every pair that exists at all — required or not — is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all.\n4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named Agent Notes merge bilingual from birth.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok — and is the work list for translation batches. It never fails; it reports.\n\nThe practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write`), exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.\n\nThe gate's limit, stated plainly: **a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.** It checks hashes and shape; it cannot judge whether the two sides actually say the same thing, or whether the woLine truncated
"content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis repo's documentation is read by people and agents both inside and outside the company, so the README, Agent Notes, and docs tree are maintained in English and Simplified Chinese. This page defines the pairing contract, the enforcement gate, and the rollout policy; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md).\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. The recorded hash also recovers the exact last-confirmed text of either side (`git cat-file -p <hash>`), so an out-of-sync pair is updated by diffing the edited side against its last-confirmed state and patching the counterpart minimally — never by re-translating whole files. After bringing the pair back in line, `pnpm run verify-translation-pairing --write` re-records both hashes; that yaml diff is the reviewable act of confirming consistency.\n- **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every file listed as `required`, and every document whose class appears in `requiredClasses`, in [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) has a complete pair. The classes are `non-readme` and `readme`; class matching is case-insensitive on the basename, so `missions/readme.md` is a README.\n2. Every pair that exists at all — required or not — is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all.\n4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named Agent Notes merge bilingual from birth.\n\nSource-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok — and is the work list for translation batches. It never fails; it reports.\n\nThe practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write`), exactly like the repo's existing doc-syLine truncated
},
{
"role": "assistant",
"content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README、Agent Note(agent 决策记录)与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。\n\n## 配对契约\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。记录的 hash 还能还原任一侧上次确认时的确切文本(`git cat-file -p <hash>`),所以失去同步的配对是「把被改的一侧与其上次确认状态做 diff、再最小化地修补另一侧」,从不整篇重译。两侧对齐后,`pnpm run verify-translation-pairing --write` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审。\n- **语言切换行。** 两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份契约:\n\n1. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 中 `required` 列出的每个文件,以及所属文档类别出现在 `requiredClasses` 中的每篇文档,都有完整配对。类别分为 `non-readme` 和 `readme`;判断类别时,basename 不区分大小写,因此 `missions/readme.md` 也属于 README。\n2. 任何已存在的配对(无论是否 required)都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。\n4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对;新建的日期命名 Agent Note 从创建起便须配齐中英文。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok),是翻译批次的工作清单。它从不失败;它只报告。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write`),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n把门禁的边界说白:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与结构签名;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分契约由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的Line truncated
"content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README、Agent Note(agent 决策记录)与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。\n\n## 配对契约\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。记录的 hash 还能还原任一侧上次确认时的确切文本(`git cat-file -p <hash>`),所以失去同步的配对是「把被改的一侧与其上次确认状态做 diff、再最小化地修补另一侧」,从不整篇重译。两侧对齐后,`pnpm run verify-translation-pairing --write` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审。\n- **语言切换行。** 两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份契约:\n\n1. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 中 `required` 列出的每个文件,以及所属文档类别出现在 `requiredClasses` 中的每篇文档,都有完整配对。类别分为 `non-readme` 和 `readme`;判断类别时,basename 不区分大小写,因此 `missions/readme.md` 也属于 README。\n2. 任何已存在的配对(无论是否 required)都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。\n4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对;新建的日期命名 Agent Note 从创建起便须配齐中英文。\n\n面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest 中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok),是翻译批次的工作清单。它从不失败;它只报告。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write`),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n把门禁的边界说白:**门禁通过意味着这组文档在当前内容上的一致性得到了确认Line truncated
},
{
"role": "user",
@@ -40,11 +40,11 @@
},
{
"role": "user",
"content": "# Agent Note: Bilingual documentation via paired sibling files and a pairing gate\n\nStatus: implemented\n\nEnglish | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)\n\n## Problem\n\nThis repo's README and docs tree are read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.\n\n## Decision\n\n- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).\n- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.\n- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: sources selected explicitly, by document class, or by the manifest's `requiredSince` cutoff have complete pairs; every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. The `requiredClasses` set in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) closes a translated class for all present and future files instead of relying on an enumerated snapshot.\n- **The enforcement frontier advances in coherent review batches, then closes by class.** Explicit `required` entries admit related files while their back-catalog is still being reviewed; after that catalog is complete, its `non-readme` or `readme` class enters `requiredClasses` and can no longer grow a backlog. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.\n- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.\n- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent.\n\n## Verification\n\nThe verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable snapshot](../../../../scripts/translation-prompt.snapshot.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible.\n\n## Alternatives considered\n\n- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this Agent Note: `.zh.md` files carried an HTML comment recordinLine truncated
"content": "# Agent Note: Bilingual documentation via paired sibling files and a pairing gate\n\nStatus: implemented\n\nEnglish | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)\n\n## Problem\n\nThis repo's README and docs tree are read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.\n\n## Decision\n\n- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).\n- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.\n- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: sources selected explicitly, by document class, or by the manifest's `requiredSince` cutoff have complete pairs; every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. The `requiredClasses` set in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) closes a translated class for all present and future files instead of relying on an enumerated snapshot. Source-oriented code gates consume a `.zh.md` fence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.\n- **The enforcement frontier advances in coherent review batches, then closes by class.** Explicit `required` entries admit related files while their back-catalog is still being reviewed; after that catalog is complete, its `non-readme` or `readme` class enters `requiredClasses` and can no longer grow a backlog. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.\n- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.\n- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent.\n\n## Verification\n\nThe verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable snapshot](../../../../scripts/translation-prompt.snapshot.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks maLine truncated
},
{
"role": "assistant",
"content": "# Agent Note:通过配对兄弟文件与配对门禁实现双语文档\n\nStatus: implemented\n\n[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文\n\n## 问题\n\n本仓库的 README 与 docs 目录树会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁会注意到。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。\n\n## 决策\n\n- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。\n- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。\n- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:通过显式指定、文档类别或 manifest(元数据清单)的 `requiredSince` 分界日期选中的源文档必须有完整配对;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `requiredClasses` 集合会将已完成翻译的类别纳入强制范围,对其当前及今后所有文件强制执行契约,而不再依赖一份枚举式快照。\n- **执行红线按连贯的评审批次推进,再以类别为单位完成强制覆盖。** 在存量文档仍处于评审阶段时,显式 `required` 条目会纳入相关文件;存量文档全部完成后,其 `non-readme` 或 `readme` 类别进入 `requiredClasses`,不再产生新的 backlog。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。\n- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。\n- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。\n\n## 验证\n\n验证契约分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、切换行和结构;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行快照](../../../../scripts/translation-prompt.snapshot.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和响应消费结果。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。\n\n## 曾考虑的替代方案\n\n- **英文为正典源、指纹放在译文内**:本 Agent Note 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译Line truncated
"content": "# Agent Note:通过配对兄弟文件与配对门禁实现双语文档\n\nStatus: implemented\n\n[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文\n\n## 问题\n\n本仓库的 README 与 docs 目录树会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁会注意到。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。\n\n## 决策\n\n- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。\n- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。\n- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:通过显式指定、文档类别或 manifest(元数据清单)的 `requiredSince` 分界日期选中的源文档必须有完整配对;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `requiredClasses` 集合会将已完成翻译的类别纳入强制范围,对其当前及今后所有文件强制执行契约,而不再依赖一份枚举式快照。只有当 `.zh.md` 围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。\n- **执行红线按连贯的评审批次推进,再以类别为单位完成强制覆盖。** 在存量文档仍处于评审阶段时,显式 `required` 条目会纳入相关文件;存量文档全部完成后,其 `non-readme` 或 `readme` 类别进入 `requiredClasses`,不再产生新的 backlog。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。\n- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。\n- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。\n\n## 验证\n\n验证契约分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、切换行和结构;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行快照](../../../../scripts/translation-prompt.snapshot.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和响应消费结果。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中Line truncated
},
{
"role": "user",
+1 -944
View File
@@ -1,5 +1,5 @@
{
"comment": "Maps each ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence: every source-equivalence block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a source-equivalence block; remove it when you remove the block.",
"comment": "Maps each primary ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Paired `.zh.md` blocks are byte-identical derivatives checked through their unsuffixed sibling and have no duplicate entry. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence between primary blocks and entries. Add an entry when you add a primary source-equivalence block; remove it when you remove the block.",
"entries": [
{
"doc": "docs/core-data-structures/core.md",
@@ -1248,949 +1248,6 @@
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionSearchHit",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "Branded",
"source": "packages/util/brand/src/index.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "ContentBlockMap",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "AssistantProvenance",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "Message",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "MessageSourceMap",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "FinishReasonMap",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "LlmProviderInfo",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "LlmModelInfo",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "LlmModelContext",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "GenerateOptions",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "ToolSchema",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "LlmCallConfig",
"source": "packages/llm/llm/src/call-config.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "SessionEvent",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "SendOptions",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "AgentCancelCause",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "InjectOptions",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "ResolvedAgentInput",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "AgentMessageId",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "AgentMessage",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "CancelOptions",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "Agent",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "HookContext",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "PromptDecision",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "ContinuationDecision",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "RequestError",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "RequestErrorDecision",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "ContinuationStop",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.zh.md",
"symbol": "SessionStartSource",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/scope.zh.md",
"symbol": "ScopeKey",
"source": "packages/core/scope/src/index.ts"
},
{
"doc": "docs/core-data-structures/scope.zh.md",
"symbol": "Scoped",
"source": "packages/core/scope/src/index.ts"
},
{
"doc": "docs/core-data-structures/scope.zh.md",
"symbol": "Scope",
"source": "packages/core/scope/src/index.ts"
},
{
"doc": "docs/core-data-structures/scope.zh.md",
"symbol": "ScopeLayer",
"source": "packages/core/scope/src/store.ts"
},
{
"doc": "docs/core-data-structures/system-prompt.zh.md",
"symbol": "AssembleContext",
"source": "packages/core/system-prompt/src/index.ts"
},
{
"doc": "docs/core-data-structures/system-prompt.zh.md",
"symbol": "PromptSection",
"source": "packages/core/system-prompt/src/index.ts"
},
{
"doc": "docs/core-data-structures/system-prompt.zh.md",
"symbol": "ToolProviderResult",
"source": "packages/core/system-prompt/src/index.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "StreamChunk",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "LlmFailure",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "TokenUsage",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "ContentBlockMap",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "AppIdentity",
"source": "packages/llm/llm/src/attribution.ts"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "BlockAssembler",
"source": "packages/llm/llm/src/assembler.ts",
"projection": "public-api"
},
{
"doc": "docs/core-data-structures/llm-streaming.zh.md",
"symbol": "LlmAdapter",
"source": "packages/llm/llm/src/index.ts",
"projection": "public-api"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "PromptMessageData",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SessionEventMap",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "OutOfBandSessionEventMap",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "EpochHeader",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "TodoItem",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SessionEvent",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "TurnTriggerMap",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "TurnEndReasonMap",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SurfaceEventType",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SurfaceOp",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SurfaceIntent",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SessionSurface",
"source": "packages/core/session/src/surface.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SurfaceFoldReplacement",
"source": "packages/core/session/src/surface.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "SurfaceFoldResult",
"source": "packages/core/session/src/surface.ts"
},
{
"doc": "docs/core-data-structures/session.zh.md",
"symbol": "Session",
"source": "packages/core/session/src/index.ts",
"projection": "public-api"
},
{
"doc": "docs/core-data-structures/persistence.zh.md",
"symbol": "SessionHeader",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.zh.md",
"symbol": "CreateSessionOptions",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.zh.md",
"symbol": "SessionLocation",
"source": "packages/session-persistence/session-persistence/src/index.ts"
},
{
"doc": "docs/core-data-structures/persistence.zh.md",
"symbol": "SessionPersistenceRevision",
"source": "packages/session-persistence/session-persistence/src/revision.ts"
},
{
"doc": "docs/core-data-structures/persistence.zh.md",
"symbol": "SessionPersistenceSnapshot",
"source": "packages/session-persistence/session-persistence/src/index.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSurface",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionRecord",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionLogSnapshot",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionSurfaceSnapshot",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionTitleObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionTitleObservationResult",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventRecord",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionResultFilter",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventResultFilter",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchDocument",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionSearchCursor",
"source": "packages/session-query/session-query/src/cursor.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionSearchRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchHit",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionSearchHit",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionLineageNode",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionLineageTrace",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionQueryErrorCode",
"source": "packages/session-query/session-query/src/config.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventReadRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventWindow",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventTraceRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventTrace",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventTraceObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolOutputDefinition",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolDefinition",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ValueSchemaSpec",
"source": "packages/core/tools/src/schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ParameterPropertySpec",
"source": "packages/core/tools/src/schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ParameterSchemaSpec",
"source": "packages/core/tools/src/schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "InferValue",
"source": "packages/core/tools/src/schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "InferArgs",
"source": "packages/core/tools/src/schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionToken",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionInput",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecution",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolDispatchExecution",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionMode",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolRunContext",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolGuard",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolRestriction",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolFailure",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionSuccess",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionFailure",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolExecutionResult",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "PreToolDecision",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "PostToolDecision",
"source": "packages/core/tools/src/index.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "JsonSchemaScalar",
"source": "packages/core/tools/src/json-schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "JsonSchemaType",
"source": "packages/core/tools/src/json-schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "JsonSchemaNode",
"source": "packages/core/tools/src/json-schema.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ObjectJsonSchema",
"source": "packages/core/tools/src/json-schema.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "AskUserQuestionOption",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "AskUserQuestionItem",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "AskUserQuestionRequest",
"source": "packages/ui/user-interaction/src/index.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "AskUserQuestionAnswerItem",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "AskUserQuestionAnswer",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "UserInteractionProvider",
"source": "packages/ui/user-interaction/src/index.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.zh.md",
"symbol": "UserInteractionError",
"source": "packages/ui/user-interaction/src/index.ts"
},
{
"doc": "docs/core-data-structures/approval.zh.md",
"symbol": "ApprovalRequestId",
"source": "packages/ui/user-approval/src/types.ts"
},
{
"doc": "docs/core-data-structures/approval.zh.md",
"symbol": "ApprovalOutcome",
"source": "packages/ui/user-approval/src/types.ts"
},
{
"doc": "docs/core-data-structures/approval.zh.md",
"symbol": "ApprovalPolicy",
"source": "packages/ui/user-approval/src/index.ts"
},
{
"doc": "docs/core-data-structures/approval.zh.md",
"symbol": "ApprovalRequest",
"source": "packages/ui/user-approval/src/index.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "DshEnvironmentKey",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "DshEnvironment",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashExecRequest",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashExecSpec",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashRunResult",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashSandboxInfo",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "CollectedOutput",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashProcess",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/bash.zh.md",
"symbol": "BashProcessRead",
"source": "packages/bash/bash/src/types.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "SandboxMode",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "ConfinedSandboxMode",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "SandboxExecutionPolicy",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "SandboxEnforcement",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "SandboxPolicy",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "SandboxPolicyRequest",
"source": "packages/sandbox/sandbox-policy/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.zh.md",
"symbol": "ConfinedArgv",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeJsonValue",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeRunRequest",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeRunResult",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeBindingNamespace",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeBindingErrorClass",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeBindingFunction",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/code-runtime.zh.md",
"symbol": "CodeRunFailure",
"source": "packages/code-runtime/code-runtime/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsTarget",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsTargetKey",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsVersion",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsInfo",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsPathInfo",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsDirEntry",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsWriteIntent",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsWriteOutcome",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsEditRequest",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsEditOutcome",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsErrorCode",
"source": "packages/fs/fs/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FsPolicyExec",
"source": "packages/fs/fs-policy/src/types.ts"
},
{
"doc": "docs/core-data-structures/filesystem.zh.md",
"symbol": "FileReadOutcome",
"source": "packages/fs/tool-fs/src/read-render.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillSource",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillResourceBase",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillSummary",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillCandidate",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillDefinition",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillRegistration",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillLookupOptions",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "SkillProvider",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/skills.zh.md",
"symbol": "Config",
"source": "packages/skill/skill/src/index.ts"
},
{
"doc": "docs/core-data-structures/compaction.zh.md",
"symbol": "CompactionResult",
"source": "packages/compact/compact/src/types.ts"
},
{
"doc": "docs/core-data-structures/compaction.zh.md",
"symbol": "CompactionTrigger",
"source": "packages/compact/compact/src/index.ts"
},
{
"doc": "docs/core-data-structures/compaction.zh.md",
"symbol": "PrunedEntry",
"source": "packages/compact/compact-tool-result-prune/src/types.ts"
},
{
"doc": "docs/core-data-structures/compaction.zh.md",
"symbol": "PruneResult",
"source": "packages/compact/compact-tool-result-prune/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentCapabilities",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentStartRequest",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentResult",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentStopReasonMap",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentRun",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/subagent.zh.md",
"symbol": "SubagentProvider",
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebSearchRequest",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebSearchResult",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebSearchSource",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebFetchRequest",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebFetchResult",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/web.zh.md",
"symbol": "WebFetchBody",
"source": "packages/web/web/src/types.ts"
},
{
"doc": "docs/core-data-structures/workflow.zh.md",
"symbol": "WorkflowStartRequest",
"source": "packages/workflow/workflow/src/types.ts"
},
{
"doc": "docs/core-data-structures/workflow.zh.md",
"symbol": "WorkflowMeta",
"source": "packages/workflow/workflow/src/types.ts"
},
{
"doc": "docs/core-data-structures/workflow.zh.md",
"symbol": "WorkflowResult",
"source": "packages/workflow/workflow/src/types.ts"
},
{
"doc": "docs/core-data-structures/workflow.zh.md",
"symbol": "WorkflowRun",
"source": "packages/workflow/workflow/src/types.ts"
}
]
}
+11 -4
View File
@@ -4,12 +4,14 @@
* declaration; `public-api` entries preserve a class's body-stripped public
* declaration. Blocks and entries have a one-to-one relationship; comparison
* ignores whitespace and non-JSDoc comments but preserves declaration
* structure and every original JSDoc comment.
* structure and every original JSDoc comment. Byte-identical `.zh.md` blocks
* reuse the manifest-backed check of their unsuffixed sibling.
*/
import { globSync, readFileSync, existsSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import ts from 'typescript'
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
const root = resolve(import.meta.dirname, '..')
@@ -223,7 +225,12 @@ const docSet = new Set<string>()
for (const pattern of MARKDOWN_GLOBS) {
for (const match of globSync(pattern, { cwd: root })) docSet.add(match.split(sep).join('/'))
}
const blocks: EquivBlock[] = [...docSet].sort().flatMap(extractEquivBlocks)
const extractedBlocks: EquivBlock[] = [...docSet].sort().flatMap(extractEquivBlocks)
const { primary: blocks, derivatives } = partitionPairedMarkdownDerivatives(
extractedBlocks,
block => block.doc,
block => `${block.projection ?? 'declaration'}\0${block.code}`,
)
const errors: string[] = []
// A manifest entry naming a doc that does not exist (or is outside the scanned
@@ -299,11 +306,11 @@ for (const e of entries) {
}
if (errors.length === 0) {
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source structure and JSDoc (1:1 with manifest).`)
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source structure and JSDoc (1:1 with manifest); ${derivatives.length} paired derivative(s).`)
process.exit(0)
}
console.error('verify-type-equiv: type-equiv verification failed:')
for (const e of errors) console.error(` ${e}`)
console.error(`\n(checked ${blocks.length} block(s) across ${new Set(blocks.map(b => b.doc)).size} doc(s); manifest at scripts/type-equiv.manifest.json)`)
console.error(`\n(checked ${blocks.length} primary block(s) across ${new Set(blocks.map(b => b.doc)).size} doc(s), ${derivatives.length} paired derivative(s); manifest at scripts/type-equiv.manifest.json)`)
process.exit(1)