minovative-mind-cli 2.14.1 → 2.14.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.
- package/README.md +40 -59
- package/dist/services/agent/slashCommands.js +144 -13
- package/dist/services/agent/toolLoop.js +11 -2
- package/dist/services/agent/types.d.ts +9 -2
- package/dist/services/agent-tools.d.ts +20 -1
- package/dist/services/agent-tools.js +272 -47
- package/dist/services/agent.js +43 -18
- package/dist/services/ai.d.ts +9 -0
- package/dist/services/ai.js +71 -21
- package/dist/services/chatHistoryService.d.ts +5 -0
- package/dist/services/contextAgent.d.ts +14 -6
- package/dist/services/contextAgent.js +36 -10
- package/dist/services/orchestration/investigationAgent.js +31 -7
- package/dist/services/orchestration/investigationCache.js +19 -9
- package/dist/services/orchestration/readCache.d.ts +1 -0
- package/dist/services/orchestration/readCache.js +5 -2
- package/dist/services/orchestration/scopedTools.js +37 -10
- package/dist/services/orchestration/subAgent.js +16 -2
- package/dist/services/proxyClient.d.ts +6 -0
- package/dist/services/proxyClient.js +24 -10
- package/dist/services/sessionSettings.d.ts +66 -0
- package/dist/services/sessionSettings.js +126 -0
- package/dist/services/userProfileService.d.ts +14 -0
- package/dist/services/userProfileService.js +105 -3
- package/dist/services/verificationService.js +24 -2
- package/dist/utils/analysisRunner.d.ts +120 -8
- package/dist/utils/analysisRunner.js +946 -125
- package/dist/utils/antiCheatingGuard.d.ts +21 -0
- package/dist/utils/antiCheatingGuard.js +554 -0
- package/dist/utils/contextPrompts.d.ts +39 -0
- package/dist/utils/contextPrompts.js +81 -9
- package/dist/utils/contextRanker.d.ts +216 -0
- package/dist/utils/contextRanker.js +603 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
- package/dist/utils/dependencyTracer/modules/graph.js +11 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
- package/dist/utils/dependencyTracer.d.ts +40 -2
- package/dist/utils/dependencyTracer.js +95 -3
- package/dist/utils/fileReadCache.d.ts +58 -0
- package/dist/utils/fileReadCache.js +162 -0
- package/dist/utils/projectStorage.js +2 -1
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +111 -15
- package/dist/utils/systemPrompts.d.ts +4 -3
- package/dist/utils/systemPrompts.js +66 -8
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import {
|
|
5
|
-
import { promisify } from 'node:util';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
6
5
|
import { createHash } from 'node:crypto';
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
6
|
/** Supported language aliases map. */
|
|
9
7
|
const LANGUAGE_ALIASES = {
|
|
10
8
|
js: 'node',
|
|
@@ -56,6 +54,85 @@ export const SUPPORTED_LANGUAGES = [
|
|
|
56
54
|
'php',
|
|
57
55
|
'java',
|
|
58
56
|
];
|
|
57
|
+
/**
|
|
58
|
+
* High-performance Least-Recently-Used (LRU) environment and runner cache with per-item TTL.
|
|
59
|
+
* Prevents repetitive filesystem traversal and binary probing across ephemeral executions.
|
|
60
|
+
*/
|
|
61
|
+
export class EnvironmentLRUCache {
|
|
62
|
+
cache = new Map();
|
|
63
|
+
max;
|
|
64
|
+
ttlMs;
|
|
65
|
+
constructor(max = 100, ttlMs = 30_000) {
|
|
66
|
+
this.max = max;
|
|
67
|
+
this.ttlMs = ttlMs;
|
|
68
|
+
}
|
|
69
|
+
get size() {
|
|
70
|
+
return this.cache.size;
|
|
71
|
+
}
|
|
72
|
+
clear() {
|
|
73
|
+
this.cache.clear();
|
|
74
|
+
}
|
|
75
|
+
delete(key) {
|
|
76
|
+
return this.cache.delete(key);
|
|
77
|
+
}
|
|
78
|
+
get(key) {
|
|
79
|
+
const entry = this.cache.get(key);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (Date.now() > entry.expiresAt) {
|
|
83
|
+
this.cache.delete(key);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
// Refresh LRU order (delete & re-insert)
|
|
87
|
+
this.cache.delete(key);
|
|
88
|
+
this.cache.set(key, entry);
|
|
89
|
+
return entry.value;
|
|
90
|
+
}
|
|
91
|
+
has(key) {
|
|
92
|
+
return this.get(key) !== undefined;
|
|
93
|
+
}
|
|
94
|
+
set(key, value, ttlMs) {
|
|
95
|
+
if (this.cache.has(key)) {
|
|
96
|
+
this.cache.delete(key);
|
|
97
|
+
}
|
|
98
|
+
else if (this.cache.size >= this.max) {
|
|
99
|
+
const oldestKey = this.cache.keys().next().value;
|
|
100
|
+
if (oldestKey !== undefined) {
|
|
101
|
+
this.cache.delete(oldestKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.cache.set(key, {
|
|
105
|
+
expiresAt: Date.now() + (ttlMs ?? this.ttlMs),
|
|
106
|
+
value,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Global LRU cache instances for workspace runner probes
|
|
111
|
+
const tsRunnerCache = new EnvironmentLRUCache(100, 30_000);
|
|
112
|
+
const pyEnvCache = new EnvironmentLRUCache(100, 30_000);
|
|
113
|
+
const moduleTypeCache = new EnvironmentLRUCache(100, 30_000);
|
|
114
|
+
const projectRuntimeCache = new EnvironmentLRUCache(100, 30_000);
|
|
115
|
+
/**
|
|
116
|
+
* Clears cached environment and runner resolution data.
|
|
117
|
+
* If workspaceRoot is provided, invalidates cache entries for that workspace;
|
|
118
|
+
* otherwise, clears all cached runner environments globally.
|
|
119
|
+
*
|
|
120
|
+
* @param workspaceRoot - Optional workspace root directory to invalidate.
|
|
121
|
+
*/
|
|
122
|
+
export function clearRunnerCache(workspaceRoot) {
|
|
123
|
+
if (workspaceRoot) {
|
|
124
|
+
tsRunnerCache.delete(workspaceRoot);
|
|
125
|
+
pyEnvCache.delete(workspaceRoot);
|
|
126
|
+
moduleTypeCache.delete(workspaceRoot);
|
|
127
|
+
projectRuntimeCache.delete(workspaceRoot);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
tsRunnerCache.clear();
|
|
131
|
+
pyEnvCache.clear();
|
|
132
|
+
moduleTypeCache.clear();
|
|
133
|
+
projectRuntimeCache.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
59
136
|
/**
|
|
60
137
|
* Normalizes user/AI provided language string to a standard runtime identifier.
|
|
61
138
|
*/
|
|
@@ -141,11 +218,15 @@ export function detectLanguageFromCode(code) {
|
|
|
141
218
|
}
|
|
142
219
|
/**
|
|
143
220
|
* Detects the dominant programming language / runtime for a workspace based on project manifest files.
|
|
221
|
+
* Caches results per workspace root via LRU cache.
|
|
144
222
|
*
|
|
145
223
|
* @param workspaceRoot - Path to the workspace root directory.
|
|
146
224
|
* @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
|
|
147
225
|
*/
|
|
148
226
|
export async function detectProjectRuntime(workspaceRoot) {
|
|
227
|
+
const cached = projectRuntimeCache.get(workspaceRoot);
|
|
228
|
+
if (cached)
|
|
229
|
+
return cached;
|
|
149
230
|
const probeFiles = [
|
|
150
231
|
{ file: 'Cargo.toml', runtime: 'rust' },
|
|
151
232
|
{ file: 'go.mod', runtime: 'go' },
|
|
@@ -164,12 +245,14 @@ export async function detectProjectRuntime(workspaceRoot) {
|
|
|
164
245
|
for (const { file, runtime } of probeFiles) {
|
|
165
246
|
try {
|
|
166
247
|
await fs.access(path.join(workspaceRoot, file));
|
|
248
|
+
projectRuntimeCache.set(workspaceRoot, runtime);
|
|
167
249
|
return runtime;
|
|
168
250
|
}
|
|
169
251
|
catch {
|
|
170
252
|
// Continue searching
|
|
171
253
|
}
|
|
172
254
|
}
|
|
255
|
+
projectRuntimeCache.set(workspaceRoot, 'node');
|
|
173
256
|
return 'node';
|
|
174
257
|
}
|
|
175
258
|
/**
|
|
@@ -194,16 +277,22 @@ export async function resolveEffectiveRuntime(workspaceRoot, language, code) {
|
|
|
194
277
|
}
|
|
195
278
|
/**
|
|
196
279
|
* Detects whether the workspace package.json specifies `"type": "module"`.
|
|
197
|
-
* Returns `'module'` or `'commonjs'`.
|
|
280
|
+
* Returns `'module'` or `'commonjs'`. Caches results per workspace root via LRU cache.
|
|
198
281
|
*/
|
|
199
282
|
export async function detectWorkspaceModuleType(workspaceRoot) {
|
|
283
|
+
const cached = moduleTypeCache.get(workspaceRoot);
|
|
284
|
+
if (cached)
|
|
285
|
+
return cached;
|
|
200
286
|
try {
|
|
201
287
|
const pkgPath = path.join(workspaceRoot, 'package.json');
|
|
202
288
|
const content = await fs.readFile(pkgPath, 'utf-8');
|
|
203
289
|
const pkg = JSON.parse(content);
|
|
204
|
-
|
|
290
|
+
const result = pkg.type === 'module' ? 'module' : 'commonjs';
|
|
291
|
+
moduleTypeCache.set(workspaceRoot, result);
|
|
292
|
+
return result;
|
|
205
293
|
}
|
|
206
294
|
catch {
|
|
295
|
+
moduleTypeCache.set(workspaceRoot, 'commonjs');
|
|
207
296
|
return 'commonjs';
|
|
208
297
|
}
|
|
209
298
|
}
|
|
@@ -254,44 +343,574 @@ function buildTempPath(code, ext) {
|
|
|
254
343
|
return path.join(os.tmpdir(), `.mino-analysis-${hash}-${nonce}${ext}`);
|
|
255
344
|
}
|
|
256
345
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
346
|
+
* Probes the workspace for available TypeScript runners (tsx, ts-node-esm, ts-node)
|
|
347
|
+
* and resolves tsconfig.json configuration for path aliases.
|
|
348
|
+
* Caches results per workspace root via LRU cache.
|
|
349
|
+
*
|
|
350
|
+
* @param workspaceRoot - Path to the root directory of the workspace.
|
|
351
|
+
* @returns Runner info with executable command and tsconfig path if available.
|
|
352
|
+
*/
|
|
353
|
+
export async function findTypeScriptRunner(workspaceRoot) {
|
|
354
|
+
const cached = tsRunnerCache.get(workspaceRoot);
|
|
355
|
+
if (cached)
|
|
356
|
+
return cached;
|
|
357
|
+
const binDir = path.join(workspaceRoot, 'node_modules', '.bin');
|
|
358
|
+
const isWin = os.platform() === 'win32';
|
|
359
|
+
const tsconfigPath = path.join(workspaceRoot, 'tsconfig.json');
|
|
360
|
+
let hasTsconfig = false;
|
|
361
|
+
try {
|
|
362
|
+
await fs.access(tsconfigPath);
|
|
363
|
+
hasTsconfig = true;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// tsconfig not present in root
|
|
367
|
+
}
|
|
368
|
+
// 1. Prefer local workspace tsx if installed (fastest, native ESM/CJS and tsconfig paths support)
|
|
369
|
+
const tsxBin = path.join(binDir, isWin ? 'tsx.cmd' : 'tsx');
|
|
370
|
+
try {
|
|
371
|
+
await fs.access(tsxBin);
|
|
372
|
+
const result = {
|
|
373
|
+
runnerType: 'local-tsx',
|
|
374
|
+
command: `"${tsxBin}"`,
|
|
375
|
+
binaryPath: tsxBin,
|
|
376
|
+
tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
|
|
377
|
+
};
|
|
378
|
+
tsRunnerCache.set(workspaceRoot, result);
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Continue probing
|
|
383
|
+
}
|
|
384
|
+
// 2. Check workspace module type for ts-node variant
|
|
385
|
+
const moduleType = await detectWorkspaceModuleType(workspaceRoot);
|
|
386
|
+
if (moduleType === 'module') {
|
|
387
|
+
const tsNodeEsmBin = path.join(binDir, isWin ? 'ts-node-esm.cmd' : 'ts-node-esm');
|
|
388
|
+
try {
|
|
389
|
+
await fs.access(tsNodeEsmBin);
|
|
390
|
+
const result = {
|
|
391
|
+
runnerType: 'local-ts-node-esm',
|
|
392
|
+
command: `"${tsNodeEsmBin}"`,
|
|
393
|
+
binaryPath: tsNodeEsmBin,
|
|
394
|
+
tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
|
|
395
|
+
};
|
|
396
|
+
tsRunnerCache.set(workspaceRoot, result);
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Continue probing
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// 3. Check local ts-node binary
|
|
404
|
+
const tsNodeBin = path.join(binDir, isWin ? 'ts-node.cmd' : 'ts-node');
|
|
405
|
+
try {
|
|
406
|
+
await fs.access(tsNodeBin);
|
|
407
|
+
const extraFlag = moduleType === 'module' ? '--esm ' : '';
|
|
408
|
+
const result = {
|
|
409
|
+
runnerType: 'local-ts-node',
|
|
410
|
+
command: `"${tsNodeBin}" ${extraFlag}`.trim(),
|
|
411
|
+
binaryPath: tsNodeBin,
|
|
412
|
+
tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
|
|
413
|
+
};
|
|
414
|
+
tsRunnerCache.set(workspaceRoot, result);
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Continue probing
|
|
419
|
+
}
|
|
420
|
+
// 4. Default fallback: npx tsx (preferred) or npx ts-node
|
|
421
|
+
const result = {
|
|
422
|
+
runnerType: 'npx-tsx',
|
|
423
|
+
command: 'npx tsx',
|
|
424
|
+
binaryPath: 'npx',
|
|
425
|
+
tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
|
|
426
|
+
};
|
|
427
|
+
tsRunnerCache.set(workspaceRoot, result);
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Probes the workspace for virtual environments (.venv, venv, env, .env, virtualenv)
|
|
432
|
+
* and resolves the appropriate Python binary path and environment directories.
|
|
433
|
+
* Caches results per workspace root via LRU cache.
|
|
434
|
+
*
|
|
435
|
+
* @param workspaceRoot - Path to workspace root directory.
|
|
436
|
+
* @returns Detected Python interpreter binary and virtual environment metadata.
|
|
259
437
|
*/
|
|
260
|
-
function
|
|
438
|
+
export async function findPythonBinary(workspaceRoot) {
|
|
439
|
+
const cached = pyEnvCache.get(workspaceRoot);
|
|
440
|
+
if (cached)
|
|
441
|
+
return cached;
|
|
442
|
+
const isWin = os.platform() === 'win32';
|
|
443
|
+
const candidateDirs = ['.venv', 'venv', 'env', '.env', 'virtualenv', '.virtualenv'];
|
|
444
|
+
for (const dir of candidateDirs) {
|
|
445
|
+
const venvDir = path.join(workspaceRoot, dir);
|
|
446
|
+
const probeBinaries = isWin
|
|
447
|
+
? [
|
|
448
|
+
path.join(venvDir, 'Scripts', 'python.exe'),
|
|
449
|
+
path.join(venvDir, 'python.exe'),
|
|
450
|
+
path.join(venvDir, 'Scripts', 'python.cmd'),
|
|
451
|
+
]
|
|
452
|
+
: [
|
|
453
|
+
path.join(venvDir, 'bin', 'python'),
|
|
454
|
+
path.join(venvDir, 'bin', 'python3'),
|
|
455
|
+
path.join(venvDir, 'bin', 'python.exe'),
|
|
456
|
+
];
|
|
457
|
+
for (const binPath of probeBinaries) {
|
|
458
|
+
try {
|
|
459
|
+
await fs.access(binPath);
|
|
460
|
+
const result = {
|
|
461
|
+
pythonBin: binPath,
|
|
462
|
+
isVenv: true,
|
|
463
|
+
venvDir,
|
|
464
|
+
binDir: path.dirname(binPath),
|
|
465
|
+
};
|
|
466
|
+
pyEnvCache.set(workspaceRoot, result);
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Continue probing
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const fallback = {
|
|
475
|
+
pythonBin: 'python3',
|
|
476
|
+
isVenv: false,
|
|
477
|
+
};
|
|
478
|
+
pyEnvCache.set(workspaceRoot, fallback);
|
|
479
|
+
return fallback;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Injects sandbox query helpers into user/agent script source code.
|
|
483
|
+
* Provides preloaded `emitResult` / `__emitResult` (structured JSON emission via dedicated FD-3 with stdout fallback)
|
|
484
|
+
* and `inspectSymbols` / `inspectObject` (reflection & AST property analysis).
|
|
485
|
+
*
|
|
486
|
+
* Preserves 1:1 source line offsets so compiler/runtime stack traces and syntax diagnostics
|
|
487
|
+
* accurately align with original source lines.
|
|
488
|
+
*
|
|
489
|
+
* @param code - Raw source code of the script.
|
|
490
|
+
* @param language - Target runtime language.
|
|
491
|
+
* @returns Source code augmented with runtime-specific helper preambles.
|
|
492
|
+
*/
|
|
493
|
+
export function injectSandboxHelpers(code, language) {
|
|
261
494
|
const normLang = normalizeLanguage(language);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const binPath = scriptPath.replace(/\.rs$/, binExt);
|
|
276
|
-
return `rustc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
495
|
+
if (code.includes('__MINO_PREAMBLE_START__')) {
|
|
496
|
+
return code;
|
|
497
|
+
}
|
|
498
|
+
if (normLang === 'node' || normLang === 'ts-node') {
|
|
499
|
+
// Single-line preamble preserving exact 1:1 line offsets
|
|
500
|
+
const preamble = `/* __MINO_PREAMBLE_START__ */ if (typeof globalThis.emitResult === 'undefined') { globalThis.emitResult = (data) => { const payload = typeof data === 'string' ? data : JSON.stringify(data); try { const _fs = typeof require !== 'undefined' ? require('node:fs') : (typeof process !== 'undefined' && process.getBuiltinModule ? process.getBuiltinModule('node:fs') : null); if (_fs && _fs.writeSync) { _fs.writeSync(3, payload + '\\n'); return; } } catch {} console.log('__MINO_RESULT__' + payload + '__MINO_RESULT__'); }; globalThis.__emitResult = globalThis.emitResult; } if (typeof globalThis.inspectSymbols === 'undefined') { globalThis.inspectSymbols = (target) => { if (target === null || target === undefined) return []; const symbols = []; const seen = new Set(); let curr = target; let depth = 0; while (curr && depth < 2) { for (const key of Object.getOwnPropertyNames(curr)) { if (key === 'constructor' && depth > 0) continue; if (seen.has(key)) continue; seen.add(key); let val; let type = 'unknown'; let isFunc = false; try { val = curr[key]; type = typeof val; isFunc = type === 'function'; } catch { type = 'inaccessible'; } symbols.push({ name: key, type, isFunction: isFunc, inherited: depth > 0 }); } curr = Object.getPrototypeOf(curr); depth++; if (curr === Object.prototype) break; } return symbols; }; globalThis.inspectObject = globalThis.inspectSymbols; } /* __MINO_PREAMBLE_END__ */ `;
|
|
501
|
+
if (code.startsWith('#!')) {
|
|
502
|
+
const firstLineEnd = code.indexOf('\n');
|
|
503
|
+
if (firstLineEnd !== -1) {
|
|
504
|
+
const shebang = code.slice(0, firstLineEnd + 1);
|
|
505
|
+
const rest = code.slice(firstLineEnd + 1);
|
|
506
|
+
return shebang + preamble + rest;
|
|
507
|
+
}
|
|
277
508
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
509
|
+
return preamble + code;
|
|
510
|
+
}
|
|
511
|
+
if (normLang === 'python') {
|
|
512
|
+
const preamble = `# __MINO_PREAMBLE_START__
|
|
513
|
+
import json as _mino_json
|
|
514
|
+
import inspect as _mino_inspect
|
|
515
|
+
import os as _mino_os
|
|
516
|
+
|
|
517
|
+
def emitResult(data):
|
|
518
|
+
payload = data if isinstance(data, str) else _mino_json.dumps(data)
|
|
519
|
+
try:
|
|
520
|
+
_mino_os.write(3, (payload + '\\n').encode('utf-8'))
|
|
521
|
+
except Exception:
|
|
522
|
+
print(f"__MINO_RESULT__{payload}__MINO_RESULT__")
|
|
523
|
+
|
|
524
|
+
__emitResult = emitResult
|
|
525
|
+
|
|
526
|
+
def inspect_symbols(target):
|
|
527
|
+
symbols = []
|
|
528
|
+
if target is None:
|
|
529
|
+
return symbols
|
|
530
|
+
for name in dir(target):
|
|
531
|
+
if name.startswith('__') and name.endswith('__'):
|
|
532
|
+
continue
|
|
533
|
+
try:
|
|
534
|
+
val = getattr(target, name)
|
|
535
|
+
is_fn = _mino_inspect.isfunction(val) or _mino_inspect.ismethod(val) or _mino_inspect.isbuiltin(val)
|
|
536
|
+
is_cls = _mino_inspect.isclass(val)
|
|
537
|
+
t_name = type(val).__name__
|
|
538
|
+
symbols.append({
|
|
539
|
+
"name": name,
|
|
540
|
+
"type": t_name,
|
|
541
|
+
"is_callable": callable(val),
|
|
542
|
+
"is_function": is_fn,
|
|
543
|
+
"is_class": is_cls
|
|
544
|
+
})
|
|
545
|
+
except Exception:
|
|
546
|
+
symbols.append({"name": name, "type": "unknown", "is_callable": False})
|
|
547
|
+
return symbols
|
|
548
|
+
|
|
549
|
+
inspectSymbols = inspect_symbols
|
|
550
|
+
inspect_object = inspect_symbols
|
|
551
|
+
inspectObject = inspect_symbols
|
|
552
|
+
# __MINO_PREAMBLE_END__\n`;
|
|
553
|
+
const lines = code.split('\n');
|
|
554
|
+
const headerLines = [];
|
|
555
|
+
const restLines = [];
|
|
556
|
+
let inHeader = true;
|
|
557
|
+
for (const line of lines) {
|
|
558
|
+
const stripped = line.trim();
|
|
559
|
+
if (inHeader &&
|
|
560
|
+
(stripped.startsWith('#') ||
|
|
561
|
+
stripped.startsWith('from __future__') ||
|
|
562
|
+
stripped === '')) {
|
|
563
|
+
headerLines.push(line);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
inHeader = false;
|
|
567
|
+
restLines.push(line);
|
|
568
|
+
}
|
|
281
569
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return `g++ -std=c++17 "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
570
|
+
if (headerLines.length > 0) {
|
|
571
|
+
return headerLines.join('\n') + '\n' + preamble + restLines.join('\n');
|
|
285
572
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
573
|
+
return preamble + code;
|
|
574
|
+
}
|
|
575
|
+
return code;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Parses universal structured result markers (__MINO_RESULT__) from stdout stream.
|
|
579
|
+
* Returns the cleaned human-readable stdout and the parsed structured result object/primitive.
|
|
580
|
+
*
|
|
581
|
+
* @param stdout - Captured standard output text.
|
|
582
|
+
*/
|
|
583
|
+
export function parseStructuredResult(stdout) {
|
|
584
|
+
const resultRegex = /__MINO_RESULT__([\s\S]*?)__MINO_RESULT__/g;
|
|
585
|
+
let match;
|
|
586
|
+
let lastPayload;
|
|
587
|
+
while ((match = resultRegex.exec(stdout)) !== null) {
|
|
588
|
+
lastPayload = match[1].trim();
|
|
589
|
+
}
|
|
590
|
+
let structuredResult;
|
|
591
|
+
if (lastPayload !== undefined) {
|
|
592
|
+
try {
|
|
593
|
+
structuredResult = JSON.parse(lastPayload);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
structuredResult = lastPayload;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const cleanedStdout = stdout
|
|
600
|
+
.replace(/__MINO_RESULT__[\s\S]*?__MINO_RESULT__/g, '')
|
|
601
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
602
|
+
.trim();
|
|
603
|
+
return { cleanedStdout, structuredResult };
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Diagnostic-first error middleware for sandbox script execution.
|
|
607
|
+
* Intercepts common sandbox execution traps (module resolution, ESM/CJS mismatch,
|
|
608
|
+
* Python import errors, TypeScript compile failures, linker errors) and produces
|
|
609
|
+
* actionable remediation advisories.
|
|
610
|
+
*
|
|
611
|
+
* @param stderr - Standard error output.
|
|
612
|
+
* @param stdout - Standard output text.
|
|
613
|
+
* @param language - Runtime language identifier.
|
|
614
|
+
* @param workspaceRoot - Path to workspace root directory.
|
|
615
|
+
* @returns Formatted diagnostic advisory string or null if no matching pattern.
|
|
616
|
+
*/
|
|
617
|
+
export function diagnoseSandboxError(stderr, stdout, language, workspaceRoot) {
|
|
618
|
+
const combined = `${stderr}\n${stdout}`;
|
|
619
|
+
const diagnostics = [];
|
|
620
|
+
// 1. Module not found (Node/TypeScript)
|
|
621
|
+
if (/ERR_MODULE_NOT_FOUND|Cannot find module\s+'([^']+)'|Cannot find package\s+'([^']+)'/i.test(combined)) {
|
|
622
|
+
const match = combined.match(/Cannot find (?:module|package)\s+'([^']+)'/i);
|
|
623
|
+
const mod = match ? match[1] : 'the requested package';
|
|
624
|
+
diagnostics.push(`• Cause: Module resolution failure for '${mod}'.\n` +
|
|
625
|
+
`• Action: Ensure '${mod}' is installed in workspace node_modules (e.g. npm install ${mod}).\n` +
|
|
626
|
+
` If referencing a local file, ensure relative path and extension (.js, .ts, .mjs) are correct.\n` +
|
|
627
|
+
` Note: NODE_PATH is configured to search ${workspaceRoot ? path.join(workspaceRoot, 'node_modules') : 'workspace node_modules'}.`);
|
|
628
|
+
}
|
|
629
|
+
// 2. ESM / CommonJS mismatch
|
|
630
|
+
if (/ERR_REQUIRE_ESM|Must use import to load ES Module|require\(\) of ES Module/i.test(combined)) {
|
|
631
|
+
diagnostics.push(`• Cause: CommonJS require() attempted to load an ES Module.\n` +
|
|
632
|
+
`• Action: Use dynamic import(...) or standard 'import ... from ...' syntax with .mjs or tsx runtime.`);
|
|
633
|
+
}
|
|
634
|
+
else if (/Cannot use import statement outside a module/i.test(combined)) {
|
|
635
|
+
diagnostics.push(`• Cause: Static ES Module import used in CommonJS execution context.\n` +
|
|
636
|
+
`• Action: Rename script to use .mjs or execute with ts-node/tsx runtime.`);
|
|
637
|
+
}
|
|
638
|
+
// 3. Python ModuleNotFoundError / ImportError
|
|
639
|
+
if (/ModuleNotFoundError:\s*No module named\s+'([^']+)'|ImportError:\s*(?:cannot import name\s+'([^']+)'|No module named\s+'([^']+)')/i.test(combined)) {
|
|
640
|
+
const match = combined.match(/No module named\s+'([^']+)'|cannot import name\s+'([^']+)'/i);
|
|
641
|
+
const mod = match ? match[1] || match[2] : 'requested module';
|
|
642
|
+
diagnostics.push(`• Cause: Python module '${mod}' could not be imported.\n` +
|
|
643
|
+
`• Action: Verify that '${mod}' is installed in your virtual environment (.venv / venv) or requirements.txt.\n` +
|
|
644
|
+
` Note: PYTHONPATH is pre-configured to include workspace root and src directory.`);
|
|
645
|
+
}
|
|
646
|
+
// 4. TypeScript compiler error
|
|
647
|
+
if (/error TS\d{4}:|TS\d{4}:/i.test(combined)) {
|
|
648
|
+
const tsCode = combined.match(/TS(\d{4})/i)?.[0] || 'TS Error';
|
|
649
|
+
diagnostics.push(`• Cause: TypeScript compiler reported an issue (${tsCode}).\n` +
|
|
650
|
+
`• Action: Verify type definitions and tsconfig.json path mappings.\n` +
|
|
651
|
+
` For ephemeral debugging scripts, add '// @ts-nocheck' at the top of the script to bypass static type checks.`);
|
|
652
|
+
}
|
|
653
|
+
// 5. Rust crate / import errors
|
|
654
|
+
if (/error\[E0432\]|error\[E0463\]|can't find crate/i.test(combined)) {
|
|
655
|
+
diagnostics.push(`• Cause: Rust crate or unresolved import.\n` +
|
|
656
|
+
`• Action: Standalone rustc scripts only have access to std.\n` +
|
|
657
|
+
` For external crates, compile via cargo or test against compiled workspace libraries.`);
|
|
658
|
+
}
|
|
659
|
+
// 6. Go package errors
|
|
660
|
+
if (/cannot find package\s+"([^"]+)"|no required module provides package/i.test(combined)) {
|
|
661
|
+
const match = combined.match(/cannot find package\s+"([^"]+)"/i);
|
|
662
|
+
const pkg = match ? match[1] : 'package';
|
|
663
|
+
diagnostics.push(`• Cause: Go package '${pkg}' not found in GOPATH/workspace module.\n` +
|
|
664
|
+
`• Action: Ensure '${pkg}' is declared in go.mod or run 'go get ${pkg}'.`);
|
|
665
|
+
}
|
|
666
|
+
// 7. C/C++ header / linker errors
|
|
667
|
+
if (/fatal error:\s*([^:]+):\s*No such file or directory/i.test(combined)) {
|
|
668
|
+
const match = combined.match(/fatal error:\s*([^:]+):\s*No such file or directory/i);
|
|
669
|
+
const hdr = match ? match[1] : 'header';
|
|
670
|
+
diagnostics.push(`• Cause: C/C++ header file '${hdr}' not found.\n` +
|
|
671
|
+
`• Action: Verify system library installation or specify include path.`);
|
|
672
|
+
}
|
|
673
|
+
else if (/undefined reference to|ld returned \d+ exit status/i.test(combined)) {
|
|
674
|
+
diagnostics.push(`• Cause: C/C++ linker unresolved symbol reference.\n` +
|
|
675
|
+
`• Action: Link required libraries (e.g., -lm, -lpthread, -lstdc++).`);
|
|
676
|
+
}
|
|
677
|
+
// 8. Timeout
|
|
678
|
+
if (/Execution timed out after \d+ms/i.test(combined)) {
|
|
679
|
+
diagnostics.push(`• Cause: Execution timed out.\n` +
|
|
680
|
+
`• Action: Increase timeoutMs in options, reduce iteration count, or check for infinite loops / blocking I/O.`);
|
|
681
|
+
}
|
|
682
|
+
if (diagnostics.length === 0)
|
|
683
|
+
return null;
|
|
684
|
+
return `[SANDBOX DIAGNOSTIC]\n` + diagnostics.join('\n\n');
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Spawns a direct subprocess with dedicated FD-3 payload streaming,
|
|
688
|
+
* zero-shell overhead, and clean signal propagation.
|
|
689
|
+
*/
|
|
690
|
+
function spawnSubprocess(executable, args, options) {
|
|
691
|
+
return new Promise((resolve) => {
|
|
692
|
+
let stdoutData = '';
|
|
693
|
+
let stderrData = '';
|
|
694
|
+
let fd3Data = '';
|
|
695
|
+
let isSettled = false;
|
|
696
|
+
let killedByTimeout = false;
|
|
697
|
+
let aborted = false;
|
|
698
|
+
let timer = null;
|
|
699
|
+
let killTimer = null;
|
|
700
|
+
// Windows compatibility for .cmd / .bat executables
|
|
701
|
+
const isWin = os.platform() === 'win32';
|
|
702
|
+
const useShell = isWin && (executable.endsWith('.cmd') || executable.endsWith('.bat'));
|
|
703
|
+
let child;
|
|
704
|
+
try {
|
|
705
|
+
child = spawn(executable, args, {
|
|
706
|
+
cwd: options.cwd,
|
|
707
|
+
env: options.env,
|
|
708
|
+
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
|
709
|
+
shell: useShell,
|
|
710
|
+
windowsHide: true,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
resolve({
|
|
715
|
+
stdout: '',
|
|
716
|
+
stderr: err.message || String(err),
|
|
717
|
+
fd3: '',
|
|
718
|
+
exitCode: 1,
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const cleanupTimers = () => {
|
|
723
|
+
if (timer) {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
timer = null;
|
|
726
|
+
}
|
|
727
|
+
if (killTimer) {
|
|
728
|
+
clearTimeout(killTimer);
|
|
729
|
+
killTimer = null;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const abortHandler = () => {
|
|
733
|
+
aborted = true;
|
|
734
|
+
cleanupTimers();
|
|
735
|
+
try {
|
|
736
|
+
child.kill('SIGTERM');
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// Ignore kill errors
|
|
740
|
+
}
|
|
741
|
+
killTimer = setTimeout(() => {
|
|
742
|
+
try {
|
|
743
|
+
child.kill('SIGKILL');
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Ignore
|
|
747
|
+
}
|
|
748
|
+
}, 500);
|
|
749
|
+
};
|
|
750
|
+
if (options.abortSignal) {
|
|
751
|
+
if (options.abortSignal.aborted) {
|
|
752
|
+
abortHandler();
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
options.abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (options.timeoutMs > 0) {
|
|
759
|
+
timer = setTimeout(() => {
|
|
760
|
+
killedByTimeout = true;
|
|
761
|
+
try {
|
|
762
|
+
child.kill('SIGTERM');
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// Ignore
|
|
766
|
+
}
|
|
767
|
+
killTimer = setTimeout(() => {
|
|
768
|
+
try {
|
|
769
|
+
child.kill('SIGKILL');
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// Ignore
|
|
773
|
+
}
|
|
774
|
+
}, 1000);
|
|
775
|
+
}, options.timeoutMs);
|
|
776
|
+
}
|
|
777
|
+
if (child.stdout) {
|
|
778
|
+
child.stdout.setEncoding('utf-8');
|
|
779
|
+
child.stdout.on('data', (chunk) => {
|
|
780
|
+
if (stdoutData.length < options.maxOutputChars * 2) {
|
|
781
|
+
stdoutData += chunk;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
if (child.stderr) {
|
|
786
|
+
child.stderr.setEncoding('utf-8');
|
|
787
|
+
child.stderr.on('data', (chunk) => {
|
|
788
|
+
if (stderrData.length < options.maxOutputChars * 2) {
|
|
789
|
+
stderrData += chunk;
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const fd3Stream = child.stdio[3];
|
|
794
|
+
if (fd3Stream) {
|
|
795
|
+
fd3Stream.setEncoding('utf-8');
|
|
796
|
+
fd3Stream.on('data', (chunk) => {
|
|
797
|
+
fd3Data += chunk;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (options.stdinContent !== undefined && child.stdin) {
|
|
801
|
+
child.stdin.on('error', () => {
|
|
802
|
+
// Avoid unhandled EPIPE if child process exits early
|
|
803
|
+
});
|
|
804
|
+
try {
|
|
805
|
+
child.stdin.write(options.stdinContent, 'utf-8');
|
|
806
|
+
child.stdin.end();
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// Ignore write errors
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else if (child.stdin) {
|
|
813
|
+
try {
|
|
814
|
+
child.stdin.end();
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
// Ignore
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const finish = (code, signal) => {
|
|
821
|
+
if (isSettled)
|
|
822
|
+
return;
|
|
823
|
+
isSettled = true;
|
|
824
|
+
cleanupTimers();
|
|
825
|
+
if (options.abortSignal) {
|
|
826
|
+
options.abortSignal.removeEventListener('abort', abortHandler);
|
|
827
|
+
}
|
|
828
|
+
let exitCode = code ?? (signal ? 128 + 15 : 0);
|
|
829
|
+
if (killedByTimeout) {
|
|
830
|
+
exitCode = 124;
|
|
831
|
+
}
|
|
832
|
+
else if (aborted) {
|
|
833
|
+
exitCode = 130;
|
|
834
|
+
}
|
|
835
|
+
resolve({
|
|
836
|
+
stdout: stdoutData,
|
|
837
|
+
stderr: stderrData,
|
|
838
|
+
fd3: fd3Data,
|
|
839
|
+
exitCode,
|
|
840
|
+
killedByTimeout,
|
|
841
|
+
aborted,
|
|
842
|
+
});
|
|
843
|
+
};
|
|
844
|
+
child.on('error', (err) => {
|
|
845
|
+
stderrData += `\n${err.message}`;
|
|
846
|
+
finish(1, null);
|
|
847
|
+
});
|
|
848
|
+
child.on('close', (code, signal) => {
|
|
849
|
+
finish(code, signal);
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Formats subprocess execution results, resolving structured results from FD-3 or stdout fallback.
|
|
855
|
+
*/
|
|
856
|
+
function handleExecutionResult(res, normLang, workspaceRoot, options) {
|
|
857
|
+
if (res.aborted) {
|
|
858
|
+
return {
|
|
859
|
+
exitCode: 130,
|
|
860
|
+
stderr: 'Analysis script aborted by user.',
|
|
861
|
+
stdout: '',
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (res.killedByTimeout) {
|
|
865
|
+
const tMs = options.timeoutMs ?? 60_000;
|
|
866
|
+
const diag = diagnoseSandboxError(`Execution timed out after ${tMs}ms.`, '', normLang, workspaceRoot);
|
|
867
|
+
const errOut = diag
|
|
868
|
+
? `Execution timed out after ${tMs}ms.\n\n${diag}`
|
|
869
|
+
: `Execution timed out after ${tMs}ms.`;
|
|
870
|
+
return {
|
|
871
|
+
exitCode: 124,
|
|
872
|
+
stderr: errOut,
|
|
873
|
+
stdout: '',
|
|
874
|
+
};
|
|
294
875
|
}
|
|
876
|
+
const rawStdout = res.stdout.trim();
|
|
877
|
+
const rawStderr = res.stderr.trim();
|
|
878
|
+
// 1. Check FD-3 structured payload first
|
|
879
|
+
let structuredResult;
|
|
880
|
+
let hasFd3Payload = false;
|
|
881
|
+
if (res.fd3 && res.fd3.trim()) {
|
|
882
|
+
const lines = res.fd3.trim().split('\n').map((l) => l.trim()).filter(Boolean);
|
|
883
|
+
if (lines.length > 0) {
|
|
884
|
+
const lastPayload = lines[lines.length - 1];
|
|
885
|
+
try {
|
|
886
|
+
structuredResult = JSON.parse(lastPayload);
|
|
887
|
+
hasFd3Payload = true;
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
structuredResult = lastPayload;
|
|
891
|
+
hasFd3Payload = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// 2. Fallback to stdout regex extraction
|
|
896
|
+
const parsed = parseStructuredResult(rawStdout);
|
|
897
|
+
if (!hasFd3Payload && parsed.structuredResult !== undefined) {
|
|
898
|
+
structuredResult = parsed.structuredResult;
|
|
899
|
+
}
|
|
900
|
+
const cleanedStdout = parsed.cleanedStdout;
|
|
901
|
+
let finalStderr = rawStderr;
|
|
902
|
+
if (finalStderr) {
|
|
903
|
+
const diag = diagnoseSandboxError(finalStderr, rawStdout, normLang, workspaceRoot);
|
|
904
|
+
if (diag) {
|
|
905
|
+
finalStderr = `${finalStderr}\n\n${diag}`.trim();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
exitCode: res.exitCode,
|
|
910
|
+
stderr: truncateOutput(finalStderr, options.maxOutputChars),
|
|
911
|
+
stdout: truncateOutput(cleanedStdout || (structuredResult !== undefined ? '' : rawStdout), options.maxOutputChars),
|
|
912
|
+
structuredResult,
|
|
913
|
+
};
|
|
295
914
|
}
|
|
296
915
|
/**
|
|
297
916
|
* Truncates a string to the specified maximum length, appending an ellipsis marker if truncated.
|
|
@@ -302,15 +921,15 @@ function truncateOutput(text, max) {
|
|
|
302
921
|
return text.substring(0, max) + '\n... (output truncated)';
|
|
303
922
|
}
|
|
304
923
|
/**
|
|
305
|
-
* Write a disposable analysis script
|
|
924
|
+
* Write a disposable analysis script or stream it directly via stdin, execute it using the
|
|
306
925
|
* specified language runtime, capture its output, and guarantee cleanup.
|
|
307
926
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
927
|
+
* For interpreted runtimes (Node, Python, Bash, Ruby, PHP), zero-disk stdin streaming is used
|
|
928
|
+
* to eliminate disk I/O, watcher churn, and temporary file artifacts.
|
|
310
929
|
*
|
|
311
930
|
* @param workspaceRoot - The workspace root, used as the `cwd` for script execution
|
|
312
931
|
* so relative file paths in the script resolve correctly.
|
|
313
|
-
* @param language - The runtime to use: "node", "ts-node", "python", "bash", "go",
|
|
932
|
+
* @param language - The runtime to use: "node", "ts-node", "python", "bash", "go", "rust", etc.
|
|
314
933
|
* @param code - The script source code to execute.
|
|
315
934
|
* @param options - Optional timeout, output cap, and abort signal.
|
|
316
935
|
* @returns Captured stdout, stderr, and exit code.
|
|
@@ -320,65 +939,250 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
|
|
|
320
939
|
const maxOutputChars = options?.maxOutputChars ?? 100_000;
|
|
321
940
|
const normLang = await resolveEffectiveRuntime(workspaceRoot, language, code);
|
|
322
941
|
const ext = await resolveScriptExtension(workspaceRoot, normLang, code);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
942
|
+
const [tsRunnerInfo, pyEnvInfo, moduleType] = await Promise.all([
|
|
943
|
+
findTypeScriptRunner(workspaceRoot),
|
|
944
|
+
findPythonBinary(workspaceRoot),
|
|
945
|
+
detectWorkspaceModuleType(workspaceRoot),
|
|
946
|
+
]);
|
|
947
|
+
const finalCode = options?.injectHelpers !== false
|
|
948
|
+
? injectSandboxHelpers(code, normLang)
|
|
949
|
+
: code;
|
|
950
|
+
const mergedEnv = {
|
|
951
|
+
...process.env,
|
|
952
|
+
...options?.env,
|
|
953
|
+
};
|
|
954
|
+
// 1. NODE_PATH injection for workspace packages
|
|
955
|
+
const workspaceNodeModules = path.join(workspaceRoot, 'node_modules');
|
|
956
|
+
if (mergedEnv.NODE_PATH) {
|
|
957
|
+
mergedEnv.NODE_PATH = `${workspaceNodeModules}${path.delimiter}${mergedEnv.NODE_PATH}`;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
mergedEnv.NODE_PATH = workspaceNodeModules;
|
|
961
|
+
}
|
|
962
|
+
// 2. TypeScript configuration
|
|
963
|
+
if (tsRunnerInfo.tsconfigPath) {
|
|
964
|
+
mergedEnv.TS_NODE_PROJECT = tsRunnerInfo.tsconfigPath;
|
|
965
|
+
mergedEnv.TSCONFIG_PATH = tsRunnerInfo.tsconfigPath;
|
|
966
|
+
}
|
|
967
|
+
// 3. Python environment & PYTHONPATH injection
|
|
968
|
+
const workspaceSrc = path.join(workspaceRoot, 'src');
|
|
969
|
+
let hasSrc = false;
|
|
970
|
+
try {
|
|
971
|
+
await fs.access(workspaceSrc);
|
|
972
|
+
hasSrc = true;
|
|
331
973
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
974
|
+
catch {
|
|
975
|
+
// no src dir
|
|
976
|
+
}
|
|
977
|
+
const pythonPaths = [workspaceRoot];
|
|
978
|
+
if (hasSrc) {
|
|
979
|
+
pythonPaths.push(workspaceSrc);
|
|
980
|
+
}
|
|
981
|
+
if (mergedEnv.PYTHONPATH) {
|
|
982
|
+
mergedEnv.PYTHONPATH = `${pythonPaths.join(path.delimiter)}${path.delimiter}${mergedEnv.PYTHONPATH}`;
|
|
338
983
|
}
|
|
339
|
-
else
|
|
340
|
-
|
|
984
|
+
else {
|
|
985
|
+
mergedEnv.PYTHONPATH = pythonPaths.join(path.delimiter);
|
|
341
986
|
}
|
|
342
|
-
|
|
343
|
-
|
|
987
|
+
mergedEnv.PYTHONUNBUFFERED = '1';
|
|
988
|
+
if (pyEnvInfo.isVenv && pyEnvInfo.venvDir && pyEnvInfo.binDir) {
|
|
989
|
+
mergedEnv.VIRTUAL_ENV = pyEnvInfo.venvDir;
|
|
990
|
+
mergedEnv.PATH = `${pyEnvInfo.binDir}${path.delimiter}${mergedEnv.PATH || ''}`;
|
|
344
991
|
}
|
|
992
|
+
// 4. Ruby RUBYLIB injection
|
|
993
|
+
const workspaceLib = path.join(workspaceRoot, 'lib');
|
|
994
|
+
let hasLib = false;
|
|
345
995
|
try {
|
|
346
|
-
await fs.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
stdout: truncateOutput(stdout.trim(), maxOutputChars),
|
|
357
|
-
stderr: truncateOutput(stderr.trim(), maxOutputChars),
|
|
358
|
-
exitCode: 0,
|
|
359
|
-
};
|
|
996
|
+
await fs.access(workspaceLib);
|
|
997
|
+
hasLib = true;
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// no lib dir
|
|
1001
|
+
}
|
|
1002
|
+
const rubyPaths = [workspaceRoot];
|
|
1003
|
+
if (hasLib) {
|
|
1004
|
+
rubyPaths.push(workspaceLib);
|
|
360
1005
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1006
|
+
if (mergedEnv.RUBYLIB) {
|
|
1007
|
+
mergedEnv.RUBYLIB = `${rubyPaths.join(path.delimiter)}${path.delimiter}${mergedEnv.RUBYLIB}`;
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
mergedEnv.RUBYLIB = rubyPaths.join(path.delimiter);
|
|
1011
|
+
}
|
|
1012
|
+
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
1013
|
+
const tempFiles = [];
|
|
1014
|
+
let executable = null;
|
|
1015
|
+
let args = [];
|
|
1016
|
+
let stdinContent;
|
|
1017
|
+
let isZeroDisk = false;
|
|
1018
|
+
switch (normLang) {
|
|
1019
|
+
case 'node': {
|
|
1020
|
+
executable = process.execPath || 'node';
|
|
1021
|
+
const hasCJS = /\b(require\s*\(|module\\.exports|exports\.)/.test(code);
|
|
1022
|
+
const hasESM = /\b(import\s+|import\(|export\s+|export\{|export\s+default)\b/.test(code) || /^\s*await\s+/m.test(code);
|
|
1023
|
+
const isESM = (hasESM && !hasCJS) || (!hasCJS && moduleType === 'module');
|
|
1024
|
+
args = isESM ? ['--input-type=module', '-'] : ['--input-type=commonjs', '-'];
|
|
1025
|
+
stdinContent = finalCode;
|
|
1026
|
+
isZeroDisk = true;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
case 'python': {
|
|
1030
|
+
executable = pyEnvInfo.pythonBin;
|
|
1031
|
+
args = ['-u', '-'];
|
|
1032
|
+
stdinContent = finalCode;
|
|
1033
|
+
isZeroDisk = true;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
case 'bash': {
|
|
1037
|
+
executable = 'bash';
|
|
1038
|
+
args = ['-s'];
|
|
1039
|
+
stdinContent = finalCode;
|
|
1040
|
+
isZeroDisk = true;
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case 'ruby': {
|
|
1044
|
+
executable = 'ruby';
|
|
1045
|
+
args = ['-'];
|
|
1046
|
+
stdinContent = finalCode;
|
|
1047
|
+
isZeroDisk = true;
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
case 'php': {
|
|
1051
|
+
executable = 'php';
|
|
1052
|
+
args = [];
|
|
1053
|
+
stdinContent = finalCode.trim().startsWith('<?php') ? finalCode : `<?php\n${finalCode}`;
|
|
1054
|
+
isZeroDisk = true;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
case 'ts-node': {
|
|
1058
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1059
|
+
tempFiles.push(scriptPath);
|
|
1060
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1061
|
+
if (tsRunnerInfo.runnerType === 'local-tsx') {
|
|
1062
|
+
executable = tsRunnerInfo.binaryPath || 'tsx';
|
|
1063
|
+
args = [scriptPath];
|
|
1064
|
+
}
|
|
1065
|
+
else if (tsRunnerInfo.runnerType === 'local-ts-node-esm') {
|
|
1066
|
+
executable = tsRunnerInfo.binaryPath || 'ts-node-esm';
|
|
1067
|
+
args = [scriptPath];
|
|
1068
|
+
}
|
|
1069
|
+
else if (tsRunnerInfo.runnerType === 'local-ts-node') {
|
|
1070
|
+
executable = tsRunnerInfo.binaryPath || 'ts-node';
|
|
1071
|
+
args = moduleType === 'module' ? ['--esm', scriptPath] : [scriptPath];
|
|
1072
|
+
}
|
|
1073
|
+
else if (tsRunnerInfo.runnerType === 'npx-ts-node') {
|
|
1074
|
+
executable = 'npx';
|
|
1075
|
+
args = ['ts-node', scriptPath];
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
executable = 'npx';
|
|
1079
|
+
args = ['tsx', scriptPath];
|
|
1080
|
+
}
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case 'go': {
|
|
1084
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1085
|
+
tempFiles.push(scriptPath);
|
|
1086
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1087
|
+
executable = 'go';
|
|
1088
|
+
args = ['run', scriptPath];
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
case 'java': {
|
|
1092
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1093
|
+
tempFiles.push(scriptPath);
|
|
1094
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1095
|
+
executable = 'java';
|
|
1096
|
+
args = [scriptPath];
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case 'rust': {
|
|
1100
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1101
|
+
const binPath = scriptPath.replace(/\.rs$/, binExt);
|
|
1102
|
+
tempFiles.push(scriptPath, binPath, scriptPath.replace(/\.rs$/, '.pdb'));
|
|
1103
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1104
|
+
// Compile step
|
|
1105
|
+
const compileRes = await spawnSubprocess('rustc', [scriptPath, '-o', binPath], {
|
|
1106
|
+
cwd: workspaceRoot,
|
|
1107
|
+
env: mergedEnv,
|
|
1108
|
+
timeoutMs: Math.min(timeoutMs, 30_000),
|
|
1109
|
+
maxOutputChars,
|
|
1110
|
+
abortSignal: options?.abortSignal,
|
|
1111
|
+
});
|
|
1112
|
+
if (compileRes.exitCode !== 0) {
|
|
1113
|
+
return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
|
|
1114
|
+
}
|
|
1115
|
+
executable = binPath;
|
|
1116
|
+
args = [];
|
|
1117
|
+
break;
|
|
365
1118
|
}
|
|
366
|
-
|
|
367
|
-
|
|
1119
|
+
case 'c': {
|
|
1120
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1121
|
+
const binPath = scriptPath.replace(/\.c$/, binExt);
|
|
1122
|
+
tempFiles.push(scriptPath, binPath);
|
|
1123
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1124
|
+
// Compile step
|
|
1125
|
+
const compileRes = await spawnSubprocess('gcc', [scriptPath, '-o', binPath], {
|
|
1126
|
+
cwd: workspaceRoot,
|
|
1127
|
+
env: mergedEnv,
|
|
1128
|
+
timeoutMs: Math.min(timeoutMs, 30_000),
|
|
1129
|
+
maxOutputChars,
|
|
1130
|
+
abortSignal: options?.abortSignal,
|
|
1131
|
+
});
|
|
1132
|
+
if (compileRes.exitCode !== 0) {
|
|
1133
|
+
return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
|
|
1134
|
+
}
|
|
1135
|
+
executable = binPath;
|
|
1136
|
+
args = [];
|
|
1137
|
+
break;
|
|
368
1138
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1139
|
+
case 'cpp': {
|
|
1140
|
+
const scriptPath = buildTempPath(code, ext);
|
|
1141
|
+
const binPath = scriptPath.replace(/\.cpp$/, binExt);
|
|
1142
|
+
tempFiles.push(scriptPath, binPath);
|
|
1143
|
+
await fs.writeFile(scriptPath, finalCode, 'utf-8');
|
|
1144
|
+
// Compile step
|
|
1145
|
+
const compileRes = await spawnSubprocess('g++', ['-std=c++17', scriptPath, '-o', binPath], {
|
|
1146
|
+
cwd: workspaceRoot,
|
|
1147
|
+
env: mergedEnv,
|
|
1148
|
+
timeoutMs: Math.min(timeoutMs, 30_000),
|
|
1149
|
+
maxOutputChars,
|
|
1150
|
+
abortSignal: options?.abortSignal,
|
|
1151
|
+
});
|
|
1152
|
+
if (compileRes.exitCode !== 0) {
|
|
1153
|
+
return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
|
|
1154
|
+
}
|
|
1155
|
+
executable = binPath;
|
|
1156
|
+
args = [];
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
default:
|
|
1160
|
+
return {
|
|
1161
|
+
stdout: '',
|
|
1162
|
+
stderr: `Unsupported language runtime: "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
|
|
1163
|
+
exitCode: 1,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
try {
|
|
1167
|
+
const runRes = await spawnSubprocess(executable, args, {
|
|
1168
|
+
cwd: workspaceRoot,
|
|
1169
|
+
env: mergedEnv,
|
|
1170
|
+
timeoutMs,
|
|
1171
|
+
maxOutputChars,
|
|
1172
|
+
abortSignal: options?.abortSignal,
|
|
1173
|
+
stdinContent,
|
|
1174
|
+
});
|
|
1175
|
+
return handleExecutionResult(runRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
|
|
373
1176
|
}
|
|
374
1177
|
finally {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1178
|
+
if (!isZeroDisk && tempFiles.length > 0) {
|
|
1179
|
+
for (const tmpFile of tempFiles) {
|
|
1180
|
+
try {
|
|
1181
|
+
await fs.rm(tmpFile, { force: true, recursive: true });
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
// ignore cleanup errors
|
|
1185
|
+
}
|
|
382
1186
|
}
|
|
383
1187
|
}
|
|
384
1188
|
}
|
|
@@ -486,7 +1290,8 @@ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node
|
|
|
486
1290
|
}
|
|
487
1291
|
const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
|
|
488
1292
|
const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
|
|
489
|
-
const isPassed = combinedOutput.includes('[PBT PASSED]') ||
|
|
1293
|
+
const isPassed = combinedOutput.includes('[PBT PASSED]') ||
|
|
1294
|
+
(scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
|
|
490
1295
|
let counterexample;
|
|
491
1296
|
let shrunkInput;
|
|
492
1297
|
let seed;
|
|
@@ -504,20 +1309,20 @@ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node
|
|
|
504
1309
|
// Extract seed
|
|
505
1310
|
const seedMatch = combinedOutput.match(/Seed(?:|\/Iteration):\s*(\d+)/i);
|
|
506
1311
|
if (seedMatch) {
|
|
507
|
-
seed = parseInt(seedMatch[1], 10);
|
|
1312
|
+
seed = Number.parseInt(seedMatch[1], 10);
|
|
508
1313
|
}
|
|
509
1314
|
// Extract completed runs
|
|
510
1315
|
const runsMatch = combinedOutput.match(/Completed\s+(\d+)\s+runs/i);
|
|
511
1316
|
if (runsMatch) {
|
|
512
|
-
numRunsCompleted = parseInt(runsMatch[1], 10);
|
|
1317
|
+
numRunsCompleted = Number.parseInt(runsMatch[1], 10);
|
|
513
1318
|
}
|
|
514
1319
|
return {
|
|
515
1320
|
...scriptResult,
|
|
516
|
-
passed: isPassed,
|
|
517
1321
|
counterexample,
|
|
518
|
-
shrunkInput,
|
|
519
|
-
seed,
|
|
520
1322
|
numRunsCompleted,
|
|
1323
|
+
passed: isPassed,
|
|
1324
|
+
seed,
|
|
1325
|
+
shrunkInput,
|
|
521
1326
|
};
|
|
522
1327
|
}
|
|
523
1328
|
/**
|
|
@@ -710,28 +1515,31 @@ export async function runFuzzProbe(workspaceRoot, code, language = 'node', confi
|
|
|
710
1515
|
failingInputs.push(match[1].trim());
|
|
711
1516
|
}
|
|
712
1517
|
let totalFuzzRuns = 0;
|
|
713
|
-
const runsMatch = combinedOutput.match(/(?:Total Runs|Runs):\s*(\d+)/i) ||
|
|
1518
|
+
const runsMatch = combinedOutput.match(/(?:Total Runs|Runs):\s*(\d+)/i) ||
|
|
1519
|
+
combinedOutput.match(/Completed\s+(\d+)\s+(?:fuzz\s+)?runs/i);
|
|
714
1520
|
if (runsMatch) {
|
|
715
|
-
totalFuzzRuns = parseInt(runsMatch[1], 10);
|
|
1521
|
+
totalFuzzRuns = Number.parseInt(runsMatch[1], 10);
|
|
716
1522
|
}
|
|
717
1523
|
let seed;
|
|
718
1524
|
const seedMatch = combinedOutput.match(/Seed:\s*(\d+)/i);
|
|
719
1525
|
if (seedMatch) {
|
|
720
|
-
seed = parseInt(seedMatch[1], 10);
|
|
1526
|
+
seed = Number.parseInt(seedMatch[1], 10);
|
|
721
1527
|
}
|
|
722
1528
|
let errorSummary;
|
|
723
1529
|
if (!isPassed) {
|
|
724
1530
|
const errLine = scriptResult.stderr.split('\n').find((l) => l.trim().length > 0) ||
|
|
725
|
-
combinedOutput
|
|
1531
|
+
combinedOutput
|
|
1532
|
+
.split('\n')
|
|
1533
|
+
.find((l) => l.toLowerCase().includes('error') || l.includes('[FUZZ FAILED]'));
|
|
726
1534
|
errorSummary = errLine?.trim() || 'Fuzzing probe encountered unexpected failure or crash.';
|
|
727
1535
|
}
|
|
728
1536
|
return {
|
|
729
1537
|
...scriptResult,
|
|
730
|
-
passed: isPassed,
|
|
731
|
-
totalFuzzRuns,
|
|
732
|
-
failingInputs: failingInputs.length > 0 ? failingInputs : undefined,
|
|
733
1538
|
errorSummary,
|
|
1539
|
+
failingInputs: failingInputs.length > 0 ? failingInputs : undefined,
|
|
1540
|
+
passed: isPassed,
|
|
734
1541
|
seed,
|
|
1542
|
+
totalFuzzRuns,
|
|
735
1543
|
};
|
|
736
1544
|
}
|
|
737
1545
|
/**
|
|
@@ -837,7 +1645,8 @@ const testIters = ${testIters};
|
|
|
837
1645
|
for (let i = 0; i < testIters; i++) {
|
|
838
1646
|
if (i % Math.max(1, Math.floor(testIters / 5)) === 0) {
|
|
839
1647
|
if (global.gc) global.gc();
|
|
840
|
-
|
|
1648
|
+
const curr = process.memoryUsage().heapUsed;
|
|
1649
|
+
console.log(\`[HEAP_SAMPLE] \${i}, \${curr}\`);
|
|
841
1650
|
}
|
|
842
1651
|
}
|
|
843
1652
|
|
|
@@ -866,26 +1675,26 @@ export async function checkHeapDelta(workspaceRoot, code, language = 'node', con
|
|
|
866
1675
|
let initialHeapBytes = 0;
|
|
867
1676
|
const startMatch = combinedOutput.match(/(?:\[HEAP_START\]|Heap start:)\s*(-?\d+)/i);
|
|
868
1677
|
if (startMatch) {
|
|
869
|
-
initialHeapBytes = parseInt(startMatch[1], 10);
|
|
1678
|
+
initialHeapBytes = Number.parseInt(startMatch[1], 10);
|
|
870
1679
|
}
|
|
871
1680
|
let finalHeapBytes = 0;
|
|
872
1681
|
const finalMatch = combinedOutput.match(/(?:\[HEAP_FINAL\]|Heap final:)\s*(-?\d+)/i);
|
|
873
1682
|
if (finalMatch) {
|
|
874
|
-
finalHeapBytes = parseInt(finalMatch[1], 10);
|
|
1683
|
+
finalHeapBytes = Number.parseInt(finalMatch[1], 10);
|
|
875
1684
|
}
|
|
876
1685
|
let deltaBytes = finalHeapBytes - initialHeapBytes;
|
|
877
1686
|
const deltaMatch = combinedOutput.match(/(?:\[HEAP_DELTA\]|Heap delta:)\s*(-?\d+)/i);
|
|
878
1687
|
if (deltaMatch) {
|
|
879
|
-
deltaBytes = parseInt(deltaMatch[1], 10);
|
|
1688
|
+
deltaBytes = Number.parseInt(deltaMatch[1], 10);
|
|
880
1689
|
}
|
|
881
1690
|
const samples = [];
|
|
882
1691
|
const sampleRegex = /(?:\[HEAP_SAMPLE\]|Sample\s+(\d+):?)\s*(\d+)(?:,\s*(\d+))?/gi;
|
|
883
1692
|
let sMatch;
|
|
884
1693
|
while ((sMatch = sampleRegex.exec(combinedOutput)) !== null) {
|
|
885
|
-
const iter = parseInt(sMatch[1], 10);
|
|
886
|
-
const bytes = sMatch[3] ? parseInt(sMatch[3], 10) : parseInt(sMatch[2], 10);
|
|
887
|
-
if (!isNaN(iter) && !isNaN(bytes)) {
|
|
888
|
-
samples.push({
|
|
1694
|
+
const iter = Number.parseInt(sMatch[1], 10);
|
|
1695
|
+
const bytes = sMatch[3] ? Number.parseInt(sMatch[3], 10) : Number.parseInt(sMatch[2], 10);
|
|
1696
|
+
if (!Number.isNaN(iter) && !Number.isNaN(bytes)) {
|
|
1697
|
+
samples.push({ heapBytes: bytes, iteration: iter });
|
|
889
1698
|
}
|
|
890
1699
|
}
|
|
891
1700
|
let growthRateBytesPerIter;
|
|
@@ -905,11 +1714,11 @@ export async function checkHeapDelta(workspaceRoot, code, language = 'node', con
|
|
|
905
1714
|
(growthRateBytesPerIter !== undefined && growthRateBytesPerIter > 10_000);
|
|
906
1715
|
return {
|
|
907
1716
|
...scriptResult,
|
|
908
|
-
initialHeapBytes,
|
|
909
|
-
finalHeapBytes,
|
|
910
1717
|
deltaBytes,
|
|
911
|
-
|
|
1718
|
+
finalHeapBytes,
|
|
912
1719
|
growthRateBytesPerIter,
|
|
1720
|
+
initialHeapBytes,
|
|
1721
|
+
leakDetected,
|
|
913
1722
|
samples: samples.length > 0 ? samples : undefined,
|
|
914
1723
|
};
|
|
915
1724
|
}
|
|
@@ -922,30 +1731,30 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
|
|
|
922
1731
|
if (typeof val1 === 'number' && typeof val2 === 'number') {
|
|
923
1732
|
if (Math.abs(val1 - val2) > tolerance) {
|
|
924
1733
|
details.push({
|
|
925
|
-
field: pathStr,
|
|
926
1734
|
baseline: val1,
|
|
927
1735
|
candidate: val2,
|
|
928
1736
|
diff: `Numeric difference exceeds tolerance (${tolerance}): |${val1} - ${val2}| = ${Math.abs(val1 - val2)}`,
|
|
1737
|
+
field: pathStr,
|
|
929
1738
|
});
|
|
930
1739
|
}
|
|
931
1740
|
return;
|
|
932
1741
|
}
|
|
933
1742
|
if (typeof val1 !== typeof val2 || val1 === null || val2 === null) {
|
|
934
1743
|
details.push({
|
|
935
|
-
field: pathStr,
|
|
936
1744
|
baseline: val1,
|
|
937
1745
|
candidate: val2,
|
|
938
1746
|
diff: `Type mismatch: baseline is ${typeof val1}, candidate is ${typeof val2}`,
|
|
1747
|
+
field: pathStr,
|
|
939
1748
|
});
|
|
940
1749
|
return;
|
|
941
1750
|
}
|
|
942
1751
|
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
943
1752
|
if (val1.length !== val2.length) {
|
|
944
1753
|
details.push({
|
|
945
|
-
field: `${pathStr}.length`,
|
|
946
1754
|
baseline: val1.length,
|
|
947
1755
|
candidate: val2.length,
|
|
948
1756
|
diff: `Array length mismatch: baseline length ${val1.length}, candidate length ${val2.length}`,
|
|
1757
|
+
field: `${pathStr}.length`,
|
|
949
1758
|
});
|
|
950
1759
|
}
|
|
951
1760
|
const minLen = Math.min(val1.length, val2.length);
|
|
@@ -962,18 +1771,18 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
|
|
|
962
1771
|
const subPath = pathStr ? `${pathStr}.${key}` : key;
|
|
963
1772
|
if (!(key in val1)) {
|
|
964
1773
|
details.push({
|
|
965
|
-
field: subPath,
|
|
966
1774
|
baseline: undefined,
|
|
967
1775
|
candidate: val2[key],
|
|
968
1776
|
diff: `Key "${key}" missing in baseline`,
|
|
1777
|
+
field: subPath,
|
|
969
1778
|
});
|
|
970
1779
|
}
|
|
971
1780
|
else if (!(key in val2)) {
|
|
972
1781
|
details.push({
|
|
973
|
-
field: subPath,
|
|
974
1782
|
baseline: val1[key],
|
|
975
1783
|
candidate: undefined,
|
|
976
1784
|
diff: `Key "${key}" missing in candidate`,
|
|
1785
|
+
field: subPath,
|
|
977
1786
|
});
|
|
978
1787
|
}
|
|
979
1788
|
else {
|
|
@@ -983,10 +1792,10 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
|
|
|
983
1792
|
return;
|
|
984
1793
|
}
|
|
985
1794
|
details.push({
|
|
986
|
-
field: pathStr,
|
|
987
1795
|
baseline: val1,
|
|
988
1796
|
candidate: val2,
|
|
989
1797
|
diff: `Value mismatch: baseline="${val1}", candidate="${val2}"`,
|
|
1798
|
+
field: pathStr,
|
|
990
1799
|
});
|
|
991
1800
|
}
|
|
992
1801
|
/**
|
|
@@ -1057,10 +1866,10 @@ export async function checkBehavioralDrift(workspaceRoot, baselineCode, candidat
|
|
|
1057
1866
|
}
|
|
1058
1867
|
if (baselineResult.exitCode !== candidateResult.exitCode) {
|
|
1059
1868
|
driftDetails.push({
|
|
1060
|
-
field: 'exitCode',
|
|
1061
1869
|
baseline: baselineResult.exitCode,
|
|
1062
1870
|
candidate: candidateResult.exitCode,
|
|
1063
1871
|
diff: `Exit code mismatch: baseline exited with ${baselineResult.exitCode}, candidate with ${candidateResult.exitCode}`,
|
|
1872
|
+
field: 'exitCode',
|
|
1064
1873
|
});
|
|
1065
1874
|
}
|
|
1066
1875
|
const hasDrift = driftDetails.length > 0;
|
|
@@ -1068,13 +1877,25 @@ export async function checkBehavioralDrift(workspaceRoot, baselineCode, candidat
|
|
|
1068
1877
|
const combinedStdout = `--- Baseline Stdout ---\n${baselineResult.stdout}\n--- Candidate Stdout ---\n${candidateResult.stdout}`;
|
|
1069
1878
|
const combinedStderr = `--- Baseline Stderr ---\n${baselineResult.stderr}\n--- Candidate Stderr ---\n${candidateResult.stderr}`;
|
|
1070
1879
|
return {
|
|
1071
|
-
stdout: combinedStdout,
|
|
1072
|
-
stderr: combinedStderr,
|
|
1073
|
-
exitCode: hasDrift ? 1 : 0,
|
|
1074
|
-
hasDrift,
|
|
1075
1880
|
baselineOutput: baselineOut,
|
|
1076
1881
|
candidateOutput: candidateOut,
|
|
1077
1882
|
diffSummary,
|
|
1078
1883
|
driftDetails: driftDetails.length > 0 ? driftDetails : undefined,
|
|
1884
|
+
exitCode: hasDrift ? 1 : 0,
|
|
1885
|
+
hasDrift,
|
|
1886
|
+
stderr: combinedStderr,
|
|
1887
|
+
stdout: combinedStdout,
|
|
1079
1888
|
};
|
|
1080
1889
|
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Runs a heap delta memory profiling script and evaluates memory growth characteristics.
|
|
1892
|
+
*/
|
|
1893
|
+
export async function runHeapDeltaCheck(workspaceRoot, code, language = 'node', config) {
|
|
1894
|
+
return checkHeapDelta(workspaceRoot, code, language, config);
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Runs two scripts (baseline vs candidate) and compares output for behavioral drift.
|
|
1898
|
+
*/
|
|
1899
|
+
export async function runBehavioralDriftCheck(workspaceRoot, baselineCode, candidateCode, language = 'node', config) {
|
|
1900
|
+
return checkBehavioralDrift(workspaceRoot, baselineCode, candidateCode, language, config);
|
|
1901
|
+
}
|