/** * Guardian Watchdog - Scans files against governance rules * Now fetches rules from API with fallback to cache/defaults */ import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import axios from 'axios'; import { getApiUrl, getApiKey } from './config.js'; interface EffectiveRule { id: string; rule_name: string; rule_type: string; value: Record; severity: 'critical' | 'warning' | 'info'; description: string; source: 'global' | 'project_override'; is_enabled: boolean; } interface ScanResult { file: string; lines: number; status: 'OK' | 'WARNING' | 'VIOLATION'; } const DEFAULT_LMAX = 400; const DEFAULT_LMAX_WARNING = 350; const CACHE_FILE = '.rigstate/rules-cache.json'; async function countLines(filePath: string): Promise { try { const content = await fs.readFile(filePath, 'utf-8'); return content.split('\n').length; } catch (e) { return 0; } } async function getFiles(dir: string, extension: string[]): Promise { const entries = await fs.readdir(dir, { withFileTypes: true }); const files = await Promise.all(entries.map(async (entry) => { const res = path.resolve(dir, entry.name); if (entry.isDirectory()) { if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next' || entry.name === 'dist') return []; return getFiles(res, extension); } else { return extension.some(ext => entry.name.endsWith(ext)) ? res : []; } })); return files.flat(); } /** * Fetch rules from API with fallback */ async function fetchRulesFromApi(projectId: string): Promise<{ lmax: number; lmaxWarning: number; source: string; }> { try { const apiUrl = getApiUrl(); const apiKey = getApiKey(); const response = await axios.get(`${apiUrl}/api/v1/guardian/rules`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }); if (response.data.success && response.data.data.settings) { return { lmax: response.data.data.settings.lmax || DEFAULT_LMAX, lmaxWarning: response.data.data.settings.lmax_warning || DEFAULT_LMAX_WARNING, source: 'API (Dynamic)' }; } } catch (error) { // Try to load from cache try { const cachePath = path.join(process.cwd(), CACHE_FILE); const content = await fs.readFile(cachePath, 'utf-8'); const cached = JSON.parse(content); if (cached.settings) { return { lmax: cached.settings.lmax || DEFAULT_LMAX, lmaxWarning: cached.settings.lmax_warning || DEFAULT_LMAX_WARNING, source: 'Cache (Fallback)' }; } } catch { // Cache read failed } } // Default fallback return { lmax: DEFAULT_LMAX, lmaxWarning: DEFAULT_LMAX_WARNING, source: 'Default (Hardcoded)' }; } export async function runGuardianWatchdog( rootPath: string, settings: Record = {}, projectId?: string ): Promise { console.log(chalk.bold('\nšŸ›”ļø Active Guardian Watchdog Initiated...')); // Try to get rules from API if projectId is provided let lmax = settings.lmax || DEFAULT_LMAX; let lmaxWarning = settings.lmax_warning || DEFAULT_LMAX_WARNING; let ruleSource = settings.lmax ? 'Settings (Passed)' : 'Default'; if (projectId) { const apiRules = await fetchRulesFromApi(projectId); lmax = apiRules.lmax; lmaxWarning = apiRules.lmaxWarning; ruleSource = apiRules.source; } console.log(chalk.dim(`Governance Rules: L_max=${lmax}, L_max_warning=${lmaxWarning}, Source: ${ruleSource}`)); const targetExtensions = ['.ts', '.tsx']; let scanTarget = rootPath; const webSrc = path.join(rootPath, 'apps', 'web', 'src'); try { await fs.access(webSrc); scanTarget = webSrc; } catch { // apps/web/src not found, scanning root or provided path } console.log(chalk.dim(`Scanning target: ${path.relative(process.cwd(), scanTarget)}`)); const files = await getFiles(scanTarget, targetExtensions); let violations = 0; let warnings = 0; const results: ScanResult[] = []; for (const file of files) { const lines = await countLines(file); const relPath = path.relative(rootPath, file); if (lines > lmax) { results.push({ file: relPath, lines, status: 'VIOLATION' }); violations++; console.log(chalk.red(`[VIOLATION] ${relPath}: ${lines} lines (Limit: ${lmax})`)); } else if (lines > lmaxWarning) { results.push({ file: relPath, lines, status: 'WARNING' }); warnings++; console.log(chalk.yellow(`[WARNING] ${relPath}: ${lines} lines (Threshold: ${lmaxWarning})`)); } } if (violations === 0 && warnings === 0) { console.log(chalk.green(`āœ” All ${files.length} files are within governance limits.`)); } else { console.log('\n' + chalk.bold('Summary:')); console.log(chalk.red(`Violations: ${violations}`)); console.log(chalk.yellow(`Warnings: ${warnings}`)); // --- GOVERNANCE INTERVENTION LOGIC --- const { getGovernanceConfig, setSoftLock, InterventionLevel } = await import('./governance.js'); const { governance } = await getGovernanceConfig(rootPath); console.log(chalk.dim(`Intervention Level: ${InterventionLevel[governance.intervention_level] || 'UNKNOWN'} (${governance.intervention_level})`)); if (violations > 0) { console.log(chalk.red.bold('\nCRITICAL: Governance violations detected. Immediate refactoring required.')); // Check for SENTINEL MODE (Level 2) if (governance.intervention_level >= InterventionLevel.SENTINEL) { console.log(chalk.red.bold('šŸ›‘ SENTINEL MODE: Session SOFT_LOCKED until resolved.')); console.log(chalk.red(' Run "rigstate override --reason \\"...\\"" if this is an emergency.')); await setSoftLock('Sentinel Mode: Governance Violations Detected', 'ARC-VIOLATION', rootPath); } } } // Sync to Cloud via API if (projectId) { try { const apiUrl = getApiUrl(); const apiKey = getApiKey(); const payloadViolations = results.filter(r => r.status === 'VIOLATION').map(v => ({ uid: 'V-' + Buffer.from(v.file).toString('base64').replace(/=/g, ''), filePath: v.file, lineCount: v.lines, limitValue: lmax, severity: 'CRITICAL' })); await axios.post(`${apiUrl}/api/v1/guardian/sync`, { projectId, violations: payloadViolations, warnings: warnings }, { headers: { Authorization: `Bearer ${apiKey}` } }); console.log(chalk.dim('āœ” Violations synced to Rigstate Cloud.')); } catch (e: any) { console.log(chalk.dim('⚠ Cloud sync skipped: ' + (e.message || 'Unknown'))); } } }