dsh-plugin-compat-check 0.1.6 → 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.
Files changed (46) hide show
  1. package/README.md +17 -30
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/index.js +1295 -550
  4. package/kernel/lib/api-semantics.mjs +223 -0
  5. package/kernel/lib/author-ci-budget.mjs +25 -0
  6. package/kernel/lib/author-ci-output.mjs +22 -0
  7. package/kernel/lib/author-ci-policy.mjs +6 -1
  8. package/kernel/lib/baseline.mjs +109 -38
  9. package/kernel/lib/catalog.mjs +11 -3
  10. package/kernel/lib/contracts.mjs +16 -1
  11. package/kernel/lib/execution-environment.mjs +52 -0
  12. package/kernel/lib/extract.mjs +57 -13
  13. package/kernel/lib/finding-guidance.mjs +6 -0
  14. package/kernel/lib/finding-review-store.mjs +11 -23
  15. package/kernel/lib/issue-report.mjs +2 -1
  16. package/kernel/lib/patch-mounts.mjs +232 -0
  17. package/kernel/lib/report-identity-worker.mjs +47 -26
  18. package/kernel/lib/report-identity.mjs +4 -3
  19. package/kernel/lib/report-service.mjs +118 -38
  20. package/kernel/lib/report.mjs +13 -17
  21. package/kernel/lib/source-analysis.mjs +258 -42
  22. package/kernel/lib/source-graph.mjs +353 -75
  23. package/kernel/lib/state-lock.mjs +206 -0
  24. package/kernel/lib/state-store.mjs +41 -30
  25. package/kernel/lib/static-input.mjs +111 -0
  26. package/kernel/lib/syntax.mjs +242 -20
  27. package/kernel/lib/upgrade-contract.mjs +1 -0
  28. package/kernel/lib/upgrade-input.mjs +9 -4
  29. package/kernel/lib/upgrade-preview.mjs +11 -15
  30. package/kernel/lib/upgrade-worker.mjs +10 -1
  31. package/kernel/lib/verdict.mjs +28 -5
  32. package/kernel/lib/version.mjs +2 -34
  33. package/kernel/lib/worker-analysis.mjs +74 -3
  34. package/kernel/rules/README.md +30 -208
  35. package/kernel/rules/dsh-0.2.1-rc.3-review.json +63 -0
  36. package/kernel/rules/dsh-api-catalog-0.2.1-rc.3.json +743 -0
  37. package/kernel/rules/severity.json +77 -0
  38. package/lib/client.js +4668 -3423
  39. package/lib/finding-guidance.js +6 -0
  40. package/lib/local-request.js +44 -0
  41. package/lib/report-contract.js +16 -1
  42. package/lib/upgrade-contract.js +1 -0
  43. package/package.json +3 -2
  44. package/scripts/prepublish-check.mjs +1 -22
  45. package/scripts/stage-contract.mjs +7 -0
  46. package/skill/SKILL.md +13 -12
@@ -2,19 +2,75 @@
2
2
  export class ReportService {
3
3
  constructor(
4
4
  compute,
5
- { ttlMs = 300000, clock = Date.now, identity = null, watchMs = 5000 } = {},
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 (!this.states.has(key))
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.state(key);
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 (!this.disposed && state.stale)
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.disposed) return;
117
+ if (!this.identity || !this.alive(state)) return;
60
118
  if (state.checking) return state.checking;
61
119
  state.checking = Promise.resolve()
62
- .then(() => this.identity(state.options))
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.disposed) return;
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 (!this.disposed) {
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 (!this.identity || state.watching || this.disposed) return;
149
+ if (state.watching || !this.alive(state)) return;
88
150
  state.watching = true;
89
151
  const schedule = () => {
90
- if (this.disposed) return;
91
- state.timer = setTimeout(async () => {
92
- state.timer = null;
93
- try {
94
- await this.observe(state);
95
- if (
96
- !this.disposed &&
97
- (state.stale || this.clock() - state.at > this.ttlMs)
98
- )
99
- void this.start(state, state.options);
100
- } catch {
101
- /* Expose observation errors and retry at the next bounded tick. */
102
- }
103
- schedule();
104
- }, this.watchMs);
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.disposed)
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.disposed || generation !== state.generation) return false;
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.disposed || generation !== state.generation) return false;
205
+ if (!this.alive(state) || generation !== state.generation)
206
+ return false;
133
207
  await this.observe(state);
134
- return !this.disposed && generation === state.generation;
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 (!this.disposed) {
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
- !this.disposed &&
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.disposed) return out;
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 state of this.states.values()) {
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
  }
@@ -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 { sealReportBody, createFailure, catalogLines } from "./contracts.mjs";
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.join(";").replace(/\s+/g, " ")
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 };