minovative-mind-cli 2.7.0 → 2.8.1
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 +1 -1
- package/dist/services/agent/slashCommands.js +12 -5
- package/dist/services/agent/toolLoop.js +52 -2
- package/dist/services/agent-tools.d.ts +26 -0
- package/dist/services/agent-tools.js +255 -0
- package/dist/services/agent.js +10 -5
- package/dist/services/orchestration/investigationOrchestrator.js +3 -0
- package/dist/services/orchestration/scopedTools.js +26 -0
- package/dist/services/orchestration/subAgent.js +1 -1
- package/dist/services/proxyClient.js +7 -4
- package/dist/utils/analysisRunner.d.ts +65 -0
- package/dist/utils/analysisRunner.js +558 -0
- package/dist/utils/config.d.ts +13 -0
- package/dist/utils/config.js +13 -0
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +6 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -3
|
@@ -70,3 +70,68 @@ export declare function generatePBTScriptTemplate(language: string, targetFuncti
|
|
|
70
70
|
* Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
|
|
71
71
|
*/
|
|
72
72
|
export declare function runPropertyBasedTest(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, config?: PropertyTestConfig & EphemeralScriptOptions): Promise<PropertyTestResult>;
|
|
73
|
+
export interface FuzzProbeConfig {
|
|
74
|
+
iterations?: number;
|
|
75
|
+
maxInputLength?: number;
|
|
76
|
+
seed?: number;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface FuzzProbeResult extends EphemeralScriptResult {
|
|
80
|
+
passed: boolean;
|
|
81
|
+
totalFuzzRuns: number;
|
|
82
|
+
failingInputs?: string[];
|
|
83
|
+
errorSummary?: string;
|
|
84
|
+
seed?: number;
|
|
85
|
+
}
|
|
86
|
+
export interface HeapDeltaConfig {
|
|
87
|
+
warmupIterations?: number;
|
|
88
|
+
testIterations?: number;
|
|
89
|
+
samplingIntervalMs?: number;
|
|
90
|
+
}
|
|
91
|
+
export interface HeapDeltaResult extends EphemeralScriptResult {
|
|
92
|
+
initialHeapBytes: number;
|
|
93
|
+
finalHeapBytes: number;
|
|
94
|
+
deltaBytes: number;
|
|
95
|
+
leakDetected: boolean;
|
|
96
|
+
growthRateBytesPerIter?: number;
|
|
97
|
+
samples?: Array<{
|
|
98
|
+
iteration: number;
|
|
99
|
+
heapBytes: number;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
export interface BehavioralDriftConfig {
|
|
103
|
+
tolerance?: number;
|
|
104
|
+
compareKeys?: string[];
|
|
105
|
+
}
|
|
106
|
+
export interface BehavioralDriftResult extends EphemeralScriptResult {
|
|
107
|
+
hasDrift: boolean;
|
|
108
|
+
baselineOutput?: string;
|
|
109
|
+
candidateOutput?: string;
|
|
110
|
+
diffSummary?: string;
|
|
111
|
+
driftDetails?: Array<{
|
|
112
|
+
field?: string;
|
|
113
|
+
baseline: any;
|
|
114
|
+
candidate: any;
|
|
115
|
+
diff?: string;
|
|
116
|
+
}>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Generates an ephemeral Fuzz Probe script template tailored to the target language.
|
|
120
|
+
*/
|
|
121
|
+
export declare function generateFuzzScriptTemplate(language: string, targetFeature: string, config?: FuzzProbeConfig): string;
|
|
122
|
+
/**
|
|
123
|
+
* Runs a fuzz probing analysis script and parses failure counterexamples and crash statistics.
|
|
124
|
+
*/
|
|
125
|
+
export declare function runFuzzProbe(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, config?: FuzzProbeConfig & EphemeralScriptOptions): Promise<FuzzProbeResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Generates an ephemeral memory heap analysis script template tailored to the target language.
|
|
128
|
+
*/
|
|
129
|
+
export declare function generateHeapCheckScriptTemplate(language: string, targetCode: string, config?: HeapDeltaConfig): string;
|
|
130
|
+
/**
|
|
131
|
+
* Runs a memory heap inspection script and extracts heap growth, leak indicators, and memory delta statistics.
|
|
132
|
+
*/
|
|
133
|
+
export declare function checkHeapDelta(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, config?: HeapDeltaConfig & EphemeralScriptOptions): Promise<HeapDeltaResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Executes baseline and candidate scripts, comparing outputs and return values to detect behavioral drift.
|
|
136
|
+
*/
|
|
137
|
+
export declare function checkBehavioralDrift(workspaceRoot: string, baselineCode: string, candidateCode: string, language?: string | EphemeralScriptOptions, config?: BehavioralDriftConfig & EphemeralScriptOptions): Promise<BehavioralDriftResult>;
|
|
@@ -324,3 +324,561 @@ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node
|
|
|
324
324
|
numRunsCompleted,
|
|
325
325
|
};
|
|
326
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
|
+
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -25,6 +25,19 @@ export declare const GEMINI_MODELS: {
|
|
|
25
25
|
export declare const DEFAULT_MODEL: "auto";
|
|
26
26
|
/** Maximum tokens the model can output per response. */
|
|
27
27
|
export declare const MAX_OUTPUT_TOKENS = 60000;
|
|
28
|
+
/**
|
|
29
|
+
* Rate limit & Tokens-Per-Minute (TPM) cooling-off delays (in milliseconds).
|
|
30
|
+
* Prevents transient 429 / 503 rate-limit errors by giving the API 60-second
|
|
31
|
+
* rolling token window time to settle during key agent transitions.
|
|
32
|
+
*/
|
|
33
|
+
export declare const TPM_COOLING_DELAYS: {
|
|
34
|
+
/** Pause before running initial intent routing between turns */
|
|
35
|
+
readonly INTER_TURN_MS: 1000;
|
|
36
|
+
/** Pause before dispatching parallel sub-agent investigations simultaneously */
|
|
37
|
+
readonly PARALLEL_DISPATCH_MS: 1000;
|
|
38
|
+
/** Pause after context gathering completes before starting the execution stream */
|
|
39
|
+
readonly POST_INVESTIGATION_MS: 3000;
|
|
40
|
+
};
|
|
28
41
|
/**
|
|
29
42
|
* Checks if BYOK is currently enabled for the user.
|
|
30
43
|
*/
|
package/dist/utils/config.js
CHANGED
|
@@ -25,6 +25,19 @@ export const GEMINI_MODELS = {
|
|
|
25
25
|
export const DEFAULT_MODEL = GEMINI_MODELS.AUTO;
|
|
26
26
|
/** Maximum tokens the model can output per response. */
|
|
27
27
|
export const MAX_OUTPUT_TOKENS = 60_000;
|
|
28
|
+
/**
|
|
29
|
+
* Rate limit & Tokens-Per-Minute (TPM) cooling-off delays (in milliseconds).
|
|
30
|
+
* Prevents transient 429 / 503 rate-limit errors by giving the API 60-second
|
|
31
|
+
* rolling token window time to settle during key agent transitions.
|
|
32
|
+
*/
|
|
33
|
+
export const TPM_COOLING_DELAYS = {
|
|
34
|
+
/** Pause before running initial intent routing between turns */
|
|
35
|
+
INTER_TURN_MS: 1000,
|
|
36
|
+
/** Pause before dispatching parallel sub-agent investigations simultaneously */
|
|
37
|
+
PARALLEL_DISPATCH_MS: 1000,
|
|
38
|
+
/** Pause after context gathering completes before starting the execution stream */
|
|
39
|
+
POST_INVESTIGATION_MS: 3000,
|
|
40
|
+
};
|
|
28
41
|
/**
|
|
29
42
|
* Checks if BYOK is currently enabled for the user.
|
|
30
43
|
*/
|