dsh-plugin-compat-check 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +39 -17
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/index.js +282 -53
  4. package/kernel/lib/author-ci-output.mjs +188 -0
  5. package/kernel/lib/author-ci-policy.mjs +285 -0
  6. package/kernel/lib/catalog.mjs +38 -0
  7. package/kernel/lib/contracts.mjs +88 -0
  8. package/kernel/lib/dependency-graph.mjs +650 -0
  9. package/kernel/lib/extract.mjs +14 -0
  10. package/kernel/lib/finding-guidance.mjs +72 -0
  11. package/kernel/lib/finding-review-store.mjs +245 -0
  12. package/kernel/lib/finding-review.mjs +338 -0
  13. package/kernel/lib/functional-health-contract.mjs +337 -0
  14. package/kernel/lib/issue-report.mjs +217 -0
  15. package/kernel/lib/observation.mjs +19 -9
  16. package/kernel/lib/report-identity-worker.mjs +153 -0
  17. package/kernel/lib/report-identity.mjs +15 -0
  18. package/kernel/lib/report-service.mjs +196 -11
  19. package/kernel/lib/report.mjs +76 -11
  20. package/kernel/lib/source-analysis.mjs +24 -6
  21. package/kernel/lib/upgrade-capture-worker.mjs +20 -0
  22. package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
  23. package/kernel/lib/upgrade-contract.mjs +471 -0
  24. package/kernel/lib/upgrade-input.mjs +309 -0
  25. package/kernel/lib/upgrade-preview.mjs +256 -0
  26. package/kernel/lib/upgrade-worker.mjs +50 -0
  27. package/kernel/lib/version.mjs +1 -1
  28. package/kernel/lib/worker-analysis.mjs +16 -1
  29. package/kernel/lib/workflow-panels.mjs +706 -0
  30. package/kernel/rules/README.md +5 -5
  31. package/lib/client.js +2974 -69
  32. package/lib/dependency-graph.js +651 -0
  33. package/lib/finding-guidance.js +73 -0
  34. package/lib/finding-review.js +339 -0
  35. package/lib/functional-health-contract.js +338 -0
  36. package/lib/report-contract.js +88 -0
  37. package/lib/upgrade-contract.js +472 -0
  38. package/lib/workflow-panels.js +707 -0
  39. package/package.json +10 -5
  40. package/scripts/prepublish-check.mjs +28 -13
  41. package/scripts/stage-contract.mjs +91 -29
  42. package/scripts/stage-kernel.mjs +3 -2
  43. package/skill/SKILL.md +90 -39
@@ -0,0 +1,651 @@
1
+ // Generated from lib/dependency-graph.mjs; run node plugin/scripts/stage-contract.mjs
2
+ /** Browser-safe explanatory graph. Declarations never establish activation or failure. */
3
+ const GRAPH_KIND = "plugin-compat-check/dependency-graph";
4
+ const GRAPH_LIMITS = Object.freeze({
5
+ nodes: 1000,
6
+ edges: 4000,
7
+ traversal: 20000,
8
+ items: 20000,
9
+ });
10
+ const graphRecord = (value) =>
11
+ value !== null && typeof value === "object" && !Array.isArray(value);
12
+ const graphText = (value) => typeof value === "string" && value.length > 0;
13
+ const graphArray = (value) => (Array.isArray(value) ? value : []);
14
+ const graphSides = (value) => {
15
+ const sides = graphArray(value).filter(
16
+ (side) => side === "host" || side === "client",
17
+ );
18
+ return sides.length ? [...new Set(sides)] : ["unknown"];
19
+ };
20
+ const graphId = (...parts) => JSON.stringify(parts);
21
+ const graphSource = (value) => ({
22
+ file: typeof value?.file === "string" ? value.file : null,
23
+ line: Number.isSafeInteger(value?.line) ? value.line : null,
24
+ start: Number.isSafeInteger(value?.start) ? value.start : null,
25
+ end: Number.isSafeInteger(value?.end) ? value.end : null,
26
+ });
27
+ const graphRequirement = (kind) =>
28
+ kind === "required-inject"
29
+ ? "required"
30
+ : kind === "scoped-inject"
31
+ ? "subcontext"
32
+ : ["optional-read", "read"].includes(kind)
33
+ ? "optional"
34
+ : "unknown";
35
+
36
+ export function buildDependencyGraph({
37
+ extraction,
38
+ verdict,
39
+ entries,
40
+ limits = {},
41
+ } = {}) {
42
+ const report = extraction?.report || extraction || {};
43
+ const budget = Object.fromEntries(
44
+ Object.entries(GRAPH_LIMITS).map(([key, max]) => [
45
+ key,
46
+ Number.isSafeInteger(limits[key]) && limits[key] >= 0
47
+ ? Math.min(limits[key], max)
48
+ : max,
49
+ ]),
50
+ );
51
+ const nodes = [],
52
+ edges = [],
53
+ impacts = [],
54
+ cycles = [],
55
+ reasons = new Set();
56
+ const nodeMap = new Map(),
57
+ edgeKeys = new Set(),
58
+ plugins = new Map();
59
+ let truncated = false,
60
+ items = 0,
61
+ steps = 0,
62
+ pathItems = 0;
63
+ const limit = (reason) => {
64
+ truncated = true;
65
+ reasons.add(reason);
66
+ };
67
+ const consume = () => {
68
+ if (items >= budget.items) {
69
+ limit("输入条目达到预算,关系及来源未完整读取");
70
+ return false;
71
+ }
72
+ items++;
73
+ return true;
74
+ };
75
+ const addNode = (node) => {
76
+ if (nodeMap.has(node.id)) return nodeMap.get(node.id);
77
+ if (nodes.length >= budget.nodes) {
78
+ limit("节点达到预算,关系图已截断");
79
+ return null;
80
+ }
81
+ nodes.push(node);
82
+ nodeMap.set(node.id, node);
83
+ return node;
84
+ };
85
+ const addEdge = (edge) => {
86
+ if (!nodeMap.has(edge.from) || !nodeMap.has(edge.to)) return;
87
+ const key = JSON.stringify(edge);
88
+ if (edgeKeys.has(key)) return;
89
+ if (edges.length >= budget.edges) {
90
+ limit("关系达到预算,关系图已截断");
91
+ return;
92
+ }
93
+ edgeKeys.add(key);
94
+ edges.push({ id: `edge:${edges.length}`, ...edge });
95
+ };
96
+ const service = (name, side) =>
97
+ addNode({
98
+ id: graphId("service", side, name),
99
+ kind: "service",
100
+ name,
101
+ side,
102
+ state: "unknown",
103
+ });
104
+ const entryMap = new Map();
105
+ const rows = Array.isArray(entries)
106
+ ? entries
107
+ : graphArray(verdict?.observation?.entries?.plugins);
108
+ for (const row of rows) {
109
+ if (!consume()) break;
110
+ if (!graphRecord(row) || !graphText(row.packageName) || row.group) continue;
111
+ if (!entryMap.has(row.packageName)) entryMap.set(row.packageName, []);
112
+ entryMap.get(row.packageName).push({
113
+ id: typeof row.id === "string" ? row.id : null,
114
+ state: typeof row.state === "string" ? row.state : "unknown",
115
+ disabled: row.disabled === true,
116
+ });
117
+ }
118
+ if (report.scanComplete !== true)
119
+ reasons.add("静态提取覆盖不完整或未声明,关系可能缺失");
120
+ for (const [name, p] of Object.entries(report.perPlugin || {})) {
121
+ if (!consume()) break;
122
+ if (!graphRecord(p)) continue;
123
+ const entryRows = entryMap.get(name) || [];
124
+ const enabled = entryRows.filter((row) => !row.disabled);
125
+ const states = [...new Set(enabled.map((row) => row.state))];
126
+ const loaderState =
127
+ entryRows.length && !enabled.length
128
+ ? "disabled"
129
+ : states.length === 1
130
+ ? states[0]
131
+ : states.length > 1
132
+ ? "mixed"
133
+ : "unknown";
134
+ if (loaderState === "unknown")
135
+ reasons.add("部分插件未提供可映射 loader 条目,状态为未知");
136
+ const node = addNode({
137
+ id: graphId("plugin", name),
138
+ kind: "plugin",
139
+ name,
140
+ entries: entryRows,
141
+ loaderState,
142
+ subContexts: [],
143
+ });
144
+ if (!node) break;
145
+ plugins.set(name, { node, plugin: p });
146
+ if (graphArray(p.diagnostics).length)
147
+ reasons.add("部分源码存在提取诊断,关系可能缺失");
148
+ for (const context of graphArray(p.subContexts)) {
149
+ if (!consume()) break;
150
+ if (!graphRecord(context)) continue;
151
+ const names = [];
152
+ for (const name of graphArray(context.names)) {
153
+ if (!consume()) break;
154
+ if (graphText(name)) names.push(name);
155
+ }
156
+ node.subContexts.push({
157
+ file: typeof context.file === "string" ? context.file : null,
158
+ names,
159
+ });
160
+ }
161
+ }
162
+ for (const { node, plugin: p } of plugins.values()) {
163
+ const declaredRelations = new Set();
164
+ for (const dependency of graphArray(p.dependencies)) {
165
+ if (!consume()) break;
166
+ if (!graphText(dependency?.service)) continue;
167
+ const sides = graphSides(dependency.sides);
168
+ if (sides.includes("unknown"))
169
+ reasons.add("部分依赖端归属未知,不用于传播推导");
170
+ if (graphRequirement(dependency.kind) === "unknown")
171
+ reasons.add("部分依赖必需性未证实,保留未知标记");
172
+ for (const side of sides) {
173
+ declaredRelations.add(graphId(dependency.service, side));
174
+ const target = service(dependency.service, side);
175
+ if (target)
176
+ addEdge({
177
+ kind: "dependency",
178
+ from: node.id,
179
+ to: target.id,
180
+ service: dependency.service,
181
+ side,
182
+ requirement: graphRequirement(dependency.kind),
183
+ declarationKind:
184
+ typeof dependency.kind === "string" ? dependency.kind : "unknown",
185
+ source: graphSource(dependency),
186
+ potential: true,
187
+ });
188
+ }
189
+ }
190
+ for (const site of graphArray(p.callSites)) {
191
+ if (!consume()) break;
192
+ if (!graphText(site?.service)) continue;
193
+ for (const side of graphSides(site.sides)) {
194
+ const key = graphId(site.service, side);
195
+ if (declaredRelations.has(key)) continue;
196
+ declaredRelations.add(key);
197
+ reasons.add("部分服务只见调用点,依赖必需性未知");
198
+ const target = service(site.service, side);
199
+ if (target)
200
+ addEdge({
201
+ kind: "dependency",
202
+ from: node.id,
203
+ to: target.id,
204
+ service: site.service,
205
+ side,
206
+ requirement: "unknown",
207
+ declarationKind: "call",
208
+ source: graphSource(site),
209
+ potential: true,
210
+ });
211
+ }
212
+ }
213
+ const provided = new Set();
214
+ for (const site of graphArray(p.provideSites)) {
215
+ if (!consume()) break;
216
+ if (!graphText(site?.service)) continue;
217
+ provided.add(site.service);
218
+ for (const side of graphSides(site.sides)) {
219
+ if (side === "unknown")
220
+ reasons.add("部分提供声明缺少端归属,只列候选,不用于传播推导");
221
+ const target = service(site.service, side);
222
+ if (target)
223
+ addEdge({
224
+ kind: "candidate-provider",
225
+ from: node.id,
226
+ to: target.id,
227
+ service: site.service,
228
+ side,
229
+ source: graphSource(site),
230
+ sideMatch: side === "unknown" ? "unverified" : "source",
231
+ potential: true,
232
+ });
233
+ }
234
+ }
235
+ for (const name of graphArray(p.providedServices)) {
236
+ if (!consume()) break;
237
+ if (!graphText(name) || provided.has(name)) continue;
238
+ reasons.add("部分提供声明缺少端归属,只列候选,不用于传播推导");
239
+ const target = service(name, "unknown");
240
+ if (target)
241
+ addEdge({
242
+ kind: "candidate-provider",
243
+ from: node.id,
244
+ to: target.id,
245
+ service: name,
246
+ side: "unknown",
247
+ source: graphSource(null),
248
+ sideMatch: "unverified",
249
+ potential: true,
250
+ });
251
+ }
252
+ if (
253
+ p.clientInject !== undefined &&
254
+ p.clientInject !== null &&
255
+ !Array.isArray(p.clientInject)
256
+ )
257
+ reasons.add("客户端包 inject 声明格式未识别,未猜测服务关系");
258
+ for (const name of graphArray(p.clientInject)) {
259
+ if (!consume()) break;
260
+ if (!graphText(name)) {
261
+ reasons.add("客户端包 inject 含未识别条目");
262
+ continue;
263
+ }
264
+ const target = addNode({
265
+ id: graphId("client-package", name),
266
+ kind: "client-package",
267
+ name,
268
+ side: "client",
269
+ });
270
+ if (target)
271
+ addEdge({
272
+ kind: "client-package-inject",
273
+ from: node.id,
274
+ to: target.id,
275
+ side: "client",
276
+ potential: true,
277
+ });
278
+ }
279
+ }
280
+ const provisions = new Map(),
281
+ consumers = new Map(),
282
+ cycleKeys = new Set();
283
+ for (const edge of edges) {
284
+ if (edge.side === "unknown") continue;
285
+ const map =
286
+ edge.kind === "candidate-provider"
287
+ ? provisions
288
+ : edge.kind === "dependency"
289
+ ? consumers
290
+ : null;
291
+ const key = edge.kind === "candidate-provider" ? edge.from : edge.to;
292
+ if (map) {
293
+ if (!map.has(key)) map.set(key, []);
294
+ map.get(key).push(edge);
295
+ }
296
+ }
297
+ const step = () => {
298
+ if (steps >= budget.traversal) {
299
+ limit("传播遍历达到预算,潜在影响及环未完整枚举");
300
+ return false;
301
+ }
302
+ steps++;
303
+ return true;
304
+ };
305
+ for (const { node } of plugins.values()) {
306
+ const affected = [],
307
+ queue = [
308
+ {
309
+ plugin: node.id,
310
+ side: null,
311
+ path: [],
312
+ requirements: [],
313
+ plugins: [node.id],
314
+ },
315
+ ],
316
+ seen = new Set();
317
+ for (let head = 0; head < queue.length; head++) {
318
+ if (!step()) break;
319
+ const current = queue[head];
320
+ for (const provision of provisions.get(current.plugin) || []) {
321
+ if (!step()) break;
322
+ if (current.side && provision.side !== current.side) continue;
323
+ for (const dependency of consumers.get(provision.to) || []) {
324
+ if (!step()) break;
325
+ const path = [...current.path, provision.id, dependency.id];
326
+ const requirement = [...current.requirements, dependency.requirement];
327
+ const pluginPath = [...current.plugins, dependency.from];
328
+ if (pathItems + path.length > budget.traversal) {
329
+ limit("关系路径达到预算,潜在影响及环未完整列出");
330
+ continue;
331
+ }
332
+ if (dependency.from === node.id) {
333
+ const ring = pluginPath.slice(0, -1);
334
+ const key = JSON.stringify([
335
+ provision.side,
336
+ [
337
+ ...ring.map((_, i) =>
338
+ JSON.stringify([...ring.slice(i), ...ring.slice(0, i)]),
339
+ ),
340
+ ].sort()[0],
341
+ ]);
342
+ if (!cycleKeys.has(key)) {
343
+ pathItems += path.length;
344
+ cycleKeys.add(key);
345
+ cycles.push({
346
+ plugins: pluginPath,
347
+ side: provision.side,
348
+ path,
349
+ potential: true,
350
+ });
351
+ }
352
+ continue;
353
+ }
354
+ const key = graphId(dependency.from, provision.side);
355
+ if (seen.has(key)) continue;
356
+ seen.add(key);
357
+ pathItems += path.length;
358
+ affected.push({
359
+ plugin: dependency.from,
360
+ side: provision.side,
361
+ path,
362
+ requirements: requirement,
363
+ potential: true,
364
+ });
365
+ queue.push({
366
+ plugin: dependency.from,
367
+ side: provision.side,
368
+ path,
369
+ requirements: requirement,
370
+ plugins: pluginPath,
371
+ });
372
+ }
373
+ }
374
+ }
375
+ impacts.push({ plugin: node.id, potential: true, affected });
376
+ }
377
+ return {
378
+ schemaVersion: 1,
379
+ kind: GRAPH_KIND,
380
+ nodes,
381
+ edges,
382
+ impacts,
383
+ cycles,
384
+ coverage: {
385
+ status: reasons.size ? "limited" : "complete",
386
+ truncated,
387
+ reasons: [...reasons],
388
+ limits: budget,
389
+ },
390
+ notes: [
391
+ "提供声明仅为候选线索;loader 条目状态不证明服务已注册。",
392
+ "多个候选不表示实际选择了其中之一;替代提供者、禁用条目及可选依赖均需在真实宿主复核。",
393
+ "受影响者及环仅表示同端声明关系的潜在影响,不表示实际挂起或失败;可选及子作用域依赖不等于根插件阻断。",
394
+ "host/client 独立;client 包 inject 不当作 host 服务依赖;端未知的声明不用于传播推导。",
395
+ ],
396
+ };
397
+ }
398
+
399
+ /** Reject malformed optional views before rendering without changing the core report. */
400
+ export function validateDependencyGraph(graph) {
401
+ if (
402
+ !graphRecord(graph) ||
403
+ graph.kind !== GRAPH_KIND ||
404
+ graph.schemaVersion !== 1
405
+ )
406
+ return false;
407
+ const coverage = graph.coverage;
408
+ if (
409
+ !graphRecord(coverage) ||
410
+ !["limited", "complete"].includes(coverage.status) ||
411
+ typeof coverage.truncated !== "boolean" ||
412
+ !Array.isArray(coverage.reasons) ||
413
+ coverage.reasons.length > 100 ||
414
+ !coverage.reasons.every(graphText) ||
415
+ !graphRecord(coverage.limits)
416
+ )
417
+ return false;
418
+ if (
419
+ (coverage.truncated && coverage.status !== "limited") ||
420
+ (coverage.status === "complete" && coverage.reasons.length) ||
421
+ (coverage.status === "limited" && !coverage.reasons.length)
422
+ )
423
+ return false;
424
+ for (const [key, max] of Object.entries(GRAPH_LIMITS))
425
+ if (
426
+ !Number.isSafeInteger(coverage.limits[key]) ||
427
+ coverage.limits[key] < 0 ||
428
+ coverage.limits[key] > max
429
+ )
430
+ return false;
431
+ if (
432
+ !Array.isArray(graph.nodes) ||
433
+ graph.nodes.length > coverage.limits.nodes ||
434
+ !Array.isArray(graph.edges) ||
435
+ graph.edges.length > coverage.limits.edges ||
436
+ !Array.isArray(graph.impacts) ||
437
+ !Array.isArray(graph.cycles) ||
438
+ !Array.isArray(graph.notes) ||
439
+ graph.notes.length > 100 ||
440
+ !graph.notes.every(graphText)
441
+ )
442
+ return false;
443
+ const nodes = new Map(),
444
+ edges = new Map();
445
+ const validSide = (side) => ["host", "client", "unknown"].includes(side);
446
+ const nonnegative = (value) =>
447
+ value === null || (Number.isSafeInteger(value) && value >= 0);
448
+ const source = (value) =>
449
+ graphRecord(value) &&
450
+ (value.file === null || typeof value.file === "string") &&
451
+ nonnegative(value.line) &&
452
+ nonnegative(value.start) &&
453
+ nonnegative(value.end) &&
454
+ (value.start === null || value.end === null || value.end >= value.start);
455
+ let nestedItems = 0,
456
+ pathItems = 0;
457
+ for (const node of graph.nodes) {
458
+ if (
459
+ !graphRecord(node) ||
460
+ !graphText(node.id) ||
461
+ nodes.has(node.id) ||
462
+ !graphText(node.name) ||
463
+ !["plugin", "service", "client-package"].includes(node.kind)
464
+ )
465
+ return false;
466
+ if (node.kind === "plugin") {
467
+ if (
468
+ !graphText(node.loaderState) ||
469
+ !Array.isArray(node.entries) ||
470
+ !Array.isArray(node.subContexts)
471
+ )
472
+ return false;
473
+ nestedItems += node.entries.length + node.subContexts.length;
474
+ if (nestedItems > coverage.limits.items) return false;
475
+ if (
476
+ !node.entries.every(
477
+ (row) =>
478
+ graphRecord(row) &&
479
+ (row.id === null || typeof row.id === "string") &&
480
+ graphText(row.state) &&
481
+ typeof row.disabled === "boolean",
482
+ )
483
+ )
484
+ return false;
485
+ for (const context of node.subContexts) {
486
+ if (
487
+ !graphRecord(context) ||
488
+ (context.file !== null && typeof context.file !== "string") ||
489
+ !Array.isArray(context.names)
490
+ )
491
+ return false;
492
+ nestedItems += context.names.length;
493
+ if (
494
+ nestedItems > coverage.limits.items ||
495
+ !context.names.every(graphText)
496
+ )
497
+ return false;
498
+ }
499
+ } else if (
500
+ !validSide(node.side) ||
501
+ (node.kind === "service" && node.state !== "unknown") ||
502
+ (node.kind === "client-package" && node.side !== "client")
503
+ )
504
+ return false;
505
+ nodes.set(node.id, node);
506
+ }
507
+ for (const edge of graph.edges) {
508
+ if (
509
+ !graphRecord(edge) ||
510
+ !graphText(edge.id) ||
511
+ edges.has(edge.id) ||
512
+ !nodes.has(edge.from) ||
513
+ !nodes.has(edge.to) ||
514
+ nodes.get(edge.from).kind !== "plugin" ||
515
+ !validSide(edge.side) ||
516
+ edge.potential !== true
517
+ )
518
+ return false;
519
+ if (edge.kind === "client-package-inject") {
520
+ if (
521
+ nodes.get(edge.to).kind !== "client-package" ||
522
+ edge.side !== "client"
523
+ )
524
+ return false;
525
+ } else if (["dependency", "candidate-provider"].includes(edge.kind)) {
526
+ if (
527
+ nodes.get(edge.to).kind !== "service" ||
528
+ !graphText(edge.service) ||
529
+ nodes.get(edge.to).name !== edge.service ||
530
+ nodes.get(edge.to).side !== edge.side ||
531
+ !source(edge.source)
532
+ )
533
+ return false;
534
+ if (
535
+ edge.kind === "dependency" &&
536
+ (!["required", "optional", "subcontext", "unknown"].includes(
537
+ edge.requirement,
538
+ ) ||
539
+ !graphText(edge.declarationKind) ||
540
+ edge.requirement !== graphRequirement(edge.declarationKind))
541
+ )
542
+ return false;
543
+ if (
544
+ edge.kind === "candidate-provider" &&
545
+ edge.sideMatch !== (edge.side === "unknown" ? "unverified" : "source")
546
+ )
547
+ return false;
548
+ } else return false;
549
+ edges.set(edge.id, edge);
550
+ }
551
+ const validPath = (path, from, to, side, requirements, pluginPath) => {
552
+ if (!Array.isArray(path) || path.length < 2 || path.length % 2)
553
+ return false;
554
+ pathItems += path.length;
555
+ if (pathItems > coverage.limits.traversal) return false;
556
+ if (
557
+ requirements &&
558
+ (!Array.isArray(requirements) || requirements.length * 2 !== path.length)
559
+ )
560
+ return false;
561
+ if (
562
+ pluginPath &&
563
+ (!Array.isArray(pluginPath) ||
564
+ pluginPath.length !== path.length / 2 + 1 ||
565
+ pluginPath[0] !== from)
566
+ )
567
+ return false;
568
+ let current = from;
569
+ for (let i = 0; i < path.length; i += 2) {
570
+ const provider = edges.get(path[i]),
571
+ consumer = edges.get(path[i + 1]);
572
+ if (
573
+ provider?.kind !== "candidate-provider" ||
574
+ consumer?.kind !== "dependency" ||
575
+ provider.side !== side ||
576
+ consumer.side !== side ||
577
+ provider.from !== current ||
578
+ provider.to !== consumer.to
579
+ )
580
+ return false;
581
+ if (requirements && requirements[i / 2] !== consumer.requirement)
582
+ return false;
583
+ current = consumer.from;
584
+ if (pluginPath && pluginPath[i / 2 + 1] !== current) return false;
585
+ }
586
+ return current === to;
587
+ };
588
+ if (
589
+ graph.impacts.length > graph.nodes.length ||
590
+ graph.cycles.length > coverage.limits.traversal
591
+ )
592
+ return false;
593
+ const roots = new Set();
594
+ let relationships = 0;
595
+ for (const impact of graph.impacts) {
596
+ if (
597
+ !graphRecord(impact) ||
598
+ nodes.get(impact.plugin)?.kind !== "plugin" ||
599
+ roots.has(impact.plugin) ||
600
+ impact.potential !== true ||
601
+ !Array.isArray(impact.affected) ||
602
+ impact.affected.length > coverage.limits.traversal
603
+ )
604
+ return false;
605
+ roots.add(impact.plugin);
606
+ const affected = new Set();
607
+ for (const row of impact.affected) {
608
+ if (
609
+ ++relationships > coverage.limits.traversal ||
610
+ !graphRecord(row) ||
611
+ nodes.get(row.plugin)?.kind !== "plugin" ||
612
+ row.plugin === impact.plugin ||
613
+ row.potential !== true ||
614
+ !["host", "client"].includes(row.side) ||
615
+ !Array.isArray(row.requirements) ||
616
+ !validPath(
617
+ row.path,
618
+ impact.plugin,
619
+ row.plugin,
620
+ row.side,
621
+ row.requirements,
622
+ )
623
+ )
624
+ return false;
625
+ const key = graphId(row.plugin, row.side);
626
+ if (affected.has(key)) return false;
627
+ affected.add(key);
628
+ }
629
+ }
630
+ for (const cycle of graph.cycles) {
631
+ if (
632
+ !graphRecord(cycle) ||
633
+ cycle.potential !== true ||
634
+ !["host", "client"].includes(cycle.side) ||
635
+ !Array.isArray(cycle.plugins) ||
636
+ cycle.plugins.length < 2 ||
637
+ cycle.plugins[0] !== cycle.plugins.at(-1) ||
638
+ !cycle.plugins.every((id) => nodes.get(id)?.kind === "plugin") ||
639
+ !validPath(
640
+ cycle.path,
641
+ cycle.plugins[0],
642
+ cycle.plugins[0],
643
+ cycle.side,
644
+ null,
645
+ cycle.plugins,
646
+ )
647
+ )
648
+ return false;
649
+ }
650
+ return true;
651
+ }