codemap.md 8.0 KB

src/hooks/apply-patch/

Responsibility

Implements OpenCode's patch application hook system with fuzzy matching and automatic rescue strategies. This module intercepts apply_patch tool invocations, parses custom patch formats, matches patch chunks to file content using multiple comparison strategies, and rewrites patches to apply cleanly across file moves, renames, and content drift.

Design Patterns

  • Hook Pattern: Intercepts tool execution via tool.execute.before hook to preprocess patches before native application
  • Strategy Pattern: Multiple rescue strategies (prefix/suffix, LCS) for matching patch chunks to file content
  • Parser Combinator: Recursive descent parser for custom patch format with strict/permissive modes
  • Visitor Pattern: Processes patch hunks through resolution pipeline to determine application locations
  • State Machine: Manages file state (exists/missing) and dependency tracking for file moves and renames

Design

The module is organized into five cohesive files:

1. index.ts (Hook Entry Point)

  • Exports createApplyPatchHook() factory that returns the tool.execute.before hook
  • Intercepts apply_patch tool calls and delegates to rewritePatch()
  • Handles error normalization and fail-open/fail-closed behavior
  • Logs hook lifecycle events (rewrite, unchanged, skipped, blocked, validation, verification, internal)

2. codec.ts (Patch Serialization)

  • Parsing: parsePatch() and parsePatchStrict() parse custom patch format with markers *** Begin Patch/*** End Patch
  • Format: Supports add/delete/update operations with optional move semantics
  • Normalization: Unicode normalization (smart quotes, dashes, ellipsis, non-breaking spaces) and line ending normalization
  • Rendering: formatPatch() converts parsed patch objects back to canonical string format
  • Diff Algorithm: Internal diffMatrix for rendering optimized patch chunks

3. matching.ts (Fuzzy Matching Engine)

  • Comparators: Multiple line comparison strategies (exact, unicode, trim-end, unicode-trim-end, trim, unicode-trim)
  • Prefix/Suffix Rescue: rescueByPrefixSuffix() matches patches by common prefix/suffix patterns
  • LCS Rescue: rescueByLcs() uses longest common subsequence for fuzzy matching when exact lines not found
  • Anchor Resolution: resolveUniqueAnchor() finds insertion points for new content
  • Utilities: seek, list, prefix, suffix, score for pattern matching and scoring

4. resolution.ts (Patch Resolution Engine)

  • File I/O: readFileLines() and readFileLinesWithEol() handle platform-specific line endings
  • Chunk Resolution: locateChunk() finds where patch chunks should be applied in files
  • Anchor Handling: Resolves insertion points for new content using change_context markers
  • Content Application: applyHits() applies resolved patch hits to file content
  • State Management: Tracks canonical old/new lines, rewrite strategies, and match comparators

5. rewrite.ts (Patch Rewriting Pipeline)

  • Dependency Tracking: Groups patches by file path to handle file moves and multiple operations on same file
  • Chunk Merging: minimizeMergedChunk() and mergeSameFileUpdateGroupChunks() merge adjacent/overlapping patch operations
  • Move Support: Handles file moves by tracking source and destination paths
  • Add/Delete Operations: Special handling for file creation and deletion
  • Output Generation: Produces normalized patch text with minimal context preservation

Flow

Hook Execution Flow

tool.execute.before (apply_patch)
  ↓
index.ts: createApplyPatchHook()
  ↓
rewritePatch()
  ↓
codec.ts: parsePatch()
  ↓
rewrite.ts: rewritePatch() pipeline
  ↓
resolution.ts: resolveUpdateChunks()
  ↓
matching.ts: seekMatch()/rescueByPrefixSuffix()/rescueByLcs()
  ↓
resolution.ts: locateChunk() → applyHits()
  ↓
rewrite.ts: generate rewritten patch
  ↓
index.ts: return modified patchText to hook

Patch Application Flow

  1. Interception: Hook intercepts apply_patch tool call with patch text
  2. Parsing: Patch is parsed into structured format (hunks with chunks)
  3. Preparation: File states are prepared (read, normalized line endings)
  4. Resolution: Each patch chunk is resolved to a location in the target file:
    • Exact match: Direct application
    • Prefix/Suffix rescue: Match by surrounding context
    • LCS rescue: Fuzzy matching using longest common subsequence
    • Anchor insertion: Insert new content at marked locations
  5. Application: Changes are applied to file content with EOL preservation
  6. Rewriting: Rewritten patch is formatted and returned to hook
  7. Hook Return: Modified patch is passed to native apply_patch tool

Error Handling Flow

  • Blocked Errors: Outside workspace operations fail open (return unchanged)
  • Validation Errors: Malformed patches throw with detailed context
  • Verification Errors: Missing files or ambiguous matches throw with file paths
  • Internal Errors: Unexpected failures are wrapped in createApplyPatchInternalError

Integration Points

Consumed By

  • Main Plugin: src/index.ts registers the hook via createApplyPatchHook(ctx)
  • CLI: src/cli/index.ts includes the hook in plugin initialization
  • OpenCode: Hook integrates with @opencode-ai/plugin tool execution system

Dependencies

  • Utils: src/utils/logger.ts for structured logging
  • Errors: Custom error hierarchy in ./errors.ts (createApplyPatchInternalError, getApplyPatchErrorDetails, etc.)
  • Types: Shared type definitions in ./types.ts

Runtime Options

The hook accepts ApplyPatchRuntimeOptions:

{
  prefixSuffix: boolean;  // Enable prefix/suffix rescue strategy
  lcsRescue: boolean;     // Enable LCS (longest common subsequence) rescue
}

Default options in hook:

const APPLY_PATCH_RESCUE_OPTIONS: ApplyPatchRuntimeOptions = {
  prefixSuffix: true,
  lcsRescue: true,
};

Error Types

  • blocked: Operations blocked by safety checks (e.g., outside workspace)
  • validation: Malformed patch format or missing required fields
  • verification: File not found or ambiguous matches
  • internal: Unexpected errors during processing

Key Algorithms

1. Prefix/Suffix Rescue Algorithm

Input: old_lines[], new_lines[], file_lines[]
1. Compute common prefix length between old and new lines
2. Compute common suffix length after prefix
3. Collect all occurrences of prefix in file
4. For each prefix occurrence, find matching suffix
5. Return first unambiguous match or error on ambiguity

2. LCS Rescue Algorithm

Input: old_lines[], new_lines[], file_lines[]
1. Compute upper bound of shared lines using line frequency
2. Collect candidate start positions where first line matches
3. Score each window using LCS algorithm
4. Select highest scoring unambiguous match
5. Return match or error on ambiguity/low confidence

3. Patch Minimization Algorithm

Input: patch chunk with old_lines and new_lines
1. Trim common prefix from both arrays
2. Trim common suffix from both arrays
3. Preserve change_context if prefix was trimmed
4. Return minimized chunk if it still produces same result

Performance Characteristics

  • Time Complexity: O(n*m) for LCS rescue where n=old lines, m=new lines
  • Space Complexity: O(n*m) for LCS scoring matrix
  • Optimizations:
    • MAX_LCS_CHUNK_LINES (48) limits LCS to small chunks
    • MAX_LCS_CANDIDATES (64) limits candidate windows
    • Early termination on unambiguous matches
    • Prefix/suffix fast path for common cases

Testing Considerations

The module handles:

  • Unicode normalization and comparison
  • Line ending preservation (\n vs \r\n)
  • File moves and renames
  • Empty file handling
  • End-of-file markers
  • Overlapping patch chunks
  • Ambiguous matches
  • Error recovery and fail-open behavior

Configuration

No external configuration required. All behavior is controlled via:

  • Runtime options passed to rewritePatch()
  • Patch format itself (change_context markers, etc.)
  • File system state at application time