packages feed

keiro-test-support 0.17.0.0 → 0.18.0.0

raw patch · 5 files changed

+809/−5 lines, 5 filesdep +directorydep +hspecdep +keiro-test-supportdep ~ephemeral-pgdep ~keiro-migrationsPVP ok

version bump matches the API change (PVP)

Dependencies added: directory, hspec, keiro-test-support, unix

Dependency ranges changed: ephemeral-pg, keiro-migrations

API changes (from Hackage documentation)

+ Keiro.Test.ReplayCompatibility: AggregateReplayImpacts :: InventorySource
+ Keiro.Test.ReplayCompatibility: Applicable :: SourceApplicability
+ Keiro.Test.ReplayCompatibility: ApplicationOwnedObligations :: InventorySource
+ Keiro.Test.ReplayCompatibility: BaselineCapture :: CaptureRole
+ Keiro.Test.ReplayCompatibility: BaselinePersistedSurfaces :: InventorySource
+ Keiro.Test.ReplayCompatibility: BuildIdentity :: Text -> Text -> Text -> Text -> BuildIdentity
+ Keiro.Test.ReplayCompatibility: BuildPair :: BuildIdentity -> BuildIdentity -> BuildPair
+ Keiro.Test.ReplayCompatibility: BuildPairMismatch :: Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: CandidateCapture :: CaptureRole
+ Keiro.Test.ReplayCompatibility: CandidatePersistedSurfaces :: InventorySource
+ Keiro.Test.ReplayCompatibility: CanonicalDecodeFailed :: Text -> NormalizationFailure
+ Keiro.Test.ReplayCompatibility: CanonicalEncodingMismatch :: NormalizationFailure
+ Keiro.Test.ReplayCompatibility: CanonicalReplayFailed :: Text -> NormalizationFailure
+ Keiro.Test.ReplayCompatibility: CaptureReport :: Text -> CaptureRole -> BuildPair -> Text -> Text -> Text -> [HighWaterMark] -> [PersistedSurface] -> DeterminismInputs -> [CaseResult] -> CaptureReport
+ Keiro.Test.ReplayCompatibility: CaseDefinitionMismatch :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: CaseResult :: RequiredCase -> EvidenceVerdict -> Maybe Observation -> CaseResult
+ Keiro.Test.ReplayCompatibility: CheckedProcessReactions :: InventorySource
+ Keiro.Test.ReplayCompatibility: ConflictingRequiredCase :: Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: CorpusIdentityMismatch :: CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: DeterminismInputMismatch :: CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: DeterminismInputs :: Text -> Text -> Text -> Text -> DeterminismInputs
+ Keiro.Test.ReplayCompatibility: DomainRoundTripFailed :: NormalizationFailure
+ Keiro.Test.ReplayCompatibility: DuplicateCaseResult :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: DuplicateHighWaterMark :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: DuplicateInventorySource :: InventorySource -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: EmptyHighWaterMarks :: CaptureRole -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: EmptyPassedObservation :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: EvidenceInventory :: Text -> Text -> BuildPair -> [InventoryContribution] -> EvidenceInventory
+ Keiro.Test.ReplayCompatibility: Failed :: Text -> EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: HighWaterMark :: Text -> Integer -> HighWaterMark
+ Keiro.Test.ReplayCompatibility: HighWaterMarkMismatch :: CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: HistoricalRead :: CaseKind
+ Keiro.Test.ReplayCompatibility: InvalidInventoryContribution :: InventorySource -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: InventoryContribution :: InventorySource -> SourceApplicability -> [RequiredCase] -> InventoryContribution
+ Keiro.Test.ReplayCompatibility: InventoryIdentityMismatch :: CaptureRole -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: MappedConsequences :: InventorySource
+ Keiro.Test.ReplayCompatibility: MissingInventorySource :: InventorySource -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: MissingRequiredCase :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: MissingSelectedSurface :: CaptureRole -> PersistedSurface -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: NonCanonicalDecodeFailed :: Text -> NormalizationFailure
+ Keiro.Test.ReplayCompatibility: NonCanonicalReplayFailed :: Text -> NormalizationFailure
+ Keiro.Test.ReplayCompatibility: NormalizedReplayDiverged :: NormalizationFailure
+ Keiro.Test.ReplayCompatibility: NotApplicable :: Text -> SourceApplicability
+ Keiro.Test.ReplayCompatibility: Observation :: Map Text Value -> [Value] -> Map Text Text -> [Text] -> Observation
+ Keiro.Test.ReplayCompatibility: ObservationContractMismatch :: CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: ObservationMismatch :: Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: OldReaderNewWriter :: CaseKind
+ Keiro.Test.ReplayCompatibility: OrdinaryCompatibilityFindings :: InventorySource
+ Keiro.Test.ReplayCompatibility: Passed :: EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: PersistedSurface :: Text -> Text -> Text -> PersistedSurface
+ Keiro.Test.ReplayCompatibility: ProcessManagerReplay :: CaseKind
+ Keiro.Test.ReplayCompatibility: RequiredCase :: Text -> CaseKind -> PersistedSurface -> RequiredCase
+ Keiro.Test.ReplayCompatibility: RequiredCaseFailed :: CaptureRole -> Text -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: RequiredCaseUnverified :: CaptureRole -> Text -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: SemanticEquivalence :: CaseKind
+ Keiro.Test.ReplayCompatibility: SnapshotReplay :: CaseKind
+ Keiro.Test.ReplayCompatibility: SourceUnverified :: Text -> SourceApplicability
+ Keiro.Test.ReplayCompatibility: UnsupportedInventoryVersion :: Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: UnsupportedReportVersion :: CaptureRole -> Text -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: Unverified :: Text -> EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: WorkflowReplay :: CaseKind
+ Keiro.Test.ReplayCompatibility: WrongCaptureRole :: CaptureRole -> CaptureRole -> CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: [applicability] :: InventoryContribution -> SourceApplicability
+ Keiro.Test.ReplayCompatibility: [baseline] :: BuildPair -> BuildIdentity
+ Keiro.Test.ReplayCompatibility: [buildPair] :: CaptureReport -> BuildPair
+ Keiro.Test.ReplayCompatibility: [candidate] :: BuildPair -> BuildIdentity
+ Keiro.Test.ReplayCompatibility: [caseId] :: RequiredCase -> Text
+ Keiro.Test.ReplayCompatibility: [caseKind] :: RequiredCase -> CaseKind
+ Keiro.Test.ReplayCompatibility: [cases] :: InventoryContribution -> [RequiredCase]
+ Keiro.Test.ReplayCompatibility: [clock] :: DeterminismInputs -> Text
+ Keiro.Test.ReplayCompatibility: [continuations] :: Observation -> [Value]
+ Keiro.Test.ReplayCompatibility: [contributions] :: EvidenceInventory -> [InventoryContribution]
+ Keiro.Test.ReplayCompatibility: [corpusHash] :: CaptureReport -> Text
+ Keiro.Test.ReplayCompatibility: [dependencyPlanHash] :: BuildIdentity -> Text
+ Keiro.Test.ReplayCompatibility: [determinismInputs] :: CaptureReport -> DeterminismInputs
+ Keiro.Test.ReplayCompatibility: [durableIdentities] :: Observation -> Map Text Text
+ Keiro.Test.ReplayCompatibility: [durableState] :: Observation -> Map Text Value
+ Keiro.Test.ReplayCompatibility: [externalResponsesHash] :: DeterminismInputs -> Text
+ Keiro.Test.ReplayCompatibility: [failureScheduleHash] :: DeterminismInputs -> Text
+ Keiro.Test.ReplayCompatibility: [freshAllocations] :: Observation -> [Text]
+ Keiro.Test.ReplayCompatibility: [highWaterMarks] :: CaptureReport -> [HighWaterMark]
+ Keiro.Test.ReplayCompatibility: [identity] :: PersistedSurface -> Text
+ Keiro.Test.ReplayCompatibility: [inventoryId] :: CaptureReport -> Text
+ Keiro.Test.ReplayCompatibility: [inventoryVersion] :: EvidenceInventory -> Text
+ Keiro.Test.ReplayCompatibility: [kind] :: PersistedSurface -> Text
+ Keiro.Test.ReplayCompatibility: [languageProfile] :: BuildIdentity -> Text
+ Keiro.Test.ReplayCompatibility: [observationContractVersion] :: CaptureReport -> Text
+ Keiro.Test.ReplayCompatibility: [observation] :: CaseResult -> Maybe Observation
+ Keiro.Test.ReplayCompatibility: [owner] :: PersistedSurface -> Text
+ Keiro.Test.ReplayCompatibility: [randomness] :: DeterminismInputs -> Text
+ Keiro.Test.ReplayCompatibility: [reportVersion] :: CaptureReport -> Text
+ Keiro.Test.ReplayCompatibility: [requiredCase] :: CaseResult -> RequiredCase
+ Keiro.Test.ReplayCompatibility: [results] :: CaptureReport -> [CaseResult]
+ Keiro.Test.ReplayCompatibility: [revision] :: HighWaterMark -> Integer
+ Keiro.Test.ReplayCompatibility: [role] :: CaptureReport -> CaptureRole
+ Keiro.Test.ReplayCompatibility: [runtimeProfile] :: BuildIdentity -> Text
+ Keiro.Test.ReplayCompatibility: [selectedSurfaces] :: CaptureReport -> [PersistedSurface]
+ Keiro.Test.ReplayCompatibility: [sourceRevision] :: BuildIdentity -> Text
+ Keiro.Test.ReplayCompatibility: [source] :: InventoryContribution -> InventorySource
+ Keiro.Test.ReplayCompatibility: [stream] :: HighWaterMark -> Text
+ Keiro.Test.ReplayCompatibility: [surface] :: RequiredCase -> PersistedSurface
+ Keiro.Test.ReplayCompatibility: [verdict] :: CaseResult -> EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: checkNormalizationLaw :: (Eq wire, Eq domain, Eq observation) => (wire -> Either Text domain) -> (domain -> wire) -> ([wire] -> Either Text observation) -> [wire] -> wire -> wire -> [wire] -> [NormalizationFailure]
+ Keiro.Test.ReplayCompatibility: compareObservation :: Text -> Observation -> Observation -> [CompatibilityFailure]
+ Keiro.Test.ReplayCompatibility: data BuildIdentity
+ Keiro.Test.ReplayCompatibility: data BuildPair
+ Keiro.Test.ReplayCompatibility: data CaptureReport
+ Keiro.Test.ReplayCompatibility: data CaptureRole
+ Keiro.Test.ReplayCompatibility: data CaseKind
+ Keiro.Test.ReplayCompatibility: data CaseResult
+ Keiro.Test.ReplayCompatibility: data CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: data DeterminismInputs
+ Keiro.Test.ReplayCompatibility: data EvidenceInventory
+ Keiro.Test.ReplayCompatibility: data EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: data HighWaterMark
+ Keiro.Test.ReplayCompatibility: data InventoryContribution
+ Keiro.Test.ReplayCompatibility: data InventorySource
+ Keiro.Test.ReplayCompatibility: data NormalizationFailure
+ Keiro.Test.ReplayCompatibility: data Observation
+ Keiro.Test.ReplayCompatibility: data PersistedSurface
+ Keiro.Test.ReplayCompatibility: data RequiredCase
+ Keiro.Test.ReplayCompatibility: data SourceApplicability
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.BuildIdentity
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.BuildPair
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.CaptureReport
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.CaptureRole
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.CaseResult
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.DeterminismInputs
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.EvidenceInventory
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.InventoryContribution
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.Observation
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Test.ReplayCompatibility.SourceApplicability
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.BuildIdentity
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.BuildPair
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.CaptureReport
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.CaptureRole
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.CaseResult
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.DeterminismInputs
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.EvidenceInventory
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.InventoryContribution
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.Observation
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Test.ReplayCompatibility.SourceApplicability
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.BuildIdentity
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.BuildPair
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.CaptureReport
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.CaptureRole
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.CaseResult
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.DeterminismInputs
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.EvidenceInventory
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.InventoryContribution
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.NormalizationFailure
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.Observation
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Eq Keiro.Test.ReplayCompatibility.SourceApplicability
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.CaptureRole
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance GHC.Classes.Ord Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Enum.Bounded Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Enum.Bounded Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Enum.Enum Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Enum.Enum Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.BuildIdentity
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.BuildPair
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.CaptureReport
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.CaseResult
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.DeterminismInputs
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.EvidenceInventory
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.InventoryContribution
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.Observation
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Generics.Generic Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.BuildIdentity
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.BuildPair
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.CaptureReport
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.CaptureRole
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.CaseKind
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.CaseResult
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.CompatibilityFailure
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.DeterminismInputs
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.EvidenceInventory
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.EvidenceVerdict
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.HighWaterMark
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.InventoryContribution
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.InventorySource
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.NormalizationFailure
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.Observation
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.PersistedSurface
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.RequiredCase
+ Keiro.Test.ReplayCompatibility: instance GHC.Internal.Show.Show Keiro.Test.ReplayCompatibility.SourceApplicability
+ Keiro.Test.ReplayCompatibility: inventorySourcesV1 :: [InventorySource]
+ Keiro.Test.ReplayCompatibility: inventoryVersionV1 :: Text
+ Keiro.Test.ReplayCompatibility: releaseReady :: EvidenceInventory -> CaptureReport -> CaptureReport -> Bool
+ Keiro.Test.ReplayCompatibility: renderCompatibilityFailure :: CompatibilityFailure -> Text
+ Keiro.Test.ReplayCompatibility: reportVersionV1 :: Text
+ Keiro.Test.ReplayCompatibility: requiredCases :: EvidenceInventory -> Map Text RequiredCase
+ Keiro.Test.ReplayCompatibility: validateCompatibility :: EvidenceInventory -> CaptureReport -> CaptureReport -> [CompatibilityFailure]

Files

CHANGELOG.md view
@@ -6,6 +6,29 @@  ## [Unreleased] +## 0.18.0.0 — 2026-09-20+++### New Features++- New exposed module `Keiro.Test.ReplayCompatibility` defines the versioned,+  build-bound replay capture report and the independently derived inventory+  contract that gate mapping evolution. A report that is missing, unverified,+  empty, duplicated, or divergent from its required observations is rejected+  rather than silently accepted. The package now carries its own+  `keiro-test-support-test` suite covering those rejections.++### Other Changes++- Require `ephemeral-pg >=0.3.1 && <0.4`. The suite fixture now starts its+  server under a stable per-user temporary root, `/tmp/ephpg-keiro-<uid>`,+  instead of `$TMPDIR`, so ephemeral-pg's startup sweep reclaims PostgreSQL+  clusters abandoned by earlier killed runs even when `$TMPDIR` is per-session+  (`nix develop`, some CI runners). Consumers whose build plan also pulls in+  `pg-migrate-test-support 1.1.0.0` need+  `allow-newer: pg-migrate-test-support:ephemeral-pg` in their `cabal.project`+  until a Hackage revision widens that cap.+ ## 0.17.0.0 — 2026-09-17  ### New Features
keiro-test-support.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: keiro-test-support-version: 0.17.0.0+version: 0.18.0.0 synopsis: Shared PostgreSQL test fixtures for Keiro test suites description:   Suite-level ephemeral-PostgreSQL fixtures shared by the Keiro test@@ -44,19 +44,43 @@     OverloadedRecordDot     OverloadedStrings -  exposed-modules: Keiro.Test.Postgres+  exposed-modules:+    Keiro.Test.Postgres+    Keiro.Test.ReplayCompatibility+   hs-source-dirs: src   build-depends:     aeson >=2.2 && <2.3,     base >=4.21 && <5,     containers >=0.6 && <0.8,+    directory >=1.3 && <1.4,     effectful >=2.6 && <2.7,-    ephemeral-pg >=0.2 && <0.3,+    ephemeral-pg >=0.3.1 && <0.4,     hasql >=1.10 && <1.11,     hasql-pool >=1.2 && <1.5,-    keiro-migrations ^>=0.17.0.0,+    keiro-migrations ^>=0.18.0.0,     kiroku-store >=0.8 && <0.9,     kiroku-store-migrations ^>=0.4.0.0,     pg-migrate ^>=1.1.0.0,     stm >=2.5 && <2.6,+    text >=2.1 && <2.2,+    unix >=2.8 && <2.9,++test-suite keiro-test-support-test+  import: warnings+  type: exitcode-stdio-1.0+  default-language: GHC2024+  default-extensions:+    BlockArguments+    OverloadedRecordDot+    OverloadedStrings++  hs-source-dirs: test+  main-is: Main.hs+  build-depends:+    aeson >=2.2 && <2.3,+    base >=4.21 && <5,+    containers >=0.6 && <0.8,+    hspec >=2.11,+    keiro-test-support,     text >=2.1 && <2.2,
src/Keiro/Test/Postgres.hs view
@@ -39,6 +39,7 @@ import Control.Concurrent.STM (TVar, atomically, newTVarIO, stateTVar) import Control.Exception (bracket, onException) import Data.List.NonEmpty (NonEmpty (..))+import Data.Monoid (Last (..)) import Data.Text (Text) import Data.Text qualified as Text import Database.PostgreSQL.Migrate (MigrationComponent, defaultRunOptions, migrationPlan, runMigrationPlan)@@ -55,6 +56,8 @@ import Kiroku.Store.Effect.Resource (KirokuStoreResource, getKirokuStore, withKirokuStore) import Kiroku.Store.Error (StoreError) import Kiroku.Store.Migrations qualified as Kiroku+import System.Directory (createDirectoryIfMissing)+import System.Posix.User (getEffectiveUserID)  -- | A running, migrated suite fixture: one cached PostgreSQL server owning a -- single migrated template database, plus a counter for unique clone names.@@ -67,6 +70,24 @@ templateDbName :: Text templateDbName = "keiro_template" +-- | The ephemeral PostgreSQL configuration for Keiro suites: 'Pg.defaultConfig'+-- with a stable, per-user temporary root.+--+-- ephemeral-pg sweeps clusters abandoned by killed runs on the next startup, but+-- only inside the configured temporary root. Left unset, the root is @$TMPDIR@,+-- which @nix develop@ and some CI runners make per-session, so each run sweeps an+-- empty directory and orphaned postmasters accumulate. A fixed root shared by+-- every Keiro suite lets any run reclaim what an earlier run left behind. The+-- effective uid keys the root so a build sandbox running as another user does not+-- collide with a developer's @0700@ directory. The keiro-migrations test suites+-- use the same root.+ephemeralPgConfig :: IO Pg.Config+ephemeralPgConfig = do+  uid <- getEffectiveUserID+  let root = "/tmp/ephpg-keiro-" <> show uid+  createDirectoryIfMissing True root+  pure Pg.defaultConfig {Pg.temporaryRoot = Last (Just root)}+ -- | Start one cached PostgreSQL server, create a template database, apply the -- Kiroku event-store schema and Keiro framework schema to it once, then run -- @action@ with the resulting 'Fixture'. The server is stopped on exit.@@ -89,7 +110,8 @@ -- @UnknownStoredMigration@. withMigratedSuiteWith :: [MigrationComponent] -> (Fixture -> IO a) -> IO a withMigratedSuiteWith extraComponents action = do-  started <- Pg.startCached Pg.defaultConfig Pg.defaultCacheConfig+  config <- ephemeralPgConfig+  started <- Pg.startCached config Pg.defaultCacheConfig   case started of     Left err -> fail (Text.unpack (Pg.renderStartError err))     Right server ->
+ src/Keiro/Test/ReplayCompatibility.hs view
@@ -0,0 +1,556 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-}++-- | Stable, application-neutral evidence for comparing retained history across+-- two independently built versions of a service.+--+-- Capture programs should link only one application version. They emit a+-- 'CaptureReport'; a separate comparator loads the baseline report, candidate+-- report, and independently generated 'EvidenceInventory'. This avoids making+-- old and new domain types coexist in one executable.+module Keiro.Test.ReplayCompatibility+  ( BuildIdentity (..),+    BuildPair (..),+    CaptureRole (..),+    CaseKind (..),+    PersistedSurface (..),+    RequiredCase (..),+    InventorySource (..),+    SourceApplicability (..),+    InventoryContribution (..),+    EvidenceInventory (..),+    HighWaterMark (..),+    DeterminismInputs (..),+    Observation (..),+    EvidenceVerdict (..),+    CaseResult (..),+    CaptureReport (..),+    CompatibilityFailure (..),+    NormalizationFailure (..),+    inventorySourcesV1,+    inventoryVersionV1,+    reportVersionV1,+    requiredCases,+    validateCompatibility,+    compareObservation,+    releaseReady,+    renderCompatibilityFailure,+    checkNormalizationLaw,+  )+where++import Data.Aeson (FromJSON (..), ToJSON (..), Value)+import Data.Aeson qualified as Aeson+import Data.Aeson.Types (Parser)+import Data.List (group, sort)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as Text+import GHC.Generics (Generic)++reportVersionV1 :: Text+reportVersionV1 = "keiro.replay-compatibility/report/v1"++inventoryVersionV1 :: Text+inventoryVersionV1 = "keiro.replay-compatibility/inventory/v1"++data BuildIdentity = BuildIdentity+  { sourceRevision :: Text,+    languageProfile :: Text,+    runtimeProfile :: Text,+    dependencyPlanHash :: Text+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON BuildIdentity++instance FromJSON BuildIdentity++data BuildPair = BuildPair+  { baseline :: BuildIdentity,+    candidate :: BuildIdentity+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON BuildPair++instance FromJSON BuildPair++data CaptureRole = BaselineCapture | CandidateCapture+  deriving stock (Eq, Ord, Show)++instance ToJSON CaptureRole where+  toJSON = Aeson.String . captureRoleToken++instance FromJSON CaptureRole where+  parseJSON = Aeson.withText "CaptureRole" $ parseToken "capture role" captureRoles++captureRoleToken :: CaptureRole -> Text+captureRoleToken = \case+  BaselineCapture -> "baseline"+  CandidateCapture -> "candidate"++captureRoles :: [(Text, CaptureRole)]+captureRoles = [(captureRoleToken role, role) | role <- [BaselineCapture, CandidateCapture]]++data CaseKind+  = HistoricalRead+  | SemanticEquivalence+  | OldReaderNewWriter+  | SnapshotReplay+  | ProcessManagerReplay+  | WorkflowReplay+  deriving stock (Eq, Ord, Show, Enum, Bounded)++instance ToJSON CaseKind where+  toJSON = Aeson.String . caseKindToken++instance FromJSON CaseKind where+  parseJSON = Aeson.withText "CaseKind" $ parseToken "case kind" caseKinds++caseKindToken :: CaseKind -> Text+caseKindToken = \case+  HistoricalRead -> "historical-read"+  SemanticEquivalence -> "semantic-equivalence"+  OldReaderNewWriter -> "old-reader-new-writer"+  SnapshotReplay -> "snapshot"+  ProcessManagerReplay -> "process-manager"+  WorkflowReplay -> "workflow"++caseKinds :: [(Text, CaseKind)]+caseKinds = [(caseKindToken kind, kind) | kind <- [minBound .. maxBound]]++data PersistedSurface = PersistedSurface+  { kind :: Text,+    owner :: Text,+    identity :: Text+  }+  deriving stock (Eq, Ord, Show, Generic)++instance ToJSON PersistedSurface++instance FromJSON PersistedSurface++data RequiredCase = RequiredCase+  { caseId :: Text,+    caseKind :: CaseKind,+    surface :: PersistedSurface+  }+  deriving stock (Eq, Ord, Show, Generic)++instance ToJSON RequiredCase++instance FromJSON RequiredCase++-- | Every v1 inventory names all of these inputs. An input may explicitly be+-- not applicable, but it may not disappear merely because a candidate report+-- omitted its cases.+data InventorySource+  = BaselinePersistedSurfaces+  | CandidatePersistedSurfaces+  | OrdinaryCompatibilityFindings+  | AggregateReplayImpacts+  | MappedConsequences+  | CheckedProcessReactions+  | ApplicationOwnedObligations+  deriving stock (Eq, Ord, Show, Enum, Bounded)++inventorySourcesV1 :: [InventorySource]+inventorySourcesV1 = [minBound .. maxBound]++instance ToJSON InventorySource where+  toJSON = Aeson.String . inventorySourceToken++instance FromJSON InventorySource where+  parseJSON = Aeson.withText "InventorySource" $ parseToken "inventory source" inventorySources++inventorySourceToken :: InventorySource -> Text+inventorySourceToken = \case+  BaselinePersistedSurfaces -> "baseline-persisted-surfaces"+  CandidatePersistedSurfaces -> "candidate-persisted-surfaces"+  OrdinaryCompatibilityFindings -> "ordinary-compatibility-findings"+  AggregateReplayImpacts -> "aggregate-replay-impacts"+  MappedConsequences -> "mapped-consequences"+  CheckedProcessReactions -> "checked-process-reactions"+  ApplicationOwnedObligations -> "application-owned-obligations"++inventorySources :: [(Text, InventorySource)]+inventorySources = [(inventorySourceToken source, source) | source <- inventorySourcesV1]++data SourceApplicability+  = Applicable+  | NotApplicable Text+  | SourceUnverified Text+  deriving stock (Eq, Show)++instance ToJSON SourceApplicability where+  toJSON = \case+    Applicable -> Aeson.object ["status" Aeson..= ("applicable" :: Text)]+    NotApplicable reason -> Aeson.object ["status" Aeson..= ("not-applicable" :: Text), "reason" Aeson..= reason]+    SourceUnverified reason -> Aeson.object ["status" Aeson..= ("unverified" :: Text), "reason" Aeson..= reason]++instance FromJSON SourceApplicability where+  parseJSON = Aeson.withObject "SourceApplicability" $ \object -> do+    status <- object Aeson..: "status"+    case status :: Text of+      "applicable" -> pure Applicable+      "not-applicable" -> NotApplicable <$> object Aeson..: "reason"+      "unverified" -> SourceUnverified <$> object Aeson..: "reason"+      other -> fail ("unsupported inventory applicability: " <> Text.unpack other)++data InventoryContribution = InventoryContribution+  { source :: InventorySource,+    applicability :: SourceApplicability,+    cases :: [RequiredCase]+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON InventoryContribution++instance FromJSON InventoryContribution++data EvidenceInventory = EvidenceInventory+  { inventoryVersion :: Text,+    inventoryId :: Text,+    buildPair :: BuildPair,+    contributions :: [InventoryContribution]+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON EvidenceInventory++instance FromJSON EvidenceInventory++data HighWaterMark = HighWaterMark+  { stream :: Text,+    revision :: Integer+  }+  deriving stock (Eq, Ord, Show, Generic)++instance ToJSON HighWaterMark++instance FromJSON HighWaterMark++data DeterminismInputs = DeterminismInputs+  { clock :: Text,+    randomness :: Text,+    externalResponsesHash :: Text,+    failureScheduleHash :: Text+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON DeterminismInputs++instance FromJSON DeterminismInputs++-- | An observation is deliberately semantic. Artifact hashes may be included+-- as values, but they do not replace the durable state, continuation, or+-- identity coordinates which establish equivalence.+data Observation = Observation+  { durableState :: Map Text Value,+    continuations :: [Value],+    durableIdentities :: Map Text Text,+    freshAllocations :: [Text]+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON Observation++instance FromJSON Observation++data EvidenceVerdict+  = Passed+  | Failed Text+  | Unverified Text+  deriving stock (Eq, Show)++instance ToJSON EvidenceVerdict where+  toJSON = \case+    Passed -> Aeson.object ["status" Aeson..= ("passed" :: Text)]+    Failed reason -> Aeson.object ["status" Aeson..= ("failed" :: Text), "reason" Aeson..= reason]+    Unverified reason -> Aeson.object ["status" Aeson..= ("unverified" :: Text), "reason" Aeson..= reason]++instance FromJSON EvidenceVerdict where+  parseJSON = Aeson.withObject "EvidenceVerdict" $ \object -> do+    status <- object Aeson..: "status"+    case status :: Text of+      "passed" -> pure Passed+      "failed" -> Failed <$> object Aeson..: "reason"+      "unverified" -> Unverified <$> object Aeson..: "reason"+      other -> fail ("unsupported evidence verdict: " <> Text.unpack other)++data CaseResult = CaseResult+  { requiredCase :: RequiredCase,+    verdict :: EvidenceVerdict,+    observation :: Maybe Observation+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON CaseResult++instance FromJSON CaseResult++data CaptureReport = CaptureReport+  { reportVersion :: Text,+    role :: CaptureRole,+    buildPair :: BuildPair,+    inventoryId :: Text,+    corpusHash :: Text,+    observationContractVersion :: Text,+    highWaterMarks :: [HighWaterMark],+    selectedSurfaces :: [PersistedSurface],+    determinismInputs :: DeterminismInputs,+    results :: [CaseResult]+  }+  deriving stock (Eq, Show, Generic)++instance ToJSON CaptureReport++instance FromJSON CaptureReport++data CompatibilityFailure+  = UnsupportedInventoryVersion Text+  | UnsupportedReportVersion CaptureRole Text+  | WrongCaptureRole CaptureRole CaptureRole+  | BuildPairMismatch Text+  | InventoryIdentityMismatch CaptureRole+  | CorpusIdentityMismatch+  | ObservationContractMismatch+  | DeterminismInputMismatch+  | MissingInventorySource InventorySource+  | DuplicateInventorySource InventorySource+  | InvalidInventoryContribution InventorySource Text+  | ConflictingRequiredCase Text+  | MissingRequiredCase CaptureRole Text+  | DuplicateCaseResult CaptureRole Text+  | CaseDefinitionMismatch CaptureRole Text+  | RequiredCaseFailed CaptureRole Text Text+  | RequiredCaseUnverified CaptureRole Text Text+  | EmptyPassedObservation CaptureRole Text+  | ObservationMismatch Text+  | MissingSelectedSurface CaptureRole PersistedSurface+  | EmptyHighWaterMarks CaptureRole+  | DuplicateHighWaterMark CaptureRole Text+  | HighWaterMarkMismatch+  deriving stock (Eq, Show)++-- | Failures for a codec whose decoder intentionally admits non-canonical wire+-- forms. The law covers normalization, the whole domain round trip, and replay+-- of the raw historical chain versus its canonical replacement.+data NormalizationFailure+  = NonCanonicalDecodeFailed Text+  | CanonicalEncodingMismatch+  | CanonicalDecodeFailed Text+  | DomainRoundTripFailed+  | NonCanonicalReplayFailed Text+  | CanonicalReplayFailed Text+  | NormalizedReplayDiverged+  deriving stock (Eq, Show)++-- | Check the shared normalization law for one non-canonical wire value.+--+-- The supplied replay function receives raw wire values, so a generated harness+-- can exercise its real parser and transducer rather than proving equality only+-- after both inputs have already been normalized.+checkNormalizationLaw ::+  (Eq wire, Eq domain, Eq observation) =>+  (wire -> Either Text domain) ->+  (domain -> wire) ->+  ([wire] -> Either Text observation) ->+  [wire] ->+  wire ->+  wire ->+  [wire] ->+  [NormalizationFailure]+checkNormalizationLaw decode encode replay prefix nonCanonical canonical suffix =+  case decode nonCanonical of+    Left problem -> [NonCanonicalDecodeFailed problem]+    Right decoded ->+      [CanonicalEncodingMismatch | encode decoded /= canonical]+        <> case decode canonical of+          Left problem -> [CanonicalDecodeFailed problem]+          Right canonicalDomain ->+            [DomainRoundTripFailed | canonicalDomain /= decoded || decode (encode decoded) /= Right decoded]+              <> replayFailures+  where+    replayFailures = case (replay (prefix <> [nonCanonical] <> suffix), replay (prefix <> [canonical] <> suffix)) of+      (Left problem, _) -> [NonCanonicalReplayFailed problem]+      (_, Left problem) -> [CanonicalReplayFailed problem]+      (Right historical, Right normalized) -> [NormalizedReplayDiverged | historical /= normalized]++-- | Union the independently supplied inventory contributions. Identical cases+-- may be required by more than one source. Conflicting definitions are reported+-- by 'validateCompatibility'.+requiredCases :: EvidenceInventory -> Map Text RequiredCase+requiredCases inventory =+  Map.fromList+    [ (required.caseId, required)+    | contribution <- inventory.contributions,+      required <- contribution.cases+    ]++validateCompatibility :: EvidenceInventory -> CaptureReport -> CaptureReport -> [CompatibilityFailure]+validateCompatibility inventory baselineReport candidateReport =+  concat+    [ inventoryFailures inventory,+      reportMetadataFailures inventory BaselineCapture baselineReport,+      reportMetadataFailures inventory CandidateCapture candidateReport,+      pairedMetadataFailures baselineReport candidateReport,+      reportCaseFailures inventory baselineReport,+      reportCaseFailures inventory candidateReport,+      observationFailures inventory baselineReport candidateReport+    ]++-- | Compare one baseline/candidate semantic observation outside the report+-- envelope. Runtime regression suites use this while constructing reports so+-- the same mismatch vocabulary covers aggregate, workflow, and process traces.+compareObservation :: Text -> Observation -> Observation -> [CompatibilityFailure]+compareObservation caseId baselineObservation candidateObservation =+  [ObservationMismatch caseId | baselineObservation /= candidateObservation]++releaseReady :: EvidenceInventory -> CaptureReport -> CaptureReport -> Bool+releaseReady inventory baselineReport candidateReport =+  null (validateCompatibility inventory baselineReport candidateReport)++inventoryFailures :: EvidenceInventory -> [CompatibilityFailure]+inventoryFailures inventory =+  versionFailure <> sourceFailures <> contributionFailures <> conflictFailures+  where+    versionFailure =+      [UnsupportedInventoryVersion inventory.inventoryVersion | inventory.inventoryVersion /= inventoryVersionV1]+    groupedSources = group (sort (map (.source) inventory.contributions))+    presentSources = Set.fromList (map (.source) inventory.contributions)+    sourceFailures =+      [MissingInventorySource source | source <- inventorySourcesV1, source `Set.notMember` presentSources]+        <> [DuplicateInventorySource source | source : remaining <- groupedSources, not (null remaining)]+    contributionFailures = concatMap validateContribution inventory.contributions+    byId =+      Map.fromListWith+        (++)+        [ (required.caseId, [required])+        | contribution <- inventory.contributions,+          required <- contribution.cases+        ]+    conflictFailures =+      [ ConflictingRequiredCase caseId+      | (caseId, definitions) <- Map.toList byId,+        Set.size (Set.fromList definitions) > 1+      ]++validateContribution :: InventoryContribution -> [CompatibilityFailure]+validateContribution contribution = case contribution.applicability of+  Applicable+    | null contribution.cases -> [InvalidInventoryContribution contribution.source "applicable source has no required cases"]+    | otherwise -> []+  NotApplicable reason+    | Text.null reason -> [InvalidInventoryContribution contribution.source "not-applicable source has no reason"]+    | not (null contribution.cases) -> [InvalidInventoryContribution contribution.source "not-applicable source supplies cases"]+    | otherwise -> []+  SourceUnverified reason ->+    [InvalidInventoryContribution contribution.source ("source is unverified: " <> reason)]++reportMetadataFailures :: EvidenceInventory -> CaptureRole -> CaptureReport -> [CompatibilityFailure]+reportMetadataFailures inventory expectedRole report =+  concat+    [ [UnsupportedReportVersion expectedRole report.reportVersion | report.reportVersion /= reportVersionV1],+      [WrongCaptureRole expectedRole report.role | report.role /= expectedRole],+      [BuildPairMismatch (captureRoleToken expectedRole) | report.buildPair /= inventory.buildPair],+      [InventoryIdentityMismatch expectedRole | report.inventoryId /= inventory.inventoryId],+      [EmptyHighWaterMarks expectedRole | null report.highWaterMarks],+      [DuplicateHighWaterMark expectedRole stream | stream <- duplicates (map (.stream) report.highWaterMarks)]+    ]++pairedMetadataFailures :: CaptureReport -> CaptureReport -> [CompatibilityFailure]+pairedMetadataFailures baselineReport candidateReport =+  concat+    [ [CorpusIdentityMismatch | baselineReport.corpusHash /= candidateReport.corpusHash],+      [ObservationContractMismatch | baselineReport.observationContractVersion /= candidateReport.observationContractVersion],+      [DeterminismInputMismatch | baselineReport.determinismInputs /= candidateReport.determinismInputs],+      [HighWaterMarkMismatch | Set.fromList baselineReport.highWaterMarks /= Set.fromList candidateReport.highWaterMarks]+    ]++reportCaseFailures :: EvidenceInventory -> CaptureReport -> [CompatibilityFailure]+reportCaseFailures inventory report =+  duplicateFailures <> concatMap checkRequired (Map.elems expected)+  where+    expected = requiredCases inventory+    role = report.role+    rowsById = Map.fromListWith (++) [(row.requiredCase.caseId, [row]) | row <- report.results]+    duplicateFailures = [DuplicateCaseResult role caseId | caseId <- duplicates (map (.requiredCase.caseId) report.results)]+    selected = Set.fromList report.selectedSurfaces+    checkRequired required = case Map.lookup required.caseId rowsById of+      Nothing -> [MissingRequiredCase role required.caseId]+      Just [] -> [MissingRequiredCase role required.caseId]+      Just (row : _) ->+        [CaseDefinitionMismatch role required.caseId | row.requiredCase /= required]+          <> [MissingSelectedSurface role required.surface | required.surface `Set.notMember` selected]+          <> verdictFailures role row++verdictFailures :: CaptureRole -> CaseResult -> [CompatibilityFailure]+verdictFailures role row = case row.verdict of+  Failed reason -> [RequiredCaseFailed role row.requiredCase.caseId reason]+  Unverified reason -> [RequiredCaseUnverified role row.requiredCase.caseId reason]+  Passed ->+    [ EmptyPassedObservation role row.requiredCase.caseId+    | maybe True observationIsEmpty row.observation+    ]++observationFailures :: EvidenceInventory -> CaptureReport -> CaptureReport -> [CompatibilityFailure]+observationFailures inventory baselineReport candidateReport =+  [ ObservationMismatch caseId+  | caseId <- Map.keys (requiredCases inventory),+    passedObservation caseId baselineReport /= passedObservation caseId candidateReport+  ]++passedObservation :: Text -> CaptureReport -> Maybe Observation+passedObservation caseId report = do+  row <- Map.lookup caseId (Map.fromList [(result.requiredCase.caseId, result) | result <- report.results])+  case row.verdict of+    Passed -> row.observation+    Failed _ -> Nothing+    Unverified _ -> Nothing++observationIsEmpty :: Observation -> Bool+observationIsEmpty observation =+  Map.null observation.durableState+    && null observation.continuations+    && Map.null observation.durableIdentities++duplicates :: (Ord a) => [a] -> [a]+duplicates values = [value | value : remaining <- group (sort values), not (null remaining)]++parseToken :: String -> [(Text, a)] -> Text -> Parser a+parseToken label tokens token =+  case lookup token tokens of+    Just value -> pure value+    Nothing -> fail ("unsupported " <> label <> ": " <> Text.unpack token)++renderCompatibilityFailure :: CompatibilityFailure -> Text+renderCompatibilityFailure = \case+  UnsupportedInventoryVersion version -> "unsupported inventory version: " <> version+  UnsupportedReportVersion role version -> captureRoleToken role <> " report has unsupported version: " <> version+  WrongCaptureRole expected actual -> "expected " <> captureRoleToken expected <> " capture, got " <> captureRoleToken actual+  BuildPairMismatch role -> role <> " report is not bound to the inventory build pair"+  InventoryIdentityMismatch role -> captureRoleToken role <> " report names a different inventory"+  CorpusIdentityMismatch -> "baseline and candidate corpus hashes differ"+  ObservationContractMismatch -> "baseline and candidate observation contracts differ"+  DeterminismInputMismatch -> "baseline and candidate determinism inputs differ"+  MissingInventorySource source -> "inventory omits source: " <> inventorySourceToken source+  DuplicateInventorySource source -> "inventory repeats source: " <> inventorySourceToken source+  InvalidInventoryContribution source reason -> inventorySourceToken source <> ": " <> reason+  ConflictingRequiredCase caseId -> "inventory gives conflicting definitions for case: " <> caseId+  MissingRequiredCase role caseId -> captureRoleToken role <> " report omits required case: " <> caseId+  DuplicateCaseResult role caseId -> captureRoleToken role <> " report repeats case: " <> caseId+  CaseDefinitionMismatch role caseId -> captureRoleToken role <> " report changes required case definition: " <> caseId+  RequiredCaseFailed role caseId reason -> captureRoleToken role <> " case failed (" <> caseId <> "): " <> reason+  RequiredCaseUnverified role caseId reason -> captureRoleToken role <> " case is unverified (" <> caseId <> "): " <> reason+  EmptyPassedObservation role caseId -> captureRoleToken role <> " case passed with an empty observation: " <> caseId+  ObservationMismatch caseId -> "baseline and candidate observations differ for case: " <> caseId+  MissingSelectedSurface role surface -> captureRoleToken role <> " report omits selected surface: " <> surface.owner <> "/" <> surface.identity+  EmptyHighWaterMarks role -> captureRoleToken role <> " report has no stream high-water marks"+  DuplicateHighWaterMark role stream -> captureRoleToken role <> " report repeats stream high-water mark: " <> stream+  HighWaterMarkMismatch -> "baseline and candidate high-water marks differ"
+ test/Main.hs view
@@ -0,0 +1,179 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Data.Aeson (Value (String))+import Data.Aeson qualified as Aeson+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Keiro.Test.ReplayCompatibility+import Test.Hspec++main :: IO ()+main = hspec do+  describe "Replay compatibility evidence" do+    it "accepts complete, build-bound, equal semantic observations" do+      releaseReady inventory baselineReport candidateReport `shouldBe` True++    it "round-trips the versioned JSON contracts" do+      Aeson.eitherDecode (Aeson.encode inventory) `shouldBe` Right inventory+      Aeson.eitherDecode (Aeson.encode baselineReport) `shouldBe` Right baselineReport++    it "rejects an empty report" do+      let empty = baselineReport {results = [], selectedSurfaces = []}+      validateCompatibility inventory empty candidateReport+        `shouldContain` [MissingRequiredCase BaselineCapture "orders/placed"]++    it "rejects unequal corpus identities" do+      let changed = candidateReport {corpusHash = "sha256:candidate-corpus"}+      validateCompatibility inventory baselineReport changed `shouldContain` [CorpusIdentityMismatch]++    it "rejects unverified required evidence" do+      let changed = candidateReport {results = [result {verdict = Unverified "random source cannot be pinned", observation = Nothing}]}+      validateCompatibility inventory baselineReport changed+        `shouldContain` [RequiredCaseUnverified CandidateCapture "orders/placed" "random source cannot be pinned"]++    it "rejects two empty observations" do+      let emptyObservation = Observation Map.empty [] Map.empty []+          old = baselineReport {results = [result {observation = Just emptyObservation}]}+          new = candidateReport {results = [result {observation = Just emptyObservation}]}+      validateCompatibility inventory old new+        `shouldContain` [EmptyPassedObservation BaselineCapture "orders/placed"]++    it "rejects an omitted old-only surface even when remaining rows pass" do+      let oldOnly = RequiredCase "orders/retired" HistoricalRead (PersistedSurface "aggregate-stream" "orders" "OrderRetired:v1")+          changedInventory =+            inventory+              { contributions =+                  map+                    ( \contribution ->+                        if contribution.source == BaselinePersistedSurfaces+                          then contribution {cases = contribution.cases <> [oldOnly]}+                          else contribution+                    )+                    inventory.contributions+              }+      validateCompatibility changedInventory baselineReport candidateReport+        `shouldContain` [MissingRequiredCase BaselineCapture "orders/retired"]++    it "rejects direct-ID, transition, process, and Hole omissions independently of mapped consequences" do+      let omittedCases =+            [ (AggregateReplayImpacts, RequiredCase "orders/direct-id" SemanticEquivalence (PersistedSurface "direct-nominal" "orders" "OrderId")),+              (AggregateReplayImpacts, RequiredCase "orders/transition" SemanticEquivalence (PersistedSurface "transition" "orders" "PlaceOrder")),+              (CheckedProcessReactions, RequiredCase "billing/process" ProcessManagerReplay (PersistedSurface "process-reaction" "billing" "InvoiceAccepted")),+              (ApplicationOwnedObligations, RequiredCase "orders/hole" SemanticEquivalence (PersistedSurface "application-hole" "orders" "PlaceOrder/Hole"))+            ]+          changedInventory = inventory {contributions = map (addCases omittedCases) inventory.contributions}+          failures = validateCompatibility changedInventory baselineReport candidateReport+      failures `shouldContain` [MissingRequiredCase BaselineCapture "orders/direct-id"]+      failures `shouldContain` [MissingRequiredCase CandidateCapture "orders/transition"]+      failures `shouldContain` [MissingRequiredCase CandidateCapture "billing/process"]+      failures `shouldContain` [MissingRequiredCase CandidateCapture "orders/hole"]++    it "rejects an unchanged-name application source as unverified" do+      let changedInventory =+            inventory+              { contributions =+                  map+                    ( \contribution ->+                        if contribution.source == ApplicationOwnedObligations+                          then contribution {applicability = SourceUnverified "Hole source hash changed without a capture"}+                          else contribution+                    )+                    inventory.contributions+              }+      validateCompatibility changedInventory baselineReport candidateReport+        `shouldContain` [ InvalidInventoryContribution+                            ApplicationOwnedObligations+                            "source is unverified: Hole source hash changed without a capture"+                        ]++  describe "Normalization law" do+    it "accepts a non-canonical spelling only when it normalizes before replay" do+      checkNormalizationLaw decodeDecimal show replaySum ["2"] "01" "1" ["3"] `shouldBe` []++    it "rejects a decoder that preserves non-canonical information" do+      let lossyDecode "01" = Right (10 :: Int)+          lossyDecode value = decodeDecimal value+      checkNormalizationLaw lossyDecode show replaySum [] "01" "1" []+        `shouldContain` [CanonicalEncodingMismatch]++addCases :: [(InventorySource, RequiredCase)] -> InventoryContribution -> InventoryContribution+addCases additions contribution =+  contribution+    { applicability = if null selected then contribution.applicability else Applicable,+      cases = contribution.cases <> selected+    }+  where+    selected = [addedCase | (source, addedCase) <- additions, source == contribution.source]++buildPair :: BuildPair+buildPair =+  BuildPair+    { baseline = BuildIdentity "baseline-revision" "language-v5" "runtime-v1" "sha256:baseline-plan",+      candidate = BuildIdentity "candidate-revision" "language-v6" "runtime-v1" "sha256:candidate-plan"+    }++fixtureSurface :: PersistedSurface+fixtureSurface = PersistedSurface "aggregate-stream" "orders" "OrderPlaced:v1"++required :: RequiredCase+required = RequiredCase "orders/placed" SemanticEquivalence fixtureSurface++inventory :: EvidenceInventory+inventory =+  EvidenceInventory+    { inventoryVersion = inventoryVersionV1,+      inventoryId = "sha256:inventory",+      buildPair = Main.buildPair,+      contributions =+        [ InventoryContribution+            { source,+              applicability = if source == BaselinePersistedSurfaces then Applicable else NotApplicable "fixture has no obligations from this source",+              cases = if source == BaselinePersistedSurfaces then [required] else []+            }+        | source <- inventorySourcesV1+        ]+    }++fixtureObservation :: Observation+fixtureObservation =+  Observation+    { durableState = Map.fromList [("status", String "placed")],+      continuations = [String "ReserveInventory"],+      durableIdentities = Map.fromList [("event", "event_01")],+      freshAllocations = ["trace-id"]+    }++result :: CaseResult+result = CaseResult required Passed (Just fixtureObservation)++report :: CaptureRole -> CaptureReport+report role =+  CaptureReport+    { reportVersion = reportVersionV1,+      role,+      buildPair = Main.buildPair,+      inventoryId = "sha256:inventory",+      corpusHash = "sha256:corpus",+      observationContractVersion = "orders-observation/v1",+      highWaterMarks = [HighWaterMark "orders-1" 42],+      selectedSurfaces = [fixtureSurface],+      determinismInputs = DeterminismInputs "2026-09-19T00:00:00Z" "seed:v1" "sha256:responses" "sha256:schedule",+      results = [result]+    }++baselineReport :: CaptureReport+baselineReport = report BaselineCapture++candidateReport :: CaptureReport+candidateReport = report CandidateCapture++decodeDecimal :: String -> Either Text Int+decodeDecimal value = case reads value of+  [(number, "")] -> Right number+  _ -> Left "invalid decimal"++replaySum :: [String] -> Either Text Int+replaySum = fmap sum . traverse decodeDecimal