PRIX / src /fix-verifier.ts
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
16.9 kB
import {readFile, writeFile, unlink, stat} from 'fs/promises'
import {existsSync} from 'fs'
import {join, basename} from 'path'
import {Project, SyntaxKind} from 'ts-morph'
import {prixExecAsync} from './utils'
import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
export interface VerificationResult {
passed: boolean
checks: VerificationCheck[]
overallFeedback: string
canRetry: boolean
errorMessage?: string
}
export interface VerificationCheck {
name: string
passed: boolean
feedback: string
duration?: number
}
export interface VerificationConfig {
enableLint: boolean
enableSyntax: boolean
enableAST: boolean
enableTests: boolean
maxRetries: number
timeout: number
}
const DEFAULT_CONFIG: VerificationConfig = {
enableLint: true,
enableSyntax: true,
enableAST: true,
enableTests: false,
maxRetries: 3,
timeout: 30000
}
export class FixVerifier {
private config: VerificationConfig
private project: Project
constructor(config: Partial<VerificationConfig> = {}, project?: Project) {
this.config = {...DEFAULT_CONFIG, ...config}
this.project = project || new Project()
}
async verify(
filename: string,
remedyCode: string,
startLine: number,
endLine: number,
workingDir: string
): Promise<VerificationResult> {
const checks: VerificationCheck[] = []
let allPassed = true
let canRetry = true
let errorMessage: string | undefined
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
}
}
private async runSyntaxCheck(
filename: string,
remedyCode: string,
workingDir: string
): Promise<VerificationCheck> {
const startTime = Date.now()
const ext = filename.split('.').pop()?.toLowerCase() || ''
let feedback = ''
let passed = true
const tempFile = join(workingDir, `__prix_verify_${Date.now()}.${ext}`)
try {
if (ext === 'ts' || ext === 'js') {
await writeFile(tempFile, remedyCode, 'utf8')
feedback = await this.verifyJSTypeScript(tempFile, ext, workingDir)
passed = !feedback.includes('❌')
} else if (ext === 'py') {
await writeFile(tempFile, remedyCode, 'utf8')
feedback = await this.verifyPython(tempFile)
passed = !feedback.includes('❌')
} else if (ext === 'go') {
await writeFile(tempFile, remedyCode, 'utf8')
feedback = await this.verifyGo(tempFile)
passed = !feedback.includes('❌')
} else {
feedback = '⚠️ Syntax check not supported for this file type'
}
} catch (e: any) {
passed = false
feedback = `❌ Syntax check failed: ${e.message}`
} finally {
await this.cleanup(tempFile)
}
return {
name: 'Syntax Check',
passed,
feedback,
duration: Date.now() - startTime
}
}
private async verifyJSTypeScript(
tempFile: string,
ext: string,
workingDir: string
): Promise<string> {
let feedback = ''
try {
if (ext === 'ts') {
const hasTypeScript =
existsSync(join(workingDir, 'node_modules/typescript')) ||
existsSync(join(workingDir, 'node_modules/.bin/tsc'))
if (hasTypeScript) {
try {
await prixExecAsync('npx', ['tsc', tempFile, '--noEmit', '--esModuleInterop', '--skipLibCheck', '--jsx', 'react'], {
cwd: workingDir,
timeout: 15000
})
feedback = 'TS check passed'
} catch (e: any) {
const errorOutput = e.stdout?.toString() || e.message || ''
const relevantErrors = this.filterErrorsForFile(
errorOutput,
basename(tempFile)
)
if (relevantErrors) {
feedback = `❌ TS error: ${relevantErrors}`
} else {
feedback = '❌ TS syntax error'
}
}
} else {
feedback = '⚠️ TypeScript not installed, skipping TS check'
}
} else {
try {
await prixExecAsync('node', ['--check', tempFile], {
timeout: 5000
})
feedback = 'Syntax check passed'
} catch (e: any) {
feedback = `❌ Syntax error: ${e.message}`
}
}
} catch (e: any) {
feedback = `❌ Check failed: ${e.message}`
}
return feedback
}
private async verifyPython(tempFile: string): Promise<string> {
try {
await prixExecAsync('python3', ['-m', 'py_compile', tempFile], {
timeout: 5000
})
return 'Python syntax passed'
} catch (e: any) {
const errorOutput = e.stderr?.toString() || e.message
return `❌ Python syntax error: ${errorOutput}`.substring(0, 200)
}
}
private async verifyGo(tempFile: string): Promise<string> {
try {
await prixExecAsync('go', ['vet', tempFile], {timeout: 5000})
return 'Go syntax passed'
} catch (e: any) {
const errorOutput = e.stderr?.toString() || e.message
return `❌ Go syntax error: ${errorOutput}`.substring(0, 200)
}
}
private async runLintCheck(
filename: string,
remedyCode: string,
workingDir: string
): Promise<VerificationCheck> {
const startTime = Date.now()
const ext = filename.split('.').pop()?.toLowerCase() || ''
let feedback = ''
let passed = true
const tempFile = join(workingDir, `__prix_lint_${Date.now()}.${ext}`)
try {
if (
(ext === 'ts' || ext === 'js') &&
existsSync(join(workingDir, 'package.json'))
) {
await writeFile(tempFile, remedyCode, 'utf8')
const hasEslint =
existsSync(join(workingDir, '.eslintrc.json')) ||
existsSync(join(workingDir, '.eslintrc.js')) ||
existsSync(join(workingDir, '.eslintrc'))
if (hasEslint) {
try {
await prixExecAsync('npx', ['eslint', tempFile, '--max-warnings', '0'], {
cwd: workingDir,
timeout: 15000
})
feedback = 'Lint passed'
} catch (e: any) {
const errorOutput = e.stdout?.toString() || e.message || ''
const relevantErrors = this.filterErrorsForFile(
errorOutput,
basename(tempFile)
)
feedback = `❌ Lint failed: ${relevantErrors || ' ESLint errors'}`
passed = false
}
} else {
feedback = '⚠️ ESLint not configured'
}
} else if (ext === 'py') {
const hasPylint = await this.commandExists('pylint')
const hasFlake8 = await this.commandExists('flake8')
await writeFile(tempFile, remedyCode, 'utf8')
if (hasPylint) {
try {
await prixExecAsync('pylint', [tempFile, '--disable=all', '--enable=E'], {
timeout: 10000
})
feedback = 'Pylint passed'
} catch (e: any) {
feedback = `❌ Pylint: ${e.message}`.substring(0, 200)
passed = false
}
} else if (hasFlake8) {
try {
await prixExecAsync('flake8', [tempFile], {timeout: 10000})
feedback = 'Flake8 passed'
} catch (e: any) {
feedback = `❌ Flake8: ${e.message}`.substring(0, 200)
passed = false
}
} else {
feedback = '⚠️ Python linter not available'
}
}
} catch (e: any) {
passed = false
feedback = `❌ Lint failed: ${e.message}`
} finally {
await this.cleanup(tempFile)
}
return {
name: 'Lint Check',
passed,
feedback,
duration: Date.now() - startTime
}
}
private runASTValidation(
filename: string,
remedyCode: string
): VerificationCheck {
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(SyntaxKind.Identifier)
const undefinedSymbols: string[] = []
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(SyntaxKind.ImportSpecifier)) continue
if (id.getParentIfKind(SyntaxKind.PropertyAccessExpression)) continue
const parentKind = id.getParent()?.getKind()
if (parentKind === 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: any) {
return {
name: 'AST Validation',
passed: false,
feedback: `❌ AST validation failed: ${e.message}`,
duration: Date.now() - startTime
}
}
}
private async runTestCheck(
filename: string,
remedyCode: string,
startLine: number,
endLine: number,
workingDir: string
): Promise<VerificationCheck> {
const startTime = Date.now()
if (!existsSync(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 = await readFile(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 = join(workingDir, `__prix_test_${Date.now()}.test.ts`)
await writeFile(tempTestFile, testsForFunction, 'utf8')
try {
const hasJest =
existsSync(join(workingDir, 'node_modules/jest')) ||
existsSync(join(workingDir, 'node_modules/.bin/jest'))
if (hasJest) {
try {
await prixExecAsync('npx', ['jest', tempTestFile, '--passWithNoTests'], {
cwd: workingDir,
timeout: 30000
})
return {
name: 'Test Check',
passed: true,
feedback: 'Test check passed',
duration: Date.now() - startTime
}
} catch (e: any) {
return {
name: 'Test Check',
passed: false,
feedback: `❌ Test failed: ${e.message}`.substring(0, 200),
duration: Date.now() - startTime
}
}
}
} finally {
await this.cleanup(tempTestFile)
}
return {
name: 'Test Check',
passed: true,
feedback: '⚠️ Test runner not available',
duration: 0
}
} catch (e: any) {
return {
name: 'Test Check',
passed: false,
feedback: `❌ Test check failed: ${e.message}`,
duration: Date.now() - startTime
}
}
}
private findCorrespondingTestFile(filename: string): string | null {
const baseDir = filename.substring(0, filename.lastIndexOf('/'))
const basename = filename.substring(filename.lastIndexOf('/') + 1)
const nameWithoutExt = basename.replace(/\.[^.]+$/, '')
const patterns = [
join(baseDir, `${nameWithoutExt}.test.ts`),
join(baseDir, `${nameWithoutExt}.spec.ts`),
join(baseDir, '__tests__', `${nameWithoutExt}.ts`),
join(baseDir, 'test', `${nameWithoutExt}.ts`),
join(baseDir, 'tests', `${nameWithoutExt}.ts`)
]
for (const pattern of patterns) {
if (existsSync(pattern)) {
return pattern
}
}
return null
}
private extractRelevantTests(
testContent: string,
sourceFile: string,
lineNumber: number
): string | null {
const functionNameMatch = testContent.match(
new RegExp(`(test|it)\\(['"]{1,2}([^'"]{1,30})['"]{1,2}`)
)
if (!functionNameMatch) return null
return testContent
}
private filterErrorsForFile(
errorOutput: string,
tempFile: string
): string | null {
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('; ')
}
private async commandExists(cmd: string): Promise<boolean> {
try {
await prixExecAsync('which', [cmd], {timeout: 1000})
return true
} catch {
return false
}
}
private async cleanup(tempFile: string): Promise<void> {
try {
if (existsSync(tempFile)) {
await unlink(tempFile)
}
} catch {
// Cleanup failures are non-fatal
}
}
}
export function createVerifier(project?: Project): FixVerifier {
return new FixVerifier({}, project)
}