dsh-plugin-compat-check 0.1.3 → 0.1.4

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,18 +1,22 @@
1
1
  # dsh-plugin-compat-check
2
2
 
3
- 检查**已经安装的 DSH 插件**与当前 DSH 版本的兼容性,提供源码位置、运行时证据与覆盖限制。包含兼容性标签、插件详情发现/角标、`compat_check` 对话工具和skill provider。
3
+ 检查**已经安装的 DSH 插件**与当前 DSH 版本的兼容性,提供源码位置、运行时证据与覆盖限制。包含兼容性标签、插件详情发现/角标、`compat_check` 对话工具和 skill provider。
4
4
 
5
5
  ![插件详情页:部分可用,含 file:line、影响与依据](https://raw.githubusercontent.com/lcy0121/dsh-plugin-compat-check/main/plugin/assets/compat-detail-partial.png)
6
6
 
7
- 本文描述 **0.1.3** 的实现;是否已发布及 npm latest 以[发布记录](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/Plan.md)与registry为准。安装公开版本(发布前不能用源码测试代替公开包验收):
7
+ 本说明对应 **0.1.4**,支持 Node `^22.19.0 || >=24`。公开版本及 latest 以 [npm](https://www.npmjs.com/package/dsh-plugin-compat-check) 为准,发布和验收记录见 [Plan](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/Plan.md)。
8
8
 
9
9
  ```bash
10
10
  dsh plugin --profile <profile> add dsh-plugin-compat-check
11
11
  ```
12
12
 
13
+ DSH 0.2.1-rc.2 入口:**更多 → 设置 → 内置插件 → 兼容性**。agent 可调用 `compat_check`;请选择实际 profile,不默认使用 desktop。
14
+
15
+ 0.1.4 补充必需 inject 未观测时的激活前提说明,修正 Origin 校验以比较协议、主机和有效端口(保留本机无 Origin 调用),并提供 `repository.directory=plugin`。仓库另增强了进程回收、Chrome 启动诊断及 Linux CI;浏览器开发工具不随 npm 插件交付。市场显示名与 1024 Store 采集问题须由相应上游修复,不因本包升级自动解决。
16
+
13
17
  ## 从源码构建安装
14
18
 
15
- 本仓库根为私有开发工作区,不声明DSH bundle。不要把根目录GitHub安装规格当成plugin子目录的安装方式。在仓库根运行 `npm ci --ignore-scripts`,然后:
19
+ 仓库根是 private 开发工作区,不是可激活的插件包。先在根运行 `npm ci --ignore-scripts`,然后:
16
20
 
17
21
  ```bash
18
22
  cd plugin
@@ -20,28 +24,20 @@ npm pack --pack-destination <已存在的绝对输出目录>
20
24
  dsh plugin --profile <临时profile> add file:<生成的tgz绝对路径>
21
25
  ```
22
26
 
23
- prepack复制权威skill、同步单源报告契约的宿主/客户端产物,并暂存共享kernel和MIT LICENSE,postpack清理kernel。固定运行时依赖为 `@babel/parser@7.29.9` 与 `semver@7.7.3`,支持 Node `^22.19.0 || >=24`。源码裸复制或file安装不保证prepack运行,应使用完整仓库开发布局或预打包tarball。
24
-
25
- npm产物**不带CLI bin、安装检查shell或probe入口**。CLI和安装检查从仓库使用;仓库源码现提供`--issue-report`默认脱敏Markdown导出,不读写基线、保留原退出码,须预览后自行分享,该功能不在npm包内。本插件不能做安装前检查。仓库使用说明:[README](https://github.com/lcy0121/dsh-plugin-compat-check#readme)。版本说明与发布状态请查看[开发计划与验收记录](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/Plan.md),本地路径为docs/Plan.md。
27
+ prepack 同步权威 skill/报告契约,暂存共享 kernel 和 MIT LICENSE,postpack 清理 kernel。运行时依赖固定为 `@babel/parser@7.29.9`、`semver@7.7.3`。不要将根 GitHub spec、裸 `plugin/` 复制或源码 file 安装等同完整 tgz。
26
28
 
27
- 0.1.3增加目标版本声明适配、rc.2目录与卸载清理;历史0.1.2只修正文案和版本,算法与0.1.1一致;0.1.3改变了新版DSH声明判定。
29
+ npm 产物**没有 CLI bin、安装检查 shell 或 probe 入口**,不支持 `npx dsh-plugin-compat-check`,本面板不做安装前检查。这些入口及 `--issue-report` 默认脱敏 Markdown 导出从[仓库](https://github.com/lcy0121/dsh-plugin-compat-check#readme)使用。显式安装检查会联网、启动宿主并执行目标插件;临时 profile 不是 OS 沙箱。
28
30
 
29
31
  ## 判定与边界
30
32
 
31
- 2026-10-18从候选完整tgz在私有rc.2宿主和真实Chrome验证:“更多 → 设置 → 内置插件 → 兼容性”可见标签和报告;禁用后报告路由消失,启用后恢复且标签唯一。59714e7中10月17日标签未显示的历史记录本次未复现,原因未定;不把这次局部UI验收当作所有业务功能认证。
32
-
33
- 当前profile在宿主进程内采样服务描述符,不执行getter;未知访问器保留未知,并纳入能可靠映射的loader FAILED/PENDING。查询其它profile只用静态证据,不借用当前宿主服务。Babel AST扫描不执行目标源码,分析运行在有时间和内存上限的worker中;动态调用和跨函数数据流仍有限。
34
-
35
- `certainty`为certain或suspected;过滤、守卫、弃权另列。省略probe键为未采样,现代观测绑定profile/DSH版本且默认24小时过期,旧报告标记未绑定。0.1.3对DSH≥0.2.1-rc.1优先自有engines.dsh,缺失时回退官方peer;旧目标保持旧规则。声明冲突/非法字段的影响为推断,不冒充实测加载失败。只读profile独立compatibility.json,按插件名、插件精确版本及DSH精确版本匹配;无效记录整条忽略并限制覆盖,不可读/超64KiB则豁免未观测,不自动写入豁免。
36
-
37
- 按已知目标选目录:DSH≥0.2.1-rc.2用rc.2快照(host98/client8),其余/未知用原0.2.0-rc.2快照(host91/client8);未来版本未经认证。转述保留 `impactBasis=inferred`、coverage限制和vacuous。API发现通常有file:line,loader/声明事实可能无位置;声明获准不证明API或业务成功。客户端待界面核验;到货工具与新增的独立业务队列均不属于本面板。仓库业务队列覆盖固定dshmarket的备注/收藏,以及dsh-better-sidebar的保存/预览HTTP路径、编码/换行保留及失败后恢复;对照仅在浏览器注入POST503,业务标签不进入兼容性判定,结果不认证其它路径或原插件有缺陷。
38
-
39
- 插件体检不安装包、不调用外部命令或联网;会启动分析worker,并默认写自己的schema2基线:`$DSH_HOME/dsh-plugin-compat-check/baseline.<profile>.<path-hash>.<mode>.json`。模式分离、锁内比较、原子替换;源码扫描不完整不推进基线。默认不输出共享loader诊断;诊断需显式私有DSC_OUT_DIR。若需完全禁止基线读写,请使用仓库CLI的 `--no-baseline`。
40
-
41
- 0.1.1的分阶段证据包括默认回归、独立tarball执行及2026-10-15的真实DSH0.2.0-rc.2/macOS arm64验收:工具、skill、面板、详情角标、基线及Chrome正负向对照。2026-10-16另在Ubuntu24.04.5 arm64 VM通过Node22.19.0/24.21.0、真实DSH/tgz/HTTP及资源边界验收。同日提交38fa4a0的[GitHub Actions](https://github.com/lcy0121/dsh-plugin-compat-check/actions/runs/38892527900)在Ubuntu x64通过Node22/24资源检查及联合18项检查(4项真实DSH、1项冻结pnpm安装、13项离线)。后续main提交49403d7的[CI](https://github.com/lcy0121/dsh-plugin-compat-check/actions/runs/38923778251)再次通过Node22/24发布前与资源检查、Linux真实集成。Windows按约定待验收;skill正文修订后的仓库验证不冒充旧现场正文验收。具体测试数量与证据见仓库docs/current-state.json、docs/Plan.md;不把历史测试数字作为当前版本的完整功能认证。
42
-
43
- 报告结构由仓库lib/contracts.mjs校验:HTTP envelope.report与compat_check.report均保留完整发现、覆盖、观测和基线;工具原summary/text字段保留。新面板拒绝损坏或不支持版本的负载,提示刷新和对齐版本。旧probe保持未绑定警告,旧schema1基线中性重建;内核现支持有界常量属性、函数参数/返回值、包内模块别名及透明Proxy;未知动态行为与超预算继续显示覆盖限制。具体字段与错误码见 [Plan契约记录](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/Plan.md#23-报告结构与兼容策略)。
33
+ - 当前 profile 在宿主进程内采样服务描述符,不执行 getter,纳入可靠映射且启用的 loader FAILED/PENDING。其它 profile 仅用静态证据,不借用当前宿主服务;未知、未采样或省略键不等于缺失。
34
+ - Babel AST 扫描不执行目标源码,分析运行在有时间/内存上限的 worker 中;有界包内值流支持部分别名、参数/返回值和透明 Proxy,动态行为仍有限。必需 inject 的激活障碍与方法失效分别判断。
35
+ - 转述保留 `certainty`、`impactBasis`、`coverage` 和 `vacuous`。API 发现通常有 `file:line`,loader/声明事实可能无位置;声明放行、激活成功、条目到货和 ok 均不证明业务正常,客户端功能须另验收。
36
+ - 从 DSH 0.2.1-rc.1 起优先自有 `engines.dsh`,缺失才回退官方 peer,旧目标保留旧规则。声明冲突/非法字段是推断风险,不冒充实际加载失败。只读独立 `compatibility.json`,按插件名、插件精确版本、DSH 精确版本匹配豁免;无效/不可读/超64KiB保留限制,不自动授予,非法字段不可豁免。
37
+ - DSH≥0.2.1-rc.2 用新目录(host98/client8),其余/未知用旧0.2.0-rc.2目录(host91/client8);未来 DSH 未认证。rc.2 的真实 Chrome 验收覆盖导航、报告渲染、禁用后路由移除及重启用后恢复/标签唯一;不认证所有插件业务。
38
+ - 体检不装包、不调用外部命令或联网;默认写自身 schema2 基线:`$DSH_HOME/dsh-plugin-compat-check/baseline.<profile>.<path-hash>.<mode>.json`。模式隔离、锁内比较、原子替换,不完整扫描不推进。完全禁基线读写须用仓库 CLI 的 `--no-baseline`。
39
+ - HTTP envelope.report 和 `compat_check.report` 保留完整发现、覆盖、观测和基线,原 summary/text 保留;新面板拒绝损坏或不支持版本的负载。旧 probe 提示未绑定,旧 schema1 基线中性重建,不兼容报告变更须升 schema。
44
40
 
45
- ## License
41
+ 限定 macOS/Linux、真实 DSH/Chrome、独立 tgz 和工程验收的逐次版本及证据见 [current-state.json](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/current-state.json)。Windows 待真实验收。独立业务/模型度量属于仓库开发工具,不随 npm 分发、不进入产品判定,默认测试/CI不调用模型;固定样本不是生态准确率或完整模型质量认证。
46
42
 
47
- MIT。源码、问题反馈与仓库文档:[GitHub](https://github.com/lcy0121/dsh-plugin-compat-check)。
43
+ [完整契约与复现](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/docs/Plan.md#2-现行判定与集成契约) · [源码及问题反馈](https://github.com/lcy0121/dsh-plugin-compat-check) · [MIT License](https://github.com/lcy0121/dsh-plugin-compat-check/blob/main/LICENSE)
package/index.js CHANGED
@@ -1000,8 +1000,12 @@ function isTrustedRequest(req) {
1000
1000
  const host = String((req.headers && (req.headers.host || req.headers.Host)) || '');
1001
1001
  const origin = String((req.headers && (req.headers.origin || req.headers.Origin)) || '');
1002
1002
  const hostOk = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i.test(host);
1003
- if (!origin) return hostOk;
1004
- return hostOk && /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i.test(origin);
1003
+ if (!hostOk) return false;
1004
+ const protocol = req.socket?.encrypted === true ? 'https:' : 'http:';
1005
+ const expectedOrigin = new URL(`${protocol}//${host}`).origin;
1006
+ if (!origin) return true;
1007
+ if ( !/^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i.test(origin)) return false;
1008
+ return new URL(origin).origin === expectedOrigin;
1005
1009
  } catch {
1006
1010
  return false;
1007
1011
  }
@@ -0,0 +1,287 @@
1
+ /** Owned process-group lifecycle. No argv, environment or global process listing
2
+ * is retained; ps is a bounded failure diagnostic, filtered to this group. */
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { privateDiagnostic } from "../plugin/lib/host-resources.js";
7
+ export function failureDiagnostic(error) {
8
+ return error?.diagnostics?.length
9
+ ? error.diagnostics
10
+ : error?.diagnostic
11
+ ? [error.diagnostic]
12
+ : [];
13
+ }
14
+ export function publishDiagnostic(diagnostic, privateDetails) {
15
+ const dir = process.env.DSC_EXECUTION_DIAGNOSTICS;
16
+ if (dir) {
17
+ const redact = (value) =>
18
+ String(value)
19
+ .slice(-8192)
20
+ .replace(/https?:\/\/[^\s"'<>]+/gi, "<url>")
21
+ .replace(
22
+ /(authorization\s*[:=]\s*)(?:Bearer\s+)?[^\s,;]+/gi,
23
+ "$1[redacted]",
24
+ )
25
+ .replace(
26
+ /((?:api[_-]?key|access[_-]?token|password)\s*[:=]\s*)[^\s,;]+/gi,
27
+ "$1[redacted]",
28
+ );
29
+ const data = {
30
+ ...diagnostic,
31
+ ...(privateDetails
32
+ ? {
33
+ privateDetails: Object.fromEntries(
34
+ Object.entries(privateDetails).map(([key, value]) => [
35
+ key,
36
+ redact(value),
37
+ ]),
38
+ ),
39
+ }
40
+ : {}),
41
+ };
42
+ const written = privateDiagnostic(
43
+ path.join(dir, `${diagnostic.component}-${randomUUID()}.json`),
44
+ data,
45
+ );
46
+ diagnostic.persistence = { requested: true, written };
47
+ }
48
+ return diagnostic;
49
+ }
50
+ export function groupSnapshot(pgid) {
51
+ if (process.platform === "win32")
52
+ return { available: false, reason: "unsupported-platform" };
53
+ const args =
54
+ process.platform === "darwin"
55
+ ? ["-g", String(pgid), "-o", "pid=,ppid=,pgid=,uid=,stat="]
56
+ : ["-axo", "pid=,ppid=,pgid=,uid=,stat="];
57
+ const r = spawnSync("/bin/ps", args, {
58
+ encoding: "utf8",
59
+ timeout: 1000,
60
+ maxBuffer: 2 * 1024 * 1024,
61
+ });
62
+ if (
63
+ process.platform === "darwin" &&
64
+ r.status === 1 &&
65
+ !r.error &&
66
+ !r.stdout.trim() &&
67
+ !r.stderr.trim()
68
+ )
69
+ return { available: true, members: [], truncated: false };
70
+ if (r.status !== 0 || r.error)
71
+ return { available: false, reason: r.error?.code || "ps-failed" };
72
+ const lines = r.stdout.trim().split("\n");
73
+ const members = [];
74
+ for (const line of lines) {
75
+ const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
76
+ if (!m) return { available: false, reason: "invalid-ps-output" };
77
+ if (Number(m[3]) === pgid)
78
+ members.push({
79
+ pid: Number(m[1]),
80
+ ppid: Number(m[2]),
81
+ pgid: Number(m[3]),
82
+ uid: Number(m[4]),
83
+ state: m[5],
84
+ });
85
+ }
86
+ return {
87
+ available: true,
88
+ members: members.slice(0, 64),
89
+ truncated: members.length > 64,
90
+ };
91
+ }
92
+ const stopping = new WeakMap();
93
+ export function stopOwnedChild(child) {
94
+ if (!child?.pid)
95
+ return Promise.resolve({ stopped: true, status: "not-started" });
96
+ if (stopping.has(child)) return stopping.get(child);
97
+ const pending = (async () => {
98
+ if (!Number.isSafeInteger(child.pid) || child.pid <= 1)
99
+ throw new Error("Invalid owned child pid");
100
+ const done = () => child.exitCode !== null || child.signalCode !== null;
101
+ let exitListener;
102
+ const exited = done()
103
+ ? Promise.resolve()
104
+ : new Promise((resolve) => {
105
+ exitListener = resolve;
106
+ child.once("exit", resolve);
107
+ });
108
+ const signals = [];
109
+ let phase = "SIGTERM";
110
+ const state = () => ({
111
+ schemaVersion: 1,
112
+ kind: "plugin-compat-check/execution-diagnostic",
113
+ component: "owned-process",
114
+ phase,
115
+ pid: child.pid,
116
+ pgid: process.platform === "win32" ? null : child.pid,
117
+ parentPid: process.pid,
118
+ uid: process.getuid?.() ?? null,
119
+ leaderExited: done(),
120
+ exitCode: child.exitCode,
121
+ signalCode: child.signalCode,
122
+ signals,
123
+ });
124
+ const waitExit = async () => {
125
+ let timer;
126
+ try {
127
+ await Promise.race([
128
+ exited,
129
+ new Promise((resolve) => {
130
+ timer = setTimeout(resolve, 2000);
131
+ }),
132
+ ]);
133
+ } finally {
134
+ clearTimeout(timer);
135
+ }
136
+ };
137
+ const signal = async (sig) => {
138
+ phase = sig;
139
+ try {
140
+ if (process.platform !== "win32") process.kill(-child.pid, sig);
141
+ else if (!done()) child.kill(sig);
142
+ signals.push({ signal: sig, result: "sent" });
143
+ } catch (error) {
144
+ signals.push({ signal: sig, result: error.code || "error" });
145
+ if (error.code === "ESRCH") return;
146
+ let group = groupSnapshot(child.pid);
147
+ if (
148
+ process.platform === "darwin" &&
149
+ error.code === "EPERM" &&
150
+ !done() &&
151
+ group.available &&
152
+ !group.truncated &&
153
+ group.members.every((m) => m.uid === process.getuid?.()) &&
154
+ (sig === "SIGKILL" ||
155
+ group.members.every(
156
+ (m) => m.state.startsWith("Z") || m.state.includes("E"),
157
+ ))
158
+ ) {
159
+ await waitExit();
160
+ group = groupSnapshot(child.pid);
161
+ }
162
+ // Darwin filters zombies out of killpg and can return EPERM when only
163
+ // zombies remain. Never infer this from EPERM alone or a failed ps.
164
+ const zombieOnly =
165
+ process.platform === "darwin" &&
166
+ error.code === "EPERM" &&
167
+ group.available &&
168
+ (done() ||
169
+ group.members.some(
170
+ (m) => m.pid === child.pid && m.state.startsWith("Z"),
171
+ )) &&
172
+ !group.truncated &&
173
+ (group.members.length > 0 || done()) &&
174
+ group.members.every(
175
+ (m) => m.uid === process.getuid?.() && m.state.startsWith("Z"),
176
+ );
177
+ if (zombieOnly) {
178
+ signals[signals.length - 1].result = group.members.length
179
+ ? "verified-zombie-only"
180
+ : "verified-exited-group";
181
+ signals[signals.length - 1].group = group;
182
+ return;
183
+ }
184
+ const failure = new Error(
185
+ `Owned process cleanup failed: ${sig} ${error.code || "error"}`,
186
+ { cause: error },
187
+ );
188
+ failure.code = "OWNED_PROCESS_CLEANUP_FAILED";
189
+ failure.diagnostic = publishDiagnostic({
190
+ ...state(),
191
+ code: failure.code,
192
+ signalError: {
193
+ code: error.code || null,
194
+ syscall: error.syscall || null,
195
+ stack: String(error.stack || "")
196
+ .split("\n")
197
+ .slice(1, 10)
198
+ .map((line) =>
199
+ line
200
+ .replace(/\((?:file:\/\/)?\/.*\/([^/]+:\d+:\d+)\)/g, "($1)")
201
+ .replace(/at (?:file:\/\/)?\/.*\/([^/]+:\d+:\d+)/g, "at $1"),
202
+ ),
203
+ },
204
+ group,
205
+ });
206
+ throw failure;
207
+ }
208
+ };
209
+ try {
210
+ await signal("SIGTERM");
211
+ await new Promise((r) => setTimeout(r, 100));
212
+ await signal("SIGKILL");
213
+ phase = "exit";
214
+ let timer;
215
+ try {
216
+ await Promise.race([
217
+ exited,
218
+ new Promise((_, reject) => {
219
+ timer = setTimeout(
220
+ () =>
221
+ reject(
222
+ Object.assign(
223
+ new Error("Owned process did not exit after SIGKILL"),
224
+ {
225
+ code: "OWNED_PROCESS_EXIT_TIMEOUT",
226
+ diagnostic: publishDiagnostic({
227
+ ...state(),
228
+ code: "OWNED_PROCESS_EXIT_TIMEOUT",
229
+ group: groupSnapshot(child.pid),
230
+ }),
231
+ },
232
+ ),
233
+ ),
234
+ 2000,
235
+ );
236
+ }),
237
+ ]);
238
+ } finally {
239
+ clearTimeout(timer);
240
+ }
241
+ return {
242
+ stopped: true,
243
+ status: signals.some((s) => s.result === "verified-zombie-only")
244
+ ? "zombie-only"
245
+ : "stopped",
246
+ ...state(),
247
+ };
248
+ } catch (error) {
249
+ child.unref?.();
250
+ throw error;
251
+ } finally {
252
+ if (exitListener) child.off("exit", exitListener);
253
+ child.stdout?.destroy();
254
+ child.stderr?.destroy();
255
+ }
256
+ })();
257
+ stopping.set(child, pending);
258
+ return pending;
259
+ }
260
+
261
+ /** Attempt each owned cleanup even when another resource fails. */
262
+ export async function cleanupOwned(tasks) {
263
+ const settled = await Promise.allSettled(
264
+ tasks.map((task) => Promise.resolve().then(task)),
265
+ );
266
+ const failures = settled
267
+ .filter((r) => r.status === "rejected")
268
+ .map((r) => r.reason);
269
+ if (failures.length) {
270
+ const error = new AggregateError(failures, "Owned resource cleanup failed");
271
+ error.code = "OWNED_RESOURCE_CLEANUP_FAILED";
272
+ error.diagnostics = failures.flatMap(failureDiagnostic);
273
+ throw error;
274
+ }
275
+ }
276
+ export function combineFailure(primary, cleanup) {
277
+ if (!primary) return cleanup;
278
+ const error = new Error(primary.message + "; " + cleanup.message, {
279
+ cause: primary,
280
+ });
281
+ error.code = primary.code || "EXECUTION_FAILED";
282
+ error.diagnostics = [
283
+ ...failureDiagnostic(primary),
284
+ ...failureDiagnostic(cleanup),
285
+ ];
286
+ return error;
287
+ }
@@ -334,7 +334,14 @@ export function buildVerdicts({
334
334
  method: null,
335
335
  location: location(hostDeps[0] || hostSites[0]),
336
336
  severity: "degraded",
337
- impact: "该依赖没有足够运行时或目录证据,不能确认可用或缺失",
337
+ evidence: {
338
+ status: observed.status,
339
+ hardInject: hard,
340
+ inHostCatalog: false,
341
+ },
342
+ impact: hard
343
+ ? "插件必需inject的激活前提尚未确认;需同profile服务观测,不能推断已缺失或已挂起"
344
+ : "该依赖没有足够运行时或目录证据,不能确认可用或缺失",
338
345
  basis: "未采样不等于不存在;仅列出待核实依赖",
339
346
  ruleSource: "default",
340
347
  ruleRef: null,
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  /** 语义化版本。改动它会同时影响三个入口的 `--version` 与报告 JSON 的 `toolVersion`。 */
32
- export const TOOL_VERSION = '0.1.3';
32
+ export const TOOL_VERSION = '0.1.4';
33
33
 
34
34
  /** 人类可读的一行,给 `--version` 用 */
35
35
  export function versionLine() {
@@ -1,6 +1,6 @@
1
1
  # 严重度规则表与维护
2
2
 
3
- 适用于0.1.3;公开版本以registry和Plan发布记录为准。当前契约见 [Plan](../docs/Plan.md),验证见 [自动统计](../docs/current-state.json)。后半保留2026-10-11旧提取器的依据,历史表不是当前覆盖率或运行时真值。
3
+ 适用于0.1.4;公开版本以registry和Plan发布记录为准。当前契约见 [Plan](../docs/Plan.md),验证见 [自动统计](../docs/current-state.json)。后半保留2026-10-11旧提取器的依据,历史表不是当前覆盖率或运行时真值。
4
4
 
5
5
  ## 文件与运行时消费者
6
6
 
@@ -28,7 +28,9 @@
28
28
 
29
29
  宿主、客户端及shared调用点必须分开。客户端方法不能拿宿主probe判缺失;同名服务也不保证同一语义。对源码归属不确定的Context/别名保留suspected。certainty与impactBasis分别表达证据强度和影响是否实测。
30
30
 
31
- 仓库独立业务队列将手审的功能调用区间内API风险与真实备注/收藏及侧栏保存/预览结果对照,保持业务、loader标签及预测隔离;不把整个插件的其它发现挪给本功能。正常/HTTP503故障组分别计分,限定样本不能改变生产报告中的certainty或impactBasis,也不是新增严重度规则的充分证据。复现见[Plan](../docs/Plan.md#52-日常检查与真实集成)。
31
+ 0.1.4补强:必需inject未观测保持degraded/suspected,仅提示激活前提未确认;同profile明确absent才推断激活障碍,optional/scoped依赖与方法缺失分别处理,不借业务答案调严重度。`--observe-services`另记服务预测身份/摘要,不覆盖静态FN,也不改变生产certainty或impactBasis。
32
+
33
+ 仓库独立业务队列将手审的功能调用区间内API风险与真实市场操作、侧栏HTTP/编辑器及有限模型回复结果对照,保持业务、loader标签及预测隔离;不把整个插件的其它发现挪给本功能。正常/自然失败与HTTP503/控件缺失故障组分别计分,限定样本不能改变生产报告中的certainty或impactBasis,也不是新增严重度规则的充分证据。复现见[Plan](../docs/Plan.md#52-日常检查与真实集成)。
32
34
 
33
35
  ## 如何更新
34
36
 
@@ -187,7 +189,7 @@ node tools/coverage.mjs --probe <报告> --write # 顺带刷新 rule
187
189
 
188
190
  ### F. 提取器已正确处理,无需规则(1 个)
189
191
 
190
- `betterSidebar` —— 它是插件自己 `ctx.provide()` 出来的服务,**提取器已正确记入 `providedServices`**(同类的还有 dshmarket 的 `market`),因此不会产生假缺口。`pet` / `agentTeams` / `companionQq` / `betterLocale` 这些**没有**被 `ctx.provide()` 捕获的(见 `docs/Plan.md` §2.1 的提供方证据约束),就落进了上面 A 组。
192
+ `betterSidebar` —— 它是插件自己 `ctx.provide()` 出来的服务,**提取器已正确记入 `providedServices`**(同类的还有 dshmarket 的 `market`),因此不会产生假缺口。`pet` / `agentTeams` / `companionQq` / `betterLocale` 这些**没有**被 `ctx.provide()` 捕获的(见 `docs/Plan.md` §2.2 的提供方证据约束),就落进了上面 A 组。
191
193
 
192
194
  ## 历史补充:已知提取误报
193
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-compat-check",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Checks installed DSH plugins against the current DSH version, with source locations, runtime evidence and coverage limits. Includes a compatibility panel and agent tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,7 +53,8 @@
53
53
  ],
54
54
  "repository": {
55
55
  "type": "git",
56
- "url": "git+https://github.com/lcy0121/dsh-plugin-compat-check.git"
56
+ "url": "git+https://github.com/lcy0121/dsh-plugin-compat-check.git",
57
+ "directory": "plugin"
57
58
  },
58
59
  "homepage": "https://github.com/lcy0121/dsh-plugin-compat-check#readme",
59
60
  "bugs": {
@@ -16,7 +16,7 @@ const REPO = path.resolve(PLUGIN, '..');
16
16
  const DEST = path.join(PLUGIN, 'kernel');
17
17
 
18
18
  // 插件半边真正 import 的内核模块(改 index.js 的 import 时要同步这里)
19
- const KERNEL_LIBS = fs.readdirSync(path.join(REPO,'lib')).filter(f=>f.endsWith('.mjs')&&!['probe-runner.mjs','debugger.mjs'].includes(f)).sort();
19
+ const KERNEL_LIBS = fs.readdirSync(path.join(REPO,'lib')).filter(f=>f.endsWith('.mjs')&&!['probe-runner.mjs','debugger.mjs','chrome-diagnostics.mjs'].includes(f)).sort();
20
20
 
21
21
  function bail(message) {
22
22
  process.stderr.write(`[stage-kernel] ${message}\n`);
package/skill/SKILL.md CHANGED
@@ -73,7 +73,7 @@ report_status=$?
73
73
  插件数: 4 有运行时面: 是
74
74
 
75
75
  采样说明: 旧格式探针未绑定 profile/DSH 版本;仅使用显式记录的服务,不推断缺项
76
- 工具版本: 0.1.3 证据覆盖有限: 4 个插件
76
+ 工具版本: 0.1.4 证据覆盖有限: 4 个插件
77
77
  结论: 可用 3 部分可用 1 不兼容 0 (其中 1 个「可用」是空洞的:它没有任何可检查的依赖)
78
78
 
79
79
  部分可用 dsh-keep-awake@0.2.0
@@ -142,6 +142,8 @@ CI 里可以直接用:`0` 过,`1`/`2` 按你的策略处理,`3` 是**工
142
142
  - `插件数: 0` + 退出码 `0` 是**最危险**的输出:它意味着一个插件都没检到,不是"全都好"。看到这个先回头确认 `--nm` 指对了没有。
143
143
 
144
144
  **3. 「激活成功」不等于「功能可用」。**
145
+ 必需inject仅在静态目录未找到时,解释为激活前提尚未确认;省略键/未采样不能推断缺失。同profile、版本及时效绑定的服务观测明确absent时,才可推断该激活前提不满足;这不等于某编辑方法已执行失败。optional或子作用域依赖不自动构成整个插件挂起,方法缺失须另列。不要用loader/业务标签倒填服务观测。
146
+
145
147
  插件可能通过`try/catch`或能力守卫处理缺失API,因此激活成功、日志干净既不能证明功能完备,也不能证明相应功能一定失效。具体业务行为须另行验收;`peerDependencies`/`engines.dsh`声明也不是功能证明,DSH官方peer版本闸门只检查semver范围,不验证每个API。
146
148
 
147
149
  ## 边界(不要越界承诺)