packages feed

ai-agent-diff-patch-0.0.1.0: src/AIAgent/DiffPatch/Unified.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Boot layer: public API for the ai-agent-diff-patch library.
--
-- Provides three file-based operations built on the CPA AppContext structure:
--
-- * 'diff'      — compare two files and write unified diff to stdout
-- * 'patch'     — apply a patch to a file and write the result to stdout
-- * 'patchFile' — apply a patch to a file and write the result back in place
--
-- IOFunc values are assembled here explicitly rather than via 'Data.Default',
-- so the I\/O wiring is visible at the single point of entry.
--
-- === Example
--
-- @
-- import AIAgent.DiffPatch.Unified
--
-- main :: IO ()
-- main = do
--   -- generate a diff and print to stdout
--   diff "old.txt" "new.txt" >>= either putStrLn pure
--
--   -- apply a patch file in place
--   patchText <- readFile "changes.patch"
--   patchFile "target.txt" patchText >>= either putStrLn pure
-- @
--
-- === Limitations
--
-- * Fuzzy search window is ±5 lines; larger line-number drift will fail.
-- * Myers diff degrades toward O(N²) for files with many differences.
-- * Binary files, multi-file patches, and VCS-specific headers are not supported.
-- * The @\\ No newline at end of file@ marker is not parsed or emitted.
--
-- See the README for the full list of known limitations.
module AIAgent.DiffPatch.Unified
  ( diff
  , patch
  , patchFile
  ) where

import AIAgent.DiffPatch.Unified.CoreModel.Type    (IOFunc (..))
import qualified AIAgent.DiffPatch.Unified.Interface.FileIO  as F
import qualified AIAgent.DiffPatch.Unified.Interface.StdIO as S
import qualified AIAgent.DiffPatch.Unified.ApplicationBase.Control as Control

-- | Compute the unified diff between two files and write the result to stdout.
--
-- Uses the Myers O(ND) algorithm with 3 context lines (GNU @diff -u@ default).
-- Returns @Left errorMessage@ if either file cannot be read.
--
-- === Example
--
-- @
-- result <- diff "old.txt" "new.txt"
-- case result of
--   Left  err -> putStrLn $ "diff failed: " ++ err
--   Right ()  -> pure ()   -- unified diff has been written to stdout
-- @
diff :: FilePath -> FilePath -> IO (Either String ())
diff oldFile newFile =
  let ioFunc = IOFunc { _readIOFunc = F.read, _writeIOFunc = S.write }
  in  Control.runDiff ioFunc oldFile newFile

-- | Apply a unified diff patch to a file and write the patched result to stdout.
--
-- Fuzzy matching tolerates line-number drift of up to ±5 lines.
-- Returns @Left errorMessage@ on parse failure, hunk mismatch, or file read error.
-- No exceptions are thrown.
--
-- === Example
--
-- @
-- let p = unlines ["@@ -1,2 +1,2 @@", " ctx", "-old", "+new"]
-- result <- patch "target.txt" p
-- case result of
--   Left  err -> putStrLn $ "patch failed: " ++ err
--   Right ()  -> pure ()
-- @
patch :: FilePath -> String -> IO (Either String ())
patch origFile patchText =
  let ioFunc = IOFunc { _readIOFunc = F.read, _writeIOFunc = S.write }
  in  Control.runPatch ioFunc origFile patchText

-- | Apply a unified diff patch to a file and write the result back to the same file.
--
-- Identical to 'patch' except the patched content replaces the original file
-- rather than being written to stdout.
-- Line endings (LF or CRLF) are detected from the original file and restored
-- in the output.
-- Returns @Left errorMessage@ on any failure; the original file is not modified
-- if the patch cannot be applied.
--
-- === Example
--
-- @
-- result <- patchFile "target.txt" patchText
-- case result of
--   Left  err -> putStrLn $ "patchFile failed: " ++ err
--   Right ()  -> pure ()   -- target.txt has been updated in place
-- @
patchFile :: FilePath -> String -> IO (Either String ())
patchFile origFile patchText =
  let ioFunc = IOFunc { _readIOFunc = F.read, _writeIOFunc = F.write }
  in  Control.runPatchFile ioFunc origFile patchText