dsh-plugin-compat-check 0.1.4 → 0.1.6

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 (43) hide show
  1. package/README.md +39 -17
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/index.js +282 -53
  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/report-identity-worker.mjs +153 -0
  17. package/kernel/lib/report-identity.mjs +15 -0
  18. package/kernel/lib/report-service.mjs +196 -11
  19. package/kernel/lib/report.mjs +76 -11
  20. package/kernel/lib/source-analysis.mjs +24 -6
  21. package/kernel/lib/upgrade-capture-worker.mjs +20 -0
  22. package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
  23. package/kernel/lib/upgrade-contract.mjs +471 -0
  24. package/kernel/lib/upgrade-input.mjs +309 -0
  25. package/kernel/lib/upgrade-preview.mjs +256 -0
  26. package/kernel/lib/upgrade-worker.mjs +50 -0
  27. package/kernel/lib/version.mjs +1 -1
  28. package/kernel/lib/worker-analysis.mjs +16 -1
  29. package/kernel/lib/workflow-panels.mjs +706 -0
  30. package/kernel/rules/README.md +5 -5
  31. package/lib/client.js +2974 -69
  32. package/lib/dependency-graph.js +651 -0
  33. package/lib/finding-guidance.js +73 -0
  34. package/lib/finding-review.js +339 -0
  35. package/lib/functional-health-contract.js +338 -0
  36. package/lib/report-contract.js +88 -0
  37. package/lib/upgrade-contract.js +472 -0
  38. package/lib/workflow-panels.js +707 -0
  39. package/package.json +10 -5
  40. package/scripts/prepublish-check.mjs +28 -13
  41. package/scripts/stage-contract.mjs +91 -29
  42. package/scripts/stage-kernel.mjs +3 -2
  43. package/skill/SKILL.md +90 -39
@@ -0,0 +1,707 @@
1
+ // Generated from lib/workflow-panels.mjs; run node plugin/scripts/stage-contract.mjs
2
+ /** Stateless React factory; staged verbatim into the shipped client. */
3
+ export function createDependencyPanel(React, GraphContract = {}) {
4
+ const h = React.createElement;
5
+ const requirement = {
6
+ required: "必需",
7
+ optional: "可选 / 软读取",
8
+ subcontext: "子作用域",
9
+ unknown: "必需性未知",
10
+ };
11
+ const table = (caption, headers, rows) =>
12
+ h(
13
+ "div",
14
+ { style: { overflowX: "auto", marginBottom: 16 } },
15
+ h(
16
+ "table",
17
+ { style: { borderCollapse: "collapse", width: "100%", fontSize: 13 } },
18
+ h(
19
+ "caption",
20
+ { style: { textAlign: "left", fontWeight: 600, padding: "8px 0" } },
21
+ caption,
22
+ ),
23
+ h(
24
+ "thead",
25
+ null,
26
+ h(
27
+ "tr",
28
+ null,
29
+ ...headers.map((label, i) =>
30
+ h(
31
+ "th",
32
+ {
33
+ key: i,
34
+ scope: "col",
35
+ style: {
36
+ textAlign: "left",
37
+ padding: 6,
38
+ borderBottom: "1px solid #777",
39
+ },
40
+ },
41
+ label,
42
+ ),
43
+ ),
44
+ ),
45
+ ),
46
+ h(
47
+ "tbody",
48
+ null,
49
+ ...rows.map((row, i) =>
50
+ h(
51
+ "tr",
52
+ { key: i },
53
+ ...row.map((cell, j) =>
54
+ h(
55
+ "td",
56
+ {
57
+ key: j,
58
+ style: {
59
+ verticalAlign: "top",
60
+ padding: 6,
61
+ borderBottom: "1px solid #9995",
62
+ overflowWrap: "anywhere",
63
+ },
64
+ },
65
+ cell,
66
+ ),
67
+ ),
68
+ ),
69
+ ),
70
+ ),
71
+ ),
72
+ );
73
+ return function DependencyPanel({ graph } = {}) {
74
+ if (graph === undefined || graph === null)
75
+ return h(
76
+ "section",
77
+ { "aria-label": "依赖关系与潜在影响" },
78
+ h("h3", null, "依赖关系与潜在影响"),
79
+ h("p", null, "本报告未提供依赖关系视图;请重新体检。"),
80
+ );
81
+ let valid = false;
82
+ try {
83
+ valid =
84
+ typeof GraphContract.validateDependencyGraph === "function" &&
85
+ GraphContract.validateDependencyGraph(graph);
86
+ } catch {}
87
+ if (!valid)
88
+ return h(
89
+ "section",
90
+ { "aria-label": "依赖关系与潜在影响" },
91
+ h("h3", null, "依赖关系与潜在影响"),
92
+ h(
93
+ "p",
94
+ { role: "status" },
95
+ "依赖关系视图不可用:扩展数据格式或引用无效,请重新体检。兼容结论仍按原始证据显示。",
96
+ ),
97
+ );
98
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
99
+ const name = (id) => nodes.get(id)?.name || id;
100
+ const entries = (node) =>
101
+ node.entries.length
102
+ ? node.entries
103
+ .map(
104
+ (row) =>
105
+ `${row.id || "未命名条目"}: ${row.disabled ? "disabled / 已禁用" : row.state}`,
106
+ )
107
+ .join(";")
108
+ : "unknown / 未提供 loader 条目";
109
+ const candidates = new Map(),
110
+ consumers = new Map();
111
+ for (const edge of graph.edges) {
112
+ const map =
113
+ edge.kind === "candidate-provider"
114
+ ? candidates
115
+ : edge.kind === "dependency"
116
+ ? consumers
117
+ : null;
118
+ if (map) {
119
+ if (!map.has(edge.to)) map.set(edge.to, []);
120
+ map.get(edge.to).push(edge);
121
+ }
122
+ }
123
+ const candidateLabel = (edge) =>
124
+ `${name(edge.from)} [${edge.side}; ${entries(nodes.get(edge.from))}]${edge.side === "unknown" ? "(端未知;不参与传播推导)" : ""}`;
125
+ const source = (edge) =>
126
+ edge.source?.file
127
+ ? `${edge.source.file}${edge.source.line === null ? "" : ":" + edge.source.line}`
128
+ : "来源位置未知";
129
+ const relationshipRows = graph.edges
130
+ .filter((edge) => edge.kind === "dependency")
131
+ .map((edge) => {
132
+ const known = candidates.get(edge.to) || [];
133
+ const unknown =
134
+ candidates.get(
135
+ JSON.stringify(["service", "unknown", edge.service]),
136
+ ) || [];
137
+ const choices = [...known, ...(edge.side === "unknown" ? [] : unknown)];
138
+ return [
139
+ name(edge.from),
140
+ edge.service,
141
+ edge.side,
142
+ requirement[edge.requirement],
143
+ choices.length
144
+ ? choices.map(candidateLabel).join(";")
145
+ : "未找到可定位的候选提供者(不等于宿主缺失)",
146
+ source(edge),
147
+ ];
148
+ });
149
+ const providerRows = graph.edges
150
+ .filter((edge) => edge.kind === "candidate-provider")
151
+ .map((edge) => [
152
+ candidateLabel(edge),
153
+ edge.service,
154
+ edge.side,
155
+ (consumers.get(edge.to) || [])
156
+ .map((c) => `${name(c.from)}(${requirement[c.requirement]})`)
157
+ .join(";") || "没有同端消费者证据",
158
+ source(edge),
159
+ ]);
160
+ const packageRows = graph.edges
161
+ .filter((edge) => edge.kind === "client-package-inject")
162
+ .map((edge) => [
163
+ name(edge.from),
164
+ name(edge.to),
165
+ "client 包级 inject;不作为 host 服务关系或传播依据",
166
+ ]);
167
+ const edgeMap = new Map(graph.edges.map((edge) => [edge.id, edge]));
168
+ const pathLabel = (origin, path) => {
169
+ const parts = [name(origin)];
170
+ for (let i = 0; i < path.length; i += 2) {
171
+ const provider = edgeMap.get(path[i]),
172
+ consumer = edgeMap.get(path[i + 1]);
173
+ parts.push(
174
+ `${provider.service} [${provider.side}]`,
175
+ `${name(consumer.from)}(${requirement[consumer.requirement]})`,
176
+ );
177
+ }
178
+ return parts.join(" → ");
179
+ };
180
+ const impactRows = graph.impacts.flatMap((impact) =>
181
+ impact.affected.map((row) => [
182
+ name(impact.plugin),
183
+ name(row.plugin),
184
+ row.side,
185
+ "潜在影响;非实测故障",
186
+ pathLabel(impact.plugin, row.path),
187
+ ]),
188
+ );
189
+ const cycleRows = graph.cycles.map((cycle) => [
190
+ cycle.side,
191
+ pathLabel(cycle.plugins[0], cycle.path),
192
+ "潜在声明环;不据此认定挂起或失败",
193
+ ]);
194
+ return h(
195
+ "section",
196
+ { "aria-label": "依赖关系与潜在影响" },
197
+ h("h3", null, "依赖关系与潜在影响"),
198
+ ...graph.notes.map((note, i) => h("p", { key: `note:${i}` }, note)),
199
+ h(
200
+ "p",
201
+ null,
202
+ `关系覆盖:${graph.coverage.status === "complete" ? "已覆盖本次提取输入(不等于运行时完整)" : "有限"}${graph.coverage.truncated ? ";已截断,不能当作全量关系或全量受影响者" : ""}。节点 ${graph.nodes.length},关系 ${graph.edges.length}。`,
203
+ ),
204
+ ...graph.coverage.reasons.map((reason, i) =>
205
+ h("p", { key: `reason:${i}`, role: "note" }, `覆盖限制:${reason}`),
206
+ ),
207
+ table(
208
+ "插件及 loader 条目",
209
+ [
210
+ "插件",
211
+ "宿主条目状态(不代表服务状态)",
212
+ "Context 名称线索(非子作用域证明)",
213
+ ],
214
+ graph.nodes
215
+ .filter((node) => node.kind === "plugin")
216
+ .map((node) => [
217
+ node.name,
218
+ entries(node),
219
+ node.subContexts
220
+ .filter((context) => context.names.length)
221
+ .map(
222
+ (context) =>
223
+ `${context.file || "位置未知"}: ${context.names.join(", ")}`,
224
+ )
225
+ .join(";") || "未提取到命名线索",
226
+ ]),
227
+ ),
228
+ relationshipRows.length
229
+ ? table(
230
+ "服务依赖及候选提供者",
231
+ [
232
+ "消费者",
233
+ "服务",
234
+ "端",
235
+ "依赖方式",
236
+ "候选提供者 / loader 状态",
237
+ "依赖来源",
238
+ ],
239
+ relationshipRows,
240
+ )
241
+ : h("p", null, "本次未提取到服务依赖;不等于没有运行时依赖。"),
242
+ providerRows.length
243
+ ? table(
244
+ "提供声明(仅候选)",
245
+ [
246
+ "候选插件 / loader 状态",
247
+ "服务",
248
+ "端",
249
+ "同端潜在消费者",
250
+ "提供来源",
251
+ ],
252
+ providerRows,
253
+ )
254
+ : null,
255
+ packageRows.length
256
+ ? table("客户端包依赖", ["消费者", "客户端包", "语义"], packageRows)
257
+ : null,
258
+ impactRows.length
259
+ ? table(
260
+ "潜在受影响插件及关系路径",
261
+ ["候选起点", "潜在受影响者", "端", "证据边界", "路径"],
262
+ impactRows,
263
+ )
264
+ : h("p", null, "未枚举到可证明同端的传播路径;不等于没有潜在影响。"),
265
+ cycleRows.length
266
+ ? table("潜在声明环", ["端", "关系路径", "说明"], cycleRows)
267
+ : h("p", null, "本次覆盖内未枚举到声明环。"),
268
+ h(
269
+ "p",
270
+ null,
271
+ `预算:节点 ${graph.coverage.limits.nodes};关系 ${graph.coverage.limits.edges};传播遍历及路径 ${graph.coverage.limits.traversal};输入条目 ${graph.coverage.limits.items}。`,
272
+ ),
273
+ );
274
+ };
275
+ }
276
+ /** Browser-safe React factory. Manual actions stay separate from kernel results. */
277
+ export function createFindingReviewPanel(React, ReviewContract) {
278
+ const h = React.createElement,
279
+ labels = ReviewContract.REVIEW_STATUS_LABELS;
280
+ function ReviewRow({ item, contextToken, onSave, disabled }) {
281
+ const [status, setStatus] = React.useState(
282
+ item.record?.status || "pending",
283
+ );
284
+ const [note, setNote] = React.useState(item.note || "");
285
+ const [busy, setBusy] = React.useState(false);
286
+ const [error, setError] = React.useState("");
287
+ const [message, setMessage] = React.useState("");
288
+ const current = React.useRef({ contextToken, findingId: item.findingId });
289
+ current.current = { contextToken, findingId: item.findingId };
290
+ React.useEffect(() => {
291
+ setStatus(item.record?.status || "pending");
292
+ setNote(item.note || "");
293
+ setError("");
294
+ setMessage("");
295
+ }, [contextToken, item.findingId, item.record?.updatedAt]);
296
+ const save = async () => {
297
+ if (busy || disabled) return;
298
+ const target = { contextToken, findingId: item.findingId };
299
+ try {
300
+ const input = ReviewContract.validateReviewInput({
301
+ ...target,
302
+ status,
303
+ note,
304
+ });
305
+ setBusy(true);
306
+ setError("");
307
+ setMessage("");
308
+ await onSave(input);
309
+ if (
310
+ current.current.contextToken === target.contextToken &&
311
+ current.current.findingId === target.findingId
312
+ )
313
+ setMessage("人工处置已保存");
314
+ } catch (reason) {
315
+ if (
316
+ current.current.contextToken === target.contextToken &&
317
+ current.current.findingId === target.findingId
318
+ )
319
+ setError(reason?.message || "保存人工处置失败");
320
+ } finally {
321
+ setBusy(false);
322
+ }
323
+ };
324
+ return h(
325
+ "article",
326
+ {
327
+ "data-review-finding": item.findingId,
328
+ style: {
329
+ border: "1px solid #d0d5dd",
330
+ padding: "12px",
331
+ marginBottom: "10px",
332
+ borderRadius: "6px",
333
+ },
334
+ },
335
+ h(
336
+ "strong",
337
+ null,
338
+ `${item.pluginName}@${item.pluginVersion} · 发现 ${item.findingIndex + 1} · ${labels[item.status] || item.status}`,
339
+ ),
340
+ item.reviewReason ? h("p", { role: "status" }, item.reviewReason) : null,
341
+ item.record
342
+ ? h(
343
+ "p",
344
+ null,
345
+ `原记录:${labels[item.record.status]} · ${item.record.updatedAt} · ${item.record.note}`,
346
+ )
347
+ : null,
348
+ item.history?.length
349
+ ? h(
350
+ "details",
351
+ null,
352
+ h(
353
+ "summary",
354
+ null,
355
+ `旧处置摘要(${item.historyCount || item.history.length};完整依据见历史处置)`,
356
+ ),
357
+ item.history.map((record) =>
358
+ h(
359
+ "p",
360
+ { key: record.findingId },
361
+ `${record.pluginName}@${record.pluginVersion} · ${labels[record.status]} · ${record.updatedAt} · ${record.note}${record.noteTruncated ? "…" : ""}`,
362
+ ),
363
+ ),
364
+ )
365
+ : null,
366
+ h(
367
+ "label",
368
+ null,
369
+ "人工状态 ",
370
+ h(
371
+ "select",
372
+ {
373
+ "aria-label": `${item.pluginName} 发现 ${item.findingIndex + 1} 人工状态`,
374
+ value: status,
375
+ disabled: busy || disabled,
376
+ onChange: (e) => {
377
+ setStatus(e.target.value);
378
+ setMessage("");
379
+ },
380
+ },
381
+ Object.keys(labels)
382
+ .filter((x) => x !== "needs-review")
383
+ .map((value) => h("option", { key: value, value }, labels[value])),
384
+ ),
385
+ ),
386
+ h(
387
+ "label",
388
+ { style: { display: "block", marginTop: "8px" } },
389
+ "核实依据 ",
390
+ h("textarea", {
391
+ "aria-label": `${item.pluginName} 发现 ${item.findingIndex + 1} 核实依据`,
392
+ value: note,
393
+ maxLength: ReviewContract.REVIEW_LIMITS.note,
394
+ disabled: busy || disabled,
395
+ rows: 3,
396
+ style: { display: "block", width: "100%" },
397
+ onChange: (e) => {
398
+ setNote(e.target.value);
399
+ setMessage("");
400
+ },
401
+ }),
402
+ ),
403
+ h(
404
+ "button",
405
+ { type: "button", disabled: busy || disabled, onClick: save },
406
+ busy ? "保存中…" : "保存人工处置",
407
+ ),
408
+ error ? h("p", { role: "alert" }, error) : null,
409
+ message ? h("p", { role: "status" }, message) : null,
410
+ );
411
+ }
412
+ return function FindingReviewPanel({ report, reviews, onSave }) {
413
+ const [saved, setSaved] = React.useState(null);
414
+ const [limit, setLimit] = React.useState(20);
415
+ const [historyOpen, setHistoryOpen] = React.useState(false);
416
+ const [historyLimit, setHistoryLimit] = React.useState(20);
417
+ const current = React.useRef(reviews?.contextToken);
418
+ current.current = reviews?.contextToken;
419
+ React.useEffect(() => {
420
+ setSaved(null);
421
+ setLimit(20);
422
+ setHistoryOpen(false);
423
+ setHistoryLimit(20);
424
+ }, [reviews?.contextToken, reviews?.revision]);
425
+ const active =
426
+ saved && saved.contextToken === reviews?.contextToken ? saved : reviews;
427
+ if (active == null) return null;
428
+ if (!ReviewContract.validateReviewEnvelope(active, report))
429
+ return h(
430
+ "section",
431
+ { "aria-label": "发现的人工处置", style: { marginTop: "16px" } },
432
+ h("h3", null, "发现的人工处置"),
433
+ h(
434
+ "p",
435
+ { role: "alert" },
436
+ "人工处置数据无效,本节暂不可用;请刷新后重试。",
437
+ ),
438
+ );
439
+ const save = async (input) => {
440
+ const result = await onSave(input),
441
+ next = result?.reviews || result;
442
+ if (!ReviewContract.validateReviewEnvelope(next, report))
443
+ throw new Error("保存响应无效");
444
+ if (next.error) throw new Error(next.error);
445
+ if (
446
+ current.current !== input.contextToken ||
447
+ next.contextToken !== input.contextToken
448
+ )
449
+ throw new Error("报告已变化,请重新核实");
450
+ setSaved(next);
451
+ return next;
452
+ };
453
+ const items = (active.items || []).filter(
454
+ (item) =>
455
+ !report?.plugins || Object.hasOwn(report.plugins, item.pluginName),
456
+ );
457
+ return h(
458
+ "section",
459
+ { "aria-label": "发现的人工处置", style: { marginTop: "16px" } },
460
+ h("h3", null, "发现的人工处置"),
461
+ h(
462
+ "p",
463
+ null,
464
+ "人工记录仅供核实追踪,不改变内核发现、总结论、退出码、基线或兼容性豁免;新风险仍完整显示。",
465
+ ),
466
+ active.error ? h("p", { role: "alert" }, active.error) : null,
467
+ items.length
468
+ ? items.slice(0, limit).map((item) =>
469
+ h(ReviewRow, {
470
+ key: item.findingId,
471
+ item,
472
+ contextToken: active.contextToken,
473
+ onSave: save,
474
+ disabled: !!active.error || typeof onSave !== "function",
475
+ }),
476
+ )
477
+ : h("p", null, "当前没有可处置的发现。"),
478
+ items.length > limit
479
+ ? h(
480
+ "button",
481
+ { type: "button", onClick: () => setLimit((n) => n + 20) },
482
+ `显示更多处置(已显示 ${Math.min(limit, items.length)} / ${items.length})`,
483
+ )
484
+ : null,
485
+ active.history?.length
486
+ ? h(
487
+ "details",
488
+ {
489
+ open: historyOpen,
490
+ onToggle: (e) => setHistoryOpen(e.target.open),
491
+ },
492
+ h("summary", null, `历史处置完整依据(${active.history.length})`),
493
+ historyOpen
494
+ ? active.history
495
+ .slice(0, historyLimit)
496
+ .map((record) =>
497
+ h(
498
+ "p",
499
+ { key: record.findingId },
500
+ `${record.pluginName}@${record.pluginVersion} · ${labels[record.status]} · ${record.updatedAt} · ${record.note}`,
501
+ ),
502
+ )
503
+ : null,
504
+ historyOpen && active.history.length > historyLimit
505
+ ? h(
506
+ "button",
507
+ {
508
+ type: "button",
509
+ onClick: () => setHistoryLimit((n) => n + 20),
510
+ },
511
+ "显示更多历史处置",
512
+ )
513
+ : null,
514
+ )
515
+ : null,
516
+ );
517
+ };
518
+ }
519
+ /** Offline UI for explicitly imported health evidence. Never starts a runner. */
520
+ export function createFunctionalHealthPanel(React, HealthContract) {
521
+ const h = React.createElement;
522
+ const statusLabels = {
523
+ "tested-pass": "所选检查通过",
524
+ "tested-failed": "所选检查失败",
525
+ error: "检查未完成",
526
+ pass: "通过",
527
+ failed: "失败",
528
+ };
529
+ return function FunctionalHealthPanel({ report }) {
530
+ const [result, setResult] = React.useState(null);
531
+ const [error, setError] = React.useState("");
532
+ const [busy, setBusy] = React.useState(false);
533
+ const request = React.useRef(0);
534
+ React.useEffect(
535
+ () => () => {
536
+ request.current++;
537
+ },
538
+ [],
539
+ );
540
+ const load = async (event) => {
541
+ const file = event.target.files?.[0],
542
+ id = ++request.current;
543
+ if (!file) return;
544
+ setBusy(true);
545
+ setError("");
546
+ try {
547
+ if (
548
+ !Number.isSafeInteger(file.size) ||
549
+ file.size < 1 ||
550
+ file.size > 512 * 1024
551
+ )
552
+ throw new Error("记录超过大小限制(512 KiB)");
553
+ const parsed = JSON.parse(await file.text());
554
+ const checked = HealthContract.validateFunctionalHealthResult(parsed);
555
+ if (!checked.valid) throw new Error(checked.errors.join(";"));
556
+ if (!globalThis.crypto?.subtle || typeof TextEncoder === "undefined")
557
+ throw new Error("当前浏览器无法核验记录摘要");
558
+ const bytes = new TextEncoder().encode(
559
+ HealthContract.canonicalFunctionalManifest(parsed.declaration),
560
+ );
561
+ const digest = Array.from(
562
+ new Uint8Array(
563
+ await globalThis.crypto.subtle.digest("SHA-256", bytes),
564
+ ),
565
+ (n) => n.toString(16).padStart(2, "0"),
566
+ ).join("");
567
+ if (digest !== parsed.identity.manifestSha256)
568
+ throw new Error("检查声明与记录摘要不一致");
569
+ if (request.current === id) setResult(parsed);
570
+ } catch (reason) {
571
+ if (request.current === id)
572
+ setError("导入失败:" + (reason?.message || reason));
573
+ } finally {
574
+ if (request.current === id) setBusy(false);
575
+ }
576
+ };
577
+ const artifact = result?.identity.artifact,
578
+ current = artifact && report?.plugins?.[artifact.name];
579
+ const versionsMatch =
580
+ current?.version === artifact?.version &&
581
+ report?.dshVersion === result?.identity.dshVersion;
582
+ return h(
583
+ "section",
584
+ { "aria-label": "功能健康检查", style: { marginTop: "16px" } },
585
+ h("h3", null, "功能健康检查(独立实测记录)"),
586
+ h(
587
+ "p",
588
+ null,
589
+ "在仓库CLI中显式运行所选HTTP/DOM检查后,可导入JSON记录。本面板只读取所选文件,不执行检查、安装插件或访问目标服务。",
590
+ ),
591
+ h("input", {
592
+ type: "file",
593
+ accept: ".json,application/json",
594
+ "aria-label": "导入功能健康检查记录",
595
+ disabled: busy,
596
+ onChange: load,
597
+ }),
598
+ busy ? h("p", { role: "status" }, "正在核验记录…") : null,
599
+ error ? h("p", { role: "alert" }, error) : null,
600
+ !result
601
+ ? h(
602
+ "p",
603
+ null,
604
+ "尚未导入功能实测记录。静态发现、宿主状态和业务健康是不同证据。",
605
+ )
606
+ : h(
607
+ "div",
608
+ { "data-functional-health": result.status },
609
+ h(
610
+ "p",
611
+ null,
612
+ `${artifact.name}@${artifact.version} · DSH ${result.identity.dshVersion} · ${statusLabels[result.status]}`,
613
+ ),
614
+ h(
615
+ "p",
616
+ { role: "status" },
617
+ versionsMatch
618
+ ? "插件与DSH版本相符;当前安装制品尚未核验,此处保留所选制品的历史记录。"
619
+ : "记录与当前报告目标不一致或插件未安装;不用于认证当前目标。",
620
+ ),
621
+ h(
622
+ "p",
623
+ null,
624
+ "测量时间:" + result.startedAt + " → " + result.completedAt,
625
+ ),
626
+ h("p", null, "制品 SHA-256:" + artifact.sha256),
627
+ h(
628
+ "p",
629
+ null,
630
+ "声明 SHA-256:" +
631
+ result.identity.manifestSha256 +
632
+ ";摘要校验不是作者签名或业务认证。",
633
+ ),
634
+ h(
635
+ "table",
636
+ { "aria-label": "功能健康检查结果" },
637
+ h(
638
+ "thead",
639
+ null,
640
+ h(
641
+ "tr",
642
+ null,
643
+ h("th", null, "检查与声明"),
644
+ h("th", null, "独立实测"),
645
+ ),
646
+ ),
647
+ h(
648
+ "tbody",
649
+ null,
650
+ result.checks.map((check, i) => {
651
+ const declaration = result.declaration.checks[i],
652
+ expected =
653
+ declaration.type === "http"
654
+ ? `GET ${declaration.path},HTTP ${declaration.status}` +
655
+ (declaration.json
656
+ ? `,JSON ${declaration.json.pointer} = ${JSON.stringify(declaration.json.equals)}`
657
+ : "")
658
+ : `${declaration.path} · ${declaration.selector} · 数量 ${declaration.minCount}–${declaration.maxCount}`;
659
+ return h(
660
+ "tr",
661
+ { key: check.id },
662
+ h("td", null, `${check.id} · ${expected}`),
663
+ h(
664
+ "td",
665
+ null,
666
+ `${statusLabels[check.status] || check.status} · ${JSON.stringify(check.evidence)}`,
667
+ ),
668
+ );
669
+ }),
670
+ ),
671
+ ),
672
+ result.error ? h("p", { role: "alert" }, result.error) : null,
673
+ h(
674
+ "p",
675
+ null,
676
+ "资源清理:" +
677
+ (result.cleanup.stopped && result.cleanup.removed
678
+ ? "已停止并清理自有环境"
679
+ : "未确认完整清理"),
680
+ ),
681
+ h(
682
+ "ul",
683
+ null,
684
+ result.limitations.map((text, i) => h("li", { key: i }, text)),
685
+ ),
686
+ h(
687
+ "p",
688
+ null,
689
+ "仅证明记录中的所选检查结果,不改普通兼容性结论、风险计数、基线或人工处置;不认证全部业务。",
690
+ ),
691
+ h(
692
+ "button",
693
+ {
694
+ type: "button",
695
+ onClick: () => {
696
+ request.current++;
697
+ setResult(null);
698
+ setError("");
699
+ setBusy(false);
700
+ },
701
+ },
702
+ "清除健康检查记录",
703
+ ),
704
+ ),
705
+ );
706
+ };
707
+ }