← vibesdk
worker/agents/output-formats/diff-formats/udiff.ts
27.2 KB
Download file
// Borrowed from the way Aider handles unified diffs
/**
* Parses a hunk string into its "before" and "after" components.
* 'before' consists of context (' ') and deletion ('-') lines.
* 'after' consists of context (' ') and addition ('+') lines.
* @param hunk - An array of strings representing the lines of a single diff hunk.
* @returns An object with 'before' and 'after' string arrays.
*/
function hunkToBeforeAfter(hunk: string[]): {
before: string[];
after: string[];
} {
const before: string[] = [];
const after: string[] = [];
for (const line of hunk) {
if (
line.startsWith('---') ||
line.startsWith('+++') ||
line.startsWith('@@')
) {
continue;
}
const op = line[0];
const restOfLine = line.substring(1);
switch (op) {
case ' ':
before.push(restOfLine);
after.push(restOfLine);
break;
case '-':
before.push(restOfLine);
break;
case '+':
after.push(restOfLine);
break;
}
}
return { before, after };
}
/**
* Normalize leading whitespace for relative indentation matching
* Handles uniformly indented/outdented code blocks
*/
function normalizeIndentation(lines: string[]): {
normalized: string[],
isEmpty: boolean
} {
if (lines.length === 0) return { normalized: [], isEmpty: true };
// Find non-empty lines for common prefix calculation
const nonEmptyLines = lines.filter(line => line.trim().length > 0);
if (nonEmptyLines.length === 0) return { normalized: lines, isEmpty: true };
// Find common leading whitespace
let commonPrefix = nonEmptyLines[0].match(/^\s*/)?.[0] || '';
for (const line of nonEmptyLines.slice(1)) {
const prefix = line.match(/^\s*/)?.[0] || '';
let i = 0;
while (i < Math.min(commonPrefix.length, prefix.length) &&
commonPrefix[i] === prefix[i]) {
i++;
}
commonPrefix = commonPrefix.substring(0, i);
}
// Remove common prefix from all lines
// SAFETY: Check if line has content beyond the common prefix
const normalized = lines.map(line => {
// Empty lines stay empty
if (line.length === 0) return line;
// Lines that are only whitespace up to common prefix become empty
if (line.length <= commonPrefix.length) return '';
// Otherwise remove the common prefix
return line.substring(commonPrefix.length);
});
return { normalized, isEmpty: false };
}
/**
* Break a large hunk into smaller overlapping sub-hunks
* Each sub-hunk contains one contiguous run of changes with context
*/
function breakIntoSubHunks(hunk: string[], maxContextLines: number = 3): string[][] {
const subHunks: string[][] = [];
// Find change runs (contiguous sequences of + and - lines)
const changeRuns: Array<{start: number, end: number, lines: string[]}> = [];
let currentRun: string[] = [];
let runStart = -1;
for (let i = 0; i < hunk.length; i++) {
const line = hunk[i];
if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
continue;
}
if (line.startsWith('-') || line.startsWith('+')) {
if (currentRun.length === 0) {
runStart = i;
}
currentRun.push(line);
} else {
if (currentRun.length > 0) {
changeRuns.push({
start: runStart,
end: i - 1,
lines: [...currentRun]
});
currentRun = [];
}
}
}
// Handle final run
if (currentRun.length > 0) {
changeRuns.push({
start: runStart,
end: hunk.length - 1,
lines: [...currentRun]
});
}
// Create sub-hunks with context
for (const run of changeRuns) {
// SAFETY: Only include context lines, not hunk headers
const contextBefore = hunk.slice(
Math.max(0, run.start - maxContextLines),
run.start
).filter(line => line.startsWith(' ')); // Only context lines
const contextAfter = hunk.slice(
run.end + 1,
Math.min(hunk.length, run.end + 1 + maxContextLines)
).filter(line => line.startsWith(' ')); // Only context lines
subHunks.push([...contextBefore, ...run.lines, ...contextAfter]);
}
return subHunks.length > 1 ? subHunks : [hunk];
}
// correctMissingPlusMarkers removed - too risky for LLM-generated diffs
// Removed isLikelyCodeAddition function as it's no longer used
// after disabling the risky correctMissingPlusMarkers strategy
// normalizeHunk function removed - it was disabled for safety
/**
* Try direct application of the hunk with enhanced robustness and fuzzy matching
*/
function tryDirectApplication(content: string, hunk: string[]): string | null {
const { before, after } = hunkToBeforeAfter(hunk);
if (before.length === 0) {
// Pure addition, append to end
return content + '\n' + after.join('\n');
}
// Strategy 1: Try exact match first
const beforeBlock = before.join('\n');
const occurrences = content.split(beforeBlock).length - 1;
if (occurrences === 1) {
const afterBlock = after.join('\n');
// Use a replacer function so `$`-sequences in the generated replacement
// (e.g. `$&`, `$$`, `$1`) are inserted literally, not interpreted by
// String.prototype.replace.
return content.replace(beforeBlock, () => afterBlock);
}
// Strategy 2: Try with whitespace normalization
if (occurrences === 0) {
const result = tryWithWhitespaceNormalization(content, before, after);
if (result !== null) return result;
}
// Strategy 3: Try with indentation-aware matching
if (occurrences === 0) {
const result = tryWithIndentationAwareMatching(content, before, after);
if (result !== null) return result;
}
// Strategy 4: Fuzzy matching disabled for safety
// Would be too risky with LLM-generated content
return null;
}
/**
* Strategy 2: Whitespace normalization matching
*/
function tryWithWhitespaceNormalization(content: string, before: string[], after: string[]): string | null {
// First normalize line endings
const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
// Helper to normalize whitespace in a line for comparison
const normalizeLineWhitespace = (line: string): string => {
// Normalize all whitespace to single spaces for comparison
// This handles tabs, multiple spaces, etc.
return line.replace(/\s+/g, ' ').trim();
};
const contentLines = normalizedContent.split('\n');
const normalizedBefore = before.map(normalizeLineWhitespace);
// Try to find the normalized before block in content
for (let i = 0; i <= contentLines.length - before.length; i++) {
let matches = true;
for (let j = 0; j < before.length; j++) {
const contentLine = normalizeLineWhitespace(contentLines[i + j] || '');
const beforeLine = normalizedBefore[j];
if (contentLine !== beforeLine) {
matches = false;
break;
}
}
if (matches) {
// Found a match! Now we need to apply the replacement
// Detect indentation pattern from the original content
const originalLines = content.split(/\r?\n/);
const originalSlice = originalLines.slice(i, i + before.length);
// Process after lines to match original indentation style
const processedAfter = after.map((line, idx) => {
if (line.trim() === '') return line;
// Get reference line from original to copy indentation style
const refLine = originalSlice[Math.min(idx, originalSlice.length - 1)] || originalSlice[0] || '';
const refIndent = refLine.match(/^[\t ]*/)?.[0] || '';
// Get the relative indentation from the diff
const diffIndent = line.match(/^[\t ]*/)?.[0] || '';
const diffSpaceCount = diffIndent.replace(/\t/g, ' ').length;
// If original uses tabs or mixed indentation
if (refIndent.includes('\t')) {
// Check if it's mixed (tab + spaces)
const leadingTabs = refIndent.match(/^\t*/)?.[0].length || 0;
const followingSpaces = refIndent.slice(leadingTabs).length;
if (diffSpaceCount === 0) {
return line.trim();
} else if (diffSpaceCount === 1 && followingSpaces > 0) {
// Single space in diff might map to mixed tab+space
return '\t' + ' '.repeat(followingSpaces) + line.trim();
} else {
// Convert spaces to tabs
const tabCount = Math.floor(diffSpaceCount / 2);
const remainingSpaces = diffSpaceCount % 2;
return '\t'.repeat(tabCount) + ' '.repeat(remainingSpaces) + line.trim();
}
} else {
// Original uses spaces, keep spaces
return ' '.repeat(diffSpaceCount) + line.trim();
}
});
// Reconstruct with original line endings
const hasCarriageReturn = content.includes('\r\n');
const lineEnding = hasCarriageReturn ? '\r\n' : '\n';
const resultLines = [
...originalLines.slice(0, i),
...processedAfter,
...originalLines.slice(i + before.length)
];
return resultLines.join(lineEnding);
}
}
return null;
}
/**
* Strategy 3: Indentation-aware matching
* Handles cases where the diff has different base indentation than the content
*/
function tryWithIndentationAwareMatching(content: string, before: string[], after: string[]): string | null {
const contentLines = content.split('\n');
// Normalize the before block to remove common indentation
const { normalized: normalizedBefore } = normalizeIndentation(before);
// Try matching with different indentation levels
for (let i = 0; i <= contentLines.length - before.length; i++) {
const contentSlice = contentLines.slice(i, i + before.length);
const { normalized: normalizedContent } = normalizeIndentation(contentSlice);
// Check if normalized content matches normalized before
let matches = true;
for (let j = 0; j < normalizedBefore.length; j++) {
if (normalizedContent[j] !== normalizedBefore[j]) {
matches = false;
break;
}
}
if (matches) {
// We found a match! Now preserve the original indentation pattern
// The key is to map the normalized 'after' back to the original indentation
// Build a mapping of normalized lines to original indentation
const indentMap = new Map<string, string>();
normalizedBefore.forEach((normLine, idx) => {
const origLine = contentSlice[idx];
const origIndent = origLine.match(/^[\t ]*/)?.[0] || '';
indentMap.set(normLine, origIndent);
});
// Apply the original indentation to the after lines
const indentedAfter = after.map((afterLine, idx) => {
const trimmedAfter = afterLine.trim();
if (trimmedAfter === '') return '';
// Find the corresponding indentation from the before block
const correspondingBeforeLine = normalizedBefore[idx];
// Use the indentation from the corresponding before line
const originalIndent = indentMap.get(correspondingBeforeLine) ||
contentSlice[idx]?.match(/^[\t ]*/)?.[0] || '';
return originalIndent + trimmedAfter;
});
const newLines = [
...contentLines.slice(0, i),
...indentedAfter,
...contentLines.slice(i + before.length)
];
return newLines.join('\n');
}
}
return null;
}
// tryWithFuzzyMatching removed - too risky for LLM-generated diffs
/**
* Try with normalized whitespace to handle indentation issues
* PERFORMANCE: Uses regex-based approach instead of O(n²) iteration
*/
function tryWithNormalizedWhitespace(content: string, hunk: string[]): string | null {
const { before, after } = hunkToBeforeAfter(hunk);
if (before.length === 0) return null;
const { normalized } = normalizeIndentation(before);
// PERFORMANCE FIX: Create a flexible regex pattern instead of iterating through all slices
// This changes from O(content_length * hunk_length) to O(content_length)
const regexPattern = createFlexibleWhitespacePattern(normalized);
if (!regexPattern) return null;
const matches = content.match(regexPattern);
if (!matches || matches.length !== 1) {
return null; // Must have exactly one match to be safe
}
// Found exactly one match - apply the replacement
const matchedText = matches[0];
const { normalized: normalizedAfter } = normalizeIndentation(after);
// Detect the actual indentation used in the matched content
const actualIndentation = detectIndentation(matchedText);
// Apply the detected indentation to the replacement
const replacementLines = normalizedAfter.map(line =>
line.length > 0 ? actualIndentation + line : line
);
const replacementText = replacementLines.join('\n');
// Replacer function: insert the replacement literally (no `$`-pattern expansion).
return content.replace(matchedText, () => replacementText);
}
/**
* PERFORMANCE HELPER: Creates a regex pattern that matches code blocks with flexible whitespace
* SAFETY: Limits pattern complexity to prevent ReDoS
*/
function createFlexibleWhitespacePattern(normalizedLines: string[]): RegExp | null {
if (normalizedLines.length === 0) return null;
// SAFETY: Limit lines to prevent ReDoS
if (normalizedLines.length > 50) return null;
// Escape special regex characters in each line
const escapedLines = normalizedLines.map(line => {
// SAFETY: Limit line length
if (line.length > 500) return null;
return line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
});
// Check if any line failed to escape
if (escapedLines.some(line => line === null)) return null;
// Create pattern that allows flexible leading whitespace
const flexibleLines = escapedLines.map(line => {
if (line!.trim() === '') {
return '\\s*'; // Empty lines can have any whitespace
}
return '\\s*' + line; // Any leading whitespace + the content
});
try {
// Create a pattern that matches the entire block
const pattern = flexibleLines.join('\\n');
// SAFETY: Test the pattern with a timeout
const testRegex = new RegExp(pattern, 'g');
// Quick test to ensure it doesn't hang
const testStr = 'test\ntest\ntest';
testRegex.test(testStr);
return testRegex;
} catch (error) {
// If regex creation fails (complex content), fall back to null
return null;
}
}
/**
* PERFORMANCE HELPER: Detects the actual indentation used in matched content
*/
function detectIndentation(matchedText: string): string {
const lines = matchedText.split('\n');
const nonEmptyLines = lines.filter(line => line.trim().length > 0);
if (nonEmptyLines.length === 0) return '';
// Find the minimum leading whitespace (common indentation)
let minIndentation = nonEmptyLines[0].match(/^\s*/)?.[0] || '';
for (const line of nonEmptyLines.slice(1)) {
const indent = line.match(/^\s*/)?.[0] || '';
if (indent.length < minIndentation.length) {
minIndentation = indent;
}
}
return minIndentation;
}
/**
* Try sub-hunk decomposition with TRANSACTIONAL INTEGRITY
* SAFETY: Only returns result if ALL sub-hunks apply successfully
*/
function trySubHunkDecomposition(content: string, hunk: string[]): string | null {
const subHunks = breakIntoSubHunks(hunk);
if (subHunks.length <= 1) {
return null; // No decomposition possible
}
// SAFETY: First verify ALL sub-hunks can be applied
let tempContent = content;
const results: string[] = [content];
for (const subHunk of subHunks) {
const result = applyHunkWithoutRecursion(tempContent, subHunk);
if (result === null) {
// ANY failure means we abort completely
// No partial application allowed
return null;
}
tempContent = result;
results.push(result);
}
// All sub-hunks succeeded - return the final result
return tempContent;
}
/**
* Apply hunk without recursion to prevent infinite loops in sub-hunk decomposition
* This is a simplified version that doesn't call trySubHunkDecomposition
*/
function applyHunkWithoutRecursion(content: string, hunk: string[]): string | null {
// Strategy 1: Direct application
const directResult = tryDirectApplication(content, hunk);
if (directResult !== null) return directResult;
// Strategy 2: Skip missing plus marker correction (disabled for safety)
// The correctMissingPlusMarkers function is disabled and just returns the input unchanged
// Strategy 3: Normalized whitespace
const whitespaceResult = tryWithNormalizedWhitespace(content, hunk);
if (whitespaceResult !== null) return whitespaceResult;
// Strategy 4: Hunk normalization disabled for safety
// Strategy 5: Context reduction (no sub-hunk decomposition to avoid recursion)
const contextResult = tryContextReduction(content, hunk);
if (contextResult !== null) return contextResult;
// All strategies failed
return null;
}
/**
* Try context reduction strategy with enhanced flexibility
*/
function tryContextReduction(content: string, hunk: string[]): string | null {
// Find core changes
const firstChangeIndex = hunk.findIndex(
(l) => l.startsWith('-') || l.startsWith('+'),
);
const lastChangeIndex =
hunk.length -
1 -
[...hunk]
.reverse()
.findIndex((l) => l.startsWith('-') || l.startsWith('+'));
if (firstChangeIndex === -1) {
return content; // No changes
}
const precedingContext = hunk.slice(0, firstChangeIndex);
const coreChangeHunk = hunk.slice(firstChangeIndex, lastChangeIndex + 1);
const followingContext = hunk.slice(lastChangeIndex + 1);
// Progressive context reduction with more aggressive settings
for (let i = precedingContext.length; i >= 0; i--) {
for (let j = followingContext.length; j >= 0; j--) {
const contextBefore = precedingContext.slice(i);
const contextAfter = followingContext.slice(0, j);
const searchBefore = hunkToBeforeAfter([
...contextBefore,
...coreChangeHunk,
...contextAfter,
]).before;
const searchBlock = searchBefore.join('\n');
if (searchBlock.trim() === '') continue;
const searchOccurrences = content.split(searchBlock).length - 1;
if (searchOccurrences === 1) {
const replaceAfter = hunkToBeforeAfter([
...contextBefore,
...coreChangeHunk,
...contextAfter,
]).after;
const replaceBlock = replaceAfter.join('\n');
// Replacer function: insert the replacement literally (no `$`-pattern expansion).
return content.replace(searchBlock, () => replaceBlock);
}
}
}
return null;
}
// tryHunkNormalization removed - can introduce errors with LLM diffs
/**
* Production-grade hunk application with comprehensive telemetry and monitoring
* Enhanced with all resilience strategies plus performance monitoring
*/
function applyHunkWithTelemetry(
content: string,
hunk: string[],
telemetry: DiffTelemetry,
monitor: PerformanceMonitor
): string | null {
// Strategy 1: Direct application (exact matching)
monitor.incrementIteration();
telemetry.strategiesAttempted.push('exact_match');
const directResult = tryDirectApplication(content, hunk);
if (directResult !== null) {
return directResult;
}
// Strategy 2: Skip missing plus marker correction (disabled for safety)
// Strategy 3: Normalized whitespace (handle outdented/indented code)
monitor.incrementIteration();
telemetry.strategiesAttempted.push('whitespace_normalization');
const whitespaceResult = tryWithNormalizedWhitespace(content, hunk);
if (whitespaceResult !== null) {
return whitespaceResult;
}
// Strategy 4: Sub-hunk decomposition
monitor.incrementIteration();
telemetry.strategiesAttempted.push('sub_hunk_decomposition');
const subHunkResult = trySubHunkDecomposition(content, hunk);
if (subHunkResult !== null) {
return subHunkResult;
}
// Strategy 5: Skip hunk normalization (disabled for safety)
// Strategy 6: Context reduction
monitor.incrementIteration();
telemetry.strategiesAttempted.push('context_reduction');
const contextResult = tryContextReduction(content, hunk);
if (contextResult !== null) {
return contextResult;
}
// All strategies failed
monitor.addWarning('All standard strategies failed, attempting fallback strategies');
return null;
}
/**
* Production-grade unified diff application system
* Enhanced with comprehensive error handling, security measures, and performance protections
*/
// Production configuration constants
const PRODUCTION_LIMITS = {
MAX_CONTENT_SIZE: 10 * 1024 * 1024, // 10MB max content size
MAX_DIFF_SIZE: 1024 * 1024, // 1MB max diff size
MAX_HUNKS: 1000, // Maximum number of hunks
MAX_HUNK_SIZE: 10000, // Maximum lines per hunk
PROCESSING_TIMEOUT: 30000, // 30 second timeout
MAX_ITERATIONS: 50000, // Maximum loop iterations
} as const;
// Telemetry and debugging interface
interface DiffTelemetry {
strategiesAttempted: string[];
processingTimeMs: number;
contentSize: number;
diffSize: number;
hunkCount: number;
success: boolean;
errorDetails?: string;
performanceWarnings: string[];
}
// Security validation utilities
class DiffSecurityValidator {
static validateContent(content: string): void {
if (typeof content !== 'string') {
throw new Error('Content must be a string');
}
if (content.length > PRODUCTION_LIMITS.MAX_CONTENT_SIZE) {
throw new Error(`Content too large: ${content.length} bytes exceeds ${PRODUCTION_LIMITS.MAX_CONTENT_SIZE} byte limit`);
}
}
static validateDiff(diff: string): void {
if (typeof diff !== 'string') {
throw new Error('Diff must be a string');
}
if (diff.length > PRODUCTION_LIMITS.MAX_DIFF_SIZE) {
throw new Error(`Diff too large: ${diff.length} bytes exceeds ${PRODUCTION_LIMITS.MAX_DIFF_SIZE} byte limit`);
}
// Validate diff format structure
const lines = diff.split('\n');
for (const line of lines) {
if (line.length > 10000) { // Prevent extremely long lines
throw new Error('Diff contains excessively long lines');
}
}
// Don't throw on invalid diff format - just let it fail gracefully later
// This allows handling of malformed diffs from LLMs
}
}
// Performance monitoring utilities
class PerformanceMonitor {
private startTime: number;
private iterationCount = 0;
private warnings: string[] = [];
constructor() {
this.startTime = Date.now();
}
checkTimeout(): void {
const elapsed = Date.now() - this.startTime;
if (elapsed > PRODUCTION_LIMITS.PROCESSING_TIMEOUT) {
throw new Error(`Processing timeout: ${elapsed}ms exceeds ${PRODUCTION_LIMITS.PROCESSING_TIMEOUT}ms limit`);
}
}
incrementIteration(): void {
this.iterationCount++;
if (this.iterationCount > PRODUCTION_LIMITS.MAX_ITERATIONS) {
throw new Error(`Maximum iterations exceeded: ${this.iterationCount}`);
}
if (this.iterationCount % 1000 === 0) {
this.checkTimeout();
}
}
addWarning(warning: string): void {
this.warnings.push(warning);
}
getTelemetry(): Omit<DiffTelemetry, 'strategiesAttempted' | 'contentSize' | 'diffSize' | 'hunkCount' | 'success' | 'errorDetails'> {
return {
processingTimeMs: Date.now() - this.startTime,
performanceWarnings: [...this.warnings],
};
}
}
/**
* Production-grade unified diff application with comprehensive hardening
* @param originalContent - The original file content
* @param diffContent - The unified diff to apply
* @param options - Optional configuration for debugging and telemetry
* @returns The modified content after applying the diff
* @throws Error with detailed diagnostics on failure
*/
export function applyDiff(
originalContent: string,
diffContent: string,
options: { enableTelemetry?: boolean; allowFallbackToRaw?: boolean } = {}
): string {
const telemetry: DiffTelemetry = {
strategiesAttempted: [],
processingTimeMs: 0,
contentSize: originalContent.length,
diffSize: diffContent.length,
hunkCount: 0,
success: false,
performanceWarnings: [],
};
const monitor = new PerformanceMonitor();
try {
// Security validation
DiffSecurityValidator.validateContent(originalContent);
DiffSecurityValidator.validateDiff(diffContent);
// Handle edge cases
if (!diffContent || diffContent.trim().length === 0) {
telemetry.success = true;
return originalContent; // No diff to apply
}
if (!originalContent && diffContent.trim().length === 0) {
telemetry.success = true;
return ''; // Empty content, empty diff
}
// Handle @@ ... @@ format (ignore line numbers)
// Only match @@ at line start (hunk headers), not in content
const cleanedDiff = diffContent.replace(/^@@ .*? @@/gm, '@@ ... @@');
// Enhanced hunk parsing with validation
const hunksRaw = cleanedDiff.match(/(?:^|\n)@@[^\n]*(?:\n(?!@@)[^\n]*)*(?=\n@@|$)/g) ||
[cleanedDiff]; // Fallback to single hunk if no matches
if (hunksRaw.length > PRODUCTION_LIMITS.MAX_HUNKS) {
throw new Error(`Too many hunks: ${hunksRaw.length} exceeds ${PRODUCTION_LIMITS.MAX_HUNKS} limit`);
}
const hunks = hunksRaw.map((h, idx) => {
monitor.incrementIteration();
// Clean up the hunk and split into lines
const cleanHunk = h.replace(/^\n/, ''); // Remove leading newline
const hunkLines = cleanHunk.split('\n').filter((line) => line.length > 0);
if (hunkLines.length > PRODUCTION_LIMITS.MAX_HUNK_SIZE) {
throw new Error(`Hunk #${idx + 1} too large: ${hunkLines.length} lines exceeds ${PRODUCTION_LIMITS.MAX_HUNK_SIZE} limit`);
}
return hunkLines;
});
telemetry.hunkCount = hunks.length;
let currentContent = originalContent;
for (let i = 0; i < hunks.length; i++) {
monitor.checkTimeout();
monitor.incrementIteration();
const hunk = hunks[i];
try {
const newContent = applyHunkWithTelemetry(currentContent, hunk, telemetry, monitor);
if (newContent !== null) {
// SAFETY: Verify the result is not corrupted
if (newContent.length === 0 && currentContent.length > 0) {
// Prevent accidental deletion of entire content
throw new Error(`Hunk #${i + 1} would delete entire file content - aborting for safety`);
}
currentContent = newContent;
} else {
// Enhanced error reporting with fallback options
const hunkPreview = hunk.join('\n');
const { before, after } = hunkToBeforeAfter(hunk);
const errorDetails = [
`Hunk #${i + 1} failed to apply cleanly after trying all strategies.`,
``,
`Hunk content (first 500 chars):`,
hunkPreview.substring(0, 500) + (hunkPreview.length > 500 ? '...' : ''),
``,
`Analysis:`,
`- Before lines: ${before.length}`,
`- After lines: ${after.length}`,
`- Strategies attempted: ${telemetry.strategiesAttempted.join(', ')}`,
`- Search pattern: "${before.slice(0, 2).join('\\n')}${before.length > 2 ? '...' : ''}"`,
`- Content size: ${currentContent.length} characters`,
`- Processing time: ${monitor.getTelemetry().processingTimeMs}ms`,
];
// SAFETY: Never use raw fallback - it's too dangerous
telemetry.errorDetails = errorDetails.join('\n');
throw new Error(errorDetails.join('\n'));
}
} catch (hunkError) {
if (hunkError instanceof Error) {
telemetry.errorDetails = `Hunk #${i + 1} processing failed: ${hunkError.message}`;
}
throw hunkError;
}
}
telemetry.success = true;
return currentContent;
} catch (error) {
telemetry.success = false;
if (error instanceof Error && !telemetry.errorDetails) {
telemetry.errorDetails = error.message;
}
throw error;
} finally {
// Collect final telemetry
Object.assign(telemetry, monitor.getTelemetry());
if (options.enableTelemetry) {
// In production, you would send this to your monitoring system
console.debug('Diff Application Telemetry:', telemetry);
}
}
}