dsh-plugin-guide 0.3.30 → 0.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-guide",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
4
4
  "description": "DSH plugin development as an on-demand agent skill: official docs mirror, version-compatibility matrix, migration seams, measured pitfalls, plus a scaffolder, checker and pack verifier CLI.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -64,6 +64,7 @@
64
64
  "typecheck:checkout": "tsc -p tsconfig.checkout.json --noEmit",
65
65
  "test": "vitest run",
66
66
  "check:readmes": "node scripts/check-readme-sync.mjs",
67
+ "check:parity": "node scripts/check-readme-parity.mjs",
67
68
  "check:ps1-bom": "node scripts/check-ps1-bom.mjs",
68
69
  "build:llms": "node scripts/build-llms-full.mjs",
69
70
  "check:kit": "pwsh -File scripts/verify-kit.ps1",
@@ -8,6 +8,23 @@
8
8
  > 跳过 `node_modules` / `.git` / `dist` / `build` / `lib` / `coverage` 产物(但把“是否入库产物”作为一个事实记录)。
9
9
  > 本文是 `guide/plugin-dev-guide.md` §7.3 与 `guide/quick-reference.md`“社区实测坑速查”的**全文证据与出处**。
10
10
 
11
+ > ⚠️ **基线声明与许可口径(2026-10-13 追加,读本文件前必看)**
12
+ >
13
+ > 1. **这是时点快照,不是现行 API 手册。** 被调研的仓库多数停在旧世代,**其中的代码示例可能使用已被上游移除的机制**。
14
+ > 引用前先到 `references/official-docs/`(逐字镜像)核对;冲突时以镜像与官方代码为准。
15
+ > **已知本文内含已被移除的机制**(保留原文以便追溯,但**不得照抄**):
16
+ > - **`ctx.get('invariants')` / `./invariant` 伴生**(§1.1 plugin-template 等):运行时 invariants 已**整体移除**
17
+ > (`upgrade-guide/v0.2.0-rc.2/remove-runtime-invariants`)。**新插件不要建 `src/invariant.ts`。**
18
+ > - **`source: { kind: 'plugin', … }`**(多处):会话格式 V4 在消息准入处**拒绝**该形态,
19
+ > 改用 `declare module '@deepseek-ai/dsh-llm'` 声明自有 kind。
20
+ > - **`@deepseek-ai/dsh-invariants` 依赖**:该包已被移除,出现在 `peerDependencies` 里会导致装不上。
21
+ > 2. **许可**:本文件调研的部分仓库**没有 LICENSE 文件**(已逐仓复核)。凡无许可证者,
22
+ > 本文**只做事实性转述并链回上游**,不复制其源码、配置或整句原文——
23
+ > §1.2(`dsh-plugin-dev`)与 §1.8(`fabric`→`stent`)两节已按此口径重写。
24
+ > **引用具体代码前请自行确认该仓库的许可证。**
25
+ >
26
+ > 见 [`guide/version-matrix.md`](../guide/version-matrix.md) §3、[`guide/migration-0.2.md`](../guide/migration-0.2.md) §4/§13。
27
+
11
28
  ---
12
29
 
13
30
  ## 0. 总览表
@@ -24,7 +41,7 @@
24
41
  | 5 | vlln/**plugin-registry** | 插件生态基建:薄控制台 + 开发引导 | bundle(console 包)+ docs + skill | TypeScript | `dsh plugin --profile web add "github:vlln/plugin-registry#main&path:/packages/plugin/console"` | 63 | 记录 repository-plugin 0809→0811 全时间线;bundle vs 纯插件双通道对比 |
25
42
  | 6 | omdsh-dev/**dsh-plugin-check** | 插件健康检查工具 | tool-bundle | TypeScript | `dsh plugin --profile web add "C:/path/to/dsh-plugin-check"` | 27 | `plugin_check` 工具 36 项检测项(清单/patch/构建/生态/hub)按形态分流 |
26
43
  | 7 | whyihaveyou/**dsh-suite** | 双语插件目录 + 脚手架 + 自研插件 | monorepo(脚手架 + 3 个第一方插件) | JS/TS | `npm create dsh-plugin@latest`;`dsh plugin add <pkg>` | 79 | 167+ 插件目录带日检兼容徽章 + `create-dsh-plugin` 三模板 + 15 条设计准则 |
27
- | 8 | omdsh-dev/**fabric** | MC-Fabric 风格 hook 处理器 | 3 包 workspace + bundle 载体 | TypeScript | `dsh plugin --profile web add github:dsh-external/fabric` | 91 | Orchestrion-JS 加载期代码变换;`before/after/around/replace` 四操作 |
44
+ | 8 | omdsh-dev/**fabric** → **stent** | MC-Fabric 风格 hook 处理器 | 3 包 workspace + bundle 载体 | TypeScript | ⚠️ **安装命令已失效**(原 `github:dsh-external/fabric` 指向已改名仓;且无许可证) | 91 | Orchestrion-JS 加载期代码变换;`before/after/around/replace` 四操作;**与官方"不得改 agent-loop"红线相冲突,本库不建议当范例**——详见 §1.8 |
28
45
  | 9 | randerous/**dsh-turn-meta** | 最小首插件范例 | monorepo 包 | TypeScript | npm/tarball(`workspace:^` 依赖) | 11 | `agent/pre-step` `{prepend:true}` waterfall 注入 + source 归属 + `./invariant` 伴生 |
29
46
  | 10 | bobleer/**deepseek-harness-plugin-mcp** | 把 DSH 插件发布给外部 agent 的 MCP server | 双入口 npm 包(stdio/HTTP MCP + DSH bundle) | TypeScript | `npx deepseek-harness-plugin-mcp`;`dsh plugin add github:bobleer/...` | 46 | Catalog/Profile/Runtime 三平面;`ctx.tools`→MCP 工具桥接 + `tools/change` 联动 |
30
47
  | 11 | Nagi-ovo/**dsh-find-plugins** | 找插件/装插件 agent skill | skill(SKILL.md + script) | Markdown + Node ESM | 拷入 skills 目录 | 5 | 身份 = `dsh-plugin` topic;bundle/cordis/skill 四类装法优先级 |
@@ -68,6 +85,13 @@
68
85
  }
69
86
  ```
70
87
 
88
+ > ❌ **这一段已过时(2026-10-13 标注)**:下面的 `cordis.patch.yml` invariant 伴生行与随后的
89
+ > `src/invariant.ts` 写法**都已被上游移除**——运行时 invariants 在 `0.2.1-alpha.1` 整体删除。
90
+ > 该模板的 `src/{index,config,runtime,invariant}.ts` 四文件结构因此**只剩三文件有效**。
91
+ > **照抄会在启动期失败**。现行做法:不要建 `src/invariant.ts`,也不要注入 `invariants` 服务。
92
+ > 官方出处:`upgrade-guide/v0.2.0-rc.2/remove-runtime-invariants`。
93
+ > 下文代码保留**仅为追溯该模板的历史形态**。
94
+
71
95
  bundle manifest(`package.json`)与 `cordis.patch.yml`(含 invariant 伴生行):
72
96
  ```json
73
97
  { "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }
@@ -147,64 +171,36 @@
147
171
 
148
172
  ### 1.2 dsh-plugin-dev(omdsh-dev/dsh-plugin-dev)— 踩坑与做法档案
149
173
 
150
- - **目的**:把 DSH 公测期(dsh-external 组织)插件开发的真实踩坑固化下来;`README.md` 自述
151
- “记录自 DSH 公测期间的插件开发实践:vendor cordis 双副本、tsconfig 三件套、Windows junction、多帧 zstd API……”。
152
- - **结构**:`skills/dsh-plugin-dev/SKILL.md` + `references/{overview,tool-plugin,build-pitfalls,bundle-patch,testing,publish}.md`;`README.md`/`README.en.md`(无 package.json)。
153
- - **一句话总纲**(`SKILL.md`):
154
- > "工具插件 = cordis 插件包 + `cordis.patch.yml`(bundle 形态)挂进 profile 就生效;但 bundle 不是唯一形态——registry(dsh.plugin.json)/ skill(SKILL.md)/ collection(catalog.json)各有合法协议,按需求选型"
155
- - **工具插件 package.json 与入口**(`references/tool-plugin.md`):
156
- ```jsonc
157
- {
158
- "name": "@deepseek-ai/dsh-tool-xxx",
159
- "main": "lib/index.js", "types": "lib/types/index.d.ts",
160
- "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build", "test": "vitest run tests" },
161
- "peerDependencies": {
162
- "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
163
- "@deepseek-ai/cordis": "^4.0.1-rc.1",
164
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1" },
165
- "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
166
- "files": ["lib", "src", "cordis.patch.yml"]
167
- }
168
- ```
169
- ```ts
170
- import type { Context } from '@deepseek-ai/cordis'
171
- import { defineTool } from '@deepseek-ai/dsh-tools'
172
- export const name = '@deepseek-ai/dsh-tool-xxx'
173
- export const inject = ['tools']
174
- export function apply(ctx: Context): void {
175
- ctx.tools.register(defineTool({
176
- name: 'xxx', description: '…',
177
- parameters: { action: { type: 'string', required: true, enum: ['a','b'] } },
178
- output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
179
- execute: args => Promise.resolve(runAction(args)), timeoutMs: 2000,
180
- }))
181
- }
182
- ```
183
- - **20 个实测坑**(`references/build-pitfalls.md`,每条含现象/根因/修法;节选最关键):
184
- - 坑 1 **cordis 双副本 → `Property 'tools' does not exist on type 'Context'`**:根因 “DSH monorepo vendor 了 cordis(`vendor/cordis`)……插件若从 `node_modules/.pnpm/cordis@.../` 副本解析,TypeScript 把两个物理副本视为两个不同模块,`@deepseek-ai/dsh-tools` 的 `declare module 'cordis'` 增强无法合并”。修法:构建期 junction 到 `<monorepo>/vendor/cordis`;npm rc.1 路径全链 scoped `@deepseek-ai/cordis`。
185
- - 坑 1b **Windows junction 只有 PowerShell 成功**:`ln -s` → `Operation not permitted`;`cmd /c mklink /J` → MSYS 参数转换破坏 `/J`;`powershell -NoProfile -Command "New-Item -ItemType Junction ..."` ✅。`@types` 不能整体 junction(内部是 pnpm 符号链接),须直达 `.pnpm/@types+node@22.20.0/node_modules/@types/node` 真实路径。
186
- - 坑 2 **tsconfig 三件套**:`allowImportingTsExtensions: true`(否则 TS5097)、`rewriteRelativeImportExtensions: true`(否则产物 `lib/index.js` 残留 `./x.ts` 导入→运行时 ESM 崩溃)、`lib: ["ES2024"]`(否则新 API TS2550)。
187
- - 坑 6 **多帧 zstd 会话文件**:dsh 会话每批追加一个新 zstd frame(0808 起 200ms 窗口;19MB 会话 ≈ 12 万帧),单帧 API `decompressZstdFrame` 误判“只有 header”。修法:
188
- ```ts
189
- import { scanZstdFrames, createZstdFrameDecoder } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.ts'
190
- const { frames } = scanZstdFrames(buf)
191
- for (const f of createZstdFrameDecoder().decode(buf, frames)) { /* 逐帧 */ }
192
- ```
193
- - 坑 7 **DSH_* 环境变量层**:“`DSH_*` 特殊变量放 `~/.dsh/.env` 会导致启动报错(启动方式相关变量必须由运行环境传入);凭据已迁移到 `$DSH_HOME/.credentials.yaml`。”
194
- - 坑 8 **tsc 报错仍 emit**:`noEmitOnError` 默认 false;修法 `tsc ... || exit 1` 或 `--noEmitOnError`;构建后 `grep -rE "from './[^']+\.ts'" lib/` 验证无 `.ts` 残留。
195
- - 坑 13 **Windows 路径分隔符**:`path.resolve()` 返回反斜杠,与外部正斜杠比较恒 false → “路径逃逸”误报;比较前两侧都 `resolve()`。
196
- - `README.md` 环境基线表还含 **`DSH_PERMISSION_MODE=danger-full-access` ⚠️ 高风险**:“Windows 无沙箱后端(bwrap/Landlock/Seatbelt),仅此模式可启动,且禁用审批提示——只应在可信的本地开发机临时使用;不要写进项目模板、CI 或共享机器”;以及 **MSYS 路径转换**:“启动用 `~/.local/bin/dsh` wrapper(不要直接跑 `bin/dsh`——Windows 下 MSYS 路径转换触发 `ERR_UNSUPPORTED_ESM_URL_SCHEME`,issue #388;wrapper 用 `file://` URL 启动 tsx 规避)”。
197
- - 坑 14(plugin-check 审查轮教训):“先定形态矩阵,再写规则……registry 原生插件可只有 `dsh.plugin.json + index.mjs`;bundle 可插入多个包,patch name 与包名不一致是合法的。”
198
- - **安装/发布指令**(verbatim,`references/bundle-patch.md`、`SKILL.md`):
199
- ```sh
200
- dsh plugin --profile web add "C:/Users/admin/Desktop/dshext/dsh-tool-xxx"
201
- dsh plugin --profile headless add "C:/Users/admin/Desktop/dshext/dsh-tool-xxx" # dsh run 默认用 headless
202
- dsh --profile web --dump-config | grep tool-xxx
203
- dsh run "用 xxx 工具做一次端到端任务"
204
- ```
205
- npm rc.1 路径:`npx -p @deepseek-ai/dsh@0.0.1-rc.1 dsh web` → `npm pack` → `dsh plugin --profile compat add ./<pkg>.tgz`。
206
- 交付闭环:`npm run typecheck` → `grep -rE "from './[^']+\.ts'" lib/ || echo 无残留` → `npm run build && npm pack` → `--dump-config | grep <id>` → `npm run verify:execution` → `npm test`。
207
- - **.dsh-plugin / repository-plugin 机制演变**:无 repository-plugin 史,但 `references/overview.md` 把 **registry(`dsh.plugin.json` 清单 + cordis 入口,`dsh registry install`)** 列为 bundle 之外的合法形态,属“已并存、后被官方 0809 覆盖”的第二代协议(详见 1.5 plugin-registry 与 1.13 awesome-dsh-plugins 的时间线)。
174
+ > ⚠️ **许可与引用口径(2026-10-13 复核)**:该仓库 **没有 LICENSE 文件**(GitHub API `license: null`),
175
+ > 因此本节**只做事实性转述并链回原文**,**不再复制其代码块、配置文件或整句引文**。
176
+ > 同组织的 `omdsh-dev/fabric` 现在**重定向到 `omdsh-dev/stent`**,同样无许可证。
177
+ > 需要逐字内容请直接读上游;本知识库只保留"有什么、可用于什么"的索引级信息。
178
+
179
+ - **目的**:把 DSH 公测期插件开发的真实踩坑固化成档案——vendor cordis 双副本、tsconfig 三件套、
180
+ Windows junction、多帧 zstd 会话 API 等。**无 `package.json`**,是纯文档 + skill 形态的仓库。
181
+ - **结构**:一份 `SKILL.md` + 一组 `references/*.md` 分面文档(工具插件、构建坑、bundle patch、测试、发布),
182
+ 外加中英双份 README。
183
+ - **自述的核心主张**:工具插件等于"cordis 插件包 + `cordis.patch.yml`",挂进 profile 即生效;
184
+ 但 bundle 并非唯一形态,registry / skill / collection 各有其协议,应按需求选型。
185
+ - **它记录并已被本库独立复核的坑**(本库在 [`plugin-dev-guide.md` §7.3](../guide/plugin-dev-guide.md) 保留同主题条目):
186
+ 1. **cordis 双副本** → `Property 'tools' does not exist on type 'Context'`(两个物理副本 = 两个模块,
187
+ `declare module` 合并失败)。
188
+ 2. **Windows junction 只有 PowerShell 的 `New-Item -ItemType Junction` 可靠**;`ln -s` 与
189
+ `cmd /c mklink /J` 都会失败(后者被 MSYS 参数转换破坏)。
190
+ 3. **tsconfig 三件套**:`allowImportingTsExtensions` + `rewriteRelativeImportExtensions` +
191
+ 较新的 `lib`(否则分别报 TS5097 / 产物残留 `./x.ts` 导入导致运行时 ESM 崩溃 / TS2550)。
192
+ 4. **多帧 zstd 会话文件**:每批追加一个 frame,单帧 API 会误判为"只有 header"——
193
+ 必须逐帧扫。本库在 [`plugin-dev-guide.md`](../guide/plugin-dev-guide.md) 与
194
+ [`migration-0.2.md`](../guide/migration-0.2.md) 记有同一结论(按魔数 `28 B5 2F FD` 切分)。
195
+ 5. **`DSH_*` 环境变量分层**:启动方式相关变量必须由运行环境传入,不能只放 `~/.dsh/.env`;
196
+ 凭据已迁到 `$DSH_HOME/.credentials.yaml`。
197
+ 6. **`tsc` 默认报错仍 emit**:需 `--noEmitOnError` 或显式 `|| exit 1`,并 grep 产物确认无 `.ts` 残留。
198
+ 7. **Windows 路径分隔符**:`path.resolve()` 的反斜杠与正斜杠比较会恒定 false,造成"路径逃逸"误报。
199
+ 8. **`DSH_PERMISSION_MODE=danger-full-access` 在其环境基线中被标为高风险**(当时 Windows 无沙箱后端);
200
+ 该模式禁用审批提示,**不应写进模板、CI 或共享机器**。
201
+ - **本库不采纳的部分**:其环境基线锁定在早期 npm rc 线,且该仓库**无许可证**,
202
+ 故**其代码骨架不作为本库的示例来源**——示例一律取自官方 `docs/` 或本仓库自己的实现(见 §4.9)。
203
+ - **上游**:<https://github.com/omdsh-dev/dsh-plugin-dev>(读原文;注意无许可证)
208
204
 
209
205
  ### 1.3 deepseek-harness-plugin-from-scratch(Opr4Mp3r)— 代码审计式渐进教程
210
206
 
@@ -334,72 +330,41 @@
334
330
  - **目录收录标准**(`CONTRIBUTING.md`):真是 DSH 插件/生态组件(npm 包带 `dsh.bundle` 或 GitHub 仓库明确面向 DSH);有真实代码;repo+stars 可核实(`gh api`);中英双语描述(≤140 字符);`compat.status` 诚实(没实测留 unknown)。蹭 tag/工具链/占位进 `watchlist`。
335
331
  - **.dsh-plugin / repository-plugin 机制演变**:无(目录按现代 bundle/npm 名收录;全仓唯一命中“repository plugins”是第三方条目 plugin-registry 的英文描述文本,非本仓库机制)。
336
332
 
337
- ### 1.8 fabric(omdsh-dev/fabric)— Fabric/Mixin 加载期变换层
338
-
339
- - **目的**:类 MC Fabric 的 hook 处理器——一个受信插件(A)无需改另一个插件(B)的源码即可改变其函数行为,通过注册 Fabric patch 针对 B 的 module/file/function 施加 `before`/`after`/`around`/`replace`(`docs/fabric.md`)。
340
- - **机制本质**(`docs/fabric.md` + `packages/cordis-fabric/package.json`):**Orchestrion-JS 之上的加载期代码变换**(依赖 `@apm-js-collab/code-transformer ^0.18.1`),**不是** bundling Cordis、不是 shim。变换 hook 重写目标函数体,把调用记录发布到进程内 bridge channel,运行时派发给当前注册 handler;无 handler 时直接委托原始函数体。`before` 改入参 / `after` 观察或替换成功结果(含 async)/ `around` 决定原体是否运行并可替换(`invoke()` 委托)/ `replace` 完全接管。
341
- - **结构**:`packages/{cordis-fabric,cordis-fabric-api,cordis-fabric-dsh}` 三包 + 根 `cordis.patch.yml`(bundle 载体);`patches/fabric-host-integration.patch` + `patches/README.md`(host 端 launcher/bootstrap + 浏览器 build seam);`docs/{fabric,fabric-api,dsh-plugin-contracts}.md`。**不入库产物**:`lib/` 由 `prepare` 现建(`.gitignore`)。
342
- - **关键 snippet**(`docs/fabric.md`、`packages/cordis-fabric/src/service.ts`):
343
- ```ts
344
- import { bootstrapFabric, FabricService } from 'cordis-fabric'
345
- const disposeHooks = bootstrapFabric([])
346
- await ctx.plugin(FabricService)
347
- disposeHooks()
348
- ```
349
- ```ts
350
- declare module 'cordis' { interface Context { fabric: FabricService } }
351
- export class FabricService extends Service {
352
- static provide = 'fabric'
353
- constructor(ctx: Context) { super(ctx, 'fabric') }
354
- }
355
- ```
356
- 注册 patch(`ctx.fabric.register` 返回 disposer,handler 是运行时绑定的可信代码):
357
- ```ts
358
- export const inject = ['fabric']
359
- export function apply(ctx: Context & { fabric: FabricService }): void {
360
- ctx.fabric.register({
361
- id: 'my-vendor/rewrite-greeting',
362
- target: { module: '@example/target-package', versionRange: '^1.0.0',
363
- filePath: 'lib/index.js', functionQuery: { functionName: 'greet', kind: 'Sync' } },
364
- operation: 'before',
365
- handler(call: FabricCall) { call.arguments[0] = String(call.arguments[0]).toUpperCase() },
366
- })
367
- }
368
- ```
369
- `cordis.patch.yml`(显式 opt-in,两行 disabled):
370
- ```yaml
371
- - insert:
372
- - id: cordis-fabric
373
- name: 'cordis-fabric'
374
- disabled: true
375
- - id: cordis-fabric-dsh
376
- name: 'cordis-fabric-dsh'
377
- disabled: true
378
- ```
379
- 根 `package.json` 的 bundle 载体用 `&path:` 子目录依赖三包:
380
- ```json
381
- "dependencies": {
382
- "cordis-fabric": "github:dsh-external/fabric#main&path:/packages/cordis-fabric",
383
- "cordis-fabric-api": "github:dsh-external/fabric#main&path:/packages/cordis-fabric-api",
384
- "cordis-fabric-dsh": "github:dsh-external/fabric#main&path:/packages/cordis-fabric-dsh"
385
- }
386
- ```
387
- `cordis-fabric-dsh` 宿主门面 `inject = ['tools','systemPrompt','commands']`,`apply` 内逐个子插件 `ctx.plugin(FabricAgentService/FabricToolsService/FabricPromptService/FabricCommandsService)`(`packages/cordis-fabric-dsh/src/index.ts`)。窄 host 契约 `packages/cordis-fabric-dsh/src/host-contracts.ts` 是 `cordis-fabric-dsh` 对 DSH 运行时的**唯一视图**(官方 `@deepseek-ai/*` 私有不可装,只能声明最小结构面)。
388
- - **安全边界**(`README.md`):Fabric patch handlers 是注册时绑定的可信代码;“patch descriptors are configuration metadata, but executable handlers are never deserialized from YAML or model input”。`required: true` + `checkRequiredPatches` 在 boot 后 fail loud(`filePath` 可能写错 launch form `src/index.ts` vs `lib/index.js`,或函数已迁移)。`docs/fabric.md` 另注:“`cordis_mount` temporary plugins and repository plugins must not receive Fabric capability without an explicit grant.”
389
- - **记录的坑**(`src/*.ts` + `patches/README.md`,root cause + fix):
390
- - `registerHooks` 无 unregister → disposer 停用状态(`active=false`)而非移除 hook(`src/node-loader.ts`);
391
- - **同步 `registerHooks` 与 tsx loader-thread hook 共存时 CJS 崩溃(版本边界)**:函数自 22.19.0 就有,但 **22.22.3 / 24.11.1 之前**同步 load chain 对 CJS 返回空 source 崩溃 → 版本门控 `supportsSyncHooks()`,低版本走 `./hook-entry` async `module.register`(bug 源 `nodejs/node#63060`、`#56241`)(`src/node-loader.ts`);
392
- - constructor 目标被显式拒绝(`super()`/`new.target` 无法搬进 traced closure)→ 改 patch 方法或工厂函数(`src/transform.ts`);
393
- - workspace 包无 `node_modules` 边界,`module-details-from-path` 无法命名 → 回退最近 `package.json`(`src/module-identity.ts`);
394
- - Node 加载期变换要求预编译 JS,`.ts` 源码直接给 load hook 会失败(`docs/fabric.md`);
395
- - src/lib 混用会分裂 bridge 监听器 → runner 与 launcher 都只 import package root(`lib/` half)(`patches/fabric-host-integration.patch`);
396
- - pnpm 11 封锁 git-resolved subdependencies / allowBuilds 只收精确 commit key → `dangerouslyAllowAllBuilds: true` + profile 模板 `blockExoticSubdeps: false`(`patches/README.md`、`patches/host-patch.config.json`);
397
- - pnpm 通过 SSH 解析 GitHub 依赖,装机需 SSH 权限(`README.md`);
398
- - RegExp `filePath` 无法通过 JSON 配置通道序列化(变 `{}`)→ `{ fabricRegexp: [source, flags] }` marker 重建(`src/node-loader.ts`);
399
- - 同一目标上的两个 `replace` patch 注册期被拒(`src/runtime.ts`);箭头参数字面命名 `arguments` 遮蔽外层捕获被跳过(`src/transform.ts`);patch 文件 trailing-space 被 git diff 误判 → `.gitattributes` `patches/*.patch whitespace=-trailing-space`。
400
- - **主机集成限制**(`README.md`):host launcher 必须在任何 target module import 前调用 trio 的 bootstrap;官方 master 尚未做——source host 需打 `patches/fabric-host-integration.patch`(`pnpm run patch:host -- <checkout>`);npm 安装的官方 `dsh` 无法打源码 patch,需等官方 merge wiring。
401
- - **安装**(verbatim,`README.md`):`dsh plugin --profile web add github:dsh-external/fabric`;仓库不入库构建产物,`prepare` 在 git 安装时构建 `lib/`;需重启 web app,再在 profile composition 里 enable `cordis-fabric`/`cordis-fabric-dsh`。仓库自包含边界由 `verify:self-contained` 强制(拒绝 local-path 依赖、越界代码/编译器路径、绝对工作站路径)。
402
- - **.dsh-plugin / repository-plugin 机制演变**:无(纯现代 bundle + host patch 分层;无 `.dsh-plugin` 目录/字段)。
333
+ ### 1.8 ~~fabric~~ → stent(omdsh-dev/fabric)— Fabric/Mixin 加载期变换层
334
+
335
+ > ⚠️ **仓库状态与引用口径(2026-10-13 复核)**:`omdsh-dev/fabric` 现在**重定向到 `omdsh-dev/stent`**,
336
+ > 且上游已把 README 改写成一份 RFC;**该仓库没有 LICENSE 文件**。因此本节
337
+ > **只保留事实性转述与本库自己的判断**,**不再复制其源码片段、配置或整句引文**——
338
+ > 原先 §1.8 内的四段代码块与多处逐字引用已移除,需要原文请直接读上游。
339
+ > 之前的 `github:dsh-external/fabric` 安装命令**已失效,不再提供**。
340
+
341
+ - **目的**:类 MC Fabric 的 hook 处理器——受信插件 A 无需改插件 B 的源码即可改变其函数行为,
342
+ 通过注册 patch 对 B 的 module / file / function 施加 before / after / around / replace 四类操作。
343
+ - **机制本质**:**加载期代码变换**(建立在 Orchestrion-JS 的 code-transformer 之上),
344
+ **不是** bundling Cordis、也不是 shim。变换 hook 重写目标函数体,把调用记录发到进程内 bridge channel,
345
+ 运行时再派发给当前注册的 handler;没有 handler 时直接委托原函数体。
346
+ 四类操作的语义差异是这条路的重点:before 改入参、after 观察或替换成功结果(含 async)、
347
+ around 决定原体是否运行并可替换、replace 完全接管。
348
+ - **结构**(三包 workspace + 一个 bundle 载体):一个核心包 + 一个 API 包 + 一个 DSH 宿主门面包,
349
+ 根 `cordis.patch.yml` 作为 bundle 载体;另有一份 host 端 launcher/bootstrap 的 patch 与说明;
350
+ 构建产物 `lib/` 不入库,由 `prepare` 现建。
351
+ - **DSH 集成的两点设计**:宿主门面把 Host 契约收窄成**唯一视图**(因为官方 `@deepseek-ai/*` 私有不可装,
352
+ 只能声明最小结构面);bundle 载体用 pnpm 的 `&path:` 子目录选择器依赖 workspace 内的三个包。
353
+ - **安全边界(值得借鉴的设计)**:patch handler 是**注册时绑定的可信代码**,
354
+ 配置只承载描述符元数据,**可执行 handler 绝不由 YAML 或模型输入反序列化**;
355
+ 必需 patch 在 boot 后 fail loud(`filePath` 写错 launch form 或目标函数已迁移都会被抓到)。
356
+ - **它记录的坑(本库只做转述)**:Node 加载期变换要求预编译 JS(`.ts` 源码直接给 load hook 会失败);
357
+ src/lib 混用会分裂 bridge 监听器;同步 registerHooks 在特定 Node 版本区间对 CJS 崩溃,需版本门控并回退
358
+ 到 async `module.register`;constructor 目标无法被变换;workspace 包缺少 `node_modules` 边界导致模块身份
359
+ 无法命名;RegExp 形式的 `filePath` 过不了 JSON 配置通道,需要用 marker 重建;
360
+ 同一目标上的两个 replace patch 在注册期即被拒。
361
+ - **主机集成限制**:host launcher 必须在任何 target module import **之前**完成 bootstrap,
362
+ 而官方 master 尚未提供这个接线点——源码宿主需自行打 patch,npm 安装的官方 `dsh` 打不了源码 patch,
363
+ 只能等官方 merge wiring。**这是该方案没能落地的真实原因。**
364
+ - **本库的结论**:**不建议把它当插件开发范例**。它是"绕过扩展点去改别人函数体"的路线,
365
+ 与官方"新行为必须挂到已文档化扩展点、不得改 agent-loop"的红线相冲突;
366
+ 加上仓库无许可证、上游已改写成 RFC、安装命令失效,**引用价值仅在于它记录的 Node 加载期变换坑**。
367
+ - **上游**:<https://github.com/omdsh-dev/stent>(原 fabric 重定向至此;无许可证)
403
368
 
404
369
  ### 1.9 dsh-turn-meta(randerous/dsh-turn-meta)— 最小首插件范例
405
370
 
@@ -3,6 +3,22 @@
3
3
  > 调研对象:本地完整 checkout `D:\deepseek-harness`(只读)+ 公开仓库 `https://github.com/deepseek-ai/deepseek-harness`。
4
4
  > 结论来源以本地仓库文档为准;每条事实标注具体文件路径或 URL。本地 checkout 未覆盖或无法核实的内容标注 `[unverified]`。
5
5
 
6
+ > ⚠️ **基线声明(2026-10-13 追加,读本文件前必看)**
7
+ >
8
+ > 本文件是**某一时点的仓库调研快照**,**不是现行 API 手册**。其中的机制可能已被上游移除,
9
+ > 而原文不会自动更新。**引用任何 API/配置键之前,先到 `references/official-docs/`(逐字镜像)核对现行版本**;
10
+ > 冲突时以镜像与官方代码为准。
11
+ >
12
+ > **已知本文件内含已被移除的机制**(保留原文以便追溯,但**不得照抄**):
13
+ >
14
+ > | 位置 | 文中表述 | 现行状态 |
15
+ > |---|---|---|
16
+ > | §(`agent.inject` 一条) | `source: { kind: 'plugin', plugin: '<name>' }` | ❌ **已退役**。会话格式 V4 在消息准入处拒绝该形态;改用 `declare module '@deepseek-ai/dsh-llm'` 声明自有 kind |
17
+ > | §(`ctx.tools` 一条) | config `mode: native \| code \| both` | ❌ **`both` 已被拒绝**(官方 `upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode`);只接受 `native` / `ptc` |
18
+ > | §(包不变式一条) | 每个包拥有 `./invariant`,由 `verify-package-invariants` 强制 | ❌ **运行时 invariants 已整体移除**(官方 `upgrade-guide/v0.2.0-rc.2/remove-runtime-invariants`),`./invariant` 子路径不复存在 |
19
+ >
20
+ > 见 [`guide/version-matrix.md`](../guide/version-matrix.md) §3 与 [`guide/migration-0.2.md`](../guide/migration-0.2.md) §4/§13。
21
+
6
22
  DeepSeek Harness(DSH)是一个**基于 vendored Cordis 框架的插件式 agent harness**,公开宣传语为 **"Everything is a Plugin"(一切都是插件)**(见 GitHub 仓库标题 `deepseek-ai/deepseek-harness: DeepSeek Harness: Everything is a Plugin.`)。模型适配器、工具注册表、会话日志、甚至 agent loop 本身都是插件,因此每一部分都能从配置替换。
7
23
 
8
24
  ---
@@ -425,6 +441,7 @@ export function apply(ctx: Context) {
425
441
  - **遵守 `exec.signal`**(取消在途工作)。
426
442
  - 可选 `output.presentationMeta(args, value)` 派生可回放的 JSON(持久到 `tool/result`)。
427
443
  - 可选 `exec.agent` 用于异步通知:`agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } })` 是下一次请求看到的持久上下文,不是唤醒(idle agent 保持 idle)。
444
+ > ❌ **已退役(2026-10-13 标注)**:`{ kind: 'plugin', plugin }` 这个形态在会话格式 V4 起**被消息准入处拒绝**。改用 `declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { '<your-kind>': { kind: '<your-kind>' } } }` 声明自有 kind,再传 `{ kind: '<your-kind>' }`。见 [`guide/plugin-dev-guide.md`](../guide/plugin-dev-guide.md) §3 与 [`guide/migration-0.2.md`](../guide/migration-0.2.md)。
428
445
  - 后台工作:`run_in_background` 用 producer config 门控,经 `ctx.jobs.start({ kind, label, owner: exec.agent, run })` 注册。
429
446
 
430
447
  **执行策略与观察**(L57–L59):`tools/pre-execute`(可扩展 allow/deny/ask)、`ctx.tools.guard()`(最终单调 deny)、`tools/execute`(包裹 dispatch 加 deadline/retry/metrics)、`tools/post-execute`(替换呈现/值、block、附加上下文)、`tools/result`(观察不可变最终结果)。
@@ -434,6 +451,7 @@ export function apply(ctx: Context) {
434
451
  **UI 呈现**(L67–L90):`output.render` 是模型内容;UI card 由纯函数 `presentCall`/`presentResult` 声明,返回 `card`-tagged render intent(`generic`/`terminal`/`diff`/`search`/`read`/`web`)。硬规则:**纯度**(live streaming 与 REPLAY 都要跑,必须只是 `args`(+result) 的纯函数,无 I/O/session state/clock/random);UI-only 格式不进 model result;`defineTool` 对显示路径软校验(malformed args 返回 `undefined` 走 generic fallback 而非 throw)。
435
452
 
436
453
  `packages/core/tools/README.md` 给出 `ctx.tools` 服务(`ToolRuntime`)的完整 API:`register`/`presentAs`/`restrict`/`get`/`schemas`/`guard`/`execute`/`executionMode`,以及 config `mode: native | code | both`、`defineTool` schema DSL(`ParameterSchemaSpec`/`ValueSchemaSpec`、`oneOf`、`additionalProperties`)、Code Mode 的 `run_code` 保留 transport、并行执行、`ToolDefinition`/`ToolExecution`/`PreToolDecision`/`PostToolDecision`/`ToolGuard` 等关键类型(L1–L198)。
454
+ > ❌ **`both` 已不再合法(2026-10-13 标注)**:`mode` 现只接受 **`native`** 或 **`ptc`**(`code` 线已改名为 PTC)。传 `both`(含环境变量 `DSH_TOOLS_MODE=both` 与 `ctx.tools.presentAs('both')`)会被直接拒绝。官方出处:`upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode`。
437
455
 
438
456
  ### 3.4 Skill 插件机制(packages/skill)
439
457
 
@@ -493,6 +511,7 @@ skill 目录结构:`packages/skill/` 下有 `skill`(registry/Service Definit
493
511
  - switch 用 discriminant tags:closed union 结尾 `assertNever`;merge-extensible union 走文档化 default。
494
512
  - opaque 跨边界 id 用 branded(`Branded<B>` from `dsh-brand`),从不裸 `string`。
495
513
  - 每个包拥有 `./invariant`(注册 manifest 名;检查 event/data 关系或给空 installer 一个包特定 `No runtime invariant:` 理由),由 `verify-package-invariants` 强制(`packages/AGENTS.md` L18)。
514
+ > ❌ **整条机制已移除(2026-10-13 标注)**:运行时 invariant 插件及各包的 `./invariant` 导出已在 `0.2.1-alpha.1` 删除,`verify-package-invariants` 门禁随之不复存在。官方出处:`upgrade-guide/v0.2.0-rc.2/remove-runtime-invariants`。**不要在新插件里建 `src/invariant.ts`**,也不要按本节校验清单照做。
496
515
  - package README 的 Model Experience 格式(canonical Model Experience format,`docs/cookbook/adding-a-package.md` §4)+ `Known Limitations and Deferred Work` 段落由 `verify-package-readme-model-experience.ts` / `verify-package-readme-limitations.ts` 门禁。
497
516
 
498
517
  ### 4.4 双语文档
@@ -0,0 +1,79 @@
1
+ # R1 决策记录:镜像裁剪(2026-10-13)
2
+
3
+ ## 结论:**不裁剪**,改为「结构化按需取用」
4
+
5
+ 审计(`v2-2`、`v2-7`)建议剔除零引用内容以回收约 80% 体积。**精确复测后这个建议不成立**,
6
+ 因为"零引用"是按**窄口径**统计的,而本库自己声明的路由是**宽 glob**。
7
+
8
+ ## 复测数据(可复跑)
9
+
10
+ 复现命令:`node scripts/measure-mirror-references.mjs`(随包发布,不需要网络)。
11
+
12
+ 扫描 33 份自有文档,把镜像文件按"被引用强度"分档:
13
+
14
+ | 引用强度 | 文件数 | 体积 |
15
+ |---|---:|---:|
16
+ | 按确切路径被点名 | 79 | 2.29 MB |
17
+ | 仅被目录 glob 覆盖 | 617 | 20.10 MB |
18
+ | 纯按 basename 命中 | 5 | 0.04 MB |
19
+ | **完全无引用** | **3** | **≈4 KB** |
20
+
21
+ 仅被 glob 覆盖的部分按扩展名拆开:
22
+
23
+ | 扩展名 | 文件数 | 体积 |
24
+ |---|---:|---:|
25
+ | `.json`(历史 session-format schema) | 45 | **13.97 MB** |
26
+ | `.md` | 356 | 5.19 MB |
27
+ | `.yaml`(`.i18n.yaml` 翻译哈希) | 212 | 0.54 MB |
28
+ | `.png` | 4 | 0.39 MB |
29
+
30
+ > **口径**:只有当 glob 目录是路径的**前缀**时才算覆盖(`docs/persistence-changes/**` 覆盖其下文件,
31
+ > 而裸 `docs/**` 不能推导出"`docs/` 下全部都被引用")。最初的审计按"段名出现在任意位置"匹配,
32
+ > 因此把镜像算成"绝大多数无引用"——**这正是本轮复测推翻它的地方**。
33
+
34
+ ## 为什么不能按"零引用"删
35
+
36
+ 本库**自己声明**的路由就是宽 glob,这些 glob 出现在 `SKILL.md` 与多份指南里:
37
+
38
+ - `references/official-docs/docs/**`(`quick-reference*.md` ×5、`official-plugin-dev-skill.md`)
39
+ - `docs/persistence-changes/**`(`SKILL.md`、`links.md`、`migration-0.2.md`、`version-matrix.md`)
40
+ - `docs/upgrade-guide/**`(`SKILL.md`、`migration-0.2.md`、`version-matrix.md`)
41
+
42
+ 其中 `docs/persistence-changes/**` **正好就是那 14 MB 的来源**。
43
+ 所以"14 MB 零引用"的真实含义是:**它们被一个我们自己写下的 `**` 覆盖着**。
44
+ 按"没被逐字点名"删掉它们,等于**静默收窄本库对读者的承诺**——读者会以为
45
+ `docs/persistence-changes/**` 是完整的,而它已经不完整了。
46
+
47
+ 这正是本轮审计反复抓到的同一类错误:**本地状态与声明不一致,且没有门禁**。
48
+ 用一次裁剪去制造一个新的同类不一致,是拿一个 bug 换一个 bug。
49
+
50
+ ## 附带否掉的子项
51
+
52
+ - **`.i18n.yaml`(212 个)**:`v2-7` 用正则确认过"0 引用",但那是**窄口径**。
53
+ 它们同时是上游 `verify-translation-pairing` 门的哈希记录,删掉会让镜像不再是逐字副本。
54
+ - **`.png`(4 个)**:是 `docs/user/guide/providers-*.png`,被同一 glob 覆盖,且仅 0.39 MB。
55
+ - **3 个真正零引用的文件**(`CONTRIBUTING.i18n.yaml`、`CONTRIBUTING.zh.md`、`README.i18n.yaml`)
56
+ 也**保留**:它们是上游仓库根级文档,属于镜像"逐字完整"的一部分,合计约 4 KB,
57
+ 删掉省不下任何东西,却要让 `sync-official-docs.ps1` 增加一个例外清单。
58
+
59
+ ## 真正的替代方案(如果体积确实成为问题)
60
+
61
+ 保持"逐字完整",改变**取用方式**,而不是改变内容:
62
+
63
+ 1. **让 npm 包瘦、让镜像独立发布**——把 `references/official-docs/` 从 npm `files[]` 移出,
64
+ 改为随 GitHub Release 提供 `mirror-<commit>.tar.zst` 资产;`SKILL.md` 的路径路由不变,
65
+ 离线能力改为"下载一次镜像包",首次使用时由脚本落盘。
66
+ 代价:失去"装完即离线可用"。
67
+ 2. **按需取用**:`SKILL.md` 已建议优先用会话内 inspection 而非静态清单;
68
+ 可再加一条"镜像页按需从上游 fetch,附 commit 钉号",把 20 MB 变成 0。
69
+ 代价:需要网络。
70
+
71
+ 两条都需要**同时改 `SKILL.md` 的承诺**,属于产品决策而非清理动作。
72
+
73
+ ## 生效的口径
74
+
75
+ **镜像保持逐字完整**。本文件的作用是把 R1 从"待清理"改判为
76
+ "**已评估、结论是不做,理由是被 glob 覆盖而非零引用**",避免下一个会话重复提出同一建议。
77
+
78
+ > 复跑方式:`node scripts/measure-mirror-references.mjs`
79
+ > (脚本随包发布,在 `scripts/` 下;无需网络,不依赖任何一次性工作稿。)
@@ -2,6 +2,10 @@
2
2
 
3
3
  > **归档更正(2026-08-13 下载复核)**:本报告撰写时子代理沙箱无法直连外网,文档站"线上渲染页"标为 `[unverified]`。随后知识库下载脚本(`scripts/download-sources.ps1`)在更宽权限下**成功爬取了 GitHub Pages 全站 168 个 HTML 页面**(中英双语,内容已验证含中文正文),归档于 `downloads/web/site/`(下载明细见 `downloads/manifest.tsv`)——本报告中关于页面清单/正文的 `[unverified]` 项已由该爬取解决;`www.deepseek.com/harness/` 页面快照亦已归档于 `downloads/web/deepseek-com-harness.html`。
4
4
 
5
+ > ⚠️ **基线声明(2026-10-13 追加)**:本文件是**某一时点的站点快照调研**,**不是现行手册**;
6
+ > 其中的机制可能已被上游移除而原文不会自动更新。**引用任何 API/配置键前先到 `references/official-docs/` 核对现行版本。**
7
+ > **已知内含已被移除的机制**:`DSH_TOOLS_MODE` 的 `both` 取值(见下方标注)、以及 `invariants`(运行时不变式,已整体移除)。
8
+
5
9
  > 调研目标:
6
10
  > - 官网入口:`https://www.deepseek.com/harness/`
7
11
  > - 文档站:`https://deepseek-harness.github.io/deepseek-harness/develop/basic/`(及其下所有可发现页面)
@@ -536,6 +540,7 @@
536
540
  - **插件管理**:`dsh plugin --profile <name> <args...>` 转发到 pnpm;成功后把 `dsh.profile.bundles` 与已安装状态对账(含 `dsh.bundle` 声明者加入层栈)。
537
541
  - **Web 别名**:`dsh web` = `--profile web`;生产 Web runner 需 `pnpm run build`,默认 `http://127.0.0.1:3080`;**不支持 `--host 0.0.0.0`**;`--trusted-host` 添加 `/api` 浏览器信任栅栏接受的主机名。
538
542
  - **部署行为**:所有模式把调用目录当默认工作区根,加载 `AGENTS.md`/`CLAUDE.md`(渲染预算 65536 字节),用内存 SQLite 会话内容索引;新会话默认 `workspace-write` 权限预设;`DSH_PERMISSION_MODE` 改进程 fallback;`DSH_TOOLS_MODE` 选 `native`/`code`/`both`;base bundle 挂原生 DeepSeek 适配器、settings/credentials provider、`web_search`、默认关闭会话遥测;凭据解析顺序 `环境 → $DSH_HOME/.credentials.yaml → 调用目录 .env → $DSH_HOME/.env`;`DSH_TELEMETRY_MODE`(FULL/FEEDBACK_ONLY)、`DSH_TELEMETRY_OTLP_URL`、`DSH_TELEMETRY_DISABLED`(硬退出开关)。
543
+ > ❌ **`both` 已不再合法(2026-10-13 标注)**:`DSH_TOOLS_MODE` 现只接受 `native` 或 `ptc`;传 `both` 会在加载期被拒。官方出处:`upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode`。
539
544
  - **源码执行**:`pnpm run build` 后 `pnpm dsh <args...>`(经 `node --import tsx/esm` 跑 `apps/cli/src/bin.ts`,不构建);安装形态跑构建后的 `apps/cli/lib/bin.js`。
540
545
 
541
546
  ---
@@ -0,0 +1,149 @@
1
+ // Content-parity gate for the five READMEs and the five quick references.
2
+ //
3
+ // `check-readme-sync.mjs` only compares `## ` headings, so four translation
4
+ // families drifted past it unnoticed: an audit on 2026-10-13 found the es/pt/hi
5
+ // READMEs missing whole entries of "What you get" (including the flagship
6
+ // version-adaptivity document), the quick references still claiming "215 pages"
7
+ // against an actual 429, and a "re-verified on 2026-10-06" date attached to a
8
+ // commit dated 2026-10-09.
9
+ //
10
+ // This gate compares STRUCTURE at the block level — the number of list items and
11
+ // table rows inside each H2 section — instead of only the H2 titles. It cannot
12
+ // judge translation quality; it catches the case that actually happens, a
13
+ // section that gained or lost items in one language only.
14
+ //
15
+ // Usage: node scripts/check-readme-parity.mjs [--json] [--max-drift N]
16
+
17
+ import { readFileSync, existsSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+
20
+ const root = process.cwd()
21
+ const json = process.argv.includes('--json')
22
+ const maxArg = process.argv.indexOf('--max-drift')
23
+ const maxDrift = maxArg >= 0 ? Number(process.argv[maxArg + 1] ?? 0) : 0
24
+
25
+ /** The English source and its translations, for each family. */
26
+ const FAMILIES = [
27
+ {
28
+ name: 'README',
29
+ base: 'README.md',
30
+ variants: ['README-zh.md', 'README-es.md', 'README-pt.md', 'README-hi.md'],
31
+ },
32
+ {
33
+ name: 'quick-reference',
34
+ base: 'guide/quick-reference.md',
35
+ variants: [
36
+ 'guide/quick-reference.zh-CN.md',
37
+ 'guide/quick-reference.es.md',
38
+ 'guide/quick-reference.pt.md',
39
+ 'guide/quick-reference.hi.md',
40
+ ],
41
+ },
42
+ ]
43
+
44
+ /**
45
+ * Split a markdown document into H2 sections and count countable blocks.
46
+ *
47
+ * Sections are keyed by ORDINAL, not by title: the quick references legitimately
48
+ * translate their headings, so matching on English titles produced 26 false
49
+ * "missing section" reports per language. What must stay identical across a
50
+ * translation family is the SEQUENCE and the SIZE of the sections, because the
51
+ * failure mode this gate exists for is a section that gained or lost content in
52
+ * one language only.
53
+ *
54
+ * @param text - raw markdown.
55
+ * @returns ordered sections with their countable blocks.
56
+ */
57
+ function sectionProfile(text) {
58
+ const lines = text.split(/\r?\n/)
59
+ const out = []
60
+ let current = { index: 0, title: '(preamble)', bullets: 0, tableRows: 0, codeBlocks: 0 }
61
+ out.push(current)
62
+ let inFence = false
63
+
64
+ for (const line of lines) {
65
+ if (/^\s*```/.test(line)) {
66
+ inFence = !inFence
67
+ if (inFence) current.codeBlocks++
68
+ continue
69
+ }
70
+ if (inFence) continue
71
+ const h2 = /^##\s+(.*)$/.exec(line)
72
+ if (h2) {
73
+ current = { index: out.length, title: h2[1].trim(), bullets: 0, tableRows: 0, codeBlocks: 0 }
74
+ out.push(current)
75
+ continue
76
+ }
77
+ if (/^\s*[-*]\s+\S/.test(line)) current.bullets++
78
+ // A table row, excluding the separator row.
79
+ if (/^\s*\|/.test(line) && !/^\s*[\s:|-]+\|\s*$/.test(line)) current.tableRows++
80
+ }
81
+ return out
82
+ }
83
+
84
+ /** Compare one variant against the English source, by section ordinal. */
85
+ function compare(basePath, variantPath) {
86
+ const base = sectionProfile(readFileSync(join(root, basePath), 'utf8'))
87
+ const variant = sectionProfile(readFileSync(join(root, variantPath), 'utf8'))
88
+ const drift = []
89
+
90
+ if (base.length !== variant.length) {
91
+ drift.push({
92
+ kind: 'section-count',
93
+ detail: `${base.length} sections in base vs ${variant.length} in variant`,
94
+ base: null,
95
+ variant: null,
96
+ section: null,
97
+ })
98
+ }
99
+
100
+ const n = Math.min(base.length, variant.length)
101
+ for (let i = 0; i < n; i++) {
102
+ for (const key of ['bullets', 'tableRows', 'codeBlocks']) {
103
+ if (base[i][key] !== variant[i][key]) {
104
+ drift.push({
105
+ section: `#${i} ${base[i].title}`,
106
+ kind: key,
107
+ base: base[i][key],
108
+ variant: variant[i][key],
109
+ detail: `base="${base[i].title}" variant="${variant[i].title}"`,
110
+ })
111
+ }
112
+ }
113
+ }
114
+ return drift
115
+ }
116
+
117
+ let total = 0
118
+ const report = []
119
+ for (const family of FAMILIES) {
120
+ if (!existsSync(join(root, family.base))) continue
121
+ for (const v of family.variants) {
122
+ if (!existsSync(join(root, v))) continue
123
+ const drift = compare(family.base, v)
124
+ total += drift.length
125
+ report.push({ family: family.name, variant: v, drift })
126
+ }
127
+ }
128
+
129
+ if (json) {
130
+ console.log(JSON.stringify({ total, report }, null, 2))
131
+ } else {
132
+ for (const r of report) {
133
+ if (r.drift.length === 0) {
134
+ console.log(`ok ${r.family.padEnd(16)} ${r.variant}`)
135
+ continue
136
+ }
137
+ console.log(`DRIFT ${r.family.padEnd(15)} ${r.variant} (${r.drift.length} block(s))`)
138
+ for (const d of r.drift.slice(0, 12)) {
139
+ if (d.kind === 'section-count') console.log(` ${d.detail}`)
140
+ else if (d.section === null) console.log(` ${d.kind}: ${d.detail ?? ''}`)
141
+ else console.log(` ${d.kind.padEnd(11)} ${d.base} vs ${d.variant} in "${d.section}"`)
142
+ }
143
+ if (r.drift.length > 12) console.log(` ... and ${r.drift.length - 12} more`)
144
+ }
145
+ console.log(`\nblock-level drift entries: ${total}`)
146
+ }
147
+
148
+ // The gate is opt-in strict: run with --max-drift N to allow a known budget.
149
+ process.exit(total > maxDrift ? 1 : 0)
@@ -1,8 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // Five-language README sync gate: the English file is the source of truth.
3
- // Every translation must carry the same `## ` section headings in the same
4
- // order and the same configuration-table keys, so a config change cannot
5
- // ship without its translations.
3
+ //
4
+ // The READMEs keep ENGLISH `## ` headings in every language (only the prose is
5
+ // translated), so heading TEXT is not compared — a translation that legitimately
6
+ // localises a heading would fail for no reason, and `check-readme-parity.mjs`
7
+ // already compares block-level structure. What this gate owns is:
8
+ // * the NUMBER and ORDER of `## ` sections (a section added to English must be
9
+ // added everywhere), and
10
+ // * the configuration-table keys, which are API names and must match verbatim.
11
+ // The quick references are the opposite convention (translated headings); they
12
+ // are covered by the parity gate instead.
6
13
  import { readFileSync, existsSync } from 'node:fs'
7
14
  import path from 'node:path'
8
15
  import { fileURLToPath } from 'node:url'
@@ -63,13 +70,11 @@ for (const [file, label] of LANGUAGES.slice(1)) {
63
70
  }
64
71
  const text = readFileSync(filePath, 'utf8')
65
72
  const headings = headingsOf(text)
66
- for (let index = 0; index < refHeadings.length; index++) {
67
- if (headings[index] !== refHeadings[index]) {
68
- failures.push(`${file}: heading ${index + 1} is ${JSON.stringify(headings[index])}, expected ${JSON.stringify(refHeadings[index])}`)
69
- }
70
- }
73
+ // Section count only. The READMEs share English headings by convention, but
74
+ // comparing the text would break the moment a translation localises one, and
75
+ // structural parity is already owned by check-readme-parity.mjs.
71
76
  if (headings.length !== refHeadings.length) {
72
- failures.push(`${file}: ${headings.length} headings, expected ${refHeadings.length}`)
77
+ failures.push(`${file}: ${headings.length} sections, expected ${refHeadings.length}`)
73
78
  }
74
79
  const keys = configKeysOf(text)
75
80
  if (keys.length !== refKeys.length) {