mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
- Introduced architecture rules to detect circular dependencies, god classes, layer violations, and mixed concerns. - Added correctness rules focusing on null checks, empty catch blocks, unreachable code, and type coercion. - Implemented performance rules addressing nested loops, synchronous I/O, memory leaks, and unnecessary re-renders in React. - Created readability rules to improve function length, variable naming, deep nesting, magic numbers, and commented code. - Established security rules to identify XSS risks, hardcoded secrets, SQL injection vulnerabilities, and insecure random generation. - Developed testing rules to enhance test quality, coverage, and maintainability, including missing assertions and error path testing. - Documented the structure and schema for rule files in the index.md for better understanding and usage.
19 KiB
19 KiB
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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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
// 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
// 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
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
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:
// 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:
{
"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
- Always use
updateState()- Never write directly to state.json - Check validation warnings - Warnings may indicate data issues
- Use convenience functions -
addFinding(),markDimensionComplete(), etc. - Monitor history - Check state-history.json for debugging
- Create named backups - Before major operations:
backupState(workDir, 'pre-deep-review')