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.
- package/README.md +39 -17
- package/THIRD_PARTY_NOTICES.md +139 -0
- package/index.js +282 -53
- package/kernel/lib/author-ci-output.mjs +188 -0
- package/kernel/lib/author-ci-policy.mjs +285 -0
- package/kernel/lib/catalog.mjs +38 -0
- package/kernel/lib/contracts.mjs +88 -0
- package/kernel/lib/dependency-graph.mjs +650 -0
- package/kernel/lib/extract.mjs +14 -0
- package/kernel/lib/finding-guidance.mjs +72 -0
- package/kernel/lib/finding-review-store.mjs +245 -0
- package/kernel/lib/finding-review.mjs +338 -0
- package/kernel/lib/functional-health-contract.mjs +337 -0
- package/kernel/lib/issue-report.mjs +217 -0
- package/kernel/lib/observation.mjs +19 -9
- package/kernel/lib/report-identity-worker.mjs +153 -0
- package/kernel/lib/report-identity.mjs +15 -0
- package/kernel/lib/report-service.mjs +196 -11
- package/kernel/lib/report.mjs +76 -11
- package/kernel/lib/source-analysis.mjs +24 -6
- package/kernel/lib/upgrade-capture-worker.mjs +20 -0
- package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
- package/kernel/lib/upgrade-contract.mjs +471 -0
- package/kernel/lib/upgrade-input.mjs +309 -0
- package/kernel/lib/upgrade-preview.mjs +256 -0
- package/kernel/lib/upgrade-worker.mjs +50 -0
- package/kernel/lib/version.mjs +1 -1
- package/kernel/lib/worker-analysis.mjs +16 -1
- package/kernel/lib/workflow-panels.mjs +706 -0
- package/kernel/rules/README.md +5 -5
- package/lib/client.js +2974 -69
- package/lib/dependency-graph.js +651 -0
- package/lib/finding-guidance.js +73 -0
- package/lib/finding-review.js +339 -0
- package/lib/functional-health-contract.js +338 -0
- package/lib/report-contract.js +88 -0
- package/lib/upgrade-contract.js +472 -0
- package/lib/workflow-panels.js +707 -0
- package/package.json +10 -5
- package/scripts/prepublish-check.mjs +28 -13
- package/scripts/stage-contract.mjs +91 -29
- package/scripts/stage-kernel.mjs +3 -2
- package/skill/SKILL.md +90 -39
package/lib/client.js
CHANGED
|
@@ -39,6 +39,61 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
const REPORT_SCHEMA = 1;
|
|
40
40
|
const REPORT_KIND = "plugin-compat-check/result";
|
|
41
41
|
const ENVELOPE_KIND = "plugin-compat-check/report";
|
|
42
|
+
/** Reviewed catalogue bytes, not a promise of plugin business compatibility.
|
|
43
|
+
* Adding a version requires source verification, rule review and integration
|
|
44
|
+
* evidence; extraction alone must never extend this list. */
|
|
45
|
+
const CATALOG_REFERENCES = Object.freeze([
|
|
46
|
+
Object.freeze({
|
|
47
|
+
version: "0.2.0-rc.2",
|
|
48
|
+
file: "dsh-api-catalog.json",
|
|
49
|
+
sha256: "1331bf3220c1e06cfe087237077118d002dc04c32d027ce01da74c6a0d1dc9ad",
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
version: "0.2.1-rc.2",
|
|
53
|
+
file: "dsh-api-catalog-0.2.1-rc.2.json",
|
|
54
|
+
sha256: "90b4ae484ebc6660c53e5c9ad14ba657581534b30e8222c636c759e19587f743",
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
function describeCatalog({
|
|
58
|
+
targetVersion = null,
|
|
59
|
+
referenceVersion = null,
|
|
60
|
+
sha256,
|
|
61
|
+
override = false,
|
|
62
|
+
}) {
|
|
63
|
+
const known =
|
|
64
|
+
!override &&
|
|
65
|
+
CATALOG_REFERENCES.find(
|
|
66
|
+
(c) => c.version === referenceVersion && c.sha256 === sha256,
|
|
67
|
+
);
|
|
68
|
+
const verified = Boolean(known && targetVersion === referenceVersion);
|
|
69
|
+
return {
|
|
70
|
+
targetVersion,
|
|
71
|
+
referenceVersion,
|
|
72
|
+
sha256,
|
|
73
|
+
selection: override ? "override" : verified ? "exact" : "fallback",
|
|
74
|
+
verified,
|
|
75
|
+
certifiedVersions: known ? [known.version] : [],
|
|
76
|
+
scope:
|
|
77
|
+
"仅核验内置目录字节及对应精确版本的集成;不代表业务验收或运行时观测。",
|
|
78
|
+
warning: verified
|
|
79
|
+
? null
|
|
80
|
+
: override
|
|
81
|
+
? "自定义参考目录未认证;目录自报版本或相同字节不扩大已验收范围,请在实际目标宿主复核。"
|
|
82
|
+
: "目标 DSH 版本未知或未认证;普通体检使用参考目录回退,不能据此认定该版本已验收。",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function catalogLines(c) {
|
|
86
|
+
if (!c) return ["API 目录身份未提供;版本范围未认证,请重新体检。"];
|
|
87
|
+
return [
|
|
88
|
+
"目标 DSH:" + (c.targetVersion || "未知"),
|
|
89
|
+
"参考目录:" + (c.referenceVersion || "未知") + " · SHA-256:" + c.sha256,
|
|
90
|
+
"目录认证范围:" +
|
|
91
|
+
(c.certifiedVersions.join("、") || "无") +
|
|
92
|
+
";" +
|
|
93
|
+
c.scope,
|
|
94
|
+
...(c.warning ? [c.warning] : []),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
42
97
|
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
43
98
|
const text = (v) => typeof v === "string" && v.length > 0;
|
|
44
99
|
const nullableText = (v) => v === null || typeof v === "string";
|
|
@@ -313,6 +368,37 @@ function validateReportBody(body) {
|
|
|
313
368
|
}
|
|
314
369
|
need(e, body.error === null, "report.error");
|
|
315
370
|
need(e, nullableText(body.dshVersion), "report.dshVersion");
|
|
371
|
+
// Older schema-1 reports remain readable, but consumers show missing identity.
|
|
372
|
+
if (body.catalog !== undefined) {
|
|
373
|
+
const c = body.catalog;
|
|
374
|
+
if (
|
|
375
|
+
!record(c) ||
|
|
376
|
+
!nullableText(c.referenceVersion) ||
|
|
377
|
+
!/^[a-f0-9]{64}$/.test(c.sha256)
|
|
378
|
+
)
|
|
379
|
+
e.push("report.catalog");
|
|
380
|
+
else {
|
|
381
|
+
const expected = describeCatalog({
|
|
382
|
+
targetVersion: body.dshVersion,
|
|
383
|
+
referenceVersion: c.referenceVersion,
|
|
384
|
+
sha256: c.sha256,
|
|
385
|
+
override: c.selection === "override",
|
|
386
|
+
});
|
|
387
|
+
if (
|
|
388
|
+
c.selection !== "override" &&
|
|
389
|
+
!CATALOG_REFERENCES.some(
|
|
390
|
+
(k) => k.version === c.referenceVersion && k.sha256 === c.sha256,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
e.push("report.catalog.identity");
|
|
394
|
+
for (const [key, value] of Object.entries(expected))
|
|
395
|
+
need(
|
|
396
|
+
e,
|
|
397
|
+
JSON.stringify(c[key]) === JSON.stringify(value),
|
|
398
|
+
"report.catalog." + key,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
316
402
|
e.push(...validateObservation(body.observation));
|
|
317
403
|
if (
|
|
318
404
|
record(body.observation) &&
|
|
@@ -497,6 +583,8 @@ function validateEnvelope(data) {
|
|
|
497
583
|
)
|
|
498
584
|
return ["envelope.schemaVersion/kind"];
|
|
499
585
|
const e = validateReportBody(data.report);
|
|
586
|
+
if (Object.hasOwn(data, "issueReport"))
|
|
587
|
+
need(e, typeof data.issueReport === "string", "envelope.issueReport");
|
|
500
588
|
need(
|
|
501
589
|
e,
|
|
502
590
|
typeof data.ok === "boolean" && data.ok === data.report?.ok,
|
|
@@ -523,11 +611,2630 @@ function assertEnvelope(data) {
|
|
|
523
611
|
return data;
|
|
524
612
|
}
|
|
525
613
|
|
|
526
|
-
return { assertEnvelope: assertEnvelope };
|
|
614
|
+
return { assertEnvelope: assertEnvelope, catalogLines: catalogLines };
|
|
527
615
|
})();
|
|
528
616
|
// END GENERATED REPORT CONTRACT
|
|
529
617
|
|
|
618
|
+
// BEGIN GENERATED FINDING GUIDANCE
|
|
619
|
+
var FindingGuidance = (function () {
|
|
620
|
+
/** Presentation-only follow-up checks. Never changes a finding or performs work. */
|
|
621
|
+
function nextSteps(finding) {
|
|
622
|
+
if (!finding || typeof finding !== "object") return [];
|
|
623
|
+
switch (finding.category) {
|
|
624
|
+
case "client-entry-missing":
|
|
625
|
+
return [
|
|
626
|
+
'检查该插件 package.json 的 dsh.client 与 exports["./client"] 客户端入口声明,并在实际安装制品中核对入口文件是否存在、可读且已随包交付。',
|
|
627
|
+
"把已装插件的精确版本与制品文件清单对照;如需作者处理,提供缺失入口的包内相对位置,不猜测修复版本。",
|
|
628
|
+
"补充同一目标的浏览器加载结果,并实际操作客户端功能、核验实际功能结果;静态入口发现不等于业务失败已实测。",
|
|
629
|
+
];
|
|
630
|
+
case "declared-incompatible":
|
|
631
|
+
return [
|
|
632
|
+
"复核已装插件 package.json 的 engines.dsh 与官方 peerDependencies、字段格式及目标 DSH 版本;按报告所用声明策略核对生效字段。",
|
|
633
|
+
"只读核对目标 profile 的 compatibility.json 是否有该插件名、插件精确版本与目标 DSH 精确版本的记录;未观测或未授予不等于可自行添加豁免。",
|
|
634
|
+
"补充该目标的实际加载与功能结果;声明冲突不证明已经实际加载失败,豁免也不证明业务通过。",
|
|
635
|
+
];
|
|
636
|
+
case "host-entry-inactive": {
|
|
637
|
+
const state = finding.evidence?.state;
|
|
638
|
+
const step =
|
|
639
|
+
state === "FAILED"
|
|
640
|
+
? "定位该包对应的 FAILED loader 条目,先在本机查看失败原因与同次启动日志,核对导入、初始化失败发生在哪一步。"
|
|
641
|
+
: state === "PENDING"
|
|
642
|
+
? "定位该包对应的 PENDING loader 条目,核对顶层必需 inject 与各依赖服务的提供者是否在同一宿主实际激活。"
|
|
643
|
+
: "在同一目标宿主重新读取该包映射的 loader 条目状态,核对条目身份、启用状态与采样时间,不从说明文本猜测状态。";
|
|
644
|
+
return [
|
|
645
|
+
step,
|
|
646
|
+
"补充同一 profile、DSH 版本和时间的服务观测及 loader 状态;LOADING、禁用或无法映射的条目不能代替 FAILED 或 PENDING 证据。",
|
|
647
|
+
"服务或条目状态恢复后仍需实际操作对应功能并核验结果;激活成功不能代替业务验收。",
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
case "unprovided-dependency": {
|
|
651
|
+
const absent = finding.evidence?.status === "absent";
|
|
652
|
+
const steps = [
|
|
653
|
+
absent
|
|
654
|
+
? "核对本次明确 absent 的服务观测是否属于同一 profile、DSH 版本与依赖作用域,并查看该服务的提供者是否实际激活。"
|
|
655
|
+
: "该依赖仍待确认:补充同一 profile、DSH 版本与有效采样时间的服务观测;省略键或未采样不能确认服务缺失,也不能确认插件挂起。",
|
|
656
|
+
finding.evidence?.hardInject === true
|
|
657
|
+
? "在报告给出的源码位置检查顶层必需 inject 与激活前提,再对照实际 loader 状态;依赖声明本身不能证明已经挂起。"
|
|
658
|
+
: "在报告给出的源码位置区分可选或子作用域依赖与顶层硬依赖;不推断整个插件挂起,实际操作相关功能核验影响。",
|
|
659
|
+
];
|
|
660
|
+
if (finding.evidence?.status === "unreadable")
|
|
661
|
+
steps.push(
|
|
662
|
+
"先在本机核对采样读取失败原因;只补安全的结构观测,不执行 getter 来消除未知。",
|
|
663
|
+
);
|
|
664
|
+
steps.push(
|
|
665
|
+
"核对服务提供者的包声明与实际运行状态;提供声明只是线索,不自动安装依赖或认定某包能提供该服务。",
|
|
666
|
+
);
|
|
667
|
+
return steps;
|
|
668
|
+
}
|
|
669
|
+
case "host-service-missing":
|
|
670
|
+
return [
|
|
671
|
+
"在报告给出的源码位置核对服务读取及必需 inject 的作用域,并确认 absent 观测绑定同一 profile、DSH 版本与采样时间。",
|
|
672
|
+
"核对该服务的实际提供者及 loader 状态,补充相关功能操作结果;服务缺失的推断影响不等于业务失败已实测。",
|
|
673
|
+
];
|
|
674
|
+
case "host-method-missing":
|
|
675
|
+
return [
|
|
676
|
+
"在报告给出的文件与行号核对实际方法调用、Context 绑定及能力守卫,检查是否存在报告尚未识别的兜底。",
|
|
677
|
+
"补充同一目标服务的完整方法结构观测,区分目录线索、未采样方法与明确缺失;实际操作对应功能核验推断影响。",
|
|
678
|
+
"需要替代 API 时先查该目标版本的源码或官方契约并保留出处;本报告不猜测替代方法或修复版本。",
|
|
679
|
+
];
|
|
680
|
+
case "declaration-mismatch":
|
|
681
|
+
return [
|
|
682
|
+
"对照已装插件的兼容声明、精确目标版本及报告中的具体 API 发现,分别核对声明范围与运行时证据。",
|
|
683
|
+
"在已有源码位置检查调用与守卫,并补充同一目标的服务观测和实际功能结果;声明放行不证明业务通过。",
|
|
684
|
+
];
|
|
685
|
+
default:
|
|
686
|
+
return [
|
|
687
|
+
"先核对该发现的类别、依据、已有包内位置和目标版本;缺少位置或状态时保留待确认,不补造证据。",
|
|
688
|
+
"补充同一 profile、DSH 版本与采样时间的相关观测,再实际操作对应功能核验影响;不根据未知类别猜测修复。",
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return { nextSteps: nextSteps };
|
|
694
|
+
})();
|
|
695
|
+
// END GENERATED FINDING GUIDANCE
|
|
696
|
+
|
|
697
|
+
// BEGIN GENERATED UPGRADE CONTRACT
|
|
698
|
+
var UpgradeContract = (function () {
|
|
699
|
+
/** Browser-safe preview contract, also staged into the plugin. No imports. */
|
|
700
|
+
const SUPPORTED_PREVIEW_VERSIONS = Object.freeze([
|
|
701
|
+
"0.2.0-rc.2",
|
|
702
|
+
"0.2.1-rc.2",
|
|
703
|
+
]);
|
|
704
|
+
const record = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
705
|
+
const text = (x) => typeof x === "string" && x.length > 0;
|
|
706
|
+
const strings = (x) =>
|
|
707
|
+
Array.isArray(x) && x.every((v) => typeof v === "string");
|
|
708
|
+
const hash = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
709
|
+
const integer = (x) => Number.isSafeInteger(x) && x >= 0;
|
|
710
|
+
const severityRank = { degraded: 0, "feature-dead": 1, fatal: 2 };
|
|
711
|
+
const certaintyRank = { suspected: 0, certain: 1 };
|
|
712
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
713
|
+
const findingKey = (plugin, f) =>
|
|
714
|
+
JSON.stringify([
|
|
715
|
+
plugin,
|
|
716
|
+
f.category,
|
|
717
|
+
f.service ?? null,
|
|
718
|
+
f.method ?? null,
|
|
719
|
+
f.location?.file ?? null,
|
|
720
|
+
f.location?.line ?? null,
|
|
721
|
+
f.location?.column ?? null,
|
|
722
|
+
f.location?.start ?? null,
|
|
723
|
+
f.location?.end ?? null,
|
|
724
|
+
]);
|
|
725
|
+
|
|
726
|
+
/** Keys retain plugin and exact source span; severity and certainty change independently. */
|
|
727
|
+
function compareUpgradeFindings(currentReport, targetReport) {
|
|
728
|
+
const changes = {
|
|
729
|
+
added: [],
|
|
730
|
+
worsened: [],
|
|
731
|
+
resolved: [],
|
|
732
|
+
unchanged: [],
|
|
733
|
+
improved: [],
|
|
734
|
+
};
|
|
735
|
+
const collect = (report) => {
|
|
736
|
+
const entries = new Map();
|
|
737
|
+
for (const plugin of Object.keys(report.plugins).sort())
|
|
738
|
+
for (const finding of report.plugins[plugin].findings) {
|
|
739
|
+
const key = findingKey(plugin, finding);
|
|
740
|
+
if (!entries.has(key)) entries.set(key, []);
|
|
741
|
+
entries.get(key).push({ plugin, finding });
|
|
742
|
+
}
|
|
743
|
+
return entries;
|
|
744
|
+
};
|
|
745
|
+
const before = collect(currentReport),
|
|
746
|
+
after = collect(targetReport);
|
|
747
|
+
for (const key of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
748
|
+
const old = before.get(key) || [],
|
|
749
|
+
next = after.get(key) || [];
|
|
750
|
+
for (let i = 0; i < Math.max(old.length, next.length); i++) {
|
|
751
|
+
if (!old[i]) {
|
|
752
|
+
changes.added.push(next[i]);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (!next[i]) {
|
|
756
|
+
changes.resolved.push(old[i]);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const a = old[i].finding,
|
|
760
|
+
b = next[i].finding;
|
|
761
|
+
const severity = severityRank[b.severity] - severityRank[a.severity];
|
|
762
|
+
const certainty = certaintyRank[b.certainty] - certaintyRank[a.certainty];
|
|
763
|
+
if (severity > 0 || certainty > 0)
|
|
764
|
+
changes.worsened.push({ plugin: old[i].plugin, before: a, after: b });
|
|
765
|
+
else if (severity < 0 || certainty < 0)
|
|
766
|
+
changes.improved.push({ plugin: old[i].plugin, before: a, after: b });
|
|
767
|
+
else changes.unchanged.push(next[i]);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return changes;
|
|
771
|
+
}
|
|
772
|
+
function upgradeExitCode(changes) {
|
|
773
|
+
const risks = [
|
|
774
|
+
...changes.added.map((x) => x.finding),
|
|
775
|
+
...changes.worsened.map((x) => x.after),
|
|
776
|
+
];
|
|
777
|
+
return risks.some((f) => f.severity === "fatal") ? 2 : risks.length ? 1 : 0;
|
|
778
|
+
}
|
|
779
|
+
function finding(f) {
|
|
780
|
+
return (
|
|
781
|
+
record(f) &&
|
|
782
|
+
text(f.category) &&
|
|
783
|
+
Object.hasOwn(severityRank, f.severity) &&
|
|
784
|
+
Object.hasOwn(certaintyRank, f.certainty) &&
|
|
785
|
+
["inferred", "measured"].includes(f.impactBasis) &&
|
|
786
|
+
text(f.impact) &&
|
|
787
|
+
text(f.basis) &&
|
|
788
|
+
(f.evidence === undefined || record(f.evidence)) &&
|
|
789
|
+
[f.service, f.method].every(
|
|
790
|
+
(x) => x === undefined || x === null || typeof x === "string",
|
|
791
|
+
) &&
|
|
792
|
+
(f.location === null ||
|
|
793
|
+
(record(f.location) &&
|
|
794
|
+
text(f.location.file) &&
|
|
795
|
+
Number.isSafeInteger(f.location.line) &&
|
|
796
|
+
f.location.line > 0 &&
|
|
797
|
+
["column", "start", "end"].every(
|
|
798
|
+
(k) =>
|
|
799
|
+
f.location[k] === undefined ||
|
|
800
|
+
f.location[k] === null ||
|
|
801
|
+
integer(f.location[k]),
|
|
802
|
+
) &&
|
|
803
|
+
(!(integer(f.location.start) && integer(f.location.end)) ||
|
|
804
|
+
f.location.end >= f.location.start)))
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
function report(body, version, artifactHash) {
|
|
808
|
+
if (
|
|
809
|
+
!record(body) ||
|
|
810
|
+
body.schemaVersion !== 1 ||
|
|
811
|
+
body.kind !== "plugin-compat-check/result" ||
|
|
812
|
+
body.ok !== true ||
|
|
813
|
+
body.error !== null ||
|
|
814
|
+
body.dshVersion !== version ||
|
|
815
|
+
!text(body.toolVersion) ||
|
|
816
|
+
body.baseline !== null ||
|
|
817
|
+
body.cache !== null ||
|
|
818
|
+
!record(body.inputs) ||
|
|
819
|
+
body.inputs.artifactSha256 !== artifactHash ||
|
|
820
|
+
body.inputs.hasRuntimeSurface !== false ||
|
|
821
|
+
!record(body.observation) ||
|
|
822
|
+
body.observation.mode !== "catalogue" ||
|
|
823
|
+
body.observation.usable !== false ||
|
|
824
|
+
body.observation.bound !== false ||
|
|
825
|
+
body.observation.profileId !== null ||
|
|
826
|
+
body.observation.dshVersion !== null ||
|
|
827
|
+
body.observation.probedAt !== null ||
|
|
828
|
+
!strings(body.observation.notes) ||
|
|
829
|
+
!record(body.plugins) ||
|
|
830
|
+
!record(body.summary) ||
|
|
831
|
+
!record(body.extraction) ||
|
|
832
|
+
typeof body.extraction.scanComplete !== "boolean" ||
|
|
833
|
+
!Array.isArray(body.extraction.diagnostics) ||
|
|
834
|
+
!Array.isArray(body.extraction.manifests)
|
|
835
|
+
)
|
|
836
|
+
return false;
|
|
837
|
+
const counts = {
|
|
838
|
+
ok: 0,
|
|
839
|
+
partial: 0,
|
|
840
|
+
broken: 0,
|
|
841
|
+
unknown: 0,
|
|
842
|
+
limited: 0,
|
|
843
|
+
vacuousOk: 0,
|
|
844
|
+
};
|
|
845
|
+
for (const [name, p] of Object.entries(body.plugins)) {
|
|
846
|
+
if (
|
|
847
|
+
!record(p) ||
|
|
848
|
+
p.name !== name ||
|
|
849
|
+
!(p.version === null || typeof p.version === "string") ||
|
|
850
|
+
!Object.hasOwn(counts, p.verdict) ||
|
|
851
|
+
["limited", "vacuousOk"].includes(p.verdict) ||
|
|
852
|
+
typeof p.vacuous !== "boolean" ||
|
|
853
|
+
!Array.isArray(p.findings) ||
|
|
854
|
+
!p.findings.every(finding) ||
|
|
855
|
+
!record(p.coverage) ||
|
|
856
|
+
!["complete", "limited"].includes(p.coverage.status) ||
|
|
857
|
+
!integer(p.coverage.verifiedCalls) ||
|
|
858
|
+
!strings(p.coverage.limitations) ||
|
|
859
|
+
!record(p.resolved)
|
|
860
|
+
)
|
|
861
|
+
return false;
|
|
862
|
+
for (const key of [
|
|
863
|
+
"checked",
|
|
864
|
+
"clientSide",
|
|
865
|
+
"filtered",
|
|
866
|
+
"abstained",
|
|
867
|
+
"runtimeFacilities",
|
|
868
|
+
])
|
|
869
|
+
if (!Array.isArray(p[key])) return false;
|
|
870
|
+
if (
|
|
871
|
+
p.verdict !== "unknown" &&
|
|
872
|
+
p.verdict !==
|
|
873
|
+
(p.findings.some((f) => f.severity === "fatal")
|
|
874
|
+
? "broken"
|
|
875
|
+
: p.findings.length
|
|
876
|
+
? "partial"
|
|
877
|
+
: "ok")
|
|
878
|
+
)
|
|
879
|
+
return false;
|
|
880
|
+
if (
|
|
881
|
+
p.vacuous !== (p.verdict === "ok" && p.coverage.verifiedCalls === 0) ||
|
|
882
|
+
(p.coverage.status === "limited") !==
|
|
883
|
+
(p.coverage.limitations.length > 0 ||
|
|
884
|
+
p.clientSide.length > 0 ||
|
|
885
|
+
p.vacuous)
|
|
886
|
+
)
|
|
887
|
+
return false;
|
|
888
|
+
const dispositions = [
|
|
889
|
+
"finding",
|
|
890
|
+
"client-side",
|
|
891
|
+
"self-provided",
|
|
892
|
+
"runtime-facility",
|
|
893
|
+
"filtered",
|
|
894
|
+
"abstained",
|
|
895
|
+
"guarded",
|
|
896
|
+
"no-calls",
|
|
897
|
+
"unknown",
|
|
898
|
+
];
|
|
899
|
+
const checked = (x) =>
|
|
900
|
+
record(x) && text(x.service) && dispositions.includes(x.disposition);
|
|
901
|
+
if (
|
|
902
|
+
!p.checked.every(checked) ||
|
|
903
|
+
!Object.entries(p.resolved).every(
|
|
904
|
+
([svc, x]) => checked(x) && x.service === svc,
|
|
905
|
+
) ||
|
|
906
|
+
!p.clientSide.every(
|
|
907
|
+
(x) => record(x) && text(x.service) && text(x.note),
|
|
908
|
+
) ||
|
|
909
|
+
!p.filtered.every(
|
|
910
|
+
(x) => record(x) && text(x.service) && text(x.method) && text(x.reason),
|
|
911
|
+
) ||
|
|
912
|
+
!p.abstained.every(
|
|
913
|
+
(x) => record(x) && text(x.service) && text(x.basis),
|
|
914
|
+
) ||
|
|
915
|
+
!p.runtimeFacilities.every(
|
|
916
|
+
(x) => record(x) && text(x.name) && text(x.reason),
|
|
917
|
+
)
|
|
918
|
+
)
|
|
919
|
+
return false;
|
|
920
|
+
counts[p.verdict]++;
|
|
921
|
+
if (p.coverage.status === "limited") counts.limited++;
|
|
922
|
+
if (p.vacuous && p.verdict === "ok") counts.vacuousOk++;
|
|
923
|
+
}
|
|
924
|
+
return (
|
|
925
|
+
Object.entries(counts).every(([k, v]) => body.summary[k] === v) &&
|
|
926
|
+
body.exitCode ===
|
|
927
|
+
(counts.broken ? 2 : counts.partial || counts.unknown ? 1 : 0)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
function assertUpgradePreview(value) {
|
|
931
|
+
const errors = [],
|
|
932
|
+
need = (ok, key) => {
|
|
933
|
+
if (!ok) errors.push(key);
|
|
934
|
+
};
|
|
935
|
+
if (!record(value)) throw new Error("升级预检契约不匹配: preview");
|
|
936
|
+
need(
|
|
937
|
+
value.schemaVersion === 1 &&
|
|
938
|
+
value.kind === "plugin-compat-check/upgrade-preview",
|
|
939
|
+
"schemaVersion/kind",
|
|
940
|
+
);
|
|
941
|
+
need(
|
|
942
|
+
value.mode === "static-version-comparison" &&
|
|
943
|
+
["compared", "unsupported-version", "error"].includes(value.status),
|
|
944
|
+
"mode/status",
|
|
945
|
+
);
|
|
946
|
+
need(
|
|
947
|
+
text(value.toolVersion) &&
|
|
948
|
+
value.baseline === null &&
|
|
949
|
+
value.cache === null &&
|
|
950
|
+
strings(value.limitations),
|
|
951
|
+
"identity/isolation",
|
|
952
|
+
);
|
|
953
|
+
need(
|
|
954
|
+
[value.currentVersion, value.targetVersion].every(
|
|
955
|
+
(x) => x === null || typeof x === "string",
|
|
956
|
+
),
|
|
957
|
+
"versions",
|
|
958
|
+
);
|
|
959
|
+
need(
|
|
960
|
+
same(value.supportedVersions, SUPPORTED_PREVIEW_VERSIONS),
|
|
961
|
+
"supportedVersions",
|
|
962
|
+
);
|
|
963
|
+
const groups = ["added", "worsened", "resolved", "unchanged", "improved"];
|
|
964
|
+
need(record(value.changes) && record(value.summary), "changes/summary");
|
|
965
|
+
for (const key of groups) {
|
|
966
|
+
need(Array.isArray(value.changes?.[key]), "changes." + key);
|
|
967
|
+
need(
|
|
968
|
+
integer(value.summary?.[key]) &&
|
|
969
|
+
value.summary[key] === value.changes?.[key]?.length,
|
|
970
|
+
"summary." + key,
|
|
971
|
+
);
|
|
972
|
+
for (const item of Array.isArray(value.changes?.[key])
|
|
973
|
+
? value.changes[key]
|
|
974
|
+
: [])
|
|
975
|
+
need(
|
|
976
|
+
record(item) &&
|
|
977
|
+
text(item.plugin) &&
|
|
978
|
+
(["worsened", "improved"].includes(key)
|
|
979
|
+
? finding(item.before) && finding(item.after)
|
|
980
|
+
: finding(item.finding)),
|
|
981
|
+
"finding." + key,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
need(integer(value.summary?.limitedPlugins), "summary.limitedPlugins");
|
|
985
|
+
if (value.status === "compared") {
|
|
986
|
+
need(
|
|
987
|
+
value.ok === true &&
|
|
988
|
+
value.error === null &&
|
|
989
|
+
SUPPORTED_PREVIEW_VERSIONS.includes(value.currentVersion) &&
|
|
990
|
+
SUPPORTED_PREVIEW_VERSIONS.includes(value.targetVersion),
|
|
991
|
+
"compared.versions/ok",
|
|
992
|
+
);
|
|
993
|
+
need(record(value.catalogs), "catalogs");
|
|
994
|
+
for (const side of ["current", "target"]) {
|
|
995
|
+
const item = value.catalogs?.[side];
|
|
996
|
+
need(
|
|
997
|
+
record(item) &&
|
|
998
|
+
item.version === value[side + "Version"] &&
|
|
999
|
+
text(item.file) &&
|
|
1000
|
+
hash(item.sha256) &&
|
|
1001
|
+
record(item.source) &&
|
|
1002
|
+
text(item.source.host) &&
|
|
1003
|
+
text(item.source.client),
|
|
1004
|
+
"catalog." + side,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
need(
|
|
1008
|
+
record(value.rules) && text(value.rules.file) && hash(value.rules.sha256),
|
|
1009
|
+
"rules",
|
|
1010
|
+
);
|
|
1011
|
+
need(
|
|
1012
|
+
record(value.artifact) &&
|
|
1013
|
+
hash(value.artifact.sha256) &&
|
|
1014
|
+
Array.isArray(value.artifact.packages) &&
|
|
1015
|
+
value.artifact.packages.every(
|
|
1016
|
+
(p) =>
|
|
1017
|
+
record(p) &&
|
|
1018
|
+
text(p.name) &&
|
|
1019
|
+
(p.version === null || typeof p.version === "string") &&
|
|
1020
|
+
hash(p.sha256),
|
|
1021
|
+
),
|
|
1022
|
+
"artifact",
|
|
1023
|
+
);
|
|
1024
|
+
const validReports =
|
|
1025
|
+
report(
|
|
1026
|
+
value.currentReport,
|
|
1027
|
+
value.currentVersion,
|
|
1028
|
+
value.artifact?.sha256,
|
|
1029
|
+
) &&
|
|
1030
|
+
report(value.targetReport, value.targetVersion, value.artifact?.sha256);
|
|
1031
|
+
need(validReports, "canonicalReports");
|
|
1032
|
+
if (validReports) {
|
|
1033
|
+
const identities = (body) =>
|
|
1034
|
+
Object.values(body.plugins)
|
|
1035
|
+
.map((p) => JSON.stringify([p.name, p.version]))
|
|
1036
|
+
.sort();
|
|
1037
|
+
const currentIdentities = identities(value.currentReport);
|
|
1038
|
+
const targetIdentities = identities(value.targetReport);
|
|
1039
|
+
const artifactIdentities = Array.isArray(value.artifact?.packages)
|
|
1040
|
+
? value.artifact.packages.map((p) =>
|
|
1041
|
+
JSON.stringify([p?.name, p?.version]),
|
|
1042
|
+
)
|
|
1043
|
+
: [];
|
|
1044
|
+
const artifactIdentitySet = new Set(artifactIdentities),
|
|
1045
|
+
currentIdentitySet = new Set(currentIdentities);
|
|
1046
|
+
need(
|
|
1047
|
+
same(currentIdentities, targetIdentities),
|
|
1048
|
+
"canonicalReports.same-input",
|
|
1049
|
+
);
|
|
1050
|
+
need(
|
|
1051
|
+
artifactIdentitySet.size === artifactIdentities.length &&
|
|
1052
|
+
currentIdentities.length === artifactIdentities.length &&
|
|
1053
|
+
currentIdentities.every((identity) =>
|
|
1054
|
+
artifactIdentitySet.has(identity),
|
|
1055
|
+
) &&
|
|
1056
|
+
artifactIdentities.every((identity) =>
|
|
1057
|
+
currentIdentitySet.has(identity),
|
|
1058
|
+
),
|
|
1059
|
+
"artifact.package-identities",
|
|
1060
|
+
);
|
|
1061
|
+
need(
|
|
1062
|
+
same(
|
|
1063
|
+
value.changes,
|
|
1064
|
+
compareUpgradeFindings(value.currentReport, value.targetReport),
|
|
1065
|
+
),
|
|
1066
|
+
"changes.evidence",
|
|
1067
|
+
);
|
|
1068
|
+
const limited = new Set(
|
|
1069
|
+
[
|
|
1070
|
+
...Object.values(value.currentReport.plugins),
|
|
1071
|
+
...Object.values(value.targetReport.plugins),
|
|
1072
|
+
]
|
|
1073
|
+
.filter((p) => p.coverage.status === "limited")
|
|
1074
|
+
.map((p) => p.name),
|
|
1075
|
+
);
|
|
1076
|
+
need(
|
|
1077
|
+
value.summary?.limitedPlugins === limited.size,
|
|
1078
|
+
"summary.limitedPlugins/count",
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (!errors.length)
|
|
1082
|
+
need(value.exitCode === upgradeExitCode(value.changes), "exitCode");
|
|
1083
|
+
} else {
|
|
1084
|
+
need(
|
|
1085
|
+
value.ok === false && text(value.error) && value.exitCode === 3,
|
|
1086
|
+
"failure.error/exitCode",
|
|
1087
|
+
);
|
|
1088
|
+
need(
|
|
1089
|
+
["artifact", "catalogs", "rules", "currentReport", "targetReport"].every(
|
|
1090
|
+
(k) => value[k] === null,
|
|
1091
|
+
),
|
|
1092
|
+
"failure.evidence",
|
|
1093
|
+
);
|
|
1094
|
+
need(
|
|
1095
|
+
groups.every((k) => value.changes?.[k]?.length === 0) &&
|
|
1096
|
+
value.summary?.limitedPlugins === 0,
|
|
1097
|
+
"failure.changes",
|
|
1098
|
+
);
|
|
1099
|
+
if (value.status === "unsupported-version")
|
|
1100
|
+
need(
|
|
1101
|
+
!SUPPORTED_PREVIEW_VERSIONS.includes(value.currentVersion) ||
|
|
1102
|
+
!SUPPORTED_PREVIEW_VERSIONS.includes(value.targetVersion),
|
|
1103
|
+
"unsupported-version",
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
if (errors.length)
|
|
1107
|
+
throw new Error("升级预检契约不匹配: " + errors.join(", "));
|
|
1108
|
+
return value;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const UPGRADE_PREVIEW_LIMITATIONS = Object.freeze([
|
|
1112
|
+
"静态版本预测未认证未来环境或业务通过;没有新增风险仅表示本次未发现新增静态风险。",
|
|
1113
|
+
"对同一组插件的私有字节副本联合静态提取一次,识别名集合取两个目录的并集;随后各按对应版本目录独立判定,不能复用普通实时体检证明未来环境。",
|
|
1114
|
+
"API目录不是运行时服务面;确定性、推断影响、覆盖限制、无已验证调用及客户端待验证信息均保留。",
|
|
1115
|
+
"当前宿主运行时、探针、条目状态、profile豁免、缓存和基线不参与预测;不执行插件代码、不安装或升级。",
|
|
1116
|
+
"输入采用有界复制及两次一致性观察,检测普通并发变更;不是操作系统原子快照。",
|
|
1117
|
+
"退出码仅反映新增或变重风险:0无新增,1新增非致命,2新增致命,3预检不可用;目标版本原有风险仍列出。",
|
|
1118
|
+
]);
|
|
1119
|
+
const emptyChanges = () => ({
|
|
1120
|
+
added: [],
|
|
1121
|
+
worsened: [],
|
|
1122
|
+
resolved: [],
|
|
1123
|
+
unchanged: [],
|
|
1124
|
+
improved: [],
|
|
1125
|
+
});
|
|
1126
|
+
function createUpgradeFailure(error, options = {}) {
|
|
1127
|
+
if (error && typeof error === "object" && !(error instanceof Error)) {
|
|
1128
|
+
options = error;
|
|
1129
|
+
error = options.error;
|
|
1130
|
+
}
|
|
1131
|
+
const value = {
|
|
1132
|
+
schemaVersion: 1,
|
|
1133
|
+
kind: "plugin-compat-check/upgrade-preview",
|
|
1134
|
+
toolVersion: text(options.toolVersion) ? options.toolVersion : "unknown",
|
|
1135
|
+
mode: "static-version-comparison",
|
|
1136
|
+
ok: false,
|
|
1137
|
+
error: String(error?.message || error || "升级预检不可用"),
|
|
1138
|
+
exitCode: 3,
|
|
1139
|
+
status:
|
|
1140
|
+
options.status === "unsupported-version"
|
|
1141
|
+
? "unsupported-version"
|
|
1142
|
+
: "error",
|
|
1143
|
+
currentVersion:
|
|
1144
|
+
typeof options.currentVersion === "string"
|
|
1145
|
+
? options.currentVersion
|
|
1146
|
+
: null,
|
|
1147
|
+
targetVersion:
|
|
1148
|
+
typeof options.targetVersion === "string" ? options.targetVersion : null,
|
|
1149
|
+
supportedVersions: [...SUPPORTED_PREVIEW_VERSIONS],
|
|
1150
|
+
catalogs: null,
|
|
1151
|
+
artifact: null,
|
|
1152
|
+
rules: null,
|
|
1153
|
+
currentReport: null,
|
|
1154
|
+
targetReport: null,
|
|
1155
|
+
changes: emptyChanges(),
|
|
1156
|
+
summary: {
|
|
1157
|
+
added: 0,
|
|
1158
|
+
worsened: 0,
|
|
1159
|
+
resolved: 0,
|
|
1160
|
+
unchanged: 0,
|
|
1161
|
+
improved: 0,
|
|
1162
|
+
limitedPlugins: 0,
|
|
1163
|
+
},
|
|
1164
|
+
limitations: [...UPGRADE_PREVIEW_LIMITATIONS],
|
|
1165
|
+
baseline: null,
|
|
1166
|
+
cache: null,
|
|
1167
|
+
};
|
|
1168
|
+
return assertUpgradePreview(value);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return { assertUpgradePreview: assertUpgradePreview, SUPPORTED_PREVIEW_VERSIONS: SUPPORTED_PREVIEW_VERSIONS };
|
|
1172
|
+
})();
|
|
1173
|
+
// END GENERATED UPGRADE CONTRACT
|
|
1174
|
+
|
|
1175
|
+
// BEGIN GENERATED DEPENDENCY GRAPH
|
|
1176
|
+
var DependencyGraphContract = (function () {
|
|
1177
|
+
/** Browser-safe explanatory graph. Declarations never establish activation or failure. */
|
|
1178
|
+
const GRAPH_KIND = "plugin-compat-check/dependency-graph";
|
|
1179
|
+
const GRAPH_LIMITS = Object.freeze({
|
|
1180
|
+
nodes: 1000,
|
|
1181
|
+
edges: 4000,
|
|
1182
|
+
traversal: 20000,
|
|
1183
|
+
items: 20000,
|
|
1184
|
+
});
|
|
1185
|
+
const graphRecord = (value) =>
|
|
1186
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1187
|
+
const graphText = (value) => typeof value === "string" && value.length > 0;
|
|
1188
|
+
const graphArray = (value) => (Array.isArray(value) ? value : []);
|
|
1189
|
+
const graphSides = (value) => {
|
|
1190
|
+
const sides = graphArray(value).filter(
|
|
1191
|
+
(side) => side === "host" || side === "client",
|
|
1192
|
+
);
|
|
1193
|
+
return sides.length ? [...new Set(sides)] : ["unknown"];
|
|
1194
|
+
};
|
|
1195
|
+
const graphId = (...parts) => JSON.stringify(parts);
|
|
1196
|
+
const graphSource = (value) => ({
|
|
1197
|
+
file: typeof value?.file === "string" ? value.file : null,
|
|
1198
|
+
line: Number.isSafeInteger(value?.line) ? value.line : null,
|
|
1199
|
+
start: Number.isSafeInteger(value?.start) ? value.start : null,
|
|
1200
|
+
end: Number.isSafeInteger(value?.end) ? value.end : null,
|
|
1201
|
+
});
|
|
1202
|
+
const graphRequirement = (kind) =>
|
|
1203
|
+
kind === "required-inject"
|
|
1204
|
+
? "required"
|
|
1205
|
+
: kind === "scoped-inject"
|
|
1206
|
+
? "subcontext"
|
|
1207
|
+
: ["optional-read", "read"].includes(kind)
|
|
1208
|
+
? "optional"
|
|
1209
|
+
: "unknown";
|
|
1210
|
+
|
|
1211
|
+
function buildDependencyGraph({
|
|
1212
|
+
extraction,
|
|
1213
|
+
verdict,
|
|
1214
|
+
entries,
|
|
1215
|
+
limits = {},
|
|
1216
|
+
} = {}) {
|
|
1217
|
+
const report = extraction?.report || extraction || {};
|
|
1218
|
+
const budget = Object.fromEntries(
|
|
1219
|
+
Object.entries(GRAPH_LIMITS).map(([key, max]) => [
|
|
1220
|
+
key,
|
|
1221
|
+
Number.isSafeInteger(limits[key]) && limits[key] >= 0
|
|
1222
|
+
? Math.min(limits[key], max)
|
|
1223
|
+
: max,
|
|
1224
|
+
]),
|
|
1225
|
+
);
|
|
1226
|
+
const nodes = [],
|
|
1227
|
+
edges = [],
|
|
1228
|
+
impacts = [],
|
|
1229
|
+
cycles = [],
|
|
1230
|
+
reasons = new Set();
|
|
1231
|
+
const nodeMap = new Map(),
|
|
1232
|
+
edgeKeys = new Set(),
|
|
1233
|
+
plugins = new Map();
|
|
1234
|
+
let truncated = false,
|
|
1235
|
+
items = 0,
|
|
1236
|
+
steps = 0,
|
|
1237
|
+
pathItems = 0;
|
|
1238
|
+
const limit = (reason) => {
|
|
1239
|
+
truncated = true;
|
|
1240
|
+
reasons.add(reason);
|
|
1241
|
+
};
|
|
1242
|
+
const consume = () => {
|
|
1243
|
+
if (items >= budget.items) {
|
|
1244
|
+
limit("输入条目达到预算,关系及来源未完整读取");
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
items++;
|
|
1248
|
+
return true;
|
|
1249
|
+
};
|
|
1250
|
+
const addNode = (node) => {
|
|
1251
|
+
if (nodeMap.has(node.id)) return nodeMap.get(node.id);
|
|
1252
|
+
if (nodes.length >= budget.nodes) {
|
|
1253
|
+
limit("节点达到预算,关系图已截断");
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
nodes.push(node);
|
|
1257
|
+
nodeMap.set(node.id, node);
|
|
1258
|
+
return node;
|
|
1259
|
+
};
|
|
1260
|
+
const addEdge = (edge) => {
|
|
1261
|
+
if (!nodeMap.has(edge.from) || !nodeMap.has(edge.to)) return;
|
|
1262
|
+
const key = JSON.stringify(edge);
|
|
1263
|
+
if (edgeKeys.has(key)) return;
|
|
1264
|
+
if (edges.length >= budget.edges) {
|
|
1265
|
+
limit("关系达到预算,关系图已截断");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
edgeKeys.add(key);
|
|
1269
|
+
edges.push({ id: `edge:${edges.length}`, ...edge });
|
|
1270
|
+
};
|
|
1271
|
+
const service = (name, side) =>
|
|
1272
|
+
addNode({
|
|
1273
|
+
id: graphId("service", side, name),
|
|
1274
|
+
kind: "service",
|
|
1275
|
+
name,
|
|
1276
|
+
side,
|
|
1277
|
+
state: "unknown",
|
|
1278
|
+
});
|
|
1279
|
+
const entryMap = new Map();
|
|
1280
|
+
const rows = Array.isArray(entries)
|
|
1281
|
+
? entries
|
|
1282
|
+
: graphArray(verdict?.observation?.entries?.plugins);
|
|
1283
|
+
for (const row of rows) {
|
|
1284
|
+
if (!consume()) break;
|
|
1285
|
+
if (!graphRecord(row) || !graphText(row.packageName) || row.group) continue;
|
|
1286
|
+
if (!entryMap.has(row.packageName)) entryMap.set(row.packageName, []);
|
|
1287
|
+
entryMap.get(row.packageName).push({
|
|
1288
|
+
id: typeof row.id === "string" ? row.id : null,
|
|
1289
|
+
state: typeof row.state === "string" ? row.state : "unknown",
|
|
1290
|
+
disabled: row.disabled === true,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (report.scanComplete !== true)
|
|
1294
|
+
reasons.add("静态提取覆盖不完整或未声明,关系可能缺失");
|
|
1295
|
+
for (const [name, p] of Object.entries(report.perPlugin || {})) {
|
|
1296
|
+
if (!consume()) break;
|
|
1297
|
+
if (!graphRecord(p)) continue;
|
|
1298
|
+
const entryRows = entryMap.get(name) || [];
|
|
1299
|
+
const enabled = entryRows.filter((row) => !row.disabled);
|
|
1300
|
+
const states = [...new Set(enabled.map((row) => row.state))];
|
|
1301
|
+
const loaderState =
|
|
1302
|
+
entryRows.length && !enabled.length
|
|
1303
|
+
? "disabled"
|
|
1304
|
+
: states.length === 1
|
|
1305
|
+
? states[0]
|
|
1306
|
+
: states.length > 1
|
|
1307
|
+
? "mixed"
|
|
1308
|
+
: "unknown";
|
|
1309
|
+
if (loaderState === "unknown")
|
|
1310
|
+
reasons.add("部分插件未提供可映射 loader 条目,状态为未知");
|
|
1311
|
+
const node = addNode({
|
|
1312
|
+
id: graphId("plugin", name),
|
|
1313
|
+
kind: "plugin",
|
|
1314
|
+
name,
|
|
1315
|
+
entries: entryRows,
|
|
1316
|
+
loaderState,
|
|
1317
|
+
subContexts: [],
|
|
1318
|
+
});
|
|
1319
|
+
if (!node) break;
|
|
1320
|
+
plugins.set(name, { node, plugin: p });
|
|
1321
|
+
if (graphArray(p.diagnostics).length)
|
|
1322
|
+
reasons.add("部分源码存在提取诊断,关系可能缺失");
|
|
1323
|
+
for (const context of graphArray(p.subContexts)) {
|
|
1324
|
+
if (!consume()) break;
|
|
1325
|
+
if (!graphRecord(context)) continue;
|
|
1326
|
+
const names = [];
|
|
1327
|
+
for (const name of graphArray(context.names)) {
|
|
1328
|
+
if (!consume()) break;
|
|
1329
|
+
if (graphText(name)) names.push(name);
|
|
1330
|
+
}
|
|
1331
|
+
node.subContexts.push({
|
|
1332
|
+
file: typeof context.file === "string" ? context.file : null,
|
|
1333
|
+
names,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
for (const { node, plugin: p } of plugins.values()) {
|
|
1338
|
+
const declaredRelations = new Set();
|
|
1339
|
+
for (const dependency of graphArray(p.dependencies)) {
|
|
1340
|
+
if (!consume()) break;
|
|
1341
|
+
if (!graphText(dependency?.service)) continue;
|
|
1342
|
+
const sides = graphSides(dependency.sides);
|
|
1343
|
+
if (sides.includes("unknown"))
|
|
1344
|
+
reasons.add("部分依赖端归属未知,不用于传播推导");
|
|
1345
|
+
if (graphRequirement(dependency.kind) === "unknown")
|
|
1346
|
+
reasons.add("部分依赖必需性未证实,保留未知标记");
|
|
1347
|
+
for (const side of sides) {
|
|
1348
|
+
declaredRelations.add(graphId(dependency.service, side));
|
|
1349
|
+
const target = service(dependency.service, side);
|
|
1350
|
+
if (target)
|
|
1351
|
+
addEdge({
|
|
1352
|
+
kind: "dependency",
|
|
1353
|
+
from: node.id,
|
|
1354
|
+
to: target.id,
|
|
1355
|
+
service: dependency.service,
|
|
1356
|
+
side,
|
|
1357
|
+
requirement: graphRequirement(dependency.kind),
|
|
1358
|
+
declarationKind:
|
|
1359
|
+
typeof dependency.kind === "string" ? dependency.kind : "unknown",
|
|
1360
|
+
source: graphSource(dependency),
|
|
1361
|
+
potential: true,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
for (const site of graphArray(p.callSites)) {
|
|
1366
|
+
if (!consume()) break;
|
|
1367
|
+
if (!graphText(site?.service)) continue;
|
|
1368
|
+
for (const side of graphSides(site.sides)) {
|
|
1369
|
+
const key = graphId(site.service, side);
|
|
1370
|
+
if (declaredRelations.has(key)) continue;
|
|
1371
|
+
declaredRelations.add(key);
|
|
1372
|
+
reasons.add("部分服务只见调用点,依赖必需性未知");
|
|
1373
|
+
const target = service(site.service, side);
|
|
1374
|
+
if (target)
|
|
1375
|
+
addEdge({
|
|
1376
|
+
kind: "dependency",
|
|
1377
|
+
from: node.id,
|
|
1378
|
+
to: target.id,
|
|
1379
|
+
service: site.service,
|
|
1380
|
+
side,
|
|
1381
|
+
requirement: "unknown",
|
|
1382
|
+
declarationKind: "call",
|
|
1383
|
+
source: graphSource(site),
|
|
1384
|
+
potential: true,
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const provided = new Set();
|
|
1389
|
+
for (const site of graphArray(p.provideSites)) {
|
|
1390
|
+
if (!consume()) break;
|
|
1391
|
+
if (!graphText(site?.service)) continue;
|
|
1392
|
+
provided.add(site.service);
|
|
1393
|
+
for (const side of graphSides(site.sides)) {
|
|
1394
|
+
if (side === "unknown")
|
|
1395
|
+
reasons.add("部分提供声明缺少端归属,只列候选,不用于传播推导");
|
|
1396
|
+
const target = service(site.service, side);
|
|
1397
|
+
if (target)
|
|
1398
|
+
addEdge({
|
|
1399
|
+
kind: "candidate-provider",
|
|
1400
|
+
from: node.id,
|
|
1401
|
+
to: target.id,
|
|
1402
|
+
service: site.service,
|
|
1403
|
+
side,
|
|
1404
|
+
source: graphSource(site),
|
|
1405
|
+
sideMatch: side === "unknown" ? "unverified" : "source",
|
|
1406
|
+
potential: true,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
for (const name of graphArray(p.providedServices)) {
|
|
1411
|
+
if (!consume()) break;
|
|
1412
|
+
if (!graphText(name) || provided.has(name)) continue;
|
|
1413
|
+
reasons.add("部分提供声明缺少端归属,只列候选,不用于传播推导");
|
|
1414
|
+
const target = service(name, "unknown");
|
|
1415
|
+
if (target)
|
|
1416
|
+
addEdge({
|
|
1417
|
+
kind: "candidate-provider",
|
|
1418
|
+
from: node.id,
|
|
1419
|
+
to: target.id,
|
|
1420
|
+
service: name,
|
|
1421
|
+
side: "unknown",
|
|
1422
|
+
source: graphSource(null),
|
|
1423
|
+
sideMatch: "unverified",
|
|
1424
|
+
potential: true,
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
if (
|
|
1428
|
+
p.clientInject !== undefined &&
|
|
1429
|
+
p.clientInject !== null &&
|
|
1430
|
+
!Array.isArray(p.clientInject)
|
|
1431
|
+
)
|
|
1432
|
+
reasons.add("客户端包 inject 声明格式未识别,未猜测服务关系");
|
|
1433
|
+
for (const name of graphArray(p.clientInject)) {
|
|
1434
|
+
if (!consume()) break;
|
|
1435
|
+
if (!graphText(name)) {
|
|
1436
|
+
reasons.add("客户端包 inject 含未识别条目");
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const target = addNode({
|
|
1440
|
+
id: graphId("client-package", name),
|
|
1441
|
+
kind: "client-package",
|
|
1442
|
+
name,
|
|
1443
|
+
side: "client",
|
|
1444
|
+
});
|
|
1445
|
+
if (target)
|
|
1446
|
+
addEdge({
|
|
1447
|
+
kind: "client-package-inject",
|
|
1448
|
+
from: node.id,
|
|
1449
|
+
to: target.id,
|
|
1450
|
+
side: "client",
|
|
1451
|
+
potential: true,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const provisions = new Map(),
|
|
1456
|
+
consumers = new Map(),
|
|
1457
|
+
cycleKeys = new Set();
|
|
1458
|
+
for (const edge of edges) {
|
|
1459
|
+
if (edge.side === "unknown") continue;
|
|
1460
|
+
const map =
|
|
1461
|
+
edge.kind === "candidate-provider"
|
|
1462
|
+
? provisions
|
|
1463
|
+
: edge.kind === "dependency"
|
|
1464
|
+
? consumers
|
|
1465
|
+
: null;
|
|
1466
|
+
const key = edge.kind === "candidate-provider" ? edge.from : edge.to;
|
|
1467
|
+
if (map) {
|
|
1468
|
+
if (!map.has(key)) map.set(key, []);
|
|
1469
|
+
map.get(key).push(edge);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const step = () => {
|
|
1473
|
+
if (steps >= budget.traversal) {
|
|
1474
|
+
limit("传播遍历达到预算,潜在影响及环未完整枚举");
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
steps++;
|
|
1478
|
+
return true;
|
|
1479
|
+
};
|
|
1480
|
+
for (const { node } of plugins.values()) {
|
|
1481
|
+
const affected = [],
|
|
1482
|
+
queue = [
|
|
1483
|
+
{
|
|
1484
|
+
plugin: node.id,
|
|
1485
|
+
side: null,
|
|
1486
|
+
path: [],
|
|
1487
|
+
requirements: [],
|
|
1488
|
+
plugins: [node.id],
|
|
1489
|
+
},
|
|
1490
|
+
],
|
|
1491
|
+
seen = new Set();
|
|
1492
|
+
for (let head = 0; head < queue.length; head++) {
|
|
1493
|
+
if (!step()) break;
|
|
1494
|
+
const current = queue[head];
|
|
1495
|
+
for (const provision of provisions.get(current.plugin) || []) {
|
|
1496
|
+
if (!step()) break;
|
|
1497
|
+
if (current.side && provision.side !== current.side) continue;
|
|
1498
|
+
for (const dependency of consumers.get(provision.to) || []) {
|
|
1499
|
+
if (!step()) break;
|
|
1500
|
+
const path = [...current.path, provision.id, dependency.id];
|
|
1501
|
+
const requirement = [...current.requirements, dependency.requirement];
|
|
1502
|
+
const pluginPath = [...current.plugins, dependency.from];
|
|
1503
|
+
if (pathItems + path.length > budget.traversal) {
|
|
1504
|
+
limit("关系路径达到预算,潜在影响及环未完整列出");
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (dependency.from === node.id) {
|
|
1508
|
+
const ring = pluginPath.slice(0, -1);
|
|
1509
|
+
const key = JSON.stringify([
|
|
1510
|
+
provision.side,
|
|
1511
|
+
[
|
|
1512
|
+
...ring.map((_, i) =>
|
|
1513
|
+
JSON.stringify([...ring.slice(i), ...ring.slice(0, i)]),
|
|
1514
|
+
),
|
|
1515
|
+
].sort()[0],
|
|
1516
|
+
]);
|
|
1517
|
+
if (!cycleKeys.has(key)) {
|
|
1518
|
+
pathItems += path.length;
|
|
1519
|
+
cycleKeys.add(key);
|
|
1520
|
+
cycles.push({
|
|
1521
|
+
plugins: pluginPath,
|
|
1522
|
+
side: provision.side,
|
|
1523
|
+
path,
|
|
1524
|
+
potential: true,
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
const key = graphId(dependency.from, provision.side);
|
|
1530
|
+
if (seen.has(key)) continue;
|
|
1531
|
+
seen.add(key);
|
|
1532
|
+
pathItems += path.length;
|
|
1533
|
+
affected.push({
|
|
1534
|
+
plugin: dependency.from,
|
|
1535
|
+
side: provision.side,
|
|
1536
|
+
path,
|
|
1537
|
+
requirements: requirement,
|
|
1538
|
+
potential: true,
|
|
1539
|
+
});
|
|
1540
|
+
queue.push({
|
|
1541
|
+
plugin: dependency.from,
|
|
1542
|
+
side: provision.side,
|
|
1543
|
+
path,
|
|
1544
|
+
requirements: requirement,
|
|
1545
|
+
plugins: pluginPath,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
impacts.push({ plugin: node.id, potential: true, affected });
|
|
1551
|
+
}
|
|
1552
|
+
return {
|
|
1553
|
+
schemaVersion: 1,
|
|
1554
|
+
kind: GRAPH_KIND,
|
|
1555
|
+
nodes,
|
|
1556
|
+
edges,
|
|
1557
|
+
impacts,
|
|
1558
|
+
cycles,
|
|
1559
|
+
coverage: {
|
|
1560
|
+
status: reasons.size ? "limited" : "complete",
|
|
1561
|
+
truncated,
|
|
1562
|
+
reasons: [...reasons],
|
|
1563
|
+
limits: budget,
|
|
1564
|
+
},
|
|
1565
|
+
notes: [
|
|
1566
|
+
"提供声明仅为候选线索;loader 条目状态不证明服务已注册。",
|
|
1567
|
+
"多个候选不表示实际选择了其中之一;替代提供者、禁用条目及可选依赖均需在真实宿主复核。",
|
|
1568
|
+
"受影响者及环仅表示同端声明关系的潜在影响,不表示实际挂起或失败;可选及子作用域依赖不等于根插件阻断。",
|
|
1569
|
+
"host/client 独立;client 包 inject 不当作 host 服务依赖;端未知的声明不用于传播推导。",
|
|
1570
|
+
],
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Reject malformed optional views before rendering without changing the core report. */
|
|
1575
|
+
function validateDependencyGraph(graph) {
|
|
1576
|
+
if (
|
|
1577
|
+
!graphRecord(graph) ||
|
|
1578
|
+
graph.kind !== GRAPH_KIND ||
|
|
1579
|
+
graph.schemaVersion !== 1
|
|
1580
|
+
)
|
|
1581
|
+
return false;
|
|
1582
|
+
const coverage = graph.coverage;
|
|
1583
|
+
if (
|
|
1584
|
+
!graphRecord(coverage) ||
|
|
1585
|
+
!["limited", "complete"].includes(coverage.status) ||
|
|
1586
|
+
typeof coverage.truncated !== "boolean" ||
|
|
1587
|
+
!Array.isArray(coverage.reasons) ||
|
|
1588
|
+
coverage.reasons.length > 100 ||
|
|
1589
|
+
!coverage.reasons.every(graphText) ||
|
|
1590
|
+
!graphRecord(coverage.limits)
|
|
1591
|
+
)
|
|
1592
|
+
return false;
|
|
1593
|
+
if (
|
|
1594
|
+
(coverage.truncated && coverage.status !== "limited") ||
|
|
1595
|
+
(coverage.status === "complete" && coverage.reasons.length) ||
|
|
1596
|
+
(coverage.status === "limited" && !coverage.reasons.length)
|
|
1597
|
+
)
|
|
1598
|
+
return false;
|
|
1599
|
+
for (const [key, max] of Object.entries(GRAPH_LIMITS))
|
|
1600
|
+
if (
|
|
1601
|
+
!Number.isSafeInteger(coverage.limits[key]) ||
|
|
1602
|
+
coverage.limits[key] < 0 ||
|
|
1603
|
+
coverage.limits[key] > max
|
|
1604
|
+
)
|
|
1605
|
+
return false;
|
|
1606
|
+
if (
|
|
1607
|
+
!Array.isArray(graph.nodes) ||
|
|
1608
|
+
graph.nodes.length > coverage.limits.nodes ||
|
|
1609
|
+
!Array.isArray(graph.edges) ||
|
|
1610
|
+
graph.edges.length > coverage.limits.edges ||
|
|
1611
|
+
!Array.isArray(graph.impacts) ||
|
|
1612
|
+
!Array.isArray(graph.cycles) ||
|
|
1613
|
+
!Array.isArray(graph.notes) ||
|
|
1614
|
+
graph.notes.length > 100 ||
|
|
1615
|
+
!graph.notes.every(graphText)
|
|
1616
|
+
)
|
|
1617
|
+
return false;
|
|
1618
|
+
const nodes = new Map(),
|
|
1619
|
+
edges = new Map();
|
|
1620
|
+
const validSide = (side) => ["host", "client", "unknown"].includes(side);
|
|
1621
|
+
const nonnegative = (value) =>
|
|
1622
|
+
value === null || (Number.isSafeInteger(value) && value >= 0);
|
|
1623
|
+
const source = (value) =>
|
|
1624
|
+
graphRecord(value) &&
|
|
1625
|
+
(value.file === null || typeof value.file === "string") &&
|
|
1626
|
+
nonnegative(value.line) &&
|
|
1627
|
+
nonnegative(value.start) &&
|
|
1628
|
+
nonnegative(value.end) &&
|
|
1629
|
+
(value.start === null || value.end === null || value.end >= value.start);
|
|
1630
|
+
let nestedItems = 0,
|
|
1631
|
+
pathItems = 0;
|
|
1632
|
+
for (const node of graph.nodes) {
|
|
1633
|
+
if (
|
|
1634
|
+
!graphRecord(node) ||
|
|
1635
|
+
!graphText(node.id) ||
|
|
1636
|
+
nodes.has(node.id) ||
|
|
1637
|
+
!graphText(node.name) ||
|
|
1638
|
+
!["plugin", "service", "client-package"].includes(node.kind)
|
|
1639
|
+
)
|
|
1640
|
+
return false;
|
|
1641
|
+
if (node.kind === "plugin") {
|
|
1642
|
+
if (
|
|
1643
|
+
!graphText(node.loaderState) ||
|
|
1644
|
+
!Array.isArray(node.entries) ||
|
|
1645
|
+
!Array.isArray(node.subContexts)
|
|
1646
|
+
)
|
|
1647
|
+
return false;
|
|
1648
|
+
nestedItems += node.entries.length + node.subContexts.length;
|
|
1649
|
+
if (nestedItems > coverage.limits.items) return false;
|
|
1650
|
+
if (
|
|
1651
|
+
!node.entries.every(
|
|
1652
|
+
(row) =>
|
|
1653
|
+
graphRecord(row) &&
|
|
1654
|
+
(row.id === null || typeof row.id === "string") &&
|
|
1655
|
+
graphText(row.state) &&
|
|
1656
|
+
typeof row.disabled === "boolean",
|
|
1657
|
+
)
|
|
1658
|
+
)
|
|
1659
|
+
return false;
|
|
1660
|
+
for (const context of node.subContexts) {
|
|
1661
|
+
if (
|
|
1662
|
+
!graphRecord(context) ||
|
|
1663
|
+
(context.file !== null && typeof context.file !== "string") ||
|
|
1664
|
+
!Array.isArray(context.names)
|
|
1665
|
+
)
|
|
1666
|
+
return false;
|
|
1667
|
+
nestedItems += context.names.length;
|
|
1668
|
+
if (
|
|
1669
|
+
nestedItems > coverage.limits.items ||
|
|
1670
|
+
!context.names.every(graphText)
|
|
1671
|
+
)
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
} else if (
|
|
1675
|
+
!validSide(node.side) ||
|
|
1676
|
+
(node.kind === "service" && node.state !== "unknown") ||
|
|
1677
|
+
(node.kind === "client-package" && node.side !== "client")
|
|
1678
|
+
)
|
|
1679
|
+
return false;
|
|
1680
|
+
nodes.set(node.id, node);
|
|
1681
|
+
}
|
|
1682
|
+
for (const edge of graph.edges) {
|
|
1683
|
+
if (
|
|
1684
|
+
!graphRecord(edge) ||
|
|
1685
|
+
!graphText(edge.id) ||
|
|
1686
|
+
edges.has(edge.id) ||
|
|
1687
|
+
!nodes.has(edge.from) ||
|
|
1688
|
+
!nodes.has(edge.to) ||
|
|
1689
|
+
nodes.get(edge.from).kind !== "plugin" ||
|
|
1690
|
+
!validSide(edge.side) ||
|
|
1691
|
+
edge.potential !== true
|
|
1692
|
+
)
|
|
1693
|
+
return false;
|
|
1694
|
+
if (edge.kind === "client-package-inject") {
|
|
1695
|
+
if (
|
|
1696
|
+
nodes.get(edge.to).kind !== "client-package" ||
|
|
1697
|
+
edge.side !== "client"
|
|
1698
|
+
)
|
|
1699
|
+
return false;
|
|
1700
|
+
} else if (["dependency", "candidate-provider"].includes(edge.kind)) {
|
|
1701
|
+
if (
|
|
1702
|
+
nodes.get(edge.to).kind !== "service" ||
|
|
1703
|
+
!graphText(edge.service) ||
|
|
1704
|
+
nodes.get(edge.to).name !== edge.service ||
|
|
1705
|
+
nodes.get(edge.to).side !== edge.side ||
|
|
1706
|
+
!source(edge.source)
|
|
1707
|
+
)
|
|
1708
|
+
return false;
|
|
1709
|
+
if (
|
|
1710
|
+
edge.kind === "dependency" &&
|
|
1711
|
+
(!["required", "optional", "subcontext", "unknown"].includes(
|
|
1712
|
+
edge.requirement,
|
|
1713
|
+
) ||
|
|
1714
|
+
!graphText(edge.declarationKind) ||
|
|
1715
|
+
edge.requirement !== graphRequirement(edge.declarationKind))
|
|
1716
|
+
)
|
|
1717
|
+
return false;
|
|
1718
|
+
if (
|
|
1719
|
+
edge.kind === "candidate-provider" &&
|
|
1720
|
+
edge.sideMatch !== (edge.side === "unknown" ? "unverified" : "source")
|
|
1721
|
+
)
|
|
1722
|
+
return false;
|
|
1723
|
+
} else return false;
|
|
1724
|
+
edges.set(edge.id, edge);
|
|
1725
|
+
}
|
|
1726
|
+
const validPath = (path, from, to, side, requirements, pluginPath) => {
|
|
1727
|
+
if (!Array.isArray(path) || path.length < 2 || path.length % 2)
|
|
1728
|
+
return false;
|
|
1729
|
+
pathItems += path.length;
|
|
1730
|
+
if (pathItems > coverage.limits.traversal) return false;
|
|
1731
|
+
if (
|
|
1732
|
+
requirements &&
|
|
1733
|
+
(!Array.isArray(requirements) || requirements.length * 2 !== path.length)
|
|
1734
|
+
)
|
|
1735
|
+
return false;
|
|
1736
|
+
if (
|
|
1737
|
+
pluginPath &&
|
|
1738
|
+
(!Array.isArray(pluginPath) ||
|
|
1739
|
+
pluginPath.length !== path.length / 2 + 1 ||
|
|
1740
|
+
pluginPath[0] !== from)
|
|
1741
|
+
)
|
|
1742
|
+
return false;
|
|
1743
|
+
let current = from;
|
|
1744
|
+
for (let i = 0; i < path.length; i += 2) {
|
|
1745
|
+
const provider = edges.get(path[i]),
|
|
1746
|
+
consumer = edges.get(path[i + 1]);
|
|
1747
|
+
if (
|
|
1748
|
+
provider?.kind !== "candidate-provider" ||
|
|
1749
|
+
consumer?.kind !== "dependency" ||
|
|
1750
|
+
provider.side !== side ||
|
|
1751
|
+
consumer.side !== side ||
|
|
1752
|
+
provider.from !== current ||
|
|
1753
|
+
provider.to !== consumer.to
|
|
1754
|
+
)
|
|
1755
|
+
return false;
|
|
1756
|
+
if (requirements && requirements[i / 2] !== consumer.requirement)
|
|
1757
|
+
return false;
|
|
1758
|
+
current = consumer.from;
|
|
1759
|
+
if (pluginPath && pluginPath[i / 2 + 1] !== current) return false;
|
|
1760
|
+
}
|
|
1761
|
+
return current === to;
|
|
1762
|
+
};
|
|
1763
|
+
if (
|
|
1764
|
+
graph.impacts.length > graph.nodes.length ||
|
|
1765
|
+
graph.cycles.length > coverage.limits.traversal
|
|
1766
|
+
)
|
|
1767
|
+
return false;
|
|
1768
|
+
const roots = new Set();
|
|
1769
|
+
let relationships = 0;
|
|
1770
|
+
for (const impact of graph.impacts) {
|
|
1771
|
+
if (
|
|
1772
|
+
!graphRecord(impact) ||
|
|
1773
|
+
nodes.get(impact.plugin)?.kind !== "plugin" ||
|
|
1774
|
+
roots.has(impact.plugin) ||
|
|
1775
|
+
impact.potential !== true ||
|
|
1776
|
+
!Array.isArray(impact.affected) ||
|
|
1777
|
+
impact.affected.length > coverage.limits.traversal
|
|
1778
|
+
)
|
|
1779
|
+
return false;
|
|
1780
|
+
roots.add(impact.plugin);
|
|
1781
|
+
const affected = new Set();
|
|
1782
|
+
for (const row of impact.affected) {
|
|
1783
|
+
if (
|
|
1784
|
+
++relationships > coverage.limits.traversal ||
|
|
1785
|
+
!graphRecord(row) ||
|
|
1786
|
+
nodes.get(row.plugin)?.kind !== "plugin" ||
|
|
1787
|
+
row.plugin === impact.plugin ||
|
|
1788
|
+
row.potential !== true ||
|
|
1789
|
+
!["host", "client"].includes(row.side) ||
|
|
1790
|
+
!Array.isArray(row.requirements) ||
|
|
1791
|
+
!validPath(
|
|
1792
|
+
row.path,
|
|
1793
|
+
impact.plugin,
|
|
1794
|
+
row.plugin,
|
|
1795
|
+
row.side,
|
|
1796
|
+
row.requirements,
|
|
1797
|
+
)
|
|
1798
|
+
)
|
|
1799
|
+
return false;
|
|
1800
|
+
const key = graphId(row.plugin, row.side);
|
|
1801
|
+
if (affected.has(key)) return false;
|
|
1802
|
+
affected.add(key);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
for (const cycle of graph.cycles) {
|
|
1806
|
+
if (
|
|
1807
|
+
!graphRecord(cycle) ||
|
|
1808
|
+
cycle.potential !== true ||
|
|
1809
|
+
!["host", "client"].includes(cycle.side) ||
|
|
1810
|
+
!Array.isArray(cycle.plugins) ||
|
|
1811
|
+
cycle.plugins.length < 2 ||
|
|
1812
|
+
cycle.plugins[0] !== cycle.plugins.at(-1) ||
|
|
1813
|
+
!cycle.plugins.every((id) => nodes.get(id)?.kind === "plugin") ||
|
|
1814
|
+
!validPath(
|
|
1815
|
+
cycle.path,
|
|
1816
|
+
cycle.plugins[0],
|
|
1817
|
+
cycle.plugins[0],
|
|
1818
|
+
cycle.side,
|
|
1819
|
+
null,
|
|
1820
|
+
cycle.plugins,
|
|
1821
|
+
)
|
|
1822
|
+
)
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return { buildDependencyGraph:buildDependencyGraph, validateDependencyGraph:validateDependencyGraph };
|
|
1829
|
+
})();
|
|
1830
|
+
// END GENERATED DEPENDENCY GRAPH
|
|
1831
|
+
|
|
1832
|
+
// BEGIN GENERATED FINDING REVIEW
|
|
1833
|
+
var FindingReviewContract = (function () {
|
|
1834
|
+
/** Browser-safe manual-review contract. Reviews never modify canonical findings. */
|
|
1835
|
+
const REVIEW_STATUS_LABELS = Object.freeze({
|
|
1836
|
+
pending: "待确认",
|
|
1837
|
+
reviewed: "已复核",
|
|
1838
|
+
"false-positive": "误报",
|
|
1839
|
+
fixed: "已修复",
|
|
1840
|
+
"needs-review": "待重审",
|
|
1841
|
+
});
|
|
1842
|
+
const REVIEW_LIMITS = Object.freeze({
|
|
1843
|
+
fileBytes: 1024 * 1024,
|
|
1844
|
+
records: 1000,
|
|
1845
|
+
note: 4000,
|
|
1846
|
+
findings: 5000,
|
|
1847
|
+
identityBytes: 128 * 1024,
|
|
1848
|
+
});
|
|
1849
|
+
const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
1850
|
+
const hash = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
1851
|
+
const safe = (x) =>
|
|
1852
|
+
typeof x === "string" &&
|
|
1853
|
+
!/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(
|
|
1854
|
+
x,
|
|
1855
|
+
);
|
|
1856
|
+
function fail(message) {
|
|
1857
|
+
throw Object.assign(new Error(message), { code: "REVIEW_INVALID" });
|
|
1858
|
+
}
|
|
1859
|
+
function stableIdentity(value) {
|
|
1860
|
+
const seen = new Set();
|
|
1861
|
+
let nodes = 0;
|
|
1862
|
+
function visit(x, depth = 0) {
|
|
1863
|
+
if (++nodes > 50000 || depth > 30) fail("Review identity exceeds limits");
|
|
1864
|
+
if (x === null || typeof x === "string" || typeof x === "boolean") return x;
|
|
1865
|
+
if (typeof x === "number" && Number.isFinite(x)) return x;
|
|
1866
|
+
if (typeof x !== "object" || seen.has(x)) fail("Invalid review identity");
|
|
1867
|
+
seen.add(x);
|
|
1868
|
+
const out = Array.isArray(x)
|
|
1869
|
+
? x.map((v) => visit(v, depth + 1))
|
|
1870
|
+
: Object.fromEntries(
|
|
1871
|
+
Object.keys(x)
|
|
1872
|
+
.sort()
|
|
1873
|
+
.filter((k) => x[k] !== undefined)
|
|
1874
|
+
.map((k) => [k, visit(x[k], depth + 1)]),
|
|
1875
|
+
);
|
|
1876
|
+
seen.delete(x);
|
|
1877
|
+
return out;
|
|
1878
|
+
}
|
|
1879
|
+
const result = JSON.stringify(visit(value));
|
|
1880
|
+
if (result.length > REVIEW_LIMITS.identityBytes)
|
|
1881
|
+
fail("Review identity exceeds limits");
|
|
1882
|
+
return result;
|
|
1883
|
+
}
|
|
1884
|
+
function validateReviewInput(input) {
|
|
1885
|
+
if (
|
|
1886
|
+
!object(input) ||
|
|
1887
|
+
Object.keys(input).sort().join(",") !== "contextToken,findingId,note,status"
|
|
1888
|
+
)
|
|
1889
|
+
fail("Invalid review fields");
|
|
1890
|
+
if (!hash(input.contextToken) || !hash(input.findingId))
|
|
1891
|
+
fail("Invalid review context or finding ID");
|
|
1892
|
+
if (
|
|
1893
|
+
!Object.hasOwn(REVIEW_STATUS_LABELS, input.status) ||
|
|
1894
|
+
input.status === "needs-review"
|
|
1895
|
+
)
|
|
1896
|
+
fail("Invalid manual review status");
|
|
1897
|
+
if (!safe(input.note) || input.note.length > REVIEW_LIMITS.note)
|
|
1898
|
+
fail("Invalid review note");
|
|
1899
|
+
if (input.status !== "pending" && !input.note.trim())
|
|
1900
|
+
fail("Review note is required");
|
|
1901
|
+
return {
|
|
1902
|
+
contextToken: input.contextToken,
|
|
1903
|
+
findingId: input.findingId,
|
|
1904
|
+
status: input.status,
|
|
1905
|
+
note: input.note.trim(),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function validateReviewDocument(doc) {
|
|
1909
|
+
if (
|
|
1910
|
+
!object(doc) ||
|
|
1911
|
+
Object.keys(doc).sort().join(",") !== "records,revision,schemaVersion" ||
|
|
1912
|
+
doc.schemaVersion !== 1 ||
|
|
1913
|
+
!hash(doc.revision) ||
|
|
1914
|
+
!Array.isArray(doc.records) ||
|
|
1915
|
+
doc.records.length > REVIEW_LIMITS.records
|
|
1916
|
+
)
|
|
1917
|
+
fail("Invalid review storage");
|
|
1918
|
+
const seen = new Set();
|
|
1919
|
+
for (const record of doc.records) {
|
|
1920
|
+
if (
|
|
1921
|
+
!object(record) ||
|
|
1922
|
+
Object.keys(record).sort().join(",") !==
|
|
1923
|
+
"findingId,identity,lineage,note,pluginName,pluginVersion,status,updatedAt"
|
|
1924
|
+
)
|
|
1925
|
+
fail("Invalid review record");
|
|
1926
|
+
validateReviewInput({
|
|
1927
|
+
contextToken: doc.revision,
|
|
1928
|
+
findingId: record.findingId,
|
|
1929
|
+
status: record.status,
|
|
1930
|
+
note: record.note,
|
|
1931
|
+
});
|
|
1932
|
+
if (
|
|
1933
|
+
!safe(record.pluginName) ||
|
|
1934
|
+
!record.pluginName ||
|
|
1935
|
+
record.pluginName.length > 500 ||
|
|
1936
|
+
!safe(record.pluginVersion) ||
|
|
1937
|
+
!record.pluginVersion ||
|
|
1938
|
+
record.pluginVersion.length > 200 ||
|
|
1939
|
+
typeof record.identity !== "string" ||
|
|
1940
|
+
record.identity.length > REVIEW_LIMITS.identityBytes ||
|
|
1941
|
+
typeof record.lineage !== "string" ||
|
|
1942
|
+
record.lineage.length > 2000 ||
|
|
1943
|
+
!Number.isFinite(Date.parse(record.updatedAt)) ||
|
|
1944
|
+
seen.has(record.findingId)
|
|
1945
|
+
)
|
|
1946
|
+
fail("Invalid review record identity");
|
|
1947
|
+
const binding = JSON.parse(record.identity);
|
|
1948
|
+
if (
|
|
1949
|
+
!object(binding) ||
|
|
1950
|
+
!object(binding.scope) ||
|
|
1951
|
+
!object(binding.finding) ||
|
|
1952
|
+
binding.pluginName !== record.pluginName ||
|
|
1953
|
+
binding.pluginVersion !== record.pluginVersion ||
|
|
1954
|
+
!safe(binding.finding.category) ||
|
|
1955
|
+
!binding.finding.category ||
|
|
1956
|
+
[
|
|
1957
|
+
"profileId",
|
|
1958
|
+
"originalPath",
|
|
1959
|
+
"mode",
|
|
1960
|
+
"toolVersion",
|
|
1961
|
+
"dshVersion",
|
|
1962
|
+
"rulesIdentity",
|
|
1963
|
+
"catalogIdentity",
|
|
1964
|
+
"sourceIdentity",
|
|
1965
|
+
].some((key) => !safe(binding.scope[key]) || !binding.scope[key]) ||
|
|
1966
|
+
stableIdentity(binding) !== record.identity ||
|
|
1967
|
+
stableIdentity([
|
|
1968
|
+
record.pluginName,
|
|
1969
|
+
binding.finding.category,
|
|
1970
|
+
binding.finding.service ?? null,
|
|
1971
|
+
binding.finding.method ?? null,
|
|
1972
|
+
]) !== record.lineage
|
|
1973
|
+
)
|
|
1974
|
+
fail("Invalid review binding");
|
|
1975
|
+
seen.add(record.findingId);
|
|
1976
|
+
}
|
|
1977
|
+
return doc;
|
|
1978
|
+
}
|
|
1979
|
+
/** Validate an optional API sidecar without trusting it to be a renderable review. */
|
|
1980
|
+
function validateReviewEnvelope(value, report) {
|
|
1981
|
+
try {
|
|
1982
|
+
const keys = (x, want) =>
|
|
1983
|
+
object(x) && Object.keys(x).sort().join(",") === want;
|
|
1984
|
+
const text = (x, max) => safe(x) && x.length > 0 && x.length <= max;
|
|
1985
|
+
const note = (x) => safe(x) && x.length <= REVIEW_LIMITS.note;
|
|
1986
|
+
const manual = (x) =>
|
|
1987
|
+
Object.hasOwn(REVIEW_STATUS_LABELS, x) && x !== "needs-review";
|
|
1988
|
+
const record = (r, summary = false) =>
|
|
1989
|
+
keys(
|
|
1990
|
+
r,
|
|
1991
|
+
summary
|
|
1992
|
+
? "findingId,note,noteTruncated,pluginName,pluginVersion,status,updatedAt"
|
|
1993
|
+
: "findingId,note,pluginName,pluginVersion,status,updatedAt",
|
|
1994
|
+
) &&
|
|
1995
|
+
hash(r.findingId) &&
|
|
1996
|
+
text(r.pluginName, 500) &&
|
|
1997
|
+
text(r.pluginVersion, 200) &&
|
|
1998
|
+
manual(r.status) &&
|
|
1999
|
+
note(r.note) &&
|
|
2000
|
+
(r.status === "pending" ||
|
|
2001
|
+
r.note.trim().length > 0 ||
|
|
2002
|
+
(summary && r.noteTruncated)) &&
|
|
2003
|
+
text(r.updatedAt, 40) &&
|
|
2004
|
+
Number.isFinite(Date.parse(r.updatedAt)) &&
|
|
2005
|
+
(!summary ||
|
|
2006
|
+
(typeof r.noteTruncated === "boolean" && r.note.length <= 200));
|
|
2007
|
+
if (
|
|
2008
|
+
!keys(value, "contextToken,error,history,items,revision,schemaVersion") ||
|
|
2009
|
+
value.schemaVersion !== 1 ||
|
|
2010
|
+
!Array.isArray(value.items) ||
|
|
2011
|
+
value.items.length > REVIEW_LIMITS.findings ||
|
|
2012
|
+
!Array.isArray(value.history) ||
|
|
2013
|
+
value.history.length > REVIEW_LIMITS.records ||
|
|
2014
|
+
!(value.error === null || text(value.error, 4000))
|
|
2015
|
+
)
|
|
2016
|
+
return false;
|
|
2017
|
+
if (value.error && value.contextToken === null && value.revision === null)
|
|
2018
|
+
return value.items.length === 0 && value.history.length === 0;
|
|
2019
|
+
if (!hash(value.contextToken) || !hash(value.revision)) return false;
|
|
2020
|
+
const seenSlots = new Set(),
|
|
2021
|
+
seenHistory = new Set();
|
|
2022
|
+
for (const item of value.items) {
|
|
2023
|
+
if (
|
|
2024
|
+
!keys(
|
|
2025
|
+
item,
|
|
2026
|
+
"findingId,findingIndex,history,historyCount,note,pluginName,pluginVersion,record,reviewReason,status",
|
|
2027
|
+
) ||
|
|
2028
|
+
!hash(item.findingId) ||
|
|
2029
|
+
!text(item.pluginName, 500) ||
|
|
2030
|
+
!text(item.pluginVersion, 200) ||
|
|
2031
|
+
!Number.isSafeInteger(item.findingIndex) ||
|
|
2032
|
+
item.findingIndex < 0 ||
|
|
2033
|
+
!Object.hasOwn(REVIEW_STATUS_LABELS, item.status) ||
|
|
2034
|
+
!note(item.note) ||
|
|
2035
|
+
!Array.isArray(item.history) ||
|
|
2036
|
+
item.history.length > 3 ||
|
|
2037
|
+
!Number.isSafeInteger(item.historyCount) ||
|
|
2038
|
+
item.historyCount < item.history.length ||
|
|
2039
|
+
item.historyCount > REVIEW_LIMITS.records ||
|
|
2040
|
+
!(item.reviewReason === null || text(item.reviewReason, 4000))
|
|
2041
|
+
)
|
|
2042
|
+
return false;
|
|
2043
|
+
const slot = JSON.stringify([item.pluginName, item.findingIndex]);
|
|
2044
|
+
if (seenSlots.has(slot)) return false;
|
|
2045
|
+
seenSlots.add(slot);
|
|
2046
|
+
if (report) {
|
|
2047
|
+
const plugin = report.plugins?.[item.pluginName];
|
|
2048
|
+
if (
|
|
2049
|
+
!plugin ||
|
|
2050
|
+
plugin.version !== item.pluginVersion ||
|
|
2051
|
+
!Array.isArray(plugin.findings) ||
|
|
2052
|
+
item.findingIndex >= plugin.findings.length
|
|
2053
|
+
)
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
if (item.record !== null) {
|
|
2057
|
+
if (
|
|
2058
|
+
!record(item.record) ||
|
|
2059
|
+
item.record.findingId !== item.findingId ||
|
|
2060
|
+
item.record.pluginName !== item.pluginName ||
|
|
2061
|
+
item.record.pluginVersion !== item.pluginVersion ||
|
|
2062
|
+
item.record.status !== item.status ||
|
|
2063
|
+
item.record.note !== item.note
|
|
2064
|
+
)
|
|
2065
|
+
return false;
|
|
2066
|
+
} else if (
|
|
2067
|
+
item.note !== "" ||
|
|
2068
|
+
!["pending", "needs-review"].includes(item.status)
|
|
2069
|
+
)
|
|
2070
|
+
return false;
|
|
2071
|
+
if (
|
|
2072
|
+
item.status === "needs-review" &&
|
|
2073
|
+
(!item.historyCount || !item.reviewReason)
|
|
2074
|
+
)
|
|
2075
|
+
return false;
|
|
2076
|
+
if (item.historyCount > 0 && !item.reviewReason) return false;
|
|
2077
|
+
const local = new Set();
|
|
2078
|
+
for (const previous of item.history) {
|
|
2079
|
+
if (
|
|
2080
|
+
!record(previous, true) ||
|
|
2081
|
+
previous.findingId === item.findingId ||
|
|
2082
|
+
previous.pluginName !== item.pluginName ||
|
|
2083
|
+
local.has(previous.findingId)
|
|
2084
|
+
)
|
|
2085
|
+
return false;
|
|
2086
|
+
local.add(previous.findingId);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
for (const previous of value.history) {
|
|
2090
|
+
if (!record(previous) || seenHistory.has(previous.findingId))
|
|
2091
|
+
return false;
|
|
2092
|
+
seenHistory.add(previous.findingId);
|
|
2093
|
+
}
|
|
2094
|
+
if (report) {
|
|
2095
|
+
let count = 0;
|
|
2096
|
+
for (const plugin of Object.values(report.plugins || {})) {
|
|
2097
|
+
if (!Array.isArray(plugin.findings)) return false;
|
|
2098
|
+
count += plugin.findings.length;
|
|
2099
|
+
}
|
|
2100
|
+
if (count !== value.items.length) return false;
|
|
2101
|
+
}
|
|
2102
|
+
return true;
|
|
2103
|
+
} catch {
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/** Exact identity includes evidence; lineage only offers old records as review history. */
|
|
2108
|
+
function combineReviews(
|
|
2109
|
+
context,
|
|
2110
|
+
records,
|
|
2111
|
+
{ revision = "0".repeat(64), error = null } = {},
|
|
2112
|
+
) {
|
|
2113
|
+
const byId = new Map(),
|
|
2114
|
+
byLineage = new Map(),
|
|
2115
|
+
currentIds = new Set(context.findings.map((f) => f.findingId));
|
|
2116
|
+
const publicRecord = (r) => ({
|
|
2117
|
+
findingId: r.findingId,
|
|
2118
|
+
pluginName: r.pluginName,
|
|
2119
|
+
pluginVersion: r.pluginVersion,
|
|
2120
|
+
status: r.status,
|
|
2121
|
+
note: r.note,
|
|
2122
|
+
updatedAt: r.updatedAt,
|
|
2123
|
+
});
|
|
2124
|
+
for (const record of records) {
|
|
2125
|
+
byId.set(record.findingId, record);
|
|
2126
|
+
if (!byLineage.has(record.lineage)) byLineage.set(record.lineage, []);
|
|
2127
|
+
byLineage.get(record.lineage).push(record);
|
|
2128
|
+
}
|
|
2129
|
+
const items = context.findings.map((entry) => {
|
|
2130
|
+
const exact = byId.get(entry.findingId);
|
|
2131
|
+
const record =
|
|
2132
|
+
exact?.identity === entry.identity ? publicRecord(exact) : null;
|
|
2133
|
+
const previous = byLineage.get(entry.lineage) || [];
|
|
2134
|
+
const historyCount = previous.length - (exact ? 1 : 0);
|
|
2135
|
+
// Keep row summaries bounded; the independent history retains each full note once.
|
|
2136
|
+
const history = [];
|
|
2137
|
+
for (let i = previous.length - 1; i >= 0 && history.length < 3; i--) {
|
|
2138
|
+
if (previous[i].findingId === entry.findingId) continue;
|
|
2139
|
+
const old = publicRecord(previous[i]);
|
|
2140
|
+
history.push({
|
|
2141
|
+
...old,
|
|
2142
|
+
note: old.note.slice(0, 200),
|
|
2143
|
+
noteTruncated: old.note.length > 200,
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
findingId: entry.findingId,
|
|
2148
|
+
pluginName: entry.pluginName,
|
|
2149
|
+
pluginVersion: entry.pluginVersion,
|
|
2150
|
+
findingIndex: entry.findingIndex,
|
|
2151
|
+
status: record?.status || (historyCount ? "needs-review" : "pending"),
|
|
2152
|
+
note: record?.note || "",
|
|
2153
|
+
record,
|
|
2154
|
+
history,
|
|
2155
|
+
historyCount,
|
|
2156
|
+
reviewReason: historyCount
|
|
2157
|
+
? "插件源码或版本、规则、目录、工具、观察目标或发现证据已变化;旧处置仅供参考,需要重新核实。"
|
|
2158
|
+
: null,
|
|
2159
|
+
};
|
|
2160
|
+
});
|
|
2161
|
+
return {
|
|
2162
|
+
schemaVersion: 1,
|
|
2163
|
+
contextToken: context.contextToken,
|
|
2164
|
+
revision,
|
|
2165
|
+
error,
|
|
2166
|
+
items,
|
|
2167
|
+
history: records
|
|
2168
|
+
.filter((r) => !currentIds.has(r.findingId))
|
|
2169
|
+
.map(publicRecord),
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
return { REVIEW_STATUS_LABELS:REVIEW_STATUS_LABELS, REVIEW_LIMITS:REVIEW_LIMITS, validateReviewInput:validateReviewInput, validateReviewEnvelope:validateReviewEnvelope };
|
|
2174
|
+
})();
|
|
2175
|
+
// END GENERATED FINDING REVIEW
|
|
2176
|
+
|
|
2177
|
+
// BEGIN GENERATED FUNCTIONAL HEALTH CONTRACT
|
|
2178
|
+
var FunctionalHealthContract = (function () {
|
|
2179
|
+
/** Pure declarations/results contract. Author declarations never imply execution or compatibility. */
|
|
2180
|
+
const FUNCTIONAL_HEALTH_KIND = "plugin-compat-check/functional-health";
|
|
2181
|
+
const FUNCTIONAL_HEALTH_LIMITS = Object.freeze({
|
|
2182
|
+
checks: 32,
|
|
2183
|
+
path: 256,
|
|
2184
|
+
selector: 160,
|
|
2185
|
+
responseBytes: 65536,
|
|
2186
|
+
requestMs: 3000,
|
|
2187
|
+
totalMs: 180000,
|
|
2188
|
+
});
|
|
2189
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
2190
|
+
const keys = (v, allowed) =>
|
|
2191
|
+
object(v) && Object.keys(v).every((k) => allowed.includes(k));
|
|
2192
|
+
const exactVersion = (v) =>
|
|
2193
|
+
typeof v === "string" &&
|
|
2194
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(
|
|
2195
|
+
v,
|
|
2196
|
+
);
|
|
2197
|
+
const digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
2198
|
+
const bounded = (v, n) =>
|
|
2199
|
+
typeof v === "string" && v.length > 0 && v.length <= n;
|
|
2200
|
+
const artifactValid = (a) =>
|
|
2201
|
+
keys(a, ["name", "version", "sha256"]) &&
|
|
2202
|
+
typeof a.name === "string" &&
|
|
2203
|
+
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(a.name) &&
|
|
2204
|
+
a.name.length <= 214 &&
|
|
2205
|
+
exactVersion(a.version) &&
|
|
2206
|
+
digest(a.sha256);
|
|
2207
|
+
function validFunctionalPath(v) {
|
|
2208
|
+
return (
|
|
2209
|
+
bounded(v, FUNCTIONAL_HEALTH_LIMITS.path) &&
|
|
2210
|
+
/^\/(?!\/)[A-Za-z0-9/_.,~-]*$/.test(v) &&
|
|
2211
|
+
!v.split("/").some((p) => p === ".." || p === ".")
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
function validateFunctionalManifest(value) {
|
|
2215
|
+
const errors = [];
|
|
2216
|
+
if (
|
|
2217
|
+
!keys(value, ["schemaVersion", "artifact", "dshVersion", "checks"]) ||
|
|
2218
|
+
value.schemaVersion !== 1 ||
|
|
2219
|
+
!artifactValid(value.artifact) ||
|
|
2220
|
+
!exactVersion(value.dshVersion)
|
|
2221
|
+
)
|
|
2222
|
+
errors.push("Invalid manifest schema or exact artifact/DSH identity");
|
|
2223
|
+
if (
|
|
2224
|
+
!Array.isArray(value?.checks) ||
|
|
2225
|
+
value.checks.length < 1 ||
|
|
2226
|
+
value.checks.length > FUNCTIONAL_HEALTH_LIMITS.checks
|
|
2227
|
+
)
|
|
2228
|
+
errors.push("Expected 1..32 bounded checks");
|
|
2229
|
+
else {
|
|
2230
|
+
const seen = new Set();
|
|
2231
|
+
for (const c of value.checks) {
|
|
2232
|
+
if (
|
|
2233
|
+
!object(c) ||
|
|
2234
|
+
!bounded(c.id, 64) ||
|
|
2235
|
+
!/^[A-Za-z0-9_-]+$/.test(c.id) ||
|
|
2236
|
+
seen.has(c.id) ||
|
|
2237
|
+
!validFunctionalPath(c.path)
|
|
2238
|
+
) {
|
|
2239
|
+
errors.push("Invalid check id/path or duplicate id");
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
seen.add(c.id);
|
|
2243
|
+
if (c.type === "http") {
|
|
2244
|
+
if (
|
|
2245
|
+
!keys(c, ["id", "type", "path", "status", "json"]) ||
|
|
2246
|
+
!Number.isInteger(c.status) ||
|
|
2247
|
+
c.status < 200 ||
|
|
2248
|
+
c.status > 599 ||
|
|
2249
|
+
(c.status >= 300 && c.status < 400)
|
|
2250
|
+
)
|
|
2251
|
+
errors.push("Invalid HTTP check");
|
|
2252
|
+
if (
|
|
2253
|
+
c.json !== undefined &&
|
|
2254
|
+
(!keys(c.json, ["pointer", "equals"]) ||
|
|
2255
|
+
typeof c.json.pointer !== "string" ||
|
|
2256
|
+
c.json.pointer.length > 256 ||
|
|
2257
|
+
!/^(?:\/(?:[^~\u0000-\u001f]|~[01])*)*$/.test(c.json.pointer) ||
|
|
2258
|
+
!Object.hasOwn(c.json, "equals") ||
|
|
2259
|
+
![null, "string", "boolean", "number"].includes(
|
|
2260
|
+
c.json.equals === null ? null : typeof c.json.equals,
|
|
2261
|
+
) ||
|
|
2262
|
+
(typeof c.json.equals === "string" && c.json.equals.length > 256) ||
|
|
2263
|
+
(typeof c.json.equals === "number" &&
|
|
2264
|
+
!Number.isFinite(c.json.equals)))
|
|
2265
|
+
)
|
|
2266
|
+
errors.push(
|
|
2267
|
+
"JSON assertion must be a bounded JSON pointer and scalar value",
|
|
2268
|
+
);
|
|
2269
|
+
} else if (c.type === "dom") {
|
|
2270
|
+
if (
|
|
2271
|
+
!keys(c, [
|
|
2272
|
+
"id",
|
|
2273
|
+
"type",
|
|
2274
|
+
"path",
|
|
2275
|
+
"selector",
|
|
2276
|
+
"minCount",
|
|
2277
|
+
"maxCount",
|
|
2278
|
+
]) ||
|
|
2279
|
+
!bounded(c.selector, FUNCTIONAL_HEALTH_LIMITS.selector) ||
|
|
2280
|
+
!/^[A-Za-z0-9_#.*\[\]="'\- >]+$/.test(c.selector) ||
|
|
2281
|
+
!Number.isInteger(c.minCount) ||
|
|
2282
|
+
!Number.isInteger(c.maxCount) ||
|
|
2283
|
+
c.minCount < 0 ||
|
|
2284
|
+
c.maxCount < c.minCount ||
|
|
2285
|
+
c.maxCount > 1000
|
|
2286
|
+
)
|
|
2287
|
+
errors.push("Invalid bounded DOM selector/count check");
|
|
2288
|
+
} else errors.push("Only HTTP GET and DOM selector counts are supported");
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return errors.length ? { valid: false, errors } : { valid: true, value };
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/** Imported evidence is an offline assertion, not a signature or compatibility verdict. */
|
|
2295
|
+
function validateFunctionalHealthResult(value, expectedIdentity = {}) {
|
|
2296
|
+
const errors = [];
|
|
2297
|
+
const iso = (v) =>
|
|
2298
|
+
typeof v === "string" &&
|
|
2299
|
+
Number.isFinite(Date.parse(v)) &&
|
|
2300
|
+
new Date(v).toISOString() === v;
|
|
2301
|
+
if (
|
|
2302
|
+
!keys(value, [
|
|
2303
|
+
"schemaVersion",
|
|
2304
|
+
"kind",
|
|
2305
|
+
"status",
|
|
2306
|
+
"identity",
|
|
2307
|
+
"startedAt",
|
|
2308
|
+
"completedAt",
|
|
2309
|
+
"checks",
|
|
2310
|
+
"cleanup",
|
|
2311
|
+
"limitations",
|
|
2312
|
+
"error",
|
|
2313
|
+
"declaration",
|
|
2314
|
+
]) ||
|
|
2315
|
+
value.schemaVersion !== 1 ||
|
|
2316
|
+
value.kind !== FUNCTIONAL_HEALTH_KIND ||
|
|
2317
|
+
!["tested-pass", "tested-failed", "error"].includes(value.status)
|
|
2318
|
+
)
|
|
2319
|
+
errors.push("Invalid functional health result schema/status");
|
|
2320
|
+
const declarationValidation = validateFunctionalManifest(value?.declaration);
|
|
2321
|
+
if (!declarationValidation.valid)
|
|
2322
|
+
errors.push("Missing or invalid executed declaration");
|
|
2323
|
+
const id = value?.identity;
|
|
2324
|
+
if (
|
|
2325
|
+
declarationValidation.valid &&
|
|
2326
|
+
(value.declaration.dshVersion !== id?.dshVersion ||
|
|
2327
|
+
["name", "version", "sha256"].some(
|
|
2328
|
+
(k) => value.declaration.artifact[k] !== id?.artifact?.[k],
|
|
2329
|
+
))
|
|
2330
|
+
)
|
|
2331
|
+
errors.push("Executed declaration identity differs from result");
|
|
2332
|
+
if (
|
|
2333
|
+
!keys(id, ["artifact", "dshVersion", "manifestSha256"]) ||
|
|
2334
|
+
!artifactValid(id.artifact) ||
|
|
2335
|
+
!exactVersion(id.dshVersion) ||
|
|
2336
|
+
!digest(id.manifestSha256)
|
|
2337
|
+
)
|
|
2338
|
+
errors.push("Invalid exact evidence identity");
|
|
2339
|
+
if (
|
|
2340
|
+
!iso(value?.startedAt) ||
|
|
2341
|
+
!iso(value?.completedAt) ||
|
|
2342
|
+
value.completedAt < value.startedAt ||
|
|
2343
|
+
Date.parse(value.completedAt) - Date.parse(value.startedAt) >
|
|
2344
|
+
FUNCTIONAL_HEALTH_LIMITS.totalMs + 30000
|
|
2345
|
+
)
|
|
2346
|
+
errors.push("Invalid bounded measurement times");
|
|
2347
|
+
if (
|
|
2348
|
+
!keys(value?.cleanup, ["stopped", "removed", "errors"]) ||
|
|
2349
|
+
typeof value.cleanup.stopped !== "boolean" ||
|
|
2350
|
+
typeof value.cleanup.removed !== "boolean" ||
|
|
2351
|
+
(value.cleanup.errors !== undefined &&
|
|
2352
|
+
(!Array.isArray(value.cleanup.errors) ||
|
|
2353
|
+
value.cleanup.errors.length > 8 ||
|
|
2354
|
+
value.cleanup.errors.some((x) => !bounded(x, 512))))
|
|
2355
|
+
)
|
|
2356
|
+
errors.push("Invalid cleanup evidence");
|
|
2357
|
+
if (
|
|
2358
|
+
!Array.isArray(value?.limitations) ||
|
|
2359
|
+
value.limitations.length < 1 ||
|
|
2360
|
+
value.limitations.length > 8 ||
|
|
2361
|
+
value.limitations.some((x) => !bounded(x, 512)) ||
|
|
2362
|
+
(value.error !== undefined && !bounded(value.error, 1024))
|
|
2363
|
+
)
|
|
2364
|
+
errors.push("Invalid limitations/error summary");
|
|
2365
|
+
const seen = new Set();
|
|
2366
|
+
if (!Array.isArray(value?.checks) || value.checks.length > 32)
|
|
2367
|
+
errors.push("Invalid check evidence");
|
|
2368
|
+
else
|
|
2369
|
+
for (const c of value.checks) {
|
|
2370
|
+
const e = c?.evidence;
|
|
2371
|
+
if (
|
|
2372
|
+
!keys(c, ["id", "type", "status", "evidence"]) ||
|
|
2373
|
+
!bounded(c.id, 64) ||
|
|
2374
|
+
!/^[A-Za-z0-9_-]+$/.test(c.id) ||
|
|
2375
|
+
seen.has(c.id) ||
|
|
2376
|
+
!["http", "dom"].includes(c.type) ||
|
|
2377
|
+
!["pass", "failed", "error"].includes(c.status) ||
|
|
2378
|
+
!keys(e, [
|
|
2379
|
+
"httpStatus",
|
|
2380
|
+
"responseBytes",
|
|
2381
|
+
"jsonMatched",
|
|
2382
|
+
"count",
|
|
2383
|
+
"elapsedMs",
|
|
2384
|
+
"error",
|
|
2385
|
+
]) ||
|
|
2386
|
+
!Number.isFinite(e.elapsedMs) ||
|
|
2387
|
+
e.elapsedMs < 0 ||
|
|
2388
|
+
e.elapsedMs > FUNCTIONAL_HEALTH_LIMITS.totalMs
|
|
2389
|
+
) {
|
|
2390
|
+
errors.push("Invalid check row");
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
seen.add(c.id);
|
|
2394
|
+
if (c.status === "error") {
|
|
2395
|
+
if (!bounded(e.error, 256)) errors.push("Missing check error");
|
|
2396
|
+
} else if (c.type === "http") {
|
|
2397
|
+
if (
|
|
2398
|
+
!Number.isInteger(e.httpStatus) ||
|
|
2399
|
+
e.httpStatus < 200 ||
|
|
2400
|
+
e.httpStatus > 599 ||
|
|
2401
|
+
!Number.isInteger(e.responseBytes) ||
|
|
2402
|
+
e.responseBytes < 0 ||
|
|
2403
|
+
e.responseBytes > 65536 ||
|
|
2404
|
+
(e.jsonMatched !== undefined && typeof e.jsonMatched !== "boolean") ||
|
|
2405
|
+
e.count !== undefined ||
|
|
2406
|
+
e.error !== undefined
|
|
2407
|
+
)
|
|
2408
|
+
errors.push("Invalid HTTP evidence");
|
|
2409
|
+
} else if (
|
|
2410
|
+
!Number.isInteger(e.count) ||
|
|
2411
|
+
e.count < 0 ||
|
|
2412
|
+
e.httpStatus !== undefined ||
|
|
2413
|
+
e.responseBytes !== undefined ||
|
|
2414
|
+
e.jsonMatched !== undefined ||
|
|
2415
|
+
e.error !== undefined
|
|
2416
|
+
)
|
|
2417
|
+
errors.push("Invalid DOM evidence");
|
|
2418
|
+
}
|
|
2419
|
+
if (errors.length) return { valid: false, errors };
|
|
2420
|
+
if (declarationValidation.valid && Array.isArray(value?.checks)) {
|
|
2421
|
+
const expected = value.declaration.checks;
|
|
2422
|
+
if (
|
|
2423
|
+
(value.status !== "error" && value.checks.length !== expected.length) ||
|
|
2424
|
+
value.checks.length > expected.length
|
|
2425
|
+
)
|
|
2426
|
+
errors.push("Evidence must cover every declared check in order");
|
|
2427
|
+
for (let i = 0; i < value.checks.length; i++) {
|
|
2428
|
+
const row = value.checks[i],
|
|
2429
|
+
check = expected[i];
|
|
2430
|
+
if (!row || !check || row.id !== check.id || row.type !== check.type) {
|
|
2431
|
+
errors.push("Evidence check identity/order differs from declaration");
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (row.status === "error") continue;
|
|
2435
|
+
const e = row.evidence;
|
|
2436
|
+
if (!object(e)) continue;
|
|
2437
|
+
const passed =
|
|
2438
|
+
check.type === "http"
|
|
2439
|
+
? e.httpStatus === check.status &&
|
|
2440
|
+
(!check.json || e.jsonMatched === true)
|
|
2441
|
+
: e.count >= check.minCount && e.count <= check.maxCount;
|
|
2442
|
+
if (
|
|
2443
|
+
row.status !== (passed ? "pass" : "failed") ||
|
|
2444
|
+
(check.type === "http" &&
|
|
2445
|
+
Boolean(check.json) !== (e.jsonMatched !== undefined))
|
|
2446
|
+
)
|
|
2447
|
+
errors.push("Evidence outcome conflicts with executed expectation");
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (
|
|
2451
|
+
value?.status === "tested-pass" &&
|
|
2452
|
+
(!value.checks?.length ||
|
|
2453
|
+
value.checks.some((c) => c?.status !== "pass") ||
|
|
2454
|
+
value.cleanup?.stopped !== true ||
|
|
2455
|
+
value.cleanup?.removed !== true ||
|
|
2456
|
+
value.cleanup?.errors?.length ||
|
|
2457
|
+
value.error !== undefined)
|
|
2458
|
+
)
|
|
2459
|
+
errors.push("Pass conflicts with checks or cleanup");
|
|
2460
|
+
if (
|
|
2461
|
+
value?.status === "tested-failed" &&
|
|
2462
|
+
(!value.checks?.some((c) => c?.status === "failed") ||
|
|
2463
|
+
value.checks.some((c) => c?.status === "error") ||
|
|
2464
|
+
value.cleanup?.stopped !== true ||
|
|
2465
|
+
value.cleanup?.removed !== true ||
|
|
2466
|
+
value.error !== undefined)
|
|
2467
|
+
)
|
|
2468
|
+
errors.push("Failed measurement conflicts with checks/cleanup");
|
|
2469
|
+
if (value?.status === "error" && !bounded(value.error, 1024))
|
|
2470
|
+
errors.push("Missing run error");
|
|
2471
|
+
if (errors.length) return { valid: false, errors };
|
|
2472
|
+
const staleReasons = [];
|
|
2473
|
+
if (
|
|
2474
|
+
!expectedIdentity.artifact ||
|
|
2475
|
+
!["name", "version", "sha256"].every(
|
|
2476
|
+
(k) => typeof expectedIdentity.artifact[k] === "string",
|
|
2477
|
+
)
|
|
2478
|
+
)
|
|
2479
|
+
staleReasons.push("Current artifact identity unavailable");
|
|
2480
|
+
else
|
|
2481
|
+
for (const key of ["name", "version", "sha256"])
|
|
2482
|
+
if (id.artifact[key] !== expectedIdentity.artifact[key])
|
|
2483
|
+
staleReasons.push("Artifact " + key + " changed");
|
|
2484
|
+
if (!expectedIdentity.dshVersion)
|
|
2485
|
+
staleReasons.push("Current DSH version unavailable");
|
|
2486
|
+
else if (id.dshVersion !== expectedIdentity.dshVersion)
|
|
2487
|
+
staleReasons.push("DSH version changed");
|
|
2488
|
+
if (!expectedIdentity.manifestSha256)
|
|
2489
|
+
staleReasons.push("Current declaration identity unavailable");
|
|
2490
|
+
else if (id.manifestSha256 !== expectedIdentity.manifestSha256)
|
|
2491
|
+
staleReasons.push("Declaration changed");
|
|
2492
|
+
return {
|
|
2493
|
+
valid: true,
|
|
2494
|
+
value,
|
|
2495
|
+
current: staleReasons.length === 0,
|
|
2496
|
+
staleReasons,
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/** SHA256 identity rule: UTF-8 of JSON with recursively sorted object keys and preserved array order, no whitespace. */
|
|
2501
|
+
function canonicalFunctionalManifest(value) {
|
|
2502
|
+
const validation = validateFunctionalManifest(value);
|
|
2503
|
+
if (!validation.valid) throw Error(validation.errors.join("; "));
|
|
2504
|
+
const ordered = (v) =>
|
|
2505
|
+
Array.isArray(v)
|
|
2506
|
+
? v.map(ordered)
|
|
2507
|
+
: object(v)
|
|
2508
|
+
? Object.fromEntries(
|
|
2509
|
+
Object.keys(v)
|
|
2510
|
+
.sort()
|
|
2511
|
+
.map((k) => [k, ordered(v[k])]),
|
|
2512
|
+
)
|
|
2513
|
+
: v;
|
|
2514
|
+
return JSON.stringify(ordered(value));
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
return { validateFunctionalHealthResult:validateFunctionalHealthResult, canonicalFunctionalManifest:canonicalFunctionalManifest };
|
|
2518
|
+
})();
|
|
2519
|
+
// END GENERATED FUNCTIONAL HEALTH CONTRACT
|
|
2520
|
+
|
|
2521
|
+
// BEGIN GENERATED WORKFLOW PANELS
|
|
2522
|
+
var WorkflowPanelFactory = (function () {
|
|
2523
|
+
/** Stateless React factory; staged verbatim into the shipped client. */
|
|
2524
|
+
function createDependencyPanel(React, GraphContract = {}) {
|
|
2525
|
+
const h = React.createElement;
|
|
2526
|
+
const requirement = {
|
|
2527
|
+
required: "必需",
|
|
2528
|
+
optional: "可选 / 软读取",
|
|
2529
|
+
subcontext: "子作用域",
|
|
2530
|
+
unknown: "必需性未知",
|
|
2531
|
+
};
|
|
2532
|
+
const table = (caption, headers, rows) =>
|
|
2533
|
+
h(
|
|
2534
|
+
"div",
|
|
2535
|
+
{ style: { overflowX: "auto", marginBottom: 16 } },
|
|
2536
|
+
h(
|
|
2537
|
+
"table",
|
|
2538
|
+
{ style: { borderCollapse: "collapse", width: "100%", fontSize: 13 } },
|
|
2539
|
+
h(
|
|
2540
|
+
"caption",
|
|
2541
|
+
{ style: { textAlign: "left", fontWeight: 600, padding: "8px 0" } },
|
|
2542
|
+
caption,
|
|
2543
|
+
),
|
|
2544
|
+
h(
|
|
2545
|
+
"thead",
|
|
2546
|
+
null,
|
|
2547
|
+
h(
|
|
2548
|
+
"tr",
|
|
2549
|
+
null,
|
|
2550
|
+
...headers.map((label, i) =>
|
|
2551
|
+
h(
|
|
2552
|
+
"th",
|
|
2553
|
+
{
|
|
2554
|
+
key: i,
|
|
2555
|
+
scope: "col",
|
|
2556
|
+
style: {
|
|
2557
|
+
textAlign: "left",
|
|
2558
|
+
padding: 6,
|
|
2559
|
+
borderBottom: "1px solid #777",
|
|
2560
|
+
},
|
|
2561
|
+
},
|
|
2562
|
+
label,
|
|
2563
|
+
),
|
|
2564
|
+
),
|
|
2565
|
+
),
|
|
2566
|
+
),
|
|
2567
|
+
h(
|
|
2568
|
+
"tbody",
|
|
2569
|
+
null,
|
|
2570
|
+
...rows.map((row, i) =>
|
|
2571
|
+
h(
|
|
2572
|
+
"tr",
|
|
2573
|
+
{ key: i },
|
|
2574
|
+
...row.map((cell, j) =>
|
|
2575
|
+
h(
|
|
2576
|
+
"td",
|
|
2577
|
+
{
|
|
2578
|
+
key: j,
|
|
2579
|
+
style: {
|
|
2580
|
+
verticalAlign: "top",
|
|
2581
|
+
padding: 6,
|
|
2582
|
+
borderBottom: "1px solid #9995",
|
|
2583
|
+
overflowWrap: "anywhere",
|
|
2584
|
+
},
|
|
2585
|
+
},
|
|
2586
|
+
cell,
|
|
2587
|
+
),
|
|
2588
|
+
),
|
|
2589
|
+
),
|
|
2590
|
+
),
|
|
2591
|
+
),
|
|
2592
|
+
),
|
|
2593
|
+
);
|
|
2594
|
+
return function DependencyPanel({ graph } = {}) {
|
|
2595
|
+
if (graph === undefined || graph === null)
|
|
2596
|
+
return h(
|
|
2597
|
+
"section",
|
|
2598
|
+
{ "aria-label": "依赖关系与潜在影响" },
|
|
2599
|
+
h("h3", null, "依赖关系与潜在影响"),
|
|
2600
|
+
h("p", null, "本报告未提供依赖关系视图;请重新体检。"),
|
|
2601
|
+
);
|
|
2602
|
+
let valid = false;
|
|
2603
|
+
try {
|
|
2604
|
+
valid =
|
|
2605
|
+
typeof GraphContract.validateDependencyGraph === "function" &&
|
|
2606
|
+
GraphContract.validateDependencyGraph(graph);
|
|
2607
|
+
} catch {}
|
|
2608
|
+
if (!valid)
|
|
2609
|
+
return h(
|
|
2610
|
+
"section",
|
|
2611
|
+
{ "aria-label": "依赖关系与潜在影响" },
|
|
2612
|
+
h("h3", null, "依赖关系与潜在影响"),
|
|
2613
|
+
h(
|
|
2614
|
+
"p",
|
|
2615
|
+
{ role: "status" },
|
|
2616
|
+
"依赖关系视图不可用:扩展数据格式或引用无效,请重新体检。兼容结论仍按原始证据显示。",
|
|
2617
|
+
),
|
|
2618
|
+
);
|
|
2619
|
+
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2620
|
+
const name = (id) => nodes.get(id)?.name || id;
|
|
2621
|
+
const entries = (node) =>
|
|
2622
|
+
node.entries.length
|
|
2623
|
+
? node.entries
|
|
2624
|
+
.map(
|
|
2625
|
+
(row) =>
|
|
2626
|
+
`${row.id || "未命名条目"}: ${row.disabled ? "disabled / 已禁用" : row.state}`,
|
|
2627
|
+
)
|
|
2628
|
+
.join(";")
|
|
2629
|
+
: "unknown / 未提供 loader 条目";
|
|
2630
|
+
const candidates = new Map(),
|
|
2631
|
+
consumers = new Map();
|
|
2632
|
+
for (const edge of graph.edges) {
|
|
2633
|
+
const map =
|
|
2634
|
+
edge.kind === "candidate-provider"
|
|
2635
|
+
? candidates
|
|
2636
|
+
: edge.kind === "dependency"
|
|
2637
|
+
? consumers
|
|
2638
|
+
: null;
|
|
2639
|
+
if (map) {
|
|
2640
|
+
if (!map.has(edge.to)) map.set(edge.to, []);
|
|
2641
|
+
map.get(edge.to).push(edge);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
const candidateLabel = (edge) =>
|
|
2645
|
+
`${name(edge.from)} [${edge.side}; ${entries(nodes.get(edge.from))}]${edge.side === "unknown" ? "(端未知;不参与传播推导)" : ""}`;
|
|
2646
|
+
const source = (edge) =>
|
|
2647
|
+
edge.source?.file
|
|
2648
|
+
? `${edge.source.file}${edge.source.line === null ? "" : ":" + edge.source.line}`
|
|
2649
|
+
: "来源位置未知";
|
|
2650
|
+
const relationshipRows = graph.edges
|
|
2651
|
+
.filter((edge) => edge.kind === "dependency")
|
|
2652
|
+
.map((edge) => {
|
|
2653
|
+
const known = candidates.get(edge.to) || [];
|
|
2654
|
+
const unknown =
|
|
2655
|
+
candidates.get(
|
|
2656
|
+
JSON.stringify(["service", "unknown", edge.service]),
|
|
2657
|
+
) || [];
|
|
2658
|
+
const choices = [...known, ...(edge.side === "unknown" ? [] : unknown)];
|
|
2659
|
+
return [
|
|
2660
|
+
name(edge.from),
|
|
2661
|
+
edge.service,
|
|
2662
|
+
edge.side,
|
|
2663
|
+
requirement[edge.requirement],
|
|
2664
|
+
choices.length
|
|
2665
|
+
? choices.map(candidateLabel).join(";")
|
|
2666
|
+
: "未找到可定位的候选提供者(不等于宿主缺失)",
|
|
2667
|
+
source(edge),
|
|
2668
|
+
];
|
|
2669
|
+
});
|
|
2670
|
+
const providerRows = graph.edges
|
|
2671
|
+
.filter((edge) => edge.kind === "candidate-provider")
|
|
2672
|
+
.map((edge) => [
|
|
2673
|
+
candidateLabel(edge),
|
|
2674
|
+
edge.service,
|
|
2675
|
+
edge.side,
|
|
2676
|
+
(consumers.get(edge.to) || [])
|
|
2677
|
+
.map((c) => `${name(c.from)}(${requirement[c.requirement]})`)
|
|
2678
|
+
.join(";") || "没有同端消费者证据",
|
|
2679
|
+
source(edge),
|
|
2680
|
+
]);
|
|
2681
|
+
const packageRows = graph.edges
|
|
2682
|
+
.filter((edge) => edge.kind === "client-package-inject")
|
|
2683
|
+
.map((edge) => [
|
|
2684
|
+
name(edge.from),
|
|
2685
|
+
name(edge.to),
|
|
2686
|
+
"client 包级 inject;不作为 host 服务关系或传播依据",
|
|
2687
|
+
]);
|
|
2688
|
+
const edgeMap = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
2689
|
+
const pathLabel = (origin, path) => {
|
|
2690
|
+
const parts = [name(origin)];
|
|
2691
|
+
for (let i = 0; i < path.length; i += 2) {
|
|
2692
|
+
const provider = edgeMap.get(path[i]),
|
|
2693
|
+
consumer = edgeMap.get(path[i + 1]);
|
|
2694
|
+
parts.push(
|
|
2695
|
+
`${provider.service} [${provider.side}]`,
|
|
2696
|
+
`${name(consumer.from)}(${requirement[consumer.requirement]})`,
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
return parts.join(" → ");
|
|
2700
|
+
};
|
|
2701
|
+
const impactRows = graph.impacts.flatMap((impact) =>
|
|
2702
|
+
impact.affected.map((row) => [
|
|
2703
|
+
name(impact.plugin),
|
|
2704
|
+
name(row.plugin),
|
|
2705
|
+
row.side,
|
|
2706
|
+
"潜在影响;非实测故障",
|
|
2707
|
+
pathLabel(impact.plugin, row.path),
|
|
2708
|
+
]),
|
|
2709
|
+
);
|
|
2710
|
+
const cycleRows = graph.cycles.map((cycle) => [
|
|
2711
|
+
cycle.side,
|
|
2712
|
+
pathLabel(cycle.plugins[0], cycle.path),
|
|
2713
|
+
"潜在声明环;不据此认定挂起或失败",
|
|
2714
|
+
]);
|
|
2715
|
+
return h(
|
|
2716
|
+
"section",
|
|
2717
|
+
{ "aria-label": "依赖关系与潜在影响" },
|
|
2718
|
+
h("h3", null, "依赖关系与潜在影响"),
|
|
2719
|
+
...graph.notes.map((note, i) => h("p", { key: `note:${i}` }, note)),
|
|
2720
|
+
h(
|
|
2721
|
+
"p",
|
|
2722
|
+
null,
|
|
2723
|
+
`关系覆盖:${graph.coverage.status === "complete" ? "已覆盖本次提取输入(不等于运行时完整)" : "有限"}${graph.coverage.truncated ? ";已截断,不能当作全量关系或全量受影响者" : ""}。节点 ${graph.nodes.length},关系 ${graph.edges.length}。`,
|
|
2724
|
+
),
|
|
2725
|
+
...graph.coverage.reasons.map((reason, i) =>
|
|
2726
|
+
h("p", { key: `reason:${i}`, role: "note" }, `覆盖限制:${reason}`),
|
|
2727
|
+
),
|
|
2728
|
+
table(
|
|
2729
|
+
"插件及 loader 条目",
|
|
2730
|
+
[
|
|
2731
|
+
"插件",
|
|
2732
|
+
"宿主条目状态(不代表服务状态)",
|
|
2733
|
+
"Context 名称线索(非子作用域证明)",
|
|
2734
|
+
],
|
|
2735
|
+
graph.nodes
|
|
2736
|
+
.filter((node) => node.kind === "plugin")
|
|
2737
|
+
.map((node) => [
|
|
2738
|
+
node.name,
|
|
2739
|
+
entries(node),
|
|
2740
|
+
node.subContexts
|
|
2741
|
+
.filter((context) => context.names.length)
|
|
2742
|
+
.map(
|
|
2743
|
+
(context) =>
|
|
2744
|
+
`${context.file || "位置未知"}: ${context.names.join(", ")}`,
|
|
2745
|
+
)
|
|
2746
|
+
.join(";") || "未提取到命名线索",
|
|
2747
|
+
]),
|
|
2748
|
+
),
|
|
2749
|
+
relationshipRows.length
|
|
2750
|
+
? table(
|
|
2751
|
+
"服务依赖及候选提供者",
|
|
2752
|
+
[
|
|
2753
|
+
"消费者",
|
|
2754
|
+
"服务",
|
|
2755
|
+
"端",
|
|
2756
|
+
"依赖方式",
|
|
2757
|
+
"候选提供者 / loader 状态",
|
|
2758
|
+
"依赖来源",
|
|
2759
|
+
],
|
|
2760
|
+
relationshipRows,
|
|
2761
|
+
)
|
|
2762
|
+
: h("p", null, "本次未提取到服务依赖;不等于没有运行时依赖。"),
|
|
2763
|
+
providerRows.length
|
|
2764
|
+
? table(
|
|
2765
|
+
"提供声明(仅候选)",
|
|
2766
|
+
[
|
|
2767
|
+
"候选插件 / loader 状态",
|
|
2768
|
+
"服务",
|
|
2769
|
+
"端",
|
|
2770
|
+
"同端潜在消费者",
|
|
2771
|
+
"提供来源",
|
|
2772
|
+
],
|
|
2773
|
+
providerRows,
|
|
2774
|
+
)
|
|
2775
|
+
: null,
|
|
2776
|
+
packageRows.length
|
|
2777
|
+
? table("客户端包依赖", ["消费者", "客户端包", "语义"], packageRows)
|
|
2778
|
+
: null,
|
|
2779
|
+
impactRows.length
|
|
2780
|
+
? table(
|
|
2781
|
+
"潜在受影响插件及关系路径",
|
|
2782
|
+
["候选起点", "潜在受影响者", "端", "证据边界", "路径"],
|
|
2783
|
+
impactRows,
|
|
2784
|
+
)
|
|
2785
|
+
: h("p", null, "未枚举到可证明同端的传播路径;不等于没有潜在影响。"),
|
|
2786
|
+
cycleRows.length
|
|
2787
|
+
? table("潜在声明环", ["端", "关系路径", "说明"], cycleRows)
|
|
2788
|
+
: h("p", null, "本次覆盖内未枚举到声明环。"),
|
|
2789
|
+
h(
|
|
2790
|
+
"p",
|
|
2791
|
+
null,
|
|
2792
|
+
`预算:节点 ${graph.coverage.limits.nodes};关系 ${graph.coverage.limits.edges};传播遍历及路径 ${graph.coverage.limits.traversal};输入条目 ${graph.coverage.limits.items}。`,
|
|
2793
|
+
),
|
|
2794
|
+
);
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
/** Browser-safe React factory. Manual actions stay separate from kernel results. */
|
|
2798
|
+
function createFindingReviewPanel(React, ReviewContract) {
|
|
2799
|
+
const h = React.createElement,
|
|
2800
|
+
labels = ReviewContract.REVIEW_STATUS_LABELS;
|
|
2801
|
+
function ReviewRow({ item, contextToken, onSave, disabled }) {
|
|
2802
|
+
const [status, setStatus] = React.useState(
|
|
2803
|
+
item.record?.status || "pending",
|
|
2804
|
+
);
|
|
2805
|
+
const [note, setNote] = React.useState(item.note || "");
|
|
2806
|
+
const [busy, setBusy] = React.useState(false);
|
|
2807
|
+
const [error, setError] = React.useState("");
|
|
2808
|
+
const [message, setMessage] = React.useState("");
|
|
2809
|
+
const current = React.useRef({ contextToken, findingId: item.findingId });
|
|
2810
|
+
current.current = { contextToken, findingId: item.findingId };
|
|
2811
|
+
React.useEffect(() => {
|
|
2812
|
+
setStatus(item.record?.status || "pending");
|
|
2813
|
+
setNote(item.note || "");
|
|
2814
|
+
setError("");
|
|
2815
|
+
setMessage("");
|
|
2816
|
+
}, [contextToken, item.findingId, item.record?.updatedAt]);
|
|
2817
|
+
const save = async () => {
|
|
2818
|
+
if (busy || disabled) return;
|
|
2819
|
+
const target = { contextToken, findingId: item.findingId };
|
|
2820
|
+
try {
|
|
2821
|
+
const input = ReviewContract.validateReviewInput({
|
|
2822
|
+
...target,
|
|
2823
|
+
status,
|
|
2824
|
+
note,
|
|
2825
|
+
});
|
|
2826
|
+
setBusy(true);
|
|
2827
|
+
setError("");
|
|
2828
|
+
setMessage("");
|
|
2829
|
+
await onSave(input);
|
|
2830
|
+
if (
|
|
2831
|
+
current.current.contextToken === target.contextToken &&
|
|
2832
|
+
current.current.findingId === target.findingId
|
|
2833
|
+
)
|
|
2834
|
+
setMessage("人工处置已保存");
|
|
2835
|
+
} catch (reason) {
|
|
2836
|
+
if (
|
|
2837
|
+
current.current.contextToken === target.contextToken &&
|
|
2838
|
+
current.current.findingId === target.findingId
|
|
2839
|
+
)
|
|
2840
|
+
setError(reason?.message || "保存人工处置失败");
|
|
2841
|
+
} finally {
|
|
2842
|
+
setBusy(false);
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
return h(
|
|
2846
|
+
"article",
|
|
2847
|
+
{
|
|
2848
|
+
"data-review-finding": item.findingId,
|
|
2849
|
+
style: {
|
|
2850
|
+
border: "1px solid #d0d5dd",
|
|
2851
|
+
padding: "12px",
|
|
2852
|
+
marginBottom: "10px",
|
|
2853
|
+
borderRadius: "6px",
|
|
2854
|
+
},
|
|
2855
|
+
},
|
|
2856
|
+
h(
|
|
2857
|
+
"strong",
|
|
2858
|
+
null,
|
|
2859
|
+
`${item.pluginName}@${item.pluginVersion} · 发现 ${item.findingIndex + 1} · ${labels[item.status] || item.status}`,
|
|
2860
|
+
),
|
|
2861
|
+
item.reviewReason ? h("p", { role: "status" }, item.reviewReason) : null,
|
|
2862
|
+
item.record
|
|
2863
|
+
? h(
|
|
2864
|
+
"p",
|
|
2865
|
+
null,
|
|
2866
|
+
`原记录:${labels[item.record.status]} · ${item.record.updatedAt} · ${item.record.note}`,
|
|
2867
|
+
)
|
|
2868
|
+
: null,
|
|
2869
|
+
item.history?.length
|
|
2870
|
+
? h(
|
|
2871
|
+
"details",
|
|
2872
|
+
null,
|
|
2873
|
+
h(
|
|
2874
|
+
"summary",
|
|
2875
|
+
null,
|
|
2876
|
+
`旧处置摘要(${item.historyCount || item.history.length};完整依据见历史处置)`,
|
|
2877
|
+
),
|
|
2878
|
+
item.history.map((record) =>
|
|
2879
|
+
h(
|
|
2880
|
+
"p",
|
|
2881
|
+
{ key: record.findingId },
|
|
2882
|
+
`${record.pluginName}@${record.pluginVersion} · ${labels[record.status]} · ${record.updatedAt} · ${record.note}${record.noteTruncated ? "…" : ""}`,
|
|
2883
|
+
),
|
|
2884
|
+
),
|
|
2885
|
+
)
|
|
2886
|
+
: null,
|
|
2887
|
+
h(
|
|
2888
|
+
"label",
|
|
2889
|
+
null,
|
|
2890
|
+
"人工状态 ",
|
|
2891
|
+
h(
|
|
2892
|
+
"select",
|
|
2893
|
+
{
|
|
2894
|
+
"aria-label": `${item.pluginName} 发现 ${item.findingIndex + 1} 人工状态`,
|
|
2895
|
+
value: status,
|
|
2896
|
+
disabled: busy || disabled,
|
|
2897
|
+
onChange: (e) => {
|
|
2898
|
+
setStatus(e.target.value);
|
|
2899
|
+
setMessage("");
|
|
2900
|
+
},
|
|
2901
|
+
},
|
|
2902
|
+
Object.keys(labels)
|
|
2903
|
+
.filter((x) => x !== "needs-review")
|
|
2904
|
+
.map((value) => h("option", { key: value, value }, labels[value])),
|
|
2905
|
+
),
|
|
2906
|
+
),
|
|
2907
|
+
h(
|
|
2908
|
+
"label",
|
|
2909
|
+
{ style: { display: "block", marginTop: "8px" } },
|
|
2910
|
+
"核实依据 ",
|
|
2911
|
+
h("textarea", {
|
|
2912
|
+
"aria-label": `${item.pluginName} 发现 ${item.findingIndex + 1} 核实依据`,
|
|
2913
|
+
value: note,
|
|
2914
|
+
maxLength: ReviewContract.REVIEW_LIMITS.note,
|
|
2915
|
+
disabled: busy || disabled,
|
|
2916
|
+
rows: 3,
|
|
2917
|
+
style: { display: "block", width: "100%" },
|
|
2918
|
+
onChange: (e) => {
|
|
2919
|
+
setNote(e.target.value);
|
|
2920
|
+
setMessage("");
|
|
2921
|
+
},
|
|
2922
|
+
}),
|
|
2923
|
+
),
|
|
2924
|
+
h(
|
|
2925
|
+
"button",
|
|
2926
|
+
{ type: "button", disabled: busy || disabled, onClick: save },
|
|
2927
|
+
busy ? "保存中…" : "保存人工处置",
|
|
2928
|
+
),
|
|
2929
|
+
error ? h("p", { role: "alert" }, error) : null,
|
|
2930
|
+
message ? h("p", { role: "status" }, message) : null,
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
return function FindingReviewPanel({ report, reviews, onSave }) {
|
|
2934
|
+
const [saved, setSaved] = React.useState(null);
|
|
2935
|
+
const [limit, setLimit] = React.useState(20);
|
|
2936
|
+
const [historyOpen, setHistoryOpen] = React.useState(false);
|
|
2937
|
+
const [historyLimit, setHistoryLimit] = React.useState(20);
|
|
2938
|
+
const current = React.useRef(reviews?.contextToken);
|
|
2939
|
+
current.current = reviews?.contextToken;
|
|
2940
|
+
React.useEffect(() => {
|
|
2941
|
+
setSaved(null);
|
|
2942
|
+
setLimit(20);
|
|
2943
|
+
setHistoryOpen(false);
|
|
2944
|
+
setHistoryLimit(20);
|
|
2945
|
+
}, [reviews?.contextToken, reviews?.revision]);
|
|
2946
|
+
const active =
|
|
2947
|
+
saved && saved.contextToken === reviews?.contextToken ? saved : reviews;
|
|
2948
|
+
if (active == null) return null;
|
|
2949
|
+
if (!ReviewContract.validateReviewEnvelope(active, report))
|
|
2950
|
+
return h(
|
|
2951
|
+
"section",
|
|
2952
|
+
{ "aria-label": "发现的人工处置", style: { marginTop: "16px" } },
|
|
2953
|
+
h("h3", null, "发现的人工处置"),
|
|
2954
|
+
h(
|
|
2955
|
+
"p",
|
|
2956
|
+
{ role: "alert" },
|
|
2957
|
+
"人工处置数据无效,本节暂不可用;请刷新后重试。",
|
|
2958
|
+
),
|
|
2959
|
+
);
|
|
2960
|
+
const save = async (input) => {
|
|
2961
|
+
const result = await onSave(input),
|
|
2962
|
+
next = result?.reviews || result;
|
|
2963
|
+
if (!ReviewContract.validateReviewEnvelope(next, report))
|
|
2964
|
+
throw new Error("保存响应无效");
|
|
2965
|
+
if (next.error) throw new Error(next.error);
|
|
2966
|
+
if (
|
|
2967
|
+
current.current !== input.contextToken ||
|
|
2968
|
+
next.contextToken !== input.contextToken
|
|
2969
|
+
)
|
|
2970
|
+
throw new Error("报告已变化,请重新核实");
|
|
2971
|
+
setSaved(next);
|
|
2972
|
+
return next;
|
|
2973
|
+
};
|
|
2974
|
+
const items = (active.items || []).filter(
|
|
2975
|
+
(item) =>
|
|
2976
|
+
!report?.plugins || Object.hasOwn(report.plugins, item.pluginName),
|
|
2977
|
+
);
|
|
2978
|
+
return h(
|
|
2979
|
+
"section",
|
|
2980
|
+
{ "aria-label": "发现的人工处置", style: { marginTop: "16px" } },
|
|
2981
|
+
h("h3", null, "发现的人工处置"),
|
|
2982
|
+
h(
|
|
2983
|
+
"p",
|
|
2984
|
+
null,
|
|
2985
|
+
"人工记录仅供核实追踪,不改变内核发现、总结论、退出码、基线或兼容性豁免;新风险仍完整显示。",
|
|
2986
|
+
),
|
|
2987
|
+
active.error ? h("p", { role: "alert" }, active.error) : null,
|
|
2988
|
+
items.length
|
|
2989
|
+
? items.slice(0, limit).map((item) =>
|
|
2990
|
+
h(ReviewRow, {
|
|
2991
|
+
key: item.findingId,
|
|
2992
|
+
item,
|
|
2993
|
+
contextToken: active.contextToken,
|
|
2994
|
+
onSave: save,
|
|
2995
|
+
disabled: !!active.error || typeof onSave !== "function",
|
|
2996
|
+
}),
|
|
2997
|
+
)
|
|
2998
|
+
: h("p", null, "当前没有可处置的发现。"),
|
|
2999
|
+
items.length > limit
|
|
3000
|
+
? h(
|
|
3001
|
+
"button",
|
|
3002
|
+
{ type: "button", onClick: () => setLimit((n) => n + 20) },
|
|
3003
|
+
`显示更多处置(已显示 ${Math.min(limit, items.length)} / ${items.length})`,
|
|
3004
|
+
)
|
|
3005
|
+
: null,
|
|
3006
|
+
active.history?.length
|
|
3007
|
+
? h(
|
|
3008
|
+
"details",
|
|
3009
|
+
{
|
|
3010
|
+
open: historyOpen,
|
|
3011
|
+
onToggle: (e) => setHistoryOpen(e.target.open),
|
|
3012
|
+
},
|
|
3013
|
+
h("summary", null, `历史处置完整依据(${active.history.length})`),
|
|
3014
|
+
historyOpen
|
|
3015
|
+
? active.history
|
|
3016
|
+
.slice(0, historyLimit)
|
|
3017
|
+
.map((record) =>
|
|
3018
|
+
h(
|
|
3019
|
+
"p",
|
|
3020
|
+
{ key: record.findingId },
|
|
3021
|
+
`${record.pluginName}@${record.pluginVersion} · ${labels[record.status]} · ${record.updatedAt} · ${record.note}`,
|
|
3022
|
+
),
|
|
3023
|
+
)
|
|
3024
|
+
: null,
|
|
3025
|
+
historyOpen && active.history.length > historyLimit
|
|
3026
|
+
? h(
|
|
3027
|
+
"button",
|
|
3028
|
+
{
|
|
3029
|
+
type: "button",
|
|
3030
|
+
onClick: () => setHistoryLimit((n) => n + 20),
|
|
3031
|
+
},
|
|
3032
|
+
"显示更多历史处置",
|
|
3033
|
+
)
|
|
3034
|
+
: null,
|
|
3035
|
+
)
|
|
3036
|
+
: null,
|
|
3037
|
+
);
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
/** Offline UI for explicitly imported health evidence. Never starts a runner. */
|
|
3041
|
+
function createFunctionalHealthPanel(React, HealthContract) {
|
|
3042
|
+
const h = React.createElement;
|
|
3043
|
+
const statusLabels = {
|
|
3044
|
+
"tested-pass": "所选检查通过",
|
|
3045
|
+
"tested-failed": "所选检查失败",
|
|
3046
|
+
error: "检查未完成",
|
|
3047
|
+
pass: "通过",
|
|
3048
|
+
failed: "失败",
|
|
3049
|
+
};
|
|
3050
|
+
return function FunctionalHealthPanel({ report }) {
|
|
3051
|
+
const [result, setResult] = React.useState(null);
|
|
3052
|
+
const [error, setError] = React.useState("");
|
|
3053
|
+
const [busy, setBusy] = React.useState(false);
|
|
3054
|
+
const request = React.useRef(0);
|
|
3055
|
+
React.useEffect(
|
|
3056
|
+
() => () => {
|
|
3057
|
+
request.current++;
|
|
3058
|
+
},
|
|
3059
|
+
[],
|
|
3060
|
+
);
|
|
3061
|
+
const load = async (event) => {
|
|
3062
|
+
const file = event.target.files?.[0],
|
|
3063
|
+
id = ++request.current;
|
|
3064
|
+
if (!file) return;
|
|
3065
|
+
setBusy(true);
|
|
3066
|
+
setError("");
|
|
3067
|
+
try {
|
|
3068
|
+
if (
|
|
3069
|
+
!Number.isSafeInteger(file.size) ||
|
|
3070
|
+
file.size < 1 ||
|
|
3071
|
+
file.size > 512 * 1024
|
|
3072
|
+
)
|
|
3073
|
+
throw new Error("记录超过大小限制(512 KiB)");
|
|
3074
|
+
const parsed = JSON.parse(await file.text());
|
|
3075
|
+
const checked = HealthContract.validateFunctionalHealthResult(parsed);
|
|
3076
|
+
if (!checked.valid) throw new Error(checked.errors.join(";"));
|
|
3077
|
+
if (!globalThis.crypto?.subtle || typeof TextEncoder === "undefined")
|
|
3078
|
+
throw new Error("当前浏览器无法核验记录摘要");
|
|
3079
|
+
const bytes = new TextEncoder().encode(
|
|
3080
|
+
HealthContract.canonicalFunctionalManifest(parsed.declaration),
|
|
3081
|
+
);
|
|
3082
|
+
const digest = Array.from(
|
|
3083
|
+
new Uint8Array(
|
|
3084
|
+
await globalThis.crypto.subtle.digest("SHA-256", bytes),
|
|
3085
|
+
),
|
|
3086
|
+
(n) => n.toString(16).padStart(2, "0"),
|
|
3087
|
+
).join("");
|
|
3088
|
+
if (digest !== parsed.identity.manifestSha256)
|
|
3089
|
+
throw new Error("检查声明与记录摘要不一致");
|
|
3090
|
+
if (request.current === id) setResult(parsed);
|
|
3091
|
+
} catch (reason) {
|
|
3092
|
+
if (request.current === id)
|
|
3093
|
+
setError("导入失败:" + (reason?.message || reason));
|
|
3094
|
+
} finally {
|
|
3095
|
+
if (request.current === id) setBusy(false);
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
const artifact = result?.identity.artifact,
|
|
3099
|
+
current = artifact && report?.plugins?.[artifact.name];
|
|
3100
|
+
const versionsMatch =
|
|
3101
|
+
current?.version === artifact?.version &&
|
|
3102
|
+
report?.dshVersion === result?.identity.dshVersion;
|
|
3103
|
+
return h(
|
|
3104
|
+
"section",
|
|
3105
|
+
{ "aria-label": "功能健康检查", style: { marginTop: "16px" } },
|
|
3106
|
+
h("h3", null, "功能健康检查(独立实测记录)"),
|
|
3107
|
+
h(
|
|
3108
|
+
"p",
|
|
3109
|
+
null,
|
|
3110
|
+
"在仓库CLI中显式运行所选HTTP/DOM检查后,可导入JSON记录。本面板只读取所选文件,不执行检查、安装插件或访问目标服务。",
|
|
3111
|
+
),
|
|
3112
|
+
h("input", {
|
|
3113
|
+
type: "file",
|
|
3114
|
+
accept: ".json,application/json",
|
|
3115
|
+
"aria-label": "导入功能健康检查记录",
|
|
3116
|
+
disabled: busy,
|
|
3117
|
+
onChange: load,
|
|
3118
|
+
}),
|
|
3119
|
+
busy ? h("p", { role: "status" }, "正在核验记录…") : null,
|
|
3120
|
+
error ? h("p", { role: "alert" }, error) : null,
|
|
3121
|
+
!result
|
|
3122
|
+
? h(
|
|
3123
|
+
"p",
|
|
3124
|
+
null,
|
|
3125
|
+
"尚未导入功能实测记录。静态发现、宿主状态和业务健康是不同证据。",
|
|
3126
|
+
)
|
|
3127
|
+
: h(
|
|
3128
|
+
"div",
|
|
3129
|
+
{ "data-functional-health": result.status },
|
|
3130
|
+
h(
|
|
3131
|
+
"p",
|
|
3132
|
+
null,
|
|
3133
|
+
`${artifact.name}@${artifact.version} · DSH ${result.identity.dshVersion} · ${statusLabels[result.status]}`,
|
|
3134
|
+
),
|
|
3135
|
+
h(
|
|
3136
|
+
"p",
|
|
3137
|
+
{ role: "status" },
|
|
3138
|
+
versionsMatch
|
|
3139
|
+
? "插件与DSH版本相符;当前安装制品尚未核验,此处保留所选制品的历史记录。"
|
|
3140
|
+
: "记录与当前报告目标不一致或插件未安装;不用于认证当前目标。",
|
|
3141
|
+
),
|
|
3142
|
+
h(
|
|
3143
|
+
"p",
|
|
3144
|
+
null,
|
|
3145
|
+
"测量时间:" + result.startedAt + " → " + result.completedAt,
|
|
3146
|
+
),
|
|
3147
|
+
h("p", null, "制品 SHA-256:" + artifact.sha256),
|
|
3148
|
+
h(
|
|
3149
|
+
"p",
|
|
3150
|
+
null,
|
|
3151
|
+
"声明 SHA-256:" +
|
|
3152
|
+
result.identity.manifestSha256 +
|
|
3153
|
+
";摘要校验不是作者签名或业务认证。",
|
|
3154
|
+
),
|
|
3155
|
+
h(
|
|
3156
|
+
"table",
|
|
3157
|
+
{ "aria-label": "功能健康检查结果" },
|
|
3158
|
+
h(
|
|
3159
|
+
"thead",
|
|
3160
|
+
null,
|
|
3161
|
+
h(
|
|
3162
|
+
"tr",
|
|
3163
|
+
null,
|
|
3164
|
+
h("th", null, "检查与声明"),
|
|
3165
|
+
h("th", null, "独立实测"),
|
|
3166
|
+
),
|
|
3167
|
+
),
|
|
3168
|
+
h(
|
|
3169
|
+
"tbody",
|
|
3170
|
+
null,
|
|
3171
|
+
result.checks.map((check, i) => {
|
|
3172
|
+
const declaration = result.declaration.checks[i],
|
|
3173
|
+
expected =
|
|
3174
|
+
declaration.type === "http"
|
|
3175
|
+
? `GET ${declaration.path},HTTP ${declaration.status}` +
|
|
3176
|
+
(declaration.json
|
|
3177
|
+
? `,JSON ${declaration.json.pointer} = ${JSON.stringify(declaration.json.equals)}`
|
|
3178
|
+
: "")
|
|
3179
|
+
: `${declaration.path} · ${declaration.selector} · 数量 ${declaration.minCount}–${declaration.maxCount}`;
|
|
3180
|
+
return h(
|
|
3181
|
+
"tr",
|
|
3182
|
+
{ key: check.id },
|
|
3183
|
+
h("td", null, `${check.id} · ${expected}`),
|
|
3184
|
+
h(
|
|
3185
|
+
"td",
|
|
3186
|
+
null,
|
|
3187
|
+
`${statusLabels[check.status] || check.status} · ${JSON.stringify(check.evidence)}`,
|
|
3188
|
+
),
|
|
3189
|
+
);
|
|
3190
|
+
}),
|
|
3191
|
+
),
|
|
3192
|
+
),
|
|
3193
|
+
result.error ? h("p", { role: "alert" }, result.error) : null,
|
|
3194
|
+
h(
|
|
3195
|
+
"p",
|
|
3196
|
+
null,
|
|
3197
|
+
"资源清理:" +
|
|
3198
|
+
(result.cleanup.stopped && result.cleanup.removed
|
|
3199
|
+
? "已停止并清理自有环境"
|
|
3200
|
+
: "未确认完整清理"),
|
|
3201
|
+
),
|
|
3202
|
+
h(
|
|
3203
|
+
"ul",
|
|
3204
|
+
null,
|
|
3205
|
+
result.limitations.map((text, i) => h("li", { key: i }, text)),
|
|
3206
|
+
),
|
|
3207
|
+
h(
|
|
3208
|
+
"p",
|
|
3209
|
+
null,
|
|
3210
|
+
"仅证明记录中的所选检查结果,不改普通兼容性结论、风险计数、基线或人工处置;不认证全部业务。",
|
|
3211
|
+
),
|
|
3212
|
+
h(
|
|
3213
|
+
"button",
|
|
3214
|
+
{
|
|
3215
|
+
type: "button",
|
|
3216
|
+
onClick: () => {
|
|
3217
|
+
request.current++;
|
|
3218
|
+
setResult(null);
|
|
3219
|
+
setError("");
|
|
3220
|
+
setBusy(false);
|
|
3221
|
+
},
|
|
3222
|
+
},
|
|
3223
|
+
"清除健康检查记录",
|
|
3224
|
+
),
|
|
3225
|
+
),
|
|
3226
|
+
);
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
return { createDependencyPanel:createDependencyPanel, createFindingReviewPanel:createFindingReviewPanel, createFunctionalHealthPanel:createFunctionalHealthPanel };
|
|
3231
|
+
})();
|
|
3232
|
+
// END GENERATED WORKFLOW PANELS
|
|
3233
|
+
|
|
530
3234
|
var React = require("react");
|
|
3235
|
+
var DependencyPanel = WorkflowPanelFactory.createDependencyPanel(React, DependencyGraphContract);
|
|
3236
|
+
var FindingReviewPanel = WorkflowPanelFactory.createFindingReviewPanel(React, FindingReviewContract);
|
|
3237
|
+
var FunctionalHealthPanel = WorkflowPanelFactory.createFunctionalHealthPanel(React, FunctionalHealthContract);
|
|
531
3238
|
|
|
532
3239
|
var SLOT = "settings.plugins.tab";
|
|
533
3240
|
var LABEL = "兼容性";
|
|
@@ -554,9 +3261,11 @@ return { assertEnvelope: assertEnvelope };
|
|
|
554
3261
|
var CATEGORY = {
|
|
555
3262
|
"host-service-missing": "服务缺失",
|
|
556
3263
|
"host-method-missing": "方法缺失",
|
|
557
|
-
"unprovided-dependency": "
|
|
3264
|
+
"unprovided-dependency": "依赖服务待确认",
|
|
558
3265
|
"declaration-mismatch": "声明与实测不符",
|
|
559
3266
|
"declared-incompatible": "版本声明不支持",
|
|
3267
|
+
"client-entry-missing": "客户端入口不可交付",
|
|
3268
|
+
"host-entry-inactive": "宿主条目未激活",
|
|
560
3269
|
};
|
|
561
3270
|
|
|
562
3271
|
// 渲染侧标记:只在我们的组件体里设置。页面上出现同名文字**不能**证明
|
|
@@ -602,44 +3311,54 @@ return { assertEnvelope: assertEnvelope };
|
|
|
602
3311
|
|
|
603
3312
|
function useCompatibility() {
|
|
604
3313
|
var state = React.useState({ phase: "loading", data: null, error: null });
|
|
605
|
-
var
|
|
3314
|
+
var alive = React.useRef(true), latest = React.useRef(null), pending = React.useRef(null), queuedForce = React.useRef(false);
|
|
606
3315
|
var set = state[1];
|
|
607
|
-
var
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
credentials: "same-origin",
|
|
615
|
-
})
|
|
616
|
-
.then(function (res) {
|
|
617
|
-
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
618
|
-
return res.json();
|
|
619
|
-
})
|
|
620
|
-
.then(function (data) {
|
|
621
|
-
data = acceptPayload(data);
|
|
622
|
-
if (!alive.current) return;
|
|
623
|
-
set({ phase: "done", data: data, error: null });
|
|
624
|
-
})
|
|
625
|
-
.catch(function (error) {
|
|
626
|
-
if (!alive.current) return;
|
|
627
|
-
// 取不到数据**也要说清楚是什么问题**。一个永远转圈的加载状态
|
|
628
|
-
// 与一个永远空白的表格,用户无法区分"工具坏了"与"没问题"。
|
|
629
|
-
set({ phase: "error", data: null, error: String((error && error.message) || error) });
|
|
630
|
-
});
|
|
631
|
-
} catch (error) {
|
|
632
|
-
set({ phase: "error", data: null, error: String((error && error.message) || error) });
|
|
3316
|
+
var load = React.useCallback(function (refresh, background) {
|
|
3317
|
+
if (pending.current) {if(refresh)queuedForce.current=true;return pending.current;}
|
|
3318
|
+
function read(url) {
|
|
3319
|
+
return fetch(url, {headers:{accept:"application/json"},credentials:"same-origin"}).then(function(res){
|
|
3320
|
+
if(!res.ok)throw new Error("HTTP " + res.status);
|
|
3321
|
+
return res.json();
|
|
3322
|
+
});
|
|
633
3323
|
}
|
|
3324
|
+
var request = Promise.resolve().then(function(){
|
|
3325
|
+
return read(refresh ? ROUTE + "?refresh=1" : background ? ROUTE + "?metadata=1" : ROUTE);
|
|
3326
|
+
}).then(function(data){
|
|
3327
|
+
if(background && data.kind === "plugin-compat-check/report-metadata" && data.ok && latest.current
|
|
3328
|
+
&& data.computedAt === latest.current.computedAt && data.nodeModules === latest.current.nodeModules
|
|
3329
|
+
&& data.dshVersion === latest.current.report.dshVersion
|
|
3330
|
+
&& (data.reviewsRevision || null) === (latest.current.reviews && latest.current.reviews.revision || null)
|
|
3331
|
+
&& (data.reviewsError || null) === (latest.current.reviews && latest.current.reviews.error || null)
|
|
3332
|
+
&& (data.reviewsContextToken || null) === (latest.current.reviews && latest.current.reviews.contextToken || null)) {
|
|
3333
|
+
return Object.assign({},latest.current,{cache:data.cache});
|
|
3334
|
+
}
|
|
3335
|
+
return background ? read(ROUTE) : data;
|
|
3336
|
+
}).then(function(data){
|
|
3337
|
+
data = acceptPayload(data);
|
|
3338
|
+
if(!data.ok && latest.current)throw new Error(data.error || "后台刷新失败");
|
|
3339
|
+
if(!alive.current)return;
|
|
3340
|
+
latest.current=data;
|
|
3341
|
+
set({phase:"done",data:data,error:null});
|
|
3342
|
+
}).catch(function(error){
|
|
3343
|
+
if(!alive.current)return;
|
|
3344
|
+
var message=String(error && error.message || error);
|
|
3345
|
+
if(latest.current && latest.current.ok){
|
|
3346
|
+
var previous=latest.current;
|
|
3347
|
+
var kept=Object.assign({},previous,{cache:Object.assign({},previous.cache,{hit:true,stale:true,refreshing:false,refreshError:message})});
|
|
3348
|
+
set({phase:"done",data:kept,error:message});
|
|
3349
|
+
}else set({phase:"error",data:null,error:message});
|
|
3350
|
+
}).then(function(){pending.current=null;if(queuedForce.current && alive.current){queuedForce.current=false;return load(true,false);}});
|
|
3351
|
+
pending.current=request;
|
|
3352
|
+
return request;
|
|
634
3353
|
}, []);
|
|
635
|
-
|
|
636
3354
|
React.useEffect(function () {
|
|
637
3355
|
alive.current = true;
|
|
638
|
-
|
|
639
|
-
|
|
3356
|
+
var timer;
|
|
3357
|
+
function schedule(){if(alive.current)timer=setTimeout(function(){return load(false,true).then(schedule);},2000);}
|
|
3358
|
+
load(false).then(schedule);
|
|
3359
|
+
return function () {alive.current=false;clearTimeout(timer);};
|
|
640
3360
|
}, [load]);
|
|
641
|
-
|
|
642
|
-
return { value: value, reload: load };
|
|
3361
|
+
return {value:state[0],reload:load};
|
|
643
3362
|
}
|
|
644
3363
|
|
|
645
3364
|
var styles = {
|
|
@@ -677,13 +3396,32 @@ return { assertEnvelope: assertEnvelope };
|
|
|
677
3396
|
return React.createElement("span", { style: { color: color, fontWeight: 600 } }, SEVERITY[s] || s);
|
|
678
3397
|
}
|
|
679
3398
|
|
|
3399
|
+
function Coverage(p) {
|
|
3400
|
+
var c = p.plugin.coverage;
|
|
3401
|
+
if (!c) return null;
|
|
3402
|
+
return React.createElement("div", { style: styles.dim },
|
|
3403
|
+
"证据覆盖:" + (c.status === "complete" ? "完整" : "有限") + ";已验证调用 " + c.verifiedCalls,
|
|
3404
|
+
c.limitations.length ? React.createElement("details", null,
|
|
3405
|
+
React.createElement("summary", { style: { cursor: "pointer" } }, "覆盖限制(" + c.limitations.length + ")"),
|
|
3406
|
+
c.limitations.map(function (note, i) {
|
|
3407
|
+
return React.createElement("div", { key: i }, "覆盖限制:" + note);
|
|
3408
|
+
})) : null);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
function EvidenceLegend() {
|
|
3412
|
+
return React.createElement("div", { style: styles.note },
|
|
3413
|
+
"确定(certain)可由明确静态事实或合格运行时观测支持;疑似(suspected)表示当前证据不足以确定该条发现。",
|
|
3414
|
+
"影响依据:实测(measured)须有对应观测,推断(inferred)不等于业务影响已实测。",
|
|
3415
|
+
"未观测不等于缺失;没有已验证调用仍可能存在服务依赖或客户端功能;调用核验和覆盖完整均不等于业务验收通过。");
|
|
3416
|
+
}
|
|
3417
|
+
|
|
680
3418
|
function Findings(p) {
|
|
681
3419
|
var pl = p.plugin;
|
|
682
3420
|
if (!pl.findings || pl.findings.length === 0) {
|
|
683
|
-
var parts = [];
|
|
3421
|
+
var parts = [p.hideCoverage ? null : React.createElement(Coverage, { key: "coverage", plugin: pl })];
|
|
684
3422
|
if (pl.vacuous) {
|
|
685
3423
|
parts.push(React.createElement("div", { key: "v", style: styles.suspect },
|
|
686
|
-
"
|
|
3424
|
+
"本轮未发现冲突,但没有已验证调用;插件仍可能依赖 DSH 服务或包含未验证的客户端功能,不能据此确认业务可用。"));
|
|
687
3425
|
}
|
|
688
3426
|
var noCalls = Object.keys(pl.resolved || {})
|
|
689
3427
|
.map(function (k) { return pl.resolved[k]; })
|
|
@@ -698,7 +3436,7 @@ return { assertEnvelope: assertEnvelope };
|
|
|
698
3436
|
}
|
|
699
3437
|
return parts.length ? React.createElement("div", null, parts) : null;
|
|
700
3438
|
}
|
|
701
|
-
return React.createElement("div", null, pl.findings.map(function (f, i) {
|
|
3439
|
+
return React.createElement("div", null, p.hideCoverage ? null : React.createElement(Coverage, { plugin: pl }), pl.findings.map(function (f, i) {
|
|
702
3440
|
var api = f.service ? (f.method ? f.service + "." + f.method : f.service) : "(包声明)";
|
|
703
3441
|
var loc = f.location ? f.location.file + ":" + f.location.line : "—";
|
|
704
3442
|
return React.createElement("div", { key: i, style: styles.finding },
|
|
@@ -706,12 +3444,19 @@ return { assertEnvelope: assertEnvelope };
|
|
|
706
3444
|
React.createElement("span", null, " " + (CATEGORY[f.category] || f.category) + " "),
|
|
707
3445
|
React.createElement("code", { style: styles.mono }, api),
|
|
708
3446
|
React.createElement("span", { style: styles.mono }, " @ " + loc),
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
:
|
|
3447
|
+
React.createElement("span", {
|
|
3448
|
+
style: styles.suspect,
|
|
3449
|
+
title: f.certainty === "certain" ? "当前证据足以支持该条发现,可来自明确静态事实或合格运行时观测;不等于影响已实测。" : "当前证据不足以确定该条发现;存在运行时观测也不代表该调用已验证。",
|
|
3450
|
+
}, f.certainty === "certain" ? " [确定]" : " [疑似]"),
|
|
712
3451
|
f.impact ? React.createElement("div", { style: styles.dim },
|
|
713
|
-
(f.impactBasis === "inferred" ? "影响(推断,非实测):" : "
|
|
714
|
-
f.basis ? React.createElement("div", { style: styles.dim }, "依据:" + f.basis) : null
|
|
3452
|
+
(f.impactBasis === "measured" ? "影响(实测):" : f.impactBasis === "inferred" ? "影响(推断,非实测):" : "影响(依据未提供):") + f.impact) : null,
|
|
3453
|
+
f.basis ? React.createElement("div", { style: styles.dim }, "依据:" + f.basis) : null,
|
|
3454
|
+
React.createElement("div", { style: { marginTop: "6px" } },
|
|
3455
|
+
React.createElement("strong", null, "下一步核实"),
|
|
3456
|
+
React.createElement("ol", { style: { margin: "4px 0", paddingLeft: "22px" } },
|
|
3457
|
+
FindingGuidance.nextSteps(f).map(function (step, j) {
|
|
3458
|
+
return React.createElement("li", { key: j }, step);
|
|
3459
|
+
}))));
|
|
715
3460
|
}));
|
|
716
3461
|
}
|
|
717
3462
|
|
|
@@ -910,8 +3655,7 @@ return { assertEnvelope: assertEnvelope };
|
|
|
910
3655
|
onClick: function () { st.reload(true); },
|
|
911
3656
|
}, "重新检查")),
|
|
912
3657
|
React.createElement(Findings, { plugin: pl }),
|
|
913
|
-
|
|
914
|
-
"这个插件的结论是空洞的:它没有用过任何 DSH 服务,所以没有可检查的东西。") : null);
|
|
3658
|
+
React.createElement(EvidenceLegend));
|
|
915
3659
|
}
|
|
916
3660
|
|
|
917
3661
|
/** C3 · 详情页角标:一行字,放在标题行 */
|
|
@@ -930,9 +3674,137 @@ return { assertEnvelope: assertEnvelope };
|
|
|
930
3674
|
}, v.text);
|
|
931
3675
|
}
|
|
932
3676
|
|
|
3677
|
+
function UpgradeDetails(p) {
|
|
3678
|
+
var r = p.result;
|
|
3679
|
+
if (!r.ok) return React.createElement("div", { role: "status", style: styles.err }, r.error);
|
|
3680
|
+
var groups = [["added", "新增"], ["worsened", "变重"], ["resolved", "消失"], ["improved", "减轻"], ["unchanged", "不变"]];
|
|
3681
|
+
return React.createElement("div", { "data-upgrade-result": "compared" },
|
|
3682
|
+
React.createElement("p", null, "静态版本对比:" + r.currentVersion + " → " + r.targetVersion),
|
|
3683
|
+
React.createElement("p", { role: "status" }, "新增 " + r.summary.added + " · 变重 " + r.summary.worsened + " · 消失 " + r.summary.resolved + " · 减轻 " + r.summary.improved + " · 不变 " + r.summary.unchanged),
|
|
3684
|
+
!r.summary.added && !r.summary.worsened ? React.createElement("p", { style: styles.note }, "本次没有发现新增或变重的静态风险;目标版本仍可能有原有风险,不等于业务通过。") : null,
|
|
3685
|
+
groups.map(function (group) {
|
|
3686
|
+
return r.changes[group[0]].length ? React.createElement("details", { key: group[0], open: group[0] === "added" || group[0] === "worsened" },
|
|
3687
|
+
React.createElement("summary", null, group[1] + "(" + r.changes[group[0]].length + ")"),
|
|
3688
|
+
r.changes[group[0]].map(function (entry, i) {
|
|
3689
|
+
var f = entry.finding || entry.after;
|
|
3690
|
+
return React.createElement("div", { key: i, style: { margin: "8px 0" } },
|
|
3691
|
+
React.createElement("div", null, entry.plugin + " · " + f.category + " · " + [f.service, f.method].filter(Boolean).join(".")),
|
|
3692
|
+
React.createElement("div", { style: styles.mono }, f.severity + " / " + f.certainty + " / " + f.impactBasis + (f.location ? " · " + f.location.file + ":" + f.location.line : "")),
|
|
3693
|
+
entry.before ? React.createElement("div", { style: styles.dim }, "此前:" + entry.before.severity + " / " + entry.before.certainty + " / " + entry.before.impactBasis) : null,
|
|
3694
|
+
React.createElement("div", null, f.impact), React.createElement("div", { style: styles.dim }, f.basis));
|
|
3695
|
+
})) : null;
|
|
3696
|
+
}),
|
|
3697
|
+
["currentReport", "targetReport"].map(function (side) {
|
|
3698
|
+
var report = r[side], s = report.summary;
|
|
3699
|
+
return React.createElement("details", { key: side },
|
|
3700
|
+
React.createElement("summary", null, (side === "currentReport" ? "当前版本" : "目标版本") + "静态结果 " + report.dshVersion + " · 不兼容 " + s.broken + " · 部分可用 " + s.partial + " · 未知 " + s.unknown),
|
|
3701
|
+
Object.keys(report.plugins).map(function (name) {
|
|
3702
|
+
var pl = report.plugins[name];
|
|
3703
|
+
return React.createElement("div", { key: name, style: { margin: "8px 0" } },
|
|
3704
|
+
React.createElement("div", null, name + "@" + (pl.version || "?") + " · " + pl.verdict + (pl.vacuous ? " · 无已验证调用" : "")),
|
|
3705
|
+
React.createElement(Findings, { plugin: pl }));
|
|
3706
|
+
}));
|
|
3707
|
+
}),
|
|
3708
|
+
React.createElement("details", null,
|
|
3709
|
+
React.createElement("summary", null, "查看输入制品、目录来源与预测限制"),
|
|
3710
|
+
React.createElement("div", { style: styles.mono }, "输入 SHA-256:" + r.artifact.sha256),
|
|
3711
|
+
r.artifact.packages.map(function (item, i) { return React.createElement("div", { key: i, style: styles.mono }, item.name + "@" + (item.version || "?") + " SHA-256:" + item.sha256); }),
|
|
3712
|
+
["current", "target"].map(function (side) {
|
|
3713
|
+
var c = r.catalogs[side];
|
|
3714
|
+
return React.createElement("div", { key: side, style: styles.mono }, "API 目录:" + c.version + " · " + c.file + " · SHA-256:" + c.sha256 + " · 来源:" + c.source.host + " / " + c.source.client);
|
|
3715
|
+
}),
|
|
3716
|
+
React.createElement("div", { style: styles.mono }, "规则:" + r.rules.file + " · SHA-256:" + r.rules.sha256),
|
|
3717
|
+
r.limitations.map(function (note, i) { return React.createElement("div", { key: i, style: styles.note }, note); })));
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
function UpgradePreview(p) {
|
|
3721
|
+
var target = React.useState(""), state = React.useState({ phase: "idle" }), generation = React.useRef(0);
|
|
3722
|
+
var currentVersion = p.data.report.dshVersion;
|
|
3723
|
+
// A review revision update may fetch a fresh JSON object for the same report.
|
|
3724
|
+
// Cache serialization by object reference; local form renders do not repeat it.
|
|
3725
|
+
var reportIdentity = React.useRef(null);
|
|
3726
|
+
if (!reportIdentity.current || reportIdentity.current.report !== p.data.report) {
|
|
3727
|
+
reportIdentity.current = { report: p.data.report, content: JSON.stringify(p.data.report) };
|
|
3728
|
+
}
|
|
3729
|
+
React.useEffect(function () {
|
|
3730
|
+
generation.current++;
|
|
3731
|
+
state[1]({ phase: "idle" });
|
|
3732
|
+
return function () { generation.current++; };
|
|
3733
|
+
}, [currentVersion, p.data.nodeModules, p.data.computedAt, p.data.reviews && p.data.reviews.contextToken, reportIdentity.current.content]);
|
|
3734
|
+
async function run() {
|
|
3735
|
+
var requested = String(target[0]).trim(), ticket = ++generation.current;
|
|
3736
|
+
state[1]({ phase: "loading" });
|
|
3737
|
+
try {
|
|
3738
|
+
var response = await fetch("/plugin-compat-check/upgrade-preview?targetVersion=" + encodeURIComponent(requested) + "¤tVersion=" + encodeURIComponent(currentVersion || ""), { credentials: "same-origin", headers: { Accept: "application/json" } });
|
|
3739
|
+
if (!response.ok) throw Error("HTTP " + response.status);
|
|
3740
|
+
var result;
|
|
3741
|
+
try { result = UpgradeContract.assertUpgradePreview(await response.json()); }
|
|
3742
|
+
catch (error) { throw Error("升级预检结果格式无效,请确认宿主与客户端版本一致。"); }
|
|
3743
|
+
if (result.currentVersion !== currentVersion || result.targetVersion !== requested) throw Error("升级预检版本已变化,请刷新普通报告后重试。");
|
|
3744
|
+
if (ticket === generation.current) state[1]({ phase: "done", result: result });
|
|
3745
|
+
} catch (error) {
|
|
3746
|
+
if (ticket === generation.current) state[1]({ phase: "error", error: String(error.message || error) });
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
return React.createElement("details", { style: { margin: "14px 0" }, "data-upgrade-preview": true },
|
|
3750
|
+
React.createElement("summary", { style: { cursor: "pointer" } }, "升级前兼容性预检(静态)"),
|
|
3751
|
+
React.createElement("p", { style: styles.note }, "比较同一组已安装制品在两个版本目录下的静态风险。静态预测不等于目标版本业务通过;不自动安装或升级。当前版本:" + (currentVersion || "未知") + "。可选目录版本:" + UpgradeContract.SUPPORTED_PREVIEW_VERSIONS.join("、")),
|
|
3752
|
+
React.createElement("input", { "aria-label": "目标 DSH 版本", placeholder: "输入精确目标版本", value: target[0], style: styles.btn,
|
|
3753
|
+
onChange: function (event) { generation.current++; target[1](event.target.value); state[1]({ phase: "idle" }); } }),
|
|
3754
|
+
React.createElement("button", { type: "button", style: styles.btn, disabled: state[0].phase === "loading" || !String(target[0]).trim(), onClick: run }, "运行升级预检"),
|
|
3755
|
+
state[0].phase === "loading" ? React.createElement("p", { role: "status" }, "正在捕获相同制品并比较静态风险……") : null,
|
|
3756
|
+
state[0].phase === "error" ? React.createElement("p", { role: "status", style: styles.err }, state[0].error) : null,
|
|
3757
|
+
state[0].phase === "done" ? React.createElement(UpgradeDetails, { result: state[0].result }) : null);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
function ReportPreview(p) {
|
|
3761
|
+
var shown = React.useState(false), copied = React.useState("");
|
|
3762
|
+
var text = typeof p.data.issueReport === "string" ? p.data.issueReport : null;
|
|
3763
|
+
return React.createElement("div", null,
|
|
3764
|
+
React.createElement("button", { type: "button", style: styles.btn,
|
|
3765
|
+
onClick: function () { shown[1](!shown[0]); copied[1](""); },
|
|
3766
|
+
}, shown[0] ? "关闭报告预览" : "预览故障报告"),
|
|
3767
|
+
shown[0] ? React.createElement("div", { style: { margin: "10px 0" } },
|
|
3768
|
+
React.createElement("p", { style: styles.note }, "这是本次完整报告,筛选不会删减导出内容。保留插件名与包内位置;请预览敏感信息后再分享。"),
|
|
3769
|
+
text === null ? React.createElement("div", { role: "status" }, "当前报告未提供导出文本,请刷新并确认宿主与客户端版本一致。") :
|
|
3770
|
+
React.createElement("div", null,
|
|
3771
|
+
React.createElement("textarea", { "aria-label": "故障报告预览", readOnly: true, value: text, rows: 14,
|
|
3772
|
+
style: mergeStyle(styles.mono, { width: "100%", boxSizing: "border-box", color: "inherit", background: "transparent" }),
|
|
3773
|
+
onFocus: function (event) { event.target.select(); },
|
|
3774
|
+
}),
|
|
3775
|
+
React.createElement("button", { type: "button", style: styles.btn, onClick: async function () {
|
|
3776
|
+
try {
|
|
3777
|
+
if (typeof navigator === "undefined" || !navigator.clipboard || !navigator.clipboard.writeText) throw Error("Clipboard unavailable");
|
|
3778
|
+
await navigator.clipboard.writeText(text); copied[1]("已复制。请确认敏感信息后再分享。");
|
|
3779
|
+
} catch (error) { copied[1]("无法自动复制,请选中预览文本手动复制。"); }
|
|
3780
|
+
} }, "复制报告"),
|
|
3781
|
+
React.createElement("span", { role: "status", style: styles.meta }, copied[0]))) : null);
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
function ReportMeta(p) {
|
|
3785
|
+
var d = p.data, c = d.cache;
|
|
3786
|
+
var cache = c && c.hit ? "缓存报告(约 " + Math.max(0, Math.round(c.ageMs / 1000)) + " 秒前)" : "本次重新计算的报告";
|
|
3787
|
+
if (c && c.stale) cache += ";输入已变化,报告待刷新";
|
|
3788
|
+
if (c && c.refreshing) cache += ";后台正在刷新";
|
|
3789
|
+
return React.createElement("div", { style: styles.note },
|
|
3790
|
+
React.createElement("div", null, "报告时间:" + (d.computedAt || "未提供") + " · " + cache),
|
|
3791
|
+
React.createElement("div", null, "体检对象:" + (d.nodeModules || "未提供") + " · 目标 DSH:" + (d.report.dshVersion || "未知")),
|
|
3792
|
+
React.createElement("div", { "data-catalog-status": d.report.catalog && d.report.catalog.verified ? "verified" : "uncertified", role: d.report.catalog && d.report.catalog.verified ? undefined : "alert", style: d.report.catalog && d.report.catalog.verified ? undefined : styles.err },
|
|
3793
|
+
ReportContract.catalogLines(d.report.catalog).map(function (line, i) { return React.createElement("div", { key: i }, line); })),
|
|
3794
|
+
c && c.refreshError ? React.createElement("div", { role: "status", style: styles.err }, "最近刷新失败,保留上次成功报告:" + c.refreshError) : null);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
async function saveFindingReview(body) {
|
|
3798
|
+
var response=await fetch('/plugin-compat-check/reviews',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(body)});
|
|
3799
|
+
var result=await response.json();
|
|
3800
|
+
if(!response.ok||!result.ok||result.reviews?.error)throw new Error(result.error||result.reviews?.error||'人工处置保存失败');
|
|
3801
|
+
return result.reviews;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
933
3804
|
function Table(p) {
|
|
934
3805
|
var data = p.data;
|
|
935
3806
|
var report = data && data.report;
|
|
3807
|
+
var queryState = React.useState(""), verdictState = React.useState("all"), certaintyState = React.useState("all"), pendingState = React.useState("all"), risksState = React.useState(false);
|
|
936
3808
|
if (!report) return null;
|
|
937
3809
|
var names = Object.keys(report.plugins);
|
|
938
3810
|
names.sort(function (a, b) {
|
|
@@ -940,46 +3812,79 @@ return { assertEnvelope: assertEnvelope };
|
|
|
940
3812
|
var d = rank[report.plugins[a].verdict] - rank[report.plugins[b].verdict];
|
|
941
3813
|
return d !== 0 ? d : a.localeCompare(b);
|
|
942
3814
|
});
|
|
3815
|
+
var diff = data.baseline && data.baseline.diff, comparable = Boolean(diff && diff.comparable), risks = {};
|
|
3816
|
+
if (comparable) {
|
|
3817
|
+
(diff.worsened || []).forEach(function (entry) { risks[entry.name] = true; });
|
|
3818
|
+
(diff.added || []).forEach(function (entry) { if (report.plugins[entry.name] && report.plugins[entry.name].findings.length) risks[entry.name] = true; });
|
|
3819
|
+
}
|
|
3820
|
+
var shown = names.filter(function (name) {
|
|
3821
|
+
var pl = report.plugins[name];
|
|
3822
|
+
var pending = pl.coverage.status !== "complete" || pl.vacuous || (pl.clientSide || []).length > 0;
|
|
3823
|
+
return name.toLowerCase().includes(String(queryState[0]).trim().toLowerCase())
|
|
3824
|
+
&& (verdictState[0] === "all" || pl.verdict === verdictState[0])
|
|
3825
|
+
&& (certaintyState[0] === "all" || pl.findings.some(function (f) { return f.certainty === certaintyState[0]; }))
|
|
3826
|
+
&& (pendingState[0] === "all" || (pendingState[0] === "pending" ? pending : !pending))
|
|
3827
|
+
&& (!risksState[0] || (comparable && risks[name]));
|
|
3828
|
+
});
|
|
943
3829
|
var s = report.summary || {};
|
|
3830
|
+
function select(label, state, options) {
|
|
3831
|
+
return React.createElement("select", { "aria-label": label, value: state[0], style: styles.btn,
|
|
3832
|
+
onChange: function (event) { state[1](event.target.value); },
|
|
3833
|
+
}, options.map(function (option) { return React.createElement("option", { key: option[0], value: option[0] }, option[1]); }));
|
|
3834
|
+
}
|
|
944
3835
|
return React.createElement("div", { style: styles.root },
|
|
945
3836
|
React.createElement("div", { style: styles.head },
|
|
946
3837
|
React.createElement("span", { style: styles.title }, "兼容性"),
|
|
947
3838
|
React.createElement("span", { style: styles.meta },
|
|
948
3839
|
names.length + " 个插件 · 可用 " + (s.ok || 0) + " · 部分可用 " + (s.partial || 0)
|
|
949
3840
|
+ " · 不兼容 " + (s.broken || 0)
|
|
950
|
-
+ (s.vacuousOk ? " · 其中 " + s.vacuousOk + "
|
|
951
|
-
React.createElement("button", {
|
|
952
|
-
|
|
953
|
-
onClick: function () { p.onReload(); },
|
|
954
|
-
}, "重新检查")),
|
|
3841
|
+
+ (s.vacuousOk ? " · 其中 " + s.vacuousOk + " 个「可用」没有已验证调用" : "")),
|
|
3842
|
+
React.createElement("button", { style: styles.btn, type: "button", onClick: function () { p.onReload(); } }, "重新检查")),
|
|
3843
|
+
React.createElement(ReportMeta, { data: data }),
|
|
955
3844
|
React.createElement(Badge, { baseline: data.baseline }),
|
|
3845
|
+
React.createElement("div", { style: mergeStyle(styles.head, { marginTop: "12px" }) },
|
|
3846
|
+
React.createElement("input", { "aria-label": "搜索插件", placeholder: "搜索插件名称", value: queryState[0],
|
|
3847
|
+
style: mergeStyle(styles.btn, { minWidth: "150px" }), onChange: function (event) { queryState[1](event.target.value); } }),
|
|
3848
|
+
select("按结论筛选", verdictState, [["all", "全部结论"], ["broken", "不兼容"], ["partial", "部分可用"], ["unknown", "未知"], ["ok", "可用"]]),
|
|
3849
|
+
select("按确定性筛选", certaintyState, [["all", "全部确定性"], ["certain", "含确定发现"], ["suspected", "含疑似发现"]]),
|
|
3850
|
+
select("按验证状态筛选", pendingState, [["all", "全部验证状态"], ["pending", "有待验证部分"], ["complete", "无待验证部分"]]),
|
|
3851
|
+
React.createElement("button", { type: "button", style: styles.btn, "aria-pressed": risksState[0], onClick: function () { risksState[1](!risksState[0]); } }, "本次新增风险"),
|
|
3852
|
+
React.createElement("button", { type: "button", style: styles.btn, onClick: function () { queryState[1](""); verdictState[1]("all"); certaintyState[1]("all"); pendingState[1]("all"); risksState[1](false); } }, "清除筛选")),
|
|
3853
|
+
risksState[0] ? React.createElement("div", { role: "status", style: styles.note }, comparable
|
|
3854
|
+
? "仅显示基线差分中的新增或变重风险;无变化不表示业务通过。"
|
|
3855
|
+
: "尚无可比较基线;本视图不能判断新增风险,不表示没有风险。") : null,
|
|
3856
|
+
React.createElement("div", { style: styles.meta }, "显示 " + shown.length + " / " + names.length + " 个插件;筛选仅改变呈现。"),
|
|
3857
|
+
React.createElement(ReportPreview, { data: data }),
|
|
3858
|
+
React.createElement(UpgradePreview, { data: data }),
|
|
3859
|
+
React.createElement(DependencyPanel, {graph: report.dependencyGraph}),
|
|
3860
|
+
React.createElement(FindingReviewPanel, {report:report,reviews:data.reviews,onSave:saveFindingReview}),
|
|
3861
|
+
React.createElement(FunctionalHealthPanel, {report:report}),
|
|
3862
|
+
!shown.length ? React.createElement("div", { role: "status", style: styles.note }, "没有符合筛选条件的插件。清除筛选可查看完整报告。") : null,
|
|
3863
|
+
React.createElement("div", { style: { overflowX: "auto" } },
|
|
956
3864
|
React.createElement("table", { style: styles.table },
|
|
957
3865
|
React.createElement("thead", null, React.createElement("tr", null,
|
|
958
3866
|
React.createElement("th", { style: styles.th }, "插件"),
|
|
959
3867
|
React.createElement("th", { style: styles.th }, "结论"),
|
|
960
|
-
React.createElement("th", { style: styles.th }, "
|
|
961
|
-
React.createElement("tbody", null,
|
|
962
|
-
var pl = report.plugins[n];
|
|
963
|
-
|
|
964
|
-
return React.createElement("tr", { key: n },
|
|
3868
|
+
React.createElement("th", { style: styles.th }, "发现与核实"))),
|
|
3869
|
+
React.createElement("tbody", null, shown.map(function (n) {
|
|
3870
|
+
var pl = report.plugins[n], v = VERDICT[pl.verdict] || VERDICT.unknown;
|
|
3871
|
+
return React.createElement("tr", { key: n, "data-compat-plugin": n },
|
|
965
3872
|
React.createElement("td", { style: styles.td },
|
|
966
3873
|
React.createElement("div", null, n),
|
|
967
|
-
React.createElement("div", { style: styles.mono },
|
|
968
|
-
|
|
3874
|
+
React.createElement("div", { style: styles.mono }, "@" + (pl.version || "?") + (pl.vacuous ? " · 无已验证调用" : ""))),
|
|
3875
|
+
React.createElement("td", { style: styles.td }, React.createElement("span", { style: Object.assign({}, styles.pill, { background: v.color }) }, v.text)),
|
|
969
3876
|
React.createElement("td", { style: styles.td },
|
|
970
|
-
React.createElement(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
"
|
|
981
|
-
+ " · 体检对象:" + (data.nodeModules || "?") + "(" + (data.nodeModulesWhy || "?") + ")"
|
|
982
|
-
+ (data.elapsedMs !== undefined ? " · 耗时 " + data.elapsedMs + "ms" : "")));
|
|
3877
|
+
React.createElement(Coverage, { plugin: pl }),
|
|
3878
|
+
React.createElement("details", null,
|
|
3879
|
+
React.createElement("summary", { style: { cursor: "pointer", marginTop: "6px" } }, "查看 " + pl.findings.length + " 条发现与未验证项"),
|
|
3880
|
+
React.createElement(Findings, { plugin: pl, hideCoverage: true }))));
|
|
3881
|
+
})))),
|
|
3882
|
+
React.createElement(EvidenceLegend),
|
|
3883
|
+
React.createElement("div", { style: styles.note }, "工具版本:" + (data.toolVersion || "未知") + " · " + (report.hasRuntimeSurface ? "方法级证据含宿主运行时观测;以每条发现为准。" : "方法级证据来自官方 API 契约目录,静态事实单独判定。")),
|
|
3884
|
+
React.createElement("details", null,
|
|
3885
|
+
React.createElement("summary", { style: styles.meta }, "查看采样与检查信息"),
|
|
3886
|
+
React.createElement("div", { style: styles.note }, (report.observation && report.observation.notes || []).join(";")),
|
|
3887
|
+
React.createElement("div", { style: styles.note }, "对象来源:" + (data.nodeModulesWhy || "未提供") + (data.elapsedMs !== undefined ? " · 耗时 " + data.elapsedMs + "ms" : ""))));
|
|
983
3888
|
}
|
|
984
3889
|
|
|
985
3890
|
function Panel() {
|