claude-gate-1.0.0: src/Claude/Gate/EditStack.hs
-- | Reading the per-turn edit stack the phases review.
--
-- record-edit appends one JSON-encoded edit per line. The phases read it back to
-- render diffs and to list the touched files; Phase A also returns a claimed
-- stack to the live stack when its reviewer fails, so the next Stop retries.
module Claude.Gate.EditStack
( readEdits
, stackFilePaths
, stackHasCode
, returnClaimedToStack
) where
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as StrictByteString
import Data.ByteString.Char8 qualified as ByteString
import Data.List (isSuffixOf, nub, sort)
import Claude.Gate.Edit (Edit, editFilePath)
import Claude.Gate.TurnState (TurnPaths (claimedStack, reviewStack), removeIfExists)
import System.Directory (doesFileExist)
-- | Read a stack file back into edits. We wrote these lines ourselves, so a line
-- that fails to decode is a bug in this program, surfaced loudly.
readEdits :: FilePath -> IO [Edit]
readEdits path = do
contents <- ByteString.readFile path
pure (map decodeEdit (filter (not . ByteString.null) (ByteString.lines contents)))
decodeEdit :: ByteString.ByteString -> Edit
decodeEdit raw = case Aeson.eitherDecodeStrict raw of
Right edit -> edit
Left err -> error ("claude-gate: corrupt edit on the review stack: " <> err)
-- | The sorted, deduplicated file paths recorded on a stack file.
stackFilePaths :: FilePath -> IO [FilePath]
stackFilePaths path = do
present <- doesFileExist path
if not present
then pure []
else do
edits <- readEdits path
pure (sort (nub (map editFilePath edits)))
-- | Whether any file on the stack is source code. Dumbify is about code
-- comprehension, so prose and config edits do not trigger it.
stackHasCode :: FilePath -> IO Bool
stackHasCode path = do
paths <- stackFilePaths path
pure (any isCode paths)
isCode :: FilePath -> Bool
isCode path = any (`isSuffixOf` path) codeExtensions
codeExtensions :: [FilePath]
codeExtensions =
[ ".hs", ".lhs", ".hsig", ".cabal", ".nix", ".sh", ".bash", ".py", ".rs"
, ".js", ".ts", ".tsx", ".go", ".c", ".h", ".cpp", ".hpp", ".java"
]
-- | Return the claimed diffs to the live stack so the next Stop re-reviews them,
-- then drop the claimed copy. Used when the rule reviewer fails: the diffs must
-- not be lost to an infrastructure hiccup.
returnClaimedToStack :: TurnPaths -> IO ()
returnClaimedToStack paths = do
present <- doesFileExist (claimedStack paths)
if not present
then pure ()
else do
claimed <- StrictByteString.readFile (claimedStack paths)
StrictByteString.appendFile (reviewStack paths) claimed
removeIfExists (claimedStack paths)