packages feed

keiro-dsl-0.12.0.0: src/Keiro/Dsl/BehaviorCoverage.hs

{-# OPTIONS_GHC -Werror=incomplete-patterns #-}

-- | Static behavioral obligations derivable from a validated aggregate graph.
--
-- This module deliberately knows nothing about consumer Haskell witnesses.  It
-- inventories the finite obligations the source graph requires; the generated
-- consumer contract reconciles and executes witness values in a later layer.
module Keiro.Dsl.BehaviorCoverage
  ( BehaviorKey (..),
    ObligationKind (..),
    EvidenceLevel (..),
    GuardCoverage (..),
    OutputEvidence (..),
    RequirementOrigin (..),
    BehaviorExactLocation (..),
    BehaviorRequirement (..),
    BehaviorRecordRow (..),
    BehaviorDerivationError (..),
    BehaviorObligationsReport (..),
    deriveAggregateBehaviorRequirements,
    deriveBehaviorRequirements,
    deriveBehaviorRequirementsForService,
    behaviorRecordRows,
    attributeBehaviorOwner,
    behaviorObligationsReport,
    renderBehaviorObligationsText,
    encodeBehaviorObligationsJson,
  )
where

import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.ByteString.Lazy qualified as BL
import Data.List (find, groupBy, sortOn)
import Data.List.NonEmpty (NonEmpty)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as Text
import Keiro.Dsl.CanonicalEncoding (canonicalTransitionOutcome)
import Keiro.Dsl.EventOutput
import Keiro.Dsl.Grammar
import Keiro.Dsl.PrettyPrint (renderExpr)
import Keiro.Dsl.ReadModelShape (fnv1a64)
import Keiro.Dsl.SemanticContract (CheckedService, checkedSpec, checkedTypeGraph)
import Keiro.Dsl.SourceIndex (TransitionOrdinal (..))
import Keiro.Dsl.TypeGraph (TypeGraph, TypeGraphError, resolveTypeGraph)

newtype BehaviorKey = BehaviorKey {unBehaviorKey :: Text}
  deriving stock (Eq, Ord, Show)

data ObligationKind
  = LiveTransition
  | RequiredRejection
  | ReplayTransition
  deriving stock (Eq, Ord, Show)

data EvidenceLevel
  = GeneratedAuthoritative
  | HoleWitnessed
  | LegacyRuntimeWitness
  deriving stock (Eq, Ord, Show)

data GuardCoverage
  = GuardTotal
  | GuardPartial
  | GuardUnknown
  | GuardNotApplicable
  deriving stock (Eq, Ord, Show)

data OutputEvidence
  = GeneratedOutput !Name
  | HandOwnedOutput !OutputObligationKey
  deriving stock (Eq, Ord, Show)

-- | The semantic source subject that owns an obligation. Unlike 'Loc', this
-- identity does not change when source text moves and can therefore be joined
-- to an independently checked exact source index.
data RequirementOrigin
  = TransitionRequirementOrigin !Name !TransitionOrdinal
  | RejectionRequirementOrigin !Name !Name
  deriving stock (Eq, Ord, Show)

-- | Current exact presentation data attached only by a source-aware reporting
-- path. It never contributes to 'requirementCanonical' or 'BehaviorKey'.
data BehaviorExactLocation = BehaviorExactLocation
  { exactSourceFile :: !FilePath,
    exactSourceLine :: !Int,
    exactSourceColumn :: !Int
  }
  deriving stock (Eq, Ord, Show)

data BehaviorRequirement = BehaviorRequirement
  { requirementKey :: !BehaviorKey,
    requirementOrigin :: !RequirementOrigin,
    requirementKind :: !ObligationKind,
    requirementEvidence :: !EvidenceLevel,
    requirementGuardCoverage :: !GuardCoverage,
    requirementContext :: !Name,
    requirementAggregate :: !Name,
    requirementSource :: !Name,
    requirementCommand :: !Name,
    requirementTarget :: !(Maybe Name),
    requirementMode :: !(Maybe TransitionMode),
    requirementEvents :: ![Name],
    requirementOutputs :: ![OutputEvidence],
    requirementDomainOutcome :: !(Maybe TransitionOutcome),
    requirementLocation :: !Loc,
    requirementExactLocation :: !(Maybe BehaviorExactLocation),
    requirementOwner :: !(Maybe FilePath),
    requirementCanonical :: !Text
  }
  deriving stock (Eq, Show)

-- | Forward-compatible record representation of one requirement.  Records
-- retain enough human-readable identity to report additions and removals
-- without re-reading or attempting to understand consumer Haskell.
data BehaviorRecordRow = BehaviorRecordRow
  { behaviorRecordKey :: !BehaviorKey,
    behaviorRecordKind :: !ObligationKind,
    behaviorRecordEvidence :: !EvidenceLevel,
    behaviorRecordAggregate :: !Name,
    behaviorRecordSource :: !Name,
    behaviorRecordCommand :: !Name,
    behaviorRecordOwner :: !(Maybe FilePath),
    behaviorRecordOutputs :: ![OutputEvidence]
  }
  deriving stock (Eq, Ord, Show)

data BehaviorDerivationError
  = InvalidEventOutput !Name !EventOutputError
  | EventlessStateChange !Name !Name !Name
  | DuplicateBehaviorIdentity !Text ![Loc]
  | BehaviorKeyCollision !BehaviorKey ![Text]
  deriving stock (Eq, Show)

data BehaviorObligationsReport = BehaviorObligationsReport
  { behaviorSubject :: !FilePath,
    behaviorWorkspaceService :: !(Maybe Text),
    behaviorRequirements :: ![BehaviorRequirement]
  }
  deriving stock (Eq, Show)

instance ToJSON BehaviorKey where
  toJSON = toJSON . unBehaviorKey

instance ToJSON ObligationKind where
  toJSON = toJSON . obligationKindText

instance ToJSON EvidenceLevel where
  toJSON = toJSON . evidenceLevelText

instance ToJSON GuardCoverage where
  toJSON = toJSON . guardCoverageText

instance ToJSON OutputEvidence where
  toJSON evidence = case evidence of
    GeneratedOutput command -> object ["ownership" .= ("generated-command-identity" :: Text), "command" .= command]
    HandOwnedOutput key -> object ["ownership" .= ("hand-owned" :: Text), "obligation" .= unOutputObligationKey key]

instance FromJSON BehaviorKey where
  parseJSON value = BehaviorKey <$> parseJSON value

instance FromJSON ObligationKind where
  parseJSON value = parseJSON value >>= parseLabel
    where
      parseLabel ("live-transition" :: Text) = pure LiveTransition
      parseLabel "required-rejection" = pure RequiredRejection
      parseLabel "replay-transition" = pure ReplayTransition
      parseLabel other = fail ("unknown behavior obligation kind: " <> T.unpack other)

instance FromJSON EvidenceLevel where
  parseJSON value = parseJSON value >>= parseLabel
    where
      parseLabel ("generated-authoritative" :: Text) = pure GeneratedAuthoritative
      parseLabel "hole-witnessed" = pure HoleWitnessed
      parseLabel "legacy-runtime-witness" = pure LegacyRuntimeWitness
      parseLabel other = fail ("unknown behavior evidence level: " <> T.unpack other)

instance FromJSON OutputEvidence where
  parseJSON = withObject "OutputEvidence" $ \fields -> do
    ownership <- fields .: "ownership"
    case (ownership :: Text) of
      "generated-command-identity" -> GeneratedOutput <$> fields .: "command"
      "hand-owned" -> HandOwnedOutput . OutputObligationKey <$> fields .: "obligation"
      other -> fail ("unknown event-output ownership: " <> T.unpack other)

instance ToJSON BehaviorRecordRow where
  toJSON row =
    object
      ( [ "key" .= behaviorRecordKey row,
          "kind" .= behaviorRecordKind row,
          "evidence" .= behaviorRecordEvidence row,
          "aggregate" .= behaviorRecordAggregate row,
          "source" .= behaviorRecordSource row,
          "command" .= behaviorRecordCommand row,
          "outputs" .= behaviorRecordOutputs row
        ]
          <> ["owner" .= owner | Just owner <- [behaviorRecordOwner row]]
      )

instance FromJSON BehaviorRecordRow where
  parseJSON = withObject "BehaviorRecordRow" $ \fields ->
    BehaviorRecordRow
      <$> fields .: "key"
      <*> fields .: "kind"
      <*> fields .: "evidence"
      <*> fields .: "aggregate"
      <*> fields .: "source"
      <*> fields .: "command"
      <*> fields .:? "owner"
      <*> fields .: "outputs"

instance ToJSON BehaviorRequirement where
  toJSON requirement =
    object
      ( [ "key" .= requirementKey requirement,
          "kind" .= requirementKind requirement,
          "evidence" .= requirementEvidence requirement,
          "guardCoverage" .= requirementGuardCoverage requirement,
          "context" .= requirementContext requirement,
          "aggregate" .= requirementAggregate requirement,
          "source" .= requirementSource requirement,
          "command" .= requirementCommand requirement,
          "target" .= requirementTarget requirement,
          "mode" .= fmap transitionModeText (requirementMode requirement),
          "events" .= requirementEvents requirement,
          "outputs" .= requirementOutputs requirement,
          "location"
            .= object
              ( ["line" .= maybe (unLoc (requirementLocation requirement)) exactSourceLine (requirementExactLocation requirement)]
                  <> ["member" .= owner | Just owner <- [requirementOwner requirement]]
                  <> ["file" .= exactSourceFile exact | Just exact <- [requirementExactLocation requirement]]
                  <> ["column" .= exactSourceColumn exact | Just exact <- [requirementExactLocation requirement]]
                  <> ["quality" .= maybe ("line-only" :: Text) (const "exact") (requirementExactLocation requirement)]
              )
        ]
          <> ["domainOutcome" .= canonicalTransitionOutcome (Just outcome) | Just outcome <- [requirementDomainOutcome requirement]]
      )

instance ToJSON BehaviorObligationsReport where
  toJSON report =
    object
      ( [ "schema" .= ("keiro-dsl/behavior-obligations/1" :: Text),
          "subject" .= behaviorSubject report,
          "requirements" .= behaviorRequirements report
        ]
          <> ["workspace" .= object ["service" .= service] | Just service <- [behaviorWorkspaceService report]]
      )

deriveBehaviorRequirements :: Spec -> Either [BehaviorDerivationError] [BehaviorRequirement]
deriveBehaviorRequirements spec = deriveBehaviorRequirementsWithGraphResult (resolveTypeGraph spec) spec

deriveBehaviorRequirementsForService :: CheckedService -> Either [BehaviorDerivationError] [BehaviorRequirement]
deriveBehaviorRequirementsForService service =
  deriveBehaviorRequirementsWithGraphResult (checkedTypeGraph service) (checkedSpec service)

deriveBehaviorRequirementsWithGraphResult :: Either (NonEmpty TypeGraphError) TypeGraph -> Spec -> Either [BehaviorDerivationError] [BehaviorRequirement]
deriveBehaviorRequirementsWithGraphResult typeGraphResult spec = case fmap concat (traverse (deriveAggregateBehaviorRequirementsWithGraphResult typeGraphResult spec) aggregates) of
  Left derivationError -> Left [derivationError]
  Right raw -> do
    rejectIdentityDefects raw
    pure (sortOn requirementKey raw)
  where
    aggregates = [aggregate | NAggregate aggregate <- specNodes spec]

deriveAggregateBehaviorRequirements :: Spec -> Aggregate -> Either BehaviorDerivationError [BehaviorRequirement]
deriveAggregateBehaviorRequirements spec = deriveAggregateBehaviorRequirementsWithGraphResult (resolveTypeGraph spec) spec

deriveAggregateBehaviorRequirementsWithGraphResult :: Either (NonEmpty TypeGraphError) TypeGraph -> Spec -> Aggregate -> Either BehaviorDerivationError [BehaviorRequirement]
deriveAggregateBehaviorRequirementsWithGraphResult typeGraphResult spec aggregate = do
  let reachable = liveReachableStates aggregate
      indexedTransitions = zip (map TransitionOrdinal [0 ..]) (aggTransitions aggregate)
      liveTransitions =
        [ (ordinal, transition)
        | (ordinal, transition) <- indexedTransitions,
          tMode transition == TmLive,
          tSource transition `Set.member` reachable
        ]
      replayTransitions = [(ordinal, transition) | (ordinal, transition) <- indexedTransitions, tMode transition == TmReplayOnly]
      commands = map cmdName (aggCommands aggregate)
      cells = [(state, command) | state <- Set.toAscList reachable, command <- commands]
      cellTransitions state command =
        [ (ordinal, transition)
        | (ordinal, transition) <- liveTransitions,
          tSource transition == state,
          tCommand transition == command
        ]
      transitionRows =
        [ transitionRequirement typeGraphResult spec aggregate (cellGuardCoverage (map snd siblings)) ordinal transition
        | (state, command) <- cells,
          let siblings = cellTransitions state command,
          (ordinal, transition) <- siblings
        ]
      rejectionRows =
        [ pure (rejectionRequirement spec aggregate state command)
        | (state, command) <- cells,
          null (cellTransitions state command)
        ]
      replayRows = [transitionRequirement typeGraphResult spec aggregate (replayGuardCoverage transition) ordinal transition | (ordinal, transition) <- replayTransitions]
  sequence (transitionRows <> rejectionRows <> replayRows)

behaviorRecordRows :: [BehaviorRequirement] -> [BehaviorRecordRow]
behaviorRecordRows = map toRow
  where
    toRow requirement =
      BehaviorRecordRow
        { behaviorRecordKey = requirementKey requirement,
          behaviorRecordKind = requirementKind requirement,
          behaviorRecordEvidence = requirementEvidence requirement,
          behaviorRecordAggregate = requirementAggregate requirement,
          behaviorRecordSource = requirementSource requirement,
          behaviorRecordCommand = requirementCommand requirement,
          behaviorRecordOwner = requirementOwner requirement,
          behaviorRecordOutputs = requirementOutputs requirement
        }

attributeBehaviorOwner :: (Name -> Maybe FilePath) -> BehaviorRequirement -> BehaviorRequirement
attributeBehaviorOwner ownerForAggregate requirement =
  requirement {requirementOwner = ownerForAggregate (requirementAggregate requirement)}

behaviorObligationsReport :: FilePath -> Spec -> Either [BehaviorDerivationError] BehaviorObligationsReport
behaviorObligationsReport subject spec =
  BehaviorObligationsReport subject Nothing <$> deriveBehaviorRequirements spec

transitionRequirement :: Either (NonEmpty TypeGraphError) TypeGraph -> Spec -> Aggregate -> GuardCoverage -> TransitionOrdinal -> Transition -> Either BehaviorDerivationError BehaviorRequirement
transitionRequirement typeGraphResult spec aggregate guardCoverage ordinal transition = do
  if null (tEmits transition) && (tSource transition /= tGoto transition || not (null (tWrites transition)))
    then Left (EventlessStateChange (aggName aggregate) (tSource transition) (tCommand transition))
    else pure ()
  mappings <-
    traverse
      (\(emitIndex, eventName) -> either (Left . InvalidEventOutput eventName) Right (eventOutputMappingFromGraphResult typeGraphResult spec aggregate transition emitIndex eventName))
      (zip [1 ..] (tEmits transition))
  let kind = if tMode transition == TmLive then LiveTransition else ReplayTransition
      outputs = map outputEvidence mappings
      canonical = transitionCanonical spec aggregate kind transition mappings
  pure
    BehaviorRequirement
      { requirementKey = canonicalKey canonical,
        requirementOrigin = TransitionRequirementOrigin (aggName aggregate) ordinal,
        requirementKind = kind,
        requirementEvidence = transitionEvidence transition,
        requirementGuardCoverage = guardCoverage,
        requirementContext = specContext spec,
        requirementAggregate = aggName aggregate,
        requirementSource = tSource transition,
        requirementCommand = tCommand transition,
        requirementTarget = Just (tGoto transition),
        requirementMode = Just (tMode transition),
        requirementEvents = tEmits transition,
        requirementOutputs = outputs,
        requirementDomainOutcome = tOutcome transition,
        requirementLocation = tLoc transition,
        requirementExactLocation = Nothing,
        requirementOwner = Nothing,
        requirementCanonical = canonical
      }

rejectionRequirement :: Spec -> Aggregate -> Name -> Name -> BehaviorRequirement
rejectionRequirement spec aggregate state command =
  BehaviorRequirement
    { requirementKey = canonicalKey canonical,
      requirementOrigin = RejectionRequirementOrigin (aggName aggregate) state,
      requirementKind = RequiredRejection,
      requirementEvidence = aggregateEvidence aggregate,
      requirementGuardCoverage = GuardNotApplicable,
      requirementContext = specContext spec,
      requirementAggregate = aggName aggregate,
      requirementSource = state,
      requirementCommand = command,
      requirementTarget = Nothing,
      requirementMode = Nothing,
      requirementEvents = [],
      requirementOutputs = [],
      requirementDomainOutcome = Nothing,
      requirementLocation = maybe (aggLoc aggregate) stLoc (find ((== state) . stName) (aggStates aggregate)),
      requirementExactLocation = Nothing,
      requirementOwner = Nothing,
      requirementCanonical = canonical
    }
  where
    canonical =
      T.intercalate
        "|"
        [ "behavior-v1",
          "kind=rejection",
          "context=" <> specContext spec,
          "aggregate=" <> aggName aggregate,
          "source=" <> state,
          "command=" <> command
        ]

transitionCanonical :: Spec -> Aggregate -> ObligationKind -> Transition -> [EventOutputMapping] -> Text
transitionCanonical spec aggregate kind transition mappings =
  T.intercalate
    "|"
    ( [ "behavior-v1",
        "kind=" <> obligationKindText kind,
        "context=" <> specContext spec,
        "aggregate=" <> aggName aggregate,
        "mode=" <> transitionModeText (tMode transition),
        "source=" <> tSource transition,
        "command=" <> tCommand transition,
        "implementation=" <> implementationText (tImplementation transition),
        "guard=" <> maybe "" renderExpr (tGuard transition),
        "writes=" <> T.intercalate ";" [name <> ":=" <> renderExpr expression | (name, expression) <- tWrites transition],
        "events=" <> T.intercalate "," (tEmits transition),
        "outputs=" <> T.intercalate "," (map eventOutputCanonical mappings),
        "target=" <> tGoto transition
      ]
        ++ outcomeSegments
    )
  where
    outcomeSegments = case aggDomainOutcomeTypes aggregate of
      Nothing -> []
      Just declaration ->
        [ "outcome-rejection-type=" <> rejectionType declaration,
          "outcome-no-op-type=" <> noOpType declaration,
          "domain-outcome=" <> canonicalTransitionOutcome (tOutcome transition)
        ]

canonicalKey :: Text -> BehaviorKey
canonicalKey canonical = BehaviorKey ("behavior-v1-" <> fnv1a64 canonical)

outputEvidence :: EventOutputMapping -> OutputEvidence
outputEvidence mapping = case mapping of
  GeneratedCommandIdentity command _ -> GeneratedOutput command
  HandOwnedEventOutput obligation -> HandOwnedOutput obligation

transitionEvidence :: Transition -> EvidenceLevel
transitionEvidence transition = case tImplementation transition of
  LegacyHoleImplementation -> LegacyRuntimeWitness
  GeneratedImplementation -> GeneratedAuthoritative
  HoleImplementation -> HoleWitnessed

aggregateEvidence :: Aggregate -> EvidenceLevel
aggregateEvidence aggregate
  | any ((/= LegacyHoleImplementation) . tImplementation) (aggTransitions aggregate) = GeneratedAuthoritative
  | otherwise = LegacyRuntimeWitness

cellGuardCoverage :: [Transition] -> GuardCoverage
cellGuardCoverage transitions
  | any ((== Nothing) . tGuard) transitions = GuardTotal
  | any crossesOneWayProjection guards = GuardUnknown
  | complementary = GuardTotal
  | all isLiteralFalse guards = GuardPartial
  | otherwise = GuardUnknown
  where
    guards = [guard | transition <- transitions, Just guard <- [tGuard transition]]
    complementary = or [left == complementExpr right | left <- guards, right <- guards, left /= right]
    isLiteralFalse (EAtom (ABool False)) = True
    isLiteralFalse _ = False

crossesOneWayProjection :: Expr -> Bool
crossesOneWayProjection expression = case expression of
  EOr left right -> crossesOneWayProjection left || crossesOneWayProjection right
  EAnd left right -> crossesOneWayProjection left || crossesOneWayProjection right
  ECmp _ left right -> crossesOneWayProjection left || crossesOneWayProjection right
  EAdd _ left right -> crossesOneWayProjection left || crossesOneWayProjection right
  ESubtract _ left right -> crossesOneWayProjection left || crossesOneWayProjection right
  EMultiply _ left right -> crossesOneWayProjection left || crossesOneWayProjection right
  EPath _ _ (_ : _ : _) -> True
  EPath {} -> False
  ELiteral {} -> False
  EAtom {} -> False

replayGuardCoverage :: Transition -> GuardCoverage
replayGuardCoverage transition
  | maybe False crossesOneWayProjection (tGuard transition) = GuardUnknown
  | otherwise = GuardNotApplicable

liveReachableStates :: Aggregate -> Set Name
liveReachableStates aggregate = case map stName (aggStates aggregate) of
  [] -> Set.empty
  initial : _ -> go (Set.singleton initial) [initial]
  where
    go seen [] = seen
    go seen (source : remaining) =
      let next =
            [ tGoto transition
            | transition <- aggTransitions aggregate,
              tMode transition == TmLive,
              tSource transition == source,
              tGoto transition `Set.notMember` seen
            ]
       in go (foldr Set.insert seen next) (remaining <> next)

rejectIdentityDefects :: [BehaviorRequirement] -> Either [BehaviorDerivationError] ()
rejectIdentityDefects requirements = case duplicateErrors <> collisionErrors of
  [] -> Right ()
  errors -> Left errors
  where
    byCanonical = groupsOn requirementCanonical requirements
    duplicateErrors =
      [ DuplicateBehaviorIdentity canonical (map requirementLocation duplicates)
      | duplicates@(first : _ : _) <- byCanonical,
        let canonical = requirementCanonical first
      ]
    byKey = groupsOn requirementKey requirements
    collisionErrors =
      [ BehaviorKeyCollision key canonicals
      | collisions@(first : _ : _) <- byKey,
        let key = requirementKey first,
        let canonicals = Set.toAscList (Set.fromList (map requirementCanonical collisions)),
        length canonicals > 1
      ]

groupsOn :: (Ord key) => (value -> key) -> [value] -> [[value]]
groupsOn key = groupBy (\left right -> key left == key right) . sortOn key

renderBehaviorObligationsText :: BehaviorObligationsReport -> Text
renderBehaviorObligationsText report =
  T.unlines
    ( [ "behavior obligations: " <> T.pack (behaviorSubject report),
        "schema: keiro-dsl/behavior-obligations/1",
        "required: " <> tshow (length (behaviorRequirements report))
      ]
        <> map renderRequirement (behaviorRequirements report)
    )
  where
    renderRequirement requirement =
      unBehaviorKey (requirementKey requirement)
        <> " "
        <> obligationKindText (requirementKind requirement)
        <> " "
        <> requirementAggregate requirement
        <> ":"
        <> requirementSource requirement
        <> " -- "
        <> requirementCommand requirement
        <> " ["
        <> evidenceLevelText (requirementEvidence requirement)
        <> ", guard="
        <> guardCoverageText (requirementGuardCoverage requirement)
        <> "]"
        <> maybe (renderLineOnly requirement) renderExact (requirementExactLocation requirement)
    renderExact exact =
      " "
        <> T.pack (exactSourceFile exact)
        <> ":"
        <> tshow (exactSourceLine exact)
        <> ":"
        <> tshow (exactSourceColumn exact)
        <> " [location-quality=exact]"
    renderLineOnly requirement =
      " line "
        <> tshow (unLoc (requirementLocation requirement))
        <> " [location-quality=line-only]"

encodeBehaviorObligationsJson :: BehaviorObligationsReport -> Text
encodeBehaviorObligationsJson = Text.decodeUtf8 . BL.toStrict . Aeson.encode

obligationKindText :: ObligationKind -> Text
obligationKindText kind = case kind of
  LiveTransition -> "live-transition"
  RequiredRejection -> "required-rejection"
  ReplayTransition -> "replay-transition"

evidenceLevelText :: EvidenceLevel -> Text
evidenceLevelText evidence = case evidence of
  GeneratedAuthoritative -> "generated-authoritative"
  HoleWitnessed -> "hole-witnessed"
  LegacyRuntimeWitness -> "legacy-runtime-witness"

guardCoverageText :: GuardCoverage -> Text
guardCoverageText coverage = case coverage of
  GuardTotal -> "proved-total"
  GuardPartial -> "provably-partial"
  GuardUnknown -> "unknown"
  GuardNotApplicable -> "not-applicable"

transitionModeText :: TransitionMode -> Text
transitionModeText mode = case mode of
  TmLive -> "live"
  TmReplayOnly -> "replay-only"

implementationText :: TransitionImplementation -> Text
implementationText implementation = case implementation of
  LegacyHoleImplementation -> "legacy-hole"
  GeneratedImplementation -> "generated"
  HoleImplementation -> "hole"

tshow :: (Show value) => value -> Text
tshow = T.pack . show