dsh-plugin-compat-check 0.1.2 → 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 +21 -21
- package/index.js +58 -10
- package/kernel/lib/catalog.mjs +16 -0
- package/kernel/lib/declarations.mjs +100 -1
- package/kernel/lib/extract.mjs +5 -0
- package/kernel/lib/owned-process.mjs +287 -0
- package/kernel/lib/profile-compatibility.mjs +69 -0
- package/kernel/lib/verdict.mjs +55 -14
- package/kernel/lib/version.mjs +1 -1
- package/kernel/rules/README.md +11 -5
- package/kernel/rules/dsh-api-catalog-0.2.1-rc.2.json +743 -0
- package/package.json +3 -2
- package/scripts/stage-kernel.mjs +1 -1
- package/skill/SKILL.md +22 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Read independent profile grants without importing or changing target plugins. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
|
|
6
|
+
const exact = (value) => {
|
|
7
|
+
const parsed = typeof value === "string" ? semver.parse(value) : null;
|
|
8
|
+
return (
|
|
9
|
+
parsed !== null &&
|
|
10
|
+
value ===
|
|
11
|
+
parsed.version + (parsed.build.length ? "+" + parsed.build.join(".") : "")
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
export function readProfileCompatibility(nodeModules) {
|
|
15
|
+
const file = path.join(path.dirname(nodeModules), "compatibility.json");
|
|
16
|
+
const unavailable = (note) => ({
|
|
17
|
+
file,
|
|
18
|
+
exemptions: undefined,
|
|
19
|
+
notes: [note],
|
|
20
|
+
});
|
|
21
|
+
let fd;
|
|
22
|
+
try {
|
|
23
|
+
fd = fs.openSync(
|
|
24
|
+
file,
|
|
25
|
+
fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0),
|
|
26
|
+
);
|
|
27
|
+
if (!fs.fstatSync(fd).isFile())
|
|
28
|
+
return unavailable("compatibility.json 不是普通文件,精确豁免未观测");
|
|
29
|
+
const buffer = Buffer.alloc(65537),
|
|
30
|
+
length = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
31
|
+
if (length > 65536)
|
|
32
|
+
return unavailable(
|
|
33
|
+
"compatibility.json 超过64KiB读取预算,精确豁免未观测",
|
|
34
|
+
);
|
|
35
|
+
const value = JSON.parse(buffer.subarray(0, length).toString("utf8"));
|
|
36
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
37
|
+
return unavailable(
|
|
38
|
+
"compatibility.json 必须为精确版本豁免对象,记录未采用",
|
|
39
|
+
);
|
|
40
|
+
const exemptions = Object.create(null),
|
|
41
|
+
notes = [];
|
|
42
|
+
for (const [key, versions] of Object.entries(value)) {
|
|
43
|
+
const i = key.lastIndexOf("@");
|
|
44
|
+
if (
|
|
45
|
+
i <= 0 ||
|
|
46
|
+
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(
|
|
47
|
+
key.slice(0, i),
|
|
48
|
+
) ||
|
|
49
|
+
!exact(key.slice(i + 1)) ||
|
|
50
|
+
!Array.isArray(versions) ||
|
|
51
|
+
!versions.every(exact)
|
|
52
|
+
) {
|
|
53
|
+
notes.push("compatibility.json 含无效的精确版本豁免记录,已忽略");
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
Object.defineProperty(exemptions, key, {
|
|
57
|
+
value: versions,
|
|
58
|
+
enumerable: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return { file, exemptions, notes };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return error.code === "ENOENT"
|
|
64
|
+
? { file, exemptions: {}, notes: [] }
|
|
65
|
+
: unavailable("compatibility.json 不可读或JSON无效,精确豁免未观测");
|
|
66
|
+
} finally {
|
|
67
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/kernel/lib/verdict.mjs
CHANGED
|
@@ -99,8 +99,8 @@ export function methodRule(rules, service, method, side = "host") {
|
|
|
99
99
|
return null;
|
|
100
100
|
return rule;
|
|
101
101
|
}
|
|
102
|
-
export function declarationOf(p, version) {
|
|
103
|
-
return declaredSupport(p, version);
|
|
102
|
+
export function declarationOf(p, version, options) {
|
|
103
|
+
return declaredSupport(p, version, options);
|
|
104
104
|
}
|
|
105
105
|
export function buildVerdicts({
|
|
106
106
|
report,
|
|
@@ -110,6 +110,8 @@ export function buildVerdicts({
|
|
|
110
110
|
dshVersion = null,
|
|
111
111
|
entryStates = [],
|
|
112
112
|
observationTarget = {},
|
|
113
|
+
compatibilityExemptions,
|
|
114
|
+
compatibilityNotes = [],
|
|
113
115
|
}) {
|
|
114
116
|
const observation = normalizeObservation(hostSurface, {
|
|
115
117
|
dshVersion,
|
|
@@ -146,6 +148,7 @@ export function buildVerdicts({
|
|
|
146
148
|
const resolved = Object.create(null);
|
|
147
149
|
const limitations = [
|
|
148
150
|
...observation.notes,
|
|
151
|
+
...compatibilityNotes,
|
|
149
152
|
...(p.diagnostics || []).map((d) => `${d.kind}: ${d.file ?? ""}`),
|
|
150
153
|
];
|
|
151
154
|
let verified = 0;
|
|
@@ -331,7 +334,14 @@ export function buildVerdicts({
|
|
|
331
334
|
method: null,
|
|
332
335
|
location: location(hostDeps[0] || hostSites[0]),
|
|
333
336
|
severity: "degraded",
|
|
334
|
-
|
|
337
|
+
evidence: {
|
|
338
|
+
status: observed.status,
|
|
339
|
+
hardInject: hard,
|
|
340
|
+
inHostCatalog: false,
|
|
341
|
+
},
|
|
342
|
+
impact: hard
|
|
343
|
+
? "插件必需inject的激活前提尚未确认;需同profile服务观测,不能推断已缺失或已挂起"
|
|
344
|
+
: "该依赖没有足够运行时或目录证据,不能确认可用或缺失",
|
|
335
345
|
basis: "未采样不等于不存在;仅列出待核实依赖",
|
|
336
346
|
ruleSource: "default",
|
|
337
347
|
ruleRef: null,
|
|
@@ -422,14 +432,32 @@ export function buildVerdicts({
|
|
|
422
432
|
(s) => methods.includes(s.method) || protectedCall(s),
|
|
423
433
|
).length;
|
|
424
434
|
}
|
|
425
|
-
const declaration = declarationOf(p, dshVersion
|
|
435
|
+
const declaration = declarationOf(p, dshVersion, {
|
|
436
|
+
exemptions: compatibilityExemptions,
|
|
437
|
+
});
|
|
426
438
|
const rejectedPeers =
|
|
439
|
+
declaration?.rejected ??
|
|
427
440
|
declaration?.raw?.peers?.filter(
|
|
428
441
|
(r) => r.source === "peerDependencies" && !r.compatible,
|
|
429
|
-
)
|
|
430
|
-
|
|
442
|
+
) ??
|
|
443
|
+
[];
|
|
444
|
+
const modern = declaration?.policy === "engines-first";
|
|
445
|
+
if (modern && rejectedPeers.length && declaration.exempted === true)
|
|
446
|
+
limitations.push(
|
|
447
|
+
"声明范围未满足,但存在此插件与目标DSH的精确版本豁免;加载许可不证明API或业务兼容",
|
|
448
|
+
);
|
|
449
|
+
if (
|
|
450
|
+
(rejectedPeers.length || declaration?.invalid) &&
|
|
451
|
+
declaration?.exempted !== true
|
|
452
|
+
) {
|
|
431
453
|
limitations.push(
|
|
432
|
-
|
|
454
|
+
modern
|
|
455
|
+
? declaration.invalid
|
|
456
|
+
? declaration.invalid
|
|
457
|
+
: "声明范围不匹配;精确版本豁免" +
|
|
458
|
+
(declaration.exempted === null ? "未观测" : "未授予") +
|
|
459
|
+
",未实测加载结果"
|
|
460
|
+
: "官方 peer 范围不匹配;未读取 exact-version 豁免,不能断言实际加载失败",
|
|
433
461
|
);
|
|
434
462
|
add({
|
|
435
463
|
category: CATEGORY.DECLARED_INCOMPATIBLE,
|
|
@@ -439,18 +467,31 @@ export function buildVerdicts({
|
|
|
439
467
|
method: null,
|
|
440
468
|
location: null,
|
|
441
469
|
severity: "degraded",
|
|
442
|
-
impact:
|
|
443
|
-
"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.
|
|
470
|
+
impact: modern
|
|
471
|
+
? "按目标DSH声明规则未获放行;须核对精确豁免及实际加载,API存在不等于完整兼容"
|
|
472
|
+
: "官方 peer 声明不支持当前 DSH;请确认版本范围与精确版本豁免,API 存在不等于完整兼容",
|
|
473
|
+
basis:
|
|
474
|
+
declaration.invalid ||
|
|
475
|
+
rejectedPeers
|
|
476
|
+
.map((r) => `${r.name}: ${r.range},当前 ${dshVersion}`)
|
|
477
|
+
.join("; "),
|
|
447
478
|
evidence: {
|
|
448
479
|
runtimeVersion: dshVersion,
|
|
449
480
|
peers: rejectedPeers,
|
|
450
|
-
exemption:
|
|
481
|
+
exemption: modern
|
|
482
|
+
? declaration.exempted === null
|
|
483
|
+
? "unobserved"
|
|
484
|
+
: "not-granted"
|
|
485
|
+
: "unobserved",
|
|
486
|
+
policy: declaration.policy ?? "legacy-peers",
|
|
487
|
+
invalid: declaration.invalid ?? null,
|
|
451
488
|
},
|
|
452
489
|
ruleSource: "declaration",
|
|
453
|
-
ruleRef:
|
|
490
|
+
ruleRef: declaration.invalid
|
|
491
|
+
? "package.declaration"
|
|
492
|
+
: rejectedPeers.some((r) => r.source === "engines.dsh")
|
|
493
|
+
? "package.engines.dsh"
|
|
494
|
+
: "package.peerDependencies",
|
|
454
495
|
});
|
|
455
496
|
}
|
|
456
497
|
if (declaration?.claimsCompatible && findings.length) {
|
package/kernel/lib/version.mjs
CHANGED
package/kernel/rules/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# 严重度规则表与维护
|
|
2
2
|
|
|
3
|
-
适用于0.1.
|
|
3
|
+
适用于0.1.4;公开版本以registry和Plan发布记录为准。当前契约见 [Plan](../docs/Plan.md),验证见 [自动统计](../docs/current-state.json)。后半保留2026-10-11旧提取器的依据,历史表不是当前覆盖率或运行时真值。
|
|
4
4
|
|
|
5
5
|
## 文件与运行时消费者
|
|
6
6
|
|
|
7
7
|
| 文件 | 用途 |
|
|
8
8
|
|---|---|
|
|
9
9
|
| severity.json | 方法严重度/影响文案;引擎读取services下的方法规则,并与代码内默认档合用 |
|
|
10
|
-
| dsh-api-catalog.json |
|
|
10
|
+
| dsh-api-catalog.json / dsh-api-catalog-0.2.1-rc.2.json | 原0.2.0-rc.2(host91/client8)与新rc.2(host98/client8)契约快照;DSH≥0.2.1-rc.2选新目录,其余/未知选旧目录,CLI显式覆盖优先;未来版本未认证 |
|
|
11
11
|
| usage-inventory.json | 带日期/版本/profile及完整性标记的AST库存快照;由coverage显式刷新,不保证与后续提取器重扫同数 |
|
|
12
12
|
|
|
13
13
|
严重度由 `lib/verdict.mjs` 的确定性逻辑、缺失证据、方法规则及默认档共同决定,不由LLM决定。方法规则的severity/impact参与判定与输出,layer控制端别:方法自身优先、服务layer回退。summary/basis/evidence/knownGoneIn/needsReview等用于解释与维护;knownExtractionArtifacts是历史反例登记,不是自动过滤黑名单。
|
|
14
14
|
|
|
15
|
-
0.2.1-rc.1
|
|
15
|
+
0.1.3按DSH目标分流声明:从0.2.1-rc.1起自有engines.dsh优先,缺失才回退全部官方peer;旧目标保留旧行为。非法/空范围、预发布和peer workspace别名均有回归;engines中的workspace不获放行。独立compatibility.json仅按插件/运行时精确版本豁免,非法字段不能靠豁免绕过;无效记录忽略并保留覆盖限制。
|
|
16
16
|
|
|
17
17
|
## 当前证据与严重度
|
|
18
18
|
|
|
@@ -21,15 +21,21 @@
|
|
|
21
21
|
| 可映射loader条目FAILED/PENDING | fatal,影响依据measured;禁用、LOADING、无法映射不冒充同一事实 |
|
|
22
22
|
| 宿主硬依赖且明确absent | 可判fatal;仅catalog缺项不冒充运行时缺失 |
|
|
23
23
|
| 非硬依赖服务明确absent,或有足够方法证据 | 按规则/默认档给feature-dead或degraded;用户影响通常是inferred |
|
|
24
|
-
|
|
|
24
|
+
| 官方声明冲突或非法字段 | declared-incompatible,degraded及推断影响;不冒充实测加载失败;有效精确豁免可消除范围冲突,但保留限制且不证明业务成功 |
|
|
25
25
|
| 未采样、读取失败、访问器未知、源码不完整 | 保留覆盖限制;不以省略键认定不存在 |
|
|
26
26
|
|
|
27
27
|
方法规则中的degraded应有具体降级/回退依据;peer声明冲突的degraded是引擎针对声明风险的独立默认,不代表观察到回退。方法缺失但没有语义规则时会明确“影响面未知”。声明provide只是候选提供方,不证明已激活,不能遮住完整的present方法观测。
|
|
28
28
|
|
|
29
29
|
宿主、客户端及shared调用点必须分开。客户端方法不能拿宿主probe判缺失;同名服务也不保证同一语义。对源码归属不确定的Context/别名保留suspected。certainty与impactBasis分别表达证据强度和影响是否实测。
|
|
30
30
|
|
|
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-日常检查与真实集成)。
|
|
34
|
+
|
|
31
35
|
## 如何更新
|
|
32
36
|
|
|
37
|
+
ASAR读取器已修正数据起点为8+headerSize,并校验头长、边界、完整读取、UTF-8/JSON及归档SHA256。2026-10-18与Electron原生ASAR读取逐字节及首尾交叉校验后生成独立rc.2目录,旧目录未改。新host增加8项、移除invariants;subagents移除start/startContinuable/drainContinuableChildren/drainContinuableDescendants,增加startActivation/drainChildren/drainDescendants/waitForChildren;其它方法差异和源摘要见current-state。client方法集合未变。旧方法规则保留其原调用语义;不把新方法当同义重命名,也不只凭目录变化新增严重度,未知影响继续回退。
|
|
38
|
+
|
|
33
39
|
先读调用点和目标版本的真实描述符/官方源码,再修改规则,添加行为回归并审查快照差异。原needsReview已在2026-10-15逐条复核,决定与证据保留于severity.json的needsReview.reviewed/evidence及 [Plan](../docs/Plan.md)。
|
|
34
40
|
|
|
35
41
|
`tools/coverage.mjs` 从AST调用点按host/client/shared分端计数,规则命中与默认影响、类别发现分列。**knownExtractionArtifacts只保留历史反例,不过滤现在的真实调用**;纯客户端规则不用于宿主同名方法。probe须为现代格式并绑定本目标/版本/时间,不能借别的profile的面。写库存要求显式或观测到的合法semver版本,非法版本退出3且不覆盖旧文件。
|
|
@@ -183,7 +189,7 @@ node tools/coverage.mjs --probe <报告> --write # 顺带刷新 rule
|
|
|
183
189
|
|
|
184
190
|
### F. 提取器已正确处理,无需规则(1 个)
|
|
185
191
|
|
|
186
|
-
`betterSidebar` —— 它是插件自己 `ctx.provide()` 出来的服务,**提取器已正确记入 `providedServices`**(同类的还有 dshmarket 的 `market`),因此不会产生假缺口。`pet` / `agentTeams` / `companionQq` / `betterLocale` 这些**没有**被 `ctx.provide()` 捕获的(见 `docs/Plan.md` §2.
|
|
192
|
+
`betterSidebar` —— 它是插件自己 `ctx.provide()` 出来的服务,**提取器已正确记入 `providedServices`**(同类的还有 dshmarket 的 `market`),因此不会产生假缺口。`pet` / `agentTeams` / `companionQq` / `betterLocale` 这些**没有**被 `ctx.provide()` 捕获的(见 `docs/Plan.md` §2.2 的提供方证据约束),就落进了上面 A 组。
|
|
187
193
|
|
|
188
194
|
## 历史补充:已知提取误报
|
|
189
195
|
|