Spaces:
Paused
Paused
| 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<PrixConfig> { | |
| const result: Partial<PrixConfig> = {} | |
| const lines = content.split('\n') | |
| let currentSection: string | null = null | |
| let currentSectionObj: Record<string, any> = {} | |
| 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>): 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 | |
| } | |
| } | |