@ruvector/edge-net 0.5.0 → 0.5.3

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.
@@ -0,0 +1,792 @@
1
+ /**
2
+ * @ruvector/edge-net Adapter Security
3
+ *
4
+ * Security for MicroLoRA adapters:
5
+ * - Quarantine before activation
6
+ * - Local evaluation gating
7
+ * - Base model matching
8
+ * - Signature verification
9
+ * - Merge lineage tracking
10
+ *
11
+ * Invariant: Adapters never applied without full verification.
12
+ *
13
+ * @module @ruvector/edge-net/models/adapter-security
14
+ */
15
+
16
+ import { createHash } from 'crypto';
17
+ import { canonicalize, hashCanonical, TrustRoot, ManifestVerifier } from './integrity.js';
18
+
19
+ // ============================================================================
20
+ // ADAPTER VERIFICATION
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Adapter verification rules
25
+ */
26
+ export const ADAPTER_REQUIREMENTS = Object.freeze({
27
+ // Base model must match exactly
28
+ requireExactBaseMatch: true,
29
+
30
+ // Checksum must match manifest
31
+ requireChecksumMatch: true,
32
+
33
+ // Signature must be verified
34
+ requireSignature: true,
35
+
36
+ // Must pass local evaluation OR have trusted quality proof
37
+ requireQualityGate: true,
38
+
39
+ // Minimum evaluation score to pass gate (0-1)
40
+ minEvaluationScore: 0.7,
41
+
42
+ // Maximum adapter size relative to base model
43
+ maxAdapterSizeRatio: 0.1, // 10% of base model
44
+
45
+ // Trusted quality proof publishers
46
+ trustedQualityProvers: ['ruvector-eval-2024', 'community-eval-2024'],
47
+ });
48
+
49
+ /**
50
+ * Adapter manifest structure
51
+ */
52
+ export function createAdapterManifest(adapter) {
53
+ return {
54
+ schemaVersion: '2.0.0',
55
+ adapter: {
56
+ id: adapter.id,
57
+ name: adapter.name,
58
+ version: adapter.version,
59
+ baseModelId: adapter.baseModelId,
60
+ baseModelVersion: adapter.baseModelVersion,
61
+ rank: adapter.rank,
62
+ alpha: adapter.alpha,
63
+ targetModules: adapter.targetModules || ['q_proj', 'v_proj'],
64
+ },
65
+ artifacts: [{
66
+ path: adapter.path,
67
+ size: adapter.size,
68
+ sha256: adapter.sha256,
69
+ format: 'safetensors',
70
+ }],
71
+ quality: {
72
+ evaluationScore: adapter.evaluationScore,
73
+ evaluationDataset: adapter.evaluationDataset,
74
+ evaluationProof: adapter.evaluationProof,
75
+ domain: adapter.domain,
76
+ capabilities: adapter.capabilities,
77
+ },
78
+ lineage: adapter.lineage || null,
79
+ provenance: {
80
+ creator: adapter.creator,
81
+ createdAt: adapter.createdAt || new Date().toISOString(),
82
+ trainedOn: adapter.trainedOn,
83
+ trainingConfig: adapter.trainingConfig,
84
+ },
85
+ integrity: {
86
+ manifestHash: null, // Computed
87
+ signatures: [],
88
+ },
89
+ };
90
+ }
91
+
92
+ // ============================================================================
93
+ // QUARANTINE SYSTEM
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Quarantine states for adapters
98
+ */
99
+ export const QuarantineState = Object.freeze({
100
+ PENDING: 'pending',
101
+ EVALUATING: 'evaluating',
102
+ PASSED: 'passed',
103
+ FAILED: 'failed',
104
+ TRUSTED: 'trusted', // Has trusted quality proof
105
+ });
106
+
107
+ /**
108
+ * Quarantine manager for adapter verification
109
+ */
110
+ export class AdapterQuarantine {
111
+ constructor(options = {}) {
112
+ this.trustRoot = options.trustRoot || new TrustRoot();
113
+ this.requirements = { ...ADAPTER_REQUIREMENTS, ...options.requirements };
114
+
115
+ // Quarantined adapters awaiting evaluation
116
+ this.quarantine = new Map();
117
+
118
+ // Approved adapters
119
+ this.approved = new Map();
120
+
121
+ // Failed adapters (blocked)
122
+ this.blocked = new Map();
123
+
124
+ // Evaluation test sets by domain
125
+ this.testSets = new Map();
126
+ }
127
+
128
+ /**
129
+ * Register a test set for a domain
130
+ */
131
+ registerTestSet(domain, testCases) {
132
+ this.testSets.set(domain, testCases);
133
+ }
134
+
135
+ /**
136
+ * Quarantine an adapter for evaluation
137
+ */
138
+ async quarantineAdapter(manifest, adapterData) {
139
+ const adapterId = manifest.adapter.id;
140
+
141
+ // 1. Verify checksum
142
+ const actualHash = createHash('sha256')
143
+ .update(Buffer.from(adapterData))
144
+ .digest('hex');
145
+
146
+ if (actualHash !== manifest.artifacts[0].sha256) {
147
+ const failure = {
148
+ adapterId,
149
+ reason: 'checksum_mismatch',
150
+ expected: manifest.artifacts[0].sha256,
151
+ actual: actualHash,
152
+ timestamp: Date.now(),
153
+ };
154
+ this.blocked.set(adapterId, failure);
155
+ return { state: QuarantineState.FAILED, failure };
156
+ }
157
+
158
+ // 2. Verify signature if required
159
+ if (this.requirements.requireSignature) {
160
+ const sigResult = this._verifySignatures(manifest);
161
+ if (!sigResult.valid) {
162
+ const failure = {
163
+ adapterId,
164
+ reason: 'invalid_signature',
165
+ details: sigResult.errors,
166
+ timestamp: Date.now(),
167
+ };
168
+ this.blocked.set(adapterId, failure);
169
+ return { state: QuarantineState.FAILED, failure };
170
+ }
171
+ }
172
+
173
+ // 3. Check for trusted quality proof
174
+ if (manifest.quality?.evaluationProof) {
175
+ const proofValid = await this._verifyQualityProof(manifest);
176
+ if (proofValid) {
177
+ this.approved.set(adapterId, {
178
+ manifest,
179
+ state: QuarantineState.TRUSTED,
180
+ approvedAt: Date.now(),
181
+ });
182
+ return { state: QuarantineState.TRUSTED };
183
+ }
184
+ }
185
+
186
+ // 4. Add to quarantine for local evaluation
187
+ this.quarantine.set(adapterId, {
188
+ manifest,
189
+ adapterData,
190
+ state: QuarantineState.PENDING,
191
+ quarantinedAt: Date.now(),
192
+ });
193
+
194
+ return { state: QuarantineState.PENDING };
195
+ }
196
+
197
+ /**
198
+ * Evaluate a quarantined adapter locally
199
+ */
200
+ async evaluateAdapter(adapterId, inferenceSession) {
201
+ const quarantined = this.quarantine.get(adapterId);
202
+ if (!quarantined) {
203
+ throw new Error(`Adapter ${adapterId} not in quarantine`);
204
+ }
205
+
206
+ quarantined.state = QuarantineState.EVALUATING;
207
+
208
+ const manifest = quarantined.manifest;
209
+ const domain = manifest.quality?.domain || 'general';
210
+
211
+ // Get test set for domain
212
+ const testSet = this.testSets.get(domain) || this._getDefaultTestSet();
213
+
214
+ if (testSet.length === 0) {
215
+ throw new Error(`No test set available for domain: ${domain}`);
216
+ }
217
+
218
+ // Run evaluation
219
+ const results = await this._runEvaluation(
220
+ quarantined.adapterData,
221
+ testSet,
222
+ inferenceSession,
223
+ manifest.adapter.baseModelId
224
+ );
225
+
226
+ // Check if passed
227
+ const passed = results.score >= this.requirements.minEvaluationScore;
228
+
229
+ if (passed) {
230
+ this.quarantine.delete(adapterId);
231
+ this.approved.set(adapterId, {
232
+ manifest,
233
+ state: QuarantineState.PASSED,
234
+ evaluationResults: results,
235
+ approvedAt: Date.now(),
236
+ });
237
+ return { state: QuarantineState.PASSED, results };
238
+ } else {
239
+ this.quarantine.delete(adapterId);
240
+ this.blocked.set(adapterId, {
241
+ adapterId,
242
+ reason: 'evaluation_failed',
243
+ score: results.score,
244
+ required: this.requirements.minEvaluationScore,
245
+ timestamp: Date.now(),
246
+ });
247
+ return { state: QuarantineState.FAILED, results };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Check if an adapter can be used
253
+ */
254
+ canUseAdapter(adapterId, baseModelId) {
255
+ const approved = this.approved.get(adapterId);
256
+ if (!approved) {
257
+ return { allowed: false, reason: 'not_approved' };
258
+ }
259
+
260
+ // Verify base model match
261
+ if (this.requirements.requireExactBaseMatch) {
262
+ const expectedBase = approved.manifest.adapter.baseModelId;
263
+ if (expectedBase !== baseModelId) {
264
+ return {
265
+ allowed: false,
266
+ reason: 'base_model_mismatch',
267
+ expected: expectedBase,
268
+ actual: baseModelId,
269
+ };
270
+ }
271
+ }
272
+
273
+ return { allowed: true, state: approved.state };
274
+ }
275
+
276
+ /**
277
+ * Get approved adapter data
278
+ */
279
+ getApprovedAdapter(adapterId) {
280
+ return this.approved.get(adapterId) || null;
281
+ }
282
+
283
+ /**
284
+ * Verify signatures on adapter manifest
285
+ */
286
+ _verifySignatures(manifest) {
287
+ if (!manifest.integrity?.signatures?.length) {
288
+ return { valid: false, errors: ['No signatures present'] };
289
+ }
290
+
291
+ return this.trustRoot.verifySignatureThreshold(
292
+ manifest.integrity.signatures,
293
+ 1 // At least one valid signature for adapters
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Verify a trusted quality proof
299
+ */
300
+ async _verifyQualityProof(manifest) {
301
+ const proof = manifest.quality.evaluationProof;
302
+ if (!proof) return false;
303
+
304
+ // Check if prover is trusted
305
+ if (!this.requirements.trustedQualityProvers.includes(proof.proverId)) {
306
+ return false;
307
+ }
308
+
309
+ // Verify proof signature
310
+ const proofPayload = {
311
+ adapterId: manifest.adapter.id,
312
+ evaluationScore: manifest.quality.evaluationScore,
313
+ evaluationDataset: manifest.quality.evaluationDataset,
314
+ timestamp: proof.timestamp,
315
+ };
316
+
317
+ // In production, verify actual signature here
318
+ return proof.signature && proof.proverId;
319
+ }
320
+
321
+ /**
322
+ * Run local evaluation on adapter
323
+ */
324
+ async _runEvaluation(adapterData, testSet, inferenceSession, baseModelId) {
325
+ const results = {
326
+ total: testSet.length,
327
+ passed: 0,
328
+ failed: 0,
329
+ errors: 0,
330
+ details: [],
331
+ };
332
+
333
+ for (const testCase of testSet) {
334
+ try {
335
+ // Apply adapter temporarily
336
+ await inferenceSession.loadAdapter(adapterData, { temporary: true });
337
+
338
+ // Run inference
339
+ const output = await inferenceSession.generate(testCase.input, {
340
+ maxTokens: testCase.maxTokens || 64,
341
+ });
342
+
343
+ // Check against expected
344
+ const passed = this._checkOutput(output, testCase.expected, testCase.criteria);
345
+
346
+ results.details.push({
347
+ input: testCase.input.slice(0, 50),
348
+ passed,
349
+ });
350
+
351
+ if (passed) {
352
+ results.passed++;
353
+ } else {
354
+ results.failed++;
355
+ }
356
+
357
+ // Unload temporary adapter
358
+ await inferenceSession.unloadAdapter();
359
+ } catch (error) {
360
+ results.errors++;
361
+ results.details.push({
362
+ input: testCase.input.slice(0, 50),
363
+ error: error.message,
364
+ });
365
+ }
366
+ }
367
+
368
+ results.score = results.passed / results.total;
369
+ return results;
370
+ }
371
+
372
+ /**
373
+ * Check if output matches expected criteria
374
+ */
375
+ _checkOutput(output, expected, criteria = 'contains') {
376
+ const outputLower = output.toLowerCase();
377
+ const expectedLower = expected.toLowerCase();
378
+
379
+ switch (criteria) {
380
+ case 'exact':
381
+ return output.trim() === expected.trim();
382
+ case 'contains':
383
+ return outputLower.includes(expectedLower);
384
+ case 'startsWith':
385
+ return outputLower.startsWith(expectedLower);
386
+ case 'regex':
387
+ return new RegExp(expected).test(output);
388
+ default:
389
+ return outputLower.includes(expectedLower);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get default test set for unknown domains
395
+ */
396
+ _getDefaultTestSet() {
397
+ return [
398
+ {
399
+ input: 'Hello, how are you?',
400
+ expected: 'hello',
401
+ criteria: 'contains',
402
+ },
403
+ {
404
+ input: 'What is 2 + 2?',
405
+ expected: '4',
406
+ criteria: 'contains',
407
+ },
408
+ {
409
+ input: 'Translate to French: hello',
410
+ expected: 'bonjour',
411
+ criteria: 'contains',
412
+ },
413
+ ];
414
+ }
415
+
416
+ /**
417
+ * Export quarantine state
418
+ */
419
+ export() {
420
+ return {
421
+ quarantine: Array.from(this.quarantine.entries()),
422
+ approved: Array.from(this.approved.entries()),
423
+ blocked: Array.from(this.blocked.entries()),
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Import quarantine state
429
+ */
430
+ import(data) {
431
+ if (data.quarantine) {
432
+ this.quarantine = new Map(data.quarantine);
433
+ }
434
+ if (data.approved) {
435
+ this.approved = new Map(data.approved);
436
+ }
437
+ if (data.blocked) {
438
+ this.blocked = new Map(data.blocked);
439
+ }
440
+ }
441
+ }
442
+
443
+ // ============================================================================
444
+ // MERGE LINEAGE TRACKING
445
+ // ============================================================================
446
+
447
+ /**
448
+ * Lineage entry for merged adapters
449
+ */
450
+ export function createMergeLineage(options) {
451
+ return {
452
+ parentAdapterIds: options.parentIds,
453
+ mergeMethod: options.method, // 'ties', 'dare', 'task_arithmetic', 'linear'
454
+ mergeParameters: options.parameters, // Method-specific params
455
+ mergeSeed: options.seed || Math.floor(Math.random() * 2 ** 32),
456
+ evaluationMetrics: options.metrics || {},
457
+ mergerIdentity: options.mergerId,
458
+ mergeTimestamp: new Date().toISOString(),
459
+ signature: null, // To be filled after signing
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Lineage tracker for adapter merges
465
+ */
466
+ export class AdapterLineage {
467
+ constructor(options = {}) {
468
+ this.trustRoot = options.trustRoot || new TrustRoot();
469
+
470
+ // DAG of adapter lineage
471
+ this.lineageGraph = new Map();
472
+
473
+ // Root adapters (no parents)
474
+ this.roots = new Set();
475
+ }
476
+
477
+ /**
478
+ * Register a new adapter in lineage
479
+ */
480
+ registerAdapter(adapterId, manifest) {
481
+ const lineage = manifest.lineage;
482
+
483
+ const node = {
484
+ adapterId,
485
+ version: manifest.adapter.version,
486
+ baseModelId: manifest.adapter.baseModelId,
487
+ parents: lineage?.parentAdapterIds || [],
488
+ children: [],
489
+ lineage,
490
+ registeredAt: Date.now(),
491
+ };
492
+
493
+ this.lineageGraph.set(adapterId, node);
494
+
495
+ // Update parent-child relationships
496
+ if (node.parents.length === 0) {
497
+ this.roots.add(adapterId);
498
+ } else {
499
+ for (const parentId of node.parents) {
500
+ const parent = this.lineageGraph.get(parentId);
501
+ if (parent) {
502
+ parent.children.push(adapterId);
503
+ }
504
+ }
505
+ }
506
+
507
+ return node;
508
+ }
509
+
510
+ /**
511
+ * Get full ancestry path for an adapter
512
+ */
513
+ getAncestry(adapterId) {
514
+ const ancestry = [];
515
+ const visited = new Set();
516
+ const queue = [adapterId];
517
+
518
+ while (queue.length > 0) {
519
+ const current = queue.shift();
520
+ if (visited.has(current)) continue;
521
+ visited.add(current);
522
+
523
+ const node = this.lineageGraph.get(current);
524
+ if (node) {
525
+ ancestry.push({
526
+ adapterId: current,
527
+ version: node.version,
528
+ baseModelId: node.baseModelId,
529
+ mergeMethod: node.lineage?.mergeMethod,
530
+ });
531
+
532
+ for (const parentId of node.parents) {
533
+ queue.push(parentId);
534
+ }
535
+ }
536
+ }
537
+
538
+ return ancestry;
539
+ }
540
+
541
+ /**
542
+ * Verify lineage integrity
543
+ */
544
+ verifyLineage(adapterId) {
545
+ const node = this.lineageGraph.get(adapterId);
546
+ if (!node) {
547
+ return { valid: false, error: 'Adapter not found' };
548
+ }
549
+
550
+ const errors = [];
551
+
552
+ // Check all parents exist
553
+ for (const parentId of node.parents) {
554
+ if (!this.lineageGraph.has(parentId)) {
555
+ errors.push(`Missing parent: ${parentId}`);
556
+ }
557
+ }
558
+
559
+ // Verify lineage signature if present
560
+ if (node.lineage?.signature) {
561
+ // In production, verify actual signature
562
+ const sigValid = true; // Placeholder
563
+ if (!sigValid) {
564
+ errors.push('Invalid lineage signature');
565
+ }
566
+ }
567
+
568
+ // Check for circular references
569
+ const hasCircle = this._detectCircle(adapterId, new Set());
570
+ if (hasCircle) {
571
+ errors.push('Circular lineage detected');
572
+ }
573
+
574
+ return {
575
+ valid: errors.length === 0,
576
+ errors,
577
+ ancestry: this.getAncestry(adapterId),
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Detect circular references in lineage
583
+ */
584
+ _detectCircle(adapterId, visited) {
585
+ if (visited.has(adapterId)) return true;
586
+ visited.add(adapterId);
587
+
588
+ const node = this.lineageGraph.get(adapterId);
589
+ if (!node) return false;
590
+
591
+ for (const parentId of node.parents) {
592
+ if (this._detectCircle(parentId, new Set(visited))) {
593
+ return true;
594
+ }
595
+ }
596
+
597
+ return false;
598
+ }
599
+
600
+ /**
601
+ * Get descendants of an adapter
602
+ */
603
+ getDescendants(adapterId) {
604
+ const descendants = [];
605
+ const queue = [adapterId];
606
+ const visited = new Set();
607
+
608
+ while (queue.length > 0) {
609
+ const current = queue.shift();
610
+ if (visited.has(current)) continue;
611
+ visited.add(current);
612
+
613
+ const node = this.lineageGraph.get(current);
614
+ if (node) {
615
+ for (const childId of node.children) {
616
+ descendants.push(childId);
617
+ queue.push(childId);
618
+ }
619
+ }
620
+ }
621
+
622
+ return descendants;
623
+ }
624
+
625
+ /**
626
+ * Compute reproducibility hash for a merge
627
+ */
628
+ computeReproducibilityHash(lineage) {
629
+ const payload = {
630
+ parents: lineage.parentAdapterIds.sort(),
631
+ method: lineage.mergeMethod,
632
+ parameters: lineage.mergeParameters,
633
+ seed: lineage.mergeSeed,
634
+ };
635
+ return hashCanonical(payload);
636
+ }
637
+
638
+ /**
639
+ * Export lineage graph
640
+ */
641
+ export() {
642
+ return {
643
+ nodes: Array.from(this.lineageGraph.entries()),
644
+ roots: Array.from(this.roots),
645
+ };
646
+ }
647
+
648
+ /**
649
+ * Import lineage graph
650
+ */
651
+ import(data) {
652
+ if (data.nodes) {
653
+ this.lineageGraph = new Map(data.nodes);
654
+ }
655
+ if (data.roots) {
656
+ this.roots = new Set(data.roots);
657
+ }
658
+ }
659
+ }
660
+
661
+ // ============================================================================
662
+ // ADAPTER POOL WITH SECURITY
663
+ // ============================================================================
664
+
665
+ /**
666
+ * Secure adapter pool with quarantine integration
667
+ */
668
+ export class SecureAdapterPool {
669
+ constructor(options = {}) {
670
+ this.maxSlots = options.maxSlots || 16;
671
+ this.quarantine = new AdapterQuarantine(options);
672
+ this.lineage = new AdapterLineage(options);
673
+
674
+ // Active adapters (LRU)
675
+ this.activeAdapters = new Map();
676
+ this.accessOrder = [];
677
+ }
678
+
679
+ /**
680
+ * Add adapter with full security checks
681
+ */
682
+ async addAdapter(manifest, adapterData, inferenceSession = null) {
683
+ const adapterId = manifest.adapter.id;
684
+
685
+ // 1. Quarantine and verify
686
+ const quarantineResult = await this.quarantine.quarantineAdapter(manifest, adapterData);
687
+
688
+ if (quarantineResult.state === QuarantineState.FAILED) {
689
+ throw new Error(`Adapter blocked: ${quarantineResult.failure.reason}`);
690
+ }
691
+
692
+ // 2. If not trusted, run local evaluation
693
+ if (quarantineResult.state === QuarantineState.PENDING) {
694
+ if (!inferenceSession) {
695
+ throw new Error('Inference session required for local evaluation');
696
+ }
697
+
698
+ const evalResult = await this.quarantine.evaluateAdapter(adapterId, inferenceSession);
699
+ if (evalResult.state === QuarantineState.FAILED) {
700
+ throw new Error(`Adapter failed evaluation: score ${evalResult.results.score}`);
701
+ }
702
+ }
703
+
704
+ // 3. Register in lineage
705
+ this.lineage.registerAdapter(adapterId, manifest);
706
+
707
+ // 4. Add to active pool
708
+ await this._addToPool(adapterId, adapterData, manifest);
709
+
710
+ return { adapterId, state: 'active' };
711
+ }
712
+
713
+ /**
714
+ * Get an adapter if allowed
715
+ */
716
+ getAdapter(adapterId, baseModelId) {
717
+ // Check if can use
718
+ const check = this.quarantine.canUseAdapter(adapterId, baseModelId);
719
+ if (!check.allowed) {
720
+ return { allowed: false, reason: check.reason };
721
+ }
722
+
723
+ // Get from pool
724
+ const adapter = this.activeAdapters.get(adapterId);
725
+ if (!adapter) {
726
+ return { allowed: false, reason: 'not_in_pool' };
727
+ }
728
+
729
+ // Update access order
730
+ this._updateAccessOrder(adapterId);
731
+
732
+ return { allowed: true, adapter };
733
+ }
734
+
735
+ /**
736
+ * Add to pool with LRU eviction
737
+ */
738
+ async _addToPool(adapterId, adapterData, manifest) {
739
+ // Evict if at capacity
740
+ while (this.activeAdapters.size >= this.maxSlots) {
741
+ const evictId = this.accessOrder.shift();
742
+ this.activeAdapters.delete(evictId);
743
+ }
744
+
745
+ this.activeAdapters.set(adapterId, {
746
+ data: adapterData,
747
+ manifest,
748
+ loadedAt: Date.now(),
749
+ });
750
+
751
+ this._updateAccessOrder(adapterId);
752
+ }
753
+
754
+ /**
755
+ * Update LRU access order
756
+ */
757
+ _updateAccessOrder(adapterId) {
758
+ const index = this.accessOrder.indexOf(adapterId);
759
+ if (index > -1) {
760
+ this.accessOrder.splice(index, 1);
761
+ }
762
+ this.accessOrder.push(adapterId);
763
+ }
764
+
765
+ /**
766
+ * Get pool statistics
767
+ */
768
+ getStats() {
769
+ return {
770
+ activeCount: this.activeAdapters.size,
771
+ maxSlots: this.maxSlots,
772
+ quarantinedCount: this.quarantine.quarantine.size,
773
+ approvedCount: this.quarantine.approved.size,
774
+ blockedCount: this.quarantine.blocked.size,
775
+ lineageNodes: this.lineage.lineageGraph.size,
776
+ };
777
+ }
778
+ }
779
+
780
+ // ============================================================================
781
+ // EXPORTS
782
+ // ============================================================================
783
+
784
+ export default {
785
+ ADAPTER_REQUIREMENTS,
786
+ QuarantineState,
787
+ createAdapterManifest,
788
+ AdapterQuarantine,
789
+ createMergeLineage,
790
+ AdapterLineage,
791
+ SecureAdapterPool,
792
+ };