dsh-plugin-compat-check 0.1.3 → 0.1.5

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 (45) hide show
  1. package/README.md +36 -20
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/index.js +288 -55
  4. package/kernel/lib/author-ci-output.mjs +188 -0
  5. package/kernel/lib/author-ci-policy.mjs +285 -0
  6. package/kernel/lib/catalog.mjs +38 -0
  7. package/kernel/lib/contracts.mjs +88 -0
  8. package/kernel/lib/dependency-graph.mjs +650 -0
  9. package/kernel/lib/extract.mjs +14 -0
  10. package/kernel/lib/finding-guidance.mjs +72 -0
  11. package/kernel/lib/finding-review-store.mjs +245 -0
  12. package/kernel/lib/finding-review.mjs +338 -0
  13. package/kernel/lib/functional-health-contract.mjs +337 -0
  14. package/kernel/lib/issue-report.mjs +217 -0
  15. package/kernel/lib/observation.mjs +19 -9
  16. package/kernel/lib/owned-process.mjs +287 -0
  17. package/kernel/lib/report-identity-worker.mjs +153 -0
  18. package/kernel/lib/report-identity.mjs +15 -0
  19. package/kernel/lib/report-service.mjs +196 -11
  20. package/kernel/lib/report.mjs +76 -11
  21. package/kernel/lib/source-analysis.mjs +24 -6
  22. package/kernel/lib/upgrade-capture-worker.mjs +20 -0
  23. package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
  24. package/kernel/lib/upgrade-contract.mjs +471 -0
  25. package/kernel/lib/upgrade-input.mjs +309 -0
  26. package/kernel/lib/upgrade-preview.mjs +256 -0
  27. package/kernel/lib/upgrade-worker.mjs +50 -0
  28. package/kernel/lib/verdict.mjs +8 -1
  29. package/kernel/lib/version.mjs +1 -1
  30. package/kernel/lib/worker-analysis.mjs +16 -1
  31. package/kernel/lib/workflow-panels.mjs +706 -0
  32. package/kernel/rules/README.md +8 -6
  33. package/lib/client.js +2974 -69
  34. package/lib/dependency-graph.js +651 -0
  35. package/lib/finding-guidance.js +73 -0
  36. package/lib/finding-review.js +339 -0
  37. package/lib/functional-health-contract.js +338 -0
  38. package/lib/report-contract.js +88 -0
  39. package/lib/upgrade-contract.js +472 -0
  40. package/lib/workflow-panels.js +707 -0
  41. package/package.json +5 -3
  42. package/scripts/prepublish-check.mjs +28 -13
  43. package/scripts/stage-contract.mjs +91 -29
  44. package/scripts/stage-kernel.mjs +4 -3
  45. package/skill/SKILL.md +92 -39
@@ -13,9 +13,11 @@
13
13
 
14
14
  import { TOOL_VERSION } from "./version.mjs";
15
15
  import path from "node:path";
16
- import { sealReportBody, createFailure } from "./contracts.mjs";
16
+ import { sealReportBody, createFailure, catalogLines } from "./contracts.mjs";
17
17
  import { DISPOSITION } from "./verdict.mjs";
18
18
  import { describeDiff } from "./baseline.mjs";
19
+ import { nextSteps } from "./finding-guidance.mjs";
20
+ import { buildDependencyGraph } from "./dependency-graph.mjs";
19
21
 
20
22
  const VERDICT_LABEL = {
21
23
  ok: "可用",
@@ -31,7 +33,7 @@ const SEVERITY_LABEL = {
31
33
  const CATEGORY_LABEL = {
32
34
  "host-service-missing": "宿主服务缺失",
33
35
  "host-method-missing": "宿主方法缺失",
34
- "unprovided-dependency": "无人提供的依赖",
36
+ "unprovided-dependency": "依赖服务待确认",
35
37
  "declaration-mismatch": "声明与实测不符",
36
38
  "declared-incompatible": "版本声明不支持",
37
39
  "host-entry-inactive": "宿主条目未激活",
@@ -63,7 +65,15 @@ export function render(result, opts) {
63
65
  if (opts.quiet) {
64
66
  const q = verdict.summary;
65
67
  L.push(
66
- `结论: ${VERDICT_LABEL.ok} ${q.ok} ${VERDICT_LABEL.partial} ${q.partial} ${VERDICT_LABEL.broken} ${q.broken}`,
68
+ `结论: ${VERDICT_LABEL.ok} ${q.ok} ${VERDICT_LABEL.partial} ${q.partial} ${VERDICT_LABEL.broken} ${q.broken}` +
69
+ (q.limited ? ` 覆盖有限 ${q.limited}` : "") +
70
+ (q.vacuousOk ? ` 其中 ${q.vacuousOk} 个无已验证调用` : "") +
71
+ ";" +
72
+ catalogLines(result.catalog).join(";") +
73
+ (verdict.observation?.notes?.length
74
+ ? ";采样说明:" +
75
+ verdict.observation.notes.join(";").replace(/\s+/g, " ")
76
+ : ""),
67
77
  );
68
78
  return L.join("\n");
69
79
  }
@@ -71,6 +81,7 @@ export function render(result, opts) {
71
81
  L.push(
72
82
  `插件数: ${meta.length} 有运行时面: ${result.inputs.hasRuntimeSurface ? "是" : "否(方法级结论降为「疑似」)"}`,
73
83
  );
84
+ L.push(...catalogLines(result.catalog));
74
85
  L.push("");
75
86
  for (const note of verdict.observation?.notes || [])
76
87
  L.push(`采样说明: ${note}`);
@@ -82,7 +93,7 @@ export function render(result, opts) {
82
93
  L.push(
83
94
  `结论: ${VERDICT_LABEL.ok} ${s.ok} ${VERDICT_LABEL.partial} ${s.partial} ${VERDICT_LABEL.broken} ${s.broken}` +
84
95
  (s.vacuousOk
85
- ? ` (其中 ${s.vacuousOk} 个「可用」是空洞的:它没有任何可检查的依赖)`
96
+ ? ` (其中 ${s.vacuousOk} 个「可用」没有已验证调用,不等于业务通过)`
86
97
  : ""),
87
98
  );
88
99
  L.push("");
@@ -122,9 +133,16 @@ export function render(result, opts) {
122
133
  for (const name of names) {
123
134
  const p = verdict.plugins[name];
124
135
  L.push(
125
- `${VERDICT_LABEL[p.verdict]} ${name}@${p.version}${p.vacuous ? " [空洞的结论:无可检查对象]" : ""}`,
136
+ `${VERDICT_LABEL[p.verdict]} ${name}@${p.version}${p.vacuous ? " [无已验证调用]" : ""}`,
126
137
  );
127
138
  if (opts.quiet) continue;
139
+ if (p.coverage) {
140
+ L.push(
141
+ ` 证据覆盖: ${p.coverage.status === "complete" ? "完整" : "有限"};已验证调用 ${p.coverage.verifiedCalls}`,
142
+ );
143
+ for (const note of p.coverage.limitations)
144
+ L.push(` 覆盖限制: ${note}`);
145
+ }
128
146
  for (const f of p.findings) {
129
147
  const api = f.service
130
148
  ? f.method
@@ -132,7 +150,7 @@ export function render(result, opts) {
132
150
  : f.service
133
151
  : "(声明)";
134
152
  const loc = f.location ? `${f.location.file}:${f.location.line}` : "-";
135
- const cert = f.certainty === "certain" ? "" : " [疑似]";
153
+ const cert = f.certainty === "certain" ? " [确定]" : " [疑似]";
136
154
  L.push(
137
155
  ` ${SEVERITY_LABEL[f.severity] ?? f.severity} / ${CATEGORY_LABEL[f.category] ?? f.category}${cert} ${api} @ ${loc}`,
138
156
  );
@@ -142,12 +160,11 @@ export function render(result, opts) {
142
160
  // 标出来,用户才知道哪一句可以去核实。
143
161
  if (f.impact) {
144
162
  L.push(
145
- f.impactBasis === "inferred"
146
- ? ` 影响(推断,非实测): ${f.impact}`
147
- : ` 影响: ${f.impact}`,
163
+ ` 影响${f.impactBasis === "measured" ? "(实测)" : f.impactBasis === "inferred" ? "(推断,非实测)" : "(依据未提供)"}: ${f.impact}`,
148
164
  );
149
165
  }
150
166
  if (f.basis) L.push(` 依据: ${f.basis}`);
167
+ for (const step of nextSteps(f)) L.push(` 下一步: ${step}`);
151
168
  }
152
169
  // 「没报」和「报了」一样要能审:过滤与弃权是结论的一部分。
153
170
  const cs = p.clientSide || [];
@@ -169,13 +186,55 @@ export function render(result, opts) {
169
186
  );
170
187
  }
171
188
 
189
+ const graph = buildDependencyGraph({
190
+ extraction: result.extract,
191
+ verdict,
192
+ entries: result.entryStates,
193
+ });
194
+ const relations = graph.edges.filter((edge) => edge.kind === "dependency");
195
+ const nodeNames = new Map(graph.nodes.map((node) => [node.id, node.name]));
196
+ const requirement = {
197
+ required: "必需",
198
+ optional: "可选",
199
+ subcontext: "子作用域",
200
+ unknown: "必需性未知",
201
+ };
202
+ L.push("");
203
+ L.push(
204
+ `依赖关系: 服务依赖 ${relations.length};潜在声明环 ${graph.cycles.length}${graph.coverage.truncated ? ";关系/传播已截断" : ""}`,
205
+ );
206
+ for (const edge of relations.slice(0, 20)) {
207
+ const candidates = graph.edges.filter(
208
+ (candidate) =>
209
+ candidate.kind === "candidate-provider" &&
210
+ candidate.service === edge.service &&
211
+ (candidate.side === edge.side || candidate.side === "unknown"),
212
+ );
213
+ L.push(
214
+ ` ${nodeNames.get(edge.from)} → ${edge.service} [${edge.side}/${requirement[edge.requirement]}];候选提供者:${candidates.map((candidate) => `${nodeNames.get(candidate.from)}(${candidate.side})`).join("、") || "未定位(不等于宿主缺失)"}`,
215
+ );
216
+ }
217
+ if (relations.length > 20)
218
+ L.push(
219
+ ` 文本仅列出前 20 条依赖;完整的本次关系见 JSON/面板(仍受图预算限制)。`,
220
+ );
221
+ L.push(
222
+ " 提供声明仅为候选;同端路径及环只表示潜在影响,不证明实际挂起或失败;client 包 inject 不当作 host 服务。",
223
+ );
224
+ for (const reason of graph.coverage.reasons)
225
+ L.push(` 关系覆盖限制: ${reason}`);
226
+
172
227
  L.push("");
173
228
  L.push(
174
- "说明: 「疑似」= 没有运行时面、只能靠官方目录推断(那份目录是契约目录,不是运行时面)。",
229
+ "说明: 确定(certain)表示当前证据足以支持该条发现,可来自明确静态事实或合格运行时观测;疑似(suspected)表示当前证据不足以确定该条发现。",
230
+ );
231
+ L.push(
232
+ " impactBasis=measured/inferred 分别表示影响有对应实测依据/影响由证据推断;确定发现不等于影响已实测。官方目录不是运行时面,未观测不等于缺失。",
175
233
  );
176
234
  L.push(
177
- " 假阳性会让用户去做不必要的处置,所以判不准时一律降级,而不是升格成确定。",
235
+ " 没有已验证调用仍可能存在服务依赖或客户端功能;调用核验、无发现和覆盖完整均不等于业务验收通过。",
178
236
  );
237
+ L.push(" 下一步是核实建议;本工具不会自动安装、升级或修改配置与源码。");
179
238
  return L.join("\n");
180
239
  }
181
240
 
@@ -211,6 +270,7 @@ export function toJSON(result) {
211
270
  }
212
271
  : null,
213
272
  dshVersion: result.verdict.dshVersion,
273
+ ...(result.catalog ? { catalog: result.catalog } : {}),
214
274
  summary: result.verdict.summary,
215
275
  observation: {
216
276
  ...result.verdict.observation,
@@ -219,6 +279,11 @@ export function toJSON(result) {
219
279
  : null,
220
280
  },
221
281
  plugins: result.verdict.plugins,
282
+ dependencyGraph: buildDependencyGraph({
283
+ extraction: result.extract,
284
+ verdict: result.verdict,
285
+ entries: result.entryStates,
286
+ }),
222
287
  extraction: {
223
288
  scanComplete: report.scanComplete,
224
289
  diagnostics: report.diagnostics,
@@ -79,8 +79,10 @@ export function analyzeSources(
79
79
  const sites = [],
80
80
  bindings = [],
81
81
  dependencies = [],
82
+ provideSites = [],
82
83
  diagnostics = [],
83
84
  subContexts = [];
85
+ const provideSiteKeys = new Set();
84
86
  let scopeId = 0,
85
87
  invocationCount = 0,
86
88
  constantCalls = 0;
@@ -586,6 +588,24 @@ export function analyzeSources(
586
588
  sides: [...(activeSides || sides)],
587
589
  });
588
590
  };
591
+ const provide = (service, node) => {
592
+ provided.add(service);
593
+ mention(service);
594
+ const sides = [...(activeSides || fileSides[file] || ["unknown"])];
595
+ const site = {
596
+ service,
597
+ file,
598
+ line: node.loc?.start.line ?? 1,
599
+ start: node.start,
600
+ end: node.end,
601
+ sides,
602
+ };
603
+ const key = JSON.stringify([service, file, site.start, sides]);
604
+ if (!provideSiteKeys.has(key)) {
605
+ provideSiteKeys.add(key);
606
+ provideSites.push(site);
607
+ }
608
+ };
589
609
  const literalNames = (node, scope) => {
590
610
  node = unwrap(node);
591
611
  if (node?.type === "Identifier")
@@ -1383,8 +1403,7 @@ export function analyzeSources(
1383
1403
  );
1384
1404
  if (provideNames.has(field))
1385
1405
  for (const s of literalNames(node.init, scope)) {
1386
- provided.add(s);
1387
- mention(s);
1406
+ provide(s, node);
1388
1407
  }
1389
1408
  }
1390
1409
  return;
@@ -1529,8 +1548,7 @@ export function analyzeSources(
1529
1548
  dependency(name, "descriptor-inject", node);
1530
1549
  if (node.type === "AssignmentExpression" && slot.key === "provide")
1531
1550
  for (const name of literalNames(node.right, scope)) {
1532
- provided.add(name);
1533
- mention(name);
1551
+ provide(name, node);
1534
1552
  }
1535
1553
  return;
1536
1554
  }
@@ -1729,8 +1747,7 @@ export function analyzeSources(
1729
1747
  operation === "provide" &&
1730
1748
  constant(node.arguments[0], scope, guards) !== null
1731
1749
  ) {
1732
- provided.add(constant(node.arguments[0], scope, guards));
1733
- mention(constant(node.arguments[0], scope, guards));
1750
+ provide(constant(node.arguments[0], scope, guards), node);
1734
1751
  }
1735
1752
  if (operation === "inject") {
1736
1753
  const injected = literalNames(node.arguments[0], scope);
@@ -1900,6 +1917,7 @@ export function analyzeSources(
1900
1917
  serviceFiles,
1901
1918
  sites,
1902
1919
  provided,
1920
+ provideSites,
1903
1921
  bindings,
1904
1922
  dependencies,
1905
1923
  diagnostics,
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parentPort, workerData } from "node:worker_threads";
4
+ import { captureUpgradeInputOwned } from "./upgrade-input.mjs";
5
+
6
+ try {
7
+ const { nm, limits, root } = workerData;
8
+ const selected = path.resolve(nm);
9
+ const installed =
10
+ path.basename(selected) !== "node_modules" &&
11
+ fs.existsSync(path.join(selected, "node_modules"))
12
+ ? path.join(selected, "node_modules")
13
+ : selected;
14
+ parentPort.postMessage({
15
+ ok: true,
16
+ value: captureUpgradeInputOwned(installed, limits, root),
17
+ });
18
+ } catch (error) {
19
+ parentPort.postMessage({ ok: false, error: error.message });
20
+ }
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parentPort, workerData } from "node:worker_threads";
4
+
5
+ try {
6
+ const { root, identity } = workerData;
7
+ const observed = fs.lstatSync(root);
8
+ if (
9
+ !observed.isDirectory() ||
10
+ observed.dev !== identity.dev ||
11
+ observed.ino !== identity.ino
12
+ )
13
+ throw new Error("私有输入目录身份变化");
14
+ // Never follow copied links. This worker only receives a parent-owned root,
15
+ // never a path selected by the preview caller or sent back by capture.
16
+ const writable = (dir) => {
17
+ if (!fs.lstatSync(dir).isDirectory())
18
+ throw new Error("私有输入目录身份变化");
19
+ fs.chmodSync(dir, 0o700);
20
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }))
21
+ if (entry.isDirectory()) writable(path.join(dir, entry.name));
22
+ };
23
+ writable(root);
24
+ fs.rmSync(root, { recursive: true, force: true });
25
+ parentPort.postMessage({ ok: true, value: null });
26
+ } catch (error) {
27
+ parentPort.postMessage({ ok: false, error: error.message });
28
+ }