dsh-plugin-compat-check 0.1.5 → 0.1.7
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 +17 -28
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/index.js +1295 -550
- package/kernel/lib/api-semantics.mjs +223 -0
- package/kernel/lib/author-ci-budget.mjs +25 -0
- package/kernel/lib/author-ci-output.mjs +22 -0
- package/kernel/lib/author-ci-policy.mjs +6 -1
- package/kernel/lib/baseline.mjs +109 -38
- package/kernel/lib/catalog.mjs +11 -3
- package/kernel/lib/contracts.mjs +16 -1
- package/kernel/lib/execution-environment.mjs +52 -0
- package/kernel/lib/extract.mjs +57 -13
- package/kernel/lib/finding-guidance.mjs +6 -0
- package/kernel/lib/finding-review-store.mjs +11 -23
- package/kernel/lib/issue-report.mjs +2 -1
- package/kernel/lib/patch-mounts.mjs +232 -0
- package/kernel/lib/report-identity-worker.mjs +47 -26
- package/kernel/lib/report-identity.mjs +4 -3
- package/kernel/lib/report-service.mjs +118 -38
- package/kernel/lib/report.mjs +13 -17
- package/kernel/lib/source-analysis.mjs +258 -42
- package/kernel/lib/source-graph.mjs +353 -75
- package/kernel/lib/state-lock.mjs +206 -0
- package/kernel/lib/state-store.mjs +41 -30
- package/kernel/lib/static-input.mjs +111 -0
- package/kernel/lib/syntax.mjs +242 -20
- package/kernel/lib/upgrade-contract.mjs +1 -0
- package/kernel/lib/upgrade-input.mjs +9 -4
- package/kernel/lib/upgrade-preview.mjs +11 -15
- package/kernel/lib/upgrade-worker.mjs +10 -1
- package/kernel/lib/verdict.mjs +28 -5
- package/kernel/lib/version.mjs +2 -34
- package/kernel/lib/worker-analysis.mjs +74 -3
- package/kernel/rules/README.md +30 -208
- package/kernel/rules/dsh-0.2.1-rc.3-review.json +63 -0
- package/kernel/rules/dsh-api-catalog-0.2.1-rc.3.json +743 -0
- package/kernel/rules/severity.json +77 -0
- package/lib/client.js +4668 -3423
- package/lib/finding-guidance.js +6 -0
- package/lib/local-request.js +44 -0
- package/lib/report-contract.js +16 -1
- package/lib/upgrade-contract.js +1 -0
- package/package.json +10 -5
- package/scripts/prepublish-check.mjs +1 -22
- package/scripts/stage-contract.mjs +7 -0
- package/skill/SKILL.md +13 -12
|
@@ -2,19 +2,75 @@
|
|
|
2
2
|
export class ReportService {
|
|
3
3
|
constructor(
|
|
4
4
|
compute,
|
|
5
|
-
{
|
|
5
|
+
{
|
|
6
|
+
ttlMs = 300000,
|
|
7
|
+
clock = Date.now,
|
|
8
|
+
identity = null,
|
|
9
|
+
watchMs = 5000,
|
|
10
|
+
maxTargets = 16,
|
|
11
|
+
idleMs = 600000,
|
|
12
|
+
onEvict = null,
|
|
13
|
+
} = {},
|
|
6
14
|
) {
|
|
15
|
+
if (
|
|
16
|
+
!Number.isSafeInteger(maxTargets) ||
|
|
17
|
+
maxTargets < 1 ||
|
|
18
|
+
maxTargets > 16 ||
|
|
19
|
+
!Number.isSafeInteger(idleMs) ||
|
|
20
|
+
idleMs < 1 ||
|
|
21
|
+
idleMs > 600000
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Invalid cache budget");
|
|
24
|
+
this.maxTargets = maxTargets;
|
|
25
|
+
this.idleMs = idleMs;
|
|
26
|
+
this.onEvict = onEvict;
|
|
7
27
|
this.compute = compute;
|
|
8
28
|
this.ttlMs = ttlMs;
|
|
9
29
|
this.clock = clock;
|
|
10
30
|
this.identity = identity;
|
|
11
31
|
this.watchMs = watchMs;
|
|
12
32
|
this.states = new Map();
|
|
33
|
+
this.requestOrder = 0;
|
|
13
34
|
this.disposed = false;
|
|
14
35
|
}
|
|
36
|
+
alive(state) {
|
|
37
|
+
return !this.disposed && !state.retired;
|
|
38
|
+
}
|
|
39
|
+
evict(key) {
|
|
40
|
+
const state = this.states.get(key);
|
|
41
|
+
if (!state) return;
|
|
42
|
+
state.retired = true;
|
|
43
|
+
state.generation++;
|
|
44
|
+
clearTimeout(state.timer);
|
|
45
|
+
state.timer = null;
|
|
46
|
+
state.controller.abort();
|
|
47
|
+
this.states.delete(key);
|
|
48
|
+
state.report = null;
|
|
49
|
+
state.options = {};
|
|
50
|
+
state.identity = undefined;
|
|
51
|
+
state.publishedIdentity = undefined;
|
|
52
|
+
this.onEvict?.(key);
|
|
53
|
+
}
|
|
54
|
+
prune() {
|
|
55
|
+
for (const [key, state] of this.states)
|
|
56
|
+
if (this.clock() - state.lastUsed >= this.idleMs) this.evict(key);
|
|
57
|
+
}
|
|
15
58
|
state(key) {
|
|
16
|
-
if (
|
|
59
|
+
if (this.disposed) return undefined;
|
|
60
|
+
if (!this.states.has(key)) {
|
|
61
|
+
this.prune();
|
|
62
|
+
if (this.states.size >= this.maxTargets) {
|
|
63
|
+
const oldest = [...this.states].sort(
|
|
64
|
+
(a, b) => a[1].lastRequest - b[1].lastRequest,
|
|
65
|
+
)[0][0];
|
|
66
|
+
this.evict(oldest);
|
|
67
|
+
}
|
|
17
68
|
this.states.set(key, {
|
|
69
|
+
key,
|
|
70
|
+
lastUsed: this.clock(),
|
|
71
|
+
lastRequest: ++this.requestOrder,
|
|
72
|
+
retired: false,
|
|
73
|
+
controller: new AbortController(),
|
|
18
74
|
report: null,
|
|
19
75
|
at: 0,
|
|
20
76
|
inflight: null,
|
|
@@ -26,11 +82,13 @@ export class ReportService {
|
|
|
26
82
|
options: {},
|
|
27
83
|
timer: null,
|
|
28
84
|
});
|
|
85
|
+
}
|
|
29
86
|
return this.states.get(key);
|
|
30
87
|
}
|
|
31
88
|
invalidate(key, reason = "报告输入已变化,等待刷新") {
|
|
32
89
|
if (this.disposed) return;
|
|
33
|
-
const state = this.
|
|
90
|
+
const state = this.states.get(key);
|
|
91
|
+
if (!state) return;
|
|
34
92
|
state.generation++;
|
|
35
93
|
state.stale = true;
|
|
36
94
|
state.reason = reason;
|
|
@@ -41,10 +99,10 @@ export class ReportService {
|
|
|
41
99
|
if (this.disposed || this.notifying) return;
|
|
42
100
|
this.notifying = Promise.resolve()
|
|
43
101
|
.then(async () => {
|
|
44
|
-
for (const state of this.states.values()) {
|
|
102
|
+
for (const state of [...this.states.values()]) {
|
|
45
103
|
try {
|
|
46
104
|
await this.observe(state);
|
|
47
|
-
if (
|
|
105
|
+
if (this.alive(state) && state.stale)
|
|
48
106
|
void this.start(state, state.options);
|
|
49
107
|
} catch {
|
|
50
108
|
/* Keep the valid report and let the watchdog retry. */
|
|
@@ -56,12 +114,16 @@ export class ReportService {
|
|
|
56
114
|
});
|
|
57
115
|
}
|
|
58
116
|
async observe(state) {
|
|
59
|
-
if (!this.identity || this.
|
|
117
|
+
if (!this.identity || !this.alive(state)) return;
|
|
60
118
|
if (state.checking) return state.checking;
|
|
61
119
|
state.checking = Promise.resolve()
|
|
62
|
-
.then(() =>
|
|
120
|
+
.then(() =>
|
|
121
|
+
this.alive(state)
|
|
122
|
+
? this.identity(state.options, { signal: state.controller.signal })
|
|
123
|
+
: undefined,
|
|
124
|
+
)
|
|
63
125
|
.then((identity) => {
|
|
64
|
-
if (this.
|
|
126
|
+
if (!this.alive(state)) return;
|
|
65
127
|
if (state.identity !== undefined && state.identity !== identity) {
|
|
66
128
|
state.generation++;
|
|
67
129
|
state.stale = true;
|
|
@@ -71,7 +133,7 @@ export class ReportService {
|
|
|
71
133
|
state.identity = identity;
|
|
72
134
|
})
|
|
73
135
|
.catch((error) => {
|
|
74
|
-
if (
|
|
136
|
+
if (this.alive(state)) {
|
|
75
137
|
state.generation++;
|
|
76
138
|
state.stale = true;
|
|
77
139
|
state.error = error.message;
|
|
@@ -84,30 +146,37 @@ export class ReportService {
|
|
|
84
146
|
return state.checking;
|
|
85
147
|
}
|
|
86
148
|
watch(state) {
|
|
87
|
-
if (
|
|
149
|
+
if (state.watching || !this.alive(state)) return;
|
|
88
150
|
state.watching = true;
|
|
89
151
|
const schedule = () => {
|
|
90
|
-
if (this.
|
|
91
|
-
state.timer = setTimeout(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
if (!this.alive(state)) return;
|
|
153
|
+
state.timer = setTimeout(
|
|
154
|
+
async () => {
|
|
155
|
+
state.timer = null;
|
|
156
|
+
if (this.clock() - state.lastUsed >= this.idleMs) {
|
|
157
|
+
this.evict(state.key);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await this.observe(state);
|
|
162
|
+
if (
|
|
163
|
+
this.alive(state) &&
|
|
164
|
+
(state.stale || this.clock() - state.at > this.ttlMs)
|
|
165
|
+
)
|
|
166
|
+
void this.start(state, state.options);
|
|
167
|
+
} catch {
|
|
168
|
+
/* Expose observation errors and retry at the next bounded tick. */
|
|
169
|
+
}
|
|
170
|
+
schedule();
|
|
171
|
+
},
|
|
172
|
+
Math.min(this.watchMs, this.idleMs),
|
|
173
|
+
);
|
|
105
174
|
state.timer.unref?.();
|
|
106
175
|
};
|
|
107
176
|
schedule();
|
|
108
177
|
}
|
|
109
178
|
start(state, options) {
|
|
110
|
-
if (this.
|
|
179
|
+
if (!this.alive(state))
|
|
111
180
|
return Promise.resolve({
|
|
112
181
|
ok: false,
|
|
113
182
|
error: "Plugin generation disposed / 已卸载",
|
|
@@ -117,21 +186,26 @@ export class ReportService {
|
|
|
117
186
|
let generation;
|
|
118
187
|
state.inflight = Promise.resolve()
|
|
119
188
|
.then(async () => {
|
|
189
|
+
if (!this.alive(state)) return { ok: false, error: "缓存目标已回收" };
|
|
120
190
|
if (this.identity) await this.observe(state);
|
|
191
|
+
if (!this.alive(state)) return { ok: false, error: "缓存目标已回收" };
|
|
121
192
|
generation = state.generation;
|
|
122
193
|
let commit;
|
|
123
194
|
const guard = {
|
|
195
|
+
signal: state.controller.signal,
|
|
124
196
|
commit: (fn) => {
|
|
125
197
|
commit = fn;
|
|
126
198
|
},
|
|
127
199
|
isCurrent: async () => {
|
|
128
|
-
if (this.
|
|
200
|
+
if (!this.alive(state) || generation !== state.generation)
|
|
201
|
+
return false;
|
|
129
202
|
// A watchdog observation may have sampled before computation
|
|
130
203
|
// finished. Drain it, then start a fresh check for this commit.
|
|
131
204
|
if (state.checking) await state.checking;
|
|
132
|
-
if (this.
|
|
205
|
+
if (!this.alive(state) || generation !== state.generation)
|
|
206
|
+
return false;
|
|
133
207
|
await this.observe(state);
|
|
134
|
-
return
|
|
208
|
+
return this.alive(state) && generation === state.generation;
|
|
135
209
|
},
|
|
136
210
|
};
|
|
137
211
|
const out = await this.compute(options, guard);
|
|
@@ -156,7 +230,7 @@ export class ReportService {
|
|
|
156
230
|
return out;
|
|
157
231
|
})
|
|
158
232
|
.catch((error) => {
|
|
159
|
-
if (
|
|
233
|
+
if (this.alive(state)) {
|
|
160
234
|
state.error = error.message;
|
|
161
235
|
state.stale = true;
|
|
162
236
|
}
|
|
@@ -165,7 +239,7 @@ export class ReportService {
|
|
|
165
239
|
.finally(() => {
|
|
166
240
|
state.inflight = null;
|
|
167
241
|
if (
|
|
168
|
-
|
|
242
|
+
this.alive(state) &&
|
|
169
243
|
generation !== undefined &&
|
|
170
244
|
generation !== state.generation &&
|
|
171
245
|
!state.error
|
|
@@ -177,7 +251,10 @@ export class ReportService {
|
|
|
177
251
|
async get(key, options = {}, force = false) {
|
|
178
252
|
if (this.disposed)
|
|
179
253
|
return { ok: false, error: "Plugin generation disposed / 已卸载" };
|
|
254
|
+
this.prune();
|
|
180
255
|
const state = this.state(key);
|
|
256
|
+
state.lastUsed = this.clock();
|
|
257
|
+
state.lastRequest = ++this.requestOrder;
|
|
181
258
|
state.options = options;
|
|
182
259
|
this.watch(state);
|
|
183
260
|
const age = this.clock() - state.at;
|
|
@@ -188,7 +265,7 @@ export class ReportService {
|
|
|
188
265
|
let out;
|
|
189
266
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
190
267
|
out = await this.start(state, options);
|
|
191
|
-
if (!out?.invalidated || this.
|
|
268
|
+
if (!out?.invalidated || !this.alive(state)) return out;
|
|
192
269
|
}
|
|
193
270
|
return { ok: false, error: "报告输入持续变化,请稍后重试" };
|
|
194
271
|
}
|
|
@@ -206,6 +283,13 @@ export class ReportService {
|
|
|
206
283
|
},
|
|
207
284
|
};
|
|
208
285
|
}
|
|
286
|
+
ownsReport(report) {
|
|
287
|
+
return (
|
|
288
|
+
!this.disposed &&
|
|
289
|
+
!!report &&
|
|
290
|
+
[...this.states.values()].some((state) => state.report?.report === report)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
209
293
|
/** The immutable identity observed for this published report, including retained stale reports. */
|
|
210
294
|
identityForReport(key, report) {
|
|
211
295
|
const state = this.states.get(key);
|
|
@@ -217,8 +301,8 @@ export class ReportService {
|
|
|
217
301
|
async isCurrent(key, report) {
|
|
218
302
|
const state = this.states.get(key);
|
|
219
303
|
const matches = () =>
|
|
220
|
-
!this.disposed &&
|
|
221
304
|
state &&
|
|
305
|
+
this.alive(state) &&
|
|
222
306
|
!state.stale &&
|
|
223
307
|
!state.error &&
|
|
224
308
|
state.report?.report === report;
|
|
@@ -231,10 +315,6 @@ export class ReportService {
|
|
|
231
315
|
}
|
|
232
316
|
dispose() {
|
|
233
317
|
this.disposed = true;
|
|
234
|
-
for (const
|
|
235
|
-
clearTimeout(state.timer);
|
|
236
|
-
state.timer = null;
|
|
237
|
-
state.generation++;
|
|
238
|
-
}
|
|
318
|
+
for (const key of this.states.keys()) this.evict(key);
|
|
239
319
|
}
|
|
240
320
|
}
|
package/kernel/lib/report.mjs
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 结论的**渲染**:给人看的表 + 给机器看的 JSON。
|
|
3
|
-
*
|
|
4
|
-
* ## 为什么单独一个文件
|
|
5
|
-
*
|
|
6
|
-
* 到 D1 为止,「一份结论」已经有三个消费方:CLI、对话工具、面板。
|
|
7
|
-
* 它们各自的**呈现**不同(终端表 / 文本 / DOM),但**取值口径**必须完全一致 ——
|
|
8
|
-
* 标签文案、排序、哪一类发现要标「疑似」、哪些"没发现"要额外解释。
|
|
9
|
-
* 把这些放在一处,是为了让"面板说可用、工具说部分可用"这类分叉**在结构上不可能发生**。
|
|
10
|
-
*
|
|
11
|
-
* 它不做判定。判定在 `lib/verdict.mjs`;这里只把判定结果翻译成人话。
|
|
12
|
-
*/
|
|
1
|
+
/** Shared report rendering for CLI, tools and UI; never changes verdict evidence. */
|
|
13
2
|
|
|
14
3
|
import { TOOL_VERSION } from "./version.mjs";
|
|
15
4
|
import path from "node:path";
|
|
16
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
sealReportBody,
|
|
7
|
+
createFailure,
|
|
8
|
+
catalogLines,
|
|
9
|
+
terminalText,
|
|
10
|
+
} from "./contracts.mjs";
|
|
17
11
|
import { DISPOSITION } from "./verdict.mjs";
|
|
18
12
|
import { describeDiff } from "./baseline.mjs";
|
|
19
13
|
import { nextSteps } from "./finding-guidance.mjs";
|
|
@@ -31,6 +25,8 @@ const SEVERITY_LABEL = {
|
|
|
31
25
|
degraded: "降级",
|
|
32
26
|
};
|
|
33
27
|
const CATEGORY_LABEL = {
|
|
28
|
+
"api-argument-invalid": "API参数不符",
|
|
29
|
+
"api-option-unsupported": "版本选项未支持",
|
|
34
30
|
"host-service-missing": "宿主服务缺失",
|
|
35
31
|
"host-method-missing": "宿主方法缺失",
|
|
36
32
|
"unprovided-dependency": "依赖服务待确认",
|
|
@@ -55,7 +51,7 @@ export function render(result, opts) {
|
|
|
55
51
|
|
|
56
52
|
if (result.error) {
|
|
57
53
|
L.push(`结论: 无法判定 —— ${result.error}`);
|
|
58
|
-
return L.join("\n");
|
|
54
|
+
return L.map(terminalText).join("\n");
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
const meta = extract.report.manifests;
|
|
@@ -72,10 +68,10 @@ export function render(result, opts) {
|
|
|
72
68
|
catalogLines(result.catalog).join(";") +
|
|
73
69
|
(verdict.observation?.notes?.length
|
|
74
70
|
? ";采样说明:" +
|
|
75
|
-
verdict.observation.notes.
|
|
71
|
+
verdict.observation.notes.map(terminalText).join(";")
|
|
76
72
|
: ""),
|
|
77
73
|
);
|
|
78
|
-
return L.join("\n");
|
|
74
|
+
return L.map(terminalText).join("\n");
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
L.push(
|
|
@@ -235,7 +231,7 @@ export function render(result, opts) {
|
|
|
235
231
|
" 没有已验证调用仍可能存在服务依赖或客户端功能;调用核验、无发现和覆盖完整均不等于业务验收通过。",
|
|
236
232
|
);
|
|
237
233
|
L.push(" 下一步是核实建议;本工具不会自动安装、升级或修改配置与源码。");
|
|
238
|
-
return L.join("\n");
|
|
234
|
+
return L.map(terminalText).join("\n");
|
|
239
235
|
}
|
|
240
236
|
|
|
241
237
|
export { VERDICT_LABEL, SEVERITY_LABEL, CATEGORY_LABEL };
|