| "use strict"; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.FixVerifier = void 0; |
| exports.createVerifier = createVerifier; |
| const child_process_1 = require("child_process"); |
| const fs_1 = require("fs"); |
| const path_1 = require("path"); |
| const ts_morph_1 = require("ts-morph"); |
| const utils_1 = require("./utils"); |
| const DEFAULT_CONFIG = { |
| enableLint: true, |
| enableSyntax: true, |
| enableAST: true, |
| enableTests: false, |
| maxRetries: 3, |
| timeout: 30000 |
| }; |
| class FixVerifier { |
| config; |
| project; |
| constructor(config = {}, project) { |
| this.config = { ...DEFAULT_CONFIG, ...config }; |
| this.project = project || new ts_morph_1.Project(); |
| } |
| async verify(filename, remedyCode, startLine, endLine, workingDir) { |
| const checks = []; |
| let allPassed = true; |
| let canRetry = true; |
| let errorMessage; |
| if (this.config.enableSyntax) { |
| const syntaxCheck = await this.runSyntaxCheck(filename, remedyCode, workingDir); |
| checks.push(syntaxCheck); |
| if (!syntaxCheck.passed) { |
| allPassed = false; |
| errorMessage = syntaxCheck.feedback; |
| } |
| } |
| if (this.config.enableLint && allPassed) { |
| const lintCheck = await this.runLintCheck(filename, remedyCode, workingDir); |
| checks.push(lintCheck); |
| if (!lintCheck.passed) { |
| allPassed = false; |
| canRetry = false; |
| errorMessage = lintCheck.feedback; |
| } |
| } |
| if (this.config.enableAST && allPassed) { |
| const astCheck = this.runASTValidation(filename, remedyCode); |
| checks.push(astCheck); |
| if (!astCheck.passed) { |
| allPassed = false; |
| errorMessage = astCheck.feedback; |
| } |
| } |
| if (this.config.enableTests && allPassed) { |
| const testCheck = await this.runTestCheck(filename, remedyCode, startLine, endLine, workingDir); |
| checks.push(testCheck); |
| if (!testCheck.passed) { |
| allPassed = false; |
| canRetry = false; |
| errorMessage = testCheck.feedback; |
| } |
| } |
| const overallFeedback = checks |
| .map(c => `${c.passed ? '✅' : '❌'} ${c.name}: ${c.feedback}`) |
| .join('\n'); |
| return { |
| passed: allPassed, |
| checks, |
| overallFeedback, |
| canRetry, |
| errorMessage |
| }; |
| } |
| async runSyntaxCheck(filename, remedyCode, workingDir) { |
| const startTime = Date.now(); |
| const ext = filename.split('.').pop()?.toLowerCase() || ''; |
| let feedback = ''; |
| let passed = true; |
| const tempFile = (0, path_1.join)(workingDir, `__prix_verify_${Date.now()}.${ext}`); |
| try { |
| if (ext === 'ts' || ext === 'js') { |
| (0, fs_1.writeFileSync)(tempFile, remedyCode, 'utf8'); |
| feedback = await this.verifyJSTypeScript(tempFile, ext, workingDir); |
| passed = !feedback.includes('❌'); |
| } |
| else if (ext === 'py') { |
| (0, fs_1.writeFileSync)(tempFile, remedyCode, 'utf8'); |
| feedback = await this.verifyPython(tempFile); |
| passed = !feedback.includes('❌'); |
| } |
| else if (ext === 'go') { |
| (0, fs_1.writeFileSync)(tempFile, remedyCode, 'utf8'); |
| feedback = await this.verifyGo(tempFile); |
| passed = !feedback.includes('❌'); |
| } |
| else { |
| feedback = '⚠️ Syntax check not supported for this file type'; |
| } |
| } |
| catch (e) { |
| passed = false; |
| feedback = `❌ Syntax check failed: ${e.message}`; |
| } |
| finally { |
| this.cleanup(tempFile); |
| } |
| return { |
| name: 'Syntax Check', |
| passed, |
| feedback, |
| duration: Date.now() - startTime |
| }; |
| } |
| async verifyJSTypeScript(tempFile, ext, workingDir) { |
| let feedback = ''; |
| try { |
| if (ext === 'ts') { |
| const hasTypeScript = (0, fs_1.existsSync)((0, path_1.join)(workingDir, 'node_modules/typescript')) || |
| (0, fs_1.existsSync)((0, path_1.join)(workingDir, 'node_modules/.bin/tsc')); |
| if (hasTypeScript) { |
| try { |
| (0, utils_1.prixExec)(`npx tsc "${tempFile}" --noEmit --esModuleInterop --skipLibCheck --jsx react`, { |
| cwd: workingDir, |
| timeout: 15000 |
| }); |
| feedback = 'TS check passed'; |
| } |
| catch (e) { |
| const errorOutput = e.stdout?.toString() || e.message || ''; |
| const relevantErrors = this.filterErrorsForFile(errorOutput, (0, path_1.basename)(tempFile)); |
| if (relevantErrors) { |
| feedback = `❌ TS error: ${relevantErrors}`; |
| } |
| else { |
| feedback = '❌ TS syntax error'; |
| } |
| } |
| } |
| else { |
| feedback = '⚠️ TypeScript not installed, skipping TS check'; |
| } |
| } |
| else { |
| try { |
| (0, child_process_1.execSync)(`node --check "${tempFile}"`, { |
| encoding: 'utf8', |
| timeout: 5000 |
| }); |
| feedback = 'Syntax check passed'; |
| } |
| catch (e) { |
| feedback = `❌ Syntax error: ${e.message}`; |
| } |
| } |
| } |
| catch (e) { |
| feedback = `❌ Check failed: ${e.message}`; |
| } |
| return feedback; |
| } |
| async verifyPython(tempFile) { |
| try { |
| (0, child_process_1.execSync)(`python3 -m py_compile "${tempFile}"`, { |
| encoding: 'utf8', |
| timeout: 5000 |
| }); |
| return 'Python syntax passed'; |
| } |
| catch (e) { |
| const errorOutput = e.stderr?.toString() || e.message; |
| return `❌ Python syntax error: ${errorOutput}`.substring(0, 200); |
| } |
| } |
| async verifyGo(tempFile) { |
| try { |
| (0, child_process_1.execSync)(`go vet "${tempFile}"`, { encoding: 'utf8', timeout: 5000 }); |
| return 'Go syntax passed'; |
| } |
| catch (e) { |
| const errorOutput = e.stderr?.toString() || e.message; |
| return `❌ Go syntax error: ${errorOutput}`.substring(0, 200); |
| } |
| } |
| async runLintCheck(filename, remedyCode, workingDir) { |
| const startTime = Date.now(); |
| const ext = filename.split('.').pop()?.toLowerCase() || ''; |
| let feedback = ''; |
| let passed = true; |
| const tempFile = (0, path_1.join)(workingDir, `__prix_lint_${Date.now()}.${ext}`); |
| try { |
| if ((ext === 'ts' || ext === 'js') && |
| (0, fs_1.existsSync)((0, path_1.join)(workingDir, 'package.json'))) { |
| (0, fs_1.writeFileSync)(tempFile, remedyCode, 'utf8'); |
| const hasEslint = (0, fs_1.existsSync)((0, path_1.join)(workingDir, '.eslintrc.json')) || |
| (0, fs_1.existsSync)((0, path_1.join)(workingDir, '.eslintrc.js')) || |
| (0, fs_1.existsSync)((0, path_1.join)(workingDir, '.eslintrc')); |
| if (hasEslint) { |
| try { |
| (0, utils_1.prixExec)(`npx eslint "${tempFile}" --max-warnings 0`, { |
| cwd: workingDir, |
| timeout: 15000 |
| }); |
| feedback = 'Lint passed'; |
| } |
| catch (e) { |
| const errorOutput = e.stdout?.toString() || e.message || ''; |
| const relevantErrors = this.filterErrorsForFile(errorOutput, (0, path_1.basename)(tempFile)); |
| feedback = `❌ Lint failed: ${relevantErrors || ' ESLint errors'}`; |
| passed = false; |
| } |
| } |
| else { |
| feedback = '⚠️ ESLint not configured'; |
| } |
| } |
| else if (ext === 'py') { |
| const hasPylint = this.commandExists('pylint'); |
| const hasFlake8 = this.commandExists('flake8'); |
| (0, fs_1.writeFileSync)(tempFile, remedyCode, 'utf8'); |
| if (hasPylint) { |
| try { |
| (0, child_process_1.execSync)(`pylint "${tempFile}" --disable=all --enable=E`, { |
| encoding: 'utf8', |
| timeout: 10000 |
| }); |
| feedback = 'Pylint passed'; |
| } |
| catch (e) { |
| feedback = `❌ Pylint: ${e.message}`.substring(0, 200); |
| passed = false; |
| } |
| } |
| else if (hasFlake8) { |
| try { |
| (0, child_process_1.execSync)(`flake8 "${tempFile}"`, { encoding: 'utf8', timeout: 10000 }); |
| feedback = 'Flake8 passed'; |
| } |
| catch (e) { |
| feedback = `❌ Flake8: ${e.message}`.substring(0, 200); |
| passed = false; |
| } |
| } |
| else { |
| feedback = '⚠️ Python linter not available'; |
| } |
| } |
| } |
| catch (e) { |
| passed = false; |
| feedback = `❌ Lint failed: ${e.message}`; |
| } |
| finally { |
| this.cleanup(tempFile); |
| } |
| return { |
| name: 'Lint Check', |
| passed, |
| feedback, |
| duration: Date.now() - startTime |
| }; |
| } |
| runASTValidation(filename, remedyCode) { |
| const startTime = Date.now(); |
| if (!filename.endsWith('.ts') && !filename.endsWith('.js')) { |
| return { |
| name: 'AST Validation', |
| passed: true, |
| feedback: 'Not applicable for this file type', |
| duration: 0 |
| }; |
| } |
| try { |
| const tempFile = `__prix_ast_${Date.now()}_${filename}`; |
| const sourceFile = this.project.createSourceFile(tempFile, remedyCode, { |
| overwrite: true |
| }); |
| const diagnostics = sourceFile.getPreEmitDiagnostics(); |
| if (diagnostics.length > 0) { |
| const errorMessages = diagnostics |
| .slice(0, 3) |
| .map(d => d.getMessageText()) |
| .join('; '); |
| return { |
| name: 'AST Validation', |
| passed: false, |
| feedback: `❌ AST errors: ${errorMessages}`, |
| duration: Date.now() - startTime |
| }; |
| } |
| const identifiers = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier); |
| const undefinedSymbols = []; |
| for (const id of identifiers) { |
| if (id.getSymbol()) |
| continue; |
| const text = id.getText(); |
| const skipList = [ |
| 'console', |
| 'process', |
| 'require', |
| 'exports', |
| 'module', |
| 'Buffer', |
| 'Array', |
| 'Object', |
| 'String', |
| 'Number', |
| 'Boolean', |
| 'Date', |
| 'Promise', |
| 'Math', |
| 'JSON', |
| 'Map', |
| 'Set', |
| 'RegExp', |
| 'Error', |
| 'undefined', |
| 'NaN', |
| 'Infinity', |
| 'parseInt', |
| 'parseFloat' |
| ]; |
| if (skipList.includes(text)) |
| continue; |
| if (id.getParentIfKind(ts_morph_1.SyntaxKind.ImportSpecifier)) |
| continue; |
| if (id.getParentIfKind(ts_morph_1.SyntaxKind.PropertyAccessExpression)) |
| continue; |
| const parentKind = id.getParent()?.getKind(); |
| if (parentKind === ts_morph_1.SyntaxKind.PropertyAssignment) |
| continue; |
| undefinedSymbols.push(text); |
| } |
| const uniqueUndefined = [...new Set(undefinedSymbols)].slice(0, 5); |
| if (uniqueUndefined.length > 0) { |
| return { |
| name: 'AST Validation', |
| passed: false, |
| feedback: `❌ Hallucination detected: undefined symbols: ${uniqueUndefined.join(', ')}`, |
| duration: Date.now() - startTime |
| }; |
| } |
| return { |
| name: 'AST Validation', |
| passed: true, |
| feedback: 'No hallucinations detected', |
| duration: Date.now() - startTime |
| }; |
| } |
| catch (e) { |
| return { |
| name: 'AST Validation', |
| passed: false, |
| feedback: `❌ AST validation failed: ${e.message}`, |
| duration: Date.now() - startTime |
| }; |
| } |
| } |
| async runTestCheck(filename, remedyCode, startLine, endLine, workingDir) { |
| const startTime = Date.now(); |
| if (!(0, fs_1.existsSync)((0, path_1.join)(workingDir, 'package.json'))) { |
| return { |
| name: 'Test Check', |
| passed: true, |
| feedback: '⚠️ No package.json found', |
| duration: 0 |
| }; |
| } |
| const testFile = this.findCorrespondingTestFile(filename); |
| if (!testFile) { |
| return { |
| name: 'Test Check', |
| passed: true, |
| feedback: '⚠️ No corresponding test file found', |
| duration: 0 |
| }; |
| } |
| try { |
| const testContent = (0, fs_1.readFileSync)(testFile, 'utf8'); |
| const testsForFunction = this.extractRelevantTests(testContent, filename, startLine); |
| if (!testsForFunction) { |
| return { |
| name: 'Test Check', |
| passed: true, |
| feedback: '⚠️ No relevant tests found for modified code', |
| duration: 0 |
| }; |
| } |
| const tempTestFile = (0, path_1.join)(workingDir, `__prix_test_${Date.now()}.test.ts`); |
| (0, fs_1.writeFileSync)(tempTestFile, testsForFunction, 'utf8'); |
| try { |
| const hasJest = (0, fs_1.existsSync)((0, path_1.join)(workingDir, 'node_modules/jest')) || |
| (0, fs_1.existsSync)((0, path_1.join)(workingDir, 'node_modules/.bin/jest')); |
| if (hasJest) { |
| try { |
| (0, utils_1.prixExec)(`npx jest "${tempTestFile}" --passWithNoTests`, { |
| cwd: workingDir, |
| timeout: 30000 |
| }); |
| return { |
| name: 'Test Check', |
| passed: true, |
| feedback: 'Test check passed', |
| duration: Date.now() - startTime |
| }; |
| } |
| catch (e) { |
| return { |
| name: 'Test Check', |
| passed: false, |
| feedback: `❌ Test failed: ${e.message}`.substring(0, 200), |
| duration: Date.now() - startTime |
| }; |
| } |
| } |
| } |
| finally { |
| this.cleanup(tempTestFile); |
| } |
| return { |
| name: 'Test Check', |
| passed: true, |
| feedback: '⚠️ Test runner not available', |
| duration: 0 |
| }; |
| } |
| catch (e) { |
| return { |
| name: 'Test Check', |
| passed: false, |
| feedback: `❌ Test check failed: ${e.message}`, |
| duration: Date.now() - startTime |
| }; |
| } |
| } |
| findCorrespondingTestFile(filename) { |
| const baseDir = filename.substring(0, filename.lastIndexOf('/')); |
| const basename = filename.substring(filename.lastIndexOf('/') + 1); |
| const nameWithoutExt = basename.replace(/\.[^.]+$/, ''); |
| const patterns = [ |
| (0, path_1.join)(baseDir, `${nameWithoutExt}.test.ts`), |
| (0, path_1.join)(baseDir, `${nameWithoutExt}.spec.ts`), |
| (0, path_1.join)(baseDir, '__tests__', `${nameWithoutExt}.ts`), |
| (0, path_1.join)(baseDir, 'test', `${nameWithoutExt}.ts`), |
| (0, path_1.join)(baseDir, 'tests', `${nameWithoutExt}.ts`) |
| ]; |
| for (const pattern of patterns) { |
| if ((0, fs_1.existsSync)(pattern)) { |
| return pattern; |
| } |
| } |
| return null; |
| } |
| extractRelevantTests(testContent, sourceFile, lineNumber) { |
| const functionNameMatch = testContent.match(new RegExp(`(test|it)\\(['"]{1,2}([^'"]{1,30})['"]{1,2}`)); |
| if (!functionNameMatch) |
| return null; |
| return testContent; |
| } |
| filterErrorsForFile(errorOutput, tempFile) { |
| const lines = errorOutput.split('\n'); |
| const relevantLines = lines.filter(l => l.includes(tempFile) || l.includes('__prix')); |
| if (relevantLines.length === 0) |
| return null; |
| return relevantLines.slice(0, 3).join('; '); |
| } |
| commandExists(cmd) { |
| try { |
| (0, child_process_1.execSync)(`which ${cmd}`, { encoding: 'utf8', timeout: 1000 }); |
| return true; |
| } |
| catch { |
| return false; |
| } |
| } |
| cleanup(tempFile) { |
| try { |
| if ((0, fs_1.existsSync)(tempFile)) { |
| (0, fs_1.unlinkSync)(tempFile); |
| } |
| } |
| catch { |
| |
| } |
| } |
| } |
| exports.FixVerifier = FixVerifier; |
| function createVerifier(project) { |
| return new FixVerifier({}, project); |
| } |
| |