keiro-dsl-0.7.0.0: src/Keiro/Dsl/FoldFingerprint.hs
-- | Canonical identities for the aggregate fold surface used while hydrating
-- event streams. The fingerprint deliberately excludes payload codecs,
-- projections, snapshot policy, and source locations: those inputs do not change
-- how an existing event log becomes aggregate state.
module Keiro.Dsl.FoldFingerprint
( aggregateFoldFingerprintForService,
aggregateFoldSurfaceForService,
aggregateFoldFingerprint,
aggregateFoldSurface,
)
where
import Data.List (find)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.AggregateType
import Keiro.Dsl.EventOutput
import Keiro.Dsl.Expression
import Keiro.Dsl.Grammar
import Keiro.Dsl.NominalType
import Keiro.Dsl.PrettyPrint (renderExpr)
import Keiro.Dsl.ReadModelShape (fnv1a64)
import Keiro.Dsl.SemanticContract (CheckedService (..), EffectiveLanguageContract, legacyCheckedService, runtimeSemanticsFingerprintSegment)
import Keiro.Dsl.TypeGraph
-- | The sixteen-hex-digit identity of an aggregate's replay fold under the
-- service's effective runtime semantics.
aggregateFoldFingerprintForService :: CheckedService -> Aggregate -> Text
aggregateFoldFingerprintForService service = fnv1a64 . aggregateFoldSurfaceForService service
-- | The sixteen-hex-digit identity of an aggregate's replay fold.
-- This compatibility wrapper selects legacy/version-1 runtime semantics.
aggregateFoldFingerprint :: Spec -> Aggregate -> Text
aggregateFoldFingerprint spec = aggregateFoldFingerprintForService (legacyCheckedService spec)
-- | Canonical pre-hash text for an aggregate's replay fold.
--
-- Rules are declarations on 'Spec', not children of 'Aggregate', so the complete
-- spec is required. Only rules reached from transition guards and writes are
-- included, transitively, in declaration order.
aggregateFoldSurface :: Spec -> Aggregate -> Text
aggregateFoldSurface spec = aggregateFoldSurfaceForService (legacyCheckedService spec)
-- | Canonical pre-hash text under a checked semantic contract. A runtime
-- discriminator is included only for a contract that can change fold behavior;
-- source declaration provenance and grammar-only versions never enter it.
aggregateFoldSurfaceForService :: CheckedService -> Aggregate -> Text
aggregateFoldSurfaceForService service aggregate =
T.intercalate
"\n"
( maybe [] pure (runtimeSemanticsFingerprintSegment (checkedLanguageContract service))
++ map stateSegment (aggStates aggregate)
++ map (registerSegment symbols) (aggRegs aggregate)
++ mappedRegisterSegments
++ nominalSegments
++ nominalEqualitySegments
++ map (transitionSegment spec aggregate) (aggTransitions aggregate)
++ map ruleSegment referencedRules
)
where
spec = checkedSpec service
symbols = aggregateSymbols spec
referencedRules =
[ rule
| rule <- specRules spec,
ruleName rule `Set.member` referencedRuleNames spec aggregate
]
mappedRegisterSegments = case resolveTypeGraph spec of
Left _ -> []
Right graph ->
[ mappedRegisterSegment graph declaration
| register <- aggRegs aggregate,
TRef typeName <- [regType register],
Just declaration <- [Map.lookup (MappedKey typeName) (tgDeclarations graph)]
]
nominalSegments = case resolveNominalTypes spec of
Left _ -> []
Right registry ->
[ nominalUseSegment useSite nominal binding
| (useSite, typeName) <- nominalUseNames aggregate,
Just nominal <- [lookupNominalType typeName registry],
ConsumerNominal binding <- [resolvedNominalOwnership nominal]
]
nominalEqualitySegments =
[ "nominal-equality-use:" <> identity
| identity <- Set.toAscList (nominalEqualityUses service aggregate)
]
-- | Equality representation belongs in the fold identity only when a guard
-- actually compares that declaration. This keeps unrelated binding metadata out
-- of replay compatibility while ensuring a witness/domain change cannot silently
-- retain the old fold fingerprint.
nominalEqualityUses :: CheckedService -> Aggregate -> Set Text
nominalEqualityUses service aggregate =
Set.fromList
[ identity
| transition <- aggTransitions aggregate,
guardSyntax <- maybeToList (tGuard transition),
Right guardExpression <- [resolveGuardExpr (expressionEnvironment spec aggregate transition) guardSyntax],
identity <- equalityIdentities (checkedLanguageContract service) guardExpression
]
where
spec = checkedSpec service
equalityIdentities :: EffectiveLanguageContract -> TypedScalarExpr -> [Text]
equalityIdentities languageContract expression =
current <> children
where
current = case typedScalarNode expression of
TypedEqual left _ -> equalityIdentity left
TypedNotEqual left _ -> equalityIdentity left
_ -> []
equalityIdentity operand = case typedScalarType operand of
AggregateNominal nominal -> maybeToList (nominalEqualityIdentityForService languageContract nominal)
_ -> []
children = case typedScalarNode expression of
TypedLiteral {} -> []
TypedRoot {} -> []
TypedProject {} -> []
TypedAdd _ left right -> recurse left right
TypedSubtract _ left right -> recurse left right
TypedMultiply _ left right -> recurse left right
TypedEqual left right -> recurse left right
TypedNotEqual left right -> recurse left right
TypedCompare _ left right -> recurse left right
TypedAnd left right -> recurse left right
TypedOr left right -> recurse left right
recurse left right = equalityIdentities languageContract left <> equalityIdentities languageContract right
nominalUseNames :: Aggregate -> [(Text, Name)]
nominalUseNames aggregate =
[ ("register:" <> regName register, typeName)
| register <- aggRegs aggregate,
TRef typeName <- [regType register]
]
<> [ ("event:" <> evName event <> "." <> aggregateFieldName field, typeName)
| event <- aggEvents aggregate,
field <- eventFields event,
TRef typeName <- maybe [] pure (aggregateFieldType field)
]
where
eventFields event = case evBody event of
EventFields fields -> fields
EventFromCommand commandName -> concat [cmdFields command | command <- aggCommands aggregate, cmdName command == commandName]
nominalUseSegment :: Text -> ResolvedNominalType -> ConsumerNominalBinding -> Text
nominalUseSegment useSite nominal binding =
T.intercalate
"|"
[ "nominal-use:" <> useSite,
"name=" <> resolvedNominalName nominal,
"representation=" <> nominalRepresentationSegment (resolvedNominalRepresentation nominal),
"canonical=" <> unCanonicalTypeId (consumerNominalCanonical binding),
"binding=" <> unQualifiedValueName (consumerNominalBinding binding),
"binding-version=" <> unBindingVersion (consumerNominalBindingVersion binding),
"initial=" <> maybe "(none)" unQualifiedValueName (consumerNominalInitial binding)
]
nominalRepresentationSegment :: NominalRepresentation -> Text
nominalRepresentationSegment representation = case representation of
IdRepresentation prefix -> "id:" <> prefix
EnumRepresentation constructors -> "enum:" <> T.intercalate "," [constructor <> "=" <> wire | (constructor, wire) <- NE.toList constructors]
ScalarRepresentation scalar -> case scalar of
NominalText -> "Text"
NominalInt -> "Int"
NominalNatural -> "Natural"
NominalBool -> "Bool"
NominalTime -> "Time"
mappedRegisterSegment :: TypeGraph -> ResolvedMappedDecl -> Text
mappedRegisterSegment graph (ResolvedStructural declaration _) =
T.intercalate
"|"
[ "mapped-register:" <> sdName declaration,
"wire=" <> wireFingerprint graph (sdName declaration),
"canonical=" <> unCanonicalTypeId (sdCanonical declaration),
"binding=" <> unQualifiedValueName (sdBinding declaration),
"binding-version=" <> unBindingVersion (sdBindingVersion declaration),
"initial=" <> maybe "(missing)" unQualifiedValueName (sdInitial declaration)
]
mappedRegisterSegment _ (ResolvedOpaque declaration) =
T.intercalate
"|"
[ "mapped-register:" <> odName declaration,
"codec=" <> unCodecIdentity (odCodecIdentity declaration),
"codec-version=" <> unCodecVersion (odCodecVersion declaration),
"initial=" <> maybe "(missing)" unQualifiedValueName (odInitial declaration)
]
stateSegment :: StateDecl -> Text
stateSegment state =
"state:"
<> stName state
<> "|terminal="
<> if stTerminal state then "true" else "false"
registerSegment :: AggregateSymbols -> RegDecl -> Text
registerSegment symbols register =
"reg:"
<> regName register
<> ":"
<> typeExprCanonicalName (regType register)
<> "="
<> canonicalInitial
where
canonicalInitial = case resolveAggregateType symbols (regLoc register) RegisterUse (regType register) of
Left _ -> renderInitial (regInitial register)
Right resolvedType -> case resolveRegisterInitial symbols (regLoc register) resolvedType (regInitial register) of
Left _ -> renderInitial (regInitial register)
Right resolvedInitial -> registerInitialCanonicalName resolvedInitial
renderInitial :: RegInitial -> Text
renderInitial (RegInitBare value) = value
renderInitial (RegInitText value) = "\"" <> escapeText value <> "\""
escapeText :: Text -> Text
escapeText = T.concatMap $ \case
'"' -> "\\\""
'\\' -> "\\\\"
'\n' -> "\\n"
'\t' -> "\\t"
'\r' -> "\\r"
character -> T.singleton character
transitionSegment :: Spec -> Aggregate -> Transition -> Text
transitionSegment spec aggregate transition =
T.intercalate
"|"
( [ "transition:" <> renderMode (tMode transition),
tSource transition,
tCommand transition
]
++ implementationSegment
++ [ "guard=" <> maybe "" renderExpr (tGuard transition),
"writes=" <> T.intercalate ";" (map renderWrite (tWrites transition)),
"emits=" <> T.intercalate "," (tEmits transition)
]
++ outputOwnershipSegment
++ ["goto=" <> tGoto transition]
)
where
renderWrite (registerName, expression) = registerName <> ":=" <> renderExpr expression
outputSegment emitIndex eventName = case eventOutputMapping spec aggregate transition emitIndex eventName of
Right mapping -> eventName <> "=" <> eventOutputCanonical mapping
Left problem -> eventName <> "=invalid:" <> T.pack (show problem)
outputOwnershipSegment = case tImplementation transition of
LegacyHoleImplementation -> []
GeneratedImplementation -> ["outputs=" <> T.intercalate "," [outputSegment emitIndex eventName | (emitIndex, eventName) <- zip [1 ..] (tEmits transition)]]
HoleImplementation -> ["outputs=" <> T.intercalate "," [outputSegment emitIndex eventName | (emitIndex, eventName) <- zip [1 ..] (tEmits transition)]]
implementationSegment = case tImplementation transition of
LegacyHoleImplementation -> []
GeneratedImplementation -> ["implementation=generated"]
HoleImplementation -> ["implementation=hole"]
renderMode :: TransitionMode -> Text
renderMode TmLive = "live"
renderMode TmReplayOnly = "replay-only"
ruleSegment :: RuleDecl -> Text
ruleSegment rule =
T.intercalate
"|"
[ "rule:" <> ruleName rule,
ruleDomain rule,
ruleCodomain rule,
"cases=" <> T.intercalate ";" (map renderCase (ruleCases rule))
]
where
renderCase (constructorName, expression) = constructorName <> "=>" <> renderExpr expression
referencedRuleNames :: Spec -> Aggregate -> Set Name
referencedRuleNames spec aggregate = close directNames
where
rules = specRules spec
directNames =
Set.unions
[ exprNames expression
| transition <- aggTransitions aggregate,
expression <- maybeToList (tGuard transition) ++ map snd (tWrites transition)
]
close names =
let expanded =
Set.unions
( names
: [ Set.unions (map (exprNames . snd) (ruleCases rule))
| name <- Set.toList names,
Just rule <- [find ((== name) . ruleName) rules]
]
)
in if expanded == names then names else close expanded
exprNames :: Expr -> Set Name
exprNames = \case
EOr left right -> exprNames left <> exprNames right
EAnd left right -> exprNames left <> exprNames right
ECmp _ left right -> exprNames left <> exprNames right
EAdd _ left right -> exprNames left <> exprNames right
ESubtract _ left right -> exprNames left <> exprNames right
EMultiply _ left right -> exprNames left <> exprNames right
EPath _ _ (name : _) -> Set.singleton name
EPath _ _ [] -> Set.empty
ELiteral {} -> Set.empty
EAtom (AName name) -> Set.singleton name
EAtom (ABool _) -> Set.empty
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just value) = [value]