packages feed

keiro-dsl 0.3.0.0 → 0.4.0.1

raw patch · 83 files changed

+17466/−4775 lines, 83 filesdep +bytestringdep +deepseqdep +keiro-coredep ~aesondep ~containersdep ~directoryPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring, deepseq, keiro-core, tasty-bench

Dependency ranges changed: aeson, containers, directory, filepath, keiki, megaparsec, optparse-applicative, parser-combinators, prettyprinter, process, text, time, uuid

API changes (from Hackage documentation)

+ Keiro.Dsl.CodecCompare: BranchArm :: !Text -> !Maybe BranchSchema -> BranchArm
+ Keiro.Dsl.CodecCompare: BranchField :: !Text -> !Bool -> !BranchSchema -> BranchField
+ Keiro.Dsl.CodecCompare: BranchList :: !BranchSchema -> BranchSchema
+ Keiro.Dsl.CodecCompare: BranchMap :: !BranchSchema -> BranchSchema
+ Keiro.Dsl.CodecCompare: BranchOptional :: !BranchSchema -> BranchSchema
+ Keiro.Dsl.CodecCompare: BranchRecord :: ![BranchField] -> BranchSchema
+ Keiro.Dsl.CodecCompare: BranchScalar :: BranchSchema
+ Keiro.Dsl.CodecCompare: BranchUnion :: !Text -> !Text -> ![BranchArm] -> BranchSchema
+ Keiro.Dsl.CodecCompare: ClassifiedObservation :: !FixtureOrigin -> !Text -> !FixtureVerdict -> ClassifiedObservation
+ Keiro.Dsl.CodecCompare: CompareProvenance :: !Text -> !Text -> !CanonicalTypeId -> !QualifiedValueName -> !BindingVersion -> !Text -> CompareProvenance
+ Keiro.Dsl.CodecCompare: CompareReport :: !CompareProvenance -> ![ClassifiedObservation] -> ![CompareInputIssue] -> ![CoverageGap] -> !Text -> CompareReport
+ Keiro.Dsl.CodecCompare: CoverageGap :: !FixtureOrigin -> !JsonPointer -> !BranchKind -> CoverageGap
+ Keiro.Dsl.CodecCompare: DeclaredBranch :: !FixtureOrigin -> !JsonPointer -> !BranchKind -> DeclaredBranch
+ Keiro.Dsl.CodecCompare: DecodeFailed :: !Text -> DecodeOutcome
+ Keiro.Dsl.CodecCompare: DecodeObservation :: !FilePath -> !Value -> !DecodeOutcome -> !DecodeOutcome -> CompareObservation
+ Keiro.Dsl.CodecCompare: DecodedShape :: !Value -> DecodeOutcome
+ Keiro.Dsl.CodecCompare: DecodedValueDifference :: !JsonPointer -> !Value -> !Value -> ComparisonDifference
+ Keiro.Dsl.CodecCompare: EncodeObservation :: !Text -> !Value -> !Value -> CompareObservation
+ Keiro.Dsl.CodecCompare: EncodedValueDifference :: !JsonPointer -> !Value -> !Value -> ComparisonDifference
+ Keiro.Dsl.CodecCompare: ExplicitNull :: BranchKind
+ Keiro.Dsl.CodecCompare: FromBinding :: FixtureOrigin
+ Keiro.Dsl.CodecCompare: GeneratedDecodeRejected :: !Text -> ComparisonDifference
+ Keiro.Dsl.CodecCompare: HistoricalCodec :: !Text -> !Text -> !a -> Value -> !Value -> Either Text a -> HistoricalCodec a
+ Keiro.Dsl.CodecCompare: HistoricalCodecProvenanceInvalid :: !Text -> CompareInputIssue
+ Keiro.Dsl.CodecCompare: HistoricalCodecRejected :: !FilePath -> !Text -> CompareInputIssue
+ Keiro.Dsl.CodecCompare: HistoricalGolden :: FixtureOrigin
+ Keiro.Dsl.CodecCompare: HistoricalGoldenUnreadable :: !FilePath -> !Text -> CompareInputIssue
+ Keiro.Dsl.CodecCompare: JsonParity :: FixtureVerdict
+ Keiro.Dsl.CodecCompare: JsonPointer :: Text -> JsonPointer
+ Keiro.Dsl.CodecCompare: ObservedBranch :: !FixtureOrigin -> !JsonPointer -> !BranchKind -> ObservedBranch
+ Keiro.Dsl.CodecCompare: OptionalMissing :: BranchKind
+ Keiro.Dsl.CodecCompare: OptionalPresent :: BranchKind
+ Keiro.Dsl.CodecCompare: ReportWriteError :: !FilePath -> !Text -> ReportWriteError
+ Keiro.Dsl.CodecCompare: RequiresVersionWork :: !ComparisonDifference -> FixtureVerdict
+ Keiro.Dsl.CodecCompare: UnionArm :: !Text -> BranchKind
+ Keiro.Dsl.CodecCompare: [baPayloadSchema] :: BranchArm -> !Maybe BranchSchema
+ Keiro.Dsl.CodecCompare: [baWireTag] :: BranchArm -> !Text
+ Keiro.Dsl.CodecCompare: [bfPresenceOptional] :: BranchField -> !Bool
+ Keiro.Dsl.CodecCompare: [bfSchema] :: BranchField -> !BranchSchema
+ Keiro.Dsl.CodecCompare: [bfWireKey] :: BranchField -> !Text
+ Keiro.Dsl.CodecCompare: [cgKind] :: CoverageGap -> !BranchKind
+ Keiro.Dsl.CodecCompare: [cgOrigin] :: CoverageGap -> !FixtureOrigin
+ Keiro.Dsl.CodecCompare: [cgPointer] :: CoverageGap -> !JsonPointer
+ Keiro.Dsl.CodecCompare: [classifiedName] :: ClassifiedObservation -> !Text
+ Keiro.Dsl.CodecCompare: [classifiedOrigin] :: ClassifiedObservation -> !FixtureOrigin
+ Keiro.Dsl.CodecCompare: [classifiedVerdict] :: ClassifiedObservation -> !FixtureVerdict
+ Keiro.Dsl.CodecCompare: [coCaseName] :: CompareObservation -> !Text
+ Keiro.Dsl.CodecCompare: [coFixturePath] :: CompareObservation -> !FilePath
+ Keiro.Dsl.CodecCompare: [coGeneratedDecode] :: CompareObservation -> !DecodeOutcome
+ Keiro.Dsl.CodecCompare: [coGeneratedValue] :: CompareObservation -> !Value
+ Keiro.Dsl.CodecCompare: [coHistoricalDecode] :: CompareObservation -> !DecodeOutcome
+ Keiro.Dsl.CodecCompare: [coHistoricalValue] :: CompareObservation -> !Value
+ Keiro.Dsl.CodecCompare: [coInputValue] :: CompareObservation -> !Value
+ Keiro.Dsl.CodecCompare: [cpBindingSymbol] :: CompareProvenance -> !QualifiedValueName
+ Keiro.Dsl.CodecCompare: [cpBindingVersion] :: CompareProvenance -> !BindingVersion
+ Keiro.Dsl.CodecCompare: [cpCanonicalType] :: CompareProvenance -> !CanonicalTypeId
+ Keiro.Dsl.CodecCompare: [cpHistoricalCodecIdentity] :: CompareProvenance -> !Text
+ Keiro.Dsl.CodecCompare: [cpHistoricalCodecVersion] :: CompareProvenance -> !Text
+ Keiro.Dsl.CodecCompare: [cpWireFingerprint] :: CompareProvenance -> !Text
+ Keiro.Dsl.CodecCompare: [crAuthority] :: CompareReport -> !Text
+ Keiro.Dsl.CodecCompare: [crCoverageGaps] :: CompareReport -> ![CoverageGap]
+ Keiro.Dsl.CodecCompare: [crInputIssues] :: CompareReport -> ![CompareInputIssue]
+ Keiro.Dsl.CodecCompare: [crObservations] :: CompareReport -> ![ClassifiedObservation]
+ Keiro.Dsl.CodecCompare: [crProvenance] :: CompareReport -> !CompareProvenance
+ Keiro.Dsl.CodecCompare: [dbKind] :: DeclaredBranch -> !BranchKind
+ Keiro.Dsl.CodecCompare: [dbOrigin] :: DeclaredBranch -> !FixtureOrigin
+ Keiro.Dsl.CodecCompare: [dbPointer] :: DeclaredBranch -> !JsonPointer
+ Keiro.Dsl.CodecCompare: [hcDecode] :: HistoricalCodec a -> !Value -> Either Text a
+ Keiro.Dsl.CodecCompare: [hcEncode] :: HistoricalCodec a -> !a -> Value
+ Keiro.Dsl.CodecCompare: [hcIdentity] :: HistoricalCodec a -> !Text
+ Keiro.Dsl.CodecCompare: [hcVersion] :: HistoricalCodec a -> !Text
+ Keiro.Dsl.CodecCompare: [obKind] :: ObservedBranch -> !BranchKind
+ Keiro.Dsl.CodecCompare: [obOrigin] :: ObservedBranch -> !FixtureOrigin
+ Keiro.Dsl.CodecCompare: [obPointer] :: ObservedBranch -> !JsonPointer
+ Keiro.Dsl.CodecCompare: [reportWriteMessage] :: ReportWriteError -> !Text
+ Keiro.Dsl.CodecCompare: [reportWritePath] :: ReportWriteError -> !FilePath
+ Keiro.Dsl.CodecCompare: [unJsonPointer] :: JsonPointer -> Text
+ Keiro.Dsl.CodecCompare: authorityStatement :: Text
+ Keiro.Dsl.CodecCompare: canonicalJsonBytes :: Value -> ByteString
+ Keiro.Dsl.CodecCompare: classifyObservation :: CompareObservation -> Either CompareInputIssue FixtureVerdict
+ Keiro.Dsl.CodecCompare: compareReport :: CompareProvenance -> [CompareInputIssue] -> [CompareObservation] -> [DeclaredBranch] -> [ObservedBranch] -> CompareReport
+ Keiro.Dsl.CodecCompare: data BranchArm
+ Keiro.Dsl.CodecCompare: data BranchField
+ Keiro.Dsl.CodecCompare: data BranchKind
+ Keiro.Dsl.CodecCompare: data BranchSchema
+ Keiro.Dsl.CodecCompare: data ClassifiedObservation
+ Keiro.Dsl.CodecCompare: data CompareInputIssue
+ Keiro.Dsl.CodecCompare: data CompareObservation
+ Keiro.Dsl.CodecCompare: data CompareProvenance
+ Keiro.Dsl.CodecCompare: data CompareReport
+ Keiro.Dsl.CodecCompare: data ComparisonDifference
+ Keiro.Dsl.CodecCompare: data CoverageGap
+ Keiro.Dsl.CodecCompare: data DeclaredBranch
+ Keiro.Dsl.CodecCompare: data DecodeOutcome
+ Keiro.Dsl.CodecCompare: data FixtureOrigin
+ Keiro.Dsl.CodecCompare: data FixtureVerdict
+ Keiro.Dsl.CodecCompare: data HistoricalCodec a
+ Keiro.Dsl.CodecCompare: data ObservedBranch
+ Keiro.Dsl.CodecCompare: data ReportWriteError
+ Keiro.Dsl.CodecCompare: declaredBranchesFor :: FixtureOrigin -> BranchSchema -> [DeclaredBranch]
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.BranchKind
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.ClassifiedObservation
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.CompareInputIssue
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.CompareProvenance
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.CompareReport
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.ComparisonDifference
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.CoverageGap
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.DeclaredBranch
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.FixtureOrigin
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.FixtureVerdict
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.JsonPointer
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.CodecCompare.ObservedBranch
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.BranchKind
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.ClassifiedObservation
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.CompareInputIssue
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.CompareProvenance
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.CompareReport
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.ComparisonDifference
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.CoverageGap
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.DeclaredBranch
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.FixtureOrigin
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.FixtureVerdict
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.JsonPointer
+ Keiro.Dsl.CodecCompare: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CodecCompare.ObservedBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.BranchArm
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.BranchField
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.BranchKind
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.BranchSchema
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.ClassifiedObservation
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.CompareInputIssue
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.CompareObservation
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.CompareProvenance
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.CompareReport
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.ComparisonDifference
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.CoverageGap
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.DeclaredBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.DecodeOutcome
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.FixtureOrigin
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.FixtureVerdict
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.JsonPointer
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.ObservedBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Eq Keiro.Dsl.CodecCompare.ReportWriteError
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.BranchKind
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.CoverageGap
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.DeclaredBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.FixtureOrigin
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.JsonPointer
+ Keiro.Dsl.CodecCompare: instance GHC.Classes.Ord Keiro.Dsl.CodecCompare.ObservedBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.BranchArm
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.BranchField
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.BranchKind
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.BranchSchema
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.ClassifiedObservation
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.CompareInputIssue
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.CompareObservation
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.CompareProvenance
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.CompareReport
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.ComparisonDifference
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.CoverageGap
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.DeclaredBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.DecodeOutcome
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.FixtureOrigin
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.FixtureVerdict
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.JsonPointer
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.ObservedBranch
+ Keiro.Dsl.CodecCompare: instance GHC.Internal.Show.Show Keiro.Dsl.CodecCompare.ReportWriteError
+ Keiro.Dsl.CodecCompare: newtype JsonPointer
+ Keiro.Dsl.CodecCompare: observedBranchesFor :: FixtureOrigin -> BranchSchema -> Value -> [ObservedBranch]
+ Keiro.Dsl.CodecCompare: renderCompareReport :: CompareReport -> Text
+ Keiro.Dsl.CodecCompare: reportSucceeded :: CompareReport -> Bool
+ Keiro.Dsl.CodecCompare: writeCompareReportAtomic :: FilePath -> CompareReport -> IO (Either ReportWriteError ())
+ Keiro.Dsl.Coverage: CoverageCounts :: !Int -> !Int -> !Int -> !Int -> CoverageCounts
+ Keiro.Dsl.Coverage: CoverageDelta :: !Int -> !Int -> !Int -> ![OpaqueBoundary] -> ![OpaqueBoundary] -> CoverageDelta
+ Keiro.Dsl.Coverage: CoverageFinding :: !Severity -> !DiagnosticCode -> ![Text] -> !Text -> CoverageFinding
+ Keiro.Dsl.Coverage: CoveragePrevious :: !Text -> !CoverageSummary -> ![OpaqueBoundary] -> CoveragePrevious
+ Keiro.Dsl.Coverage: CoverageReport :: !FilePath -> ![CoverageRoot] -> ![StructuralBoundary] -> ![OpaqueBoundary] -> ![JsonBoundary] -> ![SnapshotBoundary] -> ![UnsupportedSurface] -> !CoverageSummary -> ![CoverageFinding] -> !Maybe CoveragePrevious -> !Maybe CoverageDelta -> CoverageReport
+ Keiro.Dsl.Coverage: CoverageRoot :: !CoverageSurface -> !Text -> !Text -> !CoverageMode -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Text -> CoverageRoot
+ Keiro.Dsl.Coverage: CoverageSummary :: !CoverageCounts -> !CoverageCounts -> CoverageSummary
+ Keiro.Dsl.Coverage: JsonBoundary :: !Text -> !Text -> JsonBoundary
+ Keiro.Dsl.Coverage: OpaqueBoundary :: !Text -> !Text -> !Text -> !Text -> !Text -> OpaqueBoundary
+ Keiro.Dsl.Coverage: OpaqueCoverage :: CoverageMode
+ Keiro.Dsl.Coverage: PrivateEventPayload :: CoverageSurface
+ Keiro.Dsl.Coverage: SnapshotBoundary :: !Text -> !Text -> !Text -> !Text -> !CoverageMode -> !Text -> !Text -> !Text -> !Bool -> SnapshotBoundary
+ Keiro.Dsl.Coverage: SnapshotRegister :: CoverageSurface
+ Keiro.Dsl.Coverage: StructuralBoundary :: !Text -> !Text -> !Text -> !Text -> !Text -> StructuralBoundary
+ Keiro.Dsl.Coverage: StructuralCoverage :: CoverageMode
+ Keiro.Dsl.Coverage: UnsupportedSurface :: !Text -> !Text -> !Text -> UnsupportedSurface
+ Keiro.Dsl.Coverage: [addedOpaqueBoundaries] :: CoverageDelta -> ![OpaqueBoundary]
+ Keiro.Dsl.Coverage: [coverageDelta] :: CoverageReport -> !Maybe CoverageDelta
+ Keiro.Dsl.Coverage: [coverageFindings] :: CoverageReport -> ![CoverageFinding]
+ Keiro.Dsl.Coverage: [coverageJsonBoundaries] :: CoverageReport -> ![JsonBoundary]
+ Keiro.Dsl.Coverage: [coverageOpaqueBoundaries] :: CoverageReport -> ![OpaqueBoundary]
+ Keiro.Dsl.Coverage: [coveragePrevious] :: CoverageReport -> !Maybe CoveragePrevious
+ Keiro.Dsl.Coverage: [coverageRoots] :: CoverageReport -> ![CoverageRoot]
+ Keiro.Dsl.Coverage: [coverageSnapshotBoundaries] :: CoverageReport -> ![SnapshotBoundary]
+ Keiro.Dsl.Coverage: [coverageSpec] :: CoverageReport -> !FilePath
+ Keiro.Dsl.Coverage: [coverageStructuralBoundaries] :: CoverageReport -> ![StructuralBoundary]
+ Keiro.Dsl.Coverage: [coverageSummary] :: CoverageReport -> !CoverageSummary
+ Keiro.Dsl.Coverage: [coverageUnsupportedSurfaces] :: CoverageReport -> ![UnsupportedSurface]
+ Keiro.Dsl.Coverage: [findingCode] :: CoverageFinding -> !DiagnosticCode
+ Keiro.Dsl.Coverage: [findingMessage] :: CoverageFinding -> !Text
+ Keiro.Dsl.Coverage: [findingRoots] :: CoverageFinding -> ![Text]
+ Keiro.Dsl.Coverage: [findingSeverity] :: CoverageFinding -> !Severity
+ Keiro.Dsl.Coverage: [jsonBoundaries] :: CoverageCounts -> !Int
+ Keiro.Dsl.Coverage: [jsonPath] :: JsonBoundary -> !Text
+ Keiro.Dsl.Coverage: [jsonRoot] :: JsonBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaqueBoundaryDelta] :: CoverageDelta -> !Int
+ Keiro.Dsl.Coverage: [opaqueCodecIdentity] :: OpaqueBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaqueCodecVersion] :: OpaqueBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaqueMappedType] :: OpaqueBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaquePath] :: OpaqueBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaqueRoot] :: OpaqueBoundary -> !Text
+ Keiro.Dsl.Coverage: [opaqueRoots] :: CoverageCounts -> !Int
+ Keiro.Dsl.Coverage: [previousOpaqueBoundaries] :: CoveragePrevious -> ![OpaqueBoundary]
+ Keiro.Dsl.Coverage: [previousReference] :: CoveragePrevious -> !Text
+ Keiro.Dsl.Coverage: [previousSummary] :: CoveragePrevious -> !CoverageSummary
+ Keiro.Dsl.Coverage: [privateEventPayloads] :: CoverageSummary -> !CoverageCounts
+ Keiro.Dsl.Coverage: [privateEventRootDelta] :: CoverageDelta -> !Int
+ Keiro.Dsl.Coverage: [removedOpaqueBoundaries] :: CoverageDelta -> ![OpaqueBoundary]
+ Keiro.Dsl.Coverage: [rootCanonicalType] :: CoverageRoot -> !Maybe Text
+ Keiro.Dsl.Coverage: [rootCodecIdentity] :: CoverageRoot -> !Maybe Text
+ Keiro.Dsl.Coverage: [rootCodecVersion] :: CoverageRoot -> !Maybe Text
+ Keiro.Dsl.Coverage: [rootMappedType] :: CoverageRoot -> !Text
+ Keiro.Dsl.Coverage: [rootMode] :: CoverageRoot -> !CoverageMode
+ Keiro.Dsl.Coverage: [rootPath] :: CoverageRoot -> !Text
+ Keiro.Dsl.Coverage: [rootSurface] :: CoverageRoot -> !CoverageSurface
+ Keiro.Dsl.Coverage: [rootWireFingerprint] :: CoverageRoot -> !Text
+ Keiro.Dsl.Coverage: [snapshotAggregate] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotEnabled] :: SnapshotBoundary -> !Bool
+ Keiro.Dsl.Coverage: [snapshotEncoding] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotInvalidation] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotMappedType] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotMode] :: SnapshotBoundary -> !CoverageMode
+ Keiro.Dsl.Coverage: [snapshotRegisterRootDelta] :: CoverageDelta -> !Int
+ Keiro.Dsl.Coverage: [snapshotRegister] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotRegisters] :: CoverageSummary -> !CoverageCounts
+ Keiro.Dsl.Coverage: [snapshotRoot] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [snapshotWireFingerprint] :: SnapshotBoundary -> !Text
+ Keiro.Dsl.Coverage: [structuralCanonicalType] :: StructuralBoundary -> !Text
+ Keiro.Dsl.Coverage: [structuralMappedType] :: StructuralBoundary -> !Text
+ Keiro.Dsl.Coverage: [structuralPath] :: StructuralBoundary -> !Text
+ Keiro.Dsl.Coverage: [structuralRoot] :: StructuralBoundary -> !Text
+ Keiro.Dsl.Coverage: [structuralRoots] :: CoverageCounts -> !Int
+ Keiro.Dsl.Coverage: [structuralWireFingerprint] :: StructuralBoundary -> !Text
+ Keiro.Dsl.Coverage: [totalRoots] :: CoverageCounts -> !Int
+ Keiro.Dsl.Coverage: [unsupportedReason] :: UnsupportedSurface -> !Text
+ Keiro.Dsl.Coverage: [unsupportedSupport] :: UnsupportedSurface -> !Text
+ Keiro.Dsl.Coverage: [unsupportedSurface] :: UnsupportedSurface -> !Text
+ Keiro.Dsl.Coverage: coverageDiffReport :: FilePath -> Text -> Spec -> Spec -> Either (NonEmpty TypeGraphError) CoverageReport
+ Keiro.Dsl.Coverage: coverageReport :: FilePath -> Spec -> Either (NonEmpty TypeGraphError) CoverageReport
+ Keiro.Dsl.Coverage: coverageSucceeded :: CoverageReport -> Bool
+ Keiro.Dsl.Coverage: data CoverageCounts
+ Keiro.Dsl.Coverage: data CoverageDelta
+ Keiro.Dsl.Coverage: data CoverageFinding
+ Keiro.Dsl.Coverage: data CoverageMode
+ Keiro.Dsl.Coverage: data CoveragePrevious
+ Keiro.Dsl.Coverage: data CoverageReport
+ Keiro.Dsl.Coverage: data CoverageRoot
+ Keiro.Dsl.Coverage: data CoverageSummary
+ Keiro.Dsl.Coverage: data CoverageSurface
+ Keiro.Dsl.Coverage: data JsonBoundary
+ Keiro.Dsl.Coverage: data OpaqueBoundary
+ Keiro.Dsl.Coverage: data SnapshotBoundary
+ Keiro.Dsl.Coverage: data StructuralBoundary
+ Keiro.Dsl.Coverage: data UnsupportedSurface
+ Keiro.Dsl.Coverage: failOnOpaque :: CoverageReport -> CoverageReport
+ Keiro.Dsl.Coverage: failOnOpaqueIncrease :: CoverageReport -> CoverageReport
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageCounts
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageDelta
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageFinding
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageMode
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoveragePrevious
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageReport
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageRoot
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageSummary
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.CoverageSurface
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.JsonBoundary
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.OpaqueBoundary
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.SnapshotBoundary
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.StructuralBoundary
+ Keiro.Dsl.Coverage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.Coverage.UnsupportedSurface
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageCounts
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageDelta
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageFinding
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageMode
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoveragePrevious
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageReport
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageRoot
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageSummary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.CoverageSurface
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.JsonBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.OpaqueBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.SnapshotBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.StructuralBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Eq Keiro.Dsl.Coverage.UnsupportedSurface
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.CoverageMode
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.CoverageRoot
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.CoverageSurface
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.JsonBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.OpaqueBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.SnapshotBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.StructuralBoundary
+ Keiro.Dsl.Coverage: instance GHC.Classes.Ord Keiro.Dsl.Coverage.UnsupportedSurface
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageCounts
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageDelta
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageFinding
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageMode
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoveragePrevious
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageReport
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageRoot
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageSummary
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.CoverageSurface
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.JsonBoundary
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.OpaqueBoundary
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.SnapshotBoundary
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.StructuralBoundary
+ Keiro.Dsl.Coverage: instance GHC.Internal.Show.Show Keiro.Dsl.Coverage.UnsupportedSurface
+ Keiro.Dsl.Coverage: renderCoverageFinding :: FilePath -> CoverageFinding -> Text
+ Keiro.Dsl.Coverage: renderCoverageSummary :: CoverageReport -> Text
+ Keiro.Dsl.Coverage: writeCoverageReport :: FilePath -> CoverageReport -> IO ()
+ Keiro.Dsl.Diff: CompatibilityVector :: !SurfaceVerdict -> !SurfaceVerdict -> !SurfaceVerdict -> !SurfaceVerdict -> !SurfaceVerdict -> !SurfaceVerdict -> !Set RolloutConstraint -> CompatibilityVector
+ Keiro.Dsl.Diff: ConsumerBuild :: CompatibilitySurface
+ Keiro.Dsl.Diff: LabelAdditive :: Label
+ Keiro.Dsl.Diff: LabelAdvisory :: Label
+ Keiro.Dsl.Diff: LabelBreaking :: Label
+ Keiro.Dsl.Diff: OldBinaryReadNewEvents :: CompatibilitySurface
+ Keiro.Dsl.Diff: PersistedIdentity :: CompatibilitySurface
+ Keiro.Dsl.Diff: PrivateHistoryRead :: CompatibilitySurface
+ Keiro.Dsl.Diff: PublicConsumer :: CompatibilitySurface
+ Keiro.Dsl.Diff: RolloutDrainRequired :: RolloutConstraint
+ Keiro.Dsl.Diff: RolloutProducerLast :: RolloutConstraint
+ Keiro.Dsl.Diff: RolloutStopTheWorld :: RolloutConstraint
+ Keiro.Dsl.Diff: RolloutWorkersFirst :: RolloutConstraint
+ Keiro.Dsl.Diff: SnapshotHydration :: CompatibilitySurface
+ Keiro.Dsl.Diff: VAdvisory :: SurfaceVerdict
+ Keiro.Dsl.Diff: VBreaking :: SurfaceVerdict
+ Keiro.Dsl.Diff: VCompatible :: SurfaceVerdict
+ Keiro.Dsl.Diff: VNotApplicable :: SurfaceVerdict
+ Keiro.Dsl.Diff: [ckContext] :: ChangeKind -> !ChangeContext
+ Keiro.Dsl.Diff: [ckPaths] :: ChangeKind -> ![Text]
+ Keiro.Dsl.Diff: [ckVector] :: ChangeKind -> !CompatibilityVector
+ Keiro.Dsl.Diff: [cvConsumerBuild] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: [cvOldBinaryReadNewEvents] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: [cvPersistedIdentity] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: [cvPrivateHistoryRead] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: [cvPublicConsumer] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: [cvRollout] :: CompatibilityVector -> !Set RolloutConstraint
+ Keiro.Dsl.Diff: [cvSnapshotHydration] :: CompatibilityVector -> !SurfaceVerdict
+ Keiro.Dsl.Diff: changeContextPaths :: ChangeContext -> [Text]
+ Keiro.Dsl.Diff: changeContextRoot :: ChangeContext -> Name
+ Keiro.Dsl.Diff: classifyCompatibility :: ChangeContext -> DiagnosticCode -> CompatibilityVector
+ Keiro.Dsl.Diff: consumerBuildContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: data ChangeContext
+ Keiro.Dsl.Diff: data CompatibilitySurface
+ Keiro.Dsl.Diff: data CompatibilityVector
+ Keiro.Dsl.Diff: data Label
+ Keiro.Dsl.Diff: data RolloutConstraint
+ Keiro.Dsl.Diff: data SurfaceVerdict
+ Keiro.Dsl.Diff: defaultGate :: Set CompatibilitySurface
+ Keiro.Dsl.Diff: deriveLabel :: Set CompatibilitySurface -> CompatibilityVector -> Label
+ Keiro.Dsl.Diff: gateWith :: [CompatibilitySurface] -> Set CompatibilitySurface
+ Keiro.Dsl.Diff: gatedBreaking :: Set CompatibilitySurface -> Change -> Bool
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.ChangeContext
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.CompatibilitySurface
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.CompatibilityVector
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.ContextKind
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.Label
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.RolloutConstraint
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.SurfaceVerdict
+ Keiro.Dsl.Diff: instance GHC.Classes.Ord Keiro.Dsl.Diff.CompatibilitySurface
+ Keiro.Dsl.Diff: instance GHC.Classes.Ord Keiro.Dsl.Diff.RolloutConstraint
+ Keiro.Dsl.Diff: instance GHC.Internal.Enum.Bounded Keiro.Dsl.Diff.CompatibilitySurface
+ Keiro.Dsl.Diff: instance GHC.Internal.Enum.Enum Keiro.Dsl.Diff.CompatibilitySurface
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.ChangeContext
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.CompatibilitySurface
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.CompatibilityVector
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.ContextKind
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.Label
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.RolloutConstraint
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.SurfaceVerdict
+ Keiro.Dsl.Diff: persistedIdentityContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: privateEventAdditionContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: privateEventContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: publicContractContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: queueContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: snapshotContext :: Name -> [Text] -> ChangeContext
+ Keiro.Dsl.Diff: verdictFor :: CompatibilitySurface -> CompatibilityVector -> SurfaceVerdict
+ Keiro.Dsl.DiffReport: RemedyContractRevision :: Remedy
+ Keiro.Dsl.DiffReport: RemedyDeploymentOrder :: RolloutConstraint -> Remedy
+ Keiro.Dsl.DiffReport: RemedyDoNotDeploy :: Text -> Remedy
+ Keiro.Dsl.DiffReport: RemedyRecompileConsumers :: Remedy
+ Keiro.Dsl.DiffReport: RemedyReplayOnlyEdge :: Remedy
+ Keiro.Dsl.DiffReport: RemedyRunConformance :: Remedy
+ Keiro.Dsl.DiffReport: RemedyStateCodecBump :: Remedy
+ Keiro.Dsl.DiffReport: RemedyUpcaster :: Remedy
+ Keiro.Dsl.DiffReport: RemedyVersionBump :: Remedy
+ Keiro.Dsl.DiffReport: data DiffReport
+ Keiro.Dsl.DiffReport: data Remedy
+ Keiro.Dsl.DiffReport: diffReport :: Set CompatibilitySurface -> [Change] -> DiffReport
+ Keiro.Dsl.DiffReport: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.DiffReport.DiffReport
+ Keiro.Dsl.DiffReport: instance GHC.Classes.Eq Keiro.Dsl.DiffReport.DiffReport
+ Keiro.Dsl.DiffReport: instance GHC.Classes.Eq Keiro.Dsl.DiffReport.Remedy
+ Keiro.Dsl.DiffReport: instance GHC.Internal.Show.Show Keiro.Dsl.DiffReport.DiffReport
+ Keiro.Dsl.DiffReport: instance GHC.Internal.Show.Show Keiro.Dsl.DiffReport.Remedy
+ Keiro.Dsl.DiffReport: parseSurfaceName :: String -> Either String CompatibilitySurface
+ Keiro.Dsl.DiffReport: remediationFor :: ChangeContext -> DiagnosticCode -> NonEmpty Remedy
+ Keiro.Dsl.DiffReport: renderExplainBlock :: Change -> Text
+ Keiro.Dsl.DiffReport: renderFinding :: Change -> Text
+ Keiro.Dsl.DiffReport: renderRemedy :: Remedy -> Text
+ Keiro.Dsl.DiffReport: renderVectorLine :: CompatibilityVector -> Text
+ Keiro.Dsl.DiffReport: rolloutName :: RolloutConstraint -> Text
+ Keiro.Dsl.DiffReport: surfaceName :: CompatibilitySurface -> Text
+ Keiro.Dsl.DiffReport: verdictName :: SurfaceVerdict -> Text
+ Keiro.Dsl.ExplainBindings: BindingHole :: !Name -> !Text -> !Text -> !BindingObligationKind -> !Maybe Text -> !Text -> BindingHole
+ Keiro.Dsl.ExplainBindings: BindingObligation :: !Name -> !Text -> !Text -> !Text -> !BindingObligationKind -> !Text -> ![Text] -> !Maybe Text -> BindingObligation
+ Keiro.Dsl.ExplainBindings: BindingValue :: BindingObligationKind
+ Keiro.Dsl.ExplainBindings: FixtureValue :: BindingObligationKind
+ Keiro.Dsl.ExplainBindings: InitialValue :: BindingObligationKind
+ Keiro.Dsl.ExplainBindings: [holeKind] :: BindingHole -> !BindingObligationKind
+ Keiro.Dsl.ExplainBindings: [holeMappedName] :: BindingHole -> !Name
+ Keiro.Dsl.ExplainBindings: [holeModule] :: BindingHole -> !Text
+ Keiro.Dsl.ExplainBindings: [holePath] :: BindingHole -> !Maybe Text
+ Keiro.Dsl.ExplainBindings: [holeSignature] :: BindingHole -> !Text
+ Keiro.Dsl.ExplainBindings: [holeSymbol] :: BindingHole -> !Text
+ Keiro.Dsl.ExplainBindings: [obligationBindingVersion] :: BindingObligation -> !Maybe Text
+ Keiro.Dsl.ExplainBindings: [obligationKind] :: BindingObligation -> !BindingObligationKind
+ Keiro.Dsl.ExplainBindings: [obligationMappedName] :: BindingObligation -> !Name
+ Keiro.Dsl.ExplainBindings: [obligationModule] :: BindingObligation -> !Text
+ Keiro.Dsl.ExplainBindings: [obligationPackage] :: BindingObligation -> !Text
+ Keiro.Dsl.ExplainBindings: [obligationSignature] :: BindingObligation -> !Text
+ Keiro.Dsl.ExplainBindings: [obligationSymbol] :: BindingObligation -> !Text
+ Keiro.Dsl.ExplainBindings: [obligationUseSites] :: BindingObligation -> ![Text]
+ Keiro.Dsl.ExplainBindings: bindingHoles :: Spec -> Either (NonEmpty TypeGraphError) [BindingHole]
+ Keiro.Dsl.ExplainBindings: bindingObligations :: Spec -> Either (NonEmpty TypeGraphError) [BindingObligation]
+ Keiro.Dsl.ExplainBindings: data BindingHole
+ Keiro.Dsl.ExplainBindings: data BindingObligation
+ Keiro.Dsl.ExplainBindings: data BindingObligationKind
+ Keiro.Dsl.ExplainBindings: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.ExplainBindings.BindingHole
+ Keiro.Dsl.ExplainBindings: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.ExplainBindings.BindingObligation
+ Keiro.Dsl.ExplainBindings: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ExplainBindings.BindingHole
+ Keiro.Dsl.ExplainBindings: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ExplainBindings.BindingObligation
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Eq Keiro.Dsl.ExplainBindings.BindingHole
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Eq Keiro.Dsl.ExplainBindings.BindingObligation
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Eq Keiro.Dsl.ExplainBindings.BindingObligationKind
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Ord Keiro.Dsl.ExplainBindings.BindingHole
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Ord Keiro.Dsl.ExplainBindings.BindingObligation
+ Keiro.Dsl.ExplainBindings: instance GHC.Classes.Ord Keiro.Dsl.ExplainBindings.BindingObligationKind
+ Keiro.Dsl.ExplainBindings: instance GHC.Internal.Show.Show Keiro.Dsl.ExplainBindings.BindingHole
+ Keiro.Dsl.ExplainBindings: instance GHC.Internal.Show.Show Keiro.Dsl.ExplainBindings.BindingObligation
+ Keiro.Dsl.ExplainBindings: instance GHC.Internal.Show.Show Keiro.Dsl.ExplainBindings.BindingObligationKind
+ Keiro.Dsl.ExplainBindings: renderBindingObligations :: Text -> [BindingObligation] -> Text
+ Keiro.Dsl.FoldFingerprint: aggregateFoldFingerprint :: Spec -> Aggregate -> Text
+ Keiro.Dsl.FoldFingerprint: aggregateFoldSurface :: Spec -> Aggregate -> Text
+ Keiro.Dsl.Goldens: FileOwnedFixture :: GoldenEvidence
+ Keiro.Dsl.Goldens: GoldenPayload :: !Text -> !Text -> !Text -> !Int -> !Text -> !GoldenEvidence -> GoldenPayload
+ Keiro.Dsl.Goldens: SynthesizedWeakStandIn :: GoldenEvidence
+ Keiro.Dsl.Goldens: [goldenAggregate] :: GoldenPayload -> !Text
+ Keiro.Dsl.Goldens: [goldenContext] :: GoldenPayload -> !Text
+ Keiro.Dsl.Goldens: [goldenEvent] :: GoldenPayload -> !Text
+ Keiro.Dsl.Goldens: [goldenEvidence] :: GoldenPayload -> !GoldenEvidence
+ Keiro.Dsl.Goldens: [goldenJson] :: GoldenPayload -> !Text
+ Keiro.Dsl.Goldens: [goldenVersion] :: GoldenPayload -> !Int
+ Keiro.Dsl.Goldens: data GoldenEvidence
+ Keiro.Dsl.Goldens: data GoldenPayload
+ Keiro.Dsl.Goldens: emitGoldenPayloads :: FilePath -> Spec -> Spec -> IO [FilePath]
+ Keiro.Dsl.Goldens: goldenRelativePath :: GoldenPayload -> FilePath
+ Keiro.Dsl.Goldens: goldensForDiff :: Spec -> Spec -> [GoldenPayload]
+ Keiro.Dsl.Goldens: instance GHC.Classes.Eq Keiro.Dsl.Goldens.GoldenEvidence
+ Keiro.Dsl.Goldens: instance GHC.Classes.Eq Keiro.Dsl.Goldens.GoldenPayload
+ Keiro.Dsl.Goldens: instance GHC.Internal.Show.Show Keiro.Dsl.Goldens.GoldenEvidence
+ Keiro.Dsl.Goldens: instance GHC.Internal.Show.Show Keiro.Dsl.Goldens.GoldenPayload
+ Keiro.Dsl.Goldens: loadGoldenPayloads :: FilePath -> Spec -> IO [GoldenPayload]
+ Keiro.Dsl.Grammar: HaskellSource :: !Text -> !Text -> !Name -> HaskellSource
+ Keiro.Dsl.Grammar: IgnoreUnknown :: UnknownFields
+ Keiro.Dsl.Grammar: MappedOpaque :: !Name -> !Maybe HaskellSource -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Loc -> MappedDecl
+ Keiro.Dsl.Grammar: MappedStructural :: !Name -> !Maybe HaskellSource -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe Text -> !MappedShape -> !Loc -> MappedDecl
+ Keiro.Dsl.Grammar: OmBool :: !Bool -> OnMissing
+ Keiro.Dsl.Grammar: OmCtor :: !Name -> OnMissing
+ Keiro.Dsl.Grammar: OmEmptyList :: OnMissing
+ Keiro.Dsl.Grammar: OmEmptyMap :: OnMissing
+ Keiro.Dsl.Grammar: OmInt :: !Integer -> OnMissing
+ Keiro.Dsl.Grammar: OmNull :: OnMissing
+ Keiro.Dsl.Grammar: OmText :: !Text -> OnMissing
+ Keiro.Dsl.Grammar: POptional :: Presence
+ Keiro.Dsl.Grammar: PRequired :: Presence
+ Keiro.Dsl.Grammar: RejectUnknown :: UnknownFields
+ Keiro.Dsl.Grammar: ShapeEnum :: ![WireEnum] -> MappedShape
+ Keiro.Dsl.Grammar: ShapeRecord :: !Name -> !UnknownFields -> ![WireField] -> MappedShape
+ Keiro.Dsl.Grammar: ShapeUnion :: !UnionEncoding -> ![WireArm] -> MappedShape
+ Keiro.Dsl.Grammar: TBool :: TypeExpr
+ Keiro.Dsl.Grammar: TInt :: TypeExpr
+ Keiro.Dsl.Grammar: TJson :: TypeExpr
+ Keiro.Dsl.Grammar: TList :: !TypeExpr -> TypeExpr
+ Keiro.Dsl.Grammar: TMap :: !TypeExpr -> TypeExpr
+ Keiro.Dsl.Grammar: TNatural :: TypeExpr
+ Keiro.Dsl.Grammar: TOptional :: !TypeExpr -> TypeExpr
+ Keiro.Dsl.Grammar: TRef :: !Name -> TypeExpr
+ Keiro.Dsl.Grammar: TText :: TypeExpr
+ Keiro.Dsl.Grammar: TTime :: TypeExpr
+ Keiro.Dsl.Grammar: TaggedObject :: !Text -> !Text -> !UnknownFields -> UnionEncoding
+ Keiro.Dsl.Grammar: TmLive :: TransitionMode
+ Keiro.Dsl.Grammar: TmReplayOnly :: TransitionMode
+ Keiro.Dsl.Grammar: WireArm :: !Name -> !Text -> !Maybe TypeExpr -> !Loc -> WireArm
+ Keiro.Dsl.Grammar: WireEnum :: !Name -> !Text -> !Loc -> WireEnum
+ Keiro.Dsl.Grammar: WireField :: !Name -> !Text -> !TypeExpr -> !Presence -> !Maybe OnMissing -> !Loc -> WireField
+ Keiro.Dsl.Grammar: [evRetiring] :: Event -> !Bool
+ Keiro.Dsl.Grammar: [hsModule] :: HaskellSource -> !Text
+ Keiro.Dsl.Grammar: [hsPackage] :: HaskellSource -> !Text
+ Keiro.Dsl.Grammar: [hsType] :: HaskellSource -> !Name
+ Keiro.Dsl.Grammar: [moCodecId] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [moCodecVersion] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [moFixtures] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [moHaskell] :: MappedDecl -> !Maybe HaskellSource
+ Keiro.Dsl.Grammar: [moInitial] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [moLoc] :: MappedDecl -> !Loc
+ Keiro.Dsl.Grammar: [moName] :: MappedDecl -> !Name
+ Keiro.Dsl.Grammar: [msBindingVersion] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [msBinding] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [msCanonical] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [msFixtures] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [msHaskell] :: MappedDecl -> !Maybe HaskellSource
+ Keiro.Dsl.Grammar: [msInitial] :: MappedDecl -> !Maybe Text
+ Keiro.Dsl.Grammar: [msLoc] :: MappedDecl -> !Loc
+ Keiro.Dsl.Grammar: [msName] :: MappedDecl -> !Name
+ Keiro.Dsl.Grammar: [msShape] :: MappedDecl -> !MappedShape
+ Keiro.Dsl.Grammar: [specMapped] :: Spec -> ![MappedDecl]
+ Keiro.Dsl.Grammar: [tMode] :: Transition -> !TransitionMode
+ Keiro.Dsl.Grammar: [ueContentsField] :: UnionEncoding -> !Text
+ Keiro.Dsl.Grammar: [ueTagField] :: UnionEncoding -> !Text
+ Keiro.Dsl.Grammar: [ueUnknownFields] :: UnionEncoding -> !UnknownFields
+ Keiro.Dsl.Grammar: [waCtor] :: WireArm -> !Name
+ Keiro.Dsl.Grammar: [waLoc] :: WireArm -> !Loc
+ Keiro.Dsl.Grammar: [waPayload] :: WireArm -> !Maybe TypeExpr
+ Keiro.Dsl.Grammar: [waTag] :: WireArm -> !Text
+ Keiro.Dsl.Grammar: [weCtor] :: WireEnum -> !Name
+ Keiro.Dsl.Grammar: [weLoc] :: WireEnum -> !Loc
+ Keiro.Dsl.Grammar: [weTag] :: WireEnum -> !Text
+ Keiro.Dsl.Grammar: [wfHaskell] :: WireField -> !Name
+ Keiro.Dsl.Grammar: [wfKey] :: WireField -> !Text
+ Keiro.Dsl.Grammar: [wfOnMissing] :: WireField -> !Maybe OnMissing
+ Keiro.Dsl.Grammar: [wfPresence] :: WireField -> !Presence
+ Keiro.Dsl.Grammar: [wfType] :: WireField -> !TypeExpr
+ Keiro.Dsl.Grammar: complementExpr :: Expr -> Expr
+ Keiro.Dsl.Grammar: data HaskellSource
+ Keiro.Dsl.Grammar: data MappedDecl
+ Keiro.Dsl.Grammar: data MappedShape
+ Keiro.Dsl.Grammar: data OnMissing
+ Keiro.Dsl.Grammar: data Presence
+ Keiro.Dsl.Grammar: data TransitionMode
+ Keiro.Dsl.Grammar: data TypeExpr
+ Keiro.Dsl.Grammar: data UnionEncoding
+ Keiro.Dsl.Grammar: data UnknownFields
+ Keiro.Dsl.Grammar: data WireArm
+ Keiro.Dsl.Grammar: data WireEnum
+ Keiro.Dsl.Grammar: data WireField
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.HaskellSource
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.MappedDecl
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.MappedShape
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.OnMissing
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.Presence
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.TransitionMode
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.TypeExpr
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.UnionEncoding
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.UnknownFields
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.WireArm
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.WireEnum
+ Keiro.Dsl.Grammar: instance GHC.Classes.Eq Keiro.Dsl.Grammar.WireField
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.HaskellSource
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.MappedDecl
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.MappedShape
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.OnMissing
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.Presence
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.TransitionMode
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.TypeExpr
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.UnionEncoding
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.UnknownFields
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.WireArm
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.WireEnum
+ Keiro.Dsl.Grammar: instance GHC.Internal.Generics.Generic Keiro.Dsl.Grammar.WireField
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.HaskellSource
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.MappedDecl
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.MappedShape
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.OnMissing
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.Presence
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.TransitionMode
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.TypeExpr
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.UnionEncoding
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.UnknownFields
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.WireArm
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.WireEnum
+ Keiro.Dsl.Grammar: instance GHC.Internal.Show.Show Keiro.Dsl.Grammar.WireField
+ Keiro.Dsl.Grammar: wireFieldLoc :: WireField -> Loc
+ Keiro.Dsl.Grammar: workflowNodeLoc :: WorkflowNode -> Loc
+ Keiro.Dsl.Harness: harnessForWithGoldens :: [GoldenPayload] -> Context -> Spec -> Aggregate -> [ScaffoldModule]
+ Keiro.Dsl.MappedConsumer: ConsumerPlan :: ![Text] -> ![Text] -> ![MappingIdentity] -> ConsumerPlan
+ Keiro.Dsl.MappedConsumer: OpaqueMapping :: !Text -> !Text -> !Text -> !Text -> !Text -> !Text -> MappingIdentity
+ Keiro.Dsl.MappedConsumer: StructuralMapping :: !Text -> !Text -> !Text -> !Text -> !Text -> !Text -> !Text -> MappingIdentity
+ Keiro.Dsl.MappedConsumer: [consumerMappings] :: ConsumerPlan -> ![MappingIdentity]
+ Keiro.Dsl.MappedConsumer: [consumerModules] :: ConsumerPlan -> ![Text]
+ Keiro.Dsl.MappedConsumer: [consumerPackages] :: ConsumerPlan -> ![Text]
+ Keiro.Dsl.MappedConsumer: [mappingBindingSymbol] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingBindingVersion] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingCanonicalType] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingCodecIdentity] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingCodecVersion] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingModule] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingPackage] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingSpecName] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: [mappingType] :: MappingIdentity -> !Text
+ Keiro.Dsl.MappedConsumer: consumerPlan :: Spec -> ConsumerPlan
+ Keiro.Dsl.MappedConsumer: data ConsumerPlan
+ Keiro.Dsl.MappedConsumer: data MappingIdentity
+ Keiro.Dsl.MappedConsumer: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.MappedConsumer.MappingIdentity
+ Keiro.Dsl.MappedConsumer: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.MappedConsumer.MappingIdentity
+ Keiro.Dsl.MappedConsumer: instance GHC.Classes.Eq Keiro.Dsl.MappedConsumer.ConsumerPlan
+ Keiro.Dsl.MappedConsumer: instance GHC.Classes.Eq Keiro.Dsl.MappedConsumer.MappingIdentity
+ Keiro.Dsl.MappedConsumer: instance GHC.Internal.Show.Show Keiro.Dsl.MappedConsumer.ConsumerPlan
+ Keiro.Dsl.MappedConsumer: instance GHC.Internal.Show.Show Keiro.Dsl.MappedConsumer.MappingIdentity
+ Keiro.Dsl.MappedDiff: MappedFinding :: !Name -> !Text -> !DiagnosticCode -> !Text -> ![UsePath] -> !Maybe UnknownFields -> MappedFinding
+ Keiro.Dsl.MappedDiff: [mfCode] :: MappedFinding -> !DiagnosticCode
+ Keiro.Dsl.MappedDiff: [mfDeclaration] :: MappedFinding -> !Name
+ Keiro.Dsl.MappedDiff: [mfDetail] :: MappedFinding -> !Text
+ Keiro.Dsl.MappedDiff: [mfLeaf] :: MappedFinding -> !Text
+ Keiro.Dsl.MappedDiff: [mfOldUnknownFields] :: MappedFinding -> !Maybe UnknownFields
+ Keiro.Dsl.MappedDiff: [mfUsePaths] :: MappedFinding -> ![UsePath]
+ Keiro.Dsl.MappedDiff: data MappedFinding
+ Keiro.Dsl.MappedDiff: diffMapped :: Spec -> Spec -> [MappedFinding]
+ Keiro.Dsl.MappedDiff: instance GHC.Classes.Eq Keiro.Dsl.MappedDiff.ExprView
+ Keiro.Dsl.MappedDiff: instance GHC.Classes.Eq Keiro.Dsl.MappedDiff.MappedFinding
+ Keiro.Dsl.MappedDiff: instance GHC.Internal.Show.Show Keiro.Dsl.MappedDiff.ExprView
+ Keiro.Dsl.MappedDiff: instance GHC.Internal.Show.Show Keiro.Dsl.MappedDiff.MappedFinding
+ Keiro.Dsl.MappedDiff: renderMappedSubject :: UsePath -> Text -> Text
+ Keiro.Dsl.PrettyPrint: renderExpr :: Expr -> Text
+ Keiro.Dsl.PrettyPrint: renderHandleSurface :: HandleNode -> Text
+ Keiro.Dsl.PrettyPrint: renderResolveSurface :: ResolveDecl -> Text
+ Keiro.Dsl.PrettyPrint: renderRouterDispatchSurface :: RouterDispatchNode -> Text
+ Keiro.Dsl.PrettyPrint: renderTimerPayloadSurface :: TimerNode -> Text
+ Keiro.Dsl.PrettyPrint: renderTransition :: Transition -> Text
+ Keiro.Dsl.ReadModelShape: fnv1a64 :: Text -> Text
+ Keiro.Dsl.ReplayImpact: AggregateImpact :: !Set Name -> !Bool -> AggregateImpact
+ Keiro.Dsl.ReplayImpact: ReplayAffected :: !Map Name AggregateImpact -> ReplayImpact
+ Keiro.Dsl.ReplayImpact: ReplayNeutral :: ReplayImpact
+ Keiro.Dsl.ReplayImpact: [eventTypes] :: AggregateImpact -> !Set Name
+ Keiro.Dsl.ReplayImpact: [includeSnapshotStreams] :: AggregateImpact -> !Bool
+ Keiro.Dsl.ReplayImpact: data AggregateImpact
+ Keiro.Dsl.ReplayImpact: data ReplayImpact
+ Keiro.Dsl.ReplayImpact: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ReplayImpact.AggregateImpact
+ Keiro.Dsl.ReplayImpact: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ReplayImpact.ReplayImpact
+ Keiro.Dsl.ReplayImpact: instance GHC.Classes.Eq Keiro.Dsl.ReplayImpact.AggregateImpact
+ Keiro.Dsl.ReplayImpact: instance GHC.Classes.Eq Keiro.Dsl.ReplayImpact.ReplayImpact
+ Keiro.Dsl.ReplayImpact: instance GHC.Internal.Show.Show Keiro.Dsl.ReplayImpact.AggregateImpact
+ Keiro.Dsl.ReplayImpact: instance GHC.Internal.Show.Show Keiro.Dsl.ReplayImpact.ReplayImpact
+ Keiro.Dsl.ReplayImpact: renderReplayImpact :: ReplayImpact -> Text
+ Keiro.Dsl.ReplayImpact: replayImpact :: Spec -> Spec -> ReplayImpact
+ Keiro.Dsl.Scaffold: MappedOpaqueCat :: !OpaqueDecl -> FieldCat
+ Keiro.Dsl.Scaffold: MappedStructuralCat :: !StructuralDecl -> !ResolvedMappedShape -> FieldCat
+ Keiro.Dsl.Scaffold: StructuralProjection :: !Text -> !Text -> !Text -> !HaskellSource -> !Text -> !CanonicalTypeId -> !QualifiedValueName -> ![(Text, Text)] -> StructuralProjection
+ Keiro.Dsl.Scaffold: [aFoldFingerprint] :: Agg -> !Text
+ Keiro.Dsl.Scaffold: [aTypeGraph] :: Agg -> !Maybe TypeGraph
+ Keiro.Dsl.Scaffold: [spBinding] :: StructuralProjection -> !QualifiedValueName
+ Keiro.Dsl.Scaffold: [spCanonical] :: StructuralProjection -> !CanonicalTypeId
+ Keiro.Dsl.Scaffold: [spOwner] :: StructuralProjection -> !HaskellSource
+ Keiro.Dsl.Scaffold: [spPointer] :: StructuralProjection -> !Text
+ Keiro.Dsl.Scaffold: [spResult] :: StructuralProjection -> !Text
+ Keiro.Dsl.Scaffold: [spSelectors] :: StructuralProjection -> ![(Text, Text)]
+ Keiro.Dsl.Scaffold: [spTag] :: StructuralProjection -> !Text
+ Keiro.Dsl.Scaffold: [spWitness] :: StructuralProjection -> !Text
+ Keiro.Dsl.Scaffold: bindingSkeletonModules :: Context -> Spec -> TypeGraph -> [ScaffoldModule]
+ Keiro.Dsl.Scaffold: codecComparisonBanner :: Text
+ Keiro.Dsl.Scaffold: codecComparisonModule :: Context -> Spec -> Name -> Either Text ScaffoldModule
+ Keiro.Dsl.Scaffold: codecMappedDeclarations :: Agg -> [ResolvedMappedDecl]
+ Keiro.Dsl.Scaffold: data StructuralProjection
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Eq Keiro.Dsl.Scaffold.FieldCat
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Eq Keiro.Dsl.Scaffold.ShapeRequirement
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Eq Keiro.Dsl.Scaffold.StructuralProjection
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Ord Keiro.Dsl.Scaffold.ShapeRequirement
+ Keiro.Dsl.Scaffold: instance GHC.Internal.Show.Show Keiro.Dsl.Scaffold.FieldCat
+ Keiro.Dsl.Scaffold: instance GHC.Internal.Show.Show Keiro.Dsl.Scaffold.ShapeRequirement
+ Keiro.Dsl.Scaffold: instance GHC.Internal.Show.Show Keiro.Dsl.Scaffold.StructuralProjection
+ Keiro.Dsl.Scaffold: projectionSpecs :: TypeGraph -> [StructuralProjection]
+ Keiro.Dsl.Scaffold: resolveProjectionModules :: Context -> StructuralProjection -> StructuralProjection
+ Keiro.Dsl.Scaffold: scaffoldReplayAudit :: Context -> Spec -> [ScaffoldModule]
+ Keiro.Dsl.Scaffold: scaffoldStructural :: Context -> Spec -> [ScaffoldModule]
+ Keiro.Dsl.ScaffoldRecord: [recBindingObligations] :: ScaffoldRecord -> ![BindingHole]
+ Keiro.Dsl.ScaffoldRecord: [recMappings] :: ScaffoldRecord -> ![MappingIdentity]
+ Keiro.Dsl.ScaffoldRun: ImportCycle :: ![Text] -> Refusal
+ Keiro.Dsl.ScaffoldRun: MappingDrift :: !Text -> !Maybe MappingIdentity -> !Maybe MappingIdentity -> MappingDrift
+ Keiro.Dsl.ScaffoldRun: [driftCurrent] :: MappingDrift -> !Maybe MappingIdentity
+ Keiro.Dsl.ScaffoldRun: [driftPrevious] :: MappingDrift -> !Maybe MappingIdentity
+ Keiro.Dsl.ScaffoldRun: [driftSpecName] :: MappingDrift -> !Text
+ Keiro.Dsl.ScaffoldRun: [reportConstraintPlan] :: ScaffoldReport -> ![Text]
+ Keiro.Dsl.ScaffoldRun: [reportConsumerPlan] :: ScaffoldReport -> !ConsumerPlan
+ Keiro.Dsl.ScaffoldRun: [reportMappingDrift] :: ScaffoldReport -> ![MappingDrift]
+ Keiro.Dsl.ScaffoldRun: [reportNewHoles] :: ScaffoldReport -> ![BindingHole]
+ Keiro.Dsl.ScaffoldRun: data MappingDrift
+ Keiro.Dsl.ScaffoldRun: instance GHC.Classes.Eq Keiro.Dsl.ScaffoldRun.MappingDrift
+ Keiro.Dsl.ScaffoldRun: instance GHC.Internal.Show.Show Keiro.Dsl.ScaffoldRun.MappingDrift
+ Keiro.Dsl.ScaffoldRun: planScaffoldWithGoldens :: [GoldenPayload] -> Context -> Spec -> Either [Refusal] [ScaffoldModule]
+ Keiro.Dsl.ScaffoldRun: scaffoldModulesWithGoldens :: [GoldenPayload] -> Context -> Spec -> [ScaffoldModule]
+ Keiro.Dsl.TypeGraph: BindingVersion :: Text -> BindingVersion
+ Keiro.Dsl.TypeGraph: CanonicalTypeId :: Text -> CanonicalTypeId
+ Keiro.Dsl.TypeGraph: CheckedOpaque :: !OpaqueDecl -> CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: CheckedStructural :: !StructuralDecl -> !MappedShape -> CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: CodecIdentity :: Text -> CodecIdentity
+ Keiro.Dsl.TypeGraph: CodecVersion :: Text -> CodecVersion
+ Keiro.Dsl.TypeGraph: EmptyBindingVersion :: !Text -> MappedDeclError
+ Keiro.Dsl.TypeGraph: EmptyCanonicalTypeId :: !Text -> MappedDeclError
+ Keiro.Dsl.TypeGraph: EmptyCodecIdentity :: !Text -> MappedDeclError
+ Keiro.Dsl.TypeGraph: EmptyCodecVersion :: !Text -> MappedDeclError
+ Keiro.Dsl.TypeGraph: EmptyQualifiedValueName :: !Text -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MappedDeclAlgebra :: (StructuralDecl -> ResolvedMappedShape -> a) -> (OpaqueDecl -> a) -> MappedDeclAlgebra a
+ Keiro.Dsl.TypeGraph: MappedKey :: Name -> MappedKey
+ Keiro.Dsl.TypeGraph: MappedShapeAlgebra :: (Name -> UnknownFields -> [ResolvedWireField] -> a) -> ([WireEnum] -> a) -> (UnionEncoding -> [ResolvedWireArm] -> a) -> MappedShapeAlgebra a
+ Keiro.Dsl.TypeGraph: MissingCanonicalType :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingFixtureCases :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingHaskellSource :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingOpaqueCodecIdentity :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingOpaqueCodecVersion :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingStructuralBinding :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: MissingStructuralBindingVersion :: !Name -> MappedDeclError
+ Keiro.Dsl.TypeGraph: OpaqueDecl :: !Name -> !HaskellSource -> !CodecIdentity -> !CodecVersion -> !QualifiedValueName -> !Maybe QualifiedValueName -> !Loc -> OpaqueDecl
+ Keiro.Dsl.TypeGraph: QualifiedValueName :: Text -> QualifiedValueName
+ Keiro.Dsl.TypeGraph: RBool :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: REnum :: ![WireEnum] -> ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: RInt :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RJson :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RList :: !ResolvedTypeExpr -> ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RMap :: !ResolvedTypeExpr -> ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RNatural :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: ROptional :: !ResolvedTypeExpr -> ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RRecord :: !Name -> !UnknownFields -> ![ResolvedWireField] -> ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: RRef :: !MappedKey -> ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RText :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RTime :: ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: RUnion :: !UnionEncoding -> ![ResolvedWireArm] -> ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: ResolvedOpaque :: !OpaqueDecl -> ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: ResolvedStructural :: !StructuralDecl -> !ResolvedMappedShape -> ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: ResolvedWireArm :: !Name -> !Text -> !Maybe ResolvedTypeExpr -> !Loc -> ResolvedWireArm
+ Keiro.Dsl.TypeGraph: ResolvedWireField :: !Name -> !Text -> !ResolvedTypeExpr -> !Presence -> !Maybe OnMissing -> !Loc -> ResolvedWireField
+ Keiro.Dsl.TypeGraph: RootCommandField :: !Name -> !Name -> !Name -> !MappedKey -> UseSite
+ Keiro.Dsl.TypeGraph: RootEventField :: !Name -> !Name -> !Name -> !MappedKey -> UseSite
+ Keiro.Dsl.TypeGraph: RootRegister :: !Name -> !Name -> !MappedKey -> UseSite
+ Keiro.Dsl.TypeGraph: SegArm :: !Name -> !Text -> PathSeg
+ Keiro.Dsl.TypeGraph: SegDecl :: !Name -> PathSeg
+ Keiro.Dsl.TypeGraph: SegElem :: PathSeg
+ Keiro.Dsl.TypeGraph: SegField :: !Name -> !Text -> PathSeg
+ Keiro.Dsl.TypeGraph: SegMapValue :: PathSeg
+ Keiro.Dsl.TypeGraph: SegOptional :: PathSeg
+ Keiro.Dsl.TypeGraph: StructuralDecl :: !Name -> !HaskellSource -> !QualifiedValueName -> !BindingVersion -> !CanonicalTypeId -> !QualifiedValueName -> !Maybe QualifiedValueName -> !Loc -> StructuralDecl
+ Keiro.Dsl.TypeGraph: TGAmbiguousName :: !Name -> ![Text] -> TypeGraphError
+ Keiro.Dsl.TypeGraph: TGDeclError :: !Name -> !MappedDeclError -> TypeGraphError
+ Keiro.Dsl.TypeGraph: TGRecursive :: ![Name] -> TypeGraphError
+ Keiro.Dsl.TypeGraph: TGUnresolvedRef :: !Name -> !Name -> !Loc -> TypeGraphError
+ Keiro.Dsl.TypeGraph: TypeExprAlgebra :: a -> a -> a -> a -> a -> a -> (a -> a) -> (a -> a) -> (a -> a) -> (MappedKey -> a) -> TypeExprAlgebra a
+ Keiro.Dsl.TypeGraph: TypeGraph :: !Map MappedKey ResolvedMappedDecl -> !Map MappedKey (Set MappedKey) -> ![UseSite] -> TypeGraph
+ Keiro.Dsl.TypeGraph: UsePath :: !UseSite -> ![PathSeg] -> UsePath
+ Keiro.Dsl.TypeGraph: [odCodecIdentity] :: OpaqueDecl -> !CodecIdentity
+ Keiro.Dsl.TypeGraph: [odCodecVersion] :: OpaqueDecl -> !CodecVersion
+ Keiro.Dsl.TypeGraph: [odFixtures] :: OpaqueDecl -> !QualifiedValueName
+ Keiro.Dsl.TypeGraph: [odHaskell] :: OpaqueDecl -> !HaskellSource
+ Keiro.Dsl.TypeGraph: [odInitial] :: OpaqueDecl -> !Maybe QualifiedValueName
+ Keiro.Dsl.TypeGraph: [odLoc] :: OpaqueDecl -> !Loc
+ Keiro.Dsl.TypeGraph: [odName] :: OpaqueDecl -> !Name
+ Keiro.Dsl.TypeGraph: [onBool] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onEnum] :: MappedShapeAlgebra a -> [WireEnum] -> a
+ Keiro.Dsl.TypeGraph: [onInt] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onJson] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onList] :: TypeExprAlgebra a -> a -> a
+ Keiro.Dsl.TypeGraph: [onMap] :: TypeExprAlgebra a -> a -> a
+ Keiro.Dsl.TypeGraph: [onNatural] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onOpaqueDecl] :: MappedDeclAlgebra a -> OpaqueDecl -> a
+ Keiro.Dsl.TypeGraph: [onOptional] :: TypeExprAlgebra a -> a -> a
+ Keiro.Dsl.TypeGraph: [onRecord] :: MappedShapeAlgebra a -> Name -> UnknownFields -> [ResolvedWireField] -> a
+ Keiro.Dsl.TypeGraph: [onRef] :: TypeExprAlgebra a -> MappedKey -> a
+ Keiro.Dsl.TypeGraph: [onStructuralDecl] :: MappedDeclAlgebra a -> StructuralDecl -> ResolvedMappedShape -> a
+ Keiro.Dsl.TypeGraph: [onText] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onTime] :: TypeExprAlgebra a -> a
+ Keiro.Dsl.TypeGraph: [onUnion] :: MappedShapeAlgebra a -> UnionEncoding -> [ResolvedWireArm] -> a
+ Keiro.Dsl.TypeGraph: [rwaCtor] :: ResolvedWireArm -> !Name
+ Keiro.Dsl.TypeGraph: [rwaLoc] :: ResolvedWireArm -> !Loc
+ Keiro.Dsl.TypeGraph: [rwaPayload] :: ResolvedWireArm -> !Maybe ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: [rwaTag] :: ResolvedWireArm -> !Text
+ Keiro.Dsl.TypeGraph: [rwfHaskell] :: ResolvedWireField -> !Name
+ Keiro.Dsl.TypeGraph: [rwfKey] :: ResolvedWireField -> !Text
+ Keiro.Dsl.TypeGraph: [rwfLoc] :: ResolvedWireField -> !Loc
+ Keiro.Dsl.TypeGraph: [rwfOnMissing] :: ResolvedWireField -> !Maybe OnMissing
+ Keiro.Dsl.TypeGraph: [rwfPresence] :: ResolvedWireField -> !Presence
+ Keiro.Dsl.TypeGraph: [rwfType] :: ResolvedWireField -> !ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: [sdBindingVersion] :: StructuralDecl -> !BindingVersion
+ Keiro.Dsl.TypeGraph: [sdBinding] :: StructuralDecl -> !QualifiedValueName
+ Keiro.Dsl.TypeGraph: [sdCanonical] :: StructuralDecl -> !CanonicalTypeId
+ Keiro.Dsl.TypeGraph: [sdFixtures] :: StructuralDecl -> !QualifiedValueName
+ Keiro.Dsl.TypeGraph: [sdHaskell] :: StructuralDecl -> !HaskellSource
+ Keiro.Dsl.TypeGraph: [sdInitial] :: StructuralDecl -> !Maybe QualifiedValueName
+ Keiro.Dsl.TypeGraph: [sdLoc] :: StructuralDecl -> !Loc
+ Keiro.Dsl.TypeGraph: [sdName] :: StructuralDecl -> !Name
+ Keiro.Dsl.TypeGraph: [tgDeclarations] :: TypeGraph -> !Map MappedKey ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: [tgReachability] :: TypeGraph -> !Map MappedKey (Set MappedKey)
+ Keiro.Dsl.TypeGraph: [tgUseSites] :: TypeGraph -> ![UseSite]
+ Keiro.Dsl.TypeGraph: [unBindingVersion] :: BindingVersion -> Text
+ Keiro.Dsl.TypeGraph: [unCanonicalTypeId] :: CanonicalTypeId -> Text
+ Keiro.Dsl.TypeGraph: [unCodecIdentity] :: CodecIdentity -> Text
+ Keiro.Dsl.TypeGraph: [unCodecVersion] :: CodecVersion -> Text
+ Keiro.Dsl.TypeGraph: [unMappedKey] :: MappedKey -> Name
+ Keiro.Dsl.TypeGraph: [unQualifiedValueName] :: QualifiedValueName -> Text
+ Keiro.Dsl.TypeGraph: [upRoot] :: UsePath -> !UseSite
+ Keiro.Dsl.TypeGraph: [upSegments] :: UsePath -> ![PathSeg]
+ Keiro.Dsl.TypeGraph: checkMappedDecl :: MappedDecl -> Either (NonEmpty MappedDeclError) CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: data CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: data MappedDeclAlgebra a
+ Keiro.Dsl.TypeGraph: data MappedDeclError
+ Keiro.Dsl.TypeGraph: data MappedShapeAlgebra a
+ Keiro.Dsl.TypeGraph: data OpaqueDecl
+ Keiro.Dsl.TypeGraph: data PathSeg
+ Keiro.Dsl.TypeGraph: data ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: data ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: data ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: data ResolvedWireArm
+ Keiro.Dsl.TypeGraph: data ResolvedWireField
+ Keiro.Dsl.TypeGraph: data StructuralDecl
+ Keiro.Dsl.TypeGraph: data TypeExprAlgebra a
+ Keiro.Dsl.TypeGraph: data TypeGraph
+ Keiro.Dsl.TypeGraph: data TypeGraphError
+ Keiro.Dsl.TypeGraph: data UsePath
+ Keiro.Dsl.TypeGraph: data UseSite
+ Keiro.Dsl.TypeGraph: foldMappedDecl :: MappedDeclAlgebra a -> ResolvedMappedDecl -> a
+ Keiro.Dsl.TypeGraph: foldMappedShape :: MappedShapeAlgebra a -> ResolvedMappedShape -> a
+ Keiro.Dsl.TypeGraph: foldTypeExpr :: TypeExprAlgebra a -> ResolvedTypeExpr -> a
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.BindingVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.CanonicalTypeId
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.CodecIdentity
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.CodecVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.MappedDeclError
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.MappedKey
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.OpaqueDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.PathSeg
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.QualifiedValueName
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.ResolvedWireArm
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.ResolvedWireField
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.StructuralDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.TypeGraph
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.TypeGraphError
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.UsePath
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Eq Keiro.Dsl.TypeGraph.UseSite
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.BindingVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.CanonicalTypeId
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.CodecIdentity
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.CodecVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.MappedKey
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.PathSeg
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.QualifiedValueName
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.UsePath
+ Keiro.Dsl.TypeGraph: instance GHC.Classes.Ord Keiro.Dsl.TypeGraph.UseSite
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.BindingVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.CanonicalTypeId
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.CodecIdentity
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.CodecVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.MappedDeclError
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.MappedKey
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.OpaqueDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.PathSeg
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.QualifiedValueName
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.ResolvedWireArm
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.ResolvedWireField
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.StructuralDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.TypeGraph
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.TypeGraphError
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.UsePath
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Generics.Generic Keiro.Dsl.TypeGraph.UseSite
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.BindingVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.CanonicalTypeId
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.CheckedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.CodecIdentity
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.CodecVersion
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.MappedDeclError
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.MappedKey
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.OpaqueDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.PathSeg
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.QualifiedValueName
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.ResolvedMappedDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.ResolvedMappedShape
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.ResolvedTypeExpr
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.ResolvedWireArm
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.ResolvedWireField
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.StructuralDecl
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.TypeGraph
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.TypeGraphError
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.UsePath
+ Keiro.Dsl.TypeGraph: instance GHC.Internal.Show.Show Keiro.Dsl.TypeGraph.UseSite
+ Keiro.Dsl.TypeGraph: mkBindingVersion :: Text -> Either MappedDeclError BindingVersion
+ Keiro.Dsl.TypeGraph: mkCanonicalTypeId :: Text -> Either MappedDeclError CanonicalTypeId
+ Keiro.Dsl.TypeGraph: mkCodecIdentity :: Text -> Either MappedDeclError CodecIdentity
+ Keiro.Dsl.TypeGraph: mkCodecVersion :: Text -> Either MappedDeclError CodecVersion
+ Keiro.Dsl.TypeGraph: mkQualifiedValueName :: Text -> Either MappedDeclError QualifiedValueName
+ Keiro.Dsl.TypeGraph: newtype BindingVersion
+ Keiro.Dsl.TypeGraph: newtype CanonicalTypeId
+ Keiro.Dsl.TypeGraph: newtype CodecIdentity
+ Keiro.Dsl.TypeGraph: newtype CodecVersion
+ Keiro.Dsl.TypeGraph: newtype MappedKey
+ Keiro.Dsl.TypeGraph: newtype QualifiedValueName
+ Keiro.Dsl.TypeGraph: renderUsePath :: UsePath -> Text
+ Keiro.Dsl.TypeGraph: resolveTypeGraph :: Spec -> Either (NonEmpty TypeGraphError) TypeGraph
+ Keiro.Dsl.TypeGraph: usePaths :: TypeGraph -> Name -> [UsePath]
+ Keiro.Dsl.TypeGraph: wireFingerprint :: TypeGraph -> Name -> Text
+ Keiro.Dsl.Validate: AggFoldSurfaceChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: AggGuardTightened :: DiagnosticCode
+ Keiro.Dsl.Validate: CodecCompareCoverageGap :: DiagnosticCode
+ Keiro.Dsl.Validate: CodecCompareDifference :: DiagnosticCode
+ Keiro.Dsl.Validate: CodecCompareInvalidInput :: DiagnosticCode
+ Keiro.Dsl.Validate: CompatibilityStrengthened :: DiagnosticCode
+ Keiro.Dsl.Validate: ContractEventAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: ContractTopicAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: CoverageOpaqueBoundaryAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: CoverageOpaqueGateExceeded :: DiagnosticCode
+ Keiro.Dsl.Validate: CoverageOpaqueSurface :: DiagnosticCode
+ Keiro.Dsl.Validate: DeclarationAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: DeprecatedEventReplayHazard :: DiagnosticCode
+ Keiro.Dsl.Validate: DuplicateUpcasterSource :: DiagnosticCode
+ Keiro.Dsl.Validate: EnumCtorAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: EventRetirementAbandoned :: DiagnosticCode
+ Keiro.Dsl.Validate: EventRetirementInProgress :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedAmbiguousName :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedArmAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedArmRemoved :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedArmTagChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedBindingChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedCanonicalTypeChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDeclAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDeclRemoved :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDefaultChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDefaultIllTyped :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDefaultRemoved :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDuplicateArmName :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDuplicateFieldName :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDuplicateWireKey :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedDuplicateWireTag :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedEnumSpellingChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedEnumValueAdded :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedEnumValueRemoved :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedFieldAddedNoDefault :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedFieldAddedWithDefault :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedFieldRemoved :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedFieldTypeChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedFixturesChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedGuardUnsupported :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedHaskellSourceChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedImportConflict :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedInitialChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedInvalidHaskellName :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedInvalidIdentity :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedMissingIngredient :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedMissingInitialValue :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedModeCrossed :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedNonInjectiveNullability :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedNullabilityChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedOpaqueCodecChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedPresenceChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedRecordConstructorChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedRecursiveType :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedUnionEncodingChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedUnresolvedName :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedUnsupportedEncoding :: DiagnosticCode
+ Keiro.Dsl.Validate: MappedWireKeyChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: ProcessDecideSurfaceChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: ProcessTimerPayloadChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: ReplayOnlyCommandStillLive :: DiagnosticCode
+ Keiro.Dsl.Validate: ReplayOnlyEmitsNothing :: DiagnosticCode
+ Keiro.Dsl.Validate: RouterDecideSurfaceChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: UpcasterChainGap :: DiagnosticCode
+ Keiro.Dsl.Validate: VersionBumped :: DiagnosticCode
+ Keiro.Dsl.Validate: WorkflowEvolutionGuardAdded :: DiagnosticCode
- Keiro.Dsl.Diff: ChangeKind :: !Name -> !Text -> !Text -> !Maybe DiagnosticCode -> !Text -> ChangeKind
+ Keiro.Dsl.Diff: ChangeKind :: !Name -> !Text -> !Text -> !DiagnosticCode -> !ChangeContext -> !CompatibilityVector -> ![Text] -> !Text -> ChangeKind
- Keiro.Dsl.Diff: [ckCode] :: ChangeKind -> !Maybe DiagnosticCode
+ Keiro.Dsl.Diff: [ckCode] :: ChangeKind -> !DiagnosticCode
- Keiro.Dsl.Grammar: Event :: !Name -> !EventBody -> !Int -> !Maybe (Int, Hole) -> !Bool -> !Loc -> Event
+ Keiro.Dsl.Grammar: Event :: !Name -> !EventBody -> !Int -> !Maybe (Int, Hole) -> !Bool -> !Bool -> !Loc -> Event
- Keiro.Dsl.Grammar: Spec :: !Name -> !Maybe Text -> !Maybe Placement -> ![IdDecl] -> ![EnumDecl] -> ![RuleDecl] -> ![Node] -> Spec
+ Keiro.Dsl.Grammar: Spec :: !Name -> !Maybe Text -> !Maybe Placement -> ![IdDecl] -> ![EnumDecl] -> ![RuleDecl] -> ![MappedDecl] -> ![Node] -> Spec
- Keiro.Dsl.Grammar: Transition :: !Name -> !Name -> !Maybe Expr -> ![(Name, Expr)] -> ![Name] -> !Name -> !Loc -> Transition
+ Keiro.Dsl.Grammar: Transition :: !Name -> !Name -> !Maybe Expr -> ![(Name, Expr)] -> ![Name] -> !Name -> !TransitionMode -> !Loc -> Transition
- Keiro.Dsl.Scaffold: Agg :: !Context -> !Text -> !Text -> !Loc -> !Text -> ![IdDecl] -> ![EnumDecl] -> ![RegDecl] -> ![StateDecl] -> ![ResolvedCtor] -> ![ResolvedCtor] -> ![Transition] -> !WireSpec -> !Maybe ProjectionSpec -> !Maybe SnapshotSpec -> ![ReadModelNode] -> !Text -> !Text -> Agg
+ Keiro.Dsl.Scaffold: Agg :: !Context -> !Text -> !Text -> !Loc -> !Text -> ![IdDecl] -> ![EnumDecl] -> ![RegDecl] -> ![StateDecl] -> ![ResolvedCtor] -> ![ResolvedCtor] -> ![Transition] -> !WireSpec -> !Maybe ProjectionSpec -> !Maybe SnapshotSpec -> !Text -> ![ReadModelNode] -> !Maybe TypeGraph -> !Text -> !Text -> Agg
- Keiro.Dsl.ScaffoldRecord: ScaffoldRecord :: !Text -> !Text -> !Text -> ![(ModuleKind, FilePath)] -> ScaffoldRecord
+ Keiro.Dsl.ScaffoldRecord: ScaffoldRecord :: !Text -> !Text -> !Text -> ![(ModuleKind, FilePath)] -> ![MappingIdentity] -> ![BindingHole] -> ScaffoldRecord
- Keiro.Dsl.ScaffoldRun: ScaffoldReport :: !FilePath -> !FilePath -> !Context -> ![(ScaffoldModule, WriteDisposition)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> ScaffoldReport
+ Keiro.Dsl.ScaffoldRun: ScaffoldReport :: !FilePath -> !FilePath -> !Context -> ![(ScaffoldModule, WriteDisposition)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> !ConsumerPlan -> ![Text] -> ![MappingDrift] -> ![BindingHole] -> ScaffoldReport

Files

CHANGELOG.md view
@@ -6,7 +6,100 @@  ## [Unreleased] -_No unreleased changes._+## 0.4.0.1 — 2026-07-28++### Other Changes++- Adds PVP upper bounds to every dependency that previously carried a lower+  bound only, so `cabal check` reports no packaging warnings. No API or+  behaviour change from 0.4.0.0, which was tagged but never published.+++## 0.4.0.0 — 2026-07-28++### New Features++- Structural scaffolding now creates hand-owned binding/fixture/initial+  skeletons grouped by their declared owner modules, persists granular hole+  obligations for non-overwriting re-scaffold reports, derives `Generic` for+  private shape types, and provides `check --explain-bindings` for deterministic+  package/module/signature/use-site reports.++- Adds `Keiro.Dsl.CodecCompare`, a historical-codec comparison engine that+  classifies RFC 8785 canonical-JSON parity between a declared codec and a+  historical one, reports structured migration differences and declared-versus-+  observed branch coverage gaps, and writes stable reports atomically.+  `scaffold --codec-comparison MAPPED-NAME --comparison-out FILE` emits a+  non-production comparison module and runner for one structural mapped type.++- Adds `Keiro.Dsl.Coverage` and reporting-only `--coverage-report FILE` on both+  `check` and `diff`, recording structural and opaque mapped-root coverage as+  JSON. `check --fail-on-opaque` fails when a private persisted root still+  contains an opaque boundary; `diff --fail-on-opaque-increase` fails when a+  change adds a named opaque boundary. Without the flags, coverage is purely+  informational.++- Adds checked `mapped structural` and `mapped opaque` declarations with a+  resolved, total type-expression graph. Validation rejects recursive,+  ambiguous, non-injective, or incomplete mappings; recursive diff findings+  carry six-surface compatibility vectors and complete command/event/register+  use-site paths.++- Structural consumer-type scaffolding now emits private+  `Generated.<Context>.Structural.Shape.*` wire representations, generates+  structural codecs from declared keys/defaults/tags, delegates opaque values+  only at their declared JSON boundary, imports consumer types into aggregate+  domains, and exposes eligible total scalar getters through a narrow Keiki+  0.4 `StructuralProjections` witness facade.+- Scaffold preflight now reports consumer packages/modules, refuses consumer+  modules inside the generated namespace before writing, includes consumer+  requirements in manifests, persists canonical structural/opaque mapping+  identities as forward-compatible JSON rows, and reports mapping drift on+  subsequent runs. Mapped register wire/binding/initial identities now+  participate in the aggregate fold fingerprint.+- Generated harnesses exercise both structural binding laws, declared codec+  policy and current payload goldens, enum/union/optional fixture coverage,+  canonical projection witness agreement, and forward-versus-replay equality+  over every mapped and scalar register. The committed structural conformance+  suite includes opaque-boundary checks and three falsifying mutations.+- `Keiro.Dsl.ReplayImpact` and `diff --replay-impact-out FILE`. Diffs now+  print whether stored-data replay is unchanged and can emit a stable JSON+  affected set for targeted auditing. New aggregates, events, transitions,+  and syntactically proven guard loosenings are replay-neutral; changed+  decode/fold surfaces identify conservative event types and whether+  snapshot-bearing streams must be included.+- Scaffolding now emits one context-wide+  `Generated.<Context>.ReplayAudit` module. Its typed target list includes+  every aggregate, validates discovered stream names against the generated+  category, and documents the replay-neutral/targeted/full deployment tiers.+- `diff` now emits non-breaking, coded advisories when a router resolve or+  dispatch surface, process handle surface, or unversioned process-timer+  payload changes. The advisories explain the subscription drain and+  dead-letter procedure needed to avoid mixed old/new deterministic fan-out+  during a deployment.++- First-class replay-only transitions for guard evolution (plan 143). A+  `replay-only` prefix on a transition line marks it as serving inversion+  only: the parser accepts it, the pretty-printer round-trips it, and the+  scaffolder lowers it to `B.replayOnly` (keiki's `ReplayOnly` edge mode) in+  the transducer skeleton. New validator rules: `ReplayOnlyEmitsNothing`+  (error — a replay-only transition with no emit can invert nothing) and+  `ReplayOnlyCommandStillLive` (warning — no live sibling for the (source,+  command) pair; the fuller procedure is event retirement). A deprecated+  event may keep being emitted by a replay-only transition — replay-only+  transitions are not the write path.+- `diff` computes the guard-tightening remedy (`AggGuardTightened`+  advisory): on any live-transition guard change without a replay-only twin,+  it prints a paste-ready `replay-only` twin whose guard is the removed+  region `old ∧ ¬new`, negation eliminated inside the guard grammar by the+  new total `Keiro.Dsl.Grammar.complementExpr` (De Morgan, comparison+  flipping, `x == false` for bare boolean atoms). The twin carries the old+  transition's writes/emits/goto and re-parses as-is; it is printed, never+  auto-applied.+- `Keiro.Dsl.PrettyPrint.renderTransition` renders one transition in+  concrete `.keiro` syntax (used by the advisory).+- Requires `keiki >=0.4 && <0.5`, including `EdgeMode` and the typed structural+  projection contracts.  ## 0.3.0.0 — 2026-07-14 
app/Main.hs view
@@ -5,31 +5,47 @@ module Main (main) where  import Control.Monad (when)+import Data.Aeson qualified as Aeson import Data.Maybe (fromMaybe) import Data.Text qualified as T import Data.Text.IO qualified as TIO-import Keiro.Dsl.Diff (Change (..), ChangeKind (..), diffSpecs, isBreaking)+import Keiro.Dsl.Coverage qualified as Coverage+import Keiro.Dsl.Diff (Change (..), CompatibilitySurface, diffSpecs, gateWith, gatedBreaking)+import Keiro.Dsl.DiffReport (diffReport, parseSurfaceName, renderExplainBlock, renderFinding)+import Keiro.Dsl.ExplainBindings (bindingObligations, renderBindingObligations)+import Keiro.Dsl.Goldens (emitGoldenPayloads, loadGoldenPayloads) import Keiro.Dsl.Grammar (Placement (..), Spec (..)) import Keiro.Dsl.Parser (parseSpec) import Keiro.Dsl.PrettyPrint (renderSpec)-import Keiro.Dsl.Scaffold (Context (..))-import Keiro.Dsl.ScaffoldRun (executeScaffold, planScaffold, renderRefusals, renderScaffoldReport)+import Keiro.Dsl.ReplayImpact (renderReplayImpact, replayImpact)+import Keiro.Dsl.Scaffold (Context (..), ScaffoldModule (..), codecComparisonBanner, codecComparisonModule)+import Keiro.Dsl.ScaffoldRun (executeScaffold, planScaffoldWithGoldens, renderRefusals, renderScaffoldReport) import Keiro.Dsl.Skeleton (skeletonFor) import Keiro.Dsl.Validate (Diagnostic (..), Severity (..), renderDiagnostic, validateSpec) import Options.Applicative-import System.Directory (canonicalizePath)+import System.Directory (canonicalizePath, createDirectoryIfMissing, doesFileExist) import System.Exit (ExitCode (..), exitFailure)-import System.FilePath (makeRelative, takeDirectory)+import System.FilePath (makeRelative, normalise, takeDirectory, (</>)) import System.IO (hPutStrLn, stderr) import System.Process (readProcessWithExitCode)  data Command     = Parse FilePath-    | Check FilePath Bool-    | Scaffold FilePath FilePath (Maybe String) Bool Bool-    | Diff FilePath String+    | Check FilePath Bool Bool (Maybe CheckCoverageOptions)+    | Scaffold FilePath FilePath (Maybe String) Bool Bool (Maybe FilePath) (Maybe (String, FilePath))+    | Diff FilePath String (Maybe FilePath) (Maybe FilePath) [CompatibilitySurface] Bool (Maybe FilePath) (Maybe DiffCoverageOptions)     | New String +data CheckCoverageOptions = CheckCoverageOptions+    { checkCoveragePath :: !FilePath+    , checkFailOnOpaque :: !Bool+    }++data DiffCoverageOptions = DiffCoverageOptions+    { diffCoveragePath :: !FilePath+    , diffFailOnOpaqueIncrease :: !Bool+    }+ main :: IO () main = run =<< execParser opts   where@@ -46,13 +62,13 @@             (info (Parse <$> fileArg <**> helper) (progDesc "Parse a .keiro file and pretty-print it back"))             <> command                 "check"-                (info (Check <$> fileArg <*> emitSwitch <**> helper) (progDesc "Validate a .keiro file; print diagnostics and exit non-zero on any error"))+                (info (Check <$> fileArg <*> emitSwitch <*> explainBindingsSwitch <*> checkCoverageOptions <**> helper) (progDesc "Validate a .keiro file; print diagnostics and exit non-zero on any error"))             <> command                 "scaffold"-                (info (Scaffold <$> fileArg <*> outOpt <*> optional moduleRootOpt <*> collocateSwitch <*> forceGeneratedOverwriteSwitch <**> helper) (progDesc "Emit the generated layer + typed holes from a .keiro file"))+                (info (Scaffold <$> fileArg <*> outOpt <*> optional moduleRootOpt <*> collocateSwitch <*> forceGeneratedOverwriteSwitch <*> optional goldensOpt <*> codecComparisonOpts <**> helper) (progDesc "Emit the generated layer + typed holes from a .keiro file"))             <> command                 "diff"-                (info (Diff <$> fileArg <*> sinceOpt <**> helper) (progDesc "Classify spec changes since a git ref as ADDITIVE/WARNING/BREAKING over the decode and identity surface; exit non-zero on any BREAKING change"))+                (info (Diff <$> fileArg <*> sinceOpt <*> optional emitGoldensOpt <*> optional replayImpactOutOpt <*> many gateOpt <*> explainSwitch <*> optional reportOutOpt <*> diffCoverageOptions <**> helper) (progDesc "Classify spec changes since a git ref as per-surface compatibility vectors; exit non-zero on any gated BREAKING surface"))             <> command                 "new"                 (info (New <$> kindArg <**> helper) (progDesc "Print a minimal valid .keiro skeleton for a node kind (aggregate, process, router, contract, intake, emit, publisher, workqueue, dispatch, workflow, operation)"))@@ -70,9 +86,57 @@ forceGeneratedOverwriteSwitch :: Parser Bool forceGeneratedOverwriteSwitch = switch (long "force-generated-overwrite" <> help "Overwrite a Generated path even when the existing file lacks the @generated banner") +goldensOpt :: Parser FilePath+goldensOpt = strOption (long "goldens" <> metavar "DIR" <> help "Golden-payload root to embed in generated aggregate harnesses")++codecComparisonOpts :: Parser (Maybe (String, FilePath))+codecComparisonOpts =+    optional+        ( (,)+            <$> strOption (long "codec-comparison" <> metavar "MAPPED-NAME" <> help "Emit a non-production historical-codec comparison module for one structural mapped type (requires --comparison-out)")+            <*> strOption (long "comparison-out" <> metavar "FILE" <> help "Exact generated comparison-module path under --out (requires --codec-comparison)")+        )++emitGoldensOpt :: Parser FilePath+emitGoldensOpt = strOption (long "emit-goldens" <> metavar "DIR" <> help "Write old-shape payload fixtures for event version bumps without overwriting existing files")++replayImpactOutOpt :: Parser FilePath+replayImpactOutOpt = strOption (long "replay-impact-out" <> metavar "FILE" <> help "Write the replay-neutral or affected audit input as JSON")++gateOpt :: Parser CompatibilitySurface+gateOpt = option (eitherReader parseSurfaceName) (long "gate" <> metavar "SURFACE" <> help "Also fail on a breaking verdict for this compatibility surface (repeatable)")++explainSwitch :: Parser Bool+explainSwitch = switch (long "explain" <> help "Print containing paths, failing directions, and remediation choices")++reportOutOpt :: Parser FilePath+reportOutOpt = strOption (long "report-out" <> metavar "FILE" <> help "Write the full keiro-dsl/diff-report/1 compatibility report as JSON")++coverageReportOpt :: Parser FilePath+coverageReportOpt = strOption (long "coverage-report" <> metavar "FILE" <> help "Write reporting-only structural/opaque mapped-root coverage as JSON")++checkCoverageOptions :: Parser (Maybe CheckCoverageOptions)+checkCoverageOptions =+    optional+        ( CheckCoverageOptions+            <$> coverageReportOpt+            <*> switch (long "fail-on-opaque" <> help "Fail when a private persisted root contains an opaque boundary (requires --coverage-report)")+        )++diffCoverageOptions :: Parser (Maybe DiffCoverageOptions)+diffCoverageOptions =+    optional+        ( DiffCoverageOptions+            <$> coverageReportOpt+            <*> switch (long "fail-on-opaque-increase" <> help "Fail when diff adds a named opaque boundary (requires --coverage-report)")+        )+ emitSwitch :: Parser Bool emitSwitch = switch (long "emit" <> help "On success, pretty-print the parsed spec to stdout (folds parse + check into one call)") +explainBindingsSwitch :: Parser Bool+explainBindingsSwitch = switch (long "explain-bindings" <> help "On success, list the consumer-owned binding, fixture, and register-initial symbols required by structural mapped types")+ sinceOpt :: Parser String sinceOpt = strOption (long "since" <> metavar "GIT-REF" <> help "Git ref to diff the spec against (e.g. HEAD, a tag, a branch)") @@ -90,7 +154,7 @@             hPutStrLn stderr (T.unpack err)             exitFailure         Right spec -> TIO.putStrLn (renderSpec spec)-run (Check fp emit) = do+run (Check fp emit explainBindings coverageOptions) = do     input <- TIO.readFile fp     case parseSpec fp input of         Left err -> do@@ -101,11 +165,19 @@             mapM_ (TIO.hPutStrLn stderr . renderDiagnostic fp) diags             if any ((== Error) . severity) diags                 then exitFailure-                else-                    if emit-                        then TIO.putStrLn (renderSpec spec)-                        else putStrLn "OK"-run (Scaffold fp out cliRoot cliCollocate forceGeneratedOverwrite) = do+                else do+                    when emit (TIO.putStrLn (renderSpec spec))+                    if explainBindings+                        then case bindingObligations spec of+                            Left graphErrors -> do+                                hPutStrLn stderr ("validated spec did not resolve its mapped type graph: " <> show graphErrors)+                                exitFailure+                            Right obligations -> TIO.putStrLn (renderBindingObligations (specContext spec) obligations)+                        else pure ()+                    coverageOk <- runCheckCoverage fp spec coverageOptions+                    when (coverageOk && not emit && not explainBindings) (putStrLn "OK")+                    when (not coverageOk) exitFailure+run (Scaffold fp out cliRoot cliCollocate forceGeneratedOverwrite cliGoldens comparisonRequest) = do     input <- TIO.readFile fp     case parseSpec fp input of         Left err -> do@@ -118,22 +190,31 @@             mapM_ (TIO.hPutStrLn stderr . renderDiagnostic fp) diags             when (any ((== Error) . severity) diags) exitFailure             let ctx = mkContext cliRoot cliCollocate spec-            case planScaffold ctx spec of-                Left refusals -> do+                goldenRoot = fromMaybe (takeDirectory fp </> "golden-payloads") cliGoldens+            goldens <- loadGoldenPayloads goldenRoot spec+            case (planScaffoldWithGoldens goldens ctx spec, traverse (\(name, _) -> codecComparisonModule ctx spec (T.pack name)) comparisonRequest) of+                (Left refusals, _) -> do                     mapM_ (TIO.hPutStrLn stderr) (renderRefusals refusals)                     exitFailure-                Right modules -> do-                    result <- executeScaffold out forceGeneratedOverwrite fp ctx spec modules-                    case result of-                        Left refusals -> do-                            mapM_ (TIO.hPutStrLn stderr) (renderRefusals refusals)-                            exitFailure-                        Right report -> mapM_ (TIO.hPutStrLn stderr) (renderScaffoldReport report)+                (_, Left comparisonError) -> TIO.hPutStrLn stderr comparisonError >> exitFailure+                (Right modules, Right comparisonModule) -> do+                    comparisonReady <- preflightComparison out comparisonRequest comparisonModule+                    case comparisonReady of+                        Left comparisonError -> TIO.hPutStrLn stderr comparisonError >> exitFailure+                        Right () -> do+                            result <- executeScaffold out forceGeneratedOverwrite fp ctx spec modules+                            case result of+                                Left refusals -> do+                                    mapM_ (TIO.hPutStrLn stderr) (renderRefusals refusals)+                                    exitFailure+                                Right report -> do+                                    mapM_ (TIO.hPutStrLn stderr) (renderScaffoldReport report)+                                    writeComparison comparisonRequest comparisonModule run (New kind) =     case skeletonFor (T.pack kind) of         Left err -> hPutStrLn stderr (T.unpack err) >> exitFailure         Right skel -> TIO.putStr skel-run (Diff fp ref) = do+run (Diff fp ref emitGoldensRoot replayImpactOut gatedSurfaces explain reportOut coverageOptions) = do     -- Resolve the spec to a repo-relative path so `git show <ref>:<relpath>` works.     let dir = takeDirectory fp     rootRes <- git dir ["rev-parse", "--show-toplevel"]@@ -151,18 +232,24 @@                     case (,) <$> parseSpec (ref <> ":" <> relPath) (T.pack oldText) <*> parseSpec fp newText of                         Left perr -> hPutStrLn stderr (T.unpack perr) >> exitFailure                         Right (oldSpec, newSpec) -> do+                            written <- maybe (pure []) (\root -> emitGoldenPayloads root oldSpec newSpec) emitGoldensRoot+                            mapM_ (putStrLn . ("golden: wrote synthesized weak stand-in " <>)) written                             let changes = diffSpecs oldSpec newSpec-                            mapM_ (putStrLn . renderChange) changes-                            if any isBreaking changes then exitFailure else pure ()+                                impact = replayImpact oldSpec newSpec+                                effectiveGate = gateWith gatedSurfaces+                            mapM_ (TIO.putStrLn . renderFinding) changes+                            when explain $+                                mapM_ (TIO.putStrLn . renderExplainBlock) (filter shouldExplain changes)+                            TIO.putStrLn (renderReplayImpact impact)+                            mapM_ (`Aeson.encodeFile` impact) replayImpactOut+                            mapM_ (\path -> Aeson.encodeFile path (diffReport effectiveGate changes)) reportOut+                            coverageOk <- runDiffCoverage fp (T.pack ref) oldSpec newSpec coverageOptions+                            if any (gatedBreaking effectiveGate) changes || not coverageOk then exitFailure else pure () -renderChange :: Change -> String-renderChange c = case c of-    Additive k -> "ADDITIVE: " <> body k-    Advisory k -> "WARNING: " <> body k <> codeSuffix k-    Breaking k -> "BREAKING: " <> body k <> codeSuffix k-  where-    body k = T.unpack (ckNode k) <> " " <> T.unpack (ckFacet k) <> " " <> T.unpack (ckSubject k) <> ": " <> T.unpack (ckDetail k)-    codeSuffix k = maybe "" (\dc -> " [" <> show dc <> "]") (ckCode k)+shouldExplain :: Change -> Bool+shouldExplain Additive{} = False+shouldExplain Advisory{} = True+shouldExplain Breaking{} = True  -- | Run git in a directory, returning trimmed stdout or stderr. git :: FilePath -> [String] -> IO (Either String String)@@ -174,6 +261,70 @@  trim :: String -> String trim = f . f where f = reverse . dropWhile (`elem` (" \t\r\n" :: String))++preflightComparison :: FilePath -> Maybe (String, FilePath) -> Maybe ScaffoldModule -> IO (Either T.Text ())+preflightComparison _ Nothing Nothing = pure (Right ())+preflightComparison out (Just (_, requestedPath)) (Just comparisonModule) = do+    let expectedPath = normalise (out </> modulePath comparisonModule)+        actualPath = normalise requestedPath+    if actualPath /= expectedPath+        then+            pure+                ( Left+                    ( "--comparison-out must match the generated module path under --out: expected "+                        <> T.pack expectedPath+                    )+                )+        else do+            exists <- doesFileExist actualPath+            if not exists+                then pure (Right ())+                else do+                    existing <- TIO.readFile actualPath+                    pure+                        ( if codecComparisonBanner `elem` T.lines existing+                            then Right ()+                            else Left ("refusing to overwrite non-comparison output: " <> T.pack actualPath)+                        )+preflightComparison _ _ _ = pure (Left "internal error: incomplete codec-comparison option pair")++writeComparison :: Maybe (String, FilePath) -> Maybe ScaffoldModule -> IO ()+writeComparison Nothing Nothing = pure ()+writeComparison (Just (_, path)) (Just comparisonModule) = do+    createDirectoryIfMissing True (takeDirectory path)+    TIO.writeFile path (moduleText comparisonModule)+    TIO.hPutStrLn stderr ("comparison generated " <> T.pack path <> " (migration evidence only)")+writeComparison _ _ = hPutStrLn stderr "internal error: incomplete codec-comparison output" >> exitFailure++runCheckCoverage :: FilePath -> Spec -> Maybe CheckCoverageOptions -> IO Bool+runCheckCoverage _ _ Nothing = pure True+runCheckCoverage specPath spec (Just options) =+    case Coverage.coverageReport specPath spec of+        Left graphErrors -> do+            hPutStrLn stderr ("validated spec did not resolve its mapped type graph for coverage: " <> show graphErrors)+            pure False+        Right baseReport -> do+            let report = if checkFailOnOpaque options then Coverage.failOnOpaque baseReport else baseReport+            emitCoverageReport (checkCoveragePath options) report++runDiffCoverage :: FilePath -> T.Text -> Spec -> Spec -> Maybe DiffCoverageOptions -> IO Bool+runDiffCoverage _ _ _ _ Nothing = pure True+runDiffCoverage specPath reference oldSpec newSpec (Just options) =+    case Coverage.coverageDiffReport specPath reference oldSpec newSpec of+        Left graphErrors -> do+            hPutStrLn stderr ("diff specs did not resolve their mapped type graph for coverage: " <> show graphErrors)+            pure False+        Right baseReport -> do+            let report = if diffFailOnOpaqueIncrease options then Coverage.failOnOpaqueIncrease baseReport else baseReport+            emitCoverageReport (diffCoveragePath options) report++emitCoverageReport :: FilePath -> Coverage.CoverageReport -> IO Bool+emitCoverageReport path report = do+    mapM_ (TIO.hPutStrLn stderr . Coverage.renderCoverageFinding (Coverage.coverageSpec report)) (Coverage.coverageFindings report)+    TIO.putStr (Coverage.renderCoverageSummary report)+    Coverage.writeCoverageReport path report+    putStrLn ("coverage report written to " <> path)+    pure (Coverage.coverageSucceeded report)  {- | Fold the spec's @module@/@layout@ clauses with the CLI overrides to a 'Context'. Precedence is CLI flag > spec clause > built-in default.
+ bench/structural-codec/Main.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE OverloadedRecordDot #-}++module Main (main) where++import Conformance.Structural.Bindings qualified as Bindings+import Conformance.Structural.Domain qualified as Domain+import Control.DeepSeq (NFData)+import Control.Monad (unless, (>=>))+import Data.Aeson (Value (..), object, withObject, (.!=), (.:), (.:?), (.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.Types (Parser, parseEither)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Text (Text)+import Data.Text qualified as Text+import Generated.StructuralConformance.ArtifactCatalog.Codec (decodeArtifactInfoMapped, encodeArtifactInfoMapped)+import Keiro.Codec.Structural (FixtureCases (..))+import Test.Tasty.Bench (Benchmark, bcompareWithin, bench, bgroup, defaultMain, nf)++main :: IO ()+main = defaultMain benchmarks++benchmarks :: [Benchmark]+benchmarks =+    [ bgroup+        "encode"+        [ comparison "encode-small-record" baselineEncodeArtifact encodeArtifactInfoMapped smallArtifact+        , comparison "encode-nested-union" (map baselineEncodeArtifact) (map encodeArtifactInfoMapped) unionArtifacts+        , comparison "encode-large-list" (map baselineEncodeArtifact) (map encodeArtifactInfoMapped) largeArtifacts+        ]+    , bgroup+        "decode"+        [ comparison "decode-small-record" baselineDecodeArtifact decodeArtifactInfoMapped smallEncoded+        , comparison "decode-nested-union" baselineDecodeArtifacts generatedDecodeArtifacts unionEncoded+        , comparison "decode-large-list" baselineDecodeArtifacts generatedDecodeArtifacts largeEncoded+        ]+    ]++comparison :: (NFData result) => String -> (input -> result) -> (input -> result) -> input -> Benchmark+comparison label baseline generated input =+    bgroup+        label+        [ bench ("baseline-" <> label) (nf baseline input)+        , bcompareWithin 0 2 ("baseline-" <> label) $ bench ("generated-" <> label) (nf generated input)+        ]++allArtifacts :: [Domain.ArtifactInfo]+allArtifacts = map snd (NonEmpty.toList (fixtureCases Bindings.artifactInfoCases))++smallArtifact :: Domain.ArtifactInfo+smallArtifact = snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))++unionArtifacts :: [Domain.ArtifactInfo]+unionArtifacts = allArtifacts++largeArtifacts :: [Domain.ArtifactInfo]+largeArtifacts = take 2000 (cycle allArtifacts)++smallEncoded :: Value+smallEncoded = encodeArtifactInfoMapped smallArtifact++unionEncoded :: Value+unionEncoded = Aeson.toJSON (map encodeArtifactInfoMapped unionArtifacts)++largeEncoded :: Value+largeEncoded = Aeson.toJSON (map encodeArtifactInfoMapped largeArtifacts)++generatedDecodeArtifacts :: Value -> Either Text [Domain.ArtifactInfo]+generatedDecodeArtifacts value = do+    values <- firstText (parseEither Aeson.parseJSON value)+    traverse decodeArtifactInfoMapped values++baselineDecodeArtifacts :: Value -> Either Text [Domain.ArtifactInfo]+baselineDecodeArtifacts = firstText . parseEither (Aeson.parseJSON >=> traverse baselineParseArtifact)++baselineDecodeArtifact :: Value -> Either Text Domain.ArtifactInfo+baselineDecodeArtifact = firstText . parseEither baselineParseArtifact++firstText :: Either String value -> Either Text value+firstText = either (Left . Text.pack) Right++baselineEncodeArtifact :: Domain.ArtifactInfo -> Value+baselineEncodeArtifact value =+    object+        [ "artifact_key" .= value.artifactKey+        , "display_name" .= value.displayName+        , "artifact_hash" .= value.artifactHash+        , "artifact_kind" .= encodeKind value.artifactKind+        , "location" .= encodeLocation value.location+        , "metadata" .= object ["note" .= value.metadata.note]+        , "active" .= value.active+        , "tags" .= value.tags+        ]++baselineParseArtifact :: Value -> Parser Domain.ArtifactInfo+baselineParseArtifact = withObject "ArtifactInfo" $ \value -> do+    rejectUnknownFields "ArtifactInfo" ["artifact_key", "display_name", "artifact_hash", "artifact_kind", "location", "metadata", "active", "tags"] value+    Domain.ArtifactInfo+        <$> value .: "artifact_key"+        <*> value .: "display_name"+        <*> value .:? "artifact_hash"+        <*> (value .:? "artifact_kind" .!= String "guide" >>= parseKind)+        <*> (value .: "location" >>= parseLocation)+        <*> (value .: "metadata" >>= withObject "ArtifactMetadata" (\metadata -> Domain.ArtifactMetadata <$> metadata .: "note"))+        <*> (value .:? "active" .!= False)+        <*> (value .:? "tags" .!= [])++encodeKind :: Domain.ArtifactKind -> Value+encodeKind = \case+    Domain.Guide -> String "guide"+    Domain.Reference -> String "reference"++parseKind :: Value -> Parser Domain.ArtifactKind+parseKind = \case+    String "guide" -> pure Domain.Guide+    String "reference" -> pure Domain.Reference+    _ -> fail "unknown ArtifactKind"++encodeLocation :: Domain.ArtifactLocation -> Value+encodeLocation = \case+    Domain.LocalFile path -> tagged "local_file" (Just path)+    Domain.LocalDir path -> tagged "local_dir" (Just path)+    Domain.RepoPath path -> tagged "repo_path" (Just path)+    Domain.LocUrl url -> tagged "url" (Just url)+    Domain.Canonical -> tagged "canonical" Nothing+  where+    tagged :: Text -> Maybe Text -> Value+    tagged tag contents = object (["tag" .= tag] <> maybe [] (pure . ("contents" .=)) contents)++parseLocation :: Value -> Parser Domain.ArtifactLocation+parseLocation = withObject "ArtifactLocation" $ \value -> do+    tag <- value .: "tag" :: Parser Text+    case tag of+        "local_file" -> rejectUnknownFields "ArtifactLocation" ["tag", "contents"] value >> (Domain.LocalFile <$> value .: "contents")+        "local_dir" -> rejectUnknownFields "ArtifactLocation" ["tag", "contents"] value >> (Domain.LocalDir <$> value .: "contents")+        "repo_path" -> rejectUnknownFields "ArtifactLocation" ["tag", "contents"] value >> (Domain.RepoPath <$> value .: "contents")+        "url" -> rejectUnknownFields "ArtifactLocation" ["tag", "contents"] value >> (Domain.LocUrl <$> value .: "contents")+        "canonical" -> rejectUnknownFields "ArtifactLocation" ["tag"] value >> pure Domain.Canonical+        _ -> fail "unknown ArtifactLocation"++rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser ()+rejectUnknownFields label allowed value =+    unless (null extras) (fail (label <> " contains unknown fields: " <> show extras))+  where+    extras = filter (`notElem` allowed) (map Key.toText (KeyMap.keys value))
keiro-dsl.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            keiro-dsl-version:         0.3.0.0+version:         0.4.0.1 synopsis:        Typed specification toolchain for keiro services description:   keiro-dsl is the toolchain over a typed `.keiro` specification of a keiro@@ -18,11 +18,12 @@ extra-doc-files: CHANGELOG.md  common warnings-  ghc-options: -Wall+  ghc-options: -Wall -Werror=missing-fields  common shared   default-language:   GHC2024   default-extensions:+    DuplicateRecordFields     LambdaCase     OverloadedStrings @@ -30,41 +31,54 @@   import:          warnings, shared   hs-source-dirs:  src   exposed-modules:+    Keiro.Dsl.CodecCompare+    Keiro.Dsl.Coverage     Keiro.Dsl.Diff+    Keiro.Dsl.DiffReport+    Keiro.Dsl.ExplainBindings+    Keiro.Dsl.FoldFingerprint+    Keiro.Dsl.Goldens     Keiro.Dsl.Grammar     Keiro.Dsl.Harness     Keiro.Dsl.Manifest+    Keiro.Dsl.MappedConsumer+    Keiro.Dsl.MappedDiff     Keiro.Dsl.Parser     Keiro.Dsl.PrettyPrint     Keiro.Dsl.ReadModelShape+    Keiro.Dsl.ReplayImpact     Keiro.Dsl.Scaffold     Keiro.Dsl.ScaffoldRecord     Keiro.Dsl.ScaffoldRun     Keiro.Dsl.Skeleton+    Keiro.Dsl.TypeGraph     Keiro.Dsl.Validate    build-depends:-    , base                >=4.21 && <5-    , containers          >=0.6-    , directory           >=1.3-    , filepath            >=1.4-    , megaparsec          >=9.6-    , parser-combinators  >=1.3-    , prettyprinter       >=1.7-    , text                >=2.1+    , aeson               >=2.2.1 && <2.3+    , base                >=4.21  && <5+    , bytestring          >=0.12  && <0.13+    , containers          >=0.6   && <0.8+    , directory           >=1.3   && <1.4+    , filepath            >=1.4   && <1.6+    , megaparsec          >=9.6   && <9.9+    , parser-combinators  >=1.3   && <1.4+    , prettyprinter       >=1.7   && <1.8+    , text                >=2.1   && <2.2  executable keiro-dsl   import:         warnings, shared   hs-source-dirs: app   main-is:        Main.hs   build-depends:+    , aeson                 >=2.2  && <2.3     , base                  >=4.21 && <5-    , directory             >=1.3-    , filepath              >=1.4+    , directory             >=1.3  && <1.4+    , filepath              >=1.4  && <1.6     , keiro-dsl-    , optparse-applicative  >=0.18-    , process               >=1.6-    , text                  >=2.1+    , optparse-applicative  >=0.18 && <0.20+    , process               >=1.6  && <1.7+    , text                  >=2.1  && <2.2  test-suite keiro-dsl-test   import:         warnings, shared@@ -72,14 +86,17 @@   hs-source-dirs: test   main-is:        Main.hs   build-depends:+    , aeson       >=2.2  && <2.3     , base        >=4.21 && <5-    , containers  >=0.6-    , directory   >=1.3-    , filepath    >=1.4+    , containers  >=0.6  && <0.8+    , directory   >=1.3  && <1.4+    , filepath    >=1.4  && <1.6     , hspec       >=2.11+    , keiro-core     , keiro-dsl+    , process     >=1.6  && <1.7     , QuickCheck  >=2.14-    , text        >=2.1+    , text        >=2.1  && <2.2  -- Conformance: proves the scaffolded Generated modules plus a hand-filled -- Holes.hs compile against keiki/keiro and that the filled transducer passes@@ -92,6 +109,7 @@   hs-source-dirs: test/conformance   main-is:        Main.hs   other-modules:+    Generated.HospitalCapacity.ReplayAudit     Generated.HospitalCapacity.Reservation.Codec     Generated.HospitalCapacity.Reservation.Domain     Generated.HospitalCapacity.Reservation.EventStream@@ -100,12 +118,132 @@     HospitalCapacity.Reservation.Holes    build-depends:-    , aeson  >=2.2+    , aeson  >=2.2  && <2.3     , base   >=4.21 && <5-    , keiki  >=0.2  && <0.3+    , keiki  >=0.4  && <0.5     , keiro-    , text   >=2.1+    , text   >=2.1  && <2.2 +-- Plan 150 / IR-1: compiled structural consumer bindings, declared-wire+-- codecs, generated projection witnesses, opaque boundaries, fixture branch+-- coverage, current payload goldens, and mapped-register replay equality.+test-suite keiro-dsl-conformance-structural+  import:         warnings, shared+  type:           exitcode-stdio-1.0+  hs-source-dirs: test/conformance-structural+  main-is:        Main.hs+  other-modules:+    Conformance.Structural.Bindings+    Conformance.Structural.Domain+    Generated.StructuralConformance.ArtifactCatalog.Codec+    Generated.StructuralConformance.ArtifactCatalog.Domain+    Generated.StructuralConformance.ArtifactCatalog.EventStream+    Generated.StructuralConformance.ArtifactCatalog.Harness+    Generated.StructuralConformance.ArtifactCatalog.Projection+    Generated.StructuralConformance.ReplayAudit+    Generated.StructuralConformance.Structural.Shape.ArtifactInfo+    Generated.StructuralConformance.Structural.Shape.ArtifactKind+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation+    Generated.StructuralConformance.Structural.Shape.ArtifactMetadata+    Generated.StructuralConformance.StructuralProjections+    StructuralConformance.ArtifactCatalog.Holes++  build-depends:+    , aeson       >=2.2  && <2.3+    , base        >=4.21 && <5+    , bytestring  >=0.12 && <0.13+    , containers  >=0.6  && <0.8+    , deepseq     >=1.5  && <1.6+    , keiki       >=0.4  && <0.5+    , keiro+    , text        >=2.1  && <2.2+    , time        >=1.12 && <1.15++-- Plan 152 / Experiment B: a consumer-owned historical codec and finite JSON+-- corpus compared with the generated structural codec. The comparison module+-- is opt-in tooling output and is not part of the production scaffold record.+test-suite keiro-dsl-conformance-codec-compare+  import:         warnings, shared+  type:           exitcode-stdio-1.0+  hs-source-dirs:+    test/conformance-codec-compare test/conformance-structural++  main-is:        Main.hs+  other-modules:+    Conformance.CodecCompare.Historical+    Conformance.Structural.Bindings+    Conformance.Structural.Domain+    Generated.StructuralConformance.ArtifactCatalog.Codec+    Generated.StructuralConformance.ArtifactCatalog.Domain+    Generated.StructuralConformance.Structural.CodecCompare.ArtifactInfo+    Generated.StructuralConformance.Structural.Shape.ArtifactInfo+    Generated.StructuralConformance.Structural.Shape.ArtifactKind+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation+    Generated.StructuralConformance.Structural.Shape.ArtifactMetadata++  build-depends:+    , aeson       >=2.2.1 && <2.3+    , base        >=4.21  && <5+    , containers  >=0.6   && <0.8+    , deepseq     >=1.5   && <1.6+    , directory   >=1.3   && <1.4+    , filepath    >=1.4   && <1.6+    , keiki       >=0.4   && <0.5+    , keiro+    , keiro-dsl+    , text        >=2.1   && <2.2++-- Plan 147 M2: a dedicated honest-wire baseline plus a dormant idempotent+-- dishonest WireCtor used by replay-mutation-test.sh to prove that generated+-- forward/replay register comparisons catch state divergence missed by the+-- pre-existing validator, codec round-trip, and accept assertions.+test-suite keiro-dsl-conformance-replay+  import:         warnings, shared+  type:           exitcode-stdio-1.0+  hs-source-dirs: test/conformance-replay+  main-is:        Main.hs+  other-modules:+    Generated.ReplayDivergence.Note.Codec+    Generated.ReplayDivergence.Note.Domain+    Generated.ReplayDivergence.Note.EventStream+    Generated.ReplayDivergence.Note.Harness+    Generated.ReplayDivergence.Note.Projection+    Generated.ReplayDivergence.ReplayAudit+    ReplayDivergence.Note.Holes++  build-depends:+    , aeson  >=2.2  && <2.3+    , base   >=4.21 && <5+    , keiki  >=0.4  && <0.5+    , keiro+    , text   >=2.1  && <2.2++benchmark keiro-dsl-codec-bench+  import:         warnings, shared+  type:           exitcode-stdio-1.0+  hs-source-dirs: bench/structural-codec test/conformance-structural+  main-is:        Main.hs+  other-modules:+    Conformance.Structural.Bindings+    Conformance.Structural.Domain+    Generated.StructuralConformance.ArtifactCatalog.Codec+    Generated.StructuralConformance.ArtifactCatalog.Domain+    Generated.StructuralConformance.Structural.Shape.ArtifactInfo+    Generated.StructuralConformance.Structural.Shape.ArtifactKind+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation+    Generated.StructuralConformance.Structural.Shape.ArtifactMetadata++  build-depends:+    , aeson        >=2.2  && <2.3+    , base         >=4.21 && <5+    , containers   >=0.6  && <0.8+    , deepseq      >=1.5  && <1.6+    , keiki        >=0.4  && <0.5+    , keiro+    , tasty-bench  >=0.5  && <0.6+    , text         >=2.1  && <2.2+    , time         >=1.12 && <1.15+ -- EP-109 M2: snapshot-enabled aggregate scaffolding compiled against the live -- defaultStateCodec and stream-construction guards, with the captured codec -- identity checked against keiki's regFileShapeHash.@@ -115,17 +253,18 @@   hs-source-dirs: test/conformance-snapshot   main-is:        Main.hs   other-modules:+    Generated.HospitalCapacity.ReplayAudit     Generated.HospitalCapacity.Reservation.Codec     Generated.HospitalCapacity.Reservation.Domain     Generated.HospitalCapacity.Reservation.EventStream     HospitalCapacity.Reservation.Holes    build-depends:-    , aeson  >=2.2+    , aeson  >=2.2  && <2.3     , base   >=4.21 && <5-    , keiki  >=0.2  && <0.3+    , keiki  >=0.4  && <0.5     , keiro-    , text   >=2.1+    , text   >=2.1  && <2.2  -- EP-106 M6: every distinct `new <kind>` skeleton is scaffolded into this -- committed tree. Compiling the union proves a starter that passes `check`@@ -136,6 +275,7 @@   hs-source-dirs: test/conformance-skeletons   main-is:        Main.hs   other-modules:+    SkelAggregate.Generated.MyService.ReplayAudit     SkelAggregate.Generated.MyService.Thing.Codec     SkelAggregate.Generated.MyService.Thing.Domain     SkelAggregate.Generated.MyService.Thing.EventStream@@ -154,6 +294,7 @@     SkelProcess.Generated.MyService.Hospital.Projection     SkelProcess.Generated.MyService.HospitalSurge.Process     SkelProcess.Generated.MyService.HospitalSurge.ProcessHarness+    SkelProcess.Generated.MyService.ReplayAudit     SkelProcess.Generated.MyService.Surge.Codec     SkelProcess.Generated.MyService.Surge.Domain     SkelProcess.Generated.MyService.Surge.EventStream@@ -166,6 +307,7 @@     SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelHarness     SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelTable     SkelQueue.Generated.MyService.Reservation_work.Queue+    SkelQueue.Generated.MyService.Reservation_work.QueueCodec     SkelQueue.Generated.MyService.Reservation_work.QueuePolicy     SkelQueue.Generated.MyService.Transfer_decisions.ReadModel     SkelQueue.Generated.MyService.Transfer_decisions.ReadModelHarness@@ -179,23 +321,25 @@     SkelRouter.Generated.MyService.Page.Projection     SkelRouter.Generated.MyService.PagingRouter.Router     SkelRouter.Generated.MyService.PagingRouter.RouterHarness+    SkelRouter.Generated.MyService.ReplayAudit     SkelRouter.MyService.Page.Holes     SkelRouter.MyService.PagingRouter.RouterHoles     SkelWorkflow.Generated.MyService.HospitalTransferReservation.WorkflowFacts     SkelWorkflow.Generated.MyService.HospitalTransferReservation.WorkflowRuntime    build-depends:-    , aeson              >=2.2+    , aeson              >=2.2  && <2.3     , base               >=4.21 && <5     , containers     , effectful-core     , hasql-transaction-    , keiki              >=0.2  && <0.3+    , keiki              >=0.4  && <0.5     , keiro+    , keiro-core     , keiro-pgmq     , kiroku-store     , shibuya-core-    , text               >=2.1+    , text               >=2.1  && <2.2     , time     , uuid @@ -209,6 +353,7 @@   main-is:        Main.hs   other-modules:     Billing.Subscription.Holes+    Generated.Billing.ReplayAudit     Generated.Billing.Subscription.Codec     Generated.Billing.Subscription.Domain     Generated.Billing.Subscription.EventStream@@ -216,11 +361,11 @@     Generated.Billing.Subscription.Projection    build-depends:-    , aeson  >=2.2+    , aeson  >=2.2  && <2.3     , base   >=4.21 && <5-    , keiki  >=0.2  && <0.3+    , keiki  >=0.4  && <0.5     , keiro-    , text   >=2.1+    , text   >=2.1  && <2.2  -- Contract codec conformance (EP-4): the scaffolded self-contained contract -- payload ADT + codec, compiled + round-tripped per event type.@@ -231,9 +376,9 @@   main-is:        Main.hs   other-modules:  Generated.HospitalCapacity.Emergency.Contract   build-depends:-    , aeson  >=2.2+    , aeson  >=2.2  && <2.3     , base   >=4.21 && <5-    , text   >=2.1+    , text   >=2.1  && <2.2  -- EP-4 intake runtime conformance: the scaffolded Inbox disposition + dedupe -- policy compiled against the LIVE Keiro.Inbox.Types (InboxResult / dedupe).@@ -285,11 +430,16 @@   type:           exitcode-stdio-1.0   hs-source-dirs: test/conformance-queue   main-is:        Main.hs-  other-modules:  Generated.HospitalCapacity.Reservation_work.Queue+  other-modules:+    Generated.HospitalCapacity.Reservation_work.Queue+    Generated.HospitalCapacity.Reservation_work.QueueCodec+   build-depends:-    , aeson  >=2.2-    , base   >=4.21 && <5-    , text   >=2.1+    , aeson       >=2.2  && <2.3+    , base        >=4.21 && <5+    , keiro-core+    , keiro-pgmq+    , text        >=2.1  && <2.2  -- EP-5 pgmq runtime conformance: the scaffolded QueuePolicy (RetryPolicy + -- JobOutcome disposition) compiled against the LIVE Keiro.PGMQ.Job runtime.@@ -300,16 +450,18 @@   main-is:        Main.hs   other-modules:     Generated.HospitalCapacity.Reservation_work.Queue+    Generated.HospitalCapacity.Reservation_work.QueueCodec     Generated.HospitalCapacity.Reservation_work.QueuePolicy    build-depends:-    , aeson        >=2.2+    , aeson        >=2.2  && <2.3     , base         >=4.21 && <5+    , keiro-core     , keiro-dsl     , keiro-pgmq     , pgmq-config     , pgmq-core-    , text         >=2.1+    , text         >=2.1  && <2.2  -- EP-107 read-model runtime conformance: the scaffolded ReadModel record, -- registration/rebuild helpers, AsyncProjection, facts harness, and a filled@@ -331,7 +483,7 @@     , hasql-transaction     , keiro     , kiroku-store-    , text               >=2.1+    , text               >=2.1  && <2.2  -- EP-5 M5 full-service conformance: a complete pgmq dispatch service — scaffolded -- Job codec + retry policy + a filled worker handler assembled into a live@@ -343,15 +495,17 @@   main-is:        Main.hs   other-modules:     Generated.HospitalCapacity.Reservation_work.Queue+    Generated.HospitalCapacity.Reservation_work.QueueCodec     Generated.HospitalCapacity.Reservation_work.QueuePolicy     HospitalCapacity.ReservationWork.WorkqueueJob    build-depends:-    , aeson           >=2.2+    , aeson           >=2.2  && <2.3     , base            >=4.21 && <5     , effectful-core+    , keiro-core     , keiro-pgmq-    , text            >=2.1+    , text            >=2.1  && <2.2  -- Workflow facts harness (EP-6): the scaffolded self-contained WorkflowFacts -- module asserted against a hand-written expectation (mutation-pinnable).@@ -380,7 +534,7 @@     , base        >=4.21 && <5     , containers     , keiro-    , text        >=2.1+    , text        >=2.1  && <2.2  -- EP-3 M5 full-service conformance: a complete process service — the scaffolded -- Surge (saga) + Hospital (target) aggregates with filled transducers, plus a@@ -395,6 +549,7 @@     Generated.SurgeDemo.Hospital.Domain     Generated.SurgeDemo.Hospital.EventStream     Generated.SurgeDemo.Hospital.Projection+    Generated.SurgeDemo.ReplayAudit     Generated.SurgeDemo.Surge.Codec     Generated.SurgeDemo.Surge.Domain     Generated.SurgeDemo.Surge.EventStream@@ -405,14 +560,14 @@     SurgeDemo.SurgeFlow.Manager    build-depends:-    , aeson         >=2.2+    , aeson         >=2.2  && <2.3     , base          >=4.21 && <5-    , keiki         >=0.2  && <0.3+    , keiki         >=0.4  && <0.5     , keiro     , shibuya-core-    , text          >=2.1-    , time          >=1.12-    , uuid          >=1.3+    , text          >=2.1  && <2.2+    , time          >=1.12 && <1.15+    , uuid          >=1.3  && <1.4  -- EP-6 M5 full-service conformance: a complete durable workflow — scaffolded -- WorkflowRuntime + a filled ordered step/await body — compiled against the@@ -427,12 +582,12 @@     HospitalCapacity.HospitalTransferReservation.WorkflowBody    build-depends:-    , aeson           >=2.2+    , aeson           >=2.2  && <2.3     , base            >=4.21 && <5     , containers     , effectful-core     , keiro-    , text            >=2.1+    , text            >=2.1  && <2.2  -- EP-3 process runtime conformance: the scaffolded Process module's -- deterministic wiring (timer-request builder + fire disposition) compiled@@ -444,14 +599,14 @@   main-is:        Main.hs   other-modules:  Generated.HospitalCapacity.HospitalSurge.Process   build-depends:-    , aeson         >=2.2+    , aeson         >=2.2  && <2.3     , base          >=4.21 && <5     , keiro     , keiro-dsl     , shibuya-core-    , text          >=2.1-    , time          >=1.12-    , uuid          >=1.3+    , text          >=2.1  && <2.2+    , time          >=1.12 && <1.15+    , uuid          >=1.3  && <1.4  -- EP-108 router runtime conformance: generated policy lowering and the live -- target-keyed deterministic id contract.@@ -469,8 +624,8 @@     , keiro     , kiroku-store     , shibuya-core-    , text          >=2.1-    , uuid          >=1.3+    , text          >=2.1  && <2.2+    , uuid          >=1.3  && <1.4  -- EP-108 generated router-facts harness with hand-written expectations. test-suite keiro-dsl-conformance-router@@ -494,17 +649,18 @@     Generated.IncidentPaging.Page.EventStream     Generated.IncidentPaging.PagingRouter.Router     Generated.IncidentPaging.PagingRouter.RouterHarness+    Generated.IncidentPaging.ReplayAudit     IncidentPaging.Page.Holes     IncidentPaging.PagingRouter.RouterValue    build-depends:-    , aeson           >=2.2+    , aeson           >=2.2  && <2.3     , base            >=4.21 && <5     , effectful-core-    , keiki           >=0.2  && <0.3+    , keiki           >=0.4  && <0.5     , keiro     , shibuya-core-    , text            >=2.1+    , text            >=2.1  && <2.2  -- MP-15/EP-110 M6 cold-start: a fresh agent, given only the authoring skill -- and feature sentence, produced this aggregate + readmodel + router service.@@ -526,21 +682,22 @@     Generated.TransferRouting.Hospital_load.ReadModelTable     Generated.TransferRouting.HospitalTransferRouter.Router     Generated.TransferRouting.HospitalTransferRouter.RouterHarness+    Generated.TransferRouting.ReplayAudit     TransferRouting.Hospital.Holes     TransferRouting.Hospital_load.ReadModelHoles     TransferRouting.HospitalTransferRouter.RouterHoles     TransferRouting.HospitalTransferRouter.RouterValue    build-depends:-    , aeson              >=2.2+    , aeson              >=2.2  && <2.3     , base               >=4.21 && <5     , effectful-core     , hasql-transaction-    , keiki              >=0.2  && <0.3+    , keiki              >=0.4  && <0.5     , keiro     , kiroku-store     , shibuya-core-    , text               >=2.1+    , text               >=2.1  && <2.2  -- Process-manager facts harness (EP-3 M4): the self-contained, firewall-clean -- ProcessHarness module scaffolded from hospital-surge.keiro, compiled + run to@@ -555,7 +712,7 @@   other-modules:  Generated.HospitalCapacity.HospitalSurge.ProcessHarness   build-depends:     , base  >=4.21 && <5-    , text  >=2.1+    , text  >=2.1  && <2.2  -- Conformance for the evolved (v2) Reservation aggregate: proves the scaffolded -- Codec schemaVersion=2 + upcasters wiring compiles and that the filled upcaster@@ -568,6 +725,7 @@   hs-source-dirs: test/conformance-v2   main-is:        Main.hs   other-modules:+    Generated.HospitalCapacity.ReplayAudit     Generated.HospitalCapacity.Reservation.Codec     Generated.HospitalCapacity.Reservation.Domain     Generated.HospitalCapacity.Reservation.EventStream@@ -576,8 +734,9 @@     HospitalCapacity.Reservation.Holes    build-depends:-    , aeson  >=2.2-    , base   >=4.21 && <5-    , keiki  >=0.2  && <0.3+    , aeson      >=2.2  && <2.3+    , base       >=4.21 && <5+    , directory  >=1.3  && <1.4+    , keiki      >=0.4  && <0.5     , keiro-    , text   >=2.1+    , text       >=2.1  && <2.2
+ src/Keiro/Dsl/CodecCompare.hs view
@@ -0,0 +1,742 @@+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++{- | Pure historical-codec comparison and migration-evidence reports.++The comparison is deliberately finite evidence over typed fixture cases and+historical JSON goldens. It never changes which codec owns the wire schema and+never upgrades an opaque declaration to a structural claim.+-}+module Keiro.Dsl.CodecCompare (+    FixtureOrigin (..),+    DecodeOutcome (..),+    JsonPointer (..),+    ComparisonDifference (..),+    HistoricalCodec (..),+    CompareObservation (..),+    FixtureVerdict (..),+    CompareInputIssue (..),+    BranchKind (..),+    DeclaredBranch (..),+    ObservedBranch (..),+    CoverageGap (..),+    BranchSchema (..),+    BranchField (..),+    BranchArm (..),+    CompareProvenance (..),+    ClassifiedObservation (..),+    CompareReport (..),+    ReportWriteError (..),+    authorityStatement,+    canonicalJsonBytes,+    classifyObservation,+    declaredBranchesFor,+    observedBranchesFor,+    compareReport,+    renderCompareReport,+    reportSucceeded,+    writeCompareReportAtomic,+) where++import Control.Exception (IOException, bracketOnError, displayException, try)+import Control.Monad (when)+import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, withText, (.:), (.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.RFC8785 qualified as RFC8785+import Data.Aeson.Types (Parser)+import Data.ByteString (ByteString)+import Data.ByteString.Lazy qualified as LazyByteString+import Data.Foldable (toList)+import Data.List (sort)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.TypeGraph (BindingVersion (..), CanonicalTypeId (..), QualifiedValueName (..))+import Keiro.Dsl.Validate (DiagnosticCode (..))+import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile, renameFile)+import System.FilePath (takeDirectory, takeFileName)+import System.IO (Handle, hClose, openBinaryTempFile)++data FixtureOrigin = HistoricalGolden | FromBinding+    deriving stock (Eq, Ord, Show)++data DecodeOutcome+    = DecodedShape !Value+    | DecodeFailed !Text+    deriving stock (Eq, Show)++newtype JsonPointer = JsonPointer {unJsonPointer :: Text}+    deriving stock (Eq, Ord, Show)++data ComparisonDifference+    = EncodedValueDifference !JsonPointer !Value !Value+    | DecodedValueDifference !JsonPointer !Value !Value+    | GeneratedDecodeRejected !Text+    deriving stock (Eq, Show)++{- | A historical codec is an explicit value supplied by consumer-owned test+code. Its identity and version are report provenance, not dispatch keys.+-}+data HistoricalCodec a = HistoricalCodec+    { hcIdentity :: !Text+    , hcVersion :: !Text+    , hcEncode :: !(a -> Value)+    , hcDecode :: !(Value -> Either Text a)+    }++data CompareObservation+    = EncodeObservation+        { coCaseName :: !Text+        , coHistoricalValue :: !Value+        , coGeneratedValue :: !Value+        }+    | DecodeObservation+        { coFixturePath :: !FilePath+        , coInputValue :: !Value+        , coHistoricalDecode :: !DecodeOutcome+        , coGeneratedDecode :: !DecodeOutcome+        }+    deriving stock (Eq, Show)++data FixtureVerdict+    = JsonParity+    | RequiresVersionWork !ComparisonDifference+    deriving stock (Eq, Show)++data CompareInputIssue+    = HistoricalGoldenUnreadable !FilePath !Text+    | HistoricalCodecRejected !FilePath !Text+    | HistoricalCodecProvenanceInvalid !Text+    deriving stock (Eq, Show)++data BranchKind+    = UnionArm !Text+    | OptionalPresent+    | OptionalMissing+    | ExplicitNull+    deriving stock (Eq, Ord, Show)++data DeclaredBranch = DeclaredBranch+    { dbOrigin :: !FixtureOrigin+    , dbPointer :: !JsonPointer+    , dbKind :: !BranchKind+    }+    deriving stock (Eq, Ord, Show)++data ObservedBranch = ObservedBranch+    { obOrigin :: !FixtureOrigin+    , obPointer :: !JsonPointer+    , obKind :: !BranchKind+    }+    deriving stock (Eq, Ord, Show)++data CoverageGap = CoverageGap+    { cgOrigin :: !FixtureOrigin+    , cgPointer :: !JsonPointer+    , cgKind :: !BranchKind+    }+    deriving stock (Eq, Ord, Show)++{- | A codec-independent branch description embedded into generated+comparison runners. The generator constructs it through the checked type+graph's total algebras, so this module never has to interpret a consumer type.+-}+data BranchSchema+    = BranchScalar+    | BranchOptional !BranchSchema+    | BranchList !BranchSchema+    | BranchMap !BranchSchema+    | BranchRecord ![BranchField]+    | BranchUnion !Text !Text ![BranchArm]+    deriving stock (Eq, Show)++data BranchField = BranchField+    { bfWireKey :: !Text+    , bfPresenceOptional :: !Bool+    , bfSchema :: !BranchSchema+    }+    deriving stock (Eq, Show)++data BranchArm = BranchArm+    { baWireTag :: !Text+    , baPayloadSchema :: !(Maybe BranchSchema)+    }+    deriving stock (Eq, Show)++data CompareProvenance = CompareProvenance+    { cpHistoricalCodecIdentity :: !Text+    , cpHistoricalCodecVersion :: !Text+    , cpCanonicalType :: !CanonicalTypeId+    , cpBindingSymbol :: !QualifiedValueName+    , cpBindingVersion :: !BindingVersion+    , cpWireFingerprint :: !Text+    }+    deriving stock (Eq, Show)++data ClassifiedObservation = ClassifiedObservation+    { classifiedOrigin :: !FixtureOrigin+    , classifiedName :: !Text+    , classifiedVerdict :: !FixtureVerdict+    }+    deriving stock (Eq, Show)++data CompareReport = CompareReport+    { crProvenance :: !CompareProvenance+    , crObservations :: ![ClassifiedObservation]+    , crInputIssues :: ![CompareInputIssue]+    , crCoverageGaps :: ![CoverageGap]+    , crAuthority :: !Text+    }+    deriving stock (Eq, Show)++data ReportWriteError = ReportWriteError+    { reportWritePath :: !FilePath+    , reportWriteMessage :: !Text+    }+    deriving stock (Eq, Show)++authorityStatement :: Text+authorityStatement =+    "This comparison is MIGRATION EVIDENCE ONLY. After cutover the generated structural codec is the sole wire authority. This runner is never a runtime fallback and never upgrades an opaque declaration to structural. Resolve each difference with an explicit version bump and upcaster, or correct the declaration to match the historical wire contract; \"close enough\" is not an outcome."++-- | Render a JSON value in RFC 8785 canonical form.+canonicalJsonBytes :: Value -> ByteString+canonicalJsonBytes = LazyByteString.toStrict . RFC8785.encodeCanonical++classifyObservation :: CompareObservation -> Either CompareInputIssue FixtureVerdict+classifyObservation observation = case observation of+    EncodeObservation _ historical generated ->+        Right (classifyValues EncodedValueDifference historical generated)+    DecodeObservation fixturePath _ historical generated -> case historical of+        DecodeFailed reason -> Left (HistoricalCodecRejected fixturePath reason)+        DecodedShape historicalValue -> case generated of+            DecodeFailed reason -> Right (RequiresVersionWork (GeneratedDecodeRejected reason))+            DecodedShape generatedValue ->+                Right (classifyValues DecodedValueDifference historicalValue generatedValue)++classifyValues :: (JsonPointer -> Value -> Value -> ComparisonDifference) -> Value -> Value -> FixtureVerdict+classifyValues difference historical generated+    | canonicalJsonBytes historical == canonicalJsonBytes generated = JsonParity+    | otherwise = RequiresVersionWork (difference (firstDivergentPointer historical generated) historical generated)++compareReport ::+    CompareProvenance ->+    [CompareInputIssue] ->+    [CompareObservation] ->+    [DeclaredBranch] ->+    [ObservedBranch] ->+    CompareReport+compareReport provenance suppliedIssues observations declaredBranches observedBranches =+    CompareReport+        { crProvenance = provenance+        , crObservations = classified+        , crInputIssues = provenanceIssues provenance <> suppliedIssues <> classificationIssues+        , crCoverageGaps = coverageGaps declaredBranches observedBranches+        , crAuthority = authorityStatement+        }+  where+    outcomes = map classify observations+    classified = [value | Right value <- outcomes]+    classificationIssues = [issue | Left issue <- outcomes]++    classify observation = case classifyObservation observation of+        Left issue -> Left issue+        Right verdict ->+            Right+                ClassifiedObservation+                    { classifiedOrigin = observationOrigin observation+                    , classifiedName = observationName observation+                    , classifiedVerdict = verdict+                    }++observationOrigin :: CompareObservation -> FixtureOrigin+observationOrigin EncodeObservation{} = FromBinding+observationOrigin DecodeObservation{} = HistoricalGolden++observationName :: CompareObservation -> Text+observationName EncodeObservation{coCaseName = name} = name+observationName DecodeObservation{coFixturePath = path} = T.pack path++provenanceIssues :: CompareProvenance -> [CompareInputIssue]+provenanceIssues provenance =+    [ HistoricalCodecProvenanceInvalid "historical codec identity must not be blank"+    | T.null (T.strip (cpHistoricalCodecIdentity provenance))+    ]+        <> [ HistoricalCodecProvenanceInvalid "historical codec version must not be blank"+           | T.null (T.strip (cpHistoricalCodecVersion provenance))+           ]++coverageGaps :: [DeclaredBranch] -> [ObservedBranch] -> [CoverageGap]+coverageGaps declared observed =+    [ CoverageGap (dbOrigin branch) (dbPointer branch) (dbKind branch)+    | branch <- declared+    , branchKey branch `Set.notMember` observedKeys+    ]+  where+    observedKeys = Set.fromList (map observedBranchKey observed)+    branchKey branch = (dbOrigin branch, dbPointer branch, dbKind branch)+    observedBranchKey branch = (obOrigin branch, obPointer branch, obKind branch)++declaredBranchesFor :: FixtureOrigin -> BranchSchema -> [DeclaredBranch]+declaredBranchesFor origin = Set.toAscList . go ""+  where+    declared pointer kind = Set.singleton (DeclaredBranch origin (JsonPointer pointer) kind)+    go pointer schema = case schema of+        BranchScalar -> Set.empty+        BranchOptional nested ->+            declared pointer OptionalPresent+                <> declared pointer ExplicitNull+                <> go pointer nested+        BranchList nested -> go (appendPointer pointer "*") nested+        BranchMap nested -> go (appendPointer pointer "*") nested+        BranchRecord fields ->+            Set.unions+                [ presenceBranches pointer field <> go (appendPointer pointer (bfWireKey field)) (bfSchema field)+                | field <- fields+                ]+        BranchUnion _tagField contentsField arms ->+            Set.unions+                [ declared pointer (UnionArm (baWireTag arm))+                    <> maybe Set.empty (go (appendPointer pointer contentsField)) (baPayloadSchema arm)+                | arm <- arms+                ]+    presenceBranches pointer field+        | bfPresenceOptional field =+            let fieldPointer = appendPointer pointer (bfWireKey field)+             in case origin of+                    HistoricalGolden -> declared fieldPointer OptionalMissing <> declared fieldPointer OptionalPresent+                    FromBinding -> declared fieldPointer OptionalPresent+        | otherwise = Set.empty++observedBranchesFor :: FixtureOrigin -> BranchSchema -> Value -> [ObservedBranch]+observedBranchesFor origin schema = Set.toAscList . go "" schema+  where+    observed pointer kind = Set.singleton (ObservedBranch origin (JsonPointer pointer) kind)+    go pointer branchSchema value = case branchSchema of+        BranchScalar -> Set.empty+        BranchOptional nested -> case value of+            Null -> observed pointer ExplicitNull+            _ -> observed pointer OptionalPresent <> go pointer nested value+        BranchList nested -> case value of+            Array values -> Set.unions [go (appendPointer pointer "*") nested item | item <- toList values]+            _ -> Set.empty+        BranchMap nested -> case value of+            Object values -> Set.unions [go (appendPointer pointer "*") nested item | item <- KeyMap.elems values]+            _ -> Set.empty+        BranchRecord fields -> case value of+            Object values -> Set.unions (map (observeField pointer values) fields)+            _ -> Set.empty+        BranchUnion tagField contentsField arms -> case value of+            Object values -> case KeyMap.lookup (Key.fromText tagField) values of+                Just (String tag) -> case filter ((== tag) . baWireTag) arms of+                    arm : _ ->+                        observed pointer (UnionArm tag)+                            <> case (baPayloadSchema arm, KeyMap.lookup (Key.fromText contentsField) values) of+                                (Just nested, Just payload) -> go (appendPointer pointer contentsField) nested payload+                                _ -> Set.empty+                    [] -> Set.empty+                _ -> Set.empty+            _ -> Set.empty+    observeField pointer values field =+        let fieldPointer = appendPointer pointer (bfWireKey field)+         in case KeyMap.lookup (Key.fromText (bfWireKey field)) values of+                Nothing+                    | bfPresenceOptional field -> observed fieldPointer OptionalMissing+                    | otherwise -> Set.empty+                Just fieldValue ->+                    (if bfPresenceOptional field then observed fieldPointer OptionalPresent else Set.empty)+                        <> go fieldPointer (bfSchema field) fieldValue++reportSucceeded :: CompareReport -> Bool+reportSucceeded report =+    null (crInputIssues report)+        && null (crCoverageGaps report)+        && all ((== JsonParity) . classifiedVerdict) (crObservations report)++renderCompareReport :: CompareReport -> Text+renderCompareReport report =+    T.unlines+        ( [ "codec comparison: "+                <> unCanonicalTypeId (cpCanonicalType provenance)+                <> " (binding-version \""+                <> unBindingVersion (cpBindingVersion provenance)+                <> "\")"+          , "historical codec: \""+                <> cpHistoricalCodecIdentity provenance+                <> "\" version \""+                <> cpHistoricalCodecVersion provenance+                <> "\""+          , "observations: " <> tshow (length observations)+          , "  encode parity: " <> ratio FromBinding+          , "  structural decode agreement: " <> ratio HistoricalGolden+          , "requires explicit version/upcaster work: " <> tshow (length differences) <> " observations  [" <> codeText CodecCompareDifference <> "]"+          ]+            <> concatMap renderDifference differences+            <> [ "input issues: " <> tshow (length (crInputIssues report)) <> "  [" <> codeText CodecCompareInvalidInput <> "]"+               ]+            <> map ("  " <>) (map renderInputIssue (crInputIssues report))+            <> [ "coverage gaps: " <> tshow (length (crCoverageGaps report)) <> "  [" <> codeText CodecCompareCoverageGap <> "]"+               ]+            <> map ("  " <>) (map renderCoverageGap (crCoverageGaps report))+            <> [ if reportSucceeded report+                    then "result: PARITY"+                    else "result: NOT PARITY — " <> tshow (length differences) <> " differences"+               , crAuthority report+               ]+        )+  where+    provenance = crProvenance report+    observations = crObservations report+    differences = filter ((/= JsonParity) . classifiedVerdict) observations+    ratio origin =+        let matching = filter ((== origin) . classifiedOrigin) observations+            parityCount = length (filter ((== JsonParity) . classifiedVerdict) matching)+         in tshow parityCount <> "/" <> tshow (length matching) <> suffix origin+    suffix FromBinding = " (RFC 8785 canonical form)"+    suffix HistoricalGolden = ""++renderDifference :: ClassifiedObservation -> [Text]+renderDifference observation = case classifiedVerdict observation of+    JsonParity -> []+    RequiresVersionWork difference ->+        [ "  " <> classifiedName observation <> " [" <> direction <> "] at " <> pointerOf difference+        , "    " <> reasonOf difference+        ]+  where+    direction = case classifiedOrigin observation of+        FromBinding -> "encode"+        HistoricalGolden -> "decode"++renderInputIssue :: CompareInputIssue -> Text+renderInputIssue issue = case issue of+    HistoricalGoldenUnreadable path reason -> T.pack path <> ": unreadable historical golden: " <> reason+    HistoricalCodecRejected path reason -> T.pack path <> ": historical codec rejected its alleged golden: " <> reason+    HistoricalCodecProvenanceInvalid reason -> reason++renderCoverageGap :: CoverageGap -> Text+renderCoverageGap gap =+    originName (cgOrigin gap)+        <> " "+        <> renderPointer (cgPointer gap)+        <> ": "+        <> branchKindName (cgKind gap)++pointerOf :: ComparisonDifference -> Text+pointerOf difference = case difference of+    EncodedValueDifference pointer _ _ -> renderPointer pointer+    DecodedValueDifference pointer _ _ -> renderPointer pointer+    GeneratedDecodeRejected _ -> "<root>"++reasonOf :: ComparisonDifference -> Text+reasonOf difference = case difference of+    EncodedValueDifference _ historical generated ->+        "historical and generated encoders produced different JSON values: " <> valuePair historical generated+    DecodedValueDifference _ historical generated ->+        "historical and generated decoders normalized to different structural values: " <> valuePair historical generated+    GeneratedDecodeRejected reason -> "generated structural decoder rejected historical JSON: " <> reason++valuePair :: Value -> Value -> Text+valuePair historical generated = "historical=" <> tshow historical <> "; generated=" <> tshow generated++renderPointer :: JsonPointer -> Text+renderPointer (JsonPointer pointer)+    | T.null pointer = "<root>"+    | otherwise = pointer++writeCompareReportAtomic :: FilePath -> CompareReport -> IO (Either ReportWriteError ())+writeCompareReportAtomic path report = do+    let directory = takeDirectory path+        template = takeFileName path <> ".tmp"+    result <- try $ do+        createDirectoryIfMissing True directory+        bracketOnError+            (openBinaryTempFile directory template)+            cleanupTemporary+            ( \(temporary, handle) -> do+                LazyByteString.hPut handle (Aeson.encode report)+                hClose handle+                renameFile temporary path+            )+    pure $ case result of+        Left err -> Left (ReportWriteError path (T.pack (displayException (err :: IOException))))+        Right () -> Right ()++cleanupTemporary :: (FilePath, Handle) -> IO ()+cleanupTemporary (temporary, handle) = do+    _ <- try (hClose handle) :: IO (Either IOException ())+    exists <- doesFileExist temporary+    when exists (removeFile temporary)++firstDivergentPointer :: Value -> Value -> JsonPointer+firstDivergentPointer = go ""+  where+    go pointer (Object historical) (Object generated) =+        case firstDifferentKey historical generated of+            Nothing -> JsonPointer pointer+            Just key -> case (KeyMap.lookup (Key.fromText key) historical, KeyMap.lookup (Key.fromText key) generated) of+                (Just historicalValue, Just generatedValue) -> go (appendPointer pointer key) historicalValue generatedValue+                _ -> JsonPointer (appendPointer pointer key)+    go pointer (Array historical) (Array generated) =+        let historicalValues = toList historical+            generatedValues = toList generated+         in case firstDifferentIndex historicalValues generatedValues of+                Nothing -> JsonPointer pointer+                Just index -> case (indexMaybe index historicalValues, indexMaybe index generatedValues) of+                    (Just historicalValue, Just generatedValue) -> go (appendPointer pointer (tshow index)) historicalValue generatedValue+                    _ -> JsonPointer (appendPointer pointer (tshow index))+    go pointer _ _ = JsonPointer pointer++firstDifferentKey :: KeyMap.KeyMap Value -> KeyMap.KeyMap Value -> Maybe Text+firstDifferentKey historical generated =+    firstMatch differs allKeys+  where+    allKeys = sort (map Key.toText (KeyMap.keys historical <> KeyMap.keys generated))+    differs key = KeyMap.lookup (Key.fromText key) historical /= KeyMap.lookup (Key.fromText key) generated++firstDifferentIndex :: [Value] -> [Value] -> Maybe Int+firstDifferentIndex historical generated =+    firstMatch differs [0 .. max (length historical) (length generated) - 1]+  where+    differs index = indexMaybe index historical /= indexMaybe index generated++indexMaybe :: Int -> [a] -> Maybe a+indexMaybe index values = case drop index values of+    value : _ -> Just value+    [] -> Nothing++firstMatch :: (a -> Bool) -> [a] -> Maybe a+firstMatch predicate = \case+    [] -> Nothing+    value : rest+        | predicate value -> Just value+        | otherwise -> firstMatch predicate rest++appendPointer :: Text -> Text -> Text+appendPointer base segment = base <> "/" <> escapePointerSegment segment++escapePointerSegment :: Text -> Text+escapePointerSegment = T.replace "/" "~1" . T.replace "~" "~0"++tshow :: (Show a) => a -> Text+tshow = T.pack . show++codeText :: DiagnosticCode -> Text+codeText = T.pack . show++originName :: FixtureOrigin -> Text+originName HistoricalGolden = "historical-golden"+originName FromBinding = "typed-fixture"++parseOrigin :: Text -> Parser FixtureOrigin+parseOrigin "historical-golden" = pure HistoricalGolden+parseOrigin "typed-fixture" = pure FromBinding+parseOrigin value = fail ("unknown fixture origin: " <> T.unpack value)++branchKindName :: BranchKind -> Text+branchKindName kind = case kind of+    UnionArm arm -> "union-arm:" <> arm+    OptionalPresent -> "optional-present"+    OptionalMissing -> "optional-missing"+    ExplicitNull -> "explicit-null"++parseBranchKind :: Text -> Parser BranchKind+parseBranchKind value+    | Just arm <- T.stripPrefix "union-arm:" value = pure (UnionArm arm)+    | value == "optional-present" = pure OptionalPresent+    | value == "optional-missing" = pure OptionalMissing+    | value == "explicit-null" = pure ExplicitNull+    | otherwise = fail ("unknown branch kind: " <> T.unpack value)++instance ToJSON FixtureOrigin where+    toJSON = String . originName++instance FromJSON FixtureOrigin where+    parseJSON = withText "FixtureOrigin" parseOrigin++instance ToJSON JsonPointer where+    toJSON = String . unJsonPointer++instance FromJSON JsonPointer where+    parseJSON = withText "JsonPointer" (pure . JsonPointer)++instance ToJSON BranchKind where+    toJSON = String . branchKindName++instance FromJSON BranchKind where+    parseJSON = withText "BranchKind" parseBranchKind++instance ToJSON ComparisonDifference where+    toJSON difference = case difference of+        EncodedValueDifference pointer historical generated ->+            differenceObject "encoded-value-difference" pointer "encoder outputs differ" historical generated+        DecodedValueDifference pointer historical generated ->+            differenceObject "decoded-value-difference" pointer "normalized decoder outputs differ" historical generated+        GeneratedDecodeRejected reason ->+            object+                [ "kind" .= ("generated-decode-rejected" :: Text)+                , "pointer" .= JsonPointer ""+                , "reason" .= reason+                ]+      where+        differenceObject kind pointer reason historical generated =+            object+                [ "kind" .= (kind :: Text)+                , "pointer" .= pointer+                , "reason" .= (reason :: Text)+                , "historical" .= historical+                , "generated" .= generated+                ]++instance FromJSON ComparisonDifference where+    parseJSON = withObject "ComparisonDifference" $ \value -> do+        kind <- value .: "kind" :: Parser Text+        case kind of+            "encoded-value-difference" -> EncodedValueDifference <$> value .: "pointer" <*> value .: "historical" <*> value .: "generated"+            "decoded-value-difference" -> DecodedValueDifference <$> value .: "pointer" <*> value .: "historical" <*> value .: "generated"+            "generated-decode-rejected" -> GeneratedDecodeRejected <$> value .: "reason"+            _ -> fail ("unknown comparison difference: " <> T.unpack kind)++instance ToJSON FixtureVerdict where+    toJSON JsonParity = object ["verdict" .= ("json-parity" :: Text)]+    toJSON (RequiresVersionWork difference) =+        object+            [ "verdict" .= ("requires-version-work" :: Text)+            , "code" .= codeText CodecCompareDifference+            , "difference" .= difference+            ]++instance FromJSON FixtureVerdict where+    parseJSON = withObject "FixtureVerdict" $ \value -> do+        verdict <- value .: "verdict" :: Parser Text+        case verdict of+            "json-parity" -> pure JsonParity+            "requires-version-work" -> RequiresVersionWork <$> value .: "difference"+            _ -> fail ("unknown fixture verdict: " <> T.unpack verdict)++instance ToJSON CompareInputIssue where+    toJSON issue = case issue of+        HistoricalGoldenUnreadable path reason -> issueObject "historical-golden-unreadable" path reason+        HistoricalCodecRejected path reason -> issueObject "historical-codec-rejected" path reason+        HistoricalCodecProvenanceInvalid reason ->+            object+                [ "code" .= codeText CodecCompareInvalidInput+                , "kind" .= ("historical-codec-provenance-invalid" :: Text)+                , "reason" .= reason+                ]+      where+        issueObject kind path reason =+            object+                [ "code" .= codeText CodecCompareInvalidInput+                , "kind" .= (kind :: Text)+                , "path" .= path+                , "reason" .= reason+                ]++instance FromJSON CompareInputIssue where+    parseJSON = withObject "CompareInputIssue" $ \value -> do+        kind <- value .: "kind" :: Parser Text+        case kind of+            "historical-golden-unreadable" -> HistoricalGoldenUnreadable <$> value .: "path" <*> value .: "reason"+            "historical-codec-rejected" -> HistoricalCodecRejected <$> value .: "path" <*> value .: "reason"+            "historical-codec-provenance-invalid" -> HistoricalCodecProvenanceInvalid <$> value .: "reason"+            _ -> fail ("unknown comparison input issue: " <> T.unpack kind)++instance ToJSON DeclaredBranch where+    toJSON branch =+        object+            [ "origin" .= dbOrigin branch+            , "pointer" .= dbPointer branch+            , "branch" .= dbKind branch+            ]++instance FromJSON DeclaredBranch where+    parseJSON = withObject "DeclaredBranch" $ \value ->+        DeclaredBranch <$> value .: "origin" <*> value .: "pointer" <*> value .: "branch"++instance ToJSON ObservedBranch where+    toJSON branch =+        object+            [ "origin" .= obOrigin branch+            , "pointer" .= obPointer branch+            , "branch" .= obKind branch+            ]++instance FromJSON ObservedBranch where+    parseJSON = withObject "ObservedBranch" $ \value ->+        ObservedBranch <$> value .: "origin" <*> value .: "pointer" <*> value .: "branch"++instance ToJSON CoverageGap where+    toJSON gap =+        object+            [ "code" .= codeText CodecCompareCoverageGap+            , "origin" .= cgOrigin gap+            , "pointer" .= cgPointer gap+            , "branch" .= cgKind gap+            ]++instance FromJSON CoverageGap where+    parseJSON = withObject "CoverageGap" $ \value ->+        CoverageGap <$> value .: "origin" <*> value .: "pointer" <*> value .: "branch"++instance ToJSON CompareProvenance where+    toJSON provenance =+        object+            [ "historicalCodecIdentity" .= cpHistoricalCodecIdentity provenance+            , "historicalCodecVersion" .= cpHistoricalCodecVersion provenance+            , "canonicalType" .= unCanonicalTypeId (cpCanonicalType provenance)+            , "bindingSymbol" .= unQualifiedValueName (cpBindingSymbol provenance)+            , "bindingVersion" .= unBindingVersion (cpBindingVersion provenance)+            , "wireFingerprint" .= cpWireFingerprint provenance+            ]++instance FromJSON CompareProvenance where+    parseJSON = withObject "CompareProvenance" $ \value ->+        CompareProvenance+            <$> value .: "historicalCodecIdentity"+            <*> value .: "historicalCodecVersion"+            <*> (CanonicalTypeId <$> value .: "canonicalType")+            <*> (QualifiedValueName <$> value .: "bindingSymbol")+            <*> (BindingVersion <$> value .: "bindingVersion")+            <*> value .: "wireFingerprint"++instance ToJSON ClassifiedObservation where+    toJSON observation =+        object+            [ "origin" .= classifiedOrigin observation+            , "name" .= classifiedName observation+            , "result" .= classifiedVerdict observation+            ]++instance FromJSON ClassifiedObservation where+    parseJSON = withObject "ClassifiedObservation" $ \value ->+        ClassifiedObservation <$> value .: "origin" <*> value .: "name" <*> value .: "result"++instance ToJSON CompareReport where+    toJSON report =+        object+            [ "schema" .= ("keiro-dsl/codec-compare-report/1" :: Text)+            , "authority" .= crAuthority report+            , "provenance" .= crProvenance report+            , "success" .= reportSucceeded report+            , "summary"+                .= object+                    [ "observations" .= length (crObservations report)+                    , "parity" .= length (filter ((== JsonParity) . classifiedVerdict) (crObservations report))+                    , "differences" .= length (filter ((/= JsonParity) . classifiedVerdict) (crObservations report))+                    , "inputIssues" .= length (crInputIssues report)+                    , "coverageGaps" .= length (crCoverageGaps report)+                    ]+            , "observations" .= crObservations report+            , "inputIssues" .= crInputIssues report+            , "coverageGaps" .= crCoverageGaps report+            ]++instance FromJSON CompareReport where+    parseJSON = withObject "CompareReport" $ \value ->+        CompareReport+            <$> value .: "provenance"+            <*> value .: "observations"+            <*> value .: "inputIssues"+            <*> value .: "coverageGaps"+            <*> value .: "authority"
+ src/Keiro/Dsl/Coverage.hs view
@@ -0,0 +1,650 @@+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++{- | Reporting-only structural coverage over the checked mapped-type graph.++The report intentionally has no aggregate percentage. Private persisted event+payloads and mapped register cache boundaries have different authorities, and+queue/public-contract payloads are not represented by this graph at all.+-}+module Keiro.Dsl.Coverage (+    CoverageSurface (..),+    CoverageMode (..),+    CoverageRoot (..),+    StructuralBoundary (..),+    OpaqueBoundary (..),+    JsonBoundary (..),+    SnapshotBoundary (..),+    UnsupportedSurface (..),+    CoverageCounts (..),+    CoverageSummary (..),+    CoverageFinding (..),+    CoveragePrevious (..),+    CoverageDelta (..),+    CoverageReport (..),+    coverageReport,+    coverageDiffReport,+    failOnOpaque,+    failOnOpaqueIncrease,+    coverageSucceeded,+    renderCoverageSummary,+    renderCoverageFinding,+    writeCoverageReport,+) where++import Data.Aeson (ToJSON (..), object, (.=))+import Data.Aeson qualified as Aeson+import Data.List (sortOn)+import Data.List.NonEmpty (NonEmpty)+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Grammar+import Keiro.Dsl.TypeGraph+import Keiro.Dsl.Validate (DiagnosticCode (..), Severity (..))+import System.Directory (createDirectoryIfMissing)+import System.FilePath (takeDirectory)++data CoverageSurface = PrivateEventPayload | SnapshotRegister+    deriving stock (Eq, Ord, Show)++data CoverageMode = StructuralCoverage | OpaqueCoverage+    deriving stock (Eq, Ord, Show)++data CoverageRoot = CoverageRoot+    { rootSurface :: !CoverageSurface+    , rootPath :: !Text+    , rootMappedType :: !Text+    , rootMode :: !CoverageMode+    , rootCanonicalType :: !(Maybe Text)+    , rootCodecIdentity :: !(Maybe Text)+    , rootCodecVersion :: !(Maybe Text)+    , rootWireFingerprint :: !Text+    }+    deriving stock (Eq, Ord, Show)++data StructuralBoundary = StructuralBoundary+    { structuralRoot :: !Text+    , structuralPath :: !Text+    , structuralMappedType :: !Text+    , structuralCanonicalType :: !Text+    , structuralWireFingerprint :: !Text+    }+    deriving stock (Eq, Ord, Show)++data OpaqueBoundary = OpaqueBoundary+    { opaqueRoot :: !Text+    , opaquePath :: !Text+    , opaqueMappedType :: !Text+    , opaqueCodecIdentity :: !Text+    , opaqueCodecVersion :: !Text+    }+    deriving stock (Eq, Ord, Show)++data JsonBoundary = JsonBoundary+    { jsonRoot :: !Text+    , jsonPath :: !Text+    }+    deriving stock (Eq, Ord, Show)++data SnapshotBoundary = SnapshotBoundary+    { snapshotRoot :: !Text+    , snapshotAggregate :: !Text+    , snapshotRegister :: !Text+    , snapshotMappedType :: !Text+    , snapshotMode :: !CoverageMode+    , snapshotEncoding :: !Text+    , snapshotInvalidation :: !Text+    , snapshotWireFingerprint :: !Text+    , snapshotEnabled :: !Bool+    }+    deriving stock (Eq, Ord, Show)++data UnsupportedSurface = UnsupportedSurface+    { unsupportedSurface :: !Text+    , unsupportedSupport :: !Text+    , unsupportedReason :: !Text+    }+    deriving stock (Eq, Ord, Show)++data CoverageCounts = CoverageCounts+    { totalRoots :: !Int+    , structuralRoots :: !Int+    , opaqueRoots :: !Int+    , jsonBoundaries :: !Int+    }+    deriving stock (Eq, Show)++data CoverageSummary = CoverageSummary+    { privateEventPayloads :: !CoverageCounts+    , snapshotRegisters :: !CoverageCounts+    }+    deriving stock (Eq, Show)++data CoverageFinding = CoverageFinding+    { findingSeverity :: !Severity+    , findingCode :: !DiagnosticCode+    , findingRoots :: ![Text]+    , findingMessage :: !Text+    }+    deriving stock (Eq, Show)++data CoveragePrevious = CoveragePrevious+    { previousReference :: !Text+    , previousSummary :: !CoverageSummary+    , previousOpaqueBoundaries :: ![OpaqueBoundary]+    }+    deriving stock (Eq, Show)++data CoverageDelta = CoverageDelta+    { privateEventRootDelta :: !Int+    , snapshotRegisterRootDelta :: !Int+    , opaqueBoundaryDelta :: !Int+    , addedOpaqueBoundaries :: ![OpaqueBoundary]+    , removedOpaqueBoundaries :: ![OpaqueBoundary]+    }+    deriving stock (Eq, Show)++data CoverageReport = CoverageReport+    { coverageSpec :: !FilePath+    , coverageRoots :: ![CoverageRoot]+    , coverageStructuralBoundaries :: ![StructuralBoundary]+    , coverageOpaqueBoundaries :: ![OpaqueBoundary]+    , coverageJsonBoundaries :: ![JsonBoundary]+    , coverageSnapshotBoundaries :: ![SnapshotBoundary]+    , coverageUnsupportedSurfaces :: ![UnsupportedSurface]+    , coverageSummary :: !CoverageSummary+    , coverageFindings :: ![CoverageFinding]+    , coveragePrevious :: !(Maybe CoveragePrevious)+    , coverageDelta :: !(Maybe CoverageDelta)+    }+    deriving stock (Eq, Show)++coverageReport :: FilePath -> Spec -> Either (NonEmpty TypeGraphError) CoverageReport+coverageReport specPath spec = do+    graph <- resolveTypeGraph spec+    let roots = sortOn rootPath (map (coverageRoot graph) (persistedSites graph))+        structural = structuralBoundaryInventory graph+        opaque = opaqueBoundaryInventory graph+        json = jsonBoundaryInventory graph+        snapshots = snapshotBoundaryInventory spec graph+        summary = summarize roots json+        findings = opaqueSurfaceFindings opaque+    pure+        CoverageReport+            { coverageSpec = specPath+            , coverageRoots = roots+            , coverageStructuralBoundaries = structural+            , coverageOpaqueBoundaries = opaque+            , coverageJsonBoundaries = json+            , coverageSnapshotBoundaries = snapshots+            , coverageUnsupportedSurfaces = unsupportedInventory+            , coverageSummary = summary+            , coverageFindings = findings+            , coveragePrevious = Nothing+            , coverageDelta = Nothing+            }++coverageDiffReport :: FilePath -> Text -> Spec -> Spec -> Either (NonEmpty TypeGraphError) CoverageReport+coverageDiffReport specPath reference oldSpec newSpec = do+    oldReport <- coverageReport (T.unpack reference <> ":" <> specPath) oldSpec+    newReport <- coverageReport specPath newSpec+    let oldOpaque = Set.fromList (coverageOpaqueBoundaries oldReport)+        newOpaque = Set.fromList (coverageOpaqueBoundaries newReport)+        added = Set.toAscList (newOpaque `Set.difference` oldOpaque)+        removed = Set.toAscList (oldOpaque `Set.difference` newOpaque)+        oldSummary = coverageSummary oldReport+        newSummary = coverageSummary newReport+        delta =+            CoverageDelta+                { privateEventRootDelta = totalRoots (privateEventPayloads newSummary) - totalRoots (privateEventPayloads oldSummary)+                , snapshotRegisterRootDelta = totalRoots (snapshotRegisters newSummary) - totalRoots (snapshotRegisters oldSummary)+                , opaqueBoundaryDelta = length added - length removed+                , addedOpaqueBoundaries = added+                , removedOpaqueBoundaries = removed+                }+        addedFindings =+            [ CoverageFinding+                { findingSeverity = Warning+                , findingCode = CoverageOpaqueBoundaryAdded+                , findingRoots = [opaqueRoot boundary]+                , findingMessage = "opaque boundary added at " <> opaquePath boundary+                }+            | boundary <- added+            ]+    pure+        newReport+            { coverageFindings = coverageFindings newReport <> addedFindings+            , coveragePrevious =+                Just+                    CoveragePrevious+                        { previousReference = reference+                        , previousSummary = oldSummary+                        , previousOpaqueBoundaries = coverageOpaqueBoundaries oldReport+                        }+            , coverageDelta = Just delta+            }++failOnOpaque :: CoverageReport -> CoverageReport+failOnOpaque report+    | null boundaries = report+    | otherwise = report{coverageFindings = coverageFindings report <> [gateFinding "opaque persisted boundaries are forbidden by --fail-on-opaque" boundaries]}+  where+    boundaries = coverageOpaqueBoundaries report++failOnOpaqueIncrease :: CoverageReport -> CoverageReport+failOnOpaqueIncrease report = case coverageDelta report of+    Just delta+        | not (null (addedOpaqueBoundaries delta)) ->+            report+                { coverageFindings =+                    coverageFindings report+                        <> [gateFinding "new opaque persisted boundaries are forbidden by --fail-on-opaque-increase" (addedOpaqueBoundaries delta)]+                }+    _ -> report++coverageSucceeded :: CoverageReport -> Bool+coverageSucceeded = all ((/= Error) . findingSeverity) . coverageFindings++renderCoverageSummary :: CoverageReport -> Text+renderCoverageSummary report =+    T.unlines+        [ "structural/opaque boundaries (reporting only):"+        , "  private-event-payloads: " <> renderCounts (privateEventPayloads summary)+        , "  snapshot-registers: " <> renderCounts (snapshotRegisters summary) <> "; encoding=consumer-json-cache; invalidation=tracked"+        , "  queue-payloads: unsupported"+        , "  public-contracts: not-applicable (separately owned grammar)"+        ]+  where+    summary = coverageSummary report+    renderCounts counts =+        T.pack (show (totalRoots counts))+            <> " mapped roots ("+            <> T.pack (show (structuralRoots counts))+            <> " structural, "+            <> T.pack (show (opaqueRoots counts))+            <> " opaque, "+            <> T.pack (show (jsonBoundaries counts))+            <> " Json boundaries)"++renderCoverageFinding :: FilePath -> CoverageFinding -> Text+renderCoverageFinding specPath finding =+    T.pack specPath+        <> ":0: "+        <> severityText (findingSeverity finding)+        <> "["+        <> T.pack (show (findingCode finding))+        <> "]: "+        <> findingMessage finding+        <> rootsSuffix+  where+    severityText Error = "error"+    severityText Warning = "warning"+    rootsSuffix = case findingRoots finding of+        [] -> ""+        roots -> " (roots: " <> T.intercalate ", " roots <> ")"++writeCoverageReport :: FilePath -> CoverageReport -> IO ()+writeCoverageReport path report = do+    createDirectoryIfMissing True (takeDirectory path)+    Aeson.encodeFile path report++persistedSites :: TypeGraph -> [UseSite]+persistedSites = filter isPersisted . tgUseSites+  where+    isPersisted RootEventField{} = True+    isPersisted RootRegister{} = True+    isPersisted RootCommandField{} = False++coverageRoot :: TypeGraph -> UseSite -> CoverageRoot+coverageRoot graph site =+    let key = useSiteKey site+        path = renderUsePath (UsePath site [])+        fingerprint = wireFingerprint graph (unMappedKey key)+     in case Map.lookup key (tgDeclarations graph) of+            Just (ResolvedStructural declaration _) ->+                CoverageRoot+                    { rootSurface = useSiteSurface site+                    , rootPath = path+                    , rootMappedType = unMappedKey key+                    , rootMode = StructuralCoverage+                    , rootCanonicalType = Just (unCanonicalTypeId (sdCanonical declaration))+                    , rootCodecIdentity = Nothing+                    , rootCodecVersion = Nothing+                    , rootWireFingerprint = fingerprint+                    }+            Just (ResolvedOpaque declaration) ->+                CoverageRoot+                    { rootSurface = useSiteSurface site+                    , rootPath = path+                    , rootMappedType = unMappedKey key+                    , rootMode = OpaqueCoverage+                    , rootCanonicalType = Nothing+                    , rootCodecIdentity = Just (unCodecIdentity (odCodecIdentity declaration))+                    , rootCodecVersion = Just (unCodecVersion (odCodecVersion declaration))+                    , rootWireFingerprint = fingerprint+                    }+            Nothing -> error "coverageRoot: resolved use-site key missing from graph"++structuralBoundaryInventory :: TypeGraph -> [StructuralBoundary]+structuralBoundaryInventory graph =+    sortOn+        structuralPath+        [ StructuralBoundary+            { structuralRoot = rootText (upRoot path)+            , structuralPath = renderUsePath path+            , structuralMappedType = sdName declaration+            , structuralCanonicalType = unCanonicalTypeId (sdCanonical declaration)+            , structuralWireFingerprint = wireFingerprint graph (sdName declaration)+            }+        | ResolvedStructural declaration _ <- Map.elems (tgDeclarations graph)+        , path <- usePaths graph (sdName declaration)+        , isEventSite (upRoot path)+        ]++opaqueBoundaryInventory :: TypeGraph -> [OpaqueBoundary]+opaqueBoundaryInventory graph =+    sortOn+        opaquePath+        [ OpaqueBoundary+            { opaqueRoot = rootText (upRoot path)+            , opaquePath = renderUsePath path+            , opaqueMappedType = odName declaration+            , opaqueCodecIdentity = unCodecIdentity (odCodecIdentity declaration)+            , opaqueCodecVersion = unCodecVersion (odCodecVersion declaration)+            }+        | ResolvedOpaque declaration <- Map.elems (tgDeclarations graph)+        , path <- usePaths graph (odName declaration)+        , isEventSite (upRoot path)+        ]++jsonBoundaryInventory :: TypeGraph -> [JsonBoundary]+jsonBoundaryInventory graph =+    sortOn+        jsonPath+        [ JsonBoundary+            { jsonRoot = rootText site+            , jsonPath = renderUsePath (UsePath site segments)+            }+        | site <- persistedSites graph+        , isEventSite site+        , segments <- jsonPathsFromDecl graph Set.empty (useSiteKey site)+        ]++snapshotBoundaryInventory :: Spec -> TypeGraph -> [SnapshotBoundary]+snapshotBoundaryInventory spec graph =+    sortOn+        snapshotRoot+        [ SnapshotBoundary+            { snapshotRoot = renderUsePath (UsePath site [])+            , snapshotAggregate = aggregate+            , snapshotRegister = register+            , snapshotMappedType = unMappedKey key+            , snapshotMode = declarationMode declaration+            , snapshotEncoding = "consumer-json-cache"+            , snapshotInvalidation = "tracked-by-mapped-wire-fingerprint"+            , snapshotWireFingerprint = wireFingerprint graph (unMappedKey key)+            , snapshotEnabled = aggregateHasSnapshot aggregate+            }+        | site@(RootRegister aggregate register key) <- persistedSites graph+        , Just declaration <- [Map.lookup key (tgDeclarations graph)]+        ]+  where+    aggregateHasSnapshot name =+        any+            (\case NAggregate aggregate -> aggName aggregate == name && maybe False (const True) (aggSnapshot aggregate); _ -> False)+            (specNodes spec)++jsonPathsFromDecl :: TypeGraph -> Set.Set MappedKey -> MappedKey -> [[PathSeg]]+jsonPathsFromDecl graph visited key+    | key `Set.member` visited = []+    | otherwise = case Map.lookup key (tgDeclarations graph) of+        Nothing -> []+        Just declaration ->+            foldMappedDecl+                MappedDeclAlgebra+                    { onStructuralDecl = \_ shape -> jsonPathsFromShape graph (Set.insert key visited) shape+                    , onOpaqueDecl = const []+                    }+                declaration++jsonPathsFromShape :: TypeGraph -> Set.Set MappedKey -> ResolvedMappedShape -> [[PathSeg]]+jsonPathsFromShape graph visited =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields ->+                concat+                    [ map (SegField (rwfHaskell field) (rwfKey field) :) (jsonPathsFromExpr graph visited (rwfType field))+                    | field <- fields+                    ]+            , onEnum = const []+            , onUnion = \_ arms ->+                concat+                    [ map (SegArm (rwaCtor arm) (rwaTag arm) :) (maybe [] (jsonPathsFromExpr graph visited) (rwaPayload arm))+                    | arm <- arms+                    ]+            }++jsonPathsFromExpr :: TypeGraph -> Set.Set MappedKey -> ResolvedTypeExpr -> [[PathSeg]]+jsonPathsFromExpr graph visited =+    foldTypeExpr+        TypeExprAlgebra+            { onText = []+            , onInt = []+            , onBool = []+            , onNatural = []+            , onTime = []+            , onJson = [[]]+            , onOptional = map (SegOptional :)+            , onList = map (SegElem :)+            , onMap = map (SegMapValue :)+            , onRef = \key -> map (SegDecl (unMappedKey key) :) (jsonPathsFromDecl graph visited key)+            }++summarize :: [CoverageRoot] -> [JsonBoundary] -> CoverageSummary+summarize roots json =+    CoverageSummary+        { privateEventPayloads = countsFor PrivateEventPayload+        , snapshotRegisters = countsFor SnapshotRegister+        }+  where+    countsFor surface =+        let matching = filter ((== surface) . rootSurface) roots+            jsonCount = case surface of+                PrivateEventPayload -> length json+                SnapshotRegister -> 0+         in CoverageCounts+                { totalRoots = length matching+                , structuralRoots = length (filter ((== StructuralCoverage) . rootMode) matching)+                , opaqueRoots = length (filter ((== OpaqueCoverage) . rootMode) matching)+                , jsonBoundaries = jsonCount+                }++opaqueSurfaceFindings :: [OpaqueBoundary] -> [CoverageFinding]+opaqueSurfaceFindings boundaries =+    [ CoverageFinding+        { findingSeverity = Warning+        , findingCode = CoverageOpaqueSurface+        , findingRoots = [root]+        , findingMessage = "persisted private-event root contains opaque mapped boundaries"+        }+    | root <- Set.toAscList (Set.fromList (map opaqueRoot boundaries))+    ]++gateFinding :: Text -> [OpaqueBoundary] -> CoverageFinding+gateFinding message boundaries =+    CoverageFinding+        { findingSeverity = Error+        , findingCode = CoverageOpaqueGateExceeded+        , findingRoots = Set.toAscList (Set.fromList (map opaqueRoot boundaries))+        , findingMessage = message+        }++unsupportedInventory :: [UnsupportedSurface]+unsupportedInventory =+    [ UnsupportedSurface+        { unsupportedSurface = "queue-payloads"+        , unsupportedSupport = "unsupported"+        , unsupportedReason = "queue payloads are not roots in the mapped-type graph"+        }+    , UnsupportedSurface+        { unsupportedSurface = "public-contracts"+        , unsupportedSupport = "not-applicable"+        , unsupportedReason = "public contracts have a separately owned grammar and compatibility surface"+        }+    ]++useSiteKey :: UseSite -> MappedKey+useSiteKey (RootCommandField _ _ _ key) = key+useSiteKey (RootEventField _ _ _ key) = key+useSiteKey (RootRegister _ _ key) = key++useSiteSurface :: UseSite -> CoverageSurface+useSiteSurface RootEventField{} = PrivateEventPayload+useSiteSurface RootRegister{} = SnapshotRegister+useSiteSurface RootCommandField{} = error "command fields are not persisted coverage roots"++isEventSite :: UseSite -> Bool+isEventSite RootEventField{} = True+isEventSite RootRegister{} = False+isEventSite RootCommandField{} = False++rootText :: UseSite -> Text+rootText site = renderUsePath (UsePath site [])++declarationMode :: ResolvedMappedDecl -> CoverageMode+declarationMode =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \_ _ -> StructuralCoverage+            , onOpaqueDecl = const OpaqueCoverage+            }++instance ToJSON CoverageSurface where+    toJSON PrivateEventPayload = toJSON ("private-event-payload" :: Text)+    toJSON SnapshotRegister = toJSON ("snapshot-register" :: Text)++instance ToJSON CoverageMode where+    toJSON StructuralCoverage = toJSON ("structural" :: Text)+    toJSON OpaqueCoverage = toJSON ("opaque" :: Text)++instance ToJSON CoverageRoot where+    toJSON root =+        object+            [ "surface" .= rootSurface root+            , "path" .= rootPath root+            , "mappedType" .= rootMappedType root+            , "mode" .= rootMode root+            , "canonicalType" .= rootCanonicalType root+            , "codecIdentity" .= rootCodecIdentity root+            , "codecVersion" .= rootCodecVersion root+            , "wireFingerprint" .= rootWireFingerprint root+            ]++instance ToJSON StructuralBoundary where+    toJSON boundary =+        object+            [ "root" .= structuralRoot boundary+            , "path" .= structuralPath boundary+            , "mappedType" .= structuralMappedType boundary+            , "canonicalType" .= structuralCanonicalType boundary+            , "wireFingerprint" .= structuralWireFingerprint boundary+            ]++instance ToJSON OpaqueBoundary where+    toJSON boundary =+        object+            [ "root" .= opaqueRoot boundary+            , "path" .= opaquePath boundary+            , "mappedType" .= opaqueMappedType boundary+            , "codecIdentity" .= opaqueCodecIdentity boundary+            , "codecVersion" .= opaqueCodecVersion boundary+            ]++instance ToJSON JsonBoundary where+    toJSON boundary = object ["root" .= jsonRoot boundary, "path" .= jsonPath boundary]++instance ToJSON SnapshotBoundary where+    toJSON boundary =+        object+            [ "root" .= snapshotRoot boundary+            , "aggregate" .= snapshotAggregate boundary+            , "register" .= snapshotRegister boundary+            , "mappedType" .= snapshotMappedType boundary+            , "mode" .= snapshotMode boundary+            , "snapshotEncoding" .= snapshotEncoding boundary+            , "invalidation" .= snapshotInvalidation boundary+            , "wireFingerprint" .= snapshotWireFingerprint boundary+            , "snapshotEnabled" .= snapshotEnabled boundary+            ]++instance ToJSON UnsupportedSurface where+    toJSON surface =+        object+            [ "surface" .= unsupportedSurface surface+            , "support" .= unsupportedSupport surface+            , "reason" .= unsupportedReason surface+            ]++instance ToJSON CoverageCounts where+    toJSON counts =+        object+            [ "totalRoots" .= totalRoots counts+            , "structuralRoots" .= structuralRoots counts+            , "opaqueRoots" .= opaqueRoots counts+            , "jsonBoundaries" .= jsonBoundaries counts+            ]++instance ToJSON CoverageSummary where+    toJSON summary =+        object+            [ "privateEventPayloads" .= privateEventPayloads summary+            , "snapshotRegisters" .= snapshotRegisters summary+            ]++instance ToJSON CoverageFinding where+    toJSON finding =+        object+            [ "severity" .= severityValue (findingSeverity finding)+            , "code" .= show (findingCode finding)+            , "roots" .= findingRoots finding+            , "message" .= findingMessage finding+            ]+      where+        severityValue Error = "error" :: Text+        severityValue Warning = "advisory"++instance ToJSON CoveragePrevious where+    toJSON previous =+        object+            [ "reference" .= previousReference previous+            , "summary" .= previousSummary previous+            , "opaqueBoundaries" .= previousOpaqueBoundaries previous+            ]++instance ToJSON CoverageDelta where+    toJSON delta =+        object+            [ "privateEventRootDelta" .= privateEventRootDelta delta+            , "snapshotRegisterRootDelta" .= snapshotRegisterRootDelta delta+            , "opaqueBoundaryDelta" .= opaqueBoundaryDelta delta+            , "addedOpaqueBoundaries" .= addedOpaqueBoundaries delta+            , "removedOpaqueBoundaries" .= removedOpaqueBoundaries delta+            ]++instance ToJSON CoverageReport where+    toJSON report =+        object+            [ "schema" .= ("keiro-dsl/coverage-report/1" :: Text)+            , "spec" .= coverageSpec report+            , "roots" .= coverageRoots report+            , "structuralBoundaries" .= coverageStructuralBoundaries report+            , "opaqueBoundaries" .= coverageOpaqueBoundaries report+            , "jsonBoundaries" .= coverageJsonBoundaries report+            , "snapshotBoundaries" .= coverageSnapshotBoundaries report+            , "unsupportedSurfaces" .= coverageUnsupportedSurfaces report+            , "summary" .= coverageSummary report+            , "findings" .= coverageFindings report+            , "previous" .= coveragePrevious report+            , "delta" .= coverageDelta report+            ]
src/Keiro/Dsl/Diff.hs view
@@ -15,6 +15,27 @@ module Keiro.Dsl.Diff (     Change (..),     ChangeKind (..),+    Label (..),+    CompatibilitySurface (..),+    SurfaceVerdict (..),+    RolloutConstraint (..),+    CompatibilityVector (..),+    ChangeContext,+    privateEventContext,+    privateEventAdditionContext,+    snapshotContext,+    queueContext,+    publicContractContext,+    persistedIdentityContext,+    consumerBuildContext,+    changeContextRoot,+    changeContextPaths,+    classifyCompatibility,+    verdictFor,+    defaultGate,+    gateWith,+    deriveLabel,+    gatedBreaking,     isBreaking,     isAdvisory,     diffSpecs,@@ -31,10 +52,22 @@  import Data.List (find, (\\)) import Data.Maybe (isJust, isNothing, mapMaybe)+import Data.Set (Set)+import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Keiro.Dsl.FoldFingerprint (aggregateFoldSurface) import Keiro.Dsl.Grammar+import Keiro.Dsl.MappedDiff (MappedFinding (..), diffMapped, renderMappedSubject)+import Keiro.Dsl.PrettyPrint (+    renderHandleSurface,+    renderResolveSurface,+    renderRouterDispatchSurface,+    renderTimerPayloadSurface,+    renderTransition,+ ) import Keiro.Dsl.ReadModelShape (registryNameFor, subscriptionNameFor)+import Keiro.Dsl.TypeGraph (UsePath (..), UseSite (..)) import Keiro.Dsl.Validate (DiagnosticCode (..))  -- | A classified spec change.@@ -44,15 +77,419 @@     | Breaking ChangeKind     deriving stock (Eq, Show) +-- | The stable headline classification retained by the text interface.+data Label = LabelAdditive | LabelAdvisory | LabelBreaking+    deriving stock (Eq, Show)++-- | Independently gateable compatibility questions for one finding.+data CompatibilitySurface+    = PrivateHistoryRead+    | OldBinaryReadNewEvents+    | SnapshotHydration+    | PublicConsumer+    | PersistedIdentity+    | ConsumerBuild+    deriving stock (Eq, Ord, Show, Enum, Bounded)++-- | A verdict on one surface.  Constructor order is deliberately not policy.+data SurfaceVerdict = VCompatible | VAdvisory | VBreaking | VNotApplicable+    deriving stock (Eq, Show)++-- | Deployment ordering that remains after byte compatibility is classified.+data RolloutConstraint+    = RolloutStopTheWorld+    | RolloutWorkersFirst+    | RolloutDrainRequired+    | RolloutProducerLast+    deriving stock (Eq, Ord, Show)++-- | The explicit, compile-forcing compatibility result for one finding.+data CompatibilityVector = CompatibilityVector+    { cvPrivateHistoryRead :: !SurfaceVerdict+    , cvOldBinaryReadNewEvents :: !SurfaceVerdict+    , cvSnapshotHydration :: !SurfaceVerdict+    , cvPublicConsumer :: !SurfaceVerdict+    , cvPersistedIdentity :: !SurfaceVerdict+    , cvConsumerBuild :: !SurfaceVerdict+    , cvRollout :: !(Set RolloutConstraint)+    }+    deriving stock (Eq, Show)++data ContextKind+    = ContextGeneral+    | ContextPrivateEvent+    | ContextPrivateEventAddition+    | ContextSnapshot+    | ContextQueue+    | ContextPublicContract+    | ContextPersistedIdentity+    | ContextConsumerBuild+    deriving stock (Eq, Show)++{- | Facts that select a compatibility row.  The constructor stays private so+callers cannot manufacture contradictory ownership and surface claims.+-}+data ChangeContext = ChangeContext+    { changeContextRoot :: !Name+    , changeContextPaths :: ![Text]+    , contextKind :: !ContextKind+    , contextOriginalLabel :: !Label+    }+    deriving stock (Eq, Show)+ data ChangeKind = ChangeKind     { ckNode :: !Name     , ckFacet :: !Text     , ckSubject :: !Text-    , ckCode :: !(Maybe DiagnosticCode)+    , ckCode :: !DiagnosticCode+    , ckContext :: !ChangeContext+    , ckVector :: !CompatibilityVector+    , ckPaths :: ![Text]     , ckDetail :: !Text     }     deriving stock (Eq, Show) +privateEventContext :: Name -> [Text] -> ChangeContext+privateEventContext root paths = ChangeContext root paths ContextPrivateEvent LabelBreaking++privateEventAdditionContext :: Name -> [Text] -> ChangeContext+privateEventAdditionContext root paths = ChangeContext root paths ContextPrivateEventAddition LabelAdvisory++snapshotContext :: Name -> [Text] -> ChangeContext+snapshotContext root paths = ChangeContext root paths ContextSnapshot LabelAdvisory++queueContext :: Name -> [Text] -> ChangeContext+queueContext root paths = ChangeContext root paths ContextQueue LabelBreaking++publicContractContext :: Name -> [Text] -> ChangeContext+publicContractContext root paths = ChangeContext root paths ContextPublicContract LabelBreaking++persistedIdentityContext :: Name -> [Text] -> ChangeContext+persistedIdentityContext root paths = ChangeContext root paths ContextPersistedIdentity LabelBreaking++consumerBuildContext :: Name -> [Text] -> ChangeContext+consumerBuildContext root paths = ChangeContext root paths ContextConsumerBuild LabelAdvisory++compatibleVector :: CompatibilityVector+compatibleVector =+    CompatibilityVector+        VCompatible+        VCompatible+        VNotApplicable+        VNotApplicable+        VNotApplicable+        VNotApplicable+        Set.empty++privateDecodeBreakingVector :: CompatibilityVector+privateDecodeBreakingVector =+    CompatibilityVector+        VBreaking+        VBreaking+        VAdvisory+        VNotApplicable+        VNotApplicable+        VNotApplicable+        (Set.singleton RolloutStopTheWorld)++persistedIdentityBreakingVector :: CompatibilityVector+persistedIdentityBreakingVector =+    CompatibilityVector+        VNotApplicable+        VNotApplicable+        VNotApplicable+        VNotApplicable+        VBreaking+        VNotApplicable+        Set.empty++publicBreakingVector :: CompatibilityVector+publicBreakingVector =+    CompatibilityVector+        VNotApplicable+        VNotApplicable+        VNotApplicable+        VBreaking+        VNotApplicable+        VNotApplicable+        (Set.singleton RolloutProducerLast)++queueBreakingVector :: CompatibilityVector+queueBreakingVector =+    CompatibilityVector+        VBreaking+        VBreaking+        VNotApplicable+        VNotApplicable+        VAdvisory+        VNotApplicable+        (Set.singleton RolloutWorkersFirst)++advisoryVector :: CompatibilitySurface -> Set RolloutConstraint -> CompatibilityVector+advisoryVector surface rollout =+    compatibleVector+        { cvPrivateHistoryRead = verdict PrivateHistoryRead+        , cvOldBinaryReadNewEvents = verdict OldBinaryReadNewEvents+        , cvSnapshotHydration = verdict SnapshotHydration+        , cvPublicConsumer = verdict PublicConsumer+        , cvPersistedIdentity = verdict PersistedIdentity+        , cvConsumerBuild = verdict ConsumerBuild+        , cvRollout = rollout+        }+  where+    verdict candidate+        | candidate == surface = VAdvisory+        | otherwise = verdictFor candidate compatibleVector++{- | Classify one code at an explicitly owned use site.  Codes emitted by the+differ are grouped by their actual persisted/public surface; the context is+load-bearing for codes such as 'EnumCtorAdded' that vary by use site.+-}+classifyCompatibility :: ChangeContext -> DiagnosticCode -> CompatibilityVector+classifyCompatibility context code+    | code == MappedFieldAddedWithDefault = mappedFieldAdditionVector context+    | code `elem` [MappedArmAdded, MappedEnumValueAdded] = mappedDirectionalAdditionVector context+    | code `elem` mappedWireBreakingCodes = mappedWireBreakingVector context+    | code `elem` [MappedHaskellSourceChanged, MappedRecordConstructorChanged, MappedFixturesChanged] = mappedBuildVector+    | code == MappedBindingChanged = mappedBindingVector context+    | code `elem` [MappedInitialChanged, MappedCanonicalTypeChanged] = mappedSnapshotBuildVector context+    | code == MappedDeclAdded = compatibleVector+    | code `elem` privateDecodeCodes = privateDecodeBreakingVector+    | code `elem` identityCodes = persistedIdentityBreakingVector+    | code `elem` publicBreakingCodes = publicBreakingVector+    | code `elem` queueBreakingCodes = queueBreakingVector+    | code `elem` readModelBreakingCodes = persistedIdentityBreakingVector+    | code == ContractSchemaVersionBumped = advisoryVector PublicConsumer (Set.singleton RolloutProducerLast)+    | code == AggFoldSurfaceChanged =+        (advisoryVector PrivateHistoryRead Set.empty){cvSnapshotHydration = VAdvisory}+    | code == AggGuardTightened = advisoryVector PrivateHistoryRead Set.empty+    | code `elem` [RouterDecideSurfaceChanged, ProcessDecideSurfaceChanged] =+        compatibleVector{cvRollout = Set.singleton RolloutDrainRequired}+    | code == ProcessTimerPayloadChanged = advisoryVector PrivateHistoryRead (Set.singleton RolloutProducerLast)+    | code == TimerWindowChanged = advisoryVector PrivateHistoryRead Set.empty+    | code == ProjectionChanged = advisoryVector PersistedIdentity Set.empty+    | code == EmitMappingChanged = advisoryVector PublicConsumer (Set.singleton RolloutProducerLast)+    | code == DecodePostureChanged = advisoryVector PublicConsumer Set.empty+    | code == IntakePersistenceChanged = advisoryVector PrivateHistoryRead Set.empty+    | code `elem` [PublisherPolicyChanged, DispatchRetargeted] = advisoryVector PersistedIdentity Set.empty+    | code `elem` [DeprecatedEventReplayHazard, EventRetirementInProgress] = advisoryVector PrivateHistoryRead Set.empty+    | code == EventUndeprecated = advisoryVector OldBinaryReadNewEvents (Set.singleton RolloutProducerLast)+    | code == EnumCtorAdded = case contextKind context of+        ContextPrivateEventAddition ->+            compatibleVector+                { cvOldBinaryReadNewEvents = VBreaking+                , cvRollout = Set.singleton RolloutProducerLast+                }+        ContextSnapshot -> advisoryVector SnapshotHydration Set.empty+        _ -> compatibleVector+    | code `elem` additiveCodes = compatibleVector+    | otherwise = case contextOriginalLabel context of+        LabelAdditive -> compatibleVector+        LabelAdvisory -> advisoryVector (surfaceForContext context) Set.empty+        LabelBreaking -> breakingVectorForContext context+  where+    privateDecodeCodes =+        [ EvtFieldAddedWithoutBump+        , EvtFieldRemovedSameVersion+        , EvtFieldTypeChanged+        , EvtVersionDecreased+        , EvtVersionMissingUpcaster+        , UpcasterChainGap+        , EvtRemovedNotDeprecated+        , EnumCtorRemoved+        , EnumWireSpellingChanged+        , WireSpecChanged+        , ProcessInputChanged+        , WorkflowShapeChanged+        , WorkflowBodyChanged+        , WorkflowPatchRemoved+        , WorkflowContinueSeedChanged+        ]+    identityCodes =+        [ DerivedIdentityChanged+        , IdPrefixChanged+        , DedupeIdentityChanged+        , QueueIdentityChanged+        , RouterStableNameChanged+        , WorkflowStableNameChanged+        ]+    publicBreakingCodes =+        [ ContractEventRemoved+        , ContractFieldChanged+        , ContractDiscriminatorChanged+        , ContractTopicChanged+        , ContractSchemaVersionDecreased+        ]+    queueBreakingCodes = [WqPayloadFieldChanged, WqOrderingChanged, WqProvisionChanged, WqGroupKeyChanged]+    readModelBreakingCodes =+        [ ReadModelVersionDecreased+        , ReadModelShapeChangedWithoutBump+        , ReadModelFeedChanged+        , ReadModelConsistencyWeakened+        ]+    additiveCodes =+        [ DeclarationAdded+        , VersionBumped+        , CompatibilityStrengthened+        , EventRetirementAbandoned+        , ContractEventAdded+        , ContractTopicAdded+        , WorkflowEvolutionGuardAdded+        ]++mappedWireBreakingCodes :: [DiagnosticCode]+mappedWireBreakingCodes =+    [ MappedFieldAddedNoDefault+    , MappedFieldRemoved+    , MappedFieldTypeChanged+    , MappedPresenceChanged+    , MappedNullabilityChanged+    , MappedDefaultRemoved+    , MappedDefaultChanged+    , MappedWireKeyChanged+    , MappedUnionEncodingChanged+    , MappedArmRemoved+    , MappedArmTagChanged+    , MappedEnumValueRemoved+    , MappedEnumSpellingChanged+    , MappedOpaqueCodecChanged+    , MappedModeCrossed+    , MappedDeclRemoved+    ]++mappedFieldAdditionVector :: ChangeContext -> CompatibilityVector+mappedFieldAdditionVector context = case contextKind context of+    ContextPrivateEvent ->+        compatibleVector+            { cvOldBinaryReadNewEvents = oldBinaryVerdict+            , cvRollout = rollout+            }+      where+        rejectsUnknown = contextOriginalLabel context == LabelBreaking+        oldBinaryVerdict = if rejectsUnknown then VBreaking else VCompatible+        rollout = if rejectsUnknown then Set.singleton RolloutProducerLast else Set.empty+    ContextSnapshot -> mappedSnapshotVector+    ContextConsumerBuild -> mappedBuildVector+    _ -> compatibleVector++mappedDirectionalAdditionVector :: ChangeContext -> CompatibilityVector+mappedDirectionalAdditionVector context = case contextKind context of+    ContextPrivateEvent ->+        compatibleVector+            { cvOldBinaryReadNewEvents = VBreaking+            , cvRollout = Set.singleton RolloutProducerLast+            }+    ContextSnapshot -> mappedSnapshotVector+    ContextConsumerBuild -> mappedBuildVector+    _ -> compatibleVector++mappedWireBreakingVector :: ChangeContext -> CompatibilityVector+mappedWireBreakingVector context = case contextKind context of+    ContextPrivateEvent ->+        CompatibilityVector+            VBreaking+            VBreaking+            VNotApplicable+            VNotApplicable+            VNotApplicable+            VNotApplicable+            (Set.singleton RolloutStopTheWorld)+    ContextSnapshot -> mappedSnapshotVector+    ContextConsumerBuild -> mappedBuildVector+    _ -> mappedBuildVector++mappedBuildVector :: CompatibilityVector+mappedBuildVector =+    CompatibilityVector+        VCompatible+        VCompatible+        VNotApplicable+        VNotApplicable+        VNotApplicable+        VAdvisory+        Set.empty++mappedSnapshotVector :: CompatibilityVector+mappedSnapshotVector =+    CompatibilityVector+        VCompatible+        VCompatible+        VAdvisory+        VNotApplicable+        VNotApplicable+        VNotApplicable+        Set.empty++mappedBindingVector :: ChangeContext -> CompatibilityVector+mappedBindingVector context = case contextKind context of+    ContextPrivateEvent ->+        CompatibilityVector+            VAdvisory+            VAdvisory+            VNotApplicable+            VNotApplicable+            VNotApplicable+            VAdvisory+            Set.empty+    ContextSnapshot ->+        mappedSnapshotVector{cvConsumerBuild = VAdvisory}+    _ -> mappedBuildVector++mappedSnapshotBuildVector :: ChangeContext -> CompatibilityVector+mappedSnapshotBuildVector context = case contextKind context of+    ContextSnapshot -> mappedSnapshotVector{cvConsumerBuild = VAdvisory}+    _ -> mappedBuildVector++surfaceForContext :: ChangeContext -> CompatibilitySurface+surfaceForContext context = case contextKind context of+    ContextPrivateEvent -> PrivateHistoryRead+    ContextPrivateEventAddition -> OldBinaryReadNewEvents+    ContextSnapshot -> SnapshotHydration+    ContextQueue -> PrivateHistoryRead+    ContextPublicContract -> PublicConsumer+    ContextPersistedIdentity -> PersistedIdentity+    ContextConsumerBuild -> ConsumerBuild+    ContextGeneral -> PrivateHistoryRead++breakingVectorForContext :: ChangeContext -> CompatibilityVector+breakingVectorForContext context = case contextKind context of+    ContextPublicContract -> publicBreakingVector+    ContextPersistedIdentity -> persistedIdentityBreakingVector+    ContextQueue -> queueBreakingVector+    ContextConsumerBuild -> (advisoryVector ConsumerBuild Set.empty){cvConsumerBuild = VBreaking}+    _ -> privateDecodeBreakingVector++verdictFor :: CompatibilitySurface -> CompatibilityVector -> SurfaceVerdict+verdictFor surface vector = case surface of+    PrivateHistoryRead -> cvPrivateHistoryRead vector+    OldBinaryReadNewEvents -> cvOldBinaryReadNewEvents vector+    SnapshotHydration -> cvSnapshotHydration vector+    PublicConsumer -> cvPublicConsumer vector+    PersistedIdentity -> cvPersistedIdentity vector+    ConsumerBuild -> cvConsumerBuild vector++defaultGate :: Set CompatibilitySurface+defaultGate = Set.delete OldBinaryReadNewEvents (Set.fromList [minBound .. maxBound])++gateWith :: [CompatibilitySurface] -> Set CompatibilitySurface+gateWith surfaces = defaultGate <> Set.fromList surfaces++deriveLabel :: Set CompatibilitySurface -> CompatibilityVector -> Label+deriveLabel gate vector+    | any ((== VBreaking) . (`verdictFor` vector)) (Set.toList gate) = LabelBreaking+    | any (`elem` [VAdvisory, VBreaking]) verdicts || not (Set.null (cvRollout vector)) = LabelAdvisory+    | otherwise = LabelAdditive+  where+    verdicts = [verdictFor surface vector | surface <- [minBound .. maxBound]]++gatedBreaking :: Set CompatibilitySurface -> Change -> Bool+gatedBreaking gate change = deriveLabel gate (ckVector (changeKind change)) == LabelBreaking++changeKind :: Change -> ChangeKind+changeKind (Additive kind) = kind+changeKind (Advisory kind) = kind+changeKind (Breaking kind) = kind+ isBreaking :: Change -> Bool isBreaking (Breaking _) = True isBreaking (Additive _) = False@@ -168,12 +605,75 @@ runFamily env (DiffFamily f) = f env runFamily _ (OutOfDiffScope _) = [] --- Rules are intentionally outside the decode/identity axes: they alter guard--- behaviour but neither interpret stored bytes nor derive persisted keys.--- Shared id and enum declarations become diffed in Milestones 2 and 4.+-- Rules are outside the decode and persisted-identity axes, but referenced+-- rule bodies are compared as part of each aggregate's replay fold surface. sharedDeclarationDiff :: DiffEnv -> [Change]-sharedDeclarationDiff env = enumDiff env ++ idDiff env+sharedDeclarationDiff env = enumDiff env ++ idDiff env ++ mappedDeclarationDiff env +mappedDeclarationDiff :: DiffEnv -> [Change]+mappedDeclarationDiff env = concatMap mappedFindingChanges (diffMapped (deOld env) (deNew env))++mappedFindingChanges :: MappedFinding -> [Change]+mappedFindingChanges finding+    | mfCode finding == MappedDeclAdded = [mappedDeclarationChange LabelAdditive finding]+    | mfCode finding `elem` [MappedHaskellSourceChanged, MappedRecordConstructorChanged, MappedFixturesChanged] =+        [mappedBuildChange finding]+    | mfCode finding `elem` [MappedInitialChanged, MappedCanonicalTypeChanged] =+        mappedBuildChange finding : map (mappedUseChange finding) registerPaths+    | null paths = [mappedBuildChange finding]+    | otherwise = map (mappedUseChange finding) paths+  where+    paths = mfUsePaths finding+    registerPaths = [path | path@UsePath{upRoot = RootRegister{}} <- paths]++mappedBuildChange :: MappedFinding -> Change+mappedBuildChange finding =+    mappedChange context (mfDeclaration finding) "mapped-build" subject finding+  where+    subject = declarationSubject finding+    renderedPaths = map (\path -> renderMappedSubject path (mfLeaf finding)) (mfUsePaths finding)+    context = (consumerBuildContext (mfDeclaration finding) renderedPaths){contextOriginalLabel = LabelAdvisory}++mappedDeclarationChange :: Label -> MappedFinding -> Change+mappedDeclarationChange label finding =+    mappedChange context (mfDeclaration finding) "mapped-declaration" (declarationSubject finding) finding+  where+    context = ChangeContext (mfDeclaration finding) [] ContextGeneral label++mappedUseChange :: MappedFinding -> UsePath -> Change+mappedUseChange finding path =+    mappedChange context root facet subject finding+  where+    subject = renderMappedSubject path (mfLeaf finding)+    (root, facet, kind) = case upRoot path of+        RootCommandField aggregate _ _ _ -> (aggregate, "mapped-command", ContextConsumerBuild)+        RootEventField aggregate _ _ _ -> (aggregate, "mapped-event", ContextPrivateEvent)+        RootRegister aggregate _ _ -> (aggregate, "mapped-register", ContextSnapshot)+    context = ChangeContext root [subject] kind (mappedContextHint finding kind)++mappedContextHint :: MappedFinding -> ContextKind -> Label+mappedContextHint finding kind = case kind of+    ContextSnapshot -> LabelAdvisory+    ContextConsumerBuild -> LabelAdvisory+    ContextPrivateEvent+        | mfCode finding == MappedFieldAddedWithDefault -> case mfOldUnknownFields finding of+            Just IgnoreUnknown -> LabelAdditive+            _ -> LabelBreaking+        | mfCode finding `elem` [MappedArmAdded, MappedEnumValueAdded] -> LabelAdvisory+        | mfCode finding `elem` [MappedBindingChanged, MappedInitialChanged, MappedCanonicalTypeChanged] -> LabelAdvisory+        | otherwise -> LabelBreaking+    _ -> LabelAdvisory++mappedChange :: ChangeContext -> Name -> Text -> Text -> MappedFinding -> Change+mappedChange context node facet subject finding =+    mkChange label context node facet subject (mfCode finding) (mfDetail finding)+  where+    label = deriveLabel defaultGate (classifyCompatibility context (mfCode finding))++declarationSubject :: MappedFinding -> Text+declarationSubject finding =+    mfDeclaration finding <> if T.null (mfLeaf finding) then "" else " " <> mfLeaf finding+ nodeAggregate :: Node -> Maybe Aggregate nodeAggregate (NAggregate a) = Just a nodeAggregate _ = Nothing@@ -224,13 +724,17 @@ routerDiff :: DiffEnv -> [Change] routerDiff env =     concatMap (uncurry routerPairDiff) (prMatched paired)-        ++ [additive (rtId router) "router" (rtId router) "new router declaration" | router <- prAdded paired]+        ++ [additive (rtId router) "router" (rtId router) DeclarationAdded "new router declaration" | router <- prAdded paired]         ++ [breaking (rtId router) "router-identity" (rtId router) RouterStableNameChanged "router removed while replayable source events may still derive target-keyed dispatch ids from its stable identity" | router <- prRemoved paired]   where     paired = pairByName nodeRouter rtId env  routerPairDiff :: RouterNode -> RouterNode -> [Change]-routerPairDiff oldRouter newRouter = stableName ++ keyDerivation ++ target+routerPairDiff oldRouter newRouter =+    stableName+        ++ keyDerivation+        ++ target+        ++ routerDecideSurfaceDiff oldRouter newRouter   where     nodeName = rtId newRouter     stableName =@@ -247,6 +751,26 @@         | rtTarget oldRouter /= rtTarget newRouter         ] +routerDecideSurfaceDiff :: RouterNode -> RouterNode -> [Change]+routerDecideSurfaceDiff oldRouter newRouter =+    [ advisory+        (rtId newRouter)+        "router-decide"+        (rtId newRouter)+        RouterDecideSurfaceChanged+        "router dispatch surface changed: a source event redelivered across the deploy dispatches under the same deterministic ids, so half-old/half-new fan-out merges silently. Drain or pause the router's subscription and replay or discard dead letters before deploying; see docs/user/deploy-ordering.md. Hole-only decide changes are not visible to diff; the same drain rule applies to those too."+    | oldSurface /= newSurface+    ]+  where+    oldSurface =+        ( renderResolveSurface (rtResolve oldRouter)+        , renderRouterDispatchSurface (rtDispatch oldRouter)+        )+    newSurface =+        ( renderResolveSurface (rtResolve newRouter)+        , renderRouterDispatchSurface (rtDispatch newRouter)+        )+ readModelDiff :: DiffEnv -> [Change] readModelDiff env =     concatMap (uncurry (readModelPairDiff env)) (prMatched paired)@@ -270,7 +794,7 @@             [ breaking nodeName "read-model-version" nodeName ReadModelVersionDecreased ("version decreased from " <> tInt (rmVersion oldReadModel) <> " to " <> tInt (rmVersion newReadModel))             ]         | rmVersion newReadModel > rmVersion oldReadModel =-            [ additive nodeName "read-model-version" nodeName ("version increased from " <> tInt (rmVersion oldReadModel) <> " to " <> tInt (rmVersion newReadModel) <> "; register and rebuild the new shape before serving it")+            [ additive nodeName "read-model-version" nodeName VersionBumped ("version increased from " <> tInt (rmVersion oldReadModel) <> " to " <> tInt (rmVersion newReadModel) <> "; register and rebuild the new shape before serving it")             ]         | otherwise = []     oldShape = (rmColumns oldReadModel, rmShape oldReadModel)@@ -302,20 +826,20 @@         (Strong, Eventual) ->             [breaking nodeName "read-model-consistency" nodeName ReadModelConsistencyWeakened "default consistency changed Strong -> Eventual; callers lose the cursor-wait guarantee"]         (Eventual, Strong) ->-            [additive nodeName "read-model-consistency" nodeName "default consistency changed Eventual -> Strong; callers gain a cursor-wait guarantee"]+            [additive nodeName "read-model-consistency" nodeName CompatibilityStrengthened "default consistency changed Eventual -> Strong; callers gain a cursor-wait guarantee"]         _ -> []     oldScope = effectiveScope (rmScope oldReadModel)     newScope = effectiveScope (rmScope newReadModel)     scopeChanges         | oldScope == newScope = []         | scopeStrengthened oldScope newScope =-            [additive nodeName "read-model-scope" nodeName ("Strong scope widened " <> renderScope oldScope <> " -> " <> renderScope newScope)]+            [additive nodeName "read-model-scope" nodeName CompatibilityStrengthened ("Strong scope widened " <> renderScope oldScope <> " -> " <> renderScope newScope)]         | otherwise =             [breaking nodeName "read-model-scope" nodeName ReadModelConsistencyWeakened ("Strong scope changed " <> renderScope oldScope <> " -> " <> renderScope newScope <> "; callers no longer wait on the same event surface")]  addedReadModelDiff :: ReadModelNode -> [Change] addedReadModelDiff readModel =-    [additive (rmName readModel) "read-model" (rmName readModel) "new read model"]+    [additive (rmName readModel) "read-model" (rmName readModel) DeclarationAdded "new read model"]  removedReadModelDiff :: ReadModelNode -> [Change] removedReadModelDiff readModel =@@ -342,22 +866,92 @@  aggregateDiff :: DiffEnv -> [Change] aggregateDiff env =-    concatMap (uncurry aggregatePairDiff) (prMatched paired)+    concatMap+        (\(oldAggregate, newAggregate) -> aggregatePairDiff (deOld env) (deNew env) oldAggregate newAggregate)+        (prMatched paired)         ++ concatMap addedAggregateDiff (prAdded paired)         ++ concatMap removedAggregateDiff (prRemoved paired)   where     paired = pairByName nodeAggregate aggName env -aggregatePairDiff :: Aggregate -> Aggregate -> [Change]-aggregatePairDiff oldAgg newAgg =+aggregatePairDiff :: Spec -> Spec -> Aggregate -> Aggregate -> [Change]+aggregatePairDiff oldSpec newSpec oldAgg newAgg =     concatMap (eventDiff oldAgg newAgg) (aggEvents newAgg)         ++ removedEvents oldAgg newAgg         ++ wireDiff oldAgg newAgg         ++ projectionDiff oldAgg newAgg+        ++ guardTighteningDiff oldAgg newAgg+        ++ transitionSurfaceDiff oldSpec newSpec oldAgg newAgg +{- | Report replay-fold evolution. Regenerated scaffold code carries the new+fingerprint and invalidates old snapshots, so this remains advisory.+-}+transitionSurfaceDiff :: Spec -> Spec -> Aggregate -> Aggregate -> [Change]+transitionSurfaceDiff oldSpec newSpec oldAgg newAgg+    | aggregateFoldSurface oldSpec oldAgg == aggregateFoldSurface newSpec newAgg = []+    | otherwise =+        [ advisory+            (aggName newAgg)+            "transitions"+            (aggName newAgg)+            AggFoldSurfaceChanged+            "aggregate fold surface changed: replay now interprets the existing log under the new fold. Old snapshots are invalidated automatically once the regenerated fold fingerprint deploys; if the change is fold-neutral confirm it, otherwise re-scaffold and redeploy, and bump `state-codec version=` for any accompanying Holes-only change."+        ]++{- | Plan 143: guard changes are replay-relevant. Hydration re-inverts each+stored event and re-checks the edge guard, so a stored event legally appended+under the old guard may no longer satisfy the new one — the next command on+any stream containing such an event fails hydration with no inverting edge.+The remedy is mechanical, so the tool computes it: the removed region is+@old-guard ∧ ¬new-guard@ ('complementExpr' eliminates the negation inside the+existing grammar), and the advisory prints a paste-ready replay-only twin+carrying that region with the OLD transition's writes\/emits\/goto. Whether+history should stay replayable (paste the twin) or be truncated instead is a+business decision, so the twin is never auto-applied.++Detection is conservative: any guard change on a paired live (source,+command) transition where the new spec declares a guard and does not already+contain a replay-only twin for the pair. A pure loosening also matches; the+advisory says how to confirm no stored data is affected (the replay audit,+docs/plans/142) rather than guessing.+-}+guardTighteningDiff :: Aggregate -> Aggregate -> [Change]+guardTighteningDiff oldAgg newAgg =+    [ advisory (aggName newAgg) "transition" subject AggGuardTightened detail+    | newT <- aggTransitions newAgg+    , tMode newT == TmLive+    , Just oldT <-+        [ find+            (\o -> tSource o == tSource newT && tCommand o == tCommand newT && tMode o == TmLive)+            (aggTransitions oldAgg)+        ]+    , tGuard newT /= tGuard oldT+    , Just newGuard <- [tGuard newT]+    , not (hasReplayOnlyTwin newT)+    , let subject = tSource newT <> " -- " <> tCommand newT+    , let removedRegion =+            maybe (complementExpr newGuard) (\o -> EAnd o (complementExpr newGuard)) (tGuard oldT)+    , let twin = oldT{tGuard = Just removedRegion, tMode = TmReplayOnly}+    , let detail =+            "guard changed on "+                <> subject+                <> ". Stored events appended under the old guard may no longer invert: "+                <> "the next command on any stream containing one fails hydration with "+                <> "no inverting edge. Either confirm via the replay audit that no stored "+                <> "stream exercises the removed region, or keep history replayable by "+                <> "adding the computed replay-only twin (the removed region with the old "+                <> "transition's writes/emits/goto):\n\n"+                <> renderTransition twin+    ]+  where+    hasReplayOnlyTwin newT =+        any+            (\t -> tMode t == TmReplayOnly && tSource t == tSource newT && tCommand t == tCommand newT)+            (aggTransitions newAgg)+ addedAggregateDiff :: Aggregate -> [Change] addedAggregateDiff newAgg =-    [ additive (aggName newAgg) "event" (evName e) "new event type (new aggregate)"+    [ additive (aggName newAgg) "event" (evName e) DeclarationAdded "new event type (new aggregate)"     | e <- aggEvents newAgg     ] @@ -372,11 +966,30 @@ eventDiff oldAgg newAgg e =     case find ((== evName e) . evName) (aggEvents oldAgg) of         Nothing ->-            [additive (aggName newAgg) "event" (evName e) "new event type"]+            [additive (aggName newAgg) "event" (evName e) DeclarationAdded "new event type"]         Just oldE             | evVersion e > evVersion oldE ->                 if evVersion e == evVersion oldE + 1 && evUpcastFrom e `hasSource` evVersion oldE-                    then [additive (aggName newAgg) "event" (evName e) ("new version v" <> tInt (evVersion e) <> " with upcaster from v" <> tInt (evVersion oldE))]+                    then+                        [additive (aggName newAgg) "event" (evName e) VersionBumped ("new version v" <> tInt (evVersion e) <> " with upcaster from v" <> tInt (evVersion oldE))]+                            ++ [ breaking+                                    (aggName newAgg)+                                    "event"+                                    (evName e)+                                    UpcasterChainGap+                                    ( "bumping v"+                                        <> tInt (evVersion oldE)+                                        <> " to v"+                                        <> tInt (evVersion e)+                                        <> " replaced the 'upcast from v"+                                        <> tInt vanishedSource+                                        <> "' rung; stored v"+                                        <> tInt vanishedSource+                                        <> " payloads can no longer decode"+                                    )+                               | Just (vanishedSource, _) <- [evUpcastFrom oldE]+                               , not (aggregateHasUpcasterSource newAgg vanishedSource)+                               ]                     else                         [ breaking                             (aggName newAgg)@@ -397,11 +1010,12 @@                 sameVersionEventDiff oldAgg newAgg oldE e  {- | Events present in the old aggregate but absent in the new one. Removing a-tag entirely is breaking; keeping it as a deprecated event is safe.+tag entirely is breaking; deprecation preserves decoding but needs a retained+replay-only emitter to preserve replay. -} removedEvents :: Aggregate -> Aggregate -> [Change] removedEvents oldAgg newAgg =-    [ breaking (aggName newAgg) "event" (evName oldE) EvtRemovedNotDeprecated "event removed entirely; keep it as a 'deprecated event' so old payloads still decode"+    [ breaking (aggName newAgg) "event" (evName oldE) EvtRemovedNotDeprecated "event removed entirely; its stored payloads can neither decode nor replay. Deprecating instead restores decode-ability only — replay still fails on live streams unless an equivalent replay-only emitting transition is retained; truncate or terminalize affected streams before deleting it"     | oldE <- aggEvents oldAgg     , isNothing (find ((== evName oldE) . evName) (aggEvents newAgg))     ]@@ -410,6 +1024,16 @@ hasSource (Just (m, _)) n = m == n hasSource Nothing _ = False +aggregateHasUpcasterSource :: Aggregate -> Int -> Bool+aggregateHasUpcasterSource aggregate source =+    any ((== Just source) . fmap fst . evUpcastFrom) (aggEvents aggregate)++hasReplayOnlyEmitter :: Aggregate -> Name -> Bool+hasReplayOnlyEmitter aggregate eventName =+    any+        (\transition -> tMode transition == TmReplayOnly && eventName `elem` tEmits transition)+        (aggTransitions aggregate)+ eventFieldSigs :: Aggregate -> Event -> [(Name, Maybe Name)] eventFieldSigs agg e = case evBody e of     EventFields fs -> map fieldSig fs@@ -424,6 +1048,7 @@         ++ removedChanges         ++ typeChanges         ++ deprecationChanges+        ++ retirementChanges   where     oldFields = eventFieldSigs oldAgg oldE     newFields = eventFieldSigs newAgg newE@@ -456,10 +1081,33 @@         ]     deprecationChanges         | not (evDeprecated oldE) && evDeprecated newE =-            [additive (aggName newAgg) "event" (evName newE) "event deprecated (still decodable)"]-        | evDeprecated oldE && not (evDeprecated newE) =+            [ if hasReplayOnlyEmitter newAgg (evName newE)+                then+                    advisory+                        (aggName newAgg)+                        "event"+                        (evName newE)+                        EventRetirementInProgress+                        "event deprecated and removed from the live write path, while an equivalent replay-only transition preserves hydration. Retain that transition until every affected stream is terminal, truncated, or passes the replay audit"+                else+                    advisory+                        (aggName newAgg)+                        "event"+                        (evName newE)+                        DeprecatedEventReplayHazard+                        ( "event deprecated: old payloads remain decodable but are no longer replayable — hydration of live streams containing them fails at the first command (HydrationNoInvertingEdge). Add an equivalent replay-only emitting transition or confirm every affected stream is terminal or truncated before deploying"+                            <> if evRetiring oldE then "" else "; consider a 'retiring event' stage first"+                        )+            ]+        | evDeprecated oldE && not (evDeprecated newE) && not (evRetiring newE) =             [advisory (aggName newAgg) "event" (evName newE) EventUndeprecated "event returned to the write surface; old payloads remain decodable but new writes resume"]         | otherwise = []+    retirementChanges+        | not (evRetiring oldE) && evRetiring newE =+            [advisory (aggName newAgg) "event" (evName newE) EventRetirementInProgress "retirement started; keep the live emitting transition until affected streams are terminal or truncated, then cut over to deprecated plus an equivalent replay-only emitting transition"]+        | evRetiring oldE && not (evRetiring newE) && not (evDeprecated newE) =+            [additive (aggName newAgg) "event" (evName newE) EventRetirementAbandoned "event retirement abandoned; ordinary live writes continue"]+        | otherwise = []  renderFieldType :: Maybe Name -> Text renderFieldType Nothing = "(declared)"@@ -516,7 +1164,7 @@     ]  addedIdDiff :: IdDecl -> [Change]-addedIdDiff declaration = [additive (idName declaration) "id-prefix" (idName declaration) "new id declaration"]+addedIdDiff declaration = [additive (idName declaration) "id-prefix" (idName declaration) DeclarationAdded "new id declaration"]  removedIdDiff :: IdDecl -> [Change] removedIdDiff declaration = [breaking (idName declaration) "id-prefix" (idName declaration) IdPrefixChanged "id declaration removed; persisted ids still use its prefix"]@@ -540,15 +1188,46 @@            , Just newWire <- [lookup ctor (enumCtors newEnum)]            , oldWire /= newWire            ]-        ++ [ additive (enumName newEnum) "enum-constructor" ctor ("new constructor with wire spelling '" <> wire <> "'")-           | (ctor, wire) <- enumCtors newEnum-           , isNothing (lookup ctor (enumCtors oldEnum))-           ]+        ++ concat+            [ enumAdditionDiff oldSpec newEnum ctor wire+            | (ctor, wire) <- enumCtors newEnum+            , isNothing (lookup ctor (enumCtors oldEnum))+            ]  addedEnumDiff :: EnumDecl -> [Change] addedEnumDiff enumDecl =-    [additive (enumName enumDecl) "enum-constructor" ctor ("new enum constructor with wire spelling '" <> wire <> "'") | (ctor, wire) <- enumCtors enumDecl]+    [additive (enumName enumDecl) "enum-constructor" ctor EnumCtorAdded ("new enum constructor with wire spelling '" <> wire <> "'") | (ctor, wire) <- enumCtors enumDecl] +enumAdditionDiff :: Spec -> EnumDecl -> Name -> Text -> [Change]+enumAdditionDiff oldSpec enumDecl ctor wire = case enumUsages oldSpec (enumName enumDecl) of+    [] ->+        [ additive+            (enumName enumDecl)+            "enum-constructor"+            ctor+            EnumCtorAdded+            ("new constructor with wire spelling '" <> wire <> "'")+        ]+    usages -> map finding usages+  where+    finding usage+        | ".reg." `T.isInfixOf` usage =+            advisoryAt+                (snapshotContext (enumName enumDecl) [usage])+                (enumName enumDecl)+                "enum-constructor"+                ctor+                EnumCtorAdded+                ("new constructor with wire spelling '" <> wire <> "' is used by " <> usage <> "; invalidate or rebuild snapshots before values using the new arm hydrate")+        | otherwise =+            advisoryAt+                (privateEventAdditionContext (enumName enumDecl) [usage])+                (enumName enumDecl)+                "enum-constructor"+                ctor+                EnumCtorAdded+                ("new constructor with wire spelling '" <> wire <> "' is used by " <> usage <> "; deploy consumers before producers emit the new arm")+ removedEnumDiff :: Spec -> EnumDecl -> [Change] removedEnumDiff oldSpec enumDecl =     [ breaking (enumName enumDecl) "enum-constructor" ctor EnumCtorRemoved ("enum removed; stored wire value '" <> wire <> "' no longer decodes" <> enumUsageSuffix oldSpec (enumName enumDecl))@@ -626,13 +1305,13 @@     removedEvents' = prRemoved eventPairs     eventPairChanges (oldEvent, newEvent) = contractEventDiff oldContract newContract oldEvent newEvent     addedEventChanges event =-        [additive (ctrName newContract) "contract-event" (ceName event) "new contract event"]+        [additive (ctrName newContract) "contract-event" (ceName event) ContractEventAdded "new contract event"]     removedEventChanges event =         [breaking (ctrName newContract) "contract-event" (ceName event) ContractEventRemoved "contract event removed; existing cross-service payloads no longer have a declared decoder"]  addedContractDiff :: ContractNode -> [Change] addedContractDiff contract =-    [additive (ctrName contract) "contract-event" (ceName event) "new event in a new contract" | event <- ctrEvents contract]+    [additive (ctrName contract) "contract-event" (ceName event) ContractEventAdded "new event in a new contract" | event <- ctrEvents contract]  removedContractDiff :: ContractNode -> [Change] removedContractDiff contract =@@ -659,7 +1338,7 @@            , Just newTopic <- [lookup alias (ctrTopics newContract)]            , oldTopic /= newTopic            ]-        ++ [ additive (ctrName newContract) "contract-topic" alias ("new topic alias for '" <> topic <> "'")+        ++ [ additive (ctrName newContract) "contract-topic" alias ContractTopicAdded ("new topic alias for '" <> topic <> "'")            | (alias, topic) <- ctrTopics newContract            , isNothing (lookup alias (ctrTopics oldContract))            ]@@ -730,17 +1409,17 @@         | wqfWire oldField /= wqfWire newField = [payloadBreaking newField ("wire name changed '" <> wqfWire oldField <> "' -> '" <> wqfWire newField <> "'")]         | wqfType oldField /= wqfType newField = [payloadBreaking newField ("type changed " <> wqfType oldField <> " -> " <> wqfType newField)]         | not (wqfRequired oldField) && wqfRequired newField = [payloadBreaking newField "field changed from optional to required; queued jobs may omit it"]-        | wqfRequired oldField && not (wqfRequired newField) = [additive (wqName newQueue) "payload-field" (wqfName newField) "field changed from required to optional"]+        | wqfRequired oldField && not (wqfRequired newField) = [additive (wqName newQueue) "payload-field" (wqfName newField) CompatibilityStrengthened "field changed from required to optional"]         | otherwise = []     addedFieldDiff field         | wqfRequired field = [payloadBreaking field "new required field; queued jobs do not contain it"]-        | otherwise = [additive (wqName newQueue) "payload-field" (wqfName field) "new optional field"]+        | otherwise = [additive (wqName newQueue) "payload-field" (wqfName field) CompatibilityStrengthened "new optional field"]     removedFieldDiff field = [payloadBreaking field "field removed; queued jobs still contain the old payload shape"]     payloadBreaking field detail = breaking (wqName newQueue) "payload-field" (wqfName field) WqPayloadFieldChanged detail  addedWorkqueueDiff :: WorkqueueNode -> [Change] addedWorkqueueDiff queue =-    [additive (wqName queue) "payload-field" (wqfName field) "field belongs to a new workqueue payload" | field <- wqPayload queue]+    [additive (wqName queue) "payload-field" (wqfName field) DeclarationAdded "field belongs to a new workqueue payload" | field <- wqPayload queue]  removedWorkqueueDiff :: WorkqueueNode -> [Change] removedWorkqueueDiff queue =@@ -814,6 +1493,8 @@         ++ map (fieldChange "field removed; the generated process input decoder changed") (prRemoved fields)         ++ processIdentityDiff oldProcess newProcess         ++ processTimerWindowDiff oldProcess newProcess+        ++ processDecideSurfaceDiff oldProcess newProcess+        ++ processTimerPayloadDiff oldProcess newProcess   where     -- inName is a generated Haskell type name; the wire shape is inFields.     fields = pairDeclarations fieldName (inFields (procInput oldProcess)) (inFields (procInput newProcess))@@ -824,7 +1505,7 @@  addedProcessDiff :: ProcessNode -> [Change] addedProcessDiff process =-    [additive (procId process) "input-field" (fieldName field) "field belongs to a new process input" | field <- inFields (procInput process)]+    [additive (procId process) "input-field" (fieldName field) DeclarationAdded "field belongs to a new process input" | field <- inFields (procInput process)]  removedProcessDiff :: ProcessNode -> [Change] removedProcessDiff process =@@ -868,6 +1549,30 @@     | tmFireAt (procTimer oldProcess) /= tmFireAt (procTimer newProcess)     ] +processDecideSurfaceDiff :: ProcessNode -> ProcessNode -> [Change]+processDecideSurfaceDiff oldProcess newProcess =+    [ advisory+        (procId newProcess)+        "process-decide"+        (procId newProcess)+        ProcessDecideSurfaceChanged+        "process dispatch surface changed: a source event redelivered across the deploy dispatches under the same deterministic ids, so half-old/half-new fan-out merges silently. Drain or pause the process subscription and replay or discard dead letters before deploying; see docs/user/deploy-ordering.md. Hole-only decide changes are not visible to diff; the same drain rule applies to those too."+    | renderHandleSurface (procHandle oldProcess)+        /= renderHandleSurface (procHandle newProcess)+    ]++processTimerPayloadDiff :: ProcessNode -> ProcessNode -> [Change]+processTimerPayloadDiff oldProcess newProcess =+    [ advisory+        (procId newProcess)+        "timer-payload"+        (tmName (procTimer newProcess))+        ProcessTimerPayloadChanged+        "timer payload shape changed: rows scheduled before the deploy carry the old shape, unversioned, and fire under new code — the fire decoder must accept every historically scheduled shape or the timer dead-letters after maxAttempts. Hole-only timer-decoder changes are not visible to diff; the same drain rule applies to those too."+    | renderTimerPayloadSurface (procTimer oldProcess)+        /= renderTimerPayloadSurface (procTimer newProcess)+    ]+ renderFireAt :: FireAtExpr -> Text renderFireAt expression = "input." <> faField expression <> " + " <> faWindow expression @@ -901,7 +1606,7 @@     workflowShape field detail = breaking (wfId newWorkflow) "workflow-input" (fieldName field) WorkflowShapeChanged detail  addedWorkflowDiff :: WorkflowNode -> [Change]-addedWorkflowDiff workflow = [additive (wfId workflow) "workflow" (wfId workflow) "new workflow"]+addedWorkflowDiff workflow = [additive (wfId workflow) "workflow" (wfId workflow) DeclarationAdded "new workflow"]  removedWorkflowDiff :: WorkflowNode -> [Change] removedWorkflowDiff workflow = [breaking (wfId workflow) "workflow" (wfId workflow) WorkflowShapeChanged "workflow removed while in-flight journals and outcomes may still require its decoder"]@@ -965,7 +1670,7 @@ renderInkPersist InkPersistDedupeOnly = "dedupe-only"  addedIntakeDiff :: IntakeNode -> [Change]-addedIntakeDiff intake = [additive (inkName intake) "intake" (inkName intake) "new intake"]+addedIntakeDiff intake = [additive (inkName intake) "intake" (inkName intake) DeclarationAdded "new intake"]  removedIntakeDiff :: IntakeNode -> [Change] removedIntakeDiff intake = [breaking (inkName intake) "dedupe-identity" (inkName intake) DedupeIdentityChanged "intake removed while persisted dedupe records and redeliveries may remain"]@@ -1009,7 +1714,7 @@ emitMapping emit = (emKey emit, emDiscriminant emit, emMap emit, emSkip emit)  addedEmitDiff :: EmitNode -> [Change]-addedEmitDiff emit = [additive (emName emit) "emit" (emName emit) "new emit mapping"]+addedEmitDiff emit = [additive (emName emit) "emit" (emName emit) DeclarationAdded "new emit mapping"]  removedEmitDiff :: EmitNode -> [Change] removedEmitDiff emit = [breaking (emName emit) "derived-identity" (emName emit) DerivedIdentityChanged "emit removed while persisted outbox identities may still retry"]@@ -1043,7 +1748,7 @@            ]  addedPublisherDiff :: PublisherNode -> [Change]-addedPublisherDiff publisher = [additive (pubName publisher) "publisher" (pubName publisher) "new publisher"]+addedPublisherDiff publisher = [additive (pubName publisher) "publisher" (pubName publisher) DeclarationAdded "new publisher"]  removedPublisherDiff :: PublisherNode -> [Change] removedPublisherDiff publisher = [breaking (pubName publisher) "derived-identity" (pubName publisher) DerivedIdentityChanged "publisher removed while persisted outbox rows may still require its stable identity"]@@ -1088,7 +1793,7 @@ dispatchTargets dispatch = (pdSourceReadModel dispatch, pdEnqueueTo dispatch)  addedPgmqDispatchDiff :: PgmqDispatchNode -> [Change]-addedPgmqDispatchDiff dispatch = [additive (pdName dispatch) "dispatch" (pdName dispatch) "new pgmq dispatch"]+addedPgmqDispatchDiff dispatch = [additive (pdName dispatch) "dispatch" (pdName dispatch) DeclarationAdded "new pgmq dispatch"]  removedPgmqDispatchDiff :: PgmqDispatchNode -> [Change] removedPgmqDispatchDiff dispatch = [breaking (pdName dispatch) "dedupe-identity" (pdName dispatch) DedupeIdentityChanged "dispatch removed while persisted queue and read-model dedupe records may remain"]@@ -1106,7 +1811,7 @@         ]     | safeAdditions =         map addedPatch newPatchIds-            ++ [ additive nodeName "workflow-continue-as-new" seedType "terminal continueAsNew is additive; old generations carry no rotation marker"+            ++ [ additive nodeName "workflow-continue-as-new" seedType WorkflowEvolutionGuardAdded "terminal continueAsNew is additive; old generations carry no rotation marker"                | Just seedType <- [appendedSeed]                ]     | otherwise =@@ -1144,7 +1849,7 @@     removedPatch patchId =         breaking nodeName "workflow-patch" patchId WorkflowPatchRemoved "patch id existed in the old spec but was removed; the differ cannot prove that no workflow generation still replays its journaled branch"     addedPatch patchId =-        additive nodeName "workflow-patch" patchId "new patch guard contains the entire body change, so in-flight generations retain their journaled branch"+        additive nodeName "workflow-patch" patchId WorkflowEvolutionGuardAdded "new patch guard contains the entire body change, so in-flight generations retain their journaled branch"  normaliseWorkflowBody :: [WfBodyItem] -> [WfBodyItem] normaliseWorkflowBody = map go@@ -1179,14 +1884,106 @@     WfContinueAsNew{} : rest -> reverse rest     _ -> items -additive :: Name -> Text -> Text -> Text -> Change-additive n facet subj detail = Additive (ChangeKind n facet subj Nothing detail)+additive :: Name -> Text -> Text -> DiagnosticCode -> Text -> Change+additive n facet subj code detail =+    mkChange LabelAdditive (contextFor LabelAdditive n facet subj code) n facet subj code detail  breaking :: Name -> Text -> Text -> DiagnosticCode -> Text -> Change-breaking n facet subj c detail = Breaking (ChangeKind n facet subj (Just c) detail)+breaking n facet subj code detail =+    mkChange LabelBreaking (contextFor LabelBreaking n facet subj code) n facet subj code detail  advisory :: Name -> Text -> Text -> DiagnosticCode -> Text -> Change-advisory n facet subj c detail = Advisory (ChangeKind n facet subj (Just c) detail)+advisory n facet subj code detail =+    mkChange LabelAdvisory (contextFor LabelAdvisory n facet subj code) n facet subj code detail++advisoryAt :: ChangeContext -> Name -> Text -> Text -> DiagnosticCode -> Text -> Change+advisoryAt context n facet subj code detail =+    mkChange LabelAdvisory context n facet subj code detail++mkChange :: Label -> ChangeContext -> Name -> Text -> Text -> DiagnosticCode -> Text -> Change+mkChange label context n facet subj code detail =+    wrap+        ChangeKind+            { ckNode = n+            , ckFacet = facet+            , ckSubject = subj+            , ckCode = code+            , ckContext = context+            , ckVector = classifyCompatibility context code+            , ckPaths = changeContextPaths context+            , ckDetail = detail+            }+  where+    wrap = case label of+        LabelAdditive -> Additive+        LabelAdvisory -> Advisory+        LabelBreaking -> Breaking++contextFor :: Label -> Name -> Text -> Text -> DiagnosticCode -> ChangeContext+contextFor label root facet subject code =+    setLabel $ case () of+        _+            | code `elem` publicCodes -> publicContractContext root paths+            | code `elem` queueCodes -> queueContext root paths+            | code `elem` identityCodes -> persistedIdentityContext root paths+            | code == AggFoldSurfaceChanged -> snapshotContext root paths+            | code == EnumCtorAdded -> ChangeContext root paths ContextGeneral label+            | code `elem` privateCodes -> privateEventContext root paths+            | otherwise -> ChangeContext root paths ContextGeneral label+  where+    paths = [pathFor root facet subject]+    setLabel context = context{contextOriginalLabel = label}+    publicCodes =+        [ ContractEventRemoved+        , ContractFieldChanged+        , ContractDiscriminatorChanged+        , ContractTopicChanged+        , ContractSchemaVersionDecreased+        , ContractSchemaVersionBumped+        , ContractEventAdded+        , ContractTopicAdded+        ]+    queueCodes = [WqPayloadFieldChanged, WqOrderingChanged, WqProvisionChanged, WqGroupKeyChanged, QueueIdentityChanged]+    identityCodes =+        [ DerivedIdentityChanged+        , IdPrefixChanged+        , DedupeIdentityChanged+        , RouterStableNameChanged+        , WorkflowStableNameChanged+        , ReadModelVersionDecreased+        , ReadModelShapeChangedWithoutBump+        , ReadModelFeedChanged+        , ReadModelConsistencyWeakened+        ]+    privateCodes =+        [ EvtFieldAddedWithoutBump+        , EvtFieldRemovedSameVersion+        , EvtFieldTypeChanged+        , EvtVersionDecreased+        , EvtVersionMissingUpcaster+        , UpcasterChainGap+        , EvtRemovedNotDeprecated+        , EnumCtorRemoved+        , EnumWireSpellingChanged+        , WireSpecChanged+        , ProcessInputChanged+        , WorkflowShapeChanged+        , WorkflowBodyChanged+        , WorkflowPatchRemoved+        , WorkflowContinueSeedChanged+        , AggGuardTightened+        , DeprecatedEventReplayHazard+        , EventRetirementInProgress+        , EventUndeprecated+        , ProcessTimerPayloadChanged+        ]++pathFor :: Name -> Text -> Text -> Text+pathFor root facet subject+    | facet `elem` ["event", "event-field"] = root <> ".event." <> subject+    | facet `elem` ["contract-event", "contract-field"] = root <> ".event." <> subject+    | root == subject = root <> "." <> facet+    | otherwise = root <> "." <> facet <> "." <> subject  commas :: [Text] -> Text commas = T.intercalate ", "
+ src/Keiro/Dsl/DiffReport.hs view
@@ -0,0 +1,331 @@+{- | Pure rendering and JSON encoding for compatibility-vector diff reports.++The JSON schema identifier is @keiro-dsl/diff-report/1@.  Consumers must+ignore unknown object keys.  Vector keys and entries in the @paths@ array are+append-only so later nested type-expression work can refine findings without+invalidating version-1 readers.+-}+module Keiro.Dsl.DiffReport (+    Remedy (..),+    DiffReport,+    diffReport,+    remediationFor,+    renderRemedy,+    renderFinding,+    renderVectorLine,+    renderExplainBlock,+    surfaceName,+    parseSurfaceName,+    verdictName,+    rolloutName,+) where++import Data.Aeson (ToJSON (..), Value, object, (.=))+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NonEmpty+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Diff+import Keiro.Dsl.Validate (DiagnosticCode (..))++data Remedy+    = RemedyVersionBump+    | RemedyUpcaster+    | RemedyDeploymentOrder RolloutConstraint+    | RemedyContractRevision+    | RemedyReplayOnlyEdge+    | RemedyStateCodecBump+    | RemedyRecompileConsumers+    | RemedyRunConformance+    | RemedyDoNotDeploy Text+    deriving stock (Eq, Show)++data DiffReport = DiffReport+    { reportGate :: !(Set CompatibilitySurface)+    , reportFindings :: ![Change]+    }+    deriving stock (Eq, Show)++diffReport :: Set CompatibilitySurface -> [Change] -> DiffReport+diffReport = DiffReport++instance ToJSON DiffReport where+    toJSON report =+        object+            [ "schema" .= ("keiro-dsl/diff-report/1" :: Text)+            , "gate" .= map surfaceName (Set.toAscList (reportGate report))+            , "breaking" .= any (gatedBreaking (reportGate report)) (reportFindings report)+            , "findings" .= map (findingValue (reportGate report)) (reportFindings report)+            ]++findingValue :: Set CompatibilitySurface -> Change -> Value+findingValue gate change =+    object+        [ "label" .= labelName (deriveLabel gate (ckVector kind))+        , "node" .= ckNode kind+        , "facet" .= ckFacet kind+        , "subject" .= ckSubject kind+        , "code" .= T.pack (show (ckCode kind))+        , "paths" .= ckPaths kind+        , "vector" .= vectorValue (ckVector kind)+        , "detail" .= ckDetail kind+        , "remedies" .= map renderRemedy (NonEmpty.toList (remediationFor (ckContext kind) (ckCode kind)))+        ]+  where+    kind = changeKind change++vectorValue :: CompatibilityVector -> Value+vectorValue vector =+    object+        [ "private-history-read" .= verdictName (cvPrivateHistoryRead vector)+        , "old-binary-read-new-events" .= verdictName (cvOldBinaryReadNewEvents vector)+        , "snapshot-hydration" .= verdictName (cvSnapshotHydration vector)+        , "public-consumer" .= verdictName (cvPublicConsumer vector)+        , "persisted-identity" .= verdictName (cvPersistedIdentity vector)+        , "consumer-build" .= verdictName (cvConsumerBuild vector)+        , "rollout" .= map rolloutName (Set.toAscList (cvRollout vector))+        ]++remediationFor :: ChangeContext -> DiagnosticCode -> NonEmpty Remedy+remediationFor context code+    | code == AggGuardTightened = RemedyReplayOnlyEdge :| [RemedyRunConformance]+    | code == AggFoldSurfaceChanged = RemedyStateCodecBump :| [RemedyRunConformance]+    | code `elem` mappedWireCodes = mappedWireRemedy+    | code `elem` [MappedFieldAddedWithDefault, MappedArmAdded, MappedEnumValueAdded] = mappedAdditionRemedy+    | code `elem` [MappedHaskellSourceChanged, MappedRecordConstructorChanged] =+        RemedyRecompileConsumers :| [RemedyRunConformance]+    | code == MappedBindingChanged = mappedConformanceRemedy+    | code == MappedFixturesChanged = RemedyRunConformance :| []+    | code == MappedInitialChanged = mappedSnapshotConformanceRemedy+    | code == MappedCanonicalTypeChanged = mappedCanonicalRemedy+    | code == MappedDeclAdded = RemedyRunConformance :| []+    | code `elem` eventDecodeCodes =+        RemedyVersionBump :| [RemedyUpcaster, RemedyDeploymentOrder RolloutStopTheWorld]+    | code `elem` contractCodes =+        RemedyContractRevision :| [RemedyDeploymentOrder RolloutProducerLast]+    | code `elem` queueCodes =+        RemedyDeploymentOrder RolloutWorkersFirst :| [RemedyRunConformance]+    | code `elem` identityCodes =+        RemedyDoNotDeploy "revert the re-keying change or perform an explicit operational identity migration" :| []+    | code == EnumCtorAdded = case Set.toAscList (cvRollout vector) of+        rollout : _ -> RemedyDeploymentOrder rollout :| [snapshotRemedy]+        [] -> snapshotRemedy :| []+    | cvConsumerBuild vector `elem` [VAdvisory, VBreaking] =+        RemedyRecompileConsumers :| [RemedyRunConformance]+    | Just rollout <- firstRollout = RemedyDeploymentOrder rollout :| [RemedyRunConformance]+    | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump :| [RemedyRunConformance]+    | otherwise = RemedyRunConformance :| []+  where+    vector = classifyCompatibility context code+    firstRollout = case Set.toAscList (cvRollout vector) of+        rollout : _ -> Just rollout+        [] -> Nothing+    snapshotRemedy+        | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump+        | otherwise = RemedyRunConformance+    mappedWireRemedy+        | cvPrivateHistoryRead vector == VBreaking =+            RemedyVersionBump :| [RemedyUpcaster, RemedyDeploymentOrder RolloutStopTheWorld]+        | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump :| [RemedyRunConformance]+        | otherwise = RemedyRecompileConsumers :| [RemedyRunConformance]+    mappedAdditionRemedy+        | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump :| [RemedyRunConformance]+        | Just rollout <- firstRollout = RemedyDeploymentOrder rollout :| [RemedyRunConformance]+        | otherwise = RemedyRunConformance :| []+    mappedConformanceRemedy+        | cvSnapshotHydration vector == VAdvisory = RemedyRunConformance :| [RemedyStateCodecBump]+        | otherwise = RemedyRunConformance :| []+    mappedSnapshotConformanceRemedy+        | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump :| [RemedyRunConformance]+        | otherwise = RemedyRunConformance :| []+    mappedCanonicalRemedy+        | cvSnapshotHydration vector == VAdvisory = RemedyStateCodecBump :| [RemedyRecompileConsumers, RemedyRunConformance]+        | otherwise = RemedyRecompileConsumers :| [RemedyRunConformance]+    mappedWireCodes =+        [ MappedFieldAddedNoDefault+        , MappedFieldRemoved+        , MappedFieldTypeChanged+        , MappedPresenceChanged+        , MappedNullabilityChanged+        , MappedDefaultRemoved+        , MappedDefaultChanged+        , MappedWireKeyChanged+        , MappedUnionEncodingChanged+        , MappedArmRemoved+        , MappedArmTagChanged+        , MappedEnumValueRemoved+        , MappedEnumSpellingChanged+        , MappedOpaqueCodecChanged+        , MappedModeCrossed+        , MappedDeclRemoved+        ]+    eventDecodeCodes =+        [ EvtFieldAddedWithoutBump+        , EvtFieldRemovedSameVersion+        , EvtFieldTypeChanged+        , EvtVersionDecreased+        , EvtVersionMissingUpcaster+        , UpcasterChainGap+        , EvtRemovedNotDeprecated+        , EnumCtorRemoved+        , EnumWireSpellingChanged+        , WireSpecChanged+        , ProcessInputChanged+        , WorkflowShapeChanged+        , WorkflowBodyChanged+        , WorkflowPatchRemoved+        , WorkflowContinueSeedChanged+        ]+    contractCodes =+        [ ContractEventRemoved+        , ContractFieldChanged+        , ContractDiscriminatorChanged+        , ContractTopicChanged+        , ContractSchemaVersionDecreased+        , ContractSchemaVersionBumped+        ]+    queueCodes = [WqPayloadFieldChanged, WqOrderingChanged, WqProvisionChanged, WqGroupKeyChanged, QueueIdentityChanged]+    identityCodes =+        [ DerivedIdentityChanged+        , IdPrefixChanged+        , DedupeIdentityChanged+        , RouterStableNameChanged+        , WorkflowStableNameChanged+        , ReadModelVersionDecreased+        , ReadModelShapeChangedWithoutBump+        , ReadModelFeedChanged+        , ReadModelConsistencyWeakened+        ]++renderRemedy :: Remedy -> Text+renderRemedy remedy = case remedy of+    RemedyVersionBump -> "bump the owning schema or event version"+    RemedyUpcaster -> "add and retain a contiguous upcaster for every historical version"+    RemedyDeploymentOrder rollout -> "deploy in " <> rolloutName rollout <> " order"+    RemedyContractRevision -> "revise the independently owned public contract"+    RemedyReplayOnlyEdge -> "add the computed replay-only edge described by docs/adr/0002-replay-only-edges-are-the-sanctioned-remedy-for-guard-tightening.md"+    RemedyStateCodecBump -> "invalidate and rebuild snapshots by bumping state-codec version when automatic fingerprinting cannot see the change"+    RemedyRecompileConsumers -> "recompile every affected consumer against the generated interface"+    RemedyRunConformance -> "run the generated conformance and historical fixture suites"+    RemedyDoNotDeploy detail -> detail++renderFinding :: Change -> Text+renderFinding change =+    headline+        <> if vectorIsUniform (ckVector kind)+            then ""+            else "\n" <> renderVectorLine (ckVector kind)+  where+    kind = changeKind change+    headline =+        headlineName change+            <> ": "+            <> ckNode kind+            <> " "+            <> ckFacet kind+            <> " "+            <> ckSubject kind+            <> ": "+            <> ckDetail kind+            <> codeSuffix change kind++renderVectorLine :: CompatibilityVector -> Text+renderVectorLine vector =+    "    vector: "+        <> T.unwords+            ( [ surfaceName surface <> "=" <> verdictName verdict+              | surface <- [minBound .. maxBound]+              , let verdict = verdictFor surface vector+              , verdict /= VNotApplicable+              ]+                <> ["rollout=" <> T.intercalate "," (map rolloutName (Set.toAscList (cvRollout vector))) | not (Set.null (cvRollout vector))]+            )++renderExplainBlock :: Change -> Text+renderExplainBlock change =+    "explain ["+        <> T.pack (show (ckCode kind))+        <> "]\n"+        <> T.unlines ["  path: " <> path | path <- ckPaths kind]+        <> T.unlines (map ("  direction: " <>) directions)+        <> T.unlines ["  remedy: " <> renderRemedy remedy | remedy <- NonEmpty.toList remedies]+  where+    kind = changeKind change+    vector = ckVector kind+    directions =+        [ surfaceName surface <> " is " <> verdictName verdict <> "; " <> directionMeaning surface verdict+        | surface <- [minBound .. maxBound]+        , let verdict = verdictFor surface vector+        , verdict `elem` [VAdvisory, VBreaking]+        ]+    remedies = remediationFor (ckContext kind) (ckCode kind)++surfaceName :: CompatibilitySurface -> Text+surfaceName surface = case surface of+    PrivateHistoryRead -> "private-history-read"+    OldBinaryReadNewEvents -> "old-binary-read-new-events"+    SnapshotHydration -> "snapshot-hydration"+    PublicConsumer -> "public-consumer"+    PersistedIdentity -> "persisted-identity"+    ConsumerBuild -> "consumer-build"++parseSurfaceName :: String -> Either String CompatibilitySurface+parseSurfaceName raw = case lookup (T.pack raw) [(surfaceName surface, surface) | surface <- [minBound .. maxBound]] of+    Just surface -> Right surface+    Nothing ->+        Left+            ( "unknown compatibility surface '"+                <> raw+                <> "'; expected one of: "+                <> T.unpack (T.intercalate ", " (map surfaceName [minBound .. maxBound]))+            )++verdictName :: SurfaceVerdict -> Text+verdictName verdict = case verdict of+    VCompatible -> "compatible"+    VAdvisory -> "advisory"+    VBreaking -> "breaking"+    VNotApplicable -> "n/a"++rolloutName :: RolloutConstraint -> Text+rolloutName rollout = case rollout of+    RolloutStopTheWorld -> "stop-the-world"+    RolloutWorkersFirst -> "workers-first"+    RolloutDrainRequired -> "drain-required"+    RolloutProducerLast -> "producer-last"++labelName :: Label -> Text+labelName label = case label of+    LabelAdditive -> "additive"+    LabelAdvisory -> "warning"+    LabelBreaking -> "breaking"++headlineName :: Change -> Text+headlineName Additive{} = "ADDITIVE"+headlineName Advisory{} = "WARNING"+headlineName Breaking{} = "BREAKING"++codeSuffix :: Change -> ChangeKind -> Text+codeSuffix Additive{} _ = ""+codeSuffix _ kind = " [" <> T.pack (show (ckCode kind)) <> "]"++changeKind :: Change -> ChangeKind+changeKind (Additive kind) = kind+changeKind (Advisory kind) = kind+changeKind (Breaking kind) = kind++vectorIsUniform :: CompatibilityVector -> Bool+vectorIsUniform vector =+    Set.null (cvRollout vector)+        && all (`elem` [VCompatible, VNotApplicable]) [verdictFor surface vector | surface <- [minBound .. maxBound]]++directionMeaning :: CompatibilitySurface -> SurfaceVerdict -> Text+directionMeaning surface verdict = case (surface, verdict) of+    (PrivateHistoryRead, _) -> "the candidate binary may reinterpret or fail to read stored private history"+    (OldBinaryReadNewEvents, _) -> "a still-running old binary may reject events emitted by the candidate"+    (SnapshotHydration, _) -> "persisted snapshot seeds require invalidation or rebuild"+    (PublicConsumer, _) -> "an independently deployed consumer may reject the candidate contract"+    (PersistedIdentity, _) -> "replay or retry may derive a different persisted identity"+    (ConsumerBuild, _) -> "consumer or generated source must be rebuilt"
+ src/Keiro/Dsl/ExplainBindings.hs view
@@ -0,0 +1,293 @@+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++{- | Consumer-owned Haskell obligations implied by checked structural mapped+declarations. The same values drive create-once skeletons, scaffold-record+diffs, and the @check --explain-bindings@ report.+-}+module Keiro.Dsl.ExplainBindings (+    BindingObligationKind (..),+    BindingObligation (..),+    BindingHole (..),+    bindingObligations,+    bindingHoles,+    renderBindingObligations,+) where++import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))+import Data.List (groupBy, sortOn)+import Data.List.NonEmpty (NonEmpty)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Grammar (HaskellSource (..), Name, Spec (..), WireEnum (..))+import Keiro.Dsl.TypeGraph++data BindingObligationKind+    = BindingValue+    | FixtureValue+    | InitialValue+    deriving stock (Eq, Ord, Show)++data BindingObligation = BindingObligation+    { obligationMappedName :: !Name+    , obligationPackage :: !Text+    , obligationModule :: !Text+    , obligationSymbol :: !Text+    , obligationKind :: !BindingObligationKind+    , obligationSignature :: !Text+    , obligationUseSites :: ![Text]+    , obligationBindingVersion :: !(Maybe Text)+    }+    deriving stock (Eq, Ord, Show)++data BindingHole = BindingHole+    { holeMappedName :: !Name+    , holeModule :: !Text+    , holeSymbol :: !Text+    , holeKind :: !BindingObligationKind+    , holePath :: !(Maybe Text)+    , holeSignature :: !Text+    }+    deriving stock (Eq, Ord, Show)++instance ToJSON BindingObligation where+    toJSON obligation =+        object+            [ "schema" .= (1 :: Int)+            , "mappedName" .= obligationMappedName obligation+            , "package" .= obligationPackage obligation+            , "module" .= obligationModule obligation+            , "symbol" .= obligationSymbol obligation+            , "kind" .= renderKind (obligationKind obligation)+            , "signature" .= obligationSignature obligation+            , "useSites" .= obligationUseSites obligation+            , "bindingVersion" .= obligationBindingVersion obligation+            ]++instance FromJSON BindingObligation where+    parseJSON = withObject "keiro-dsl binding obligation" $ \value -> do+        schema <- value .: "schema"+        if schema /= (1 :: Int)+            then fail "unsupported binding obligation schema"+            else do+                kindText <- value .: "kind"+                kindValue <- maybe (fail "unknown binding obligation kind") pure (parseKind kindText)+                BindingObligation+                    <$> value .: "mappedName"+                    <*> value .: "package"+                    <*> value .: "module"+                    <*> value .: "symbol"+                    <*> pure kindValue+                    <*> value .: "signature"+                    <*> value .: "useSites"+                    <*> value .:? "bindingVersion"++instance ToJSON BindingHole where+    toJSON hole =+        object+            [ "schema" .= (1 :: Int)+            , "mappedName" .= holeMappedName hole+            , "module" .= holeModule hole+            , "symbol" .= holeSymbol hole+            , "kind" .= renderKind (holeKind hole)+            , "path" .= holePath hole+            , "signature" .= holeSignature hole+            ]++instance FromJSON BindingHole where+    parseJSON = withObject "keiro-dsl binding hole" $ \value -> do+        schema <- value .: "schema"+        if schema /= (1 :: Int)+            then fail "unsupported binding hole schema"+            else do+                kindText <- value .: "kind"+                kindValue <- maybe (fail "unknown binding hole kind") pure (parseKind kindText)+                BindingHole+                    <$> value .: "mappedName"+                    <*> value .: "module"+                    <*> value .: "symbol"+                    <*> pure kindValue+                    <*> value .:? "path"+                    <*> value .: "signature"++bindingObligations :: Spec -> Either (NonEmpty TypeGraphError) [BindingObligation]+bindingObligations spec = do+    graph <- resolveTypeGraph spec+    pure . sortOn obligationSortKey . concat $+        [ obligationsFor graph declaration+        | ResolvedStructural declaration _ <- Map.elems (tgDeclarations graph)+        ]++bindingHoles :: Spec -> Either (NonEmpty TypeGraphError) [BindingHole]+bindingHoles spec = do+    graph <- resolveTypeGraph spec+    obligations <- bindingObligations spec+    pure . sortOn holeSortKey . concat $+        [ holesFor graph declaration shape obligations+        | ResolvedStructural declaration shape <- Map.elems (tgDeclarations graph)+        ]++holesFor :: TypeGraph -> StructuralDecl -> ResolvedMappedShape -> [BindingObligation] -> [BindingHole]+holesFor _graph declaration shape obligations = bindingEntries <> auxiliaryEntries+  where+    own = filter ((== sdName declaration) . obligationMappedName) obligations+    binding = onlyKind BindingValue+    bindingEntries = case binding of+        Nothing -> []+        Just obligation -> map (bindingHole obligation) (shapeHolePaths shape)+    auxiliaryEntries =+        [ BindingHole+            { holeMappedName = obligationMappedName obligation+            , holeModule = obligationModule obligation+            , holeSymbol = obligationSymbol obligation+            , holeKind = obligationKind obligation+            , holePath = Nothing+            , holeSignature = obligationSignature obligation+            }+        | obligation <- own+        , obligationKind obligation /= BindingValue+        ]+    onlyKind wanted = case filter ((== wanted) . obligationKind) own of+        entry : _ -> Just entry+        [] -> Nothing+    bindingHole obligation (path, expectedType) =+        BindingHole+            { holeMappedName = obligationMappedName obligation+            , holeModule = obligationModule obligation+            , holeSymbol = obligationSymbol obligation+            , holeKind = BindingValue+            , holePath = Just path+            , holeSignature = obligationSymbol obligation <> "." <> path <> " :: " <> expectedType+            }++shapeHolePaths :: ResolvedMappedShape -> [(Text, Text)]+shapeHolePaths =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields -> [(rwfHaskell field, renderExprType (rwfType field)) | field <- fields]+            , onEnum = \entries -> [(weCtor entry, "constructor case") | entry <- entries]+            , onUnion = \_ arms ->+                [ (rwaCtor arm, maybe "constructor case" renderExprType (rwaPayload arm))+                | arm <- arms+                ]+            }++renderExprType :: ResolvedTypeExpr -> Text+renderExprType =+    foldTypeExpr+        TypeExprAlgebra+            { onText = "Text"+            , onInt = "Int"+            , onBool = "Bool"+            , onNatural = "Natural"+            , onTime = "UTCTime"+            , onJson = "Value"+            , onOptional = \value -> "Maybe (" <> value <> ")"+            , onList = \value -> "[" <> value <> "]"+            , onMap = \value -> "Map Text (" <> value <> ")"+            , onRef = unMappedKey+            }++obligationsFor :: TypeGraph -> StructuralDecl -> [BindingObligation]+obligationsFor graph declaration = bindingEntry : fixtureEntry : initialEntries+  where+    source = sdHaskell declaration+    consumerType = hsModule source <> "." <> hsType source+    shapeType = sdName declaration <> "Shape"+    paths = map renderUsePath (usePaths graph (sdName declaration))+    registerPaths =+        [ renderUsePath path+        | path@UsePath{upRoot = RootRegister{}} <- usePaths graph (sdName declaration)+        ]+    bindingEntry =+        obligationFor+            declaration+            (sdBinding declaration)+            BindingValue+            ("StructuralBinding " <> consumerType <> " " <> shapeType)+            paths+            (Just (unBindingVersion (sdBindingVersion declaration)))+    fixtureEntry =+        obligationFor+            declaration+            (sdFixtures declaration)+            FixtureValue+            ("FixtureCases " <> consumerType)+            paths+            Nothing+    initialEntries = case (registerPaths, sdInitial declaration) of+        ([], _) -> []+        (_, Nothing) -> []+        (_, Just initialValue) ->+            [ obligationFor declaration initialValue InitialValue consumerType registerPaths Nothing+            ]++obligationFor :: StructuralDecl -> QualifiedValueName -> BindingObligationKind -> Text -> [Text] -> Maybe Text -> BindingObligation+obligationFor declaration qualified kindValue signature paths version =+    BindingObligation+        { obligationMappedName = sdName declaration+        , obligationPackage = hsPackage (sdHaskell declaration)+        , obligationModule = ownerModule+        , obligationSymbol = symbol+        , obligationKind = kindValue+        , obligationSignature = symbol <> " :: " <> signature+        , obligationUseSites = paths+        , obligationBindingVersion = version+        }+  where+    (ownerModule, symbol) = splitQualified (unQualifiedValueName qualified)++renderBindingObligations :: Text -> [BindingObligation] -> Text+renderBindingObligations context obligations = case obligations of+    [] -> "no binding obligations for context " <> context+    _ ->+        T.unlines $+            ["binding obligations for context " <> context]+                <> concatMap renderGroup grouped+  where+    grouped = groupBy sameOwner (sortOn obligationSortKey obligations)+    sameOwner left right = ownerKey left == ownerKey right+    renderGroup [] = []+    renderGroup entries@(first : _) =+        ("  " <> obligationModule first <> " (package " <> obligationPackage first <> ")")+            : concatMap renderEntry entries+    renderEntry obligation =+        [ "    " <> obligationSignature obligation+        , "      reason: " <> renderKind (obligationKind obligation) <> " — structural mapped type " <> obligationMappedName obligation <> renderPaths (obligationUseSites obligation)+        ]+            <> maybe [] (\version -> ["      provenance: binding-version " <> quoted version]) (obligationBindingVersion obligation)+    renderPaths [] = " (not currently used by an aggregate root)"+    renderPaths paths = " (" <> T.intercalate "; " paths <> ")"+    quoted value = T.pack (show value)++obligationSortKey :: BindingObligation -> (Text, Text, Text, BindingObligationKind, Text)+obligationSortKey obligation =+    ( obligationPackage obligation+    , obligationModule obligation+    , obligationMappedName obligation+    , obligationKind obligation+    , obligationSymbol obligation+    )++ownerKey :: BindingObligation -> (Text, Text)+ownerKey obligation = (obligationPackage obligation, obligationModule obligation)++holeSortKey :: BindingHole -> (Text, Name, BindingObligationKind, Maybe Text, Text)+holeSortKey hole =+    (holeModule hole, holeMappedName hole, holeKind hole, holePath hole, holeSymbol hole)++renderKind :: BindingObligationKind -> Text+renderKind BindingValue = "binding"+renderKind FixtureValue = "fixtures"+renderKind InitialValue = "initial-value"++parseKind :: Text -> Maybe BindingObligationKind+parseKind "binding" = Just BindingValue+parseKind "fixtures" = Just FixtureValue+parseKind "initial-value" = Just InitialValue+parseKind _ = Nothing++splitQualified :: Text -> (Text, Text)+splitQualified value =+    let (prefix, name) = T.breakOnEnd "." value+     in (T.dropEnd 1 prefix, name)
+ src/Keiro/Dsl/FoldFingerprint.hs view
@@ -0,0 +1,167 @@+{- | 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 (+    aggregateFoldFingerprint,+    aggregateFoldSurface,+) where++import Data.List (find)+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.Grammar+import Keiro.Dsl.PrettyPrint (renderExpr)+import Keiro.Dsl.ReadModelShape (fnv1a64)+import Keiro.Dsl.TypeGraph++-- | The sixteen-hex-digit identity of an aggregate's replay fold.+aggregateFoldFingerprint :: Spec -> Aggregate -> Text+aggregateFoldFingerprint spec = fnv1a64 . aggregateFoldSurface 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 aggregate =+    T.intercalate+        "\n"+        ( map stateSegment (aggStates aggregate)+            ++ map registerSegment (aggRegs aggregate)+            ++ mappedRegisterSegments+            ++ map transitionSegment (aggTransitions aggregate)+            ++ map ruleSegment referencedRules+        )+  where+    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+            , Just declaration <- [Map.lookup (MappedKey (regType register)) (tgDeclarations graph)]+            ]++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 :: RegDecl -> Text+registerSegment register =+    "reg:"+        <> regName register+        <> ":"+        <> regType register+        <> "="+        <> renderInitial (regInitial register)++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 :: Transition -> Text+transitionSegment transition =+    T.intercalate+        "|"+        [ "transition:" <> renderMode (tMode transition)+        , tSource transition+        , tCommand transition+        , "guard=" <> maybe "" renderExpr (tGuard transition)+        , "writes=" <> T.intercalate ";" (map renderWrite (tWrites transition))+        , "emits=" <> T.intercalate "," (tEmits transition)+        , "goto=" <> tGoto transition+        ]+  where+    renderWrite (registerName, expression) = registerName <> ":=" <> renderExpr expression++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+    EAtom (AName name) -> Set.singleton name+    EAtom (ABool _) -> Set.empty++maybeToList :: Maybe a -> [a]+maybeToList Nothing = []+maybeToList (Just value) = [value]
+ src/Keiro/Dsl/Goldens.hs view
@@ -0,0 +1,228 @@+{- | Versioned event-payload fixtures captured at spec-diff time.++The current aggregate specification cannot reconstruct an older payload shape,+so golden payloads are synthesized while both the old and new specifications+are available. Existing files are never overwritten: a hand-captured+production payload is always more authoritative than a synthesized sample.+-}+module Keiro.Dsl.Goldens (+    GoldenEvidence (..),+    GoldenPayload (..),+    goldensForDiff,+    emitGoldenPayloads,+    loadGoldenPayloads,+    goldenRelativePath,+) where++import Data.Aeson (Value (..))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.Text qualified as AesonText+import Data.List (find)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.IO qualified as TIO+import Data.Text.Lazy qualified as TL+import Keiro.Dsl.Grammar+import Keiro.Dsl.Scaffold (Agg (..), ResolvedCtor (..), defaultContext, resolveAgg)+import Keiro.Dsl.TypeGraph+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist)+import System.FilePath (dropTrailingPathSeparator, takeDirectory, takeFileName, (</>))++data GoldenEvidence = SynthesizedWeakStandIn | FileOwnedFixture+    deriving stock (Eq, Show)++data GoldenPayload = GoldenPayload+    { goldenContext :: !Text+    , goldenAggregate :: !Text+    , goldenEvent :: !Text+    , goldenVersion :: !Int+    , goldenJson :: !Text+    , goldenEvidence :: !GoldenEvidence+    }+    deriving stock (Eq, Show)++{- | Synthesize one old-shape payload for each event whose version increases.+The result is deterministic and ordered like the old specification.+-}+goldensForDiff :: Spec -> Spec -> [GoldenPayload]+goldensForDiff oldSpec newSpec =+    [ GoldenPayload+        { goldenContext = specContext oldSpec+        , goldenAggregate = aggName oldAggregate+        , goldenEvent = evName oldEvent+        , goldenVersion = evVersion oldEvent+        , goldenJson = renderGolden oldSpec oldResolved oldResolvedEvent+        , goldenEvidence = SynthesizedWeakStandIn+        }+    | oldAggregate <- aggregates oldSpec+    , Just newAggregate <- [find ((== aggName oldAggregate) . aggName) (aggregates newSpec)]+    , let oldResolved = resolveAgg (defaultContext (specContext oldSpec)) oldSpec oldAggregate+    , oldEvent <- aggEvents oldAggregate+    , Just newEvent <- [find ((== evName oldEvent) . evName) (aggEvents newAggregate)]+    , evVersion newEvent > evVersion oldEvent+    , Just oldResolvedEvent <- [find ((== evName oldEvent) . rcName) (aEvents oldResolved)]+    ]+  where+    aggregates spec = [aggregate | NAggregate aggregate <- specNodes spec]++{- | Write newly synthesized fixtures below+@<root>/<context>/<aggregate>/<event>.v<version>.json@. Existing files are+left untouched and omitted from the returned path list.+-}+emitGoldenPayloads :: FilePath -> Spec -> Spec -> IO [FilePath]+emitGoldenPayloads root oldSpec newSpec =+    fmap concat . traverse writeIfMissing $ goldensForDiff oldSpec newSpec+  where+    writeIfMissing golden = do+        let path = root </> goldenRelativePath golden+        exists <- doesFileExist path+        if exists+            then pure []+            else do+                createDirectoryIfMissing True (takeDirectory path)+                TIO.writeFile path (goldenJson golden)+                pure [path]++{- | Load only the fixtures relevant to declared upcasters in @spec@.+@root@ may name the global golden root or its context child directory.+-}+loadGoldenPayloads :: FilePath -> Spec -> IO [GoldenPayload]+loadGoldenPayloads root spec = do+    contextRoot <- resolveContextRoot root (T.unpack (specContext spec))+    fmap concat . traverse (loadAggregate contextRoot) $ aggregates spec+  where+    aggregates current = [aggregate | NAggregate aggregate <- specNodes current]++    loadAggregate contextRoot aggregate =+        fmap concat . traverse (loadEvent contextRoot aggregate) $ aggEvents aggregate++    loadEvent contextRoot aggregate event = case evUpcastFrom event of+        Nothing -> pure []+        Just (sourceVersion, _) -> do+            let golden =+                    GoldenPayload+                        { goldenContext = specContext spec+                        , goldenAggregate = aggName aggregate+                        , goldenEvent = evName event+                        , goldenVersion = sourceVersion+                        , goldenJson = ""+                        , goldenEvidence = FileOwnedFixture+                        }+                path = contextRoot </> aggregateRelativePath golden+            exists <- doesFileExist path+            if exists+                then do+                    contents <- TIO.readFile path+                    pure [golden{goldenJson = contents}]+                else pure []++goldenRelativePath :: GoldenPayload -> FilePath+goldenRelativePath golden =+    T.unpack (goldenContext golden) </> aggregateRelativePath golden++aggregateRelativePath :: GoldenPayload -> FilePath+aggregateRelativePath golden =+    T.unpack (goldenAggregate golden)+        </> T.unpack (goldenEvent golden)+            <> ".v"+            <> show (goldenVersion golden)+            <> ".json"++resolveContextRoot :: FilePath -> FilePath -> IO FilePath+resolveContextRoot root contextName = do+    let nested = root </> contextName+    nestedExists <- doesDirectoryExist nested+    pure $+        if nestedExists+            then nested+            else+                if takeFileName (dropTrailingPathSeparator root) == contextName+                    then root+                    else nested++renderGolden :: Spec -> Agg -> ResolvedCtor -> Text+renderGolden spec aggregate event =+    TL.toStrict (AesonText.encodeToLazyText (Object (KeyMap.fromList entries))) <> "\n"+  where+    graph = either (const Nothing) Just (resolveTypeGraph spec)+    entries =+        (Key.fromText "kind", String (rcName event))+            : [(Key.fromText fieldName, sampleValue graph spec aggregate fieldType) | (fieldName, fieldType) <- rcFields event]++sampleValue :: Maybe TypeGraph -> Spec -> Agg -> Text -> Value+sampleValue graph spec _aggregate fieldType+    | Just identifier <- find ((== fieldType) . idName) (specIds spec) =+        String (idPrefix identifier <> "_01hzy3v7q2e8kaw2m5x0d41n9c")+    | Just enum <- find ((== fieldType) . enumName) (specEnums spec)+    , (_, wireValue) : _ <- enumCtors enum =+        String wireValue+    | Just resolved <- graph+    , Just declaration <- Map.lookup (MappedKey fieldType) (tgDeclarations resolved) =+        sampleMappedDeclaration resolved declaration+    | fieldType == "Int" = Number 1+    | fieldType == "Bool" = Bool True+    | fieldType `elem` ["Time", "UTCTime"] = String "2026-01-01T00:00:00Z"+    | otherwise = String "sample"++sampleMappedDeclaration :: TypeGraph -> ResolvedMappedDecl -> Value+sampleMappedDeclaration graph =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \_ -> sampleMappedShape graph+            , onOpaqueDecl = const emptyObject+            }++sampleMappedShape :: TypeGraph -> ResolvedMappedShape -> Value+sampleMappedShape graph =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields ->+                Object . KeyMap.fromList $+                    [ (Key.fromText (rwfKey field), sampleMappedExpression graph (rwfType field))+                    | field <- fields+                    , includeField field+                    ]+            , onEnum = \entries -> case entries of+                firstEntry : _ -> String (weTag firstEntry)+                [] -> String "sample"+            , onUnion = \encoding arms -> case arms of+                firstArm : _ ->+                    Object . KeyMap.fromList $+                        [(Key.fromText (ueTagField encoding), String (rwaTag firstArm))]+                            <> [ (Key.fromText (ueContentsField encoding), sampleMappedExpression graph payload)+                               | payload <- maybeToList (rwaPayload firstArm)+                               ]+                [] -> emptyObject+            }+  where+    includeField field = case rwfPresence field of+        PRequired -> True+        POptional -> isNothingValue (rwfOnMissing field)++sampleMappedExpression :: TypeGraph -> ResolvedTypeExpr -> Value+sampleMappedExpression graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = String "sample"+            , onInt = Number 1+            , onBool = Bool True+            , onNatural = Number 1+            , onTime = String "2026-01-01T00:00:00Z"+            , onJson = emptyObject+            , onOptional = id+            , onList = \value -> Array (pure value)+            , onMap = \value -> Object (KeyMap.singleton (Key.fromText "sample") value)+            , onRef = \key -> maybe emptyObject (sampleMappedDeclaration graph) (Map.lookup key (tgDeclarations graph))+            }++emptyObject :: Value+emptyObject = Object KeyMap.empty++maybeToList :: Maybe a -> [a]+maybeToList = maybe [] pure++isNothingValue :: Maybe a -> Bool+isNothingValue Nothing = True+isNothingValue Just{} = False
src/Keiro/Dsl/Grammar.hs view
@@ -15,6 +15,20 @@     EnumDecl (..),     RuleDecl (..), +    -- * Consumer-owned mapped types (EP-149)+    TypeExpr (..),+    Presence (..),+    UnknownFields (..),+    OnMissing (..),+    WireField (..),+    wireFieldLoc,+    UnionEncoding (..),+    WireEnum (..),+    WireArm (..),+    MappedShape (..),+    HaskellSource (..),+    MappedDecl (..),+     -- * The eight hole-kind types     Derivation (..),     DerivStrategy (..),@@ -28,6 +42,7 @@     Expr (..),     CmpOp (..),     Atom (..),+    complementExpr,      -- * The aggregate node     RegInitial (..),@@ -39,6 +54,7 @@     EventBody (..),     Hole (..),     Transition (..),+    TransitionMode (..),     WireSpec (..),     ProjectionSpec (..),     Consistency (..),@@ -112,6 +128,7 @@     -- * The workflow/operation nodes (EP-6)     WfBodyItem (..),     WorkflowNode (..),+    workflowNodeLoc,     OperationShape (..),     OperationNode (..), @@ -179,6 +196,111 @@     }     deriving stock (Eq, Show, Generic) +-- Consumer-owned mapped types (EP-149). The parser-facing declarations keep+-- required facts optional so `keiro-dsl check` can report stable, located+-- diagnostics for omissions. Keiro.Dsl.TypeGraph turns valid values into a+-- checked representation before downstream consumers inspect them.++data TypeExpr+    = TText+    | TInt+    | TBool+    | TNatural+    | TTime+    | TJson+    | TOptional !TypeExpr+    | TList !TypeExpr+    | TMap !TypeExpr+    | TRef !Name+    deriving stock (Eq, Show, Generic)++data Presence = PRequired | POptional+    deriving stock (Eq, Show, Generic)++data UnknownFields = RejectUnknown | IgnoreUnknown+    deriving stock (Eq, Show, Generic)++data OnMissing+    = OmNull+    | OmText !Text+    | OmInt !Integer+    | OmBool !Bool+    | OmEmptyList+    | OmEmptyMap+    | OmCtor !Name+    deriving stock (Eq, Show, Generic)++data WireField = WireField+    { wfHaskell :: !Name+    , wfKey :: !Text+    , wfType :: !TypeExpr+    , wfPresence :: !Presence+    , wfOnMissing :: !(Maybe OnMissing)+    , wfLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++wireFieldLoc :: WireField -> Loc+wireFieldLoc WireField{wfLoc = loc} = loc++data UnionEncoding = TaggedObject+    { ueTagField :: !Text+    , ueContentsField :: !Text+    , ueUnknownFields :: !UnknownFields+    }+    deriving stock (Eq, Show, Generic)++data WireEnum = WireEnum+    { weCtor :: !Name+    , weTag :: !Text+    , weLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data WireArm = WireArm+    { waCtor :: !Name+    , waTag :: !Text+    , waPayload :: !(Maybe TypeExpr)+    , waLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data MappedShape+    = ShapeRecord !Name !UnknownFields ![WireField]+    | ShapeEnum ![WireEnum]+    | ShapeUnion !UnionEncoding ![WireArm]+    deriving stock (Eq, Show, Generic)++data HaskellSource = HaskellSource+    { hsPackage :: !Text+    , hsModule :: !Text+    , hsType :: !Name+    }+    deriving stock (Eq, Show, Generic)++data MappedDecl+    = MappedStructural+        { msName :: !Name+        , msHaskell :: !(Maybe HaskellSource)+        , msBinding :: !(Maybe Text)+        , msBindingVersion :: !(Maybe Text)+        , msCanonical :: !(Maybe Text)+        , msFixtures :: !(Maybe Text)+        , msInitial :: !(Maybe Text)+        , msShape :: !MappedShape+        , msLoc :: !Loc+        }+    | MappedOpaque+        { moName :: !Name+        , moHaskell :: !(Maybe HaskellSource)+        , moCodecId :: !(Maybe Text)+        , moCodecVersion :: !(Maybe Text)+        , moFixtures :: !(Maybe Text)+        , moInitial :: !(Maybe Text)+        , moLoc :: !Loc+        }+    deriving stock (Eq, Show, Generic)+ -- The eight hole-kind types. EP-1 only exercises hole-kinds 1–3 against the -- aggregate vertical; the rest exist so EP-3…EP-6 reuse the same types. @@ -257,6 +379,33 @@     | ABool !Bool     deriving stock (Eq, Show, Generic) +{- | The logical complement of a guard, expressed inside the existing grammar —+'Expr' has no negation constructor, but negation is eliminable: De Morgan over+'EOr'\/'EAnd', comparison-operator flipping, boolean-literal flip, and+@x == false@ for a bare name atom (guards are boolean-valued, so a bare name+in guard position is a boolean read). Used by @diff@ to compute the+replay-only twin of a tightened guard (@old ∧ ¬new@, plan 143): the printed+complement re-parses as a valid guard today.++Caveat: comparison flipping is classical — @¬(a < b) = a >= b@ — which is+correct over the DSL's total ordered domains.+-}+complementExpr :: Expr -> Expr+complementExpr = \case+    EOr l r -> EAnd (complementExpr l) (complementExpr r)+    EAnd l r -> EOr (complementExpr l) (complementExpr r)+    ECmp op l r -> ECmp (complementCmp op) l r+    EAtom (ABool b) -> EAtom (ABool (not b))+    e@(EAtom (AName _)) -> ECmp OpEq e (EAtom (ABool False))+  where+    complementCmp = \case+        OpEq -> OpNeq+        OpNeq -> OpEq+        OpLt -> OpGe+        OpLe -> OpGt+        OpGt -> OpLe+        OpGe -> OpLt+ {- | @name Type = initial@ — a named register with its declared type and the initial value (an identifier: a literal like @placeholder@, an enum constructor, or a state name).@@ -303,10 +452,10 @@     deriving stock (Eq, Show, Generic)  {- | @event Name { … }@ or @event Name = fields(Command)@. EP-2 (evolution) adds-the version/upcaster/deprecation fields: an unversioned event is @evVersion = 1@,-@evUpcastFrom = Nothing@, @evDeprecated = False@, reproducing the EP-1 surface.-These fields live on the shared 'Event' so every node family's events inherit-schema-versioning for free.+the version/upcaster/retirement fields: an unversioned event is @evVersion = 1@,+@evUpcastFrom = Nothing@, @evRetiring = False@, and @evDeprecated = False@,+reproducing the EP-1 surface. These fields live on the shared 'Event' so every+node family's events inherit schema-versioning for free. -} data Event = Event     { evName :: !Name@@ -317,9 +466,16 @@     {- ^ The source version this shape migrates /from/, paired with the upcaster     hole. @Just (n-1, …)@ for a @vN@ shape; 'Nothing' for v1.     -}+    , evRetiring :: !Bool+    {- ^ Retirement is in progress. The event must keep at least one live+    emitting transition while operators terminalize or truncate affected+    streams; cut over to @deprecated@ plus a replay-only emitting transition+    afterwards.+    -}     , evDeprecated :: !Bool-    {- ^ Retired from the write path (no transition may @emit@ it) but still-    decodable from the log.+    {- ^ Retired from the write path (no live transition may @emit@ it) but+    still decodable from the log. A replay-only emitting transition must remain+    while live streams can still contain the event.     -}     , evLoc :: !Loc     }@@ -346,10 +502,23 @@     , tWrites :: ![(Name, Expr)]     , tEmits :: ![Name]     , tGoto :: !Name+    , tMode :: !TransitionMode     , tLoc :: !Loc     }     deriving stock (Eq, Show, Generic) +{- | Whether a transition serves forward execution or replay only (plan 143).+A @replay-only@ transition lowers to a keiki 'ReplayOnly' edge: it is never+taken by a new command and exists so events emitted under a retired rule keep+an inverting edge. Spelled as a @replay-only@ prefix on the transition line:++@+replay-only Held -- ConfirmReservation --> guard … ; emit … ; goto …+@+-}+data TransitionMode = TmLive | TmReplayOnly+    deriving stock (Eq, Show, Generic)+ {- | @wire kind=ctorName fields=camelCase schemaVersion=1@ — how events serialize. -}@@ -927,6 +1096,9 @@     }     deriving stock (Eq, Show, Generic) +workflowNodeLoc :: WorkflowNode -> Loc+workflowNodeLoc WorkflowNode{wfLoc = loc} = loc+ -- | The four operation shapes. data OperationShape     = -- | @command on <Agg> stream from <field> via <fn> project [ … ]@@@ -981,7 +1153,7 @@     deriving stock (Eq, Show, Generic)  {- | A whole @.keiro@ file: one context name, an optional module-placement-override (the @module@/@layout@ clauses), the shared id/enum/rule declarations,+override (the @module@/@layout@ clauses), the shared id/enum/rule/mapped declarations, and the list of nodes. 'specModuleRoot' and 'specLayout' are 'Nothing' when the spec omits the clauses, reproducing the historical default. -}@@ -992,6 +1164,7 @@     , specIds :: ![IdDecl]     , specEnums :: ![EnumDecl]     , specRules :: ![RuleDecl]+    , specMapped :: ![MappedDecl]     , specNodes :: ![Node]     }     deriving stock (Eq, Show, Generic)
src/Keiro/Dsl/Harness.hs view
@@ -14,35 +14,60 @@      stepping a sample command lands on the declared @goto@ vertex. This is the      check a wrong guard fails — flipping @./=@ to @.==@ in the filled body turns      it red while leaving the scaffold untouched.+  5. a forward/replay equality check per live, event-emitting transition out of+     the initial state: emitted events cross the generated codec boundary, then+     replay must reconstruct the forward vertex and every declared register.++@Text@ samples include their field name so same-typed field swaps remain visible+to the replay check. Other sample kinds remain uniform until fixture bindings can+supply a wider, consumer-owned corpus. -} module Keiro.Dsl.Harness (     harnessFor,+    harnessForWithGoldens,     harnessProcess,     harnessRouter,     harnessReadModel,     harnessWorkflow, ) where +import Data.List (find)+import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T+import Keiro.Dsl.Goldens (GoldenPayload (..)) import Keiro.Dsl.Grammar import Keiro.Dsl.ReadModelShape (deriveShapeHash, registryNameFor, subscriptionNameFor) import Keiro.Dsl.Scaffold+import Keiro.Dsl.TypeGraph  {- | Emit the harness test module for one aggregate. Like 'scaffoldAggregate', it takes the 'Spec' for the shared id\/enum declarations. -} harnessFor :: Context -> Spec -> Aggregate -> [ScaffoldModule]-harnessFor ctx spec agg =+harnessFor = harnessForWithGoldens []++{- | Emit an aggregate harness with checked-in old-payload fixtures embedded+as string literals. Embedding keeps the generated test independent of runtime+file paths while retaining the golden file as regeneration source of truth.+-}+harnessForWithGoldens :: [GoldenPayload] -> Context -> Spec -> Aggregate -> [ScaffoldModule]+harnessForWithGoldens goldens ctx spec agg =     [ ScaffoldModule         { modulePath = T.unpack (T.replace "." "/" (aGenPrefix a) <> "/Harness.hs")-        , moduleText = emitHarness a+        , moduleText = emitHarness relevantGoldens a         , kind = Generated         , origin = "aggregate " <> aggName agg <> locSuffix (aggLoc agg)         }     ]   where     a = resolveAgg ctx spec agg+    relevantGoldens =+        [ golden+        | golden <- goldens+        , goldenContext golden == specContext spec+        , goldenAggregate golden == aggName agg+        ]  {- | Emit a self-contained, firewall-clean facts harness for a process manager, pinning the spec's deterministic decisions: the time-injection formula, the@@ -239,13 +264,13 @@         { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/WorkflowFacts.hs")         , moduleText = emitWorkflowFacts genPrefix w         , kind = Generated-        , origin = "workflow " <> wfId w <> locSuffix (wfLoc w)+        , origin = "workflow " <> wfId w <> locSuffix (workflowNodeLoc w)         }     , ScaffoldModule         { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/WorkflowRuntime.hs")         , moduleText = emitWorkflowRuntime genPrefix w         , kind = Generated-        , origin = "workflow " <> wfId w <> locSuffix (wfLoc w)+        , origin = "workflow " <> wfId w <> locSuffix (workflowNodeLoc w)         }     ]   where@@ -351,75 +376,141 @@     go (WfPatch patchId items _) = patchId : workflowPatchIds items     go _ = [] -emitHarness :: Agg -> Text-emitHarness a =+emitHarness :: [GoldenPayload] -> Agg -> Text+emitHarness goldens a =     nl $-        [ "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> aGenPrefix a <> ".Harness (harnessAssertions) where"-        , ""-        , "import " <> aGenPrefix a <> ".Domain"-        , "import " <> aGenPrefix a <> ".Codec (encode" <> nm <> "Event, parse" <> nm <> "Event" <> codecValueImport <> ")"-        , "import " <> aHolePrefix a <> ".Holes (" <> lowerFirst nm <> "Transducer)"-        , "import Keiki.Core (defaultValidationOptions, step, validateTransducer)"-        , codecDecodeRawImport-        , ""-        , "{- | (label, passed). A driver runs these and exits non-zero on any False,"-        , "naming the failing assertion. Filling a hole wrongly turns a specific"-        , "entry False; the scaffold cannot."-        , "-}"-        , "harnessAssertions :: [(String, Bool)]"-        , "harnessAssertions ="-        , "  [ (\"validateTransducer is empty\", null (validateTransducer defaultValidationOptions " <> lowerFirst nm <> "Transducer))"-        , "  , (\"clock-free: spec samples no wall clock\", " <> clockFreeLit <> ")"+        [ "{-# LANGUAGE DataKinds #-}"+        , "{-# LANGUAGE OverloadedLabels #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"         ]+            ++ ["{-# LANGUAGE TypeApplications #-}" | hasMappedHarness a]+            ++ [ generatedBanner+               , "module " <> aGenPrefix a <> ".Harness (harnessAssertions) where"+               , ""+               , "import " <> aGenPrefix a <> ".Domain"+               , "import " <> aGenPrefix a <> ".Codec (encode" <> nm <> "Event, parse" <> nm <> "Event" <> codecValueImport <> mappedCodecHarnessExports a <> ")"+               , "import " <> aHolePrefix a <> ".Holes (" <> lowerFirst nm <> "Transducer)"+               , "import Keiki.Core (" <> T.intercalate ", " coreImports <> ")"+               , codecDecodeRawImport+               ]+            ++ mappedHarnessImports a+            ++ goldenImports+            ++ [ ""+               , "{- | (label, passed). A driver runs these and exits non-zero on any False,"+               , "naming the failing assertion. Filling a hole wrongly turns a specific"+               , "entry False; the scaffold cannot."+               , "-}"+               , "harnessAssertions :: [(String, Bool)]"+               , "harnessAssertions ="+               , "  [ (\"validateTransducer is empty\", null (validateTransducer defaultValidationOptions " <> lowerFirst nm <> "Transducer))"+               , "  , (\"clock-free: spec samples no wall clock\", " <> clockFreeLit <> ")"+               ]             ++ [ "  , (\"golden round-trip: " <> rcName e <> "\", roundTrips sampleEvent" <> rcName e <> ")"                | e <- aEvents a                ]             ++ [ "  , (\"accepts " <> tCommand t <> " from " <> initialVertex a <> "\", accept" <> tCommand t <> ")"                | t <- initialTransitions a                ]-            ++ [ "  , (\"upcaster wired: a v" <> tInt m <> " " <> rcName e <> " payload decodes through the chain\", upcasts" <> rcName e <> ")"-               | e <- upcastEvents-               , Just m <- [rcUpcastFrom e]-               ]             ++ [ "  ]"-               , ""+               ]+            ++ ["  ++ mappedConformanceAssertions" | hasMappedHarness a]+            ++ [ "  ++ forwardReplay" <> tCommand t+               | t <- replayTransitions+               ]+            ++ ( if null upcastEvents+                    then []+                    else+                        [ "  ++ [ " <> T.intercalate "\n     , " upcastAssertions+                        , "     ]"+                        ]+               )+            ++ [ ""                , "roundTrips :: " <> nm <> "Event -> Bool"                , "roundTrips e = parse" <> nm <> "Event (eventType " <> lowerFirst nm <> "Codec e) (encode" <> nm <> "Event e) == Right e"                ]             ++ concatMap (sampleEventDecl a) (aEvents a)             ++ concatMap (acceptDecl a) (initialTransitions a)-            ++ concatMap (upcastDecl a) upcastEvents+            ++ concatMap (forwardReplayDecl a) replayTransitions+            ++ concatMap (upcastDecl goldens a) upcastEvents+            ++ mappedHarnessDeclarations a   where     nm = aName a     -- Bake the clock-free result computed from the spec at scaffold time.     clockFreeLit = if specIsClockFree a then "True" else "False"     upcastEvents = [e | e <- aEvents a, rcUpcastFrom e /= Nothing]+    replayTransitions =+        [ t+        | t <- initialTransitions a+        , tMode t == TmLive+        , not (null (tEmits t))+        ]+    coreImports =+        ["applyEventsEither" | not (null replayTransitions)]+            ++ ["defaultValidationOptions", "step", "validateTransducer"]+            ++ ["fieldWitnessAgrees" | not (null (mappedProjectionSpecs a))]+            ++ ["(!)" | not (null replayTransitions) && not (null (aRegs a))]+    upcastAssertions =+        [ "(" <> tshow (upcastLabel e m) <> ", upcasts" <> rcName e <> ")"+        | e <- upcastEvents+        , Just m <- [rcUpcastFrom e]+        ]     codecValueImport = ", " <> lowerFirst nm <> "Codec"     codecDecodeRawImport =         if null upcastEvents             then "import Keiro.Codec (eventType)"             else "import Keiro.Codec (EventType (..), decodeRaw, eventType)"+    goldenImports =+        if any (hasGolden goldens) upcastEvents+            then+                [ "import Data.Aeson (eitherDecodeStrict)"+                , "import Data.Text.Encoding (encodeUtf8)"+                ]+            else []+    upcastLabel event source =+        case goldenFor goldens event of+            Just _ -> "golden " <> rcName event <> ".v" <> tInt source <> " decodes"+            Nothing ->+                "upcast "+                    <> rcName event+                    <> " chain wired (current-shape stand-in; add a golden payload)" -{- | A wiring-proof assertion: feed a current-shape payload tagged at the-upcaster's source version through @decodeRaw@, which runs the upcaster chain-then @decode@. Red while the upcaster hole returns @Left@; green once filled.-(The grammar records only the current event shape, not the per-version field-delta, so this proves the chain is wired and the hole must be filled rather-than re-deriving the exact old payload.)+{- | Decode a genuine embedded old payload when available. Without a golden,+retain the weaker current-shape wiring assertion and label it honestly. -}-upcastDecl :: Agg -> ResolvedCtor -> [Text]-upcastDecl a e = case rcUpcastFrom e of+upcastDecl :: [GoldenPayload] -> Agg -> ResolvedCtor -> [Text]+upcastDecl goldens a e = case rcUpcastFrom e of     Nothing -> []-    Just m ->-        [ ""-        , "upcasts" <> rcName e <> " :: Bool"-        , "upcasts" <> rcName e <> " ="-        , "  either (const False) (const True)"-        , "    (decodeRaw " <> lowerFirst (aName a) <> "Codec (EventType " <> tshow (rcName e) <> ") " <> tInt m <> " (encode" <> aName a <> "Event sampleEvent" <> rcName e <> "))"-        ]+    Just m -> case goldenFor goldens e of+        Just golden ->+            [ ""+            , "upcasts" <> rcName e <> " :: Bool"+            , "upcasts" <> rcName e <> " ="+            , "  case eitherDecodeStrict (encodeUtf8 " <> tshow (goldenJson golden) <> ") of"+            , "    Left _ -> False"+            , "    Right payload ->"+            , "      either (const False) (const True)"+            , "        (decodeRaw " <> lowerFirst (aName a) <> "Codec (EventType " <> tshow (rcName e) <> ") " <> tInt m <> " payload)"+            ]+        Nothing ->+            [ ""+            , "upcasts" <> rcName e <> " :: Bool"+            , "upcasts" <> rcName e <> " ="+            , "  either (const False) (const True)"+            , "    (decodeRaw " <> lowerFirst (aName a) <> "Codec (EventType " <> tshow (rcName e) <> ") " <> tInt m <> " (encode" <> aName a <> "Event sampleEvent" <> rcName e <> "))"+            ] +hasGolden :: [GoldenPayload] -> ResolvedCtor -> Bool+hasGolden goldens event = case goldenFor goldens event of+    Just _ -> True+    Nothing -> False++goldenFor :: [GoldenPayload] -> ResolvedCtor -> Maybe GoldenPayload+goldenFor goldens event = do+    source <- rcUpcastFrom event+    find+        (\golden -> goldenEvent golden == rcName event && goldenVersion golden == source)+        goldens+ tInt :: Int -> Text tInt = T.pack . show @@ -451,7 +542,8 @@     [] -> []  {- | @sampleEvent<Ctor> :: <Agg>Event@ — a sample built from per-field sample-values (enum→first constructor, Bool→False, id→placeholder, Text→\"sample\").+values (enum→first constructor, Bool→False, id→placeholder,+Text→\"sample-<fieldName>\"). -} sampleEventDecl :: Agg -> ResolvedCtor -> [Text] sampleEventDecl a e =@@ -474,20 +566,515 @@         (c : _) -> "(" <> ctorExpr a c <> ")"         [] -> "(error \"no command\")" +forwardReplayDecl :: Agg -> Transition -> [Text]+forwardReplayDecl a t =+    [ ""+    , "-- forward/replay equality (plan 147): cross the persisted codec boundary,"+    , "-- replay the emitted chain, and compare the final vertex and every register."+    , helperName <> " :: [(String, Bool)]"+    , helperName <> " ="+    , "  case step " <> transducer <> " (" <> initial <> ", " <> initialRegs <> ") " <> cmdSample <> " of"+    , "    Nothing -> [(prefix <> \"forward step accepted\", False)]"+    , "    Just (forwardVertex, " <> forwardRegsName <> ", emitted) ->"+    , "      case mapM (\\event -> parse" <> nm <> "Event (eventType " <> codec <> " event) (encode" <> nm <> "Event event)) emitted of"+    , "        Left _ -> [(prefix <> \"emitted chain decodes\", False)]"+    , "        Right decodedEvents ->"+    , "          case applyEventsEither " <> transducer <> " (" <> initial <> ", " <> initialRegs <> ") decodedEvents of"+    , "            Left _ -> [(prefix <> \"replay succeeds\", False)]"+    , "            Right (replayVertex, " <> replayRegsName <> ") ->"+    , "              [ (prefix <> \"final vertex\", replayVertex == forwardVertex)"+    ]+        ++ [ "              , (prefix <> \"register " <> regName reg <> "\", (replayRegs ! #" <> regName reg <> ") == (forwardRegs ! #" <> regName reg <> "))"+           | reg <- aRegs a+           ]+        ++ [ "              ]"+           , "  where"+           , "    prefix = \"forward/replay equality: " <> tCommand t <> " from " <> initial <> " -- \""+           ]+  where+    nm = aName a+    helperName = "forwardReplay" <> tCommand t+    transducer = lowerFirst nm <> "Transducer"+    codec = lowerFirst nm <> "Codec"+    initial = initialVertex a+    initialRegs = "initial" <> nm <> "Regs"+    forwardRegsName = if null (aRegs a) then "_forwardRegs" else "forwardRegs"+    replayRegsName = if null (aRegs a) then "_replayRegs" else "replayRegs"+    cmdSample = case [c | c <- aCommands a, rcName c == tCommand t] of+        (c : _) -> "(" <> ctorExpr a c <> ")"+        [] -> "(error \"no command\")"+ -- | @(<Ctor> (<Ctor>Data v1 v2 …))@ with positional sample field values. ctorExpr :: Agg -> ResolvedCtor -> Text ctorExpr a rc =     "(" <> rcName rc <> " (" <> rcName rc <> "Data" <> args <> "))"   where-    args = T.concat [" " <> sampleValue a ty | (_, ty) <- rcFields rc]+    args = T.concat [" " <> sampleValue a fieldName ty | (fieldName, ty) <- rcFields rc] -sampleValue :: Agg -> Text -> Text-sampleValue a ty = case fieldCat a ty of+sampleValue :: Agg -> Text -> Text -> Text+sampleValue a fieldName ty = case fieldCat a ty of     IdCat -> "(" <> ty <> " \"sample\")"     EnumCat -> maybe ("(error \"no enum ctor\")") id (firstEnumCtor a ty)+    MappedStructuralCat declaration _ -> fixtureSample (sdFixtures declaration)+    MappedOpaqueCat declaration -> fixtureSample (odFixtures declaration)     OtherCat         | ty == "Bool" -> "False"         | ty == "Int" -> "0"-        | ty == "Text" -> "\"sample\""+        | ty == "Text" -> tshow ("sample-" <> fieldName)         | ty == aVertexType a -> initialVertex a         | otherwise -> "(error \"sample: unsupported type " <> ty <> "\")"++mappedHarnessImports :: Agg -> [Text]+mappedHarnessImports aggregate+    | null fixtures = []+    | otherwise =+        [ "import Data.Aeson qualified as Aeson"+        , "import Data.Aeson.Key qualified as AesonKey"+        , "import Data.Aeson.KeyMap qualified as AesonKeyMap"+        , "import Data.Either (isLeft, isRight)"+        , "import Data.List (nub)"+        , "import Data.List.NonEmpty qualified as NonEmpty"+        , "import Data.Maybe (isJust, isNothing)"+        , "import Data.Proxy (Proxy (..))"+        , "import Data.Text qualified as T"+        , "import Keiki.Shape (CanonicalTypeName (..))"+        , "import Keiro.Codec.Structural (FixtureCases (..), bindingDomainRoundTrip, bindingShapeRoundTrip, bindingToShape)"+        ]+            ++ map (\moduleName -> "import " <> moduleName <> " qualified") (unique (modules <> bindingModules <> shapeModules <> consumerModules))+            ++ ["import " <> structuralProjectionModuleName (aContext aggregate) <> " qualified as StructuralProjections" | not (null (mappedProjectionSpecs aggregate))]+  where+    fixtures = [mappedFixtures declaration | declaration <- mappedHarnessDeclarationsResolved aggregate]+    modules = unique [fst (splitQualifiedHarness (unQualifiedValueName qualified)) | qualified <- fixtures]+    bindingModules =+        [ fst (splitQualifiedHarness (unQualifiedValueName (sdBinding declaration)))+        | ResolvedStructural declaration _ <- mappedHarnessDeclarationsResolved aggregate+        ]+    shapeModules =+        [ structuralShapeModuleName (aContext aggregate) (sdName declaration)+        | ResolvedStructural declaration _ <- mappedHarnessDeclarationsResolved aggregate+        ]+    consumerModules =+        [ hsModule (sdHaskell declaration)+        | ResolvedStructural declaration _ <- mappedHarnessDeclarationsResolved aggregate+        ]++mappedCodecHarnessExports :: Agg -> Text+mappedCodecHarnessExports aggregate =+    T.concat+        [ ", encode" <> sdName declaration <> "Mapped, decode" <> sdName declaration <> "Mapped"+        | ResolvedStructural declaration _ <- codecMappedDeclarations aggregate+        ]++fixtureSample :: QualifiedValueName -> Text+fixtureSample qualified =+    "(snd (NonEmpty.head (fixtureCases " <> unQualifiedValueName qualified <> ")))"++splitQualifiedHarness :: Text -> (Text, Text)+splitQualifiedHarness value =+    let (prefix, name) = T.breakOnEnd "." value+     in (T.dropEnd 1 prefix, name)++unique :: (Eq value) => [value] -> [value]+unique = foldr (\value values -> if value `elem` values then values else value : values) []++hasMappedHarness :: Agg -> Bool+hasMappedHarness = not . null . mappedHarnessDeclarationsResolved++mappedHarnessDeclarationsResolved :: Agg -> [ResolvedMappedDecl]+mappedHarnessDeclarationsResolved aggregate = case aTypeGraph aggregate of+    Nothing -> []+    Just graph -> Map.elems (tgDeclarations graph)++mappedProjectionSpecs :: Agg -> [StructuralProjection]+mappedProjectionSpecs aggregate = case aTypeGraph aggregate of+    Nothing -> []+    Just graph -> map (resolveProjectionModules (aContext aggregate)) (projectionSpecs graph)++structuralShapeModuleName :: Context -> Name -> Text+structuralShapeModuleName context name = case placement context of+    GeneratedPrefix -> root <> "Generated." <> contextSegment <> ".Structural.Shape." <> name+    CollocatedLeaf -> root <> contextSegment <> ".Generated.Structural.Shape." <> name+  where+    root = if T.null (moduleRoot context) then "" else moduleRoot context <> "."+    contextSegment = pascalFromKebab (contextName context)++structuralProjectionModuleName :: Context -> Text+structuralProjectionModuleName context = case placement context of+    GeneratedPrefix -> root <> "Generated." <> contextSegment <> ".StructuralProjections"+    CollocatedLeaf -> root <> contextSegment <> ".Generated.StructuralProjections"+  where+    root = if T.null (moduleRoot context) then "" else moduleRoot context <> "."+    contextSegment = pascalFromKebab (contextName context)++mappedHarnessDeclarations :: Agg -> [Text]+mappedHarnessDeclarations aggregate+    | not (hasMappedHarness aggregate) = []+    | otherwise =+        [ ""+        , "mappedConformanceAssertions :: [(String, Bool)]"+        , "mappedConformanceAssertions ="+        , "  concat"+        , "    [ " <> T.intercalate "\n    , " assertionLists+        , "    ]"+        , ""+        , "validFixtureLabels :: NonEmpty.NonEmpty (T.Text, value) -> Bool"+        , "validFixtureLabels cases ="+        , "  all (not . T.null) labels && length labels == length (nub labels)"+        , "  where"+        , "    labels = map fst (NonEmpty.toList cases)"+        ]+            ++ concatMap (bindingAssertionDecl aggregate) structural+            ++ concatMap (opaqueAssertionDecl aggregate) opaque+            ++ concatMap (coverageDecl aggregate) structural+            ++ concatMap (mappedEventAssertionDecl aggregate) mappedEventFields+            ++ wirePolicyAssertionDecls aggregate structuralWire+            ++ projectionAssertionDecls aggregate structural+            ++ wirePolicyHelpers structuralWire+  where+    declarations = mappedHarnessDeclarationsResolved aggregate+    structural = [(declaration, shape) | ResolvedStructural declaration shape <- declarations]+    opaque = [declaration | ResolvedOpaque declaration <- declarations]+    structuralWire = [(declaration, shape) | ResolvedStructural declaration shape <- codecMappedDeclarations aggregate]+    mappedEventFields =+        [ (event, fieldName, fieldType, declaration)+        | event <- aEvents aggregate+        , (fieldName, fieldType) <- rcFields event+        , declaration <- maybeToListHarness (mappedDeclaration aggregate fieldType)+        ]+    assertionLists =+        [lowerFirst (sdName declaration) <> "BindingAssertions" | (declaration, _) <- structural]+            <> [lowerFirst (odName declaration) <> "OpaqueAssertions" | declaration <- opaque]+            <> [ "[(\"fixture coverage: "+                    <> unCanonicalTypeId (sdCanonical declaration)+                    <> "\", coverage"+                    <> sdName declaration+                    <> ")]"+               | (declaration, _) <- structural+               ]+            <> [ mappedEventAssertionName event fieldName <> "Assertions"+               | (event, fieldName, _, _) <- mappedEventFields+               ]+            <> ["structuralWirePolicyAssertions" | not (null structuralWire)]+            <> ["structuralProjectionAssertions" | not (null (mappedProjectionSpecs aggregate))]++mappedDeclaration :: Agg -> Text -> Maybe ResolvedMappedDecl+mappedDeclaration aggregate name = do+    graph <- aTypeGraph aggregate+    Map.lookup (MappedKey name) (tgDeclarations graph)++bindingAssertionDecl :: Agg -> (StructuralDecl, ResolvedMappedShape) -> [Text]+bindingAssertionDecl _aggregate (declaration, _shape) =+    [ ""+    , valueName <> " :: [(String, Bool)]"+    , valueName <> " ="+    , "  (\"fixture labels: " <> canonical <> "\", validFixtureLabels cases) :"+    , "  (\"canonical identity: " <> canonical <> "\", canonicalTypeName (Proxy @" <> consumerType <> ") == " <> tshow canonical <> ") :"+    , "  concat"+    , "    [ [ (\"binding domain round-trip: " <> canonical <> "/\" <> T.unpack label, bindingDomainRoundTrip " <> binding <> " value)"+    , "      , (\"binding shape round-trip: " <> canonical <> "/\" <> T.unpack label, bindingShapeRoundTrip " <> binding <> " (bindingToShape " <> binding <> " value))"+    , "      ]"+    , "    | (label, value) <- NonEmpty.toList cases"+    , "    ]"+    , "  where"+    , "    cases = fixtureCases " <> fixtures+    ]+  where+    valueName = lowerFirst (sdName declaration) <> "BindingAssertions"+    canonical = unCanonicalTypeId (sdCanonical declaration)+    consumerType = hsModule (sdHaskell declaration) <> "." <> hsType (sdHaskell declaration)+    binding = unQualifiedValueName (sdBinding declaration)+    fixtures = unQualifiedValueName (sdFixtures declaration)++opaqueAssertionDecl :: Agg -> OpaqueDecl -> [Text]+opaqueAssertionDecl _aggregate declaration =+    [ ""+    , valueName <> " :: [(String, Bool)]"+    , valueName <> " ="+    , "  (\"opaque boundary fixtures: " <> label <> "\", validFixtureLabels cases) :"+    , "  [ (\"opaque codec round-trip: " <> label <> "/\" <> T.unpack caseLabel, case Aeson.fromJSON (Aeson.toJSON value) of Aeson.Success decoded -> decoded == value; Aeson.Error _ -> False)"+    , "  | (caseLabel, value) <- NonEmpty.toList cases"+    , "  ]"+    , "  where"+    , "    cases = fixtureCases " <> fixtures+    ]+  where+    valueName = lowerFirst (odName declaration) <> "OpaqueAssertions"+    label = unCodecIdentity (odCodecIdentity declaration) <> "@" <> unCodecVersion (odCodecVersion declaration)+    fixtures = unQualifiedValueName (odFixtures declaration)++coverageDecl :: Agg -> (StructuralDecl, ResolvedMappedShape) -> [Text]+coverageDecl aggregate (declaration, shape) =+    [ ""+    , "coverage" <> sdName declaration <> " :: Bool"+    , "coverage" <> sdName declaration <> " = " <> coverageExpression aggregate declaration shape+    ]++coverageExpression :: Agg -> StructuralDecl -> ResolvedMappedShape -> Text+coverageExpression aggregate declaration shape = case obligations of+    [] -> "True"+    _ -> T.intercalate " && " obligations <> "\n  where\n    shapes = map (bindingToShape " <> binding <> " . snd) (NonEmpty.toList (fixtureCases " <> fixtures <> "))"+  where+    shapeModule = structuralShapeModuleName (aContext aggregate) (sdName declaration)+    binding = unQualifiedValueName (sdBinding declaration)+    fixtures = unQualifiedValueName (sdFixtures declaration)+    obligations = case shape of+        RRecord _ _ fields -> concatMap (recordFieldObligation shapeModule) fields+        REnum entries ->+            [ "any (\\case " <> shapeModule <> "." <> weCtor entry <> " -> True; _ -> False) shapes"+            | entry <- entries+            ]+        RUnion _ arms -> concatMap (unionArmObligations shapeModule) arms++recordFieldObligation :: Text -> ResolvedWireField -> [Text]+recordFieldObligation shapeModule field = case rwfType field of+    ROptional _ ->+        [ "any (isNothing . " <> selector <> ") shapes"+        , "any (isJust . " <> selector <> ") shapes"+        ]+    _ -> []+  where+    selector = shapeModule <> "." <> rwfHaskell field++unionArmObligations :: Text -> ResolvedWireArm -> [Text]+unionArmObligations shapeModule arm =+    ["any (\\case " <> patternText <> " -> True; _ -> False) shapes"] <> optionalPayload+  where+    constructor = shapeModule <> "." <> rwaCtor arm+    patternText = constructor <> maybe "" (const "{}") (rwaPayload arm)+    optionalPayload = case rwaPayload arm of+        Just (ROptional _) ->+            [ "any (\\case " <> constructor <> " Nothing -> True; _ -> False) shapes"+            , "any (\\case " <> constructor <> " (Just _) -> True; _ -> False) shapes"+            ]+        _ -> []++mappedEventAssertionDecl :: Agg -> (ResolvedCtor, Text, Text, ResolvedMappedDecl) -> [Text]+mappedEventAssertionDecl aggregate (event, fieldName, _fieldType, declaration) =+    [ ""+    , valueName <> "Assertions :: [(String, Bool)]"+    , valueName <> "Assertions ="+    , "  [ (\"mapped codec round-trip: " <> rcName event <> "/" <> fieldName <> "/\" <> T.unpack label, roundTrips " <> eventExpression <> ")"+    , "  | (label, mappedValue) <- NonEmpty.toList (fixtureCases " <> fixtures <> ")"+    , "  ]"+    ]+  where+    valueName = mappedEventAssertionName event fieldName+    fixtures = unQualifiedValueName (mappedFixtures declaration)+    eventExpression = ctorExprWithOverride aggregate event fieldName "mappedValue"++mappedEventAssertionName :: ResolvedCtor -> Text -> Text+mappedEventAssertionName event fieldName = lowerFirst (rcName event) <> pascal fieldName++wirePolicyAssertionDecls :: Agg -> [(StructuralDecl, ResolvedMappedShape)] -> [Text]+wirePolicyAssertionDecls _aggregate [] = []+wirePolicyAssertionDecls aggregate declarations =+    [ ""+    , "structuralWirePolicyAssertions :: [(String, Bool)]"+    , "structuralWirePolicyAssertions ="+    , "  [ " <> T.intercalate "\n  , " assertions+    , "  ]"+    ]+  where+    assertions = concatMap (wirePolicyAssertions aggregate) declarations++wirePolicyAssertions :: Agg -> (StructuralDecl, ResolvedMappedShape) -> [Text]+wirePolicyAssertions aggregate (declaration, shape) = case shape of+    RRecord _ unknownFields fields ->+        concatMap (recordMissingAssertions aggregate declaration) [field | field <- fields, rwfPresence field == POptional]+            <> [unknownFieldAssertion declaration unknownFields]+    REnum entries -> map (enumArmAssertion declaration) entries <> [enumUnknownAssertion declaration]+    RUnion encoding arms ->+        map (unionArmAssertion declaration encoding) arms+            <> [unknownFieldAssertion declaration (ueUnknownFields encoding)]++recordMissingAssertions :: Agg -> StructuralDecl -> ResolvedWireField -> [Text]+recordMissingAssertions aggregate declaration field =+    [ "(\"wire policy missing default: "+        <> canonical+        <> "/"+        <> rwfKey field+        <> "\", case "+        <> decoder+        <> " (deleteObjectField "+        <> tshow (rwfKey field)+        <> " ("+        <> encodedSample+        <> ")) of Left _ -> False; Right decoded -> objectField "+        <> tshow (rwfKey field)+        <> " ("+        <> encoder+        <> " decoded) == Just ("+        <> missingExpectedValue aggregate field+        <> "))"+    , "(\"wire policy explicit null: "+        <> canonical+        <> "/"+        <> rwfKey field+        <> "\", "+        <> nullExpectation+        <> " ("+        <> decoder+        <> " (insertObjectField "+        <> tshow (rwfKey field)+        <> " Aeson.Null ("+        <> encodedSample+        <> "))))"+    ]+  where+    canonical = unCanonicalTypeId (sdCanonical declaration)+    encoder = "encode" <> sdName declaration <> "Mapped"+    decoder = "decode" <> sdName declaration <> "Mapped"+    fixtures = unQualifiedValueName (sdFixtures declaration)+    encodedSample = encoder <> " (snd (NonEmpty.head (fixtureCases " <> fixtures <> ")))"+    nullExpectation = case rwfType field of+        ROptional _ -> "isRight"+        _ -> "isLeft"++missingExpectedValue :: Agg -> ResolvedWireField -> Text+missingExpectedValue aggregate field = case rwfOnMissing field of+    Just OmNull -> "Aeson.Null"+    Just (OmText value) -> "Aeson.String " <> tshow value+    Just (OmInt value) -> "Aeson.toJSON (" <> T.pack (show value) <> " :: Int)"+    Just (OmBool value) -> if value then "Aeson.Bool True" else "Aeson.Bool False"+    Just OmEmptyList -> "Aeson.toJSON ([] :: [Aeson.Value])"+    Just OmEmptyMap -> "Aeson.Object mempty"+    Just (OmCtor constructor) -> case (aTypeGraph aggregate, rwfType field) of+        (Just graph, RRef key) -> case Map.lookup key (tgDeclarations graph) of+            Just (ResolvedStructural _ (REnum entries)) -> case find ((== constructor) . weCtor) entries of+                Just entry -> "Aeson.String " <> tshow (weTag entry)+                Nothing -> "error \"missing enum default constructor\""+            _ -> "error \"non-enum constructor default\""+        _ -> "error \"non-reference constructor default\""+    Nothing -> "error \"optional field lacks on-missing policy\""++unknownFieldAssertion :: StructuralDecl -> UnknownFields -> Text+unknownFieldAssertion declaration policy =+    "(\"wire policy unknown fields: "+        <> unCanonicalTypeId (sdCanonical declaration)+        <> "\", all (\\(_, value) -> "+        <> expectation+        <> " (decode"+        <> sdName declaration+        <> "Mapped (insertObjectField \"__keiro_unknown\" (Aeson.Bool True) (encode"+        <> sdName declaration+        <> "Mapped value)))) (NonEmpty.toList (fixtureCases "+        <> unQualifiedValueName (sdFixtures declaration)+        <> ")))"+  where+    expectation = case policy of+        RejectUnknown -> "isLeft"+        IgnoreUnknown -> "isRight"++enumArmAssertion :: StructuralDecl -> WireEnum -> Text+enumArmAssertion declaration entry =+    "(\"wire enum arm: "+        <> unCanonicalTypeId (sdCanonical declaration)+        <> "/"+        <> weTag entry+        <> "\", any (\\(_, value) -> encode"+        <> sdName declaration+        <> "Mapped value == Aeson.String "+        <> tshow (weTag entry)+        <> " && decode"+        <> sdName declaration+        <> "Mapped (Aeson.String "+        <> tshow (weTag entry)+        <> ") == Right value) (NonEmpty.toList (fixtureCases "+        <> unQualifiedValueName (sdFixtures declaration)+        <> ")))"++enumUnknownAssertion :: StructuralDecl -> Text+enumUnknownAssertion declaration =+    "(\"wire enum unknown tag: "+        <> unCanonicalTypeId (sdCanonical declaration)+        <> "\", isLeft (decode"+        <> sdName declaration+        <> "Mapped (Aeson.String \"__keiro_unknown\")))"++unionArmAssertion :: StructuralDecl -> UnionEncoding -> ResolvedWireArm -> Text+unionArmAssertion declaration encoding arm =+    "(\"wire union arm: "+        <> unCanonicalTypeId (sdCanonical declaration)+        <> "/"+        <> rwaTag arm+        <> "\", any (\\(_, value) -> objectField "+        <> tshow (ueTagField encoding)+        <> " (encode"+        <> sdName declaration+        <> "Mapped value) == Just (Aeson.String "+        <> tshow (rwaTag arm)+        <> ") && decode"+        <> sdName declaration+        <> "Mapped (encode"+        <> sdName declaration+        <> "Mapped value) == Right value) (NonEmpty.toList (fixtureCases "+        <> unQualifiedValueName (sdFixtures declaration)+        <> ")))"++wirePolicyHelpers :: [(StructuralDecl, ResolvedMappedShape)] -> [Text]+wirePolicyHelpers [] = []+wirePolicyHelpers _ =+    [ ""+    , "deleteObjectField :: T.Text -> Aeson.Value -> Aeson.Value"+    , "deleteObjectField key (Aeson.Object objectValue) = Aeson.Object (AesonKeyMap.delete (AesonKey.fromText key) objectValue)"+    , "deleteObjectField _ value = value"+    , ""+    , "insertObjectField :: T.Text -> Aeson.Value -> Aeson.Value -> Aeson.Value"+    , "insertObjectField key inserted (Aeson.Object objectValue) = Aeson.Object (AesonKeyMap.insert (AesonKey.fromText key) inserted objectValue)"+    , "insertObjectField _ _ value = value"+    , ""+    , "objectField :: T.Text -> Aeson.Value -> Maybe Aeson.Value"+    , "objectField key (Aeson.Object objectValue) = AesonKeyMap.lookup (AesonKey.fromText key) objectValue"+    , "objectField _ _ = Nothing"+    ]++mappedFixtures :: ResolvedMappedDecl -> QualifiedValueName+mappedFixtures (ResolvedStructural declaration _) = sdFixtures declaration+mappedFixtures (ResolvedOpaque declaration) = odFixtures declaration++ctorExprWithOverride :: Agg -> ResolvedCtor -> Text -> Text -> Text+ctorExprWithOverride aggregate constructor target replacement =+    "(" <> rcName constructor <> " (" <> rcName constructor <> "Data" <> arguments <> "))"+  where+    arguments =+        T.concat+            [ " " <> if fieldName == target then replacement else sampleValue aggregate fieldName fieldType+            | (fieldName, fieldType) <- rcFields constructor+            ]++projectionAssertionDecls :: Agg -> [(StructuralDecl, ResolvedMappedShape)] -> [Text]+projectionAssertionDecls aggregate structural+    | null specs = []+    | otherwise =+        [ ""+        , "structuralProjectionAssertions :: [(String, Bool)]"+        , "structuralProjectionAssertions ="+        , "  [ " <> T.intercalate "\n  , " (map assertion specs)+        , "  ]"+        ]+  where+    specs = mappedProjectionSpecs aggregate+    assertion spec =+        "(\"projection witness agreement: "+            <> unCanonicalTypeId (spCanonical spec)+            <> spPointer spec+            <> "\", all (\\(_, owner) -> fieldWitnessAgrees StructuralProjections."+            <> spWitness spec+            <> " (\\referenceOwner -> "+            <> projectionGetter "referenceOwner" spec+            <> ") owner) (NonEmpty.toList (fixtureCases "+            <> ownerFixtures spec+            <> ")))"+    ownerFixtures spec = case find (\(declaration, _) -> sdCanonical declaration == spCanonical spec) structural of+        Just (declaration, _) -> unQualifiedValueName (sdFixtures declaration)+        Nothing -> "error \"projection owner fixtures missing\""++projectionGetter :: Text -> StructuralProjection -> Text+projectionGetter owner spec =+    foldl+        (\value (shapeModule, selector) -> shapeModule <> "." <> selector <> " (" <> value <> ")")+        ("bindingToShape " <> unQualifiedValueName (spBinding spec) <> " " <> owner)+        (spSelectors spec)++maybeToListHarness :: Maybe value -> [value]+maybeToListHarness = maybe [] pure
src/Keiro/Dsl/Manifest.hs view
@@ -16,7 +16,8 @@   * intake/emit/publisher (full integration path)                         => effectful-core, hasql-transaction, keiro, kiroku-store                                                          (…-intake-full)-  * workqueue           => aeson, keiro-pgmq, text       (…-queue, …-queue-runtime)+  * workqueue           => aeson, keiro-core, keiro-pgmq, text+                                                         (…-queue, …-queue-runtime)   * dispatch            => aeson, effectful-core, keiro-pgmq, text                                                          (…-dispatch-full)   * workflow/operation  => containers, effectful-core, keiro, text@@ -35,6 +36,7 @@ import Data.Text (Text) import Data.Text qualified as T import Keiro.Dsl.Grammar+import Keiro.Dsl.MappedConsumer (ConsumerPlan (..), consumerPlan) import Keiro.Dsl.Scaffold (ScaffoldModule (..))  {- | Render a Cabal-pasteable manifest from the modules a scaffold run produced@@ -56,6 +58,20 @@                , "build-depends:"                ]             ++ map ("    , " <>) (manifestDependencies spec)+            ++ consumerBlocks+  where+    plan = consumerPlan spec+    consumerBlocks+        | null (consumerMappings plan) = []+        | otherwise =+            [ ""+            , "consumer-packages:"+            ]+                ++ map ("    " <>) (consumerPackages plan)+                ++ [ ""+                   , "consumer-modules:"+                   ]+                ++ map ("    " <>) (consumerModules plan)  {- | The dotted module name recovered from a 'ScaffoldModule' path: drop the trailing @.hs@ and replace @/@ with @.@.@@ -68,7 +84,7 @@ -} manifestDependencies :: Spec -> [Text] manifestDependencies spec =-    sort (nub ("base" : concatMap depsForNode (specNodes spec)))+    sort (nub ("base" : consumerPackages (consumerPlan spec) <> concatMap depsForNode (specNodes spec)))  -- | The dependencies a single node kind implies (see the module header table). depsForNode :: Node -> [Text]@@ -80,7 +96,7 @@     NIntake{} -> integration     NEmit{} -> integration     NPublisher{} -> integration-    NWorkqueue{} -> ["aeson", "keiro-pgmq", "text"]+    NWorkqueue{} -> ["aeson", "keiro-core", "keiro-pgmq", "text"]     NPgmqDispatch{} -> ["aeson", "effectful-core", "keiro-pgmq", "text"]     NReadModel{} -> ["effectful-core", "hasql-transaction", "keiro", "kiroku-store", "text"]     NWorkflow{} -> ["containers", "effectful-core", "keiro", "text"]
+ src/Keiro/Dsl/MappedConsumer.hs view
@@ -0,0 +1,162 @@+{- | One checked projection of mapped declarations for every scaffold+integration surface. Keeping dependency requirements and persisted identities+together prevents the manifest, preflight report, and scaffold record from+silently disagreeing.+-}+module Keiro.Dsl.MappedConsumer (+    ConsumerPlan (..),+    MappingIdentity (..),+    consumerPlan,+) where++import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=))+import Data.List (nub, sort)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Grammar (HaskellSource (..), Spec)+import Keiro.Dsl.TypeGraph++data ConsumerPlan = ConsumerPlan+    { consumerPackages :: ![Text]+    , consumerModules :: ![Text]+    , consumerMappings :: ![MappingIdentity]+    }+    deriving stock (Eq, Show)++data MappingIdentity+    = StructuralMapping+        { mappingSpecName :: !Text+        , mappingCanonicalType :: !Text+        , mappingPackage :: !Text+        , mappingModule :: !Text+        , mappingType :: !Text+        , mappingBindingSymbol :: !Text+        , mappingBindingVersion :: !Text+        }+    | OpaqueMapping+        { mappingSpecName :: !Text+        , mappingPackage :: !Text+        , mappingModule :: !Text+        , mappingType :: !Text+        , mappingCodecIdentity :: !Text+        , mappingCodecVersion :: !Text+        }+    deriving stock (Eq, Show)++instance ToJSON MappingIdentity where+    toJSON StructuralMapping{mappingSpecName, mappingCanonicalType, mappingPackage, mappingModule, mappingType, mappingBindingSymbol, mappingBindingVersion} =+        object+            [ "schema" .= (1 :: Int)+            , "mode" .= ("structural" :: Text)+            , "specName" .= mappingSpecName+            , "canonicalType" .= mappingCanonicalType+            , "package" .= mappingPackage+            , "module" .= mappingModule+            , "type" .= mappingType+            , "bindingSymbol" .= mappingBindingSymbol+            , "bindingVersion" .= mappingBindingVersion+            ]+    toJSON OpaqueMapping{mappingSpecName, mappingPackage, mappingModule, mappingType, mappingCodecIdentity, mappingCodecVersion} =+        object+            [ "schema" .= (1 :: Int)+            , "mode" .= ("opaque" :: Text)+            , "specName" .= mappingSpecName+            , "package" .= mappingPackage+            , "module" .= mappingModule+            , "type" .= mappingType+            , "codecIdentity" .= mappingCodecIdentity+            , "codecVersion" .= mappingCodecVersion+            ]++instance FromJSON MappingIdentity where+    parseJSON = withObject "keiro-dsl mapping identity" $ \value -> do+        schema <- value .: "schema"+        if schema /= (1 :: Int)+            then fail "unsupported mapping identity schema"+            else do+                mode <- value .: "mode"+                case (mode :: Text) of+                    "structural" ->+                        StructuralMapping+                            <$> value .: "specName"+                            <*> value .: "canonicalType"+                            <*> value .: "package"+                            <*> value .: "module"+                            <*> value .: "type"+                            <*> value .: "bindingSymbol"+                            <*> value .: "bindingVersion"+                    "opaque" ->+                        OpaqueMapping+                            <$> value .: "specName"+                            <*> value .: "package"+                            <*> value .: "module"+                            <*> value .: "type"+                            <*> value .: "codecIdentity"+                            <*> value .: "codecVersion"+                    _ -> fail "unknown mapping identity mode"++consumerPlan :: Spec -> ConsumerPlan+consumerPlan spec = case resolveTypeGraph spec of+    Left _ -> ConsumerPlan [] [] []+    Right graph ->+        ConsumerPlan+            { consumerPackages = uniqueSorted [hsPackage (mappedSource declaration) | declaration <- declarations]+            , consumerModules = uniqueSorted (concatMap mappedModules declarations)+            , consumerMappings = sortMappings (map mappingIdentity declarations)+            }+      where+        declarations = Map.elems (tgDeclarations graph)++mappedSource :: ResolvedMappedDecl -> HaskellSource+mappedSource (ResolvedStructural declaration _) = sdHaskell declaration+mappedSource (ResolvedOpaque declaration) = odHaskell declaration++mappedModules :: ResolvedMappedDecl -> [Text]+mappedModules (ResolvedStructural declaration _) =+    hsModule (sdHaskell declaration)+        : qualifiedModule (sdBinding declaration)+        : qualifiedModule (sdFixtures declaration)+        : maybe [] (pure . qualifiedModule) (sdInitial declaration)+mappedModules (ResolvedOpaque declaration) =+    hsModule (odHaskell declaration)+        : qualifiedModule (odFixtures declaration)+        : maybe [] (pure . qualifiedModule) (odInitial declaration)++mappingIdentity :: ResolvedMappedDecl -> MappingIdentity+mappingIdentity (ResolvedStructural declaration _) =+    StructuralMapping+        { mappingSpecName = sdName declaration+        , mappingCanonicalType = unCanonicalTypeId (sdCanonical declaration)+        , mappingPackage = hsPackage (sdHaskell declaration)+        , mappingModule = hsModule (sdHaskell declaration)+        , mappingType = hsType (sdHaskell declaration)+        , mappingBindingSymbol = unQualifiedValueName (sdBinding declaration)+        , mappingBindingVersion = unBindingVersion (sdBindingVersion declaration)+        }+mappingIdentity (ResolvedOpaque declaration) =+    OpaqueMapping+        { mappingSpecName = odName declaration+        , mappingPackage = hsPackage (odHaskell declaration)+        , mappingModule = hsModule (odHaskell declaration)+        , mappingType = hsType (odHaskell declaration)+        , mappingCodecIdentity = unCodecIdentity (odCodecIdentity declaration)+        , mappingCodecVersion = unCodecVersion (odCodecVersion declaration)+        }++qualifiedModule :: QualifiedValueName -> Text+qualifiedModule qualified = T.dropEnd 1 (fst (T.breakOnEnd "." (unQualifiedValueName qualified)))++sortMappings :: [MappingIdentity] -> [MappingIdentity]+sortMappings = sortOnName+  where+    sortOnName [] = []+    sortOnName mappings =+        [ mapping+        | name <- sort (map mappingSpecName mappings)+        , mapping <- mappings+        , mappingSpecName mapping == name+        ]++uniqueSorted :: [Text] -> [Text]+uniqueSorted = sort . nub
+ src/Keiro/Dsl/MappedDiff.hs view
@@ -0,0 +1,539 @@+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++{- | Recursive, wire-aware differences for consumer-owned mapped types.++This module deliberately returns mapped findings rather than importing the+ordinary 'Change' type: 'Keiro.Dsl.Diff' owns compatibility vectors and turns+each complete mapped use path into the appropriate event, snapshot, or build+finding. Keeping that seam acyclic also makes the recursive comparison usable+by mutation coverage without rendering a report.+-}+module Keiro.Dsl.MappedDiff (+    MappedFinding (..),+    diffMapped,+    renderMappedSubject,+) where++import Data.List (find, nubBy, sortOn)+import Data.Map.Strict qualified as Map+import Data.Maybe (isNothing)+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Grammar+import Keiro.Dsl.TypeGraph+import Keiro.Dsl.Validate (DiagnosticCode (..))++data MappedFinding = MappedFinding+    { mfDeclaration :: !Name+    , mfLeaf :: !Text+    , mfCode :: !DiagnosticCode+    , mfDetail :: !Text+    , mfUsePaths :: ![UsePath]+    , mfOldUnknownFields :: !(Maybe UnknownFields)+    }+    deriving stock (Eq, Show)++{- | Compare valid old/new mapped graphs. A spec that cannot resolve has+already failed @check@; the ordinary differ therefore emits no speculative+mapped compatibility claim for it.+-}+diffMapped :: Spec -> Spec -> [MappedFinding]+diffMapped oldSpec newSpec = case (resolveTypeGraph oldSpec, resolveTypeGraph newSpec) of+    (Right oldGraph, Right newGraph) ->+        concatMap (uncurry (diffDeclaration oldGraph newGraph)) matched+            ++ map (addedDeclaration newGraph) added+            ++ map (removedDeclaration oldGraph) removed+      where+        oldDeclarations = tgDeclarations oldGraph+        newDeclarations = tgDeclarations newGraph+        matched =+            [ (oldDeclaration, newDeclaration)+            | (key, newDeclaration) <- Map.toList newDeclarations+            , oldDeclaration <- maybeToList (Map.lookup key oldDeclarations)+            ]+        added =+            [ (key, declaration)+            | (key, declaration) <- Map.toList newDeclarations+            , Map.notMember key oldDeclarations+            ]+        removed =+            [ (key, declaration)+            | (key, declaration) <- Map.toList oldDeclarations+            , Map.notMember key newDeclarations+            ]+    _ -> []++renderMappedSubject :: UsePath -> Text -> Text+renderMappedSubject path leaf =+    renderUsePath path <> if T.null leaf then "" else " " <> leaf++data DeclView+    = StructuralView !StructuralDecl !ShapeView+    | OpaqueView !OpaqueDecl++data ShapeView+    = RecordView !Name !UnknownFields ![ResolvedWireField]+    | EnumView ![WireEnum]+    | UnionView !UnionEncoding ![ResolvedWireArm]++data ExprView+    = ExprText+    | ExprInt+    | ExprBool+    | ExprNatural+    | ExprTime+    | ExprJson+    | ExprOptional !ExprView+    | ExprList !ExprView+    | ExprMap !ExprView+    | ExprRef !MappedKey+    deriving stock (Eq, Show)++declView :: ResolvedMappedDecl -> DeclView+declView =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \declaration shape -> StructuralView declaration (shapeView shape)+            , onOpaqueDecl = OpaqueView+            }++shapeView :: ResolvedMappedShape -> ShapeView+shapeView =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = RecordView+            , onEnum = EnumView+            , onUnion = UnionView+            }++exprView :: ResolvedTypeExpr -> ExprView+exprView =+    foldTypeExpr+        TypeExprAlgebra+            { onText = ExprText+            , onInt = ExprInt+            , onBool = ExprBool+            , onNatural = ExprNatural+            , onTime = ExprTime+            , onJson = ExprJson+            , onOptional = ExprOptional+            , onList = ExprList+            , onMap = ExprMap+            , onRef = ExprRef+            }++diffDeclaration :: TypeGraph -> TypeGraph -> ResolvedMappedDecl -> ResolvedMappedDecl -> [MappedFinding]+diffDeclaration oldGraph newGraph oldResolved newResolved =+    case (declView oldResolved, declView newResolved) of+        (StructuralView oldDeclaration oldShape, StructuralView newDeclaration newShape) ->+            metadataDiff paths oldDeclaration newDeclaration+                ++ diffShape paths name oldShape newShape+        (OpaqueView oldDeclaration, OpaqueView newDeclaration) ->+            opaqueMetadataDiff paths oldDeclaration newDeclaration+        _ ->+            [ finding+                paths+                name+                ""+                MappedModeCrossed+                "mapped declaration crossed the structural/opaque boundary; no structural proof can establish codec parity"+            ]+  where+    name = resolvedName newResolved+    paths = pathsFor oldGraph newGraph name++metadataDiff :: [UsePath] -> StructuralDecl -> StructuralDecl -> [MappedFinding]+metadataDiff paths oldDeclaration newDeclaration =+    [ finding+        paths+        name+        "haskell"+        MappedHaskellSourceChanged+        "consumer package, module, or type changed without changing declared wire identity; recompile every affected consumer"+    | sdHaskell oldDeclaration /= sdHaskell newDeclaration+    ]+        ++ [ finding+                paths+                name+                "binding"+                MappedBindingChanged+                "binding symbol or binding-version changed; diff cannot inspect binding behavior, so run the two-law, codec, and historical-fixture conformance suite"+           | (sdBinding oldDeclaration, sdBindingVersion oldDeclaration)+                /= (sdBinding newDeclaration, sdBindingVersion newDeclaration)+           ]+        ++ [ finding+                paths+                name+                "fixtures"+                MappedFixturesChanged+                "fixture evidence symbol changed; runtime wire policy is unchanged, but the complete conformance suite must run"+           | sdFixtures oldDeclaration /= sdFixtures newDeclaration+           ]+        ++ [ finding+                paths+                name+                "initial"+                MappedInitialChanged+                "mapped initial symbol changed; new streams and snapshot fingerprints may change while historical event decoding does not"+           | sdInitial oldDeclaration /= sdInitial newDeclaration+           ]+        ++ [ finding+                paths+                name+                "canonical-type"+                MappedCanonicalTypeChanged+                "canonical type identity changed; rebuild generated projections and invalidate mapped snapshots while declared event bytes remain unchanged"+           | sdCanonical oldDeclaration /= sdCanonical newDeclaration+           ]+  where+    name = sdName newDeclaration++opaqueMetadataDiff :: [UsePath] -> OpaqueDecl -> OpaqueDecl -> [MappedFinding]+opaqueMetadataDiff paths oldDeclaration newDeclaration =+    [ finding+        paths+        name+        "haskell"+        MappedHaskellSourceChanged+        "consumer package, module, or type changed without changing the opaque codec claim; recompile every affected consumer"+    | odHaskell oldDeclaration /= odHaskell newDeclaration+    ]+        ++ [ finding+                paths+                name+                "codec"+                MappedOpaqueCodecChanged+                "opaque codec identity or version changed; Keiro cannot inspect the codec and historical payload compatibility is unproven"+           | (odCodecIdentity oldDeclaration, odCodecVersion oldDeclaration)+                /= (odCodecIdentity newDeclaration, odCodecVersion newDeclaration)+           ]+        ++ [ finding+                paths+                name+                "fixtures"+                MappedFixturesChanged+                "fixture evidence symbol changed; runtime codec identity is unchanged, but the complete conformance suite must run"+           | odFixtures oldDeclaration /= odFixtures newDeclaration+           ]+        ++ [ finding+                paths+                name+                "initial"+                MappedInitialChanged+                "mapped initial symbol changed; new streams and snapshot fingerprints may change while historical event decoding does not"+           | odInitial oldDeclaration /= odInitial newDeclaration+           ]+  where+    name = odName newDeclaration++diffShape :: [UsePath] -> Name -> ShapeView -> ShapeView -> [MappedFinding]+diffShape paths declaration oldShape newShape = case (oldShape, newShape) of+    (RecordView oldConstructor oldUnknown oldFields, RecordView newConstructor newUnknown newFields) ->+        [ finding+            paths+            declaration+            "constructor"+            MappedRecordConstructorChanged+            "record constructor changed without changing the JSON wire identity; recompile affected consumers"+        | oldConstructor /= newConstructor+        ]+            ++ [ finding+                    paths+                    declaration+                    "unknown-fields"+                    MappedUnionEncodingChanged+                    "record unknown-fields policy changed; historical and mixed-version decoding posture is no longer the same"+               | oldUnknown /= newUnknown+               ]+            ++ diffRecord paths declaration oldUnknown oldFields newFields+    (EnumView oldEntries, EnumView newEntries) -> diffEnum paths declaration oldEntries newEntries+    (UnionView oldEncoding oldArms, UnionView newEncoding newArms) ->+        [ finding+            paths+            declaration+            "encoding"+            MappedUnionEncodingChanged+            "tagged-object encoding changed; version and upcast every affected private event root"+        | oldEncoding /= newEncoding+        ]+            ++ diffUnion paths declaration oldArms newArms+    _ ->+        [ finding+            paths+            declaration+            "shape"+            MappedUnionEncodingChanged+            "structural shape kind changed; version and upcast every affected private event root"+        ]++diffRecord :: [UsePath] -> Name -> UnknownFields -> [ResolvedWireField] -> [ResolvedWireField] -> [MappedFinding]+diffRecord paths declaration oldUnknown oldFields newFields =+    concatMap (uncurry (diffField paths declaration)) matched+        ++ map addedFinding added+        ++ map removedFinding removed+  where+    (matched, added, removed) = pairFields oldFields newFields+    addedFinding field =+        (findingWithUnknown paths declaration (fieldLeaf field) code detail (Just oldUnknown))+      where+        hasDefault = isJustValue (rwfOnMissing field)+        code+            | hasDefault = MappedFieldAddedWithDefault+            | otherwise = MappedFieldAddedNoDefault+        oldPolicy = case oldUnknown of RejectUnknown -> "reject"; IgnoreUnknown -> "ignore"+        detail+            | hasDefault =+                "field added with an explicit on-missing default; new readers preserve old meaning, while old readers use unknown-fields="+                    <> oldPolicy+            | otherwise =+                "field added without an on-missing default; old payloads do not contain it, so version and upcast every affected private event root"+    removedFinding field =+        finding+            paths+            declaration+            (fieldLeaf field)+            MappedFieldRemoved+            "field removed; replay-relevant removal remains breaking even when a tolerant decoder would ignore the historical key"++pairFields :: [ResolvedWireField] -> [ResolvedWireField] -> ([(ResolvedWireField, ResolvedWireField)], [ResolvedWireField], [ResolvedWireField])+pairFields oldFields newFields = (exact <> fallback, added, removed)+  where+    exact =+        [ (oldField, newField)+        | newField <- newFields+        , oldField <- maybeToList (find ((== rwfHaskell newField) . rwfHaskell) oldFields)+        ]+    matchedOld = map (rwfHaskell . fst) exact+    matchedNew = map (rwfHaskell . snd) exact+    unmatchedOld = [field | field <- oldFields, rwfHaskell field `notElem` matchedOld]+    unmatchedNew = [field | field <- newFields, rwfHaskell field `notElem` matchedNew]+    fallback =+        [ (oldField, newField)+        | newField <- unmatchedNew+        , oldField <- maybeToList (find ((== rwfKey newField) . rwfKey) unmatchedOld)+        ]+    fallbackOld = map (rwfHaskell . fst) fallback+    fallbackNew = map (rwfHaskell . snd) fallback+    removed = [field | field <- unmatchedOld, rwfHaskell field `notElem` fallbackOld]+    added = [field | field <- unmatchedNew, rwfHaskell field `notElem` fallbackNew]++diffField :: [UsePath] -> Name -> ResolvedWireField -> ResolvedWireField -> [MappedFinding]+diffField paths declaration oldField newField =+    [ finding+        paths+        declaration+        leaf+        MappedWireKeyChanged+        ("wire key changed '" <> rwfKey oldField <> "' -> '" <> rwfKey newField <> "'; version and upcast every affected private event root")+    | rwfKey oldField /= rwfKey newField+    ]+        ++ [ finding+                paths+                declaration+                leaf+                MappedPresenceChanged+                "field presence changed between required and optional; historical decode policy changed"+           | rwfPresence oldField /= rwfPresence newField+           ]+        ++ defaultChanges+        ++ diffExpr paths declaration (leaf <> ".type") (rwfType oldField) (rwfType newField)+  where+    leaf = fieldLeaf newField+    defaultChanges = case (rwfOnMissing oldField, rwfOnMissing newField) of+        (Just _, Nothing) ->+            [ finding+                paths+                declaration+                leaf+                MappedDefaultRemoved+                "on-missing default was removed; old payloads may no longer decode with preserved meaning"+            ]+        (oldDefault, newDefault)+            | oldDefault /= newDefault ->+                [ finding+                    paths+                    declaration+                    leaf+                    MappedDefaultChanged+                    "on-missing default changed; the same historical bytes now construct a different consumer value"+                ]+        _ -> []++diffExpr :: [UsePath] -> Name -> Text -> ResolvedTypeExpr -> ResolvedTypeExpr -> [MappedFinding]+diffExpr paths declaration leaf oldExpression newExpression =+    case (exprView oldExpression, exprView newExpression) of+        (oldView, newView)+            | oldView == newView -> []+        (ExprOptional oldValue, ExprOptional newValue) -> recurse ".optional" oldValue newValue+        (ExprList oldValue, ExprList newValue) -> recurse "[]" oldValue newValue+        (ExprMap oldValue, ExprMap newValue) -> recurse "{}" oldValue newValue+        (ExprOptional _, _) -> nullability+        (_, ExprOptional _) -> nullability+        _ ->+            [ finding+                paths+                declaration+                leaf+                MappedFieldTypeChanged+                "wire type changed; version and upcast every affected private event root"+            ]+  where+    recurse suffix oldView newView = diffExprViews paths declaration (leaf <> suffix) oldView newView+    nullability =+        [ finding+            paths+            declaration+            leaf+            MappedNullabilityChanged+            "Optional nullability changed; historical null and non-null meanings are no longer stable"+        ]++diffExprViews :: [UsePath] -> Name -> Text -> ExprView -> ExprView -> [MappedFinding]+diffExprViews paths declaration leaf oldView newView = case (oldView, newView) of+    _ | oldView == newView -> []+    (ExprOptional oldValue, ExprOptional newValue) -> diffExprViews paths declaration (leaf <> ".optional") oldValue newValue+    (ExprList oldValue, ExprList newValue) -> diffExprViews paths declaration (leaf <> "[]") oldValue newValue+    (ExprMap oldValue, ExprMap newValue) -> diffExprViews paths declaration (leaf <> "{}") oldValue newValue+    (ExprOptional _, _) -> nullability+    (_, ExprOptional _) -> nullability+    _ -> [finding paths declaration leaf MappedFieldTypeChanged "wire type changed; version and upcast every affected private event root"]+  where+    nullability = [finding paths declaration leaf MappedNullabilityChanged "Optional nullability changed; historical null and non-null meanings are no longer stable"]++diffEnum :: [UsePath] -> Name -> [WireEnum] -> [WireEnum] -> [MappedFinding]+diffEnum paths declaration oldEntries newEntries =+    [ finding+        paths+        declaration+        (enumLeaf newEntry)+        MappedEnumSpellingChanged+        ("enum wire spelling changed '" <> weTag oldEntry <> "' -> '" <> weTag newEntry <> "'")+    | newEntry <- newEntries+    , oldEntry <- maybeToList (find ((== weCtor newEntry) . weCtor) oldEntries)+    , weTag oldEntry /= weTag newEntry+    ]+        ++ [ finding+                paths+                declaration+                (enumLeaf entry)+                MappedEnumValueAdded+                "enum value added; existing history remains readable, but deploy readers before writers emit the new spelling; a future public surface exposing this closed enum would classify the addition as consumer-breaking"+           | entry <- newEntries+           , isNothing (find ((== weCtor entry) . weCtor) oldEntries)+           ]+        ++ [ finding+                paths+                declaration+                (enumLeaf entry)+                MappedEnumValueRemoved+                "enum value removed; historical payloads carrying its wire spelling no longer decode"+           | entry <- oldEntries+           , isNothing (find ((== weCtor entry) . weCtor) newEntries)+           ]++diffUnion :: [UsePath] -> Name -> [ResolvedWireArm] -> [ResolvedWireArm] -> [MappedFinding]+diffUnion paths declaration oldArms newArms =+    concatMap (uncurry pairedArm) matched+        ++ map addedArm added+        ++ map removedArm removed+  where+    (matched, added, removed) = pairArms oldArms newArms+    pairedArm oldArm newArm =+        [ finding+            paths+            declaration+            (armLeaf newArm)+            MappedArmTagChanged+            ("union arm tag changed '" <> rwaTag oldArm <> "' -> '" <> rwaTag newArm <> "'")+        | rwaTag oldArm /= rwaTag newArm+        ]+            ++ case (rwaPayload oldArm, rwaPayload newArm) of+                (Nothing, Nothing) -> []+                (Just oldPayload, Just newPayload) -> diffExpr paths declaration (armLeaf newArm <> ".payload") oldPayload newPayload+                _ -> [finding paths declaration (armLeaf newArm) MappedFieldTypeChanged "union arm payload presence changed; historical tagged objects no longer share one wire shape"]+    addedArm arm = finding paths declaration (armLeaf arm) MappedArmAdded "union arm added; existing history remains readable, but older binaries cannot read the new arm once emitted, so deploy readers before writers; a future public surface exposing this closed union would classify the addition as consumer-breaking"+    removedArm arm = finding paths declaration (armLeaf arm) MappedArmRemoved "union arm removed; historical tagged objects carrying that tag no longer decode"++pairArms :: [ResolvedWireArm] -> [ResolvedWireArm] -> ([(ResolvedWireArm, ResolvedWireArm)], [ResolvedWireArm], [ResolvedWireArm])+pairArms oldArms newArms = (exact <> fallback, added, removed)+  where+    exact =+        [ (oldArm, newArm)+        | newArm <- newArms+        , oldArm <- maybeToList (find ((== rwaCtor newArm) . rwaCtor) oldArms)+        ]+    matchedOld = map (rwaCtor . fst) exact+    matchedNew = map (rwaCtor . snd) exact+    unmatchedOld = [arm | arm <- oldArms, rwaCtor arm `notElem` matchedOld]+    unmatchedNew = [arm | arm <- newArms, rwaCtor arm `notElem` matchedNew]+    fallback =+        [ (oldArm, newArm)+        | newArm <- unmatchedNew+        , oldArm <- maybeToList (find ((== rwaTag newArm) . rwaTag) unmatchedOld)+        ]+    fallbackOld = map (rwaCtor . fst) fallback+    fallbackNew = map (rwaCtor . snd) fallback+    removed = [arm | arm <- unmatchedOld, rwaCtor arm `notElem` fallbackOld]+    added = [arm | arm <- unmatchedNew, rwaCtor arm `notElem` fallbackNew]++addedDeclaration :: TypeGraph -> (MappedKey, ResolvedMappedDecl) -> MappedFinding+addedDeclaration _ (key, _) =+    finding+        []+        (unMappedKey key)+        ""+        MappedDeclAdded+        "new mapped declaration; use-site changes retain their own compatibility classification"++removedDeclaration :: TypeGraph -> (MappedKey, ResolvedMappedDecl) -> MappedFinding+removedDeclaration graph (key, _) =+    finding+        (usePaths graph (unMappedKey key))+        (unMappedKey key)+        ""+        MappedDeclRemoved+        "mapped declaration removed; persisted roots using its historical decoder require migration, while an unused source-only declaration requires consumer rebuild only"++pathsFor :: TypeGraph -> TypeGraph -> Name -> [UsePath]+pathsFor oldGraph newGraph name =+    nubBy sameRendered . sortOn renderUsePath $ usePaths oldGraph name <> usePaths newGraph name+  where+    sameRendered left right = renderUsePath left == renderUsePath right++resolvedName :: ResolvedMappedDecl -> Name+resolvedName =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \declaration _ -> sdName declaration+            , onOpaqueDecl = odName+            }++fieldLeaf :: ResolvedWireField -> Text+fieldLeaf field = ".field " <> rwfHaskell field <> "[\"" <> rwfKey field <> "\"]"++armLeaf :: ResolvedWireArm -> Text+armLeaf arm = ".arm " <> rwaCtor arm <> "[\"" <> rwaTag arm <> "\"]"++enumLeaf :: WireEnum -> Text+enumLeaf entry = ".enum " <> weCtor entry <> "[\"" <> weTag entry <> "\"]"++finding :: [UsePath] -> Name -> Text -> DiagnosticCode -> Text -> MappedFinding+finding paths declaration leaf code detail =+    findingWithUnknown paths declaration leaf code detail Nothing++findingWithUnknown :: [UsePath] -> Name -> Text -> DiagnosticCode -> Text -> Maybe UnknownFields -> MappedFinding+findingWithUnknown paths declaration leaf code detail unknownFields =+    MappedFinding+        { mfDeclaration = declaration+        , mfLeaf = leaf+        , mfCode = code+        , mfDetail = detail+        , mfUsePaths = paths+        , mfOldUnknownFields = unknownFields+        }++isJustValue :: Maybe a -> Bool+isJustValue = not . isNothing++maybeToList :: Maybe a -> [a]+maybeToList = maybe [] pure
src/Keiro/Dsl/Parser.hs view
@@ -14,6 +14,7 @@  import Control.Monad.Combinators.Expr (Operator (..), makeExprParser) import Data.Char (isAlpha, isAlphaNum, isAscii, isDigit, isUpper)+import Data.Maybe (mapMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Void (Void)@@ -118,6 +119,7 @@     , "id"     , "enum"     , "rule"+    , "mapped"     , "ex"     , "aggregate"     , "regs"@@ -136,6 +138,7 @@     , "status-map"     , "true"     , "false"+    , "retiring"     , "deprecated"     , "upcast"     , "from"@@ -232,6 +235,7 @@     = TIId IdDecl     | TIEnum EnumDecl     | TIRule RuleDecl+    | TIMapped MappedDecl     | TINode Node  pSpec :: P Spec@@ -249,6 +253,7 @@             , specIds = [d | TIId d <- items]             , specEnums = [d | TIEnum d <- items]             , specRules = [d | TIRule d <- items]+            , specMapped = [d | TIMapped d <- items]             , specNodes = [n | TINode n <- items]             } @@ -285,6 +290,7 @@         [ TIId <$> pIdDecl         , TIEnum <$> pEnumDecl         , TIRule <$> pRuleDecl+        , TIMapped <$> pMappedDecl         , TINode . NRouter <$> pRouter         , TINode . NProcess <$> pProcess         , TINode . NContract <$> pContract@@ -350,6 +356,243 @@         pure (c, e)  --------------------------------------------------------------------------------+-- Consumer-owned mapped types (EP-149)+--------------------------------------------------------------------------------++data MappedKind = MappedRecord | MappedEnum | MappedUnion++data MappedClause+    = MCHaskell HaskellSource+    | MCBinding Text+    | MCBindingVersion Text+    | MCCanonical Text+    | MCFixtures Text+    | MCInitial Text+    | MCCodec Text+    | MCCodecVersion Text+    | MCShape MappedShape++pMappedDecl :: P MappedDecl+pMappedDecl = do+    loc <- getLoc+    keyword "mapped"+    choice [pStructural loc, pOpaque loc]+  where+    pStructural loc = do+        keyword "structural"+        kind <-+            choice+                [ MappedRecord <$ keyword "record"+                , MappedEnum <$ keyword "enum"+                , MappedUnion <$ keyword "union"+                ]+        name <- ident+        clauses <- braces (many (pStructuralClause kind))+        hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses+        binding <- oneClause "binding" (\case MCBinding value -> Just value; _ -> Nothing) clauses+        bindingVersion <- oneClause "binding-version" (\case MCBindingVersion value -> Just value; _ -> Nothing) clauses+        canonical <- oneClause "canonical-type" (\case MCCanonical value -> Just value; _ -> Nothing) clauses+        fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses+        initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses+        shape <- requiredClause "wire" (\case MCShape value -> Just value; _ -> Nothing) clauses+        pure+            MappedStructural+                { msName = name+                , msHaskell = hs+                , msBinding = binding+                , msBindingVersion = bindingVersion+                , msCanonical = canonical+                , msFixtures = fixtures+                , msInitial = initial+                , msShape = shape+                , msLoc = loc+                }++    pOpaque loc = do+        keyword "opaque"+        name <- ident+        clauses <- braces (many pOpaqueClause)+        hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses+        codec <- oneClause "codec" (\case MCCodec value -> Just value; _ -> Nothing) clauses+        version <- oneClause "version" (\case MCCodecVersion value -> Just value; _ -> Nothing) clauses+        fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses+        initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses+        pure+            MappedOpaque+                { moName = name+                , moHaskell = hs+                , moCodecId = codec+                , moCodecVersion = version+                , moFixtures = fixtures+                , moInitial = initial+                , moLoc = loc+                }++pStructuralClause :: MappedKind -> P MappedClause+pStructuralClause kind =+    choice+        [ MCHaskell <$> pHaskellSource+        , MCBindingVersion <$> pQuotedFact "binding-version"+        , MCBinding <$> pQuotedFact "binding"+        , MCCanonical <$> pQuotedFact "canonical-type"+        , MCFixtures <$> pQuotedFact "fixtures"+        , MCInitial <$> pQuotedFact "initial"+        , MCShape <$> pMappedShape kind+        ]++pOpaqueClause :: P MappedClause+pOpaqueClause =+    choice+        [ MCHaskell <$> pHaskellSource+        , MCCodec <$> pQuotedFact "codec"+        , MCCodecVersion <$> pQuotedFact "version"+        , MCFixtures <$> pQuotedFact "fixtures"+        , MCInitial <$> pQuotedFact "initial"+        ]++pHaskellSource :: P HaskellSource+pHaskellSource = do+    keyword "haskell"+    keyword "package"+    _ <- symbol "="+    packageName <- wireWord+    keyword "module"+    _ <- symbol "="+    moduleName <- pModulePrefix+    keyword "type"+    _ <- symbol "="+    typeName <- ident+    pure HaskellSource{hsPackage = packageName, hsModule = moduleName, hsType = typeName}++pQuotedFact :: Text -> P Text+pQuotedFact factName = keyword factName *> symbol "=" *> stringLit++pMappedShape :: MappedKind -> P MappedShape+pMappedShape kind = do+    keyword "wire"+    case kind of+        MappedRecord -> do+            keyword "object"+            keyword "constructor"+            _ <- symbol "="+            constructor <- ident+            unknownFields <- pUnknownFieldsFact+            fields <- braces (many pWireField)+            pure (ShapeRecord constructor unknownFields fields)+        MappedEnum -> do+            keyword "string"+            ShapeEnum <$> braces (many pWireEnum)+        MappedUnion -> do+            keyword "tagged-object"+            keyword "tag"+            _ <- symbol "="+            tagField <- stringLit+            keyword "contents"+            _ <- symbol "="+            contentsField <- stringLit+            unknownFields <- pUnknownFieldsFact+            arms <- braces (many pWireArm)+            pure (ShapeUnion (TaggedObject tagField contentsField unknownFields) arms)++pUnknownFieldsFact :: P UnknownFields+pUnknownFieldsFact = do+    keyword "unknown-fields"+    _ <- symbol "="+    choice [RejectUnknown <$ keyword "reject", IgnoreUnknown <$ keyword "ignore"]++pWireField :: P WireField+pWireField = do+    loc <- getLoc+    haskellName <- ident+    keyword "as"+    wireKey <- stringLit+    _ <- symbol ":"+    fieldType <- pMappedTypeExpr+    presence <- choice [PRequired <$ keyword "required", POptional <$ keyword "optional"]+    onMissing <- optional (keyword "on-missing" *> symbol "=" *> pOnMissing)+    pure+        WireField+            { wfHaskell = haskellName+            , wfKey = wireKey+            , wfType = fieldType+            , wfPresence = presence+            , wfOnMissing = onMissing+            , wfLoc = loc+            }++pWireEnum :: P WireEnum+pWireEnum = do+    loc <- getLoc+    constructor <- ident+    keyword "as"+    wireTag <- stringLit+    pure WireEnum{weCtor = constructor, weTag = wireTag, weLoc = loc}++pWireArm :: P WireArm+pWireArm = do+    loc <- getLoc+    constructor <- ident+    keyword "as"+    wireTag <- stringLit+    payload <- optional (symbol ":" *> pMappedTypeExpr)+    pure WireArm{waCtor = constructor, waTag = wireTag, waPayload = payload, waLoc = loc}++pMappedTypeExpr :: P TypeExpr+pMappedTypeExpr =+    choice+        [ TOptional <$> (keyword "Optional" *> pTypeArgument)+        , TList <$> (keyword "List" *> pTypeArgument)+        , TMap <$> (keyword "Map" *> pTypeArgument)+        , TText <$ keyword "Text"+        , TInt <$ keyword "Int"+        , TBool <$ keyword "Bool"+        , TNatural <$ keyword "Natural"+        , TTime <$ (keyword "Time" <|> keyword "UTCTime")+        , TJson <$ keyword "Json"+        , TRef <$> ident+        ]+  where+    pTypeArgument = parens pMappedTypeExpr <|> pTypeAtom+    pTypeAtom =+        choice+            [ TText <$ keyword "Text"+            , TInt <$ keyword "Int"+            , TBool <$ keyword "Bool"+            , TNatural <$ keyword "Natural"+            , TTime <$ (keyword "Time" <|> keyword "UTCTime")+            , TJson <$ keyword "Json"+            , TRef <$> ident+            ]++pOnMissing :: P OnMissing+pOnMissing =+    choice+        [ OmNull <$ keyword "null"+        , OmEmptyList <$ (symbol "[" *> symbol "]")+        , OmEmptyMap <$ (symbol "{" *> symbol "}")+        , OmBool True <$ keyword "true"+        , OmBool False <$ keyword "false"+        , OmText <$> stringLit+        , OmInt <$> integerLiteral+        , OmCtor <$> ident+        ]++integerLiteral :: P Integer+integerLiteral = lexeme (L.signed (pure ()) L.decimal)++oneClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P (Maybe a)+oneClause clauseName select clauses =+    case mapMaybe select clauses of+        [] -> pure Nothing+        [value] -> pure (Just value)+        _ -> fail ("duplicate " <> clauseName <> " clause in mapped declaration")++requiredClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P a+requiredClause clauseName select clauses = do+    found <- oneClause clauseName select clauses+    maybe (fail ("missing " <> clauseName <> " clause in mapped structural declaration")) pure found++-------------------------------------------------------------------------------- -- Aggregate node -------------------------------------------------------------------------------- @@ -428,6 +671,10 @@     -- 'pTransition'.     pStateDecl = try $ do         loc <- getLoc+        -- A @replay-only@ transition marker directly after the states line+        -- must not be swallowed: 'ident' would take @replay@ (hyphens are+        -- not identifier characters) and strand @-only@.+        notFollowedBy (keyword "replay-only")         n <- ident         term <- option False (True <$ symbol "!")         notFollowedBy (symbol "--")@@ -477,7 +724,14 @@ pEvent :: P Event pEvent = do     loc <- getLoc-    dep <- option False (True <$ keyword "deprecated")+    (retiring, deprecated) <-+        option+            (False, False)+            ( choice+                [ (True, False) <$ keyword "retiring"+                , (False, True) <$ keyword "deprecated"+                ]+            )     keyword "event"     name <- ident     ver <- option 1 pVersion@@ -493,7 +747,8 @@             , evBody = body             , evVersion = ver             , evUpcastFrom = up-            , evDeprecated = dep+            , evRetiring = retiring+            , evDeprecated = deprecated             , evLoc = loc             }   where@@ -1458,6 +1713,9 @@ pTransition = do     startOffset <- getOffset     loc <- getLoc+    -- Plan 143: a @replay-only@ prefix marks the transition as serving+    -- inversion only; it lowers to a keiki 'ReplayOnly' edge.+    mode <- option TmLive (TmReplayOnly <$ keyword "replay-only")     src <- ident     _ <- symbol "--"     cmd <- ident@@ -1482,6 +1740,7 @@             , tWrites = [(r, e) | CWrite r e <- clauses]             , tEmits = [n | CEmit n <- clauses]             , tGoto = gt+            , tMode = mode             , tLoc = loc             } 
src/Keiro/Dsl/PrettyPrint.hs view
@@ -8,6 +8,12 @@ -} module Keiro.Dsl.PrettyPrint (     renderSpec,+    renderTransition,+    renderExpr,+    renderHandleSurface,+    renderResolveSurface,+    renderRouterDispatchSurface,+    renderTimerPayloadSurface, ) where @@ -19,10 +25,24 @@  -- | Render a whole spec to text. renderSpec :: Spec -> Text-renderSpec = renderStrict . layoutPretty opts . docSpec-  where-    opts = LayoutOptions{layoutPageWidth = Unbounded}+renderSpec = renderDoc . docSpec +renderHandleSurface :: HandleNode -> Text+renderHandleSurface = renderDoc . docHandle++renderResolveSurface :: ResolveDecl -> Text+renderResolveSurface = renderDoc . docResolve++renderRouterDispatchSurface :: RouterDispatchNode -> Text+renderRouterDispatchSurface = renderDoc . docRouterDispatch++renderTimerPayloadSurface :: TimerNode -> Text+renderTimerPayloadSurface timer =+    renderDoc ("payload" <+> braced (map docFieldBinding (tmPayload timer)))++renderDoc :: Doc ann -> Text+renderDoc = renderStrict . layoutPretty LayoutOptions{layoutPageWidth = Unbounded}+ docSpec :: Spec -> Doc ann docSpec s =     vsep $@@ -36,6 +56,8 @@             ++ blankAfter (specEnums s)             ++ map docRule (specRules s)             ++ blankAfter (specRules s)+            ++ map docMapped (specMapped s)+            ++ blankAfter (specMapped s)             ++ map docNode (specNodes s)   where     blankAfter xs = if null xs then [] else [mempty]@@ -62,6 +84,121 @@   where     cas (c, e) = pretty c <+> "=>" <+> docExpr 0 e +docMapped :: MappedDecl -> Doc ann+docMapped MappedStructural{msName = name, msHaskell = haskell, msBinding = binding, msBindingVersion = bindingVersion, msCanonical = canonical, msFixtures = fixtures, msInitial = initial, msShape = shape} =+    vsep $+        ["mapped structural" <+> docShapeKind shape <+> pretty name <+> "{"]+            ++ maybe [] (pure . indent 2 . docHaskellSource) haskell+            ++ maybe [] (pure . indent 2 . docQuotedFact "binding") binding+            ++ maybe [] (pure . indent 2 . docQuotedFact "binding-version") bindingVersion+            ++ maybe [] (pure . indent 2 . docQuotedFact "canonical-type") canonical+            ++ maybe [] (pure . indent 2 . docQuotedFact "fixtures") fixtures+            ++ maybe [] (pure . indent 2 . docQuotedFact "initial") initial+            ++ [indent 2 (docMappedShape shape), "}"]+docMapped MappedOpaque{moName = name, moHaskell = haskell, moCodecId = codec, moCodecVersion = version, moFixtures = fixtures, moInitial = initial} =+    vsep $+        ["mapped opaque" <+> pretty name <+> "{"]+            ++ maybe [] (pure . indent 2 . docHaskellSource) haskell+            ++ maybe [] (pure . indent 2 . docQuotedFact "codec") codec+            ++ maybe [] (pure . indent 2 . docQuotedFact "version") version+            ++ maybe [] (pure . indent 2 . docQuotedFact "fixtures") fixtures+            ++ maybe [] (pure . indent 2 . docQuotedFact "initial") initial+            ++ ["}"]++docShapeKind :: MappedShape -> Doc ann+docShapeKind (ShapeRecord _ _ _) = "record"+docShapeKind (ShapeEnum _) = "enum"+docShapeKind (ShapeUnion _ _) = "union"++docHaskellSource :: HaskellSource -> Doc ann+docHaskellSource source =+    "haskell"+        <+> ("package=" <> pretty (hsPackage source))+        <+> ("module=" <> pretty (hsModule source))+        <+> ("type=" <> pretty (hsType source))++docQuotedFact :: Doc ann -> Text -> Doc ann+docQuotedFact label value = label <+> "=" <+> dquoted value++docMappedShape :: MappedShape -> Doc ann+docMappedShape (ShapeRecord constructor unknownFields fields) =+    vsep $+        [ "wire object"+            <+> ("constructor=" <> pretty constructor)+            <+> ("unknown-fields=" <> docUnknownFields unknownFields)+            <+> "{"+        ]+            ++ map (indent 2 . docWireField) fields+            ++ ["}"]+docMappedShape (ShapeEnum entries) =+    vsep $ ["wire string {"] ++ map (indent 2 . docWireEnum) entries ++ ["}"]+docMappedShape (ShapeUnion encoding arms) =+    vsep $+        [ "wire tagged-object"+            <+> ("tag=" <> dquoted (ueTagField encoding))+            <+> ("contents=" <> dquoted (ueContentsField encoding))+            <+> ("unknown-fields=" <> docUnknownFields (ueUnknownFields encoding))+            <+> "{"+        ]+            ++ map (indent 2 . docWireArm) arms+            ++ ["}"]++docUnknownFields :: UnknownFields -> Doc ann+docUnknownFields RejectUnknown = "reject"+docUnknownFields IgnoreUnknown = "ignore"++docWireField :: WireField -> Doc ann+docWireField field =+    pretty (wfHaskell field)+        <+> "as"+        <+> dquoted (wfKey field)+        <+> ":"+        <+> docTypeExpr (wfType field)+        <+> docPresence (wfPresence field)+        <> maybe mempty (\value -> " on-missing=" <> docOnMissing value) (wfOnMissing field)++docPresence :: Presence -> Doc ann+docPresence PRequired = "required"+docPresence POptional = "optional"++docOnMissing :: OnMissing -> Doc ann+docOnMissing OmNull = "null"+docOnMissing (OmText value) = dquoted value+docOnMissing (OmInt value) = pretty value+docOnMissing (OmBool True) = "true"+docOnMissing (OmBool False) = "false"+docOnMissing OmEmptyList = "[]"+docOnMissing OmEmptyMap = "{}"+docOnMissing (OmCtor constructor) = pretty constructor++docWireEnum :: WireEnum -> Doc ann+docWireEnum entry = pretty (weCtor entry) <+> "as" <+> dquoted (weTag entry)++docWireArm :: WireArm -> Doc ann+docWireArm arm =+    pretty (waCtor arm)+        <+> "as"+        <+> dquoted (waTag arm)+        <> maybe mempty (\payload -> " : " <> docTypeExpr payload) (waPayload arm)++docTypeExpr :: TypeExpr -> Doc ann+docTypeExpr TText = "Text"+docTypeExpr TInt = "Int"+docTypeExpr TBool = "Bool"+docTypeExpr TNatural = "Natural"+docTypeExpr TTime = "Time"+docTypeExpr TJson = "Json"+docTypeExpr (TOptional value) = "Optional" <+> docTypeArgument value+docTypeExpr (TList value) = "List" <+> docTypeArgument value+docTypeExpr (TMap value) = "Map" <+> docTypeArgument value+docTypeExpr (TRef name) = pretty name++docTypeArgument :: TypeExpr -> Doc ann+docTypeArgument value@TOptional{} = parens (docTypeExpr value)+docTypeArgument value@TList{} = parens (docTypeExpr value)+docTypeArgument value@TMap{} = parens (docTypeExpr value)+docTypeArgument value = docTypeExpr value+ docNode :: Node -> Doc ann docNode (NAggregate a) = docAggregate a docNode (NProcess p) = docProcess p@@ -549,7 +686,11 @@         Nothing -> line1         Just (m, _) -> vsep [line1, indent 2 ("upcast from v" <> pretty m <+> "=" <+> "HOLE")]   where-    kw = if evDeprecated e then "deprecated event" else "event"+    kw = case (evRetiring e, evDeprecated e) of+        (False, False) -> "event"+        (True, False) -> "retiring event"+        (False, True) -> "deprecated event"+        (True, True) -> "retiring deprecated event"     nameVer =         pretty (evName e)             <> (if evVersion e > 1 then " v" <> pretty (evVersion e) else mempty)@@ -558,12 +699,32 @@         EventFields fs -> braced (map docField fs)     line1 = kw <+> nameVer <+> bodyDoc +{- | Render one transition in concrete @.keiro@ syntax. Exported for @diff@'s+guard-tightening advisory, which prints a paste-ready replay-only twin+(plan 143).+-}+renderTransition :: Transition -> Text+renderTransition =+    renderStrict+        . layoutPretty LayoutOptions{layoutPageWidth = Unbounded}+        . docTransition++-- | Render one expression in canonical concrete syntax.+renderExpr :: Expr -> Text+renderExpr =+    renderStrict+        . layoutPretty LayoutOptions{layoutPageWidth = Unbounded}+        . docExpr 0+ docTransition :: Transition -> Doc ann docTransition t =     vsep $-        [pretty (tSource t) <+> "--" <+> pretty (tCommand t) <+> "-->"]+        [modePrefix <> pretty (tSource t) <+> "--" <+> pretty (tCommand t) <+> "-->"]             ++ map (indent 2) clauses   where+    modePrefix = case tMode t of+        TmLive -> mempty+        TmReplayOnly -> "replay-only "     clauses =         maybe [] (\g -> ["guard" <+> docExpr 0 g]) (tGuard t)             ++ map (\(r, e) -> "write" <+> pretty r <+> ":=" <+> docExpr 0 e) (tWrites t)
src/Keiro/Dsl/ReadModelShape.hs view
@@ -5,6 +5,7 @@ module Keiro.Dsl.ReadModelShape (     canonicalShape,     deriveShapeHash,+    fnv1a64,     registryNameFor,     subscriptionNameFor, ) where@@ -33,9 +34,14 @@ -- | A fixed-width FNV-1a-64 digest over the canonical shape's UTF-8 bytes. deriveShapeHash :: ReadModelNode -> Text deriveShapeHash readModel =-    "fnv1a:" <> T.justifyRight 16 '0' (T.pack (showHex digest ""))+    "fnv1a:" <> fnv1a64 (canonicalShape readModel)++-- | A fixed-width FNV-1a-64 digest over a 'Text' value's UTF-8 bytes.+fnv1a64 :: Text -> Text+fnv1a64 input =+    T.justifyRight 16 '0' (T.pack (showHex digest ""))   where-    digest = foldl' step offsetBasis (concatMap utf8Bytes (T.unpack (canonicalShape readModel)))+    digest = foldl' step offsetBasis (concatMap utf8Bytes (T.unpack input))     step hash byte = (hash `xor` byte) * fnvPrime  -- | The runtime registry identity derived from context and notation name.
+ src/Keiro/Dsl/ReplayImpact.hs view
@@ -0,0 +1,236 @@+{- | Stored-data replay impact for a specification diff.++The ordinary differ classifies compatibility across every persisted surface.+This module answers a narrower deployment question: can the candidate binary+interpret an already-stored aggregate log differently?++The result is deliberately conservative. New aggregates, events, and+transitions are replay-neutral because no old log depends on them. A removed+or changed old transition affects the event types emitted by either side, and+a decode-surface change affects that event type directly. Snapshot-bearing+streams are included whenever the fold itself can change.+-}+module Keiro.Dsl.ReplayImpact (+    AggregateImpact (..),+    ReplayImpact (..),+    replayImpact,+    renderReplayImpact,+) where++import Data.Aeson (ToJSON (..), object, (.=))+import Data.List (delete, find)+import Data.Map.Strict (Map)+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 Text+import Keiro.Dsl.FoldFingerprint (aggregateFoldSurface)+import Keiro.Dsl.Grammar+import Keiro.Dsl.PrettyPrint (renderTransition)+import Keiro.Dsl.TypeGraph (MappedKey (..), TypeGraph (..), resolveTypeGraph, wireFingerprint)++-- | The smallest conservative audit input for one aggregate.+data AggregateImpact = AggregateImpact+    { eventTypes :: !(Set Name)+    , includeSnapshotStreams :: !Bool+    }+    deriving stock (Eq, Show)++-- | A deploy either preserves replay or carries per-aggregate audit inputs.+data ReplayImpact+    = ReplayNeutral+    | ReplayAffected !(Map Name AggregateImpact)+    deriving stock (Eq, Show)++instance ToJSON AggregateImpact where+    toJSON impact =+        object+            [ "eventTypes" .= Set.toAscList (eventTypes impact)+            , "includeSnapshotStreams" .= includeSnapshotStreams impact+            ]++instance ToJSON ReplayImpact where+    toJSON ReplayNeutral = object ["verdict" .= ("replay-neutral" :: Text)]+    toJSON (ReplayAffected aggregates) =+        object+            [ "verdict" .= ("affected" :: Text)+            , "aggregates" .= aggregates+            ]++-- | Compute replay impact for every aggregate that existed in the old spec.+replayImpact :: Spec -> Spec -> ReplayImpact+replayImpact oldSpec newSpec =+    case Map.filter hasImpact impacts of+        filtered+            | Map.null filtered -> ReplayNeutral+            | otherwise -> ReplayAffected filtered+  where+    oldAggregates = [(aggName aggregate, aggregate) | NAggregate aggregate <- specNodes oldSpec]+    newAggregates = Map.fromList [(aggName aggregate, aggregate) | NAggregate aggregate <- specNodes newSpec]+    impacts =+        Map.fromList+            [ (name, maybe (removedAggregateImpact oldAggregate) (matchedAggregateImpact oldSpec newSpec oldAggregate) (Map.lookup name newAggregates))+            | (name, oldAggregate) <- oldAggregates+            ]++hasImpact :: AggregateImpact -> Bool+hasImpact impact =+    not (Set.null (eventTypes impact))+        || includeSnapshotStreams impact++removedAggregateImpact :: Aggregate -> AggregateImpact+removedAggregateImpact aggregate =+    AggregateImpact+        { eventTypes = Set.fromList (evName <$> aggEvents aggregate)+        , includeSnapshotStreams = True+        }++matchedAggregateImpact :: Spec -> Spec -> Aggregate -> Aggregate -> AggregateImpact+matchedAggregateImpact oldSpec newSpec oldAggregate newAggregate =+    AggregateImpact+        { eventTypes =+            decodeAffected+                <> transitionAffected+                <> if nonTransitionFoldChanged then oldEventTypes else Set.empty+        , includeSnapshotStreams = transitionFoldChanged || nonTransitionFoldChanged || mappedRegisterChanged+        }+  where+    oldEventTypes = Set.fromList (evName <$> aggEvents oldAggregate)+    decodeAffected = decodeSurfaceAffected oldSpec newSpec oldAggregate newAggregate+    mappedRegisterChanged =+        mappedRegisterSurface oldSpec oldAggregate+            /= mappedRegisterSurface newSpec newAggregate+    (transitionAffected, transitionFoldChanged) =+        changedTransitionEvents (aggTransitions oldAggregate) (aggTransitions newAggregate)+    nonTransitionFoldChanged =+        aggregateFoldSurface oldSpec oldAggregate+            /= aggregateFoldSurface+                newSpec+                newAggregate+                    { aggTransitions = aggTransitions oldAggregate+                    }++decodeSurfaceAffected :: Spec -> Spec -> Aggregate -> Aggregate -> Set Name+decodeSurfaceAffected oldSpec newSpec oldAggregate newAggregate =+    removedOrChanged <> wireAffected+  where+    newEvents = Map.fromList [(evName event, event) | event <- aggEvents newAggregate]+    removedOrChanged =+        Set.fromList+            [ evName oldEvent+            | oldEvent <- aggEvents oldAggregate+            , maybe True ((/= eventSurface oldSpec oldAggregate oldEvent) . eventSurface newSpec newAggregate) (Map.lookup (evName oldEvent) newEvents)+            ]+    wireAffected+        | aggWire oldAggregate == aggWire newAggregate = Set.empty+        | otherwise = Set.fromList (evName <$> aggEvents oldAggregate)++eventDecodeSurface :: Event -> (EventBody, Int, Maybe (Int, Hole))+eventDecodeSurface event =+    (evBody event, evVersion event, evUpcastFrom event)++eventSurface :: Spec -> Aggregate -> Event -> ((EventBody, Int, Maybe (Int, Hole)), [(Name, Text)])+eventSurface spec aggregate event =+    (eventDecodeSurface event, mappedFieldSurface spec aggregate event)++mappedFieldSurface :: Spec -> Aggregate -> Event -> [(Name, Text)]+mappedFieldSurface spec aggregate event = case resolveTypeGraph spec of+    Left _ -> []+    Right graph ->+        [ (fieldName field, wireFingerprint graph typeName)+        | field <- eventFields aggregate event+        , typeName <- maybeToList (fieldType field)+        , Map.member (MappedKey typeName) (tgDeclarations graph)+        ]++mappedRegisterSurface :: Spec -> Aggregate -> [(Name, Name, Text)]+mappedRegisterSurface spec aggregate = case resolveTypeGraph spec of+    Left _ -> []+    Right graph ->+        [ (regName register, regType register, wireFingerprint graph (regType register))+        | register <- aggRegs aggregate+        , Map.member (MappedKey (regType register)) (tgDeclarations graph)+        ]++eventFields :: Aggregate -> Event -> [Field]+eventFields aggregate event = case evBody event of+    EventFields fields -> fields+    EventFromCommand commandName ->+        concat [cmdFields command | command <- aggCommands aggregate, cmdName command == commandName]++maybeToList :: Maybe a -> [a]+maybeToList = maybe [] pure++changedTransitionEvents :: [Transition] -> [Transition] -> (Set Name, Bool)+changedTransitionEvents oldTransitions newTransitions =+    go oldTransitions newTransitions Set.empty False+  where+    go [] _ affected changed = (affected, changed)+    go (oldTransition : remainingOld) remainingNew affected changed =+        case find (sameSurface oldTransition) remainingNew of+            Just exact ->+                go remainingOld (delete exact remainingNew) affected changed+            Nothing ->+                case find (sameIdentity oldTransition) remainingNew of+                    Just candidate+                        | guardOnlyLoosening oldTransition candidate ->+                            go remainingOld (delete candidate remainingNew) affected changed+                        | otherwise ->+                            go+                                remainingOld+                                (delete candidate remainingNew)+                                (affected <> emittedBy oldTransition <> emittedBy candidate)+                                True+                    Nothing ->+                        go+                            remainingOld+                            remainingNew+                            (affected <> emittedBy oldTransition)+                            True++    sameSurface left right = renderTransition left == renderTransition right+    sameIdentity left right =+        tMode left == tMode right+            && tSource left == tSource right+            && tCommand left == tCommand right+    emittedBy = Set.fromList . tEmits++{- | A syntactically provable loosening preserves every old transition match.++Unknown shapes return 'False', deliberately over-approximating impact. The+recognized fragment proves @old => new@ through equality, true/false,+conjunction elimination, and disjunction introduction.+-}+guardOnlyLoosening :: Transition -> Transition -> Bool+guardOnlyLoosening oldTransition newTransition =+    oldTransition{tGuard = tGuard newTransition} == newTransition+        && guardImplies (tGuard oldTransition) (tGuard newTransition)++guardImplies :: Maybe Expr -> Maybe Expr -> Bool+guardImplies _ Nothing = True+guardImplies Nothing (Just _) = False+guardImplies (Just oldGuard) (Just newGuard) = implies oldGuard newGuard+  where+    implies old new+        | old == new = True+    implies (EAtom (ABool False)) _ = True+    implies _ (EAtom (ABool True)) = True+    implies (EAnd left right) new = implies left new || implies right new+    implies old (EOr left right) = implies old left || implies old right+    implies _ _ = False++renderReplayImpact :: ReplayImpact -> Text+renderReplayImpact ReplayNeutral =+    "replay-neutral: stored-data replay is unchanged by this diff"+renderReplayImpact (ReplayAffected aggregates) =+    "replay-affected: run the candidate binary's targeted replay audit for "+        <> Text.intercalate+            "; "+            [ aggregateName+                <> " events=["+                <> Text.intercalate "," (Set.toAscList (eventTypes impact))+                <> "] snapshots="+                <> if includeSnapshotStreams impact then "yes" else "no"+            | (aggregateName, impact) <- Map.toAscList aggregates+            ]
src/Keiro/Dsl/Scaffold.hs view
@@ -25,2005 +25,3280 @@     defaultContext,     genPrefixFor,     holePrefixFor,-    scaffoldAggregate,-    scaffoldProcess,-    scaffoldRouter,-    scaffoldContract,-    scaffoldIntake,-    scaffoldPublisher,-    scaffoldWorkqueue,-    scaffoldReadModel,-    scaffoldRefusals,-    windowSeconds,--    -- * Firewall self-check (M3)-    FirewallSurface (..),-    firewallSurface,-    firewallBreaches,--    -- * Internal resolution, shared with "Keiro.Dsl.Harness"-    Agg (..),-    ResolvedCtor (..),-    resolveAgg,-    FieldCat (..),-    fieldCat,-    vertexCtor,-    initialVertex,-    firstEnumCtor,-    lowerFirst,-    pascal,-    pascalFromKebab,-    generatedBanner,-) where--import Data.Char (isAlpha, isAlphaNum, isUpper, toLower, toUpper)-import Data.List (find)-import Data.Maybe (fromMaybe, mapMaybe)-import Data.Text (Text)-import Data.Text qualified as T-import Keiro.Dsl.Grammar-import Keiro.Dsl.ReadModelShape (registryNameFor, subscriptionNameFor)-import Keiro.Dsl.Validate (sagaCategoryError)-import Text.Read (readMaybe)--{- | One emitted module: its on-disk path (relative to the scaffold @--out@-directory), its full text, and whether it is overwritten every run-('Generated') or written only when absent ('HoleStub').--}-data ScaffoldModule = ScaffoldModule-    { modulePath :: !FilePath-    , moduleText :: !Text-    , kind :: !ModuleKind-    , origin :: !Text-    }-    deriving stock (Eq, Show)--data ModuleKind-    = -- | @-- \@generated@; overwritten on every scaffold.-      Generated-    | -- | Hand-owned; created only when absent, never overwritten.-      HoleStub-    deriving stock (Eq, Show)--{- | The threading context: the spec's @context@ name, the chosen output-module-namespace root, and the placement style. Extended additively (never-re-shaped) by later verticals.--}-data Context = Context-    { contextName :: !Text-    , moduleRoot :: !Text-    -- ^ @""@ means no namespace prefix (the historical default).-    , placement :: !Placement-    -- ^ 'GeneratedPrefix' is the historical default.-    }-    deriving stock (Eq, Show)--{- | A context with today's default placement ('GeneratedPrefix', no root prefix)-for the given @context@ name. Callers that do not care about placement (the-@parse@ path, tests) build their context with this.--}-defaultContext :: Text -> Context-defaultContext name = Context{contextName = name, moduleRoot = "", placement = GeneratedPrefix}--{- | The generated-layer namespace for a node, honouring the root prefix and the-placement style. The 'Text' argument is the already-pascalised node name (e.g.-@Reservation@, @HospitalSurge@). For 'GeneratedPrefix' this is-@\<root\>.Generated.\<Ctx\>.\<Node\>@ (identical to the historical layout); for-'CollocatedLeaf' it is @\<root\>.\<Ctx\>.\<Node\>.Generated@.--}-genPrefixFor :: Context -> Text -> Text-genPrefixFor ctx node = case placement ctx of-    GeneratedPrefix -> rootPrefix ctx <> "Generated." <> ctxPascalOf ctx <> "." <> node-    CollocatedLeaf -> rootPrefix ctx <> ctxPascalOf ctx <> "." <> node <> ".Generated"--{- | The hand-owned (hole) namespace for a node: @\<root\>.\<Ctx\>.\<Node\>@ —-the same for both placement styles (holes always sit beside the domain).--}-holePrefixFor :: Context -> Text -> Text-holePrefixFor ctx node = rootPrefix ctx <> ctxPascalOf ctx <> "." <> node---- | The root namespace prefix, dot-terminated, or @""@ when no root is set.-rootPrefix :: Context -> Text-rootPrefix ctx = case moduleRoot ctx of r | T.null r -> ""; r -> r <> "."---- | The context name in PascalCase, e.g. @hospital-capacity@ -> @HospitalCapacity@.-ctxPascalOf :: Context -> Text-ctxPascalOf = pascalFromKebab . contextName------------------------------------------------------------------------------------- Firewall self-check (M3)-----------------------------------------------------------------------------------{- | The canonical keiki surface forbidden in generated modules. Symbolic-operators are matched as maximal Haskell symbol tokens, identifiers as complete-tokens, qualifiers by their leading module alias, and imports structurally.--}-data FirewallSurface = FirewallSurface-    { forbiddenSymbolic :: ![Text]-    , forbiddenIdents :: ![Text]-    , forbiddenQualifiers :: ![Text]-    , forbiddenImports :: ![Text]-    , restrictedImports :: ![(Text, [Text])]-    }-    deriving stock (Eq, Show)--firewallSurface :: FirewallSurface-firewallSurface =-    FirewallSurface-        { forbiddenSymbolic = [".==", "./=", ".<", ".<=", ".>", ".>=", ".&&", ".||", ".+", ".-", ".*", "=:", "*:"]-        , forbiddenIdents = ["lit", "pnot", "tadd", "tsub", "tmul"]-        , forbiddenQualifiers = ["B"]-        , forbiddenImports = ["Keiki.Builder", "Keiki.Operators", "Keiki.Symbolic"]-        , -- Generated aggregate modules use the first two names; generated-          -- harnesses use the final three to validate and step filled holes.-          restrictedImports = [("Keiki.Core", ["RegFile", "HsPred", "defaultValidationOptions", "step", "validateTransducer"])]-        }--{- | Scan generated modules for firewall breaches, returning every offending-@(module path, token, 1-based line number)@. Only modules whose 'kind' is-'Generated' are scanned. Strings and comments are skipped, symbol runs use-maximal munch, and keiki imports are checked independently of token spelling.--}-firewallBreaches :: [ScaffoldModule] -> [(FilePath, Text, Int)]-firewallBreaches mods =-    [ (modulePath m, breach, n)-    | m <- mods-    , kind m == Generated-    , (n, line) <- zip [1 ..] (T.lines (moduleText m))-    , breach <- lineBreaches line-    ]--lineBreaches :: Text -> [Text]-lineBreaches line = case importModule line of-    Just _ -> importBreaches line-    Nothing -> tokenBreaches (codeTokens line)-  where-    tokenBreaches = mapMaybe breachFor-    breachFor (IdentToken ident)-        | ident `elem` forbiddenIdents firewallSurface = Just ident-    breachFor (QualifiedToken qualifier)-        | qualifier `elem` forbiddenQualifiers firewallSurface = Just (qualifier <> ".*")-    breachFor (SymbolToken symbol)-        | symbol `elem` forbiddenSymbolic firewallSurface = Just symbol-    breachFor _ = Nothing--data CodeToken = IdentToken !Text | QualifiedToken !Text | SymbolToken !Text--codeTokens :: Text -> [CodeToken]-codeTokens = go . T.unpack-  where-    go [] = []-    go ('-' : '-' : _) = []-    go ('"' : rest) = go (dropString rest)-    go ('\'' : rest) = go (dropChar rest)-    go (c : rest)-        | isIdentStart c =-            let (identTail, afterIdent) = span isIdentContinue rest-                ident = T.pack (c : identTail)-             in case afterIdent of-                    '.' : next : more-                        | isUpper c && isIdentStart next ->-                            let (_member, afterMember) = span isIdentContinue more-                             in QualifiedToken ident : go afterMember-                    _ -> IdentToken ident : go afterIdent-        | isSymbolChar c =-            let (symbolTail, afterSymbol) = span isSymbolChar rest-             in SymbolToken (T.pack (c : symbolTail)) : go afterSymbol-        | otherwise = go rest-    isIdentStart c = isAlpha c || c == '_'-    isIdentContinue c = isAlphaNum c || c == '_' || c == '\''-    isSymbolChar c = c `elem` ("!#$%&*+./<=>?@\\^|-~:" :: String)-    dropString [] = []-    dropString ('\\' : _escaped : rest) = dropString rest-    dropString ('"' : rest) = rest-    dropString (_ : rest) = dropString rest-    dropChar [] = []-    dropChar ('\\' : _escaped : rest) = dropChar rest-    dropChar ('\'' : rest) = rest-    dropChar (_ : rest) = dropChar rest--importBreaches :: Text -> [Text]-importBreaches line = case importModule line of-    Nothing -> []-    Just imported-        | imported `elem` forbiddenImports firewallSurface -> ["import:" <> imported]-        | Just allowed <- lookup imported (restrictedImports firewallSurface)-        , not (hasAllowedExplicitImportList allowed line) ->-            ["import:" <> imported]-        | otherwise -> []--importModule :: Text -> Maybe Text-importModule line = case T.words (T.strip line) of-    "import" : rest -> find (T.isPrefixOf "Keiki.") rest-    _ -> Nothing--hasAllowedExplicitImportList :: [Text] -> Text -> Bool-hasAllowedExplicitImportList allowed line =-    case (T.breakOn "(" line, T.breakOnEnd ")" line) of-        ((_, open), (close, _))-            | not (T.null open) && not (T.null close) ->-                let inside = T.takeWhile (/= ')') (T.drop 1 open)-                    names = filter (not . T.null) (T.split (not . isAlphaNum) inside)-                 in all (`elem` allowed) names-        _ -> False------------------------------------------------------------------------------------- Derived naming------------------------------------------------------------------------------------- | Resolved, denormalized view of an aggregate used by every emitter.-data Agg = Agg-    { aContext :: !Context-    , aCtxPascal :: !Text-    , aName :: !Text-    , aLoc :: !Loc-    , aVertexType :: !Text-    , aIds :: ![IdDecl]-    , aEnums :: ![EnumDecl]-    , aRegs :: ![RegDecl]-    , aStates :: ![StateDecl]-    , aCommands :: ![ResolvedCtor]-    , aEvents :: ![ResolvedCtor]-    , aTransitions :: ![Transition]-    , aWire :: !WireSpec-    , aProjection :: !(Maybe ProjectionSpec)-    , aSnapshot :: !(Maybe SnapshotSpec)-    , aReadModels :: ![ReadModelNode]-    , aGenPrefix :: !Text-    -- ^ e.g. @Generated.HospitalCapacity.Reservation@-    , aHolePrefix :: !Text-    -- ^ e.g. @HospitalCapacity.Reservation@-    }---- | A command or event constructor with its fully-resolved field types.-data ResolvedCtor = ResolvedCtor-    { rcName :: !Text-    , rcFields :: ![(Text, Text)]-    -- ^ (field name, resolved Haskell type)-    , rcVersion :: !Int-    -- ^ EP-2: schema version (1 for commands and unversioned events).-    , rcUpcastFrom :: !(Maybe Int)-    -- ^ EP-2: the source version this event migrates from (the upcaster step).-    }--defaultWire :: WireSpec-defaultWire = WireSpec{wireKind = "ctorName", wireFields = "camelCase", wireSchemaVersion = 1}--resolveAgg :: Context -> Spec -> Aggregate -> Agg-resolveAgg ctx spec agg =-    Agg-        { aContext = ctx-        , aCtxPascal = ctxPascal-        , aName = nm-        , aLoc = aggLoc agg-        , aVertexType = vertexType-        , aIds = specIds spec-        , aEnums = specEnums spec-        , aRegs = aggRegs agg-        , aStates = aggStates agg-        , aCommands = map resolveCommand (aggCommands agg)-        , aEvents = map resolveEvent (aggEvents agg)-        , aTransitions = aggTransitions agg-        , aWire = fromMaybe defaultWire (aggWire agg)-        , aProjection = aggProjection agg-        , aSnapshot = aggSnapshot agg-        , aReadModels = [readModel | NReadModel readModel <- specNodes spec]-        , aGenPrefix = genPrefixFor ctx nm-        , aHolePrefix = holePrefixFor ctx nm-        }-  where-    nm = aggName agg-    ctxPascal = pascalFromKebab (contextName ctx)-    vertexType = nm <> "Vertex"-    commandFieldTypes = [(cmdName c, cmdFields c) | c <- aggCommands agg]-    resolveCommand c = (mkCtor (cmdName c) (cmdFields c)){rcVersion = 1, rcUpcastFrom = Nothing}-    resolveEvent e =-        (mkCtor (evName e) (eventFields e))-            { rcVersion = evVersion e-            , rcUpcastFrom = fst <$> evUpcastFrom e-            }-      where-        eventFields ev = case evBody ev of-            EventFields fs -> fs-            EventFromCommand cn -> fromMaybe [] (lookup cn commandFieldTypes)-    mkCtor cn fs =-        ResolvedCtor-            { rcName = cn-            , rcFields = map (\f -> (fieldName f, resolveFieldType f)) fs-            , rcVersion = 1-            , rcUpcastFrom = Nothing-            }-    regTypes = [(regName r, regType r) | r <- aggRegs agg]-    idNames = map idName (specIds spec)-    enumNames = map enumName (specEnums spec)-    -- A bare field reuses a register's type if one shares its name; else it-    -- Pascal-cases to a declared id/enum/vertex; else falls back to Text.-    resolveFieldType f = case fieldType f of-        Just ty -> ty-        Nothing ->-            let nme = fieldName f-                pas = pascal nme-             in case lookup nme regTypes of-                    Just ty -> ty-                    Nothing-                        | pas `elem` idNames -> pas-                        | pas `elem` enumNames -> pas-                        | pas == vertexType -> pas-                        | otherwise -> "Text"------------------------------------------------------------------------------------- Entry point-----------------------------------------------------------------------------------{- | Emit all modules for one aggregate. The 'Spec' is needed for the shared-id\/enum declarations.--}-scaffoldAggregate :: Context -> Spec -> Aggregate -> [ScaffoldModule]-scaffoldAggregate ctx spec agg =-    [ genModule a "Domain" (emitDomain a)-    , genModule a "Codec" (emitCodec a)-    , genModule a "EventStream" (emitEventStream a)-    , genModule a "Projection" (emitProjection a)-    , holeModule a (emitHoles a)-    ]-  where-    a = resolveAgg ctx spec agg--genModule :: Agg -> Text -> Text -> ScaffoldModule-genModule a name body =-    ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" (aGenPrefix a) <> "/" <> name <> ".hs")-        , moduleText = body-        , kind = Generated-        , origin = nodeOrigin "aggregate" (aName a) (aLoc a)-        }--holeModule :: Agg -> Text -> ScaffoldModule-holeModule a body =-    ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" (aHolePrefix a) <> "/" <> "Holes.hs")-        , moduleText = body-        , kind = HoleStub-        , origin = nodeOrigin "aggregate" (aName a) (aLoc a)-        }------------------------------------------------------------------------------------- Integration contract (EP-4): a self-contained payload ADT + codec-----------------------------------------------------------------------------------{- | Emit the deterministic, symbol-free contract layer: a payload ADT-(per-event records), the topic constants, the @messageType@ discriminator, and a-strict encode\/decode keyed by it. Self-contained (base\/text\/aeson), so it-compiles standalone — the cross-service schema both producer and consumer agree-on. No keiki symbolic operator (firewall holds).--}-scaffoldContract :: Context -> ContractNode -> [ScaffoldModule]-scaffoldContract ctx c =-    [ ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Contract.hs")-        , moduleText = emitContractGen genPrefix c-        , kind = Generated-        , origin = nodeOrigin "contract" (ctrName c) (ctrLoc c)-        }-    ]-  where-    genPrefix = genPrefixFor ctx (pascal (ctrName c))--emitContractGen :: Text -> ContractNode -> Text-emitContractGen genPrefix c =-    nl $-        [ "{-# LANGUAGE DuplicateRecordFields #-}"-        , "{-# LANGUAGE OverloadedRecordDot #-}"-        , "{-# LANGUAGE OverloadedStrings #-}"-        , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Contract"-        , "  ( " <> payloadTy <> " (..)"-        , nl ["  , " <> ceName e <> "Data (..)" | e <- ctrEvents c]-        , "  , messageTypeOf"-        , "  , encode" <> payloadTy-        , "  , parse" <> payloadTy-        , "  ) where"-        , ""-        , "import Data.Aeson (Value, object, withObject, (.:), (.=))"-        , "import Data.Aeson.Types (Parser, parseEither)"-        , "import Data.Text (Text)"-        , "import qualified Data.Text as T"-        , ""-        , "-- topic constants"-        ]-            ++ [lowerFirst alias <> "Topic :: Text\n" <> lowerFirst alias <> "Topic = " <> tshow t | (alias, t) <- ctrTopics c]-            ++ [ ""-               , "-- the closed payload set (discriminated by " <> tshow (ctrDiscriminator c) <> ")"-               ]-            ++ [emitPayloadAdt payloadTy (ctrEvents c)]-            ++ [ ""-               , "messageTypeOf :: " <> payloadTy <> " -> Text"-               , "messageTypeOf = \\case"-               ]-            ++ ["  " <> ceName e <> " {} -> " <> tshow (ceName e) | e <- ctrEvents c]-            ++ [ ""-               , "encode" <> payloadTy <> " :: " <> payloadTy <> " -> Value"-               , "encode" <> payloadTy <> " = \\case"-               ]-            ++ concatMap encodeArm (ctrEvents c)-            ++ [ ""-               , "parse" <> payloadTy <> " :: Value -> Either Text " <> payloadTy-               , "parse" <> payloadTy <> " = mapLeftText . parseEither (withObject " <> tshow payloadTy <> " go)"-               , "  where"-               , "    go o = do"-               , "      kind <- o .: " <> tshow (ctrDiscriminator c) <> " :: Parser Text"-               , "      case kind of"-               ]-            ++ concatMap decodeArm (ctrEvents c)-            ++ [ "        _ -> fail \"unknown message type\""-               , ""-               , "mapLeftText :: Either String b -> Either Text b"-               , "mapLeftText = either (Left . T.pack) Right"-               ]-  where-    payloadTy = pascal (ctrName c) <> "Payload"-    encodeArm e =-        [ "  " <> ceName e <> " payload ->"-        , "    object"-        ]-            ++ [lead i kv | (i, kv) <- zip [(0 :: Int) ..] ((tshow (ctrDiscriminator c) <> " .= (" <> tshow (ceName e) <> " :: Text)") : [tshow (cfName f) <> " .= payload." <> cfName f | f <- ceFields e])]-            ++ ["      ]"]-    lead 0 kv = "      [ " <> kv-    lead _ kv = "      , " <> kv-    decodeArm e =-        [ "        " <> tshow (ceName e) <> " ->"-        , "          " <> ceName e <> " <$> (" <> ceName e <> "Data" <> fieldApps (ceFields e) <> ")"-        ]-    fieldApps [] = ""-    fieldApps fs = " <$> " <> T.intercalate " <*> " ["o .: " <> tshow (cfName f) | f <- fs]--emitPayloadAdt :: Text -> [ContractEvent] -> Text-emitPayloadAdt tyName events =-    sectionsOf [map dataRecord events, [sumDecl]]-  where-    hsType CText = "Text"-    hsType CInt = "Int"-    hsType (CTypeId _) = "Text"-    dataRecord e =-        "data "-            <> ceName e-            <> "Data = "-            <> ceName e-            <> "Data { "-            <> T.intercalate ", " [cfName f <> " :: !" <> hsType (cfType f) | f <- ceFields e]-            <> " }\n  deriving stock (Eq, Show)"-    arm e = ceName e <> " !" <> ceName e <> "Data"-    sumDecl = case events of-        [] -> "data " <> tyName <> " = " <> tyName <> "Empty\n  deriving stock (Eq, Show)"-        (e : es) ->-            nl $-                ["data " <> tyName <> " = " <> arm e]-                    ++ ["  | " <> arm e2 | e2 <- es]-                    ++ ["  deriving stock (Eq, Show)"]------------------------------------------------------------------------------------- Integration intake (EP-4): inbox disposition vs the live Keiro.Inbox runtime-----------------------------------------------------------------------------------{- | Emit the inbox node's deterministic disposition wiring compiled against the-LIVE @Keiro.Inbox.Types@: the dedupe policy (a real 'InboxDedupePolicy') and a-disposition function over the real @InboxResult@ (Processed\/Duplicate\/-InProgress\/PreviouslyFailed). This pins the dangerous inversions-(duplicate ⇒ ackOk, previouslyFailed ⇒ deadLetter) as compiled code over the-runtime types. The handler-level decode\/dedupe\/store failures are noted but not-part of @InboxResult@. Firewall holds (no keiki symbolic operator).--}-scaffoldIntake :: Context -> IntakeNode -> [ScaffoldModule]-scaffoldIntake ctx i =-    [ ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Inbox.hs")-        , moduleText = emitIntakeGen genPrefix i-        , kind = Generated-        , origin = nodeOrigin "intake" (inkName i) (inkLoc i)-        }-    ]-  where-    genPrefix = genPrefixFor ctx (pascal (inkName i))--emitIntakeGen :: Text -> IntakeNode -> Text-emitIntakeGen genPrefix i =-    nl-        [ "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Inbox"-        , "  ( InboxAck (..)"-        , "  , inboxDedupePolicy"-        , "  , inboxPersistence"-        , "  , inboxDisposition"-        , "  ) where"-        , ""-        , "import Keiro.Inbox.Types (InboxDedupePolicy (..), InboxPersistence (..), InboxResult (..))"-        , ""-        , "-- The dedupe policy (hole-kind 4), lowered to the live InboxDedupePolicy."-        , "inboxDedupePolicy :: InboxDedupePolicy"-        , "inboxDedupePolicy = " <> inkDedupePolicy i-        , ""-        , "{- | Success-path envelope retention passed to runInboxTransactionWith."-        , "Failures always retain their full operator-facing dead-letter envelope."-        , "Dedupe-only success rows decode with an empty payload."-        , "-}"-        , "inboxPersistence :: InboxPersistence"-        , "inboxPersistence = " <> persistenceCtor (inkPersist i)-        , ""-        , "-- The service's ack decision for each inbox classification."-        , "data InboxAck = InboxAckOk | InboxRetry | InboxDeadLetter"-        , "  deriving stock (Eq, Show)"-        , ""-        , "-- The disposition table (hole-kind 2) over the LIVE Keiro.Inbox.Types.InboxResult."-        , "-- duplicate => ackOk and previouslyFailed => deadLetter are the dangerous"-        , "-- inversions the spec states explicitly."-        , "inboxDisposition :: InboxResult a -> InboxAck"-        , "inboxDisposition r = case r of"-        , "  InboxProcessed _ -> " <> ackFor "processed"-        , "  InboxDuplicate -> " <> ackFor "duplicate"-        , "  InboxInProgress -> " <> ackFor "inProgress"-        , "  InboxPreviouslyFailed _ -> " <> ackFor "previouslyFailed"-        , ""-        , "-- handler-level failures (not InboxResult): decodeFailed => "-            <> ackText "decodeFailed"-            <> ", dedupeFailed => "-            <> ackText "dedupeFailed"-            <> ", storeFailed => "-            <> ackText "storeFailed"-        ]-  where-    act o = lookup o [(drOutcome r, drAction r) | r <- inkDisposition i]-    ackFor o = case act o of-        Just IAckOk -> "InboxAckOk"-        Just (IRetry _) -> "InboxRetry"-        Just (IDeadLetter _) -> "InboxDeadLetter"-        Nothing -> "InboxRetry"-    ackText o = case act o of-        Just IAckOk -> "ackOk"-        Just (IRetry _) -> "retry"-        Just (IDeadLetter _) -> "deadLetter"-        Nothing -> "retry"-    persistenceCtor InkPersistFull = "PersistFullEnvelope"-    persistenceCtor InkPersistDedupeOnly = "PersistDedupeOnly"------------------------------------------------------------------------------------- Integration publisher (EP-4): config vs the live Keiro.Outbox runtime-----------------------------------------------------------------------------------{- | Emit the publisher's at-least-once policy compiled against the LIVE-@Keiro.Outbox.Types@: the ordering policy (a real 'OrderingPolicy'), the backoff-curve (a real 'BackoffSchedule'), and the max-attempts ceiling. Firewall holds.--}-scaffoldPublisher :: Context -> PublisherNode -> [ScaffoldModule]-scaffoldPublisher ctx pb =-    [ ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Publisher.hs")-        , moduleText = emitPublisherGen genPrefix pb-        , kind = Generated-        , origin = nodeOrigin "publisher" (pubName pb) (pubLoc pb)-        }-    ]-  where-    genPrefix = genPrefixFor ctx (pascal (pubName pb))--emitPublisherGen :: Text -> PublisherNode -> Text-emitPublisherGen genPrefix pb =-    nl-        [ "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Publisher"-        , "  ( publisherOrdering"-        , "  , publisherBackoff"-        , "  , publisherMaxAttempts"-        , "  ) where"-        , ""-        , "import Keiro.Outbox.Types (BackoffSchedule (..), ExponentialBackoffOptions (..), OrderingPolicy (..))"-        , ""-        , "publisherOrdering :: OrderingPolicy"-        , "publisherOrdering = " <> pubOrdering pb-        , ""-        , "publisherBackoff :: BackoffSchedule"-        , "publisherBackoff = " <> backoffExpr (pubBackoff pb)-        , ""-        , "publisherMaxAttempts :: Int"-        , "publisherMaxAttempts = " <> tshow' (pubMaxAttempts pb)-        ]-  where-    backoffExpr b = case boKind b of-        "constant" -> "ConstantBackoff " <> windowText (boWindow b)-        "exponential" ->-            "ExponentialBackoff ExponentialBackoffOptions { initial = "-                <> windowText (boWindow b)-                <> ", maxDelay = "-                <> maybe "0" windowText (boMax b)-                <> ", multiplier = "-                <> fromMaybe "0" (boMultiplier b)-                <> " }"-        _ -> "error \"keiro-dsl: unlowerable backoff kind\""------------------------------------------------------------------------------------- pgmq workqueue (EP-5): a self-contained Job payload record + codec-----------------------------------------------------------------------------------{- | Emit the deterministic, symbol-free pgmq layer: the Job payload record, the-field→wire-name JSON codec, and the captured physical\/dlq\/table name constants.-Self-contained (base\/text\/aeson). The fan-out body and the raw-SQL dedup-predicate are holes (not emitted). Firewall holds.--}-scaffoldWorkqueue :: Context -> WorkqueueNode -> [ScaffoldModule]-scaffoldWorkqueue ctx w =-    [ ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Queue.hs")-        , moduleText = emitWorkqueueGen genPrefix w-        , kind = Generated-        , origin = nodeOrigin "workqueue" (wqName w) (wqLoc w)-        }-    , ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/QueuePolicy.hs")-        , moduleText = emitQueuePolicy genPrefix w-        , kind = Generated-        , origin = nodeOrigin "workqueue" (wqName w) (wqLoc w)-        }-    ]-  where-    genPrefix = genPrefixFor ctx (pascal (wqName w))--emitWorkqueueGen :: Text -> WorkqueueNode -> Text-emitWorkqueueGen genPrefix w =-    nl $-        [ "{-# LANGUAGE OverloadedRecordDot #-}"-        , "{-# LANGUAGE OverloadedStrings #-}"-        , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Queue"-        , "  ( " <> payloadTy <> " (..)"-        , "  , encode" <> payloadTy-        , "  , parse" <> payloadTy-        , "  , queuePhysical, queueDlq, queueTable"-        , groupKeyExport-        , "  ) where"-        , ""-        , "import Data.Aeson (Value, object, withObject, (.:), (.=))"-        , "import Data.Aeson.Types (parseEither)"-        , "import Data.Text (Text)"-        , "import qualified Data.Text as T"-        , ""-        , "queuePhysical, queueDlq, queueTable :: Text"-        , "queuePhysical = " <> tshow (wqPhysical w)-        , "queueDlq = " <> tshow (wqDlq w)-        , "queueTable = " <> tshow (wqTable w)-        , ""-        ]-            ++ groupKeyLines-            ++ [ "data " <> payloadTy <> " = " <> payloadTy-               , "  { " <> T.intercalate "\n  , " [wqfName f <> " :: !" <> hsType (wqfType f) | f <- wqPayload w]-               , "  }"-               , "  deriving stock (Eq, Show)"-               , ""-               , "encode" <> payloadTy <> " :: " <> payloadTy <> " -> Value"-               , "encode" <> payloadTy <> " p ="-               , "  object"-               ]-            ++ [lead i (tshow (wqfWire f) <> " .= p." <> wqfName f) | (i, f) <- zip [(0 :: Int) ..] (wqPayload w)]-            ++ [ "    ]"-               , ""-               , "parse" <> payloadTy <> " :: Value -> Either Text " <> payloadTy-               , "parse" <> payloadTy <> " = mapLeftText . parseEither (withObject " <> tshow payloadTy <> " go)"-               , "  where"-               , "    go o = " <> payloadTy <> fieldApps (wqPayload w)-               , ""-               , "mapLeftText :: Either String b -> Either Text b"-               , "mapLeftText = either (Left . T.pack) Right"-               ]-  where-    payloadTy = wqPayloadName w-    groupKeyExport = case wqGroupKey w of-        Nothing -> ""-        Just groupKey-            | gkVia groupKey == "raw" -> "  , groupKeyField, groupKeyFor"-            | otherwise -> "  , groupKeyField"-    groupKeyLines = case wqGroupKey w of-        Nothing -> []-        Just groupKey -> common <> derivationLines groupKey-          where-            common =-                [ "groupKeyField :: Text"-                , "groupKeyField = " <> tshow (gkField groupKey)-                , ""-                ]-            derivationLines key-                | gkVia key == "raw" =-                    [ "groupKeyFor :: " <> payloadTy <> " -> Text"-                    , "groupKeyFor payload = payload." <> gkField key-                    , ""-                    ]-                | otherwise =-                    [ "-- Opaque group-key derivation '" <> gkVia key <> "' remains hand-owned."-                    , "-- Captured fixture: " <> fromMaybe "<missing>" (gkFixture key)-                    , ""-                    ]-    hsType "bool" = "Bool"-    hsType "int" = "Int"-    hsType _ = "Text"-    lead 0 kv = "    [ " <> kv-    lead _ kv = "    , " <> kv-    fieldApps [] = ""-    fieldApps fs = " <$> " <> T.intercalate " <*> " ["o .: " <> tshow (wqfWire f) | f <- fs]--{- | Emit the pgmq retry policy + JobOutcome disposition compiled against the-LIVE @Keiro.PGMQ.Job@ runtime (RetryPolicy / JobOutcome / RetryDelay). This pins-the dangerous inversions over the runtime types: storeFailure ⇒ Retry (transient)-and decodeFailure ⇒ Dead (poison).--}-emitQueuePolicy :: Text -> WorkqueueNode -> Text-emitQueuePolicy genPrefix w =-    nl $-        [ "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".QueuePolicy"-        , "  ( retryPolicy, jobOutcomeFor"-        , "  , jobOrdering, jobTuningFor, queueProvision"-        , "  ) where"-        , ""-        , "import Data.Text (Text)"-        , "import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)"-        , ""-        , "jobOrdering :: JobOrdering"-        , "jobOrdering = " <> orderingCtor-        , ""-        , "-- Deployment owns visibility timeout, batch size, and polling; the spec owns ordering."-        , "jobTuningFor :: JobTuning -> JobTuning"-        , "jobTuningFor = withOrdering jobOrdering"-        , ""-        , "-- Pass this to ensureJobQueueWith at worker startup. FIFO adds the required GIN index; the DLQ remains standard."-        , "queueProvision :: QueueProvision"-        , "queueProvision = " <> provisionExpr-        , ""-        , "retryPolicy :: RetryPolicy"-        , "retryPolicy ="-        , "  RetryPolicy"-        , "    { maxRetries = " <> tshow' (wqMaxRetries w)-        , "    , defaultRetryDelay = RetryDelay " <> windowText (wqDelay w)-        , "    , useDeadLetter = " <> (if wqDlqOn w then "True" else "False")-        , "    }"-        , ""-        , "-- The consumer JobOutcome disposition over the spec's named domain outcomes,"-        , "-- lowered to the live Keiro.PGMQ.Job.JobOutcome."-        , "jobOutcomeFor :: Text -> JobOutcome"-        , "jobOutcomeFor o = case o of"-        ]-            ++ ["  " <> tshow (wqdOutcome r) <> " -> " <> outcome (wqdAction r) | r <- wqDisposition w]-            ++ ["  _ -> Retry (RetryDelay " <> windowText (wqDelay w) <> ")"]-  where-    orderingCtor = case wqOrdering w of-        WqUnordered -> "Unordered"-        WqFifoThroughput -> "FifoThroughput"-        WqFifoRoundRobin -> "FifoRoundRobin"-    provisionExpr = fifoWrap baseProvision-    fifoWrap expression = case wqOrdering w of-        WqUnordered -> expression-        _ -> "withFifoIndexProvision (" <> expression <> ")"-    baseProvision = case wqProvision w of-        WqStandard -> "standardProvision"-        WqUnlogged -> "unloggedProvision"-        WqPartitioned interval retention ->-            "partitionedProvision (PartitionSpec { partitionInterval = "-                <> tshow interval-                <> ", retentionInterval = "-                <> tshow retention-                <> " })"-    outcome IAckOk = "Done"-    outcome (IRetry win) = "Retry (RetryDelay " <> windowText win <> ")"-    outcome (IDeadLetter mr) = "Dead " <> tshow (fromMaybe "dead-lettered" mr)------------------------------------------------------------------------------------- First-class read models (EP-107)-----------------------------------------------------------------------------------{- | Emit an acyclic three-module read-model vertical. @ReadModelTable@ owns the-qualified-table constant shared by the hand-owned query and the generated-runtime record; @ReadModel@ re-exports it as part of the public surface.--}-scaffoldReadModel :: Context -> ReadModelNode -> [ScaffoldModule]-scaffoldReadModel ctx readModel =-    [ generated "ReadModelTable" (emitReadModelTable tableModule stem readModel)-    , generated "ReadModel" (emitReadModelGen ctx readModelModule tableModule readModelHolePrefix stem readModel)-    , ScaffoldModule-        { modulePath = modulePathFor readModelHolePrefix "ReadModelHoles"-        , moduleText = emitReadModelHoles tableModule readModelHolePrefix stem readModel-        , kind = HoleStub-        , origin = readModelOrigin-        }-    ]-  where-    nodeSegment = pascal (rmName readModel)-    stem = readModelStem readModel-    readModelModule = genPrefixFor ctx nodeSegment-    tableModule = readModelModule <> ".ReadModelTable"-    readModelHolePrefix = holePrefixFor ctx nodeSegment-    readModelOrigin = nodeOrigin "readmodel" (rmName readModel) (rmLoc readModel)-    generated leaf body =-        ScaffoldModule-            { modulePath = modulePathFor readModelModule leaf-            , moduleText = body-            , kind = Generated-            , origin = readModelOrigin-            }--modulePathFor :: Text -> Text -> FilePath-modulePathFor prefix leaf = T.unpack (T.replace "." "/" prefix <> "/" <> leaf <> ".hs")--readModelStem :: ReadModelNode -> Text-readModelStem = lowerFirst . T.concat . map pascal . T.splitOn "_" . rmName--emitReadModelTable :: Text -> Text -> ReadModelNode -> Text-emitReadModelTable tableModule stem readModel =-    nl-        [ "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> tableModule <> " (" <> qualifiedName <> ") where"-        , ""-        , "import Data.Text (Text)"-        , "import Keiro.Connection (qualifyTable)"-        , ""-        , "-- The fully-qualified, double-quoted data-table reference."-        , qualifiedName <> " :: Text"-        , qualifiedName <> " = qualifyTable " <> tshow (rmSchema readModel) <> " " <> tshow (rmTable readModel)-        ]-  where-    qualifiedName = stem <> "QualifiedTable"--emitReadModelGen :: Context -> Text -> Text -> Text -> Text -> ReadModelNode -> Text-emitReadModelGen ctx readModelModule tableModule readModelHolePrefix stem readModel =-    nl $-        [ "{-# LANGUAGE OverloadedRecordDot #-}"-        , "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> readModelModule <> ".ReadModel"-        , "  ( " <> T.intercalate "\n  , " exports-        , "  ) where"-        , ""-        , "import Data.Functor (void)"-        , "import Effectful (Eff, (:>))"-        , "import " <> tableModule <> " (" <> qualifiedName <> ")"-        , "import " <> readModelHolePrefix <> ".ReadModelHoles (" <> T.intercalate ", " holeImports <> ")"-        ]-            ++ asyncImports-            ++ [ "import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)"-               , "import Keiro.ReadModel.Rebuild qualified as Rebuild"-               , "import Kiroku.Store.Effect (Store)"-               , "import Kiroku.Store.Types (" <> kirokuTypes <> ")"-               , ""-               , readModelName <> " :: ReadModel " <> queryInputType <> " " <> queryResultType-               , readModelName <> " ="-               , "  ReadModel"-               , "    { name = " <> tshow registryName-               , "    , tableName = " <> tshow (rmTable readModel)-               , "    , schema = " <> tshow (rmSchema readModel)-               , "    , subscriptionName = " <> tshow subscriptionName-               , "    , version = " <> tshow' (rmVersion readModel)-               , "    , shapeHash = " <> tshow (rmShape readModel)-               , "    , defaultConsistency = " <> consistencyExpr (rmConsistency readModel)-               , "    , strongScope = " <> scopeExpr (rmScope readModel)-               , "    , query = " <> queryName-               , "    }"-               , ""-               , "-- Call once at projection startup before serving queries."-               , registerName <> " :: (Store :> es) => Eff es ()"-               , registerName <> " ="-               , "  void (registerReadModel " <> tshow registryName <> " " <> tshow' (rmVersion readModel) <> " " <> tshow (rmShape readModel) <> ")"-               , ""-               , startName <> " :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata"-               , startName <> " ="-               , "  Rebuild.startRebuild " <> readModelName <> " " <> projectionNames-               , ""-               , finishName <> " :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)"-               , finishName <> " ="-               , "  Rebuild.finishRebuild " <> readModelName <> " " <> projectionNames-               , ""-               , abandonName <> " :: (Store :> es) => Eff es ReadModelMetadata"-               , abandonName <> " = Rebuild.abandonRebuild " <> readModelName-               ]-            ++ asyncDefinition-  where-    registryName = registryNameFor (contextName ctx) readModel-    subscriptionName = subscriptionNameFor (contextName ctx) readModel-    asyncName = registryName <> "-async"-    readModelName = stem <> "ReadModel"-    qualifiedName = stem <> "QualifiedTable"-    registerName = "register" <> pascal stem-    startName = "start" <> pascal stem <> "Rebuild"-    finishName = "finish" <> pascal stem <> "Rebuild"-    abandonName = "abandon" <> pascal stem <> "Rebuild"-    asyncValueName = stem <> "AsyncProjection"-    queryInputType = pascal stem <> "QueryInput"-    queryResultType = pascal stem <> "QueryResult"-    queryName = stem <> "Query"-    applyName = "apply" <> pascal stem-    exports =-        [ readModelName-        , qualifiedName-        , registerName-        , startName-        , finishName-        , abandonName-        ]-            ++ [asyncValueName | rmFeed readModel == RmSubscription]-    holeImports = [queryInputType, queryResultType, queryName] ++ [applyName | rmFeed readModel == RmSubscription]-    asyncImports = case rmFeed readModel of-        RmInline -> []-        RmSubscription -> ["import Keiro.Projection (AsyncProjection (..))"]-    kirokuTypes = case rmFeed readModel of-        RmInline -> "GlobalPosition"-        RmSubscription -> "GlobalPosition, RecordedEvent (..)"-    projectionNames = case rmFeed readModel of-        RmInline -> "[]"-        RmSubscription -> "[" <> tshow asyncName <> "]"-    asyncDefinition = case rmFeed readModel of-        RmInline -> []-        RmSubscription ->-            [ ""-            , asyncValueName <> " :: AsyncProjection"-            , asyncValueName <> " ="-            , "  AsyncProjection"-            , "    { name = " <> tshow asyncName-            , "    , readModelName = " <> tshow registryName-            , "    , subscriptionName = " <> tshow subscriptionName-            , "    , applyRecorded = " <> applyName-            , "    , idempotencyKey = \\recorded -> recorded.eventId"-            , "    }"-            ]-    consistencyExpr Strong = "Strong"-    consistencyExpr Eventual = "Eventual"-    scopeExpr Nothing = "EntireLog"-    scopeExpr (Just RmEntireLog) = "EntireLog"-    scopeExpr (Just (RmCategory categoryName)) = "CategoryHead " <> tshow categoryName--emitReadModelHoles :: Text -> Text -> Text -> ReadModelNode -> Text-emitReadModelHoles tableModule readModelHolePrefix stem readModel =-    nl $-        [ "-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it."-        , "module " <> readModelHolePrefix <> ".ReadModelHoles"-        , "  ( " <> T.intercalate "\n  , " exports-        , "  ) where"-        , ""-        , "import " <> tableModule <> " (" <> qualifiedName <> ")"-        , "import Hasql.Transaction qualified as Tx"-        ]-            ++ ["import Kiroku.Store.Types (RecordedEvent(..))" | rmFeed readModel == RmSubscription]-            ++ [ ""-               , "-- HOLE: replace these aliases with the real query input and result types."-               , "type " <> queryInputType <> " = ()"-               , "type " <> queryResultType <> " = ()"-               , ""-               , "-- HOLE: query " <> qualifiedTableLiteral readModel <> " via " <> qualifiedName <> "; never rely on search_path."-               , "-- Declared columns:"-               ]-            ++ map (("--   " <>) . readModelColumnDoc) (rmColumns readModel)-            ++ [ queryName <> " :: " <> queryInputType <> " -> Tx.Transaction " <> queryResultType-               , queryName <> " _input = " <> qualifiedName <> " `seq` error " <> tshow ("HOLE: fill " <> rmName readModel <> " query")-               ]-            ++ applyStub-  where-    qualifiedName = stem <> "QualifiedTable"-    queryInputType = pascal stem <> "QueryInput"-    queryResultType = pascal stem <> "QueryResult"-    queryName = stem <> "Query"-    applyName = "apply" <> pascal stem-    exports = [queryInputType, queryResultType, queryName] ++ [applyName | rmFeed readModel == RmSubscription]-    applyStub = case rmFeed readModel of-        RmInline -> []-        RmSubscription ->-            [ ""-            , "-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe."-            , applyName <> " :: RecordedEvent -> Tx.Transaction ()"-            , applyName <> " _recorded = error " <> tshow ("HOLE: fill " <> rmName readModel <> " async apply")-            ]--qualifiedTableLiteral :: ReadModelNode -> Text-qualifiedTableLiteral readModel = quoteSqlIdentifier (rmSchema readModel) <> "." <> quoteSqlIdentifier (rmTable readModel)--quoteSqlIdentifier :: Text -> Text-quoteSqlIdentifier identifier = "\"" <> T.replace "\"" "\"\"" identifier <> "\""--readModelColumnDoc :: RmColumn -> Text-readModelColumnDoc columnDecl =-    rmcName columnDecl-        <> " "-        <> rmcType columnDecl-        <> if rmcRequired columnDecl then " NOT NULL" else ""------------------------------------------------------------------------------------- Router + shared worker-policy lowering (EP-108)-----------------------------------------------------------------------------------scaffoldRouter :: Context -> RouterNode -> [ScaffoldModule]-scaffoldRouter ctx router =-    [ ScaffoldModule-        { modulePath = modulePathFor genPrefix "Router"-        , moduleText = emitRouterGen genPrefix router-        , kind = Generated-        , origin = routerOrigin-        }-    , ScaffoldModule-        { modulePath = modulePathFor holePrefix "RouterHoles"-        , moduleText = emitRouterHoles holePrefix router-        , kind = HoleStub-        , origin = routerOrigin-        }-    ]-  where-    genPrefix = genPrefixFor ctx (rtId router)-    holePrefix = holePrefixFor ctx (rtId router)-    routerOrigin = nodeOrigin "router" (rtId router) (rtLoc router)--emitRouterGen :: Text -> RouterNode -> Text-emitRouterGen genPrefix router =-    nl $-        [ "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Router"-        , "  ( " <> stem <> "Name"-        , "  , " <> stem <> "WorkerOptions"-        , "  ) where"-        , ""-        , "import Data.Text (Text)"-        ]-            ++ workerPolicyImports (rtPoison router)-            ++ [ ""-               , "-- The STABLE router name. It participates in every target-keyed"-               , "-- deterministicRouterCommandId; renaming it re-keys replayed dispatches."-               , stem <> "Name :: Text"-               , stem <> "Name = " <> tshow (rtName router)-               , ""-               , "-- Runtime-owned dispatch id inputs: (name, key, sourceEventId,"-               , "-- targetStreamName, occurrence). Target-keyed, not positional."-               , ""-               , "-- Node-level worker policy lowered from the spec. Pass this value to"-               , "-- Keiro.Router.runRouterWorkerWith; do not silently use defaultWorkerOptions."-               ]-            ++ workerOptionsLines (stem <> "WorkerOptions") (rtRejected router) (rtPoison router)-  where-    stem = lowerFirst (rtId router)--emitRouterHoles :: Text -> RouterNode -> Text-emitRouterHoles holePrefix router =-    nl-        [ "-- HAND-OWNED hole module for the router's behaviour-bearing bodies."-        , "-- keiro-dsl creates it once and never overwrites it."-        , "module " <> holePrefix <> ".RouterHoles () where"-        , ""-        , "-- HOLE resolve :: " <> inName (rtInput router) <> " -> Eff es [PMCommand targetCommand]"-        , "--   Spec source: " <> resolveSourceText (rvSource (rtResolve router)) <> "."-        , "--   The spec's 'stable' keyword acknowledges that retry attempts accumulate"-        , "--   the UNION of resolved target identities. Keep the recipient set stable"-        , "--   for a source event whenever an exact recipient set matters."-        , "-- HOLE router value: assemble Keiro.Router.Router with name = " <> lowerFirst (rtId router) <> "Name,"-        , "--   key, resolve, targetEventStream, and targetProjections; run it with"-        , "--   runRouterWorkerWith " <> lowerFirst (rtId router) <> "WorkerOptions."-        , "-- HOLE targetProjections: spec projections = " <> renderNames (rtProjections router) <> "."-        , "-- NOTE on-duplicate AckOk is sound because Keiro.Router confirms a duplicate"-        , "--   event id against the TARGET stream via confirmBenignDuplicate before"-        , "--   returning PMCommandDuplicate. Hand-rolled dispatch paths must do likewise."-        ]-  where-    renderNames names = "[" <> T.intercalate ", " names <> "]"--resolveSourceText :: ResolveSource -> Text-resolveSourceText (ResolveReadModel name) = "read-model " <> name <> " (typically Keiro.ReadModel.runQuery)"-resolveSourceText ResolveHole = "typed resolver hole"--workerPolicyImports :: PolicyChoice -> [Text]-workerPolicyImports poison =-    [ "import Keiro.ProcessManager (PoisonPolicy (..), RejectedCommandPolicy (..), WorkerOptions (..))"-    , "import Shibuya.Core.Ack (RetryDelay (..))"-    ]-        ++ if poison == PolHalt-            then []-            else ["import Effectful (Eff)", "import Shibuya.Core.Types (Envelope)"]--workerOptionsLines :: Text -> PolicyChoice -> PolicyChoice -> [Text]-workerOptionsLines valueName rejected poison =-    [ valueName <> signature-    , valueName <> argument <> " ="-    , "  WorkerOptions"-    , "    { poisonPolicy = " <> poisonExpr-    , "    , rejectedCommandPolicy = " <> rejectedExpr rejected-    , "    , transientRetryDelay = RetryDelay 5 -- matches defaultWorkerOptions; runtime tuning"-    , "    , metrics = Nothing                  -- runtime configuration; install at call site"-    , "    }"-    ]-  where-    signature = case poison of-        PolHalt -> " :: WorkerOptions es msg"-        _ -> " :: (Envelope msg -> Eff es ()) -> WorkerOptions es msg"-    argument = case poison of-        PolHalt -> ""-        _ -> " poisonCallback"-    poisonExpr = case poison of-        PolHalt -> "PoisonHalt"-        PolDeadLetter -> "PoisonDeadLetter poisonCallback"-        PolSkip -> "PoisonSkip poisonCallback"-    rejectedExpr = \case-        PolHalt -> "RejectedHalt"-        PolDeadLetter -> "RejectedDeadLetter"-        PolSkip -> "RejectedSkip"------------------------------------------------------------------------------------- Process manager + durable timer (EP-3)-----------------------------------------------------------------------------------{- | Emit the symbol-free deterministic wiring for a process manager + its timer-into a @Generated@ module, plus a create-if-absent @ProcessHoles@ module for the-behaviour-bearing bodies (the @handle@ reaction, the deadline window, and the-fire command). The @Generated@ module contains no keiki symbolic operator (the-saga's transducer is the separate aggregate hole), so the firewall invariant-holds. The timer worker uses the spec's @max-attempts@ ceiling, never the-dangerous @defaultTimerWorkerOptions@ (@Nothing@) default.--}-scaffoldProcess :: Context -> ProcessNode -> [ScaffoldModule]-scaffoldProcess ctx p =-    [ ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Process.hs")-        , moduleText = emitProcessGen ctxPascal genPrefix holePrefix p-        , kind = Generated-        , origin = nodeOrigin "process" (procId p) (procLoc p)-        }-    , ScaffoldModule-        { modulePath = T.unpack (T.replace "." "/" holePrefix <> "/ProcessHoles.hs")-        , moduleText = emitProcessHoles genPrefix holePrefix p-        , kind = HoleStub-        , origin = nodeOrigin "process" (procId p) (procLoc p)-        }-    ]-  where-    ctxPascal = pascalFromKebab (contextName ctx)-    genPrefix = genPrefixFor ctx (procId p)-    holePrefix = holePrefixFor ctx (procId p)--emitProcessGen :: Text -> Text -> Text -> ProcessNode -> Text-emitProcessGen _ctxPascal genPrefix _holePrefix p =-    nl $-        [ "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> genPrefix <> ".Process"-        , "  ( " <> lo <> "ProcessName"-        , "  , " <> lo <> "Category"-        , "  , " <> lo <> "ProcessWorkerOptions"-        , "  , " <> lo <> "TimerRequest"-        , "  , " <> lo <> "FireOutcome"-        , "  ) where"-        , ""-        , "import Data.Aeson (Value, object, (.=))"-        , "import Data.Text (Text)"-        , "import qualified Data.Text as T"-        , "import Data.Time (UTCTime)"-        , "import Data.UUID (UUID)"-        , "import qualified Data.UUID.V5 as UUID.V5"-        , "import Keiro.Command (CommandError (..))"-        , "import Keiro.Stream qualified as Stream"-        , "import Keiro.Timer (TimerId (..), TimerRequest (..))"-        ]-            ++ workerPolicyImports (procPoison p)-            ++ [ ""-               , "-- The define-once ProcessManager name (hole-kind 5: referenced, never retyped)."-               , lo <> "ProcessName :: Text"-               , lo <> "ProcessName = " <> tshow (procName p)-               , ""-               , "-- The validated saga stream category (hole-kind 5: referenced, never retyped)."-               , "-- Saga streams are '<category>-<correlationId>' via Keiro.Stream.entityStream."-               , "-- categoryUnsafe is safe here because keiro-dsl check proved the literal legal."-               , lo <> "Category :: Stream.StreamCategory a"-               , lo <> "Category = Stream.categoryUnsafe " <> tshow categoryName-               , ""-               , "-- Node-level worker policy lowered from the spec. Pass this value to"-               , "-- Keiro.ProcessManager.runProcessManagerWorkerWith."-               ]-            ++ workerOptionsLines (lo <> "ProcessWorkerOptions") (procRejected p) (procPoison p)-            ++ [ ""-               , "-- The deterministic timer-request builder: id derived from the correlation"-               , "-- key (hole-kind 1), processManagerName referenced, payload from the spec."-               , "-- (timer id derived as uuidv5 of " <> tshow (idePrefix (tmId timer)) <> " <> correlationId)"-               , lo <> "TimerRequest :: Text -> UTCTime -> TimerRequest"-               , lo <> "TimerRequest correlationId fireAtTime ="-               , "  TimerRequest"-               , "    { timerId = TimerId (namedUuid (" <> tshow (idePrefix (tmId timer)) <> " <> correlationId))"-               , "    , processManagerName = " <> lo <> "ProcessName"-               , "    , correlationId = correlationId"-               , "    , fireAt = fireAtTime"-               , "    , payload = " <> payloadExpr (tmPayload timer)-               , "    }"-               , ""-               , "-- The timer-fire disposition table (hole-kind 2), derived from the spec."-               , "-- on-reject => " <> showOutcome (onReject fd) <> " is the benign inversion."-               , "-- A duplicate append reaches on-error unless it is confirmed against the"-               , "-- target stream. Use Keiro.ProcessManager.confirmBenignDuplicate:"-               , "--   StreamName -> EventId -> CommandError -> Eff es Bool"-               , "-- Fold True into the duplicate result and surface False as the failure."-               , lo <> "FireOutcome :: Either CommandError a -> Maybe ()"-               , lo <> "FireOutcome result = case result of"-               , "  Right{} -> " <> outcomeToMaybe (onOk fd)-               , "  Left CommandRejected -> " <> outcomeToMaybe (onReject fd)-               , "  Left (CommandAmbiguous _) -> " <> outcomeToMaybe (onAmbiguous fd) <> "  -- explicit definition-bug arm"-               , "  Left{} -> " <> outcomeToMaybe (onError fd)-               , ""-               , "-- max-attempts = " <> tshow' (tmMaxAttempts timer) <> ", dead-letter = " <> tshow (tmDeadLetter timer)-               , "-- (the timer worker must pass Just " <> tshow' (tmMaxAttempts timer) <> " to runTimerWorkerWith, never the"-               , "--  defaultTimerWorkerOptions Nothing ceiling that retries forever)."-               , ""-               , "-- deterministic v5 UUID of a correlation-keyed string (hole-kind 1)."-               , "namedUuid :: Text -> UUID"-               , "namedUuid v = UUID.V5.generateNamed UUID.V5.namespaceURL (map (fromIntegral . fromEnum) (T.unpack v))"-               ]-  where-    lo = lowerFirst (procId p)-    categoryName = staticCategory ("process " <> procId p) (sagaCategory (procSaga p))-    timer = procTimer p-    fd = fireDisposition (tmFire timer)--{- | The timer payload, restricted to the spec's literal (@name=\"value\"@)-bindings so it compiles in the deterministic builder. Bare fields and-ref-valued bindings are input-driven (the agent-written hole), not emitted.--}-payloadExpr :: [FieldBinding] -> Text-payloadExpr fs = case [b | b <- fs, isLiteral b] of-    [] -> "object []"-    lits -> "object [ " <> T.intercalate ", " (map kv lits) <> " ]"-  where-    isLiteral b = maybe False (const True) (fbValue b >>= stripWrappingQuotes)-    kv b = tshow (fbName b) <> " .= (" <> maybe "\"\"" tshow (fbValue b >>= stripWrappingQuotes) <> " :: Value)"-    stripWrappingQuotes value = T.stripPrefix "\"" value >>= T.stripSuffix "\""--showOutcome :: FireOutcome -> Text-showOutcome OFired = "Fired"-showOutcome ORetry = "Retry"--outcomeToMaybe :: FireOutcome -> Text-outcomeToMaybe OFired = "Just ()  -- Fired"-outcomeToMaybe ORetry = "Nothing  -- Retry"--emitProcessHoles :: Text -> Text -> ProcessNode -> Text-emitProcessHoles _genPrefix holePrefix p =-    nl-        [ "-- HAND-OWNED hole module for the process manager's behaviour-bearing bodies."-        , "-- keiro-dsl creates it once and never overwrites it."-        , "module " <> holePrefix <> ".ProcessHoles () where"-        , ""-        , "-- HOLE handle: build the ProcessManagerAction (the self-advance"-        , "--   '" <> advCommand (hAdvance (procHandle p)) <> "', the dispatch(es), and the timer) from the input."-        , "-- HOLE streams: build streamFor with entityStream " <> lowerFirst (procId p) <> "Category;"-        , "--   build target streams with entityStream " <> lowerFirst (procTarget p) <> "Category. Never concatenate raw stream names."-        , "-- HOLE window: the deadline policy, e.g. surgeWindow :: NominalDiffTime;"-        , "--   surgeDeadline observedAt = addUTCTime surgeWindow observedAt  (TIME INJECTED)."-        , "-- HOLE fire command: construct " <> fireCommand (tmFire (procTimer p)) <> " for the timer fire,"-        , "--   keyed by correlationId; the fired-event-id is the deterministic uuidv5 of"-        , "--   " <> tshow (idePrefix (fireFiredEventId (tmFire (procTimer p)))) <> " <> correlationId."-        , "-- NOTE on-duplicate AckOk is sound because the runtime confirms a duplicate"-        , "--   event id against the TARGET stream via confirmBenignDuplicate before"-        , "--   returning PMCommandDuplicate. Its effective signature is:"-        , "--     StreamName -> EventId -> CommandError -> Eff es Bool"-        , "--   Hand-rolled paths must call it with the target stream and attempted event id,"-        , "--   fold True into the duplicate result, and surface False as the original failure."-        , "--   Never pattern-match DuplicateEvent as success: event ids are globally unique."-        ]------------------------------------------------------------------------------------- Domain module-----------------------------------------------------------------------------------emitDomain :: Agg -> Text-emitDomain a =-    nl $-        [ "{-# LANGUAGE DataKinds #-}"-        , "{-# LANGUAGE DuplicateRecordFields #-}"-        ]-            ++ ["{-# LANGUAGE DeriveAnyClass #-}" | hasSnapshot a]-            ++ [ "{-# LANGUAGE OverloadedStrings #-}"-               , "{-# LANGUAGE TemplateHaskell #-}"-               , "{-# LANGUAGE TypeApplications #-}"-               , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"-               , generatedBanner-               , "module " <> aGenPrefix a <> ".Domain where"-               , ""-               ]-            ++ ["import Data.Aeson (FromJSON, ToJSON)" | hasSnapshot a]-            ++ [ "import Data.Proxy (Proxy (..))"-               , "import Data.Text (Text)"-               , "import GHC.Generics (Generic)"-               , "import Keiki.Core (RegFile (..))"-               ]-            ++ ["import Keiki.Shape (CanonicalTypeName)" | hasSnapshot a]-            ++ [ "import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)"-               , ""-               , sectionsOf-                    [ map (emitId a) (aIds a)-                    , map (emitEnum a) (aEnums a)-                    , [emitVertex a]-                    , map (emitRecord) (aCommands a)-                    , [emitSum (aName a <> "Command") (aCommands a)]-                    , map (emitRecord) (aEvents a)-                    , [emitSum (aName a <> "Event") (aEvents a)]-                    , [emitRegsType a, emitInitialRegs a]-                    ,-                        [ "$(deriveAggregateCtorsAll ''" <> aName a <> "Command ''" <> aName a <> "Regs)"-                        , ""-                        , "$(deriveWireCtorsAll ''" <> aName a <> "Event)"-                        ]-                    ]-               ]--hasSnapshot :: Agg -> Bool-hasSnapshot = maybe False (const True) . aSnapshot--emitId :: Agg -> IdDecl -> Text-emitId a d =-    nl $-        [ "newtype " <> idName d <> " = " <> idName d <> " Text"-        , "  deriving stock (Generic, Eq, Ord, Show)"-        ]-            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]-            ++ ["instance CanonicalTypeName " <> idName d | hasSnapshot a]-            ++ [ ""-               , lowerFirst (idName d) <> "Text :: " <> idName d <> " -> Text"-               , lowerFirst (idName d) <> "Text (" <> idName d <> " t) = t"-               ]--emitEnum :: Agg -> EnumDecl -> Text-emitEnum a d =-    nl $-        [ "data " <> enumName d <> " = " <> T.intercalate " | " (map fst (enumCtors d))-        , "  deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)"-        ]-            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]-            ++ ["instance CanonicalTypeName " <> enumName d | hasSnapshot a]-            ++ [ ""-               , lowerFirst (enumName d) <> "Text :: " <> enumName d <> " -> Text"-               , lowerFirst (enumName d) <> "Text = \\case"-               , nl ["  " <> c <> " -> " <> tshow w | (c, w) <- enumCtors d]-               ]--emitVertex :: Agg -> Text-emitVertex a =-    nl $-        [ "data " <> aVertexType a <> " = " <> T.intercalate " | " (map (vertexCtor a . stName) (aStates a))-        , "  deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)"-        ]-            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]-            ++ ["instance CanonicalTypeName " <> aVertexType a | hasSnapshot a]--emitRecord :: ResolvedCtor -> Text-emitRecord rc =-    nl $-        [ "data " <> rcName rc <> "Data = " <> rcName rc <> "Data"-        ]-            ++ recordFields (rcFields rc)-            ++ ["  deriving stock (Generic, Eq, Show)"]--recordFields :: [(Text, Text)] -> [Text]-recordFields [] =-    ["  {"]-        <> ["  }"]-recordFields fs =-    [ lead i <> n <> " :: !" <> ty-    | (i, (n, ty)) <- zip [(0 :: Int) ..] fs-    ]-        ++ ["  }"]-  where-    lead 0 = "  { "-    lead _ = "  , "--emitSum :: Text -> [ResolvedCtor] -> Text-emitSum tyName ctors =-    nl $-        [firstLine] ++ restLines ++ ["  deriving stock (Generic, Eq, Show)"]-  where-    arm rc = rc' rc-    rc' rc = rcName rc <> " !" <> rcName rc <> "Data"-    (firstLine, restLines) = case ctors of-        [] -> ("data " <> tyName <> " = ()", [])-        (c : cs) ->-            ( "data " <> tyName <> " = " <> arm c-            , ["  | " <> arm c2 | c2 <- cs]-            )--emitRegsType :: Agg -> Text-emitRegsType a =-    nl $-        ["type " <> aName a <> "Regs ="]-            ++ regListLines (aRegs a)--regListLines :: [RegDecl] -> [Text]-regListLines [] = ["  '[]"]-regListLines rs =-    [ lead i <> "'(" <> tshow (regName r) <> ", " <> regType r <> ")"-    | (i, r) <- zip [(0 :: Int) ..] rs-    ]-        ++ ["   ]"]-  where-    lead 0 = "  '[ "-    lead _ = "   , "--emitInitialRegs :: Agg -> Text-emitInitialRegs a =-    nl $-        [ "initial" <> aName a <> "Regs :: RegFile " <> aName a <> "Regs"-        , "initial" <> aName a <> "Regs ="-        ]-            ++ chain (aRegs a)-  where-    chain [] = ["  RNil"]-    chain rs =-        [ "  RCons (Proxy @" <> tshow (regName r) <> ") " <> regInitialValue a r <> " $"-        | r <- init rs-        ]-            ++ ["  RCons (Proxy @" <> tshow (regName lastR) <> ") " <> regInitialValue a lastR <> " RNil"]-      where-        lastR = last rs---- | The Haskell initial value for a register, by the category of its type.-regInitialValue :: Agg -> RegDecl -> Text-regInitialValue a r-    | regType r `elem` idNames = "(" <> regType r <> " \"\")"-    | regType r == aVertexType a = maybe "(error \"invalid vertex initial\")" (vertexCtor a) (bareInitial r)-    | regType r == "Text" = maybe "(error \"Text initial must be quoted\")" tshow (textInitial r)-    | otherwise = maybe "(error \"invalid register initial\")" id (bareInitial r)-  where-    idNames = map idName (aIds a)-    bareInitial reg = case regInitial reg of-        RegInitBare value -> Just value-        RegInitText _ -> Nothing-    textInitial reg = case regInitial reg of-        RegInitText value -> Just value-        RegInitBare _ -> Nothing------------------------------------------------------------------------------------- Codec module-----------------------------------------------------------------------------------emitCodec :: Agg -> Text-emitCodec a =-    nl-        [ "{-# LANGUAGE OverloadedRecordDot #-}"-        , "{-# LANGUAGE OverloadedStrings #-}"-        , generatedBanner-        , "module " <> aGenPrefix a <> ".Codec ("-        , "    " <> lowerFirst (aName a) <> "Codec,"-        , "    parse" <> aName a <> "Event,"-        , "    encode" <> aName a <> "Event,"-        , ") where"-        , ""-        , "import " <> aGenPrefix a <> ".Domain"-        , "import Data.Aeson (Value, object, withObject, (.:), (.=))"-        , "import Data.Aeson.Types (Parser, parseEither)"-        , "import Data.List.NonEmpty (NonEmpty (..))"-        , "import Data.Text (Text)"-        , "import qualified Data.Text as T"-        , "import Keiro.Codec (Codec (..), EventType (..))"-        , upcasterImport a-        , ""-        , emitEnumParsers a-        , ""-        , emitCodecValue a-        , ""-        , emitEncode a-        , ""-        , emitDecode a-        , ""-        , "mapLeftText :: Either String b -> Either Text b"-        , "mapLeftText = either (Left . T.pack) Right"-        ]--emitEnumParsers :: Agg -> Text-emitEnumParsers a = sectionsOf [[emitEnumParser e | e <- aEnums a]]--emitEnumParser :: EnumDecl -> Text-emitEnumParser d =-    nl $-        [ "parse" <> enumName d <> " :: Text -> Parser " <> enumName d-        , "parse" <> enumName d <> " = \\case"-        ]-            ++ ["  " <> tshow w <> " -> pure " <> c | (c, w) <- enumCtors d]-            ++ ["  _ -> fail " <> tshow ("unknown " <> enumName d)]--emitCodecValue :: Agg -> Text-emitCodecValue a =-    nl $-        [ lowerFirst (aName a) <> "Codec :: Codec " <> aName a <> "Event"-        , lowerFirst (aName a) <> "Codec ="-        , "  Codec"-        , "    { eventTypes = " <> eventTypesExpr-        , "    , eventType = \\case"-        ]-            ++ ["        " <> rcName e <> "{} -> EventType " <> tshow (rcName e) | e <- aEvents a]-            ++ [ "    , schemaVersion = " <> tshow' (maxEventVersion a)-               , "    , encode = encode" <> aName a <> "Event"-               , "    , decode = parse" <> aName a <> "Event"-               , "    , upcasters = " <> upcastersExpr a-               , "    }"-               ]-  where-    eventTypesExpr = case map rcName (aEvents a) of-        [] -> "error \"no events\""-        (e : es) -> "EventType " <> tshow e <> " :| [" <> T.intercalate ", " (map (("EventType " <>) . tshow) es) <> "]"---- | The codec's @schemaVersion@: the maximum declared event version (EP-2).-maxEventVersion :: Agg -> Int-maxEventVersion a = maximum (1 : map rcVersion (aEvents a))--{- | One @(sourceVersion, upcasterName)@ entry per event that declares an-@upcast from@. The upcaster name is per-event (e.g. @upcastFooV1@) and its-body is a hole in the hand-owned Holes module.--}-upcasterEntries :: Agg -> [(Int, Text)]-upcasterEntries a =-    [ (m, "upcast" <> rcName e <> "V" <> tshow' m)-    | e <- aEvents a-    , Just m <- [rcUpcastFrom e]-    ]--upcastersExpr :: Agg -> Text-upcastersExpr a =-    "[" <> T.intercalate ", " ["(" <> tshow' m <> ", const " <> fn <> ")" | (m, fn) <- upcasterEntries a] <> "]"--{- | When the codec references upcasters, it imports their (hole) definitions-from the hand-owned Holes module.--}-upcasterImport :: Agg -> Text-upcasterImport a = case upcasterEntries a of-    [] -> ""-    es -> "import " <> aHolePrefix a <> ".Holes (" <> T.intercalate ", " (map snd es) <> ")"--emitEncode :: Agg -> Text-emitEncode a =-    nl $-        [ "encode" <> aName a <> "Event :: " <> aName a <> "Event -> Value"-        , "encode" <> aName a <> "Event = \\case"-        ]-            ++ concatMap encodeArm (aEvents a)-  where-    encodeArm e =-        [ "  " <> rcName e <> " payload ->"-        , "    object"-        ]-            ++ [ lead i <> kv-               | (i, kv) <- zip [(0 :: Int) ..] (("\"kind\" .= (" <> tshow (rcName e) <> " :: Text)") : map encodeField (rcFields e))-               ]-            ++ ["      ]"]-    lead 0 = "      [ "-    lead _ = "      , "-    encodeField (n, ty) =-        tshow n-            <> " .= "-            <> case fieldCat a ty of-                IdCat -> lowerFirst ty <> "Text payload." <> n-                EnumCat -> lowerFirst ty <> "Text payload." <> n-                _ -> "payload." <> n--emitDecode :: Agg -> Text-emitDecode a =-    nl $-        [ "parse" <> aName a <> "Event :: EventType -> Value -> Either Text " <> aName a <> "Event"-        , "parse" <> aName a <> "Event (EventType tag) = mapLeftText . parseEither (withObject " <> tshow (aName a <> "Event") <> " go)"-        , "  where"-        , "    go o = do"-        , "      case tag of"-        ]-            ++ concatMap decodeArm (aEvents a)-            ++ ["        _ -> fail \"unknown event type\""]-  where-    decodeArm e =-        [ "        " <> tshow (rcName e) <> " ->"-        , "          " <> rcName e <> " <$> (" <> rcName e <> "Data" <> fieldApps (rcFields e) <> ")"-        ]-    fieldApps [] = ""-    fieldApps fs = " <$> " <> T.intercalate " <*> " (map decodeField fs)-    -- The first field uses <$> (handled above), the rest <*>. We instead build-    -- a uniform list and join; for an empty record there are no fields.-    decodeField (n, ty) = case fieldCat a ty of-        IdCat -> "(" <> ty <> " <$> o .: " <> tshow n <> ")"-        EnumCat -> "(o .: " <> tshow n <> " >>= parse" <> ty <> ")"-        _ -> "o .: " <> tshow n------------------------------------------------------------------------------------- EventStream module-----------------------------------------------------------------------------------emitEventStream :: Agg -> Text-emitEventStream a =-    nl $-        [ generatedBanner-        , "module " <> aGenPrefix a <> ".EventStream"-        , "  ( " <> lowerFirst (aName a) <> "Category"-        , "  , " <> lowerFirst (aName a) <> "EventStream"-        , "  , " <> lowerFirst (aName a) <> "EventStreamDef"-        , "  , " <> aName a <> "EventStream"-        , "  , " <> aName a <> "EventStreamDef"-        ]-            ++ ["  , " <> lowerFirst (aName a) <> "SnapshotFixture" | hasSnapshot a]-            ++ [ "  ) where"-               , ""-               , "import " <> aGenPrefix a <> ".Domain"-               , "import " <> aGenPrefix a <> ".Codec (" <> lowerFirst (aName a) <> "Codec)"-               , "import " <> aHolePrefix a <> ".Holes (" <> lowerFirst (aName a) <> "Transducer)"-               , "import Keiki.Core (HsPred)"-               , "import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))"-               , "import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)"-               ]-            ++ ["import Data.Text (Text)" | hasSnapshot a]-            ++ ["import Keiro.Snapshot.Codec (defaultStateCodec)" | hasSnapshot a]-            ++ [ "import Keiro.Stream qualified as Stream"-               , ""-               , "-- The validated aggregate stream category (hole-kind 5: referenced, never retyped)."-               , "-- Entity streams are '<category>-<id>' via Keiro.Stream.entityStream."-               , "-- categoryUnsafe is safe here because this generated literal passed the DSL category proof."-               , lowerFirst (aName a) <> "Category :: Stream.StreamCategory a"-               , lowerFirst (aName a) <> "Category = Stream.categoryUnsafe " <> tshow categoryName-               , ""-               , "type " <> aName a <> "EventStreamDef ="-               , "  EventStream (HsPred " <> aName a <> "Regs " <> aName a <> "Command) " <> aName a <> "Regs " <> aVertexType a <> " " <> aName a <> "Command " <> aName a <> "Event"-               , ""-               , "type " <> aName a <> "EventStream ="-               , "  ValidatedEventStream (HsPred " <> aName a <> "Regs " <> aName a <> "Command) " <> aName a <> "Regs " <> aVertexType a <> " " <> aName a <> "Command " <> aName a <> "Event"-               , ""-               , lowerFirst (aName a) <> "EventStreamDef :: " <> aName a <> "EventStreamDef"-               , lowerFirst (aName a) <> "EventStreamDef ="-               , "  EventStream"-               , "    { transducer = " <> lowerFirst (aName a) <> "Transducer"-               , "    , initialState = " <> initialVertex a-               , "    , initialRegisters = initial" <> aName a <> "Regs"-               , "    , eventCodec = " <> lowerFirst (aName a) <> "Codec"-               , "    , resolveStreamName = Stream.streamName"-               , "    , snapshotPolicy = " <> snapshotPolicyExpr a-               , "    , stateCodec = " <> stateCodecExpr a-               , "    }"-               , ""-               ]-            ++ snapshotFixtureLines a-            ++ [ lowerFirst (aName a) <> "EventStream :: " <> aName a <> "EventStream"-               , lowerFirst (aName a) <> "EventStream ="-               , "  mkEventStreamOrThrow " <> tshow (aName a) <> " " <> lowerFirst (aName a) <> "EventStreamDef"-               ]-  where-    categoryName = staticCategory ("aggregate " <> aName a) (lowerFirst (aName a))--snapshotPolicyExpr :: Agg -> Text-snapshotPolicyExpr aggregate = case aSnapshot aggregate of-    Nothing -> "Never"-    Just snapshot -> case snapPolicy snapshot of-        SnapEvery interval -> "Every " <> tshow' interval-        SnapOnTerminal -> "OnTerminal"--stateCodecExpr :: Agg -> Text-stateCodecExpr aggregate = case aSnapshot aggregate of-    Nothing -> "Nothing"-    Just snapshot -> "Just (defaultStateCodec " <> tshow' (snapCodecVersion snapshot) <> ")"--snapshotFixtureLines :: Agg -> [Text]-snapshotFixtureLines aggregate = case aSnapshot aggregate of-    Nothing -> []-    Just snapshot ->-        [ lowerFirst (aName aggregate) <> "SnapshotFixture :: (Int, Text)"-        , lowerFirst (aName aggregate) <> "SnapshotFixture = (" <> tshow' (snapCodecVersion snapshot) <> ", " <> tshow (snapShapeHash snapshot) <> ")"-        , ""-        ]------------------------------------------------------------------------------------- Projection module-----------------------------------------------------------------------------------emitProjection :: Agg -> Text-emitProjection a = case aProjection a of-    Nothing -> nl [generatedBanner, "module " <> aGenPrefix a <> ".Projection () where"]-    Just p ->-        nl-            [ "{-# LANGUAGE OverloadedRecordDot #-}"-            , "{-# LANGUAGE OverloadedStrings #-}"-            , generatedBanner-            , "module " <> aGenPrefix a <> ".Projection"-            , "  ( " <> lowerFirst (projTable p) <> "Projection"-            , "  , " <> lowerFirst (projTable p) <> "StatusFor"-            , "  ) where"-            , ""-            , "import " <> aGenPrefix a <> ".Domain"-            , "import " <> aHolePrefix a <> ".Holes (apply" <> pascal (projTable p) <> ")"-            , "import Data.Text (Text)"-            , "import Keiro.Projection (InlineProjection (..))"-            , ""-            , "-- The deterministic event->status mapping (hole-kind 3, /mapping/), derived"-            , "-- from the spec's status-map. The read-model SQL that consumes it lives in"-            , "-- the hand-owned Holes module (a DB-coupled hole, delegated to codd)."-            , projectionTableComment a p-            , lowerFirst (projTable p) <> "StatusFor :: " <> aName a <> "Event -> Maybe Text"-            , lowerFirst (projTable p) <> "StatusFor = \\case"-            , nl (statusArms a p)-            , ""-            , lowerFirst (projTable p) <> "Projection :: InlineProjection " <> aName a <> "Event"-            , lowerFirst (projTable p) <> "Projection ="-            , "  InlineProjection"-            , "    { name = " <> tshow (contextNameToProjName a p)-            , "    , apply = apply" <> pascal (projTable p)-            , "    }"-            ]--statusArms :: Agg -> ProjectionSpec -> [Text]-statusArms a p =-    [ "  " <> rcName e <> " {} -> " <> statusFor e-    | e <- aEvents a-    ]-        ++ ["  _ -> Nothing" | hasWildcard]-  where-    pairs = maybe [] mapPairs (projStatusMap p)-    statusFor e = case lookup (rcName e) pairs of-        Just value -> "Just " <> tshow value-        Nothing -> "Nothing"-    -- A wildcard is only needed if some event is uncovered; otherwise every arm-    -- is explicit and a wildcard would be redundant (and -Wall would warn).-    hasWildcard = False--contextNameToProjName :: Agg -> ProjectionSpec -> Text-contextNameToProjName a p = contextKebab a <> "-" <> projTable p <> "-inline"--contextKebab :: Agg -> Text-contextKebab = kebabFromPascal . aCtxPascal--projectionReadModel :: Agg -> Maybe ReadModelNode-projectionReadModel aggregate = do-    projection <- aProjection aggregate-    find ((== projTable projection) . rmName) (aReadModels aggregate)--projectionTableComment :: Agg -> ProjectionSpec -> Text-projectionTableComment aggregate projection = case projectionReadModel aggregate of-    Nothing ->-        "-- WARNING: no readmodel node declares '"-            <> projTable projection-            <> "'; unqualified SQL depends on search_path."-    Just readModel ->-        "-- Qualified table "-            <> qualifiedTableLiteral readModel-            <> "; use "-            <> genPrefixFor (aContext aggregate) (pascal (rmName readModel))-            <> ".ReadModelTable."-            <> readModelStem readModel-            <> "QualifiedTable."------------------------------------------------------------------------------------- Holes module (create-if-absent)-----------------------------------------------------------------------------------emitHoles :: Agg -> Text-emitHoles a =-    nl-        [ "{-# LANGUAGE BlockArguments #-}"-        , "{-# LANGUAGE DataKinds #-}"-        , "{-# LANGUAGE OverloadedRecordDot #-}"-        , "{-# LANGUAGE QualifiedDo #-}"-        , "{-# LANGUAGE TypeApplications #-}"-        , "-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never"-        , "-- overwrites it. Fill the transducer body (and any other holes) against the"-        , "-- generated signatures, then run the harness to confirm behaviour."-        , "module " <> aHolePrefix a <> ".Holes"-        , "  ( " <> lowerFirst (aName a) <> "Transducer"-        , holeProjectionExport a-        , holeUpcasterExports a-        , "  ) where"-        , ""-        , "import " <> aGenPrefix a <> ".Domain"-        , "import Keiki.Builder ((=:))"-        , "import qualified Keiki.Builder as B"-        , "import Keiki.Core (HsPred, RegFile, SymTransducer, lit, (.==), (./=), (.||))"-        , holeUpcasterImports a-        , holeProjectionImports a-        , ""-        , "-- HOLE: the transducer body. Reproduce the structure below, replacing each"-        , "-- `-- HOLE` line with the keiki symbolic operators it describes."-        , lowerFirst (aName a) <> "Transducer"-        , "  :: SymTransducer"-        , "       (HsPred " <> aName a <> "Regs " <> aName a <> "Command)"-        , "       " <> aName a <> "Regs"-        , "       " <> aVertexType a-        , "       " <> aName a <> "Command"-        , "       " <> aName a <> "Event"-        , lowerFirst (aName a) <> "Transducer ="-        , "  B.buildTransducer " <> initialVertex a <> " initial" <> aName a <> "Regs isTerminal do"-        , nl (concatMap (fromBlock a) (groupBySource a))-        , " where"-        , "  isTerminal = \\case"-        , nl ["    " <> vertexCtor a (stName s) <> " -> True" | s <- aStates a, stTerminal s]-        , "    _ -> False"-        , holeProjectionStub a-        , holeUpcasterStubs a-        ]---- | Export, import, and stub the per-event upcaster holes (EP-2 evolution).-holeUpcasterExports :: Agg -> Text-holeUpcasterExports a = case upcasterEntries a of-    [] -> ""-    es -> nl ["  , " <> fn | (_, fn) <- es]--holeUpcasterImports :: Agg -> Text-holeUpcasterImports a = case upcasterEntries a of-    [] -> ""-    _ -> nl ["import Data.Aeson (Value)", "import Data.Text (Text)"]--holeUpcasterStubs :: Agg -> Text-holeUpcasterStubs a = case upcasterEntries a of-    [] -> ""-    es ->-        nl $-            concat-                [ [ ""-                  , "-- HOLE upcaster: bring a " <> fn <> " payload up one version. Decide the"-                  , "-- default/derivation for any field added at the new version here."-                  , fn <> " :: Value -> Either Text Value"-                  , fn <> " _ = Left \"HOLE: upcaster not implemented\""-                  ]-                | (_, fn) <- es-                ]--holeProjectionExport :: Agg -> Text-holeProjectionExport a = case aProjection a of-    Nothing -> "  -- (no projection)"-    Just p -> "  , apply" <> pascal (projTable p)--holeProjectionImports :: Agg -> Text-holeProjectionImports aggregate = case projectionReadModel aggregate of-    Nothing -> ""-    Just readModel ->-        "import "-            <> genPrefixFor (aContext aggregate) (pascal (rmName readModel))-            <> ".ReadModelTable ("-            <> readModelStem readModel-            <> "QualifiedTable)"--holeProjectionStub :: Agg -> Text-holeProjectionStub a = case aProjection a of-    Nothing -> ""-    Just p ->-        nl-            ( [ ""-              , "-- HOLE: the read-model SQL for the projection (a DB-coupled hole; the"-              , "-- pure event->status mapping is generated as " <> lowerFirst (projTable p) <> "StatusFor)."-              ]-                ++ projectionGuidance-                ++ [ "apply" <> pascal (projTable p) <> " :: " <> aName a <> "Event -> recorded -> txn ()"-                   , "apply" <> pascal (projTable p) <> " _event _recorded = " <> projectionTableUse <> "error \"HOLE: fill " <> projTable p <> " projection apply\""-                   ]-            )-      where-        projectionGuidance = case projectionReadModel a of-            Nothing ->-                ["-- WARNING: no readmodel node declares this table's schema; unqualified SQL depends on search_path."]-            Just readModel ->-                [ "-- Table: " <> qualifiedTableLiteral readModel <> ". Use " <> readModelStem readModel <> "QualifiedTable; never rely on search_path."-                , "-- Declared columns:"-                ]-                    ++ map (("--   " <>) . readModelColumnDoc) (rmColumns readModel)-        projectionTableUse = case projectionReadModel a of-            Nothing -> ""-            Just readModel -> readModelStem readModel <> "QualifiedTable `seq` "---- Group transitions by source state, preserving order, for the B.from blocks.-groupBySource :: Agg -> [(Text, [Transition])]-groupBySource a = go [] (transitionsOf a)-  where-    go acc [] = reverse acc-    go acc (t : ts) =-        let src = tSource t-            (same, rest) = span ((== src) . tSource) ts-         in go ((src, t : same) : acc) rest---- We don't keep the original Aggregate around in Agg, so reconstruct--- transitions from a stored field. (Filled in resolveAgg via aTransitions.)-transitionsOf :: Agg -> [Transition]-transitionsOf = aTransitions--fromBlock :: Agg -> (Text, [Transition]) -> [Text]-fromBlock a (src, ts) =-    [ "    B.from " <> vertexCtor a src <> " do"-    ]-        ++ concatMap (onCmdBlock a) ts--onCmdBlock :: Agg -> Transition -> [Text]-onCmdBlock a t =-    [ "      B.onCmd inCtor" <> tCommand t <> " $ \\d -> B.do"-    ]-        ++ maybe [] (\g -> ["        -- HOLE guard: " <> renderGuard g]) (tGuard t)-        ++ ["        -- HOLE write " <> r <> " := " <> renderGuard e | (r, e) <- tWrites t]-        ++ ["        -- HOLE emit " <> ev <> " (B.emit wire" <> ev <> " ...)" | ev <- tEmits t]-        ++ ["        B.goto " <> vertexCtor a (tGoto t)]------------------------------------------------------------------------------------- Field categories and shared helpers-----------------------------------------------------------------------------------data FieldCat = IdCat | EnumCat | OtherCat--fieldCat :: Agg -> Text -> FieldCat-fieldCat a ty-    | ty `elem` map idName (aIds a) = IdCat-    | ty `elem` map enumName (aEnums a) = EnumCat-    | otherwise = OtherCat---- | The first constructor of a declared enum, used to build sample values.-firstEnumCtor :: Agg -> Text -> Maybe Text-firstEnumCtor a ty =-    case [c | e <- aEnums a, enumName e == ty, (c, _) <- take 1 (enumCtors e)] of-        (c : _) -> Just c-        [] -> Nothing--vertexCtor :: Agg -> Text -> Text-vertexCtor a s = aName a <> s--initialVertex :: Agg -> Text-initialVertex a = case aStates a of-    (s : _) -> vertexCtor a (stName s)-    [] -> aName a <> "Init"--generatedBanner :: Text-generatedBanner = "-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec."--nodeOrigin :: Text -> Text -> Loc -> Text-nodeOrigin nodeKind nodeName loc =-    nodeKind <> " " <> nodeName <> case unLoc loc of-        0 -> ""-        line -> " (line " <> tshow' line <> ")"--{- | Conditions that the deterministic emitters cannot lower faithfully. The-pre-write scaffold pipeline treats each returned message as a refusal. The-list is extended alongside the policy and type lowering milestones.--}-scaffoldRefusals :: Spec -> [Text]-scaffoldRefusals spec =-    concatMap aggregateRefusals aggregates-        <> concatMap contractRefusals contracts-        <> concatMap publisherRefusals publishers-  where-    aggregates = [aggregate | NAggregate aggregate <- specNodes spec]-    contracts = [contract | NContract contract <- specNodes spec]-    publishers = [publisher | NPublisher publisher <- specNodes spec]-    idTypes = map idName (specIds spec)-    enumTypes = map enumName (specEnums spec)-    enumCtorsFor ty = case [map fst (enumCtors enum) | enum <- specEnums spec, enumName enum == ty] of-        ctors : _ -> ctors-        [] -> []-    aggregateRefusals aggregate =-        [ "AggregateEmpty: aggregate '" <> aggName aggregate <> "' must declare at least one command, event, and transition"-        | null (aggCommands aggregate) || null (aggEvents aggregate) || null (aggTransitions aggregate)-        ]-            <> concatMap (registerRefusals aggregate) (aggRegs aggregate)-            <> [ "FieldTypeUnrepresentable: aggregate '" <> aggName aggregate <> "' field '" <> fieldName field <> "' has unsupported explicit type '" <> ty <> "'"-               | field <- aggregateFields aggregate-               , Just ty <- [fieldType field]-               , not (supportedType aggregate ty)-               ]-    registerRefusals aggregate reg =-        [ "RegTypeUnsupported: aggregate '" <> aggName aggregate <> "' register '" <> regName reg <> "' has unsupported type '" <> regType reg <> "'"-        | not (supportedType aggregate (regType reg))-        ]-            <> [ "RegTextInitialNotQuoted: aggregate '" <> aggName aggregate <> "' Text register '" <> regName reg <> "' must use a quoted initial"-               | regType reg == "Text"-               , RegInitBare _ <- [regInitial reg]-               ]-            <> [ "RegInitialNotEnumCtor: aggregate '" <> aggName aggregate <> "' register '" <> regName reg <> "' must start at a constructor of enum '" <> regType reg <> "'"-               | regType reg `elem` enumTypes-               , case regInitial reg of-                    RegInitBare value -> value `notElem` enumCtorsFor (regType reg)-                    RegInitText _ -> True-               ]-            <> [ "RegInitialInvalidLiteral: aggregate '" <> aggName aggregate <> "' Bool register '" <> regName reg <> "' must start at True or False"-               | regType reg == "Bool"-               , case regInitial reg of RegInitBare value -> value `notElem` ["True", "False"]; RegInitText _ -> True-               ]-            <> [ "RegInitialInvalidLiteral: aggregate '" <> aggName aggregate <> "' Int register '" <> regName reg <> "' must start at an integer literal"-               | regType reg == "Int"-               , case regInitial reg of RegInitBare value -> (readMaybe (T.unpack value) :: Maybe Int) == Nothing; RegInitText _ -> True-               ]-    aggregateFields aggregate =-        concatMap cmdFields (aggCommands aggregate)-            <> concat [fields | event <- aggEvents aggregate, EventFields fields <- [evBody event]]-    supportedType aggregate ty =-        ty `elem` (["Text", "Int", "Bool", aggName aggregate <> "Vertex"] <> idTypes <> enumTypes)+    scaffoldReplayAudit,+    scaffoldStructural,+    codecComparisonModule,+    codecComparisonBanner,+    bindingSkeletonModules,+    scaffoldAggregate,+    scaffoldProcess,+    scaffoldRouter,+    scaffoldContract,+    scaffoldIntake,+    scaffoldPublisher,+    scaffoldWorkqueue,+    scaffoldReadModel,+    scaffoldRefusals,+    windowSeconds,++    -- * Firewall self-check (M3)+    FirewallSurface (..),+    firewallSurface,+    firewallBreaches,++    -- * Internal resolution, shared with "Keiro.Dsl.Harness"+    Agg (..),+    ResolvedCtor (..),+    StructuralProjection (..),+    resolveAgg,+    projectionSpecs,+    resolveProjectionModules,+    codecMappedDeclarations,+    FieldCat (..),+    fieldCat,+    vertexCtor,+    initialVertex,+    firstEnumCtor,+    lowerFirst,+    pascal,+    pascalFromKebab,+    generatedBanner,+) where++import Data.Char (isAlpha, isAlphaNum, isUpper, ord, toLower, toUpper)+import Data.List (find, groupBy, nub, sort, sortOn)+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe, mapMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.CodecCompare (BranchArm (..), BranchField (..), BranchSchema (..))+import Keiro.Dsl.ExplainBindings (BindingObligation (..), BindingObligationKind (..), bindingObligations)+import Keiro.Dsl.FoldFingerprint (aggregateFoldFingerprint)+import Keiro.Dsl.Grammar+import Keiro.Dsl.ReadModelShape (registryNameFor, subscriptionNameFor)+import Keiro.Dsl.TypeGraph+import Keiro.Dsl.Validate (sagaCategoryError)+import Numeric (showHex)+import Text.Read (readMaybe)++{- | One emitted module: its on-disk path (relative to the scaffold @--out@+directory), its full text, and whether it is overwritten every run+('Generated') or written only when absent ('HoleStub').+-}+data ScaffoldModule = ScaffoldModule+    { modulePath :: !FilePath+    , moduleText :: !Text+    , kind :: !ModuleKind+    , origin :: !Text+    }+    deriving stock (Eq, Show)++data ModuleKind+    = -- | @-- \@generated@; overwritten on every scaffold.+      Generated+    | -- | Hand-owned; created only when absent, never overwritten.+      HoleStub+    deriving stock (Eq, Show)++{- | The threading context: the spec's @context@ name, the chosen output+module-namespace root, and the placement style. Extended additively (never+re-shaped) by later verticals.+-}+data Context = Context+    { contextName :: !Text+    , moduleRoot :: !Text+    -- ^ @""@ means no namespace prefix (the historical default).+    , placement :: !Placement+    -- ^ 'GeneratedPrefix' is the historical default.+    }+    deriving stock (Eq, Show)++{- | A context with today's default placement ('GeneratedPrefix', no root prefix)+for the given @context@ name. Callers that do not care about placement (the+@parse@ path, tests) build their context with this.+-}+defaultContext :: Text -> Context+defaultContext name = Context{contextName = name, moduleRoot = "", placement = GeneratedPrefix}++{- | The generated-layer namespace for a node, honouring the root prefix and the+placement style. The 'Text' argument is the already-pascalised node name (e.g.+@Reservation@, @HospitalSurge@). For 'GeneratedPrefix' this is+@\<root\>.Generated.\<Ctx\>.\<Node\>@ (identical to the historical layout); for+'CollocatedLeaf' it is @\<root\>.\<Ctx\>.\<Node\>.Generated@.+-}+genPrefixFor :: Context -> Text -> Text+genPrefixFor ctx node = case placement ctx of+    GeneratedPrefix -> rootPrefix ctx <> "Generated." <> ctxPascalOf ctx <> "." <> node+    CollocatedLeaf -> rootPrefix ctx <> ctxPascalOf ctx <> "." <> node <> ".Generated"++{- | The hand-owned (hole) namespace for a node: @\<root\>.\<Ctx\>.\<Node\>@ —+the same for both placement styles (holes always sit beside the domain).+-}+holePrefixFor :: Context -> Text -> Text+holePrefixFor ctx node = rootPrefix ctx <> ctxPascalOf ctx <> "." <> node++-- | The root namespace prefix, dot-terminated, or @""@ when no root is set.+rootPrefix :: Context -> Text+rootPrefix ctx = case moduleRoot ctx of r | T.null r -> ""; r -> r <> "."++-- | The context name in PascalCase, e.g. @hospital-capacity@ -> @HospitalCapacity@.+ctxPascalOf :: Context -> Text+ctxPascalOf = pascalFromKebab . contextName++--------------------------------------------------------------------------------+-- Firewall self-check (M3)+--------------------------------------------------------------------------------++{- | The canonical keiki surface forbidden in generated modules. Symbolic+operators are matched as maximal Haskell symbol tokens, identifiers as complete+tokens, qualifiers by their leading module alias, and imports structurally.+-}+data FirewallSurface = FirewallSurface+    { forbiddenSymbolic :: ![Text]+    , forbiddenIdents :: ![Text]+    , forbiddenQualifiers :: ![Text]+    , forbiddenImports :: ![Text]+    , restrictedImports :: ![(Text, [Text])]+    }+    deriving stock (Eq, Show)++firewallSurface :: FirewallSurface+firewallSurface =+    FirewallSurface+        { forbiddenSymbolic = [".==", "./=", ".<", ".<=", ".>", ".>=", ".&&", ".||", ".+", ".-", ".*", "=:", "*:"]+        , forbiddenIdents = ["lit", "pnot", "tadd", "tsub", "tmul"]+        , forbiddenQualifiers = ["B"]+        , forbiddenImports = ["Keiki.Builder", "Keiki.Operators", "Keiki.Symbolic"]+        , -- Generated aggregate modules use the first two names; generated+          -- harnesses validate, step, and replay filled holes register by register.+          restrictedImports =+            [+                ( "Keiki.Core"+                ,+                    [ "RegFile"+                    , "HsPred"+                    , "FieldProjection"+                    , "FieldWitness"+                    , "fieldWitness"+                    , "fieldWitnessAgrees"+                    , "applyEventsEither"+                    , "defaultValidationOptions"+                    , "step"+                    , "validateTransducer"+                    , "!"+                    ]+                )+            ]+        }++{- | Scan generated modules for firewall breaches, returning every offending+@(module path, token, 1-based line number)@. Only modules whose 'kind' is+'Generated' are scanned. Strings and comments are skipped, symbol runs use+maximal munch, and keiki imports are checked independently of token spelling.+-}+firewallBreaches :: [ScaffoldModule] -> [(FilePath, Text, Int)]+firewallBreaches mods =+    [ (modulePath m, breach, n)+    | m <- mods+    , kind m == Generated+    , (n, line) <- zip [1 ..] (T.lines (moduleText m))+    , breach <- lineBreaches line+    ]++lineBreaches :: Text -> [Text]+lineBreaches line = case importModule line of+    Just _ -> importBreaches line+    Nothing -> tokenBreaches (codeTokens line)+  where+    tokenBreaches = mapMaybe breachFor+    breachFor (IdentToken ident)+        | ident `elem` forbiddenIdents firewallSurface = Just ident+    breachFor (QualifiedToken qualifier)+        | qualifier `elem` forbiddenQualifiers firewallSurface = Just (qualifier <> ".*")+    breachFor (SymbolToken symbol)+        | symbol `elem` forbiddenSymbolic firewallSurface = Just symbol+    breachFor _ = Nothing++data CodeToken = IdentToken !Text | QualifiedToken !Text | SymbolToken !Text++codeTokens :: Text -> [CodeToken]+codeTokens = go . T.unpack+  where+    go [] = []+    go ('-' : '-' : _) = []+    go ('"' : rest) = go (dropString rest)+    go ('\'' : rest) = go (dropChar rest)+    go (c : rest)+        | isIdentStart c =+            let (identTail, afterIdent) = span isIdentContinue rest+                ident = T.pack (c : identTail)+             in case afterIdent of+                    '.' : next : more+                        | isUpper c && isIdentStart next ->+                            let (_member, afterMember) = span isIdentContinue more+                             in QualifiedToken ident : go afterMember+                    _ -> IdentToken ident : go afterIdent+        | isSymbolChar c =+            let (symbolTail, afterSymbol) = span isSymbolChar rest+             in SymbolToken (T.pack (c : symbolTail)) : go afterSymbol+        | otherwise = go rest+    isIdentStart c = isAlpha c || c == '_'+    isIdentContinue c = isAlphaNum c || c == '_' || c == '\''+    isSymbolChar c = c `elem` ("!#$%&*+./<=>?@\\^|-~:" :: String)+    dropString [] = []+    dropString ('\\' : _escaped : rest) = dropString rest+    dropString ('"' : rest) = rest+    dropString (_ : rest) = dropString rest+    dropChar [] = []+    dropChar ('\\' : _escaped : rest) = dropChar rest+    dropChar ('\'' : rest) = rest+    dropChar (_ : rest) = dropChar rest++importBreaches :: Text -> [Text]+importBreaches line = case importModule line of+    Nothing -> []+    Just imported+        | imported `elem` forbiddenImports firewallSurface -> ["import:" <> imported]+        | Just allowed <- lookup imported (restrictedImports firewallSurface)+        , not (hasAllowedExplicitImportList allowed line) ->+            ["import:" <> imported]+        | otherwise -> []++importModule :: Text -> Maybe Text+importModule line = case T.words (T.strip line) of+    "import" : rest -> find (T.isPrefixOf "Keiki.") rest+    _ -> Nothing++hasAllowedExplicitImportList :: [Text] -> Text -> Bool+hasAllowedExplicitImportList allowed line =+    case (T.breakOn "(" line, T.breakOnEnd ")" line) of+        ((_, open), (close, _))+            | not (T.null open) && not (T.null close) ->+                let inside = T.takeWhile (/= ')') (T.drop 1 open)+                    names = filter (not . T.null) (T.split (not . isAlphaNum) inside)+                 in all (`elem` allowed) names+        _ -> False++--------------------------------------------------------------------------------+-- Derived naming+--------------------------------------------------------------------------------++-- | Resolved, denormalized view of an aggregate used by every emitter.+data Agg = Agg+    { aContext :: !Context+    , aCtxPascal :: !Text+    , aName :: !Text+    , aLoc :: !Loc+    , aVertexType :: !Text+    , aIds :: ![IdDecl]+    , aEnums :: ![EnumDecl]+    , aRegs :: ![RegDecl]+    , aStates :: ![StateDecl]+    , aCommands :: ![ResolvedCtor]+    , aEvents :: ![ResolvedCtor]+    , aTransitions :: ![Transition]+    , aWire :: !WireSpec+    , aProjection :: !(Maybe ProjectionSpec)+    , aSnapshot :: !(Maybe SnapshotSpec)+    , aFoldFingerprint :: !Text+    , aReadModels :: ![ReadModelNode]+    , aTypeGraph :: !(Maybe TypeGraph)+    , aGenPrefix :: !Text+    -- ^ e.g. @Generated.HospitalCapacity.Reservation@+    , aHolePrefix :: !Text+    -- ^ e.g. @HospitalCapacity.Reservation@+    }++-- | A command or event constructor with its fully-resolved field types.+data ResolvedCtor = ResolvedCtor+    { rcName :: !Text+    , rcFields :: ![(Text, Text)]+    -- ^ (field name, resolved Haskell type)+    , rcVersion :: !Int+    -- ^ EP-2: schema version (1 for commands and unversioned events).+    , rcUpcastFrom :: !(Maybe Int)+    -- ^ EP-2: the source version this event migrates from (the upcaster step).+    }++defaultWire :: WireSpec+defaultWire = WireSpec{wireKind = "ctorName", wireFields = "camelCase", wireSchemaVersion = 1}++resolveAgg :: Context -> Spec -> Aggregate -> Agg+resolveAgg ctx spec agg =+    Agg+        { aContext = ctx+        , aCtxPascal = ctxPascal+        , aName = nm+        , aLoc = aggLoc agg+        , aVertexType = vertexType+        , aIds = specIds spec+        , aEnums = specEnums spec+        , aRegs = aggRegs agg+        , aStates = aggStates agg+        , aCommands = map resolveCommand (aggCommands agg)+        , aEvents = map resolveEvent (aggEvents agg)+        , aTransitions = aggTransitions agg+        , aWire = fromMaybe defaultWire (aggWire agg)+        , aProjection = aggProjection agg+        , aSnapshot = aggSnapshot agg+        , aFoldFingerprint = aggregateFoldFingerprint spec agg+        , aReadModels = [readModel | NReadModel readModel <- specNodes spec]+        , aTypeGraph = either (const Nothing) Just (resolveTypeGraph spec)+        , aGenPrefix = genPrefixFor ctx nm+        , aHolePrefix = holePrefixFor ctx nm+        }+  where+    nm = aggName agg+    ctxPascal = pascalFromKebab (contextName ctx)+    vertexType = nm <> "Vertex"+    commandFieldTypes = [(cmdName c, cmdFields c) | c <- aggCommands agg]+    resolveCommand c = (mkCtor (cmdName c) (cmdFields c)){rcVersion = 1, rcUpcastFrom = Nothing}+    resolveEvent e =+        (mkCtor (evName e) (eventFields e))+            { rcVersion = evVersion e+            , rcUpcastFrom = fst <$> evUpcastFrom e+            }+      where+        eventFields ev = case evBody ev of+            EventFields fs -> fs+            EventFromCommand cn -> fromMaybe [] (lookup cn commandFieldTypes)+    mkCtor cn fs =+        ResolvedCtor+            { rcName = cn+            , rcFields = map (\f -> (fieldName f, resolveFieldType f)) fs+            , rcVersion = 1+            , rcUpcastFrom = Nothing+            }+    regTypes = [(regName r, regType r) | r <- aggRegs agg]+    idNames = map idName (specIds spec)+    enumNames = map enumName (specEnums spec)+    -- A bare field reuses a register's type if one shares its name; else it+    -- Pascal-cases to a declared id/enum/vertex; else falls back to Text.+    resolveFieldType f = case fieldType f of+        Just ty -> ty+        Nothing ->+            let nme = fieldName f+                pas = pascal nme+             in case lookup nme regTypes of+                    Just ty -> ty+                    Nothing+                        | pas `elem` idNames -> pas+                        | pas `elem` enumNames -> pas+                        | pas == vertexType -> pas+                        | otherwise -> "Text"++--------------------------------------------------------------------------------+-- Entry point+--------------------------------------------------------------------------------++{- | Emit the context-level private structural stratum. Shape modules contain+only generated wire representations. The projection facade contains only+schema-derived Keiki field witnesses; neither layer owns consumer behavior.+-}+scaffoldStructural :: Context -> Spec -> [ScaffoldModule]+scaffoldStructural ctx spec = case resolveTypeGraph spec of+    Left _ -> []+    Right graph -> map (shapeModule ctx graph) structural <> projectionModules <> bindingSkeletonModules ctx spec graph+      where+        structural =+            [ (declaration, shape)+            | ResolvedStructural declaration shape <- Map.elems (tgDeclarations graph)+            ]+        projectionModules =+            [ ScaffoldModule+                { modulePath = T.unpack (T.replace "." "/" (structuralProjectionModule ctx) <> ".hs")+                , moduleText = emitStructuralProjections ctx graph+                , kind = Generated+                , origin = "context " <> specContext spec <> " mapped structural facade"+                }+            | not (null (projectionSpecs graph))+            ]++{- | Plan one opt-in, non-production historical-codec comparison module.++The module is intentionally absent from 'scaffoldStructural' and therefore+from production manifests and scaffold records. It must be requested by name+and is compiled only by consumer-owned test/tool components.+-}+codecComparisonModule :: Context -> Spec -> Name -> Either Text ScaffoldModule+codecComparisonModule ctx spec requestedName = do+    graph <- either (Left . ("mapped type graph did not resolve: " <>) . T.pack . show) Right (resolveTypeGraph spec)+    (declaration, shape) <- case Map.lookup (MappedKey requestedName) (tgDeclarations graph) of+        Nothing -> Left ("codec comparison target is not a mapped declaration: " <> requestedName)+        Just (ResolvedOpaque _) ->+            Left+                ( "codec comparison target "+                    <> requestedName+                    <> " is opaque; finite evidence must never upgrade an opaque declaration to a structural claim"+                )+        Just (ResolvedStructural declaration shape) -> Right (declaration, shape)+    owner <- case sortOn aggName (comparisonOwners declaration) of+        [] ->+            Left+                ( "codec comparison target "+                    <> requestedName+                    <> " is not reachable from a persisted private event payload"+                )+        aggregate : _ -> Right aggregate+    let moduleName = structuralPrefix ctx <> ".CodecCompare." <> requestedName+    pure+        ScaffoldModule+            { modulePath = T.unpack (T.replace "." "/" moduleName <> ".hs")+            , moduleText = emitCodecComparison ctx moduleName graph declaration shape owner+            , kind = Generated+            , origin = "non-production codec comparison " <> requestedName+            }+  where+    comparisonOwners declaration =+        [ aggregate+        | NAggregate aggregate <- specNodes spec+        , let resolved = resolveAgg ctx spec aggregate+        , any ((== sdName declaration) . mappedName) (codecMappedDeclarations resolved)+        ]+      where+        mappedName (ResolvedStructural structural _) = sdName structural+        mappedName (ResolvedOpaque opaque) = odName opaque++codecComparisonBanner :: Text+codecComparisonBanner =+    "-- @generated by keiro-dsl codec comparison; non-production migration evidence; do not edit."++emitCodecComparison :: Context -> Text -> TypeGraph -> StructuralDecl -> ResolvedMappedShape -> Aggregate -> Text+emitCodecComparison ctx moduleName graph declaration shape owner =+    nl+        [ "{-# LANGUAGE ImportQualifiedPost #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , ""+        , codecComparisonBanner+        , "-- This module compares historical and generated codecs in consumer-owned tests only."+        , "-- It is never a runtime fallback and never changes the generated codec's authority."+        , "module " <> moduleName <> " (compareWithHistorical) where"+        , ""+        , "import Control.Monad (filterM)"+        , "import Data.Aeson (Value)"+        , "import Data.Aeson qualified as Aeson"+        , "import Data.List (sort)"+        , "import Data.List.NonEmpty qualified as NonEmpty"+        , "import Data.Text (Text)"+        , "import Data.Text qualified"+        , "import " <> codecModule <> " qualified as GeneratedCodec"+        , "import Keiro.Codec.Structural (FixtureCases (..))"+        , "import Keiro.Dsl.CodecCompare"+        , "import Keiro.Dsl.TypeGraph (BindingVersion (..), CanonicalTypeId (..), QualifiedValueName (..))"+        , "import System.Directory (doesFileExist, listDirectory)"+        , "import System.FilePath (takeExtension, (</>))"+        , ""+        , "import " <> fixtureModule <> " qualified as ConsumerFixtures"+        , "import " <> hsModule (sdHaskell declaration) <> " qualified as ConsumerDomain"+        , ""+        , "compareWithHistorical :: HistoricalCodec " <> domainType <> " -> FilePath -> IO CompareReport"+        , "compareWithHistorical historicalCodec goldenDirectory = do"+        , "  names <- sort . filter ((== \".json\") . takeExtension) <$> listDirectory goldenDirectory"+        , "  files <- filterM doesFileExist [goldenDirectory </> name | name <- names]"+        , "  loaded <- traverse (loadGolden historicalCodec) files"+        , "  let inputIssues = [issue | Left issue <- loaded]"+        , "      entries = [entry | Right entry <- loaded]"+        , "      typedCases = NonEmpty.toList (fixtureCases ConsumerFixtures." <> fixtureSymbol <> ")"+        , "      encodeObservations ="+        , "        [ EncodeObservation label (hcEncode historicalCodec value) (GeneratedCodec.encode" <> name <> "Mapped value)"+        , "        | (label, value) <- typedCases"+        , "        ]"+        , "      decodeObservations = [observation | (observation, _) <- entries]"+        , "      typedObserved ="+        , "        concat"+        , "          [ observedBranchesFor FromBinding branchSchema (GeneratedCodec.encode" <> name <> "Mapped value)"+        , "          | (_, value) <- typedCases"+        , "          ]"+        , "      historicalObserved ="+        , "        concat [observedBranchesFor HistoricalGolden branchSchema value | (_, values) <- entries, value <- values]"+        , "      declared = declaredBranchesFor FromBinding branchSchema <> declaredBranchesFor HistoricalGolden branchSchema"+        , "      provenance ="+        , "        CompareProvenance"+        , "          { cpHistoricalCodecIdentity = hcIdentity historicalCodec"+        , "          , cpHistoricalCodecVersion = hcVersion historicalCodec"+        , "          , cpCanonicalType = CanonicalTypeId " <> tshow (unCanonicalTypeId (sdCanonical declaration))+        , "          , cpBindingSymbol = QualifiedValueName " <> tshow (unQualifiedValueName (sdBinding declaration))+        , "          , cpBindingVersion = BindingVersion " <> tshow (unBindingVersion (sdBindingVersion declaration))+        , "          , cpWireFingerprint = " <> tshow (wireFingerprint graph name)+        , "          }"+        , "  pure (compareReport provenance inputIssues (encodeObservations <> decodeObservations) declared (typedObserved <> historicalObserved))"+        , ""+        , "loadGolden :: HistoricalCodec " <> domainType <> " -> FilePath -> IO (Either CompareInputIssue (CompareObservation, [Value]))"+        , "loadGolden historicalCodec path = do"+        , "  decoded <- Aeson.eitherDecodeFileStrict path"+        , "  pure $ case decoded of"+        , "    Left reason -> Left (HistoricalGoldenUnreadable path (fromString reason))"+        , "    Right inputValue ->"+        , "      let historicalDecoded = hcDecode historicalCodec inputValue"+        , "          historicalOutcome = normalizeDecode historicalDecoded"+        , "          generatedOutcome = normalizeDecode (GeneratedCodec.decode" <> name <> "Mapped inputValue)"+        , "          observation = DecodeObservation path inputValue historicalOutcome generatedOutcome"+        , "          coveredValues = case historicalDecoded of"+        , "            Right value -> [inputValue, GeneratedCodec.encode" <> name <> "Mapped value]"+        , "            Left _ -> []"+        , "       in Right (observation, coveredValues)"+        , ""+        , "normalizeDecode :: Either Text " <> domainType <> " -> DecodeOutcome"+        , "normalizeDecode = either DecodeFailed (DecodedShape . GeneratedCodec.encode" <> name <> "Mapped)"+        , ""+        , "fromString :: String -> Text"+        , "fromString = Data.Text.pack"+        , ""+        , "branchSchema :: BranchSchema"+        , "branchSchema = " <> renderBranchSchema (branchSchemaFor graph (ResolvedStructural declaration shape))+        ]+  where+    name = sdName declaration+    domainType = "ConsumerDomain." <> hsType (sdHaskell declaration)+    codecModule = genPrefixFor ctx (aggName owner) <> ".Codec"+    fixtureModule = qualifiedModule (sdFixtures declaration)+    fixtureSymbol = lastSegment (unQualifiedValueName (sdFixtures declaration))++branchSchemaFor :: TypeGraph -> ResolvedMappedDecl -> BranchSchema+branchSchemaFor graph =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \_ shape ->+                foldMappedShape+                    MappedShapeAlgebra+                        { onRecord = \_ _ fields ->+                            BranchRecord+                                [ BranchField+                                    (rwfKey field)+                                    (rwfPresence field == POptional)+                                    (branchExpr graph (rwfType field))+                                | field <- fields+                                ]+                        , onEnum = const BranchScalar+                        , onUnion = \encoding arms ->+                            BranchUnion+                                (ueTagField encoding)+                                (ueContentsField encoding)+                                [BranchArm (rwaTag arm) (branchExpr graph <$> rwaPayload arm) | arm <- arms]+                        }+                    shape+            , onOpaqueDecl = const BranchScalar+            }++branchExpr :: TypeGraph -> ResolvedTypeExpr -> BranchSchema+branchExpr graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = BranchScalar+            , onInt = BranchScalar+            , onBool = BranchScalar+            , onNatural = BranchScalar+            , onTime = BranchScalar+            , onJson = BranchScalar+            , onOptional = BranchOptional+            , onList = BranchList+            , onMap = BranchMap+            , onRef = \key -> maybe BranchScalar (branchSchemaFor graph) (Map.lookup key (tgDeclarations graph))+            }++renderBranchSchema :: BranchSchema -> Text+renderBranchSchema schema = case schema of+    BranchScalar -> "BranchScalar"+    BranchOptional nested -> "BranchOptional (" <> renderBranchSchema nested <> ")"+    BranchList nested -> "BranchList (" <> renderBranchSchema nested <> ")"+    BranchMap nested -> "BranchMap (" <> renderBranchSchema nested <> ")"+    BranchRecord fields ->+        "BranchRecord ["+            <> T.intercalate+                ", "+                [ "BranchField "+                    <> tshow (bfWireKey field)+                    <> " "+                    <> (if bfPresenceOptional field then "True" else "False")+                    <> " ("+                    <> renderBranchSchema (bfSchema field)+                    <> ")"+                | field <- fields+                ]+            <> "]"+    BranchUnion tagField contentsField arms ->+        "BranchUnion "+            <> tshow tagField+            <> " "+            <> tshow contentsField+            <> " ["+            <> T.intercalate+                ", "+                [ "BranchArm "+                    <> tshow (baWireTag arm)+                    <> " "+                    <> maybe "Nothing" (\nested -> "(Just (" <> renderBranchSchema nested <> "))") (baPayloadSchema arm)+                | arm <- arms+                ]+            <> "]"++{- | Emit one create-once consumer module per distinct qualified obligation+owner. Multiple mapped declarations may intentionally share a leaf binding+module, so grouping happens by module rather than by declaration.+-}+bindingSkeletonModules :: Context -> Spec -> TypeGraph -> [ScaffoldModule]+bindingSkeletonModules ctx spec graph = case bindingObligations spec of+    Left _ -> []+    Right obligations ->+        [ emitBindingSkeleton ctx graph owner entries+        | (owner, entries) <- Map.toAscList (Map.fromListWith (<>) [(obligationModule obligation, [obligation]) | obligation <- obligations])+        ]++emitBindingSkeleton :: Context -> TypeGraph -> Text -> [BindingObligation] -> ScaffoldModule+emitBindingSkeleton ctx graph owner obligations =+    ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" owner <> ".hs")+        , moduleText =+            nl $+                [ "{-# LANGUAGE LambdaCase #-}"+                , ""+                , "-- This is a HAND-OWNED structural binding skeleton. keiro-dsl creates it once"+                , "-- and never overwrites it. Fill each HOLE and run the generated harness."+                , "module " <> owner <> " ("+                ]+                    <> exportLines+                    <> [") where", ""]+                    <> map ("import " <>) imports+                    <> [""]+                    <> intercalateBlank (map renderObligation obligations)+        , kind = HoleStub+        , origin = "mapped structural binding skeleton " <> owner+        }+  where+    exportLines =+        [ (if index == (0 :: Int) then "    " else "  , ") <> obligationSymbol obligation+        | (index, obligation) <- zip [0 ..] obligations+        ]+    imports =+        sort . nub $+            [ hsModule (sdHaskell declaration) <> " qualified"+            | obligation <- obligations+            , Just (declaration, _) <- [structuralFor obligation]+            ]+                <> [ structuralShapeModule ctx (sdName declaration) <> " qualified"+                   | obligation <- obligations+                   , obligationKind obligation == BindingValue+                   , Just (declaration, _) <- [structuralFor obligation]+                   ]+                <> [ "Keiro.Codec.Structural (FixtureCases, StructuralBinding (..))"+                   | any ((`elem` [BindingValue, FixtureValue]) . obligationKind) obligations+                   ]+    renderObligation obligation = case structuralFor obligation of+        Nothing -> ["-- HOLE: declaration disappeared before skeleton rendering"]+        Just (declaration, shape) -> case obligationKind obligation of+            BindingValue -> renderBinding ctx declaration shape obligation+            FixtureValue ->+                [ "-- HOLE: provide deterministic labelled conformance fixtures for " <> sdName declaration+                , obligationSignature obligation+                , obligationSymbol obligation <> " = error " <> tshow ("HOLE: fill " <> sdName declaration <> " fixtures")+                ]+            InitialValue ->+                [ "-- HOLE: provide the initial register value for " <> sdName declaration+                , obligationSignature obligation+                , obligationSymbol obligation <> " = error " <> tshow ("HOLE: fill " <> sdName declaration <> " initial value")+                ]+    structuralFor obligation = case Map.lookup (MappedKey (obligationMappedName obligation)) (tgDeclarations graph) of+        Just (ResolvedStructural declaration shape) -> Just (declaration, shape)+        _ -> Nothing+    intercalateBlank [] = []+    intercalateBlank (section : rest) = section <> concatMap ("" :) rest++renderBinding :: Context -> StructuralDecl -> ResolvedMappedShape -> BindingObligation -> [Text]+renderBinding ctx declaration shape obligation =+    [ "-- HOLE: complete both total directions; wire policy remains in the generated codec."+    , obligationSymbol obligation <> " :: StructuralBinding " <> domainType <> " " <> shapeType+    , obligationSymbol obligation <> " ="+    , "  StructuralBinding"+    , "    { bindingToShape = \\case"+    ]+        <> indentCases (bindingCases True)+        <> ["    , bindingFromShape = \\case"]+        <> indentCases (bindingCases False)+        <> ["    }"]+  where+    domainModule = hsModule (sdHaskell declaration)+    domainType = domainModule <> "." <> hsType (sdHaskell declaration)+    shapeModuleName = structuralShapeModule ctx (sdName declaration)+    shapeType = shapeModuleName <> "." <> sdName declaration <> "Shape"+    domainCtor constructor = domainModule <> "." <> constructor+    shapeCtor constructor = shapeModuleName <> "." <> constructor+    indentCases = map ("      " <>)+    bindingCases toShapeDirection =+        foldMappedShape+            MappedShapeAlgebra+                { onRecord = \constructor _ fields -> [recordCase toShapeDirection constructor fields]+                , onEnum = \entries -> map (enumCase toShapeDirection . weCtor) entries+                , onUnion = \_ arms -> map (unionCase toShapeDirection) arms+                }+            shape+    recordCase toShapeDirection constructor fields =+        sourceCtor+            <> arguments variables+            <> " -> "+            <> targetCtor+            <> arguments (map (holeFor toShapeDirection . rwfHaskell) fields)+      where+        variables = map (("_" <>) . (<> "Value") . rwfHaskell) fields+        sourceCtor = if toShapeDirection then domainCtor constructor else shapeCtor constructor+        targetCtor = if toShapeDirection then shapeCtor constructor else domainCtor constructor+    enumCase toShapeDirection constructor =+        sourceCtor <> " -> " <> holeFor toShapeDirection constructor+      where+        sourceCtor = if toShapeDirection then domainCtor constructor else shapeCtor constructor+    unionCase toShapeDirection arm =+        sourceCtor+            <> maybe "" (const " _payloadValue") (rwaPayload arm)+            <> " -> "+            <> case rwaPayload arm of+                Nothing -> holeFor toShapeDirection (rwaCtor arm)+                Just _ -> targetCtor <> " " <> holeFor toShapeDirection (rwaCtor arm <> ".payload")+      where+        sourceCtor = if toShapeDirection then domainCtor (rwaCtor arm) else shapeCtor (rwaCtor arm)+        targetCtor = if toShapeDirection then shapeCtor (rwaCtor arm) else domainCtor (rwaCtor arm)+    arguments [] = ""+    arguments values = " " <> T.unwords values+    holeFor toShapeDirection fieldName =+        "(error "+            <> tshow+                ( "HOLE: fill "+                    <> sdName declaration+                    <> (if toShapeDirection then " bindingToShape." else " bindingFromShape.")+                    <> fieldName+                )+            <> ")"++shapeModule :: Context -> TypeGraph -> (StructuralDecl, ResolvedMappedShape) -> ScaffoldModule+shapeModule ctx graph (declaration, shape) =+    ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" (structuralShapeModule ctx (sdName declaration)) <> ".hs")+        , moduleText = emitShape ctx graph declaration shape+        , kind = Generated+        , origin = nodeOrigin "mapped structural" (sdName declaration) (sdLoc declaration)+        }++structuralPrefix :: Context -> Text+structuralPrefix ctx = case placement ctx of+    GeneratedPrefix -> rootPrefix ctx <> "Generated." <> ctxPascalOf ctx <> ".Structural"+    CollocatedLeaf -> rootPrefix ctx <> ctxPascalOf ctx <> ".Generated.Structural"++structuralShapeModule :: Context -> Name -> Text+structuralShapeModule ctx name = structuralPrefix ctx <> ".Shape." <> name++structuralProjectionModule :: Context -> Text+structuralProjectionModule ctx = structuralPrefix ctx <> "Projections"++emitShape :: Context -> TypeGraph -> StructuralDecl -> ResolvedMappedShape -> Text+emitShape ctx graph declaration shape =+    nl $+        languagePragmas+            <> [ generatedBanner+               , "module " <> moduleName <> " (" <> shapeType <> " (..)) where"+               , ""+               ]+            <> map ("import " <>) imports+            <> ["" | not (null imports)]+            <> [shapeDeclaration]+  where+    moduleName = structuralShapeModule ctx (sdName declaration)+    shapeType = sdName declaration <> "Shape"+    requirements = shapeRequirements ctx graph shape+    languagePragmas =+        ["{-# LANGUAGE DeriveGeneric #-}"]+            <> ["{-# LANGUAGE DuplicateRecordFields #-}" | shapeHasRecord shape]+    imports =+        sort . nub $+            ["Data.Aeson (Value)" | ReqJson `elem` requirements]+                <> ["Data.Map.Strict (Map)" | ReqMap `elem` requirements]+                <> ["Data.Text (Text)" | ReqText `elem` requirements]+                <> ["Data.Time (UTCTime)" | ReqTime `elem` requirements]+                <> ["GHC.Generics (Generic)"]+                <> ["Numeric.Natural (Natural)" | ReqNatural `elem` requirements]+                <> [m <> " qualified" | ReqModule m <- requirements]+    shapeDeclaration =+        foldMappedShape+            MappedShapeAlgebra+                { onRecord = \constructor _ fields ->+                    nl $+                        ["data " <> shapeType <> " = " <> constructor]+                            <> recordFields+                                [ (rwfHaskell field, renderShapeType ctx graph (rwfType field))+                                | field <- fields+                                ]+                            <> ["  deriving stock (Eq, Generic, Show)"]+                , onEnum = \entries ->+                    "data "+                        <> shapeType+                        <> " = "+                        <> T.intercalate " | " (map weCtor entries)+                        <> "\n  deriving stock (Eq, Generic, Show)"+                , onUnion = \_ arms ->+                    nl $+                        case arms of+                            [] -> ["data " <> shapeType <> " = " <> shapeType <> "Empty", "  deriving stock (Eq, Generic, Show)"]+                            firstArm : rest ->+                                ["data " <> shapeType <> " = " <> renderArm firstArm]+                                    <> ["  | " <> renderArm arm | arm <- rest]+                                    <> ["  deriving stock (Eq, Generic, Show)"]+                }+            shape+    renderArm arm = rwaCtor arm <> maybe "" ((" !" <>) . renderShapeType ctx graph) (rwaPayload arm)++data ShapeRequirement+    = ReqJson+    | ReqMap+    | ReqText+    | ReqTime+    | ReqNatural+    | ReqModule !Text+    deriving stock (Eq, Ord, Show)++shapeHasRecord :: ResolvedMappedShape -> Bool+shapeHasRecord =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ _ -> True+            , onEnum = const False+            , onUnion = \_ _ -> False+            }++shapeRequirements :: Context -> TypeGraph -> ResolvedMappedShape -> [ShapeRequirement]+shapeRequirements ctx graph =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields -> concatMap (exprRequirements ctx graph . rwfType) fields+            , onEnum = const []+            , onUnion = \_ arms -> concatMap (maybe [] (exprRequirements ctx graph) . rwaPayload) arms+            }++exprRequirements :: Context -> TypeGraph -> ResolvedTypeExpr -> [ShapeRequirement]+exprRequirements ctx graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = [ReqText]+            , onInt = []+            , onBool = []+            , onNatural = [ReqNatural]+            , onTime = [ReqTime]+            , onJson = [ReqJson]+            , onOptional = id+            , onList = id+            , onMap = (ReqMap :) . (ReqText :)+            , onRef = \key -> case Map.lookup key (tgDeclarations graph) of+                Just (ResolvedStructural declaration _) -> [ReqModule (structuralShapeModule ctx (sdName declaration))]+                Just (ResolvedOpaque declaration) -> [ReqModule (hsModule (odHaskell declaration))]+                Nothing -> []+            }++renderShapeType :: Context -> TypeGraph -> ResolvedTypeExpr -> Text+renderShapeType ctx graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = "Text"+            , onInt = "Int"+            , onBool = "Bool"+            , onNatural = "Natural"+            , onTime = "UTCTime"+            , onJson = "Value"+            , onOptional = \value -> "(Maybe (" <> value <> "))"+            , onList = \value -> "([" <> value <> "])"+            , onMap = \value -> "(Map Text (" <> value <> "))"+            , onRef = \key -> case Map.lookup key (tgDeclarations graph) of+                Just (ResolvedStructural nested _) ->+                    structuralShapeModule ctx (sdName nested) <> "." <> sdName nested <> "Shape"+                Just (ResolvedOpaque opaque) ->+                    hsModule (odHaskell opaque) <> "." <> hsType (odHaskell opaque)+                Nothing -> "()"+            }++data StructuralProjection = StructuralProjection+    { spTag :: !Text+    , spWitness :: !Text+    , spPointer :: !Text+    , spOwner :: !HaskellSource+    , spResult :: !Text+    , spCanonical :: !CanonicalTypeId+    , spBinding :: !QualifiedValueName+    , spSelectors :: ![(Text, Text)]+    }+    deriving stock (Eq, Show)++projectionSpecs :: TypeGraph -> [StructuralProjection]+projectionSpecs graph =+    sortOn spTag . concat $+        [ projectionsForRoot graph declaration shape+        | ResolvedStructural declaration shape <- Map.elems (tgDeclarations graph)+        ]++projectionsForRoot :: TypeGraph -> StructuralDecl -> ResolvedMappedShape -> [StructuralProjection]+projectionsForRoot graph root rootShape = case rootShape of+    RRecord _ _ fields -> concatMap (walkField [] []) fields+    REnum{} -> []+    RUnion{} -> []+  where+    walkField keys selectors field+        | rwfPresence field /= PRequired = []+        | otherwise = case projectionScalar (rwfType field) of+            Just result -> [mkProjection (keys <> [rwfKey field]) (selectors <> [(shapeModuleForOwner, rwfHaskell field)]) result]+            Nothing -> case rwfType field of+                RRef key -> case Map.lookup key (tgDeclarations graph) of+                    Just (ResolvedStructural nested (RRecord _ _ nestedFields)) ->+                        concatMap+                            (walkNested nested (keys <> [rwfKey field]) (selectors <> [(shapeModuleForOwner, rwfHaskell field)]))+                            nestedFields+                    _ -> []+                _ -> []+      where+        shapeModuleForOwner = "__SHAPE__." <> sdName root++    walkNested owner keys selectors field+        | rwfPresence field /= PRequired = []+        | otherwise = case projectionScalar (rwfType field) of+            Just result -> [mkProjection (keys <> [rwfKey field]) (selectors <> [(shapeModuleFor owner, rwfHaskell field)]) result]+            Nothing -> case rwfType field of+                RRef key -> case Map.lookup key (tgDeclarations graph) of+                    Just (ResolvedStructural nested (RRecord _ _ nestedFields)) ->+                        concatMap+                            (walkNested nested (keys <> [rwfKey field]) (selectors <> [(shapeModuleFor owner, rwfHaskell field)]))+                            nestedFields+                    _ -> []+                _ -> []++    -- Context is supplied when rendering; this marker is replaced there.+    shapeModuleFor declaration = "__SHAPE__." <> sdName declaration+    mkProjection keys selectors result =+        StructuralProjection+            { spTag = projectionTag (sdName root) pointer+            , spWitness = lowerFirst (projectionTag (sdName root) pointer) <> "Witness"+            , spPointer = pointer+            , spOwner = sdHaskell root+            , spResult = result+            , spCanonical = sdCanonical root+            , spBinding = sdBinding root+            , spSelectors = selectors+            }+      where+        pointer = T.concat ["/" <> escapePointer key | key <- keys]++projectionScalar :: ResolvedTypeExpr -> Maybe Text+projectionScalar = \case+    RText -> Just "Text"+    RInt -> Just "Int"+    RBool -> Just "Bool"+    RTime -> Just "UTCTime"+    RNatural -> Nothing+    RJson -> Nothing+    ROptional{} -> Nothing+    RList{} -> Nothing+    RMap{} -> Nothing+    RRef{} -> Nothing++escapePointer :: Text -> Text+escapePointer = T.replace "/" "~1" . T.replace "~" "~0"++projectionTag :: Name -> Text -> Text+projectionTag owner pointer = "StructuralProjection" <> encodeIdentifier (owner <> pointer)++encodeIdentifier :: Text -> Text+encodeIdentifier = T.concatMap (\character -> "C" <> T.pack (showHex (ord character) "") <> "Z")++emitStructuralProjections :: Context -> TypeGraph -> Text+emitStructuralProjections ctx graph =+    nl $+        [ "{-# LANGUAGE DataKinds #-}"+        , "{-# LANGUAGE TypeApplications #-}"+        , "{-# LANGUAGE TypeFamilies #-}"+        , generatedBanner+        , "-- Equality witnesses are emitted for Text, Int, Bool, and UTCTime."+        , "-- Only Int and UTCTime belong to Keiki's v1 ordered subset."+        , "module " <> moduleName+        , "  ( " <> T.intercalate "\n  , " (map spWitness specs)+        , "  ) where"+        , ""+        , "import Data.Text (Text)"+        , "import Data.Time (UTCTime)"+        , "import Keiro.Codec.Structural (bindingToShape)"+        , "import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)"+        ]+            <> map ("import " <>) imports+            <> concatMap renderProjection specs+  where+    moduleName = structuralProjectionModule ctx+    specs = map (resolveProjectionModules ctx) (projectionSpecs graph)+    imports =+        sort . nub $+            [hsModule (spOwner spec) <> " qualified" | spec <- specs]+                <> [qualifiedModule (spBinding spec) <> " qualified" | spec <- specs]+                <> [shapeModuleName <> " qualified" | spec <- specs, (shapeModuleName, _) <- spSelectors spec]+    renderProjection spec =+        [ ""+        , "data " <> spTag spec+        , ""+        , "instance FieldProjection " <> spTag spec <> " where"+        , "  type FieldName " <> spTag spec <> " = " <> tshow (spPointer spec)+        , "  type FieldOwner " <> spTag spec <> " = " <> renderHaskellSource (spOwner spec)+        , "  type FieldResult " <> spTag spec <> " = " <> spResult spec+        , "  fieldShapeId _ = " <> tshow (unCanonicalTypeId (spCanonical spec))+        , "  projectFieldValue _ owner = " <> renderGetter spec+        , ""+        , spWitness spec <> " :: FieldWitness " <> spTag spec+        , spWitness spec <> " = fieldWitness @" <> spTag spec+        ]+    renderGetter spec =+        foldl+            (\value (shapeModuleName, selector) -> shapeModuleName <> "." <> selector <> " (" <> value <> ")")+            ("bindingToShape " <> unQualifiedValueName (spBinding spec) <> " owner")+            (spSelectors spec)++resolveProjectionModules :: Context -> StructuralProjection -> StructuralProjection+resolveProjectionModules ctx spec =+    spec+        { spSelectors =+            [ (replaceModule marker, selector)+            | (marker, selector) <- spSelectors spec+            ]+        }+  where+    replaceModule marker+        | Just name <- T.stripPrefix "__SHAPE__." marker = structuralShapeModule ctx name+        | otherwise = structuralShapeModule ctx (lastSegment marker)++qualifiedModule :: QualifiedValueName -> Text+qualifiedModule = fst . splitQualified . unQualifiedValueName++renderHaskellSource :: HaskellSource -> Text+renderHaskellSource source = hsModule source <> "." <> hsType source++splitQualified :: Text -> (Text, Text)+splitQualified value =+    let (prefix, name) = T.breakOnEnd "." value+     in (T.dropEnd 1 prefix, name)++lastSegment :: Text -> Text+lastSegment = snd . T.breakOnEnd "."++{- | Emit all modules for one aggregate. The 'Spec' is needed for the shared+id\/enum declarations.+-}+scaffoldAggregate :: Context -> Spec -> Aggregate -> [ScaffoldModule]+scaffoldAggregate ctx spec agg =+    [ genModule a "Domain" (emitDomain a)+    , genModule a "Codec" (emitCodec a)+    , genModule a "EventStream" (emitEventStream a)+    , genModule a "Projection" (emitProjection a)+    , holeModule a (emitHoles a)+    ]+  where+    a = resolveAgg ctx spec agg++{- | Emit the context-wide replay-audit target assembly.++There is one existential target per aggregate declaration. Process saga+aggregates are ordinary aggregate nodes referenced by 'SagaRef', so they are+included by the same single source of truth rather than being duplicated from+the process declaration.+-}+scaffoldReplayAudit :: Context -> Spec -> [ScaffoldModule]+scaffoldReplayAudit ctx spec+    | null aggregates = []+    | otherwise =+        [ ScaffoldModule+            { modulePath = T.unpack (T.replace "." "/" moduleName <> ".hs")+            , moduleText = emitReplayAudit+            , kind = Generated+            , origin = "context " <> specContext spec <> " replay-audit assembly"+            }+        ]+  where+    aggregates = [aggregate | NAggregate aggregate <- specNodes spec]+    moduleName = contextGeneratedPrefix ctx <> ".ReplayAudit"+    contextGeneratedPrefix context = case placement context of+        GeneratedPrefix -> rootPrefix context <> "Generated." <> ctxPascalOf context+        CollocatedLeaf -> rootPrefix context <> ctxPascalOf context <> ".Generated"+    emitReplayAudit =+        nl $+            [ "{-# LANGUAGE GADTs #-}"+            , generatedBanner+            , "--"+            , "-- Deployment contract:"+            , "--   * replay-neutral diff: no data audit is required;"+            , "--   * affected diff: run AuditTargeted with the emitted affected set"+            , "--     against a production copy under the candidate binary;"+            , "--   * one-time runtime cutover: run AuditFull;"+            , "--   * any non-zero audit exit blocks deployment."+            , "module " <> moduleName <> " (auditTargets) where"+            , ""+            ]+                ++ [ "import " <> genPrefixFor ctx (aggName aggregate) <> ".EventStream qualified as " <> aggName aggregate+                   | aggregate <- aggregates+                   ]+                ++ [ "import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)"+                   , "import Keiro.Stream qualified as Stream"+                   , ""+                   , "auditTargets :: [SomeAuditTarget]"+                   , "auditTargets ="+                   ]+                ++ concat+                    [ [ if index == (0 :: Int) then "  [ SomeAuditTarget" else "  , SomeAuditTarget"+                      , "      AuditTarget"+                      , "        { eventStream = " <> aggregateName <> "." <> lowerFirst aggregateName <> "EventStream"+                      , "        , category = Stream.categoryText " <> aggregateName <> "." <> lowerFirst aggregateName <> "Category"+                      , "        , mkStream = streamInCategory (Stream.categoryText " <> aggregateName <> "." <> lowerFirst aggregateName <> "Category)"+                      , "        }"+                      ]+                    | (index, aggregate) <- zip [0 ..] aggregates+                    , let aggregateName = aggName aggregate+                    ]+                ++ ["  ]"]++genModule :: Agg -> Text -> Text -> ScaffoldModule+genModule a name body =+    ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" (aGenPrefix a) <> "/" <> name <> ".hs")+        , moduleText = body+        , kind = Generated+        , origin = nodeOrigin "aggregate" (aName a) (aLoc a)+        }++holeModule :: Agg -> Text -> ScaffoldModule+holeModule a body =+    ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" (aHolePrefix a) <> "/" <> "Holes.hs")+        , moduleText = body+        , kind = HoleStub+        , origin = nodeOrigin "aggregate" (aName a) (aLoc a)+        }++--------------------------------------------------------------------------------+-- Integration contract (EP-4): a self-contained payload ADT + codec+--------------------------------------------------------------------------------++{- | Emit the deterministic, symbol-free contract layer: a payload ADT+(per-event records), the topic constants, the @messageType@ discriminator, and a+strict encode\/decode keyed by it. Self-contained (base\/text\/aeson), so it+compiles standalone — the cross-service schema both producer and consumer agree+on. No keiki symbolic operator (firewall holds).+-}+scaffoldContract :: Context -> ContractNode -> [ScaffoldModule]+scaffoldContract ctx c =+    [ ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Contract.hs")+        , moduleText = emitContractGen genPrefix c+        , kind = Generated+        , origin = nodeOrigin "contract" (ctrName c) (ctrLoc c)+        }+    ]+  where+    genPrefix = genPrefixFor ctx (pascal (ctrName c))++emitContractGen :: Text -> ContractNode -> Text+emitContractGen genPrefix c =+    nl $+        [ "{-# LANGUAGE DuplicateRecordFields #-}"+        , "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Contract"+        , "  ( " <> payloadTy <> " (..)"+        , nl ["  , " <> ceName e <> "Data (..)" | e <- ctrEvents c]+        , "  , messageTypeOf"+        , "  , encode" <> payloadTy+        , "  , parse" <> payloadTy+        , "  ) where"+        , ""+        , "import Data.Aeson (Value, object, withObject, (.:), (.=))"+        , "import Data.Aeson.Types (Parser, parseEither)"+        , "import Data.Text (Text)"+        , "import qualified Data.Text as T"+        , ""+        , "-- topic constants"+        ]+            ++ [lowerFirst alias <> "Topic :: Text\n" <> lowerFirst alias <> "Topic = " <> tshow t | (alias, t) <- ctrTopics c]+            ++ [ ""+               , "-- the closed payload set (discriminated by " <> tshow (ctrDiscriminator c) <> ")"+               ]+            ++ [emitPayloadAdt payloadTy (ctrEvents c)]+            ++ [ ""+               , "messageTypeOf :: " <> payloadTy <> " -> Text"+               , "messageTypeOf = \\case"+               ]+            ++ ["  " <> ceName e <> " {} -> " <> tshow (ceName e) | e <- ctrEvents c]+            ++ [ ""+               , "encode" <> payloadTy <> " :: " <> payloadTy <> " -> Value"+               , "encode" <> payloadTy <> " = \\case"+               ]+            ++ concatMap encodeArm (ctrEvents c)+            ++ [ ""+               , "parse" <> payloadTy <> " :: Value -> Either Text " <> payloadTy+               , "parse" <> payloadTy <> " = mapLeftText . parseEither (withObject " <> tshow payloadTy <> " go)"+               , "  where"+               , "    go o = do"+               , "      kind <- o .: " <> tshow (ctrDiscriminator c) <> " :: Parser Text"+               , "      case kind of"+               ]+            ++ concatMap decodeArm (ctrEvents c)+            ++ [ "        _ -> fail \"unknown message type\""+               , ""+               , "mapLeftText :: Either String b -> Either Text b"+               , "mapLeftText = either (Left . T.pack) Right"+               ]+  where+    payloadTy = pascal (ctrName c) <> "Payload"+    encodeArm e =+        [ "  " <> ceName e <> " payload ->"+        , "    object"+        ]+            ++ [lead i kv | (i, kv) <- zip [(0 :: Int) ..] ((tshow (ctrDiscriminator c) <> " .= (" <> tshow (ceName e) <> " :: Text)") : [tshow (cfName f) <> " .= payload." <> cfName f | f <- ceFields e])]+            ++ ["      ]"]+    lead 0 kv = "      [ " <> kv+    lead _ kv = "      , " <> kv+    decodeArm e =+        [ "        " <> tshow (ceName e) <> " ->"+        , "          " <> ceName e <> " <$> (" <> ceName e <> "Data" <> fieldApps (ceFields e) <> ")"+        ]+    fieldApps [] = ""+    fieldApps fs = " <$> " <> T.intercalate " <*> " ["o .: " <> tshow (cfName f) | f <- fs]++emitPayloadAdt :: Text -> [ContractEvent] -> Text+emitPayloadAdt tyName events =+    sectionsOf [map dataRecord events, [sumDecl]]+  where+    hsType CText = "Text"+    hsType CInt = "Int"+    hsType (CTypeId _) = "Text"+    dataRecord e =+        "data "+            <> ceName e+            <> "Data = "+            <> ceName e+            <> "Data { "+            <> T.intercalate ", " [cfName f <> " :: !" <> hsType (cfType f) | f <- ceFields e]+            <> " }\n  deriving stock (Eq, Show)"+    arm e = ceName e <> " !" <> ceName e <> "Data"+    sumDecl = case events of+        [] -> "data " <> tyName <> " = " <> tyName <> "Empty\n  deriving stock (Eq, Show)"+        (e : es) ->+            nl $+                ["data " <> tyName <> " = " <> arm e]+                    ++ ["  | " <> arm e2 | e2 <- es]+                    ++ ["  deriving stock (Eq, Show)"]++--------------------------------------------------------------------------------+-- Integration intake (EP-4): inbox disposition vs the live Keiro.Inbox runtime+--------------------------------------------------------------------------------++{- | Emit the inbox node's deterministic disposition wiring compiled against the+LIVE @Keiro.Inbox.Types@: the dedupe policy (a real 'InboxDedupePolicy') and a+disposition function over the real @InboxResult@ (Processed\/Duplicate\/+InProgress\/PreviouslyFailed). This pins the dangerous inversions+(duplicate ⇒ ackOk, previouslyFailed ⇒ deadLetter) as compiled code over the+runtime types. The handler-level decode\/dedupe\/store failures are noted but not+part of @InboxResult@. Firewall holds (no keiki symbolic operator).+-}+scaffoldIntake :: Context -> IntakeNode -> [ScaffoldModule]+scaffoldIntake ctx i =+    [ ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Inbox.hs")+        , moduleText = emitIntakeGen genPrefix i+        , kind = Generated+        , origin = nodeOrigin "intake" (inkName i) (inkLoc i)+        }+    ]+  where+    genPrefix = genPrefixFor ctx (pascal (inkName i))++emitIntakeGen :: Text -> IntakeNode -> Text+emitIntakeGen genPrefix i =+    nl+        [ "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Inbox"+        , "  ( InboxAck (..)"+        , "  , inboxDedupePolicy"+        , "  , inboxPersistence"+        , "  , inboxDisposition"+        , "  ) where"+        , ""+        , "import Keiro.Inbox.Types (InboxDedupePolicy (..), InboxPersistence (..), InboxResult (..))"+        , ""+        , "-- The dedupe policy (hole-kind 4), lowered to the live InboxDedupePolicy."+        , "inboxDedupePolicy :: InboxDedupePolicy"+        , "inboxDedupePolicy = " <> inkDedupePolicy i+        , ""+        , "{- | Success-path envelope retention passed to runInboxTransactionWith."+        , "Failures always retain their full operator-facing dead-letter envelope."+        , "Dedupe-only success rows decode with an empty payload."+        , "-}"+        , "inboxPersistence :: InboxPersistence"+        , "inboxPersistence = " <> persistenceCtor (inkPersist i)+        , ""+        , "-- The service's ack decision for each inbox classification."+        , "data InboxAck = InboxAckOk | InboxRetry | InboxDeadLetter"+        , "  deriving stock (Eq, Show)"+        , ""+        , "-- The disposition table (hole-kind 2) over the LIVE Keiro.Inbox.Types.InboxResult."+        , "-- duplicate => ackOk and previouslyFailed => deadLetter are the dangerous"+        , "-- inversions the spec states explicitly."+        , "inboxDisposition :: InboxResult a -> InboxAck"+        , "inboxDisposition r = case r of"+        , "  InboxProcessed _ -> " <> ackFor "processed"+        , "  InboxDuplicate -> " <> ackFor "duplicate"+        , "  InboxInProgress -> " <> ackFor "inProgress"+        , "  InboxPreviouslyFailed _ -> " <> ackFor "previouslyFailed"+        , ""+        , "-- handler-level failures (not InboxResult): decodeFailed => "+            <> ackText "decodeFailed"+            <> ", dedupeFailed => "+            <> ackText "dedupeFailed"+            <> ", storeFailed => "+            <> ackText "storeFailed"+        ]+  where+    act o = lookup o [(drOutcome r, drAction r) | r <- inkDisposition i]+    ackFor o = case act o of+        Just IAckOk -> "InboxAckOk"+        Just (IRetry _) -> "InboxRetry"+        Just (IDeadLetter _) -> "InboxDeadLetter"+        Nothing -> "InboxRetry"+    ackText o = case act o of+        Just IAckOk -> "ackOk"+        Just (IRetry _) -> "retry"+        Just (IDeadLetter _) -> "deadLetter"+        Nothing -> "retry"+    persistenceCtor InkPersistFull = "PersistFullEnvelope"+    persistenceCtor InkPersistDedupeOnly = "PersistDedupeOnly"++--------------------------------------------------------------------------------+-- Integration publisher (EP-4): config vs the live Keiro.Outbox runtime+--------------------------------------------------------------------------------++{- | Emit the publisher's at-least-once policy compiled against the LIVE+@Keiro.Outbox.Types@: the ordering policy (a real 'OrderingPolicy'), the backoff+curve (a real 'BackoffSchedule'), and the max-attempts ceiling. Firewall holds.+-}+scaffoldPublisher :: Context -> PublisherNode -> [ScaffoldModule]+scaffoldPublisher ctx pb =+    [ ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Publisher.hs")+        , moduleText = emitPublisherGen genPrefix pb+        , kind = Generated+        , origin = nodeOrigin "publisher" (pubName pb) (pubLoc pb)+        }+    ]+  where+    genPrefix = genPrefixFor ctx (pascal (pubName pb))++emitPublisherGen :: Text -> PublisherNode -> Text+emitPublisherGen genPrefix pb =+    nl+        [ "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Publisher"+        , "  ( publisherOrdering"+        , "  , publisherBackoff"+        , "  , publisherMaxAttempts"+        , "  ) where"+        , ""+        , "import Keiro.Outbox.Types (BackoffSchedule (..), ExponentialBackoffOptions (..), OrderingPolicy (..))"+        , ""+        , "publisherOrdering :: OrderingPolicy"+        , "publisherOrdering = " <> pubOrdering pb+        , ""+        , "publisherBackoff :: BackoffSchedule"+        , "publisherBackoff = " <> backoffExpr (pubBackoff pb)+        , ""+        , "publisherMaxAttempts :: Int"+        , "publisherMaxAttempts = " <> tshow' (pubMaxAttempts pb)+        ]+  where+    backoffExpr b = case boKind b of+        "constant" -> "ConstantBackoff " <> windowText (boWindow b)+        "exponential" ->+            "ExponentialBackoff ExponentialBackoffOptions { initial = "+                <> windowText (boWindow b)+                <> ", maxDelay = "+                <> maybe "0" windowText (boMax b)+                <> ", multiplier = "+                <> fromMaybe "0" (boMultiplier b)+                <> " }"+        _ -> "error \"keiro-dsl: unlowerable backoff kind\""++--------------------------------------------------------------------------------+-- pgmq workqueue (EP-5): a self-contained Job payload record + codec+--------------------------------------------------------------------------------++{- | Emit the deterministic, symbol-free pgmq layer: the Job payload record, the+field→wire-name JSON codec, and the captured physical\/dlq\/table name constants.+Self-contained (base\/text\/aeson). The fan-out body and the raw-SQL dedup+predicate are holes (not emitted). Firewall holds.+-}+scaffoldWorkqueue :: Context -> WorkqueueNode -> [ScaffoldModule]+scaffoldWorkqueue ctx w =+    [ ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Queue.hs")+        , moduleText = emitWorkqueueGen genPrefix w+        , kind = Generated+        , origin = nodeOrigin "workqueue" (wqName w) (wqLoc w)+        }+    , ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/QueuePolicy.hs")+        , moduleText = emitQueuePolicy genPrefix w+        , kind = Generated+        , origin = nodeOrigin "workqueue" (wqName w) (wqLoc w)+        }+    , ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/QueueCodec.hs")+        , moduleText = emitQueueCodec genPrefix w+        , kind = Generated+        , origin = nodeOrigin "workqueue" (wqName w) (wqLoc w)+        }+    ]+  where+    genPrefix = genPrefixFor ctx (pascal (wqName w))++emitWorkqueueGen :: Text -> WorkqueueNode -> Text+emitWorkqueueGen genPrefix w =+    nl $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Queue"+        , "  ( " <> payloadTy <> " (..)"+        , "  , encode" <> payloadTy+        , "  , parse" <> payloadTy+        , "  , queuePhysical, queueDlq, queueTable"+        , groupKeyExport+        , "  ) where"+        , ""+        , "import Data.Aeson (Value, object, withObject, (.:), (.=))"+        , "import Data.Aeson.Types (parseEither)"+        , "import Data.Text (Text)"+        , "import qualified Data.Text as T"+        , ""+        , "queuePhysical, queueDlq, queueTable :: Text"+        , "queuePhysical = " <> tshow (wqPhysical w)+        , "queueDlq = " <> tshow (wqDlq w)+        , "queueTable = " <> tshow (wqTable w)+        , ""+        ]+            ++ groupKeyLines+            ++ [ "data " <> payloadTy <> " = " <> payloadTy+               , "  { " <> T.intercalate "\n  , " [wqfName f <> " :: !" <> hsType (wqfType f) | f <- wqPayload w]+               , "  }"+               , "  deriving stock (Eq, Show)"+               , ""+               , "encode" <> payloadTy <> " :: " <> payloadTy <> " -> Value"+               , "encode" <> payloadTy <> " p ="+               , "  object"+               ]+            ++ [lead i (tshow (wqfWire f) <> " .= p." <> wqfName f) | (i, f) <- zip [(0 :: Int) ..] (wqPayload w)]+            ++ [ "    ]"+               , ""+               , "parse" <> payloadTy <> " :: Value -> Either Text " <> payloadTy+               , "parse" <> payloadTy <> " = mapLeftText . parseEither (withObject " <> tshow payloadTy <> " go)"+               , "  where"+               , "    go o = " <> payloadTy <> fieldApps (wqPayload w)+               , ""+               , "mapLeftText :: Either String b -> Either Text b"+               , "mapLeftText = either (Left . T.pack) Right"+               ]+  where+    payloadTy = wqPayloadName w+    groupKeyExport = case wqGroupKey w of+        Nothing -> ""+        Just groupKey+            | gkVia groupKey == "raw" -> "  , groupKeyField, groupKeyFor"+            | otherwise -> "  , groupKeyField"+    groupKeyLines = case wqGroupKey w of+        Nothing -> []+        Just groupKey -> common <> derivationLines groupKey+          where+            common =+                [ "groupKeyField :: Text"+                , "groupKeyField = " <> tshow (gkField groupKey)+                , ""+                ]+            derivationLines key+                | gkVia key == "raw" =+                    [ "groupKeyFor :: " <> payloadTy <> " -> Text"+                    , "groupKeyFor payload = payload." <> gkField key+                    , ""+                    ]+                | otherwise =+                    [ "-- Opaque group-key derivation '" <> gkVia key <> "' remains hand-owned."+                    , "-- Captured fixture: " <> fromMaybe "<missing>" (gkFixture key)+                    , ""+                    ]+    hsType "bool" = "Bool"+    hsType "int" = "Int"+    hsType _ = "Text"+    lead 0 kv = "    [ " <> kv+    lead _ kv = "    , " <> kv+    fieldApps [] = ""+    fieldApps fs = " <$> " <> T.intercalate " <*> " ["o .: " <> tshow (wqfWire f) | f <- fs]++{- | Emit the versioned PGMQ envelope adapter.  The payload record remains+symbol-free and dependency-light in Queue.hs; this runtime-facing module is+the opt-in assembly point applications import into their Job values.+-}+emitQueueCodec :: Text -> WorkqueueNode -> Text+emitQueueCodec genPrefix w =+    nl+        [ "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "{- | Versioned job payload envelope: @{\\\"v\\\",\\\"t\\\",\\\"data\\\"}@."+        , ""+        , "Deploy workers before producers when raising its schema version. Do not"+        , "adopt this codec on a non-empty bare-payload queue without draining it"+        , "(or supplying a transitional codec), or in-flight messages will"+        , "dead-letter. This is telemetry-neutral:"+        , "docs/adr/0001-keiro-pgmq-job-processing-telemetry-contract.md owns"+        , "spans and acknowledgement vocabulary."+        , "-}"+        , "module " <> genPrefix <> ".QueueCodec (" <> stem <> "PayloadCodec, " <> stem <> "JobCodec) where"+        , ""+        , "import Data.List.NonEmpty (NonEmpty (..))"+        , "import Keiro.Codec (Codec (..), EventType (..))"+        , "import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)"+        , "import " <> genPrefix <> ".Queue (" <> payloadTy <> ", encode" <> payloadTy <> ", parse" <> payloadTy <> ")"+        , ""+        , stem <> "PayloadCodec :: Codec " <> payloadTy+        , stem <> "PayloadCodec ="+        , "  Codec"+        , "    { eventTypes = EventType " <> tshow payloadTy <> " :| []"+        , "    , eventType = \\_ -> EventType " <> tshow payloadTy+        , "    , schemaVersion = 1"+        , "    , encode = encode" <> payloadTy+        , "    , decode = \\_ -> parse" <> payloadTy+        , "    , upcasters = []"+        , "    }"+        , ""+        , stem <> "JobCodec :: JobCodec " <> payloadTy+        , stem <> "JobCodec = keiroJobCodec " <> stem <> "PayloadCodec"+        ]+  where+    payloadTy = wqPayloadName w+    stem = lowerFirst (T.concat (map pascal (T.splitOn "_" (wqName w))))++{- | Emit the pgmq retry policy + JobOutcome disposition compiled against the+LIVE @Keiro.PGMQ.Job@ runtime (RetryPolicy / JobOutcome / RetryDelay). This pins+the dangerous inversions over the runtime types: storeFailure ⇒ Retry (transient)+and decodeFailure ⇒ Dead (poison).+-}+emitQueuePolicy :: Text -> WorkqueueNode -> Text+emitQueuePolicy genPrefix w =+    nl $+        [ "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".QueuePolicy"+        , "  ( retryPolicy, jobOutcomeFor"+        , "  , jobOrdering, jobTuningFor, queueProvision"+        , "  ) where"+        , ""+        , "import Data.Text (Text)"+        , "import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)"+        , ""+        , "jobOrdering :: JobOrdering"+        , "jobOrdering = " <> orderingCtor+        , ""+        , "-- Deployment owns visibility timeout, batch size, and polling; the spec owns ordering."+        , "jobTuningFor :: JobTuning -> JobTuning"+        , "jobTuningFor = withOrdering jobOrdering"+        , ""+        , "-- Pass this to ensureJobQueueWith at worker startup. FIFO adds the required GIN index; the DLQ remains standard."+        , "queueProvision :: QueueProvision"+        , "queueProvision = " <> provisionExpr+        , ""+        , "retryPolicy :: RetryPolicy"+        , "retryPolicy ="+        , "  RetryPolicy"+        , "    { maxRetries = " <> tshow' (wqMaxRetries w)+        , "    , defaultRetryDelay = RetryDelay " <> windowText (wqDelay w)+        , "    , useDeadLetter = " <> (if wqDlqOn w then "True" else "False")+        , "    }"+        , ""+        , "-- The consumer JobOutcome disposition over the spec's named domain outcomes,"+        , "-- lowered to the live Keiro.PGMQ.Job.JobOutcome."+        , "jobOutcomeFor :: Text -> JobOutcome"+        , "jobOutcomeFor o = case o of"+        ]+            ++ ["  " <> tshow (wqdOutcome r) <> " -> " <> outcome (wqdAction r) | r <- wqDisposition w]+            ++ ["  _ -> Retry (RetryDelay " <> windowText (wqDelay w) <> ")"]+  where+    orderingCtor = case wqOrdering w of+        WqUnordered -> "Unordered"+        WqFifoThroughput -> "FifoThroughput"+        WqFifoRoundRobin -> "FifoRoundRobin"+    provisionExpr = fifoWrap baseProvision+    fifoWrap expression = case wqOrdering w of+        WqUnordered -> expression+        _ -> "withFifoIndexProvision (" <> expression <> ")"+    baseProvision = case wqProvision w of+        WqStandard -> "standardProvision"+        WqUnlogged -> "unloggedProvision"+        WqPartitioned interval retention ->+            "partitionedProvision (PartitionSpec { partitionInterval = "+                <> tshow interval+                <> ", retentionInterval = "+                <> tshow retention+                <> " })"+    outcome IAckOk = "Done"+    outcome (IRetry win) = "Retry (RetryDelay " <> windowText win <> ")"+    outcome (IDeadLetter mr) = "Dead " <> tshow (fromMaybe "dead-lettered" mr)++--------------------------------------------------------------------------------+-- First-class read models (EP-107)+--------------------------------------------------------------------------------++{- | Emit an acyclic three-module read-model vertical. @ReadModelTable@ owns the+qualified-table constant shared by the hand-owned query and the generated+runtime record; @ReadModel@ re-exports it as part of the public surface.+-}+scaffoldReadModel :: Context -> ReadModelNode -> [ScaffoldModule]+scaffoldReadModel ctx readModel =+    [ generated "ReadModelTable" (emitReadModelTable tableModule stem readModel)+    , generated "ReadModel" (emitReadModelGen ctx readModelModule tableModule readModelHolePrefix stem readModel)+    , ScaffoldModule+        { modulePath = modulePathFor readModelHolePrefix "ReadModelHoles"+        , moduleText = emitReadModelHoles tableModule readModelHolePrefix stem readModel+        , kind = HoleStub+        , origin = readModelOrigin+        }+    ]+  where+    nodeSegment = pascal (rmName readModel)+    stem = readModelStem readModel+    readModelModule = genPrefixFor ctx nodeSegment+    tableModule = readModelModule <> ".ReadModelTable"+    readModelHolePrefix = holePrefixFor ctx nodeSegment+    readModelOrigin = nodeOrigin "readmodel" (rmName readModel) (rmLoc readModel)+    generated leaf body =+        ScaffoldModule+            { modulePath = modulePathFor readModelModule leaf+            , moduleText = body+            , kind = Generated+            , origin = readModelOrigin+            }++modulePathFor :: Text -> Text -> FilePath+modulePathFor prefix leaf = T.unpack (T.replace "." "/" prefix <> "/" <> leaf <> ".hs")++readModelStem :: ReadModelNode -> Text+readModelStem = lowerFirst . T.concat . map pascal . T.splitOn "_" . rmName++emitReadModelTable :: Text -> Text -> ReadModelNode -> Text+emitReadModelTable tableModule stem readModel =+    nl+        [ "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> tableModule <> " (" <> qualifiedName <> ") where"+        , ""+        , "import Data.Text (Text)"+        , "import Keiro.Connection (qualifyTable)"+        , ""+        , "-- The fully-qualified, double-quoted data-table reference."+        , qualifiedName <> " :: Text"+        , qualifiedName <> " = qualifyTable " <> tshow (rmSchema readModel) <> " " <> tshow (rmTable readModel)+        ]+  where+    qualifiedName = stem <> "QualifiedTable"++emitReadModelGen :: Context -> Text -> Text -> Text -> Text -> ReadModelNode -> Text+emitReadModelGen ctx readModelModule tableModule readModelHolePrefix stem readModel =+    nl $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> readModelModule <> ".ReadModel"+        , "  ( " <> T.intercalate "\n  , " exports+        , "  ) where"+        , ""+        , "import Data.Functor (void)"+        , "import Effectful (Eff, (:>))"+        , "import " <> tableModule <> " (" <> qualifiedName <> ")"+        , "import " <> readModelHolePrefix <> ".ReadModelHoles (" <> T.intercalate ", " holeImports <> ")"+        ]+            ++ asyncImports+            ++ [ "import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)"+               , "import Keiro.ReadModel.Rebuild qualified as Rebuild"+               , "import Kiroku.Store.Effect (Store)"+               , "import Kiroku.Store.Types (" <> kirokuTypes <> ")"+               , ""+               , readModelName <> " :: ReadModel " <> queryInputType <> " " <> queryResultType+               , readModelName <> " ="+               , "  ReadModel"+               , "    { name = " <> tshow registryName+               , "    , tableName = " <> tshow (rmTable readModel)+               , "    , schema = " <> tshow (rmSchema readModel)+               , "    , subscriptionName = " <> tshow subscriptionName+               , "    , version = " <> tshow' (rmVersion readModel)+               , "    , shapeHash = " <> tshow (rmShape readModel)+               , "    , defaultConsistency = " <> consistencyExpr (rmConsistency readModel)+               , "    , strongScope = " <> scopeExpr (rmScope readModel)+               , "    , query = " <> queryName+               , "    }"+               , ""+               , "-- Call once at projection startup before serving queries."+               , registerName <> " :: (Store :> es) => Eff es ()"+               , registerName <> " ="+               , "  void (registerReadModel " <> tshow registryName <> " " <> tshow' (rmVersion readModel) <> " " <> tshow (rmShape readModel) <> ")"+               , ""+               , startName <> " :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata"+               , startName <> " ="+               , "  Rebuild.startRebuild " <> readModelName <> " " <> projectionNames+               , ""+               , finishName <> " :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)"+               , finishName <> " ="+               , "  Rebuild.finishRebuild " <> readModelName <> " " <> projectionNames+               , ""+               , abandonName <> " :: (Store :> es) => Eff es ReadModelMetadata"+               , abandonName <> " = Rebuild.abandonRebuild " <> readModelName+               ]+            ++ asyncDefinition+  where+    registryName = registryNameFor (contextName ctx) readModel+    subscriptionName = subscriptionNameFor (contextName ctx) readModel+    asyncName = registryName <> "-async"+    readModelName = stem <> "ReadModel"+    qualifiedName = stem <> "QualifiedTable"+    registerName = "register" <> pascal stem+    startName = "start" <> pascal stem <> "Rebuild"+    finishName = "finish" <> pascal stem <> "Rebuild"+    abandonName = "abandon" <> pascal stem <> "Rebuild"+    asyncValueName = stem <> "AsyncProjection"+    queryInputType = pascal stem <> "QueryInput"+    queryResultType = pascal stem <> "QueryResult"+    queryName = stem <> "Query"+    applyName = "apply" <> pascal stem+    exports =+        [ readModelName+        , qualifiedName+        , registerName+        , startName+        , finishName+        , abandonName+        ]+            ++ [asyncValueName | rmFeed readModel == RmSubscription]+    holeImports = [queryInputType, queryResultType, queryName] ++ [applyName | rmFeed readModel == RmSubscription]+    asyncImports = case rmFeed readModel of+        RmInline -> []+        RmSubscription -> ["import Keiro.Projection (AsyncProjection (..))"]+    kirokuTypes = case rmFeed readModel of+        RmInline -> "GlobalPosition"+        RmSubscription -> "GlobalPosition, RecordedEvent (..)"+    projectionNames = case rmFeed readModel of+        RmInline -> "[]"+        RmSubscription -> "[" <> tshow asyncName <> "]"+    asyncDefinition = case rmFeed readModel of+        RmInline -> []+        RmSubscription ->+            [ ""+            , asyncValueName <> " :: AsyncProjection"+            , asyncValueName <> " ="+            , "  AsyncProjection"+            , "    { name = " <> tshow asyncName+            , "    , readModelName = " <> tshow registryName+            , "    , subscriptionName = " <> tshow subscriptionName+            , "    , applyRecorded = " <> applyName+            , "    , idempotencyKey = \\recorded -> recorded.eventId"+            , "    }"+            ]+    consistencyExpr Strong = "Strong"+    consistencyExpr Eventual = "Eventual"+    scopeExpr Nothing = "EntireLog"+    scopeExpr (Just RmEntireLog) = "EntireLog"+    scopeExpr (Just (RmCategory categoryName)) = "CategoryHead " <> tshow categoryName++emitReadModelHoles :: Text -> Text -> Text -> ReadModelNode -> Text+emitReadModelHoles tableModule readModelHolePrefix stem readModel =+    nl $+        [ "-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it."+        , "module " <> readModelHolePrefix <> ".ReadModelHoles"+        , "  ( " <> T.intercalate "\n  , " exports+        , "  ) where"+        , ""+        , "import " <> tableModule <> " (" <> qualifiedName <> ")"+        , "import Hasql.Transaction qualified as Tx"+        ]+            ++ ["import Kiroku.Store.Types (RecordedEvent(..))" | rmFeed readModel == RmSubscription]+            ++ [ ""+               , "-- HOLE: replace these aliases with the real query input and result types."+               , "type " <> queryInputType <> " = ()"+               , "type " <> queryResultType <> " = ()"+               , ""+               , "-- HOLE: query " <> qualifiedTableLiteral readModel <> " via " <> qualifiedName <> "; never rely on search_path."+               , "-- Declared columns:"+               ]+            ++ map (("--   " <>) . readModelColumnDoc) (rmColumns readModel)+            ++ [ queryName <> " :: " <> queryInputType <> " -> Tx.Transaction " <> queryResultType+               , queryName <> " _input = " <> qualifiedName <> " `seq` error " <> tshow ("HOLE: fill " <> rmName readModel <> " query")+               ]+            ++ applyStub+  where+    qualifiedName = stem <> "QualifiedTable"+    queryInputType = pascal stem <> "QueryInput"+    queryResultType = pascal stem <> "QueryResult"+    queryName = stem <> "Query"+    applyName = "apply" <> pascal stem+    exports = [queryInputType, queryResultType, queryName] ++ [applyName | rmFeed readModel == RmSubscription]+    applyStub = case rmFeed readModel of+        RmInline -> []+        RmSubscription ->+            [ ""+            , "-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe."+            , applyName <> " :: RecordedEvent -> Tx.Transaction ()"+            , applyName <> " _recorded = error " <> tshow ("HOLE: fill " <> rmName readModel <> " async apply")+            ]++qualifiedTableLiteral :: ReadModelNode -> Text+qualifiedTableLiteral readModel = quoteSqlIdentifier (rmSchema readModel) <> "." <> quoteSqlIdentifier (rmTable readModel)++quoteSqlIdentifier :: Text -> Text+quoteSqlIdentifier identifier = "\"" <> T.replace "\"" "\"\"" identifier <> "\""++readModelColumnDoc :: RmColumn -> Text+readModelColumnDoc columnDecl =+    rmcName columnDecl+        <> " "+        <> rmcType columnDecl+        <> if rmcRequired columnDecl then " NOT NULL" else ""++--------------------------------------------------------------------------------+-- Router + shared worker-policy lowering (EP-108)+--------------------------------------------------------------------------------++scaffoldRouter :: Context -> RouterNode -> [ScaffoldModule]+scaffoldRouter ctx router =+    [ ScaffoldModule+        { modulePath = modulePathFor genPrefix "Router"+        , moduleText = emitRouterGen genPrefix router+        , kind = Generated+        , origin = routerOrigin+        }+    , ScaffoldModule+        { modulePath = modulePathFor holePrefix "RouterHoles"+        , moduleText = emitRouterHoles holePrefix router+        , kind = HoleStub+        , origin = routerOrigin+        }+    ]+  where+    genPrefix = genPrefixFor ctx (rtId router)+    holePrefix = holePrefixFor ctx (rtId router)+    routerOrigin = nodeOrigin "router" (rtId router) (rtLoc router)++emitRouterGen :: Text -> RouterNode -> Text+emitRouterGen genPrefix router =+    nl $+        [ "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Router"+        , "  ( " <> stem <> "Name"+        , "  , " <> stem <> "WorkerOptions"+        , "  ) where"+        , ""+        , "import Data.Text (Text)"+        ]+            ++ workerPolicyImports (rtPoison router)+            ++ [ ""+               , "-- The STABLE router name. It participates in every target-keyed"+               , "-- deterministicRouterCommandId; renaming it re-keys replayed dispatches."+               , stem <> "Name :: Text"+               , stem <> "Name = " <> tshow (rtName router)+               , ""+               , "-- Runtime-owned dispatch id inputs: (name, key, sourceEventId,"+               , "-- targetStreamName, occurrence). Target-keyed, not positional."+               , ""+               , "-- Node-level worker policy lowered from the spec. Pass this value to"+               , "-- Keiro.Router.runRouterWorkerWith; do not silently use defaultWorkerOptions."+               ]+            ++ workerOptionsLines (stem <> "WorkerOptions") (rtRejected router) (rtPoison router)+  where+    stem = lowerFirst (rtId router)++emitRouterHoles :: Text -> RouterNode -> Text+emitRouterHoles holePrefix router =+    nl+        [ "-- HAND-OWNED hole module for the router's behaviour-bearing bodies."+        , "-- keiro-dsl creates it once and never overwrites it."+        , "module " <> holePrefix <> ".RouterHoles () where"+        , ""+        , "-- HOLE resolve :: " <> inName (rtInput router) <> " -> Eff es [PMCommand targetCommand]"+        , "--   Spec source: " <> resolveSourceText (rvSource (rtResolve router)) <> "."+        , "--   The spec's 'stable' keyword acknowledges that retry attempts accumulate"+        , "--   the UNION of resolved target identities. Keep the recipient set stable"+        , "--   for a source event whenever an exact recipient set matters."+        , "-- HOLE router value: assemble Keiro.Router.Router with name = " <> lowerFirst (rtId router) <> "Name,"+        , "--   key, resolve, targetEventStream, and targetProjections; run it with"+        , "--   runRouterWorkerWith " <> lowerFirst (rtId router) <> "WorkerOptions."+        , "-- HOLE targetProjections: spec projections = " <> renderNames (rtProjections router) <> "."+        , "-- NOTE on-duplicate AckOk is sound because Keiro.Router confirms a duplicate"+        , "--   event id against the TARGET stream via confirmBenignDuplicate before"+        , "--   returning PMCommandDuplicate. Hand-rolled dispatch paths must do likewise."+        ]+  where+    renderNames names = "[" <> T.intercalate ", " names <> "]"++resolveSourceText :: ResolveSource -> Text+resolveSourceText (ResolveReadModel name) = "read-model " <> name <> " (typically Keiro.ReadModel.runQuery)"+resolveSourceText ResolveHole = "typed resolver hole"++workerPolicyImports :: PolicyChoice -> [Text]+workerPolicyImports poison =+    [ "import Keiro.ProcessManager (PoisonPolicy (..), RejectedCommandPolicy (..), WorkerOptions (..))"+    , "import Shibuya.Core.Ack (RetryDelay (..))"+    ]+        ++ if poison == PolHalt+            then []+            else ["import Effectful (Eff)", "import Shibuya.Core.Types (Envelope)"]++workerOptionsLines :: Text -> PolicyChoice -> PolicyChoice -> [Text]+workerOptionsLines valueName rejected poison =+    [ valueName <> signature+    , valueName <> argument <> " ="+    , "  WorkerOptions"+    , "    { poisonPolicy = " <> poisonExpr+    , "    , rejectedCommandPolicy = " <> rejectedExpr rejected+    , "    , transientRetryDelay = RetryDelay 5 -- matches defaultWorkerOptions; runtime tuning"+    , "    , metrics = Nothing                  -- runtime configuration; install at call site"+    , "    }"+    ]+  where+    signature = case poison of+        PolHalt -> " :: WorkerOptions es msg"+        _ -> " :: (Envelope msg -> Eff es ()) -> WorkerOptions es msg"+    argument = case poison of+        PolHalt -> ""+        _ -> " poisonCallback"+    poisonExpr = case poison of+        PolHalt -> "PoisonHalt"+        PolDeadLetter -> "PoisonDeadLetter poisonCallback"+        PolSkip -> "PoisonSkip poisonCallback"+    rejectedExpr = \case+        PolHalt -> "RejectedHalt"+        PolDeadLetter -> "RejectedDeadLetter"+        PolSkip -> "RejectedSkip"++--------------------------------------------------------------------------------+-- Process manager + durable timer (EP-3)+--------------------------------------------------------------------------------++{- | Emit the symbol-free deterministic wiring for a process manager + its timer+into a @Generated@ module, plus a create-if-absent @ProcessHoles@ module for the+behaviour-bearing bodies (the @handle@ reaction, the deadline window, and the+fire command). The @Generated@ module contains no keiki symbolic operator (the+saga's transducer is the separate aggregate hole), so the firewall invariant+holds. The timer worker uses the spec's @max-attempts@ ceiling, never the+dangerous @defaultTimerWorkerOptions@ (@Nothing@) default.+-}+scaffoldProcess :: Context -> ProcessNode -> [ScaffoldModule]+scaffoldProcess ctx p =+    [ ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" genPrefix <> "/Process.hs")+        , moduleText = emitProcessGen ctxPascal genPrefix holePrefix p+        , kind = Generated+        , origin = nodeOrigin "process" (procId p) (procLoc p)+        }+    , ScaffoldModule+        { modulePath = T.unpack (T.replace "." "/" holePrefix <> "/ProcessHoles.hs")+        , moduleText = emitProcessHoles genPrefix holePrefix p+        , kind = HoleStub+        , origin = nodeOrigin "process" (procId p) (procLoc p)+        }+    ]+  where+    ctxPascal = pascalFromKebab (contextName ctx)+    genPrefix = genPrefixFor ctx (procId p)+    holePrefix = holePrefixFor ctx (procId p)++emitProcessGen :: Text -> Text -> Text -> ProcessNode -> Text+emitProcessGen _ctxPascal genPrefix _holePrefix p =+    nl $+        [ "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> genPrefix <> ".Process"+        , "  ( " <> lo <> "ProcessName"+        , "  , " <> lo <> "Category"+        , "  , " <> lo <> "ProcessWorkerOptions"+        , "  , " <> lo <> "TimerRequest"+        , "  , " <> lo <> "FireOutcome"+        , "  ) where"+        , ""+        , "import Data.Aeson (Value, object, (.=))"+        , "import Data.Text (Text)"+        , "import qualified Data.Text as T"+        , "import Data.Time (UTCTime)"+        , "import Data.UUID (UUID)"+        , "import qualified Data.UUID.V5 as UUID.V5"+        , "import Keiro.Command (CommandError (..))"+        , "import Keiro.Stream qualified as Stream"+        , "import Keiro.Timer (TimerId (..), TimerRequest (..))"+        ]+            ++ workerPolicyImports (procPoison p)+            ++ [ ""+               , "-- The define-once ProcessManager name (hole-kind 5: referenced, never retyped)."+               , lo <> "ProcessName :: Text"+               , lo <> "ProcessName = " <> tshow (procName p)+               , ""+               , "-- The validated saga stream category (hole-kind 5: referenced, never retyped)."+               , "-- Saga streams are '<category>-<correlationId>' via Keiro.Stream.entityStream."+               , "-- categoryUnsafe is safe here because keiro-dsl check proved the literal legal."+               , lo <> "Category :: Stream.StreamCategory a"+               , lo <> "Category = Stream.categoryUnsafe " <> tshow categoryName+               , ""+               , "-- Node-level worker policy lowered from the spec. Pass this value to"+               , "-- Keiro.ProcessManager.runProcessManagerWorkerWith."+               ]+            ++ workerOptionsLines (lo <> "ProcessWorkerOptions") (procRejected p) (procPoison p)+            ++ [ ""+               , "-- The deterministic timer-request builder: id derived from the correlation"+               , "-- key (hole-kind 1), processManagerName referenced, payload from the spec."+               , "-- (timer id derived as uuidv5 of " <> tshow (idePrefix (tmId timer)) <> " <> correlationId)"+               , lo <> "TimerRequest :: Text -> UTCTime -> TimerRequest"+               , lo <> "TimerRequest correlationId fireAtTime ="+               , "  TimerRequest"+               , "    { timerId = TimerId (namedUuid (" <> tshow (idePrefix (tmId timer)) <> " <> correlationId))"+               , "    , processManagerName = " <> lo <> "ProcessName"+               , "    , correlationId = correlationId"+               , "    , fireAt = fireAtTime"+               , "    , payload = " <> payloadExpr (tmPayload timer)+               , "    }"+               , ""+               , "-- The timer-fire disposition table (hole-kind 2), derived from the spec."+               , "-- on-reject => " <> showOutcome (onReject fd) <> " is the benign inversion."+               , "-- A duplicate append reaches on-error unless it is confirmed against the"+               , "-- target stream. Use Keiro.ProcessManager.confirmBenignDuplicate:"+               , "--   StreamName -> EventId -> CommandError -> Eff es Bool"+               , "-- Fold True into the duplicate result and surface False as the failure."+               , lo <> "FireOutcome :: Either CommandError a -> Maybe ()"+               , lo <> "FireOutcome result = case result of"+               , "  Right{} -> " <> outcomeToMaybe (onOk fd)+               , "  Left CommandRejected -> " <> outcomeToMaybe (onReject fd)+               , "  Left (CommandAmbiguous _) -> " <> outcomeToMaybe (onAmbiguous fd) <> "  -- explicit definition-bug arm"+               , "  Left{} -> " <> outcomeToMaybe (onError fd)+               , ""+               , "-- max-attempts = " <> tshow' (tmMaxAttempts timer) <> ", dead-letter = " <> tshow (tmDeadLetter timer)+               , "-- (the timer worker must pass Just " <> tshow' (tmMaxAttempts timer) <> " to runTimerWorkerWith, never the"+               , "--  defaultTimerWorkerOptions Nothing ceiling that retries forever)."+               , ""+               , "-- deterministic v5 UUID of a correlation-keyed string (hole-kind 1)."+               , "namedUuid :: Text -> UUID"+               , "namedUuid v = UUID.V5.generateNamed UUID.V5.namespaceURL (map (fromIntegral . fromEnum) (T.unpack v))"+               ]+  where+    lo = lowerFirst (procId p)+    categoryName = staticCategory ("process " <> procId p) (sagaCategory (procSaga p))+    timer = procTimer p+    fd = fireDisposition (tmFire timer)++{- | The timer payload, restricted to the spec's literal (@name=\"value\"@)+bindings so it compiles in the deterministic builder. Bare fields and+ref-valued bindings are input-driven (the agent-written hole), not emitted.+-}+payloadExpr :: [FieldBinding] -> Text+payloadExpr fs = case [b | b <- fs, isLiteral b] of+    [] -> "object []"+    lits -> "object [ " <> T.intercalate ", " (map kv lits) <> " ]"+  where+    isLiteral b = maybe False (const True) (fbValue b >>= stripWrappingQuotes)+    kv b = tshow (fbName b) <> " .= (" <> maybe "\"\"" tshow (fbValue b >>= stripWrappingQuotes) <> " :: Value)"+    stripWrappingQuotes value = T.stripPrefix "\"" value >>= T.stripSuffix "\""++showOutcome :: FireOutcome -> Text+showOutcome OFired = "Fired"+showOutcome ORetry = "Retry"++outcomeToMaybe :: FireOutcome -> Text+outcomeToMaybe OFired = "Just ()  -- Fired"+outcomeToMaybe ORetry = "Nothing  -- Retry"++emitProcessHoles :: Text -> Text -> ProcessNode -> Text+emitProcessHoles _genPrefix holePrefix p =+    nl+        [ "-- HAND-OWNED hole module for the process manager's behaviour-bearing bodies."+        , "-- keiro-dsl creates it once and never overwrites it."+        , "module " <> holePrefix <> ".ProcessHoles () where"+        , ""+        , "-- HOLE handle: build the ProcessManagerAction (the self-advance"+        , "--   '" <> advCommand (hAdvance (procHandle p)) <> "', the dispatch(es), and the timer) from the input."+        , "-- HOLE streams: build streamFor with entityStream " <> lowerFirst (procId p) <> "Category;"+        , "--   build target streams with entityStream " <> lowerFirst (procTarget p) <> "Category. Never concatenate raw stream names."+        , "-- HOLE window: the deadline policy, e.g. surgeWindow :: NominalDiffTime;"+        , "--   surgeDeadline observedAt = addUTCTime surgeWindow observedAt  (TIME INJECTED)."+        , "-- HOLE fire command: construct " <> fireCommand (tmFire (procTimer p)) <> " for the timer fire,"+        , "--   keyed by correlationId; the fired-event-id is the deterministic uuidv5 of"+        , "--   " <> tshow (idePrefix (fireFiredEventId (tmFire (procTimer p)))) <> " <> correlationId."+        , "-- NOTE on-duplicate AckOk is sound because the runtime confirms a duplicate"+        , "--   event id against the TARGET stream via confirmBenignDuplicate before"+        , "--   returning PMCommandDuplicate. Its effective signature is:"+        , "--     StreamName -> EventId -> CommandError -> Eff es Bool"+        , "--   Hand-rolled paths must call it with the target stream and attempted event id,"+        , "--   fold True into the duplicate result, and surface False as the original failure."+        , "--   Never pattern-match DuplicateEvent as success: event ids are globally unique."+        ]++--------------------------------------------------------------------------------+-- Domain module+--------------------------------------------------------------------------------++emitDomain :: Agg -> Text+emitDomain a =+    nl $+        [ "{-# LANGUAGE DataKinds #-}"+        , "{-# LANGUAGE DuplicateRecordFields #-}"+        ]+            ++ ["{-# LANGUAGE DeriveAnyClass #-}" | hasSnapshot a]+            ++ [ "{-# LANGUAGE OverloadedStrings #-}"+               , "{-# LANGUAGE TemplateHaskell #-}"+               , "{-# LANGUAGE TypeApplications #-}"+               , "{-# OPTIONS_GHC -Wno-unused-top-binds #-}"+               , generatedBanner+               , "module " <> aGenPrefix a <> ".Domain where"+               , ""+               ]+            ++ ["import Data.Aeson (FromJSON, ToJSON)" | hasSnapshot a]+            ++ [ "import Data.Proxy (Proxy (..))"+               , "import Data.Text (Text)"+               , "import GHC.Generics (Generic)"+               , "import Keiki.Core (RegFile (..))"+               ]+            ++ ["import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)" | hasSnapshot a]+            ++ map ("import " <>) (domainConsumerImports a)+            ++ [ "import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)"+               , ""+               , sectionsOf+                    [ map (emitId a) (aIds a)+                    , map (emitEnum a) (aEnums a)+                    , [emitVertex a]+                    , map (emitRecord a) (aCommands a)+                    , [emitSum (aName a <> "Command") (aCommands a)]+                    , map (emitRecord a) (aEvents a)+                    , [emitSum (aName a <> "Event") (aEvents a)]+                    , [emitRegsType a, emitInitialRegs a]+                    ,+                        [ "$(deriveAggregateCtorsAll ''" <> aName a <> "Command ''" <> aName a <> "Regs)"+                        , ""+                        , "$(deriveWireCtorsAll ''" <> aName a <> "Event)"+                        ]+                    ]+               ]++hasSnapshot :: Agg -> Bool+hasSnapshot = maybe False (const True) . aSnapshot++emitId :: Agg -> IdDecl -> Text+emitId a d =+    nl $+        [ "newtype " <> idName d <> " = " <> idName d <> " Text"+        , "  deriving stock (Generic, Eq, Ord, Show)"+        ]+            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]+            ++ ["instance CanonicalTypeName " <> idName d | hasSnapshot a]+            ++ [ ""+               , lowerFirst (idName d) <> "Text :: " <> idName d <> " -> Text"+               , lowerFirst (idName d) <> "Text (" <> idName d <> " t) = t"+               ]++emitEnum :: Agg -> EnumDecl -> Text+emitEnum a d =+    nl $+        [ "data " <> enumName d <> " = " <> T.intercalate " | " (map fst (enumCtors d))+        , "  deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)"+        ]+            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]+            ++ ["instance CanonicalTypeName " <> enumName d | hasSnapshot a]+            ++ [ ""+               , lowerFirst (enumName d) <> "Text :: " <> enumName d <> " -> Text"+               , lowerFirst (enumName d) <> "Text = \\case"+               , nl ["  " <> c <> " -> " <> tshow w | (c, w) <- enumCtors d]+               ]++emitVertex :: Agg -> Text+emitVertex a =+    nl $+        [ "data " <> aVertexType a <> " = " <> T.intercalate " | " (map (vertexCtor a . stName) (aStates a))+        , "  deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)"+        ]+            ++ ["  deriving anyclass (ToJSON, FromJSON)" | hasSnapshot a]+            ++ [ line+               | hasSnapshot a+               , line <-+                    [ "instance CanonicalStateShape " <> aVertexType a+                    , "instance CanonicalTypeName " <> aVertexType a+                    ]+               ]++emitRecord :: Agg -> ResolvedCtor -> Text+emitRecord a rc =+    nl $+        [ "data " <> rcName rc <> "Data = " <> rcName rc <> "Data"+        ]+            ++ recordFields [(name, renderDomainType a fieldType) | (name, fieldType) <- rcFields rc]+            ++ ["  deriving stock (Generic, Eq, Show)"]++recordFields :: [(Text, Text)] -> [Text]+recordFields [] =+    ["  {"]+        <> ["  }"]+recordFields fs =+    [ lead i <> n <> " :: !" <> ty+    | (i, (n, ty)) <- zip [(0 :: Int) ..] fs+    ]+        ++ ["  }"]+  where+    lead 0 = "  { "+    lead _ = "  , "++emitSum :: Text -> [ResolvedCtor] -> Text+emitSum tyName ctors =+    nl $+        [firstLine] ++ restLines ++ ["  deriving stock (Generic, Eq, Show)"]+  where+    arm rc = rc' rc+    rc' rc = rcName rc <> " !" <> rcName rc <> "Data"+    (firstLine, restLines) = case ctors of+        [] -> ("data " <> tyName <> " = ()", [])+        (c : cs) ->+            ( "data " <> tyName <> " = " <> arm c+            , ["  | " <> arm c2 | c2 <- cs]+            )++emitRegsType :: Agg -> Text+emitRegsType a =+    nl $+        ["type " <> aName a <> "Regs ="]+            ++ regListLines a (aRegs a)++regListLines :: Agg -> [RegDecl] -> [Text]+regListLines _ [] = ["  '[]"]+regListLines a rs =+    [ lead i <> "'(" <> tshow (regName r) <> ", " <> renderDomainType a (regType r) <> ")"+    | (i, r) <- zip [(0 :: Int) ..] rs+    ]+        ++ ["   ]"]+  where+    lead 0 = "  '[ "+    lead _ = "   , "++emitInitialRegs :: Agg -> Text+emitInitialRegs a =+    nl $+        [ "initial" <> aName a <> "Regs :: RegFile " <> aName a <> "Regs"+        , "initial" <> aName a <> "Regs ="+        ]+            ++ chain (aRegs a)+  where+    chain [] = ["  RNil"]+    chain rs =+        [ "  RCons (Proxy @" <> tshow (regName r) <> ") " <> regInitialValue a r <> " $"+        | r <- init rs+        ]+            ++ ["  RCons (Proxy @" <> tshow (regName lastR) <> ") " <> regInitialValue a lastR <> " RNil"]+      where+        lastR = last rs++-- | The Haskell initial value for a register, by the category of its type.+regInitialValue :: Agg -> RegDecl -> Text+regInitialValue a r+    | Just declaration <- mappedDeclFor a (regType r) = case mappedInitial declaration of+        Just initialValue -> unQualifiedValueName initialValue+        Nothing -> "(error \"mapped register initial rejected before generation\")"+    | regType r `elem` idNames = "(" <> regType r <> " \"\")"+    | regType r == aVertexType a = maybe "(error \"invalid vertex initial\")" (vertexCtor a) (bareInitial r)+    | regType r == "Text" = maybe "(error \"Text initial must be quoted\")" tshow (textInitial r)+    | otherwise = maybe "(error \"invalid register initial\")" id (bareInitial r)+  where+    idNames = map idName (aIds a)+    bareInitial reg = case regInitial reg of+        RegInitBare value -> Just value+        RegInitText _ -> Nothing+    textInitial reg = case regInitial reg of+        RegInitText value -> Just value+        RegInitBare _ -> Nothing++domainConsumerImports :: Agg -> [Text]+domainConsumerImports a =+    sort . nub $+        [ hsModule (mappedHaskell declaration) <> " qualified"+        | declaration <- mappedUses a+        ]+            <> [ qualifiedModule initialValue <> " qualified"+               | declaration <- mappedUses a+               , initialValue <- maybeToListText (mappedInitial declaration)+               ]++mappedUses :: Agg -> [ResolvedMappedDecl]+mappedUses a =+    [ declaration+    | fieldType <-+        map snd (concatMap rcFields (aCommands a <> aEvents a))+            <> map regType (aRegs a)+    , declaration <- maybeToListText (mappedDeclFor a fieldType)+    ]++mappedDeclFor :: Agg -> Text -> Maybe ResolvedMappedDecl+mappedDeclFor a name = do+    graph <- aTypeGraph a+    Map.lookup (MappedKey name) (tgDeclarations graph)++mappedHaskell :: ResolvedMappedDecl -> HaskellSource+mappedHaskell (ResolvedStructural declaration _) = sdHaskell declaration+mappedHaskell (ResolvedOpaque declaration) = odHaskell declaration++mappedInitial :: ResolvedMappedDecl -> Maybe QualifiedValueName+mappedInitial (ResolvedStructural declaration _) = sdInitial declaration+mappedInitial (ResolvedOpaque declaration) = odInitial declaration++renderDomainType :: Agg -> Text -> Text+renderDomainType a fieldType =+    maybe fieldType (renderHaskellSource . mappedHaskell) (mappedDeclFor a fieldType)++maybeToListText :: Maybe value -> [value]+maybeToListText = maybe [] pure++--------------------------------------------------------------------------------+-- Codec module+--------------------------------------------------------------------------------++emitCodec :: Agg -> Text+emitCodec a =+    nl $+        [ "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE OverloadedStrings #-}"+        , generatedBanner+        , "module " <> aGenPrefix a <> ".Codec ("+        , "    " <> lowerFirst (aName a) <> "Codec,"+        , "    parse" <> aName a <> "Event,"+        , "    encode" <> aName a <> "Event,"+        ]+            ++ concatMap mappedExports (codecMappedDeclarations a)+            ++ [ ") where"+               , ""+               , "import " <> aGenPrefix a <> ".Domain"+               ]+            ++ ( if hasMappedCodec a+                    then+                        [ "import Control.Monad (unless)"+                        , "import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, withText, (.:), (.=))"+                        , "import Data.Aeson.Key qualified as Key"+                        , "import Data.Aeson.KeyMap qualified as KeyMap"+                        ]+                    else ["import Data.Aeson (Value, object, withObject, (.:), (.=))"]+               )+            ++ [ "import Data.Aeson.Types (Parser, parseEither)"+               , "import Data.List.NonEmpty (NonEmpty (..))"+               ]+            ++ ( if hasMappedCodec a+                    then ["import Data.Map.Strict (Map)", "import Data.Map.Strict qualified as Map"]+                    else []+               )+            ++ [ "import Data.Text (Text)"+               , "import qualified Data.Text as T"+               ]+            ++ ["import Keiro.Codec.Structural (bindingFromShape, bindingToShape)" | hasMappedCodec a]+            ++ [ "import Keiro.Codec (Codec (..), EventType (..))"+               , upcasterImport a+               ]+            ++ [nl (map ("import " <>) (codecMappedImports a)) | hasMappedCodec a]+            ++ [ ""+               , emitEnumParsers a+               ]+            ++ [emitMappedCodecs a | hasMappedCodec a]+            ++ [ ""+               , emitCodecValue a+               , ""+               , emitEncode a+               , ""+               , emitDecode a+               , ""+               , "mapLeftText :: Either String b -> Either Text b"+               , "mapLeftText = either (Left . T.pack) Right"+               ]+            ++ ( if hasMappedCodec a+                    then+                        [ ""+                        , "rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser ()"+                        , "rejectUnknownFields label allowed objectValue ="+                        , "  unless (null extras) (fail (label <> \" contains unknown fields: \" <> show extras))"+                        , "  where"+                        , "    extras = filter (`notElem` allowed) (map Key.toText (KeyMap.keys objectValue))"+                        ]+                    else []+               )+  where+    mappedExports (ResolvedStructural declaration _) =+        [ "    encode" <> sdName declaration <> "Mapped,"+        , "    decode" <> sdName declaration <> "Mapped,"+        ]+    mappedExports ResolvedOpaque{} = []++hasMappedCodec :: Agg -> Bool+hasMappedCodec = not . null . codecMappedDeclarations++emitEnumParsers :: Agg -> Text+emitEnumParsers a = sectionsOf [[emitEnumParser e | e <- aEnums a]]++emitEnumParser :: EnumDecl -> Text+emitEnumParser d =+    nl $+        [ "parse" <> enumName d <> " :: Text -> Parser " <> enumName d+        , "parse" <> enumName d <> " = \\case"+        ]+            ++ ["  " <> tshow w <> " -> pure " <> c | (c, w) <- enumCtors d]+            ++ ["  _ -> fail " <> tshow ("unknown " <> enumName d)]++emitCodecValue :: Agg -> Text+emitCodecValue a =+    nl $+        [ lowerFirst (aName a) <> "Codec :: Codec " <> aName a <> "Event"+        , lowerFirst (aName a) <> "Codec ="+        , "  Codec"+        , "    { eventTypes = " <> eventTypesExpr+        , "    , eventType = \\case"+        ]+            ++ ["        " <> rcName e <> "{} -> EventType " <> tshow (rcName e) | e <- aEvents a]+            ++ [ "    , schemaVersion = " <> tshow' (maxEventVersion a)+               , "    , encode = encode" <> aName a <> "Event"+               , "    , decode = parse" <> aName a <> "Event"+               , "    , upcasters = " <> upcastersExpr a+               , "    }"+               ]+            ++ upcasterRungDecls a+  where+    eventTypesExpr = case map rcName (aEvents a) of+        [] -> "error \"no events\""+        (e : es) -> "EventType " <> tshow e <> " :| [" <> T.intercalate ", " (map (("EventType " <>) . tshow) es) <> "]"++-- | The codec's @schemaVersion@: the maximum declared event version (EP-2).+maxEventVersion :: Agg -> Int+maxEventVersion a = maximum (1 : map rcVersion (aEvents a))++{- | One @(sourceVersion, upcasterName)@ entry per event that declares an+@upcast from@. The upcaster name is per-event (e.g. @upcastFooV1@) and its+body is a hole in the hand-owned Holes module.+-}+upcasterEntries :: Agg -> [(Int, Text, Text)]+upcasterEntries a =+    [ (m, rcName e, "upcast" <> rcName e <> "V" <> tshow' m)+    | e <- aEvents a+    , Just m <- [rcUpcastFrom e]+    ]++upcastersExpr :: Agg -> Text+upcastersExpr a =+    "[" <> T.intercalate ", " ["(" <> tshow' m <> ", upcastRungV" <> tshow' m <> ")" | (m, _) <- upcasterRungs a] <> "]"++{- | Group event-specific holes into one migration rung per aggregate-global+source version.  Event metadata stamps every kind with the aggregate's+schema version, so a rung must explicitly pass foreign event kinds through.+-}+upcasterRungs :: Agg -> [(Int, [(Text, Text)])]+upcasterRungs a =+    [ (source, [(eventName, fn) | (_, eventName, fn) <- entries])+    | entries@((source, _, _) : _) <- groupBy sameSource (sortOn firstSource (upcasterEntries a))+    ]+  where+    firstSource (source, _, _) = source+    sameSource (source, _, _) (otherSource, _, _) = source == otherSource++upcasterRungDecls :: Agg -> [Text]+upcasterRungDecls a = concatMap rung (upcasterRungs a)+  where+    rung (source, entries) =+        [ ""+        , "upcastRungV" <> tshow' source <> " :: EventType -> Value -> Either Text Value"+        ]+            ++ [ "upcastRungV" <> tshow' source <> " (EventType " <> tshow eventName <> ") value = " <> fn <> " value"+               | (eventName, fn) <- entries+               ]+            ++ [ "-- Kinds whose shape did not change at this rung pass through unchanged; their"+               , "-- stamped version is aggregate-global, not their own shape history."+               , "upcastRungV" <> tshow' source <> " _ value = Right value"+               ]++{- | When the codec references upcasters, it imports their (hole) definitions+from the hand-owned Holes module.+-}+upcasterImport :: Agg -> Text+upcasterImport a = case upcasterEntries a of+    [] -> ""+    es -> "import " <> aHolePrefix a <> ".Holes (" <> T.intercalate ", " [fn | (_, _, fn) <- es] <> ")"++emitEncode :: Agg -> Text+emitEncode a =+    nl $+        [ "encode" <> aName a <> "Event :: " <> aName a <> "Event -> Value"+        , "encode" <> aName a <> "Event = \\case"+        ]+            ++ concatMap encodeArm (aEvents a)+  where+    encodeArm e =+        [ "  " <> rcName e <> " payload ->"+        , "    object"+        ]+            ++ [ lead i <> kv+               | (i, kv) <- zip [(0 :: Int) ..] (("\"kind\" .= (" <> tshow (rcName e) <> " :: Text)") : map encodeField (rcFields e))+               ]+            ++ ["      ]"]+    lead 0 = "      [ "+    lead _ = "      , "+    encodeField (n, ty) =+        tshow n+            <> " .= "+            <> case fieldCat a ty of+                IdCat -> lowerFirst ty <> "Text payload." <> n+                EnumCat -> lowerFirst ty <> "Text payload." <> n+                MappedStructuralCat declaration _ -> "encode" <> sdName declaration <> "Mapped payload." <> n+                MappedOpaqueCat{} -> "toJSON payload." <> n+                _ -> "payload." <> n++emitDecode :: Agg -> Text+emitDecode a =+    nl $+        [ "parse" <> aName a <> "Event :: EventType -> Value -> Either Text " <> aName a <> "Event"+        , "parse" <> aName a <> "Event (EventType tag) = mapLeftText . parseEither (withObject " <> tshow (aName a <> "Event") <> " go)"+        , "  where"+        , "    go o = do"+        , "      case tag of"+        ]+            ++ concatMap decodeArm (aEvents a)+            ++ ["        _ -> fail \"unknown event type\""]+  where+    decodeArm e =+        [ "        " <> tshow (rcName e) <> " ->"+        , "          " <> rcName e <> " <$> (" <> rcName e <> "Data" <> fieldApps (rcFields e) <> ")"+        ]+    fieldApps [] = ""+    fieldApps fs = " <$> " <> T.intercalate " <*> " (map decodeField fs)+    -- The first field uses <$> (handled above), the rest <*>. We instead build+    -- a uniform list and join; for an empty record there are no fields.+    decodeField (n, ty) = case fieldCat a ty of+        IdCat -> "(" <> ty <> " <$> o .: " <> tshow n <> ")"+        EnumCat -> "(o .: " <> tshow n <> " >>= parse" <> ty <> ")"+        MappedStructuralCat declaration _ -> "(o .: " <> tshow n <> " >>= parse" <> sdName declaration <> "Mapped)"+        MappedOpaqueCat{} -> "o .: " <> tshow n+        _ -> "o .: " <> tshow n++codecMappedImports :: Agg -> [Text]+codecMappedImports a = case aTypeGraph a of+    Nothing -> []+    Just graph ->+        sort . nub $+            [ structuralShapeModule (aContext a) (sdName declaration) <> " qualified"+            | ResolvedStructural declaration _ <- codecMappedDeclarations a+            ]+                <> [ hsModule (sdHaskell declaration) <> " qualified"+                   | ResolvedStructural declaration _ <- codecMappedDeclarations a+                   ]+                <> [ qualifiedModule (sdBinding declaration) <> " qualified"+                   | ResolvedStructural declaration _ <- codecMappedDeclarations a+                   ]+                <> [ hsModule (odHaskell declaration) <> " qualified"+                   | ResolvedOpaque declaration <- codecMappedDeclarations a+                   ]+                <> [ hsModule (odHaskell declaration) <> " qualified"+                   | ResolvedStructural _ shape <- codecMappedDeclarations a+                   , key <- directShapeRefs shape+                   , Just (ResolvedOpaque declaration) <- [Map.lookup key (tgDeclarations graph)]+                   ]++codecMappedDeclarations :: Agg -> [ResolvedMappedDecl]+codecMappedDeclarations a = case aTypeGraph a of+    Nothing -> []+    Just graph ->+        mapMaybe (\key -> Map.lookup key (tgDeclarations graph)) (sort (Map.keys selected))+      where+        roots =+            [ MappedKey fieldType+            | event <- aEvents a+            , (_, fieldType) <- rcFields event+            , Map.member (MappedKey fieldType) (tgDeclarations graph)+            ]+        selected =+            Map.fromList+                [ (key, ())+                | root <- roots+                , key <- root : maybe [] (Map.keys . Map.fromSet (const ())) (Map.lookup root (tgReachability graph))+                ]++directShapeRefs :: ResolvedMappedShape -> [MappedKey]+directShapeRefs =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields -> concatMap (exprRefs . rwfType) fields+            , onEnum = const []+            , onUnion = \_ arms -> concatMap (maybe [] exprRefs . rwaPayload) arms+            }++exprRefs :: ResolvedTypeExpr -> [MappedKey]+exprRefs =+    foldTypeExpr+        TypeExprAlgebra+            { onText = []+            , onInt = []+            , onBool = []+            , onNatural = []+            , onTime = []+            , onJson = []+            , onOptional = id+            , onList = id+            , onMap = id+            , onRef = pure+            }++emitMappedCodecs :: Agg -> Text+emitMappedCodecs a = case aTypeGraph a of+    Nothing -> ""+    Just graph ->+        T.intercalate+            "\n\n"+            [ emitStructuralCodec a graph declaration shape+            | ResolvedStructural declaration shape <- codecMappedDeclarations a+            ]++emitStructuralCodec :: Agg -> TypeGraph -> StructuralDecl -> ResolvedMappedShape -> Text+emitStructuralCodec a graph declaration shape =+    nl+        [ "encode" <> name <> "Mapped :: " <> consumerType <> " -> Value"+        , "encode" <> name <> "Mapped = encode" <> name <> "Shape . bindingToShape " <> binding+        , ""+        , "parse" <> name <> "Mapped :: Value -> Parser " <> consumerType+        , "parse" <> name <> "Mapped value = bindingFromShape " <> binding <> " <$> parse" <> name <> "Shape value"+        , ""+        , "decode" <> name <> "Mapped :: Value -> Either Text " <> consumerType+        , "decode" <> name <> "Mapped = mapLeftText . parseEither parse" <> name <> "Mapped"+        , ""+        , "encode" <> name <> "Shape :: " <> shapeType <> " -> Value"+        , emitShapeEncoder a graph declaration shape+        , ""+        , "parse" <> name <> "Shape :: Value -> Parser " <> shapeType+        , emitShapeDecoder a graph declaration shape+        ]+  where+    name = sdName declaration+    consumerType = renderHaskellSource (sdHaskell declaration)+    shapeType = structuralShapeModule (aContext a) name <> "." <> name <> "Shape"+    binding = unQualifiedValueName (sdBinding declaration)++emitShapeEncoder :: Agg -> TypeGraph -> StructuralDecl -> ResolvedMappedShape -> Text+emitShapeEncoder a graph declaration =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields ->+                nl $+                    ["encode" <> name <> "Shape shape =", "  object"]+                        <> objectEntries+                            [ tshow (rwfKey field)+                                <> " .= "+                                <> encodeShapeExpr a graph (rwfType field) (shapeModuleName <> "." <> rwfHaskell field <> " shape")+                            | field <- fields+                            ]+            , onEnum = \entries ->+                nl $+                    ["encode" <> name <> "Shape = \\case"]+                        <> ["  " <> shapeModuleName <> "." <> weCtor entry <> " -> String " <> tshow (weTag entry) | entry <- entries]+            , onUnion = \encoding arms ->+                nl $+                    ["encode" <> name <> "Shape = \\case"]+                        <> concatMap (unionEncodeArm encoding) arms+            }+  where+    name = sdName declaration+    shapeModuleName = structuralShapeModule (aContext a) name+    unionEncodeArm encoding arm =+        [ "  " <> shapeModuleName <> "." <> rwaCtor arm <> payloadPattern <> " ->"+        , "    object"+        ]+            <> objectEntries+                ( [tshow (ueTagField encoding) <> " .= (" <> tshow (rwaTag arm) <> " :: Text)"]+                    <> [ tshow (ueContentsField encoding) <> " .= " <> encodeShapeExpr a graph payload "payload"+                       | payload <- maybeToListText (rwaPayload arm)+                       ]+                )+      where+        payloadPattern = maybe "" (const " payload") (rwaPayload arm)++emitShapeDecoder :: Agg -> TypeGraph -> StructuralDecl -> ResolvedMappedShape -> Text+emitShapeDecoder a graph declaration =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \constructor unknownFields fields ->+                nl $+                    [ "parse" <> name <> "Shape = withObject " <> tshow (name <> "Shape") <> " $ \\objectValue -> do"+                    ]+                        <> rejectLine "  " unknownFields (map rwfKey fields) "objectValue"+                        <> [ "  " <> shapeModuleName <> "." <> constructor+                           , "    <$> " <> T.intercalate "\n    <*> " (map (decodeRecordField a graph) fields)+                           ]+            , onEnum = \entries ->+                nl $+                    [ "parse" <> name <> "Shape = withText " <> tshow (name <> "Shape") <> " $ \\tag -> case tag of"+                    ]+                        <> ["  " <> tshow (weTag entry) <> " -> pure " <> shapeModuleName <> "." <> weCtor entry | entry <- entries]+                        <> ["  _ -> fail " <> tshow ("unknown " <> name <> " wire value")]+            , onUnion = \encoding arms ->+                nl $+                    [ "parse" <> name <> "Shape = withObject " <> tshow (name <> "Shape") <> " $ \\objectValue -> do"+                    , "  tag <- objectValue .: " <> tshow (ueTagField encoding) <> " :: Parser Text"+                    , "  case tag of"+                    ]+                        <> concatMap (unionDecodeArm encoding) arms+                        <> ["    _ -> fail " <> tshow ("unknown " <> name <> " union tag")]+            }+  where+    name = sdName declaration+    shapeModuleName = structuralShapeModule (aContext a) name+    rejectLine _ IgnoreUnknown _ _ = []+    rejectLine indent RejectUnknown allowed objectName =+        [indent <> "rejectUnknownFields " <> tshow name <> " " <> renderTextList allowed <> " " <> objectName]+    unionDecodeArm encoding arm =+        ["    " <> tshow (rwaTag arm) <> " -> do"]+            <> rejectLine "      " (ueUnknownFields encoding) allowed "objectValue"+            <> [ case rwaPayload arm of+                    Nothing -> "      pure " <> shapeModuleName <> "." <> rwaCtor arm+                    Just payload ->+                        "      "+                            <> shapeModuleName+                            <> "."+                            <> rwaCtor arm+                            <> " <$> (objectValue .: "+                            <> tshow (ueContentsField encoding)+                            <> " >>= ("+                            <> decodeShapeExpr a graph payload+                            <> "))"+               ]+      where+        allowed = ueTagField encoding : [ueContentsField encoding | rwaPayload arm /= Nothing]++decodeRecordField :: Agg -> TypeGraph -> ResolvedWireField -> Text+decodeRecordField a graph field = case rwfPresence field of+    PRequired ->+        "((objectValue .: " <> key <> " :: Parser Value) >>= (" <> decoder <> "))"+    POptional ->+        "(case KeyMap.lookup (Key.fromText "+            <> key+            <> ") objectValue of Nothing -> "+            <> missing+            <> "; Just presentValue -> "+            <> "("+            <> decoder+            <> ") presentValue)"+  where+    key = tshow (rwfKey field)+    decoder = decodeShapeExpr a graph (rwfType field)+    missing = case rwfOnMissing field of+        Nothing -> "fail " <> tshow ("missing optional field without default: " <> rwfKey field)+        Just onMissing -> "pure " <> renderMissingDefault a graph (rwfType field) onMissing++encodeShapeExpr :: Agg -> TypeGraph -> ResolvedTypeExpr -> Text -> Text+encodeShapeExpr _a graph expression value =+    foldTypeExpr+        TypeExprAlgebra+            { onText = \v -> "toJSON (" <> v <> ")"+            , onInt = \v -> "toJSON (" <> v <> ")"+            , onBool = \v -> "toJSON (" <> v <> ")"+            , onNatural = \v -> "toJSON (" <> v <> ")"+            , onTime = \v -> "toJSON (" <> v <> ")"+            , onJson = id+            , onOptional = \encode v -> "maybe Null (\\item -> " <> encode "item" <> ") (" <> v <> ")"+            , onList = \encode v -> "toJSON (map (\\item -> " <> encode "item" <> ") (" <> v <> "))"+            , onMap = \encode v -> "toJSON (Map.map (\\item -> " <> encode "item" <> ") (" <> v <> "))"+            , onRef = \key v -> case Map.lookup key (tgDeclarations graph) of+                Just (ResolvedStructural nested _) -> "encode" <> sdName nested <> "Shape (" <> v <> ")"+                Just (ResolvedOpaque _) -> "toJSON (" <> v <> ")"+                Nothing -> "toJSON (" <> v <> ")"+            }+        expression+        value++decodeShapeExpr :: Agg -> TypeGraph -> ResolvedTypeExpr -> Text+decodeShapeExpr _a graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = "parseJSON"+            , onInt = "parseJSON"+            , onBool = "parseJSON"+            , onNatural = "parseJSON"+            , onTime = "parseJSON"+            , onJson = "pure"+            , onOptional = \decode -> "\\value -> case value of Null -> pure Nothing; other -> Just <$> " <> decode <> " other"+            , onList = \decode -> "\\value -> (parseJSON value :: Parser [Value]) >>= traverse (" <> decode <> ")"+            , onMap = \decode -> "\\value -> (parseJSON value :: Parser (Map Text Value)) >>= traverse (" <> decode <> ")"+            , onRef = \key -> case Map.lookup key (tgDeclarations graph) of+                Just (ResolvedStructural nested _) -> "parse" <> sdName nested <> "Shape"+                Just (ResolvedOpaque _) -> "parseJSON"+                Nothing -> "parseJSON"+            }++renderMissingDefault :: Agg -> TypeGraph -> ResolvedTypeExpr -> OnMissing -> Text+renderMissingDefault a graph expression = \case+    OmNull -> "Nothing"+    OmText value -> tshow value+    OmInt value -> T.pack (show value)+    OmBool value -> if value then "True" else "False"+    OmEmptyList -> "[]"+    OmEmptyMap -> "Map.empty"+    OmCtor constructor -> case expression of+        RRef key -> case Map.lookup key (tgDeclarations graph) of+            Just (ResolvedStructural declaration _) -> structuralShapeModule (aContext a) (sdName declaration) <> "." <> constructor+            _ -> constructor+        _ -> constructor++objectEntries :: [Text] -> [Text]+objectEntries entries =+    [lead index <> entry | (index, entry) <- zip [(0 :: Int) ..] entries]+        <> ["      ]"]+  where+    lead 0 = "      [ "+    lead _ = "      , "++renderTextList :: [Text] -> Text+renderTextList values = "[" <> T.intercalate ", " (map tshow values) <> "]"++--------------------------------------------------------------------------------+-- EventStream module+--------------------------------------------------------------------------------++emitEventStream :: Agg -> Text+emitEventStream a =+    nl $+        [ generatedBanner+        , "module " <> aGenPrefix a <> ".EventStream"+        , "  ( " <> lowerFirst (aName a) <> "Category"+        , "  , " <> lowerFirst (aName a) <> "EventStream"+        , "  , " <> lowerFirst (aName a) <> "EventStreamDef"+        , "  , " <> aName a <> "EventStream"+        , "  , " <> aName a <> "EventStreamDef"+        ]+            ++ ["  , " <> lowerFirst (aName a) <> "SnapshotFixture" | hasSnapshot a]+            ++ [ "  ) where"+               , ""+               , "import " <> aGenPrefix a <> ".Domain"+               , "import " <> aGenPrefix a <> ".Codec (" <> lowerFirst (aName a) <> "Codec)"+               , "import " <> aHolePrefix a <> ".Holes (" <> lowerFirst (aName a) <> "Transducer)"+               , "import Keiki.Core (HsPred)"+               , "import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))"+               , "import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)"+               ]+            ++ ["import Data.Text (Text)" | hasSnapshot a]+            ++ ["import Keiro.Snapshot.Codec (defaultStateCodec, withFoldFingerprint)" | hasSnapshot a]+            ++ [ "import Keiro.Stream qualified as Stream"+               , ""+               , "-- The validated aggregate stream category (hole-kind 5: referenced, never retyped)."+               , "-- Entity streams are '<category>-<id>' via Keiro.Stream.entityStream."+               , "-- categoryUnsafe is safe here because this generated literal passed the DSL category proof."+               , lowerFirst (aName a) <> "Category :: Stream.StreamCategory a"+               , lowerFirst (aName a) <> "Category = Stream.categoryUnsafe " <> tshow categoryName+               , ""+               , "type " <> aName a <> "EventStreamDef ="+               , "  EventStream (HsPred " <> aName a <> "Regs " <> aName a <> "Command) " <> aName a <> "Regs " <> aVertexType a <> " " <> aName a <> "Command " <> aName a <> "Event"+               , ""+               , "type " <> aName a <> "EventStream ="+               , "  ValidatedEventStream (HsPred " <> aName a <> "Regs " <> aName a <> "Command) " <> aName a <> "Regs " <> aVertexType a <> " " <> aName a <> "Command " <> aName a <> "Event"+               , ""+               , lowerFirst (aName a) <> "EventStreamDef :: " <> aName a <> "EventStreamDef"+               , lowerFirst (aName a) <> "EventStreamDef ="+               , "  EventStream"+               , "    { transducer = " <> lowerFirst (aName a) <> "Transducer"+               , "    , initialState = " <> initialVertex a+               , "    , initialRegisters = initial" <> aName a <> "Regs"+               , "    , eventCodec = " <> lowerFirst (aName a) <> "Codec"+               , "    , resolveStreamName = Stream.streamName"+               , "    , snapshotPolicy = " <> snapshotPolicyExpr a+               ]+            ++ stateCodecFieldLines a+            ++ [ "    }"+               , ""+               ]+            ++ snapshotFixtureLines a+            ++ [ lowerFirst (aName a) <> "EventStream :: " <> aName a <> "EventStream"+               , lowerFirst (aName a) <> "EventStream ="+               , "  mkEventStreamOrThrow " <> tshow (aName a) <> " " <> lowerFirst (aName a) <> "EventStreamDef"+               ]+  where+    categoryName = staticCategory ("aggregate " <> aName a) (lowerFirst (aName a))++snapshotPolicyExpr :: Agg -> Text+snapshotPolicyExpr aggregate = case aSnapshot aggregate of+    Nothing -> "Never"+    Just snapshot -> case snapPolicy snapshot of+        SnapEvery interval -> "Every " <> tshow' interval+        SnapOnTerminal -> "OnTerminal"++stateCodecExpr :: Agg -> Text+stateCodecExpr aggregate = case aSnapshot aggregate of+    Nothing -> "Nothing"+    Just snapshot ->+        "Just (withFoldFingerprint "+            <> tshow (aFoldFingerprint aggregate)+            <> " (defaultStateCodec "+            <> tshow' (snapCodecVersion snapshot)+            <> "))"++stateCodecFieldLines :: Agg -> [Text]+stateCodecFieldLines aggregate = case aSnapshot aggregate of+    Nothing -> ["    , stateCodec = Nothing"]+    Just _ ->+        [ "    -- The snapshot discriminator composes: the spec's state-codec version (bump it"+        , "    -- in the spec's `state-codec version=` clause), keiki's register and"+        , "    -- control-state shape hashes, and this fold fingerprint derived from the"+        , "    -- spec's transition surface (guards, writes, emits, states, register"+        , "    -- initials, referenced rules). Spec-visible fold changes invalidate old"+        , "    -- snapshots automatically. Fold changes made ONLY in the hand-owned Holes"+        , "    -- module are invisible here: bump `state-codec version=` manually or old"+        , "    -- snapshots will be served stale."+        , "    , stateCodec = " <> stateCodecExpr aggregate+        ]++snapshotFixtureLines :: Agg -> [Text]+snapshotFixtureLines aggregate = case aSnapshot aggregate of+    Nothing -> []+    Just snapshot ->+        [ lowerFirst (aName aggregate) <> "SnapshotFixture :: (Int, Text)"+        , lowerFirst (aName aggregate) <> "SnapshotFixture = (" <> tshow' (snapCodecVersion snapshot) <> ", " <> tshow (snapShapeHash snapshot) <> ")"+        , ""+        ]++--------------------------------------------------------------------------------+-- Projection module+--------------------------------------------------------------------------------++emitProjection :: Agg -> Text+emitProjection a = case aProjection a of+    Nothing -> nl [generatedBanner, "module " <> aGenPrefix a <> ".Projection () where"]+    Just p ->+        nl+            [ "{-# LANGUAGE OverloadedRecordDot #-}"+            , "{-# LANGUAGE OverloadedStrings #-}"+            , generatedBanner+            , "module " <> aGenPrefix a <> ".Projection"+            , "  ( " <> lowerFirst (projTable p) <> "Projection"+            , "  , " <> lowerFirst (projTable p) <> "StatusFor"+            , "  ) where"+            , ""+            , "import " <> aGenPrefix a <> ".Domain"+            , "import " <> aHolePrefix a <> ".Holes (apply" <> pascal (projTable p) <> ")"+            , "import Data.Text (Text)"+            , "import Keiro.Projection (InlineProjection (..))"+            , ""+            , "-- The deterministic event->status mapping (hole-kind 3, /mapping/), derived"+            , "-- from the spec's status-map. The read-model SQL that consumes it lives in"+            , "-- the hand-owned Holes module (a DB-coupled hole, delegated to codd)."+            , projectionTableComment a p+            , lowerFirst (projTable p) <> "StatusFor :: " <> aName a <> "Event -> Maybe Text"+            , lowerFirst (projTable p) <> "StatusFor = \\case"+            , nl (statusArms a p)+            , ""+            , lowerFirst (projTable p) <> "Projection :: InlineProjection " <> aName a <> "Event"+            , lowerFirst (projTable p) <> "Projection ="+            , "  InlineProjection"+            , "    { name = " <> tshow (contextNameToProjName a p)+            , "    , apply = apply" <> pascal (projTable p)+            , "    }"+            ]++statusArms :: Agg -> ProjectionSpec -> [Text]+statusArms a p =+    [ "  " <> rcName e <> " {} -> " <> statusFor e+    | e <- aEvents a+    ]+        ++ ["  _ -> Nothing" | hasWildcard]+  where+    pairs = maybe [] mapPairs (projStatusMap p)+    statusFor e = case lookup (rcName e) pairs of+        Just value -> "Just " <> tshow value+        Nothing -> "Nothing"+    -- A wildcard is only needed if some event is uncovered; otherwise every arm+    -- is explicit and a wildcard would be redundant (and -Wall would warn).+    hasWildcard = False++contextNameToProjName :: Agg -> ProjectionSpec -> Text+contextNameToProjName a p = contextKebab a <> "-" <> projTable p <> "-inline"++contextKebab :: Agg -> Text+contextKebab = kebabFromPascal . aCtxPascal++projectionReadModel :: Agg -> Maybe ReadModelNode+projectionReadModel aggregate = do+    projection <- aProjection aggregate+    find ((== projTable projection) . rmName) (aReadModels aggregate)++projectionTableComment :: Agg -> ProjectionSpec -> Text+projectionTableComment aggregate projection = case projectionReadModel aggregate of+    Nothing ->+        "-- WARNING: no readmodel node declares '"+            <> projTable projection+            <> "'; unqualified SQL depends on search_path."+    Just readModel ->+        "-- Qualified table "+            <> qualifiedTableLiteral readModel+            <> "; use "+            <> genPrefixFor (aContext aggregate) (pascal (rmName readModel))+            <> ".ReadModelTable."+            <> readModelStem readModel+            <> "QualifiedTable."++--------------------------------------------------------------------------------+-- Holes module (create-if-absent)+--------------------------------------------------------------------------------++emitHoles :: Agg -> Text+emitHoles a =+    nl+        [ "{-# LANGUAGE BlockArguments #-}"+        , "{-# LANGUAGE DataKinds #-}"+        , "{-# LANGUAGE OverloadedRecordDot #-}"+        , "{-# LANGUAGE QualifiedDo #-}"+        , "{-# LANGUAGE TypeApplications #-}"+        , "-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never"+        , "-- overwrites it. Fill the transducer body (and any other holes) against the"+        , "-- generated signatures, then run the harness to confirm behaviour."+        , "module " <> aHolePrefix a <> ".Holes"+        , "  ( " <> lowerFirst (aName a) <> "Transducer"+        , holeProjectionExport a+        , holeUpcasterExports a+        , "  ) where"+        , ""+        , "import " <> aGenPrefix a <> ".Domain"+        , "import Keiki.Builder ((=:))"+        , "import qualified Keiki.Builder as B"+        , "import Keiki.Core (HsPred, RegFile, SymTransducer, lit, (.==), (./=), (.||))"+        , holeUpcasterImports a+        , holeProjectionImports a+        , ""+        , "-- HOLE: the transducer body. Reproduce the structure below, replacing each"+        , "-- `-- HOLE` line with the keiki symbolic operators it describes."+        , lowerFirst (aName a) <> "Transducer"+        , "  :: SymTransducer"+        , "       (HsPred " <> aName a <> "Regs " <> aName a <> "Command)"+        , "       " <> aName a <> "Regs"+        , "       " <> aVertexType a+        , "       " <> aName a <> "Command"+        , "       " <> aName a <> "Event"+        , lowerFirst (aName a) <> "Transducer ="+        , "  B.buildTransducer " <> initialVertex a <> " initial" <> aName a <> "Regs isTerminal do"+        , nl (concatMap (fromBlock a) (groupBySource a))+        , " where"+        , "  isTerminal = \\case"+        , nl ["    " <> vertexCtor a (stName s) <> " -> True" | s <- aStates a, stTerminal s]+        , "    _ -> False"+        , holeProjectionStub a+        , holeUpcasterStubs a+        ]++-- | Export, import, and stub the per-event upcaster holes (EP-2 evolution).+holeUpcasterExports :: Agg -> Text+holeUpcasterExports a = case upcasterEntries a of+    [] -> ""+    es -> nl ["  , " <> fn | (_, _, fn) <- es]++holeUpcasterImports :: Agg -> Text+holeUpcasterImports a = case upcasterEntries a of+    [] -> ""+    _ -> nl ["import Data.Aeson (Value)", "import Data.Text (Text)"]++holeUpcasterStubs :: Agg -> Text+holeUpcasterStubs a = case upcasterEntries a of+    [] -> ""+    es ->+        nl $+            concat+                [ [ ""+                  , "-- HOLE upcaster: this hole receives ONLY " <> eventName <> " payloads stored at"+                  , "-- aggregate schema version " <> tshow' source <> "; other event kinds pass through the"+                  , "-- generated rung dispatch automatically. Bring this payload up one version and decide"+                  , "-- the default/derivation for any field added at the new version here."+                  , fn <> " :: Value -> Either Text Value"+                  , fn <> " _ = Left \"HOLE: upcaster not implemented\""+                  ]+                | (source, eventName, fn) <- es+                ]++holeProjectionExport :: Agg -> Text+holeProjectionExport a = case aProjection a of+    Nothing -> "  -- (no projection)"+    Just p -> "  , apply" <> pascal (projTable p)++holeProjectionImports :: Agg -> Text+holeProjectionImports aggregate = case projectionReadModel aggregate of+    Nothing -> ""+    Just readModel ->+        "import "+            <> genPrefixFor (aContext aggregate) (pascal (rmName readModel))+            <> ".ReadModelTable ("+            <> readModelStem readModel+            <> "QualifiedTable)"++holeProjectionStub :: Agg -> Text+holeProjectionStub a = case aProjection a of+    Nothing -> ""+    Just p ->+        nl+            ( [ ""+              , "-- HOLE: the read-model SQL for the projection (a DB-coupled hole; the"+              , "-- pure event->status mapping is generated as " <> lowerFirst (projTable p) <> "StatusFor)."+              ]+                ++ projectionGuidance+                ++ [ "apply" <> pascal (projTable p) <> " :: " <> aName a <> "Event -> recorded -> txn ()"+                   , "apply" <> pascal (projTable p) <> " _event _recorded = " <> projectionTableUse <> "error \"HOLE: fill " <> projTable p <> " projection apply\""+                   ]+            )+      where+        projectionGuidance = case projectionReadModel a of+            Nothing ->+                ["-- WARNING: no readmodel node declares this table's schema; unqualified SQL depends on search_path."]+            Just readModel ->+                [ "-- Table: " <> qualifiedTableLiteral readModel <> ". Use " <> readModelStem readModel <> "QualifiedTable; never rely on search_path."+                , "-- Declared columns:"+                ]+                    ++ map (("--   " <>) . readModelColumnDoc) (rmColumns readModel)+        projectionTableUse = case projectionReadModel a of+            Nothing -> ""+            Just readModel -> readModelStem readModel <> "QualifiedTable `seq` "++-- Group transitions by source state, preserving order, for the B.from blocks.+groupBySource :: Agg -> [(Text, [Transition])]+groupBySource a = go [] (transitionsOf a)+  where+    go acc [] = reverse acc+    go acc (t : ts) =+        let src = tSource t+            (same, rest) = span ((== src) . tSource) ts+         in go ((src, t : same) : acc) rest++-- We don't keep the original Aggregate around in Agg, so reconstruct+-- transitions from a stored field. (Filled in resolveAgg via aTransitions.)+transitionsOf :: Agg -> [Transition]+transitionsOf = aTransitions++fromBlock :: Agg -> (Text, [Transition]) -> [Text]+fromBlock a (src, ts) =+    [ "    B.from " <> vertexCtor a src <> " do"+    ]+        ++ concatMap (onCmdBlock a) ts++onCmdBlock :: Agg -> Transition -> [Text]+onCmdBlock a t =+    [ "      B.onCmd inCtor" <> tCommand t <> " $ \\d -> B.do"+    ]+        -- Plan 143: the mode is structural, not hole-owned — a replay-only+        -- transition lowers to B.replayOnly (keiki ReplayOnly edge).+        ++ ["        B.replayOnly" | tMode t == TmReplayOnly]+        ++ maybe [] (\g -> ["        -- HOLE guard: " <> renderGuard g]) (tGuard t)+        ++ ["        -- HOLE write " <> r <> " := " <> renderGuard e | (r, e) <- tWrites t]+        ++ ["        -- HOLE emit " <> ev <> " (B.emit wire" <> ev <> " ...)" | ev <- tEmits t]+        ++ ["        B.goto " <> vertexCtor a (tGoto t)]++--------------------------------------------------------------------------------+-- Field categories and shared helpers+--------------------------------------------------------------------------------++data FieldCat+    = IdCat+    | EnumCat+    | MappedStructuralCat !StructuralDecl !ResolvedMappedShape+    | MappedOpaqueCat !OpaqueDecl+    | OtherCat+    deriving stock (Eq, Show)++fieldCat :: Agg -> Text -> FieldCat+fieldCat a ty+    | ty `elem` map idName (aIds a) = IdCat+    | ty `elem` map enumName (aEnums a) = EnumCat+    | Just (ResolvedStructural declaration shape) <- mappedDeclFor a ty = MappedStructuralCat declaration shape+    | Just (ResolvedOpaque declaration) <- mappedDeclFor a ty = MappedOpaqueCat declaration+    | otherwise = OtherCat++-- | The first constructor of a declared enum, used to build sample values.+firstEnumCtor :: Agg -> Text -> Maybe Text+firstEnumCtor a ty =+    case [c | e <- aEnums a, enumName e == ty, (c, _) <- take 1 (enumCtors e)] of+        (c : _) -> Just c+        [] -> Nothing++vertexCtor :: Agg -> Text -> Text+vertexCtor a s = aName a <> s++initialVertex :: Agg -> Text+initialVertex a = case aStates a of+    (s : _) -> vertexCtor a (stName s)+    [] -> aName a <> "Init"++generatedBanner :: Text+generatedBanner = "-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec."++nodeOrigin :: Text -> Text -> Loc -> Text+nodeOrigin nodeKind nodeName loc =+    nodeKind <> " " <> nodeName <> case unLoc loc of+        0 -> ""+        line -> " (line " <> tshow' line <> ")"++{- | Conditions that the deterministic emitters cannot lower faithfully. The+pre-write scaffold pipeline treats each returned message as a refusal. The+list is extended alongside the policy and type lowering milestones.+-}+scaffoldRefusals :: Spec -> [Text]+scaffoldRefusals spec =+    concatMap aggregateRefusals aggregates+        <> concatMap contractRefusals contracts+        <> concatMap publisherRefusals publishers+  where+    aggregates = [aggregate | NAggregate aggregate <- specNodes spec]+    contracts = [contract | NContract contract <- specNodes spec]+    publishers = [publisher | NPublisher publisher <- specNodes spec]+    idTypes = map idName (specIds spec)+    enumTypes = map enumName (specEnums spec)+    mappedTypes = case resolveTypeGraph spec of+        Left _ -> []+        Right graph -> map unMappedKey (Map.keys (tgDeclarations graph))+    mappedDeclaration typeName = case resolveTypeGraph spec of+        Left _ -> Nothing+        Right graph -> Map.lookup (MappedKey typeName) (tgDeclarations graph)+    enumCtorsFor ty = case [map fst (enumCtors enum) | enum <- specEnums spec, enumName enum == ty] of+        ctors : _ -> ctors+        [] -> []+    aggregateRefusals aggregate =+        [ "AggregateEmpty: aggregate '" <> aggName aggregate <> "' must declare at least one command, event, and transition"+        | null (aggCommands aggregate) || null (aggEvents aggregate) || null (aggTransitions aggregate)+        ]+            <> concatMap (registerRefusals aggregate) (aggRegs aggregate)+            <> [ "FieldTypeUnrepresentable: aggregate '" <> aggName aggregate <> "' field '" <> fieldName field <> "' has unsupported explicit type '" <> ty <> "'"+               | field <- aggregateFields aggregate+               , Just ty <- [fieldType field]+               , not (supportedType aggregate ty)+               ]+    registerRefusals aggregate reg =+        [ "RegTypeUnsupported: aggregate '" <> aggName aggregate <> "' register '" <> regName reg <> "' has unsupported type '" <> regType reg <> "'"+        | not (supportedType aggregate (regType reg))+        ]+            <> [ "RegTextInitialNotQuoted: aggregate '" <> aggName aggregate <> "' Text register '" <> regName reg <> "' must use a quoted initial"+               | regType reg == "Text"+               , RegInitBare _ <- [regInitial reg]+               ]+            <> [ "RegInitialNotEnumCtor: aggregate '" <> aggName aggregate <> "' register '" <> regName reg <> "' must start at a constructor of enum '" <> regType reg <> "'"+               | regType reg `elem` enumTypes+               , case regInitial reg of+                    RegInitBare value -> value `notElem` enumCtorsFor (regType reg)+                    RegInitText _ -> True+               ]+            <> [ "RegInitialInvalidLiteral: aggregate '" <> aggName aggregate <> "' Bool register '" <> regName reg <> "' must start at True or False"+               | regType reg == "Bool"+               , case regInitial reg of RegInitBare value -> value `notElem` ["True", "False"]; RegInitText _ -> True+               ]+            <> [ "RegInitialInvalidLiteral: aggregate '" <> aggName aggregate <> "' Int register '" <> regName reg <> "' must start at an integer literal"+               | regType reg == "Int"+               , case regInitial reg of RegInitBare value -> (readMaybe (T.unpack value) :: Maybe Int) == Nothing; RegInitText _ -> True+               ]+            <> [ "MappedRegisterInitialMissing: aggregate '" <> aggName aggregate <> "' register '" <> regName reg <> "' requires the mapped declaration's initial symbol"+               | Just declaration <- [mappedDeclaration (regType reg)]+               , mappedInitial declaration == Nothing+               ]+    aggregateFields aggregate =+        concatMap cmdFields (aggCommands aggregate)+            <> concat [fields | event <- aggEvents aggregate, EventFields fields <- [evBody event]]+    supportedType aggregate ty =+        ty `elem` (["Text", "Int", "Bool", aggName aggregate <> "Vertex"] <> idTypes <> enumTypes <> mappedTypes)     contractRefusals contract =         [ "ContractEmpty: contract '" <> ctrName contract <> "' must declare at least one event"         | null (ctrEvents contract)
src/Keiro/Dsl/ScaffoldRecord.hs view
@@ -1,6 +1,7 @@-{- | Versioned, dependency-free persistence for the files produced by one-successful scaffold run. Unknown header fields are ignored so v1 readers can-consume records extended by later tool versions.+{- | Versioned persistence for the files and mapped consumer identities used by+one successful scaffold run. Unknown header fields are ignored so v1 readers+can consume records extended by later tool versions. Mapping rows are canonical+single-line JSON after a @mapping @ prefix; old readers ignore that row kind. -} module Keiro.Dsl.ScaffoldRecord (     ScaffoldRecord (..),@@ -9,8 +10,14 @@     recordFileName, ) where +import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as BL+import Data.List (nub) import Data.Text (Text) import Data.Text qualified as T+import Data.Text.Encoding qualified as Text+import Keiro.Dsl.ExplainBindings (BindingHole (..))+import Keiro.Dsl.MappedConsumer (MappingIdentity (..)) import Keiro.Dsl.Scaffold (ModuleKind (..)) import System.FilePath (isAbsolute, splitDirectories) @@ -19,6 +26,8 @@     , recModuleRoot :: !Text     , recLayout :: !Text     , recFiles :: ![(ModuleKind, FilePath)]+    , recMappings :: ![MappingIdentity]+    , recBindingObligations :: ![BindingHole]     }     deriving stock (Eq, Show) @@ -31,10 +40,16 @@         , "layout: " <> recLayout record         ]             <> map renderFile (recFiles record)+            <> map renderMapping (recMappings record)+            <> map renderBindingObligation (recBindingObligations record)   where     rootLabel = if T.null (recModuleRoot record) then "(none)" else recModuleRoot record     renderFile (Generated, path) = "generated " <> T.pack path     renderFile (HoleStub, path) = "hole " <> T.pack path+    renderMapping mapping =+        "mapping " <> Text.decodeUtf8 (BL.toStrict (Aeson.encode mapping))+    renderBindingObligation obligation =+        "binding " <> Text.decodeUtf8 (BL.toStrict (Aeson.encode obligation))  {- | Parse a v1 record. The version header and the three required fields must be present exactly once. Unknown lines are ignored for forward compatibility;@@ -48,13 +63,20 @@             rootLabel <- exactlyOne "module-root: " rows             layout <- exactlyOne "layout: " rows             files <- traverse parseFile (filter isFileRow rows)-            pure-                ScaffoldRecord-                    { recSpecPath = specPath-                    , recModuleRoot = if rootLabel == "(none)" then "" else rootLabel-                    , recLayout = layout-                    , recFiles = files-                    }+            mappings <- traverse parseMapping (filter ("mapping " `T.isPrefixOf`) rows)+            bindingEntries <- traverse parseBindingObligation (filter ("binding " `T.isPrefixOf`) rows)+            if hasDuplicateMappingNames mappings || hasDuplicateBindingObligations bindingEntries+                then Nothing+                else+                    pure+                        ScaffoldRecord+                            { recSpecPath = specPath+                            , recModuleRoot = if rootLabel == "(none)" then "" else rootLabel+                            , recLayout = layout+                            , recFiles = files+                            , recMappings = mappings+                            , recBindingObligations = bindingEntries+                            }     _ -> Nothing   where     exactlyOne prefix rows = case [value | row <- rows, Just value <- [T.stripPrefix prefix row]] of@@ -70,6 +92,25 @@          in if null path || isAbsolute path || ".." `elem` splitDirectories path                 then Nothing                 else Just (fileKind, path)+    parseMapping row = do+        payload <- T.stripPrefix "mapping " row+        Aeson.decodeStrict' (Text.encodeUtf8 payload)+    parseBindingObligation row = do+        payload <- T.stripPrefix "binding " row+        Aeson.decodeStrict' (Text.encodeUtf8 payload)+    hasDuplicateMappingNames mappings =+        let names = map mappingSpecName mappings+         in length names /= length (nub names)+    hasDuplicateBindingObligations obligations =+        let keys = map bindingKey obligations+         in length keys /= length (nub keys)+    bindingKey hole =+        ( holeMappedName hole+        , holeModule hole+        , holeSymbol hole+        , holeKind hole+        , holePath hole+        )  recordFileName :: Text -> FilePath recordFileName context = "keiro-dsl-scaffold-record." <> T.unpack context <> ".txt"
src/Keiro/Dsl/ScaffoldRun.hs view
@@ -5,9 +5,12 @@     Refusal (..),     WriteDisposition (..),     StaleModule (..),+    MappingDrift (..),     ScaffoldReport (..),     scaffoldModules,+    scaffoldModulesWithGoldens,     planScaffold,+    planScaffoldWithGoldens,     executeScaffold,     renderRefusals,     renderScaffoldReport,@@ -19,11 +22,15 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as TIO+import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligationKind (..), bindingHoles)+import Keiro.Dsl.Goldens (GoldenPayload) import Keiro.Dsl.Grammar (Node (..), Spec (..))-import Keiro.Dsl.Harness (harnessFor, harnessProcess, harnessReadModel, harnessRouter, harnessWorkflow)+import Keiro.Dsl.Harness (harnessForWithGoldens, harnessProcess, harnessReadModel, harnessRouter, harnessWorkflow) import Keiro.Dsl.Manifest (moduleNameOf, renderManifest)+import Keiro.Dsl.MappedConsumer (ConsumerPlan (..), MappingIdentity (..), consumerPlan) import Keiro.Dsl.Scaffold import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName, renderRecord)+import Keiro.Dsl.TypeGraph (MappedKey (..), TypeGraph (..), UseSite (..), resolveTypeGraph) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (takeDirectory, (</>)) @@ -32,6 +39,7 @@     | FirewallBreach ![(FilePath, Text, Int)]     | LoweringRefusal ![Text]     | MissingGeneratedBanner ![FilePath]+    | ImportCycle ![Text]     deriving stock (Eq, Show)  data WriteDisposition = Overwritten | Created | Skipped@@ -43,6 +51,13 @@     }     deriving stock (Eq, Show) +data MappingDrift = MappingDrift+    { driftSpecName :: !Text+    , driftPrevious :: !(Maybe MappingIdentity)+    , driftCurrent :: !(Maybe MappingIdentity)+    }+    deriving stock (Eq, Show)+ data ScaffoldReport = ScaffoldReport     { reportSpecPath :: !FilePath     , reportOutDir :: !FilePath@@ -52,6 +67,10 @@     , reportRecordPath :: !FilePath     , reportPreviousSpecPath :: !(Maybe Text)     , reportStale :: ![StaleModule]+    , reportConsumerPlan :: !ConsumerPlan+    , reportConstraintPlan :: ![Text]+    , reportMappingDrift :: ![MappingDrift]+    , reportNewHoles :: ![BindingHole]     }     deriving stock (Eq, Show) @@ -59,37 +78,93 @@ this registry in one place prevents the CLI and tests from drifting apart. -} scaffoldModules :: Context -> Spec -> [ScaffoldModule]-scaffoldModules ctx spec =-    concat-        [ case node of-            NAggregate agg -> scaffoldAggregate ctx spec agg <> harnessFor ctx spec agg-            NProcess process -> scaffoldProcess ctx process <> harnessProcess ctx process-            NRouter router -> scaffoldRouter ctx router <> harnessRouter ctx router-            NContract contract -> scaffoldContract ctx contract-            NIntake intake -> scaffoldIntake ctx intake-            NPublisher publisher -> scaffoldPublisher ctx publisher-            NWorkqueue workqueue -> scaffoldWorkqueue ctx workqueue-            NReadModel readModel -> scaffoldReadModel ctx readModel <> harnessReadModel ctx readModel-            NWorkflow workflow -> harnessWorkflow ctx workflow-            NEmit _ -> []-            NPgmqDispatch _ -> []-            NOperation _ -> []-        | node <- specNodes spec-        ]+scaffoldModules = scaffoldModulesWithGoldens [] +scaffoldModulesWithGoldens :: [GoldenPayload] -> Context -> Spec -> [ScaffoldModule]+scaffoldModulesWithGoldens goldens ctx spec =+    scaffoldStructural ctx spec+        <> scaffoldReplayAudit ctx spec+        <> concat+            [ case node of+                NAggregate agg -> scaffoldAggregate ctx spec agg <> harnessForWithGoldens goldens ctx spec agg+                NProcess process -> scaffoldProcess ctx process <> harnessProcess ctx process+                NRouter router -> scaffoldRouter ctx router <> harnessRouter ctx router+                NContract contract -> scaffoldContract ctx contract+                NIntake intake -> scaffoldIntake ctx intake+                NPublisher publisher -> scaffoldPublisher ctx publisher+                NWorkqueue workqueue -> scaffoldWorkqueue ctx workqueue+                NReadModel readModel -> scaffoldReadModel ctx readModel <> harnessReadModel ctx readModel+                NWorkflow workflow -> harnessWorkflow ctx workflow+                NEmit _ -> []+                NPgmqDispatch _ -> []+                NOperation _ -> []+            | node <- specNodes spec+            ]+ {- | Run every pure refusal gate. A successful result is the exact write set; a refusal has no write set and therefore cannot be accidentally executed. -} planScaffold :: Context -> Spec -> Either [Refusal] [ScaffoldModule]-planScaffold ctx spec =-    let modules = scaffoldModules ctx spec+planScaffold = planScaffoldWithGoldens []++planScaffoldWithGoldens :: [GoldenPayload] -> Context -> Spec -> Either [Refusal] [ScaffoldModule]+planScaffoldWithGoldens goldens ctx spec =+    let modules = scaffoldModulesWithGoldens goldens ctx spec         breaches = firewallBreaches modules         refusals =             collisionRefusals modules+                <> dependencyRefusals ctx spec modules                 <> [FirewallBreach breaches | not (null breaches)]                 <> [LoweringRefusal lowering | let lowering = scaffoldRefusals spec, not (null lowering)]      in if null refusals then Right modules else Left refusals +dependencyRefusals :: Context -> Spec -> [ScaffoldModule] -> [Refusal]+dependencyRefusals ctx spec modules = collisionWithConsumers <> namespaceCycles+  where+    plan = consumerPlan spec+    generatedByName = Map.fromList [(moduleNameOf (modulePath moduleValue), moduleValue) | moduleValue <- modules, kind moduleValue == Generated]+    collisionWithConsumers =+        [ PathCollision+            (modulePath generated)+            [origin generated, "consumer module " <> consumerModule]+        | consumerModule <- consumerModules plan+        , Just generated <- [Map.lookup consumerModule generatedByName]+        ]+    namespaceCycles =+        [ ImportCycle [importer, consumerModule, importer]+        | consumerModule <- consumerModules plan+        , generatedNamespaceOwned ctx consumerModule+        , importer <- take 1 (importersOf consumerModule modules <> [contextGeneratedRoot ctx])+        ]++generatedNamespaceOwned :: Context -> Text -> Bool+generatedNamespaceOwned ctx consumerModule = case placement ctx of+    GeneratedPrefix -> contextGeneratedRoot ctx `T.isPrefixOf` consumerModule+    CollocatedLeaf ->+        (root <> contextSegment <> ".") `T.isPrefixOf` consumerModule+            && ".Generated" `T.isInfixOf` consumerModule+  where+    root = if T.null (moduleRoot ctx) then "" else moduleRoot ctx <> "."+    contextSegment = pascalFromKebab (contextName ctx)++contextGeneratedRoot :: Context -> Text+contextGeneratedRoot ctx = case placement ctx of+    GeneratedPrefix -> root <> "Generated." <> contextSegment+    CollocatedLeaf -> root <> contextSegment <> ".Generated"+  where+    root = if T.null (moduleRoot ctx) then "" else moduleRoot ctx <> "."+    contextSegment = pascalFromKebab (contextName ctx)++importersOf :: Text -> [ScaffoldModule] -> [Text]+importersOf imported =+    map (moduleNameOf . modulePath)+        . filter (any (importsModule imported) . T.lines . moduleText)++importsModule :: Text -> Text -> Bool+importsModule expected line = case T.words (T.strip line) of+    "import" : rest -> expected `elem` rest+    _ -> False+ collisionRefusals :: [ScaffoldModule] -> [Refusal] collisionRefusals modules =     [ PathCollision (modulePath first) (map origin (first : rest))@@ -115,11 +190,15 @@             let recordPath = out </> recordFileName (specContext spec)             previousRecord <- readRecord recordPath             stale <- maybe (pure []) (existingStale out modules) previousRecord+            let currentConsumerPlan = consumerPlan spec+                drift = maybe [] (mappingDrift (consumerMappings currentConsumerPlan) . recMappings) previousRecord+                currentObligations = either (const []) id (bindingHoles spec)+                newHoles = maybe [] (newBindingObligations currentObligations . recBindingObligations) previousRecord             createDirectoryIfMissing True out             dispositions <- mapM (writeModule out) modules             let manifestPath = out </> ("keiro-dsl-manifest." <> T.unpack (specContext spec) <> ".txt")             TIO.writeFile manifestPath (renderManifest (T.pack specPath) modules spec)-            TIO.writeFile recordPath (renderRecord (currentRecord specPath ctx modules))+            TIO.writeFile recordPath (renderRecord (currentRecord specPath ctx spec modules))             pure $                 Right                     ScaffoldReport@@ -131,8 +210,54 @@                         , reportRecordPath = recordPath                         , reportPreviousSpecPath = recSpecPath <$> previousRecord                         , reportStale = stale+                        , reportConsumerPlan = currentConsumerPlan+                        , reportConstraintPlan = constraintPlan spec currentConsumerPlan+                        , reportMappingDrift = drift+                        , reportNewHoles = newHoles                         } +constraintPlan :: Spec -> ConsumerPlan -> [Text]+constraintPlan spec plan = case resolveTypeGraph spec of+    Left _ -> []+    Right graph ->+        let registerRoots =+                Set.fromList+                    [ key+                    | RootRegister _ _ key <- tgUseSites graph+                    ]+         in map (constraintFor registerRoots) (consumerMappings plan)+  where+    constraintFor registerRoots mapping =+        mappingSpecName mapping+            <> ": "+            <> T.intercalate ", " (baseConstraints mapping <> registerConstraints registerRoots mapping)+    baseConstraints StructuralMapping{} = ["Eq", "Show", "CanonicalTypeName", "StructuralBinding"]+    baseConstraints OpaqueMapping{} = ["Eq", "Show", "ToJSON", "FromJSON"]+    registerConstraints roots mapping+        | MappedKey (mappingSpecName mapping) `Set.member` roots = ["register initial", "snapshot ToJSON", "snapshot FromJSON"]+        | otherwise = []++mappingDrift :: [MappingIdentity] -> [MappingIdentity] -> [MappingDrift]+mappingDrift current previous =+    [ MappingDrift name old new+    | name <- Set.toAscList (Map.keysSet oldByName <> Map.keysSet newByName)+    , let old = Map.lookup name oldByName+    , let new = Map.lookup name newByName+    , old /= new+    ]+  where+    oldByName = Map.fromList [(mappingSpecName mapping, mapping) | mapping <- previous]+    newByName = Map.fromList [(mappingSpecName mapping, mapping) | mapping <- current]++newBindingObligations :: [BindingHole] -> [BindingHole] -> [BindingHole]+newBindingObligations current previous =+    [ obligation+    | obligation <- current+    , obligation `Set.notMember` previousSet+    ]+  where+    previousSet = Set.fromList previous+ readRecord :: FilePath -> IO (Maybe ScaffoldRecord) readRecord path = do     exists <- doesFileExist path@@ -147,13 +272,15 @@         exists <- doesFileExist (out </> path)         pure [StaleModule fileKind path | exists] -currentRecord :: FilePath -> Context -> [ScaffoldModule] -> ScaffoldRecord-currentRecord specPath ctx modules =+currentRecord :: FilePath -> Context -> Spec -> [ScaffoldModule] -> ScaffoldRecord+currentRecord specPath ctx spec modules =     ScaffoldRecord         { recSpecPath = T.pack specPath         , recModuleRoot = moduleRoot ctx         , recLayout = case placement ctx of GeneratedPrefix -> "prefixed"; CollocatedLeaf -> "collocated"         , recFiles = [(kind m, modulePath m) | m <- modules]+        , recMappings = consumerMappings (consumerPlan spec)+        , recBindingObligations = either (const []) id (bindingHoles spec)         }  missingGeneratedBanners :: FilePath -> [ScaffoldModule] -> IO [FilePath]@@ -204,6 +331,11 @@         ]             <> map ("  " <>) (map T.pack paths)             <> ["  (adopted as hand code? move it, or re-run with --force-generated-overwrite)", "nothing was written"]+    render (ImportCycle path) =+        [ "error: generated/consumer import cycle -- refusing to scaffold; nothing was written"+        , "  " <> T.intercalate " -> " path+        , "  keep bindings in a leaf module that imports only Structural.Shape.* and Keiro.Codec.Structural"+        ]  renderScaffoldReport :: ScaffoldReport -> [Text] renderScaffoldReport report =@@ -212,9 +344,14 @@         <> map moduleLine dispositions         <> [ "firewall: OK (" <> tshow generatedCount <> " generated modules scanned, 0 forbidden operators)"            , harnessLine+           , dependencyLine            , "manifest: " <> T.pack (reportManifestPath report)+           , "record:   " <> T.pack (reportRecordPath report)            ]         <> previousSpecNote+        <> constraintSection+        <> newHolesSection+        <> mappingDriftSection         <> staleSection   where     ctx = reportContext report@@ -242,6 +379,23 @@     harnessLine = case harnesses of         [] -> "harness:  (none emitted)"         _ -> "harness:  run `cabal test <your-component>` over " <> T.unwords harnesses+    dependencyLine =+        "dependency plan: consumer packages "+            <> renderBracketed (consumerPackages (reportConsumerPlan report))+            <> ", consumer modules "+            <> renderBracketed (consumerModules (reportConsumerPlan report))+    constraintSection = case reportConstraintPlan report of+        [] -> []+        constraints -> "constraint plan:" : map ("  " <>) constraints+    newHolesSection = case reportNewHoles report of+        [] -> []+        obligations ->+            ["newly required holes since last scaffold: " <> tshow (length obligations)]+                <> concatMap obligationLines obligations+    obligationLines hole =+        [ "  " <> holeModule hole+        , "    " <> holeSignature hole <> " (" <> obligationKindLabel (holeKind hole) <> ")"+        ]     previousSpecNote = case reportPreviousSpecPath report of         Just previous             | previous /= T.pack (reportSpecPath report) ->@@ -249,6 +403,16 @@                 , "      specs sharing context " <> contextName ctx <> " in one --out also share " <> T.pack (reportManifestPath report)                 ]         _ -> []+    mappingDriftSection = case reportMappingDrift report of+        [] -> []+        drifts ->+            ["mapping drift: " <> tshow (length drifts) <> " declaration(s) changed since the previous scaffold:"]+                <> concatMap driftLines drifts+    driftLines drift =+        [ "  " <> driftSpecName drift+        , "    previous: " <> maybe "(absent)" renderMappingIdentity (driftPrevious drift)+        , "    current:  " <> maybe "(absent)" renderMappingIdentity (driftCurrent drift)+        ]     staleSection = case reportStale report of         [] -> []         stale ->@@ -259,6 +423,38 @@     staleLine stale = case staleKind stale of         Generated -> "  generated " <> T.pack (stalePath stale) <> "  (safe to delete; still on disk)"         HoleStub -> "  hole      " <> T.pack (stalePath stale) <> "  (hand-owned — review before deleting)"++obligationKindLabel :: BindingObligationKind -> Text+obligationKindLabel BindingValue = "binding"+obligationKindLabel FixtureValue = "fixtures"+obligationKindLabel InitialValue = "initial-value"++renderBracketed :: [Text] -> Text+renderBracketed values = "[" <> T.intercalate ", " values <> "]"++renderMappingIdentity :: MappingIdentity -> Text+renderMappingIdentity StructuralMapping{mappingPackage, mappingModule, mappingType, mappingBindingSymbol, mappingBindingVersion} =+    "structural "+        <> mappingPackage+        <> ":"+        <> mappingModule+        <> "."+        <> mappingType+        <> " binding="+        <> mappingBindingSymbol+        <> " version="+        <> mappingBindingVersion+renderMappingIdentity OpaqueMapping{mappingPackage, mappingModule, mappingType, mappingCodecIdentity, mappingCodecVersion} =+    "opaque "+        <> mappingPackage+        <> ":"+        <> mappingModule+        <> "."+        <> mappingType+        <> " codec="+        <> mappingCodecIdentity+        <> " version="+        <> mappingCodecVersion  tshow :: (Show a) => a -> Text tshow = T.pack . show
+ src/Keiro/Dsl/TypeGraph.hs view
@@ -0,0 +1,664 @@+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++{- | Checked, resolved consumer-owned mapped types. Parser declarations keep+mandatory facts optional so diagnostics can name omissions; this module is+the phase boundary after which missing facts and unresolved references are+unrepresentable.+-}+module Keiro.Dsl.TypeGraph (+    QualifiedValueName (..),+    CanonicalTypeId (..),+    BindingVersion (..),+    CodecIdentity (..),+    CodecVersion (..),+    mkQualifiedValueName,+    mkCanonicalTypeId,+    mkBindingVersion,+    mkCodecIdentity,+    mkCodecVersion,+    MappedDeclError (..),+    CheckedMappedDecl (..),+    StructuralDecl (..),+    OpaqueDecl (..),+    checkMappedDecl,+    MappedKey (..),+    ResolvedTypeExpr (..),+    ResolvedWireField (..),+    ResolvedWireArm (..),+    ResolvedMappedShape (..),+    ResolvedMappedDecl (..),+    TypeGraphError (..),+    TypeGraph (..),+    UseSite (..),+    PathSeg (..),+    UsePath (..),+    resolveTypeGraph,+    usePaths,+    renderUsePath,+    TypeExprAlgebra (..),+    foldTypeExpr,+    MappedShapeAlgebra (..),+    foldMappedShape,+    MappedDeclAlgebra (..),+    foldMappedDecl,+    wireFingerprint,+) where++import Data.Bifunctor (first)+import Data.Bits (xor)+import Data.Char (ord)+import Data.Either (partitionEithers)+import Data.Graph (SCC (..), stronglyConnComp)+import Data.List (sortOn)+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict (Map)+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 Data.Word (Word64)+import GHC.Generics (Generic)+import Keiro.Dsl.Grammar+import Numeric (showHex)++newtype QualifiedValueName = QualifiedValueName {unQualifiedValueName :: Text}+    deriving stock (Eq, Ord, Show, Generic)++newtype CanonicalTypeId = CanonicalTypeId {unCanonicalTypeId :: Text}+    deriving stock (Eq, Ord, Show, Generic)++newtype BindingVersion = BindingVersion {unBindingVersion :: Text}+    deriving stock (Eq, Ord, Show, Generic)++newtype CodecIdentity = CodecIdentity {unCodecIdentity :: Text}+    deriving stock (Eq, Ord, Show, Generic)++newtype CodecVersion = CodecVersion {unCodecVersion :: Text}+    deriving stock (Eq, Ord, Show, Generic)++data MappedDeclError+    = MissingHaskellSource !Name+    | MissingStructuralBinding !Name+    | MissingStructuralBindingVersion !Name+    | MissingCanonicalType !Name+    | MissingFixtureCases !Name+    | MissingOpaqueCodecIdentity !Name+    | MissingOpaqueCodecVersion !Name+    | EmptyQualifiedValueName !Text+    | EmptyCanonicalTypeId !Text+    | EmptyBindingVersion !Text+    | EmptyCodecIdentity !Text+    | EmptyCodecVersion !Text+    deriving stock (Eq, Show, Generic)++mkQualifiedValueName :: Text -> Either MappedDeclError QualifiedValueName+mkQualifiedValueName value+    | T.null (T.strip value) = Left (EmptyQualifiedValueName value)+    | otherwise = Right (QualifiedValueName value)++mkCanonicalTypeId :: Text -> Either MappedDeclError CanonicalTypeId+mkCanonicalTypeId value+    | T.null (T.strip value) = Left (EmptyCanonicalTypeId value)+    | otherwise = Right (CanonicalTypeId value)++mkBindingVersion :: Text -> Either MappedDeclError BindingVersion+mkBindingVersion value+    | T.null (T.strip value) = Left (EmptyBindingVersion value)+    | otherwise = Right (BindingVersion value)++mkCodecIdentity :: Text -> Either MappedDeclError CodecIdentity+mkCodecIdentity value+    | T.null (T.strip value) = Left (EmptyCodecIdentity value)+    | otherwise = Right (CodecIdentity value)++mkCodecVersion :: Text -> Either MappedDeclError CodecVersion+mkCodecVersion value+    | T.null (T.strip value) = Left (EmptyCodecVersion value)+    | otherwise = Right (CodecVersion value)++data StructuralDecl = StructuralDecl+    { sdName :: !Name+    , sdHaskell :: !HaskellSource+    , sdBinding :: !QualifiedValueName+    , sdBindingVersion :: !BindingVersion+    , sdCanonical :: !CanonicalTypeId+    , sdFixtures :: !QualifiedValueName+    , sdInitial :: !(Maybe QualifiedValueName)+    , sdLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data OpaqueDecl = OpaqueDecl+    { odName :: !Name+    , odHaskell :: !HaskellSource+    , odCodecIdentity :: !CodecIdentity+    , odCodecVersion :: !CodecVersion+    , odFixtures :: !QualifiedValueName+    , odInitial :: !(Maybe QualifiedValueName)+    , odLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data CheckedMappedDecl+    = CheckedStructural !StructuralDecl !MappedShape+    | CheckedOpaque !OpaqueDecl+    deriving stock (Eq, Show, Generic)++checkMappedDecl :: MappedDecl -> Either (NonEmpty MappedDeclError) CheckedMappedDecl+checkMappedDecl MappedStructural{msName = name, msHaskell = haskell, msBinding = binding, msBindingVersion = bindingVersion, msCanonical = canonical, msFixtures = fixtures, msInitial = initial, msShape = shape, msLoc = loc} = do+    checkedHaskell <- require (MissingHaskellSource name) haskell+    checkedBinding <- require (MissingStructuralBinding name) binding >>= liftOne . mkQualifiedValueName+    checkedBindingVersion <- require (MissingStructuralBindingVersion name) bindingVersion >>= liftOne . mkBindingVersion+    checkedCanonical <- require (MissingCanonicalType name) canonical >>= liftOne . mkCanonicalTypeId+    checkedFixtures <- require (MissingFixtureCases name) fixtures >>= liftOne . mkQualifiedValueName+    checkedInitial <- traverse (liftOne . mkQualifiedValueName) initial+    pure+        ( CheckedStructural+            StructuralDecl+                { sdName = name+                , sdHaskell = checkedHaskell+                , sdBinding = checkedBinding+                , sdBindingVersion = checkedBindingVersion+                , sdCanonical = checkedCanonical+                , sdFixtures = checkedFixtures+                , sdInitial = checkedInitial+                , sdLoc = loc+                }+            shape+        )+checkMappedDecl MappedOpaque{moName = name, moHaskell = haskell, moCodecId = codecIdentity, moCodecVersion = codecVersion, moFixtures = fixtures, moInitial = initial, moLoc = loc} = do+    checkedHaskell <- require (MissingHaskellSource name) haskell+    checkedCodecIdentity <- require (MissingOpaqueCodecIdentity name) codecIdentity >>= liftOne . mkCodecIdentity+    checkedCodecVersion <- require (MissingOpaqueCodecVersion name) codecVersion >>= liftOne . mkCodecVersion+    checkedFixtures <- require (MissingFixtureCases name) fixtures >>= liftOne . mkQualifiedValueName+    checkedInitial <- traverse (liftOne . mkQualifiedValueName) initial+    pure+        ( CheckedOpaque+            OpaqueDecl+                { odName = name+                , odHaskell = checkedHaskell+                , odCodecIdentity = checkedCodecIdentity+                , odCodecVersion = checkedCodecVersion+                , odFixtures = checkedFixtures+                , odInitial = checkedInitial+                , odLoc = loc+                }+        )++require :: e -> Maybe a -> Either (NonEmpty e) a+require err = maybe (Left (err :| [])) Right++liftOne :: Either e a -> Either (NonEmpty e) a+liftOne = first (:| [])++newtype MappedKey = MappedKey {unMappedKey :: Name}+    deriving stock (Eq, Ord, Show, Generic)++data ResolvedTypeExpr+    = RText+    | RInt+    | RBool+    | RNatural+    | RTime+    | RJson+    | ROptional !ResolvedTypeExpr+    | RList !ResolvedTypeExpr+    | RMap !ResolvedTypeExpr+    | RRef !MappedKey+    deriving stock (Eq, Show, Generic)++data ResolvedWireField = ResolvedWireField+    { rwfHaskell :: !Name+    , rwfKey :: !Text+    , rwfType :: !ResolvedTypeExpr+    , rwfPresence :: !Presence+    , rwfOnMissing :: !(Maybe OnMissing)+    , rwfLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data ResolvedWireArm = ResolvedWireArm+    { rwaCtor :: !Name+    , rwaTag :: !Text+    , rwaPayload :: !(Maybe ResolvedTypeExpr)+    , rwaLoc :: !Loc+    }+    deriving stock (Eq, Show, Generic)++data ResolvedMappedShape+    = RRecord !Name !UnknownFields ![ResolvedWireField]+    | REnum ![WireEnum]+    | RUnion !UnionEncoding ![ResolvedWireArm]+    deriving stock (Eq, Show, Generic)++data ResolvedMappedDecl+    = ResolvedStructural !StructuralDecl !ResolvedMappedShape+    | ResolvedOpaque !OpaqueDecl+    deriving stock (Eq, Show, Generic)++data TypeGraphError+    = TGDeclError !Name !MappedDeclError+    | TGAmbiguousName !Name ![Text]+    | TGUnresolvedRef !Name !Name !Loc+    | TGRecursive ![Name]+    deriving stock (Eq, Show, Generic)++data UseSite+    = RootCommandField !Name !Name !Name !MappedKey+    | RootEventField !Name !Name !Name !MappedKey+    | RootRegister !Name !Name !MappedKey+    deriving stock (Eq, Ord, Show, Generic)++data PathSeg+    = SegField !Name !Text+    | SegArm !Name !Text+    | SegElem+    | SegMapValue+    | SegOptional+    | SegDecl !Name+    deriving stock (Eq, Ord, Show, Generic)++data UsePath = UsePath+    { upRoot :: !UseSite+    , upSegments :: ![PathSeg]+    }+    deriving stock (Eq, Ord, Show, Generic)++data TypeGraph = TypeGraph+    { tgDeclarations :: !(Map MappedKey ResolvedMappedDecl)+    , tgReachability :: !(Map MappedKey (Set MappedKey))+    , tgUseSites :: ![UseSite]+    }+    deriving stock (Eq, Show, Generic)++resolveTypeGraph :: Spec -> Either (NonEmpty TypeGraphError) TypeGraph+resolveTypeGraph spec = do+    checked <- collectChecked (specMapped spec)+    rejectMany (ambiguityErrors spec checked)+    let keyByName = Map.fromList [(checkedName decl, MappedKey (checkedName decl)) | decl <- checked]+        (resolveErrors, resolvedPairs) = partitionEithers (map (resolveCheckedDecl keyByName) checked)+    rejectMany resolveErrors+    let declarations = Map.fromList resolvedPairs+    rejectMany (cycleErrors declarations)+    let reachability = Map.mapWithKey (reachableFrom declarations) declarations+    pure+        TypeGraph+            { tgDeclarations = declarations+            , tgReachability = reachability+            , tgUseSites = collectUseSites keyByName spec+            }++collectChecked :: [MappedDecl] -> Either (NonEmpty TypeGraphError) [CheckedMappedDecl]+collectChecked declarations =+    let checked = [(rawName declaration, checkMappedDecl declaration) | declaration <- declarations]+        errors =+            [ TGDeclError name err+            | (name, Left declarationErrors) <- checked+            , err <- NE.toList declarationErrors+            ]+     in case NE.nonEmpty errors of+            Just nonEmptyErrors -> Left nonEmptyErrors+            Nothing -> Right [declaration | (_, Right declaration) <- checked]++rejectMany :: [e] -> Either (NonEmpty e) ()+rejectMany errors = maybe (Right ()) Left (NE.nonEmpty errors)++rawName :: MappedDecl -> Name+rawName MappedStructural{msName = name} = name+rawName MappedOpaque{moName = name} = name++checkedName :: CheckedMappedDecl -> Name+checkedName (CheckedStructural declaration _) = sdName declaration+checkedName (CheckedOpaque declaration) = odName declaration++ambiguityErrors :: Spec -> [CheckedMappedDecl] -> [TypeGraphError]+ambiguityErrors spec declarations =+    [ TGAmbiguousName name origins+    | (name, origins) <- Map.toList allOrigins+    , length origins > 1+    ]+  where+    builtins = ["Text", "Int", "Bool", "Natural", "Time", "UTCTime", "Json", "Optional", "List", "Map"]+    originPairs =+        [(checkedName declaration, "mapped") | declaration <- declarations]+            ++ [(idName declaration, "id") | declaration <- specIds spec]+            ++ [(enumName declaration, "enum") | declaration <- specEnums spec]+            ++ [(name, "built-in") | name <- builtins]+    allOrigins = Map.fromListWith (++) [(name, [origin]) | (name, origin) <- originPairs]++resolveCheckedDecl :: Map Name MappedKey -> CheckedMappedDecl -> Either TypeGraphError (MappedKey, ResolvedMappedDecl)+resolveCheckedDecl _ (CheckedOpaque declaration) =+    Right (MappedKey (odName declaration), ResolvedOpaque declaration)+resolveCheckedDecl keyByName (CheckedStructural declaration shape) = do+    resolvedShape <- resolveShape keyByName (sdName declaration) shape+    pure (MappedKey (sdName declaration), ResolvedStructural declaration resolvedShape)++resolveShape :: Map Name MappedKey -> Name -> MappedShape -> Either TypeGraphError ResolvedMappedShape+resolveShape keyByName owner (ShapeRecord constructor unknownFields fields) =+    RRecord constructor unknownFields <$> traverse resolveField fields+  where+    resolveField field =+        ResolvedWireField+            (wfHaskell field)+            (wfKey field)+            <$> resolveExpr keyByName owner (wireFieldLoc field) (wfType field)+            <*> pure (wfPresence field)+            <*> pure (wfOnMissing field)+            <*> pure (wireFieldLoc field)+resolveShape _ _ (ShapeEnum entries) = Right (REnum entries)+resolveShape keyByName owner (ShapeUnion encoding arms) =+    RUnion encoding <$> traverse resolveArm arms+  where+    resolveArm arm =+        ResolvedWireArm+            (waCtor arm)+            (waTag arm)+            <$> traverse (resolveExpr keyByName owner (waLoc arm)) (waPayload arm)+            <*> pure (waLoc arm)++resolveExpr :: Map Name MappedKey -> Name -> Loc -> TypeExpr -> Either TypeGraphError ResolvedTypeExpr+resolveExpr _ _ _ TText = Right RText+resolveExpr _ _ _ TInt = Right RInt+resolveExpr _ _ _ TBool = Right RBool+resolveExpr _ _ _ TNatural = Right RNatural+resolveExpr _ _ _ TTime = Right RTime+resolveExpr _ _ _ TJson = Right RJson+resolveExpr names owner loc (TOptional value) = ROptional <$> resolveExpr names owner loc value+resolveExpr names owner loc (TList value) = RList <$> resolveExpr names owner loc value+resolveExpr names owner loc (TMap value) = RMap <$> resolveExpr names owner loc value+resolveExpr names owner loc (TRef name) =+    maybe (Left (TGUnresolvedRef owner name loc)) (Right . RRef) (Map.lookup name names)++cycleErrors :: Map MappedKey ResolvedMappedDecl -> [TypeGraphError]+cycleErrors declarations =+    [ TGRecursive (map unMappedKey keys)+    | CyclicSCC keys <- stronglyConnComp vertices+    ]+  where+    vertices =+        [ (key, key, Set.toList (directRefs declaration))+        | (key, declaration) <- Map.toList declarations+        ]++directRefs :: ResolvedMappedDecl -> Set MappedKey+directRefs =+    foldMappedDecl+        MappedDeclAlgebra+            { onStructuralDecl = \_ shape -> refsInShape shape+            , onOpaqueDecl = const Set.empty+            }++refsInShape :: ResolvedMappedShape -> Set MappedKey+refsInShape =+    foldMappedShape+        MappedShapeAlgebra+            { onRecord = \_ _ fields -> Set.unions (map (refsInExpr . rwfType) fields)+            , onEnum = const Set.empty+            , onUnion = \_ arms -> Set.unions (map (maybe Set.empty refsInExpr . rwaPayload) arms)+            }++refsInExpr :: ResolvedTypeExpr -> Set MappedKey+refsInExpr =+    foldTypeExpr+        TypeExprAlgebra+            { onText = Set.empty+            , onInt = Set.empty+            , onBool = Set.empty+            , onNatural = Set.empty+            , onTime = Set.empty+            , onJson = Set.empty+            , onOptional = id+            , onList = id+            , onMap = id+            , onRef = Set.singleton+            }++reachableFrom :: Map MappedKey ResolvedMappedDecl -> MappedKey -> ResolvedMappedDecl -> Set MappedKey+reachableFrom declarations origin declaration = go Set.empty (Set.toList (directRefs declaration))+  where+    go visited [] = Set.delete origin visited+    go visited (key : rest)+        | key `Set.member` visited = go visited rest+        | otherwise =+            let next = maybe [] (Set.toList . directRefs) (Map.lookup key declarations)+             in go (Set.insert key visited) (next ++ rest)++collectUseSites :: Map Name MappedKey -> Spec -> [UseSite]+collectUseSites keyByName spec = concatMap aggregateSites [aggregate | NAggregate aggregate <- specNodes spec]+  where+    aggregateSites aggregate =+        [ RootCommandField (aggName aggregate) (cmdName command) (fieldName field) key+        | command <- aggCommands aggregate+        , field <- cmdFields command+        , key <- maybeToList (fieldType field >>= (`Map.lookup` keyByName))+        ]+            ++ [ RootEventField (aggName aggregate) (evName event) (fieldName field) key+               | event <- aggEvents aggregate+               , field <- eventFields aggregate event+               , key <- maybeToList (fieldType field >>= (`Map.lookup` keyByName))+               ]+            ++ [ RootRegister (aggName aggregate) (regName register) key+               | register <- aggRegs aggregate+               , key <- maybeToList (Map.lookup (regType register) keyByName)+               ]++    eventFields aggregate event = case evBody event of+        EventFields fields -> fields+        EventFromCommand commandName ->+            concat [cmdFields command | command <- aggCommands aggregate, cmdName command == commandName]++    maybeToList = maybe [] pure++usePaths :: TypeGraph -> Name -> [UsePath]+usePaths graph targetName = case Map.lookup (MappedKey targetName) (tgDeclarations graph) of+    Nothing -> []+    Just _ ->+        [ UsePath site segments+        | site <- tgUseSites graph+        , segments <- sitePaths site+        ]+  where+    target = MappedKey targetName+    sitePaths site+        | siteKey site == target = [[]]+        | otherwise = pathsFromDecl Set.empty (siteKey site)++    pathsFromDecl visited current+        | current `Set.member` visited = []+        | otherwise = case Map.lookup current (tgDeclarations graph) of+            Nothing -> []+            Just declaration ->+                foldMappedDecl+                    MappedDeclAlgebra+                        { onStructuralDecl = \_ shape -> pathsInShape (Set.insert current visited) shape+                        , onOpaqueDecl = const []+                        }+                    declaration++    pathsInShape visited =+        foldMappedShape+            MappedShapeAlgebra+                { onRecord = \_ _ fields ->+                    concat+                        [ map (SegField (rwfHaskell field) (rwfKey field) :) (pathsInExpr visited (rwfType field))+                        | field <- fields+                        ]+                , onEnum = const []+                , onUnion = \_ arms ->+                    concat+                        [ map (SegArm (rwaCtor arm) (rwaTag arm) :) (maybe [] (pathsInExpr visited) (rwaPayload arm))+                        | arm <- arms+                        ]+                }++    pathsInExpr visited = \case+        RText -> []+        RInt -> []+        RBool -> []+        RNatural -> []+        RTime -> []+        RJson -> []+        ROptional value -> map (SegOptional :) (pathsInExpr visited value)+        RList value -> map (SegElem :) (pathsInExpr visited value)+        RMap value -> map (SegMapValue :) (pathsInExpr visited value)+        RRef key+            | key == target -> [[SegDecl (unMappedKey key)]]+            | otherwise -> map (SegDecl (unMappedKey key) :) (pathsFromDecl visited key)++siteKey :: UseSite -> MappedKey+siteKey (RootCommandField _ _ _ key) = key+siteKey (RootEventField _ _ _ key) = key+siteKey (RootRegister _ _ key) = key++renderUsePath :: UsePath -> Text+renderUsePath (UsePath root segments) = renderRoot root <> T.concat (map renderSegment segments)+  where+    renderRoot (RootCommandField aggregate command field key) =+        aggregate <> " command " <> command <> " ." <> field <> " : " <> unMappedKey key+    renderRoot (RootEventField aggregate event field key) =+        aggregate <> " event " <> event <> " ." <> field <> " : " <> unMappedKey key+    renderRoot (RootRegister aggregate register key) =+        aggregate <> " register " <> register <> " : " <> unMappedKey key++    renderSegment (SegField haskellName wireName)+        | haskellName == wireName = " ." <> haskellName+        | otherwise = " ." <> haskellName <> " as " <> quoted wireName+    renderSegment (SegArm _ wireTag) = " arm " <> quoted wireTag+    renderSegment SegElem = " []"+    renderSegment SegMapValue = " {}"+    renderSegment SegOptional = " optional"+    renderSegment (SegDecl name) = " : " <> name+    quoted value = T.pack (show value)++data TypeExprAlgebra a = TypeExprAlgebra+    { onText :: a+    , onInt :: a+    , onBool :: a+    , onNatural :: a+    , onTime :: a+    , onJson :: a+    , onOptional :: a -> a+    , onList :: a -> a+    , onMap :: a -> a+    , onRef :: MappedKey -> a+    }++foldTypeExpr :: TypeExprAlgebra a -> ResolvedTypeExpr -> a+foldTypeExpr algebra = \case+    RText -> onText algebra+    RInt -> onInt algebra+    RBool -> onBool algebra+    RNatural -> onNatural algebra+    RTime -> onTime algebra+    RJson -> onJson algebra+    ROptional value -> onOptional algebra (foldTypeExpr algebra value)+    RList value -> onList algebra (foldTypeExpr algebra value)+    RMap value -> onMap algebra (foldTypeExpr algebra value)+    RRef key -> onRef algebra key++data MappedShapeAlgebra a = MappedShapeAlgebra+    { onRecord :: Name -> UnknownFields -> [ResolvedWireField] -> a+    , onEnum :: [WireEnum] -> a+    , onUnion :: UnionEncoding -> [ResolvedWireArm] -> a+    }++foldMappedShape :: MappedShapeAlgebra a -> ResolvedMappedShape -> a+foldMappedShape algebra = \case+    RRecord constructor unknownFields fields -> onRecord algebra constructor unknownFields fields+    REnum entries -> onEnum algebra entries+    RUnion encoding arms -> onUnion algebra encoding arms++data MappedDeclAlgebra a = MappedDeclAlgebra+    { onStructuralDecl :: StructuralDecl -> ResolvedMappedShape -> a+    , onOpaqueDecl :: OpaqueDecl -> a+    }++foldMappedDecl :: MappedDeclAlgebra a -> ResolvedMappedDecl -> a+foldMappedDecl algebra = \case+    ResolvedStructural declaration shape -> onStructuralDecl algebra declaration shape+    ResolvedOpaque declaration -> onOpaqueDecl algebra declaration++wireFingerprint :: TypeGraph -> Name -> Text+wireFingerprint graph name = fnv1a64 (wireDecl Set.empty (MappedKey name))+  where+    declarations = tgDeclarations graph++    wireDecl visited key+        | key `Set.member` visited = "recursive"+        | otherwise = case Map.lookup key declarations of+            Nothing -> "missing:" <> unMappedKey key+            Just declaration ->+                foldMappedDecl+                    MappedDeclAlgebra+                        { onStructuralDecl = \_ shape -> wireShape (Set.insert key visited) shape+                        , onOpaqueDecl = \opaque ->+                            "opaque(" <> atom (unCodecIdentity (odCodecIdentity opaque)) <> "," <> atom (unCodecVersion (odCodecVersion opaque)) <> ")"+                        }+                    declaration++    wireShape visited =+        foldMappedShape+            MappedShapeAlgebra+                { onRecord = \_ unknownFields fields ->+                    "record(" <> renderUnknown unknownFields <> ";" <> T.intercalate ";" (map (wireField visited) (sortOn rwfKey fields)) <> ")"+                , onEnum = \entries ->+                    "enum(" <> T.intercalate ";" (map (atom . weTag) (sortOn weTag entries)) <> ")"+                , onUnion = \encoding arms ->+                    "union("+                        <> atom (ueTagField encoding)+                        <> ","+                        <> atom (ueContentsField encoding)+                        <> ","+                        <> renderUnknown (ueUnknownFields encoding)+                        <> ";"+                        <> T.intercalate ";" (map (wireArm visited) (sortOn rwaTag arms))+                        <> ")"+                }++    wireField visited field =+        atom (rwfKey field)+            <> ":"+            <> wireExpr visited (rwfType field)+            <> ":"+            <> renderPresence (rwfPresence field)+            <> ":"+            <> maybe "none" (renderDefault field) (rwfOnMissing field)++    wireArm visited arm = atom (rwaTag arm) <> maybe ":unit" ((":" <>) . wireExpr visited) (rwaPayload arm)++    wireExpr visited = \case+        RText -> "text"+        RInt -> "int"+        RBool -> "bool"+        RNatural -> "natural"+        RTime -> "time"+        RJson -> "json"+        ROptional value -> "optional(" <> wireExpr visited value <> ")"+        RList value -> "list(" <> wireExpr visited value <> ")"+        RMap value -> "map(" <> wireExpr visited value <> ")"+        RRef key -> wireDecl visited key++    renderDefault field (OmCtor constructor) =+        case rwfType field of+            RRef key -> case Map.lookup key declarations of+                Just (ResolvedStructural _ (REnum entries)) ->+                    maybe ("ctor:" <> atom constructor) ("enum:" <>) (lookup constructor [(weCtor entry, atom (weTag entry)) | entry <- entries])+                _ -> "ctor:" <> atom constructor+            _ -> "ctor:" <> atom constructor+    renderDefault _ value = T.pack (show value)++    renderUnknown RejectUnknown = "reject"+    renderUnknown IgnoreUnknown = "ignore"+    renderPresence PRequired = "required"+    renderPresence POptional = "optional"+    atom value = T.pack (show value)++fnv1a64 :: Text -> Text+fnv1a64 input =+    let offsetBasis = 14695981039346656037 :: Word64+        prime = 1099511628211 :: Word64+        digest = T.foldl' (\hash char -> (hash `xor` fromIntegral (ord char)) * prime) offsetBasis input+        hexadecimal = showHex digest ""+     in T.pack (replicate (16 - length hexadecimal) '0' <> hexadecimal)
src/Keiro/Dsl/Validate.hs view
@@ -21,6 +21,7 @@ import Data.Bits (xor) import Data.Char (isControl, isSpace, ord) import Data.List (sortOn)+import Data.List.NonEmpty qualified as NE import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set (Set)@@ -30,6 +31,7 @@ import Data.Word (Word64) import Keiro.Dsl.Grammar import Keiro.Dsl.ReadModelShape (deriveShapeHash)+import Keiro.Dsl.TypeGraph import Numeric (showHex)  data Severity = Error | Warning@@ -45,10 +47,14 @@     | GuardAtomOutOfScope     | StatusMapNotTotal     | ClockSampled-    | -- EP-2 (evolution). The first three fire in single-spec @validateSpec@;-      -- the last two are emitted by the @diff@ path (they need a prior spec) and-      -- live here so the enum is the single registry of evolution rules.+    | -- EP-2 (evolution). These codes are shared by single-spec validation and+      -- the cross-spec diff path, so the enum remains the single registry of+      -- evolution rules.       EvtVersionMissingUpcaster+    | DuplicateUpcasterSource+    | UpcasterChainGap+    | DeprecatedEventReplayHazard+    | EventRetirementInProgress     | DeprecatedEventStillEmitted     | WireSchemaVersionMismatch     | EvtFieldAddedWithoutBump@@ -183,6 +189,78 @@     | AmbiguousMarkedBenign     | AmbiguousFollowsRejectedPolicy     | RouterStableNameChanged+    | -- Plan 143 (first-class replay-only transitions for guard evolution).+      -- The first two fire in single-spec @validateSpec@; the third is the+      -- diff-path guard-tightening advisory that prints the computed+      -- replay-only twin.+      ReplayOnlyEmitsNothing+    | ReplayOnlyCommandStillLive+    | AggGuardTightened+    | AggFoldSurfaceChanged+    | RouterDecideSurfaceChanged+    | ProcessDecideSurfaceChanged+    | ProcessTimerPayloadChanged+    | -- MasterPlan 25 / EP-5: append-only codes for findings that were+      -- formerly additive but uncoded.+      DeclarationAdded+    | VersionBumped+    | CompatibilityStrengthened+    | EnumCtorAdded+    | EventRetirementAbandoned+    | ContractEventAdded+    | ContractTopicAdded+    | WorkflowEvolutionGuardAdded+    | -- MasterPlan 25 / EP-149 (consumer-owned mapped types).+      MappedUnresolvedName+    | MappedAmbiguousName+    | MappedDuplicateFieldName+    | MappedDuplicateWireKey+    | MappedDuplicateArmName+    | MappedDuplicateWireTag+    | MappedNonInjectiveNullability+    | MappedRecursiveType+    | MappedUnsupportedEncoding+    | MappedMissingIngredient+    | MappedMissingInitialValue+    | MappedInvalidHaskellName+    | MappedInvalidIdentity+    | MappedImportConflict+    | MappedDefaultIllTyped+    | MappedGuardUnsupported+    | -- MasterPlan 25 / EP-149 mapped evolution codes.+      MappedFieldAddedWithDefault+    | MappedFieldAddedNoDefault+    | MappedFieldRemoved+    | MappedFieldTypeChanged+    | MappedPresenceChanged+    | MappedNullabilityChanged+    | MappedDefaultRemoved+    | MappedDefaultChanged+    | MappedWireKeyChanged+    | MappedUnionEncodingChanged+    | MappedArmAdded+    | MappedArmRemoved+    | MappedArmTagChanged+    | MappedEnumValueAdded+    | MappedEnumValueRemoved+    | MappedEnumSpellingChanged+    | MappedHaskellSourceChanged+    | MappedRecordConstructorChanged+    | MappedBindingChanged+    | MappedFixturesChanged+    | MappedInitialChanged+    | MappedCanonicalTypeChanged+    | MappedOpaqueCodecChanged+    | MappedModeCrossed+    | MappedDeclAdded+    | MappedDeclRemoved+    | -- MasterPlan 25 / EP-152 reporting and migration-evidence codes.+      CoverageOpaqueSurface+    | CoverageOpaqueBoundaryAdded+    | CoverageOpaqueGateExceeded+    | CodecCompareDifference+    | CodecCompareCoverageGap+    | CodecCompareInvalidInput     deriving stock (Eq, Show)  -- | A line-numbered, structured diagnostic.@@ -222,8 +300,488 @@ -} validateSpec :: Spec -> [Diagnostic] validateSpec spec =-    sortOn line (validateNames spec ++ specLevelRules spec ++ concatMap (validateNode spec) (specNodes spec))+    sortOn line (validateNames spec ++ validateMapped spec ++ specLevelRules spec ++ concatMap (validateNode spec) (specNodes spec)) +{- | Validate consumer-owned mapped declarations without inspecting consumer+Haskell. Symbol-shaped facts are checked lexically here; GHC remains the+authority for whether the named packages, modules, values, types, and+instances actually exist with the promised types.+-}+validateMapped :: Spec -> [Diagnostic]+validateMapped spec =+    mappedLexicalRules spec+        ++ mappedIdentityRules spec+        ++ mappedConflictRules spec+        ++ case resolveTypeGraph spec of+            Left errors -> concatMap (typeGraphDiagnostic spec) (NE.toList errors)+            Right graph -> mappedGraphRules spec graph++typeGraphDiagnostic :: Spec -> TypeGraphError -> [Diagnostic]+typeGraphDiagnostic spec = \case+    TGDeclError name declarationError ->+        [ mkErr (mappedLine spec name) diagnosticCode $+            "mapped declaration '" <> name <> "': " <> declarationErrorMessage declarationError+        ]+      where+        diagnosticCode = case declarationError of+            MissingHaskellSource{} -> MappedMissingIngredient+            MissingStructuralBinding{} -> MappedMissingIngredient+            MissingStructuralBindingVersion{} -> MappedMissingIngredient+            MissingCanonicalType{} -> MappedMissingIngredient+            MissingFixtureCases{} -> MappedMissingIngredient+            MissingOpaqueCodecIdentity{} -> MappedMissingIngredient+            MissingOpaqueCodecVersion{} -> MappedMissingIngredient+            EmptyQualifiedValueName{} -> MappedInvalidHaskellName+            EmptyCanonicalTypeId{} -> MappedInvalidIdentity+            EmptyBindingVersion{} -> MappedInvalidIdentity+            EmptyCodecIdentity{} -> MappedInvalidIdentity+            EmptyCodecVersion{} -> MappedInvalidIdentity+    TGAmbiguousName name origins ->+        [ mkErr (mappedLine spec name) MappedAmbiguousName $+            "type name '" <> name <> "' is ambiguous across " <> T.intercalate ", " origins+        ]+    TGUnresolvedRef owner missing loc ->+        [ mkErr (locLine loc) MappedUnresolvedName $+            "mapped declaration '" <> owner <> "' references unresolved mapped type '" <> missing <> "'"+        ]+    TGRecursive names ->+        [ mkErr (mappedLine spec (headOr "<mapped>" names)) MappedRecursiveType $+            "recursive structural mapping is unsupported: " <> T.intercalate " -> " (names <> take 1 names)+        ]++declarationErrorMessage :: MappedDeclError -> Text+declarationErrorMessage = \case+    MissingHaskellSource _ -> "missing complete haskell package/module/type ingredient"+    MissingStructuralBinding _ -> "missing binding ingredient; GHC will verify the named value and its type"+    MissingStructuralBindingVersion _ -> "missing binding-version ingredient"+    MissingCanonicalType _ -> "missing canonical-type ingredient"+    MissingFixtureCases _ -> "missing fixtures ingredient; GHC will verify the named FixtureCases value"+    MissingOpaqueCodecIdentity _ -> "missing opaque codec identity ingredient"+    MissingOpaqueCodecVersion _ -> "missing opaque codec version ingredient"+    EmptyQualifiedValueName _ -> "a binding, fixture, or initial symbol is empty; GHC will verify a syntactically valid qualified value"+    EmptyCanonicalTypeId _ -> "canonical-type must be non-empty"+    EmptyBindingVersion _ -> "binding-version must be non-empty"+    EmptyCodecIdentity _ -> "opaque codec identity must be non-empty"+    EmptyCodecVersion _ -> "opaque codec version must be non-empty"++mappedLine :: Spec -> Name -> Int+mappedLine spec name =+    maybe 1 (locLine . mappedLoc) (firstMatching ((== name) . mappedName) (specMapped spec))++mappedName :: MappedDecl -> Name+mappedName MappedStructural{msName = name} = name+mappedName MappedOpaque{moName = name} = name++mappedLoc :: MappedDecl -> Loc+mappedLoc MappedStructural{msLoc = loc} = loc+mappedLoc MappedOpaque{moLoc = loc} = loc++mappedHaskell :: MappedDecl -> Maybe HaskellSource+mappedHaskell MappedStructural{msHaskell = source} = source+mappedHaskell MappedOpaque{moHaskell = source} = source++mappedCanonical :: MappedDecl -> Maybe Text+mappedCanonical MappedStructural{msCanonical = canonical} = canonical+mappedCanonical MappedOpaque{} = Nothing++mappedLexicalRules :: Spec -> [Diagnostic]+mappedLexicalRules spec = concatMap declarationRules (specMapped spec)+  where+    declarationRules declaration =+        constructorRule "mapped declaration name" (mappedName declaration) declaration+            ++ maybe [] (haskellRules declaration) (mappedHaskell declaration)+            ++ qualifiedFacts declaration+            ++ shapeConstructorRules declaration++    haskellRules declaration source =+        [ invalid declaration $ "Haskell package '" <> hsPackage source <> "' does not follow Cabal package-name grammar"+        | not (cabalPackageName (hsPackage source))+        ]+            ++ [ invalid declaration $ "Haskell module '" <> hsModule source <> "' must be dot-separated Upper identifiers"+               | not (moduleNameSafe (hsModule source))+               ]+            ++ [ invalid declaration $ "Haskell type '" <> hsType source <> "' must be an Upper identifier"+               | not (constructorSafe (hsType source))+               ]++    qualifiedFacts MappedStructural{msBinding = binding, msFixtures = fixtures, msInitial = initial, msLoc = loc} =+        concatMap (qualifiedRule loc) [("binding", binding), ("fixtures", fixtures), ("initial", initial)]+    qualifiedFacts MappedOpaque{moFixtures = fixtures, moInitial = initial, moLoc = loc} =+        concatMap (qualifiedRule loc) [("fixtures", fixtures), ("initial", initial)]++    qualifiedRule loc (category, value) = case value of+        Just symbol+            | not (T.null symbol) && not (qualifiedValueSafe symbol) ->+                [ mkErr (locLine loc) MappedInvalidHaskellName $+                    category <> " symbol '" <> symbol <> "' must be a module path plus a lower-initial value; GHC will verify that it exists with the promised type"+                ]+        _ -> []++    shapeConstructorRules declaration = case declaration of+        MappedStructural{msShape = ShapeRecord constructor _ fields} ->+            constructorRule "record constructor" constructor declaration+                ++ [ invalidAt (wireFieldLoc field) $ "record selector '" <> wfHaskell field <> "' must be a lower-initial Haskell identifier"+                   | field <- fields+                   , not (lowerIdentifierSafe (wfHaskell field))+                   ]+        MappedStructural{msShape = ShapeEnum entries} ->+            [ invalidAt (weLoc entry) $ "enum constructor '" <> weCtor entry <> "' must be an Upper identifier"+            | entry <- entries+            , not (constructorSafe (weCtor entry))+            ]+        MappedStructural{msShape = ShapeUnion _ arms} ->+            [ invalidAt (waLoc arm) $ "union constructor '" <> waCtor arm <> "' must be an Upper identifier"+            | arm <- arms+            , not (constructorSafe (waCtor arm))+            ]+        MappedOpaque{} -> []++    constructorRule category value declaration =+        [ invalid declaration $ category <> " '" <> value <> "' must be an Upper identifier"+        | not (constructorSafe value)+        ]+    invalid declaration detail = invalidAt (mappedLoc declaration) detail+    invalidAt loc detail =+        mkErr (locLine loc) MappedInvalidHaskellName (detail <> "; this is a syntax check only, and GHC will verify the consumer declaration")++mappedIdentityRules :: Spec -> [Diagnostic]+mappedIdentityRules spec =+    [ mkErr (locLine (mappedLoc declaration)) MappedInvalidIdentity $+        "mapped declaration '" <> mappedName declaration <> "' has an identity/version containing an ASCII control character"+    | declaration <- specMapped spec+    , value <- identityValues declaration+    , T.any asciiControl value+    ]+  where+    identityValues MappedStructural{msBindingVersion = bindingVersion, msCanonical = canonical} = present [bindingVersion, canonical]+    identityValues MappedOpaque{moCodecId = codecIdentity, moCodecVersion = codecVersion} = present [codecIdentity, codecVersion]+    present = foldr (maybe id (:)) []++mappedConflictRules :: Spec -> [Diagnostic]+mappedConflictRules spec = sourceCollisions ++ canonicalCollisions ++ packageCollisions+  where+    declarations = specMapped spec+    sourceFacts = [(declaration, source) | declaration <- declarations, source <- maybeToList (mappedHaskell declaration)]+    sourceCollisions =+        [ conflict declaration $+            "Haskell target '" <> hsModule source <> "." <> hsType source <> "' is claimed by more than one mapped declaration"+        | (declaration, source) <- duplicatesBy (\(_, value) -> (hsModule value, hsType value)) sourceFacts+        ]+    canonicalFacts = [(declaration, canonical) | declaration <- declarations, canonical <- maybeToList (mappedCanonical declaration), not (T.null canonical)]+    canonicalCollisions =+        [ conflict declaration $ "canonical-type '" <> canonical <> "' is claimed by more than one mapped declaration"+        | (declaration, canonical) <- duplicatesBy snd canonicalFacts+        ]+    moduleFacts = [(declaration, hsModule source, hsPackage source) | (declaration, source) <- sourceFacts]+    packageCollisions =+        [ conflict declaration $+            "Haskell module '" <> moduleName <> "' is declared from conflicting packages '" <> oldPackage <> "' and '" <> packageName <> "'"+        | (index, (declaration, moduleName, packageName)) <- zip [0 :: Int ..] moduleFacts+        , (_, oldModule, oldPackage) <- take index moduleFacts+        , oldModule == moduleName+        , oldPackage /= packageName+        ]+    conflict declaration detail = mkErr (locLine (mappedLoc declaration)) MappedImportConflict detail+    maybeToList = maybe [] pure++mappedGraphRules :: Spec -> TypeGraph -> [Diagnostic]+mappedGraphRules spec graph =+    concatMap declarationRules (Map.elems (tgDeclarations graph))+        ++ mappedRegisterInitialRules spec graph+        ++ if null (specMapped spec) then [] else mappedGuardRules spec graph+  where+    declarationRules =+        foldMappedDecl+            MappedDeclAlgebra+                { onStructuralDecl = \declaration shape ->+                    foldMappedShape (shapeRules declaration) shape+                , onOpaqueDecl = const []+                }++    shapeRules declaration =+        MappedShapeAlgebra+            { onRecord = \_ _ fields ->+                [ mappedError (rwfLoc field) MappedDuplicateFieldName declaration $+                    "record selector '" <> rwfHaskell field <> "' is declared more than once"+                | field <- duplicatesBy rwfHaskell fields+                ]+                    ++ [ mappedError (rwfLoc field) MappedDuplicateWireKey declaration $+                            "record wire key '" <> rwfKey field <> "' is declared more than once"+                       | field <- duplicatesBy rwfKey fields+                       ]+                    ++ [ mappedError (rwfLoc field) MappedUnsupportedEncoding declaration "record wire keys must be non-empty"+                       | field <- fields+                       , T.null (rwfKey field)+                       ]+                    ++ concatMap (fieldRules declaration) fields+            , onEnum = \entries ->+                [ mappedError (weLoc entry) MappedDuplicateArmName declaration $+                    "enum constructor '" <> weCtor entry <> "' is declared more than once"+                | entry <- duplicatesBy weCtor entries+                ]+                    ++ [ mappedError (weLoc entry) MappedDuplicateWireTag declaration $+                            "enum wire spelling '" <> weTag entry <> "' is declared more than once"+                       | entry <- duplicatesBy weTag entries+                       ]+                    ++ [ mappedError (weLoc entry) MappedUnsupportedEncoding declaration "enum wire spellings must be non-empty"+                       | entry <- entries+                       , T.null (weTag entry)+                       ]+            , onUnion = \encoding arms ->+                [ mappedError (sdLoc declaration) MappedUnsupportedEncoding declaration "tagged-object tag and contents keys must be distinct"+                | ueTagField encoding == ueContentsField encoding+                ]+                    ++ [ mappedError (sdLoc declaration) MappedUnsupportedEncoding declaration "tagged-object tag and contents keys must be non-empty"+                       | T.null (ueTagField encoding) || T.null (ueContentsField encoding)+                       ]+                    ++ [ mappedError (rwaLoc arm) MappedDuplicateArmName declaration $+                            "union constructor '" <> rwaCtor arm <> "' is declared more than once"+                       | arm <- duplicatesBy rwaCtor arms+                       ]+                    ++ [ mappedError (rwaLoc arm) MappedDuplicateWireTag declaration $+                            "union wire tag '" <> rwaTag arm <> "' is declared more than once"+                       | arm <- duplicatesBy rwaTag arms+                       ]+                    ++ [ mappedError (rwaLoc arm) MappedUnsupportedEncoding declaration "union wire tags must be non-empty"+                       | arm <- arms+                       , T.null (rwaTag arm)+                       ]+                    ++ concatMap (armRules declaration) arms+            }++    fieldRules declaration field =+        defaultRules declaration field+            ++ [ mappedError (rwfLoc field) MappedNonInjectiveNullability declaration $+                    "field '" <> rwfHaskell field <> "' contains Optional around a null-capable Json, Optional, or opaque mapped value"+               | hasNonInjectiveOptional graph (rwfType field)+               ]++    armRules declaration arm =+        [ mappedError (rwaLoc arm) MappedNonInjectiveNullability declaration $+            "union arm '" <> rwaCtor arm <> "' contains Optional around a null-capable Json, Optional, or opaque mapped value"+        | payload <- maybeToList (rwaPayload arm)+        , hasNonInjectiveOptional graph payload+        ]++    defaultRules declaration field = case (rwfPresence field, rwfOnMissing field) of+        (PRequired, Just _) -> [illTyped "required fields cannot declare on-missing"]+        (POptional, Nothing) ->+            [ mappedError (rwfLoc field) MappedMissingIngredient declaration $+                "optional field '" <> rwfHaskell field <> "' is missing its on-missing policy"+            ]+        (POptional, Just value)+            | not (defaultMatches graph (rwfType field) value) -> [illTyped "on-missing value does not match the field type or numeric bounds"]+        _ -> []+      where+        illTyped detail =+            mappedError (rwfLoc field) MappedDefaultIllTyped declaration $+                "field '" <> rwfHaskell field <> "': " <> detail++    mappedError loc diagnosticCode declaration detail =+        mkErr (locLine loc) diagnosticCode $+            "mapped declaration '" <> sdName declaration <> "' " <> detail+    maybeToList = maybe [] pure++data DefaultType+    = DefaultText+    | DefaultInt+    | DefaultBool+    | DefaultNatural+    | DefaultOptional+    | DefaultList+    | DefaultMap+    | DefaultEnum !(Set Name)+    | DefaultOther++defaultMatches :: TypeGraph -> ResolvedTypeExpr -> OnMissing -> Bool+defaultMatches graph expression value = case (defaultType graph expression, value) of+    (DefaultText, OmText _) -> True+    (DefaultInt, OmInt integer) -> integer >= toInteger (minBound :: Int) && integer <= toInteger (maxBound :: Int)+    (DefaultBool, OmBool _) -> True+    (DefaultNatural, OmInt integer) -> integer >= 0+    (DefaultOptional, OmNull) -> True+    (DefaultList, OmEmptyList) -> True+    (DefaultMap, OmEmptyMap) -> True+    (DefaultEnum constructors, OmCtor constructor) -> constructor `Set.member` constructors+    _ -> False++defaultType :: TypeGraph -> ResolvedTypeExpr -> DefaultType+defaultType graph =+    foldTypeExpr+        TypeExprAlgebra+            { onText = DefaultText+            , onInt = DefaultInt+            , onBool = DefaultBool+            , onNatural = DefaultNatural+            , onTime = DefaultOther+            , onJson = DefaultOther+            , onOptional = const DefaultOptional+            , onList = const DefaultList+            , onMap = const DefaultMap+            , onRef = referencedDefaultType graph+            }++referencedDefaultType :: TypeGraph -> MappedKey -> DefaultType+referencedDefaultType graph key = case Map.lookup key (tgDeclarations graph) of+    Nothing -> DefaultOther+    Just declaration ->+        foldMappedDecl+            MappedDeclAlgebra+                { onStructuralDecl = \_ shape ->+                    foldMappedShape+                        MappedShapeAlgebra+                            { onRecord = \_ _ _ -> DefaultOther+                            , onEnum = DefaultEnum . Set.fromList . map weCtor+                            , onUnion = \_ _ -> DefaultOther+                            }+                        shape+                , onOpaqueDecl = const DefaultOther+                }+            declaration++data NullabilityFacts = NullabilityFacts+    { nfTopNull :: !Bool+    , nfBadOptional :: !Bool+    }++hasNonInjectiveOptional :: TypeGraph -> ResolvedTypeExpr -> Bool+hasNonInjectiveOptional graph =+    nfBadOptional+        . foldTypeExpr+            TypeExprAlgebra+                { onText = nonNull+                , onInt = nonNull+                , onBool = nonNull+                , onNatural = nonNull+                , onTime = nonNull+                , onJson = nullable+                , onOptional = \child -> NullabilityFacts True (nfTopNull child || nfBadOptional child)+                , onList = nestedNonNull+                , onMap = nestedNonNull+                , onRef = \key -> if mappedRefIsOpaque graph key then nullable else nonNull+                }+  where+    nonNull = NullabilityFacts False False+    nullable = NullabilityFacts True False+    nestedNonNull child = NullabilityFacts False (nfBadOptional child)++mappedRefIsOpaque :: TypeGraph -> MappedKey -> Bool+mappedRefIsOpaque graph key = case Map.lookup key (tgDeclarations graph) of+    Nothing -> False+    Just declaration ->+        foldMappedDecl+            MappedDeclAlgebra+                { onStructuralDecl = \_ _ -> False+                , onOpaqueDecl = const True+                }+            declaration++mappedRegisterInitialRules :: Spec -> TypeGraph -> [Diagnostic]+mappedRegisterInitialRules spec graph =+    concatMap aggregateRules [aggregate | NAggregate aggregate <- specNodes spec]+  where+    aggregateRules aggregate = concatMap registerRule (aggRegs aggregate)+    registerRule register = case Map.lookup (MappedKey (regType register)) (tgDeclarations graph) of+        Nothing -> []+        Just declaration -> case regInitial register of+            RegInitBare "initial"+                | mappedInitial declaration == Nothing ->+                    [ mkErr (locLine (regLoc register)) MappedMissingInitialValue $+                        "mapped register '" <> regName register <> "' requires declaration '" <> regType register <> "' to name an explicit initial value"+                    ]+                | otherwise -> []+            _ ->+                [ mkErr (locLine (regLoc register)) RegisterInitialOutOfScope $+                    "mapped register '" <> regName register <> "' must use the bare initial token; the declaration-owned symbol is verified by GHC"+                ]+    mappedInitial =+        foldMappedDecl+            MappedDeclAlgebra+                { onStructuralDecl = \declaration _ -> sdInitial declaration+                , onOpaqueDecl = odInitial+                }++{- | Mapped values support whole-value writes and event copies, but guards may+only operate on Keiki's curated scalar set. Nested access has no spelling in+the grammar, so it is unrepresentable rather than silently accepted.+-}+mappedGuardRules :: Spec -> TypeGraph -> [Diagnostic]+mappedGuardRules spec graph =+    [ mkErr (locLine (tLoc transition)) MappedGuardUnsupported $+        "guard operand '" <> operand <> "' has non-symbolic type '" <> operandType <> "'; mapped values support whole-value copy, while guards are limited to Text, Int, Bool, and Time"+    | NAggregate aggregate <- specNodes spec+    , transition <- aggTransitions aggregate+    , guardExpression <- maybe [] pure (tGuard transition)+    , operand <- dedup (exprNames guardExpression)+    , operandType <- maybeToList (guardOperandType aggregate transition operand)+    , not (guardTypeSupported graph operandType)+    ]+  where+    maybeToList = maybe [] pure++guardOperandType :: Aggregate -> Transition -> Name -> Maybe Name+guardOperandType aggregate transition operand =+    case [regType register | register <- aggRegs aggregate, regName register == operand] of+        value : _ -> Just value+        [] -> case [fieldType field | command <- aggCommands aggregate, cmdName command == tCommand transition, field <- cmdFields command, fieldName field == operand] of+            value : _ -> value+            [] -> Nothing++guardTypeSupported :: TypeGraph -> Name -> Bool+guardTypeSupported graph typeName =+    typeName `Set.member` Set.fromList ["Text", "Int", "Bool", "Time", "UTCTime"]+        && Map.notMember (MappedKey typeName) (tgDeclarations graph)++cabalPackageName :: Text -> Bool+cabalPackageName packageName =+    not (null components) && all validComponent components+  where+    components = T.splitOn "-" packageName+    validComponent component =+        not (T.null component)+            && T.all asciiAlphaNum component+            && T.any asciiLetter component++moduleNameSafe :: Text -> Bool+moduleNameSafe moduleName =+    not (null components) && all constructorSafe components+  where+    components = T.splitOn "." moduleName++qualifiedValueSafe :: Text -> Bool+qualifiedValueSafe qualified = case reverse (T.splitOn "." qualified) of+    value : reversedModule ->+        not (null reversedModule)+            && lowerIdentifierSafe value+            && all constructorSafe reversedModule+    [] -> False++lowerIdentifierSafe :: Text -> Bool+lowerIdentifierSafe name = case T.uncons name of+    Just (first, rest) -> asciiLower first && T.all asciiAlphaNumOrUnderscore rest && name `Set.notMember` haskellKeywords+    Nothing -> False++asciiAlphaNum :: Char -> Bool+asciiAlphaNum c = asciiLetter c || (c >= '0' && c <= '9')++asciiLetter :: Char -> Bool+asciiLetter c = asciiUpper c || asciiLower c++asciiControl :: Char -> Bool+asciiControl c = ord c < 32 || ord c == 127++firstMatching :: (a -> Bool) -> [a] -> Maybe a+firstMatching predicate = \case+    [] -> Nothing+    value : rest+        | predicate value -> Just value+        | otherwise -> firstMatching predicate rest++headOr :: a -> [a] -> a+headOr fallback = \case+    [] -> fallback+    value : _ -> value+ {- | Reject names that would make the scaffolder emit illegal Haskell. The parser enforces the ASCII alphabet; this pass applies the category-specific uppercase/lowercase and keyword rules that require AST context.@@ -263,7 +821,7 @@                 ++ concatMap (\field -> fieldNameRule "workqueue payload field" (wqfName field) (wqLoc workqueue)) (wqPayload workqueue)         NPgmqDispatch dispatch -> pascalizedNodeName "dispatch" (pdName dispatch) (pdLoc dispatch)         NReadModel readModel -> pascalizedNodeName "readmodel" (rmName readModel) (rmLoc readModel)-        NWorkflow workflow -> constructorName "workflow name" (wfId workflow) (wfLoc workflow)+        NWorkflow workflow -> constructorName "workflow name" (wfId workflow) (workflowNodeLoc workflow)         NOperation _ -> []      aggregateNames aggregate =@@ -450,7 +1008,7 @@ nodeIdentity (NWorkqueue w) = ("workqueue", wqName w, wqLoc w) nodeIdentity (NPgmqDispatch d) = ("dispatch", pdName d, pdLoc d) nodeIdentity (NReadModel r) = ("readmodel", rmName r, rmLoc r)-nodeIdentity (NWorkflow w) = ("workflow", wfId w, wfLoc w)+nodeIdentity (NWorkflow w) = ("workflow", wfId w, workflowNodeLoc w) nodeIdentity (NOperation o) = ("operation", opName o, opLoc o)  validateNode :: Spec -> Node -> [Diagnostic]@@ -505,7 +1063,7 @@     idField = case wfIdField w of         Just field             | field `notElem` inputFields ->-                [ mkErr (locLine (wfLoc w)) WorkflowIdFieldUnresolved $+                [ mkErr (locLine (workflowNodeLoc w)) WorkflowIdFieldUnresolved $                     "workflow '" <> wfId w <> "' derives its id from undeclared input field '" <> field <> "'"                 ]         _ -> []@@ -1230,6 +1788,7 @@         , statusMapTotality         , evolutionRules         , snapshotRules+        , replayOnlyRules         ]   where     states = Set.fromList (map stName (aggStates agg))@@ -1439,9 +1998,26 @@                        ]      -- EP-2 evolution rules (single-spec; the diff path adds the cross-spec ones).-    evolutionRules = versionUpcasterRule ++ deprecatedEmitRule ++ wireVersionRule-    emittedNames = Set.fromList (concatMap tEmits (aggTransitions agg))+    evolutionRules =+        versionUpcasterRule+            ++ duplicateUpcasterSourceRule+            ++ upcasterChainGapRule+            ++ deprecatedEmitRule+            ++ eventRetirementRules+            ++ wireVersionRule+    -- Only live transitions are the write path: a replay-only transition can+    -- never fire forward, so its emits exist purely to invert stored events —+    -- which is exactly where a deprecated event is allowed to remain+    -- (plan 143; supersedes the guarded-but-inert retained-edge pattern).+    liveEmittedNames = Set.fromList (concatMap tEmits [t | t <- aggTransitions agg, tMode t == TmLive])+    replayEmittedNames = Set.fromList (concatMap tEmits [t | t <- aggTransitions agg, tMode t == TmReplayOnly])     maxEventVersion = maximum (1 : map evVersion (aggEvents agg))+    upcasterSources =+        Set.fromList+            [ source+            | event <- aggEvents agg+            , Just (source, _) <- [evUpcastFrom event]+            ]      -- A non-initial event version must carry a contiguous upcaster (from v-1).     versionUpcasterRule =@@ -1452,15 +2028,82 @@         , maybe True ((/= evVersion e - 1) . fst) (evUpcastFrom e)         ] +    -- A generated rung dispatches by event type, so different events may+    -- deliberately share a source version when they changed in one release.+    -- Duplicate declarations for one event cannot survive the parser's unique+    -- event-name rule, so no additional duplicate-source diagnostic is needed.+    duplicateUpcasterSourceRule =+        []++    -- Aggregate schema stamps are global, so every source version below the+    -- current maximum needs a permanent rung regardless of which event owns it.+    upcasterChainGapRule =+        [ mkErr (locLine (aggLoc agg)) UpcasterChainGap $+            "no event declares 'upcast from v"+                <> tInt missing+                <> "'; stored payloads stamped v"+                <> tInt missing+                <> " can never reach v"+                <> tInt maxEventVersion+                <> " (GapInUpcasterChain at hydration). A rung, once shipped, must exist forever — restore the upcaster for v"+                <> tInt missing+                <> " (re-declare it on the event whose shape changed at v"+                <> tInt (missing + 1)+                <> ")"+        | missing <- [1 .. maxEventVersion - 1]+        , missing `Set.notMember` upcasterSources+        ]+     -- A deprecated event must have left the write path.     deprecatedEmitRule =         [ mkErr (locLine (evLoc e)) DeprecatedEventStillEmitted $             "deprecated event '" <> evName e <> "' is still emitted by a transition"         | e <- aggEvents agg         , evDeprecated e-        , evName e `Set.member` emittedNames+        , evName e `Set.member` liveEmittedNames         ] +    -- Retirement is a two-stage protocol. The pre-cutover marker keeps a live+    -- emitter. The deprecated stage removes that live emitter but retains a+    -- replay-only emitter until old payloads no longer need hydration.+    eventRetirementRules = concatMap eventRetirementRule (aggEvents agg)+    eventRetirementRule event+        | evRetiring event =+            [ mkErr (locLine (evLoc event)) EventRetirementInProgress $+                "retiring event '" <> evName event <> "' has no live emitting transition; keep it emitting while streams are terminalized or truncated, or cut over to 'deprecated event' with a replay-only emitting transition"+            | evName event `Set.notMember` liveEmittedNames+            ]+                ++ [ Diagnostic+                        { line = locLine (evLoc event)+                        , severity = Warning+                        , code = EventRetirementInProgress+                        , message =+                            "event '" <> evName event <> "' is retiring: it stays fully live and replayable. Keep its live emitting transition until every affected stream is terminal or truncated; then flip it to 'deprecated event' and retain an equivalent replay-only emitting transition for as long as old payloads may be hydrated"+                        }+                   | evName event `Set.member` liveEmittedNames+                   ]+        | evDeprecated event =+            [ Diagnostic+                { line = locLine (evLoc event)+                , severity = Warning+                , code = DeprecatedEventReplayHazard+                , message =+                    "deprecated event '" <> evName event <> "' stays decodable but is not replayable: no replay-only transition emits it, so hydration of a live stream containing it fails with HydrationNoInvertingEdge. Restore an equivalent replay-only emitting transition, or terminalize/truncate every affected stream before deployment"+                }+            | any (not . stTerminal) (aggStates agg)+            , evName event `Set.notMember` replayEmittedNames+            ]+                ++ [ Diagnostic+                        { line = locLine (evLoc event)+                        , severity = Warning+                        , code = EventRetirementInProgress+                        , message =+                            "deprecated event '" <> evName event <> "' is off the live write path and remains replayable through a replay-only transition; retain that transition until every stream containing the event is terminal, truncated, or passes the replay audit"+                        }+                   | evName event `Set.member` replayEmittedNames+                   ]+        | otherwise = []+     -- The explicit `wire schemaVersion=` (if any) must equal the max event version.     wireVersionRule = case aggWire agg of         Just w@@ -1474,6 +2117,29 @@                     }                 ]         _ -> []++    -- Plan 143: replay-only transition discipline. A replay-only transition+    -- exists to invert stored events, so one that emits nothing is dead+    -- weight (error); one whose (source, command) pair has no live sibling+    -- means the command is fully retired at that state — legitimate, but the+    -- fuller procedure is event retirement (docs/plans/139), so warn.+    replayOnlyRules = concatMap replayOnlyRule (aggTransitions agg)+    replayOnlyRule t+        | tMode t /= TmReplayOnly = []+        | otherwise =+            [ mkErr (locLine (tLoc t)) ReplayOnlyEmitsNothing $+                "replay-only transition '" <> tSource t <> " -- " <> tCommand t <> "' emits no event; a replay-only transition exists to invert stored events and is dead weight without an emit"+            | null (tEmits t)+            ]+                ++ [ Diagnostic+                        { line = locLine (tLoc t)+                        , severity = Warning+                        , code = ReplayOnlyCommandStillLive+                        , message =+                            "replay-only transition '" <> tSource t <> " -- " <> tCommand t <> "' has no live sibling; command '" <> tCommand t <> "' is fully retired at state '" <> tSource t <> "' — if the intent is to retire its events too, follow the event-retirement procedure (docs/plans/139)"+                        }+                   | not (any (\sibling -> tMode sibling == TmLive && tSource sibling == tSource t && tCommand sibling == tCommand t) (aggTransitions agg))+                   ]  {- | The validator's re-derivation of the live 'Keiro.PGMQ.Runtime.queueRef' trio: physical queue, dead-letter queue, and
test/Main.hs view
@@ -1,2462 +1,4206 @@-{- | Test driver for keiro-dsl. EP-1 milestone 1 tests: the @parse . pretty@-round-trip property over generated specs, and a unit test pinning the shape-of the canonical Reservation fixture.--}-module Main (main) where--import Control.Exception (bracket)-import Control.Monad (filterM, forM_)-import Data.Either (isLeft)-import Data.List (partition, sort)-import Data.Text qualified as T-import Data.Text.IO qualified as TIO-import Keiro.Dsl.Diff (Change (..), ChangeKind (..), FamilyDiff (..), NodeFamily, diffSpecs, familyRegistry, isAdvisory, isBreaking)-import Keiro.Dsl.Grammar-import Keiro.Dsl.Harness (harnessFor, harnessReadModel, harnessRouter, harnessWorkflow)-import Keiro.Dsl.Manifest (manifestDependencies, moduleNameOf, renderManifest)-import Keiro.Dsl.Parser (parseSpec)-import Keiro.Dsl.PrettyPrint (renderSpec)-import Keiro.Dsl.ReadModelShape (canonicalShape, deriveShapeHash, registryNameFor, subscriptionNameFor)-import Keiro.Dsl.Scaffold (Context (..), ModuleKind (..), ScaffoldModule (..), defaultContext, firewallBreaches, genPrefixFor, holePrefixFor, scaffoldAggregate, scaffoldIntake, scaffoldProcess, scaffoldPublisher, scaffoldReadModel, scaffoldRefusals, scaffoldRouter, scaffoldWorkqueue, windowSeconds)-import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName)-import Keiro.Dsl.ScaffoldRun (Refusal (..), ScaffoldReport (..), StaleModule (..), executeScaffold, planScaffold, renderScaffoldReport, scaffoldModules)-import Keiro.Dsl.Skeleton (skeletonFor, skeletonKinds)-import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), derivedQueueTrio, validateSpec)-import System.Directory (createDirectory, createDirectoryIfMissing, doesFileExist, getTemporaryDirectory, removeFile, removePathForcibly)-import System.Environment (lookupEnv)-import System.FilePath (takeDirectory, (</>))-import System.IO (hClose, openTempFile)-import Test.Hspec hiding (Spec)-import Test.QuickCheck--main :: IO ()-main = hspec $ do-    describe "parse . pretty round-trip" $-        do-            it "re-parses any generated spec to an equal AST (modulo source locations)" $-                checkCoverage $-                    forAll genSpec $ \s ->-                        let families = map nodeTag (specNodes s)-                            roundTrip = parseSpec "<gen>" (renderSpec s) === Right s-                         in foldr (\family -> cover 1 (family `elem` families) family) roundTrip allNodeTags-            it "round-trips an aggregate with no states" $-                parseSpec "<empty-states>" (renderSpec emptyStatesSpec) `shouldBe` Right emptyStatesSpec-            it "separates transition emit clauses from following nodes" $ do-                spec <- parseInlineSpec "<cross-family-boundaries>" crossFamilyBoundarySpec-                case specNodes spec of-                    [NAggregate first, NEmit _, NAggregate second, NPgmqDispatch _] -> do-                        concatMap tEmits (aggTransitions first) `shouldBe` ["Changed"]-                        aggStates second `shouldBe` []-                    nodes -> expectationFailure ("unexpected node sequence: " <> show (map nodeTag nodes))--    describe "string literal integrity" $ do-        it "parses an escaped emit-map value as exactly one row" $ do-            let src =-                    T.unlines-                        [ "context svc"-                        , ""-                        , "emit e {"-                        , "  contract c"-                        , "  topic events"-                        , "  source \"svc\""-                        , "  key thingId"-                        , "  map status {"-                        , "    \"a\\\" => Wat \\\"b\" => ThingAccepted"-                        , "    _ => skip"-                        , "  }"-                        , "  messageId derive hole"-                        , "  idempotencyKey derive hole"-                        , "}"-                        ]-            case parseSpec "<escaped-map>" src of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> case [row | NEmit e <- specNodes spec, row <- emMap e] of-                    [row] -> do-                        emrValue row `shouldBe` "a\" => Wat \"b"-                        emrEvent row `shouldBe` "ThingAccepted"-                    rows -> expectationFailure ("expected one emit-map row, got " <> show (length rows))-        it "rejects a raw newline inside a quoted string" $ do-            let src = "context svc\n\ncontract c {\n  schemaVersion 1\n  discriminator kind\n  topic events \"first\nsecond\"\n}\n"-            parseSpec "<raw-newline>" src `shouldSatisfy` leftContains "unescaped newline"-        it "rejects an unknown escape sequence" $ do-            let src = "context svc\n\ncontract c {\n  schemaVersion 1\n  discriminator kind\n  topic events \"bad\\q\"\n}\n"-            parseSpec "<unknown-escape>" src `shouldSatisfy` leftContains "unknown escape"-        it "round-trips adversarial text through topics, emit maps, and quoted bindings" $-            property $-                forAll genAdversarialText $ \t ->-                    let spec = escapedSpec t-                        rendered = renderSpec spec-                     in counterexample (T.unpack rendered) (parseSpec "<escaped-round-trip>" rendered === Right spec)--    describe "partial status maps" $ do-        it "suppresses totality only when the partial marker is present" $ do-            partial <- parseInlineSpec "<partial-status-map>" (statusMapSpec " partial")-            totalSpec <- parseInlineSpec "<total-status-map>" (statusMapSpec "")-            map code (validateSpec partial) `shouldNotContain` [StatusMapNotTotal]-            map code (validateSpec totalSpec) `shouldContain` [StatusMapNotTotal]-            parseSpec "<partial-round-trip>" (renderSpec partial) `shouldBe` Right partial--    describe "positioned parser diagnostics" $ do-        it "rejects a duplicate goto at the second clause" $ do-            err <- parseErrorOf "<duplicate-goto>" duplicateGotoSpec-            err `shouldSatisfy` T.isInfixOf "duplicate goto"-            err `shouldSatisfy` T.isInfixOf "<duplicate-goto>:10:"-        it "rejects duplicate wire and projection blocks at their second occurrences" $ do-            wireErr <- parseErrorOf "<duplicate-wire>" duplicateWireSpec-            wireErr `shouldSatisfy` T.isInfixOf "duplicate wire block"-            wireErr `shouldSatisfy` T.isInfixOf "<duplicate-wire>:8:"-            projectionErr <- parseErrorOf "<duplicate-projection>" duplicateProjectionSpec-            projectionErr `shouldSatisfy` T.isInfixOf "duplicate projection block"-            projectionErr `shouldSatisfy` T.isInfixOf "<duplicate-projection>:9:"-        it "anchors a missing goto on the transition line" $ do-            err <- parseErrorOf "<missing-goto>" missingGotoSpec-            err `shouldSatisfy` T.isInfixOf "missing a goto clause"-            err `shouldSatisfy` T.isInfixOf "<missing-goto>:8:"-        it "stops before a misplaced dispatch-id and expects schedule at its start" $ do-            let src = misplacedDispatchIdSpec-                expectedPosition =-                    "<misplaced-dispatch-id>:"-                        <> T.pack (show (lineNumberContaining "dispatch-id" src))-                        <> ":5:"-            err <- parseErrorOf "<misplaced-dispatch-id>" src-            err `shouldSatisfy` T.isInfixOf "schedule"-            err `shouldSatisfy` T.isInfixOf expectedPosition-        it "keeps a malformed register declaration's equals error" $ do-            err <- parseErrorOf "<malformed-register>" malformedRegisterSpec-            err `shouldSatisfy` T.isInfixOf "expecting '='"--    describe "bounded decimal literals" $ do-        forM_ decimalOverflowSpecs $ \(site, src) ->-            it ("rejects overflow at " <> site) $ do-                err <- parseErrorOf ("<overflow-" <> site <> ">") src-                err `shouldSatisfy` T.isInfixOf ("decimal literal " <> decimalOverflow <> " is out of range")-        it "accepts maxBound without changing its value" $ do-            spec <- parseInlineSpec "<max-bound>" (wireDecimalSpec (T.pack (show (maxBound :: Int))))-            [wireSchemaVersion wire | NAggregate aggregate <- specNodes spec, Just wire <- [aggWire aggregate]]-                `shouldBe` [maxBound]--    describe "identifier hygiene" $ do-        it "reports constructor shape and Haskell keywords at their owning declarations" $ do-            spec <- parseInlineSpec "<identifier-hygiene>" identifierHygieneSpec-            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic `elem` [IdentNotConstructorSafe, IdentHaskellKeyword]]-                `shouldContain` [(IdentNotConstructorSafe, 3), (IdentHaskellKeyword, 7)]-        it "rejects generated vertex constructors that collide with event constructors" $ do-            spec <- parseInlineSpec "<vertex-collision>" vertexCollisionSpec-            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic == VertexCtorCollision]-                `shouldBe` [(VertexCtorCollision, 3)]-        it "rejects underscore-leading names whose title-casing cannot make a module segment" $ do-            spec <- parseInlineSpec "<underscore-node>" underscoreNodeSpec-            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic == IdentNotConstructorSafe]-                `shouldBe` [(IdentNotConstructorSafe, 3)]-        it "rejects non-ASCII identifier characters in the parser" $-            parseSpec "<unicode-identifier>" unicodeIdentifierSpec `shouldSatisfy` leftContains "unexpected"--    describe "canonical reservation.keiro" $-        it "parses into the expected aggregate shape" $ do-            input <- readTestText "test/fixtures/reservation.keiro"-            case parseSpec "test/fixtures/reservation.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> do-                    specContext spec `shouldBe` "hospital-capacity"-                    length (specIds spec) `shouldBe` 3-                    length (specEnums spec) `shouldBe` 3-                    length (specRules spec) `shouldBe` 1-                    case specNodes spec of-                        [NAggregate a] -> do-                            aggName a `shouldBe` "Reservation"-                            length (aggStates a) `shouldBe` 6-                            length (aggCommands a) `shouldBe` 2-                            length (aggEvents a) `shouldBe` 2-                            length (aggTransitions a) `shouldBe` 2-                            map stTerminal (aggStates a) `shouldBe` [False, False, False, True, True, True]-                        other -> expectationFailure ("expected one aggregate node, got " <> show (length other))--    describe "validator" $ do-        it "accepts the canonical reservation.keiro" $ do-            codes <- errorCodesOf "test/fixtures/reservation.keiro"-            codes `shouldBe` []-        it "rejects a missing status-map as StatusMapNotTotal" $ do-            codes <- diagnosticCodesOf "test/fixtures/reservation-no-statusmap.keiro"-            codes `shouldContain` [StatusMapNotTotal]-        it "rejects an undeclared command as UndeclaredCommand" $ do-            codes <- diagnosticCodesOf "test/fixtures/reservation-bad-command.keiro"-            codes `shouldContain` [UndeclaredCommand]-        it "rejects a wall-clock guard atom as ClockSampled" $ do-            codes <- diagnosticCodesOf "test/fixtures/reservation-clock.keiro"-            codes `shouldContain` [ClockSampled]-        it "accepts a v2 event with a contiguous upcaster hole" $ do-            codes <- errorCodesOf "test/fixtures/reservation-v2.keiro"-            codes `shouldBe` []-        it "rejects a v2 event with no upcaster as EvtVersionMissingUpcaster" $ do-            codes <- diagnosticCodesOf "test/fixtures/reservation-v2-noupcast.keiro"-            codes `shouldContain` [EvtVersionMissingUpcaster]-        it "requires exact, unique status-map event keys" $ do-            dangling <- errorCodesOf "test/fixtures/statusmap-dangling.keiro"-            mapM_ (\expected -> dangling `shouldContain` [expected]) [StatusMapDanglingKey, StatusMapNotTotal]-            duplicate <- errorCodesOf "test/fixtures/statusmap-dup-key.keiro"-            duplicate `shouldContain` [StatusMapDuplicateKey]-        it "rejects duplicate spec and aggregate names" $ do-            codes <- errorCodesOf "test/fixtures/duplicate-names.keiro"-            mapM_-                (\expected -> codes `shouldContain` [expected])-                [ DuplicateNodeName-                , DuplicateEnumCtor-                , DuplicateEnumWire-                , DuplicateIdPrefix-                , DuplicateCommandName-                , DuplicateEventName-                ]-        it "rejects aggregate-local references that do not resolve" $ do-            codes <- errorCodesOf "test/fixtures/aggregate-bad-refs.keiro"-            codes `shouldContain` [RegisterInitialOutOfScope, UndeclaredCommand, WriteTargetNotRegister]-        it "anchors UnreachableState on the state row" $ do-            let src =-                    T.unlines-                        [ "context repro"-                        , ""-                        , "aggregate Thing"-                        , "  regs"-                        , "  states"-                        , "    Initial"-                        , "    Unreachable"-                        ]-            case parseSpec "<unreachable-row>" src of-                Left err -> expectationFailure (T.unpack err)-                Right spec ->-                    [line d | d <- validateSpec spec, code d == UnreachableState]-                        `shouldBe` [7]--    describe "evolution parsing" $-        it "parses event version and upcaster from reservation-v2.keiro" $ do-            input <- readTestText "test/fixtures/reservation-v2.keiro"-            case parseSpec "test/fixtures/reservation-v2.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> case [e | NAggregate a <- specNodes spec, e <- aggEvents a, evName e == "TransferReservationCreated"] of-                    (e : _) -> do-                        evVersion e `shouldBe` 2-                        evUpcastFrom e `shouldBe` Just (1, Hole)-                    [] -> expectationFailure "TransferReservationCreated not found"--    describe "aggregate snapshots (EP-109)" $ do-        it "parses, validates, and round-trips a snapshot policy with codec fixture" $ do-            spec <- specOf "test/fixtures/reservation-snapshot.keiro"-            errorCodesOf "test/fixtures/reservation-snapshot.keiro" `shouldReturn` []-            parseSpec "<snapshot-round-trip>" (renderSpec spec) `shouldBe` Right spec-            case [aggregate | NAggregate aggregate <- specNodes spec] of-                [aggregate] -> aggSnapshot aggregate `shouldBe` Just (SnapshotSpec (SnapEvery 100) 1 "7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc" noLoc)-                aggregates -> expectationFailure ("expected one snapshot aggregate, got " <> show (length aggregates))-        it "rejects disabled intervals and invalid codec fixtures" $ do-            source <- readTestText "test/fixtures/reservation-snapshot.keiro"-            interval <- parseInlineSpec "<snapshot-zero>" (T.replace "snapshot every 100" "snapshot every 0" source)-            map code (validateSpec interval) `shouldContain` [SnapshotIntervalInvalid]-            version <- parseInlineSpec "<snapshot-version-zero>" (T.replace "state-codec version=1" "state-codec version=0" source)-            map code (validateSpec version) `shouldContain` [SnapshotCodecFixtureInvalid]-            emptyHash <- parseInlineSpec "<snapshot-empty-hash>" (T.replace "shape-hash=\"7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc\"" "shape-hash=\"\"" source)-            map code (validateSpec emptyHash) `shouldContain` [SnapshotCodecFixtureInvalid]-        it "conditionally lowers JSON instances and the live defaultStateCodec" $ do-            snapshot <- specOf "test/fixtures/reservation-snapshot.keiro"-            ordinary <- specOf "test/fixtures/reservation.keiro"-            case ([aggregate | NAggregate aggregate <- specNodes snapshot], [aggregate | NAggregate aggregate <- specNodes ordinary]) of-                ([snapshotAggregate], [ordinaryAggregate]) -> do-                    let snapshotModules = scaffoldAggregate (defaultContext (specContext snapshot)) snapshot snapshotAggregate-                        ordinaryModules = scaffoldAggregate (defaultContext (specContext ordinary)) ordinary ordinaryAggregate-                        snapshotDomain = generatedTextEndingIn "Domain.hs" snapshotModules-                        snapshotStream = generatedTextEndingIn "EventStream.hs" snapshotModules-                        ordinaryDomain = generatedTextEndingIn "Domain.hs" ordinaryModules-                        ordinaryStream = generatedTextEndingIn "EventStream.hs" ordinaryModules-                    snapshotDomain `shouldSatisfy` T.isInfixOf "deriving anyclass (ToJSON, FromJSON)"-                    snapshotStream `shouldSatisfy` T.isInfixOf "snapshotPolicy = Every 100"-                    snapshotStream `shouldSatisfy` T.isInfixOf "stateCodec = Just (defaultStateCodec 1)"-                    snapshotStream `shouldSatisfy` T.isInfixOf "reservationSnapshotFixture = (1, \"7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc\")"-                    ordinaryDomain `shouldNotSatisfy` T.isInfixOf "DeriveAnyClass"-                    ordinaryStream `shouldSatisfy` T.isInfixOf "snapshotPolicy = Never"-                    ordinaryStream `shouldSatisfy` T.isInfixOf "stateCodec = Nothing"-                    ordinaryStream `shouldSatisfy` T.isInfixOf "reservationCategory = Stream.categoryUnsafe \"reservation\""-                    firewallBreaches snapshotModules `shouldBe` []-                _ -> expectationFailure "expected one aggregate in each snapshot test spec"--    describe "process/timer (EP-3)" $ do-        it "parses the hospital-surge process + nested timer" $ do-            input <- readTestText "test/fixtures/hospital-surge.keiro"-            case parseSpec "test/fixtures/hospital-surge.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> case [p | NProcess p <- specNodes spec] of-                    (p : _) -> do-                        procId p `shouldBe` "HospitalSurge"-                        procName p `shouldBe` "hospital-surge"-                        procRejected p `shouldBe` PolHalt-                        procPoison p `shouldBe` PolHalt-                        sagaCategory (procSaga p) `shouldBe` "hospitalSurge"-                        tmName (procTimer p) `shouldBe` "surgeFollowUp"-                        onReject (fireDisposition (tmFire (procTimer p))) `shouldBe` OFired-                        onAmbiguous (fireDisposition (tmFire (procTimer p))) `shouldBe` ORetry-                        tmMaxAttempts (procTimer p) `shouldBe` 5-                    [] -> expectationFailure "no process node parsed"-        it "round-trips the hospital-surge spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/hospital-surge.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the hospital-surge spec (no errors; benign-inversion warnings only)" $ do-            codes <- errorCodesOf "test/fixtures/hospital-surge.keiro"-            codes `shouldBe` []-        it "rejects illegal saga categories and no longer parses the raw stream-prefix clause" $ do-            spec <- specOf "test/fixtures/hospital-surge.keiro"-            mapM_-                (\categoryName -> processErrorCodes (\process -> process{procSaga = (procSaga process){sagaCategory = categoryName}}) spec `shouldContain` [SagaCategoryIllegal])-                ["", "$all", "hospital-surge", "hospital surge", "wf:surge"]-            source <- readTestText "test/fixtures/hospital-surge.keiro"-            parseSpec "<legacy-saga>" (T.replace "saga Surge category \"hospitalSurge\"" "saga Surge stream=\"hospital-surge-\" <> correlationId" source)-                `shouldSatisfy` isLeft-        it "rejects a wall-clock fireAt as ProcessFireAtNotInjected" $ do-            codes <- errorCodesOf "test/fixtures/hospital-surge-clock.keiro"-            codes `shouldContain` [ProcessFireAtNotInjected]-        it "reports one ProcessFireAtNotInjected for a wholly unknown fireAt field" $ do-            codes <- errorCodesOf "test/fixtures/hospital-surge-clock.keiro"-            length (filter (== ProcessFireAtNotInjected) codes) `shouldBe` 1-        it "rejects a user-supplied dispatch id as ProcessDispatchIdSupplied" $ do-            codes <- errorCodesOf "test/fixtures/hospital-surge-dispatchid.keiro"-            codes `shouldContain` [ProcessDispatchIdSupplied]-        it "rejects an unresolved saga reference as ProcessUnresolvedRef" $ do-            codes <- errorCodesOf "test/fixtures/hospital-surge-badref.keiro"-            codes `shouldContain` [ProcessUnresolvedRef]-        it "rejects unresolved process commands, projections, schedules, and advance ids" $ do-            codes <- errorCodesOf "test/fixtures/process-ghost-refs.keiro"-            length (filter (== ProcessUnresolvedRef) codes) `shouldBe` 5-            codes `shouldContain` [ProcessDispatchIdSupplied]--    describe "router (EP-108)" $ do-        it "parses the incident-paging router shape" $ do-            input <- readTestText "test/fixtures/incident-paging/incident-paging.keiro"-            case parseSpec "test/fixtures/incident-paging/incident-paging.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> case [router | NRouter router <- specNodes spec] of-                    [router] -> do-                        rtId router `shouldBe` "PagingRouter"-                        rtName router `shouldBe` "jitsurei-paging"-                        corrField (rtKey router) `shouldBe` "incidentId"-                        rvSource (rtResolve router) `shouldBe` ResolveReadModel "service_oncall"-                        rvRow (rtResolve router) `shouldBe` ["responderId"]-                        rdCommand (rtDispatch router) `shouldBe` "SendPage"-                        rtRejected router `shouldBe` PolDeadLetter-                        rtPoison router `shouldBe` PolHalt-                    routers -> expectationFailure ("expected one router, got " <> show (length routers))-        it "round-trips the incident-paging spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/incident-paging/incident-paging.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the incident-paging router with warnings only" $ do-            codes <- errorCodesOf "test/fixtures/incident-paging/incident-paging.keiro"-            codes `shouldBe` []-            diagnostics <- diagnosticCodesOf "test/fixtures/incident-paging/incident-paging.keiro"-            diagnostics `shouldContain` [PolicyDeadLetterUnused, AmbiguousFollowsRejectedPolicy]-        it "rejects unresolved targets, keys, commands, and binding scopes" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            routerErrorCodes (\router -> router{rtTarget = "Pge"}) spec `shouldContain` [RouterUnresolvedRef]-            routerErrorCodes (\router -> router{rtKey = (rtKey router){corrField = "incidntId"}}) spec `shouldContain` [RouterKeyFieldUnknown]-            routerErrorCodes (\router -> router{rtDispatch = (rtDispatch router){rdCommand = "SendPag"}}) spec `shouldContain` [RouterCommandUnknown]-            routerErrorCodes-                ( \router ->-                    let dispatch = rtDispatch router-                     in router{rtDispatch = dispatch{rdFields = [FieldBinding "responderId" (Just "resolved.responder")]}}-                )-                spec-                `shouldContain` [RouterBindingUnscoped]-        it "rejects unresolved read models and contradictory rejection policies" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            let withoutReadModel = removeReadModel "service_oncall" spec-            errorCodes withoutReadModel `shouldContain` [RouterUnresolvedRef]-            routerErrorCodes-                ( \router ->-                    let dispatch = rtDispatch router-                        disposition = rdDisposition dispatch-                     in router-                            { rtRejected = PolHalt-                            , rtDispatch = dispatch{rdDisposition = disposition{onFailed = DDeadLetter "page rejected"}}-                            }-                )-                spec-                `shouldContain` [PolicyContradiction]-        it "rejects on-ambiguous Fired for process timers" $ do-            spec <- specOf "test/fixtures/hospital-surge.keiro"-            let changed =-                    spec-                        { specNodes =-                            [ case node of-                                NProcess process ->-                                    let timer = procTimer process-                                        fire = tmFire timer-                                        disposition = fireDisposition fire-                                     in NProcess process{procTimer = timer{tmFire = fire{fireDisposition = disposition{onAmbiguous = OFired}}}}-                                _ -> node-                            | node <- specNodes spec-                            ]-                        }-            errorCodes changed `shouldContain` [AmbiguousMarkedBenign]-        it "requires explicit policy and ambiguity clauses in the grammar" $ do-            source <- readTestText "test/fixtures/hospital-surge.keiro"-            parseSpec "<missing-poison>" (T.replace "  poison => halt\n" "" source) `shouldSatisfy` isLeft-            parseSpec "<missing-ambiguous>" (T.replace " ; on-ambiguous Retry" "" source) `shouldSatisfy` isLeft-        it "scaffolds firewall-clean router wiring, policies, and typed-hole guidance" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            case [router | NRouter router <- specNodes spec] of-                [router] -> do-                    let ctx = defaultContext (specContext spec)-                        modules = scaffoldRouter ctx router-                        generated = [m | m <- modules, kind m == Generated]-                        holes = [m | m <- modules, kind m == HoleStub]-                    firewallBreaches generated `shouldBe` []-                    case (generated, holes) of-                        ([generatedModule], [holeModule]) -> do-                            moduleText generatedModule `shouldSatisfy` T.isInfixOf "pagingRouterWorkerOptions"-                            moduleText generatedModule `shouldSatisfy` T.isInfixOf "rejectedCommandPolicy = RejectedDeadLetter"-                            moduleText holeModule `shouldSatisfy` T.isInfixOf "UNION of resolved target identities"-                            moduleText holeModule `shouldSatisfy` T.isInfixOf "confirmBenignDuplicate"-                        _ -> expectationFailure "expected one generated router module and one router hole module"-                routers -> expectationFailure ("expected one router, got " <> show (length routers))-        it "requires a caller callback for non-halting poison policies" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            case [router | NRouter router <- specNodes spec] of-                [router] -> do-                    let ctx = defaultContext (specContext spec)-                        generatedFor choice = [moduleText m | m <- scaffoldRouter ctx router{rtPoison = choice}, kind m == Generated]-                    mapM_-                        ( \(choice, constructor) -> case generatedFor choice of-                            [generatedModule] -> do-                                generatedModule `shouldSatisfy` T.isInfixOf "(Envelope msg -> Eff es ()) -> WorkerOptions es msg"-                                generatedModule `shouldSatisfy` T.isInfixOf (constructor <> " poisonCallback")-                            _ -> expectationFailure "expected one generated router module"-                        )-                        [(PolDeadLetter, "PoisonDeadLetter"), (PolSkip, "PoisonSkip")]-                    case [moduleText m | m <- scaffoldRouter ctx router{rtRejected = PolSkip}, kind m == Generated] of-                        [generatedModule] -> generatedModule `shouldSatisfy` T.isInfixOf "rejectedCommandPolicy = RejectedSkip"-                        _ -> expectationFailure "expected one generated router module"-                routers -> expectationFailure ("expected one router, got " <> show (length routers))-        it "emits router harness facts that pin policy and target-keyed identity" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            case [router | NRouter router <- specNodes spec] of-                [router] -> case harnessRouter (defaultContext (specContext spec)) router of-                    [facts] -> do-                        moduleText facts `shouldSatisfy` T.isInfixOf "(\"rejectedPolicy\", \"deadLetter\")"-                        moduleText facts `shouldSatisfy` T.isInfixOf "targetStreamName, occurrence"-                    modules -> expectationFailure ("expected one router harness, got " <> show (length modules))-                routers -> expectationFailure ("expected one router, got " <> show (length routers))-        it "rejects invalid timer ceilings and target field bindings" $ do-            codes <- errorCodesOf "test/fixtures/process-bad-timer.keiro"-            mapM_-                (\expected -> codes `shouldContain` [expected])-                [ProcessTimerCeilingInvalid, ProcessFieldBindingUnresolved]-        it "accepts resolved process projection references" $ do-            codes <- errorCodesOf "test/fixtures/surge-service.keiro"-            codes `shouldBe` []-        it "scaffolds the process: Generated wiring is firewall-clean + a HoleStub" $ do-            mods <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"-            let gens = [m | m <- mods, kind m == Generated]-                holes = [m | m <- mods, kind m == HoleStub]-            length holes `shouldBe` 1-            firewallBreaches gens `shouldBe` []-            case gens of-                [generatedModule] -> do-                    -- the worker uses the spec's ceiling, never the dangerous default-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "max-attempts = 5"-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeProcessWorkerOptions"-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeCategory = Stream.categoryUnsafe \"hospitalSurge\""-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "confirmBenignDuplicate"-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "StreamName -> EventId -> CommandError -> Eff es Bool"-                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "Left (CommandAmbiguous _)"-                    case holes of-                        [holeModule] -> moduleText holeModule `shouldSatisfy` T.isInfixOf "entityStream hospitalSurgeCategory"-                        _ -> expectationFailure "expected one process hole module"-                _ -> expectationFailure "expected one generated process module"-        it "process scaffold is deterministic" $ do-            a <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"-            b <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"-            map moduleText a `shouldBe` map moduleText b--    describe "contract (EP-4)" $ do-        it "parses the emergency contract (topics + events-on-topic + typed fields)" $ do-            input <- readTestText "test/fixtures/contract.keiro"-            case parseSpec "test/fixtures/contract.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> case [c | NContract c <- specNodes spec] of-                    (c : _) -> do-                        ctrName c `shouldBe` "emergency"-                        ctrDiscriminator c `shouldBe` "messageType"-                        map fst (ctrTopics c) `shouldBe` ["incidentEvents", "hospitalEvents"]-                        map ceName (ctrEvents c) `shouldBe` ["IncidentTransferNeedDeclared", "TransferReservationAccepted"]-                    [] -> expectationFailure "no contract node parsed"-        it "round-trips the contract spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/contract.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "round-trips the intake (inbox) spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/intake.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the intake spec (complete disposition, no inversions)" $ do-            codes <- errorCodesOf "test/fixtures/intake.keiro"-            codes `shouldBe` []-        it "lowers explicit dedupe-only persistence and defaults omission to full-envelope" $ do-            spec <- specOf "test/fixtures/intake.keiro"-            ordinary <- specOf "test/fixtures/intake-decode.keiro"-            case ([intake | NIntake intake <- specNodes spec], [intake | NIntake intake <- specNodes ordinary]) of-                ([intake], [defaultIntake]) -> do-                    inkPersist intake `shouldBe` InkPersistDedupeOnly-                    inkPersist defaultIntake `shouldBe` InkPersistFull-                    renderSpec spec `shouldSatisfy` T.isInfixOf "persist = dedupe-only"-                    renderSpec ordinary `shouldNotSatisfy` T.isInfixOf "persist ="-                    let inbox = generatedTextEndingIn "Inbox.hs" (scaffoldIntake (defaultContext (specContext spec)) intake)-                    inbox `shouldSatisfy` T.isInfixOf "inboxPersistence = PersistDedupeOnly"-                (intakes, defaultIntakes) ->-                    expectationFailure ("expected one intake in each fixture, got " <> show (length intakes, length defaultIntakes))-        it "rejects duplicate => retry (inversion 1)" $ do-            codes <- errorCodesOf "test/fixtures/intake-dup-retry.keiro"-            codes `shouldContain` [DispositionDuplicateRetry]-        it "rejects previouslyFailed => retry (inversion 2)" $ do-            codes <- errorCodesOf "test/fixtures/intake-pf-retry.keiro"-            codes `shouldContain` [DispositionPreviouslyFailedRetry]-        it "rejects an incomplete disposition table" $ do-            codes <- errorCodesOf "test/fixtures/intake-incomplete.keiro"-            codes `shouldContain` [DispositionIncomplete]-        it "rejects a shadowing duplicate intake disposition row" $ do-            codes <- errorCodesOf "test/fixtures/intake-dup-row.keiro"-            codes `shouldContain` [DispositionDuplicateOutcome]-        it "rejects intake events declared on another topic" $ do-            codes <- errorCodesOf "test/fixtures/intake-topic-mismatch.keiro"-            codes `shouldContain` [TopicAffinityMismatch]-        it "round-trips the emit/publisher spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/emit.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the emit/publisher spec (skip present, coupling resolves)" $ do-            codes <- errorCodesOf "test/fixtures/emit.keiro"-            codes `shouldBe` []-        it "rejects a missing _ => skip catch-all as EmitSkipMissing" $ do-            codes <- errorCodesOf "test/fixtures/emit-noskip.keiro"-            codes `shouldContain` [EmitSkipMissing]-        it "rejects mapping to an undeclared contract event as EmitUnresolvedContract" $ do-            codes <- errorCodesOf "test/fixtures/emit-badevent.keiro"-            codes `shouldContain` [EmitUnresolvedContract]-        it "rejects emit events declared on another topic" $ do-            codes <- errorCodesOf "test/fixtures/emit-topic-mismatch.keiro"-            codes `shouldContain` [TopicAffinityMismatch]--    describe "pgmq workqueue/dispatch (EP-5)" $ do-        it "round-trips the reservation-work spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/reservation-work.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the reservation-work spec (physical matches, no inversions)" $ do-            codes <- errorCodesOf "test/fixtures/reservation-work.keiro"-            codes `shouldBe` []-        it "rejects a divergent captured physical name as WqPhysicalDivergence" $ do-            codes <- errorCodesOf "test/fixtures/reservation-work-divergent.keiro"-            codes `shouldContain` [WqPhysicalDivergence]-        it "rejects storeFailure => deadLetter as WqStoreFailureNotRetry" $ do-            codes <- errorCodesOf "test/fixtures/reservation-work-sf-deadletter.keiro"-            codes `shouldContain` [WqStoreFailureNotRetry]-        it "rejects decodeFailure => retry as WqDecodeFailureNotDeadLetter" $ do-            codes <- errorCodesOf "test/fixtures/reservation-work-df-retry.keiro"-            codes `shouldContain` [WqDecodeFailureNotDeadLetter]-        it "requires complete, unique workqueue disposition rows" $ do-            incomplete <- errorCodesOf "test/fixtures/workqueue-incomplete.keiro"-            incomplete `shouldContain` [WqDispositionIncomplete]-            duplicateSpec <- specOf "test/fixtures/workqueue-dup-row.keiro"-            let duplicateDiagnostics = [d | d <- validateSpec duplicateSpec, code d == DispositionDuplicateOutcome]-            map line duplicateDiagnostics `shouldBe` [17]-        it "checks the captured queueRef dlq and table fixtures" $ do-            dlqCodes <- errorCodesOf "test/fixtures/workqueue-dlq-divergent.keiro"-            dlqCodes `shouldContain` [WqDlqDivergence]-            tableCodes <- errorCodesOf "test/fixtures/workqueue-table-divergent.keiro"-            tableCodes `shouldContain` [WqTableDivergence]-        it "matches queueRef for upper-case, punctuation, and hashed logical names" $ do-            upper <- errorCodesOf "test/fixtures/workqueue-uppercase-logical.keiro"-            upper `shouldBe` []-            hashed <- errorCodesOf "test/fixtures/workqueue-hashed-logical.keiro"-            hashed `shouldBe` []-            derivedQueueTrio "hospital_capacity.reservation_work.per_hospital_fifo_lane_assignments"-                `shouldBe` ( "hospital_capacity_reservat_757040df00976c33"-                           , "hospital_capacity_reservat_757040df00976c33_dlq"-                           , "pgmq.q_hospital_capacity_reservat_757040df00976c33"-                           )-        it "resolves dispatch dedup queues and payload wire fields" $ do-            ghost <- errorCodesOf "test/fixtures/dispatch-dedup-ghost-queue.keiro"-            ghost `shouldContain` [DispatchDedupQueueUnresolved]-            field <- errorCodesOf "test/fixtures/dispatch-dedup-bad-field.keiro"-            field `shouldContain` [DispatchDedupFieldUnresolved]-        it "requires a resolvable group key exactly when ordering is FIFO" $ do-            noKey <- errorCodesOf "test/fixtures/reservation-work-fifo-nokey.keiro"-            noKey `shouldContain` [WqGroupKeyMissing]-            unordered <- errorCodesOf "test/fixtures/reservation-work-key-unordered.keiro"-            unordered `shouldContain` [WqGroupKeyWithoutFifo]-            source <- readTestText "test/fixtures/reservation-work.keiro"-            unresolved <- parseInlineSpec "<unresolved-group-key>" (T.replace "group key from reservationId" "group key from missingId" source)-            map code (validateSpec unresolved) `shouldContain` [WqGroupKeyUnresolved]-        it "warns on unlogged storage and rejects empty partition settings" $ do-            warningCodes <- diagnosticCodesOf "test/fixtures/reservation-work-unlogged.keiro"-            warningCodes `shouldContain` [WqUnloggedDurability]-            partitionCodes <- errorCodesOf "test/fixtures/reservation-work-partitioned-empty.keiro"-            partitionCodes `shouldContain` [WqPartitionSpecEmpty]-        it "lowers ordering, provisioning, and raw group-key projection" $ do-            spec <- specOf "test/fixtures/reservation-work.keiro"-            case [workqueue | NWorkqueue workqueue <- specNodes spec] of-                workqueue : _ -> do-                    let modules = scaffoldWorkqueue (defaultContext (specContext spec)) workqueue-                        queue = generatedTextEndingIn "Queue.hs" modules-                        policy = generatedTextEndingIn "QueuePolicy.hs" modules-                    queue `shouldSatisfy` T.isInfixOf "groupKeyFor payload = payload.reservationId"-                    policy `shouldSatisfy` T.isInfixOf "jobOrdering = FifoThroughput"-                    policy `shouldSatisfy` T.isInfixOf "withFifoIndexProvision (standardProvision)"-                    firewallBreaches modules `shouldBe` []-                [] -> expectationFailure "reservation-work fixture has no workqueue"--    describe "readmodel (EP-107)" $ do-        it "parses and round-trips first-class read models" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            case [readModel | NReadModel readModel <- specNodes spec] of-                [subscriptionModel, inlineModel] -> do-                    rmName subscriptionModel `shouldBe` "transfer_decisions"-                    rmColumns subscriptionModel-                        `shouldBe` [ RmColumn "reservation_id" "text" True-                                   , RmColumn "hospital_id" "text" True-                                   , RmColumn "status" "text" True-                                   , RmColumn "decided_at" "timestamptz" False-                                   ]-                    rmScope subscriptionModel `shouldBe` Just (RmCategory "reservation")-                    rmFeed subscriptionModel `shouldBe` RmSubscription-                    rmSubscription subscriptionModel `shouldBe` Just "hospital-capacity-transfer-decisions-sub"-                    rmName inlineModel `shouldBe` "subscriptions"-                    rmScope inlineModel `shouldBe` Nothing-                    rmFeed inlineModel `shouldBe` RmInline-                nodes -> expectationFailure ("expected two readmodel nodes, got " <> show (length nodes))-            parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts an aggregate projection without a consistency clause" $ do-            spec <- parseInlineSpec "<projection-without-consistency>" projectionWithoutConsistencySpec-            case [projection | NAggregate aggregate <- specNodes spec, Just projection <- [aggProjection aggregate]] of-                [projection] -> projConsistency projection `shouldBe` Nothing-                projections -> expectationFailure ("expected one projection, got " <> show (length projections))-        it "pins the canonical UTF-8 shape digest and runtime identities" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            case [readModel | NReadModel readModel <- specNodes spec] of-                (subscriptionModel : inlineModel : _) -> do-                    canonicalShape subscriptionModel-                        `shouldBe` "transfer_decisions|reservation_id:text:req|hospital_id:text:req|status:text:req|decided_at:timestamptz:null"-                    deriveShapeHash subscriptionModel `shouldBe` "fnv1a:3717f6d9e3c44bd6"-                    deriveShapeHash inlineModel `shouldBe` "fnv1a:f54d9bb2f40a6738"-                    registryNameFor (specContext spec) subscriptionModel `shouldBe` "hospital-capacity-transfer-decisions"-                    subscriptionNameFor (specContext spec) subscriptionModel `shouldBe` "hospital-capacity-transfer-decisions-sub"-                    subscriptionNameFor "billing" inlineModel `shouldBe` "billing-subscriptions-sub"-                nodes -> expectationFailure ("expected readmodel nodes, got " <> show (length nodes))-        it "accepts the positive readmodel fixture with all references resolved" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            validateSpec spec `shouldBe` []-        it "rejects shape drift and unknown SQL column types" $ do-            codes <- errorCodesOf "test/fixtures/readmodel-shape-drift.keiro"-            codes `shouldContain` [RmShapeHashDrift, RmUnknownColumnType]-        it "rejects Strong on inline and standalone projections" $ do-            inlineCodes <- errorCodesOf "test/fixtures/readmodel-strong-inline.keiro"-            inlineCodes `shouldContain` [RmStrongInlineOnly]-            standalone <- specOf "test/fixtures/readmodel-strong-standalone.keiro"-            let diagnostics = validateSpec standalone-            map code diagnostics `shouldContain` [RmStrongInlineOnly, RmProjectionWithoutNode]-            [severity diagnostic | diagnostic <- diagnostics, code diagnostic == RmProjectionWithoutNode]-                `shouldBe` [Warning]-        it "rejects scope without Strong and an unreferenced inline feed" $ do-            scopeCodes <- errorCodesOf "test/fixtures/readmodel-scope-eventual.keiro"-            scopeCodes `shouldContain` [RmScopeWithoutStrong]-            inlineCodes <- errorCodesOf "test/fixtures/readmodel-inline-unreferenced.keiro"-            inlineCodes `shouldContain` [RmInlineFeedUnreferenced]-        it "rejects projection consistency conflicts" $ do-            codes <- errorCodesOf "test/fixtures/readmodel-consistency-conflict.keiro"-            codes `shouldContain` [RmConsistencyConflict]-        it "resolves query read models and validates query consistency" $ do-            codes <- errorCodesOf "test/fixtures/readmodel-query-unresolved.keiro"-            codes `shouldContain` [QueryUnresolvedReadModel, QueryConsistencyInvalid]-        it "resolves dispatch read models and declared dedup columns" $ do-            codes <- errorCodesOf "test/fixtures/readmodel-dispatch-unresolved.keiro"-            codes `shouldContain` [DispatchReadModelUnresolved, DispatchReadModelFieldUnknown]-        it "scaffolds runtime records, rebuild helpers, async wiring, and typed holes" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            let ctx = defaultContext (specContext spec)-                readModels = [readModel | NReadModel readModel <- specNodes spec]-                modules = concatMap (scaffoldReadModel ctx) readModels-                transfer = generatedTextEndingIn "Transfer_decisions/ReadModel.hs" modules-                inline = generatedTextEndingIn "Subscriptions/ReadModel.hs" modules-                transferHoles = [moduleText m | m <- modules, "Transfer_decisions/ReadModelHoles.hs" `T.isSuffixOf` T.pack (modulePath m)]-            length modules `shouldBe` 6-            length [m | m <- modules, kind m == Generated] `shouldBe` 4-            length [m | m <- modules, kind m == HoleStub] `shouldBe` 2-            firewallBreaches modules `shouldBe` []-            transfer `shouldSatisfy` T.isInfixOf "registerTransferDecisions"-            transfer `shouldSatisfy` T.isInfixOf "Rebuild.startRebuild transferDecisionsReadModel [\"hospital-capacity-transfer-decisions-async\"]"-            transfer `shouldSatisfy` T.isInfixOf "strongScope = CategoryHead \"reservation\""-            transfer `shouldSatisfy` T.isInfixOf "transferDecisionsAsyncProjection"-            inline `shouldSatisfy` T.isInfixOf "Rebuild.startRebuild subscriptionsReadModel []"-            inline `shouldNotSatisfy` T.isInfixOf "AsyncProjection"-            transferHoles `shouldSatisfy` any (T.isInfixOf "RecordedEvent -> Tx.Transaction ()")-        it "threads qualified table and column guidance into aggregate projection holes" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            case [aggregate | NAggregate aggregate <- specNodes spec] of-                [aggregate] -> do-                    let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate-                        holes = [moduleText m | m <- modules, kind m == HoleStub]-                        projection = generatedTextEndingIn "Projection.hs" modules-                    holes `shouldSatisfy` any (T.isInfixOf "subscriptionsQualifiedTable")-                    holes `shouldSatisfy` any (T.isInfixOf "Table: \"billing\".\"subscriptions\"")-                    projection `shouldSatisfy` T.isInfixOf "ReadModelTable.subscriptionsQualifiedTable"-                aggregates -> expectationFailure ("expected one aggregate, got " <> show (length aggregates))-        it "emits runtime-free derivation facts for each read model" $ do-            spec <- specOf "test/fixtures/readmodel.keiro"-            case [readModel | NReadModel readModel <- specNodes spec] of-                (subscriptionModel : _) -> do-                    let modules = harnessReadModel (defaultContext (specContext spec)) subscriptionModel-                        harnessText = generatedTextEndingIn "ReadModelHarness.hs" modules-                    length modules `shouldBe` 1-                    firewallBreaches modules `shouldBe` []-                    harnessText `shouldSatisfy` T.isInfixOf "(\"shapeHash\", \"fnv1a:3717f6d9e3c44bd6\", \"fnv1a:3717f6d9e3c44bd6\")"-                    harnessText `shouldSatisfy` T.isInfixOf "(\"strongScope\", \"CategoryHead reservation\", \"CategoryHead reservation\")"-                    harnessText `shouldSatisfy` T.isInfixOf "runReadModelFacts"-                nodes -> expectationFailure ("expected readmodel nodes, got " <> show (length nodes))--    describe "workflow/operation (EP-6)" $ do-        it "round-trips the workflow spec through parse . pretty" $ do-            input <- readTestText "test/fixtures/workflow.keiro"-            case parseSpec "in" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec-        it "accepts the workflow spec (await<->signal matches, run resolves)" $ do-            codes <- errorCodesOf "test/fixtures/workflow.keiro"-            codes `shouldBe` []-        it "rejects a signal label with no matching await as AwaitSignalMismatch" $ do-            codes <- errorCodesOf "test/fixtures/workflow-signal-mismatch.keiro"-            codes `shouldContain` [AwaitSignalMismatch]-        it "rejects duplicate workflow labels" $ do-            codes <- errorCodesOf "test/fixtures/workflow-dup-label.keiro"-            codes `shouldContain` [WorkflowDuplicateLabel]-        it "rejects unresolved workflow id and sleep fields" $ do-            codes <- errorCodesOf "test/fixtures/workflow-unresolved-fields.keiro"-            codes `shouldContain` [WorkflowIdFieldUnresolved, WorkflowSleepDelayUnresolved]-        it "validates rule domains, totality, case constructors, and bodies" $ do-            unresolved <- errorCodesOf "test/fixtures/rule-bad-domain.keiro"-            unresolved `shouldBe` [RuleDomainUnresolved]-            codes <- errorCodesOf "test/fixtures/rule-not-total.keiro"-            mapM_-                (\expected -> codes `shouldContain` [expected])-                [RuleNotTotal, RuleCaseUnknownCtor, ClockSampled, GuardAtomOutOfScope]-        it "rejects unresolved command operation references" $ do-            codes <- errorCodesOf "test/fixtures/operation-ghost-aggregate.keiro"-            codes `shouldContain` [OperationUnresolvedRef]-        it "rejects a signal value type that differs from its await" $ do-            codes <- errorCodesOf "test/fixtures/operation-signal-value.keiro"-            codes `shouldContain` [AwaitSignalValueMismatch]-        it "round-trips guarded patches and terminal continueAsNew" $ do-            input <- readTestText "test/fixtures/workflow-evolution.keiro"-            case parseSpec "workflow-evolution" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> do-                    parseSpec "workflow-evolution" (renderSpec spec) `shouldBe` Right spec-                    errorCodes spec `shouldBe` []-        it "rejects duplicate patch ids anywhere in the workflow body" $ do-            codes <- errorCodesOf "test/fixtures/workflow-patch-dup.keiro"-            codes `shouldBe` [WorkflowPatchDuplicate]-        it "rejects non-terminal and nested continueAsNew" $ do-            codes <- errorCodesOf "test/fixtures/workflow-can-mid.keiro"-            codes `shouldBe` [WorkflowContinueAsNewNotTerminal, WorkflowContinueAsNewNotTerminal]-        it "rejects a colon in a patch id with a workflow diagnostic" $ do-            codes <- errorCodesOf "test/fixtures/workflow-patch-colon.keiro"-            codes `shouldBe` [WorkflowPatchIdInvalid]-        it "lowers patch facts and live runtime declarations" $ do-            spec <- specOf "test/fixtures/workflow-evolution.keiro"-            case [workflow | NWorkflow workflow <- specNodes spec] of-                [workflow] -> do-                    let modules = harnessWorkflow (defaultContext (specContext spec)) workflow-                        facts = generatedTextEndingIn "WorkflowFacts.hs" modules-                        runtime = generatedTextEndingIn "WorkflowRuntime.hs" modules-                    facts `shouldSatisfy` T.isInfixOf "patch:fraud-check-v2(step:fraud-check)"-                    facts `shouldSatisfy` T.isInfixOf "continueAsNew:RolloverSeed"-                    facts `shouldSatisfy` T.isInfixOf "(\"patches\", \"fraud-check-v2\")"-                    runtime `shouldSatisfy` T.isInfixOf "declaredPatches = Set.fromList [PatchId \"fraud-check-v2\"]"-                    runtime `shouldSatisfy` T.isInfixOf "opts{activePatches = declaredPatches}"-                workflows -> expectationFailure ("expected one workflow, got " <> show (length workflows))--    describe "diff (evolution classification)" $ do-        it "covers every node family exactly once and explains exclusions" $ do-            sort (map fst familyRegistry) `shouldBe` ([minBound .. maxBound] :: [NodeFamily])-            [reason | (_, OutOfDiffScope reason) <- familyRegistry, T.null reason] `shouldBe` []-        it "classifies a field added without a version bump as BREAKING" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldadd.keiro"-            any isBreaking cs `shouldBe` True-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtFieldAddedWithoutBump]-        it "classifies the same field wrapped as v2 + upcaster as ADDITIVE" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-v2.keiro"-            any isBreaking cs `shouldBe` False-            [ck | Additive ck <- cs] `shouldSatisfy` any ((== "TransferReservationCreated") . ckSubject)-        it "reports no breaking change when the spec is unchanged" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation.keiro"-            any isBreaking cs `shouldBe` False-        it "classifies a direct event field type change as EvtFieldTypeChanged" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldtype.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtFieldTypeChanged]-        it "resolves fields(Command) before comparing event field types" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-cmdfieldtype.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtFieldTypeChanged]-        it "uses EvtFieldRemovedSameVersion for an unchanged-version removal" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldremove.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtFieldRemovedSameVersion]-        it "uses EvtVersionDecreased for a version decrease" $ do-            cs <- diffFixtures "test/fixtures/reservation-v2.keiro" "test/fixtures/reservation.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtVersionDecreased]-        it "rejects a v1 to v3 jump whose only upcaster starts at v2" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-v3-dangling.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EvtVersionMissingUpcaster]-        it "classifies an enum constructor removal as EnumCtorRemoved" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumdrop.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EnumCtorRemoved]-        it "classifies an enum wire-spelling change as EnumWireSpellingChanged" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumwire.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just EnumWireSpellingChanged]-        it "classifies an enum constructor addition as additive" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumadd.keiro"-            any isBreaking cs `shouldBe` False-            [ckSubject k | Additive k <- cs] `shouldContain` ["BlackTag"]-        it "classifies an effective wire convention change as WireSpecChanged" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-wire.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WireSpecChanged]-        it "keeps deprecation additive and reports un-deprecation as EventUndeprecated" $ do-            deprecated <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-deprecated.keiro"-            any isBreaking deprecated `shouldBe` False-            restored <- diffFixtures "test/fixtures/reservation-deprecated.keiro" "test/fixtures/reservation.keiro"-            any isAdvisory restored `shouldBe` True-            [ckCode k | Advisory k <- restored] `shouldContain` [Just EventUndeprecated]-        it "classifies a removed contract event as ContractEventRemoved" $ do-            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-eventdrop.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just ContractEventRemoved]-        it "classifies contract field type changes and unversioned additions as ContractFieldChanged" $ do-            changed <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-fieldtype.keiro"-            [ckCode k | Breaking k <- changed] `shouldContain` [Just ContractFieldChanged]-            added <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-fieldadd.keiro"-            [ckCode k | Breaking k <- added] `shouldContain` [Just ContractFieldChanged]-        it "reports a field addition with a contract version bump as an advisory" $ do-            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-bump-fieldadd.keiro"-            any isBreaking cs `shouldBe` False-            [ckCode k | Advisory k <- cs] `shouldContain` [Just ContractSchemaVersionBumped]-        it "classifies a contract schema version decrease separately" $ do-            cs <- diffFixtures "test/fixtures/contract-bump-fieldadd.keiro" "test/fixtures/contract.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just ContractSchemaVersionDecreased]-        it "classifies contract topic and discriminator changes separately" $ do-            topic <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-topic.keiro"-            [ckCode k | Breaking k <- topic] `shouldContain` [Just ContractTopicChanged]-            discriminator <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-discriminator.keiro"-            [ckCode k | Breaking k <- discriminator] `shouldContain` [Just ContractDiscriminatorChanged]-        it "classifies a new contract event as additive" $ do-            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-eventadd.keiro"-            any isBreaking cs `shouldBe` False-            [ckSubject k | Additive k <- cs] `shouldContain` ["IncidentTransferNeedCancelled"]-        it "classifies workqueue wire names, types, and required additions as WqPayloadFieldChanged" $ do-            wire <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-wirename.keiro"-            [ckCode k | Breaking k <- wire] `shouldContain` [Just WqPayloadFieldChanged]-            fieldTypeChange <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-fieldtype.keiro"-            [ckCode k | Breaking k <- fieldTypeChange] `shouldContain` [Just WqPayloadFieldChanged]-            required <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-reqfield.keiro"-            [ckCode k | Breaking k <- required] `shouldContain` [Just WqPayloadFieldChanged]-        it "classifies a new optional workqueue payload field as additive" $ do-            cs <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-optfield.keiro"-            any isBreaking cs `shouldBe` False-            [ckSubject k | Additive k <- cs] `shouldContain` ["note"]-        it "classifies workqueue ordering changes as breaking delivery-contract changes" $ do-            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-ordering-change.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WqOrderingChanged]-            [ckDetail k | Breaking k <- cs, ckCode k == Just WqOrderingChanged]-                `shouldSatisfy` any (T.isInfixOf "delivery-order contract")-        it "classifies workqueue provision changes as operational migrations" $ do-            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-provision-change.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WqProvisionChanged]-            [ckDetail k | Breaking k <- cs, ckCode k == Just WqProvisionChanged]-                `shouldSatisfy` any (T.isInfixOf "migrate the existing queue operationally")-        it "classifies workqueue group-key changes as breaking repartitioning" $ do-            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-group-key-change.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WqGroupKeyChanged]-            [ckDetail k | Breaking k <- cs, ckCode k == Just WqGroupKeyChanged]-                `shouldSatisfy` any (T.isInfixOf "re-partitioned")-        it "classifies a process input type change as ProcessInputChanged" $ do-            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-inputtype.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just ProcessInputChanged]-        it "classifies workflow input and output changes as WorkflowShapeChanged" $ do-            input <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-inputfield.keiro"-            [ckCode k | Breaking k <- input] `shouldContain` [Just WorkflowShapeChanged]-            output <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-output.keiro"-            [ckCode k | Breaking k <- output] `shouldContain` [Just WorkflowShapeChanged]-        it "classifies workflow relabeling and appends as WorkflowBodyChanged" $ do-            relabeled <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-body.keiro"-            [ckCode k | Breaking k <- relabeled] `shouldContain` [Just WorkflowBodyChanged]-            appended <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-stepadd.keiro"-            [ckCode k | Breaking k <- appended] `shouldContain` [Just WorkflowBodyChanged]-            [ckDetail k | Breaking k <- appended, ckCode k == Just WorkflowBodyChanged]-                `shouldSatisfy` any (T.isInfixOf "new patch guard")-        it "classifies a body addition wholly guarded by a new patch as additive" $ do-            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-evolution-diff.keiro"-            any isBreaking cs `shouldBe` False-            [ckSubject k | Additive k <- cs, ckFacet k == "workflow-patch"] `shouldContain` ["fraud-check-v2"]-            [ckSubject k | Additive k <- cs, ckFacet k == "workflow-continue-as-new"] `shouldContain` ["RolloverSeed"]-        it "classifies removing an existing patch as breaking" $ do-            cs <- diffFixtures "test/fixtures/workflow-evolution-diff.keiro" "test/fixtures/workflow-continue.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WorkflowPatchRemoved]-            [ckDetail k | Breaking k <- cs, ckCode k == Just WorkflowPatchRemoved]-                `shouldSatisfy` any (T.isInfixOf "cannot prove")-        it "classifies terminal continueAsNew append as additive and seed drift as breaking" $ do-            appended <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-continue.keiro"-            any isBreaking appended `shouldBe` False-            [ckFacet k | Additive k <- appended] `shouldContain` ["workflow-continue-as-new"]-            changed <- diffFixtures "test/fixtures/workflow-continue.keiro" "test/fixtures/workflow-continue-seed-v2.keiro"-            [ckCode k | Breaking k <- changed] `shouldContain` [Just WorkflowContinueSeedChanged]-            [ckDetail k | Breaking k <- changed, ckCode k == Just WorkflowContinueSeedChanged]-                `shouldSatisfy` any (T.isInfixOf "restoreSeed")-        it "classifies a workflow stable-name change as WorkflowStableNameChanged" $ do-            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-rename.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just WorkflowStableNameChanged]-        it "classifies workflow id-derivation changes as DerivedIdentityChanged" $ do-            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-idfield.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just DerivedIdentityChanged]-        it "classifies an id prefix change as IdPrefixChanged" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-idprefix.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just IdPrefixChanged]-        it "classifies intake dedupe key and policy changes as DedupeIdentityChanged" $ do-            policy <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-dedupepolicy.keiro"-            [ckCode k | Breaking k <- policy] `shouldContain` [Just DedupeIdentityChanged]-            key <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-dedupekey.keiro"-            [ckCode k | Breaking k <- key] `shouldContain` [Just DedupeIdentityChanged]-        it "reports intake decode-posture changes as warnings" $ do-            cs <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-decode.keiro"-            any isBreaking cs `shouldBe` False-            [ckCode k | Advisory k <- cs] `shouldContain` [Just DecodePostureChanged]-            [ckCode k | Advisory k <- cs] `shouldContain` [Just IntakePersistenceChanged]-        it "classifies process and timer derivation changes as DerivedIdentityChanged" $ do-            processName <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-procname.keiro"-            [ckCode k | Breaking k <- processName] `shouldContain` [Just DerivedIdentityChanged]-            timerId <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-timerid.keiro"-            [ckCode k | Breaking k <- timerId] `shouldContain` [Just DerivedIdentityChanged]-            base <- specOf "test/fixtures/hospital-surge.keiro"-            let categoryChange = diffSpecs base (modifyProcess "HospitalSurge" (\process -> process{procSaga = (procSaga process){sagaCategory = "hospitalSurgeV2"}}) base)-            [ckCode k | Breaking k <- categoryChange] `shouldContain` [Just DerivedIdentityChanged]-        it "classifies router stable names, keys, and targets as identity-bearing" $ do-            base <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            let stableName = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtName = "paging-v2"}) base)-                keyDerivation = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtKey = (rtKey router){corrVia = "otherIdText"}}) base)-                target = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtTarget = "OtherPage"}) base)-            [ckCode k | Breaking k <- stableName] `shouldContain` [Just RouterStableNameChanged]-            [ckCode k | Breaking k <- keyDerivation] `shouldContain` [Just DerivedIdentityChanged]-            [ckCode k | Breaking k <- target] `shouldContain` [Just DerivedIdentityChanged]-        it "reports a timer window change as a warning" $ do-            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-window.keiro"-            any isBreaking cs `shouldBe` False-            [ckCode k | Advisory k <- cs] `shouldContain` [Just TimerWindowChanged]-        it "reports emit-map changes as warnings and derive changes as breaking" $ do-            mapping <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-mapchange.keiro"-            any isBreaking mapping `shouldBe` False-            [ckCode k | Advisory k <- mapping] `shouldContain` [Just EmitMappingChanged]-            derive <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-derive.keiro"-            [ckCode k | Breaking k <- derive] `shouldContain` [Just DerivedIdentityChanged]-        it "classifies publisher outbox identity and ordering independently" $ do-            outbox <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-outboxfield.keiro"-            [ckCode k | Breaking k <- outbox] `shouldContain` [Just DerivedIdentityChanged]-            ordering <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-ordering.keiro"-            any isBreaking ordering `shouldBe` False-            [ckCode k | Advisory k <- ordering] `shouldContain` [Just PublisherPolicyChanged]-        it "classifies workqueue names as QueueIdentityChanged" $ do-            cs <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-rename.keiro"-            [ckCode k | Breaking k <- cs] `shouldContain` [Just QueueIdentityChanged]-        it "classifies pgmq dispatch dedupe and retargeting independently" $ do-            dedupe <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-dedupkey.keiro"-            [ckCode k | Breaking k <- dedupe] `shouldContain` [Just DedupeIdentityChanged]-            retarget <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-retarget.keiro"-            any isBreaking retarget `shouldBe` False-            [ckCode k | Advisory k <- retarget] `shouldContain` [Just DispatchRetargeted]-        it "reports aggregate projection changes as warnings" $ do-            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-projection.keiro"-            any isBreaking cs `shouldBe` False-            [ckCode k | Advisory k <- cs] `shouldContain` [Just ProjectionChanged]-        it "classifies read-model version and unversioned shape changes" $ do-            base <- specOf "test/fixtures/readmodel-runtime.keiro"-            let versionTwo = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmVersion = 2}) base-                changedShape = modifyReadModel "transfer_decisions" changeReadModelShape base-                bumpedShape = modifyReadModel "transfer_decisions" (\readModel -> (changeReadModelShape readModel){rmVersion = 2}) base-                decreased = diffSpecs versionTwo base-                unversioned = diffSpecs base changedShape-                bumped = diffSpecs base bumpedShape-            [ckCode k | Breaking k <- decreased] `shouldContain` [Just ReadModelVersionDecreased]-            [ckCode k | Breaking k <- unversioned] `shouldContain` [Just ReadModelShapeChangedWithoutBump]-            any isBreaking bumped `shouldBe` False-            [ckFacet k | Additive k <- bumped] `shouldContain` ["read-model-version"]-        it "classifies read-model registry, table, subscription, and removal identities" $ do-            base <- specOf "test/fixtures/readmodel-runtime.keiro"-            let tableChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmTable = "transfer_decisions_v2"}) base-                subscriptionChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmSubscription = Just "transfer-decisions-v2"}) base-                renamed = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmName = "reservation_decisions"}) base-                removed = removeReadModel "transfer_decisions" base-            mapM_-                (\changes -> [ckCode k | Breaking k <- changes] `shouldContain` [Just DerivedIdentityChanged])-                [diffSpecs base tableChanged, diffSpecs base subscriptionChanged, diffSpecs base renamed, diffSpecs base removed]-        it "classifies read-model feed flips and consistency/scope weakening as breaking" $ do-            base <- specOf "test/fixtures/readmodel-runtime.keiro"-            let feedChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmFeed = RmInline}) base-                consistencyWeakened = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmConsistency = Eventual}) base-                entireLog = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmScope = Just RmEntireLog}) base-            [ckCode k | Breaking k <- diffSpecs base feedChanged] `shouldContain` [Just ReadModelFeedChanged]-            [ckCode k | Breaking k <- diffSpecs base consistencyWeakened] `shouldContain` [Just ReadModelConsistencyWeakened]-            [ckCode k | Breaking k <- diffSpecs entireLog base] `shouldContain` [Just ReadModelConsistencyWeakened]-        it "classifies Eventual to Strong read-model consistency as additive" $ do-            strong <- specOf "test/fixtures/readmodel-runtime.keiro"-            let eventual = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmConsistency = Eventual}) strong-                changes = diffSpecs eventual strong-            any isBreaking changes `shouldBe` False-            [ckFacet k | Additive k <- changes] `shouldContain` ["read-model-consistency"]--    describe "module placement (M1)" $ do-        it "GeneratedPrefix is today's namespace (Generated.<Ctx>.<Node>, holes at <Ctx>.<Node>)" $ do-            let ctx = defaultContext "hospital-capacity"-            genPrefixFor ctx "Reservation" `shouldBe` "Generated.HospitalCapacity.Reservation"-            holePrefixFor ctx "Reservation" `shouldBe` "HospitalCapacity.Reservation"-        it "module-root prefixes both layers" $ do-            let ctx = (defaultContext "hospital-capacity"){moduleRoot = "Acme"}-            genPrefixFor ctx "Reservation" `shouldBe` "Acme.Generated.HospitalCapacity.Reservation"-            holePrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation"-        it "CollocatedLeaf places the generated layer under the domain leaf" $ do-            let ctx = (defaultContext "hospital-capacity"){moduleRoot = "Acme", placement = CollocatedLeaf}-            genPrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation.Generated"-            holePrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation"-        it "parses and preserves the module/layout clauses through parse . pretty" $ do-            let src = "context hospital-capacity\nmodule Acme.Services\nlayout collocated\n\naggregate Reservation\n  regs\n  states Open\n"-            case parseSpec "<m1>" src of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> do-                    specModuleRoot spec `shouldBe` Just "Acme.Services"-                    specLayout spec `shouldBe` Just CollocatedLeaf-                    parseSpec "<m1>" (renderSpec spec) `shouldBe` Right spec-        it "a spec without the clauses leaves placement at the default" $ do-            input <- readTestText "test/fixtures/reservation.keiro"-            case parseSpec "test/fixtures/reservation.keiro" input of-                Left err -> expectationFailure (T.unpack err)-                Right spec -> do-                    specModuleRoot spec `shouldBe` Nothing-                    specLayout spec `shouldBe` Nothing--    describe "manifest (M2)" $ do-        it "lists exactly the modules the scaffolder produced" $ do-            mods <- scaffoldFixture "test/fixtures/reservation.keiro"-            spec <- specOf "test/fixtures/reservation.keiro"-            let manifest = renderManifest "reservation.keiro" mods spec-                expectedNames = sort (map (moduleNameOf . modulePath) mods)-            -- every produced module name appears in the manifest…-            mapM_ (\m -> (m `T.isInfixOf` manifest) `shouldBe` True) expectedNames-            -- …and the module list is exactly the scaffolder's output set.-            expectedNames-                `shouldBe` sort-                    [ "Generated.HospitalCapacity.Reservation.Codec"-                    , "Generated.HospitalCapacity.Reservation.Domain"-                    , "Generated.HospitalCapacity.Reservation.EventStream"-                    , "Generated.HospitalCapacity.Reservation.Harness"-                    , "Generated.HospitalCapacity.Reservation.Projection"-                    , "HospitalCapacity.Reservation.Holes"-                    ]-        it "derives the dependency set from the node kinds present (aggregate)" $ do-            spec <- specOf "test/fixtures/reservation.keiro"-            manifestDependencies spec `shouldBe` ["aeson", "base", "keiki", "keiro", "text"]-        it "derives the process dependency set, including worker-policy runtime imports" $ do-            spec <- specOf "test/fixtures/hospital-surge.keiro"-            let dependencies = manifestDependencies spec-            mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["time", "uuid", "shibuya-core", "keiki", "keiro"]-        it "uses the registered shibuya-core package name for router scaffolds" $ do-            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"-            let dependencies = manifestDependencies spec-            mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["effectful-core", "keiro", "shibuya-core"]-            dependencies `shouldNotContain` ["shibuya"]--    describe "new <kind> skeletons (M5)" $ do-        it "every skeleton parses and validates with zero error diagnostics" $-            mapM_ assertSkeletonValid skeletonKinds-        it "every skeleton passes the scaffold refusal gates" $-            mapM_ assertSkeletonScaffoldable skeletonKinds-        it "fresh skeleton scaffolds match the committed compiling modules" $-            mapM_ (uncurry assertSkeletonMatchesCommitted) skeletonModuleRoots-        it "rejects an unknown kind with a helpful message" $-            case skeletonFor "bogus" of-                Left msg -> ("Valid kinds:" `T.isInfixOf` msg) `shouldBe` True-                Right _ -> expectationFailure "expected an error for an unknown kind"--    describe "firewall self-check (M3)" $ do-        it "flags a forbidden operator in a Generated module" $ do-            let m = ScaffoldModule{modulePath = "Gen/Foo.hs", moduleText = "x = a ./= b", kind = Generated, origin = "test"}-            firewallBreaches [m] `shouldBe` [("Gen/Foo.hs", "./=", 1)]-        it "ignores forbidden operators in a HoleStub module (holes own them)" $ do-            let m = ScaffoldModule{modulePath = "Foo/Holes.hs", moduleText = "x = lit 1 .== y", kind = HoleStub, origin = "test"}-            firewallBreaches [m] `shouldBe` []-        it "matches `lit` as a word, not a substring of quality/split" $ do-            let clean = ScaffoldModule{modulePath = "Gen/Q.hs", moduleText = "quality = split facility", kind = Generated, origin = "test"}-                dirty = ScaffoldModule{modulePath = "Gen/L.hs", moduleText = "v = lit foo", kind = Generated, origin = "test"}-            firewallBreaches [clean] `shouldBe` []-            firewallBreaches [dirty] `shouldBe` [("Gen/L.hs", "lit", 1)]-        it "skips strings and comments and maximal-munches symbolic tokens" $ do-            let clean = syntheticGenerated "Gen/Clean.hs" "wire = \"lit .== B.slot\"\n-- x =: y\nx = a .<= b"-                dirty = syntheticGenerated "Gen/Dirty.hs" "x = a .< b\ny = c =: d"-            firewallBreaches [clean] `shouldBe` [("Gen/Clean.hs", ".<=", 3)]-            firewallBreaches [dirty] `shouldBe` [("Gen/Dirty.hs", ".<", 1), ("Gen/Dirty.hs", "=:", 2)]-        it "guards keiki imports while allowing the generated Core allowlist" $ do-            let forbidden = syntheticGenerated "Gen/Builder.hs" "import Keiki.Builder"-                restricted = syntheticGenerated "Gen/CoreBad.hs" "import Keiki.Core (lit)"-                allowed = syntheticGenerated "Gen/CoreGood.hs" "import Keiki.Core (RegFile (..), HsPred, step)"-            firewallBreaches [forbidden] `shouldBe` [("Gen/Builder.hs", "import:Keiki.Builder", 1)]-            firewallBreaches [restricted] `shouldBe` [("Gen/CoreBad.hs", "import:Keiki.Core", 1)]-            firewallBreaches [allowed] `shouldBe` []-        it "finds no breach in real scaffolder output (aggregate + process fixtures)" $ do-            aggMods <- scaffoldFixture "test/fixtures/reservation.keiro"-            procMods <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"-            firewallBreaches (aggMods <> procMods) `shouldBe` []--    describe "scaffold gates" $ do-        it "refuses duplicate and case-folded module paths with both origins" $ do-            spec <- specOf "test/fixtures/reservation.keiro"-            case [aggregate | NAggregate aggregate <- specNodes spec] of-                aggregate : _ -> do-                    let duplicate = spec{specNodes = [NAggregate aggregate, NAggregate aggregate]}-                        caseVariant = spec{specNodes = [NAggregate aggregate, NAggregate aggregate{aggName = T.toUpper (aggName aggregate)}]}-                    planScaffold (defaultContext (specContext spec)) duplicate `shouldSatisfy` hasPathCollisionWithTwoOrigins-                    planScaffold (defaultContext (specContext spec)) caseVariant `shouldSatisfy` hasPathCollisionWithTwoOrigins-                [] -> expectationFailure "reservation fixture has no aggregate"-        it "refuses a bannerless Generated target without changing its bytes" $-            withTempDirectory "keiro-dsl-banner" $ \out -> do-                spec <- specOf "test/fixtures/reservation.keiro"-                let ctx = defaultContext (specContext spec)-                case planScaffold ctx spec of-                    Left refusals -> expectationFailure ("unexpected planning refusal: " <> show refusals)-                    Right modules -> case [m | m <- modules, kind m == Generated] of-                        generated : _ -> do-                            let target = out </> modulePath generated-                            createDirectoryIfMissing True (takeDirectory target)-                            TIO.writeFile target "hand owned\n"-                            result <- executeScaffold out False "test/fixtures/reservation.keiro" ctx spec modules-                            result `shouldSatisfy` isMissingBannerRefusal-                            TIO.readFile target `shouldReturn` "hand owned\n"-                            forced <- executeScaffold out True "test/fixtures/reservation.keiro" ctx spec modules-                            forced `shouldSatisfy` isSuccessfulScaffold-                            TIO.readFile target `shouldReturn` moduleText generated-                        [] -> expectationFailure "reservation scaffold has no Generated module"-        it "reports renamed-node modules as stale without deleting them" $-            withTempDirectory "keiro-dsl-stale-rename" $ \out -> do-                spec <- parseInlineSpec "<stale-rename>" loweringAggregateSpec-                first <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext spec)) spec-                let renamed = spec{specNodes = map renameCounter (specNodes spec)}-                second <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext renamed)) renamed-                let oldDomain = onlyPathEndingIn "Counter/Domain.hs" (map fst (reportDispositions first))-                    oldHoles = onlyPathEndingIn "Counter/Holes.hs" (map fst (reportDispositions first))-                reportStale second `shouldSatisfy` \stale -> StaleModule Generated oldDomain `elem` stale && StaleModule HoleStub oldHoles `elem` stale-                doesFileExist (out </> oldDomain) `shouldReturn` True-                doesFileExist (out </> oldHoles) `shouldReturn` True-        it "reports the entire old tree across a module-root flip" $-            withTempDirectory "keiro-dsl-stale-root" $ \out -> do-                spec <- parseInlineSpec "<stale-root>" loweringAggregateSpec-                let initialCtx = defaultContext (specContext spec)-                    rootedCtx = initialCtx{moduleRoot = "Acme"}-                first <- executePlannedScaffold out "counter.keiro" initialCtx spec-                second <- executePlannedScaffold out "moved-counter.keiro" rootedCtx spec-                reportStale second-                    `shouldMatchList` [StaleModule (kind m) (modulePath m) | (m, _) <- reportDispositions first]-                forM_ (reportStale second) $ \stale -> doesFileExist (out </> stalePath stale) `shouldReturn` True-                renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "previous scaffold record used spec counter.keiro")-        it "reports moved generated modules across a layout flip" $-            withTempDirectory "keiro-dsl-stale-layout" $ \out -> do-                spec <- parseInlineSpec "<stale-layout>" loweringAggregateSpec-                let initialCtx = defaultContext (specContext spec)-                    collocatedCtx = initialCtx{placement = CollocatedLeaf}-                first <- executePlannedScaffold out "counter.keiro" initialCtx spec-                second <- executePlannedScaffold out "counter.keiro" collocatedCtx spec-                let oldGenerated = [StaleModule Generated (modulePath m) | (m, _) <- reportDispositions first, kind m == Generated]-                reportStale second `shouldSatisfy` all (`elem` oldGenerated)-                length (reportStale second) `shouldBe` length oldGenerated-        it "writes a parseable record and no stale section for a fresh output" $-            withTempDirectory "keiro-dsl-record" $ \out -> do-                spec <- parseInlineSpec "<fresh-record>" loweringAggregateSpec-                let ctx = defaultContext (specContext spec)-                report <- executePlannedScaffold out "counter.keiro" ctx spec-                reportStale report `shouldBe` []-                renderScaffoldReport report `shouldSatisfy` all (not . T.isPrefixOf "stale:")-                contents <- TIO.readFile (out </> recordFileName (specContext spec))-                parseRecord contents-                    `shouldBe` Just-                        ScaffoldRecord-                            { recSpecPath = "counter.keiro"-                            , recModuleRoot = ""-                            , recLayout = "prefixed"-                            , recFiles = [(kind m, modulePath m) | (m, _) <- reportDispositions report]-                            }-                parseRecord (T.replace "spec: " "future-field: retained\nspec: " contents) `shouldBe` parseRecord contents-                parseRecord (T.replace "record v1" "record v2" contents) `shouldBe` Nothing--    describe "faithful scaffold lowering" $ do-        it "escapes a trailing-backslash payload literal exactly once" $ do-            spec <- specOf "test/fixtures/hospital-surge.keiro"-            case [process | NProcess process <- specNodes spec] of-                process : _ -> do-                    let timer = (procTimer process){tmPayload = [FieldBinding "kind" (Just "\"follow-up\\\"")]}-                        modules = scaffoldProcess (defaultContext (specContext spec)) process{procTimer = timer}-                    generatedTextEndingIn "Process.hs" modules-                        `shouldSatisfy` T.isInfixOf "\"kind\" .= (\"follow-up\\\\\" :: Value)"-                [] -> expectationFailure "hospital-surge fixture has no process"-        it "preserves quoted Text register initials and refuses unsafe register shapes" $ do-            spec <- parseInlineSpec "<register-initials>" loweringAggregateSpec-            let modules = scaffoldAggregate (defaultContext (specContext spec)) spec =<< [aggregate | NAggregate aggregate <- specNodes spec]-                domain = generatedTextEndingIn "Domain.hs" modules-            domain `shouldSatisfy` T.isInfixOf "RCons (Proxy @\"note\") \"hello world\""-            scaffoldRefusals spec `shouldBe` []-            bare <- parseInlineSpec "<bare-text-initial>" (T.replace "\"hello world\"" "hello" loweringAggregateSpec)-            scaffoldRefusals bare `shouldSatisfy` any (T.isInfixOf "RegTextInitialNotQuoted")-            unsupported <- parseInlineSpec "<unsupported-field>" (T.replace "count:Int" "count:Time" loweringAggregateSpec)-            scaffoldRefusals unsupported `shouldSatisfy` any (T.isInfixOf "FieldTypeUnrepresentable")-        it "lowers seconds, minutes, hours, and both backoff constructors faithfully" $ do-            windowSeconds "90s" `shouldBe` Right 90-            windowSeconds "5m" `shouldBe` Right 300-            windowSeconds "2h" `shouldBe` Right 7200-            emitSource <- readTestText "test/fixtures/emit.keiro"-            let exponentialSource = T.replace "backoff constant 2s" "backoff exponential 2s max=60s multiplier=2.0" emitSource-            exponential <- parseInlineSpec "<exponential-backoff>" exponentialSource-            case [publisher | NPublisher publisher <- specNodes exponential] of-                publisher : _ -> do-                    let generated = generatedTextEndingIn "Publisher.hs" (scaffoldPublisher (defaultContext (specContext exponential)) publisher)-                    generated `shouldSatisfy` T.isInfixOf "ExponentialBackoff ExponentialBackoffOptions { initial = 2, maxDelay = 60, multiplier = 2.0 }"-                    parseSpec "<exponential-round-trip>" (renderSpec exponential) `shouldBe` Right exponential-                [] -> expectationFailure "emit fixture has no publisher"-            constant <- parseInlineSpec "<constant-backoff>" (T.replace "backoff constant 2s" "backoff constant 2m" emitSource)-            case [publisher | NPublisher publisher <- specNodes constant] of-                publisher : _ -> generatedTextEndingIn "Publisher.hs" (scaffoldPublisher (defaultContext (specContext constant)) publisher) `shouldSatisfy` T.isInfixOf "ConstantBackoff 120"-                [] -> expectationFailure "emit fixture has no publisher"-        it "refuses incomplete exponential backoff and rejects unknown window units" $ do-            emitSource <- readTestText "test/fixtures/emit.keiro"-            incomplete <- parseInlineSpec "<incomplete-backoff>" (T.replace "backoff constant 2s" "backoff exponential 2s" emitSource)-            scaffoldRefusals incomplete `shouldSatisfy` any (T.isInfixOf "BackoffExponentialIncomplete")-            parseSpec "<bad-window>" (T.replace "backoff constant 2s" "backoff constant 2x" emitSource)-                `shouldSatisfy` leftContains "time unit: s, m, or h"-        it "lowers workqueue retry windows in minutes to seconds" $ do-            queueSource <- readTestText "test/fixtures/reservation-work.keiro"-            queueSpec <- parseInlineSpec "<minute-queue>" (T.replace "5s" "5m" queueSource)-            case [workqueue | NWorkqueue workqueue <- specNodes queueSpec] of-                workqueue : _ -> do-                    let policy = generatedTextEndingIn "QueuePolicy.hs" (scaffoldWorkqueue (defaultContext (specContext queueSpec)) workqueue)-                    policy `shouldSatisfy` T.isInfixOf "defaultRetryDelay = RetryDelay 300"-                    policy `shouldSatisfy` T.isInfixOf "Retry (RetryDelay 300)"-                [] -> expectationFailure "queue fixture has no workqueue"-        it "uses exact status-map keys and emits total Int harness samples" $ do-            statusSpec <- parseInlineSpec "<exact-status>" exactStatusSpec-            case [aggregate | NAggregate aggregate <- specNodes statusSpec] of-                aggregate : _ -> do-                    let ctx = defaultContext (specContext statusSpec)-                        projection = generatedTextEndingIn "Projection.hs" (scaffoldAggregate ctx statusSpec aggregate)-                        harness = generatedTextEndingIn "Harness.hs" (harnessFor ctx statusSpec aggregate)-                    projection `shouldSatisfy` T.isInfixOf "ReservationUnHeld {} -> Just \"available\""-                    harness `shouldSatisfy` T.isInfixOf "CountBumpedData 0"-                    harness `shouldNotSatisfy` T.isInfixOf "sample: unsupported"-                [] -> expectationFailure "exact-status spec has no aggregate"--    describe "scaffold" $ do-        it "never emits a keiki symbolic operator into a Generated module (firewall)" $ do-            mods <- scaffoldFixture "test/fixtures/reservation.keiro"-            firewallBreaches mods `shouldBe` []-        it "marks the Holes module HoleStub and the rest Generated" $ do-            mods <- scaffoldFixture "test/fixtures/reservation.keiro"-            let holes = [m | m <- mods, "Holes.hs" `T.isSuffixOf` T.pack (modulePath m)]-            map kind holes `shouldBe` [HoleStub]-            -- Domain, Codec, EventStream, Projection, Harness.-            length [m | m <- mods, kind m == Generated] `shouldBe` 5-        it "is deterministic (re-scaffolding yields byte-identical text)" $ do-            a <- scaffoldFixture "test/fixtures/reservation.keiro"-            b <- scaffoldFixture "test/fixtures/reservation.keiro"-            map moduleText a `shouldBe` map moduleText b-        it "matches the committed compiling Generated conformance modules (modulo whitespace)" $ do-            mods <- scaffoldFixture "test/fixtures/reservation.keiro"-            mapM_ assertMatchesCommitted [m | m <- mods, kind m == Generated]-        it "matches every committed new-surface Generated module (modulo formatting)" $ do-            spec <- specOf "test/fixtures/transfer-routing.keiro"-            let modules = scaffoldModules (defaultContext (specContext spec)) spec-            forM_ [m | m <- modules, kind m == Generated] $ \m -> do-                committed <- readTestText ("test/conformance-newsurface/" <> modulePath m)-                normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)-        it "scaffolds the register-free OrderStream smoke target without error" $ do-            mods <- scaffoldFixture "test/fixtures/order.keiro"-            -- 5 Generated (Domain/Codec/EventStream/Projection/Harness) + 1 Holes.-            length mods `shouldBe` 6-            firewallBreaches mods `shouldBe` []--syntheticGenerated :: FilePath -> T.Text -> ScaffoldModule-syntheticGenerated path contents =-    ScaffoldModule{modulePath = path, moduleText = contents, kind = Generated, origin = "test"}--generatedTextEndingIn :: T.Text -> [ScaffoldModule] -> T.Text-generatedTextEndingIn suffix modules = case [moduleText m | m <- modules, kind m == Generated, suffix `T.isSuffixOf` T.pack (modulePath m)] of-    contents : _ -> contents-    [] -> ""--loweringAggregateSpec :: T.Text-loweringAggregateSpec =-    T.unlines-        [ "context samples"-        , ""-        , "aggregate Counter"-        , "  regs"-        , "    note Text = \"hello world\""-        , "    count Int = 0"-        , "    state CounterVertex = Pending"-        , "  states Pending Done!"-        , "  command Bump { count:Int }"-        , "  event CountBumped { count:Int }"-        , "  Pending -- Bump --> emit CountBumped ; goto Done"-        ]--exactStatusSpec :: T.Text-exactStatusSpec =-    T.unlines-        [ "context samples"-        , ""-        , "aggregate Reservation"-        , "  regs"-        , "    state ReservationVertex = Open"-        , "  states Open Closed!"-        , "  command Bump { count:Int }"-        , "  event ReservationHeld { count:Int }"-        , "  event ReservationUnHeld { count:Int }"-        , "  event CountBumped { count:Int }"-        , "  Open -- Bump --> emit CountBumped ; goto Closed"-        , "  projection reservation_status consistency=Eventual key=count"-        , "    status-map { ReservationHeld=>held ReservationUnHeld=>available CountBumped=>bumped }"-        ]--hasPathCollisionWithTwoOrigins :: Either [Refusal] [ScaffoldModule] -> Bool-hasPathCollisionWithTwoOrigins = \case-    Left refusals -> any hasTwo refusals-    Right _ -> False-  where-    hasTwo (PathCollision _ origins) = length origins == 2-    hasTwo _ = False--isMissingBannerRefusal :: Either [Refusal] a -> Bool-isMissingBannerRefusal = \case-    Left [MissingGeneratedBanner paths] -> not (null paths)-    _ -> False--isSuccessfulScaffold :: Either [Refusal] a -> Bool-isSuccessfulScaffold = \case-    Right _ -> True-    Left _ -> False--executePlannedScaffold :: FilePath -> FilePath -> Context -> Spec -> IO ScaffoldReport-executePlannedScaffold out specPath ctx spec = case planScaffold ctx spec of-    Left refusals -> expectationFailure ("unexpected scaffold refusal: " <> show refusals) >> error "unreachable"-    Right modules -> do-        result <- executeScaffold out False specPath ctx spec modules-        case result of-            Left refusals -> expectationFailure ("unexpected execution refusal: " <> show refusals) >> error "unreachable"-            Right report -> pure report--renameCounter :: Node -> Node-renameCounter (NAggregate aggregate) =-    NAggregate-        aggregate-            { aggName = "Widget"-            , aggRegs = [reg{regType = if regType reg == "CounterVertex" then "WidgetVertex" else regType reg} | reg <- aggRegs aggregate]-            }-renameCounter node = node--onlyPathEndingIn :: FilePath -> [ScaffoldModule] -> FilePath-onlyPathEndingIn suffix modules = case [modulePath m | m <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath m)] of-    [path] -> path-    paths -> error ("expected one path ending in " <> suffix <> ", got " <> show paths)--withTempDirectory :: String -> (FilePath -> IO a) -> IO a-withTempDirectory template = bracket acquire removePathForcibly-  where-    acquire = do-        base <- getTemporaryDirectory-        (path, handle) <- openTempFile base template-        hClose handle-        removeFile path-        createDirectory path-        pure path--{- | Parse a fixture and return the validator's diagnostic codes (failing the-test on a parse error).--}-diagnosticCodesOf :: FilePath -> IO [DiagnosticCode]-diagnosticCodesOf path = do-    input <- readTestText path-    case parseSpec path input of-        Left err -> expectationFailure (T.unpack err) >> pure []-        Right spec -> pure (map code (validateSpec spec))--{- | Like 'diagnosticCodesOf' but only the Error-severity codes (warnings, e.g.-the benign-inversion notices, are excluded).--}-errorCodesOf :: FilePath -> IO [DiagnosticCode]-errorCodesOf path = do-    input <- readTestText path-    case parseSpec path input of-        Left err -> expectationFailure (T.unpack err) >> pure []-        Right spec -> pure [code d | d <- validateSpec spec, severity d == Error]---- | Parse two fixtures and diff them (old, new).-diffFixtures :: FilePath -> FilePath -> IO [Change]-diffFixtures oldP newP = do-    old <- readTestText oldP-    new <- readTestText newP-    case (,) <$> parseSpec oldP old <*> parseSpec newP new of-        Left err -> expectationFailure (T.unpack err) >> pure []-        Right (o, n) -> pure (diffSpecs o n)--modifyReadModel :: Name -> (ReadModelNode -> ReadModelNode) -> Spec -> Spec-modifyReadModel target update spec =-    spec-        { specNodes =-            [ case node of-                NReadModel readModel | rmName readModel == target -> NReadModel (update readModel)-                _ -> node-            | node <- specNodes spec-            ]-        }--removeReadModel :: Name -> Spec -> Spec-removeReadModel target spec =-    spec{specNodes = [node | node <- specNodes spec, not (isTarget node)]}-  where-    isTarget (NReadModel readModel) = rmName readModel == target-    isTarget _ = False--modifyRouter :: Name -> (RouterNode -> RouterNode) -> Spec -> Spec-modifyRouter target update spec =-    spec-        { specNodes =-            [ case node of-                NRouter router | rtId router == target -> NRouter (update router)-                _ -> node-            | node <- specNodes spec-            ]-        }--routerErrorCodes :: (RouterNode -> RouterNode) -> Spec -> [DiagnosticCode]-routerErrorCodes update = errorCodes . modifyRouter "PagingRouter" update--modifyProcess :: Name -> (ProcessNode -> ProcessNode) -> Spec -> Spec-modifyProcess target update spec =-    spec-        { specNodes =-            [ case node of-                NProcess process | procId process == target -> NProcess (update process)-                _ -> node-            | node <- specNodes spec-            ]-        }--processErrorCodes :: (ProcessNode -> ProcessNode) -> Spec -> [DiagnosticCode]-processErrorCodes update = errorCodes . modifyProcess "HospitalSurge" update--errorCodes :: Spec -> [DiagnosticCode]-errorCodes spec = [code diagnostic | diagnostic <- validateSpec spec, severity diagnostic == Error]--changeReadModelShape :: ReadModelNode -> ReadModelNode-changeReadModelShape readModel =-    readModel-        { rmColumns = rmColumns readModel <> [RmColumn "reviewed_by" "text" False]-        , rmShape = "fnv1a:0000000000000000"-        }--{- | Assert a @new \<kind\>@ skeleton parses and validates with zero-error-severity diagnostics.--}-assertSkeletonValid :: T.Text -> IO ()-assertSkeletonValid kind = case skeletonFor kind of-    Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))-    Right src -> case parseSpec ("new:" <> T.unpack kind) src of-        Left perr -> expectationFailure (T.unpack ("skeleton for " <> kind <> " failed to parse: " <> perr))-        Right spec ->-            [code d | d <- validateSpec spec, severity d == Error]-                `shouldBe` ([] :: [DiagnosticCode])--assertSkeletonScaffoldable :: T.Text -> IO ()-assertSkeletonScaffoldable kind = case skeletonFor kind of-    Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))-    Right src -> case parseSpec ("new:" <> T.unpack kind) src of-        Left perr -> expectationFailure (T.unpack perr)-        Right spec -> planScaffold (defaultContext (specContext spec)) spec `shouldSatisfy` isSuccessfulScaffold--skeletonModuleRoots :: [(T.Text, T.Text)]-skeletonModuleRoots =-    [ ("aggregate", "SkelAggregate")-    , ("process", "SkelProcess")-    , ("router", "SkelRouter")-    , ("contract", "SkelContract")-    , ("intake", "SkelIntake")-    , ("emit", "SkelEmit")-    , ("workqueue", "SkelQueue")-    , ("workflow", "SkelWorkflow")-    ]--assertSkeletonMatchesCommitted :: T.Text -> T.Text -> IO ()-assertSkeletonMatchesCommitted kind root = case skeletonFor kind of-    Left err -> expectationFailure (T.unpack err)-    Right source -> case parseSpec ("new:" <> T.unpack kind) source of-        Left err -> expectationFailure (T.unpack err)-        Right spec -> do-            let ctx = (defaultContext (specContext spec)){moduleRoot = root}-            forM_ [m | m <- scaffoldModules ctx spec, kindOf m == Generated] $ \m -> do-                committed <- readTestText ("test/conformance-skeletons/" <> modulePath m)-                normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)-  where-    kindOf = Keiro.Dsl.Scaffold.kind---- | Parse a fixture into a 'Spec', failing the test on a parse error.-specOf :: FilePath -> IO Spec-specOf path = do-    input <- readTestText path-    case parseSpec path input of-        Left err -> expectationFailure (T.unpack err) >> error "unreachable"-        Right spec -> pure spec---- | Parse a fixture and scaffold every aggregate in it.-scaffoldFixture :: FilePath -> IO [ScaffoldModule]-scaffoldFixture path = do-    input <- readTestText path-    case parseSpec path input of-        Left err -> expectationFailure (T.unpack err) >> pure []-        Right spec ->-            pure $-                concat-                    [ scaffoldAggregate (ctx spec) spec agg <> harnessFor (ctx spec) spec agg-                    | NAggregate agg <- specNodes spec-                    ]-  where-    ctx spec = defaultContext (specContext spec)--scaffoldProcessFixture :: FilePath -> IO [ScaffoldModule]-scaffoldProcessFixture path = do-    input <- readTestText path-    case parseSpec path input of-        Left err -> expectationFailure (T.unpack err) >> pure []-        Right spec ->-            pure $ concat [scaffoldProcess (ctx spec) p | NProcess p <- specNodes spec]-  where-    ctx spec = defaultContext (specContext spec)--{- | Assert a freshly-scaffolded Generated module matches its committed copy-under test/conformance/ (whitespace-normalized). The committed copies are the-ones the keiro-dsl-conformance suite compiles, so this pins the live scaffolder-to known-compiling output.--}-assertMatchesCommitted :: ScaffoldModule -> IO ()-assertMatchesCommitted m = do-    let committedPath = "test/conformance/" <> modulePath m-    committed <- readTestText committedPath-    normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)--normalizeGenerated :: T.Text -> (T.Text, [T.Text])-normalizeGenerated text =-    let (imports, body) = partition isImport (T.lines text)-     in (normalizeBody body, sort (map normalizeImport imports))-  where-    -- Compare the deterministic body exactly as before and imports as a sorted,-    -- whitespace-normalized list. Sorting tolerates formatter reordering while-    -- additions, removals, and renamed imports now fail the pin.-    normalizeBody =-        T.replace " , )" " )"-            . T.unwords-            . T.words-            . T.replace "}" " } "-            . T.replace "{" " { "-            . T.replace "]" " ] "-            . T.replace "[" " [ "-            . T.replace "," " , "-            . T.unlines-    normalizeImport line =-        let reordered = case T.words line of-                "import" : "qualified" : moduleName : rest -> T.unwords ("import" : moduleName : "qualified" : rest)-                wordsInImport -> T.unwords wordsInImport-            (prefix, explicit) = T.breakOn " (" reordered-         in if T.null explicit-                then prefix-                else-                    let members =-                            sort-                                . map (T.unwords . T.words)-                                . T.splitOn ","-                                . T.dropEnd 1-                                $ T.drop 2 explicit-                     in prefix <> " (" <> T.intercalate "," members <> ")"-    isImport line = case T.words line of-        "import" : _ -> True-        _ -> False--{- | Locate and read a test fixture or committed conformance source regardless-of whether the suite was launched from the package directory or repo root.--}-readTestText :: FilePath -> IO T.Text-readTestText path = resolveTestPath path >>= TIO.readFile---- | Locate a repo file regardless of the test process's current directory.-resolveTestPath :: FilePath -> IO FilePath-resolveTestPath rel = do-    override <- lookupEnv "KEIRO_DSL_TEST_ROOT"-    let candidates = [rel, "keiro-dsl" </> rel] <> maybe [] (\root -> [root </> rel]) override-    existing <- filterM doesFileExist candidates-    case existing of-        path : _ -> pure path-        [] ->-            fail $-                "unable to locate keiro-dsl test file "-                    <> show rel-                    <> "; tried "-                    <> show candidates--leftContains :: T.Text -> Either T.Text a -> Bool-leftContains needle = \case-    Left err -> needle `T.isInfixOf` err-    Right _ -> False--parseInlineSpec :: FilePath -> T.Text -> IO Spec-parseInlineSpec sourceName src = case parseSpec sourceName src of-    Left err -> expectationFailure (T.unpack err) >> error "unreachable"-    Right spec -> pure spec--statusMapSpec :: T.Text -> T.Text-statusMapSpec marker =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  event Created { }"-        , "  event Changed { }"-        , ""-        , "  projection things consistency=Eventual key=thingId"-        , "    status-map" <> marker <> " { Created=>held }"-        ]--parseErrorOf :: FilePath -> T.Text -> IO T.Text-parseErrorOf sourceName src = case parseSpec sourceName src of-    Left err -> pure err-    Right _ -> expectationFailure ("expected parse failure for " <> sourceName) >> error "unreachable"--duplicateGotoSpec :: T.Text-duplicateGotoSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states A B C"-        , ""-        , "  command Go { }"-        , "  A -- Go -->"-        , "    goto B"-        , "    goto C"-        ]--missingGotoSpec :: T.Text-missingGotoSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states A B"-        , ""-        , "  command Go { }"-        , "  A -- Go -->"-        , "    emit Changed"-        ]--duplicateWireSpec :: T.Text-duplicateWireSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  wire kind=ctorName fields=camelCase schemaVersion=1"-        , "  wire kind=typeName fields=snakeCase schemaVersion=2"-        ]--duplicateProjectionSpec :: T.Text-duplicateProjectionSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  projection first consistency=Strong key=thingId"-        , "    status-map partial { }"-        , "  projection second consistency=Eventual key=thingId"-        ]--projectionWithoutConsistencySpec :: T.Text-projectionWithoutConsistencySpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  projection things key=thingId"-        ]--malformedRegisterSpec :: T.Text-malformedRegisterSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "    status Status"-        , "  states Open"-        ]--misplacedDispatchIdSpec :: T.Text-misplacedDispatchIdSpec =-    T.replace-        "    schedule timer\n\n  dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId, emitIndex)\n"-        "    dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId, emitIndex)\n    schedule timer\n"-        (renderSpec (Spec "svc" Nothing Nothing [] [] [] [NProcess (processWithLiteral "literal")]))--lineNumberContaining :: T.Text -> T.Text -> Int-lineNumberContaining needle = go 1 . T.lines-  where-    go current = \case-        [] -> current-        lineText : rest-            | needle `T.isInfixOf` lineText -> current-            | otherwise -> go (current + 1) rest--decimalOverflow :: T.Text-decimalOverflow = "18446744073709551617"--decimalOverflowSpecs :: [(String, T.Text)]-decimalOverflowSpecs =-    [ ("event-version", eventVersionDecimalSpec decimalOverflow)-    , ("wire-schema", wireDecimalSpec decimalOverflow)-    , ("contract-schema", contractDecimalSpec decimalOverflow)-    , ("decode-schema", decodeDecimalSpec decimalOverflow)-    , ("publisher-attempts", publisherDecimalSpec decimalOverflow)-    , ("workqueue-retries", workqueueDecimalSpec decimalOverflow)-    , ("timer-attempts", timerDecimalSpec decimalOverflow)-    ]--eventVersionDecimalSpec :: T.Text -> T.Text-eventVersionDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  event Changed v" <> value <> " { }"-        ]--wireDecimalSpec :: T.Text -> T.Text-wireDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  wire kind=ctorName fields=camelCase schemaVersion=" <> value-        ]--contractDecimalSpec :: T.Text -> T.Text-contractDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "contract Contract {"-        , "  schemaVersion " <> value-        , "  discriminator kind"-        , "}"-        ]--decodeDecimalSpec :: T.Text -> T.Text-decodeDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "intake Inbox {"-        , "  contract Contract"-        , "  topic events"-        , "  accept Event"-        , "  dedupe key messageId policy PreferIntegrationMessageId"-        , "  decode { envelope strict-required lenient-optional body strict schemaVersion == " <> value <> " }"-        , "  disposition { }"-        , "}"-        ]--publisherDecimalSpec :: T.Text -> T.Text-publisherDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "publisher Publisher {"-        , "  emit Emit"-        , "  ordering PerKeyHeadOfLine"-        , "  maxAttempts " <> value-        , "  backoff constant 2s"-        , "  outboxId stable from messageId"-        , "}"-        ]--workqueueDecimalSpec :: T.Text -> T.Text-workqueueDecimalSpec value =-    T.unlines-        [ "context svc"-        , ""-        , "workqueue Queue {"-        , "  queue logical = \"queue\""-        , "  derive physical = \"queue\""-        , "    dlq = \"queue_dlq\""-        , "    table = \"pgmq.q_queue\""-        , "  payload Job { }"-        , "  retry maxRetries = " <> value <> " delay = 5s dlq = on"-        , "  disposition { }"-        , "}"-        ]--timerDecimalSpec :: T.Text -> T.Text-timerDecimalSpec value =-    T.replace-        "max-attempts 5"-        ("max-attempts " <> value)-        (renderSpec (Spec "svc" Nothing Nothing [] [] [] [NProcess (processWithLiteral "literal")]))--identifierHygieneSpec :: T.Text-identifierHygieneSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate thing"-        , "  regs"-        , "  states Open"-        , ""-        , "  command DoIt { data }"-        ]--vertexCollisionSpec :: T.Text-vertexCollisionSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Reservation"-        , "  regs"-        , "  states Created"-        , ""-        , "  event ReservationCreated { }"-        ]--underscoreNodeSpec :: T.Text-underscoreNodeSpec =-    T.unlines-        [ "context svc"-        , ""-        , "contract _contract {"-        , "  schemaVersion 1"-        , "  discriminator kind"-        , "}"-        ]--unicodeIdentifierSpec :: T.Text-unicodeIdentifierSpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate Résumé"-        , "  regs"-        , "  states Open"-        ]--emptyStatesSpec :: Spec-emptyStatesSpec =-    Spec-        "svc"-        Nothing-        Nothing-        []-        []-        []-        [NAggregate (Aggregate "Thing" [] [] [] [] [] Nothing Nothing Nothing noLoc)]--crossFamilyBoundarySpec :: T.Text-crossFamilyBoundarySpec =-    T.unlines-        [ "context svc"-        , ""-        , "aggregate First"-        , "  regs"-        , "  states A B"-        , "  command Go { }"-        , "  A -- Go -->"-        , "    emit Changed"-        , "    goto B"-        , ""-        , "emit Output {"-        , "  contract Contract"-        , "  topic events"-        , "  source \"source\""-        , "  key thingId"-        , "  map status { _ => skip }"-        , "  messageId derive hole"-        , "  idempotencyKey derive hole"-        , "}"-        , ""-        , "aggregate Second"-        , "  regs"-        , "  states"-        , ""-        , "dispatch QueueDispatch {"-        , "  source readModel = source key = thingId"-        , "  fanout body = resolveFanout"-        , "  dedup key = thingId"-        , "    seenIn readModel = seen field = thingId"-        , "    seenIn queue = workQueue field = thingId"-        , "  enqueue to = workQueue"-        , "}"-        ]------------------------------------------------------------------------------------- Generators (bounded; restricted to valid, non-reserved identifiers)-----------------------------------------------------------------------------------{- | Text that exercises every supported escape plus notation punctuation that-used to be able to split one emit-map row into several rows.--}-genAdversarialText :: Gen T.Text-genAdversarialText =-    T.concat-        <$> resize-            20-            (listOf (elements ["a", "Z", "\"", "\\", "\n", "\t", "\r", "=>", "#", "{", "}", " "]))--{- | One spec carrying the same adversarial value through three distinct-printer paths: a contract topic, an emit-map value, and a quote-wrapped-field-binding literal.--}-escapedSpec :: T.Text -> Spec-escapedSpec value =-    Spec-        "escape"-        Nothing-        Nothing-        []-        []-        []-        [ NContract-            ContractNode-                { ctrName = "Contract"-                , ctrSchemaVersion = 1-                , ctrDiscriminator = "kind"-                , ctrTopics = [("events", value)]-                , ctrEvents = []-                , ctrLoc = noLoc-                }-        , NEmit-            EmitNode-                { emName = "Emit"-                , emContract = "Contract"-                , emTopic = "events"-                , emSource = "source"-                , emKey = "key"-                , emDiscriminant = "status"-                , emMap = [EmitMapRow value "Event" noLoc]-                , emSkip = True-                , emMessageId = DeriveSpec Nothing-                , emIdempotencyKey = DeriveSpec Nothing-                , emLoc = noLoc-                }-        , NProcess (processWithLiteral value)-        ]--processWithLiteral :: T.Text -> ProcessNode-processWithLiteral value =-    ProcessNode-        { procId = "Process"-        , procName = "process"-        , procInput = InputDecl "Input" []-        , procCorrelate = CorrelateDecl "key" "idText"-        , procSaga = SagaRef "Saga" "saga"-        , procTarget = "Target"-        , procProjections = []-        , procHandle =-            HandleNode-                { hOn = "Input"-                , hAdvance = AdvanceNode "Advance" [FieldBinding "literal" (Just ("\"" <> value <> "\""))]-                , hDispatch = []-                , hSchedule = "timer"-                }-        , procRejected = PolHalt-        , procPoison = PolHalt-        , procTimer =-            TimerNode-                { tmName = "timer"-                , tmId = IdExpr UuidV5Id "timer:"-                , tmFireAt = FireAtExpr "observedAt" "5m"-                , tmPayload = []-                , tmFire =-                    FireNode-                        { fireTarget = "Target"-                        , fireKey = "correlationId"-                        , fireCommand = "Fire"-                        , fireFields = []-                        , fireFiredEventId = IdExpr UuidV5Id "fired:"-                        , fireDisposition = FireDisposition OFired OFired ORetry ORetry ORetry-                        }-                , tmDecodeUnknown = "Cancelled"-                , tmMaxAttempts = 5-                , tmDeadLetter = "exhausted"-                , tmLoc = noLoc-                }-        , procLoc = noLoc-        }--genName :: Gen Name-genName =-    frequency-        [-            ( 3-            , do-                base <- elements ["Aa", "Bb", "Cc", "Dd", "St", "Cmd", "Ev", "Reg", "Fld", "Foo", "Bar", "Qux"]-                n <- choose (0, 9 :: Int)-                pure (T.pack (base <> show n))-            )-        , (1, elements ["data1", "typeA", "whereX", "gotoX", "guardY", "emitZ", "_lead"])-        ]--genWire :: Gen T.Text-genWire = do-    base <- elements ["red", "blue", "green", "ctorName", "camelCase", "rsv", "hosp", "held", "partial-divert", "1st"]-    n <- choose (0, 9 :: Int)-    pure (T.pack (base <> show n))--genWireWord :: Gen T.Text-genWireWord = genWire--smallList :: Gen a -> Gen [a]-smallList g = choose (0, 3 :: Int) >>= \n -> vectorOf n g--nonEmptyList :: Gen a -> Gen [a]-nonEmptyList g = choose (1, 3 :: Int) >>= \n -> vectorOf n g--genMaybe :: Gen a -> Gen (Maybe a)-genMaybe g = oneof [pure Nothing, Just <$> g]--genCmp :: Gen CmpOp-genCmp = elements [OpEq, OpNeq, OpLt, OpLe, OpGt, OpGe]--genAtom :: Gen Expr-genAtom = EAtom <$> oneof [AName <$> genName, ABool <$> arbitrary]--genExpr :: Gen Expr-genExpr = go (3 :: Int)-  where-    go 0 = genAtom-    go d =-        oneof-            [ genAtom-            , EOr <$> go (d - 1) <*> go (d - 1)-            , EAnd <$> go (d - 1) <*> go (d - 1)-            , ECmp <$> genCmp <*> go (d - 1) <*> go (d - 1)-            ]--genField :: Gen Field-genField = Field <$> genName <*> oneof [pure Nothing, Just <$> genName]--genReg :: Gen RegDecl-genReg = RegDecl <$> genName <*> genName <*> genRegInitial <*> pure noLoc--genRegInitial :: Gen RegInitial-genRegInitial = oneof [RegInitBare <$> genName, RegInitText <$> genAdversarialText]--genState :: Gen StateDecl-genState = StateDecl <$> genName <*> arbitrary <*> pure noLoc--genCommand :: Gen Command-genCommand = Command <$> genName <*> smallList genField <*> pure noLoc--genEvent :: Gen Event-genEvent =-    Event-        <$> genName-        <*> body-        <*> choose (1, 3)-        <*> genMaybe ((,) <$> choose (0, 3) <*> pure Hole)-        <*> arbitrary-        <*> pure noLoc-  where-    body = oneof [EventFromCommand <$> genName, EventFields <$> smallList genField]--genTransition :: Gen Transition-genTransition =-    Transition-        <$> genName-        <*> genName-        <*> genMaybe genExpr-        <*> smallList ((,) <$> genName <*> genExpr)-        <*> smallList genName-        <*> genName-        <*> pure noLoc--genWireSpec :: Gen WireSpec-genWireSpec = WireSpec <$> genWire <*> genWire <*> (getNonNegative <$> arbitrary)--genProjection :: Gen ProjectionSpec-genProjection =-    ProjectionSpec-        <$> genName-        <*> genMaybe (elements [Strong, Eventual])-        <*> genName-        <*> genMaybe (Mapping <$> smallList ((,) <$> genName <*> genWire) <*> arbitrary)-        <*> pure noLoc--genAggregate :: Gen Aggregate-genAggregate =-    Aggregate-        <$> genName-        <*> smallList genReg-        <*> smallList genState-        <*> smallList genCommand-        <*> smallList genEvent-        <*> smallList genTransition-        <*> genMaybe genWireSpec-        <*> genMaybe genProjection-        <*> genMaybe (SnapshotSpec <$> oneof [SnapEvery <$> choose (0, 5), pure SnapOnTerminal] <*> choose (0, 5) <*> genAdversarialText <*> pure noLoc)-        <*> pure noLoc--genDottedRef :: Gen T.Text-genDottedRef = elements ["input.id", "input.hospitalId", "timer.id", "correlationId", "payload.messageId"]--genWindow :: Gen T.Text-genWindow = elements ["0s", "5s", "2m", "1h"]--genFieldBinding :: Gen FieldBinding-genFieldBinding =-    FieldBinding-        <$> genName-        <*> oneof-            [ pure Nothing-            , Just <$> genDottedRef-            , Just . (\raw -> "\"" <> raw <> "\"") <$> genAdversarialText-            ]--genDispatchDisposition :: Gen DispatchDisposition-genDispatchDisposition = DispatchDisposition <$> genDisp <*> genDisp <*> genDisp-  where-    genDisp = oneof [pure DAckOk, pure DRetry, DDeadLetter <$> genAdversarialText]--genDispatchNode :: Gen DispatchNode-genDispatchNode =-    DispatchNode-        <$> genName-        <*> genDottedRef-        <*> genName-        <*> smallList genFieldBinding-        <*> genDispatchDisposition-        <*> pure noLoc--genFireDisposition :: Gen FireDisposition-genFireDisposition =-    FireDisposition-        <$> elements [OFired, ORetry]-        <*> elements [OFired, ORetry]-        <*> elements [OFired, ORetry]-        <*> elements [OFired, ORetry]-        <*> elements [OFired, ORetry]--genIdExpr :: Gen IdExpr-genIdExpr = IdExpr UuidV5Id <$> genAdversarialText--genFireNode :: Gen FireNode-genFireNode =-    FireNode-        <$> genName-        <*> genDottedRef-        <*> genName-        <*> smallList genFieldBinding-        <*> genIdExpr-        <*> genFireDisposition--genTimerNode :: Gen TimerNode-genTimerNode =-    TimerNode-        <$> genName-        <*> genIdExpr-        <*> (FireAtExpr <$> genName <*> genWindow)-        <*> smallList genFieldBinding-        <*> genFireNode-        <*> genName-        <*> choose (0, 5)-        <*> genAdversarialText-        <*> pure noLoc--genProcess :: Gen ProcessNode-genProcess =-    ProcessNode-        <$> genName-        <*> genAdversarialText-        <*> (InputDecl <$> genName <*> smallList genField)-        <*> (CorrelateDecl <$> genName <*> genName)-        <*> (SagaRef <$> genName <*> genAdversarialText)-        <*> genName-        <*> smallList genName-        <*> (HandleNode <$> genName <*> (AdvanceNode <$> genName <*> smallList genFieldBinding) <*> smallList genDispatchNode <*> genName)-        <*> elements [PolHalt, PolDeadLetter, PolSkip]-        <*> elements [PolHalt, PolDeadLetter, PolSkip]-        <*> genTimerNode-        <*> pure noLoc--genResolveSource :: Gen ResolveSource-genResolveSource = oneof [ResolveReadModel <$> genName, pure ResolveHole]--genRouter :: Gen RouterNode-genRouter =-    RouterNode-        <$> genName-        <*> genAdversarialText-        <*> (InputDecl <$> genName <*> smallList genField)-        <*> (CorrelateDecl <$> genName <*> genName)-        <*> (ResolveDecl <$> genResolveSource <*> smallList genName <*> pure noLoc)-        <*> genName-        <*> smallList genName-        <*> (RouterDispatchNode <$> genName <*> smallList genFieldBinding <*> genDispatchDisposition <*> pure noLoc)-        <*> elements [PolHalt, PolDeadLetter, PolSkip]-        <*> elements [PolHalt, PolDeadLetter, PolSkip]-        <*> pure noLoc--genContractField :: Gen ContractField-genContractField = ContractField <$> genName <*> oneof [CTypeId <$> genAdversarialText, pure CText, pure CInt]--genContractEvent :: Gen ContractEvent-genContractEvent = ContractEvent <$> genName <*> genName <*> smallList genContractField--genContract :: Gen ContractNode-genContract =-    ContractNode-        <$> genName-        <*> choose (0, 5)-        <*> genName-        <*> smallList ((,) <$> genName <*> genAdversarialText)-        <*> smallList genContractEvent-        <*> pure noLoc--genWireSource :: Gen WireSource-genWireSource = oneof [SrcHeader <$> genAdversarialText, pure SrcBody, pure SrcKafkaKey, pure SrcKafkaCursor]--genInboxAction :: Gen InboxAction-genInboxAction = oneof [pure IAckOk, IRetry <$> genWindow, IDeadLetter <$> genMaybe genAdversarialText]--genDispositionRow :: Gen DispositionRow-genDispositionRow = DispositionRow <$> genName <*> genInboxAction <*> pure noLoc--genDecodeSpec :: Gen DecodeSpec-genDecodeSpec =-    DecodeSpec-        <$> ((\first second -> first <> " " <> second) <$> genWireWord <*> genWireWord)-        <*> arbitrary-        <*> choose (0, 5)--genIntake :: Gen IntakeNode-genIntake =-    IntakeNode-        <$> genName-        <*> genName-        <*> genName-        <*> nonEmptyList genName-        <*> smallList (BindRow <$> genName <*> genWireSource <*> arbitrary <*> arbitrary)-        <*> genName-        <*> genName-        <*> elements [InkPersistFull, InkPersistDedupeOnly]-        <*> genDecodeSpec-        <*> smallList genDispositionRow-        <*> pure noLoc--genDeriveSpec :: Gen DeriveSpec-genDeriveSpec = DeriveSpec <$> genMaybe genAdversarialText--genEmit :: Gen EmitNode-genEmit =-    EmitNode-        <$> genName-        <*> genName-        <*> genName-        <*> genAdversarialText-        <*> genName-        <*> genName-        <*> smallList (EmitMapRow <$> genAdversarialText <*> genName <*> pure noLoc)-        <*> arbitrary-        <*> genDeriveSpec-        <*> genDeriveSpec-        <*> pure noLoc--genPublisher :: Gen PublisherNode-genPublisher =-    PublisherNode-        <$> genName-        <*> genName-        <*> genName-        <*> choose (0, 5)-        <*> (BackoffSpec <$> genName <*> genWindow <*> genMaybe genWindow <*> genMaybe (elements ["1.0", "2.0", "3"]))-        <*> genName-        <*> pure noLoc--genWqField :: Gen WqField-genWqField = WqField <$> genName <*> genAdversarialText <*> genName <*> arbitrary--genWqDispRow :: Gen WqDispRow-genWqDispRow = WqDispRow <$> genName <*> genInboxAction <*> pure noLoc--genWorkqueue :: Gen WorkqueueNode-genWorkqueue =-    WorkqueueNode-        <$> genName-        <*> genAdversarialText-        <*> genAdversarialText-        <*> genAdversarialText-        <*> genAdversarialText-        <*> elements [WqUnordered, WqFifoThroughput, WqFifoRoundRobin]-        <*> genMaybe (WqGroupKey <$> genName <*> genName <*> genMaybe genAdversarialText)-        <*> oneof [pure WqStandard, pure WqUnlogged, WqPartitioned <$> genAdversarialText <*> genAdversarialText]-        <*> genName-        <*> smallList genWqField-        <*> choose (0, 5)-        <*> genWindow-        <*> arbitrary-        <*> smallList genWqDispRow-        <*> pure noLoc--genReadModel :: Gen ReadModelNode-genReadModel =-    ReadModelNode-        <$> genName-        <*> genAdversarialText-        <*> genAdversarialText-        <*> smallList (RmColumn <$> genWireWord <*> genName <*> arbitrary)-        <*> choose (0, 5)-        <*> genAdversarialText-        <*> elements [Strong, Eventual]-        <*> genMaybe (oneof [pure RmEntireLog, RmCategory <$> genAdversarialText])-        <*> elements [RmInline, RmSubscription]-        <*> genMaybe genAdversarialText-        <*> pure noLoc--genPgmqDispatch :: Gen PgmqDispatchNode-genPgmqDispatch =-    PgmqDispatchNode-        <$> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> genName-        <*> pure noLoc--genWfBodyItem :: Gen WfBodyItem-genWfBodyItem = sized go-  where-    go size =-        oneof $-            [ WfStep <$> genWireWord <*> genName <*> pure noLoc-            , WfAwait <$> genWireWord <*> genName <*> pure noLoc-            , WfSleep <$> genWireWord <*> genName <*> pure noLoc-            , WfChild <$> genWireWord <*> genName <*> genName <*> pure noLoc-            , WfContinueAsNew <$> genName <*> pure noLoc-            ]-                ++ [ WfPatch <$> genWireWord <*> resize (size `div` 2) (smallList genWfBodyItem) <*> pure noLoc-                   | size > 0-                   ]--genWorkflow :: Gen WorkflowNode-genWorkflow =-    WorkflowNode-        <$> genName-        <*> genAdversarialText-        <*> genName-        <*> smallList genField-        <*> genName-        <*> genMaybe genName-        <*> genName-        <*> smallList genWfBodyItem-        <*> pure noLoc--genOperationShape :: Gen OperationShape-genOperationShape =-    oneof-        [ CommandOp <$> genName <*> genName <*> genName <*> smallList genName-        , QueryOp <$> genName <*> genName <*> ((\parts -> T.unwords parts) <$> nonEmptyList genName) <*> genName-        , SignalOp <$> genWireWord <*> genName <*> genName <*> genName <*> genName-        , RunOp <$> genName <*> genName <*> genName-        ]--genOperation :: Gen OperationNode-genOperation = OperationNode <$> genName <*> genOperationShape <*> pure noLoc--allNodeTags :: [String]-allNodeTags = ["aggregate", "process", "router", "contract", "intake", "emit", "publisher", "workqueue", "pgmq-dispatch", "readmodel", "workflow", "operation"]--nodeTag :: Node -> String-nodeTag = \case-    NAggregate _ -> "aggregate"-    NProcess _ -> "process"-    NRouter _ -> "router"-    NContract _ -> "contract"-    NIntake _ -> "intake"-    NEmit _ -> "emit"-    NPublisher _ -> "publisher"-    NWorkqueue _ -> "workqueue"-    NPgmqDispatch _ -> "pgmq-dispatch"-    NReadModel _ -> "readmodel"-    NWorkflow _ -> "workflow"-    NOperation _ -> "operation"--genId :: Gen IdDecl-genId = IdDecl <$> genName <*> genWire <*> pure noLoc--genEnum :: Gen EnumDecl-genEnum = EnumDecl <$> genName <*> smallList ((,) <$> genName <*> genWire) <*> pure noLoc--genRule :: Gen RuleDecl-genRule =-    RuleDecl-        <$> genName-        <*> genName-        <*> genName-        <*> nonEmptyList ((,) <$> genName <*> genExpr)-        <*> pure noLoc--genSpec :: Gen Spec-genSpec =-    Spec-        <$> genWire-        <*> genMaybe genModuleRoot-        <*> genMaybe (elements [GeneratedPrefix, CollocatedLeaf])-        <*> smallList genId-        <*> smallList genEnum-        <*> smallList genRule-        <*> smallList genNode+{-# LANGUAGE ImportQualifiedPost #-}++{- | Test driver for keiro-dsl. EP-1 milestone 1 tests: the @parse . pretty@+round-trip property over generated specs, and a unit test pinning the shape+of the canonical Reservation fixture.+-}+module Main (main) where++import Control.Exception (bracket)+import Control.Monad (filterM, forM, forM_, unless)+import Data.Aeson (Value, object, (.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Either (isLeft, isRight)+import Data.List (partition, sort)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text qualified as T+import Data.Text.IO qualified as TIO+import Keiro.Codec (Codec (..), EventType (..), decodeRaw)+import Keiro.Dsl.CodecCompare+import Keiro.Dsl.Coverage qualified as Coverage+import Keiro.Dsl.Diff (Change (..), ChangeKind (..), CompatibilitySurface (..), CompatibilityVector (..), FamilyDiff (..), Label (..), NodeFamily, RolloutConstraint (..), SurfaceVerdict (..), defaultGate, deriveLabel, diffSpecs, familyRegistry, gateWith, gatedBreaking, isAdvisory, isBreaking, verdictFor)+import Keiro.Dsl.DiffReport (diffReport, parseSurfaceName, remediationFor, renderExplainBlock, renderFinding)+import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligation (..), BindingObligationKind (..), bindingObligations, renderBindingObligations)+import Keiro.Dsl.FoldFingerprint (aggregateFoldFingerprint, aggregateFoldSurface)+import Keiro.Dsl.Goldens (GoldenEvidence (..), GoldenPayload (..), emitGoldenPayloads, goldenRelativePath, goldensForDiff)+import Keiro.Dsl.Grammar+import Keiro.Dsl.Harness (harnessFor, harnessForWithGoldens, harnessReadModel, harnessRouter, harnessWorkflow)+import Keiro.Dsl.Manifest (manifestDependencies, moduleNameOf, renderManifest)+import Keiro.Dsl.MappedConsumer (ConsumerPlan (..))+import Keiro.Dsl.Parser (parseSpec)+import Keiro.Dsl.PrettyPrint (renderSpec, renderTransition)+import Keiro.Dsl.ReadModelShape (canonicalShape, deriveShapeHash, registryNameFor, subscriptionNameFor)+import Keiro.Dsl.ReplayImpact (AggregateImpact (..), ReplayImpact (..))+import Keiro.Dsl.ReplayImpact qualified as ReplayImpact+import Keiro.Dsl.Scaffold (Context (..), ModuleKind (..), ScaffoldModule (..), codecComparisonBanner, codecComparisonModule, defaultContext, firewallBreaches, genPrefixFor, holePrefixFor, scaffoldAggregate, scaffoldIntake, scaffoldProcess, scaffoldPublisher, scaffoldReadModel, scaffoldRefusals, scaffoldReplayAudit, scaffoldRouter, scaffoldWorkqueue, windowSeconds)+import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName)+import Keiro.Dsl.ScaffoldRun (MappingDrift (..), Refusal (..), ScaffoldReport (..), StaleModule (..), WriteDisposition (..), executeScaffold, planScaffold, renderRefusals, renderScaffoldReport, scaffoldModules)+import Keiro.Dsl.Skeleton (skeletonFor, skeletonKinds)+import Keiro.Dsl.TypeGraph+import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), derivedQueueTrio, validateSpec)+import System.Directory (createDirectory, createDirectoryIfMissing, doesFileExist, getTemporaryDirectory, removeFile, removePathForcibly)+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.FilePath (takeDirectory, (</>))+import System.IO (hClose, openTempFile)+import System.Process (readProcessWithExitCode)+import Test.Hspec hiding (Spec)+import Test.QuickCheck++main :: IO ()+main = hspec $ do+    describe "historical codec comparison" $ do+        it "treats object-key order as RFC 8785 parity" $ do+            let historical = object ["z" .= (1 :: Int), "a" .= (2 :: Int)]+                generated = object ["a" .= (2 :: Int), "z" .= (1 :: Int)]+            classifyObservation (EncodeObservation "ordered-object" historical generated)+                `shouldBe` Right JsonParity+        it "classifies an omitted key versus explicit null as version work at that pointer" $ do+            let historical = object []+                generated = object ["description" .= Aeson.Null]+            classifyObservation (EncodeObservation "absent-description" historical generated)+                `shouldBe` Right (RequiresVersionWork (EncodedValueDifference (JsonPointer "/description") historical generated))+        it "classifies generated rejection of a historical value as version work" $+            classifyObservation+                ( DecodeObservation+                    "legacy.json"+                    (object ["tag" .= ("legacy" :: T.Text)])+                    (DecodedShape (object ["tag" .= ("legacy" :: T.Text)]))+                    (DecodeFailed "unknown tag")+                )+                `shouldBe` Right (RequiresVersionWork (GeneratedDecodeRejected "unknown tag"))+        it "treats historical-codec rejection as invalid input rather than parity" $+            classifyObservation+                ( DecodeObservation+                    "corrupt.json"+                    Aeson.Null+                    (DecodeFailed "not historical data")+                    (DecodeFailed "not generated data")+                )+                `shouldBe` Left (HistoricalCodecRejected "corrupt.json" "not historical data")+        it "reports uncovered union arms separately by corpus origin" $ do+            let canonical = DeclaredBranch HistoricalGolden (JsonPointer "/location") (UnionArm "canonical")+                local = DeclaredBranch HistoricalGolden (JsonPointer "/location") (UnionArm "local_file")+                report = compareReport comparisonProvenance [] [] [canonical, local] [ObservedBranch HistoricalGolden (JsonPointer "/location") (UnionArm "local_file")]+            crCoverageGaps report+                `shouldBe` [CoverageGap HistoricalGolden (JsonPointer "/location") (UnionArm "canonical")]+            reportSucceeded report `shouldBe` False+        it "derives optional, null, and union-arm observations from a generated branch schema" $ do+            let schema =+                    BranchRecord+                        [ BranchField "description" True (BranchOptional BranchScalar)+                        , BranchField "location" False (BranchUnion "tag" "contents" [BranchArm "local" (Just BranchScalar), BranchArm "canonical" Nothing])+                        ]+                historical = object ["location" .= object ["tag" .= ("canonical" :: T.Text)]]+            observedBranchesFor HistoricalGolden schema historical+                `shouldBe` [ ObservedBranch HistoricalGolden (JsonPointer "/description") OptionalMissing+                           , ObservedBranch HistoricalGolden (JsonPointer "/location") (UnionArm "canonical")+                           ]+            let declared = declaredBranchesFor HistoricalGolden schema+            forM_+                [ DeclaredBranch HistoricalGolden (JsonPointer "/description") OptionalMissing+                , DeclaredBranch HistoricalGolden (JsonPointer "/description") OptionalPresent+                , DeclaredBranch HistoricalGolden (JsonPointer "/description") ExplicitNull+                , DeclaredBranch HistoricalGolden (JsonPointer "/location") (UnionArm "local")+                , DeclaredBranch HistoricalGolden (JsonPointer "/location") (UnionArm "canonical")+                ]+                (\branch -> declared `shouldContain` [branch])+        it "round-trips the stable machine report" $ do+            let observation = EncodeObservation "parity" (object ["a" .= (1 :: Int)]) (object ["a" .= (1 :: Int)])+                report = compareReport comparisonProvenance [] [observation] [] []+            Aeson.eitherDecode (Aeson.encode report) `shouldBe` Right report+        it "atomically writes and replaces the machine report" $+            withTempDirectory "keiro-dsl-codec-compare" $ \out -> do+                let path = out </> "report.json"+                    firstReport = compareReport comparisonProvenance [] [] [] []+                    secondReport = compareReport comparisonProvenance [HistoricalGoldenUnreadable "bad.json" "bad JSON"] [] [] []+                writeCompareReportAtomic path firstReport `shouldReturn` Right ()+                Aeson.eitherDecodeFileStrict path `shouldReturn` Right firstReport+                writeCompareReportAtomic path secondReport `shouldReturn` Right ()+                Aeson.eitherDecodeFileStrict path `shouldReturn` Right secondReport++    describe "historical codec comparison scaffold" $ do+        it "emits an opt-in non-production runner without entering the ordinary module registry" $ do+            spec <- specOf "test/fixtures/structural-conformance.keiro"+            let ctx = defaultContext (specContext spec)+                planned = codecComparisonModule ctx spec "ArtifactInfo"+                ordinary = scaffoldModules ctx spec+            case planned of+                Left err -> expectationFailure (T.unpack err)+                Right comparisonModule -> do+                    modulePath comparisonModule+                        `shouldBe` "Generated/StructuralConformance/Structural/CodecCompare/ArtifactInfo.hs"+                    moduleText comparisonModule `shouldSatisfy` T.isInfixOf codecComparisonBanner+                    moduleText comparisonModule `shouldSatisfy` T.isInfixOf "Generated.StructuralConformance.ArtifactCatalog.Codec qualified as GeneratedCodec"+                    moduleText comparisonModule `shouldSatisfy` T.isInfixOf "branchSchema = BranchRecord"+                    map modulePath ordinary `shouldNotContain` [modulePath comparisonModule]+        it "refuses opaque selections rather than upgrading their claim" $ do+            spec <- specOf "test/fixtures/structural-conformance.keiro"+            codecComparisonModule (defaultContext (specContext spec)) spec "VendorGeometry"+                `shouldSatisfy` either (T.isInfixOf "is opaque") (const False)++    describe "structural/opaque coverage reporting" $ do+        it "reports mapped private-event roots and consumer-json register boundaries without a percentage" $ do+            spec <- specOf "test/fixtures/structural-conformance.keiro"+            report <- shouldResolveCoverage "structural-conformance.keiro" spec+            Coverage.privateEventPayloads (Coverage.coverageSummary report)+                `shouldBe` Coverage.CoverageCounts 2 1 1 0+            Coverage.snapshotRegisters (Coverage.coverageSummary report)+                `shouldBe` Coverage.CoverageCounts 2 1 1 0+            map Coverage.opaqueMappedType (Coverage.coverageOpaqueBoundaries report)+                `shouldBe` ["VendorGeometry"]+            map Coverage.snapshotEncoding (Coverage.coverageSnapshotBoundaries report)+                `shouldBe` ["consumer-json-cache", "consumer-json-cache"]+            map Coverage.snapshotInvalidation (Coverage.coverageSnapshotBoundaries report)+                `shouldBe` ["tracked-by-mapped-wire-fingerprint", "tracked-by-mapped-wire-fingerprint"]+            map Coverage.findingCode (Coverage.coverageFindings report)+                `shouldBe` [CoverageOpaqueSurface]+            map Coverage.findingSeverity (Coverage.coverageFindings report)+                `shouldBe` [Warning]+            case Aeson.toJSON report of+                Aeson.Object values ->+                    forM_ ["spec", "roots", "opaqueBoundaries", "snapshotBoundaries", "unsupportedSurfaces"] $+                        \key -> KeyMap.member key values `shouldBe` True+                value -> expectationFailure ("coverage report was not an object: " <> show value)+        it "reports explicit Json leaves by their complete persisted path" $ do+            spec <- withMetadataJson <$> specOf "test/fixtures/structural-conformance.keiro"+            report <- shouldResolveCoverage "structural-conformance-json.keiro" spec+            Coverage.jsonBoundaries (Coverage.privateEventPayloads (Coverage.coverageSummary report))+                `shouldBe` 1+            map Coverage.jsonPath (Coverage.coverageJsonBoundaries report)+                `shouldBe` ["ArtifactCatalog event ArtifactRecorded .artifact : ArtifactInfo .metadata : ArtifactMetadata .note"]+        it "keeps a zero-opaque spec advisory-free and makes rejection explicitly opt-in" $ do+            original <- specOf "test/fixtures/structural-conformance.keiro"+            clear <- shouldResolveCoverage "structural-only.keiro" (withoutVendorGeometry original)+            Coverage.opaqueRoots (Coverage.privateEventPayloads (Coverage.coverageSummary clear)) `shouldBe` 0+            Coverage.coverageOpaqueBoundaries clear `shouldBe` []+            Coverage.coverageFindings clear `shouldBe` []+            opaque <- shouldResolveCoverage "structural-conformance.keiro" original+            Coverage.coverageSucceeded opaque `shouldBe` True+            let gated = Coverage.failOnOpaque opaque+            Coverage.coverageSucceeded gated `shouldBe` False+            map Coverage.findingCode (Coverage.coverageFindings gated)+                `shouldBe` [CoverageOpaqueSurface, CoverageOpaqueGateExceeded]+            map Coverage.findingSeverity (Coverage.coverageFindings gated)+                `shouldBe` [Warning, Error]+        it "diffs named opaque boundaries and fails only an explicitly gated increase" $ do+            newSpec <- specOf "test/fixtures/structural-conformance.keiro"+            report <- case Coverage.coverageDiffReport "structural-conformance.keiro" "HEAD" (withoutVendorGeometry newSpec) newSpec of+                Left err -> expectationFailure (show err) >> fail "unreachable"+                Right value -> pure value+            fmap Coverage.opaqueBoundaryDelta (Coverage.coverageDelta report) `shouldBe` Just 1+            fmap (map Coverage.opaqueMappedType . Coverage.addedOpaqueBoundaries) (Coverage.coverageDelta report)+                `shouldBe` Just ["VendorGeometry"]+            map Coverage.findingCode (Coverage.coverageFindings report)+                `shouldBe` [CoverageOpaqueSurface, CoverageOpaqueBoundaryAdded]+            Coverage.coverageSucceeded report `shouldBe` True+            let gated = Coverage.failOnOpaqueIncrease report+            Coverage.coverageSucceeded gated `shouldBe` False+            map Coverage.findingCode (Coverage.coverageFindings gated)+                `shouldBe` [CoverageOpaqueSurface, CoverageOpaqueBoundaryAdded, CoverageOpaqueGateExceeded]+        it "appends the six stable coverage and comparison registry codes" $+            map+                show+                [ CoverageOpaqueSurface+                , CoverageOpaqueBoundaryAdded+                , CoverageOpaqueGateExceeded+                , CodecCompareDifference+                , CodecCompareCoverageGap+                , CodecCompareInvalidInput+                ]+                `shouldBe` [ "CoverageOpaqueSurface"+                           , "CoverageOpaqueBoundaryAdded"+                           , "CoverageOpaqueGateExceeded"+                           , "CodecCompareDifference"+                           , "CodecCompareCoverageGap"+                           , "CodecCompareInvalidInput"+                           ]++    describe "parse . pretty round-trip" $+        do+            it "re-parses any generated spec to an equal AST (modulo source locations)" $+                checkCoverage $+                    forAll genSpec $ \s ->+                        let families = map nodeTag (specNodes s)+                            roundTrip = parseSpec "<gen>" (renderSpec s) === Right s+                         in cover 5 (not (null (specMapped s))) "mapped" $+                                foldr (\family -> cover 1 (family `elem` families) family) roundTrip allNodeTags+            it "round-trips an aggregate with no states" $+                parseSpec "<empty-states>" (renderSpec emptyStatesSpec) `shouldBe` Right emptyStatesSpec+            it "separates transition emit clauses from following nodes" $ do+                spec <- parseInlineSpec "<cross-family-boundaries>" crossFamilyBoundarySpec+                case specNodes spec of+                    [NAggregate first, NEmit _, NAggregate second, NPgmqDispatch _] -> do+                        concatMap tEmits (aggTransitions first) `shouldBe` ["Changed"]+                        aggStates second `shouldBe` []+                    nodes -> expectationFailure ("unexpected node sequence: " <> show (map nodeTag nodes))++    describe "mapped types (EP-149)" $ do+        it "round-trips the canonical structural and opaque consumer fixture" $ do+            source <- TIO.readFile "test/fixtures/consumer-types.keiro"+            spec <- parseInlineSpec "test/fixtures/consumer-types.keiro" source+            parseSpec "<consumer-types-round-trip>" (renderSpec spec) `shouldBe` Right spec+            length (specMapped spec) `shouldBe` 4+        it "preserves every missing-value policy, nested type expression, and unit union arm" $ do+            source <- TIO.readFile "test/fixtures/consumer-types.keiro"+            spec <- parseInlineSpec "test/fixtures/consumer-types.keiro" source+            let fields = [field | MappedStructural{msShape = ShapeRecord _ _ recordFields} <- specMapped spec, field <- recordFields]+                arms = [arm | MappedStructural{msShape = ShapeUnion _ unionArms} <- specMapped spec, arm <- unionArms]+            [value | field <- fields, Just value <- [wfOnMissing field]]+                `shouldBe` [OmCtor "Guide", OmNull, OmInt 0, OmBool False, OmEmptyList, OmEmptyMap]+            [wfType field | field <- fields, wfHaskell field == "labels"]+                `shouldBe` [TList (TOptional TText)]+            [waCtor arm | arm <- arms, waPayload arm == Nothing]+                `shouldBe` ["Unknown"]+        it "rejects every mapped validation fixture with its stable diagnostic code" $ do+            let cases =+                    [ ("mapped-unresolved.keiro", MappedUnresolvedName)+                    , ("mapped-ambiguous.keiro", MappedAmbiguousName)+                    , ("mapped-dup-fieldname.keiro", MappedDuplicateFieldName)+                    , ("mapped-dup-wirekey.keiro", MappedDuplicateWireKey)+                    , ("mapped-dup-armname.keiro", MappedDuplicateArmName)+                    , ("mapped-dup-tag.keiro", MappedDuplicateWireTag)+                    , ("mapped-recursive.keiro", MappedRecursiveType)+                    , ("mapped-recursive-mutual.keiro", MappedRecursiveType)+                    , ("mapped-bad-encoding.keiro", MappedUnsupportedEncoding)+                    , ("mapped-union-key-collision.keiro", MappedUnsupportedEncoding)+                    , ("mapped-optional-json.keiro", MappedNonInjectiveNullability)+                    , ("mapped-optional-optional.keiro", MappedNonInjectiveNullability)+                    , ("mapped-optional-opaque.keiro", MappedNonInjectiveNullability)+                    , ("mapped-missing-binding.keiro", MappedMissingIngredient)+                    , ("mapped-missing-binding-version.keiro", MappedMissingIngredient)+                    , ("mapped-missing-canonical.keiro", MappedMissingIngredient)+                    , ("mapped-missing-fixture.keiro", MappedMissingIngredient)+                    , ("mapped-missing-initial.keiro", MappedMissingInitialValue)+                    , ("mapped-bad-haskell-name.keiro", MappedInvalidHaskellName)+                    , ("mapped-empty-identity.keiro", MappedInvalidIdentity)+                    , ("mapped-import-conflict.keiro", MappedImportConflict)+                    , ("mapped-illtyped-default.keiro", MappedDefaultIllTyped)+                    , ("mapped-guard.keiro", MappedGuardUnsupported)+                    , ("mapped-guard-natural.keiro", MappedGuardUnsupported)+                    ]+            forM_ cases $ \(fixture, expected) ->+                errorCodesOf ("test/fixtures/" <> fixture) `shouldReturn` [expected]+        it "keeps Time in Keiki's curated guard set while rejecting Natural" $+            errorCodesOf "test/fixtures/mapped-guard-time.keiro" `shouldReturn` []+        it "rejects required defaults, missing optional policies, Int overflow, and negative Natural defaults" $ do+            let invalidFields =+                    [ WireField "requiredDefault" "requiredDefault" TText PRequired (Just (OmText "x")) noLoc+                    , WireField "missingPolicy" "missingPolicy" TText POptional Nothing noLoc+                    , WireField "overflow" "overflow" TInt POptional (Just (OmInt (toInteger (maxBound :: Int) + 1))) noLoc+                    , WireField "negativeNatural" "negativeNatural" TNatural POptional (Just (OmInt (-1))) noLoc+                    ]+                declaration = completeStructural "Defaults" (ShapeRecord "Defaults" RejectUnknown invalidFields)+            errorCodes (mappedSpec [declaration])+                `shouldBe` [MappedDefaultIllTyped, MappedMissingIngredient, MappedDefaultIllTyped, MappedDefaultIllTyped]++    describe "mapped type graph (EP-149)" $ do+        it "resolves checked declarations, transitive reachability, and every aggregate root path" $ do+            source <- TIO.readFile "test/fixtures/consumer-types.keiro"+            spec <- parseInlineSpec "test/fixtures/consumer-types.keiro" source+            graph <- shouldResolveTypeGraph spec+            Map.size (tgDeclarations graph) `shouldBe` 4+            Map.lookup (MappedKey "ArtifactInfo") (tgReachability graph)+                `shouldBe` Just (Set.fromList [MappedKey "ArtifactKind", MappedKey "ArtifactLocation"])+            map renderUsePath (usePaths graph "ArtifactLocation")+                `shouldBe` [ "Catalog command ObserveArtifact .artifact : ArtifactInfo .location : ArtifactLocation"+                           , "Catalog event ArtifactObserved .artifact : ArtifactInfo .location : ArtifactLocation"+                           , "Catalog register currentArtifact : ArtifactInfo .location : ArtifactLocation"+                           ]+        it "resolves every builtin through the complete expression algebra" $ do+            source <- TIO.readFile "test/fixtures/consumer-types.keiro"+            spec <- parseInlineSpec "test/fixtures/consumer-types.keiro" source+            graph <- shouldResolveTypeGraph spec+            case Map.lookup (MappedKey "ArtifactInfo") (tgDeclarations graph) of+                Just (ResolvedStructural _ (RRecord _ _ fields)) ->+                    Set.fromList (concatMap (foldTypeExpr expressionTags . rwfType) fields)+                        `shouldBe` Set.fromList ["text", "int", "bool", "natural", "time", "json", "optional", "list", "map", "ref:ArtifactKind", "ref:ArtifactLocation"]+                declaration -> expectationFailure ("unexpected ArtifactInfo declaration: " <> show declaration)+        it "rejects direct, mutual, wrapped, and union-arm recursion" $ do+            let direct = mappedSpec [completeStructural "A" (recordShape [TRef "A"])]+                mutual = mappedSpec [completeStructural "A" (recordShape [TRef "B"]), completeStructural "B" (recordShape [TRef "A"])]+                wrapped = mappedSpec [completeStructural "A" (recordShape [TList (TOptional (TRef "A"))])]+                throughArm = mappedSpec [completeStructural "A" (ShapeUnion (TaggedObject "tag" "contents" RejectUnknown) [WireArm "Again" "again" (Just (TRef "A")) noLoc])]+            map (hasTypeGraphError isRecursive . resolveTypeGraph) [direct, mutual, wrapped, throughArm]+                `shouldBe` replicate 4 True+        it "keeps existing ids and enums outside the mapped-reference namespace" $ do+            let spec =+                    (mappedSpec [completeStructural "A" (recordShape [TRef "ExistingId"])])+                        { specIds = [IdDecl "ExistingId" "id" noLoc]+                        }+            resolveTypeGraph spec `shouldSatisfy` hasTypeGraphError isUnresolved+        it "fingerprints wire identity while ignoring Haskell selector names" $ do+            source <- TIO.readFile "test/fixtures/consumer-types.keiro"+            base <- parseInlineSpec "test/fixtures/consumer-types.keiro" source+            baseGraph <- shouldResolveTypeGraph base+            haskellRenameGraph <- shouldResolveTypeGraph (mapArtifactField (\field -> field{wfHaskell = "renamedKey"}) base)+            wireRenameGraph <- shouldResolveTypeGraph (mapArtifactField (\field -> field{wfKey = "renamed_key"}) base)+            wireFingerprint haskellRenameGraph "ArtifactInfo" `shouldBe` wireFingerprint baseGraph "ArtifactInfo"+            wireFingerprint wireRenameGraph "ArtifactInfo" `shouldNotBe` wireFingerprint baseGraph "ArtifactInfo"++    describe "string literal integrity" $ do+        it "parses an escaped emit-map value as exactly one row" $ do+            let src =+                    T.unlines+                        [ "context svc"+                        , ""+                        , "emit e {"+                        , "  contract c"+                        , "  topic events"+                        , "  source \"svc\""+                        , "  key thingId"+                        , "  map status {"+                        , "    \"a\\\" => Wat \\\"b\" => ThingAccepted"+                        , "    _ => skip"+                        , "  }"+                        , "  messageId derive hole"+                        , "  idempotencyKey derive hole"+                        , "}"+                        ]+            case parseSpec "<escaped-map>" src of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> case [row | NEmit e <- specNodes spec, row <- emMap e] of+                    [row] -> do+                        emrValue row `shouldBe` "a\" => Wat \"b"+                        emrEvent row `shouldBe` "ThingAccepted"+                    rows -> expectationFailure ("expected one emit-map row, got " <> show (length rows))+        it "rejects a raw newline inside a quoted string" $ do+            let src = "context svc\n\ncontract c {\n  schemaVersion 1\n  discriminator kind\n  topic events \"first\nsecond\"\n}\n"+            parseSpec "<raw-newline>" src `shouldSatisfy` leftContains "unescaped newline"+        it "rejects an unknown escape sequence" $ do+            let src = "context svc\n\ncontract c {\n  schemaVersion 1\n  discriminator kind\n  topic events \"bad\\q\"\n}\n"+            parseSpec "<unknown-escape>" src `shouldSatisfy` leftContains "unknown escape"+        it "round-trips adversarial text through topics, emit maps, and quoted bindings" $+            property $+                forAll genAdversarialText $ \t ->+                    let spec = escapedSpec t+                        rendered = renderSpec spec+                     in counterexample (T.unpack rendered) (parseSpec "<escaped-round-trip>" rendered === Right spec)++    describe "partial status maps" $ do+        it "suppresses totality only when the partial marker is present" $ do+            partial <- parseInlineSpec "<partial-status-map>" (statusMapSpec " partial")+            totalSpec <- parseInlineSpec "<total-status-map>" (statusMapSpec "")+            map code (validateSpec partial) `shouldNotContain` [StatusMapNotTotal]+            map code (validateSpec totalSpec) `shouldContain` [StatusMapNotTotal]+            parseSpec "<partial-round-trip>" (renderSpec partial) `shouldBe` Right partial++    describe "positioned parser diagnostics" $ do+        it "rejects a duplicate goto at the second clause" $ do+            err <- parseErrorOf "<duplicate-goto>" duplicateGotoSpec+            err `shouldSatisfy` T.isInfixOf "duplicate goto"+            err `shouldSatisfy` T.isInfixOf "<duplicate-goto>:10:"+        it "rejects duplicate wire and projection blocks at their second occurrences" $ do+            wireErr <- parseErrorOf "<duplicate-wire>" duplicateWireSpec+            wireErr `shouldSatisfy` T.isInfixOf "duplicate wire block"+            wireErr `shouldSatisfy` T.isInfixOf "<duplicate-wire>:8:"+            projectionErr <- parseErrorOf "<duplicate-projection>" duplicateProjectionSpec+            projectionErr `shouldSatisfy` T.isInfixOf "duplicate projection block"+            projectionErr `shouldSatisfy` T.isInfixOf "<duplicate-projection>:9:"+        it "anchors a missing goto on the transition line" $ do+            err <- parseErrorOf "<missing-goto>" missingGotoSpec+            err `shouldSatisfy` T.isInfixOf "missing a goto clause"+            err `shouldSatisfy` T.isInfixOf "<missing-goto>:8:"+        it "stops before a misplaced dispatch-id and expects schedule at its start" $ do+            let src = misplacedDispatchIdSpec+                expectedPosition =+                    "<misplaced-dispatch-id>:"+                        <> T.pack (show (lineNumberContaining "dispatch-id" src))+                        <> ":5:"+            err <- parseErrorOf "<misplaced-dispatch-id>" src+            err `shouldSatisfy` T.isInfixOf "schedule"+            err `shouldSatisfy` T.isInfixOf expectedPosition+        it "keeps a malformed register declaration's equals error" $ do+            err <- parseErrorOf "<malformed-register>" malformedRegisterSpec+            err `shouldSatisfy` T.isInfixOf "expecting '='"++    describe "bounded decimal literals" $ do+        forM_ decimalOverflowSpecs $ \(site, src) ->+            it ("rejects overflow at " <> site) $ do+                err <- parseErrorOf ("<overflow-" <> site <> ">") src+                err `shouldSatisfy` T.isInfixOf ("decimal literal " <> decimalOverflow <> " is out of range")+        it "accepts maxBound without changing its value" $ do+            spec <- parseInlineSpec "<max-bound>" (wireDecimalSpec (T.pack (show (maxBound :: Int))))+            [wireSchemaVersion wire | NAggregate aggregate <- specNodes spec, Just wire <- [aggWire aggregate]]+                `shouldBe` [maxBound]++    describe "identifier hygiene" $ do+        it "reports constructor shape and Haskell keywords at their owning declarations" $ do+            spec <- parseInlineSpec "<identifier-hygiene>" identifierHygieneSpec+            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic `elem` [IdentNotConstructorSafe, IdentHaskellKeyword]]+                `shouldContain` [(IdentNotConstructorSafe, 3), (IdentHaskellKeyword, 7)]+        it "rejects generated vertex constructors that collide with event constructors" $ do+            spec <- parseInlineSpec "<vertex-collision>" vertexCollisionSpec+            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic == VertexCtorCollision]+                `shouldBe` [(VertexCtorCollision, 3)]+        it "rejects underscore-leading names whose title-casing cannot make a module segment" $ do+            spec <- parseInlineSpec "<underscore-node>" underscoreNodeSpec+            [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic == IdentNotConstructorSafe]+                `shouldBe` [(IdentNotConstructorSafe, 3)]+        it "rejects non-ASCII identifier characters in the parser" $+            parseSpec "<unicode-identifier>" unicodeIdentifierSpec `shouldSatisfy` leftContains "unexpected"++    describe "canonical reservation.keiro" $+        it "parses into the expected aggregate shape" $ do+            input <- readTestText "test/fixtures/reservation.keiro"+            case parseSpec "test/fixtures/reservation.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> do+                    specContext spec `shouldBe` "hospital-capacity"+                    length (specIds spec) `shouldBe` 3+                    length (specEnums spec) `shouldBe` 3+                    length (specRules spec) `shouldBe` 1+                    case specNodes spec of+                        [NAggregate a] -> do+                            aggName a `shouldBe` "Reservation"+                            length (aggStates a) `shouldBe` 6+                            length (aggCommands a) `shouldBe` 2+                            length (aggEvents a) `shouldBe` 2+                            length (aggTransitions a) `shouldBe` 2+                            map stTerminal (aggStates a) `shouldBe` [False, False, False, True, True, True]+                        other -> expectationFailure ("expected one aggregate node, got " <> show (length other))++    describe "validator" $ do+        it "accepts the canonical reservation.keiro" $ do+            codes <- errorCodesOf "test/fixtures/reservation.keiro"+            codes `shouldBe` []+        it "rejects a missing status-map as StatusMapNotTotal" $ do+            codes <- diagnosticCodesOf "test/fixtures/reservation-no-statusmap.keiro"+            codes `shouldContain` [StatusMapNotTotal]+        it "rejects an undeclared command as UndeclaredCommand" $ do+            codes <- diagnosticCodesOf "test/fixtures/reservation-bad-command.keiro"+            codes `shouldContain` [UndeclaredCommand]+        it "rejects a wall-clock guard atom as ClockSampled" $ do+            codes <- diagnosticCodesOf "test/fixtures/reservation-clock.keiro"+            codes `shouldContain` [ClockSampled]+        it "accepts a v2 event with a contiguous upcaster hole" $ do+            codes <- errorCodesOf "test/fixtures/reservation-v2.keiro"+            codes `shouldBe` []+        it "rejects a v2 event with no upcaster as EvtVersionMissingUpcaster" $ do+            codes <- diagnosticCodesOf "test/fixtures/reservation-v2-noupcast.keiro"+            codes `shouldContain` [EvtVersionMissingUpcaster]+        it "accepts shared upcaster sources for different event kinds" $ do+            codes <- errorCodesOf "test/fixtures/reservation-dup-upcast-source.keiro"+            codes `shouldNotContain` [DuplicateUpcasterSource]+        it "rejects a gap in the aggregate-global upcaster chain" $ do+            codes <- errorCodesOf "test/fixtures/reservation-chain-gap.keiro"+            codes `shouldContain` [UpcasterChainGap]+        it "warns while a retiring event keeps its live emitting transition" $ do+            diagnostics <- diagnosticsOf "test/fixtures/reservation-retiring.keiro"+            [code d | d <- diagnostics, severity d == Error] `shouldBe` []+            [code d | d <- diagnostics, severity d == Warning]+                `shouldContain` [EventRetirementInProgress]+        it "rejects a retiring event after its live emitting transition disappears" $ do+            source <- readTestText "test/fixtures/reservation-retiring.keiro"+            spec <- parseInlineSpec "<retiring-without-emitter>" (T.replace " ; emit TransferReservationConfirmed" "" source)+            [code d | d <- validateSpec spec, severity d == Error]+                `shouldContain` [EventRetirementInProgress]+        it "warns when a deprecated event has no replay-only emitting transition" $ do+            diagnostics <- diagnosticsOf "test/fixtures/reservation-deprecated.keiro"+            [code d | d <- diagnostics, severity d == Error] `shouldBe` []+            [code d | d <- diagnostics, severity d == Warning]+                `shouldContain` [DeprecatedEventReplayHazard]+        it "recognises deprecated plus replay-only as the replay-safe cutover" $ do+            diagnostics <- diagnosticsOf "test/fixtures/reservation-deprecated-replay-only.keiro"+            [code d | d <- diagnostics, severity d == Error] `shouldBe` []+            [code d | d <- diagnostics, severity d == Warning]+                `shouldContain` [EventRetirementInProgress]+            [code d | d <- diagnostics] `shouldNotContain` [DeprecatedEventReplayHazard]+        it "requires exact, unique status-map event keys" $ do+            dangling <- errorCodesOf "test/fixtures/statusmap-dangling.keiro"+            mapM_ (\expected -> dangling `shouldContain` [expected]) [StatusMapDanglingKey, StatusMapNotTotal]+            duplicate <- errorCodesOf "test/fixtures/statusmap-dup-key.keiro"+            duplicate `shouldContain` [StatusMapDuplicateKey]+        it "rejects duplicate spec and aggregate names" $ do+            codes <- errorCodesOf "test/fixtures/duplicate-names.keiro"+            mapM_+                (\expected -> codes `shouldContain` [expected])+                [ DuplicateNodeName+                , DuplicateEnumCtor+                , DuplicateEnumWire+                , DuplicateIdPrefix+                , DuplicateCommandName+                , DuplicateEventName+                ]+        it "rejects aggregate-local references that do not resolve" $ do+            codes <- errorCodesOf "test/fixtures/aggregate-bad-refs.keiro"+            codes `shouldContain` [RegisterInitialOutOfScope, UndeclaredCommand, WriteTargetNotRegister]+        it "anchors UnreachableState on the state row" $ do+            let src =+                    T.unlines+                        [ "context repro"+                        , ""+                        , "aggregate Thing"+                        , "  regs"+                        , "  states"+                        , "    Initial"+                        , "    Unreachable"+                        ]+            case parseSpec "<unreachable-row>" src of+                Left err -> expectationFailure (T.unpack err)+                Right spec ->+                    [line d | d <- validateSpec spec, code d == UnreachableState]+                        `shouldBe` [7]+        it "accepts a replay-only twin with a live sibling (plan 143)" $ do+            codes <- errorCodesOf "test/fixtures/reservation-guard-tightened-twin.keiro"+            codes `shouldBe` []+        it "rejects a replay-only transition that emits nothing" $ do+            case parseSpec "<replay-only-no-emit>" (replayOnlySpecWith ["    write reservationState := Held", "    goto  Held"]) of+                Left err -> expectationFailure (T.unpack err)+                Right spec ->+                    [code d | d <- validateSpec spec, severity d == Error]+                        `shouldContain` [ReplayOnlyEmitsNothing]+        it "warns when a replay-only transition has no live sibling" $ do+            case parseSpec "<replay-only-orphan>" (replayOnlySpecWith ["    emit  TransferReservationCreated", "    goto  Held"]) of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> do+                    [code d | d <- validateSpec spec, severity d == Warning]+                        `shouldContain` [ReplayOnlyCommandStillLive]+                    [code d | d <- validateSpec spec, severity d == Error]+                        `shouldNotContain` [ReplayOnlyCommandStillLive]++    describe "complementExpr (plan 143)" $ do+        it "applies De Morgan over and/or and flips comparison operators" $ do+            let a = EAtom (AName "a")+                b = EAtom (AName "b")+            complementExpr (EAnd a b)+                `shouldBe` EOr (ECmp OpEq a (EAtom (ABool False))) (ECmp OpEq b (EAtom (ABool False)))+            complementExpr (ECmp OpLt a b) `shouldBe` ECmp OpGe a b+            complementExpr (ECmp OpEq a b) `shouldBe` ECmp OpNeq a b+            complementExpr (ECmp OpLe a b) `shouldBe` ECmp OpGt a b+            complementExpr (ECmp OpGt a b) `shouldBe` ECmp OpLe a b+            complementExpr (ECmp OpGe a b) `shouldBe` ECmp OpLt a b+            complementExpr (ECmp OpNeq a b) `shouldBe` ECmp OpEq a b+        it "flips boolean literals and grounds bare names as == false" $ do+            complementExpr (EAtom (ABool True)) `shouldBe` EAtom (ABool False)+            complementExpr (EAtom (AName "open"))+                `shouldBe` ECmp OpEq (EAtom (AName "open")) (EAtom (ABool False))+        it "stays inside the grammar: the complement of any guard re-parses" $+            property $+                forAll genExpr $ \e ->+                    let twin =+                            replayOnlySpecWith+                                [ "    guard " <> renderExprText (complementExpr e)+                                , "    emit  TransferReservationCreated"+                                , "    goto  Held"+                                ]+                     in case parseSpec "<complement>" twin of+                            Left err -> counterexample (T.unpack err) False+                            Right spec ->+                                [tGuard t | NAggregate a <- specNodes spec, t <- aggTransitions a]+                                    === [Just (complementExpr e)]++    describe "evolution parsing" $ do+        it "parses event version and upcaster from reservation-v2.keiro" $ do+            input <- readTestText "test/fixtures/reservation-v2.keiro"+            case parseSpec "test/fixtures/reservation-v2.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> case [e | NAggregate a <- specNodes spec, e <- aggEvents a, evName e == "TransferReservationCreated"] of+                    (e : _) -> do+                        evVersion e `shouldBe` 2+                        evUpcastFrom e `shouldBe` Just (1, Hole)+                    [] -> expectationFailure "TransferReservationCreated not found"+        it "round-trips the retiring marker" $ do+            spec <- specOf "test/fixtures/reservation-retiring.keiro"+            parseSpec "<retiring-round-trip>" (renderSpec spec) `shouldBe` Right spec+            [evRetiring event | NAggregate aggregate <- specNodes spec, event <- aggEvents aggregate, evName event == "TransferReservationConfirmed"]+                `shouldBe` [True]+        it "rejects an event marked both retiring and deprecated" $ do+            source <- readTestText "test/fixtures/reservation-retiring.keiro"+            let conflicting = T.replace "retiring event TransferReservationConfirmed" "retiring deprecated event TransferReservationConfirmed" source+            parseSpec "<conflicting-retirement-markers>" conflicting `shouldSatisfy` isLeft++    describe "aggregate snapshots (EP-109)" $ do+        it "parses, validates, and round-trips a snapshot policy with codec fixture" $ do+            spec <- specOf "test/fixtures/reservation-snapshot.keiro"+            errorCodesOf "test/fixtures/reservation-snapshot.keiro" `shouldReturn` []+            parseSpec "<snapshot-round-trip>" (renderSpec spec) `shouldBe` Right spec+            case [aggregate | NAggregate aggregate <- specNodes spec] of+                [aggregate] -> aggSnapshot aggregate `shouldBe` Just (SnapshotSpec (SnapEvery 100) 1 "7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc" noLoc)+                aggregates -> expectationFailure ("expected one snapshot aggregate, got " <> show (length aggregates))+        it "rejects disabled intervals and invalid codec fixtures" $ do+            source <- readTestText "test/fixtures/reservation-snapshot.keiro"+            interval <- parseInlineSpec "<snapshot-zero>" (T.replace "snapshot every 100" "snapshot every 0" source)+            map code (validateSpec interval) `shouldContain` [SnapshotIntervalInvalid]+            version <- parseInlineSpec "<snapshot-version-zero>" (T.replace "state-codec version=1" "state-codec version=0" source)+            map code (validateSpec version) `shouldContain` [SnapshotCodecFixtureInvalid]+            emptyHash <- parseInlineSpec "<snapshot-empty-hash>" (T.replace "shape-hash=\"7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc\"" "shape-hash=\"\"" source)+            map code (validateSpec emptyHash) `shouldContain` [SnapshotCodecFixtureInvalid]+        it "conditionally lowers JSON instances and the live defaultStateCodec" $ do+            snapshot <- specOf "test/fixtures/reservation-snapshot.keiro"+            ordinary <- specOf "test/fixtures/reservation.keiro"+            case ([aggregate | NAggregate aggregate <- specNodes snapshot], [aggregate | NAggregate aggregate <- specNodes ordinary]) of+                ([snapshotAggregate], [ordinaryAggregate]) -> do+                    let snapshotModules = scaffoldAggregate (defaultContext (specContext snapshot)) snapshot snapshotAggregate+                        ordinaryModules = scaffoldAggregate (defaultContext (specContext ordinary)) ordinary ordinaryAggregate+                        snapshotDomain = generatedTextEndingIn "Domain.hs" snapshotModules+                        snapshotStream = generatedTextEndingIn "EventStream.hs" snapshotModules+                        ordinaryDomain = generatedTextEndingIn "Domain.hs" ordinaryModules+                        ordinaryStream = generatedTextEndingIn "EventStream.hs" ordinaryModules+                    snapshotDomain `shouldSatisfy` T.isInfixOf "deriving anyclass (ToJSON, FromJSON)"+                    snapshotStream `shouldSatisfy` T.isInfixOf "snapshotPolicy = Every 100"+                    snapshotStream `shouldSatisfy` T.isInfixOf "stateCodec = Just (withFoldFingerprint"+                    snapshotStream `shouldSatisfy` T.isInfixOf "Spec-visible fold changes invalidate old"+                    snapshotStream `shouldSatisfy` T.isInfixOf "module are invisible here"+                    snapshotStream `shouldSatisfy` T.isInfixOf "reservationSnapshotFixture = (1, \"7eb3a94f62f947231375d44083e2a1c8029d91ffe0329107d55092ed3430efcc\")"+                    ordinaryDomain `shouldNotSatisfy` T.isInfixOf "DeriveAnyClass"+                    ordinaryStream `shouldSatisfy` T.isInfixOf "snapshotPolicy = Never"+                    ordinaryStream `shouldSatisfy` T.isInfixOf "stateCodec = Nothing"+                    ordinaryStream `shouldSatisfy` T.isInfixOf "reservationCategory = Stream.categoryUnsafe \"reservation\""+                    firewallBreaches snapshotModules `shouldBe` []+                _ -> expectationFailure "expected one aggregate in each snapshot test spec"++    describe "aggregate fold fingerprints (plan 138)" $ do+        it "is deterministic across repeated parses and formatting-only changes" $ do+            source <- readTestText "test/fixtures/reservation.keiro"+            first <- parseInlineSpec "<first>" source+            second <- parseInlineSpec "<second>" ("\n\n" <> renderSpec first <> "\n")+            aggregateFoldFingerprint first (onlyAggregate first)+                `shouldBe` aggregateFoldFingerprint second (onlyAggregate second)+        it "changes for transition writes, guards, and referenced rule bodies" $ do+            base <- specOf "test/fixtures/reservation.keiro"+            writeChanged <- specOf "test/fixtures/reservation-foldchange.keiro"+            guardChanged <- specOf "test/fixtures/reservation-guard-tightened.keiro"+            source <- readTestText "test/fixtures/reservation.keiro"+            ruleChanged <- parseInlineSpec "<rule-change>" (T.replace "RedTag => true" "RedTag => false" source)+            let baseFingerprint = aggregateFoldFingerprint base (onlyAggregate base)+            aggregateFoldFingerprint writeChanged (onlyAggregate writeChanged) `shouldNotBe` baseFingerprint+            aggregateFoldFingerprint guardChanged (onlyAggregate guardChanged) `shouldNotBe` baseFingerprint+            aggregateFoldFingerprint ruleChanged (onlyAggregate ruleChanged) `shouldNotBe` baseFingerprint+        it "ignores wire and projection changes" $ do+            base <- specOf "test/fixtures/reservation.keiro"+            wireChanged <- specOf "test/fixtures/reservation-wire.keiro"+            source <- readTestText "test/fixtures/reservation.keiro"+            projectionChanged <- parseInlineSpec "<projection-change>" (T.replace "projection transfer_decisions" "projection renamed_projection" source)+            let surface = aggregateFoldSurface base (onlyAggregate base)+            aggregateFoldSurface wireChanged (onlyAggregate wireChanged) `shouldBe` surface+            aggregateFoldSurface projectionChanged (onlyAggregate projectionChanged) `shouldBe` surface+        it "invalidates mapped-register snapshots when binding or wire identity changes" $ do+            base <- specOf "test/fixtures/consumer-types.keiro"+            bindingChanged <- specOf "test/fixtures/consumer-types-binding-change.keiro"+            wireChanged <- specOf "test/fixtures/consumer-types-wirekey.keiro"+            let baseFingerprint = aggregateFoldFingerprint base (onlyAggregate base)+            aggregateFoldFingerprint bindingChanged (onlyAggregate bindingChanged) `shouldNotBe` baseFingerprint+            aggregateFoldFingerprint wireChanged (onlyAggregate wireChanged) `shouldNotBe` baseFingerprint++    describe "process/timer (EP-3)" $ do+        it "parses the hospital-surge process + nested timer" $ do+            input <- readTestText "test/fixtures/hospital-surge.keiro"+            case parseSpec "test/fixtures/hospital-surge.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> case [p | NProcess p <- specNodes spec] of+                    (p : _) -> do+                        procId p `shouldBe` "HospitalSurge"+                        procName p `shouldBe` "hospital-surge"+                        procRejected p `shouldBe` PolHalt+                        procPoison p `shouldBe` PolHalt+                        sagaCategory (procSaga p) `shouldBe` "hospitalSurge"+                        tmName (procTimer p) `shouldBe` "surgeFollowUp"+                        onReject (fireDisposition (tmFire (procTimer p))) `shouldBe` OFired+                        onAmbiguous (fireDisposition (tmFire (procTimer p))) `shouldBe` ORetry+                        tmMaxAttempts (procTimer p) `shouldBe` 5+                    [] -> expectationFailure "no process node parsed"+        it "round-trips the hospital-surge spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/hospital-surge.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the hospital-surge spec (no errors; benign-inversion warnings only)" $ do+            codes <- errorCodesOf "test/fixtures/hospital-surge.keiro"+            codes `shouldBe` []+        it "rejects illegal saga categories and no longer parses the raw stream-prefix clause" $ do+            spec <- specOf "test/fixtures/hospital-surge.keiro"+            mapM_+                (\categoryName -> processErrorCodes (\process -> process{procSaga = (procSaga process){sagaCategory = categoryName}}) spec `shouldContain` [SagaCategoryIllegal])+                ["", "$all", "hospital-surge", "hospital surge", "wf:surge"]+            source <- readTestText "test/fixtures/hospital-surge.keiro"+            parseSpec "<legacy-saga>" (T.replace "saga Surge category \"hospitalSurge\"" "saga Surge stream=\"hospital-surge-\" <> correlationId" source)+                `shouldSatisfy` isLeft+        it "rejects a wall-clock fireAt as ProcessFireAtNotInjected" $ do+            codes <- errorCodesOf "test/fixtures/hospital-surge-clock.keiro"+            codes `shouldContain` [ProcessFireAtNotInjected]+        it "reports one ProcessFireAtNotInjected for a wholly unknown fireAt field" $ do+            codes <- errorCodesOf "test/fixtures/hospital-surge-clock.keiro"+            length (filter (== ProcessFireAtNotInjected) codes) `shouldBe` 1+        it "rejects a user-supplied dispatch id as ProcessDispatchIdSupplied" $ do+            codes <- errorCodesOf "test/fixtures/hospital-surge-dispatchid.keiro"+            codes `shouldContain` [ProcessDispatchIdSupplied]+        it "rejects an unresolved saga reference as ProcessUnresolvedRef" $ do+            codes <- errorCodesOf "test/fixtures/hospital-surge-badref.keiro"+            codes `shouldContain` [ProcessUnresolvedRef]+        it "rejects unresolved process commands, projections, schedules, and advance ids" $ do+            codes <- errorCodesOf "test/fixtures/process-ghost-refs.keiro"+            length (filter (== ProcessUnresolvedRef) codes) `shouldBe` 5+            codes `shouldContain` [ProcessDispatchIdSupplied]++    describe "router (EP-108)" $ do+        it "parses the incident-paging router shape" $ do+            input <- readTestText "test/fixtures/incident-paging/incident-paging.keiro"+            case parseSpec "test/fixtures/incident-paging/incident-paging.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> case [router | NRouter router <- specNodes spec] of+                    [router] -> do+                        rtId router `shouldBe` "PagingRouter"+                        rtName router `shouldBe` "jitsurei-paging"+                        corrField (rtKey router) `shouldBe` "incidentId"+                        rvSource (rtResolve router) `shouldBe` ResolveReadModel "service_oncall"+                        rvRow (rtResolve router) `shouldBe` ["responderId"]+                        rdCommand (rtDispatch router) `shouldBe` "SendPage"+                        rtRejected router `shouldBe` PolDeadLetter+                        rtPoison router `shouldBe` PolHalt+                    routers -> expectationFailure ("expected one router, got " <> show (length routers))+        it "round-trips the incident-paging spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/incident-paging/incident-paging.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the incident-paging router with warnings only" $ do+            codes <- errorCodesOf "test/fixtures/incident-paging/incident-paging.keiro"+            codes `shouldBe` []+            diagnostics <- diagnosticCodesOf "test/fixtures/incident-paging/incident-paging.keiro"+            diagnostics `shouldContain` [PolicyDeadLetterUnused, AmbiguousFollowsRejectedPolicy]+        it "rejects unresolved targets, keys, commands, and binding scopes" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            routerErrorCodes (\router -> router{rtTarget = "Pge"}) spec `shouldContain` [RouterUnresolvedRef]+            routerErrorCodes (\router -> router{rtKey = (rtKey router){corrField = "incidntId"}}) spec `shouldContain` [RouterKeyFieldUnknown]+            routerErrorCodes (\router -> router{rtDispatch = (rtDispatch router){rdCommand = "SendPag"}}) spec `shouldContain` [RouterCommandUnknown]+            routerErrorCodes+                ( \router ->+                    let dispatch = rtDispatch router+                     in router{rtDispatch = dispatch{rdFields = [FieldBinding "responderId" (Just "resolved.responder")]}}+                )+                spec+                `shouldContain` [RouterBindingUnscoped]+        it "rejects unresolved read models and contradictory rejection policies" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            let withoutReadModel = removeReadModel "service_oncall" spec+            errorCodes withoutReadModel `shouldContain` [RouterUnresolvedRef]+            routerErrorCodes+                ( \router ->+                    let dispatch = rtDispatch router+                        disposition = rdDisposition dispatch+                     in router+                            { rtRejected = PolHalt+                            , rtDispatch = dispatch{rdDisposition = disposition{onFailed = DDeadLetter "page rejected"}}+                            }+                )+                spec+                `shouldContain` [PolicyContradiction]+        it "rejects on-ambiguous Fired for process timers" $ do+            spec <- specOf "test/fixtures/hospital-surge.keiro"+            let changed =+                    spec+                        { specNodes =+                            [ case node of+                                NProcess process ->+                                    let timer = procTimer process+                                        fire = tmFire timer+                                        disposition = fireDisposition fire+                                     in NProcess process{procTimer = timer{tmFire = fire{fireDisposition = disposition{onAmbiguous = OFired}}}}+                                _ -> node+                            | node <- specNodes spec+                            ]+                        }+            errorCodes changed `shouldContain` [AmbiguousMarkedBenign]+        it "requires explicit policy and ambiguity clauses in the grammar" $ do+            source <- readTestText "test/fixtures/hospital-surge.keiro"+            parseSpec "<missing-poison>" (T.replace "  poison => halt\n" "" source) `shouldSatisfy` isLeft+            parseSpec "<missing-ambiguous>" (T.replace " ; on-ambiguous Retry" "" source) `shouldSatisfy` isLeft+        it "scaffolds firewall-clean router wiring, policies, and typed-hole guidance" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            case [router | NRouter router <- specNodes spec] of+                [router] -> do+                    let ctx = defaultContext (specContext spec)+                        modules = scaffoldRouter ctx router+                        generated = [m | m <- modules, kind m == Generated]+                        holes = [m | m <- modules, kind m == HoleStub]+                    firewallBreaches generated `shouldBe` []+                    case (generated, holes) of+                        ([generatedModule], [holeModule]) -> do+                            moduleText generatedModule `shouldSatisfy` T.isInfixOf "pagingRouterWorkerOptions"+                            moduleText generatedModule `shouldSatisfy` T.isInfixOf "rejectedCommandPolicy = RejectedDeadLetter"+                            moduleText holeModule `shouldSatisfy` T.isInfixOf "UNION of resolved target identities"+                            moduleText holeModule `shouldSatisfy` T.isInfixOf "confirmBenignDuplicate"+                        _ -> expectationFailure "expected one generated router module and one router hole module"+                routers -> expectationFailure ("expected one router, got " <> show (length routers))+        it "requires a caller callback for non-halting poison policies" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            case [router | NRouter router <- specNodes spec] of+                [router] -> do+                    let ctx = defaultContext (specContext spec)+                        generatedFor choice = [moduleText m | m <- scaffoldRouter ctx router{rtPoison = choice}, kind m == Generated]+                    mapM_+                        ( \(choice, constructor) -> case generatedFor choice of+                            [generatedModule] -> do+                                generatedModule `shouldSatisfy` T.isInfixOf "(Envelope msg -> Eff es ()) -> WorkerOptions es msg"+                                generatedModule `shouldSatisfy` T.isInfixOf (constructor <> " poisonCallback")+                            _ -> expectationFailure "expected one generated router module"+                        )+                        [(PolDeadLetter, "PoisonDeadLetter"), (PolSkip, "PoisonSkip")]+                    case [moduleText m | m <- scaffoldRouter ctx router{rtRejected = PolSkip}, kind m == Generated] of+                        [generatedModule] -> generatedModule `shouldSatisfy` T.isInfixOf "rejectedCommandPolicy = RejectedSkip"+                        _ -> expectationFailure "expected one generated router module"+                routers -> expectationFailure ("expected one router, got " <> show (length routers))+        it "emits router harness facts that pin policy and target-keyed identity" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            case [router | NRouter router <- specNodes spec] of+                [router] -> case harnessRouter (defaultContext (specContext spec)) router of+                    [facts] -> do+                        moduleText facts `shouldSatisfy` T.isInfixOf "(\"rejectedPolicy\", \"deadLetter\")"+                        moduleText facts `shouldSatisfy` T.isInfixOf "targetStreamName, occurrence"+                    modules -> expectationFailure ("expected one router harness, got " <> show (length modules))+                routers -> expectationFailure ("expected one router, got " <> show (length routers))+        it "rejects invalid timer ceilings and target field bindings" $ do+            codes <- errorCodesOf "test/fixtures/process-bad-timer.keiro"+            mapM_+                (\expected -> codes `shouldContain` [expected])+                [ProcessTimerCeilingInvalid, ProcessFieldBindingUnresolved]+        it "accepts resolved process projection references" $ do+            codes <- errorCodesOf "test/fixtures/surge-service.keiro"+            codes `shouldBe` []+        it "scaffolds the process: Generated wiring is firewall-clean + a HoleStub" $ do+            mods <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+            let gens = [m | m <- mods, kind m == Generated]+                holes = [m | m <- mods, kind m == HoleStub]+            length holes `shouldBe` 1+            firewallBreaches gens `shouldBe` []+            case gens of+                [generatedModule] -> do+                    -- the worker uses the spec's ceiling, never the dangerous default+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "max-attempts = 5"+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeProcessWorkerOptions"+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeCategory = Stream.categoryUnsafe \"hospitalSurge\""+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "confirmBenignDuplicate"+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "StreamName -> EventId -> CommandError -> Eff es Bool"+                    moduleText generatedModule `shouldSatisfy` T.isInfixOf "Left (CommandAmbiguous _)"+                    case holes of+                        [holeModule] -> moduleText holeModule `shouldSatisfy` T.isInfixOf "entityStream hospitalSurgeCategory"+                        _ -> expectationFailure "expected one process hole module"+                _ -> expectationFailure "expected one generated process module"+        it "process scaffold is deterministic" $ do+            a <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+            b <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+            map moduleText a `shouldBe` map moduleText b++    describe "contract (EP-4)" $ do+        it "parses the emergency contract (topics + events-on-topic + typed fields)" $ do+            input <- readTestText "test/fixtures/contract.keiro"+            case parseSpec "test/fixtures/contract.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> case [c | NContract c <- specNodes spec] of+                    (c : _) -> do+                        ctrName c `shouldBe` "emergency"+                        ctrDiscriminator c `shouldBe` "messageType"+                        map fst (ctrTopics c) `shouldBe` ["incidentEvents", "hospitalEvents"]+                        map ceName (ctrEvents c) `shouldBe` ["IncidentTransferNeedDeclared", "TransferReservationAccepted"]+                    [] -> expectationFailure "no contract node parsed"+        it "round-trips the contract spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/contract.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "round-trips the intake (inbox) spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/intake.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the intake spec (complete disposition, no inversions)" $ do+            codes <- errorCodesOf "test/fixtures/intake.keiro"+            codes `shouldBe` []+        it "lowers explicit dedupe-only persistence and defaults omission to full-envelope" $ do+            spec <- specOf "test/fixtures/intake.keiro"+            ordinary <- specOf "test/fixtures/intake-decode.keiro"+            case ([intake | NIntake intake <- specNodes spec], [intake | NIntake intake <- specNodes ordinary]) of+                ([intake], [defaultIntake]) -> do+                    inkPersist intake `shouldBe` InkPersistDedupeOnly+                    inkPersist defaultIntake `shouldBe` InkPersistFull+                    renderSpec spec `shouldSatisfy` T.isInfixOf "persist = dedupe-only"+                    renderSpec ordinary `shouldNotSatisfy` T.isInfixOf "persist ="+                    let inbox = generatedTextEndingIn "Inbox.hs" (scaffoldIntake (defaultContext (specContext spec)) intake)+                    inbox `shouldSatisfy` T.isInfixOf "inboxPersistence = PersistDedupeOnly"+                (intakes, defaultIntakes) ->+                    expectationFailure ("expected one intake in each fixture, got " <> show (length intakes, length defaultIntakes))+        it "rejects duplicate => retry (inversion 1)" $ do+            codes <- errorCodesOf "test/fixtures/intake-dup-retry.keiro"+            codes `shouldContain` [DispositionDuplicateRetry]+        it "rejects previouslyFailed => retry (inversion 2)" $ do+            codes <- errorCodesOf "test/fixtures/intake-pf-retry.keiro"+            codes `shouldContain` [DispositionPreviouslyFailedRetry]+        it "rejects an incomplete disposition table" $ do+            codes <- errorCodesOf "test/fixtures/intake-incomplete.keiro"+            codes `shouldContain` [DispositionIncomplete]+        it "rejects a shadowing duplicate intake disposition row" $ do+            codes <- errorCodesOf "test/fixtures/intake-dup-row.keiro"+            codes `shouldContain` [DispositionDuplicateOutcome]+        it "rejects intake events declared on another topic" $ do+            codes <- errorCodesOf "test/fixtures/intake-topic-mismatch.keiro"+            codes `shouldContain` [TopicAffinityMismatch]+        it "round-trips the emit/publisher spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/emit.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the emit/publisher spec (skip present, coupling resolves)" $ do+            codes <- errorCodesOf "test/fixtures/emit.keiro"+            codes `shouldBe` []+        it "rejects a missing _ => skip catch-all as EmitSkipMissing" $ do+            codes <- errorCodesOf "test/fixtures/emit-noskip.keiro"+            codes `shouldContain` [EmitSkipMissing]+        it "rejects mapping to an undeclared contract event as EmitUnresolvedContract" $ do+            codes <- errorCodesOf "test/fixtures/emit-badevent.keiro"+            codes `shouldContain` [EmitUnresolvedContract]+        it "rejects emit events declared on another topic" $ do+            codes <- errorCodesOf "test/fixtures/emit-topic-mismatch.keiro"+            codes `shouldContain` [TopicAffinityMismatch]++    describe "pgmq workqueue/dispatch (EP-5)" $ do+        it "round-trips the reservation-work spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/reservation-work.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the reservation-work spec (physical matches, no inversions)" $ do+            codes <- errorCodesOf "test/fixtures/reservation-work.keiro"+            codes `shouldBe` []+        it "rejects a divergent captured physical name as WqPhysicalDivergence" $ do+            codes <- errorCodesOf "test/fixtures/reservation-work-divergent.keiro"+            codes `shouldContain` [WqPhysicalDivergence]+        it "rejects storeFailure => deadLetter as WqStoreFailureNotRetry" $ do+            codes <- errorCodesOf "test/fixtures/reservation-work-sf-deadletter.keiro"+            codes `shouldContain` [WqStoreFailureNotRetry]+        it "rejects decodeFailure => retry as WqDecodeFailureNotDeadLetter" $ do+            codes <- errorCodesOf "test/fixtures/reservation-work-df-retry.keiro"+            codes `shouldContain` [WqDecodeFailureNotDeadLetter]+        it "requires complete, unique workqueue disposition rows" $ do+            incomplete <- errorCodesOf "test/fixtures/workqueue-incomplete.keiro"+            incomplete `shouldContain` [WqDispositionIncomplete]+            duplicateSpec <- specOf "test/fixtures/workqueue-dup-row.keiro"+            let duplicateDiagnostics = [d | d <- validateSpec duplicateSpec, code d == DispositionDuplicateOutcome]+            map line duplicateDiagnostics `shouldBe` [17]+        it "checks the captured queueRef dlq and table fixtures" $ do+            dlqCodes <- errorCodesOf "test/fixtures/workqueue-dlq-divergent.keiro"+            dlqCodes `shouldContain` [WqDlqDivergence]+            tableCodes <- errorCodesOf "test/fixtures/workqueue-table-divergent.keiro"+            tableCodes `shouldContain` [WqTableDivergence]+        it "matches queueRef for upper-case, punctuation, and hashed logical names" $ do+            upper <- errorCodesOf "test/fixtures/workqueue-uppercase-logical.keiro"+            upper `shouldBe` []+            hashed <- errorCodesOf "test/fixtures/workqueue-hashed-logical.keiro"+            hashed `shouldBe` []+            derivedQueueTrio "hospital_capacity.reservation_work.per_hospital_fifo_lane_assignments"+                `shouldBe` ( "hospital_capacity_reservat_757040df00976c33"+                           , "hospital_capacity_reservat_757040df00976c33_dlq"+                           , "pgmq.q_hospital_capacity_reservat_757040df00976c33"+                           )+        it "resolves dispatch dedup queues and payload wire fields" $ do+            ghost <- errorCodesOf "test/fixtures/dispatch-dedup-ghost-queue.keiro"+            ghost `shouldContain` [DispatchDedupQueueUnresolved]+            field <- errorCodesOf "test/fixtures/dispatch-dedup-bad-field.keiro"+            field `shouldContain` [DispatchDedupFieldUnresolved]+        it "requires a resolvable group key exactly when ordering is FIFO" $ do+            noKey <- errorCodesOf "test/fixtures/reservation-work-fifo-nokey.keiro"+            noKey `shouldContain` [WqGroupKeyMissing]+            unordered <- errorCodesOf "test/fixtures/reservation-work-key-unordered.keiro"+            unordered `shouldContain` [WqGroupKeyWithoutFifo]+            source <- readTestText "test/fixtures/reservation-work.keiro"+            unresolved <- parseInlineSpec "<unresolved-group-key>" (T.replace "group key from reservationId" "group key from missingId" source)+            map code (validateSpec unresolved) `shouldContain` [WqGroupKeyUnresolved]+        it "warns on unlogged storage and rejects empty partition settings" $ do+            warningCodes <- diagnosticCodesOf "test/fixtures/reservation-work-unlogged.keiro"+            warningCodes `shouldContain` [WqUnloggedDurability]+            partitionCodes <- errorCodesOf "test/fixtures/reservation-work-partitioned-empty.keiro"+            partitionCodes `shouldContain` [WqPartitionSpecEmpty]+        it "lowers ordering, provisioning, and raw group-key projection" $ do+            spec <- specOf "test/fixtures/reservation-work.keiro"+            case [workqueue | NWorkqueue workqueue <- specNodes spec] of+                workqueue : _ -> do+                    let modules = scaffoldWorkqueue (defaultContext (specContext spec)) workqueue+                        queue = generatedTextEndingIn "Queue.hs" modules+                        policy = generatedTextEndingIn "QueuePolicy.hs" modules+                    queue `shouldSatisfy` T.isInfixOf "groupKeyFor payload = payload.reservationId"+                    policy `shouldSatisfy` T.isInfixOf "jobOrdering = FifoThroughput"+                    policy `shouldSatisfy` T.isInfixOf "withFifoIndexProvision (standardProvision)"+                    firewallBreaches modules `shouldBe` []+                [] -> expectationFailure "reservation-work fixture has no workqueue"++    describe "readmodel (EP-107)" $ do+        it "parses and round-trips first-class read models" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            case [readModel | NReadModel readModel <- specNodes spec] of+                [subscriptionModel, inlineModel] -> do+                    rmName subscriptionModel `shouldBe` "transfer_decisions"+                    rmColumns subscriptionModel+                        `shouldBe` [ RmColumn "reservation_id" "text" True+                                   , RmColumn "hospital_id" "text" True+                                   , RmColumn "status" "text" True+                                   , RmColumn "decided_at" "timestamptz" False+                                   ]+                    rmScope subscriptionModel `shouldBe` Just (RmCategory "reservation")+                    rmFeed subscriptionModel `shouldBe` RmSubscription+                    rmSubscription subscriptionModel `shouldBe` Just "hospital-capacity-transfer-decisions-sub"+                    rmName inlineModel `shouldBe` "subscriptions"+                    rmScope inlineModel `shouldBe` Nothing+                    rmFeed inlineModel `shouldBe` RmInline+                nodes -> expectationFailure ("expected two readmodel nodes, got " <> show (length nodes))+            parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts an aggregate projection without a consistency clause" $ do+            spec <- parseInlineSpec "<projection-without-consistency>" projectionWithoutConsistencySpec+            case [projection | NAggregate aggregate <- specNodes spec, Just projection <- [aggProjection aggregate]] of+                [projection] -> projConsistency projection `shouldBe` Nothing+                projections -> expectationFailure ("expected one projection, got " <> show (length projections))+        it "pins the canonical UTF-8 shape digest and runtime identities" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            case [readModel | NReadModel readModel <- specNodes spec] of+                (subscriptionModel : inlineModel : _) -> do+                    canonicalShape subscriptionModel+                        `shouldBe` "transfer_decisions|reservation_id:text:req|hospital_id:text:req|status:text:req|decided_at:timestamptz:null"+                    deriveShapeHash subscriptionModel `shouldBe` "fnv1a:3717f6d9e3c44bd6"+                    deriveShapeHash inlineModel `shouldBe` "fnv1a:f54d9bb2f40a6738"+                    registryNameFor (specContext spec) subscriptionModel `shouldBe` "hospital-capacity-transfer-decisions"+                    subscriptionNameFor (specContext spec) subscriptionModel `shouldBe` "hospital-capacity-transfer-decisions-sub"+                    subscriptionNameFor "billing" inlineModel `shouldBe` "billing-subscriptions-sub"+                nodes -> expectationFailure ("expected readmodel nodes, got " <> show (length nodes))+        it "accepts the positive readmodel fixture with all references resolved" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            validateSpec spec `shouldBe` []+        it "rejects shape drift and unknown SQL column types" $ do+            codes <- errorCodesOf "test/fixtures/readmodel-shape-drift.keiro"+            codes `shouldContain` [RmShapeHashDrift, RmUnknownColumnType]+        it "rejects Strong on inline and standalone projections" $ do+            inlineCodes <- errorCodesOf "test/fixtures/readmodel-strong-inline.keiro"+            inlineCodes `shouldContain` [RmStrongInlineOnly]+            standalone <- specOf "test/fixtures/readmodel-strong-standalone.keiro"+            let diagnostics = validateSpec standalone+            map code diagnostics `shouldContain` [RmStrongInlineOnly, RmProjectionWithoutNode]+            [severity diagnostic | diagnostic <- diagnostics, code diagnostic == RmProjectionWithoutNode]+                `shouldBe` [Warning]+        it "rejects scope without Strong and an unreferenced inline feed" $ do+            scopeCodes <- errorCodesOf "test/fixtures/readmodel-scope-eventual.keiro"+            scopeCodes `shouldContain` [RmScopeWithoutStrong]+            inlineCodes <- errorCodesOf "test/fixtures/readmodel-inline-unreferenced.keiro"+            inlineCodes `shouldContain` [RmInlineFeedUnreferenced]+        it "rejects projection consistency conflicts" $ do+            codes <- errorCodesOf "test/fixtures/readmodel-consistency-conflict.keiro"+            codes `shouldContain` [RmConsistencyConflict]+        it "resolves query read models and validates query consistency" $ do+            codes <- errorCodesOf "test/fixtures/readmodel-query-unresolved.keiro"+            codes `shouldContain` [QueryUnresolvedReadModel, QueryConsistencyInvalid]+        it "resolves dispatch read models and declared dedup columns" $ do+            codes <- errorCodesOf "test/fixtures/readmodel-dispatch-unresolved.keiro"+            codes `shouldContain` [DispatchReadModelUnresolved, DispatchReadModelFieldUnknown]+        it "scaffolds runtime records, rebuild helpers, async wiring, and typed holes" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            let ctx = defaultContext (specContext spec)+                readModels = [readModel | NReadModel readModel <- specNodes spec]+                modules = concatMap (scaffoldReadModel ctx) readModels+                transfer = generatedTextEndingIn "Transfer_decisions/ReadModel.hs" modules+                inline = generatedTextEndingIn "Subscriptions/ReadModel.hs" modules+                transferHoles = [moduleText m | m <- modules, "Transfer_decisions/ReadModelHoles.hs" `T.isSuffixOf` T.pack (modulePath m)]+            length modules `shouldBe` 6+            length [m | m <- modules, kind m == Generated] `shouldBe` 4+            length [m | m <- modules, kind m == HoleStub] `shouldBe` 2+            firewallBreaches modules `shouldBe` []+            transfer `shouldSatisfy` T.isInfixOf "registerTransferDecisions"+            transfer `shouldSatisfy` T.isInfixOf "Rebuild.startRebuild transferDecisionsReadModel [\"hospital-capacity-transfer-decisions-async\"]"+            transfer `shouldSatisfy` T.isInfixOf "strongScope = CategoryHead \"reservation\""+            transfer `shouldSatisfy` T.isInfixOf "transferDecisionsAsyncProjection"+            inline `shouldSatisfy` T.isInfixOf "Rebuild.startRebuild subscriptionsReadModel []"+            inline `shouldNotSatisfy` T.isInfixOf "AsyncProjection"+            transferHoles `shouldSatisfy` any (T.isInfixOf "RecordedEvent -> Tx.Transaction ()")+        it "threads qualified table and column guidance into aggregate projection holes" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            case [aggregate | NAggregate aggregate <- specNodes spec] of+                [aggregate] -> do+                    let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate+                        holes = [moduleText m | m <- modules, kind m == HoleStub]+                        projection = generatedTextEndingIn "Projection.hs" modules+                    holes `shouldSatisfy` any (T.isInfixOf "subscriptionsQualifiedTable")+                    holes `shouldSatisfy` any (T.isInfixOf "Table: \"billing\".\"subscriptions\"")+                    projection `shouldSatisfy` T.isInfixOf "ReadModelTable.subscriptionsQualifiedTable"+                aggregates -> expectationFailure ("expected one aggregate, got " <> show (length aggregates))+        it "emits runtime-free derivation facts for each read model" $ do+            spec <- specOf "test/fixtures/readmodel.keiro"+            case [readModel | NReadModel readModel <- specNodes spec] of+                (subscriptionModel : _) -> do+                    let modules = harnessReadModel (defaultContext (specContext spec)) subscriptionModel+                        harnessText = generatedTextEndingIn "ReadModelHarness.hs" modules+                    length modules `shouldBe` 1+                    firewallBreaches modules `shouldBe` []+                    harnessText `shouldSatisfy` T.isInfixOf "(\"shapeHash\", \"fnv1a:3717f6d9e3c44bd6\", \"fnv1a:3717f6d9e3c44bd6\")"+                    harnessText `shouldSatisfy` T.isInfixOf "(\"strongScope\", \"CategoryHead reservation\", \"CategoryHead reservation\")"+                    harnessText `shouldSatisfy` T.isInfixOf "runReadModelFacts"+                nodes -> expectationFailure ("expected readmodel nodes, got " <> show (length nodes))++    describe "workflow/operation (EP-6)" $ do+        it "round-trips the workflow spec through parse . pretty" $ do+            input <- readTestText "test/fixtures/workflow.keiro"+            case parseSpec "in" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> parseSpec "in" (renderSpec spec) `shouldBe` Right spec+        it "accepts the workflow spec (await<->signal matches, run resolves)" $ do+            codes <- errorCodesOf "test/fixtures/workflow.keiro"+            codes `shouldBe` []+        it "rejects a signal label with no matching await as AwaitSignalMismatch" $ do+            codes <- errorCodesOf "test/fixtures/workflow-signal-mismatch.keiro"+            codes `shouldContain` [AwaitSignalMismatch]+        it "rejects duplicate workflow labels" $ do+            codes <- errorCodesOf "test/fixtures/workflow-dup-label.keiro"+            codes `shouldContain` [WorkflowDuplicateLabel]+        it "rejects unresolved workflow id and sleep fields" $ do+            codes <- errorCodesOf "test/fixtures/workflow-unresolved-fields.keiro"+            codes `shouldContain` [WorkflowIdFieldUnresolved, WorkflowSleepDelayUnresolved]+        it "validates rule domains, totality, case constructors, and bodies" $ do+            unresolved <- errorCodesOf "test/fixtures/rule-bad-domain.keiro"+            unresolved `shouldBe` [RuleDomainUnresolved]+            codes <- errorCodesOf "test/fixtures/rule-not-total.keiro"+            mapM_+                (\expected -> codes `shouldContain` [expected])+                [RuleNotTotal, RuleCaseUnknownCtor, ClockSampled, GuardAtomOutOfScope]+        it "rejects unresolved command operation references" $ do+            codes <- errorCodesOf "test/fixtures/operation-ghost-aggregate.keiro"+            codes `shouldContain` [OperationUnresolvedRef]+        it "rejects a signal value type that differs from its await" $ do+            codes <- errorCodesOf "test/fixtures/operation-signal-value.keiro"+            codes `shouldContain` [AwaitSignalValueMismatch]+        it "round-trips guarded patches and terminal continueAsNew" $ do+            input <- readTestText "test/fixtures/workflow-evolution.keiro"+            case parseSpec "workflow-evolution" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> do+                    parseSpec "workflow-evolution" (renderSpec spec) `shouldBe` Right spec+                    errorCodes spec `shouldBe` []+        it "rejects duplicate patch ids anywhere in the workflow body" $ do+            codes <- errorCodesOf "test/fixtures/workflow-patch-dup.keiro"+            codes `shouldBe` [WorkflowPatchDuplicate]+        it "rejects non-terminal and nested continueAsNew" $ do+            codes <- errorCodesOf "test/fixtures/workflow-can-mid.keiro"+            codes `shouldBe` [WorkflowContinueAsNewNotTerminal, WorkflowContinueAsNewNotTerminal]+        it "rejects a colon in a patch id with a workflow diagnostic" $ do+            codes <- errorCodesOf "test/fixtures/workflow-patch-colon.keiro"+            codes `shouldBe` [WorkflowPatchIdInvalid]+        it "lowers patch facts and live runtime declarations" $ do+            spec <- specOf "test/fixtures/workflow-evolution.keiro"+            case [workflow | NWorkflow workflow <- specNodes spec] of+                [workflow] -> do+                    let modules = harnessWorkflow (defaultContext (specContext spec)) workflow+                        facts = generatedTextEndingIn "WorkflowFacts.hs" modules+                        runtime = generatedTextEndingIn "WorkflowRuntime.hs" modules+                    facts `shouldSatisfy` T.isInfixOf "patch:fraud-check-v2(step:fraud-check)"+                    facts `shouldSatisfy` T.isInfixOf "continueAsNew:RolloverSeed"+                    facts `shouldSatisfy` T.isInfixOf "(\"patches\", \"fraud-check-v2\")"+                    runtime `shouldSatisfy` T.isInfixOf "declaredPatches = Set.fromList [PatchId \"fraud-check-v2\"]"+                    runtime `shouldSatisfy` T.isInfixOf "opts{activePatches = declaredPatches}"+                workflows -> expectationFailure ("expected one workflow, got " <> show (length workflows))++    describe "replay impact" $ do+        it "treats new events and transitions as replay-neutral" $ do+            old <- specOf "test/fixtures/reservation.keiro"+            let aggregate = onlyAggregate old+            case (aggEvents aggregate, aggTransitions aggregate) of+                (event : _, transition : _) -> do+                    let newEvent =+                            event+                                { evName = "ReservationReviewed"+                                , evLoc = noLoc+                                }+                        newTransition =+                            transition+                                { tEmits = ["ReservationReviewed"]+                                , tLoc = noLoc+                                }+                        new =+                            modifyAggregate+                                "Reservation"+                                ( \candidate ->+                                    candidate+                                        { aggEvents = aggEvents candidate <> [newEvent]+                                        , aggTransitions = aggTransitions candidate <> [newTransition]+                                        }+                                )+                                old+                    ReplayImpact.replayImpact old new `shouldBe` ReplayNeutral+                _ -> expectationFailure "reservation fixture must contain an event and transition"++        it "narrows a guard edit to that transition's event types" $ do+            impact <- replayImpactFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-guard-tightened.keiro"+            impact+                `shouldBe` ReplayAffected+                    ( Map.singleton+                        "Reservation"+                        AggregateImpact+                            { eventTypes = Set.singleton "TransferReservationCreated"+                            , includeSnapshotStreams = True+                            }+                    )++        it "proves a syntactic guard loosening replay-neutral" $ do+            old <- specOf "test/fixtures/reservation.keiro"+            let loosened =+                    modifyAggregate+                        "Reservation"+                        ( \aggregate ->+                            aggregate+                                { aggTransitions =+                                    [ transition{tGuard = Nothing}+                                    | transition <- aggTransitions aggregate+                                    ]+                                }+                        )+                        old+            ReplayImpact.replayImpact old loosened `shouldBe` ReplayNeutral++        it "marks every existing event when the aggregate wire convention changes" $ do+            impact <- replayImpactFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-wire.keiro"+            case impact of+                ReplayAffected aggregates ->+                    ReplayImpact.eventTypes <$> Map.lookup "Reservation" aggregates+                        `shouldBe` Just (Set.fromList ["TransferReservationCreated", "TransferReservationConfirmed"])+                ReplayNeutral -> expectationFailure "expected a wire-clause replay impact"++        it "includes snapshot streams when a write expression changes" $ do+            impact <- replayImpactFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-foldchange.keiro"+            case impact of+                ReplayAffected aggregates ->+                    includeSnapshotStreams <$> Map.lookup "Reservation" aggregates+                        `shouldBe` Just True+                ReplayNeutral -> expectationFailure "expected a fold replay impact"++        it "detects codec evolution and ignores formatting-only rewrites" $ do+            changed <- replayImpactFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-v2.keiro"+            changed `shouldSatisfy` (/= ReplayNeutral)+            old <- specOf "test/fixtures/reservation.keiro"+            formatted <- parseInlineSpec "<formatted>" (renderSpec old)+            ReplayImpact.replayImpact old formatted `shouldBe` ReplayNeutral++        it "names mapped nested event and snapshot roots while ignoring Haskell-only changes" $ do+            nested <- replayImpactFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-nested-propagation.keiro"+            case nested of+                ReplayAffected aggregates ->+                    Map.lookup "Catalog" aggregates+                        `shouldBe` Just AggregateImpact{eventTypes = Set.singleton "ArtifactObserved", includeSnapshotStreams = True}+                ReplayNeutral -> expectationFailure "expected nested mapped wire change to affect replay"+            sourceOnly <- replayImpactFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-haskell-rename.keiro"+            sourceOnly `shouldBe` ReplayNeutral++        it "generates one context target for every aggregate, including the process saga" $ do+            spec <- specOf "test/fixtures/surge-service.keiro"+            case scaffoldReplayAudit (defaultContext (specContext spec)) spec of+                [assembly] -> do+                    modulePath assembly `shouldBe` "Generated/SurgeDemo/ReplayAudit.hs"+                    moduleText assembly `shouldSatisfy` T.isInfixOf "Hospital.hospitalEventStream"+                    moduleText assembly `shouldSatisfy` T.isInfixOf "Surge.surgeEventStream"+                    T.count "      AuditTarget" (moduleText assembly) `shouldBe` 2+                assemblies -> expectationFailure ("expected one replay-audit assembly, got " <> show (length assemblies))++    describe "diff (evolution classification)" $ do+        it "covers every node family exactly once and explains exclusions" $ do+            sort (map fst familyRegistry) `shouldBe` ([minBound .. maxBound] :: [NodeFamily])+            [reason | (_, OutOfDiffScope reason) <- familyRegistry, T.null reason] `shouldBe` []+        it "derives every exercised headline from its vector under the default gate" $ do+            changes <-+                concat+                    <$> mapM+                        (uncurry diffFixtures)+                        [ ("test/fixtures/reservation.keiro", "test/fixtures/reservation-fieldadd.keiro")+                        , ("test/fixtures/reservation.keiro", "test/fixtures/reservation-v2.keiro")+                        , ("test/fixtures/reservation.keiro", "test/fixtures/reservation-enumadd.keiro")+                        , ("test/fixtures/contract.keiro", "test/fixtures/contract-fieldadd.keiro")+                        , ("test/fixtures/reservation-work.keiro", "test/fixtures/reservation-work-rename.keiro")+                        ]+            forM_ changes $ \change ->+                do+                    deriveLabel defaultGate (ckVector (kindOfChange change))+                        `shouldBe` labelOfChange change+                    gatedBreaking defaultGate change `shouldBe` isBreaking change+        it "never removes a breaking result when the gate grows" $+            property $+                forAll genCompatibilityVector $ \compatibility ->+                    forAll genSurfaceSet $ \gate ->+                        forAll genSurfaceSet $ \extra ->+                            deriveLabel gate compatibility+                                == LabelBreaking+                                    ==> deriveLabel (gate <> extra) compatibility+                                == LabelBreaking+        it "renders the consumer-neutral matrix with separate private, snapshot, and public surfaces" $ do+            changes <- diffFixtures "test/fixtures/compatibility-vector-old.keiro" "test/fixtures/compatibility-vector-new.keiro"+            golden <- readTestText "test/fixtures/compatibility-vector.diff.golden"+            let rendered = T.intercalate "\n" (map renderFinding changes)+                explained = T.intercalate "\n" (map renderExplainBlock changes)+                reportJson = T.pack (show (Aeson.toJSON (diffReport defaultGate changes)))+            T.stripEnd rendered `shouldBe` T.stripEnd golden+            rendered `shouldSatisfy` T.isInfixOf "Reservation.event.TransferReservationCreated.patientAcuity"+            rendered `shouldSatisfy` T.isInfixOf "old-binary-read-new-events=breaking"+            rendered `shouldSatisfy` T.isInfixOf "snapshot-hydration=advisory"+            rendered `shouldSatisfy` T.isInfixOf "public-consumer=breaking"+            explained `shouldSatisfy` T.isInfixOf "invalidate and rebuild snapshots"+            reportJson `shouldSatisfy` T.isInfixOf "keiro-dsl/diff-report/1"+            reportJson `shouldSatisfy` T.isInfixOf "Reservation.event.TransferReservationCreated.patientAcuity"+            let eventEnumFindings =+                    [ change+                    | change@(Advisory kind) <- changes+                    , ckCode kind == EnumCtorAdded+                    , verdictFor OldBinaryReadNewEvents (ckVector kind) == VBreaking+                    ]+            eventEnumFindings `shouldSatisfy` all (not . gatedBreaking defaultGate)+            eventEnumFindings `shouldSatisfy` all (gatedBreaking (gateWith [OldBinaryReadNewEvents]))+            forM_ changes $ \change ->+                remediationFor (ckContext (kindOfChange change)) (ckCode (kindOfChange change))+                    `shouldSatisfy` (not . null)+        it "rejects unknown --gate values with the valid surface list" $ do+            parseSurfaceName "mystery-surface"+                `shouldSatisfy` either (T.isInfixOf "old-binary-read-new-events" . T.pack) (const False)+        it "covers the mapped evolution matrix with stable codes and non-empty remedies" $ do+            let cases =+                    [ ("consumer-types-fieldadd-default.keiro", MappedFieldAddedWithDefault)+                    , ("consumer-types-fieldadd-nodefault.keiro", MappedFieldAddedNoDefault)+                    , ("consumer-types-fieldremove.keiro", MappedFieldRemoved)+                    , ("consumer-types-wirekey.keiro", MappedWireKeyChanged)+                    , ("consumer-types-haskell-rename.keiro", MappedHaskellSourceChanged)+                    , ("consumer-types-binding-change.keiro", MappedBindingChanged)+                    , ("consumer-types-fixtures-change.keiro", MappedFixturesChanged)+                    , ("consumer-types-initial-change.keiro", MappedInitialChanged)+                    , ("consumer-types-armadd.keiro", MappedArmAdded)+                    , ("consumer-types-tagchange.keiro", MappedArmTagChanged)+                    , ("consumer-types-enumadd.keiro", MappedEnumValueAdded)+                    , ("consumer-types-enumremove.keiro", MappedEnumValueRemoved)+                    , ("consumer-types-enumspelling.keiro", MappedEnumSpellingChanged)+                    , ("consumer-types-encoding.keiro", MappedUnionEncodingChanged)+                    , ("consumer-types-opaque-version.keiro", MappedOpaqueCodecChanged)+                    , ("consumer-types-mode-cross.keiro", MappedModeCrossed)+                    , ("consumer-types-nested-propagation.keiro", MappedArmTagChanged)+                    ]+            forM_ cases $ \(fixture, expectedCode) -> do+                changes <- diffFixtures "test/fixtures/consumer-types.keiro" ("test/fixtures/" <> fixture)+                map (ckCode . kindOfChange) changes `shouldContain` [expectedCode]+                forM_ changes $ \change ->+                    remediationFor (ckContext (kindOfChange change)) (ckCode (kindOfChange change))+                        `shouldSatisfy` (not . null)+        it "separates mapped event migration, snapshot invalidation, and directional rollout" $ do+            breakingAdd <- diffFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-fieldadd-nodefault.keiro"+            let noDefault = [change | change <- breakingAdd, ckCode (kindOfChange change) == MappedFieldAddedNoDefault]+            [ckFacet kind | Breaking kind <- noDefault] `shouldContain` ["mapped-event"]+            [ckFacet kind | Advisory kind <- noDefault] `shouldContain` ["mapped-register"]+            defaulted <- diffFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-fieldadd-default.keiro"+            [change | change <- defaulted, isBreaking change] `shouldBe` []+            let eventDefaults = [kind | Advisory kind <- defaulted, ckCode kind == MappedFieldAddedWithDefault, ckFacet kind == "mapped-event"]+            eventDefaults `shouldSatisfy` any ((== VBreaking) . verdictFor OldBinaryReadNewEvents . ckVector)+            armAdded <- diffFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-armadd.keiro"+            [change | change <- armAdded, isBreaking change] `shouldBe` []+            [kind | Advisory kind <- armAdded, ckCode kind == MappedArmAdded, ckFacet kind == "mapped-event"]+                `shouldSatisfy` any ((== VBreaking) . verdictFor OldBinaryReadNewEvents . ckVector)+        it "propagates a nested mapped leaf to complete command, event, and register paths" $ do+            changes <- diffFixtures "test/fixtures/consumer-types.keiro" "test/fixtures/consumer-types-nested-propagation.keiro"+            let subjects =+                    [ ckSubject kind+                    | change <- changes+                    , let kind = kindOfChange change+                    , ckCode kind == MappedArmTagChanged+                    ]+            subjects+                `shouldContain` [ "Catalog command ObserveArtifact .artifact : ArtifactInfo .location : ArtifactLocation .arm RepoPath[\"repository_path\"]"+                                , "Catalog event ArtifactObserved .artifact : ArtifactInfo .location : ArtifactLocation .arm RepoPath[\"repository_path\"]"+                                , "Catalog register currentArtifact : ArtifactInfo .location : ArtifactLocation .arm RepoPath[\"repository_path\"]"+                                ]+        it "classifies every remaining mapped field and declaration evolution row" $ do+            base <- specOf "test/fixtures/consumer-types.keiro"+            let mutationCodes =+                    [ (mapArtifactNamedField "key" (\field -> field{wfType = TInt}) base, MappedFieldTypeChanged)+                    , (mapArtifactNamedField "key" (\field -> field{wfPresence = POptional, wfOnMissing = Just (OmText "")}) base, MappedPresenceChanged)+                    , (mapArtifactNamedField "key" (\field -> field{wfType = TOptional TText}) base, MappedNullabilityChanged)+                    , (mapArtifactNamedField "description" (\field -> field{wfOnMissing = Nothing}) base, MappedDefaultRemoved)+                    , (mapArtifactNamedField "count" (\field -> field{wfOnMissing = Just (OmInt 1)}) base, MappedDefaultChanged)+                    , (mapMappedStructural "ArtifactInfo" renameMappedRecordConstructor base, MappedRecordConstructorChanged)+                    , (mapMappedStructural "ArtifactInfo" changeMappedCanonical base, MappedCanonicalTypeChanged)+                    ]+            forM_ mutationCodes $ \(candidate, expectedCode) ->+                map (ckCode . kindOfChange) (diffSpecs base candidate) `shouldContain` [expectedCode]+            let declarationA = completeStructural "A" (recordShape [TText])+                declarationB = completeStructural "B" (recordShape [TInt])+                onlyA = mappedSpec [declarationA]+                withB = mappedSpec [declarationA, declarationB]+            map (ckCode . kindOfChange) (diffSpecs onlyA withB) `shouldContain` [MappedDeclAdded]+            map (ckCode . kindOfChange) (diffSpecs withB onlyA) `shouldContain` [MappedDeclRemoved]+            diffSpecs base (mapArtifactNamedField "key" (\field -> field{wfHaskell = "renamedKey"}) base)+                `shouldBe` []+        it "visits every mapped wire mutation and reports every complete root path" $ do+            base <- specOf "test/fixtures/consumer-types.keiro"+            let mutations = mappedWireMutations base+            mutations `shouldSatisfy` (not . null)+            visited <- fmap Set.unions . forM mutations $ \mutation -> do+                let changes =+                        [ change+                        | change <- diffSpecs base (mmCandidate mutation)+                        , ckCode (kindOfChange change) == mmCode mutation+                        ]+                    actualSubjects = Set.fromList (map (ckSubject . kindOfChange) changes)+                changes `shouldSatisfy` any (not . isAdditiveChange)+                actualSubjects `shouldBe` mmExpectedSubjects mutation+                pure actualSubjects+            visited `shouldBe` Set.unions (map mmExpectedSubjects mutations)+        it "reports the exact ingredient code when every required mapped fact is deleted" $ do+            base <- specOf "test/fixtures/consumer-types.keiro"+            forM_ (mappedIngredientMutations base) $ \(candidate, expectedCode) ->+                errorCodes candidate `shouldContain` [expectedCode]+        it "classifies a field added without a version bump as BREAKING" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldadd.keiro"+            any isBreaking cs `shouldBe` True+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtFieldAddedWithoutBump]+        it "classifies the same field wrapped as v2 + upcaster as ADDITIVE" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-v2.keiro"+            any isBreaking cs `shouldBe` False+            [ck | Additive ck <- cs] `shouldSatisfy` any ((== "TransferReservationCreated") . ckSubject)+        it "reports no breaking change when the spec is unchanged" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation.keiro"+            any isBreaking cs `shouldBe` False+        it "classifies a direct event field type change as EvtFieldTypeChanged" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldtype.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtFieldTypeChanged]+        it "resolves fields(Command) before comparing event field types" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-cmdfieldtype.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtFieldTypeChanged]+        it "uses EvtFieldRemovedSameVersion for an unchanged-version removal" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-fieldremove.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtFieldRemovedSameVersion]+        it "uses EvtVersionDecreased for a version decrease" $ do+            cs <- diffFixtures "test/fixtures/reservation-v2.keiro" "test/fixtures/reservation.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtVersionDecreased]+        it "rejects a v1 to v3 jump whose only upcaster starts at v2" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-v3-dangling.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EvtVersionMissingUpcaster]+        it "classifies a vanished historical upcaster rung as UpcasterChainGap" $ do+            cs <- diffFixtures "test/fixtures/reservation-v2.keiro" "test/fixtures/reservation-chain-gap.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [UpcasterChainGap]+        it "classifies an enum constructor removal as EnumCtorRemoved" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumdrop.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EnumCtorRemoved]+        it "classifies an enum wire-spelling change as EnumWireSpellingChanged" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumwire.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [EnumWireSpellingChanged]+        it "classifies an enum constructor addition per use site as advisory" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-enumadd.keiro"+            any isBreaking cs `shouldBe` False+            let enumFindings = [k | Advisory k <- cs, ckCode k == EnumCtorAdded]+            [ckSubject k | k <- enumFindings] `shouldContain` ["BlackTag"]+            [verdictFor SnapshotHydration (ckVector k) | k <- enumFindings]+                `shouldContain` [VAdvisory]+        it "classifies an effective wire convention change as WireSpecChanged" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-wire.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WireSpecChanged]+        it "advises when the aggregate fold surface changes" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-foldchange.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [AggFoldSurfaceChanged]+        it "advises on hazardous deprecation and reports un-deprecation" $ do+            deprecated <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-deprecated.keiro"+            any isBreaking deprecated `shouldBe` False+            [ckCode k | Advisory k <- deprecated] `shouldContain` [DeprecatedEventReplayHazard]+            restored <- diffFixtures "test/fixtures/reservation-deprecated.keiro" "test/fixtures/reservation.keiro"+            any isAdvisory restored `shouldBe` True+            [ckCode k | Advisory k <- restored] `shouldContain` [EventUndeprecated]+        it "recognises replay-only deprecation as a replay-safe retirement cutover" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-deprecated-replay-only.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [EventRetirementInProgress]+            [ckCode k | Advisory k <- cs] `shouldNotContain` [DeprecatedEventReplayHazard]+        it "advises when event retirement starts" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-retiring.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [EventRetirementInProgress]+        it "does not recommend decode-only deprecation for an event removal" $ do+            old <- specOf "test/fixtures/reservation.keiro"+            let new =+                    old+                        { specNodes =+                            [ case node of+                                NAggregate aggregate ->+                                    NAggregate+                                        aggregate+                                            { aggEvents =+                                                [ event+                                                | event <- aggEvents aggregate+                                                , evName event /= "TransferReservationConfirmed"+                                                ]+                                            }+                                _ -> node+                            | node <- specNodes old+                            ]+                        }+                removals = [change | change@(Breaking kind) <- diffSpecs old new, ckCode kind == EvtRemovedNotDeprecated]+            removals `shouldSatisfy` (not . null)+            [ckDetail kind | Breaking kind <- removals]+                `shouldSatisfy` all (not . T.isInfixOf "so old payloads still decode")+        it "prints a paste-ready replay-only twin when a guard tightens (plan 143)" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-guard-tightened.keiro"+            any isBreaking cs `shouldBe` False+            let advisories = [k | Advisory k <- cs, ckCode k == AggGuardTightened]+            map ckSubject advisories `shouldBe` ["Unrequested -- RequestTransferReservation"]+            detail <- case advisories of+                [k] -> pure (ckDetail k)+                other -> expectationFailure ("expected one advisory, got " <> show other) >> pure ""+            detail `shouldSatisfy` T.isInfixOf "replay-only Unrequested -- RequestTransferReservation"+            -- The printed twin is paste-ready: appended to the new spec it+            -- parses, validates without errors, and silences the advisory.+            tightened <- readTestText "test/fixtures/reservation-guard-tightened.keiro"+            let twinText = snd (T.breakOnEnd "\n\n" detail)+                pasted = tightened <> "\n" <> twinText <> "\n"+            case parseSpec "<pasted-twin>" pasted of+                Left err -> expectationFailure (T.unpack err)+                Right pastedSpec -> do+                    [code d | d <- validateSpec pastedSpec, severity d == Error] `shouldBe` []+                    base <- specOf "test/fixtures/reservation.keiro"+                    [k | Advisory k <- diffSpecs base pastedSpec, ckCode k == AggGuardTightened]+                        `shouldBe` []+        it "omits the twin advisory when the twin is already present (plan 143)" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-guard-tightened-twin.keiro"+            [k | Advisory k <- cs, ckCode k == AggGuardTightened] `shouldBe` []+        it "classifies a removed contract event as ContractEventRemoved" $ do+            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-eventdrop.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [ContractEventRemoved]+        it "classifies contract field type changes and unversioned additions as ContractFieldChanged" $ do+            changed <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-fieldtype.keiro"+            [ckCode k | Breaking k <- changed] `shouldContain` [ContractFieldChanged]+            added <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-fieldadd.keiro"+            [ckCode k | Breaking k <- added] `shouldContain` [ContractFieldChanged]+        it "reports a field addition with a contract version bump as an advisory" $ do+            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-bump-fieldadd.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [ContractSchemaVersionBumped]+        it "classifies a contract schema version decrease separately" $ do+            cs <- diffFixtures "test/fixtures/contract-bump-fieldadd.keiro" "test/fixtures/contract.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [ContractSchemaVersionDecreased]+        it "classifies contract topic and discriminator changes separately" $ do+            topic <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-topic.keiro"+            [ckCode k | Breaking k <- topic] `shouldContain` [ContractTopicChanged]+            discriminator <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-discriminator.keiro"+            [ckCode k | Breaking k <- discriminator] `shouldContain` [ContractDiscriminatorChanged]+        it "classifies a new contract event as additive" $ do+            cs <- diffFixtures "test/fixtures/contract.keiro" "test/fixtures/contract-eventadd.keiro"+            any isBreaking cs `shouldBe` False+            [ckSubject k | Additive k <- cs] `shouldContain` ["IncidentTransferNeedCancelled"]+        it "classifies workqueue wire names, types, and required additions as WqPayloadFieldChanged" $ do+            wire <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-wirename.keiro"+            [ckCode k | Breaking k <- wire] `shouldContain` [WqPayloadFieldChanged]+            fieldTypeChange <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-fieldtype.keiro"+            [ckCode k | Breaking k <- fieldTypeChange] `shouldContain` [WqPayloadFieldChanged]+            required <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-reqfield.keiro"+            [ckCode k | Breaking k <- required] `shouldContain` [WqPayloadFieldChanged]+        it "classifies a new optional workqueue payload field as additive" $ do+            cs <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-optfield.keiro"+            any isBreaking cs `shouldBe` False+            [ckSubject k | Additive k <- cs] `shouldContain` ["note"]+        it "classifies workqueue ordering changes as breaking delivery-contract changes" $ do+            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-ordering-change.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WqOrderingChanged]+            [ckDetail k | Breaking k <- cs, ckCode k == WqOrderingChanged]+                `shouldSatisfy` any (T.isInfixOf "delivery-order contract")+        it "classifies workqueue provision changes as operational migrations" $ do+            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-provision-change.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WqProvisionChanged]+            [ckDetail k | Breaking k <- cs, ckCode k == WqProvisionChanged]+                `shouldSatisfy` any (T.isInfixOf "migrate the existing queue operationally")+        it "classifies workqueue group-key changes as breaking repartitioning" $ do+            cs <- diffFixtures "test/fixtures/workqueue-policy-base.keiro" "test/fixtures/workqueue-group-key-change.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WqGroupKeyChanged]+            [ckDetail k | Breaking k <- cs, ckCode k == WqGroupKeyChanged]+                `shouldSatisfy` any (T.isInfixOf "re-partitioned")+        it "classifies a process input type change as ProcessInputChanged" $ do+            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-inputtype.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [ProcessInputChanged]+        it "classifies workflow input and output changes as WorkflowShapeChanged" $ do+            input <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-inputfield.keiro"+            [ckCode k | Breaking k <- input] `shouldContain` [WorkflowShapeChanged]+            output <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-output.keiro"+            [ckCode k | Breaking k <- output] `shouldContain` [WorkflowShapeChanged]+        it "classifies workflow relabeling and appends as WorkflowBodyChanged" $ do+            relabeled <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-body.keiro"+            [ckCode k | Breaking k <- relabeled] `shouldContain` [WorkflowBodyChanged]+            appended <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-stepadd.keiro"+            [ckCode k | Breaking k <- appended] `shouldContain` [WorkflowBodyChanged]+            [ckDetail k | Breaking k <- appended, ckCode k == WorkflowBodyChanged]+                `shouldSatisfy` any (T.isInfixOf "new patch guard")+        it "classifies a body addition wholly guarded by a new patch as additive" $ do+            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-evolution-diff.keiro"+            any isBreaking cs `shouldBe` False+            [ckSubject k | Additive k <- cs, ckFacet k == "workflow-patch"] `shouldContain` ["fraud-check-v2"]+            [ckSubject k | Additive k <- cs, ckFacet k == "workflow-continue-as-new"] `shouldContain` ["RolloverSeed"]+        it "classifies removing an existing patch as breaking" $ do+            cs <- diffFixtures "test/fixtures/workflow-evolution-diff.keiro" "test/fixtures/workflow-continue.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WorkflowPatchRemoved]+            [ckDetail k | Breaking k <- cs, ckCode k == WorkflowPatchRemoved]+                `shouldSatisfy` any (T.isInfixOf "cannot prove")+        it "classifies terminal continueAsNew append as additive and seed drift as breaking" $ do+            appended <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-continue.keiro"+            any isBreaking appended `shouldBe` False+            [ckFacet k | Additive k <- appended] `shouldContain` ["workflow-continue-as-new"]+            changed <- diffFixtures "test/fixtures/workflow-continue.keiro" "test/fixtures/workflow-continue-seed-v2.keiro"+            [ckCode k | Breaking k <- changed] `shouldContain` [WorkflowContinueSeedChanged]+            [ckDetail k | Breaking k <- changed, ckCode k == WorkflowContinueSeedChanged]+                `shouldSatisfy` any (T.isInfixOf "restoreSeed")+        it "classifies a workflow stable-name change as WorkflowStableNameChanged" $ do+            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-rename.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [WorkflowStableNameChanged]+        it "classifies workflow id-derivation changes as DerivedIdentityChanged" $ do+            cs <- diffFixtures "test/fixtures/workflow.keiro" "test/fixtures/workflow-idfield.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [DerivedIdentityChanged]+        it "classifies an id prefix change as IdPrefixChanged" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-idprefix.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [IdPrefixChanged]+        it "classifies intake dedupe key and policy changes as DedupeIdentityChanged" $ do+            policy <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-dedupepolicy.keiro"+            [ckCode k | Breaking k <- policy] `shouldContain` [DedupeIdentityChanged]+            key <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-dedupekey.keiro"+            [ckCode k | Breaking k <- key] `shouldContain` [DedupeIdentityChanged]+        it "reports intake decode-posture changes as warnings" $ do+            cs <- diffFixtures "test/fixtures/intake.keiro" "test/fixtures/intake-decode.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [DecodePostureChanged]+            [ckCode k | Advisory k <- cs] `shouldContain` [IntakePersistenceChanged]+        it "classifies process and timer derivation changes as DerivedIdentityChanged" $ do+            processName <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-procname.keiro"+            [ckCode k | Breaking k <- processName] `shouldContain` [DerivedIdentityChanged]+            timerId <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-timerid.keiro"+            [ckCode k | Breaking k <- timerId] `shouldContain` [DerivedIdentityChanged]+            base <- specOf "test/fixtures/hospital-surge.keiro"+            let categoryChange = diffSpecs base (modifyProcess "HospitalSurge" (\process -> process{procSaga = (procSaga process){sagaCategory = "hospitalSurgeV2"}}) base)+            [ckCode k | Breaking k <- categoryChange] `shouldContain` [DerivedIdentityChanged]+        it "classifies router stable names, keys, and targets as identity-bearing" $ do+            base <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            let stableName = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtName = "paging-v2"}) base)+                keyDerivation = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtKey = (rtKey router){corrVia = "otherIdText"}}) base)+                target = diffSpecs base (modifyRouter "PagingRouter" (\router -> router{rtTarget = "OtherPage"}) base)+            [ckCode k | Breaking k <- stableName] `shouldContain` [RouterStableNameChanged]+            [ckCode k | Breaking k <- keyDerivation] `shouldContain` [DerivedIdentityChanged]+            [ckCode k | Breaking k <- target] `shouldContain` [DerivedIdentityChanged]+        it "advises on router dispatch-surface changes without making them breaking" $ do+            cs <- diffFixtures "test/fixtures/incident-paging/incident-paging.keiro" "test/fixtures/incident-paging/incident-paging-dispatch.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldBe` [RouterDecideSurfaceChanged]+        it "advises on process dispatch-surface changes without making them breaking" $ do+            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-handle.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldBe` [ProcessDecideSurfaceChanged]+        it "advises on unversioned timer payload changes without making them breaking" $ do+            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-payload.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldBe` [ProcessTimerPayloadChanged]+        it "ignores formatting-only process and timer surface rewrites" $ do+            original <- specOf "test/fixtures/hospital-surge.keiro"+            formatted <- parseInlineSpec "<formatted-process>" (renderSpec original)+            diffSpecs original formatted `shouldBe` []+        it "reports a timer window change as a warning" $ do+            cs <- diffFixtures "test/fixtures/hospital-surge.keiro" "test/fixtures/hospital-surge-window.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [TimerWindowChanged]+        it "reports emit-map changes as warnings and derive changes as breaking" $ do+            mapping <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-mapchange.keiro"+            any isBreaking mapping `shouldBe` False+            [ckCode k | Advisory k <- mapping] `shouldContain` [EmitMappingChanged]+            derive <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-derive.keiro"+            [ckCode k | Breaking k <- derive] `shouldContain` [DerivedIdentityChanged]+        it "classifies publisher outbox identity and ordering independently" $ do+            outbox <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-outboxfield.keiro"+            [ckCode k | Breaking k <- outbox] `shouldContain` [DerivedIdentityChanged]+            ordering <- diffFixtures "test/fixtures/emit.keiro" "test/fixtures/emit-ordering.keiro"+            any isBreaking ordering `shouldBe` False+            [ckCode k | Advisory k <- ordering] `shouldContain` [PublisherPolicyChanged]+        it "classifies workqueue names as QueueIdentityChanged" $ do+            cs <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-rename.keiro"+            [ckCode k | Breaking k <- cs] `shouldContain` [QueueIdentityChanged]+        it "classifies pgmq dispatch dedupe and retargeting independently" $ do+            dedupe <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-dedupkey.keiro"+            [ckCode k | Breaking k <- dedupe] `shouldContain` [DedupeIdentityChanged]+            retarget <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-retarget.keiro"+            any isBreaking retarget `shouldBe` False+            [ckCode k | Advisory k <- retarget] `shouldContain` [DispatchRetargeted]+        it "reports aggregate projection changes as warnings" $ do+            cs <- diffFixtures "test/fixtures/reservation.keiro" "test/fixtures/reservation-projection.keiro"+            any isBreaking cs `shouldBe` False+            [ckCode k | Advisory k <- cs] `shouldContain` [ProjectionChanged]+        it "classifies read-model version and unversioned shape changes" $ do+            base <- specOf "test/fixtures/readmodel-runtime.keiro"+            let versionTwo = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmVersion = 2}) base+                changedShape = modifyReadModel "transfer_decisions" changeReadModelShape base+                bumpedShape = modifyReadModel "transfer_decisions" (\readModel -> (changeReadModelShape readModel){rmVersion = 2}) base+                decreased = diffSpecs versionTwo base+                unversioned = diffSpecs base changedShape+                bumped = diffSpecs base bumpedShape+            [ckCode k | Breaking k <- decreased] `shouldContain` [ReadModelVersionDecreased]+            [ckCode k | Breaking k <- unversioned] `shouldContain` [ReadModelShapeChangedWithoutBump]+            any isBreaking bumped `shouldBe` False+            [ckFacet k | Additive k <- bumped] `shouldContain` ["read-model-version"]+        it "classifies read-model registry, table, subscription, and removal identities" $ do+            base <- specOf "test/fixtures/readmodel-runtime.keiro"+            let tableChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmTable = "transfer_decisions_v2"}) base+                subscriptionChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmSubscription = Just "transfer-decisions-v2"}) base+                renamed = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmName = "reservation_decisions"}) base+                removed = removeReadModel "transfer_decisions" base+            mapM_+                (\changes -> [ckCode k | Breaking k <- changes] `shouldContain` [DerivedIdentityChanged])+                [diffSpecs base tableChanged, diffSpecs base subscriptionChanged, diffSpecs base renamed, diffSpecs base removed]+        it "classifies read-model feed flips and consistency/scope weakening as breaking" $ do+            base <- specOf "test/fixtures/readmodel-runtime.keiro"+            let feedChanged = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmFeed = RmInline}) base+                consistencyWeakened = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmConsistency = Eventual}) base+                entireLog = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmScope = Just RmEntireLog}) base+            [ckCode k | Breaking k <- diffSpecs base feedChanged] `shouldContain` [ReadModelFeedChanged]+            [ckCode k | Breaking k <- diffSpecs base consistencyWeakened] `shouldContain` [ReadModelConsistencyWeakened]+            [ckCode k | Breaking k <- diffSpecs entireLog base] `shouldContain` [ReadModelConsistencyWeakened]+        it "classifies Eventual to Strong read-model consistency as additive" $ do+            strong <- specOf "test/fixtures/readmodel-runtime.keiro"+            let eventual = modifyReadModel "transfer_decisions" (\readModel -> readModel{rmConsistency = Eventual}) strong+                changes = diffSpecs eventual strong+            any isBreaking changes `shouldBe` False+            [ckFacet k | Additive k <- changes] `shouldContain` ["read-model-consistency"]++    describe "module placement (M1)" $ do+        it "GeneratedPrefix is today's namespace (Generated.<Ctx>.<Node>, holes at <Ctx>.<Node>)" $ do+            let ctx = defaultContext "hospital-capacity"+            genPrefixFor ctx "Reservation" `shouldBe` "Generated.HospitalCapacity.Reservation"+            holePrefixFor ctx "Reservation" `shouldBe` "HospitalCapacity.Reservation"+        it "module-root prefixes both layers" $ do+            let ctx = (defaultContext "hospital-capacity"){moduleRoot = "Acme"}+            genPrefixFor ctx "Reservation" `shouldBe` "Acme.Generated.HospitalCapacity.Reservation"+            holePrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation"+        it "CollocatedLeaf places the generated layer under the domain leaf" $ do+            let ctx = (defaultContext "hospital-capacity"){moduleRoot = "Acme", placement = CollocatedLeaf}+            genPrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation.Generated"+            holePrefixFor ctx "Reservation" `shouldBe` "Acme.HospitalCapacity.Reservation"+        it "parses and preserves the module/layout clauses through parse . pretty" $ do+            let src = "context hospital-capacity\nmodule Acme.Services\nlayout collocated\n\naggregate Reservation\n  regs\n  states Open\n"+            case parseSpec "<m1>" src of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> do+                    specModuleRoot spec `shouldBe` Just "Acme.Services"+                    specLayout spec `shouldBe` Just CollocatedLeaf+                    parseSpec "<m1>" (renderSpec spec) `shouldBe` Right spec+        it "a spec without the clauses leaves placement at the default" $ do+            input <- readTestText "test/fixtures/reservation.keiro"+            case parseSpec "test/fixtures/reservation.keiro" input of+                Left err -> expectationFailure (T.unpack err)+                Right spec -> do+                    specModuleRoot spec `shouldBe` Nothing+                    specLayout spec `shouldBe` Nothing++    describe "structural scaffold" $ do+        it "emits one private shape module per structural declaration and one context facade" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+                paths = map modulePath modules+            paths+                `shouldContain` [ "Generated/ConsumerDemo/Structural/Shape/ArtifactInfo.hs"+                                , "Generated/ConsumerDemo/Structural/Shape/ArtifactKind.hs"+                                , "Generated/ConsumerDemo/Structural/Shape/ArtifactLocation.hs"+                                , "Generated/ConsumerDemo/StructuralProjections.hs"+                                ]+            paths `shouldNotContain` ["Generated/ConsumerDemo/Structural/Shape/VendorGeometry.hs"]+            firewallBreaches modules `shouldBe` []+        it "emits one create-once binding skeleton per owning module and derives Generic for private shapes" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+                skeletons = [moduleValue | moduleValue <- modules, kind moduleValue == HoleStub, modulePath moduleValue == "Example/Artifact/KeiroBindings.hs"]+                shape = generatedTextEndingIn "Structural/Shape/ArtifactInfo.hs" modules+            case skeletons of+                [skeleton] -> do+                    moduleText skeleton `shouldSatisfy` T.isInfixOf "artifactInfoBinding :: StructuralBinding"+                    moduleText skeleton `shouldSatisfy` T.isInfixOf "artifactKindBinding :: StructuralBinding"+                    moduleText skeleton `shouldSatisfy` T.isInfixOf "artifactLocationBinding :: StructuralBinding"+                    moduleText skeleton `shouldSatisfy` T.isInfixOf "HOLE: fill ArtifactInfo bindingToShape.key"+                _ -> expectationFailure ("expected exactly one shared binding skeleton, got " <> show (map modulePath skeletons))+            shape `shouldSatisfy` T.isInfixOf "deriving stock (Eq, Generic, Show)"+            shape `shouldSatisfy` T.isInfixOf "import GHC.Generics (Generic)"+        it "never overwrites an existing binding skeleton" $+            withTempDirectory "keiro-dsl-binding-create-once" $ \out -> do+                spec <- specOf "test/fixtures/consumer-types.keiro"+                let ctx = defaultContext (specContext spec)+                    bindingPath = out </> "Example/Artifact/KeiroBindings.hs"+                _ <- executePlannedScaffold out "consumer-types.keiro" ctx spec+                TIO.writeFile bindingPath "hand-owned binding\n"+                second <- executePlannedScaffold out "consumer-types.keiro" ctx spec+                TIO.readFile bindingPath `shouldReturn` "hand-owned binding\n"+                reportDispositions second+                    `shouldSatisfy` any (\(moduleValue, disposition) -> modulePath moduleValue == "Example/Artifact/KeiroBindings.hs" && disposition == Skipped)+        it "fresh binding skeletons compile at the application boundary" $+            withTempDirectory "keiro-dsl-binding-compiles" $ \out -> do+                spec <- specOf "test/fixtures/structural-conformance.keiro"+                let ctx = defaultContext (specContext spec)+                    bindingSource = out </> "Conformance/Structural/Bindings.hs"+                    ghcOutput = out </> ".ghc"+                _ <- executePlannedScaffold out "structural-conformance.keiro" ctx spec+                createDirectoryIfMissing True ghcOutput+                (exitCode, standardOutput, standardError) <-+                    readProcessWithExitCode+                        "cabal"+                        [ "exec"+                        , "--"+                        , "ghc"+                        , "-XGHC2024"+                        , "-XOverloadedStrings"+                        , "-fno-code"+                        , "-fforce-recomp"+                        , "-outputdir"+                        , ghcOutput+                        , "-i" <> out+                        , "-itest/conformance-structural"+                        , "-i../keiro-core/src"+                        , bindingSource+                        ]+                        ""+                unless (exitCode == ExitSuccess) $+                    expectationFailure (standardOutput <> standardError)+        it "keeps consumer types in Domain while the generated Codec owns keys, tags, and defaults" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+                domain = generatedTextEndingIn "Catalog/Domain.hs" modules+                codec = generatedTextEndingIn "Catalog/Codec.hs" modules+            domain `shouldSatisfy` T.isInfixOf "Example.Artifact.Domain.ArtifactInfo"+            domain `shouldSatisfy` T.isInfixOf "Vendor.Geometry.Geometry"+            domain `shouldSatisfy` T.isInfixOf "Example.Artifact.KeiroBindings.emptyArtifactInfo"+            codec `shouldSatisfy` T.isInfixOf "\"location\" .= encodeArtifactLocationShape"+            codec `shouldSatisfy` T.isInfixOf "\"local_file\""+            codec `shouldSatisfy` T.isInfixOf "Nothing -> pure Generated.ConsumerDemo.Structural.Shape.ArtifactKind.Guide"+            codec `shouldSatisfy` T.isInfixOf "rejectUnknownFields \"ArtifactInfo\""+            codec `shouldSatisfy` T.isInfixOf "toJSON payload.geometry"+            codec `shouldSatisfy` (not . T.isInfixOf "vendor.geometry.json")+        it "generates shape-only nested types and schema-derived Keiki witnesses" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+                shape = generatedTextEndingIn "Structural/Shape/ArtifactInfo.hs" modules+                facade = generatedTextEndingIn "StructuralProjections.hs" modules+            shape `shouldSatisfy` T.isInfixOf "data ArtifactInfoShape = ArtifactInfo"+            shape `shouldSatisfy` T.isInfixOf "ArtifactKind.ArtifactKindShape"+            shape `shouldSatisfy` (not . T.isInfixOf "KeiroBindings")+            facade `shouldSatisfy` T.isInfixOf "type FieldName"+            facade `shouldSatisfy` T.isInfixOf "= \"/key\""+            facade `shouldSatisfy` T.isInfixOf "fieldShapeId _ = \"example.artifact.ArtifactInfo.v1\""+            facade `shouldSatisfy` T.isInfixOf "bindingToShape Example.Artifact.KeiroBindings.artifactInfoBinding owner"++    describe "structural manifest" $ do+        it "lists consumer packages and every domain, binding, fixture, and initial module" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+                manifest = renderManifest "consumer-types.keiro" modules spec+            mapM_ (\packageName -> manifestDependencies spec `shouldContain` [packageName]) ["artifact-domain", "vendor-geometry"]+            manifest `shouldSatisfy` T.isInfixOf "consumer-packages:\n    artifact-domain\n    vendor-geometry"+            mapM_+                (\moduleName -> manifest `shouldSatisfy` T.isInfixOf moduleName)+                [ "Example.Artifact.Domain"+                , "Example.Artifact.KeiroBindings"+                , "Vendor.Geometry"+                , "Vendor.Geometry.KeiroBindings"+                ]++    describe "structural scaffold record" $ do+        it "round-trips canonical mapping rows and reports binding drift on the next run" $+            withTempDirectory "keiro-dsl-mapping-record" $ \out -> do+                spec <- specOf "test/fixtures/consumer-types.keiro"+                let ctx = defaultContext (specContext spec)+                first <- executePlannedScaffold out "consumer-types.keiro" ctx spec+                length (consumerMappings (reportConsumerPlan first)) `shouldBe` 4+                recordText <- TIO.readFile (out </> recordFileName (specContext spec))+                let mappingRows = filter (T.isPrefixOf "mapping ") (T.lines recordText)+                    bindingRows = filter (T.isPrefixOf "binding ") (T.lines recordText)+                length mappingRows `shouldBe` 4+                bindingRows `shouldSatisfy` (not . null)+                fmap recMappings (parseRecord recordText) `shouldSatisfy` maybe False ((== 4) . length)+                fmap recBindingObligations (parseRecord recordText) `shouldSatisfy` maybe False ((== length bindingRows) . length)+                let bumped = spec{specMapped = map bumpArtifactBindingVersion (specMapped spec)}+                second <- executePlannedScaffold out "consumer-types.keiro" ctx bumped+                reportMappingDrift second+                    `shouldSatisfy` any (\drift -> driftSpecName drift == "ArtifactInfo" && driftPrevious drift /= driftCurrent drift)+                renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "mapping drift:")+                case mappingRows of+                    row : _ -> parseRecord (recordText <> row <> "\n") `shouldBe` Nothing+                    [] -> expectationFailure "expected mapping rows"+                case bindingRows of+                    row : _ -> parseRecord (recordText <> row <> "\n") `shouldBe` Nothing+                    [] -> expectationFailure "expected binding rows"+        it "reports exactly the newly added binding field without rewriting the shared skeleton" $+            withTempDirectory "keiro-dsl-binding-drift" $ \out -> do+                spec <- specOf "test/fixtures/consumer-types.keiro"+                let ctx = defaultContext (specContext spec)+                _ <- executePlannedScaffold out "consumer-types.keiro" ctx spec+                let extended = spec{specMapped = map addArtifactSummaryField (specMapped spec)}+                second <- executePlannedScaffold out "consumer-types.keiro" ctx extended+                reportNewHoles second+                    `shouldBe` [ BindingHole+                                    { holeMappedName = "ArtifactInfo"+                                    , holeModule = "Example.Artifact.KeiroBindings"+                                    , holeSymbol = "artifactInfoBinding"+                                    , holeKind = BindingValue+                                    , holePath = Just "summary"+                                    , holeSignature = "artifactInfoBinding.summary :: Text"+                                    }+                               ]+                renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "artifactInfoBinding.summary :: Text")+        it "rejects malformed known mapping JSON while ignoring unrelated future rows" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            withTempDirectory "keiro-dsl-mapping-malformed" $ \out -> do+                report <- executePlannedScaffold out "consumer-types.keiro" (defaultContext (specContext spec)) spec+                recordText <- TIO.readFile (reportRecordPath report)+                parseRecord (recordText <> "mapping {not-json}\n") `shouldBe` Nothing+                parseRecord (recordText <> "future-row retained\n") `shouldBe` parseRecord recordText++    describe "structural import plan" $ do+        it "reports the successful dependency plan in the scaffold report" $+            withTempDirectory "keiro-dsl-dependency-plan" $ \out -> do+                spec <- specOf "test/fixtures/consumer-types.keiro"+                report <- executePlannedScaffold out "consumer-types.keiro" (defaultContext (specContext spec)) spec+                renderScaffoldReport report+                    `shouldSatisfy` any (T.isInfixOf "dependency plan: consumer packages [artifact-domain, vendor-geometry]")+        it "refuses a binding module inside the generated namespace with the exact cycle" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let cyclic = spec{specMapped = map moveArtifactBindingIntoGenerated (specMapped spec)}+            case planScaffold (defaultContext (specContext cyclic)) cyclic of+                Left refusals -> do+                    refusals `shouldSatisfy` any isImportCycle+                    renderRefusals refusals `shouldSatisfy` any (T.isInfixOf "Generated.ConsumerDemo.Bindings")+                Right _ -> expectationFailure "expected an import-cycle refusal"+        it "refuses missing mapped register initials but permits command/event-only use" $ do+            missing <- specOf "test/fixtures/mapped-missing-initial.keiro"+            planScaffold (defaultContext (specContext missing)) missing `shouldSatisfy` isLoweringRefusal+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let commandOnly = removeMappedRegisterRequirements spec+            planScaffold (defaultContext (specContext commandOnly)) commandOnly `shouldSatisfy` isRight++    describe "binding explanations" $ do+        it "lists binding, fixture, and use-site-scoped initial obligations deterministically" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligations spec)+            length obligations `shouldBe` 7+            obligations+                `shouldSatisfy` any+                    ( \obligation ->+                        obligationKind obligation == BindingValue+                            && obligationSymbol obligation == "artifactInfoBinding"+                            && obligationBindingVersion obligation == Just "1"+                    )+            obligations+                `shouldSatisfy` any+                    ( \obligation ->+                        obligationKind obligation == InitialValue+                            && obligationSymbol obligation == "emptyArtifactInfo"+                            && any (T.isInfixOf "Catalog register currentArtifact") (obligationUseSites obligation)+                    )+            let rendered = renderBindingObligations (specContext spec) obligations+            rendered `shouldSatisfy` T.isInfixOf "binding obligations for context consumer-demo"+            rendered `shouldSatisfy` T.isInfixOf "artifactInfoBinding :: StructuralBinding Example.Artifact.Domain.ArtifactInfo ArtifactInfoShape"+            rendered `shouldSatisfy` T.isInfixOf "provenance: binding-version \"1\""+        it "states explicitly when a spec has no structural obligations" $ do+            spec <- specOf "test/fixtures/reservation.keiro"+            obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligations spec)+            renderBindingObligations (specContext spec) obligations+                `shouldBe` "no binding obligations for context hospital-capacity"++    describe "exact generic structural bindings" $ do+        forM_+            [ ("renamed-field", "selector mismatch")+            , ("reordered-field", "selector mismatch")+            , ("arity-mismatch", "no exact nominal correspondence")+            , ("incompatible-type", "no exact nominal correspondence")+            ]+            $ \(fixture, diagnostic) ->+                it ("rejects " <> fixture <> " and directs the author to the scaffolded module") $+                    expectGenericCompileFailure fixture diagnostic++    describe "structural harness" $ do+        it "emits every structural, wire-policy, projection, and replay assertion family" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let aggregate = onlyAggregate spec+                ctx = defaultContext (specContext spec)+                harness = generatedTextEndingIn "Harness.hs" (harnessFor ctx spec aggregate)+            mapM_+                (\needle -> harness `shouldSatisfy` T.isInfixOf needle)+                [ "binding domain round-trip: example.artifact.ArtifactInfo.v1/"+                , "binding shape round-trip: example.artifact.ArtifactInfo.v1/"+                , "mapped codec round-trip: ArtifactObserved/artifact/"+                , "fixture coverage: example.artifact.ArtifactLocation.v1"+                , "wire policy missing default: example.artifact.ArtifactInfo.v1/description"+                , "wire policy explicit null: example.artifact.ArtifactInfo.v1/description"+                , "wire policy unknown fields: example.artifact.ArtifactInfo.v1"+                , "wire union arm: example.artifact.ArtifactLocation.v1/local_file"+                , "canonical identity: example.artifact.ArtifactInfo.v1"+                , "projection witness agreement: example.artifact.ArtifactInfo.v1/key"+                , "forward/replay equality: ObserveArtifact from CatalogEmpty -- "+                , "register currentArtifact"+                ]+        it "keeps opaque assertions at the declared codec boundary" $ do+            spec <- specOf "test/fixtures/consumer-types.keiro"+            let aggregate = onlyAggregate spec+                ctx = defaultContext (specContext spec)+                modules = scaffoldAggregate ctx spec aggregate <> harnessFor ctx spec aggregate+                harness = generatedTextEndingIn "Harness.hs" modules+                codec = generatedTextEndingIn "Codec.hs" modules+            harness `shouldSatisfy` T.isInfixOf "opaque codec round-trip: vendor.geometry.json@3/"+            harness `shouldNotSatisfy` T.isInfixOf "wire policy unknown fields: vendor.geometry.json"+            harness `shouldNotSatisfy` T.isInfixOf "fixture coverage: vendor.geometry"+            codec `shouldNotSatisfy` T.isInfixOf "encodeVendorGeometryShape"++    describe "manifest (M2)" $ do+        it "lists exactly the modules the scaffolder produced" $ do+            mods <- scaffoldFixture "test/fixtures/reservation.keiro"+            spec <- specOf "test/fixtures/reservation.keiro"+            let manifest = renderManifest "reservation.keiro" mods spec+                expectedNames = sort (map (moduleNameOf . modulePath) mods)+            -- every produced module name appears in the manifest…+            mapM_ (\m -> (m `T.isInfixOf` manifest) `shouldBe` True) expectedNames+            -- …and the module list is exactly the scaffolder's output set.+            expectedNames+                `shouldBe` sort+                    [ "Generated.HospitalCapacity.Reservation.Codec"+                    , "Generated.HospitalCapacity.Reservation.Domain"+                    , "Generated.HospitalCapacity.Reservation.EventStream"+                    , "Generated.HospitalCapacity.Reservation.Harness"+                    , "Generated.HospitalCapacity.Reservation.Projection"+                    , "HospitalCapacity.Reservation.Holes"+                    ]+        it "derives the dependency set from the node kinds present (aggregate)" $ do+            spec <- specOf "test/fixtures/reservation.keiro"+            manifestDependencies spec `shouldBe` ["aeson", "base", "keiki", "keiro", "text"]+        it "derives the process dependency set, including worker-policy runtime imports" $ do+            spec <- specOf "test/fixtures/hospital-surge.keiro"+            let dependencies = manifestDependencies spec+            mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["time", "uuid", "shibuya-core", "keiki", "keiro"]+        it "uses the registered shibuya-core package name for router scaffolds" $ do+            spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+            let dependencies = manifestDependencies spec+            mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["effectful-core", "keiro", "shibuya-core"]+            dependencies `shouldNotContain` ["shibuya"]++    describe "new <kind> skeletons (M5)" $ do+        it "every skeleton parses and validates with zero error diagnostics" $+            mapM_ assertSkeletonValid skeletonKinds+        it "every skeleton passes the scaffold refusal gates" $+            mapM_ assertSkeletonScaffoldable skeletonKinds+        it "fresh skeleton scaffolds match the committed compiling modules" $+            mapM_ (uncurry assertSkeletonMatchesCommitted) skeletonModuleRoots+        it "rejects an unknown kind with a helpful message" $+            case skeletonFor "bogus" of+                Left msg -> ("Valid kinds:" `T.isInfixOf` msg) `shouldBe` True+                Right _ -> expectationFailure "expected an error for an unknown kind"++    describe "firewall self-check (M3)" $ do+        it "flags a forbidden operator in a Generated module" $ do+            let m = ScaffoldModule{modulePath = "Gen/Foo.hs", moduleText = "x = a ./= b", kind = Generated, origin = "test"}+            firewallBreaches [m] `shouldBe` [("Gen/Foo.hs", "./=", 1)]+        it "ignores forbidden operators in a HoleStub module (holes own them)" $ do+            let m = ScaffoldModule{modulePath = "Foo/Holes.hs", moduleText = "x = lit 1 .== y", kind = HoleStub, origin = "test"}+            firewallBreaches [m] `shouldBe` []+        it "matches `lit` as a word, not a substring of quality/split" $ do+            let clean = ScaffoldModule{modulePath = "Gen/Q.hs", moduleText = "quality = split facility", kind = Generated, origin = "test"}+                dirty = ScaffoldModule{modulePath = "Gen/L.hs", moduleText = "v = lit foo", kind = Generated, origin = "test"}+            firewallBreaches [clean] `shouldBe` []+            firewallBreaches [dirty] `shouldBe` [("Gen/L.hs", "lit", 1)]+        it "skips strings and comments and maximal-munches symbolic tokens" $ do+            let clean = syntheticGenerated "Gen/Clean.hs" "wire = \"lit .== B.slot\"\n-- x =: y\nx = a .<= b"+                dirty = syntheticGenerated "Gen/Dirty.hs" "x = a .< b\ny = c =: d"+            firewallBreaches [clean] `shouldBe` [("Gen/Clean.hs", ".<=", 3)]+            firewallBreaches [dirty] `shouldBe` [("Gen/Dirty.hs", ".<", 1), ("Gen/Dirty.hs", "=:", 2)]+        it "guards keiki imports while allowing the generated Core allowlist" $ do+            let forbidden = syntheticGenerated "Gen/Builder.hs" "import Keiki.Builder"+                restricted = syntheticGenerated "Gen/CoreBad.hs" "import Keiki.Core (lit)"+                allowed = syntheticGenerated "Gen/CoreGood.hs" "import Keiki.Core (RegFile (..), HsPred, step)"+            firewallBreaches [forbidden] `shouldBe` [("Gen/Builder.hs", "import:Keiki.Builder", 1)]+            firewallBreaches [restricted] `shouldBe` [("Gen/CoreBad.hs", "import:Keiki.Core", 1)]+            firewallBreaches [allowed] `shouldBe` []+        it "finds no breach in real scaffolder output (aggregate + process fixtures)" $ do+            aggMods <- scaffoldFixture "test/fixtures/reservation.keiro"+            procMods <- scaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+            firewallBreaches (aggMods <> procMods) `shouldBe` []++    describe "scaffold gates" $ do+        it "refuses duplicate and case-folded module paths with both origins" $ do+            spec <- specOf "test/fixtures/reservation.keiro"+            case [aggregate | NAggregate aggregate <- specNodes spec] of+                aggregate : _ -> do+                    let duplicate = spec{specNodes = [NAggregate aggregate, NAggregate aggregate]}+                        caseVariant = spec{specNodes = [NAggregate aggregate, NAggregate aggregate{aggName = T.toUpper (aggName aggregate)}]}+                    planScaffold (defaultContext (specContext spec)) duplicate `shouldSatisfy` hasPathCollisionWithTwoOrigins+                    planScaffold (defaultContext (specContext spec)) caseVariant `shouldSatisfy` hasPathCollisionWithTwoOrigins+                [] -> expectationFailure "reservation fixture has no aggregate"+        it "refuses a bannerless Generated target without changing its bytes" $+            withTempDirectory "keiro-dsl-banner" $ \out -> do+                spec <- specOf "test/fixtures/reservation.keiro"+                let ctx = defaultContext (specContext spec)+                case planScaffold ctx spec of+                    Left refusals -> expectationFailure ("unexpected planning refusal: " <> show refusals)+                    Right modules -> case [m | m <- modules, kind m == Generated] of+                        generated : _ -> do+                            let target = out </> modulePath generated+                            createDirectoryIfMissing True (takeDirectory target)+                            TIO.writeFile target "hand owned\n"+                            result <- executeScaffold out False "test/fixtures/reservation.keiro" ctx spec modules+                            result `shouldSatisfy` isMissingBannerRefusal+                            TIO.readFile target `shouldReturn` "hand owned\n"+                            forced <- executeScaffold out True "test/fixtures/reservation.keiro" ctx spec modules+                            forced `shouldSatisfy` isSuccessfulScaffold+                            TIO.readFile target `shouldReturn` moduleText generated+                        [] -> expectationFailure "reservation scaffold has no Generated module"+        it "reports renamed-node modules as stale without deleting them" $+            withTempDirectory "keiro-dsl-stale-rename" $ \out -> do+                spec <- parseInlineSpec "<stale-rename>" loweringAggregateSpec+                first <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext spec)) spec+                let renamed = spec{specNodes = map renameCounter (specNodes spec)}+                second <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext renamed)) renamed+                let oldDomain = onlyPathEndingIn "Counter/Domain.hs" (map fst (reportDispositions first))+                    oldHoles = onlyPathEndingIn "Counter/Holes.hs" (map fst (reportDispositions first))+                reportStale second `shouldSatisfy` \stale -> StaleModule Generated oldDomain `elem` stale && StaleModule HoleStub oldHoles `elem` stale+                doesFileExist (out </> oldDomain) `shouldReturn` True+                doesFileExist (out </> oldHoles) `shouldReturn` True+        it "reports the entire old tree across a module-root flip" $+            withTempDirectory "keiro-dsl-stale-root" $ \out -> do+                spec <- parseInlineSpec "<stale-root>" loweringAggregateSpec+                let initialCtx = defaultContext (specContext spec)+                    rootedCtx = initialCtx{moduleRoot = "Acme"}+                first <- executePlannedScaffold out "counter.keiro" initialCtx spec+                second <- executePlannedScaffold out "moved-counter.keiro" rootedCtx spec+                reportStale second+                    `shouldMatchList` [StaleModule (kind m) (modulePath m) | (m, _) <- reportDispositions first]+                forM_ (reportStale second) $ \stale -> doesFileExist (out </> stalePath stale) `shouldReturn` True+                renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "previous scaffold record used spec counter.keiro")+        it "reports moved generated modules across a layout flip" $+            withTempDirectory "keiro-dsl-stale-layout" $ \out -> do+                spec <- parseInlineSpec "<stale-layout>" loweringAggregateSpec+                let initialCtx = defaultContext (specContext spec)+                    collocatedCtx = initialCtx{placement = CollocatedLeaf}+                first <- executePlannedScaffold out "counter.keiro" initialCtx spec+                second <- executePlannedScaffold out "counter.keiro" collocatedCtx spec+                let oldGenerated = [StaleModule Generated (modulePath m) | (m, _) <- reportDispositions first, kind m == Generated]+                reportStale second `shouldSatisfy` all (`elem` oldGenerated)+                length (reportStale second) `shouldBe` length oldGenerated+        it "writes a parseable record and no stale section for a fresh output" $+            withTempDirectory "keiro-dsl-record" $ \out -> do+                spec <- parseInlineSpec "<fresh-record>" loweringAggregateSpec+                let ctx = defaultContext (specContext spec)+                report <- executePlannedScaffold out "counter.keiro" ctx spec+                reportStale report `shouldBe` []+                renderScaffoldReport report `shouldSatisfy` all (not . T.isPrefixOf "stale:")+                contents <- TIO.readFile (out </> recordFileName (specContext spec))+                parseRecord contents+                    `shouldBe` Just+                        ScaffoldRecord+                            { recSpecPath = "counter.keiro"+                            , recModuleRoot = ""+                            , recLayout = "prefixed"+                            , recFiles = [(kind m, modulePath m) | (m, _) <- reportDispositions report]+                            , recMappings = []+                            , recBindingObligations = []+                            }+                parseRecord (T.replace "spec: " "future-field: retained\nspec: " contents) `shouldBe` parseRecord contents+                parseRecord (T.replace "record v1" "record v2" contents) `shouldBe` Nothing++    describe "faithful scaffold lowering" $ do+        it "escapes a trailing-backslash payload literal exactly once" $ do+            spec <- specOf "test/fixtures/hospital-surge.keiro"+            case [process | NProcess process <- specNodes spec] of+                process : _ -> do+                    let timer = (procTimer process){tmPayload = [FieldBinding "kind" (Just "\"follow-up\\\"")]}+                        modules = scaffoldProcess (defaultContext (specContext spec)) process{procTimer = timer}+                    generatedTextEndingIn "Process.hs" modules+                        `shouldSatisfy` T.isInfixOf "\"kind\" .= (\"follow-up\\\\\" :: Value)"+                [] -> expectationFailure "hospital-surge fixture has no process"+        it "preserves quoted Text register initials and refuses unsafe register shapes" $ do+            spec <- parseInlineSpec "<register-initials>" loweringAggregateSpec+            let modules = scaffoldAggregate (defaultContext (specContext spec)) spec =<< [aggregate | NAggregate aggregate <- specNodes spec]+                domain = generatedTextEndingIn "Domain.hs" modules+            domain `shouldSatisfy` T.isInfixOf "RCons (Proxy @\"note\") \"hello world\""+            scaffoldRefusals spec `shouldBe` []+            bare <- parseInlineSpec "<bare-text-initial>" (T.replace "\"hello world\"" "hello" loweringAggregateSpec)+            scaffoldRefusals bare `shouldSatisfy` any (T.isInfixOf "RegTextInitialNotQuoted")+            unsupported <- parseInlineSpec "<unsupported-field>" (T.replace "count:Int" "count:Time" loweringAggregateSpec)+            scaffoldRefusals unsupported `shouldSatisfy` any (T.isInfixOf "FieldTypeUnrepresentable")+        it "lowers seconds, minutes, hours, and both backoff constructors faithfully" $ do+            windowSeconds "90s" `shouldBe` Right 90+            windowSeconds "5m" `shouldBe` Right 300+            windowSeconds "2h" `shouldBe` Right 7200+            emitSource <- readTestText "test/fixtures/emit.keiro"+            let exponentialSource = T.replace "backoff constant 2s" "backoff exponential 2s max=60s multiplier=2.0" emitSource+            exponential <- parseInlineSpec "<exponential-backoff>" exponentialSource+            case [publisher | NPublisher publisher <- specNodes exponential] of+                publisher : _ -> do+                    let generated = generatedTextEndingIn "Publisher.hs" (scaffoldPublisher (defaultContext (specContext exponential)) publisher)+                    generated `shouldSatisfy` T.isInfixOf "ExponentialBackoff ExponentialBackoffOptions { initial = 2, maxDelay = 60, multiplier = 2.0 }"+                    parseSpec "<exponential-round-trip>" (renderSpec exponential) `shouldBe` Right exponential+                [] -> expectationFailure "emit fixture has no publisher"+            constant <- parseInlineSpec "<constant-backoff>" (T.replace "backoff constant 2s" "backoff constant 2m" emitSource)+            case [publisher | NPublisher publisher <- specNodes constant] of+                publisher : _ -> generatedTextEndingIn "Publisher.hs" (scaffoldPublisher (defaultContext (specContext constant)) publisher) `shouldSatisfy` T.isInfixOf "ConstantBackoff 120"+                [] -> expectationFailure "emit fixture has no publisher"+        it "refuses incomplete exponential backoff and rejects unknown window units" $ do+            emitSource <- readTestText "test/fixtures/emit.keiro"+            incomplete <- parseInlineSpec "<incomplete-backoff>" (T.replace "backoff constant 2s" "backoff exponential 2s" emitSource)+            scaffoldRefusals incomplete `shouldSatisfy` any (T.isInfixOf "BackoffExponentialIncomplete")+            parseSpec "<bad-window>" (T.replace "backoff constant 2s" "backoff constant 2x" emitSource)+                `shouldSatisfy` leftContains "time unit: s, m, or h"+        it "lowers workqueue retry windows in minutes to seconds" $ do+            queueSource <- readTestText "test/fixtures/reservation-work.keiro"+            queueSpec <- parseInlineSpec "<minute-queue>" (T.replace "5s" "5m" queueSource)+            case [workqueue | NWorkqueue workqueue <- specNodes queueSpec] of+                workqueue : _ -> do+                    let policy = generatedTextEndingIn "QueuePolicy.hs" (scaffoldWorkqueue (defaultContext (specContext queueSpec)) workqueue)+                    policy `shouldSatisfy` T.isInfixOf "defaultRetryDelay = RetryDelay 300"+                    policy `shouldSatisfy` T.isInfixOf "Retry (RetryDelay 300)"+                [] -> expectationFailure "queue fixture has no workqueue"+        it "uses exact status-map keys and emits total Int harness samples" $ do+            statusSpec <- parseInlineSpec "<exact-status>" exactStatusSpec+            case [aggregate | NAggregate aggregate <- specNodes statusSpec] of+                aggregate : _ -> do+                    let ctx = defaultContext (specContext statusSpec)+                        projection = generatedTextEndingIn "Projection.hs" (scaffoldAggregate ctx statusSpec aggregate)+                        harness = generatedTextEndingIn "Harness.hs" (harnessFor ctx statusSpec aggregate)+                    projection `shouldSatisfy` T.isInfixOf "ReservationUnHeld {} -> Just \"available\""+                    harness `shouldSatisfy` T.isInfixOf "CountBumpedData 0"+                    harness `shouldNotSatisfy` T.isInfixOf "sample: unsupported"+                [] -> expectationFailure "exact-status spec has no aggregate"++    describe "scaffold" $ do+        it "synthesizes the exact old wire shape and embeds it in the harness" $ do+            oldSpec <- specOf "test/fixtures/reservation.keiro"+            newSpec <- specOf "test/fixtures/reservation-v2.keiro"+            case goldensForDiff oldSpec newSpec of+                [golden] -> do+                    goldenRelativePath golden+                        `shouldBe` "hospital-capacity/Reservation/TransferReservationCreated.v1.json"+                    goldenJson golden+                        `shouldBe` "{\"commandId\":\"cmd_01hzy3v7q2e8kaw2m5x0d41n9c\",\"divertStatus\":\"open\",\"hospitalId\":\"hosp_01hzy3v7q2e8kaw2m5x0d41n9c\",\"kind\":\"TransferReservationCreated\",\"lifeCriticalOverride\":true,\"patientAcuity\":\"red\",\"reservationId\":\"rsv_01hzy3v7q2e8kaw2m5x0d41n9c\"}\n"+                    goldenEvidence golden `shouldBe` SynthesizedWeakStandIn+                    let aggregate = onlyAggregate newSpec+                        modules =+                            harnessForWithGoldens+                                [golden]+                                (defaultContext (specContext newSpec))+                                newSpec+                                aggregate+                        harness = generatedTextEndingIn "Harness.hs" modules+                    harness `shouldSatisfy` T.isInfixOf "golden TransferReservationCreated.v1 decodes"+                    harness `shouldSatisfy` T.isInfixOf "\\\"reservationId\\\":\\\"rsv_"+                    harness `shouldSatisfy` (not . T.isInfixOf "current-shape stand-in")+                goldens -> expectationFailure ("expected one synthesized golden, got " <> show goldens)+        it "synthesizes complete nested mapped old shapes deterministically and never overwrites captured evidence" $ do+            oldSpec <- specOf "test/fixtures/consumer-types.keiro"+            newSpec <- specOf "test/fixtures/consumer-types-v2.keiro"+            case goldensForDiff oldSpec newSpec of+                [golden] -> do+                    goldenEvidence golden `shouldBe` SynthesizedWeakStandIn+                    goldenJson golden `shouldSatisfy` T.isInfixOf "\"artifact\":{"+                    goldenJson golden `shouldSatisfy` T.isInfixOf "\"location\":{\"contents\":\"sample\",\"tag\":\"local_file\"}"+                    goldenJson golden `shouldSatisfy` T.isInfixOf "\"labels\":[\"sample\"]"+                    goldenJson golden `shouldSatisfy` T.isInfixOf "\"revision\":1"+                    goldenJson golden `shouldSatisfy` T.isInfixOf "\"observedAt\":\"2026-01-01T00:00:00Z\""+                    goldensForDiff oldSpec newSpec `shouldBe` [golden]+                    withTempDirectory "keiro-golden-preserve" $ \root -> do+                        let target = root </> goldenRelativePath golden+                        createDirectoryIfMissing True (takeDirectory target)+                        TIO.writeFile target "hand captured\n"+                        emitGoldenPayloads root oldSpec newSpec `shouldReturn` []+                        TIO.readFile target `shouldReturn` "hand captured\n"+                    withTempDirectory "keiro-golden-write" $ \root -> do+                        let target = root </> goldenRelativePath golden+                        emitGoldenPayloads root oldSpec newSpec `shouldReturn` [target]+                        TIO.readFile target `shouldReturn` goldenJson golden+                goldens -> expectationFailure ("expected one nested synthesized golden, got " <> show goldens)+        it "dispatches shared-version upcasters by wire event type and passes foreign kinds through" $ do+            source <- readTestText "test/fixtures/reservation-dup-upcast-source.keiro"+            spec <- parseInlineSpec "<shared-upcaster-source>" source+            case [aggregate | NAggregate aggregate <- specNodes spec] of+                [aggregate] -> do+                    let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate+                        codec = generatedTextEndingIn "Codec.hs" modules+                        holes = case [moduleText m | m <- modules, "Holes.hs" `T.isSuffixOf` T.pack (modulePath m)] of+                            [text] -> text+                            _ -> ""+                    codec `shouldSatisfy` T.isInfixOf "upcasters = [(1, upcastRungV1)]"+                    codec `shouldSatisfy` T.isInfixOf "upcastRungV1 (EventType \"TransferReservationCreated\") value = upcastTransferReservationCreatedV1 value"+                    codec `shouldSatisfy` T.isInfixOf "upcastRungV1 (EventType \"TransferReservationConfirmed\") value = upcastTransferReservationConfirmedV1 value"+                    codec `shouldSatisfy` T.isInfixOf "upcastRungV1 _ value = Right value"+                    holes `shouldSatisfy` T.isInfixOf "receives ONLY TransferReservationCreated payloads"+                _ -> expectationFailure "expected exactly one aggregate"+        it "keeps foreign payloads byte-for-byte and invokes both same-rung event upcasters" $ do+            let payloadA = object ["kind" .= ("AmountScaled" :: T.Text), "amount" .= (2 :: Int)]+                payloadB = object ["kind" .= ("AmountRenamed" :: T.Text), "amount" .= (3 :: Int)]+                foreignPayload = object ["kind" .= ("AmountObserved" :: T.Text), "amount" .= (7 :: Int)]+                upcastA _ = Right (object ["kind" .= ("AmountScaled" :: T.Text), "amount" .= (200 :: Int)])+                upcastB _ = Right (object ["kind" .= ("AmountRenamed" :: T.Text), "amountInCents" .= (300 :: Int)])+                rung (EventType "AmountScaled") = upcastA+                rung (EventType "AmountRenamed") = upcastB+                rung _ = Right+                codec =+                    Codec+                        { eventTypes = EventType "AmountScaled" :| [EventType "AmountRenamed", EventType "AmountObserved"]+                        , eventType = const (EventType "AmountObserved")+                        , schemaVersion = 2+                        , encode = id+                        , decode = \_ -> Right+                        , upcasters = [(1, rung)]+                        } ::+                        Codec Value+            decodeRaw codec (EventType "AmountObserved") 1 foreignPayload `shouldBe` Right foreignPayload+            decodeRaw codec (EventType "AmountScaled") 1 payloadA+                `shouldBe` Right (object ["kind" .= ("AmountScaled" :: T.Text), "amount" .= (200 :: Int)])+            decodeRaw codec (EventType "AmountRenamed") 1 payloadB+                `shouldBe` Right (object ["kind" .= ("AmountRenamed" :: T.Text), "amountInCents" .= (300 :: Int)])+        it "never emits a keiki symbolic operator into a Generated module (firewall)" $ do+            mods <- scaffoldFixture "test/fixtures/reservation.keiro"+            firewallBreaches mods `shouldBe` []+        it "marks the Holes module HoleStub and the rest Generated" $ do+            mods <- scaffoldFixture "test/fixtures/reservation.keiro"+            let holes = [m | m <- mods, "Holes.hs" `T.isSuffixOf` T.pack (modulePath m)]+            map kind holes `shouldBe` [HoleStub]+            -- Domain, Codec, EventStream, Projection, Harness.+            length [m | m <- mods, kind m == Generated] `shouldBe` 5+        it "is deterministic (re-scaffolding yields byte-identical text)" $ do+            a <- scaffoldFixture "test/fixtures/reservation.keiro"+            b <- scaffoldFixture "test/fixtures/reservation.keiro"+            map moduleText a `shouldBe` map moduleText b+        it "keeps retiring as validator-only metadata in generated modules" $ do+            ordinary <- scaffoldFixture "test/fixtures/reservation.keiro"+            retiring <- scaffoldFixture "test/fixtures/reservation-retiring.keiro"+            map (\m -> (modulePath m, kind m, moduleText m)) retiring+                `shouldBe` map (\m -> (modulePath m, kind m, moduleText m)) ordinary+        it "matches the committed compiling Generated conformance modules (modulo whitespace)" $ do+            mods <- scaffoldFixture "test/fixtures/reservation.keiro"+            mapM_ assertMatchesCommitted [m | m <- mods, kind m == Generated]+        it "matches every committed new-surface Generated module (modulo formatting)" $ do+            spec <- specOf "test/fixtures/transfer-routing.keiro"+            let modules = scaffoldModules (defaultContext (specContext spec)) spec+            forM_ [m | m <- modules, kind m == Generated] $ \m -> do+                committed <- readTestText ("test/conformance-newsurface/" <> modulePath m)+                normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)+        it "scaffolds the register-free OrderStream smoke target without error" $ do+            mods <- scaffoldFixture "test/fixtures/order.keiro"+            -- 5 Generated (Domain/Codec/EventStream/Projection/Harness) + 1 Holes.+            length mods `shouldBe` 6+            firewallBreaches mods `shouldBe` []+            let harness = generatedTextEndingIn "Harness.hs" mods+            harness `shouldSatisfy` T.isInfixOf "prefix = \"forward/replay equality: PlaceOrder from OrderNotStarted -- \""+            harness `shouldSatisfy` T.isInfixOf "prefix <> \"final vertex\""+            harness `shouldNotSatisfy` T.isInfixOf "prefix <> \"register "+        it "emits forward/replay checks with field-distinct Text samples" $ do+            spec <- parseInlineSpec "<forward-replay-samples>" (T.replace "command Bump { count:Int }" "command Bump { count:Int noteText:Text echo:Text }" loweringAggregateSpec)+            case [aggregate | NAggregate aggregate <- specNodes spec] of+                aggregate : _ -> do+                    let ctx = defaultContext (specContext spec)+                        harness = generatedTextEndingIn "Harness.hs" (harnessFor ctx spec aggregate)+                    harness `shouldSatisfy` T.isInfixOf "\"sample-noteText\" \"sample-echo\""+                    harness `shouldSatisfy` T.isInfixOf "prefix = \"forward/replay equality: Bump from CounterPending -- \""+                    harness `shouldSatisfy` T.isInfixOf "prefix <> \"register note\""+                [] -> expectationFailure "forward/replay sample spec has no aggregate"+        it "emits the canonical reservation register checks" $ do+            mods <- scaffoldFixture "test/fixtures/reservation.keiro"+            let harness = generatedTextEndingIn "Harness.hs" mods+            harness `shouldSatisfy` T.isInfixOf "prefix = \"forward/replay equality: RequestTransferReservation from ReservationUnrequested -- \""+            harness `shouldSatisfy` T.isInfixOf "prefix <> \"register reservationState\""+        it "lowers a replay-only transition to B.replayOnly in the holes skeleton (plan 143)" $ do+            twinMods <- scaffoldFixture "test/fixtures/reservation-guard-tightened-twin.keiro"+            let twinHoles = [moduleText m | m <- twinMods, kind m == HoleStub]+            twinHoles `shouldSatisfy` any (T.isInfixOf "B.replayOnly")+            let twinHarness = generatedTextEndingIn "Harness.hs" twinMods+            T.count "forwardReplayRequestTransferReservation ::" twinHarness `shouldBe` 1+            plainMods <- scaffoldFixture "test/fixtures/reservation.keiro"+            let plainHoles = [moduleText m | m <- plainMods, kind m == HoleStub]+            plainHoles `shouldSatisfy` all (not . T.isInfixOf "B.replayOnly")++comparisonProvenance :: CompareProvenance+comparisonProvenance =+    CompareProvenance+        { cpHistoricalCodecIdentity = "example.historical"+        , cpHistoricalCodecVersion = "legacy-v1"+        , cpCanonicalType = CanonicalTypeId "example.Artifact.v1"+        , cpBindingSymbol = QualifiedValueName "Example.Bindings.artifactBinding"+        , cpBindingVersion = BindingVersion "1"+        , cpWireFingerprint = "deadbeef"+        }++syntheticGenerated :: FilePath -> T.Text -> ScaffoldModule+syntheticGenerated path contents =+    ScaffoldModule{modulePath = path, moduleText = contents, kind = Generated, origin = "test"}++generatedTextEndingIn :: T.Text -> [ScaffoldModule] -> T.Text+generatedTextEndingIn suffix modules = case [moduleText m | m <- modules, kind m == Generated, suffix `T.isSuffixOf` T.pack (modulePath m)] of+    contents : _ -> contents+    [] -> ""++onlyAggregate :: Spec -> Aggregate+onlyAggregate spec = case [aggregate | NAggregate aggregate <- specNodes spec] of+    [aggregate] -> aggregate+    aggregates -> error ("expected one aggregate, got " <> show (length aggregates))++loweringAggregateSpec :: T.Text+loweringAggregateSpec =+    T.unlines+        [ "context samples"+        , ""+        , "aggregate Counter"+        , "  regs"+        , "    note Text = \"hello world\""+        , "    count Int = 0"+        , "    state CounterVertex = Pending"+        , "  states Pending Done!"+        , "  command Bump { count:Int }"+        , "  event CountBumped { count:Int }"+        , "  Pending -- Bump --> emit CountBumped ; goto Done"+        ]++exactStatusSpec :: T.Text+exactStatusSpec =+    T.unlines+        [ "context samples"+        , ""+        , "aggregate Reservation"+        , "  regs"+        , "    state ReservationVertex = Open"+        , "  states Open Closed!"+        , "  command Bump { count:Int }"+        , "  event ReservationHeld { count:Int }"+        , "  event ReservationUnHeld { count:Int }"+        , "  event CountBumped { count:Int }"+        , "  Open -- Bump --> emit CountBumped ; goto Closed"+        , "  projection reservation_status consistency=Eventual key=count"+        , "    status-map { ReservationHeld=>held ReservationUnHeld=>available CountBumped=>bumped }"+        ]++hasPathCollisionWithTwoOrigins :: Either [Refusal] [ScaffoldModule] -> Bool+hasPathCollisionWithTwoOrigins = \case+    Left refusals -> any hasTwo refusals+    Right _ -> False+  where+    hasTwo (PathCollision _ origins) = length origins == 2+    hasTwo _ = False++isMissingBannerRefusal :: Either [Refusal] a -> Bool+isMissingBannerRefusal = \case+    Left [MissingGeneratedBanner paths] -> not (null paths)+    _ -> False++isSuccessfulScaffold :: Either [Refusal] a -> Bool+isSuccessfulScaffold = \case+    Right _ -> True+    Left _ -> False++executePlannedScaffold :: FilePath -> FilePath -> Context -> Spec -> IO ScaffoldReport+executePlannedScaffold out specPath ctx spec = case planScaffold ctx spec of+    Left refusals -> expectationFailure ("unexpected scaffold refusal: " <> show refusals) >> error "unreachable"+    Right modules -> do+        result <- executeScaffold out False specPath ctx spec modules+        case result of+            Left refusals -> expectationFailure ("unexpected execution refusal: " <> show refusals) >> error "unreachable"+            Right report -> pure report++renameCounter :: Node -> Node+renameCounter (NAggregate aggregate) =+    NAggregate+        aggregate+            { aggName = "Widget"+            , aggRegs = [reg{regType = if regType reg == "CounterVertex" then "WidgetVertex" else regType reg} | reg <- aggRegs aggregate]+            }+renameCounter node = node++onlyPathEndingIn :: FilePath -> [ScaffoldModule] -> FilePath+onlyPathEndingIn suffix modules = case [modulePath m | m <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath m)] of+    [path] -> path+    paths -> error ("expected one path ending in " <> suffix <> ", got " <> show paths)++withTempDirectory :: String -> (FilePath -> IO a) -> IO a+withTempDirectory template = bracket acquire removePathForcibly+  where+    acquire = do+        base <- getTemporaryDirectory+        (path, handle) <- openTempFile base template+        hClose handle+        removeFile path+        createDirectory path+        pure path++{- | Parse a fixture and return the validator's diagnostic codes (failing the+test on a parse error).+-}+diagnosticCodesOf :: FilePath -> IO [DiagnosticCode]+diagnosticCodesOf path = do+    map code <$> diagnosticsOf path++-- | Parse a fixture and return all validator diagnostics.+diagnosticsOf :: FilePath -> IO [Diagnostic]+diagnosticsOf path = do+    input <- readTestText path+    case parseSpec path input of+        Left err -> expectationFailure (T.unpack err) >> pure []+        Right spec -> pure (validateSpec spec)++{- | Like 'diagnosticCodesOf' but only the Error-severity codes (warnings, e.g.+the benign-inversion notices, are excluded).+-}+errorCodesOf :: FilePath -> IO [DiagnosticCode]+errorCodesOf path = do+    diagnostics <- diagnosticsOf path+    pure [code d | d <- diagnostics, severity d == Error]++{- | Parse two fixtures and diff them (old, new).+| Plan 143: render an Expr in concrete guard syntax by printing a dummy+transition through the real pretty-printer and slicing its guard clause,+so the test exercises the exact printer the diff advisory uses.+-}+renderExprText :: Expr -> T.Text+renderExprText e =+    case [T.strip l | l <- T.lines rendered, "guard " `T.isPrefixOf` T.strip l] of+        [guardLine] -> T.strip (T.drop (T.length "guard ") guardLine)+        _ -> error ("renderExprText: unexpected printer output: " <> T.unpack rendered)+  where+    rendered =+        renderTransition+            Transition+                { tSource = "S"+                , tCommand = "C"+                , tGuard = Just e+                , tWrites = []+                , tEmits = []+                , tGoto = "S"+                , tMode = TmLive+                , tLoc = noLoc+                }++{- | Plan 143: a minimal spec whose only transition is replay-only, with the+supplied clause lines spliced into its body.+-}+replayOnlySpecWith :: [T.Text] -> T.Text+replayOnlySpecWith clauseLines =+    T.unlines $+        [ "context hospital-capacity"+        , ""+        , "id TransferReservationId prefix=rsv"+        , ""+        , "aggregate Reservation"+        , "  regs"+        , "    reservationId    TransferReservationId = placeholder"+        , "    reservationState ReservationVertex     = Unrequested"+        , "  states Unrequested Held"+        , ""+        , "  command RequestTransferReservation { reservationId }"+        , ""+        , "  event TransferReservationCreated = fields(RequestTransferReservation)"+        , ""+        , "  replay-only Unrequested -- RequestTransferReservation -->"+        ]+            ++ clauseLines++diffFixtures :: FilePath -> FilePath -> IO [Change]+diffFixtures oldP newP = do+    old <- readTestText oldP+    new <- readTestText newP+    case (,) <$> parseSpec oldP old <*> parseSpec newP new of+        Left err -> expectationFailure (T.unpack err) >> pure []+        Right (o, n) -> pure (diffSpecs o n)++kindOfChange :: Change -> ChangeKind+kindOfChange (Additive kind) = kind+kindOfChange (Advisory kind) = kind+kindOfChange (Breaking kind) = kind++labelOfChange :: Change -> Label+labelOfChange Additive{} = LabelAdditive+labelOfChange Advisory{} = LabelAdvisory+labelOfChange Breaking{} = LabelBreaking++genSurfaceSet :: Gen (Set.Set CompatibilitySurface)+genSurfaceSet = Set.fromList <$> listOf (elements [minBound .. maxBound])++genCompatibilityVector :: Gen CompatibilityVector+genCompatibilityVector =+    CompatibilityVector+        <$> genVerdict+        <*> genVerdict+        <*> genVerdict+        <*> genVerdict+        <*> genVerdict+        <*> genVerdict+        <*> (Set.fromList <$> listOf (elements rolloutConstraints))+  where+    genVerdict = elements [VCompatible, VAdvisory, VBreaking, VNotApplicable]+    rolloutConstraints =+        [ RolloutStopTheWorld+        , RolloutWorkersFirst+        , RolloutDrainRequired+        , RolloutProducerLast+        ]++replayImpactFixtures :: FilePath -> FilePath -> IO ReplayImpact+replayImpactFixtures oldPath newPath = do+    old <- specOf oldPath+    new <- specOf newPath+    pure (ReplayImpact.replayImpact old new)++modifyAggregate :: Name -> (Aggregate -> Aggregate) -> Spec -> Spec+modifyAggregate target update spec =+    spec+        { specNodes =+            [ case node of+                NAggregate aggregate | aggName aggregate == target -> NAggregate (update aggregate)+                _ -> node+            | node <- specNodes spec+            ]+        }++modifyReadModel :: Name -> (ReadModelNode -> ReadModelNode) -> Spec -> Spec+modifyReadModel target update spec =+    spec+        { specNodes =+            [ case node of+                NReadModel readModel | rmName readModel == target -> NReadModel (update readModel)+                _ -> node+            | node <- specNodes spec+            ]+        }++removeReadModel :: Name -> Spec -> Spec+removeReadModel target spec =+    spec{specNodes = [node | node <- specNodes spec, not (isTarget node)]}+  where+    isTarget (NReadModel readModel) = rmName readModel == target+    isTarget _ = False++modifyRouter :: Name -> (RouterNode -> RouterNode) -> Spec -> Spec+modifyRouter target update spec =+    spec+        { specNodes =+            [ case node of+                NRouter router | rtId router == target -> NRouter (update router)+                _ -> node+            | node <- specNodes spec+            ]+        }++routerErrorCodes :: (RouterNode -> RouterNode) -> Spec -> [DiagnosticCode]+routerErrorCodes update = errorCodes . modifyRouter "PagingRouter" update++modifyProcess :: Name -> (ProcessNode -> ProcessNode) -> Spec -> Spec+modifyProcess target update spec =+    spec+        { specNodes =+            [ case node of+                NProcess process | procId process == target -> NProcess (update process)+                _ -> node+            | node <- specNodes spec+            ]+        }++processErrorCodes :: (ProcessNode -> ProcessNode) -> Spec -> [DiagnosticCode]+processErrorCodes update = errorCodes . modifyProcess "HospitalSurge" update++errorCodes :: Spec -> [DiagnosticCode]+errorCodes spec = [code diagnostic | diagnostic <- validateSpec spec, severity diagnostic == Error]++changeReadModelShape :: ReadModelNode -> ReadModelNode+changeReadModelShape readModel =+    readModel+        { rmColumns = rmColumns readModel <> [RmColumn "reviewed_by" "text" False]+        , rmShape = "fnv1a:0000000000000000"+        }++{- | Assert a @new \<kind\>@ skeleton parses and validates with zero+error-severity diagnostics.+-}+assertSkeletonValid :: T.Text -> IO ()+assertSkeletonValid kind = case skeletonFor kind of+    Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))+    Right src -> case parseSpec ("new:" <> T.unpack kind) src of+        Left perr -> expectationFailure (T.unpack ("skeleton for " <> kind <> " failed to parse: " <> perr))+        Right spec ->+            [code d | d <- validateSpec spec, severity d == Error]+                `shouldBe` ([] :: [DiagnosticCode])++assertSkeletonScaffoldable :: T.Text -> IO ()+assertSkeletonScaffoldable kind = case skeletonFor kind of+    Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))+    Right src -> case parseSpec ("new:" <> T.unpack kind) src of+        Left perr -> expectationFailure (T.unpack perr)+        Right spec -> planScaffold (defaultContext (specContext spec)) spec `shouldSatisfy` isSuccessfulScaffold++skeletonModuleRoots :: [(T.Text, T.Text)]+skeletonModuleRoots =+    [ ("aggregate", "SkelAggregate")+    , ("process", "SkelProcess")+    , ("router", "SkelRouter")+    , ("contract", "SkelContract")+    , ("intake", "SkelIntake")+    , ("emit", "SkelEmit")+    , ("workqueue", "SkelQueue")+    , ("workflow", "SkelWorkflow")+    ]++assertSkeletonMatchesCommitted :: T.Text -> T.Text -> IO ()+assertSkeletonMatchesCommitted kind root = case skeletonFor kind of+    Left err -> expectationFailure (T.unpack err)+    Right source -> case parseSpec ("new:" <> T.unpack kind) source of+        Left err -> expectationFailure (T.unpack err)+        Right spec -> do+            let ctx = (defaultContext (specContext spec)){moduleRoot = root}+            forM_ [m | m <- scaffoldModules ctx spec, kindOf m == Generated] $ \m -> do+                committed <- readTestText ("test/conformance-skeletons/" <> modulePath m)+                normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)+  where+    kindOf = Keiro.Dsl.Scaffold.kind++bumpArtifactBindingVersion :: MappedDecl -> MappedDecl+bumpArtifactBindingVersion declaration@MappedStructural{msName = "ArtifactInfo"} =+    declaration{msBindingVersion = Just "2"}+bumpArtifactBindingVersion declaration = declaration++addArtifactSummaryField :: MappedDecl -> MappedDecl+addArtifactSummaryField declaration@MappedStructural{msName = "ArtifactInfo", msShape = ShapeRecord constructor unknownFields fields} =+    declaration+        { msShape =+            ShapeRecord+                constructor+                unknownFields+                ( fields+                    <> [ WireField+                            { wfHaskell = "summary"+                            , wfKey = "summary"+                            , wfType = TText+                            , wfPresence = PRequired+                            , wfOnMissing = Nothing+                            , wfLoc = Loc 0+                            }+                       ]+                )+        }+addArtifactSummaryField declaration = declaration++expectGenericCompileFailure :: FilePath -> String -> Expectation+expectGenericCompileFailure fixture expectedDiagnostic = do+    let fixtureDir = "../keiro-core/test/compile-fail" </> fixture+        fixtureSource = fixtureDir </> "Fixture.hs"+    (exitCode, standardOutput, standardError) <-+        readProcessWithExitCode+            "cabal"+            [ "exec"+            , "--"+            , "ghc"+            , "-XGHC2024"+            , "-fno-code"+            , "-fforce-recomp"+            , "-i../keiro-core/src"+            , "-i" <> fixtureDir+            , fixtureSource+            ]+            ""+    exitCode `shouldSatisfy` (/= ExitSuccess)+    let compilerOutput = standardOutput <> standardError+    compilerOutput `shouldContain` expectedDiagnostic+    compilerOutput `shouldContain` "Run keiro-dsl scaffold and fill the binding by hand at this error location in the scaffolded module."+    compilerOutput `shouldContain` fixtureSource++moveArtifactBindingIntoGenerated :: MappedDecl -> MappedDecl+moveArtifactBindingIntoGenerated declaration@MappedStructural{msName = "ArtifactInfo"} =+    declaration{msBinding = Just "Generated.ConsumerDemo.Bindings.artifactInfoBinding"}+moveArtifactBindingIntoGenerated declaration = declaration++removeMappedRegisterRequirements :: Spec -> Spec+removeMappedRegisterRequirements spec =+    spec+        { specMapped = map removeInitial (specMapped spec)+        , specNodes = map removeRegisters (specNodes spec)+        }+  where+    removeInitial declaration@MappedStructural{} = declaration{msInitial = Nothing}+    removeInitial declaration@MappedOpaque{} = declaration{moInitial = Nothing}+    removeRegisters (NAggregate aggregate) =+        NAggregate+            aggregate+                { aggRegs = []+                , aggTransitions = [transition{tWrites = []} | transition <- aggTransitions aggregate]+                }+    removeRegisters node = node++isImportCycle :: Refusal -> Bool+isImportCycle ImportCycle{} = True+isImportCycle _ = False++isLoweringRefusal :: Either [Refusal] modules -> Bool+isLoweringRefusal (Left refusals) = any isLowering refusals+  where+    isLowering LoweringRefusal{} = True+    isLowering _ = False+isLoweringRefusal (Right _) = False++-- | Parse a fixture into a 'Spec', failing the test on a parse error.+specOf :: FilePath -> IO Spec+specOf path = do+    input <- readTestText path+    case parseSpec path input of+        Left err -> expectationFailure (T.unpack err) >> error "unreachable"+        Right spec -> pure spec++-- | Parse a fixture and scaffold every aggregate in it.+scaffoldFixture :: FilePath -> IO [ScaffoldModule]+scaffoldFixture path = do+    input <- readTestText path+    case parseSpec path input of+        Left err -> expectationFailure (T.unpack err) >> pure []+        Right spec ->+            pure $+                concat+                    [ scaffoldAggregate (ctx spec) spec agg <> harnessFor (ctx spec) spec agg+                    | NAggregate agg <- specNodes spec+                    ]+  where+    ctx spec = defaultContext (specContext spec)++scaffoldProcessFixture :: FilePath -> IO [ScaffoldModule]+scaffoldProcessFixture path = do+    input <- readTestText path+    case parseSpec path input of+        Left err -> expectationFailure (T.unpack err) >> pure []+        Right spec ->+            pure $ concat [scaffoldProcess (ctx spec) p | NProcess p <- specNodes spec]+  where+    ctx spec = defaultContext (specContext spec)++{- | Assert a freshly-scaffolded Generated module matches its committed copy+under test/conformance/ (whitespace-normalized). The committed copies are the+ones the keiro-dsl-conformance suite compiles, so this pins the live scaffolder+to known-compiling output.+-}+assertMatchesCommitted :: ScaffoldModule -> IO ()+assertMatchesCommitted m = do+    let committedPath = "test/conformance/" <> modulePath m+    committed <- readTestText committedPath+    normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText m)++normalizeGenerated :: T.Text -> (T.Text, [T.Text])+normalizeGenerated text =+    let (imports, body) = partition isImport (T.lines text)+     in (normalizeBody body, sort (map normalizeImport imports))+  where+    -- Compare the deterministic body exactly as before and imports as a sorted,+    -- whitespace-normalized list. Sorting tolerates formatter reordering while+    -- additions, removals, and renamed imports now fail the pin.+    normalizeBody =+        T.replace " , )" " )"+            . T.unwords+            . T.words+            . T.replace "}" " } "+            . T.replace "{" " { "+            . T.replace "]" " ] "+            . T.replace "[" " [ "+            . T.replace "," " , "+            . T.unlines+    normalizeImport line =+        let reordered = case T.words line of+                "import" : "qualified" : moduleName : rest -> T.unwords ("import" : moduleName : "qualified" : rest)+                wordsInImport -> T.unwords wordsInImport+            (prefix, explicit) = T.breakOn " (" reordered+         in if T.null explicit+                then prefix+                else+                    let members =+                            sort+                                . map (T.unwords . T.words)+                                . T.splitOn ","+                                . T.dropEnd 1+                                $ T.drop 2 explicit+                     in prefix <> " (" <> T.intercalate "," members <> ")"+    isImport line = case T.words line of+        "import" : _ -> True+        _ -> False++{- | Locate and read a test fixture or committed conformance source regardless+of whether the suite was launched from the package directory or repo root.+-}+readTestText :: FilePath -> IO T.Text+readTestText path = resolveTestPath path >>= TIO.readFile++-- | Locate a repo file regardless of the test process's current directory.+resolveTestPath :: FilePath -> IO FilePath+resolveTestPath rel = do+    override <- lookupEnv "KEIRO_DSL_TEST_ROOT"+    let candidates = [rel, "keiro-dsl" </> rel] <> maybe [] (\root -> [root </> rel]) override+    existing <- filterM doesFileExist candidates+    case existing of+        path : _ -> pure path+        [] ->+            fail $+                "unable to locate keiro-dsl test file "+                    <> show rel+                    <> "; tried "+                    <> show candidates++leftContains :: T.Text -> Either T.Text a -> Bool+leftContains needle = \case+    Left err -> needle `T.isInfixOf` err+    Right _ -> False++parseInlineSpec :: FilePath -> T.Text -> IO Spec+parseInlineSpec sourceName src = case parseSpec sourceName src of+    Left err -> expectationFailure (T.unpack err) >> error "unreachable"+    Right spec -> pure spec++shouldResolveTypeGraph :: Spec -> IO TypeGraph+shouldResolveTypeGraph spec = case resolveTypeGraph spec of+    Left errors -> expectationFailure ("type graph failed: " <> show errors) >> error "unreachable"+    Right graph -> pure graph++shouldResolveCoverage :: FilePath -> Spec -> IO Coverage.CoverageReport+shouldResolveCoverage path spec = case Coverage.coverageReport path spec of+    Left errors -> expectationFailure ("coverage graph failed: " <> show errors) >> error "unreachable"+    Right report -> pure report++withoutVendorGeometry :: Spec -> Spec+withoutVendorGeometry spec =+    spec+        { specMapped = filter (not . isVendorGeometry) (specMapped spec)+        , specNodes = map stripNode (specNodes spec)+        }+  where+    isVendorGeometry MappedOpaque{moName = "VendorGeometry"} = True+    isVendorGeometry _ = False+    stripNode (NAggregate aggregate) =+        NAggregate+            aggregate+                { aggRegs = filter ((/= "VendorGeometry") . regType) (aggRegs aggregate)+                , aggCommands = map stripCommand (aggCommands aggregate)+                , aggEvents = map stripEvent (aggEvents aggregate)+                }+    stripNode node = node+    stripCommand command = command{cmdFields = filter ((/= Just "VendorGeometry") . fieldType) (cmdFields command)}+    stripEvent event = event{evBody = case evBody event of EventFields fields -> EventFields (filter ((/= Just "VendorGeometry") . fieldType) fields); body -> body}++withMetadataJson :: Spec -> Spec+withMetadataJson spec = spec{specMapped = map updateDeclaration (specMapped spec)}+  where+    updateDeclaration declaration@MappedStructural{msName = "ArtifactMetadata", msShape = ShapeRecord constructor unknownFields fields} =+        declaration+            { msShape =+                ShapeRecord+                    constructor+                    unknownFields+                    [if wfHaskell field == "note" then field{wfType = TJson} else field | field <- fields]+            }+    updateDeclaration declaration = declaration++expressionTags :: TypeExprAlgebra [T.Text]+expressionTags =+    TypeExprAlgebra+        { onText = ["text"]+        , onInt = ["int"]+        , onBool = ["bool"]+        , onNatural = ["natural"]+        , onTime = ["time"]+        , onJson = ["json"]+        , onOptional = ("optional" :)+        , onList = ("list" :)+        , onMap = ("map" :)+        , onRef = \key -> ["ref:" <> unMappedKey key]+        }++hasTypeGraphError :: (TypeGraphError -> Bool) -> Either (NonEmpty TypeGraphError) TypeGraph -> Bool+hasTypeGraphError predicate = \case+    Left errors -> any predicate errors+    Right _ -> False++isRecursive :: TypeGraphError -> Bool+isRecursive TGRecursive{} = True+isRecursive _ = False++isUnresolved :: TypeGraphError -> Bool+isUnresolved TGUnresolvedRef{} = True+isUnresolved _ = False++mappedSpec :: [MappedDecl] -> Spec+mappedSpec declarations = Spec "mapped-test" Nothing Nothing [] [] [] declarations []++completeStructural :: Name -> MappedShape -> MappedDecl+completeStructural name shape =+    MappedStructural+        { msName = name+        , msHaskell = Just (HaskellSource "mapped-test" "Example.Mapped" name)+        , msBinding = Just ("Example.Mapped." <> T.toLower name <> "Binding")+        , msBindingVersion = Just "1"+        , msCanonical = Just ("example.mapped." <> name)+        , msFixtures = Just ("Example.Mapped." <> T.toLower name <> "Cases")+        , msInitial = Nothing+        , msShape = shape+        , msLoc = noLoc+        }++recordShape :: [TypeExpr] -> MappedShape+recordShape types =+    ShapeRecord+        "MappedRecord"+        RejectUnknown+        [ WireField+            { wfHaskell = "field" <> T.pack (show index)+            , wfKey = "field" <> T.pack (show index)+            , wfType = fieldType+            , wfPresence = PRequired+            , wfOnMissing = Nothing+            , wfLoc = noLoc+            }+        | (index, fieldType) <- zip [(1 :: Int) ..] types+        ]++mapArtifactField :: (WireField -> WireField) -> Spec -> Spec+mapArtifactField = mapArtifactNamedField "key"++mapArtifactNamedField :: Name -> (WireField -> WireField) -> Spec -> Spec+mapArtifactNamedField target transform spec = spec{specMapped = map updateDeclaration (specMapped spec)}+  where+    updateDeclaration declaration@MappedStructural{msName = "ArtifactInfo", msShape = ShapeRecord constructor unknownFields fields} =+        declaration+            { msShape =+                ShapeRecord+                    constructor+                    unknownFields+                    [if wfHaskell field == target then transform field else field | field <- fields]+            }+    updateDeclaration declaration = declaration++mapMappedStructural :: Name -> (MappedDecl -> MappedDecl) -> Spec -> Spec+mapMappedStructural target transform spec =+    spec+        { specMapped =+            [ case declaration of+                MappedStructural{msName = name}+                    | name == target -> transform declaration+                _ -> declaration+            | declaration <- specMapped spec+            ]+        }++renameRecordConstructor :: MappedShape -> MappedShape+renameRecordConstructor (ShapeRecord _ unknownFields fields) = ShapeRecord "ArtifactInfoV2" unknownFields fields+renameRecordConstructor shape = shape++renameMappedRecordConstructor :: MappedDecl -> MappedDecl+renameMappedRecordConstructor declaration@MappedStructural{msShape = shape} =+    declaration{msShape = renameRecordConstructor shape}+renameMappedRecordConstructor declaration = declaration++changeMappedCanonical :: MappedDecl -> MappedDecl+changeMappedCanonical declaration@MappedStructural{} =+    declaration{msCanonical = Just "example.artifact.ArtifactInfo.v2"}+changeMappedCanonical declaration = declaration++data MappedMutation = MappedMutation+    { mmCandidate :: !Spec+    , mmCode :: !DiagnosticCode+    , mmExpectedSubjects :: !(Set.Set T.Text)+    }+    deriving stock (Show)++mappedWireMutations :: Spec -> [MappedMutation]+mappedWireMutations spec = case resolveTypeGraph spec of+    Left _ -> []+    Right graph -> concatMap (uncurry (declarationMutations graph)) (zip [0 :: Int ..] (specMapped spec))+  where+    declarationMutations graph declarationIndex declaration = case declaration of+        MappedStructural{msName = declarationName, msShape = shape} -> case shape of+            ShapeRecord _ _ fields ->+                concat+                    [ [ mutation+                            graph+                            declarationName+                            MappedWireKeyChanged+                            (fieldSubject field{wfKey = wfKey field <> "__mutated"})+                            (mutateRecordField declarationIndex fieldIndex (\value -> value{wfKey = wfKey value <> "__mutated"}) spec)+                      , mutation+                            graph+                            declarationName+                            MappedPresenceChanged+                            (fieldSubject field)+                            (mutateRecordField declarationIndex fieldIndex (\value -> value{wfPresence = flipPresence (wfPresence value)}) spec)+                      ]+                        <> [ mutation+                                graph+                                declarationName+                                defaultCode+                                (fieldSubject field)+                                (mutateRecordField declarationIndex fieldIndex (\value -> value{wfOnMissing = changedDefault}) spec)+                           | oldDefault <- maybeToListTest (wfOnMissing field)+                           , let (changedDefault, defaultCode) = mutateDefault oldDefault+                           ]+                    | (fieldIndex, field) <- zip [0 :: Int ..] fields+                    ]+            ShapeEnum entries ->+                [ mutation+                    graph+                    declarationName+                    MappedEnumSpellingChanged+                    (enumSubject entry{weTag = weTag entry <> "__mutated"})+                    (mutateEnumEntry declarationIndex entryIndex (\value -> value{weTag = weTag value <> "__mutated"}) spec)+                | (entryIndex, entry) <- zip [0 :: Int ..] entries+                ]+            ShapeUnion _ arms ->+                [ mutation+                    graph+                    declarationName+                    MappedArmTagChanged+                    (armSubject arm{waTag = waTag arm <> "__mutated"})+                    (mutateUnionArm declarationIndex armIndex (\value -> value{waTag = waTag value <> "__mutated"}) spec)+                | (armIndex, arm) <- zip [0 :: Int ..] arms+                ]+        MappedOpaque{moName = declarationName, moCodecVersion = version} ->+            [ mutation+                graph+                declarationName+                MappedOpaqueCodecChanged+                "codec"+                ( updateMappedAt+                    declarationIndex+                    ( \case+                        value@MappedOpaque{} -> value{moCodecVersion = fmap (<> "__mutated") version}+                        value -> value+                    )+                    spec+                )+            ]++    mutation graph declarationName diagnosticCode leaf candidate =+        MappedMutation+            { mmCandidate = candidate+            , mmCode = diagnosticCode+            , mmExpectedSubjects =+                Set.fromList+                    [ renderUsePath path <> " " <> leaf+                    | path <- usePaths graph declarationName+                    ]+            }++fieldSubject :: WireField -> T.Text+fieldSubject field = ".field " <> wfHaskell field <> "[\"" <> wfKey field <> "\"]"++enumSubject :: WireEnum -> T.Text+enumSubject entry = ".enum " <> weCtor entry <> "[\"" <> weTag entry <> "\"]"++armSubject :: WireArm -> T.Text+armSubject arm = ".arm " <> waCtor arm <> "[\"" <> waTag arm <> "\"]"++flipPresence :: Presence -> Presence+flipPresence PRequired = POptional+flipPresence POptional = PRequired++mutateDefault :: OnMissing -> (Maybe OnMissing, DiagnosticCode)+mutateDefault = \case+    OmNull -> (Nothing, MappedDefaultRemoved)+    OmText value -> (Just (OmText (value <> "__mutated")), MappedDefaultChanged)+    OmInt value -> (Just (OmInt (value + 1)), MappedDefaultChanged)+    OmBool value -> (Just (OmBool (not value)), MappedDefaultChanged)+    OmEmptyList -> (Nothing, MappedDefaultRemoved)+    OmEmptyMap -> (Nothing, MappedDefaultRemoved)+    OmCtor constructor -> (Just (OmCtor (constructor <> "Mutated")), MappedDefaultChanged)++mutateRecordField :: Int -> Int -> (WireField -> WireField) -> Spec -> Spec+mutateRecordField declarationIndex fieldIndex transform =+    updateMappedAt declarationIndex $ \case+        declaration@MappedStructural{msShape = ShapeRecord constructor unknownFields fields} ->+            declaration{msShape = ShapeRecord constructor unknownFields (updateAt fieldIndex transform fields)}+        declaration -> declaration++mutateEnumEntry :: Int -> Int -> (WireEnum -> WireEnum) -> Spec -> Spec+mutateEnumEntry declarationIndex entryIndex transform =+    updateMappedAt declarationIndex $ \case+        declaration@MappedStructural{msShape = ShapeEnum entries} ->+            declaration{msShape = ShapeEnum (updateAt entryIndex transform entries)}+        declaration -> declaration++mutateUnionArm :: Int -> Int -> (WireArm -> WireArm) -> Spec -> Spec+mutateUnionArm declarationIndex armIndex transform =+    updateMappedAt declarationIndex $ \case+        declaration@MappedStructural{msShape = ShapeUnion encoding arms} ->+            declaration{msShape = ShapeUnion encoding (updateAt armIndex transform arms)}+        declaration -> declaration++updateMappedAt :: Int -> (MappedDecl -> MappedDecl) -> Spec -> Spec+updateMappedAt declarationIndex transform spec =+    spec{specMapped = updateAt declarationIndex transform (specMapped spec)}++updateAt :: Int -> (a -> a) -> [a] -> [a]+updateAt target transform values =+    [if index == target then transform value else value | (index, value) <- zip [0 :: Int ..] values]++maybeToListTest :: Maybe a -> [a]+maybeToListTest = maybe [] pure++isAdditiveChange :: Change -> Bool+isAdditiveChange Additive{} = True+isAdditiveChange Advisory{} = False+isAdditiveChange Breaking{} = False++mappedIngredientMutations :: Spec -> [(Spec, DiagnosticCode)]+mappedIngredientMutations spec =+    [ (mapMappedStructural "ArtifactInfo" clearStructuralHaskell spec, MappedMissingIngredient)+    , (mapMappedStructural "ArtifactInfo" clearStructuralBinding spec, MappedMissingIngredient)+    , (mapMappedStructural "ArtifactInfo" clearStructuralBindingVersion spec, MappedMissingIngredient)+    , (mapMappedStructural "ArtifactInfo" clearStructuralCanonical spec, MappedMissingIngredient)+    , (mapMappedStructural "ArtifactInfo" clearStructuralFixtures spec, MappedMissingIngredient)+    , (mapMappedStructural "ArtifactInfo" clearStructuralInitial spec, MappedMissingInitialValue)+    , (mapMappedDeclaration "VendorGeometry" clearOpaqueHaskell spec, MappedMissingIngredient)+    , (mapMappedDeclaration "VendorGeometry" clearOpaqueCodec spec, MappedMissingIngredient)+    , (mapMappedDeclaration "VendorGeometry" clearOpaqueCodecVersion spec, MappedMissingIngredient)+    , (mapMappedDeclaration "VendorGeometry" clearOpaqueFixtures spec, MappedMissingIngredient)+    ]+  where+    clearStructuralHaskell declaration@MappedStructural{} = declaration{msHaskell = Nothing}+    clearStructuralHaskell declaration = declaration+    clearStructuralBinding declaration@MappedStructural{} = declaration{msBinding = Nothing}+    clearStructuralBinding declaration = declaration+    clearStructuralBindingVersion declaration@MappedStructural{} = declaration{msBindingVersion = Nothing}+    clearStructuralBindingVersion declaration = declaration+    clearStructuralCanonical declaration@MappedStructural{} = declaration{msCanonical = Nothing}+    clearStructuralCanonical declaration = declaration+    clearStructuralFixtures declaration@MappedStructural{} = declaration{msFixtures = Nothing}+    clearStructuralFixtures declaration = declaration+    clearStructuralInitial declaration@MappedStructural{} = declaration{msInitial = Nothing}+    clearStructuralInitial declaration = declaration+    clearOpaqueHaskell declaration@MappedOpaque{} = declaration{moHaskell = Nothing}+    clearOpaqueHaskell declaration = declaration+    clearOpaqueCodec declaration@MappedOpaque{} = declaration{moCodecId = Nothing}+    clearOpaqueCodec declaration = declaration+    clearOpaqueCodecVersion declaration@MappedOpaque{} = declaration{moCodecVersion = Nothing}+    clearOpaqueCodecVersion declaration = declaration+    clearOpaqueFixtures declaration@MappedOpaque{} = declaration{moFixtures = Nothing}+    clearOpaqueFixtures declaration = declaration++mapMappedDeclaration :: Name -> (MappedDecl -> MappedDecl) -> Spec -> Spec+mapMappedDeclaration target transform spec =+    spec+        { specMapped =+            [ if mappedDeclarationName declaration == target then transform declaration else declaration+            | declaration <- specMapped spec+            ]+        }++mappedDeclarationName :: MappedDecl -> Name+mappedDeclarationName MappedStructural{msName = name} = name+mappedDeclarationName MappedOpaque{moName = name} = name++statusMapSpec :: T.Text -> T.Text+statusMapSpec marker =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  event Created { }"+        , "  event Changed { }"+        , ""+        , "  projection things consistency=Eventual key=thingId"+        , "    status-map" <> marker <> " { Created=>held }"+        ]++parseErrorOf :: FilePath -> T.Text -> IO T.Text+parseErrorOf sourceName src = case parseSpec sourceName src of+    Left err -> pure err+    Right _ -> expectationFailure ("expected parse failure for " <> sourceName) >> error "unreachable"++duplicateGotoSpec :: T.Text+duplicateGotoSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states A B C"+        , ""+        , "  command Go { }"+        , "  A -- Go -->"+        , "    goto B"+        , "    goto C"+        ]++missingGotoSpec :: T.Text+missingGotoSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states A B"+        , ""+        , "  command Go { }"+        , "  A -- Go -->"+        , "    emit Changed"+        ]++duplicateWireSpec :: T.Text+duplicateWireSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  wire kind=ctorName fields=camelCase schemaVersion=1"+        , "  wire kind=typeName fields=snakeCase schemaVersion=2"+        ]++duplicateProjectionSpec :: T.Text+duplicateProjectionSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  projection first consistency=Strong key=thingId"+        , "    status-map partial { }"+        , "  projection second consistency=Eventual key=thingId"+        ]++projectionWithoutConsistencySpec :: T.Text+projectionWithoutConsistencySpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  projection things key=thingId"+        ]++malformedRegisterSpec :: T.Text+malformedRegisterSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "    status Status"+        , "  states Open"+        ]++misplacedDispatchIdSpec :: T.Text+misplacedDispatchIdSpec =+    T.replace+        "    schedule timer\n\n  dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId, emitIndex)\n"+        "    dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId, emitIndex)\n    schedule timer\n"+        (renderSpec (Spec "svc" Nothing Nothing [] [] [] [] [NProcess (processWithLiteral "literal")]))++lineNumberContaining :: T.Text -> T.Text -> Int+lineNumberContaining needle = go 1 . T.lines+  where+    go current = \case+        [] -> current+        lineText : rest+            | needle `T.isInfixOf` lineText -> current+            | otherwise -> go (current + 1) rest++decimalOverflow :: T.Text+decimalOverflow = "18446744073709551617"++decimalOverflowSpecs :: [(String, T.Text)]+decimalOverflowSpecs =+    [ ("event-version", eventVersionDecimalSpec decimalOverflow)+    , ("wire-schema", wireDecimalSpec decimalOverflow)+    , ("contract-schema", contractDecimalSpec decimalOverflow)+    , ("decode-schema", decodeDecimalSpec decimalOverflow)+    , ("publisher-attempts", publisherDecimalSpec decimalOverflow)+    , ("workqueue-retries", workqueueDecimalSpec decimalOverflow)+    , ("timer-attempts", timerDecimalSpec decimalOverflow)+    ]++eventVersionDecimalSpec :: T.Text -> T.Text+eventVersionDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  event Changed v" <> value <> " { }"+        ]++wireDecimalSpec :: T.Text -> T.Text+wireDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  wire kind=ctorName fields=camelCase schemaVersion=" <> value+        ]++contractDecimalSpec :: T.Text -> T.Text+contractDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "contract Contract {"+        , "  schemaVersion " <> value+        , "  discriminator kind"+        , "}"+        ]++decodeDecimalSpec :: T.Text -> T.Text+decodeDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "intake Inbox {"+        , "  contract Contract"+        , "  topic events"+        , "  accept Event"+        , "  dedupe key messageId policy PreferIntegrationMessageId"+        , "  decode { envelope strict-required lenient-optional body strict schemaVersion == " <> value <> " }"+        , "  disposition { }"+        , "}"+        ]++publisherDecimalSpec :: T.Text -> T.Text+publisherDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "publisher Publisher {"+        , "  emit Emit"+        , "  ordering PerKeyHeadOfLine"+        , "  maxAttempts " <> value+        , "  backoff constant 2s"+        , "  outboxId stable from messageId"+        , "}"+        ]++workqueueDecimalSpec :: T.Text -> T.Text+workqueueDecimalSpec value =+    T.unlines+        [ "context svc"+        , ""+        , "workqueue Queue {"+        , "  queue logical = \"queue\""+        , "  derive physical = \"queue\""+        , "    dlq = \"queue_dlq\""+        , "    table = \"pgmq.q_queue\""+        , "  payload Job { }"+        , "  retry maxRetries = " <> value <> " delay = 5s dlq = on"+        , "  disposition { }"+        , "}"+        ]++timerDecimalSpec :: T.Text -> T.Text+timerDecimalSpec value =+    T.replace+        "max-attempts 5"+        ("max-attempts " <> value)+        (renderSpec (Spec "svc" Nothing Nothing [] [] [] [] [NProcess (processWithLiteral "literal")]))++identifierHygieneSpec :: T.Text+identifierHygieneSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate thing"+        , "  regs"+        , "  states Open"+        , ""+        , "  command DoIt { data }"+        ]++vertexCollisionSpec :: T.Text+vertexCollisionSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Reservation"+        , "  regs"+        , "  states Created"+        , ""+        , "  event ReservationCreated { }"+        ]++underscoreNodeSpec :: T.Text+underscoreNodeSpec =+    T.unlines+        [ "context svc"+        , ""+        , "contract _contract {"+        , "  schemaVersion 1"+        , "  discriminator kind"+        , "}"+        ]++unicodeIdentifierSpec :: T.Text+unicodeIdentifierSpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate Résumé"+        , "  regs"+        , "  states Open"+        ]++emptyStatesSpec :: Spec+emptyStatesSpec =+    Spec+        "svc"+        Nothing+        Nothing+        []+        []+        []+        []+        [NAggregate (Aggregate "Thing" [] [] [] [] [] Nothing Nothing Nothing noLoc)]++crossFamilyBoundarySpec :: T.Text+crossFamilyBoundarySpec =+    T.unlines+        [ "context svc"+        , ""+        , "aggregate First"+        , "  regs"+        , "  states A B"+        , "  command Go { }"+        , "  A -- Go -->"+        , "    emit Changed"+        , "    goto B"+        , ""+        , "emit Output {"+        , "  contract Contract"+        , "  topic events"+        , "  source \"source\""+        , "  key thingId"+        , "  map status { _ => skip }"+        , "  messageId derive hole"+        , "  idempotencyKey derive hole"+        , "}"+        , ""+        , "aggregate Second"+        , "  regs"+        , "  states"+        , ""+        , "dispatch QueueDispatch {"+        , "  source readModel = source key = thingId"+        , "  fanout body = resolveFanout"+        , "  dedup key = thingId"+        , "    seenIn readModel = seen field = thingId"+        , "    seenIn queue = workQueue field = thingId"+        , "  enqueue to = workQueue"+        , "}"+        ]++--------------------------------------------------------------------------------+-- Generators (bounded; restricted to valid, non-reserved identifiers)+--------------------------------------------------------------------------------++{- | Text that exercises every supported escape plus notation punctuation that+used to be able to split one emit-map row into several rows.+-}+genAdversarialText :: Gen T.Text+genAdversarialText =+    T.concat+        <$> resize+            20+            (listOf (elements ["a", "Z", "\"", "\\", "\n", "\t", "\r", "=>", "#", "{", "}", " "]))++{- | One spec carrying the same adversarial value through three distinct+printer paths: a contract topic, an emit-map value, and a quote-wrapped+field-binding literal.+-}+escapedSpec :: T.Text -> Spec+escapedSpec value =+    Spec+        "escape"+        Nothing+        Nothing+        []+        []+        []+        []+        [ NContract+            ContractNode+                { ctrName = "Contract"+                , ctrSchemaVersion = 1+                , ctrDiscriminator = "kind"+                , ctrTopics = [("events", value)]+                , ctrEvents = []+                , ctrLoc = noLoc+                }+        , NEmit+            EmitNode+                { emName = "Emit"+                , emContract = "Contract"+                , emTopic = "events"+                , emSource = "source"+                , emKey = "key"+                , emDiscriminant = "status"+                , emMap = [EmitMapRow value "Event" noLoc]+                , emSkip = True+                , emMessageId = DeriveSpec Nothing+                , emIdempotencyKey = DeriveSpec Nothing+                , emLoc = noLoc+                }+        , NProcess (processWithLiteral value)+        ]++processWithLiteral :: T.Text -> ProcessNode+processWithLiteral value =+    ProcessNode+        { procId = "Process"+        , procName = "process"+        , procInput = InputDecl "Input" []+        , procCorrelate = CorrelateDecl "key" "idText"+        , procSaga = SagaRef "Saga" "saga"+        , procTarget = "Target"+        , procProjections = []+        , procHandle =+            HandleNode+                { hOn = "Input"+                , hAdvance = AdvanceNode "Advance" [FieldBinding "literal" (Just ("\"" <> value <> "\""))]+                , hDispatch = []+                , hSchedule = "timer"+                }+        , procRejected = PolHalt+        , procPoison = PolHalt+        , procTimer =+            TimerNode+                { tmName = "timer"+                , tmId = IdExpr UuidV5Id "timer:"+                , tmFireAt = FireAtExpr "observedAt" "5m"+                , tmPayload = []+                , tmFire =+                    FireNode+                        { fireTarget = "Target"+                        , fireKey = "correlationId"+                        , fireCommand = "Fire"+                        , fireFields = []+                        , fireFiredEventId = IdExpr UuidV5Id "fired:"+                        , fireDisposition = FireDisposition OFired OFired ORetry ORetry ORetry+                        }+                , tmDecodeUnknown = "Cancelled"+                , tmMaxAttempts = 5+                , tmDeadLetter = "exhausted"+                , tmLoc = noLoc+                }+        , procLoc = noLoc+        }++genName :: Gen Name+genName =+    frequency+        [+            ( 3+            , do+                base <- elements ["Aa", "Bb", "Cc", "Dd", "St", "Cmd", "Ev", "Reg", "Fld", "Foo", "Bar", "Qux"]+                n <- choose (0, 9 :: Int)+                pure (T.pack (base <> show n))+            )+        , (1, elements ["data1", "typeA", "whereX", "gotoX", "guardY", "emitZ", "_lead"])+        ]++genWire :: Gen T.Text+genWire = do+    base <- elements ["red", "blue", "green", "ctorName", "camelCase", "rsv", "hosp", "held", "partial-divert", "1st"]+    n <- choose (0, 9 :: Int)+    pure (T.pack (base <> show n))++genWireWord :: Gen T.Text+genWireWord = genWire++smallList :: Gen a -> Gen [a]+smallList g = choose (0, 3 :: Int) >>= \n -> vectorOf n g++nonEmptyList :: Gen a -> Gen [a]+nonEmptyList g = choose (1, 3 :: Int) >>= \n -> vectorOf n g++genMaybe :: Gen a -> Gen (Maybe a)+genMaybe g = oneof [pure Nothing, Just <$> g]++genCmp :: Gen CmpOp+genCmp = elements [OpEq, OpNeq, OpLt, OpLe, OpGt, OpGe]++genAtom :: Gen Expr+genAtom = EAtom <$> oneof [AName <$> genName, ABool <$> arbitrary]++genExpr :: Gen Expr+genExpr = go (3 :: Int)+  where+    go 0 = genAtom+    go d =+        oneof+            [ genAtom+            , EOr <$> go (d - 1) <*> go (d - 1)+            , EAnd <$> go (d - 1) <*> go (d - 1)+            , ECmp <$> genCmp <*> go (d - 1) <*> go (d - 1)+            ]++genField :: Gen Field+genField = Field <$> genName <*> oneof [pure Nothing, Just <$> genName]++genReg :: Gen RegDecl+genReg = RegDecl <$> genName <*> genName <*> genRegInitial <*> pure noLoc++genRegInitial :: Gen RegInitial+genRegInitial = oneof [RegInitBare <$> genName, RegInitText <$> genAdversarialText]++genState :: Gen StateDecl+genState = StateDecl <$> genName <*> arbitrary <*> pure noLoc++genCommand :: Gen Command+genCommand = Command <$> genName <*> smallList genField <*> pure noLoc++genEvent :: Gen Event+genEvent = do+    name <- genName+    eventBody <- body+    version <- choose (1, 3)+    upcast <- genMaybe ((,) <$> choose (0, 3) <*> pure Hole)+    (retiring, deprecated) <- elements [(False, False), (True, False), (False, True)]+    pure+        Event+            { evName = name+            , evBody = eventBody+            , evVersion = version+            , evUpcastFrom = upcast+            , evRetiring = retiring+            , evDeprecated = deprecated+            , evLoc = noLoc+            }+  where+    body = oneof [EventFromCommand <$> genName, EventFields <$> smallList genField]++genTransition :: Gen Transition+genTransition =+    Transition+        <$> genName+        <*> genName+        <*> genMaybe genExpr+        <*> smallList ((,) <$> genName <*> genExpr)+        <*> smallList genName+        <*> genName+        <*> elements [TmLive, TmReplayOnly]+        <*> pure noLoc++genWireSpec :: Gen WireSpec+genWireSpec = WireSpec <$> genWire <*> genWire <*> (getNonNegative <$> arbitrary)++genProjection :: Gen ProjectionSpec+genProjection =+    ProjectionSpec+        <$> genName+        <*> genMaybe (elements [Strong, Eventual])+        <*> genName+        <*> genMaybe (Mapping <$> smallList ((,) <$> genName <*> genWire) <*> arbitrary)+        <*> pure noLoc++genAggregate :: Gen Aggregate+genAggregate =+    Aggregate+        <$> genName+        <*> smallList genReg+        <*> smallList genState+        <*> smallList genCommand+        <*> smallList genEvent+        <*> smallList genTransition+        <*> genMaybe genWireSpec+        <*> genMaybe genProjection+        <*> genMaybe (SnapshotSpec <$> oneof [SnapEvery <$> choose (0, 5), pure SnapOnTerminal] <*> choose (0, 5) <*> genAdversarialText <*> pure noLoc)+        <*> pure noLoc++genDottedRef :: Gen T.Text+genDottedRef = elements ["input.id", "input.hospitalId", "timer.id", "correlationId", "payload.messageId"]++genWindow :: Gen T.Text+genWindow = elements ["0s", "5s", "2m", "1h"]++genFieldBinding :: Gen FieldBinding+genFieldBinding =+    FieldBinding+        <$> genName+        <*> oneof+            [ pure Nothing+            , Just <$> genDottedRef+            , Just . (\raw -> "\"" <> raw <> "\"") <$> genAdversarialText+            ]++genDispatchDisposition :: Gen DispatchDisposition+genDispatchDisposition = DispatchDisposition <$> genDisp <*> genDisp <*> genDisp+  where+    genDisp = oneof [pure DAckOk, pure DRetry, DDeadLetter <$> genAdversarialText]++genDispatchNode :: Gen DispatchNode+genDispatchNode =+    DispatchNode+        <$> genName+        <*> genDottedRef+        <*> genName+        <*> smallList genFieldBinding+        <*> genDispatchDisposition+        <*> pure noLoc++genFireDisposition :: Gen FireDisposition+genFireDisposition =+    FireDisposition+        <$> elements [OFired, ORetry]+        <*> elements [OFired, ORetry]+        <*> elements [OFired, ORetry]+        <*> elements [OFired, ORetry]+        <*> elements [OFired, ORetry]++genIdExpr :: Gen IdExpr+genIdExpr = IdExpr UuidV5Id <$> genAdversarialText++genFireNode :: Gen FireNode+genFireNode =+    FireNode+        <$> genName+        <*> genDottedRef+        <*> genName+        <*> smallList genFieldBinding+        <*> genIdExpr+        <*> genFireDisposition++genTimerNode :: Gen TimerNode+genTimerNode =+    TimerNode+        <$> genName+        <*> genIdExpr+        <*> (FireAtExpr <$> genName <*> genWindow)+        <*> smallList genFieldBinding+        <*> genFireNode+        <*> genName+        <*> choose (0, 5)+        <*> genAdversarialText+        <*> pure noLoc++genProcess :: Gen ProcessNode+genProcess =+    ProcessNode+        <$> genName+        <*> genAdversarialText+        <*> (InputDecl <$> genName <*> smallList genField)+        <*> (CorrelateDecl <$> genName <*> genName)+        <*> (SagaRef <$> genName <*> genAdversarialText)+        <*> genName+        <*> smallList genName+        <*> (HandleNode <$> genName <*> (AdvanceNode <$> genName <*> smallList genFieldBinding) <*> smallList genDispatchNode <*> genName)+        <*> elements [PolHalt, PolDeadLetter, PolSkip]+        <*> elements [PolHalt, PolDeadLetter, PolSkip]+        <*> genTimerNode+        <*> pure noLoc++genResolveSource :: Gen ResolveSource+genResolveSource = oneof [ResolveReadModel <$> genName, pure ResolveHole]++genRouter :: Gen RouterNode+genRouter =+    RouterNode+        <$> genName+        <*> genAdversarialText+        <*> (InputDecl <$> genName <*> smallList genField)+        <*> (CorrelateDecl <$> genName <*> genName)+        <*> (ResolveDecl <$> genResolveSource <*> smallList genName <*> pure noLoc)+        <*> genName+        <*> smallList genName+        <*> (RouterDispatchNode <$> genName <*> smallList genFieldBinding <*> genDispatchDisposition <*> pure noLoc)+        <*> elements [PolHalt, PolDeadLetter, PolSkip]+        <*> elements [PolHalt, PolDeadLetter, PolSkip]+        <*> pure noLoc++genContractField :: Gen ContractField+genContractField = ContractField <$> genName <*> oneof [CTypeId <$> genAdversarialText, pure CText, pure CInt]++genContractEvent :: Gen ContractEvent+genContractEvent = ContractEvent <$> genName <*> genName <*> smallList genContractField++genContract :: Gen ContractNode+genContract =+    ContractNode+        <$> genName+        <*> choose (0, 5)+        <*> genName+        <*> smallList ((,) <$> genName <*> genAdversarialText)+        <*> smallList genContractEvent+        <*> pure noLoc++genWireSource :: Gen WireSource+genWireSource = oneof [SrcHeader <$> genAdversarialText, pure SrcBody, pure SrcKafkaKey, pure SrcKafkaCursor]++genInboxAction :: Gen InboxAction+genInboxAction = oneof [pure IAckOk, IRetry <$> genWindow, IDeadLetter <$> genMaybe genAdversarialText]++genDispositionRow :: Gen DispositionRow+genDispositionRow = DispositionRow <$> genName <*> genInboxAction <*> pure noLoc++genDecodeSpec :: Gen DecodeSpec+genDecodeSpec =+    DecodeSpec+        <$> ((\first second -> first <> " " <> second) <$> genWireWord <*> genWireWord)+        <*> arbitrary+        <*> choose (0, 5)++genIntake :: Gen IntakeNode+genIntake =+    IntakeNode+        <$> genName+        <*> genName+        <*> genName+        <*> nonEmptyList genName+        <*> smallList (BindRow <$> genName <*> genWireSource <*> arbitrary <*> arbitrary)+        <*> genName+        <*> genName+        <*> elements [InkPersistFull, InkPersistDedupeOnly]+        <*> genDecodeSpec+        <*> smallList genDispositionRow+        <*> pure noLoc++genDeriveSpec :: Gen DeriveSpec+genDeriveSpec = DeriveSpec <$> genMaybe genAdversarialText++genEmit :: Gen EmitNode+genEmit =+    EmitNode+        <$> genName+        <*> genName+        <*> genName+        <*> genAdversarialText+        <*> genName+        <*> genName+        <*> smallList (EmitMapRow <$> genAdversarialText <*> genName <*> pure noLoc)+        <*> arbitrary+        <*> genDeriveSpec+        <*> genDeriveSpec+        <*> pure noLoc++genPublisher :: Gen PublisherNode+genPublisher =+    PublisherNode+        <$> genName+        <*> genName+        <*> genName+        <*> choose (0, 5)+        <*> (BackoffSpec <$> genName <*> genWindow <*> genMaybe genWindow <*> genMaybe (elements ["1.0", "2.0", "3"]))+        <*> genName+        <*> pure noLoc++genWqField :: Gen WqField+genWqField = WqField <$> genName <*> genAdversarialText <*> genName <*> arbitrary++genWqDispRow :: Gen WqDispRow+genWqDispRow = WqDispRow <$> genName <*> genInboxAction <*> pure noLoc++genWorkqueue :: Gen WorkqueueNode+genWorkqueue =+    WorkqueueNode+        <$> genName+        <*> genAdversarialText+        <*> genAdversarialText+        <*> genAdversarialText+        <*> genAdversarialText+        <*> elements [WqUnordered, WqFifoThroughput, WqFifoRoundRobin]+        <*> genMaybe (WqGroupKey <$> genName <*> genName <*> genMaybe genAdversarialText)+        <*> oneof [pure WqStandard, pure WqUnlogged, WqPartitioned <$> genAdversarialText <*> genAdversarialText]+        <*> genName+        <*> smallList genWqField+        <*> choose (0, 5)+        <*> genWindow+        <*> arbitrary+        <*> smallList genWqDispRow+        <*> pure noLoc++genReadModel :: Gen ReadModelNode+genReadModel =+    ReadModelNode+        <$> genName+        <*> genAdversarialText+        <*> genAdversarialText+        <*> smallList (RmColumn <$> genWireWord <*> genName <*> arbitrary)+        <*> choose (0, 5)+        <*> genAdversarialText+        <*> elements [Strong, Eventual]+        <*> genMaybe (oneof [pure RmEntireLog, RmCategory <$> genAdversarialText])+        <*> elements [RmInline, RmSubscription]+        <*> genMaybe genAdversarialText+        <*> pure noLoc++genPgmqDispatch :: Gen PgmqDispatchNode+genPgmqDispatch =+    PgmqDispatchNode+        <$> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> genName+        <*> pure noLoc++genWfBodyItem :: Gen WfBodyItem+genWfBodyItem = sized go+  where+    go size =+        oneof $+            [ WfStep <$> genWireWord <*> genName <*> pure noLoc+            , WfAwait <$> genWireWord <*> genName <*> pure noLoc+            , WfSleep <$> genWireWord <*> genName <*> pure noLoc+            , WfChild <$> genWireWord <*> genName <*> genName <*> pure noLoc+            , WfContinueAsNew <$> genName <*> pure noLoc+            ]+                ++ [ WfPatch <$> genWireWord <*> resize (size `div` 2) (smallList genWfBodyItem) <*> pure noLoc+                   | size > 0+                   ]++genWorkflow :: Gen WorkflowNode+genWorkflow =+    WorkflowNode+        <$> genName+        <*> genAdversarialText+        <*> genName+        <*> smallList genField+        <*> genName+        <*> genMaybe genName+        <*> genName+        <*> smallList genWfBodyItem+        <*> pure noLoc++genOperationShape :: Gen OperationShape+genOperationShape =+    oneof+        [ CommandOp <$> genName <*> genName <*> genName <*> smallList genName+        , QueryOp <$> genName <*> genName <*> ((\parts -> T.unwords parts) <$> nonEmptyList genName) <*> genName+        , SignalOp <$> genWireWord <*> genName <*> genName <*> genName <*> genName+        , RunOp <$> genName <*> genName <*> genName+        ]++genOperation :: Gen OperationNode+genOperation = OperationNode <$> genName <*> genOperationShape <*> pure noLoc++allNodeTags :: [String]+allNodeTags = ["aggregate", "process", "router", "contract", "intake", "emit", "publisher", "workqueue", "pgmq-dispatch", "readmodel", "workflow", "operation"]++nodeTag :: Node -> String+nodeTag = \case+    NAggregate _ -> "aggregate"+    NProcess _ -> "process"+    NRouter _ -> "router"+    NContract _ -> "contract"+    NIntake _ -> "intake"+    NEmit _ -> "emit"+    NPublisher _ -> "publisher"+    NWorkqueue _ -> "workqueue"+    NPgmqDispatch _ -> "pgmq-dispatch"+    NReadModel _ -> "readmodel"+    NWorkflow _ -> "workflow"+    NOperation _ -> "operation"++genId :: Gen IdDecl+genId = IdDecl <$> genName <*> genWire <*> pure noLoc++genEnum :: Gen EnumDecl+genEnum = EnumDecl <$> genName <*> smallList ((,) <$> genName <*> genWire) <*> pure noLoc++genRule :: Gen RuleDecl+genRule =+    RuleDecl+        <$> genName+        <*> genName+        <*> genName+        <*> nonEmptyList ((,) <$> genName <*> genExpr)+        <*> pure noLoc++genMappedDecls :: Gen [MappedDecl]+genMappedDecls = do+    count <- choose (0, 4 :: Int)+    let names = take count ["MappedA", "MappedB", "MappedC", "MappedD"]+    traverse (genMappedDecl names) names++genMappedDecl :: [Name] -> Name -> Gen MappedDecl+genMappedDecl names name =+    oneof+        [ MappedStructural name+            <$> genMaybe genHaskellSource+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMappedShape names+            <*> pure noLoc+        , MappedOpaque name+            <$> genMaybe genHaskellSource+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> genMaybe genAdversarialText+            <*> pure noLoc+        ]++genHaskellSource :: Gen HaskellSource+genHaskellSource =+    HaskellSource+        <$> genWire+        <*> genModuleRoot+        <*> genName++genMappedShape :: [Name] -> Gen MappedShape+genMappedShape names =+    oneof+        [ ShapeRecord+            <$> genName+            <*> elements [RejectUnknown, IgnoreUnknown]+            <*> smallList (genWireField names)+        , ShapeEnum <$> smallList (WireEnum <$> genName <*> genAdversarialText <*> pure noLoc)+        , ShapeUnion+            <$> (TaggedObject <$> genAdversarialText <*> genAdversarialText <*> elements [RejectUnknown, IgnoreUnknown])+            <*> smallList (WireArm <$> genName <*> genAdversarialText <*> genMaybe (genTypeExpr names) <*> pure noLoc)+        ]++genWireField :: [Name] -> Gen WireField+genWireField names =+    WireField+        <$> genName+        <*> genAdversarialText+        <*> genTypeExpr names+        <*> elements [PRequired, POptional]+        <*> genMaybe genOnMissing+        <*> pure noLoc++genTypeExpr :: [Name] -> Gen TypeExpr+genTypeExpr names = sized (go . min 3)+  where+    go 0 = base+    go depth =+        frequency+            [ (4, base)+            , (1, TOptional <$> go (depth - 1))+            , (1, TList <$> go (depth - 1))+            , (1, TMap <$> go (depth - 1))+            ]+    base = elements ([TText, TInt, TBool, TNatural, TTime, TJson] ++ map TRef names)++genOnMissing :: Gen OnMissing+genOnMissing =+    oneof+        [ pure OmNull+        , OmText <$> genAdversarialText+        , OmInt <$> choose (-10, 10)+        , OmBool <$> arbitrary+        , pure OmEmptyList+        , pure OmEmptyMap+        , OmCtor <$> genName+        ]++genSpec :: Gen Spec+genSpec = do+    contextName <- genWire+    moduleRoot <- genMaybe genModuleRoot+    layout <- genMaybe (elements [GeneratedPrefix, CollocatedLeaf])+    ids <- smallList genId+    enums <- smallList genEnum+    rules <- smallList genRule+    mapped <- genMappedDecls+    nodes <- smallList genNode+    pure (Spec contextName moduleRoot layout ids enums rules mapped nodes)   where     genNode =         oneof
+ test/conformance-codec-compare/Conformance/CodecCompare/Historical.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE OverloadedRecordDot #-}++module Conformance.CodecCompare.Historical (+    historicalArtifactInfoCodec,+    generatedEquivalentArtifactInfoCodec,+)+where++import Conformance.Structural.Domain qualified as Domain+import Data.Aeson (Value (..), object, withObject, withText, (.!=), (.:), (.:?), (.=))+import Data.Aeson.Types (Parser, parseEither)+import Data.Text (Text)+import Data.Text qualified as T+import Generated.StructuralConformance.ArtifactCatalog.Codec qualified as GeneratedCodec+import Keiro.Dsl.CodecCompare (HistoricalCodec (..))++historicalArtifactInfoCodec :: HistoricalCodec Domain.ArtifactInfo+historicalArtifactInfoCodec =+    HistoricalCodec+        { hcIdentity = "conformance.structural.ArtifactInfo.aeson"+        , hcVersion = "legacy-v3"+        , hcEncode = encodeArtifactInfo+        , hcDecode = either (Left . T.pack) Right . parseEither parseArtifactInfo+        }++{- | Acceptance control: this stands in for the historical codec after its two+migration quirks have been removed.  Keeping it beside the genuinely+historical codec lets the consumer-level test exercise the runner's success+exit condition as well as its refusal path.+-}+generatedEquivalentArtifactInfoCodec :: HistoricalCodec Domain.ArtifactInfo+generatedEquivalentArtifactInfoCodec =+    HistoricalCodec+        { hcIdentity = "conformance.structural.ArtifactInfo.generated-equivalent"+        , hcVersion = "cutover-v4"+        , hcEncode = GeneratedCodec.encodeArtifactInfoMapped+        , hcDecode = GeneratedCodec.decodeArtifactInfoMapped+        }++encodeArtifactInfo :: Domain.ArtifactInfo -> Value+encodeArtifactInfo value =+    object+        ( [ "artifact_key" .= value.artifactKey+          , "display_name" .= value.displayName+          , "artifact_kind" .= encodeArtifactKind value.artifactKind+          , "location" .= encodeLocation value.location+          , "metadata" .= object ["note" .= value.metadata.note]+          , "active" .= value.active+          , "tags" .= value.tags+          ]+            <> maybe [] (pure . ("artifact_hash" .=)) value.artifactHash+        )++encodeArtifactKind :: Domain.ArtifactKind -> Value+encodeArtifactKind Domain.Guide = String "guide"+encodeArtifactKind Domain.Reference = String "reference"++encodeLocation :: Domain.ArtifactLocation -> Value+encodeLocation location = case location of+    Domain.LocalFile payload -> tagged "local_file" (Just payload)+    Domain.LocalDir payload -> tagged "local_dir" (Just payload)+    Domain.RepoPath payload -> tagged "repo_path" (Just payload)+    Domain.LocUrl payload -> tagged "url" (Just payload)+    Domain.Canonical -> tagged "Canonical" Nothing+  where+    tagged :: Text -> Maybe Text -> Value+    tagged tag payload = object (["tag" .= tag] <> maybe [] (pure . ("contents" .=)) payload)++parseArtifactInfo :: Value -> Parser Domain.ArtifactInfo+parseArtifactInfo = withObject "historical ArtifactInfo" $ \value ->+    Domain.ArtifactInfo+        <$> value .: "artifact_key"+        <*> value .: "display_name"+        <*> value .:? "artifact_hash"+        <*> (value .:? "artifact_kind" >>= maybe (pure Domain.Guide) parseArtifactKind)+        <*> (value .: "location" >>= parseLocation)+        <*> (value .: "metadata" >>= parseMetadata)+        <*> value .:? "active" .!= False+        <*> value .:? "tags" .!= []++parseArtifactKind :: Value -> Parser Domain.ArtifactKind+parseArtifactKind = withText "historical ArtifactKind" $ \value -> case value of+    "guide" -> pure Domain.Guide+    "reference" -> pure Domain.Reference+    _ -> fail "unknown historical artifact kind"++parseLocation :: Value -> Parser Domain.ArtifactLocation+parseLocation = withObject "historical ArtifactLocation" $ \value -> do+    tag <- value .: "tag" :: Parser Text+    case tag of+        "local_file" -> Domain.LocalFile <$> value .: "contents"+        "local_dir" -> Domain.LocalDir <$> value .: "contents"+        "repo_path" -> Domain.RepoPath <$> value .: "contents"+        "url" -> Domain.LocUrl <$> value .: "contents"+        "Canonical" -> pure Domain.Canonical+        _ -> fail "unknown historical artifact location"++parseMetadata :: Value -> Parser Domain.ArtifactMetadata+parseMetadata = withObject "historical ArtifactMetadata" $ \value ->+    Domain.ArtifactMetadata <$> value .:? "note"
+ test/conformance-codec-compare/Main.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE ImportQualifiedPost #-}++module Main (main) where++import Conformance.CodecCompare.Historical (generatedEquivalentArtifactInfoCodec, historicalArtifactInfoCodec)+import Control.Monad (unless)+import Data.Text qualified as T+import Generated.StructuralConformance.Structural.CodecCompare.ArtifactInfo (compareWithHistorical)+import Keiro.Dsl.CodecCompare+import System.Exit (exitFailure)++main :: IO ()+main = do+    report <- compareWithHistorical historicalArtifactInfoCodec corpusPath+    missingArm <- compareWithHistorical historicalArtifactInfoCodec missingArmPath+    parityReport <- compareWithHistorical generatedEquivalentArtifactInfoCodec parityCorpusPath+    let differences =+            [ difference+            | observation <- crObservations report+            , RequiresVersionWork difference <- [classifiedVerdict observation]+            ]+        assertions =+            [ ("comparison has explicit differences", not (null differences))+            , ("omitted key is not parity", any isArtifactHashDifference differences)+            , ("legacy union tag is not parity", any isCanonicalTagDifference differences)+            , ("historical corpus is valid", null (crInputIssues report))+            , ("historical and typed branch coverage is complete", null (crCoverageGaps report))+            , ("differences make the report fail", not (reportSucceeded report))+            , ("authority framing is mandatory", "MIGRATION EVIDENCE ONLY" `T.isInfixOf` renderCompareReport report)+            , ("missing canonical arm is a coverage gap", any isCanonicalGap (crCoverageGaps missingArm))+            , ("removing the historical quirks yields parity", reportSucceeded parityReport)+            , ("parity retains authority framing", "MIGRATION EVIDENCE ONLY" `T.isInfixOf` renderCompareReport parityReport)+            ]+    mapM_ (\(label, ok) -> putStrLn ((if ok then "PASS  " else "FAIL  ") <> label)) assertions+    unless (all snd assertions) exitFailure+  where+    corpusPath = "test/conformance-codec-compare/fixtures/artifact-info"+    missingArmPath = "test/conformance-codec-compare/fixtures/missing-arm"+    parityCorpusPath = "test/conformance-codec-compare/fixtures/generated-parity"++isArtifactHashDifference :: ComparisonDifference -> Bool+isArtifactHashDifference difference = case difference of+    EncodedValueDifference (JsonPointer pointer) _ _ -> pointer == "/artifact_hash"+    _ -> False++isCanonicalTagDifference :: ComparisonDifference -> Bool+isCanonicalTagDifference (GeneratedDecodeRejected reason) = "unknown ArtifactLocation union tag" `T.isInfixOf` reason+isCanonicalTagDifference _ = False++isCanonicalGap :: CoverageGap -> Bool+isCanonicalGap gap = case cgKind gap of+    UnionArm arm -> arm == "canonical"+    _ -> False
+ test/conformance-coldstart/Generated/Billing/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.Billing.ReplayAudit (auditTargets) where++import Generated.Billing.Subscription.EventStream qualified as Subscription+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Subscription.subscriptionEventStream+            , category = Stream.categoryText Subscription.subscriptionCategory+            , mkStream = streamInCategory (Stream.categoryText Subscription.subscriptionCategory)+            }+    ]
test/conformance-coldstart/Generated/Billing/Subscription/Harness.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.@@ -6,7 +8,7 @@ import Billing.Subscription.Holes (subscriptionTransducer) import Generated.Billing.Subscription.Codec (encodeSubscriptionEvent, parseSubscriptionEvent, subscriptionCodec) import Generated.Billing.Subscription.Domain-import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!)) import Keiro.Codec (eventType)  {- | (label, passed). A driver runs these and exits non-zero on any False,@@ -21,6 +23,7 @@     , ("golden round-trip: SubscriptionCancelled", roundTrips sampleEventSubscriptionCancelled)     , ("accepts ActivateSubscription from SubscriptionInactive", acceptActivateSubscription)     ]+        ++ forwardReplayActivateSubscription  roundTrips :: SubscriptionEvent -> Bool roundTrips e = parseSubscriptionEvent (eventType subscriptionCodec e) (encodeSubscriptionEvent e) == Right e@@ -36,3 +39,23 @@     case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) ((ActivateSubscription (ActivateSubscriptionData (SubscriptionId "sample") (CustomerId "sample") Paid))) of         Just (v, _, _) -> v == SubscriptionActive         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayActivateSubscription :: [(String, Bool)]+forwardReplayActivateSubscription =+    case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) ((ActivateSubscription (ActivateSubscriptionData (SubscriptionId "sample") (CustomerId "sample") Paid))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseSubscriptionEvent (eventType subscriptionCodec event) (encodeSubscriptionEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register plan", (replayRegs ! #plan) == (forwardRegs ! #plan))+                            , (prefix <> "register subscriptionState", (replayRegs ! #subscriptionState) == (forwardRegs ! #subscriptionState))+                            ]+  where+    prefix = "forward/replay equality: ActivateSubscription from SubscriptionInactive -- "
+ test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.++{- | Versioned job payload envelope: @{\"v\",\"t\",\"data\"}@.++Deploy workers before producers when raising its schema version. Do not+adopt this codec on a non-empty bare-payload queue without draining it+(or supplying a transitional codec), or in-flight messages will+dead-letter. This is telemetry-neutral:+docs/adr/0001-keiro-pgmq-job-processing-telemetry-contract.md owns+spans and acknowledgement vocabulary.+-}+module Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem, encodeReservationWorkItem, parseReservationWorkItem)+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)++reservationWorkPayloadCodec :: Codec ReservationWorkItem+reservationWorkPayloadCodec =+    Codec+        { eventTypes = EventType "ReservationWorkItem" :| []+        , eventType = \_ -> EventType "ReservationWorkItem"+        , schemaVersion = 1+        , encode = encodeReservationWorkItem+        , decode = \_ -> parseReservationWorkItem+        , upcasters = []+        }++reservationWorkJobCodec :: JobCodec ReservationWorkItem+reservationWorkJobCodec = keiroJobCodec reservationWorkPayloadCodec
test/conformance-dispatch-full/HospitalCapacity/ReservationWork/WorkqueueJob.hs view
@@ -12,11 +12,9 @@ import Effectful (Eff) import Generated.HospitalCapacity.Reservation_work.Queue (     ReservationWorkItem,-    encodeReservationWorkItem,-    parseReservationWorkItem,  )+import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec) import Generated.HospitalCapacity.Reservation_work.QueuePolicy (retryPolicy)-import Keiro.PGMQ.Codec (mkJobCodec) import Keiro.PGMQ.Job (Job (..), JobOutcome (..)) import Keiro.PGMQ.Runtime (queueRef) @@ -26,10 +24,7 @@     Job         { jobName = "reservation-work"         , jobQueue = queueRef "hospital_capacity.reservation_work"-        , jobCodec =-            mkJobCodec-                encodeReservationWorkItem-                parseReservationWorkItem+        , jobCodec = reservationWorkJobCodec         , jobPolicy = retryPolicy         } 
test/conformance-newsurface/Generated/TransferRouting/Hospital/Harness.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.@@ -5,7 +7,7 @@  import Generated.TransferRouting.Hospital.Codec (encodeHospitalEvent, hospitalCodec, parseHospitalEvent) import Generated.TransferRouting.Hospital.Domain-import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer) import Keiro.Codec (eventType) import TransferRouting.Hospital.Holes (hospitalTransducer) @@ -20,15 +22,34 @@     , ("golden round-trip: AcceptedTransferNeedRouted", roundTrips sampleEventAcceptedTransferNeedRouted)     , ("accepts RouteAcceptedTransferNeed from HospitalAccepting", acceptRouteAcceptedTransferNeed)     ]+        ++ forwardReplayRouteAcceptedTransferNeed  roundTrips :: HospitalEvent -> Bool roundTrips e = parseHospitalEvent (eventType hospitalCodec e) (encodeHospitalEvent e) == Right e  sampleEventAcceptedTransferNeedRouted :: HospitalEvent-sampleEventAcceptedTransferNeedRouted = (AcceptedTransferNeedRouted (AcceptedTransferNeedRoutedData "sample" "sample"))+sampleEventAcceptedTransferNeedRouted = (AcceptedTransferNeedRouted (AcceptedTransferNeedRoutedData "sample-transferNeedId" "sample-hospitalId"))  acceptRouteAcceptedTransferNeed :: Bool acceptRouteAcceptedTransferNeed =-    case step hospitalTransducer (HospitalAccepting, initialHospitalRegs) ((RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "sample" "sample"))) of+    case step hospitalTransducer (HospitalAccepting, initialHospitalRegs) ((RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "sample-transferNeedId" "sample-hospitalId"))) of         Just (v, _, _) -> v == HospitalAccepting         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayRouteAcceptedTransferNeed :: [(String, Bool)]+forwardReplayRouteAcceptedTransferNeed =+    case step hospitalTransducer (HospitalAccepting, initialHospitalRegs) ((RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "sample-transferNeedId" "sample-hospitalId"))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, _forwardRegs, emitted) ->+            case mapM (\event -> parseHospitalEvent (eventType hospitalCodec event) (encodeHospitalEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither hospitalTransducer (HospitalAccepting, initialHospitalRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, _replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            ]+  where+    prefix = "forward/replay equality: RouteAcceptedTransferNeed from HospitalAccepting -- "
+ test/conformance-newsurface/Generated/TransferRouting/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.TransferRouting.ReplayAudit (auditTargets) where++import Generated.TransferRouting.Hospital.EventStream qualified as Hospital+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Hospital.hospitalEventStream+            , category = Stream.categoryText Hospital.hospitalCategory+            , mkStream = streamInCategory (Stream.categoryText Hospital.hospitalCategory)+            }+    ]
+ test/conformance-process-full/Generated/SurgeDemo/ReplayAudit.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.SurgeDemo.ReplayAudit (auditTargets) where++import Generated.SurgeDemo.Hospital.EventStream qualified as Hospital+import Generated.SurgeDemo.Surge.EventStream qualified as Surge+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Hospital.hospitalEventStream+            , category = Stream.categoryText Hospital.hospitalCategory+            , mkStream = streamInCategory (Stream.categoryText Hospital.hospitalCategory)+            }+    , SomeAuditTarget+        AuditTarget+            { eventStream = Surge.surgeEventStream+            , category = Stream.categoryText Surge.surgeCategory+            , mkStream = streamInCategory (Stream.categoryText Surge.surgeCategory)+            }+    ]
+ test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.++{- | Versioned job payload envelope: @{\"v\",\"t\",\"data\"}@.++Deploy workers before producers when raising its schema version. Do not+adopt this codec on a non-empty bare-payload queue without draining it+(or supplying a transitional codec), or in-flight messages will+dead-letter. This is telemetry-neutral:+docs/adr/0001-keiro-pgmq-job-processing-telemetry-contract.md owns+spans and acknowledgement vocabulary.+-}+module Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem, encodeReservationWorkItem, parseReservationWorkItem)+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)++reservationWorkPayloadCodec :: Codec ReservationWorkItem+reservationWorkPayloadCodec =+    Codec+        { eventTypes = EventType "ReservationWorkItem" :| []+        , eventType = \_ -> EventType "ReservationWorkItem"+        , schemaVersion = 1+        , encode = encodeReservationWorkItem+        , decode = \_ -> parseReservationWorkItem+        , upcasters = []+        }++reservationWorkJobCodec :: JobCodec ReservationWorkItem+reservationWorkJobCodec = keiroJobCodec reservationWorkPayloadCodec
test/conformance-queue-runtime/Main.hs view
@@ -8,10 +8,10 @@  import Control.Monad (unless) import Data.Text (Text)-import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem (..), encodeReservationWorkItem, groupKeyFor, parseReservationWorkItem)+import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem (..), groupKeyFor)+import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec) import Generated.HospitalCapacity.Reservation_work.QueuePolicy (jobOrdering, jobOutcomeFor, jobTuningFor, queueProvision, retryPolicy) import Keiro.Dsl.Validate (derivedQueueTrio)-import Keiro.PGMQ.Codec (mkJobCodec) import Keiro.PGMQ.Job (Job (..), JobOrdering (..), JobOutcome (..), JobTuning (..), RetryPolicy (..), defaultJobTuning, queueProvisionConfigs) import Keiro.PGMQ.Runtime (QueueRef (..), queueRef) import Pgmq.Config qualified as Config@@ -37,7 +37,7 @@             Job                 { jobName = "reservation-work"                 , jobQueue = queueRef "hospital_capacity.reservation_work"-                , jobCodec = mkJobCodec encodeReservationWorkItem parseReservationWorkItem+                , jobCodec = reservationWorkJobCodec                 , jobPolicy = retryPolicy                 }         provisionOk = case queueProvisionConfigs queueProvision job of
+ test/conformance-queue/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.++{- | Versioned job payload envelope: @{\"v\",\"t\",\"data\"}@.++Deploy workers before producers when raising its schema version. Do not+adopt this codec on a non-empty bare-payload queue without draining it+(or supplying a transitional codec), or in-flight messages will+dead-letter. This is telemetry-neutral:+docs/adr/0001-keiro-pgmq-job-processing-telemetry-contract.md owns+spans and acknowledgement vocabulary.+-}+module Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem, encodeReservationWorkItem, parseReservationWorkItem)+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)++reservationWorkPayloadCodec :: Codec ReservationWorkItem+reservationWorkPayloadCodec =+    Codec+        { eventTypes = EventType "ReservationWorkItem" :| []+        , eventType = \_ -> EventType "ReservationWorkItem"+        , schemaVersion = 1+        , encode = encodeReservationWorkItem+        , decode = \_ -> parseReservationWorkItem+        , upcasters = []+        }++reservationWorkJobCodec :: JobCodec ReservationWorkItem+reservationWorkJobCodec = keiroJobCodec reservationWorkPayloadCodec
test/conformance-queue/Main.hs view
@@ -7,16 +7,29 @@ module Main (main) where  import Control.Monad (unless)+import Data.Aeson (object, (.=)) import Generated.HospitalCapacity.Reservation_work.Queue+import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec)+import Keiro.PGMQ.Codec (JobCodec (..)) import System.Exit (exitFailure)  main :: IO () main = do     let sample = ReservationWorkItem "rsv-1" "hsp-1" "cmd-1" True         roundTrips = parseReservationWorkItem (encodeReservationWorkItem sample) == Right sample+        envelope =+            object+                [ "v" .= (1 :: Int)+                , "t" .= ("ReservationWorkItem" :: String)+                , "data" .= encodeReservationWorkItem sample+                ]+        envelopeOk =+            encodeJob reservationWorkJobCodec sample == envelope+                && decodeJob reservationWorkJobCodec envelope == Right sample         physicalOk = queuePhysical == "hospital_capacity_reservation_work"         groupKeyOk = groupKeyFor sample == "rsv-1"     putStrLn ((if roundTrips then "PASS  " else "FAIL  ") <> "Job codec round-trip")+    putStrLn ((if envelopeOk then "PASS  " else "FAIL  ") <> "versioned {v,t,data} job envelope")     putStrLn ((if physicalOk then "PASS  " else "FAIL  ") <> "captured physical name")     putStrLn ((if groupKeyOk then "PASS  " else "FAIL  ") <> "raw FIFO group-key projection")-    unless (roundTrips && physicalOk && groupKeyOk) exitFailure+    unless (roundTrips && envelopeOk && physicalOk && groupKeyOk) exitFailure
+ test/conformance-replay/Generated/ReplayDivergence/Note/Codec.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.ReplayDivergence.Note.Codec (+    noteCodec,+    parseNoteEvent,+    encodeNoteEvent,+) where++import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (Parser, parseEither)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Text (Text)+import Data.Text qualified as T+import Generated.ReplayDivergence.Note.Domain+import Keiro.Codec (Codec (..), EventType (..))++noteCodec :: Codec NoteEvent+noteCodec =+    Codec+        { eventTypes = EventType "NoteWritten" :| []+        , eventType = \case+            NoteWritten{} -> EventType "NoteWritten"+        , schemaVersion = 1+        , encode = encodeNoteEvent+        , decode = parseNoteEvent+        , upcasters = []+        }++encodeNoteEvent :: NoteEvent -> Value+encodeNoteEvent = \case+    NoteWritten payload ->+        object+            [ "kind" .= ("NoteWritten" :: Text)+            , "noteText" .= payload.noteText+            , "echo" .= payload.echo+            ]++parseNoteEvent :: EventType -> Value -> Either Text NoteEvent+parseNoteEvent (EventType tag) = mapLeftText . parseEither (withObject "NoteEvent" go)+  where+    go o = do+        case tag of+            "NoteWritten" ->+                NoteWritten <$> (NoteWrittenData <$> o .: "noteText" <*> o .: "echo")+            _ -> fail "unknown event type"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right
+ test/conformance-replay/Generated/ReplayDivergence/Note/Domain.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.ReplayDivergence.Note.Domain where++import Data.Proxy (Proxy (..))+import Data.Text (Text)+import GHC.Generics (Generic)+import Keiki.Core (RegFile (..))+import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)++data NoteVertex = NoteEmpty | NoteRecorded+    deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)++data WriteNoteData = WriteNoteData+    { noteText :: !Text+    , echo :: !Text+    }+    deriving stock (Generic, Eq, Show)++data NoteCommand = WriteNote !WriteNoteData+    deriving stock (Generic, Eq, Show)++data NoteWrittenData = NoteWrittenData+    { noteText :: !Text+    , echo :: !Text+    }+    deriving stock (Generic, Eq, Show)++data NoteEvent = NoteWritten !NoteWrittenData+    deriving stock (Generic, Eq, Show)++type NoteRegs =+    '[ '("note", Text)+     ]++initialNoteRegs :: RegFile NoteRegs+initialNoteRegs =+    RCons (Proxy @"note") "" RNil++$(deriveAggregateCtorsAll ''NoteCommand ''NoteRegs)++$(deriveWireCtorsAll ''NoteEvent)
+ test/conformance-replay/Generated/ReplayDivergence/Note/EventStream.hs view
@@ -0,0 +1,44 @@+-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.ReplayDivergence.Note.EventStream (+    noteCategory,+    noteEventStream,+    noteEventStreamDef,+    NoteEventStream,+    NoteEventStreamDef,+) where++import Generated.ReplayDivergence.Note.Codec (noteCodec)+import Generated.ReplayDivergence.Note.Domain+import Keiki.Core (HsPred)+import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))+import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)+import Keiro.Stream qualified as Stream+import ReplayDivergence.Note.Holes (noteTransducer)++-- The validated aggregate stream category (hole-kind 5: referenced, never retyped).+-- Entity streams are '<category>-<id>' via Keiro.Stream.entityStream.+-- categoryUnsafe is safe here because this generated literal passed the DSL category proof.+noteCategory :: Stream.StreamCategory a+noteCategory = Stream.categoryUnsafe "note"++type NoteEventStreamDef =+    EventStream (HsPred NoteRegs NoteCommand) NoteRegs NoteVertex NoteCommand NoteEvent++type NoteEventStream =+    ValidatedEventStream (HsPred NoteRegs NoteCommand) NoteRegs NoteVertex NoteCommand NoteEvent++noteEventStreamDef :: NoteEventStreamDef+noteEventStreamDef =+    EventStream+        { transducer = noteTransducer+        , initialState = NoteEmpty+        , initialRegisters = initialNoteRegs+        , eventCodec = noteCodec+        , resolveStreamName = Stream.streamName+        , snapshotPolicy = Never+        , stateCodec = Nothing+        }++noteEventStream :: NoteEventStream+noteEventStream =+    mkEventStreamOrThrow "Note" noteEventStreamDef
+ test/conformance-replay/Generated/ReplayDivergence/Note/Harness.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.ReplayDivergence.Note.Harness (harnessAssertions) where++import Generated.ReplayDivergence.Note.Codec (encodeNoteEvent, noteCodec, parseNoteEvent)+import Generated.ReplayDivergence.Note.Domain+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!))+import Keiro.Codec (eventType)+import ReplayDivergence.Note.Holes (noteTransducer)++{- | (label, passed). A driver runs these and exits non-zero on any False,+naming the failing assertion. Filling a hole wrongly turns a specific+entry False; the scaffold cannot.+-}+harnessAssertions :: [(String, Bool)]+harnessAssertions =+    [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions noteTransducer))+    , ("clock-free: spec samples no wall clock", True)+    , ("golden round-trip: NoteWritten", roundTrips sampleEventNoteWritten)+    , ("accepts WriteNote from NoteEmpty", acceptWriteNote)+    ]+        ++ forwardReplayWriteNote++roundTrips :: NoteEvent -> Bool+roundTrips e = parseNoteEvent (eventType noteCodec e) (encodeNoteEvent e) == Right e++sampleEventNoteWritten :: NoteEvent+sampleEventNoteWritten = (NoteWritten (NoteWrittenData "sample-noteText" "sample-echo"))++acceptWriteNote :: Bool+acceptWriteNote =+    case step noteTransducer (NoteEmpty, initialNoteRegs) ((WriteNote (WriteNoteData "sample-noteText" "sample-echo"))) of+        Just (v, _, _) -> v == NoteRecorded+        Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayWriteNote :: [(String, Bool)]+forwardReplayWriteNote =+    case step noteTransducer (NoteEmpty, initialNoteRegs) ((WriteNote (WriteNoteData "sample-noteText" "sample-echo"))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseNoteEvent (eventType noteCodec event) (encodeNoteEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither noteTransducer (NoteEmpty, initialNoteRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register note", (replayRegs ! #note) == (forwardRegs ! #note))+                            ]+  where+    prefix = "forward/replay equality: WriteNote from NoteEmpty -- "
+ test/conformance-replay/Generated/ReplayDivergence/Note/Projection.hs view
@@ -0,0 +1,2 @@+-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.ReplayDivergence.Note.Projection () where
+ test/conformance-replay/Generated/ReplayDivergence/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.ReplayDivergence.ReplayAudit (auditTargets) where++import Generated.ReplayDivergence.Note.EventStream qualified as Note+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Note.noteEventStream+            , category = Stream.categoryText Note.noteCategory+            , mkStream = streamInCategory (Stream.categoryText Note.noteCategory)+            }+    ]
+ test/conformance-replay/Main.hs view
@@ -0,0 +1,19 @@+{- | Conformance driver for the replay-divergence mutation fixture. It prints+every generated assertion and exits non-zero when any assertion fails, so the+mutation script can distinguish the new forward/replay register check from all+pre-existing checks.+-}+module Main (main) where++import Control.Monad (forM_, unless)+import Generated.ReplayDivergence.Note.Harness (harnessAssertions)+import System.Exit (exitFailure)++main :: IO ()+main = do+    forM_ harnessAssertions $ \(label, ok) ->+        putStrLn ((if ok then "PASS  " else "FAIL  ") <> label)+    let failed = [label | (label, ok) <- harnessAssertions, not ok]+    unless (null failed) $ do+        putStrLn ("harness: " <> show (length failed) <> " assertion(s) failed")+        exitFailure
+ test/conformance-replay/ReplayDivergence/Note/Holes.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE QualifiedDo #-}+{-# LANGUAGE TypeApplications #-}++-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never+-- overwrites it. Fill the transducer body (and any other holes) against the+-- generated signatures, then run the harness to confirm behaviour.+module ReplayDivergence.Note.Holes (+    noteTransducer,+    dishonestWireNoteWritten,+) where++import Data.Text (Text)+import Generated.ReplayDivergence.Note.Domain+import Keiki.Builder ((=:))+import Keiki.Builder qualified as B+import Keiki.Core (HsPred, SymTransducer, WireCtor (..))++-- HOLE: the transducer body. Reproduce the structure below, replacing each+-- `-- HOLE` line with the keiki symbolic operators it describes.+noteTransducer ::+    SymTransducer+        (HsPred NoteRegs NoteCommand)+        NoteRegs+        NoteVertex+        NoteCommand+        NoteEvent+noteTransducer =+    B.buildTransducer NoteEmpty initialNoteRegs isTerminal do+        B.from NoteEmpty do+            B.onCmd inCtorWriteNote $ \d -> B.do+                B.slot @"note" =: d.noteText+                B.emit+                    emitWire+                    NoteWrittenTermFields+                        { noteText = d.noteText+                        , echo = d.echo+                        }+                B.goto NoteRecorded+  where+    isTerminal = \case+        NoteRecorded -> True+        _ -> False++-- The honest generated wire ctor sits behind an indirection that the mutation+-- test changes in one line.+emitWire :: WireCtor NoteEvent (Text, (Text, ()))+emitWire = wireNoteWritten++-- This dormant dishonest ctor copies echo into both event fields. Unlike a+-- simple swap, the rewrite is idempotent: replay's event rebuild check accepts+-- the observed event, then the recovered command writes echo into the note+-- register. Only the generated forward/replay register comparison catches it.+dishonestWireNoteWritten :: WireCtor NoteEvent (Text, (Text, ()))+dishonestWireNoteWritten =+    wireNoteWritten+        { wcBuild = wcBuild wireNoteWritten . duplicateEcho+        }++duplicateEcho :: (Text, (Text, ())) -> (Text, (Text, ()))+duplicateEcho (_noteText, (echo, ())) = (echo, (echo, ()))
+ test/conformance-router-full/Generated/IncidentPaging/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.IncidentPaging.ReplayAudit (auditTargets) where++import Generated.IncidentPaging.Page.EventStream qualified as Page+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Page.pageEventStream+            , category = Stream.categoryText Page.pageCategory+            , mkStream = streamInCategory (Stream.categoryText Page.pageCategory)+            }+    ]
+ test/conformance-skeletons/SkelAggregate/Generated/MyService/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module SkelAggregate.Generated.MyService.ReplayAudit (auditTargets) where++import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream+import SkelAggregate.Generated.MyService.Thing.EventStream qualified as Thing++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Thing.thingEventStream+            , category = Stream.categoryText Thing.thingCategory+            , mkStream = streamInCategory (Stream.categoryText Thing.thingCategory)+            }+    ]
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Harness.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec. module SkelAggregate.Generated.MyService.Thing.Harness (harnessAssertions) where -import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!)) import Keiro.Codec (eventType) import SkelAggregate.Generated.MyService.Thing.Codec (encodeThingEvent, parseThingEvent, thingCodec) import SkelAggregate.Generated.MyService.Thing.Domain@@ -20,6 +22,7 @@     , ("golden round-trip: ThingCompleted", roundTrips sampleEventThingCompleted)     , ("accepts DoThing from ThingPending", acceptDoThing)     ]+        ++ forwardReplayDoThing  roundTrips :: ThingEvent -> Bool roundTrips e = parseThingEvent (eventType thingCodec e) (encodeThingEvent e) == Right e@@ -32,3 +35,23 @@     case step thingTransducer (ThingPending, initialThingRegs) ((DoThing (DoThingData (ThingId "sample") 0))) of         Just (v, _, _) -> v == ThingDone         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayDoThing :: [(String, Bool)]+forwardReplayDoThing =+    case step thingTransducer (ThingPending, initialThingRegs) ((DoThing (DoThingData (ThingId "sample") 0))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseThingEvent (eventType thingCodec event) (encodeThingEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither thingTransducer (ThingPending, initialThingRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register thingId", (replayRegs ! #thingId) == (forwardRegs ! #thingId))+                            , (prefix <> "register state", (replayRegs ! #state) == (forwardRegs ! #state))+                            ]+  where+    prefix = "forward/replay equality: DoThing from ThingPending -- "
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Harness.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec. module SkelProcess.Generated.MyService.Hospital.Harness (harnessAssertions) where -import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer) import Keiro.Codec (eventType) import SkelProcess.Generated.MyService.Hospital.Codec (encodeHospitalEvent, hospitalCodec, parseHospitalEvent) import SkelProcess.Generated.MyService.Hospital.Domain@@ -20,6 +22,7 @@     , ("golden round-trip: SurgeActivated", roundTrips sampleEventSurgeActivated)     , ("accepts ActivateSurge from HospitalOperational", acceptActivateSurge)     ]+        ++ forwardReplayActivateSurge  roundTrips :: HospitalEvent -> Bool roundTrips e = parseHospitalEvent (eventType hospitalCodec e) (encodeHospitalEvent e) == Right e@@ -32,3 +35,21 @@     case step hospitalTransducer (HospitalOperational, initialHospitalRegs) ((ActivateSurge (ActivateSurgeData (HospitalId "sample")))) of         Just (v, _, _) -> v == HospitalSurging         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayActivateSurge :: [(String, Bool)]+forwardReplayActivateSurge =+    case step hospitalTransducer (HospitalOperational, initialHospitalRegs) ((ActivateSurge (ActivateSurgeData (HospitalId "sample")))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, _forwardRegs, emitted) ->+            case mapM (\event -> parseHospitalEvent (eventType hospitalCodec event) (encodeHospitalEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither hospitalTransducer (HospitalOperational, initialHospitalRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, _replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            ]+  where+    prefix = "forward/replay equality: ActivateSurge from HospitalOperational -- "
+ test/conformance-skeletons/SkelProcess/Generated/MyService/ReplayAudit.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module SkelProcess.Generated.MyService.ReplayAudit (auditTargets) where++import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream+import SkelProcess.Generated.MyService.Hospital.EventStream qualified as Hospital+import SkelProcess.Generated.MyService.Surge.EventStream qualified as Surge++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Surge.surgeEventStream+            , category = Stream.categoryText Surge.surgeCategory+            , mkStream = streamInCategory (Stream.categoryText Surge.surgeCategory)+            }+    , SomeAuditTarget+        AuditTarget+            { eventStream = Hospital.hospitalEventStream+            , category = Stream.categoryText Hospital.hospitalCategory+            , mkStream = streamInCategory (Stream.categoryText Hospital.hospitalCategory)+            }+    ]
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Harness.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec. module SkelProcess.Generated.MyService.Surge.Harness (harnessAssertions) where -import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer) import Keiro.Codec (eventType) import SkelProcess.Generated.MyService.Surge.Codec (encodeSurgeEvent, parseSurgeEvent, surgeCodec) import SkelProcess.Generated.MyService.Surge.Domain@@ -22,24 +24,62 @@     , ("accepts NoteSurgeThreshold from SurgeIdle", acceptNoteSurgeThreshold)     , ("accepts MarkSurgeTimerFired from SurgeIdle", acceptMarkSurgeTimerFired)     ]+        ++ forwardReplayNoteSurgeThreshold+        ++ forwardReplayMarkSurgeTimerFired  roundTrips :: SurgeEvent -> Bool roundTrips e = parseSurgeEvent (eventType surgeCodec e) (encodeSurgeEvent e) == Right e  sampleEventSurgeThresholdNoted :: SurgeEvent-sampleEventSurgeThresholdNoted = (SurgeThresholdNoted (SurgeThresholdNotedData (HospitalId "sample") 0 0 "sample"))+sampleEventSurgeThresholdNoted = (SurgeThresholdNoted (SurgeThresholdNotedData (HospitalId "sample") 0 0 "sample-timerId"))  sampleEventSurgeTimerMarked :: SurgeEvent-sampleEventSurgeTimerMarked = (SurgeTimerMarked (SurgeTimerMarkedData (HospitalId "sample") "sample"))+sampleEventSurgeTimerMarked = (SurgeTimerMarked (SurgeTimerMarkedData (HospitalId "sample") "sample-timerId"))  acceptNoteSurgeThreshold :: Bool acceptNoteSurgeThreshold =-    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((NoteSurgeThreshold (NoteSurgeThresholdData (HospitalId "sample") 0 0 "sample"))) of+    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((NoteSurgeThreshold (NoteSurgeThresholdData (HospitalId "sample") 0 0 "sample-timerId"))) of         Just (v, _, _) -> v == SurgeIdle         Nothing -> False  acceptMarkSurgeTimerFired :: Bool acceptMarkSurgeTimerFired =-    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((MarkSurgeTimerFired (MarkSurgeTimerFiredData (HospitalId "sample") "sample"))) of+    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((MarkSurgeTimerFired (MarkSurgeTimerFiredData (HospitalId "sample") "sample-timerId"))) of         Just (v, _, _) -> v == SurgeFired         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayNoteSurgeThreshold :: [(String, Bool)]+forwardReplayNoteSurgeThreshold =+    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((NoteSurgeThreshold (NoteSurgeThresholdData (HospitalId "sample") 0 0 "sample-timerId"))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, _forwardRegs, emitted) ->+            case mapM (\event -> parseSurgeEvent (eventType surgeCodec event) (encodeSurgeEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither surgeTransducer (SurgeIdle, initialSurgeRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, _replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            ]+  where+    prefix = "forward/replay equality: NoteSurgeThreshold from SurgeIdle -- "++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayMarkSurgeTimerFired :: [(String, Bool)]+forwardReplayMarkSurgeTimerFired =+    case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((MarkSurgeTimerFired (MarkSurgeTimerFiredData (HospitalId "sample") "sample-timerId"))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, _forwardRegs, emitted) ->+            case mapM (\event -> parseSurgeEvent (eventType surgeCodec event) (encodeSurgeEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither surgeTransducer (SurgeIdle, initialSurgeRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, _replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            ]+  where+    prefix = "forward/replay equality: MarkSurgeTimerFired from SurgeIdle -- "
+ test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/QueueCodec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.++{- | Versioned job payload envelope: @{\"v\",\"t\",\"data\"}@.++Deploy workers before producers when raising its schema version. Do not+adopt this codec on a non-empty bare-payload queue without draining it+(or supplying a transitional codec), or in-flight messages will+dead-letter. This is telemetry-neutral:+docs/adr/0001-keiro-pgmq-job-processing-telemetry-contract.md owns+spans and acknowledgement vocabulary.+-}+module SkelQueue.Generated.MyService.Reservation_work.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)+import SkelQueue.Generated.MyService.Reservation_work.Queue (ReservationWorkItem, encodeReservationWorkItem, parseReservationWorkItem)++reservationWorkPayloadCodec :: Codec ReservationWorkItem+reservationWorkPayloadCodec =+    Codec+        { eventTypes = EventType "ReservationWorkItem" :| []+        , eventType = \_ -> EventType "ReservationWorkItem"+        , schemaVersion = 1+        , encode = encodeReservationWorkItem+        , decode = \_ -> parseReservationWorkItem+        , upcasters = []+        }++reservationWorkJobCodec :: JobCodec ReservationWorkItem+reservationWorkJobCodec = keiroJobCodec reservationWorkPayloadCodec
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Harness.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec. module SkelRouter.Generated.MyService.Page.Harness (harnessAssertions) where -import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer) import Keiro.Codec (eventType) import SkelRouter.Generated.MyService.Page.Codec (encodePageEvent, pageCodec, parsePageEvent) import SkelRouter.Generated.MyService.Page.Domain@@ -20,15 +22,34 @@     , ("golden round-trip: PageSent", roundTrips sampleEventPageSent)     , ("accepts SendPage from PagePending", acceptSendPage)     ]+        ++ forwardReplaySendPage  roundTrips :: PageEvent -> Bool roundTrips e = parsePageEvent (eventType pageCodec e) (encodePageEvent e) == Right e  sampleEventPageSent :: PageEvent-sampleEventPageSent = (PageSent (PageSentData "sample" "sample"))+sampleEventPageSent = (PageSent (PageSentData "sample-incidentId" "sample-responderId"))  acceptSendPage :: Bool acceptSendPage =-    case step pageTransducer (PagePending, initialPageRegs) ((SendPage (SendPageData "sample" "sample"))) of+    case step pageTransducer (PagePending, initialPageRegs) ((SendPage (SendPageData "sample-incidentId" "sample-responderId"))) of         Just (v, _, _) -> v == PageDelivered         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplaySendPage :: [(String, Bool)]+forwardReplaySendPage =+    case step pageTransducer (PagePending, initialPageRegs) ((SendPage (SendPageData "sample-incidentId" "sample-responderId"))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, _forwardRegs, emitted) ->+            case mapM (\event -> parsePageEvent (eventType pageCodec event) (encodePageEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither pageTransducer (PagePending, initialPageRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, _replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            ]+  where+    prefix = "forward/replay equality: SendPage from PagePending -- "
+ test/conformance-skeletons/SkelRouter/Generated/MyService/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module SkelRouter.Generated.MyService.ReplayAudit (auditTargets) where++import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream+import SkelRouter.Generated.MyService.Page.EventStream qualified as Page++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Page.pageEventStream+            , category = Stream.categoryText Page.pageCategory+            , mkStream = streamInCategory (Stream.categoryText Page.pageCategory)+            }+    ]
+ test/conformance-snapshot/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.HospitalCapacity.ReplayAudit (auditTargets) where++import Generated.HospitalCapacity.Reservation.EventStream qualified as Reservation+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Reservation.reservationEventStream+            , category = Stream.categoryText Reservation.reservationCategory+            , mkStream = streamInCategory (Stream.categoryText Reservation.reservationCategory)+            }+    ]
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Domain.hs view
@@ -15,7 +15,7 @@ import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)-import Keiki.Shape (CanonicalTypeName)+import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)  newtype TransferReservationId = TransferReservationId Text     deriving stock (Generic, Eq, Ord, Show)@@ -76,6 +76,7 @@ data ReservationVertex = ReservationUnrequested | ReservationHeld | ReservationConfirmed | ReservationExpired | ReservationAdmitted | ReservationReleased     deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)     deriving anyclass (ToJSON, FromJSON)+instance CanonicalStateShape ReservationVertex instance CanonicalTypeName ReservationVertex  data RequestTransferReservationData = RequestTransferReservationData
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/EventStream.hs view
@@ -15,7 +15,7 @@ import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)-import Keiro.Snapshot.Codec (defaultStateCodec)+import Keiro.Snapshot.Codec (defaultStateCodec, withFoldFingerprint) import Keiro.Stream qualified as Stream  -- The validated aggregate stream category (hole-kind 5: referenced, never retyped).@@ -39,7 +39,15 @@         , eventCodec = reservationCodec         , resolveStreamName = Stream.streamName         , snapshotPolicy = Every 100-        , stateCodec = Just (defaultStateCodec 1)+        , -- The snapshot discriminator composes: the spec's state-codec version (bump it+          -- in the spec's `state-codec version=` clause), keiki's register and+          -- control-state shape hashes, and this fold fingerprint derived from the+          -- spec's transition surface (guards, writes, emits, states, register+          -- initials, referenced rules). Spec-visible fold changes invalidate old+          -- snapshots automatically. Fold changes made ONLY in the hand-owned Holes+          -- module are invisible here: bump `state-codec version=` manually or old+          -- snapshots will be served stale.+          stateCodec = Just (withFoldFingerprint "2367ef6fadf0e751" (defaultStateCodec 1))         }  reservationSnapshotFixture :: (Int, Text)
test/conformance-snapshot/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-}  module Main (main) where@@ -6,9 +7,9 @@ import Control.Exception (evaluate) import Control.Monad (unless) import Data.Proxy (Proxy (..))-import Generated.HospitalCapacity.Reservation.Domain (ReservationRegs)+import Generated.HospitalCapacity.Reservation.Domain (ReservationRegs, ReservationVertex) import Generated.HospitalCapacity.Reservation.EventStream (reservationEventStream, reservationEventStreamDef, reservationSnapshotFixture)-import Keiki.Shape (regFileShapeHash)+import Keiki.Shape qualified as Shape import Keiro.EventStream (EventStream (..), SnapshotPolicy (..), StateCodec (..)) import System.Exit (exitFailure) @@ -23,7 +24,10 @@             let (fixtureVersion, fixtureHash) = reservationSnapshotFixture                 versionOk = stateCodecVersion liveCodec == fixtureVersion                 hashOk = shapeHash liveCodec == fixtureHash-                hashDerived = shapeHash liveCodec == regFileShapeHash (Proxy @ReservationRegs)+                hashDerived = shapeHash liveCodec == Shape.regFileShapeHash (Proxy @ReservationRegs)+                stateShapeDerived =+                    stateShapeHash liveCodec+                        == Shape.stateShapeHash (Proxy @ReservationVertex) <> ";fold=2367ef6fadf0e751"                 policyOk = case snapshotPolicy reservationEventStreamDef of                     Every interval -> interval == 100                     _ -> False@@ -31,11 +35,12 @@                 roundTripOk = case decode liveCodec encoded of                     Left _ -> False                     Right decoded -> encode liveCodec decoded == encoded-                checks = [versionOk, hashOk, hashDerived, policyOk, roundTripOk]+                checks = [versionOk, hashOk, hashDerived, stateShapeDerived, policyOk, roundTripOk]             putStrLn ("live snapshot shape hash: " <> show (shapeHash liveCodec))             putStrLn ("codec version matches captured fixture: " <> show versionOk)             putStrLn ("shape hash matches captured fixture: " <> show hashOk)             putStrLn ("shape hash matches live regFileShapeHash: " <> show hashDerived)+            putStrLn ("state shape and fold fingerprint match live derivation: " <> show stateShapeDerived)             putStrLn ("snapshot policy is Every 100: " <> show policyOk)             putStrLn ("initial snapshot JSON round-trips: " <> show roundTripOk)             unless (and checks) exitFailure
+ test/conformance-structural/Conformance/Structural/Bindings.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++module Conformance.Structural.Bindings (+    artifactInfoBinding,+    artifactInfoCases,+    emptyArtifactInfo,+    artifactMetadataBinding,+    artifactMetadataCases,+    artifactKindBinding,+    artifactKindCases,+    artifactLocationBinding,+    artifactLocationCases,+    geometryCases,+    emptyGeometry,+) where++import Conformance.Structural.Domain qualified as Domain+import Data.List.NonEmpty (NonEmpty (..))+import Data.Text (Text)+import Generated.StructuralConformance.Structural.Shape.ArtifactInfo qualified as InfoShape+import Generated.StructuralConformance.Structural.Shape.ArtifactKind qualified as KindShape+import Generated.StructuralConformance.Structural.Shape.ArtifactLocation qualified as LocationShape+import Generated.StructuralConformance.Structural.Shape.ArtifactMetadata qualified as MetadataShape+import Keiro.Codec.Structural (FixtureCases (..), StructuralBinding (..))+import Keiro.Codec.Structural.Generic (genericStructuralBinding)++artifactKindBinding :: StructuralBinding Domain.ArtifactKind KindShape.ArtifactKindShape+artifactKindBinding = genericStructuralBinding++artifactLocationBinding :: StructuralBinding Domain.ArtifactLocation LocationShape.ArtifactLocationShape+artifactLocationBinding = genericStructuralBinding++artifactMetadataBinding :: StructuralBinding Domain.ArtifactMetadata MetadataShape.ArtifactMetadataShape+artifactMetadataBinding = genericStructuralBinding++artifactInfoBinding :: StructuralBinding Domain.ArtifactInfo InfoShape.ArtifactInfoShape+artifactInfoBinding =+    StructuralBinding+        { bindingToShape = \value ->+            InfoShape.ArtifactInfo+                value.artifactKey+                value.displayName+                value.artifactHash+                (bindingToShape artifactKindBinding value.artifactKind)+                (bindingToShape artifactLocationBinding value.location)+                (bindingToShape artifactMetadataBinding value.metadata)+                value.active+                value.tags+        , bindingFromShape = \(InfoShape.ArtifactInfo artifactKey displayName artifactHash artifactKind location metadata active tags) ->+            Domain.ArtifactInfo+                artifactKey+                displayName+                artifactHash+                (bindingFromShape artifactKindBinding artifactKind)+                (bindingFromShape artifactLocationBinding location)+                (bindingFromShape artifactMetadataBinding metadata)+                active+                tags+        }++artifactKindCases :: FixtureCases Domain.ArtifactKind+artifactKindCases = FixtureCases (("guide", Domain.Guide) :| [("reference", Domain.Reference)])++artifactLocationCases :: FixtureCases Domain.ArtifactLocation+artifactLocationCases =+    FixtureCases+        ( ("local-file", Domain.LocalFile "/tmp/artifact.txt")+            :| [ ("local-dir", Domain.LocalDir "/tmp/artifacts")+               , ("repo-path", Domain.RepoPath "docs/artifact.md")+               , ("url", Domain.LocUrl "https://example.test/artifact")+               , ("canonical", Domain.Canonical)+               ]+        )++artifactMetadataCases :: FixtureCases Domain.ArtifactMetadata+artifactMetadataCases =+    FixtureCases+        ( ("without-note", Domain.ArtifactMetadata Nothing)+            :| [("with-note", Domain.ArtifactMetadata (Just "consumer note"))]+        )++artifactInfoCases :: FixtureCases Domain.ArtifactInfo+artifactInfoCases =+    FixtureCases+        ( ( "local-file-no-hash"+          , artifact "artifact-local-file" "Local file" Nothing Domain.Guide (Domain.LocalFile "/tmp/artifact.txt") Nothing+          )+            :| [+                   ( "local-dir-with-hash"+                   , artifact "artifact-local-dir" "Local directory" (Just "sha256:01") Domain.Reference (Domain.LocalDir "/tmp/artifacts") (Just "directory")+                   )+               ,+                   ( "repo-path"+                   , artifact "artifact-repo" "Repository path" Nothing Domain.Guide (Domain.RepoPath "docs/artifact.md") (Just "repository")+                   )+               ,+                   ( "url"+                   , artifact "artifact-url" "URL" (Just "sha256:02") Domain.Reference (Domain.LocUrl "https://example.test/artifact") Nothing+                   )+               ,+                   ( "canonical"+                   , artifact "artifact-canonical" "Canonical" Nothing Domain.Guide Domain.Canonical (Just "canonical")+                   )+               ]+        )++artifact :: Text -> Text -> Maybe Text -> Domain.ArtifactKind -> Domain.ArtifactLocation -> Maybe Text -> Domain.ArtifactInfo+artifact artifactKey displayName artifactHash artifactKind location note =+    Domain.ArtifactInfo+        { Domain.artifactKey = artifactKey+        , Domain.displayName = displayName+        , Domain.artifactHash = artifactHash+        , Domain.artifactKind = artifactKind+        , Domain.location = location+        , Domain.metadata = Domain.ArtifactMetadata note+        , Domain.active = True+        , Domain.tags = ["conformance", artifactKey]+        }++emptyArtifactInfo :: Domain.ArtifactInfo+emptyArtifactInfo = artifact "artifact-empty" "Empty" Nothing Domain.Guide Domain.Canonical Nothing++geometryCases :: FixtureCases Domain.Geometry+geometryCases =+    FixtureCases+        ( ("point", Domain.Geometry "POINT (1 2)")+            :| [("polygon", Domain.Geometry "POLYGON ((0 0, 1 0, 1 1, 0 0))")]+        )++emptyGeometry :: Domain.Geometry+emptyGeometry = Domain.Geometry "GEOMETRYCOLLECTION EMPTY"
+ test/conformance-structural/Conformance/Structural/Domain.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}++module Conformance.Structural.Domain (+    ArtifactInfo (..),+    ArtifactMetadata (..),+    ArtifactKind (..),+    ArtifactLocation (..),+    Geometry (..),+) where++import Control.DeepSeq (NFData)+import Data.Aeson (FromJSON, ToJSON)+import Data.Proxy (Proxy)+import Data.Text (Text)+import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName (..))++data ArtifactInfo = ArtifactInfo+    { artifactKey :: !Text+    , displayName :: !Text+    , artifactHash :: !(Maybe Text)+    , artifactKind :: !ArtifactKind+    , location :: !ArtifactLocation+    , metadata :: !ArtifactMetadata+    , active :: !Bool+    , tags :: ![Text]+    }+    deriving stock (Eq, Show, Generic)+    deriving anyclass (FromJSON, NFData, ToJSON)++data ArtifactMetadata = ArtifactMetadata+    { note :: !(Maybe Text)+    }+    deriving stock (Eq, Show, Generic)+    deriving anyclass (FromJSON, NFData, ToJSON)++data ArtifactKind = Guide | Reference+    deriving stock (Eq, Show, Generic)+    deriving anyclass (FromJSON, NFData, ToJSON)++data ArtifactLocation+    = LocalFile !Text+    | LocalDir !Text+    | RepoPath !Text+    | LocUrl !Text+    | Canonical+    deriving stock (Eq, Show, Generic)+    deriving anyclass (FromJSON, NFData, ToJSON)++newtype Geometry = Geometry {geometryWkt :: Text}+    deriving stock (Eq, Show, Generic)+    deriving anyclass (FromJSON, NFData, ToJSON)++instance CanonicalTypeName ArtifactInfo where+    canonicalTypeName :: Proxy ArtifactInfo -> Text+    canonicalTypeName _ = "conformance.structural.ArtifactInfo.v1"++instance CanonicalTypeName ArtifactMetadata where+    canonicalTypeName :: Proxy ArtifactMetadata -> Text+    canonicalTypeName _ = "conformance.structural.ArtifactMetadata.v1"++instance CanonicalTypeName ArtifactKind where+    canonicalTypeName :: Proxy ArtifactKind -> Text+    canonicalTypeName _ = "conformance.structural.ArtifactKind.v1"++instance CanonicalTypeName ArtifactLocation where+    canonicalTypeName :: Proxy ArtifactLocation -> Text+    canonicalTypeName _ = "conformance.structural.ArtifactLocation.v1"
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Codec.hs view
@@ -0,0 +1,218 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.ArtifactCatalog.Codec (+    artifactCatalogCodec,+    parseArtifactCatalogEvent,+    encodeArtifactCatalogEvent,+    encodeArtifactInfoMapped,+    decodeArtifactInfoMapped,+    encodeArtifactKindMapped,+    decodeArtifactKindMapped,+    encodeArtifactLocationMapped,+    decodeArtifactLocationMapped,+    encodeArtifactMetadataMapped,+    decodeArtifactMetadataMapped,+) where++import Control.Monad (unless)+import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, withText, (.:), (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.Types (Parser, parseEither)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Generated.StructuralConformance.ArtifactCatalog.Domain+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.Codec.Structural (bindingFromShape, bindingToShape)++import Conformance.Structural.Bindings qualified+import Conformance.Structural.Domain qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactInfo qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactKind qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactLocation qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactMetadata qualified++encodeArtifactInfoMapped :: Conformance.Structural.Domain.ArtifactInfo -> Value+encodeArtifactInfoMapped = encodeArtifactInfoShape . bindingToShape Conformance.Structural.Bindings.artifactInfoBinding++parseArtifactInfoMapped :: Value -> Parser Conformance.Structural.Domain.ArtifactInfo+parseArtifactInfoMapped value = bindingFromShape Conformance.Structural.Bindings.artifactInfoBinding <$> parseArtifactInfoShape value++decodeArtifactInfoMapped :: Value -> Either Text Conformance.Structural.Domain.ArtifactInfo+decodeArtifactInfoMapped = mapLeftText . parseEither parseArtifactInfoMapped++encodeArtifactInfoShape :: Generated.StructuralConformance.Structural.Shape.ArtifactInfo.ArtifactInfoShape -> Value+encodeArtifactInfoShape shape =+    object+        [ "artifact_key" .= toJSON (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactKey shape)+        , "display_name" .= toJSON (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.displayName shape)+        , "artifact_hash" .= maybe Null (\item -> toJSON (item)) (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactHash shape)+        , "artifact_kind" .= encodeArtifactKindShape (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactKind shape)+        , "location" .= encodeArtifactLocationShape (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.location shape)+        , "metadata" .= encodeArtifactMetadataShape (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.metadata shape)+        , "active" .= toJSON (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.active shape)+        , "tags" .= toJSON (map (\item -> toJSON (item)) (Generated.StructuralConformance.Structural.Shape.ArtifactInfo.tags shape))+        ]++parseArtifactInfoShape :: Value -> Parser Generated.StructuralConformance.Structural.Shape.ArtifactInfo.ArtifactInfoShape+parseArtifactInfoShape = withObject "ArtifactInfoShape" $ \objectValue -> do+    rejectUnknownFields "ArtifactInfo" ["artifact_key", "display_name", "artifact_hash", "artifact_kind", "location", "metadata", "active", "tags"] objectValue+    Generated.StructuralConformance.Structural.Shape.ArtifactInfo.ArtifactInfo+        <$> ((objectValue .: "artifact_key" :: Parser Value) >>= (parseJSON))+        <*> ((objectValue .: "display_name" :: Parser Value) >>= (parseJSON))+        <*> (case KeyMap.lookup (Key.fromText "artifact_hash") objectValue of Nothing -> pure Nothing; Just presentValue -> (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) presentValue)+        <*> (case KeyMap.lookup (Key.fromText "artifact_kind") objectValue of Nothing -> pure Generated.StructuralConformance.Structural.Shape.ArtifactKind.Guide; Just presentValue -> (parseArtifactKindShape) presentValue)+        <*> ((objectValue .: "location" :: Parser Value) >>= (parseArtifactLocationShape))+        <*> ((objectValue .: "metadata" :: Parser Value) >>= (parseArtifactMetadataShape))+        <*> (case KeyMap.lookup (Key.fromText "active") objectValue of Nothing -> pure False; Just presentValue -> (parseJSON) presentValue)+        <*> (case KeyMap.lookup (Key.fromText "tags") objectValue of Nothing -> pure []; Just presentValue -> (\value -> (parseJSON value :: Parser [Value]) >>= traverse (parseJSON)) presentValue)++encodeArtifactKindMapped :: Conformance.Structural.Domain.ArtifactKind -> Value+encodeArtifactKindMapped = encodeArtifactKindShape . bindingToShape Conformance.Structural.Bindings.artifactKindBinding++parseArtifactKindMapped :: Value -> Parser Conformance.Structural.Domain.ArtifactKind+parseArtifactKindMapped value = bindingFromShape Conformance.Structural.Bindings.artifactKindBinding <$> parseArtifactKindShape value++decodeArtifactKindMapped :: Value -> Either Text Conformance.Structural.Domain.ArtifactKind+decodeArtifactKindMapped = mapLeftText . parseEither parseArtifactKindMapped++encodeArtifactKindShape :: Generated.StructuralConformance.Structural.Shape.ArtifactKind.ArtifactKindShape -> Value+encodeArtifactKindShape = \case+    Generated.StructuralConformance.Structural.Shape.ArtifactKind.Guide -> String "guide"+    Generated.StructuralConformance.Structural.Shape.ArtifactKind.Reference -> String "reference"++parseArtifactKindShape :: Value -> Parser Generated.StructuralConformance.Structural.Shape.ArtifactKind.ArtifactKindShape+parseArtifactKindShape = withText "ArtifactKindShape" $ \tag -> case tag of+    "guide" -> pure Generated.StructuralConformance.Structural.Shape.ArtifactKind.Guide+    "reference" -> pure Generated.StructuralConformance.Structural.Shape.ArtifactKind.Reference+    _ -> fail "unknown ArtifactKind wire value"++encodeArtifactLocationMapped :: Conformance.Structural.Domain.ArtifactLocation -> Value+encodeArtifactLocationMapped = encodeArtifactLocationShape . bindingToShape Conformance.Structural.Bindings.artifactLocationBinding++parseArtifactLocationMapped :: Value -> Parser Conformance.Structural.Domain.ArtifactLocation+parseArtifactLocationMapped value = bindingFromShape Conformance.Structural.Bindings.artifactLocationBinding <$> parseArtifactLocationShape value++decodeArtifactLocationMapped :: Value -> Either Text Conformance.Structural.Domain.ArtifactLocation+decodeArtifactLocationMapped = mapLeftText . parseEither parseArtifactLocationMapped++encodeArtifactLocationShape :: Generated.StructuralConformance.Structural.Shape.ArtifactLocation.ArtifactLocationShape -> Value+encodeArtifactLocationShape = \case+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalFile payload ->+        object+            [ "tag" .= ("local_file" :: Text)+            , "contents" .= toJSON (payload)+            ]+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalDir payload ->+        object+            [ "tag" .= ("local_dir" :: Text)+            , "contents" .= toJSON (payload)+            ]+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation.RepoPath payload ->+        object+            [ "tag" .= ("repo_path" :: Text)+            , "contents" .= toJSON (payload)+            ]+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocUrl payload ->+        object+            [ "tag" .= ("url" :: Text)+            , "contents" .= toJSON (payload)+            ]+    Generated.StructuralConformance.Structural.Shape.ArtifactLocation.Canonical ->+        object+            [ "tag" .= ("canonical" :: Text)+            ]++parseArtifactLocationShape :: Value -> Parser Generated.StructuralConformance.Structural.Shape.ArtifactLocation.ArtifactLocationShape+parseArtifactLocationShape = withObject "ArtifactLocationShape" $ \objectValue -> do+    tag <- objectValue .: "tag" :: Parser Text+    case tag of+        "local_file" -> do+            rejectUnknownFields "ArtifactLocation" ["tag", "contents"] objectValue+            Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalFile <$> (objectValue .: "contents" >>= (parseJSON))+        "local_dir" -> do+            rejectUnknownFields "ArtifactLocation" ["tag", "contents"] objectValue+            Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalDir <$> (objectValue .: "contents" >>= (parseJSON))+        "repo_path" -> do+            rejectUnknownFields "ArtifactLocation" ["tag", "contents"] objectValue+            Generated.StructuralConformance.Structural.Shape.ArtifactLocation.RepoPath <$> (objectValue .: "contents" >>= (parseJSON))+        "url" -> do+            rejectUnknownFields "ArtifactLocation" ["tag", "contents"] objectValue+            Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocUrl <$> (objectValue .: "contents" >>= (parseJSON))+        "canonical" -> do+            rejectUnknownFields "ArtifactLocation" ["tag"] objectValue+            pure Generated.StructuralConformance.Structural.Shape.ArtifactLocation.Canonical+        _ -> fail "unknown ArtifactLocation union tag"++encodeArtifactMetadataMapped :: Conformance.Structural.Domain.ArtifactMetadata -> Value+encodeArtifactMetadataMapped = encodeArtifactMetadataShape . bindingToShape Conformance.Structural.Bindings.artifactMetadataBinding++parseArtifactMetadataMapped :: Value -> Parser Conformance.Structural.Domain.ArtifactMetadata+parseArtifactMetadataMapped value = bindingFromShape Conformance.Structural.Bindings.artifactMetadataBinding <$> parseArtifactMetadataShape value++decodeArtifactMetadataMapped :: Value -> Either Text Conformance.Structural.Domain.ArtifactMetadata+decodeArtifactMetadataMapped = mapLeftText . parseEither parseArtifactMetadataMapped++encodeArtifactMetadataShape :: Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.ArtifactMetadataShape -> Value+encodeArtifactMetadataShape shape =+    object+        [ "note" .= maybe Null (\item -> toJSON (item)) (Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.note shape)+        ]++parseArtifactMetadataShape :: Value -> Parser Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.ArtifactMetadataShape+parseArtifactMetadataShape = withObject "ArtifactMetadataShape" $ \objectValue -> do+    Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.ArtifactMetadata+        <$> ((objectValue .: "note" :: Parser Value) >>= (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other))++artifactCatalogCodec :: Codec ArtifactCatalogEvent+artifactCatalogCodec =+    Codec+        { eventTypes = EventType "ArtifactRecorded" :| [EventType "ArtifactAccepted"]+        , eventType = \case+            ArtifactRecorded{} -> EventType "ArtifactRecorded"+            ArtifactAccepted{} -> EventType "ArtifactAccepted"+        , schemaVersion = 1+        , encode = encodeArtifactCatalogEvent+        , decode = parseArtifactCatalogEvent+        , upcasters = []+        }++encodeArtifactCatalogEvent :: ArtifactCatalogEvent -> Value+encodeArtifactCatalogEvent = \case+    ArtifactRecorded payload ->+        object+            [ "kind" .= ("ArtifactRecorded" :: Text)+            , "artifact" .= encodeArtifactInfoMapped payload.artifact+            , "geometry" .= toJSON payload.geometry+            , "accepted" .= payload.accepted+            ]+    ArtifactAccepted payload ->+        object+            [ "kind" .= ("ArtifactAccepted" :: Text)+            , "accepted" .= payload.accepted+            ]++parseArtifactCatalogEvent :: EventType -> Value -> Either Text ArtifactCatalogEvent+parseArtifactCatalogEvent (EventType tag) = mapLeftText . parseEither (withObject "ArtifactCatalogEvent" go)+  where+    go o = do+        case tag of+            "ArtifactRecorded" ->+                ArtifactRecorded <$> (ArtifactRecordedData <$> (o .: "artifact" >>= parseArtifactInfoMapped) <*> o .: "geometry" <*> o .: "accepted")+            "ArtifactAccepted" ->+                ArtifactAccepted <$> (ArtifactAcceptedData <$> o .: "accepted")+            _ -> fail "unknown event type"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right++rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser ()+rejectUnknownFields label allowed objectValue =+    unless (null extras) (fail (label <> " contains unknown fields: " <> show extras))+  where+    extras = filter (`notElem` allowed) (map Key.toText (KeyMap.keys objectValue))
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Domain.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.ArtifactCatalog.Domain where++import Conformance.Structural.Bindings qualified+import Conformance.Structural.Domain qualified+import Data.Proxy (Proxy (..))+import Data.Text (Text)+import GHC.Generics (Generic)+import Keiki.Core (RegFile (..))+import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)++data ArtifactCatalogVertex = ArtifactCatalogEmpty | ArtifactCatalogObserved+    deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)++data ObserveArtifactData = ObserveArtifactData+    { artifact :: !Conformance.Structural.Domain.ArtifactInfo+    , geometry :: !Conformance.Structural.Domain.Geometry+    , accepted :: !Bool+    }+    deriving stock (Generic, Eq, Show)++data ArtifactCatalogCommand = ObserveArtifact !ObserveArtifactData+    deriving stock (Generic, Eq, Show)++data ArtifactRecordedData = ArtifactRecordedData+    { artifact :: !Conformance.Structural.Domain.ArtifactInfo+    , geometry :: !Conformance.Structural.Domain.Geometry+    , accepted :: !Bool+    }+    deriving stock (Generic, Eq, Show)++data ArtifactAcceptedData = ArtifactAcceptedData+    { accepted :: !Bool+    }+    deriving stock (Generic, Eq, Show)++data ArtifactCatalogEvent+    = ArtifactRecorded !ArtifactRecordedData+    | ArtifactAccepted !ArtifactAcceptedData+    deriving stock (Generic, Eq, Show)++type ArtifactCatalogRegs =+    '[ '("currentArtifact", Conformance.Structural.Domain.ArtifactInfo)+     , '("currentGeometry", Conformance.Structural.Domain.Geometry)+     , '("acceptedCount", Int)+     ]++initialArtifactCatalogRegs :: RegFile ArtifactCatalogRegs+initialArtifactCatalogRegs =+    RCons (Proxy @"currentArtifact") Conformance.Structural.Bindings.emptyArtifactInfo $+        RCons (Proxy @"currentGeometry") Conformance.Structural.Bindings.emptyGeometry $+            RCons (Proxy @"acceptedCount") 0 RNil++$(deriveAggregateCtorsAll ''ArtifactCatalogCommand ''ArtifactCatalogRegs)++$(deriveWireCtorsAll ''ArtifactCatalogEvent)
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/EventStream.hs view
@@ -0,0 +1,44 @@+-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.ArtifactCatalog.EventStream (+    artifactCatalogCategory,+    artifactCatalogEventStream,+    artifactCatalogEventStreamDef,+    ArtifactCatalogEventStream,+    ArtifactCatalogEventStreamDef,+) where++import Generated.StructuralConformance.ArtifactCatalog.Codec (artifactCatalogCodec)+import Generated.StructuralConformance.ArtifactCatalog.Domain+import Keiki.Core (HsPred)+import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))+import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)+import Keiro.Stream qualified as Stream+import StructuralConformance.ArtifactCatalog.Holes (artifactCatalogTransducer)++-- The validated aggregate stream category (hole-kind 5: referenced, never retyped).+-- Entity streams are '<category>-<id>' via Keiro.Stream.entityStream.+-- categoryUnsafe is safe here because this generated literal passed the DSL category proof.+artifactCatalogCategory :: Stream.StreamCategory a+artifactCatalogCategory = Stream.categoryUnsafe "artifactCatalog"++type ArtifactCatalogEventStreamDef =+    EventStream (HsPred ArtifactCatalogRegs ArtifactCatalogCommand) ArtifactCatalogRegs ArtifactCatalogVertex ArtifactCatalogCommand ArtifactCatalogEvent++type ArtifactCatalogEventStream =+    ValidatedEventStream (HsPred ArtifactCatalogRegs ArtifactCatalogCommand) ArtifactCatalogRegs ArtifactCatalogVertex ArtifactCatalogCommand ArtifactCatalogEvent++artifactCatalogEventStreamDef :: ArtifactCatalogEventStreamDef+artifactCatalogEventStreamDef =+    EventStream+        { transducer = artifactCatalogTransducer+        , initialState = ArtifactCatalogEmpty+        , initialRegisters = initialArtifactCatalogRegs+        , eventCodec = artifactCatalogCodec+        , resolveStreamName = Stream.streamName+        , snapshotPolicy = Never+        , stateCodec = Nothing+        }++artifactCatalogEventStream :: ArtifactCatalogEventStream+artifactCatalogEventStream =+    mkEventStreamOrThrow "ArtifactCatalog" artifactCatalogEventStreamDef
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Harness.hs view
@@ -0,0 +1,240 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.ArtifactCatalog.Harness (harnessAssertions) where++import Conformance.Structural.Bindings qualified+import Conformance.Structural.Domain qualified+import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as AesonKey+import Data.Aeson.KeyMap qualified as AesonKeyMap+import Data.Either (isLeft, isRight)+import Data.List (nub)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Maybe (isJust, isNothing)+import Data.Proxy (Proxy (..))+import Data.Text qualified as T+import Generated.StructuralConformance.ArtifactCatalog.Codec (artifactCatalogCodec, decodeArtifactInfoMapped, decodeArtifactKindMapped, decodeArtifactLocationMapped, decodeArtifactMetadataMapped, encodeArtifactCatalogEvent, encodeArtifactInfoMapped, encodeArtifactKindMapped, encodeArtifactLocationMapped, encodeArtifactMetadataMapped, parseArtifactCatalogEvent)+import Generated.StructuralConformance.ArtifactCatalog.Domain+import Generated.StructuralConformance.Structural.Shape.ArtifactInfo qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactKind qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactLocation qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactMetadata qualified+import Generated.StructuralConformance.StructuralProjections qualified as StructuralProjections+import Keiki.Core (applyEventsEither, defaultValidationOptions, fieldWitnessAgrees, step, validateTransducer, (!))+import Keiki.Shape (CanonicalTypeName (..))+import Keiro.Codec (eventType)+import Keiro.Codec.Structural (FixtureCases (..), bindingDomainRoundTrip, bindingShapeRoundTrip, bindingToShape)+import StructuralConformance.ArtifactCatalog.Holes (artifactCatalogTransducer)++{- | (label, passed). A driver runs these and exits non-zero on any False,+naming the failing assertion. Filling a hole wrongly turns a specific+entry False; the scaffold cannot.+-}+harnessAssertions :: [(String, Bool)]+harnessAssertions =+    [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions artifactCatalogTransducer))+    , ("clock-free: spec samples no wall clock", True)+    , ("golden round-trip: ArtifactRecorded", roundTrips sampleEventArtifactRecorded)+    , ("golden round-trip: ArtifactAccepted", roundTrips sampleEventArtifactAccepted)+    , ("accepts ObserveArtifact from ArtifactCatalogEmpty", acceptObserveArtifact)+    ]+        ++ mappedConformanceAssertions+        ++ forwardReplayObserveArtifact++roundTrips :: ArtifactCatalogEvent -> Bool+roundTrips e = parseArtifactCatalogEvent (eventType artifactCatalogCodec e) (encodeArtifactCatalogEvent e) == Right e++sampleEventArtifactRecorded :: ArtifactCatalogEvent+sampleEventArtifactRecorded = (ArtifactRecorded (ArtifactRecordedData (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.geometryCases))) False))++sampleEventArtifactAccepted :: ArtifactCatalogEvent+sampleEventArtifactAccepted = (ArtifactAccepted (ArtifactAcceptedData False))++acceptObserveArtifact :: Bool+acceptObserveArtifact =+    case step artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) ((ObserveArtifact (ObserveArtifactData (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.geometryCases))) False))) of+        Just (v, _, _) -> v == ArtifactCatalogObserved+        Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayObserveArtifact :: [(String, Bool)]+forwardReplayObserveArtifact =+    case step artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) ((ObserveArtifact (ObserveArtifactData (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.geometryCases))) False))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseArtifactCatalogEvent (eventType artifactCatalogCodec event) (encodeArtifactCatalogEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register currentArtifact", (replayRegs ! #currentArtifact) == (forwardRegs ! #currentArtifact))+                            , (prefix <> "register currentGeometry", (replayRegs ! #currentGeometry) == (forwardRegs ! #currentGeometry))+                            , (prefix <> "register acceptedCount", (replayRegs ! #acceptedCount) == (forwardRegs ! #acceptedCount))+                            ]+  where+    prefix = "forward/replay equality: ObserveArtifact from ArtifactCatalogEmpty -- "++mappedConformanceAssertions :: [(String, Bool)]+mappedConformanceAssertions =+    concat+        [ artifactInfoBindingAssertions+        , artifactKindBindingAssertions+        , artifactLocationBindingAssertions+        , artifactMetadataBindingAssertions+        , vendorGeometryOpaqueAssertions+        , [("fixture coverage: conformance.structural.ArtifactInfo.v1", coverageArtifactInfo)]+        , [("fixture coverage: conformance.structural.ArtifactKind.v1", coverageArtifactKind)]+        , [("fixture coverage: conformance.structural.ArtifactLocation.v1", coverageArtifactLocation)]+        , [("fixture coverage: conformance.structural.ArtifactMetadata.v1", coverageArtifactMetadata)]+        , artifactRecordedArtifactAssertions+        , artifactRecordedGeometryAssertions+        , structuralWirePolicyAssertions+        , structuralProjectionAssertions+        ]++validFixtureLabels :: NonEmpty.NonEmpty (T.Text, value) -> Bool+validFixtureLabels cases =+    all (not . T.null) labels && length labels == length (nub labels)+  where+    labels = map fst (NonEmpty.toList cases)++artifactInfoBindingAssertions :: [(String, Bool)]+artifactInfoBindingAssertions =+    ("fixture labels: conformance.structural.ArtifactInfo.v1", validFixtureLabels cases)+        : ("canonical identity: conformance.structural.ArtifactInfo.v1", canonicalTypeName (Proxy @Conformance.Structural.Domain.ArtifactInfo) == "conformance.structural.ArtifactInfo.v1")+        : concat+            [ [ ("binding domain round-trip: conformance.structural.ArtifactInfo.v1/" <> T.unpack label, bindingDomainRoundTrip Conformance.Structural.Bindings.artifactInfoBinding value)+              , ("binding shape round-trip: conformance.structural.ArtifactInfo.v1/" <> T.unpack label, bindingShapeRoundTrip Conformance.Structural.Bindings.artifactInfoBinding (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding value))+              ]+            | (label, value) <- NonEmpty.toList cases+            ]+  where+    cases = fixtureCases Conformance.Structural.Bindings.artifactInfoCases++artifactKindBindingAssertions :: [(String, Bool)]+artifactKindBindingAssertions =+    ("fixture labels: conformance.structural.ArtifactKind.v1", validFixtureLabels cases)+        : ("canonical identity: conformance.structural.ArtifactKind.v1", canonicalTypeName (Proxy @Conformance.Structural.Domain.ArtifactKind) == "conformance.structural.ArtifactKind.v1")+        : concat+            [ [ ("binding domain round-trip: conformance.structural.ArtifactKind.v1/" <> T.unpack label, bindingDomainRoundTrip Conformance.Structural.Bindings.artifactKindBinding value)+              , ("binding shape round-trip: conformance.structural.ArtifactKind.v1/" <> T.unpack label, bindingShapeRoundTrip Conformance.Structural.Bindings.artifactKindBinding (bindingToShape Conformance.Structural.Bindings.artifactKindBinding value))+              ]+            | (label, value) <- NonEmpty.toList cases+            ]+  where+    cases = fixtureCases Conformance.Structural.Bindings.artifactKindCases++artifactLocationBindingAssertions :: [(String, Bool)]+artifactLocationBindingAssertions =+    ("fixture labels: conformance.structural.ArtifactLocation.v1", validFixtureLabels cases)+        : ("canonical identity: conformance.structural.ArtifactLocation.v1", canonicalTypeName (Proxy @Conformance.Structural.Domain.ArtifactLocation) == "conformance.structural.ArtifactLocation.v1")+        : concat+            [ [ ("binding domain round-trip: conformance.structural.ArtifactLocation.v1/" <> T.unpack label, bindingDomainRoundTrip Conformance.Structural.Bindings.artifactLocationBinding value)+              , ("binding shape round-trip: conformance.structural.ArtifactLocation.v1/" <> T.unpack label, bindingShapeRoundTrip Conformance.Structural.Bindings.artifactLocationBinding (bindingToShape Conformance.Structural.Bindings.artifactLocationBinding value))+              ]+            | (label, value) <- NonEmpty.toList cases+            ]+  where+    cases = fixtureCases Conformance.Structural.Bindings.artifactLocationCases++artifactMetadataBindingAssertions :: [(String, Bool)]+artifactMetadataBindingAssertions =+    ("fixture labels: conformance.structural.ArtifactMetadata.v1", validFixtureLabels cases)+        : ("canonical identity: conformance.structural.ArtifactMetadata.v1", canonicalTypeName (Proxy @Conformance.Structural.Domain.ArtifactMetadata) == "conformance.structural.ArtifactMetadata.v1")+        : concat+            [ [ ("binding domain round-trip: conformance.structural.ArtifactMetadata.v1/" <> T.unpack label, bindingDomainRoundTrip Conformance.Structural.Bindings.artifactMetadataBinding value)+              , ("binding shape round-trip: conformance.structural.ArtifactMetadata.v1/" <> T.unpack label, bindingShapeRoundTrip Conformance.Structural.Bindings.artifactMetadataBinding (bindingToShape Conformance.Structural.Bindings.artifactMetadataBinding value))+              ]+            | (label, value) <- NonEmpty.toList cases+            ]+  where+    cases = fixtureCases Conformance.Structural.Bindings.artifactMetadataCases++vendorGeometryOpaqueAssertions :: [(String, Bool)]+vendorGeometryOpaqueAssertions =+    ("opaque boundary fixtures: vendor.geometry.json@3", validFixtureLabels cases)+        : [ ("opaque codec round-trip: vendor.geometry.json@3/" <> T.unpack caseLabel, case Aeson.fromJSON (Aeson.toJSON value) of Aeson.Success decoded -> decoded == value; Aeson.Error _ -> False)+          | (caseLabel, value) <- NonEmpty.toList cases+          ]+  where+    cases = fixtureCases Conformance.Structural.Bindings.geometryCases++coverageArtifactInfo :: Bool+coverageArtifactInfo = any (isNothing . Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactHash) shapes && any (isJust . Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactHash) shapes+  where+    shapes = map (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding . snd) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))++coverageArtifactKind :: Bool+coverageArtifactKind = any (\case Generated.StructuralConformance.Structural.Shape.ArtifactKind.Guide -> True; _ -> False) shapes && any (\case Generated.StructuralConformance.Structural.Shape.ArtifactKind.Reference -> True; _ -> False) shapes+  where+    shapes = map (bindingToShape Conformance.Structural.Bindings.artifactKindBinding . snd) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactKindCases))++coverageArtifactLocation :: Bool+coverageArtifactLocation = any (\case Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalFile{} -> True; _ -> False) shapes && any (\case Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocalDir{} -> True; _ -> False) shapes && any (\case Generated.StructuralConformance.Structural.Shape.ArtifactLocation.RepoPath{} -> True; _ -> False) shapes && any (\case Generated.StructuralConformance.Structural.Shape.ArtifactLocation.LocUrl{} -> True; _ -> False) shapes && any (\case Generated.StructuralConformance.Structural.Shape.ArtifactLocation.Canonical -> True; _ -> False) shapes+  where+    shapes = map (bindingToShape Conformance.Structural.Bindings.artifactLocationBinding . snd) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases))++coverageArtifactMetadata :: Bool+coverageArtifactMetadata = any (isNothing . Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.note) shapes && any (isJust . Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.note) shapes+  where+    shapes = map (bindingToShape Conformance.Structural.Bindings.artifactMetadataBinding . snd) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactMetadataCases))++artifactRecordedArtifactAssertions :: [(String, Bool)]+artifactRecordedArtifactAssertions =+    [ ("mapped codec round-trip: ArtifactRecorded/artifact/" <> T.unpack label, roundTrips (ArtifactRecorded (ArtifactRecordedData mappedValue (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.geometryCases))) False)))+    | (label, mappedValue) <- NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)+    ]++artifactRecordedGeometryAssertions :: [(String, Bool)]+artifactRecordedGeometryAssertions =+    [ ("mapped codec round-trip: ArtifactRecorded/geometry/" <> T.unpack label, roundTrips (ArtifactRecorded (ArtifactRecordedData (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))) mappedValue False)))+    | (label, mappedValue) <- NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.geometryCases)+    ]++structuralWirePolicyAssertions :: [(String, Bool)]+structuralWirePolicyAssertions =+    [ ("wire policy missing default: conformance.structural.ArtifactInfo.v1/artifact_hash", case decodeArtifactInfoMapped (deleteObjectField "artifact_hash" (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))))) of Left _ -> False; Right decoded -> objectField "artifact_hash" (encodeArtifactInfoMapped decoded) == Just (Aeson.Null))+    , ("wire policy explicit null: conformance.structural.ArtifactInfo.v1/artifact_hash", isRight (decodeArtifactInfoMapped (insertObjectField "artifact_hash" Aeson.Null (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))))))+    , ("wire policy missing default: conformance.structural.ArtifactInfo.v1/artifact_kind", case decodeArtifactInfoMapped (deleteObjectField "artifact_kind" (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))))) of Left _ -> False; Right decoded -> objectField "artifact_kind" (encodeArtifactInfoMapped decoded) == Just (Aeson.String "guide"))+    , ("wire policy explicit null: conformance.structural.ArtifactInfo.v1/artifact_kind", isLeft (decodeArtifactInfoMapped (insertObjectField "artifact_kind" Aeson.Null (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))))))+    , ("wire policy missing default: conformance.structural.ArtifactInfo.v1/active", case decodeArtifactInfoMapped (deleteObjectField "active" (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))))) of Left _ -> False; Right decoded -> objectField "active" (encodeArtifactInfoMapped decoded) == Just (Aeson.Bool False))+    , ("wire policy explicit null: conformance.structural.ArtifactInfo.v1/active", isLeft (decodeArtifactInfoMapped (insertObjectField "active" Aeson.Null (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))))))+    , ("wire policy missing default: conformance.structural.ArtifactInfo.v1/tags", case decodeArtifactInfoMapped (deleteObjectField "tags" (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases))))) of Left _ -> False; Right decoded -> objectField "tags" (encodeArtifactInfoMapped decoded) == Just (Aeson.toJSON ([] :: [Aeson.Value])))+    , ("wire policy explicit null: conformance.structural.ArtifactInfo.v1/tags", isLeft (decodeArtifactInfoMapped (insertObjectField "tags" Aeson.Null (encodeArtifactInfoMapped (snd (NonEmpty.head (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))))))+    , ("wire policy unknown fields: conformance.structural.ArtifactInfo.v1", all (\(_, value) -> isLeft (decodeArtifactInfoMapped (insertObjectField "__keiro_unknown" (Aeson.Bool True) (encodeArtifactInfoMapped value)))) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))+    , ("wire enum arm: conformance.structural.ArtifactKind.v1/guide", any (\(_, value) -> encodeArtifactKindMapped value == Aeson.String "guide" && decodeArtifactKindMapped (Aeson.String "guide") == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactKindCases)))+    , ("wire enum arm: conformance.structural.ArtifactKind.v1/reference", any (\(_, value) -> encodeArtifactKindMapped value == Aeson.String "reference" && decodeArtifactKindMapped (Aeson.String "reference") == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactKindCases)))+    , ("wire enum unknown tag: conformance.structural.ArtifactKind.v1", isLeft (decodeArtifactKindMapped (Aeson.String "__keiro_unknown")))+    , ("wire union arm: conformance.structural.ArtifactLocation.v1/local_file", any (\(_, value) -> objectField "tag" (encodeArtifactLocationMapped value) == Just (Aeson.String "local_file") && decodeArtifactLocationMapped (encodeArtifactLocationMapped value) == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire union arm: conformance.structural.ArtifactLocation.v1/local_dir", any (\(_, value) -> objectField "tag" (encodeArtifactLocationMapped value) == Just (Aeson.String "local_dir") && decodeArtifactLocationMapped (encodeArtifactLocationMapped value) == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire union arm: conformance.structural.ArtifactLocation.v1/repo_path", any (\(_, value) -> objectField "tag" (encodeArtifactLocationMapped value) == Just (Aeson.String "repo_path") && decodeArtifactLocationMapped (encodeArtifactLocationMapped value) == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire union arm: conformance.structural.ArtifactLocation.v1/url", any (\(_, value) -> objectField "tag" (encodeArtifactLocationMapped value) == Just (Aeson.String "url") && decodeArtifactLocationMapped (encodeArtifactLocationMapped value) == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire union arm: conformance.structural.ArtifactLocation.v1/canonical", any (\(_, value) -> objectField "tag" (encodeArtifactLocationMapped value) == Just (Aeson.String "canonical") && decodeArtifactLocationMapped (encodeArtifactLocationMapped value) == Right value) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire policy unknown fields: conformance.structural.ArtifactLocation.v1", all (\(_, value) -> isLeft (decodeArtifactLocationMapped (insertObjectField "__keiro_unknown" (Aeson.Bool True) (encodeArtifactLocationMapped value)))) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactLocationCases)))+    , ("wire policy unknown fields: conformance.structural.ArtifactMetadata.v1", all (\(_, value) -> isRight (decodeArtifactMetadataMapped (insertObjectField "__keiro_unknown" (Aeson.Bool True) (encodeArtifactMetadataMapped value)))) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactMetadataCases)))+    ]++structuralProjectionAssertions :: [(String, Bool)]+structuralProjectionAssertions =+    [ ("projection witness agreement: conformance.structural.ArtifactInfo.v1/artifact_key", all (\(_, owner) -> fieldWitnessAgrees StructuralProjections.structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness (\referenceOwner -> Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactKey (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding referenceOwner)) owner) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))+    , ("projection witness agreement: conformance.structural.ArtifactInfo.v1/display_name", all (\(_, owner) -> fieldWitnessAgrees StructuralProjections.structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65ZWitness (\referenceOwner -> Generated.StructuralConformance.Structural.Shape.ArtifactInfo.displayName (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding referenceOwner)) owner) (NonEmpty.toList (fixtureCases Conformance.Structural.Bindings.artifactInfoCases)))+    ]++deleteObjectField :: T.Text -> Aeson.Value -> Aeson.Value+deleteObjectField key (Aeson.Object objectValue) = Aeson.Object (AesonKeyMap.delete (AesonKey.fromText key) objectValue)+deleteObjectField _ value = value++insertObjectField :: T.Text -> Aeson.Value -> Aeson.Value -> Aeson.Value+insertObjectField key inserted (Aeson.Object objectValue) = Aeson.Object (AesonKeyMap.insert (AesonKey.fromText key) inserted objectValue)+insertObjectField _ _ value = value++objectField :: T.Text -> Aeson.Value -> Maybe Aeson.Value+objectField key (Aeson.Object objectValue) = AesonKeyMap.lookup (AesonKey.fromText key) objectValue+objectField _ _ = Nothing
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Projection.hs view
@@ -0,0 +1,2 @@+-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.ArtifactCatalog.Projection () where
+ test/conformance-structural/Generated/StructuralConformance/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.StructuralConformance.ReplayAudit (auditTargets) where++import Generated.StructuralConformance.ArtifactCatalog.EventStream qualified as ArtifactCatalog+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = ArtifactCatalog.artifactCatalogEventStream+            , category = Stream.categoryText ArtifactCatalog.artifactCatalogCategory+            , mkStream = streamInCategory (Stream.categoryText ArtifactCatalog.artifactCatalogCategory)+            }+    ]
+ test/conformance-structural/Generated/StructuralConformance/Structural/CodecCompare/ArtifactInfo.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE OverloadedStrings #-}++-- @generated by keiro-dsl codec comparison; non-production migration evidence; do not edit.+-- This module compares historical and generated codecs in consumer-owned tests only.+-- It is never a runtime fallback and never changes the generated codec's authority.+module Generated.StructuralConformance.Structural.CodecCompare.ArtifactInfo (compareWithHistorical) where++import Control.Monad (filterM)+import Data.Aeson (Value)+import Data.Aeson qualified as Aeson+import Data.List (sort)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Text (Text)+import Data.Text qualified+import Generated.StructuralConformance.ArtifactCatalog.Codec qualified as GeneratedCodec+import Keiro.Codec.Structural (FixtureCases (..))+import Keiro.Dsl.CodecCompare+import Keiro.Dsl.TypeGraph (BindingVersion (..), CanonicalTypeId (..), QualifiedValueName (..))+import System.Directory (doesFileExist, listDirectory)+import System.FilePath (takeExtension, (</>))++import Conformance.Structural.Bindings qualified as ConsumerFixtures+import Conformance.Structural.Domain qualified as ConsumerDomain++compareWithHistorical :: HistoricalCodec ConsumerDomain.ArtifactInfo -> FilePath -> IO CompareReport+compareWithHistorical historicalCodec goldenDirectory = do+    names <- sort . filter ((== ".json") . takeExtension) <$> listDirectory goldenDirectory+    files <- filterM doesFileExist [goldenDirectory </> name | name <- names]+    loaded <- traverse (loadGolden historicalCodec) files+    let inputIssues = [issue | Left issue <- loaded]+        entries = [entry | Right entry <- loaded]+        typedCases = NonEmpty.toList (fixtureCases ConsumerFixtures.artifactInfoCases)+        encodeObservations =+            [ EncodeObservation label (hcEncode historicalCodec value) (GeneratedCodec.encodeArtifactInfoMapped value)+            | (label, value) <- typedCases+            ]+        decodeObservations = [observation | (observation, _) <- entries]+        typedObserved =+            concat+                [ observedBranchesFor FromBinding branchSchema (GeneratedCodec.encodeArtifactInfoMapped value)+                | (_, value) <- typedCases+                ]+        historicalObserved =+            concat [observedBranchesFor HistoricalGolden branchSchema value | (_, values) <- entries, value <- values]+        declared = declaredBranchesFor FromBinding branchSchema <> declaredBranchesFor HistoricalGolden branchSchema+        provenance =+            CompareProvenance+                { cpHistoricalCodecIdentity = hcIdentity historicalCodec+                , cpHistoricalCodecVersion = hcVersion historicalCodec+                , cpCanonicalType = CanonicalTypeId "conformance.structural.ArtifactInfo.v1"+                , cpBindingSymbol = QualifiedValueName "Conformance.Structural.Bindings.artifactInfoBinding"+                , cpBindingVersion = BindingVersion "1"+                , cpWireFingerprint = "f3b9417666fcf445"+                }+    pure (compareReport provenance inputIssues (encodeObservations <> decodeObservations) declared (typedObserved <> historicalObserved))++loadGolden :: HistoricalCodec ConsumerDomain.ArtifactInfo -> FilePath -> IO (Either CompareInputIssue (CompareObservation, [Value]))+loadGolden historicalCodec path = do+    decoded <- Aeson.eitherDecodeFileStrict path+    pure $ case decoded of+        Left reason -> Left (HistoricalGoldenUnreadable path (fromString reason))+        Right inputValue ->+            let historicalDecoded = hcDecode historicalCodec inputValue+                historicalOutcome = normalizeDecode historicalDecoded+                generatedOutcome = normalizeDecode (GeneratedCodec.decodeArtifactInfoMapped inputValue)+                observation = DecodeObservation path inputValue historicalOutcome generatedOutcome+                coveredValues = case historicalDecoded of+                    Right value -> [inputValue, GeneratedCodec.encodeArtifactInfoMapped value]+                    Left _ -> []+             in Right (observation, coveredValues)++normalizeDecode :: Either Text ConsumerDomain.ArtifactInfo -> DecodeOutcome+normalizeDecode = either DecodeFailed (DecodedShape . GeneratedCodec.encodeArtifactInfoMapped)++fromString :: String -> Text+fromString = Data.Text.pack++branchSchema :: BranchSchema+branchSchema = BranchRecord [BranchField "artifact_key" False (BranchScalar), BranchField "display_name" False (BranchScalar), BranchField "artifact_hash" True (BranchOptional (BranchScalar)), BranchField "artifact_kind" True (BranchScalar), BranchField "location" False (BranchUnion "tag" "contents" [BranchArm "local_file" (Just (BranchScalar)), BranchArm "local_dir" (Just (BranchScalar)), BranchArm "repo_path" (Just (BranchScalar)), BranchArm "url" (Just (BranchScalar)), BranchArm "canonical" Nothing]), BranchField "metadata" False (BranchRecord [BranchField "note" False (BranchOptional (BranchScalar))]), BranchField "active" True (BranchScalar), BranchField "tags" True (BranchList (BranchScalar))]
+ test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactInfo.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.Structural.Shape.ArtifactInfo (ArtifactInfoShape (..)) where++import Data.Text (Text)+import GHC.Generics (Generic)+import Generated.StructuralConformance.Structural.Shape.ArtifactKind qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactLocation qualified+import Generated.StructuralConformance.Structural.Shape.ArtifactMetadata qualified++data ArtifactInfoShape = ArtifactInfo+    { artifactKey :: !Text+    , displayName :: !Text+    , artifactHash :: !(Maybe (Text))+    , artifactKind :: !Generated.StructuralConformance.Structural.Shape.ArtifactKind.ArtifactKindShape+    , location :: !Generated.StructuralConformance.Structural.Shape.ArtifactLocation.ArtifactLocationShape+    , metadata :: !Generated.StructuralConformance.Structural.Shape.ArtifactMetadata.ArtifactMetadataShape+    , active :: !Bool+    , tags :: !([Text])+    }+    deriving stock (Eq, Generic, Show)
+ test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactKind.hs view
@@ -0,0 +1,9 @@+{-# LANGUAGE DeriveGeneric #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.Structural.Shape.ArtifactKind (ArtifactKindShape (..)) where++import GHC.Generics (Generic)++data ArtifactKindShape = Guide | Reference+    deriving stock (Eq, Generic, Show)
+ test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactLocation.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE DeriveGeneric #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.Structural.Shape.ArtifactLocation (ArtifactLocationShape (..)) where++import Data.Text (Text)+import GHC.Generics (Generic)++data ArtifactLocationShape+    = LocalFile !Text+    | LocalDir !Text+    | RepoPath !Text+    | LocUrl !Text+    | Canonical+    deriving stock (Eq, Generic, Show)
+ test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactMetadata.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+module Generated.StructuralConformance.Structural.Shape.ArtifactMetadata (ArtifactMetadataShape (..)) where++import Data.Text (Text)+import GHC.Generics (Generic)++data ArtifactMetadataShape = ArtifactMetadata+    { note :: !(Maybe (Text))+    }+    deriving stock (Eq, Generic, Show)
+ test/conformance-structural/Generated/StructuralConformance/StructuralProjections.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+-- Equality witnesses are emitted for Text, Int, Bool, and UTCTime.+-- Only Int and UTCTime belong to Keiki's v1 ordered subset.+module Generated.StructuralConformance.StructuralProjections (+    structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness,+    structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65ZWitness,+) where++import Conformance.Structural.Bindings qualified+import Conformance.Structural.Domain qualified+import Data.Text (Text)+import Data.Time (UTCTime)+import Generated.StructuralConformance.Structural.Shape.ArtifactInfo qualified+import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)+import Keiro.Codec.Structural (bindingToShape)++data StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z++instance FieldProjection StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z where+    type FieldName StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z = "/artifact_key"+    type FieldOwner StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z = Conformance.Structural.Domain.ArtifactInfo+    type FieldResult StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z = Text+    fieldShapeId _ = "conformance.structural.ArtifactInfo.v1"+    projectFieldValue _ owner = Generated.StructuralConformance.Structural.Shape.ArtifactInfo.artifactKey (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding owner)++structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness :: FieldWitness StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z+structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness = fieldWitness @StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79Z++data StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z++instance FieldProjection StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z where+    type FieldName StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z = "/display_name"+    type FieldOwner StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z = Conformance.Structural.Domain.ArtifactInfo+    type FieldResult StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z = Text+    fieldShapeId _ = "conformance.structural.ArtifactInfo.v1"+    projectFieldValue _ owner = Generated.StructuralConformance.Structural.Shape.ArtifactInfo.displayName (bindingToShape Conformance.Structural.Bindings.artifactInfoBinding owner)++structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65ZWitness :: FieldWitness StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z+structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65ZWitness = fieldWitness @StructuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC64ZC69ZC73ZC70ZC6cZC61ZC79ZC5fZC6eZC61ZC6dZC65Z
+ test/conformance-structural/Main.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}++module Main (main) where++import Conformance.Structural.Bindings qualified as Bindings+import Control.Monad (forM_, unless)+import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as LazyByteString+import Data.ByteString.Lazy.Char8 qualified as LazyChar8+import Data.List.NonEmpty qualified as NonEmpty+import Generated.StructuralConformance.ArtifactCatalog.Codec (encodeArtifactCatalogEvent)+import Generated.StructuralConformance.ArtifactCatalog.Domain (ArtifactCatalogCommand, ArtifactCatalogEvent (..), ArtifactCatalogRegs, ArtifactRecordedData (..), inCtorObserveArtifact)+import Generated.StructuralConformance.ArtifactCatalog.Harness (harnessAssertions)+import Generated.StructuralConformance.StructuralProjections qualified as StructuralProjections+import Keiki.Core (HsPred, inpProj, (./=))+import Keiki.Symbolic (symIsBot)+import Keiro.Codec.Structural (FixtureCases (..))+import System.Exit (exitFailure)++main :: IO ()+main = do+    goldenAssertions <- loadGoldenAssertions+    let assertions = harnessAssertions <> projectionAssertions <> goldenAssertions+    forM_ assertions $ \(label, ok) ->+        putStrLn ((if ok then "PASS  " else "FAIL  ") <> label)+    let failed = [label | (label, ok) <- assertions, not ok]+    unless (null failed) $ do+        putStrLn ("structural harness: " <> show (length failed) <> " assertion(s) failed")+        exitFailure++projectionAssertions :: [(String, Bool)]+projectionAssertions =+    [+        ( "projection key sharing: conformance.structural.ArtifactInfo.v1/artifact_key"+        , symIsBot+            ( inpProj artifactKeyWitness inCtorObserveArtifact #artifact+                ./= inpProj artifactKeyWitness inCtorObserveArtifact #artifact ::+                HsPred ArtifactCatalogRegs ArtifactCatalogCommand+            )+        )+    ]+  where+    artifactKeyWitness = StructuralProjections.structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness++loadGoldenAssertions :: IO [(String, Bool)]+loadGoldenAssertions = do+    actual <- LazyByteString.readFile "test/golden-payloads/structural-conformance/ArtifactCatalog/ArtifactRecorded.v1.json"+    let artifact = snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))+        geometry = snd (NonEmpty.head (fixtureCases Bindings.geometryCases))+        event = ArtifactRecorded ArtifactRecordedData{artifact, geometry, accepted = False}+        expected = Aeson.encode (encodeArtifactCatalogEvent event) <> "\n"+    unless (actual == expected) $ do+        LazyChar8.putStrLn ("expected: " <> expected)+        LazyChar8.putStrLn ("actual:   " <> actual)+    pure [("current JSON golden: ArtifactRecorded.v1", actual == expected)]
+ test/conformance-structural/StructuralConformance/ArtifactCatalog/Holes.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE QualifiedDo #-}+{-# LANGUAGE TypeApplications #-}++-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never+-- overwrites it. Fill the transducer body (and any other holes) against the+-- generated signatures, then run the harness to confirm behaviour.+module StructuralConformance.ArtifactCatalog.Holes (+    artifactCatalogTransducer,+) where++import Generated.StructuralConformance.ArtifactCatalog.Domain+import Generated.StructuralConformance.StructuralProjections qualified as StructuralProjections+import Keiki.Builder ((=:))+import Keiki.Builder qualified as B+import Keiki.Core (HsPred, SymTransducer, inpProj, lit)++-- HOLE: the transducer body. Reproduce the structure below, replacing each+-- `-- HOLE` line with the keiki symbolic operators it describes.+artifactCatalogTransducer ::+    SymTransducer+        (HsPred ArtifactCatalogRegs ArtifactCatalogCommand)+        ArtifactCatalogRegs+        ArtifactCatalogVertex+        ArtifactCatalogCommand+        ArtifactCatalogEvent+artifactCatalogTransducer =+    B.buildTransducer ArtifactCatalogEmpty initialArtifactCatalogRegs isTerminal do+        B.from ArtifactCatalogEmpty do+            B.onCmd inCtorObserveArtifact $ \d -> B.do+                B.requireEq+                    (inpProj StructuralProjections.structuralProjectionC41ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC49ZC6eZC66ZC6fZC2fZC61ZC72ZC74ZC69ZC66ZC61ZC63ZC74ZC5fZC6bZC65ZC79ZWitness inCtorObserveArtifact #artifact)+                    (lit "artifact-local-file")+                B.slot @"currentArtifact" =: d.artifact+                B.slot @"currentGeometry" =: d.geometry+                B.slot @"acceptedCount" =: B.reg @"acceptedCount"+                B.emit wireArtifactRecorded ArtifactRecordedTermFields{artifact = d.artifact, geometry = d.geometry, accepted = d.accepted}+                B.emit wireArtifactAccepted ArtifactAcceptedTermFields{accepted = d.accepted}+                B.goto ArtifactCatalogObserved+  where+    isTerminal = \case+        ArtifactCatalogObserved -> True+        _ -> False
+ test/conformance-v2/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.HospitalCapacity.ReplayAudit (auditTargets) where++import Generated.HospitalCapacity.Reservation.EventStream qualified as Reservation+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Reservation.reservationEventStream+            , category = Stream.categoryText Reservation.reservationCategory+            , mkStream = streamInCategory (Stream.categoryText Reservation.reservationCategory)+            }+    ]
test/conformance-v2/Generated/HospitalCapacity/Reservation/Codec.hs view
@@ -47,8 +47,14 @@         , schemaVersion = 2         , encode = encodeReservationEvent         , decode = parseReservationEvent-        , upcasters = [(1, const upcastTransferReservationCreatedV1)]+        , upcasters = [(1, upcastRungV1)]         }++upcastRungV1 :: EventType -> Value -> Either Text Value+upcastRungV1 (EventType "TransferReservationCreated") value = upcastTransferReservationCreatedV1 value+-- Kinds whose shape did not change at this rung pass through unchanged; their+-- stamped version is aggregate-global, not their own shape history.+upcastRungV1 _ value = Right value  encodeReservationEvent :: ReservationEvent -> Value encodeReservationEvent = \case
test/conformance-v2/Generated/HospitalCapacity/Reservation/Harness.hs view
@@ -1,12 +1,16 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec. module Generated.HospitalCapacity.Reservation.Harness (harnessAssertions) where +import Data.Aeson (eitherDecodeStrict)+import Data.Text.Encoding (encodeUtf8) import Generated.HospitalCapacity.Reservation.Codec (encodeReservationEvent, parseReservationEvent, reservationCodec) import Generated.HospitalCapacity.Reservation.Domain import HospitalCapacity.Reservation.Holes (reservationTransducer)-import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!)) import Keiro.Codec (EventType (..), decodeRaw, eventType)  {- | (label, passed). A driver runs these and exits non-zero on any False,@@ -20,14 +24,16 @@     , ("golden round-trip: TransferReservationCreated", roundTrips sampleEventTransferReservationCreated)     , ("golden round-trip: TransferReservationConfirmed", roundTrips sampleEventTransferReservationConfirmed)     , ("accepts RequestTransferReservation from ReservationUnrequested", acceptRequestTransferReservation)-    , ("upcaster wired: a v1 TransferReservationCreated payload decodes through the chain", upcastsTransferReservationCreated)     ]+        ++ forwardReplayRequestTransferReservation+        ++ [ ("golden TransferReservationCreated.v1 decodes", upcastsTransferReservationCreated)+           ]  roundTrips :: ReservationEvent -> Bool roundTrips e = parseReservationEvent (eventType reservationCodec e) (encodeReservationEvent e) == Right e  sampleEventTransferReservationCreated :: ReservationEvent-sampleEventTransferReservationCreated = (TransferReservationCreated (TransferReservationCreatedData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample") RedTag Open False "sample"))+sampleEventTransferReservationCreated = (TransferReservationCreated (TransferReservationCreatedData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample") RedTag Open False "sample-triageNote"))  sampleEventTransferReservationConfirmed :: ReservationEvent sampleEventTransferReservationConfirmed = (TransferReservationConfirmed (TransferReservationConfirmedData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample")))@@ -38,9 +44,34 @@         Just (v, _, _) -> v == ReservationHeld         Nothing -> False +-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayRequestTransferReservation :: [(String, Bool)]+forwardReplayRequestTransferReservation =+    case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample") RedTag Open False))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseReservationEvent (eventType reservationCodec event) (encodeReservationEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither reservationTransducer (ReservationUnrequested, initialReservationRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register reservationId", (replayRegs ! #reservationId) == (forwardRegs ! #reservationId))+                            , (prefix <> "register hospitalId", (replayRegs ! #hospitalId) == (forwardRegs ! #hospitalId))+                            , (prefix <> "register patientAcuity", (replayRegs ! #patientAcuity) == (forwardRegs ! #patientAcuity))+                            , (prefix <> "register reservationState", (replayRegs ! #reservationState) == (forwardRegs ! #reservationState))+                            ]+  where+    prefix = "forward/replay equality: RequestTransferReservation from ReservationUnrequested -- "+ upcastsTransferReservationCreated :: Bool upcastsTransferReservationCreated =-    either-        (const False)-        (const True)-        (decodeRaw reservationCodec (EventType "TransferReservationCreated") 1 (encodeReservationEvent sampleEventTransferReservationCreated))+    case eitherDecodeStrict (encodeUtf8 "{\n  \"kind\": \"TransferReservationCreated\",\n  \"reservationId\": \"rsv_01hzy3v7q2e8kaw2m5x0d41n9c\",\n  \"hospitalId\": \"hosp_01hzy3v7q2e8kaw2m5x0d41n9d\",\n  \"commandId\": \"cmd_01hzy3v7q2e8kaw2m5x0d41n9e\",\n  \"patientAcuity\": \"red\",\n  \"divertStatus\": \"open\",\n  \"lifeCriticalOverride\": true\n}\n") of+        Left _ -> False+        Right payload ->+            either+                (const False)+                (const True)+                (decodeRaw reservationCodec (EventType "TransferReservationCreated") 1 payload)
test/conformance-v2/Generated/HospitalCapacity/Reservation/Projection.hs view
@@ -1,27 +1,29 @@ {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}+ -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.-module Generated.HospitalCapacity.Reservation.Projection-  ( transfer_decisionsProjection-  , transfer_decisionsStatusFor-  ) where+module Generated.HospitalCapacity.Reservation.Projection (+    transfer_decisionsProjection,+    transfer_decisionsStatusFor,+) where +import Data.Text (Text) import Generated.HospitalCapacity.Reservation.Domain import HospitalCapacity.Reservation.Holes (applyTransfer_decisions)-import Data.Text (Text) import Keiro.Projection (InlineProjection (..))  -- The deterministic event->status mapping (hole-kind 3, /mapping/), derived -- from the spec's status-map. The read-model SQL that consumes it lives in -- the hand-owned Holes module (a DB-coupled hole, delegated to codd).+-- WARNING: no readmodel node declares 'transfer_decisions'; unqualified SQL depends on search_path. transfer_decisionsStatusFor :: ReservationEvent -> Maybe Text transfer_decisionsStatusFor = \case-  TransferReservationCreated {} -> Just "held"-  TransferReservationConfirmed {} -> Just "confirmed"+    TransferReservationCreated{} -> Just "held"+    TransferReservationConfirmed{} -> Just "confirmed"  transfer_decisionsProjection :: InlineProjection ReservationEvent transfer_decisionsProjection =-  InlineProjection-    { name = "hospital-capacity-transfer_decisions-inline"-    , apply = applyTransfer_decisions-    }+    InlineProjection+        { name = "hospital-capacity-transfer_decisions-inline"+        , apply = applyTransfer_decisions+        }
test/conformance-v2/Main.hs view
@@ -8,15 +8,54 @@ -} module Main (main) where -import Control.Monad (forM_, unless)+import Control.Monad (forM, forM_, unless)+import Data.Aeson (Value, eitherDecodeFileStrict')+import Data.List (sort)+import Data.Text qualified as T+import Generated.HospitalCapacity.Reservation.Codec (reservationCodec) import Generated.HospitalCapacity.Reservation.Harness (harnessAssertions)+import Keiro.Codec (EventType (..), decodeRaw)+import System.Directory (listDirectory) import System.Exit (exitFailure)+import Text.Read (readMaybe)  main :: IO () main = do-    forM_ harnessAssertions $ \(label, ok) ->+    goldenAssertions <- loadGoldenAssertions+    let assertions = harnessAssertions <> goldenAssertions+    forM_ assertions $ \(label, ok) ->         putStrLn ((if ok then "PASS  " else "FAIL  ") <> label)-    let failed = [label | (label, ok) <- harnessAssertions, not ok]+    let failed = [label | (label, ok) <- assertions, not ok]     unless (null failed) $ do         putStrLn ("harness: " <> show (length failed) <> " assertion(s) failed")         exitFailure++loadGoldenAssertions :: IO [(String, Bool)]+loadGoldenAssertions = do+    files <- sort <$> listDirectory goldenDirectory+    forM [file | file <- files, ".json" `T.isSuffixOf` T.pack file] $ \file ->+        case parseGoldenName file of+            Nothing -> pure ("golden " <> file <> " (invalid fixture filename)", False)+            Just (tag, version) -> do+                payloadResult <- eitherDecodeFileStrict' (goldenDirectory <> "/" <> file) :: IO (Either String Value)+                pure+                    ( "golden " <> T.unpack tag <> ".v" <> show version+                    , case payloadResult of+                        Left _ -> False+                        Right payload ->+                            either+                                (const False)+                                (const True)+                                (decodeRaw reservationCodec (EventType tag) version payload)+                    )++goldenDirectory :: FilePath+goldenDirectory = "test/golden-payloads/hospital-capacity/Reservation"++parseGoldenName :: FilePath -> Maybe (T.Text, Int)+parseGoldenName file = do+    stem <- T.stripSuffix ".json" (T.pack file)+    let (tagWithMarker, versionText) = T.breakOnEnd ".v" stem+        tag = T.dropEnd 2 tagWithMarker+    version <- readMaybe (T.unpack versionText)+    if T.null tag || T.null versionText then Nothing else Just (tag, version)
+ test/conformance/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GADTs #-}++-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.+--+-- Deployment contract:+--   * replay-neutral diff: no data audit is required;+--   * affected diff: run AuditTargeted with the emitted affected set+--     against a production copy under the candidate binary;+--   * one-time runtime cutover: run AuditFull;+--   * any non-zero audit exit blocks deployment.+module Generated.HospitalCapacity.ReplayAudit (auditTargets) where++import Generated.HospitalCapacity.Reservation.EventStream qualified as Reservation+import Keiro.ReplayAudit (AuditTarget (..), SomeAuditTarget (..), streamInCategory)+import Keiro.Stream qualified as Stream++auditTargets :: [SomeAuditTarget]+auditTargets =+    [ SomeAuditTarget+        AuditTarget+            { eventStream = Reservation.reservationEventStream+            , category = Stream.categoryText Reservation.reservationCategory+            , mkStream = streamInCategory (Stream.categoryText Reservation.reservationCategory)+            }+    ]
test/conformance/Generated/HospitalCapacity/Reservation/Harness.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}  -- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.@@ -6,7 +8,7 @@ import Generated.HospitalCapacity.Reservation.Codec (encodeReservationEvent, parseReservationEvent, reservationCodec) import Generated.HospitalCapacity.Reservation.Domain import HospitalCapacity.Reservation.Holes (reservationTransducer)-import Keiki.Core (defaultValidationOptions, step, validateTransducer)+import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!)) import Keiro.Codec (eventType)  {- | (label, passed). A driver runs these and exits non-zero on any False,@@ -21,6 +23,7 @@     , ("golden round-trip: TransferReservationConfirmed", roundTrips sampleEventTransferReservationConfirmed)     , ("accepts RequestTransferReservation from ReservationUnrequested", acceptRequestTransferReservation)     ]+        ++ forwardReplayRequestTransferReservation  roundTrips :: ReservationEvent -> Bool roundTrips e = parseReservationEvent (eventType reservationCodec e) (encodeReservationEvent e) == Right e@@ -36,3 +39,25 @@     case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample") RedTag Open False))) of         Just (v, _, _) -> v == ReservationHeld         Nothing -> False++-- forward/replay equality (plan 147): cross the persisted codec boundary,+-- replay the emitted chain, and compare the final vertex and every register.+forwardReplayRequestTransferReservation :: [(String, Bool)]+forwardReplayRequestTransferReservation =+    case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (TransferReservationId "sample") (HospitalId "sample") (CommandId "sample") RedTag Open False))) of+        Nothing -> [(prefix <> "forward step accepted", False)]+        Just (forwardVertex, forwardRegs, emitted) ->+            case mapM (\event -> parseReservationEvent (eventType reservationCodec event) (encodeReservationEvent event)) emitted of+                Left _ -> [(prefix <> "emitted chain decodes", False)]+                Right decodedEvents ->+                    case applyEventsEither reservationTransducer (ReservationUnrequested, initialReservationRegs) decodedEvents of+                        Left _ -> [(prefix <> "replay succeeds", False)]+                        Right (replayVertex, replayRegs) ->+                            [ (prefix <> "final vertex", replayVertex == forwardVertex)+                            , (prefix <> "register reservationId", (replayRegs ! #reservationId) == (forwardRegs ! #reservationId))+                            , (prefix <> "register hospitalId", (replayRegs ! #hospitalId) == (forwardRegs ! #hospitalId))+                            , (prefix <> "register patientAcuity", (replayRegs ! #patientAcuity) == (forwardRegs ! #patientAcuity))+                            , (prefix <> "register reservationState", (replayRegs ! #reservationState) == (forwardRegs ! #reservationState))+                            ]+  where+    prefix = "forward/replay equality: RequestTransferReservation from ReservationUnrequested -- "