@tokensapi/dsh-plugin-check 0.3.3 → 0.4.0

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/README.md CHANGED
@@ -1,95 +1,116 @@
1
- # dsh-plugin-check
2
-
3
- TokensCowork / DSH 插件合格性体检。给所有插件开发者的统一标准:一条命令,当场知道你的插件**能否安全装入宿主、会不会把用户的 Cowork 搞崩**。同一套规则同时用于:开发者本地自查、插件仓库 CI、插件市场上架门禁。
4
-
5
- ## 用法
6
-
7
- ```bash
8
- # 在插件目录里
9
- npx @tokensapi/dsh-plugin-check
10
-
11
- # 指定目标运行时(启用兼容范围判定;当前产品运行时版本见市场公告)
12
- npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1
13
-
14
- # 只跑离线清单体检(秒级,无网络)
15
- npx @tokensapi/dsh-plugin-check --skip-smoke
16
-
17
- # 机器可读输出(CI / 市场门禁)
18
- npx @tokensapi/dsh-plugin-check --json
19
-
20
- # 体检一个已发布的 npm 包(市场上架审核用)
21
- npx @tokensapi/dsh-plugin-check --package @scope/name@1.2.3 --runtime 0.1.3-alpha.1
22
- ```
23
-
24
- 退出码:`0` 合格,`1` 存在 error(`--strict` 时 warning 也计入)。冒烟失败时加 `--keep-workspace` 保留临时工作区排查。
25
-
26
- ## 体检分两个阶段
27
-
28
- **阶段① 清单体检**(纯静态、离线、秒级):检查 package.json 与 cordis 补丁的声明是否合规。
29
- **阶段② 隔离启动冒烟**(联网、约 1–3 分钟):在临时工作区按你声明的 peer 范围装出真实运行时,然后逐补丁行做真实网关启动时做的事——解析入口 → `import` → `new Context()` + `ctx.plugin()` 应用。三段都过,你的插件就不会以"装上即崩"的方式杀死用户的 Cowork。全程 `--ignore-scripts`,你的代码只在一次性子进程里运行,超时即杀。
30
-
31
- 清单存在 error 时冒烟不执行——先修再跑。
32
-
33
- ## 规则清单
34
-
35
- 每条规则都对应一种真实发生过的宿主故障。
36
-
37
- | 规则 | 级别 | 内容 | 拦的是什么事故 |
38
- |---|---|---|---|
39
- | M1-manifest | error/warning | package.json 合法,name/version 齐全;预发布版本给出上架提示 | 无法入库 |
40
- | M2-lifecycle | error | 禁止 `preinstall/install/postinstall/prepare` 脚本(构建用 `prepack`) | 安装即执行任意代码;市场受控安装的既有红线 |
41
- | M3-core-peer | error | `@deepseek-ai/cordis*`、`@deepseek-ai/dsh*` 只能是 peerDependencies | 双内核实例 Symbol 不等 → 服务注册对不上 → **Cowork 启动失败** |
42
- | M4-bundle | error | `dsh.bundle.patch` 必填、文件存在、能解析出插件行 | 宿主无法把插件挂进加载树 |
43
- | M5-row-scope | error | 补丁行只能指向本包(或其子路径) | 插件行劫持挂载其他包 |
44
- | M6-engine | error/warning | `dsh.engine` 声明目标运行时 SemVer 范围;与 `--runtime` 不相交为 error(未声明当前为 warning,将转必填) | 装进不适配的宿主版本 |
45
- | M7-peer-range | warning | `dsh-*` peer 范围应包含目标运行时 | 宿主升级后接口错配,运行时断裂 |
46
- | M8-node-engines | warning | 建议声明 `engines.node`(宿主为 Node 22+) | 语法/API 不可用 |
47
- | M9-license | warning | 建议声明 license | 分发合规 |
48
- | M10-secrets | error/warning | 发布内容不得包含 `.env`、私钥等凭据样式文件 | 凭据泄露 |
49
- | S1-pack | error | `npm pack --ignore-scripts` 必须成功 | 包本身发布不出来 |
50
- | S2-install | error/warning | 按你声明的依赖必须装得出来;宿主内核包(`@deepseek-ai/*`)的内部版本在公开源取不到时降级为 warning 并跳过冒烟 | 用户受控安装会同样失败 |
51
- | S3/S4-apply | error | 每个补丁行:入口可解析、`import` 不抛、`ctx.plugin()` 应用不抛(声明 `inject` 等待服务注入属正常,不算失败) | **启动链击穿**——一行 import 失败会拖死整棵插件树 |
52
-
53
- ## 会话内体检(装进 Cowork)
54
-
55
- 本包同时是一个 Cowork 插件。装进宿主后会注册 `plugin_check` 工具,在会话里直接问"这个插件能装吗",不必记命令行:
56
-
57
- ```
58
- # 正在开发、还没发布的插件 —— 直接读工作区
59
- plugin_check(path="D:/code/my-plugin")
60
-
61
- # 已经发布到 npm 的包
62
- plugin_check(package="@scope/name", version="1.2.3")
63
- ```
64
-
65
- **发版前用 `path`**:读的是你本地的 `package.json` 与 `cordis.patch.yml`,不需要先 `npm publish`,改一行问一次都行。已发布的包用 `package`,跑的是**清单规则 + 包内补丁行检查**:工具会把已发布的 tarball 取下来,解出 `cordis.patch.yml` 真读补丁行——只看 registry 清单会漏掉"补丁行 `name` 写成 cordis 插件名而不是 npm 包名"这类装上即让整棵插件树崩溃的缺陷。取不到包时如实标注补丁未检查(warning),不会因网络问题把插件判成不合格。
66
-
67
- 隔离启动冒烟要装依赖、起子进程,不在宿主进程里执行;完整体检仍走 CLI Plugin Check workflow。
68
-
69
- ## `dsh.engine` 字段
70
-
71
- package.json 声明你适配的 DSH 运行时范围:
72
-
73
- ```json
74
- {
75
- "dsh": {
76
- "engine": ">=0.1.3-alpha.1 <0.2.0",
77
- "bundle": { "patch": "./cordis.patch.yml" }
78
- }
79
- }
80
- ```
81
-
82
- 宿主升级越过你的范围时,市场会把你的插件标记为"待适配"而不是硬载崩溃。
83
-
84
- ## CI 集成示例
85
-
86
- ```yaml
87
- # .github/workflows/check.yml
88
- - run: npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1 --strict
89
- ```
90
-
91
- ## 上架流程中的位置
92
-
93
- 1. 开发者:发布 npm 版本前本地跑体检,全绿再发。
94
- 2. 管理员:在仓库 Actions 的 **Plugin Check** workflow 填「包名@版本」手动触发,体检报告生成在该次运行的 Summary。
95
- 3. 上架:后台 publish 对话框把这次运行的链接填入「检查记录链接」——链接的红/绿即审核证据,`reviewed_version` 随上架落库。存在 error 的版本不满足上架条件。
1
+ # dsh-plugin-check
2
+
3
+ TokensCowork / DSH 插件合格性体检。给所有插件开发者的统一标准:一条命令,当场知道你的插件**能否安全装入宿主、会不会把用户的 Cowork 搞崩**。同一套规则同时用于:开发者本地自查、插件仓库 CI、插件市场上架门禁。
4
+
5
+ ## 用法
6
+
7
+ ```bash
8
+ # 在插件目录里
9
+ npx @tokensapi/dsh-plugin-check
10
+
11
+ # 指定目标运行时(启用兼容范围判定;当前产品运行时版本见市场公告)
12
+ npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1
13
+
14
+ # 只跑离线清单体检(秒级,无网络)
15
+ npx @tokensapi/dsh-plugin-check --skip-smoke
16
+
17
+ # 机器可读输出(CI / 市场门禁)
18
+ npx @tokensapi/dsh-plugin-check --json
19
+
20
+ # 体检一个已发布的 npm 包(市场上架审核用)
21
+ npx @tokensapi/dsh-plugin-check --package @scope/name@1.2.3 --runtime 0.1.3-alpha.1
22
+ ```
23
+
24
+ 退出码:`0` 合格,`1` 存在 error(`--strict` 时 warning 也计入)。冒烟失败时加 `--keep-workspace` 保留临时工作区排查。
25
+
26
+ ## 体检分两个阶段
27
+
28
+ **阶段① 清单体检**(纯静态、离线、秒级):检查 package.json 与 cordis 补丁的声明是否合规。
29
+ **阶段② 隔离启动冒烟**(联网、约 1–3 分钟):在临时工作区按你声明的 peer 范围装出真实运行时,然后逐补丁行做真实网关启动时做的事——解析入口 → `import` → `new Context()` + `ctx.plugin()` 应用。三段都过,你的插件就不会以"装上即崩"的方式杀死用户的 Cowork。全程 `--ignore-scripts`,你的代码只在一次性子进程里运行,超时即杀。
30
+
31
+ 清单存在 error 时冒烟不执行——先修再跑。跳过冒烟时输出会说明**真实原因**(清单有 error / 补丁没有可用行 / 读不到 package.json / `--skip-smoke`),不会拿一个笼统的理由搪塞。
32
+
33
+ ## 规则清单
34
+
35
+ 每条规则都对应一种真实发生过的宿主故障。**每条规则在判什么、为什么是这个级别、锚在上游哪一段代码,完整写在 [`docs/CHECKS.md`](docs/CHECKS.md)。**
36
+
37
+ | 规则 | 级别 | 内容 | 拦的是什么事故 |
38
+ |---|---|---|---|
39
+ | C0-contract | warning | `--runtime` 不在本工具核对过的版本里时提示 | 拿过时的上游口径当结论 |
40
+ | M1-manifest | error/warning | package.json 合法,name/version 齐全且包名满足市场约束(不在上游黑名单里);预发布版本给出上架提示 | 无法入库 |
41
+ | M2-lifecycle | warning | 不要声明 `preinstall/install/postinstall/prepare`(构建用 `prepack`) | 受控安装未加 `--ignore-scripts`:脚本要么在用户机器上执行任意代码,要么被包管理器默认策略拦下、脚本产物缺失导致装完即坏 |
42
+ | M3-core-peer | error | `@deepseek-ai/cordis*`、`@deepseek-ai/dsh*` 只能是 peerDependencies | 双内核实例 → Symbol 不等 → 服务注册对不上 → **Cowork 启动失败** |
43
+ | M4-bundle | error | `dsh.bundle.patch` 必填、文件存在、能解析出插件行,且路径形状满足上游 `safeBundlePatch`(相对路径、无反斜杠、无 `..`、段内无冒号、≤512 字节) | 宿主无法把插件挂进加载树;形状不合规则受控安装验证器直接拒绝,用户端只剩手动命令 |
44
+ | M5-row-scope | error | 补丁行只能指向本包(或其子路径) | 插件行劫持挂载其他包 |
45
+ | M6-engine | error/warning | `dsh.engine` 声明目标运行时 SemVer 范围;与 `--runtime` 不相交为 error(未声明为 warning——上游目前不强制读取此字段) | 装进不适配的宿主版本 |
46
+ | M7-peer-range | error/warning | 核心 peer(`@deepseek-ai/cordis*`、`@deepseek-ai/dsh*`)的范围必须合法;`dsh-*` 的范围应包含目标运行时 | 范围写坏则受控安装装不出来;不含目标运行时则宿主升级后接口错配 |
47
+ | M8-node-engines | warning | 建议声明 `engines.node`,且要与宿主的 `^22.19.0 \|\| >=24.0.0` 有交集 | 语法/API 不可用;无交集则安装阶段报 unsupported engine |
48
+ | M9-license | warning | 建议声明 license | 分发合规 |
49
+ | M10-secrets | error/warning | 发布内容不得包含 `.env`、私钥等凭据样式文件 | 凭据泄露 |
50
+ | M11-row-identity | error | 补丁行 `id` 同层不得重复、不得含 `:`、不得占用宿主保留 id(`settings`、`web-runtime` 等) | **重复 id 会让用户整个 Desktop 起不来**;而市场安装装完从不重新解析补丁、也没有回滚,一路绿灯装上、下次开机才炸 |
51
+ | M12-client | error/warning | 声明 `dsh.client` 时,字段形状与 `exports["./client"]` 必须成立 | 宿主构造期同步解析,一份写坏会让整个 client-modules 失败——**同宿主其他插件的前端模块一起挂** |
52
+ | S1-pack | error | `npm pack --ignore-scripts` 必须成功 | 包本身发布不出来 |
53
+ | S2-install | error/warning | 按你声明的依赖必须装得出来;宿主内核包(`@deepseek-ai/*`)的内部版本在公开源取不到时降级为 warning 并跳过冒烟 | 用户受控安装会同样失败 |
54
+ | S3/S4-apply | error | 每个补丁行:入口可解析、`import` 不抛、`ctx.plugin()` 应用不抛(声明 `inject` 等待服务注入属正常,不算失败) | **启动链击穿**——一行 import 失败会拖死整棵插件树 |
55
+
56
+ ## 适配的上游版本(契约基线)
57
+
58
+ 规则不是写法偏好,是照着**某个上游版本的真实行为**写下的判定。上游一变,这个检查器
59
+ 可能要改,也可能完全不用改 —— 所以基线是机器可读的,写在 `lib/contract.mjs`,并跟着
60
+ 每次体检结果一起输出:CLI 抬头一行、`--json` 的 `contract` 字段、会话内工具结果的
61
+ `contract` 字段。
62
+
63
+ ```
64
+ 契约基线 2026-09-10 · desktop 2.0.5 / market 0.1.0-dev.0 · 已核对运行时 0.1.0-rc.8、0.1.3-alpha.1
65
+ ```
66
+
67
+ `--runtime` 传了核对范围之外的版本,会多一条 **C0-contract** warning,明说"这个版本
68
+ 没核对过,结论可能过时或过严",不假装照样有效。
69
+
70
+ 每条规则锚在上游哪个文件/符号、上游升级后逐条怎么复核,见
71
+ [`docs/CHECKS.md` §5](docs/CHECKS.md)。
72
+
73
+ ## 会话内体检(装进 Cowork)
74
+
75
+ 本包同时是一个 Cowork 插件。装进宿主后会注册 `plugin_check` 工具,在会话里直接问"这个插件能装吗",不必记命令行:
76
+
77
+ ```
78
+ # 正在开发、还没发布的插件 —— 直接读工作区
79
+ plugin_check(path="D:/code/my-plugin")
80
+
81
+ # 已经发布到 npm 的包
82
+ plugin_check(package="@scope/name", version="1.2.3")
83
+ ```
84
+
85
+ **发版前用 `path`**:读的是你本地的 `package.json` 与 `cordis.patch.yml`,不需要先 `npm publish`,改一行问一次都行。已发布的包用 `package`,跑的是**清单规则 + 包内补丁行检查**:工具会把已发布的 tarball 取下来,解出 `cordis.patch.yml` 真读补丁行——只看 registry 清单会漏掉"补丁行 `name` 写成 cordis 插件名而不是 npm 包名"这类装上即让整棵插件树崩溃的缺陷。取不到包时如实标注补丁未检查(warning),不会因网络问题把插件判成不合格。
86
+
87
+ 隔离启动冒烟要装依赖、起子进程,不在宿主进程里执行;完整体检仍走 CLI 或 Plugin Check workflow。
88
+
89
+ ## `dsh.engine` 字段
90
+
91
+ package.json 声明你适配的 DSH 运行时范围:
92
+
93
+ ```json
94
+ {
95
+ "dsh": {
96
+ "engine": ">=0.1.3-alpha.1 <0.2.0",
97
+ "bundle": { "patch": "./cordis.patch.yml" }
98
+ }
99
+ }
100
+ ```
101
+
102
+ 这个字段目前**没有被上游强制读取**(2026-09-10 核对),它的作用是让体检能判断你与目标
103
+ 运行时是否相容:声明了,`--runtime` 越出范围时会直接报 error;不声明,这一层就判不了。
104
+
105
+ ## CI 集成示例
106
+
107
+ ```yaml
108
+ # .github/workflows/check.yml
109
+ - run: npx @tokensapi/dsh-plugin-check --runtime 0.1.3-alpha.1 --strict
110
+ ```
111
+
112
+ ## 上架流程中的位置
113
+
114
+ 1. 开发者:发布 npm 版本前本地跑体检,全绿再发。
115
+ 2. 管理员:在仓库 Actions 的 **Plugin Check** workflow 填「包名@版本」手动触发,体检报告生成在该次运行的 Summary。
116
+ 3. 上架:后台 publish 对话框把这次运行的链接填入「检查记录链接」——链接的红/绿即审核证据,`reviewed_version` 随上架落库。存在 error 的版本不满足上架条件。
@@ -18,6 +18,7 @@ import { resolve } from 'node:path'
18
18
  import { checkManifest } from '../lib/manifest-check.mjs'
19
19
  import { checkSmoke } from '../lib/smoke-check.mjs'
20
20
  import { fetchPackage } from '../lib/fetch-package.mjs'
21
+ import { CONTRACT, contractLine, checkContractBaseline } from '../lib/contract.mjs'
21
22
 
22
23
  const args = process.argv.slice(2)
23
24
  const options = { dir: process.cwd(), package: undefined, runtime: undefined, skipSmoke: false, keepWorkspace: false, strict: false, json: false }
@@ -53,23 +54,37 @@ if (options.package !== undefined) {
53
54
  options.dir = fetched.dir
54
55
  }
55
56
 
57
+ // C0 先跑:目标运行时没核对过时,后面所有结论都要打个折扣。
58
+ findings.push(...checkContractBaseline(options.runtime))
59
+
56
60
  const manifestResult = checkManifest(options.dir, { runtime: options.runtime })
57
61
  findings.push(...manifestResult.findings)
58
62
  phases.push({ phase: 'manifest', findings: manifestResult.findings })
59
63
 
60
- let smokeSkipped = true
61
- if (!options.skipSmoke
62
- && manifestResult.manifest !== undefined
63
- && manifestResult.rows !== undefined
64
- && !manifestResult.findings.some(finding => finding.level === 'error')) {
65
- smokeSkipped = false
64
+ /**
65
+ * 冒烟跳过的**真实**原因。以前无论因为什么跳过都打印"清单体检存在
66
+ * error",作者拿着一份 0 error 的报告被告知有 error,只能干瞪眼。
67
+ * @returns {string | undefined} 不跳过时返回 undefined。
68
+ */
69
+ function smokeSkipReason() {
70
+ if (options.skipSmoke) return '按 --skip-smoke 跳过'
71
+ if (manifestResult.manifest === undefined) return '读不到 package.json,清单体检未完成'
72
+ if (manifestResult.findings.some(finding => finding.level === 'error')) return '清单体检存在 error,冒烟不再执行'
73
+ if (manifestResult.rows === undefined) return 'dsh.bundle.patch 缺失或补丁文件读不到,没有可冒烟的插件行'
74
+ if (manifestResult.rows.length === 0) return '补丁未声明任何插件行,没有可冒烟的对象'
75
+ return undefined
76
+ }
77
+
78
+ const skipReason = smokeSkipReason()
79
+ const smokeSkipped = skipReason !== undefined
80
+ if (!smokeSkipped) {
66
81
  const smokeResult = checkSmoke(options.dir, manifestResult.manifest, manifestResult.rows, {
67
82
  keepWorkspace: options.keepWorkspace,
68
83
  })
69
84
  findings.push(...smokeResult.findings)
70
85
  phases.push({ phase: 'smoke', findings: smokeResult.findings, workspace: smokeResult.workspace })
71
86
  } else if (!options.skipSmoke) {
72
- phases.push({ phase: 'smoke', skipped: '清单体检存在 error,冒烟不再执行' })
87
+ phases.push({ phase: 'smoke', skipped: skipReason })
73
88
  }
74
89
 
75
90
  const errors = findings.filter(finding => finding.level === 'error')
@@ -83,8 +98,10 @@ if (options.json) {
83
98
  plugin: manifestResult.manifest?.name,
84
99
  version: manifestResult.manifest?.version,
85
100
  runtime: options.runtime ?? null,
101
+ contract: CONTRACT,
86
102
  errors,
87
103
  warnings,
104
+ infos,
88
105
  phases,
89
106
  }, undefined, 2)}\n`)
90
107
  process.exit(failed ? 1 : 0)
@@ -95,6 +112,7 @@ const title = manifestResult.manifest?.name !== undefined
95
112
  : options.dir
96
113
  process.stdout.write(`\ndsh-plugin-check · ${title}\n`)
97
114
  if (options.runtime !== undefined) process.stdout.write(`目标运行时: ${options.runtime}\n`)
115
+ process.stdout.write(`${contractLine()}\n`)
98
116
  process.stdout.write('\n')
99
117
  for (const finding of errors) process.stdout.write(` ❌ [${finding.rule}] ${finding.message}\n`)
100
118
  for (const finding of warnings) process.stdout.write(` ⚠️ [${finding.rule}] ${finding.message}\n`)
@@ -102,11 +120,7 @@ for (const finding of infos) process.stdout.write(` ✅ ${finding.message}
102
120
  `)
103
121
  if (findings.length === 0) process.stdout.write(' 全部规则通过\n')
104
122
  process.stdout.write('\n')
105
- if (smokeSkipped && !options.skipSmoke && errors.length > 0) {
106
- process.stdout.write('清单存在 error,隔离冒烟未执行;修复后重跑。\n')
107
- } else if (options.skipSmoke) {
108
- process.stdout.write('已按 --skip-smoke 跳过隔离冒烟。\n')
109
- }
123
+ if (smokeSkipped) process.stdout.write(`隔离冒烟未执行:${skipReason}。\n`)
110
124
  process.stdout.write(failed
111
125
  ? `结论: 不合格(${errors.length} error / ${warnings.length} warning)\n`
112
126
  : `结论: 合格(0 error / ${warnings.length} warning)\n`)
package/docs/CHECKS.md ADDED
@@ -0,0 +1,335 @@
1
+ # 校验逻辑与上游契约
2
+
3
+ 这份文档说明 `@tokensapi/dsh-plugin-check` **每一条规则在判什么、判到什么程度、
4
+ 为什么这么判、锚在上游哪一段代码**,以及**上游变了之后怎么逐条复核**。
5
+
6
+ 规则不是写法偏好。每一条都对应「插件装进用户的 TokensCowork 之后会出什么事」的
7
+ 一条具体路径,而那条路径由宿主/市场的代码决定 —— 所以本文档给每条规则都标了
8
+ 上游锚点(文件 + 符号),复核时照着看一遍就知道该不该改。
9
+
10
+ ---
11
+
12
+ ## 0. 契约基线(先看这个)
13
+
14
+ 规则是**照着某个上游版本的行为**写的。基线写在 `lib/contract.mjs`,并随每次体检
15
+ 结果一起输出(CLI 抬头一行、`--json` 的 `contract` 字段、会话内工具结果的
16
+ `contract` 字段)。
17
+
18
+ | 项 | 值 |
19
+ | --- | --- |
20
+ | 核对日期 | 2026-09-10 |
21
+ | `dsh-plugin-desktop` | 2.0.5 |
22
+ | `dsh-community-market` | 0.1.0-dev.0 |
23
+ | `@deepseek-ai/cordis` | 4.0.2 |
24
+ | `@deepseek-ai/dsh-tools` | 0.1.3-alpha.1 |
25
+ | 受控安装的包管理器 | pnpm 11.8.0 |
26
+ | 已核对的 DSH 运行时 | `0.1.0-rc.8`、`0.1.3-alpha.1` |
27
+ | 核对过的运行时区间 | `>=0.1.0-rc.8 <0.2.0` |
28
+
29
+ `--runtime` 传了核对范围之外的版本时,会多出一条 **C0-contract** warning:明确
30
+ 告诉你「这个版本没核对过,结论可能过时或过严」,而不是假装照样有效。
31
+
32
+ > 基线只保证「这些规则在这些上游版本上是对的」。上游升级后请走
33
+ > [§5 上游变动后怎么复核](#5-上游变动后怎么复核)。
34
+
35
+ ---
36
+
37
+ ## 1. 三条执行路径(判定同源,能做的事不同)
38
+
39
+ | 入口 | 拿到的是什么 | 跑哪些规则 |
40
+ | --- | --- | --- |
41
+ | CLI `dsh-plugin-check [目录]` | 本地工作区文件 | M1–M12 + S1–S4(完整) |
42
+ | CLI `--package <name@ver>` | `npm pack` 下载并解包的目录 | M1–M12 + S1–S4(完整) |
43
+ | 会话内 `plugin_check(path=…)` | 本地工作区文件 | M1–M12 |
44
+ | 会话内 `plugin_check(package=…)` | registry 版本清单 + tarball 里抽出的 `cordis.patch.yml` | M1–M12(除 M10) |
45
+
46
+ 会话内不跑冒烟:冒烟要装依赖、起子进程、执行被测插件代码,这些不能发生在用户的
47
+ Cowork 宿主进程里。所以会话内的结论只覆盖清单侧,输出里的 `note` 会如实说明这
48
+ 一点,不冒充完整体检。
49
+
50
+ ### 同源:两条路径不允许对同一个包给出不同结论
51
+
52
+ - **只看清单就能判的规则只有一份实现** —— `lib/manifest-check.mjs` 的
53
+ `checkManifestFields`,注册表路径的 `checkPublishedManifest` 就是它的直接调用。
54
+ 版本类判定进一步收在 `lib/semver-rules.mjs`,`dsh.client` 收在
55
+ `lib/client-check.mjs`,路径/包名/保留 id 这些谓词收在 `lib/contract.mjs`。
56
+ - **M4/M5/M11 的补丁内容部分也只有一份** —— `lib/patch-rows.mjs` 的
57
+ `checkPatchContent`,两边只是取文件的方式不同(读盘 vs 解 tarball)。
58
+ - `tests/cross-path.test.mjs` 是这条约束的闸门:同一份清单喂给两条路径,
59
+ 逐条比对 `level:rule`,不一致直接红。
60
+
61
+ 这些都是踩过坑之后收的。0.3.x 时两边各写各的:会话内看不到 M5,把「装上即崩」的
62
+ 插件判成合格;注册表侧还自带一套手写迷你 SemVer,把 `>=1.2`、`1.x`、`1.2.0 - 2.0.0`
63
+ 这些完全合法的 npm 范围判成「不可识别」→ error,合格插件被判不合格。
64
+
65
+ **唯一允许的差异是 M10-secrets**:它要扫工作区文件,registry 路径根本没有工作区。
66
+ 这是有意的取舍,不是漏。
67
+
68
+ ---
69
+
70
+ ## 2. 清单规则(M 系列)
71
+
72
+ 判定实现:`lib/manifest-check.mjs` 的 `checkManifestFields` —— **两条路径共用这一份**
73
+ (见 §1)。版本范围一律用真 `semver`,并且一律带 `includePrerelease`:目标运行时本身
74
+ 长期是 `0.1.3-alpha.1` 这种预发布版,不带这个选项 semver 会把它排除在任何范围之外,
75
+ 得出「谁都不兼容」的荒谬结论。
76
+
77
+ ### M1-manifest — 清单本身可用
78
+
79
+ - **error**:`package.json` 不存在 / 不是合法 JSON / `name` 缺失 / `version` 不是
80
+ 合法 SemVer。
81
+ - **error**:`name` 不满足市场的包名约束(小写字母或数字开头,只含 `a-z 0-9 . _ -`,
82
+ 可带一层 `@scope`,总长 ≤214),或落在上游黑名单(`dsh-plugin-desktop`、
83
+ `dsh-plugin-desktop-beta`、`dsh-community-market`、`@deepseek-ai/dsh-desktop-app`)。
84
+ 这两类包无论内容多干净都装不进市场 —— 验证器在看清单内容之前就拒了。
85
+ **上游锚点**:`install/service.ts:26` 的 `PACKAGE_NAME_PATTERN`、
86
+ `desktop-plugins.ts:193` 的 `safePackageName`、`BLOCKED_PRODUCT_PACKAGES`。
87
+ - **warning**:`version` 是预发布版(如 `0.2.0-beta.1`)。
88
+ - **warning**:声明了 `dsh.profile` 或 `dsh.moduleFallback` —— 这两个是宿主/Profile
89
+ 侧字段,插件包声明它没有作用,多半是从 Profile 的 `package.json` 误抄过来的。
90
+ - **为什么只是 warning**:上游市场的 npm 验证器要求 `latest` 是**稳定的精确版**,
91
+ 预发布版拿不到一键安装按钮,只会退回手动命令 —— 但插件完全可以把预发布发在
92
+ `next` 之类的 dist-tag 上,让 `latest` 保持稳定。工具看到的是清单里的版本号,
93
+ 看不到它会不会被打成 `latest`,所以不越权判死。
94
+ - **上游锚点**:`dsh-community-market/src/install/service.ts` → `stableExactVersion()`
95
+ (`valid(v) === v && prerelease(v) === null`),在 `createNpmRegistryVerifier` 里强制。
96
+
97
+ ### M2-lifecycle — 安装期生命周期脚本
98
+
99
+ - **warning**:声明了 `preinstall` / `install` / `postinstall` / `prepare`。
100
+ - **判什么**:受控安装是 `pnpm add --save-exact --registry=…`,**没有**
101
+ `--ignore-scripts`;Profile 的 `pnpm-workspace.yaml` 也没有配
102
+ `onlyBuiltDependencies` 允许名单。于是两种结局,都不好:
103
+ - 脚本真执行 → 安装即在用户机器上跑任意代码;
104
+ - 脚本被包管理器默认策略拦下 → 依赖脚本产物的插件**装完即坏**。
105
+
106
+ `prepare` 另说:从 npm tarball 装**不执行**,从 `github:` 源装**执行** —— 同一个
107
+ 包两条安装路径行为不一致,本身就是缺陷。构建请改用 `prepack`。
108
+ - **为什么是 warning 而不是 error(2026-09-10 改的)**:这条以前是 error,理由是
109
+ 「市场受控安装会拒绝」。核对下来这个理由**已经不成立**:上游验证器现在明确接受
110
+ 带生命周期脚本的包。它仍然是个真问题,但不再是上架阻断项,所以降级并改了措辞。
111
+ - **上游锚点**:`dsh-community-market/src/install/service.ts` → `createNpmRegistryVerifier`
112
+ (只校验 name / 稳定版本 / `dsh.bundle.patch`,不看 `scripts`)、`installOptions()`
113
+ (`--save-exact --registry=…`)、`executeInstall()` 里的 `pnpm add`;测试
114
+ `dsh-community-market/tests/market-install.spec.ts` →
115
+ `'uses npm latest and accepts lifecycle, deprecated, and unrelated repository metadata'`。
116
+
117
+ ### M3-core-peer — 双内核
118
+
119
+ - **error**:`@deepseek-ai/cordis`、`@deepseek-ai/cordis-*`、`@deepseek-ai/dsh*` 出现在
120
+ `dependencies`。
121
+ - **判什么**:这些包携带**身份语义** —— cordis 的 Context 与 dsh 的服务注册靠模块
122
+ 实例 identity 对齐。插件自己装一份,宿主一份、插件一份,Symbol 不等,注册对不上,
123
+ 典型症状是整个 Cowork 启动失败(不是这个插件坏,是全都起不来)。必须声明为
124
+ `peerDependencies`,由宿主提供唯一实例。
125
+ - **不在此列**:`schemastery`、`cosmokit` 等纯工具库没有身份语义,插件可以正常依赖。
126
+
127
+ ### M4-bundle — 补丁存在且可解析
128
+
129
+ - **error**:`dsh.bundle.patch` 缺失;指向的文件不存在(目录体检)或不在发布内容里
130
+ (会话内体检,常见原因是 `files` 白名单漏了它);补丁不是合法 YAML;补丁没声明
131
+ 任何插件行。
132
+ - **error**:`dsh.bundle.patch` 的**路径形状**不满足上游 `safeBundlePatch` —— 绝对路径
133
+ (`/abs/patch.yml`)、越出包根(`../outside/patch.yml`)、含反斜杠(`dist\patch.yml`)、
134
+ 空路径段(`./a//b.yml`)、段内含冒号(`./c:/patch.yml`)、含 NUL、超过 512 字节。
135
+ 这一条 0.3.x 时是**放行**的:这类包能装、能跑、体检全绿,但市场受控安装的验证器
136
+ 一律拒绝 —— 用户端永远看不到一键安装按钮,只会看到手动安装命令。
137
+ - **warning**:会话内体检取不到 tarball(离线/超时/过大)—— 如实标注「补丁行本次
138
+ 未检查」,绝不因网络问题把插件判成不合格。
139
+ - **判什么**:宿主靠 `dsh.bundle.patch` 把插件挂进 cordis 加载树。取不到补丁,插件
140
+ 根本挂不上。
141
+ - **上游锚点**:`dsh-community-market/src/install/service.ts:268` → `safeBundlePatch()`
142
+ (`lib/contract.mjs` 里逐字照抄),在 `createNpmRegistryVerifier` 里强制;宿主侧
143
+ `dsh-plugin-desktop/src/profile.ts` 的 `DESKTOP_PATCH_PATH` 与 `profile-checkpoint.ts`
144
+ 里的 `cordis.patch.yml` 条目。
145
+
146
+ ### M5-row-scope — 补丁行只能挂自己
147
+
148
+ - **error**:补丁行的 `name` 既不等于本包包名,也不是 `<包名>/子路径`。
149
+ - **判什么**:这是**最容易写错、后果最重**的一条。补丁行里:
150
+ - `id` = 加载器条目 id(惯例上取插件自己 `export const name` 的那个 cordis 插件名);
151
+ - `name` = **用于模块解析的 npm 包说明符**。
152
+
153
+ 把 `name` 写成 cordis 插件名(而不是 npm 包名),宿主会拿着这个名字去 Profile 的
154
+ `node_modules` 找包,找不到就抛 `PackageOverlayNotFoundError` —— 而这个异常发生在
155
+ 加载树构建期,**整棵插件树都起不来**,不只是这一个插件。用户看到的是 Cowork 打不开。
156
+ - **为什么允许 `<包名>/子路径`**:一个包挂多个入口(`@scope/pkg/worker`)是合法用法。
157
+ - **上游锚点**:`dsh-plugin-desktop/src/module-resolution.ts` → `selectedOverlayCandidate()`,
158
+ `PackageOverlayNotFoundError` 在第 360 / 371 / 380 / 492 / 545 行抛出。
159
+
160
+ ### M6-engine — 目标运行时声明
161
+
162
+ - **warning**:`dsh.engine` 未声明。
163
+ - **error**:`dsh.engine` 不是合法 SemVer 范围;或给了 `--runtime` 而该范围不覆盖它。
164
+ - **诚实说明**:核对下来,**上游目前没有任何代码读 `dsh.engine`** —— 它是约定,不是
165
+ 被强制的契约。所以「未声明」只给 warning。一旦声明了,就按声明判:自己写了范围又
166
+ 不覆盖目标运行时,是插件自己的矛盾,判 error 不冤。
167
+
168
+ ### M7-peer-range — peer 范围与运行时的交集
169
+
170
+ - **error**:核心 peer 的范围不是合法 SemVer 范围。范围口径是全部
171
+ `isIdentityCore` 的包 —— `@deepseek-ai/cordis`、`@deepseek-ai/cordis-*`、
172
+ `@deepseek-ai/dsh*`。0.3.x 时过滤写的是 `startsWith('@deepseek-ai/dsh')`,
173
+ 于是把 `@deepseek-ai/cordis` 的范围写成 `not-a-range` **完全不报** —— 偏偏
174
+ cordis 正是 S2 冒烟真正要装的那个包,范围写坏下一步必然装不出来。
175
+ - **warning**:给了 `--runtime` 而 peer 范围不含它。
176
+ - **「是否含目标运行时」只对 `@deepseek-ai/dsh*` 判**:核心包里只有它跟着 DSH 运行时
177
+ 版本走,`@deepseek-ai/cordis` 自成一条 4.x 版本线。拿 `"4.0.1 || 4.0.2"` 去比对运行时
178
+ `0.1.3-alpha.1` 是范畴错误,不是插件的问题(本工具自己的清单就是这个形状)。
179
+ 区分写在 `lib/contract.mjs` 的 `isRuntimeVersioned`。
180
+ - **为什么只是 warning**:范围不含目标运行时,不代表当场就崩 —— 它预示的是宿主升级
181
+ 之后的接口错配。判死会挡住「暂时还能跑、只是没来得及放宽范围」的插件。
182
+
183
+ ### M8-node-engines / M9-license
184
+
185
+ - **warning**:`engines.node` 未声明(宿主跑在 Node 22+)、`license` 未声明。
186
+ - **warning**:`engines.node` 声明了但不是合法范围,或与宿主的
187
+ `^22.19.0 || >=24.0.0` **无交集** —— 包管理器会在安装阶段报 unsupported engine。
188
+ - 都是发布卫生问题,不影响能不能装,所以都不判死。
189
+
190
+ ### M10-secrets — 别把凭据发上去
191
+
192
+ - **error**:`files` 白名单里出现疑似凭据文件(`.env*`、`id_rsa`、`*.pem`、`*.p12`、`*.key`)。
193
+ - **warning**:目录里有 `.env` / `id_rsa` **且**没有用 `files` 收口 —— `npm pack` 很可能
194
+ 把它带上。
195
+ - 只有目录体检能做后半条(要看文件系统);会话内 `package=` 模式看不到工作区。
196
+
197
+ ### M11-row-identity — 补丁行的 id 会崩掉整个 Desktop
198
+
199
+ - **error**:同一层里 `id` 重复;`id` 含 `:`;`id` 撞宿主保留行 id
200
+ (`settings`、`web-runtime`、`desktop-webserver`、`community-market`、`dsh-market`);
201
+ 行 `name` 撞宿主保留包名(`dsh-community-market`、`dshmarket`)。
202
+ - **判什么**:这一条查的不是「插件自己能不能跑」,而是「装上之后用户的 Desktop
203
+ 还起不起得来」。三种写法各自对应一个宿主硬失败:
204
+
205
+ | 写法 | 宿主真实行为 | 上游锚点 |
206
+ | --- | --- | --- |
207
+ | 同层 `id` 重复 | `TypeError: duplicate loader entry id: <id>`;Desktop 还有一道前置检查先抛 `duplicate loader entry id "<id>" in the composed profile` | `vendor/loader/src/config/group.ts:64`;`dsh-plugin-desktop/src/profile.ts:633-646`(在 `:949` 对**合成后**的整份行集调用) |
208
+ | `id` 含 `:` | `EntryTree.sep === ':'`,嵌套行的寻址被破坏 | `vendor/loader/src/config/tree.ts:8` |
209
+ | 撞保留身份 | 行被静默剥离,并让整个 Market provider 以 `conflicting Market provider Loader identity was removed` 失败;`settings` / `web-runtime` 则直接抛错 | `profile.ts:122-129, 704-750, 913, 955-958, 977-979`;`desktop-market.ts:28-39` |
210
+
211
+ - **为什么必须是 error**:市场安装路径装完**从不重新解析** `cordis.patch.yml`
212
+ (`install/service.ts:626-665` 只比对版本号,而且**没有回滚**)。所以这类包一路绿灯
213
+ 装上,下次开机才炸 —— 到那时用户面对的是一个打不开的 Cowork,而不是一个坏掉的插件。
214
+ - **唯一性按「层」判**,与上游 `assertUniqueEntryIds` 同构:它每递归一层新建一个 Set,
215
+ 所以父层与子层的同名 `id` 不算冲突。`group: true` 行的 `config` 数组会递归进去。
216
+ - **查不到的部分(如实说明)**:跨插件的 id 冲突、以及与宿主自身行集的冲突,需要完整
217
+ 的宿主行清单;那要求本工具跟宿主行集同步演进,成本与收益不匹配。这里只查已知保留 id。
218
+ - **顶层的「修改」操作不参与判定**:形如 `- id: web-runtime` + `config:`(没有 `insert:`)
219
+ 的顶层条目是**按 id 修改一条已存在的行**,不是新增。宿主自己的
220
+ `dsh-plugin-desktop/cordis.patch.yml` 就这么用,判它反而是误伤。
221
+
222
+ ### M12-client — `dsh.client` 声明错会拖垮别的插件
223
+
224
+ - **error**:`dsh.client` 不是对象;`platform` 缺失或不是字符串;`inject` / `external`
225
+ 不是字符串数组;`external` 条目不是精确的裸包根(scope 名恰好两段、非 scope 名不含
226
+ `/`)或含本包自身;`immediately` 不是布尔值;声明了 `dsh.client` 却没有
227
+ `exports["./client"]`,或它不是字符串 / 不是带字符串 `default` 的对象。
228
+ - **warning**:`platform !== 'web'` —— 宿主只把 `platform === 'web'` 的行当作前端模块,
229
+ 其他取值等于这段声明不会生效。
230
+ - **为什么是 error 而不是「你自己的事」**:宿主的 client-modules 在**构造期同步**解析
231
+ 所有已加载包的 `dsh.client`,一份声明写坏会抛出并让整个 client-modules fiber FAIL ——
232
+ 受害的不止这个插件,而是同一宿主里所有需要前端模块的插件。
233
+ - **字段全集**就是 `{ platform, inject?, external?, immediately? }`,未知键被忽略。
234
+ - **上游锚点**:`packages/client/modules/src/index.ts` → `parseDshClient`(`:200-221`)、
235
+ `exactPackageSpecifier`(`:192-198`)、`clientExportOf`(`:224-234`)、
236
+ 自引用检查(`:454-461`)、`platform === 'web'` 过滤(`:756`)、
237
+ 缺 `./client` 的抛出(`:760-763`)。
238
+ - 没有 `dsh.client` 的插件(绝大多数)完全不产生任何发现。
239
+
240
+ ---
241
+
242
+ ## 3. 隔离冒烟(S 系列,只在 CLI)
243
+
244
+ 实现:`lib/smoke-check.mjs`。在系统临时目录建一次性工作区,全程 `--ignore-scripts`
245
+ (被测代码没有机会在检查器进程里跑安装钩子),插件代码只在一次性子进程里运行,
246
+ 超时即杀。
247
+
248
+ | 步骤 | 做什么 | 判定 |
249
+ | --- | --- | --- |
250
+ | **S1-pack** | `npm pack --ignore-scripts` | 打不出包 → error |
251
+ | **S2-install** | 按插件自己声明的 peer 范围装出一套真实运行时,插件本体走刚打出的 tgz(等价用户侧受控安装) | 装不出来 → error;例外见 §4 |
252
+ | **S3-rows** | 取补丁里 `disabled !== true` 的行 | 没有启用行 → warning(冒烟无事可做) |
253
+ | **S4-apply** | 子进程里逐行 `require.resolve` → `import` → `new Context()` → `ctx.plugin(…)` | 任一阶段抛异常 → error,并标出是哪一段 |
254
+
255
+ S4 之所以要在子进程里做,是因为它**真的执行插件代码**:顶层异常、缺模块、ESM 形状
256
+ 不对、apply 里抛出,都会在这里现形 —— 这正是「装进用户 Cowork 会在启动链同点崩溃」
257
+ 的那个点。子进程结果写文件而不是 stdout,避免插件打日志污染判定。
258
+
259
+ ---
260
+
261
+ ## 4. 通用性:为什么这套规则不因插件而异
262
+
263
+ **规则判的全是「插件与宿主之间的契约」,不是插件的业务行为。** 无论插件是做笔记、
264
+ 调模型还是控浏览器,它挂进宿主的方式完全一样:一份 `package.json`、一份
265
+ `cordis.patch.yml`、一次 `pnpm add`、一次加载树构建。M 系列查的就是这条通路上的
266
+ 声明,S 系列执行的就是这条通路本身。所以这套规则对所有插件同样适用。
267
+
268
+ 真正会因插件而异的是**业务是否正确**(接口对不对、功能好不好用)—— 那不在体检范围
269
+ 内,本工具不碰。
270
+
271
+ 体检里唯一「看插件长相」的地方是 S4:它会真的 `import` 并 `apply` 你的插件。为了不
272
+ 因为插件的正常差异而误判,有**四处刻意不致命**:
273
+
274
+ 1. **网络取不到** —— 会话内取 tarball 失败、CLI 装不上公开源上确实没有的包,都只给
275
+ warning 并如实标注「未检查」,不判不合格。检查环境的限制不该算在插件头上。
276
+ 2. **宿主内核的私有版本** —— `@deepseek-ai/*` 的内部版本本就不发公开源,S2 会先探测
277
+ 一次(`npm view <name>@<range>`),确认是「内核私有版本取不到」才降级为 warning;
278
+ 其它包解析不到,仍然是插件自己范围写错,照判 error。
279
+ 3. **补丁配置含动态表达式** —— `cordis.patch.yml` 可以带 `!!js` 之类的自定义标签。解析
280
+ 用的是宽容 schema(未知标签收敛成占位对象),行照常提取,该行的 config 标记为
281
+ 不可静态求值,冒烟时**以空配置 apply**,并给一条 warning 说明。不因为看不懂配置
282
+ 就拒绝这个插件。
283
+
284
+ > 这条到 0.4.0 才真正做到。0.3.x 时宽容标签只注册了 `'!'` 前缀,而 js-yaml 的
285
+ > multi type 查找是**对解析后的完整标签串做前缀匹配**:`!env FOO` 解析成 `!env`
286
+ > (命中),`!!js foo` 解析成 `tag:yaml.org,2002:js`(**不命中**)。于是宿主**唯一**的
287
+ > 动态配置写法反而被判成「补丁不是合法 YAML」→ M4-bundle error —— 而宿主自带的
288
+ > 6 份 `cordis.patch.yml` 有 5 份在用它(如 dsh-base 的
289
+ > `root: !!js dshHomePath('sessions')`),照抄宿主惯用法的插件因此被判不合格。
290
+ > 现在两个前缀都注册(`'!'` 与 `'tag:yaml.org,2002:js'`,后者同时覆盖裸 `!!js`
291
+ > 与 `!!js/function`),并拿这 6 份真实补丁做过回归:全部 `problems === []`、行数 > 0。
292
+ > 标签定义见 `deepseek-harness/scripts/cordis-yaml.ts`。
293
+ 4. **`inject` 声明的服务没注入** —— 插件声明了依赖服务而冒烟环境里没有,cordis 会
294
+ 正常停车等待。这是预期行为,不算失败。
295
+
296
+ ---
297
+
298
+ ## 5. 上游变动后怎么复核
299
+
300
+ 上游一个版本升上去,这个检查器**可能要改,也可能完全不用改**。别猜,照下表看一遍
301
+ 就有答案。每格里的锚点都是本次(2026-09-10)实际读过的代码位置。
302
+
303
+ | 规则 | 去看什么 | 什么情况下必须改 |
304
+ | --- | --- | --- |
305
+ | M1 | market `src/install/service.ts` → `stableExactVersion`、`PACKAGE_NAME_PATTERN`(`:26`);`desktop-plugins.ts:193` → `safePackageName`、`BLOCKED_PRODUCT_PACKAGES` | 稳定版要求放宽/收紧 → 调级别与措辞;包名约束或黑名单变化 → 同步 `lib/contract.mjs` |
306
+ | M2 | 同上 → `createNpmRegistryVerifier` 是否重新检查 `scripts`;`installOptions()` 是否加了 `--ignore-scripts`;Profile 的 `pnpm-workspace.yaml` 是否配了 `onlyBuiltDependencies` | 验证器重新拒绝脚本 → 升回 error;安装加了 `--ignore-scripts` → 改措辞 |
307
+ | M3 | `@deepseek-ai/cordis` 主版本、dsh 服务注册方式 | 身份对齐机制改变 → 重新划 `isIdentityCore` 的范围 |
308
+ | M4 | market `src/install/service.ts:268` → `safeBundlePatch`(本仓逐字照抄在 `lib/contract.mjs`);`dsh-plugin-desktop/src/profile.ts` 的补丁路径 | 谓词改一个字 → 同步照抄,否则我们放行的包上游照拒 |
309
+ | M5 | `dsh-plugin-desktop/src/module-resolution.ts` → `selectedOverlayCandidate` / `PackageOverlayNotFoundError` | 行 `name` 的解析语义变化 → 这条必须跟着改 |
310
+ | M6 | 全仓搜 `dsh.engine` 有没有被真正读取 | 一旦上游开始强制 → 「未声明」从 warning 升 error |
311
+ | M7 | 宿主提供的 `@deepseek-ai/dsh*` 版本;`@deepseek-ai/cordis` 是否并入同一条版本线 | 版本变了只需更新 `CONTRACT.runtimes`;版本线合并 → 改 `isRuntimeVersioned` |
312
+ | M8/M9 | 宿主 `engines.node`(现为 `^22.19.0 \|\| >=24.0.0`,记在 `HOST_NODE_ENGINES`)、上架元数据要求 | 宿主 Node 区间变化 → 同步常量 |
313
+ | M10 | 无上游依赖(发布卫生) | 基本不用改 |
314
+ | M11 | `vendor/loader/src/config/group.ts:64`(duplicate id 抛出)、`tree.ts:8`(`EntryTree.sep`)、`dsh-plugin-desktop/src/profile.ts:633-646` → `assertUniqueEntryIds`、`desktop-market.ts:28-39` → `DESKTOP_MARKET_IDENTITIES` | 分隔符或保留 id 集合变化 → 同步 `RESERVED_ROW_IDS` / `RESERVED_ROW_NAMES`;唯一性作用域改成全局 → 改 `checkRowIdentity` 的按层逻辑 |
315
+ | M12 | `packages/client/modules/src/index.ts:200-221` → `parseDshClient`;`:192-198` → `exactPackageSpecifier`;`:224-234` → `clientExportOf` | 字段全集或接受形状变化 → 同步 `lib/client-check.mjs` |
316
+ | S1–S4 | cordis 的 `new Context()` / `ctx.plugin()` API | cordis 主版本升级 → 复核 `RUNNER_SOURCE` |
317
+
318
+ 另外一条**没有对应规则、但复核时值得看一眼**的:`scripts/verify-cordis-config.ts` 要求
319
+ 「补丁引用的包名必须在自己的 `dependencies` 里」。这条**故意不照搬** —— 它是 monorepo
320
+ 内部约束,而第三方插件的核心包必须走 `peerDependencies`(M3),照搬会与 M3 直接打架、
321
+ 制造假报。
322
+
323
+ 复核完成后,请一并更新 `lib/contract.mjs` 里的 `verifiedAt`、`upstream`、`runtimes`
324
+ 和 `runtimeRange`,再 bump 版本发布 —— 这样用户从输出的基线一行就能看出「这套结论
325
+ 是照着哪个上游核对的」。
326
+
327
+ ---
328
+
329
+ ## 6. 判定级别的含义
330
+
331
+ - **error** — 装进用户的 Cowork 会坏(启动失败、挂不上、装不出来)。修好再发。
332
+ - **warning** — 现在能用,但预示未来断裂,或者拿不到受控安装这类更好的路径。
333
+ - **info** — 只是过程说明(比如冒烟通过了几行)。
334
+
335
+ `--strict` 会把 warning 也计入失败,退出码 1。默认只有 error 才失败。