packages feed

keiro-dsl-0.12.0.0: test/conformance-domain-outcomes/Generated/DomainOutcomes/Reservation/BehaviorContract.hs

{-# LANGUAGE OverloadedLabels #-}
-- @generated by keiro-dsl 0.12.0.0 (language keiro-dsl 5) from aggregate Reservation; do not edit.
module Generated.DomainOutcomes.Reservation.BehaviorContract
  ( BehaviorKey (..)
  , ObligationKind (..)
  , EvidenceLevel (..)
  , GuardCoverage (..)
  , BehaviorRequirement (..)
  , RejectionClass (..)
  , LiveExpectation (..)
  , BehaviorWitness (..)
  , BehaviorFailure (..)
  , BehaviorConformanceReport (..)
  , behaviorRequirements
  , behaviorCoverageReport
  , behaviorConformancePassed
  , behaviorConformancePassedWith
  , renderBehaviorConformanceText
  ) where

import Generated.DomainOutcomes.Reservation.Codec (encodeReservationEvent, parseReservationEvent, reservationCodec)
import Generated.DomainOutcomes.Reservation.Domain
import Generated.DomainOutcomes.Reservation.Transducer (reservationTransducer)
import Generated.DomainOutcomes.BehaviorSourceMap qualified as BehaviorSourceMap
import Generated.DomainOutcomes.Reservation.EventStream (reservationDomainCommandHandler)
import Generated.DomainOutcomes.Nominals (ReservationNoOp, ReservationRejection)
import Keiro.Command (DomainCommandHandler (..), SilentCommandContext (..), SilentDomainDecision (..))
import Data.Aeson (ToJSON (..), object, (.=))
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither, (!))
import Keiro.Codec qualified as Codec (Codec (eventType), EventType (..))

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 BehaviorRequirement = BehaviorRequirement
  { requirementKey :: !BehaviorKey
  , requirementKind :: !ObligationKind
  , requirementEvidence :: !EvidenceLevel
  , requirementGuardCoverage :: !GuardCoverage
  , requirementSource :: !ReservationVertex
  , requirementCommandName :: !Text
  , requirementExpectedEdge :: !(Maybe (K.EdgeRef ReservationVertex))
  , requirementTarget :: !(Maybe ReservationVertex)
  , requirementEventKinds :: ![Text]
  }
  deriving stock (Eq, Show)

data RejectionClass = RejectNoOutgoingEdges | RejectNoMatchingEdge
  deriving stock (Eq, Show)

data LiveExpectation
  = Emits (NonEmpty ReservationEvent)
  | Rejects RejectionClass
  | RejectedWith ReservationRejection
  | NoOpWith ReservationNoOp
  | NoOp
  deriving stock (Eq, Show)

data BehaviorWitness
  = Pending BehaviorKey
  | LiveWitness
      { witnessKey :: BehaviorKey
      , witnessHistory :: [ReservationEvent]
      , witnessCommand :: ReservationCommand
      , witnessExpected :: LiveExpectation
      }
  | ReplayWitness
      { witnessKey :: BehaviorKey
      , witnessHistoryPrefix :: [ReservationEvent]
      , witnessObservedChunk :: [ReservationEvent]
      }
  deriving stock (Eq, Show)

data BehaviorFailure = BehaviorFailure
  { failureKey :: !BehaviorKey
  , failureSubject :: !Text
  , failureCode :: !Text
  , failureDetail :: !Text
  }
  deriving stock (Eq, Show)

instance ToJSON BehaviorFailure where
  toJSON behaviorFailure = object
    [ "key" .= unBehaviorKey (failureKey behaviorFailure)
    , "subject" .= failureSubject behaviorFailure
    , "code" .= failureCode behaviorFailure
    , "detail" .= failureDetail behaviorFailure
    ]

data BehaviorConformanceReport = BehaviorConformanceReport
  { reportRequired :: ![BehaviorKey]
  , reportFilled :: ![BehaviorKey]
  , reportPending :: ![BehaviorKey]
  , reportMissing :: ![BehaviorKey]
  , reportDuplicate :: ![BehaviorKey]
  , reportStale :: ![BehaviorKey]
  , reportFailed :: ![BehaviorFailure]
  , reportVerified :: ![BehaviorKey]
  , reportUnverified :: ![BehaviorKey]
  }
  deriving stock (Eq, Show)

instance ToJSON BehaviorConformanceReport where
  toJSON report = object
    [ "schema" .= ("keiro/behavior-conformance/1" :: Text)
    , "required" .= keyTexts (reportRequired report)
    , "filled" .= keyTexts (reportFilled report)
    , "pending" .= keyTexts (reportPending report)
    , "missing" .= keyTexts (reportMissing report)
    , "duplicate" .= keyTexts (reportDuplicate report)
    , "stale" .= keyTexts (reportStale report)
    , "failed" .= reportFailed report
    , "verified" .= keyTexts (reportVerified report)
    , "unverified" .= keyTexts (reportUnverified report)
    ]

behaviorRequirements :: [BehaviorRequirement]
behaviorRequirements =
  [ -- ReservationEligible x Cancel: live transition
    BehaviorRequirement
      { requirementKey = BehaviorKey "behavior-v1-0b1588f69a27bc77"
      , requirementKind = LiveTransition
      , requirementEvidence = GeneratedAuthoritative
      , requirementGuardCoverage = GuardTotal
      , requirementSource = ReservationEligible
      , requirementCommandName = "Cancel"
      , requirementExpectedEdge = (Just (K.EdgeRef ReservationEligible 0))
      , requirementTarget = Just ReservationCancelledState
      , requirementEventKinds = ["Cancelled"]
      }
  , -- ReservationCancelledState x Cancel: live transition
    BehaviorRequirement
      { requirementKey = BehaviorKey "behavior-v1-316c9e96fc1a94b1"
      , requirementKind = LiveTransition
      , requirementEvidence = GeneratedAuthoritative
      , requirementGuardCoverage = GuardTotal
      , requirementSource = ReservationCancelledState
      , requirementCommandName = "Cancel"
      , requirementExpectedEdge = (Just (K.EdgeRef ReservationCancelledState 0))
      , requirementTarget = Just ReservationCancelledState
      , requirementEventKinds = []
      }
  , -- ReservationCancelledState x Cancel: live transition
    BehaviorRequirement
      { requirementKey = BehaviorKey "behavior-v1-fd391b55fbbfa640"
      , requirementKind = LiveTransition
      , requirementEvidence = GeneratedAuthoritative
      , requirementGuardCoverage = GuardTotal
      , requirementSource = ReservationCancelledState
      , requirementCommandName = "Cancel"
      , requirementExpectedEdge = (Just (K.EdgeRef ReservationCancelledState 1))
      , requirementTarget = Just ReservationCancelledState
      , requirementEventKinds = []
      }
  ]

behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport
behaviorCoverageReport witnesses =
  BehaviorConformanceReport
    { reportRequired = sortedKeys (Map.keys requiredByKey)
    , reportFilled = sortedKeys [key | (key, [witness]) <- Map.toList witnessGroups, Map.member key requiredByKey, not (isPending witness)]
    , reportPending = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, Map.member key requiredByKey, any isPending rows]
    , reportMissing = sortedKeys [key | key <- Map.keys requiredByKey, Map.notMember key witnessGroups]
    , reportDuplicate = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, length rows > 1]
    , reportStale = sortedKeys [key | key <- Map.keys witnessGroups, Map.notMember key requiredByKey]
    , reportFailed = sortOn (unBehaviorKey . failureKey) failures
    , reportVerified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, proofStrength requirement]
    , reportUnverified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, not (proofStrength requirement)]
    }
 where
  requiredByKey = Map.fromList [(requirementKey requirement, requirement) | requirement <- behaviorRequirements]
  witnessGroups = Map.fromListWith (flip (<>)) [(behaviorWitnessKey witness, [witness]) | witness <- witnesses]
  executions =
    [ (requirement, runWitness requirement witness)
    | (key, [witness]) <- Map.toList witnessGroups
    , not (isPending witness)
    , Just requirement <- [Map.lookup key requiredByKey]
    ]
  failures = [behaviorFailure | (_, Left behaviorFailure) <- executions]

behaviorConformancePassed :: BehaviorConformanceReport -> Bool
behaviorConformancePassed = behaviorConformancePassedWith False

behaviorConformancePassedWith :: Bool -> BehaviorConformanceReport -> Bool
behaviorConformancePassedWith failOnUnverified report =
  null (reportPending report)
    && null (reportMissing report)
    && null (reportDuplicate report)
    && null (reportStale report)
    && null (reportFailed report)
    && (not failOnUnverified || null (reportUnverified report))

renderBehaviorConformanceText :: BehaviorConformanceReport -> Text
renderBehaviorConformanceText report = T.unlines
  [ "behavior conformance: Reservation"
  , "schema: keiro/behavior-conformance/1"
  , countLine "required" (reportRequired report)
  , countLine "filled" (reportFilled report)
  , countLine "pending" (reportPending report)
  , countLine "missing" (reportMissing report)
  , countLine "duplicate" (reportDuplicate report)
  , countLine "stale" (reportStale report)
  , "failed: " <> tshow (length (reportFailed report))
  , countLine "verified" (reportVerified report)
  , countLine "unverified" (reportUnverified report)
  ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report]

runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure ()
runWitness requirement witness = case witness of
  Pending _ -> failure requirement "pending" "witness is still Pending"
  LiveWitness _ history command expectation -> runLive requirement history command expectation
  ReplayWitness _ prefix chunk -> runReplay requirement prefix chunk

runLive :: BehaviorRequirement -> [ReservationEvent] -> ReservationCommand -> LiveExpectation -> Either BehaviorFailure ()
runLive requirement history command expectation = do
  settled <- settleHistory requirement "history" history
  ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history does not settle at the required source vertex"
  ensure requirement (commandKind command == requirementCommandName requirement) "command-mismatch" "witness command constructor does not match the required state/command cell"
  case requirementKind requirement of
    ReplayTransition -> failure requirement "witness-kind" "a replay-only requirement needs ReplayWitness"
    RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation
    LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation

runRejection :: BehaviorRequirement -> (ReservationVertex, K.RegFile ReservationRegs) -> ReservationCommand -> LiveExpectation -> Either BehaviorFailure ()
runRejection requirement seed command expectation = case expectation of
  Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events"
  RejectedWith _ -> failure requirement "expectation-kind" "an unmatched-command rejection cannot expect a selected domain rejection"
  NoOpWith _ -> failure requirement "expectation-kind" "an unmatched-command rejection cannot expect a selected domain no-op"
  NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"
  Rejects expectedClass -> case K.stepDetailedEither reservationTransducer seed command of
    Left K.NoOutgoingEdges {} -> ensure requirement (expectedClass == RejectNoOutgoingEdges) "rejection-class" "expected NoMatchingEdge but runtime returned NoOutgoingEdges"
    Left K.NoMatchingEdge {} -> ensure requirement (expectedClass == RejectNoMatchingEdge) "rejection-class" "expected NoOutgoingEdges but runtime returned NoMatchingEdge"
    Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness"
    Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject"

runAcceptance :: BehaviorRequirement -> (ReservationVertex, K.RegFile ReservationRegs) -> ReservationCommand -> LiveExpectation -> Either BehaviorFailure ()
runAcceptance requirement seed command expectation = case expectation of
  Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp"
  RejectedWith expectedReason -> do
    decision <- runSilentDecision requirement seed command
    case decision of
      SilentRejected actualReason -> ensure requirement (actualReason == expectedReason) "domain-rejection-reason" ("selected rejection reason differs; actual=" <> tshow actualReason <> " expected=" <> tshow expectedReason)
      SilentNoOp actualReason -> failure requirement "domain-outcome-kind" ("expected a selected rejection but classifier returned no-op " <> tshow actualReason)
  NoOpWith expectedReason -> do
    decision <- runSilentDecision requirement seed command
    case decision of
      SilentRejected actualReason -> failure requirement "domain-outcome-kind" ("expected a selected no-op but classifier returned rejection " <> tshow actualReason)
      SilentNoOp actualReason -> ensure requirement (actualReason == expectedReason) "domain-noop-reason" ("selected no-op reason differs; actual=" <> tshow actualReason <> " expected=" <> tshow expectedReason)
  NoOp -> failure requirement "expectation-kind" "an outcome-enabled transition requires RejectedWith or NoOpWith exact reason evidence"
  Emits expectedEvents -> case K.stepDetailedEither reservationTransducer seed command of
    Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)
    Right success -> do
      checkAcceptedEnvelope requirement success
      let expected = NonEmpty.toList expectedEvents
          actual = K.stepSuccessOutputs success
      ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)
      ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement))
      decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual)
      replayed <- case K.applyEventsDetailedEither reservationTransducer seed decoded of
        Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)
        Right replaySuccess -> Right replaySuccess
      ensure requirement (K.replaySuccessState replayed == K.stepSuccessState success) "forward-replay-vertex" "decoded emissions replay to a different vertex"
      ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers"
      checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed)

runSilentDecision
  :: BehaviorRequirement
  -> (ReservationVertex, K.RegFile ReservationRegs)
  -> ReservationCommand
  -> Either BehaviorFailure (SilentDomainDecision ReservationRejection ReservationNoOp)
runSilentDecision requirement seed command = case K.stepDetailedEither reservationTransducer seed command of
  Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)
  Right success -> do
    checkAcceptedEnvelope requirement success
    ensure requirement (null (K.stepSuccessOutputs success)) "silent-emitted" "typed silent outcome emitted one or more events"
    ensure requirement (K.stepSuccessState success == fst seed) "silent-vertex-change" "typed silent outcome changed the control vertex"
    ensure requirement (regsEqual (K.stepSuccessRegs success) (snd seed)) "silent-register-change" "typed silent outcome changed one or more registers"
    case reservationDomainCommandHandler of
      DomainCommandHandler _ classify ->
        Right (classify (SilentCommandContext (fst seed) (snd seed) command (K.stepSuccessEdge success)))

checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ReservationRegs ReservationVertex ReservationEvent -> Either BehaviorFailure ()
checkAcceptedEnvelope requirement success = do
  ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)
  ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))
  ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement))

runReplay :: BehaviorRequirement -> [ReservationEvent] -> [ReservationEvent] -> Either BehaviorFailure ()
runReplay requirement prefix chunk = case requirementKind requirement of
  ReplayTransition -> do
    settled <- settleHistory requirement "history-prefix" prefix
    ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history prefix does not settle at the replay edge source"
    ensure requirement (not (null chunk)) "empty-replay-chunk" "a replay-only edge has no observable empty chunk"
    decoded <- either (failure requirement "replay-chunk-codec-decode") Right (decodeEvents chunk)
    replayed <- case K.applyEventsDetailedEither reservationTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of
      Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure)
      Right replaySuccess -> Right replaySuccess
    ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement))
    checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed)
  _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement"

checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ReservationVertex] -> Either BehaviorFailure ()
checkSingleAttribution requirement expectedMode eventCount trace = case trace of
  [attribution] -> do
    ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))
    ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)
    ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))
    ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))
    ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount))
  _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution"

settleHistory :: BehaviorRequirement -> Text -> [ReservationEvent] -> Either BehaviorFailure (K.ReplaySuccess ReservationRegs ReservationVertex)
settleHistory requirement label history = do
  decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history)
  case K.applyEventsDetailedEither reservationTransducer (ReservationEligible, initialReservationRegs) decoded of
    Left replayFailure -> failure requirement (label <> "-replay-failed") (tshow replayFailure)
    Right replaySuccess -> Right replaySuccess

decodeEvents :: [ReservationEvent] -> Either Text [ReservationEvent]
decodeEvents = traverse (\event -> parseReservationEvent (Codec.eventType reservationCodec event) (encodeReservationEvent event))

commandKind :: ReservationCommand -> Text
commandKind command = case command of
  Cancel _ -> "Cancel"

eventKind :: ReservationEvent -> Text
eventKind event = case Codec.eventType reservationCodec event of Codec.EventType tag -> tag

regsEqual :: K.RegFile ReservationRegs -> K.RegFile ReservationRegs -> Bool
regsEqual left right = (left K.! #lastRequestId) == (right K.! #lastRequestId)

proofStrength :: BehaviorRequirement -> Bool
proofStrength requirement =
  requirementEvidence requirement == GeneratedAuthoritative
    && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable]

behaviorWitnessKey :: BehaviorWitness -> BehaviorKey
behaviorWitnessKey witness = case witness of
  Pending key -> key
  LiveWitness { witnessKey = key } -> key
  ReplayWitness { witnessKey = key } -> key

isPending :: BehaviorWitness -> Bool
isPending Pending {} = True
isPending _ = False

ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure ()
ensure requirement condition code detail = if condition then Right () else failure requirement code detail
failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed
failure requirement code detail =
  Left
    ( BehaviorFailure
        (requirementKey requirement)
        (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (" <> BehaviorSourceMap.renderBehaviorSourceLocation (unBehaviorKey (requirementKey requirement)) <> ")")
        code
        detail
    )
 where
  kindPhrase = case requirementKind requirement of
    LiveTransition -> "live transition"
    RequiredRejection -> "required rejection"
    ReplayTransition -> "replay-only transition"
sortedKeys :: [BehaviorKey] -> [BehaviorKey]
sortedKeys = sortOn unBehaviorKey
keyTexts :: [BehaviorKey] -> [Text]
keyTexts = map unBehaviorKey
countLine :: Text -> [BehaviorKey] -> Text
countLine label values = label <> ": " <> tshow (length values)
tshow :: Show value => value -> Text
tshow = T.pack . show