import {readFileSync, existsSync} from 'fs' import {join} from 'path' import {als} from './context' import {prixExec} from './utils' export interface PrixConfig { version: string severity: { minimumLevel: 'critical' | 'major' | 'minor' | 'info' showConfidenceThreshold: number } filters: { excludePatterns: string[] includePatterns: string[] disableTypes: string[] } linting: { enabled: boolean eslint: boolean prettier: boolean failOnError: boolean } review: { maxFilesPerPR: number maxCommentsPerFile: number denseMode: boolean showImpactAnalysis: boolean } remedies: { enabled: boolean autoCreatePR: boolean requireApproval: boolean } confidence: { minCritical: number minMajor: number minMinor: number minInfo: number } } export const DEFAULT_PRIX_CONFIG: PrixConfig = { version: '1.0', severity: { minimumLevel: 'minor', showConfidenceThreshold: 50 }, filters: { excludePatterns: [], includePatterns: ['**/*'], disableTypes: [] }, linting: { enabled: true, eslint: true, prettier: true, failOnError: false }, review: { maxFilesPerPR: 50, maxCommentsPerFile: 10, denseMode: false, showImpactAnalysis: true }, remedies: { enabled: false, autoCreatePR: false, requireApproval: true }, confidence: { minCritical: 0, minMajor: 60, minMinor: 70, minInfo: 80 } } export class PrixConfigLoader { private config: PrixConfig | null = null load(workingDir: string): PrixConfig { if (this.config) { return this.config } const possiblePaths = [ join(workingDir, '.prix.yml'), join(workingDir, 'prix.yml'), join(workingDir, '.prix.yaml'), join(workingDir, 'prix.yaml') ] for (const configPath of possiblePaths) { if (existsSync(configPath)) { try { const content = readFileSync(configPath, 'utf8') this.config = this.mergeWithDefaults(this.parseYAML(content)) return this.config } catch (e) { console.warn(`Failed to load prix config from ${configPath}: ${e}`) } } } this.config = DEFAULT_PRIX_CONFIG return this.config } private parseYAML(content: string): Partial { const result: Partial = {} const lines = content.split('\n') let currentSection: string | null = null let currentSectionObj: Record = {} let sectionKey = '' for (const line of lines) { const sectionMatch = line.match(/^(\w+):\s*$/) if (sectionMatch) { if (currentSection && sectionKey) { ;(result as any)[sectionKey] = currentSectionObj } currentSection = sectionMatch[1] sectionKey = currentSection currentSectionObj = {} continue } const keyValueMatch = line.match(/^\s*(\w+):\s*(.+)$/) if (keyValueMatch && currentSection) { const key = keyValueMatch[1] let value: any = keyValueMatch[2].trim() if (value === 'true') value = true else if (value === 'false') value = false else if (!isNaN(Number(value))) value = Number(value) currentSectionObj[key] = value } } if (currentSection && sectionKey) { ;(result as any)[sectionKey] = currentSectionObj } return result } private mergeWithDefaults(config: Partial): PrixConfig { return { version: config.version || DEFAULT_PRIX_CONFIG.version, severity: { ...DEFAULT_PRIX_CONFIG.severity, ...config.severity }, filters: { ...DEFAULT_PRIX_CONFIG.filters, ...config.filters }, linting: { ...DEFAULT_PRIX_CONFIG.linting, ...config.linting }, review: { ...DEFAULT_PRIX_CONFIG.review, ...config.review }, remedies: { ...DEFAULT_PRIX_CONFIG.remedies, ...config.remedies }, confidence: { ...DEFAULT_PRIX_CONFIG.confidence, ...config.confidence } } } shouldIncludeFile(config: PrixConfig, filename: string): boolean { const {excludePatterns, includePatterns} = config.filters for (const pattern of excludePatterns) { if (this.matchesPattern(filename, pattern)) { return false } } if (includePatterns.length === 0) { return true } for (const pattern of includePatterns) { if (this.matchesPattern(filename, pattern)) { return true } } return false } private matchesPattern(filename: string, pattern: string): boolean { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.') .replace(/\*\*/g, '.*') return new RegExp(`^${regexPattern}$`).test(filename) } } export const configLoader = new PrixConfigLoader() export function getConfig(): PrixConfig { const ctx = als.getStore() const workingDir = ctx?.workingDir || process.cwd() return configLoader.load(workingDir) } export function meetsConfidenceThreshold( severity: 'critical' | 'major' | 'minor' | 'info', confidence: number ): boolean { const config = getConfig() const thresholds = config.confidence switch (severity) { case 'critical': return confidence >= thresholds.minCritical case 'major': return confidence >= thresholds.minMajor case 'minor': return confidence >= thresholds.minMinor case 'info': return confidence >= thresholds.minInfo default: return true } } export function meetsMinimumSeverity(severity: string): boolean { const config = getConfig() const levels = ['critical', 'major', 'minor', 'info'] const minimumLevel = config.severity.minimumLevel const minimumIndex = levels.indexOf(minimumLevel) const severityIndex = levels.indexOf(severity.toLowerCase()) return severityIndex >= minimumIndex } export async function lintSuggestion( suggestion: string, filename: string, language: string ): Promise<{valid: boolean; errors: string[]}> { const config = getConfig() if (!config.linting.enabled) { return {valid: true, errors: []} } const errors: string[] = [] const workingDir = als.getStore()?.workingDir || process.cwd() if (config.linting.prettier) { try { prixExec( `echo "${suggestion.replace( /"/g, '\\"' )}" | prettier --stdin-filepath ${filename}`, { cwd: workingDir, stdio: 'pipe' } ) } catch { errors.push('Prettier formatting check failed') } } if ( config.linting.eslint && (language === 'javascript' || language === 'typescript') ) { try { const ext = filename.endsWith('.ts') ? 'ts' : 'js' const tempFile = `.prix_temp_suggestion.${ext}` prixExec( `echo "${suggestion.replace( /"/g, '\\"' )}" > ${tempFile} && npx eslint ${tempFile} --format json 2>&1 || true`, { cwd: workingDir, stdio: 'pipe' } ) } catch { errors.push('ESLint check failed') } } return { valid: config.linting.failOnError ? errors.length === 0 : true, errors } }