dsh-plugin-compat-check 0.0.0-stage → 0.1.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 +78 -2
- package/cordis.patch.yml +3 -0
- package/index.js +1584 -0
- package/kernel/lib/baseline.mjs +233 -0
- package/kernel/lib/extract.mjs +1643 -0
- package/kernel/lib/verdict.mjs +620 -0
- package/kernel/lib/version.mjs +37 -0
- package/kernel/rules/README.md +232 -0
- package/kernel/rules/dsh-api-catalog.json +689 -0
- package/kernel/rules/severity.json +1056 -0
- package/kernel/rules/usage-inventory.json +144 -0
- package/lib/client.js +562 -0
- package/package.json +55 -4
- package/scripts/prepublish-check.mjs +84 -0
- package/scripts/stage-kernel.mjs +68 -0
- package/scripts/stage-skill.mjs +53 -0
- package/scripts/unstage-kernel.mjs +21 -0
- package/skill/SKILL.md +149 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基线:回答「**变了没有**」,以及变了多少、往哪个方向变。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它(这不是「锦上添花」)
|
|
5
|
+
*
|
|
6
|
+
* 本产品的核心是"你现在装的东西跟这版 DSH 兼不兼容"。但一次体检只能回答
|
|
7
|
+
* **当前状态**,而用户真正会问的是:
|
|
8
|
+
*
|
|
9
|
+
* 「我昨天还是好的,今天怎么就不行了?」
|
|
10
|
+
*
|
|
11
|
+
* 这个问题的答案**不在任何单次输出里** —— 它是一次差分。触发差分的通常是
|
|
12
|
+
* 用户没做、也没注意到的事:DSH 升级了、某个插件自动更新了、pnpm 换了版本。
|
|
13
|
+
* 所以基线不是缓存,是**把"和上次比"变成一件不需要用户记得的事**。
|
|
14
|
+
*
|
|
15
|
+
* ## 两条硬约束
|
|
16
|
+
*
|
|
17
|
+
* 1. **只写自己的状态目录**(Plan.md §2 第 1 题:插件严格只读之外的唯一例外)。
|
|
18
|
+
* 不联网、不外传 —— 「我装了哪些插件」是本机信息。
|
|
19
|
+
* 2. **绝不把「没变」当成「没问题」。** 基线只回答差分;结论仍然来自判定引擎。
|
|
20
|
+
* 两者混在一份输出里,会让人以为"没有变化"等于"兼容"。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 基线的存储版本:结构变了就要能识别出旧基线,而不是读出一个错的差分 */
|
|
24
|
+
export const BASELINE_SCHEMA = 1;
|
|
25
|
+
|
|
26
|
+
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从一次判定结果里提取**用于比对的那部分**。
|
|
30
|
+
*
|
|
31
|
+
* 刻意**不含** `checked` / `clientSide` / `filtered` 这些台账:
|
|
32
|
+
* 它们会随提取器的实现细节变化,把它们放进基线会让差分充满噪音,
|
|
33
|
+
* 从而掩盖真正的变化("新增一条降级"会被几十行台账变动淹没)。
|
|
34
|
+
*
|
|
35
|
+
* @param {object} verdict buildVerdicts 的返回值
|
|
36
|
+
* @param {string} dshVersion 当前 DSH 版本(它变了,方法级结论理应跟着变)
|
|
37
|
+
*/
|
|
38
|
+
export function snapshotForBaseline(verdict, dshVersion) {
|
|
39
|
+
const plugins = {};
|
|
40
|
+
const names = Object.keys(verdict.plugins || {}).sort();
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const p = verdict.plugins[name];
|
|
43
|
+
plugins[name] = {
|
|
44
|
+
version: p.version ?? null,
|
|
45
|
+
verdict: p.verdict,
|
|
46
|
+
vacuous: p.vacuous === true,
|
|
47
|
+
// 发现按稳定键排序:顺序不该影响「变了没有」
|
|
48
|
+
findings: (p.findings || [])
|
|
49
|
+
.map((f) => ({
|
|
50
|
+
key: f.service ? (f.method ? `${f.service}.${f.method}` : f.service) : '(declaration)',
|
|
51
|
+
category: f.category,
|
|
52
|
+
severity: f.severity,
|
|
53
|
+
certainty: f.certainty,
|
|
54
|
+
location: f.location ? `${f.location.file}:${f.location.line}` : null,
|
|
55
|
+
}))
|
|
56
|
+
.sort((a, b) => a.key.localeCompare(b.key) || a.category.localeCompare(b.category)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { schemaVersion: BASELINE_SCHEMA, dshVersion: dshVersion ?? null, plugins };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 结论档位的"好坏"序:用于判断某个变化是变好还是变差 */
|
|
63
|
+
const RANK = { broken: 0, partial: 1, unknown: 2, ok: 3 };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 比出「变了什么」,并且**给每个变化一个方向**。
|
|
67
|
+
*
|
|
68
|
+
* 方向不是装饰:用户看到"角标亮了"之后的下一个问题是"我该高兴还是该担心"。
|
|
69
|
+
* 一个不带方向的角标只制造焦虑。
|
|
70
|
+
*
|
|
71
|
+
* 变化分五类,每一类都要能单独说清:
|
|
72
|
+
* - `added` 新装了插件
|
|
73
|
+
* - `removed` 卸载了插件
|
|
74
|
+
* - `worsened` 同一个插件变差(档位下降,或新增了发现)
|
|
75
|
+
* - `improved` 同一个插件变好(档位上升,或少了发现)
|
|
76
|
+
* - `dshChanged` DSH 版本变了 —— 它单独一类,因为它常常是上面几类的**原因**
|
|
77
|
+
*/
|
|
78
|
+
export function diffAgainstBaseline(prev, next) {
|
|
79
|
+
const out = {
|
|
80
|
+
comparable: false,
|
|
81
|
+
reason: null,
|
|
82
|
+
dshVersionChanged: false,
|
|
83
|
+
dshVersionFrom: null,
|
|
84
|
+
dshVersionTo: null,
|
|
85
|
+
added: [],
|
|
86
|
+
removed: [],
|
|
87
|
+
worsened: [],
|
|
88
|
+
improved: [],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (!prev || prev.schemaVersion !== BASELINE_SCHEMA) {
|
|
92
|
+
out.reason = prev
|
|
93
|
+
? `基线结构版本不匹配(存的 ${prev.schemaVersion},当前认 ${BASELINE_SCHEMA})`
|
|
94
|
+
: '还没有基线(这是第一次体检)';
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
out.comparable = true;
|
|
98
|
+
out.dshVersionFrom = prev.dshVersion ?? null;
|
|
99
|
+
out.dshVersionTo = next.dshVersion ?? null;
|
|
100
|
+
out.dshVersionChanged = (prev.dshVersion ?? null) !== (next.dshVersion ?? null);
|
|
101
|
+
|
|
102
|
+
const a = prev.plugins || {};
|
|
103
|
+
const b = next.plugins || {};
|
|
104
|
+
|
|
105
|
+
for (const name of Object.keys(a)) if (!has(b, name)) out.removed.push({ name, version: a[name].version });
|
|
106
|
+
for (const name of Object.keys(b)) if (!has(a, name)) out.added.push({ name, version: b[name].version });
|
|
107
|
+
|
|
108
|
+
for (const name of Object.keys(a)) {
|
|
109
|
+
if (!has(b, name)) continue;
|
|
110
|
+
const x = a[name];
|
|
111
|
+
const y = b[name];
|
|
112
|
+
// 版本变了但结论没变 —— 也要记,否则用户会以为"什么都没发生"
|
|
113
|
+
const versionChanged = x.version !== y.version;
|
|
114
|
+
|
|
115
|
+
const verdictRank = { from: x.verdict, to: y.verdict };
|
|
116
|
+
const worseVerdict = RANK[y.verdict] < RANK[x.verdict];
|
|
117
|
+
const betterVerdict = RANK[y.verdict] > RANK[x.verdict];
|
|
118
|
+
|
|
119
|
+
const xk = new Set(x.findings.map((f) => `${f.key}|${f.category}`));
|
|
120
|
+
const yk = new Set(y.findings.map((f) => `${f.key}|${f.category}`));
|
|
121
|
+
const newFindings = y.findings.filter((f) => !xk.has(`${f.key}|${f.category}`));
|
|
122
|
+
const goneFindings = x.findings.filter((f) => !yk.has(`${f.key}|${f.category}`));
|
|
123
|
+
|
|
124
|
+
const entry = {
|
|
125
|
+
name,
|
|
126
|
+
versionFrom: x.version,
|
|
127
|
+
versionTo: y.version,
|
|
128
|
+
versionChanged,
|
|
129
|
+
verdictFrom: x.verdict,
|
|
130
|
+
verdictTo: y.verdict,
|
|
131
|
+
newFindings,
|
|
132
|
+
goneFindings,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 变差:档位下降,或**新增了发现**(档位可能没动,例如两条降级变三条)。
|
|
136
|
+
// 注意 `newFindings.length` 优先于档位判断 —— 档位相同但多了发现也是变差。
|
|
137
|
+
if (worseVerdict || newFindings.length > 0) {
|
|
138
|
+
entry.reason = worseVerdict
|
|
139
|
+
? `结论从「${x.verdict}」变为「${y.verdict}」`
|
|
140
|
+
: `新增 ${newFindings.length} 条发现(档位未变)`
|
|
141
|
+
out.worsened.push(entry);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (betterVerdict || goneFindings.length > 0) {
|
|
145
|
+
entry.reason = betterVerdict
|
|
146
|
+
? `结论从「${x.verdict}」变为「${y.verdict}」`
|
|
147
|
+
: `少了 ${goneFindings.length} 条发现(档位未变)`
|
|
148
|
+
out.improved.push(entry);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// 什么都没变,但版本动了 —— 这也是一条要说的事
|
|
152
|
+
if (versionChanged) {
|
|
153
|
+
entry.reason = '版本变了,但结论与发现都没变'
|
|
154
|
+
out.improved.push(entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 稳定排序:角标与列表的顺序不该随对象键顺序变化
|
|
159
|
+
for (const k of ['added', 'removed', 'worsened', 'improved']) {
|
|
160
|
+
out[k].sort((p, q) => p.name.localeCompare(q.name));
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 角标该显示什么。
|
|
167
|
+
*
|
|
168
|
+
* 规则:**任何变化都打**,并标明方向(Plan.md §2 第 5 题)。
|
|
169
|
+
* 只有"变差"才亮会漏掉"变好" —— 而"某个插件被修好了、你可以放心留着了"
|
|
170
|
+
* 同样是用户需要知道的事。
|
|
171
|
+
*
|
|
172
|
+
* @returns {{show: boolean, text: string|null, direction: 'worse'|'better'|'mixed'|'neutral'|null, detail: string}}
|
|
173
|
+
*/
|
|
174
|
+
export function badgeFor(diff) {
|
|
175
|
+
if (!diff || !diff.comparable) {
|
|
176
|
+
// 没有基线不是"没变化" —— 它意味着**还比不了**。角标不打,但要说清为什么。
|
|
177
|
+
return { show: false, text: null, direction: null, detail: diff ? diff.reason : '没有基线' };
|
|
178
|
+
}
|
|
179
|
+
const w = diff.worsened.length;
|
|
180
|
+
const b = diff.improved.length;
|
|
181
|
+
const add = diff.added.length;
|
|
182
|
+
const rm = diff.removed.length;
|
|
183
|
+
|
|
184
|
+
if (w === 0 && b === 0 && add === 0 && rm === 0) {
|
|
185
|
+
return {
|
|
186
|
+
show: false, text: null, direction: 'neutral',
|
|
187
|
+
detail: diff.dshVersionChanged
|
|
188
|
+
? `DSH 从 ${diff.dshVersionFrom ?? '?'} 变为 ${diff.dshVersionTo ?? '?'},但所有插件的结论都没变`
|
|
189
|
+
: '与上次相比没有任何变化',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const bits = [];
|
|
194
|
+
if (w) bits.push(`变差 ${w}`);
|
|
195
|
+
if (b) bits.push(`变好 ${b}`);
|
|
196
|
+
if (add) bits.push(`新增 ${add}`);
|
|
197
|
+
if (rm) bits.push(`移除 ${rm}`);
|
|
198
|
+
const direction = w > 0 && b > 0 ? 'mixed' : (w > 0 ? 'worse' : (b > 0 ? 'better' : 'neutral'));
|
|
199
|
+
const arrow = direction === 'worse' ? '↓' : (direction === 'better' ? '↑' : '•');
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
show: true,
|
|
203
|
+
text: `${arrow} ${bits.join(' · ')}`,
|
|
204
|
+
direction,
|
|
205
|
+
detail: diff.dshVersionChanged
|
|
206
|
+
? `DSH 版本也从 ${diff.dshVersionFrom ?? '?'} 变成了 ${diff.dshVersionTo ?? '?'} —— 这常常是上面这些变化的原因`
|
|
207
|
+
: 'DSH 版本没变',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 一句话总结这次差分 —— 给对话工具与面板用。
|
|
213
|
+
*
|
|
214
|
+
* 刻意把「变差」放在最前面:用户最需要先知道的是坏消息,
|
|
215
|
+
* 而坏消息被"另外还发现两个插件变好了"稀释掉,就等于没说。
|
|
216
|
+
*/
|
|
217
|
+
export function describeDiff(diff) {
|
|
218
|
+
if (!diff || !diff.comparable) return diff ? diff.reason : '没有基线';
|
|
219
|
+
const badge = badgeFor(diff);
|
|
220
|
+
if (!badge.show) return badge.detail;
|
|
221
|
+
const L = [badge.text];
|
|
222
|
+
for (const e of diff.worsened) {
|
|
223
|
+
L.push(` ↓ ${e.name}@${e.versionFrom}→${e.versionTo}:${e.reason}`);
|
|
224
|
+
for (const f of e.newFindings) L.push(` 新增:${f.key} @ ${f.location || '—'}`);
|
|
225
|
+
}
|
|
226
|
+
for (const e of diff.improved) {
|
|
227
|
+
L.push(` ↑ ${e.name}:${e.reason}`);
|
|
228
|
+
for (const f of e.goneFindings) L.push(` 不再报:${f.key}`);
|
|
229
|
+
}
|
|
230
|
+
for (const e of diff.added) L.push(` + ${e.name}@${e.version}`);
|
|
231
|
+
for (const e of diff.removed) L.push(` − ${e.name}@${e.version}`);
|
|
232
|
+
return L.join('\n');
|
|
233
|
+
}
|