minovative-mind-cli 2.6.4 → 2.8.0

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.
@@ -207,3 +207,678 @@ export async function runDebugScript(workspaceRoot, code, language = 'node', opt
207
207
  }
208
208
  return runEphemeralScript(workspaceRoot, lang, code, opts);
209
209
  }
210
+ /**
211
+ * Generates an ephemeral Property-Based Testing script template tailored to the target language and properties.
212
+ */
213
+ export function generatePBTScriptTemplate(language, targetFunctionOrFeature, properties) {
214
+ const normLang = normalizeLanguage(language);
215
+ if (normLang === 'python') {
216
+ return `import sys, random
217
+
218
+ # Target: ${targetFunctionOrFeature}
219
+ # Properties to verify:
220
+ ${properties.map((p) => `# - ${p}`).join('\n')}
221
+
222
+ def property_test():
223
+ runs = 100
224
+ for i in range(runs):
225
+ # Generate random inputs
226
+ val_int = random.randint(-10000, 10000)
227
+ val_str = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=random.randint(0, 50)))
228
+
229
+ # Test property invariants
230
+ try:
231
+ # Assertion placeholder
232
+ assert isinstance(val_int, int)
233
+ except AssertionError as e:
234
+ print(f"[PBT FAILED] Seed/Iteration: {i}")
235
+ print(f"Counterexample: val_int={val_int}, val_str={val_str!r}")
236
+ sys.exit(1)
237
+
238
+ print(f"[PBT PASSED] Completed {runs} runs successfully.")
239
+
240
+ if __name__ == "__main__":
241
+ property_test()
242
+ `;
243
+ }
244
+ // Default Node / TypeScript / JS template
245
+ return `// Target: ${targetFunctionOrFeature}
246
+ // Properties to verify:
247
+ ${properties.map((p) => `// - ${p}`).join('\n')}
248
+
249
+ async function runPBT() {
250
+ const numRuns = 100;
251
+ let passed = 0;
252
+
253
+ for (let i = 0; i < numRuns; i++) {
254
+ // Generate pseudo-random inputs
255
+ const randInt = Math.floor(Math.random() * 20000) - 10000;
256
+ const randStr = Math.random().toString(36).substring(2);
257
+
258
+ try {
259
+ // Test property invariant
260
+ if (typeof randInt !== 'number' || isNaN(randInt)) {
261
+ throw new Error('Type invariant failed');
262
+ }
263
+ passed++;
264
+ } catch (err) {
265
+ console.log(\`[PBT FAILED] Iteration: \${i}\`);
266
+ console.log(\`Counterexample: randInt=\${randInt}, randStr="\${randStr}"\`);
267
+ console.log(\`Error: \${err.message}\`);
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ console.log(\`[PBT PASSED] Completed \${passed} runs successfully.\`);
273
+ }
274
+
275
+ runPBT();
276
+ `;
277
+ }
278
+ /**
279
+ * Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
280
+ */
281
+ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node', config) {
282
+ let lang = 'node';
283
+ let opts;
284
+ if (typeof language === 'string') {
285
+ lang = language;
286
+ opts = config;
287
+ }
288
+ else if (typeof language === 'object' && language !== null) {
289
+ opts = language;
290
+ }
291
+ const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
292
+ const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
293
+ const isPassed = combinedOutput.includes('[PBT PASSED]') || (scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
294
+ let counterexample;
295
+ let shrunkInput;
296
+ let seed;
297
+ let numRunsCompleted;
298
+ // Extract counterexample
299
+ const counterMatch = combinedOutput.match(/Counterexample:\s*([^\n]+)/i);
300
+ if (counterMatch) {
301
+ counterexample = counterMatch[1].trim();
302
+ }
303
+ // Extract shrunk input
304
+ const shrinkMatch = combinedOutput.match(/(?:Shrunk Input|Shrunk|Minimal):\s*([^\n]+)/i);
305
+ if (shrinkMatch) {
306
+ shrunkInput = shrinkMatch[1].trim();
307
+ }
308
+ // Extract seed
309
+ const seedMatch = combinedOutput.match(/Seed(?:|\/Iteration):\s*(\d+)/i);
310
+ if (seedMatch) {
311
+ seed = parseInt(seedMatch[1], 10);
312
+ }
313
+ // Extract completed runs
314
+ const runsMatch = combinedOutput.match(/Completed\s+(\d+)\s+runs/i);
315
+ if (runsMatch) {
316
+ numRunsCompleted = parseInt(runsMatch[1], 10);
317
+ }
318
+ return {
319
+ ...scriptResult,
320
+ passed: isPassed,
321
+ counterexample,
322
+ shrunkInput,
323
+ seed,
324
+ numRunsCompleted,
325
+ };
326
+ }
327
+ /**
328
+ * Generates an ephemeral Fuzz Probe script template tailored to the target language.
329
+ */
330
+ export function generateFuzzScriptTemplate(language, targetFeature, config) {
331
+ const normLang = normalizeLanguage(language);
332
+ const iterations = config?.iterations ?? 500;
333
+ const maxLen = config?.maxInputLength ?? 256;
334
+ if (normLang === 'python') {
335
+ return `import sys, random, traceback
336
+
337
+ # Fuzz Probe Target: ${targetFeature}
338
+ def fuzz_target(data: bytes):
339
+ if len(data) > 0 and data[0] == 0xff:
340
+ raise ValueError("Simulated fuzz anomaly detected")
341
+
342
+ def run_fuzz():
343
+ runs = ${iterations}
344
+ failed_inputs = []
345
+
346
+ for i in range(runs):
347
+ input_len = random.randint(0, ${maxLen})
348
+ fuzz_data = random.randbytes(input_len) if hasattr(random, 'randbytes') else bytes([random.randint(0, 255) for _ in range(input_len)])
349
+
350
+ try:
351
+ fuzz_target(fuzz_data)
352
+ except Exception as e:
353
+ failed_inputs.append(fuzz_data.hex())
354
+ print(f"[FUZZ FAILED] Iteration: {i}, Error: {e}")
355
+ if len(failed_inputs) >= 5:
356
+ break
357
+
358
+ if failed_inputs:
359
+ print(f"[FUZZ SUMMARY] Total Runs: {runs}, Failures: {len(failed_inputs)}")
360
+ sys.exit(1)
361
+ else:
362
+ print(f"[FUZZ PASSED] Completed {runs} fuzz runs without crashes.")
363
+
364
+ if __name__ == "__main__":
365
+ run_fuzz()
366
+ `;
367
+ }
368
+ if (normLang === 'go') {
369
+ return `package main
370
+
371
+ import (
372
+ "fmt"
373
+ "math/rand"
374
+ "os"
375
+ )
376
+
377
+ func fuzzTarget(data []byte) {
378
+ if len(data) > 0 && data[0] == 0xff {
379
+ panic("Simulated fuzz anomaly detected")
380
+ }
381
+ }
382
+
383
+ func main() {
384
+ runs := ${iterations}
385
+ failures := 0
386
+
387
+ for i := 0; i < runs; i++ {
388
+ length := rand.Intn(${maxLen} + 1)
389
+ data := make([]byte, length)
390
+ rand.Read(data)
391
+
392
+ func() {
393
+ defer func() {
394
+ if r := recover(); r != nil {
395
+ failures++
396
+ fmt.Printf("[FUZZ FAILED] Iteration %d: %v\\n", i, r)
397
+ }
398
+ }()
399
+ fuzzTarget(data)
400
+ }()
401
+
402
+ if failures >= 5 {
403
+ break
404
+ }
405
+ }
406
+
407
+ if failures > 0 {
408
+ fmt.Printf("[FUZZ SUMMARY] Total Runs: %d, Failures: %d\\n", runs, failures)
409
+ os.Exit(1)
410
+ } else {
411
+ fmt.Printf("[FUZZ PASSED] Completed %d fuzz runs without crashes.\\n", runs)
412
+ }
413
+ }
414
+ `;
415
+ }
416
+ if (normLang === 'rust') {
417
+ return `use std::panic;
418
+
419
+ fn fuzz_target(data: &[u8]) {
420
+ if !data.is_empty() && data[0] == 0xff {
421
+ panic!("Simulated fuzz anomaly detected");
422
+ }
423
+ }
424
+
425
+ fn main() {
426
+ let runs = ${iterations};
427
+ let mut failures = 0;
428
+
429
+ for i in 0..runs {
430
+ let len = (i * 37) % ${maxLen};
431
+ let data: Vec<u8> = (0..len).map(|j| ((i + j) % 256) as u8).collect();
432
+
433
+ let result = panic::catch_unwind(|| {
434
+ fuzz_target(&data);
435
+ });
436
+
437
+ if result.is_err() {
438
+ failures += 1;
439
+ println!("[FUZZ FAILED] Iteration {}", i);
440
+ if failures >= 5 {
441
+ break;
442
+ }
443
+ }
444
+ }
445
+
446
+ if failures > 0 {
447
+ println!("[FUZZ SUMMARY] Total Runs: {}, Failures: {}", runs, failures);
448
+ std::process::exit(1);
449
+ } else {
450
+ println!("[FUZZ PASSED] Completed {} fuzz runs without crashes.", runs);
451
+ }
452
+ }
453
+ `;
454
+ }
455
+ // Default Node / TypeScript template
456
+ return `// Fuzz Probe Target: ${targetFeature}
457
+ function fuzzTarget(buffer) {
458
+ if (buffer.length > 0 && buffer[0] === 0xff) {
459
+ throw new Error('Simulated fuzz anomaly detected');
460
+ }
461
+ }
462
+
463
+ function runFuzz() {
464
+ const runs = ${iterations};
465
+ const maxLen = ${maxLen};
466
+ const failingInputs = [];
467
+
468
+ for (let i = 0; i < runs; i++) {
469
+ const len = Math.floor(Math.random() * maxLen);
470
+ const buf = Buffer.alloc(len);
471
+ for (let j = 0; j < len; j++) buf[j] = Math.floor(Math.random() * 256);
472
+
473
+ try {
474
+ fuzzTarget(buf);
475
+ } catch (err) {
476
+ failingInputs.push(buf.toString('hex'));
477
+ console.log(\`[FUZZ FAILED] Iteration: \${i}, Error: \${err.message}\`);
478
+ if (failingInputs.length >= 5) break;
479
+ }
480
+ }
481
+
482
+ if (failingInputs.length > 0) {
483
+ console.log(\`[FUZZ SUMMARY] Total Runs: \${runs}, Failures: \${failingInputs.length}\`);
484
+ process.exit(1);
485
+ } else {
486
+ console.log(\`[FUZZ PASSED] Completed \${runs} fuzz runs without crashes.\`);
487
+ }
488
+ }
489
+
490
+ runFuzz();
491
+ `;
492
+ }
493
+ /**
494
+ * Runs a fuzz probing analysis script and parses failure counterexamples and crash statistics.
495
+ */
496
+ export async function runFuzzProbe(workspaceRoot, code, language = 'node', config) {
497
+ let lang = 'node';
498
+ let opts;
499
+ if (typeof language === 'string') {
500
+ lang = language;
501
+ opts = config;
502
+ }
503
+ else if (typeof language === 'object' && language !== null) {
504
+ opts = language;
505
+ }
506
+ const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
507
+ const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
508
+ const isPassed = scriptResult.exitCode === 0 &&
509
+ (combinedOutput.includes('[FUZZ PASSED]') || !combinedOutput.includes('[FUZZ FAILED]'));
510
+ const failingInputs = [];
511
+ const failRegex = /(?:\[FUZZ FAILED\]|Failing Input|Counterexample)[:\s]\s*([^\n]+)/gi;
512
+ let match;
513
+ while ((match = failRegex.exec(combinedOutput)) !== null) {
514
+ failingInputs.push(match[1].trim());
515
+ }
516
+ let totalFuzzRuns = 0;
517
+ const runsMatch = combinedOutput.match(/(?:Total Runs|Runs):\s*(\d+)/i) || combinedOutput.match(/Completed\s+(\d+)\s+(?:fuzz\s+)?runs/i);
518
+ if (runsMatch) {
519
+ totalFuzzRuns = parseInt(runsMatch[1], 10);
520
+ }
521
+ let seed;
522
+ const seedMatch = combinedOutput.match(/Seed:\s*(\d+)/i);
523
+ if (seedMatch) {
524
+ seed = parseInt(seedMatch[1], 10);
525
+ }
526
+ let errorSummary;
527
+ if (!isPassed) {
528
+ const errLine = scriptResult.stderr.split('\n').find((l) => l.trim().length > 0) ||
529
+ combinedOutput.split('\n').find((l) => l.toLowerCase().includes('error') || l.includes('[FUZZ FAILED]'));
530
+ errorSummary = errLine?.trim() || 'Fuzzing probe encountered unexpected failure or crash.';
531
+ }
532
+ return {
533
+ ...scriptResult,
534
+ passed: isPassed,
535
+ totalFuzzRuns,
536
+ failingInputs: failingInputs.length > 0 ? failingInputs : undefined,
537
+ errorSummary,
538
+ seed,
539
+ };
540
+ }
541
+ /**
542
+ * Generates an ephemeral memory heap analysis script template tailored to the target language.
543
+ */
544
+ export function generateHeapCheckScriptTemplate(language, targetCode, config) {
545
+ const normLang = normalizeLanguage(language);
546
+ const warmup = config?.warmupIterations ?? 10;
547
+ const testIters = config?.testIterations ?? 100;
548
+ if (normLang === 'python') {
549
+ return `import sys, gc
550
+
551
+ # Target Logic:
552
+ # ${targetCode.replace(/\n/g, '\n# ')}
553
+
554
+ def main():
555
+ gc.collect()
556
+ start_bytes = 0
557
+ try:
558
+ import os, psutil
559
+ process = psutil.Process(os.getpid())
560
+ start_bytes = process.memory_info().rss
561
+ except Exception:
562
+ import resource
563
+ start_bytes = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
564
+
565
+ print(f"[HEAP_START] {start_bytes}")
566
+
567
+ for _ in range(${warmup}):
568
+ pass
569
+
570
+ test_iters = ${testIters}
571
+ for i in range(test_iters):
572
+ if i % max(1, test_iters // 5) == 0:
573
+ gc.collect()
574
+ try:
575
+ import os, psutil
576
+ curr = psutil.Process(os.getpid()).memory_info().rss
577
+ except Exception:
578
+ import resource
579
+ curr = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
580
+ print(f"[HEAP_SAMPLE] {i}, {curr}")
581
+
582
+ gc.collect()
583
+ final_bytes = start_bytes
584
+ try:
585
+ import os, psutil
586
+ final_bytes = psutil.Process(os.getpid()).memory_info().rss
587
+ except Exception:
588
+ import resource
589
+ final_bytes = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
590
+
591
+ delta = final_bytes - start_bytes
592
+ print(f"[HEAP_FINAL] {final_bytes}")
593
+ print(f"[HEAP_DELTA] {delta}")
594
+
595
+ if __name__ == "__main__":
596
+ main()
597
+ `;
598
+ }
599
+ if (normLang === 'go') {
600
+ return `package main
601
+
602
+ import (
603
+ "fmt"
604
+ "runtime"
605
+ )
606
+
607
+ func main() {
608
+ var m runtime.MemStats
609
+ runtime.GC()
610
+ runtime.ReadMemStats(&m)
611
+ initial := m.Alloc
612
+ fmt.Printf("[HEAP_START] %d\\n", initial)
613
+
614
+ testIters := ${testIters}
615
+ for i := 0; i < testIters; i++ {
616
+ if i%(testIters/5+1) == 0 {
617
+ runtime.ReadMemStats(&m)
618
+ fmt.Printf("[HEAP_SAMPLE] %d, %d\\n", i, m.Alloc)
619
+ }
620
+ }
621
+
622
+ runtime.GC()
623
+ runtime.ReadMemStats(&m)
624
+ finalHeap := m.Alloc
625
+ delta := int64(finalHeap) - int64(initial)
626
+ fmt.Printf("[HEAP_FINAL] %d\\n", finalHeap)
627
+ fmt.Printf("[HEAP_DELTA] %d\\n", delta)
628
+ }
629
+ `;
630
+ }
631
+ // Default Node / TypeScript template
632
+ return `if (global.gc) global.gc();
633
+ const initial = process.memoryUsage().heapUsed;
634
+ console.log(\`[HEAP_START] \${initial}\`);
635
+
636
+ for (let i = 0; i < ${warmup}; i++) {
637
+ // Warmup iteration
638
+ }
639
+
640
+ const testIters = ${testIters};
641
+ for (let i = 0; i < testIters; i++) {
642
+ if (i % Math.max(1, Math.floor(testIters / 5)) === 0) {
643
+ if (global.gc) global.gc();
644
+ console.log(\`[HEAP_SAMPLE] \${i}, \${process.memoryUsage().heapUsed}\`);
645
+ }
646
+ }
647
+
648
+ if (global.gc) global.gc();
649
+ const finalHeap = process.memoryUsage().heapUsed;
650
+ const delta = finalHeap - initial;
651
+ console.log(\`[HEAP_FINAL] \${finalHeap}\`);
652
+ console.log(\`[HEAP_DELTA] \${delta}\`);
653
+ `;
654
+ }
655
+ /**
656
+ * Runs a memory heap inspection script and extracts heap growth, leak indicators, and memory delta statistics.
657
+ */
658
+ export async function checkHeapDelta(workspaceRoot, code, language = 'node', config) {
659
+ let lang = 'node';
660
+ let opts;
661
+ if (typeof language === 'string') {
662
+ lang = language;
663
+ opts = config;
664
+ }
665
+ else if (typeof language === 'object' && language !== null) {
666
+ opts = language;
667
+ }
668
+ const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
669
+ const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
670
+ let initialHeapBytes = 0;
671
+ const startMatch = combinedOutput.match(/(?:\[HEAP_START\]|Heap start:)\s*(-?\d+)/i);
672
+ if (startMatch) {
673
+ initialHeapBytes = parseInt(startMatch[1], 10);
674
+ }
675
+ let finalHeapBytes = 0;
676
+ const finalMatch = combinedOutput.match(/(?:\[HEAP_FINAL\]|Heap final:)\s*(-?\d+)/i);
677
+ if (finalMatch) {
678
+ finalHeapBytes = parseInt(finalMatch[1], 10);
679
+ }
680
+ let deltaBytes = finalHeapBytes - initialHeapBytes;
681
+ const deltaMatch = combinedOutput.match(/(?:\[HEAP_DELTA\]|Heap delta:)\s*(-?\d+)/i);
682
+ if (deltaMatch) {
683
+ deltaBytes = parseInt(deltaMatch[1], 10);
684
+ }
685
+ const samples = [];
686
+ const sampleRegex = /(?:\[HEAP_SAMPLE\]|Sample\s+(\d+):?)\s*(\d+)(?:,\s*(\d+))?/gi;
687
+ let sMatch;
688
+ while ((sMatch = sampleRegex.exec(combinedOutput)) !== null) {
689
+ const iter = parseInt(sMatch[1], 10);
690
+ const bytes = sMatch[3] ? parseInt(sMatch[3], 10) : parseInt(sMatch[2], 10);
691
+ if (!isNaN(iter) && !isNaN(bytes)) {
692
+ samples.push({ iteration: iter, heapBytes: bytes });
693
+ }
694
+ }
695
+ let growthRateBytesPerIter;
696
+ if (samples.length >= 2) {
697
+ const first = samples[0];
698
+ const last = samples[samples.length - 1];
699
+ const iterDiff = last.iteration - first.iteration;
700
+ if (iterDiff > 0) {
701
+ growthRateBytesPerIter = (last.heapBytes - first.heapBytes) / iterDiff;
702
+ }
703
+ }
704
+ else if (config?.testIterations && config.testIterations > 0) {
705
+ growthRateBytesPerIter = deltaBytes / config.testIterations;
706
+ }
707
+ const leakDetected = combinedOutput.includes('[LEAK_DETECTED]') ||
708
+ deltaBytes > (config?.warmupIterations ? 1_048_576 : 5_242_880) ||
709
+ (growthRateBytesPerIter !== undefined && growthRateBytesPerIter > 10_000);
710
+ return {
711
+ ...scriptResult,
712
+ initialHeapBytes,
713
+ finalHeapBytes,
714
+ deltaBytes,
715
+ leakDetected,
716
+ growthRateBytesPerIter,
717
+ samples: samples.length > 0 ? samples : undefined,
718
+ };
719
+ }
720
+ /**
721
+ * Helper to deep-compare JSON values with optional numerical tolerance.
722
+ */
723
+ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
724
+ if (val1 === val2)
725
+ return;
726
+ if (typeof val1 === 'number' && typeof val2 === 'number') {
727
+ if (Math.abs(val1 - val2) > tolerance) {
728
+ details.push({
729
+ field: pathStr,
730
+ baseline: val1,
731
+ candidate: val2,
732
+ diff: `Numeric difference exceeds tolerance (${tolerance}): |${val1} - ${val2}| = ${Math.abs(val1 - val2)}`,
733
+ });
734
+ }
735
+ return;
736
+ }
737
+ if (typeof val1 !== typeof val2 || val1 === null || val2 === null) {
738
+ details.push({
739
+ field: pathStr,
740
+ baseline: val1,
741
+ candidate: val2,
742
+ diff: `Type mismatch: baseline is ${typeof val1}, candidate is ${typeof val2}`,
743
+ });
744
+ return;
745
+ }
746
+ if (Array.isArray(val1) && Array.isArray(val2)) {
747
+ if (val1.length !== val2.length) {
748
+ details.push({
749
+ field: `${pathStr}.length`,
750
+ baseline: val1.length,
751
+ candidate: val2.length,
752
+ diff: `Array length mismatch: baseline length ${val1.length}, candidate length ${val2.length}`,
753
+ });
754
+ }
755
+ const minLen = Math.min(val1.length, val2.length);
756
+ for (let i = 0; i < minLen; i++) {
757
+ compareJsonValues(val1[i], val2[i], `${pathStr}[${i}]`, tolerance, details);
758
+ }
759
+ return;
760
+ }
761
+ if (typeof val1 === 'object' && typeof val2 === 'object') {
762
+ const keys1 = Object.keys(val1);
763
+ const keys2 = Object.keys(val2);
764
+ const allKeys = new Set([...keys1, ...keys2]);
765
+ for (const key of allKeys) {
766
+ const subPath = pathStr ? `${pathStr}.${key}` : key;
767
+ if (!(key in val1)) {
768
+ details.push({
769
+ field: subPath,
770
+ baseline: undefined,
771
+ candidate: val2[key],
772
+ diff: `Key "${key}" missing in baseline`,
773
+ });
774
+ }
775
+ else if (!(key in val2)) {
776
+ details.push({
777
+ field: subPath,
778
+ baseline: val1[key],
779
+ candidate: undefined,
780
+ diff: `Key "${key}" missing in candidate`,
781
+ });
782
+ }
783
+ else {
784
+ compareJsonValues(val1[key], val2[key], subPath, tolerance, details);
785
+ }
786
+ }
787
+ return;
788
+ }
789
+ details.push({
790
+ field: pathStr,
791
+ baseline: val1,
792
+ candidate: val2,
793
+ diff: `Value mismatch: baseline="${val1}", candidate="${val2}"`,
794
+ });
795
+ }
796
+ /**
797
+ * Computes a line-by-line diff string between baseline and candidate output strings.
798
+ */
799
+ function computeTextDiff(text1, text2) {
800
+ const lines1 = text1.split('\n');
801
+ const lines2 = text2.split('\n');
802
+ const diffs = [];
803
+ const maxLines = Math.max(lines1.length, lines2.length);
804
+ for (let i = 0; i < maxLines; i++) {
805
+ const l1 = lines1[i];
806
+ const l2 = lines2[i];
807
+ if (l1 !== l2) {
808
+ if (l1 === undefined) {
809
+ diffs.push(`+ Line ${i + 1}: ${l2}`);
810
+ }
811
+ else if (l2 === undefined) {
812
+ diffs.push(`- Line ${i + 1}: ${l1}`);
813
+ }
814
+ else {
815
+ diffs.push(`- Line ${i + 1}: ${l1}`);
816
+ diffs.push(`+ Line ${i + 1}: ${l2}`);
817
+ }
818
+ }
819
+ }
820
+ return diffs.join('\n');
821
+ }
822
+ /**
823
+ * Executes baseline and candidate scripts, comparing outputs and return values to detect behavioral drift.
824
+ */
825
+ export async function checkBehavioralDrift(workspaceRoot, baselineCode, candidateCode, language = 'node', config) {
826
+ let lang = 'node';
827
+ let opts;
828
+ if (typeof language === 'string') {
829
+ lang = language;
830
+ opts = config;
831
+ }
832
+ else if (typeof language === 'object' && language !== null) {
833
+ opts = language;
834
+ }
835
+ const [baselineResult, candidateResult] = await Promise.all([
836
+ runEphemeralScript(workspaceRoot, lang, baselineCode, opts),
837
+ runEphemeralScript(workspaceRoot, lang, candidateCode, opts),
838
+ ]);
839
+ const baselineOut = baselineResult.stdout.trim();
840
+ const candidateOut = candidateResult.stdout.trim();
841
+ const driftDetails = [];
842
+ const tolerance = config?.tolerance ?? 1e-6;
843
+ let isJsonComparison = false;
844
+ try {
845
+ const jsonBaseline = JSON.parse(baselineOut);
846
+ const jsonCandidate = JSON.parse(candidateOut);
847
+ isJsonComparison = true;
848
+ compareJsonValues(jsonBaseline, jsonCandidate, '', tolerance, driftDetails);
849
+ }
850
+ catch {
851
+ // Non-JSON comparison fallback
852
+ }
853
+ if (!isJsonComparison) {
854
+ if (baselineOut !== candidateOut) {
855
+ driftDetails.push({
856
+ baseline: baselineOut,
857
+ candidate: candidateOut,
858
+ diff: 'Plain text output mismatch',
859
+ });
860
+ }
861
+ }
862
+ if (baselineResult.exitCode !== candidateResult.exitCode) {
863
+ driftDetails.push({
864
+ field: 'exitCode',
865
+ baseline: baselineResult.exitCode,
866
+ candidate: candidateResult.exitCode,
867
+ diff: `Exit code mismatch: baseline exited with ${baselineResult.exitCode}, candidate with ${candidateResult.exitCode}`,
868
+ });
869
+ }
870
+ const hasDrift = driftDetails.length > 0;
871
+ const diffSummary = hasDrift ? computeTextDiff(baselineOut, candidateOut) : undefined;
872
+ const combinedStdout = `--- Baseline Stdout ---\n${baselineResult.stdout}\n--- Candidate Stdout ---\n${candidateResult.stdout}`;
873
+ const combinedStderr = `--- Baseline Stderr ---\n${baselineResult.stderr}\n--- Candidate Stderr ---\n${candidateResult.stderr}`;
874
+ return {
875
+ stdout: combinedStdout,
876
+ stderr: combinedStderr,
877
+ exitCode: hasDrift ? 1 : 0,
878
+ hasDrift,
879
+ baselineOutput: baselineOut,
880
+ candidateOutput: candidateOut,
881
+ diffSummary,
882
+ driftDetails: driftDetails.length > 0 ? driftDetails : undefined,
883
+ };
884
+ }
@@ -7,7 +7,8 @@ import path from 'node:path';
7
7
  */
8
8
  export async function atomicWriteFile(targetPath, data, encoding = 'utf-8') {
9
9
  const dir = path.dirname(targetPath);
10
- const tempPath = path.join(dir, `.${path.basename(targetPath)}.${Date.now()}.tmp`);
10
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
11
+ const tempPath = path.join(dir, `.${path.basename(targetPath)}.${Date.now()}.${randomSuffix}.tmp`);
11
12
  try {
12
13
  // Ensure the directory exists
13
14
  await fs.mkdir(dir, { recursive: true });