# State Manager Centralized state management module for Code Review workflow. Provides atomic operations, automatic backups, validation, and rollback capabilities. ## Overview This module solves the fragile state management problem by providing: - **Atomic updates** - Write to temp file, then rename (prevents corruption) - **Automatic backups** - Every update creates a backup first - **Rollback capability** - Restore from backup on failure - **Schema validation** - Ensure state structure integrity - **Change history** - Track all state modifications ## File Structure ``` {workDir}/ state.json # Current state state.backup.json # Latest backup state-history.json # Change history log ``` ## API Reference ### initState(workDir) Initialize a new state file with default values. ```javascript /** * Initialize state file with default structure * @param {string} workDir - Working directory path * @returns {object} - Initial state object */ function initState(workDir) { const now = new Date().toISOString(); const initialState = { status: 'pending', started_at: now, updated_at: now, context: null, scan_completed: false, scan_summary: null, reviewed_dimensions: [], current_dimension: null, findings: { correctness: [], readability: [], performance: [], security: [], testing: [], architecture: [] }, report_generated: false, report_path: null, current_action: null, completed_actions: [], errors: [], error_count: 0, summary: null }; // Write state file const statePath = `${workDir}/state.json`; Write(statePath, JSON.stringify(initialState, null, 2)); // Initialize history log const historyPath = `${workDir}/state-history.json`; const historyEntry = { entries: [{ timestamp: now, action: 'init', changes: { type: 'initialize', status: 'pending' } }] }; Write(historyPath, JSON.stringify(historyEntry, null, 2)); console.log(`[StateManager] Initialized state at ${statePath}`); return initialState; } ``` ### getState(workDir) Read and parse current state from file. ```javascript /** * Read current state from file * @param {string} workDir - Working directory path * @returns {object|null} - Current state or null if not found */ function getState(workDir) { const statePath = `${workDir}/state.json`; try { const content = Read(statePath); const state = JSON.parse(content); // Validate structure before returning const validation = validateState(state); if (!validation.valid) { console.warn(`[StateManager] State validation warnings: ${validation.warnings.join(', ')}`); } return state; } catch (error) { console.error(`[StateManager] Failed to read state: ${error.message}`); return null; } } ``` ### updateState(workDir, updates) Safely update state with atomic write and automatic backup. ```javascript /** * Safely update state with atomic write * @param {string} workDir - Working directory path * @param {object} updates - Partial state updates to apply * @returns {object} - Updated state object * @throws {Error} - If update fails (automatically rolls back) */ function updateState(workDir, updates) { const statePath = `${workDir}/state.json`; const tempPath = `${workDir}/state.tmp.json`; const backupPath = `${workDir}/state.backup.json`; const historyPath = `${workDir}/state-history.json`; // Step 1: Read current state let currentState; try { currentState = JSON.parse(Read(statePath)); } catch (error) { throw new Error(`Cannot read current state: ${error.message}`); } // Step 2: Create backup before any modification try { Write(backupPath, JSON.stringify(currentState, null, 2)); } catch (error) { throw new Error(`Cannot create backup: ${error.message}`); } // Step 3: Merge updates const now = new Date().toISOString(); const newState = deepMerge(currentState, { ...updates, updated_at: now }); // Step 4: Validate new state const validation = validateState(newState); if (!validation.valid && validation.errors.length > 0) { throw new Error(`Invalid state after update: ${validation.errors.join(', ')}`); } // Step 5: Write to temp file first (atomic preparation) try { Write(tempPath, JSON.stringify(newState, null, 2)); } catch (error) { throw new Error(`Cannot write temp state: ${error.message}`); } // Step 6: Atomic rename (replace original with temp) try { // Read temp and write to original (simulating atomic rename) const tempContent = Read(tempPath); Write(statePath, tempContent); // Clean up temp file Bash(`rm -f "${tempPath}"`); } catch (error) { // Rollback: restore from backup console.error(`[StateManager] Update failed, rolling back: ${error.message}`); try { const backup = Read(backupPath); Write(statePath, backup); } catch (rollbackError) { throw new Error(`Critical: Update failed and rollback failed: ${rollbackError.message}`); } throw new Error(`Update failed, rolled back: ${error.message}`); } // Step 7: Record in history try { let history = { entries: [] }; try { history = JSON.parse(Read(historyPath)); } catch (e) { // History file may not exist, start fresh } history.entries.push({ timestamp: now, action: 'update', changes: summarizeChanges(currentState, newState, updates) }); // Keep only last 100 entries if (history.entries.length > 100) { history.entries = history.entries.slice(-100); } Write(historyPath, JSON.stringify(history, null, 2)); } catch (error) { // History logging failure is non-critical console.warn(`[StateManager] Failed to log history: ${error.message}`); } console.log(`[StateManager] State updated successfully`); return newState; } /** * Deep merge helper - merges nested objects */ function deepMerge(target, source) { const result = { ...target }; for (const key of Object.keys(source)) { if (source[key] === null || source[key] === undefined) { result[key] = source[key]; } else if (Array.isArray(source[key])) { result[key] = source[key]; } else if (typeof source[key] === 'object' && typeof target[key] === 'object') { result[key] = deepMerge(target[key], source[key]); } else { result[key] = source[key]; } } return result; } /** * Summarize changes for history logging */ function summarizeChanges(oldState, newState, updates) { const changes = {}; for (const key of Object.keys(updates)) { if (key === 'updated_at') continue; const oldVal = oldState[key]; const newVal = newState[key]; if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { changes[key] = { from: typeof oldVal === 'object' ? '[object]' : oldVal, to: typeof newVal === 'object' ? '[object]' : newVal }; } } return changes; } ``` ### validateState(state) Validate state structure against schema. ```javascript /** * Validate state structure * @param {object} state - State object to validate * @returns {object} - { valid: boolean, errors: string[], warnings: string[] } */ function validateState(state) { const errors = []; const warnings = []; // Required fields const requiredFields = ['status', 'started_at', 'updated_at']; for (const field of requiredFields) { if (state[field] === undefined) { errors.push(`Missing required field: ${field}`); } } // Status validation const validStatuses = ['pending', 'running', 'completed', 'failed', 'user_exit']; if (state.status && !validStatuses.includes(state.status)) { errors.push(`Invalid status: ${state.status}. Must be one of: ${validStatuses.join(', ')}`); } // Timestamp format validation const timestampFields = ['started_at', 'updated_at', 'completed_at']; for (const field of timestampFields) { if (state[field] && !isValidISOTimestamp(state[field])) { warnings.push(`Invalid timestamp format for ${field}`); } } // Findings structure validation if (state.findings) { const expectedDimensions = ['correctness', 'readability', 'performance', 'security', 'testing', 'architecture']; for (const dim of expectedDimensions) { if (!Array.isArray(state.findings[dim])) { warnings.push(`findings.${dim} should be an array`); } } } // Context validation (when present) if (state.context !== null && state.context !== undefined) { const contextFields = ['target_path', 'files', 'language', 'total_lines', 'file_count']; for (const field of contextFields) { if (state.context[field] === undefined) { warnings.push(`context.${field} is missing`); } } } // Error count validation if (typeof state.error_count !== 'number') { warnings.push('error_count should be a number'); } // Array fields validation const arrayFields = ['reviewed_dimensions', 'completed_actions', 'errors']; for (const field of arrayFields) { if (state[field] !== undefined && !Array.isArray(state[field])) { errors.push(`${field} must be an array`); } } return { valid: errors.length === 0, errors, warnings }; } /** * Check if string is valid ISO timestamp */ function isValidISOTimestamp(str) { if (typeof str !== 'string') return false; const date = new Date(str); return !isNaN(date.getTime()) && str.includes('T'); } ``` ### backupState(workDir) Create a manual backup of current state. ```javascript /** * Create a manual backup of current state * @param {string} workDir - Working directory path * @param {string} [suffix] - Optional suffix for backup file name * @returns {string} - Backup file path */ function backupState(workDir, suffix = null) { const statePath = `${workDir}/state.json`; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupName = suffix ? `state.backup.${suffix}.json` : `state.backup.${timestamp}.json`; const backupPath = `${workDir}/${backupName}`; try { const content = Read(statePath); Write(backupPath, content); console.log(`[StateManager] Backup created: ${backupPath}`); return backupPath; } catch (error) { throw new Error(`Failed to create backup: ${error.message}`); } } ``` ### restoreState(workDir, backupPath) Restore state from a backup file. ```javascript /** * Restore state from a backup file * @param {string} workDir - Working directory path * @param {string} [backupPath] - Path to backup file (default: latest backup) * @returns {object} - Restored state object */ function restoreState(workDir, backupPath = null) { const statePath = `${workDir}/state.json`; const defaultBackup = `${workDir}/state.backup.json`; const historyPath = `${workDir}/state-history.json`; const sourcePath = backupPath || defaultBackup; try { // Read backup const backupContent = Read(sourcePath); const backupState = JSON.parse(backupContent); // Validate backup state const validation = validateState(backupState); if (!validation.valid) { throw new Error(`Backup state is invalid: ${validation.errors.join(', ')}`); } // Create backup of current state before restore (for safety) try { const currentContent = Read(statePath); Write(`${workDir}/state.pre-restore.json`, currentContent); } catch (e) { // Current state may not exist, that's okay } // Update timestamp const now = new Date().toISOString(); backupState.updated_at = now; // Write restored state Write(statePath, JSON.stringify(backupState, null, 2)); // Log to history try { let history = { entries: [] }; try { history = JSON.parse(Read(historyPath)); } catch (e) {} history.entries.push({ timestamp: now, action: 'restore', changes: { source: sourcePath } }); Write(historyPath, JSON.stringify(history, null, 2)); } catch (e) { console.warn(`[StateManager] Failed to log restore to history`); } console.log(`[StateManager] State restored from ${sourcePath}`); return backupState; } catch (error) { throw new Error(`Failed to restore state: ${error.message}`); } } ``` ## Convenience Functions ### getNextDimension(state) Get the next dimension to review based on current state. ```javascript /** * Get next dimension to review * @param {object} state - Current state * @returns {string|null} - Next dimension or null if all reviewed */ function getNextDimension(state) { const dimensions = ['correctness', 'security', 'performance', 'readability', 'testing', 'architecture']; const reviewed = state.reviewed_dimensions || []; for (const dim of dimensions) { if (!reviewed.includes(dim)) { return dim; } } return null; } ``` ### addFinding(workDir, finding) Add a new finding to the state. ```javascript /** * Add a finding to the appropriate dimension * @param {string} workDir - Working directory path * @param {object} finding - Finding object (must include dimension field) * @returns {object} - Updated state */ function addFinding(workDir, finding) { if (!finding.dimension) { throw new Error('Finding must have a dimension field'); } const state = getState(workDir); const dimension = finding.dimension; // Generate ID if not provided if (!finding.id) { const prefixes = { correctness: 'CORR', readability: 'READ', performance: 'PERF', security: 'SEC', testing: 'TEST', architecture: 'ARCH' }; const prefix = prefixes[dimension] || 'MISC'; const count = (state.findings[dimension]?.length || 0) + 1; finding.id = `${prefix}-${String(count).padStart(3, '0')}`; } const currentFindings = state.findings[dimension] || []; return updateState(workDir, { findings: { ...state.findings, [dimension]: [...currentFindings, finding] } }); } ``` ### markDimensionComplete(workDir, dimension) Mark a dimension as reviewed. ```javascript /** * Mark a dimension as reviewed * @param {string} workDir - Working directory path * @param {string} dimension - Dimension name * @returns {object} - Updated state */ function markDimensionComplete(workDir, dimension) { const state = getState(workDir); const reviewed = state.reviewed_dimensions || []; if (reviewed.includes(dimension)) { console.warn(`[StateManager] Dimension ${dimension} already marked as reviewed`); return state; } return updateState(workDir, { reviewed_dimensions: [...reviewed, dimension], current_dimension: null }); } ``` ### recordError(workDir, action, message) Record an error in state. ```javascript /** * Record an execution error * @param {string} workDir - Working directory path * @param {string} action - Action that failed * @param {string} message - Error message * @returns {object} - Updated state */ function recordError(workDir, action, message) { const state = getState(workDir); const errors = state.errors || []; const errorCount = (state.error_count || 0) + 1; const newError = { action, message, timestamp: new Date().toISOString() }; const newState = updateState(workDir, { errors: [...errors, newError], error_count: errorCount }); // Auto-fail if error count exceeds threshold if (errorCount >= 3) { return updateState(workDir, { status: 'failed' }); } return newState; } ``` ## Usage Examples ### Initialize and Run Review ```javascript // Initialize new review session const workDir = '/path/to/review-session'; const state = initState(workDir); // Update status to running updateState(workDir, { status: 'running' }); // After collecting context updateState(workDir, { context: { target_path: '/src/auth', files: ['auth.ts', 'login.ts'], language: 'typescript', total_lines: 500, file_count: 2 } }); // After completing quick scan updateState(workDir, { scan_completed: true, scan_summary: { risk_areas: [{ file: 'auth.ts', reason: 'Complex logic', priority: 'high' }], complexity_score: 7.5, quick_issues: [] } }); ``` ### Add Findings During Review ```javascript // Add a security finding addFinding(workDir, { dimension: 'security', severity: 'high', category: 'injection', file: 'auth.ts', line: 45, description: 'SQL injection vulnerability', recommendation: 'Use parameterized queries' }); // Mark dimension complete markDimensionComplete(workDir, 'security'); ``` ### Error Handling with Rollback ```javascript try { updateState(workDir, { status: 'running', current_action: 'deep-review' }); // ... do review work ... } catch (error) { // Record error recordError(workDir, 'deep-review', error.message); // If needed, restore from backup restoreState(workDir); } ``` ### Check Review Progress ```javascript const state = getState(workDir); const nextDim = getNextDimension(state); if (nextDim) { console.log(`Next dimension to review: ${nextDim}`); updateState(workDir, { current_dimension: nextDim }); } else { console.log('All dimensions reviewed'); } ``` ## Integration with Orchestrator Update the orchestrator to use StateManager: ```javascript // In orchestrator.md - Replace direct state operations with StateManager calls // OLD: const state = JSON.parse(Read(`${workDir}/state.json`)); // NEW: const state = getState(workDir); // OLD: function updateState(updates) { const state = JSON.parse(Read(`${workDir}/state.json`)); const newState = { ...state, ...updates, updated_at: new Date().toISOString() }; Write(`${workDir}/state.json`, JSON.stringify(newState, null, 2)); return newState; } // NEW: // Import from state-manager.md // updateState(workDir, updates) - handles atomic write, backup, validation // Error handling - OLD: updateState({ errors: [...(state.errors || []), { action: actionId, message: error.message, timestamp: new Date().toISOString() }], error_count: (state.error_count || 0) + 1 }); // Error handling - NEW: recordError(workDir, actionId, error.message); ``` ## State History Format The `state-history.json` file tracks all state changes: ```json { "entries": [ { "timestamp": "2024-01-01T10:00:00.000Z", "action": "init", "changes": { "type": "initialize", "status": "pending" } }, { "timestamp": "2024-01-01T10:01:00.000Z", "action": "update", "changes": { "status": { "from": "pending", "to": "running" }, "current_action": { "from": null, "to": "action-collect-context" } } }, { "timestamp": "2024-01-01T10:05:00.000Z", "action": "restore", "changes": { "source": "/path/state.backup.json" } } ] } ``` ## Error Recovery Strategies | Scenario | Strategy | Function | |----------|----------|----------| | State file corrupted | Restore from backup | `restoreState(workDir)` | | Invalid state after update | Auto-rollback (built-in) | N/A (automatic) | | Multiple errors | Auto-fail after 3 | `recordError()` | | Need to retry from checkpoint | Restore specific backup | `restoreState(workDir, backupPath)` | | Review interrupted | Resume from saved state | `getState(workDir)` | ## Best Practices 1. **Always use `updateState()`** - Never write directly to state.json 2. **Check validation warnings** - Warnings may indicate data issues 3. **Use convenience functions** - `addFinding()`, `markDimensionComplete()`, etc. 4. **Monitor history** - Check state-history.json for debugging 5. **Create named backups** - Before major operations: `backupState(workDir, 'pre-deep-review')`