mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
docs(i18n): re-translate RFC batch with the prompt-v4 pipeline
146 篇 RFC 译文按 v4 基线(#348)重出:v4 模板+术语表、金标 few-shot、三段协议、切换行后处理;全量机械核对零异常(一处 task id 术语违规已修)。三篇超长 RFC(code-mode 已入,web-seam/ agent-scope/sandbox/cds-core 仍在长文档通道产出)随后补。
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-doc-sync-enforcement.md: 44ea84daddcae73ce07b0a8240f83ee9945e449d
|
||||
2026-06-11-doc-sync-enforcement.zh.md: e7abe2d87fd97211f653b63ddb8818077532af48
|
||||
2026-06-11-doc-sync-enforcement.zh.md: c739e9661bb926c772f1d5399529a813ac59991c
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# RFC:Doc-sync 强制
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-06-11-doc-sync-enforcement.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
AGENTS.md 承诺文档与代码严格同步,但这一承诺此前只靠肉眼验证。评审曾两次发现漂移:一次是实操手册(cookbook)示例与类型策略矛盾,一次是 README 引用了错误的 `registerAdapter` 调用。失去同步的文档比没有文档更糟;而本代码库主要由 agent 构建,agent 对门禁的遵从远比对行文的遵从可靠(机械质量门禁)。有两类文档漂移可以被机械检查:不再能编译的代码块,以及重复了 `interface Events` 声明的事件分类体系表。
|
||||
AGENTS.md 承诺文档与代码严格同步,但这一承诺此前仅靠人眼核查。评审曾两次发现漂移:一次是实操手册(cookbook)示例与类型策略矛盾,一次是 README 引用了错误的 `registerAdapter` 调用。失去同步的文档比没有文档更糟;而本代码库主要由 agent(智能体)构建,agent 遵守门禁远比遵守行文约定可靠(机械质量门禁)。有两类文档漂移可以被机械检查:不再能编译的代码块,以及与 `interface Events` 声明重复的事件分类体系表。
|
||||
|
||||
## 决策
|
||||
|
||||
两道门禁,沿用既有的 `scripts/` 风格(tsx ESM,每个脚本一项职责):
|
||||
|
||||
1. **`doc-typecheck`** 从 `README.md`、`docs/**` 和 `packages/*/README.md` 中提取所有 ` ```ts ` 围栏代码块,写入一个继承根 `tsconfig.json` 的临时项目,然后用 `tsc -b` 编译。临时项目复用源码的 `paths` 映射和根 project references,因此文档示例能看到源码,而 vendor 代码仍在其自身的 tsconfig 设置下被检查。刻意作为草图的代码块可以用显式的 ` ```ts ignore-check ` 信息字符串退出检查;脚本会报告退出比例,超过一半则失败,防止逃生口悄悄变成常态。
|
||||
2. **`verify-event-taxonomy`** 从 `packages/*/src` 的 `interface Events` 块中提取事件名,再从 `docs/architecture.md` 的分类体系表中提取事件名,断言两个集合完全一致。只校验、不生成:表格保留手写的 Mode/Purpose 列,只检查名称集合。(落地此门禁时发现了表格缺失的三个事件:`tools/change`、`llm/adapter-change`、`system-prompt/change`。)**已被取代**:[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)取代了此门禁及其 `architecture.md` 表格,改为完全生成的 `docs/cordis-catalog/events.md` + `docs/cordis-catalog/services.md` 及其 `verify-cordis-catalog` 新鲜度门禁。本文的其他门禁(`doc-typecheck` 以及下文修订的 `verify-md-wrap`)不受影响。
|
||||
1. **`doc-typecheck`** 从 `README.md`、`docs/**` 和 `packages/*/README.md` 中提取所有 ` ```ts ` 围栏代码块,写入一个继承根 `tsconfig.json` 的临时项目,然后用 `tsc -b` 编译。临时项目复用源码的 `paths` 映射和根 project references,因此文档示例能看到源码,而 vendor 代码仍在其自身的 tsconfig 设置下被检查。刻意作为草图的代码块可通过显式的 ` ```ts ignore-check ` 信息字符串来 opt-out;脚本会报告 opt-out 比例,超过一半即失败,防止该豁免机制悄然成为常态。
|
||||
2. **`verify-event-taxonomy`** 从 `packages/*/src` 中的 `interface Events` 块和 `docs/architecture.md` 中的分类体系表分别提取事件名称,断言两个集合完全一致。只校验,不生成:表格保留手写的 Mode/Purpose 列,仅检查名称集合。(落地此门禁时发现了表格遗漏的三个事件:`tools/change`、`llm/adapter-change`、`system-prompt/change`。)**已被取代**:由[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)取代。此门禁及其 `architecture.md` 表格已退役,取而代之的是完全生成的 `docs/cordis-catalog/events.md` + `docs/cordis-catalog/services.md` 及其 `verify-cordis-catalog` 新鲜度门禁。本 RFC 中的其他门禁(`doc-typecheck` 以及下文修订中的 `verify-md-wrap`)不受影响。
|
||||
|
||||
两者通过一个共享的 `doc-sync` package.json 脚本运行,lefthook pre-push 钩子和 CI 都调用它([机械质量门禁](2026-06-11-quality-gates.md):钩子和 CI 调用相同的脚本,因此门禁在推送前就在本地触发,而不仅仅在推送后)。它们在 `pnpm run typecheck` 之后运行,后者校验 doc-typecheck 所引用的 package/vendor 构建图。
|
||||
两者通过一个共享的 doc-sync(文档同步门禁)`package.json` 脚本运行,lefthook pre-push 钩子和 CI 都调用它([机械质量门禁](2026-06-11-quality-gates.md):钩子与 CI 调用相同脚本,因此门禁在推送前就在本地触发,而非仅在推送后)。它们在 `pnpm run typecheck` 之后运行,后者校验 doc-typecheck 所引用的 package/vendor 构建图。
|
||||
|
||||
**修订(2026-06-17):** 第三道门禁 **`verify-md-wrap`** 后来也被纳入 `doc-sync`。它用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件(`README.md`、`docs/**`、`packages/*/README.md`,加上 `AGENTS.md` / `packages/AGENTS.md`),对任何跨越多行的 `paragraph` 节点报错,强制执行 docs/AGENTS.md 中「一个段落一个物理行」的写作规则。同样遵循只校验不生成的原则:它报告硬换行,从不重写,因此不会引入格式化噪音。`doc-sync` 现在包含三道门禁。
|
||||
**修订(2026-06-17):** 第三道门禁 **`verify-md-wrap`** 随后被纳入 `doc-sync`。它使用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件(`README.md`、`docs/**`、`packages/*/README.md`,加上 `AGENTS.md` / `packages/AGENTS.md`),如果任何 `paragraph` 节点跨越多个源码行则失败,从而强制执行 docs/AGENTS.md 中「一个段落一个物理行」的写作规则。同样遵循只校验不生成的原则:它报告硬换行但从不重写,因此不会引入格式化噪音。`doc-sync` 现在包含三道门禁。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **API-extractor 黄金报告**([已推迟的提案](../../proposed/process/2026-06-11-api-extractor-reports.md)):有意推迟。对于评审者已经能看到源码 diff 的内部 monorepo 而言价值不高,且依赖笨重、配置繁琐。
|
||||
- **从源码生成分类体系表**而非校验名称:否决,机制比问题本身更重;表格保留手写的 Mode/Purpose 列,直到[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)完全取代了这项检查。
|
||||
- **API-extractor 金标报告**([已推迟的提案](../../proposed/process/2026-06-11-api-extractor-reports.md)):有意推迟。对于评审者已能直接看到源码 diff 的内部 monorepo 而言价值有限,且依赖重、配置繁琐。
|
||||
- **从源码生成分类体系表**而非仅校验名称:否决,机制比问题本身更重;表格保留了手写的 Mode/Purpose 列,直到[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)完全取代了这项检查。
|
||||
|
||||
## 后果
|
||||
|
||||
- 可机械检查的文档漂移现在会让 pre-push 钩子和 CI 失败,而非等待评审者发现。这是「机械门禁优于行文约定」原则的一个实例。
|
||||
- 让文档代码片段可编译需要少量 stub import 或 `declare`;`ignore-check` 比例必须保持低位,否则门禁形同虚设(比例守卫强制执行这一点)。
|
||||
- 分类体系检查仅限名称:Mode 或 Purpose 列的错误仍需人工评审。
|
||||
- 如果这些包(package)将来对外发布,API 报告仍可重新考虑。
|
||||
- 可检查类别的文档漂移现在会让 pre-push 钩子和 CI 失败,而非等待评审者发现。这是「机械门禁优于行文约定」原则的一个实例。
|
||||
- 让文档代码片段可编译需要少量 stub import/`declare`;`ignore-check` 比例必须保持低位,否则门禁形同虚设(比例守卫强制执行此约束)。
|
||||
- 分类体系检查仅限名称——Mode 或 Purpose 列的错误仍需人工评审。
|
||||
- 如果 package 未来对外发布,API 报告方案仍可重新考虑。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-quality-gates.md: 9862dc019dd6ff3b4639983821256395d0ee7b77
|
||||
2026-06-11-quality-gates.zh.md: 7a9dd7cead0e7a4964a44b650664ceb7ff570c7b
|
||||
2026-06-11-quality-gates.zh.md: a9c17bb700db091d21f7930942a8f3bbf55958a0
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# RFC:以机械质量门禁取代行文约定
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-06-11-quality-gates.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
本代码库主要由 coding agent 开发。相比行文约定,agent 遵守强制门禁的可靠性远高得多;而当劳动由 agent 完成时,「工作量大」不构成成本论据。早期证据:未通过类型检查的测试被提交了(vitest 不做类型检查),只在评审时才被发现。
|
||||
本代码库主要由 coding agent(智能体)开发。相比行文约定,agent 遵守强制门禁的可靠性远高得多;而当劳动由 agent 承担时,「工作量大」不构成成本论据。早期证据:未通过类型检查的测试被提交(vitest 不做类型检查),仅在评审中才被发现。
|
||||
|
||||
## 决策
|
||||
|
||||
AGENTS.md 中的每一项承诺都对应一条退出码非零的命令,同时接入 git 钩子和 CI,两者调用相同的 package.json 脚本:
|
||||
AGENTS.md 中的每一条承诺都对应一个以非零退出码表示失败的命令,通过 git 钩子和 CI 调用同一套 package.json 脚本来执行:
|
||||
|
||||
- 最严格的 TypeScript(`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录 no-emit `tsconfig.json` 在 CI 中进行类型检查,而 package/vendor 代码保持在各自 project-reference 边界之后。
|
||||
- ESLint strict-type-checked + @stylistic(作为强制执行的项目风格),包括文件内重复逻辑检查;vendor 代码排除在外。
|
||||
- jscpd 检测 package 生产 TypeScript 和仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。
|
||||
- `packages/*/*/src` 的逐文件 100% 覆盖率(v8);不可达的防御性守卫保留 `/* v8 ignore */ ` 并注明理由,而非删除。
|
||||
- knip(死代码/依赖)、publint(包正确性)、workspace 约束(workspace 规则:private、cordis peer+dev、统一版本、ESM),以及对构建出的包声明文件进行 NodeNext 消费方类型检查。
|
||||
- lefthook pre-commit(lint 暂存文件、类型检查、vendor manifest 守卫)和 pre-push(测试、hygiene);CI 在 Node 22.19/24/26 上运行完整矩阵,外加一个端到端驱动 echo-agent 的演示冒烟测试。
|
||||
- 最严格的 TypeScript 配置(`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录的 no-emit `tsconfig.json` 在 CI 中进行类型检查,而 package/vendor 代码保持在各自 project-reference 边界之后。
|
||||
- ESLint strict-type-checked + @stylistic(作为强制执行的统一代码风格),包括文件内重复逻辑检查;vendor 代码排除在外。
|
||||
- jscpd 检测 package 生产 TypeScript 与仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。
|
||||
- `packages/*/*/src` 下按文件 100% 覆盖率(v8);不可达的防御性守卫使用 `/* v8 ignore */` 并注明理由,而非删除。
|
||||
- knip(死代码/依赖)、publint(包(package)正确性)、workspace 约束(workspace 规则:private、cordis peer+dev、统一版本、ESM),以及对构建出的包声明文件进行 NodeNext 消费方类型检查。
|
||||
- lefthook pre-commit(lint 暂存文件、类型检查、vendor manifest(元数据清单)守卫)和 pre-push(测试、hygiene);CI 在 Node 22.19/24/26 上运行完整矩阵,外加一个驱动 echo-agent 端到端的演示冒烟测试。
|
||||
|
||||
## 后果
|
||||
|
||||
- 约定在 agent 更替后仍然存续;违规在本地快速失败。
|
||||
- 约定在 agent 更替中得以存续;违规在本地快速失败。
|
||||
- 门禁本身也是需要维护的代码;配置变更与其他变更一样需要评审。
|
||||
- 100% 覆盖率的压力可能催生无断言的测试——变异测试是计划中的对冲手段(见[变异测试提案](../../proposed/testing/2026-06-11-mutation-testing.md))。
|
||||
- 100% 覆盖率的压力可能催生无断言的测试——变异测试是计划中的对策(见[变异测试提案](../../proposed/testing/2026-06-11-mutation-testing.md))。
|
||||
|
||||
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-tsdown-over-dumble.md: c16ac691a9452d952303cf73b40693447d25015c
|
||||
2026-06-11-tsdown-over-dumble.zh.md: 637a15a49bf22a0dd006039dd1ae2d0ea8120424
|
||||
2026-06-11-tsdown-over-dumble.zh.md: 5e9dc5242225e4420e1faa6ef19c8e8b9b3fdbcd
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
# RFC:用 tsdown 替代 dumble 进行 JS 打包
|
||||
|
||||
Status: implemented
|
||||
# RFC:使用 tsdown 替代 dumble 进行 JS 打包
|
||||
|
||||
[English](2026-06-11-tsdown-over-dumble.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
初始构建使用 **dumble**——cordiverse 的零配置 esbuild 包装层,上游 Cordis 本身也用它构建——与 vendor 包的约定最大程度对齐(它读取每个 package.json,从 `exports` 字段推断入口和格式)。但 dumble 作为本仓库的承重工具是一个隐患:v0.2.x,每周约 530 次 npm 下载,实质上只有一位维护者,而且由于它没有 workspace 模式,我们不得不通过一个自定义编排脚本(`scripts/build.ts`)来调用它。
|
||||
最初的构建使用 **dumble**,即 cordiverse 的零配置 esbuild 包装层——上游 Cordis 自身也用它构建——与 vendor 包(package)的约定最大程度对齐(它读取每个 package.json 并从 `exports` 字段推断入口/格式)。但 dumble 作为本仓库的承重工具存在隐患:v0.2.x,每周约 530 次 npm 下载,实质上只有一位维护者,而且由于它没有 workspace 模式,我们不得不通过自定义编排脚本(`scripts/build.ts`)来调用它。
|
||||
|
||||
目前构建产物只对 `pnpm run build` + publint 有意义(尚无包发布;开发/测试/演示通过 tsx 直接运行未打包的源码),因此切换成本现在最低,一旦包开始发布就只会更高。
|
||||
目前构建产物只在 `pnpm run build` + publint 中有意义(尚未发布任何包;开发/测试/演示通过 tsx 直接运行未打包的源码),因此切换成本现在最低,一旦包开始发布就只会更高。
|
||||
|
||||
## 决策
|
||||
|
||||
用 **tsdown**(基于 rolldown,每周约 250 万次下载,VoidZero 支持,活跃发布)替代 dumble:
|
||||
|
||||
- 根目录 `tsdown.config.ts`,配置 `workspace: ['vendor/*', 'packages/*/*']`(显式 glob 将打包范围限定在 vendor Cordis 和 TypeScript 包树;`workspace: true` 还会发现示例 manifest 和不需要打包的 workspace 成员)。
|
||||
- 共享形态:入口 `lib/types/index.js`,`outDir: 'lib'`,ESM,`platform: node`,`target: es2024`,`fixedExtension: false`(对 `"type": "module"` 的包保持 `.js` 扩展名),`dts: false`(声明文件由 tsc -b 负责),`clean: false`(lib/ 同时存放 TSC 的 `lib/types` 中间产物树)。入口最初是 `src/index.ts`;[TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 后来将 tsdown 改为打包 TSC 输出的 JS,使 TypeScript 转换行为来自同一个编译器。
|
||||
- vendor/ 中有两个逐包覆盖配置(属于我们的修改,与重新生成的 tsconfig 一样;记录在 vendor/README.md 中):schemastery(通过 `outExtensions` 输出双格式 `.mjs`/`.cjs`)、logger-console(两次单入口 pass,使共享基类内联到每个入口而非生成 hash 命名的 chunk,与上游发布形态一致)。
|
||||
- 删除 `scripts/build.ts`;`pnpm run build` = `tsc -b tsconfig.build.json && tsdown`。
|
||||
- 根目录 `tsdown.config.ts`,配置 `workspace: ['vendor/*', 'packages/*/*']`(显式 glob 将打包范围限定在 vendor 的 Cordis 与 TypeScript 包目录树内;`workspace: true` 还会发现示例 manifest 和不需要打包的 workspace 成员)。
|
||||
- 共享形态:入口 `lib/types/index.js`,`outDir: 'lib'`,ESM,`platform: node`,`target: es2024`,`fixedExtension: false`(为 `"type": "module"` 的包保持 `.js` 扩展名),`dts: false`(声明文件由 tsc -b 负责),`clean: false`(lib/ 同时存放 TSC 的 `lib/types` 中间产物树)。入口最初是 `src/index.ts`;[TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 后来将 tsdown 改为打包 TSC 输出的 JS,使 TypeScript 转换行为统一来自一个编译器。
|
||||
- vendor/ 中有两个按包覆盖的配置(属于我们自己的修改,与重新生成的 tsconfig 类似;记录在 vendor/README.md 中):schemastery(通过 `outExtensions` 输出双格式 `.mjs`/`.cjs`)、logger-console(两次单入口 pass,使共享基类被内联到每个入口而非生成哈希命名的 chunk,与上游发布形态一致)。
|
||||
- `scripts/build.ts` 删除;`pnpm run build` = `tsc -b tsconfig.build.json && tsdown`。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **直接编写 esbuild 脚本**:最成熟的引擎,零包装层风险,但需要手动维护 tsdown workspace 模式自动提供的逐包规格表。
|
||||
- **pkgroll**:理念上最接近的直接替代品,但每周仅 78k 下载且基于 Rollup:维护前景严格弱于 tsdown。
|
||||
- **保留 dumble**:与上游完美对齐,但 bus factor 不可接受。
|
||||
- **直接编写 esbuild 脚本**:最成熟的引擎,零包装层风险,但需要手动维护 tsdown workspace 模式自动提供的按包规格表。
|
||||
- **pkgroll**:理念上最接近的直接替代品,但每周仅 78k 下载且基于 Rollup,维护前景严格弱于 tsdown。
|
||||
- **保留 dumble**:与上游完美对齐,但巴士因子不可接受。
|
||||
|
||||
## 后果
|
||||
|
||||
运行时打包产物仍遵循 dumble 时代的公开入口形态(`lib/index.js`,加上包特有的变体如 `schemastery` 的 `lib/index.mjs`/`lib/index.cjs` 和 `logger-console` 的 `lib/browser.js`);声明文件现在按 [TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 放在 `lib/types` 下。外部依赖仍来自各包的 dependencies/peerDependencies。我们放弃了 dumble 的 exports 字段推断能力:入口形态非默认的新包需要一个逐包的 `tsdown.config.ts`,而不能仅靠 package.json 字段。未来选项:如果 `tsc -b` 成为瓶颈,tsdown 也可以接管声明文件打包(isolatedDeclarations);那将是一个新的 RFC。
|
||||
运行时打包产物仍遵循 dumble 时代的公开入口形态(`lib/index.js`,以及按包特定的变体,如 `schemastery` 的 `lib/index.mjs`/`lib/index.cjs` 和 `logger-console` 的 `lib/browser.js`);声明文件现在位于 `lib/types` 下,见 [TSC 优先构建 RFC](2026-06-17-ts-build-config.md)。外部依赖仍来自各包的 dependencies/peerDependencies。我们放弃了 dumble 的 exports 字段推断功能:新增的非默认形态的包需要编写按包的 `tsdown.config.ts`,而不能仅靠 package.json 字段。未来可选方向:如果 `tsc -b` 成为瓶颈,tsdown 还可以接管声明文件打包(isolatedDeclarations);那将是一个新的 RFC。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-vendor-cordis-as-source.md: 39506300dec73d0c9eb1b7b2246caa23f1b10f7f
|
||||
2026-06-11-vendor-cordis-as-source.zh.md: 1c942291481f50f602d6733e3c25a892885d47fe
|
||||
2026-06-11-vendor-cordis-as-source.zh.md: 0e794d97c4d535b74279bab11bda519e7da2e366
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# RFC:以源码形式收录 Cordis,而非 npm 依赖
|
||||
|
||||
Status: implemented
|
||||
# RFC:将 Cordis 以源码形式收录,而非作为 npm 依赖
|
||||
|
||||
[English](2026-06-11-vendor-cordis-as-source.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
DeepSeek Harness SDK 基于 Cordis 框架构建。本仓库启动时,Cordis core 处于 4.0.0-rc.6(一个发布候选版本);harness 依赖框架内部实现(fiber 生命周期、effect dispose(资源释放)、waterfall(瀑布式事件)分发),这些行为的精确语义直接关系到 agent loop(智能体循环)的正确性保证。
|
||||
DeepSeek Harness SDK 构建于 Cordis 框架之上。本仓库启动时,Cordis core 处于 4.0.0-rc.6(一个候选发布版本);harness 依赖框架内部实现(fiber 生命周期、dispose(资源释放)、waterfall(瀑布式事件)分发),其确切行为直接关系到 agent loop(智能体循环)的正确性保证。
|
||||
|
||||
## 决策
|
||||
|
||||
将所需的 Cordis 包(core、loader、include、group、timer、hmr、logger-console)及 cordiverse 基础库(cosmokit、schemastery)以源码形式扁平复制到 `vendor/`,保留其原始 npm 包名,使 workspace 解析透明。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍留在 npm。
|
||||
将所需的 Cordis 包(core、loader、include、group、timer、hmr、logger-console)与 cordiverse 基础库(cosmokit、schemastery)以源码形式复制到 `vendor/`,扁平化放置,保留其原始 npm 包名以实现透明的 workspace 解析。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍从 npm 获取。
|
||||
|
||||
`vendor/README.md` 是 manifest(元数据清单):记录每个包的上游仓库 + commit SHA,以及一份详尽的本地修改日志。pre-commit 守卫(`scripts/check-vendor-manifest.sh`)会拒绝未在同一次提交中更新 manifest 的 vendor 源码改动。
|
||||
`vendor/README.md` 是 manifest(元数据清单):记录每个包(package)的上游仓库 + commit SHA,以及一份详尽的本地修改日志。pre-commit 守卫(`scripts/check-vendor-manifest.sh`)会拒绝未在同一次提交中更新 manifest 的 vendor 源码变更。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **依赖 npm 包**:否决。core 处于发布候选阶段,且 harness 依赖框架内部实现(fiber 生命周期、effect dispose、waterfall 分发),agent loop 的正确性保证取决于这些行为的精确语义;上游 RC 版本升级可能在没有本地修复路径的情况下破坏它们。
|
||||
- **传递性地收录所有依赖**:否决。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍留在 npm;只有内部实现对我们有影响的框架层才被纳入自有管理。
|
||||
- **依赖 npm 包**:否决。core 处于候选发布阶段,harness 依赖框架内部实现(fiber 生命周期、dispose、waterfall 分发),agent loop 的正确性保证取决于这些行为的确切表现;上游 RC 版本升级可能在没有本地修复路径的情况下破坏它们。
|
||||
- **递归收录所有传递依赖**:否决。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍从 npm 获取;只有内部实现对我们有影响的框架层才需要自行持有。
|
||||
|
||||
## 后果
|
||||
|
||||
- harness 完全拥有其框架层:可审计、可打补丁、版本锁定。上游 RC 无法破坏我们,框架 bug 可以在仓库内直接修复。
|
||||
- 上游同步是手动的(manifest 中记录了操作步骤)。修改日志使 diff 面始终可知。
|
||||
- vendor 包保留上游代码风格;lint 与严格性门禁将其排除(它们的 tsconfig 在本地放宽了我们较新的编译器 flag)。
|
||||
- 从第一天起就存在一个本地补丁:移除了 HMR 的 locale-YAML 导入(运行时 YAML 导入钩子未被收录)。
|
||||
- harness 完全持有其框架层:可审计、可打补丁、版本锁定。上游 RC 无法影响我们,框架 bug 可以在仓库内直接修复。
|
||||
- 上游同步是手动操作(流程记录在 manifest 中)。修改日志使 diff 范围始终可知。
|
||||
- 收录的包保留上游代码风格;lint 与严格性门禁将其排除(它们的 tsconfig 在本地放宽了我们较新的编译器选项)。
|
||||
- 从第一天起就有一个本地补丁:移除了 hmr 的 locale-YAML 导入(运行时 YAML 导入钩子未被收录)。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-16-pnpm-over-yarn.md: 6e7a6e1f53056e36f54f44b87b305afa593da549
|
||||
2026-06-16-pnpm-over-yarn.zh.md: 809f10dbd63d347eccb4d00641a39da51788ee9f
|
||||
2026-06-16-pnpm-over-yarn.zh.md: ba63575909f2bafbbad2102c85d6bede77999997
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
# RFC:以 pnpm 替代 Yarn 4 作为包管理器
|
||||
|
||||
Status: implemented
|
||||
# RFC:使用 pnpm 替代 Yarn 4 作为包管理器
|
||||
|
||||
[English](2026-06-16-pnpm-over-yarn.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
本仓库最初使用 **Yarn 4** 搭配 `node-modules` linker 发布——这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时提供 Yarn 的 workspace 和 `yarn constraints`。它能用。但 Yarn 4 的 Plug'n'Play 血统使得 `node-modules` linker 成为非主流模式,而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent 构建、偶尔有人类贡献者阅读的仓库来说,「大多数工具和人所预期的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的答案。
|
||||
本仓库最初使用 **Yarn 4** 搭配 `node-modules` 链接器启动。这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时享有 Yarn 的 workspaces 和 `yarn constraints`。它能正常工作。但 Yarn 4 源自 Plug'n'Play 的血统,使得 `node-modules` 链接器成为非主流模式;而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者的熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent(智能体)构建、偶尔有人类贡献者阅读的仓库而言,「大多数工具和人所期望的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的解答。
|
||||
|
||||
切换成本目前处于最低点。本仓库尚无任何包发布(所有 package 均为 `private: true`);开发/测试/演示全部通过 tsx **未构建**运行,因此包管理器只需做到 (a) 解析并链接 `node_modules`,(b) 运行 workspace 脚本,(c) 强制执行 workspace 约束。唯一的 Yarn 专属资产是 `yarn.config.cjs`(`@yarnpkg/types` 约束引擎),体量小且可机械地重新表达。这与 [tsdown 决策](2026-06-11-tsdown-over-dumble.md)的逻辑一致:趁爆炸半径还小,把承重工具换成生态更健康的选项。
|
||||
切换成本目前处于最低点。本仓库尚无任何包(package)发布(每个包都是 `private: true`);开发/测试/演示全部通过 tsx **未构建**运行,因此包管理器只需做到:(a) 解析并链接 `node_modules`,(b) 运行 workspace 脚本,(c) 强制执行 workspace 约束。唯一的 Yarn 特有资产是 `yarn.config.cjs`(`@yarnpkg/types` 约束引擎),体量小且可机械地重新表达。这与 [tsdown 决策](2026-06-11-tsdown-over-dumble.md)的逻辑一致:在爆炸半径尚小时,将承重工具换为生态更健康的选项。
|
||||
|
||||
## 决策
|
||||
|
||||
采用 **pnpm 11.7.0**,通过 `packageManager` 字段固定、经 Corepack 安装(与 Yarn 使用的机制相同):
|
||||
采用 **pnpm 11.7.0**,通过 `packageManager` 字段固定版本,经 Corepack 安装(与 Yarn 使用的机制相同):
|
||||
|
||||
- **Workspace** 从 `package.json` 的 `workspaces` 数组加 `.yarnrc.yml` 迁移到 `pnpm-workspace.yaml`(`vendor/*`、`packages/*`——相同的 glob;`examples/*` 保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。
|
||||
- **严格符号链接 linker**(pnpm 默认)取代 Yarn 的 hoisted `node-modules` linker。我们刻意**不**添加 `node-linker=hoisted` / `shamefully-hoist` 逃生口:pnpm 的非扁平 `node_modules` 会让幽灵依赖(引用未声明的传递依赖)大声失败,这对一个以机械门禁为整体质量策略的仓库而言是一个*优点*(见[机械质量门禁](2026-06-11-quality-gates.md))。门禁套件——类型检查、lint、测试、构建、knip——是安全网,证明不存在此类幽灵引用。
|
||||
- **构建脚本白名单。** pnpm 10+ 不运行依赖的生命周期脚本,除非显式列入白名单。`pnpm-workspace.yaml` 携带一份显式的 `allowBuilds` 映射(`esbuild`、`lefthook`、`@google/genai`、`protobufjs`)——与本仓库对模型/工具输出已有的供应链加固姿态一致,现在将其扩展到安装时的代码执行。`peerDependencyRules.allowedVersions.typescript: '>=5 <7'` 消除仓库内 TypeScript 的良性 peer 范围警告。
|
||||
- **约束变为包管理器无关。** `yarn.config.cjs`(导入 `@yarnpkg/types`、使用 `Yarn.workspaces()` / `workspace.set()`)被 `scripts/check-workspace-constraints.ts` 取代——一个纯 tsx 脚本,以 `pnpm run constraints` 运行。它在相同的 `vendor` + `packages` 范围上强制执行完全相同的不变式:所有 package `private: true`;`@deepseek-ai/dsh-*` 包将 `cordis` 同时声明为对等依赖(peer dependency)和 dev 依赖且范围匹配、使用根 `package.json` 的版本、设置 `type: module`;vendor 包仅检查 privacy。
|
||||
- 所有 CI、lefthook 钩子、`package.json` 脚本和文档中的 `yarn …` 动词统一改为 `pnpm …` / `pnpm run …`。`yarn.lock` → `pnpm-lock.yaml`(lockfile v9)。`.gitignore` 将 `.yarn/` 换为 `.pnpm-store/`。vendor README(如 `vendor/cordis/README.md`)按 Vendoring Policy 保留其上游的 `yarn` 示例不动。
|
||||
- **Workspaces** 从 `package.json` 的 `workspaces` 数组 + `.yarnrc.yml` 迁移到 `pnpm-workspace.yaml`(`vendor/*`、`packages/*`——同样的 glob;`examples/*` 保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。
|
||||
- **严格符号链接链接器**(pnpm 默认)取代 Yarn 的提升式 `node-modules` 链接器。我们刻意**不**添加 `node-linker=hoisted` / `shamefully-hoist` 逃生口:pnpm 的非扁平 `node_modules` 会让幻影依赖(引用未声明的传递依赖)大声失败,这对于一个以机械门禁为核心质量保障的仓库(见[机械质量门禁](2026-06-11-quality-gates.md))是一项*优势*。门禁套件(typecheck、lint、test、build、knip)是证明不存在此类幻影导入的安全网。
|
||||
- **构建脚本白名单。** pnpm 10+ 不运行依赖的生命周期脚本,除非将其加入白名单。`pnpm-workspace.yaml` 携带一份显式的 `allowBuilds` 映射(`esbuild`、`lefthook`、`@google/genai`、`protobufjs`)——与本仓库对模型/工具输出已有的供应链加固姿态一致,现在也应用于安装时的代码执行。`peerDependencyRules.allowedVersions.typescript: '>=5 <7'` 消除仓库内 TypeScript 的良性 peer 范围警告。
|
||||
- **约束变为包管理器无关。** `yarn.config.cjs`(导入 `@yarnpkg/types`,使用 `Yarn.workspaces()` / `workspace.set()`)被 `scripts/check-workspace-constraints.ts` 取代——一个纯 tsx 脚本,通过 `pnpm run constraints` 运行。它在相同的 `vendor` + `packages` 范围上强制执行完全相同的不变式:每个包 `private: true`;`@deepseek-ai/dsh-*` 包将 `cordis` 同时声明为对等依赖(peer dependency)和 dev 依赖且范围一致、使用根 `package.json` 的版本、设置 `type: module`;vendor 包仅检查 privacy。
|
||||
- 所有 CI、lefthook 钩子、`package.json` 脚本和文档中的 `yarn …` 动词变为 `pnpm …` / `pnpm run …`。`yarn.lock` → `pnpm-lock.yaml`(lockfile v9)。`.gitignore` 将 `.yarn/` 换为 `.pnpm-store/`。vendor README(如 `vendor/cordis/README.md`)按 Vendoring Policy 保持其上游 `yarn` 示例不变。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **保留 Yarn 4**:零变动,但押注于使用者更少的 linker 模式和绑定单一包管理器的约束引擎。
|
||||
- **npm workspaces**:无处不在,但没有约束机制,monorepo 人体工学也更弱。
|
||||
- **pnpm 搭配 hoisted linker**:迁移更平滑,但放弃了幽灵依赖安全性——而这正是迁移的首要正确性理由。
|
||||
- **保留 Yarn 4**——零变动,但押注于使用率较低的链接器模式和一个绑定单一包管理器的约束引擎。
|
||||
- **npm workspaces**——无处不在,但没有约束方案,monorepo 人体工学也较弱。
|
||||
- **pnpm 搭配提升式链接器**——迁移更平滑,但放弃了幻影依赖安全性,而这正是迁移的核心正确性理由。
|
||||
|
||||
## 后果
|
||||
|
||||
约束检查失去了 Yarn 的自动**修复**能力(`workspace.set()` 可以就地改写 manifest);tsx 脚本仅做检查,不通过时以非零退出码加消息退出。这是可接受的:CI 从未运行过 `--fix`,且需要手动改一行的情况很少。贡献者现在为 pnpm 而非 Yarn 运行 `corepack enable`;`pnpm exec lefthook install` 取代 `yarn lefthook install`(`postinstall` 钩子仍会运行 `lefthook install`)。
|
||||
约束检查失去了 Yarn 的自动**修复**能力(`workspace.set()` 能原地改写 manifest);tsx 脚本仅做检查,不通过时以非零退出码和消息退出。这是可接受的:CI 从未运行过 `--fix`,且需要手动编辑的情况很少。贡献者现在为 pnpm 而非 Yarn 运行 `corepack enable`;`pnpm exec lefthook install` 取代 `yarn lefthook install`(`postinstall` 钩子仍会运行 `lefthook install`)。
|
||||
|
||||
性能(迁移时在开发 NFS 文件系统上测量;单次运行样本,方差大——仅供方向性参考,非基准测试套件):
|
||||
|
||||
| 场景 | Yarn 4 | pnpm 11 |
|
||||
|---|---|---|
|
||||
| Cold (empty cache/store, no `node_modules`) | ~14 s | ~16 s |
|
||||
| Warm relink (cache/store warm, `node_modules` removed) | ~12–14 s | ~15–22 s |
|
||||
| Frozen, `node_modules` present (no-op revalidate) | ~2–8 s | ~0.5–7 s |
|
||||
| 冷启动(空缓存/store,无 `node_modules`) | ~14 s | ~16 s |
|
||||
| 热重链接(缓存/store 已热,`node_modules` 已删除) | ~12–14 s | ~15–22 s |
|
||||
| 冻结,`node_modules` 存在(无操作重验证) | ~2–8 s | ~0.5–7 s |
|
||||
|
||||
在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装上胜出,尤其在多次 checkout 的**磁盘占用**上优势明显(一个全局 store 通过硬链接进入每个 `node_modules`,而 Yarn 每个 worktree 复制约 279 MB——部分开发者日常保持约 10 个或更多 worktree)。这一去重优势在上述迁移时数据中**未**体现,因为测试 store 和 `node_modules` 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上该优势成立。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幽灵依赖安全性和跨 checkout 磁盘去重——而非原始安装时间的胜出。
|
||||
在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装中胜出,尤其在多个检出之间的**磁盘占用**方面优势明显(一个全局 store 通过硬链接接入每个 `node_modules`,而 Yarn 每个 worktree 复制约 279 MB——部分开发者经常为本仓库保持约 10 个或更多 worktree)。该去重优势在上述迁移时数据中**未能**体现,因为测试 store 和 `node_modules` 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上则适用。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幻影依赖安全性和跨检出磁盘去重,而非原始安装时间的胜出。
|
||||
|
||||
所有质量门禁(约束、类型检查、lint、doc-sync、100% test:coverage、构建、knip、publint、echo-agent 演示冒烟测试)在 pnpm 上原样通过,这是 linker 切换未引入幽灵依赖破坏的正确性证明。
|
||||
所有质量门禁(constraints、typecheck、lint、doc-sync、test:coverage 100%、build、knip、publint、echo-agent 演示冒烟测试)在 pnpm 上原样通过,这是链接器切换未引入幻影依赖破坏的正确性证明。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-17-ts-build-config.md: cf70014b5873f74da8476c21dd71feedb956f59f
|
||||
2026-06-17-ts-build-config.zh.md: f70619de5a48c7040e816c54d21f81772a51a90f
|
||||
2026-06-17-ts-build-config.zh.md: d3dd0fb13edd22f1ae365286cbf8144fa0bc3f69
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
# RFC:以 TSC 为核心的构建与统一 tsconfig
|
||||
|
||||
Status: implemented
|
||||
# RFC:TSC 优先的构建与单一 tsconfig
|
||||
|
||||
[English](2026-06-17-ts-build-config.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
当时的 TypeScript 构建与类型检查配置存在以下问题:
|
||||
此前的 TypeScript 构建与类型检查配置存在以下问题:
|
||||
|
||||
- `build` 使用 `tsc` 将 `packages/<group>/<pkg>` 和 `vendor/*` 下的 `.ts` 转换为 `.d.ts`,再用 `tsdown` 将 `.ts` 转换为打包后的 `.js`。这导致两个工具各自做一次 TypeScript 转换。
|
||||
- `typecheck` 倾向于通过一个根级 typecheck 配置来校验 package、vendor 源码、示例、测试和脚本。
|
||||
- `build` 使用 `tsc` 将 `packages/<group>/<pkg>` 和 `vendor/*` 下的 `.ts` 转换为 `.d.ts` 文件,然后使用 `tsdown` 将 `.ts` 转换为打包后的 `.js` 文件。这导致两个工具各自执行 TypeScript 转换。
|
||||
- `typecheck` 倾向于通过一个根目录的 typecheck 配置来校验 package、vendor 源码、示例、测试和脚本。
|
||||
|
||||
目标是让 build 和 typecheck 使用一致的 tsconfig 边界与 TypeScript 解析/转换行为。build 应通过同一个编译器和配置生成 `.js`、`.d.ts`、`.js.map` 和 `.d.ts.map`,使发布产物与类型校验保持一致。
|
||||
目标是让构建与类型检查使用一致的 tsconfig 边界和 TypeScript 解析/转换行为。构建应通过单一编译器和配置生成 `.js`、`.d.ts`、`.js.map` 和 `.d.ts.map`,使发布产物与类型校验保持一致。
|
||||
|
||||
验证过程中发现了若干具体技术问题和可能的路径:
|
||||
验证过程中发现了若干具体的技术问题和可能的路径:
|
||||
|
||||
- `tsdown` 使用 `oxc` 做 TypeScript 转换,其行为与 `tsc` 不同。
|
||||
- `tsdown` 使用 `oxc` 进行 TypeScript 转换,其行为与 `tsc` 不同。
|
||||
- `tsdown` 输出的打包 `.d.ts` 与 Cordis 内部的相对模块增强(module augmentation)结构冲突。
|
||||
- tsc 的输出受 `allowImportingTsExtensions` 影响,因此需要确保生成的 `.js` 不会 import `.ts` 文件,且生成的 `.d.ts` 保留 NodeNext/Node16 可接受的显式相对说明符。为此,包内相对导入在 TypeScript 源码中使用显式 `.ts` 说明符,由 `rewriteRelativeImportExtensions` 在输出的 JS 中将其改写为 `.js`。
|
||||
- `tsdown` 输出的打包 `.js` 与 `tsc -b` 逐文件输出的 `.js` 行为不同,例如 decorator 转换行为。
|
||||
- `vendor/*/src`、示例、测试和脚本无法全部以 plain-include 方式放入一个根级严格程序。
|
||||
- 在根级严格配置下直接对 `vendor/*/src` 做类型检查,会触发大量不属于本项目的类型错误。
|
||||
- `packages/*/*` 对 `vendor` 的依赖解析到 `vendor/*/lib`,以适应不同的 tsconfig 严格度。
|
||||
|
||||
- `tsc` 的输出受 `allowImportingTsExtensions` 影响,因此需要确保生成的 `.js` 文件不会导入 `.ts` 文件,且生成的 `.d.ts` 文件保留 NodeNext/Node16 接受的显式相对说明符。为此,包内相对导入在 TypeScript 源码中使用显式 `.ts` 说明符,由 `rewriteRelativeImportExtensions` 在输出的 JS 中将其重写为 `.js`。
|
||||
- `tsdown` 输出的打包 `.js` 与 `tsc -b` 逐文件输出的 `.js` 行为不同,例如装饰器转换行为。
|
||||
- `vendor/*/src`、示例、测试和脚本无法全部以 plain-include 方式纳入一个根目录的严格程序。
|
||||
- 在根目录严格配置下直接对 `vendor/*/src` 做类型检查,会触发大量不属于本项目所有权范围的类型错误。
|
||||
- `packages/*/*` 对 `vendor` 的包依赖解析到 `vendor/*/lib`,以适应不同的 tsconfig 严格度。
|
||||
|
||||
## 决策
|
||||
|
||||
包内相对导入使用显式 `.ts` 说明符。
|
||||
|
||||
`pnpm run build` 分两阶段:
|
||||
`pnpm run build` 是两阶段构建:
|
||||
|
||||
- 阶段 1:`tsc -b tsconfig.build.json` 将逐模块的 `.js`、声明文件 `.d.ts`、JS sourcemap `.js.map` 和声明 sourcemap `.d.ts.map` 输出到各包的 `lib/types`。这是权威的 TypeScript 编译结果。发布时保留 `.d.ts` / `.d.ts.map`,忽略 `.js` / `.js.map`。
|
||||
- 构建项目使用 `tsc -b` 编译的 project-reference 图。例如,根 `tsconfig.build.json` 引用包和 vendor 的 tsconfig,校验并输出包/vendor 的构建结果。
|
||||
- 阶段 2:bundler 读取 `lib/types` 下输出的 JS,将打包后的运行时入口写为 `lib/index.js` 或 `lib/index.mjs`(沿用当前行为)。此阶段仅做打包,不得读取 TypeScript 源码,也不得输出声明文件。
|
||||
- 阶段 1:`tsc -b tsconfig.build.json` 将逐模块的 `.js`、声明文件 `.d.ts`、JS sourcemap `.js.map` 和声明 sourcemap `.d.ts.map` 输出到各 package 的 `lib/types`。这是权威的 TypeScript 编译结果。发布时保留 `.d.ts` / `.d.ts.map`,忽略 `.js` / `.js.map`。
|
||||
- 构建项目使用 `tsc -b` 编译的 project-reference 图。例如,根 `tsconfig.build.json` 引用 package 和 vendor 的 tsconfig,校验并输出 package/vendor 的构建结果。
|
||||
- 阶段 2:打包器读取 `lib/types` 下输出的 JS,将打包后的运行时入口写为 `lib/index.js` 或 `lib/index.mjs`(沿用当前行为)。此阶段仅做打包,禁止读取 TypeScript 源码或输出声明文件。
|
||||
|
||||
`tsdown` 不再负责 TypeScript 编译或声明文件输出。
|
||||
|
||||
`pnpm run typecheck` 以 build 模式运行根 `tsconfig.json`。
|
||||
- 根 `tsconfig.json` 是唯一的开发/类型检查项目。它以 `noEmit` 检查示例、测试和脚本,并通过 references 校验包/vendor 源码。
|
||||
- 被引用的包/vendor 项目保持与 build 相同的输出行为,因此 typecheck 可以刷新它们的 `lib/types` 产物,而无需使用单独的 no-emit 图。项目特有的严格度设置放在各自的 `packages/*/*/tsconfig.json` 或 `vendor/*/tsconfig.json` 中。
|
||||
- 根 no-emit 项目禁用 `rewriteRelativeImportExtensions`;它不输出任何文件,且包含跨 project-reference 边界导入 helper 的测试。包/vendor 的输出项目保持该改写启用。
|
||||
- 根 `tsconfig.json` 是唯一的开发/类型检查项目。它以 `noEmit` 方式检查示例、测试和脚本,并通过 references 校验 package/vendor 源码。
|
||||
- 被引用的 package/vendor 项目保持与 build 相同的输出行为,因此 typecheck 可以刷新它们的 `lib/types` 输出,而无需使用独立的 no-emit 图。项目特定的严格度变更放在各自的 `packages/*/*/tsconfig.json` 或 `vendor/*/tsconfig.json` 中。
|
||||
- 根 no-emit 项目禁用 `rewriteRelativeImportExtensions`;它不输出任何文件,且包含跨 project-reference 边界导入 helper 的测试。package/vendor 的 emit 项目保持重写开启。
|
||||
|
||||
命令编排如下:
|
||||
命令编排结构如下:
|
||||
|
||||
```sh
|
||||
pnpm run build:
|
||||
@@ -59,20 +58,20 @@ tsc -b tsconfig.json
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **继续使用 `tsdown`/oxc 作为 TypeScript 转换器**:oxc 的转换行为与 `tsc` 不同(decorator 转换有差异、打包 JS 与逐文件输出不同),且其打包 `.d.ts` 与 Cordis 内部的相对模块增强结构冲突。
|
||||
- **一个根级严格程序覆盖包、vendor、示例、测试和脚本**:vendor 源码在根级严格 flag 下会触发不属于本项目的类型错误;带有各项目独立严格度的 project references 才是可行的边界。
|
||||
- **继续使用 `tsdown`/oxc 作为 TypeScript 转换器**:oxc 的转换行为与 `tsc` 不同(装饰器转换有差异、打包 JS 与逐文件输出不同),且其打包 `.d.ts` 与 Cordis 内部的相对模块增强结构冲突。
|
||||
- **用一个根目录严格程序覆盖 package、vendor、示例、测试和脚本**:vendor 源码在根目录严格标志下会触发不属于本项目所有权范围的类型错误;带有逐项目严格度的 project references 才是可行的边界。
|
||||
|
||||
## 后果
|
||||
|
||||
构建职责更加清晰:
|
||||
|
||||
- `packages/<group>/<pkg>` 和 `vendor/*` 下的每个模块都有一个本地 tsconfig,同时服务于 build、typecheck 以及直接运行源码的工具(如 `tsx` 和 `vitest`)。
|
||||
- `build` 命令使用 `tsconfig.build.json`。`tsc -b` 负责可发布的逐模块 `.js` 和 `.d.ts` 输出,bundler 只负责 `lib/index.*`。
|
||||
- `lib/types/*.d.ts` 和 `.d.ts.map` 是发布用的声明文件产物。
|
||||
- `packages/<group>/<pkg>` 和 `vendor/*` 下的每个模块有一份本地 tsconfig,同时服务于构建、类型检查和直接运行源码的工具(如 `tsx` 和 `vitest`)。
|
||||
- `build` 命令使用 `tsconfig.build.json`。`tsc -b` 负责可发布的逐模块 `.js` 和 `.d.ts` 输出,打包器仅负责 `lib/index.*`。
|
||||
- `lib/types/*.d.ts` 和 `.d.ts.map` 是发布用的声明输出。
|
||||
- `lib/types/*.d.ts` 使用显式 `.ts` 相对说明符,TypeScript 的 NodeNext/Node16 解析器会将其映射到同级的 `.d.ts` 文件。
|
||||
- `lib/types/*.js` 仅作为 bundler 输入,不得用作运行时入口或公开导入目标。
|
||||
- `lib/index.*` 是发布用的运行时产物,由 bundler(当前为 `tsdown`)生成。
|
||||
- `pnpm run verify-node-next-types` 扫描构建出的声明文件,检查是否存在缺少文件扩展名的相对说明符,然后以 `moduleResolution: "NodeNext"` 对构建出的 `types`/`exports` 表面进行临时外部 ESM 消费方的类型检查,使声明说明符的回归在发布前即被捕获。
|
||||
- `typecheck` 命令使用 `tsconfig.json`。示例、测试和脚本由根 no-emit 项目检查,包和 vendor 模块保持与 `build` 相同的输出行为。包和 vendor 源码始终处于 project-reference 边界之后。
|
||||
- `lib/types/*.js` 仅作为打包器输入,禁止用作运行时入口或公开导入目标。
|
||||
- `lib/index.*` 是发布用的运行时输出,由打包器(当前为 `tsdown`)生成。
|
||||
- `pnpm run verify-node-next-types` 扫描构建出的声明文件,检查是否存在缺少文件扩展名的相对说明符,然后以 `moduleResolution: "NodeNext"` 对构建出的 `types`/`exports` 接口进行临时外部 ESM 消费方的类型检查,确保声明说明符的回归在发布前被捕获。
|
||||
- `typecheck` 命令使用 `tsconfig.json`。示例、测试和脚本由根 no-emit 项目检查,package 和 vendor 模块保持与 `build` 相同的输出行为。package 和 vendor 源码始终处于 project-reference 边界之后。
|
||||
|
||||
Cordis vendor 副本现在与上游多了一处类型结构差异。上游同步时,必须重新应用该差异或明确将其退役。
|
||||
Cordis 的 vendor 副本现在与上游多了一处类型结构差异。在上游同步时,该差异必须被重新应用或明确废弃。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-18-markdown-cross-link-lint.md: c802b4071abf652824647e4417cde3f518776353
|
||||
2026-06-18-markdown-cross-link-lint.zh.md: abbb5930acb18287effc7c47009b9e3e910f6c89
|
||||
2026-06-18-markdown-cross-link-lint.zh.md: 917580ffce71258896f3d23ef7efef4be0176949
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
# RFC:Markdown 交叉链接有效性 lint
|
||||
|
||||
Status: implemented
|
||||
# RFC:Markdown 交叉链接有效性检查
|
||||
|
||||
[English](2026-06-18-markdown-cross-link-lint.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
本仓库的文档通过相对路径互相链接:`[topic](../implemented/2026-…-….md)`、`[the cookbook](adding-a-tool.md)`、`[architecture.md](../../architecture.md)`。此前没有任何机制验证这些目标是否存在。一次重命名或移动会悄无声息地打断所有入站链接,直到读者点击时才会发现。[Doc-sync 强制](2026-06-11-doc-sync-enforcement.md)已经将两类文档漂移机械化了(不可编译的代码块、陈旧的事件分类体系表),[verify-md-wrap](2026-06-11-doc-sync-enforcement.md) 处理了第三类(硬换行的行文段落),但死链接是第四类同样可机械检查的问题,此前仍靠肉眼验证。
|
||||
本仓库的文档通过相对路径互相链接:`[topic](../implemented/2026-…-….md)`、`[the cookbook](adding-a-tool.md)`、`[architecture.md](../../architecture.md)`。此前没有任何机制验证这些目标是否存在。重命名或移动文件会静默破坏所有指向它的链接,且在读者点击之前不可见。[Doc-sync 强制](2026-06-11-doc-sync-enforcement.md)已经将两类文档漂移机械化(无法编译的代码块、陈旧的事件分类表),[verify-md-wrap](2026-06-11-doc-sync-enforcement.md) 覆盖了第三类(硬换行的段落),但死链是第四类同样可机械检查、却仍靠肉眼验证的问题。
|
||||
|
||||
直接触发本门禁的案例是引入它的那次 RFC 目录重组:将 `docs/adr/` + `docs/rfc/` 统一为一个 `docs/rfc/`,下设 `proposed/`、`implemented/`、`rejected/` 子目录,手动改写了约四十条文档间链接。任何一条路径的手误都会让断链随代码一起合入,而没有任何东西能拦住它。
|
||||
触发本门禁的直接案例是引入它的那次 RFC 目录重组:将 `docs/adr/` + `docs/rfc/` 统一为一个 `docs/rfc/`,下设 `proposed/`/`implemented/`/`rejected/` 子目录,手动改写了约四十条文档间链接。任何一个手误路径都会让一条死链随代码入库,而没有任何东西能拦住它。
|
||||
|
||||
## 决策
|
||||
|
||||
新增第四道 `doc-sync` 门禁 `verify-md-links`(`scripts/verify-md-links.ts`),风格与 `verify-md-wrap` 一致(tsx ESM、基于 AST、只验证不生成):
|
||||
|
||||
- 用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件,遍历所有 `link`、`image` 和 `definition` 节点。
|
||||
- 仅当目标是**相对路径**时才检查。跳过带协议的 URL(`https:`、`mailto:` 等)、协议相对路径(`//host`)、根绝对路径(`/path`——在 checkout 中没有稳定基准)以及纯页内锚点(`#section`)。去除 `#fragment`/`?query`,相对于链接所在文件的目录解析路径,并断言该路径在磁盘上存在。
|
||||
- 只报告,不改写;发现第一条断链即以非零状态退出。
|
||||
- 使用 `mdast-util-from-markdown` + GFM 解析每个范围内的 Markdown 文件,遍历所有 `link`、`image` 和 `definition` 节点。
|
||||
- 仅当目标是**相对路径**时才检查。跳过带协议的 URL(`https:`、`mailto:` 等)、协议相对路径(`//host`)、根绝对路径(`/path`,在检出目录中没有稳定基准)以及纯页内锚点(`#section`)。剥除 `#fragment`/`?query`,相对于链接所在文件的目录解析路径,并断言目标在磁盘上存在。
|
||||
- 只报告、不改写;发现第一条死链即以非零状态退出。
|
||||
|
||||
范围与其他门禁一致,另加 AGENTS.md 对和 `.agents/skills/` 下仓库自有的 agent skill Markdown(这些 skill 文件交叉链接到 docs 目录树,因此本次重组也改写了其中的链接):`README.md`、`docs/**/*.md`、`packages/*/README.md`、`AGENTS.md`、`packages/AGENTS.md`、`.agents/skills/**/*.md`,按真实路径去重(`CLAUDE.md` 符号链接解析到 AGENTS.md 文件)。该门禁接入 lefthook pre-push 钩子和 CI 都会运行的 `doc-sync` 脚本,因此断链在推送前就会在本地失败——与[机械质量门禁](2026-06-11-quality-gates.md)保持一致。
|
||||
范围与其他门禁一致,另外加上 AGENTS.md 对和 `.agents/skills/` 下仓库自有的 agent skill Markdown(这些 skill 文件交叉链接到 docs 目录,因此本次重组也改写了其中的链接):`README.md`、`docs/**/*.md`、`packages/*/README.md`、`AGENTS.md`、`packages/AGENTS.md`、`.agents/skills/**/*.md`,按真实路径去重(`CLAUDE.md` 符号链接解析到 AGENTS.md 文件)。它接入 lefthook pre-push 钩子和 CI 都会运行的 `doc-sync` 脚本,因此死链在推送前就会在本地失败——与[机械化质量门禁](2026-06-11-quality-gates.md)一致。
|
||||
|
||||
本门禁检查的是**文件存在性**,而非锚点有效性:链接到一个真实文件但带有 `#wrong-heading` 片段的仍然通过(文件可解析;片段被剥离)。
|
||||
本门禁检查的是**文件存在性**,而非锚点有效性:指向一个真实文件但带有 `#wrong-heading` 片段的链接仍会通过(文件可解析;片段被剥除)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**锚点级有效性检查**:更重且价值更低;实际造成问题的是文件级死链接。这一范围裁剪是有意为之:作者在链接到某个锚点时自行验证 `#fragment`。
|
||||
**锚点级有效性检查**:更重且价值更低;实际造成问题的是文件级死链。这一范围裁剪是有意为之:作者在链接到某个锚点时自行验证 `#fragment`。
|
||||
|
||||
## 后果
|
||||
|
||||
- 重命名或移动导致交叉链接悬空时,pre-push 钩子和 CI 会立即失败,而不是等读者点击死链接才发现。这使得引入本门禁的 RFC 重组具有自验证性:同一个 PR 既改写了四十条链接,也加入了证明无一悬空的检查。
|
||||
- `doc-sync` 链中多了一个快速 tsx 脚本;无新增依赖(mdast/GFM 技术栈已在 devDependencies 中供 `verify-md-wrap` 使用)。
|
||||
- 本门禁强制的约定——通过可机械检查的相对链接交叉引用文档,而非裸文字或编号——记录在 [docs/AGENTS.md](../../../AGENTS.md) 中,让作者知道这道门禁的存在及其原因。
|
||||
- 重命名或移动文件导致交叉链接悬空时,现在会在 pre-push 钩子和 CI 中失败,而不是等读者点击死链才发现。这使得引入本门禁的 RFC 重组具备自验证能力:改写四十条链接的同一个 PR 也添加了证明无一悬空的检查。
|
||||
- `doc-sync` 链中多了一个快速 tsx 脚本;无新增依赖(mdast/GFM 技术栈已作为 `verify-md-wrap` 的 devDependencies 存在)。
|
||||
- 本门禁强制的约定——通过可机械检查的相对链接引用文档,而非裸文本或编号——记录在 [docs/AGENTS.md](../../../AGENTS.md) 中,让作者知晓门禁的存在与原因。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: 5f1232f2f0d0644d4043af217a7177451155030b
|
||||
2026-06-20-core-data-structures-catalog.zh.md: d35a4d9d32eb59971bbf8b105d01dd38c0811fb0
|
||||
2026-06-20-core-data-structures-catalog.zh.md: 8ad4453890d8be5dc4e743d1f6f9aa6a9330ed17
|
||||
|
||||
@@ -6,55 +6,55 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
想要理解 harness 的读者可以在 [architecture.md](../../../architecture.md) 中找到它的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系),但没有一个集中的地方描述它的*词汇*——那些行为所操作的数据结构。类型定义只存在于源码中,散落在各个 `packages/*/src/types.ts` 里,因此要理解「什么是 `Message`、`SessionEvent`、`StreamChunk`」就意味着直接阅读声明。一份行文目录会有帮助,但如果目录是对类型定义的转述或粘贴复制,那么字段一改它就会腐烂——而一份失去同步的类型文档比没有更糟,因为读者会信任它。
|
||||
一位想要理解 harness 的读者,可以在 [architecture.md](../../../architecture.md) 中找到它的*行为*(服务映射、session/turn/step 生命周期、事件分类体系),但没有一个集中的地方描述它的*词汇*——即行为所操作的数据结构。类型形状只存在于源码中,分散在各个 `packages/*/src/types.ts` 里,因此要理解「什么是 `Message`、`SessionEvent`、`StreamChunk`」就得直接阅读声明。一份行文目录会有帮助,但如果目录是对类型定义的转述或粘贴复制,那么字段一改它就会腐烂——而失去同步的类型文档比没有更糟,因为读者会信任它。
|
||||
|
||||
因此这项工作包含两个交织的问题:**这样一份目录应当收录什么**(范围界定问题:一个 harness 有数十个跨包(package)的类型,全部堆上去对谁都没帮助),以及**如何防止粘贴的类型定义漂移**(持久性问题)。本 RFC 记录这两项决策。它的姊妹篇 [生成式 Cordis 事件 + 服务目录](2026-06-20-generated-cordis-catalog.md) 是*接线*轴向的补充:本篇编目数据结构,那篇编目移动它们的事件与服务。
|
||||
因此这项工作包含两个交织的问题:**这样的目录应当收录什么**(范围界定问题——一个 harness 有数十个跨包类型,全部堆上去对谁都没帮助),以及**如何防止粘贴的类型定义漂移**(持久性问题)。本 RFC 记录两项决策。它的姊妹篇 [生成式 Cordis 事件 + 服务目录](2026-06-20-generated-cordis-catalog.md) 是*接线*轴的补充:本篇编目数据结构,那篇编目传递数据结构的事件与服务。
|
||||
|
||||
## 决策
|
||||
|
||||
新建 `docs/core-data-structures/` 目录编目词汇,并新增 `verify-type-equiv` doc-sync(文档同步门禁)门禁,确保每一处粘贴的类型定义与源码逐字节一致。
|
||||
新建 `docs/core-data-structures/` 文件夹编目词汇,并新增 `verify-type-equiv` doc-sync(文档同步门禁)门禁,确保每处粘贴的类型定义与源码逐字节一致。
|
||||
|
||||
### 什么算「核心」——主干与 seam 的分界线
|
||||
### 何为"核心"——主干与 seam 的分界线
|
||||
|
||||
范围界定不是自上而下拍板的,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop 主干;如果这些算「核心」,那「核心」就等于*所有跨包词汇*,目录就是一份平铺的全量转储;如果它们不算,「核心」就意味着*中央主干*,bash 词汇属于子页面。后者胜出,由此确定了整体结构:一个**分层目录**,而非一份平铺文档。
|
||||
范围界定并非自上而下拍定,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop(智能体循环)主干;如果这些算"核心",那么"核心"就意味着*所有跨包词汇*,目录沦为平铺罗列;如果不算,"核心"就意味着*中央主干*,bash 词汇归入子页面。后者胜出,由此确定了整体结构:一个**分层文件夹**,而非一份平铺文档。
|
||||
|
||||
解决剩余案例的规则:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下:
|
||||
确定其余案例的规则是:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下:
|
||||
|
||||
- 一个数据结构是**核心**的,如果它流经 agent loop 主干——无论加载了哪些插件,循环在每个轮次都持有、派生、流式输出或记录它(`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄)——**或者**它是插件作者面向某条流水线编写的唯一标题类型(`ToolDefinition`)。
|
||||
- `ToolDefinition` 是核心(它是每个工具作者编写的东西),**即使循环从不持有它**——对于这一个标题类型,撰写重要性覆盖了严格的「流经主干」规则。但它的类型推导机制——`SchemaSpec`/`InferArgs` DSL——是子页面细节(你编写的是 `ToolDefinition`;为其提供类型推导的机制你不直接接触)。这就是主干与 seam 分界线的精确表述。
|
||||
- `ToolSchema` 是核心(它是 `GenerateOptions` 的字段,而 `GenerateOptions` 是流经每个步骤的模型请求),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。
|
||||
- 一个数据结构是**核心**的,如果它流经 agent loop 主干——无论加载了哪些插件,循环在每个轮次都会持有、派生、流式输出或记录它(`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄)——**或者**它是插件作者面对某条流水线时编写的唯一标志性类型(`ToolDefinition`)。
|
||||
- `ToolDefinition` 是核心(它是每个工具作者编写的东西),**即使循环从不持有它**——对于这一个标志性类型,撰写重要性压过了严格的"流经主干"规则。但它的类型推导机制——`SchemaSpec`/`InferArgs` DSL——是子页面细节(你编写的是 `ToolDefinition`;为其提供类型推导的机制你并不直接接触)。这就是主干与 seam 分界线的精确表述。
|
||||
- `ToolSchema` 是核心(它是 `GenerateOptions` 的一个字段,而 `GenerateOptions` 是流经每个步骤的模型请求),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。
|
||||
- 工具展示词汇(`ToolCallView`/`ToolResultView` 等)、`SessionPersistence` 持久性 seam 以及 bash 词汇是子页面。
|
||||
|
||||
`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,配以最少的行文,并链接到各 seam 细节的子页面。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界从 session 中拆出)、`tools.md` 和 `bash.md`。
|
||||
`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到子页面获取各 seam 的细节。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从 session 拆出)、`tools.md` 和 `bash.md`。
|
||||
|
||||
### `ts type-equiv` 机制——逐字且防漂移
|
||||
### `ts type-equiv` 机制——既逐字又防漂移
|
||||
|
||||
持久性要求很具体:文档应展示**逐字**的当前类型定义(让读者看到真实形状,而非转述),**并且**机械地保证与源码一致。仓库已经能编译围栏 ` ```ts ` 块(`doc-typecheck`),但一个真正通过类型检查的块需要 import 噪音,且只能证明*可赋值性*而非*逐字节相等*——一个改了名但类型相同的字段仍能通过。因此:
|
||||
持久性需求很具体:文档应当展示**当前类型定义的原文**(让读者看到真实形状,而非转述),**并且**机械地保证与源码一致。仓库已经能编译围栏 ` ```ts ` 块(`doc-typecheck`),但一个真正可编译的块需要 import 噪音,且只证明*可赋值性*而非*字节相等*——一个类型相同但改了名的字段会通过。因此:
|
||||
|
||||
- 类型定义逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。`doc-typecheck` 识别该围栏并跳过它(裸定义不能独立编译),并**将其排除在 opt-out 比例之外**——它是一个独立检查的类别,而非未检查的草稿。
|
||||
- 新增 `scripts/verify-type-equiv.ts`,通过 TypeScript 解析器提取每个块,并对声明的符号断言**逐字源码匹配**——之所以选择这种方式而非编译式 `_Check` 可赋值性断言,正是因为逐字节相等而非可赋值性才是我们需要的属性。
|
||||
- 来源信息保存在中央 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本强制执行 **1:1 对应**:每个 type-equiv 块恰好有一条 manifest 条目,反之亦然;因此不会有块被静默漏检,也不会有条目腐烂。
|
||||
- 接入 `doc-sync`,因此与其他文档门禁在同一个 lefthook pre-push 和 CI 路径中运行。
|
||||
- 类型定义逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。`doc-typecheck` 识别该围栏并跳过它(裸定义不能独立编译),且**将其排除在 opt-out 比例之外**——它是一个独立检查的类别,而非未检查的草稿。
|
||||
- 新增的 `scripts/verify-type-equiv.ts` 通过 TypeScript 解析器提取每个块,并断言其与声明的符号**逐字节匹配源码**——之所以选择这种方式而非编译式 `_Check` 可赋值性断言,正是因为我们需要的属性是字节相等,而非可赋值性。
|
||||
- 来源信息存放在集中的 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本强制执行 **1:1 对应**:每个 type-equiv 块恰好有一条 manifest 条目,反之亦然;因此一个块永远不会被静默漏检,一条条目也永远不会腐烂。
|
||||
- 接入 `doc-sync`,因此与其他文档门禁在同一条 lefthook pre-push 和 CI 路径中运行。
|
||||
|
||||
### 维护是作者的职责,门禁作为兜底
|
||||
|
||||
`verify-type-equiv` 能捕获已记录类型的*粘贴漂移*,但无法告诉你一个全新的核心类型没有被记录。因此 AGENTS.md 和 `dsh-code-review` skill 已更新,要求在变更添加或重塑已记录类型时同步更新目录——门禁处理漂移,人处理新增表面。
|
||||
`verify-type-equiv` 能捕获已记录类型的*粘贴漂移*,但无法告诉你一个全新的核心类型没有被记录。因此 AGENTS.md 和 `dsh-code-review` skill(技能)已更新,要求在变更添加或重塑已记录类型时同步更新目录——门禁处理漂移,人处理新增表面。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **平铺转储所有跨包词汇**:`BashExecRequest` 测试案例否决了它。如果 seam 词汇算「核心」,目录对谁都没帮助;分层的主干与 seam 结构胜出。
|
||||
- **编译式 `_Check` 可赋值性断言**替代逐字源码匹配:否决,因为逐字节相等而非可赋值性才是我们需要的属性——一个改了名但类型相同的字段能通过可赋值性检查。
|
||||
- **来源信息作为行文中的指令注释**:否决,改用中央 manifest;其强制的 1:1 对应确保不会有块被静默漏检,也不会有条目腐烂。
|
||||
- **平铺罗列所有跨包词汇**:`BashExecRequest` 测试案例否决了它。如果 seam 词汇算"核心",目录对谁都没帮助;分层的主干与 seam 结构胜出。
|
||||
- **编译式 `_Check` 可赋值性断言**替代逐字节源码匹配:否决,因为我们需要的属性是字节相等而非可赋值性——一个类型相同但改了名的字段会通过可赋值性检查。
|
||||
- **来源信息作为行文中的指令注释**:否决,改用集中 manifest;其强制的 1:1 对应确保一个块永远不会被静默漏检,一条条目也永远不会腐烂。
|
||||
|
||||
## 验证教训
|
||||
|
||||
主干与 seam 规则在采纳前经过了 `BashExecRequest`、工具 schema 与定义、schema DSL、展示类型以及 session/persistence 拆分的逐一测试。
|
||||
|
||||
`verify-type-equiv` 必须扫描完整的 Markdown 范围,而不仅是 manifest 中列出的文档。否则未登记的 `type-equiv` 块会逃脱所声称的一对一检查。因此门禁将此类块报告为遗留块。本 RFC 将这条快速失败的扫描规则与主干/seam 分界和逐字匹配决策一并记录;生成式 Cordis 目录在[其 RFC](2026-06-20-generated-cordis-catalog.md) 中有对称的设计记录。
|
||||
`verify-type-equiv` 必须扫描完整的 Markdown 范围,而不仅仅是 manifest 中列出的文档。否则一个未登记的 `type-equiv` 块会逃脱所声称的一对一检查。因此门禁将此类块报告为遗留块。本 RFC 将这条快速失败的扫描规则与主干-seam 分界线和逐字节匹配决策一并记录;生成式 Cordis 目录在[其 RFC](2026-06-20-generated-cordis-catalog.md) 中有对称的设计记录。
|
||||
|
||||
## 后果
|
||||
|
||||
- 词汇现在有了一个**不会静默漂移**的唯一归属地:源码中的字段重命名会在 pre-push 钩子和 CI 中使 `verify-type-equiv` 失败,直到粘贴内容被刷新。
|
||||
- 主干与 seam 分界线是一个可复用的范围界定工具,而非一次性决策:同一条「你编写/持有/接收的东西是核心;为其提供类型推导/渲染/持久化的机制是细节」规则,后来也被用于界定事件/服务目录的 harness 层与继承层分层。
|
||||
- `ts type-equiv` 围栏是继 ` ```ts `(编译)和 ` ```ts ignore-check `(草稿)之后的第三种文档块类别。后续又新增了第四种 ` ```ts cordis-catalog `(生成签名),复用了相同的跳过并排除处理。
|
||||
- 词汇现在有了一个**不会静默漂移**的唯一归属:源码中的字段重命名会在 pre-push 钩子和 CI 中导致 `verify-type-equiv` 失败,直到粘贴内容被刷新。
|
||||
- 主干与 seam 分界线是一个可复用的范围界定工具,而非一次性的:同一条「你编写/持有/接收的东西是核心;为其提供类型推导/渲染/持久化的机制是细节」规则,后来也被用于界定事件/服务目录的 harness 层与继承层分层。
|
||||
- ` ```ts type-equiv ` 围栏是继 ` ```ts `(编译)和 ` ```ts ignore-check `(草稿)之后的第三种文档块类别。后续的姊妹门禁又增加了第四种 ` ```ts cordis-catalog `(生成签名),复用了相同的跳过并排除处理。
|
||||
- 添加或重塑核心类型现在附带一项文档义务,作者必须履行(门禁无法检测缺失的*新*类型),由 `dsh-code-review` 检查清单兜底。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-generated-cordis-catalog.md: 6b451e31965f8f00210aa927ed236fed28699351
|
||||
2026-06-20-generated-cordis-catalog.zh.md: 9c7150ff5f12d4d9e4a13158acdf8f52e84fd95a
|
||||
2026-06-20-generated-cordis-catalog.zh.md: 5550d07b5f5635d1da6496e35049c5725329e114
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:生成式 Cordis 事件 + 服务目录
|
||||
# RFC:生成式 Cordis 事件与服务目录
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,36 +6,36 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
插件作者需要两个参考面,而此前没有任何单一文档能提供:他们可以监听的每一个 Cordis **事件**(含精确签名与分发模式),以及他们可以调用的每一个 `ctx.<key>` **服务**(含精确接口)。相关信息已经存在,但散落各处:`docs/architecture.md` 中一张手工维护的事件分类*表格*(名称 + 行文描述的 Mode/Purpose,由 `verify-event-taxonomy` 做名称集合校验)、一张服务映射表(8 行角色描述),以及 `interface Events` / `interface Context` 声明本身。分类表还有一个盲区:它无法捕获全新的*未记录*事件——名称集合校验器只检查两侧已有的名称。
|
||||
插件作者需要两个参考面,而此前没有任何单一文档能提供:他们可以监听的每一个 Cordis **事件**(含精确签名与分发模式),以及他们可以调用的每一个 `ctx.<key>` **服务**(含精确接口)。相关信息虽然存在,但散落各处:`docs/architecture.md` 中一张手工维护的事件分类*表格*(名称 + 行文描述的 Mode/Purpose,由 `verify-event-taxonomy` 做名称集合校验)、一张服务映射表(8 行角色描述),以及 `interface Events` / `interface Context` 声明本身。分类表格还有一个盲区:它无法捕获全新的*未记录*事件——名称集合校验器只检查两侧已有的名称。
|
||||
|
||||
这是[核心数据结构目录](../../../core-data-structures/core.md)([对应 RFC](2026-06-20-core-data-structures-catalog.md))在连线轴上的互补件:那份目录记录 agent loop 流转的*数据结构*(经校验的手工粘贴);本目录记录流转它们的*事件与服务*。
|
||||
这是 [core-data-structures 目录](../../../core-data-structures/core.md)([其 RFC](2026-06-20-core-data-structures-catalog.md))在连线轴上的补充:后者编目的是 agent loop(智能体循环)流转的*数据结构*(经校验的手工粘贴);本 RFC 编目的是移动这些数据结构的*事件与服务*。
|
||||
|
||||
## 决策
|
||||
|
||||
从源码生成目录,而非手工维护表格再校验子集。
|
||||
从源码生成目录,取代手工维护表格并校验子集的方式。
|
||||
|
||||
`scripts/gen-cordis-catalog.ts` 使用 TypeScript 编译器 API,从声明和源码 JSDoc 分别输出事件参考与服务参考。事件包含分发模式;服务包含公开签名。确定性的 `--write` 与 `--check` 模式使两个页面成为生成产物,新鲜度由 doc-sync 强制。
|
||||
`scripts/gen-cordis-catalog.ts` 使用 TypeScript 编译器 API,从声明和源码 JSDoc 分别输出事件参考与服务参考。事件包含分发模式;服务包含公开签名。确定性的 `--write` 和 `--check` 模式使两个页面成为生成产物,新鲜度由 `doc-sync`(文档同步门禁)强制保障。
|
||||
|
||||
纯生成在这里是正确的,因为代码库足够规范,AST 即全部真相:每个事件/服务名称都是字符串字面量,能往返映射到一个静态声明——没有动态命名的事件,也没有仅运行时存在的服务。因此生成的文档不可能出错,并且从结构上消除了未记录事件的缺口(生成器枚举源码,而非检查手写子集)。
|
||||
纯生成在此处是正确的,因为代码库足够规范,AST 就是全部事实:每个事件/服务名称都是字符串字面量,可以往返映射到静态声明——不存在动态命名的事件,也不存在仅运行时的服务。因此生成的文档不可能出错,且从结构上消除了未记录事件的缺口(生成器枚举源码,而非校验手写子集)。
|
||||
|
||||
具体选择:
|
||||
|
||||
- **`@mode` 标签,交叉校验。** 每个 harness 事件的 JSDoc 携带显式的 `@mode emit|waterfall|parallel|serial` 标签;缺少标签时生成器直接报错。当签名形状具有结论性时——尾部参数为 `next: () => …` 在结构上即为 waterfall——生成器断言标签与之一致,矛盾时直接报错。emit/parallel/serial 的区分在结构上不可见(`session/flush` 返回 `Promise<void> | void` 且无 `next`,有序的 `agent/pre-step` 检查点亦然),因此信任标签。撰写规则见 [AGENTS.md](../../../../AGENTS.md)。
|
||||
- **分层范围。** harness 层(8 个 `@deepseek-ai/dsh-*` 服务及其事件)从源码完整渲染。继承层(cordis-core 的 `ctx.on/emit/effect/provide/…` + `internal/*` 事件 + loader/HMR/timer)是插件同样可见的固定 vendor 源;它以精简形式渲染(名称 + 一行说明 + 源码指针),数据来自生成器中的一张手工策展表,而**不是**遍历 vendor AST——cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段(`root`、`baseUrl`、`logger`),且 vendor 表面仅在有意的 vendor 同步时才变化。
|
||||
- **交叉链接到数据结构目录。** 签名中出现的类型名(`GenerateOptions`、`StreamChunk`、`ToolDefinition` 等)链接到记录该类型的核心数据结构页面。映射是生成器中一个小型手工策展的 const,而**不是** `type-equiv.manifest.json`——后者记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。
|
||||
- **专用围栏。** 签名块使用 ` ```ts cordis-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它(裸签名片段不能独立编译),不计入 opt-out 比例——与 `type-equiv` 块的处理方式相同。
|
||||
- **`@mode` 标签,交叉校验。** 每个 harness 事件的 JSDoc 携带一个显式的 `@mode emit|waterfall|parallel|serial` 标签;缺少标签时生成器直接报错。当签名形状具有决定性时——尾部参数为 `next: () => …` 在结构上即为 waterfall(瀑布式事件)——生成器断言标签与之一致,矛盾时直接报错。emit/parallel/serial 的区别在结构上不可见(`session/flush` 返回 `Promise<void> | void` 且无 `next`,有序的 `agent/pre-step` 检查点亦然),因此信任标签。编写规则见 [AGENTS.md](../../../../AGENTS.md)。
|
||||
- **分层范围。** harness 层(8 个 `@deepseek-ai/dsh-*` 服务及其事件)从源码完整渲染。继承层(cordis-core 的 `ctx.on/emit/effect/provide/…` + `internal/*` 事件 + loader/hmr/timer)是插件同样可见的固定 vendor 源码;它从生成器中一张人工维护的表格简洁渲染(名称 + 一行描述 + 源码指针),而**非**遍历 vendor AST。原因是 cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段(`root`、`baseUrl`、`logger`),且 vendor 接口面仅在有意的 vendor 同步时才变化。
|
||||
- **交叉链接到数据结构目录。** 签名中的类型名(`GenerateOptions`、`StreamChunk`、`ToolDefinition` 等)链接到记录该类型的 core-data-structures 页面。映射是生成器中一个小型的人工维护常量,而**非** `type-equiv.manifest.json`——后者记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。
|
||||
- **专用围栏。** 签名块使用 ` ```ts cordis-catalog ` 信息字符串,`doc-typecheck` 识别后跳过(裸签名片段不能独立编译),并排除在 opt-out 比例之外——与 `type-equiv` 块获得相同待遇。
|
||||
|
||||
本决策**取代** [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)中的事件分类部分:`verify-event-taxonomy` 及其 `docs/architecture.md` 表格退役(architecture.md 的标题保留,正文改为指向目录;服务映射角色表作为策展行文保留)。doc-typecheck、verify-md-wrap、verify-md-links 与 verify-type-equiv 不受影响。
|
||||
本决策**取代** [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)中事件分类的那一半:`verify-event-taxonomy` 及其 `docs/architecture.md` 表格退役(architecture.md 的标题保留,正文改为指向目录;服务映射的角色表格作为人工行文保留)。doc-typecheck、verify-md-wrap、verify-md-links 和 verify-type-equiv 不受影响。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **校验而非生成(退役的分类检查所做的事)**:*仅对此表面*反转了方向。这里的数据可以机械地完整获取,因此生成严格强于名称集合校验(完整签名、不会漂移、能捕获未记录事件)。
|
||||
- **遍历 vendor AST 以获取继承层**:否决,改用策展表。cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段,且固定的 vendor 表面仅在有意同步时才变化。
|
||||
- **复用 `type-equiv.manifest.json` 作为签名交叉链接映射**:否决,改用小型手工策展 const。manifest 记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。
|
||||
- **校验而非生成(退役的分类检查所做的事)**:*仅对本参考面*反转了这一策略。此处的数据可以机械地完整获取,因此生成严格强于对手工表格做名称集合校验(完整签名、不会漂移、能捕获未记录事件)。
|
||||
- **遍历 vendor AST 以获取继承层**:否决,改用人工维护表格。cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段,且固定的 vendor 接口面仅在有意同步时才变化。
|
||||
- **复用 `type-equiv.manifest.json` 作为签名交叉链接映射**:否决,改用小型人工维护常量。manifest 记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目录不会漂移:源码变化而已提交文件未反映时,`verify-cordis-catalog` 在 pre-push 钩子和 CI 中失败。新事件缺少 `@mode` 标签、或标签与签名矛盾时,生成器直接报错。
|
||||
- 事件的行文描述现在只有一个归属地——声明处的 JSDoc。JSDoc 写得薄,目录条目就薄,这迫使作者在源头做好文档(生成器是 AGENTS.md「每个导出都有语义 JSDoc」规则的强制函数)。
|
||||
- 继承层是手工摘要的,因此 vendor 同步若增加或重命名了 cordis-core 事件或 `ctx` 成员,需要同步编辑 `gen-cordis-catalog.ts` 中的策展表。这是不遍历固定 vendor 源的有意代价;变化很少,且在生成器中有明确标注。
|
||||
- `verify-event-taxonomy.ts` 被删除,`docs/architecture.md` 的事件表格消失;之前链接到特定表格行的人现在会落到生成目录上。
|
||||
- 目录不会漂移:源码变更而已提交文件未反映时,`verify-cordis-catalog` 在 pre-push 钩子和 CI 中失败。新事件缺少 `@mode` 标签,或标签与签名矛盾,生成器直接报错。
|
||||
- 事件的行文描述现在有了唯一归属地——声明处的 JSDoc。JSDoc 写得单薄,目录条目就单薄,这迫使作者在源码处做好文档(生成器是 AGENTS.md「每个导出都有语义 JSDoc」规则的强制函数)。
|
||||
- 继承层是手工摘要,因此 vendor 同步若新增或重命名了 cordis-core 事件或 `ctx` 成员,需要同步编辑 `gen-cordis-catalog.ts` 中的人工维护表格。这是不遍历固定 vendor 源码的有意代价;它很少变化,且在生成器中有明确标注。
|
||||
- `verify-event-taxonomy.ts` 被删除,`docs/architecture.md` 的事件表格也已移除;之前链接到特定表格行的人现在会落在生成目录上。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-rfc-classification.md: 201852a209be7f40b05de45d148a36b9185767a3
|
||||
2026-06-20-rfc-classification.zh.md: 38eb920ac216e69087f0c84c95cdd9effca7b9b3
|
||||
2026-06-20-rfc-classification.zh.md: 554ac8014719c99ed447a33ff843c40fde761eed
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
# RFC:通过路径编码的子目录对 RFC 进行分类
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-06-20-rfc-classification.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`docs/rfc/` 此前仅按**生命周期**分组:`proposed/`/`implemented/`/`rejected/`。没有任何机制记录每篇 RFC 属于哪一*类*决策。索引是每个生命周期下的一个扁平列表,无法按需筛选「所有精简类」或「所有测试策略类」决策。同一天落地的一批精简类 RFC 让这个缺口变得具体:浏览 `proposed/` 的读者无法在不逐一打开文件的情况下区分新能力、移除和工具策略变更。
|
||||
`docs/rfc/` 过去仅按**生命周期**分组 RFC:`proposed/`/`implemented/`/`rejected/`。没有任何机制记录每个 RFC 属于哪一*类*决策。索引在每个生命周期下只是一个扁平列表,无法按需筛选「所有简化类」或「所有测试策略类」决策。一批简化类 RFC 在同一天落地后,这个缺口变得具体:浏览 `proposed/` 的读者无法在不逐一打开文件的情况下区分新能力、移除和工具策略变更。
|
||||
|
||||
本仓库的一贯倾向是[机械质量门禁优先于行文指南](2026-06-11-quality-gates.md):不被机器检查的约定终将腐烂。因此这里的分类体系必须可强制执行,而非靠自觉的文件头。
|
||||
本仓库一贯的倾向是[机械质量门禁优于行文规范](2026-06-11-quality-gates.md):不被机器检查的约定终将腐烂。因此这里的分类方案必须可强制执行,而非靠自觉的文件头。
|
||||
|
||||
## 决策
|
||||
|
||||
增加第二个维度——RFC 的**类别**——并将其编码在路径中:`{lifecycle}/{class}/yyyy-mm-dd-topic.md`。文件夹*就是*标签。文件的位置声明其类别,封闭集合是「这些文件夹且仅限这些」,而既有的 [verify-md-links](2026-06-18-markdown-cross-link-lint.md) 门禁已经保护了移动文件所需的路径重写。
|
||||
增加第二个维度——RFC 的**类别**——并将其编码在路径中:`{lifecycle}/{class}/yyyy-mm-dd-topic.md`。文件夹本身就是标签。文件的位置声明其类别,封闭集合是「这些文件夹且仅限这些」,而既有的 [verify-md-links](2026-06-18-markdown-cross-link-lint.md) 门禁已经保护了移动文件所需的路径重写。
|
||||
|
||||
### 六个类别的封闭集合
|
||||
|
||||
| 类别 | 覆盖范围 |
|
||||
| 类别 | 涵盖范围 |
|
||||
|---|---|
|
||||
| `feature` | 面向用户或模型的新能力。 |
|
||||
| `bug-fix` | 修正缺陷或弥补事后复盘暴露的缺口。 |
|
||||
| `simplification` | 移除代码、行为或接口面,不增加新能力。 |
|
||||
| `architecture` | 关于**交付源码**的结构性决策:包之间的关系、运行时词汇是什么。 |
|
||||
| `process` | 围绕代码的工具、策略或工作流,不涉及运行时行为。 |
|
||||
| `bug-fix` | 修正缺陷或填补事后复盘暴露的空白。 |
|
||||
| `simplification` | 移除代码、行为或对外表面积,不引入新能力。 |
|
||||
| `architecture` | 关于**交付源码**的结构性决策——包(package)之间的关系、运行时词汇。 |
|
||||
| `process` | 围绕代码的工具、策略或工作流,而非运行时行为。 |
|
||||
| `testing` | 测试基础设施与策略。 |
|
||||
|
||||
`architecture` 与 `process` 的分界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。本 RFC 本身是一项 `process` 决策——它改变的是仓库的组织方式和门禁,而非 harness 在运行时的行为——因此它位于 `implemented/process/` 下。
|
||||
`architecture` 与 `process` 的分界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。本 RFC 本身是一个 `process` 决策——它改变的是仓库的组织方式和门禁,而非 harness 的运行时行为——因此它位于 `implemented/process/` 下。
|
||||
|
||||
### 两道门禁
|
||||
|
||||
两者都是 `doc-sync` 的成员,风格与 `verify-md-wrap` 一致(tsx ESM,只校验不生成,首个违规即以非零退出码退出):
|
||||
两者都是 `doc-sync`(文档同步门禁)的成员,风格与 `verify-md-wrap` 一致(tsx ESM,只校验不生成,首个违规即以非零退出码退出):
|
||||
|
||||
- **`scripts/verify-rfc-classification.ts`**:封闭集合与索引新鲜度(freshness)。它断言每个生命周期文件夹下的文件都位于规范集合中的某个类别文件夹内(直接放在生命周期根目录的 `.md`,或未知的类别文件夹,都会失败),并断言生成的 [INDEX.md](../../INDEX.md) 与从目录树重新渲染的结果逐字节一致(见[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md))。规范类别集合以 `const` 形式定义在 `scripts/rfc-index.ts` 中——这是与生成器共享的机器真源——[README](../../README.md) 以行文形式记录它;类别*描述*保持手写,索引则是生成的。
|
||||
- **`scripts/verify-doc-refs.ts`**:源码注释中的文档引用。RFC 路径不仅在 Markdown 中被引用,也出现在 TypeScript 文档注释中(根相对路径,如 `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`)。`verify-md-links` 从未扫描过这些引用,因此重组可能会悄悄使它们变成悬空引用。此门禁扫描 `packages/**` 和 `examples/**` 下仓库自有的 `.ts` 文件(排除构建产物 `lib/` 和 `vendor/`),查找 `docs/….md` 形式的 token,将每个根相对路径解析并断言其存在。它要求 `.md` 扩展名,因此无扩展名的行文引用(`docs/postmortem/0001`、`docs/architecture.md § Extending The Harness`)不受影响。
|
||||
- **`scripts/verify-rfc-classification.ts`**——封闭集合与索引新鲜度。它断言生命周期文件夹下的每个文件都位于规范集合中的某个类别文件夹内(生命周期根目录下的散落 `.md` 或未知类别文件夹均判定失败),并断言生成的 [INDEX.md](../../INDEX.md) 与从目录树重新渲染的结果逐字节一致(见[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md))。规范类别集合以 `const` 形式定义在 `scripts/rfc-index.ts` 中——这是与生成器共享的机器真源——而 [README](../../README.md) 以行文形式记录它;类别*描述*保持手写,索引由机器生成。
|
||||
- **`scripts/verify-doc-refs.ts`**——源码注释中的文档引用。RFC 路径不仅被 Markdown 引用,也被 TypeScript 文档注释引用(以仓库根为起点的路径,如 `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`)。`verify-md-links` 从未扫描过这些引用,因此重组可能使它们静默失效。此门禁扫描 `packages/**` 和 `examples/**` 下仓库自有的 `.ts` 文件(排除构建产物 `lib/` 和 `vendor/`),查找 `docs/….md` 形式的 token,将每个以仓库根为起点的路径解析并断言其存在。它要求 `.md` 扩展名,因此无扩展名的行文引用(`docs/postmortem/0001`、`docs/architecture.md § Extending The Harness`)不受影响。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在每个文件中加一行 `Classification:` 行文**(紧挨 `Status:`),由门禁解析。可行,但它把路径已经能承载的事实重复到了文件内,而且这一行可能与所在文件夹不一致。路径编码让标签与其存储合二为一——没有需要保持同步的东西。
|
||||
- **设立 `refactor` 类别。**它与 `simplification` 几乎完全重叠;唯一有人试图用来区分的标准是「可观测行为是否改变?」,而 `simplification` 已经编码了这一点(它不改变)。一个类别,不要两个。
|
||||
- **从文件系统自动生成索引。**此处最初否决,以保持索引手写;后来被[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md)取代——当堆叠的提案波使手写表格成为仓库中冲突最频繁的文档区域后,列表改为完全生成的 [INDEX.md](../../INDEX.md),而 README 行文保持人工策展。
|
||||
- **在每个文件中添加 `Classification:` 行文行**(紧邻 `Status:`),由门禁解析。可行,但它将路径已能承载的事实重复到文件中,且行内容可能与所在文件夹不一致。路径编码使标签与其存储合二为一,没有需要保持同步的东西。
|
||||
- **设立 `refactor` 类别。** 与 `simplification` 几乎完全重叠;唯一有人试图用来区分的标准是「可观察行为是否改变?」,而 `simplification` 已经编码了这一点(它不改变)。一个类别即可,无需两个。
|
||||
- **从文件系统自动生成索引。** 此处最初否决,以保持索引手写;后被[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md)取代——当堆叠的提案潮使手写表格成为仓库中冲突最频繁的文档区域后,列表改为完全生成的 [INDEX.md](../../INDEX.md),而 README 行文保持人工维护。
|
||||
|
||||
## 后果
|
||||
|
||||
- 每篇 RFC 现在都位于一个类别文件夹下,索引在每个生命周期内按类别分组。读者扫一个标题就能看到所有精简类或所有测试类决策。
|
||||
- `doc-sync` 链中多了两个快速 tsx 脚本;无新增依赖(mdast/GFM 栈已因 `verify-md-wrap`/`verify-md-links` 而存在)。
|
||||
- 新增类别是一个刻意的动作:修改 `scripts/rfc-index.ts` 中的 `const` 以及 [Classification 章节](../../README.md#classification),而不是仅仅 `mkdir` 一个文件夹。门禁会拒绝未知文件夹,因此临时类别无法悄悄混入。
|
||||
- 源码注释中的文档引用现在也受门禁保护:一个被移动或重命名的文档如果被 `.ts` 注释引用,pre-push 钩子就会失败,从而封堵了 `verify-md-links` 在结构上无法看到的一类漂移。
|
||||
- 每个 RFC 现在都位于一个类别文件夹下,索引在每个生命周期内按类别分组。读者只需扫一个标题即可看到所有简化类或所有测试类决策。
|
||||
- `doc-sync` 链中多了两个快速 tsx 脚本;无新依赖(mdast/GFM 栈已因 `verify-md-wrap`/`verify-md-links` 而存在)。
|
||||
- 新增类别是一个刻意的动作:修改 `scripts/rfc-index.ts` 中的 `const` 和 [Classification 章节](../../README.md#classification),而非仅仅 `mkdir` 一个文件夹。门禁拒绝未知文件夹,因此临时类别无法悄悄混入。
|
||||
- 源码注释中的文档引用现在也受门禁保护——一个被移动或重命名的文档如果被 `.ts` 注释引用,pre-push 钩子就会失败,堵住了 `verify-md-links` 在结构上无法看到的一类漂移。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-02-tool-schema-catalog.md: 9a99fafd36b3546be4f51a7cd9e9a47fdaaf4c2d
|
||||
2026-07-02-tool-schema-catalog.zh.md: 373c681fa5870696645b138f12cad7f296434184
|
||||
2026-07-02-tool-schema-catalog.zh.md: 5860d1617ce6592c8665e4cb59304b0f6d35c99a
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
# RFC:生成式工具 schema 目录(启动并采集)
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-02-tool-schema-catalog.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
仓库此前没有一份统一的参考,列出实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 目录和数据结构目录覆盖的是接线与词汇,而非工具本身。
|
||||
仓库此前没有一份统一的参考文档来记录实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 目录和数据结构目录覆盖的是接线与词汇,而非工具。
|
||||
|
||||
## 决策
|
||||
|
||||
通过**启动每个工具插件并读取其注册的 schema** 来生成目录,而非解析源码。`scripts/gen-tool-catalog.ts` 将每个已发布的工具包(package)挂载到一个全新的 Cordis `Context` 上(带 `SystemPrompt` + `ToolRegistry` 以及插件 `apply` 所读取的注入 seam),调用 `ctx.tools.schemas()`(即发送给模型的 `ToolSchema[]`),dispose 上下文,然后为每个包渲染一个 `## <package>` 小节,每个工具对应一个 ` ```json ` 的 `parameters` 块。它沿用 `gen-cordis-catalog` / `gen-module-graph` 的 CLI 形态:默认 `--write` 重新生成,`--check` 在已提交副本陈旧时失败,输出是确定性的(按 manifest 排序,工具按名称排序)。`verify-tool-catalog`(即 `--check`)运行在 doc-sync 内部,因此新鲜度门禁与其他文档门禁一样在 lefthook pre-push 和 CI 路径中触发。
|
||||
通过**启动每个工具插件并读取其注册的 schema** 来生成目录,而非解析源码。`scripts/gen-tool-catalog.ts` 将每个已发布的工具包(package)挂载到一个新的 Cordis `Context`(带 `SystemPrompt` + `ToolRegistry` 以及插件 `apply` 所读取的注入 seam),调用 `ctx.tools.schemas()`(即发送给模型的 `ToolSchema[]`),dispose(资源释放)该 context,然后为每个包渲染一个 `## <package>` 小节,每个工具一个 ` ```json ` 的 `parameters` 块。它与 `gen-cordis-catalog` / `gen-module-graph` 的 CLI(命令行界面)形态一致:默认 `--write` 重新生成,`--check` 在已提交副本陈旧时失败,输出是确定性的(按 manifest(元数据清单)排序,工具按名称排序)。`verify-tool-catalog`(即 `--check`)在 doc-sync(文档同步门禁)内运行,因此新鲜度门禁在 lefthook pre-push 和 CI 路径中与其他文档门禁一同触发。
|
||||
|
||||
### 为什么启动而非解析(核心论点)
|
||||
### 为何启动而非解析(核心要点)
|
||||
|
||||
Cordis 目录是纯 TypeScript AST 遍历,因为每个事件/服务名都是字符串字面量,能往返映射到静态声明——AST 就是全部事实。**工具 schema 不是静态可知的**,因此同样的技术会产出一份说谎的文档:
|
||||
Cordis 目录是纯 TypeScript AST 遍历,因为每个事件/服务名都是字符串字面量,可以往返映射到静态声明——AST 即全部事实。**工具 schema 在静态层面不可知**,因此同样的技术会产出一份说谎的文档:
|
||||
|
||||
- `tool-todo` 写了 `enum: [...STATUSES]`——对一个运行时 `const` 的展开。AST 看到的是展开表达式,而非 `["pending","in_progress","completed"]`。
|
||||
- 每段 description 都由字符串**拼接**构建(`'…' + '…'`)。AST 看到的是拼接节点,而非模型实际读到的最终文本。
|
||||
- `tool-subagent` 的工具名是 `config.toolName ?? 'subagent'`——加载时选定,不是字面量。
|
||||
- MCP 插件可以通过 `ctx.tools.register()` 直接注册**原始 JSON Schema**,完全不经过 `defineTool`,因此结构化枚举 `defineTool(` 调用点会漏计。
|
||||
- 每条 description 都通过字符串**拼接**构建(`'…' + '…'`)。AST 看到的是拼接节点,而非模型实际读到的最终文本。
|
||||
- `tool-subagent` 的工具名是 `config.toolName ?? 'subagent'`——加载时选定,并非字面量。
|
||||
- MCP 插件可以通过 `ctx.tools.register()` 直接注册**原始 JSON Schema**,完全不经过 `defineTool`,因此结构化枚举 `defineTool(` 调用点会遗漏。
|
||||
|
||||
唯一忠实的真源是插件加载后注册表实际持有的 schema。启动即是[测试策略](../../../testing.md)中「验证世界,而非自我报告」这一原则在文档生成器上的应用:读取已发布的产物,而非对它的重新推导。
|
||||
唯一忠实的真源是插件加载后注册表实际持有的 schema。启动是[测试策略](../../../testing.md)中「验证世界,而非自我报告」这一原则在文档生成器上的应用:读取已发布的产物,而非对它的再推导。
|
||||
|
||||
### 恢复「不会静默遗漏」
|
||||
### 恢复「不会静默遗漏」的保证
|
||||
|
||||
启动有一项 AST 遍历不具备的代价:没有源码声明集合可供枚举,因此新增的工具包可能被遗忘。一道**完整性守卫**恢复了这一保证——`assertManifestComplete` 对 `packages/` 下所有 `tool-*` 包做 glob,若有任何一个不在生成器的启动 manifest 中则硬报错。新增工具包会导致生成器失败,进而导致 doc-sync 失败,直到该包被注册。这与 Cordis 生成器通过枚举源码免费获得的结构性保证相同,只是为启动式生成器重新实现了一遍。
|
||||
启动有一项 AST 遍历不存在的代价:没有源码声明集合可供枚举,新工具包可能被遗忘。一个**完整性守卫**恢复了这项保证——`assertManifestComplete` 对 `packages/` 下所有 `tool-*` 包进行 glob,若有任何一个不在生成器的启动 manifest 中则直接报错。新工具包在注册之前会导致生成器失败,进而导致 doc-sync 失败。这与 Cordis 生成器通过枚举源码免费获得的结构性属性相同,只是为基于启动的生成器重新实现了一遍。
|
||||
|
||||
### 手工维护的启动 manifest 是不可约减的策略
|
||||
### 手动维护的启动 manifest 是不可化约的策略
|
||||
|
||||
文件系统负责发现工具包清单,完整性守卫负责拒绝遗漏。`TOOL_PACKAGES` 仍然为每个包持有一份显式的启动配方,因为所需的 seam 实现和配置是**策略**,不是能从目录布局或注入名称安全推断的事实。
|
||||
文件系统负责发现工具包清单,完整性守卫负责拒绝遗漏。`TOOL_PACKAGES` 仍然为每个包持有一份显式的启动配方,因为所需的 seam 实现和配置属于策略,不是能从目录布局或注入名称安全推断的事实。
|
||||
|
||||
### 范围
|
||||
|
||||
`packages/*/tool-*` 下已发布的产品级工具包,各以默认配置启动:`dsh-tool-bash`(`bash`、`bash_output`、`bash_kill`)、`dsh-tool-todo`(`todo_write`)、`dsh-tool-subagent`(`subagent`)。`examples/` 下的演示工具(`echo`)被排除,与 Cordis 目录的 packages-only 范围一致——演示工具不属于读者所要查阅的产品接口。
|
||||
`packages/*/tool-*` 下已发布的产品工具包,每个以默认配置启动:`dsh-tool-bash`(`bash`、`bash_output`、`bash_kill`)、`dsh-tool-todo`(`todo_write`)、`dsh-tool-subagent`(`subagent`)。`examples/` 下的演示工具(`echo`)被排除,与 Cordis 目录仅覆盖 packages 的范围一致——演示工具不属于读者所查阅的产品接口。
|
||||
|
||||
目录的单位是包,而非每个已配置的工具实例。每个包以默认配置启动一次;加载时的别名(如 `subagent_fork`)会注明,但不枚举每种部署排列。部署清单是一个独立的、无界的接口。
|
||||
目录的单位是包,而非每个配置化的工具实例。每个包以默认配置启动一次;加载时的别名(如 `subagent_fork`)会注明,但不枚举所有部署排列。部署清单是一个独立的、无界的接口。
|
||||
|
||||
### 使用普通 `json` 围栏
|
||||
|
||||
schema 块使用 ` ```json `,而非自定义的 `ts` 系围栏。`doc-typecheck` 只提取 `ts*` 围栏,因此 JSON 块对它不可见——无需 `BlockKind` 接线(不同于 Cordis 目录的 `ts cordis-catalog` 围栏,后者必须加入白名单以避免裸签名片段被编译)。
|
||||
schema 块使用 ` ```json `,而非自定义的 `ts` 系围栏。`doc-typecheck` 只提取 `ts*` 围栏,因此 JSON 块对它不可见——无需 `BlockKind` 接线(不同于 Cordis 目录的 `ts cordis-catalog` 围栏,后者需要加入白名单以避免裸签名片段被编译)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **纯 TypeScript AST 遍历,如 Cordis 目录**:工具 schema 不是静态可知的(见上文核心论点):运行时展开、字符串拼接、配置选定的名称,以及原始 `ctx.tools.register()` 注册,都会让 AST 推导出的文档说谎。
|
||||
- **从各包的 inject 推断启动配方**:[发现包清单提案](../../proposed/process/2026-06-20-discover-package-inventory.md)所警告的「过于聪明」的路径;配方保持手写策略,清单由文件系统发现并受完整性守卫保护。
|
||||
- **纯 TypeScript AST 遍历,如 Cordis 目录**:工具 schema 在静态层面不可知(见上文核心要点):运行时展开、字符串拼接、配置选定的名称,以及原始 `ctx.tools.register()` 注册,都会让 AST 推导出的文档说谎。
|
||||
- **从各包的 inject 推断启动配方**:属于[发现包清单提案](../../proposed/process/2026-06-20-discover-package-inventory.md)所警告的「过度聪明」路径;配方保持为手写策略,清单由文件系统发现并由完整性守卫把关。
|
||||
- **为 schema 块使用自定义 `ts` 系围栏**:不必要。普通 ` ```json ` 围栏对 `doc-typecheck` 不可见,无需 `BlockKind` 白名单。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目录不会漂移:工具 schema 变更而已提交文件未反映时,`verify-tool-catalog` 在 pre-push 钩子和 CI 中失败。新增 `tool-*` 包未加入 manifest 时,完整性守卫直接报错。
|
||||
- 工具描述文本只有一个归属地——源码中 `defineTool` 的 `description`——生成的条目质量完全取决于它,与 Cordis 目录对事件 JSDoc 施加的推动力相同。
|
||||
- 生成器导入并执行工作区包(这是仓库中第一个这样做的脚本;其他脚本只读取文本)。它通过根 `tsconfig` 的 `paths` 映射在 `tsx` 下运行,走的是演示和测试所用的同一条未构建源码路径,因此不需要构建步骤。
|
||||
- 未来某个工具背后新增能力 seam 时,意味着 manifest 中新增一条配方条目(需要挂载哪些 seam)。这是上文明确指出的手写代价;仅在新增工具包时才需变更。
|
||||
- 目录不会漂移:工具 schema 变更而已提交文件未反映,`verify-tool-catalog` 会在 pre-push 钩子和 CI 中失败。新 `tool-*` 包未加入 manifest 则完整性守卫直接报错。
|
||||
- 工具描述文本有唯一归属——源码中 `defineTool` 的 `description`——生成的条目质量取决于它,与 Cordis 目录对事件 JSDoc 施加的强制力相同。
|
||||
- 生成器导入并执行工作区包(这是仓库中第一个这样做的脚本;其他脚本只读文本)。它通过根 `tsconfig` 的 `paths` 映射在 `tsx` 下运行,使用与演示和测试相同的未构建源码路径,因此不需要构建步骤。
|
||||
- 未来某个工具背后新增一个能力 seam,意味着 manifest 中需要新增一条配方条目(声明要挂载哪些 seam)。这正是上文指出的有意为之的手写成本;仅在新增工具包时才需变更。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-03-documentation-graph-atlas.md: d10b57e5114684ab0a2caed66fa84b84959bdf12
|
||||
2026-07-03-documentation-graph-atlas.zh.md: 8473bc27994bb33eac61659196acc53b69a16449
|
||||
2026-07-03-documentation-graph-atlas.zh.md: 6edcfbdbbc3af5808e60888acad04919ce681396
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
# RFC:面向维护者和 SDK 用户的文档关系图索引
|
||||
|
||||
Status: implemented
|
||||
# RFC:面向维护者与 SDK 用户的文档关系图索引
|
||||
|
||||
[English](2026-07-03-documentation-graph-atlas.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
仓库此前已有若干高可信度的文档面,各自覆盖不同维度:[module-graph.md](../../../module-graph.md) 由 package 的 `peerDependencies` 生成;生成的 [Cordis 事件目录](../../../cordis-catalog/events.md)和[服务目录](../../../cordis-catalog/services.md)由 Cordis 的 `Events` 与 `Context` 声明生成;[tool-catalog.md](../../../tool-catalog.md) 通过启动已发布的工具插件生成;[core-data-structures/](../../../core-data-structures/core.md) 使用 `ts type-equiv` 块保持粘贴的类型定义与源码同步。
|
||||
仓库已有若干高可信度的文档面,各自覆盖不同维度:[module-graph.md](../../../module-graph.md) 由包(package)的 `peerDependencies` 生成;生成的 [Cordis events](../../../cordis-catalog/events.md) 与 [services](../../../cordis-catalog/services.md) 目录由 Cordis 的 `Events` 和 `Context` 声明生成;[tool-catalog.md](../../../tool-catalog.md) 通过启动已发布的 tool 插件生成;[core-data-structures/](../../../core-data-structures/core.md) 使用 `ts type-equiv` 块保持粘贴的类型定义与源码同步。
|
||||
|
||||
这些参考资料是准确的,但它们大多是目录。维护者仍需自行综合关系:哪些 package 构成一条能力 seam、哪个应用捆绑了具体的主干、哪个事件是持久的而哪个是实时的、钩子或策略插件可以在哪里拦截工作、以及哪个面向模型的工具依赖哪个服务。SDK 用户从另一个角度面临同样的问题:「我想要某种行为,该安装或加载哪个 package?该扩展哪个事件/服务/工具?」
|
||||
这些参考文档是准确的,但大多是目录式的。维护者仍需自行综合关系:哪些包构成一个能力 seam、哪个应用组装了具体的主干、哪些事件是持久的而哪些是实时的、钩子或策略插件在哪里可以拦截工作、以及哪个面向模型的工具依赖哪个服务。SDK 用户从另一个角度面临同样的问题:「我想要某种行为,应该安装或加载哪个包?应该扩展哪个事件/服务/工具?」
|
||||
|
||||
钩子子系统使事件的生产者/消费者拓扑与拦截点变得更加重要;文件系统 seam 使能力 seam、策略否决、工具呈现和 SDK 组装路径变得更加重要。如果关系图仅限于一个小的 bash/todo/subagent 表面,它们会立刻陈旧。
|
||||
钩子子系统使事件的生产者/消费者拓扑与拦截点变得更加重要;文件系统 seam 使能力 seam、策略否决、工具呈现与 SDK 组装路径变得更加重要。如果关系图的范围仅限于一个小的 bash/todo/subagent 表面,它们会立即陈旧。
|
||||
|
||||
## 决策
|
||||
|
||||
新增生成的关系图文档,索引位于 [docs/graph-atlas.md](../../../graph-atlas.md),由专门的生成器产出,并由 `pnpm run verify-doc-graphs` 及既有的目录新鲜度检查(作为 `doc-sync` 的一环)进行验证。
|
||||
新增生成的关系图文档,索引位于 [docs/graph-atlas.md](../../../graph-atlas.md),由专用生成器产出,并通过 `pnpm run verify-doc-graphs` 及既有的目录新鲜度检查(作为 `doc-sync` 的一环)进行验证。
|
||||
|
||||
该索引是既有目录之上的关系层。它不替代精确的参考资料,而是链接到它们并解释各部分如何组合在一起。
|
||||
该索引是既有目录之上的关系层。它不取代精确的参考文档,而是链接到它们并解释各部分如何组合在一起。
|
||||
|
||||
### 维护模式
|
||||
|
||||
每个关系图页面声明一种维护模式:
|
||||
|
||||
- **生成(Generated)**:所有节点和边均从源码发现;如果已提交的产物陈旧,`--check` 失败。
|
||||
- **混合生成(Hybrid generated)**:源码发现清单,一份小型 manifest 对不可约的策略进行分类,完整性守卫在发现的条目未被分类时失败。
|
||||
- **人工维护(Curated)**:图表解释设计意图、时序或归属;它由生成器输出以保证关系图文档作为一个可重新生成的整体,但内容是有意撰写的。
|
||||
- **Generated(生成)**:所有节点和边均从源码发现;如果已提交的产物陈旧,`--check` 失败。
|
||||
- **Hybrid generated(混合生成)**:源码发现清单,一个小型 manifest 对不可约的策略进行分类,完整性守卫在发现的条目未被分类时失败。
|
||||
- **Curated(人工策划)**:图表解释设计意图、时序或归属;它由生成器输出以使关系图文档保持为可重新生成的整体,但内容是有意撰写的。
|
||||
|
||||
### 首批交付的索引
|
||||
### 首批发布的索引
|
||||
|
||||
首批索引链接十个关系面。package 拓扑与工具-package 能力映射位于既有的生成目录中(这些目录已拥有相应事实);其余的专项图表由 `scripts/gen-doc-graphs.ts` 生成。
|
||||
首批索引链接十个关系面。包拓扑与工具-包能力映射位于已有的生成目录中(这些目录已拥有相应事实);其余聚焦图表由 `scripts/gen-doc-graphs.ts` 生成。
|
||||
|
||||
| 关系图 | 维护模式 | 真源 |
|
||||
|---|---|---|
|
||||
| [模块依赖图](../../../module-graph.md) | 生成 | `packages/*/*/package.json` 的 peer dependencies 加 package 分组路径 |
|
||||
| [工具 schema 目录与 package 映射](../../../tool-catalog.md) | 生成 | 启动采集的工具 schema 加工具-package 的服务/副作用元数据 |
|
||||
| [能力 seam 与核心服务](../../../capability-seams.md) | 混合生成 | Cordis 服务声明加 `gen-doc-graphs.ts` 中的角色 manifest |
|
||||
| [echo-agent 应用组合](../../../../examples/echo-agent/composition.md) | 混合生成 | `examples/echo-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 |
|
||||
| [coding-agent 应用组合](../../../../examples/coding-agent/composition.md) | 混合生成 | `examples/coding-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 |
|
||||
| [acp-agent 应用组合](../../../../examples/acp-agent/composition.md) | 混合生成 | `examples/acp-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 |
|
||||
| [事件生产者/消费者矩阵](../../../event-producer-consumer.md) | 混合生成 | Cordis 事件声明、AST 扫描的 `ctx.on/emit/parallel/serial/waterfall` 调用点,以及显式的动态分发覆盖 |
|
||||
| [agent 轮次与步骤生命周期](../../../agent-lifecycle.md) | 人工维护 | architecture.md 的循环生命周期、Cordis 目录链接与会话事件语义 |
|
||||
| [工具执行流水线](../../../tool-execution-pipeline.md) | 人工维护 | 工具流水线语义与 `tools/execute` waterfall(瀑布式事件) |
|
||||
| [ACP 快照回放](../../../../packages/ui/acp/snapshot-replay.md) | 人工维护 | 快照 harness 行为 |
|
||||
| [模块依赖图](../../../module-graph.md) | generated | `packages/*/*/package.json` 的 peer dependencies 加包分组路径 |
|
||||
| [工具 schema 目录与包映射](../../../tool-catalog.md) | generated | 启动收集的工具 schema 加工具-包的服务/副作用元数据 |
|
||||
| [能力 seam 与核心服务](../../../capability-seams.md) | hybrid generated | Cordis 服务声明加 `gen-doc-graphs.ts` 中的角色 manifest |
|
||||
| [echo-agent 应用组合](../../../../examples/echo-agent/composition.md) | hybrid generated | `examples/echo-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 |
|
||||
| [coding-agent 应用组合](../../../../examples/coding-agent/composition.md) | hybrid generated | `examples/coding-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 |
|
||||
| [acp-agent 应用组合](../../../../examples/acp-agent/composition.md) | hybrid generated | `examples/acp-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 |
|
||||
| [事件生产者/消费者矩阵](../../../event-producer-consumer.md) | hybrid generated | Cordis 事件声明、AST 扫描的 `ctx.on/emit/parallel/serial/waterfall` 调用点,以及显式的动态分发覆盖 |
|
||||
| [agent 轮次与步骤生命周期](../../../agent-lifecycle.md) | curated | architecture.md 的循环生命周期、Cordis 目录链接与会话事件语义 |
|
||||
| [工具执行流水线](../../../tool-execution-pipeline.md) | curated | 工具流水线语义与 `tools/execute` waterfall(瀑布式事件) |
|
||||
| [ACP 快照回放](../../../../packages/ui/acp/snapshot-replay.md) | curated | 快照 harness 行为 |
|
||||
|
||||
### 为什么由生成器拥有这些文档
|
||||
### 为什么由生成器拥有文档
|
||||
|
||||
package 拓扑留在 `gen-module-graph.ts`,工具-package 能力映射留在 `gen-tool-catalog.ts`,因为这些生成器已经拥有权威事实和新鲜度门禁。`gen-doc-graphs.ts` 拥有其余关系页面和索引。代价是人工维护的图表需要在 TypeScript 字符串块中编辑,而非直接编辑 Markdown。对首批交付而言这是可接受的,因为面向用户的产物仍是纯 Markdown/Mermaid;如果撰写体验比可重新生成更重要,未来可以将人工维护的页面拆分出来。
|
||||
包拓扑留在 `gen-module-graph.ts`,工具-包能力映射留在 `gen-tool-catalog.ts`,因为这些生成器已经拥有权威事实和新鲜度门禁。`gen-doc-graphs.ts` 拥有其余关系页面和索引。代价是人工策划的图表需要在 TypeScript 字符串块中编辑,而非直接编辑 Markdown。对于首版来说这是可接受的,因为面向用户的产物仍然是纯 Markdown/Mermaid;未来如果撰写体验比可重新生成更重要,可以将人工策划的页面拆分出去。
|
||||
|
||||
### 完整性守卫
|
||||
|
||||
混合生成的页面在其 manifest 陈旧时必须显式失败:
|
||||
混合生成的页面在其 manifest 陈旧时必须显式报错:
|
||||
|
||||
- 模块图读取每个 package 的 `peerDependencies`,并按 `packages/<group>/<pkg>` 路径对 package 分组。
|
||||
- 工具目录通过启动采集已发布的工具,并从同一份 manifest(其完整性守卫已在检查)渲染 package/服务/副作用映射。
|
||||
- 能力 seam 图导入 Cordis 服务收集器,断言每个被发现的 harness `ctx.<key>` 都已在 `SERVICE_ROLES` 中分类,且每个已分类的 key 仍然存在。
|
||||
- 事件生产者/消费者矩阵标记为混合生成,因为 subagent 生命周期事件有意使用 `ctx.events.dispatch` 实现逐监听器隔离;这些动态边是显式覆盖而非无声遗漏。
|
||||
- `verify-mermaid` 用 Mermaid 自身的解析器解析仓库中每个 ` ```mermaid ` 围栏,因此语法错误会在本地和 CI 的 `doc-sync` 中失败,而不是在 GitHub 渲染时才显示为损坏的图表。
|
||||
- 模块图读取每个包的 `peerDependencies`,并按 `packages/<group>/<pkg>` 路径对包进行分组。
|
||||
- 工具目录通过启动收集已发布的工具,并从同一份 manifest 渲染包/服务/副作用映射(其完整性守卫已在检查该 manifest)。
|
||||
- 能力 seam 图导入 Cordis 服务收集器,断言每个发现的 harness `ctx.<key>` 都已在 `SERVICE_ROLES` 中分类,且每个已分类的 key 仍然存在。
|
||||
- 事件生产者/消费者矩阵标记为 hybrid,因为 subagent 生命周期事件有意使用 `ctx.events.dispatch` 实现逐监听器隔离;这些动态边是显式覆盖而非无声遗漏。
|
||||
- `verify-mermaid` 使用 Mermaid 自身的解析器解析仓库中每个 ` ```mermaid ` 围栏,因此语法错误在本地和 CI 的 `doc-sync` 阶段即被捕获,而非在 GitHub 渲染时才显示为损坏的图表。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
已提交的图表使用 Mermaid,因为 GitHub 在 Markdown 中原生渲染它,且不引入新的文档构建依赖;密集的多对多数据(如事件生产者/消费者关系)则使用 Markdown 表格。**PlantUML、托管图表服务和生成的 SVG** 曾被考虑,但在 Mermaid 成为瓶颈之前有意不采用。
|
||||
已提交的图表使用 Mermaid,因为 GitHub 在 Markdown 中原生渲染它且不引入新的文档构建依赖;密集的多对多数据(如事件生产者/消费者关系)改用 Markdown 表格。**PlantUML、托管图表服务和生成的 SVG** 曾被考虑,但在 Mermaid 成为瓶颈之前有意不采用。
|
||||
|
||||
## 后果
|
||||
|
||||
- 维护者获得了拓扑、seam、事件流、生命周期、应用组合和快照行为的可视化入口。
|
||||
- SDK 用户获得了从用例到 package 组合的路径,而不仅仅是自底向上的 package 参考。
|
||||
- `doc-sync` 现在包含 `verify-doc-graphs` 和 `verify-mermaid`,因此关系图漂移和 Mermaid 语法错误与其他文档新鲜度门禁一同被捕获。
|
||||
- 维护者获得了拓扑、seam、事件流、生命周期、应用组合与快照行为的可视化入口。
|
||||
- SDK 用户获得了从用例到包组合的路径,而非仅有自底向上的包参考。
|
||||
- `doc-sync` 现在包含 `verify-doc-graphs` 和 `verify-mermaid`,因此关系图漂移和 Mermaid 语法错误与其他文档新鲜度门禁一起被捕获。
|
||||
- 未来的文件系统和钩子工作有了承载新复杂度的具体位置:文件系统应扩展能力文档和工具目录,钩子应扩展事件矩阵和工具执行流水线。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-cordis-jsdoc-completeness-gate.md: 44a0ddce9c929deac3e03bb421aec1d5145e65ba
|
||||
2026-07-04-cordis-jsdoc-completeness-gate.zh.md: 6fea7f96a62295bd37e778a0aadd1e9ee6c8f2b4
|
||||
2026-07-04-cordis-jsdoc-completeness-gate.zh.md: 073054072748bf6cfed09cdcc222087bcc0ed929
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# RFC:Cordis 接口的 JSDoc 完整性门禁
|
||||
|
||||
Status: implemented
|
||||
# RFC:针对 Cordis 对外服务接口的 JSDoc 完整性门禁
|
||||
|
||||
[English](2026-07-04-cordis-jsdoc-completeness-gate.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
生成的 Cordis 目录已强制检查事件的 dispatch 模式,但未检查服务与事件契约的完整性。方法可以缺少描述,参数或返回值可以在跨插件 API 接口上不写文档——而这恰恰是 IDE 引导最重要的地方。
|
||||
生成的 Cordis 目录此前强制了事件分发模式,但未强制要求完整的服务与事件契约。方法可以缺少描述,参数或返回值可以在跨插件 API 接口上不写文档——而这恰恰是 IDE 引导最重要的地方。
|
||||
|
||||
AGENTS.md 规则("每个导出都有解释语义的 JSDoc")只能靠评审以行文方式检查;本仓库的既定偏好是将不变式编码为机械门禁。"Cordis 服务函数与事件"这一范围有一个精确的机器定义,只有目录生成器知道:事件是 `declare module 'cordis'` 内 `interface Events` 的成员,服务接口是每个 `interface Context` 键所指向的类的公开方法。ESLint 规则看不到这层映射;生成器在每次运行时都会计算它。
|
||||
AGENTS.md 中的规则(「每个导出都有解释语义的 JSDoc」)只能靠评审以行文形式检查;本仓库的既定偏好是将不变式编码为机械门禁。「Cordis 服务函数与事件」这一范围有精确的机器定义,只有目录生成器知道:事件是 `declare module 'cordis'` 内 `interface Events` 的成员,服务接口是每个 `interface Context` 键所指向的类的公开方法。ESLint 规则看不到这层映射;生成器在每次运行时计算它。
|
||||
|
||||
## 决策
|
||||
|
||||
扩展 `scripts/gen-cordis-catalog.ts`——同一次遍历、同一个 `@mode` 先例——对其目录化的所有内容强制 JSDoc 完整性。`verify-cordis-catalog` 运行在 `doc-sync` 内部,CI 和 lefthook pre-push 钩子都已执行 `doc-sync`,因此门禁无需新增任何接线(质量门禁原则:单一真源)。
|
||||
扩展 `scripts/gen-cordis-catalog.ts`(同一次遍历、同一个 `@mode` 先例),对其编目的所有内容强制 JSDoc 完整性。`verify-cordis-catalog` 在 `doc-sync`(文档同步门禁)内运行,CI 和 lefthook pre-push 钩子都已执行 `doc-sync`,因此门禁无需新增任何接线(质量门禁原则:单一真源)。
|
||||
|
||||
契约如下:
|
||||
|
||||
- **事件**需要描述文字,并为每个**载荷参数**提供非空 `@param`。载荷参数是签名中承载事件数据的参数;`this` 接收者注解和尾部的 waterfall `next` 免检——`next` 是 dispatch 机制,其语义已由 `@mode waterfall` 标签(及其结构交叉检查)拥有,逐事件重述只是样板。对免检参数写文档是允许的;门禁只检查缺失。
|
||||
- **服务类**需要类级 JSDoc,每个公开方法需要描述文字、每个参数一个非空 `@param`,以及一个非空 `@returns`(除非标注的返回类型是 `void`/`Promise<void>`,此时 `@returns` 可选——解析时机可能值得记录——但从不强制要求)。
|
||||
- **事件**需要描述性文字,以及为每个**载荷参数**提供非空的 `@param`。载荷参数是携带事件数据的签名参数;`this` 接收者注解和尾部的 waterfall(瀑布式事件) `next` 免检——`next` 是分发机制,其语义已由 `@mode waterfall` 标签(及其结构交叉检查)拥有,逐事件重述只是样板代码。为免检参数写文档是允许的;只有缺失才被检查。
|
||||
- **服务类**需要类级 JSDoc,每个公开方法需要描述性文字、为每个参数提供非空的 `@param`,以及非空的 `@returns`——除非标注的返回类型是 `void`/`Promise<void>`(此时 `@returns` 可选——resolve 时机有时值得记录——但从不强制要求)。
|
||||
- **陈旧标签报错**:`@param` 命名了一个不存在的参数即为违规,与 `@mode` 与签名矛盾的检查对称。标签描述必须非空;超出此范围的语义质量由评审负责。
|
||||
- **遍历可检查的显式性**:门禁是纯 AST 遍历(不使用类型检查器),因此服务方法必须显式标注返回类型(推断的返回类型无法分类),接口参数必须是简单标识符(解构模式没有名字供 `@param` 匹配)。
|
||||
- **违规聚合**为一条错误信息,列出所有违规项——修复时一次看到全部。此前快速失败的 `@mode` 检查也移入同一份聚合报告,消息文本不变。
|
||||
- **遍历可检查的显式性**:门禁是纯 AST 遍历(不使用类型检查器),因此服务方法必须显式标注返回类型(推断的返回类型无法分类),接口参数必须是简单标识符(解构模式没有名称供 `@param` 匹配)。
|
||||
- **违规聚合**为一条错误信息,列出所有违规项——修复时一次看到完整清单。此前快速失败的 `@mode` 检查也移入同一份聚合报告,消息文本不变。
|
||||
|
||||
这些标签**仅用于门禁强制**:`parseJsDoc` 现在在遇到第一个块标签时截止描述文字(标准 JSDoc 语义,同时也防止多行标签描述泄漏到目录中成为正文),因此 `@param`/`@returns` 永远不会改变渲染出的目录。
|
||||
这些标签**仅用于强制检查**:`parseJsDoc` 现在在遇到第一个块标签时截止描述性文字(标准 JSDoc 语义,同时也防止多行标签描述泄漏到目录中充当正文),因此 `@param`/`@returns` 不会改变渲染出的目录。
|
||||
|
||||
`packages/core/agent/tests/gen-cordis-catalog.spec.ts` 中的负向路径测试用合成 fixture(测试前置数据)驱动 `collectEvents`/`collectServices`,证明每个守卫都能触发且免检规则成立。撰写规则写在根 [AGENTS.md](../../../../AGENTS.md) 约定条目中,与 `@mode` 规则并列。
|
||||
`packages/core/agent/tests/gen-cordis-catalog.spec.ts` 中的负路径测试对合成 fixture(测试前置数据)运行 `collectEvents`/`collectServices`,验证每条守卫都会触发且免检规则成立。撰写规则写在根 [AGENTS.md](../../../../AGENTS.md) 的约定条目中,与 `@mode` 规则并列。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **ESLint 规则**:看不到范围的机器定义(哪些 `interface Events` 成员、哪些 `ctx.<key>` 类构成 Cordis 接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在那里。
|
||||
- **将标签渲染到目录中**:曾考虑将服务部分重构为逐方法条目,但有意推迟:方法文档的消费场景是源码 JSDoc 加 IDE 悬浮提示,目录保持索引定位。
|
||||
- **逃生标签**:不设。接口面小且经过策划(采纳时 12 个服务、57 个方法、27 个事件),重点在于检查不可豁免。
|
||||
- **ESLint 规则**:无法看到该范围的机器定义(哪些 `interface Events` 成员、哪些 `ctx.<key>` 类构成 Cordis 对外服务接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在那里。
|
||||
- **将标签渲染到目录中**:曾考虑将服务部分重构为逐方法条目,但有意推迟:方法文档的消费场景是源码 JSDoc 加 IDE 悬停,目录保持索引定位。
|
||||
- **逃逸标签**:不设。该接口面小且经过策展(采纳时 12 个服务、57 个方法、27 个事件),要点在于检查不可豁免。
|
||||
|
||||
## 后果
|
||||
|
||||
- 新增事件或服务方法如果参数或返回值未写文档,就无法合入:生成器拒绝重新生成,`verify-cordis-catalog` 在 pre-push 和 CI 中失败。采纳时发现的约 139 处缺口在同一个变更中补齐,门禁以绿色状态落地。
|
||||
- 服务接口必须显式标注返回类型并使用标识符参数。两项约束在采纳时均未构成负担(所有方法已有标注;不存在解构的 seam 参数);二者现在都是承重要求,违反时会被机械发现。
|
||||
- AGENTS.md 的通用 JSDoc 规则("一行能说清就写一行")在此接口上获得一条更严格的特例:只有当方法无参数且返回 void 时,一行摘要才仍然足够。
|
||||
- 对 `next` 或 `this` 写 `@param` 合法但不检查——这是有意的不对称:门禁强制载荷契约,拒绝索要样板。
|
||||
- 标签不改变渲染出的目录(正文在第一个块标签处截止)。如果日后需要方法级渲染,那是目录设计的独立决策,不是本门禁的缺口。
|
||||
- 新增事件或服务方法时,若参数或返回值未写文档则无法落地:生成器拒绝重新生成,`verify-cordis-catalog` 在 pre-push 和 CI 中失败。采纳时发现的约 139 处缺口在同一个变更中补齐,门禁以绿色状态落地。
|
||||
- 服务接口必须显式标注返回类型并使用标识符参数。两项约束在采纳时均未构成限制(所有方法已有标注;不存在解构的 seam 参数);但二者现在是承重要求,违反时会被机械检测到。
|
||||
- AGENTS.md 中通用的 JSDoc 规则(「一行能说清就用一行」)在此接口上获得了更严格的特例:仅当方法无参数且返回 void 时,一行摘要才足够。
|
||||
- 为 `next` 或 `this` 写 `@param` 合法但不检查——这是有意的不对称:门禁强制载荷契约,拒绝要求样板代码。
|
||||
- 渲染出的目录不受这些标签影响(正文在第一个块标签处截止)。如果后续需要方法级渲染,那是一个独立的目录设计决策,而非本门禁的缺口。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-doc-tiers-and-budgets.md: ca9da847849f61f2fb244932e657cca8fec69696
|
||||
2026-07-04-doc-tiers-and-budgets.zh.md: 37447d11de181a007bcea23e29084aefd97b1ab5
|
||||
2026-07-04-doc-tiers-and-budgets.zh.md: ae34e3f04d7986f78d1b3ceeaacb3bc4c7bc64f5
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# RFC:文档分层、预算与上限门禁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-04-doc-tiers-and-budgets.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
尽管已有写作指导,常设文档仍然积累了重复的规则、重述的事件、重复的 package 地图和陈旧的 RFC 摘要。由于仅靠评审无法阻止这种膨胀,仓库需要在文档分类体系之外再加一道机械化的预算。
|
||||
尽管已有写作指导,常设文档仍然积累了重复的规则、重述的事故、重复的包(package)映射和陈旧的 RFC 摘要。仅靠评审无法阻止这种膨胀,因此仓库需要在文档分类体系之外增加一道机械化的预算约束。
|
||||
|
||||
## 决策
|
||||
|
||||
- **分层分类体系,每条事实只有一个归属。** [docs/AGENTS.md](../../../AGENTS.md) 是文档标准:它为每个 Markdown 层级指定唯一职责(常设指令、系统地图、类型目录、决策记录、事件故事、实操手册(cookbook)、package 契约、生成目录、工作流),禁止在归属层级之外重述事实(应改为链接),并附带一份在撰写或评审任何文档时使用的冗余检查清单。
|
||||
- **窄范围、硬约束的预算门禁。** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 加入 doc-sync:凡列入 [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 的文档都必须低于其字数上限(`wc -w` 语义,整个文件),且已设预算的文件若缺失也会使门禁失败,防止重命名时预算被静默遗留。范围有意仅限于容易膨胀的常设文档:根目录与子树的 `AGENTS.md`、`architecture.md`、`packages/README.md`,以及它们将内容分流到的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、RFC 和 package README 不设预算:当每一行都是事实时,长度是合理的,由评审加冗余检查清单管控。
|
||||
- **上限是只进不退的执行红线。** 上限设定在文档当前大小的至少 5% 以上(留出操作余量,使日常措辞修改不会触发门禁,而真正的膨胀仍会被拦截),并随着文档被压缩到目标预算(根 `AGENTS.md` ≤ 1,500 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600;`packages/README.md` ≤ 600)而保持该余量向下收紧——与[翻译配对 `required` 清单](2026-07-02-bilingual-docs-and-pairing-gate.md)的推进机制相同。门禁变红时,修复方式是按分类体系迁移或精简内容;只有在 PR 描述中给出明确理由时才允许提高上限,manifest diff 本身即为可评审的动作。
|
||||
- **轻量工作流 skill,契约在文档中。** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) 承载归位/审计/红灯修复工作流,并将文档标准作为真源——与 [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) 对 i18n 契约的分工方式相同。
|
||||
- **分层分类体系,每条事实只有一个归属地。** [docs/AGENTS.md](../../../AGENTS.md) 是文档标准:它为每个 Markdown 层级指定唯一职责(常设指令、系统地图、类型目录、决策记录、事故叙事、实操手册、逐包契约、生成目录、工作流),禁止在归属层级之外重述事实(应以链接代替),并附带一份在撰写或评审任何文档时使用的冗余检查清单。
|
||||
- **窄范围、硬约束的预算门禁。** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 加入 `doc-sync`(文档同步门禁):[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 中列出的每篇文档都必须低于其字数上限(`wc -w` 语义,整个文件),且受预算约束的文件如果缺失也会导致门禁失败,防止重命名后预算被静默遗留。范围刻意限定为容易膨胀的常设文档:根目录和子树的 `AGENTS.md`、`architecture.md`、`packages/README.md`,以及它们将内容分流到的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、RFC 和 package README 不设预算:当每一行都是事实时,长度是合理的,由评审加冗余检查清单来管控。
|
||||
- **上限是只进不退的执行红线。** 上限设定为文档当前字数的至少 105%(留出工作余量,使日常措辞调整能通过,而真正的膨胀仍会触发门禁),并随着文档被精简到目标预算而同步下调、保持该余量(根 `AGENTS.md` ≤ 1,500 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600;`packages/README.md` ≤ 600)。推进机制与[翻译配对的 `required` 清单](2026-07-02-bilingual-docs-and-pairing-gate.md)相同。门禁变红时,修复方式是按分类体系迁移或压缩内容;只有在 PR(Pull Request)描述中给出明确理由时才允许提高上限,manifest(元数据清单)的 diff 本身即为可评审的动作。
|
||||
- **轻量工作流 skill(技能),契约在文档中。** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) 承载放置/审计/红灯修复工作流,并将文档标准作为真源,与 [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) 对 i18n 契约的分工方式一致。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **仅靠 skill 与评审纪律,不设门禁**:否决。上述膨胀正是在既有的现状规则和评审者注意力下发生的;一条没有机械后盾的行文规则在这里已被证明守不住,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.md)说的是:值得保持的不变式就值得编码。
|
||||
- **对所有文档层级设置宽泛门禁**:否决。一刀切的上限恰恰惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实,例如 `packages/ui/acp/acp-feature-support.md`),并产生逐文件的例外修改,训练贡献者无脑批准上调。
|
||||
- **将标准放在 skill 内部**:否决。契约放在文档中,工作流放在 skill 中;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent 就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被加载给所有在 `docs/` 下工作的人。
|
||||
- **仅靠 skill 和评审纪律,不设门禁**:否决。上述膨胀正是在现行规则和评审注意力已经存在的情况下发生的;一条没有机械后盾的行文规则在此处已被证明无法维持,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.md)认为值得保持的不变式就值得编码。
|
||||
- **对所有文档层级全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如特性矩阵或类型目录,每一行都是事实,例如 `packages/ui/acp/acp-feature-support.md`),并产生逐文件的例外变更,训练贡献者机械地批准提限。
|
||||
- **将标准放在 skill 内部**:否决。契约归文档,工作流归 skill;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent(智能体)就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被任何在 `docs/` 下工作的人加载。
|
||||
|
||||
## 后果
|
||||
|
||||
- 向已设预算的文档添加内容现在需要置换:将新增内容迁移到其分类归属处并留下指针,或精简既有行文为其腾出空间。只增不减会导致 CI 失败。
|
||||
- 将文档压缩到目标预算的重写以堆叠的后续 PR 落地,每个合并时都将 manifest 中的上限向下收紧;在各自落地之前,文档的冻结上限仅阻止进一步膨胀。
|
||||
- 字数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它恰好在内容被添加的那一刻强制触发迁移决策——而那正是作者拥有足够上下文来正确归位内容的时刻。
|
||||
- 向受预算约束的文档添加内容现在需要置换:将新增内容迁移到其分类体系归属地并留下指针,或压缩现有行文来腾出空间。只增不减会导致 CI 失败。
|
||||
- 精简到目标预算的重写以堆叠的后续 PR 落地,每次合并时同步下调 manifest 中的上限;在各自落地之前,文档冻结的上限仅阻止进一步膨胀。
|
||||
- 字数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它在内容被添加的那一刻强制触发迁移决策,而那正是作者拥有足够上下文来正确放置内容的时刻。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-generate-rfc-index-tables.md: 6a8888eda8b7cf7105a44802774bf49d6463952d
|
||||
2026-07-04-generate-rfc-index-tables.zh.md: 42ae64306aa1d5cf9117ca2b4d698a5d8effdeec
|
||||
2026-07-04-generate-rfc-index-tables.zh.md: aad1652edc9de81a70a7a391700f63be9499035c
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
# RFC:生成 RFC 索引表
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-04-generate-rfc-index-tables.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
RFC 索引中按生命周期/按类别的表格所列信息完全可推导:RFC 的路径编码了生命周期与类别,文件名编码了首次提出日期,H1 标题即为标题。手工维护这些事实的副本恰恰是本仓库文档中冲突最频繁的热点:每一波提案都向同几行追加行,因此并行的 RFC 分支恰好在此处冲突,而在其他所有地方都没有分歧;每次冲突都要手动合并那些文件系统本已知晓内容的行。[分类 RFC](2026-06-20-rfc-classification.md) 最初为了策展目的保留手写索引,但 README 中真正需要策展的部分是行文,而行文从不冲突;冲突的只有机械表格。
|
||||
RFC 索引中按生命周期/按分类的表格所列信息完全可以推导:RFC 的路径编码了生命周期与分类,文件名编码了首次提出日期,H1 标题承载了标题文本。这些信息的手工维护副本也是仓库中冲突最频繁的文档热点:每一波提案都在同几行后追加新行,因此并发的 RFC 分支恰好在此处冲突,而其他地方完全一致;每次冲突都要手工合并那些文件系统本已知晓的行。[分类 RFC](2026-06-20-rfc-classification.md) 最初为了可策展性而保留手写索引,但 README 中真正需要策展的是行文,而行文从不冲突;冲突的只有机械表格。
|
||||
|
||||
## 决策
|
||||
|
||||
保留策展行文;生成列表。表格位于 [`docs/rfc/INDEX.md`](../../INDEX.md),是一个**完全生成的文件**;策展行文留在 README.md 中,README.md 不包含任何索引行。[`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) 是共享的真源:树遍历器(拥有封闭的生命周期/类别集合与结构规则,包括 H1 可解析的要求)和渲染器(行来自 H1 标题并去除 `RFC: ` 前缀,加上文件名日期,按日期再按文件名排序,以 `### {Class}` 分节、按规范类别顺序分组)。两个轻量消费方共享它:
|
||||
保留策展行文;生成列表。表格位于 [`docs/rfc/INDEX.md`](../../INDEX.md),是一个**完全生成的文件**——策展行文留在 README.md 中,README.md 不包含任何索引行。[`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) 是共享的真源:树遍历器(拥有封闭的生命周期/分类集合与结构规则,包括对可解析 H1 的要求)和渲染器(行来自 H1 标题并去掉 `RFC: ` 前缀,加上文件名日期,按日期再按文件名排序,以 `### {Class}` 分节、按规范分类顺序分组)。两个轻量消费方共享它:
|
||||
|
||||
- [`scripts/gen-rfc-index.ts`](../../../../scripts/gen-rfc-index.ts)(`pnpm run gen-rfc-index`)从目录树完整重写 INDEX.md。
|
||||
- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts)(doc-sync 的一个成员)检查结构,断言已提交的 INDEX.md 与新鲜渲染结果逐字节一致(与 `gen-cordis-catalog`/`verify-cordis-catalog` 模式相同),并拒绝在策展 README 中出现索引格式的行。新鲜度检查涵盖了索引完整性检查:从磁盘生成的表格在定义上就是完整且标题正确的。
|
||||
- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts)(doc-sync(文档同步门禁)的一个成员)检查结构,断言已提交的 INDEX.md 与新鲜渲染结果逐字节一致(`gen-cordis-catalog`/`verify-cordis-catalog` 模式),并拒绝在策展 README 中出现索引格式的行。新鲜度检查涵盖了索引完整性检查:从磁盘生成的表格在定义上就是完整的、标题正确的。
|
||||
|
||||
添加、移动或删除一个 RFC 只需编辑 RFC 文件本身并运行生成器;分类 RFC 的「否决替代方案」记录中带有取代关系的交叉链接。
|
||||
添加、移动或删除一个 RFC 只需编辑 RFC 文件本身并运行生成器;分类 RFC 的「已否决替代方案」记录中带有替代关系的交叉链接。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不在 README.md 内使用标记分隔区域?
|
||||
|
||||
最初落地的形态:生成器在 README.md 中的 `gen-rfc-index` 标记注释之间、每个 `## {Lifecycle}` 标题下拼接表格。在 README 同时吸收了文件内格式契约([统一格式 RFC](2026-07-05-uniform-rfc-format.md))之后,被整文件 INDEX.md 方案取代:一个门面 README 承载数百行生成行,会淹没其策展行文;而拼接机制(标记对、标题检查、区域外行检测)的存在只是为了保护策展文本——专用的生成文件根本不包含策展文本。
|
||||
最初落地的形态是:生成器将表格拼接到 README.md 中 `gen-rfc-index` 标记注释之间、各 `## {Lifecycle}` 标题之下。在 README 同时吸收了文件内格式契约([统一格式 RFC](2026-07-05-uniform-rfc-format.md))之后,被整文件 INDEX.md 方案取代:一个门面 README 承载数百行生成内容会淹没其策展行文,而拼接机制(标记对、标题检查、区域外行检测)的存在仅仅是为了保护策展文本——专用的生成文件根本不包含这类文本。
|
||||
|
||||
### 为什么不采用纯校验模式?
|
||||
### 为什么不采用纯校验器模式?
|
||||
|
||||
纯校验能捕获错误,但每次提案编辑仍然要在手工维护的表格中触碰共享热点;对于一行纯机械内容,校验失败比生成器更令人烦恼:作者已经命名并放置了文件,索引副本不增加任何信息。这与 [package-inventory 提案](../../proposed/process/2026-06-20-discover-package-inventory.md) 对 tsconfig references 和 knip stanzas 所做的「手工列表 vs. 推导」判断相同——应用于这张确实会冲突的列表。
|
||||
校验器能捕获错误,但每次提案编辑仍然要在手工维护的表格中触碰共享热点;对于纯机械的行,校验器失败比生成器更令人烦恼:作者已经命名并放置了文件,索引副本不增加任何信息。这与 [package-inventory 提案](../../proposed/process/2026-06-20-discover-package-inventory.md) 对 tsconfig references 和 knip stanzas 所做的手写列表与推导之间的判断一致——应用于这张确实会冲突的列表。
|
||||
|
||||
## 后果
|
||||
|
||||
- 生成文件是显式的:其横幅标注了生成器名称,文件内没有需要保护的策展区域,且生成器在目录树结构无效时拒绝运行。
|
||||
- 格式错误或缺失的 H1 在生成器和门禁中都是硬错误:H1 现在是承重的,它是索引标题的来源。
|
||||
- 并行的 RFC 分支通过重新运行生成器来解决索引冲突,而非手动合并行。
|
||||
- 格式错误或缺失的 H1 在生成器和门禁中都是硬错误——H1 现在是索引标题的承重来源。
|
||||
- 并发的 RFC 分支通过重新运行生成器解决索引冲突,从不手工合并行。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-persistence-log-catalog.md: f8f831ca0470cf3b5c7634550115e67f7deac840
|
||||
2026-07-04-persistence-log-catalog.zh.md: d8cd5f74e24968fa2ad01f128dafe8c867f3406c
|
||||
2026-07-04-persistence-log-catalog.zh.md: 1daa76e2b23484ad6434f6a55482672abf456eb7
|
||||
|
||||
@@ -6,31 +6,31 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`SessionEventMap` 是磁盘上的词汇(vocabulary),但其声明分散在所属的 session 包与声明合并中。生成式持久化目录是每个事件及其 payload 的唯一参考;手工维护的表格会漂移,已被移除。这些记录不是 Cordis 事件:观察者通过唯一的 `session/event` 总线事件接收它们,因此 Cordis 目录无法覆盖。生成器会发现所有声明,doc-sync(文档同步门禁)新鲜度门禁会拒绝遗漏或陈旧的输出。
|
||||
`SessionEventMap` 是磁盘上的词汇,但其声明分散在拥有它的 session 包(package)与声明合并中。生成式持久化目录是所有事件与 payload 的唯一参考;手工维护的表格会漂移,已被移除。这些记录不是 Cordis 事件,观察者通过单一的 `session/event` 总线事件接收它们,因此 Cordis 目录无法覆盖。生成器发现所有声明,doc-sync(文档同步门禁)的新鲜度门禁拒绝遗漏或陈旧的输出。
|
||||
|
||||
## 决策
|
||||
|
||||
从源码生成 `docs/persistence-catalog.md`,配以新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的*记录*,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。
|
||||
从源码生成 `docs/persistence-catalog.md`,配合新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的*记录*,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。
|
||||
|
||||
`gen-persistence-catalog.ts` 使用 TypeScript AST 扫描所有所属的和声明合并的 `SessionEventMap`。它渲染源码 JSDoc、payload 类型、派生的 surface 徽章、参考链接和源码位置。doc-sync 新鲜度检查会拒绝任何词汇变更后未重新生成目录的情况。
|
||||
`gen-persistence-catalog.ts` 使用 TypeScript AST 扫描所有拥有方与声明合并的 `SessionEventMap`。它渲染源码 JSDoc、payload 类型、派生的 surface 徽章、参考链接与源码位置。doc-sync 新鲜度检查会拒绝任何词汇变更后未重新生成目录的情况。
|
||||
|
||||
具体选择:
|
||||
|
||||
- **JSDoc 完整性,强制执行。** 每个成员必须携带描述性文字:JSDoc 即为目录条目,与 Cordis 目录对总线事件施加的强制函数相同。成员上的 `@mode` 标签是硬错误:dispatch mode 属于 Cordis 总线事件,日志事件没有 mode;该标签会被误读为「此事件以 mode X 在总线上触发」。违规项聚合为一条错误,列出所有违规者。
|
||||
- **surface 徽章由派生得出,而非手工列举。** `SurfaceEventType`(产生 LLM 消息且可能携带 `surfaceOp` 的子集)从所属包中的 union 声明解析而来;union 成员如果命名了一个未声明的事件,则为硬错误(否则一个陈旧的 union 成员会静默地不标注任何事件)。其余一律渲染为 **log-only**。
|
||||
- **专用围栏。** payload 块使用 ` ```ts persistence-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它,不计入 opt-out 比例——与 `ts cordis-catalog` 的处理方式相同(裸 payload 片段不能独立编译)。
|
||||
- **仓库范围。** 目录枚举本仓库中的包,与兄弟目录的 packages-only 范围一致;下游插件可以合并更多事件类型,但它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:所属的顶层 `interface SessionEventMap` 必须是 `@deepseek-ai/dsh-session` 中唯一的导出声明(一个无关的、局部的或重复的同名接口不能被当作磁盘词汇编入目录);任何声明不得携带 `extends`(继承的键会加入 `keyof SessionEventMap` 却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入 `keyof` 却被静默遍历跳过);跨声明的重复成员会失败。
|
||||
- **JSDoc 完整性,强制执行。** 每个成员必须带有描述性文字——JSDoc 即为目录条目,与 Cordis 目录对总线事件施加的强制机制相同。成员上的 `@mode` 标签是硬错误:dispatch mode 属于 Cordis 总线事件,日志事件没有 mode,该标签会被误读为「此事件以模式 X 在总线上触发」。违规项聚合为一条错误,列出所有违规者。
|
||||
- **surface 徽章由派生得出,而非手工列举。** `SurfaceEventType`(产生 LLM(大语言模型)消息且可能携带 `surfaceOp` 的子集)从拥有方包中的 union 声明解析;如果 union 成员命名了一个未声明的事件,则为硬错误(否则陈旧的 union 成员会静默地不标注任何内容)。其余一律渲染为 **log-only**。
|
||||
- **专用围栏。** payload 块使用 ` ```ts persistence-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它,不计入 opt-out 比例——与 `ts cordis-catalog` 的处理方式相同(裸 payload 片段无法独立编译)。
|
||||
- **仓库范围。** 目录枚举本仓库中的包,与兄弟文档的 packages-only 范围一致;下游插件可以合并更多事件类型,它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:拥有方的顶层 `interface SessionEventMap` 必须是 `@deepseek-ai/dsh-session` 中唯一的导出声明(无关的、局部的或同名重复的接口不能被当作磁盘词汇编入目录);任何声明不得携带 `extends`(继承的键会加入 `keyof SessionEventMap` 却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入 `keyof` 却在静默遍历中被漏过);跨声明的重复成员也会失败。
|
||||
|
||||
这取代了手工副本:session.md 的 `hook/*` 表格、compact README 的事件表格、hook-protocol README 的 payload 列表,以及 session README 的名称列表现在链接到目录,而非重述 payload(周围的语义行文保留原位)。hook-protocol 合并成员上两个多余的 `@mode emit` 标签已被移除——新门禁将其拒绝为它们本来就是的类别错误。
|
||||
本方案取代了手工副本:session.md 的 `hook/*` 表格、精简版 README 的事件表格、hook-protocol README 的 payload 条目列表,以及 session README 的名称列表现在链接到目录,而不再重述 payload(周围的语义说明文字保留原位)。hook-protocol 合并成员上的两个误加的 `@mode emit` 标签已被移除——新门禁将它们作为类别错误拒绝。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **基于启动的生成器(如工具目录的方式)**:日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。
|
||||
- **保留手工副本**:手工副本只能检查作者已经写下的名称;目录落地时 session README 的合并说明已经漂移。
|
||||
- **基于启动的生成器(类似工具目录)**:日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。
|
||||
- **保留手工副本**:手工副本只能检查作者已经写下的名称;目录落地时,session README 的合并说明已经漂移。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目录不可能漂移:词汇变更而已提交文件未反映的,`verify-persistence-catalog` 在 pre-push 钩子和 CI 中会失败;新合并的事件如果没有 JSDoc,生成器直接报错——插件不能再添加未文档化的磁盘记录类型。
|
||||
- 事件描述有唯一归属地:声明处的 JSDoc。JSDoc 写得薄,目录条目就薄,这对作者形成在源头写文档的压力。
|
||||
- 目录不会漂移:词汇变更若未反映在已提交的文件中,`verify-persistence-catalog` 会在 pre-push 钩子和 CI 中失败;新合并的事件若缺少 JSDoc,生成器直接报错——插件不再能添加未文档化的磁盘记录类型。
|
||||
- 事件描述有唯一归属地,即声明处的 JSDoc;JSDoc 写得单薄,目录条目就单薄,这迫使作者在源头做好文档。
|
||||
- `SurfaceEventType` union 现在对文档具有结构性承载作用:重命名事件而不更新 union(或反过来)会导致生成器失败,而不仅仅是编译器失败。
|
||||
- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个所有者;如果重构偏离了这一形状,必须在同一个变更中更新生成器。
|
||||
- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个拥有方;如果重构偏离了这一形状,必须在同一个变更中更新生成器。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-05-uniform-rfc-format.md: f67c61c0b627950cf07e7d57672324308a9462ec
|
||||
2026-07-05-uniform-rfc-format.zh.md: a11cba1c343f150a67c1af4a04a8d89480185207
|
||||
2026-07-05-uniform-rfc-format.zh.md: b175c2d5b7537e793a61c95b6524d08bc4216384
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
# RFC:为 RFC 统一一种有门禁保障的文件内格式
|
||||
|
||||
Status: implemented
|
||||
# RFC:RFC 的统一受门禁约束的文件内格式
|
||||
|
||||
[English](2026-07-05-uniform-rfc-format.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
RFC 的路径已编码了生命周期与分类,但文件内容仍然混杂着不同的标题风格、状态格式、ADR 模板与提案模板,以及已实施记录中残留的提案时期章节。作者复制手边找到的任何邻居文件作为模板,而生命周期迁移可以跳过必要的改写,因为没有门禁强制执行文件内契约。
|
||||
RFC 的路径已经编码了生命周期和分类,但文件内容仍然混杂着不同的标题风格、状态格式、ADR 与 proposal 模板,以及已实现记录中残留的 proposal 时期的章节。作者随手复制找到的任何邻近文件,生命周期迁移时可以跳过必要的改写,因为没有门禁强制执行文件内契约。
|
||||
|
||||
## 决策
|
||||
|
||||
[README.md § The file format](../../README.md#the-file-format) 即为文件内契约:头部块(`# RFC: <title>` 加上不含日期、与所在文件夹一致的 `Status:` 枚举,其唯一内容是否决原因);按生命周期区分的正文骨架(所有阶段都以 `Problem` 开头;`proposed/` 中使用 `Proposal`/`Acceptance criteria`/`Risks`;`implemented/` 中使用现在时的 `Decision`/`Consequences` 且禁止提案时期标题;`rejected/` 中冻结提案形态);强制的 `Alternatives considered` 章节;以及规范的章节词汇表——在这些固定章节之间,自定义的技术章节保持自由格式。`pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../../../scripts/verify-rfc-format.ts))作为 doc-sync(文档同步门禁)的一环强制执行每一条机械化条款,因此跳过改写的生命周期迁移现在会导致 CI 失败,而非依赖评审者的记忆。
|
||||
[README.md § The file format](../../README.md#the-file-format) 即文件内契约:头部块(`# RFC: <title>` 加上无日期、与所在文件夹一致的 `Status:` 枚举,唯一的正文内容是 rejection reason);按生命周期区分的正文骨架(所有阶段都以 `Problem` 开头;`proposed/` 中为 `Proposal`/`Acceptance criteria`/`Risks`;`implemented/` 中为现在时态的 `Decision`/`Consequences` 且禁止 proposal 时期的标题;`rejected/` 中冻结 proposal 形态);必须包含 `Alternatives considered` 章节;以及规范的章节词汇表,其间的自定义技术章节保持自由形式。`pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../../../scripts/verify-rfc-format.ts))作为 doc-sync(文档同步门禁)的一环强制执行每条机械化条款,因此生命周期迁移时跳过改写现在会让 CI 失败,而不是依赖评审者的记忆。
|
||||
|
||||
整个语料库在定义格式的同一个变更中完成了规范化——这是预发布阶段的立场:不设过渡期,不容忍双格式并存。唯一的祖父条款针对内容而非格式:替代方案只记录已有的,不凭空编造;因此如果一篇格式定义之前的 RFC 的替代方案无法从记录中重建,它会携带 `rfc-format: alternatives-not-recorded` 注释,门禁仅对日期早于本 RFC 的文件接受该注释。
|
||||
整个语料库在定义格式的同一个变更中完成了规范化,这是预发布阶段的立场:没有过渡期,不容忍双格式并存。唯一的祖父条款针对内容而非格式:替代方案是记录下来的,不是凭空编造的;因此如果一篇格式定义前的 RFC 的替代方案无法从记录中重建,它会携带 `rfc-format: alternatives-not-recorded` 这条精确注释,门禁仅对日期早于本 RFC 的文件接受该注释。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **完全刚性模板**(每个生命周期一套固定章节顺序,所有 RFC 重构以适配):否决。大型设计 RFC 携带八到十五个自定义技术章节(包拓扑、协议格式契约、schema),这些是承重内容而非漂移;刚性顺序会迫使当下进行破坏性改写,并永远与模板对抗。
|
||||
- **仅规范化头部**(H1 与 Status,正文不动):否决。技术债标记指出的正是*正文*的体裁分裂,让 `Context`/`Decision` 与 `Problem`/`Proposal` 无限期并存什么也解决不了。
|
||||
- **不设 Status 行**(文件夹本身就是状态;三篇最新的格式定义前 RFC(及其中一篇的中文对侧文件)省略了该行):否决,保留自描述文件。当初促使去掉该行的漂移风险,已被「门禁将该行与文件夹做一致性校验」所消除。
|
||||
- **带日期的状态**(`Status: implemented (accepted YYYY-MM-DD)`):否决。接受日期属于叙述性历史,写作规则将其排除在文档之外;文件名承载首次提出日期,git 承载其余信息,门禁能检查日期格式但永远无法检查其真实性。
|
||||
- **裸 `# <title>` H1**:否决。`RFC: ` 前缀是语料库中的多数形式,且在文件脱离目录树阅读时能自描述体裁;索引生成器会剥离它,因此索引行无论哪种写法都一样。
|
||||
- **`## What we give up` 作为已实施记录的收尾章节**(README 自身用来描述 RFC 所记录内容的措辞):否决。它只命名了代价,而诚实的后果章节同时记录权衡所换来的收益。
|
||||
- **约定而无门禁**(写下契约,靠评审强制执行):否决。slop checklist 已通过约定禁止在 `implemented/` 中使用规范体措辞,而十九个文件展示了纯靠约定在这里能达到什么效果。
|
||||
- **独立的 `FORMAT.md` 契约文件**:最初落在此处;在生成索引迁出至 [INDEX.md](../../INDEX.md) 后折入 README.md:表格移走后 README 重新有了空间,一个前门同时承载布局、分类与格式,优于将契约拆分到两个文件。
|
||||
- **完全刚性的模板**(每个生命周期一个固定章节序列,所有 RFC 重构以适配):否决。大型设计 RFC 包含八到十五个自定义技术章节(包拓扑、协议格式契约、schema),这些是承重内容而非漂移;刚性序列会立即强制破坏性改写,并永远带来与模板的对抗。
|
||||
- **仅规范化头部**(H1 和 Status,正文不动):否决。债务标记指出的是*正文*的体裁分裂,让 `Context`/`Decision` 与 `Problem`/`Proposal` 无限期并存什么也解决不了。
|
||||
- **不设 Status 行**(文件夹本身就是状态;格式定义前最新的三篇 RFC(以及其中一篇的中文对侧文件)省略了该行):否决,保留自描述文件。省略 Status 行的动机是防止漂移,而将该行与文件夹做门禁校验即可消除漂移风险。
|
||||
- **带日期的 Status**(`Status: implemented (accepted YYYY-MM-DD)`):否决。接受日期属于叙述性历史,写作规则将其排除在文档之外;文件名承载首次提出日期,git 承载其余信息;门禁能检查日期格式,但永远无法检查其真实性。
|
||||
- **裸 `# <title>` H1**:否决。`RFC: ` 前缀是语料库中的多数形式,且在文件脱离目录树被阅读时能自描述体裁;索引生成器会剥离前缀,因此索引行无论哪种写法都一样。
|
||||
- **`## What we give up` 作为 implemented 的结尾章节**(README 自身对 RFC 记录内容的措辞):否决。它只命名了代价,而诚实的后果章节同样记录这笔权衡换来了什么。
|
||||
- **只有约定没有门禁**(写下契约,靠评审强制执行):否决。slop checklist 已经通过约定禁止在 `implemented/` 中使用 spec 语气,而十九个文件展示了仅靠约定在此处能达到什么效果。
|
||||
- **独立的 `FORMAT.md` 契约文件**:最初的落地位置;在生成索引迁出到 [INDEX.md](../../INDEX.md) 之后折入 README.md:表格移走后 README 重新有了空间,一个前门同时承载布局、分类和格式,优于将契约拆分到两个文件。
|
||||
|
||||
## 后果
|
||||
|
||||
每篇 RFC 现在多了少许结构成本,而强制的 `Alternatives considered` 章节是有意为之的摩擦:一个不记录被否决方案的决策,会招来 RFC 本应防止的反复讨论。格式定义前的 RFC 若其替代方案无法重建,则永久携带祖父条款注释——这是记录上的诚实空白,而非编造的理由。doc-sync 新增一道门禁,在生命周期文件夹之间迁移 RFC 现在是迁移时的实际工作(即迁移本就欠下的正文改写),而非无人追踪的延后清理。三十九个技术债标记已全部消除,由它们等待的模板所解决。
|
||||
每篇 RFC 现在需要略多一些结构,而必须包含 `Alternatives considered` 章节是刻意的摩擦:一个没有记录被否决方案的决策,会招致 RFC 本来就是为了防止的重新争论。格式定义前的 RFC 如果替代方案无法重建,则永久携带祖父条款注释,这是记录上的诚实缺口,而非编造的理由。`doc-sync` 增加一道门禁,将 RFC 在生命周期文件夹之间迁移现在是迁移时的实际工作(即迁移本就欠下的正文改写),而非无人追踪的延后清理。三十九个债务标记已全部消除,由它们等待的模板所解决。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-06-export-surface-jsdoc-gate.md: fd255cc6212d7f6f919ad23f999fa68b01478a73
|
||||
2026-07-06-export-surface-jsdoc-gate.zh.md: 02a5c2682095c66818a51cc14fd565111b7e6e80
|
||||
2026-07-06-export-surface-jsdoc-gate.zh.md: 796b5bb8f3a2a890986c73511bb63e167c831a5f
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
# RFC:导出表面 JSDoc 门禁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-06-export-surface-jsdoc-gate.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[cordis JSDoc 完整性门禁](2026-07-04-cordis-jsdoc-completeness-gate.md)使 cordis 表面上的未文档化参数和返回值不再可能——`interface Events` 成员与 `ctx.<key>` 服务类——但那只是插件作者所导入内容的一小部分。AGENTS.md 中「每个导出(及非显而易见的方法)都应有 JSDoc 说明语义」这条规则在其他地方仍然只能靠评审以行文方式检查,而且没有任何机制要求普通导出函数带 `@param`/`@returns`。采纳时的一次调查发现 34 个包中有 203 个文档不完整的模块级导出:seam 相关辅助函数(`runBash`、`readForEdit`、`htmlToMarkdown`)、格式编解码器、整个未文档化的接口和类型别名——正是 IDE 消费方悬停时看到的那些名字。
|
||||
[Cordis JSDoc 完整性门禁](2026-07-04-cordis-jsdoc-completeness-gate.md)使得 Cordis 表面上的参数和返回值不可能缺少文档——`interface Events` 成员和 `ctx.<key>` 服务类——但这只是插件作者所导入内容的一小部分。AGENTS.md 中的规则「每个导出(以及非显而易见的方法)都必须有解释语义的 JSDoc」在其他地方只能靠评审以行文方式检查,而且没有任何机制要求普通导出函数带 `@param`/`@returns`。采纳时的一次调查发现 34 个包(package)中有 203 个文档不完整的模块级导出:seam 相关辅助函数(`runBash`、`readForEdit`、`htmlToMarkdown`)、格式编解码器、完全无文档的接口和类型别名——恰恰是 IDE 消费方悬停查看的那些名称。
|
||||
|
||||
## 决策
|
||||
|
||||
新增门禁 `scripts/verify-export-jsdoc.ts`(`pnpm run verify-export-jsdoc`,接入 doc-sync,与 `verify-cordis-catalog` 并列),遍历每个 `packages/<group>/<pkg>/src/` 目录树下所有模块级导出名。解析与检查辅助函数从 `gen-cordis-catalog.ts` 移入共享的 `scripts/jsdoc.ts`,因此「已文档化」在两个表面上含义一致:描述文本在第一个块标签处截止,每个可检查参数需要非空 `@param`,非 void 的**已标注**返回值需要非空 `@returns`,过时的 `@param` 报错,违规汇总为一份报告。
|
||||
新增门禁 `scripts/verify-export-jsdoc.ts`(`pnpm run verify-export-jsdoc`,接入 `doc-sync`(文档同步门禁),与 `verify-cordis-catalog` 并列),遍历每个 `packages/<group>/<pkg>/src/` 目录树下的所有模块级导出名称。解析与检查辅助函数从 `gen-cordis-catalog.ts` 移入共享的 `scripts/jsdoc.ts`,使得「已文档化」在两个表面上含义一致:描述性文字在第一个块标签处截止、每个可检查参数需要非空 `@param`、非 void 且有显式标注的返回值需要非空 `@returns`、过时的 `@param` 报错,违规项汇总为一份报告。
|
||||
|
||||
按声明类型划分的契约:
|
||||
|
||||
- 每个导出名都需要带有非空描述文本的 JSDoc。
|
||||
- 函数类导出(函数声明;以函数初始化器或行内可调用标注的 const;非标识符的函数默认导出)遵循完整的函数契约,分类前会剥离包装表达式(括号、`as`/`satisfies` 转型、非空断言)。如果 const 的声明器标注了一个**命名**类型(`export const f: Handler = …`),则签名契约推迟到该类型自身的声明,`@returns` 可选;行内 `(x: T) => U` 标注或单调用签名字面量即为表面签名本身,适用完整契约;而字面量中混合了调用/构造签名与其他成员的情况则直接拒绝(没有单一签名可供标签对照——请提取命名类型)。
|
||||
- 导出类需要类级别的描述文本;公开方法(包括静态方法——可通过导出名访问)遵循函数契约;公开属性和访问器需要描述文本(get/set 对由 getter 覆盖)。重载实现免检——由签名承载文档。
|
||||
- 导出的接口、类型别名和枚举需要声明级别的描述文本;成员级别的强制有意推迟(承载关键成员契约的 seam 服务类已在 cordis 门禁下)。
|
||||
- 导出的命名空间递归检查(在 ambient `declare` 命名空间内,每个成员隐式导出);命名空间本身仅在不与同名已文档化声明合并时才需要描述文本(Config 命名空间惯用法只需文档化插件一次)。
|
||||
- `declare module` / `declare global` 体和 `export … from` 再导出语句被跳过:augmentation 不是包的导出,再导出的定义在其定义处检查。`export import X = N.member` 别名文档化**自身**——其目标可能是遍历不会访问的非导出命名空间成员——且仅支持纯描述文本的目标类型:可调用、类或命名空间目标携带别名描述文本无法承载的签名/成员契约,门禁拒绝此类情况并要求直接导出该声明。
|
||||
- 其余一切按**封闭**原则失败:`export =` 直接拒绝;基类从未命名的参数即使作为绑定模式仍保留 `@param` 义务;调度未识别的导出语句类型本身即为违规——没有任何导出形式能因遗漏而免检。
|
||||
- 每个导出名称都需要带有非空描述文字的 JSDoc。
|
||||
- 函数类导出(函数声明;初始化器为函数或带有内联可调用标注的 const;非标识符的函数默认导出)遵循完整的函数契约,分类前会剥离包装表达式(括号、`as`/`satisfies` 类型断言、非空断言)。如果 const 声明器标注了一个具名类型(`export const f: Handler = …`),签名契约推迟到该类型自身的声明处,`@returns` 保持可选;内联的 `(x: T) => U` 标注或单调用签名字面量本身就是表面签名,适用完整契约;而混合了调用/构造签名与其他成员的字面量则直接拒绝(没有单一签名可供标签对照——请提取具名类型)。
|
||||
- 导出类需要类级别的描述文字;公开方法(包括静态方法——可通过导出名称访问)遵循函数契约;公开属性和访问器需要描述文字(get/set 对由 getter 覆盖)。重载实现体免检——签名承载文档。
|
||||
- 导出接口、类型别名和枚举需要声明级别的描述文字;成员级别的强制有意推迟(承载关键成员契约的 seam 服务类已在 Cordis 门禁之下)。
|
||||
- 导出命名空间递归检查(在 ambient `declare` 命名空间内,每个成员隐式导出);命名空间本身仅在不与同名的已文档化声明合并时才需要描述文字(Config-namespace 惯用法只需文档化插件一次)。
|
||||
- `declare module`/`declare global` 体和 `export … from` 重导出语句被跳过:augmentation 不是包的导出,重导出的定义在其定义处检查。`export import X = N.member` 别名需要文档化**自身**——其目标可能是遍历不会访问的非导出命名空间成员——且门禁仅支持纯描述文字的目标类型:可调用、类或命名空间目标携带别名描述文字无法承载的签名/成员契约,门禁会拒绝并要求直接导出该声明。
|
||||
- 其余情况按封闭原则失败:`export =` 直接拒绝;基类从未命名的参数即使作为绑定模式仍需 `@param`;dispatch 不识别的导出语句类型本身就是违规——没有任何导出形式能因遗漏而免检。
|
||||
|
||||
三类豁免避免门禁要求样板代码,精神与 cordis 门禁的 `this`/`next` 豁免一致(对已豁免的名字主动写文档是允许的;只有缺失才不被检查):
|
||||
三类豁免避免门禁要求样板代码,精神与 Cordis 门禁的 `this`/`next` 豁免一致(为已豁免的名称编写文档是允许的;只有缺失才不被检查):
|
||||
|
||||
- **继承成员。**重写从基类声明继承文档。新增的公开表面仍需文档:新增参数、将 protected 成员公开重写、或在 void 基类之上给出具体返回值。继承查找与推断返回值分类是门禁唯一需要类型检查器的工作;其他检查使用 AST。
|
||||
- **插件协议槽位。**顶层 `name` / `inject` / `reusable` / `Config` const 与 `apply` 入口,以及插件类上作为静态成员的相同槽位,属于框架协议:其形状由 cordis 固定,模块文档注释加 `interface Config` 承载插件的真实语义。
|
||||
- **构造函数**,与 cordis 门禁一致:插件类由框架构造,类文档承载全部说明。
|
||||
- **继承成员。** 重写从其基类声明继承文档。新增的公开表面仍需文档:新增参数、将 protected 成员公开重写、或在 void 基类之上返回具体类型。继承查找和推断返回值分类是门禁唯一需要类型检查器的工作;其他检查使用 AST。
|
||||
- **插件协议槽位。** 顶层的 `name`/`inject`/`reusable`/`Config` 常量和 `apply` 入口,以及插件类上的同名静态成员,属于框架协议:其形状由 Cordis 固定,模块文档注释加 `interface Config` 承载插件的真实语义。
|
||||
- **构造函数**,与 Cordis 门禁一致:插件类由框架构造,类文档承载全部说明。
|
||||
|
||||
`collectExportJsdocViolations()` 返回违规列表(CLI 在非空时以 exit 1 退出),因此 `packages/core/agent/tests/verify-export-jsdoc.spec.ts` 中的负路径测试直接对发现结果断言,通过 fixture 包驱动每一种拒绝和每一种豁免。
|
||||
`collectExportJsdocViolations()` 返回违规列表(CLI 在非空时以 1 退出),因此 `packages/core/agent/tests/verify-export-jsdoc.spec.ts` 中的负路径测试直接断言发现项,通过 fixture(测试前置数据)包驱动每一种拒绝和每一种豁免。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **eslint-plugin-jsdoc**(`require-jsdoc`/`require-param`/`require-returns`):覆盖了机械核心,但无法表达本仓库的契约:继承成员豁免需要跨包类型解析,协议槽位和命名空间合并惯用法是 cordis 特有的,而完整性语义(标签前描述文本、过时标签报错、聚合报告)已在 `scripts/jsdoc.ts` 中与 catalog 生成器共享一处。两套微妙不同的「已文档化」定义正是本仓库「一处为家」规则要防止的失败模式。
|
||||
- **扩展 `gen-cordis-catalog.ts`**:catalog 生成器渲染一个精选表面并门禁其新鲜度(freshness);仓库级遍历没有 catalog 可渲染。共享辅助函数但保持遍历分离,使每个门禁的职责清晰可读。
|
||||
- **强制接口/类型别名的成员文档**:推迟。这会将检查表面扩大到大量自描述字段,而承载关键成员契约的 seam 类已在门禁下。如果评审中出现成员文档漂移再重新考虑。
|
||||
- **eslint-plugin-jsdoc**(`require-jsdoc`/`require-param`/`require-returns`):覆盖了机械核心,但无法表达本仓库的契约。继承成员豁免需要跨包的类型解析,协议槽位和命名空间合并惯用法是 Cordis 特有的,而完整性语义(标签前描述文字、过时标签报错、汇总报告)已在 `scripts/jsdoc.ts` 中与 catalog 生成器共享。两套微妙不同的「已文档化」定义,正是本仓库「单一归属」规则所要防止的失败模式。
|
||||
- **扩展 `gen-cordis-catalog.ts`**:catalog 生成器渲染一个精选表面并守卫其新鲜度;仓库级遍历没有 catalog 可渲染。共享辅助函数、保持遍历独立,使每个门禁的职责清晰可读。
|
||||
- **强制接口/类型别名的成员文档**:推迟。这会使检查表面成倍增长,而这些成员大多是自描述的字段;承载关键成员契约的 seam 服务类已有门禁。如果评审中出现成员文档漂移再重新考虑。
|
||||
|
||||
## 后果
|
||||
|
||||
- 新导出不能在无文档的情况下落地:`verify-export-jsdoc` 使 doc-sync 失败,而 pre-push 和 CI 已经运行 doc-sync。采纳时发现的 203 处缺口在同一个变更中补齐,因此门禁以绿色状态落地。
|
||||
- 导出函数必须标注返回类型(采纳时已全面覆盖,现在成为承载性要求),且在 `@param` 需要命名的地方使用标识符参数。
|
||||
- seam 文档是权威的:实现从继承链继承文档,值得保留在实现上的行为说明是补充,而非必需。
|
||||
- 门禁构建一个 `ts.Program`(约 6 秒)——唯一需要类型解析的文档门禁;在已经编译文档片段的 doc-sync 中可以接受。
|
||||
- 协议槽位名在模块顶层按约定保留;一个碰巧名为 `apply` 或 `Config` 的非协议导出会免检——已接受,记录于此。
|
||||
- 新增导出不能在无文档的情况下合入:`verify-export-jsdoc` 使 `doc-sync` 失败,而 pre-push 和 CI 已运行 `doc-sync`。采纳时发现的 203 处缺口在同一个变更中补齐,门禁以绿色状态落地。
|
||||
- 导出函数必须标注返回类型(采纳时已全面满足,现在成为门禁依赖),并在 `@param` 需要命名参数时使用标识符参数。
|
||||
- seam 文档是权威的:实现从其继承链继承文档,值得保留在实现上的行为说明是补充,而非必需。
|
||||
- 门禁构建一个 `ts.Program`(约 6 秒)——唯一需要类型解析的文档门禁;在已编译文档片段的 `doc-sync` 内可以接受。
|
||||
- 协议槽位名称按约定保留在模块顶层;一个恰好命名为 `apply` 或 `Config` 的非协议导出将不被检查——已接受,记录于此。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-06-generated-config-catalog.md: 50ba0dc70b0d8ea54a4f93911f3a087806774626
|
||||
2026-07-06-generated-config-catalog.zh.md: 2dd0cbe5303f08aa2f3300c6f8613a7823c2bc78
|
||||
2026-07-06-generated-config-catalog.zh.md: 87a861bab394ec268fb3c870e848db37fa4d6fcf
|
||||
|
||||
@@ -6,35 +6,35 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
仓库此前没有以源码为后盾的插件配置参考。各 package 的 README 对字段的记录方式不一致,没有列举哪些包可被加载,也没有校验运行时 schema 是否与声明的配置类型一致。
|
||||
仓库此前没有以源码为后盾的插件配置参考。各 package 的 README 对字段的记录方式不一致,未列举哪些包可被加载,也未校验运行时 schema 与声明的配置类型是否一致。
|
||||
|
||||
## 决策
|
||||
|
||||
`scripts/gen-config-catalog.ts` 从每个插件声明的配置类型与 JSDoc 生成 [docs/config-catalog.md](../../../config-catalog.md),包含注入要求、引用类型链接和源码指针。package 内部类型被传递性地包含;workspace 和外部类型以链接或名称引用。确定性的 `--write` 和 `--check` 模式使提交到仓库的页面成为一个生成产物(artifact)。
|
||||
`scripts/gen-config-catalog.ts` 从每个插件声明的配置类型与 JSDoc 生成 [docs/config-catalog.md](../../../config-catalog.md),包含注入要求、引用类型链接和源码指针。包内局部类型被传递性地包含;workspace 和外部类型以链接或名称形式引用。确定性的 `--write` 和 `--check` 模式使提交到仓库的页面成为一个生成产物。
|
||||
|
||||
此处采用纯 AST 生成是正确的,原因与事件/服务目录相同,也与工具目录不同:配置类型是静态声明,仓库中每个 schemastery schema 都是静态的 `z.object`/`z.intersect` 字面量,因此源码就是全部真相——配置表面没有任何部分是运行时组合的。
|
||||
此处采用纯 AST 生成是正确的,原因与 events/services catalog 相同,而与 tool catalog 不同:配置类型是静态声明,仓库中每个 schemastery schema 都是静态的 `z.object`/`z.intersect` 字面量,因此源码即全部真相——配置表面没有任何部分是运行时组合的。
|
||||
|
||||
具体选择:
|
||||
|
||||
- **配置类型取自第二参数类型。** 目录记录的是 `apply(ctx, config)` / 服务构造函数 `(ctx, config)` 的声明参数类型——即 Cordis 实际传入的值——而非按命名约定定位的 `Config` 导出。这使得遍历是全量的:无论接口名为 `AcpConfig` 还是 `BasicCompactConfig`,无论类型声明在兄弟文件中,还是插件根本没有校验 schema,都能正常工作。
|
||||
- **分类是全量的。** 每个 `packages/<group>/<pkg>` 条目都会被解析(镜像 Loader 的 `unwrapExports`(`exports.default ?? exports`)),归入以下之一:可配置插件、无配置插件、抽象 seam 类或库——各自渲染在独立章节中——无法归类的条目会硬错误。新 package 不可能被静默地遗漏。
|
||||
- **逐字段 JSDoc 强制要求。** 粘贴的声明中每个属性(包括嵌套的类型字面量)都需要非空的 JSDoc 描述,否则生成失败。粘贴本身就是文档,因此这与事件目录通过 `@mode` 施加的强制函数相同:源码文档不足时门禁失败,而非产出一份单薄的目录。
|
||||
- **Schema 键与声明类型交叉检查。** 生成器通过本地和 workspace 类型解析嵌套的对象与数组路径。确定缺失的路径会失败;无法枚举的外部或动态形状则跳过。检查有意设计为单向的,因为声明类型可能包含从 loader 配置中排除的运行时专用字段。
|
||||
- **配置类型是第二参数的类型。** catalog 记录的是 `apply(ctx, config)` / 服务构造函数 `(ctx, config)` 的声明参数类型——即 Cordis 实际传入的值——而非按命名约定定位的 `Config` 导出。这使得遍历是全量的:无论接口叫 `AcpConfig` 还是 `BasicCompactConfig`,无论类型声明在兄弟文件中,还是插件完全没有验证 schema,都能正常工作。
|
||||
- **分类是全量的。** 每个 `packages/<group>/<pkg>` 条目都会被解析(镜像 Loader 的 `unwrapExports`:`exports.default ?? exports`),归入可配置插件、无配置插件、抽象 seam 类或库之一——各自渲染在独立小节中——无法归类的条目直接报错。新 package 不可能被悄悄遗漏。
|
||||
- **逐字段 JSDoc 强制要求。** 粘贴的声明中每个属性(包括嵌套的类型字面量)都需要非空的 JSDoc 描述,否则生成失败。粘贴本身就是文档,因此这与 events catalog 通过 `@mode` 施加的强制函数相同:源码文档过于单薄时门禁报错,而非产出单薄的 catalog。
|
||||
- **Schema 键与声明类型做比对。** 生成器通过局部和 workspace 类型解析嵌套的对象与数组路径。确定缺失的路径报错;无法枚举的外部或动态形状则跳过。比对有意设计为单向的,因为声明类型可能包含被排除在 loader 配置之外的运行时专用字段。
|
||||
- **专用围栏。** 粘贴的声明使用 ` ```ts config-catalog ` 信息字符串,`doc-typecheck` 会跳过它(引用了导入类型的孤立声明无法独立编译),并将其排除在 opt-out 比例之外——与 `cordis-catalog` 和 `persistence-catalog` 围栏的处理方式相同。
|
||||
- **单文件 `docs/config-catalog.md`**,而非一个单文件目录:该页面服务于单一受众(`cordis.yml` 的编写者),只有一个维度,不同于 `cordis-catalog/`(它包含两个并列页面)。
|
||||
- **单文件 `docs/config-catalog.md`**,而非一个单文件目录:该页面面向单一受众(`cordis.yml` 的编写者),只有一个维度,不同于 `cordis-catalog/`(其中包含两个并列页面)。
|
||||
|
||||
各 package README 的 `## Config` 章节保留。这种重叠是有意接受的:README 是精心策划的逐 package 契约(部署上下文中的配置语义,连同限制与扩展点),目录则是穷举式的生成枚举。因为目录是生成的,二者之间的分歧说明 README 有误,修复方式是编辑 README——目录不会漂移。
|
||||
各 package README 中的 `## Config` 小节保留。重叠是有意接受的:README 是经过策划的逐包契约(在部署上下文中描述配置语义,连同限制与扩展点),catalog 则是穷举式的生成枚举。由于 catalog 是生成的,二者不一致时说明 README 有误,修复方式是编辑 README——catalog 不会漂移。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **合成式逐字段渲染**:为每个字段生成一个项目符号列表、表格或带注释的 YAML 片段,由解析后的 JSDoc 加 schema 元数据组装。否决,改用逐字粘贴:带 JSDoc 的接口本身就是以其原始形式撰写的契约,合成渲染器会重新格式化它不拥有的行文,增加一层可能歪曲原意的渲染。
|
||||
- **运行时启动 + schema 内省(如工具目录的做法)**:否决。此处没有任何内容是运行时组合的,而且 schema 本身对配置表面的文档化不足(行文记录的默认值、运行时专用字段、完全没有 schema 的插件)。启动只会增加脆弱性而不增加真相。
|
||||
- **双向 schema/接口相等性检查**:否决,改用子集检查。声明类型合理地包含 schema 拒绝从配置接受的成员(运行时专用的 seam)。
|
||||
- **在同一变更中废弃 README 的 `## Config` 章节**:否决。接受的重叠使逐 package 契约在原地可读,而一次清扫需要先把每个 README 的额外事实折入字段 JSDoc——这是可分离的工作,目录不依赖它。
|
||||
- **合成式逐字段渲染**:为每个字段生成项目符号列表、表格或带注释的 YAML 片段,从解析的 JSDoc 加 schema 元数据组装。否决,改用逐字粘贴:接口连同其 JSDoc 本身就是以原始形式撰写的契约,合成渲染器会重新格式化它不拥有的行文,增加一个可能歪曲原意的渲染层。
|
||||
- **运行时启动 + schema 内省(如 tool catalog 所做的那样)**:否决。此处没有任何内容是运行时组合的,且 schema 本身对配置表面的文档化不足(以行文记录的默认值、运行时专用字段、完全没有 schema 的插件)。启动只会增加脆弱性而不增加真相。
|
||||
- **双向 schema/接口等价检查**:否决,改用子集检查。声明类型合理地包含 schema 拒绝从配置接受的成员(运行时专用 seam)。
|
||||
- **在同一变更中废除 README `## Config` 小节**:否决。保留可接受的重叠使逐包契约在原处可读,而清理工作需要先把每个 README 的额外事实折入字段 JSDoc——这是可分离的工作,catalog 不依赖它。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目录不会漂移:源码变化而提交的文件未反映时,`verify-config-catalog` 在 pre-push 和 CI 中失败。未记录的配置字段、无法解析的引用类型名称、或 schema 键在配置类型中缺失,都会导致生成器直接报错。
|
||||
- 配置行文现在在声明处有了强制函数:编写新的配置字段意味着编写其 JSDoc,而 JSDoc 会逐字成为目录条目。
|
||||
- 生成器对无法静态遍历的形状硬错误——别名化的 package 内部配置导入、非 `object`/`intersect` 组合构建的 schema、未列入的全局类型名。引入这样的形状就必须同时教会生成器(否则该形状不能进入仓库),这正是设计意图:目录始终是全部真相。
|
||||
- `gen-cordis-catalog.ts` 导出其 JSDoc/指针辅助函数与 `LINK_MAP` 供复用,因此两个目录以相同方式交叉链接类型,新增一条 link-map 条目同时服务于两者。
|
||||
- catalog 不会漂移:源码变更而提交的文件未反映时,`verify-config-catalog` 在 pre-push 和 CI 中报错。未文档化的配置字段、无法解析的引用类型名、或 schema 键在配置类型中缺失,都会直接导致生成器报错。
|
||||
- 配置行文现在有了声明处的强制函数:编写新配置字段意味着编写其 JSDoc,而该 JSDoc 将逐字成为 catalog 条目。
|
||||
- 生成器对无法静态遍历的形状直接报错——别名化的包内配置导入、非 `object`/`intersect` 组合构建的 schema、未列入的全局类型名。引入此类形状时必须同时教会生成器(否则该形状不能进入仓库),这正是设计意图:catalog 始终是全部真相。
|
||||
- `gen-cordis-catalog.ts` 导出其 JSDoc/指针辅助函数与 `LINK_MAP` 供复用,因此两个 catalog 以相同方式交叉链接类型,新增一条 link-map 条目同时服务于两者。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-06-node-engine-floor.md: 561b6b4a124b6eaa8e2ba0756a835e35519b30b8
|
||||
2026-07-06-node-engine-floor.zh.md: c6ace7a1296b5e3049ff4ef55f29b689fce7f4ff
|
||||
2026-07-06-node-engine-floor.zh.md: 21af2da919754b1ae4667b46ef2f65c14a279b49
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# RFC:将 Node LTS 引擎下限提升至 22.19
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-06-node-engine-floor.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
根 `engines.node` 范围中的 Node 22 分支是对已安装工作区的契约,而不仅仅是 harness 源码直接调用的运行时 API 的契约。该分支的下限不得低于工作区在该分支上安装的依赖所声明的 package `engines.node`;否则 `pnpm install --engine-strict` 会在一个被宣传的 LTS 版本上失败,而非严格模式的安装则会在依赖所支持的运行时范围之外运行。
|
||||
根 `engines.node` 范围中的 Node 22 分支是对已安装工作区的契约,而不仅仅是 harness 源码直接调用的运行时 API 的契约。它不得低于工作区在该分支上安装的依赖所声明的 package `engines.node`;否则 `pnpm install --engine-strict` 会在一个已宣传的 LTS 版本上失败,而非严格模式的安装则会在依赖所支持的运行时范围之外运行。
|
||||
|
||||
## 决策
|
||||
|
||||
将 `engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 keyless CI 兼容性矩阵中测试 `['22.19', 24, 26]`。每个矩阵分支都运行 TypeScript 类型检查加一次 keyless 的源码模式 worker 冒烟测试,因此引擎下限同时通过完整的源码类型检查和真实的未构建运行时路径得到验证。真实 API 的 e2e 工作流保持在 Node 24 上运行,因为它验证的是 API 集成而非运行时下限。
|
||||
将 `engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 keyless CI 兼容性矩阵中测试 `['22.19', 24, 26]`。每条矩阵分支都运行 TypeScript 类型检查加一次 keyless 的源码模式 worker 冒烟测试,因此引擎下限通过完整的源码类型检查和真实的未构建运行时路径两条路径得到验证。真实 API 的 e2e 工作流保持在 Node 24 上,因为它验证的是 API 集成而非运行时下限。
|
||||
|
||||
两项 Node 特性决定了源码运行时的下限:
|
||||
两个 Node 特性决定了源码运行时的门槛:
|
||||
|
||||
- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` flag 要求;在此之前,导入它会在加载时抛出异常。
|
||||
- **原生 TypeScript 类型剥离**:`packages/examples/stdio-demo/tests/built-bin.e2e.ts` 冒烟测试在纯 `node`(无 tsx)下启动已发布的 `lib/bin.js`,并加载示例的 `.ts` 插件(`mock-llm.ts`、`echo-tool.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;在此之前需要 `--experimental-strip-types`。
|
||||
- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。
|
||||
- **原生 TypeScript 类型剥离**:`packages/examples/stdio-demo/tests/built-bin.e2e.ts` 冒烟测试在纯 `node`(不用 tsx)下启动已发布的 `lib/bin.js`,并加载示例的 `.ts` 插件(`mock-llm.ts`、`echo-tool.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;在此之前需要 `--experimental-strip-types`。
|
||||
|
||||
这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步抬高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的 package 声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不连续范围完全排除 Node 23:Node 23.0–23.5 仍有至少一项源码特性需要 flag,而 23 线是非 LTS/已 EOL,宣传 `>=23.6` 只会增加一个已死的发布线和一个不应被任何部署使用的 CI 分支。
|
||||
这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的 package 声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。
|
||||
|
||||
`@types/node` 继续固定在 22.x 线(`^22.20.0`),以匹配 LTS 支持线:如果使用了 Node 23+/24+/25+ 才有的 API,`tsc` 会在所有机器和类型检查门禁中报错,而不是编译通过后存活到只有下限矩阵分支才能捕获的运行时失败。整棵树目前在 Node 22 类型表面上类型检查全部通过,因此这个固定没有代价。
|
||||
`@types/node` 继续固定在 22.x 线(`^22.20.0`),以匹配 LTS 支持线:使用 Node 23+/24+/25+ 的 API 会在所有机器和类型检查门禁中导致 `tsc` 失败,而不是编译通过、直到仅下限矩阵分支才能捕获的运行时错误才暴露。目前整个代码树在 Node 22 类型表面上类型检查全部通过,因此这一固定没有任何代价。
|
||||
|
||||
## 后果
|
||||
|
||||
- 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。
|
||||
- CI 用 Node 22.19 直接验证 Node 22 LTS 下限,Node 24 分支保持 `node: 24`,Node 26 用于下一个偶数线;每个分支都对源码图做类型检查,并实际启动未构建的工作流 worker。
|
||||
- built-bin 冒烟测试不需要版本条件 flag:在 22.19 上类型剥离已是默认行为,因此测试保持其文档记录的纯 `node lib/bin.js` 路径。
|
||||
- 未来如有依赖或源码 API 抬高运行时下限,必须在同一个变更中同步修改 `engines.node`、兼容性矩阵与本 RFC。
|
||||
- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,Node 24 分支保持 `node: 24`,Node 26 用于下一个偶数线;每条分支都对源码图执行类型检查,并实际启动未构建的工作流 worker。
|
||||
- built-bin 冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此测试保持其文档所述的纯 `node lib/bin.js` 路径。
|
||||
- 未来如果有依赖或源码 API 提高运行时下限,必须在同一个变更中同步修改 `engines.node`、兼容性矩阵和本 RFC。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **保持 `^22.18.0 || >=24.0.0`。** 否决:它宣传的 LTS 版本低于 Pi 适配器依赖的下限。`@earendil-works/pi-ai@0.79.3` 要求 `>=22.19.0`。
|
||||
- **降级或固定 `@earendil-works/pi-ai` 以保留 22.18 的宣传范围。** 否决:当前的 Pi 适配器依赖是工作区的预期组成部分,且 22.19 仍在 Node 22 LTS 线内。
|
||||
- **下限设为 `>=22.13`(`node:sqlite` 边界),在 22.13–22.17 的 built-bin 冒烟测试中加 `--experimental-strip-types`。** 否决:为一个窄范围增加版本条件测试 flag,并将对实验性 flag 的依赖伪装成正式支持。Pi 适配器依赖已经要求更高的 LTS 下限。
|
||||
- **开放式 `>=22.19`。** 否决:它宣传支持 Node 23.0–23.5,而在这些版本上 `node:sqlite`(直到 23.4)或类型剥离(直到 23.6)仍需 flag。
|
||||
- **包含 Node 23.6+(`^22.19.0 || >=23.6.0`)。** 否决:23.6+ 确实能无 flag 运行两项源码特性,但 Node 23 已 end-of-life;宣传一个已死的发布线只会增加一个范围项和一个 CI 分支,用于一个不应被任何部署使用的运行时。
|
||||
- **矩阵用 `[22, 24, 26]` 而非固定 `22.19`。** 否决:浮动的主版本号条目会随时间上漂,悄然不再验证所声明的 LTS 下限。
|
||||
- **让 `@types/node` 超前于下限(`^25`)。** 否决:类型定义超前于运行时下限会让仅 Node 24/25 才有的 API 编译通过,仅在 22.x 上运行时才失败。将 `@types/node` 固定在 22.x 线上,会把这种情况变成所有环境下的编译错误。
|
||||
- **降级或固定 `@earendil-works/pi-ai` 以保留 22.18 的宣传范围。** 否决:当前 Pi 适配器依赖是预期工作区的一部分,且 22.19 仍在 Node 22 LTS 线内。
|
||||
- **下限 `>=22.13`(`node:sqlite` 边界)加上在 22.13–22.17 的 built-bin 冒烟测试中使用 `--experimental-strip-types`。** 否决:它为一个狭窄范围增加了版本条件测试标志,并将实验性标志依赖包装为正式支持。Pi 适配器依赖已经要求更高的 LTS 下限。
|
||||
- **开放式 `>=22.19`。** 否决:它宣传支持 Node 23.0–23.5,而在这些版本上 `node:sqlite`(直到 23.4)或类型剥离(直到 23.6)仍需标志。
|
||||
- **包含 Node 23.6+(`^22.19.0 || >=23.6.0`)。** 否决:23.6+ 确实能无标志运行两个源码特性,但 Node 23 已 end-of-life;宣传一条已终止的发布线会增加一个范围项和一条 CI 分支,而没有任何部署应当使用该运行时。
|
||||
- **矩阵 `[22, 24, 26]` 而非固定 `22.19`。** 否决:浮动的主版本号条目会随时间上漂,悄然不再验证所声明的 LTS 下限。
|
||||
- **将 `@types/node` 保持在下限之前(`^25`)。** 否决:类型定义超前于运行时下限会让仅 Node 24/25 才有的 API 编译通过,仅在 22.x 上运行时才失败。将 `@types/node` 固定在 22.x 线上可将此类问题转化为所有环境下的编译错误。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-06-parallel-github-ci-gates.md: 890adf58ac8a20a39806aa028d035cb253d5a4f1
|
||||
2026-07-06-parallel-github-ci-gates.zh.md: 02502b1005a1f8e6f9f539878c792a9f0585e926
|
||||
2026-07-06-parallel-github-ci-gates.zh.md: 47562ed6a83b650a3275b4045c39c4de6d2ecac6
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# RFC:并行 GitHub CI 门禁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-06-parallel-github-ci-gates.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
keyless GitHub CI 门禁大多彼此正交:类型检查、lint、文档新鲜度、覆盖率、快照回放、构建、包发布卫生检查、demo 冒烟测试和 built-bin 冒烟测试各自因不同原因失败,且不需要彼此的运行时状态。将它们串成一条有序命令链,工作流的挂钟时间等于所有门禁之和;而将每个叶子门禁拆成独立的 GitHub job,则会重复 checkout、Node 搭建、pnpm restore 和 install 工作,直到编排开销本身成为瓶颈。
|
||||
keyless GitHub CI 门禁大多相互正交:类型检查、lint、文档新鲜度、覆盖率、快照回放、构建、包发布卫生检查、demo 冒烟测试与 built-bin 冒烟测试各自因不同原因失败,彼此不需要对方的运行时状态。将它们串成一条有序命令链,工作流的挂钟时间等于所有门禁之和;而把每个叶子门禁拆成独立的 GitHub job,则会重复 checkout、Node 设置、pnpm restore 和 install 工作,直到编排开销本身成为瓶颈。
|
||||
|
||||
难点在于产物边界。`publint`、`verify-node-next-types` 和 built-bin 冒烟测试需要构建出的 `lib/` 输出,而大多数门禁只需要源码和依赖。盲目扇出要么让这些产物消费方在 `pnpm run build` 输出声明文件和 bundle 之前就开始执行,要么在每个依赖产物的 job 中重复构建。
|
||||
难点在于产物边界。`publint`、`verify-node-next-types` 和 built-bin 冒烟测试需要构建出的 `lib/` 输出,而大多数门禁只需要源码和依赖。盲目扇出要么让这些产物消费方在 `pnpm run build` 输出声明文件和 bundle 之前就开始竞跑,要么在每个依赖产物的 job 中重复构建。
|
||||
|
||||
## 决策
|
||||
|
||||
[CI](../../../../.github/workflows/ci.yml) 将 keyless 检查分组为若干宽粒度的主运行时 lane,外加一个兼容性矩阵。工作流文件拥有当前 lane 和运行时清单的定义权。
|
||||
|
||||
每个 lane 委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),后者以有界并发调度独立门禁,并为每个门禁打印一个可归因的结果块。产物消费方在各自 lane 内依赖一次 build;兼容性 job 则将类型检查与一次真实的未构建 worker 启动相结合,以覆盖运行时特定的 loader 行为。
|
||||
每个 lane 委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),该脚本以有界并发调度独立门禁,并为每个门禁打印一个可归因的结果块。产物消费方依赖其所在 lane 内的一次 build,而兼容性 job 将类型检查与一次真实的未构建 worker 启动结合,以覆盖运行时特定的 loader 行为。
|
||||
|
||||
生成的 `.sessions/` 日志和 `.doc-typecheck-*` 临时目录被 lint 忽略。聚合的本地 CI 模式仍在 lint 之后运行 demo 冒烟测试;而拆分后的 GitHub 静态 lane 可以直接运行 demo 冒烟测试,因为 lint 已隔离在自己的 lane 中。
|
||||
生成的 `.sessions/` 日志和 `.doc-typecheck-*` 临时目录被 lint 忽略。聚合的本地 CI 模式仍在 lint 之后运行 demo 冒烟测试,而拆分后的 GitHub static lane 可以直接运行 demo 冒烟测试,因为 lint 已隔离在自己的 lane 中。
|
||||
|
||||
构建输出在 Node 24 产物 lane 中只生成一次。产物消费方(`publint`、`verify-node-next-types` 和 built-bin 冒烟测试)声明对 `build` 的依赖,因此没有 upload/download 交接,消费方也不可能抢在声明文件或 bundle 之前执行。CI 覆盖率报告仅为文本格式,本地覆盖率则保留 HTML 报告。
|
||||
构建输出在 Node 24 的产物 lane 中只生成一次。产物消费方(`publint`、`verify-node-next-types` 和 built-bin 冒烟测试)声明对 `build` 的依赖,因此没有 upload/download 交接,消费方也不可能在声明文件或 bundle 就绪之前抢跑。CI 覆盖率报告仅输出文本,本地覆盖率则保留 HTML 报告。
|
||||
|
||||
两个工作流都缓存 pnpm store。真实 API 工作流使用共享的有界 Vitest 文件池,而非为每组测试单独开 job。
|
||||
两个工作流都缓存 pnpm store。真实 API 工作流使用共享的有界 Vitest 文件池,而非为每组测试单独开一个 job。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在 Node 矩阵中保留完整串行链**:最容易理解,但会重复执行不产生 Node 版本特定信号的仓库级门禁,且让每个 PR 等待所有门禁之和。
|
||||
- **每个门禁各开一个 GitHub job**:最大化 GitHub 可见的扇出,但产生过多 check,且对运行时间短于 runner 准备时间的门禁反复支付 setup/install 开销。
|
||||
- **将构建产物上传给依赖产物的 job**:在多 job 间保持正确性,但增加了 artifact upload/download 时间,且在产物消费方可以通过主 job 内的本地依赖运行时仍保持工作流过宽。
|
||||
- **并发运行 `typecheck` 和 `build`**:向调度器暴露更多工作,但两者都调用 `tsc -b`;在它们之间共享增量构建状态是一场不必要的竞争,换来的挂钟收益很小。
|
||||
- **使用无界的真实 API e2e 并行度**:否决。该套件包含大量真实模型/工具场景;worker 池需要一个显式的 `DSH_E2E_MAX_WORKERS` 上限,这样 CI 和本地运行都能扇出而不会把配额或资源问题隐藏在不稳定的限流失败背后。
|
||||
- **在 Node 矩阵中保留完整串行链**:最容易推理,但会重复执行不产生 Node 版本特定信号的仓库级门禁,且让每个 PR 等待所有门禁的总和。
|
||||
- **每个门禁作为独立 GitHub job 运行**:最大化 GitHub 可见的扇出,但产生过多 check,且对运行时间短于 runner 准备时间的门禁而言,重复的 setup/install 开销得不偿失。
|
||||
- **将构建产物上传给依赖产物的 job**:在多 job 间保持正确性,但增加了 artifact upload/download 时间,且当产物消费方可以在主 job 内通过本地依赖排序运行时,工作流仍然过宽。
|
||||
- **并发运行 `typecheck` 与 `build`**:向调度器暴露更多工作,但两个命令都调用 `tsc -b`;在它们之间共享增量构建状态是一场不必要的竞争,换来的挂钟收益很小。
|
||||
- **使用无界的真实 API e2e 并行度**:否决。该套件包含大量真实模型/工具场景;worker 池需要一个显式的 `DSH_E2E_MAX_WORKERS` 上限,使 CI 和本地运行都能扇出,同时不会把配额或资源问题隐藏在不稳定的限流失败背后。
|
||||
|
||||
## 后果
|
||||
|
||||
PR 反馈以少量 GitHub check 呈现,每个宽粒度 job 内部包含结构化的逐门禁日志块。这使 runner setup 开销可控、Actions UI 紧凑,代价是失去了每个叶子门禁各自独立的 status check。
|
||||
PR 反馈以少量 GitHub check 的形式呈现,每个宽粒度 job 内部包含结构化的逐门禁日志块。这将 runner 设置开销控制在有限范围内,并保持 Actions UI 紧凑,代价是失去了每个叶子门禁独立的状态标记。
|
||||
|
||||
宽粒度 lane 拆分比单一主 job 更频繁地重复 checkout、setup 和 install。这一 setup 开销是有意为之:在 GitHub 托管 runner 上,将 lint、覆盖率和快照回放放在同一个进程池中运行会严重超额占用 CPU,以至于单 job 的关键路径反而长于重复 setup 的方案。
|
||||
宽 lane 拆分比单一主 job 更频繁地重复 checkout、setup 和 install。这一设置开销是有意为之的:在 GitHub 托管 runner 上,将 lint、覆盖率和快照回放放在同一个进程池中运行会严重超额占用 CPU,以至于单 job 的关键路径比重复设置还要长。
|
||||
|
||||
这种拆分引入了一项维护义务:当 `package.json` 增删属于 CI 的门禁时,[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 需要相应增删叶子。这一义务是有意的,因为该 runner 是同一套门禁词汇的并行执行计划,而非独立的质量策略。
|
||||
这种拆分引入了一项维护义务:当 `package.json` 新增或移除一个应纳入 CI 的门禁时,[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 需要添加或删除对应的叶子。这一义务是有意为之的,因为该 runner 是同一套门禁词汇的并行执行计划,而非独立的质量策略。
|
||||
|
||||
兼容性信号窄于主 Node 24 信号。它证明源码图在每个宣称支持的运行时上能通过类型检查、且真实的未构建 workflow-worker 启动路径能正常执行,而不必重复文档、覆盖率、发布卫生、快照回放和其他不因 Node 版本而异的冒烟检查。
|
||||
兼容性信号比主 Node 24 信号更窄。它证明源码图在每个声明支持的运行时上都能通过类型检查,且真实的未构建 workflow-worker 启动路径能够执行,而不必重复文档、覆盖率、发布、快照回放以及那些不因 Node 版本而异的无关冒烟测试。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-06-parallel-pre-push-gates.md: 8a8813f2f3f6726ab2ab028757406d366ceb8b6d
|
||||
2026-07-06-parallel-pre-push-gates.zh.md: 36207e522982990c193f9fe909faf380de53bca6
|
||||
2026-07-06-parallel-pre-push-gates.zh.md: 6ee3a8003853092d75cda9908bfae13ee0d4c7a2
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
# RFC:并行 pre-push 门禁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-06-parallel-pre-push-gates.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
pre-push 钩子是分支离开本地机器前的最后一道检查点,因此它的挂钟时间直接影响贡献者是否愿意保持启用并信任其信号。Lefthook 已经能并行运行顶层 job,但 `pnpm run hygiene` 和 `pnpm run doc-sync` 这类聚合 job 在单个 job 内部隐藏了长串的顺序执行链。因此钩子可以配置为并行,却仍在等待那些成员彼此独立的串行子命令。
|
||||
pre-push 钩子是分支离开本地机器前的最后一道检查点,因此它的挂钟时间直接影响贡献者是否愿意保持启用并信任其信号。Lefthook 已经能并行运行顶层 job,但 `pnpm run hygiene` 和 `pnpm run doc-sync` 等聚合 job 在单个 job 内部隐藏了长串的顺序执行链。钩子因此可能在配置上看似并行,实际仍在等待那些成员彼此独立却串行执行的子命令。
|
||||
|
||||
把这些成员直接展平到 `lefthook.yml` 只能解决本地钩子的问题。CI 有同样的调度问题,而在 YAML 中重复一份长长的叶子列表会让未来的脚本改动有两处可能漂移。
|
||||
将这些成员直接展平到 `lefthook.yml` 只能解决本地钩子的问题。CI 面临同样的调度问题,而在 YAML 中复制一长串叶子列表会让未来的脚本改动有两处可能漂移。
|
||||
|
||||
`publint` 在更低一层也有同样的形态。每个包独立地针对自身的 manifest 和构建产物做 lint,但运行器按顺序逐个遍历所有包。在本仓库中,这意味着一个包发布门禁消耗的时间与包数量成正比,尽管各检查之间并不共享可变状态。
|
||||
`publint` 在更低一层也有同样的形态。每个包(package)独立地根据自身 manifest(元数据清单)和构建产物做 lint,但 runner 按顺序遍历所有包。在本仓库中,这意味着一个包发布门禁的耗时与包的数量成正比,尽管各检查之间并不共享可变状态。
|
||||
|
||||
## 决策
|
||||
|
||||
[lefthook.yml](../../../../lefthook.yml) 保留一个名为 `full check` 的 pre-push job,运行 `pnpm run check:pre-push`。该包脚本委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),即 CI 使用的同一个有界调度器。
|
||||
[lefthook.yml](../../../../lefthook.yml) 保留一个名为 `full check` 的 pre-push job,运行 `pnpm run check:pre-push`。该 package 脚本委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),即 CI 使用的同一个有界调度器。
|
||||
|
||||
`pre-push` 模式展开为以下叶子门禁:单元测试套件、快照测试套件、构建、`hygiene` 成员、`doc-sync` 成员,以及 module-graph 新鲜度。叶子列表保持与包脚本相同的门禁词汇(包括 RFC 分类和 RFC 格式),运行器并发调度独立检查,并为每个门禁打印一个计时/输出块。
|
||||
`pre-push` 模式展开为以下叶子门禁:单元测试套件、快照测试套件、构建、`hygiene` 成员、`doc-sync` 成员,以及 module-graph 新鲜度。叶子列表保持与 package 脚本相同的门禁词汇,包括 RFC 分类和 RFC 格式,同时 runner 并发调度独立检查,并为每个门禁打印一个计时/输出块。
|
||||
|
||||
构建门禁使钩子在干净 worktree 上也能自给自足。`publint` 和 `verify-node-next-types` 等待构建产物,而仅依赖源码的门禁继续并行执行。
|
||||
构建门禁使钩子在干净的 worktree 上也能自足运行。`publint` 和 `verify-node-next-types` 等待构建产物就绪,而仅依赖源码的门禁继续并行执行。
|
||||
|
||||
[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包列表,并使用大小取自 `availableParallelism()` 的 worker 池运行 `publint`。`DSH_PUBLINT_CONCURRENCY` 可以为资源配置不同的本地机器和 CI runner 设置 worker 数量上限或提高上限。结果按包缓冲,并按确定性的包顺序打印,因此并行执行不会打乱每个包的日志块。
|
||||
[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包列表,并使用大小取自 `availableParallelism()` 的 worker 池运行 `publint`。`DSH_PUBLINT_CONCURRENCY` 可为资源配置不同的本地机器和 CI runner 设定或提高 worker 数量上限。结果按包缓冲,并以确定性的包顺序打印,因此并行执行不会打乱各包的日志块。
|
||||
|
||||
聚合包脚本仍然是临时本地运行的真源。调度器是对其成员门禁的并行执行计划,而非替代词汇。
|
||||
聚合 package 脚本仍然是临时本地运行的真源。调度器是对其成员门禁的并行执行计划,而非替代词汇。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在钩子中保留聚合的 `hygiene` 和 `doc-sync` job**:配置更简单,但 pre-push 的大部分挂钟时间仍然花在 lefthook 看不到也无法调度的串行命令链内部。
|
||||
- **为每个叶子门禁声明一个 lefthook job**:通过 lefthook 原生的 job 模型暴露并行性,但会让钩子文件承载一份 CI 无法复用的长成员列表。
|
||||
- **要求开发者在推送前手动构建**:省去一个钩子门禁,但会导致 `publint` 在干净 worktree 上失败,并把最后的本地检查点从可运行的检查降格为一项约定。
|
||||
- **在钩子中保留聚合的 `hygiene` 和 `doc-sync` job**:配置更简单,但 pre-push 的大部分挂钟时间仍然消耗在 lefthook 看不到也无法调度的串行命令链内部。
|
||||
- **为每个叶子门禁声明一个 lefthook job**:通过 lefthook 原生 job 模型暴露并行性,但会让钩子文件承载一长串成员列表,CI 无法复用。
|
||||
- **要求开发者在推送前手动构建**:可以省去一个钩子门禁,但会导致 `publint` 在干净 worktree 上失败,并把最后的本地检查点从可运行的检查降级为一种约定。
|
||||
- **在 shell 脚本中使用后台子命令**:能并行化工作,但会丢失 lefthook 的 job 名称、逐 job 计时和失败分组,且信号处理更难推理。
|
||||
- **为每个包声明一个 publint lefthook job**:暴露最大并行度,但会把钩子变成一份手工维护的包清单,恰好在新增包时漂移。
|
||||
- **以无界并发运行 publint**:仅在小型机器上以赌进程数、内存压力、包 tarball 创建和日志可读性为代价来最小化耗时。
|
||||
- **为每个包声明一个 publint lefthook job**:暴露最大并行度,但会让钩子变成一份手动维护的包清单,恰好在新增包时漂移。
|
||||
- **以无界并发运行 publint**:仅在小型机器上以赌注方式最小化耗时,代价是进程数、内存压力、包 tarball 创建和日志可读性的风险。
|
||||
|
||||
## 后果
|
||||
|
||||
钩子的关键路径变为最慢的那个实际门禁,而非隐藏门禁链的总和。Lefthook 报告一个 `full check` job,运行器在该 job 内部报告逐门禁计时,因此本地检查点慢时仍能指出主导耗时的那个门禁。
|
||||
钩子的关键路径变为最慢的单个真实门禁,而非隐藏门禁链的总和。Lefthook 报告一个 `full check` job,runner 在该 job 内部报告逐门禁计时,因此本地检查点偏慢时仍能指向主导耗时的那个门禁。
|
||||
|
||||
钩子文件保持简短,重复的成员列表集中在 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中,CI 和 pre-push 可以共享。代价是一个自定义调度器脚本(而非纯 lefthook 配置),外加本地 pre-push 路径中的一次构建。
|
||||
钩子文件保持简短,重复的成员列表集中在 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中,CI 和 pre-push 可以共享。代价是引入一个自定义调度器脚本(而非纯 lefthook 配置),外加本地 pre-push 路径中的一次构建。
|
||||
|
||||
`publint-all.ts` 变为异步代码,缓冲命令输出而非实时继承 stdio。收益是包级并行、稳定的输出顺序,以及一个用于资源调优的环境变量。
|
||||
`publint-all.ts` 变为异步代码,缓冲命令输出而非实时继承 stdio。收益是包级别的并行性、稳定的输出顺序,以及一个用于资源调优的环境变量。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-10-readme-known-limitations-gate.md: b7f45421bf0d4d50ec1a19941934e782f52e7926
|
||||
2026-07-10-readme-known-limitations-gate.zh.md: 4dd8db1df9b0b2be73e7ae6a64e11b8dabc2add1
|
||||
2026-07-10-readme-known-limitations-gate.zh.md: dc023ac43890d8aaaefaece2e592001629e2a74e
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# RFC:在每个 package README 中设置受门禁保护的「已知限制」章节
|
||||
|
||||
Status: implemented
|
||||
# RFC:在每个 package README 中设置受门禁保护的 Known Limitations 章节
|
||||
|
||||
[English](2026-07-10-readme-known-limitations-gate.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[文档标准](../../../AGENTS.md)将限制事项归属于 package README。如果没有统一的格式,缺失的章节无法区分「经过审计确认无限制」和「忘记写了」,而各式各样的标题也让仓库级搜索无从下手。
|
||||
[文档标准](../../../AGENTS.md)将限制事项指定在 package README 中记录。如果没有统一的格式,缺失的章节无法区分「经审计确认无此内容」与「忘了写文档」,而标题写法不一致也会妨碍全仓库搜索。
|
||||
|
||||
## 决策
|
||||
|
||||
`packages/<group>/<pkg>/package.json` 下的每个 package manifest(元数据清单)都有一个同级 README,其中包含规范的 `## Known Limitations and Deferred Work` 章节。该章节的条目记录该 package 拥有的持久性消费方缺口与非显而易见的维护约束;普通的清理工作留在源码 TODO 或所属 RFC 中。[`verify-package-readme-limitations` 门禁](../../../../scripts/verify-package-readme-limitations.ts)从 manifest 推导 package 集合,拒绝缺少 README 的情况,并要求恰好有一个规范的 h2 标题且至少包含一个顶级条目。近似标题(如 "Limitations"、"Deferred"、"What is NOT here" 或 "Non-goals")会导致失败。
|
||||
`packages/<group>/<pkg>/package.json` 下的每个包(package)manifest(元数据清单)都有一个同目录的 README,其中包含规范的 `## Known Limitations and Deferred Work` 章节。该章节的条目记录该包拥有的持久性消费方缺口与非显而易见的维护者约束;常规清理工作仍留在源码 TODO 或所属 RFC 中。[`verify-package-readme-limitations` 门禁](../../../../scripts/verify-package-readme-limitations.ts)从 manifest 推导包集合,拒绝缺少 README 的情况,并要求恰好有一个规范的 h2 标题且至少包含一个顶级条目。近似标题(如 "Limitations"、"Deferred"、"What is NOT here" 或 "Non-goals")会导致失败。
|
||||
|
||||
如果一个 package 确实没有需要声明的限制,则将其列入 `NO_LIMITATIONS` 并省略该章节。新增限制时必须移除该条目;重命名或删除条目会失败,因为每个条目必须对应一个被扫描的 package。
|
||||
如果一个包确实没有需要声明的限制事项,则将其列入 `NO_LIMITATIONS` 并省略该章节。新增限制事项时须移除该条目;重命名或移除条目会失败,因为每个条目都必须对应一个被扫描的包。
|
||||
|
||||
门禁检查存在性、格式和白名单。覆盖率与准确性由文档标准和[行文标准](../../../../.agents/skills/dsh-prose-standard/SKILL.md)下的评审负责。常设规则见 [packages/AGENTS.md](../../../../packages/AGENTS.md)。
|
||||
门禁检查的是存在性、格式与白名单。覆盖面和准确性由文档标准与 [prose 标准](../../../../.agents/skills/dsh-prose-standard/SKILL.md)下的评审负责。常设规则见 [packages/AGENTS.md](../../../../packages/AGENTS.md)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **自由格式标题**:无法统一搜索,仍然需要近似标题检测。
|
||||
- **要求空章节或写 "None."**:样板文字可能在 package 新增限制后仍然残留;白名单使「确认无限制」显式且可评审。
|
||||
- **施加字数上限**:合理的限制条目数量因 package 而异,因此由评审管控这一不设预算的 README 层级。
|
||||
- **自由格式标题**:无法统一搜索,仍需近似标题检测。
|
||||
- **要求空章节或写 "None."**:样板文字可能在包新增限制事项后仍然残留;白名单使「确无限制」这一状态显式且可评审。
|
||||
- **设置字数上限**:合理的限制事项数量因包而异,因此由评审管控这一不设预算的 README 层级。
|
||||
|
||||
## 后果
|
||||
|
||||
- 新 package 要么声明符合条件的限制事项,要么显式加入白名单;缺失、漂移或空白的章节会在本地和 CI 的 `doc-sync` 中失败。
|
||||
- 门禁向 `doc-sync` 新增一个无外部依赖的 TypeScript 脚本。
|
||||
- 重命名被强制的标题需要同时修改脚本和所有 package README。
|
||||
- 新建的包须声明符合条件的限制事项,或显式加入白名单;缺失、漂移或空的章节会在本地和 CI 的 `doc-sync` 中失败。
|
||||
- 门禁为 `doc-sync` 新增一个无外部依赖的 TypeScript 脚本。
|
||||
- 重命名受强制的标题需要同时修改脚本和所有 package README。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-12-package-model-experience-contract.md: 036efbc9510d6d0ae9e3c52a5ba8f39647adc4c9
|
||||
2026-07-12-package-model-experience-contract.zh.md: 6ee96f3befbb2ead7196f412dccf915f475cfc6d
|
||||
2026-07-12-package-model-experience-contract.zh.md: b1efa712bc4f6fa7b23c0afe965e56eabf068d97
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
# RFC:包(package)模型体验契约
|
||||
|
||||
Status: implemented
|
||||
# RFC:Package Model Experience 契约
|
||||
|
||||
[English](2026-07-12-package-model-experience-contract.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
一个包的 README 可以解释 API 和运行时机制,却不回答主导 agent harness(智能体框架)行为与成本的核心问题:这个包中有什么内容会进入模型请求、在什么条件下进入、以及这些 token 会保留多久。在插件架构中,这一缺失尤其难以审计。消费方可能把后端结果转为工具消息,策略插件可能把成功替换为错误,压缩(compaction)可能移除旧历史,agent 作用域的注册可能改变某个 agent 的提示词或 schema 而对其他 agent 毫无影响。因此只阅读名义上面向模型的包会遗漏真实的上下文影响,而逐依赖阅读源码对于日常评审又过于昂贵。
|
||||
一个 package(包)的 README 可以解释 API 和运行时机制,却不回答那个主导 agent harness(智能体框架)行为与成本的问题:本 package 中有什么内容会进入模型请求、在什么条件下进入、以及这些 token 会保留多久。在插件架构中,这一缺失尤其难以审计。消费方可能把后端结果转为工具消息,策略插件可能把成功替换为错误,上下文压缩(context compaction)可能移除旧历史,agent 作用域的注册可能改变某个 agent 的提示词或 schema 而其他 agent 不受影响。因此,只阅读名义上面向模型的 package 会遗漏真实的上下文影响,而跨所有依赖阅读源码对日常评审来说又太昂贵。
|
||||
|
||||
## 决策
|
||||
|
||||
每个具有面向模型或模型相邻契约的 workspace 包 README,都以规范的 [Model Experience 章节](../../../cookbook/adding-a-package.md#4-write-the-package-readme)结尾,紧接在 `## Known Limitations and Deferred Work` 之前;如果包在 no-limitations 允许列表上,则以 Model Experience 本身结尾。经审计确认为模型无关的通用包通过 `NO_MODEL_EXPERIENCE_SECTION` 省略该章节。
|
||||
每个具有面向模型或模型相邻契约的 workspace package README,在末尾、`## Known Limitations and Deferred Work` 之前放置规范的 [Model Experience 章节](../../../cookbook/adding-a-package.md#4-write-the-package-readme);位于 no-limitations 允许列表上的 package 以 Model Experience 本身作为末尾章节。经审计确认为模型无关的通用 package 通过 `NO_MODEL_EXPERIENCE_SECTION` 省略该章节。
|
||||
|
||||
具有直接、条件性、有上限、生命周期性、多表面或辅助模型效应的包,每个上下文表面使用一个 H3。每个 H3 说明相关模型接收到什么内容、何时接收,并对 token 效应进行分类。包所拥有的稳定文本逐字引用:系统提示词及其他长文本使用嵌套 H4 加 `markdown` 围栏,短文本则以行内形式保留,带命名的插值占位符。工具 schema 表面链接到生成的[工具目录](../../../tool-catalog.md)中对应的锚点章节,只陈述组合或配置差异;仅在运行时定义的则说明目录为何未收录。数据依赖和提供方拥有的文本以摘要形式呈现。agent 作用域的可见性须显式标注;当作用域可以隐藏提示词而不隐藏 schema(或反之)时,提示词和 schema 表面保持分开记录。
|
||||
具有直接、条件性、有上限、生命周期性、多表面或辅助模型效应的 package,每个上下文表面使用一个 H3。每个 H3 说明相关模型接收到什么内容以及何时接收,然后对 token 效应进行分类。由 package 拥有的稳定文本逐字引用:系统提示词行文和其他长字面量使用嵌套 H4 加 `markdown` 围栏,短字面量则以行内形式呈现并使用命名插值占位符。工具 schema 表面链接到生成的[工具目录](../../../tool-catalog.md)中对应的锚定章节,仅说明组合或配置差异;仅在运行时定义的工具则解释为何目录中未收录。数据依赖和提供方拥有的文本以摘要形式描述。agent 作用域的可见性须显式说明;当作用域可以隐藏其中一个而不影响另一个时,提示词表面与 schema 表面保持分开。
|
||||
|
||||
没有模型上下文效应的包,或其路径完全由另一个包渲染的包,使用验证器审计过的单句形式:`None, as ` 或 `Indirectly, through `。纯传输和无 ctx key 的测试支持包在不产生模型绑定内容时使用 none 形式。提供方后端即使会截断或过滤数据,也使用 indirect 形式;组装 bundle 在所有效应由具名子包拥有时同样使用 indirect 形式。这些句子定位贡献所在,而不重述消费方的内容。结构化章节同样只记录包自身拥有的输入、转换和差异。
|
||||
没有模型上下文效应的 package,或其路径完全由另一个 package 渲染的 package,使用验证器审计过的单句形式:`None, as ` 或 `Indirectly, through `。纯传输和无密钥的测试支持 package 在不创建模型绑定内容时使用 none 形式。提供方后端即使对数据进行上限或过滤,也使用 indirect 形式;组装 bundle 在命名子 package 拥有全部效应时同样使用 indirect 形式。这些句子定位贡献所在,而不重述消费方的内容。结构化章节同样只记录 package 自身拥有的输入、变换和差异。
|
||||
|
||||
`verify-package-readme-model-experience` 发现包的 manifest 并验证三种分类、规范的末尾章节顺序、必填字段、具体的文本证据、嵌套的逐字块以及锚定的工具目录链接。它在 `doc-sync` 和并行门禁运行器中运行。覆盖面、链接相关性和事实准确性仍由评审把关。
|
||||
`verify-package-readme-model-experience` 发现 package manifest(元数据清单)并验证三种分类、规范的末尾章节顺序、必填字段、具体字面量证据、嵌套逐字块和锚定的工具目录链接。它在 doc-sync(文档同步门禁)和并行门禁运行器中执行。覆盖面、链接相关性和事实准确性仍由评审把关。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **只记录注册了提示词或工具的包**:否决。后端、策略插件、适配器、持久化、作用域和压缩都会改变 token 的内容或生命周期,却不拥有面向模型的 schema。
|
||||
- **从源码生成一份中央上下文成本目录**:否决。AST 能找到注册点,但无法推断语义条件,例如历史保留、输出截断、父子可见性或辅助模型边界。包 README 是实现本地的契约;中央副本会增加又一个漂移面。
|
||||
- **要求给出数值 token 计数**:否决。精确计数取决于所选模型的 tokenizer、适配器序列化方式、配置和运行时数据。稳定的契约是增长形态:每请求固定、每调用条件性、保留、替换、有上限或零直接。
|
||||
- **使用三列表格**:否决。精确的源文本和条件性结果形态使单元格过于密集、难以扫读。重复的子章节为每个上下文表面提供可读的纵向空间,同时保留相同的字段。
|
||||
- **允许所有零影响包省略该章节**:否决。无约束的缺失在「经审计的零影响」和「忘了写文档」之间是歧义的。省略仅限于在验证器中以理由具名的模型无关通用包;模型相邻的零影响包保留一句显式说明。
|
||||
- **要求经审计的零影响或简单间接包也使用完整结构化形式**:否决。围绕一个事实重复标签没有意义。一句受门禁约束的句子在保持显式覆盖的同时免去了仪式感。
|
||||
- **只有约定、没有门禁**:否决。仓库级契约必须覆盖未来的每个包;评审者的记忆无法可靠地检测到遗漏的 README 章节。
|
||||
- **只记录注册提示词或工具的 package**:否决。后端、策略插件、适配器、持久化、作用域和压缩都会改变 token 的内容或生命周期,却不拥有面向模型的 schema。
|
||||
- **从源码生成一份集中式上下文成本目录**:否决。AST 能找到注册点,但无法推断语义条件,如历史保留、输出截断、父子可见性或辅助模型边界。package README 是实现本地的契约;集中副本会增加又一个漂移面。
|
||||
- **要求给出精确 token 数**:否决。精确数量取决于所选模型的 tokenizer、适配器序列化方式、配置和运行时数据。稳定的契约是增长形状:每请求固定、每调用条件性、保留、替换、有上限或零直接影响。
|
||||
- **使用三列表格**:否决。精确的源文本和条件性结果形状使单元格密集且难以扫读。重复的子章节为每个上下文表面提供可读的纵向空间,同时保留相同的字段。
|
||||
- **允许所有零影响 package 省略该章节**:否决。无约束的缺失在「经审计的零影响」和「忘记写文档」之间有歧义。省略仅限于在验证器中以理由命名的模型无关通用 package;模型相邻的零影响 package 保留一句显式说明。
|
||||
- **对审计过的零影响或简单间接 package 也要求完整结构化形式**:否决。围绕一个事实重复标签没有意义。受门禁约束的单句保留了显式覆盖而无需繁文缛节。
|
||||
- **只有约定而无门禁**:否决。仓库级契约必须覆盖未来的每个 package;评审者的记忆无法可靠地检测到遗漏的 README 章节。
|
||||
|
||||
## 后果
|
||||
|
||||
评审者可以从任何面向模型或模型相邻的包出发,看到它对会话模型、子模型和辅助调用的贡献,而无需重建完整的插件图。token 预算工作可以区分每次请求的重复开销与数据依赖的历史,agent 作用域的变更有了显式的文档检查点。包作者在模型可见行为变化时维护一个或多个紧凑的上下文表面块,或一句经分类的句子;经审计的通用包不带无关的模型样板文字。结构化字段不承诺提供方精确的 token 计数;测量仍然是模型和负载特定的,而文档化的增长形态与可见性契约保持稳定。
|
||||
评审者可以从任何面向模型或模型相邻的 package 出发,直接看到它对会话模型、子模型和辅助调用的贡献,无需重建完整的插件图。token 预算工作可以区分每次请求的重复开销与数据依赖的历史,agent 作用域的变更有了显式的文档检查点。package 作者在模型可见行为变更时维护一个或多个紧凑的上下文表面块或一句分类说明;经审计的通用 package 不承载无关的模型样板文字。结构化字段不承诺提供方精确的 token 数;测量仍然是模型和负载特定的,而文档化的增长与可见性契约保持稳定。
|
||||
|
||||
Reference in New Issue
Block a user