packages feed

keiro-core 0.3.0.0 → 0.4.0.1

raw patch · 6 files changed

+365/−27 lines, 6 filesdep ~aesondep ~aeson-casingdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, aeson-casing, bytestring, deepseq, generic-lens, keiki, lens, scientific, text, time, uuid

API changes (from Hackage documentation)

+ Keiro.Codec.Structural: FixtureCases :: NonEmpty (Text, a) -> FixtureCases a
+ Keiro.Codec.Structural: StructuralBinding :: !a -> shape -> !shape -> a -> StructuralBinding a shape
+ Keiro.Codec.Structural: [bindingFromShape] :: StructuralBinding a shape -> !shape -> a
+ Keiro.Codec.Structural: [bindingToShape] :: StructuralBinding a shape -> !a -> shape
+ Keiro.Codec.Structural: [fixtureCases] :: FixtureCases a -> NonEmpty (Text, a)
+ Keiro.Codec.Structural: bindingDomainRoundTrip :: Eq a => StructuralBinding a shape -> a -> Bool
+ Keiro.Codec.Structural: bindingShapeRoundTrip :: Eq shape => StructuralBinding a shape -> shape -> Bool
+ Keiro.Codec.Structural: data StructuralBinding a shape
+ Keiro.Codec.Structural: decodeViaBinding :: StructuralBinding a shape -> (Value -> Either Text shape) -> Value -> Either Text a
+ Keiro.Codec.Structural: encodeViaBinding :: StructuralBinding a shape -> (shape -> Value) -> a -> Value
+ Keiro.Codec.Structural: instance GHC.Classes.Eq a => GHC.Classes.Eq (Keiro.Codec.Structural.FixtureCases a)
+ Keiro.Codec.Structural: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Keiro.Codec.Structural.FixtureCases a)
+ Keiro.Codec.Structural: newtype FixtureCases a
+ Keiro.Codec.Structural.Generic: class GNominalBinding (domain :: Type -> Type) (shape :: Type -> Type)
+ Keiro.Codec.Structural.Generic: genericStructuralBinding :: (Generic domain, Generic shape, GNominalBinding (Rep domain) (Rep shape)) => StructuralBinding domain shape
+ Keiro.Codec.Structural.Generic: instance (Keiro.Codec.Structural.Generic.GNominalBinding domainLeft shapeLeft, Keiro.Codec.Structural.Generic.GNominalBinding domainRight shapeRight) => Keiro.Codec.Structural.Generic.GNominalBinding (domainLeft GHC.Internal.Generics.:*: domainRight) (shapeLeft GHC.Internal.Generics.:*: shapeRight)
+ Keiro.Codec.Structural.Generic: instance (Keiro.Codec.Structural.Generic.GNominalBinding domainLeft shapeLeft, Keiro.Codec.Structural.Generic.GNominalBinding domainRight shapeRight) => Keiro.Codec.Structural.Generic.GNominalBinding (domainLeft GHC.Internal.Generics.:+: domainRight) (shapeLeft GHC.Internal.Generics.:+: shapeRight)
+ Keiro.Codec.Structural.Generic: instance (Keiro.Codec.Structural.Generic.SameConstructor domainMeta shapeMeta, Keiro.Codec.Structural.Generic.GNominalBinding domain shape) => Keiro.Codec.Structural.Generic.GNominalBinding (GHC.Internal.Generics.M1 GHC.Internal.Generics.C domainMeta domain) (GHC.Internal.Generics.M1 GHC.Internal.Generics.C shapeMeta shape)
+ Keiro.Codec.Structural.Generic: instance (Keiro.Codec.Structural.Generic.SameSelector domainMeta shapeMeta, Keiro.Codec.Structural.Generic.GNominalBinding domain shape) => Keiro.Codec.Structural.Generic.GNominalBinding (GHC.Internal.Generics.M1 GHC.Internal.Generics.S domainMeta domain) (GHC.Internal.Generics.M1 GHC.Internal.Generics.S shapeMeta shape)
+ Keiro.Codec.Structural.Generic: instance (TypeError ...) => Keiro.Codec.Structural.Generic.GNominalBinding domain shape
+ Keiro.Codec.Structural.Generic: instance Keiro.Codec.Structural.Generic.GNominalBinding (GHC.Internal.Generics.K1 domainIndex value) (GHC.Internal.Generics.K1 shapeIndex value)
+ Keiro.Codec.Structural.Generic: instance Keiro.Codec.Structural.Generic.GNominalBinding GHC.Internal.Generics.U1 GHC.Internal.Generics.U1
+ Keiro.Codec.Structural.Generic: instance Keiro.Codec.Structural.Generic.GNominalBinding GHC.Internal.Generics.V1 GHC.Internal.Generics.V1
+ Keiro.Codec.Structural.Generic: instance Keiro.Codec.Structural.Generic.GNominalBinding domain shape => Keiro.Codec.Structural.Generic.GNominalBinding (GHC.Internal.Generics.M1 GHC.Internal.Generics.D domainMeta domain) (GHC.Internal.Generics.M1 GHC.Internal.Generics.D shapeMeta shape)
+ Keiro.EventStream: [stateShapeHash] :: StateCodec state -> !Text
- Keiro.EventStream: StateCodec :: !Int -> !Text -> !state -> Value -> !Value -> Either Text state -> StateCodec state
+ Keiro.EventStream: StateCodec :: !Int -> !Text -> !Text -> !state -> Value -> !Value -> Either Text state -> StateCodec state

Files

CHANGELOG.md view
@@ -6,7 +6,37 @@  ## [Unreleased] -_No unreleased changes._+## 0.4.0.1 — 2026-07-28++### Other Changes++- Adds PVP upper bounds to every dependency that previously carried a lower+  bound only, so `cabal check` reports no packaging warnings. No API or+  behaviour change from 0.4.0.0, which was tagged but never published.+++## 0.4.0.0 — 2026-07-28++### Breaking Changes++- `StateCodec` includes the control-state/fold shape discriminator used with+  the register-layout hash; stale snapshot seeds are invalidated when either+  executable state shape changes.+- Validated event-stream construction rejects invalid event codec schemas,+  tags, and upcaster chains at assembly. `mkEventStreamUnchecked` remains the+  explicit emergency-forensics bypass.+- Requires Keiki 0.4 and handles its typed projection terms and validation+  findings explicitly.++### New Features++- Adds the stable `Keiro.Codec.Structural` integration surface for generated+  structural codecs: total `StructuralBinding` values, deterministic labelled+  `FixtureCases`, both binding-law helpers, and one-way JSON delegation helpers.+- Adds `Keiro.Codec.Structural.Generic.genericStructuralBinding`, an opt-in+  nominal adapter that accepts only exact constructor, selector, arity, order,+  and field-type correspondence and otherwise emits a compile-time direction+  to the scaffolded binding module.  ## 0.3.0.0 — 2026-07-14 
keiro-core.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            keiro-core-version:         0.3.0.0+version:         0.4.0.1 synopsis:        Core contracts for Keiro packages description:   Stable stream, codec, event-stream, and integration-event contracts@@ -40,6 +40,8 @@   import:          warnings, shared   exposed-modules:     Keiro.Codec+    Keiro.Codec.Structural+    Keiro.Codec.Structural.Generic     Keiro.EventStream     Keiro.EventStream.Validate     Keiro.Integration.Event@@ -50,16 +52,16 @@    hs-source-dirs:  src   build-depends:-    , aeson         >=2.2-    , aeson-casing  >=0.2+    , aeson         >=2.2  && <2.3+    , aeson-casing  >=0.2  && <0.3     , base          >=4.21 && <5-    , bytestring    >=0.11-    , deepseq       >=1.5-    , generic-lens  >=2.2-    , keiki         >=0.2  && <0.3+    , bytestring    >=0.11 && <0.13+    , deepseq       >=1.5  && <1.6+    , generic-lens  >=2.2  && <2.4+    , keiki         >=0.4  && <0.5     , kiroku-store  >=0.3  && <0.4-    , lens          >=5.2-    , scientific    >=0.3-    , text          >=2.1-    , time          >=1.12-    , uuid          >=1.3+    , lens          >=5.2  && <5.4+    , scientific    >=0.3  && <0.4+    , text          >=2.1  && <2.2+    , time          >=1.12 && <1.15+    , uuid          >=1.3  && <1.4
+ src/Keiro/Codec/Structural.hs view
@@ -0,0 +1,82 @@+{- | Stable runtime contract between consumer-owned domain types and+Keiro-generated structural wire shapes.++Stability: this module is a published integration point for consumer binding+modules and @keiro-dsl@ generated code. Its types and record field names are+stable within a major version. Additions are permitted; renames or semantic+changes require a major version bump and a coordinated @keiro-dsl@ release.++The generated codec is the single authority for the wire schema. A consumer+'Data.Aeson.ToJSON' or 'Data.Aeson.FromJSON' instance may delegate to that+generated codec through a binding, but Keiro never delegates structural+encoding to a consumer instance.+-}+module Keiro.Codec.Structural (+    StructuralBinding (..),+    FixtureCases (..),+    bindingDomainRoundTrip,+    bindingShapeRoundTrip,+    encodeViaBinding,+    decodeViaBinding,+)+where++import Data.Aeson (Value)+import Data.List.NonEmpty (NonEmpty)+import Data.Text (Text)++{- | A total construction and destruction boundary between a consumer-owned+type and its declared structural shape.++Consumer invariants must be represented by the shape. If converting a valid+shape into the consumer type can fail, the declaration is not structural and+must use Keiro's opaque mapping mode instead.+-}+data StructuralBinding a shape = StructuralBinding+    { bindingToShape :: !(a -> shape)+    , bindingFromShape :: !(shape -> a)+    }++{- | Deterministic, labelled consumer values used by generated conformance+harnesses.++The generator checks these cases against every declared union and nullable+branch. Values are never invented through @Arbitrary@ or @Default@. Generated+harnesses also reject empty or duplicate labels so failures keep a stable+identity.+-}+newtype FixtureCases a = FixtureCases+    { fixtureCases :: NonEmpty (Text, a)+    }+    deriving stock (Eq, Show)++-- | Check that a consumer value survives conversion through its shape.+bindingDomainRoundTrip :: (Eq a) => StructuralBinding a shape -> a -> Bool+bindingDomainRoundTrip binding value =+    bindingFromShape binding (bindingToShape binding value) == value++-- | Check that every declared shape survives conversion through the consumer type.+bindingShapeRoundTrip :: (Eq shape) => StructuralBinding a shape -> shape -> Bool+bindingShapeRoundTrip binding shape =+    bindingToShape binding (bindingFromShape binding shape) == shape++{- | Encode a consumer value by first converting it to the generated shape.++This is the sanctioned delegation direction for a consumer-owned JSON+instance: the generated shape encoder remains the wire authority.+-}+encodeViaBinding :: StructuralBinding a shape -> (shape -> Value) -> a -> Value+encodeViaBinding binding encodeShape = encodeShape . bindingToShape binding++{- | Decode the generated shape and then apply the binding's total constructor.++All failure comes from parsing JSON into the generated shape. Applying the+binding introduces no hidden semantic rejection.+-}+decodeViaBinding ::+    StructuralBinding a shape ->+    (Value -> Either Text shape) ->+    Value ->+    Either Text a+decodeViaBinding binding decodeShape value =+    bindingFromShape binding <$> decodeShape value
+ src/Keiro/Codec/Structural/Generic.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE EmptyCase #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Exact, opt-in nominal derivation for structural bindings.++The derivation supplies only nominal construction and destruction; wire keys,+union tags, presence, nullability, and defaults remain exclusively in the+@.keiro@ spec and the generated codec. Exact representation mismatches fail at+compile time; both binding laws and finite codec cases remain required evidence+against implementation defects and semantic mistakes.+-}+module Keiro.Codec.Structural.Generic (+    GNominalBinding,+    genericStructuralBinding,+) where++import Data.Kind (Constraint, Type)+import GHC.Generics+import GHC.TypeLits (ErrorMessage (..), TypeError)+import Keiro.Codec.Structural (StructuralBinding (..))++{- | Generic representations with identical constructor names, selector names,+field order, arity, and field types. There are deliberately no coercion,+prefix-stripping, or positional-only options.+-}+class GNominalBinding (domain :: Type -> Type) (shape :: Type -> Type) where+    gNominalToShape :: domain parameter -> shape parameter+    gNominalFromShape :: shape parameter -> domain parameter++instance {-# OVERLAPPING #-} (GNominalBinding domain shape) => GNominalBinding (M1 D domainMeta domain) (M1 D shapeMeta shape) where+    gNominalToShape (M1 value) = M1 (gNominalToShape value)+    gNominalFromShape (M1 value) = M1 (gNominalFromShape value)++instance {-# OVERLAPPING #-} (SameConstructor domainMeta shapeMeta, GNominalBinding domain shape) => GNominalBinding (M1 C domainMeta domain) (M1 C shapeMeta shape) where+    gNominalToShape (M1 value) = M1 (gNominalToShape value)+    gNominalFromShape (M1 value) = M1 (gNominalFromShape value)++instance {-# OVERLAPPING #-} (SameSelector domainMeta shapeMeta, GNominalBinding domain shape) => GNominalBinding (M1 S domainMeta domain) (M1 S shapeMeta shape) where+    gNominalToShape (M1 value) = M1 (gNominalToShape value)+    gNominalFromShape (M1 value) = M1 (gNominalFromShape value)++instance {-# OVERLAPPING #-} (GNominalBinding domainLeft shapeLeft, GNominalBinding domainRight shapeRight) => GNominalBinding (domainLeft :*: domainRight) (shapeLeft :*: shapeRight) where+    gNominalToShape (left :*: right) = gNominalToShape left :*: gNominalToShape right+    gNominalFromShape (left :*: right) = gNominalFromShape left :*: gNominalFromShape right++instance {-# OVERLAPPING #-} (GNominalBinding domainLeft shapeLeft, GNominalBinding domainRight shapeRight) => GNominalBinding (domainLeft :+: domainRight) (shapeLeft :+: shapeRight) where+    gNominalToShape (L1 value) = L1 (gNominalToShape value)+    gNominalToShape (R1 value) = R1 (gNominalToShape value)+    gNominalFromShape (L1 value) = L1 (gNominalFromShape value)+    gNominalFromShape (R1 value) = R1 (gNominalFromShape value)++instance {-# OVERLAPPING #-} GNominalBinding (K1 domainIndex value) (K1 shapeIndex value) where+    gNominalToShape (K1 value) = K1 value+    gNominalFromShape (K1 value) = K1 value++instance {-# OVERLAPPING #-} GNominalBinding U1 U1 where+    gNominalToShape U1 = U1+    gNominalFromShape U1 = U1++instance {-# OVERLAPPING #-} GNominalBinding V1 V1 where+    gNominalToShape value = case value of {}+    gNominalFromShape value = case value of {}++instance+    {-# OVERLAPPABLE #-}+    ( TypeError+        ( 'Text "keiro structural binding has no exact nominal correspondence between "+            ':<>: 'ShowType domain+            ':<>: 'Text " and "+            ':<>: 'ShowType shape+            ':$$: 'Text "Run keiro-dsl scaffold and fill the binding by hand at this error location in the scaffolded module."+        )+    ) =>+    GNominalBinding domain shape+    where+    gNominalToShape _ = error "unreachable: TypeError prevents generic structural binding construction"+    gNominalFromShape _ = error "unreachable: TypeError prevents generic structural binding construction"++type family SameConstructor (domainMeta :: Meta) (shapeMeta :: Meta) :: Constraint where+    SameConstructor ('MetaCons name domainFixity domainRecord) ('MetaCons name shapeFixity shapeRecord) = ()+    SameConstructor ('MetaCons domainName domainFixity domainRecord) ('MetaCons shapeName shapeFixity shapeRecord) =+        TypeError+            ( 'Text "keiro structural binding constructor mismatch: "+                ':<>: 'ShowType domainName+                ':<>: 'Text " versus "+                ':<>: 'ShowType shapeName+                ':$$: 'Text "Run keiro-dsl scaffold and fill the binding by hand at this error location in the scaffolded module."+            )++type family SameSelector (domainMeta :: Meta) (shapeMeta :: Meta) :: Constraint where+    SameSelector ('MetaSel name domainUnpack domainStrict domainDecided) ('MetaSel name shapeUnpack shapeStrict shapeDecided) = ()+    SameSelector ('MetaSel domainName domainUnpack domainStrict domainDecided) ('MetaSel shapeName shapeUnpack shapeStrict shapeDecided) =+        TypeError+            ( 'Text "keiro structural binding selector mismatch: "+                ':<>: 'ShowType domainName+                ':<>: 'Text " versus "+                ':<>: 'ShowType shapeName+                ':$$: 'Text "Run keiro-dsl scaffold and fill the binding by hand at this error location in the scaffolded module."+            )++-- | Derive a total binding when both generic representations correspond exactly.+genericStructuralBinding ::+    ( Generic domain+    , Generic shape+    , GNominalBinding (Rep domain) (Rep shape)+    ) =>+    StructuralBinding domain shape+genericStructuralBinding =+    StructuralBinding+        { bindingToShape = to . gNominalToShape . from+        , bindingFromShape = to . gNominalFromShape . from+        }
src/Keiro/EventStream.hs view
@@ -87,21 +87,29 @@  {- | How to serialize and deserialize a stream's snapshot state. -'stateCodecVersion' and 'shapeHash' together gate snapshot reuse: a stored-snapshot is only loaded when both match the current codec, so a change to-the snapshot encoding or to the shape of the folded state invalidates older-snapshots and forces a clean rehydration from events.+'stateCodecVersion', 'shapeHash', and 'stateShapeHash' together gate snapshot+reuse: a stored snapshot is only loaded when all three match the current+codec, so incompatible encodings, register layouts, and control-state shapes+invalidate older snapshots and force a clean rehydration from events.  * 'stateCodecVersion' — bumped when the snapshot encoding changes-  incompatibly.-* 'shapeHash' — a digest of the folded-state shape; protects against-  silently loading a snapshot whose structure no longer matches.+  incompatibly, and whenever fold logic changes in a way the structural hashes+  and any composed fold fingerprint cannot see.+* 'shapeHash' — a digest of the register-file layout.+* 'stateShapeHash' — a digest of the control-state shape, optionally composed+  with a fold fingerprint. * 'encode' \/ 'decode' — the JSON serialization of the @(state,   registers)@ pair.++Hand-written guard and update function bodies are not structurally+inspectable. Changing them without also changing a composed fold fingerprint+MUST bump 'stateCodecVersion'; otherwise an old snapshot can still match and+be served as a stale hydration seed. -} data StateCodec state = StateCodec     { stateCodecVersion :: !Int     , shapeHash :: !Text+    , stateShapeHash :: !Text     , encode :: !(state -> Value)     , decode :: !(Value -> Either Text state)     }
src/Keiro/EventStream/Validate.hs view
@@ -8,6 +8,10 @@ also checks guard determinism and structural reachability. This module lifts keiki's umbrella check ('Keiki.validateTransducer') to the 'EventStream' boundary so a service can assert all of its streams are sound before hydration.+The boundary also validates the event 'Keiro.Codec.Codec' construction+invariants, so malformed schema versions, duplicate event tags or upcaster+sources, out-of-range rungs, and incomplete chains fail before the service+touches stored streams.  Every warning enabled by the selected 'ValidationOptions' makes construction fail. This is intentionally stricter than a pure keiki use: events are keiro's@@ -52,6 +56,7 @@     defaultValidationOptions,     validateTransducer,  )+import Keiro.Codec qualified as Codec import Keiro.EventStream (EventStream (..), SnapshotPolicy (..), StateCodec (..)) import System.IO.Unsafe (unsafePerformIO) @@ -82,8 +87,9 @@ {- | Run keiki's pure umbrella check over a stream's transducer with the default options. This includes hidden-input, head recoverability, inversion ambiguity, guarded input reads, state-changing epsilon edges, determinism, and dead-edge-checks. An empty list means the stream passed every enabled check. Pure; no-solver.+checks, plus the event codec's schema and upcaster-chain construction+invariants. An empty list means the stream passed every enabled check. Pure;+no solver. -} validateEventStream ::     (Bounded s, Enum s, Ord s, Show s) =>@@ -106,7 +112,8 @@     EventStream (HsPred rs ci) rs s ci co ->     [EventStreamWarning] validateEventStreamWith opts label es =-    snapshotWarnings label es+    codecConfigWarnings label es+        <> snapshotWarnings label es         <> initialSnapshotEncodeWarnings label es         <> [ EventStreamWarning{eswStreamLabel = label, eswReason = renderWarning w}            | w <- validateTransducer (forceReplayContract opts) (transducer es)@@ -170,9 +177,10 @@                     <> show warns  {- | Wrap an 'EventStream' /without validation/. This skips every keiki and-keiro check, including the replay-contract checks that 'mkEventStream'-force-enables. A stream admitted through this function can silently lose state-changes and fail hydration. Tests and emergency forensics only; never use it+keiro check, including event-codec construction validation and the+replay-contract checks that 'mkEventStream' force-enables. A stream admitted+through this function can silently lose state changes, select the wrong+upcaster, or fail hydration. Tests and emergency forensics only; never use it for production streams. Prefer 'mkEventStream'. -} mkEventStreamUnchecked ::@@ -180,7 +188,45 @@     ValidatedEventStream phi rs s ci co mkEventStreamUnchecked = ValidatedEventStream -{- | Render a keiki warning to a human-readable reason. All eight constructors+codecConfigWarnings :: Text -> EventStream phi rs s ci co -> [EventStreamWarning]+codecConfigWarnings label es =+    case Codec.mkCodec (eventCodec es) of+        Right _ -> []+        Left err ->+            [ EventStreamWarning+                { eswStreamLabel = label+                , eswReason = "event codec misconfigured: " <> renderCodecConfigError err+                }+            ]++renderCodecConfigError :: Codec.CodecConfigError -> Text+renderCodecConfigError = \case+    Codec.CodecSchemaVersionInvalid version ->+        "schema version must be at least 1, got " <> showT version+    Codec.CodecDuplicateEventTypes eventTypes ->+        "duplicate event type tag(s): "+            <> Text.intercalate ", " [tag | Codec.EventType tag <- eventTypes]+    Codec.CodecDuplicateUpcasterSources versions ->+        "duplicate upcaster source version(s): "+            <> renderVersions versions+            <> "; only one rung may own each source version"+    Codec.CodecUpcasterSourceOutOfRange source target ->+        "upcaster source version "+            <> showT source+            <> " is outside the valid range 1.."+            <> showT (target - 1)+            <> " for target schema version "+            <> showT target+    Codec.CodecUpcasterChainIncomplete missing target ->+        "missing upcaster source version(s): "+            <> renderVersions missing+            <> "; stored payloads cannot reach target schema version "+            <> showT target+  where+    showT = Text.pack . show+    renderVersions = Text.intercalate ", " . map showT++{- | Render a keiki warning to a human-readable reason. Every constructor carry @tvwDetail@; the source vertex is @edgeSource . tvwEdge@ (or @tvwSource@ for pair warnings). -}@@ -202,6 +248,57 @@         "possibly-dead @" <> showT (edgeSource e) <> ": " <> Text.pack d     OpaqueGuard{tvwEdge = e, tvwDetail = d} ->         "opaque-guard @" <> showT (edgeSource e) <> ": " <> Text.pack d+    ProjectionResultUnsupported+        { tvwEdge = e+        , tvwProjectionPath = path+        , tvwProjectionShape = shape+        , tvwProjectionResultType = resultType+        , tvwDetail = d+        } ->+            "projection-result-unsupported @"+                <> showT (edgeSource e)+                <> " ["+                <> Text.pack path+                <> ", shape "+                <> Text.pack shape+                <> ", result "+                <> Text.pack resultType+                <> "]: "+                <> Text.pack d+    ProjectionOrderingUnsupported+        { tvwEdge = e+        , tvwProjectionPath = path+        , tvwProjectionShape = shape+        , tvwProjectionResultType = resultType+        , tvwDetail = d+        } ->+            "projection-ordering-unsupported @"+                <> showT (edgeSource e)+                <> " ["+                <> Text.pack path+                <> ", shape "+                <> Text.pack shape+                <> ", result "+                <> Text.pack resultType+                <> "]: "+                <> Text.pack d+    ProjectionOutsideGuard+        { tvwEdge = e+        , tvwProjectionPath = path+        , tvwProjectionShape = shape+        , tvwProjectionLocation = location+        , tvwDetail = d+        } ->+            "projection-outside-guard @"+                <> showT (edgeSource e)+                <> " ["+                <> Text.pack path+                <> ", shape "+                <> Text.pack shape+                <> ", location "+                <> Text.pack location+                <> "]: "+                <> Text.pack d   where     showT = Text.pack . show