@unrdf/kgc-probe 26.4.2

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,785 @@
1
+ /**
2
+ * @file Persistence Probe - Output persistence and storage characteristics
3
+ * @module @unrdf/kgc-probe/probes/persistence
4
+ *
5
+ * @description
6
+ * Probes output persistence and storage characteristics:
7
+ * - Output persistence across runs (write, read, verify)
8
+ * - Quota behavior (write until failure, record limit)
9
+ * - Temp directory behavior (location, cleanup, permissions)
10
+ * - File locking semantics (if observable via flock or exclusive open)
11
+ * - Directory permissions (can create, rename, delete)
12
+ * - Atomic operations (rename, link)
13
+ * - Storage type detection (in-memory, disk, network if detectable)
14
+ *
15
+ * GUARD CONSTRAINTS:
16
+ * - ONLY write within config.out directory
17
+ * - NO access to system directories
18
+ * - Clean up test files after probing
19
+ * - Limit quota test to 100MB max
20
+ * - Timeout operations (5s per operation)
21
+ */
22
+
23
+ import { promises as fs } from 'node:fs';
24
+ import { join, resolve, dirname } from 'node:path';
25
+ import { tmpdir } from 'node:os';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { ObservationSchema, ProbeConfigSchema } from '../types.mjs';
28
+
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+
32
+ // =============================================================================
33
+ // Guard Functions
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Check if path is within allowed output directory
38
+ * @param {string} path - Path to check
39
+ * @param {string} outDir - Allowed output directory
40
+ * @returns {Object} Guard decision
41
+ */
42
+ function guardPathAccess(path, outDir) {
43
+ const resolvedPath = resolve(path);
44
+ const resolvedOutDir = resolve(outDir);
45
+
46
+ const allowed = resolvedPath.startsWith(resolvedOutDir);
47
+
48
+ return {
49
+ path: resolvedPath,
50
+ allowed,
51
+ reason: allowed
52
+ ? 'Within config.out directory'
53
+ : `Outside config.out directory (${resolvedOutDir})`,
54
+ policy: 'output-only',
55
+ timestamp: Date.now(),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create observation with guard decision
61
+ * @param {string} probeName - Name of probe
62
+ * @param {string} category - Observation category
63
+ * @param {string} observation - Observation description
64
+ * @param {any} value - Observed value
65
+ * @param {Object} guardDecision - Guard decision
66
+ * @param {Object} metadata - Additional metadata
67
+ * @returns {Object} Validated observation
68
+ */
69
+ function createObservation(probeName, category, observation, value, guardDecision, metadata = {}) {
70
+ const obs = {
71
+ probeName,
72
+ timestamp: Date.now(),
73
+ category,
74
+ observation,
75
+ value,
76
+ guardDecision,
77
+ metadata,
78
+ };
79
+
80
+ // Validate before returning
81
+ const validated = ObservationSchema.parse(obs);
82
+ return validated;
83
+ }
84
+
85
+ /**
86
+ * Create error observation
87
+ * @param {string} probeName - Name of probe
88
+ * @param {string} category - Observation category
89
+ * @param {string} observation - Observation description
90
+ * @param {Error} error - Error object
91
+ * @param {Object} guardDecision - Guard decision
92
+ * @returns {Object} Validated observation
93
+ */
94
+ function createErrorObservation(probeName, category, observation, error, guardDecision) {
95
+ return createObservation(
96
+ probeName,
97
+ category,
98
+ observation,
99
+ null,
100
+ guardDecision,
101
+ {
102
+ error: {
103
+ message: error.message,
104
+ code: error.code,
105
+ stack: error.stack,
106
+ },
107
+ }
108
+ );
109
+ }
110
+
111
+ // =============================================================================
112
+ // Persistence Probe Functions
113
+ // =============================================================================
114
+
115
+ /**
116
+ * Test basic write/read/verify persistence
117
+ * @param {Object} config - Probe configuration
118
+ * @returns {Promise<Object[]>} Observations
119
+ */
120
+ async function probeBasicPersistence(config) {
121
+ const observations = [];
122
+ const testFile = join(config.out, 'persistence-test.txt');
123
+ const testContent = `Persistence test at ${Date.now()}`;
124
+
125
+ const guardDecision = guardPathAccess(testFile, config.out);
126
+
127
+ if (!guardDecision.allowed) {
128
+ observations.push(
129
+ createObservation(
130
+ 'persistence',
131
+ 'security',
132
+ 'Basic persistence test blocked by guard',
133
+ false,
134
+ guardDecision
135
+ )
136
+ );
137
+ return observations;
138
+ }
139
+
140
+ try {
141
+ // Write
142
+ const writeStart = Date.now();
143
+ await fs.writeFile(testFile, testContent, 'utf8');
144
+ const writeTime = Date.now() - writeStart;
145
+
146
+ observations.push(
147
+ createObservation(
148
+ 'persistence',
149
+ 'storage',
150
+ 'Write operation successful',
151
+ true,
152
+ guardDecision,
153
+ { writeTime, fileSize: testContent.length }
154
+ )
155
+ );
156
+
157
+ // Read
158
+ const readStart = Date.now();
159
+ const readContent = await fs.readFile(testFile, 'utf8');
160
+ const readTime = Date.now() - readStart;
161
+
162
+ const contentMatches = readContent === testContent;
163
+ observations.push(
164
+ createObservation(
165
+ 'persistence',
166
+ 'storage',
167
+ 'Read operation and content verification',
168
+ contentMatches,
169
+ guardDecision,
170
+ { readTime, contentMatches }
171
+ )
172
+ );
173
+
174
+ // Check file stats
175
+ const stats = await fs.stat(testFile);
176
+ observations.push(
177
+ createObservation(
178
+ 'persistence',
179
+ 'filesystem',
180
+ 'File metadata accessible',
181
+ true,
182
+ guardDecision,
183
+ {
184
+ size: stats.size,
185
+ mode: stats.mode.toString(8),
186
+ created: stats.birthtime.getTime(),
187
+ modified: stats.mtime.getTime(),
188
+ }
189
+ )
190
+ );
191
+
192
+ // Clean up
193
+ await fs.unlink(testFile);
194
+ observations.push(
195
+ createObservation(
196
+ 'persistence',
197
+ 'filesystem',
198
+ 'File deletion successful',
199
+ true,
200
+ guardDecision
201
+ )
202
+ );
203
+ } catch (error) {
204
+ observations.push(createErrorObservation('persistence', 'storage', 'Basic persistence test failed', error, guardDecision));
205
+ }
206
+
207
+ return observations;
208
+ }
209
+
210
+ /**
211
+ * Test persistence across runs by checking for marker file
212
+ * @param {Object} config - Probe configuration
213
+ * @returns {Promise<Object[]>} Observations
214
+ */
215
+ async function probeCrossRunPersistence(config) {
216
+ const observations = [];
217
+ const markerFile = join(config.out, '.persistence-marker');
218
+
219
+ const guardDecision = guardPathAccess(markerFile, config.out);
220
+
221
+ if (!guardDecision.allowed) {
222
+ observations.push(
223
+ createObservation(
224
+ 'persistence',
225
+ 'security',
226
+ 'Cross-run persistence test blocked by guard',
227
+ false,
228
+ guardDecision
229
+ )
230
+ );
231
+ return observations;
232
+ }
233
+
234
+ try {
235
+ let markerExists = false;
236
+ let previousTimestamp = null;
237
+
238
+ try {
239
+ const content = await fs.readFile(markerFile, 'utf8');
240
+ previousTimestamp = parseInt(content, 10);
241
+ markerExists = true;
242
+ } catch (error) {
243
+ if (error.code !== 'ENOENT') {
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ observations.push(
249
+ createObservation(
250
+ 'persistence',
251
+ 'storage',
252
+ 'Persistence marker from previous run',
253
+ markerExists,
254
+ guardDecision,
255
+ { previousTimestamp, markerExists }
256
+ )
257
+ );
258
+
259
+ // Write new marker
260
+ const currentTimestamp = Date.now();
261
+ await fs.writeFile(markerFile, currentTimestamp.toString(), 'utf8');
262
+
263
+ observations.push(
264
+ createObservation(
265
+ 'persistence',
266
+ 'storage',
267
+ 'Persistence marker written for next run',
268
+ true,
269
+ guardDecision,
270
+ { currentTimestamp }
271
+ )
272
+ );
273
+ } catch (error) {
274
+ observations.push(
275
+ createErrorObservation('persistence', 'storage', 'Cross-run persistence test failed', error, guardDecision)
276
+ );
277
+ }
278
+
279
+ return observations;
280
+ }
281
+
282
+ /**
283
+ * Test quota limits by writing incrementally
284
+ * @param {Object} config - Probe configuration
285
+ * @returns {Promise<Object[]>} Observations
286
+ */
287
+ async function probeQuotaLimits(config) {
288
+ const observations = [];
289
+ const quotaTestFile = join(config.out, 'quota-test.bin');
290
+
291
+ const guardDecision = guardPathAccess(quotaTestFile, config.out);
292
+
293
+ if (!guardDecision.allowed) {
294
+ observations.push(
295
+ createObservation(
296
+ 'persistence',
297
+ 'security',
298
+ 'Quota test blocked by guard',
299
+ false,
300
+ guardDecision
301
+ )
302
+ );
303
+ return observations;
304
+ }
305
+
306
+ try {
307
+ const chunkSize = config.chunkSize || 1024 * 1024; // 1 MB
308
+ const maxQuota = config.maxQuota || 100 * 1024 * 1024; // 100 MB
309
+ const chunk = Buffer.alloc(chunkSize, 'A');
310
+
311
+ let bytesWritten = 0;
312
+ let quotaReached = false;
313
+ let quotaError = null;
314
+
315
+ const startTime = Date.now();
316
+
317
+ // Write chunks until we hit quota or max limit
318
+ while (bytesWritten < maxQuota && !quotaReached) {
319
+ try {
320
+ await fs.appendFile(quotaTestFile, chunk);
321
+ bytesWritten += chunkSize;
322
+
323
+ // Check if file size matches expected
324
+ const stats = await fs.stat(quotaTestFile);
325
+ if (stats.size !== bytesWritten) {
326
+ quotaReached = true;
327
+ quotaError = `Size mismatch: expected ${bytesWritten}, got ${stats.size}`;
328
+ break;
329
+ }
330
+
331
+ // Timeout check (don't spend more than config.timeout on quota test)
332
+ if (Date.now() - startTime > config.timeout) {
333
+ break;
334
+ }
335
+ } catch (error) {
336
+ quotaReached = true;
337
+ quotaError = error.message;
338
+ break;
339
+ }
340
+ }
341
+
342
+ const duration = Date.now() - startTime;
343
+
344
+ observations.push(
345
+ createObservation(
346
+ 'persistence',
347
+ 'quota',
348
+ quotaReached ? 'Quota limit reached' : 'Quota test completed without hitting limit',
349
+ bytesWritten,
350
+ guardDecision,
351
+ {
352
+ bytesWritten,
353
+ quotaReached,
354
+ quotaError,
355
+ duration,
356
+ throughputMBps: bytesWritten / duration / 1024,
357
+ }
358
+ )
359
+ );
360
+
361
+ // Clean up
362
+ try {
363
+ await fs.unlink(quotaTestFile);
364
+ } catch (error) {
365
+ // Ignore cleanup errors
366
+ }
367
+ } catch (error) {
368
+ observations.push(createErrorObservation('persistence', 'quota', 'Quota test failed', error, guardDecision));
369
+ }
370
+
371
+ return observations;
372
+ }
373
+
374
+ /**
375
+ * Test directory permissions (create, rename, delete)
376
+ * @param {Object} config - Probe configuration
377
+ * @returns {Promise<Object[]>} Observations
378
+ */
379
+ async function probeDirectoryPermissions(config) {
380
+ const observations = [];
381
+ const testDir = join(config.out, 'perm-test-dir');
382
+ const renamedDir = join(config.out, 'perm-test-dir-renamed');
383
+
384
+ const guardDecision = guardPathAccess(testDir, config.out);
385
+ const guardDecisionRenamed = guardPathAccess(renamedDir, config.out);
386
+
387
+ if (!guardDecision.allowed || !guardDecisionRenamed.allowed) {
388
+ observations.push(
389
+ createObservation(
390
+ 'persistence',
391
+ 'security',
392
+ 'Directory permissions test blocked by guard',
393
+ false,
394
+ guardDecision
395
+ )
396
+ );
397
+ return observations;
398
+ }
399
+
400
+ try {
401
+ // Create directory
402
+ await fs.mkdir(testDir, { recursive: true });
403
+ observations.push(
404
+ createObservation(
405
+ 'persistence',
406
+ 'permissions',
407
+ 'Directory creation successful',
408
+ true,
409
+ guardDecision
410
+ )
411
+ );
412
+
413
+ // Check directory stats
414
+ const stats = await fs.stat(testDir);
415
+ observations.push(
416
+ createObservation(
417
+ 'persistence',
418
+ 'permissions',
419
+ 'Directory is accessible',
420
+ stats.isDirectory(),
421
+ guardDecision,
422
+ { mode: stats.mode.toString(8) }
423
+ )
424
+ );
425
+
426
+ // Rename directory
427
+ await fs.rename(testDir, renamedDir);
428
+ observations.push(
429
+ createObservation(
430
+ 'persistence',
431
+ 'permissions',
432
+ 'Directory rename successful',
433
+ true,
434
+ guardDecisionRenamed
435
+ )
436
+ );
437
+
438
+ // Delete directory
439
+ await fs.rmdir(renamedDir);
440
+ observations.push(
441
+ createObservation(
442
+ 'persistence',
443
+ 'permissions',
444
+ 'Directory deletion successful',
445
+ true,
446
+ guardDecisionRenamed
447
+ )
448
+ );
449
+ } catch (error) {
450
+ observations.push(
451
+ createErrorObservation('persistence', 'permissions', 'Directory permissions test failed', error, guardDecision)
452
+ );
453
+
454
+ // Cleanup on error
455
+ try {
456
+ await fs.rmdir(testDir).catch(() => {});
457
+ await fs.rmdir(renamedDir).catch(() => {});
458
+ } catch (e) {
459
+ // Ignore cleanup errors
460
+ }
461
+ }
462
+
463
+ return observations;
464
+ }
465
+
466
+ /**
467
+ * Test atomic operations (rename, link if supported)
468
+ * @param {Object} config - Probe configuration
469
+ * @returns {Promise<Object[]>} Observations
470
+ */
471
+ async function probeAtomicOperations(config) {
472
+ const observations = [];
473
+ const sourceFile = join(config.out, 'atomic-source.txt');
474
+ const targetFile = join(config.out, 'atomic-target.txt');
475
+ const linkFile = join(config.out, 'atomic-link.txt');
476
+
477
+ const guardDecisionSource = guardPathAccess(sourceFile, config.out);
478
+ const guardDecisionTarget = guardPathAccess(targetFile, config.out);
479
+ const guardDecisionLink = guardPathAccess(linkFile, config.out);
480
+
481
+ if (!guardDecisionSource.allowed || !guardDecisionTarget.allowed || !guardDecisionLink.allowed) {
482
+ observations.push(
483
+ createObservation(
484
+ 'persistence',
485
+ 'security',
486
+ 'Atomic operations test blocked by guard',
487
+ false,
488
+ guardDecisionSource
489
+ )
490
+ );
491
+ return observations;
492
+ }
493
+
494
+ try {
495
+ // Create source file
496
+ await fs.writeFile(sourceFile, 'atomic test content', 'utf8');
497
+
498
+ // Test atomic rename
499
+ const renameStart = Date.now();
500
+ await fs.rename(sourceFile, targetFile);
501
+ const renameTime = Date.now() - renameStart;
502
+
503
+ observations.push(
504
+ createObservation(
505
+ 'persistence',
506
+ 'filesystem',
507
+ 'Atomic rename successful',
508
+ true,
509
+ guardDecisionTarget,
510
+ { renameTime }
511
+ )
512
+ );
513
+
514
+ // Test hard link (may not be supported on all filesystems)
515
+ try {
516
+ await fs.link(targetFile, linkFile);
517
+ const linkStats = await fs.stat(linkFile);
518
+
519
+ observations.push(
520
+ createObservation(
521
+ 'persistence',
522
+ 'filesystem',
523
+ 'Hard link creation successful',
524
+ true,
525
+ guardDecisionLink,
526
+ { nlink: linkStats.nlink }
527
+ )
528
+ );
529
+
530
+ // Clean up link
531
+ await fs.unlink(linkFile);
532
+ } catch (error) {
533
+ observations.push(
534
+ createObservation(
535
+ 'persistence',
536
+ 'filesystem',
537
+ 'Hard link not supported or failed',
538
+ false,
539
+ guardDecisionLink,
540
+ { error: error.message }
541
+ )
542
+ );
543
+ }
544
+
545
+ // Clean up
546
+ await fs.unlink(targetFile);
547
+ } catch (error) {
548
+ observations.push(
549
+ createErrorObservation('persistence', 'filesystem', 'Atomic operations test failed', error, guardDecisionSource)
550
+ );
551
+
552
+ // Cleanup on error
553
+ try {
554
+ await fs.unlink(sourceFile).catch(() => {});
555
+ await fs.unlink(targetFile).catch(() => {});
556
+ await fs.unlink(linkFile).catch(() => {});
557
+ } catch (e) {
558
+ // Ignore cleanup errors
559
+ }
560
+ }
561
+
562
+ return observations;
563
+ }
564
+
565
+ /**
566
+ * Test temp directory behavior
567
+ * @param {Object} config - Probe configuration
568
+ * @returns {Promise<Object[]>} Observations
569
+ */
570
+ async function probeTempDirectory(config) {
571
+ const observations = [];
572
+
573
+ try {
574
+ const tempDir = tmpdir();
575
+
576
+ // We don't write to temp dir (guard constraint), just observe it
577
+ const guardDecision = {
578
+ path: tempDir,
579
+ allowed: false,
580
+ reason: 'System temp directory - read-only observation',
581
+ policy: 'observe-only',
582
+ timestamp: Date.now(),
583
+ };
584
+
585
+ observations.push(
586
+ createObservation(
587
+ 'persistence',
588
+ 'filesystem',
589
+ 'System temp directory location',
590
+ tempDir,
591
+ guardDecision
592
+ )
593
+ );
594
+
595
+ // Try to check if temp dir is accessible (read-only)
596
+ try {
597
+ const stats = await fs.stat(tempDir);
598
+ observations.push(
599
+ createObservation(
600
+ 'persistence',
601
+ 'filesystem',
602
+ 'System temp directory accessible for reading',
603
+ true,
604
+ guardDecision,
605
+ { mode: stats.mode.toString(8) }
606
+ )
607
+ );
608
+ } catch (error) {
609
+ observations.push(
610
+ createObservation(
611
+ 'persistence',
612
+ 'filesystem',
613
+ 'System temp directory not accessible',
614
+ false,
615
+ guardDecision,
616
+ { error: error.message }
617
+ )
618
+ );
619
+ }
620
+ } catch (error) {
621
+ const guardDecision = {
622
+ path: 'tmpdir()',
623
+ allowed: false,
624
+ reason: 'System temp directory access failed',
625
+ policy: 'observe-only',
626
+ timestamp: Date.now(),
627
+ };
628
+
629
+ observations.push(
630
+ createErrorObservation('persistence', 'filesystem', 'Temp directory probe failed', error, guardDecision)
631
+ );
632
+ }
633
+
634
+ return observations;
635
+ }
636
+
637
+ /**
638
+ * Detect storage type (heuristic based on performance and behavior)
639
+ * @param {Object} config - Probe configuration
640
+ * @returns {Promise<Object[]>} Observations
641
+ */
642
+ async function probeStorageType(config) {
643
+ const observations = [];
644
+ const testFile = join(config.out, 'storage-type-test.bin');
645
+
646
+ const guardDecision = guardPathAccess(testFile, config.out);
647
+
648
+ if (!guardDecision.allowed) {
649
+ observations.push(
650
+ createObservation(
651
+ 'persistence',
652
+ 'security',
653
+ 'Storage type detection blocked by guard',
654
+ false,
655
+ guardDecision
656
+ )
657
+ );
658
+ return observations;
659
+ }
660
+
661
+ try {
662
+ // Write a small file and measure latency
663
+ const testData = Buffer.alloc(4096, 'X');
664
+ const iterations = 10;
665
+ const latencies = [];
666
+
667
+ for (let i = 0; i < iterations; i++) {
668
+ const start = Date.now();
669
+ await fs.writeFile(testFile, testData);
670
+ const handle = await fs.open(testFile, 'r+');
671
+ try {
672
+ await handle.sync();
673
+ } finally {
674
+ await handle.close();
675
+ }
676
+ latencies.push(Date.now() - start);
677
+ }
678
+
679
+ const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
680
+ const minLatency = Math.min(...latencies);
681
+ const maxLatency = Math.max(...latencies);
682
+
683
+ // Heuristic: in-memory < 1ms, SSD < 5ms, HDD > 5ms
684
+ let storageType = 'unknown';
685
+ if (avgLatency < 1) {
686
+ storageType = 'in-memory (likely tmpfs or ramfs)';
687
+ } else if (avgLatency < 5) {
688
+ storageType = 'fast-storage (likely SSD or cached)';
689
+ } else {
690
+ storageType = 'slow-storage (likely HDD or network)';
691
+ }
692
+
693
+ observations.push(
694
+ createObservation(
695
+ 'persistence',
696
+ 'performance',
697
+ 'Storage type detection (heuristic)',
698
+ storageType,
699
+ guardDecision,
700
+ {
701
+ avgLatency,
702
+ minLatency,
703
+ maxLatency,
704
+ latencies,
705
+ }
706
+ )
707
+ );
708
+
709
+ // Clean up
710
+ await fs.unlink(testFile);
711
+ } catch (error) {
712
+ observations.push(
713
+ createErrorObservation('persistence', 'performance', 'Storage type detection failed', error, guardDecision)
714
+ );
715
+ }
716
+
717
+ return observations;
718
+ }
719
+
720
+ // =============================================================================
721
+ // Main Probe Function
722
+ // =============================================================================
723
+
724
+ /**
725
+ * Probe output persistence and storage characteristics
726
+ *
727
+ * @param {Object} config - Probe configuration
728
+ * @param {string} config.out - Output directory (REQUIRED)
729
+ * @param {number} [config.timeout=5000] - Timeout per operation (ms)
730
+ * @param {number} [config.maxQuota=104857600] - Max quota to test (bytes, default 100MB)
731
+ * @param {number} [config.chunkSize=1048576] - Chunk size for quota test (bytes, default 1MB)
732
+ * @returns {Promise<Object[]>} Array of observations
733
+ *
734
+ * @example
735
+ * const observations = await probePersistence({
736
+ * out: '/home/user/output',
737
+ * timeout: 5000,
738
+ * maxQuota: 100 * 1024 * 1024
739
+ * });
740
+ */
741
+ export async function probePersistence(config) {
742
+ // Validate config
743
+ const validatedConfig = ProbeConfigSchema.parse(config);
744
+
745
+ const observations = [];
746
+
747
+ try {
748
+ // Ensure output directory exists
749
+ await fs.mkdir(validatedConfig.out, { recursive: true });
750
+
751
+ // Run all persistence probes
752
+ const results = await Promise.all([
753
+ probeBasicPersistence(validatedConfig),
754
+ probeCrossRunPersistence(validatedConfig),
755
+ probeQuotaLimits(validatedConfig),
756
+ probeDirectoryPermissions(validatedConfig),
757
+ probeAtomicOperations(validatedConfig),
758
+ probeTempDirectory(validatedConfig),
759
+ probeStorageType(validatedConfig),
760
+ ]);
761
+
762
+ // Flatten results
763
+ results.forEach(result => observations.push(...result));
764
+ } catch (error) {
765
+ const guardDecision = {
766
+ path: validatedConfig.out,
767
+ allowed: false,
768
+ reason: 'Probe execution failed',
769
+ policy: 'error',
770
+ timestamp: Date.now(),
771
+ };
772
+
773
+ observations.push(
774
+ createErrorObservation('persistence', 'storage', 'Persistence probe failed', error, guardDecision)
775
+ );
776
+ }
777
+
778
+ return observations;
779
+ }
780
+
781
+ // =============================================================================
782
+ // Module Exports
783
+ // =============================================================================
784
+
785
+ export default probePersistence;