import { readFile, writeFile, mkdir } from 'fs/promises'; import { dirname } from 'path'; import path from 'path'; import axios from 'axios'; export interface SkillTrigger { skillId: string; patterns: { imports?: string[]; // e.g. "@stripe/stripe-js" content?: string[]; // Regex strings e.g. "payment_intent" files?: string[]; // Glob patterns e.g. "**/*.sql" violation_id?: string; metric_threshold?: number; }; confidence: 'high' | 'medium' | 'low'; } export interface HeuristicMatch { skillId: string; file: string; reason: string; confidence: 'high' | 'medium' | 'low'; } // TODO: In the future, this should fetch from the Rigstate API (The Registry) const GLOBAL_HEURISTICS: SkillTrigger[] = [ { skillId: 'payment-expert', patterns: { imports: ['@stripe/', 'stripe'], content: ['PaymentIntent', 'CheckoutSession'], }, confidence: 'high' }, { skillId: 'rigstate-integrity-gate', patterns: { files: ['**/release.config.js', '**/manifest.json', '**/.rigstate/release/*'], content: ['[CORE INTEGRITY]', 'prepare_release'] }, confidence: 'high' }, { skillId: 'database-architect', patterns: { files: ['**/*.sql', '**/schema.prisma', '**/migrations/*'], imports: ['@supabase/supabase-js', 'drizzle-orm', 'prisma'] }, confidence: 'medium' } ]; export class HeuristicEngine { private rules: SkillTrigger[] = []; private cachePath: string; constructor() { this.cachePath = path.join(process.cwd(), '.rigstate', 'cache', 'heuristics.json'); this.loadRules(); } private async loadRules() { try { const cached = await readFile(this.cachePath, 'utf-8'); const data = JSON.parse(cached); if (Array.isArray(data) && data.length > 0) { this.rules = data; return; } } catch (e) { // No cache, use defaults } this.rules = GLOBAL_HEURISTICS; } async refreshRules(projectId: string, apiUrl: string, apiKey: string) { try { // Ensure cache directory exists await mkdir(dirname(this.cachePath), { recursive: true }); const endpoint = `${apiUrl}/api/v1/skills/triggers`; const response = await axios.get(endpoint, { headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' } }); if (response.data && Array.isArray(response.data.triggers)) { const cloudRules = response.data.triggers; // Write to cache await writeFile(this.cachePath, JSON.stringify(cloudRules, null, 2)); this.rules = cloudRules; return true; } } catch (error) { return false; } } async analyzeFile(filePath: string, metrics?: { lineCount: number, rules: any[] }): Promise { try { const content = await readFile(filePath, 'utf-8'); const matches: HeuristicMatch[] = []; // Use dynamic rules const activeRules = this.rules.length > 0 ? this.rules : GLOBAL_HEURISTICS; for (const heuristic of activeRules) { const match = this.checkHeuristic(filePath, content, heuristic, metrics); if (match) { matches.push(match); } } return matches; } catch (error) { // Ignore file read errors (deleted files, etc) return []; } } private checkHeuristic( filePath: string, content: string, heuristic: SkillTrigger, metrics?: { lineCount: number, rules: any[] } ): HeuristicMatch | null { // 0. Check Metric Thresholds (The 80% Rule) if (heuristic.patterns.metric_threshold && metrics) { const lineLimitRule = metrics.rules.find(r => r.rule_type === 'MAX_FILE_LINES'); if (lineLimitRule) { const limit = (lineLimitRule.value as any).limit; const threshold = limit * heuristic.patterns.metric_threshold; if (metrics.lineCount >= threshold && metrics.lineCount < limit) { return { skillId: heuristic.skillId, file: filePath, reason: `File reached ${Math.round((metrics.lineCount / limit) * 100)}% of its line limit (${metrics.lineCount}/${limit})`, confidence: 'high' }; } } } // 1. Check File Path Patterns if (heuristic.patterns.files) { // Simple endsWith check for now, ideally use micromatch const matchesFile = heuristic.patterns.files.some(pattern => { if (pattern.startsWith('**/*')) return filePath.endsWith(pattern.replace('**/*', '')); return filePath.includes(pattern); }); if (matchesFile) { return { skillId: heuristic.skillId, file: filePath, reason: `Matches file pattern: ${heuristic.patterns.files.join(', ')}`, confidence: heuristic.confidence }; } } // 2. Check Imports if (heuristic.patterns.imports) { for (const imp of heuristic.patterns.imports) { // Regex to find import or require const importRegex = new RegExp(`(import .* from ['"]${imp}|require\\(['"]${imp})`, 'i'); if (importRegex.test(content)) { return { skillId: heuristic.skillId, file: filePath, reason: `Detected import: ${imp}`, confidence: heuristic.confidence }; } } } // 3. Check Content if (heuristic.patterns.content) { for (const pattern of heuristic.patterns.content) { if (content.includes(pattern)) { return { skillId: heuristic.skillId, file: filePath, reason: `Detected content pattern: ${pattern}`, confidence: heuristic.confidence }; } } } return null; } } export function createHeuristicEngine() { return new HeuristicEngine(); }