UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

450 lines (399 loc) 12.8 kB
/** * MDAP Diff Applicator Module * * Pure functions for applying fix instructions to source code. * No LLM involvement - deterministic code transformation with syntax validation. * * Extracted from Trigger.dev cfn-mdap-implementer diff mode logic * * @module diff-applicator * @version 1.0.0 */ import { type FixInstruction, } from './types.js'; // ============================================= // Types // ============================================= /** Result of applying fixes */ export interface ApplyFixesResult { /** Whether all fixes were applied successfully */ success: boolean; /** The modified content */ content: string; /** Number of fixes applied */ fixesApplied: number; /** Any fixes that failed to apply */ failedFixes: Array<{ fix: FixInstruction; reason: string }>; /** Whether syntax validation passed */ syntaxValid: boolean; } // ============================================= // Syntax Validation Functions // ============================================= /** * Validate bracket/brace/paren balance in code * Skips strings and comments for accurate validation */ function validateSyntax(content: string, language: string = 'typescript'): boolean { const stack: string[] = []; const pairs: Record<string, string> = { "(": ")", "[": "]", "{": "}" }; const opens = new Set(Object.keys(pairs)); const closes = new Set(Object.values(pairs)); let inString = false; let stringChar = ""; let inLineComment = false; let inBlockComment = false; // Handle language-specific comment delimiters const isTypeScriptLike = ['typescript', 'javascript', 'tsx', 'jsx'].includes(language.toLowerCase()); const isRust = language.toLowerCase() === 'rust'; for (let i = 0; i < content.length; i++) { const char = content[i]; const next = content[i + 1]; const prev = content[i - 1]; // Handle newlines if (char === "\n") { inLineComment = false; continue; } // Skip line comments if (!inString && !inBlockComment) { if (isTypeScriptLike && char === "/" && next === "/") { inLineComment = true; continue; } if (isRust && char === "/" && next === "/") { inLineComment = true; continue; } } if (inLineComment) continue; // Skip block comments if (!inString && !inBlockComment) { if (isTypeScriptLike && char === "/" && next === "*") { inBlockComment = true; i++; continue; } if (isRust && char === "/" && next === "*") { inBlockComment = true; i++; continue; } } if (inBlockComment) { if (char === "*" && next === "/") { inBlockComment = false; i++; continue; } continue; } // Handle strings (supports escape characters) if ((char === '"' || char === "'" || char === "`") && prev !== "\\") { if (!inString) { inString = true; stringChar = char; } else if (char === stringChar) { inString = false; } continue; } if (inString) continue; // Track brackets if (opens.has(char)) { stack.push(pairs[char]); } else if (closes.has(char)) { if (stack.length === 0 || stack.pop() !== char) { return false; } } } return stack.length === 0; } // ============================================= // Fix Application Functions // ============================================= /** * Apply fix instructions to file content deterministically * No LLM involved - pure code transformation with syntax validation */ export function applyFixes( content: string, fixes: FixInstruction[], language: string = 'typescript' ): ApplyFixesResult { const lines = content.split('\n'); const failedFixes: Array<{ fix: FixInstruction; reason: string }> = []; let fixesApplied = 0; // Sort fixes in reverse line order to preserve line numbers during modification const sortedFixes = [...fixes].sort((a, b) => { // Primary sort: by line number in descending order if (b.line !== a.line) return b.line - a.line; // Secondary sort: by action priority for same line // This ensures predictable behavior when multiple fixes target the same line const actionPriority = { delete: 3, // Delete first (removes lines) replace: 2, // Then replace insert_after: 1, // Then insert after insert_before: 0 // Finally insert before }; return actionPriority[b.action] - actionPriority[a.action]; }); for (const fix of sortedFixes) { const lineIndex = fix.line - 1; // Convert to 0-indexed // Validate line number is in range if (lineIndex < 0 || lineIndex >= lines.length) { failedFixes.push({ fix, reason: `Line ${fix.line} out of range (file has ${lines.length} lines)` }); continue; } try { switch (fix.action) { case 'replace': { if (fix.content === undefined) { failedFixes.push({ fix, reason: 'Replace action requires content' }); continue; } const endLine = fix.endLine ? fix.endLine - 1 : lineIndex; // Validate end line is in range if (endLine >= lines.length) { failedFixes.push({ fix, reason: `End line ${fix.endLine} out of range (file has ${lines.length} lines)` }); continue; } // Validate end line is not before start line if (endLine < lineIndex) { failedFixes.push({ fix, reason: `End line ${fix.endLine} is before start line ${fix.line}` }); continue; } const deleteCount = endLine - lineIndex + 1; const newLines = fix.content.split('\n'); lines.splice(lineIndex, deleteCount, ...newLines); fixesApplied++; break; } case 'insert_before': { if (fix.content === undefined) { failedFixes.push({ fix, reason: 'Insert action requires content' }); continue; } const newLines = fix.content.split('\n'); lines.splice(lineIndex, 0, ...newLines); fixesApplied++; break; } case 'insert_after': { if (fix.content === undefined) { failedFixes.push({ fix, reason: 'Insert action requires content' }); continue; } const newLines = fix.content.split('\n'); lines.splice(lineIndex + 1, 0, ...newLines); fixesApplied++; break; } case 'delete': { const endLine = fix.endLine ? fix.endLine - 1 : lineIndex; // Validate end line is in range if (endLine >= lines.length) { failedFixes.push({ fix, reason: `End line ${fix.endLine} out of range (file has ${lines.length} lines)` }); continue; } // Validate end line is not before start line if (endLine < lineIndex) { failedFixes.push({ fix, reason: `End line ${fix.endLine} is before start line ${fix.line}` }); continue; } const deleteCount = endLine - lineIndex + 1; lines.splice(lineIndex, deleteCount); fixesApplied++; break; } default: failedFixes.push({ fix, reason: `Unknown action: ${(fix as any).action}` }); } } catch (err) { failedFixes.push({ fix, reason: `Exception: ${(err as Error).message}` }); } } const newContent = lines.join('\n'); const syntaxValid = validateSyntax(newContent, language); return { success: failedFixes.length === 0 && syntaxValid, content: newContent, fixesApplied, failedFixes, syntaxValid }; } /** * Validate a single fix instruction before applying * Returns detailed validation result */ export function validateFix( fix: FixInstruction, content: string, language: string = 'typescript' ): { valid: boolean; reason?: string } { const lines = content.split('\n'); const lineIndex = fix.line - 1; // Check line number is positive if (fix.line < 1) { return { valid: false, reason: 'Line number must be >= 1' }; } // Check action is valid if (!['replace', 'insert_before', 'insert_after', 'delete'].includes(fix.action)) { return { valid: false, reason: `Invalid action: ${fix.action}` }; } // Check content is provided when needed if (fix.action !== 'delete' && fix.content === undefined) { return { valid: false, reason: `Content required for ${fix.action} action` }; } // Check line range if (lineIndex >= lines.length) { return { valid: false, reason: `Line ${fix.line} out of range (file has ${lines.length} lines)` }; } // Check end line for multi-line operations if (fix.endLine) { if (fix.endLine < fix.line) { return { valid: false, reason: `End line ${fix.endLine} is before start line ${fix.line}` }; } if (fix.endLine - 1 >= lines.length) { return { valid: false, reason: `End line ${fix.endLine} out of range (file has ${lines.length} lines)` }; } } // For replace operations, optionally check original content matches if (fix.action === 'replace' && fix.original) { const endLine = fix.endLine ? fix.endLine - 1 : lineIndex; const actualContent = lines.slice(lineIndex, endLine + 1).join('\n'); if (actualContent !== fix.original) { return { valid: false, reason: `Original content mismatch at line ${fix.line}` }; } } return { valid: true }; } /** * Apply a single fix instruction with detailed error reporting */ export function applySingleFix( content: string, fix: FixInstruction, language: string = 'typescript' ): ApplyFixesResult { // Validate the fix first const validation = validateFix(fix, content, language); if (!validation.valid) { return { success: false, content, fixesApplied: 0, failedFixes: [{ fix, reason: validation.reason! }], syntaxValid: validateSyntax(content, language) }; } // Apply the fix return applyFixes(content, [fix], language); } /** * Preview changes without applying them * Returns a diff-like representation of what would change */ export function previewFixes( content: string, fixes: FixInstruction[] ): Array<{ line: number; action: string; before?: string; after?: string; description: string; }> { const lines = content.split('\n'); const previews: Array<{ line: number; action: string; before?: string; after?: string; description: string; }> = []; for (const fix of fixes) { const lineIndex = fix.line - 1; const description: string[] = []; switch (fix.action) { case 'replace': { const endLine = fix.endLine ? fix.endLine - 1 : lineIndex; const beforeLines = lines.slice(lineIndex, endLine + 1); previews.push({ line: fix.line, action: 'replace', before: beforeLines.join('\n'), after: fix.content, description: `Replace line(s) ${fix.line}${fix.endLine ? `-${fix.endLine}` : ''}` }); break; } case 'insert_before': { previews.push({ line: fix.line, action: 'insert_before', after: fix.content, description: `Insert before line ${fix.line}` }); break; } case 'insert_after': { previews.push({ line: fix.line, action: 'insert_after', after: fix.content, description: `Insert after line ${fix.line}` }); break; } case 'delete': { const endLine = fix.endLine ? fix.endLine - 1 : lineIndex; const beforeLines = lines.slice(lineIndex, endLine + 1); previews.push({ line: fix.line, action: 'delete', before: beforeLines.join('\n'), description: `Delete line(s) ${fix.line}${fix.endLine ? `-${fix.endLine}` : ''}` }); break; } } } return previews; }