never executed always true always false
    1 module PureClaw.Transcript.Types
    2   ( -- * Direction
    3     Direction (..)
    4     -- * Transcript entry
    5   , TranscriptEntry (..)
    6     -- * Filter
    7   , TranscriptFilter (..)
    8   , emptyFilter
    9   , matchesFilter
   10   , applyFilter
   11     -- * Payload helpers
   12   , encodePayload
   13   , decodePayload
   14   ) where
   15 
   16 import Data.Aeson (FromJSON, ToJSON, Value)
   17 import Data.ByteString (ByteString)
   18 import Data.Map.Strict (Map)
   19 import Data.Text (Text)
   20 import Data.Text.Encoding qualified as TE
   21 import Data.Time (UTCTime)
   22 import GHC.Generics (Generic)
   23 
   24 -- | Direction of an API call.
   25 data Direction = Request | Response
   26   deriving stock (Show, Eq, Generic)
   27 
   28 instance ToJSON Direction
   29 instance FromJSON Direction
   30 
   31 -- | A single transcript entry recording one request or response.
   32 data TranscriptEntry = TranscriptEntry
   33   { _te_id            :: !Text          -- ^ UUID
   34   , _te_timestamp     :: !UTCTime
   35   , _te_harness       :: !(Maybe Text)  -- ^ e.g. Just "claude-code", Nothing if direct
   36   , _te_model         :: !(Maybe Text)  -- ^ e.g. Just "llama3", Nothing if unknown
   37   , _te_direction     :: !Direction
   38   , _te_payload       :: !Text          -- ^ Raw text payload
   39   , _te_durationMs    :: !(Maybe Int)   -- ^ present on Response entries only
   40   , _te_correlationId :: !Text          -- ^ shared UUID linking Request to its Response
   41   , _te_metadata      :: !(Map Text Value) -- ^ extensible
   42   }
   43   deriving stock (Show, Eq, Generic)
   44 
   45 instance ToJSON TranscriptEntry
   46 instance FromJSON TranscriptEntry
   47 
   48 -- | Record-of-Maybe filter; all fields AND together.
   49 data TranscriptFilter = TranscriptFilter
   50   { _tf_harness   :: !(Maybe Text)
   51   , _tf_model     :: !(Maybe Text)
   52   , _tf_direction :: !(Maybe Direction)
   53   , _tf_timeRange :: !(Maybe (UTCTime, UTCTime))
   54   , _tf_limit     :: !(Maybe Int)
   55   }
   56   deriving stock (Show, Eq)
   57 
   58 -- | A filter that matches all entries with no limit.
   59 emptyFilter :: TranscriptFilter
   60 emptyFilter = TranscriptFilter
   61   { _tf_harness   = Nothing
   62   , _tf_model     = Nothing
   63   , _tf_direction = Nothing
   64   , _tf_timeRange = Nothing
   65   , _tf_limit     = Nothing
   66   }
   67 
   68 -- | Pure predicate: does an entry match the non-limit filter criteria?
   69 -- '_tf_limit' is intentionally NOT checked here — it is applied by 'applyFilter'.
   70 matchesFilter :: TranscriptFilter -> TranscriptEntry -> Bool
   71 matchesFilter tf entry =
   72   maybe True (\h -> _te_harness entry == Just h) (_tf_harness tf)
   73     && maybe True (\m -> _te_model entry == Just m) (_tf_model tf)
   74     && maybe True (\d -> _te_direction entry == d) (_tf_direction tf)
   75     && maybe True (\(lo, hi) ->
   76          let ts = _te_timestamp entry in ts >= lo && ts <= hi) (_tf_timeRange tf)
   77 
   78 -- | Apply the filter to a list of entries.
   79 -- First filters by 'matchesFilter', then applies '_tf_limit' (take N).
   80 applyFilter :: TranscriptFilter -> [TranscriptEntry] -> [TranscriptEntry]
   81 applyFilter tf = applyLimit . filter (matchesFilter tf)
   82   where
   83     applyLimit = maybe id take (_tf_limit tf)
   84 
   85 -- | Encode raw bytes to text for storage in '_te_payload'.
   86 -- Uses UTF-8 decoding (lenient — replaces invalid bytes with U+FFFD).
   87 encodePayload :: ByteString -> Text
   88 encodePayload = TE.decodeUtf8Lenient
   89 
   90 -- | Encode text payload back to raw bytes.
   91 decodePayload :: Text -> Maybe ByteString
   92 decodePayload = Just . TE.encodeUtf8