packages feed

keiki 0.7.0.0 → 0.8.0.0

raw patch · 16 files changed

+987/−215 lines, 16 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Keiki.Render.Mermaid: [showGuardSummary] :: MermaidOptions -> Bool
- Keiki.Render.Mermaid: [showWrittenSlots] :: MermaidOptions -> Bool
+ Keiki.Core: [TOpaqueLit] :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). r -> Term rs ci ifs r
+ Keiki.Core: opaqueLit :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). r -> Term rs ci ifs r
+ Keiki.Render.Mermaid: MermaidUpdateHidden :: MermaidUpdateMode
+ Keiki.Render.Mermaid: MermaidUpdatePretty :: MermaidUpdateMode
+ Keiki.Render.Mermaid: MermaidUpdateWrittenSlots :: MermaidUpdateMode
+ Keiki.Render.Mermaid: [updateMode] :: MermaidOptions -> MermaidUpdateMode
+ Keiki.Render.Mermaid: data MermaidUpdateMode
+ Keiki.Render.Mermaid: instance GHC.Classes.Eq Keiki.Render.Mermaid.MermaidUpdateMode
+ Keiki.Render.Mermaid: instance GHC.Internal.Show.Show Keiki.Render.Mermaid.MermaidUpdateMode
+ Keiki.Render.Mermaid: toMermaidAlternativeWithOptions :: forall (rs1 :: [Slot]) (rs2 :: [Slot]) s1 s2 ci1 ci2 co1 co2. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2) => MermaidOptions -> Text -> Text -> SymTransducer (HsPred rs1 ci1) rs1 s1 ci1 co1 -> SymTransducer (HsPred rs2 ci2) rs2 s2 ci2 co2 -> Text
+ Keiki.Render.Mermaid: toMermaidCompose3NestedWith :: forall (rs :: [Slot]) s1 s2 s3 ci co. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2, Enum s3, Bounded s3, Show s3) => MermaidOptions -> SymTransducer (HsPred rs ci) rs (Composite s1 (Composite s2 s3)) ci co -> Text
+ Keiki.Render.Mermaid: toMermaidCompose3With :: forall (rs :: [Slot]) s1 s2 s3 ci co. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2, Enum s3, Bounded s3, Show s3) => MermaidOptions -> SymTransducer (HsPred rs ci) rs (Composite s1 (Composite s2 s3)) ci co -> Text
+ Keiki.Render.Mermaid: toMermaidCompositeNestedWith :: forall (rs :: [Slot]) s1 s2 ci co. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2) => MermaidOptions -> SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co -> Text
+ Keiki.Render.Mermaid: toMermaidCompositeWith :: forall s1 s2 (rs :: [Slot]) ci co. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2) => MermaidOptions -> SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co -> Text
+ Keiki.Render.Mermaid: toMermaidFeedback1With :: forall s1 s2 (rs1 :: [Slot]) (rs2 :: [Slot]) ci co. (Enum s1, Bounded s1, Show s1, Enum s2, Bounded s2, Show s2, WeakenR rs1, WeakenR rs2, Disjoint (Names rs2) (Names rs1), Disjoint (Names rs1) (Names (Append rs2 rs1))) => MermaidOptions -> SymTransducer (HsPred rs1 ci) rs1 s1 ci co -> SymTransducer (HsPred rs2 co) rs2 s2 co ci -> Text
+ Keiki.Render.Mermaid: toTopologyMermaid :: forall s (rs :: [Slot]) ci co. (Enum s, Bounded s, Show s) => SymTransducer (HsPred rs ci) rs s ci co -> Text
+ Keiki.Render.Mermaid: topologyMermaidOptions :: MermaidOptions
- Keiki.Core: [TLit] :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). r -> Term rs ci ifs r
+ Keiki.Core: [TLit] :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). Show r => r -> Term rs ci ifs r
- Keiki.Core: lit :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). r -> Term rs ci ifs r
+ Keiki.Core: lit :: forall r (rs :: [Slot]) ci (ifs :: [Slot]). Show r => r -> Term rs ci ifs r
- Keiki.Render.Mermaid: MermaidOptions :: Bool -> Bool -> MermaidGuardMode -> MermaidLabelLayout -> Maybe Int -> Maybe Int -> MermaidOutputLayout -> MermaidOptions
+ Keiki.Render.Mermaid: MermaidOptions :: MermaidGuardMode -> MermaidUpdateMode -> MermaidLabelLayout -> Maybe Int -> Maybe Int -> MermaidOutputLayout -> MermaidOptions

Files

CHANGELOG.md view
@@ -9,6 +9,36 @@ ## [Unreleased]  +## [0.8.0.0] — 2026-08-02++### Added++- `TOpaqueLit` and `opaqueLit` preserve exact concrete, replay, pure-analysis,+  and symbolic literal semantics while deliberately rendering the stored value+  as `<lit>`.+- `MermaidUpdateMode`, `topologyMermaidOptions`, and `toTopologyMermaid` make+  complete updates, compact written-slot summaries, and Keiki 0.7-compatible+  topology output explicit policies.+- Options-aware Mermaid entry points now cover composite, nested, three-way,+  alternative, feedback, and labeled diagrams. Backend-level regression tests+  pin semantic-label escaping through the documentation site's+  `beautiful-mermaid` renderer.++### Changed++- **Breaking:** `TLit` and `lit` now require `Show` so renderers derive ordinary+  literal text from the executable value. Exhaustive `Term` matches must handle+  `TOpaqueLit`; values without `Show`, secrets, and deliberately redacted values+  use `opaqueLit`.+- **Breaking:** `toMermaid` and every no-options shape renderer now default to+  readable guards, complete register assignments, multiline labels, and no+  semantic truncation. Use `toTopologyMermaid` or an options-aware shape+  renderer with `topologyMermaidOptions` for the previous compact bytes.+- **Breaking:** `MermaidOptions` removes `showWrittenSlots` and+  `showGuardSummary`. Set `updateMode = MermaidUpdateWrittenSlots` and+  `guardMode = MermaidGuardStructuralSummary` respectively.++ ## [0.7.0.0] — 2026-08-01  ### Added
README.md view
@@ -56,7 +56,8 @@ - per-vertex projections (the "B-presentation" view), - checked sequential composition (`composeChecked`) plus `alternative` and `feedback1`, - profunctor / `Category` / `Strong` / `Choice` / `Arrow` instances,-- Mermaid and Markdown renderers for documentation,+- behavior-readable Mermaid and Markdown renderers, with explicit compact+  topology output, - eager builder validation, default-on replay-safety checks, and optional   single-valuedness checks via SBV + z3. @@ -66,9 +67,9 @@  ## Status -Pre-1.0, prepared for the initial `0.1.0.0` Hackage release. The-planned v0.1 surface is implemented and validated against in-tree tests-plus the downstream `jitsurei` worked-example package.+Pre-1.0 and released under the Haskell PVP. The current surface is validated+against the in-tree suites plus the downstream `jitsurei` worked-example+package; see [`CHANGELOG.md`](CHANGELOG.md) for release history and migrations.  The core package is intentionally codec-free. JSON support lives in [`keiki-codec-json`](keiki-codec-json/README.md), and downstream codec@@ -117,6 +118,12 @@ Forward decisions, structured replay, `Acceptor`s, and the per-vertex view all operate from this one declaration. See the downstream `jitsurei` package for the full worked aggregates the test suite drives.++The primary `toMermaid emailDelivery` diagram is derived from the same edge AST+and shows its command, event constructor, complete register assignments, and+guard. Use `toTopologyMermaid emailDelivery` when only shape is intended.+Ordinary `lit` values appear in readable diagrams; model a value with+`opaqueLit` when it lacks `Show` or must remain redacted.  ## Repository documentation 
keiki.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            keiki-version:         0.7.0.0+version:         0.8.0.0 synopsis:        Pure core for symbolic-register transducer event sourcing. description:   A Haskell library for the pure core of event sourcing, workflow
src/Keiki/Composition.hs view
@@ -248,12 +248,13 @@ weakenL (SIdx i) = SIdx (weakenL @_ @rs2 i)  -- | Walk a 'Term' and weaken every register read across an rs2--- suffix. 'TInpCtorField' / 'TLit' do not touch the register file,+-- suffix. 'TInpCtorField', 'TLit', and 'TOpaqueLit' do not touch the register file, -- so they pass through unchanged. weakenLTerm ::   forall rs1 rs2 ci ifs r.   Term rs1 ci ifs r -> Term (Append rs1 rs2) ci ifs r weakenLTerm (TLit r) = TLit r+weakenLTerm (TOpaqueLit r) = TOpaqueLit r weakenLTerm (TReg ix) = TReg (weakenL @rs1 @rs2 ix) weakenLTerm (TInpCtorField ic ix) = TInpCtorField ic ix weakenLTerm (TApp1 f t) = TApp1 f (weakenLTerm @rs1 @rs2 t)@@ -331,6 +332,7 @@   (WeakenR rs1) =>   Term rs2 ci ifs r -> Term (Append rs1 rs2) ci ifs r weakenRTerm (TLit r) = TLit r+weakenRTerm (TOpaqueLit r) = TOpaqueLit r weakenRTerm (TReg ix) = TReg (weakenR @rs1 ix) weakenRTerm (TInpCtorField ic ix) = TInpCtorField ic ix weakenRTerm (TApp1 f t) = TApp1 f (weakenRTerm @rs1 @rs2 t)@@ -466,12 +468,13 @@ -- Structural walkers see an opaque 'TApp1' over a harmless literal, so a -- constructor-mismatched composite edge cannot crash validation or rendering. poisonTerm :: String -> Term rs ci ifs r-poisonTerm message = TApp1 (\() -> error message) (TLit ())+poisonTerm message = TApp1 (\() -> error message) (TOpaqueLit ())  -- | Detect a field read for a constructor other than the mid constructor -- produced by the t1 output currently being substituted. termHasCtorMismatch :: String -> Term rs ci ifs r -> Bool termHasCtorMismatch _ (TLit _) = False+termHasCtorMismatch _ (TOpaqueLit _) = False termHasCtorMismatch _ (TReg _) = False termHasCtorMismatch expected (TInpCtorField ic _) = icName ic /= expected termHasCtorMismatch expected (TApp1 _ term) = termHasCtorMismatch expected term@@ -500,6 +503,7 @@   OutTerm rs1 ci1 mid ->   Term (Append rs1 rs2) ci1 ifsR r substTerm (TLit r) _o1 = TLit r+substTerm (TOpaqueLit r) _o1 = TOpaqueLit r substTerm (TReg ix2) _o1 = TReg (weakenR @rs1 ix2) substTerm (TInpCtorField ic2 ix2) o1 =   substInputField @rs1 @rs2 ic2 ix2 o1@@ -593,7 +597,9 @@ projectThroughTermWithStatus witness (TInpCtorField ic ix) =   (TFieldProj witness (PBInp ic ix), ProjectionPreserved) projectThroughTermWithStatus witness (TLit owner) =-  (TLit (fieldWitnessGet witness owner), ProjectionFolded)+  (TOpaqueLit (fieldWitnessGet witness owner), ProjectionFolded)+projectThroughTermWithStatus witness (TOpaqueLit owner) =+  (TOpaqueLit (fieldWitnessGet witness owner), ProjectionFolded) projectThroughTermWithStatus witness ownerTerm =   (TApp1 (fieldWitnessGet witness) ownerTerm, ProjectionLowered) @@ -805,12 +811,13 @@  -- | Lift a 'Term' from the left side's input alphabet to -- @Either ci1 ci2@. Walks the AST and adjusts every 'TInpCtorField'--- to read through 'leftInCtor'. 'TLit' / 'TReg' don't depend on+-- to read through 'leftInCtor'. Literals and 'TReg' don't depend on -- @ci@ and pass through unchanged. liftLTermAlt ::   forall rs ci1 ci2 ifs r.   Term rs ci1 ifs r -> Term rs (Either ci1 ci2) ifs r liftLTermAlt (TLit r) = TLit r+liftLTermAlt (TOpaqueLit r) = TOpaqueLit r liftLTermAlt (TReg ix) = TReg ix liftLTermAlt (TInpCtorField ic ix) = TInpCtorField (leftInCtor ic) ix liftLTermAlt (TApp1 f t) = TApp1 f (liftLTermAlt @rs @ci1 @ci2 t)@@ -829,6 +836,7 @@   forall rs ci1 ci2 ifs r.   Term rs ci2 ifs r -> Term rs (Either ci1 ci2) ifs r liftRTermAlt (TLit r) = TLit r+liftRTermAlt (TOpaqueLit r) = TOpaqueLit r liftRTermAlt (TReg ix) = TReg ix liftRTermAlt (TInpCtorField ic ix) = TInpCtorField (rightInCtor ic) ix liftRTermAlt (TApp1 f t) = TApp1 f (liftRTermAlt @rs @ci1 @ci2 t)@@ -1104,6 +1112,7 @@   Term rs ci ifs r ->   Term rs ci ifs r applyEnvTerm _ (TLit r) = TLit r+applyEnvTerm _ (TOpaqueLit r) = TOpaqueLit r applyEnvTerm env (TReg ix) = maybe (TReg ix) id (lookupPending ix env) applyEnvTerm _ (TInpCtorField ic ix) = TInpCtorField ic ix applyEnvTerm env (TApp1 f term) = TApp1 f (applyEnvTerm env term)@@ -1251,6 +1260,7 @@  termExpectedReads :: Term rs ci ifs r -> [(String, Int)] termExpectedReads (TLit _) = []+termExpectedReads (TOpaqueLit _) = [] termExpectedReads (TReg _) = [] termExpectedReads (TInpCtorField ic ix) = [(icName ic, indexInt ix)] termExpectedReads (TApp1 _ term) = termExpectedReads term@@ -1341,6 +1351,7 @@      goTerm :: forall ifs r. Term rs2 mid ifs r -> [ComposeAlignmentWarning s1 s2]     goTerm (TLit _) = []+    goTerm (TOpaqueLit _) = []     goTerm (TReg _) = []     goTerm (TInpCtorField _ _) = []     goTerm (TApp1 _ term) = goTerm term@@ -1393,6 +1404,7 @@      goTerm :: forall ifs r. Term rs ci ifs r -> [ComposeAlignmentWarning s1 s2]     goTerm (TLit _) = []+    goTerm (TOpaqueLit _) = []     goTerm (TReg _) = []     goTerm (TInpCtorField _ _) = []     goTerm (TApp1 _ term) = goTerm term
src/Keiki/Core.hs view
@@ -119,6 +119,7 @@     regProj,     inpProj,     lit,+    opaqueLit,     tadd,     tsub,     tmul,@@ -536,7 +537,7 @@ -- The @ifs :: [Slot]@ parameter is the /input field schema/ this term -- may project from: it is pinned by 'TInpCtorField' (whose 'Index' is -- into @ifs@) and left free by terms that do not read an input field--- ('TLit', 'TReg'). Threading @ifs@ through the AST is what lets an+-- ('TLit', 'TOpaqueLit', 'TReg'). Threading @ifs@ through the AST is what lets an -- 'OutFields' (and hence an 'OPack') guarantee /by construction/ that -- every top-level input projection reads the same constructor schema as -- the 'OPack''s 'InCtor' — so 'solveOutput' recovers a command field@@ -545,7 +546,12 @@ -- existentially hide @ifs@, so it never leaks into the 'Edge' / -- 'SymTransducer' surface. See @docs/research/tinpproj-design.md@. data Term (rs :: [Slot]) (ci :: Type) (ifs :: [Slot]) (r :: Type) where-  TLit :: r -> Term rs ci ifs r+  -- | An ordinary literal. The 'Show' dictionary is presentation evidence+  -- retained for renderers; evaluation and proof paths use only the value.+  TLit :: (Show r) => r -> Term rs ci ifs r+  -- | A display-opaque literal. It has the same concrete and symbolic meaning+  -- as 'TLit' but renderers deliberately do not expose its value.+  TOpaqueLit :: r -> Term rs ci ifs r   TReg :: Index rs r -> Term rs ci ifs r   -- | Structural input projection: read field @ix@ of the input   --     constructor described by @ic@. The 'InCtor' value names the@@ -1032,10 +1038,16 @@   Term rs ci ifs (FieldResult projection) inpProj witness ic ix = TFieldProj witness (PBInp ic ix) --- | A constant 'Term'.-lit :: r -> Term rs ci ifs r+-- | A readable constant 'Term'. Renderers derive its text from 'show'; use+-- 'opaqueLit' when the value has no 'Show' instance or must remain redacted.+lit :: (Show r) => r -> Term rs ci ifs r lit = TLit +-- | A display-opaque constant 'Term'. This changes presentation only: concrete+-- evaluation and symbolic translation still treat the stored value exactly.+opaqueLit :: r -> Term rs ci ifs r+opaqueLit = TOpaqueLit+ -- | Structural arithmetic smart constructors. @tadd@\/@tsub@\/@tmul@ -- build a 'TArith' over @+@\/@-@\/@*@. The operand type must be numeric -- ('Num') and 'Typeable'; the SBV translator reads them structurally@@ -1137,6 +1149,7 @@ -- | Evaluate a 'Term' against a register file and an input symbol. evalTerm :: Term rs ci ifs r -> RegFile rs -> ci -> r evalTerm (TLit r) _ _ = r+evalTerm (TOpaqueLit r) _ _ = r evalTerm (TReg ix) regs _ = regs ! ix evalTerm (TInpCtorField ic ix) _ ci = case icMatch ic ci of   Just rf -> rf ! ix@@ -2022,7 +2035,8 @@ -- == Recompute-and-verify (EP-47) -- -- The command is recovered from the /invertible/ fields alone--- (@TLit@\/@TReg@\/@TInpCtorField@); /derived/ fields (@TArith@\/@TApp1@\/+-- (@TLit@\/@TOpaqueLit@\/@TReg@\/@TInpCtorField@); /derived/ fields+-- (@TArith@\/@TApp1@\/ -- @TApp2@) are skipped during recovery by 'gatherInpEntries'. After the -- command is rebuilt, the observed field tuple is rebuilt with each -- /derived/ field recomputed forward (via 'recomputeDerivedFields') and@@ -2067,7 +2081,7 @@ -- | Rebuild an observed output-field tuple, recomputing each /derived/ -- field ('TApp1'\/'TApp2'\/'TArith') forward via 'evalTerm' against the -- recovered command and the pre-update registers, while leaving every--- /invertible/ field ('TLit'\/'TReg'\/'TInpCtorField') at its observed+-- /invertible/ field ('TLit'\/'TOpaqueLit'\/'TReg'\/'TInpCtorField') at its observed -- value. Used by 'solveOutput' (EP-47 recompute-and-verify): comparing the -- rebuilt event to the observed one (via 'Eq' on @co@) then verifies -- exactly the derived fields. Invertible fields are deliberately /not/@@ -2091,7 +2105,7 @@  -- | Walk an 'OutFields' HList in lockstep with an observed-fields -- tuple, gathering '(Index, value)' pairs for the named 'InCtor' from--- the /invertible/ fields. 'TLit'\/'TReg' contribute nothing; a+-- the /invertible/ fields. 'TLit'\/'TOpaqueLit'\/'TReg' contribute nothing; a -- 'TInpCtorField' for the matching 'InCtor' contributes its -- '(Index, value)' pair. Since EP-47 the /derived/ fields -- ('TArith'\/'TApp1'\/'TApp2') are /skipped/ (they contribute no@@ -2127,6 +2141,7 @@   where     stepOne :: forall f. Term rs ci ifs f -> f -> InCtor ci ifs -> Maybe [ByIndex ifs]     stepOne (TLit _) _val _ = Just []+    stepOne (TOpaqueLit _) _val _ = Just []     stepOne (TReg _) _val _ = Just []     stepOne (TInpCtorField ic2 ix) val ic1       | icName ic1 == icName ic2 = Just [ByIndex ix val]@@ -2322,6 +2337,7 @@ -- | Does the 'Term' read the input symbol via 'TInpCtorField'? termReadsInput :: Term rs ci ifs r -> Bool termReadsInput (TLit _) = False+termReadsInput (TOpaqueLit _) = False termReadsInput (TReg _) = False termReadsInput (TInpCtorField _ _) = True termReadsInput (TApp1 _ t) = termReadsInput t@@ -2338,6 +2354,7 @@   where     termHasInpCtorField :: Term rs ci ifs r -> Bool     termHasInpCtorField (TLit _) = False+    termHasInpCtorField (TOpaqueLit _) = False     termHasInpCtorField (TReg _) = False     termHasInpCtorField (TInpCtorField _ _) = True     termHasInpCtorField (TApp1 _ t') = termHasInpCtorField t'@@ -2710,6 +2727,7 @@ -- deliberately falls back to a fresh domain-valid value. termHasOpaqueFallback :: forall rs ci ifs r. Term rs ci ifs r -> Bool termHasOpaqueFallback (TLit _) = False+termHasOpaqueFallback (TOpaqueLit _) = False termHasOpaqueFallback (TReg _) = False termHasOpaqueFallback (TInpCtorField _ _) = False termHasOpaqueFallback (TApp1 _ _) = True@@ -2774,6 +2792,7 @@  termProjectionInfos :: Term rs ci ifs r -> [ProjectionInfo] termProjectionInfos (TLit _) = []+termProjectionInfos (TOpaqueLit _) = [] termProjectionInfos (TReg _) = [] termProjectionInfos (TInpCtorField _ _) = [] termProjectionInfos (TApp1 _ term) = termProjectionInfos term@@ -2905,6 +2924,7 @@  termInCtorNames :: Term rs ci ifs r -> [String] termInCtorNames (TLit _) = []+termInCtorNames (TOpaqueLit _) = [] termInCtorNames (TReg _) = [] termInCtorNames (TInpCtorField ic _) = [icName ic] termInCtorNames (TApp1 _ t) = termInCtorNames t@@ -3094,8 +3114,9 @@ -- -- This structural check intentionally over-approximates ambiguity. It cannot -- prove semantic guard disjointness over recovered values or registers; it--- cannot compare differing 'TLit' values because 'TLit' carries no 'Eq' or--- 'Typeable' evidence; and it does not predict derived-field verification in+-- cannot compare differing literal values without the 'Eq' or 'Typeable'+-- evidence supplied by a predicate constructor; and it does not predict+-- derived-field verification in -- 'solveOutput'. It ignores tail events because replay equality-checks rather -- than inverts them. Different head constructor names are safe under the -- documented honesty law of 'wcMatch'. Literal-'PBot' guards are exempt because@@ -3311,17 +3332,27 @@   Term rs ci ifs1 r ->   Term rs ci ifs2 r ->   PureFragment-pureEquality (TLit a) (TLit b)-  | a == b = PureKnown emptyPureGuard-  | otherwise = PureUnsatisfiable-pureEquality variable (TLit literalValue)-  | Just name <- pureVariable variable =+pureEquality left right+  | Just a <- termLiteralValue left,+    Just b <- termLiteralValue right,+    a == b =+      PureKnown emptyPureGuard+  | Just _ <- termLiteralValue left,+    Just _ <- termLiteralValue right =+      PureUnsatisfiable+  | Just literalValue <- termLiteralValue right,+    Just name <- pureVariable left =       knownComparison name PureEq literalValue (== literalValue)-pureEquality (TLit literalValue) variable-  | Just name <- pureVariable variable =+  | Just literalValue <- termLiteralValue left,+    Just name <- pureVariable right =       knownComparison name PureEq literalValue (== literalValue)-pureEquality _ _ = PureUnknown+  | otherwise = PureUnknown +termLiteralValue :: Term rs ci ifs r -> Maybe r+termLiteralValue (TLit value) = Just value+termLiteralValue (TOpaqueLit value) = Just value+termLiteralValue _ = Nothing+ pureOrdering ::   forall rs ci ifs1 ifs2 r.   (Ord r, Typeable r) =>@@ -3329,26 +3360,31 @@   Term rs ci ifs1 r ->   Term rs ci ifs2 r ->   PureFragment-pureOrdering relation (TLit a) (TLit b)-  | applyPureCmp relation a b = PureKnown emptyPureGuard-  | otherwise = PureUnsatisfiable-pureOrdering relation variable (TLit literalValue)-  | Just name <- pureVariable variable =+pureOrdering relation left right+  | Just a <- termLiteralValue left,+    Just b <- termLiteralValue right,+    applyPureCmp relation a b =+      PureKnown emptyPureGuard+  | Just _ <- termLiteralValue left,+    Just _ <- termLiteralValue right =+      PureUnsatisfiable+  | Just literalValue <- termLiteralValue right,+    Just name <- pureVariable left =       let normalized = pureRelation relation        in knownComparison             name             normalized             literalValue             (\value -> applyPureRelation normalized value literalValue)-pureOrdering relation (TLit literalValue) variable-  | Just name <- pureVariable variable =+  | Just literalValue <- termLiteralValue left,+    Just name <- pureVariable right =       let normalized = flipPureRelation (pureRelation relation)        in knownComparison             name             normalized             literalValue             (\value -> applyPureRelation normalized value literalValue)-pureOrdering _ _ _ = PureUnknown+  | otherwise = PureUnknown  knownComparison ::   (Typeable r) =>
src/Keiki/Profunctor.hs view
@@ -762,6 +762,7 @@           Term rs ci ifs a ->           Term rs (ci, c) ('("snd", c) ': ifs) a         goTerm (TLit r) = TLit r+        goTerm (TOpaqueLit r) = TOpaqueLit r         goTerm (TReg ix) = TReg ix         goTerm (TInpCtorField _ ix) = TInpCtorField cic (SIdx ix)         goTerm (TApp1 h a) = TApp1 h (goTerm a)@@ -956,6 +957,7 @@   where     go :: forall a. Term rs ci ifs a -> Term rs ci' ifs a     go (TLit r) = TLit r+    go (TOpaqueLit r) = TOpaqueLit r     go (TReg ix) = TReg ix     go (TInpCtorField ic ix) = TInpCtorField (contraInCtor f ic) ix     go (TApp1 h a) = TApp1 h (go a)@@ -971,6 +973,7 @@   where     go :: forall a. Term rs ci ifs a -> Term rs ci' ifs a     go (TLit r) = TLit r+    go (TOpaqueLit r) = TOpaqueLit r     go (TReg ix) = TReg ix     go (TInpCtorField ic ix) = TInpCtorField (contraMaybeInCtor f ic) ix     go (TApp1 h a) = TApp1 h (go a)
src/Keiki/Render/Inspector.hs view
@@ -1,7 +1,7 @@ -- | A Markdown edge-detail renderer for 'SymTransducer', a sibling to--- the Mermaid topology renderer in "Keiki.Render.Mermaid". Where--- 'Keiki.Render.Mermaid.toMermaid' shows the /shape/ of a workflow (one--- line per edge), this renderer lays out every edge in /full/: its+-- the behavioral Mermaid renderer in "Keiki.Render.Mermaid". Where+-- 'Keiki.Render.Mermaid.toMermaid' keeps executable behavior compact in+-- one transition label, this renderer expands every edge's metadata: its -- source and target states, its 0-based edge index, the input -- (command) constructor, the output (event) constructor(s), the guard -- predicate (structural and/or domain-readable), the register slots it
src/Keiki/Render/Mermaid.hs view
@@ -8,8 +8,8 @@ -- -- The rendered output is a @stateDiagram-v2@ block as 'Data.Text.Text'. -- It can be pasted into a Markdown file or Notion page; GitHub renders--- Mermaid blocks inline so PR reviewers see the topology diff alongside--- the source diff.+-- Mermaid blocks inline so PR reviewers see executable guard and update+-- semantics alongside the topology and source diff. -- -- The renderer is specialised to @phi ~ 'HsPred' rs ci@ — extracting -- the input-constructor name from an edge guard requires walking the@@ -24,13 +24,20 @@ --     for the initiative motivation. module Keiki.Render.Mermaid   ( toMermaid,+    toTopologyMermaid,     toMermaidAlternative,     toMermaidAlternativeWith,+    toMermaidAlternativeWithOptions,     toMermaidComposite,+    toMermaidCompositeWith,     toMermaidCompositeNested,+    toMermaidCompositeNestedWith,     toMermaidCompose3,+    toMermaidCompose3With,     toMermaidCompose3Nested,+    toMermaidCompose3NestedWith,     toMermaidFeedback1,+    toMermaidFeedback1With,     toMermaidAtlas,     toMermaidAtlasWith,     MermaidSection (..),@@ -42,11 +49,13 @@     toMermaidWithLabels,     MermaidOptions (..),     MermaidGuardMode (..),+    MermaidUpdateMode (..),     MermaidLabelLayout (..),     MermaidOutputLayout (..),     MermaidStateLabels (..),     duplicateStateIds,     defaultMermaidOptions,+    topologyMermaidOptions,     vertexLabel,     compositeLabel,     compose3Label,@@ -73,7 +82,7 @@   ) import Keiki.Generics (Append) import Keiki.Internal.Slots (indexNName)-import Keiki.Render.Pretty (prettyPred)+import Keiki.Render.Pretty (prettyPred, prettyUpdate)  -- | How an edge's guard predicate is rendered into the @[g: …]@ -- segment.@@ -81,7 +90,7 @@ --   * 'MermaidGuardHidden' — no guard segment at all. --   * 'MermaidGuardStructuralSummary' — the structural constructor-tag --     walk produced by 'guardSummary', e.g. @PAnd PInCtor PEq@. This is---     the legacy rendering that the 'showGuardSummary' flag selects.+--     the compact structural alternative to readable guards. --   * 'MermaidGuardPretty' — the domain-readable rendering produced by --     'Keiki.Render.Pretty.prettyPred', e.g. --     @(ConfirmAccount && ConfirmAccount.confirmCode == confirmCode)@.@@ -91,6 +100,19 @@   | MermaidGuardPretty   deriving stock (Eq, Show) +-- | How an edge's register update is rendered.+--+--   * 'MermaidUpdateHidden' — no update segment at all.+--   * 'MermaidUpdateWrittenSlots' — only the names of written slots,+--     retained as a compact compatibility option.+--   * 'MermaidUpdatePretty' — the complete update produced by+--     'Keiki.Render.Pretty.prettyUpdate', including right-hand terms.+data MermaidUpdateMode+  = MermaidUpdateHidden+  | MermaidUpdateWrittenSlots+  | MermaidUpdatePretty+  deriving stock (Eq, Show)+ -- | How dense edge labels are laid out. -- --   * 'MermaidLabelInline' — the current single-line @[seg; seg]@ form.@@ -116,32 +138,18 @@   | MermaidOutputCounted   deriving stock (Eq, Show) --- | Rendering options for the structural edge-summary suffix. All--- fields default to the no-suffix setting in 'defaultMermaidOptions', so--- the default rendering is byte-identical to 'toMermaid'.+-- | Rendering options for edge semantics and layout. ----- The record is extended /additively/: new fields are appended with a--- default that reproduces the existing behaviour, and existing fields--- are never removed or reordered. The legacy 'showGuardSummary' flag and--- the newer 'guardMode' field are reconciled by 'renderGuardSegment' (an--- explicit 'guardMode' wins; otherwise 'showGuardSummary' is honoured as--- the legacy spelling of 'MermaidGuardStructuralSummary').+-- 'defaultMermaidOptions' is the readable primary policy: complete guards+-- and updates, multiline labels, and no semantic truncation.+-- 'topologyMermaidOptions' is the explicit compact compatibility policy and+-- reproduces Keiki 0.7's topology-only bytes. data MermaidOptions = MermaidOptions-  { -- | When 'True', append the update's written-slot names, e.g.-    --     @[w: email; confirmCode; registeredAt]@.-    showWrittenSlots :: Bool,-    -- | When 'True', append a structural guard summary listing the-    --     guard's constructor / comparison tags, e.g. @[g: PAnd PInCtor PEq]@.-    --     This is the legacy spelling of @'guardMode' = 'MermaidGuardStructuralSummary'@;-    --     it is honoured only when 'guardMode' is left at its default-    --     ('MermaidGuardHidden').-    showGuardSummary :: Bool,-    -- | How the guard segment is rendered. When set to anything other-    --     than 'MermaidGuardHidden', it takes precedence over the legacy-    --     'showGuardSummary' flag. Defaults to 'MermaidGuardHidden'.+  { -- | How the guard segment is rendered.     guardMode :: MermaidGuardMode,-    -- | Inline (default, byte-identical) or multiline @<br/>@ layout-    --     of an edge label's segments. Defaults to 'MermaidLabelInline'.+    -- | How the register-update segment is rendered.+    updateMode :: MermaidUpdateMode,+    -- | Inline bracketed segments or multiline @<br/>@ layout.     labelLayout :: MermaidLabelLayout,     -- | When @'Just' k@ and an edge writes @n > k@ slots, show the     --     first @k@ then a single @+{n-k} more@ token. 'Nothing' (the@@ -157,14 +165,26 @@     outputLayout :: MermaidOutputLayout   } --- | The default: no summary suffix. @'toMermaid' t@ equals--- @'toMermaidWith' 'defaultMermaidOptions' t@.+-- | The readable primary policy used by every no-options renderer. defaultMermaidOptions :: MermaidOptions defaultMermaidOptions =   MermaidOptions-    { showWrittenSlots = False,-      showGuardSummary = False,-      guardMode = MermaidGuardHidden,+    { guardMode = MermaidGuardPretty,+      updateMode = MermaidUpdatePretty,+      labelLayout = MermaidLabelMultiline,+      maxInlineWrittenSlots = Nothing,+      maxInlineGuardWidth = Nothing,+      outputLayout = MermaidOutputSemicolon+    }++-- | Keiki 0.7's compact topology-only policy. This is intentionally a+-- separate named value rather than an implicit default so callers make the+-- loss of business semantics explicit.+topologyMermaidOptions :: MermaidOptions+topologyMermaidOptions =+  MermaidOptions+    { guardMode = MermaidGuardHidden,+      updateMode = MermaidUpdateHidden,       labelLayout = MermaidLabelInline,       maxInlineWrittenSlots = Nothing,       maxInlineGuardWidth = Nothing,@@ -178,22 +198,27 @@ -- 'show'. The output begins with @stateDiagram-v2@, followed by an -- initial-state line (@[*] --> <initial>@), one line per outgoing -- edge of every vertex, and a final-state line (@<vertex> --> [*]@)--- for every vertex where 'isFinal' returns 'True'. Edge labels follow--- the format described by 'edgeLabel'.+-- for every vertex where 'isFinal' returns 'True'. The readable default+-- adds complete update and guard segments on renderer-owned @<br/>@ lines;+-- use 'toTopologyMermaid' for the compact 'edgeLabel' form. toMermaid ::   (Enum s, Bounded s, Show s) =>   SymTransducer (HsPred rs ci) rs s ci co ->   Text toMermaid = toMermaidWith defaultMermaidOptions --- | Like 'toMermaid', but takes 'MermaidOptions' controlling the--- structural edge-summary suffix. @'toMermaidWith' 'defaultMermaidOptions'@--- is byte-identical to 'toMermaid'. With 'showWrittenSlots' and/or--- 'showGuardSummary' enabled, each edge label gains a compact bracketed--- suffix, e.g. @… [w: email; confirmCode; registeredAt; g: PAnd PInCtor PTop]@.------ Only the single-transducer path is annotated; the composite renderers--- ('toMermaidComposite' and relatives) keep the guard-free default.+-- | Render only topology using Keiki 0.7's compact label policy. Use this+-- when guards and register assignments are intentionally out of scope.+toTopologyMermaid ::+  (Enum s, Bounded s, Show s) =>+  SymTransducer (HsPred rs ci) rs s ci co ->+  Text+toTopologyMermaid = toMermaidWith topologyMermaidOptions++-- | Like 'toMermaid', but takes the complete edge-semantics and layout+-- policy explicitly. @'toMermaidWith' 'defaultMermaidOptions'@ is+-- byte-identical to 'toMermaid'; use 'topologyMermaidOptions' for the+-- Keiki 0.7 topology-only form. toMermaidWith ::   (Enum s, Bounded s, Show s) =>   MermaidOptions ->@@ -267,8 +292,8 @@ -- Uses the **flat cross-product** shape: each composite vertex -- @'Composite' a b@ becomes a single Mermaid identifier -- @<show a>_<show b>@. The structure is otherwise identical to--- 'toMermaid' — same initial / final / edge emission rules, same--- 'edgeLabel' format. See EP-31's Decision Log+-- 'toMermaid' — same initial / final / edge emission rules and readable+-- semantics policy. See EP-31's Decision Log -- (@docs/plans/31-mermaid-rendering-for-composite-symtransducers.md@) -- for why the flat shape was chosen over Mermaid's nested-subgraph -- syntax (Shape B in the plan).@@ -282,8 +307,23 @@   ) =>   SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co ->   Text-toMermaidComposite = renderTopology compositeLabel+toMermaidComposite = toMermaidCompositeWith defaultMermaidOptions +-- | Options-aware variant of 'toMermaidComposite'.+toMermaidCompositeWith ::+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2+  ) =>+  MermaidOptions ->+  SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co ->+  Text+toMermaidCompositeWith opts =+  renderTopologyWith opts compositeLabel compositeLabel+ -- | Render a right-associative 3-deep -- @t1 \`'Keiki.Composition.compose'\` (t2 \`'Keiki.Composition.compose'\` t3)@ -- composite (vertex type @'Composite' s1 ('Composite' s2 s3)@) to a@@ -292,8 +332,8 @@ -- Each composite vertex becomes a single Mermaid identifier -- @\<show s1\>_\<show s2\>_\<show s3\>@ via 'compose3Label'. The -- structure is otherwise identical to 'toMermaid' /--- 'toMermaidComposite' — same initial / final / edge emission rules,--- same 'edgeLabel' format. See EP-35's plan+-- 'toMermaidComposite' — same initial / final / edge emission rules and+-- readable semantics policy. See EP-35's plan -- (@docs/plans/35-mermaid-renderer-for-right-associative-3-deep-compose-composites.md@) -- for the rationale and for the comparison against the -- nested-subgraph variant 'toMermaidCompose3Nested'.@@ -316,8 +356,32 @@     ci     co ->   Text-toMermaidCompose3 = renderTopology compose3Label+toMermaidCompose3 = toMermaidCompose3With defaultMermaidOptions +-- | Options-aware variant of 'toMermaidCompose3'.+toMermaidCompose3With ::+  forall rs s1 s2 s3 ci co.+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2,+    Enum s3,+    Bounded s3,+    Show s3+  ) =>+  MermaidOptions ->+  SymTransducer+    (HsPred rs ci)+    rs+    (Composite s1 (Composite s2 s3))+    ci+    co ->+  Text+toMermaidCompose3With opts =+  renderTopologyWith opts compose3Label compose3Label+ -- | Render a composite 'SymTransducer' (a 'Keiki.Composition.compose' -- result, vertex type @'Composite' s1 s2@) using the **nested-subgraph** -- shape (Shape B): each outer @s1@ vertex hosts a@@ -359,7 +423,23 @@   ) =>   SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co ->   Text-toMermaidCompositeNested t =+toMermaidCompositeNested =+  toMermaidCompositeNestedWith defaultMermaidOptions++-- | Options-aware variant of 'toMermaidCompositeNested'.+toMermaidCompositeNestedWith ::+  forall rs s1 s2 ci co.+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2+  ) =>+  MermaidOptions ->+  SymTransducer (HsPred rs ci) rs (Composite s1 s2) ci co ->+  Text+toMermaidCompositeNestedWith opts t =   let outers = [minBound .. maxBound] :: [s1]       inners = [minBound .. maxBound] :: [s2]       composites = [minBound .. maxBound] :: [Composite s1 s2]@@ -390,7 +470,7 @@             <> arrow             <> compositeLabel (target e)             <> colon-            <> edgeLabel e+            <> edgeLabelWith opts e         | s <- composites,           e <- edgesOut t s         ]@@ -448,7 +528,31 @@     ci     co ->   Text-toMermaidCompose3Nested t =+toMermaidCompose3Nested =+  toMermaidCompose3NestedWith defaultMermaidOptions++-- | Options-aware variant of 'toMermaidCompose3Nested'.+toMermaidCompose3NestedWith ::+  forall rs s1 s2 s3 ci co.+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2,+    Enum s3,+    Bounded s3,+    Show s3+  ) =>+  MermaidOptions ->+  SymTransducer+    (HsPred rs ci)+    rs+    (Composite s1 (Composite s2 s3))+    ci+    co ->+  Text+toMermaidCompose3NestedWith opts t =   let outers = [minBound .. maxBound] :: [s1]       inners = [minBound .. maxBound] :: [Composite s2 s3]       composites = [minBound .. maxBound] :: [Composite s1 (Composite s2 s3)]@@ -479,7 +583,7 @@             <> arrow             <> compose3Label (target e)             <> colon-            <> edgeLabel e+            <> edgeLabelWith opts e         | s <- composites,           e <- edgesOut t s         ]@@ -516,10 +620,11 @@ -- 'toMermaidAlternativeWith' to override (e.g. for domain-specific -- naming such as @EmailArm@ / @PingerArm@). ----- Edge labels are the standard @\<input ctor\> / \<output ctor\>@ format--- ('edgeLabel'). Because 'Keiki.Composition' lifters preserve--- @icName@ and @wcName@ verbatim, the label reads naturally even--- though the runtime input is @'Left' …@ / @'Right' …@.+-- Edge labels use the same readable guard/update policy as 'toMermaid'.+-- Because 'Keiki.Composition' lifters preserve @icName@ and @wcName@+-- verbatim, the base label reads naturally even though the runtime input is+-- @'Left' …@ / @'Right' …@. Use 'toMermaidAlternativeWithOptions' with+-- 'topologyMermaidOptions' for the historical compact form. -- -- See @docs/plans/33-shape-aware-mermaid-renderers-for-alternative-and-feedback1-composites.md@ -- for the full design record.@@ -535,7 +640,10 @@   SymTransducer (HsPred rs2 ci2) rs2 s2 ci2 co2 ->   Text toMermaidAlternative =-  toMermaidAlternativeWith (T.pack "LeftArm") (T.pack "RightArm")+  toMermaidAlternativeWithOptions+    defaultMermaidOptions+    (T.pack "LeftArm")+    (T.pack "RightArm")  -- | The arm-name-overridable variant of 'toMermaidAlternative'. The -- two 'Text' arguments name the left and right @state … { … }@@@ -560,7 +668,27 @@   SymTransducer (HsPred rs1 ci1) rs1 s1 ci1 co1 ->   SymTransducer (HsPred rs2 ci2) rs2 s2 ci2 co2 ->   Text-toMermaidAlternativeWith leftName rightName t1 t2 =+toMermaidAlternativeWith =+  toMermaidAlternativeWithOptions defaultMermaidOptions++-- | Options-aware, arm-name-overridable variant of+-- 'toMermaidAlternative'.+toMermaidAlternativeWithOptions ::+  forall rs1 rs2 s1 s2 ci1 ci2 co1 co2.+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2+  ) =>+  MermaidOptions ->+  Text ->+  Text ->+  SymTransducer (HsPred rs1 ci1) rs1 s1 ci1 co1 ->+  SymTransducer (HsPred rs2 ci2) rs2 s2 ci2 co2 ->+  Text+toMermaidAlternativeWithOptions opts leftName rightName t1 t2 =   let ind = T.pack "    "       ind2 = T.pack "        "       arrow = T.pack " --> "@@ -587,7 +715,7 @@                    <> arrow                    <> vertexLabel (target e)                    <> colon-                   <> edgeLabel e+                   <> edgeLabelWith opts e                | s <- [minBound .. maxBound],                  e <- edgesOut t s                ]@@ -645,8 +773,28 @@   SymTransducer (HsPred rs1 ci) rs1 s1 ci co ->   SymTransducer (HsPred rs2 co) rs2 s2 co ci ->   Text-toMermaidFeedback1 t f = renderTopology feedback1Label (feedback1 t f)+toMermaidFeedback1 = toMermaidFeedback1With defaultMermaidOptions +-- | Options-aware variant of 'toMermaidFeedback1'.+toMermaidFeedback1With ::+  ( Enum s1,+    Bounded s1,+    Show s1,+    Enum s2,+    Bounded s2,+    Show s2,+    WeakenR rs1,+    WeakenR rs2,+    Disjoint (Names rs2) (Names rs1),+    Disjoint (Names rs1) (Names (Append rs2 rs1))+  ) =>+  MermaidOptions ->+  SymTransducer (HsPred rs1 ci) rs1 s1 ci co ->+  SymTransducer (HsPred rs2 co) rs2 s2 co ci ->+  Text+toMermaidFeedback1With opts t f =+  renderTopologyWith opts feedback1Label feedback1Label (feedback1 t f)+ -- | The Mermaid identifier for a 'feedback1' composite vertex. -- @\<show outer\>_\<show policy\>_\<show inner\>@. Like -- 'compositeLabel', joined with underscores so the result still@@ -685,25 +833,9 @@     <> T.pack "_"     <> T.pack (show c) --- | The shared rendering core: walk @[minBound .. maxBound]@, emit--- the @stateDiagram-v2@ header, the initial-state line, one line per--- outgoing edge, and one final-state line per vertex where--- 'isFinal' fires. The vertex-label function is the only piece that--- varies between 'toMermaid' (single transducer) and--- 'toMermaidComposite' (composite); factoring it out keeps the--- rendering logic in one place.-renderTopology ::-  (Enum s, Bounded s) =>-  (s -> Text) ->-  SymTransducer (HsPred rs ci) rs s ci co ->-  Text-renderTopology label = renderTopologyWith defaultMermaidOptions label label---- | The options-aware rendering core. Identical to 'renderTopology'--- except the per-edge line calls 'edgeLabelWith' so the structural--- summary suffix appears when 'MermaidOptions' requests it. With--- 'defaultMermaidOptions' the output is byte-identical to the original--- 'renderTopology', which is what keeps 'toMermaid' guard-free.+-- | The shared options-aware rendering core: walk+-- @[minBound .. maxBound]@ and render every transition with+-- 'edgeLabelWith'. renderTopologyWith ::   (Enum s, Bounded s) =>   MermaidOptions ->@@ -827,7 +959,7 @@   many -> Just (render layout (Prelude.map wcN many))   where     wcN :: OutTerm rs ci co -> Text-    wcN (OPack _ wc _) = T.pack (wcName wc)+    wcN (OPack _ wc _) = escapeSemanticText (T.pack (wcName wc))     render :: MermaidOutputLayout -> [Text] -> Text     render MermaidOutputSemicolon ns       | length ns == 2 = T.intercalate (T.pack "; ") ns@@ -852,16 +984,13 @@   Edge (HsPred rs ci) rs ci co s ->   Text edgeLabelWithLayout layout e =-  let inp = maybe (T.pack "?") id (edgeInputName e)+  let inp = escapeSemanticText (maybe (T.pack "?") id (edgeInputName e))       out = maybe (T.pack "\x03B5") id (edgeOutputNameWith layout e)    in inp <> T.pack " / " <> out --- | The options-aware edge label: 'edgeLabel' plus an optional--- structural suffix @[w: …; g: …]@. When neither flag is set this is--- exactly 'edgeLabel' (no trailing space, no brackets), which is what--- keeps the 'toMermaid' default byte-identical. The written-slots part--- is omitted entirely when the edge writes nothing (an empty @w:@ would--- be noise); the guard part renders the full structural tag walk.+-- | The options-aware edge label. Semantic text is escaped before this+-- function inserts its own @<br/>@ layout separators, so user/domain text+-- cannot create extra label lines. edgeLabelWith ::   MermaidOptions ->   Edge (HsPred rs ci) rs ci co s ->@@ -874,15 +1003,13 @@   -- the output rendering; guard /text/ is produced by   -- 'renderGuardSegment' (EP-61's chokepoint).   let base = edgeLabelWithLayout (outputLayout opts) e-      ws = if showWrittenSlots opts then writtenSlots u else []-      wPart =-        case truncateSlots (maxInlineWrittenSlots opts) ws of-          [] -> []-          xs -> [T.pack "w: " <> T.intercalate (T.pack "; ") xs]+      uPart = case renderUpdateSegment opts u of+        Just t -> [t]+        Nothing -> []       gPart = case renderGuardSegment opts g of         Just t -> [T.pack "g: " <> truncateGuard (maxInlineGuardWidth opts) t]         Nothing -> []-      parts = wPart ++ gPart+      parts = uPart ++ gPart    in case labelLayout opts of         MermaidLabelInline ->           if null parts@@ -920,28 +1047,61 @@ writtenSlots (USet ix _) = [T.pack (indexNName ix)] writtenSlots (UCombine a b) = writtenSlots a ++ writtenSlots b +-- | Produce the update segment text for an edge. Pretty mode retains the+-- complete right-hand expressions; written-slot mode is the compact legacy+-- summary and may use its dedicated truncation option.+renderUpdateSegment :: MermaidOptions -> Update rs w ci -> Maybe Text+renderUpdateSegment opts u =+  case updateMode opts of+    MermaidUpdateHidden -> Nothing+    MermaidUpdateWrittenSlots ->+      case truncateSlots+        (maxInlineWrittenSlots opts)+        (Prelude.map escapeSemanticText (writtenSlots u)) of+        [] -> Nothing+        xs -> Just (T.pack "w: " <> T.intercalate (T.pack "; ") xs)+    MermaidUpdatePretty ->+      Just (T.pack "u: " <> escapeSemanticText (prettyUpdate u))+ -- | Produce the guard segment text for an edge, or 'Nothing' when no--- guard segment should appear. The effective mode reconciles the legacy--- 'showGuardSummary' flag with the new 'guardMode': an explicit--- 'guardMode' (anything other than 'MermaidGuardHidden') wins; otherwise--- 'showGuardSummary' is honoured as the legacy spelling of--- 'MermaidGuardStructuralSummary'.------ This is the single chokepoint for guard /text/ production. A sibling--- renderer that changes how edge-label /segments are laid out/ (inline--- vs. multiline) wraps the assembly of segments and leaves this--- function's text untouched.+-- guard segment should appear. Semantic text is escaped here, before label+-- layout is assembled. renderGuardSegment :: MermaidOptions -> HsPred rs ci -> Maybe Text renderGuardSegment opts g =-  case effectiveMode of+  escapeSemanticText <$> case guardMode opts of     MermaidGuardHidden -> Nothing     MermaidGuardStructuralSummary -> Just (guardSummary g)     MermaidGuardPretty -> Just (prettyPred g)++-- | Escape domain-derived label text exactly once into a visible,+-- non-structural alphabet. The documentation backend decodes XML entities+-- before parsing Mermaid and also turns both @<br>@ spellings and literal+-- @\\n@ into line breaks, so entity encoding alone cannot isolate semantic+-- text. Full-width punctuation keeps the value recognizable while preventing+-- parser control. XML entities retain punctuation that the backend can safely+-- decode, full-width angle brackets keep break-tag-like input inert, and+-- control pictures make raw line endings visible rather than structural.+-- Renderer-owned ASCII @<br/>@ is inserted only after this pass. The known+-- opaque marker @<lit>@ is encoded as entities because it cannot be a break+-- tag and should remain visually exact.+escapeSemanticText :: Text -> Text+escapeSemanticText =+  T.intercalate (T.pack "&lt;lit&gt;")+    . Prelude.map (T.concatMap escapeChar)+    . T.splitOn (T.pack "<lit>")   where-    effectiveMode-      | guardMode opts /= MermaidGuardHidden = guardMode opts-      | showGuardSummary opts = MermaidGuardStructuralSummary-      | otherwise = MermaidGuardHidden+    escapeChar '\r' = T.pack "\x240D"+    escapeChar '\n' = T.pack "\x240A"+    escapeChar '\\' = T.pack "\xFF3C"+    escapeChar '\'' = T.pack "&apos;"+    escapeChar '"' = T.pack "&quot;"+    escapeChar '&' = T.pack "&amp;"+    escapeChar '<' = T.pack "\xFF1C"+    escapeChar '>' = T.pack "\xFF1E"+    escapeChar '|' = T.pack "&#124;"+    escapeChar '{' = T.pack "&#123;"+    escapeChar '}' = T.pack "&#125;"+    escapeChar c = T.singleton c  -- | A structural, total summary of a guard predicate: its constructor -- tags in left-to-right (prefix) order, with 'PCmp' carrying its 'Cmp'
src/Keiki/Render/Pretty.hs view
@@ -2,14 +2,14 @@  -- | Pure, domain-readable pretty-printer for keiki's predicate, term, -- and update syntax trees ('HsPred', 'Term', 'Update'). Produces--- 'Data.Text.Text'. No solver, no IO. Shared by the Mermaid topology+-- 'Data.Text.Text'. No solver, no IO. Shared by the Mermaid behavioral -- renderer ('Keiki.Render.Mermaid') and the sibling edge-inspector / -- multiline-label renderers. ----- Two things are provably unprintable and are marked, not dropped:--- applied opaque Haskell functions render as @<fn>(...)@; literal--- values render as @<lit>@ (a 'TLit' carries an unconstrained type--- with no 'Show').+-- Two things are deliberately marked rather than invented: applied opaque+-- Haskell functions render as @<fn>(...)@, and explicit display-opaque literal+-- values render as @<lit>@. Ordinary 'TLit' values use their retained 'Show'+-- evidence. module Keiki.Render.Pretty   ( indexName,     prettyTerm,@@ -44,11 +44,12 @@  -- | Render a 'Term' as domain-readable 'Text'. Register reads render by -- slot name, input-field reads as @ctor.field@, arithmetic structurally--- with @+ - *@. Opaque applied functions render @<fn>(...)@; literal--- values render @<lit>@ (a 'TLit' carries an unconstrained type with no--- 'Show').+-- with @+ - *@. Opaque applied functions render @<fn>(...)@. Ordinary+-- literals use their value's 'Show' instance; explicit opaque literals render+-- as @<lit>@. prettyTerm :: Term rs ci ifs r -> Text-prettyTerm (TLit _) = T.pack "<lit>"+prettyTerm (TLit value) = T.pack (show value)+prettyTerm (TOpaqueLit _) = T.pack "<lit>" prettyTerm (TReg ix) = T.pack (indexName ix) prettyTerm (TInpCtorField ic ix) =   T.pack (icName ic) <> T.pack "." <> T.pack (indexName ix)
src/Keiki/Symbolic.hs view
@@ -576,7 +576,7 @@ -- | Translate a 'Term rs ci r' to an SBV expression of the carrier's -- representation type. Requires 'Sym' evidence for @r@. ----- The translation is /structural/ for 'TLit', 'TReg',+-- The translation is /structural/ for 'TLit', 'TOpaqueLit', 'TReg', -- 'TInpCtorField', and (since EP-43) 'TArith': a 'TArith' over a type -- whose 'SymRep' is SBV-numeric (a 'discoverSymNum' hit) emits a real -- @+@ \/ @-@ \/ @*@ over the translated operands, so a guard over a@@ -624,6 +624,7 @@   Term rs ci ifs r ->   SBV.Symbolic (SBV.SBV (SymRep r)) translateTermSym _env (TLit r) = pure (symLit r)+translateTermSym _env (TOpaqueLit r) = pure (symLit r) translateTermSym env (TReg ix) =   memoFree @r env (RegVar (indexName ix)) translateTermSym env (TInpCtorField ic ix) =@@ -1045,6 +1046,7 @@   Term rs ci ifs r ->   [TranslationReportEvent] termReportEvents _root _context (TLit _) = []+termReportEvents _root _context (TOpaqueLit _) = [] termReportEvents _root _context (TReg ix) =   [DirectOwnerReadEvent (registerBaseDescriptor @r ix)] termReportEvents _root _context (TInpCtorField ic ix) =
test/Keiki/CompositionAlignmentSpec.hs view
@@ -1,12 +1,14 @@ module Keiki.CompositionAlignmentSpec (spec) where  import Data.Proxy (Proxy (..))+import Data.Text qualified as T import Keiki.Composition import Keiki.Core import Keiki.FieldProjSpec qualified as FieldProj import Keiki.Fixtures.ComposeStateful import Keiki.Fixtures.CounterPipeline import Keiki.Profunctor (rmapCo)+import Keiki.Render.Pretty (prettyPred, prettyTerm) import Test.Hspec  type Payload1 = '[ '("payload", Int)]@@ -218,6 +220,7 @@         inputTerm = TInpCtorField projectionSourceCtor #doc         passThrough = projectionSource inputTerm         literalOwner = projectionSource (TLit matchingDoc)+        opaqueOwner = projectionSource (opaqueLit matchingDoc)         computedOwner = projectionSource (TApp1 id inputTerm)      it "preserves a stable input-field owner through checked composition" $ do@@ -226,15 +229,39 @@         Left warnings -> expectationFailure ("stable projection warned: " <> show warnings)         Right pipeline -> opaqueGuardWarnings pipeline `shouldBe` [] -    it "constant-folds a literal owner without introducing opacity" $ do-      let pipeline = compose literalOwner projectionSink-      opaqueGuardWarnings pipeline `shouldBe` []-      case stepEither-        pipeline-        (initial pipeline, initialRegs pipeline)-        (ProjectionSourceCmd (FieldProj.DocInfo "ignored" "" [])) of-        Left failure -> expectationFailure ("literal-folded pipeline failed: " <> show failure)-        Right _ -> pure ()+    it "constant-folds a literal owner with display opacity but exact semantics" $ do+      case composeChecked literalOwner projectionSink of+        Left warnings -> expectationFailure ("literal fold failed checked composition: " <> show warnings)+        Right pipeline -> do+          opaqueGuardWarnings pipeline `shouldBe` []+          case edgesOut pipeline (initial pipeline) of+            [edge] ->+              prettyPred (guard edge)+                `shouldSatisfy` T.isInfixOf (T.pack "<lit>")+            _ -> expectationFailure "literal-folded pipeline no longer has one edge"+          case stepEither+            pipeline+            (initial pipeline, initialRegs pipeline)+            (ProjectionSourceCmd (FieldProj.DocInfo "ignored" "" [])) of+            Left failure -> expectationFailure ("literal-folded pipeline failed: " <> show failure)+            Right _ -> pure ()++    it "preserves literal display evidence through positional substitution" $ do+      let midRead = TInpCtorField projectionMidCtor #doc+      case edgesOut literalOwner (initial literalOwner) of+        [Edge {output = [out]}] -> do+          let substituted =+                substTerm @'[] @'[] midRead out ::+                  Term '[] ProjectionSourceCmd ProjectionSourceFields FieldProj.DocInfo+          prettyTerm substituted `shouldBe` T.pack (show matchingDoc)+        _ -> expectationFailure "readable literal source no longer has one output"+      case edgesOut opaqueOwner (initial opaqueOwner) of+        [Edge {output = [out]}] -> do+          let substituted =+                substTerm @'[] @'[] midRead out ::+                  Term '[] ProjectionSourceCmd ProjectionSourceFields FieldProj.DocInfo+          prettyTerm substituted `shouldBe` T.pack "<lit>"+        _ -> expectationFailure "opaque literal source no longer has one output"      it "keeps raw composition forward-correct but rejects a computed owner at the checked boundary" $ do       let pipeline = compose computedOwner projectionSink
test/Keiki/CoreSpec.hs view
@@ -8,6 +8,142 @@ -- | A two-constructor input symbol used by the 'TInpCtorField' tests. data TinyCmd = TinyFoo Int Int | TinyBar Int deriving (Eq, Show) +-- EP-84 guarantee-ledger fixtures. 'NoShowValue' proves the term language can+-- still carry a value with no display dictionary. 'ThrowingShowValue' carries+-- a dictionary whose method is bottom, so execution and replay tests detect+-- any accidental renderer observation on a semantic path.+data NoShowValue = NoShowValue Int deriving (Eq)++data NoShowEvent = NoShowEvent NoShowValue deriving (Eq)++data ThrowingShowValue = ThrowingShowValue Int deriving (Eq)++instance Show ThrowingShowValue where+  show _ = error "EP-84: semantic execution forced Show"++data LiteralLedgerEvent = LiteralLedgerEvent ThrowingShowValue deriving (Eq)++data LiteralLedgerVertex = LiteralLedgerStart | LiteralLedgerDone+  deriving stock (Eq, Ord, Show, Enum, Bounded)++type LiteralLedgerRegs = '[ '("payload", ThrowingShowValue)]++literalLedgerInCtor :: InCtor () '[]+literalLedgerInCtor =+  InCtor+    { icName = "LiteralLedgerCommand",+      icMatch = \() -> Just RNil,+      icBuild = \RNil -> ()+    }++literalLedgerWireCtor :: WireCtor LiteralLedgerEvent (ThrowingShowValue, ())+literalLedgerWireCtor =+  WireCtor+    { wcName = "LiteralLedgerEvent",+      wcMatch = \(LiteralLedgerEvent value) -> Just (value, ()),+      wcBuild = \(value, ()) -> LiteralLedgerEvent value+    }++literalLedgerValue :: ThrowingShowValue+literalLedgerValue = ThrowingShowValue 41++literalLedgerTransducerWith ::+  Term LiteralLedgerRegs () '[] ThrowingShowValue ->+  SymTransducer+    (HsPred LiteralLedgerRegs ())+    LiteralLedgerRegs+    LiteralLedgerVertex+    ()+    LiteralLedgerEvent+literalLedgerTransducerWith literalTerm =+  SymTransducer+    { edgesOut = \case+        LiteralLedgerStart ->+          [ Edge+              { guard = PEq literalTerm literalTerm,+                update =+                  USet+                    (#payload :: IndexN "payload" LiteralLedgerRegs ThrowingShowValue)+                    literalTerm,+                output =+                  [ pack+                      literalLedgerInCtor+                      literalLedgerWireCtor+                      (OFCons literalTerm OFNil)+                  ],+                target = LiteralLedgerDone,+                mode = Live+              }+          ]+        LiteralLedgerDone -> [],+      initial = LiteralLedgerStart,+      initialRegs =+        RCons (Proxy @"payload") (ThrowingShowValue 0) RNil,+      isFinal = (== LiteralLedgerDone)+    }++literalLedgerTransducer ::+  SymTransducer+    (HsPred LiteralLedgerRegs ())+    LiteralLedgerRegs+    LiteralLedgerVertex+    ()+    LiteralLedgerEvent+literalLedgerTransducer = literalLedgerTransducerWith (lit literalLedgerValue)++opaqueLiteralLedgerTransducer ::+  SymTransducer+    (HsPred LiteralLedgerRegs ())+    LiteralLedgerRegs+    LiteralLedgerVertex+    ()+    LiteralLedgerEvent+opaqueLiteralLedgerTransducer =+  literalLedgerTransducerWith (opaqueLit literalLedgerValue)++type NoShowRegs = '[ '("payload", NoShowValue)]++noShowWireCtor :: WireCtor NoShowEvent (NoShowValue, ())+noShowWireCtor =+  WireCtor+    { wcName = "NoShowEvent",+      wcMatch = \(NoShowEvent value) -> Just (value, ()),+      wcBuild = \(value, ()) -> NoShowEvent value+    }++noShowTransducer ::+  SymTransducer+    (HsPred NoShowRegs ())+    NoShowRegs+    LiteralLedgerVertex+    ()+    NoShowEvent+noShowTransducer =+  SymTransducer+    { edgesOut = \case+        LiteralLedgerStart ->+          [ Edge+              { guard = PTop,+                update =+                  USet+                    (#payload :: IndexN "payload" NoShowRegs NoShowValue)+                    (opaqueLit (NoShowValue 7)),+                output =+                  [ pack+                      literalLedgerInCtor+                      noShowWireCtor+                      (OFCons (opaqueLit (NoShowValue 7)) OFNil)+                  ],+                target = LiteralLedgerDone,+                mode = Live+              }+          ]+        LiteralLedgerDone -> [],+      initial = LiteralLedgerStart,+      initialRegs = RCons (Proxy @"payload") (NoShowValue 0) RNil,+      isFinal = (== LiteralLedgerDone)+    }+ type SnapshotRegs =   '[ '("x", Int),      '("y", Int)@@ -85,6 +221,88 @@  spec :: Spec spec = do+  describe "EP-84 literal semantic guarantee ledger" $ do+    it "evaluates a display-opaque value with no Show instance" $+      case evalTerm (opaqueLit (NoShowValue 7)) RNil () of+        NoShowValue 7 -> pure ()+        _ -> expectationFailure "literal evaluation changed the no-Show value"++    it "updates, emits, and replays a value with no Show instance" $ do+      case step+        noShowTransducer+        (initial noShowTransducer, initialRegs noShowTransducer)+        () of+        Just+          ( LiteralLedgerDone,+            RCons _ (NoShowValue 7) RNil,+            [NoShowEvent (NoShowValue 7)]+            ) -> pure ()+        _ -> expectationFailure "no-Show literal step changed its value"+      case reconstitute noShowTransducer [NoShowEvent (NoShowValue 7)] of+        Just (LiteralLedgerDone, RCons _ (NoShowValue 7) RNil) -> pure ()+        _ -> expectationFailure "no-Show literal replay changed its value"++    it "evaluates, compares, updates, and emits without forcing Show" $ do+      evalPred+        (PEq (lit literalLedgerValue) (lit literalLedgerValue) :: HsPred '[] ())+        RNil+        ()+        `shouldBe` True+      case step+        literalLedgerTransducer+        (initial literalLedgerTransducer, initialRegs literalLedgerTransducer)+        () of+        Just+          ( LiteralLedgerDone,+            RCons _ (ThrowingShowValue 41) RNil,+            [LiteralLedgerEvent (ThrowingShowValue 41)]+            ) -> pure ()+        _ -> expectationFailure "literal step changed state, registers, or output"++    it "solves and replays the emitted literal without forcing Show" $ do+      let observed = LiteralLedgerEvent literalLedgerValue+      case edgesOut literalLedgerTransducer LiteralLedgerStart of+        [Edge {output = [out]}] -> solveOutput out (initialRegs literalLedgerTransducer) observed `shouldBe` Just ()+        _ -> expectationFailure "literal ledger fixture no longer has one output"+      case reconstitute literalLedgerTransducer [observed] of+        Just+          ( LiteralLedgerDone,+            RCons _ (ThrowingShowValue 41) RNil+            ) -> pure ()+        _ -> expectationFailure "literal replay changed state or registers"++    it "retains the clean validation classification" $+      validateTransducer defaultValidationOptions literalLedgerTransducer+        `shouldBe` []++    it "gives readable and display-opaque literals identical step and replay results" $ do+      let readableStep =+            step+              literalLedgerTransducer+              (initial literalLedgerTransducer, initialRegs literalLedgerTransducer)+              ()+          opaqueStep =+            step+              opaqueLiteralLedgerTransducer+              (initial opaqueLiteralLedgerTransducer, initialRegs opaqueLiteralLedgerTransducer)+              ()+      case (readableStep, opaqueStep) of+        ( Just (LiteralLedgerDone, RCons _ (ThrowingShowValue 41) RNil, [LiteralLedgerEvent (ThrowingShowValue 41)]),+          Just (LiteralLedgerDone, RCons _ (ThrowingShowValue 41) RNil, [LiteralLedgerEvent (ThrowingShowValue 41)])+          ) -> pure ()+        _ -> expectationFailure "readable and opaque literal steps diverged"+      let observed = [LiteralLedgerEvent literalLedgerValue]+      case ( reconstitute literalLedgerTransducer observed,+             reconstitute opaqueLiteralLedgerTransducer observed+           ) of+        ( Just (LiteralLedgerDone, RCons _ (ThrowingShowValue 41) RNil),+          Just (LiteralLedgerDone, RCons _ (ThrowingShowValue 41) RNil)+          ) -> pure ()+        _ -> expectationFailure "readable and opaque literal replay diverged"++    it "does not classify display opacity as an opaque semantic guard" $+      opaqueGuardWarnings opaqueLiteralLedgerTransducer `shouldBe` []+   describe "evalTerm" $ do     it "evaluates TLit" $       evalTerm (TLit (42 :: Int)) RNil () `shouldBe` 42
test/Keiki/Render/MermaidSpec.hs view
@@ -30,6 +30,7 @@ --     nested) and the synthetic three-toy fixture. module Keiki.Render.MermaidSpec (spec) where +import Data.Proxy (Proxy (..)) import Data.Text (Text) import Data.Text qualified as T import Keiki.Composition (Composite, compose)@@ -41,13 +42,16 @@     EdgeMode (..),     HsPred (..),     InCtor (..),+    IndexN (..),     OutFields (..),     RegFile (..),     SymTransducer (..),     Update (..),     WireCtor (..),+    opaqueLit,     pack,   )+import Keiki.Fixtures.CounterPipeline (stageB) import Keiki.Fixtures.EmailDelivery (emailDelivery) import Keiki.Fixtures.UserRegistration (Vertex (..), userReg) import Keiki.Render.Mermaid@@ -60,57 +64,112 @@     MermaidSection (..),     MermaidSectionKind (..),     MermaidStateLabels (..),+    MermaidUpdateMode (..),     defaultMermaidAtlasOptions,     defaultMermaidOptions,     duplicateStateIds,     toMermaid,     toMermaidAlternative,+    toMermaidAlternativeWithOptions,     toMermaidAtlas,     toMermaidAtlasWith,     toMermaidCompose3,     toMermaidCompose3Nested,+    toMermaidCompose3NestedWith,+    toMermaidCompose3With,     toMermaidComposite,     toMermaidCompositeNested,+    toMermaidCompositeNestedWith,+    toMermaidCompositeWith,     toMermaidFeedback1,+    toMermaidFeedback1With,     toMermaidWith,     toMermaidWithLabels,+    toTopologyMermaid,+    topologyMermaidOptions,     vertexLabel,   )+import Keiki.Render.Validate+  ( MermaidValidationOptions (..),+    defaultMermaidValidationOptions,+    validateMermaidDiagram,+  ) import Test.Hspec  spec :: Spec spec = do-  describe "toMermaid (single SymTransducer)" $+  describe "toMermaid (readable single SymTransducer)" $     it "renders userReg to the canonical stateDiagram-v2 block" $-      toMermaid userReg `shouldBe` userRegCanonical+      toMermaid userReg `shouldBe` userRegReadableCanonical -  describe "toMermaidComposite (composite SymTransducer)" $-    it "renders the AlertSource ⨾ EmailDelivery pipeline" $+  describe "toMermaid (complete business expressions, EP-84)" $+    it "shows update right-hand expressions and ordinary literal values" $ do+      let diagram = toMermaid stageB+      diagram `shouldSatisfy` T.isInfixOf (T.pack "u: regB := (regB + MsgB.payload)")+      diagram `shouldSatisfy` T.isInfixOf (T.pack "g: (MsgB &amp;&amp; regB >= 0)")+      diagram `shouldNotSatisfy` T.isInfixOf (T.pack "<lit>")++  describe "toMermaid (explicit display opacity, EP-84)" $+    it "shows opaqueLit as <lit> while topology mode hides the expression" $ do+      toMermaid opaqueValue+        `shouldSatisfy` T.isInfixOf (T.pack "u: secret := &lt;lit&gt;")+      toTopologyMermaid opaqueValue+        `shouldNotSatisfy` T.isInfixOf (T.pack "lit")++  describe "toTopologyMermaid (0.7 compatibility)" $+    it "renders userReg to the prior topology-only block exactly" $+      toTopologyMermaid userReg `shouldBe` userRegCanonical++  describe "toMermaidComposite (composite SymTransducer)" $ do+    it "renders readable semantics for the AlertSource ⨾ EmailDelivery pipeline" $       toMermaidComposite (compose alertSource emailDelivery)+        `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 topology with explicit options" $+      toMermaidCompositeWith topologyMermaidOptions (compose alertSource emailDelivery)         `shouldBe` alertEmailCompositeCanonical -  describe "toMermaidCompositeNested (composite SymTransducer)" $-    it "renders the AlertSource ⨾ EmailDelivery pipeline in nested form" $+  describe "toMermaidCompositeNested (composite SymTransducer)" $ do+    it "renders readable semantics in nested form" $       toMermaidCompositeNested (compose alertSource emailDelivery)+        `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 nested topology with explicit options" $+      toMermaidCompositeNestedWith topologyMermaidOptions (compose alertSource emailDelivery)         `shouldBe` alertEmailCompositeNestedCanonical -  describe "toMermaidAlternative (alternative composite)" $-    it "renders alternative emailDelivery pinger as parallel arms" $+  describe "toMermaidAlternative (alternative composite)" $ do+    it "renders readable semantics for both parallel arms" $       toMermaidAlternative emailDelivery pinger+        `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 parallel-arm topology with explicit options" $+      toMermaidAlternativeWithOptions+        topologyMermaidOptions+        (T.pack "LeftArm")+        (T.pack "RightArm")+        emailDelivery+        pinger         `shouldBe` emailPingerAltCanonical -  describe "toMermaidFeedback1 (feedback1 composite)" $-    it "renders feedback1 toggleAgg togglePolicy as flat 3-deep cross-product" $+  describe "toMermaidFeedback1 (feedback1 composite)" $ do+    it "renders readable feedback semantics" $       toMermaidFeedback1 toggleAgg togglePolicy+        `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 flat feedback topology with explicit options" $+      toMermaidFeedback1With topologyMermaidOptions toggleAgg togglePolicy         `shouldBe` toggleFeedback1Canonical -  describe "toMermaidCompose3 (right-associative 3-deep compose)" $-    it "renders the toy1 ⨾ (toy2 ⨾ toy3) flat block" $-      toMermaidCompose3 toy3deep `shouldBe` toy3deepFlatCanonical+  describe "toMermaidCompose3 (right-associative 3-deep compose)" $ do+    it "renders readable semantics in the flat block" $+      toMermaidCompose3 toy3deep `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 flat topology with explicit options" $+      toMermaidCompose3With topologyMermaidOptions toy3deep+        `shouldBe` toy3deepFlatCanonical -  describe "toMermaidCompose3Nested (right-associative 3-deep compose)" $-    it "renders the toy1 ⨾ (toy2 ⨾ toy3) one-level nested block" $-      toMermaidCompose3Nested toy3deep `shouldBe` toy3deepNestedCanonical+  describe "toMermaidCompose3Nested (right-associative 3-deep compose)" $ do+    it "renders readable semantics in the one-level nested block" $+      toMermaidCompose3Nested toy3deep `shouldSatisfy` hasReadableSemantics+    it "reproduces the 0.7 nested topology with explicit options" $+      toMermaidCompose3NestedWith topologyMermaidOptions toy3deep+        `shouldBe` toy3deepNestedCanonical    -- EP-50 M3: the default must stay byte-identical to today (the   -- guard-free pedagogy in deriving-lifecycle-transitions.md depends on@@ -124,23 +183,27 @@   describe "toMermaidWith (annotated edge summary)" $     it "renders userReg with written-slot and guard-summary suffixes" $       toMermaidWith-        (defaultMermaidOptions {showWrittenSlots = True, showGuardSummary = True})+        ( topologyMermaidOptions+            { updateMode = MermaidUpdateWrittenSlots,+              guardMode = MermaidGuardStructuralSummary+            }+        )         userReg         `shouldBe` userRegAnnotatedCanonical    describe "toMermaidWith (MermaidGuardPretty, EP-61)" $     it "renders userReg guards in domain-readable form" $       toMermaidWith-        (defaultMermaidOptions {guardMode = MermaidGuardPretty})+        (topologyMermaidOptions {guardMode = MermaidGuardPretty})         userReg         `shouldBe` userRegPrettyGuardCanonical    describe "toMermaidWith (multiline label layout, EP-63)" $     it "renders userReg labels with <br/>-separated segments" $       toMermaidWith-        ( defaultMermaidOptions-            { showWrittenSlots = True,-              showGuardSummary = True,+        ( topologyMermaidOptions+            { updateMode = MermaidUpdateWrittenSlots,+              guardMode = MermaidGuardStructuralSummary,               labelLayout = MermaidLabelMultiline             }         )@@ -150,8 +213,8 @@   describe "toMermaidWith (written-slot truncation, EP-63)" $     it "truncates a long written-slot list with +N more" $       toMermaidWith-        ( defaultMermaidOptions-            { showWrittenSlots = True,+        ( topologyMermaidOptions+            { updateMode = MermaidUpdateWrittenSlots,               maxInlineWrittenSlots = Just 2             }         )@@ -161,8 +224,8 @@   describe "toMermaidWith (guard-width truncation, EP-63)" $     it "truncates an over-long guard segment with an ellipsis" $       toMermaidWith-        ( defaultMermaidOptions-            { showGuardSummary = True,+        ( topologyMermaidOptions+            { guardMode = MermaidGuardStructuralSummary,               maxInlineGuardWidth = Just 10             }         )@@ -171,25 +234,36 @@    describe "toMermaidWith (MermaidOutputSemicolon default, EP-63)" $     it "renders multiEvt with the length-based default output layout" $-      toMermaid multiEvt `shouldBe` multiEvtSemicolonCanonical+      toTopologyMermaid multiEvt `shouldBe` multiEvtSemicolonCanonical    describe "toMermaidWith (MermaidOutputMultiline, EP-63)" $     it "renders every multi-event edge one event per line" $       toMermaidWith-        (defaultMermaidOptions {outputLayout = MermaidOutputMultiline})+        (topologyMermaidOptions {outputLayout = MermaidOutputMultiline})         multiEvt         `shouldBe` multiEvtMultilineCanonical    describe "toMermaidWith (MermaidOutputCounted, EP-63)" $     it "renders multi-event edges as an N events count" $       toMermaidWith-        (defaultMermaidOptions {outputLayout = MermaidOutputCounted})+        (topologyMermaidOptions {outputLayout = MermaidOutputCounted})         multiEvt         `shouldBe` multiEvtCountedCanonical +  describe "semantic-label escaping (EP-84)" $ do+    it "escapes every backend control spelling before inserting layout" $+      toMermaid specialChars `shouldBe` specialCharsCanonical+    it "preserves the diagram and transition line counts" $ do+      length (T.lines (toMermaid specialChars)) `shouldBe` 4+      T.count (T.pack " --> ") (toMermaid specialChars) `shouldBe` 3+      T.count (T.pack "<br/>") (toMermaid specialChars) `shouldBe` 2+    it "passes suspicious-character validation without disabling that check" $+      validateMermaidDiagram semanticValidationOptions (toMermaid specialChars)+        `shouldBe` []+   describe "toMermaidWithLabels (stable ASCII ids, spaced display labels, EP-64)" $     it "renders userReg with friendly labels and stable ids" $-      toMermaidWithLabels defaultMermaidOptions userRegLabels userReg+      toMermaidWithLabels topologyMermaidOptions userRegLabels userReg         `shouldBe` userRegLabeledCanonical    describe "toMermaidWithLabels (id == display is byte-identical, EP-64)" $@@ -210,8 +284,8 @@   describe "toMermaidAtlas (multi-diagram document)" $     it "assembles two labelled diagrams into one document" $       toMermaidAtlas-        [ (T.pack "User registration", toMermaid userReg),-          (T.pack "Alert \x2A3E Email", toMermaidComposite (compose alertSource emailDelivery))+        [ (T.pack "User registration", toTopologyMermaid userReg),+          (T.pack "Alert \x2A3E Email", toMermaidCompositeWith topologyMermaidOptions (compose alertSource emailDelivery))         ]         `shouldBe` atlasCanonical @@ -228,19 +302,38 @@             (T.pack "incident-command")             (T.pack "Incident Command")             AggregateDiagram-            (toMermaid userReg),+            (toTopologyMermaid userReg),           MermaidSection             (T.pack "dispatch")             (T.pack "Dispatch")             ProcessManagerDiagram-            (toMermaidComposite (compose alertSource emailDelivery))+            (toMermaidCompositeWith topologyMermaidOptions (compose alertSource emailDelivery))         ]         `shouldBe` typedAtlasCanonical --- | The canonical Mermaid block for @userReg@, mirrored verbatim from--- the aggregate's diagram in @docs/guide/diagrams/user-registration.md@.--- Stored inline (not in an external fixture file) so a formatting change--- requires touching this file alongside the producer change.+hasReadableSemantics :: Text -> Bool+hasReadableSemantics diagram =+  T.pack "<br/>u: " `T.isInfixOf` diagram+    && T.pack "<br/>g: " `T.isInfixOf` diagram++-- | The readable-primary golden. Unlike the Keiki 0.7 topology golden below,+-- every transition includes the complete update and guard expression.+userRegReadableCanonical :: Text+userRegReadableCanonical =+  T.intercalate+    (T.pack "\n")+    [ "stateDiagram-v2",+      "    [*] --> PotentialCustomer",+      "    PotentialCustomer --> RequiresConfirmation : StartRegistration / RegistrationStarted; ConfirmationEmailSent<br/>u: registeredAt := StartRegistration.at, confirmCode := StartRegistration.confirmCode, email := StartRegistration.email, (keep)<br/>g: StartRegistration",+      "    RequiresConfirmation --> Confirmed : ConfirmAccount / AccountConfirmed<br/>u: confirmedAt := ConfirmAccount.at, (keep)<br/>g: (ConfirmAccount &amp;&amp; ConfirmAccount.confirmCode == confirmCode)",+      "    RequiresConfirmation --> RequiresConfirmation : ResendConfirmation / ConfirmationResent<br/>u: registeredAt := ResendConfirmation.at, confirmCode := ResendConfirmation.code, (keep)<br/>g: ResendConfirmation",+      "    RequiresConfirmation --> Deleted : FulfillGDPRRequest / AccountDeleted<br/>u: deletedAt := FulfillGDPRRequest.at, (keep)<br/>g: FulfillGDPRRequest",+      "    Confirmed --> Deleted : FulfillGDPRRequest / AccountDeleted<br/>u: deletedAt := FulfillGDPRRequest.at, (keep)<br/>g: FulfillGDPRRequest",+      "    Deleted --> [*]"+    ]++-- | The Keiki 0.7 topology-only block for @userReg@, retained as the+-- compatibility golden for 'toTopologyMermaid'. userRegCanonical :: Text userRegCanonical =   unlinesNoTrail@@ -259,8 +352,8 @@   where     unlinesNoTrail = T.intercalate (T.pack "\n") --- | EP-50: the canonical block for @userReg@ rendered with both summary--- flags on (@MermaidOptions True True@). Differs from 'userRegCanonical'+-- | EP-50: the canonical block for @userReg@ rendered with written-slot+-- and structural-guard modes. Differs from 'userRegCanonical' -- only by the bracketed @[w: …; g: …]@ suffixes. Captured verbatim from -- the renderer (the slot order is the @UCombine@ nesting order, and each -- guard is the actual 'HsPred' shape @onCmd@ produced — a bare 'PInCtor'@@ -280,8 +373,8 @@     ]  -- | EP-61: the canonical block for @userReg@ rendered with--- @guardMode = MermaidGuardPretty@ and 'showWrittenSlots' left at its--- default 'False', so each label carries only a domain-readable+-- @guardMode = MermaidGuardPretty@ and updates hidden, so each label carries+-- only a domain-readable -- @[g: …]@ segment. Differs from 'userRegAnnotatedCanonical' by -- rendering real names — @ConfirmAccount@, @confirmCode@ — instead of -- the structural constructor-tag walk (@PAnd PInCtor PEq@). Captured@@ -293,14 +386,14 @@     [ "stateDiagram-v2",       "    [*] --> PotentialCustomer",       "    PotentialCustomer --> RequiresConfirmation : StartRegistration / RegistrationStarted; ConfirmationEmailSent [g: StartRegistration]",-      "    RequiresConfirmation --> Confirmed : ConfirmAccount / AccountConfirmed [g: (ConfirmAccount && ConfirmAccount.confirmCode == confirmCode)]",+      "    RequiresConfirmation --> Confirmed : ConfirmAccount / AccountConfirmed [g: (ConfirmAccount &amp;&amp; ConfirmAccount.confirmCode == confirmCode)]",       "    RequiresConfirmation --> RequiresConfirmation : ResendConfirmation / ConfirmationResent [g: ResendConfirmation]",       "    RequiresConfirmation --> Deleted : FulfillGDPRRequest / AccountDeleted [g: FulfillGDPRRequest]",       "    Confirmed --> Deleted : FulfillGDPRRequest / AccountDeleted [g: FulfillGDPRRequest]",       "    Deleted --> [*]"     ] --- | EP-63: @userReg@ rendered with both summary flags on and+-- | EP-63: @userReg@ rendered with written-slot and structural-guard modes and -- @labelLayout = MermaidLabelMultiline@. Same per-edge content as -- 'userRegAnnotatedCanonical', but the bracketed inline suffix is replaced -- by @<br/>@-separated segments: the @command / event@ base on the first@@ -322,7 +415,7 @@       "    Deleted --> [*]"     ] --- | EP-63: @userReg@ rendered with @showWrittenSlots = True@ and+-- | EP-63: @userReg@ rendered with @updateMode = MermaidUpdateWrittenSlots@ and -- @maxInlineWrittenSlots = Just 2@. The only edge writing more than two -- slots is @StartRegistration@ (three slots), which truncates to the first -- two followed by a single @+1 more@ token; every other edge writes two or@@ -341,7 +434,7 @@       "    Deleted --> [*]"     ] --- | EP-63: @userReg@ rendered with @showGuardSummary = True@ and+-- | EP-63: @userReg@ rendered with structural guard summaries and -- @maxInlineGuardWidth = Just 10@. The only guard whose structural text -- exceeds ten characters is @ConfirmAccount@'s @PAnd PInCtor PEq@ (length -- 16), truncated to the first ten characters plus the ellipsis @…@. The@@ -749,6 +842,124 @@       "    T1A_T2A_T3A --> T1B_T2B_T3B : Tick / Tick",       "    T1B_T2B_T3B --> [*]"     ]++-- * Semantic-label escaping fixture (EP-84) -------------------------++type SecretRegs = '[ '("secret", Int)]++data SecretCmd = SetSecret++data SecretEvt = SecretSet++data SecretState = SecretState+  deriving (Eq, Show, Enum, Bounded)++secretInCtor :: InCtor SecretCmd '[]+secretInCtor =+  InCtor+    { icName = "SetSecret",+      icMatch = \SetSecret -> Just RNil,+      icBuild = \RNil -> SetSecret+    }++secretWireCtor :: WireCtor SecretEvt ()+secretWireCtor =+  WireCtor+    { wcName = "SecretSet",+      wcMatch = \SecretSet -> Just (),+      wcBuild = \() -> SecretSet+    }++opaqueValue ::+  SymTransducer+    (HsPred SecretRegs SecretCmd)+    SecretRegs+    SecretState+    SecretCmd+    SecretEvt+opaqueValue =+  SymTransducer+    { edgesOut = \SecretState ->+        [ Edge+            { guard = PInCtor secretInCtor,+              update = USet IZ (opaqueLit (7 :: Int)),+              output = [pack secretInCtor secretWireCtor OFNil],+              target = SecretState,+              mode = Live+            }+        ],+      initial = SecretState,+      initialRegs = RCons (Proxy @"secret") 0 RNil,+      isFinal = const True+    }++data SpecialCmd = SpecialCmd++data SpecialEvt = SpecialEvt++data SpecialState = SpecialStart | SpecialDone+  deriving (Eq, Show, Enum, Bounded)++specialName :: String+specialName =+  "Command \"quote\" 'apostrophe' <br> <br/> <br /> &lt;br&gt; | {value}\r\n\\n"++specialInCtor :: InCtor SpecialCmd '[]+specialInCtor =+  InCtor+    { icName = specialName,+      icMatch = \SpecialCmd -> Just RNil,+      icBuild = \RNil -> SpecialCmd+    }++specialWireCtor :: WireCtor SpecialEvt ()+specialWireCtor =+  WireCtor+    { wcName = "Event",+      wcMatch = \SpecialEvt -> Just (),+      wcBuild = \() -> SpecialEvt+    }++specialChars ::+  SymTransducer+    (HsPred '[] SpecialCmd)+    '[]+    SpecialState+    SpecialCmd+    SpecialEvt+specialChars =+  SymTransducer+    { edgesOut = \case+        SpecialStart ->+          [ Edge+              { guard = PInCtor specialInCtor,+                update = UKeep,+                output = [pack specialInCtor specialWireCtor OFNil],+                target = SpecialDone,+                mode = Live+              }+          ]+        SpecialDone -> [],+      initial = SpecialStart,+      initialRegs = RNil,+      isFinal = (== SpecialDone)+    }++specialCharsCanonical :: Text+specialCharsCanonical =+  T.intercalate+    (T.pack "\n")+    [ "stateDiagram-v2",+      "    [*] --> SpecialStart",+      "    SpecialStart --> SpecialDone : Command &quot;quote&quot; &apos;apostrophe&apos; <br> <br/> <br /> &amp;lt;br&amp;gt; &#124; &#123;value&#125;␍␊\n / Event<br/>u: (keep)<br/>g: Command &quot;quote&quot; &apos;apostrophe&apos; <br> <br/> <br /> &amp;lt;br&amp;gt; &#124; &#123;value&#125;␍␊\n",+      "    SpecialDone --> [*]"+    ]++semanticValidationOptions :: MermaidValidationOptions+semanticValidationOptions =+  defaultMermaidValidationOptions+    { maxLabelLength = Nothing+    }  -- * Multi-event output fixture (EP-63 M2) ----------------------------- 
test/Keiki/Render/PrettySpec.hs view
@@ -15,6 +15,7 @@     Term (..),     Update (..),     inpProj,+    opaqueLit,     regProj,   ) import Keiki.FieldProjSpec qualified as FieldProj@@ -66,8 +67,11 @@     it "renders an input-field read as ctor.field" $       prettyTerm (TInpCtorField inCtorDeposit amountIx :: Term Regs Cmd DepFields Int)         `shouldBe` T.pack "Deposit.amount"-    it "renders a literal opaquely as <lit>" $+    it "renders an ordinary literal from its value" $       prettyTerm (TLit (42 :: Int) :: Term Regs Cmd '[] Int)+        `shouldBe` T.pack "42"+    it "renders an explicit display-opaque literal as <lit>" $+      prettyTerm (opaqueLit (42 :: Int) :: Term Regs Cmd '[] Int)         `shouldBe` T.pack "<lit>"     it "renders TApp1 as <fn>(arg)" $       prettyTerm (TApp1 (+ (1 :: Int)) (TReg balanceIx) :: Term Regs Cmd '[] Int)@@ -105,9 +109,9 @@     it "renders PInCtor as the constructor name" $       prettyPred (PInCtor inCtorDeposit :: HsPred Regs Cmd)         `shouldBe` T.pack "Deposit"-    it "renders PEq structurally with <lit> on the literal side" $+    it "renders PEq structurally with the literal value" $       prettyPred (PEq (TReg balanceIx) (TLit (0 :: Int)) :: HsPred Regs Cmd)-        `shouldBe` T.pack "balance == <lit>"+        `shouldBe` T.pack "balance == 0"     it "renders each PCmp direction" $ do       prettyPred (PCmp CmpLt (TReg balanceIx) (TReg limitIx) :: HsPred Regs Cmd)         `shouldBe` T.pack "balance < limit"@@ -127,14 +131,14 @@             ) ::             HsPred Regs Cmd         )-        `shouldBe` T.pack "(Deposit && (balance >= <lit> || !(limit == <lit>)))"+        `shouldBe` T.pack "(Deposit && (balance >= 0 || !(limit == 0)))"    describe "prettyUpdate" $ do     it "renders UKeep" $       prettyUpdate (UKeep :: Update Regs '[] Cmd) `shouldBe` T.pack "(keep)"     it "renders USet as slot := term" $       prettyUpdate (USet balanceN (TLit (0 :: Int)) :: Update Regs '["balance"] Cmd)-        `shouldBe` T.pack "balance := <lit>"+        `shouldBe` T.pack "balance := 0"     it "renders UCombine comma-separated" $       prettyUpdate         ( UCombine@@ -142,4 +146,4 @@             (USet balanceN (TLit (1 :: Int))) ::             Update Regs '["balance", "balance"] Cmd         )-        `shouldBe` T.pack "balance := limit, balance := <lit>"+        `shouldBe` T.pack "balance := limit, balance := 1"
test/Keiki/SymbolicSpec.hs view
@@ -1,5 +1,6 @@ module Keiki.SymbolicSpec (spec) where +import Control.Monad (forM_) import Data.Int (Int32, Int64) import Data.Kind (Type) import Data.Maybe (isJust, isNothing)@@ -476,6 +477,48 @@       predicateTranslationExact unsatisfiable `shouldBe` True       verifyPredicate satisfiable `shouldReturn` VerifiedSatisfiable       verifyPredicate unsatisfiable `shouldReturn` VerifiedUnsatisfiable++    it "keeps every readable/opaque literal equality pairing exact" $ do+      let readable value = lit value :: Term '[] () '[] Bool+          hidden value = opaqueLit value :: Term '[] () '[] Bool+          equalPairs =+            [ (readable True, readable True),+              (readable True, hidden True),+              (hidden True, readable True),+              (hidden True, hidden True)+            ]+          unequalPairs =+            [ (readable True, readable False),+              (readable True, hidden False),+              (hidden True, readable False),+              (hidden True, hidden False)+            ]+      forM_ equalPairs $ \(left, right) -> do+        let predicate = PEq left right :: HsPred '[] ()+        predicateTranslationReport predicate `shouldBe` ExactTranslation+        verifyPredicate predicate `shouldReturn` VerifiedSatisfiable+        detail <- verifyPredicateDetailed predicate+        detail `shouldSatisfy` \case+          PredicateSatisfiable ExactTranslation [] -> True+          _ -> False+      forM_ unequalPairs $ \(left, right) -> do+        let predicate = PEq left right :: HsPred '[] ()+        predicateTranslationReport predicate `shouldBe` ExactTranslation+        verifyPredicate predicate `shouldReturn` VerifiedUnsatisfiable++    it "keeps every readable/opaque literal ordering pairing exact" $ do+      let readable value = lit value :: Term '[] () '[] Int+          hidden value = opaqueLit value :: Term '[] () '[] Int+          pairings =+            [ (readable 3, readable 5),+              (readable 3, hidden 5),+              (hidden 3, readable 5),+              (hidden 3, hidden 5)+            ]+      forM_ pairings $ \(left, right) -> do+        let predicate = PCmp CmpLt left right :: HsPred '[] ()+        predicateTranslationReport predicate `shouldBe` ExactTranslation+        verifyPredicate predicate `shouldReturn` VerifiedSatisfiable      it "verifies supported structural arithmetic" $ do       let arithmetic =
test/Keiki/ValidationSpec.hs view
@@ -1,5 +1,6 @@ module Keiki.ValidationSpec (spec) where +import Control.Monad (forM_) import Data.List (isInfixOf) import Data.Proxy (Proxy (..)) import Data.Word (Word8)@@ -675,6 +676,23 @@     it "uses a mentioned non-integral literal as a concrete witness" $       checkTransitionDeterminismPure boolLiteralWitnessT         `shouldSatisfy` (not . null)++    it "treats every readable/opaque literal pairing identically" $ do+      let readable value = lit value :: Term OverlapRegs Cmd '[] Int+          hidden value = opaqueLit value :: Term OverlapRegs Cmd '[] Int+          pairings =+            [ (readable 3, readable 5),+              (readable 3, hidden 5),+              (hidden 3, readable 5),+              (hidden 3, hidden 5)+            ]+      forM_ pairings $ \(left, right) -> do+        checkTransitionDeterminismPure+          (overlapFixture (PEq left left) (PEq right right))+          `shouldSatisfy` (not . null)+        checkTransitionDeterminismPure+          (overlapFixture (PCmp CmpLt left right) PTop)+          `shouldSatisfy` (not . null)      it "finds an interior overlap in Natural's zero-bounded domain" $ do       let purePairs = map warningPair (checkTransitionDeterminismPure naturalInteriorOverlapT)