keiro-dsl 0.10.0.0 → 0.11.0.0
raw patch · 338 files changed
+22612/−14050 lines, 338 filesdep ~containersdep ~keikidep ~keiro-corePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: containers, keiki, keiro-core
API changes (from Hackage documentation)
- Keiro.Dsl.Grammar: [wqfRequired] :: WqField -> !Bool
- Keiro.Dsl.Validate: DuplicateUpcasterSource :: DiagnosticCode
- Keiro.Dsl.Validate: IdentHaskellKeyword :: DiagnosticCode
- Keiro.Dsl.Validate: IdentNotConstructorSafe :: DiagnosticCode
- Keiro.Dsl.Validate: MappedGuardUnsupported :: DiagnosticCode
+ Keiro.Dsl.CheckReport: CheckReportEnforcement :: !Maybe LanguageVersion -> !Bool -> ![DiagnosticCode] -> CheckReportEnforcement
+ Keiro.Dsl.CheckReport: CheckReportEntry :: !DiagnosticCode -> !Severity -> !FilePath -> !Int -> !Text -> !Bool -> ![CheckReportRelated] -> CheckReportEntry
+ Keiro.Dsl.CheckReport: CheckReportLanguage :: !Text -> !Maybe LanguageVersion -> !LanguageVersion -> !Text -> !LanguageSupport -> !Bool -> CheckReportLanguage
+ Keiro.Dsl.CheckReport: CheckReportMember :: !FilePath -> !Text -> !Maybe LanguageVersion -> CheckReportMember
+ Keiro.Dsl.CheckReport: CheckReportRelated :: !FilePath -> !Int -> !Text -> CheckReportRelated
+ Keiro.Dsl.CheckReport: CheckReportSummary :: !Int -> !Int -> !Int -> CheckReportSummary
+ Keiro.Dsl.CheckReport: [entryCode] :: CheckReportEntry -> !DiagnosticCode
+ Keiro.Dsl.CheckReport: [entryDenied] :: CheckReportEntry -> !Bool
+ Keiro.Dsl.CheckReport: [entryFile] :: CheckReportEntry -> !FilePath
+ Keiro.Dsl.CheckReport: [entryLine] :: CheckReportEntry -> !Int
+ Keiro.Dsl.CheckReport: [entryMessage] :: CheckReportEntry -> !Text
+ Keiro.Dsl.CheckReport: [entryRelated] :: CheckReportEntry -> ![CheckReportRelated]
+ Keiro.Dsl.CheckReport: [entrySeverity] :: CheckReportEntry -> !Severity
+ Keiro.Dsl.CheckReport: [memberDeclaredLanguageVersion] :: CheckReportMember -> !Maybe LanguageVersion
+ Keiro.Dsl.CheckReport: [memberPath] :: CheckReportMember -> !FilePath
+ Keiro.Dsl.CheckReport: [memberSourceForm] :: CheckReportMember -> !Text
+ Keiro.Dsl.CheckReport: [relatedFile] :: CheckReportRelated -> !FilePath
+ Keiro.Dsl.CheckReport: [relatedLine] :: CheckReportRelated -> !Int
+ Keiro.Dsl.CheckReport: [relatedNote] :: CheckReportRelated -> !Text
+ Keiro.Dsl.CheckReport: [reportDeclaredLanguageVersion] :: CheckReportLanguage -> !Maybe LanguageVersion
+ Keiro.Dsl.CheckReport: [reportDenyCodes] :: CheckReportEnforcement -> ![DiagnosticCode]
+ Keiro.Dsl.CheckReport: [reportDenyWarnings] :: CheckReportEnforcement -> !Bool
+ Keiro.Dsl.CheckReport: [reportEffectiveLanguageVersion] :: CheckReportLanguage -> !LanguageVersion
+ Keiro.Dsl.CheckReport: [reportLanguageSupport] :: CheckReportLanguage -> !LanguageSupport
+ Keiro.Dsl.CheckReport: [reportMinLanguage] :: CheckReportEnforcement -> !Maybe LanguageVersion
+ Keiro.Dsl.CheckReport: [reportRuntimeSemantics] :: CheckReportLanguage -> !Text
+ Keiro.Dsl.CheckReport: [reportSourceForm] :: CheckReportLanguage -> !Text
+ Keiro.Dsl.CheckReport: [reportStable] :: CheckReportLanguage -> !Bool
+ Keiro.Dsl.CheckReport: [summaryDeniedWarnings] :: CheckReportSummary -> !Int
+ Keiro.Dsl.CheckReport: [summaryErrors] :: CheckReportSummary -> !Int
+ Keiro.Dsl.CheckReport: [summaryWarnings] :: CheckReportSummary -> !Int
+ Keiro.Dsl.CheckReport: checkReport :: FilePath -> SourceLanguage -> EffectiveLanguageContract -> CheckReportEnforcement -> [Diagnostic] -> Set DiagnosticCode -> CheckReport
+ Keiro.Dsl.CheckReport: data CheckReport
+ Keiro.Dsl.CheckReport: data CheckReportEnforcement
+ Keiro.Dsl.CheckReport: data CheckReportEntry
+ Keiro.Dsl.CheckReport: data CheckReportLanguage
+ Keiro.Dsl.CheckReport: data CheckReportMember
+ Keiro.Dsl.CheckReport: data CheckReportRelated
+ Keiro.Dsl.CheckReport: data CheckReportSummary
+ Keiro.Dsl.CheckReport: effectiveDenyCodes :: CheckReportEnforcement -> Set DiagnosticCode
+ Keiro.Dsl.CheckReport: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.CheckReport.CheckReport
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReport
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportEnforcement
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportEntry
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportKind
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportLanguage
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportMember
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportRelated
+ Keiro.Dsl.CheckReport: instance GHC.Classes.Eq Keiro.Dsl.CheckReport.CheckReportSummary
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReport
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportEnforcement
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportEntry
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportKind
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportLanguage
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportMember
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportRelated
+ Keiro.Dsl.CheckReport: instance GHC.Internal.Show.Show Keiro.Dsl.CheckReport.CheckReportSummary
+ Keiro.Dsl.CheckReport: workspaceCheckReport :: FilePath -> WorkspaceSpec -> EffectiveLanguageContract -> CheckReportEnforcement -> [WorkspaceDiagnostic] -> Set DiagnosticCode -> CheckReport
+ Keiro.Dsl.CheckReport: workspaceRefusalReport :: FilePath -> CheckReportEnforcement -> NonEmpty WorkspaceDiagnostic -> Set DiagnosticCode -> CheckReport
+ Keiro.Dsl.ConformancePackage: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.ConformancePackage.ConformanceFileRow
+ Keiro.Dsl.ConformancePackage: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ConformancePackage.ConformanceFileRow
+ Keiro.Dsl.ConformancePackage: parseLegacyConformancePackageRecord :: Text -> Maybe ConformancePackageRecord
+ Keiro.Dsl.Coverage: coverageFindingMessage :: CoverageFinding -> Text
+ Keiro.Dsl.Diff: instance GHC.Classes.Eq Keiro.Dsl.Diff.EventFieldSig
+ Keiro.Dsl.Diff: instance GHC.Internal.Show.Show Keiro.Dsl.Diff.EventFieldSig
+ Keiro.Dsl.DiffReport: RemedyRescaffoldGenerated :: Remedy
+ Keiro.Dsl.Grammar: [aggregateFieldSelector] :: AggregateField -> !Maybe Name
+ Keiro.Dsl.Grammar: [aggregateFieldWireKey] :: AggregateField -> !Maybe Text
+ Keiro.Dsl.Grammar: [cfLoc] :: ContractField -> !Loc
+ Keiro.Dsl.Grammar: [cfSelector] :: ContractField -> !Maybe Name
+ Keiro.Dsl.Grammar: [cfWireKey] :: ContractField -> !Maybe Text
+ Keiro.Dsl.Grammar: [ideField] :: IdExpr -> !Name
+ Keiro.Dsl.HaskellSourceMove: AmbiguousLegacyModule :: !FilePath -> ![FilePath] -> SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: AmbiguousModuleRole :: !ModuleRole -> ![FilePath] -> SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: MalformedHaskellLexicalInput :: !Text -> SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: SourceMove :: !ModuleRole -> !ModuleKind -> !Text -> !Text -> !FilePath -> !FilePath -> !FilePath -> !Maybe Text -> !Maybe Text -> SourceMove
+ Keiro.Dsl.HaskellSourceMove: [moveBackupPath] :: SourceMove -> !FilePath
+ Keiro.Dsl.HaskellSourceMove: [moveContentDigest] :: SourceMove -> !Maybe Text
+ Keiro.Dsl.HaskellSourceMove: [moveKind] :: SourceMove -> !ModuleKind
+ Keiro.Dsl.HaskellSourceMove: [moveNewModule] :: SourceMove -> !Text
+ Keiro.Dsl.HaskellSourceMove: [moveNewPath] :: SourceMove -> !FilePath
+ Keiro.Dsl.HaskellSourceMove: [moveOldModule] :: SourceMove -> !Text
+ Keiro.Dsl.HaskellSourceMove: [moveOldPath] :: SourceMove -> !FilePath
+ Keiro.Dsl.HaskellSourceMove: [moveRole] :: SourceMove -> !ModuleRole
+ Keiro.Dsl.HaskellSourceMove: [moveTransformedDigest] :: SourceMove -> !Maybe Text
+ Keiro.Dsl.HaskellSourceMove: contentDigest :: Text -> Text
+ Keiro.Dsl.HaskellSourceMove: data SourceMove
+ Keiro.Dsl.HaskellSourceMove: data SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Classes.Eq Keiro.Dsl.HaskellSourceMove.LexState
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Classes.Eq Keiro.Dsl.HaskellSourceMove.SourceMove
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Classes.Eq Keiro.Dsl.HaskellSourceMove.SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Internal.Show.Show Keiro.Dsl.HaskellSourceMove.LexState
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Internal.Show.Show Keiro.Dsl.HaskellSourceMove.SourceMove
+ Keiro.Dsl.HaskellSourceMove: instance GHC.Internal.Show.Show Keiro.Dsl.HaskellSourceMove.SourceMoveError
+ Keiro.Dsl.HaskellSourceMove: moduleNameFromPath :: FilePath -> Text
+ Keiro.Dsl.HaskellSourceMove: planSourceMoves :: [(Maybe ModuleRole, ModuleKind, FilePath)] -> [ScaffoldModule] -> Either (NonEmpty SourceMoveError) [SourceMove]
+ Keiro.Dsl.HaskellSourceMove: rewriteHaskellModuleReferences :: Map Text Text -> Text -> Either SourceMoveError Text
+ Keiro.Dsl.LanguageVersion: FieldAliasSyntax :: LanguageFeature
+ Keiro.Dsl.Scaffold: ModuleRole :: !Text -> !Text -> !Text -> ModuleRole
+ Keiro.Dsl.Scaffold: [roleFamily] :: ModuleRole -> !Text
+ Keiro.Dsl.Scaffold: [roleOwnerKind] :: ModuleRole -> !Text
+ Keiro.Dsl.Scaffold: [roleOwnerName] :: ModuleRole -> !Text
+ Keiro.Dsl.Scaffold: data ModuleRole
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Eq Keiro.Dsl.Scaffold.ModuleRole
+ Keiro.Dsl.Scaffold: instance GHC.Classes.Ord Keiro.Dsl.Scaffold.ModuleRole
+ Keiro.Dsl.Scaffold: instance GHC.Internal.Show.Show Keiro.Dsl.Scaffold.ModuleRole
+ Keiro.Dsl.Scaffold: moduleRole :: ScaffoldModule -> ModuleRole
+ Keiro.Dsl.ScaffoldRecord: IdiomaticNamingV1 :: GeneratedHaskellNamingEdition
+ Keiro.Dsl.ScaffoldRecord: LegacyNamingV1 :: GeneratedHaskellNamingEdition
+ Keiro.Dsl.ScaffoldRecord: ScaffoldModuleRoleRow :: !ModuleRole -> !ModuleKind -> !FilePath -> ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRecord: [recModuleRoles] :: ScaffoldRecord -> ![ScaffoldModuleRoleRow]
+ Keiro.Dsl.ScaffoldRecord: [recNamingEdition] :: ScaffoldRecord -> !GeneratedHaskellNamingEdition
+ Keiro.Dsl.ScaffoldRecord: [srrKind] :: ScaffoldModuleRoleRow -> !ModuleKind
+ Keiro.Dsl.ScaffoldRecord: [srrPath] :: ScaffoldModuleRoleRow -> !FilePath
+ Keiro.Dsl.ScaffoldRecord: [srrRole] :: ScaffoldModuleRoleRow -> !ModuleRole
+ Keiro.Dsl.ScaffoldRecord: data GeneratedHaskellNamingEdition
+ Keiro.Dsl.ScaffoldRecord: data ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRecord: instance Data.Aeson.Types.FromJSON.FromJSON Keiro.Dsl.ScaffoldRecord.ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRecord: instance Data.Aeson.Types.ToJSON.ToJSON Keiro.Dsl.ScaffoldRecord.ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRecord: instance GHC.Classes.Eq Keiro.Dsl.ScaffoldRecord.ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRecord: instance GHC.Internal.Show.Show Keiro.Dsl.ScaffoldRecord.ScaffoldModuleRoleRow
+ Keiro.Dsl.ScaffoldRun: GeneratedNameInvariantViolation :: ![Text] -> Refusal
+ Keiro.Dsl.ScaffoldRun: NameMigrationRefusal :: ![Text] -> Refusal
+ Keiro.Dsl.ScaffoldRun: NameMigrationRequired :: ![SourceMove] -> Refusal
+ Keiro.Dsl.ScaffoldRun: SidecarMigrationRefusal :: ![Text] -> Refusal
+ Keiro.Dsl.ScaffoldRun: SidecarMigrationRequired :: ![SidecarMove] -> Refusal
+ Keiro.Dsl.ScaffoldRun: SidecarMovesAlreadyApplied :: ![SidecarMove] -> Refusal
+ Keiro.Dsl.ScaffoldRun: [reportInertNodes] :: ScaffoldReport -> ![(Text, Text)]
+ Keiro.Dsl.ScaffoldRun: [reportNameMoves] :: ScaffoldReport -> ![SourceMove]
+ Keiro.Dsl.ScaffoldRun: [reportSidecarMoves] :: ScaffoldReport -> ![SidecarMove]
+ Keiro.Dsl.ScaffoldRun: applyPreparedSourceMoves :: FilePath -> [PreparedSourceMove] -> IO ()
+ Keiro.Dsl.ScaffoldRun: auditGeneratedHaskell :: ScaffoldModule -> [Text]
+ Keiro.Dsl.ScaffoldRun: checkServiceDiagnostics :: Maybe RuntimePackageName -> Context -> CheckedService -> [Diagnostic]
+ Keiro.Dsl.ScaffoldRun: data PreparedSourceMove
+ Keiro.Dsl.ScaffoldRun: executeServiceScaffoldWithRuntimePackageAndNameMigrations :: Maybe RuntimePackageName -> Bool -> FilePath -> Bool -> FilePath -> SourceLanguage -> Context -> CheckedService -> [ScaffoldModule] -> IO (Either [Refusal] ScaffoldReport)
+ Keiro.Dsl.ScaffoldRun: inertNodesOf :: Spec -> [(Text, Text)]
+ Keiro.Dsl.ScaffoldRun: originLine :: Text -> Maybe Int
+ Keiro.Dsl.ScaffoldRun: planningGatePipeline :: Context -> CheckedService -> Either [Refusal] [ScaffoldModule] -> Either [Refusal] () -> Either [Refusal] [ScaffoldModule]
+ Keiro.Dsl.ScaffoldRun: planningRefusalDiagnostics :: [Refusal] -> [Diagnostic]
+ Keiro.Dsl.ScaffoldRun: preflightSourceMoves :: FilePath -> [SourceMove] -> IO (Either [Text] [PreparedSourceMove])
+ Keiro.Dsl.ScaffoldRun: preparedSourceMove :: PreparedSourceMove -> SourceMove
+ Keiro.Dsl.ScaffoldRun: renderInertNodeSection :: [(Text, Text)] -> [Text]
+ Keiro.Dsl.ScaffoldRun: withSidecarMovesApplied :: [SidecarMove] -> [Refusal] -> [Refusal]
+ Keiro.Dsl.SemanticContract: languageContractNotice :: FilePath -> Text -> EffectiveLanguageContract -> Maybe Text
+ Keiro.Dsl.SidecarMigration: ContextSidecars :: !Text -> SidecarScope
+ Keiro.Dsl.SidecarMigration: ConvertLegacyConformanceLedger :: SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: RenameSidecar :: SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: RetireLegacySidecar :: SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: SidecarMove :: !FilePath -> !FilePath -> !Maybe FilePath -> !SidecarMoveDisposition -> SidecarMove
+ Keiro.Dsl.SidecarMigration: WorkspaceSidecars :: !Text -> SidecarScope
+ Keiro.Dsl.SidecarMigration: [sidecarBackupPath] :: SidecarMove -> !Maybe FilePath
+ Keiro.Dsl.SidecarMigration: [sidecarMoveDisposition] :: SidecarMove -> !SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: [sidecarNewPath] :: SidecarMove -> !FilePath
+ Keiro.Dsl.SidecarMigration: [sidecarOldPath] :: SidecarMove -> !FilePath
+ Keiro.Dsl.SidecarMigration: applyPreparedSidecarMoves :: FilePath -> [PreparedSidecarMove] -> IO ()
+ Keiro.Dsl.SidecarMigration: data PreparedSidecarMove
+ Keiro.Dsl.SidecarMigration: data SidecarMove
+ Keiro.Dsl.SidecarMigration: data SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: data SidecarScope
+ Keiro.Dsl.SidecarMigration: instance GHC.Classes.Eq Keiro.Dsl.SidecarMigration.PreparedSidecarMove
+ Keiro.Dsl.SidecarMigration: instance GHC.Classes.Eq Keiro.Dsl.SidecarMigration.SidecarMove
+ Keiro.Dsl.SidecarMigration: instance GHC.Classes.Eq Keiro.Dsl.SidecarMigration.SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: instance GHC.Classes.Eq Keiro.Dsl.SidecarMigration.SidecarScope
+ Keiro.Dsl.SidecarMigration: instance GHC.Internal.Show.Show Keiro.Dsl.SidecarMigration.PreparedSidecarMove
+ Keiro.Dsl.SidecarMigration: instance GHC.Internal.Show.Show Keiro.Dsl.SidecarMigration.SidecarMove
+ Keiro.Dsl.SidecarMigration: instance GHC.Internal.Show.Show Keiro.Dsl.SidecarMigration.SidecarMoveDisposition
+ Keiro.Dsl.SidecarMigration: instance GHC.Internal.Show.Show Keiro.Dsl.SidecarMigration.SidecarScope
+ Keiro.Dsl.SidecarMigration: planSidecarMigrations :: FilePath -> SidecarScope -> Maybe ConformancePackagePlan -> IO (Either [Text] [PreparedSidecarMove])
+ Keiro.Dsl.SidecarMigration: preparedSidecarMove :: PreparedSidecarMove -> SidecarMove
+ Keiro.Dsl.SidecarMigration: renderSidecarMove :: SidecarMove -> Text
+ Keiro.Dsl.SidecarNames: conformanceLedgerFileName :: FilePath
+ Keiro.Dsl.SidecarNames: contextCabalFragmentFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: contextLedgerFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: legacyConformanceRecordFileName :: FilePath
+ Keiro.Dsl.SidecarNames: legacyContextManifestFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: legacyContextRecordFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: legacyWorkspaceManifestFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: legacyWorkspaceRecordFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: workspaceCabalFragmentFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: workspaceLedgerFileName :: Text -> FilePath
+ Keiro.Dsl.SidecarNames: workspaceMigrationReportFileName :: Text -> FilePath
+ Keiro.Dsl.Validate: AggProjectionKeyUnresolved :: DiagnosticCode
+ Keiro.Dsl.Validate: AggregateEmpty :: DiagnosticCode
+ Keiro.Dsl.Validate: BehaviorDerivationInvalid :: DiagnosticCode
+ Keiro.Dsl.Validate: CheckDiagnostic :: DiagnosticOrigin
+ Keiro.Dsl.Validate: CodecCompareDiagnostic :: DiagnosticOrigin
+ Keiro.Dsl.Validate: ConformanceFactKeyCollision :: DiagnosticCode
+ Keiro.Dsl.Validate: ContractEmpty :: DiagnosticCode
+ Keiro.Dsl.Validate: CoverageDiagnostic :: DiagnosticOrigin
+ Keiro.Dsl.Validate: DecodeBodyPostureUnsupported :: DiagnosticCode
+ Keiro.Dsl.Validate: DiffDiagnostic :: DiagnosticOrigin
+ Keiro.Dsl.Validate: DispatchOnAppendedUnsupported :: DiagnosticCode
+ Keiro.Dsl.Validate: EvtFieldWireKeyChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: FieldWireKeyCollision :: DiagnosticCode
+ Keiro.Dsl.Validate: FieldWireKeyInvalid :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedHaskellNameChanged :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedImportCycle :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedOccurrenceCollision :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedOccurrenceReserved :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedPathCollision :: DiagnosticCode
+ Keiro.Dsl.Validate: GeneratedPlanningInvariantViolation :: DiagnosticCode
+ Keiro.Dsl.Validate: IdentUnsafeNormalization :: DiagnosticCode
+ Keiro.Dsl.Validate: IntakeBindFlagUnenforced :: DiagnosticCode
+ Keiro.Dsl.Validate: IntakeBindHeaderUnknown :: DiagnosticCode
+ Keiro.Dsl.Validate: LanguageVersionBelowMinimum :: DiagnosticCode
+ Keiro.Dsl.Validate: PgmqFanoutFunctionInvalid :: DiagnosticCode
+ Keiro.Dsl.Validate: ProcessBindingUnscoped :: DiagnosticCode
+ Keiro.Dsl.Validate: ProcessDispatchKeyUnresolved :: DiagnosticCode
+ Keiro.Dsl.Validate: ProcessKeyFieldUnknown :: DiagnosticCode
+ Keiro.Dsl.Validate: PublisherOutboxFieldUnresolved :: DiagnosticCode
+ Keiro.Dsl.Validate: RmInlineSubscriptionIgnored :: DiagnosticCode
+ Keiro.Dsl.Validate: RouterBenignInversion :: DiagnosticCode
+ Keiro.Dsl.Validate: TimerDeadLetterTextInvalid :: DiagnosticCode
+ Keiro.Dsl.Validate: TimerDecodeStatusUnknown :: DiagnosticCode
+ Keiro.Dsl.Validate: TimerIdFieldNotCorrelation :: DiagnosticCode
+ Keiro.Dsl.Validate: TimerNotMineUnsupported :: DiagnosticCode
+ Keiro.Dsl.Validate: WindowOutOfRange :: DiagnosticCode
+ Keiro.Dsl.Validate: WqPayloadTypeUnknown :: DiagnosticCode
+ Keiro.Dsl.Validate: [relatedLocations] :: Diagnostic -> ![(Int, Text)]
+ Keiro.Dsl.Validate: canonicalEnvelopeHeaders :: [Text]
+ Keiro.Dsl.Validate: data DiagnosticOrigin
+ Keiro.Dsl.Validate: diagnosticCodeText :: DiagnosticCode -> Text
+ Keiro.Dsl.Validate: diagnosticOrigin :: DiagnosticCode -> DiagnosticOrigin
+ Keiro.Dsl.Validate: instance GHC.Classes.Eq Keiro.Dsl.Validate.DiagnosticOrigin
+ Keiro.Dsl.Validate: instance GHC.Classes.Ord Keiro.Dsl.Validate.DiagnosticCode
+ Keiro.Dsl.Validate: instance GHC.Classes.Ord Keiro.Dsl.Validate.DiagnosticOrigin
+ Keiro.Dsl.Validate: instance GHC.Internal.Enum.Bounded Keiro.Dsl.Validate.DiagnosticCode
+ Keiro.Dsl.Validate: instance GHC.Internal.Enum.Bounded Keiro.Dsl.Validate.DiagnosticOrigin
+ Keiro.Dsl.Validate: instance GHC.Internal.Enum.Enum Keiro.Dsl.Validate.DiagnosticCode
+ Keiro.Dsl.Validate: instance GHC.Internal.Enum.Enum Keiro.Dsl.Validate.DiagnosticOrigin
+ Keiro.Dsl.Validate: instance GHC.Internal.Show.Show Keiro.Dsl.Validate.DiagnosticOrigin
+ Keiro.Dsl.Validate: minimumLanguageDiagnostics :: LanguageVersion -> SourceLanguage -> [Diagnostic]
+ Keiro.Dsl.Validate: parseDiagnosticCode :: Text -> Maybe DiagnosticCode
+ Keiro.Dsl.Validate: runtimeTimerStatuses :: [Text]
+ Keiro.Dsl.WorkspaceRecord: [wrNamingEdition] :: WorkspaceRecord -> !GeneratedHaskellNamingEdition
+ Keiro.Dsl.WorkspaceRecord: [wrmRole] :: WorkspaceModuleRow -> !Maybe ModuleRole
+ Keiro.Dsl.WorkspaceScaffold: [wsrInertNodes] :: WorkspaceScaffoldReport -> ![(Text, Text)]
+ Keiro.Dsl.WorkspaceScaffold: [wsrNameMoves] :: WorkspaceScaffoldReport -> ![SourceMove]
+ Keiro.Dsl.WorkspaceScaffold: [wsrSidecarMoves] :: WorkspaceScaffoldReport -> ![SidecarMove]
+ Keiro.Dsl.WorkspaceScaffold: executeWorkspaceScaffoldWithNameMigrations :: FilePath -> Bool -> Bool -> WorkspacePlan -> IO (Either [Refusal] WorkspaceScaffoldReport)
- Keiro.Dsl.Grammar: AggregateField :: !Name -> !Maybe TypeExpr -> !Loc -> AggregateField
+ Keiro.Dsl.Grammar: AggregateField :: !Name -> !Maybe Name -> !Maybe Text -> !Maybe TypeExpr -> !Loc -> AggregateField
- Keiro.Dsl.Grammar: ContractField :: !Name -> !ContractType -> ContractField
+ Keiro.Dsl.Grammar: ContractField :: !Name -> !Maybe Name -> !Maybe Text -> !ContractType -> !Loc -> ContractField
- Keiro.Dsl.Grammar: IdExpr :: !IdStrategy -> !Text -> IdExpr
+ Keiro.Dsl.Grammar: IdExpr :: !IdStrategy -> !Text -> !Name -> IdExpr
- Keiro.Dsl.Grammar: WqField :: !Name -> !Text -> !Name -> !Bool -> WqField
+ Keiro.Dsl.Grammar: WqField :: !Name -> !Text -> !Name -> WqField
- Keiro.Dsl.Scaffold: ResolvedCtor :: !Text -> ![(Text, ResolvedAggregateType)] -> !Int -> !Maybe Int -> ResolvedCtor
+ Keiro.Dsl.Scaffold: ResolvedCtor :: !Text -> ![(ResolvedFieldIdentity, ResolvedAggregateType)] -> !Int -> !Maybe Int -> ResolvedCtor
- Keiro.Dsl.Scaffold: [rcFields] :: ResolvedCtor -> ![(Text, ResolvedAggregateType)]
+ Keiro.Dsl.Scaffold: [rcFields] :: ResolvedCtor -> ![(ResolvedFieldIdentity, ResolvedAggregateType)]
- Keiro.Dsl.ScaffoldRecord: ScaffoldRecord :: !Text -> !Text -> !Text -> !SourceLanguage -> !EffectiveLanguageContract -> ![(ModuleKind, FilePath)] -> ![MappingIdentity] -> ![Text] -> ![Text] -> ![BindingHole] -> ![BehaviorRecordRow] -> ScaffoldRecord
+ Keiro.Dsl.ScaffoldRecord: ScaffoldRecord :: !Text -> !Text -> !Text -> !SourceLanguage -> !EffectiveLanguageContract -> !GeneratedHaskellNamingEdition -> ![ScaffoldModuleRoleRow] -> ![(ModuleKind, FilePath)] -> ![MappingIdentity] -> ![Text] -> ![Text] -> ![BindingHole] -> ![BehaviorRecordRow] -> ScaffoldRecord
- Keiro.Dsl.ScaffoldRun: ScaffoldReport :: !FilePath -> !FilePath -> !Context -> ![(ScaffoldModule, WriteDisposition)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> !ConsumerPlan -> ![Text] -> ![MappingDrift] -> !Maybe SourceLanguageDrift -> ![BindingHole] -> ![BehaviorRecordRow] -> ![BehaviorRecordRow] -> ![(Text, Text)] -> !Maybe ConformancePackageReport -> ScaffoldReport
+ Keiro.Dsl.ScaffoldRun: ScaffoldReport :: !FilePath -> !FilePath -> !Context -> ![(ScaffoldModule, WriteDisposition)] -> ![(Text, Text)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> !ConsumerPlan -> ![Text] -> ![MappingDrift] -> !Maybe SourceLanguageDrift -> ![BindingHole] -> ![BehaviorRecordRow] -> ![BehaviorRecordRow] -> ![(Text, Text)] -> !Maybe ConformancePackageReport -> ![SourceMove] -> ![SidecarMove] -> ScaffoldReport
- Keiro.Dsl.Validate: Diagnostic :: !Int -> !Severity -> !DiagnosticCode -> !Text -> Diagnostic
+ Keiro.Dsl.Validate: Diagnostic :: !Int -> !Severity -> !DiagnosticCode -> ![(Int, Text)] -> !Text -> Diagnostic
- Keiro.Dsl.WorkspaceRecord: WorkspaceModuleRow :: !ModuleKind -> !FilePath -> !Maybe FilePath -> WorkspaceModuleRow
+ Keiro.Dsl.WorkspaceRecord: WorkspaceModuleRow :: !ModuleKind -> !FilePath -> !Maybe FilePath -> !Maybe ModuleRole -> WorkspaceModuleRow
- Keiro.Dsl.WorkspaceRecord: WorkspaceRecord :: !Text -> !Text -> !Text -> !Text -> !Text -> ![FilePath] -> ![WorkspaceSourceLanguageRow] -> !EffectiveLanguageContract -> ![WorkspaceModuleRow] -> ![MappingIdentity] -> ![Text] -> ![Text] -> ![BindingHole] -> ![BehaviorRecordRow] -> ![AdoptedRow] -> WorkspaceRecord
+ Keiro.Dsl.WorkspaceRecord: WorkspaceRecord :: !Text -> !Text -> !Text -> !Text -> !Text -> ![FilePath] -> ![WorkspaceSourceLanguageRow] -> !EffectiveLanguageContract -> !GeneratedHaskellNamingEdition -> ![WorkspaceModuleRow] -> ![MappingIdentity] -> ![Text] -> ![Text] -> ![BindingHole] -> ![BehaviorRecordRow] -> ![AdoptedRow] -> WorkspaceRecord
- Keiro.Dsl.WorkspaceScaffold: WorkspaceScaffoldReport :: !FilePath -> !FilePath -> !Text -> !Context -> ![FilePath] -> ![(ScaffoldModule, ModuleProvenance, WriteDisposition)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> ![OwnershipMove] -> !ConsumerPlan -> ![Text] -> ![MappingDrift] -> ![WorkspaceSourceLanguageDrift] -> ![BindingHole] -> ![BehaviorRecordRow] -> ![BehaviorRecordRow] -> ![(Text, Text)] -> !Maybe ConformancePackageReport -> !Maybe MigrationReport -> WorkspaceScaffoldReport
+ Keiro.Dsl.WorkspaceScaffold: WorkspaceScaffoldReport :: !FilePath -> !FilePath -> !Text -> !Context -> ![FilePath] -> ![(ScaffoldModule, ModuleProvenance, WriteDisposition)] -> !FilePath -> !FilePath -> !Maybe Text -> ![StaleModule] -> ![OwnershipMove] -> !ConsumerPlan -> ![Text] -> ![MappingDrift] -> ![WorkspaceSourceLanguageDrift] -> ![BindingHole] -> ![BehaviorRecordRow] -> ![BehaviorRecordRow] -> ![(Text, Text)] -> ![(Text, Text)] -> !Maybe ConformancePackageReport -> ![SourceMove] -> ![SidecarMove] -> !Maybe MigrationReport -> WorkspaceScaffoldReport
Files
- CHANGELOG.md +281/−1
- app/Main.hs +400/−48
- keiro-dsl.cabal +117/−52
- src/Keiro/Dsl/AggregateGenerationPlan.hs +42/−0
- src/Keiro/Dsl/AggregateType.hs +14/−8
- src/Keiro/Dsl/CheckReport.hs +341/−0
- src/Keiro/Dsl/ConformancePackage.hs +96/−16
- src/Keiro/Dsl/Coverage.hs +12/−3
- src/Keiro/Dsl/Diff.hs +265/−61
- src/Keiro/Dsl/DiffReport.hs +4/−0
- src/Keiro/Dsl/ExplainBindings.hs +12/−5
- src/Keiro/Dsl/FieldIdentity.hs +41/−0
- src/Keiro/Dsl/Goldens.hs +2/−1
- src/Keiro/Dsl/Grammar.hs +15/−7
- src/Keiro/Dsl/Harness.hs +218/−108
- src/Keiro/Dsl/HaskellImport.hs +1/−39
- src/Keiro/Dsl/HaskellName.hs +351/−0
- src/Keiro/Dsl/HaskellSourceMove.hs +214/−0
- src/Keiro/Dsl/LanguageVersion.hs +11/−1
- src/Keiro/Dsl/MappedDiff.hs +34/−3
- src/Keiro/Dsl/NominalType.hs +1/−30
- src/Keiro/Dsl/Parser/Aggregate.hs +10/−1
- src/Keiro/Dsl/Parser/Coordination.hs +25/−22
- src/Keiro/Dsl/Parser/Document.hs +1/−1
- src/Keiro/Dsl/Parser/Integration.hs +8/−3
- src/Keiro/Dsl/Parser/Queue.hs +5/−2
- src/Keiro/Dsl/PrettyPrint.hs +19/−6
- src/Keiro/Dsl/ReplayImpact.hs +12/−5
- src/Keiro/Dsl/Scaffold.hs +706/−189
- src/Keiro/Dsl/ScaffoldRecord.hs +69/−3
- src/Keiro/Dsl/ScaffoldRun.hs +693/−63
- src/Keiro/Dsl/SemanticContract.hs +28/−1
- src/Keiro/Dsl/ServiceHarness.hs +23/−11
- src/Keiro/Dsl/SidecarMigration.hs +270/−0
- src/Keiro/Dsl/SidecarNames.hs +56/−0
- src/Keiro/Dsl/Skeleton.hs +12/−9
- src/Keiro/Dsl/Validate.hs +3889/−2897
- src/Keiro/Dsl/Workspace.hs +29/−16
- src/Keiro/Dsl/WorkspaceAdoption.hs +28/−14
- src/Keiro/Dsl/WorkspaceRecord.hs +41/−21
- src/Keiro/Dsl/WorkspaceScaffold.hs +113/−16
- test/Keiro/Dsl/FrontendCompatibility.hs +2/−0
- test/Keiro/Dsl/FrontendProfiles.hs +23/−14
- test/Main.hs +10703/−8590
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ReplayAudit.hs +1/−1
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Codec.hs +42/−7
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Domain.hs +20/−2
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/EventStream.hs +1/−1
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Harness.hs +13/−6
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Projection.hs +3/−1
- test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Transducer.hs +1/−1
- test/conformance-behavior-complete/BehaviorComplete/Journey/BehaviorHoles.hs +13/−2
- test/conformance-behavior-complete/BehaviorComplete/Journey/Holes.hs +3/−2
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/BehaviorContract.hs +313/−35
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Codec.hs +25/−10
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Domain.hs +15/−5
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/EventStream.hs +1/−1
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Harness.hs +23/−10
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Projection.hs +3/−1
- test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Transducer.hs +29/−6
- test/conformance-behavior-complete/Generated/BehaviorComplete/Nominals.hs +1/−8
- test/conformance-behavior-complete/Generated/BehaviorComplete/Nominals/Internal.hs +4/−1
- test/conformance-behavior-complete/Generated/BehaviorComplete/ReplayAudit.hs +1/−1
- test/conformance-behavior-complete/Generated/BehaviorComplete/Structural/Shape/StartPayload.hs +1/−1
- test/conformance-behavior-complete/Generated/BehaviorComplete/StructuralProjections.hs +1/−3
- test/conformance-coldstart/Generated/Billing/Nominals.hs +4/−7
- test/conformance-coldstart/Generated/Billing/Nominals/Internal.hs +6/−1
- test/conformance-coldstart/Generated/Billing/ReplayAudit.hs +1/−1
- test/conformance-coldstart/Generated/Billing/Subscription/Codec.hs +5/−5
- test/conformance-coldstart/Generated/Billing/Subscription/Domain.hs +2/−3
- test/conformance-coldstart/Generated/Billing/Subscription/EventStream.hs +2/−2
- test/conformance-coldstart/Generated/Billing/Subscription/Harness.hs +18/−6
- test/conformance-coldstart/Generated/Billing/Subscription/Projection.hs +1/−1
- test/conformance-coldstart/Generated/Billing/Subscription/Transducer.hs +1/−1
- test/conformance-contract-v1-compat/Generated/HospitalCapacity/Emergency/Contract.hs +1/−1
- test/conformance-contract/Generated/HospitalCapacity/Emergency/Contract.hs +8/−4
- test/conformance-contract/Main.hs +12/−2
- test/conformance-dispatch-full/Generated/HospitalCapacity/ReservationWork/Queue.hs +50/−0
- test/conformance-dispatch-full/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs +29/−0
- test/conformance-dispatch-full/Generated/HospitalCapacity/ReservationWork/QueuePolicy.hs +43/−0
- test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/Queue.hs +0/−50
- test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs +0/−29
- test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/QueuePolicy.hs +0/−43
- test/conformance-dispatch-full/HospitalCapacity/ReservationWork/WorkqueueJob.hs +3/−3
- test/conformance-id-domain-migration/Generated/IdDomainMigration/Nominals.hs +1/−8
- test/conformance-id-domain-migration/Generated/IdDomainMigration/Nominals/Internal.hs +4/−1
- test/conformance-id-domain-migration/Generated/IdDomainMigration/OrderBook/Codec.hs +7/−7
- test/conformance-id-domain-migration/Generated/IdDomainMigration/OrderBook/Domain.hs +1/−2
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/BehaviorContract.hs +92/−24
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Codec.hs +5/−10
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Domain.hs +1/−3
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/EventStream.hs +2/−2
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Harness.hs +6/−18
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Projection.hs +3/−1
- test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Transducer.hs +1/−1
- test/conformance-import-planning/Generated/ImportPlanningCollisions/NominalProjections.hs +3/−3
- test/conformance-import-planning/Generated/ImportPlanningCollisions/ReplayAudit.hs +1/−1
- test/conformance-import-planning/Generated/ImportPlanningCollisions/Structural/Shape/Details.hs +1/−1
- test/conformance-import-planning/Generated/ImportPlanningCollisions/StructuralProjections.hs +1/−3
- test/conformance-intake-full/Generated/HospitalCapacity/IncidentInbox/Inbox.hs +1/−1
- test/conformance-intake-runtime/Generated/HospitalCapacity/IncidentInbox/Inbox.hs +1/−1
- test/conformance-newsurface/Generated/TransferRouting/Conformance.hs +9/−9
- test/conformance-newsurface/Generated/TransferRouting/Hospital/Codec.hs +6/−6
- test/conformance-newsurface/Generated/TransferRouting/Hospital/Domain.hs +1/−2
- test/conformance-newsurface/Generated/TransferRouting/Hospital/EventStream.hs +2/−2
- test/conformance-newsurface/Generated/TransferRouting/Hospital/Harness.hs +5/−5
- test/conformance-newsurface/Generated/TransferRouting/Hospital/Projection.hs +3/−1
- test/conformance-newsurface/Generated/TransferRouting/Hospital/Transducer.hs +1/−1
- test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModel.hs +61/−0
- test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModelHarness.hs +33/−0
- test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModelTable.hs +9/−0
- test/conformance-newsurface/Generated/TransferRouting/HospitalTransferRouter/Router.hs +1/−1
- test/conformance-newsurface/Generated/TransferRouting/HospitalTransferRouter/RouterHarness.hs +1/−1
- test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModel.hs +0/−61
- test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModelHarness.hs +0/−23
- test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModelTable.hs +0/−9
- test/conformance-newsurface/Generated/TransferRouting/ReplayAudit.hs +1/−1
- test/conformance-newsurface/Main.hs +1/−1
- test/conformance-newsurface/TransferRouting/HospitalLoad/ReadModelHoles.hs +35/−0
- test/conformance-newsurface/TransferRouting/HospitalTransferRouter/RouterValue.hs +1/−1
- test/conformance-newsurface/TransferRouting/Hospital_load/ReadModelHoles.hs +0/−35
- test/conformance-nominal-scalars/Generated/NominalScalars/Nominal/Shape/OrderStatus.hs +1/−1
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/BehaviorContract.hs +91/−23
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Codec.hs +5/−5
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Domain.hs +1/−2
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/EventStream.hs +1/−1
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Harness.hs +5/−6
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Projection.hs +3/−1
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Transducer.hs +2/−2
- test/conformance-nominal-scalars/Generated/NominalScalars/NominalProjections.hs +4/−4
- test/conformance-nominal-scalars/Generated/NominalScalars/ReplayAudit.hs +1/−1
- test/conformance-nominal-scalars/Main.hs +27/−2
- test/conformance-process-full/Generated/SurgeDemo/Hospital/Codec.hs +7/−7
- test/conformance-process-full/Generated/SurgeDemo/Hospital/Domain.hs +2/−4
- test/conformance-process-full/Generated/SurgeDemo/Hospital/EventStream.hs +2/−2
- test/conformance-process-full/Generated/SurgeDemo/Hospital/Projection.hs +1/−1
- test/conformance-process-full/Generated/SurgeDemo/Hospital/Transducer.hs +1/−1
- test/conformance-process-full/Generated/SurgeDemo/Nominals.hs +1/−8
- test/conformance-process-full/Generated/SurgeDemo/Nominals/Internal.hs +4/−1
- test/conformance-process-full/Generated/SurgeDemo/ReplayAudit.hs +1/−1
- test/conformance-process-full/Generated/SurgeDemo/Surge/Codec.hs +7/−7
- test/conformance-process-full/Generated/SurgeDemo/Surge/Domain.hs +2/−4
- test/conformance-process-full/Generated/SurgeDemo/Surge/EventStream.hs +2/−2
- test/conformance-process-full/Generated/SurgeDemo/Surge/Projection.hs +1/−1
- test/conformance-process-full/Generated/SurgeDemo/Surge/Transducer.hs +1/−1
- test/conformance-process-full/Generated/SurgeDemo/SurgeFlow/Process.hs +1/−1
- test/conformance-process-runtime/Generated/HospitalCapacity/HospitalSurge/Process.hs +1/−1
- test/conformance-process-runtime/Generated/HospitalCapacity/Nominals.hs +1/−10
- test/conformance-process-runtime/Generated/HospitalCapacity/Nominals/Internal.hs +6/−1
- test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Codec.hs +7/−7
- test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Domain.hs +2/−3
- test/conformance-process-runtime/Generated/HospitalCapacity/Surge/EventStream.hs +2/−2
- test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Transducer.hs +1/−1
- test/conformance-process-runtime/HospitalCapacity/Surge/Holes.hs +1/−2
- test/conformance-process/Generated/HospitalCapacity/HospitalSurge/ProcessHarness.hs +1/−1
- test/conformance-publisher-runtime/Generated/HospitalCapacity/HospitalPublisher/Publisher.hs +2/−2
- test/conformance-queue-runtime/Generated/HospitalCapacity/ReservationWork/Queue.hs +50/−0
- test/conformance-queue-runtime/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs +29/−0
- test/conformance-queue-runtime/Generated/HospitalCapacity/ReservationWork/QueuePolicy.hs +43/−0
- test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/Queue.hs +0/−50
- test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs +0/−29
- test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/QueuePolicy.hs +0/−43
- test/conformance-queue-runtime/Main.hs +3/−3
- test/conformance-queue/Generated/HospitalCapacity/ReservationWork/Queue.hs +50/−0
- test/conformance-queue/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs +29/−0
- test/conformance-queue/Generated/HospitalCapacity/Reservation_work/Queue.hs +0/−50
- test/conformance-queue/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs +0/−29
- test/conformance-queue/Main.hs +3/−3
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModel.hs +61/−0
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModelHarness.hs +33/−0
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModelTable.hs +9/−0
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModel.hs +0/−61
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModelHarness.hs +0/−19
- test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModelTable.hs +0/−9
- test/conformance-readmodel-runtime/HospitalCapacity/TransferDecisions/ReadModelHoles.hs +30/−0
- test/conformance-readmodel-runtime/HospitalCapacity/Transfer_decisions/ReadModelHoles.hs +0/−30
- test/conformance-readmodel-runtime/Main.hs +2/−2
- test/conformance-replay/Generated/ReplayDivergence/Note/Codec.hs +6/−6
- test/conformance-replay/Generated/ReplayDivergence/Note/Domain.hs +1/−1
- test/conformance-replay/Generated/ReplayDivergence/Note/EventStream.hs +2/−2
- test/conformance-replay/Generated/ReplayDivergence/Note/Harness.hs +5/−5
- test/conformance-replay/Generated/ReplayDivergence/Note/Projection.hs +3/−1
- test/conformance-replay/Generated/ReplayDivergence/Note/Transducer.hs +1/−1
- test/conformance-replay/Generated/ReplayDivergence/ReplayAudit.hs +1/−1
- test/conformance-replay/ReplayDivergence/Note/Holes.hs +5/−4
- test/conformance-router-full/Generated/IncidentPaging/Page/Codec.hs +6/−6
- test/conformance-router-full/Generated/IncidentPaging/Page/Domain.hs +1/−2
- test/conformance-router-full/Generated/IncidentPaging/Page/EventStream.hs +2/−2
- test/conformance-router-full/Generated/IncidentPaging/Page/Transducer.hs +1/−1
- test/conformance-router-full/Generated/IncidentPaging/PagingRouter/Router.hs +1/−1
- test/conformance-router-full/Generated/IncidentPaging/PagingRouter/RouterHarness.hs +1/−1
- test/conformance-router-full/Generated/IncidentPaging/ReplayAudit.hs +1/−1
- test/conformance-router-runtime/Generated/IncidentPaging/PagingRouter/Router.hs +1/−1
- test/conformance-router-runtime/Generated/IncidentPaging/PagingRouter/RouterHarness.hs +1/−1
- test/conformance-router/Generated/IncidentPaging/PagingRouter/RouterHarness.hs +1/−1
- test/conformance-scalar-expressions/AggregateScalarExpressions/ScalarAccount/Holes.hs +1/−2
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Nominals.hs +3/−4
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Nominals/Internal.hs +4/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ReplayAudit.hs +1/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/BehaviorContract.hs +143/−27
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Codec.hs +6/−8
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Domain.hs +2/−2
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/EventStream.hs +1/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Harness.hs +18/−18
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Projection.hs +3/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Transducer.hs +1/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Structural/Shape/Limits.hs +1/−1
- test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/StructuralProjections.hs +1/−3
- test/conformance-scalar-expressions/Main.hs +4/−2
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Nominals.hs +1/−8
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Nominals/Internal.hs +4/−1
- test/conformance-skeletons/SkelAggregate/Generated/MyService/ReplayAudit.hs +1/−1
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/BehaviorContract.hs +91/−23
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Codec.hs +7/−7
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Domain.hs +1/−2
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/EventStream.hs +2/−2
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Harness.hs +11/−5
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Projection.hs +3/−1
- test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Transducer.hs +1/−1
- test/conformance-skeletons/SkelAggregate/MyService/Thing/BehaviorHoles.hs +2/−2
- test/conformance-skeletons/SkelContract/Generated/MyService/MyContract/Contract.hs +1/−1
- test/conformance-skeletons/SkelEmit/Generated/MyService/MyContract/Contract.hs +2/−2
- test/conformance-skeletons/SkelEmit/Generated/MyService/ThingPublisher/Publisher.hs +2/−2
- test/conformance-skeletons/SkelIntake/Generated/MyService/MyContract/Contract.hs +2/−2
- test/conformance-skeletons/SkelIntake/Generated/MyService/ThingInbox/Inbox.hs +1/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Codec.hs +7/−7
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Domain.hs +2/−4
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/EventStream.hs +2/−2
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Harness.hs +11/−5
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Projection.hs +3/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Transducer.hs +1/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/HospitalSurge/Process.hs +1/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/HospitalSurge/ProcessHarness.hs +1/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/Nominals.hs +1/−10
- test/conformance-skeletons/SkelProcess/Generated/MyService/Nominals/Internal.hs +6/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/ReplayAudit.hs +1/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Codec.hs +7/−7
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Domain.hs +2/−3
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/EventStream.hs +2/−2
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Harness.hs +14/−8
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Projection.hs +3/−1
- test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Transducer.hs +1/−1
- test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModel.hs +61/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModelHarness.hs +33/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModelTable.hs +9/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModel.hs +0/−61
- test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModelHarness.hs +0/−23
- test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModelTable.hs +0/−9
- test/conformance-skeletons/SkelQueue/Generated/MyService/ReservationWork/Queue.hs +40/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/ReservationWork/QueueCodec.hs +29/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/ReservationWork/QueuePolicy.hs +43/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/Queue.hs +0/−40
- test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/QueueCodec.hs +0/−29
- test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/QueuePolicy.hs +0/−43
- test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModel.hs +61/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModelHarness.hs +33/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModelTable.hs +9/−0
- test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModel.hs +0/−61
- test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModelHarness.hs +0/−23
- test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModelTable.hs +0/−9
- test/conformance-skeletons/SkelQueue/MyService/AcceptedTransferNeeds/ReadModelHoles.hs +26/−0
- test/conformance-skeletons/SkelQueue/MyService/Accepted_transfer_needs/ReadModelHoles.hs +0/−26
- test/conformance-skeletons/SkelQueue/MyService/TransferDecisions/ReadModelHoles.hs +25/−0
- test/conformance-skeletons/SkelQueue/MyService/Transfer_decisions/ReadModelHoles.hs +0/−25
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Codec.hs +6/−6
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Domain.hs +1/−2
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/EventStream.hs +2/−2
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Harness.hs +5/−5
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Projection.hs +3/−1
- test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Transducer.hs +1/−1
- test/conformance-skeletons/SkelRouter/Generated/MyService/PagingRouter/Router.hs +1/−1
- test/conformance-skeletons/SkelRouter/Generated/MyService/PagingRouter/RouterHarness.hs +1/−1
- test/conformance-skeletons/SkelRouter/Generated/MyService/ReplayAudit.hs +1/−1
- test/conformance-skeletons/SkelWorkflow/Generated/MyService/HospitalTransferReservation/WorkflowFacts.hs +1/−1
- test/conformance-skeletons/SkelWorkflow/Generated/MyService/HospitalTransferReservation/WorkflowRuntime.hs +1/−1
- test/conformance-snapshot/Generated/HospitalCapacity/Nominals.hs +5/−8
- test/conformance-snapshot/Generated/HospitalCapacity/Nominals/Internal.hs +8/−1
- test/conformance-snapshot/Generated/HospitalCapacity/ReplayAudit.hs +1/−1
- test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Codec.hs +5/−5
- test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Domain.hs +2/−3
- test/conformance-snapshot/Generated/HospitalCapacity/Reservation/EventStream.hs +1/−1
- test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Transducer.hs +1/−1
- test/conformance-snapshot/HospitalCapacity/Reservation/Holes.hs +3/−3
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/BehaviorContract.hs +348/−0
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Codec.hs +15/−11
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Domain.hs +1/−2
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/EventStream.hs +2/−2
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Harness.hs +7/−7
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Projection.hs +3/−1
- test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Transducer.hs +1/−2
- test/conformance-structural/Generated/StructuralConformance/ReplayAudit.hs +1/−1
- test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactInfo.hs +1/−1
- test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactKind.hs +1/−1
- test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactLocation.hs +1/−1
- test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactMetadata.hs +1/−1
- test/conformance-structural/Generated/StructuralConformance/StructuralProjections.hs +1/−3
- test/conformance-v2/Generated/HospitalCapacity/Nominals.hs +5/−8
- test/conformance-v2/Generated/HospitalCapacity/Nominals/Internal.hs +8/−1
- test/conformance-v2/Generated/HospitalCapacity/ReplayAudit.hs +1/−1
- test/conformance-v2/Generated/HospitalCapacity/Reservation/Codec.hs +5/−5
- test/conformance-v2/Generated/HospitalCapacity/Reservation/Domain.hs +2/−2
- test/conformance-v2/Generated/HospitalCapacity/Reservation/EventStream.hs +2/−2
- test/conformance-v2/Generated/HospitalCapacity/Reservation/Harness.hs +24/−6
- test/conformance-v2/Generated/HospitalCapacity/Reservation/Projection.hs +9/−9
- test/conformance-v2/Generated/HospitalCapacity/Reservation/Transducer.hs +1/−1
- test/conformance-v2/HospitalCapacity/Reservation/Holes.hs +3/−3
- test/conformance-workflow-full/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowRuntime.hs +1/−1
- test/conformance-workflow-runtime/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowRuntime.hs +1/−1
- test/conformance-workflow/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowFacts.hs +13/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Nominals.hs +3/−4
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Nominals/Internal.hs +4/−1
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/BehaviorContract.hs +143/−27
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Codec.hs +5/−5
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Domain.hs +1/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/EventStream.hs +2/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Harness.hs +12/−6
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Projection.hs +3/−1
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Transducer.hs +2/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/BehaviorContract.hs +91/−23
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Codec.hs +5/−5
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Domain.hs +1/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/EventStream.hs +2/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Harness.hs +11/−5
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Projection.hs +3/−1
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Transducer.hs +2/−2
- test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ReplayAudit.hs +1/−1
- test/conformance/Generated/HospitalCapacity/Nominals.hs +5/−8
- test/conformance/Generated/HospitalCapacity/Nominals/Internal.hs +8/−1
- test/conformance/Generated/HospitalCapacity/ReplayAudit.hs +1/−1
- test/conformance/Generated/HospitalCapacity/Reservation/Codec.hs +5/−5
- test/conformance/Generated/HospitalCapacity/Reservation/Domain.hs +2/−3
- test/conformance/Generated/HospitalCapacity/Reservation/EventStream.hs +2/−2
- test/conformance/Generated/HospitalCapacity/Reservation/Harness.hs +24/−6
- test/conformance/Generated/HospitalCapacity/Reservation/Projection.hs +9/−9
- test/conformance/Generated/HospitalCapacity/Reservation/Transducer.hs +1/−1
- test/conformance/HospitalCapacity/Reservation/Holes.hs +3/−3
- test/haskell-name/Main.hs +149/−0
- test/runtime-vocabulary/Main.hs +32/−0
CHANGELOG.md view
@@ -4,7 +4,287 @@ [Keep a Changelog](https://keepachangelog.com/), and the package follows the [Haskell Package Versioning Policy](https://pvp.haskell.org/). -## [Unreleased]+## [0.11.0.0] - 2026-08-05++### Breaking Changes++- `DiagnosticCode` gains `DecodeBodyPostureUnsupported`,+ `DispatchOnAppendedUnsupported`, `TimerNotMineUnsupported`, and+ `IntakeBindHeaderUnknown`; exhaustive matches must be extended. Each closes a+ spelling the grammar accepted that no runtime implements, and each warns on+ released languages below 4 and errors from language 4 on. `intake … body+ lenient` is refused because generated codecs decode a body strictly;+ `on-appended` other than `AckOk` is refused on both process and router+ dispatch rows because a successful append is always acked; `not-mine Fired` is+ refused because the timer worker marks a timer `Fired` only when the fire+ action returns an appended event id; and an intake `bind … from header "x"`+ naming a header outside keiro's canonical envelope set is refused because the+ Kafka inbox reads a fixed header set and cannot be remapped. The spelling that+ matches the runtime stays accepted and silent in every case.+- A process `dispatch-id` line is now checked as strictly as a router's. It must+ read exactly `strategy=uuidv5 from=(name, correlationId, sourceEventId,+ emitIndex)` — the tuple `Keiro.ProcessManager` actually derives from — where+ previously any strategy identifier and any tuple parsed and were discarded.+ Routers keep their own `(name, key, sourceEventId, targetStreamName,+ occurrence)`. A process spec that wrote anything else is now a parse error.+- `check`'s warning policy now covers structural coverage. When an invocation+ supplies `--coverage-report`, its findings are ordinary diagnostics of this+ run: `--deny-warnings` and `--deny CoverageOpaqueSurface` escalate them, they+ appear in the `keiro-dsl/check-report/1` diagnostics array as line-0 entries,+ and the report's `ok` therefore accounts for them. Previously+ `check spec --coverage-report r --deny-warnings` printed coverage warnings and+ exited 0, so a CI job that believed it had a no-warning gate did not have one.+ Coverage output is now emitted with the other diagnostics, before `--emit` and+ `--explain-bindings` output rather than after.+- `check --deny CODE` now refuses a code `check` cannot emit instead of+ accepting a denial that can never match. Cross-revision codes (for example+ `EvtFieldWireKeyChanged`, `WorkflowShapeChanged`) are a `diff` concern and are+ rejected outright; structural-coverage codes are rejected unless the same+ invocation passes `--coverage-report`; `--deny CoverageOpaqueGateExceeded` is+ rejected in every invocation, because that code is the error `--fail-on-opaque`+ itself raises and never a deniable warning — pass the flag instead. Any CI+ file that named such a code was not gating on it and now fails loudly.+ `Keiro.Dsl.Validate` exports the underlying `DiagnosticOrigin` and+ `diagnosticOrigin` registry.+- `keiro-dsl new intake` no longer spells the unenforced `required cross-check+ body` flags on its bind row, so the intake skeleton passes the documented+ `check --min-language 4 --deny-warnings` CI gate as generated (previously it+ warned `IntakeBindFlagUnenforced` out of the box). The router and process+ skeletons keep their idiomatic benign-inversion spellings and therefore gate+ CI with a selective `--deny` list rather than `--deny-warnings`.+- The `keiro-dsl/coverage-report/1` JSON now spells warning severity `"warning"`+ rather than `"advisory"`, matching `keiro-dsl/check-report/1`. There is one+ severity vocabulary across keiro-dsl's JSON. Consumers matching the literal+ string `"advisory"` (available since 0.4.0.0) must be updated.+- `DiagnosticCode` loses `EmitDeriveHoleUnrealized` and+ `WqFieldOptionalUnsupported`, and gains `TimerDecodeStatusUnknown`,+ `TimerDeadLetterTextInvalid`, and `PgmqFanoutFunctionInvalid`; exhaustive+ matches must be extended. The code fired unconditionally on every `emit` node because+ `derive … hole` is mandatory grammar, so it carried no information and made+ `check --deny-warnings` permanently red for any emit-bearing service. The+ scaffold report's no-modules line already names each emit node that+ contributes nothing. `WqFieldOptionalUnsupported` is removed with the model it+ described: every workqueue payload field is required, because generated+ decoders read all of them with `o .:`. `WqField` therefore loses `wqfRequired`,+ the `required` marker is accepted but selects nothing, and adding a payload+ field is now classified breaking however it is spelled — a job already queued+ under the old shape does not contain it, which the previous "new optional+ field is additive" classification denied.+- The three surfaces ExecPlan 197 recorded as descriptive-only are now checked+ against the references they name. A timer's `decode unknown-status` must be a+ status the timer table stores (`Scheduled`, `Firing`, `Fired`, `Cancelled`,+ `Dead`); its `dead-letter` reason must not be blank, since it is the text an+ operator-written worker passes to `Keiro.Timer.deadLetterTimer`; a pgmq+ `dedup key` must be a generated selector of its source read model, reported+ with the existing `DispatchReadModelFieldUnknown`; and a pgmq `fanout body`+ must be spellable as a Haskell value identifier. Emit `source`, `key`, and map+ discriminant names remain the one documented descriptive-only surface, because+ no typed source namespace exists to resolve them against.+- `WorkspaceScaffoldReport` gains `wsrInertNodes`, and a workspace scaffold now+ renders the same `no-modules:` line the single-spec report has always+ rendered. `Keiro.Dsl.ScaffoldRun` exports the shared `inertNodesOf` and+ `renderInertNodeSection`.+- `Keiro.Timer.Schema.TimerStatus` (in **keiro**) additionally derives `Enum` and+ `Bounded` so consumers can enumerate the lifecycle rather than restate it.+- `Keiro.Dsl.ScaffoldRun`'s `Refusal` gains `SidecarMovesAlreadyApplied`;+ exhaustive matches must be extended. Sidecar renames are applied before the+ later scaffold gates run, so a refusal after that point used to claim "nothing+ was written" while the renames were on disk. The note is appended to such a+ refusal set and states that the renames are idempotent and need no undo.+- A legacy `keiro-dsl-conformance-record.txt` anywhere in the output tree is now+ migrated, not only when the run also plans a conformance package. A spec that+ stopped generating one previously left the record orphaned and unreadable,+ because the current reader has no legacy parser. The scan is depth-bounded and+ skips the `.keiro-dsl-name-migrations` backup root.+- `FieldWireKeyInvalid` additionally refuses a wire key with leading or trailing+ whitespace or a control character, naming the offending codepoint. Aliases stay+ exempt from a declared `wire … fields=camelCase` convention — preserving a+ brownfield key is what they are for — so this is structural usability only. A+ spec that shipped `as "family "` now fails `check`.+- `Keiro.Dsl.CheckReport`'s `reportLanguage` is now+ `Maybe CheckReportLanguage`, and the new `workspaceRefusalReport` builds the+ report for a workspace refused during composition. Such a report serializes+ `"language": null` because no service graph was composed.+- Scaffold sidecars now use role-bearing names. On disk,+ `keiro-dsl-scaffold-record.<context>.txt` becomes+ `keiro-dsl-ledger.context.<context>.txt`, the workspace form becomes+ `keiro-dsl-ledger.workspace.<service>.txt`, `keiro-dsl-conformance-record.txt`+ becomes `keiro-dsl-conformance-ledger.txt`, and the generated+ `keiro-dsl-manifest.<context>.<ext>` / `keiro-dsl-manifest.workspace.*` files+ become `keiro-dsl-cabal-fragment.context.<context>.<ext>` and+ `keiro-dsl-cabal-fragment.workspace.<service>.<ext>`. The Cabal fragment — not+ the former manifest name — is the authoritative generated build inventory.+ An out tree holding legacy names now **refuses without writing**; review the+ listed moves and rerun `scaffold --apply-name-migrations` to rename them+ losslessly. Duplicate old files that cannot be renamed are preserved under+ `.keiro-dsl-name-migrations/sidecar-v1/`.+- `Keiro.Dsl.ScaffoldRun`'s `Refusal` gains `SidecarMigrationRequired` and+ `SidecarMigrationRefusal`; `ScaffoldReport` gains `reportSidecarMoves` and+ `WorkspaceScaffoldReport` gains `wsrSidecarMoves`. Exhaustive matches and+ record constructions must be extended. Two new modules are exposed:+ `Keiro.Dsl.SidecarNames` (the role-bearing name vocabulary and legacy-name+ recognition) and `Keiro.Dsl.SidecarMigration` (the planned move set and its+ apply path).+- The renamed conformance ledger also changes format: the whitespace-row+ `keiro-dsl-conformance-record.txt` becomes the versioned+ `keiro-dsl conformance ledger v1` with typed JSON file rows. The parser+ ignores unknown row kinds and unknown JSON keys so future rows decode, while+ retaining the service-key, malformed-record, unsafe-path, and case-folded+ duplicate-path refusals. Legacy conformance records are converted **only** by+ the explicit `--apply-name-migrations` path; no implicit upgrade exists.+- `AggregateField` gains `aggregateFieldSelector :: Maybe Name` and+ `aggregateFieldWireKey :: Maybe Text`, and `ContractField` gains+ `cfSelector :: Maybe Name`, `cfWireKey :: Maybe Text`, and `cfLoc :: Loc`+ (both in `Keiro.Dsl.Grammar`). Code that constructs these records positionally+ or matches them exhaustively must be updated; `Nothing` selector and wire-key+ values reproduce the pre-alias behavior exactly.+- `RouterReadModelUnverified` previously existed as a declared but never-emitted+ `DiagnosticCode` constructor. It now carries real semantics and is emitted+ when a router's resolved read model cannot be verified, so a consumer that+ matched the constructor as unreachable — or a `--deny` list that named it as a+ no-op — now sees live diagnostics.+- Router duplicate-disposition notices moved from `ProcessBenignInversion` to+ the distinct `RouterBenignInversion`. An adopter CI list that passes+ `--deny ProcessBenignInversion` silently stops matching router rows; add+ `--deny RouterBenignInversion` to preserve the previous gate.+- Requires `keiki >=0.9 && <0.10`, replacing `>=0.8 && <0.9`. Generated+ aggregate domains continue to use `deriveAggregateCtorsAll` and+ `deriveWireCtorsAll`, which now produce Keiki's trusted structural+ constructor evidence. Consumers must solve with Keiki 0.9 and recompile;+ generated validation results may change where 0.9 can distinguish structural+ heads or prove replay candidates disjoint.+- Generated module segments, types, constructors, values, selectors, and create-once+ source paths now use one checked UpperCamelCase/lowerCamelCase policy. Compound+ logical names such as `service_oncall` therefore move from `Service_oncall` to+ `ServiceOncall`; consumers must apply the reported source migration and recompile.+- `DiagnosticCode` gains `IdentUnsafeNormalization`,+ `GeneratedOccurrenceReserved`, `GeneratedOccurrenceCollision`, and+ `GeneratedHaskellNameChanged`. `Refusal` gains+ `GeneratedNameInvariantViolation`, `NameMigrationRequired`, and+ `NameMigrationRefusal`. Exhaustive matches must be extended.+- `ScaffoldReport` and `WorkspaceScaffoldReport` gain generated-name source-move+ evidence. `ScaffoldRecord` and `WorkspaceRecord` gain a generated-Haskell naming+ edition and stable module-role rows; missing additive rows decode as the legacy+ naming edition.+- `DiagnosticCode` gains `FieldWireKeyCollision`, `FieldWireKeyInvalid`, and+ `EvtFieldWireKeyChanged`. Exhaustive matches must be extended.+- `DiagnosticCode` appends `LanguageVersionBelowMinimum` and now derives `Ord`,+ `Enum`, and `Bounded`. Exhaustive matches must be extended. `check`, `scaffold`,+ and the working-tree side of `diff` add a stderr language-contract notice for+ compatibility-only sources, changing exact-stderr consumers.+- `DiagnosticCode` appends `AggregateEmpty`, `ContractEmpty`,+ `GeneratedPathCollision`, `GeneratedImportCycle`, `BehaviorDerivationInvalid`,+ `ConformanceFactKeyCollision`, and `GeneratedPlanningInvariantViolation`.+ Exhaustive matches must be extended; specs that scaffold already could not lower+ now fail earlier during `check`.+- `DiagnosticCode` appends thirteen accepted-surface warning and language-4 error+ codes, including process/router resolution, bounded windows, queue payload types,+ derived IDs, projection/outbox fields, and the distinct+ `RouterBenignInversion`; exhaustive matches must be extended. The never-emitted+ `DuplicateUpcasterSource`, `IdentHaskellKeyword`, `IdentNotConstructorSafe`, and+ `MappedGuardUnsupported` constructors are removed.+- `IdExpr` gains the parsed `ideField`, and `ScaffoldReport` gains+ `reportInertNodes`; callers constructing or exhaustively matching these exported+ records must be updated.++### New Features++- Generated-name collision planning now registers the selector generation+ actually emits — the explicit `haskell` alias when declared, otherwise the raw+ DSL name — instead of a camelized rendering of it. `{foo_bar, fooBar}` in one+ record is no longer reported as a collision between two fields that in fact+ generate distinct selectors, and the diagnostic no longer claims `foo_bar`+ "normalizes to" `fooBar`, which generation never did. A name that cannot be a+ Haskell selector is still refused, by the generated-name audit that owns that+ rule and names the offending declaration.+- `check --report-out` creates missing parent directories, and a workspace+ refused during composition now writes the report a single spec has always+ written for the equivalent failure. A parse failure and an unreadable or+ unparseable workspace manifest still write none: they precede any coded+ diagnostic.+- `scaffold --apply-name-migrations` applies an explicitly reviewed legacy-to-current+ source move, backs up original generated and create-once files, rewrites exact+ Haskell module references outside comments/literals, journals content digests, and+ resumes exact interrupted states. Ordinary scaffolding reports the complete plan+ and writes nothing.+- Generated-only source evolution is classified as consumer-build advisory with+ re-scaffold, recompile, and conformance remedies while wire, replay, SQL, runtime,+ and persisted identities remain compatible.+- Language 4 direct aggregate and integration-contract fields accept independent+ `haskell <selector>` and `as "<wire-key>"` aliases. Generated records use the+ selector, codecs/goldens use the wire key, and `fields(Command)` preserves both.+- `check --min-language N` enforces a registered released-language floor.+ `--deny-warnings` and repeatable/comma-separated `--deny CODE` make selected+ warnings CI-failing without changing their severity. `--report-out` writes the+ append-only `keiro-dsl/check-report/1` schema for sources and workspaces through+ the new `Keiro.Dsl.CheckReport` module.+- Language 4 now resolves every internally decidable process, router, projection,+ publisher, queue, pgmq source-key, read-model identity, and timer-ID surface,+ and rejects duration values that cannot fit the runtime `Int` seconds+ representation. Released languages 1–3 keep their prior acceptance.++### Other Changes++- The conformance-corpus driver cross-checks its plan against+ `keiro-dsl.cabal`: a suite compiling generated Haskell that no plan entry+ regenerates now fails `corpus-regen check` naming the suite, and a plan entry+ no test-suite compiles fails too. The plan is derived from tracked ledgers+ alone, so deleting one silently dropped a suite from regeneration *and* from+ both existing consistency checks, which are scoped to plan entries.+ `regenerate` also refuses a dirty corpus (with `--allow-dirty` for local+ iteration) and prints the exact `git checkout --` recovery line.+- The scaffold report's two sidecar lines are relabelled `fragment:` and+ `ledger:` to match the files they name; they read `manifest:` and `record:`+ while pointing at `keiro-dsl-cabal-fragment.*` and `keiro-dsl-ledger.*`.+ Consumers scraping those stderr labels must be updated.+- The `generated-output` Cabal stanza adds `-Werror` behind the new manual flag+ `werror-generated` (default off, because Hackage rejects an unconditional+ `-Werror`; this repository's `cabal.project` turns it on), so a warning+ regression in generated code fails the build instead of building green. `src`+ keeps `-Wall` non-fatal. Enabling it exposed five real import over-approximations in the+ generator, all now fixed: a publisher imported `ExponentialBackoffOptions`+ under a constant backoff; a nominal-projections module imported the four+ `Keiki.ProjectionDomain` text combinators used only by the unenforced-ID+ pattern; a transducer imported the type of every write source, when only guard+ operands are rendered with a `K.Index … Type` annotation; a transducer imported+ an enforced ID's `parse<Id>`, which only a literal names; and a contract codec+ imported `(.:)` when every field decodes through `explicitParseField`.+ Generated output is correspondingly smaller; no behavior changed.+ `keiro-dsl-conformance-skeletons` keeps `-Wunused-imports`/`-Wunused-matches`+ non-fatal, because its hole modules are deliberately unfilled templates.++- Generated behavior contracts and harnesses now carry complete signatures,+ annotated behavior cells, named sample constants, runtime-backed read-model+ facts, evidence-rich failures, and usage-conditional imports. The+ `keiro/behavior-conformance/1` JSON failure object adds the append-only+ `subject` field. The conformance corpus was regenerated under `-Wall` and now+ has a clean-tree regeneration policy gate; consumers should re-scaffold and+ recompile. Behavior keys, wire data, shape hashes, fold identity, and replay+ semantics are unchanged.+- Adds a record-derived conformance-corpus driver with focused public-CLI replay,+ create-once preservation, record/disk and Cabal/disk consistency checks, and a+ test-owned accept mode for the six renderer goldens.+- Adds the exported stable `ModuleRole`/`moduleRole` projection for+ `ScaffoldModule` artifacts and a generated-source naming audit plus repository+ policy gate.+- Language 4 selects syntax profile 3. The generated occurrence reserved set is+ narrowed to the 23 term identifiers GHC rejects under the advertised GHC2024+ contract, so contextual words including `family`, `via`, and `qualified` are+ accepted and compile-proven.+- Generated aggregate conformance now derives transducer rendering, predicate+ verification, behavior `EdgeRef`s, and initial live probes from one source-wide+ transition layout. Replay-only initial edges receive detailed replay witnesses but+ never `step`-based acceptance helpers, and duplicate generated declarations are+ rejected before writes.+- Source and workspace checking and both scaffold planners now share one pure gate+ order: fold surface, lowering, module construction, conformance-package planning,+ then module-plan refusals. Workspace planning therefore reports a lowering refusal+ before a simultaneous facade-key refusal. Valid generated output is unchanged.+- Accepted but inert intake flags, emit derivations, optional queue markers, and+ inline subscriptions now warn; scaffold reports list emit, pgmq dispatch, and+ operation nodes that contribute no modules. Timer dead-letter text, pgmq fanout+ functions, and pgmq top-level dedupe keys are explicitly descriptive-only.+ Existing valid generated output remains byte-identical. ## 0.10.0.0 — 2026-08-03
app/Main.hs view
@@ -13,40 +13,43 @@ import Data.Text.IO qualified as TIO import Data.Text.Lazy.IO qualified as TLIO import Keiro.Dsl.BehaviorCoverage qualified as Behavior+import Keiro.Dsl.CheckReport qualified as CheckReport import Keiro.Dsl.Coverage qualified as Coverage import Keiro.Dsl.Diff (Change (..), CompatibilitySurface, diffSources, gateWith, gatedBreaking) import Keiro.Dsl.DiffReport (diffReport, parseSurfaceName, renderExplainBlock, renderFinding) import Keiro.Dsl.ExplainBindings (bindingObligationsForService, renderBindingObligations) import Keiro.Dsl.FoldFingerprint (renderFoldSurfaceError) import Keiro.Dsl.Goldens (emitGoldenPayloads, loadGoldenPayloads)-import Keiro.Dsl.Grammar (Placement (..), Spec (..))-import Keiro.Dsl.LanguageVersion (ParsedSource (..), SourceLanguage, declaredLanguageVersionMaybe, effectiveLanguageVersion, sourceFormText)+import Keiro.Dsl.Grammar (Loc (..), Placement (..), Spec (..))+import Keiro.Dsl.LanguageVersion (LanguageVersion, ParsedSource (..), SourceLanguage (..), declaredLanguageVersionMaybe, effectiveLanguageVersion, languageVersion, languageVersionText, lookupLanguageDefinition, sourceFormText, supportedLanguageVersions) import Keiro.Dsl.Parser (parseSource, renderParseFailure) import Keiro.Dsl.PrettyPrint (renderSource, renderSpec) import Keiro.Dsl.ReplayImpact (renderReplayImpact, replayImpactServices) import Keiro.Dsl.RuntimePackage (RuntimePackageName, mkRuntimePackageName) import Keiro.Dsl.Scaffold (Context (..), ScaffoldModule (..), codecComparisonBanner, codecComparisonModule)-import Keiro.Dsl.ScaffoldRun (executeServiceScaffoldWithRuntimePackage, planServiceScaffoldWithRuntimePackageAndGoldens, renderRefusals, renderScaffoldReport)-import Keiro.Dsl.SemanticContract (CheckedService (..), checkedSource)+import Keiro.Dsl.ScaffoldRun (checkServiceDiagnostics, executeServiceScaffoldWithRuntimePackageAndNameMigrations, planServiceScaffoldWithRuntimePackageAndGoldens, renderRefusals, renderScaffoldReport)+import Keiro.Dsl.SemanticContract (CheckedService (..), checkedSource, effectiveContractLanguageVersion, languageContractNotice) import Keiro.Dsl.Skeleton (skeletonFor)-import Keiro.Dsl.Validate (Diagnostic (..), Severity (..), renderDiagnostic, validateService)-import Keiro.Dsl.Workspace (ContentSource (..), LineMap (..), OwnershipIndex (..), WorkspaceDiagnostic (..), WorkspaceFailure, WorkspaceManifest (..), WorkspaceMember (..), WorkspaceMemberRef (..), WorkspaceSpec (..), checkWorkspace, checkedWorkspace, fileContentSource, isWorkspacePath, loadWorkspace, nodeOwner, parseWorkspaceManifest, renderWorkspaceDiagnostic, renderWorkspaceFailure, renderWorkspaceManifest)+import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), DiagnosticOrigin (..), Severity (..), diagnosticCodeText, diagnosticOrigin, minimumLanguageDiagnostics, parseDiagnosticCode, renderDiagnostic, validateService)+import Keiro.Dsl.Workspace (ContentSource (..), LineMap (..), OwnershipIndex (..), WorkspaceDiagnostic (..), WorkspaceFailure (..), WorkspaceFile (..), WorkspaceLocation (..), WorkspaceManifest (..), WorkspaceMember (..), WorkspaceMemberRef (..), WorkspaceSpec (..), checkWorkspace, checkedWorkspace, fileContentSource, isWorkspacePath, loadWorkspace, nodeOwner, parseWorkspaceManifest, renderWorkspaceDiagnostic, renderWorkspaceFailure, renderWorkspaceManifest) import Keiro.Dsl.WorkspaceDiff (WorkspaceChange (..), WorkspaceMeta (..), diffWorkspaces, renderWorkspaceFinding, workspaceDiffReport)-import Keiro.Dsl.WorkspaceScaffold (executeWorkspaceScaffold, planWorkspaceScaffoldWithRuntimePackageAndGoldens, renderWorkspaceScaffoldReport)+import Keiro.Dsl.WorkspaceScaffold (executeWorkspaceScaffoldWithNameMigrations, planWorkspaceScaffoldWithRuntimePackageAndGoldens, renderWorkspaceScaffoldReport)+import Numeric.Natural (Natural) import Options.Applicative import System.Directory (canonicalizePath, createDirectoryIfMissing, doesFileExist) import System.Exit (ExitCode (..), exitFailure) import System.FilePath (isAbsolute, makeRelative, normalise, takeDirectory, takeFileName, (</>)) import System.IO (hPutStrLn, stderr) import System.Process (readProcessWithExitCode)+import Text.Read (readMaybe) data Command = Parse FilePath | Pretty FilePath- | Check FilePath Bool Bool (Maybe CheckCoverageOptions)+ | Check FilePath CheckOptions | Inspect FilePath InspectionFormat | BehaviorObligations FilePath BehaviorFormat- | Scaffold FilePath FilePath (Maybe String) (Maybe RuntimePackageName) Bool Bool (Maybe FilePath) (Maybe (String, FilePath))+ | Scaffold FilePath FilePath (Maybe String) (Maybe RuntimePackageName) Bool Bool Bool (Maybe FilePath) (Maybe (String, FilePath)) | Diff FilePath String (Maybe FilePath) (Maybe FilePath) [CompatibilitySurface] Bool (Maybe FilePath) (Maybe DiffCoverageOptions) | New String @@ -59,6 +62,16 @@ checkFailOnOpaque :: !Bool } +data CheckOptions = CheckOptions+ { checkEmit :: !Bool,+ checkExplainBindings :: !Bool,+ checkCoverage :: !(Maybe CheckCoverageOptions),+ checkMinLanguage :: !(Maybe LanguageVersion),+ checkDenyWarnings :: !Bool,+ checkDenyCodes :: ![DiagnosticCode],+ checkReportOut :: !(Maybe FilePath)+ }+ data DiffCoverageOptions = DiffCoverageOptions { diffCoveragePath :: !FilePath, diffFailOnOpaqueIncrease :: !Bool@@ -83,7 +96,7 @@ (info (Pretty <$> fileArg <**> helper) (progDesc "Parse a .keiro file and print its canonical source form")) <> command "check"- (info (Check <$> fileArg <*> emitSwitch <*> explainBindingsSwitch <*> checkCoverageOptions <**> helper) (progDesc "Validate a .keiro file; print diagnostics and exit non-zero on any error"))+ (info (Check <$> fileArg <*> checkOptions <**> helper) (progDesc "Validate a .keiro file; print diagnostics and exit non-zero on any error")) <> command "inspect" (info (Inspect <$> fileArg <*> inspectionFormatOpt <**> helper) (progDesc "Inspect source-language provenance for a .keiro file or workspace as JSON"))@@ -92,7 +105,7 @@ (info (BehaviorObligations <$> fileArg <*> behaviorFormatOpt <**> helper) (progDesc "List static aggregate behavior obligations for a .keiro file or workspace")) <> command "scaffold"- (info (Scaffold <$> fileArg <*> outOpt <*> optional moduleRootOpt <*> optional runtimePackageOpt <*> collocateSwitch <*> forceGeneratedOverwriteSwitch <*> optional goldensOpt <*> codecComparisonOpts <**> helper) (progDesc "Emit the generated layer + typed holes from a .keiro file"))+ (info (Scaffold <$> fileArg <*> outOpt <*> optional moduleRootOpt <*> optional runtimePackageOpt <*> collocateSwitch <*> forceGeneratedOverwriteSwitch <*> applyNameMigrationsSwitch <*> optional goldensOpt <*> codecComparisonOpts <**> helper) (progDesc "Emit the generated layer + typed holes from a .keiro file")) <> command "diff" (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"))@@ -122,6 +135,9 @@ forceGeneratedOverwriteSwitch :: Parser Bool forceGeneratedOverwriteSwitch = switch (long "force-generated-overwrite" <> help "Overwrite a Generated path even when the existing file lacks the @generated banner") +applyNameMigrationsSwitch :: Parser Bool+applyNameMigrationsSwitch = switch (long "apply-name-migrations" <> help "Apply reviewed generated-Haskell source and sidecar moves with recoverable backups")+ goldensOpt :: Parser FilePath goldensOpt = strOption (long "goldens" <> metavar "DIR" <> help "Golden-payload root to embed in generated aggregate harnesses") @@ -159,6 +175,115 @@ <*> switch (long "fail-on-opaque" <> help "Fail when a private persisted root contains an opaque boundary (requires --coverage-report)") ) +checkOptions :: Parser CheckOptions+checkOptions =+ CheckOptions+ <$> emitSwitch+ <*> explainBindingsSwitch+ <*> checkCoverageOptions+ <*> optional minLanguageOpt+ <*> switch (long "deny-warnings" <> help "Exit non-zero when any warning-severity diagnostic fires")+ <*> denyCodesOptions+ <*> optional checkReportOutOpt++minLanguageOpt :: Parser LanguageVersion+minLanguageOpt =+ option+ (eitherReader parseMinimumLanguage)+ (long "min-language" <> metavar "N" <> help "Require at least released keiro-dsl language version N")++parseMinimumLanguage :: String -> Either String LanguageVersion+parseMinimumLanguage raw =+ case (readMaybe raw :: Maybe Natural) >>= languageVersion of+ Just version+ | Just _ <- lookupLanguageDefinition version -> Right version+ _ ->+ Left+ ( "minimum keiro-dsl language version "+ <> raw+ <> " is unsupported; supported versions: "+ <> T.unpack (T.intercalate ", " (map languageVersionText (NE.toList supportedLanguageVersions)))+ )++denyCodesOptions :: Parser [DiagnosticCode]+denyCodesOptions =+ concat+ <$> many+ ( option+ (eitherReader parseDenyCodes)+ (long "deny" <> metavar "CODE[,CODE...]" <> help "Exit non-zero for warning diagnostics with these stable codes (repeatable)")+ )++-- | Parse a @--deny@ argument, refusing codes @check@ can never emit.+--+-- A denial that can never match is worse than no denial: it reads like a gate in+-- a CI file and silently is not one. Diff-side and codec-comparison codes are+-- therefore rejected outright here. Coverage codes are accepted at this stage+-- and validated against @--coverage-report@ once the whole invocation is known,+-- because an option reader cannot see its sibling options.+parseDenyCodes :: String -> Either String [DiagnosticCode]+parseDenyCodes raw = traverse parseOne (T.splitOn "," (T.pack raw))+ where+ parseOne token+ | T.null token = Left "--deny requires one or more comma-separated diagnostic codes"+ | otherwise = case parseDiagnosticCode token of+ Just diagnosticCode -> classify token diagnosticCode+ Nothing ->+ Left+ ( "unknown diagnostic code `"+ <> T.unpack token+ <> "`; copy the spelling exactly from warning[Code] output"+ )+ classify token diagnosticCode = case diagnosticOrigin diagnosticCode of+ CheckDiagnostic -> Right diagnosticCode+ CoverageDiagnostic -> Right diagnosticCode+ DiffDiagnostic ->+ Left+ ( "diagnostic code `"+ <> T.unpack token+ <> "` is emitted by `keiro-dsl diff`, which compares two revisions; `check` can never emit it, so denying it here would never match"+ )+ CodecCompareDiagnostic ->+ Left+ ( "diagnostic code `"+ <> T.unpack token+ <> "` is emitted only by the generated codec-comparison path; `check` can never emit it, so denying it here would never match"+ )++-- | Reject a @--deny@ selection of a coverage code when this invocation never+-- runs the coverage pass, for the same reason 'parseDenyCodes' rejects+-- diff-side codes: the denial could not fire.+--+-- 'CoverageOpaqueGateExceeded' is refused outright: it is the error+-- @--fail-on-opaque@ itself raises, never a warning, so the deny scan can never+-- match it — without the flag the code does not fire at all, and with the flag+-- the run already fails. A denial in CI must be either effective or an+-- immediate error, and this one could only ever be a silent no-op.+validateCheckDenyCodes :: CheckOptions -> IO ()+validateCheckDenyCodes options = do+ when (CoverageOpaqueGateExceeded `elem` checkDenyCodes options) $ do+ TIO.hPutStrLn+ stderr+ "check: --deny CoverageOpaqueGateExceeded can never match; the code is the error --fail-on-opaque itself raises, so pass --fail-on-opaque instead of denying it"+ exitFailure+ case [diagnosticCode | diagnosticCode <- checkDenyCodes options, diagnosticOrigin diagnosticCode == CoverageDiagnostic, diagnosticCode /= CoverageOpaqueGateExceeded] of+ [] -> pure ()+ unreachable+ | Just _ <- checkCoverage options -> pure ()+ | otherwise -> do+ TIO.hPutStrLn+ stderr+ ( "check: --deny "+ <> T.intercalate ", " (map diagnosticCodeText unreachable)+ <> " selects a structural-coverage code, which only the coverage pass emits; add --coverage-report FILE or drop the code"+ )+ exitFailure++checkReportOutOpt :: Parser FilePath+checkReportOutOpt =+ strOption+ (long "report-out" <> metavar "FILE" <> help "Write the full keiro-dsl/check-report/1 validation report as JSON")+ diffCoverageOptions :: Parser (Maybe DiffCoverageOptions) diffCoverageOptions = optional@@ -201,19 +326,172 @@ parseFormat "json" = Right BehaviorJson parseFormat other = Left ("unsupported behavior obligation format: " <> other <> " (expected text or json)") +emitLanguageContractNotice :: FilePath -> T.Text -> CheckedService -> IO ()+emitLanguageContractNotice subject sourceFormSummary service =+ mapM_+ (TIO.hPutStrLn stderr)+ (languageContractNotice subject sourceFormSummary (checkedLanguageContract service))++emitWorkspaceLanguageContractNotice :: FilePath -> WorkspaceSpec -> IO ()+emitWorkspaceLanguageContractNotice subject workspace =+ emitLanguageContractNotice subject (workspaceSourceFormSummary workspace) (checkedWorkspace workspace)++workspaceSourceFormSummary :: WorkspaceSpec -> T.Text+workspaceSourceFormSummary workspace =+ "workspace, "+ <> T.pack (show legacyCount)+ <> " legacy-unversioned member(s)"+ where+ legacyCount = length [() | member <- wsMembers workspace, LegacyUnversioned <- [wmSourceLanguage member]]++minimumWorkspaceLanguageDiagnostics :: LanguageVersion -> WorkspaceSpec -> [WorkspaceDiagnostic]+minimumWorkspaceLanguageDiagnostics floorVersion workspace+ | effectiveVersion >= floorVersion = []+ | otherwise =+ [ WorkspaceDiagnostic+ { wdLocations =+ NE.fromList+ ( WorkspaceLocation WorkspaceManifestFile 1 ""+ : [ WorkspaceLocation+ (WorkspaceMemberFile (wmPath member))+ (sourceLanguageLine (wmSourceLanguage member))+ ( "member selects effective language version "+ <> languageVersionText (effectiveLanguageVersion (wmSourceLanguage member))+ )+ | member <- wsMembers workspace+ ]+ ),+ wdSeverity = Error,+ wdCode = LanguageVersionBelowMinimum,+ wdSourceLanguageCause = Nothing,+ wdMessage =+ "effective language version "+ <> languageVersionText effectiveVersion+ <> " (workspace-composed) is below the required minimum "+ <> languageVersionText floorVersion+ <> "; declare `language keiro-dsl "+ <> languageVersionText floorVersion+ <> "` in every member"+ }+ ]+ where+ effectiveVersion = effectiveContractLanguageVersion (checkedLanguageContract (checkedWorkspace workspace))+ sourceLanguageLine LegacyUnversioned = 1+ sourceLanguageLine DeclaredLanguage {languageVersionLoc = Loc lineNumber} = lineNumber++deniesWarningCode :: CheckOptions -> DiagnosticCode -> Bool+deniesWarningCode options diagnosticCode =+ checkDenyWarnings options || diagnosticCode `elem` checkDenyCodes options++deniedSourceWarningCodes :: CheckOptions -> [Diagnostic] -> [DiagnosticCode]+deniedSourceWarningCodes options diagnostics =+ [ code diagnostic+ | diagnostic <- diagnostics,+ severity diagnostic == Warning,+ deniesWarningCode options (code diagnostic)+ ]++deniedWorkspaceWarningCodes :: CheckOptions -> [WorkspaceDiagnostic] -> [DiagnosticCode]+deniedWorkspaceWarningCodes options diagnostics =+ [ wdCode diagnostic+ | diagnostic <- diagnostics,+ wdSeverity diagnostic == Warning,+ deniesWarningCode options (wdCode diagnostic)+ ]++emitDeniedWarningSummary :: [DiagnosticCode] -> IO ()+emitDeniedWarningSummary deniedCodes =+ when (not (null deniedCodes)) $+ TIO.hPutStrLn stderr $+ "check: "+ <> T.pack (show (length deniedCodes))+ <> " warning(s) escalated to failure (denied: "+ <> T.intercalate ", " [diagnosticCodeText diagnosticCode | diagnosticCode <- [minBound .. maxBound], diagnosticCode `elem` deniedCodes]+ <> ")"++checkReportEnforcement :: CheckOptions -> CheckReport.CheckReportEnforcement+checkReportEnforcement options =+ CheckReport.CheckReportEnforcement+ { CheckReport.reportMinLanguage = checkMinLanguage options,+ CheckReport.reportDenyWarnings = checkDenyWarnings options,+ CheckReport.reportDenyCodes = checkDenyCodes options+ }++-- | Write a check report to @--report-out@, creating any missing parent+-- directories first. CI recipes routinely point this at a not-yet-created+-- artifact directory; the coverage writer has always done this.+writeCheckReportFile :: CheckOptions -> CheckReport.CheckReport -> IO ()+writeCheckReportFile options report =+ mapM_+ ( \path -> do+ createDirectoryIfMissing True (takeDirectory path)+ Aeson.encodeFile path report+ )+ (checkReportOut options)++writeSourceCheckReport :: FilePath -> ParsedSource -> CheckedService -> CheckOptions -> [Diagnostic] -> IO ()+writeSourceCheckReport subject parsedSource service options diagnostics =+ writeCheckReportFile+ options+ ( CheckReport.checkReport+ subject+ (parsedSourceLanguage parsedSource)+ (checkedLanguageContract service)+ enforcement+ diagnostics+ (CheckReport.effectiveDenyCodes enforcement)+ )+ where+ enforcement = checkReportEnforcement options++writeWorkspaceCheckReport :: FilePath -> WorkspaceSpec -> CheckedService -> CheckOptions -> [WorkspaceDiagnostic] -> IO ()+writeWorkspaceCheckReport subject workspace service options diagnostics =+ writeCheckReportFile+ options+ ( CheckReport.workspaceCheckReport+ subject+ workspace+ (checkedLanguageContract service)+ enforcement+ diagnostics+ (CheckReport.effectiveDenyCodes enforcement)+ )+ where+ enforcement = checkReportEnforcement options++-- | Write the machine report for a workspace refused during composition. The+-- single-spec path already reports the equivalent failure, so a CI job that+-- consumes @--report-out@ must not lose the workspace one.+writeWorkspaceRefusalReport :: FilePath -> CheckOptions -> WorkspaceFailure -> IO ()+writeWorkspaceRefusalReport subject options failure = case failure of+ WorkspaceRefused diagnostics ->+ writeCheckReportFile+ options+ ( CheckReport.workspaceRefusalReport+ subject+ enforcement+ diagnostics+ (CheckReport.effectiveDenyCodes enforcement)+ )+ -- An unreadable or unparseable manifest has no coded diagnostic, exactly as a+ -- `.keiro` parse error has none on the single-spec path. Both write no report.+ _ -> pure ()+ where+ enforcement = checkReportEnforcement options+ run :: Command -> IO () run (Pretty fp) = run (Parse fp) -- Workspace dispatch. A @FILE@ ending in @.keiro-workspace@ is a workspace -- manifest; everything else takes the untouched single-file path below. run (Parse fp) | isWorkspacePath fp = runWorkspaceParse fp-run (Check fp emit explainBindings coverageOptions)- | isWorkspacePath fp = runWorkspaceCheck fp emit explainBindings coverageOptions+run (Check fp checkOptionsValue)+ | isWorkspacePath fp = runWorkspaceCheck fp checkOptionsValue run (Inspect fp format) | isWorkspacePath fp = runWorkspaceInspect fp format run (BehaviorObligations fp format) | isWorkspacePath fp = runWorkspaceBehaviorObligations fp format-run (Scaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite cliGoldens comparisonRequest)- | isWorkspacePath fp = runWorkspaceScaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite cliGoldens comparisonRequest+run (Scaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite applyNameMigrations cliGoldens comparisonRequest)+ | isWorkspacePath fp = runWorkspaceScaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite applyNameMigrations cliGoldens comparisonRequest run (Diff fp ref emitGoldensRoot replayImpactOut gatedSurfaces explain reportOut coverageOptions) | isWorkspacePath fp = runWorkspaceDiff fp ref emitGoldensRoot replayImpactOut gatedSurfaces explain reportOut coverageOptions run (Parse fp) = do@@ -223,32 +501,44 @@ hPutStrLn stderr (T.unpack (renderParseFailure failure)) exitFailure Right parsedSource -> TIO.putStrLn (renderSource parsedSource)-run (Check fp emit explainBindings coverageOptions) = do+run (Check fp options) = do input <- TIO.readFile fp case parseSource fp input of Left failure -> do hPutStrLn stderr (T.unpack (renderParseFailure failure)) exitFailure Right parsedSource -> do+ validateCheckDenyCodes options let service = checkedSource parsedSource spec = checkedSpec service- diags = validateService service- mapM_ (TIO.hPutStrLn stderr . renderDiagnostic fp) diags- if any ((== Error) . severity) diags+ floorDiags = maybe [] (\floorVersion -> minimumLanguageDiagnostics floorVersion (parsedSourceLanguage parsedSource)) (checkMinLanguage options)+ semanticDiags = floorDiags <> checkServiceDiagnostics Nothing (mkContext Nothing False spec) service+ semanticFailed = any ((== Error) . severity) semanticDiags+ emitLanguageContractNotice fp (sourceFormText (parsedSourceLanguage parsedSource)) service+ mapM_ (TIO.hPutStrLn stderr . renderDiagnostic fp) semanticDiags+ -- Coverage is part of this invocation's diagnostic surface, not a+ -- success-path artifact: its findings must reach the deny policy, the exit+ -- code, and the check report. It still runs only after semantic validation+ -- passes, because an unresolvable graph has nothing to cover.+ coveragePlan <- planCheckCoverage fp spec (if semanticFailed then Nothing else checkCoverage options)+ coverageOk <- emitPlannedCoverage coveragePlan+ let diags = semanticDiags <> plannedCoverageDiagnostics coveragePlan+ deniedWarningCodes = deniedSourceWarningCodes options diags+ emitDeniedWarningSummary deniedWarningCodes+ writeSourceCheckReport fp parsedSource service options diags+ if semanticFailed || not coverageOk || not (null deniedWarningCodes) then exitFailure else do- when emit (TIO.putStrLn (renderSource parsedSource))- if explainBindings+ when (checkEmit options) (TIO.putStrLn (renderSource parsedSource))+ if checkExplainBindings options then case bindingObligationsForService service 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 cliRuntimePackage cliCollocate forceGeneratedOverwrite cliGoldens comparisonRequest) = do+ when (not (checkEmit options) && not (checkExplainBindings options)) (putStrLn "OK")+run (Scaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite applyNameMigrations cliGoldens comparisonRequest) = do input <- TIO.readFile fp case parseSource fp input of Left failure -> do@@ -257,6 +547,7 @@ Right parsedSource -> do let service = checkedSource parsedSource spec = checkedSpec service+ emitLanguageContractNotice fp (sourceFormText (parsedSourceLanguage parsedSource)) service -- Validation gate: never scaffold an invalid spec. Abort on any -- error-severity diagnostic before writing a single module. let diags = validateService service@@ -275,7 +566,7 @@ case comparisonReady of Left comparisonError -> TIO.hPutStrLn stderr comparisonError >> exitFailure Right () -> do- result <- executeServiceScaffoldWithRuntimePackage cliRuntimePackage out forceGeneratedOverwrite fp (parsedSourceLanguage parsedSource) ctx service modules+ result <- executeServiceScaffoldWithRuntimePackageAndNameMigrations cliRuntimePackage applyNameMigrations out forceGeneratedOverwrite fp (parsedSourceLanguage parsedSource) ctx service modules case result of Left refusals -> do mapM_ (TIO.hPutStrLn stderr) (renderRefusals refusals)@@ -330,6 +621,7 @@ newService = checkedSource newSource oldSpec = checkedSpec oldService newSpec = checkedSpec newService+ emitLanguageContractNotice fp (sourceFormText (parsedSourceLanguage newSource)) newService case (,) <$> diffSources oldSource newSource <*> replayImpactServices oldService newService of Left surfaceError -> TIO.hPutStrLn stderr (renderFoldSurfaceError surfaceError) >> exitFailure Right (changes, impact) -> do@@ -445,32 +737,43 @@ -- @--emit@ prints the canonical whole-service view, @--explain-bindings@ lists the -- service's binding obligations, and the coverage options report on the merged -- mapped-type graph with the manifest as the report's subject.-runWorkspaceCheck :: FilePath -> Bool -> Bool -> Maybe CheckCoverageOptions -> IO ()-runWorkspaceCheck fp emit explainBindings coverageOptions = do+runWorkspaceCheck :: FilePath -> CheckOptions -> IO ()+runWorkspaceCheck fp options = do+ validateCheckDenyCodes options loaded <- loadWorkspace (fileContentSource (takeDirectory fp)) fp case loaded of Left failure -> do mapM_ (TIO.hPutStrLn stderr) (renderWorkspaceFailure fp failure)+ writeWorkspaceRefusalReport fp options failure exitFailure Right workspace -> do let service = checkedWorkspace workspace- diags = checkWorkspace workspace+ floorDiags = maybe [] (\floorVersion -> minimumWorkspaceLanguageDiagnostics floorVersion workspace) (checkMinLanguage options)+ semanticDiags = floorDiags <> checkWorkspace workspace spec = checkedSpec service- mapM_ (TIO.hPutStrLn stderr . renderWorkspaceDiagnostic fp) diags- if any ((== Error) . wdSeverity) diags+ semanticFailed = any ((== Error) . wdSeverity) semanticDiags+ emitWorkspaceLanguageContractNotice fp workspace+ mapM_ (TIO.hPutStrLn stderr . renderWorkspaceDiagnostic fp) semanticDiags+ -- Same contract as the single-spec path: coverage findings are gated+ -- diagnostics, not success-path output. See `run (Check …)` above.+ coveragePlan <- planCheckCoverage fp spec (if semanticFailed then Nothing else checkCoverage options)+ coverageOk <- emitPlannedCoverage coveragePlan+ let diags = semanticDiags <> map (workspaceCoverageDiagnostic fp) (plannedCoverageFindings coveragePlan)+ deniedWarningCodes = deniedWorkspaceWarningCodes options diags+ emitDeniedWarningSummary deniedWarningCodes+ writeWorkspaceCheckReport fp workspace service options diags+ if semanticFailed || not coverageOk || not (null deniedWarningCodes) then exitFailure else do- when emit (TIO.putStrLn (renderSpec spec))- if explainBindings+ when (checkEmit options) (TIO.putStrLn (renderSpec spec))+ if checkExplainBindings options then case bindingObligationsForService service of Left graphErrors -> do hPutStrLn stderr ("validated workspace did not resolve its mapped type graph: " <> show graphErrors) exitFailure Right obligations -> TIO.putStrLn (renderBindingObligations (wsContext workspace) obligations) else pure ()- coverageOk <- runCheckCoverage fp spec coverageOptions- when (coverageOk && not emit && not explainBindings) (putStrLn "OK")- when (not coverageOk) exitFailure+ when (not (checkEmit options) && not (checkExplainBindings options)) (putStrLn "OK") -- | @scaffold@ on a workspace manifest: compose the whole service, then plan -- and emit the complete module set for every member in one invocation.@@ -491,16 +794,18 @@ Maybe RuntimePackageName -> Bool -> Bool ->+ Bool -> Maybe FilePath -> Maybe (String, FilePath) -> IO ()-runWorkspaceScaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite cliGoldens comparisonRequest = do+runWorkspaceScaffold fp out cliRoot cliRuntimePackage cliCollocate forceGeneratedOverwrite applyNameMigrations cliGoldens comparisonRequest = do loaded <- loadWorkspace (fileContentSource (takeDirectory fp)) fp case loaded of Left failure -> do mapM_ (TIO.hPutStrLn stderr) (renderWorkspaceFailure fp failure) exitFailure Right workspace -> do+ emitWorkspaceLanguageContractNotice fp workspace -- Validation gate: never scaffold an invalid service. Abort on any -- error-severity diagnostic before writing a single module. let diags = checkWorkspace workspace@@ -525,7 +830,7 @@ case comparisonReady of Left comparisonError -> TIO.hPutStrLn stderr comparisonError >> exitFailure Right () -> do- result <- executeWorkspaceScaffold out forceGeneratedOverwrite plan+ result <- executeWorkspaceScaffoldWithNameMigrations out forceGeneratedOverwrite applyNameMigrations plan case result of Left refusals -> do mapM_ (TIO.hPutStrLn stderr) (renderRefusals refusals)@@ -572,6 +877,7 @@ case newLoaded of Left failure -> printWorkspaceFailure fp failure Right newWorkspace -> do+ emitWorkspaceLanguageContractNotice fp newWorkspace currentManifestText <- TIO.readFile fp case parseWorkspaceManifest fp currentManifestText of Left err -> hPutStrLn stderr (T.unpack err) >> exitFailure@@ -766,16 +1072,62 @@ 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+-- | What this @check@ invocation's coverage pass will do, decided before the+-- warning policy is applied so the findings can take part in it.+data PlannedCoverage+ = -- | @--coverage-report@ was not supplied, or semantic validation already failed.+ NoCoverage+ | -- | The report to render, and the path to write it to.+ PlannedCoverage !FilePath !Coverage.CoverageReport+ | -- | The mapped-type graph did not resolve; the pass cannot run.+ CoverageUnresolved !String++planCheckCoverage :: FilePath -> Spec -> Maybe CheckCoverageOptions -> IO PlannedCoverage+planCheckCoverage _ _ Nothing = pure NoCoverage+planCheckCoverage specPath spec (Just options) =+ pure $ case Coverage.coverageReport specPath spec of+ Left graphErrors -> CoverageUnresolved (show graphErrors)+ Right baseReport ->+ PlannedCoverage+ (checkCoveragePath options)+ (if checkFailOnOpaque options then Coverage.failOnOpaque baseReport else baseReport)++plannedCoverageFindings :: PlannedCoverage -> [Coverage.CoverageFinding]+plannedCoverageFindings (PlannedCoverage _ report) = Coverage.coverageFindings report+plannedCoverageFindings _ = []++-- | Coverage findings as ordinary source diagnostics. They carry no line, which+-- the rendered form has always shown as @:0:@.+plannedCoverageDiagnostics :: PlannedCoverage -> [Diagnostic]+plannedCoverageDiagnostics plan =+ [ Diagnostic+ { line = 0,+ severity = Coverage.findingSeverity finding,+ code = Coverage.findingCode finding,+ relatedLocations = [],+ message = Coverage.coverageFindingMessage finding+ }+ | finding <- plannedCoverageFindings plan+ ]++-- | The workspace twin: coverage runs on the merged graph, so every finding is+-- attributed to the manifest rather than to one member.+workspaceCoverageDiagnostic :: FilePath -> Coverage.CoverageFinding -> WorkspaceDiagnostic+workspaceCoverageDiagnostic _ finding =+ WorkspaceDiagnostic+ { wdLocations = NE.fromList [WorkspaceLocation WorkspaceManifestFile 0 ""],+ wdSeverity = Coverage.findingSeverity finding,+ wdCode = Coverage.findingCode finding,+ wdSourceLanguageCause = Nothing,+ wdMessage = Coverage.coverageFindingMessage finding+ }++emitPlannedCoverage :: PlannedCoverage -> IO Bool+emitPlannedCoverage NoCoverage = pure True+emitPlannedCoverage (CoverageUnresolved graphErrors) = do+ hPutStrLn stderr ("validated spec did not resolve its mapped type graph for coverage: " <> graphErrors)+ pure False+emitPlannedCoverage (PlannedCoverage path report) = emitCoverageReport path report runDiffCoverage :: FilePath -> T.Text -> Spec -> Spec -> Maybe DiffCoverageOptions -> IO Bool runDiffCoverage _ _ _ _ Nothing = pure True
keiro-dsl.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: keiro-dsl-version: 0.10.0.0+version: 0.11.0.0 synopsis: Typed specification toolchain for keiro services description: keiro-dsl is the toolchain over a typed `.keiro` specification of a keiro@@ -29,10 +29,27 @@ OverloadedLabels OverloadedStrings +-- Every component importing this stanza compiles only `keiro-dsl scaffold`+-- output plus hand-filled holes. Generated code is regenerated wholesale, so a+-- warning in it is a generator defect with nothing for a human to fix in place:+-- -Werror keeps such a regression from building green. This is deliberately not+-- added to `warnings`, where src/ keeps -Wall non-fatal. Hackage rejects an+-- unconditional -Werror, so the gate rides a manual flag that this repository's+-- cabal.project switches on; the published sdist defaults it off.+flag werror-generated+ description:+ Promote warnings in committed generated conformance output to errors++ manual: True+ default: False+ common generated-output default-language: GHC2024 default-extensions: OverloadedStrings + if flag(werror-generated)+ ghc-options: -Werror+ library import: warnings, shared hs-source-dirs: src@@ -41,6 +58,7 @@ Keiro.Dsl.AggregateType Keiro.Dsl.BehaviorCoverage Keiro.Dsl.CanonicalEncoding+ Keiro.Dsl.CheckReport Keiro.Dsl.CodecCompare Keiro.Dsl.ConformancePackage Keiro.Dsl.Coverage@@ -54,6 +72,7 @@ Keiro.Dsl.Goldens Keiro.Dsl.Grammar Keiro.Dsl.Harness+ Keiro.Dsl.HaskellSourceMove Keiro.Dsl.IdDomain Keiro.Dsl.LanguageVersion Keiro.Dsl.Manifest@@ -70,6 +89,8 @@ Keiro.Dsl.ScaffoldRun Keiro.Dsl.SemanticContract Keiro.Dsl.ServiceHarness+ Keiro.Dsl.SidecarMigration+ Keiro.Dsl.SidecarNames Keiro.Dsl.Skeleton Keiro.Dsl.Source Keiro.Dsl.Syntax@@ -82,9 +103,12 @@ Keiro.Dsl.WorkspaceScaffold other-modules:+ Keiro.Dsl.AggregateGenerationPlan+ Keiro.Dsl.FieldIdentity Keiro.Dsl.Frontend.Internal Keiro.Dsl.GeneratedHaskellLanguage Keiro.Dsl.HaskellImport+ Keiro.Dsl.HaskellName Keiro.Dsl.Parser.Aggregate Keiro.Dsl.Parser.Coordination Keiro.Dsl.Parser.Core@@ -106,8 +130,8 @@ , containers >=0.6 && <0.8 , directory >=1.3 && <1.4 , filepath >=1.4 && <1.6- , keiki >=0.8 && <0.9- , keiro-core ^>=0.10.0.0+ , keiki >=0.9 && <0.10+ , keiro-core ^>=0.11.0.0 , megaparsec >=9.6 && <9.9 , mmzk-typeid >=0.7 && <0.8 , parser-combinators >=1.3 && <1.4@@ -150,7 +174,7 @@ , directory >=1.3 && <1.4 , filepath >=1.4 && <1.6 , hspec >=2.11- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro-core , keiro-dsl , mmzk-typeid >=0.7 && <0.8@@ -163,13 +187,44 @@ type: exitcode-stdio-1.0 hs-source-dirs: test/import-planning src main-is: Main.hs- other-modules: Keiro.Dsl.HaskellImport+ other-modules:+ Keiro.Dsl.HaskellImport+ Keiro.Dsl.HaskellName+ build-depends: , base >=4.21 && <5 , containers >=0.6 && <0.8 , hspec >=2.11 , text >=2.1 && <2.2 +test-suite keiro-dsl-haskell-name-test+ import: warnings, shared+ type: exitcode-stdio-1.0+ hs-source-dirs: test/haskell-name src+ main-is: Main.hs+ other-modules: Keiro.Dsl.HaskellName+ build-depends:+ , base >=4.21 && <5+ , containers >=0.6 && <0.8+ , hspec >=2.11+ , QuickCheck >=2.14+ , text >=2.1 && <2.2++-- Plan 199: keiro-dsl validates specs without depending on the keiro runtime+-- package, so a few runtime vocabularies are restated in Keiro.Dsl.Validate.+-- This suite depends on both packages and fails when a restatement drifts from+-- the definition it mirrors.+test-suite keiro-dsl-runtime-vocabulary-test+ import: warnings, shared+ type: exitcode-stdio-1.0+ hs-source-dirs: test/runtime-vocabulary+ main-is: Main.hs+ build-depends:+ , base >=4.21 && <5+ , keiro+ , keiro-dsl+ , 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 -- keiki's validator and the codec round-trips. The Generated.* modules under@@ -196,7 +251,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , bytestring >=0.12 && <0.13- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 @@ -221,7 +276,7 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 , time >=1.12 && <1.15@@ -256,7 +311,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , containers >=0.6 && <0.8- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , keiro-dsl , text >=2.1 && <2.2@@ -290,7 +345,7 @@ , base >=4.21 && <5 , bytestring >=0.12 && <0.13 , containers >=0.6 && <0.8- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 , time >=1.12 && <1.15@@ -321,7 +376,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , containers >=0.6 && <0.8- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , mmzk-typeid >=0.7 && <0.8 , text >=2.1 && <2.2@@ -361,7 +416,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , containers >=0.6 && <0.8- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 @@ -376,6 +431,7 @@ other-modules: Conformance.Structural.Bindings Conformance.Structural.Domain+ Generated.StructuralConformance.ArtifactCatalog.BehaviorContract Generated.StructuralConformance.ArtifactCatalog.Codec Generated.StructuralConformance.ArtifactCatalog.Domain Generated.StructuralConformance.ArtifactCatalog.EventStream@@ -396,7 +452,7 @@ , bytestring >=0.12 && <0.13 , containers >=0.6 && <0.8 , deepseq >=1.5 && <1.6- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 , time >=1.12 && <1.15@@ -432,7 +488,7 @@ , base >=4.21 && <5 , bytestring >=0.12 && <0.13 , containers >=0.6 && <0.8- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 , time >=1.12 && <1.15@@ -466,7 +522,7 @@ , deepseq >=1.5 && <1.6 , directory >=1.3 && <1.4 , filepath >=1.4 && <1.6- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , keiro-dsl , text >=2.1 && <2.2@@ -493,7 +549,7 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 @@ -517,7 +573,7 @@ , base >=4.21 && <5 , containers >=0.6 && <0.8 , deepseq >=1.5 && <1.6- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , tasty-bench >=0.5 && <0.6 , text >=2.1 && <2.2@@ -544,15 +600,24 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , 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` -- cannot emit syntactically or type-invalid Haskell.+-- The only corpus suite whose hole modules are deliberately *unfilled*: it+-- exists to prove `keiro-dsl new <kind>` scaffolds a skeleton that compiles, so+-- its create-once files still carry their `-- HOLE` markers. A template cannot+-- be clean under -Wunused-imports/-Wunused-matches by construction — its imports+-- and its command binder are there for the fill that has not happened yet — so+-- those two classes are non-fatal here and everywhere-else-fatal. Every other+-- warning class remains an error, and the generated modules in this suite are+-- held to the same standard as the rest of the corpus. test-suite keiro-dsl-conformance-skeletons import: warnings, generated-output+ ghc-options: -Wno-error=unused-imports -Wno-error=unused-matches type: exitcode-stdio-1.0 hs-source-dirs: test/conformance-skeletons main-is: Main.hs@@ -594,17 +659,17 @@ SkelProcess.MyService.Hospital.Holes SkelProcess.MyService.HospitalSurge.ProcessHoles SkelProcess.MyService.Surge.Holes- SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModel- 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- SkelQueue.Generated.MyService.Transfer_decisions.ReadModelTable- SkelQueue.MyService.Accepted_transfer_needs.ReadModelHoles- SkelQueue.MyService.Transfer_decisions.ReadModelHoles+ SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModel+ SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelHarness+ SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelTable+ SkelQueue.Generated.MyService.ReservationWork.Queue+ SkelQueue.Generated.MyService.ReservationWork.QueueCodec+ SkelQueue.Generated.MyService.ReservationWork.QueuePolicy+ SkelQueue.Generated.MyService.TransferDecisions.ReadModel+ SkelQueue.Generated.MyService.TransferDecisions.ReadModelHarness+ SkelQueue.Generated.MyService.TransferDecisions.ReadModelTable+ SkelQueue.MyService.AcceptedTransferNeeds.ReadModelHoles+ SkelQueue.MyService.TransferDecisions.ReadModelHoles SkelRouter.Generated.MyService.Page.Codec SkelRouter.Generated.MyService.Page.Domain SkelRouter.Generated.MyService.Page.EventStream@@ -625,7 +690,7 @@ , containers , effectful-core , hasql-transaction- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , keiro-core , keiro-pgmq@@ -659,7 +724,7 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 @@ -746,8 +811,8 @@ hs-source-dirs: test/conformance-queue main-is: Main.hs other-modules:- Generated.HospitalCapacity.Reservation_work.Queue- Generated.HospitalCapacity.Reservation_work.QueueCodec+ Generated.HospitalCapacity.ReservationWork.Queue+ Generated.HospitalCapacity.ReservationWork.QueueCodec build-depends: , aeson >=2.2 && <2.3@@ -764,9 +829,9 @@ hs-source-dirs: test/conformance-queue-runtime main-is: Main.hs other-modules:- Generated.HospitalCapacity.Reservation_work.Queue- Generated.HospitalCapacity.Reservation_work.QueueCodec- Generated.HospitalCapacity.Reservation_work.QueuePolicy+ Generated.HospitalCapacity.ReservationWork.Queue+ Generated.HospitalCapacity.ReservationWork.QueueCodec+ Generated.HospitalCapacity.ReservationWork.QueuePolicy build-depends: , aeson >=2.2 && <2.3@@ -788,10 +853,10 @@ hs-source-dirs: test/conformance-readmodel-runtime main-is: Main.hs other-modules:- Generated.HospitalCapacity.Transfer_decisions.ReadModel- Generated.HospitalCapacity.Transfer_decisions.ReadModelHarness- Generated.HospitalCapacity.Transfer_decisions.ReadModelTable- HospitalCapacity.Transfer_decisions.ReadModelHoles+ Generated.HospitalCapacity.TransferDecisions.ReadModel+ Generated.HospitalCapacity.TransferDecisions.ReadModelHarness+ Generated.HospitalCapacity.TransferDecisions.ReadModelTable+ HospitalCapacity.TransferDecisions.ReadModelHoles build-depends: , base >=4.21 && <5@@ -810,9 +875,9 @@ hs-source-dirs: test/conformance-dispatch-full main-is: Main.hs other-modules:- Generated.HospitalCapacity.Reservation_work.Queue- Generated.HospitalCapacity.Reservation_work.QueueCodec- Generated.HospitalCapacity.Reservation_work.QueuePolicy+ Generated.HospitalCapacity.ReservationWork.Queue+ Generated.HospitalCapacity.ReservationWork.QueueCodec+ Generated.HospitalCapacity.ReservationWork.QueuePolicy HospitalCapacity.ReservationWork.WorkqueueJob build-depends:@@ -882,7 +947,7 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , shibuya-core , text >=2.1 && <2.2@@ -930,7 +995,7 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , keiro-dsl , shibuya-core@@ -988,7 +1053,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , effectful-core- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , shibuya-core , text >=2.1 && <2.2@@ -1010,14 +1075,14 @@ Generated.TransferRouting.Hospital.Harness Generated.TransferRouting.Hospital.Projection Generated.TransferRouting.Hospital.Transducer- Generated.TransferRouting.Hospital_load.ReadModel- Generated.TransferRouting.Hospital_load.ReadModelHarness- Generated.TransferRouting.Hospital_load.ReadModelTable+ Generated.TransferRouting.HospitalLoad.ReadModel+ Generated.TransferRouting.HospitalLoad.ReadModelHarness+ Generated.TransferRouting.HospitalLoad.ReadModelTable Generated.TransferRouting.HospitalTransferRouter.Router Generated.TransferRouting.HospitalTransferRouter.RouterHarness Generated.TransferRouting.ReplayAudit TransferRouting.Hospital.Holes- TransferRouting.Hospital_load.ReadModelHoles+ TransferRouting.HospitalLoad.ReadModelHoles TransferRouting.HospitalTransferRouter.RouterHoles TransferRouting.HospitalTransferRouter.RouterValue @@ -1026,7 +1091,7 @@ , base >=4.21 && <5 , effectful-core , hasql-transaction- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , kiroku-store , shibuya-core@@ -1073,7 +1138,7 @@ , aeson >=2.2 && <2.3 , base >=4.21 && <5 , directory >=1.3 && <1.4- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro , text >=2.1 && <2.2 @@ -1094,6 +1159,6 @@ build-depends: , aeson >=2.2 && <2.3 , base >=4.21 && <5- , keiki >=0.8 && <0.9+ , keiki >=0.9 && <0.10 , keiro-core , text >=2.1 && <2.2
+ src/Keiro/Dsl/AggregateGenerationPlan.hs view
@@ -0,0 +1,42 @@+module Keiro.Dsl.AggregateGenerationPlan+ ( TransitionLayoutEntry (..),+ transitionLayout,+ groupTransitionLayoutBySource,+ transitionLayoutForSource,+ )+where++import Data.List (mapAccumL)+import Data.Map.Strict qualified as Map+import Keiro.Dsl.Grammar (Name, Transition (..))++data TransitionLayoutEntry = TransitionLayoutEntry+ { layoutDeclarationIndex :: !Int,+ layoutOutgoingIndex :: !Int,+ layoutTransition :: !Transition+ }+ deriving stock (Eq, Show)++transitionLayout :: [Transition] -> [TransitionLayoutEntry]+transitionLayout transitions = snd (mapAccumL buildEntry Map.empty (zip [1 ..] transitions))+ where+ buildEntry counts (declarationIndex, transition) =+ let source = tSource transition+ outgoingIndex = Map.findWithDefault 0 source counts+ counts' = Map.insert source (outgoingIndex + 1) counts+ entry = TransitionLayoutEntry declarationIndex outgoingIndex transition+ in (counts', entry)++groupTransitionLayoutBySource :: [TransitionLayoutEntry] -> [(Name, [TransitionLayoutEntry])]+groupTransitionLayoutBySource entries =+ [ (source, transitionLayoutForSource source entries)+ | source <- firstOccurrences (map (tSource . layoutTransition) entries)+ ]+ where+ firstOccurrences = foldl appendNew []+ appendNew seen value+ | value `elem` seen = seen+ | otherwise = seen ++ [value]++transitionLayoutForSource :: Name -> [TransitionLayoutEntry] -> [TransitionLayoutEntry]+transitionLayoutForSource source = filter ((== source) . tSource . layoutTransition)
src/Keiro/Dsl/AggregateType.hs view
@@ -32,7 +32,6 @@ ) where -import Data.Char (toUpper) import Data.List.NonEmpty qualified as NE import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map@@ -45,6 +44,7 @@ import Data.Time.Format.ISO8601 (iso8601ParseM) import Keiro.Dsl.Grammar import Keiro.Dsl.HaskellImport+import Keiro.Dsl.HaskellName qualified as HaskellName import Keiro.Dsl.NominalType import Keiro.Dsl.TypeGraph import Numeric.Natural (Natural)@@ -261,10 +261,7 @@ mappedHaskell (ResolvedStructural declaration _) = sdHaskell declaration mappedHaskell (ResolvedOpaque declaration) = odHaskell declaration timeImports =- Set.fromList- [ "Data.Time.Calendar (fromGregorian)",- "Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)"- ]+ Set.singleton "Data.Time.Clock (UTCTime)" aggregateSourceReferences :: AggregateHaskellSource -> Set HaskellReference aggregateSourceReferences source = maybe Set.empty Set.singleton (aggregateSourceReference source)@@ -420,9 +417,18 @@ InitialMapped {} -> "initial" pascal :: Text -> Text-pascal value = case T.uncons value of- Just (first, rest) -> T.cons (toUpper first) rest- Nothing -> value+pascal value =+ case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> value+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedTypeSite,+ HaskellName.siteLogicalName = value,+ HaskellName.siteOwner = "aggregate-type",+ HaskellName.siteLine = 0+ } tshow :: Text -> Text tshow = T.pack . show
+ src/Keiro/Dsl/CheckReport.hs view
@@ -0,0 +1,341 @@+-- | Pure construction and JSON encoding for @check@ reports.+--+-- The JSON schema identifier is @keiro-dsl/check-report/1@. Object keys and+-- array element keys are append-only, and consumers must ignore unknown keys.+-- Source and workspace checks share the schema; workspace inputs add a+-- top-level @members@ array. The report's @ok@ field covers parse-successful+-- semantic validation, minimum-language enforcement, denied warnings, and — when+-- the invocation supplies @--coverage-report@ — the structural-coverage+-- findings, which appear as ordinary diagnostic entries at line 0 so one warning+-- policy governs every warning @check@ can emit. The separate coverage report+-- remains the place for the full root and boundary inventory.+--+-- Severity is spelled @"error"@ or @"warning"@ here and in the coverage report;+-- there is exactly one severity vocabulary across keiro-dsl's JSON.+module Keiro.Dsl.CheckReport+ ( CheckReportLanguage (..),+ CheckReportEnforcement (..),+ CheckReportRelated (..),+ CheckReportEntry (..),+ CheckReportSummary (..),+ CheckReportMember (..),+ CheckReport,+ effectiveDenyCodes,+ checkReport,+ workspaceCheckReport,+ workspaceRefusalReport,+ )+where++import Data.Aeson (ToJSON (..), Value, object, (.=))+import Data.List.NonEmpty qualified as NE+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Text (Text)+import Keiro.Dsl.LanguageVersion (LanguageSupport (..), LanguageVersion, SourceLanguage, declaredLanguageVersionMaybe, languageSupportText, sourceFormText)+import Keiro.Dsl.SemanticContract (EffectiveLanguageContract, effectiveContractLanguageVersion, effectiveLanguageSupport, effectiveRuntimeSemantics)+import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode, Severity (..), diagnosticCodeText)+import Keiro.Dsl.Workspace (WorkspaceDiagnostic (..), WorkspaceLocation (..), WorkspaceMember (..), WorkspaceSpec (..), workspaceDisplayPath)++data CheckReportLanguage = CheckReportLanguage+ { reportSourceForm :: !Text,+ reportDeclaredLanguageVersion :: !(Maybe LanguageVersion),+ reportEffectiveLanguageVersion :: !LanguageVersion,+ reportRuntimeSemantics :: !Text,+ reportLanguageSupport :: !LanguageSupport,+ reportStable :: !Bool+ }+ deriving stock (Eq, Show)++data CheckReportEnforcement = CheckReportEnforcement+ { reportMinLanguage :: !(Maybe LanguageVersion),+ reportDenyWarnings :: !Bool,+ reportDenyCodes :: ![DiagnosticCode]+ }+ deriving stock (Eq, Show)++data CheckReportRelated = CheckReportRelated+ { relatedFile :: !FilePath,+ relatedLine :: !Int,+ relatedNote :: !Text+ }+ deriving stock (Eq, Show)++data CheckReportEntry = CheckReportEntry+ { entryCode :: !DiagnosticCode,+ entrySeverity :: !Severity,+ entryFile :: !FilePath,+ entryLine :: !Int,+ entryMessage :: !Text,+ entryDenied :: !Bool,+ entryRelated :: ![CheckReportRelated]+ }+ deriving stock (Eq, Show)++data CheckReportSummary = CheckReportSummary+ { summaryErrors :: !Int,+ summaryWarnings :: !Int,+ summaryDeniedWarnings :: !Int+ }+ deriving stock (Eq, Show)++data CheckReportMember = CheckReportMember+ { memberPath :: !FilePath,+ memberSourceForm :: !Text,+ memberDeclaredLanguageVersion :: !(Maybe LanguageVersion)+ }+ deriving stock (Eq, Show)++data CheckReportKind = SourceReport | WorkspaceReport+ deriving stock (Eq, Show)++data CheckReport = CheckReport+ { reportKind :: !CheckReportKind,+ reportSubject :: !FilePath,+ -- | 'Nothing' only for a workspace that was refused during composition:+ -- there is no composed service, so no effective language contract exists to+ -- describe. Such a report serializes @"language": null@.+ reportLanguage :: !(Maybe CheckReportLanguage),+ reportEnforcement :: !CheckReportEnforcement,+ reportDiagnostics :: ![CheckReportEntry],+ reportSummary :: !CheckReportSummary,+ reportOk :: !Bool,+ reportMembers :: ![CheckReportMember]+ }+ deriving stock (Eq, Show)++-- | Expand one invocation's warning policy to the actual stable-code set used+-- by report entries. @--deny-warnings@ is the union with every registered code.+effectiveDenyCodes :: CheckReportEnforcement -> Set DiagnosticCode+effectiveDenyCodes enforcement+ | reportDenyWarnings enforcement = Set.fromList [minBound .. maxBound]+ | otherwise = Set.fromList (reportDenyCodes enforcement)++checkReport ::+ FilePath ->+ SourceLanguage ->+ EffectiveLanguageContract ->+ CheckReportEnforcement ->+ [Diagnostic] ->+ Set DiagnosticCode ->+ CheckReport+checkReport subject sourceLanguage contract enforcement diagnostics deniedCodes =+ buildReport+ SourceReport+ subject+ (Just (sourceLanguageValue sourceLanguage contract))+ enforcement+ (map (sourceEntry subject deniedCodes) diagnostics)+ []++workspaceCheckReport ::+ FilePath ->+ WorkspaceSpec ->+ EffectiveLanguageContract ->+ CheckReportEnforcement ->+ [WorkspaceDiagnostic] ->+ Set DiagnosticCode ->+ CheckReport+workspaceCheckReport subject workspace contract enforcement diagnostics deniedCodes =+ buildReport+ WorkspaceReport+ subject+ (Just (workspaceLanguageValue contract))+ enforcement+ (map (workspaceEntry subject deniedCodes) diagnostics)+ (map memberValue (wsMembers workspace))++-- | The report for a workspace refused during composition, before any service+-- graph exists. Composition refusals are coded diagnostics, so they belong in+-- the same machine contract as every other refusal; the single-spec path has+-- always written one for the equivalent failure. There is no composed language+-- contract and no member inventory to report, so both are omitted.+workspaceRefusalReport ::+ FilePath ->+ CheckReportEnforcement ->+ NE.NonEmpty WorkspaceDiagnostic ->+ Set DiagnosticCode ->+ CheckReport+workspaceRefusalReport subject enforcement diagnostics deniedCodes =+ buildReport+ WorkspaceReport+ subject+ Nothing+ enforcement+ (map (workspaceEntry subject deniedCodes) (NE.toList diagnostics))+ []++buildReport ::+ CheckReportKind ->+ FilePath ->+ Maybe CheckReportLanguage ->+ CheckReportEnforcement ->+ [CheckReportEntry] ->+ [CheckReportMember] ->+ CheckReport+buildReport kind subject language enforcement entries members =+ CheckReport+ { reportKind = kind,+ reportSubject = subject,+ reportLanguage = language,+ reportEnforcement = enforcement,+ reportDiagnostics = entries,+ reportSummary = summary,+ reportOk = summaryErrors summary == 0 && summaryDeniedWarnings summary == 0,+ reportMembers = members+ }+ where+ summary =+ CheckReportSummary+ { summaryErrors = length [() | entry <- entries, entrySeverity entry == Error],+ summaryWarnings = length [() | entry <- entries, entrySeverity entry == Warning],+ summaryDeniedWarnings = length [() | entry <- entries, entryDenied entry]+ }++sourceLanguageValue :: SourceLanguage -> EffectiveLanguageContract -> CheckReportLanguage+sourceLanguageValue sourceLanguage contract =+ languageValue+ (sourceFormText sourceLanguage)+ (declaredLanguageVersionMaybe sourceLanguage)+ contract++workspaceLanguageValue :: EffectiveLanguageContract -> CheckReportLanguage+workspaceLanguageValue = languageValue "workspace-composed" Nothing++languageValue :: Text -> Maybe LanguageVersion -> EffectiveLanguageContract -> CheckReportLanguage+languageValue sourceForm declared contract =+ CheckReportLanguage+ { reportSourceForm = sourceForm,+ reportDeclaredLanguageVersion = declared,+ reportEffectiveLanguageVersion = effectiveContractLanguageVersion contract,+ reportRuntimeSemantics = effectiveRuntimeSemantics contract,+ reportLanguageSupport = support,+ reportStable = support == Stable+ }+ where+ support = effectiveLanguageSupport contract++memberValue :: WorkspaceMember -> CheckReportMember+memberValue member =+ CheckReportMember+ { memberPath = wmPath member,+ memberSourceForm = sourceFormText (wmSourceLanguage member),+ memberDeclaredLanguageVersion = declaredLanguageVersionMaybe (wmSourceLanguage member)+ }++sourceEntry :: FilePath -> Set DiagnosticCode -> Diagnostic -> CheckReportEntry+sourceEntry subject deniedCodes diagnostic =+ CheckReportEntry+ { entryCode = code diagnostic,+ entrySeverity = severity diagnostic,+ entryFile = subject,+ entryLine = line diagnostic,+ entryMessage = message diagnostic,+ entryDenied = warningDenied deniedCodes (severity diagnostic) (code diagnostic),+ entryRelated =+ [ CheckReportRelated subject relatedLineNumber note+ | (relatedLineNumber, note) <- relatedLocations diagnostic+ ]+ }++workspaceEntry :: FilePath -> Set DiagnosticCode -> WorkspaceDiagnostic -> CheckReportEntry+workspaceEntry subject deniedCodes diagnostic =+ CheckReportEntry+ { entryCode = wdCode diagnostic,+ entrySeverity = wdSeverity diagnostic,+ entryFile = workspaceDisplayPath subject (wlFile primary),+ entryLine = wlLine primary,+ entryMessage = wdMessage diagnostic,+ entryDenied = warningDenied deniedCodes (wdSeverity diagnostic) (wdCode diagnostic),+ entryRelated =+ [ CheckReportRelated+ (workspaceDisplayPath subject (wlFile location))+ (wlLine location)+ (wlRole location)+ | location <- NE.tail (wdLocations diagnostic)+ ]+ }+ where+ primary = NE.head (wdLocations diagnostic)++warningDenied :: Set DiagnosticCode -> Severity -> DiagnosticCode -> Bool+warningDenied deniedCodes severityValue diagnosticCode =+ severityValue == Warning && diagnosticCode `Set.member` deniedCodes++instance ToJSON CheckReport where+ toJSON report =+ object+ ( [ "schema" .= ("keiro-dsl/check-report/1" :: Text),+ "kind" .= kindText (reportKind report),+ "subject" .= reportSubject report,+ "language" .= fmap languageJson (reportLanguage report),+ "enforcement" .= enforcementJson (reportEnforcement report),+ "diagnostics" .= map entryJson (reportDiagnostics report),+ "summary" .= summaryJson (reportSummary report),+ "ok" .= reportOk report+ ]+ <> ["members" .= map memberJson (reportMembers report) | reportKind report == WorkspaceReport]+ )++kindText :: CheckReportKind -> Text+kindText SourceReport = "source"+kindText WorkspaceReport = "workspace"++languageJson :: CheckReportLanguage -> Value+languageJson language =+ object+ [ "sourceForm" .= reportSourceForm language,+ "declaredLanguageVersion" .= reportDeclaredLanguageVersion language,+ "effectiveLanguageVersion" .= reportEffectiveLanguageVersion language,+ "runtimeSemantics" .= reportRuntimeSemantics language,+ "languageSupport" .= languageSupportText (reportLanguageSupport language),+ "stable" .= reportStable language+ ]++enforcementJson :: CheckReportEnforcement -> Value+enforcementJson enforcement =+ object+ [ "minLanguage" .= reportMinLanguage enforcement,+ "denyWarnings" .= reportDenyWarnings enforcement,+ "denyCodes" .= map diagnosticCodeText (Set.toAscList (Set.fromList (reportDenyCodes enforcement)))+ ]++entryJson :: CheckReportEntry -> Value+entryJson entry =+ object+ [ "code" .= diagnosticCodeText (entryCode entry),+ "severity" .= severityText (entrySeverity entry),+ "file" .= entryFile entry,+ "line" .= entryLine entry,+ "message" .= entryMessage entry,+ "denied" .= entryDenied entry,+ "related" .= map relatedJson (entryRelated entry)+ ]++relatedJson :: CheckReportRelated -> Value+relatedJson related =+ object+ [ "file" .= relatedFile related,+ "line" .= relatedLine related,+ "note" .= relatedNote related+ ]++summaryJson :: CheckReportSummary -> Value+summaryJson summary =+ object+ [ "errors" .= summaryErrors summary,+ "warnings" .= summaryWarnings summary,+ "deniedWarnings" .= summaryDeniedWarnings summary+ ]++memberJson :: CheckReportMember -> Value+memberJson member =+ object+ [ "path" .= memberPath member,+ "sourceForm" .= memberSourceForm member,+ "declaredLanguageVersion" .= memberDeclaredLanguageVersion member+ ]++severityText :: Severity -> Text+severityText Error = "error"+severityText Warning = "warning"
src/Keiro/Dsl/ConformancePackage.hs view
@@ -18,6 +18,7 @@ conformanceRecordFileName, planConformancePackage, parseConformancePackageRecord,+ parseLegacyConformancePackageRecord, renderConformancePackageRecord, preflightConformancePackage, executePreparedConformancePackage,@@ -28,17 +29,22 @@ where import Control.Monad (forM)+import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=))+import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as BL import Data.Char (isAlphaNum, isAscii, ord) import Data.List (groupBy, sortOn) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Data.Text.Encoding qualified as Text import Data.Text.IO qualified as TIO import Keiro.Dsl.RuntimePackage (RuntimePackageName (..), isCabalPackageName, mkRuntimePackageName) import Keiro.Dsl.Scaffold (ModuleKind (..), generatedBannerFor, isGeneratedBannerLine) import Keiro.Dsl.SemanticContract (CheckedService (..)) import Keiro.Dsl.ServiceHarness (serviceConformanceFactValues)+import Keiro.Dsl.SidecarNames (conformanceLedgerFileName) import Numeric (showHex) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (isAbsolute, splitDirectories, takeDirectory, (</>))@@ -126,7 +132,7 @@ deriving stock (Eq, Show) conformanceRecordFileName :: FilePath-conformanceRecordFileName = "keiro-dsl-conformance-record.txt"+conformanceRecordFileName = conformanceLedgerFileName conformancePackageDirectory :: ConformanceServiceKey -> FilePath conformancePackageDirectory = \case@@ -209,6 +215,7 @@ " hs-source-dirs: src", " main-is: Main.hs", " other-modules: KeiroConformance.Expectations",+ " ghc-options: -Wall", " build-depends:", " base >=4.18 && <5", " , " <> unRuntimePackageName runtimePackage,@@ -312,25 +319,49 @@ renderConformancePackageRecord :: ConformancePackageRecord -> Text renderConformancePackageRecord record = T.unlines $- [ "schema " <> tshow (cprSchema record),+ [ conformanceLedgerHeader, "service-key " <> renderServiceKey (cprServiceKey record), "runtime-package " <> unRuntimePackageName (cprRuntimePackage record), "facade-module " <> cprFacadeModule record ]- <> ["file " <> kindLabel fileKind <> " " <> T.pack path | (fileKind, path) <- cprFiles record]- where- kindLabel Generated = "generated"- kindLabel HoleStub = "create-once"+ <> ["file " <> encodeConformanceFileRow (ConformanceFileRow fileKind path) | (fileKind, path) <- cprFiles record] parseConformancePackageRecord :: Text -> Maybe ConformancePackageRecord-parseConformancePackageRecord input = do+parseConformancePackageRecord input = case meaningfulLines input of+ header : rows+ | header == conformanceLedgerHeader -> parseRows rows+ _ -> Nothing+ where+ parseRows rows = do+ serviceKey <- exactlyOne [value | row <- rows, Just raw <- [T.stripPrefix "service-key " row], Just value <- [parseServiceKeyText raw]]+ runtimePackage <- exactlyOne [value | row <- rows, Just raw <- [T.stripPrefix "runtime-package " row], Right value <- [mkRuntimePackageName raw]]+ facadeModule <- exactlyOne [value | row <- rows, Just value <- [T.stripPrefix "facade-module " row], not (T.null value), T.all (not . (`elem` [' ', '\t'])) value]+ fileRows <- traverse decodeConformanceFileRow [row | row <- rows, "file " `T.isPrefixOf` row]+ let files = [(conformanceRowKind row, conformanceRowPath row) | row <- fileRows]+ if safeServiceKey serviceKey && safeConformanceFiles files+ then+ Just+ ConformancePackageRecord+ { cprSchema = 1,+ cprServiceKey = serviceKey,+ cprRuntimePackage = runtimePackage,+ cprFacadeModule = facadeModule,+ cprFiles = files+ }+ else Nothing++-- | Parser for the record format emitted before the ledger rename. It remains+-- exported only so the refuse-then-apply sidecar migration can convert an old+-- record without losing its stale-file history.+parseLegacyConformancePackageRecord :: Text -> Maybe ConformancePackageRecord+parseLegacyConformancePackageRecord input = do schema <- exactlyOne [value | ["schema", raw] <- rows, Just value <- [readInt raw]]- serviceKey <- exactlyOne [value | "service-key" : rest <- rows, Just value <- [parseServiceKey rest]]+ serviceKey <- exactlyOne [value | "service-key" : rest <- rows, Just value <- [parseServiceKeyWords rest]] runtimePackage <- exactlyOne [value | ["runtime-package", raw] <- rows, Right value <- [mkRuntimePackageName raw]] facadeModule <- exactlyOne [value | ["facade-module", value] <- rows] files <- traverse parseFile [row | row@(keyword : _) <- rows, keyword == "file"] let knownRows = 4 + length files- if schema == 1 && knownRows == length rows && safeServiceKey serviceKey && safeFiles files+ if schema == 1 && knownRows == length rows && safeServiceKey serviceKey && safeConformanceFiles files then Just ConformancePackageRecord@@ -342,19 +373,68 @@ } else Nothing where- rows = [T.words line | line <- T.lines input, let stripped = T.strip line, not (T.null stripped), not (isGeneratedBannerLine stripped)]- parseServiceKey ["workspace", value] = Just (WorkspaceConformanceService value)- parseServiceKey ["standalone", value] = Just (StandaloneConformanceService value)- parseServiceKey _ = Nothing+ rows = map T.words (meaningfulLines input) parseFile ["file", "generated", path] = Just (Generated, T.unpack path) parseFile ["file", "create-once", path] = Just (HoleStub, T.unpack path) parseFile _ = Nothing- safeFiles files =- all (safeRelativePath . snd) files- && length files == Set.size (Set.fromList (map (T.toCaseFold . T.pack . snd) files)) readInt raw = case reads (T.unpack raw) of [(value, "")] -> Just value _ -> Nothing++conformanceLedgerHeader :: Text+conformanceLedgerHeader = "keiro-dsl conformance ledger v1"++data ConformanceFileRow = ConformanceFileRow+ { conformanceRowKind :: !ModuleKind,+ conformanceRowPath :: !FilePath+ }++instance ToJSON ConformanceFileRow where+ toJSON row =+ object+ [ "kind" .= case conformanceRowKind row of Generated -> "generated" :: Text; HoleStub -> "create-once",+ "path" .= T.pack (conformanceRowPath row)+ ]++instance FromJSON ConformanceFileRow where+ parseJSON = withObject "ConformanceFileRow" $ \fields -> do+ kindLabel <- fields .: "kind"+ fileKind <- case (kindLabel :: Text) of+ "generated" -> pure Generated+ "create-once" -> pure HoleStub+ other -> fail ("unknown conformance file kind: " <> T.unpack other)+ path <- fields .: "path"+ pure (ConformanceFileRow fileKind (T.unpack (path :: Text)))++encodeConformanceFileRow :: ConformanceFileRow -> Text+encodeConformanceFileRow = Text.decodeUtf8 . BL.toStrict . Aeson.encode++decodeConformanceFileRow :: Text -> Maybe ConformanceFileRow+decodeConformanceFileRow row = do+ payload <- T.stripPrefix "file " row+ Aeson.decodeStrict' (Text.encodeUtf8 payload)++meaningfulLines :: Text -> [Text]+meaningfulLines input =+ [ stripped+ | line <- T.lines input,+ let stripped = T.strip line,+ not (T.null stripped),+ not (isGeneratedBannerLine stripped)+ ]++parseServiceKeyText :: Text -> Maybe ConformanceServiceKey+parseServiceKeyText = parseServiceKeyWords . T.words++parseServiceKeyWords :: [Text] -> Maybe ConformanceServiceKey+parseServiceKeyWords ["workspace", value] = Just (WorkspaceConformanceService value)+parseServiceKeyWords ["standalone", value] = Just (StandaloneConformanceService value)+parseServiceKeyWords _ = Nothing++safeConformanceFiles :: [(ModuleKind, FilePath)] -> Bool+safeConformanceFiles files =+ all (safeRelativePath . snd) files+ && length files == Set.size (Set.fromList (map (T.toCaseFold . T.pack . snd) files)) preflightConformancePackage :: FilePath -> Bool -> ConformancePackagePlan -> IO (Either [ConformancePackageFailure] PreparedConformancePackage) preflightConformancePackage out forceGeneratedOverwrite plan = do
src/Keiro/Dsl/Coverage.hs view
@@ -27,6 +27,7 @@ coverageSucceeded, renderCoverageSummary, renderCoverageFinding,+ coverageFindingMessage, writeCoverageReport, ) where@@ -275,11 +276,16 @@ <> "[" <> T.pack (show (findingCode finding)) <> "]: "- <> findingMessage finding- <> rootsSuffix+ <> coverageFindingMessage finding where severityText Error = "error" severityText Warning = "warning"++-- | The finding's message with its root list appended, shared by the rendered+-- stderr line and the machine check-report entry so both say the same thing.+coverageFindingMessage :: CoverageFinding -> Text+coverageFindingMessage finding = findingMessage finding <> rootsSuffix+ where rootsSuffix = case findingRoots finding of [] -> "" roots -> " (roots: " <> T.intercalate ", " roots <> ")"@@ -612,8 +618,11 @@ "message" .= findingMessage finding ] where+ -- One severity vocabulary across every keiro-dsl JSON report. The check+ -- report has always spelled this "warning"; coverage spelled the same+ -- severity "advisory" until ExecPlan 199 unified them. severityValue Error = "error" :: Text- severityValue Warning = "advisory"+ severityValue Warning = "warning" instance ToJSON CoveragePrevious where toJSON previous =
src/Keiro/Dsl/Diff.hs view
@@ -62,8 +62,14 @@ import Data.Text (Text) import Data.Text qualified as T import Keiro.Dsl.AggregateType (typeExprCanonicalName)+import Keiro.Dsl.FieldIdentity+ ( ResolvedFieldIdentity (..),+ resolveAggregateFieldIdentity,+ resolveContractFieldIdentity,+ ) import Keiro.Dsl.FoldFingerprint (FoldSurfaceError, aggregateFoldSurfaceForService) import Keiro.Dsl.Grammar+import Keiro.Dsl.HaskellName qualified as HaskellName import Keiro.Dsl.IdDomain (IdDomainContract (..), contractIdDomainContractFor, idDomainContractFor) import Keiro.Dsl.LanguageVersion (ParsedSource (..), SourceLanguage, declaredLanguageVersionMaybe, languageVersionText, sourceFormText) import Keiro.Dsl.MappedDiff (MappedFinding (..), diffMapped, renderMappedSubject)@@ -267,6 +273,7 @@ classifyCompatibility :: ChangeContext -> DiagnosticCode -> CompatibilityVector classifyCompatibility context code | code == SourceLanguageDeclarationChanged = sourceProvenanceVector+ | code == GeneratedHaskellNameChanged = sourceProvenanceVector {cvConsumerBuild = VAdvisory} | code `elem` [OwnershipMoved, WorkspaceAuthorityChanged] = mappedBuildVector | code == MappedFieldAddedWithDefault = mappedFieldAdditionVector context | code `elem` [MappedArmAdded, MappedEnumValueAdded] = mappedDirectionalAdditionVector context@@ -320,6 +327,7 @@ privateDecodeCodes = [ EvtFieldAddedWithoutBump, EvtFieldRemovedSameVersion,+ EvtFieldWireKeyChanged, EvtFieldTypeChanged, EvtVersionDecreased, EvtVersionMissingUpcaster,@@ -814,7 +822,7 @@ mappedFindingChanges :: MappedFinding -> [Change] mappedFindingChanges finding | mfCode finding == MappedDeclAdded = [mappedDeclarationChange LabelAdditive finding]- | mfCode finding `elem` [MappedHaskellSourceChanged, MappedRecordConstructorChanged, MappedFixturesChanged] =+ | mfCode finding `elem` [MappedHaskellSourceChanged, MappedRecordConstructorChanged, MappedFixturesChanged, GeneratedHaskellNameChanged] = [mappedBuildChange finding] | mfCode finding `elem` [MappedInitialChanged, MappedCanonicalTypeChanged] = mappedBuildChange finding : map (mappedUseChange finding) registerPaths@@ -1073,7 +1081,8 @@ aggregatePairDiff :: Spec -> Spec -> Aggregate -> Aggregate -> [Change] aggregatePairDiff oldSpec newSpec oldAgg newAgg =- concatMap (eventDiff oldAgg newAgg) (aggEvents newAgg)+ commandFieldIdentityDiff oldAgg newAgg+ ++ concatMap (eventDiff oldAgg newAgg) (aggEvents newAgg) ++ removedEvents oldAgg newAgg ++ wireDiff oldAgg newAgg ++ projectionDiff oldAgg newAgg@@ -1164,45 +1173,49 @@ [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) 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)- "event"- (evName e)- EvtVersionMissingUpcaster- ( "version changed from v"- <> tInt (evVersion oldE)- <> " to v"- <> tInt (evVersion e)- <> " without the required contiguous upcaster from v"- <> tInt (evVersion oldE)- )- ]+ selectorChanges oldE+ ++ if evVersion e == evVersion oldE + 1 && evUpcastFrom e `hasSource` 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)+ "event"+ (evName e)+ EvtVersionMissingUpcaster+ ( "version changed from v"+ <> tInt (evVersion oldE)+ <> " to v"+ <> tInt (evVersion e)+ <> " without the required contiguous upcaster from v"+ <> tInt (evVersion oldE)+ )+ ] | evVersion e < evVersion oldE ->- [breaking (aggName newAgg) "event" (evName e) EvtVersionDecreased ("version decreased from v" <> tInt (evVersion oldE) <> " to v" <> tInt (evVersion e))]+ selectorChanges oldE+ ++ [breaking (aggName newAgg) "event" (evName e) EvtVersionDecreased ("version decreased from v" <> tInt (evVersion oldE) <> " to v" <> tInt (evVersion e))] | otherwise ->- sameVersionEventDiff oldAgg newAgg oldE e+ selectorChanges oldE ++ sameVersionEventDiff oldAgg newAgg oldE e+ where+ selectorChanges oldEvent = eventFieldSelectorChanges oldAgg newAgg oldEvent e -- | Events present in the old aggregate but absent in the new one. Removing a -- tag entirely is breaking; deprecation preserves decoding but needs a retained@@ -1228,33 +1241,80 @@ (\transition -> tMode transition == TmReplayOnly && eventName `elem` tEmits transition) (aggTransitions aggregate) -eventFieldSigs :: Aggregate -> Event -> [(Name, Maybe TypeExpr)]+data EventFieldSig = EventFieldSig+ { eventFieldDslName :: !Name,+ eventFieldSelector :: !Name,+ eventFieldWireKey :: !Text,+ eventFieldType :: !(Maybe TypeExpr)+ }+ deriving stock (Eq, Show)++eventFieldSigs :: Aggregate -> Event -> [EventFieldSig] eventFieldSigs agg e = case evBody e of EventFields fs -> map fieldSig fs EventFromCommand cn -> maybe [] (map fieldSig . cmdFields) (find ((== cn) . cmdName) (aggCommands agg)) where- fieldSig f = (aggregateFieldName f, aggregateFieldType f)+ fieldSig field =+ let identity = resolveAggregateFieldIdentity field+ in EventFieldSig+ { eventFieldDslName = fieldDslName identity,+ eventFieldSelector = fieldSelector identity,+ eventFieldWireKey = fieldWireKey identity,+ eventFieldType = aggregateFieldType field+ } +eventFieldSelectorChanges :: Aggregate -> Aggregate -> Event -> Event -> [Change]+eventFieldSelectorChanges oldAggregate newAggregate oldEvent newEvent =+ [ fieldSelectorChange+ (aggName newAggregate)+ "event-field-selector"+ (evName newEvent <> "." <> eventFieldDslName newField)+ (eventFieldSelector oldField)+ (eventFieldSelector newField)+ "event field selector"+ | newField <- eventFieldSigs newAggregate newEvent,+ Just oldField <- [find ((== eventFieldDslName newField) . eventFieldDslName) (eventFieldSigs oldAggregate oldEvent)],+ eventFieldSelector oldField /= eventFieldSelector newField+ ]++commandFieldIdentityDiff :: Aggregate -> Aggregate -> [Change]+commandFieldIdentityDiff oldAggregate newAggregate =+ [ fieldSelectorChange+ (aggName newAggregate)+ "command-field-selector"+ (cmdName newCommand <> "." <> aggregateFieldName newField)+ (fieldSelector (resolveAggregateFieldIdentity oldField))+ (fieldSelector (resolveAggregateFieldIdentity newField))+ "command field selector"+ | newCommand <- aggCommands newAggregate,+ Just oldCommand <- [find ((== cmdName newCommand) . cmdName) (aggCommands oldAggregate)],+ newField <- cmdFields newCommand,+ Just oldField <- [find ((== aggregateFieldName newField) . aggregateFieldName) (cmdFields oldCommand)],+ fieldSelector (resolveAggregateFieldIdentity oldField)+ /= fieldSelector (resolveAggregateFieldIdentity newField)+ ]+ sameVersionEventDiff :: Aggregate -> Aggregate -> Event -> Event -> [Change] sameVersionEventDiff oldAgg newAgg oldE newE = addedChanges ++ removedChanges ++ typeChanges+ ++ wireKeyChanges ++ deprecationChanges ++ retirementChanges where oldFields = eventFieldSigs oldAgg oldE newFields = eventFieldSigs newAgg newE- oldNames = map fst oldFields- newNames = map fst newFields+ oldNames = map eventFieldDslName oldFields+ newNames = map eventFieldDslName newFields added = newNames \\ oldNames removed = oldNames \\ newNames changed =- [ (field, oldType, newType)- | (field, oldType) <- oldFields,- Just newType <- [lookup field newFields],- oldType /= newType+ [ (eventFieldDslName oldField, eventFieldType oldField, eventFieldType newField)+ | oldField <- oldFields,+ Just newField <- [find ((== eventFieldDslName oldField) . eventFieldDslName) newFields],+ eventFieldType oldField /= eventFieldType newField ] addedChanges = [ breaking (aggName newAgg) "event" (evName newE) EvtFieldAddedWithoutBump ("field(s) " <> commas added <> " added at the same version v" <> tInt (evVersion newE) <> " without a version bump or upcaster")@@ -1273,6 +1333,22 @@ ("type changed " <> renderAggregateFieldType oldType <> " -> " <> renderAggregateFieldType newType <> " at the same version v" <> tInt (evVersion newE)) | (field, oldType, newType) <- changed ]+ wireKeyChanges =+ [ breaking+ (aggName newAgg)+ "event-field-wire-key"+ (evName newE <> "." <> eventFieldDslName newField)+ EvtFieldWireKeyChanged+ ( "wire key changed '"+ <> eventFieldWireKey oldField+ <> "' -> '"+ <> eventFieldWireKey newField+ <> "'; restore the old key, or version the event and retain an upcaster"+ )+ | newField <- newFields,+ Just oldField <- [find ((== eventFieldDslName newField) . eventFieldDslName) oldFields],+ eventFieldWireKey oldField /= eventFieldWireKey newField+ ] deprecationChanges | not (evDeprecated oldE) && evDeprecated newE = [ if hasReplayOnlyEmitter newAgg (evName newE)@@ -1570,10 +1646,11 @@ enumUsages :: Spec -> Name -> [Text] enumUsages spec enumType = [aggName agg <> ".reg." <> regName reg | agg <- aggregates, reg <- aggRegs agg, regType reg == TRef enumType]- ++ [ aggName agg <> ".event." <> evName event <> "." <> field+ ++ [ aggName agg <> ".event." <> evName event <> "." <> eventFieldDslName field | agg <- aggregates, event <- aggEvents agg,- (field, Just fieldTypeName) <- eventFieldSigs agg event,+ field <- eventFieldSigs agg event,+ Just fieldTypeName <- [eventFieldType field], fieldTypeName == TRef enumType ] where@@ -1676,6 +1753,8 @@ topicAliasChange ++ removedFieldChanges ++ changedFieldChanges+ ++ selectorFieldChanges+ ++ wireKeyFieldChanges ++ addedFieldChanges where fieldPairs = pairDeclarations cfName (ceFields oldEvent) (ceFields newEvent)@@ -1702,6 +1781,34 @@ | (oldField, newField) <- prMatched fieldPairs, cfType oldField /= cfType newField ]+ selectorFieldChanges =+ [ fieldSelectorChange+ (ctrName newContract)+ "contract-field-selector"+ (ceName newEvent <> "." <> cfName newField)+ (fieldSelector (resolveContractFieldIdentity oldField))+ (fieldSelector (resolveContractFieldIdentity newField))+ "contract field selector"+ | (oldField, newField) <- prMatched fieldPairs,+ fieldSelector (resolveContractFieldIdentity oldField)+ /= fieldSelector (resolveContractFieldIdentity newField)+ ]+ wireKeyFieldChanges =+ [ breaking+ (ctrName newContract)+ "contract-field"+ (ceName newEvent <> "." <> cfName newField)+ ContractFieldChanged+ ( "wire key changed '"+ <> fieldWireKey (resolveContractFieldIdentity oldField)+ <> "' -> '"+ <> fieldWireKey (resolveContractFieldIdentity newField)+ <> "'; restore the old key or revise the public contract with a consumer-first rollout"+ )+ | (oldField, newField) <- prMatched fieldPairs,+ fieldWireKey (resolveContractFieldIdentity oldField)+ /= fieldWireKey (resolveContractFieldIdentity newField)+ ] addedFieldChanges = [ if ctrSchemaVersion newContract > ctrSchemaVersion oldContract then advisory (ctrName newContract) "contract-field" subject ContractSchemaVersionBumped ("field added with schemaVersion bump " <> tInt (ctrSchemaVersion oldContract) <> " -> " <> tInt (ctrSchemaVersion newContract) <> "; coordinate the cross-service rollout")@@ -1721,27 +1828,77 @@ ++ concatMap addedWorkqueueDiff (prAdded paired) ++ concatMap removedWorkqueueDiff (prRemoved paired) where- paired = pairByName nodeWorkqueue wqName env+ paired = pairWorkqueues env +-- | Prefer source identity, then pair a uniquely renamed queue by its complete+-- explicit runtime identity. This permits a generated module-segment rename to+-- remain a build-only finding without guessing when an external identity is+-- ambiguous.+pairWorkqueues :: DiffEnv -> Paired WorkqueueNode+pairWorkqueues env =+ Paired+ { prMatched = exact <> fallback,+ prAdded = [queue | queue <- unmatchedNew, queue `notElem` map snd fallback],+ prRemoved = [queue | queue <- unmatchedOld, queue `notElem` map fst fallback]+ }+ where+ oldQueues = mapMaybe nodeWorkqueue (specNodes (deOld env))+ newQueues = mapMaybe nodeWorkqueue (specNodes (deNew env))+ exact =+ [ (oldQueue, newQueue)+ | newQueue <- newQueues,+ Just oldQueue <- [find ((== wqName newQueue) . wqName) oldQueues]+ ]+ exactOldNames = map (wqName . fst) exact+ exactNewNames = map (wqName . snd) exact+ unmatchedOld = [queue | queue <- oldQueues, wqName queue `notElem` exactOldNames]+ unmatchedNew = [queue | queue <- newQueues, wqName queue `notElem` exactNewNames]+ fallback =+ [ (oldQueue, newQueue)+ | newQueue <- unmatchedNew,+ let matchingOld = [queue | queue <- unmatchedOld, queueIdentity queue == queueIdentity newQueue],+ [oldQueue] <- [matchingOld],+ length [queue | queue <- unmatchedNew, queueIdentity queue == queueIdentity newQueue] == 1+ ]+ workqueuePairDiff :: WorkqueueNode -> WorkqueueNode -> [Change] workqueuePairDiff oldQueue newQueue =- concatMap pairedFieldDiff (prMatched fields)+ generatedNameChanges+ ++ concatMap pairedFieldDiff (prMatched fields) ++ concatMap addedFieldDiff (prAdded fields) ++ concatMap removedFieldDiff (prRemoved fields) ++ queueIdentityDiff oldQueue newQueue ++ queuePolicyDiff oldQueue newQueue where- -- wqPayloadName is a generated Haskell type name, not a wire-visible name.+ generatedNameChanges =+ [ generatedNameChange+ (wqName newQueue)+ "workqueue-module"+ (wqName newQueue)+ (wqName oldQueue)+ (wqName newQueue)+ "workqueue module segment"+ | wqName oldQueue /= wqName newQueue,+ normalizedGeneratedUpper (wqName oldQueue) /= normalizedGeneratedUpper (wqName newQueue)+ ]+ ++ [ generatedNameChange+ (wqName newQueue)+ "workqueue-payload-type"+ (wqPayloadName newQueue)+ (wqPayloadName oldQueue)+ (wqPayloadName newQueue)+ "workqueue payload type"+ | wqPayloadName oldQueue /= wqPayloadName newQueue,+ normalizedGeneratedUpper (wqPayloadName oldQueue) /= normalizedGeneratedUpper (wqPayloadName newQueue)+ ] fields = pairDeclarations wqfName (wqPayload oldQueue) (wqPayload newQueue) pairedFieldDiff (oldField, newField) | 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) 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) CompatibilityStrengthened "new optional field"]+ -- Every payload field is required, so adding one always breaks jobs already+ -- queued under the old shape; there is no optional variant to strengthen.+ addedFieldDiff field = [payloadBreaking field "new required field; queued jobs do not contain it"] 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 @@ -1768,6 +1925,50 @@ queueIdentity :: WorkqueueNode -> (Text, Text, Text, Text) queueIdentity queue = (wqLogical queue, wqPhysical queue, wqDlq queue, wqTable queue) +generatedNameChange :: Name -> Text -> Text -> Text -> Text -> Text -> Change+generatedNameChange node facet subject oldLogical newLogical occurrenceKind =+ advisory+ node+ facet+ subject+ GeneratedHaskellNameChanged+ ( occurrenceKind+ <> " changed '"+ <> normalizedGeneratedUpper oldLogical+ <> "' -> '"+ <> normalizedGeneratedUpper newLogical+ <> "' while wire, SQL, queue, registry, subscription, and persisted runtime identities remain unchanged; re-scaffold and recompile consumers"+ )++fieldSelectorChange :: Name -> Text -> Text -> Text -> Text -> Text -> Change+fieldSelectorChange node facet subject oldSelector newSelector occurrenceKind =+ advisory+ node+ facet+ subject+ GeneratedHaskellNameChanged+ ( occurrenceKind+ <> " changed '"+ <> oldSelector+ <> "' -> '"+ <> newSelector+ <> "' while DSL and wire identities remain unchanged; re-scaffold and recompile consumers"+ )++normalizedGeneratedUpper :: Text -> Text+normalizedGeneratedUpper logicalName =+ case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> logicalName+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedTypeSite,+ HaskellName.siteLogicalName = logicalName,+ HaskellName.siteOwner = "diff",+ HaskellName.siteLine = 0+ }+ queuePolicyDiff :: WorkqueueNode -> WorkqueueNode -> [Change] queuePolicyDiff oldQueue newQueue = ordering ++ provision ++ groupKey where@@ -1847,18 +2048,20 @@ "derived-identity" (procId newProcess) DerivedIdentityChanged- "process name, correlation derivation, saga stream category, timer id prefix, or fired-event-id prefix changed; replays and retries no longer derive the persisted identity"+ "process name, correlation derivation, saga stream category, timer id expression, or fired-event-id expression changed; replays and retries no longer derive the persisted identity" | processIdentity oldProcess /= processIdentity newProcess ] -processIdentity :: ProcessNode -> (Text, Name, Name, Text, Text, Text)+processIdentity :: ProcessNode -> (Text, Name, Name, Text, Text, Name, Text, Name) processIdentity process = ( procName process, corrField (procCorrelate process), corrVia (procCorrelate process), sagaCategory (procSaga process), idePrefix (tmId (procTimer process)),- idePrefix (fireFiredEventId (tmFire (procTimer process)))+ ideField (tmId (procTimer process)),+ idePrefix (fireFiredEventId (tmFire (procTimer process))),+ ideField (fireFiredEventId (tmFire (procTimer process))) ) processTimerWindowDiff :: ProcessNode -> ProcessNode -> [Change]@@ -2253,7 +2456,7 @@ | code `elem` publicCodes -> publicContractContext root paths | code `elem` queueCodes -> queueContext root paths | code `elem` identityCodes -> persistedIdentityContext root paths- | code `elem` [OwnershipMoved, WorkspaceAuthorityChanged] -> consumerBuildContext root paths+ | code `elem` [OwnershipMoved, WorkspaceAuthorityChanged, GeneratedHaskellNameChanged] -> consumerBuildContext root paths | code == AggFoldSurfaceChanged -> snapshotContext root paths | code == EnumCtorAdded -> ChangeContext root paths ContextGeneral label | code `elem` privateCodes -> privateEventContext root paths@@ -2287,6 +2490,7 @@ privateCodes = [ EvtFieldAddedWithoutBump, EvtFieldRemovedSameVersion,+ EvtFieldWireKeyChanged, EvtFieldTypeChanged, EvtVersionDecreased, EvtVersionMissingUpcaster,
src/Keiro/Dsl/DiffReport.hs view
@@ -46,6 +46,7 @@ | RemedyReplayOnlyEdge | RemedyStateCodecBump | RemedyRecompileConsumers+ | RemedyRescaffoldGenerated | RemedyRescaffoldWorkspace | RemedyRunConformance | RemedyNoSemanticAction@@ -182,6 +183,7 @@ remediationFor :: ChangeContext -> DiagnosticCode -> NonEmpty Remedy remediationFor context code | code == SourceLanguageDeclarationChanged = RemedyNoSemanticAction :| []+ | code == GeneratedHaskellNameChanged = RemedyRescaffoldGenerated :| [RemedyRecompileConsumers, RemedyRunConformance] | code == OwnershipMoved = RemedyRescaffoldWorkspace :| [] | code == WorkspaceAuthorityChanged = RemedyRescaffoldWorkspace :| [RemedyRecompileConsumers] | code == AggGuardTightened = RemedyReplayOnlyEdge :| [RemedyRunConformance]@@ -266,6 +268,7 @@ eventDecodeCodes = [ EvtFieldAddedWithoutBump, EvtFieldRemovedSameVersion,+ EvtFieldWireKeyChanged, EvtFieldTypeChanged, EvtVersionDecreased, EvtVersionMissingUpcaster,@@ -310,6 +313,7 @@ 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"+ RemedyRescaffoldGenerated -> "re-run the scaffold so generated modules and create-once imports use the candidate Haskell names" RemedyRescaffoldWorkspace -> "re-run the whole-workspace scaffold so the record's ownership and golden roots follow the change" RemedyRunConformance -> "run the generated conformance and historical fixture suites" RemedyNoSemanticAction -> "no semantic action is required; only source-language provenance changed"
src/Keiro/Dsl/ExplainBindings.hs view
@@ -18,13 +18,13 @@ import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=)) import Data.Bifunctor (first)-import Data.Char (toUpper) 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+import Keiro.Dsl.HaskellName qualified as HaskellName import Keiro.Dsl.IdDomain (idDomainContractFor, idDomainVersion) import Keiro.Dsl.LanguageVersion (SourceLanguage (..)) import Keiro.Dsl.NominalType@@ -380,10 +380,17 @@ CollocatedLeaf -> root <> contextName <> ".Nominal.Shape." <> name <> ".Generated" where root = maybe "" (<> ".") (specModuleRoot spec)- contextName = T.concat (map titleSegment (T.splitOn "-" (specContext spec)))- titleSegment value = case T.uncons value of- Nothing -> ""- Just (initialChar, rest) -> T.cons (toUpper initialChar) rest+ contextName =+ case HaskellName.deriveHaskellName HaskellName.LogicalWireWord site of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> specContext spec+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.ContextModuleSite,+ HaskellName.siteLogicalName = specContext spec,+ HaskellName.siteOwner = "binding-obligation-context",+ HaskellName.siteLine = 0+ } renderBindingObligations :: Text -> [BindingObligation] -> Text renderBindingObligations context obligations = case obligations of
+ src/Keiro/Dsl/FieldIdentity.hs view
@@ -0,0 +1,41 @@+-- | Resolved field identities shared by validation and generation.+--+-- A direct aggregate or contract field has three independent namespaces: its+-- logical DSL identity, its generated Haskell selector, and its serialized wire+-- key. Resolution is total; validation of spelling and collisions belongs to+-- "Keiro.Dsl.Validate".+module Keiro.Dsl.FieldIdentity+ ( ResolvedFieldIdentity (..),+ resolveAggregateFieldIdentity,+ resolveContractFieldIdentity,+ )+where++import Data.Text (Text)+import Keiro.Dsl.Grammar++data ResolvedFieldIdentity = ResolvedFieldIdentity+ { fieldDslName :: !Name,+ fieldSelector :: !Text,+ fieldWireKey :: !Text,+ fieldLoc :: !Loc+ }+ deriving stock (Eq, Show)++resolveAggregateFieldIdentity :: AggregateField -> ResolvedFieldIdentity+resolveAggregateFieldIdentity field =+ ResolvedFieldIdentity+ { fieldDslName = aggregateFieldName field,+ fieldSelector = maybe (aggregateFieldName field) id (aggregateFieldSelector field),+ fieldWireKey = maybe (aggregateFieldName field) id (aggregateFieldWireKey field),+ fieldLoc = aggregateFieldLoc field+ }++resolveContractFieldIdentity :: ContractField -> ResolvedFieldIdentity+resolveContractFieldIdentity field =+ ResolvedFieldIdentity+ { fieldDslName = cfName field,+ fieldSelector = maybe (cfName field) id (cfSelector field),+ fieldWireKey = maybe (cfName field) id (cfWireKey field),+ fieldLoc = cfLoc field+ }
src/Keiro/Dsl/Goldens.hs view
@@ -27,6 +27,7 @@ import Data.Text.IO qualified as TIO import Data.Text.Lazy qualified as TL import Keiro.Dsl.AggregateType+import Keiro.Dsl.FieldIdentity (fieldWireKey) import Keiro.Dsl.Grammar import Keiro.Dsl.NominalType import Keiro.Dsl.Scaffold (Agg (..), ResolvedCtor (..), defaultContext, resolveAgg)@@ -150,7 +151,7 @@ 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]+ : [(Key.fromText (fieldWireKey identity), sampleValue graph spec aggregate fieldType) | (identity, fieldType) <- rcFields event] sampleValue :: Maybe TypeGraph -> Spec -> Agg -> ResolvedAggregateType -> Value sampleValue graph spec _aggregate resolvedType =
src/Keiro/Dsl/Grammar.hs view
@@ -458,11 +458,16 @@ } deriving stock (Eq, Show, Generic) --- | An aggregate command/event field. A bare name reuses the field's inferred--- aggregate type; @name:Type@ accepts the complete 'TypeExpr' grammar so semantic--- validation can reject unsupported direct shapes with a located diagnostic.+-- | An aggregate command/event field. The logical DSL name remains the identity+-- used by expressions and evolution pairing. Optional aliases independently+-- select the generated Haskell record selector and serialized wire key.+-- A bare name reuses the field's inferred aggregate type; @name:Type@ accepts+-- the complete 'TypeExpr' grammar so semantic validation can reject unsupported+-- direct shapes with a located diagnostic. data AggregateField = AggregateField { aggregateFieldName :: !Name,+ aggregateFieldSelector :: !(Maybe Name),+ aggregateFieldWireKey :: !(Maybe Text), aggregateFieldType :: !(Maybe TypeExpr), aggregateFieldLoc :: !Loc }@@ -688,7 +693,8 @@ -- | A deterministic id derivation: @uuidv5 \"prefix:\" <> correlationId@. data IdExpr = IdExpr { ideStrategy :: !IdStrategy,- idePrefix :: !Text+ idePrefix :: !Text,+ ideField :: !Name } deriving stock (Eq, Show, Generic) @@ -813,7 +819,10 @@ data ContractField = ContractField { cfName :: !Name,- cfType :: !ContractType+ cfSelector :: !(Maybe Name),+ cfWireKey :: !(Maybe Text),+ cfType :: !ContractType,+ cfLoc :: !Loc } deriving stock (Eq, Show, Generic) @@ -967,8 +976,7 @@ data WqField = WqField { wqfName :: !Name, wqfWire :: !Text,- wqfType :: !Name,- wqfRequired :: !Bool+ wqfType :: !Name } deriving stock (Eq, Show, Generic)
src/Keiro/Dsl/Harness.hs view
@@ -42,14 +42,15 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Keiro.Dsl.AggregateGenerationPlan import Keiro.Dsl.AggregateType+import Keiro.Dsl.FieldIdentity import Keiro.Dsl.GeneratedHaskellLanguage import Keiro.Dsl.Goldens (GoldenPayload (..)) import Keiro.Dsl.Grammar import Keiro.Dsl.HaskellImport import Keiro.Dsl.IdDomain (idDomainContractFor, idDomainSampleText) import Keiro.Dsl.NominalType-import Keiro.Dsl.ReadModelShape (deriveShapeHash, registryNameFor, subscriptionNameFor) import Keiro.Dsl.Scaffold import Keiro.Dsl.SemanticContract (CheckedService (..), legacyCheckedService) import Keiro.Dsl.TypeGraph@@ -179,44 +180,57 @@ emitReadModelHarness :: Text -> Context -> ReadModelNode -> Text emitReadModelHarness genPrefix ctx readModel =- nl- [ generatedBanner,- "module " <> genPrefix <> ".ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where",- "",- "-- | (fact, expected from notation, actual shared derivation/lowering).",- "readModelFacts :: [(String, String, String)]",- "readModelFacts =",- " [ (\"registryName\", " <> tshow expectedRegistry <> ", " <> tshow actualRegistry <> ")",- " , (\"subscriptionName\", " <> tshow expectedSubscription <> ", " <> tshow actualSubscription <> ")",- " , (\"shapeHash\", " <> tshow (rmShape readModel) <> ", " <> tshow (deriveShapeHash readModel) <> ")",- " , (\"asyncProjectionName\", " <> tshow expectedAsync <> ", " <> tshow actualAsync <> ")",- " , (\"consistency\", " <> tshow consistency <> ", " <> tshow consistency <> ")",- " , (\"strongScope\", " <> tshow scope <> ", " <> tshow scope <> ")",- " ]",- "",- "readModelFactResults :: [(String, Bool)]",- "readModelFactResults =",- " [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]",- "",- "runReadModelFacts :: IO Bool",- "runReadModelFacts = do",- " let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]",- " mapM_ (\\(fact, expected, actual) -> putStrLn (\"FAIL \" <> fact <> \" expected=\" <> show expected <> \" actual=\" <> show actual)) failures",- " pure (null failures)"- ]+ nl $+ renderGeneratedLanguagePragmas [ExtOverloadedRecordDot]+ <> [ generatedBanner,+ "module " <> genPrefix <> ".ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where",+ "",+ "import " <> genPrefix <> ".ReadModel (" <> T.intercalate ", " readModelImports <> ")",+ "import Data.Text qualified as T",+ "import Keiro.ReadModel (ReadModel (..), StrongScope (..))"+ ]+ <> ["import Keiro.Projection (AsyncProjection (..))" | rmFeed readModel == RmSubscription]+ <> [ "",+ "-- | (fact, expected from notation, actual generated runtime value).",+ "readModelFacts :: [(String, String, String)]",+ "readModelFacts =",+ " [ (\"registryName\", " <> tshow expectedRegistry <> ", T.unpack " <> readModelName <> ".name)",+ " , (\"subscriptionName\", " <> tshow expectedSubscription <> ", T.unpack " <> readModelName <> ".subscriptionName)",+ " , (\"shapeHash\", " <> tshow (rmShape readModel) <> ", T.unpack " <> readModelName <> ".shapeHash)",+ asyncFactRow,+ " , (\"consistency\", " <> tshow consistency <> ", show " <> readModelName <> ".defaultConsistency)",+ " , (\"strongScope\", " <> tshow scope <> ", renderStrongScope " <> readModelName <> ".strongScope)",+ " ]",+ "",+ "renderStrongScope :: StrongScope -> String",+ "renderStrongScope EntireLog = \"EntireLog\"",+ "renderStrongScope (CategoryHead categoryName) = \"CategoryHead \" <> T.unpack categoryName",+ "",+ "readModelFactResults :: [(String, Bool)]",+ "readModelFactResults =",+ " [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]",+ "",+ "runReadModelFacts :: IO Bool",+ "runReadModelFacts = do",+ " let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]",+ " mapM_ (\\(fact, expected, actual) -> putStrLn (\"FAIL \" <> fact <> \" expected=\" <> show expected <> \" actual=\" <> show actual)) failures",+ " pure (null failures)"+ ] where+ stem = lowerFirst (pascal (rmName readModel))+ readModelName = stem <> "ReadModel"+ asyncProjectionName = stem <> "AsyncProjection"+ readModelImports = readModelName : [asyncProjectionName | rmFeed readModel == RmSubscription] expectedRegistry = contextName ctx <> "-" <> T.replace "_" "-" (rmName readModel)- actualRegistry = registryNameFor (contextName ctx) readModel expectedSubscription = case rmSubscription readModel of Just name -> name Nothing -> expectedRegistry <> "-sub"- actualSubscription = subscriptionNameFor (contextName ctx) readModel expectedAsync = case rmFeed readModel of RmInline -> "none" RmSubscription -> expectedRegistry <> "-async"- actualAsync = case rmFeed readModel of- RmInline -> "none"- RmSubscription -> actualRegistry <> "-async"+ asyncFactRow = case rmFeed readModel of+ RmInline -> " , (\"asyncProjectionName\", \"none\", \"none\") -- Definitionally inert: inline feeds have no AsyncProjection value."+ RmSubscription -> " , (\"asyncProjectionName\", " <> tshow expectedAsync <> ", T.unpack " <> asyncProjectionName <> ".name)" consistency = case rmConsistency readModel of Strong -> "Strong" Eventual -> "Eventual"@@ -463,14 +477,14 @@ "-- 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 <> ")"+ " [ (\"validateTransducer is empty\", null (validateTransducer defaultValidationOptions " <> lowerFirst nm <> "Transducer))" ]+ ++ clockFreeRows ++ [ " , (\"golden round-trip: " <> rcName e <> "\", roundTrips sampleEvent" <> rcName e <> ")" | e <- aEvents a ] ++ [ " , (\"accepts " <> tCommand t <> " from " <> initialVertex a <> "\", accept" <> tCommand t <> ")"- | t <- initialTransitions a+ | t <- map layoutTransition (initialLiveTransitionEntries a) ] ++ [ " ]" ]@@ -490,21 +504,24 @@ "roundTrips :: " <> nm <> "Event -> Bool", "roundTrips e = parse" <> nm <> "Event (eventType " <> lowerFirst nm <> "Codec e) (encode" <> nm <> "Event e) == Right e" ]+ ++ harnessSampleDeclarations a ++ concatMap (sampleEventDecl a) (aEvents a)- ++ concatMap (acceptDecl a) (initialTransitions a)+ ++ concatMap (acceptDecl a . layoutTransition) (initialLiveTransitionEntries a) ++ concatMap (forwardReplayDecl a) replayTransitions ++ concatMap (upcastDecl goldens a) upcastEvents ++ mappedHarnessDeclarations a ++ nominalHarnessDeclarations 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"+ clockFreeRows =+ if specIsClockFree a+ then [" -- clock-free: spec samples no wall clock (verified at scaffold time)"]+ else [" , (\"clock-free: spec samples no wall clock\", False)"] upcastEvents = [e | e <- aEvents a, rcUpcastFrom e /= Nothing] replayTransitions = [ t- | t <- initialTransitions a,- tMode t == TmLive,+ | entry <- initialLiveTransitionEntries a,+ let t = layoutTransition entry, not (null (tEmits t)) ] coreImports =@@ -637,9 +654,13 @@ exprNames (EAtom (AName n)) = [n] exprNames (EAtom (ABool _)) = [] -initialTransitions :: Agg -> [Transition]-initialTransitions a = case map stName (aStates a) of- (s0 : _) -> [t | t <- aTransitions a, tSource t == s0]+initialLiveTransitionEntries :: Agg -> [TransitionLayoutEntry]+initialLiveTransitionEntries a = case map stName (aStates a) of+ (s0 : _) ->+ [ entry+ | entry <- transitionLayoutForSource s0 (transitionLayout (aTransitions a)),+ tMode (layoutTransition entry) == TmLive+ ] [] -> [] -- | @sampleEvent<Ctor> :: <Agg>Event@ — a sample built from per-field sample@@ -652,6 +673,38 @@ "sampleEvent" <> rcName e <> " = " <> ctorExpr a e ] +harnessSampleDeclarations :: Agg -> [Text]+harnessSampleDeclarations aggregate =+ concatMap generatedIdDeclaration generatedIds <> timeDeclaration+ where+ generatedIds =+ [ nominal+ | nominal <- generatedNominalHarnessTypes aggregate,+ IdRepresentation prefix <- [resolvedNominalRepresentation nominal],+ idDomainContractFor (aLanguageContract aggregate) prefix /= Nothing+ ]+ generatedIdDeclaration nominal = case resolvedNominalRepresentation nominal of+ IdRepresentation prefix -> case idDomainContractFor (aLanguageContract aggregate) prefix of+ Just contract ->+ let typeName = resolvedNominalName nominal+ constantName = generatedIdSampleName nominal+ in [ "",+ constantName <> " :: " <> typeName,+ constantName <> " =",+ " case parse" <> typeName <> " " <> tshow (idDomainSampleText contract) <> " of",+ " Right parsed -> parsed",+ " Left problem -> error (show problem)"+ ]+ Nothing -> []+ _ -> []+ timeDeclaration+ | harnessUsesTime aggregate =+ [ "",+ harnessTimeSampleName aggregate <> " :: UTCTime",+ harnessTimeSampleName aggregate <> " = UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)"+ ]+ | otherwise = []+ acceptDecl :: Agg -> Transition -> [Text] acceptDecl a t = [ "",@@ -707,9 +760,9 @@ -- | @(<Ctor> (<Ctor>Data v1 v2 …))@ with positional sample field values. ctorExpr :: Agg -> ResolvedCtor -> Text ctorExpr a rc =- "(" <> rcName rc <> " (" <> rcName rc <> "Data" <> args <> "))"+ rcName rc <> " (" <> rcName rc <> "Data" <> args <> ")" where- args = T.concat [" " <> sampleValue a fieldName ty | (fieldName, ty) <- rcFields rc]+ args = T.concat [" " <> sampleValue a (fieldDslName identity) ty | (identity, ty) <- rcFields rc] -- | A transition command sample prefers the initial value of a same-named, -- same-typed register only when the guard explicitly equates those two paths.@@ -717,9 +770,9 @@ -- not accidentally inherit a forbidden @Free@ register initial value. commandCtorExpr :: Agg -> Transition -> ResolvedCtor -> Text commandCtorExpr a transition rc =- "(" <> rcName rc <> " (" <> rcName rc <> "Data" <> args <> "))"+ rcName rc <> " (" <> rcName rc <> "Data" <> args <> ")" where- args = T.concat [" " <> commandSampleValue a transition fieldName ty | (fieldName, ty) <- rcFields rc]+ args = T.concat [" " <> commandSampleValue a transition (fieldDslName identity) ty | (identity, ty) <- rcFields rc] commandSampleValue :: Agg -> Transition -> Text -> ResolvedAggregateType -> Text commandSampleValue aggregate transition fieldName fieldType = case fieldType of@@ -737,7 +790,10 @@ _ -> case rrType register of AggregateNominal nominal -> case resolvedNominalOwnership nominal of ConsumerNominal {} -> renderRegisterInitial (rrInitial register)- GeneratedNominal -> fromMaybe (renderRegisterInitial (rrInitial register)) (generatedIdSampleHaskell owner nominal)+ GeneratedNominal ->+ fromMaybe+ (renderRegisterInitial (rrInitial register))+ (generatedIdSampleName nominal <$ generatedIdSampleHaskell owner nominal) _ -> renderRegisterInitial (rrInitial register) guardEquatesCommandAndRegister :: Transition -> Text -> Bool@@ -756,13 +812,14 @@ sampleValue a fieldName ty = case ty of AggregateNominal nominal | GeneratedNominal <- resolvedNominalOwnership nominal,- Just sample <- generatedIdSampleHaskell a nominal ->- sample+ Just _ <- generatedIdSampleHaskell a nominal ->+ generatedIdSampleName nominal AggregateNominal nominal | ConsumerNominal binding <- resolvedNominalOwnership nominal -> "(nominalFixtureDomain (NonEmpty.head (nominalFixtureCases " <> renderHarnessReference a (harnessQualifiedValueReference (consumerNominalFixtures binding)) <> ")))"+ AggregateTime -> harnessTimeSampleName a _ -> fallback where fallback = case fieldCat a ty of@@ -775,14 +832,27 @@ | vertexType == aVertexType a -> initialVertex a _ -> aggregateSampleHaskell (aSymbols a) fieldName ty +generatedIdSampleName :: ResolvedNominalType -> Text+generatedIdSampleName nominal = "sample" <> resolvedNominalName nominal++harnessUsesTime :: Agg -> Bool+harnessUsesTime aggregate =+ any ((== AggregateTime) . snd) (concatMap rcFields (aCommands aggregate <> aEvents aggregate))++harnessTimeSampleName :: Agg -> Text+harnessTimeSampleName aggregate+ | any isObservedAtTime (concatMap rcFields (aCommands aggregate <> aEvents aggregate)) = "sampleObservedAt"+ | otherwise = "sampleTime"+ where+ isObservedAtTime (identity, resolvedType) = fieldDslName identity == "observedAt" && resolvedType == AggregateTime+ aggregateHarnessImports :: Agg -> [Text]-aggregateHarnessImports aggregate =- unique- [ "import " <> imported- | resolvedType <- map snd (concatMap rcFields (aCommands aggregate <> aEvents aggregate)),- AggregateTime <- [resolvedType],- imported <- Set.toAscList (aggregateSourceStaticImports (aggregateConsumerHaskellSource (aSymbols aggregate) resolvedType))- ]+aggregateHarnessImports aggregate+ | harnessUsesTime aggregate =+ [ "import Data.Time.Calendar (fromGregorian)",+ "import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime)"+ ]+ | otherwise = [] nominalHarnessImports :: Agg -> [Text] nominalHarnessImports aggregate@@ -927,23 +997,36 @@ mappedHarnessImports :: Agg -> [Text] mappedHarnessImports aggregate- | null fixtures = []+ | null declarations = [] | 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)"- ]+ ["import Data.Aeson qualified as Aeson" | not (null opaque) || not (null structuralWire)]+ ++ (if null structuralWire then [] else ["import Data.Aeson.Key qualified as AesonKey", "import Data.Aeson.KeyMap qualified as AesonKeyMap"])+ ++ [renderImport "Data.Either" eitherImports | not (null eitherImports)]+ ++ ["import Data.List (nub)", "import Data.List.NonEmpty qualified as NonEmpty"]+ ++ [renderImport "Data.Maybe" ["isJust", "isNothing"] | any shapeUsesMaybe structural]+ ++ ["import Data.Proxy (Proxy (..))" | not (null structural)]+ ++ ["import Data.Text qualified as T"]+ ++ ["import Keiki.Shape (CanonicalTypeName (..))" | not (null structural)]+ ++ [renderImport "Keiro.Codec.Structural" structuralCodecImports] ++ ["import " <> structuralProjectionModuleName (aContext aggregate) <> " qualified as StructuralProjections" | not (null (mappedProjectionSpecs aggregate))] where- fixtures = [mappedFixtures declaration | declaration <- mappedHarnessDeclarationsResolved aggregate]+ declarations = mappedHarnessDeclarationsResolved aggregate+ structural = [(declaration, shape) | ResolvedStructural declaration shape <- declarations]+ opaque = [declaration | ResolvedOpaque declaration <- declarations]+ structuralWire = [(declaration, shape) | ResolvedStructural declaration shape <- codecMappedDeclarations aggregate]+ eitherImports =+ ["isLeft" | wirePoliciesUseIsLeft structuralWire]+ <> ["isRight" | wirePoliciesUseIsRight structuralWire]+ structuralCodecImports =+ ["FixtureCases (..)"]+ <> if null structural then [] else ["bindingDomainRoundTrip", "bindingShapeRoundTrip", "bindingToShape"]+ renderImport moduleName names = "import " <> moduleName <> " (" <> T.intercalate ", " names <> ")"+ shapeUsesMaybe (_, shape) = case shape of+ RRecord _ _ fields -> any (isOptional . rwfType) fields+ RUnion _ arms -> any (maybe False isOptional . rwaPayload) arms+ REnum {} -> False+ isOptional ROptional {} = True+ isOptional _ = False mappedCodecHarnessExports :: Agg -> Text mappedCodecHarnessExports aggregate =@@ -981,27 +1064,18 @@ references ) where- resolvedTypes =- map snd (concatMap rcFields (aCommands aggregate <> aEvents aggregate))- <> map rrType (aRegs aggregate)- aggregateReferences =- Set.unions- [ aggregateSourceReferences (aggregateConsumerHaskellSource (aSymbols aggregate) resolvedType)- | resolvedType <- resolvedTypes- ] consumerNominalReferences = Set.fromList [ reference | nominal <- consumerNominalHarnessTypes aggregate, ConsumerNominal binding <- [resolvedNominalOwnership nominal], reference <-- harnessTypeReference (consumerNominalHaskell binding)- : map- harnessQualifiedValueReference- ( consumerNominalBinding binding- : consumerNominalFixtures binding- : maybeToListHarness (consumerNominalInitial binding)- )+ map+ harnessQualifiedValueReference+ ( consumerNominalBinding binding+ : consumerNominalFixtures binding+ : maybeToListHarness (consumerNominalInitial binding)+ ) ] mappedReferences = Set.fromList@@ -1014,10 +1088,9 @@ harnessQualifiedValueReference (sdBinding structural : sdFixtures structural : maybeToListHarness (sdInitial structural)) ResolvedOpaque opaque ->- harnessTypeReference (odHaskell opaque)- : map- harnessQualifiedValueReference- (odFixtures opaque : maybeToListHarness (odInitial opaque))+ map+ harnessQualifiedValueReference+ (odFixtures opaque : maybeToListHarness (odInitial opaque)) ] shapeReferences = Set.fromList@@ -1031,7 +1104,7 @@ | projection <- mappedProjectionSpecs aggregate, (shapeModule, selector) <- spSelectors projection ]- references = aggregateReferences <> consumerNominalReferences <> mappedReferences <> shapeReferences <> projectionReferences+ references = consumerNominalReferences <> mappedReferences <> shapeReferences <> projectionReferences harnessTypeReference :: HaskellSource -> HaskellReference harnessTypeReference source =@@ -1139,9 +1212,9 @@ opaque = [declaration | ResolvedOpaque declaration <- declarations] structuralWire = [(declaration, shape) | ResolvedStructural declaration shape <- codecMappedDeclarations aggregate] mappedEventFields =- [ (event, fieldName, fieldType, declaration)+ [ (event, fieldDslName identity, fieldType, declaration) | event <- aEvents aggregate,- (fieldName, fieldType) <- rcFields event,+ (identity, fieldType) <- rcFields event, declaration <- maybeToListHarness (mappedDeclaration aggregate fieldType) ] assertionLists =@@ -1293,6 +1366,26 @@ map (unionArmAssertion aggregate declaration encoding) arms <> [unknownFieldAssertion aggregate declaration (ueUnknownFields encoding)] +wirePoliciesUseIsLeft :: [(StructuralDecl, ResolvedMappedShape)] -> Bool+wirePoliciesUseIsLeft = any $ \(_, shape) -> case shape of+ RRecord _ unknownFields fields ->+ unknownFields == RejectUnknown+ || any (\field -> rwfPresence field == POptional && not (isOptionalType (rwfType field))) fields+ REnum {} -> True+ RUnion encoding _ -> ueUnknownFields encoding == RejectUnknown++wirePoliciesUseIsRight :: [(StructuralDecl, ResolvedMappedShape)] -> Bool+wirePoliciesUseIsRight = any $ \(_, shape) -> case shape of+ RRecord _ unknownFields fields ->+ unknownFields == IgnoreUnknown+ || any (\field -> rwfPresence field == POptional && isOptionalType (rwfType field)) fields+ REnum {} -> False+ RUnion encoding _ -> ueUnknownFields encoding == IgnoreUnknown++isOptionalType :: ResolvedTypeExpr -> Bool+isOptionalType ROptional {} = True+isOptionalType _ = False+ recordMissingAssertions :: Agg -> StructuralDecl -> ResolvedWireField -> [Text] recordMissingAssertions aggregate declaration field = [ "(\"wire policy missing default: "@@ -1419,20 +1512,37 @@ 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"- ]+wirePolicyHelpers declarations =+ ( if usesDelete+ then+ [ "",+ "deleteObjectField :: T.Text -> Aeson.Value -> Aeson.Value",+ "deleteObjectField key (Aeson.Object objectValue) = Aeson.Object (AesonKeyMap.delete (AesonKey.fromText key) objectValue)",+ "deleteObjectField _ value = value"+ ]+ else []+ )+ <> [ "",+ "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"+ ]+ <> ( if usesObjectField+ then+ [ "",+ "objectField :: T.Text -> Aeson.Value -> Maybe Aeson.Value",+ "objectField key (Aeson.Object objectValue) = AesonKeyMap.lookup (AesonKey.fromText key) objectValue",+ "objectField _ _ = Nothing"+ ]+ else []+ )+ where+ usesDelete = any hasOptionalRecordField declarations+ usesObjectField = usesDelete || any isUnion declarations+ hasOptionalRecordField (_, RRecord _ _ fields) = any ((== POptional) . rwfPresence) fields+ hasOptionalRecordField _ = False+ isUnion (_, RUnion {}) = True+ isUnion _ = False mappedFixtures :: ResolvedMappedDecl -> QualifiedValueName mappedFixtures (ResolvedStructural declaration _) = sdFixtures declaration@@ -1444,8 +1554,8 @@ where arguments = T.concat- [ " " <> if fieldName == target then replacement else sampleValue aggregate fieldName fieldType- | (fieldName, fieldType) <- rcFields constructor+ [ " " <> if fieldDslName identity == target then replacement else sampleValue aggregate (fieldDslName identity) fieldType+ | (identity, fieldType) <- rcFields constructor ] projectionAssertionDecls :: Agg -> [(StructuralDecl, ResolvedMappedShape)] -> [Text]
src/Keiro/Dsl/HaskellImport.hs view
@@ -24,6 +24,7 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Keiro.Dsl.HaskellName (haskellKeywords) data HaskellNamespace = TypeNamespace@@ -231,42 +232,3 @@ asciiDigit :: Char -> Bool asciiDigit character = character >= '0' && character <= '9'--haskellKeywords :: Set Text-haskellKeywords =- Set.fromList- [ "as",- "case",- "class",- "data",- "default",- "deriving",- "do",- "else",- "family",- "foreign",- "forall",- "if",- "import",- "in",- "infix",- "infixl",- "infixr",- "instance",- "let",- "mdo",- "module",- "newtype",- "of",- "proc",- "qualified",- "rec",- "safe",- "signature",- "stock",- "then",- "type",- "unsafe",- "via",- "where"- ]
+ src/Keiro/Dsl/HaskellName.hs view
@@ -0,0 +1,351 @@+-- | Checked names for every Haskell declaration generated by keiro-dsl.+--+-- Logical DSL names and external spellings remain ordinary 'Text'. A value+-- crosses into generated Haskell only through the total constructors in this+-- module, which share one ASCII word-segmentation policy and one keyword set.+module Keiro.Dsl.HaskellName+ ( UpperCamelName,+ LowerCamelName,+ HaskellModuleSegment,+ HaskellModuleName,+ DerivedHaskellName (..),+ NameSourceKind (..),+ NameSiteKind (..),+ HaskellOccurrenceSpace (..),+ NameSite (..),+ HaskellOccurrenceKey (..),+ PlannedOccurrence (..),+ HaskellNameError (..),+ GeneratedHaskellNamingEdition (..),+ currentGeneratedHaskellNamingEdition,+ renderGeneratedHaskellNamingEdition,+ parseGeneratedHaskellNamingEdition,+ deriveHaskellName,+ checkedModuleSegment,+ checkedModuleName,+ checkedUpperOccurrence,+ checkedLowerOccurrence,+ plannedOccurrence,+ detectNameCollisions,+ renderUpperCamelName,+ renderLowerCamelName,+ renderModuleSegment,+ renderModuleName,+ haskellKeywords,+ )+where++import Data.Char (toLower, toUpper)+import Data.List (sortOn)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map.Strict qualified as Map+import Data.Maybe (listToMaybe)+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T++newtype UpperCamelName = UpperCamelName Text+ deriving stock (Eq, Ord, Show)++newtype LowerCamelName = LowerCamelName Text+ deriving stock (Eq, Ord, Show)++newtype HaskellModuleSegment = HaskellModuleSegment Text+ deriving stock (Eq, Ord, Show)++newtype HaskellModuleName = HaskellModuleName Text+ deriving stock (Eq, Ord, Show)++data DerivedHaskellName = DerivedHaskellName+ { upperCamel :: !UpperCamelName,+ lowerCamel :: !LowerCamelName+ }+ deriving stock (Eq, Show)++data NameSourceKind+ = LogicalIdentifier+ | LogicalWireWord+ | ExplicitHaskellName+ deriving stock (Eq, Ord, Show)++-- | Stable declaration categories used in diagnostics and source-move roles.+data NameSiteKind+ = ContextModuleSite+ | NodeModuleSite+ | GeneratedTypeSite+ | GeneratedConstructorSite+ | GeneratedValueSite+ | GeneratedFieldSite+ | GeneratedHelperSite+ | ImportAliasSite+ deriving stock (Eq, Ord, Enum, Bounded, Show)++data HaskellOccurrenceSpace+ = ModuleSpace+ | TypeSpace+ | ConstructorSpace+ | ValueSpace+ | FieldSpace+ deriving stock (Eq, Ord, Enum, Bounded, Show)++-- | The source declaration responsible for a generated occurrence. The+-- owner and kind form a stable identity; the line is evidence, not identity.+data NameSite = NameSite+ { siteKind :: !NameSiteKind,+ siteLogicalName :: !Text,+ siteOwner :: !Text,+ siteLine :: !Int+ }+ deriving stock (Eq, Ord, Show)++-- | A collision key. Module names are compared case-insensitively because+-- generated trees must remain safe on case-insensitive filesystems. Scope is+-- normally empty; record fields use their owning record so+-- DuplicateRecordFields can keep identical selectors on different records.+data HaskellOccurrenceKey = HaskellOccurrenceKey+ { occurrenceModule :: !Text,+ occurrenceSpace :: !HaskellOccurrenceSpace,+ occurrenceScope :: !Text,+ occurrenceName :: !Text+ }+ deriving stock (Eq, Ord, Show)++data PlannedOccurrence = PlannedOccurrence+ { plannedOccurrenceKey :: !HaskellOccurrenceKey,+ plannedOccurrenceSite :: !NameSite+ }+ deriving stock (Eq, Ord, Show)++data HaskellNameError+ = EmptyNameSegment !NameSite+ | UnsafeNameSeparator !NameSite !Text+ | ReservedGeneratedOccurrence !NameSite !Text+ | NormalizedNameCollision !HaskellOccurrenceKey !(NonEmpty NameSite)+ | InvalidExplicitHaskellName !NameSite !Text+ deriving stock (Eq, Show)++data GeneratedHaskellNamingEdition+ = LegacyNamingV1+ | IdiomaticNamingV1+ deriving stock (Eq, Ord, Show)++currentGeneratedHaskellNamingEdition :: GeneratedHaskellNamingEdition+currentGeneratedHaskellNamingEdition = IdiomaticNamingV1++renderGeneratedHaskellNamingEdition :: GeneratedHaskellNamingEdition -> Text+renderGeneratedHaskellNamingEdition = \case+ LegacyNamingV1 -> "legacy-v1"+ IdiomaticNamingV1 -> "idiomatic-v1"++parseGeneratedHaskellNamingEdition :: Text -> Maybe GeneratedHaskellNamingEdition+parseGeneratedHaskellNamingEdition = \case+ "legacy-v1" -> Just LegacyNamingV1+ "idiomatic-v1" -> Just IdiomaticNamingV1+ _ -> Nothing++renderUpperCamelName :: UpperCamelName -> Text+renderUpperCamelName (UpperCamelName value) = value++renderLowerCamelName :: LowerCamelName -> Text+renderLowerCamelName (LowerCamelName value) = value++renderModuleSegment :: HaskellModuleSegment -> Text+renderModuleSegment (HaskellModuleSegment value) = value++renderModuleName :: HaskellModuleName -> Text+renderModuleName (HaskellModuleName value) = value++-- | Derive both Haskell cases from exactly the same word list. Explicit+-- Haskell references are checked by the namespace-specific smart constructors+-- below and are intentionally never re-cased here.+deriveHaskellName :: NameSourceKind -> NameSite -> Either HaskellNameError DerivedHaskellName+deriveHaskellName source site+ | source == ExplicitHaskellName = Left (InvalidExplicitHaskellName site (siteLogicalName site))+ | otherwise = do+ words' <- segmentLogicalName (source == LogicalWireWord) site+ case words' of+ [] -> Left (EmptyNameSegment site)+ firstWord : restWords -> do+ let upper = T.concat (map upperWord words')+ lower = lowerFirstWord firstWord <> T.concat (map upperWord restWords)+ upperName <- checkedUpperOccurrence site upper+ lowerName <- checkedLowerOccurrence site lower+ pure DerivedHaskellName {upperCamel = upperName, lowerCamel = lowerName}++checkedModuleSegment :: NameSite -> Text -> Either HaskellNameError HaskellModuleSegment+checkedModuleSegment site candidate+ | validUpperIdentifier False candidate = Right (HaskellModuleSegment candidate)+ | otherwise = Left (InvalidExplicitHaskellName site candidate)++checkedModuleName :: NameSite -> Text -> Either HaskellNameError HaskellModuleName+checkedModuleName site candidate+ | not (null components) && all (validUpperIdentifier True) components = Right (HaskellModuleName candidate)+ | otherwise = Left (InvalidExplicitHaskellName site candidate)+ where+ components = T.splitOn "." candidate++checkedUpperOccurrence :: NameSite -> Text -> Either HaskellNameError UpperCamelName+checkedUpperOccurrence site candidate+ | validUpperIdentifier False candidate = Right (UpperCamelName candidate)+ | otherwise = Left (InvalidExplicitHaskellName site candidate)++checkedLowerOccurrence :: NameSite -> Text -> Either HaskellNameError LowerCamelName+checkedLowerOccurrence site candidate+ | Set.member candidate haskellKeywords = Left (ReservedGeneratedOccurrence site candidate)+ | validLowerIdentifier candidate = Right (LowerCamelName candidate)+ | otherwise = Left (InvalidExplicitHaskellName site candidate)++-- | Construct a collision input after the occurrence has passed its checked+-- wrapper. Field scope is the owning record name; use the empty string for+-- every other namespace.+plannedOccurrence :: Text -> HaskellOccurrenceSpace -> Text -> Text -> NameSite -> PlannedOccurrence+plannedOccurrence targetModule space scope rendered site =+ PlannedOccurrence+ { plannedOccurrenceKey =+ HaskellOccurrenceKey+ { occurrenceModule = T.toCaseFold targetModule,+ occurrenceSpace = space,+ occurrenceScope = T.toCaseFold scope,+ occurrenceName = case space of+ ModuleSpace -> T.toCaseFold rendered+ _ -> rendered+ },+ plannedOccurrenceSite = site+ }++-- | Report deterministic collisions independently of declaration traversal+-- order. Repeated inventory entries for the same stable site are collapsed.+detectNameCollisions :: [PlannedOccurrence] -> [HaskellNameError]+detectNameCollisions occurrences =+ [ NormalizedNameCollision key (first :| rest)+ | (key, sites) <- Map.toAscList grouped,+ let orderedSites = Set.toAscList sites,+ first : rest <- [orderedSites]+ ]+ where+ grouped =+ Map.filter ((> 1) . Set.size) $+ Map.fromListWith+ Set.union+ [ (plannedOccurrenceKey occurrence, Set.singleton (plannedOccurrenceSite occurrence))+ | occurrence <- sortOn plannedOccurrenceKey occurrences+ ]++segmentLogicalName :: Bool -> NameSite -> Either HaskellNameError [Text]+segmentLogicalName allowHyphen site+ | T.null raw = Left (EmptyNameSegment site)+ | badUnderscore = Left (UnsafeNameSeparator site "underscore separators must be single and may not lead or trail a name")+ | not allowHyphen && T.any (== '-') raw = Left (UnsafeNameSeparator site "hyphens are allowed only in wire-word sources")+ | allowHyphen && badHyphen = Left (UnsafeNameSeparator site "hyphen separators must be single and may not lead or trail a name")+ | any T.null separated = Left (EmptyNameSegment site)+ | otherwise = Right (concatMap splitCamelWord separated)+ where+ raw = siteLogicalName site+ badUnderscore =+ T.isPrefixOf "_" raw+ || T.isSuffixOf "_" raw+ || T.isInfixOf "__" raw+ badHyphen =+ T.isPrefixOf "-" raw+ || T.isSuffixOf "-" raw+ || T.isInfixOf "--" raw+ separated = T.split (`elem` separators) raw+ separators = if allowHyphen then ['_', '-'] else ['_']++splitCamelWord :: Text -> [Text]+splitCamelWord value = map T.pack (go [] (T.unpack value))+ where+ go current [] = [reverse current | not (null current)]+ go [] (character : rest) = go [character] rest+ go current@(previous : _) remaining@(character : rest)+ | camelBoundary previous character (listToMaybe rest) = reverse current : go [] remaining+ | otherwise = go (character : current) rest++camelBoundary :: Char -> Char -> Maybe Char -> Bool+camelBoundary previous current next =+ (asciiLower previous || asciiDigit previous) && asciiUpper current+ || asciiUpper previous && asciiUpper current && maybe False asciiLower next++upperWord :: Text -> Text+upperWord word = case T.uncons word of+ Nothing -> ""+ Just (first, rest) -> T.cons (asciiToUpper first) rest++lowerFirstWord :: Text -> Text+lowerFirstWord word+ | hasUpper && T.all (\character -> not (asciiLower character)) word = T.map asciiToLower word+ | otherwise = case T.uncons word of+ Nothing -> ""+ Just (first, rest) -> T.cons (asciiToLower first) rest+ where+ hasUpper = T.any asciiUpper word++validUpperIdentifier :: Bool -> Text -> Bool+validUpperIdentifier allowUnderscore name = case T.uncons name of+ Just (first, rest) -> asciiUpper first && T.all (identifierTail allowUnderscore) rest+ Nothing -> False++validLowerIdentifier :: Text -> Bool+validLowerIdentifier name = case T.uncons name of+ Just (first, rest) -> asciiLower first && T.all (identifierTail False) rest+ Nothing -> False++identifierTail :: Bool -> Char -> Bool+identifierTail allowUnderscore character =+ asciiUpper character+ || asciiLower character+ || asciiDigit character+ || character == '\''+ || allowUnderscore && character == '_'++asciiUpper :: Char -> Bool+asciiUpper character = character >= 'A' && character <= 'Z'++asciiLower :: Char -> Bool+asciiLower character = character >= 'a' && character <= 'z'++asciiDigit :: Char -> Bool+asciiDigit character = character >= '0' && character <= '9'++asciiToUpper :: Char -> Char+asciiToUpper character+ | asciiLower character = toUpper character+ | otherwise = character++asciiToLower :: Char -> Char+asciiToLower character+ | asciiUpper character = toLower character+ | otherwise = character++-- | Words GHC rejects as term-level identifiers under the generated manifest+-- contract (GHC2024 plus the closed local-extension set in ADR 0019). Contextual+-- words are deliberately absent; widening this refusal set requires compile+-- evidence against that exact contract.+haskellKeywords :: Set Text+haskellKeywords =+ Set.fromList+ [ "case",+ "class",+ "data",+ "default",+ "deriving",+ "do",+ "else",+ "foreign",+ "forall",+ "if",+ "import",+ "in",+ "infix",+ "infixl",+ "infixr",+ "instance",+ "let",+ "module",+ "newtype",+ "of",+ "then",+ "type",+ "where"+ ]
+ src/Keiro/Dsl/HaskellSourceMove.hs view
@@ -0,0 +1,214 @@+-- | Pure planning and token-aware rewriting for generated-Haskell name moves.+module Keiro.Dsl.HaskellSourceMove+ ( SourceMove (..),+ SourceMoveError (..),+ planSourceMoves,+ rewriteHaskellModuleReferences,+ moduleNameFromPath,+ contentDigest,+ )+where++import Data.Bits (xor)+import Data.ByteString qualified as BS+import Data.List (sortBy, sortOn)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Ord (Down (..), comparing)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as TE+import Data.Word (Word64, Word8)+import Keiro.Dsl.HaskellName qualified as HaskellName+import Keiro.Dsl.Scaffold (ModuleKind, ModuleRole, ScaffoldModule, modulePath, moduleRole)+import Numeric (showHex)++data SourceMove = SourceMove+ { moveRole :: !ModuleRole,+ moveKind :: !ModuleKind,+ moveOldModule :: !Text,+ moveNewModule :: !Text,+ moveOldPath :: !FilePath,+ moveNewPath :: !FilePath,+ moveBackupPath :: !FilePath,+ -- | Digest of the exact legacy bytes. Planning cannot populate it because+ -- it is pure and filesystem-independent; the complete migration preflight+ -- hydrates it before a move is reported or applied.+ moveContentDigest :: !(Maybe Text),+ -- | Digest of the token-aware transformed bytes, used with the durable+ -- migration state to recognize an installed or prepared crash state.+ moveTransformedDigest :: !(Maybe Text)+ }+ deriving stock (Eq, Show)++data SourceMoveError+ = AmbiguousLegacyModule !FilePath ![FilePath]+ | AmbiguousModuleRole !ModuleRole ![FilePath]+ | MalformedHaskellLexicalInput !Text+ deriving stock (Eq, Show)++-- | Pair prior paths with the current artifact plan. New records provide a+-- stable role; historical records fall back to exact legacy-name+-- normalization. Only actual path changes become moves.+planSourceMoves ::+ [(Maybe ModuleRole, ModuleKind, FilePath)] ->+ [ScaffoldModule] ->+ Either (NonEmpty SourceMoveError) [SourceMove]+planSourceMoves previous current =+ case errors of+ first : rest -> Left (first :| rest)+ [] -> Right (sortOn moveOldPath moves)+ where+ currentByRole = Map.fromListWith (<>) [(moduleRole scaffoldModule, [scaffoldModule]) | scaffoldModule <- current]+ currentByModule = Map.fromListWith (<>) [(moduleNameFromPath (modulePath scaffoldModule), [scaffoldModule]) | scaffoldModule <- current]+ planned = map pair previous+ errors = [err | Left err <- planned]+ moves = [move | Right (Just move) <- planned]++ pair (previousRole, previousKind, previousPath) = do+ candidate <- case previousRole of+ Just role -> uniqueRole role (Map.findWithDefault [] role currentByRole)+ Nothing ->+ let normalized = normalizeLegacyModuleName (moduleNameFromPath previousPath)+ in uniqueLegacy previousPath (Map.findWithDefault [] normalized currentByModule)+ case candidate of+ Nothing -> Right Nothing+ Just currentModule+ | modulePath currentModule == previousPath -> Right Nothing+ | normalizeLegacyModuleName (moduleNameFromPath previousPath) /= moduleNameFromPath (modulePath currentModule) -> Right Nothing+ | otherwise ->+ Right . Just $+ SourceMove+ { moveRole = moduleRole currentModule,+ moveKind = previousKind,+ moveOldModule = moduleNameFromPath previousPath,+ moveNewModule = moduleNameFromPath (modulePath currentModule),+ moveOldPath = previousPath,+ moveNewPath = modulePath currentModule,+ moveBackupPath = ".keiro-dsl-name-migrations/legacy-v1-to-idiomatic-v1/" <> previousPath,+ moveContentDigest = Nothing,+ moveTransformedDigest = Nothing+ }++ uniqueRole _ [] = Right Nothing+ uniqueRole _ [candidate] = Right (Just candidate)+ uniqueRole role candidates = Left (AmbiguousModuleRole role (map modulePath candidates))+ uniqueLegacy _ [] = Right Nothing+ uniqueLegacy _ [candidate] = Right (Just candidate)+ uniqueLegacy path candidates = Left (AmbiguousLegacyModule path (map modulePath candidates))++moduleNameFromPath :: FilePath -> Text+moduleNameFromPath = T.replace "/" "." . T.dropEnd 3 . T.pack++normalizeLegacyModuleName :: Text -> Text+normalizeLegacyModuleName = T.intercalate "." . map normalizeSegment . T.splitOn "."+ where+ normalizeSegment segment =+ case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> segment+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.NodeModuleSite,+ HaskellName.siteLogicalName = segment,+ HaskellName.siteOwner = "legacy-module-segment",+ HaskellName.siteLine = 0+ }++data LexState+ = Code+ | LineComment+ | BlockComment !Int+ | StringLiteral+ | CharacterLiteral+ deriving stock (Eq, Show)++-- | Rewrite exact module token sequences in Haskell code while preserving+-- comments, strings, character literals, and nested block comments byte for+-- byte. Longer old modules win when one is a prefix of another.+rewriteHaskellModuleReferences :: Map Text Text -> Text -> Either SourceMoveError Text+rewriteHaskellModuleReferences replacements source =+ fmap (T.pack . reverse) (go Code Nothing (T.unpack source) [])+ where+ ordered =+ sortBy+ (comparing (Down . length . fst))+ [(T.unpack old, T.unpack new) | (old, new) <- Map.toAscList replacements, old /= new]++ go state previous input output = case (state, input) of+ (Code, []) -> Right output+ (LineComment, []) -> Right output+ (BlockComment _, []) -> Left (MalformedHaskellLexicalInput "unterminated block comment")+ (StringLiteral, []) -> Left (MalformedHaskellLexicalInput "unterminated string literal")+ (CharacterLiteral, []) -> Left (MalformedHaskellLexicalInput "unterminated character literal")+ (Code, '-' : '-' : rest) -> go LineComment (Just '-') rest ('-' : '-' : output)+ (Code, '{' : '-' : rest) -> go (BlockComment 1) (Just '-') rest ('-' : '{' : output)+ (Code, '"' : rest) -> go StringLiteral (Just '"') rest ('"' : output)+ (Code, '\'' : rest)+ | looksLikeCharacterLiteral rest -> go CharacterLiteral (Just '\'') rest ('\'' : output)+ (Code, remaining@(character : rest)) -> case firstReplacement previous remaining ordered of+ Just (old, new) ->+ let untouched = drop (length old) remaining+ newOutput = reverse new <> output+ newPrevious = case reverse old of oldLast : _ -> Just oldLast; [] -> previous+ in go Code newPrevious untouched newOutput+ Nothing -> go Code (Just character) rest (character : output)+ (LineComment, '\n' : rest) -> go Code (Just '\n') rest ('\n' : output)+ (LineComment, character : rest) -> go LineComment (Just character) rest (character : output)+ (BlockComment depth, '{' : '-' : rest) -> go (BlockComment (depth + 1)) (Just '-') rest ('-' : '{' : output)+ (BlockComment 1, '-' : '}' : rest) -> go Code (Just '}') rest ('}' : '-' : output)+ (BlockComment depth, '-' : '}' : rest) -> go (BlockComment (depth - 1)) (Just '}') rest ('}' : '-' : output)+ (BlockComment depth, character : rest) -> go (BlockComment depth) (Just character) rest (character : output)+ (StringLiteral, '\\' : escaped : rest) -> go StringLiteral (Just escaped) rest (escaped : '\\' : output)+ (StringLiteral, '"' : rest) -> go Code (Just '"') rest ('"' : output)+ (StringLiteral, character : rest) -> go StringLiteral (Just character) rest (character : output)+ (CharacterLiteral, '\\' : escaped : rest) -> go CharacterLiteral (Just escaped) rest (escaped : '\\' : output)+ (CharacterLiteral, '\'' : rest) -> go Code (Just '\'') rest ('\'' : output)+ (CharacterLiteral, character : rest) -> go CharacterLiteral (Just character) rest (character : output)++ firstReplacement previous remaining = firstMatch+ where+ firstMatch [] = Nothing+ firstMatch ((old, new) : rest)+ | old `isPrefixOfString` remaining,+ maybe True (not . moduleTokenCharacter) previous,+ afterBoundary old remaining =+ Just (old, new)+ | otherwise = firstMatch rest++ afterBoundary old remaining = case drop (length old) remaining of+ [] -> True+ next : _ -> not (identifierCharacter next)++ moduleTokenCharacter character = identifierCharacter character || character == '.'+ identifierCharacter character =+ (character >= 'A' && character <= 'Z')+ || (character >= 'a' && character <= 'z')+ || (character >= '0' && character <= '9')+ || character == '_'+ || character == '\''++ looksLikeCharacterLiteral rest = case rest of+ '\\' : _escaped : '\'' : _ -> True+ _character : '\'' : _ -> True+ _ -> False++isPrefixOfString :: String -> String -> Bool+isPrefixOfString [] _ = True+isPrefixOfString _ [] = False+isPrefixOfString (left : leftRest) (right : rightRest) = left == right && isPrefixOfString leftRest rightRest++-- | Stable UTF-8 content digest used by the local migration journal. FNV-1a+-- is sufficient here: this is an integrity/recovery fingerprint rather than a+-- security boundary, and keeping it local avoids changing the package surface.+contentDigest :: Text -> Text+contentDigest source = T.pack (pad16 (showHex digest ""))+ where+ digest = BS.foldl' step fnvOffset (TE.encodeUtf8 source)+ step :: Word64 -> Word8 -> Word64+ step hash byte = (hash `xor` fromIntegral byte) * fnvPrime+ fnvOffset = 14695981039346656037 :: Word64+ fnvPrime = 1099511628211 :: Word64+ pad16 rendered = replicate (16 - length rendered) '0' <> rendered
src/Keiro/Dsl/LanguageVersion.hs view
@@ -219,7 +219,7 @@ LanguageDefinition version1 Nothing LanguageBodyParserV1 profileV1 runtimeProfileV1 CompatibilityOnly :| [ LanguageDefinition version2 (Just version1) LanguageBodyParserV2 profileV2 runtimeProfileV1 CompatibilityOnly, LanguageDefinition version3 (Just version2) LanguageBodyParserV2 profileV2 runtimeProfileV2 CompatibilityOnly,- LanguageDefinition version4 (Just version3) LanguageBodyParserV2 profileV2 runtimeProfileV3 Stable+ LanguageDefinition version4 (Just version3) LanguageBodyParserV2 profileV3 runtimeProfileV3 Stable ] profileV1 :: SyntaxProfile@@ -237,6 +237,15 @@ ] ) +profileV3 :: SyntaxProfile+profileV3 =+ SyntaxProfile+ "keiro-dsl/syntax-profile/3"+ ( Set.insert+ FieldAliasSyntax+ (profileFeatures profileV2)+ )+ runtimeProfileV1 :: RuntimeSemanticsProfile runtimeProfileV1 = RuntimeSemanticsProfile@@ -287,6 +296,7 @@ | IntegerScalarSyntax | TypedAggregateExpressionSyntax | ExplicitTransitionImplementationSyntax+ | FieldAliasSyntax deriving stock (Eq, Ord, Show) -- | The first released contract that owns each grammar feature.
src/Keiro/Dsl/MappedDiff.hs view
@@ -20,6 +20,7 @@ import Data.Text (Text) import Data.Text qualified as T import Keiro.Dsl.Grammar+import Keiro.Dsl.HaskellName qualified as HaskellName import Keiro.Dsl.TypeGraph import Keiro.Dsl.Validate (DiagnosticCode (..)) @@ -324,14 +325,30 @@ paths declaration leaf- MappedWireKeyChanged- ("wire key changed '" <> rwfKey oldField <> "' -> '" <> rwfKey newField <> "'; version and upcast every affected private event root")- | rwfKey oldField /= rwfKey newField+ GeneratedHaskellNameChanged+ ( "generated record selector changed '"+ <> normalizedGeneratedLower (rwfHaskell oldField)+ <> "' -> '"+ <> normalizedGeneratedLower (rwfHaskell newField)+ <> "' while wire key '"+ <> rwfKey newField+ <> "' remains unchanged; re-scaffold and recompile consumers"+ )+ | rwfKey oldField == rwfKey newField,+ normalizedGeneratedLower (rwfHaskell oldField) /= normalizedGeneratedLower (rwfHaskell 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@@ -359,6 +376,20 @@ "on-missing default changed; the same historical bytes now construct a different consumer value" ] _ -> []++normalizedGeneratedLower :: Text -> Text+normalizedGeneratedLower logicalName =+ case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderLowerCamelName (HaskellName.lowerCamel derived)+ Left _ -> logicalName+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedFieldSite,+ HaskellName.siteLogicalName = logicalName,+ HaskellName.siteOwner = "mapped-diff",+ HaskellName.siteLine = 0+ } diffExpr :: [UsePath] -> Name -> Text -> ResolvedTypeExpr -> ResolvedTypeExpr -> [MappedFinding] diffExpr paths declaration leaf oldExpression newExpression =
src/Keiro/Dsl/NominalType.hs view
@@ -38,6 +38,7 @@ import Data.TypeID qualified as TypeID import GHC.Generics (Generic) import Keiro.Dsl.Grammar+import Keiro.Dsl.HaskellName (haskellKeywords) import Keiro.Dsl.IdDomain (enforcedIdDomainVersion) import Keiro.Dsl.LanguageVersion (RuntimeCapability (..), runtimeProfileHasCapability) import Keiro.Dsl.SemanticContract (CheckedService (..), EffectiveLanguageContract, effectiveRuntimeProfile)@@ -421,33 +422,3 @@ asciiAlphaNum c = asciiLetter c || (isAscii c && isDigit c) asciiAlphaNumOrUnderscore c = asciiAlphaNum c || c == '_' asciiControl c = ord c < 32 || ord c == 127--haskellKeywords :: Set.Set Text-haskellKeywords =- Set.fromList- [ "case",- "class",- "data",- "default",- "deriving",- "do",- "else",- "foreign",- "if",- "import",- "in",- "infix",- "infixl",- "infixr",- "instance",- "let",- "module",- "newtype",- "of",- "then",- "type",- "where",- "mdo",- "rec",- "proc"- ]
src/Keiro/Dsl/Parser/Aggregate.hs view
@@ -153,8 +153,17 @@ pAggregateField context = do loc <- getLoc n <- ident+ selector <- optionalLanguageFeature context FieldAliasSyntax "haskell" (try (keyword "haskell" *> ident))+ wireKey <- optionalLanguageFeature context FieldAliasSyntax "as" (try (keyword "as" *> stringLit)) mty <- optional (symbol ":" *> pMappedTypeExpr context)- pure AggregateField {aggregateFieldName = n, aggregateFieldType = mty, aggregateFieldLoc = loc}+ pure+ AggregateField+ { aggregateFieldName = n,+ aggregateFieldSelector = selector,+ aggregateFieldWireKey = wireKey,+ aggregateFieldType = mty,+ aggregateFieldLoc = loc+ } pEvent :: FrontendContext -> P (Event, [Located SurfaceElement]) pEvent context = do
src/Keiro/Dsl/Parser/Coordination.hs view
@@ -5,6 +5,7 @@ ) where +import Data.List (intersperse) import Data.Text (Text) import Data.Text qualified as T import Keiro.Dsl.Grammar@@ -114,24 +115,23 @@ <*> (symbol ";" *> keyword "on-failed" *> pDisp) pure RouterDispatchNode {rdCommand = command, rdFields = fields, rdDisposition = disposition, rdLoc = loc} -pRouterDispatchIdLine :: P ()-pRouterDispatchIdLine = do+-- | @dispatch-id strategy=uuidv5 from=(…)@ where the tuple is fixed by the+-- runtime that derives the id. The line documents a derivation the spec cannot+-- change, so the only sound thing to accept is the exact spelling that is true.+pFixedDispatchIdLine :: [Text] -> P ()+pFixedDispatchIdLine inputs = do keyword "dispatch-id" _ <- symbol "strategy" *> symbol "=" *> keyword "uuidv5"- _ <- symbol "from" *> symbol "=" *> parens fixedInputs+ _ <- symbol "from" *> symbol "=" *> parens (sequence_ (intersperse (() <$ symbol ",") (map keyword inputs))) pure ()- where- fixedInputs = do- keyword "name"- _ <- symbol ","- keyword "key"- _ <- symbol ","- keyword "sourceEventId"- _ <- symbol ","- keyword "targetStreamName"- _ <- symbol ","- keyword "occurrence" +-- | @Keiro.Router.deterministicRouterCommandId@ keys on the router name, the+-- correlation key, the source event, the resolved target stream, and the+-- same-stream occurrence.+pRouterDispatchIdLine :: P ()+pRouterDispatchIdLine =+ pFixedDispatchIdLine ["name", "key", "sourceEventId", "targetStreamName", "occurrence"]+ pPolicyLine :: Text -> P PolicyChoice pPolicyLine clause = keyword clause *> symbol "=>" *> pPolicyChoice @@ -208,13 +208,16 @@ DDeadLetter <$> (keyword "DeadLetter" *> stringLit) ] --- The dispatch-id line is a fixed, runtime-owned strategy; parse and discard.+-- | A process manager's twin of 'pRouterDispatchIdLine'.+-- @Keiro.ProcessManager.deterministicCommandId@ keys on the manager name, the+-- correlation id, the source event, and the positional emit index — a different+-- fixed tuple from the router's, so the two lines are checked separately but+-- equally strictly. Before ExecPlan 199 a process accepted any strategy+-- identifier and any tuple, so `dispatch-id strategy=md5 from=(banana)` checked+-- clean here while the same line was a parse error on a router. pDispatchIdLine :: P ()-pDispatchIdLine = do- keyword "dispatch-id"- _ <- symbol "strategy" *> symbol "=" *> ident- _ <- symbol "from" *> symbol "=" *> parens (sepBy dottedRef (symbol ","))- pure ()+pDispatchIdLine =+ pFixedDispatchIdLine ["name", "correlationId", "sourceEventId", "emitIndex"] pTimerNode :: P TimerNode pTimerNode = do@@ -249,8 +252,8 @@ keyword "uuidv5" pfx <- stringLit _ <- symbol "<>"- _ <- ident -- correlationId (fixed)- pure IdExpr {ideStrategy = UuidV5Id, idePrefix = pfx}+ field <- ident+ pure IdExpr {ideStrategy = UuidV5Id, idePrefix = pfx, ideField = field} pFireAt :: P FireAtExpr pFireAt = do
src/Keiro/Dsl/Parser/Document.hs view
@@ -127,7 +127,7 @@ ] ++ [ plain (SurfaceNode . NRouter <$> pRouter), plain (SurfaceNode . NProcess <$> pProcess),- plain (SurfaceNode . NContract <$> pContract),+ plain (SurfaceNode . NContract <$> pContract context), plain (SurfaceNode . NIntake <$> pIntake), plain (SurfaceNode . NEmit <$> pEmit), plain (SurfaceNode . NPublisher <$> pPublisher),
src/Keiro/Dsl/Parser/Integration.hs view
@@ -7,15 +7,17 @@ ) where +import Keiro.Dsl.Frontend.Internal (FrontendContext) import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion (LanguageFeature (FieldAliasSyntax)) import Keiro.Dsl.Parser.Core import Text.Megaparsec -- Integration contract (EP-4) -------------------------------------------------------------------------------- -pContract :: P ContractNode-pContract = do+pContract :: FrontendContext -> P ContractNode+pContract context = do loc <- getLoc keyword "contract" nm <- ident@@ -50,11 +52,14 @@ fs <- braces (many pContractField) pure ContractEvent {ceName = nm, ceTopic = topicAlias, ceFields = fs} pContractField = do+ loc <- getLoc n <- ident+ selector <- optionalLanguageFeature context FieldAliasSyntax "haskell" (try (keyword "haskell" *> ident))+ wireKey <- optionalLanguageFeature context FieldAliasSyntax "as" (try (keyword "as" *> stringLit)) _ <- symbol ":" ty <- pContractType _ <- optional (symbol ";")- pure ContractField {cfName = n, cfType = ty}+ pure ContractField {cfName = n, cfSelector = selector, cfWireKey = wireKey, cfType = ty, cfLoc = loc} pContractType = choice [ CTypeId <$> (keyword "typeid" *> stringLit),
src/Keiro/Dsl/Parser/Queue.hs view
@@ -93,8 +93,11 @@ _ <- symbol "->" w <- stringLit ty <- ident- req <- option False (True <$ keyword "required")- pure WqField {wqfName = n, wqfWire = w, wqfType = ty, wqfRequired = req}+ -- Every payload field is required: generated decoders use `o .:` for all+ -- of them. The keyword stays accepted so existing sources still parse, but+ -- it selects nothing, so it is not retained. See ExecPlan 199.+ _ <- optional (keyword "required")+ pure WqField {wqfName = n, wqfWire = w, wqfType = ty} pWqDispRow = do loc <- getLoc o <- ident
src/Keiro/Dsl/PrettyPrint.hs view
@@ -349,7 +349,9 @@ WqUnlogged -> [indent 2 "provision unlogged"] WqPartitioned interval retention -> [indent 2 ("provision partitioned(interval=" <> dquoted interval <> ", retention=" <> dquoted retention <> ")")]- field f = pretty (wqfName f) <+> "->" <+> dquoted (wqfWire f) <+> pretty (wqfType f) <> (if wqfRequired f then " required" else mempty)+ -- Always rendered: every payload field is required, and stating it keeps+ -- the canonical form self-describing.+ field f = pretty (wqfName f) <+> "->" <+> dquoted (wqfWire f) <+> pretty (wqfType f) <> " required" dispRow r = pretty (wqdOutcome r) <+> "->" <+> act (wqdAction r) act IAckOk = "ackOk" act (IRetry win) = "retry" <+> pretty win@@ -493,7 +495,14 @@ ["event" <+> pretty (ceName e) <+> "on" <+> pretty (ceTopic e) <+> "{"] ++ map (indent 2 . docContractField) (ceFields e) ++ ["}"]- docContractField f = pretty (cfName f) <> ":" <+> docContractType (cfType f)+ docContractField f =+ hsep+ ( [pretty (cfName f)]+ ++ maybe [] (\selector -> ["haskell", pretty selector]) (cfSelector f)+ ++ maybe [] (\wireKey -> ["as", dquoted wireKey]) (cfWireKey f)+ )+ <> ":"+ <+> docContractType (cfType f) docContractType (CTypeId p) = "typeid" <+> dquoted p docContractType CText = "text" docContractType CInt = "int"@@ -613,7 +622,7 @@ ] docIdExpr :: IdExpr -> Doc ann-docIdExpr e = "uuidv5" <+> dquoted (idePrefix e) <+> "<>" <+> "correlationId"+docIdExpr e = "uuidv5" <+> dquoted (idePrefix e) <+> "<>" <+> pretty (ideField e) docFireAt :: FireAtExpr -> Doc ann docFireAt f = ("input." <> pretty (faField f)) <+> "+" <+> pretty (faWindow f)@@ -723,9 +732,13 @@ docCommand c = "command" <+> pretty (cmdName c) <+> braced (map docAggregateField (cmdFields c)) docAggregateField :: AggregateField -> Doc ann-docAggregateField f = case aggregateFieldType f of- Nothing -> pretty (aggregateFieldName f)- Just ty -> pretty (aggregateFieldName f) <> ":" <> docTypeExpr ty+docAggregateField f =+ hsep+ ( [pretty (aggregateFieldName f)]+ ++ maybe [] (\selector -> ["haskell", pretty selector]) (aggregateFieldSelector f)+ ++ maybe [] (\wireKey -> ["as", dquoted wireKey]) (aggregateFieldWireKey f)+ )+ <> maybe mempty (\ty -> ":" <> docTypeExpr ty) (aggregateFieldType f) docField :: Field -> Doc ann docField f = case fieldType f of
src/Keiro/Dsl/ReplayImpact.hs view
@@ -29,6 +29,7 @@ import Data.Text qualified as Text import Keiro.Dsl.AggregateType import Keiro.Dsl.CanonicalEncoding (canonicalExpr, canonicalTransition)+import Keiro.Dsl.FieldIdentity (ResolvedFieldIdentity (..), resolveAggregateFieldIdentity) import Keiro.Dsl.FoldFingerprint (FoldSurfaceError, aggregateFoldSurfaceForService) import Keiro.Dsl.Grammar import Keiro.Dsl.NominalType@@ -137,13 +138,19 @@ | 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)+eventDecodeSurface :: Aggregate -> Event -> (Int, Maybe (Int, Hole), [(Name, Text, Maybe TypeExpr)])+eventDecodeSurface aggregate event =+ ( evVersion event,+ evUpcastFrom event,+ [ (fieldDslName identity, fieldWireKey identity, aggregateFieldType field)+ | field <- eventFields aggregate event,+ let identity = resolveAggregateFieldIdentity field+ ]+ ) -eventSurface :: Spec -> Aggregate -> Event -> ((EventBody, Int, Maybe (Int, Hole)), [(Name, Text)])+eventSurface :: Spec -> Aggregate -> Event -> ((Int, Maybe (Int, Hole), [(Name, Text, Maybe TypeExpr)]), [(Name, Text)]) eventSurface spec aggregate event =- (eventDecodeSurface event, mappedFieldSurface spec aggregate event)+ (eventDecodeSurface aggregate event, mappedFieldSurface spec aggregate event) mappedFieldSurface :: Spec -> Aggregate -> Event -> [(Name, Text)] mappedFieldSurface spec aggregate event = mapped <> nominal
src/Keiro/Dsl/Scaffold.hs view
@@ -19,6 +19,8 @@ -- concern. module Keiro.Dsl.Scaffold ( ScaffoldModule (..),+ ModuleRole (..),+ moduleRole, ModuleKind (..), Context (..), Placement (..),@@ -92,26 +94,29 @@ ) where -import Data.Char (isAlpha, isAlphaNum, isDigit, isUpper, toLower, toUpper)+import Data.Char (isAlpha, isAlphaNum, isDigit, isUpper) import Data.List (find, findIndex, groupBy, isSuffixOf, nub, sort, sortOn) import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map-import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T import Data.Version (showVersion)+import Keiro.Dsl.AggregateGenerationPlan import Keiro.Dsl.AggregateType import Keiro.Dsl.BehaviorCoverage qualified as Behavior import Keiro.Dsl.CodecCompare (BranchArm (..), BranchField (..), BranchSchema (..)) import Keiro.Dsl.EventOutput import Keiro.Dsl.ExplainBindings (BindingObligation (..), BindingObligationKind (..), bindingObligations) import Keiro.Dsl.Expression+import Keiro.Dsl.FieldIdentity import Keiro.Dsl.FoldFingerprint (aggregateFoldFingerprintForService, renderFoldSurfaceError) import Keiro.Dsl.GeneratedHaskellLanguage import Keiro.Dsl.Grammar import Keiro.Dsl.HaskellImport+import Keiro.Dsl.HaskellName qualified as HaskellName import Keiro.Dsl.IdDomain (IdDomainContract, contractIdDomainContractFor, idDomainContractFor, idDomainPrefix, idDomainSampleText) import Keiro.Dsl.LanguageVersion (SourceLanguage (LegacyUnversioned), languageVersionText) import Keiro.Dsl.NominalType@@ -134,6 +139,32 @@ } deriving stock (Eq, Show) +-- | Stable semantic identity for a generated artifact. It is deliberately+-- independent of the cased module path: source-name migrations pair artifacts+-- by this role, then compare their old and current paths.+data ModuleRole = ModuleRole+ { roleOwnerKind :: !Text,+ roleOwnerName :: !Text,+ roleFamily :: !Text+ }+ deriving stock (Eq, Ord, Show)++moduleRole :: ScaffoldModule -> ModuleRole+moduleRole scaffoldModule =+ ModuleRole+ { roleOwnerKind = headOr "module" originWords,+ roleOwnerName = origin scaffoldModule,+ roleFamily = case reverse (T.splitOn "." moduleName) of+ family : _ -> family+ [] -> moduleName+ }+ where+ originWords = T.words (origin scaffoldModule)+ moduleName = T.replace "/" "." (T.dropEnd 3 (T.pack (modulePath scaffoldModule)))+ headOr fallback = \case+ value : _ -> value+ [] -> fallback+ data ModuleKind = -- | @-- \@generated@; overwritten on every scaffold. Generated@@ -414,11 +445,12 @@ } deriving stock (Eq, Show) --- | A command or event constructor with its fully-resolved field types.+-- | A command or event constructor with its fully-resolved field identities and+-- aggregate types. data ResolvedCtor = ResolvedCtor { rcName :: !Text,- -- | (field name, canonical aggregate type)- rcFields :: ![(Text, ResolvedAggregateType)],+ -- | (DSL/selector/wire identity, canonical aggregate type)+ rcFields :: ![(ResolvedFieldIdentity, ResolvedAggregateType)], -- | EP-2: schema version (1 for commands and unversioned events). rcVersion :: !Int, -- | EP-2: the source version this event migrates from (the upcaster step).@@ -489,7 +521,7 @@ mkCtor useSite cn fs = ResolvedCtor { rcName = cn,- rcFields = map (\field -> (aggregateFieldName field, orDie (inferAggregateFieldType symbols agg useSite field))) fs,+ rcFields = map (\field -> (resolveAggregateFieldIdentity field, orDie (inferAggregateFieldType symbols agg useSite field))) fs, rcVersion = 1, rcUpcastFrom = Nothing }@@ -1240,21 +1272,30 @@ ( renderGeneratedLanguagePragmas localExtensions <> [ generatedBanner, moduleHeader,- "",- "import Data.Aeson (FromJSON, ToJSON)",- "import Data.Text (Text)",- "import GHC.Generics (Generic)",- "import Keiki.Shape (CanonicalTypeName)"+ "" ]+ <> baseImports <> internalImports <> equalityImports- <> [ "",- sectionsOf [map emitOwner owners]- ]+ <> if T.null declarations then [] else ["", declarations] ) where usesEquality = any nominalEqualityUsed owners- usesExactEquality = any (\owner -> nominalEqualityUsed owner && exactOwner (nominalDeclaration owner)) owners+ exactEqualityOwners = [owner | owner <- owners, nominalEqualityUsed owner, exactOwner (nominalDeclaration owner)]+ inexactEqualityOwners = [owner | owner <- owners, nominalEqualityUsed owner, not (exactOwner (nominalDeclaration owner))]+ usesExactEquality = not (null exactEqualityOwners)+ usesInexactEquality = not (null inexactEqualityOwners)+ legacyNominals =+ [ nominal+ | owner <- owners,+ let nominal = nominalDeclaration owner,+ case resolvedNominalRepresentation nominal of+ IdRepresentation prefix -> not (isJust (idDomainContractFor languageContract prefix))+ EnumRepresentation {} -> True+ ScalarRepresentation {} -> False+ ]+ hasExactEnum = any (\owner -> case resolvedNominalRepresentation (nominalDeclaration owner) of EnumRepresentation {} -> True; _ -> False) exactEqualityOwners+ hasExactEnforcedId = any (\owner -> case resolvedNominalRepresentation (nominalDeclaration owner) of IdRepresentation prefix -> isJust (idDomainContractFor languageContract prefix); _ -> False) exactEqualityOwners enforcingIds = [ nominal | owner <- owners,@@ -1287,12 +1328,28 @@ localExtensions = [ExtDeriveAnyClass | any (nominalUsesDeriveAnyClass . nominalDeclaration) owners] <> [ExtTypeFamilies | usesEquality]+ baseImports =+ ["import Data.Aeson (FromJSON, ToJSON)" | not (null legacyNominals)]+ <> ["import Data.Text (Text)" | not (null legacyNominals) || usesEquality]+ <> ["import GHC.Generics (Generic)" | not (null legacyNominals)]+ <> ["import Keiki.Shape (CanonicalTypeName)" | not (null legacyNominals)] equalityImports =- ["import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)" | usesEquality]- <> ["import Data.List.NonEmpty (NonEmpty (..))" | usesExactEquality]- <> ["import Keiki.ProjectionDomain (finiteProjectionDomain)" | usesExactEquality && null enforcingIds]- <> ["import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)" | usesExactEquality && not (null enforcingIds)]- <> ["import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)" | not (null enforcingIds)]+ [ "import Keiki.Core (" <> T.intercalate ", " coreImports <> ")"+ | usesEquality+ ]+ <> ["import Data.List.NonEmpty (NonEmpty (..))" | hasExactEnum]+ <> [ "import Keiki.ProjectionDomain (" <> T.intercalate ", " projectionDomainImports <> ")"+ | not (null projectionDomainImports)+ ]+ <> ["import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)" | hasExactEnforcedId]+ where+ coreImports =+ ["FieldProjection (..)", "FieldWitness"]+ <> (if usesExactEquality then ["ExactFieldProjection (..)", "exactFieldWitness"] else [])+ <> ["fieldWitness" | usesInexactEquality]+ projectionDomainImports =+ ["finiteProjectionDomain" | hasExactEnum]+ <> (if hasExactEnforcedId then ["TextPattern", "textProjectionDomain"] else []) internalImports = [ "import " <> generatedNominalInternalModule ctx@@ -1303,6 +1360,7 @@ <> ")" | not (null enforcingIds) ]+ declarations = T.dropWhileEnd (== '\n') (sectionsOf [map emitOwner owners]) emitOwner owner = emitGeneratedNominal languageContract (nominalEqualityUsed owner) (nominalDeclaration owner) exactOwner nominal = case resolvedNominalRepresentation nominal of EnumRepresentation {} -> True@@ -1317,9 +1375,7 @@ emitGeneratedNominal languageContract equalityUsed nominal = case resolvedNominalRepresentation nominal of IdRepresentation prefix | Just _ <- idDomainContractFor languageContract prefix ->- nl $- ["instance CanonicalTypeName " <> name]- <> equalitySection+ nl equalitySection IdRepresentation {} -> nl $ [ "newtype " <> name <> " = " <> name <> " Text",@@ -1412,6 +1468,7 @@ "import Data.Text (Text)", "import Data.Text qualified as T", "import GHC.Generics (Generic)",+ "import Keiki.Shape (CanonicalTypeName)", "import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText)", "", sectionsOf [map emitInternal nominals]@@ -1429,6 +1486,8 @@ [ "newtype " <> name <> " = " <> name <> " Text", " deriving stock (Generic, Eq, Ord, Show)", "",+ "instance CanonicalTypeName " <> name,+ "", "instance ToJSON " <> name <> " where", " toJSON = toJSON . " <> textName, "",@@ -1482,8 +1541,18 @@ ] generatedNominalTypeImportsForService :: CheckedService -> Context -> [ResolvedNominalType] -> [Text]-generatedNominalTypeImportsForService _ _ [] = [] generatedNominalTypeImportsForService service ctx nominals =+ generatedNominalTypeImportsWithParsers service ctx nominals nominals++-- | As 'generatedNominalTypeImportsForService', but importing an enforced ID's+-- @parse\<Name\>@ only for the nominals in @parsing@.+--+-- The parser is emitted only where a literal of that ID is constructed. A module+-- that merely mentions the type — a guard operand, say — needs the type name and+-- nothing else, and importing the parser there is an unused import.+generatedNominalTypeImportsWithParsers :: CheckedService -> Context -> [ResolvedNominalType] -> [ResolvedNominalType] -> [Text]+generatedNominalTypeImportsWithParsers _ _ [] _ = []+generatedNominalTypeImportsWithParsers service ctx nominals parsing = [ "import " <> generatedNominalModule ctx <> " ("@@ -1491,10 +1560,12 @@ <> ")" ] where+ parsingNames = map resolvedNominalName (stableNominals parsing) importsFor nominal = case resolvedNominalRepresentation nominal of IdRepresentation prefix | Just _ <- idDomainContractFor (checkedLanguageContract service) prefix ->- [resolvedNominalName nominal, "parse" <> resolvedNominalName nominal]+ [resolvedNominalName nominal]+ <> ["parse" <> resolvedNominalName nominal | resolvedNominalName nominal `elem` parsingNames] _ -> [resolvedNominalName nominal <> " (..)"] generatedNominalCodecImports :: CheckedService -> Context -> [ResolvedNominalType] -> [Text]@@ -1506,7 +1577,7 @@ <> T.intercalate ", " ( concat- [ [typeImport nominal, nominalTextName nominal]+ [ publicImports nominal | nominal <- stableNominals nominals ] )@@ -1520,10 +1591,10 @@ | not (null enforcingIds) ] where- typeImport nominal = case resolvedNominalRepresentation nominal of+ publicImports nominal = case resolvedNominalRepresentation nominal of IdRepresentation prefix- | Just _ <- idDomainContractFor (checkedLanguageContract service) prefix -> resolvedNominalName nominal- _ -> resolvedNominalName nominal <> " (..)"+ | Just _ <- idDomainContractFor (checkedLanguageContract service) prefix -> [nominalTextName nominal]+ _ -> [resolvedNominalName nominal <> " (..)", nominalTextName nominal] enforcingIds = [ nominal | nominal <- stableNominals nominals,@@ -1634,21 +1705,46 @@ moduleName = nominalProjectionModule ctx imports = sort . nub $- [ "Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)",- "Keiro.Codec.Nominal (nominalFromRepresentation, nominalToRepresentation)"+ [ "Keiki.Core (" <> T.intercalate ", " coreImports <> ")",+ "Keiro.Codec.Nominal (" <> T.intercalate ", " nominalCodecImports <> ")" ] <> ["Data.KindID qualified as KindID" | any hasId nominals] <> ["Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain, validateIdDomainText)" | any hasEnforcedId nominals] <> ["Data.List.NonEmpty (NonEmpty (..))" | any hasExactDomain nominals] <> ["Data.Text (Text)" | any usesText nominals] <> ["Data.Time (UTCTime)" | any (hasScalar NominalTime) nominals]- <> ["Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, matchesTextPattern, textCharSet, textConcat, textLiteral, textProjectionDomain, textRepeatBetween)" | any hasExactDomain nominals]+ -- The four text combinators build the legacy hand-rolled TypeID+ -- pattern and are used only by the unenforced-ID branch; importing+ -- them unconditionally warns under -Wunused-imports whenever every+ -- exact-domain nominal is an enum or an enforced ID.+ <> [ "Keiki.ProjectionDomain ("+ <> T.intercalate+ ", "+ ( ["TextPattern", "finiteProjectionDomain", "matchesTextPattern", "textProjectionDomain"]+ <> (if any hasUnenforcedId nominals then ["textCharSet", "textConcat", "textLiteral", "textRepeatBetween"] else [])+ )+ <> ")"+ | any hasExactDomain nominals+ ] <> ["Numeric.Natural (Natural)" | any (hasScalar NominalNatural) nominals]+ hasExactProjection nominal = case resolvedNominalRepresentation nominal of ScalarRepresentation {} -> False; _ -> True+ hasInexactProjection = any (not . hasExactProjection) nominals+ hasReconstruction = any hasExactProjection nominals+ coreImports =+ ["FieldProjection (..)", "FieldWitness"]+ <> (if hasReconstruction then ["ExactFieldProjection (..)", "exactFieldWitness"] else [])+ <> ["fieldWitness" | hasInexactProjection]+ nominalCodecImports =+ ["nominalToRepresentation"]+ <> ["nominalFromRepresentation" | hasReconstruction] hasScalar wanted nominal = resolvedNominalRepresentation nominal == ScalarRepresentation wanted hasId nominal = case resolvedNominalRepresentation nominal of IdRepresentation {} -> True; _ -> False hasEnforcedId nominal = case resolvedNominalRepresentation nominal of IdRepresentation prefix -> isJust (idDomainContractFor languageContract prefix) _ -> False+ hasUnenforcedId nominal = case resolvedNominalRepresentation nominal of+ IdRepresentation prefix -> isNothing (idDomainContractFor languageContract prefix)+ _ -> False hasExactDomain nominal = case resolvedNominalRepresentation nominal of ScalarRepresentation {} -> False; _ -> True usesText nominal = case resolvedNominalRepresentation nominal of ScalarRepresentation NominalText -> True; IdRepresentation {} -> True; EnumRepresentation {} -> True; _ -> False importPlan =@@ -2094,25 +2190,28 @@ emitStructuralProjections :: Context -> TypeGraph -> Text emitStructuralProjections ctx graph = nl $- renderGeneratedLanguagePragmas [ExtTypeFamilies]+ renderGeneratedLanguagePragmas [ExtTypeFamilies | not (null specs)] <> [ generatedBanner, "-- Equality witnesses are emitted for Text, Int, Bool, Natural, and UTCTime.", "-- Int, Natural, and UTCTime belong to Keiki's ordered subset.", "module " <> moduleName, " ( " <> T.intercalate "\n , " (map spWitness specs), " ) where",- "",- "import Data.Text (Text)",- "import Data.Time (UTCTime)",- "import Numeric.Natural (Natural)",- "import Keiro.Codec.Structural (bindingToShape)",- "import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)"+ "" ]+ <> staticImports <> T.lines (renderPlannedImports importPlan) <> concatMap renderProjection specs where moduleName = structuralProjectionModule ctx specs = map (resolveProjectionModules ctx) (projectionSpecs graph)+ resultTypes = Set.fromList (map spResult specs)+ staticImports =+ ["import Data.Text (Text)" | "Text" `Set.member` resultTypes]+ <> ["import Data.Time (UTCTime)" | "UTCTime" `Set.member` resultTypes]+ <> ["import Numeric.Natural (Natural)" | "Natural" `Set.member` resultTypes]+ <> ["import Keiro.Codec.Structural (bindingToShape)" | not (null specs)]+ <> ["import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)" | not (null specs)] importPlan = planImportsOrDie moduleName@@ -2263,9 +2362,24 @@ emitBehaviorContract aggregate = nl $ renderGeneratedLanguagePragmas [ExtOverloadedLabels | not (null (aRegs aggregate))]- <> [ "{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}",- generatedBanner,- "module " <> aGenPrefix aggregate <> ".BehaviorContract where",+ <> [ generatedBanner,+ "module " <> aGenPrefix aggregate <> ".BehaviorContract",+ " ( BehaviorKey (..)",+ " , ObligationKind (..)",+ " , EvidenceLevel (..)",+ " , GuardCoverage (..)",+ " , BehaviorRequirement (..)",+ " , RejectionClass (..)",+ " , LiveExpectation (..)",+ " , BehaviorWitness (..)",+ " , BehaviorFailure (..)",+ " , BehaviorConformanceReport (..)",+ " , behaviorRequirements",+ " , behaviorCoverageReport",+ " , behaviorConformancePassed",+ " , behaviorConformancePassedWith",+ " , renderBehaviorConformanceText",+ " ) where", "", "import " <> aGenPrefix aggregate <> ".Codec (encode" <> name <> "Event, parse" <> name <> "Event, " <> valueStem <> "Codec)", "import " <> aGenPrefix aggregate <> ".Domain",@@ -2277,7 +2391,7 @@ "import Data.Map.Strict qualified as Map", "import Data.Text (Text)", "import Data.Text qualified as T",- "import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither, (!))",+ "import Keiki.Core qualified as K (" <> T.intercalate ", " behaviorCoreImports <> ")", "import Keiro.Codec qualified as Codec (Codec (eventType), EventType (..))", "", "newtype BehaviorKey = BehaviorKey { unBehaviorKey :: Text }",@@ -2332,16 +2446,18 @@ "", "data BehaviorFailure = BehaviorFailure", " { failureKey :: !BehaviorKey",+ " , failureSubject :: !Text", " , failureCode :: !Text", " , failureDetail :: !Text", " }", " deriving stock (Eq, Show)", "", "instance ToJSON BehaviorFailure where",- " toJSON failure = object",- " [ \"key\" .= unBehaviorKey (failureKey failure)",- " , \"code\" .= failureCode failure",- " , \"detail\" .= failureDetail failure",+ " toJSON behaviorFailure = object",+ " [ \"key\" .= unBehaviorKey (failureKey behaviorFailure)",+ " , \"subject\" .= failureSubject behaviorFailure",+ " , \"code\" .= failureCode behaviorFailure",+ " , \"detail\" .= failureDetail behaviorFailure", " ]", "", "data BehaviorConformanceReport = BehaviorConformanceReport",@@ -2398,7 +2514,7 @@ " , not (isPending witness)", " , Just requirement <- [Map.lookup key requiredByKey]", " ]",- " failures = [failure | (_, Left failure) <- executions]",+ " failures = [behaviorFailure | (_, Left behaviorFailure) <- executions]", "", "behaviorConformancePassed :: BehaviorConformanceReport -> Bool", "behaviorConformancePassed = behaviorConformancePassedWith False",@@ -2425,7 +2541,7 @@ " , \"failed: \" <> tshow (length (reportFailed report))", " , countLine \"verified\" (reportVerified report)", " , countLine \"unverified\" (reportUnverified report)",- " ] <> T.unlines [\"FAIL \" <> unBehaviorKey (failureKey failure) <> \" [\" <> failureCode failure <> \"] \" <> failureDetail failure | failure <- reportFailed report]",+ " ] <> T.unlines [\"FAIL \" <> unBehaviorKey (failureKey behaviorFailure) <> \" \" <> failureSubject behaviorFailure <> \" [\" <> failureCode behaviorFailure <> \"] \" <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report]", "", "runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure ()", "runWitness requirement witness = case witness of",@@ -2443,6 +2559,7 @@ " RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation", " LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation", "",+ "runRejection :: BehaviorRequirement -> (" <> aVertexType aggregate <> ", K.RegFile " <> name <> "Regs) -> " <> name <> "Command -> LiveExpectation -> Either BehaviorFailure ()", "runRejection requirement seed command expectation = case expectation of", " Emits _ -> failure requirement \"expectation-kind\" \"a rejection requirement cannot expect emitted events\"", " NoOp -> failure requirement \"expectation-kind\" \"a rejection requirement cannot expect an accepted no-op\"",@@ -2452,6 +2569,7 @@ " Left K.AmbiguousEdges {} -> failure requirement \"ambiguous-edges\" \"AmbiguousEdges can never satisfy a rejection witness\"", " Right _ -> failure requirement \"unexpected-acceptance\" \"runtime accepted a command required to reject\"", "",+ "runAcceptance :: BehaviorRequirement -> (" <> aVertexType aggregate <> ", K.RegFile " <> name <> "Regs) -> " <> name <> "Command -> LiveExpectation -> Either BehaviorFailure ()", "runAcceptance requirement seed command expectation = case expectation of", " Rejects _ -> failure requirement \"expectation-kind\" \"a live-transition requirement needs Emits or NoOp\"", " NoOp -> case K.stepDetailedEither " <> valueStem <> "Transducer seed command of",@@ -2467,8 +2585,8 @@ " checkAcceptedEnvelope requirement success", " let expected = NonEmpty.toList expectedEvents", " actual = K.stepSuccessOutputs success",- " ensure requirement (actual == expected) \"event-value-mismatch\" \"runtime event values differ from the exact witness expectation\"",- " ensure requirement (map eventKind actual == requirementEventKinds requirement) \"event-envelope-mismatch\" \"runtime event kinds differ from the declared ordered envelope\"",+ " ensure requirement (actual == expected) \"event-value-mismatch\" (\"runtime event values differ from the exact witness expectation; actual=\" <> tshow actual <> \" expected=\" <> tshow expected)",+ " ensure requirement (map eventKind actual == requirementEventKinds requirement) \"event-envelope-mismatch\" (\"runtime event kinds differ from the declared ordered envelope; actual=\" <> tshow (map eventKind actual) <> \" expected=\" <> tshow (requirementEventKinds requirement))", " decoded <- either (failure requirement \"emitted-codec-decode\") Right (decodeEvents actual)", " replayed <- case K.applyEventsDetailedEither " <> valueStem <> "Transducer seed decoded of", " Left replayFailure -> failure requirement \"emitted-replay-failed\" (tshow replayFailure)",@@ -2477,10 +2595,11 @@ " ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) \"forward-replay-registers\" \"decoded emissions replay to different registers\"", " checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed)", "",+ "checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess " <> name <> "Regs " <> aVertexType aggregate <> " " <> name <> "Event -> Either BehaviorFailure ()", "checkAcceptedEnvelope requirement success = do",- " ensure requirement (K.stepSuccessMode success == K.Live) \"forward-mode\" \"forward execution selected a non-live edge\"",- " ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) \"edge-attribution\" \"runtime selected a different guarded sibling\"",- " ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) \"target-mismatch\" \"runtime reached a different target vertex\"",+ " ensure requirement (K.stepSuccessMode success == K.Live) \"forward-mode\" (\"forward execution selected a non-live edge; actual=\" <> tshow (K.stepSuccessMode success) <> \" expected=\" <> tshow K.Live)",+ " ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) \"edge-attribution\" (\"runtime selected a different guarded sibling; actual=\" <> tshow (Just (K.stepSuccessEdge success)) <> \" expected=\" <> tshow (requirementExpectedEdge requirement))",+ " ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) \"target-mismatch\" (\"runtime reached a different target vertex; actual=\" <> tshow (Just (K.stepSuccessState success)) <> \" expected=\" <> tshow (requirementTarget requirement))", "", "runReplay :: BehaviorRequirement -> [" <> name <> "Event] -> [" <> name <> "Event] -> Either BehaviorFailure ()", "runReplay requirement prefix chunk = case requirementKind requirement of",@@ -2492,19 +2611,21 @@ " replayed <- case K.applyEventsDetailedEither " <> valueStem <> "Transducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of", " Left replayFailure -> failure requirement \"replay-chunk-failed\" (tshow replayFailure)", " Right replaySuccess -> Right replaySuccess",- " ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) \"target-mismatch\" \"replay chunk reached a different target vertex\"",+ " ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) \"target-mismatch\" (\"replay chunk reached a different target vertex; actual=\" <> tshow (Just (K.replaySuccessState replayed)) <> \" expected=\" <> tshow (requirementTarget requirement))", " checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed)", " _ -> failure requirement \"witness-kind\" \"ReplayWitness supplied for a non-replay requirement\"", "",+ "checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution " <> aVertexType aggregate <> "] -> Either BehaviorFailure ()", "checkSingleAttribution requirement expectedMode eventCount trace = case trace of", " [attribution] -> do",- " ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) \"replay-edge-attribution\" \"replay selected a different edge\"",- " ensure requirement (K.replayAttributionMode attribution == expectedMode) \"replay-mode-attribution\" \"replay selected the wrong live/replay-only phase\"",- " ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) \"replay-source-attribution\" \"replay attribution starts at the wrong source\"",- " ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) \"replay-target-attribution\" \"replay attribution ends at the wrong target\"",- " ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) \"replay-span-attribution\" \"replay attribution did not consume the exact chunk\"",+ " ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) \"replay-edge-attribution\" (\"replay selected a different edge; actual=\" <> tshow (Just (K.replayAttributionEdge attribution)) <> \" expected=\" <> tshow (requirementExpectedEdge requirement))",+ " ensure requirement (K.replayAttributionMode attribution == expectedMode) \"replay-mode-attribution\" (\"replay selected the wrong live/replay-only phase; actual=\" <> tshow (K.replayAttributionMode attribution) <> \" expected=\" <> tshow expectedMode)",+ " ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) \"replay-source-attribution\" (\"replay attribution starts at the wrong source; actual=\" <> tshow (K.replayAttributionSource attribution) <> \" expected=\" <> tshow (requirementSource requirement))",+ " ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) \"replay-target-attribution\" (\"replay attribution ends at the wrong target; actual=\" <> tshow (Just (K.replayAttributionTarget attribution)) <> \" expected=\" <> tshow (requirementTarget requirement))",+ " ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) \"replay-span-attribution\" (\"replay attribution did not consume the exact chunk; actual=\" <> tshow (K.replayAttributionSpan attribution) <> \" expected=\" <> tshow (K.ReplayEventSpan 0 eventCount))", " _ -> failure requirement \"replay-trace-cardinality\" \"expected exactly one completed-edge attribution\"", "",+ "settleHistory :: BehaviorRequirement -> Text -> [" <> name <> "Event] -> Either BehaviorFailure (K.ReplaySuccess " <> name <> "Regs " <> aVertexType aggregate <> ")", "settleHistory requirement label history = do", " decoded <- either (failure requirement (label <> \"-codec-decode\")) Right (decodeEvents history)", " case K.applyEventsDetailedEither " <> valueStem <> "Transducer (" <> initialVertex aggregate <> ", initial" <> name <> "Regs) decoded of",@@ -2516,27 +2637,48 @@ ] <> renderCommandKind aggregate <> [ "",+ "eventKind :: " <> name <> "Event -> Text", "eventKind event = case Codec.eventType " <> valueStem <> "Codec event of Codec.EventType tag -> tag", "", "regsEqual :: K.RegFile " <> name <> "Regs -> K.RegFile " <> name <> "Regs -> Bool", regsEqualityExpression aggregate, "",+ "proofStrength :: BehaviorRequirement -> Bool", "proofStrength requirement =", " requirementEvidence requirement == GeneratedAuthoritative", " && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable]", "",+ "behaviorWitnessKey :: BehaviorWitness -> BehaviorKey", "behaviorWitnessKey witness = case witness of", " Pending key -> key", " LiveWitness { witnessKey = key } -> key", " ReplayWitness { witnessKey = key } -> key", "",+ "isPending :: BehaviorWitness -> Bool", "isPending Pending {} = True", "isPending _ = False", "",+ "ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure ()", "ensure requirement condition code detail = if condition then Right () else failure requirement code detail",- "failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)",+ "failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed",+ "failure requirement code detail =",+ " Left",+ " ( BehaviorFailure",+ " (requirementKey requirement)",+ " (tshow (requirementSource requirement) <> \" x \" <> requirementCommandName requirement <> \": \" <> kindPhrase <> \" (spec line \" <> tshow (requirementLine requirement) <> \")\")",+ " code",+ " detail",+ " )",+ " where",+ " kindPhrase = case requirementKind requirement of",+ " LiveTransition -> \"live transition\"",+ " RequiredRejection -> \"required rejection\"",+ " ReplayTransition -> \"replay-only transition\"",+ "sortedKeys :: [BehaviorKey] -> [BehaviorKey]", "sortedKeys = sortOn unBehaviorKey",+ "keyTexts :: [BehaviorKey] -> [Text]", "keyTexts = map unBehaviorKey",+ "countLine :: Text -> [BehaviorKey] -> Text", "countLine label values = label <> \": \" <> tshow (length values)", "tshow :: Show value => value -> Text", "tshow = T.pack . show"@@ -2544,12 +2686,26 @@ where name = aName aggregate valueStem = lowerFirst name+ behaviorCoreImports =+ [ "EdgeMode (..)",+ "EdgeRef (..)",+ "RegFile",+ "ReplayAttribution (..)",+ "ReplayEventSpan (..)",+ "ReplaySuccess (..)",+ "StepFailure (..)",+ "StepSuccess (..)",+ "applyEventsDetailedEither",+ "stepDetailedEither"+ ]+ <> ["(!)" | not (null (aRegs aggregate))] renderCommandKind :: Agg -> [Text] renderCommandKind aggregate = case aCommands aggregate of- [] -> ["", "commandKind _ = \"\""]+ [] -> ["", "commandKind :: " <> aName aggregate <> "Command -> Text", "commandKind _ = \"\""] commands -> [ "",+ "commandKind :: " <> aName aggregate <> "Command -> Text", "commandKind command = case command of" ] <> [" " <> rcName command <> " _ -> " <> tshow (rcName command) | command <- commands]@@ -2559,34 +2715,43 @@ case behaviorRequirementsFor aggregate of [] -> [" []"] requirements ->- [ (if index == (0 :: Int) then " [ " else " , ") <> render requirement- | (index, requirement) <- zip [0 ..] requirements- ]+ concat+ [ render index requirement+ | (index, requirement) <- zip [0 ..] requirements+ ] <> [" ]"] where- render requirement =- "BehaviorRequirement "- <> keyExpr requirement- <> " "- <> T.pack (show (Behavior.requirementKind requirement))- <> " "- <> T.pack (show (Behavior.requirementEvidence requirement))- <> " "- <> T.pack (show (Behavior.requirementGuardCoverage requirement))- <> " "- <> vertexCtor aggregate (Behavior.requirementSource requirement)- <> " "- <> tshow (Behavior.requirementCommand requirement)- <> " "- <> edgeExpr aggregate requirement- <> " "- <> maybe "Nothing" (\target -> "(Just " <> vertexCtor aggregate target <> ")") (Behavior.requirementTarget requirement)- <> " "- <> renderBehaviorTextList (Behavior.requirementEvents requirement)- <> " "- <> tshow' (unLoc (Behavior.requirementLocation requirement))- keyExpr requirement = "(BehaviorKey " <> tshow (Behavior.unBehaviorKey (Behavior.requirementKey requirement)) <> ")"+ render index requirement =+ [ (if index == (0 :: Int) then " [ -- " else " , -- ") <> behaviorRequirementLabel aggregate requirement,+ " BehaviorRequirement",+ " { requirementKey = BehaviorKey " <> tshow (Behavior.unBehaviorKey (Behavior.requirementKey requirement)),+ " , requirementKind = " <> T.pack (show (Behavior.requirementKind requirement)),+ " , requirementEvidence = " <> T.pack (show (Behavior.requirementEvidence requirement)),+ " , requirementGuardCoverage = " <> T.pack (show (Behavior.requirementGuardCoverage requirement)),+ " , requirementSource = " <> vertexCtor aggregate (Behavior.requirementSource requirement),+ " , requirementCommandName = " <> tshow (Behavior.requirementCommand requirement),+ " , requirementExpectedEdge = " <> edgeExpr aggregate requirement,+ " , requirementTarget = " <> maybe "Nothing" (\target -> "Just " <> vertexCtor aggregate target) (Behavior.requirementTarget requirement),+ " , requirementEventKinds = " <> renderBehaviorTextList (Behavior.requirementEvents requirement),+ " , requirementLine = " <> tshow' (unLoc (Behavior.requirementLocation requirement)),+ " }"+ ] +behaviorRequirementLabel :: Agg -> Behavior.BehaviorRequirement -> Text+behaviorRequirementLabel aggregate requirement =+ vertexCtor aggregate (Behavior.requirementSource requirement)+ <> " x "+ <> Behavior.requirementCommand requirement+ <> ": "+ <> ( case Behavior.requirementKind requirement of+ Behavior.LiveTransition -> "live transition"+ Behavior.RequiredRejection -> "required rejection"+ Behavior.ReplayTransition -> "replay-only transition"+ )+ <> " (spec line "+ <> tshow' (unLoc (Behavior.requirementLocation requirement))+ <> ")"+ edgeExpr :: Agg -> Behavior.BehaviorRequirement -> Text edgeExpr aggregate requirement = case Behavior.requirementKind requirement of Behavior.RequiredRejection -> "Nothing"@@ -2658,7 +2823,8 @@ [ (if index == (0 :: Int) then " [ " else " , ") <> "Pending (BehaviorKey " <> tshow (Behavior.unBehaviorKey (Behavior.requirementKey requirement))- <> ")"+ <> ") -- "+ <> behaviorRequirementLabel aggregate requirement | (index, requirement) <- zip [0 ..] requirements ] <> [" ]"]@@ -2774,7 +2940,12 @@ ++ [generatedBanner] ++ moduleHeader ++ [ "",- "import Data.Aeson (Value, object, withObject, withText, (.:), (.=))",+ -- A typed-TypeID field decodes through explicitParseField, so a+ -- contract whose every field is one never uses (.:) and would warn+ -- under -Wunused-imports.+ "import Data.Aeson ("+ <> T.intercalate ", " (["Value", "object", "withObject", "withText"] <> ["(.:)" | usesPlainFieldDecode] <> ["(.=)"])+ <> ")", aesonTypesImport ] ++ typedKindIdImports@@ -2824,6 +2995,7 @@ where payloadTy = pascal (ctrName c) <> "Payload" hasTypedTypeIds = any (any (isTypedTypeId . cfType) . ceFields) (ctrEvents c)+ usesPlainFieldDecode = any (any (not . isTypedTypeId . cfType) . ceFields) (ctrEvents c) pragmas = renderGeneratedLanguagePragmas ( [ExtDuplicateRecordFields | contractNeedsDuplicateRecordFields c]@@ -2882,20 +3054,24 @@ ] ++ [" )"] encodeField field =- tshow (cfName field)+ tshow (fieldWireKey identity) <> " .= " <> case cfType field of CTypeId prefix- | isJust (contractIdDomainContractFor languageContract prefix) -> "KindID.toText payload." <> cfName field- _ -> "payload." <> cfName field+ | isJust (contractIdDomainContractFor languageContract prefix) -> "KindID.toText payload." <> fieldSelector identity+ _ -> "payload." <> fieldSelector identity+ where+ identity = resolveContractFieldIdentity field decodeField field = case cfType field of CTypeId prefix | isJust (contractIdDomainContractFor languageContract prefix) ->- "explicitParseField (parseKindIdV7Value @" <> tshow prefix <> ") o " <> tshow (cfName field)- _ -> "o .: " <> tshow (cfName field)+ "explicitParseField (parseKindIdV7Value @" <> tshow prefix <> ") o " <> tshow wireKey+ _ -> "o .: " <> tshow wireKey+ where+ wireKey = fieldWireKey (resolveContractFieldIdentity field) contractNeedsDuplicateRecordFields :: ContractNode -> Bool-contractNeedsDuplicateRecordFields = hasDuplicateNames . concatMap (map cfName . ceFields) . ctrEvents+contractNeedsDuplicateRecordFields = hasDuplicateNames . concatMap (map (fieldSelector . resolveContractFieldIdentity) . ceFields) . ctrEvents contractUsesRecordDot :: ContractNode -> Bool contractUsesRecordDot = any (not . null . ceFields) . ctrEvents@@ -2918,7 +3094,7 @@ <> "Data = " <> ceName e <> (if hasTypedTypeIds then "Data {" else "Data { ")- <> T.intercalate ", " [cfName f <> " :: !" <> hsType (cfType f) | f <- ceFields e]+ <> T.intercalate ", " [fieldSelector (resolveContractFieldIdentity f) <> " :: !" <> hsType (cfType f) | f <- ceFields e] <> (if hasTypedTypeIds then "}\n deriving stock (Eq, Show)" else " }\n deriving stock (Eq, Show)") arm e = ceName e <> " !" <> ceName e <> "Data" sumDecl = case events of@@ -3068,7 +3244,12 @@ " , publisherMaxAttempts", " ) where", "",- "import Keiro.Outbox.Types (BackoffSchedule (..), ExponentialBackoffOptions (..), OrderingPolicy (..))",+ -- Only an exponential schedule mentions ExponentialBackoffOptions, and an+ -- unconditional import makes a constant-backoff publisher warn under+ -- -Wunused-imports. Generated code compiles under -Werror.+ "import Keiro.Outbox.Types (BackoffSchedule (..), OrderingPolicy (..)"+ <> (if boKind (pubBackoff pb) == "exponential" then ", ExponentialBackoffOptions (..)" else "")+ <> ")", "", "publisherOrdering :: OrderingPolicy", "publisherOrdering = " <> pubOrdering pb,@@ -3263,7 +3444,7 @@ " , jobOrdering, jobTuningFor, queueProvision", " ) where", "",- "import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)",+ "import Keiro.PGMQ.Job (" <> T.intercalate ", " queuePolicyImports <> ")", "", "jobOrdering :: JobOrdering", "jobOrdering = " <> orderingCtor,@@ -3296,6 +3477,21 @@ ++ [" " <> pascal (wqdOutcome r) <> " -> " <> outcome (wqdAction r) | r <- wqDisposition w] where outcomeType = T.concat (map pascal (T.splitOn "_" (wqName w))) <> "Outcome"+ queuePolicyImports =+ [ "JobOrdering (..)",+ "JobOutcome (..)",+ "JobTuning",+ "QueueProvision",+ "RetryDelay (..)",+ "RetryPolicy (..)"+ ]+ <> ( case wqProvision w of+ WqStandard -> ["standardProvision"]+ WqUnlogged -> ["unloggedProvision"]+ WqPartitioned {} -> ["PartitionSpec (..)", "partitionedProvision"]+ )+ <> ["withFifoIndexProvision" | wqOrdering w /= WqUnordered]+ <> ["withOrdering"] orderingCtor = case wqOrdering w of WqUnordered -> "Unordered" WqFifoThroughput -> "FifoThroughput"@@ -3820,13 +4016,13 @@ "" ] ++ ["import Data.Aeson (FromJSON, ToJSON)" | hasSnapshot a]- ++ [ "import Data.Proxy (Proxy (..))",- "import Data.Text (Text)",- "import GHC.Generics (Generic)",+ ++ ["import Data.Proxy (Proxy (..))" | not (null (aRegs a))]+ ++ ["import Data.Text (Text)" | AggregateText `elem` aggregateTypes a]+ ++ [ "import GHC.Generics (Generic)", "import Keiki.Core (RegFile (..))" ] ++ ["import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)" | hasSnapshot a]- ++ generatedNominalTypeImportsForService (aggregateCheckedService a) (aContext a) (aGeneratedNominals a)+ ++ generatedNominalDomainImports a ++ map ("import " <>) (domainStaticImports a) ++ T.lines (renderPlannedImports importPlan) ++ [ "import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)",@@ -3850,8 +4046,8 @@ domainNeedsDuplicateRecordFields :: Agg -> Bool domainNeedsDuplicateRecordFields aggregate = hasDuplicateNames selectorNames where- commandSelectors = concatMap (map fst . rcFields) (aCommands aggregate)- eventSelectors = concatMap (map fst . rcFields) (aEvents aggregate)+ commandSelectors = concatMap (map (fieldSelector . fst) . rcFields) (aCommands aggregate)+ eventSelectors = concatMap (map (fieldSelector . fst) . rcFields) (aEvents aggregate) registerSelectors = map rrName (aRegs aggregate) -- deriveWireCtorsAll creates one event TermFields record that repeats each -- payload selector, so every field-bearing event contributes twice.@@ -3883,7 +4079,7 @@ nl $ [ "data " <> rcName rc <> "Data = " <> rcName rc <> "Data" ]- ++ recordFields [(name, renderDomainType importPlan a fieldType) | (name, fieldType) <- rcFields rc]+ ++ recordFields [(fieldSelector identity, renderDomainType importPlan a fieldType) | (identity, fieldType) <- rcFields rc] ++ [" deriving stock (Generic, Eq, Show)"] recordFields :: [(Text, Text)] -> [Text]@@ -3988,8 +4184,44 @@ ] ) +generatedNominalDomainImports :: Agg -> [Text]+generatedNominalDomainImports aggregate+ | null nominals = []+ | otherwise =+ [ "import "+ <> generatedNominalModule (aContext aggregate)+ <> " ("+ <> T.intercalate ", " (concatMap importsFor nominals)+ <> ")"+ ]+ where+ nominals = stableNominals (aGeneratedNominals aggregate)+ importsFor nominal = case resolvedNominalRepresentation nominal of+ IdRepresentation prefix+ | Just _ <- idDomainContractFor (aLanguageContract aggregate) prefix ->+ resolvedNominalName nominal+ : ["parse" <> resolvedNominalName nominal | needsParser nominal]+ _ -> [resolvedNominalName nominal <> " (..)"]+ needsParser nominal =+ any+ (\register -> rrType register == AggregateNominal nominal && case rrInitial register of InitialId {} -> True; _ -> False)+ (aRegs aggregate)+ domainStaticImports :: Agg -> [Text]-domainStaticImports = Set.toAscList . Set.unions . map aggregateSourceStaticImports . domainAggregateSources+domainStaticImports aggregate =+ Set.toAscList (Set.delete timeTypeImport sourceImports <> Set.fromList timeImports)+ where+ sourceImports = Set.unions (map aggregateSourceStaticImports (domainAggregateSources aggregate))+ timeTypeImport = "Data.Time.Clock (UTCTime)"+ usesTimeType = AggregateTime `elem` aggregateTypes aggregate+ usesTimeLiteral = any (\register -> case rrInitial register of InitialTime {} -> True; _ -> False) (aRegs aggregate)+ timeImports+ | usesTimeLiteral =+ [ "Data.Time.Calendar (fromGregorian)",+ "Data.Time.Clock (UTCTime (..), picosecondsToDiffTime)"+ ]+ | usesTimeType = [timeTypeImport]+ | otherwise = [] domainAggregateSources :: Agg -> [AggregateHaskellSource] domainAggregateSources aggregate =@@ -4050,20 +4282,15 @@ "import " <> aGenPrefix a <> ".Domain" ] ++ generatedNominalCodecImports (aggregateCheckedService a) (aContext a) (codecGeneratedNominals a)- ++ ( 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, withText, (.:), (.=))"]- )- ++ [ "import Data.Aeson.Types (Parser, explicitParseField, parseEither)",+ ++ ["import Control.Monad (unless)" | codecUsesUnknownFieldRejection a]+ ++ [codecAesonImport a]+ ++ ["import Data.Aeson.Key qualified as Key" | codecUsesKeyMap a]+ ++ ["import Data.Aeson.KeyMap qualified as KeyMap" | codecUsesKeyMap a]+ ++ [ "import Data.Aeson.Types (" <> T.intercalate ", " (codecAesonTypesImports a) <> ")", "import Data.List.NonEmpty (NonEmpty (..))", "import Data.List.NonEmpty qualified as NonEmpty" ]- ++ ( if hasMappedCodec a+ ++ ( if codecUsesMap a then ["import Data.Map.Strict (Map)", "import Data.Map.Strict qualified as Map"] else [] )@@ -4073,7 +4300,7 @@ ++ ["import Data.KindID qualified as KindID" | hasConsumerNominalIdCodec a] ++ ["import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText)" | hasEnforcedConsumerNominalIdCodec a] ++ ["import Keiro.Codec.Nominal (nominalFromRepresentation, nominalToRepresentation)" | hasConsumerNominalCodec a]- ++ ["import Keiro.Codec.Structural (bindingFromShape, bindingToShape)" | hasMappedCodec a]+ ++ ["import Keiro.Codec.Structural (bindingFromShape, bindingToShape)" | hasStructuralMappedCodec a] ++ [ "import Keiro.Codec (Codec (..), EventType (..))", upcasterImport a ]@@ -4097,16 +4324,27 @@ "mapLeftText :: Either String b -> Either Text b", "mapLeftText = either (Left . T.pack) Right", "",- "_renderEventTypes :: NonEmpty EventType -> String",- "_renderEventTypes =",+ "renderExpectedEventTypes :: NonEmpty EventType -> String",+ "renderExpectedEventTypes =", " T.unpack", " . T.intercalate \", \"", " . map (\\(EventType eventTypeName) -> eventTypeName)", " . NonEmpty.toList" ]- ++ ( if hasMappedCodec a+ ++ ( if codecUsesOptionalFieldHelper a then [ "",+ "parseOptionalField :: Parser fieldValue -> (Value -> Parser fieldValue) -> KeyMap.KeyMap Value -> Key.Key -> Parser fieldValue",+ "parseOptionalField onMissing parseItem objectValue key =",+ " case KeyMap.lookup key objectValue of",+ " Nothing -> onMissing",+ " Just _ -> explicitParseField parseItem objectValue key"+ ]+ else []+ )+ ++ ( if codecUsesUnknownFieldRejection a+ then+ [ "", "rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser ()", "rejectUnknownFields label allowed objectValue =", " unless (null extras) (fail (label <> \" contains unknown fields: \" <> show extras))",@@ -4129,6 +4367,189 @@ hasMappedCodec :: Agg -> Bool hasMappedCodec = not . null . codecMappedDeclarations +hasStructuralMappedCodec :: Agg -> Bool+hasStructuralMappedCodec = any isStructural . codecMappedDeclarations+ where+ isStructural ResolvedStructural {} = True+ isStructural ResolvedOpaque {} = False++codecAesonImport :: Agg -> Text+codecAesonImport aggregate =+ "import Data.Aeson (" <> T.intercalate ", " imports <> ")"+ where+ imports =+ [if codecUsesValueConstructors aggregate then "Value (..)" else "Value"]+ <> ["object" | codecUsesObject aggregate]+ <> ["parseJSON" | codecUsesParseJSON aggregate]+ <> ["toJSON" | codecUsesToJSON aggregate]+ <> ["withObject"]+ <> ["withText" | codecUsesWithText aggregate]+ <> ["(.:)" | codecUsesDotColon aggregate]+ <> ["(.=)" | codecUsesObject aggregate]++codecUsesObject :: Agg -> Bool+codecUsesObject aggregate =+ not (null (aEvents aggregate)) || any structuralUsesObject (codecMappedDeclarations aggregate)+ where+ structuralUsesObject (ResolvedStructural _ shape) = case shape of REnum {} -> False; _ -> True+ structuralUsesObject ResolvedOpaque {} = False++codecAesonTypesImports :: Agg -> [Text]+codecAesonTypesImports aggregate =+ ["Parser" | codecUsesParserType aggregate]+ <> ["explicitParseField" | codecUsesExplicitParseField aggregate]+ <> ["parseEither"]++codecUsesParserType :: Agg -> Bool+codecUsesParserType aggregate =+ codecUsesWithText aggregate || hasStructuralMappedCodec aggregate++codecUsesExplicitParseField :: Agg -> Bool+codecUsesExplicitParseField aggregate =+ codecUsesWithText aggregate || any structuralUsesExplicitParseField (codecMappedDeclarations aggregate)+ where+ structuralUsesExplicitParseField (ResolvedStructural _ shape) = case shape of+ RRecord _ _ fields -> not (null fields)+ REnum {} -> False+ RUnion {} -> True+ structuralUsesExplicitParseField ResolvedOpaque {} = False++codecUsesWithText :: Agg -> Bool+codecUsesWithText aggregate =+ any nominalUsesWithText (codecGeneratedNominals aggregate <> codecConsumerNominals aggregate)+ || any mappedUsesWithText (codecMappedDeclarations aggregate)+ where+ nominalUsesWithText nominal = case resolvedNominalRepresentation nominal of+ EnumRepresentation {} -> True+ IdRepresentation {} -> case resolvedNominalOwnership nominal of ConsumerNominal {} -> True; GeneratedNominal -> False+ ScalarRepresentation {} -> False+ mappedUsesWithText (ResolvedStructural _ shape) = case shape of+ REnum {} -> True+ RUnion {} -> True+ RRecord {} -> False+ mappedUsesWithText ResolvedOpaque {} = False++codecUsesDotColon :: Agg -> Bool+codecUsesDotColon aggregate = any fieldUsesDotColon (concatMap rcFields (aEvents aggregate))+ where+ fieldUsesDotColon (_, resolvedType) = case resolvedType of+ AggregateNominal nominal -> case (resolvedNominalOwnership nominal, resolvedNominalRepresentation nominal) of+ (GeneratedNominal, EnumRepresentation {}) -> False+ (ConsumerNominal {}, IdRepresentation {}) -> False+ (ConsumerNominal {}, EnumRepresentation {}) -> False+ _ -> True+ _ -> case fieldCat aggregate resolvedType of+ MappedStructuralCat {} -> False+ _ -> True++codecUsesKeyMap :: Agg -> Bool+codecUsesKeyMap aggregate = codecUsesOptionalFieldHelper aggregate || codecUsesUnknownFieldRejection aggregate++codecUsesUnknownFieldRejection :: Agg -> Bool+codecUsesUnknownFieldRejection = any declarationRejectsUnknown . codecMappedDeclarations+ where+ declarationRejectsUnknown (ResolvedStructural _ shape) = case shape of+ RRecord _ RejectUnknown _ -> True+ RUnion encoding _ -> ueUnknownFields encoding == RejectUnknown+ _ -> False+ declarationRejectsUnknown ResolvedOpaque {} = False++codecUsesMap :: Agg -> Bool+codecUsesMap = any declarationUsesMap . codecMappedDeclarations+ where+ declarationUsesMap (ResolvedStructural _ shape) = any typeUsesMap (shapeTypeExpressions shape)+ declarationUsesMap ResolvedOpaque {} = False++codecUsesParseJSON :: Agg -> Bool+codecUsesParseJSON aggregate = any (declarationUsesAesonConversion aggregate) (codecMappedDeclarations aggregate)++codecUsesToJSON :: Agg -> Bool+codecUsesToJSON aggregate =+ any directOpaqueField (concatMap rcFields (aEvents aggregate))+ || any (declarationUsesAesonConversion aggregate) (codecMappedDeclarations aggregate)+ where+ directOpaqueField (_, resolvedType) = case fieldCat aggregate resolvedType of MappedOpaqueCat {} -> True; _ -> False++codecUsesValueConstructors :: Agg -> Bool+codecUsesValueConstructors = any declarationUsesConstructors . codecMappedDeclarations+ where+ declarationUsesConstructors (ResolvedStructural _ shape) =+ case shape of REnum {} -> True; _ -> any typeUsesOptional (shapeTypeExpressions shape)+ declarationUsesConstructors ResolvedOpaque {} = False++declarationUsesAesonConversion :: Agg -> ResolvedMappedDecl -> Bool+declarationUsesAesonConversion aggregate (ResolvedStructural _ shape) =+ any (typeUsesAesonConversion aggregate) (shapeTypeExpressions shape)+declarationUsesAesonConversion _ ResolvedOpaque {} = False++shapeTypeExpressions :: ResolvedMappedShape -> [ResolvedTypeExpr]+shapeTypeExpressions = \case+ RRecord _ _ fields -> map rwfType fields+ REnum {} -> []+ RUnion _ arms -> mapMaybe rwaPayload arms++typeUsesMap :: ResolvedTypeExpr -> Bool+typeUsesMap =+ foldTypeExpr+ TypeExprAlgebra+ { onText = False,+ onInt = False,+ onInteger = False,+ onBool = False,+ onNatural = False,+ onTime = False,+ onJson = False,+ onOptional = id,+ onList = id,+ onMap = const True,+ onRef = const False+ }++typeUsesOptional :: ResolvedTypeExpr -> Bool+typeUsesOptional =+ foldTypeExpr+ TypeExprAlgebra+ { onText = False,+ onInt = False,+ onInteger = False,+ onBool = False,+ onNatural = False,+ onTime = False,+ onJson = False,+ onOptional = const True,+ onList = id,+ onMap = id,+ onRef = const False+ }++typeUsesAesonConversion :: Agg -> ResolvedTypeExpr -> Bool+typeUsesAesonConversion aggregate =+ foldTypeExpr+ TypeExprAlgebra+ { onText = True,+ onInt = True,+ onInteger = True,+ onBool = True,+ onNatural = True,+ onTime = True,+ onJson = False,+ onOptional = id,+ onList = const True,+ onMap = const True,+ onRef = \key -> case aTypeGraph aggregate >>= \graph -> Map.lookup key (tgDeclarations graph) of+ Just ResolvedOpaque {} -> True+ _ -> False+ }++codecUsesOptionalFieldHelper :: Agg -> Bool+codecUsesOptionalFieldHelper aggregate =+ any structuralHasOptionalField (codecMappedDeclarations aggregate)+ where+ structuralHasOptionalField (ResolvedStructural _ (RRecord _ _ fields)) =+ any ((== POptional) . rwfPresence) fields+ structuralHasOptionalField (ResolvedStructural _ _) = False+ structuralHasOptionalField ResolvedOpaque {} = False+ hasConsumerNominalCodec :: Agg -> Bool hasConsumerNominalCodec = not . null . codecConsumerNominals @@ -4308,16 +4729,16 @@ ++ [" ]"] lead 0 = " [ " lead _ = " , "- encodeField (n, ty) =- tshow n+ encodeField (identity, ty) =+ tshow (fieldWireKey identity) <> " .= "- <> encodeFieldValue n ty- encodeFieldValue name ty = case ty of- AggregateNominal nominal -> encodeNominalValue nominal ("payload." <> name)+ <> encodeFieldValue (fieldSelector identity) ty+ encodeFieldValue selector ty = case ty of+ AggregateNominal nominal -> encodeNominalValue nominal ("payload." <> selector) _ -> case fieldCat a ty of- MappedStructuralCat declaration _ -> "encode" <> sdName declaration <> "Mapped payload." <> name- MappedOpaqueCat {} -> "toJSON payload." <> name- _ -> "payload." <> name+ MappedStructuralCat declaration _ -> "encode" <> sdName declaration <> "Mapped payload." <> selector+ MappedOpaqueCat {} -> "toJSON payload." <> selector+ _ -> "payload." <> selector encodeNominalValue nominal value = case resolvedNominalOwnership nominal of GeneratedNominal -> case resolvedNominalRepresentation nominal of IdRepresentation {} -> lowerFirst (resolvedNominalName nominal) <> "Text " <> value@@ -4359,12 +4780,12 @@ | (index, field) <- zip [(0 :: Int) ..] fields ] ++ [" )"]- decodeField (n, ty) = case ty of- AggregateNominal nominal -> decodeNominalField n nominal+ decodeField (identity, ty) = case ty of+ AggregateNominal nominal -> decodeNominalField (fieldWireKey identity) nominal _ -> case fieldCat a ty of- MappedStructuralCat declaration _ -> "explicitParseField parse" <> sdName declaration <> "Mapped o " <> tshow n- MappedOpaqueCat {} -> "o .: " <> tshow n- _ -> "o .: " <> tshow n+ MappedStructuralCat declaration _ -> "explicitParseField parse" <> sdName declaration <> "Mapped o " <> tshow (fieldWireKey identity)+ MappedOpaqueCat {} -> "o .: " <> tshow (fieldWireKey identity)+ _ -> "o .: " <> tshow (fieldWireKey identity) decodeNominalField name nominal = case resolvedNominalOwnership nominal of GeneratedNominal -> case resolvedNominalRepresentation nominal of IdRepresentation prefix -> case idDomainContractFor (aLanguageContract a) prefix of@@ -4418,11 +4839,12 @@ [ reference | nominal <- codecConsumerNominals aggregate, ConsumerNominal binding <- [resolvedNominalOwnership nominal],- reference <-- [ haskellTypeReference (consumerNominalHaskell binding),- qualifiedValueReference (consumerNominalBinding binding)- ]+ reference <- qualifiedValueReference (consumerNominalBinding binding) : nominalParserTypeReferences nominal binding ]+ nominalParserTypeReferences nominal binding = case resolvedNominalRepresentation nominal of+ IdRepresentation {} -> [haskellTypeReference (consumerNominalHaskell binding)]+ EnumRepresentation {} -> [haskellTypeReference (consumerNominalHaskell binding)]+ ScalarRepresentation {} -> [] mappedReferences = [ reference | ResolvedStructural declaration _ <- codecMappedDeclarations aggregate,@@ -4600,7 +5022,7 @@ [ "parse" <> name <> "Shape = withText " <> tshow (name <> "Shape") <> " $ \\tag -> case tag of" ] <> [" " <> tshow (weTag entry) <> " -> pure " <> shapeConstructor (weCtor entry) | entry <- entries]- <> [" tag -> " <> renderUnknownFailure (name <> " wire value") "tag" (map weTag entries)],+ <> [" unknownTag -> " <> renderUnknownFailure (name <> " wire value") "unknownTag" (map weTag entries)], onUnion = \encoding arms -> nl $ [ "parse" <> name <> "Shape = withObject " <> tshow (name <> "Shape") <> " $ \\objectValue -> do",@@ -4644,15 +5066,12 @@ PRequired -> "explicitParseField (" <> decoder <> ") objectValue " <> key POptional ->- "(case KeyMap.lookup (Key.fromText "- <> key- <> ") objectValue of Nothing -> "+ "parseOptionalField (" <> missing- <> "; Just _ -> explicitParseField ("+ <> ") (" <> decoder <> ") objectValue " <> key- <> ")" where key = tshow (rwfKey field) decoder = decodeShapeExpr a graph (rwfType field)@@ -4751,7 +5170,7 @@ <> variable <> " <> " <> tshow "; expected one of: "- <> " <> _renderEventTypes "+ <> " <> renderExpectedEventTypes " <> eventTypesName aggregate <> ")" @@ -4763,7 +5182,10 @@ hasVersion2Ownership = any ((/= LegacyHoleImplementation) . tImplementation) . aTransitions transitionEntries :: Agg -> [(Int, Transition)]-transitionEntries aggregate = zip [1 ..] (aTransitions aggregate)+transitionEntries aggregate =+ [ (layoutDeclarationIndex entry, layoutTransition entry)+ | entry <- transitionLayout (aTransitions aggregate)+ ] transitionStem :: Int -> Transition -> Text transitionStem index transition =@@ -4801,7 +5223,9 @@ outputFunctionName transitionIndex transition emitIndex eventName ) | aggregate <- [value | NAggregate value <- specNodes spec],- (transitionIndex, transition) <- zip [1 ..] (aggTransitions aggregate),+ entry <- transitionLayout (aggTransitions aggregate),+ let transitionIndex = layoutDeclarationIndex entry+ transition = layoutTransition entry, (emitIndex, eventName) <- zip [1 ..] (tEmits transition), Right GeneratedCommandIdentity {} <- [eventOutputMapping spec aggregate transition emitIndex eventName] ]@@ -4872,6 +5296,32 @@ maybe [] pure (resolvedTransitionGuard resolved) <> map snd (resolvedTransitionWrites resolved) +-- | Guard expressions only.+--+-- A guard renders its operands as @K.Index … SomeType@ annotations and so needs+-- each operand type in scope. A write renders @B.slot \@"x" =: d.x@, whose type+-- is inferred — importing its source type adds an unused import, which is an+-- error under the generated-output @-Werror@. Literals name their type in either+-- position and are collected separately.+generatedTransitionGuards :: [ResolvedGeneratedTransition] -> [TypedScalarExpr]+generatedTransitionGuards = concatMap (maybe [] pure . resolvedTransitionGuard)++-- | Every literal node in an expression tree.+typedExpressionLiterals :: TypedScalarExpr -> [TypedScalarExpr]+typedExpressionLiterals expression = own <> concatMap typedExpressionLiterals (typedExpressionChildren expression)+ where+ own = case typedScalarNode expression of+ TypedLiteral {} -> [expression]+ _ -> []++-- | Types named by literal construction anywhere in an expression.+typedExpressionLiteralTypes :: TypedScalarExpr -> [ResolvedAggregateType]+typedExpressionLiteralTypes expression = own <> concatMap typedExpressionLiteralTypes (typedExpressionChildren expression)+ where+ own = case typedScalarNode expression of+ TypedLiteral {} -> [typedScalarType expression]+ _ -> []+ anyTypedExpression :: (TypedScalarExpr -> Bool) -> TypedScalarExpr -> Bool anyTypedExpression predicate expression = predicate expression || any (anyTypedExpression predicate) (typedExpressionChildren expression)@@ -5108,7 +5558,7 @@ <> " inCtor" <> tCommand transition <> " (#"- <> fieldName+ <> commandFieldSelector aggregate (tCommand transition) fieldName <> " :: K.Index (" <> commandFieldsType transition <> ") "@@ -5127,7 +5577,8 @@ renderKeikiTerm importPlan aliases aggregate transition expression = case typedScalarNode expression of TypedLiteral value -> renderedAtom (renderKeikiLiteral importPlan aggregate (typedScalarType expression) value) TypedRoot (ScalarRegisterRoot registerName _) -> renderedAtom ("B.reg @" <> tshow registerName)- TypedRoot (ScalarCommandRoot fieldName _) -> renderedAtom ("d." <> fieldName)+ TypedRoot (ScalarCommandRoot fieldName _) ->+ renderedAtom ("d." <> commandFieldSelector aggregate (tCommand transition) fieldName) TypedProject provenance projection -> renderedAtom (projectionAliasFor aliases (StructuralProjectionAlias provenance projection)) TypedAdd _ left right -> arithmetic 6 ".+" left right@@ -5166,7 +5617,7 @@ <> " inCtor" <> tCommand transition <> " (#"- <> fieldName+ <> commandFieldSelector aggregate (tCommand transition) fieldName <> " :: K.Index (" <> commandFieldsType transition <> ") "@@ -5260,7 +5711,11 @@ ++ ["import Data.Time.Calendar (fromGregorian)" | expressionUsesTimeLiteral] ++ ["import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime)" | expressionUsesTimeLiteral] ++ ["import Numeric.Natural (Natural)" | expressionUsesNaturalLiteral]- ++ generatedNominalTypeImportsForService (aggregateCheckedService aggregate) (aContext aggregate) generatedExpressionNominals+ ++ generatedNominalTypeImportsWithParsers+ (aggregateCheckedService aggregate)+ (aContext aggregate)+ generatedExpressionNominals+ generatedLiteralNominals ++ structuralProjectionImport ++ generatedNominalProjectionImport ++ consumerNominalProjectionImport@@ -5329,7 +5784,11 @@ ] consumerImports = T.lines (renderPlannedImports importPlan)- expressionImportTypes = nub (concatMap typedExpressionImportTypes resolvedExpressions)+ expressionImportTypes =+ nub+ ( concatMap typedExpressionImportTypes (generatedTransitionGuards resolvedTransitions)+ <> concatMap typedExpressionLiteralTypes resolvedExpressions+ ) consumerLiteralNominals = nub [nominal | expression <- resolvedExpressions, nominal <- typedConsumerLiteralNominals expression] importPlan = transducerImportPlan aggregate expressionImportTypes consumerLiteralNominals generatedExpressionNominals =@@ -5338,6 +5797,14 @@ | expression <- resolvedExpressions, nominal <- typedGeneratedNominals expression ]+ -- Only a literal names `parse<Id>`; see 'generatedNominalTypeImportsWithParsers'.+ generatedLiteralNominals =+ stableNominals+ [ nominal+ | expression <- resolvedExpressions,+ literal <- typedExpressionLiterals expression,+ nominal <- typedGeneratedNominals literal+ ] expressionUsesTimeLiteral = any (anyTypedExpression isTimeLiteral) resolvedExpressions expressionUsesNaturalLiteral = any (anyTypedExpression isNaturalLiteral) resolvedExpressions expressionUsesConsumerNominalLiteral = not (null consumerLiteralNominals)@@ -5476,8 +5943,11 @@ where entries = [ (source, edgeIndex, transitionIndex, transition)- | (source, transitions) <- groupTransitionEntriesBySource aggregate,- (edgeIndex, (transitionIndex, transition)) <- zip [0 ..] transitions+ | (source, transitions) <- groupTransitionLayoutBySource (transitionLayout (aTransitions aggregate)),+ entry <- transitions,+ let edgeIndex = layoutOutgoingIndex entry+ transitionIndex = layoutDeclarationIndex entry+ transition = layoutTransition entry ] foldFingerprintExpression :: Agg -> Text@@ -5497,13 +5967,12 @@ ] groupTransitionEntriesBySource :: Agg -> [(Text, [(Int, Transition)])]-groupTransitionEntriesBySource aggregate = go [] (transitionEntries aggregate)- where- go accumulated [] = reverse accumulated- go accumulated (entry@(_, transition) : remaining) =- let source = tSource transition- (same, rest) = span ((== source) . tSource . snd) remaining- in go ((source, entry : same) : accumulated) rest+groupTransitionEntriesBySource aggregate =+ [ ( source,+ [(layoutDeclarationIndex entry, layoutTransition entry) | entry <- entries]+ )+ | (source, entries) <- groupTransitionLayoutBySource (transitionLayout (aTransitions aggregate))+ ] generatedFromBlock :: HaskellImportPlan -> Agg -> [ResolvedGeneratedTransition] -> (Text, [(Int, Transition)]) -> [Text] generatedFromBlock importPlan aggregate resolvedTransitions (source, transitions) =@@ -5583,15 +6052,15 @@ generatedOutputLines :: Agg -> Int -> Transition -> Int -> Name -> [Text] generatedOutputLines aggregate transitionIndex transition emitIndex eventName = case outputMappingFor aggregate transitionIndex emitIndex of- GeneratedCommandIdentity _ fields -> case fields of+ GeneratedCommandIdentity sourceCommand fields -> case fields of [] -> [" B.emit wire" <> eventName <> " B.oNil"] _ -> [ " B.emit wire" <> eventName <> " (" <> eventName <> "TermFields" ] <> [ lead fieldIndex- <> outputSelector field+ <> resolvedSelector sourceCommand field <> " = d."- <> outputSelector field+ <> resolvedSelector sourceCommand field | (fieldIndex, field) <- zip [0 :: Int ..] fields ] <> [" })"]@@ -5605,7 +6074,20 @@ where lead 0 = " { " lead _ = " , "+ resolvedSelector sourceCommand copiedField =+ commandFieldSelector aggregate sourceCommand (outputSelector copiedField) +commandFieldSelector :: Agg -> Name -> Name -> Text+commandFieldSelector aggregate commandName dslFieldName =+ case [ fieldSelector identity+ | command <- aCommands aggregate,+ rcName command == commandName,+ (identity, _) <- rcFields command,+ fieldDslName identity == dslFieldName+ ] of+ selector : _ -> selector+ [] -> error "validated generated command-field selector was not found"+ outputMappingFor :: Agg -> Int -> Int -> EventOutputMapping outputMappingFor aggregate transitionIndex emitIndex = fromMaybe@@ -5703,8 +6185,7 @@ "import " <> aGenPrefix aggregate <> ".Transducer ("- <> lowerFirst (aName aggregate)- <> "FoldFingerprint, "+ <> (if hasSnapshot aggregate then lowerFirst (aName aggregate) <> "FoldFingerprint, " else "") <> lowerFirst (aName aggregate) <> "Transducer)" | otherwise =@@ -5761,7 +6242,15 @@ emitProjection :: Agg -> Text emitProjection a = case aProjection a of- Nothing -> nl (renderGeneratedLanguagePragmas [] <> [generatedBanner, "module " <> aGenPrefix a <> ".Projection () where"])+ Nothing ->+ nl+ ( renderGeneratedLanguagePragmas []+ <> [ generatedBanner,+ "module " <> aGenPrefix a <> ".Projection () where",+ "",+ "-- No projection declarations are present; this module keeps the generated manifest inventory total."+ ]+ ) Just p -> nl [ generatedBanner,@@ -6003,16 +6492,21 @@ | otherwise = rcName event <> "TermFields\n"- <> nl (valueRecord [(fieldName, outputFieldValue fieldName fieldType) | (fieldName, fieldType) <- rcFields event])+ <> nl+ ( valueRecord+ [ (fieldSelector identity, outputFieldValue identity fieldType)+ | (identity, fieldType) <- rcFields event+ ]+ ) command = commandForTransition aggregate transition- outputFieldValue fieldName fieldType- | Just commandType <- lookup fieldName (rcFields command),+ outputFieldValue identity fieldType+ | Just (commandIdentity, commandType) <- find ((== fieldDslName identity) . fieldDslName . fst) (rcFields command), commandType == fieldType =- "d." <> fieldName- | Just register <- find ((== fieldName) . rrName) (aRegs aggregate),+ "d." <> fieldSelector commandIdentity+ | Just register <- find ((== fieldDslName identity) . rrName) (aRegs aggregate), rrType register == fieldType =- "B.reg @" <> tshow fieldName- | otherwise = "error " <> tshow ("HOLE: fill output field " <> rcName event <> "." <> fieldName)+ "B.reg @" <> tshow (fieldDslName identity)+ | otherwise = "error " <> tshow ("HOLE: fill output field " <> rcName event <> "." <> fieldDslName identity) valueRecord fields = [ lead fieldIndex <> fieldName <> " = " <> fieldValue | (fieldIndex, (fieldName, fieldValue)) <- zip [0 :: Int ..] fields@@ -6022,12 +6516,26 @@ lead _ = " , " emitHoleImplementation :: Agg -> Int -> Transition -> [Text]-emitHoleImplementation _ index transition+emitHoleImplementation aggregate index transition | tImplementation transition /= HoleImplementation = [] | otherwise = [ "", "-- HOLE: add the predicate and ordered register updates for this transition.", "-- The generated transducer still owns command matching, mode, emits, and goto.",+ holeFunctionName index transition+ <> " :: "+ <> payloadProjectionType aggregate transition+ <> " -> B.EdgeBuilder "+ <> aName aggregate+ <> "Regs "+ <> aName aggregate+ <> "Command "+ <> aName aggregate+ <> "Event "+ <> aVertexType aggregate+ <> " ('Just ("+ <> commandFieldsType transition+ <> ")) writes writes ()", holeFunctionName index transition <> " _d = B.requireGuard K.PTop", "", "-- Bump this token whenever the Hole predicate or updates change.",@@ -6107,13 +6615,10 @@ -- 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+groupBySource a =+ [ (source, map layoutTransition entries)+ | (source, entries) <- groupTransitionLayoutBySource (transitionLayout (transitionsOf a))+ ] -- We don't keep the original Aggregate around in Agg, so reconstruct -- transitions from a stored field. (Filled in resolveAgg via aTransitions.)@@ -6361,9 +6866,7 @@ sectionsOf = T.intercalate "\n\n" . filter (not . T.null) . map (T.intercalate "\n\n") lowerFirst :: Text -> Text-lowerFirst t = case T.uncons t of- Just (c, rest) -> T.cons (toLower c) rest- Nothing -> t+lowerFirst = generatedCase HaskellName.LogicalIdentifier False -- | Assert the shared category proof at emission time as a belt-and-braces -- guard for callers that bypass the CLI's normal validate-before-scaffold path.@@ -6373,12 +6876,26 @@ Just reason -> error (T.unpack ("keiro-dsl scaffold: illegal " <> owner <> " category " <> tshow value <> " " <> reason)) pascal :: Text -> Text-pascal t = case T.uncons t of- Just (c, rest) -> T.cons (toUpper c) rest- Nothing -> t+pascal = generatedCase HaskellName.LogicalIdentifier True pascalFromKebab :: Text -> Text-pascalFromKebab = T.concat . map pascal . T.splitOn "-"+pascalFromKebab = generatedCase HaskellName.LogicalWireWord True++generatedCase :: HaskellName.NameSourceKind -> Bool -> Text -> Text+generatedCase source upper name =+ case HaskellName.deriveHaskellName source site of+ Right derived+ | upper -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ | otherwise -> HaskellName.renderLowerCamelName (HaskellName.lowerCamel derived)+ Left _ -> name+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedHelperSite,+ HaskellName.siteLogicalName = name,+ HaskellName.siteOwner = "scaffold-renderer",+ HaskellName.siteLine = 0+ } kebabFromPascal :: Text -> Text kebabFromPascal = T.intercalate "-" . map T.toLower . splitCamel
src/Keiro/Dsl/ScaffoldRecord.hs view
@@ -4,12 +4,15 @@ -- single-line JSON after a @mapping @ prefix; old readers ignore that row kind. module Keiro.Dsl.ScaffoldRecord ( ScaffoldRecord (..),+ ScaffoldModuleRoleRow (..),+ GeneratedHaskellNamingEdition (..), renderRecord, parseRecord, recordFileName, ) where +import Data.Aeson ((.:), (.=)) import Data.Aeson qualified as Aeson import Data.ByteString.Lazy qualified as BL import Data.List (nub)@@ -18,10 +21,12 @@ import Data.Text.Encoding qualified as Text import Keiro.Dsl.BehaviorCoverage (BehaviorRecordRow (..)) import Keiro.Dsl.ExplainBindings (BindingHole (..))+import Keiro.Dsl.HaskellName (GeneratedHaskellNamingEdition (..), parseGeneratedHaskellNamingEdition, renderGeneratedHaskellNamingEdition) import Keiro.Dsl.LanguageVersion (SourceLanguage (..)) import Keiro.Dsl.MappedConsumer (MappingIdentity (..))-import Keiro.Dsl.Scaffold (ModuleKind (..))+import Keiro.Dsl.Scaffold (ModuleKind (..), ModuleRole (..)) import Keiro.Dsl.SemanticContract (EffectiveLanguageContract, effectiveLanguageContract)+import Keiro.Dsl.SidecarNames (contextLedgerFileName) import System.FilePath (isAbsolute, splitDirectories) data ScaffoldRecord = ScaffoldRecord@@ -30,6 +35,8 @@ recLayout :: !Text, recSourceLanguage :: !SourceLanguage, recLanguageContract :: !EffectiveLanguageContract,+ recNamingEdition :: !GeneratedHaskellNamingEdition,+ recModuleRoles :: ![ScaffoldModuleRoleRow], recFiles :: ![(ModuleKind, FilePath)], recMappings :: ![MappingIdentity], recIdDomains :: ![Text],@@ -39,6 +46,43 @@ } deriving stock (Eq, Show) +data ScaffoldModuleRoleRow = ScaffoldModuleRoleRow+ { srrRole :: !ModuleRole,+ srrKind :: !ModuleKind,+ srrPath :: !FilePath+ }+ deriving stock (Eq, Show)++instance Aeson.ToJSON ScaffoldModuleRoleRow where+ toJSON row =+ Aeson.object+ [ "ownerKind" .= roleOwnerKind role,+ "ownerName" .= roleOwnerName role,+ "family" .= roleFamily role,+ "kind" .= (case srrKind row of Generated -> "generated" :: Text; HoleStub -> "hole"),+ "path" .= T.pack (srrPath row)+ ]+ where+ role = srrRole row++instance Aeson.FromJSON ScaffoldModuleRoleRow where+ parseJSON = Aeson.withObject "ScaffoldModuleRoleRow" $ \fields -> do+ ownerKind <- fields .: "ownerKind"+ ownerName <- fields .: "ownerName"+ family <- fields .: "family"+ kindLabel <- fields .: "kind"+ rowKind <- case (kindLabel :: Text) of+ "generated" -> pure Generated+ "hole" -> pure HoleStub+ other -> fail ("unknown module kind: " <> T.unpack other)+ rowPath <- fields .: "path"+ pure+ ScaffoldModuleRoleRow+ { srrRole = ModuleRole ownerKind ownerName family,+ srrKind = rowKind,+ srrPath = T.unpack (rowPath :: Text)+ }+ renderRecord :: ScaffoldRecord -> Text renderRecord record = T.unlines $@@ -47,8 +91,10 @@ "module-root: " <> rootLabel, "layout: " <> recLayout record, "source-language " <> Text.decodeUtf8 (BL.toStrict (Aeson.encode (recSourceLanguage record))),- "semantic-contract " <> Text.decodeUtf8 (BL.toStrict (Aeson.encode (recLanguageContract record)))+ "semantic-contract " <> Text.decodeUtf8 (BL.toStrict (Aeson.encode (recLanguageContract record))),+ "naming-edition " <> renderGeneratedHaskellNamingEdition (recNamingEdition record) ]+ <> map ("module-role " <>) (map (Text.decodeUtf8 . BL.toStrict . Aeson.encode) (recModuleRoles record)) <> map renderFile (recFiles record) <> map renderMapping (recMappings record) <> map ("id-domain " <>) (recIdDomains record)@@ -78,6 +124,8 @@ layout <- exactlyOne "layout: " rows sourceLanguage <- parseSourceLanguage rows languageContract <- parseLanguageContract sourceLanguage rows+ namingEdition <- parseNamingEdition rows+ moduleRoles <- traverse parseModuleRole (filter ("module-role " `T.isPrefixOf`) rows) files <- traverse parseFile (filter isFileRow rows) ordinaryMappings <- traverse (parseMapping "mapping ") (filter ("mapping " `T.isPrefixOf`) rows) nominalMappings <- traverse (parseMapping "nominal-mapping ") (filter ("nominal-mapping " `T.isPrefixOf`) rows)@@ -96,6 +144,8 @@ recLayout = layout, recSourceLanguage = sourceLanguage, recLanguageContract = languageContract,+ recNamingEdition = namingEdition,+ recModuleRoles = moduleRoles, recFiles = files, recMappings = mappings, recIdDomains = idDomains,@@ -127,6 +177,18 @@ parseBehaviorRequirement row = do payload <- T.stripPrefix "behavior " row Aeson.decodeStrict' (Text.encodeUtf8 payload)+ parseModuleRole row = do+ payload <- T.stripPrefix "module-role " row+ decoded <- Aeson.decodeStrict' (Text.encodeUtf8 payload)+ checkedRole decoded+ checkedRole roleRow = do+ path <- checkedPath (T.pack (srrPath roleRow))+ pure roleRow {srrPath = path}+ checkedPath pathText =+ let path = T.unpack pathText+ in if null path || isAbsolute path || ".." `elem` splitDirectories path+ then Nothing+ else Just path parseSourceLanguage rows = case filter ("source-language " `T.isPrefixOf`) rows of [] -> Just LegacyUnversioned [row] -> do@@ -140,6 +202,10 @@ contract <- Aeson.decodeStrict' (Text.encodeUtf8 payload) if contract == effectiveLanguageContract sourceLanguage then Just contract else Nothing _ -> Nothing+ parseNamingEdition rows = case filter ("naming-edition " `T.isPrefixOf`) rows of+ [] -> Just LegacyNamingV1+ [row] -> T.stripPrefix "naming-edition " row >>= parseGeneratedHaskellNamingEdition+ _ -> Nothing hasDuplicateMappingNames mappings = let names = map mappingSpecName mappings in length names /= length (nub names)@@ -159,7 +225,7 @@ in length keys /= length (nub keys) recordFileName :: Text -> FilePath-recordFileName context = "keiro-dsl-scaffold-record." <> T.unpack context <> ".txt"+recordFileName = contextLedgerFileName mappingRowPrefix :: MappingIdentity -> Text mappingRowPrefix NominalMapping {} = "nominal-mapping "
src/Keiro/Dsl/ScaffoldRun.hs view
@@ -20,6 +20,7 @@ planScaffoldWithGoldens, executeServiceScaffold, executeServiceScaffoldWithRuntimePackage,+ executeServiceScaffoldWithRuntimePackageAndNameMigrations, executeScaffold, executeScaffoldWithLanguage, renderRefusals,@@ -29,9 +30,21 @@ -- -- $shared+ planningGatePipeline,+ planningRefusalDiagnostics,+ checkServiceDiagnostics,+ inertNodesOf,+ renderInertNodeSection,+ withSidecarMovesApplied,+ originLine, pureRefusals,+ auditGeneratedHaskell, missingGeneratedBanners, staleAgainst,+ PreparedSourceMove,+ preparedSourceMove,+ preflightSourceMoves,+ applyPreparedSourceMoves, constraintPlan, mappingDrift, behaviorDrift,@@ -42,12 +55,14 @@ where import Data.List (sortOn)+import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as TIO import Keiro.Dsl.BehaviorCoverage (BehaviorDerivationError, BehaviorKey (..), BehaviorRecordRow (..), behaviorRecordRows, deriveBehaviorRequirements)+import Keiro.Dsl.BehaviorCoverage qualified as Behavior import Keiro.Dsl.ConformancePackage ( ConformancePackageFailure, ConformancePackageReport,@@ -61,8 +76,11 @@ import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligationKind (..), bindingHolesForService) import Keiro.Dsl.FoldFingerprint (FoldSurfaceError, aggregateFoldSurfaceForService, renderFoldSurfaceError) import Keiro.Dsl.Goldens (GoldenPayload)-import Keiro.Dsl.Grammar (Node (..), Spec (..))+import Keiro.Dsl.Grammar (EmitNode (..), Loc (..), Node (..), OperationNode (..), PgmqDispatchNode (..), Spec (..)) import Keiro.Dsl.Harness (harnessForServiceWithGoldens, harnessProcess, harnessReadModel, harnessRouter, harnessWorkflow)+import Keiro.Dsl.HaskellName (currentGeneratedHaskellNamingEdition)+import Keiro.Dsl.HaskellName qualified as HaskellName+import Keiro.Dsl.HaskellSourceMove import Keiro.Dsl.IdDomain (idDomainIdentitiesForService) import Keiro.Dsl.LanguageVersion (SourceLanguage (..), effectiveLanguageVersion, languageVersionText, sourceFormText) import Keiro.Dsl.Manifest (moduleNameOf, renderManifestForServiceWithFacade)@@ -70,12 +88,16 @@ import Keiro.Dsl.NominalType (nominalEqualityIdentitiesForService) import Keiro.Dsl.RuntimePackage (RuntimePackageName) import Keiro.Dsl.Scaffold-import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName, renderRecord)+import Keiro.Dsl.ScaffoldRecord (ScaffoldModuleRoleRow (..), ScaffoldRecord (..), parseRecord, recordFileName, renderRecord) import Keiro.Dsl.SemanticContract (CheckedService (..), checkedService, effectiveLanguageContract, legacyCheckedService) import Keiro.Dsl.ServiceHarness (DuplicateServiceFactKey (..), serviceConformanceModuleName, serviceHarnessModule)+import Keiro.Dsl.SidecarMigration+import Keiro.Dsl.SidecarNames (contextCabalFragmentFileName) import Keiro.Dsl.TypeGraph (MappedKey (..), TypeGraph (..), UseSite (..), resolveTypeGraph)-import System.Directory (createDirectoryIfMissing, doesFileExist)+import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), validateService)+import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile, renameFile) import System.FilePath (takeDirectory, (</>))+import Text.Read (readMaybe) -- $shared -- These are the pieces whole-workspace scaffolding reuses verbatim rather than@@ -102,6 +124,17 @@ GoldenRootDivergence !FilePath ![FilePath] | DuplicateConformanceFactKeys ![DuplicateServiceFactKey] | ConformancePackageRefusal !ConformancePackageFailure+ | GeneratedNameInvariantViolation ![Text]+ | NameMigrationRequired ![SourceMove]+ | NameMigrationRefusal ![Text]+ | SidecarMigrationRequired ![SidecarMove]+ | SidecarMigrationRefusal ![Text]+ | -- | Not a refusal on its own: an accompanying note that the run had already+ -- applied its sidecar renames before a later gate refused. Every other+ -- refusal says "nothing was written", which without this note is false.+ -- The renames are idempotent and forward-consistent, so re-running after+ -- fixing the refusal is correct and needs no undo.+ SidecarMovesAlreadyApplied ![SidecarMove] deriving stock (Eq, Show) -- | What one module write did. 'Unchanged' is produced only by the workspace@@ -141,6 +174,7 @@ reportOutDir :: !FilePath, reportContext :: !Context, reportDispositions :: ![(ScaffoldModule, WriteDisposition)],+ reportInertNodes :: ![(Text, Text)], reportManifestPath :: !FilePath, reportRecordPath :: !FilePath, reportPreviousSpecPath :: !(Maybe Text),@@ -153,7 +187,9 @@ reportAddedBehavior :: ![BehaviorRecordRow], reportRemovedBehavior :: ![BehaviorRecordRow], reportObsoleteOutputHooks :: ![(Text, Text)],- reportConformancePackage :: !(Maybe ConformancePackageReport)+ reportConformancePackage :: !(Maybe ConformancePackageReport),+ reportNameMoves :: ![SourceMove],+ reportSidecarMoves :: ![SidecarMove] } deriving stock (Eq, Show) @@ -208,23 +244,163 @@ planServiceScaffoldWithRuntimePackageAndGoldens :: [GoldenPayload] -> Maybe RuntimePackageName -> Context -> CheckedService -> Either [Refusal] [ScaffoldModule] planServiceScaffoldWithRuntimePackageAndGoldens goldens runtimePackage ctx service =+ planningGatePipeline ctx service modulePlan (Right ())+ where+ facadeModules = case runtimePackage of+ Nothing -> Right []+ Just _ -> fmap pure (serviceHarnessModule ctx service)+ modulePlan =+ case facadeModules of+ Left duplicates -> Left [DuplicateConformanceFactKeys duplicates]+ Right facades ->+ Right $+ stampGeneratedModules+ (checkedLanguageContract service)+ (scaffoldServiceModulesWithGoldens goldens ctx service <> facades)++-- | The one pure scaffold-planning gate sequence. Both scaffold planners and+-- both check paths consume this function, so the first reported refusal cannot+-- drift by input shape.+planningGatePipeline ::+ Context ->+ CheckedService ->+ Either [Refusal] [ScaffoldModule] ->+ Either [Refusal] () ->+ Either [Refusal] [ScaffoldModule]+planningGatePipeline ctx service modulePlan packagePlan = case traverse (aggregateFoldSurfaceForService service) [aggregate | NAggregate aggregate <- specNodes spec] of Left surfaceError -> Left [FoldSurfaceRefusal surfaceError] Right _ -> case scaffoldRefusals spec of lowering@(_ : _) -> Left [LoweringRefusal lowering]- [] -> case facadeModules of- Left duplicates -> Left [DuplicateConformanceFactKeys duplicates]- Right facades ->- let modules = stampGeneratedModules (checkedLanguageContract service) (scaffoldServiceModulesWithGoldens goldens ctx service <> facades)- in case pureRefusals ctx spec modules of- [] -> Right modules- refusals -> Left refusals+ [] -> case modulePlan of+ Left refusals -> Left refusals+ Right modules -> case packagePlan of+ Left refusals -> Left refusals+ Right () -> case pureRefusals ctx spec modules of+ [] -> Right modules+ refusals -> Left refusals where spec = checkedSpec service- facadeModules = case runtimePackage of- Nothing -> Right []- Just _ -> fmap pure (serviceHarnessModule ctx service) +-- | Validate a checked service, then run the shared planning gates unless an+-- error makes module generation unsound. 'GeneratedOccurrenceCollision' is the+-- deliberate exception: the workspace path already plans through it so the+-- stronger whole-path collision can cite every claimant. Existing diagnostics+-- retain their order and planning diagnostics follow them.+-- | The nodes a spec declares that contribute no generated module.+--+-- They are still parsed, validated, and diff-classified; naming them in the+-- scaffold report is what stops an author from concluding the toolchain lost+-- their declaration. Shared by the single-spec and workspace planners so a+-- workspace — the recommended layout — reports exactly what one spec reports.+inertNodesOf :: Spec -> [(Text, Text)]+inertNodesOf spec =+ [ (kindLabel, nodeName)+ | node <- specNodes spec,+ (kindLabel, nodeName) <- case node of+ NEmit emitNode -> [("emit", emName emitNode)]+ NPgmqDispatch dispatchNode -> [("dispatch", pdName dispatchNode)]+ NOperation operationNode -> [("operation", opName operationNode)]+ _ -> []+ ]++-- | The report line naming 'inertNodesOf', or nothing when every declaration+-- produced a module.+renderInertNodeSection :: [(Text, Text)] -> [Text]+renderInertNodeSection = \case+ [] -> []+ nodes ->+ [ "no-modules: "+ <> T.intercalate ", " [kindLabel <> " " <> nodeName | (kindLabel, nodeName) <- nodes]+ <> " (validated and diff-classified; no generated modules)"+ ]++checkServiceDiagnostics :: Maybe RuntimePackageName -> Context -> CheckedService -> [Diagnostic]+checkServiceDiagnostics runtimePackage ctx service+ | any blocksPlanning validationDiagnostics = validationDiagnostics+ | otherwise =+ validationDiagnostics+ <> case planServiceScaffoldWithRuntimePackage runtimePackage ctx service of+ Right _ -> []+ Left refusals -> planningRefusalDiagnostics refusals+ where+ validationDiagnostics = validateService service+ blocksPlanning diagnostic =+ severity diagnostic == Error+ && code diagnostic /= GeneratedOccurrenceCollision++-- | Present pure planning refusals through check's stable located diagnostic+-- vocabulary. Planner-invariant failures retain the detailed scaffold refusal+-- text in their message while receiving one machine code.+planningRefusalDiagnostics :: [Refusal] -> [Diagnostic]+planningRefusalDiagnostics = concatMap diagnosticsFor+ where+ diagnosticsFor (PathCollision path origins) = [pathCollisionDiagnostic path origins]+ diagnosticsFor (ImportCycle path) =+ [ planningError 1 GeneratedImportCycle $+ "generated/consumer import cycle "+ <> T.intercalate " -> " path+ <> "; keep bindings in a leaf module that imports only Structural.Shape.* and Keiro.Codec.Structural"+ ]+ diagnosticsFor (BehaviorRefusal errors) =+ [ planningError (behaviorErrorLine behaviorError) BehaviorDerivationInvalid $+ "behavior obligations cannot be derived soundly: " <> T.pack (show behaviorError)+ | behaviorError <- errors+ ]+ diagnosticsFor (DuplicateConformanceFactKeys duplicates) =+ [ planningError 1 ConformanceFactKeyCollision $+ "normalized service conformance fact key '"+ <> duplicateServiceFactKey duplicate+ <> "' is produced more than once"+ | duplicate <- duplicates+ ]+ diagnosticsFor refusal =+ [ planningError 1 GeneratedPlanningInvariantViolation $+ "validated service failed an internal scaffold-planning invariant: "+ <> T.intercalate " | " (renderRefusals [refusal])+ ]++ pathCollisionDiagnostic path origins =+ Diagnostic+ { line = primaryLine,+ severity = Error,+ code = GeneratedPathCollision,+ relatedLocations =+ [ (claimLine, "claimed here by " <> claimOrigin)+ | (claimLine, claimOrigin) <- remainingClaims+ ],+ message =+ "generated module path '"+ <> T.pack path+ <> "' is claimed more than once; on a case-insensitive filesystem these are one file; claimants: "+ <> T.intercalate "; " origins+ }+ where+ orderedClaims = reverse (sortOn fst [(claimLine, claimOrigin) | claimOrigin <- origins, Just claimLine <- [originLine claimOrigin]])+ (primaryLine, remainingClaims) = case orderedClaims of+ (claimLine, _) : rest -> (claimLine, rest)+ [] -> (1, [])++ behaviorErrorLine (Behavior.DuplicateBehaviorIdentity _ locations) = maximum (1 : map unLoc locations)+ behaviorErrorLine _ = 1++ planningError diagnosticLine diagnosticCode diagnosticMessage =+ Diagnostic+ { line = diagnosticLine,+ severity = Error,+ code = diagnosticCode,+ relatedLocations = [],+ message = diagnosticMessage+ }++-- | Recover the source line embedded in a generated module's origin text,+-- which scaffold formats as @<kind> <name> (line N)@.+originLine :: Text -> Maybe Int+originLine originText = do+ withoutClose <- T.stripSuffix ")" originText+ let (before, after) = T.breakOnEnd " (line " withoutClose+ if T.null before then Nothing else readMaybe (T.unpack after)+ -- | 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]@@ -243,11 +419,203 @@ collisionRefusals modules <> dependencyRefusals ctx spec modules <> [FirewallBreach breaches | not (null breaches)]+ <> [GeneratedNameInvariantViolation namingViolations | not (null namingViolations)] <> [LoweringRefusal lowering | let lowering = scaffoldRefusals spec, not (null lowering)] <> [BehaviorRefusal errors | Left errors <- [deriveBehaviorRequirements spec]] where breaches = firewallBreaches modules+ namingViolations = generatedNameInvariantViolations modules +generatedNameInvariantViolations :: [ScaffoldModule] -> [Text]+generatedNameInvariantViolations = concatMap auditGeneratedHaskell++-- | Inventory and check declarations in one generated source file. The+-- lexical mask keeps comments and literals out of the declaration inventory;+-- this is deliberately a final defense after the typed naming plan, so a+-- literal template declaration cannot bypass the checked constructors.+auditGeneratedHaskell :: ScaffoldModule -> [Text]+auditGeneratedHaskell scaffoldModule = lexicalErrors <> declarationErrors <> duplicateDeclarationErrors <> occurrenceErrors+ where+ expectedModule = moduleNameOf (modulePath scaffoldModule)+ (lexicalErrors, codeSource) = case maskNonCode (moduleText scaffoldModule) of+ Left message -> ([prefix 1 <> message], "")+ Right masked -> ([], masked)+ sourceLines = zip [1 :: Int ..] (T.lines codeSource)+ declarationErrors = moduleDeclarationErrors <> moduleSegmentErrors+ moduleDeclarationErrors = case declaredModuleName codeSource of+ Nothing -> [T.pack (modulePath scaffoldModule) <> ": missing Haskell module declaration"]+ Just declared+ | declared == expectedModule -> []+ | otherwise ->+ [ T.pack (modulePath scaffoldModule)+ <> ": declares "+ <> declared+ <> " but its planned module is "+ <> expectedModule+ ]+ moduleSegmentErrors =+ [ prefix 1 <> "module segment '" <> segment <> "' is not UpperCamelCase"+ | segment <- T.splitOn "." expectedModule,+ isLeftName (HaskellName.checkedModuleSegment (auditSite HaskellName.NodeModuleSite segment 1) segment)+ ]+ occurrenceErrors =+ concat+ [ checkCandidates lineNumber (signatureCandidates sourceLine)+ <> checkCandidates lineNumber (typeCandidates sourceLine)+ <> checkCandidates lineNumber (constructorCandidates sourceLine)+ <> checkCandidates lineNumber (topLevelValueCandidates sourceLine)+ | (lineNumber, sourceLine) <- sourceLines+ ]++ duplicateDeclarationErrors = duplicateErrors "top-level type signature" signatureCandidates <> duplicateErrors "top-level type declaration" typeCandidates+ duplicateErrors label candidates =+ [ prefix laterLine+ <> "repeated "+ <> label+ <> " '"+ <> candidate+ <> "' (first declared at line "+ <> tshow firstLine+ <> ")"+ | (candidate, declarationLines) <- Map.toAscList declarations,+ firstLine : laterLines <- [declarationLines],+ laterLine <- laterLines+ ]+ where+ declarations =+ Map.fromListWith+ (flip (++))+ [ (candidate, [lineNumber])+ | (lineNumber, sourceLine) <- sourceLines,+ sourceLine == T.stripStart sourceLine,+ candidate <- candidates sourceLine+ ]++ checkCandidates lineNumber = concatMap (checkCandidate lineNumber)+ checkCandidate lineNumber candidate+ | T.null candidate = []+ | asciiUpperInitial candidate =+ [prefix lineNumber <> "generated declaration '" <> candidate <> "' is not UpperCamelCase" | isLeftName (HaskellName.checkedUpperOccurrence (auditSite HaskellName.GeneratedTypeSite candidate lineNumber) candidate)]+ | otherwise =+ [prefix lineNumber <> "generated declaration '" <> candidate <> "' is not lowerCamelCase" | isLeftName (HaskellName.checkedLowerOccurrence (auditSite HaskellName.GeneratedValueSite candidate lineNumber) candidate)]++ prefix lineNumber = T.pack (modulePath scaffoldModule) <> ":" <> tshow lineNumber <> ": "++ auditSite kind candidate lineNumber =+ HaskellName.NameSite+ { HaskellName.siteKind = kind,+ HaskellName.siteLogicalName = candidate,+ HaskellName.siteOwner = T.pack (modulePath scaffoldModule),+ HaskellName.siteLine = lineNumber+ }++isLeftName :: Either left right -> Bool+isLeftName = \case Left _ -> True; Right _ -> False++declaredModuleName :: Text -> Maybe Text+declaredModuleName source =+ case [T.takeWhile moduleCharacter (T.drop 7 sourceLine) | sourceLine <- T.lines source, "module " `T.isPrefixOf` sourceLine] of+ declaration : _ | not (T.null declaration) -> Just declaration+ _ -> Nothing+ where+ moduleCharacter character = identifierCharacter character || character == '.'++signatureCandidates :: Text -> [Text]+signatureCandidates sourceLine+ | T.null suffix || T.any (`elem` ['=', '(', ')', '[', ']']) prefix = []+ | otherwise = filter isIdentifier (map T.strip (T.splitOn "," prefix))+ where+ (prefix, suffix) = T.breakOn "::" (T.strip sourceLine)++typeCandidates :: Text -> [Text]+typeCandidates sourceLine = case T.words (T.strip sourceLine) of+ keyword : candidate : _+ | keyword `elem` ["data", "newtype", "type"], candidate /= "family", candidate /= "instance" -> [cleanIdentifier candidate]+ _ -> []++constructorCandidates :: Text -> [Text]+constructorCandidates sourceLine+ | "|" `T.isPrefixOf` stripped = takeFollowingIdentifier (T.drop 1 stripped)+ | any (`T.isPrefixOf` stripped) ["data ", "newtype "] = takeFollowingIdentifier (T.drop 1 (snd (T.breakOn "=" stripped)))+ | otherwise = []+ where+ stripped = T.strip sourceLine+ takeFollowingIdentifier value = case T.words value of+ candidate : _ | asciiUpperInitial (cleanIdentifier candidate) -> [cleanIdentifier candidate]+ _ -> []++topLevelValueCandidates :: Text -> [Text]+topLevelValueCandidates sourceLine+ | T.null sourceLine || T.head sourceLine == ' ' || T.head sourceLine == '\t' = []+ | T.null suffix = []+ | otherwise = case T.words prefix of+ candidate : _+ | candidate `notElem` declarationKeywords,+ isIdentifier candidate ->+ [candidate]+ _ -> []+ where+ (prefix, suffix) = T.breakOn "=" sourceLine+ declarationKeywords = ["data", "newtype", "type", "class", "instance", "module", "import", "deriving", "infix", "infixl", "infixr"]++cleanIdentifier :: Text -> Text+cleanIdentifier = T.takeWhile identifierCharacter . T.dropWhile (not . identifierCharacter)++isIdentifier :: Text -> Bool+isIdentifier candidate = not (T.null candidate) && T.all identifierCharacter candidate++identifierCharacter :: Char -> Bool+identifierCharacter character =+ (character >= 'A' && character <= 'Z')+ || (character >= 'a' && character <= 'z')+ || (character >= '0' && character <= '9')+ || character == '_'+ || character == '\''++asciiUpperInitial :: Text -> Bool+asciiUpperInitial candidate = case T.uncons candidate of+ Just (first, _) -> first >= 'A' && first <= 'Z'+ Nothing -> False++data AuditLexState = AuditCode | AuditLineComment | AuditBlockComment !Int | AuditString | AuditCharacter++maskNonCode :: Text -> Either Text Text+maskNonCode = fmap T.pack . go AuditCode . T.unpack+ where+ go state input = case (state, input) of+ (AuditCode, []) -> Right []+ (AuditLineComment, []) -> Right []+ (AuditBlockComment _, []) -> Left "unterminated block comment in generated source"+ (AuditString, []) -> Left "unterminated string literal in generated source"+ (AuditCharacter, []) -> Left "unterminated character literal in generated source"+ (AuditCode, '-' : '-' : rest) -> prependSpaces 2 <$> go AuditLineComment rest+ (AuditCode, '{' : '-' : rest) -> prependSpaces 2 <$> go (AuditBlockComment 1) rest+ (AuditCode, '"' : rest) -> (' ' :) <$> go AuditString rest+ (AuditCode, '\'' : rest)+ | looksLikeCharacterLiteral rest -> (' ' :) <$> go AuditCharacter rest+ (AuditCode, character : rest) -> (character :) <$> go AuditCode rest+ (AuditLineComment, '\n' : rest) -> ('\n' :) <$> go AuditCode rest+ (AuditLineComment, _ : rest) -> (' ' :) <$> go AuditLineComment rest+ (AuditBlockComment depth, '{' : '-' : rest) -> prependSpaces 2 <$> go (AuditBlockComment (depth + 1)) rest+ (AuditBlockComment 1, '-' : '}' : rest) -> prependSpaces 2 <$> go AuditCode rest+ (AuditBlockComment depth, '-' : '}' : rest) -> prependSpaces 2 <$> go (AuditBlockComment (depth - 1)) rest+ (AuditBlockComment depth, '\n' : rest) -> ('\n' :) <$> go (AuditBlockComment depth) rest+ (AuditBlockComment depth, _ : rest) -> (' ' :) <$> go (AuditBlockComment depth) rest+ (AuditString, '\\' : _escaped : rest) -> prependSpaces 2 <$> go AuditString rest+ (AuditString, '"' : rest) -> (' ' :) <$> go AuditCode rest+ (AuditString, '\n' : _) -> Left "newline in generated string literal"+ (AuditString, _ : rest) -> (' ' :) <$> go AuditString rest+ (AuditCharacter, '\\' : _escaped : rest) -> prependSpaces 2 <$> go AuditCharacter rest+ (AuditCharacter, '\'' : rest) -> (' ' :) <$> go AuditCode rest+ (AuditCharacter, '\n' : _) -> Left "newline in generated character literal"+ (AuditCharacter, _ : rest) -> (' ' :) <$> go AuditCharacter rest++ prependSpaces count suffix = replicate count ' ' <> suffix+ looksLikeCharacterLiteral = \case+ '\\' : _escaped : '\'' : _ -> True+ _character : '\'' : _ -> True+ _ -> False+ dependencyRefusals :: Context -> Spec -> [ScaffoldModule] -> [Refusal] dependencyRefusals ctx spec modules = collisionWithConsumers <> namespaceCycles where@@ -326,17 +694,36 @@ executeServiceScaffold = executeServiceScaffoldWithRuntimePackage Nothing executeServiceScaffoldWithRuntimePackage :: Maybe RuntimePackageName -> FilePath -> Bool -> FilePath -> SourceLanguage -> Context -> CheckedService -> [ScaffoldModule] -> IO (Either [Refusal] ScaffoldReport)-executeServiceScaffoldWithRuntimePackage runtimePackage out forceGeneratedOverwrite specPath sourceLanguage ctx service plannedModules+executeServiceScaffoldWithRuntimePackage runtimePackage =+ executeServiceScaffoldWithRuntimePackageAndNameMigrations runtimePackage False++executeServiceScaffoldWithRuntimePackageAndNameMigrations :: Maybe RuntimePackageName -> Bool -> FilePath -> Bool -> FilePath -> SourceLanguage -> Context -> CheckedService -> [ScaffoldModule] -> IO (Either [Refusal] ScaffoldReport)+executeServiceScaffoldWithRuntimePackageAndNameMigrations runtimePackage applyNameMigrations out forceGeneratedOverwrite specPath sourceLanguage ctx service plannedModules | effectiveLanguageContract sourceLanguage /= checkedLanguageContract service = pure (Left [SemanticContractMismatch "source provenance and checked service selected different effective language contracts"]) | otherwise = case packagePlan of Left failures -> pure (Left (map ConformancePackageRefusal failures))- Right Nothing -> executeCheckedScaffold Nothing- Right (Just plannedPackage) -> do- prepared <- preflightConformancePackage out forceGeneratedOverwrite plannedPackage- case prepared of- Left failures -> pure (Left (map ConformancePackageRefusal failures))- Right packageReady -> executeCheckedScaffold (Just packageReady)+ Right plannedPackage -> do+ sidecarResult <- planSidecarMigrations out (ContextSidecars (specContext spec)) plannedPackage+ case sidecarResult of+ Left reasons -> pure (Left [SidecarMigrationRefusal reasons])+ Right preparedSidecars+ | not (null preparedSidecars) && not applyNameMigrations ->+ pure (Left [SidecarMigrationRequired (map preparedSidecarMove preparedSidecars)])+ | otherwise -> do+ applyPreparedSidecarMoves out preparedSidecars+ let moves = map preparedSidecarMove preparedSidecars+ -- Past this point the renames are on disk, so a later+ -- refusal's "nothing was written" needs qualifying.+ noteApplied = withSidecarMovesApplied moves+ result <- case plannedPackage of+ Nothing -> executeCheckedScaffold moves Nothing+ Just package -> do+ preparedPackage <- preflightConformancePackage out forceGeneratedOverwrite package+ case preparedPackage of+ Left failures -> pure (Left (map ConformancePackageRefusal failures))+ Right packageReady -> executeCheckedScaffold moves (Just packageReady)+ pure (either (Left . noteApplied) Right result) where spec = checkedSpec service modules = stampGeneratedModules (checkedLanguageContract service) plannedModules@@ -347,7 +734,7 @@ traverse (\packageName -> planConformancePackage (StandaloneConformanceService (contextName ctx)) packageName (serviceConformanceModuleName ctx) service) runtimePackage- executeCheckedScaffold preparedPackage =+ executeCheckedScaffold sidecarMoves preparedPackage = case deriveBehaviorRequirements spec of Left errors -> pure (Left [BehaviorRefusal errors]) Right requirements -> do@@ -357,46 +744,227 @@ else do 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- languageDrift = do- previous <- previousRecord- if recSourceLanguage previous == sourceLanguage- then Nothing- else Just (SourceLanguageDrift (recSourceLanguage previous) sourceLanguage)- currentObligations = either (const []) id (bindingHolesForService service)- newHoles = maybe [] (newBindingObligations currentObligations . recBindingObligations) previousRecord- currentBehavior = behaviorRecordRows requirements- (addedBehavior, removedBehavior) = maybe (currentBehavior, []) (behaviorDrift currentBehavior . recBehaviorRequirements) previousRecord- createDirectoryIfMissing True out- dispositions <- mapM (writeModule out) modules- let manifestPath = out </> ("keiro-dsl-manifest." <> T.unpack (specContext spec) <> ".txt")- TIO.writeFile manifestPath (renderManifestForServiceWithFacade facadeModule (T.pack specPath) modules service)- TIO.writeFile recordPath (renderRecord (currentRecord specPath sourceLanguage ctx service modules currentBehavior))- packageReport <- traverse executePreparedConformancePackage preparedPackage- pure $- Right- ScaffoldReport- { reportSpecPath = specPath,- reportOutDir = out,- reportContext = ctx,- reportDispositions = dispositions,- reportManifestPath = manifestPath,- reportRecordPath = recordPath,- reportPreviousSpecPath = recSpecPath <$> previousRecord,- reportStale = stale,- reportConsumerPlan = currentConsumerPlan,- reportConstraintPlan = constraintPlan spec currentConsumerPlan,- reportMappingDrift = drift,- reportSourceLanguageDrift = languageDrift,- reportNewHoles = newHoles,- reportAddedBehavior = addedBehavior,- reportRemovedBehavior = removedBehavior,- reportObsoleteOutputHooks = obsoleteGeneratedOutputHooks spec,- reportConformancePackage = packageReport- }+ case planRecordedSourceMoves previousRecord modules of+ Left moveErrors -> pure (Left [NameMigrationRefusal [T.pack (show moveError) | moveError <- NE.toList moveErrors]])+ Right moves -> do+ preparedMoves <- preflightSourceMoves out moves+ case preparedMoves of+ Left moveErrors -> pure (Left [NameMigrationRefusal moveErrors])+ Right prepared+ | not (null prepared) && not applyNameMigrations ->+ pure (Left [NameMigrationRequired (map preparedSourceMove prepared)])+ | otherwise -> do+ applyPreparedSourceMoves out prepared+ stale <- maybe (pure []) (existingStale out modules) previousRecord+ let currentConsumerPlan = consumerPlan spec+ drift = maybe [] (mappingDrift (consumerMappings currentConsumerPlan) . recMappings) previousRecord+ languageDrift = do+ previous <- previousRecord+ if recSourceLanguage previous == sourceLanguage+ then Nothing+ else Just (SourceLanguageDrift (recSourceLanguage previous) sourceLanguage)+ currentObligations = either (const []) id (bindingHolesForService service)+ newHoles = maybe [] (newBindingObligations currentObligations . recBindingObligations) previousRecord+ currentBehavior = behaviorRecordRows requirements+ (addedBehavior, removedBehavior) = maybe (currentBehavior, []) (behaviorDrift currentBehavior . recBehaviorRequirements) previousRecord+ createDirectoryIfMissing True out+ dispositions <- mapM (writeModule out) modules+ let manifestPath = out </> contextCabalFragmentFileName (specContext spec)+ TIO.writeFile manifestPath (renderManifestForServiceWithFacade facadeModule (T.pack specPath) modules service)+ TIO.writeFile recordPath (renderRecord (currentRecord specPath sourceLanguage ctx service modules currentBehavior))+ packageReport <- traverse executePreparedConformancePackage preparedPackage+ pure $+ Right+ ScaffoldReport+ { reportSpecPath = specPath,+ reportOutDir = out,+ reportContext = ctx,+ reportDispositions = dispositions,+ reportInertNodes = inertNodesOf spec,+ reportManifestPath = manifestPath,+ reportRecordPath = recordPath,+ reportPreviousSpecPath = recSpecPath <$> previousRecord,+ reportStale = stale,+ reportConsumerPlan = currentConsumerPlan,+ reportConstraintPlan = constraintPlan spec currentConsumerPlan,+ reportMappingDrift = drift,+ reportSourceLanguageDrift = languageDrift,+ reportNewHoles = newHoles,+ reportAddedBehavior = addedBehavior,+ reportRemovedBehavior = removedBehavior,+ reportObsoleteOutputHooks = obsoleteGeneratedOutputHooks spec,+ reportConformancePackage = packageReport,+ reportNameMoves = map preparedSourceMove prepared,+ reportSidecarMoves = sidecarMoves+ } +planRecordedSourceMoves :: Maybe ScaffoldRecord -> [ScaffoldModule] -> Either (NE.NonEmpty SourceMoveError) [SourceMove]+planRecordedSourceMoves Nothing _ = Right []+planRecordedSourceMoves (Just previous) current =+ planSourceMoves priorArtifacts current+ where+ priorArtifacts = case recModuleRoles previous of+ [] -> [(Nothing, fileKind, path) | (fileKind, path) <- recFiles previous]+ rows -> [(Just (srrRole row), srrKind row, srrPath row) | row <- rows]++data PreparedSourceMove+ = SourceMoveReady !SourceMove !Text+ | SourceMoveAlreadyApplied !SourceMove++preparedSourceMove :: PreparedSourceMove -> SourceMove+preparedSourceMove = \case+ SourceMoveReady move _ -> move+ SourceMoveAlreadyApplied move -> move++preflightSourceMoves :: FilePath -> [SourceMove] -> IO (Either [Text] [PreparedSourceMove])+preflightSourceMoves out moves = do+ prepared <- mapM preflight moves+ let errors = [message | Left message <- prepared]+ pure $ if null errors then Right [value | Right value <- prepared] else Left errors+ where+ replacements = Map.fromList [(moveOldModule move, moveNewModule move) | move <- moves]+ preflight move = do+ let oldPath = out </> moveOldPath move+ newPath = out </> moveNewPath move+ backupPath = out </> moveBackupPath move+ preparedPath = preparedSourcePath out move+ statePath = sourceMoveStatePath out move+ oldExists <- doesFileExist oldPath+ newExists <- doesFileExist newPath+ backupExists <- doesFileExist backupPath+ preparedExists <- doesFileExist preparedPath+ stateExists <- doesFileExist statePath+ case (oldExists, backupExists) of+ (True, True) -> conflict move newExists backupExists preparedExists "both legacy source and backup exist"+ (False, False) ->+ if newExists+ then conflict move newExists backupExists preparedExists "target exists without a recoverable legacy source"+ else pure (Left (T.pack (moveOldPath move) <> ": recorded legacy source is missing"))+ _ -> do+ source <- TIO.readFile (if oldExists then oldPath else backupPath)+ if moveKind move == Generated && not (any isGeneratedBannerLine (T.lines source))+ then pure (Left (T.pack (moveOldPath move) <> ": generated source lacks an exact generated banner"))+ else case rewriteHaskellModuleReferences replacements source of+ Left lexicalError -> pure (Left (T.pack (moveOldPath move) <> ": " <> T.pack (show lexicalError)))+ Right rewritten+ | not (declaresExpectedModule (moveNewModule move) rewritten) ->+ pure+ ( Left+ ( T.pack (moveOldPath move)+ <> ": transformed source does not declare expected module "+ <> moveNewModule move+ )+ )+ | otherwise -> do+ let hydrated =+ move+ { moveContentDigest = Just (contentDigest source),+ moveTransformedDigest = Just (contentDigest rewritten)+ }+ expectedState = renderSourceMoveState hydrated+ stateError <- verifyOptionalText stateExists statePath expectedState "migration state"+ preparedError <- verifyOptionalDigest preparedExists preparedPath (contentDigest rewritten) "prepared source"+ targetError <- verifyOptionalDigest newExists newPath (contentDigest rewritten) "target source"+ case [message | Just message <- [stateError, preparedError, targetError]] of+ message : _ -> pure (Left (T.pack (moveOldPath move) <> ": " <> message))+ []+ | newExists && not backupExists && not oldExists -> conflict hydrated newExists backupExists preparedExists "target has no recoverable backup"+ | newExists && backupExists && not oldExists -> pure (Right (SourceMoveAlreadyApplied hydrated))+ | otherwise -> pure (Right (SourceMoveReady hydrated rewritten))++ conflict move newExists backupExists preparedExists reason =+ pure+ ( Left+ ( T.pack (moveOldPath move)+ <> ": migration state conflicts ("+ <> reason+ <> "; target="+ <> T.pack (show newExists)+ <> ", backup="+ <> T.pack (show backupExists)+ <> ", prepared="+ <> T.pack (show preparedExists)+ <> ")"+ )+ )++ verifyOptionalText False _ _ _ = pure Nothing+ verifyOptionalText True path expected label = do+ actual <- TIO.readFile path+ pure $ if actual == expected then Nothing else Just (label <> " digest/path evidence does not match")++ verifyOptionalDigest False _ _ _ = pure Nothing+ verifyOptionalDigest True path expected label = do+ actual <- contentDigest <$> TIO.readFile path+ pure $ if actual == expected then Nothing else Just (label <> " digest does not match " <> expected)++ declaresExpectedModule expected source =+ any (T.isPrefixOf ("module " <> expected <> " ")) (T.lines source)+ || any (== ("module " <> expected)) (T.lines source)++applyPreparedSourceMoves :: FilePath -> [PreparedSourceMove] -> IO ()+applyPreparedSourceMoves out prepared = do+ -- Prepare every transformed file and durable digest record before moving a+ -- single active source. The temporary file lives beside its destination,+ -- so installation is a same-filesystem rename.+ mapM_ prepareMove prepared+ mapM_ backupMove prepared+ mapM_ installMove prepared+ where+ prepareMove preparedMove = do+ let move = preparedSourceMove preparedMove+ statePath = sourceMoveStatePath out move+ createDirectoryIfMissing True (takeDirectory statePath)+ TIO.writeFile statePath (renderSourceMoveState move)+ case preparedMove of+ SourceMoveAlreadyApplied _ -> pure ()+ SourceMoveReady _ rewritten -> do+ let path = preparedSourcePath out move+ createDirectoryIfMissing True (takeDirectory path)+ exists <- doesFileExist path+ if exists then pure () else TIO.writeFile path rewritten++ backupMove (SourceMoveAlreadyApplied _) = pure ()+ backupMove (SourceMoveReady move _) = do+ let oldPath = out </> moveOldPath move+ backupPath = out </> moveBackupPath move+ oldExists <- doesFileExist oldPath+ if oldExists+ then do+ createDirectoryIfMissing True (takeDirectory backupPath)+ renameFile oldPath backupPath+ else pure ()++ installMove preparedMove = do+ let move = preparedSourceMove preparedMove+ preparedPath = preparedSourcePath out move+ newPath = out </> moveNewPath move+ newExists <- doesFileExist newPath+ preparedExists <- doesFileExist preparedPath+ if newExists+ then if preparedExists then removeFile preparedPath else pure ()+ else do+ createDirectoryIfMissing True (takeDirectory newPath)+ renameFile preparedPath newPath++preparedSourcePath :: FilePath -> SourceMove -> FilePath+preparedSourcePath out move = out </> (moveNewPath move <> ".keiro-dsl-name-migration-prepared")++sourceMoveStatePath :: FilePath -> SourceMove -> FilePath+sourceMoveStatePath out move = out </> (moveBackupPath move <> ".keiro-dsl-name-migration-state")++renderSourceMoveState :: SourceMove -> Text+renderSourceMoveState move =+ T.unlines+ [ "keiro-dsl-name-migration-state v1",+ "old-path " <> T.pack (moveOldPath move),+ "new-path " <> T.pack (moveNewPath move),+ "old-module " <> moveOldModule move,+ "new-module " <> moveNewModule move,+ "source-digest " <> maybe "<missing>" id (moveContentDigest move),+ "transformed-digest " <> maybe "<missing>" id (moveTransformedDigest move)+ ]+ constraintPlan :: Spec -> ConsumerPlan -> [Text] constraintPlan spec plan = case resolveTypeGraph spec of Left _ -> []@@ -489,6 +1057,8 @@ recLayout = case placement ctx of GeneratedPrefix -> "prefixed"; CollocatedLeaf -> "collocated", recSourceLanguage = sourceLanguage, recLanguageContract = checkedLanguageContract service,+ recNamingEdition = currentGeneratedHaskellNamingEdition,+ recModuleRoles = [ScaffoldModuleRoleRow (moduleRole m) (kind m) (modulePath m) | m <- modules], recFiles = [(kind m, modulePath m) | m <- modules], recMappings = consumerMappings (consumerPlan spec), recIdDomains = idDomainIdentitiesForService service,@@ -526,6 +1096,16 @@ then pure (m, Skipped) else TIO.writeFile path (moduleText m) >> pure (m, Created) +-- | Qualify a refusal set raised after the run's sidecar renames were applied.+--+-- Every refusal message says "nothing was written", which is true of the module+-- tree but not of the renames, so the note is appended rather than the claim+-- being weakened everywhere. A refusal set that is empty stays empty.+withSidecarMovesApplied :: [SidecarMove] -> [Refusal] -> [Refusal]+withSidecarMovesApplied [] refusals = refusals+withSidecarMovesApplied _ [] = []+withSidecarMovesApplied moves refusals = refusals <> [SidecarMovesAlreadyApplied moves]+ renderRefusals :: [Refusal] -> [Text] renderRefusals = concatMap render where@@ -555,6 +1135,34 @@ render (BehaviorRefusal errors) = ["error: behavior obligations cannot be derived soundly -- refusing to scaffold; nothing was written"] <> [" " <> T.pack (show behaviorError) | behaviorError <- errors]+ render (GeneratedNameInvariantViolation violations) =+ ["error: generated Haskell name invariant violated -- refusing to scaffold; nothing was written"]+ <> map (" " <>) violations+ render (NameMigrationRequired moves) =+ [ "error: name migration required: legacy-v1 -> idiomatic-v1; nothing was written",+ "re-run scaffold with --apply-name-migrations after reviewing these source moves:"+ ]+ <> map renderMove moves+ render (NameMigrationRefusal reasons) =+ ["error: name migration could not be applied safely; nothing was written"]+ <> map (" " <>) reasons+ render (SidecarMigrationRequired moves) =+ [ "error: sidecar migration required; nothing was written",+ "re-run scaffold with --apply-name-migrations after reviewing these sidecar renames:"+ ]+ <> map ((" " <>) . renderSidecarMove) moves+ render (SidecarMigrationRefusal reasons) =+ ["error: sidecar migration could not be applied safely; nothing was written"]+ <> map (" " <>) reasons+ render (SidecarMovesAlreadyApplied moves) =+ [ "note: this run had already applied "+ <> tshow (length moves)+ <> " sidecar rename(s) before the refusal above, so \"nothing was written\" excludes them:"+ ]+ <> map ((" " <>) . renderSidecarMove) moves+ <> [ "The renames are idempotent and carry no spec content, so re-running scaffold",+ "after fixing the refusal is correct; nothing needs to be undone."+ ] render (FoldSurfaceRefusal surfaceError) = [ "error: aggregate fold identity could not be resolved -- refusing to scaffold; nothing was written", " " <> renderFoldSurfaceError surfaceError@@ -575,17 +1183,26 @@ ["error: duplicate normalized service conformance fact keys -- refusing to scaffold; nothing was written"] <> [" " <> duplicateServiceFactKey duplicate | duplicate <- duplicates] render (ConformancePackageRefusal failure) = renderConformancePackageFailure failure+ renderMove move =+ " "+ <> (case moveKind move of Generated -> "generated "; HoleStub -> "hole ")+ <> moveOldModule move+ <> " -> "+ <> moveNewModule move+ <> " backup: "+ <> T.pack (moveBackupPath move) renderScaffoldReport :: ScaffoldReport -> [Text] renderScaffoldReport report = [ "scaffold: " <> T.pack (reportSpecPath report) <> " -> " <> T.pack (reportOutDir report) <> " (module-root=" <> rootLabel <> ", layout=" <> layoutLabel <> ")" ] <> map moduleLine dispositions+ <> inertNodeSection <> [ "firewall: OK (" <> tshow generatedCount <> " generated modules scanned, 0 forbidden operators)", harnessLine, dependencyLine,- "manifest: " <> T.pack (reportManifestPath report),- "record: " <> T.pack (reportRecordPath report)+ "fragment: " <> T.pack (reportManifestPath report),+ "ledger: " <> T.pack (reportRecordPath report) ] <> previousSpecNote <> constraintSection@@ -594,6 +1211,8 @@ <> sourceLanguageDriftSection <> behaviorDriftSection <> obsoleteOutputSection+ <> sidecarMoveSection+ <> nameMoveSection <> staleSection <> maybe [] renderConformancePackageReport (reportConformancePackage report) where@@ -613,6 +1232,7 @@ dispositionTag Unchanged = "(unchanged)" pad name = name <> T.replicate (nameWidth - T.length name) " " generatedCount = length [() | (m, _) <- dispositions, kind m == Generated]+ inertNodeSection = renderInertNodeSection (reportInertNodes report) harnesses = sortOn id@@ -688,6 +1308,16 @@ hooks -> ["obsolete identity-copy output hooks (if still present, they are unused and may be removed):"] <> [" " <> aggregate <> ".Holes." <> hook | (aggregate, hook) <- hooks]+ sidecarMoveSection = case reportSidecarMoves report of+ [] -> []+ moves ->+ ["sidecar migration: applied (" <> tshow (length moves) <> " move(s))"]+ <> map ((" " <>) . renderSidecarMove) moves+ nameMoveSection = case reportNameMoves report of+ [] -> []+ moves ->+ ["name migration: applied (" <> tshow (length moves) <> " source move(s))"]+ <> [" backup: " <> T.pack (moveBackupPath move) | move <- moves] staleSection = case reportStale report of [] -> [] stale ->
src/Keiro/Dsl/SemanticContract.hs view
@@ -11,6 +11,7 @@ effectiveRuntimeProfile, effectiveRuntimeSemantics, effectiveLanguageSupport,+ languageContractNotice, effectiveLanguageContract, effectiveLanguageContractForVersion, runtimeSemanticsFingerprintSegments,@@ -24,19 +25,22 @@ import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) import Data.Maybe (fromMaybe) import Data.Text (Text)+import Data.Text qualified as T import Keiro.Dsl.Grammar (Spec) import Keiro.Dsl.LanguageVersion- ( LanguageSupport,+ ( LanguageSupport (..), LanguageVersion, ParsedSource (..), RuntimeSemanticsProfile, SourceLanguage (..),+ currentStableLanguageVersion, definitionRuntimeSemanticsProfile, effectiveLanguageVersion, languageSupportForVersion, languageSupportText, languageVersion, languageVersionNumber,+ languageVersionText, lookupLanguageDefinition, runtimeProfileFoldSegments, runtimeProfileIdentifier,@@ -65,6 +69,29 @@ fromMaybe (error "keiro-dsl internal invariant: effective contract selected an unregistered language version") (languageSupportForVersion (effectiveContractLanguageVersion contract))++-- | One stderr line naming a non-stable effective contract. Stable sources+-- stay silent so adopting the current contract does not add CLI noise.+languageContractNotice :: FilePath -> Text -> EffectiveLanguageContract -> Maybe Text+languageContractNotice subject sourceFormSummary contract+ | effectiveLanguageSupport contract == Stable = Nothing+ | otherwise =+ Just+ ( T.pack subject+ <> ": language contract: effective keiro-dsl "+ <> languageVersionText (effectiveContractLanguageVersion contract)+ <> " ("+ <> sourceFormSummary+ <> ", "+ <> languageSupportText (effectiveLanguageSupport contract)+ <> ", runtime semantics "+ <> effectiveRuntimeSemantics contract+ <> "); language-"+ <> languageVersionText currentStableLanguageVersion+ <> " strict spec-surface validation is not applied — declare `language keiro-dsl "+ <> languageVersionText currentStableLanguageVersion+ <> "` to adopt the stable contract"+ ) instance ToJSON EffectiveLanguageContract where toJSON contract =
src/Keiro/Dsl/ServiceHarness.hs view
@@ -9,7 +9,8 @@ ) where -import Data.List (group, sort, sortOn)+import Data.List (group, mapAccumL, sort, sortOn)+import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T import Keiro.Dsl.Grammar@@ -76,19 +77,30 @@ <> [""] <> renderFacts factSources where- indexed = zip [0 :: Int ..] (serviceHarnessNodes service)+ aliased = aliasNodes (serviceHarnessNodes service) importLines- | null indexed = []- | otherwise = "" : map (renderImport ctx) indexed- checkSources = [(node, aliasFor index) | (index, node) <- indexed, producesChecks node]- factSources = [(node, aliasFor index) | (index, node) <- indexed, producesFacts node]+ | null aliased = []+ | otherwise = "" : map (renderImport ctx) aliased+ checkSources = [(node, alias) | (node, alias) <- aliased, producesChecks node]+ factSources = [(node, alias) | (node, alias) <- aliased, producesFacts node] -renderImport :: Context -> (Int, Node) -> Text-renderImport ctx (index, node) =- "import " <> harnessModuleName ctx node <> " qualified as " <> aliasFor index+renderImport :: Context -> (Node, Text) -> Text+renderImport ctx (node, alias) =+ "import " <> harnessModuleName ctx node <> " qualified as " <> alias -aliasFor :: Int -> Text-aliasFor index = "Harness" <> T.pack (show index)+aliasNodes :: [Node] -> [(Node, Text)]+aliasNodes nodes = snd (mapAccumL assign Map.empty nodes)+ where+ assign counts node =+ let base = aliasForNode node+ occurrence = Map.findWithDefault 0 base counts + 1+ alias = base <> if occurrence == (1 :: Int) then "" else T.pack (show occurrence)+ in (Map.insert base occurrence counts, (node, alias))++aliasForNode :: Node -> Text+aliasForNode node =+ let (_, nodeName, _) = nodeIdentity node+ in pascal nodeName harnessModuleName :: Context -> Node -> Text harnessModuleName ctx = \case
+ src/Keiro/Dsl/SidecarMigration.hs view
@@ -0,0 +1,270 @@+-- | Refuse-then-apply migration for renamed scaffold sidecars.+--+-- Planning inspects exact old and new paths before any ledger read. Applying a+-- rename preserves the bytes directly; applying a retirement moves the legacy+-- duplicate into the recoverable sidecar backup slot. Legacy conformance+-- records are converted to the forward-compatible ledger format before their+-- original bytes are retired.+module Keiro.Dsl.SidecarMigration+ ( SidecarScope (..),+ SidecarMoveDisposition (..),+ SidecarMove (..),+ PreparedSidecarMove,+ preparedSidecarMove,+ planSidecarMigrations,+ applyPreparedSidecarMoves,+ renderSidecarMove,+ )+where++import Control.Exception (IOException, bracketOnError, try)+import Control.Monad (filterM, when)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.IO qualified as TIO+import Keiro.Dsl.ConformancePackage+ ( ConformancePackagePlan (cppDirectory),+ parseLegacyConformancePackageRecord,+ renderConformancePackageRecord,+ )+import Keiro.Dsl.Scaffold (isGeneratedBannerLine)+import Keiro.Dsl.SidecarNames+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeFile, renameFile)+import System.FilePath (takeDirectory, takeFileName, (</>))+import System.IO (Handle, hClose, openTempFile)++data SidecarScope+ = ContextSidecars !Text+ | WorkspaceSidecars !Text+ deriving stock (Eq, Show)++data SidecarMoveDisposition+ = RenameSidecar+ | RetireLegacySidecar+ | ConvertLegacyConformanceLedger+ deriving stock (Eq, Show)++data SidecarMove = SidecarMove+ { sidecarOldPath :: !FilePath,+ sidecarNewPath :: !FilePath,+ sidecarBackupPath :: !(Maybe FilePath),+ sidecarMoveDisposition :: !SidecarMoveDisposition+ }+ deriving stock (Eq, Show)++data PreparedSidecarMove = PreparedSidecarMove+ { preparedSidecarMove :: !SidecarMove,+ preparedConvertedContents :: !(Maybe Text)+ }+ deriving stock (Eq, Show)++-- | Inspect the two scope-specific sidecars and the optional generated+-- conformance package. An old-name file always produces a move: either a direct+-- rename when the new path is absent, or retirement when the new path is already+-- authoritative.+planSidecarMigrations :: FilePath -> SidecarScope -> Maybe ConformancePackagePlan -> IO (Either [Text] [PreparedSidecarMove])+planSidecarMigrations out scope conformancePlan = do+ ordinaryResults <- traverse (planOrdinary out) (scopePairs scope)+ -- Legacy conformance records are migrated wherever they are found in the out+ -- tree, not only when this run happens to plan a conformance package. Keying+ -- the migration on the plan left a record orphaned — and unreadable, since the+ -- legacy parser is no longer reachable from the current reader — as soon as a+ -- spec stopped generating a conformance package. See ExecPlan 199.+ legacyDirectories <- legacyConformanceDirectories out plannedDirectory+ conformanceResults <-+ traverse+ ( \directory ->+ planConformance+ out+ (directory </> legacyConformanceRecordFileName)+ (directory </> conformanceLedgerFileName)+ )+ (plannedDirectories <> legacyDirectories)+ let results = ordinaryResults <> conformanceResults+ errors = [message | Left message <- results]+ moves = [move | Right (Just move) <- results]+ pure $ if null errors then Right moves else Left errors+ where+ plannedDirectory = fmap cppDirectory conformancePlan+ plannedDirectories = maybe [] (: []) plannedDirectory++-- | Directories under @out@ holding a legacy conformance record, excluding the+-- one this run already plans. Bounded to the depth generated conformance+-- packages actually use, so it never walks a consumer's whole source tree.+legacyConformanceDirectories :: FilePath -> Maybe FilePath -> IO [FilePath]+legacyConformanceDirectories out planned = do+ candidates <- descend legacyConformanceSearchDepth ""+ filterM+ (\directory -> doesFileExist (out </> directory </> legacyConformanceRecordFileName))+ [directory | directory <- candidates, Just directory /= planned]+ where+ descend :: Int -> FilePath -> IO [FilePath]+ descend depth relative+ | depth < 0 = pure []+ | otherwise = do+ entries <- listDirectorySafe (out </> relative)+ children <-+ filterM+ (\name -> doesDirectoryExist (out </> relative </> name))+ -- Never descend into the migration backup root. A retired legacy+ -- record lives there permanently by design, so scanning it would+ -- make every later run want to "migrate" the backup, forever.+ [name | name <- entries, name /= sidecarBackupRootName]+ nested <-+ concat+ <$> traverse+ (\name -> descend (depth - 1) (if null relative then name else relative </> name))+ children+ pure ((if null relative then [] else [relative]) <> nested)++-- | Generated conformance packages sit at most this many directories below the+-- scaffold output root (@<out>/<package-dir>/@ plus room for a nested layout).+legacyConformanceSearchDepth :: Int+legacyConformanceSearchDepth = 3++listDirectorySafe :: FilePath -> IO [FilePath]+listDirectorySafe path = do+ exists <- doesDirectoryExist path+ if exists then listDirectory path else pure []++scopePairs :: SidecarScope -> [(FilePath, FilePath)]+scopePairs = \case+ ContextSidecars context ->+ [ (legacyContextRecordFileName context, contextLedgerFileName context),+ (legacyContextManifestFileName context, contextCabalFragmentFileName context)+ ]+ WorkspaceSidecars service ->+ [ (legacyWorkspaceRecordFileName service, workspaceLedgerFileName service),+ (legacyWorkspaceManifestFileName service, workspaceCabalFragmentFileName service)+ ]++planOrdinary :: FilePath -> (FilePath, FilePath) -> IO (Either Text (Maybe PreparedSidecarMove))+planOrdinary out (oldRelative, newRelative) = do+ oldExists <- doesFileExist (out </> oldRelative)+ newExists <- doesFileExist (out </> newRelative)+ if not oldExists+ then pure (Right Nothing)+ else+ if newExists+ then prepareRetirement out oldRelative newRelative+ else+ pure . Right . Just $+ PreparedSidecarMove+ { preparedSidecarMove =+ SidecarMove+ { sidecarOldPath = oldRelative,+ sidecarNewPath = newRelative,+ sidecarBackupPath = Nothing,+ sidecarMoveDisposition = RenameSidecar+ },+ preparedConvertedContents = Nothing+ }++planConformance :: FilePath -> FilePath -> FilePath -> IO (Either Text (Maybe PreparedSidecarMove))+planConformance out oldRelative newRelative = do+ oldExists <- doesFileExist (out </> oldRelative)+ newExists <- doesFileExist (out </> newRelative)+ if not oldExists+ then pure (Right Nothing)+ else+ if newExists+ then prepareRetirement out oldRelative newRelative+ else do+ legacyContents <- TIO.readFile (out </> oldRelative)+ case parseLegacyConformancePackageRecord legacyContents of+ Nothing -> pure (Left (T.pack oldRelative <> ": legacy conformance record is invalid and cannot be converted"))+ Just record -> do+ let backupRelative = sidecarBackupRelative oldRelative+ backupExists <- doesFileExist (out </> backupRelative)+ if backupExists+ then pure (Left (T.pack oldRelative <> ": sidecar migration backup already exists at " <> T.pack backupRelative))+ else+ pure . Right . Just $+ PreparedSidecarMove+ { preparedSidecarMove =+ SidecarMove+ { sidecarOldPath = oldRelative,+ sidecarNewPath = newRelative,+ sidecarBackupPath = Just backupRelative,+ sidecarMoveDisposition = ConvertLegacyConformanceLedger+ },+ preparedConvertedContents = Just (preserveBanner legacyContents <> renderConformancePackageRecord record)+ }++prepareRetirement :: FilePath -> FilePath -> FilePath -> IO (Either Text (Maybe PreparedSidecarMove))+prepareRetirement out oldRelative newRelative = do+ let backupRelative = sidecarBackupRelative oldRelative+ backupExists <- doesFileExist (out </> backupRelative)+ if backupExists+ then pure (Left (T.pack oldRelative <> ": sidecar migration backup already exists at " <> T.pack backupRelative))+ else+ pure . Right . Just $+ PreparedSidecarMove+ { preparedSidecarMove =+ SidecarMove+ { sidecarOldPath = oldRelative,+ sidecarNewPath = newRelative,+ sidecarBackupPath = Just backupRelative,+ sidecarMoveDisposition = RetireLegacySidecar+ },+ preparedConvertedContents = Nothing+ }++sidecarBackupRelative :: FilePath -> FilePath+sidecarBackupRelative oldRelative = sidecarBackupRootName </> "sidecar-v1" </> oldRelative++-- | The directory holding recoverable originals of retired legacy sidecars.+sidecarBackupRootName :: FilePath+sidecarBackupRootName = ".keiro-dsl-name-migrations"++preserveBanner :: Text -> Text+preserveBanner contents = T.unlines [line | line <- T.lines contents, isGeneratedBannerLine line]++applyPreparedSidecarMoves :: FilePath -> [PreparedSidecarMove] -> IO ()+applyPreparedSidecarMoves out = mapM_ applyOne+ where+ applyOne prepared = case sidecarMoveDisposition move of+ RenameSidecar -> do+ createDirectoryIfMissing True (takeDirectory newPath)+ renameFile oldPath newPath+ RetireLegacySidecar -> retire move oldPath+ ConvertLegacyConformanceLedger -> case preparedConvertedContents prepared of+ Nothing -> error "prepared conformance sidecar conversion lacks converted contents"+ Just converted -> do+ writeTextAtomic newPath converted+ retire move oldPath+ where+ move = preparedSidecarMove prepared+ oldPath = out </> sidecarOldPath move+ newPath = out </> sidecarNewPath move++ retire move oldPath = case sidecarBackupPath move of+ Nothing -> error "prepared sidecar retirement lacks a backup path"+ Just backupRelative -> do+ let backupPath = out </> backupRelative+ createDirectoryIfMissing True (takeDirectory backupPath)+ renameFile oldPath backupPath++writeTextAtomic :: FilePath -> Text -> IO ()+writeTextAtomic path contents = do+ let directory = takeDirectory path+ createDirectoryIfMissing True directory+ bracketOnError+ (openTempFile directory (takeFileName path <> ".tmp"))+ cleanupTemporary+ (\(temporary, handle) -> TIO.hPutStr handle contents >> hClose handle >> renameFile temporary path)++cleanupTemporary :: (FilePath, Handle) -> IO ()+cleanupTemporary (temporary, handle) = do+ _ <- try (hClose handle) :: IO (Either IOException ())+ exists <- doesFileExist temporary+ when exists (removeFile temporary)++renderSidecarMove :: SidecarMove -> Text+renderSidecarMove move = case sidecarMoveDisposition move of+ RenameSidecar -> path (sidecarOldPath move) <> " -> " <> path (sidecarNewPath move)+ RetireLegacySidecar -> path (sidecarOldPath move) <> " -> retired to " <> backup+ ConvertLegacyConformanceLedger -> path (sidecarOldPath move) <> " -> " <> path (sidecarNewPath move) <> "; original retired to " <> backup+ where+ path = T.pack+ backup = maybe "<missing-backup>" path (sidecarBackupPath move)
+ src/Keiro/Dsl/SidecarNames.hs view
@@ -0,0 +1,56 @@+-- | One naming authority for every sidecar emitted beside generated Haskell.+--+-- A ledger is machine-owned and read by later scaffold runs. A Cabal fragment+-- is human-facing text to paste into a component stanza. The explicit+-- @context@ and @workspace@ slots keep those namespaces disjoint by+-- construction, including for a context literally named @workspace@.+module Keiro.Dsl.SidecarNames+ ( contextLedgerFileName,+ workspaceLedgerFileName,+ conformanceLedgerFileName,+ contextCabalFragmentFileName,+ workspaceCabalFragmentFileName,+ workspaceMigrationReportFileName,+ legacyContextRecordFileName,+ legacyWorkspaceRecordFileName,+ legacyConformanceRecordFileName,+ legacyContextManifestFileName,+ legacyWorkspaceManifestFileName,+ )+where++import Data.Text (Text)+import Data.Text qualified as T++contextLedgerFileName :: Text -> FilePath+contextLedgerFileName context = "keiro-dsl-ledger.context." <> T.unpack context <> ".txt"++workspaceLedgerFileName :: Text -> FilePath+workspaceLedgerFileName service = "keiro-dsl-ledger.workspace." <> T.unpack service <> ".txt"++conformanceLedgerFileName :: FilePath+conformanceLedgerFileName = "keiro-dsl-conformance-ledger.txt"++contextCabalFragmentFileName :: Text -> FilePath+contextCabalFragmentFileName context = "keiro-dsl-cabal-fragment.context." <> T.unpack context <> ".txt"++workspaceCabalFragmentFileName :: Text -> FilePath+workspaceCabalFragmentFileName service = "keiro-dsl-cabal-fragment.workspace." <> T.unpack service <> ".txt"++workspaceMigrationReportFileName :: Text -> FilePath+workspaceMigrationReportFileName service = "keiro-dsl-migration-report.workspace." <> T.unpack service <> ".txt"++legacyContextRecordFileName :: Text -> FilePath+legacyContextRecordFileName context = "keiro-dsl-scaffold-record." <> T.unpack context <> ".txt"++legacyWorkspaceRecordFileName :: Text -> FilePath+legacyWorkspaceRecordFileName service = "keiro-dsl-scaffold-record.workspace." <> T.unpack service <> ".txt"++legacyConformanceRecordFileName :: FilePath+legacyConformanceRecordFileName = "keiro-dsl-conformance-record.txt"++legacyContextManifestFileName :: Text -> FilePath+legacyContextManifestFileName context = "keiro-dsl-manifest." <> T.unpack context <> ".txt"++legacyWorkspaceManifestFileName :: Text -> FilePath+legacyWorkspaceManifestFileName service = "keiro-dsl-manifest.workspace." <> T.unpack service <> ".txt"
src/Keiro/Dsl/Skeleton.hs view
@@ -197,7 +197,10 @@ " topic events", " accept ThingHappened", "",- " bind messageId from header \"keiro-message-id\" required cross-check body",+ -- No `required`/`cross-check body` flags here: they are unenforced+ -- descriptive notation (IntakeBindFlagUnenforced), and the skeleton must+ -- pass the documented `--deny-warnings` CI gate as generated.+ " bind messageId from header \"keiro-message-id\"", "", " dedupe key messageId policy PreferIntegrationMessageId", "",@@ -259,7 +262,7 @@ T.unlines [ "context my-service", "",- "readmodel accepted_transfer_needs {",+ "readmodel acceptedTransferNeeds {", " table = \"accepted_transfer_needs\"", " schema = \"my_service\"", " columns {",@@ -272,7 +275,7 @@ " feed = subscription", "}", "",- "readmodel transfer_decisions {",+ "readmodel transferDecisions {", " table = \"transfer_decisions\"", " schema = \"my_service\"", " columns {",@@ -284,7 +287,7 @@ " feed = subscription", "}", "",- "workqueue reservation_work {",+ "workqueue reservationWork {", " queue logical = \"my_service.reservation_work\"", " derive physical = \"my_service_reservation_work\"", " dlq = \"my_service_reservation_work_dlq\"",@@ -305,13 +308,13 @@ " }", "}", "",- "dispatch reservation_work_dispatch {",- " source readModel = accepted_transfer_needs key = reservationId",+ "dispatch reservationWorkDispatch {",+ " source readModel = acceptedTransferNeeds key = reservationId", " fanout body = resolveTransferCandidates", " dedup key = reservationId",- " seenIn readModel = transfer_decisions field = reservation_id",- " seenIn queue = reservation_work field = reservation_id",- " enqueue to = reservation_work",+ " seenIn readModel = transferDecisions field = reservation_id",+ " seenIn queue = reservationWork field = reservation_id",+ " enqueue to = reservationWork", "}" ]
src/Keiro/Dsl/Validate.hs view
@@ -10,2903 +10,3895 @@ module Keiro.Dsl.Validate ( Severity (..), DiagnosticCode (..),- Diagnostic (..),- renderDiagnostic,- validateService,- validateSpec,- derivedQueueTrio,- sagaCategoryError,- nodeIdentity,- )-where--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)-import Data.Set qualified as Set-import Data.Text (Text)-import Data.Text qualified as T-import Data.TypeID qualified as TypeID-import Data.Word (Word64)-import Keiro.Dsl.AggregateType-import Keiro.Dsl.EventOutput-import Keiro.Dsl.Expression-import Keiro.Dsl.Grammar-import Keiro.Dsl.IdDomain (contractIdDomainContractFor, idDomainContractFor)-import Keiro.Dsl.LanguageVersion (RuntimeCapability (..), runtimeProfileHasCapability)-import Keiro.Dsl.NominalType qualified as Nominal-import Keiro.Dsl.ReadModelShape (deriveShapeHash)-import Keiro.Dsl.RuntimePackage (isCabalPackageName)-import Keiro.Dsl.SemanticContract (CheckedService (..), EffectiveLanguageContract, effectiveRuntimeProfile, legacyCheckedService)-import Keiro.Dsl.TypeGraph-import Numeric (showHex)-import Text.Read (readMaybe)--data Severity = Error | Warning- deriving stock (Eq, Show)---- | A machine-checkable code per rule, so tests match on the code, not prose.-data DiagnosticCode- = UndeclaredCommand- | UndeclaredEvent- | UndeclaredState- | UnreachableState- | TerminalHasOutgoing- | GuardAtomOutOfScope- | StatusMapNotTotal- | ClockSampled- | -- 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- | EvtRemovedNotDeprecated- | -- EP-3 (process manager + durable timer).- ProcessFireAtNotInjected- | ProcessDispatchIdSupplied- | ProcessUnresolvedRef- | ProcessBenignInversion- | SagaCategoryIllegal- | -- EP-4 (integration intake / inbox disposition).- DispositionIncomplete- | DispositionDuplicateRetry- | DispositionPreviouslyFailedRetry- | DispositionDecodeUnboundedRetry- | -- EP-4 (integration coupling).- EmitSkipMissing- | EmitUnresolvedContract- | PublisherUnresolvedEmit- | IntakeUnresolvedContract- | -- EP-5 (pgmq workqueue/dispatch).- WqPhysicalDivergence- | WqStoreFailureNotRetry- | WqDecodeFailureNotDeadLetter- | WqDlqWithoutCeiling- | WqGroupKeyMissing- | WqGroupKeyWithoutFifo- | WqGroupKeyUnresolved- | WqUnloggedDurability- | WqPartitionSpecEmpty- | SnapshotIntervalInvalid- | SnapshotCodecFixtureInvalid- | DispatchEnqueueUnresolved- | -- EP-6 (workflow/operation).- AwaitSignalMismatch- | RunWorkflowUnresolved- | WorkflowPatchDuplicate- | WorkflowPatchIdInvalid- | WorkflowContinueAsNewNotTerminal- | -- Diff-only (cross-spec) decode and identity evolution rules.- EvtFieldTypeChanged- | EvtFieldRemovedSameVersion- | EvtVersionDecreased- | EnumCtorRemoved- | EnumWireSpellingChanged- | WireSpecChanged- | ContractEventRemoved- | ContractFieldChanged- | ContractDiscriminatorChanged- | ContractTopicChanged- | ContractSchemaVersionDecreased- | WqPayloadFieldChanged- | ProcessInputChanged- | WorkflowShapeChanged- | WorkflowBodyChanged- | WorkflowStableNameChanged- | WorkflowPatchRemoved- | WorkflowContinueSeedChanged- | WqOrderingChanged- | WqProvisionChanged- | WqGroupKeyChanged- | IdPrefixChanged- | DedupeIdentityChanged- | DerivedIdentityChanged- | QueueIdentityChanged- | TimerWindowChanged- | EmitMappingChanged- | DecodePostureChanged- | IntakePersistenceChanged- | ProjectionChanged- | PublisherPolicyChanged- | DispatchRetargeted- | ContractSchemaVersionBumped- | EventUndeprecated- | -- EP-104 (validator soundness).- WorkflowDuplicateLabel- | WorkflowSleepDelayUnresolved- | WorkflowIdFieldUnresolved- | RuleDomainUnresolved- | RuleNotTotal- | RuleCaseUnknownCtor- | ProcessFieldBindingUnresolved- | ProcessTimerCeilingInvalid- | OperationUnresolvedRef- | AwaitSignalValueMismatch- | WqDispositionIncomplete- | DispositionDuplicateOutcome- | TopicAffinityMismatch- | StatusMapDanglingKey- | StatusMapDuplicateKey- | WriteTargetNotRegister- | RegisterInitialOutOfScope- | DuplicateNodeName- | DuplicateEnumCtor- | DuplicateEnumWire- | DuplicateIdPrefix- | DuplicateCommandName- | DuplicateEventName- | WqDlqDivergence- | WqTableDivergence- | DispatchDedupQueueUnresolved- | DispatchDedupFieldUnresolved- | -- EP-105 (notation integrity and scaffold-safe names).- IdentHaskellKeyword- | IdentNotConstructorSafe- | VertexCtorCollision- | -- EP-107 (first-class read models).- RmShapeHashDrift- | RmStrongInlineOnly- | RmScopeWithoutStrong- | RmUnknownColumnType- | RmInlineFeedUnreferenced- | RmConsistencyConflict- | RmProjectionWithoutNode- | QueryUnresolvedReadModel- | QueryConsistencyInvalid- | DispatchReadModelUnresolved- | DispatchReadModelFieldUnknown- | -- EP-107 diff-only read-model evolution rules.- ReadModelVersionDecreased- | ReadModelShapeChangedWithoutBump- | ReadModelFeedChanged- | ReadModelConsistencyWeakened- | -- EP-108 (router and worker-policy surfaces).- RouterUnresolvedRef- | RouterKeyFieldUnknown- | RouterBindingUnscoped- | RouterCommandUnknown- | RouterReadModelUnverified- | PolicyContradiction- | PolicyDeadLetterUnused- | 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- | -- MasterPlan 26 / EP-153: whole-service composition refusals, emitted by- -- "Keiro.Dsl.Workspace" when several @.keiro@ members are composed into- -- one service graph. They live in this registry, not a parallel enum, so- -- every gate stays correlatable by code (ADR 0004). Manifest syntax and- -- structure errors deliberately have no code here: like a @.keiro@ parse- -- error, they are refused before any graph exists to diagnose.- WorkspaceMemberUnreadable- | WorkspaceMemberParseFailed- | WorkspaceContextMismatch- | WorkspaceAuthorityConflict- | WorkspaceDuplicateDeclaration- | WorkspaceDuplicateNodeName- | WorkspacePathCollision- | -- MasterPlan 26 / EP-155: whole-workspace diff facts. These are- -- advisory consumer-build obligations, distinct from wire evolution.- OwnershipMoved- | WorkspaceAuthorityChanged- | -- EP-157: canonical aggregate type resolution and capabilities.- AggregateTypeUnknown- | AggregateTypeUnsupportedAtUse- | AggregateRegisterInitialInvalid- | AggregateGuardTypeMismatch- | AggregateGuardCapabilityUnsupported- | AggregateExpressionRootUnknown- | AggregateExpressionRootAmbiguous- | AggregateExpressionPathInvalid- | AggregateExpressionPathUnsupported- | AggregateExpressionLiteralNeedsType- | AggregateExpressionLiteralInvalid- | AggregateExpressionOperandTypeMismatch- | AggregateExpressionOperatorUnsupported- | AggregateExpressionBooleanRequired- | AggregateExpressionGuardBoolRequired- | AggregateExpressionWriteTargetUnknown- | AggregateExpressionWriteTypeMismatch- | AggregateTransitionOwnershipConflict- | CollectionExpressionUnsupported- | -- EP-160: append-only source-language composition and diff facts.- WorkspaceLanguageVersionMismatch- | SourceLanguageDeclarationChanged- | -- EP-158: checked consumer-owned nominal IDs, enums, and scalars.- NominalMissingIngredient- | NominalInvalidHaskellSource- | NominalInvalidQualifiedName- | NominalInvalidIdentity- | NominalInvalidIdPrefix- | NominalUnsupportedRepresentation- | NominalEmptyEnumRepresentation- | NominalMissingInitialValue- | NominalNameCollision- | NominalBindingChanged- | NominalFixturesChanged- | NominalCanonicalTypeChanged- | NominalInitialChanged- | NominalRepresentationChanged- | NominalIdDecoderTightened- | -- ExecPlan 171 / IR-14: versioned prefix-bearing ID admission policy.- IdDomainContractChanged- | -- ExecPlan 159 / IR-13: @fields(Command)@ output authority.- EventOutputCommandMismatch- | AggregateEventlessStateChange- | -- ExecPlan 178: language-4 integration contract TypeID admission.- ContractInvalidTypeIdPrefix- | ContractTypeIdDomainChanged- | -- ExecPlan 180: accepted-but-unenforced spec surfaces.- PublisherOrderingUnknown- | PublisherBackoffInvalid- | IntakeDedupePolicyUnknown- | PublisherMaxAttemptsBelowMinimum- | ContractSchemaVersionBelowMinimum- | ReadModelVersionBelowMinimum- | IntakeDecodeSchemaVersionBelowMinimum- | AggregateDuplicateFieldName- | ContractDuplicateFieldName- | ContractFieldShadowsDiscriminator- | TransitionDuplicateUnguarded- | ContractDuplicateEvent- | ContractDuplicateTopicAlias- | AggregateDuplicateState- | AggregateDuplicateRegister- | NominalDuplicateDeclaration- | EmitMapDuplicateCase- | TransitionUnguardedSibling- | RuntimeIdentityInvalid- | RuntimeIdentityDuplicate- | ContractTopicNameInvalid- | ReadModelIdentifierInvalid- | ReadModelDuplicateColumn- | IntakeBindUnresolved- | IntakeDedupeKeyUnresolved- | IntakeEnvelopePolicyUnknown- | IntakeDecodeSchemaVersionMismatch- | ContractTopicAliasUnresolved- | WireClauseUnsupported- deriving stock (Eq, Show)---- | A line-numbered, structured diagnostic.-data Diagnostic = Diagnostic- { line :: !Int,- severity :: !Severity,- code :: !DiagnosticCode,- message :: !Text- }- deriving stock (Eq, Show)---- | Render a diagnostic in the conventional--- @\<file\>:\<line\>: error[\<code\>]: \<message\>@ form.-renderDiagnostic :: FilePath -> Diagnostic -> Text-renderDiagnostic file d =- T.pack file- <> ":"- <> T.pack (show (line d))- <> ": "- <> sev- <> "["- <> T.pack (show (code d))- <> "]: "- <> message d- where- sev = case severity d of Error -> "error"; Warning -> "warning"---- | Reserved wall-clock atom names. Sampling any of these inside a guard or--- write breaks deterministic replay: TIME IS INJECTED, NOT SAMPLED.-clockAtoms :: Set Name-clockAtoms = Set.fromList ["now", "currentTime", "wallClock", "today", "utcNow"]---- | Validate a whole service under its effective released semantic contract.--- An empty list means valid. Current released versions share this policy, but--- selecting it at this boundary prevents successor semantics from being lost.-validateService :: CheckedService -> [Diagnostic]-validateService service = validateCheckedSpec (checkedLanguageContract service) (checkedSpec service)---- | Compatibility wrapper for callers that have only a normalized graph. It--- explicitly selects legacy/version-1 semantics; production source/workspace--- routes use 'validateService'.-validateSpec :: Spec -> [Diagnostic]-validateSpec = validateService . legacyCheckedService--validateCheckedSpec :: EffectiveLanguageContract -> Spec -> [Diagnostic]-validateCheckedSpec languageContract spec =- sortOn line (validateNames spec ++ validateMapped spec ++ validateNominal languageContract spec ++ validateAggregateTypes spec ++ specLevelRules languageContract spec ++ concatMap (validateNode languageContract spec) (specNodes spec))---- | Rules added before language 4 ships consult the effective semantic--- contract, not the numeric source spelling. Versions 1 through 3 retain their--- released acceptance; runtime semantics 3 is the unreleased tightening gate.-enforcesSpecSurfaceClosures :: EffectiveLanguageContract -> Bool-enforcesSpecSurfaceClosures languageContract =- runtimeProfileHasCapability (effectiveRuntimeProfile languageContract) StrictSpecSurfaceValidation--validateNominal :: EffectiveLanguageContract -> Spec -> [Diagnostic]-validateNominal languageContract spec = domainErrors <> resolutionErrors- where- domainErrors =- [ mkErr (locLine (idLoc declaration)) NominalInvalidIdPrefix $- "id '" <> idName declaration <> "' has invalid TypeID prefix '" <> idPrefix declaration <> "': " <> T.pack (show reason)- | declaration <- specIds spec,- Just _ <- [idDomainContractFor languageContract (idPrefix declaration)],- Just reason <- [TypeID.checkPrefix (idPrefix declaration)]- ]- resolutionErrors = case Nominal.resolveNominalTypes spec of- Right _ -> []- Left errors -> map nominalTypeDiagnostic (NE.toList errors)--nominalTypeDiagnostic :: Nominal.NominalTypeError -> Diagnostic-nominalTypeDiagnostic nominalError = case nominalError of- Nominal.NominalMissingIngredient name loc ingredient ->- problem loc NominalMissingIngredient $ "nominal declaration '" <> name <> "' is missing required " <> ingredient <> " provenance"- Nominal.NominalInvalidHaskellSource name loc ingredient ->- problem loc NominalInvalidHaskellSource $ "nominal declaration '" <> name <> "' has an invalid Haskell " <> ingredient <> " name"- Nominal.NominalInvalidQualifiedValue name loc ingredient value ->- problem loc NominalInvalidQualifiedName $- "nominal declaration '" <> name <> "' has invalid " <> ingredient <> " symbol '" <> value <> "'; expected a module path plus a lower-initial value"- Nominal.NominalInvalidIdentity name loc ingredient value ->- problem loc NominalInvalidIdentity $ "nominal declaration '" <> name <> "' has invalid " <> ingredient <> " '" <> value <> "'"- Nominal.NominalInvalidIdPrefix name loc prefix detail ->- problem loc NominalInvalidIdPrefix $ "id '" <> name <> "' has invalid TypeID prefix '" <> prefix <> "': " <> detail- Nominal.NominalUnsupportedScalar name loc representation ->- problem loc NominalUnsupportedRepresentation $- "nominal scalar '" <> name <> "' uses unsupported representation '" <> representation <> "'; supported representations are Text, Int, Natural, Bool, and Time"- Nominal.NominalEmptyEnum name loc ->- problem loc NominalEmptyEnumRepresentation $ "enum '" <> name <> "' must declare at least one closed representation constructor"- Nominal.NominalMissingRegisterInitial name loc registerName ->- problem loc NominalMissingInitialValue $- "consumer-owned nominal type '" <> name <> "' is used by register '" <> registerName <> "' and must name an initial symbol"- Nominal.NominalDeclarationCollision name loc categories ->- problem loc NominalNameCollision $ "declaration name '" <> name <> "' collides across " <> T.intercalate ", " categories- where- problem loc diagnosticCode detail = mkErr (locLine loc) diagnosticCode (detail <> "; GHC and conformance validate consumer function bodies")---- | Resolve every direct aggregate type once at the earliest semantic gate.-validateAggregateTypes :: Spec -> [Diagnostic]-validateAggregateTypes spec = case Nominal.resolveNominalTypes spec of- Left _ -> []- Right _ -> concatMap aggregateRules aggregates- where- symbols = aggregateSymbols spec- aggregates = [aggregate | NAggregate aggregate <- specNodes spec]-- aggregateRules aggregate =- concatMap commandRules (aggCommands aggregate)- ++ concatMap eventRules (aggEvents aggregate)- ++ concatMap registerRules (aggRegs aggregate)- ++ concatMap (transitionRules aggregate) (aggTransitions aggregate)- where- commandRules command = concatMap (fieldRule aggregate CommandFieldUse) (cmdFields command)- eventRules event = case evBody event of- EventFields fields -> concatMap (fieldRule aggregate EventFieldUse) fields- EventFromCommand _ -> []- registerRules register = case resolveAggregateType symbols (regLoc register) RegisterUse (regType register) of- Left typeError -> [aggregateTypeDiagnostic typeError]- Right AggregateMapped {} -> []- Right resolved -> case resolveRegisterInitial symbols (regLoc register) resolved (regInitial register) of- Left initialError -> [aggregateTypeDiagnostic initialError]- Right _ -> []-- fieldRule aggregate useSite field =- either (pure . aggregateTypeDiagnostic) (const []) (inferAggregateFieldType symbols aggregate useSite field)-- transitionRules aggregate transition = case tImplementation transition of- LegacyHoleImplementation ->- concatMap (comparisonRule aggregate transition) (maybe [] comparisons (tGuard transition))- GeneratedImplementation ->- let environment = expressionEnvironment spec aggregate transition- in maybe [] (expressionDiagnostics . resolveGuardExpr environment) (tGuard transition)- ++ concatMap (expressionDiagnostics . uncurry (resolveWriteExpr environment)) (tWrites transition)- HoleImplementation ->- [ mkErr (locLine (tLoc transition)) AggregateTransitionOwnershipConflict $- "transition '"- <> tSource transition- <> " -- "- <> tCommand transition- <> "' selects implementation hole and therefore cannot also declare guard or write clauses"- | tGuard transition /= Nothing || not (null (tWrites transition))- ]-- expressionDiagnostics = either (map expressionDiagnostic . NE.toList) (const [])-- expressionDiagnostic diagnostic =- mkErr- (locLine (expressionDiagnosticLoc diagnostic))- (expressionCode (expressionDiagnosticCode diagnostic))- (expressionDiagnosticMessage diagnostic)-- expressionCode = \case- ScalarRootUnknown -> AggregateExpressionRootUnknown- ScalarRootAmbiguous -> AggregateExpressionRootAmbiguous- ScalarPathInvalid -> AggregateExpressionPathInvalid- ScalarPathUnsupported -> AggregateExpressionPathUnsupported- ScalarLiteralNeedsType -> AggregateExpressionLiteralNeedsType- ScalarLiteralInvalid -> AggregateExpressionLiteralInvalid- ScalarOperandTypeMismatch -> AggregateExpressionOperandTypeMismatch- ScalarOperatorUnsupported -> AggregateExpressionOperatorUnsupported- ScalarBooleanOperandRequired -> AggregateExpressionBooleanRequired- ScalarGuardBoolRequired -> AggregateExpressionGuardBoolRequired- ScalarWriteTargetUnknown -> AggregateExpressionWriteTargetUnknown- ScalarWriteTypeMismatch -> AggregateExpressionWriteTypeMismatch-- comparisonRule aggregate transition (operator, left, right) =- case (expressionType aggregate transition left, expressionType aggregate transition right) of- (Right leftType, Right rightType)- | leftType /= rightType ->- [ mkErr (locLine (tLoc transition)) AggregateGuardTypeMismatch $- "comparison operands have different aggregate types '"- <> aggregateCanonicalName leftType- <> "' and '"- <> aggregateCanonicalName rightType- <> "'"- ]- | aggregateCapability useSite leftType == Unsupported ->- [ mkErr (locLine (tLoc transition)) AggregateGuardCapabilityUnsupported $- renderAggregateUseSite useSite- <> " is unsupported for aggregate type '"- <> aggregateCanonicalName leftType- <> "'"- ]- | otherwise -> []- _ -> []- where- useSite = case operator of- OpEq -> EqualityGuardUse- OpNeq -> EqualityGuardUse- OpLt -> OrderingGuardUse- OpLe -> OrderingGuardUse- OpGt -> OrderingGuardUse- OpGe -> OrderingGuardUse-- expressionType aggregate transition expression = case expression of- EAtom (ABool _) -> pure AggregateBool- EAtom (AName name) -> atomType aggregate transition name- EOr {} -> pure AggregateBool- EAnd {} -> pure AggregateBool- ECmp {} -> pure AggregateBool- EAdd _ left _ -> expressionType aggregate transition left- ESubtract _ left _ -> expressionType aggregate transition left- EMultiply _ left _ -> expressionType aggregate transition left- EPath loc _ path -> case path of- name : _ -> atomType aggregate transition name- [] -> Left (AggregateTypeError loc EqualityGuardUse (UnknownAggregateType "<empty-path>"))- ELiteral _ literal -> case literal of- LiteralBool {} -> pure AggregateBool- LiteralText {} -> pure AggregateText- LiteralIntegral {} -> Left (AggregateTypeError (exprLoc expression) EqualityGuardUse (UnknownAggregateType "<contextual-integral-literal>"))- LiteralQualified typeName _ -> resolveAggregateType symbols (exprLoc expression) EqualityGuardUse (TRef typeName)- LiteralId typeName _ -> resolveAggregateType symbols (exprLoc expression) EqualityGuardUse (TRef typeName)-- atomType aggregate transition name = case [register | register <- aggRegs aggregate, regName register == name] of- register : _ -> resolveAggregateType symbols (regLoc register) RegisterUse (regType register)- [] -> case [field | command <- aggCommands aggregate, cmdName command == tCommand transition, field <- cmdFields command, aggregateFieldName field == name] of- field : _ -> inferAggregateFieldType symbols aggregate CommandFieldUse field- [] -> case [enumName declaration | declaration <- specEnums spec, name `elem` map fst (enumCtors declaration)] of- enumType : _ -> resolveAggregateType symbols (tLoc transition) CommandFieldUse (TRef enumType)- []- | name `elem` map stName (aggStates aggregate) -> pure (AggregateVertex (aggName aggregate <> "Vertex"))- | Just rule <- firstMatching ((== name) . ruleName) (specRules spec) ->- resolveAggregateType symbols (ruleLoc rule) EqualityGuardUse (nameTypeExpr (ruleCodomain rule))- | otherwise -> Left (AggregateTypeError (tLoc transition) EqualityGuardUse (UnknownAggregateType name))-- nameTypeExpr name = case name of- "Text" -> TText- "Int" -> TInt- "Bool" -> TBool- "Natural" -> TNatural- "Time" -> TTime- "UTCTime" -> TTime- "Json" -> TJson- _ -> TRef name-- comparisons expression = case expression of- EOr left right -> comparisons left <> comparisons right- EAnd left right -> comparisons left <> comparisons right- ECmp operator left right -> (operator, left, right) : comparisons left <> comparisons right- EAdd _ left right -> comparisons left <> comparisons right- ESubtract _ left right -> comparisons left <> comparisons right- EMultiply _ left right -> comparisons left <> comparisons right- EPath {} -> []- ELiteral {} -> []- EAtom {} -> []--aggregateTypeDiagnostic :: AggregateTypeError -> Diagnostic-aggregateTypeDiagnostic aggregateError =- mkErr (locLine (aggregateTypeErrorLoc aggregateError)) diagnosticCode diagnosticMessage- where- diagnosticCode = case aggregateTypeErrorReason aggregateError of- UnknownAggregateType {} -> AggregateTypeUnknown- UnsupportedAggregateShape {} -> AggregateTypeUnsupportedAtUse- UnsupportedAggregateCapability {} -> case aggregateTypeErrorUseSite aggregateError of- EqualityGuardUse -> AggregateGuardCapabilityUnsupported- OrderingGuardUse -> AggregateGuardCapabilityUnsupported- _ -> AggregateTypeUnsupportedAtUse- InvalidRegisterInitial {} -> AggregateRegisterInitialInvalid- diagnosticMessage = case aggregateTypeErrorReason aggregateError of- UnknownAggregateType name ->- "unknown aggregate type '" <> name <> "' at " <> renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)- UnsupportedAggregateShape expression ->- "direct aggregate type '"- <> typeExprCanonicalName expression- <> "' is unsupported at "- <> renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)- <> "; use a mapped structural declaration for Json or container shapes"- UnsupportedAggregateCapability resolved ->- renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)- <> " is unsupported for aggregate type '"- <> aggregateCanonicalName resolved- <> "'"- InvalidRegisterInitial resolved detail ->- "invalid " <> aggregateCanonicalName resolved <> " register initial: " <> detail--renderAggregateUseSite :: AggregateUseSite -> Text-renderAggregateUseSite useSite = case useSite of- CommandFieldUse -> "command field"- EventFieldUse -> "event field"- RegisterUse -> "register"- EqualityGuardUse -> "equality guard"- OrderingGuardUse -> "ordering guard"- WholeValueWriteUse -> "whole-value write"- CodecUse -> "JSON codec"- SnapshotUse -> "snapshot"- HarnessSampleUse -> "harness sample"- HaskellLoweringUse -> "Haskell lowering"---- | 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 (isCabalPackageName (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,- onInteger = 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,- onInteger = 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 regType register of- TRef typeName -> case Map.lookup (MappedKey typeName) (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 '" <> typeName <> "' 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 = []--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--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.-validateNames :: Spec -> [Diagnostic]-validateNames spec =- concat- [ concatMap idNames (specIds spec),- concatMap enumNames (specEnums spec),- concatMap nominalNames (specNominalScalars spec),- concatMap nodeNames (specNodes spec)- ]- where- idNames declaration =- constructorName "id name" (idName declaration) (idLoc declaration)-- enumNames declaration =- constructorName "enum name" (enumName declaration) (enumLoc declaration)- ++ concatMap- (\(ctor, _) -> constructorName ("constructor of enum '" <> enumName declaration <> "'") ctor (enumLoc declaration))- (enumCtors declaration)-- nominalNames declaration =- constructorName "nominal scalar name" (nominalScalarName declaration) (nominalScalarLoc declaration)-- nodeNames = \case- NAggregate aggregate -> aggregateNames aggregate- NProcess process -> processNames process- NRouter router -> routerNames router- NContract contract ->- pascalizedNodeName "contract" (ctrName contract) (ctrLoc contract)- ++ concatMap- (\event -> constructorName "contract event name" (ceName event) (ctrLoc contract) ++ concatMap (contractFieldName contract) (ceFields event))- (ctrEvents contract)- NIntake intake -> pascalizedNodeName "intake" (inkName intake) (inkLoc intake)- NEmit emitNode -> pascalizedNodeName "emit" (emName emitNode) (emLoc emitNode)- NPublisher publisher -> pascalizedNodeName "publisher" (pubName publisher) (pubLoc publisher)- NWorkqueue workqueue ->- pascalizedNodeName "workqueue" (wqName workqueue) (wqLoc workqueue)- ++ constructorName "workqueue payload name" (wqPayloadName workqueue) (wqLoc workqueue)- ++ 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) (workflowNodeLoc workflow)- NOperation _ -> []-- aggregateNames aggregate =- constructorName "aggregate name" (aggName aggregate) (aggLoc aggregate)- ++ concatMap- (\register -> fieldNameRule "register name" (regName register) (regLoc register))- (aggRegs aggregate)- ++ concatMap commandNames (aggCommands aggregate)- ++ concatMap eventNames (aggEvents aggregate)- ++ maybe [] (\projection -> fieldNameRule "projection key" (projKey projection) (projLoc projection)) (aggProjection aggregate)- ++ vertexCollisions aggregate- where- commandNames command =- constructorName "command name" (cmdName command) (cmdLoc command)- ++ concatMap (\field -> fieldNameRule "command field" (aggregateFieldName field) (aggregateFieldLoc field)) (cmdFields command)- eventNames event =- constructorName "event name" (evName event) (evLoc event)- ++ case evBody event of- EventFields fields -> concatMap (\field -> fieldNameRule "event field" (aggregateFieldName field) (aggregateFieldLoc field)) fields- EventFromCommand _ -> []-- processNames process =- constructorName "process name" (procId process) (procLoc process)- ++ constructorName "process input name" (inName input) (procLoc process)- ++ concatMap (\field -> fieldNameRule "process input field" (fieldName field) (procLoc process)) (inFields input)- ++ concatMap (bindingName "advance field binding" (procLoc process)) (advFields (hAdvance handle))- ++ concatMap dispatchBindings (hDispatch handle)- ++ concatMap (bindingName "timer payload field binding" (tmLoc timer)) (tmPayload timer)- ++ concatMap (bindingName "timer fire field binding" (tmLoc timer)) (fireFields (tmFire timer))- where- input = procInput process- handle = procHandle process- timer = procTimer process- dispatchBindings dispatch = concatMap (bindingName "dispatch field binding" (dispLoc dispatch)) (dispFields dispatch)-- routerNames router =- constructorName "router name" (rtId router) (rtLoc router)- ++ constructorName "router input name" (inName input) (rtLoc router)- ++ concatMap (\field -> fieldNameRule "router input field" (fieldName field) (rtLoc router)) (inFields input)- ++ concatMap (\field -> fieldNameRule "router resolve-row field" field (rvLoc resolve)) (rvRow resolve)- ++ concatMap (bindingName "router dispatch field binding" (rdLoc dispatch)) (rdFields dispatch)- where- input = rtInput router- resolve = rtResolve router- dispatch = rtDispatch router-- bindingName category anchor binding = fieldNameRule category (fbName binding) anchor- contractFieldName contract field = fieldNameRule "contract field" (cfName field) (ctrLoc contract)-- constructorName category name anchor- | constructorSafe name = []- | otherwise =- [ mkErr (locLine anchor) IdentNotConstructorSafe $- category <> " '" <> name <> "' must be PascalCase: it becomes a Haskell constructor, type name, or module segment in scaffolded code"- ]-- pascalizedNodeName category name anchor- | "_" `T.isPrefixOf` name =- [ mkErr (locLine anchor) IdentNotConstructorSafe $- category <> " name '" <> name <> "' cannot begin with '_': title-casing leaves an invalid Haskell module segment"- ]- | otherwise = []-- fieldNameRule category name anchor- | name `Set.member` haskellKeywords =- [ mkErr (locLine anchor) IdentHaskellKeyword $- category <> " '" <> name <> "' is a Haskell keyword and cannot become a record field in generated code"- ]- | fieldSafe name = []- | otherwise =- [ mkErr (locLine anchor) IdentNotConstructorSafe $- category <> " '" <> name <> "' must begin with a lowercase ASCII letter or underscore to become a Haskell record field"- ]-- vertexCollisions aggregate =- [ mkErr (locLine (aggLoc aggregate)) VertexCtorCollision $- "aggregate '"- <> aggName aggregate- <> "' state '"- <> stName state- <> "' generates vertex constructor '"- <> vertex- <> "', which collides with "- <> declarationKind- <> " '"- <> vertex- <> "' in the generated Domain constructor namespace"- | state <- aggStates aggregate,- let vertex = aggName aggregate <> stName state,- declarationKind <- collisionKinds aggregate vertex- ]-- collisionKinds aggregate vertex =- ["event" | vertex `elem` map evName (aggEvents aggregate)]- ++ ["command" | vertex `elem` map cmdName (aggCommands aggregate)]- ++ ["enum constructor" | vertex `elem` [ctor | enum <- specEnums spec, (ctor, _) <- enumCtors enum]]---- Haskell 2010 reserved identifiers plus commonly enabled extension keywords.-haskellKeywords :: Set Name-haskellKeywords =- Set.fromList- [ "case",- "class",- "data",- "default",- "deriving",- "do",- "else",- "foreign",- "if",- "import",- "in",- "infix",- "infixl",- "infixr",- "instance",- "let",- "module",- "newtype",- "of",- "then",- "type",- "where",- "mdo",- "rec",- "proc"- ]--constructorSafe :: Name -> Bool-constructorSafe name = case T.uncons name of- Just (first, rest) -> asciiUpper first && T.all asciiAlphaNumOrUnderscore rest- Nothing -> False--fieldSafe :: Name -> Bool-fieldSafe name = case T.uncons name of- Just (first, rest) -> (asciiLower first || first == '_') && T.all asciiAlphaNumOrUnderscore rest- Nothing -> False--asciiUpper :: Char -> Bool-asciiUpper c = c >= 'A' && c <= 'Z'--asciiLower :: Char -> Bool-asciiLower c = c >= 'a' && c <= 'z'--asciiAlphaNumOrUnderscore :: Char -> Bool-asciiAlphaNumOrUnderscore c = asciiUpper c || asciiLower c || (c >= '0' && c <= '9') || c == '_'--kafkaTopicError :: Text -> Maybe Text-kafkaTopicError topic- | T.null topic = Just "is empty"- | T.length topic > 249 = Just "is longer than Kafka's 249-character limit"- | topic == "." || topic == ".." = Just "is reserved by Kafka"- | Just illegal <- T.find (not . kafkaTopicCharacter) topic =- Just ("contains character " <> T.pack (show illegal) <> "; use only ASCII letters, digits, '.', '_', or '-'")- | otherwise = Nothing- where- kafkaTopicCharacter character = asciiAlphaNumOrUnderscore character || character == '.' || character == '-'--validPostgresIdentifier :: Text -> Bool-validPostgresIdentifier identifier =- T.length identifier <= 63- && case T.uncons identifier of- Nothing -> False- Just (firstCharacter, rest) ->- (asciiLower firstCharacter || firstCharacter == '_')- && T.all (\character -> asciiLower character || (character >= '0' && character <= '9') || character == '_') rest---- | Rules over namespaces shared by the whole specification.-specLevelRules :: EffectiveLanguageContract -> Spec -> [Diagnostic]-specLevelRules languageContract spec = duplicateNodes ++ duplicateEnumMembers ++ duplicateIdPrefixes ++ duplicateDeclarations ++ runtimeIdentities ++ duplicateRuntimeIdentities ++ ruleDiagnostics- where- duplicateNodes =- [ mkErr (locLine loc) DuplicateNodeName $- "duplicate " <> kind <> " node name '" <> name <> "'"- | node <- duplicatesBy nodeKey (specNodes spec),- let (kind, name, loc) = nodeIdentity node- ]- nodeKey node = let (kind, name, _) = nodeIdentity node in (kind, name)- duplicateEnumMembers = concatMap enumDuplicates (specEnums spec)- enumDuplicates e =- [ mkErr (locLine (enumLoc e)) DuplicateEnumCtor $- "enum '" <> enumName e <> "' declares constructor '" <> ctor <> "' more than once"- | (ctor, _) <- duplicatesBy fst (enumCtors e)- ]- ++ [ mkErr (locLine (enumLoc e)) DuplicateEnumWire $- "enum '" <> enumName e <> "' declares wire spelling '" <> wire <> "' more than once"- | (_, wire) <- duplicatesBy snd (enumCtors e)- ]- duplicateIdPrefixes =- [ mkErr (locLine (idLoc d)) DuplicateIdPrefix $- "id '" <> idName d <> "' reuses prefix '" <> idPrefix d <> "'"- | d <- duplicatesBy idPrefix (specIds spec)- ]- duplicateDeclarations =- [ mkErr (locLine loc) NominalDuplicateDeclaration $- "duplicate " <> category <> " declaration '" <> name <> "'; the last declaration would silently replace the earlier one"- | enforcesSpecSurfaceClosures languageContract,- (category, name, loc) <- duplicatesBy (\(category, name, _) -> (category, name)) declarationOrigins- ]- declarationOrigins =- [("id", idName value, idLoc value) | value <- specIds spec]- <> [("enum", enumName value, enumLoc value) | value <- specEnums spec]- <> [("nominal scalar", nominalScalarName value, nominalScalarLoc value) | value <- specNominalScalars spec]- <> [("mapped", mappedName value, mappedLoc value) | value <- specMapped spec]- <> [("rule", ruleName value, ruleLoc value) | value <- specRules spec]- runtimeIdentities =- [ mkErr (locLine loc) RuntimeIdentityInvalid $- kind <> " stable identity " <> T.pack (show identity) <> " " <> reason- | enforcesSpecSurfaceClosures languageContract,- (kind, identity, loc) <- stableIdentityOrigins,- Just reason <- [stableIdentityError identity]- ]- duplicateRuntimeIdentities =- [ mkErr (locLine loc) RuntimeIdentityDuplicate $- kind <> " stable identity " <> T.pack (show identity) <> " is already used by another workflow, process, or router"- | enforcesSpecSurfaceClosures languageContract,- (kind, identity, loc) <- duplicatesBy (\(_, identity, _) -> identity) stableIdentityOrigins- ]- stableIdentityOrigins =- [("workflow", wfStable workflow, workflowNodeLoc workflow) | NWorkflow workflow <- specNodes spec]- <> [("process", procName process, procLoc process) | NProcess process <- specNodes spec]- <> [("router", rtName router, rtLoc router) | NRouter router <- specNodes spec]- ruleDiagnostics = concatMap (validateRule spec) (specRules spec)--nodeIdentity :: Node -> (Text, Name, Loc)-nodeIdentity (NAggregate a) = ("aggregate", aggName a, aggLoc a)-nodeIdentity (NProcess p) = ("process", procId p, procLoc p)-nodeIdentity (NRouter r) = ("router", rtId r, rtLoc r)-nodeIdentity (NContract c) = ("contract", ctrName c, ctrLoc c)-nodeIdentity (NIntake i) = ("intake", inkName i, inkLoc i)-nodeIdentity (NEmit e) = ("emit", emName e, emLoc e)-nodeIdentity (NPublisher p) = ("publisher", pubName p, pubLoc p)-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, workflowNodeLoc w)-nodeIdentity (NOperation o) = ("operation", opName o, opLoc o)--validateNode :: EffectiveLanguageContract -> Spec -> Node -> [Diagnostic]-validateNode languageContract spec (NAggregate agg) = validateAggregate languageContract spec agg-validateNode _languageContract spec (NProcess p) = validateProcess spec p-validateNode _languageContract spec (NRouter router) = validateRouter spec router-validateNode languageContract _spec (NContract contract) = validateContract languageContract contract-validateNode languageContract spec (NIntake i) = validateIntake languageContract i ++ intakeCoupling languageContract spec i-validateNode languageContract spec (NEmit e) = validateEmit languageContract spec e-validateNode languageContract spec (NPublisher p) = validatePublisher languageContract spec p-validateNode _languageContract _spec (NWorkqueue w) = validateWorkqueue w-validateNode _languageContract spec (NPgmqDispatch d) = validatePgmqDispatch spec d-validateNode languageContract spec (NReadModel readModel) = validateReadModel languageContract spec readModel-validateNode _languageContract _spec (NWorkflow w) = validateWorkflow w-validateNode _languageContract spec (NOperation o) = validateOperation spec o--validateContract :: EffectiveLanguageContract -> ContractNode -> [Diagnostic]-validateContract languageContract contract =- typeIdPrefixErrors- <> schemaVersionFloor- <> topicNames- <> duplicateEvents- <> duplicateTopicAliases- <> duplicateFields- <> discriminatorShadows- <> unresolvedTopicAliases- where- typeIdPrefixErrors =- [ mkErr (locLine (ctrLoc contract)) ContractInvalidTypeIdPrefix $- "contract '"- <> ctrName contract- <> "' event '"- <> ceName event- <> "' field '"- <> cfName field- <> "' has invalid TypeID prefix '"- <> prefix- <> "': "- <> T.pack (show reason)- | event <- ctrEvents contract,- field <- ceFields event,- CTypeId prefix <- [cfType field],- Just _ <- [contractIdDomainContractFor languageContract prefix],- Just reason <- [TypeID.checkPrefix prefix]- ]- schemaVersionFloor =- [ mkErr (locLine (ctrLoc contract)) ContractSchemaVersionBelowMinimum $- "contract '" <> ctrName contract <> "' schemaVersion must be at least 1"- | enforcesSpecSurfaceClosures languageContract,- ctrSchemaVersion contract < 1- ]- topicNames =- [ mkErr (locLine (ctrLoc contract)) ContractTopicNameInvalid $- "contract '" <> ctrName contract <> "' topic alias '" <> alias <> "' has invalid Kafka topic " <> T.pack (show topic) <> ": " <> reason- | (alias, topic) <- ctrTopics contract,- T.null topic || enforcesSpecSurfaceClosures languageContract,- Just reason <- [kafkaTopicError topic]- ]- duplicateEvents =- [ mkErr (locLine (ctrLoc contract)) ContractDuplicateEvent $- "contract '" <> ctrName contract <> "' declares event '" <> ceName event <> "' more than once"- | event <- duplicatesBy ceName (ctrEvents contract)- ]- duplicateTopicAliases =- [ mkErr (locLine (ctrLoc contract)) ContractDuplicateTopicAlias $- "contract '" <> ctrName contract <> "' declares topic alias '" <> alias <> "' more than once"- | (alias, _) <- duplicatesBy fst (ctrTopics contract)- ]- duplicateFields =- [ mkErr (locLine (ctrLoc contract)) ContractDuplicateFieldName $- "contract '" <> ctrName contract <> "' event '" <> ceName event <> "' declares field '" <> cfName field <> "' more than once"- | event <- ctrEvents contract,- field <- duplicatesBy cfName (ceFields event)- ]- discriminatorShadows =- [ mkErr (locLine (ctrLoc contract)) ContractFieldShadowsDiscriminator $- "contract '"- <> ctrName contract- <> "' event '"- <> ceName event- <> "' field '"- <> cfName field- <> "' shadows the payload discriminator"- | enforcesSpecSurfaceClosures languageContract,- event <- ctrEvents contract,- field <- ceFields event,- cfName field == ctrDiscriminator contract- ]- unresolvedTopicAliases =- [ mkErr (locLine (ctrLoc contract)) ContractTopicAliasUnresolved $- "contract '" <> ctrName contract <> "' event '" <> ceName event <> "' names undeclared topic alias '" <> ceTopic event <> "'"- | enforcesSpecSurfaceClosures languageContract,- event <- ctrEvents contract,- ceTopic event `notElem` map fst (ctrTopics contract)- ]---- | Workflow replay keys, patch guards, rotation, and injected inputs must be unambiguous.-validateWorkflow :: WorkflowNode -> [Diagnostic]-validateWorkflow w = duplicateLabels ++ sleepFields ++ patchDuplicates ++ patchIds ++ continuePositions ++ idField- where- inputFields = map fieldName (wfInputFields w)- labelledItems = workflowLabelledItems (wfBody w)- patchItems = workflowPatchItems (wfBody w)- duplicateLabels =- [ mkErr (locLine (wfBodyLoc item)) WorkflowDuplicateLabel $- "workflow '" <> wfId w <> "' declares label '" <> label <> "' more than once; labels key deterministic replay, so a duplicate label replays the first occurrence's journaled result"- | (label, item) <- duplicatesBy fst labelledItems- ]- sleepFields =- [ mkErr (locLine loc) WorkflowSleepDelayUnresolved $- "workflow '" <> wfId w <> "' sleep '" <> label <> "' references undeclared input field '" <> delay <> "'"- | WfSleep label delay loc <- map snd labelledItems,- delay `notElem` inputFields- ]- patchDuplicates =- [ mkErr (locLine loc) WorkflowPatchDuplicate $- "workflow '" <> wfId w <> "' declares patch id '" <> patchId <> "' more than once; patch decisions journal under one stable key"- | (patchId, _, loc) <- duplicatesBy (\(patchId, _, _) -> patchId) patchItems- ]- patchIds =- [ mkErr (locLine loc) WorkflowPatchIdInvalid $- "workflow '" <> wfId w <> "' patch id '" <> patchId <> "' contains ':'; the runtime reserves that separator for the patch journal-key prefix"- | (patchId, _, loc) <- patchItems,- ":" `T.isInfixOf` patchId- ]- continuePositions =- [ mkErr (locLine loc) WorkflowContinueAsNewNotTerminal $- "workflow '" <> wfId w <> "' continueAsNew must be the last top-level body item and may not appear inside a patch"- | (isTopLevelTerminal, loc) <- workflowContinueItems (wfBody w),- not isTopLevelTerminal- ]- idField = case wfIdField w of- Just field- | field `notElem` inputFields ->- [ mkErr (locLine (workflowNodeLoc w)) WorkflowIdFieldUnresolved $- "workflow '" <> wfId w <> "' derives its id from undeclared input field '" <> field <> "'"- ]- _ -> []--wfBodyLoc :: WfBodyItem -> Loc-wfBodyLoc (WfStep _ _ loc) = loc-wfBodyLoc (WfAwait _ _ loc) = loc-wfBodyLoc (WfSleep _ _ loc) = loc-wfBodyLoc (WfChild _ _ _ loc) = loc-wfBodyLoc (WfPatch _ _ loc) = loc-wfBodyLoc (WfContinueAsNew _ loc) = loc--workflowLabelledItems :: [WfBodyItem] -> [(Name, WfBodyItem)]-workflowLabelledItems = concatMap go- where- go item@(WfStep label _ _) = [(label, item)]- go item@(WfAwait label _ _) = [(label, item)]- go item@(WfSleep label _ _) = [(label, item)]- go item@(WfChild label _ _ _) = [(label, item)]- go (WfPatch _ items _) = workflowLabelledItems items- go WfContinueAsNew {} = []--workflowPatchItems :: [WfBodyItem] -> [(Name, [WfBodyItem], Loc)]-workflowPatchItems = concatMap go- where- go (WfPatch patchId items loc) = (patchId, items, loc) : workflowPatchItems items- go _ = []---- | Pair every rotation with whether it is the final top-level item.-workflowContinueItems :: [WfBodyItem] -> [(Bool, Loc)]-workflowContinueItems items = topLevel ++ nested- where- topLevel =- [ (index == length items - 1, loc)- | (index, WfContinueAsNew _ loc) <- zip [0 ..] items- ]- nested =- [ (False, loc)- | WfPatch _ patchBody _ <- items,- (_, loc) <- workflowContinueItems patchBody- ]---- | A top-level rule is a total, clock-free function over one declared enum.-validateRule :: Spec -> RuleDecl -> [Diagnostic]-validateRule spec rule = case [e | e <- specEnums spec, enumName e == ruleDomain rule] of- [] ->- [ mkErr rl RuleDomainUnresolved $- "rule '" <> ruleName rule <> "' has undeclared enum domain '" <> ruleDomain rule <> "'"- ]- (domain : _) -> totality domain ++ unknownCases domain ++ bodyDiagnostics- where- rl = locLine (ruleLoc rule)- caseNames = map fst (ruleCases rule)- allEnumCtors = Set.fromList [ctor | e <- specEnums spec, (ctor, _) <- enumCtors e]- totality domain =- let missing = [ctor | (ctor, _) <- enumCtors domain, ctor `notElem` caseNames]- in [ mkErr rl RuleNotTotal $- "rule '" <> ruleName rule <> "' is not total over enum '" <> enumName domain <> "'; missing cases {" <> T.intercalate ", " missing <> "}"- | not (null missing)- ]- unknownCases domain =- [ mkErr rl RuleCaseUnknownCtor $- "rule '" <> ruleName rule <> "' has case '" <> ctor <> "' which is not a constructor of enum '" <> enumName domain <> "'"- | (ctor, _) <- ruleCases rule,- ctor `notElem` map fst (enumCtors domain)- ]- bodyDiagnostics = concatMap validateBody (ruleCases rule)- validateBody (ctor, expr) =- [ mkErr rl ClockSampled $- "rule '" <> ruleName rule <> "' case '" <> ctor <> "' samples the wall clock via '" <> atom <> "'; rules must be deterministic"- | atom <- dedup (exprNames expr),- atom `Set.member` clockAtoms- ]- ++ [ mkErr rl GuardAtomOutOfScope $- "atom '" <> atom <> "' in rule '" <> ruleName rule <> "' resolves to no enum constructor or boolean literal"- | atom <- dedup (exprNames expr),- atom `Set.notMember` clockAtoms,- atom `Set.notMember` allEnumCtors- ]---- | Operation rules resolve command aggregates, stream fields, projections,--- read models, workflow signal labels and value types, and run targets.-validateOperation :: Spec -> OperationNode -> [Diagnostic]-validateOperation spec o = case opShape o of- CommandOp aggregate streamField _ projections ->- aggregateRef aggregate streamField ++ projectionRefs projections- QueryOp readModel _ _ consistency ->- resolveReadModelRef QueryUnresolvedReadModel spec (opLoc o) ("query operation '" <> opName o <> "'") readModel- ++ [ mkErr ol QueryConsistencyInvalid $- "query operation '" <> opName o <> "' has unknown consistency '" <> consistency <> "'; expected Strong, Eventual, or PositionWait"- | consistency `notElem` (["Strong", "Eventual", "PositionWait"] :: [Name])- ]- SignalOp lbl wf _ _ valueType ->- case lookupWorkflow wf of- Nothing ->- [mkErr ol AwaitSignalMismatch ("signal operation '" <> opName o <> "' targets undeclared workflow '" <> wf <> "'")]- Just w -> case [(resultType, loc) | (_, WfAwait label resultType loc) <- workflowLabelledItems (wfBody w), label == lbl] of- [] ->- [ mkErr ol AwaitSignalMismatch $- "signal '" <> lbl <> "' of " <> wf <> " has no matching 'await' (workflow declares awaits {" <> T.intercalate ", " (awaitLabels w) <> "}); the deterministic awakeable id will not match and the workflow will wait forever"- ]- ((resultType, _) : _)- | valueType == resultType -> []- | otherwise ->- [ mkErr ol AwaitSignalValueMismatch $- "signal '" <> lbl <> "' of " <> wf <> " carries value type '" <> valueType <> "' but the await expects '" <> resultType <> "'"- ]- RunOp wf _ _ ->- [ mkErr ol RunWorkflowUnresolved ("run operation '" <> opName o <> "' targets undeclared workflow '" <> wf <> "'")- | wf `notElem` map wfId workflows- ]- where- ol = locLine (opLoc o)- workflows = [w | NWorkflow w <- specNodes spec]- aggregates = [a | NAggregate a <- specNodes spec]- projectionTables = [projTable p | a <- aggregates, Just p <- [aggProjection a]]- lookupWorkflow n = case [w | w <- workflows, wfId w == n] of (w : _) -> Just w; [] -> Nothing- awaitLabels w = [l | (_, WfAwait l _ _) <- workflowLabelledItems (wfBody w)]- aggregateRef name streamField = case [a | a <- aggregates, aggName a == name] of- [] ->- [ mkErr ol OperationUnresolvedRef $- "command operation '" <> opName o <> "' targets undeclared aggregate '" <> name <> "'"- ]- (aggregate : _) ->- [ mkErr ol OperationUnresolvedRef $- "command operation '" <> opName o <> "' stream field '" <> streamField <> "' is not declared by any command of aggregate '" <> name <> "'"- | streamField `notElem` [aggregateFieldName field | command <- aggCommands aggregate, field <- cmdFields command]- ]- projectionRefs projections =- [ mkErr ol OperationUnresolvedRef $- "command operation '" <> opName o <> "' references undeclared projection table '" <> projection <> "'"- | projection <- projections,- projection `notElem` projectionTables- ]---- | Resolve a named read-model node using the caller's diagnostic code.-resolveReadModelRef :: DiagnosticCode -> Spec -> Loc -> Text -> Name -> [Diagnostic]-resolveReadModelRef diagnosticCode spec diagnosticLoc context name =- [ mkErr (locLine diagnosticLoc) diagnosticCode $- context <> " references undeclared readmodel '" <> name <> "'"- | name `notElem` [rmName readModel | NReadModel readModel <- specNodes spec]- ]---- | Validate captured identity, feed semantics, and the declared column surface.-validateReadModel :: EffectiveLanguageContract -> Spec -> ReadModelNode -> [Diagnostic]-validateReadModel languageContract spec readModel =- shapeFixture ++ columnTypes ++ strongFeed ++ scopeMode ++ inlineReference ++ versionFloor ++ identifiers ++ duplicateColumns- where- readModelLine = locLine (rmLoc readModel)- expectedShape = deriveShapeHash readModel- shapeFixture =- [ mkErr readModelLine RmShapeHashDrift $- "readmodel '"- <> rmName readModel- <> "': captured shape \""- <> rmShape readModel- <> "\" does not match the declared columns (expected \""- <> expectedShape- <> "\"); update the fixture AND bump version if the table shape really changed"- | rmShape readModel /= expectedShape- ]- allowedColumnTypes = Set.fromList ["text", "int", "bigint", "bool", "timestamptz", "jsonb", "numeric"]- columnTypes =- [ mkErr readModelLine RmUnknownColumnType $- "readmodel '" <> rmName readModel <> "' column '" <> rmcName columnDecl <> "' has unknown type '" <> rmcType columnDecl <> "'"- | columnDecl <- rmColumns readModel,- rmcType columnDecl `Set.notMember` allowedColumnTypes- ]- strongFeed =- [ mkErr readModelLine RmStrongInlineOnly $- "readmodel '"- <> rmName readModel- <> "': consistency = Strong with feed = inline; an inline-only model has no subscription worker to advance the cursor a Strong read waits on. Use consistency = Eventual, or feed = subscription"- | rmFeed readModel == RmInline,- rmConsistency readModel == Strong- ]- scopeMode =- [ mkErr readModelLine RmScopeWithoutStrong $- "readmodel '" <> rmName readModel <> "': scope is meaningful only with consistency = Strong"- | rmScope readModel /= Nothing,- rmConsistency readModel /= Strong- ]- inlineReference =- [ mkErr readModelLine RmInlineFeedUnreferenced $- "readmodel '" <> rmName readModel <> "' declares feed = inline but no aggregate projection references it"- | rmFeed readModel == RmInline,- rmName readModel `notElem` [projTable projection | NAggregate aggregate <- specNodes spec, Just projection <- [aggProjection aggregate]]- ]- versionFloor =- [ mkErr readModelLine ReadModelVersionBelowMinimum $- "readmodel '" <> rmName readModel <> "' version must be at least 1"- | enforcesSpecSurfaceClosures languageContract,- rmVersion readModel < 1- ]- identifiers =- [ mkErr readModelLine ReadModelIdentifierInvalid $- "readmodel '" <> rmName readModel <> "' " <> kind <> " " <> T.pack (show identifier) <> " is not a PostgreSQL unquoted identifier"- | enforcesSpecSurfaceClosures languageContract,- (kind, identifier) <-- [("schema", rmSchema readModel), ("table", rmTable readModel)]- <> [("column", rmcName columnDecl) | columnDecl <- rmColumns readModel],- not (validPostgresIdentifier identifier)- ]- duplicateColumns =- [ mkErr readModelLine ReadModelDuplicateColumn $- "readmodel '" <> rmName readModel <> "' declares column '" <> rmcName columnDecl <> "' more than once"- | enforcesSpecSurfaceClosures languageContract,- columnDecl <- duplicatesBy rmcName (rmColumns readModel)- ]---- | EP-5 workqueue rules: the captured physical name must match the queueRef--- derivation; the disposition inversions (storeFailure transient => must retry;--- decodeFailure poison => must dead-letter); and dlq=on requires a retry ceiling.-validateWorkqueue :: WorkqueueNode -> [Diagnostic]-validateWorkqueue w = concat [divergence, completeness, duplicateRows, inversions, retryCeiling, orderingRules, groupKeyRules, provisionRules]- where- wl = locLine (wqLoc w)- rows = wqDisposition w- (derivedPhysical, derivedDlq, derivedTable) = derivedQueueTrio (wqLogical w)- divergence =- [ mkErr wl WqPhysicalDivergence $- "workqueue '" <> wqName w <> "': captured physical \"" <> wqPhysical w <> "\" diverges from queueRef(\"" <> wqLogical w <> "\") = \"" <> derivedPhysical <> "\""- | wqPhysical w /= derivedPhysical- ]- ++ [ mkErr wl WqDlqDivergence $- "workqueue '" <> wqName w <> "': captured dlq \"" <> wqDlq w <> "\" diverges from queueRef = \"" <> derivedDlq <> "\""- | wqDlq w /= derivedDlq- ]- ++ [ mkErr wl WqTableDivergence $- "workqueue '" <> wqName w <> "': captured table \"" <> wqTable w <> "\" diverges from queueRef table = \"" <> derivedTable <> "\""- | wqTable w /= derivedTable- ]- requiredOutcomes = ["storeFailure", "commandRejected", "decodeFailure", "onCodecReject"]- completeness =- [ mkErr wl WqDispositionIncomplete $- "workqueue '" <> wqName w <> "' disposition table is missing outcome '" <> outcome <> "'"- | outcome <- requiredOutcomes,- outcome `notElem` map wqdOutcome rows- ]- duplicateRows =- [ mkErr (locLine (wqdLoc row)) DispositionDuplicateOutcome $- "workqueue '" <> wqName w <> "' repeats disposition outcome '" <> wqdOutcome row <> "'; the first row would shadow this row"- | row <- duplicatesBy wqdOutcome rows- ]- firstRow outcome = case [row | row <- rows, wqdOutcome row == outcome] of- (row : _) -> Just row- [] -> Nothing- isRetry row = case wqdAction row of IRetry _ -> True; _ -> False- isDeadLetter row = case wqdAction row of IDeadLetter _ -> True; _ -> False- inversions =- [ mkErr (locLine (wqdLoc row)) WqStoreFailureNotRetry ("workqueue '" <> wqName w <> "': 'storeFailure' is transient and MUST retry, not dead-letter")- | Just row <- [firstRow "storeFailure"],- isDeadLetter row- ]- ++ [ mkErr (locLine (wqdLoc row)) WqDecodeFailureNotDeadLetter ("workqueue '" <> wqName w <> "': 'decodeFailure' is poison and MUST dead-letter, not retry")- | Just row <- [firstRow "decodeFailure"],- isRetry row- ]- retryCeiling =- [ mkErr wl WqDlqWithoutCeiling ("workqueue '" <> wqName w <> "': dlq=on requires maxRetries >= 1 (an absent ceiling never dead-letters)")- | wqDlqOn w && wqMaxRetries w < 1- ]- fifo = wqOrdering w /= WqUnordered- orderingRules =- [ mkErr wl WqGroupKeyMissing $- "workqueue '" <> wqName w <> "': FIFO delivery is per group, so ordering requires a 'group key' clause that makes enqueueToGroup deterministic"- | fifo && wqGroupKey w == Nothing- ]- ++ [ mkErr wl WqGroupKeyWithoutFifo $- "workqueue '" <> wqName w <> "': a group key with unordered reads would be ignored; declare a FIFO ordering or remove the key"- | not fifo && wqGroupKey w /= Nothing- ]- groupKeyRules = case wqGroupKey w of- Nothing -> []- Just groupKey ->- case [field | field <- wqPayload w, wqfName field == gkField groupKey] of- [] ->- [ mkErr wl WqGroupKeyUnresolved $- "workqueue '" <> wqName w <> "': group key field '" <> gkField groupKey <> "' is not declared in its payload"- ]- field : _ ->- [ mkErr wl WqGroupKeyUnresolved $- "workqueue '" <> wqName w <> "': group key via raw requires a text payload field, but '" <> gkField groupKey <> "' has type '" <> wqfType field <> "'"- | gkVia groupKey == "raw" && wqfType field /= "text"- ]- ++ [ mkErr wl WqGroupKeyUnresolved $- "workqueue '" <> wqName w <> "': opaque group-key derivation '" <> gkVia groupKey <> "' requires a captured fixture"- | gkVia groupKey /= "raw" && gkFixture groupKey == Nothing- ]- provisionRules = case wqProvision w of- WqStandard -> []- WqUnlogged ->- [ Diagnostic- { line = wl,- severity = Warning,- code = WqUnloggedDurability,- message = "workqueue '" <> wqName w <> "': provision unlogged is truncated to empty on a database crash; use it only for transient, regenerable work"- }- ]- WqPartitioned interval retention ->- [ mkErr wl WqPartitionSpecEmpty $- "workqueue '" <> wqName w <> "': partition interval and retention must be non-empty; they are create-time settings and the additive reconciler will not migrate an existing queue"- | T.null interval || T.null retention- ]---- | EP-5 dispatch rule: the @enqueue to@ target must resolve to a declared workqueue.-validatePgmqDispatch :: Spec -> PgmqDispatchNode -> [Diagnostic]-validatePgmqDispatch spec d = enqueueRef ++ dedupQueueRef ++ sourceReadModelRef ++ dedupReadModelRef ++ dedupReadModelField- where- dl = locLine (pdLoc d)- workqueues = [w | NWorkqueue w <- specNodes spec]- enqueueRef =- [ mkErr dl DispatchEnqueueUnresolved ("dispatch '" <> pdName d <> "' enqueues to undeclared workqueue '" <> pdEnqueueTo d <> "'")- | pdEnqueueTo d `notElem` map wqName workqueues- ]- dedupQueueRef = case [w | w <- workqueues, wqName w == pdDedupQueue d] of- [] ->- [ mkErr dl DispatchDedupQueueUnresolved $- "dispatch '" <> pdName d <> "' checks an undeclared dedup queue '" <> pdDedupQueue d <> "'"- ]- (queue : _) ->- [ mkErr dl DispatchDedupFieldUnresolved $- "dispatch '" <> pdName d <> "' dedup field '" <> pdDedupQueueField d <> "' is not a payload wire field of queue '" <> pdDedupQueue d <> "'"- | pdDedupQueueField d `notElem` map wqfWire (wqPayload queue)- ]- sourceReadModelRef =- resolveReadModelRef DispatchReadModelUnresolved spec (pdLoc d) ("dispatch '" <> pdName d <> "' source") (pdSourceReadModel d)- dedupReadModelRef =- resolveReadModelRef DispatchReadModelUnresolved spec (pdLoc d) ("dispatch '" <> pdName d <> "' dedup") (pdDedupReadModel d)- dedupReadModelField = case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == pdDedupReadModel d] of- [] -> []- (readModel : _) ->- [ mkErr dl DispatchReadModelFieldUnknown $- "dispatch '" <> pdName d <> "' dedup field '" <> pdDedupReadModelField d <> "' is not a declared column of readmodel '" <> pdDedupReadModel d <> "'"- | pdDedupReadModelField d `notElem` map rmcName (rmColumns readModel)- ]---- | The declared contracts in a spec, by name.-specContracts :: Spec -> [ContractNode]-specContracts spec = [c | NContract c <- specNodes spec]---- | EP-4 cross-node coupling: an intake's contract/topic/accepted-events resolve.-intakeCoupling :: EffectiveLanguageContract -> Spec -> IntakeNode -> [Diagnostic]-intakeCoupling languageContract spec i = case lookupContract (inkContract i) of- Nothing ->- [mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' references undeclared contract '" <> inkContract i <> "'")]- Just c ->- [ mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' topic '" <> inkTopic i <> "' is not a topic of contract '" <> inkContract i <> "'")- | inkTopic i `notElem` map fst (ctrTopics c)- ]- ++ [ mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' accepts event '" <> ev <> "' not declared in contract '" <> inkContract i <> "'")- | ev <- inkAccept i,- ev `notElem` map ceName (ctrEvents c)- ]- ++ [ mkErr (locLine (inkLoc i)) TopicAffinityMismatch $- "intake '" <> inkName i <> "' subscribes to topic '" <> inkTopic i <> "' but accepted event '" <> ceName event <> "' is declared on topic '" <> ceTopic event <> "'"- | event <- ctrEvents c,- ceName event `elem` inkAccept i,- ceTopic event /= inkTopic i- ]- ++ [ mkErr (locLine (inkLoc i)) IntakeBindUnresolved $- "intake '" <> inkName i <> "' binds undeclared envelope or accepted-event field '" <> brField binding <> "'"- | enforcesSpecSurfaceClosures languageContract,- binding <- inkBinds i,- brField binding `Set.notMember` resolvableFields c- ]- ++ [ mkErr (locLine (inkLoc i)) IntakeDedupeKeyUnresolved $- "intake '" <> inkName i <> "' dedupe key '" <> inkDedupeKey i <> "' is not an envelope or accepted-event field"- | enforcesSpecSurfaceClosures languageContract,- inkDedupeKey i `Set.notMember` resolvableFields c- ]- ++ [ mkErr (locLine (inkLoc i)) IntakeDecodeSchemaVersionMismatch $- "intake '"- <> inkName i- <> "' decode schemaVersion "- <> tInt (decBodySchemaVersion (inkDecode i))- <> " does not match contract '"- <> ctrName c- <> "' schemaVersion "- <> tInt (ctrSchemaVersion c)- | enforcesSpecSurfaceClosures languageContract,- decBodySchemaVersion (inkDecode i) /= ctrSchemaVersion c- ]- where- lookupContract n = case [c | c <- specContracts spec, ctrName c == n] of (c : _) -> Just c; [] -> Nothing- resolvableFields contract =- canonicalIntakeEnvelopeFields- <> Set.fromList- [ cfName field- | event <- ctrEvents contract,- ceName event `elem` inkAccept i,- field <- ceFields event- ]--validateEmit :: EffectiveLanguageContract -> Spec -> EmitNode -> [Diagnostic]-validateEmit languageContract spec e = skipRule ++ duplicateCases ++ coupling- where- el = locLine (emLoc e)- skipRule =- [ mkErr el EmitSkipMissing ("emit '" <> emName e <> "' map must end with an explicit '_ => skip' catch-all (hole-kind 7 optionality)")- | not (emSkip e)- ]- duplicateCases =- [ mkErr (locLine (emrLoc row)) EmitMapDuplicateCase $- "emit '" <> emName e <> "' repeats map discriminant '" <> emrValue row <> "'; the first row would shadow this row"- | enforcesSpecSurfaceClosures languageContract,- row <- duplicatesBy emrValue (emMap e)- ]- coupling = case [c | c <- specContracts spec, ctrName c == emContract e] of- [] -> [mkErr el EmitUnresolvedContract ("emit '" <> emName e <> "' references undeclared contract '" <> emContract e <> "'")]- (c : _) ->- [ mkErr el EmitUnresolvedContract ("emit '" <> emName e <> "' topic '" <> emTopic e <> "' is not a topic of contract '" <> emContract e <> "'")- | emTopic e `notElem` map fst (ctrTopics c)- ]- ++ [ mkErr (locLine (emrLoc r)) EmitUnresolvedContract ("emit '" <> emName e <> "' maps to event '" <> emrEvent r <> "' not declared in contract '" <> emContract e <> "'")- | r <- emMap e,- emrEvent r `notElem` map ceName (ctrEvents c)- ]- ++ [ mkErr (locLine (emrLoc row)) TopicAffinityMismatch $- "emit '" <> emName e <> "' publishes on topic '" <> emTopic e <> "' but mapped event '" <> emrEvent row <> "' is declared on topic '" <> ceTopic event <> "'"- | row <- emMap e,- event <- ctrEvents c,- ceName event == emrEvent row,- ceTopic event /= emTopic e- ]--validatePublisher :: EffectiveLanguageContract -> Spec -> PublisherNode -> [Diagnostic]-validatePublisher languageContract spec p =- unresolvedEmit ++ orderingVocabulary ++ backoffPolicy ++ attemptsFloor- where- publisherLine = locLine (pubLoc p)- unresolvedEmit =- [ mkErr publisherLine PublisherUnresolvedEmit ("publisher '" <> pubName p <> "' references undeclared emit '" <> pubEmit p <> "'")- | pubEmit p `notElem` [emName e | NEmit e <- specNodes spec]- ]- orderingVocabulary =- [ mkErr publisherLine PublisherOrderingUnknown $- "publisher '"- <> pubName p- <> "' has unknown ordering '"- <> pubOrdering p- <> "'; expected PerKeyHeadOfLine, PerSourceStream, StopTheLine, or BestEffort"- | pubOrdering p `Set.notMember` publisherOrderings- ]- backoffPolicy =- [ mkErr publisherLine PublisherBackoffInvalid $- "publisher '" <> pubName p <> "' has an invalid " <> problem- | Just problem <- [backoffProblemMaybe (pubBackoff p)]- ]- attemptsFloor =- [ mkErr publisherLine PublisherMaxAttemptsBelowMinimum $- "publisher '" <> pubName p <> "' maxAttempts must be at least 1"- | enforcesSpecSurfaceClosures languageContract,- pubMaxAttempts p < 1- ]--publisherOrderings :: Set Name-publisherOrderings = Set.fromList ["PerKeyHeadOfLine", "PerSourceStream", "StopTheLine", "BestEffort"]--backoffProblemMaybe :: BackoffSpec -> Maybe Text-backoffProblemMaybe backoff = case boKind backoff of- "constant" -> Nothing- "exponential" -> case (boMax backoff, boMultiplier backoff) of- (Just maximumWindow, Just multiplierText) ->- case (validationWindowSeconds (boWindow backoff), validationWindowSeconds maximumWindow, readMaybe (T.unpack multiplierText) :: Maybe Double) of- (Just initialSeconds, Just maximumSeconds, Just multiplier)- | initialSeconds > 0 && maximumSeconds >= initialSeconds && multiplier >= 1 -> Nothing- _ -> Just "exponential backoff; initial must be positive, max must be at least initial, and multiplier must be at least 1"- _ -> Just "exponential backoff; both max and multiplier are required"- other -> Just ("backoff kind '" <> other <> "'; expected constant or exponential")--validationWindowSeconds :: Text -> Maybe Int-validationWindowSeconds window = case T.unsnoc window of- Just (digits, unit) -> do- amount <- readMaybe (T.unpack digits)- case unit of- 's' -> Just amount- 'm' -> Just (amount * 60)- 'h' -> Just (amount * 3600)- _ -> Nothing- Nothing -> Nothing---- | EP-4 inbox disposition rules: the table must be complete over the seven--- outcomes, and the three dangerous inversions must be stated the safe way.-validateIntake :: EffectiveLanguageContract -> IntakeNode -> [Diagnostic]-validateIntake languageContract i = concat [completeness, duplicateRows, inversions, dedupeVocabulary, decodeVersionFloor, envelopeVocabulary]- where- il = locLine (inkLoc i)- rows = inkDisposition i- requiredOutcomes =- ["processed", "duplicate", "inProgress", "previouslyFailed", "decodeFailed", "dedupeFailed", "storeFailed"]- completeness =- [ mkErr il DispositionIncomplete $- "intake '" <> inkName i <> "' disposition table is missing outcome '" <> o <> "'"- | o <- requiredOutcomes,- o `notElem` map drOutcome rows- ]- duplicateRows =- [ mkErr (locLine (drLoc row)) DispositionDuplicateOutcome $- "intake '" <> inkName i <> "' repeats disposition outcome '" <> drOutcome row <> "'; the first row would shadow this row"- | row <- duplicatesBy drOutcome rows- ]- dedupeVocabulary =- [ mkErr il IntakeDedupePolicyUnknown $- "intake '"- <> inkName i- <> "' has unknown dedupe policy '"- <> inkDedupePolicy i- <> "'; expected PreferIntegrationMessageId, PreferSourceEventIdentity, or KafkaDeliveryIdentity"- | inkDedupePolicy i `Set.notMember` intakeDedupePolicies- ]- decodeVersionFloor =- [ mkErr il IntakeDecodeSchemaVersionBelowMinimum $- "intake '" <> inkName i <> "' decode schemaVersion must be at least 1"- | enforcesSpecSurfaceClosures languageContract,- decBodySchemaVersion (inkDecode i) < 1- ]- envelopeVocabulary =- [ mkErr il IntakeEnvelopePolicyUnknown $- "intake '" <> inkName i <> "' has unsupported envelope policy " <> T.pack (show (decEnvelope (inkDecode i))) <> "; expected \"strict-required lenient-optional\""- | enforcesSpecSurfaceClosures languageContract,- decEnvelope (inkDecode i) /= "strict-required lenient-optional"- ]- firstRow outcome = case [row | row <- rows, drOutcome row == outcome] of- (row : _) -> Just row- [] -> Nothing- isRetry row = case drAction row of IRetry _ -> True; _ -> False- inversions =- [ mkErr (locLine (drLoc row)) DispositionDuplicateRetry $- "intake '" <> inkName i <> "': a 'duplicate' redelivery must be ackOk (success), not retry"- | Just row <- [firstRow "duplicate"],- isRetry row- ]- ++ [ mkErr (locLine (drLoc row)) DispositionPreviouslyFailedRetry $- "intake '" <> inkName i <> "': 'previouslyFailed' must dead-letter, not retry (a prior failure won't succeed on replay)"- | Just row <- [firstRow "previouslyFailed"],- isRetry row- ]- ++ [ mkErr (locLine (drLoc row)) DispositionDecodeUnboundedRetry $- "intake '" <> inkName i <> "': 'decodeFailed' must dead-letter (terminal), not retry unboundedly"- | Just row <- [firstRow "decodeFailed"],- isRetry row- ]--intakeDedupePolicies :: Set Name-intakeDedupePolicies = Set.fromList ["PreferIntegrationMessageId", "PreferSourceEventIdentity", "KafkaDeliveryIdentity"]--canonicalIntakeEnvelopeFields :: Set Name-canonicalIntakeEnvelopeFields =- Set.fromList- [ "messageId",- "source",- "destination",- "key",- "eventType",- "schemaVersion",- "contentType",- "schemaReference",- "sourceEventId",- "sourceGlobalPosition",- "payloadBytes",- "occurredAt",- "causationId",- "correlationId",- "traceContext",- "attributes",- "idempotencyKey"- ]---- | EP-3 rules for a process manager + its nested timer.-validateProcess :: Spec -> ProcessNode -> [Diagnostic]-validateProcess spec p =- concat [sagaCategoryRule, noWallClock, runtimeOwnedDispatchId, crossNodeCoupling, timerCeiling, policyRules, ambiguityRule, benignInversions]- where- aggregates = [a | NAggregate a <- specNodes spec]- aggNames = map aggName aggregates- projectionTables = [projTable projection | aggregate <- aggregates, Just projection <- [aggProjection aggregate]]- inputFields = map fieldName (inFields (procInput p))- timeFields = [fieldName f | f <- inFields (procInput p), fieldType f == Just "Time"]- timer = procTimer p- pl = locLine (procLoc p)-- sagaCategoryRule =- [ mkErr pl SagaCategoryIllegal $- "saga category " <> T.pack (show (sagaCategory (procSaga p))) <> " " <> reason- | Just reason <- [sagaCategoryError (sagaCategory (procSaga p))]- ]-- -- TIME IS INJECTED, NOT SAMPLED: fireAt's field must be a declared :Time- -- input field. (FireAtExpr has no clock-sampling constructor, so this is a- -- field-resolution + typed-as-Time check.)- noWallClock =- let f = faField (tmFireAt timer)- in if f `notElem` inputFields- then- [ mkErr (locLine (tmLoc timer)) ProcessFireAtNotInjected $- "timer '" <> tmName timer <> "' fireAt field '" <> f <> "' is not a field of input '" <> inName (procInput p) <> "'"- ]- else- [ mkErr (locLine (tmLoc timer)) ProcessFireAtNotInjected $- "timer '" <> tmName timer <> "' fireAt references '" <> f <> "', which is not a declared :Time field of input '" <> inName (procInput p) <> "'"- | f `notElem` timeFields- ]-- -- Dispatched (and fired) command ids are runtime-owned; no field binding may- -- supply a commandId/id.- runtimeOwnedDispatchId =- [ mkErr pl ProcessDispatchIdSupplied $- "advance command '" <> advCommand advance <> "' supplies a runtime-owned id field '" <> fbName binding <> "'; remove it"- | let advance = hAdvance (procHandle p),- binding <- advFields advance,- fbName binding `elem` (["commandId", "id"] :: [Name])- ]- ++ [ mkErr (locLine (dispLoc d)) ProcessDispatchIdSupplied $- "dispatch to '" <> dispTarget d <> "' supplies a runtime-owned id field '" <> fbName b <> "'; remove it"- | d <- hDispatch (procHandle p),- b <- dispFields d,- fbName b `elem` (["commandId", "id"] :: [Name])- ]- ++ [ mkErr (locLine (tmLoc timer)) ProcessDispatchIdSupplied $- "timer fire supplies a runtime-owned id field '" <> fbName b <> "'; remove it"- | b <- fireFields (tmFire timer),- fbName b `elem` (["commandId", "id"] :: [Name])- ]-- -- Aggregate, command, field, timer, and projection references must resolve.- crossNodeCoupling =- [ mkErr pl ProcessUnresolvedRef ("saga '" <> sagaAgg (procSaga p) <> "' does not resolve to a declared aggregate")- | sagaAgg (procSaga p) `notElem` aggNames- ]- ++ [ mkErr pl ProcessUnresolvedRef ("target '" <> procTarget p <> "' does not resolve to a declared aggregate")- | procTarget p `notElem` aggNames- ]- ++ [ mkErr (locLine (tmLoc timer)) ProcessUnresolvedRef ("timer fire target '" <> fireTarget (tmFire timer) <> "' must be the saga or the target aggregate")- | fireTarget (tmFire timer) `notElem` [sagaAgg (procSaga p), procTarget p]- ]- ++ resolveCommand pl "advance" (sagaAgg (procSaga p)) (advCommand advance) (advFields advance)- ++ concatMap resolveDispatch (hDispatch (procHandle p))- ++ resolveCommand (locLine (tmLoc timer)) "timer fire" (fireTarget fire) (fireCommand fire) (fireFields fire)- ++ [ mkErr pl ProcessUnresolvedRef $- "process '" <> procId p <> "' schedules undeclared timer '" <> hSchedule (procHandle p) <> "'; declared timer is '" <> tmName timer <> "'"- | hSchedule (procHandle p) /= tmName timer- ]- ++ [ mkErr pl ProcessUnresolvedRef $- "process '" <> procId p <> "' references undeclared projection table '" <> projection <> "'"- | projection <- procProjections p,- projection `notElem` projectionTables- ]- where- advance = hAdvance (procHandle p)- fire = tmFire timer- resolveDispatch dispatch =- resolveCommand- (locLine (dispLoc dispatch))- "dispatch"- (dispTarget dispatch)- (dispCommand dispatch)- (dispFields dispatch)- resolveCommand diagnosticLine context target command bindings = case lookupAggregate target of- Nothing -> []- Just aggregate -> case [decl | decl <- aggCommands aggregate, cmdName decl == command] of- [] ->- [ mkErr diagnosticLine ProcessUnresolvedRef $- context <> " command '" <> command <> "' is not declared by aggregate '" <> target <> "'"- ]- (declaration : _) ->- [ mkErr diagnosticLine ProcessFieldBindingUnresolved $- context <> " command '" <> command <> "' binds undeclared target field '" <> fbName binding <> "'"- | binding <- bindings,- fbName binding `notElem` map aggregateFieldName (cmdFields declaration)- ]- lookupAggregate name = case [aggregate | aggregate <- aggregates, aggName aggregate == name] of- (aggregate : _) -> Just aggregate- [] -> Nothing-- timerCeiling =- [ mkErr (locLine (tmLoc timer)) ProcessTimerCeilingInvalid $- "timer '" <> tmName timer <> "' max-attempts must be at least 1"- | tmMaxAttempts timer < 1- ]-- policyRules =- policyConsistency- (procId p)- (procLoc p)- (procRejected p)- [ (dispCommand dispatch, dispLoc dispatch, dispDisposition dispatch)- | dispatch <- hDispatch (procHandle p)- ]-- ambiguityRule =- [ mkErr (locLine (tmLoc timer)) AmbiguousMarkedBenign $- "timer '" <> tmName timer <> "' maps on-ambiguous => Fired; CommandAmbiguous means multiple aggregate edges matched and is never a benign success. Use on-ambiguous Retry so the attempts ceiling dead-letters the definition bug"- | onAmbiguous (fireDisposition (tmFire timer)) == OFired- ]-- -- Surface the dangerous benign inversions the author confirmed (warnings).- benignInversions =- [ Diagnostic (locLine (tmLoc timer)) Warning ProcessBenignInversion $- "timer '" <> tmName timer <> "' maps on-reject => Fired (a CommandRejected is treated as benign success)"- | onReject (fireDisposition (tmFire timer)) == OFired- ]- ++ [ Diagnostic (locLine (dispLoc d)) Warning ProcessBenignInversion $- "dispatch to '" <> dispTarget d <> "' maps on-duplicate => AckOk (a duplicate is treated as benign success)"- | d <- hDispatch (procHandle p),- onDuplicate (dispDisposition d) == DAckOk- ]---- | Explain why a process saga category is illegal. The first four cases--- mirror 'Keiro.Stream.category' without introducing a runtime dependency into--- the toolchain library. The final @:@ case is deliberately stricter because--- that prefix is reserved for the @wf:<name>@ workflow stream family.-sagaCategoryError :: Text -> Maybe Text-sagaCategoryError = runtimeIdentityError False--stableIdentityError :: Text -> Maybe Text-stableIdentityError = runtimeIdentityError True--runtimeIdentityError :: Bool -> Text -> Maybe Text-runtimeIdentityError allowsHyphen identity- | T.null identity = Just "is empty; use a non-empty stable name"- | identity == "$all" = Just "is reserved by the event store; choose a service-owned stable name"- | not allowsHyphen && T.isInfixOf "-" identity = Just "contains '-' (kiroku's category/id boundary); write compound categories in camelCase, for example \"hospitalSurge\""- | Just illegal <- T.find (\character -> isSpace character || isControl character) identity =- Just ("contains whitespace or control character " <> T.pack (show illegal) <> "; remove it and use camelCase")- | T.isInfixOf ":" identity = Just "contains ':' which is reserved for runtime stream-family prefixes; choose a stable name without ':'"- | otherwise = Nothing---- | EP-108 rules for a stateless content-based router.-validateRouter :: Spec -> RouterNode -> [Diagnostic]-validateRouter spec router =- concat- [ references,- keyField,- bindingScope,- commandReference,- readModelReference,- policyRules,- duplicateNotice- ]- where- aggregates = [aggregate | NAggregate aggregate <- specNodes spec]- readModels = [readModel | NReadModel readModel <- specNodes spec]- inputFields = map fieldName (inFields (rtInput router))- resolvedFields = rvRow (rtResolve router)- dispatch = rtDispatch router- routerLine = locLine (rtLoc router)- dispatchLine = locLine (rdLoc dispatch)-- targetAggregate = case [aggregate | aggregate <- aggregates, aggName aggregate == rtTarget router] of- aggregate : _ -> Just aggregate- [] -> Nothing-- projectionTables = [projTable projection | aggregate <- aggregates, Just projection <- [aggProjection aggregate]]-- references =- [ mkErr routerLine RouterUnresolvedRef $- "router '" <> rtId router <> "' targets aggregate '" <> rtTarget router <> "' but no such aggregate is declared"- | targetAggregate == Nothing- ]- ++ [ mkErr routerLine RouterUnresolvedRef $- "router '" <> rtId router <> "' references undeclared projection table '" <> projection <> "'"- | projection <- rtProjections router,- projection `notElem` projectionTables- ]-- keyField =- [ mkErr routerLine RouterKeyFieldUnknown $- "key references 'input." <> corrField (rtKey router) <> "' but input '" <> inName (rtInput router) <> "' does not declare that field"- | corrField (rtKey router) `notElem` inputFields- ]-- bindingScope =- [ mkErr dispatchLine RouterBindingUnscoped $- "dispatch binding '" <> fbName binding <> maybe "" ("=" <>) (fbValue binding) <> "' is outside the router input and resolve-row scopes"- | binding <- rdFields dispatch,- not (bindingInScope binding)- ]- where- bindingInScope binding = case fbValue binding of- Nothing -> fbName binding `elem` inputFields- Just value- | isQuoted value -> True- | Just field <- T.stripPrefix "input." value -> field `elem` inputFields- | Just field <- T.stripPrefix "resolved." value -> field `elem` resolvedFields- | otherwise -> False- isQuoted value = T.length value >= 2 && T.head value == '"' && T.last value == '"'-- commandReference = case targetAggregate of- Nothing -> []- Just aggregate -> case [command | command <- aggCommands aggregate, cmdName command == rdCommand dispatch] of- [] ->- [ mkErr dispatchLine RouterCommandUnknown $- "dispatch command '" <> rdCommand dispatch <> "' is not declared by aggregate '" <> aggName aggregate <> "'"- ]- command : _ ->- [ mkErr dispatchLine RouterCommandUnknown $- "dispatch command '" <> rdCommand dispatch <> "' binds undeclared target field '" <> fbName binding <> "'"- | binding <- rdFields dispatch,- fbName binding `notElem` map aggregateFieldName (cmdFields command)- ]-- readModelReference = case rvSource (rtResolve router) of- ResolveHole -> []- ResolveReadModel name ->- [ mkErr (locLine (rvLoc (rtResolve router))) RouterUnresolvedRef $- "router '" <> rtId router <> "' resolve names readmodel '" <> name <> "' but no such readmodel node is declared"- | name `notElem` map rmName readModels- ]-- policyRules =- policyConsistency- (rtId router)- (rtLoc router)- (rtRejected router)- [(rdCommand dispatch, rdLoc dispatch, rdDisposition dispatch)]-- duplicateNotice =- [ Diagnostic dispatchLine Warning ProcessBenignInversion $- "router dispatch '" <> rdCommand dispatch <> "' maps on-duplicate => AckOk; Keiro.Router confirms the event id against the target stream before treating the duplicate as benign"- | onDuplicate (rdDisposition dispatch) == DAckOk- ]---- | Reconcile per-dispatch prose with the one node-level policy the runtime--- actually applies to a rejection-class failure group.-policyConsistency :: Name -> Loc -> PolicyChoice -> [(Name, Loc, DispatchDisposition)] -> [Diagnostic]-policyConsistency nodeName nodeLoc rejectedPolicy dispatches = contradictions ++ divergent ++ unused ++ ambiguityWarning- where- contradictions =- [ mkErr (locLine dispatchLoc) PolicyContradiction $- "dispatch '" <> command <> "' declares on-failed DeadLetter, but node '" <> nodeName <> "' does not declare rejected => deadLetter; align the dispatch story with the node-level RejectedCommandPolicy"- | (command, dispatchLoc, disposition) <- dispatches,- DDeadLetter _ <- [onFailed disposition],- rejectedPolicy /= PolDeadLetter- ]-- divergent = case dispatches of- [] -> []- (_, _, firstDisposition) : rest ->- [ mkErr (locLine dispatchLoc) PolicyContradiction $- "dispatch '" <> command <> "' has a different on-failed action from another dispatch in node '" <> nodeName <> "'; the runtime applies one RejectedCommandPolicy to the whole failure group"- | (command, dispatchLoc, disposition) <- rest,- not (sameFailureAction (onFailed disposition) (onFailed firstDisposition))- ]-- unused =- [ Diagnostic (locLine nodeLoc) Warning PolicyDeadLetterUnused $- "node '" <> nodeName <> "' declares rejected => deadLetter but no dispatch on-failed arm says DeadLetter; the runtime policy is live, but the per-dispatch notation does not acknowledge it"- | rejectedPolicy == PolDeadLetter,- all (not . isDeadLetter . onFailed . third) dispatches- ]-- ambiguityWarning =- [ Diagnostic (locLine nodeLoc) Warning AmbiguousFollowsRejectedPolicy $- "node '" <> nodeName <> "' acknowledges rejection-class failures; CommandAmbiguous follows the same rejected policy, and a dead-letter errorClass is the durable witness of that definition bug"- | rejectedPolicy `elem` [PolDeadLetter, PolSkip]- ]-- third (_, _, value) = value- sameFailureAction DDeadLetter {} DDeadLetter {} = True- sameFailureAction left right = left == right- isDeadLetter DDeadLetter {} = True- isDeadLetter _ = False--validateAggregate :: EffectiveLanguageContract -> Spec -> Aggregate -> [Diagnostic]-validateAggregate languageContract spec agg =- concat- [ duplicateMembers,- declaredRefs,- eventBodyRefs,- outputMappingRules,- registerInitialScope,- reachability,- terminalNoOutgoing,- guardScope,- clockFree,- projectionSafety,- statusMapTotality,- evolutionRules,- snapshotRules,- replayOnlyRules,- eventlessStateChangeRules,- wirePolicyRules- ]- where- states = Set.fromList (map stName (aggStates agg))- terminals = Set.fromList [stName s | s <- aggStates agg, stTerminal s]- commandFields :: Map Name [Name]- commandFields = Map.fromList [(cmdName c, map aggregateFieldName (cmdFields c)) | c <- aggCommands agg]- commandNames = Map.keysSet commandFields- eventNames = Set.fromList (map evName (aggEvents agg))- enumCtorNames = Set.fromList [c | e <- specEnums spec, (c, _) <- enumCtors e]- ruleNames = Set.fromList (map ruleName (specRules spec))- registerNames = Set.fromList (map regName (aggRegs agg))-- snapshotRules = case aggSnapshot agg of- Nothing -> []- Just snapshot ->- [ mkErr (locLine (snapLoc snapshot)) SnapshotIntervalInvalid $- "aggregate '" <> aggName agg <> "': snapshot every requires an interval of at least 1; non-positive runtime intervals silently disable snapshots"- | SnapEvery interval <- [snapPolicy snapshot],- interval < 1- ]- ++ [ mkErr (locLine (snapLoc snapshot)) SnapshotCodecFixtureInvalid $- "aggregate '" <> aggName agg <> "': snapshot state-codec version must be at least 1 and shape-hash must be non-empty"- | snapCodecVersion snapshot < 1 || T.null (snapShapeHash snapshot)- ]-- wirePolicyRules = case aggWire agg of- Just wire- | enforcesSpecSurfaceClosures languageContract,- wireKind wire /= "ctorName" || wireFields wire /= "camelCase" ->- [ mkErr (locLine (aggLoc agg)) WireClauseUnsupported $- "aggregate '"- <> aggName agg- <> "' wire clause describes kind="- <> wireKind wire- <> " fields="- <> wireFields wire- <> "; generated bytes currently support only kind=ctorName fields=camelCase"- ]- _ -> []-- duplicateMembers =- [ mkErr (locLine (cmdLoc c)) DuplicateCommandName $- "aggregate '" <> aggName agg <> "' declares command '" <> cmdName c <> "' more than once"- | c <- duplicatesBy cmdName (aggCommands agg)- ]- ++ [ mkErr (locLine (evLoc e)) DuplicateEventName $- "aggregate '" <> aggName agg <> "' declares event '" <> evName e <> "' more than once"- | e <- duplicatesBy evName (aggEvents agg)- ]- ++ [ mkErr (locLine (aggregateFieldLoc field)) AggregateDuplicateFieldName $- "aggregate '" <> aggName agg <> "' command '" <> cmdName command <> "' declares field '" <> aggregateFieldName field <> "' more than once"- | command <- aggCommands agg,- field <- duplicatesBy aggregateFieldName (cmdFields command)- ]- ++ [ mkErr (locLine (aggregateFieldLoc field)) AggregateDuplicateFieldName $- "aggregate '" <> aggName agg <> "' event '" <> evName event <> "' declares field '" <> aggregateFieldName field <> "' more than once"- | event <- aggEvents agg,- EventFields fields <- [evBody event],- field <- duplicatesBy aggregateFieldName fields- ]- ++ [ mkErr (locLine (stLoc state)) AggregateDuplicateState $- "aggregate '" <> aggName agg <> "' declares state '" <> stName state <> "' more than once"- | state <- duplicatesBy stName (aggStates agg)- ]- ++ [ mkErr (locLine (regLoc register)) AggregateDuplicateRegister $- "aggregate '" <> aggName agg <> "' declares register '" <> regName register <> "' more than once"- | enforcesSpecSurfaceClosures languageContract,- register <- duplicatesBy regName (aggRegs agg)- ]- ++ [ mkErr (locLine (tLoc transition)) TransitionDuplicateUnguarded $- "aggregate '"- <> aggName agg- <> "' has more than one live unguarded transition for '"- <> tSource transition- <> " -- "- <> tCommand transition- <> "'; every matching command would be ambiguous"- | group <- duplicateGroupsBy transitionKey unguardedTransitions,- transition <- group- ]- ++ [ mkErr (locLine (tLoc guarded)) TransitionUnguardedSibling $- "aggregate '"- <> aggName agg- <> "' guarded transition '"- <> tSource guarded- <> " -- "- <> tCommand guarded- <> "' overlaps an unguarded sibling at line "- <> tInt (locLine (tLoc unguarded))- | enforcesSpecSurfaceClosures languageContract,- guarded <- liveTransitions,- tGuard guarded /= Nothing,- unguarded : _ <- [[candidate | candidate <- unguardedTransitions, transitionKey candidate == transitionKey guarded]]- ]- liveTransitions = [transition | transition <- aggTransitions agg, tMode transition == TmLive]- unguardedTransitions = [transition | transition <- liveTransitions, tGuard transition == Nothing]- transitionKey transition = (tSource transition, tCommand transition)-- eventBodyRefs =- [ mkErr (locLine (evLoc e)) UndeclaredCommand $- "event '" <> evName e <> "' copies fields from undeclared command '" <> command <> "'"- | e <- aggEvents agg,- EventFromCommand command <- [evBody e],- command `Set.notMember` commandNames- ]-- outputMappingRules =- [ mkErr (locLine (tLoc transition)) EventOutputCommandMismatch $- "transition '"- <> tSource transition- <> " -- "- <> consuming- <> "' emits event '"- <> eventName- <> "' declared as fields("- <> declared- <> "); generated identity output is legal only when the transition consumes that same command"- | transition <- aggTransitions agg,- (emitIndex, eventName) <- zip [1 ..] (tEmits transition),- Left OutputCommandMismatch {declaredSourceCommand = declared, consumingTransitionCommand = consuming} <- [eventOutputMapping spec agg transition emitIndex eventName]- ]-- registerInitialScope = concatMap checkRegisterInitial (aggRegs agg)- checkRegisterInitial r = case [e | e <- specEnums spec, TRef (enumName e) == regType r] of- (e : _) -> case enumBinding e of- Just _ ->- [ outOfScope r "declaration-owned symbol selected by" "initial"- | regInitialBare r /= Just "initial"- ]- Nothing ->- [ outOfScope r "constructor of enum" (enumName e)- | regInitialBare r `notElem` map (Just . fst) (enumCtors e)- ]- []- | regType r == TRef (aggName agg <> "Vertex") ->- [ outOfScope r "state of aggregate" (aggName agg)- | maybe True (`Set.notMember` states) (regInitialBare r)- ]- | Just identifier <- firstMatching (\declaration -> regType r == TRef (idName declaration)) (specIds spec) -> case idBinding identifier of- Just _ ->- [ outOfScope r "declaration-owned symbol selected by" "initial"- | regInitialBare r /= Just "initial"- ]- Nothing ->- [ outOfScope r "literal" "placeholder"- | regInitialBare r /= Just "placeholder"- ]- | otherwise -> []- outOfScope r expected domain =- mkErr (locLine (regLoc r)) RegisterInitialOutOfScope $- "register '" <> regName r <> "' initial '" <> renderRegInitial (regInitial r) <> "' is not a " <> expected <> " '" <> domain <> "'"- regInitialBare r = case regInitial r of- RegInitBare value -> Just value- RegInitText _ -> Nothing- renderRegInitial = \case- RegInitBare value -> value- RegInitText value -> value-- -- Rule 1: declared-reference for command / emit / goto / source.- declaredRefs =- concatMap transitionRefs (aggTransitions agg)- transitionRefs t =- [ mkErr (locLine (tLoc t)) UndeclaredCommand $- "transition references undeclared command '" <> tCommand t <> "'"- | not (tCommand t `Set.member` commandNames)- ]- ++ [ mkErr (locLine (tLoc t)) UndeclaredState $- "transition source '" <> tSource t <> "' is not a declared state"- | not (tSource t `Set.member` states)- ]- ++ [ mkErr (locLine (tLoc t)) UndeclaredState $- "transition goto '" <> tGoto t <> "' is not a declared state"- | not (tGoto t `Set.member` states)- ]- ++ [ mkErr (locLine (tLoc t)) UndeclaredEvent $- "emit references undeclared event '" <> ev <> "'"- | ev <- tEmits t,- not (ev `Set.member` eventNames)- ]-- -- Rule 2: reachability of every non-terminal state from the initial state- -- (the first state in the list).- reachability = case map stName (aggStates agg) of- [] -> []- (initial : _) ->- let reached = bfs (Set.singleton initial) [initial]- in [ mkErr (locLine (stLoc s)) UnreachableState $- "state '" <> stName s <> "' is not reachable from the initial state '" <> initial <> "'"- | s <- aggStates agg,- not (stTerminal s),- not (stName s `Set.member` reached)- ]- edgesFrom src = [tGoto t | t <- aggTransitions agg, tSource t == src]- bfs seen [] = seen- bfs seen (x : xs) =- let nexts = [n | n <- edgesFrom x, not (n `Set.member` seen)]- in bfs (foldr Set.insert seen nexts) (xs ++ nexts)-- -- Rule 3: a terminal state has no outgoing transition.- terminalNoOutgoing =- [ mkErr (locLine (tLoc t)) TerminalHasOutgoing $- "terminal state '" <> tSource t <> "' has an outgoing transition"- | t <- aggTransitions agg,- tSource t `Set.member` terminals- ]-- -- Rule 4: every atom in a guard or write Expr resolves to a register, a- -- field of the transition's command, an enum constructor, a rule, or a bool.- guardScope = concatMap transitionScope (aggTransitions agg)- transitionScope t =- let inScope =- registerNames- `Set.union` Set.fromList (Map.findWithDefault [] (tCommand t) commandFields)- `Set.union` enumCtorNames- `Set.union` ruleNames- -- State names are constructors of the implicit vertex enum, so a- -- @write reservationState := Held@ references a state legitimately.- `Set.union` states- exprs = maybe [] pure (tGuard t) ++ map snd (tWrites t)- badAtoms =- [ n- | e <- exprs,- n <- exprNames e,- not (n `Set.member` clockAtoms), -- clock atoms reported separately- not (n `Set.member` inScope)- ]- badTargets = [target | (target, _) <- tWrites t, target `Set.notMember` registerNames]- in [ mkErr (locLine (tLoc t)) WriteTargetNotRegister $- "write target '" <> target <> "' is not a register of aggregate '" <> aggName agg <> "'"- | target <- dedup badTargets- ]- ++ [ mkErr (locLine (tLoc t)) GuardAtomOutOfScope $- "atom '" <> n <> "' in transition '" <> tSource t <> " -- " <> tCommand t <> "' resolves to no register, command field, enum constructor, or rule"- | n <- dedup badAtoms- ]-- -- Rule 5 (cross-cutting): no guard or write Expr samples a wall clock.- clockFree = concatMap transitionClock (aggTransitions agg)- transitionClock t =- let exprs = maybe [] pure (tGuard t) ++ map snd (tWrites t)- sampled = [n | e <- exprs, n <- exprNames e, n `Set.member` clockAtoms]- in [ mkErr (locLine (tLoc t)) ClockSampled $- "transition '" <> tSource t <> " -- " <> tCommand t <> "' samples the wall clock via '" <> n <> "'; time must be an injected input field, not sampled"- | n <- dedup sampled- ]-- -- EP-107: a projection references a first-class read model when one exists.- -- Legacy standalone projections remain legal, but are surfaced as warnings.- projectionSafety = case aggProjection agg of- Nothing -> []- Just projection -> case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == projTable projection] of- [] ->- [ mkErr (locLine (projLoc projection)) RmStrongInlineOnly $- "projection '" <> projTable projection <> "' declares consistency = Strong but has no readmodel node; a standalone projection is inline-only and has no subscription cursor"- | projConsistency projection == Just Strong- ]- ++ [ Diagnostic- { line = locLine (projLoc projection),- severity = Warning,- code = RmProjectionWithoutNode,- message = "projection '" <> projTable projection <> "' has no readmodel node; registration, schema identity, consistency, and rebuild helpers are unavailable"- }- ]- (readModel : _) ->- [ mkErr (locLine (projLoc projection)) RmConsistencyConflict $- "projection '" <> projTable projection <> "' declares consistency " <> T.pack (show projectionConsistency) <> " but its readmodel node declares " <> T.pack (show (rmConsistency readModel))- | Just projectionConsistency <- [projConsistency projection],- projectionConsistency /= rmConsistency readModel- ]-- -- Rule 6 (hole-kind 3, mapping): keys are exact event names, never suffixes;- -- duplicates and dangling keys are errors, and non-partial maps are total.- statusMapTotality = case aggProjection agg of- Nothing -> []- Just p ->- let evs = map evName (aggEvents agg)- pairs = maybe [] mapPairs (projStatusMap p)- keys = map fst pairs- partial = maybe False mapPartial (projStatusMap p)- uncovered = [event | event <- evs, event `notElem` keys]- dangling = [key | key <- keys, key `notElem` evs]- duplicateKeys = map fst (duplicatesBy fst pairs)- in [ mkErr (locLine (projLoc p)) StatusMapDanglingKey $- "projection '" <> projTable p <> "' status-map key '" <> key <> "' is not an event name of aggregate '" <> aggName agg <> "'"- | key <- dangling- ]- ++ [ mkErr (locLine (projLoc p)) StatusMapDuplicateKey $- "projection '" <> projTable p <> "' repeats status-map key '" <> key <> "'"- | key <- duplicateKeys- ]- ++ [ mkErr (locLine (projLoc p)) StatusMapNotTotal $- "projection '" <> projTable p <> "' status-map is not total over events {" <> T.intercalate ", " uncovered <> "}"- | not partial,- not (null evs),- not (null uncovered)- ]-- -- EP-2 evolution rules (single-spec; the diff path adds the cross-spec ones).- 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 =- [ mkErr (locLine (evLoc e)) EvtVersionMissingUpcaster $- "event '" <> evName e <> "' version " <> tInt (evVersion e) <> " has no 'upcast from v" <> tInt (evVersion e - 1) <> "' clause"- | e <- aggEvents agg,- evVersion e > 1,- 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` 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- | wireSchemaVersion w /= maxEventVersion ->- [ Diagnostic- { line = locLine (aggLoc agg),- severity = Warning,- code = WireSchemaVersionMismatch,- message =- "wire schemaVersion=" <> tInt (wireSchemaVersion w) <> " does not match the maximum event version " <> tInt maxEventVersion- }- ]- _ -> []-- -- 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))- ]-- eventlessStateChangeRules =- [ mkErr (locLine (tLoc transition)) AggregateEventlessStateChange $- "transition '"- <> tSource transition- <> " -- "- <> tCommand transition- <> "' emits no event but changes "- <> changeDescription transition- <> "; event-sourced state changes require persisted evidence, while a no-op must keep both vertex and registers unchanged"- | transition <- aggTransitions agg,- null (tEmits transition),- tSource transition /= tGoto transition || not (null (tWrites transition))- ]- changeDescription transition- | tSource transition /= tGoto transition && not (null (tWrites transition)) = "the target vertex and registers"- | tSource transition /= tGoto transition = "the target vertex"- | otherwise = "registers"---- | The validator's re-derivation of the live--- 'Keiro.PGMQ.Runtime.queueRef' trio: physical queue, dead-letter queue, and--- PGMQ backing table. Parity is pinned by the queue-runtime conformance suite.-derivedQueueTrio :: Text -> (Text, Text, Text)-derivedQueueTrio logical = (physical, physical <> "_dlq", "pgmq.q_" <> physical)- where- physical = physicalBase logical--physicalBase :: Text -> Text-physicalBase logical- | T.length base <= 43 && not ("_dlq" `T.isSuffixOf` base) = base- | otherwise = hashedBase logical base- where- base = sanitizeQueueName logical--sanitizeQueueName :: Text -> Text-sanitizeQueueName =- ensureLeadingLetter- . T.intercalate "_"- . filter (not . T.null)- . T.splitOn "_"- . T.map toLegal- . T.toLower- where- toLegal c- | (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' = c- | otherwise = '_'- ensureLeadingLetter value = case T.uncons value of- Nothing -> "q"- Just (c, _)- | c >= 'a' && c <= 'z' -> value- | otherwise -> T.cons 'q' value--hashedBase :: Text -> Text -> Text-hashedBase logical base = prefix <> "_" <> fnv1a64Hex logical- where- trimmedPrefix = T.dropWhileEnd (== '_') (T.take 26 base)- prefix- | T.null trimmedPrefix = "q"- | otherwise = trimmedPrefix--fnv1a64Hex :: Text -> Text-fnv1a64Hex logical = T.pack (replicate (16 - length rendered) '0' <> rendered)- where- rendered = showHex (T.foldl' step offset logical) ""- offset :: Word64- offset = 0xcbf29ce484222325- prime :: Word64- prime = 0x100000001b3- step hash character = (hash `xor` fromIntegral (ord character)) * prime--tInt :: Int -> Text-tInt = T.pack . show--mkErr :: Int -> DiagnosticCode -> Text -> Diagnostic-mkErr l c m = Diagnostic {line = l, severity = Error, code = c, message = m}+ DiagnosticOrigin (..),+ Diagnostic (..),+ diagnosticCodeText,+ diagnosticOrigin,+ parseDiagnosticCode,+ renderDiagnostic,+ minimumLanguageDiagnostics,+ runtimeTimerStatuses,+ canonicalEnvelopeHeaders,+ validateService,+ validateSpec,+ derivedQueueTrio,+ sagaCategoryError,+ nodeIdentity,+ )+where++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)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Data.TypeID qualified as TypeID+import Data.Word (Word64)+import Keiro.Dsl.AggregateType+import Keiro.Dsl.EventOutput+import Keiro.Dsl.Expression+import Keiro.Dsl.FieldIdentity+import Keiro.Dsl.Grammar+import Keiro.Dsl.HaskellName qualified as HaskellName+import Keiro.Dsl.IdDomain (contractIdDomainContractFor, idDomainContractFor)+import Keiro.Dsl.LanguageVersion (LanguageVersion, RuntimeCapability (..), SourceLanguage (..), effectiveLanguageVersion, languageVersionText, runtimeProfileHasCapability, sourceFormText)+import Keiro.Dsl.NominalType qualified as Nominal+import Keiro.Dsl.ReadModelShape (deriveShapeHash)+import Keiro.Dsl.RuntimePackage (isCabalPackageName)+import Keiro.Dsl.SemanticContract (CheckedService (..), EffectiveLanguageContract, effectiveRuntimeProfile, legacyCheckedService)+import Keiro.Dsl.TypeGraph+import Keiro.Integration.Event qualified as Event+import Numeric (showHex)+import Text.Read (readMaybe)++data Severity = Error | Warning+ deriving stock (Eq, Show)++diagnosticCodeText :: DiagnosticCode -> Text+diagnosticCodeText = T.pack . show++parseDiagnosticCode :: Text -> Maybe DiagnosticCode+parseDiagnosticCode raw =+ case [diagnosticCode | diagnosticCode <- [minBound .. maxBound], diagnosticCodeText diagnosticCode == raw] of+ diagnosticCode : _ -> Just diagnosticCode+ [] -> Nothing++-- | A machine-checkable code per rule, so tests match on the code, not prose.+data DiagnosticCode+ = UndeclaredCommand+ | UndeclaredEvent+ | UndeclaredState+ | UnreachableState+ | TerminalHasOutgoing+ | GuardAtomOutOfScope+ | StatusMapNotTotal+ | ClockSampled+ | -- 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+ | UpcasterChainGap+ | DeprecatedEventReplayHazard+ | EventRetirementInProgress+ | DeprecatedEventStillEmitted+ | WireSchemaVersionMismatch+ | EvtFieldAddedWithoutBump+ | EvtRemovedNotDeprecated+ | -- EP-3 (process manager + durable timer).+ ProcessFireAtNotInjected+ | ProcessDispatchIdSupplied+ | ProcessUnresolvedRef+ | ProcessBenignInversion+ | SagaCategoryIllegal+ | -- EP-4 (integration intake / inbox disposition).+ DispositionIncomplete+ | DispositionDuplicateRetry+ | DispositionPreviouslyFailedRetry+ | DispositionDecodeUnboundedRetry+ | -- EP-4 (integration coupling).+ EmitSkipMissing+ | EmitUnresolvedContract+ | PublisherUnresolvedEmit+ | IntakeUnresolvedContract+ | -- EP-5 (pgmq workqueue/dispatch).+ WqPhysicalDivergence+ | WqStoreFailureNotRetry+ | WqDecodeFailureNotDeadLetter+ | WqDlqWithoutCeiling+ | WqGroupKeyMissing+ | WqGroupKeyWithoutFifo+ | WqGroupKeyUnresolved+ | WqUnloggedDurability+ | WqPartitionSpecEmpty+ | SnapshotIntervalInvalid+ | SnapshotCodecFixtureInvalid+ | DispatchEnqueueUnresolved+ | -- EP-6 (workflow/operation).+ AwaitSignalMismatch+ | RunWorkflowUnresolved+ | WorkflowPatchDuplicate+ | WorkflowPatchIdInvalid+ | WorkflowContinueAsNewNotTerminal+ | -- Diff-only (cross-spec) decode and identity evolution rules.+ EvtFieldTypeChanged+ | EvtFieldRemovedSameVersion+ | EvtVersionDecreased+ | EnumCtorRemoved+ | EnumWireSpellingChanged+ | WireSpecChanged+ | ContractEventRemoved+ | ContractFieldChanged+ | ContractDiscriminatorChanged+ | ContractTopicChanged+ | ContractSchemaVersionDecreased+ | WqPayloadFieldChanged+ | ProcessInputChanged+ | WorkflowShapeChanged+ | WorkflowBodyChanged+ | WorkflowStableNameChanged+ | WorkflowPatchRemoved+ | WorkflowContinueSeedChanged+ | WqOrderingChanged+ | WqProvisionChanged+ | WqGroupKeyChanged+ | IdPrefixChanged+ | DedupeIdentityChanged+ | DerivedIdentityChanged+ | QueueIdentityChanged+ | TimerWindowChanged+ | EmitMappingChanged+ | DecodePostureChanged+ | IntakePersistenceChanged+ | ProjectionChanged+ | PublisherPolicyChanged+ | DispatchRetargeted+ | ContractSchemaVersionBumped+ | EventUndeprecated+ | -- EP-104 (validator soundness).+ WorkflowDuplicateLabel+ | WorkflowSleepDelayUnresolved+ | WorkflowIdFieldUnresolved+ | RuleDomainUnresolved+ | RuleNotTotal+ | RuleCaseUnknownCtor+ | ProcessFieldBindingUnresolved+ | ProcessTimerCeilingInvalid+ | OperationUnresolvedRef+ | AwaitSignalValueMismatch+ | WqDispositionIncomplete+ | DispositionDuplicateOutcome+ | TopicAffinityMismatch+ | StatusMapDanglingKey+ | StatusMapDuplicateKey+ | WriteTargetNotRegister+ | RegisterInitialOutOfScope+ | DuplicateNodeName+ | DuplicateEnumCtor+ | DuplicateEnumWire+ | DuplicateIdPrefix+ | DuplicateCommandName+ | DuplicateEventName+ | WqDlqDivergence+ | WqTableDivergence+ | DispatchDedupQueueUnresolved+ | DispatchDedupFieldUnresolved+ | -- EP-105 (notation integrity and scaffold-safe names).+ VertexCtorCollision+ | IdentUnsafeNormalization+ | GeneratedOccurrenceReserved+ | GeneratedOccurrenceCollision+ | -- EP-107 (first-class read models).+ RmShapeHashDrift+ | RmStrongInlineOnly+ | RmScopeWithoutStrong+ | RmUnknownColumnType+ | RmInlineFeedUnreferenced+ | RmConsistencyConflict+ | RmProjectionWithoutNode+ | QueryUnresolvedReadModel+ | QueryConsistencyInvalid+ | DispatchReadModelUnresolved+ | DispatchReadModelFieldUnknown+ | -- EP-107 diff-only read-model evolution rules.+ ReadModelVersionDecreased+ | ReadModelShapeChangedWithoutBump+ | ReadModelFeedChanged+ | ReadModelConsistencyWeakened+ | -- EP-108 (router and worker-policy surfaces).+ RouterUnresolvedRef+ | RouterKeyFieldUnknown+ | RouterBindingUnscoped+ | RouterCommandUnknown+ | RouterReadModelUnverified+ | PolicyContradiction+ | PolicyDeadLetterUnused+ | 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+ | -- 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+ | -- MasterPlan 26 / EP-153: whole-service composition refusals, emitted by+ -- "Keiro.Dsl.Workspace" when several @.keiro@ members are composed into+ -- one service graph. They live in this registry, not a parallel enum, so+ -- every gate stays correlatable by code (ADR 0004). Manifest syntax and+ -- structure errors deliberately have no code here: like a @.keiro@ parse+ -- error, they are refused before any graph exists to diagnose.+ WorkspaceMemberUnreadable+ | WorkspaceMemberParseFailed+ | WorkspaceContextMismatch+ | WorkspaceAuthorityConflict+ | WorkspaceDuplicateDeclaration+ | WorkspaceDuplicateNodeName+ | WorkspacePathCollision+ | -- MasterPlan 26 / EP-155: whole-workspace diff facts. These are+ -- advisory consumer-build obligations, distinct from wire evolution.+ OwnershipMoved+ | WorkspaceAuthorityChanged+ | -- EP-157: canonical aggregate type resolution and capabilities.+ AggregateTypeUnknown+ | AggregateTypeUnsupportedAtUse+ | AggregateRegisterInitialInvalid+ | AggregateGuardTypeMismatch+ | AggregateGuardCapabilityUnsupported+ | AggregateExpressionRootUnknown+ | AggregateExpressionRootAmbiguous+ | AggregateExpressionPathInvalid+ | AggregateExpressionPathUnsupported+ | AggregateExpressionLiteralNeedsType+ | AggregateExpressionLiteralInvalid+ | AggregateExpressionOperandTypeMismatch+ | AggregateExpressionOperatorUnsupported+ | AggregateExpressionBooleanRequired+ | AggregateExpressionGuardBoolRequired+ | AggregateExpressionWriteTargetUnknown+ | AggregateExpressionWriteTypeMismatch+ | AggregateTransitionOwnershipConflict+ | CollectionExpressionUnsupported+ | -- EP-160: append-only source-language composition and diff facts.+ WorkspaceLanguageVersionMismatch+ | SourceLanguageDeclarationChanged+ | -- EP-158: checked consumer-owned nominal IDs, enums, and scalars.+ NominalMissingIngredient+ | NominalInvalidHaskellSource+ | NominalInvalidQualifiedName+ | NominalInvalidIdentity+ | NominalInvalidIdPrefix+ | NominalUnsupportedRepresentation+ | NominalEmptyEnumRepresentation+ | NominalMissingInitialValue+ | NominalNameCollision+ | NominalBindingChanged+ | NominalFixturesChanged+ | NominalCanonicalTypeChanged+ | NominalInitialChanged+ | NominalRepresentationChanged+ | NominalIdDecoderTightened+ | -- ExecPlan 171 / IR-14: versioned prefix-bearing ID admission policy.+ IdDomainContractChanged+ | -- ExecPlan 159 / IR-13: @fields(Command)@ output authority.+ EventOutputCommandMismatch+ | AggregateEventlessStateChange+ | -- ExecPlan 178: language-4 integration contract TypeID admission.+ ContractInvalidTypeIdPrefix+ | ContractTypeIdDomainChanged+ | -- ExecPlan 180: accepted-but-unenforced spec surfaces.+ PublisherOrderingUnknown+ | PublisherBackoffInvalid+ | IntakeDedupePolicyUnknown+ | PublisherMaxAttemptsBelowMinimum+ | ContractSchemaVersionBelowMinimum+ | ReadModelVersionBelowMinimum+ | IntakeDecodeSchemaVersionBelowMinimum+ | AggregateDuplicateFieldName+ | ContractDuplicateFieldName+ | ContractFieldShadowsDiscriminator+ | TransitionDuplicateUnguarded+ | ContractDuplicateEvent+ | ContractDuplicateTopicAlias+ | AggregateDuplicateState+ | AggregateDuplicateRegister+ | NominalDuplicateDeclaration+ | EmitMapDuplicateCase+ | TransitionUnguardedSibling+ | RuntimeIdentityInvalid+ | RuntimeIdentityDuplicate+ | ContractTopicNameInvalid+ | ReadModelIdentifierInvalid+ | ReadModelDuplicateColumn+ | IntakeBindUnresolved+ | IntakeDedupeKeyUnresolved+ | IntakeEnvelopePolicyUnknown+ | IntakeDecodeSchemaVersionMismatch+ | ContractTopicAliasUnresolved+ | WireClauseUnsupported+ | -- ExecPlan 190: an unchanged semantic/external declaration now presents a+ -- different generated Haskell occurrence.+ GeneratedHaskellNameChanged+ | -- ExecPlan 192: resolved field wire identities are checked before lowering.+ FieldWireKeyCollision+ | FieldWireKeyInvalid+ | -- ExecPlan 192: changing an aggregate event field's resolved wire key+ -- changes the persisted event decode surface.+ EvtFieldWireKeyChanged+ | -- ExecPlan 193: a CI-required released language floor was not met.+ LanguageVersionBelowMinimum+ | -- ExecPlan 194: scaffold's empty-node refusals are reported by check at+ -- the owning declaration before planning begins.+ AggregateEmpty+ | ContractEmpty+ | -- ExecPlan 194: pure scaffold-planning gates share check's located,+ -- machine-readable diagnostic pipeline.+ GeneratedPathCollision+ | GeneratedImportCycle+ | BehaviorDerivationInvalid+ | ConformanceFactKeyCollision+ | GeneratedPlanningInvariantViolation+ | -- ExecPlan 197: accepted but currently inert spec surfaces are reported+ -- through the ordinary warning pipeline.+ IntakeBindFlagUnenforced+ | RmInlineSubscriptionIgnored+ | -- ExecPlan 197: process and router references close under language 4.+ ProcessKeyFieldUnknown+ | ProcessDispatchKeyUnresolved+ | ProcessBindingUnscoped+ | -- ExecPlan 197: remaining accepted surfaces close under language 4.+ WqPayloadTypeUnknown+ | WindowOutOfRange+ | TimerIdFieldNotCorrelation+ | AggProjectionKeyUnresolved+ | PublisherOutboxFieldUnresolved+ | RouterBenignInversion+ | -- ExecPlan 199: spellings the grammar accepts that no runtime implements.+ -- Each names one concrete runtime fact the declaration contradicts, warns on+ -- released languages below 4, and errors from language 4 on.+ DecodeBodyPostureUnsupported+ | DispatchOnAppendedUnsupported+ | TimerNotMineUnsupported+ | IntakeBindHeaderUnknown+ | -- ExecPlan 199: the surfaces ExecPlan 197 parked as descriptive-only, closed+ -- by checking the reference each one actually names.+ TimerDecodeStatusUnknown+ | TimerDeadLetterTextInvalid+ | PgmqFanoutFunctionInvalid+ deriving stock (Eq, Ord, Show, Enum, Bounded)++-- | Which command pipeline can actually produce a given 'DiagnosticCode'.+--+-- The registry exists so a CI warning policy cannot silently name a code the+-- selected command never emits. Before ExecPlan 199, @keiro-dsl check --deny+-- EvtFieldWireKeyChanged@ was accepted and then matched nothing forever,+-- because that code is only reachable from @diff@'s cross-revision comparison.+data DiagnosticOrigin+ = -- | Reachable from @check@ on a single spec or a workspace. This includes+ -- the pure scaffold-planning gates that @check@ replays, and it is the+ -- default for any code not positively classified below.+ CheckDiagnostic+ | -- | Reachable only from the structural-coverage pass, which runs only when+ -- @--coverage-report@ is supplied.+ CoverageDiagnostic+ | -- | Reachable only from @diff@, which compares two revisions of a spec.+ -- Nothing in a single-revision @check@ can produce these.+ DiffDiagnostic+ | -- | Reachable only from the generated codec-comparison path, which no+ -- @check@ or @diff@ invocation runs.+ CodecCompareDiagnostic+ deriving stock (Eq, Ord, Show, Enum, Bounded)++-- | Classify a code by the pipeline that emits it.+--+-- Only codes proven non-@check@ are listed; everything else falls through to+-- 'CheckDiagnostic'. The default is deliberately the permissive one: a+-- misclassification here would reject a working CI invocation, whereas falling+-- through merely preserves the pre-199 behavior of accepting the code.+diagnosticOrigin :: DiagnosticCode -> DiagnosticOrigin+diagnosticOrigin diagnosticCode = case diagnosticCode of+ -- Structural coverage, reachable from `check --coverage-report`.+ CoverageOpaqueSurface -> CoverageDiagnostic+ CoverageOpaqueGateExceeded -> CoverageDiagnostic+ -- Coverage delta, computed only against a previous revision.+ CoverageOpaqueBoundaryAdded -> DiffDiagnostic+ -- Generated codec comparison.+ CodecCompareDifference -> CodecCompareDiagnostic+ CodecCompareCoverageGap -> CodecCompareDiagnostic+ CodecCompareInvalidInput -> CodecCompareDiagnostic+ -- Cross-revision evolution facts.+ AggFoldSurfaceChanged -> DiffDiagnostic+ AggGuardTightened -> DiffDiagnostic+ CompatibilityStrengthened -> DiffDiagnostic+ ContractDiscriminatorChanged -> DiffDiagnostic+ ContractEventAdded -> DiffDiagnostic+ ContractEventRemoved -> DiffDiagnostic+ ContractFieldChanged -> DiffDiagnostic+ ContractSchemaVersionBumped -> DiffDiagnostic+ ContractSchemaVersionDecreased -> DiffDiagnostic+ ContractTopicAdded -> DiffDiagnostic+ ContractTopicChanged -> DiffDiagnostic+ ContractTypeIdDomainChanged -> DiffDiagnostic+ DeclarationAdded -> DiffDiagnostic+ DecodePostureChanged -> DiffDiagnostic+ DedupeIdentityChanged -> DiffDiagnostic+ DerivedIdentityChanged -> DiffDiagnostic+ DispatchRetargeted -> DiffDiagnostic+ EmitMappingChanged -> DiffDiagnostic+ EnumCtorAdded -> DiffDiagnostic+ EnumCtorRemoved -> DiffDiagnostic+ EnumWireSpellingChanged -> DiffDiagnostic+ EventRetirementAbandoned -> DiffDiagnostic+ EventUndeprecated -> DiffDiagnostic+ EvtFieldAddedWithoutBump -> DiffDiagnostic+ EvtFieldRemovedSameVersion -> DiffDiagnostic+ EvtFieldTypeChanged -> DiffDiagnostic+ EvtFieldWireKeyChanged -> DiffDiagnostic+ EvtRemovedNotDeprecated -> DiffDiagnostic+ EvtVersionDecreased -> DiffDiagnostic+ GeneratedHaskellNameChanged -> DiffDiagnostic+ IdDomainContractChanged -> DiffDiagnostic+ IdPrefixChanged -> DiffDiagnostic+ IntakePersistenceChanged -> DiffDiagnostic+ MappedArmAdded -> DiffDiagnostic+ MappedArmRemoved -> DiffDiagnostic+ MappedArmTagChanged -> DiffDiagnostic+ MappedBindingChanged -> DiffDiagnostic+ MappedCanonicalTypeChanged -> DiffDiagnostic+ MappedDeclAdded -> DiffDiagnostic+ MappedDeclRemoved -> DiffDiagnostic+ MappedDefaultChanged -> DiffDiagnostic+ MappedDefaultRemoved -> DiffDiagnostic+ MappedEnumSpellingChanged -> DiffDiagnostic+ MappedEnumValueAdded -> DiffDiagnostic+ MappedEnumValueRemoved -> DiffDiagnostic+ MappedFieldAddedNoDefault -> DiffDiagnostic+ MappedFieldAddedWithDefault -> DiffDiagnostic+ MappedFieldRemoved -> DiffDiagnostic+ MappedFieldTypeChanged -> DiffDiagnostic+ MappedFixturesChanged -> DiffDiagnostic+ MappedHaskellSourceChanged -> DiffDiagnostic+ MappedInitialChanged -> DiffDiagnostic+ MappedModeCrossed -> DiffDiagnostic+ MappedNullabilityChanged -> DiffDiagnostic+ MappedOpaqueCodecChanged -> DiffDiagnostic+ MappedPresenceChanged -> DiffDiagnostic+ MappedRecordConstructorChanged -> DiffDiagnostic+ MappedUnionEncodingChanged -> DiffDiagnostic+ MappedWireKeyChanged -> DiffDiagnostic+ NominalBindingChanged -> DiffDiagnostic+ NominalCanonicalTypeChanged -> DiffDiagnostic+ NominalFixturesChanged -> DiffDiagnostic+ NominalIdDecoderTightened -> DiffDiagnostic+ NominalInitialChanged -> DiffDiagnostic+ NominalRepresentationChanged -> DiffDiagnostic+ OwnershipMoved -> DiffDiagnostic+ ProcessDecideSurfaceChanged -> DiffDiagnostic+ ProcessInputChanged -> DiffDiagnostic+ ProcessTimerPayloadChanged -> DiffDiagnostic+ ProjectionChanged -> DiffDiagnostic+ PublisherPolicyChanged -> DiffDiagnostic+ QueueIdentityChanged -> DiffDiagnostic+ ReadModelConsistencyWeakened -> DiffDiagnostic+ ReadModelFeedChanged -> DiffDiagnostic+ ReadModelShapeChangedWithoutBump -> DiffDiagnostic+ ReadModelVersionDecreased -> DiffDiagnostic+ RouterDecideSurfaceChanged -> DiffDiagnostic+ RouterStableNameChanged -> DiffDiagnostic+ SourceLanguageDeclarationChanged -> DiffDiagnostic+ TimerWindowChanged -> DiffDiagnostic+ VersionBumped -> DiffDiagnostic+ WireSpecChanged -> DiffDiagnostic+ WorkflowBodyChanged -> DiffDiagnostic+ WorkflowContinueSeedChanged -> DiffDiagnostic+ WorkflowEvolutionGuardAdded -> DiffDiagnostic+ WorkflowPatchRemoved -> DiffDiagnostic+ WorkflowShapeChanged -> DiffDiagnostic+ WorkflowStableNameChanged -> DiffDiagnostic+ WorkspaceAuthorityChanged -> DiffDiagnostic+ WqGroupKeyChanged -> DiffDiagnostic+ WqOrderingChanged -> DiffDiagnostic+ WqPayloadFieldChanged -> DiffDiagnostic+ WqProvisionChanged -> DiffDiagnostic+ _ -> CheckDiagnostic++-- | A line-numbered, structured diagnostic.+data Diagnostic = Diagnostic+ { line :: !Int,+ severity :: !Severity,+ code :: !DiagnosticCode,+ relatedLocations :: ![(Int, Text)],+ message :: !Text+ }+ deriving stock (Eq, Show)++-- | Render a diagnostic in the conventional+-- @\<file\>:\<line\>: error[\<code\>]: \<message\>@ form.+renderDiagnostic :: FilePath -> Diagnostic -> Text+renderDiagnostic file d =+ T.intercalate "\n" (primary : notes)+ where+ primary =+ T.pack file+ <> ":"+ <> T.pack (show (line d))+ <> ": "+ <> sev+ <> "["+ <> T.pack (show (code d))+ <> "]: "+ <> message d+ notes =+ [ " " <> T.pack file <> ":" <> T.pack (show noteLine) <> ": note: " <> note+ | (noteLine, note) <- relatedLocations d+ ]+ sev = case severity d of Error -> "error"; Warning -> "warning"++-- | Error diagnostics produced when the effective source language is below a+-- CI-required released floor. A legacy source has no preamble, so line 1 is the+-- actionable location where one should be added.+minimumLanguageDiagnostics :: LanguageVersion -> SourceLanguage -> [Diagnostic]+minimumLanguageDiagnostics floorVersion sourceLanguage+ | effectiveVersion >= floorVersion = []+ | otherwise =+ [ Diagnostic+ { line = sourceLanguageLine sourceLanguage,+ severity = Error,+ code = LanguageVersionBelowMinimum,+ relatedLocations = [],+ message =+ "effective language version "+ <> languageVersionText effectiveVersion+ <> " ("+ <> sourceFormText sourceLanguage+ <> ") is below the required minimum "+ <> languageVersionText floorVersion+ <> "; declare `language keiro-dsl "+ <> languageVersionText floorVersion+ <> "`"+ }+ ]+ where+ effectiveVersion = effectiveLanguageVersion sourceLanguage+ sourceLanguageLine LegacyUnversioned = 1+ sourceLanguageLine DeclaredLanguage {languageVersionLoc = Loc lineNumber} = lineNumber++-- | Reserved wall-clock atom names. Sampling any of these inside a guard or+-- write breaks deterministic replay: TIME IS INJECTED, NOT SAMPLED.+clockAtoms :: Set Name+clockAtoms = Set.fromList ["now", "currentTime", "wallClock", "today", "utcNow"]++-- | Validate a whole service under its effective released semantic contract.+-- An empty list means valid. Current released versions share this policy, but+-- selecting it at this boundary prevents successor semantics from being lost.+validateService :: CheckedService -> [Diagnostic]+validateService service = validateCheckedSpec (checkedLanguageContract service) (checkedSpec service)++-- | Compatibility wrapper for callers that have only a normalized graph. It+-- explicitly selects legacy/version-1 semantics; production source/workspace+-- routes use 'validateService'.+validateSpec :: Spec -> [Diagnostic]+validateSpec = validateService . legacyCheckedService++validateCheckedSpec :: EffectiveLanguageContract -> Spec -> [Diagnostic]+validateCheckedSpec languageContract spec =+ sortOn line (validateNames languageContract spec ++ validateMapped spec ++ validateNominal languageContract spec ++ validateAggregateTypes spec ++ specLevelRules languageContract spec ++ concatMap (validateNode languageContract spec) (specNodes spec))++-- | Rules added before language 4 ships consult the effective semantic+-- contract, not the numeric source spelling. Versions 1 through 3 retain their+-- released acceptance; runtime semantics 3 is the unreleased tightening gate.+enforcesSpecSurfaceClosures :: EffectiveLanguageContract -> Bool+enforcesSpecSurfaceClosures languageContract =+ runtimeProfileHasCapability (effectiveRuntimeProfile languageContract) StrictSpecSurfaceValidation++validateNominal :: EffectiveLanguageContract -> Spec -> [Diagnostic]+validateNominal languageContract spec = domainErrors <> resolutionErrors+ where+ domainErrors =+ [ mkErr (locLine (idLoc declaration)) NominalInvalidIdPrefix $+ "id '" <> idName declaration <> "' has invalid TypeID prefix '" <> idPrefix declaration <> "': " <> T.pack (show reason)+ | declaration <- specIds spec,+ Just _ <- [idDomainContractFor languageContract (idPrefix declaration)],+ Just reason <- [TypeID.checkPrefix (idPrefix declaration)]+ ]+ resolutionErrors = case Nominal.resolveNominalTypes spec of+ Right _ -> []+ Left errors -> map nominalTypeDiagnostic (NE.toList errors)++nominalTypeDiagnostic :: Nominal.NominalTypeError -> Diagnostic+nominalTypeDiagnostic nominalError = case nominalError of+ Nominal.NominalMissingIngredient name loc ingredient ->+ problem loc NominalMissingIngredient $ "nominal declaration '" <> name <> "' is missing required " <> ingredient <> " provenance"+ Nominal.NominalInvalidHaskellSource name loc ingredient ->+ problem loc NominalInvalidHaskellSource $ "nominal declaration '" <> name <> "' has an invalid Haskell " <> ingredient <> " name"+ Nominal.NominalInvalidQualifiedValue name loc ingredient value ->+ problem loc NominalInvalidQualifiedName $+ "nominal declaration '" <> name <> "' has invalid " <> ingredient <> " symbol '" <> value <> "'; expected a module path plus a lower-initial value"+ Nominal.NominalInvalidIdentity name loc ingredient value ->+ problem loc NominalInvalidIdentity $ "nominal declaration '" <> name <> "' has invalid " <> ingredient <> " '" <> value <> "'"+ Nominal.NominalInvalidIdPrefix name loc prefix detail ->+ problem loc NominalInvalidIdPrefix $ "id '" <> name <> "' has invalid TypeID prefix '" <> prefix <> "': " <> detail+ Nominal.NominalUnsupportedScalar name loc representation ->+ problem loc NominalUnsupportedRepresentation $+ "nominal scalar '" <> name <> "' uses unsupported representation '" <> representation <> "'; supported representations are Text, Int, Natural, Bool, and Time"+ Nominal.NominalEmptyEnum name loc ->+ problem loc NominalEmptyEnumRepresentation $ "enum '" <> name <> "' must declare at least one closed representation constructor"+ Nominal.NominalMissingRegisterInitial name loc registerName ->+ problem loc NominalMissingInitialValue $+ "consumer-owned nominal type '" <> name <> "' is used by register '" <> registerName <> "' and must name an initial symbol"+ Nominal.NominalDeclarationCollision name loc categories ->+ problem loc NominalNameCollision $ "declaration name '" <> name <> "' collides across " <> T.intercalate ", " categories+ where+ problem loc diagnosticCode detail = mkErr (locLine loc) diagnosticCode (detail <> "; GHC and conformance validate consumer function bodies")++-- | Resolve every direct aggregate type once at the earliest semantic gate.+validateAggregateTypes :: Spec -> [Diagnostic]+validateAggregateTypes spec = case Nominal.resolveNominalTypes spec of+ Left _ -> []+ Right _ -> concatMap aggregateRules aggregates+ where+ symbols = aggregateSymbols spec+ aggregates = [aggregate | NAggregate aggregate <- specNodes spec]++ aggregateRules aggregate =+ concatMap commandRules (aggCommands aggregate)+ ++ concatMap eventRules (aggEvents aggregate)+ ++ concatMap registerRules (aggRegs aggregate)+ ++ concatMap (transitionRules aggregate) (aggTransitions aggregate)+ where+ commandRules command = concatMap (fieldRule aggregate CommandFieldUse) (cmdFields command)+ eventRules event = case evBody event of+ EventFields fields -> concatMap (fieldRule aggregate EventFieldUse) fields+ EventFromCommand _ -> []+ registerRules register = case resolveAggregateType symbols (regLoc register) RegisterUse (regType register) of+ Left typeError -> [aggregateTypeDiagnostic typeError]+ Right AggregateMapped {} -> []+ Right resolved -> case resolveRegisterInitial symbols (regLoc register) resolved (regInitial register) of+ Left initialError -> [aggregateTypeDiagnostic initialError]+ Right _ -> []++ fieldRule aggregate useSite field =+ either (pure . aggregateTypeDiagnostic) (const []) (inferAggregateFieldType symbols aggregate useSite field)++ transitionRules aggregate transition = case tImplementation transition of+ LegacyHoleImplementation ->+ concatMap (comparisonRule aggregate transition) (maybe [] comparisons (tGuard transition))+ GeneratedImplementation ->+ let environment = expressionEnvironment spec aggregate transition+ in maybe [] (expressionDiagnostics . resolveGuardExpr environment) (tGuard transition)+ ++ concatMap (expressionDiagnostics . uncurry (resolveWriteExpr environment)) (tWrites transition)+ HoleImplementation ->+ [ mkErr (locLine (tLoc transition)) AggregateTransitionOwnershipConflict $+ "transition '"+ <> tSource transition+ <> " -- "+ <> tCommand transition+ <> "' selects implementation hole and therefore cannot also declare guard or write clauses"+ | tGuard transition /= Nothing || not (null (tWrites transition))+ ]++ expressionDiagnostics = either (map expressionDiagnostic . NE.toList) (const [])++ expressionDiagnostic diagnostic =+ mkErr+ (locLine (expressionDiagnosticLoc diagnostic))+ (expressionCode (expressionDiagnosticCode diagnostic))+ (expressionDiagnosticMessage diagnostic)++ expressionCode = \case+ ScalarRootUnknown -> AggregateExpressionRootUnknown+ ScalarRootAmbiguous -> AggregateExpressionRootAmbiguous+ ScalarPathInvalid -> AggregateExpressionPathInvalid+ ScalarPathUnsupported -> AggregateExpressionPathUnsupported+ ScalarLiteralNeedsType -> AggregateExpressionLiteralNeedsType+ ScalarLiteralInvalid -> AggregateExpressionLiteralInvalid+ ScalarOperandTypeMismatch -> AggregateExpressionOperandTypeMismatch+ ScalarOperatorUnsupported -> AggregateExpressionOperatorUnsupported+ ScalarBooleanOperandRequired -> AggregateExpressionBooleanRequired+ ScalarGuardBoolRequired -> AggregateExpressionGuardBoolRequired+ ScalarWriteTargetUnknown -> AggregateExpressionWriteTargetUnknown+ ScalarWriteTypeMismatch -> AggregateExpressionWriteTypeMismatch++ comparisonRule aggregate transition (operator, left, right) =+ case (expressionType aggregate transition left, expressionType aggregate transition right) of+ (Right leftType, Right rightType)+ | leftType /= rightType ->+ [ mkErr (locLine (tLoc transition)) AggregateGuardTypeMismatch $+ "comparison operands have different aggregate types '"+ <> aggregateCanonicalName leftType+ <> "' and '"+ <> aggregateCanonicalName rightType+ <> "'"+ ]+ | aggregateCapability useSite leftType == Unsupported ->+ [ mkErr (locLine (tLoc transition)) AggregateGuardCapabilityUnsupported $+ renderAggregateUseSite useSite+ <> " is unsupported for aggregate type '"+ <> aggregateCanonicalName leftType+ <> "'"+ ]+ | otherwise -> []+ _ -> []+ where+ useSite = case operator of+ OpEq -> EqualityGuardUse+ OpNeq -> EqualityGuardUse+ OpLt -> OrderingGuardUse+ OpLe -> OrderingGuardUse+ OpGt -> OrderingGuardUse+ OpGe -> OrderingGuardUse++ expressionType aggregate transition expression = case expression of+ EAtom (ABool _) -> pure AggregateBool+ EAtom (AName name) -> atomType aggregate transition name+ EOr {} -> pure AggregateBool+ EAnd {} -> pure AggregateBool+ ECmp {} -> pure AggregateBool+ EAdd _ left _ -> expressionType aggregate transition left+ ESubtract _ left _ -> expressionType aggregate transition left+ EMultiply _ left _ -> expressionType aggregate transition left+ EPath loc _ path -> case path of+ name : _ -> atomType aggregate transition name+ [] -> Left (AggregateTypeError loc EqualityGuardUse (UnknownAggregateType "<empty-path>"))+ ELiteral _ literal -> case literal of+ LiteralBool {} -> pure AggregateBool+ LiteralText {} -> pure AggregateText+ LiteralIntegral {} -> Left (AggregateTypeError (exprLoc expression) EqualityGuardUse (UnknownAggregateType "<contextual-integral-literal>"))+ LiteralQualified typeName _ -> resolveAggregateType symbols (exprLoc expression) EqualityGuardUse (TRef typeName)+ LiteralId typeName _ -> resolveAggregateType symbols (exprLoc expression) EqualityGuardUse (TRef typeName)++ atomType aggregate transition name = case [register | register <- aggRegs aggregate, regName register == name] of+ register : _ -> resolveAggregateType symbols (regLoc register) RegisterUse (regType register)+ [] -> case [field | command <- aggCommands aggregate, cmdName command == tCommand transition, field <- cmdFields command, aggregateFieldName field == name] of+ field : _ -> inferAggregateFieldType symbols aggregate CommandFieldUse field+ [] -> case [enumName declaration | declaration <- specEnums spec, name `elem` map fst (enumCtors declaration)] of+ enumType : _ -> resolveAggregateType symbols (tLoc transition) CommandFieldUse (TRef enumType)+ []+ | name `elem` map stName (aggStates aggregate) -> pure (AggregateVertex (aggName aggregate <> "Vertex"))+ | Just rule <- firstMatching ((== name) . ruleName) (specRules spec) ->+ resolveAggregateType symbols (ruleLoc rule) EqualityGuardUse (nameTypeExpr (ruleCodomain rule))+ | otherwise -> Left (AggregateTypeError (tLoc transition) EqualityGuardUse (UnknownAggregateType name))++ nameTypeExpr name = case name of+ "Text" -> TText+ "Int" -> TInt+ "Bool" -> TBool+ "Natural" -> TNatural+ "Time" -> TTime+ "UTCTime" -> TTime+ "Json" -> TJson+ _ -> TRef name++ comparisons expression = case expression of+ EOr left right -> comparisons left <> comparisons right+ EAnd left right -> comparisons left <> comparisons right+ ECmp operator left right -> (operator, left, right) : comparisons left <> comparisons right+ EAdd _ left right -> comparisons left <> comparisons right+ ESubtract _ left right -> comparisons left <> comparisons right+ EMultiply _ left right -> comparisons left <> comparisons right+ EPath {} -> []+ ELiteral {} -> []+ EAtom {} -> []++aggregateTypeDiagnostic :: AggregateTypeError -> Diagnostic+aggregateTypeDiagnostic aggregateError =+ mkErr (locLine (aggregateTypeErrorLoc aggregateError)) diagnosticCode diagnosticMessage+ where+ diagnosticCode = case aggregateTypeErrorReason aggregateError of+ UnknownAggregateType {} -> AggregateTypeUnknown+ UnsupportedAggregateShape {} -> AggregateTypeUnsupportedAtUse+ UnsupportedAggregateCapability {} -> case aggregateTypeErrorUseSite aggregateError of+ EqualityGuardUse -> AggregateGuardCapabilityUnsupported+ OrderingGuardUse -> AggregateGuardCapabilityUnsupported+ _ -> AggregateTypeUnsupportedAtUse+ InvalidRegisterInitial {} -> AggregateRegisterInitialInvalid+ diagnosticMessage = case aggregateTypeErrorReason aggregateError of+ UnknownAggregateType name ->+ "unknown aggregate type '" <> name <> "' at " <> renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)+ UnsupportedAggregateShape expression ->+ "direct aggregate type '"+ <> typeExprCanonicalName expression+ <> "' is unsupported at "+ <> renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)+ <> "; use a mapped structural declaration for Json or container shapes"+ UnsupportedAggregateCapability resolved ->+ renderAggregateUseSite (aggregateTypeErrorUseSite aggregateError)+ <> " is unsupported for aggregate type '"+ <> aggregateCanonicalName resolved+ <> "'"+ InvalidRegisterInitial resolved detail ->+ "invalid " <> aggregateCanonicalName resolved <> " register initial: " <> detail++renderAggregateUseSite :: AggregateUseSite -> Text+renderAggregateUseSite useSite = case useSite of+ CommandFieldUse -> "command field"+ EventFieldUse -> "event field"+ RegisterUse -> "register"+ EqualityGuardUse -> "equality guard"+ OrderingGuardUse -> "ordering guard"+ WholeValueWriteUse -> "whole-value write"+ CodecUse -> "JSON codec"+ SnapshotUse -> "snapshot"+ HarnessSampleUse -> "harness sample"+ HaskellLoweringUse -> "Haskell lowering"++-- | 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 (isCabalPackageName (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+ 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,+ onInteger = 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,+ onInteger = 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 regType register of+ TRef typeName -> case Map.lookup (MappedKey typeName) (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 '" <> typeName <> "' 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+ }++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` HaskellName.haskellKeywords+ Nothing -> False++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++-- | Check every logical name before a renderer can turn it into Haskell. The+-- parser enforces the ASCII alphabet; 'HaskellName' owns word segmentation,+-- casing, keywords, and normalized collision keys.+validateNames :: EffectiveLanguageContract -> Spec -> [Diagnostic]+validateNames languageContract spec =+ concat+ [ concatMap idNames (specIds spec),+ concatMap enumNames (specEnums spec),+ concatMap nominalNames (specNominalScalars spec),+ concatMap nodeNames (specNodes spec),+ normalizedCollisions+ ]+ where+ idNames declaration =+ constructorName "id name" (idName declaration) (idLoc declaration)++ enumNames declaration =+ constructorName "enum name" (enumName declaration) (enumLoc declaration)+ ++ concatMap+ (\(ctor, _) -> constructorName ("constructor of enum '" <> enumName declaration <> "'") ctor (enumLoc declaration))+ (enumCtors declaration)++ nominalNames declaration =+ constructorName "nominal scalar name" (nominalScalarName declaration) (nominalScalarLoc declaration)++ nodeNames = \case+ NAggregate aggregate -> aggregateNames aggregate+ NProcess process -> processNames process+ NRouter router -> routerNames router+ NContract contract ->+ pascalizedNodeName "contract" (ctrName contract) (ctrLoc contract)+ ++ concatMap+ (\event -> constructorName "contract event name" (ceName event) (ctrLoc contract) ++ concatMap contractFieldName (ceFields event))+ (ctrEvents contract)+ NIntake intake -> pascalizedNodeName "intake" (inkName intake) (inkLoc intake)+ NEmit emitNode -> pascalizedNodeName "emit" (emName emitNode) (emLoc emitNode)+ NPublisher publisher -> pascalizedNodeName "publisher" (pubName publisher) (pubLoc publisher)+ NWorkqueue workqueue ->+ pascalizedNodeName "workqueue" (wqName workqueue) (wqLoc workqueue)+ ++ constructorName "workqueue payload name" (wqPayloadName workqueue) (wqLoc workqueue)+ ++ 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) (workflowNodeLoc workflow)+ NOperation _ -> []++ aggregateNames aggregate =+ constructorName "aggregate name" (aggName aggregate) (aggLoc aggregate)+ ++ concatMap+ (\register -> fieldNameRule "register name" (regName register) (regLoc register))+ (aggRegs aggregate)+ ++ concatMap commandNames (aggCommands aggregate)+ ++ concatMap eventNames (aggEvents aggregate)+ ++ maybe [] (\projection -> fieldNameRule "projection key" (projKey projection) (projLoc projection)) (aggProjection aggregate)+ ++ vertexCollisions aggregate+ where+ commandNames command =+ constructorName "command name" (cmdName command) (cmdLoc command)+ ++ concatMap (aggregateFieldNameRule "command field") (cmdFields command)+ eventNames event =+ constructorName "event name" (evName event) (evLoc event)+ ++ case evBody event of+ EventFields fields -> concatMap (aggregateFieldNameRule "event field") fields+ EventFromCommand _ -> []++ processNames process =+ constructorName "process name" (procId process) (procLoc process)+ ++ constructorName "process input name" (inName input) (procLoc process)+ ++ concatMap (\field -> fieldNameRule "process input field" (fieldName field) (procLoc process)) (inFields input)+ ++ concatMap (bindingName "advance field binding" (procLoc process)) (advFields (hAdvance handle))+ ++ concatMap dispatchBindings (hDispatch handle)+ ++ concatMap (bindingName "timer payload field binding" (tmLoc timer)) (tmPayload timer)+ ++ concatMap (bindingName "timer fire field binding" (tmLoc timer)) (fireFields (tmFire timer))+ where+ input = procInput process+ handle = procHandle process+ timer = procTimer process+ dispatchBindings dispatch = concatMap (bindingName "dispatch field binding" (dispLoc dispatch)) (dispFields dispatch)++ routerNames router =+ constructorName "router name" (rtId router) (rtLoc router)+ ++ constructorName "router input name" (inName input) (rtLoc router)+ ++ concatMap (\field -> fieldNameRule "router input field" (fieldName field) (rtLoc router)) (inFields input)+ ++ concatMap (\field -> fieldNameRule "router resolve-row field" field (rvLoc resolve)) (rvRow resolve)+ ++ concatMap (bindingName "router dispatch field binding" (rdLoc dispatch)) (rdFields dispatch)+ where+ input = rtInput router+ resolve = rtResolve router+ dispatch = rtDispatch router++ bindingName category anchor binding = fieldNameRule category (fbName binding) anchor+ contractFieldName = contractFieldNameRule "contract field"++ aggregateFieldNameRule category field =+ case aggregateFieldSelector field of+ Nothing -> fieldNameRule category (aggregateFieldName field) (aggregateFieldLoc field)+ Just selector -> explicitFieldSelectorRule category (aggregateFieldName field) selector (aggregateFieldLoc field)++ contractFieldNameRule category field =+ case cfSelector field of+ Nothing -> fieldNameRule category (cfName field) (cfLoc field)+ Just selector -> explicitFieldSelectorRule category (cfName field) selector (cfLoc field)++ explicitFieldSelectorRule category dslName selector anchor =+ case HaskellName.checkedLowerOccurrence site selector of+ Right _ -> []+ Left nameError -> [nameErrorDiagnostic (category <> " selector") nameError]+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedFieldSite,+ HaskellName.siteLogicalName = selector,+ HaskellName.siteOwner = category <> ":" <> dslName,+ HaskellName.siteLine = locLine anchor+ }++ constructorName category name anchor = checkedLogicalName HaskellName.GeneratedTypeSite category name anchor++ pascalizedNodeName category name anchor = checkedLogicalName HaskellName.NodeModuleSite (category <> " name") name anchor++ fieldNameRule category name anchor = checkedLogicalName HaskellName.GeneratedFieldSite category name anchor++ checkedLogicalName kind category name anchor =+ case deriveAt HaskellName.LogicalIdentifier kind category name anchor of+ Right _ -> []+ Left nameError -> [nameErrorDiagnostic category nameError]++ deriveAt source kind category name anchor =+ HaskellName.deriveHaskellName source (nameSite kind category name anchor)++ nameSite kind category name anchor =+ HaskellName.NameSite+ { HaskellName.siteKind = kind,+ HaskellName.siteLogicalName = name,+ HaskellName.siteOwner = category <> ":" <> name,+ HaskellName.siteLine = locLine anchor+ }++ nameErrorDiagnostic category = \case+ HaskellName.EmptyNameSegment site ->+ mkErr (HaskellName.siteLine site) IdentUnsafeNormalization $+ category <> " '" <> HaskellName.siteLogicalName site <> "' has an empty generated-Haskell word"+ HaskellName.UnsafeNameSeparator site reason ->+ mkErr (HaskellName.siteLine site) IdentUnsafeNormalization $+ category <> " '" <> HaskellName.siteLogicalName site <> "' cannot be normalized safely: " <> reason+ HaskellName.ReservedGeneratedOccurrence site occurrence ->+ mkErr (HaskellName.siteLine site) GeneratedOccurrenceReserved $+ category <> " '" <> HaskellName.siteLogicalName site <> "' normalizes to reserved Haskell occurrence '" <> occurrence <> "'"+ HaskellName.InvalidExplicitHaskellName site occurrence ->+ mkErr (HaskellName.siteLine site) IdentUnsafeNormalization $+ category <> " '" <> HaskellName.siteLogicalName site <> "' cannot become generated Haskell occurrence '" <> occurrence <> "'"+ collision@HaskellName.NormalizedNameCollision {} -> collisionDiagnostic collision++ normalizedCollisions = map collisionDiagnostic (HaskellName.detectNameCollisions collisionOccurrences)++ collisionOccurrences =+ nodeModuleOccurrences+ <> sharedTypeOccurrences+ <> concatMap aggregateFieldOccurrences aggregates+ <> concatMap aggregateHarnessOccurrences aggregates+ <> concatMap contractFieldOccurrences contracts++ aggregates = [aggregate | NAggregate aggregate <- specNodes spec]+ contracts = [contract | NContract contract <- specNodes spec]++ contextSegment =+ case deriveAt HaskellName.LogicalWireWord HaskellName.ContextModuleSite "context" (specContext spec) (Loc 1) of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> specContext spec++ nodeModuleOccurrences =+ [ HaskellName.plannedOccurrence contextSegment HaskellName.ModuleSpace "" rendered site+ | node <- specNodes spec,+ let (category, raw, anchor) = nodeNameAndLoc node,+ let site = nameSite HaskellName.NodeModuleSite category raw anchor,+ Right derived <- [HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site],+ let rendered = HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ ]++ sharedTypeOccurrences =+ [ HaskellName.plannedOccurrence ("Generated." <> contextSegment <> ".Nominals") HaskellName.TypeSpace "" rendered site+ | (category, raw, anchor) <-+ [("id", idName declaration, idLoc declaration) | declaration <- specIds spec]+ <> [("enum", enumName declaration, enumLoc declaration) | declaration <- specEnums spec]+ <> [("nominal", nominalScalarName declaration, nominalScalarLoc declaration) | declaration <- specNominalScalars spec],+ let site = nameSite HaskellName.GeneratedTypeSite category raw anchor,+ Right derived <- [HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site],+ let rendered = HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ ]++ aggregateFieldOccurrences aggregate = commandFields <> eventFields+ where+ targetModule = "Generated." <> contextSegment <> "." <> normalizedUpper "aggregate" (aggName aggregate) (aggLoc aggregate) <> ".Domain"+ commandFields =+ [ fieldOccurrence targetModule (cmdName command) "command field" field+ | command <- aggCommands aggregate,+ field <- cmdFields command+ ]+ eventFields =+ [ fieldOccurrence targetModule (evName event) "event field" field+ | event <- aggEvents aggregate,+ field <- eventFieldsFor aggregate event+ ]++ eventFieldsFor aggregate event =+ case evBody event of+ EventFields fields -> fields+ EventFromCommand commandName ->+ [ field+ | command <- aggCommands aggregate,+ cmdName command == commandName,+ field <- cmdFields command+ ]++ -- The occurrence registered here must be the selector generation actually+ -- emits — 'resolveAggregateFieldIdentity', which falls back to the raw DSL+ -- name, not a camelized rendering of it. Registering the camelized form made+ -- the planner claim `foo_bar` "normalizes to" `fooBar`, which generation+ -- never does, and let two fields that generate distinct selectors be+ -- reported as colliding. A field whose raw name is not lowerCamelCase is+ -- still refused, by the generated-name audit that owns that rule and says so.+ fieldOccurrence targetModule scope category field =+ let identity = resolveAggregateFieldIdentity field+ site = nameSite HaskellName.GeneratedFieldSite category (fieldDslName identity) (aggregateFieldLoc field)+ in HaskellName.plannedOccurrence targetModule HaskellName.FieldSpace scope (fieldSelector identity) site++ aggregateHarnessOccurrences aggregate =+ transitionHelpers <> sampleConstants+ where+ transitionHelpers =+ concat+ [ [helperOccurrence "accept" ("accept" <> commandName) transition]+ <> [helperOccurrence "forward/replay" ("forwardReplay" <> commandName) transition | not (null (tEmits transition))]+ | transition <- aggTransitions aggregate,+ tSource transition == initialState,+ tMode transition == TmLive,+ let commandName = tCommand transition+ ]+ sampleConstants = map idSampleOccurrence generatedIds <> maybe [] (pure . timeSampleOccurrence) timeSample+ resolvedHarnessFields =+ [ (field, resolvedType)+ | (useSite, field) <- harnessFields aggregate,+ Right resolvedType <- [inferAggregateFieldType symbols aggregate useSite field]+ ]+ generatedIds =+ Map.elems . Map.fromList $+ [ (Nominal.resolvedNominalName nominal, nominal)+ | (_, AggregateNominal nominal) <- resolvedHarnessFields,+ Nominal.GeneratedNominal <- [Nominal.resolvedNominalOwnership nominal],+ Nominal.IdRepresentation prefix <- [Nominal.resolvedNominalRepresentation nominal],+ idDomainContractFor languageContract prefix /= Nothing+ ]+ timeFields = [field | (field, AggregateTime) <- resolvedHarnessFields]+ timeSample = case filter ((== "observedAt") . aggregateFieldName) timeFields of+ field : _ -> Just ("sampleObservedAt", field)+ [] -> case timeFields of+ field : _ -> Just ("sampleTime", field)+ [] -> Nothing+ initialState = case aggStates aggregate of+ state : _ -> stName state+ [] -> ""+ symbols = aggregateSymbols spec+ targetModule =+ "Generated."+ <> contextSegment+ <> "."+ <> normalizedUpper "aggregate" (aggName aggregate) (aggLoc aggregate)+ <> ".Harness"+ helperOccurrence helperKind rendered transition =+ HaskellName.plannedOccurrence targetModule HaskellName.ValueSpace "" rendered site+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedHelperSite,+ HaskellName.siteLogicalName = tCommand transition,+ HaskellName.siteOwner = "aggregate:" <> aggName aggregate <> ":" <> helperKind <> ":line:" <> T.pack (show (locLine (tLoc transition))),+ HaskellName.siteLine = locLine (tLoc transition)+ }+ idSampleOccurrence nominal =+ HaskellName.plannedOccurrence targetModule HaskellName.ValueSpace "" ("sample" <> nominalName) site+ where+ nominalName = Nominal.resolvedNominalName nominal+ nominalLoc = Nominal.resolvedNominalLoc nominal+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedHelperSite,+ HaskellName.siteLogicalName = nominalName,+ HaskellName.siteOwner = "aggregate:" <> aggName aggregate <> ":sample-id:" <> nominalName,+ HaskellName.siteLine = locLine nominalLoc+ }+ timeSampleOccurrence (rendered, field) =+ HaskellName.plannedOccurrence targetModule HaskellName.ValueSpace "" rendered site+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedHelperSite,+ HaskellName.siteLogicalName = aggregateFieldName field,+ HaskellName.siteOwner = "aggregate:" <> aggName aggregate <> ":sample-time",+ HaskellName.siteLine = locLine (aggregateFieldLoc field)+ }++ harnessFields aggregate =+ [(CommandFieldUse, field) | command <- aggCommands aggregate, field <- cmdFields command]+ <> [ (EventFieldUse, field)+ | event <- aggEvents aggregate,+ field <- eventFieldsFor aggregate event+ ]++ contractFieldOccurrences contract =+ [ contractFieldOccurrence targetModule (ceName event <> "Data") field+ | event <- ctrEvents contract,+ field <- ceFields event+ ]+ where+ targetModule =+ "Generated."+ <> contextSegment+ <> "."+ <> normalizedUpper "contract" (ctrName contract) (ctrLoc contract)+ <> ".Contract"++ contractFieldOccurrence targetModule scope field =+ let identity = resolveContractFieldIdentity field+ site = nameSite HaskellName.GeneratedFieldSite "contract field" (fieldDslName identity) (fieldLoc identity)+ rendered = case cfSelector field of+ Just selector -> selector+ Nothing -> case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderLowerCamelName (HaskellName.lowerCamel derived)+ Left _ -> fieldSelector identity+ in HaskellName.plannedOccurrence targetModule HaskellName.FieldSpace scope rendered site++ normalizedUpper category raw anchor =+ case deriveAt HaskellName.LogicalIdentifier HaskellName.GeneratedTypeSite category raw anchor of+ Right derived -> HaskellName.renderUpperCamelName (HaskellName.upperCamel derived)+ Left _ -> raw++ nodeNameAndLoc = \case+ NAggregate value -> ("aggregate", aggName value, aggLoc value)+ NProcess value -> ("process", procId value, procLoc value)+ NRouter value -> ("router", rtId value, rtLoc value)+ NContract value -> ("contract", ctrName value, ctrLoc value)+ NIntake value -> ("intake", inkName value, inkLoc value)+ NEmit value -> ("emit", emName value, emLoc value)+ NPublisher value -> ("publisher", pubName value, pubLoc value)+ NWorkqueue value -> ("workqueue", wqName value, wqLoc value)+ NPgmqDispatch value -> ("dispatch", pdName value, pdLoc value)+ NReadModel value -> ("readmodel", rmName value, rmLoc value)+ NWorkflow value -> ("workflow", wfId value, workflowNodeLoc value)+ NOperation value -> ("operation", opName value, opLoc value)++ collisionDiagnostic (HaskellName.NormalizedNameCollision key sites) =+ case reverse (NE.toList sites) of+ primary : reversedEarlier ->+ Diagnostic+ { line = HaskellName.siteLine primary,+ severity = Error,+ code = GeneratedOccurrenceCollision,+ relatedLocations =+ [ (HaskellName.siteLine site, "'" <> HaskellName.siteLogicalName site <> "' also normalizes here")+ | site <- reverse reversedEarlier+ ],+ message =+ "logical declarations "+ <> T.intercalate ", " ["'" <> HaskellName.siteLogicalName site <> "'" | site <- NE.toList sites]+ <> " normalize to the same Haskell occurrence '"+ <> HaskellName.occurrenceName key+ <> "' in "+ <> HaskellName.occurrenceModule key+ <> " ("+ <> T.pack (show (HaskellName.occurrenceSpace key))+ <> ")"+ }+ [] -> mkErr 1 GeneratedOccurrenceCollision "internal error: normalized collision without source sites"+ collisionDiagnostic nameError = nameErrorDiagnostic "generated declaration" nameError++ vertexCollisions aggregate =+ [ mkErr (locLine (aggLoc aggregate)) VertexCtorCollision $+ "aggregate '"+ <> aggName aggregate+ <> "' state '"+ <> stName state+ <> "' generates vertex constructor '"+ <> vertex+ <> "', which collides with "+ <> declarationKind+ <> " '"+ <> vertex+ <> "' in the generated Domain constructor namespace"+ | state <- aggStates aggregate,+ let vertex = normalizedUpper "aggregate" (aggName aggregate) (aggLoc aggregate) <> normalizedUpper "state" (stName state) (stLoc state),+ declarationKind <- collisionKinds aggregate vertex+ ]++ collisionKinds aggregate vertex =+ ["event" | vertex `elem` [normalizedUpper "event" (evName event) (evLoc event) | event <- aggEvents aggregate]]+ ++ ["command" | vertex `elem` [normalizedUpper "command" (cmdName command) (cmdLoc command) | command <- aggCommands aggregate]]+ ++ ["enum constructor" | vertex `elem` [normalizedUpper "enum constructor" ctor (enumLoc enum) | enum <- specEnums spec, (ctor, _) <- enumCtors enum]]++-- Explicit consumer-owned Haskell references keep their spelling and use the+-- historical lexical check. Generated names never call this helper.+constructorSafe :: Name -> Bool+constructorSafe name = case T.uncons name of+ Just (first, rest) -> asciiUpper first && T.all asciiAlphaNumOrUnderscore rest+ Nothing -> False++asciiUpper :: Char -> Bool+asciiUpper c = c >= 'A' && c <= 'Z'++asciiLower :: Char -> Bool+asciiLower c = c >= 'a' && c <= 'z'++asciiAlphaNumOrUnderscore :: Char -> Bool+asciiAlphaNumOrUnderscore c = asciiUpper c || asciiLower c || (c >= '0' && c <= '9') || c == '_'++kafkaTopicError :: Text -> Maybe Text+kafkaTopicError topic+ | T.null topic = Just "is empty"+ | T.length topic > 249 = Just "is longer than Kafka's 249-character limit"+ | topic == "." || topic == ".." = Just "is reserved by Kafka"+ | Just illegal <- T.find (not . kafkaTopicCharacter) topic =+ Just ("contains character " <> T.pack (show illegal) <> "; use only ASCII letters, digits, '.', '_', or '-'")+ | otherwise = Nothing+ where+ kafkaTopicCharacter character = asciiAlphaNumOrUnderscore character || character == '.' || character == '-'++validPostgresIdentifier :: Text -> Bool+validPostgresIdentifier identifier =+ T.length identifier <= 63+ && case T.uncons identifier of+ Nothing -> False+ Just (firstCharacter, rest) ->+ (asciiLower firstCharacter || firstCharacter == '_')+ && T.all (\character -> asciiLower character || (character >= '0' && character <= '9') || character == '_') rest++-- | Rules over namespaces shared by the whole specification.+specLevelRules :: EffectiveLanguageContract -> Spec -> [Diagnostic]+specLevelRules languageContract spec = duplicateNodes ++ duplicateEnumMembers ++ duplicateIdPrefixes ++ duplicateDeclarations ++ runtimeIdentities ++ duplicateRuntimeIdentities ++ ruleDiagnostics+ where+ duplicateNodes =+ [ mkErr (locLine loc) DuplicateNodeName $+ "duplicate " <> kind <> " node name '" <> name <> "'"+ | node <- duplicatesBy nodeKey (specNodes spec),+ let (kind, name, loc) = nodeIdentity node+ ]+ nodeKey node = let (kind, name, _) = nodeIdentity node in (kind, name)+ duplicateEnumMembers = concatMap enumDuplicates (specEnums spec)+ enumDuplicates e =+ [ mkErr (locLine (enumLoc e)) DuplicateEnumCtor $+ "enum '" <> enumName e <> "' declares constructor '" <> ctor <> "' more than once"+ | (ctor, _) <- duplicatesBy fst (enumCtors e)+ ]+ ++ [ mkErr (locLine (enumLoc e)) DuplicateEnumWire $+ "enum '" <> enumName e <> "' declares wire spelling '" <> wire <> "' more than once"+ | (_, wire) <- duplicatesBy snd (enumCtors e)+ ]+ duplicateIdPrefixes =+ [ mkErr (locLine (idLoc d)) DuplicateIdPrefix $+ "id '" <> idName d <> "' reuses prefix '" <> idPrefix d <> "'"+ | d <- duplicatesBy idPrefix (specIds spec)+ ]+ duplicateDeclarations =+ [ mkErr (locLine loc) NominalDuplicateDeclaration $+ "duplicate " <> category <> " declaration '" <> name <> "'; the last declaration would silently replace the earlier one"+ | enforcesSpecSurfaceClosures languageContract,+ (category, name, loc) <- duplicatesBy (\(category, name, _) -> (category, name)) declarationOrigins+ ]+ declarationOrigins =+ [("id", idName value, idLoc value) | value <- specIds spec]+ <> [("enum", enumName value, enumLoc value) | value <- specEnums spec]+ <> [("nominal scalar", nominalScalarName value, nominalScalarLoc value) | value <- specNominalScalars spec]+ <> [("mapped", mappedName value, mappedLoc value) | value <- specMapped spec]+ <> [("rule", ruleName value, ruleLoc value) | value <- specRules spec]+ runtimeIdentities =+ [ mkErr (locLine loc) RuntimeIdentityInvalid $+ kind <> " stable identity " <> T.pack (show identity) <> " " <> reason+ | enforcesSpecSurfaceClosures languageContract,+ (kind, identity, loc) <- stableIdentityOrigins,+ Just reason <- [stableIdentityError identity]+ ]+ duplicateRuntimeIdentities =+ [ mkErr (locLine loc) RuntimeIdentityDuplicate $+ kind <> " stable identity " <> T.pack (show identity) <> " is already used by another workflow, process, or router"+ | enforcesSpecSurfaceClosures languageContract,+ (kind, identity, loc) <- duplicatesBy (\(_, identity, _) -> identity) stableIdentityOrigins+ ]+ stableIdentityOrigins =+ [("workflow", wfStable workflow, workflowNodeLoc workflow) | NWorkflow workflow <- specNodes spec]+ <> [("process", procName process, procLoc process) | NProcess process <- specNodes spec]+ <> [("router", rtName router, rtLoc router) | NRouter router <- specNodes spec]+ ruleDiagnostics = concatMap (validateRule spec) (specRules spec)++nodeIdentity :: Node -> (Text, Name, Loc)+nodeIdentity (NAggregate a) = ("aggregate", aggName a, aggLoc a)+nodeIdentity (NProcess p) = ("process", procId p, procLoc p)+nodeIdentity (NRouter r) = ("router", rtId r, rtLoc r)+nodeIdentity (NContract c) = ("contract", ctrName c, ctrLoc c)+nodeIdentity (NIntake i) = ("intake", inkName i, inkLoc i)+nodeIdentity (NEmit e) = ("emit", emName e, emLoc e)+nodeIdentity (NPublisher p) = ("publisher", pubName p, pubLoc p)+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, workflowNodeLoc w)+nodeIdentity (NOperation o) = ("operation", opName o, opLoc o)++validateNode :: EffectiveLanguageContract -> Spec -> Node -> [Diagnostic]+validateNode languageContract spec (NAggregate agg) = validateAggregate languageContract spec agg+validateNode languageContract spec (NProcess p) = validateProcess languageContract spec p+validateNode languageContract spec (NRouter router) = validateRouter languageContract spec router+validateNode languageContract _spec (NContract contract) = validateContract languageContract contract+validateNode languageContract spec (NIntake i) = validateIntake languageContract i ++ intakeCoupling languageContract spec i+validateNode languageContract spec (NEmit e) = validateEmit languageContract spec e+validateNode languageContract spec (NPublisher p) = validatePublisher languageContract spec p+validateNode languageContract _spec (NWorkqueue w) = validateWorkqueue languageContract w+validateNode languageContract spec (NPgmqDispatch d) = validatePgmqDispatch languageContract spec d+validateNode languageContract spec (NReadModel readModel) = validateReadModel languageContract spec readModel+validateNode _languageContract _spec (NWorkflow w) = validateWorkflow w+validateNode _languageContract spec (NOperation o) = validateOperation spec o++validateContract :: EffectiveLanguageContract -> ContractNode -> [Diagnostic]+validateContract languageContract contract =+ emptyContract+ <> typeIdPrefixErrors+ <> schemaVersionFloor+ <> topicNames+ <> duplicateEvents+ <> duplicateTopicAliases+ <> duplicateFields+ <> discriminatorShadows+ <> fieldWireKeyRules+ <> unresolvedTopicAliases+ where+ emptyContract =+ [ mkErr (locLine (ctrLoc contract)) ContractEmpty $+ "contract '"+ <> ctrName contract+ <> "' declares no events; scaffold cannot lower an empty contract -- declare at least one event"+ | null (ctrEvents contract)+ ]+ typeIdPrefixErrors =+ [ mkErr (locLine (cfLoc field)) ContractInvalidTypeIdPrefix $+ "contract '"+ <> ctrName contract+ <> "' event '"+ <> ceName event+ <> "' field '"+ <> cfName field+ <> "' has invalid TypeID prefix '"+ <> prefix+ <> "': "+ <> T.pack (show reason)+ | event <- ctrEvents contract,+ field <- ceFields event,+ CTypeId prefix <- [cfType field],+ Just _ <- [contractIdDomainContractFor languageContract prefix],+ Just reason <- [TypeID.checkPrefix prefix]+ ]+ schemaVersionFloor =+ [ mkErr (locLine (ctrLoc contract)) ContractSchemaVersionBelowMinimum $+ "contract '" <> ctrName contract <> "' schemaVersion must be at least 1"+ | enforcesSpecSurfaceClosures languageContract,+ ctrSchemaVersion contract < 1+ ]+ topicNames =+ [ mkErr (locLine (ctrLoc contract)) ContractTopicNameInvalid $+ "contract '" <> ctrName contract <> "' topic alias '" <> alias <> "' has invalid Kafka topic " <> T.pack (show topic) <> ": " <> reason+ | (alias, topic) <- ctrTopics contract,+ T.null topic || enforcesSpecSurfaceClosures languageContract,+ Just reason <- [kafkaTopicError topic]+ ]+ duplicateEvents =+ [ mkErr (locLine (ctrLoc contract)) ContractDuplicateEvent $+ "contract '" <> ctrName contract <> "' declares event '" <> ceName event <> "' more than once"+ | event <- duplicatesBy ceName (ctrEvents contract)+ ]+ duplicateTopicAliases =+ [ mkErr (locLine (ctrLoc contract)) ContractDuplicateTopicAlias $+ "contract '" <> ctrName contract <> "' declares topic alias '" <> alias <> "' more than once"+ | (alias, _) <- duplicatesBy fst (ctrTopics contract)+ ]+ duplicateFields =+ [ mkErr (locLine (cfLoc field)) ContractDuplicateFieldName $+ "contract '" <> ctrName contract <> "' event '" <> ceName event <> "' declares field '" <> cfName field <> "' more than once"+ | event <- ctrEvents contract,+ field <- duplicatesBy cfName (ceFields event)+ ]+ discriminatorShadows =+ [ mkErr (locLine (cfLoc field)) ContractFieldShadowsDiscriminator $+ "contract '"+ <> ctrName contract+ <> "' event '"+ <> ceName event+ <> "' field '"+ <> cfName field+ <> "' shadows the payload discriminator"+ | enforcesSpecSurfaceClosures languageContract,+ event <- ctrEvents contract,+ field <- ceFields event,+ fieldWireKey (resolveContractFieldIdentity field) == ctrDiscriminator contract+ ]+ fieldWireKeyRules =+ concat+ [ wireKeyRulesForRecord+ ("contract '" <> ctrName contract <> "' event '" <> ceName event <> "'")+ (Just (ctrDiscriminator contract, "payload discriminator"))+ (map resolveContractFieldIdentity (ceFields event))+ | event <- ctrEvents contract+ ]+ unresolvedTopicAliases =+ [ mkErr (locLine (ctrLoc contract)) ContractTopicAliasUnresolved $+ "contract '" <> ctrName contract <> "' event '" <> ceName event <> "' names undeclared topic alias '" <> ceTopic event <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ event <- ctrEvents contract,+ ceTopic event `notElem` map fst (ctrTopics contract)+ ]++-- | Workflow replay keys, patch guards, rotation, and injected inputs must be unambiguous.+validateWorkflow :: WorkflowNode -> [Diagnostic]+validateWorkflow w = duplicateLabels ++ sleepFields ++ patchDuplicates ++ patchIds ++ continuePositions ++ idField+ where+ inputFields = map fieldName (wfInputFields w)+ labelledItems = workflowLabelledItems (wfBody w)+ patchItems = workflowPatchItems (wfBody w)+ duplicateLabels =+ [ mkErr (locLine (wfBodyLoc item)) WorkflowDuplicateLabel $+ "workflow '" <> wfId w <> "' declares label '" <> label <> "' more than once; labels key deterministic replay, so a duplicate label replays the first occurrence's journaled result"+ | (label, item) <- duplicatesBy fst labelledItems+ ]+ sleepFields =+ [ mkErr (locLine loc) WorkflowSleepDelayUnresolved $+ "workflow '" <> wfId w <> "' sleep '" <> label <> "' references undeclared input field '" <> delay <> "'"+ | WfSleep label delay loc <- map snd labelledItems,+ delay `notElem` inputFields+ ]+ patchDuplicates =+ [ mkErr (locLine loc) WorkflowPatchDuplicate $+ "workflow '" <> wfId w <> "' declares patch id '" <> patchId <> "' more than once; patch decisions journal under one stable key"+ | (patchId, _, loc) <- duplicatesBy (\(patchId, _, _) -> patchId) patchItems+ ]+ patchIds =+ [ mkErr (locLine loc) WorkflowPatchIdInvalid $+ "workflow '" <> wfId w <> "' patch id '" <> patchId <> "' contains ':'; the runtime reserves that separator for the patch journal-key prefix"+ | (patchId, _, loc) <- patchItems,+ ":" `T.isInfixOf` patchId+ ]+ continuePositions =+ [ mkErr (locLine loc) WorkflowContinueAsNewNotTerminal $+ "workflow '" <> wfId w <> "' continueAsNew must be the last top-level body item and may not appear inside a patch"+ | (isTopLevelTerminal, loc) <- workflowContinueItems (wfBody w),+ not isTopLevelTerminal+ ]+ idField = case wfIdField w of+ Just field+ | field `notElem` inputFields ->+ [ mkErr (locLine (workflowNodeLoc w)) WorkflowIdFieldUnresolved $+ "workflow '" <> wfId w <> "' derives its id from undeclared input field '" <> field <> "'"+ ]+ _ -> []++wfBodyLoc :: WfBodyItem -> Loc+wfBodyLoc (WfStep _ _ loc) = loc+wfBodyLoc (WfAwait _ _ loc) = loc+wfBodyLoc (WfSleep _ _ loc) = loc+wfBodyLoc (WfChild _ _ _ loc) = loc+wfBodyLoc (WfPatch _ _ loc) = loc+wfBodyLoc (WfContinueAsNew _ loc) = loc++workflowLabelledItems :: [WfBodyItem] -> [(Name, WfBodyItem)]+workflowLabelledItems = concatMap go+ where+ go item@(WfStep label _ _) = [(label, item)]+ go item@(WfAwait label _ _) = [(label, item)]+ go item@(WfSleep label _ _) = [(label, item)]+ go item@(WfChild label _ _ _) = [(label, item)]+ go (WfPatch _ items _) = workflowLabelledItems items+ go WfContinueAsNew {} = []++workflowPatchItems :: [WfBodyItem] -> [(Name, [WfBodyItem], Loc)]+workflowPatchItems = concatMap go+ where+ go (WfPatch patchId items loc) = (patchId, items, loc) : workflowPatchItems items+ go _ = []++-- | Pair every rotation with whether it is the final top-level item.+workflowContinueItems :: [WfBodyItem] -> [(Bool, Loc)]+workflowContinueItems items = topLevel ++ nested+ where+ topLevel =+ [ (index == length items - 1, loc)+ | (index, WfContinueAsNew _ loc) <- zip [0 ..] items+ ]+ nested =+ [ (False, loc)+ | WfPatch _ patchBody _ <- items,+ (_, loc) <- workflowContinueItems patchBody+ ]++-- | A top-level rule is a total, clock-free function over one declared enum.+validateRule :: Spec -> RuleDecl -> [Diagnostic]+validateRule spec rule = case [e | e <- specEnums spec, enumName e == ruleDomain rule] of+ [] ->+ [ mkErr rl RuleDomainUnresolved $+ "rule '" <> ruleName rule <> "' has undeclared enum domain '" <> ruleDomain rule <> "'"+ ]+ (domain : _) -> totality domain ++ unknownCases domain ++ bodyDiagnostics+ where+ rl = locLine (ruleLoc rule)+ caseNames = map fst (ruleCases rule)+ allEnumCtors = Set.fromList [ctor | e <- specEnums spec, (ctor, _) <- enumCtors e]+ totality domain =+ let missing = [ctor | (ctor, _) <- enumCtors domain, ctor `notElem` caseNames]+ in [ mkErr rl RuleNotTotal $+ "rule '" <> ruleName rule <> "' is not total over enum '" <> enumName domain <> "'; missing cases {" <> T.intercalate ", " missing <> "}"+ | not (null missing)+ ]+ unknownCases domain =+ [ mkErr rl RuleCaseUnknownCtor $+ "rule '" <> ruleName rule <> "' has case '" <> ctor <> "' which is not a constructor of enum '" <> enumName domain <> "'"+ | (ctor, _) <- ruleCases rule,+ ctor `notElem` map fst (enumCtors domain)+ ]+ bodyDiagnostics = concatMap validateBody (ruleCases rule)+ validateBody (ctor, expr) =+ [ mkErr rl ClockSampled $+ "rule '" <> ruleName rule <> "' case '" <> ctor <> "' samples the wall clock via '" <> atom <> "'; rules must be deterministic"+ | atom <- dedup (exprNames expr),+ atom `Set.member` clockAtoms+ ]+ ++ [ mkErr rl GuardAtomOutOfScope $+ "atom '" <> atom <> "' in rule '" <> ruleName rule <> "' resolves to no enum constructor or boolean literal"+ | atom <- dedup (exprNames expr),+ atom `Set.notMember` clockAtoms,+ atom `Set.notMember` allEnumCtors+ ]++-- | Operation rules resolve command aggregates, stream fields, projections,+-- read models, workflow signal labels and value types, and run targets.+validateOperation :: Spec -> OperationNode -> [Diagnostic]+validateOperation spec o = case opShape o of+ CommandOp aggregate streamField _ projections ->+ aggregateRef aggregate streamField ++ projectionRefs projections+ QueryOp readModel _ _ consistency ->+ resolveReadModelRef QueryUnresolvedReadModel spec (opLoc o) ("query operation '" <> opName o <> "'") readModel+ ++ [ mkErr ol QueryConsistencyInvalid $+ "query operation '" <> opName o <> "' has unknown consistency '" <> consistency <> "'; expected Strong, Eventual, or PositionWait"+ | consistency `notElem` (["Strong", "Eventual", "PositionWait"] :: [Name])+ ]+ SignalOp lbl wf _ _ valueType ->+ case lookupWorkflow wf of+ Nothing ->+ [mkErr ol AwaitSignalMismatch ("signal operation '" <> opName o <> "' targets undeclared workflow '" <> wf <> "'")]+ Just w -> case [(resultType, loc) | (_, WfAwait label resultType loc) <- workflowLabelledItems (wfBody w), label == lbl] of+ [] ->+ [ mkErr ol AwaitSignalMismatch $+ "signal '" <> lbl <> "' of " <> wf <> " has no matching 'await' (workflow declares awaits {" <> T.intercalate ", " (awaitLabels w) <> "}); the deterministic awakeable id will not match and the workflow will wait forever"+ ]+ ((resultType, _) : _)+ | valueType == resultType -> []+ | otherwise ->+ [ mkErr ol AwaitSignalValueMismatch $+ "signal '" <> lbl <> "' of " <> wf <> " carries value type '" <> valueType <> "' but the await expects '" <> resultType <> "'"+ ]+ RunOp wf _ _ ->+ [ mkErr ol RunWorkflowUnresolved ("run operation '" <> opName o <> "' targets undeclared workflow '" <> wf <> "'")+ | wf `notElem` map wfId workflows+ ]+ where+ ol = locLine (opLoc o)+ workflows = [w | NWorkflow w <- specNodes spec]+ aggregates = [a | NAggregate a <- specNodes spec]+ projectionTables = [projTable p | a <- aggregates, Just p <- [aggProjection a]]+ lookupWorkflow n = case [w | w <- workflows, wfId w == n] of (w : _) -> Just w; [] -> Nothing+ awaitLabels w = [l | (_, WfAwait l _ _) <- workflowLabelledItems (wfBody w)]+ aggregateRef name streamField = case [a | a <- aggregates, aggName a == name] of+ [] ->+ [ mkErr ol OperationUnresolvedRef $+ "command operation '" <> opName o <> "' targets undeclared aggregate '" <> name <> "'"+ ]+ (aggregate : _) ->+ [ mkErr ol OperationUnresolvedRef $+ "command operation '" <> opName o <> "' stream field '" <> streamField <> "' is not declared by any command of aggregate '" <> name <> "'"+ | streamField `notElem` [aggregateFieldName field | command <- aggCommands aggregate, field <- cmdFields command]+ ]+ projectionRefs projections =+ [ mkErr ol OperationUnresolvedRef $+ "command operation '" <> opName o <> "' references undeclared projection table '" <> projection <> "'"+ | projection <- projections,+ projection `notElem` projectionTables+ ]++-- | Resolve a named read-model node using the caller's diagnostic code.+resolveReadModelRef :: DiagnosticCode -> Spec -> Loc -> Text -> Name -> [Diagnostic]+resolveReadModelRef diagnosticCode spec diagnosticLoc context name =+ [ mkErr (locLine diagnosticLoc) diagnosticCode $+ context <> " references undeclared readmodel '" <> name <> "'"+ | name `notElem` [rmName readModel | NReadModel readModel <- specNodes spec]+ ]++-- | Validate captured identity, feed semantics, and the declared column surface.+validateReadModel :: EffectiveLanguageContract -> Spec -> ReadModelNode -> [Diagnostic]+validateReadModel languageContract spec readModel =+ shapeFixture ++ columnTypes ++ strongFeed ++ scopeMode ++ inlineSubscription ++ inlineReference ++ versionFloor ++ identifiers ++ runtimeIdentities ++ duplicateColumns+ where+ readModelLine = locLine (rmLoc readModel)+ expectedShape = deriveShapeHash readModel+ shapeFixture =+ [ mkErr readModelLine RmShapeHashDrift $+ "readmodel '"+ <> rmName readModel+ <> "': captured shape \""+ <> rmShape readModel+ <> "\" does not match the declared columns (expected \""+ <> expectedShape+ <> "\"); update the fixture AND bump version if the table shape really changed"+ | rmShape readModel /= expectedShape+ ]+ allowedColumnTypes = Set.fromList ["text", "int", "bigint", "bool", "timestamptz", "jsonb", "numeric"]+ columnTypes =+ [ mkErr readModelLine RmUnknownColumnType $+ "readmodel '" <> rmName readModel <> "' column '" <> rmcName columnDecl <> "' has unknown type '" <> rmcType columnDecl <> "'"+ | columnDecl <- rmColumns readModel,+ rmcType columnDecl `Set.notMember` allowedColumnTypes+ ]+ strongFeed =+ [ mkErr readModelLine RmStrongInlineOnly $+ "readmodel '"+ <> rmName readModel+ <> "': consistency = Strong with feed = inline; an inline-only model has no subscription worker to advance the cursor a Strong read waits on. Use consistency = Eventual, or feed = subscription"+ | rmFeed readModel == RmInline,+ rmConsistency readModel == Strong+ ]+ scopeMode =+ [ mkErr readModelLine RmScopeWithoutStrong $+ "readmodel '" <> rmName readModel <> "': scope is meaningful only with consistency = Strong"+ | rmScope readModel /= Nothing,+ rmConsistency readModel /= Strong+ ]+ inlineSubscription =+ [ Diagnostic+ { line = readModelLine,+ severity = Warning,+ code = RmInlineSubscriptionIgnored,+ relatedLocations = [],+ message = "readmodel '" <> rmName readModel <> "': subscription override is ignored when feed = inline; remove it or select feed = subscription"+ }+ | rmFeed readModel == RmInline,+ rmSubscription readModel /= Nothing+ ]+ inlineReference =+ [ mkErr readModelLine RmInlineFeedUnreferenced $+ "readmodel '" <> rmName readModel <> "' declares feed = inline but no aggregate projection references it"+ | rmFeed readModel == RmInline,+ rmName readModel `notElem` [projTable projection | NAggregate aggregate <- specNodes spec, Just projection <- [aggProjection aggregate]]+ ]+ versionFloor =+ [ mkErr readModelLine ReadModelVersionBelowMinimum $+ "readmodel '" <> rmName readModel <> "' version must be at least 1"+ | enforcesSpecSurfaceClosures languageContract,+ rmVersion readModel < 1+ ]+ identifiers =+ [ mkErr readModelLine ReadModelIdentifierInvalid $+ "readmodel '" <> rmName readModel <> "' " <> kind <> " " <> T.pack (show identifier) <> " is not a PostgreSQL unquoted identifier"+ | enforcesSpecSurfaceClosures languageContract,+ (kind, identifier) <-+ [("schema", rmSchema readModel), ("table", rmTable readModel)]+ <> [("column", rmcName columnDecl) | columnDecl <- rmColumns readModel],+ not (validPostgresIdentifier identifier)+ ]+ runtimeIdentities =+ [ mkErr readModelLine RuntimeIdentityInvalid $+ "readmodel '" <> rmName readModel <> "' subscription " <> T.pack (show subscription) <> " " <> reason+ | enforcesSpecSurfaceClosures languageContract,+ Just subscription <- [rmSubscription readModel],+ Just reason <- [stableIdentityError subscription]+ ]+ ++ [ mkErr readModelLine RuntimeIdentityInvalid $+ "readmodel '" <> rmName readModel <> "' scope category " <> T.pack (show category) <> " " <> reason+ | enforcesSpecSurfaceClosures languageContract,+ Just (RmCategory category) <- [rmScope readModel],+ Just reason <- [runtimeIdentityError False category]+ ]+ duplicateColumns =+ [ mkErr readModelLine ReadModelDuplicateColumn $+ "readmodel '" <> rmName readModel <> "' declares column '" <> rmcName columnDecl <> "' more than once"+ | enforcesSpecSurfaceClosures languageContract,+ columnDecl <- duplicatesBy rmcName (rmColumns readModel)+ ]++-- | EP-5 workqueue rules: the captured physical name must match the queueRef+-- derivation; the disposition inversions (storeFailure transient => must retry;+-- decodeFailure poison => must dead-letter); and dlq=on requires a retry ceiling.+validateWorkqueue :: EffectiveLanguageContract -> WorkqueueNode -> [Diagnostic]+validateWorkqueue languageContract w = concat [divergence, completeness, duplicateRows, inversions, retryCeiling, orderingRules, groupKeyRules, payloadTypes, windows, provisionRules]+ where+ wl = locLine (wqLoc w)+ rows = wqDisposition w+ (derivedPhysical, derivedDlq, derivedTable) = derivedQueueTrio (wqLogical w)+ divergence =+ [ mkErr wl WqPhysicalDivergence $+ "workqueue '" <> wqName w <> "': captured physical \"" <> wqPhysical w <> "\" diverges from queueRef(\"" <> wqLogical w <> "\") = \"" <> derivedPhysical <> "\""+ | wqPhysical w /= derivedPhysical+ ]+ ++ [ mkErr wl WqDlqDivergence $+ "workqueue '" <> wqName w <> "': captured dlq \"" <> wqDlq w <> "\" diverges from queueRef = \"" <> derivedDlq <> "\""+ | wqDlq w /= derivedDlq+ ]+ ++ [ mkErr wl WqTableDivergence $+ "workqueue '" <> wqName w <> "': captured table \"" <> wqTable w <> "\" diverges from queueRef table = \"" <> derivedTable <> "\""+ | wqTable w /= derivedTable+ ]+ requiredOutcomes = ["storeFailure", "commandRejected", "decodeFailure", "onCodecReject"]+ completeness =+ [ mkErr wl WqDispositionIncomplete $+ "workqueue '" <> wqName w <> "' disposition table is missing outcome '" <> outcome <> "'"+ | outcome <- requiredOutcomes,+ outcome `notElem` map wqdOutcome rows+ ]+ duplicateRows =+ [ mkErr (locLine (wqdLoc row)) DispositionDuplicateOutcome $+ "workqueue '" <> wqName w <> "' repeats disposition outcome '" <> wqdOutcome row <> "'; the first row would shadow this row"+ | row <- duplicatesBy wqdOutcome rows+ ]+ firstRow outcome = case [row | row <- rows, wqdOutcome row == outcome] of+ (row : _) -> Just row+ [] -> Nothing+ isRetry row = case wqdAction row of IRetry _ -> True; _ -> False+ isDeadLetter row = case wqdAction row of IDeadLetter _ -> True; _ -> False+ inversions =+ [ mkErr (locLine (wqdLoc row)) WqStoreFailureNotRetry ("workqueue '" <> wqName w <> "': 'storeFailure' is transient and MUST retry, not dead-letter")+ | Just row <- [firstRow "storeFailure"],+ isDeadLetter row+ ]+ ++ [ mkErr (locLine (wqdLoc row)) WqDecodeFailureNotDeadLetter ("workqueue '" <> wqName w <> "': 'decodeFailure' is poison and MUST dead-letter, not retry")+ | Just row <- [firstRow "decodeFailure"],+ isRetry row+ ]+ retryCeiling =+ [ mkErr wl WqDlqWithoutCeiling ("workqueue '" <> wqName w <> "': dlq=on requires maxRetries >= 1 (an absent ceiling never dead-letters)")+ | wqDlqOn w && wqMaxRetries w < 1+ ]+ fifo = wqOrdering w /= WqUnordered+ orderingRules =+ [ mkErr wl WqGroupKeyMissing $+ "workqueue '" <> wqName w <> "': FIFO delivery is per group, so ordering requires a 'group key' clause that makes enqueueToGroup deterministic"+ | fifo && wqGroupKey w == Nothing+ ]+ ++ [ mkErr wl WqGroupKeyWithoutFifo $+ "workqueue '" <> wqName w <> "': a group key with unordered reads would be ignored; declare a FIFO ordering or remove the key"+ | not fifo && wqGroupKey w /= Nothing+ ]+ groupKeyRules = case wqGroupKey w of+ Nothing -> []+ Just groupKey ->+ case [field | field <- wqPayload w, wqfName field == gkField groupKey] of+ [] ->+ [ mkErr wl WqGroupKeyUnresolved $+ "workqueue '" <> wqName w <> "': group key field '" <> gkField groupKey <> "' is not declared in its payload"+ ]+ field : _ ->+ [ mkErr wl WqGroupKeyUnresolved $+ "workqueue '" <> wqName w <> "': group key via raw requires a text payload field, but '" <> gkField groupKey <> "' has type '" <> wqfType field <> "'"+ | gkVia groupKey == "raw" && wqfType field /= "text"+ ]+ ++ [ mkErr wl WqGroupKeyUnresolved $+ "workqueue '" <> wqName w <> "': opaque group-key derivation '" <> gkVia groupKey <> "' requires a captured fixture"+ | gkVia groupKey /= "raw" && gkFixture groupKey == Nothing+ ]+ payloadTypes =+ [ mkErr wl WqPayloadTypeUnknown $+ "workqueue '" <> wqName w <> "' payload field '" <> wqfName field <> "' has unknown type '" <> wqfType field <> "'; expected text, int, or bool"+ | enforcesSpecSurfaceClosures languageContract,+ field <- wqPayload w,+ wqfType field `Set.notMember` Set.fromList ["text", "int", "bool"]+ ]+ windows =+ windowRangeRule languageContract wl ("workqueue '" <> wqName w <> "' delay") (wqDelay w)+ ++ concat+ [ windowRangeRule languageContract (locLine (wqdLoc row)) ("workqueue '" <> wqName w <> "' retry") window+ | row <- rows,+ IRetry window <- [wqdAction row]+ ]+ provisionRules = case wqProvision w of+ WqStandard -> []+ WqUnlogged ->+ [ Diagnostic+ { line = wl,+ severity = Warning,+ code = WqUnloggedDurability,+ relatedLocations = [],+ message = "workqueue '" <> wqName w <> "': provision unlogged is truncated to empty on a database crash; use it only for transient, regenerable work"+ }+ ]+ WqPartitioned interval retention ->+ [ mkErr wl WqPartitionSpecEmpty $+ "workqueue '" <> wqName w <> "': partition interval and retention must be non-empty; they are create-time settings and the additive reconciler will not migrate an existing queue"+ | T.null interval || T.null retention+ ]++-- | EP-5 dispatch rule: the @enqueue to@ target must resolve to a declared workqueue.+validatePgmqDispatch :: EffectiveLanguageContract -> Spec -> PgmqDispatchNode -> [Diagnostic]+validatePgmqDispatch languageContract spec d = enqueueRef ++ dedupQueueRef ++ sourceReadModelRef ++ sourceReadModelField ++ dedupReadModelRef ++ dedupReadModelField ++ dedupKeyField ++ fanoutFunctionName+ where+ dl = locLine (pdLoc d)+ -- The top-level `dedup key` is the logical value being deduped. Its two+ -- physical locations are already checked against the seenIn read model and+ -- queue; the key itself comes from the source read model's row, so it must+ -- be one of that model's generated selectors — exactly the rule `source key`+ -- already obeys. ExecPlan 197 parked this as descriptive-only.+ dedupKeyField = case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == pdSourceReadModel d] of+ [] -> []+ readModel : _ ->+ [ mkSurfaceRefusal languageContract dl DispatchReadModelFieldUnknown $+ "dispatch '"+ <> pdName d+ <> "' dedup key '"+ <> pdDedupKey d+ <> "' is not a generated logical selector for a column of source readmodel '"+ <> pdSourceReadModel d+ <> "'"+ | pdDedupKey d `notElem` map (logicalFieldSelector . rmcName) (rmColumns readModel)+ ]++ -- `fanout body` names a hand-written function that expands one source row+ -- into queued jobs. A pgmq dispatch generates no module, so there is no+ -- typed namespace to resolve the name against — but a name that is not a+ -- legal Haskell value identifier cannot be the name of any function anyone+ -- could write, which is decidable here. ExecPlan 197 parked this too.+ fanoutFunctionName =+ [ mkSurfaceRefusal languageContract dl PgmqFanoutFunctionInvalid $+ "dispatch '"+ <> pdName d+ <> "' fanout body '"+ <> pdFanoutBody d+ <> "' cannot name a Haskell function; it must be a lowercase-initial identifier that is not a reserved word"+ | not (lowerIdentifierSafe (pdFanoutBody d))+ ]+ workqueues = [w | NWorkqueue w <- specNodes spec]+ enqueueRef =+ [ mkErr dl DispatchEnqueueUnresolved ("dispatch '" <> pdName d <> "' enqueues to undeclared workqueue '" <> pdEnqueueTo d <> "'")+ | pdEnqueueTo d `notElem` map wqName workqueues+ ]+ dedupQueueRef = case [w | w <- workqueues, wqName w == pdDedupQueue d] of+ [] ->+ [ mkErr dl DispatchDedupQueueUnresolved $+ "dispatch '" <> pdName d <> "' checks an undeclared dedup queue '" <> pdDedupQueue d <> "'"+ ]+ (queue : _) ->+ [ mkErr dl DispatchDedupFieldUnresolved $+ "dispatch '" <> pdName d <> "' dedup field '" <> pdDedupQueueField d <> "' is not a payload wire field of queue '" <> pdDedupQueue d <> "'"+ | pdDedupQueueField d `notElem` map wqfWire (wqPayload queue)+ ]+ sourceReadModelRef =+ resolveReadModelRef DispatchReadModelUnresolved spec (pdLoc d) ("dispatch '" <> pdName d <> "' source") (pdSourceReadModel d)+ sourceReadModelField = case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == pdSourceReadModel d] of+ [] -> []+ readModel : _ ->+ [ mkErr dl DispatchReadModelFieldUnknown $+ "dispatch '" <> pdName d <> "' source key '" <> pdSourceKey d <> "' is not a generated logical selector for a column of readmodel '" <> pdSourceReadModel d <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ pdSourceKey d `notElem` map (logicalFieldSelector . rmcName) (rmColumns readModel)+ ]+ dedupReadModelRef =+ resolveReadModelRef DispatchReadModelUnresolved spec (pdLoc d) ("dispatch '" <> pdName d <> "' dedup") (pdDedupReadModel d)+ dedupReadModelField = case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == pdDedupReadModel d] of+ [] -> []+ (readModel : _) ->+ [ mkErr dl DispatchReadModelFieldUnknown $+ "dispatch '" <> pdName d <> "' dedup field '" <> pdDedupReadModelField d <> "' is not a declared column of readmodel '" <> pdDedupReadModel d <> "'"+ | pdDedupReadModelField d `notElem` map rmcName (rmColumns readModel)+ ]++-- | The declared contracts in a spec, by name.+specContracts :: Spec -> [ContractNode]+specContracts spec = [c | NContract c <- specNodes spec]++-- | EP-4 cross-node coupling: an intake's contract/topic/accepted-events resolve.+intakeCoupling :: EffectiveLanguageContract -> Spec -> IntakeNode -> [Diagnostic]+intakeCoupling languageContract spec i = bindFlagWarnings ++ bindHeaderNames ++ contractCoupling+ where+ -- The Kafka inbox reconstructs an envelope from the canonical header names+ -- in "Keiro.Integration.Event"; nothing reads a spec-declared header. A row+ -- naming a canonical header is descriptive and true, so it stays silent. A+ -- row naming any other header reads like remapping and silently is not.+ bindHeaderNames =+ [ mkSurfaceRefusal languageContract (locLine (inkLoc i)) IntakeBindHeaderUnknown $+ "intake '"+ <> inkName i+ <> "' binds '"+ <> brField binding+ <> "' from header "+ <> T.pack (show headerName)+ <> ", which is not one of keiro's canonical envelope headers; the Kafka inbox reads a fixed header set and cannot be remapped, so this row would not take effect. Use one of: "+ <> T.intercalate ", " (map (T.pack . show) canonicalEnvelopeHeaders)+ | binding <- inkBinds i,+ SrcHeader headerName <- [brSource binding],+ headerName `notElem` canonicalEnvelopeHeaders+ ]+ contractCoupling = case lookupContract (inkContract i) of+ Nothing ->+ [mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' references undeclared contract '" <> inkContract i <> "'")]+ Just c ->+ concat+ [ [ mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' topic '" <> inkTopic i <> "' is not a topic of contract '" <> inkContract i <> "'")+ | inkTopic i `notElem` map fst (ctrTopics c)+ ],+ [ mkErr (locLine (inkLoc i)) IntakeUnresolvedContract ("intake '" <> inkName i <> "' accepts event '" <> ev <> "' not declared in contract '" <> inkContract i <> "'")+ | ev <- inkAccept i,+ ev `notElem` map ceName (ctrEvents c)+ ],+ [ mkErr (locLine (inkLoc i)) TopicAffinityMismatch $+ "intake '" <> inkName i <> "' subscribes to topic '" <> inkTopic i <> "' but accepted event '" <> ceName event <> "' is declared on topic '" <> ceTopic event <> "'"+ | event <- ctrEvents c,+ ceName event `elem` inkAccept i,+ ceTopic event /= inkTopic i+ ],+ [ mkErr (locLine (inkLoc i)) IntakeBindUnresolved $+ "intake '" <> inkName i <> "' binds undeclared envelope or accepted-event field '" <> brField binding <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ binding <- inkBinds i,+ brField binding `Set.notMember` resolvableFields c+ ],+ [ mkErr (locLine (inkLoc i)) IntakeDedupeKeyUnresolved $+ "intake '" <> inkName i <> "' dedupe key '" <> inkDedupeKey i <> "' is not an envelope or accepted-event field"+ | enforcesSpecSurfaceClosures languageContract,+ inkDedupeKey i `Set.notMember` resolvableFields c+ ],+ [ mkErr (locLine (inkLoc i)) IntakeDecodeSchemaVersionMismatch $+ "intake '"+ <> inkName i+ <> "' decode schemaVersion "+ <> tInt (decBodySchemaVersion (inkDecode i))+ <> " does not match contract '"+ <> ctrName c+ <> "' schemaVersion "+ <> tInt (ctrSchemaVersion c)+ | enforcesSpecSurfaceClosures languageContract,+ decBodySchemaVersion (inkDecode i) /= ctrSchemaVersion c+ ]+ ]+ bindFlagWarnings =+ [ Diagnostic+ { line = locLine (inkLoc i),+ severity = Warning,+ code = IntakeBindFlagUnenforced,+ relatedLocations = [],+ message =+ "intake '"+ <> inkName i+ <> "' bind for '"+ <> brField binding+ <> "' declares "+ <> bindFlagText binding+ <> ", but generated code does not consume envelope bindings"+ }+ | binding <- inkBinds i,+ brRequired binding || brCrossCheck binding+ ]+ lookupContract n = case [c | c <- specContracts spec, ctrName c == n] of (c : _) -> Just c; [] -> Nothing+ resolvableFields contract =+ canonicalIntakeEnvelopeFields+ <> Set.fromList+ [ cfName field+ | event <- ctrEvents contract,+ ceName event `elem` inkAccept i,+ field <- ceFields event+ ]+ bindFlagText binding = case (brRequired binding, brCrossCheck binding) of+ (True, True) -> "'required' and 'cross-check body' flags"+ (True, False) -> "a 'required' flag"+ (False, True) -> "a 'cross-check body' flag"+ (False, False) -> "no enforcement flags"++-- Note: @derive … hole@ is mandatory emit grammar, so a per-emit warning about+-- it would fire on every emit node in every spec and carry no information. The+-- fact that an emit generates no module is reported once, per scaffold run, by+-- the report's inert-node line. See ExecPlan 199.+validateEmit :: EffectiveLanguageContract -> Spec -> EmitNode -> [Diagnostic]+validateEmit languageContract spec e = skipRule ++ duplicateCases ++ coupling+ where+ el = locLine (emLoc e)+ skipRule =+ [ mkErr el EmitSkipMissing ("emit '" <> emName e <> "' map must end with an explicit '_ => skip' catch-all (hole-kind 7 optionality)")+ | not (emSkip e)+ ]+ duplicateCases =+ [ mkErr (locLine (emrLoc row)) EmitMapDuplicateCase $+ "emit '" <> emName e <> "' repeats map discriminant '" <> emrValue row <> "'; the first row would shadow this row"+ | enforcesSpecSurfaceClosures languageContract,+ row <- duplicatesBy emrValue (emMap e)+ ]+ coupling = case [c | c <- specContracts spec, ctrName c == emContract e] of+ [] -> [mkErr el EmitUnresolvedContract ("emit '" <> emName e <> "' references undeclared contract '" <> emContract e <> "'")]+ (c : _) ->+ [ mkErr el EmitUnresolvedContract ("emit '" <> emName e <> "' topic '" <> emTopic e <> "' is not a topic of contract '" <> emContract e <> "'")+ | emTopic e `notElem` map fst (ctrTopics c)+ ]+ ++ [ mkErr (locLine (emrLoc r)) EmitUnresolvedContract ("emit '" <> emName e <> "' maps to event '" <> emrEvent r <> "' not declared in contract '" <> emContract e <> "'")+ | r <- emMap e,+ emrEvent r `notElem` map ceName (ctrEvents c)+ ]+ ++ [ mkErr (locLine (emrLoc row)) TopicAffinityMismatch $+ "emit '" <> emName e <> "' publishes on topic '" <> emTopic e <> "' but mapped event '" <> emrEvent row <> "' is declared on topic '" <> ceTopic event <> "'"+ | row <- emMap e,+ event <- ctrEvents c,+ ceName event == emrEvent row,+ ceTopic event /= emTopic e+ ]++validatePublisher :: EffectiveLanguageContract -> Spec -> PublisherNode -> [Diagnostic]+validatePublisher languageContract spec p =+ unresolvedEmit ++ orderingVocabulary ++ backoffPolicy ++ attemptsFloor ++ outboxField ++ windows+ where+ publisherLine = locLine (pubLoc p)+ unresolvedEmit =+ [ mkErr publisherLine PublisherUnresolvedEmit ("publisher '" <> pubName p <> "' references undeclared emit '" <> pubEmit p <> "'")+ | pubEmit p `notElem` [emName e | NEmit e <- specNodes spec]+ ]+ orderingVocabulary =+ [ mkErr publisherLine PublisherOrderingUnknown $+ "publisher '"+ <> pubName p+ <> "' has unknown ordering '"+ <> pubOrdering p+ <> "'; expected PerKeyHeadOfLine, PerSourceStream, StopTheLine, or BestEffort"+ | pubOrdering p `Set.notMember` publisherOrderings+ ]+ backoffPolicy =+ [ mkErr publisherLine PublisherBackoffInvalid $+ "publisher '" <> pubName p <> "' has an invalid " <> problem+ | Just problem <- [backoffProblemMaybe (pubBackoff p)]+ ]+ attemptsFloor =+ [ mkErr publisherLine PublisherMaxAttemptsBelowMinimum $+ "publisher '" <> pubName p <> "' maxAttempts must be at least 1"+ | enforcesSpecSurfaceClosures languageContract,+ pubMaxAttempts p < 1+ ]+ outboxField = case [emitNode | NEmit emitNode <- specNodes spec, emName emitNode == pubEmit p] of+ [] -> []+ emitNode : _ ->+ [ mkErr publisherLine PublisherOutboxFieldUnresolved $+ "publisher '" <> pubName p <> "' outboxId field '" <> pubOutboxField p <> "' is not messageId, idempotencyKey, or a field of an event mapped by emit '" <> pubEmit p <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ pubOutboxField p `Set.notMember` allowedOutboxFields emitNode+ ]+ allowedOutboxFields emitNode =+ Set.fromList ("messageId" : "idempotencyKey" : mappedContractFields emitNode)+ mappedContractFields emitNode =+ [ fieldDslName (resolveContractFieldIdentity field)+ | contract <- specContracts spec,+ ctrName contract == emContract emitNode,+ event <- ctrEvents contract,+ ceName event `elem` map emrEvent (emMap emitNode),+ field <- ceFields event+ ]+ windows =+ windowRangeRule languageContract publisherLine ("publisher '" <> pubName p <> "' backoff") (boWindow (pubBackoff p))+ ++ maybe [] (windowRangeRule languageContract publisherLine ("publisher '" <> pubName p <> "' maximum backoff")) (boMax (pubBackoff p))++publisherOrderings :: Set Name+publisherOrderings = Set.fromList ["PerKeyHeadOfLine", "PerSourceStream", "StopTheLine", "BestEffort"]++backoffProblemMaybe :: BackoffSpec -> Maybe Text+backoffProblemMaybe backoff = case boKind backoff of+ "constant" -> Nothing+ "exponential" -> case (boMax backoff, boMultiplier backoff) of+ (Just maximumWindow, Just multiplierText) ->+ case (validationWindowSeconds (boWindow backoff), validationWindowSeconds maximumWindow, readMaybe (T.unpack multiplierText) :: Maybe Double) of+ (Just initialSeconds, Just maximumSeconds, Just multiplier)+ | initialSeconds > 0 && maximumSeconds >= initialSeconds && multiplier >= 1 -> Nothing+ _ -> Just "exponential backoff; initial must be positive, max must be at least initial, and multiplier must be at least 1"+ _ -> Just "exponential backoff; both max and multiplier are required"+ other -> Just ("backoff kind '" <> other <> "'; expected constant or exponential")++validationWindowSeconds :: Text -> Maybe Int+validationWindowSeconds window = case T.unsnoc window of+ Just (digits, unit) -> do+ amount <- readMaybe (T.unpack digits)+ case unit of+ 's' -> Just amount+ 'm' -> Just (amount * 60)+ 'h' -> Just (amount * 3600)+ _ -> Nothing+ Nothing -> Nothing++windowSecondsBounded :: Text -> Either Text Int+windowSecondsBounded window = case T.unsnoc window of+ Nothing -> Left "has no unit"+ Just (digits, unit) -> case readMaybe (T.unpack digits) :: Maybe Integer of+ Nothing -> Left "has invalid digits"+ Just amount -> case unitFactor unit of+ Nothing -> Left "has an unknown unit"+ Just factor+ | seconds > fromIntegral (maxBound :: Int) -> Left "exceeds the runtime Int seconds range"+ | otherwise -> Right (fromIntegral seconds)+ where+ seconds = amount * factor+ where+ unitFactor 's' = Just 1+ unitFactor 'm' = Just 60+ unitFactor 'h' = Just 3600+ unitFactor _ = Nothing++windowRangeRule :: EffectiveLanguageContract -> Int -> Text -> Text -> [Diagnostic]+windowRangeRule languageContract diagnosticLine context window =+ [ mkErr diagnosticLine WindowOutOfRange $+ context <> " window '" <> window <> "' " <> reason+ | enforcesSpecSurfaceClosures languageContract,+ Left reason <- [windowSecondsBounded window]+ ]++-- | EP-4 inbox disposition rules: the table must be complete over the seven+-- outcomes, and the three dangerous inversions must be stated the safe way.+validateIntake :: EffectiveLanguageContract -> IntakeNode -> [Diagnostic]+validateIntake languageContract i = concat [completeness, duplicateRows, inversions, dedupeVocabulary, decodeVersionFloor, envelopeVocabulary, decodePosture, windows]+ where+ il = locLine (inkLoc i)+ -- `decBodyStrict` reaches nothing but the pretty-printer: generated contract+ -- codecs decode every declared body field as required and admit no lenient+ -- mode, so `body strict` describes what happens and `body lenient` does not.+ decodePosture =+ [ mkSurfaceRefusal languageContract il DecodeBodyPostureUnsupported $+ "intake '"+ <> inkName i+ <> "' declares 'body lenient', but generated contract codecs decode a body strictly: every declared field is required and no lenient fallback is emitted. Write 'body strict' to describe what runs"+ | not (decBodyStrict (inkDecode i))+ ]+ rows = inkDisposition i+ requiredOutcomes =+ ["processed", "duplicate", "inProgress", "previouslyFailed", "decodeFailed", "dedupeFailed", "storeFailed"]+ completeness =+ [ mkErr il DispositionIncomplete $+ "intake '" <> inkName i <> "' disposition table is missing outcome '" <> o <> "'"+ | o <- requiredOutcomes,+ o `notElem` map drOutcome rows+ ]+ duplicateRows =+ [ mkErr (locLine (drLoc row)) DispositionDuplicateOutcome $+ "intake '" <> inkName i <> "' repeats disposition outcome '" <> drOutcome row <> "'; the first row would shadow this row"+ | row <- duplicatesBy drOutcome rows+ ]+ windows =+ concat+ [ windowRangeRule languageContract (locLine (drLoc row)) ("intake '" <> inkName i <> "' retry") window+ | row <- rows,+ IRetry window <- [drAction row]+ ]+ dedupeVocabulary =+ [ mkErr il IntakeDedupePolicyUnknown $+ "intake '"+ <> inkName i+ <> "' has unknown dedupe policy '"+ <> inkDedupePolicy i+ <> "'; expected PreferIntegrationMessageId, PreferSourceEventIdentity, or KafkaDeliveryIdentity"+ | inkDedupePolicy i `Set.notMember` intakeDedupePolicies+ ]+ decodeVersionFloor =+ [ mkErr il IntakeDecodeSchemaVersionBelowMinimum $+ "intake '" <> inkName i <> "' decode schemaVersion must be at least 1"+ | enforcesSpecSurfaceClosures languageContract,+ decBodySchemaVersion (inkDecode i) < 1+ ]+ envelopeVocabulary =+ [ mkErr il IntakeEnvelopePolicyUnknown $+ "intake '" <> inkName i <> "' has unsupported envelope policy " <> T.pack (show (decEnvelope (inkDecode i))) <> "; expected \"strict-required lenient-optional\""+ | enforcesSpecSurfaceClosures languageContract,+ decEnvelope (inkDecode i) /= "strict-required lenient-optional"+ ]+ firstRow outcome = case [row | row <- rows, drOutcome row == outcome] of+ (row : _) -> Just row+ [] -> Nothing+ isRetry row = case drAction row of IRetry _ -> True; _ -> False+ inversions =+ [ mkErr (locLine (drLoc row)) DispositionDuplicateRetry $+ "intake '" <> inkName i <> "': a 'duplicate' redelivery must be ackOk (success), not retry"+ | Just row <- [firstRow "duplicate"],+ isRetry row+ ]+ ++ [ mkErr (locLine (drLoc row)) DispositionPreviouslyFailedRetry $+ "intake '" <> inkName i <> "': 'previouslyFailed' must dead-letter, not retry (a prior failure won't succeed on replay)"+ | Just row <- [firstRow "previouslyFailed"],+ isRetry row+ ]+ ++ [ mkErr (locLine (drLoc row)) DispositionDecodeUnboundedRetry $+ "intake '" <> inkName i <> "': 'decodeFailed' must dead-letter (terminal), not retry unboundedly"+ | Just row <- [firstRow "decodeFailed"],+ isRetry row+ ]++intakeDedupePolicies :: Set Name+intakeDedupePolicies = Set.fromList ["PreferIntegrationMessageId", "PreferSourceEventIdentity", "KafkaDeliveryIdentity"]++-- | The timer statuses a stored row can hold, mirroring @TimerStatus@ in+-- @keiro@'s "Keiro.Timer.Schema". keiro-dsl deliberately does not depend on the+-- runtime package, so the list is restated here; the conformance suite that does+-- depend on @keiro@ asserts the two agree.+runtimeTimerStatuses :: [Text]+runtimeTimerStatuses = ["Scheduled", "Firing", "Fired", "Cancelled", "Dead"]++-- | Every header name keiro's integration envelope actually uses on the wire,+-- taken from the runtime's own definitions in "Keiro.Integration.Event" rather+-- than restated here, so the two cannot drift apart.+canonicalEnvelopeHeaders :: [Text]+canonicalEnvelopeHeaders =+ [ Event.headerMessageId,+ Event.headerSource,+ Event.headerDestination,+ Event.headerEventType,+ Event.headerSchemaVersion,+ Event.headerContentType,+ Event.headerSchemaRegistry,+ Event.headerSchemaSubject,+ Event.headerSchemaVersionRef,+ Event.headerSchemaId,+ Event.headerSchemaFingerprint,+ Event.headerSourceEventId,+ Event.headerSourceGlobalPosition,+ Event.headerCausationId,+ Event.headerCorrelationId,+ Event.headerTraceParent,+ Event.headerTraceState,+ Event.headerOccurredAt,+ Event.headerAttributes+ ]++canonicalIntakeEnvelopeFields :: Set Name+canonicalIntakeEnvelopeFields =+ Set.fromList+ [ "messageId",+ "source",+ "destination",+ "key",+ "eventType",+ "schemaVersion",+ "contentType",+ "schemaReference",+ "sourceEventId",+ "sourceGlobalPosition",+ "payloadBytes",+ "occurredAt",+ "causationId",+ "correlationId",+ "traceContext",+ "attributes",+ "idempotencyKey"+ ]++-- | EP-3 rules for a process manager + its nested timer.+validateProcess :: EffectiveLanguageContract -> Spec -> ProcessNode -> [Diagnostic]+validateProcess languageContract spec p =+ concat [sagaCategoryRule, noWallClock, runtimeOwnedDispatchId, crossNodeCoupling, strictSurfaceResolution, timerCeiling, policyRules, ambiguityRule, benignInversions, onAppendedArms, notMineArm, decodeUnknownStatus, deadLetterText]+ where+ -- Generated dispatch code appends and then acks; `Keiro.ProcessManager` has+ -- no branch that retries or dead-letters a *successful* append. Only AckOk+ -- describes what runs.+ onAppendedArms =+ [ mkSurfaceRefusal languageContract (locLine (dispLoc d)) DispatchOnAppendedUnsupported $+ "dispatch to '"+ <> dispTarget d+ <> "' maps on-appended => "+ <> dispText (onAppended (dispDisposition d))+ <> ", but a successful append is always acked: no runtime path retries or dead-letters an event it just appended. Write 'on-appended AckOk'"+ | d <- hDispatch (procHandle p),+ onAppended (dispDisposition d) /= DAckOk+ ]++ -- `decode unknown-status => X` names the status a row that fails to decode+ -- is read as. X must be a status the timer table actually has.+ decodeUnknownStatus =+ [ mkSurfaceRefusal languageContract (locLine (tmLoc timer)) TimerDecodeStatusUnknown $+ "timer '"+ <> tmName timer+ <> "' maps decode unknown-status => '"+ <> tmDecodeUnknown timer+ <> "', which is not a timer status; a stored timer row is one of: "+ <> T.intercalate ", " runtimeTimerStatuses+ | tmDecodeUnknown timer `notElem` runtimeTimerStatuses+ ]++ -- The dead-letter reason is a hand-owned obligation: `runTimerWorkerWith`+ -- composes its own message for the attempt ceiling, and the generated+ -- comment surfaces this text so an operator-written worker can pass it to+ -- `Keiro.Timer.deadLetterTimer`. Nothing can check what the prose says, but+ -- an empty or blank reason names no obligation at all.+ deadLetterText =+ [ mkSurfaceRefusal languageContract (locLine (tmLoc timer)) TimerDeadLetterTextInvalid $+ "timer '"+ <> tmName timer+ <> "' declares a blank dead-letter reason; the reason is the hand-owned text an operator-written timer worker passes to Keiro.Timer.deadLetterTimer, so it must say something"+ | T.null (T.strip (tmDeadLetter timer))+ ]++ -- The timer worker marks a timer Fired only when the fire action returns the+ -- id of an event it appended (`Keiro.Timer.runTimerWorkerWith`). A not-mine+ -- dispatch produces no such id, so the row is left Firing and requeued on a+ -- later pass — which is exactly Retry. Fired is not reachable.+ notMineArm =+ [ mkSurfaceRefusal languageContract (locLine (tmLoc timer)) TimerNotMineUnsupported $+ "timer '"+ <> tmName timer+ <> "' maps not-mine => Fired, but the timer worker marks a timer Fired only when the fire action returns the id of the event it appended; a dispatch that is not this timer's has no such id, so the row is requeued instead. Write 'not-mine Retry'"+ | notMine (fireDisposition (tmFire timer)) == OFired+ ]+ aggregates = [a | NAggregate a <- specNodes spec]+ aggNames = map aggName aggregates+ projectionTables = [projTable projection | aggregate <- aggregates, Just projection <- [aggProjection aggregate]]+ inputFields = map fieldName (inFields (procInput p))+ timeFields = [fieldName f | f <- inFields (procInput p), fieldType f == Just "Time"]+ timer = procTimer p+ pl = locLine (procLoc p)++ sagaCategoryRule =+ [ mkErr pl SagaCategoryIllegal $+ "saga category " <> T.pack (show (sagaCategory (procSaga p))) <> " " <> reason+ | Just reason <- [sagaCategoryError (sagaCategory (procSaga p))]+ ]++ -- TIME IS INJECTED, NOT SAMPLED: fireAt's field must be a declared :Time+ -- input field. (FireAtExpr has no clock-sampling constructor, so this is a+ -- field-resolution + typed-as-Time check.)+ noWallClock =+ let f = faField (tmFireAt timer)+ in if f `notElem` inputFields+ then+ [ mkErr (locLine (tmLoc timer)) ProcessFireAtNotInjected $+ "timer '" <> tmName timer <> "' fireAt field '" <> f <> "' is not a field of input '" <> inName (procInput p) <> "'"+ ]+ else+ [ mkErr (locLine (tmLoc timer)) ProcessFireAtNotInjected $+ "timer '" <> tmName timer <> "' fireAt references '" <> f <> "', which is not a declared :Time field of input '" <> inName (procInput p) <> "'"+ | f `notElem` timeFields+ ]++ -- Dispatched (and fired) command ids are runtime-owned; no field binding may+ -- supply a commandId/id.+ runtimeOwnedDispatchId =+ [ mkErr pl ProcessDispatchIdSupplied $+ "advance command '" <> advCommand advance <> "' supplies a runtime-owned id field '" <> fbName binding <> "'; remove it"+ | let advance = hAdvance (procHandle p),+ binding <- advFields advance,+ fbName binding `elem` (["commandId", "id"] :: [Name])+ ]+ ++ [ mkErr (locLine (dispLoc d)) ProcessDispatchIdSupplied $+ "dispatch to '" <> dispTarget d <> "' supplies a runtime-owned id field '" <> fbName b <> "'; remove it"+ | d <- hDispatch (procHandle p),+ b <- dispFields d,+ fbName b `elem` (["commandId", "id"] :: [Name])+ ]+ ++ [ mkErr (locLine (tmLoc timer)) ProcessDispatchIdSupplied $+ "timer fire supplies a runtime-owned id field '" <> fbName b <> "'; remove it"+ | b <- fireFields (tmFire timer),+ fbName b `elem` (["commandId", "id"] :: [Name])+ ]++ -- Aggregate, command, field, timer, and projection references must resolve.+ crossNodeCoupling =+ [ mkErr pl ProcessUnresolvedRef ("saga '" <> sagaAgg (procSaga p) <> "' does not resolve to a declared aggregate")+ | sagaAgg (procSaga p) `notElem` aggNames+ ]+ ++ [ mkErr pl ProcessUnresolvedRef ("target '" <> procTarget p <> "' does not resolve to a declared aggregate")+ | procTarget p `notElem` aggNames+ ]+ ++ [ mkErr (locLine (tmLoc timer)) ProcessUnresolvedRef ("timer fire target '" <> fireTarget (tmFire timer) <> "' must be the saga or the target aggregate")+ | fireTarget (tmFire timer) `notElem` [sagaAgg (procSaga p), procTarget p]+ ]+ ++ resolveCommand pl "advance" (sagaAgg (procSaga p)) (advCommand advance) (advFields advance)+ ++ concatMap resolveDispatch (hDispatch (procHandle p))+ ++ resolveCommand (locLine (tmLoc timer)) "timer fire" (fireTarget fire) (fireCommand fire) (fireFields fire)+ ++ [ mkErr pl ProcessUnresolvedRef $+ "process '" <> procId p <> "' schedules undeclared timer '" <> hSchedule (procHandle p) <> "'; declared timer is '" <> tmName timer <> "'"+ | hSchedule (procHandle p) /= tmName timer+ ]+ ++ [ mkErr pl ProcessUnresolvedRef $+ "process '" <> procId p <> "' references undeclared projection table '" <> projection <> "'"+ | projection <- procProjections p,+ projection `notElem` projectionTables+ ]+ where+ advance = hAdvance (procHandle p)+ fire = tmFire timer+ resolveDispatch dispatch =+ resolveCommand+ (locLine (dispLoc dispatch))+ "dispatch"+ (dispTarget dispatch)+ (dispCommand dispatch)+ (dispFields dispatch)+ resolveCommand diagnosticLine context target command bindings = case lookupAggregate target of+ Nothing -> []+ Just aggregate -> case [decl | decl <- aggCommands aggregate, cmdName decl == command] of+ [] ->+ [ mkErr diagnosticLine ProcessUnresolvedRef $+ context <> " command '" <> command <> "' is not declared by aggregate '" <> target <> "'"+ ]+ (declaration : _) ->+ [ mkErr diagnosticLine ProcessFieldBindingUnresolved $+ context <> " command '" <> command <> "' binds undeclared target field '" <> fbName binding <> "'"+ | binding <- bindings,+ fbName binding `notElem` map aggregateFieldName (cmdFields declaration)+ ]+ lookupAggregate name = case [aggregate | aggregate <- aggregates, aggName aggregate == name] of+ (aggregate : _) -> Just aggregate+ [] -> Nothing++ strictSurfaceResolution+ | not (enforcesSpecSurfaceClosures languageContract) = []+ | otherwise = correlateFieldRule ++ dispatchKeyRules ++ bindingScopeRules ++ idFieldRules ++ fireWindowRule++ correlateFieldRule =+ [ mkErr pl ProcessKeyFieldUnknown $+ "correlate references 'input." <> corrField (procCorrelate p) <> "' but input '" <> inName (procInput p) <> "' does not declare that field"+ | corrField (procCorrelate p) `notElem` inputFields+ ]++ dispatchKeyRules =+ [ mkErr (locLine (dispLoc dispatch)) ProcessDispatchKeyUnresolved $+ "dispatch to '" <> dispTarget dispatch <> "' uses unresolved key '" <> dispKey dispatch <> "'; expected correlationId or input.<declared-field>"+ | dispatch <- hDispatch (procHandle p),+ not (processKeyInScope (dispKey dispatch))+ ]+ ++ [ mkErr (locLine (tmLoc timer)) ProcessDispatchKeyUnresolved $+ "timer fire to '" <> fireTarget (tmFire timer) <> "' uses unresolved key '" <> fireKey (tmFire timer) <> "'; expected correlationId or input.<declared-field>"+ | not (processKeyInScope (fireKey (tmFire timer)))+ ]++ processKeyInScope value =+ value == "correlationId"+ || case T.stripPrefix "input." value of+ Just field -> field `elem` inputFields+ Nothing -> False++ bindingScopeRules =+ bindingRules pl "advance" inputFields (advFields (hAdvance (procHandle p)))+ ++ concatMap+ (\dispatch -> bindingRules (locLine (dispLoc dispatch)) "dispatch" inputFields (dispFields dispatch))+ (hDispatch (procHandle p))+ ++ bindingRules+ (locLine (tmLoc timer))+ "timer fire"+ (inputFields <> map fbName (tmPayload timer) <> ["timerId"])+ (fireFields (tmFire timer))++ bindingRules diagnosticLine context bareScope bindings =+ [ mkErr diagnosticLine ProcessBindingUnscoped $+ context <> " binding '" <> fbName binding <> maybe "" ("=" <>) (fbValue binding) <> "' is outside the process input and timer scopes"+ | binding <- bindings,+ not (bindingInScope bareScope binding)+ ]++ bindingInScope bareScope binding = case fbValue binding of+ Nothing -> fbName binding `elem` bareScope+ Just value+ | isQuoted value -> True+ | value == "timer.id" -> True+ | Just field <- T.stripPrefix "input." value -> field `elem` inputFields+ | otherwise -> value `elem` bareScope++ isQuoted value = T.length value >= 2 && T.head value == '"' && T.last value == '"'++ idFieldRules =+ [ mkErr (locLine (tmLoc timer)) TimerIdFieldNotCorrelation $+ "timer '" <> tmName timer <> "' " <> context <> " derives from '" <> ideField expression <> "'; only correlationId is implemented by generated runtime code"+ | (context, expression) <- [("id", tmId timer), ("fired-event-id", fireFiredEventId (tmFire timer))],+ ideField expression /= "correlationId"+ ]++ fireWindowRule =+ windowRangeRule languageContract (locLine (tmLoc timer)) ("timer '" <> tmName timer <> "' fireAt") (faWindow (tmFireAt timer))++ timerCeiling =+ [ mkErr (locLine (tmLoc timer)) ProcessTimerCeilingInvalid $+ "timer '" <> tmName timer <> "' max-attempts must be at least 1"+ | tmMaxAttempts timer < 1+ ]++ policyRules =+ policyConsistency+ (procId p)+ (procLoc p)+ (procRejected p)+ [ (dispCommand dispatch, dispLoc dispatch, dispDisposition dispatch)+ | dispatch <- hDispatch (procHandle p)+ ]++ ambiguityRule =+ [ mkErr (locLine (tmLoc timer)) AmbiguousMarkedBenign $+ "timer '" <> tmName timer <> "' maps on-ambiguous => Fired; CommandAmbiguous means multiple aggregate edges matched and is never a benign success. Use on-ambiguous Retry so the attempts ceiling dead-letters the definition bug"+ | onAmbiguous (fireDisposition (tmFire timer)) == OFired+ ]++ -- Surface the dangerous benign inversions the author confirmed (warnings).+ benignInversions =+ [ Diagnostic (locLine (tmLoc timer)) Warning ProcessBenignInversion [] $+ "timer '" <> tmName timer <> "' maps on-reject => Fired (a CommandRejected is treated as benign success)"+ | onReject (fireDisposition (tmFire timer)) == OFired+ ]+ ++ [ Diagnostic (locLine (dispLoc d)) Warning ProcessBenignInversion [] $+ "dispatch to '" <> dispTarget d <> "' maps on-duplicate => AckOk (a duplicate is treated as benign success)"+ | d <- hDispatch (procHandle p),+ onDuplicate (dispDisposition d) == DAckOk+ ]++-- | Explain why a process saga category is illegal. The first four cases+-- mirror 'Keiro.Stream.category' without introducing a runtime dependency into+-- the toolchain library. The final @:@ case is deliberately stricter because+-- that prefix is reserved for the @wf:<name>@ workflow stream family.+sagaCategoryError :: Text -> Maybe Text+sagaCategoryError = runtimeIdentityError False++stableIdentityError :: Text -> Maybe Text+stableIdentityError = runtimeIdentityError True++runtimeIdentityError :: Bool -> Text -> Maybe Text+runtimeIdentityError allowsHyphen identity+ | T.null identity = Just "is empty; use a non-empty stable name"+ | identity == "$all" = Just "is reserved by the event store; choose a service-owned stable name"+ | not allowsHyphen && T.isInfixOf "-" identity = Just "contains '-' (kiroku's category/id boundary); write compound categories in camelCase, for example \"hospitalSurge\""+ | Just illegal <- T.find (\character -> isSpace character || isControl character) identity =+ Just ("contains whitespace or control character " <> T.pack (show illegal) <> "; remove it and use camelCase")+ | T.isInfixOf ":" identity = Just "contains ':' which is reserved for runtime stream-family prefixes; choose a stable name without ':'"+ | otherwise = Nothing++-- | The generated lower-camel selector for a logical field or SQL column.+-- Read-model notation stores SQL names such as @responder_id@ while router+-- resolve rows and dispatch keys use the generated selector @responderId@.+logicalFieldSelector :: Text -> Text+logicalFieldSelector raw =+ case HaskellName.deriveHaskellName HaskellName.LogicalIdentifier site of+ Right derived -> HaskellName.renderLowerCamelName (HaskellName.lowerCamel derived)+ Left _ -> raw+ where+ site =+ HaskellName.NameSite+ { HaskellName.siteKind = HaskellName.GeneratedFieldSite,+ HaskellName.siteLogicalName = raw,+ HaskellName.siteOwner = "validation field resolution",+ HaskellName.siteLine = 0+ }++-- | EP-108 rules for a stateless content-based router.+validateRouter :: EffectiveLanguageContract -> Spec -> RouterNode -> [Diagnostic]+validateRouter languageContract spec router =+ concat+ [ references,+ keyField,+ bindingScope,+ commandReference,+ readModelReference,+ policyRules,+ duplicateNotice,+ onAppendedArm+ ]+ where+ -- The process twin of this rule is in 'validateProcess'; both say the same+ -- thing because both runtimes do: a successful append is always acked.+ onAppendedArm =+ [ mkSurfaceRefusal languageContract dispatchLine DispatchOnAppendedUnsupported $+ "router dispatch '"+ <> rdCommand dispatch+ <> "' maps on-appended => "+ <> dispText (onAppended (rdDisposition dispatch))+ <> ", but a successful append is always acked: no runtime path retries or dead-letters an event it just appended. Write 'on-appended AckOk'"+ | onAppended (rdDisposition dispatch) /= DAckOk+ ]+ aggregates = [aggregate | NAggregate aggregate <- specNodes spec]+ readModels = [readModel | NReadModel readModel <- specNodes spec]+ inputFields = map fieldName (inFields (rtInput router))+ resolvedFields = rvRow (rtResolve router)+ dispatch = rtDispatch router+ routerLine = locLine (rtLoc router)+ dispatchLine = locLine (rdLoc dispatch)++ targetAggregate = case [aggregate | aggregate <- aggregates, aggName aggregate == rtTarget router] of+ aggregate : _ -> Just aggregate+ [] -> Nothing++ projectionTables = [projTable projection | aggregate <- aggregates, Just projection <- [aggProjection aggregate]]++ references =+ [ mkErr routerLine RouterUnresolvedRef $+ "router '" <> rtId router <> "' targets aggregate '" <> rtTarget router <> "' but no such aggregate is declared"+ | targetAggregate == Nothing+ ]+ ++ [ mkErr routerLine RouterUnresolvedRef $+ "router '" <> rtId router <> "' references undeclared projection table '" <> projection <> "'"+ | projection <- rtProjections router,+ projection `notElem` projectionTables+ ]++ keyField =+ [ mkErr routerLine RouterKeyFieldUnknown $+ "key references 'input." <> corrField (rtKey router) <> "' but input '" <> inName (rtInput router) <> "' does not declare that field"+ | corrField (rtKey router) `notElem` inputFields+ ]++ bindingScope =+ [ mkErr dispatchLine RouterBindingUnscoped $+ "dispatch binding '" <> fbName binding <> maybe "" ("=" <>) (fbValue binding) <> "' is outside the router input and resolve-row scopes"+ | binding <- rdFields dispatch,+ not (bindingInScope binding)+ ]+ where+ bindingInScope binding = case fbValue binding of+ Nothing -> fbName binding `elem` inputFields+ Just value+ | isQuoted value -> True+ | Just field <- T.stripPrefix "input." value -> field `elem` inputFields+ | Just field <- T.stripPrefix "resolved." value -> field `elem` resolvedFields+ | otherwise -> False+ isQuoted value = T.length value >= 2 && T.head value == '"' && T.last value == '"'++ commandReference = case targetAggregate of+ Nothing -> []+ Just aggregate -> case [command | command <- aggCommands aggregate, cmdName command == rdCommand dispatch] of+ [] ->+ [ mkErr dispatchLine RouterCommandUnknown $+ "dispatch command '" <> rdCommand dispatch <> "' is not declared by aggregate '" <> aggName aggregate <> "'"+ ]+ command : _ ->+ [ mkErr dispatchLine RouterCommandUnknown $+ "dispatch command '" <> rdCommand dispatch <> "' binds undeclared target field '" <> fbName binding <> "'"+ | binding <- rdFields dispatch,+ fbName binding `notElem` map aggregateFieldName (cmdFields command)+ ]++ readModelReference = case rvSource (rtResolve router) of+ ResolveHole -> []+ ResolveReadModel name ->+ case [readModel | readModel <- readModels, rmName readModel == name] of+ [] ->+ [ mkErr (locLine (rvLoc (rtResolve router))) RouterUnresolvedRef $+ "router '" <> rtId router <> "' resolve names readmodel '" <> name <> "' but no such readmodel node is declared"+ ]+ readModel : _ ->+ [ mkErr (locLine (rvLoc (rtResolve router))) RouterReadModelUnverified $+ "router '" <> rtId router <> "' resolve row field '" <> column <> "' is not a declared column of readmodel '" <> name <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ column <- rvRow (rtResolve router),+ column `notElem` map (logicalFieldSelector . rmcName) (rmColumns readModel)+ ]++ policyRules =+ policyConsistency+ (rtId router)+ (rtLoc router)+ (rtRejected router)+ [(rdCommand dispatch, rdLoc dispatch, rdDisposition dispatch)]++ duplicateNotice =+ [ Diagnostic dispatchLine Warning RouterBenignInversion [] $+ "router dispatch '" <> rdCommand dispatch <> "' maps on-duplicate => AckOk; Keiro.Router confirms the event id against the target stream before treating the duplicate as benign"+ | onDuplicate (rdDisposition dispatch) == DAckOk+ ]++-- | Reconcile per-dispatch prose with the one node-level policy the runtime+-- actually applies to a rejection-class failure group.+policyConsistency :: Name -> Loc -> PolicyChoice -> [(Name, Loc, DispatchDisposition)] -> [Diagnostic]+policyConsistency nodeName nodeLoc rejectedPolicy dispatches = contradictions ++ divergent ++ unused ++ ambiguityWarning+ where+ contradictions =+ [ mkErr (locLine dispatchLoc) PolicyContradiction $+ "dispatch '" <> command <> "' declares on-failed DeadLetter, but node '" <> nodeName <> "' does not declare rejected => deadLetter; align the dispatch story with the node-level RejectedCommandPolicy"+ | (command, dispatchLoc, disposition) <- dispatches,+ DDeadLetter _ <- [onFailed disposition],+ rejectedPolicy /= PolDeadLetter+ ]++ divergent = case dispatches of+ [] -> []+ (_, _, firstDisposition) : rest ->+ [ mkErr (locLine dispatchLoc) PolicyContradiction $+ "dispatch '" <> command <> "' has a different on-failed action from another dispatch in node '" <> nodeName <> "'; the runtime applies one RejectedCommandPolicy to the whole failure group"+ | (command, dispatchLoc, disposition) <- rest,+ not (sameFailureAction (onFailed disposition) (onFailed firstDisposition))+ ]++ unused =+ [ Diagnostic (locLine nodeLoc) Warning PolicyDeadLetterUnused [] $+ "node '" <> nodeName <> "' declares rejected => deadLetter but no dispatch on-failed arm says DeadLetter; the runtime policy is live, but the per-dispatch notation does not acknowledge it"+ | rejectedPolicy == PolDeadLetter,+ all (not . isDeadLetter . onFailed . third) dispatches+ ]++ ambiguityWarning =+ [ Diagnostic (locLine nodeLoc) Warning AmbiguousFollowsRejectedPolicy [] $+ "node '" <> nodeName <> "' acknowledges rejection-class failures; CommandAmbiguous follows the same rejected policy, and a dead-letter errorClass is the durable witness of that definition bug"+ | rejectedPolicy `elem` [PolDeadLetter, PolSkip]+ ]++ third (_, _, value) = value+ sameFailureAction DDeadLetter {} DDeadLetter {} = True+ sameFailureAction left right = left == right+ isDeadLetter DDeadLetter {} = True+ isDeadLetter _ = False++validateAggregate :: EffectiveLanguageContract -> Spec -> Aggregate -> [Diagnostic]+validateAggregate languageContract spec agg =+ concat+ [ emptyAggregate,+ duplicateMembers,+ declaredRefs,+ eventBodyRefs,+ outputMappingRules,+ registerInitialScope,+ reachability,+ terminalNoOutgoing,+ guardScope,+ clockFree,+ projectionKeyResolution,+ projectionSafety,+ statusMapTotality,+ evolutionRules,+ snapshotRules,+ replayOnlyRules,+ eventlessStateChangeRules,+ wirePolicyRules,+ fieldWireKeyRules+ ]+ where+ emptyAggregate =+ [ mkErr (locLine (aggLoc agg)) AggregateEmpty $+ "aggregate '"+ <> aggName agg+ <> "' declares "+ <> renderMissing missingAggregateParts+ <> "; scaffold cannot lower an empty aggregate -- declare at least one command, one event, and one transition"+ | not (null missingAggregateParts)+ ]+ missingAggregateParts =+ [ label+ | (isMissing, label) <-+ [ (null (aggCommands agg), "no commands"),+ (null (aggEvents agg), "no events"),+ (null (aggTransitions agg), "no transitions")+ ],+ isMissing+ ]+ renderMissing [] = ""+ renderMissing [onlyPart] = onlyPart+ renderMissing [firstPart, secondPart] = firstPart <> " and " <> secondPart+ renderMissing parts = T.intercalate ", " (init parts) <> ", and " <> last parts++ states = Set.fromList (map stName (aggStates agg))+ terminals = Set.fromList [stName s | s <- aggStates agg, stTerminal s]+ commandFields :: Map Name [Name]+ commandFields = Map.fromList [(cmdName c, map aggregateFieldName (cmdFields c)) | c <- aggCommands agg]+ commandNames = Map.keysSet commandFields+ eventNames = Set.fromList (map evName (aggEvents agg))+ enumCtorNames = Set.fromList [c | e <- specEnums spec, (c, _) <- enumCtors e]+ ruleNames = Set.fromList (map ruleName (specRules spec))+ registerNames = Set.fromList (map regName (aggRegs agg))++ eventFieldsFor event =+ case evBody event of+ EventFields fields -> fields+ EventFromCommand commandName ->+ [ field+ | command <- aggCommands agg,+ cmdName command == commandName,+ field <- cmdFields command+ ]++ fieldWireKeyRules =+ concat+ [ wireKeyRulesForRecord+ ("aggregate '" <> aggName agg <> "' command '" <> cmdName command <> "'")+ Nothing+ (map resolveAggregateFieldIdentity (cmdFields command))+ | command <- aggCommands agg+ ]+ <> concat+ [ wireKeyRulesForRecord+ ("aggregate '" <> aggName agg <> "' event '" <> evName event <> "'")+ (Just ("kind", "event envelope key"))+ (map resolveAggregateFieldIdentity (eventFieldsFor event))+ | event <- aggEvents agg+ ]++ snapshotRules = case aggSnapshot agg of+ Nothing -> []+ Just snapshot ->+ [ mkErr (locLine (snapLoc snapshot)) SnapshotIntervalInvalid $+ "aggregate '" <> aggName agg <> "': snapshot every requires an interval of at least 1; non-positive runtime intervals silently disable snapshots"+ | SnapEvery interval <- [snapPolicy snapshot],+ interval < 1+ ]+ ++ [ mkErr (locLine (snapLoc snapshot)) SnapshotCodecFixtureInvalid $+ "aggregate '" <> aggName agg <> "': snapshot state-codec version must be at least 1 and shape-hash must be non-empty"+ | snapCodecVersion snapshot < 1 || T.null (snapShapeHash snapshot)+ ]++ wirePolicyRules = case aggWire agg of+ Just wire+ | enforcesSpecSurfaceClosures languageContract,+ wireKind wire /= "ctorName" || wireFields wire /= "camelCase" ->+ [ mkErr (locLine (aggLoc agg)) WireClauseUnsupported $+ "aggregate '"+ <> aggName agg+ <> "' wire clause describes kind="+ <> wireKind wire+ <> " fields="+ <> wireFields wire+ <> "; generated bytes currently support only kind=ctorName fields=camelCase"+ ]+ _ -> []++ duplicateMembers =+ [ mkErr (locLine (cmdLoc c)) DuplicateCommandName $+ "aggregate '" <> aggName agg <> "' declares command '" <> cmdName c <> "' more than once"+ | c <- duplicatesBy cmdName (aggCommands agg)+ ]+ ++ [ mkErr (locLine (evLoc e)) DuplicateEventName $+ "aggregate '" <> aggName agg <> "' declares event '" <> evName e <> "' more than once"+ | e <- duplicatesBy evName (aggEvents agg)+ ]+ ++ [ mkErr (locLine (aggregateFieldLoc field)) AggregateDuplicateFieldName $+ "aggregate '" <> aggName agg <> "' command '" <> cmdName command <> "' declares field '" <> aggregateFieldName field <> "' more than once"+ | command <- aggCommands agg,+ field <- duplicatesBy aggregateFieldName (cmdFields command)+ ]+ ++ [ mkErr (locLine (aggregateFieldLoc field)) AggregateDuplicateFieldName $+ "aggregate '" <> aggName agg <> "' event '" <> evName event <> "' declares field '" <> aggregateFieldName field <> "' more than once"+ | event <- aggEvents agg,+ EventFields fields <- [evBody event],+ field <- duplicatesBy aggregateFieldName fields+ ]+ ++ [ mkErr (locLine (stLoc state)) AggregateDuplicateState $+ "aggregate '" <> aggName agg <> "' declares state '" <> stName state <> "' more than once"+ | state <- duplicatesBy stName (aggStates agg)+ ]+ ++ [ mkErr (locLine (regLoc register)) AggregateDuplicateRegister $+ "aggregate '" <> aggName agg <> "' declares register '" <> regName register <> "' more than once"+ | enforcesSpecSurfaceClosures languageContract,+ register <- duplicatesBy regName (aggRegs agg)+ ]+ ++ [ mkErr (locLine (tLoc transition)) TransitionDuplicateUnguarded $+ "aggregate '"+ <> aggName agg+ <> "' has more than one live unguarded transition for '"+ <> tSource transition+ <> " -- "+ <> tCommand transition+ <> "'; every matching command would be ambiguous"+ | group <- duplicateGroupsBy transitionKey unguardedTransitions,+ transition <- group+ ]+ ++ [ mkErr (locLine (tLoc guarded)) TransitionUnguardedSibling $+ "aggregate '"+ <> aggName agg+ <> "' guarded transition '"+ <> tSource guarded+ <> " -- "+ <> tCommand guarded+ <> "' overlaps an unguarded sibling at line "+ <> tInt (locLine (tLoc unguarded))+ | enforcesSpecSurfaceClosures languageContract,+ guarded <- liveTransitions,+ tGuard guarded /= Nothing,+ unguarded : _ <- [[candidate | candidate <- unguardedTransitions, transitionKey candidate == transitionKey guarded]]+ ]+ liveTransitions = [transition | transition <- aggTransitions agg, tMode transition == TmLive]+ unguardedTransitions = [transition | transition <- liveTransitions, tGuard transition == Nothing]+ transitionKey transition = (tSource transition, tCommand transition)++ eventBodyRefs =+ [ mkErr (locLine (evLoc e)) UndeclaredCommand $+ "event '" <> evName e <> "' copies fields from undeclared command '" <> command <> "'"+ | e <- aggEvents agg,+ EventFromCommand command <- [evBody e],+ command `Set.notMember` commandNames+ ]++ outputMappingRules =+ [ mkErr (locLine (tLoc transition)) EventOutputCommandMismatch $+ "transition '"+ <> tSource transition+ <> " -- "+ <> consuming+ <> "' emits event '"+ <> eventName+ <> "' declared as fields("+ <> declared+ <> "); generated identity output is legal only when the transition consumes that same command"+ | transition <- aggTransitions agg,+ (emitIndex, eventName) <- zip [1 ..] (tEmits transition),+ Left OutputCommandMismatch {declaredSourceCommand = declared, consumingTransitionCommand = consuming} <- [eventOutputMapping spec agg transition emitIndex eventName]+ ]++ registerInitialScope = concatMap checkRegisterInitial (aggRegs agg)+ checkRegisterInitial r = case [e | e <- specEnums spec, TRef (enumName e) == regType r] of+ (e : _) -> case enumBinding e of+ Just _ ->+ [ outOfScope r "declaration-owned symbol selected by" "initial"+ | regInitialBare r /= Just "initial"+ ]+ Nothing ->+ [ outOfScope r "constructor of enum" (enumName e)+ | regInitialBare r `notElem` map (Just . fst) (enumCtors e)+ ]+ []+ | regType r == TRef (aggName agg <> "Vertex") ->+ [ outOfScope r "state of aggregate" (aggName agg)+ | maybe True (`Set.notMember` states) (regInitialBare r)+ ]+ | Just identifier <- firstMatching (\declaration -> regType r == TRef (idName declaration)) (specIds spec) -> case idBinding identifier of+ Just _ ->+ [ outOfScope r "declaration-owned symbol selected by" "initial"+ | regInitialBare r /= Just "initial"+ ]+ Nothing ->+ [ outOfScope r "literal" "placeholder"+ | regInitialBare r /= Just "placeholder"+ ]+ | otherwise -> []+ outOfScope r expected domain =+ mkErr (locLine (regLoc r)) RegisterInitialOutOfScope $+ "register '" <> regName r <> "' initial '" <> renderRegInitial (regInitial r) <> "' is not a " <> expected <> " '" <> domain <> "'"+ regInitialBare r = case regInitial r of+ RegInitBare value -> Just value+ RegInitText _ -> Nothing+ renderRegInitial = \case+ RegInitBare value -> value+ RegInitText value -> value++ -- Rule 1: declared-reference for command / emit / goto / source.+ declaredRefs =+ concatMap transitionRefs (aggTransitions agg)+ transitionRefs t =+ [ mkErr (locLine (tLoc t)) UndeclaredCommand $+ "transition references undeclared command '" <> tCommand t <> "'"+ | not (tCommand t `Set.member` commandNames)+ ]+ ++ [ mkErr (locLine (tLoc t)) UndeclaredState $+ "transition source '" <> tSource t <> "' is not a declared state"+ | not (tSource t `Set.member` states)+ ]+ ++ [ mkErr (locLine (tLoc t)) UndeclaredState $+ "transition goto '" <> tGoto t <> "' is not a declared state"+ | not (tGoto t `Set.member` states)+ ]+ ++ [ mkErr (locLine (tLoc t)) UndeclaredEvent $+ "emit references undeclared event '" <> ev <> "'"+ | ev <- tEmits t,+ not (ev `Set.member` eventNames)+ ]++ -- Rule 2: reachability of every non-terminal state from the initial state+ -- (the first state in the list).+ reachability = case map stName (aggStates agg) of+ [] -> []+ (initial : _) ->+ let reached = bfs (Set.singleton initial) [initial]+ in [ mkErr (locLine (stLoc s)) UnreachableState $+ "state '" <> stName s <> "' is not reachable from the initial state '" <> initial <> "'"+ | s <- aggStates agg,+ not (stTerminal s),+ not (stName s `Set.member` reached)+ ]+ edgesFrom src = [tGoto t | t <- aggTransitions agg, tSource t == src]+ bfs seen [] = seen+ bfs seen (x : xs) =+ let nexts = [n | n <- edgesFrom x, not (n `Set.member` seen)]+ in bfs (foldr Set.insert seen nexts) (xs ++ nexts)++ -- Rule 3: a terminal state has no outgoing transition.+ terminalNoOutgoing =+ [ mkErr (locLine (tLoc t)) TerminalHasOutgoing $+ "terminal state '" <> tSource t <> "' has an outgoing transition"+ | t <- aggTransitions agg,+ tSource t `Set.member` terminals+ ]++ -- Rule 4: every atom in a guard or write Expr resolves to a register, a+ -- field of the transition's command, an enum constructor, a rule, or a bool.+ guardScope = concatMap transitionScope (aggTransitions agg)+ transitionScope t =+ let inScope =+ registerNames+ `Set.union` Set.fromList (Map.findWithDefault [] (tCommand t) commandFields)+ `Set.union` enumCtorNames+ `Set.union` ruleNames+ -- State names are constructors of the implicit vertex enum, so a+ -- @write reservationState := Held@ references a state legitimately.+ `Set.union` states+ exprs = maybe [] pure (tGuard t) ++ map snd (tWrites t)+ badAtoms =+ [ n+ | e <- exprs,+ n <- exprNames e,+ not (n `Set.member` clockAtoms), -- clock atoms reported separately+ not (n `Set.member` inScope)+ ]+ badTargets = [target | (target, _) <- tWrites t, target `Set.notMember` registerNames]+ in [ mkErr (locLine (tLoc t)) WriteTargetNotRegister $+ "write target '" <> target <> "' is not a register of aggregate '" <> aggName agg <> "'"+ | target <- dedup badTargets+ ]+ ++ [ mkErr (locLine (tLoc t)) GuardAtomOutOfScope $+ "atom '" <> n <> "' in transition '" <> tSource t <> " -- " <> tCommand t <> "' resolves to no register, command field, enum constructor, or rule"+ | n <- dedup badAtoms+ ]++ -- Rule 5 (cross-cutting): no guard or write Expr samples a wall clock.+ clockFree = concatMap transitionClock (aggTransitions agg)+ transitionClock t =+ let exprs = maybe [] pure (tGuard t) ++ map snd (tWrites t)+ sampled = [n | e <- exprs, n <- exprNames e, n `Set.member` clockAtoms]+ in [ mkErr (locLine (tLoc t)) ClockSampled $+ "transition '" <> tSource t <> " -- " <> tCommand t <> "' samples the wall clock via '" <> n <> "'; time must be an injected input field, not sampled"+ | n <- dedup sampled+ ]++ -- EP-107: a projection references a first-class read model when one exists.+ -- Legacy standalone projections remain legal, but are surfaced as warnings.+ projectionKeyResolution =+ [ mkErr (locLine (projLoc projection)) AggProjectionKeyUnresolved $+ "projection '" <> projTable projection <> "' key '" <> projKey projection <> "' is not a register, command field, or event field of aggregate '" <> aggName agg <> "'"+ | enforcesSpecSurfaceClosures languageContract,+ Just projection <- [aggProjection agg],+ projKey projection `Set.notMember` projectionFields+ ]+ projectionFields =+ registerNames+ `Set.union` Set.fromList [fieldDslName (resolveAggregateFieldIdentity field) | command <- aggCommands agg, field <- cmdFields command]+ `Set.union` Set.fromList [fieldDslName (resolveAggregateFieldIdentity field) | event <- aggEvents agg, field <- eventFieldsFor event]++ projectionSafety = case aggProjection agg of+ Nothing -> []+ Just projection -> case [readModel | NReadModel readModel <- specNodes spec, rmName readModel == projTable projection] of+ [] ->+ [ mkErr (locLine (projLoc projection)) RmStrongInlineOnly $+ "projection '" <> projTable projection <> "' declares consistency = Strong but has no readmodel node; a standalone projection is inline-only and has no subscription cursor"+ | projConsistency projection == Just Strong+ ]+ ++ [ Diagnostic+ { line = locLine (projLoc projection),+ severity = Warning,+ code = RmProjectionWithoutNode,+ relatedLocations = [],+ message = "projection '" <> projTable projection <> "' has no readmodel node; registration, schema identity, consistency, and rebuild helpers are unavailable"+ }+ ]+ (readModel : _) ->+ [ mkErr (locLine (projLoc projection)) RmConsistencyConflict $+ "projection '" <> projTable projection <> "' declares consistency " <> T.pack (show projectionConsistency) <> " but its readmodel node declares " <> T.pack (show (rmConsistency readModel))+ | Just projectionConsistency <- [projConsistency projection],+ projectionConsistency /= rmConsistency readModel+ ]++ -- Rule 6 (hole-kind 3, mapping): keys are exact event names, never suffixes;+ -- duplicates and dangling keys are errors, and non-partial maps are total.+ statusMapTotality = case aggProjection agg of+ Nothing -> []+ Just p ->+ let evs = map evName (aggEvents agg)+ pairs = maybe [] mapPairs (projStatusMap p)+ keys = map fst pairs+ partial = maybe False mapPartial (projStatusMap p)+ uncovered = [event | event <- evs, event `notElem` keys]+ dangling = [key | key <- keys, key `notElem` evs]+ duplicateKeys = map fst (duplicatesBy fst pairs)+ in [ mkErr (locLine (projLoc p)) StatusMapDanglingKey $+ "projection '" <> projTable p <> "' status-map key '" <> key <> "' is not an event name of aggregate '" <> aggName agg <> "'"+ | key <- dangling+ ]+ ++ [ mkErr (locLine (projLoc p)) StatusMapDuplicateKey $+ "projection '" <> projTable p <> "' repeats status-map key '" <> key <> "'"+ | key <- duplicateKeys+ ]+ ++ [ mkErr (locLine (projLoc p)) StatusMapNotTotal $+ "projection '" <> projTable p <> "' status-map is not total over events {" <> T.intercalate ", " uncovered <> "}"+ | not partial,+ not (null evs),+ not (null uncovered)+ ]++ -- EP-2 evolution rules (single-spec; the diff path adds the cross-spec ones).+ evolutionRules =+ versionUpcasterRule+ ++ 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 =+ [ mkErr (locLine (evLoc e)) EvtVersionMissingUpcaster $+ "event '" <> evName e <> "' version " <> tInt (evVersion e) <> " has no 'upcast from v" <> tInt (evVersion e - 1) <> "' clause"+ | e <- aggEvents agg,+ evVersion e > 1,+ maybe True ((/= evVersion e - 1) . fst) (evUpcastFrom e)+ ]++ -- 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` 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,+ relatedLocations = [],+ 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,+ relatedLocations = [],+ 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,+ relatedLocations = [],+ 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+ | wireSchemaVersion w /= maxEventVersion ->+ [ Diagnostic+ { line = locLine (aggLoc agg),+ severity = Warning,+ code = WireSchemaVersionMismatch,+ relatedLocations = [],+ message =+ "wire schemaVersion=" <> tInt (wireSchemaVersion w) <> " does not match the maximum event version " <> tInt maxEventVersion+ }+ ]+ _ -> []++ -- 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,+ relatedLocations = [],+ 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))+ ]++ eventlessStateChangeRules =+ [ mkErr (locLine (tLoc transition)) AggregateEventlessStateChange $+ "transition '"+ <> tSource transition+ <> " -- "+ <> tCommand transition+ <> "' emits no event but changes "+ <> changeDescription transition+ <> "; event-sourced state changes require persisted evidence, while a no-op must keep both vertex and registers unchanged"+ | transition <- aggTransitions agg,+ null (tEmits transition),+ tSource transition /= tGoto transition || not (null (tWrites transition))+ ]+ changeDescription transition+ | tSource transition /= tGoto transition && not (null (tWrites transition)) = "the target vertex and registers"+ | tSource transition /= tGoto transition = "the target vertex"+ | otherwise = "registers"++-- | The validator's re-derivation of the live+-- 'Keiro.PGMQ.Runtime.queueRef' trio: physical queue, dead-letter queue, and+-- PGMQ backing table. Parity is pinned by the queue-runtime conformance suite.+derivedQueueTrio :: Text -> (Text, Text, Text)+derivedQueueTrio logical = (physical, physical <> "_dlq", "pgmq.q_" <> physical)+ where+ physical = physicalBase logical++physicalBase :: Text -> Text+physicalBase logical+ | T.length base <= 43 && not ("_dlq" `T.isSuffixOf` base) = base+ | otherwise = hashedBase logical base+ where+ base = sanitizeQueueName logical++sanitizeQueueName :: Text -> Text+sanitizeQueueName =+ ensureLeadingLetter+ . T.intercalate "_"+ . filter (not . T.null)+ . T.splitOn "_"+ . T.map toLegal+ . T.toLower+ where+ toLegal c+ | (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' = c+ | otherwise = '_'+ ensureLeadingLetter value = case T.uncons value of+ Nothing -> "q"+ Just (c, _)+ | c >= 'a' && c <= 'z' -> value+ | otherwise -> T.cons 'q' value++hashedBase :: Text -> Text -> Text+hashedBase logical base = prefix <> "_" <> fnv1a64Hex logical+ where+ trimmedPrefix = T.dropWhileEnd (== '_') (T.take 26 base)+ prefix+ | T.null trimmedPrefix = "q"+ | otherwise = trimmedPrefix++fnv1a64Hex :: Text -> Text+fnv1a64Hex logical = T.pack (replicate (16 - length rendered) '0' <> rendered)+ where+ rendered = showHex (T.foldl' step offset logical) ""+ offset :: Word64+ offset = 0xcbf29ce484222325+ prime :: Word64+ prime = 0x100000001b3+ step hash character = (hash `xor` fromIntegral (ord character)) * prime++tInt :: Int -> Text+tInt = T.pack . show++mkErr :: Int -> DiagnosticCode -> Text -> Diagnostic+mkErr l c m = Diagnostic {line = l, severity = Error, code = c, relatedLocations = [], message = m}++-- | A dispatch disposition as the author spelled it.+dispText :: Disp -> Text+dispText DAckOk = "AckOk"+dispText DRetry = "Retry"+dispText (DDeadLetter reason) = "DeadLetter " <> T.pack (show reason)++-- | A spec surface the grammar accepts but no runtime implements.+--+-- Released languages below 4 keep their acceptance and only warn, so an+-- existing source does not stop checking when it is pinned to an older+-- language. From language 4 — which promised strict spec-surface validation —+-- the same sentence is an error. The message never changes with the severity, so+-- an author reads one explanation before and after the tightening.+mkSurfaceRefusal :: EffectiveLanguageContract -> Int -> DiagnosticCode -> Text -> Diagnostic+mkSurfaceRefusal languageContract l c m =+ Diagnostic+ { line = l,+ severity = if enforcesSpecSurfaceClosures languageContract then Error else Warning,+ code = c,+ relatedLocations = [],+ message = m+ }++wireKeyRulesForRecord :: Text -> Maybe (Text, Text) -> [ResolvedFieldIdentity] -> [Diagnostic]+wireKeyRulesForRecord owner reservedKey fields = invalidKeys <> duplicateKeys <> reservedCollisions+ where+ -- Structural safety only. An alias exists to preserve a brownfield key that+ -- the current naming convention would reject, so checking alias *style*+ -- would defeat the feature. What is checked is that the key can be a key at+ -- all: a stray space or control character in `as "family "` ships a+ -- permanently mis-keyed public field that no later rename can fix without a+ -- wire break. See ADR 0021.+ invalidKeys = concatMap invalidKeyRule fields+ invalidKeyRule field+ | T.null key = [refuse "resolves to an empty wire key"]+ | key /= T.strip key =+ [ refuse+ ( "resolves to wire key "+ <> T.pack (show key)+ <> ", which has leading or trailing whitespace; the wire key is the exact bytes on the wire, so the surrounding space would be part of every encoded field name"+ )+ ]+ | Just offending <- firstMatching isControl (T.unpack key) =+ [ refuse+ ( "resolves to wire key "+ <> T.pack (show key)+ <> ", which contains the control character U+"+ <> T.justifyRight 4 '0' (T.toUpper (T.pack (showHex (ord offending) "")))+ )+ ]+ | otherwise = []+ where+ key = fieldWireKey field+ refuse detail =+ mkErr (locLine (fieldLoc field)) FieldWireKeyInvalid $+ owner <> " field '" <> fieldDslName field <> "' " <> detail+ duplicateKeys =+ [ Diagnostic+ { line = locLine (fieldLoc field),+ severity = Error,+ code = FieldWireKeyCollision,+ relatedLocations = [(locLine (fieldLoc earlier), "wire key '" <> fieldWireKey field <> "' is first declared here")],+ message = owner <> " fields resolve to duplicate wire key '" <> fieldWireKey field <> "'"+ }+ | (index, field) <- zip [0 :: Int ..] fields,+ earlier : _ <- [[candidate | candidate <- take index fields, fieldWireKey candidate == fieldWireKey field]]+ ]+ reservedCollisions =+ [ mkErr (locLine (fieldLoc field)) FieldWireKeyCollision $+ owner+ <> " field '"+ <> fieldDslName field+ <> "' resolves to wire key '"+ <> key+ <> "', which collides with the "+ <> description+ | Just (key, description) <- [reservedKey],+ field <- fields,+ fieldWireKey field == key+ ] locLine :: Loc -> Int locLine = unLoc
src/Keiro/Dsl/Workspace.hs view
@@ -104,7 +104,7 @@ import Keiro.Dsl.Parser (ParseError, ParseFailure (..), parseSource, renderParseFailure) import Keiro.Dsl.RuntimePackage (RuntimePackageName (..), mkRuntimePackageName) import Keiro.Dsl.Scaffold (Context (..))-import Keiro.Dsl.ScaffoldRun (Refusal (..), planServiceScaffoldWithRuntimePackageAndGoldens)+import Keiro.Dsl.ScaffoldRun (Refusal (..), originLine, planServiceScaffoldWithRuntimePackageAndGoldens, planningRefusalDiagnostics) import Keiro.Dsl.SemanticContract (CheckedService (..), EffectiveLanguageContract, checkedSource, effectiveLanguageContract) import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), nodeIdentity, validateService) import System.Directory (doesFileExist)@@ -112,7 +112,6 @@ import Text.Megaparsec hiding (ParseError) import Text.Megaparsec.Char (char, space1) import Text.Megaparsec.Char.Lexer qualified as L-import Text.Read (readMaybe) -- | A parsed workspace manifest. Members are held in canonical order -- (codepoint-sorted normalized paths), so two manifests that list the same@@ -741,7 +740,11 @@ checkWorkspace :: WorkspaceSpec -> [WorkspaceDiagnostic] checkWorkspace workspace = [ WorkspaceDiagnostic- { wdLocations = pure (locationFor (line diagnostic)),+ { wdLocations =+ locationFor (line diagnostic)+ :| [ (locationFor noteLine) {wlRole = note}+ | (noteLine, note) <- relatedLocations diagnostic+ ], wdSeverity = severity diagnostic, wdCode = code diagnostic, wdSourceLanguageCause = Nothing,@@ -1058,10 +1061,28 @@ -- Only ask the scaffold planner about a spec that already validates. -- An invalid merged spec is 'checkWorkspace''s report to make, and the -- planner is only designed to see specs that passed validation.- | any ((== Error) . severity) (validateService (checkedWorkspace composed)) = []+ | any blocksCollisionPlanning (validateService (checkedWorkspace composed)) = [] | otherwise = case planServiceScaffoldWithRuntimePackageAndGoldens [] (wmfRuntimePackage manifest) plannerContext (checkedWorkspace composed) of Right _ -> []- Left plannerRefusals -> concatMap crossMemberCollision plannerRefusals+ Left plannerRefusals -> concatMap planningRefusal plannerRefusals+ planningRefusal refusal = case crossMemberCollision refusal of+ [] -> map liftPlanningDiagnostic (planningRefusalDiagnostics [refusal])+ workspaceDiagnostics -> workspaceDiagnostics+ liftPlanningDiagnostic diagnostic =+ WorkspaceDiagnostic+ { wdLocations =+ planningLocation (line diagnostic) ""+ :| [ planningLocation noteLine note+ | (noteLine, note) <- relatedLocations diagnostic+ ],+ wdSeverity = severity diagnostic,+ wdCode = code diagnostic,+ wdSourceLanguageCause = Nothing,+ wdMessage = message diagnostic+ }+ planningLocation mergedLine role = case lookupLine mergedLine of+ Just (owner, original) -> WorkspaceLocation (WorkspaceMemberFile owner) original role+ Nothing -> WorkspaceLocation WorkspaceManifestFile (max 1 mergedLine) role crossMemberCollision (PathCollision path origins) = [ WorkspaceDiagnostic { wdLocations =@@ -1087,6 +1108,9 @@ Just (owner, original) <- [lookupLine mergedLine] ] crossMemberCollision _ = []+ blocksCollisionPlanning diagnostic =+ severity diagnostic == Error+ && code diagnostic /= GeneratedOccurrenceCollision lookupLine n = listToMaybe [ (path, n - low)@@ -1179,17 +1203,6 @@ (leading : _) <- [T.words (T.takeWhile (/= '#') raw)], leading == clauseKeyword ]---- | The merged-spec line embedded in a scaffold module's origin string, which--- "Keiro.Dsl.Scaffold" formats as @\<kind\> \<name\> (line N)@. Context-level--- modules carry no line and yield 'Nothing', which is correct: they belong to the--- workspace, not to any one member, so they can never be a cross-member--- collision.-originLine :: Text -> Maybe Int-originLine origin = do- withoutClose <- T.stripSuffix ")" origin- let (before, after) = T.breakOnEnd " (line " withoutClose- if T.null before then Nothing else readMaybe (T.unpack after) -------------------------------------------------------------------------------- -- Loading
src/Keiro/Dsl/WorkspaceAdoption.hs view
@@ -48,8 +48,9 @@ import Data.Text qualified as T import Data.Text.IO qualified as TIO import Keiro.Dsl.Scaffold (ModuleKind (..), ScaffoldModule (..), isGeneratedBannerLine)-import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName)+import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord) import Keiro.Dsl.ScaffoldRun (StaleGeneratedEvidence (..), StaleModule (..), staleAgainst)+import Keiro.Dsl.SidecarNames (contextLedgerFileName, legacyContextRecordFileName) import Keiro.Dsl.WorkspaceRecord (AdoptedRow (..), supersededByLine, workspaceMigrationReportFileName) import System.Directory (doesDirectoryExist, doesFileExist, listDirectory) import System.FilePath ((</>))@@ -89,9 +90,11 @@ -- or marked. adoptionReport :: FilePath -> Text -> Text -> [ScaffoldModule] -> IO (Maybe MigrationReport) adoptionReport out context service modules = do- legacy <- readLegacyRecord (out </> legacyName)+ ledger <- readContextLedger out context present <- Set.fromList <$> outputTreeFiles out- let plannedGenerated = [modulePath m | m <- modules, kind m == Generated]+ let legacyName = maybe (contextLedgerFileName context) fst ledger+ legacy = snd <$> ledger+ plannedGenerated = [modulePath m | m <- modules, kind m == Generated] plannedAll = Set.fromList (map modulePath modules) onDisk path = path `Set.member` present @@ -147,8 +150,6 @@ if null claimed && null likelyStale && null unclaimed && isNothing legacy then Nothing else Just report- where- legacyName = recordFileName context -- | The record rows an adopting run adds to the new workspace record. adoptedRows :: MigrationReport -> [AdoptedRow]@@ -169,11 +170,11 @@ -- as it is, so a re-run after an interrupted adoption cannot accumulate markers. markLegacyRecordSuperseded :: FilePath -> Text -> Text -> IO () markLegacyRecordSuperseded out context service = do- let path = out </> recordFileName context- exists <- doesFileExist path- if not exists- then pure ()- else do+ ledgerPath <- existingContextLedgerPath out context+ case ledgerPath of+ Nothing -> pure ()+ Just relativePath -> do+ let path = out </> relativePath contents <- TIO.readFile path let marker = supersededByLine service if marker `elem` T.lines contents@@ -200,10 +201,23 @@ then walk child else pure [child | ".hs" `T.isSuffixOf` T.pack child] -readLegacyRecord :: FilePath -> IO (Maybe ScaffoldRecord)-readLegacyRecord path = do- exists <- doesFileExist path- if exists then parseRecord <$> TIO.readFile path else pure Nothing+readContextLedger :: FilePath -> Text -> IO (Maybe (FilePath, ScaffoldRecord))+readContextLedger out context = do+ relativePath <- existingContextLedgerPath out context+ case relativePath of+ Nothing -> pure Nothing+ Just path -> fmap ((,) path) . parseRecord <$> TIO.readFile (out </> path)++existingContextLedgerPath :: FilePath -> Text -> IO (Maybe FilePath)+existingContextLedgerPath out context = do+ let current = contextLedgerFileName context+ legacy = legacyContextRecordFileName context+ currentExists <- doesFileExist (out </> current)+ if currentExists+ then pure (Just current)+ else do+ legacyExists <- doesFileExist (out </> legacy)+ pure (if legacyExists then Just legacy else Nothing) hasGeneratedBanner :: FilePath -> IO Bool hasGeneratedBanner path = do
src/Keiro/Dsl/WorkspaceRecord.hs view
@@ -6,15 +6,13 @@ -- emitted module__. The last one is what makes moving an aggregate from one member -- file to another an ownership move rather than a stale/new pair. ----- __Coexistence.__ Workspace history is keyed by the service name in a distinct--- file-name slot, @keiro-dsl-scaffold-record.workspace.\<service\>.txt@, and never--- by context. A context name is lexed as letters, digits, @_@ and @-@ and can--- never contain a dot, so this slot provably cannot collide with a legacy--- context-keyed name even when a service is named after its context. Legacy--- records and a workspace record may therefore share one output directory: the--- workspace path never writes a context-keyed name, and an older keiro-dsl binary--- is structurally incapable of parsing — and therefore of clobbering — workspace--- history. The one exception is the explicit adoption step, which /appends/ a+-- __Coexistence.__ Workspace history is keyed by the service name in the explicit+-- @keiro-dsl-ledger.workspace.\<service\>.txt@ slot, while standalone history uses+-- @keiro-dsl-ledger.context.\<context\>.txt@. The distinct literal slot segments+-- make collision impossible by construction, including for a context literally+-- named @workspace@. Legacy records can coexist during adoption because their+-- @keiro-dsl-scaffold-record.*@ stem is distinct. The one exception to ordinary+-- workspace isolation is the explicit adoption step, which /appends/ a -- @superseded-by:@ line to a legacy record; the v1 parser ignores unknown lines, -- so old binaries still read it. --@@ -33,7 +31,7 @@ -- module {"kind":"generated","path":"Demo/Project/Project/Generated/Domain.hs","owner":"domain/project.keiro"} -- mapping {…} -- binding {…}--- adopted {"path":"…","evidence":"record","source":"keiro-dsl-scaffold-record.demo-project.txt"}+-- adopted {"path":"…","evidence":"record","source":"keiro-dsl-ledger.context.demo-project.txt"} -- @ -- -- @module@ rows are canonical single-line JSON, following the precedent set for@@ -66,10 +64,12 @@ import Data.Text.Encoding qualified as Text import Keiro.Dsl.BehaviorCoverage (BehaviorRecordRow (..)) import Keiro.Dsl.ExplainBindings (BindingHole (..))+import Keiro.Dsl.HaskellName (GeneratedHaskellNamingEdition (..), parseGeneratedHaskellNamingEdition, renderGeneratedHaskellNamingEdition) import Keiro.Dsl.LanguageVersion (SourceLanguage (..), declaredLanguageVersionMaybe, effectiveLanguageVersion, sourceFormText) import Keiro.Dsl.MappedConsumer (MappingIdentity (..))-import Keiro.Dsl.Scaffold (ModuleKind (..))+import Keiro.Dsl.Scaffold (ModuleKind (..), ModuleRole (..)) import Keiro.Dsl.SemanticContract (EffectiveLanguageContract, effectiveLanguageContract)+import Keiro.Dsl.SidecarNames qualified as SidecarNames import System.FilePath (isAbsolute, splitDirectories) -- | One emitted module: what kind it is, where it landed relative to the output@@ -78,7 +78,8 @@ data WorkspaceModuleRow = WorkspaceModuleRow { wrmKind :: !ModuleKind, wrmPath :: !FilePath,- wrmOwner :: !(Maybe FilePath)+ wrmOwner :: !(Maybe FilePath),+ wrmRole :: !(Maybe ModuleRole) } deriving stock (Eq, Show) @@ -89,6 +90,9 @@ "path" .= T.pack (wrmPath row) ] <> ["owner" .= T.pack owner | Just owner <- [wrmOwner row]]+ <> ["roleOwnerKind" .= roleOwnerKind role | Just role <- [wrmRole row]]+ <> ["roleOwnerName" .= roleOwnerName role | Just role <- [wrmRole row]]+ <> ["roleFamily" .= roleFamily role | Just role <- [wrmRole row]] instance FromJSON WorkspaceModuleRow where parseJSON = withObject "WorkspaceModuleRow" $ \fields -> do@@ -99,11 +103,19 @@ other -> fail ("unknown module kind: " <> T.unpack other) path <- fields .: "path" owner <- fields .:? "owner"+ roleOwnerKindValue <- fields .:? "roleOwnerKind"+ roleOwnerNameValue <- fields .:? "roleOwnerName"+ roleFamilyValue <- fields .:? "roleFamily"+ moduleRoleValue <- case (roleOwnerKindValue, roleOwnerNameValue, roleFamilyValue) of+ (Nothing, Nothing, Nothing) -> pure Nothing+ (Just ownerKind, Just ownerName, Just family) -> pure (Just (ModuleRole ownerKind ownerName family))+ _ -> fail "module role fields must be all present or all absent" pure WorkspaceModuleRow { wrmKind = moduleKind, wrmPath = T.unpack (path :: Text),- wrmOwner = T.unpack <$> (owner :: Maybe Text)+ wrmOwner = T.unpack <$> (owner :: Maybe Text),+ wrmRole = moduleRoleValue } -- | One member's source-language provenance in a workspace record.@@ -187,6 +199,7 @@ wrMembers :: ![FilePath], wrSourceLanguages :: ![WorkspaceSourceLanguageRow], wrLanguageContract :: !EffectiveLanguageContract,+ wrNamingEdition :: !GeneratedHaskellNamingEdition, wrModules :: ![WorkspaceModuleRow], wrMappings :: ![MappingIdentity], wrIdDomains :: ![Text],@@ -208,7 +221,8 @@ "manifest: " <> wrManifest record, "context: " <> wrContext record, "module-root: " <> rootLabel,- "layout: " <> wrLayout record+ "layout: " <> wrLayout record,+ "naming-edition " <> renderGeneratedHaskellNamingEdition (wrNamingEdition record) ] <> ["member " <> T.pack path | path <- wrMembers record] <> ["source-language " <> encodeRow row | row <- wrSourceLanguages record]@@ -241,6 +255,7 @@ members <- traverse safePath [path | row <- rows, Just path <- [T.stripPrefix "member " row]] sourceLanguages <- parseSourceLanguages members rows languageContract <- parseLanguageContract sourceLanguages rows+ namingEdition <- parseNamingEdition rows modules <- traverse (decodeRow "module ") (rowsWith "module " rows) checkedModules <- traverse checkedModule modules ordinaryMappings <- traverse (decodeRow "mapping ") (rowsWith "mapping " rows)@@ -271,6 +286,7 @@ wrMembers = members, wrSourceLanguages = sourceLanguages, wrLanguageContract = languageContract,+ wrNamingEdition = namingEdition, wrModules = checkedModules, wrMappings = mappings, wrIdDomains = idDomains,@@ -315,6 +331,10 @@ contract <- decodeRow "semantic-contract " row if contract == common then Just contract else Nothing _ -> Nothing+ parseNamingEdition rows = case rowsWith "naming-edition " rows of+ [] -> Just LegacyNamingV1+ [row] -> T.stripPrefix "naming-edition " row >>= parseGeneratedHaskellNamingEdition+ _ -> Nothing checkedSourceLanguage row = do path <- safePath (T.pack (wrslPath row)) pure row {wrslPath = path}@@ -333,20 +353,20 @@ holePath hole ) --- | @keiro-dsl-scaffold-record.workspace.\<service\>.txt@ — the workspace--- history file. See the module header for why the @workspace.@ slot cannot--- collide with a context-keyed name.+-- | @keiro-dsl-ledger.workspace.\<service\>.txt@ — the machine-owned workspace+-- history ledger. workspaceRecordFileName :: Text -> FilePath-workspaceRecordFileName service = "keiro-dsl-scaffold-record.workspace." <> T.unpack service <> ".txt"+workspaceRecordFileName = SidecarNames.workspaceLedgerFileName --- | @keiro-dsl-manifest.workspace.\<service\>.txt@ — the Cabal build manifest.+-- | @keiro-dsl-cabal-fragment.workspace.\<service\>.txt@ — the human-facing+-- Cabal fragment. Kept under the historical helper name for API compatibility. workspaceManifestFileName :: Text -> FilePath-workspaceManifestFileName service = "keiro-dsl-manifest.workspace." <> T.unpack service <> ".txt"+workspaceManifestFileName = SidecarNames.workspaceCabalFragmentFileName -- | @keiro-dsl-migration-report.workspace.\<service\>.txt@ — the durable review -- artifact written once, on the run that adopts pre-workspace scaffold output. workspaceMigrationReportFileName :: Text -> FilePath-workspaceMigrationReportFileName service = "keiro-dsl-migration-report.workspace." <> T.unpack service <> ".txt"+workspaceMigrationReportFileName = SidecarNames.workspaceMigrationReportFileName -- | The single line adoption appends to a superseded legacy record. The v1 -- parser ignores unknown lines, so the legacy record keeps parsing for old
src/Keiro/Dsl/WorkspaceScaffold.hs view
@@ -45,11 +45,13 @@ WorkspaceSourceLanguageDrift (..), WorkspaceScaffoldReport (..), executeWorkspaceScaffold,+ executeWorkspaceScaffoldWithNameMigrations, renderWorkspaceScaffoldReport, ) where import Data.List (nub, sortOn)+import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T@@ -65,10 +67,11 @@ renderConformancePackageReport, ) import Keiro.Dsl.ExplainBindings (BindingHole (..), bindingHolesForService)-import Keiro.Dsl.FoldFingerprint (aggregateFoldSurfaceForService) import Keiro.Dsl.Goldens (GoldenPayload) import Keiro.Dsl.Grammar import Keiro.Dsl.Harness (harnessForServiceWithGoldens, harnessProcess, harnessReadModel, harnessRouter, harnessWorkflow)+import Keiro.Dsl.HaskellName (currentGeneratedHaskellNamingEdition)+import Keiro.Dsl.HaskellSourceMove (SourceMove (..), SourceMoveError, planSourceMoves) import Keiro.Dsl.IdDomain (idDomainIdentitiesForService) import Keiro.Dsl.LanguageVersion (SourceLanguage, effectiveLanguageVersion, languageVersionText, sourceFormText) import Keiro.Dsl.Manifest (moduleNameOf, renderManifestForServiceWithFacade)@@ -78,22 +81,30 @@ import Keiro.Dsl.Scaffold import Keiro.Dsl.ScaffoldRun ( MappingDrift (..),+ PreparedSourceMove, Refusal (..), StaleGeneratedEvidence (..), StaleModule (..), WriteDisposition (..),+ applyPreparedSourceMoves, behaviorDrift, constraintPlan,+ inertNodesOf, mappingDrift, missingGeneratedBanners, newBindingObligations, obligationKindLabel,- pureRefusals,+ planningGatePipeline,+ preflightSourceMoves,+ preparedSourceMove,+ renderInertNodeSection, renderMappingIdentity, staleAgainst,+ withSidecarMovesApplied, ) import Keiro.Dsl.SemanticContract (CheckedService (..)) import Keiro.Dsl.ServiceHarness (DuplicateServiceFactKey, serviceConformanceModuleName, serviceHarnessModule)+import Keiro.Dsl.SidecarMigration import Keiro.Dsl.Validate (nodeIdentity) import Keiro.Dsl.Workspace (WorkspaceMember (..), WorkspaceSpec (..), checkedWorkspace, declarationOwner, nodeOwner) import Keiro.Dsl.WorkspaceAdoption (MigrationReport (..), adoptedRows, adoptionReport, markLegacyRecordSuperseded, renderMigrationReport)@@ -162,14 +173,14 @@ Context -> WorkspaceSpec -> Either [Refusal] WorkspacePlan-planWorkspaceScaffoldWithRuntimePackageAndGoldens goldens runtimePackage goldenRoot ctx workspace = case workspaceModules goldens runtimePackage ctx workspace of- Left duplicates -> Left [DuplicateConformanceFactKeys duplicates]- Right tagged -> case packagePlan of- Left failures -> Left (map ConformancePackageRefusal failures)- Right plannedPackage -> case traverse (aggregateFoldSurfaceForService service) [aggregate | NAggregate aggregate <- specNodes merged] of- Left surfaceError -> Left [FoldSurfaceRefusal surfaceError]- Right _ -> case pureRefusals ctx merged (map fst tagged) of- [] ->+planWorkspaceScaffoldWithRuntimePackageAndGoldens goldens runtimePackage goldenRoot ctx workspace =+ case planningGatePipeline ctx service modulePlan packageGate of+ Left refusals -> Left refusals+ Right _ -> case taggedModules of+ Left duplicates -> Left [DuplicateConformanceFactKeys duplicates]+ Right tagged -> case packagePlan of+ Left failures -> Left (map ConformancePackageRefusal failures)+ Right plannedPackage -> Right WorkspacePlan { wpWorkspace = workspace,@@ -180,14 +191,19 @@ wpGoldenRoot = goldenRoot, wpModules = tagged }- refusals -> Left refusals where service = checkedWorkspace workspace- merged = checkedSpec service+ taggedModules = workspaceModules goldens runtimePackage ctx workspace+ modulePlan = case taggedModules of+ Left duplicates -> Left [DuplicateConformanceFactKeys duplicates]+ Right tagged -> Right (map fst tagged) packagePlan = traverse (\packageName -> planConformancePackage (WorkspaceConformanceService (wsService workspace)) packageName (serviceConformanceModuleName ctx) service) runtimePackage+ packageGate = case packagePlan of+ Left failures -> Left (map ConformancePackageRefusal failures)+ Right _ -> Right () -- | The tagged module set, in exactly the order -- 'Keiro.Dsl.ScaffoldRun.scaffoldModulesWithGoldens' produces for the merged spec.@@ -361,7 +377,13 @@ wsrAddedBehavior :: ![BehaviorRecordRow], wsrRemovedBehavior :: ![BehaviorRecordRow], wsrObsoleteOutputHooks :: ![(Text, Text)],+ -- | Declarations in the merged service that produce no generated module.+ -- Same content, same rendering as the single-spec+ -- 'Keiro.Dsl.ScaffoldRun.reportInertNodes'.+ wsrInertNodes :: ![(Text, Text)], wsrConformancePackage :: !(Maybe ConformancePackageReport),+ wsrNameMoves :: ![SourceMove],+ wsrSidecarMoves :: ![SidecarMove], -- | Present only on the run that adopted pre-workspace scaffold output. wsrMigration :: !(Maybe MigrationReport) }@@ -387,7 +409,56 @@ -- not rewritten, which is what makes idempotence observable rather than -- merely claimed. executeWorkspaceScaffold :: FilePath -> Bool -> WorkspacePlan -> IO (Either [Refusal] WorkspaceScaffoldReport)-executeWorkspaceScaffold out forceGeneratedOverwrite plan = do+executeWorkspaceScaffold out forceGeneratedOverwrite =+ executeWorkspaceScaffoldWithNameMigrations out forceGeneratedOverwrite False++executeWorkspaceScaffoldWithNameMigrations :: FilePath -> Bool -> Bool -> WorkspacePlan -> IO (Either [Refusal] WorkspaceScaffoldReport)+executeWorkspaceScaffoldWithNameMigrations out forceGeneratedOverwrite applyNameMigrations plan = do+ sidecarResult <- planSidecarMigrations out (WorkspaceSidecars (wsService (wpWorkspace plan))) (wpConformancePackage plan)+ case sidecarResult of+ Left reasons -> pure (Left [SidecarMigrationRefusal reasons])+ Right preparedSidecars+ | not (null preparedSidecars) && not applyNameMigrations ->+ pure (Left [SidecarMigrationRequired (map preparedSidecarMove preparedSidecars)])+ | otherwise -> do+ applyPreparedSidecarMoves out preparedSidecars+ -- Past this point the renames are on disk, so a later refusal's+ -- "nothing was written" needs qualifying. Mirrors the single-spec path.+ let sidecarMoves = map preparedSidecarMove preparedSidecars+ noteApplied = withSidecarMovesApplied sidecarMoves+ previous <- readWorkspaceRecord recordPath+ result <- case planWorkspaceSourceMoves previous modules of+ Left moveErrors -> pure (Left [NameMigrationRefusal [T.pack (show moveError) | moveError <- NE.toList moveErrors]])+ Right moves -> do+ preparedMoves <- preflightSourceMoves out moves+ case preparedMoves of+ Left moveErrors -> pure (Left [NameMigrationRefusal moveErrors])+ Right prepared+ | not (null prepared) && not applyNameMigrations -> pure (Left [NameMigrationRequired (map preparedSourceMove prepared)])+ | otherwise ->+ executeWorkspaceScaffoldBase+ out+ forceGeneratedOverwrite+ sidecarMoves+ (map preparedSourceMove prepared)+ prepared+ plan+ pure (either (Left . noteApplied) Right result)+ where+ modules = map fst (wpModules plan)+ recordPath = out </> workspaceRecordFileName (wsService (wpWorkspace plan))++planWorkspaceSourceMoves :: Maybe WorkspaceRecord -> [ScaffoldModule] -> Either (NE.NonEmpty SourceMoveError) [SourceMove]+planWorkspaceSourceMoves previous current =+ case previous of+ Nothing -> Right []+ Just record ->+ planSourceMoves+ [(wrmRole row, wrmKind row, wrmPath row) | row <- wrModules record]+ current++executeWorkspaceScaffoldBase :: FilePath -> Bool -> [SidecarMove] -> [SourceMove] -> [PreparedSourceMove] -> WorkspacePlan -> IO (Either [Refusal] WorkspaceScaffoldReport)+executeWorkspaceScaffoldBase out forceGeneratedOverwrite sidecarMoves nameMoves preparedNameMoves plan = do stranded <- goldenRootDivergence (wpGoldenRoot plan) workspace bannerless <- if forceGeneratedOverwrite then pure [] else missingGeneratedBanners out modules packagePreflight <- case wpConformancePackage plan of@@ -397,6 +468,7 @@ case stranded <> [MissingGeneratedBanner bannerless | not (null bannerless)] <> packageRefusals of refusals@(_ : _) -> pure (Left refusals) [] -> do+ applyPreparedSourceMoves out preparedNameMoves previous <- readWorkspaceRecord recordPath stale <- staleAgainst out (map modulePath modules) (previousFiles previous) -- Adoption is a one-shot, guarded by the absence of workspace@@ -456,7 +528,10 @@ wsrAddedBehavior = addedBehavior, wsrRemovedBehavior = removedBehavior, wsrObsoleteOutputHooks = obsoleteGeneratedOutputHooks merged,+ wsrInertNodes = inertNodesOf merged, wsrConformancePackage = packageReport,+ wsrNameMoves = nameMoves,+ wsrSidecarMoves = sidecarMoves, wsrMigration = migration } where@@ -494,11 +569,13 @@ | member <- wsMembers workspace ], wrLanguageContract = checkedLanguageContract (wpCheckedService plan),+ wrNamingEdition = currentGeneratedHaskellNamingEdition, wrModules = [ WorkspaceModuleRow { wrmKind = kind m, wrmPath = modulePath m,- wrmOwner = provenanceOwner provenance+ wrmOwner = provenanceOwner provenance,+ wrmRole = Just (moduleRole m) } | (m, provenance) <- wpModules plan ],@@ -608,17 +685,20 @@ <> [ "firewall: OK (" <> tshow generatedCount <> " generated modules scanned, 0 forbidden operators)", harnessLine, dependencyLine,- "manifest: " <> T.pack (wsrBuildManifestPath report),- "record: " <> T.pack (wsrRecordPath report)+ "fragment: " <> T.pack (wsrBuildManifestPath report),+ "ledger: " <> T.pack (wsrRecordPath report) ] <> previousManifestNote <> migrationSection+ <> sidecarMoveSection+ <> nameMoveSection <> constraintSection <> newHolesSection <> mappingDriftSection <> sourceLanguageDriftSection <> behaviorDriftSection <> obsoleteOutputSection+ <> renderInertNodeSection (wsrInertNodes report) <> ownershipSection <> staleSection <> maybe [] renderConformancePackageReport (wsrConformancePackage report)@@ -666,6 +746,23 @@ Just previous -> ["note: the previous workspace record was written from manifest " <> previous] Nothing -> [] migrationSection = maybe [] renderMigrationReport (wsrMigration report)+ sidecarMoveSection = case wsrSidecarMoves report of+ [] -> []+ moves ->+ ["sidecar migration: applied (" <> tshow (length moves) <> " move(s))"]+ <> map ((" " <>) . renderSidecarMove) moves+ nameMoveSection = case wsrNameMoves report of+ [] -> []+ moves ->+ ["name migration: applied (" <> tshow (length moves) <> " source move(s))"]+ <> [ " "+ <> T.pack (moveOldPath move)+ <> " -> "+ <> T.pack (moveNewPath move)+ <> " backup="+ <> T.pack (moveBackupPath move)+ | move <- moves+ ] constraintSection = case wsrConstraintPlan report of [] -> [] constraints -> "constraint plan:" : map (" " <>) constraints
test/Keiro/Dsl/FrontendCompatibility.hs view
@@ -462,6 +462,8 @@ "aggregate Thing", " regs", " states Open",+ " command Known { }",+ " event KnownHappened = fields(Known)", " Open -- MissingCommand -->", " goto Open" ]
test/Keiro/Dsl/FrontendProfiles.hs view
@@ -25,7 +25,7 @@ `shouldBe` [ (1, Nothing, "keiro-dsl/syntax-profile/1", "keiro-dsl/runtime-semantics/1"), (2, Just 1, "keiro-dsl/syntax-profile/2", "keiro-dsl/runtime-semantics/1"), (3, Just 2, "keiro-dsl/syntax-profile/2", "keiro-dsl/runtime-semantics/2"),- (4, Just 3, "keiro-dsl/syntax-profile/2", "keiro-dsl/runtime-semantics/3")+ (4, Just 3, "keiro-dsl/syntax-profile/3", "keiro-dsl/runtime-semantics/3") ] map definitionCapabilities (NE.toList languageRegistry) `shouldBe` [ [],@@ -58,11 +58,11 @@ not wasSupported || runtimeProfileHasCapability (definitionRuntimeSemanticsProfile successor) capability forM_ allFeatures $ \feature -> do- languageFeatureMinimumVersion feature `shouldBe` version 2- languageSupportsFeature (version 1) feature `shouldBe` False- languageSupportsFeature (version 2) feature `shouldBe` True- languageSupportsFeature (version 3) feature `shouldBe` True- languageSupportsFeature (version 4) feature `shouldBe` True+ let minimumVersion = if feature == FieldAliasSyntax then version 4 else version 2+ languageFeatureMinimumVersion feature `shouldBe` minimumVersion+ forM_ [1, 2, 3, 4] $ \versionNumber ->+ languageSupportsFeature (version versionNumber) feature+ `shouldBe` (version versionNumber >= minimumVersion) it "does not infer a hypothetical successor profile or runtime contract" $ do lookupLanguageDefinition (version 5) `shouldBe` Nothing@@ -88,16 +88,16 @@ forM_ [1, 2, 3, 4] $ \versionNumber -> do let sourceName = "profile-" <> show versionNumber <> ".keiro" source = preamble versionNumber <> body- case (versionNumber, parseSurfaceSource sourceName source) of- (1, Left FrontendFailure {phase, code, span, supportedVersions}) -> do+ case (languageSupportsFeature (version versionNumber) feature, parseSurfaceSource sourceName source) of+ (False, Left FrontendFailure {phase, code, span, supportedVersions}) -> do phase `shouldBe` BodyParsingPhase code `shouldBe` SourceLanguageError LanguageFeatureRequiresVersion spanText source span `shouldBe` marker- supportedVersions `shouldBe` [version 2, version 3, version 4]+ supportedVersions `shouldBe` languageVersionsSupportingFeature feature languageSupportsFeature (version versionNumber) feature `shouldBe` False- (1, result) -> expectationFailure ("expected v1 feature refusal, got " <> show result)- (_, Right _) -> languageSupportsFeature (version versionNumber) feature `shouldBe` True- (_, Left failure) -> expectationFailure (T.unpack (renderFrontendFailure failure))+ (False, result) -> expectationFailure ("expected feature refusal, got " <> show result)+ (True, Right _) -> languageSupportsFeature (version versionNumber) feature `shouldBe` True+ (True, Left failure) -> expectationFailure (T.unpack (renderFrontendFailure failure)) it "keeps feature spellings inert in comments, strings, wire keys, and identifiers" $ do inertBody <- readRepoText "keiro-dsl/test/fixtures/language-identifier-v1.keiro"@@ -163,7 +163,8 @@ [ FeatureCase NominalBindingSyntax "using" (featureBody NominalBindingSyntax), FeatureCase IntegerScalarSyntax "Integer" (featureBody IntegerScalarSyntax), FeatureCase TypedAggregateExpressionSyntax "cmd." (featureBody TypedAggregateExpressionSyntax),- FeatureCase ExplicitTransitionImplementationSyntax "implementation hole" (featureBody ExplicitTransitionImplementationSyntax)+ FeatureCase ExplicitTransitionImplementationSyntax "implementation hole" (featureBody ExplicitTransitionImplementationSyntax),+ FeatureCase FieldAliasSyntax "haskell" (featureBody FieldAliasSyntax) ] featureBody :: LanguageFeature -> Text@@ -191,9 +192,17 @@ " event Ticked = fields(Tick)", " Open -- Tick --> implementation hole ; emit Ticked ; goto Open" ]+ FieldAliasSyntax ->+ T.unlines+ [ "context profile",+ "aggregate Profile",+ " regs",+ " states Open",+ " command Rename { type haskell payloadType as \"type\":Text }"+ ] allFeatures :: [LanguageFeature]-allFeatures = [NominalBindingSyntax, IntegerScalarSyntax, TypedAggregateExpressionSyntax, ExplicitTransitionImplementationSyntax]+allFeatures = [NominalBindingSyntax, IntegerScalarSyntax, TypedAggregateExpressionSyntax, ExplicitTransitionImplementationSyntax, FieldAliasSyntax] allRuntimeCapabilities :: [RuntimeCapability] allRuntimeCapabilities = [minBound .. maxBound]
test/Main.hs view
@@ -7,8596 +7,10709 @@ 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.Aeson.Types (parseEither)-import Data.Either (isLeft, isRight)-import Data.Foldable (toList)-import Data.KindID qualified as KindID-import Data.List (partition, permutations, sort, (\\))-import Data.List.NonEmpty (NonEmpty (..))-import Data.List.NonEmpty qualified as NE-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 Data.Text.Lazy qualified as LazyText-import Data.Text.Lazy.Encoding qualified as LazyTextEncoding-import Data.Version (showVersion)-import Keiki.ProjectionDomain (matchesTextPattern)-import Keiro.Codec (Codec (..), EventType (..), decodeRaw)-import Keiro.Codec.IdDomain (IdDomainFailure (..), idDomainSampleText, idDomainTextPattern, parseKindIdV7Text, parseKindIdV7Value, typeIdV7Domain, validateIdDomainText)-import Keiro.Dsl.AggregateType-import Keiro.Dsl.BehaviorCoverage qualified as Behavior-import Keiro.Dsl.CanonicalEncoding (foldFingerprint128)-import Keiro.Dsl.CodecCompare-import Keiro.Dsl.ConformanceBaseline (conformanceBaselineSpec)-import Keiro.Dsl.ConformancePackage-import Keiro.Dsl.Coverage qualified as Coverage-import Keiro.Dsl.Diff (Change (..), ChangeKind (..), CompatibilitySurface (..), CompatibilityVector (..), FamilyDiff (..), Label (..), NodeFamily, RolloutConstraint (..), SurfaceVerdict (..), defaultGate, deriveLabel, familyRegistry, gateWith, gatedBreaking, isAdvisory, isBreaking, verdictFor)-import Keiro.Dsl.Diff qualified as CheckedDiff-import Keiro.Dsl.DiffReport (Remedy (..), diffReport, parseSurfaceName, remediationFor, renderExplainBlock, renderFinding)-import Keiro.Dsl.EventOutput-import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligation (..), BindingObligationKind (..), bindingHoles, bindingObligations, bindingObligationsForService, renderBindingObligations)-import Keiro.Dsl.Expression-import Keiro.Dsl.FoldFingerprint (FoldSurfaceError (..))-import Keiro.Dsl.FoldFingerprint qualified as CheckedFold-import Keiro.Dsl.FrontendCompatibility (frontendCompatibilitySpec)-import Keiro.Dsl.FrontendProfiles (frontendProfilesSpec)-import Keiro.Dsl.FrontendSurface (frontendSurfaceSpec)-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.IdDomain (IdDomainContract (..), contractIdDomainContractFor, idDomainContractFor, idDomainIdentitiesForService)-import Keiro.Dsl.LanguageVersion-import Keiro.Dsl.Manifest (manifestDependencies, manifestDependenciesForService, moduleNameOf, renderManifest, renderManifestForService, renderManifestForServiceWithFacade)-import Keiro.Dsl.MappedConsumer (ConsumerPlan (..), MappingIdentity (..), consumerPlan)-import Keiro.Dsl.NominalType hiding (NominalInvalidHaskellSource, NominalInvalidIdPrefix, NominalInvalidIdentity, NominalMissingIngredient)-import Keiro.Dsl.Parser (parseSource, parseSpec)-import Keiro.Dsl.PrettyPrint (renderSource, 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 (..), NominalGenerationOwner (..), NominalUseSite (..), ScaffoldModule (..), StructuralProjection (..), codecComparisonBanner, codecComparisonModule, defaultContext, firewallBreaches, genPrefixFor, generatedBanner, generatedBannerFor, generatedNominalModule, holePrefixFor, isGeneratedBannerLine, obsoleteGeneratedOutputHooks, planNominalGeneration, projectionSpecs, scaffoldAggregate, scaffoldContract, scaffoldContractForService, scaffoldIntake, scaffoldProcess, scaffoldPublisher, scaffoldReadModel, scaffoldRefusals, scaffoldReplayAudit, scaffoldRouter, scaffoldStructural, scaffoldWorkqueue, windowSeconds)-import Keiro.Dsl.ScaffoldRecord (ScaffoldRecord (..), parseRecord, recordFileName, renderRecord)-import Keiro.Dsl.ScaffoldRun (MappingDrift (..), Refusal (..), ScaffoldReport (..), SourceLanguageDrift (..), StaleGeneratedEvidence (..), StaleModule (..), WriteDisposition (..), executeScaffold, executeScaffoldWithLanguage, executeServiceScaffold, executeServiceScaffoldWithRuntimePackage, planScaffold, planServiceScaffold, planServiceScaffoldWithRuntimePackage, renderRefusals, renderScaffoldReport, scaffoldModules, scaffoldServiceModules)-import Keiro.Dsl.SemanticContract-import Keiro.Dsl.ServiceHarness-import Keiro.Dsl.Skeleton (skeletonFor, skeletonKinds)-import Keiro.Dsl.TypeGraph-import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), derivedQueueTrio, renderDiagnostic, validateService, validateSpec)-import Keiro.Dsl.Workspace-import Keiro.Dsl.WorkspaceAdoption-import Keiro.Dsl.WorkspaceDiff hiding (diffWorkspaces)-import Keiro.Dsl.WorkspaceDiff qualified as CheckedWorkspaceDiff-import Keiro.Dsl.WorkspaceRecord-import Keiro.Dsl.WorkspaceScaffold-import Paths_keiro_dsl qualified as Package-import System.Directory (canonicalizePath, createDirectory, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getTemporaryDirectory, listDirectory, removeFile, removePathForcibly)-import System.Environment (lookupEnv)-import System.Exit (ExitCode (..))-import System.FilePath (takeDirectory, takeExtension, takeFileName, (</>))-import System.IO (hClose, openTempFile)-import System.Process (readProcessWithExitCode)-import Test.Hspec hiding (Spec)-import Test.QuickCheck--resolvedFold :: Either FoldSurfaceError value -> value-resolvedFold = either (error . ("unexpected fold-surface failure in checked fixture: " <>) . show) id--aggregateFoldFingerprintForService :: CheckedService -> Aggregate -> T.Text-aggregateFoldFingerprintForService service aggregate = resolvedFold (CheckedFold.aggregateFoldFingerprintForService service aggregate)--aggregateFoldSurfaceForService :: CheckedService -> Aggregate -> T.Text-aggregateFoldSurfaceForService service aggregate = resolvedFold (CheckedFold.aggregateFoldSurfaceForService service aggregate)--aggregateFoldFingerprint :: Spec -> Aggregate -> T.Text-aggregateFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (stableCheckedService spec) aggregate--aggregateFoldSurface :: Spec -> Aggregate -> T.Text-aggregateFoldSurface spec aggregate = aggregateFoldSurfaceForService (stableCheckedService spec) aggregate--legacyAggregateFoldFingerprint :: Spec -> Aggregate -> T.Text-legacyAggregateFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (legacyCheckedService spec) aggregate--legacyAggregateFoldSurface :: Spec -> Aggregate -> T.Text-legacyAggregateFoldSurface spec aggregate = aggregateFoldSurfaceForService (legacyCheckedService spec) aggregate--diffServices :: CheckedService -> CheckedService -> [Change]-diffServices old new = resolvedFold (CheckedDiff.diffServices old new)--diffSources :: ParsedSource -> ParsedSource -> [Change]-diffSources old new = resolvedFold (CheckedDiff.diffSources old new)--diffSpecs :: Spec -> Spec -> [Change]-diffSpecs old new = diffServices (stableCheckedService old) (stableCheckedService new)--legacyDiffSpecs :: Spec -> Spec -> [Change]-legacyDiffSpecs old new = diffServices (legacyCheckedService old) (legacyCheckedService new)--diffWorkspaces :: WorkspaceSpec -> WorkspaceSpec -> [WorkspaceChange]-diffWorkspaces old new = resolvedFold (CheckedWorkspaceDiff.diffWorkspaces old new)--replayImpactSpecs :: Spec -> Spec -> ReplayImpact-replayImpactSpecs old new =- resolvedFold (ReplayImpact.replayImpactServices (stableCheckedService old) (stableCheckedService new))--legacyReplayImpactSpecs :: Spec -> Spec -> ReplayImpact-legacyReplayImpactSpecs old new =- resolvedFold (ReplayImpact.replayImpactServices (legacyCheckedService old) (legacyCheckedService new))--nominalEqualityIdentities :: Spec -> [T.Text]-nominalEqualityIdentities = nominalEqualityIdentitiesForService . stableCheckedService--stableCheckedService :: Spec -> CheckedService-stableCheckedService = checkedService stableSourceLanguage--stableSourceLanguage :: SourceLanguage-stableSourceLanguage =- DeclaredLanguage- { declaredLanguageVersion = currentStableLanguageVersion,- languageVersionLoc = noLoc- }--main :: IO ()-main = hspec $ do- conformanceBaselineSpec- frontendCompatibilitySpec- frontendSurfaceSpec- frontendProfilesSpec-- describe "language support" $ do- it "serializes support from the registered version and decodes older records" $ do- v1Contract <- maybe (expectationFailure "missing v1 contract" >> fail "unreachable") pure (effectiveLanguageContractForVersion =<< languageVersion 1)- v4Contract <- maybe (expectationFailure "missing v4 contract" >> fail "unreachable") pure (effectiveLanguageContractForVersion =<< languageVersion 4)- effectiveLanguageSupport v1Contract `shouldBe` CompatibilityOnly- effectiveLanguageSupport v4Contract `shouldBe` Stable- Aeson.toJSON v4Contract- `shouldBe` object- [ "languageVersion" .= (4 :: Int),- "runtimeSemantics" .= ("keiro-dsl/runtime-semantics/3" :: T.Text),- "languageSupport" .= ("stable" :: T.Text)- ]- Aeson.eitherDecode "{\"languageVersion\":1,\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/1\"}"- `shouldBe` Right v1Contract-- it "reports stable and compatibility-only support through source inspection" $ do- (stableCode, stableOut, stableErr) <- runKeiroDsl ["inspect", "test/fixtures/contract-v4.keiro", "--format=json"]- stableCode `shouldBe` ExitSuccess- stableErr `shouldBe` ""- stableOut `shouldContain` "\"languageVersion\":4"- stableOut `shouldContain` "\"languageSupport\":\"stable\""- (compatibilityCode, compatibilityOut, compatibilityErr) <- runKeiroDsl ["inspect", "test/fixtures/language-v1.keiro", "--format=json"]- compatibilityCode `shouldBe` ExitSuccess- compatibilityErr `shouldBe` ""- compatibilityOut `shouldContain` "\"languageVersion\":1"- compatibilityOut `shouldContain` "\"languageSupport\":\"compatibility-only\""-- describe "runtime capability and fold identity baseline (plan 181)" $ do- it "pins the fold-only FNV-1a-128 UTF-8 encoding" $ do- foldFingerprint128 "" `shouldBe` "6c62272e07bb014262b821756295c58d"- foldFingerprint128 "雪" `shouldBe` "a68afaae758b5822836dbc787bb233bd"-- it "pins complete fold surfaces and fingerprints across representative aggregates" $ do- scalar <- checkedServiceOf "test/fixtures/aggregate-scalar-expressions-v2.keiro"- nominal <- checkedServiceOf "test/fixtures/nominal-scalars.keiro"- idDomain <- checkedServiceOf "test/fixtures/id-domain-migration-v3.keiro"- behavior <- checkedServiceOf "test/fixtures/behavior-complete.keiro"- workspace <- shouldComposeWorkspace "test/fixtures/workspace-nominals/service.keiro-workspace"- let actual =- T.intercalate- "\n\n"- [ renderFoldBaseline "aggregate-scalar-expressions-v2" scalar,- renderFoldBaseline "nominal-scalars" nominal,- renderFoldBaseline "id-domain-migration-v3" idDomain,- renderFoldBaseline "behavior-complete" behavior,- renderFoldBaseline "workspace-nominals" (checkedWorkspace workspace)- ]- golden <- readTestText "test/fixtures/fold-identity-baseline.golden"- T.stripEnd actual `shouldBe` T.stripEnd golden-- it "pins all four runtime gates and fingerprint segment projections" $ do- nominalSpec <- specOf "test/fixtures/id-domain-migration-v3.keiro"- nominalRegistry <- case resolveNominalTypes nominalSpec of- Left errors -> expectationFailure (show errors) >> fail "unreachable"- Right value -> pure value- nominal <- case lookupNominalType "OrderId" nominalRegistry of- Nothing -> expectationFailure "missing OrderId nominal" >> fail "unreachable"- Just value -> pure value- strictSpec <-- parseInlineSpec- "<strict-profile>"- ( T.unlines- [ "context strict-profile",- "aggregate DuplicateRegister",- " regs",- " value Int = 0",- " value Int = 0",- " states Open"- ]- )- rows <- forM [1 .. 4 :: Int] $ \number -> do- contract <- case languageVersion (fromIntegral number) >>= effectiveLanguageContractForVersion of- Nothing -> expectationFailure ("missing released language contract " <> show number) >> fail "unreachable"- Just value -> pure value- let hasAggregateIdDomain = maybe False (const True) (idDomainContractFor contract "ord")- hasContractIdDomain = maybe False (const True) (contractIdDomainContractFor contract "ord")- nominalContract = equalityContractVersion <$> nominalEqualityContractForService contract nominal- strictService = CheckedService contract strictSpec- hasStrictValidation = any ((== AggregateDuplicateRegister) . code) (validateService strictService)- pure- ( number,- effectiveRuntimeSemantics contract,- runtimeSemanticsFingerprintSegments contract,- hasAggregateIdDomain,- hasContractIdDomain,- nominalContract,- hasStrictValidation- )- rows- `shouldBe` [ (1, "keiro-dsl/runtime-semantics/1", [], False, False, Just "keiro-dsl/nominal-equality/1", False),- (2, "keiro-dsl/runtime-semantics/1", [], False, False, Just "keiro-dsl/nominal-equality/1", False),- (3, "keiro-dsl/runtime-semantics/2", ["semantic-contract:keiro-dsl/runtime-semantics/2"], True, False, Just "keiro-dsl/nominal-equality/2", False),- (4, "keiro-dsl/runtime-semantics/3", ["semantic-contract:keiro-dsl/runtime-semantics/2"], True, True, Just "keiro-dsl/nominal-equality/2", True)- ]-- it "explains a serialized runtime-profile mismatch" $ do- case (Aeson.eitherDecode "{\"languageVersion\":4,\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/2\"}" :: Either String EffectiveLanguageContract) of- Left message -> do- message `shouldContain` "runtimeSemantics does not match language version 4"- message `shouldContain` "keiro-dsl/runtime-semantics/3"- message `shouldContain` "keiro-dsl/runtime-semantics/2"- Right _ -> expectationFailure "expected a runtime-profile mismatch"-- it "reports every formerly silent fold-resolution failure and propagates it" $ do- baseService <- checkedServiceOf "test/fixtures/id-domain-migration-v3.keiro"- recursiveMapped <- specOf "test/fixtures/mapped-recursive.keiro"- brokenNominal <- specOf "test/fixtures/nominal-missing-facts.keiro"- missingInitial <- specOf "test/fixtures/mapped-missing-initial.keiro"- let contract = checkedLanguageContract baseService- baseSpec = checkedSpec baseService- baseAggregate = onlyAggregate baseSpec- withAggregate transform =- baseSpec- { specNodes =- [ NAggregate (transform aggregate)- | NAggregate aggregate <- specNodes baseSpec- ]- }- replaceFirstTransition transform aggregate =- aggregate- { aggTransitions = case aggTransitions aggregate of- transition : rest -> transform transition : rest- [] -> []- }- guardSpec = withAggregate (replaceFirstTransition (\transition -> transition {tGuard = Just (EAtom (AName "missingGuardRoot"))}))- outputSpec = withAggregate (replaceFirstTransition (\transition -> transition {tEmits = ["MissingEvent"]}))- typeGraphSpec = baseSpec {specMapped = specMapped recursiveMapped}- nominalSpec = baseSpec {specNominalScalars = specNominalScalars brokenNominal}- cases =- [ (CheckedService contract typeGraphSpec, baseAggregate, \case FoldTypeGraphResolutionFailed {} -> True; _ -> False),- (CheckedService contract nominalSpec, baseAggregate, \case FoldNominalResolutionFailed {} -> True; _ -> False),- (CheckedService contract missingInitial, onlyAggregate missingInitial, \case FoldRegisterInitialResolutionFailed {} -> True; _ -> False),- (CheckedService contract guardSpec, onlyAggregate guardSpec, \case FoldGuardResolutionFailed {} -> True; _ -> False),- (CheckedService contract outputSpec, onlyAggregate outputSpec, \case FoldEventOutputResolutionFailed {} -> True; _ -> False)- ]- forM_ cases $ \(service, aggregate, matches) -> do- CheckedFold.aggregateFoldSurfaceForService service aggregate- `shouldSatisfy` either matches (const False)- CheckedFold.aggregateFoldFingerprintForService service aggregate- `shouldSatisfy` either matches (const False)- let brokenService = CheckedService contract guardSpec- CheckedDiff.diffServices brokenService baseService `shouldSatisfy` isLeft- ReplayImpact.replayImpactServices brokenService baseService `shouldSatisfy` isLeft- planServiceScaffold (defaultContext (specContext guardSpec)) brokenService- `shouldSatisfy` \case- Left refusals -> any (\case FoldSurfaceRefusal {} -> True; _ -> False) refusals- Right _ -> False-- it "pins representative diff and replay-impact rendering" $ do- old <- parsedSourceOf "test/fixtures/reservation.keiro"- new <- parsedSourceOf "test/fixtures/reservation-guard-tightened.keiro"- let changes = diffSources old new- impact = resolvedFold (ReplayImpact.replayImpactServices (checkedSource old) (checkedSource new))- actual =- T.intercalate- "\n"- ( "diff:"- : map renderFinding changes- <> ["replay:", ReplayImpact.renderReplayImpact impact]- )- golden <- readTestText "test/fixtures/fold-identity-diff-replay.golden"- T.stripEnd actual `shouldBe` T.stripEnd golden-- it "pins unrelated public 64-bit identities outside the fold digest" $ do- readModelSpec <- specOf "test/fixtures/readmodel.keiro"- wireSpec <- specOf "test/fixtures/consumer-types.keiro"- behaviorSpec <- specOf "test/fixtures/behavior-complete.keiro"- readModel <- case [value | NReadModel value <- specNodes readModelSpec] of- value : _ -> pure value- [] -> expectationFailure "missing read-model fixture" >> fail "unreachable"- graph <- shouldResolveTypeGraph wireSpec- behaviorKey <- case Behavior.deriveBehaviorRequirements behaviorSpec of- Right (requirement : _) -> pure (Behavior.unBehaviorKey (Behavior.requirementKey requirement))- result -> expectationFailure ("missing behavior requirement: " <> show result) >> fail "unreachable"- deriveShapeHash readModel `shouldBe` "fnv1a:3717f6d9e3c44bd6"- wireFingerprint graph "ArtifactInfo" `shouldBe` "2bd99b3e57bcde9b"- behaviorKey `shouldBe` "behavior-v1-2e1fd6b9580e1a3d"-- describe "source language version" $ do- let legacy = "context hospital-capacity\n"- declared = "# leading comment\n\nlanguage keiro-dsl 1\ncontext hospital-capacity\n"- failureCode source = case parseSource "source.keiro" source of- Left (SourceLanguageFailure diagnostic) -> Just (sourceLanguageErrorCode diagnostic)- _ -> Nothing- parseRight name source = case parseSource name source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- declaredVersionOf DeclaredLanguage {declaredLanguageVersion = version} = Just version- declaredVersionOf LegacyUnversioned = Nothing- orderedSubstrings needles haystack = go (map T.pack needles) (T.pack haystack)- where- go [] _ = True- go (needle : rest) remaining =- let (_, suffix) = T.breakOn needle remaining- in not (T.null suffix) && go rest (T.drop (T.length needle) suffix)-- it "selects declared v1 after comments while preserving semantic equality" $ do- legacySource <- parseRight "legacy.keiro" legacy- declaredSource <- parseRight "declared.keiro" declared- parsedSpec legacySource `shouldBe` parsedSpec declaredSource- parsedSourceLanguage legacySource `shouldBe` LegacyUnversioned- declaredVersionOf (parsedSourceLanguage declaredSource) `shouldBe` languageVersion 1- effectiveLanguageVersion (parsedSourceLanguage legacySource)- `shouldBe` effectiveLanguageVersion (parsedSourceLanguage declaredSource)-- it "threads paired released versions through one checked semantic boundary" $ do- let body = T.unlines ["context semantic-pair", "aggregate Counter", " regs", " states Open"]- v1Text = "language keiro-dsl 1\n" <> body- v2Text = "language keiro-dsl 2\n" <> body- v1Source <- parseRight "reservation-v1.keiro" v1Text- v2Source <- parseRight "reservation-v2.keiro" v2Text- let v1Service = checkedSource v1Source- v2Service = checkedSource v2Source- v1Spec = checkedSpec v1Service- v2Spec = checkedSpec v2Service- ctx = defaultContext (specContext v1Spec)- aggregates spec = [aggregate | NAggregate aggregate <- specNodes spec]- v1Spec `shouldBe` v2Spec- Just (effectiveContractLanguageVersion (checkedLanguageContract v1Service)) `shouldBe` languageVersion 1- Just (effectiveContractLanguageVersion (checkedLanguageContract v2Service)) `shouldBe` languageVersion 2- effectiveRuntimeSemantics (checkedLanguageContract v1Service)- `shouldBe` effectiveRuntimeSemantics (checkedLanguageContract v2Service)- validateService v1Service `shouldBe` validateService v2Service- scaffoldServiceModules ctx v1Service `shouldBe` scaffoldServiceModules ctx v2Service- case (aggregates v1Spec, aggregates v2Spec) of- ([v1Aggregate], [v2Aggregate]) -> do- aggregateFoldSurfaceForService v1Service v1Aggregate- `shouldBe` aggregateFoldSurfaceForService v2Service v2Aggregate- aggregateFoldFingerprintForService v1Service v1Aggregate- `shouldBe` aggregateFoldFingerprintForService v2Service v2Aggregate- other -> expectationFailure ("expected one aggregate per paired source, got " <> show (fmap length other))- diffServices v1Service v2Service `shouldBe` []- resolvedFold (ReplayImpact.replayImpactServices v1Service v2Service) `shouldBe` ReplayNeutral-- it "retains one effective contract for same-version workspaces and refuses mixed versions" $ do- let manifest = "service semantic-workspace\nspec domain/a.keiro\nspec domain/b.keiro\n"- v1Body = "language keiro-dsl 1\ncontext semantic-workspace\n"- v2Body = "language keiro-dsl 2\ncontext semantic-workspace\n"- sourceWith b =- memoryContentSource- ( Map.fromList- [ ("service.keiro-workspace", manifest),- ("domain/a.keiro", b),- ("domain/b.keiro", b)- ]- )- mixedSource =- memoryContentSource- ( Map.fromList- [ ("service.keiro-workspace", manifest),- ("domain/a.keiro", v1Body),- ("domain/b.keiro", v2Body)- ]- )- sameVersion <- loadWorkspace (sourceWith v2Body) "service.keiro-workspace"- case sameVersion of- Right workspace -> do- Just (effectiveContractLanguageVersion (checkedLanguageContract (checkedWorkspace workspace))) `shouldBe` languageVersion 2- validateService (checkedWorkspace workspace) `shouldBe` []- Left failure -> expectationFailure (show failure)- mixed <- loadWorkspace mixedSource "service.keiro-workspace"- case mixed of- Left (WorkspaceRefused diagnostics) ->- map wdCode (NE.toList diagnostics) `shouldContain` [WorkspaceLanguageVersionMismatch]- other -> expectationFailure ("expected a mixed-version refusal, got " <> show other)-- it "refuses a source/service contract mismatch before creating the output directory" $ do- v1Version <- maybe (expectationFailure "version 1 missing" >> fail "unreachable") pure (languageVersion 1)- parsed <- parseRight "semantic-v2.keiro" "language keiro-dsl 2\ncontext semantic-refusal\n"- let service = checkedSource parsed- ctx = defaultContext "semantic-refusal"- modules <- case planServiceScaffold ctx service of- Left refusals -> expectationFailure (show refusals) >> fail "unreachable"- Right planned -> pure planned- withTempDirectory "keiro-dsl-semantic-refusal" $ \root -> do- let out = root </> "not-created"- result <- executeServiceScaffold out False "semantic-v2.keiro" (DeclaredLanguage v1Version noLoc) ctx service modules- result `shouldBe` Left [SemanticContractMismatch "source provenance and checked service selected different effective language contracts"]- doesDirectoryExist out `shouldReturn` False-- it "retains explicit declarations in source rendering and leaves legacy unversioned" $ do- legacySource <- parseRight "legacy.keiro" legacy- declaredSource <- parseRight "declared.keiro" declared- renderSource legacySource `shouldBe` "context hospital-capacity\n"- renderSource declaredSource `shouldBe` "language keiro-dsl 1\ncontext hospital-capacity\n"- parseSpec "declared.keiro" declared `shouldBe` Right (parsedSpec declaredSource)-- it "classifies invalid, unsupported, duplicate, and misplaced preambles" $ do- failureCode "language keiro-dsl 0\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion- failureCode "language keiro-dsl nope\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion- failureCode "language keiro-dsl -1\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion- failureCode "language keiro-dsl 5\ncontext hospital-capacity\n" `shouldBe` Just UnsupportedLanguageVersion- failureCode "language keiro-dsl 1\nlanguage keiro-dsl 1\ncontext hospital-capacity\n" `shouldBe` Just DuplicateLanguagePreamble- failureCode "context hospital-capacity\nlanguage keiro-dsl 1\n" `shouldBe` Just MisplacedLanguagePreamble-- it "treats language and successor spellings as data in nested grammar positions" $ do- forM_ ["language-identifier-v1.keiro", "language-identifier-v2.keiro"] $ \fixture -> do- source <- readTestText ("test/fixtures/" <> fixture)- parsed <- parseRight fixture source- validateSpec (parsedSpec parsed) `shouldBe` []- v1 <- readTestText "test/fixtures/language-identifier-v1.keiro"- let manifest = "service language-collisions\nspec domain/collisions.keiro\n"- workspaceSource = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/collisions.keiro", v1)])- loaded <- loadWorkspace workspaceSource "service.keiro-workspace"- loaded `shouldSatisfy` isRight-- it "keeps duplicate and misplaced preamble diagnostics on their grammar lines" $ do- let sourceFailureAt expectedCode expectedLine source =- case parseSource "located.keiro" source of- Left (SourceLanguageFailure diagnostic) -> do- sourceLanguageErrorCode diagnostic `shouldBe` expectedCode- unLoc (sourceLanguageLoc diagnostic) `shouldBe` expectedLine- other -> expectationFailure ("expected located source-language failure, got " <> show other)- sourceFailureAt DuplicateLanguagePreamble 2 "language keiro-dsl 1\nlanguage keiro-dsl 1\ncontext located\n"- sourceFailureAt MisplacedLanguagePreamble 3 "context located\nid language prefix=lang\nlanguage keiro-dsl 1\n"-- it "rejects a future version before parsing an invalid v1 body" $- case parseSource "future.keiro" "language keiro-dsl 5\nthis is not a v2 body\n" of- Left failure@(SourceLanguageFailure diagnostic) -> do- sourceLanguageErrorCode diagnostic `shouldBe` UnsupportedLanguageVersion- renderParseFailure failure `shouldSatisfy` T.isInfixOf "supported versions: 1, 2, 3, 4"- renderParseFailure failure `shouldNotSatisfy` T.isInfixOf "expecting `context`"- other -> expectationFailure ("expected source-language failure, got " <> show other)-- it "accepts and canonically round-trips nominal declarations only in v2" $ do- let nominalSource =- T.unlines- [ "language keiro-dsl 2",- "context orders",- "id OrderId prefix=ord using {",- " haskell package=orders-domain module=Orders.Id type=OrderId",- " binding = \"Orders.KeiroBindings.orderIdBinding\"",- " binding-version = \"1\"",- " canonical-type = \"orders.OrderId.v1\"",- " fixtures = \"Orders.KeiroBindings.orderIdFixtures\"",- "}",- "enum OrderStatus { Draft=draft Submitted=submitted } using {",- " haskell package=orders-domain module=Orders.Order type=OrderStatus",- " binding = \"Orders.KeiroBindings.orderStatusBinding\"",- " binding-version = \"1\"",- " canonical-type = \"orders.OrderStatus.v1\"",- " fixtures = \"Orders.KeiroBindings.orderStatusFixtures\"",- "}",- "mapped nominal AccountNumber : Text {",- " haskell package=orders-domain module=Orders.Account type=AccountNumber",- " binding = \"Orders.KeiroBindings.accountNumberBinding\"",- " binding-version = \"1\"",- " canonical-type = \"orders.AccountNumber.v1\"",- " fixtures = \"Orders.KeiroBindings.accountNumberFixtures\"",- " initial = \"Orders.KeiroBindings.initialAccountNumber\"",- "}"- ]- parsed <- parseRight "nominal.keiro" nominalSource- length (specIds (parsedSpec parsed)) `shouldBe` 1- length (specEnums (parsedSpec parsed)) `shouldBe` 1- length (specNominalScalars (parsedSpec parsed)) `shouldBe` 1- parseSource "nominal-round-trip.keiro" (renderSource parsed) `shouldBe` Right parsed-- it "reports successor nominal syntax as one language-version diagnostic under v1 and legacy" $ do- let body = "context orders\nmapped nominal AccountNumber : Text {}\n"- failureCode ("language keiro-dsl 1\n" <> body) `shouldBe` Just LanguageFeatureRequiresVersion- failureCode body `shouldBe` Just LanguageFeatureRequiresVersion-- it "attributes every successor feature gate to its owning grammar production" $ do- let featureFailureAt expectedLine source =- case parseSource "feature.keiro" source of- Left (SourceLanguageFailure diagnostic) -> do- sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion- unLoc (sourceLanguageLoc diagnostic) `shouldBe` expectedLine- other -> expectationFailure ("expected a located feature gate, got " <> show other)- aggregateWith clause =- T.unlines- [ "language keiro-dsl 1",- "context feature-gates",- "aggregate Account",- " regs",- " balance Text = \"0\"",- " states Open",- " command Adjust { amount:Text }",- " event Adjusted = fields(Adjust)",- " Open -- Adjust --> " <> clause <> " ; emit Adjusted ; goto Open"- ]- featureFailureAt 3 (T.unlines ["language keiro-dsl 1", "context feature-gates", "id AccountId prefix=acct using {"])- featureFailureAt 3 (T.unlines ["language keiro-dsl 1", "context feature-gates", "mapped nominal AccountNumber : Text {}"])- featureFailureAt 5 (T.unlines ["language keiro-dsl 1", "context feature-gates", "aggregate Account", " regs", " balance Integer = 0", " states Open"])- featureFailureAt 9 (aggregateWith "guard reg.balance == cmd.amount")- featureFailureAt 9 (aggregateWith "implementation hole")-- it "reports a declaration-only rewrite without semantic, generated, fold, or replay impact" $ do- fixture <- readTestText "test/fixtures/language-v1.keiro"- let legacyFixture = T.unlines (drop 1 (T.lines fixture))- legacySource <- parseRight "legacy.keiro" legacyFixture- declaredSource <- parseRight "declared.keiro" fixture- let oldSpec = parsedSpec legacySource- newSpec = parsedSpec declaredSource- changes = diffSources legacySource declaredSource- vectors = [ckVector kind | change <- changes, let kind = workspaceChangeKind change]- map changeCode changes `shouldBe` [SourceLanguageDeclarationChanged]- legacyDiffSpecs oldSpec newSpec `shouldBe` []- vectors `shouldSatisfy` all (\compatibility -> all ((== VCompatible) . (`verdictFor` compatibility)) [minBound .. maxBound])- case changes of- [change] ->- remediationFor (ckContext (workspaceChangeKind change)) SourceLanguageDeclarationChanged- `shouldBe` (RemedyNoSemanticAction :| [])- _ -> expectationFailure "expected one source-language change"- let legacyGeneratedSurface spec =- [ (modulePath scaffoldModule, moduleText scaffoldModule, kind scaffoldModule)- | scaffoldModule <- scaffoldModules (defaultContext (specContext spec)) spec- ]- legacyFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (legacyCheckedService spec) aggregate- legacyGeneratedSurface oldSpec `shouldBe` legacyGeneratedSurface newSpec- [legacyFoldFingerprint oldSpec aggregate | NAggregate aggregate <- specNodes oldSpec]- `shouldBe` [legacyFoldFingerprint newSpec aggregate | NAggregate aggregate <- specNodes newSpec]- legacyReplayImpactSpecs oldSpec newSpec `shouldBe` ReplayNeutral-- it "exposes stable JSON inspection for a source and canonically ordered workspace members" $ do- (sourceCode, sourceOut, sourceErr) <- runKeiroDsl ["inspect", "test/fixtures/reservation.keiro", "--format=json"]- sourceCode `shouldBe` ExitSuccess- sourceErr `shouldBe` ""- sourceOut `shouldContain` "\"schema\":\"keiro-dsl/source-inspection/1\""- sourceOut `shouldContain` "\"kind\":\"source\""- sourceOut `shouldContain` "\"sourceForm\":\"declared\""- sourceOut `shouldContain` "\"declaredLanguageVersion\":4"- sourceOut `shouldContain` "\"effectiveLanguageVersion\":4"- sourceOut `shouldContain` "\"effectiveSemanticContract\":{"- sourceOut `shouldContain` "\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/3\""- sourceOut `shouldContain` "\"languageSupport\":\"stable\""- (workspaceCode, workspaceOut, workspaceErr) <- runKeiroDsl ["inspect", canonicalWorkspacePath, "--format=json"]- workspaceCode `shouldBe` ExitSuccess- workspaceErr `shouldBe` ""- workspaceOut `shouldContain` "\"kind\":\"workspace\""- workspaceOut `shouldContain` "\"service\":\"demo-project\""- workspaceOut `shouldContain` "\"effectiveSemanticContract\":{"- workspaceOut `shouldContain` "\"languageSupport\":\"stable\""- workspaceOut `shouldSatisfy` orderedSubstrings ["domain/project-artifact.keiro", "domain/project.keiro", "domain/shared.keiro"]-- it "keeps only the named source-version compatibility fixtures outside stable v4" $ do- fixtureTree <- treeSnapshot "test/fixtures"- let outsideStableV4 =- sort- [ path- | (path, contents) <- fixtureTree,- takeExtension path == ".keiro",- "language keiro-dsl 4" `notElem` T.lines contents- ]- outsideStableV4- `shouldBe` sort- [ "aggregate-collection-expressions-v2-rejects.keiro",- "aggregate-scalar-expressions-v1-rejects.keiro",- "contract-v1-compat.keiro",- "id-domain-migration-v3.keiro",- "language-duplicate.keiro",- "language-future.keiro",- "language-identifier-v1.keiro",- "language-identifier-v2.keiro",- "language-legacy.keiro",- "language-malformed.keiro",- "language-misplaced.keiro",- "language-v1.keiro",- "language-zero.keiro",- "nominal-v1.keiro"- ]-- it "checks v1, rejects a future contract once, and inspects legacy explicitly" $ do- (v1Code, v1Out, v1Err) <- runKeiroDsl ["check", "test/fixtures/language-v1.keiro"]- v1Code `shouldBe` ExitSuccess- v1Out `shouldBe` "OK\n"- v1Err `shouldBe` ""- (futureCode, _, futureErr) <- runKeiroDsl ["check", "test/fixtures/language-future.keiro"]- futureCode `shouldBe` ExitFailure 1- T.count "UnsupportedLanguageVersion" (T.pack futureErr) `shouldBe` 1- futureErr `shouldContain` "supported versions: 1, 2, 3, 4"- futureErr `shouldNotContain` "expecting `context`"- (legacyCode, legacyOut, legacyErr) <- runKeiroDsl ["inspect", "test/fixtures/language-legacy.keiro", "--format=json"]- legacyCode `shouldBe` ExitSuccess- legacyErr `shouldBe` ""- legacyOut `shouldContain` "\"sourceForm\":\"legacy-unversioned\""- legacyOut `shouldContain` "\"declaredLanguageVersion\":null"- legacyOut `shouldContain` "\"effectiveLanguageVersion\":1"- legacyOut `shouldContain` "\"languageSupport\":\"compatibility-only\""-- it "checks and scaffolds contextual language identifiers through the CLI" $ do- forM_ ["language-identifier-v1.keiro", "language-identifier-v2.keiro"] $ \fixture -> do- let sourcePath = "test/fixtures/" <> fixture- (checkCode, checkOut, checkErr) <- runKeiroDsl ["check", sourcePath]- checkCode `shouldBe` ExitSuccess- checkOut `shouldBe` "OK\n"- checkErr `shouldBe` ""- withTempDirectory ("keiro-dsl-" <> fixture) $ \out -> do- (scaffoldCode, _, scaffoldErr) <- runKeiroDsl ["scaffold", sourcePath, "--out", out]- scaffoldCode `shouldBe` ExitSuccess- scaffoldErr `shouldContain` "firewall: OK"-- it "preserves a workspace member's source-selection code beneath outer attribution" $ do- let manifest = "service demo\nspec domain/future.keiro\n"- futureSource = "language keiro-dsl 5\nthis body must not parse\n"- source = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/future.keiro", futureSource)])- loaded <- loadWorkspace source "service.keiro-workspace"- case loaded of- Left (WorkspaceRefused (diagnostic :| [])) -> do- wdCode diagnostic `shouldBe` WorkspaceMemberParseFailed- sourceLanguageErrorCode <$> wdSourceLanguageCause diagnostic- `shouldBe` Just UnsupportedLanguageVersion- renderWorkspaceDiagnostic "service.keiro-workspace" diagnostic- `shouldSatisfy` T.isInfixOf "UnsupportedLanguageVersion"- other -> expectationFailure ("expected one attributed source-language refusal, got " <> show other)-- it "attributes a workspace provenance-only diff to the changed member" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- case wsMembers workspace of- firstMember : remaining -> do- let changedMember = firstMember {wmSourceLanguage = LegacyUnversioned}- changedWorkspace = workspace {wsMembers = changedMember : remaining}- changes = diffWorkspaces workspace changedWorkspace- map (changeCode . wcChange) changes `shouldBe` [SourceLanguageDeclarationChanged]- map (fmap osFile . wcDeclarationSite) changes `shouldBe` [Just (wmPath firstMember)]- map wcChange changes `shouldSatisfy` all (not . gatedBreaking (gateWith [minBound .. maxBound]))- _ -> expectationFailure "canonical workspace had no member"-- describe "ID domain" $ do- let parseRight name source = case parseSource name source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value-- it "registers language 3 as the first enforced runtime contract" $ do- parsed <- case parseSource "id-domain-v3.keiro" "language keiro-dsl 3\ncontext id-domain\n" of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let contract = checkedLanguageContract (checkedSource parsed)- effectiveRuntimeSemantics contract `shouldBe` "keiro-dsl/runtime-semantics/2"- effectiveContractLanguageVersion contract `shouldBe` maybe (error "missing v3") id (languageVersion 3)- idDomainContractFor contract "req" `shouldSatisfy` (/= Nothing)-- it "registers language 4 as contract admission semantics without changing aggregate ID admission" $ do- v3 <- parseRight "id-domain-v3.keiro" "language keiro-dsl 3\ncontext id-domain\n"- v4 <- parseRight "id-domain-v4.keiro" "language keiro-dsl 4\ncontext id-domain\n"- let v3Contract = checkedLanguageContract (checkedSource v3)- v4Contract = checkedLanguageContract (checkedSource v4)- effectiveRuntimeSemantics v4Contract `shouldBe` "keiro-dsl/runtime-semantics/3"- effectiveContractLanguageVersion v4Contract `shouldBe` maybe (error "missing v4") id (languageVersion 4)- idDomainContractFor v4Contract "req" `shouldBe` idDomainContractFor v3Contract "req"- contractIdDomainContractFor v3Contract "req" `shouldBe` Nothing- contractIdDomainContractFor v4Contract "req" `shouldBe` Just (typeIdV7Domain "req")-- it "constructs typed KindIDs only after the frozen four-way admission policy" $ do- let valid = "req_01h455vb4pex5vsknk084sn02q"- uppercase = "req_01H455VB4PEX5VSKNK084SN02Q"- nonV7 = "req_00041061050r3gg28a1c60t3gf"- (KindID.toText @"req" <$> parseKindIdV7Text @"req" valid) `shouldBe` Right valid- parseKindIdV7Text @"req" "req-1" `shouldSatisfy` \case- Left IdDomainMalformed {} -> True- _ -> False- parseKindIdV7Text @"req" "other_01h455vb4pex5vsknk084sn02q" `shouldSatisfy` \case- Left (IdDomainWrongPrefix "req" "other") -> True- _ -> False- parseKindIdV7Text @"req" uppercase `shouldBe` Left IdDomainNonCanonical- parseKindIdV7Text @"req" nonV7 `shouldSatisfy` \case- Left IdDomainNotUuidV7 {} -> True- _ -> False- parseEither (parseKindIdV7Value @"req") (Aeson.String uppercase)- `shouldSatisfy` \case- Left problem -> "not canonical lowercase" `T.isInfixOf` T.pack problem- Right _ -> False-- it "validates contract TypeID prefixes only at the language-4 boundary" $ do- let source versionNumber =- T.unlines- [ "language keiro-dsl " <> T.pack (show versionNumber),- "context invalid-contract-prefix",- "contract emergency {",- " schemaVersion 1",- " discriminator messageType",- " topic incidentEvents \"emergency.incident.events\"",- " event IncidentDeclared on incidentEvents {",- " incidentId: typeid \"Bad\"",- " }",- "}"- ]- v3 <- parseRight "contract-prefix-v3.keiro" (source (3 :: Int))- v4 <- parseRight "contract-prefix-v4.keiro" (source (4 :: Int))- validateService (checkedSource v3) `shouldBe` []- case validateService (checkedSource v4) of- [diagnostic] -> do- code diagnostic `shouldBe` ContractInvalidTypeIdPrefix- line diagnostic `shouldBe` 3- message diagnostic `shouldSatisfy` T.isInfixOf "contract 'emergency' event 'IncidentDeclared' field 'incidentId'"- message diagnostic `shouldSatisfy` T.isInfixOf "invalid TypeID prefix 'Bad'"- diagnostics -> expectationFailure ("expected one invalid contract prefix diagnostic, got " <> show diagnostics)-- it "keeps version-3 and version-4 aggregate fold and replay semantics equal" $ do- v3Text <- readTestText "test/fixtures/id-domain-migration-v3.keiro"- v3 <- parseRight "fold-v3.keiro" v3Text- v4 <- parseRight "fold-v4.keiro" (T.replace "language keiro-dsl 3" "language keiro-dsl 4" v3Text)- let v3Service = checkedSource v3- v4Service = checkedSource v4- fingerprints service =- [ aggregateFoldFingerprintForService service aggregate- | NAggregate aggregate <- specNodes (checkedSpec service)- ]- fingerprints v4Service `shouldBe` fingerprints v3Service- diffServices v3Service v4Service `shouldBe` []- resolvedFold (ReplayImpact.replayImpactServices v3Service v4Service) `shouldBe` ReplayNeutral-- it "keeps runtime validation and the exact Keiki text image in agreement" $ do- let contract = typeIdV7Domain "req"- sample = idDomainSampleText contract- suffix = T.drop (T.length "req_") sample- replaceAt position replacement value =- T.take position value <> T.singleton replacement <> T.drop (position + 1) value- accepted =- [ sample,- replaceAt (T.length "req_" + 10) 'f' sample,- replaceAt (T.length "req_" + 13) 'v' sample- ]- rejected =- [ "",- "req_",- "other_" <> suffix,- "req__" <> suffix,- T.dropEnd 1 sample,- sample <> "0",- T.toUpper sample,- replaceAt (T.length "req_" + 0) '8' sample,- replaceAt (T.length "req_" + 10) 'd' sample,- replaceAt (T.length "req_" + 13) 'c' sample- ]- patternValue = either (error . show) id (idDomainTextPattern contract)- idDomainVersion contract `shouldBe` "keiro-dsl/id-domain/typeid-v7/1"- idDomainSeparator contract `shouldBe` '_'- idDomainSuffixLength contract `shouldBe` 26- idDomainMaxLength contract `shouldBe` T.length sample- forM_ accepted $ \value -> do- validateIdDomainText contract value `shouldBe` Right ()- matchesTextPattern patternValue value `shouldBe` True- forM_ rejected $ \value -> do- validateIdDomainText contract value `shouldSatisfy` isLeft- matchesTextPattern patternValue value `shouldBe` False-- it "agrees for generated canonical and malformed domain values" $ property $ do- let crockford = "0123456789abcdefghjkmnpqrstvwxyz"- segment count = vectorOf count (elements crockford)- leading <- elements "01234567"- beforeVersion <- segment 9- version <- elements "ef"- beforeVariant <- segment 2- variant <- elements "89abrstv"- afterVariant <- segment 12- let value = T.pack ("req_" <> [leading] <> beforeVersion <> [version] <> beforeVariant <> [variant] <> afterVariant)- contract = typeIdV7Domain "req"- patternValue = either (error . show) id (idDomainTextPattern contract)- invalidValues = [T.toUpper value, "other_" <> T.drop 4 value, T.dropEnd 1 value, value <> "0"]- pure $- conjoin- ( counterexample (T.unpack value) (validateIdDomainText contract value == Right () && matchesTextPattern patternValue value)- : [counterexample (T.unpack invalid) (isLeft (validateIdDomainText contract invalid) && not (matchesTextPattern patternValue invalid)) | invalid <- invalidValues]- )-- it "enforces the same contract before consumer binding conversion and explains its version" $ do- v2Source <- readTestText "test/fixtures/nominal-scalars.keiro"- let v3Text = T.replace "language keiro-dsl 2" "language keiro-dsl 3" v2Source- parsed <- case parseSource "nominal-scalars-v3.keiro" v3Text of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let service = checkedSource parsed- spec = checkedSpec service- modules = scaffoldServiceModules (defaultContext (specContext spec)) service- generatedText suffix = case [moduleText value | value <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath value)] of- [value] -> value- values -> error ("expected one generated module ending in " <> suffix <> ", got " <> show (length values))- codecModule = generatedText "NominalLedger/Codec.hs"- projectionModule = generatedText "NominalProjections.hs"- harnessModule = generatedText "NominalLedger/Harness.hs"- validateService service `shouldBe` []- codecModule `shouldSatisfy` T.isInfixOf "case validateIdDomainText (typeIdV7Domain \"ord\") input of"- codecModule `shouldSatisfy` T.isInfixOf "Right () -> case KindID.parseText @\"ord\" input of"- projectionModule `shouldSatisfy` T.isInfixOf "idDomainTextPattern (typeIdV7Domain \"ord\")"- projectionModule `shouldSatisfy` T.isInfixOf "validateIdDomainText (typeIdV7Domain \"ord\") value"- harnessModule `shouldSatisfy` T.isInfixOf "nominal ID binding preserves canonical representations: OrderId"- harnessModule `shouldSatisfy` T.isInfixOf "nominal ID boundary rejects wrong-prefix and normalized text: OrderId"- obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligationsForService service)- let orderIdBindings = [obligation | obligation <- obligations, obligationMappedName obligation == "OrderId", obligationKind obligation == BindingValue]- map obligationIdDomainContract orderIdBindings `shouldBe` [Just "keiro-dsl/id-domain/typeid-v7/1"]- renderBindingObligations (specContext spec) obligations- `shouldSatisfy` T.isInfixOf "id-domain-contract: \"keiro-dsl/id-domain/typeid-v7/1\""-- it "reports adoption by boundary, invalidates snapshots, and preserves replay compatibility" $ do- v2Text <- readTestText "test/fixtures/id-domain-migration-v3.keiro"- let oldText = T.replace "language keiro-dsl 3" "language keiro-dsl 2" v2Text- oldSource <- case parseSource "id-domain-migration-v2.keiro" oldText of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- newSource <- case parseSource "id-domain-migration-v3.keiro" v2Text of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let oldService = checkedSource oldSource- newService = checkedSource newSource- changes = diffSources oldSource newSource- findings = [kindOfChange change | change <- changes, changeCode change == IdDomainContractChanged]- length findings `shouldBe` 1- forM_ findings $ \finding -> do- verdictFor PrivateHistoryRead (ckVector finding) `shouldBe` VCompatible- verdictFor OldBinaryReadNewEvents (ckVector finding) `shouldBe` VCompatible- verdictFor SnapshotHydration (ckVector finding) `shouldBe` VAdvisory- verdictFor PublicConsumer (ckVector finding) `shouldBe` VBreaking- verdictFor PersistedIdentity (ckVector finding) `shouldBe` VCompatible- verdictFor ConsumerBuild (ckVector finding) `shouldBe` VAdvisory- ckDetail finding `shouldSatisfy` T.isInfixOf "historical event replay retains its legacy decoder"- remediationFor (ckContext finding) (ckCode finding)- `shouldBe` RemedyDeploymentOrder RolloutProducerLast :| [RemedyStateCodecBump, RemedyRecompileConsumers, RemedyRunConformance]- [ckDetail finding | change <- changes, changeCode change == SourceLanguageDeclarationChanged, let finding = kindOfChange change]- `shouldSatisfy` all (T.isInfixOf "effective runtime semantics changed")- idDomainIdentitiesForService oldService `shouldBe` []- idDomainIdentitiesForService newService- `shouldSatisfy` any (T.isInfixOf "contract=keiro-dsl/id-domain/typeid-v7/1")- resolvedFold (ReplayImpact.replayImpactServices oldService newService) `shouldSatisfy` \case- ReplayImpact.ReplayAffected impacts ->- maybe False includeSnapshotStreams (Map.lookup "OrderBook" impacts)- ReplayImpact.ReplayNeutral -> False-- it "keeps the raw constructor outside the compiled public module surface" $- withTempDirectory "keiro-dsl-id-domain-hidden-constructor" $ \out -> do- sourceText <- readTestText "test/fixtures/id-domain-migration-v3.keiro"- parsed <- case parseSource "id-domain-migration-v3.keiro" sourceText of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let service = checkedSource parsed- spec = checkedSpec service- ctx = defaultContext (specContext spec)- modules = scaffoldServiceModules ctx service- attempt = out </> "Attempt.hs"- ghcOutput = out </> ".ghc"- result <- executeServiceScaffold out False "id-domain-migration-v3.keiro" (parsedSourceLanguage parsed) ctx service modules- result `shouldSatisfy` isRight- recordContents <- TIO.readFile (out </> recordFileName (specContext spec))- record <- case parseRecord recordContents of- Nothing -> expectationFailure "generated ID-domain scaffold record did not parse" >> fail "unreachable"- Just value -> pure value- recIdDomains record `shouldBe` idDomainIdentitiesForService service- recNominalEqualities record- `shouldSatisfy` any (T.isInfixOf "keiro-dsl/id-domain/typeid-v7/1")- createDirectoryIfMissing True ghcOutput- TIO.writeFile- attempt- ( T.unlines- [ "module Attempt where",- "import Generated.IdDomainMigration.Nominals (OrderId (..))",- "bad :: OrderId",- "bad = OrderId \"ord_LEGACY-NOT-TYPEID\""- ]- )- (exitCode, standardOutput, standardError) <-- readProcessWithExitCode- "cabal"- [ "exec",- "--",- "ghc",- "-XGHC2024",- "-XOverloadedStrings",- "-fno-code",- "-fforce-recomp",- "-outputdir",- ghcOutput,- "-i" <> out,- attempt- ]- ""- exitCode `shouldSatisfy` (/= ExitSuccess)- (standardOutput <> standardError) `shouldContain` "OrderId"-- it "emits one enforced nominal owner for a version-3 workspace" $ do- manifest <- readTestText "test/fixtures/workspace-nominals/service.keiro-workspace"- shared <- readTestText "test/fixtures/workspace-nominals/domain/shared.keiro"- project <- readTestText "test/fixtures/workspace-nominals/domain/project.keiro"- artifact <- readTestText "test/fixtures/workspace-nominals/domain/project-artifact.keiro"- let v3 = T.replace "language keiro-dsl 2" "language keiro-dsl 3"- source =- memoryContentSource- ( Map.fromList- [ ("service.keiro-workspace", manifest),- ("domain/shared.keiro", v3 shared),- ("domain/project.keiro", v3 project),- ("domain/project-artifact.keiro", v3 artifact)- ]- )- loaded <- loadWorkspace source "service.keiro-workspace"- workspace <- case loaded of- Left failure -> expectationFailure (show failure) >> fail "unreachable"- Right value -> pure value- plan <- case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of- Left refusals -> expectationFailure (show refusals) >> fail "unreachable"- Right value -> pure value- let paths = map (modulePath . fst) (wpModules plan)- length (filter (== "Generated/WorkspaceNominalProof/Nominals.hs") paths) `shouldBe` 1- length (filter (== "Generated/WorkspaceNominalProof/Nominals/Internal.hs") paths) `shouldBe` 1- forM_ [moduleText value | (value, _) <- wpModules plan, "/Domain.hs" `T.isSuffixOf` T.pack (modulePath value)] $ \domainText ->- domainText `shouldSatisfy` (not . T.isInfixOf "ProjectId (..)")- withTempDirectory "keiro-dsl-v3-workspace-record" $ \out -> do- emitted <- executeWorkspaceScaffold out False plan- emitted `shouldSatisfy` isRight- recordContents <- TIO.readFile (out </> workspaceRecordFileName (wsService workspace))- record <- case parseWorkspaceRecord recordContents of- Nothing -> expectationFailure "version-3 workspace record did not parse" >> fail "unreachable"- Just value -> pure value- wrIdDomains record `shouldBe` idDomainIdentitiesForService (wpCheckedService plan)- wrNominalEqualities record- `shouldSatisfy` any (T.isInfixOf "keiro-dsl/id-domain/typeid-v7/1")-- it "emits an abstract public ID, an internal legacy seam, and exact equality" $ do- v2Source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"- let v3Source = T.replace "language keiro-dsl 2" "language keiro-dsl 3" v2Source- parsed <- case parseSource "aggregate-scalar-expressions-v3.keiro" v3Source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let service = checkedSource parsed- spec = checkedSpec service- modules = scaffoldServiceModules (defaultContext (specContext spec)) service- moduleAt path = case [value | value <- modules, modulePath value == path] of- [value] -> pure value- values -> expectationFailure ("expected one module at " <> path <> ", got " <> show (map modulePath values)) >> fail "unreachable"- validateService service `shouldBe` []- publicNominals <- moduleAt "Generated/AggregateScalarExpressions/Nominals.hs"- internalNominals <- moduleAt "Generated/AggregateScalarExpressions/Nominals/Internal.hs"- domainModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Domain.hs"- codecModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Codec.hs"- transducerModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Transducer.hs"- moduleText publicNominals `shouldSatisfy` T.isInfixOf "parseRequestId"- moduleText publicNominals `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection RequestIdEqualityProjection"- moduleText publicNominals `shouldSatisfy` T.isInfixOf "idDomainTextPattern (typeIdV7Domain \"req\")"- moduleText publicNominals `shouldSatisfy` (not . T.isInfixOf "unsafeRequestIdFromLegacyText")- moduleText publicNominals `shouldSatisfy` (not . T.isInfixOf "newtype RequestId")- moduleText internalNominals `shouldSatisfy` T.isInfixOf "newtype RequestId = RequestId Text"- moduleText internalNominals `shouldSatisfy` T.isInfixOf "unsafeRequestIdFromLegacyText"- moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "RequestId (..)")- moduleText codecModule `shouldSatisfy` T.isInfixOf "unsafeRequestIdFromLegacyText <$>"- moduleText transducerModule `shouldSatisfy` T.isInfixOf "case parseRequestId"- firewallBreaches modules `shouldBe` []-- describe "scalar expressions" $ do- it "parses, validates, and round-trips the authoritative stable scalar fixture" $ do- source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"- parsed <- case parseSource "aggregate-scalar-expressions-v2.keiro" source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- validateSpec (parsedSpec parsed) `shouldBe` []- parseSource "round-trip.keiro" (renderSource parsed) `shouldBe` Right parsed- case [aggregate | NAggregate aggregate <- specNodes (parsedSpec parsed)] of- [aggregate] -> case aggTransitions aggregate of- transition : holeTransition : [] -> do- tImplementation transition `shouldBe` GeneratedImplementation- tImplementation holeTransition `shouldBe` HoleImplementation- let environment = expressionEnvironment (parsedSpec parsed) aggregate transition- case lookup "reserved" (tWrites transition) >>= either (const Nothing) Just . resolveWriteExpr environment "reserved" of- Just resolved -> do- typedScalarType resolved `shouldBe` AggregateNatural- show (typedScalarNode resolved) `shouldContain` "TotalNaturalArithmetic"- Nothing -> expectationFailure "reserved write did not resolve"- let service = checkedSource parsed- modules = scaffoldServiceModules (defaultContext (specContext (parsedSpec parsed))) service- transducer = generatedTextEndingIn "Transducer.hs" modules- holes = holeTextEndingIn "Holes.hs" modules- surface = aggregateFoldSurfaceForService service aggregate- manifest = renderManifestForService "aggregate-scalar-expressions-v2.keiro" modules service- readableTransducer = T.unwords (T.words transducer)- aggregateFoldFingerprintForService service aggregate `shouldBe` "60f4f059f718b2ee2bca06360ea20221"- T.lines surface- `shouldBe` [ "semantic-contract:keiro-dsl/runtime-semantics/2",- "state:Open|terminal=false",- "state:Reviewed|terminal=false",- "state:Closed|terminal=true",- "reg:balance:Integer=0",- "reg:reserved:Natural=0",- "reg:capacity:Natural=5",- "reg:machine:Int=0",- "reg:label:Text=\"\"",- "reg:active:Bool=False",- "reg:mode:AccountMode=Normal",- "reg:requestId:RequestId=placeholder",- "reg:openedAt:Time=(UTCTime (fromGregorian 2026 1 1) (picosecondsToDiffTime 0))",- "reg:limits:Limits=initial",- "mapped-register:Limits|wire=4463db782a5b9924|canonical=scalar-expressions.Limits.v1|binding=ScalarExpressions.Bindings.limitsBinding|binding-version=1|initial=ScalarExpressions.Bindings.initialLimits",- "nominal-equality-use:nominal-equality|name=AccountMode|contract=keiro-dsl/nominal-equality/1|key=Text|domain=finite-text:normal,restricted|owner=generated",- "nominal-equality-use:nominal-equality|name=RequestId|contract=keiro-dsl/nominal-equality/2|key=Text|domain=typeid-v7-text:req:keiro-dsl/id-domain/typeid-v7/1|owner=generated",- "transition:live|Open|Adjust|implementation=generated|guard=cmd.balance + reg.balance >= -100 && reg.reserved + cmd.requested <= reg.capacity && cmd.observedAt >= reg.openedAt && cmd.limits.minimum >= reg.limits.minimum && cmd.active == false && cmd.mode == reg.mode && cmd.requestId == reg.requestId|writes=balance:=reg.balance + cmd.balance * 2;reserved:=reg.reserved + (cmd.requested - reg.capacity);machine:=-7;label:=\"adjusted\";active:=true;mode:=AccountMode.Restricted;requestId:=RequestId(\"req_01h455vb4pex5vsknk084sn02q\");openedAt:=\"2026-02-03T04:05:06Z\";limits:=cmd.limits|emits=Adjusted|outputs=Adjusted=generated-command-identity:Adjust[balance=balance:Integer,requested=requested:Natural,machine=machine:Int,label=label:Text,active=active:Bool,mode=mode:AccountMode,requestId=requestId:RequestId,observedAt=observedAt:Time,limits=limits:Limits]|goto=Reviewed",- "transition:live|Reviewed|Close|implementation=hole|guard=|writes=|emits=ClosedEvent|outputs=ClosedEvent=generated-command-identity:Close[balance=balance:Integer]|goto=Closed"- ]- diffServices service service `shouldBe` []- resolvedFold (ReplayImpact.replayImpactServices service service) `shouldBe` ReplayNeutral- manifest `shouldSatisfy` (not . T.isInfixOf "Generated.AggregateScalarExpressions.ScalarAccount.Expressions")- map modulePath modules `shouldSatisfy` all (not . T.isSuffixOf "Expressions.hs" . T.pack)- map modulePath modules `shouldSatisfy` any (T.isSuffixOf "Transducer.hs" . T.pack)- transducer `shouldSatisfy` T.isInfixOf "let commandLimitsMinimum = K.inpProj"- transducer `shouldSatisfy` T.isInfixOf "registerLimitsMinimum = K.regProj"- readableTransducer `shouldSatisfy` T.isInfixOf "B.requireGuard $ (((((d.balance .+ B.reg @\"balance\" .>= K.lit (-100 :: Integer) .&& B.reg @\"reserved\" .+ d.requested .<= B.reg @\"capacity\") .&& d.observedAt .>= B.reg @\"openedAt\") .&& commandLimitsMinimum .>= registerLimitsMinimum) .&& d.active .== K.lit False) .&& commandMode .== registerMode) .&& commandRequestId .== registerRequestId"- transducer `shouldSatisfy` T.isInfixOf "B.slot @\"balance\" =: (B.reg @\"balance\" .+ d.balance .* K.lit (2 :: Integer))"- transducer `shouldSatisfy` T.isInfixOf "B.slot @\"reserved\" =: (B.reg @\"reserved\" .+ (d.requested .- B.reg @\"capacity\"))"- transducer `shouldSatisfy` (not . T.isInfixOf "K.PAnd")- transducer `shouldSatisfy` (not . T.isInfixOf "K.tadd")- transducer `shouldSatisfy` T.isInfixOf "scalarAccountPredicateVerifications"- transducer `shouldSatisfy` T.isInfixOf "S.verifyPredicate predicate"- transducer `shouldSatisfy` T.isInfixOf "B.emit wireAdjusted (AdjustedTermFields"- transducer `shouldSatisfy` T.isInfixOf "balance = d.balance"- transducer `shouldSatisfy` (not . T.isInfixOf "transition1OpenAdjustOutput1Adjusted")- holes `shouldSatisfy` (not . T.isInfixOf "transition1OpenAdjustOutput1Adjusted")- holes `shouldSatisfy` (not . T.isInfixOf "transition2ReviewedCloseOutput1ClosedEvent")- holes `shouldSatisfy` T.isInfixOf "transition2ReviewedCloseHoleFoldVersion"- holes `shouldSatisfy` (not . T.isInfixOf "scalarAccountTransducer")- firewallBreaches modules `shouldBe` []- _ -> expectationFailure "expected one generated and one Hole scalar transition"- _ -> expectationFailure "expected one scalar aggregate"-- it "pins every readable operator, equal-precedence child position, and bare Boolean guard" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context readable-renderer",- "enum RenderStatus { Ready=ready Waiting=waiting }",- "aggregate Renderer",- " regs",- " total Integer = 0",- " leftNested Integer = 0",- " product Integer = 1",- " status RenderStatus = Ready",- " states Open Closed!",- " command Evaluate { left:Integer right:Integer third:Integer status:RenderStatus }",- " event Evaluated = fields(Evaluate)",- " Open -- Evaluate -->",- " guard ((cmd.left < cmd.right || cmd.left <= cmd.right) || (cmd.left > cmd.right || cmd.left >= cmd.right))",- " && (cmd.left == cmd.right && cmd.left != cmd.third)",- " && cmd.status == RenderStatus.Waiting",- " write total := reg.total + (cmd.left - cmd.right)",- " write leftNested := (reg.leftNested + cmd.left) - cmd.right",- " write product := cmd.left * (cmd.right * cmd.third)",- " write status := RenderStatus.Ready",- " emit Evaluated",- " goto Closed",- "aggregate BooleanRenderer",- " regs",- " enabled Bool = False",- " states Open Closed!",- " command Enable { enabled:Bool }",- " event Enabled = fields(Enable)",- " Open -- Enable -->",- " guard cmd.enabled",- " write enabled := cmd.enabled",- " emit Enabled",- " goto Closed"- ]- spec <- parseInlineSpec "<readable-renderer>" source- errorCodes spec `shouldBe` []- let modules = scaffoldModules (defaultContext (specContext spec)) spec- moduleAt suffix = case [moduleText value | value <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath value)] of- [value] -> pure value- values -> expectationFailure ("expected one generated module ending in " <> suffix <> ", got " <> show (length values)) >> fail "unreachable"- renderer <- moduleAt "/Renderer/Transducer.hs"- booleanRenderer <- moduleAt "/BooleanRenderer/Transducer.hs"- let normalizedRenderer = T.unwords (T.words renderer)- normalizedBooleanRenderer = T.unwords (T.words booleanRenderer)- renderer- `shouldSatisfy` T.isInfixOf "import Keiki.Core (HsPred, SymTransducer, (.*), (.+), (.-), (.==), (./=), (.<), (.<=), (.>), (.>=), (.&&), (.||))"- normalizedRenderer- `shouldSatisfy` T.isInfixOf "(d.left .< d.right .|| d.left .<= d.right) .|| d.left .> d.right .|| d.left .>= d.right"- normalizedRenderer- `shouldSatisfy` T.isInfixOf ".&& d.left .== d.right .&& d.left ./= d.third"- normalizedRenderer- `shouldSatisfy` T.isInfixOf ".&& commandStatus .== K.lit (\"waiting\" :: Text)"- renderer- `shouldSatisfy` T.isInfixOf "B.slot @\"total\" =: (B.reg @\"total\" .+ (d.left .- d.right))"- renderer- `shouldSatisfy` T.isInfixOf "B.slot @\"leftNested\" =: (B.reg @\"leftNested\" .+ d.left .- d.right)"- renderer- `shouldSatisfy` T.isInfixOf "B.slot @\"product\" =: d.left .* (d.right .* d.third)"- normalizedBooleanRenderer `shouldSatisfy` T.isInfixOf "B.requireGuard $ d.enabled .== K.lit True"- firewallBreaches modules `shouldBe` []-- it "suffixes normalized projection-alias collisions deterministically" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context projection-alias-collision",- "mapped structural record AliasCollision {",- " haskell package=keiro-dsl module=Renderer.Domain type=AliasCollision",- " binding = \"Renderer.Bindings.aliasCollisionBinding\"",- " binding-version = \"1\"",- " canonical-type = \"renderer.AliasCollision.v1\"",- " fixtures = \"Renderer.Bindings.aliasCollisionCases\"",- " initial = \"Renderer.Bindings.initialAliasCollision\"",- " wire object constructor=AliasCollision unknown-fields=reject {",- " dash as \"foo-bar\" : Integer required",- " underscore as \"foo_bar\" : Integer required",- " }",- "}",- "aggregate AliasRenderer",- " regs",- " values AliasCollision = initial",- " states Open Closed!",- " command Compare { values:AliasCollision }",- " event Compared = fields(Compare)",- " Open -- Compare -->",- " guard cmd.values.dash == reg.values.dash",- " && cmd.values.underscore == reg.values.underscore",- " emit Compared",- " goto Closed"- ]- spec <- parseInlineSpec "<projection-alias-collision>" source- errorCodes spec `shouldBe` []- let transducer = generatedTextEndingIn "Transducer.hs" (scaffoldModules (defaultContext (specContext spec)) spec)- transducer `shouldSatisfy` T.isInfixOf "let commandValuesFooBar = K.inpProj"- transducer `shouldSatisfy` T.isInfixOf "registerValuesFooBar = K.regProj"- transducer `shouldSatisfy` T.isInfixOf "commandValuesFooBar2 = K.inpProj"- transducer `shouldSatisfy` T.isInfixOf "registerValuesFooBar2 = K.regProj"-- it "keeps evolution identity independent of module layout but sensitive to checked behavior" $ do- source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"- original <- case parseSource "aggregate-scalar-expressions-v2.keiro" source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- changed <- case parseSource "aggregate-scalar-expressions-changed.keiro" (T.replace "cmd.active == false" "cmd.active == true" source) of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- case ( [aggregate | NAggregate aggregate <- specNodes (parsedSpec original)],- [aggregate | NAggregate aggregate <- specNodes (parsedSpec changed)]- ) of- ([originalAggregate], [changedAggregate]) -> do- let service = checkedSource original- changedService = checkedSource changed- prefixed = defaultContext (specContext (parsedSpec original))- collocated = prefixed {moduleRoot = "Acme", placement = CollocatedLeaf}- prefixedModules = scaffoldServiceModules prefixed service- collocatedModules = scaffoldServiceModules collocated service- originalSurface = aggregateFoldSurfaceForService service originalAggregate- originalFingerprint = aggregateFoldFingerprintForService service originalAggregate- map modulePath prefixedModules `shouldNotBe` map modulePath collocatedModules- sum (map (T.length . moduleText) prefixedModules) `shouldSatisfy` (> 0)- sum (map (T.length . moduleText) collocatedModules) `shouldSatisfy` (> 0)- aggregateFoldSurfaceForService service originalAggregate `shouldBe` originalSurface- aggregateFoldFingerprintForService service originalAggregate `shouldBe` originalFingerprint- aggregateFoldSurfaceForService changedService changedAggregate `shouldNotBe` originalSurface- aggregateFoldFingerprintForService changedService changedAggregate `shouldNotBe` originalFingerprint- found -> expectationFailure ("expected one aggregate before and after behavior mutation, got " <> show (length (fst found), length (snd found)))-- it "rejects cross-command fields(Command) output before scaffolding" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context output-command-mismatch",- "aggregate Account",- " regs",- " states Open Closed!",- " command OpenAccount { accountId:Text }",- " command CloseAccount { accountId:Text }",- " event AccountOpened = fields(OpenAccount)",- " Open -- CloseAccount --> emit AccountOpened ; goto Closed"- ]- spec <- parseInlineSpec "<output-command-mismatch>" source- errorCodes spec `shouldContain` [EventOutputCommandMismatch]- case [aggregate | NAggregate aggregate <- specNodes spec] of- [aggregate] -> case aggTransitions aggregate of- [transition] ->- eventOutputMapping spec aggregate transition 1 "AccountOpened"- `shouldBe` Left (OutputCommandMismatch "OpenAccount" "CloseAccount" "AccountOpened")- _ -> expectationFailure "expected one transition"- _ -> expectationFailure "expected one aggregate"-- it "rejects Int arithmetic and mixed numeric operands before scaffolding" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-errors",- "aggregate Counter",- " regs",- " machine Int = 0",- " exact Integer = 0",- " states Open Closed!",- " command Add { machine:Int exact:Integer }",- " event Added = fields(Add)",- " Open -- Add -->",- " guard cmd.machine + 1 >= 0 && cmd.exact == cmd.machine",- " emit Added",- " goto Closed"- ]- spec <- parseInlineSpec "<scalar-errors>" source- errorCodes spec `shouldContain` [AggregateExpressionOperatorUnsupported, AggregateExpressionOperandTypeMismatch]-- it "rejects nominal type confusion and unqualified enum values at source checking" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context nominal-type-confusion",- "id OrderId prefix=ord",- "id UserId prefix=usr",- "enum OrderStatus { Draft=draft Submitted=submitted }",- "enum UserStatus { Active=active Disabled=disabled }",- "aggregate Account",- " regs",- " orderId OrderId = placeholder",- " status OrderStatus = Draft",- " states Open Closed!",- " command Compare { orderId:OrderId userId:UserId status:OrderStatus userStatus:UserStatus label:Text }",- " event Compared = fields(Compare)",- " Open -- Compare -->",- " guard cmd.orderId == cmd.userId",- " && cmd.status == cmd.userStatus",- " && cmd.orderId == cmd.label",- " && cmd.status == Draft",- " emit Compared",- " goto Closed"- ]- spec <- parseInlineSpec "<nominal-type-confusion>" source- let diagnostics = validateSpec spec- length [() | diagnostic <- diagnostics, code diagnostic == AggregateExpressionOperandTypeMismatch]- `shouldBe` 3- errorCodes spec `shouldContain` [AggregateExpressionRootUnknown]- T.unlines (map message diagnostics) `shouldSatisfy` T.isInfixOf "qualify"-- it "rejects machine-Int arithmetic at both platform bounds" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-int-bounds",- "aggregate Counter",- " regs",- " machine Int = 0",- " states Open Closed!",- " command Set { machine:Int }",- " event SetEvent = fields(Set)",- " Open -- Set -->",- " guard cmd.machine + 1 >= " <> T.pack (show (minBound :: Int)),- " && cmd.machine - 1 <= " <> T.pack (show (maxBound :: Int)),- " emit SetEvent",- " goto Closed"- ]- spec <- parseInlineSpec "<scalar-int-bounds>" source- length [() | diagnostic <- validateSpec spec, code diagnostic == AggregateExpressionOperatorUnsupported]- `shouldBe` 2-- it "rejects predicate-valued Bool writes that Keiki cannot represent as scalar terms" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-bool-write",- "aggregate Flag",- " regs",- " active Bool = False",- " states Open Closed!",- " command Set { active:Bool }",- " event SetEvent = fields(Set)",- " Open -- Set -->",- " write active := cmd.active == true",- " emit SetEvent",- " goto Closed"- ]- spec <- parseInlineSpec "<scalar-bool-write>" source- errorCodes spec `shouldContain` [AggregateExpressionOperatorUnsupported]-- it "requires explicit roots for a same-named register and command field" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-ambiguity",- "aggregate Counter",- " regs",- " amount Integer = 0",- " states Open Closed!",- " command Set { amount:Integer }",- " event SetEvent = fields(Set)",- " Open -- Set -->",- " guard amount == 0",- " emit SetEvent",- " goto Closed"- ]- spec <- parseInlineSpec "<scalar-ambiguity>" source- errorCodes spec `shouldContain` [AggregateExpressionRootAmbiguous]-- it "enforces exclusive Hole ownership and preserves its canonical spelling" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-hole",- "aggregate Counter",- " regs",- " amount Integer = 0",- " states Open Closed!",- " command Set { amount:Integer }",- " event SetEvent = fields(Set)",- " Open -- Set -->",- " implementation hole",- " guard cmd.amount >= 0",- " emit SetEvent",- " goto Closed"- ]- parsed <- case parseSource "<scalar-hole>" source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- errorCodes (parsedSpec parsed) `shouldContain` [AggregateTransitionOwnershipConflict]- renderSource parsed `shouldSatisfy` T.isInfixOf "implementation hole"-- it "generates a stable per-transition Hole boundary and fold token" $ do- let source =- T.unlines- [ "language keiro-dsl 2",- "context scalar-hole",- "aggregate Counter",- " regs",- " amount Integer = 0",- " states Open Closed!",- " command Set { amount:Integer }",- " event SetEvent = fields(Set)",- " Open -- Set -->",- " implementation hole",- " emit SetEvent",- " goto Closed"- ]- spec <- parseInlineSpec "<scalar-hole-valid>" source- aggregate <- case [value | NAggregate value <- specNodes spec] of- [value] -> pure value- _ -> expectationFailure "expected one Hole aggregate" >> fail "unreachable"- let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate- transducer = generatedTextEndingIn "Transducer.hs" modules- holes = holeTextEndingIn "Holes.hs" modules- errorCodes spec `shouldBe` []- map modulePath modules `shouldSatisfy` all (not . T.isSuffixOf "Expressions.hs" . T.pack)- transducer `shouldSatisfy` T.isInfixOf "Holes.transition1OpenSetHole d"- transducer `shouldSatisfy` T.isInfixOf "foldToken Holes.transition1OpenSetHoleFoldVersion"- holes `shouldSatisfy` T.isInfixOf "transition1OpenSetHole _d = B.requireGuard K.PTop"- holes `shouldSatisfy` T.isInfixOf "transition1OpenSetHoleFoldVersion = FoldVersion"- holes `shouldSatisfy` (not . T.isInfixOf "counterTransducer")-- it "pins v1 and collection rejection at their stable boundaries" $ do- v1Source <- readTestText "test/fixtures/aggregate-scalar-expressions-v1-rejects.keiro"- case parseSource "v1.keiro" v1Source of- Left (SourceLanguageFailure diagnostic) -> sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion- other -> expectationFailure ("expected v1 source-language refusal, got " <> show other)- collectionSource <- readTestText "test/fixtures/aggregate-collection-expressions-v2-rejects.keiro"- case parseSource "collections.keiro" collectionSource of- Left failure -> renderParseFailure failure `shouldSatisfy` T.isInfixOf "CollectionExpressionUnsupported"- Right _ -> expectationFailure "collection syntax unexpectedly parsed"-- it "keeps arithmetic operands intact when complementing a scalar comparison" $ do- let left = EAdd noLoc (EPath noLoc CommandRoot ["balance"]) (ELiteral noLoc (LiteralIntegral 2))- right = ESubtract noLoc (EPath noLoc RegisterRoot ["balance"]) (ELiteral noLoc (LiteralIntegral 3))- predicate = ECmp OpLt left right- complementExpr predicate `shouldBe` ECmp OpGe left right- complementExpr (complementExpr predicate) `shouldBe` predicate-- it "keeps the committed scalar-expression conformance tree fresh" $ do- modules <- scaffoldFixture "test/fixtures/aggregate-scalar-expressions-v2.keiro"- forM_ [generatedModule | generatedModule <- modules, kind generatedModule == Generated] $ \generatedModule -> do- committed <- readTestText ("test/conformance-scalar-expressions/" <> modulePath generatedModule)- normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generatedModule)-- describe "behavior obligations" $ do- it "inventories every live-reachable cell, guarded edge, terminal rejection, and replay edge" $ do- spec <- specOf "test/fixtures/behavior-complete.keiro"- requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)- length requirements `shouldBe` 14- length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.LiveTransition] `shouldBe` 5- length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.RequiredRejection] `shouldBe` 8- length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.ReplayTransition] `shouldBe` 1- [Behavior.requirementSource requirement | requirement <- requirements, Behavior.requirementKind requirement == Behavior.RequiredRejection]- `shouldContain` ["Active", "Closed"]- length [() | requirement <- requirements, Behavior.requirementGuardCoverage requirement == Behavior.GuardTotal] `shouldBe` 4- length [() | requirement <- requirements, Behavior.requirementGuardCoverage requirement == Behavior.GuardUnknown] `shouldBe` 1- let report = Behavior.BehaviorObligationsReport "behavior-complete.keiro" Nothing requirements- encoded = Behavior.encodeBehaviorObligationsJson report- encoded `shouldSatisfy` T.isInfixOf "\"schema\":\"keiro-dsl/behavior-obligations/1\""- encoded `shouldSatisfy` T.isInfixOf "\"source\":\"Closed\""- encoded `shouldSatisfy` T.isInfixOf "\"kind\":\"replay-transition\""- encoded `shouldSatisfy` (not . T.isInfixOf "\"filled\"")- encoded `shouldSatisfy` (not . T.isInfixOf "\"missing\"")-- it "keeps semantic keys stable across line movement and canonical pretty printing" $ do- source <- readTestText "test/fixtures/behavior-complete.keiro"- parsed <- case parseSource "behavior-complete.keiro" source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let original = parsedSpec parsed- moved <- parseInlineSpec "behavior-complete-moved.keiro" ("# line movement must not rename witnesses\n\n" <> source)- pretty <- parseInlineSpec "behavior-complete-pretty.keiro" (renderSource parsed)- let keys spec = fmap (map Behavior.requirementKey) (Behavior.deriveBehaviorRequirements spec)- keys moved `shouldBe` keys original- keys pretty `shouldBe` keys original-- it "generates direct fields(Command) output and separate create-once pending witnesses" $ do- spec <- specOf "test/fixtures/behavior-complete.keiro"- aggregate <- case [value | NAggregate value <- specNodes spec] of- [value] -> pure value- _ -> expectationFailure "expected one behavior-complete aggregate" >> fail "unreachable"- let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate- transducer = generatedTextEndingIn "Transducer.hs" modules- codec = generatedTextEndingIn "Codec.hs" modules- contract = generatedTextEndingIn "BehaviorContract.hs" modules- behaviorHoles = case [moduleText value | value <- modules, T.isSuffixOf "BehaviorHoles.hs" (T.pack (modulePath value))] of- [value] -> value- values -> error ("expected one BehaviorHoles module, got " <> show (length values))- ordinaryHoles = [value | value <- modules, T.isSuffixOf "/Holes.hs" (T.pack (modulePath value)), not (T.isSuffixOf "BehaviorHoles.hs" (T.pack (modulePath value)))]- transducer `shouldSatisfy` T.isInfixOf "requestId = d.requestId"- transducer `shouldSatisfy` T.isInfixOf "observedAt = d.observedAt"- transducer `shouldSatisfy` T.isInfixOf "amount = d.amount"- transducer `shouldSatisfy` T.isInfixOf "details = d.details"- codec `shouldSatisfy` T.isInfixOf "display_label"- codec `shouldSatisfy` T.isInfixOf "optional_note"- transducer `shouldSatisfy` (not . T.isInfixOf "Output")- ordinaryHoles `shouldBe` []- obsoleteGeneratedOutputHooks spec `shouldContain` [("Journey", "transition1EmptyStartOutput1Started")]- contract `shouldSatisfy` T.isInfixOf "keiro/behavior-conformance/1"- contract `shouldSatisfy` T.isInfixOf "commandKind command == requirementCommandName requirement"- T.count "Pending (BehaviorKey " behaviorHoles `shouldBe` 14- behaviorHoles `shouldSatisfy` (not . T.isInfixOf "undefined")- behaviorHoles `shouldSatisfy` (not . T.isInfixOf "error")-- it "rejects eventless state or register changes while accepting a true no-op" $ do- invalid <-- parseInlineSpec "<eventless-change>" $- T.unlines- [ "language keiro-dsl 2",- "context eventless-change",- "aggregate Counter",- " regs",- " count Natural = 0",- " states Open Closed!",- " command Tick { count:Natural }",- " Open -- Tick --> write count := cmd.count ; goto Closed"- ]- errorCodes invalid `shouldContain` [AggregateEventlessStateChange]- valid <-- parseInlineSpec "<eventless-noop>" $- T.unlines- [ "language keiro-dsl 2",- "context eventless-noop",- "aggregate Counter",- " regs",- " count Natural = 0",- " states Open",- " command Tick { count:Natural }",- " Open -- Tick --> goto Open"- ]- errorCodes valid `shouldBe` []-- it "refuses duplicate semantic behavior identities before scaffolding" $ do- duplicate <-- parseInlineSpec "<duplicate-behavior>" $- T.unlines- [ "language keiro-dsl 2",- "context duplicate-behavior",- "aggregate Counter",- " regs",- " states Open",- " command Tick { amount:Natural }",- " event Ticked = fields(Tick)",- " Open -- Tick --> emit Ticked ; goto Open",- " Open -- Tick --> emit Ticked ; goto Open"- ]- let isBehaviorRefusal (BehaviorRefusal _) = True- isBehaviorRefusal _ = False- case planScaffold (defaultContext (specContext duplicate)) duplicate of- Left refusals -> refusals `shouldSatisfy` any isBehaviorRefusal- Right _ -> expectationFailure "duplicate behavior identity reached a scaffold write set"-- it "round-trips additive single-file and workspace behavior rows with member ownership" $ do- spec <- specOf "test/fixtures/behavior-complete.keiro"- requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)- version <- maybe (expectationFailure "language version 2 was not constructible" >> fail "unreachable") pure (languageVersion 2)- let rows = Behavior.behaviorRecordRows requirements- singleRecord =- ScaffoldRecord- { recSpecPath = "behavior-complete.keiro",- recModuleRoot = "",- recLayout = "prefixed",- recSourceLanguage = DeclaredLanguage version noLoc,- recLanguageContract = effectiveLanguageContract (DeclaredLanguage version noLoc),- recFiles = [],- recMappings = [],- recIdDomains = [],- recNominalEqualities = [],- recBindingObligations = [],- recBehaviorRequirements = rows- }- T.count "behavior " (renderRecord singleRecord) `shouldBe` 14- parseRecord (renderRecord singleRecord) `shouldBe` Just singleRecord-- workspace <- shouldComposeWorkspace "test/fixtures/behavior-complete-workspace/service.keiro-workspace"- workspaceRequirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements (wsMergedSpec workspace))- let ownedRequirements =- map- (Behavior.attributeBehaviorOwner (fmap fst . nodeOwner (wsOwnership workspace) "aggregate"))- workspaceRequirements- ownedRows = Behavior.behaviorRecordRows ownedRequirements- workspaceRecord =- WorkspaceRecord- { wrService = wsService workspace,- wrManifest = "service.keiro-workspace",- wrContext = wsContext workspace,- wrModuleRoot = "",- wrLayout = "prefixed",- wrMembers = map wmPath (wsMembers workspace),- wrSourceLanguages = [WorkspaceSourceLanguageRow (wmPath member) (wmSourceLanguage member) | member <- wsMembers workspace],- wrLanguageContract = wsLanguageContract workspace,- wrModules = [],- wrMappings = [],- wrIdDomains = [],- wrNominalEqualities = [],- wrBindingObligations = [],- wrBehaviorRequirements = ownedRows,- wrAdopted = []- }- map Behavior.behaviorRecordOwner ownedRows `shouldSatisfy` all (== Just "journey.keiro")- T.count "behavior " (renderWorkspaceRecord workspaceRecord) `shouldBe` 14- parseWorkspaceRecord (renderWorkspaceRecord workspaceRecord) `shouldBe` Just workspaceRecord-- describe "nominal consumer types" $ do- it "resolves every category through one checked registry and explains exact obligations" $ do- spec <- specOf "test/fixtures/nominal-scalars.keiro"- errorCodes spec `shouldBe` []- registry <- case resolveNominalTypes spec of- Left errors -> expectationFailure (show errors) >> fail "unreachable"- Right value -> pure value- Map.keys (nominalTypes registry)- `shouldBe` ["AccountNumber", "FeatureFlag", "ObservedAt", "OrderId", "OrderStatus", "RiskScore", "SequenceNumber"]- obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligations spec)- length obligations `shouldBe` 21- map obligationCategory obligations `shouldSatisfy` all (`elem` ["nominal-id", "nominal-enum", "nominal-scalar"])- length [() | obligation <- obligations, obligationEqualityContract obligation /= Nothing] `shouldBe` 2- renderBindingObligations (specContext spec) obligations `shouldSatisfy` T.isInfixOf "equality-contract:"- let signatures = map obligationSignature obligations- forM_- [ "orderIdBinding :: NominalBinding NominalConformance.Domain.OrderId (KindID \"ord\")",- "orderStatusBinding :: NominalBinding NominalConformance.Domain.OrderStatus Generated.NominalScalars.Nominal.Shape.OrderStatus.OrderStatusRepresentation",- "accountNumberBinding :: NominalBinding NominalConformance.Domain.AccountNumber Text",- "orderIdFixtures :: NominalFixtureCases NominalConformance.Domain.OrderId",- "initialAccountNumber :: NominalConformance.Domain.AccountNumber"- ]- (`shouldSatisfy` (`elem` signatures))- map obligationCanonicalType obligations `shouldSatisfy` all (/= Nothing)- let rendered = renderBindingObligations (specContext spec) obligations- rendered `shouldSatisfy` T.isInfixOf "nominal-id type OrderId"- rendered `shouldSatisfy` T.isInfixOf "canonical-type: \"nominal.OrderId.v1\""- case obligations of- firstObligation : _ ->- (Aeson.eitherDecode (Aeson.encode firstObligation) :: Either String BindingObligation)- `shouldBe` Right firstObligation- [] -> expectationFailure "expected nominal binding obligations"-- it "allocates distinct stable diagnostics for incomplete or incompatible nominal declarations" $ do- missing <- errorCodesOf "test/fixtures/nominal-missing-facts.keiro"- missing `shouldBe` replicate 5 NominalMissingIngredient- errorCodesOf "test/fixtures/nominal-bad-qualified.keiro" `shouldReturn` [NominalInvalidQualifiedName]- errorCodesOf "test/fixtures/nominal-invalid-prefix.keiro" `shouldReturn` replicate 2 NominalInvalidIdPrefix- errorCodesOf "test/fixtures/nominal-unsupported-representation.keiro" `shouldReturn` [NominalUnsupportedRepresentation]- errorCodesOf "test/fixtures/nominal-missing-initial.keiro" `shouldReturn` [NominalMissingInitialValue]- errorCodesOf "test/fixtures/nominal-name-collision.keiro" `shouldReturn` [NominalNameCollision, NominalNameCollision]-- it "keeps v1 rejection at the source-language boundary" $ do- source <- readTestText "test/fixtures/nominal-v1.keiro"- case parseSource "nominal-v1.keiro" source of- Left (SourceLanguageFailure diagnostic) -> sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion- other -> expectationFailure ("expected source-language refusal, got " <> show other)-- it "scaffolds consumer types, checked codecs, enum representation, projections, and deterministic manifests" $ do- spec <- specOf "test/fixtures/nominal-scalars.keiro"- let ctx = defaultContext (specContext spec)- modules = scaffoldModules ctx spec- moduleAt path = case [value | value <- modules, modulePath value == path] of- [value] -> pure value- values -> expectationFailure ("expected one module at " <> path <> ", got " <> show (map modulePath values)) >> fail "unreachable"- domainModule <- moduleAt "Generated/NominalScalars/NominalLedger/Domain.hs"- codecModule <- moduleAt "Generated/NominalScalars/NominalLedger/Codec.hs"- enumModule <- moduleAt "Generated/NominalScalars/Nominal/Shape/OrderStatus.hs"- projectionModule <- moduleAt "Generated/NominalScalars/NominalProjections.hs"- bindingModule <- moduleAt "NominalConformance/Bindings.hs"- map modulePath modules `shouldNotContain` ["NominalScalars/NominalLedger/Holes.hs"]- moduleText domainModule `shouldSatisfy` T.isInfixOf "import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)"- moduleText domainModule `shouldSatisfy` T.isInfixOf "orderId :: !OrderId"- moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "NominalConformance.Domain.OrderId")- moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "newtype OrderId")- moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "data OrderStatus =")- moduleText codecModule `shouldSatisfy` T.isInfixOf "KindID.parseText @\"ord\""- moduleText codecModule `shouldSatisfy` T.isInfixOf "KindID.toText (nominalToRepresentation"- moduleText codecModule `shouldSatisfy` T.isInfixOf "nominalFromRepresentation"- forM_ ["coerce", "unsafe", "read ", "error "] $ \forbidden ->- moduleText codecModule `shouldSatisfy` (not . T.isInfixOf forbidden)- moduleText enumModule `shouldSatisfy` T.isInfixOf "data OrderStatusRepresentation = Draft | Submitted"- moduleText enumModule `shouldSatisfy` (not . T.isInfixOf "NominalConformance")- moduleText projectionModule `shouldSatisfy` T.isInfixOf "type FieldOwner AccountNumberNominalProjection = AccountNumber"- moduleText projectionModule `shouldSatisfy` T.isInfixOf "projectFieldValue _ = nominalToRepresentation Bindings.accountNumberBinding"- moduleText projectionModule `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection OrderIdEqualityProjection"- moduleText projectionModule `shouldSatisfy` T.isInfixOf "textProjectionDomain orderIdEqualityPattern"- moduleText projectionModule `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection OrderStatusEqualityProjection"- moduleText projectionModule `shouldSatisfy` T.isInfixOf "finiteProjectionDomain (\"draft\" :| [\"submitted\"])"- kind bindingModule `shouldBe` HoleStub- moduleText bindingModule `shouldSatisfy` T.isInfixOf "import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)"- moduleText bindingModule `shouldSatisfy` T.isInfixOf "orderIdBinding :: NominalBinding OrderId (KindID \"ord\")"- moduleText bindingModule `shouldSatisfy` T.isInfixOf "orderStatusBinding :: NominalBinding OrderStatus ShapeOrderStatus.OrderStatusRepresentation"- firewallBreaches modules `shouldBe` []- scaffoldModules ctx spec `shouldBe` modules- manifestDependencies spec `shouldContain` ["mmzk-typeid", "nominal-conformance"]-- it "persists nominal provenance in a separate forward-compatible row kind" $ do- spec <- specOf "test/fixtures/nominal-scalars.keiro"- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let plan = consumerPlan spec- record =- ScaffoldRecord- { recSpecPath = "nominal-scalars.keiro",- recModuleRoot = "",- recLayout = "prefixed",- recSourceLanguage = LegacyUnversioned,- recLanguageContract = effectiveLanguageContract LegacyUnversioned,- recFiles = [],- recMappings = consumerMappings plan,- recIdDomains = [],- recNominalEqualities = nominalEqualityIdentities spec,- recBindingObligations = [],- recBehaviorRequirements = []- }- encoded = renderRecord record- workspaceRecord =- (sampleWorkspaceRecord workspace)- { wrMappings = consumerMappings plan- }- workspaceEncoded = renderWorkspaceRecord workspaceRecord- consumerPackages plan `shouldBe` ["nominal-conformance"]- length [() | NominalMapping {} <- consumerMappings plan] `shouldBe` 7- T.count "nominal-mapping " encoded `shouldBe` 7- T.count "nominal-equality " encoded `shouldBe` 2- T.count "\nmapping " encoded `shouldBe` 0- parseRecord encoded `shouldBe` Just record- T.count "nominal-mapping " workspaceEncoded `shouldBe` 7- T.count "nominal-equality " workspaceEncoded `shouldSatisfy` (>= 2)- T.count "\nmapping " workspaceEncoded `shouldBe` 0- parseWorkspaceRecord workspaceEncoded `shouldBe` Just workspaceRecord-- it "reports bound-ID decoder tightening and makes binding provenance replay-visible" $ do- current <- specOf "test/fixtures/nominal-scalars.keiro"- let useGeneratedIdInitial (NAggregate aggregate) =- NAggregate- aggregate- { aggRegs =- [ if regName register == "orderId"- then register {regInitial = RegInitBare "placeholder"}- else register- | register <- aggRegs aggregate- ]- }- useGeneratedIdInitial node = node- unbound =- current- { specIds = [declaration {idBinding = Nothing} | declaration <- specIds current],- specNodes = map useGeneratedIdInitial (specNodes current)- }- adoption = diffSpecs unbound current- decoderFindings = [kindOfChange change | change <- adoption, changeCode change == NominalIdDecoderTightened]- map ckSubject decoderFindings `shouldContain` ["NominalLedger event NominalsRecorded .orderId"]- decoderFindings `shouldSatisfy` all ((== VAdvisory) . verdictFor PrivateHistoryRead . ckVector)- let bumped =- current- { specIds =- [ declaration- { idBinding = fmap (\binding -> binding {nominalBindingVersion = Just "2"}) (idBinding declaration)- }- | declaration <- specIds current- ]- }- bindingChanges = diffSpecs current bumped- map changeCode bindingChanges `shouldContain` [NominalBindingChanged]- replayImpactSpecs current bumped `shouldSatisfy` \case- ReplayImpact.ReplayAffected impacts ->- maybe False (\impact -> Set.member "NominalsRecorded" (ReplayImpact.eventTypes impact) && includeSnapshotStreams impact) (Map.lookup "NominalLedger" impacts)- ReplayImpact.ReplayNeutral -> False- case [aggregate | NAggregate aggregate <- specNodes current] of- aggregate : _ -> do- aggregateFoldSurface current aggregate `shouldSatisfy` T.isInfixOf "nominal-equality-use:"- aggregateFoldSurface current aggregate `shouldNotBe` aggregateFoldSurface bumped aggregate- [] -> expectationFailure "expected nominal aggregate"-- 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- parseStableRenderedSpec "<consumer-types-round-trip>" 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", AggregateExpressionOperatorUnsupported)- ]- forM_ cases $ \(fixture, expected) ->- errorCodesOf ("test/fixtures/" <> fixture) `shouldReturn` [expected]- it "keeps Time and Natural in Keiki's curated comparison set" $ do- errorCodesOf "test/fixtures/mapped-guard-time.keiro" `shouldReturn` []- errorCodesOf "test/fixtures/mapped-guard-natural.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 "aggregate type capabilities" $ do- it "enumerates the policy for every resolved type and use site" $ do- let resolvedTypes =- [ AggregateText,- AggregateInt,- AggregateBool,- AggregateTime,- AggregateNatural,- AggregateNominal (ResolvedNominalType "EntityId" (IdRepresentation "ent") GeneratedNominal noLoc),- AggregateNominal (ResolvedNominalType "Status" (EnumRepresentation (("Active", "active") :| [])) GeneratedNominal noLoc),- AggregateNominal (ResolvedNominalType "Amount" (ScalarRepresentation NominalInt) (consumerNominalFor "Amount") noLoc),- AggregateNominal (ResolvedNominalType "Label" (ScalarRepresentation NominalText) (consumerNominalFor "Label") noLoc),- AggregateVertex "EntityVertex",- AggregateMapped (MappedKey "ConsumerValue")- ]- useSites = [minBound .. maxBound]- expected useSite resolvedType = case useSite of- OrderingGuardUse -> case resolvedType of- AggregateInt -> SolverVisible- AggregateTime -> SolverVisible- AggregateNatural -> SolverVisible- AggregateNominal nominal -> case resolvedNominalRepresentation nominal of- ScalarRepresentation NominalInt -> SolverVisible- ScalarRepresentation NominalNatural -> SolverVisible- ScalarRepresentation NominalTime -> SolverVisible- _ -> Unsupported- _ -> Unsupported- EqualityGuardUse -> case resolvedType of- AggregateMapped {} -> Unsupported- AggregateNominal {} -> SolverVisible- AggregateVertex {} -> OpaqueOnly- _ -> SolverVisible- _ -> case resolvedType of- AggregateNominal nominal -> case resolvedNominalRepresentation nominal of- ScalarRepresentation {} -> SolverVisible- _ -> OpaqueOnly- AggregateVertex {} -> OpaqueOnly- AggregateMapped {} -> OpaqueOnly- _ -> SolverVisible- actual =- [ (useSite, resolvedType, aggregateCapability useSite resolvedType)- | useSite <- useSites,- resolvedType <- resolvedTypes- ]- wanted =- [ (useSite, resolvedType, expected useSite resolvedType)- | useSite <- useSites,- resolvedType <- resolvedTypes- ]- actual `shouldBe` wanted- it "lowers direct Time and Natural through every generated aggregate boundary" $ do- spec <- specOf "test/fixtures/aggregate-scalars.keiro"- errorCodes spec `shouldBe` []- let aggregate = onlyAggregate spec- modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate- generated =- [ moduleText generatedModule- | generatedModule <- modules,- Keiro.Dsl.Scaffold.kind generatedModule == Generated- ]- domain = generatedTextEndingIn "Domain.hs" modules- codec = generatedTextEndingIn "Codec.hs" modules- domain `shouldSatisfy` T.isInfixOf "observedAt :: !UTCTime"- domain `shouldSatisfy` T.isInfixOf "revision :: !Natural"- domain `shouldSatisfy` T.isInfixOf "UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)"- domain `shouldSatisfy` T.isInfixOf "import Data.Time.Calendar (fromGregorian)"- domain `shouldSatisfy` T.isInfixOf "import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)"- domain `shouldSatisfy` T.isInfixOf "import Numeric.Natural (Natural)"- manifestDependencies spec `shouldContain` ["time"]- manifestDependencies spec `shouldNotContain` ["keiki-codec-json"]- codec `shouldSatisfy` T.isInfixOf "scalarLedgerEventTypes :: NonEmpty EventType"- codec `shouldSatisfy` T.isInfixOf "eventTypes = scalarLedgerEventTypes"- codec `shouldSatisfy` T.isInfixOf "_renderEventTypes scalarLedgerEventTypes"- codec `shouldSatisfy` (not . T.isInfixOf "; expected one of: ScalarsRecorded\"")- generated `shouldSatisfy` all (not . T.isInfixOf "error")- generated `shouldSatisfy` all (not . T.isInfixOf "getCurrentTime")- generated `shouldSatisfy` all (not . T.isInfixOf "iso8601ParseM")- it "keeps the event-list binding disjoint from the private formatter" $ do- source <- readTestText "test/fixtures/aggregate-scalars.keiro"- spec <- parseInlineSpec "<render-aggregate>" (T.replace "aggregate ScalarLedger" "aggregate Render" source)- let aggregate = onlyAggregate spec- modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate- codec = generatedTextEndingIn "Codec.hs" modules- codecLines = T.lines codec- codecLines `shouldContain` ["renderEventTypes :: NonEmpty EventType"]- codecLines `shouldContain` ["_renderEventTypes :: NonEmpty EventType -> String"]- codec `shouldSatisfy` T.isInfixOf "eventTypes = renderEventTypes"- codec `shouldSatisfy` T.isInfixOf "_renderEventTypes renderEventTypes"- it "canonicalizes Time and UTCTime across pretty, diff, and fold identity" $ do- source <- readTestText "test/fixtures/aggregate-scalars.keiro"- canonical <- parseInlineSpec "<time>" source- alias <- parseInlineSpec "<utctime>" (T.replace ":Time" ":UTCTime" (T.replace " Time =" " UTCTime =" source))- renderSpec alias `shouldBe` renderSpec canonical- legacyDiffSpecs canonical alias `shouldBe` []- legacyAggregateFoldFingerprint canonical (onlyAggregate canonical)- `shouldBe` legacyAggregateFoldFingerprint alias (onlyAggregate alias)- legacyAggregateFoldSurface canonical (onlyAggregate canonical)- `shouldBe` legacyAggregateFoldSurface alias (onlyAggregate alias)- it "keeps the committed scalar conformance generated tree fresh" $ do- modules <- scaffoldFixture "test/fixtures/aggregate-scalars.keiro"- forM_ [generatedModule | generatedModule <- modules, Keiro.Dsl.Scaffold.kind generatedModule == Generated] $ \generatedModule -> do- committed <- readTestText ("test/conformance-aggregate-scalars/" <> modulePath generatedModule)- normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generatedModule)- it "never sends a clean scalar aggregate to a type scaffold refusal" $- property $- forAll (elements scalarRegisterCases) $ \(typeName, initialValue) ->- case parseSpec "<clean-scalar>" (cleanScalarAggregateSpec typeName initialValue) of- Left parseError -> counterexample (T.unpack parseError) False- Right spec ->- let diagnostics = [diagnostic | diagnostic <- validateSpec spec, severity diagnostic == Error]- modules = scaffoldModules (defaultContext (specContext spec)) spec- in counterexample- (show diagnostics <> "\n" <> show (scaffoldRefusals spec))- ( null diagnostics- && null (scaffoldRefusals spec)- && all (not . T.null . moduleText) modules- )-- describe "aggregate scalar diagnostics" $ do- it "reports unsupported shapes, invalid initials, and mismatched guards at stable lines" $ do- diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-unsupported.keiro"- [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]- `shouldBe` [ (AggregateRegisterInitialInvalid, 6),- (AggregateRegisterInitialInvalid, 7),- (AggregateTypeUnsupportedAtUse, 10),- (AggregateExpressionOperandTypeMismatch, 14)- ]- map message diagnostics `shouldSatisfy` any (T.isInfixOf "non-negative integral literals")- map message diagnostics `shouldSatisfy` any (T.isInfixOf "ISO-8601 UTC timestamps")- map message diagnostics `shouldSatisfy` any (T.isInfixOf "mapped structural declaration")- it "accepts Natural aggregate arithmetic in the stable language" $ do- diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-arithmetic.keiro"- [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]- `shouldBe` []- it "covers unknown, container, fractional, out-of-range, and ordering failures" $ do- diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-invalid-capabilities.keiro"- [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]- `shouldBe` [ (AggregateRegisterInitialInvalid, 6),- (AggregateRegisterInitialInvalid, 7),- (AggregateTypeUnknown, 10),- (AggregateTypeUnsupportedAtUse, 10),- (AggregateTypeUnsupportedAtUse, 10),- (AggregateTypeUnsupportedAtUse, 10),- (AggregateExpressionOperatorUnsupported, 13)- ]- it "keeps one-member workspace diagnostics identical to the single file" $ do- direct <- diagnosticsOf "test/fixtures/aggregate-scalars-unsupported.keiro"- composed <- shouldComposeWorkspace "test/fixtures/aggregate-scalars-workspace/service.keiro-workspace"- let directErrors =- [(code diagnostic, line diagnostic, message diagnostic) | diagnostic <- direct, severity diagnostic == Error]- workspaceErrors =- [ (wdCode diagnostic, wlLine (NE.head (wdLocations diagnostic)), wdMessage diagnostic)- | diagnostic <- checkWorkspace composed,- wdSeverity diagnostic == Error- ]- workspaceErrors `shouldBe` directErrors-- 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" Nothing 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 policy words that generated Haskell cannot lower" $ do- emitSpec <- specOf "test/fixtures/emit.keiro"- intakeSpec <- specOf "test/fixtures/intake.keiro"- let unknownOrdering = mapPublisher (\publisher -> publisher {pubOrdering = "banana"}) emitSpec- unknownBackoff =- mapPublisher- (\publisher -> publisher {pubBackoff = (pubBackoff publisher) {boKind = "banana"}})- emitSpec- incompleteBackoff =- mapPublisher- (\publisher -> publisher {pubBackoff = BackoffSpec "exponential" "2s" Nothing Nothing})- emitSpec- unknownDedupe = mapIntake (\intake -> intake {inkDedupePolicy = "Banana"}) intakeSpec- errorCodes unknownOrdering `shouldContain` [PublisherOrderingUnknown]- errorCodes unknownBackoff `shouldContain` [PublisherBackoffInvalid]- errorCodes incompleteBackoff `shouldContain` [PublisherBackoffInvalid]- errorCodes unknownDedupe `shouldContain` [IntakeDedupePolicyUnknown]- it "gates numeric floors on the unreleased language-4 contract" $ do- emitSpec <- specOf "test/fixtures/emit.keiro"- intakeSpec <- specOf "test/fixtures/intake.keiro"- readModelSpec <- specOf "test/fixtures/workflow.keiro"- let zeroContract = mapContract (\contract -> contract {ctrSchemaVersion = 0}) emitSpec- zeroAttempts = mapPublisher (\publisher -> publisher {pubMaxAttempts = 0}) emitSpec- zeroDecode = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decBodySchemaVersion = 0}}) intakeSpec- zeroReadModel = modifyReadModel "transferDecision" (\readModel -> readModel {rmVersion = 0}) readModelSpec- floors =- [ (zeroContract, ContractSchemaVersionBelowMinimum),- (zeroAttempts, PublisherMaxAttemptsBelowMinimum),- (zeroDecode, IntakeDecodeSchemaVersionBelowMinimum),- (zeroReadModel, ReadModelVersionBelowMinimum)- ]- forM_ floors $ \(candidate, expected) -> do- serviceErrorCodes 3 candidate `shouldNotContain` [expected]- serviceErrorCodes 4 candidate `shouldContain` [expected]- it "rejects duplicate declarations whose generated behavior cannot work" $ do- reservation <- specOf "test/fixtures/reservation.keiro"- integration <- specOf "test/fixtures/emit.keiro"- let duplicateCommandField =- modifyAggregate- "Reservation"- (\aggregate -> aggregate {aggCommands = updateFirst (\command -> command {cmdFields = duplicateFirst (cmdFields command)}) (aggCommands aggregate)})- reservation- duplicateState = modifyAggregate "Reservation" (\aggregate -> aggregate {aggStates = duplicateFirst (aggStates aggregate)}) reservation- duplicateTransition =- modifyAggregate- "Reservation"- (\aggregate -> aggregate {aggTransitions = aggTransitions aggregate <> take 1 (reverse (aggTransitions aggregate))})- reservation- duplicateContractField =- mapContract- (\contract -> contract {ctrEvents = updateFirst (\event -> event {ceFields = duplicateFirst (ceFields event)}) (ctrEvents contract)})- integration- duplicateContractEvent = mapContract (\contract -> contract {ctrEvents = duplicateFirst (ctrEvents contract)}) integration- duplicateTopicAlias = mapContract (\contract -> contract {ctrTopics = duplicateFirst (ctrTopics contract)}) integration- cases =- [ (duplicateCommandField, AggregateDuplicateFieldName),- (duplicateState, AggregateDuplicateState),- (duplicateTransition, TransitionDuplicateUnguarded),- (duplicateContractField, ContractDuplicateFieldName),- (duplicateContractEvent, ContractDuplicateEvent),- (duplicateTopicAlias, ContractDuplicateTopicAlias)- ]- forM_ cases $ \(candidate, expected) -> errorCodes candidate `shouldContain` [expected]- it "gates ambiguous and silently shadowed duplicate surfaces on language 4" $ do- reservation <- specOf "test/fixtures/reservation.keiro"- integration <- specOf "test/fixtures/emit.keiro"- let duplicateRegister = modifyAggregate "Reservation" (\aggregate -> aggregate {aggRegs = duplicateFirst (aggRegs aggregate)}) reservation- duplicateNominal = reservation {specIds = duplicateFirst (specIds reservation)}- duplicateMap = mapEmit (\emitNode -> emitNode {emMap = duplicateFirst (emMap emitNode)}) integration- shadowDiscriminator =- mapContract- ( \contract ->- contract- { ctrEvents =- updateFirst- (\event -> event {ceFields = updateFirst (\field -> field {cfName = ctrDiscriminator contract}) (ceFields event)})- (ctrEvents contract)- }- )- integration- guardedSibling =- modifyAggregate- "Reservation"- ( \aggregate ->- aggregate- { aggTransitions =- aggTransitions aggregate- <> [transition {tGuard = Just (EAtom (ABool True))} | transition <- take 1 (reverse (aggTransitions aggregate))]- }- )- reservation- cases =- [ (duplicateRegister, AggregateDuplicateRegister),- (duplicateNominal, NominalDuplicateDeclaration),- (duplicateMap, EmitMapDuplicateCase),- (shadowDiscriminator, ContractFieldShadowsDiscriminator),- (guardedSibling, TransitionUnguardedSibling)- ]- forM_ cases $ \(candidate, expected) -> do- serviceErrorCodes 3 candidate `shouldNotContain` [expected]- serviceErrorCodes 4 candidate `shouldContain` [expected]- it "gates stable identities and external names on language 4" $ do- workflowSpec <- specOf "test/fixtures/workflow.keiro"- processSpec <- specOf "test/fixtures/surge-service.keiro"- routerSpec <- specOf "test/fixtures/transfer-routing.keiro"- integration <- specOf "test/fixtures/emit.keiro"- let invalidIdentity = mapWorkflow (\workflow -> workflow {wfStable = ""}) workflowSpec- duplicateIdentity =- processSpec- { specNodes =- specNodes processSpec- <> [NRouter (router {rtName = "surge-demo"}) | NRouter router <- specNodes routerSpec]- }- invalidTopic = mapContract (\contract -> contract {ctrTopics = [(alias, "bad topic") | (alias, _) <- ctrTopics contract]}) integration- emptyTopic = mapContract (\contract -> contract {ctrTopics = [(alias, "") | (alias, _) <- ctrTopics contract]}) integration- invalidReadModel = modifyReadModel "transferDecision" (\readModel -> readModel {rmTable = "Bad-Table"}) workflowSpec- duplicateColumn = modifyReadModel "transferDecision" (\readModel -> readModel {rmColumns = duplicateFirst (rmColumns readModel)}) workflowSpec- gatedCases =- [ (invalidIdentity, RuntimeIdentityInvalid),- (duplicateIdentity, RuntimeIdentityDuplicate),- (invalidTopic, ContractTopicNameInvalid),- (invalidReadModel, ReadModelIdentifierInvalid),- (duplicateColumn, ReadModelDuplicateColumn)- ]- forM_ gatedCases $ \(candidate, expected) -> do- serviceErrorCodes 3 candidate `shouldNotContain` [expected]- serviceErrorCodes 4 candidate `shouldContain` [expected]- serviceErrorCodes 3 emptyTopic `shouldContain` [ContractTopicNameInvalid]- serviceErrorCodes 4 emptyTopic `shouldContain` [ContractTopicNameInvalid]- it "gates declared integration and wire couplings on language 4" $ do- intakeSpec <- specOf "test/fixtures/intake.keiro"- reservation <- specOf "test/fixtures/reservation.keiro"- let unresolvedBind =- mapIntake- (\intake -> intake {inkBinds = updateFirst (\binding -> binding {brField = "ghost"}) (inkBinds intake)})- intakeSpec- acceptedEventBind =- mapIntake- (\intake -> intake {inkBinds = updateFirst (\binding -> binding {brField = "region"}) (inkBinds intake)})- intakeSpec- unresolvedDedupe = mapIntake (\intake -> intake {inkDedupeKey = "ghost"}) intakeSpec- unknownEnvelope = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decEnvelope = "banana policy"}}) intakeSpec- mismatchedSchema = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decBodySchemaVersion = 2}}) intakeSpec- unresolvedAlias =- mapContract- (\contract -> contract {ctrEvents = updateFirst (\event -> event {ceTopic = "ghost"}) (ctrEvents contract)})- intakeSpec- unsupportedWire =- modifyAggregate- "Reservation"- (\aggregate -> aggregate {aggWire = fmap (\wire -> wire {wireKind = "banana"}) (aggWire aggregate)})- reservation- cases =- [ (unresolvedBind, IntakeBindUnresolved),- (unresolvedDedupe, IntakeDedupeKeyUnresolved),- (unknownEnvelope, IntakeEnvelopePolicyUnknown),- (mismatchedSchema, IntakeDecodeSchemaVersionMismatch),- (unresolvedAlias, ContractTopicAliasUnresolved),- (unsupportedWire, WireClauseUnsupported)- ]- forM_ cases $ \(candidate, expected) -> do- serviceErrorCodes 3 candidate `shouldNotContain` [expected]- serviceErrorCodes 4 candidate `shouldContain` [expected]- serviceErrorCodes 4 acceptedEventBind `shouldNotContain` [IntakeBindUnresolved]- it "pins every emitted legacy single-spec diagnostic that lacked a direct negative test" $ do- reservation <- specOf "test/fixtures/reservation.keiro"- intakeSpec <- specOf "test/fixtures/intake.keiro"- emitSpec <- specOf "test/fixtures/emit.keiro"- processSpec <- specOf "test/fixtures/surge-service.keiro"- queueSpec <- specOf "test/fixtures/reservation-work.keiro"- workflowSpec <- specOf "test/fixtures/workflow.keiro"- let updateFirstTransition update aggregate = aggregate {aggTransitions = updateFirst update (aggTransitions aggregate)}- undeclaredEvent = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tEmits = ["GhostEvent"]})) reservation- undeclaredState = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tGoto = "GhostState"})) reservation- terminalOutgoing = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tSource = "Expired"})) reservation- deprecatedEmitted = modifyAggregate "Reservation" (\aggregate -> aggregate {aggEvents = updateFirst (\event -> event {evDeprecated = True}) (aggEvents aggregate)}) reservation- wireVersionMismatch = modifyAggregate "Reservation" (\aggregate -> aggregate {aggWire = fmap (\wire -> wire {wireSchemaVersion = 2}) (aggWire aggregate)}) reservation- decodeRetry =- mapIntake- ( \intake ->- intake- { inkDisposition =- [ if drOutcome row == "decodeFailed" then row {drAction = IRetry "5s"} else row- | row <- inkDisposition intake- ]- }- )- intakeSpec- unresolvedPublisher = mapPublisher (\publisher -> publisher {pubEmit = "ghost"}) emitSpec- unresolvedIntake = mapIntake (\intake -> intake {inkContract = "ghost"}) intakeSpec- unboundedQueue = mapWorkqueue (\queue -> queue {wqMaxRetries = 0}) queueSpec- unresolvedEnqueue = mapDispatch (\dispatch -> dispatch {pdEnqueueTo = "ghost"}) queueSpec- unresolvedWorkflow =- mapOperation- ( \operation -> case opShape operation of- RunOp _ input outcome -> operation {opShape = RunOp "GhostWorkflow" input outcome}- _ -> operation- )- workflowSpec- cases =- [ (undeclaredEvent, UndeclaredEvent),- (undeclaredState, UndeclaredState),- (terminalOutgoing, TerminalHasOutgoing),- (deprecatedEmitted, DeprecatedEventStillEmitted),- (wireVersionMismatch, WireSchemaVersionMismatch),- (processSpec, ProcessBenignInversion),- (decodeRetry, DispositionDecodeUnboundedRetry),- (unresolvedPublisher, PublisherUnresolvedEmit),- (unresolvedIntake, IntakeUnresolvedContract),- (unboundedQueue, WqDlqWithoutCeiling),- (unresolvedEnqueue, DispatchEnqueueUnresolved),- (unresolvedWorkflow, RunWorkflowUnresolved)- ]- forM_ cases $ \(candidate, expected) -> diagnosticCodes candidate `shouldContain` [expected]- 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"- mapM_ (\expected -> codes `shouldContain` [expected]) [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"- parseStableRenderedSpec "<retiring-round-trip>" 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` []- parseStableRenderedSpec "<snapshot-round-trip>" spec `shouldBe` Right spec- case [aggregate | NAggregate aggregate <- specNodes spec] of- [aggregate] -> aggSnapshot aggregate `shouldBe` Just (SnapshotSpec (SnapEvery 100) 1 "7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28" 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=\"7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28\"" "shape-hash=\"\"" source)- map code (validateSpec emptyHash) `shouldContain` [SnapshotCodecFixtureInvalid]- it "conditionally lowers JSON instances and the live defaultStateCodec" $ do- snapshotService <- checkedServiceOf "test/fixtures/reservation-snapshot.keiro"- ordinaryService <- checkedServiceOf "test/fixtures/reservation.keiro"- let snapshot = checkedSpec snapshotService- ordinary = checkedSpec ordinaryService- case ([aggregate | NAggregate aggregate <- specNodes snapshot], [aggregate | NAggregate aggregate <- specNodes ordinary]) of- ([snapshotAggregate], [ordinaryAggregate]) -> do- let snapshotModules = scaffoldServiceModules (defaultContext (specContext snapshot)) snapshotService- ordinaryModules = scaffoldServiceModules (defaultContext (specContext ordinary)) ordinaryService- 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 "reservationSnapshotFixture = (1, \"7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28\")"- 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 <- shouldParseStableRenderedSpec "<second>" first- 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 -> parseStableRenderedSpec "in" 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 -> parseStableRenderedSpec "in" 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 <- legacyScaffoldProcessFixture "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 "import Generated.HospitalCapacity.Surge.EventStream (SurgeEventStreamDef)"- moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeCategory :: Stream.StreamCategory SurgeEventStreamDef"- 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 <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"- b <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"- map moduleText a `shouldBe` map moduleText b- it "separates aggregate event-stream and command-target categories and emits stable typed sums" $ do- spec <- specOf "test/fixtures/hospital-surge.keiro"- let ctx = defaultContext (specContext spec)- modules = concat [scaffoldAggregate ctx spec aggregate | NAggregate aggregate <- specNodes spec]- surgeStream = generatedTextEndingIn "Surge/EventStream.hs" modules- surgeDomain = generatedTextEndingIn "Surge/Domain.hs" modules- surgeStream `shouldSatisfy` T.isInfixOf "surgeCategory :: Stream.StreamCategory SurgeEventStreamDef"- surgeStream `shouldSatisfy` T.isInfixOf "surgeCommandCategory :: Stream.StreamCategory SurgeCommand"- surgeDomain `shouldNotSatisfy` T.isInfixOf "{-# LANGUAGE EmptyDataDecls #-}"- surgeDomain `shouldSatisfy` T.isInfixOf "data SurgeEvent = SurgeThresholdNoted"- surgeDomain `shouldSatisfy` (not . T.isInfixOf "data SurgeEvent = ()")-- 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 "branches contract scaffolding, manifests, and durable identities only for language 4" $ do- sourceText <- readTestText "test/fixtures/contract-v4.keiro"- parsed <- case parseSource "contract-v4.keiro" sourceText of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let service = checkedSource parsed- spec = checkedSpec service- ctx = defaultContext (specContext spec)- contract <- case [value | NContract value <- specNodes spec] of- [value] -> pure value- values -> expectationFailure ("expected one contract, got " <> show (length values)) >> fail "unreachable"- legacyModule <- case scaffoldContract ctx contract of- [value] -> pure value- values -> expectationFailure ("expected one legacy module, got " <> show (length values)) >> fail "unreachable"- typedModule <- case scaffoldContractForService ctx service contract of- [value] -> pure value- values -> expectationFailure ("expected one typed module, got " <> show (length values)) >> fail "unreachable"- let dependencies = manifestDependenciesForService service- identities = idDomainIdentitiesForService service- manifestText = renderManifestForService "contract-v4.keiro" [typedModule] service- assertGeneratedHaskellContract "contract-v4.keiro" manifestText- committed <- readTestText "test/conformance-contract/Generated/HospitalCapacity/Emergency/Contract.hs"- normalizeGenerated (moduleText typedModule) `shouldBe` normalizeGenerated committed- moduleText legacyModule `shouldSatisfy` T.isInfixOf "incidentId :: !Text"- moduleText legacyModule `shouldSatisfy` (not . T.isInfixOf "KindID")- moduleText typedModule `shouldSatisfy` T.isInfixOf "incidentId :: !(KindID \"inc\")"- moduleText typedModule `shouldSatisfy` T.isInfixOf "KindID.toText payload.incidentId"- moduleText typedModule `shouldSatisfy` T.isInfixOf "explicitParseField (parseKindIdV7Value @\"inc\") o \"incidentId\""- moduleText typedModule `shouldSatisfy` T.isInfixOf " , incidentEventsTopic"- moduleText typedModule `shouldSatisfy` T.isInfixOf " , hospitalEventsTopic"- moduleText typedModule `shouldSatisfy` (not . T.isInfixOf "Wno-unused-top-binds")- dependencies `shouldBe` ["aeson", "base", "keiro-core", "mmzk-typeid", "text"]- manifestDependencies spec `shouldBe` ["aeson", "base", "text"]- forM_ dependencies $ \dependency -> manifestText `shouldSatisfy` T.isInfixOf (" , " <> dependency)- identities- `shouldBe` [ "id-domain|name=contract:emergency.IncidentTransferNeedDeclared.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text",- "id-domain|name=contract:emergency.TransferReservationAccepted.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text",- "id-domain|name=contract:emergency.TransferReservationAccepted.reservationId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=rsv|separator=_|json=canonical-json-text",- "id-domain|name=contract:emergency.TransferReservationAccepted.hospitalId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=hsp|separator=_|json=canonical-json-text"- ]-- it "persists contract ID domains in single-file and workspace records with owner attribution" $ do- sourceText <- readTestText "test/fixtures/contract-v4.keiro"- parsed <- case parseSource "contract-v4.keiro" sourceText of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right value -> pure value- let service = checkedSource parsed- spec = checkedSpec service- ctx = defaultContext (specContext spec)- modules = scaffoldServiceModules ctx service- identities = idDomainIdentitiesForService service- duplicateIdentity <- case identities of- value : _ -> pure value- [] -> expectationFailure "typed contract service did not expose ID-domain identities" >> fail "unreachable"- withTempDirectory "keiro-dsl-v4-contract-record" $ \out -> do- result <- executeServiceScaffold out False "contract-v4.keiro" (parsedSourceLanguage parsed) ctx service modules- result `shouldSatisfy` isRight- contents <- TIO.readFile (out </> recordFileName (specContext spec))- record <- maybe (expectationFailure "typed contract scaffold record did not parse" >> fail "unreachable") pure (parseRecord contents)- recIdDomains record `shouldBe` identities- parseRecord (contents <> "id-domain " <> duplicateIdentity <> "\n") `shouldBe` Nothing-- let manifest = "service hospital-capacity\nspec domain/contract.keiro\n"- source = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/contract.keiro", sourceText)])- loaded <- loadWorkspace source "service.keiro-workspace"- workspace <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure loaded- workspacePlan <- either (\refusals -> expectationFailure (show refusals) >> fail "unreachable") pure (planWorkspaceScaffold "goldens" ctx workspace)- case [provenance | (scaffoldModule, provenance) <- wpModules workspacePlan, modulePath scaffoldModule == "Generated/HospitalCapacity/Emergency/Contract.hs"] of- [MemberOwned owner] -> owner `shouldBe` "domain/contract.keiro"- values -> expectationFailure ("expected one member-owned contract module, got " <> show values)- withTempDirectory "keiro-dsl-v4-contract-workspace-record" $ \out -> do- result <- executeWorkspaceScaffold out False workspacePlan- result `shouldSatisfy` isRight- contents <- TIO.readFile (out </> workspaceRecordFileName (wsService workspace))- record <- maybe (expectationFailure "typed contract workspace record did not parse" >> fail "unreachable") pure (parseWorkspaceRecord contents)- wrIdDomains record `shouldBe` identities- [(wrmPath row, wrmOwner row) | row <- wrModules record, wrmPath row == "Generated/HospitalCapacity/Emergency/Contract.hs"]- `shouldBe` [("Generated/HospitalCapacity/Emergency/Contract.hs", Just "domain/contract.keiro")]- parseWorkspaceRecord (contents <> "id-domain " <> duplicateIdentity <> "\n") `shouldBe` Nothing- 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"- inbox `shouldSatisfy` T.isInfixOf "data IncidentInboxOutcome"- inbox `shouldSatisfy` T.isInfixOf "data IncidentInboxDisposition"- inbox `shouldSatisfy` T.isInfixOf "InboxRetryAfter !RetryDelay !(Maybe InboxFailure)"- inbox `shouldSatisfy` T.isInfixOf "InboxDeadLetter !(Maybe Text) !(Maybe InboxFailure)"- inbox `shouldSatisfy` T.isInfixOf "InboxHandlerFailed reason attempts ->"- inbox `shouldNotSatisfy` T.isInfixOf "Nothing -> InboxRetry"- (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` [18]- 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)"- policy `shouldSatisfy` T.isInfixOf "data ReservationWorkOutcome"- policy `shouldSatisfy` T.isInfixOf "jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome"- policy `shouldNotSatisfy` T.isInfixOf "jobOutcomeFor :: Text -> JobOutcome"- policy `shouldNotSatisfy` T.isInfixOf " _ -> Retry"- 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))- parseStableRenderedSpec "in" 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 -> parseStableRenderedSpec "in" 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 "data WorkflowFacts = WorkflowFacts"- facts `shouldSatisfy` T.isInfixOf "workflowFactBody = [\"step:create-transfer-hold\", \"patch:fraud-check-v2(step:fraud-check)\""- facts `shouldSatisfy` T.isInfixOf "workflowFactAwaitLabels = [\"reservation-confirmation\"]"- facts `shouldSatisfy` T.isInfixOf "workflowFactPatchIds = [\"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- replayImpactSpecs 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- replayImpactSpecs old loosened `shouldBe` ReplayNeutral-- it "pairs guard-disambiguated siblings independently of both declaration orders" $ do- base <- specOf "test/fixtures/reservation.keiro"- let aggregate = onlyAggregate base- case (aggTransitions aggregate, aggEvents aggregate) of- (prototype : _, firstEvent : secondEvent : _) -> do- let sibling guardExpression eventName =- prototype- { tGuard = guardExpression,- tEmits = [eventName],- tLoc = noLoc- }- commandOverride = EPath noLoc CommandRoot ["lifeCriticalOverride"]- exact = sibling (Just (EAtom (ABool True))) (evName firstEvent)- loosenedOld = sibling (Just commandOverride) (evName firstEvent)- loosenedNew = sibling Nothing (evName firstEvent)- changedOld = sibling (Just (ECmp OpEq commandOverride (ELiteral noLoc (LiteralBool False)))) (evName secondEvent)- changedNew = sibling (Just (ECmp OpEq commandOverride (ELiteral noLoc (LiteralBool True)))) (evName firstEvent)- oldSiblings = [exact, loosenedOld, changedOld]- newSiblings = [exact, loosenedNew, changedNew]- withTransitions transitions =- modifyAggregate- (aggName aggregate)- (\candidate -> candidate {aggTransitions = transitions})- base- impacts =- [ replayImpactSpecs (withTransitions oldOrder) (withTransitions newOrder)- | oldOrder <- permutations oldSiblings,- newOrder <- permutations newSiblings- ]- case impacts of- firstImpact : remainingImpacts -> do- remainingImpacts `shouldSatisfy` all (== firstImpact)- firstImpact `shouldSatisfy` (/= ReplayNeutral)- [] -> expectationFailure "permutations unexpectedly produced no replay comparisons"- _ -> expectationFailure "reservation fixture must contain one transition and two events"-- 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 <- shouldParseStableRenderedSpec "<formatted>" old- replayImpactSpecs 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"- ],- aggTransitions =- [ transition {tEmits = filter (/= "TransferReservationConfirmed") (tEmits transition)}- | transition <- aggTransitions aggregate- ]- }- _ -> 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 "goldens language-3 to language-4 contract TypeID admission and rollout" $ do- let source versionNumber prefix =- T.unlines- [ "language keiro-dsl " <> T.pack (show versionNumber),- "context hospital-capacity",- "contract emergency {",- " schemaVersion 1",- " discriminator messageType",- " topic incidentEvents \"emergency.incident.events\"",- " event IncidentTransferNeedDeclared on incidentEvents {",- " incidentId: typeid \"" <> prefix <> "\"",- " }",- "}"- ]- checked name input = case parseSource name input of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right parsed -> pure (checkedSource parsed)- v1 <- checked "contract-typeid-v1.keiro" (source (1 :: Int) "inc")- v3 <- checked "contract-typeid-v3.keiro" (source (3 :: Int) "inc")- v4 <- checked "contract-typeid-v4.keiro" (source (4 :: Int) "inc")- v4Edited <- checked "contract-typeid-v4-edited.keiro" (source (4 :: Int) "rsv")- let changes = diffServices v3 v4- textGolden = T.intercalate "\n" (map renderFinding changes)- jsonGolden = LazyText.toStrict (LazyTextEncoding.decodeUtf8 (Aeson.encode (diffReport defaultGate changes)))- findings = [kind | Breaking kind <- changes, ckCode kind == ContractTypeIdDomainChanged]- expectedText <- readTestText "test/fixtures/contract-typeid-domain.diff.golden"- expectedJson <- readTestText "test/fixtures/contract-typeid-domain.diff.json.golden"- T.stripEnd textGolden `shouldBe` T.stripEnd expectedText- T.stripEnd jsonGolden `shouldBe` T.stripEnd expectedJson- case findings of- [finding] -> do- verdictFor PublicConsumer (ckVector finding) `shouldBe` VBreaking- verdictFor ConsumerBuild (ckVector finding) `shouldBe` VBreaking- [verdictFor surface (ckVector finding) | surface <- [PrivateHistoryRead, OldBinaryReadNewEvents, SnapshotHydration, PersistedIdentity]]- `shouldBe` replicate 4 VNotApplicable- cvRollout (ckVector finding) `shouldBe` Set.fromList [RolloutDrainRequired, RolloutProducerFirst]- deriveLabel (Set.singleton PublicConsumer) (ckVector finding) `shouldBe` LabelBreaking- deriveLabel (Set.singleton ConsumerBuild) (ckVector finding) `shouldBe` LabelBreaking- remediationFor (ckContext finding) (ckCode finding)- `shouldBe` RemedyEmitContractTypeIdDomain :| [RemedyDrainLegacyInvalidContractMessages, RemedyRescaffoldContractConsumers, RemedyRunContractConformance]- values -> expectationFailure ("expected one contract TypeID-domain finding, got " <> show (length values))- [kind | change <- diffServices v1 v3, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []- [kind | change <- diffServices v4 v4, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []- let edited = diffServices v3 v4Edited- map (ckCode . kindOfChange) edited `shouldContain` [ContractFieldChanged]- [kind | change <- edited, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []- 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 <- shouldParseStableRenderedSpec "<formatted-process>" 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 "import Example.Artifact.Domain (ArtifactInfo)"- domain `shouldSatisfy` T.isInfixOf "import Vendor.Geometry (Geometry)"- domain `shouldSatisfy` T.isInfixOf "artifact :: !ArtifactInfo"- domain `shouldSatisfy` T.isInfixOf "RCons (Proxy @\"currentArtifact\") ArtifactKeiroBindings.emptyArtifactInfo"- domain `shouldSatisfy` (not . T.isInfixOf "Example.Artifact.Domain.ArtifactInfo")- codec `shouldSatisfy` T.isInfixOf "\"location\" .= encodeArtifactLocationShape"- codec `shouldSatisfy` T.isInfixOf "\"local_file\""- codec `shouldSatisfy` T.isInfixOf "Nothing -> pure ShapeArtifactKind.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"- mapM_- (shape `shouldSatisfy`)- [ T.isInfixOf "description :: !(Maybe Text)",- T.isInfixOf "tags :: ![Text]",- T.isInfixOf "labels :: ![Maybe Text]",- T.isInfixOf "attributes :: !(Map Text Text)"- ]- mapM_- (shape `shouldNotSatisfy`)- [ T.isInfixOf "description :: !(Maybe (Text))",- T.isInfixOf "tags :: !([Text])",- T.isInfixOf "labels :: !([(Maybe (Text))])",- T.isInfixOf "attributes :: !(Map Text (Text))"- ]- 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 "type FieldOwner ArtifactInfoKeyProjection = ArtifactInfo"- facade `shouldSatisfy` T.isInfixOf "bindingToShape KeiroBindings.artifactInfoBinding owner"- facade `shouldSatisfy` (not . T.isInfixOf "Example.Artifact.Domain.ArtifactInfo")- facade `shouldSatisfy` T.isInfixOf "artifactInfoKeyWitness"- facade `shouldNotSatisfy` T.isInfixOf "structuralProjectionC"- it "suffixes only structural witness names that collide after normalization" $ do- source <- readTestText "test/fixtures/consumer-types.keiro"- collisionSpec <-- parseInlineSpec- "<projection-name-collision>"- ( T.replace- " key as \"key\" : Text required"- ( T.unlines- [ " key as \"key\" : Text required",- " fooDash as \"foo-bar\" : Text required",- " fooUnder as \"foo_bar\" : Text required"- ]- )- source- )- graph <- shouldResolveTypeGraph collisionSpec- let specs = projectionSpecs graph- keyWitnesses = [spWitness spec | spec <- specs, spPointer spec == "/key"]- collidedWitnesses = [spWitness spec | spec <- specs, spPointer spec `elem` ["/foo-bar", "/foo_bar"]]- keyWitnesses `shouldBe` ["artifactInfoKeyWitness"]- length collidedWitnesses `shouldBe` 2- Set.size (Set.fromList collidedWitnesses) `shouldBe` 2- collidedWitnesses `shouldSatisfy` all (T.isPrefixOf "artifactInfoFooBar")- collidedWitnesses `shouldSatisfy` all (T.isSuffixOf "Witness")- collidedWitnesses `shouldSatisfy` all ((== 8) . T.length . T.dropEnd (T.length ("Witness" :: T.Text)) . T.drop (T.length ("artifactInfoFooBar" :: T.Text)))- it "uses only precedence-required parentheses in nested record field types" $ do- let spec =- mappedSpec- [ completeStructural- "Nested"- ( recordShape- [ TMap (TOptional TText),- TOptional (TList TText),- TOptional (TMap TText)- ]- )- ]- shape = generatedTextEndingIn "Structural/Shape/Nested.hs" (scaffoldStructural (defaultContext (specContext spec)) spec)- mapM_- (shape `shouldSatisfy`)- [ T.isInfixOf "field1 :: !(Map Text (Maybe Text))",- T.isInfixOf "field2 :: !(Maybe [Text])",- T.isInfixOf "field3 :: !(Maybe (Map Text Text))"- ]- it "uses the same precedence rules for strict union payloads" $ do- let spec =- mappedSpec- [ completeStructural- "Payload"- ( ShapeUnion- (TaggedObject "tag" "contents" RejectUnknown)- [ WireArm "OptionalPayload" "optional" (Just (TOptional TText)) noLoc,- WireArm "ListPayload" "list" (Just (TList (TOptional TText))) noLoc,- WireArm "MapPayload" "map" (Just (TMap (TOptional TText))) noLoc- ]- )- ]- shape = generatedTextEndingIn "Structural/Shape/Payload.hs" (scaffoldStructural (defaultContext (specContext spec)) spec)- mapM_- (shape `shouldSatisfy`)- [ T.isInfixOf "OptionalPayload !(Maybe Text)",- T.isInfixOf "ListPayload ![Maybe Text]",- T.isInfixOf "MapPayload !(Map Text (Maybe Text))"- ]-- 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- assertGeneratedHaskellContract "consumer-types.keiro" manifest- 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` isFoldSurfaceRefusal- 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 "generated Haskell language contract" $ do- it "limits every representative generated module to the closed local extension set" $ do- let allowed =- Set.fromList- [ "BlockArguments",- "DeriveAnyClass",- "DuplicateRecordFields",- "OverloadedLabels",- "OverloadedRecordDot",- "QualifiedDo",- "TemplateHaskell",- "TypeFamilies"- ]- fixtures =- [ "test/fixtures/aggregate-scalar-expressions-v2.keiro",- "test/fixtures/nominal-scalars.keiro",- "test/fixtures/structural-conformance.keiro",- "test/fixtures/reservation.keiro",- "test/fixtures/contract-v4.keiro",- "test/fixtures/intake.keiro",- "test/fixtures/reservation-work.keiro",- "test/fixtures/readmodel-runtime.keiro"- ]- forM_ fixtures $ \fixture -> do- modules <- scaffoldFixture fixture- forM_ [generatedModule | generatedModule <- modules, kind generatedModule == Generated] $ \generatedModule -> do- let actual = Set.fromList (generatedLocalExtensions generatedModule)- unless (actual `Set.isSubsetOf` allowed) $- expectationFailure (fixture <> ":" <> modulePath generatedModule <> ": disallowed local extensions " <> show (Set.toList (actual `Set.difference` allowed)))-- it "retains specialized syntax extensions and removes GHC2024-covered pragmas" $ do- scalar <- scaffoldFixture "test/fixtures/aggregate-scalar-expressions-v2.keiro"- structural <- scaffoldFixture "test/fixtures/structural-conformance.keiro"- reservation <- scaffoldFixture "test/fixtures/reservation.keiro"- contract <- scaffoldFixture "test/fixtures/contract-v4.keiro"- intake <- scaffoldFixture "test/fixtures/intake.keiro"- queue <- scaffoldFixture "test/fixtures/reservation-work.keiro"- readModel <- scaffoldFixture "test/fixtures/readmodel-runtime.keiro"- generatedExtensionsEndingIn "ScalarAccount/Domain.hs" scalar- `shouldBe` ["DeriveAnyClass", "DuplicateRecordFields", "TemplateHaskell"]- generatedExtensionsEndingIn "ScalarAccount/Transducer.hs" scalar- `shouldBe` ["BlockArguments", "OverloadedLabels", "OverloadedRecordDot", "QualifiedDo"]- generatedExtensionsEndingIn "Nominals.hs" scalar `shouldContain` ["DeriveAnyClass", "TypeFamilies"]- generatedExtensionsEndingIn "Nominals/Internal.hs" scalar `shouldBe` []- generatedExtensionsEndingIn "StructuralProjections.hs" structural `shouldBe` ["TypeFamilies"]- let structuralShapeExtensions =- [ generatedLocalExtensions generatedModule- | generatedModule <- structural,- "/Structural/Shape/" `T.isInfixOf` T.pack (modulePath generatedModule)- ]- structuralShapeExtensions `shouldSatisfy` all null- generatedExtensionsEndingIn "Projection.hs" reservation `shouldBe` []- generatedExtensionsEndingIn "ReplayAudit.hs" reservation `shouldBe` []- generatedExtensionsEndingIn "Contract.hs" contract `shouldBe` ["DuplicateRecordFields", "OverloadedRecordDot"]- generatedExtensionsEndingIn "Inbox.hs" intake `shouldBe` []- generatedExtensionsEndingIn "Queue.hs" queue `shouldBe` ["OverloadedRecordDot"]- generatedExtensionsEndingIn "ReadModel.hs" readModel `shouldBe` ["OverloadedRecordDot"]-- it "conditions record, label, derivation, and duplicate-selector extensions on emitted syntax" $ do- mappedGuardSource <- readTestText "test/fixtures/mapped-guard.keiro"- mappedGuardParsed <- case parseSource "mapped-guard-no-expression.keiro" (T.replace "guard current == current ; " "" mappedGuardSource) of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right parsed -> pure parsed- let mappedGuardService = checkedSource mappedGuardParsed- mappedGuard = scaffoldServiceModules (defaultContext (specContext (checkedSpec mappedGuardService))) mappedGuardService- registerFree <- scaffoldFixture "test/fixtures/order.keiro"- readModels <- scaffoldFixture "test/fixtures/readmodel.keiro"- snapshot <- scaffoldFixture "test/fixtures/reservation-snapshot.keiro"- ordinary <- scaffoldFixture "test/fixtures/reservation.keiro"- generatedExtensionsEndingIn "Holder/Domain.hs" mappedGuard `shouldBe` ["TemplateHaskell"]- generatedExtensionsEndingIn "Holder/Codec.hs" mappedGuard `shouldBe` []- generatedExtensionsEndingIn "Holder/Transducer.hs" mappedGuard- `shouldBe` ["BlockArguments", "QualifiedDo"]- generatedExtensionsEndingIn "Holder/Harness.hs" mappedGuard `shouldBe` ["OverloadedLabels"]- generatedExtensionsEndingIn "Order/Harness.hs" registerFree `shouldBe` []- generatedExtensionsEndingIn "Transfer_decisions/ReadModel.hs" readModels `shouldBe` ["OverloadedRecordDot"]- generatedExtensionsEndingIn "Subscriptions/ReadModel.hs" readModels `shouldBe` []- generatedExtensionsEndingIn "Reservation/Domain.hs" snapshot `shouldContain` ["DeriveAnyClass"]- generatedExtensionsEndingIn "Reservation/Domain.hs" ordinary `shouldNotContain` ["DeriveAnyClass"]-- disjoint <-- parseInlineSpec "<disjoint-contract>" $- T.unlines- [ "language keiro-dsl 4",- "context language-contract",- "contract disjoint {",- " schemaVersion 1",- " discriminator kind",- " topic events \"events\"",- " event First on events { first: text }",- " event Second on events { second: text }",- "}"- ]- emptyPayload <-- parseInlineSpec "<empty-contract>" $- T.unlines- [ "language keiro-dsl 4",- "context language-contract",- "contract empty {",- " schemaVersion 1",- " discriminator kind",- " topic events \"events\"",- " event Empty on events { }",- "}"- ]- let contractExtensions spec =- generatedExtensionsEndingIn- "Contract.hs"- [ generatedModule- | contractNode <- [contractNode | NContract contractNode <- specNodes spec],- generatedModule <- scaffoldContract (defaultContext (specContext spec)) contractNode- ]- contractExtensions disjoint `shouldBe` ["OverloadedRecordDot"]- contractExtensions emptyPayload `shouldBe` []-- describe "manifest (M2)" $ do- it "lists exactly the modules the scaffolder produced" $ do- mods <- scaffoldFixture "test/fixtures/reservation.keiro"- service <- checkedServiceOf "test/fixtures/reservation.keiro"- let manifest = renderManifestForService "reservation.keiro" mods service- expectedNames = sort (map (moduleNameOf . modulePath) mods)- assertGeneratedHaskellContract "reservation.keiro" manifest- -- 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.BehaviorContract",- "Generated.HospitalCapacity.Reservation.Domain",- "Generated.HospitalCapacity.Reservation.EventStream",- "Generated.HospitalCapacity.Reservation.Harness",- "Generated.HospitalCapacity.Reservation.Projection",- "Generated.HospitalCapacity.Reservation.Transducer",- "Generated.HospitalCapacity.Nominals",- "Generated.HospitalCapacity.Nominals.Internal",- "Generated.HospitalCapacity.ReplayAudit",- "HospitalCapacity.Reservation.BehaviorHoles",- "HospitalCapacity.Reservation.Holes"- ]- it "derives the dependency set from the node kinds present (aggregate)" $ do- service <- checkedServiceOf "test/fixtures/reservation.keiro"- manifestDependenciesForService service `shouldBe` ["aeson", "base", "keiki", "keiro", "text"]- it "derives the process dependency set, including worker-policy runtime imports" $ do- service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"- let dependencies = manifestDependenciesForService service- mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["time", "uuid", "shibuya-core", "keiki", "keiro"]- it "uses the registered shibuya-core package name for router scaffolds" $ do- service <- checkedServiceOf "test/fixtures/incident-paging/incident-paging.keiro"- let dependencies = manifestDependenciesForService service- mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["effectful-core", "keiro", "shibuya-core"]- dependencies `shouldNotContain` ["shibuya"]-- describe "service conformance facade (plan 188 M2)" $ do- it "normalizes aggregate and read-model checks behind one base-only API" $ do- service <- checkedServiceOf "test/fixtures/transfer-routing.keiro"- let ctx = defaultContext (specContext (checkedSpec service))- case serviceHarnessModule ctx service of- Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)- Right facade -> do- committed <- readTestText ("test/conformance-newsurface/" <> modulePath facade)- normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText facade)- moduleNameOf (modulePath facade) `shouldBe` "Generated.TransferRouting.Conformance"- moduleText facade `shouldSatisfy` T.isInfixOf ".harnessAssertions"- moduleText facade `shouldSatisfy` T.isInfixOf ".readModelFactResults"- moduleText facade `shouldSatisfy` T.isInfixOf "aggregate/Hospital/"- moduleText facade `shouldSatisfy` T.isInfixOf "readmodel/hospital_load/"- moduleText facade `shouldNotSatisfy` T.isInfixOf "TransferRouting.Hospital.Holes"- it "projects process, router, and workflow facts with qualified stable keys" $ do- processService <- checkedServiceOf "test/fixtures/hospital-surge.keiro"- routerService <- checkedServiceOf "test/fixtures/incident-paging/incident-paging.keiro"- workflowService <- checkedServiceOf "test/fixtures/workflow-evolution.keiro"- let select predicate = filter predicate . specNodes . checkedSpec- factNodes =- select (\case NProcess {} -> True; _ -> False) processService- <> select (\case NRouter {} -> True; _ -> False) routerService- <> select (\case NWorkflow {} -> True; _ -> False) workflowService- baseSpec = checkedSpec processService- service = processService {checkedSpec = baseSpec {specNodes = factNodes}}- ctx = defaultContext (specContext baseSpec)- forM_- [ "process/HospitalSurge/maxAttempts",- "router/PagingRouter/dispatchCommand",- "workflow/HospitalTransferReservation/body"- ]- (\key -> serviceConformanceFactKeys service `shouldSatisfy` elem key)- case serviceHarnessModule ctx service of- Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)- Right facade -> do- moduleText facade `shouldSatisfy` T.isInfixOf ".processHarnessValues"- moduleText facade `shouldSatisfy` T.isInfixOf ".routerHarnessValues"- moduleText facade `shouldSatisfy` T.isInfixOf ".workflowFactValues"- it "uses the shared context-level placement policy" $ do- service <- checkedServiceOf "test/fixtures/contract-v4.keiro"- let ctx = Context {contextName = "modules", moduleRoot = "Mori", placement = CollocatedLeaf}- serviceConformanceModuleName ctx `shouldBe` "Mori.Modules.Generated.Conformance"- case serviceHarnessModule ctx service of- Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)- Right facade -> do- moduleText facade `shouldSatisfy` T.isInfixOf "runServiceConformanceChecks = pure []"- moduleText facade `shouldSatisfy` T.isInfixOf "serviceConformanceFacts = []"- it "adds one facade only to configured single-file plans and exposes only it" $ do- service <- checkedServiceOf "test/fixtures/reservation.keiro"- let ctx = defaultContext (specContext (checkedSpec service))- runtimePackage = RuntimePackageName "reservation-runtime"- unconfigured <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffold ctx service)- configured <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)- let facadeName = serviceConformanceModuleName ctx- facades = [moduleValue | moduleValue <- configured, moduleNameOf (modulePath moduleValue) == facadeName]- manifest = renderManifestForServiceWithFacade (Just facadeName) "reservation.keiro" configured service- length configured `shouldBe` length unconfigured + 1- length facades `shouldBe` 1- manifest `shouldSatisfy` T.isInfixOf ("exposed-modules:\n " <> facadeName)- T.count facadeName manifest `shouldBe` 1- it "emits one context-level facade for a multi-member workspace regardless of member order" $ do- canonical <- shouldComposeWorkspace canonicalWorkspacePath- reordered <- shouldComposeWorkspace "test/fixtures/workspace/service-reordered.keiro-workspace"- let runtimePackage = Just (RuntimePackageName "demo-runtime")- plan workspace =- planWorkspaceScaffoldWithRuntimePackageAndGoldens [] runtimePackage "goldens" (workspaceContext workspace) workspace- facades workspacePlan =- [ (moduleText moduleValue, provenance)- | (moduleValue, provenance) <- wpModules workspacePlan,- ".Conformance" `T.isSuffixOf` moduleNameOf (modulePath moduleValue)- ]- canonicalPlan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (plan canonical)- reorderedPlan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (plan reordered)- facades canonicalPlan `shouldBe` facades reorderedPlan- map snd (facades canonicalPlan) `shouldBe` [ContextLevel]- it "refuses duplicate normalized fact keys before planning writes" $ do- service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"- let spec = checkedSpec service- processes = [node | node@NProcess {} <- specNodes spec]- duplicated = service {checkedSpec = spec {specNodes = processes <> processes}}- serviceHarnessModule (defaultContext (specContext spec)) duplicated `shouldSatisfy` isLeft-- describe "runnable service conformance package (plan 188 M3)" $ do- it "uses readable ordinary names and collision-safe punctuation encoding" $ do- cabaliseConformanceService "mori" `shouldBe` "mori"- cabaliseConformanceService "mori_core" `shouldNotBe` cabaliseConformanceService "mori-core"- cabaliseConformanceService "Mori" `shouldNotBe` cabaliseConformanceService "mori"- conformancePackageDirectory (WorkspaceConformanceService "mori") `shouldBe` "keiro-dsl-conformance.workspace.mori"- conformancePackageDirectory (StandaloneConformanceService "mori") `shouldBe` "keiro-dsl-conformance.mori"- it "plans one base-only package and round-trips its complete generated record" $ do- service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"- let runtimePackage = RuntimePackageName "hospital-runtime"- facade = "Generated.HospitalSurge.Conformance"- plan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planConformancePackage (StandaloneConformanceService "hospital-surge") runtimePackage facade service)- cppPackageName plan `shouldBe` "keiro-hospital-surge-conformance"- length [file | file <- cppFiles plan, takeExtension (conformanceFilePath file) == ".cabal"] `shouldBe` 1- cabalFile <- case [file | file <- cppFiles plan, takeExtension (conformanceFilePath file) == ".cabal"] of- [file] -> pure file- files -> expectationFailure ("expected one Cabal file, got " <> show (map conformanceFilePath files)) >> fail "unreachable"- let cabalText = conformanceFileText cabalFile- cabalText `shouldSatisfy` T.isInfixOf "base >=4.18 && <5"- T.lines cabalText `shouldSatisfy` (\lines' -> case lines' of first : _ -> first == "cabal-version: 3.0"; [] -> False)- cabalText `shouldSatisfy` T.isInfixOf "hospital-runtime"- cabalText `shouldNotSatisfy` T.isInfixOf " , keiro-dsl\n"- recordFile <- case [file | file <- cppFiles plan, conformanceFilePath file == conformanceRecordFileName] of- [file] -> pure file- files -> expectationFailure ("expected one package record, got " <> show (map conformanceFilePath files)) >> fail "unreachable"- let recordText = conformanceFileText recordFile- parseConformancePackageRecord recordText- `shouldBe` Just- ConformancePackageRecord- { cprSchema = 1,- cprServiceKey = cppServiceKey plan,- cprRuntimePackage = runtimePackage,- cprFacadeModule = facade,- cprFiles = [(conformanceFileKind file, conformanceFilePath file) | file <- cppFiles plan]- }- it "compares unique facts by key and distinguishes mismatch, missing, and unexpected" $ do- compareConformanceFacts [("a", "1"), ("b", "2"), ("d", "4")] [("c", "3"), ("a", "1"), ("b", "9")]- `shouldBe` Right- [ ConformanceFactMatch "a" "1",- ConformanceFactMismatch "b" "2" "9",- ConformanceFactUnexpected "c" "3",- ConformanceFactMissing "d" "4"- ]- compareConformanceFacts [("a", "1"), ("a", "2")] []- `shouldBe` Left [DuplicateFactKey ExpectedFact "a"]- it "creates once, reports generated files unchanged, and preserves accepted expectations" $ do- withTempDirectory "keiro-dsl-conformance-package" $ \out -> do- parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"- let service = checkedSource parsed- spec = checkedSpec service- ctx = defaultContext (specContext spec)- runtimePackage = RuntimePackageName "hospital-runtime"- modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)- first <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules- firstReport <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure first- firstPackage <- maybe (expectationFailure "expected conformance package report" >> fail "unreachable") pure (reportConformancePackage firstReport)- let packageRoot = out </> conformancePackageDirectory (StandaloneConformanceService (contextName ctx))- expectationsPath = packageRoot </> "src/KeiroConformance/Expectations.hs"- accepted = "module KeiroConformance.Expectations where\n-- accepted by the application\n"- map snd (conformanceReportDispositions firstPackage) `shouldContain` [ConformanceCreated]- TIO.writeFile expectationsPath accepted- second <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out True "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules- secondReport <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure second- secondPackage <- maybe (expectationFailure "expected conformance package report" >> fail "unreachable") pure (reportConformancePackage secondReport)- TIO.readFile expectationsPath `shouldReturn` accepted- [ disposition- | (file, disposition) <- conformanceReportDispositions secondPackage,- conformanceFileKind file == Generated- ]- `shouldSatisfy` all (== ConformanceUnchanged)- [ disposition- | (file, disposition) <- conformanceReportDispositions secondPackage,- conformanceFileKind file == HoleStub- ]- `shouldBe` [ConformanceSkipped]- it "refuses a bannerless package file before changing any runtime byte" $ do- withTempDirectory "keiro-dsl-conformance-atomic" $ \out -> do- parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"- let service = checkedSource parsed- spec = checkedSpec service- ctx = defaultContext (specContext spec)- runtimePackage = RuntimePackageName "hospital-runtime"- modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules- >>= either (\failure -> expectationFailure (show failure)) (const (pure ()))- facade <- case [moduleValue | moduleValue <- modules, ".Conformance" `T.isSuffixOf` moduleNameOf (modulePath moduleValue)] of- [moduleValue] -> pure moduleValue- values -> expectationFailure ("expected one facade, got " <> show (map modulePath values)) >> fail "unreachable"- let facadePath = out </> modulePath facade- serviceKey = contextName ctx- cabalPath = out </> conformancePackageDirectory (StandaloneConformanceService serviceKey) </> T.unpack ("keiro-" <> cabaliseConformanceService serviceKey <> "-conformance.cabal")- TIO.appendFile facadePath "-- would be overwritten if runtime execution began\n"- TIO.writeFile cabalPath "hand-owned cabal file\n"- packageTree <- treeSnapshot out- refused <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules- refused `shouldSatisfy` isLeft- treeSnapshot out `shouldReturn` packageTree- it "keeps a two-aggregate workspace at exactly one Cabal package" $ do- withTempDirectory "keiro-dsl-conformance-workspace" $ \out -> do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let runtimePackage = Just (RuntimePackageName "workspace-runtime")- plan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planWorkspaceScaffoldWithRuntimePackageAndGoldens [] runtimePackage "goldens" (workspaceContext workspace) workspace)- length [() | NAggregate {} <- specNodes (checkedSpec (checkedWorkspace workspace))] `shouldBe` 2- executeWorkspaceScaffold out False plan >>= either (\failure -> expectationFailure (show failure)) (const (pure ()))- packageDirectories <- filter (T.isPrefixOf "keiro-dsl-conformance.workspace." . T.pack) <$> listDirectory out- packageDirectories `shouldBe` ["keiro-dsl-conformance.workspace.demo-project"]- case packageDirectories of- [packageDirectory] -> do- cabalFiles <- filter ((== ".cabal") . takeExtension) <$> listDirectory (out </> packageDirectory)- length cabalFiles `shouldBe` 1- _ -> expectationFailure "expected one package directory"- it "scaffolds the multi-member proof idempotently through the public CLI" $ do- withTempDirectory "keiro-dsl-conformance-proof-cli" $ \base -> do- let fixture = "test/conformance-service-package"- copied = base </> "fixture"- out = copied </> "runtime/src"- sourcePaths =- [ "service.keiro-workspace",- "domain/alpha.keiro",- "domain/beta.keiro",- "domain/evidence.keiro",- "domain/shared.keiro"- ]- fixtureManifest <- resolveTestPath (fixture </> "service.keiro-workspace") >>= canonicalizePath- let fixtureRoot = takeDirectory fixtureManifest- forM_ sourcePaths $ \relative -> TIO.readFile (fixtureRoot </> relative) >>= writeFileWithParents (copied </> relative)- (firstCode, firstOut, firstErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]- unless (firstCode == ExitSuccess) (expectationFailure (firstOut <> firstErr))- firstTree <- treeSnapshot out- length [path | (path, _) <- firstTree, takeExtension path == ".cabal"] `shouldBe` 1- length [path | (path, _) <- firstTree, "Generated/Conformance.hs" `T.isSuffixOf` T.pack path] `shouldBe` 1- let recordPath = out </> "keiro-dsl-conformance.workspace.workspace-proof/keiro-dsl-conformance-record.txt"- record <- parseConformancePackageRecord <$> TIO.readFile recordPath- cprServiceKey <$> record `shouldBe` Just (WorkspaceConformanceService "workspace-proof")- (secondCode, secondOut, secondErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]- unless (secondCode == ExitSuccess) (expectationFailure (secondOut <> secondErr))- secondErr `shouldSatisfy` isInfixOfString "keiro-workspace-proof-conformance.cabal (unchanged)"- secondErr `shouldSatisfy` isInfixOfString "Expectations.hs (skipped: already present)"- secondErr `shouldSatisfy` isInfixOfString "Generated.Conformance"- treeSnapshot out `shouldReturn` firstTree- it "keeps Expectations fixed and turns the generated target red for a changed workflow fact" $ do- withTempDirectory "keiro-dsl-conformance-proof-mutation" $ \base -> do- fixtureManifest <- resolveTestPath "test/conformance-service-package/service.keiro-workspace" >>= canonicalizePath- let fixtureRoot = takeDirectory fixtureManifest- let copied = base </> "fixture"- out = copied </> "runtime/src"- evidencePath = copied </> "domain/evidence.keiro"- expectationsPath = out </> "keiro-dsl-conformance.workspace.workspace-proof/src/KeiroConformance/Expectations.hs"- copyTextTree fixtureRoot copied- acceptedExpectations <- TIO.readFile expectationsPath- TIO.readFile evidencePath- >>= TIO.writeFile evidencePath . T.replace "name \"workspace-proof-workflow\"" "name \"workspace-proof-workflow-v2\""- (scaffoldCode, scaffoldOut, scaffoldErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]- unless (scaffoldCode == ExitSuccess) (expectationFailure (scaffoldOut <> scaffoldErr))- TIO.readFile expectationsPath `shouldReturn` acceptedExpectations- let repositoryRoot = takeDirectory (takeDirectory (takeDirectory fixtureRoot))- projectPath = base </> "mutation.project"- buildDirectory = base </> "dist-newstyle"- packageRoot = out </> "keiro-dsl-conformance.workspace.workspace-proof"- TIO.writeFile- projectPath- ( T.unlines- [ "packages:",- " " <> T.pack (repositoryRoot </> "keiro"),- " " <> T.pack (repositoryRoot </> "keiro-core"),- " " <> T.pack (copied </> "runtime"),- " " <> T.pack packageRoot,- "",- "allow-newer:",- " haxl:time"- ]- )- (testCode, testOut, testErr) <-- readProcessWithExitCode- "cabal"- [ "test",- "--project-file=" <> projectPath,- "--builddir=" <> buildDirectory,- "keiro-workspace-proof-conformance"- ]- ""- testCode `shouldNotBe` ExitSuccess- (testOut <> testErr)- `shouldSatisfy` isInfixOfString "FAIL workflow/WorkspaceProofWorkflow/name expected=\"workspace-proof-workflow\" actual=\"workspace-proof-workflow-v2\""-- describe "new <kind> skeletons (M5)" $ do- forM_ skeletonKinds $ \skeletonKind ->- it ("the " <> T.unpack skeletonKind <> " skeleton selects and preserves the registered stable language") $- assertSkeletonUsesStableLanguage skeletonKind- 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 "stable skeleton scaffolds match the committed compiling modules" $- mapM_ (uncurry assertStableSkeletonMatchesCommitted) 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 "exempts only the authoritative generated transducer module path" $ do- let expressions = syntheticGenerated "Gen/Aggregate/Expressions.hs" "import Keiki.Core qualified as K\nx = K.lit 1"- transducer = syntheticGenerated "Gen/Aggregate/Transducer.hs" "import Keiki.Builder qualified as B\nx = B.slot"- ordinary = syntheticGenerated "Gen/Aggregate/Projection.hs" "import Keiki.Builder qualified as B"- firewallBreaches [expressions, transducer]- `shouldBe` [("Gen/Aggregate/Expressions.hs", "import:Keiki.Core", 1)]- firewallBreaches [ordinary] `shouldBe` [("Gen/Aggregate/Projection.hs", "import:Keiki.Builder", 1)]- it "finds no breach in real scaffolder output (aggregate + process fixtures)" $ do- aggMods <- scaffoldFixture "test/fixtures/reservation.keiro"- procMods <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"- firewallBreaches (aggMods <> procMods) `shouldBe` []-- describe "generated provenance banners (plan 182 M4)" $ do- it "stamps the running package version, effective language, and module origin" $ do- service <- checkedServiceOf "test/fixtures/contract-v4.keiro"- let ctx = defaultContext (specContext (checkedSpec service))- case planServiceScaffold ctx service of- Left refusals -> expectationFailure (show refusals)- Right modules -> do- let generated = [moduleValue | moduleValue <- modules, kind moduleValue == Generated]- generated `shouldSatisfy` (not . null)- forM_ generated $ \moduleValue -> do- let recognized = filter isGeneratedBannerLine (T.lines (moduleText moduleValue))- expected = generatedBannerFor (checkedLanguageContract service) (origin moduleValue)- recognized `shouldBe` [expected]- expected- `shouldSatisfy` T.isInfixOf- ( "keiro-dsl "- <> T.pack (showVersion Package.version)- <> " (language keiro-dsl 4) from contract emergency"- )- workspace <- shouldComposeWorkspace canonicalWorkspacePath- workspacePlan <- shouldPlanWorkspaceSpec workspace- forM_ [moduleValue | (moduleValue, _) <- wpModules workspacePlan, kind moduleValue == Generated] $ \moduleValue ->- filter isGeneratedBannerLine (T.lines (moduleText moduleValue))- `shouldBe` [generatedBannerFor (checkedLanguageContract (checkedWorkspace workspace)) (origin moduleValue)]- it "recognizes only the historical banner and the stamped format" $ do- let contract = effectiveLanguageContract LegacyUnversioned- isGeneratedBannerLine generatedBanner `shouldBe` True- isGeneratedBannerLine (generatedBannerFor contract "aggregate Counter (line 2)") `shouldBe` True- isGeneratedBannerLine "-- @generated by another tool" `shouldBe` False- isGeneratedBannerLine codecComparisonBanner `shouldBe` False- it "migrates a legacy-banner file and keeps repeated scaffold bytes stable" $- withTempDirectory "keiro-dsl-stamped-banner" $ \out -> do- spec <- parseInlineSpec "<stamped-banner>" loweringAggregateSpec- let ctx = defaultContext (specContext spec)- modules <- case planScaffold ctx spec of- Left refusals -> expectationFailure (show refusals) >> pure []- Right planned -> pure planned- case [moduleValue | moduleValue <- modules, kind moduleValue == Generated] of- target : _ -> do- let path = out </> modulePath target- stamped = generatedBannerFor (effectiveLanguageContract LegacyUnversioned) (origin target)- legacyText = T.replace stamped generatedBanner (moduleText target)- createDirectoryIfMissing True (takeDirectory path)- TIO.writeFile path legacyText- first <- executeScaffold out False "counter.keiro" ctx spec modules- first `shouldSatisfy` isSuccessfulScaffold- firstTree <- treeSnapshot out- second <- executeScaffold out False "counter.keiro" ctx spec modules- second `shouldSatisfy` isSuccessfulScaffold- treeSnapshot out `shouldReturn` firstTree- TIO.readFile path `shouldReturn` moduleText target- [] -> expectationFailure "counter scaffold has no Generated module"-- describe "service-aware fixture helpers" $ do- it "retains version-4 contract TypeIDs and their durable admission identities" $ do- service <- checkedServiceOf "test/fixtures/contract-v4.keiro"- modules <- scaffoldFixture "test/fixtures/contract-v4.keiro"- let contractModule = generatedTextEndingIn "Contract.hs" modules- identities = idDomainIdentitiesForService service- contractModule `shouldSatisfy` T.isInfixOf "incidentId :: !(KindID \"inc\")"- contractModule `shouldSatisfy` T.isInfixOf "reservationId :: !(KindID \"rsv\")"- identities- `shouldContain` ["id-domain|name=contract:emergency.IncidentTransferNeedDeclared.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text"]-- 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 (Just ExactGeneratedBannerPresent) `elem` stale- && StaleModule HoleStub oldHoles Nothing `elem` stale- doesFileExist (out </> oldDomain) `shouldReturn` True- doesFileExist (out </> oldHoles) `shouldReturn` True- renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "exact generated banner present; verify unchanged bytes before deleting")- renderScaffoldReport second `shouldSatisfy` all (not . T.isInfixOf "safe to delete")- it "preserves a stale generated path whose exact banner is missing" $- withTempDirectory "keiro-dsl-stale-banner" $ \out -> do- spec <- parseInlineSpec "<stale-banner>" loweringAggregateSpec- first <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext spec)) spec- let oldDomain = onlyPathEndingIn "Counter/Domain.hs" (map fst (reportDispositions first))- renamed = spec {specNodes = map renameCounter (specNodes spec)}- TIO.writeFile (out </> oldDomain) "-- generated by something else\n"- second <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext renamed)) renamed- reportStale second `shouldSatisfy` elem (StaleModule Generated oldDomain (Just ExactGeneratedBannerMissing))- renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "exact generated banner missing; preserve and review")- TIO.readFile (out </> oldDomain) `shouldReturn` "-- generated by something else\n"- 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) (if kind m == Generated then Just ExactGeneratedBannerPresent else Nothing)- | (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) (Just ExactGeneratedBannerPresent) | (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))- requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)- let expected =- ScaffoldRecord- { recSpecPath = "counter.keiro",- recModuleRoot = "",- recLayout = "prefixed",- recSourceLanguage = LegacyUnversioned,- recLanguageContract = effectiveLanguageContract LegacyUnversioned,- recFiles = [(kind m, modulePath m) | (m, _) <- reportDispositions report],- recMappings = [],- recIdDomains = [],- recNominalEqualities = [],- recBindingObligations = [],- recBehaviorRequirements = Behavior.behaviorRecordRows requirements- }- sourceRows = filter ("source-language " `T.isPrefixOf`) (T.lines contents)- withoutSourceRows = T.unlines (filter (not . T.isPrefixOf "source-language ") (T.lines contents))- semanticRows = filter ("semantic-contract " `T.isPrefixOf`) (T.lines contents)- withoutSemanticRows = T.unlines (filter (not . T.isPrefixOf "semantic-contract ") (T.lines contents))- parseRecord contents `shouldBe` Just expected- parseRecord withoutSourceRows `shouldBe` Just expected- parseRecord withoutSemanticRows `shouldBe` Just expected- case sourceRows of- [sourceRow] -> do- parseRecord (T.replace sourceRow (sourceRow <> "\n" <> sourceRow) contents) `shouldBe` Nothing- parseRecord (T.replace sourceRow "source-language {malformed}" contents) `shouldBe` Nothing- _ -> expectationFailure "expected exactly one source-language row"- case semanticRows of- [semanticRow] -> do- parseRecord (T.replace semanticRow (semanticRow <> "\n" <> semanticRow) contents) `shouldBe` Nothing- parseRecord (T.replace semanticRow "semantic-contract {malformed}" contents) `shouldBe` Nothing- parseRecord (T.replace "\"languageVersion\":1" "\"languageVersion\":2" contents) `shouldBe` Nothing- _ -> expectationFailure "expected exactly one semantic-contract row"- parseRecord (T.replace "spec: " "future-field: retained\nspec: " contents) `shouldBe` parseRecord contents- parseRecord (T.replace "record v1" "record v2" contents) `shouldBe` Nothing- it "records declared provenance and reports a header-only scaffold drift" $- withTempDirectory "keiro-dsl-language-drift" $ \out -> do- spec <- parseInlineSpec "<language-drift>" loweringAggregateSpec- let ctx = defaultContext (specContext spec)- modules <- case planScaffold ctx spec of- Left refusals -> expectationFailure (show refusals) >> fail "unreachable"- Right planned -> pure planned- _ <- executePlannedScaffold out "counter.keiro" ctx spec- case languageVersion 1 of- Nothing -> expectationFailure "version 1 was not constructible"- Just version -> do- let declared = DeclaredLanguage version noLoc- result <- executeScaffoldWithLanguage out False "counter.keiro" declared ctx spec modules- case result of- Left refusals -> expectationFailure (show refusals)- Right report -> do- reportSourceLanguageDrift report- `shouldBe` Just (SourceLanguageDrift LegacyUnversioned declared)- contents <- TIO.readFile (reportRecordPath report)- recSourceLanguage <$> parseRecord contents `shouldBe` Just declared-- 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:Json" 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- parsed <- parsedSourceOf "test/fixtures/reservation-dup-upcast-source.keiro"- let spec = parsedSpec parsed- case [aggregate | NAggregate aggregate <- specNodes spec] of- [aggregate] -> do- let modules = scaffoldServiceModules (defaultContext (specContext spec)) (checkedSource parsed)- 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, kind m == HoleStub]- map (takeFileName . modulePath) holes `shouldBe` ["BehaviorHoles.hs", "Holes.hs"]- -- Context nominals/internal/replay plus the stable aggregate surface.- length [m | m <- mods, kind m == Generated] `shouldBe` 10- 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- modules <- scaffoldFixture "test/fixtures/transfer-routing.keiro"- 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"- -- Stable context and aggregate modules plus both hand-owned hole surfaces.- length mods `shouldBe` 12- 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 "keeps inequality-guard samples distinct from register initials" $ do- mods <- scaffoldFixture "test/fixtures/subscription.keiro"- let harness = generatedTextEndingIn "Harness.hs" mods- harness `shouldSatisfy` T.isInfixOf "ActivateSubscriptionData"- harness `shouldSatisfy` T.isInfixOf "Paid"- harness `shouldNotSatisfy` T.isInfixOf "ActivateSubscriptionData (case parseSubscriptionId \"sub_01h455vb4pex5vsknk084sn02q\" of Right parsed -> parsed; Left _ -> error \"generated valid ID sample failed to parse\") (case parseCustomerId \"cust_01h455vb4pex5vsknk084sn02q\" of Right parsed -> parsed; Left _ -> error \"generated valid ID sample failed to parse\") Free"- it "uses consumer-owned nominal initials for equality-guard samples" $ do- mods <- scaffoldFixture "test/fixtures/nominal-scalars.keiro"- let harness = generatedTextEndingIn "Harness.hs" mods- harness `shouldSatisfy` T.isInfixOf "Bindings.initialOrderId"- harness `shouldSatisfy` (not . T.isInfixOf "NominalConformance.Bindings.initialOrderId")- harness `shouldNotSatisfy` T.isInfixOf "case parseOrderId"- 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 reservationId\""- harness `shouldSatisfy` T.isInfixOf "prefix <> \"register hospitalId\""- harness `shouldSatisfy` T.isInfixOf "prefix <> \"register patientAcuity\""- harness `shouldNotSatisfy` 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"- map moduleText twinMods `shouldSatisfy` any (T.isInfixOf "B.replayOnly")- let twinHarness = generatedTextEndingIn "Harness.hs" twinMods- T.count "forwardReplayRequestTransferReservation ::" twinHarness `shouldBe` 1- plainMods <- scaffoldFixture "test/fixtures/reservation.keiro"- map moduleText plainMods `shouldSatisfy` all (not . T.isInfixOf "B.replayOnly")-- describe "service workspace (EP-153)" $ do- describe "manifest grammar" $ do- it "round-trips the canonical fixture manifest byte-for-byte" $ do- source <- readTestText canonicalWorkspacePath- manifest <- shouldParseManifest canonicalWorkspacePath source- wmfService manifest `shouldBe` "demo-project"- wmfRuntimePackage manifest `shouldBe` Nothing- wmfModuleRoot manifest `shouldBe` Just "Demo.Modules.Project"- wmfLayout manifest `shouldBe` Just CollocatedLeaf- map wmrPath (NE.toList (wmfMembers manifest))- `shouldBe` [ "domain/project-artifact.keiro",- "domain/project.keiro",- "domain/shared.keiro"- ]- renderWorkspaceManifest manifest- `shouldBe` T.intercalate- "\n"- [ "service demo-project",- "module Demo.Modules.Project",- "layout collocated",- "spec domain/project-artifact.keiro",- "spec domain/project.keiro",- "spec domain/shared.keiro"- ]- it "round-trips runtime-package canonically immediately after service" $ do- manifest <-- shouldParseManifest "<runtime-package>" $- T.unlines- [ "service mori",- "module Mori.Modules",- "spec domain/mori.keiro",- "runtime-package mori-core",- "layout collocated"- ]- wmfRuntimePackage manifest `shouldBe` Just (RuntimePackageName "mori-core")- effectiveRuntimePackage Nothing manifest `shouldBe` Just (RuntimePackageName "mori-core")- effectiveRuntimePackage (Just (RuntimePackageName "mori-dev")) manifest- `shouldBe` Just (RuntimePackageName "mori-dev")- renderWorkspaceManifest manifest- `shouldBe` T.intercalate- "\n"- [ "service mori",- "runtime-package mori-core",- "module Mori.Modules",- "layout collocated",- "spec domain/mori.keiro"- ]- it "validates runtime package names with the mapped-source Cabal grammar" $ do- mkRuntimePackageName "mori-core" `shouldBe` Right (RuntimePackageName "mori-core")- mkRuntimePackageName "mori_core" `shouldBe` Left "runtime package 'mori_core' does not follow Cabal package-name grammar"- it "treats membership as a set: source order changes neither the AST nor the bytes" $ do- canonical <- readTestText canonicalWorkspacePath >>= shouldParseManifest canonicalWorkspacePath- reordered <-- shouldParseManifest "<reordered>" $- T.unlines- [ "service demo-project",- "layout collocated",- "spec domain/shared.keiro",- "module Demo.Modules.Project",- "spec domain/project.keiro",- "spec ./domain/project-artifact.keiro"- ]- reordered `shouldBe` canonical- renderWorkspaceManifest reordered `shouldBe` renderWorkspaceManifest canonical- it "satisfies parse . render == id and render . parse . render == render" $- property $- forAll genWorkspaceManifest $ \manifest ->- let rendered = renderWorkspaceManifest manifest- in case parseWorkspaceManifest "<generated>" rendered of- Left err -> counterexample (T.unpack err) False- Right reparsed ->- counterexample (T.unpack rendered) $- reparsed == manifest && renderWorkspaceManifest reparsed == rendered- it "recognizes a workspace manifest by extension, case-insensitively" $ do- map- isWorkspacePath- [ "service.keiro-workspace",- "a/b/Service.KEIRO-Workspace",- "service.keiro",- ".keiro-workspace",- "keiro-workspace"- ]- `shouldBe` [True, True, False, False, False]- describe "manifest refusals" $ do- let rejects description source expected =- it description $ case parseWorkspaceManifest "<manifest>" source of- Right _ -> expectationFailure ("expected a refusal, got a manifest for:\n" <> T.unpack source)- Left err -> T.unpack err `shouldContain` expected- rejects- "an empty manifest"- "# only a comment\n"- "must begin with a 'service <name>' clause"- rejects- "a manifest with no service clause"- "spec domain/a.keiro\n"- "first clause of a workspace manifest must be 'service <name>'"- rejects- "a manifest whose first clause is not service"- "module Demo\nservice demo\nspec domain/a.keiro\n"- "first clause of a workspace manifest must be 'service <name>'"- rejects- "a duplicate service clause"- "service demo\nservice demo\nspec domain/a.keiro\n"- "duplicate 'service' clause"- rejects- "a duplicate module clause"- "service demo\nmodule Demo\nmodule Demo\nspec domain/a.keiro\n"- "duplicate 'module' clause"- rejects- "a duplicate runtime-package clause"- "service demo\nruntime-package demo-core\nruntime-package demo-api\nspec domain/a.keiro\n"- "duplicate 'runtime-package' clause"- it "locates a malformed runtime-package at its manifest line" $ case parseWorkspaceManifest "<manifest>" "service demo\nspec domain/a.keiro\nruntime-package demo_core\n" of- Right _ -> expectationFailure "expected a malformed runtime package refusal"- Left err -> do- T.unpack err `shouldContain` "<manifest>:3:1"- T.unpack err `shouldContain` "does not follow Cabal package-name grammar"- rejects- "a duplicate layout clause"- "service demo\nlayout prefixed\nlayout prefixed\nspec domain/a.keiro\n"- "duplicate 'layout' clause"- rejects- "a manifest with no members"- "service demo\nmodule Demo\n"- "must list at least one 'spec <path>.keiro' member"- rejects- "the same member listed twice"- "service demo\nspec domain/a.keiro\nspec ./domain/a.keiro\n"- "duplicate workspace member 'domain/a.keiro'"- rejects- "two members that differ only by case"- "service demo\nspec domain/a.keiro\nspec domain/A.keiro\n"- "differ only by case"- rejects- "an absolute member path"- "service demo\nspec /etc/a.keiro\n"- "must be relative, not absolute"- rejects- "a member path escaping the manifest directory"- "service demo\nspec ../escape.keiro\n"- "must not contain '..' segments"- rejects- "a member that is not a .keiro spec"- "service demo\nspec domain/a.txt\n"- "must name a .keiro spec"- rejects- "a manifest listing another manifest"- "service demo\nspec domain/other.keiro-workspace\n"- "must name a .keiro spec"- describe "line relocation" $ do- it "shifts every location the AST carries, and only the locations" $ do- spec <- specOf "test/fixtures/reservation.keiro"- let shifted = relocateLocs (+ 1000) spec- collectLocs spec `shouldSatisfy` (not . null)- collectLocs shifted `shouldBe` map (+ 1000) (collectLocs spec)- -- Loc's Eq deliberately ignores the line, so relocation cannot- -- change any equality-based behavior anywhere downstream.- shifted `shouldBe` spec- it "leaves the placeholder location alone so it never lands inside a member range" $ do- spec <- specOf "test/fixtures/reservation.keiro"- let blanked = relocateLocs (const 0) spec- reshifted = relocateLocs (\n -> if n <= 0 then n else n + 500) blanked- collectLocs reshifted `shouldBe` map (const 0) (collectLocs spec)- describe "composition" $ do- it "resolves cross-file ids, enums, mapped types, and read-model feeds" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- wsService workspace `shouldBe` "demo-project"- wsContext workspace `shouldBe` "demo-project"- wsModuleRoot workspace `shouldBe` Just "Demo.Modules.Project"- wsLayout workspace `shouldBe` Just CollocatedLeaf- map wmPath (wsMembers workspace)- `shouldBe` [ "domain/project-artifact.keiro",- "domain/project.keiro",- "domain/shared.keiro"- ]- -- Every member is individually incomplete; together they check.- checkWorkspace workspace `shouldBe` []- it "records which member owns each shared declaration and node" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let ownership = wsOwnership workspace- fmap fst (declarationOwner ownership "id" "ProjectId")- `shouldBe` Just "domain/shared.keiro"- fmap fst (declarationOwner ownership "enum" "ProjectPhase")- `shouldBe` Just "domain/shared.keiro"- fmap fst (declarationOwner ownership "rule" "phaseIsTerminal")- `shouldBe` Just "domain/shared.keiro"- fmap fst (declarationOwner ownership "mapped" "ProjectSummary")- `shouldBe` Just "domain/shared.keiro"- fmap fst (nodeOwner ownership "aggregate" "Project")- `shouldBe` Just "domain/project.keiro"- fmap fst (nodeOwner ownership "aggregate" "ProjectArtifact")- `shouldBe` Just "domain/project-artifact.keiro"- fmap fst (nodeOwner ownership "readmodel" "project_activity")- `shouldBe` Just "domain/project-artifact.keiro"- it "maps every merged line back to the member that wrote it" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let bases = [(wmPath m, wmLineBase m, wmLineCount m) | m <- wsMembers workspace]- -- Ranges are disjoint and contiguous from zero.- map (\(_, base, _) -> base) bases `shouldBe` scanl (+) 0 (init [c | (_, _, c) <- bases])- sequence_- [ resolveWorkspaceLine workspace (base + offset) `shouldBe` Just (path, offset)- | (path, base, memberLines) <- bases,- offset <- [1, memberLines]- ]- resolveWorkspaceLine workspace 0 `shouldBe` Nothing- it "is insensitive to the order members are listed in" $ do- canonical <- shouldComposeWorkspace canonicalWorkspacePath- reordered <- shouldComposeWorkspace reorderedWorkspacePath- reordered {wsManifestPath = wsManifestPath canonical} `shouldBe` canonical- it "checks a single .keiro file as a one-member workspace, diagnostic for diagnostic" $ do- let fixtures =- [ "test/fixtures/reservation.keiro",- "test/fixtures/consumer-types.keiro",- "test/fixtures/aggregate-bad-refs.keiro",- "test/fixtures/readmodel.keiro"- ]- forM_ fixtures $ \path -> do- spec <- specOf path- let workspace = oneMemberWorkspace path spec- viaWorkspace = map (renderWorkspaceDiagnostic path) (checkWorkspace workspace)- direct = map (renderDiagnostic path) (validateSpec spec)- viaWorkspace `shouldBe` direct- -- At least one of those fixtures must actually produce errors,- -- or the equivalence claim is vacuous.- badRefs <- specOf "test/fixtures/aggregate-bad-refs.keiro"- checkWorkspace (oneMemberWorkspace "test/fixtures/aggregate-bad-refs.keiro" badRefs)- `shouldSatisfy` any ((== Error) . wdSeverity)- describe "composition refusals" $ do- let refusesWith path expectedCode expectedFiles = do- diagnostics <- shouldRefuseWorkspace path- map wdCode (NE.toList diagnostics) `shouldContain` [expectedCode]- let cited =- [ wlFile location- | diagnostic <- NE.toList diagnostics,- wdCode diagnostic == expectedCode,- location <- NE.toList (wdLocations diagnostic)- ]- sort (nubOrd cited) `shouldBe` sort expectedFiles- it "refuses members that declare different contexts, citing every context clause" $- refusesWith- "test/fixtures/workspace-context-mismatch/service.keiro-workspace"- WorkspaceContextMismatch- [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]- it "refuses a member layout clause that contradicts the manifest authority" $- refusesWith- "test/fixtures/workspace-authority-conflict/service.keiro-workspace"- WorkspaceAuthorityConflict- [WorkspaceManifestFile, WorkspaceMemberFile "domain/b.keiro"]- it "refuses a textually identical shared declaration owned by two members" $- refusesWith- "test/fixtures/workspace-dup-decl/service.keiro-workspace"- WorkspaceDuplicateDeclaration- [WorkspaceMemberFile "domain/project.keiro", WorkspaceMemberFile "domain/shared.keiro"]- it "refuses one aggregate defined in two members" $- refusesWith- "test/fixtures/workspace-dup-node/service.keiro-workspace"- WorkspaceDuplicateNodeName- [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]- it "refuses generated paths that collide across members under case folding" $- refusesWith- "test/fixtures/workspace-path-collision/service.keiro-workspace"- WorkspacePathCollision- [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]- it "reports a listed member that is missing from disk" $- refusesWith- "test/fixtures/workspace-missing-member/service.keiro-workspace"- WorkspaceMemberUnreadable- [WorkspaceManifestFile]- it "reports a member that does not parse" $- refusesWith- "test/fixtures/workspace-member-parse-failed/service.keiro-workspace"- WorkspaceMemberParseFailed- [WorkspaceManifestFile]- it "surfaces a cross-file unresolved reference through the merged validator" $ do- workspace <- shouldComposeWorkspace "test/fixtures/workspace-unresolved/service.keiro-workspace"- let errors = [d | d <- checkWorkspace workspace, wdSeverity d == Error]- map wdCode errors `shouldContain` [GuardAtomOutOfScope]- [wlFile location | d <- errors, location <- NE.toList (wdLocations d)]- `shouldContain` [WorkspaceMemberFile "domain/project.keiro"]- describe "multi-file diagnostic rendering" $ do- it "puts the primary location in the established shape and every other file on a note line" $ do- diagnostics <- shouldRefuseWorkspace "test/fixtures/workspace-dup-decl/service.keiro-workspace"- let manifest = "keiro-dsl/test/fixtures/workspace-dup-decl/service.keiro-workspace"- map (renderWorkspaceDiagnostic manifest) (NE.toList diagnostics)- `shouldBe` [ T.intercalate- "\n"- [ "keiro-dsl/test/fixtures/workspace-dup-decl/domain/project.keiro:4: error[WorkspaceDuplicateDeclaration]: duplicate declaration 'ProjectId': a shared declaration has exactly one owning member (identical duplicates do not merge)",- " keiro-dsl/test/fixtures/workspace-dup-decl/domain/shared.keiro:4: note: also declared here, as id 'ProjectId'"- ]- ]- describe "whole-service check through the CLI" $ do- it "prints OK and exits zero for the composed fixture workspace" $ do- (exitCode, out, err) <- runKeiroDsl ["check", canonicalWorkspacePath]- unless (exitCode == ExitSuccess) (expectationFailure (out <> err))- lines out `shouldBe` ["OK"]- it "exits non-zero and names every involved file for a cross-file refusal" $ do- (exitCode, _, err) <-- runKeiroDsl ["check", "test/fixtures/workspace-dup-decl/service.keiro-workspace"]- exitCode `shouldBe` ExitFailure 1- err `shouldContain` "error[WorkspaceDuplicateDeclaration]"- err `shouldContain` "workspace-dup-decl/domain/project.keiro:4"- err `shouldContain` "workspace-dup-decl/domain/shared.keiro:4"- it "attributes a merged-graph validation error to the member that wrote it" $ do- (exitCode, _, err) <-- runKeiroDsl ["check", "test/fixtures/workspace-unresolved/service.keiro-workspace"]- exitCode `shouldBe` ExitFailure 1- err `shouldContain` "workspace-unresolved/domain/project.keiro:12: error[GuardAtomOutOfScope]"- it "produces byte-identical output for a manifest whose members are listed in reverse" $ do- (canonicalCode, canonicalOut, _) <- runKeiroDsl ["check", canonicalWorkspacePath, "--emit"]- (reorderedCode, reorderedOut, _) <- runKeiroDsl ["check", reorderedWorkspacePath, "--emit"]- canonicalCode `shouldBe` ExitSuccess- reorderedCode `shouldBe` ExitSuccess- reorderedOut `shouldBe` canonicalOut- (_, canonicalParse, _) <- runKeiroDsl ["parse", canonicalWorkspacePath]- (_, reorderedParse, _) <- runKeiroDsl ["parse", reorderedWorkspacePath]- reorderedParse `shouldBe` canonicalParse- it "keeps the single-file path working, byte for byte" $ do- (exitCode, out, err) <- runKeiroDsl ["check", "test/fixtures/reservation.keiro"]- unless (exitCode == ExitSuccess) (expectationFailure (out <> err))- lines out `shouldBe` ["OK"]- it "explains bindings and reports coverage against the merged graph" $ do- (bindingsCode, bindingsOut, _) <-- runKeiroDsl ["check", canonicalWorkspacePath, "--explain-bindings"]- bindingsCode `shouldBe` ExitSuccess- bindingsOut `shouldContain` "binding obligations for context demo-project"- -- The obligation's use sites span both aggregate members, which- -- is only possible because the graph was resolved once, merged.- bindingsOut `shouldContain` "Project register summary : ProjectSummary"- bindingsOut `shouldContain` "ProjectArtifact command RecordArtifact .artifactSummary : ProjectSummary"- withTempDirectory "keiro-dsl-workspace-coverage" $ \out -> do- let reportPath = out </> "coverage.json"- (coverageCode, coverageOut, _) <-- runKeiroDsl ["check", canonicalWorkspacePath, "--coverage-report", reportPath]- coverageCode `shouldBe` ExitSuccess- coverageOut `shouldContain` "structural/opaque boundaries (reporting only)"- report <- Aeson.eitherDecodeFileStrict reportPath- case report of- Left err -> expectationFailure err- Right value -> coverageSpecPath value `shouldBe` Just (T.pack canonicalWorkspacePath)-- describe "workspace diff revision loading (EP-155 M1)" $ do- it "composes added, removed, and renamed members through an in-memory content source" $ do- project <- readTestText "test/fixtures/workspace/domain/project.keiro"- artifact <- readTestText "test/fixtures/workspace/domain/project-artifact.keiro"- shared <- readTestText "test/fixtures/workspace/domain/shared.keiro"- let extra = "language keiro-dsl 4\ncontext demo-project\n\nid ExtraId prefix=extra\n"- manifest members =- T.unlines- ( ["service demo-project", "module Demo.Modules.Project", "layout collocated"]- <> ["spec " <> T.pack member | member <- members]- )- baseFiles =- Map.fromList- [ ("domain/project.keiro", project),- ("domain/project-artifact.keiro", artifact),- ("domain/shared.keiro", shared)- ]- loadFrom members files =- loadWorkspace- (memoryContentSource (Map.insert "service.keiro-workspace" (manifest members) files))- "service.keiro-workspace"- baseMembers = ["domain/project.keiro", "domain/project-artifact.keiro", "domain/shared.keiro"]- expectLoaded result = case result of- Left failure -> expectationFailure (show failure) >> error "unreachable"- Right workspace -> pure workspace-- oldAdded <- loadFrom baseMembers baseFiles >>= expectLoaded- newAdded <-- loadFrom- (baseMembers <> ["domain/extra.keiro"])- (Map.insert "domain/extra.keiro" extra baseFiles)- >>= expectLoaded- map changeCode (diffSpecs (wsMergedSpec oldAdded) (wsMergedSpec newAdded))- `shouldContain` [DeclarationAdded]-- oldRemoved <- loadFrom baseMembers baseFiles >>= expectLoaded- newRemoved <-- loadFrom- ["domain/project.keiro", "domain/shared.keiro"]- (Map.delete "domain/project-artifact.keiro" baseFiles)- >>= expectLoaded- map changeCode (diffSpecs (wsMergedSpec oldRemoved) (wsMergedSpec newRemoved))- `shouldContain` [EvtRemovedNotDeprecated]-- oldRenamed <- loadFrom baseMembers baseFiles >>= expectLoaded- let renamedMembers = ["domain/project-renamed.keiro", "domain/project-artifact.keiro", "domain/shared.keiro"]- renamedFiles = Map.insert "domain/project-renamed.keiro" project (Map.delete "domain/project.keiro" baseFiles)- newRenamed <- loadFrom renamedMembers renamedFiles >>= expectLoaded- diffSpecs (wsMergedSpec oldRenamed) (wsMergedSpec newRenamed) `shouldBe` []-- describe "workspace diff ownership and unified reports (EP-155 M2)" $ do- it "classifies shared declarations at use sites across every member with owned citations" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"- let changes = diffWorkspaces old new- enumChanges = filter ((== EnumCtorAdded) . changeCode . wcChange) changes- mappedChanges = filter ((== MappedFieldTypeChanged) . changeCode . wcChange) changes- citedFiles workspaceChanges =- [ osFile site- | change <- workspaceChanges,- (_, Just site) <- wcUseSites change- ]- enumChanges `shouldSatisfy` (not . null)- mappedChanges `shouldSatisfy` (not . null)- let enumWireChanges =- [ change- | workspaceChange <- enumChanges,- let change = wcChange workspaceChange,- OldBinaryReadNewEvents `elem` breakingSurfaces change- ]- enumWireChanges `shouldSatisfy` (not . null)- enumWireChanges `shouldSatisfy` all (not . gatedBreaking defaultGate)- enumWireChanges `shouldSatisfy` all (gatedBreaking (gateWith [OldBinaryReadNewEvents]))- map (fmap osFile . wcDeclarationSite) (enumChanges <> mappedChanges)- `shouldSatisfy` all (== Just "domain/shared.keiro")- citedFiles enumChanges `shouldContain` ["domain/order.keiro", "domain/shipment.keiro"]- citedFiles mappedChanges `shouldContain` ["domain/order.keiro", "domain/shipment.keiro"]- let rendered = T.intercalate "\n" (map renderWorkspaceFinding (enumChanges <> mappedChanges))- rendered `shouldSatisfy` T.isInfixOf " declared: domain/shared.keiro:4"- rendered `shouldSatisfy` T.isInfixOf " use-site: Order"- rendered `shouldSatisfy` T.isInfixOf "(domain/order.keiro:"- rendered `shouldSatisfy` T.isInfixOf "(domain/shipment.keiro:"- golden <- readTestText "test/fixtures/workspace-diff-new/workspace.diff.golden"- T.unlines (map renderWorkspaceFinding changes) `shouldBe` golden-- it "emits one additive version-1 report with workspace provenance" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"- let changes = diffWorkspaces old new- meta =- WorkspaceMeta- { wmIdentity = wsService new,- wmManifest = "service.keiro-workspace",- wmSince = "HEAD",- wmMembersOld = map wmPath (wsMembers old),- wmMembersNew = map wmPath (wsMembers new),- wmAdoptionBaseline = False- }- case Aeson.toJSON (workspaceDiffReport meta defaultGate changes) of- Aeson.Object report -> do- KeyMap.lookup "schema" report `shouldBe` Just (Aeson.String "keiro-dsl/diff-report/1")- case KeyMap.lookup "workspace" report of- Just (Aeson.Object workspace) -> do- KeyMap.lookup "identity" workspace `shouldBe` Just (Aeson.String "workspace-diff")- KeyMap.lookup "adoptionBaseline" workspace `shouldBe` Just (Aeson.Bool False)- other -> expectationFailure ("missing workspace report metadata: " <> show other)- case KeyMap.lookup "findings" report of- Just (Aeson.Array findings) -> do- findings `shouldSatisfy` (not . null)- let objects = [finding | Aeson.Object finding <- toList findings]- objects `shouldSatisfy` any (KeyMap.member "declaration")- objects `shouldSatisfy` any (KeyMap.member "useSites")- other -> expectationFailure ("missing workspace findings: " <> show other)- other -> expectationFailure ("workspace report was not an object: " <> show other)-- it "computes one replay-impact value over both aggregates" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"- case replayImpactSpecs (wsMergedSpec old) (wsMergedSpec new) of- ReplayAffected affected -> Map.keysSet affected `shouldBe` Set.fromList ["Order", "Shipment"]- ReplayNeutral -> expectationFailure "shared mapped evolution unexpectedly reported replay-neutral"-- describe "workspace ownership and authority changes (EP-155 M3)" $ do- it "reports an unchanged aggregate move once without wire evolution" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- moved <- shouldComposeWorkspace "test/fixtures/workspace-diff-moved/service.keiro-workspace"- let changes = diffWorkspaces old moved- map (changeCode . wcChange) changes `shouldBe` [OwnershipMoved]- forM_ changes $ \workspaceMove -> do- let move = wcChange workspaceMove- move `shouldSatisfy` isAdvisory- move `shouldSatisfy` (not . gatedBreaking defaultGate)- move `shouldSatisfy` (not . gatedBreaking (gateWith [minBound .. maxBound]))- deriveLabel defaultGate (ckVector (workspaceChangeKind move)) `shouldBe` LabelAdvisory- remediationFor (ckContext (workspaceChangeKind move)) OwnershipMoved- `shouldBe` (RemedyRescaffoldWorkspace :| [])- renderWorkspaceFinding workspaceMove- `shouldSatisfy` T.isInfixOf "declaration moved domain/shipment.keiro -> domain/order.keiro"-- it "treats a member rename as the same owner-map change" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- let ownership = wsOwnership old- renamed =- old- { wsOwnership =- ownership- { oiNodes =- Map.adjust- (\(_, loc) -> ("domain/shipping.keiro", loc))- ("aggregate", "Shipment")- (oiNodes ownership)- }- }- moves = filter ((== OwnershipMoved) . changeCode . wcChange) (diffWorkspaces old renamed)- length moves `shouldBe` 1- forM_ moves $ \move ->- renderWorkspaceFinding move `shouldSatisfy` T.isInfixOf "domain/shipment.keiro -> domain/shipping.keiro"-- it "reports ownership motion beside an independently classified wire edit" $ do- old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"- edited <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"- let ownership = wsOwnership edited- movedAndEdited =- edited- { wsOwnership =- ownership- { oiNodes =- Map.adjust- (\(_, loc) -> ("domain/order.keiro", loc))- ("aggregate", "Shipment")- (oiNodes ownership)- }- }- codes = map (changeCode . wcChange) (diffWorkspaces old movedAndEdited)- codes `shouldContain` [OwnershipMoved]- codes `shouldContain` [MappedFieldTypeChanged]-- it "reports context authority separately from derived read-model identity breaks" $ do- old <- shouldComposeWorkspace canonicalWorkspacePath- let newContext = "demo-project-renamed"- renamed =- old- { wsContext = newContext,- wsMergedSpec = (wsMergedSpec old) {specContext = newContext}- }- changes = diffWorkspaces old renamed- codes = map (changeCode . wcChange) changes- codes `shouldContain` [WorkspaceAuthorityChanged]- codes `shouldContain` [DerivedIdentityChanged]- map wcChange changes `shouldSatisfy` any (gatedBreaking defaultGate)-- it "keeps service, module-root, and layout authority advisories non-blocking" $ do- old <- shouldComposeWorkspace canonicalWorkspacePath- let changed =- old- { wsService = "demo-project-renamed",- wsModuleRoot = Just "Demo.Modules.Renamed",- wsLayout = Just GeneratedPrefix- }- authority = filter ((== WorkspaceAuthorityChanged) . changeCode . wcChange) (diffWorkspaces old changed)- length authority `shouldBe` 3- forM_ (map wcChange authority) $ \change -> do- deriveLabel defaultGate (ckVector (workspaceChangeKind change)) `shouldBe` LabelAdvisory- change `shouldSatisfy` (not . gatedBreaking (gateWith [minBound .. maxBound]))- remediationFor (ckContext (workspaceChangeKind change)) WorkspaceAuthorityChanged- `shouldBe` (RemedyRescaffoldWorkspace :| [RemedyRecompileConsumers])-- describe "workspace scaffold (EP-154)" $ do- describe "workspace record" $ do- it "round-trips modules, owners, members, mappings, obligations, and adoptions" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let record = sampleWorkspaceRecord workspace- rendered = renderWorkspaceRecord record- parseWorkspaceRecord rendered `shouldBe` Just record- -- The header pins the schema: a v1 context-keyed record and a- -- workspace record can never be read as each other.- T.lines rendered `shouldSatisfy` \case- header : _ -> header == "keiro-dsl workspace scaffold record v1"- [] -> False- parseRecord rendered `shouldBe` Nothing- parseWorkspaceRecord (T.replace "record v1" "record v2" rendered) `shouldBe` Nothing- it "ignores unknown rows and unknown JSON keys, and keeps context-level rows ownerless" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let record = sampleWorkspaceRecord workspace- rendered = renderWorkspaceRecord record- parseWorkspaceRecord (T.replace "service: " "future-row: retained\nservice: " rendered)- `shouldBe` Just record- parseWorkspaceRecord (T.replace "\"kind\":\"generated\"" "\"kind\":\"generated\",\"future\":1" rendered)- `shouldBe` Just record- [row | row <- wrModules record, wrmOwner row == Nothing]- `shouldSatisfy` (not . null)- it "rejects absent stable language rows and partial, duplicate, malformed, or inconsistent contracts" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let record = sampleWorkspaceRecord workspace- rendered = renderWorkspaceRecord record- sourceRows = filter ("source-language " `T.isPrefixOf`) (T.lines rendered)- withoutSourceRows = T.unlines (filter (not . T.isPrefixOf "source-language ") (T.lines rendered))- semanticRows = filter ("semantic-contract " `T.isPrefixOf`) (T.lines rendered)- withoutSemanticRows = T.unlines (filter (not . T.isPrefixOf "semantic-contract ") (T.lines rendered))- parseWorkspaceRecord withoutSourceRows `shouldBe` Nothing- case sourceRows of- firstRow : secondRow : _ -> do- parseWorkspaceRecord (T.unlines (filter (/= secondRow) (T.lines rendered))) `shouldBe` Nothing- parseWorkspaceRecord (T.replace firstRow (firstRow <> "\n" <> firstRow) rendered) `shouldBe` Nothing- parseWorkspaceRecord (T.replace firstRow "source-language {malformed}" rendered) `shouldBe` Nothing- _ -> expectationFailure "expected multiple workspace source-language rows"- parseWorkspaceRecord withoutSemanticRows `shouldBe` Just record- case semanticRows of- [semanticRow] -> do- parseWorkspaceRecord (T.replace semanticRow (semanticRow <> "\n" <> semanticRow) rendered) `shouldBe` Nothing- parseWorkspaceRecord (T.replace semanticRow "semantic-contract {malformed}" rendered) `shouldBe` Nothing- parseWorkspaceRecord (T.replace "\"languageVersion\":4" "\"languageVersion\":3" rendered) `shouldBe` Nothing- _ -> expectationFailure "expected one workspace semantic-contract row"- it "rejects unsafe module, owner, member, and adoption paths" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let rendered = renderWorkspaceRecord (sampleWorkspaceRecord workspace)- corrupt from to = parseWorkspaceRecord (T.replace from to rendered)- corrupt "member domain/shared.keiro" "member /etc/passwd" `shouldBe` Nothing- corrupt "member domain/shared.keiro" "member ../escape.keiro" `shouldBe` Nothing- corrupt "\"owner\":\"domain/shared.keiro\"" "\"owner\":\"../shared.keiro\"" `shouldBe` Nothing- corrupt "\"path\":\"claimed/One.hs\"" "\"path\":\"/tmp/One.hs\"" `shouldBe` Nothing- it "keys history by service in a slot no context name can reach" $ do- -- A context name is lexed as letters/digits/_/- and can never- -- contain a dot, so the workspace slot cannot alias a legacy- -- record even when the service is named after its context.- workspaceRecordFileName "demo-project"- `shouldBe` "keiro-dsl-scaffold-record.workspace.demo-project.txt"- workspaceManifestFileName "demo-project"- `shouldBe` "keiro-dsl-manifest.workspace.demo-project.txt"- workspaceRecordFileName "demo-project" `shouldNotBe` recordFileName "demo-project"- map- (T.isInfixOf "." . T.pack)- [ workspaceRecordFileName "demo-project",- recordFileName "demo-project"- ]- `shouldBe` [True, True]- supersededByLine "demo-project"- `shouldBe` "superseded-by: keiro-dsl-scaffold-record.workspace.demo-project.txt"-- describe "workspace plan" $ do- it "emits the context-level facade and replay-audit exactly once from the merged graph" $ do- plan <- shouldPlanWorkspace canonicalWorkspacePath- let modules = map fst (wpModules plan)- facades = [m | m <- modules, "StructuralProjections.hs" `isSuffixOfPath` m]- audits = [m | m <- modules, "ReplayAudit.hs" `isSuffixOfPath` m]- shapes = [m | m <- modules, "Structural/Shape/ProjectSummary.hs" `isSuffixOfPath` m]- length facades `shouldBe` 1- length audits `shouldBe` 1- length shapes `shouldBe` 1- -- The audit assembles aggregates owned by two different member- -- files, which is only possible from one merged graph.- forM_ audits $ \audit -> do- moduleText audit `shouldSatisfy` T.isInfixOf "Project.projectEventStream"- moduleText audit `shouldSatisfy` T.isInfixOf "ProjectArtifact.projectArtifactEventStream"- it "gives every generated ID and enum one context owner and imports only aggregate uses" $ do- plan <- shouldPlanWorkspace canonicalWorkspacePath- let ctx = wpContext plan- modules = map fst (wpModules plan)- nominalModules = [m | m <- modules, modulePath m == T.unpack (T.replace "." "/" (generatedNominalModule ctx) <> ".hs")]- internalNominalModules = [m | m <- modules, modulePath m == T.unpack (T.replace "." "/" (generatedNominalModule ctx) <> "/Internal.hs")]- domainFor suffix = case [m | m <- modules, suffix `isSuffixOfPath` m] of- [m] -> pure m- found -> expectationFailure ("expected one domain ending in " <> suffix <> ", got " <> show (map modulePath found)) >> fail "unreachable"- ownerModule <- case nominalModules of- [m] -> pure m- found -> expectationFailure ("expected one generated nominal owner, got " <> show (map modulePath found)) >> fail "unreachable"- internalOwnerModule <- case internalNominalModules of- [m] -> pure m- found -> expectationFailure ("expected one generated internal nominal owner, got " <> show (map modulePath found)) >> fail "unreachable"- let nominalText = moduleText ownerModule- internalNominalText = moduleText internalOwnerModule- T.count "newtype ProjectId" nominalText `shouldBe` 0- T.count "newtype ProjectId" internalNominalText `shouldBe` 1- T.count "data ProjectPhase =" nominalText `shouldBe` 1- T.count "data WorkspaceVisibility =" nominalText `shouldBe` 1- projectDomain <- domainFor "Project/Generated/Domain.hs"- artifactDomain <- domainFor "ProjectArtifact/Generated/Domain.hs"- forM_ [projectDomain, artifactDomain] $ \domain -> do- moduleText domain `shouldSatisfy` (not . T.isInfixOf "newtype ProjectId")- moduleText domain `shouldSatisfy` (not . T.isInfixOf "data ProjectPhase")- moduleText domain `shouldSatisfy` T.isInfixOf (generatedNominalModule ctx <> " (ProjectId, parseProjectId, ProjectPhase (..))")- moduleText domain `shouldSatisfy` (not . T.isInfixOf "WorkspaceVisibility")- singleFileModules <- case planServiceScaffold ctx (stableCheckedService (wsMergedSpec (wpWorkspace plan))) of- Left refusals -> expectationFailure (show refusals) >> fail "unreachable"- Right values -> pure values- let withoutOrigin m = (modulePath m, moduleText m, kind m)- map withoutOrigin singleFileModules `shouldBe` map withoutOrigin modules- owners <- case planNominalGeneration ctx (wsMergedSpec (wpWorkspace plan)) of- Left errors -> expectationFailure (show errors) >> fail "unreachable"- Right values -> pure values- map (resolvedNominalName . nominalDeclaration) owners- `shouldBe` ["ProjectId", "ProjectPhase", "WorkspaceVisibility"]- case [owner | owner <- owners, resolvedNominalName (nominalDeclaration owner) == "ProjectId"] of- [owner] -> do- nominalModule owner `shouldBe` generatedNominalModule ctx- Set.fromList [NominalUseSite "Project" RegisterUse, NominalUseSite "ProjectArtifact" EventFieldUse]- `shouldSatisfy` (`Set.isSubsetOf` nominalUseSites owner)- found -> expectationFailure ("expected one ProjectId owner, got " <> show (length found))- it "attributes every module to its owning member and leaves shared ones context-level" $ do- plan <- shouldPlanWorkspace canonicalWorkspacePath- let memberPaths = map wmPath (wsMembers (wpWorkspace plan))- ownerOf suffix =- case [provenance | (m, provenance) <- wpModules plan, suffix `isSuffixOfPath` m] of- [provenance] -> Just provenance- _ -> Nothing- ownerOf "StructuralProjections.hs" `shouldBe` Just ContextLevel- ownerOf "Generated/Nominals.hs" `shouldBe` Just ContextLevel- ownerOf "ReplayAudit.hs" `shouldBe` Just ContextLevel- ownerOf "Structural/Shape/ProjectSummary.hs"- `shouldBe` Just (MemberOwned "domain/shared.keiro")- ownerOf "Project/Generated/Domain.hs"- `shouldBe` Just (MemberOwned "domain/project.keiro")- ownerOf "ProjectArtifact/Generated/Domain.hs"- `shouldBe` Just (MemberOwned "domain/project-artifact.keiro")- ownerOf "Project_activity/Generated/ReadModel.hs"- `shouldBe` Just (MemberOwned "domain/project-artifact.keiro")- -- No module may claim an owner that is not a member of the- -- workspace: the record's owner column has to stay resolvable.- map (provenanceOwner . snd) (wpModules plan)- `shouldSatisfy` all (maybe True (`elem` memberPaths))- it "keeps the compiled workspace nominal conformance tree byte-current" $ do- workspace <- shouldComposeWorkspace "test/fixtures/workspace-nominals/service.keiro-workspace"- plan <- shouldPlanWorkspaceSpec workspace- let compiledPaths =- [ "Generated/WorkspaceNominalProof/Nominals.hs",- "Generated/WorkspaceNominalProof/Project/Domain.hs",- "Generated/WorkspaceNominalProof/Project/Codec.hs",- "Generated/WorkspaceNominalProof/Project/Transducer.hs",- "Generated/WorkspaceNominalProof/Project/BehaviorContract.hs",- "Generated/WorkspaceNominalProof/Project/EventStream.hs",- "Generated/WorkspaceNominalProof/Project/Harness.hs",- "Generated/WorkspaceNominalProof/Project/Projection.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/Domain.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/Codec.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/Transducer.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/BehaviorContract.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/EventStream.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/Harness.hs",- "Generated/WorkspaceNominalProof/ProjectArtifact/Projection.hs",- "Generated/WorkspaceNominalProof/ReplayAudit.hs"- ]- map fst (wpModules plan) `shouldSatisfy` all (not . isSuffixOfPath "/Holes.hs")- forM_ compiledPaths $ \path ->- case [m | (m, _) <- wpModules plan, modulePath m == path] of- [generated] -> do- committed <- readTestText ("test/conformance-workspace-nominals/" <> path)- normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generated)- found -> expectationFailure ("expected one generated module at " <> path <> ", got " <> show (map modulePath found))- it "plans a one-member workspace byte-identically to the single-file path" $ do- let fixtures =- [ "test/fixtures/reservation.keiro",- "test/fixtures/consumer-types.keiro",- "test/fixtures/readmodel.keiro",- "test/fixtures/hospital-surge.keiro"- ]- -- Modules and refusals both: hospital-surge refuses on both- -- paths, which proves the gates agree as well as the emitters.- forM_ fixtures $ \path -> do- spec <- specOf path- let ctx = defaultContext (specContext spec)- workspace = oneMemberWorkspace path spec- fmap (map fst . wpModules) (planWorkspaceScaffold "goldens" ctx workspace)- `shouldBe` planScaffold ctx spec- -- The equality is not vacuous: at least one fixture plans, and- -- its per-node modules are attributed to the single member.- spec <- specOf "test/fixtures/reservation.keiro"- let workspace = oneMemberWorkspace "test/fixtures/reservation.keiro" spec- case planWorkspaceScaffold "goldens" (defaultContext (specContext spec)) workspace of- Left refusals -> expectationFailure ("reservation should plan: " <> show refusals)- Right plan -> do- wpModules plan `shouldSatisfy` (not . null)- map snd (wpModules plan)- `shouldSatisfy` all (`elem` [ContextLevel, MemberOwned "reservation.keiro"])- map snd (wpModules plan)- `shouldSatisfy` elem (MemberOwned "reservation.keiro")- it "computes obligations from the complete merged graph, spanning members" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- case bindingObligations (wsMergedSpec workspace) of- Left graphErrors -> expectationFailure ("merged graph did not resolve: " <> show graphErrors)- Right obligations ->- case [o | o <- obligations, obligationMappedName o == "ProjectSummary", obligationKind o == BindingValue] of- [obligation] -> do- obligationUseSites obligation- `shouldSatisfy` any (T.isInfixOf "Project register summary")- obligationUseSites obligation- `shouldSatisfy` any (T.isInfixOf "ProjectArtifact command RecordArtifact")- found -> expectationFailure ("expected one ProjectSummary binding obligation, got " <> show (length found))- it "refuses a case-folded path collision across members, naming both files" $ do- workspace <- shouldComposeWorkspace canonicalWorkspacePath- let collided = withCaseVariantAggregate workspace- case planWorkspaceScaffold "goldens" (workspaceContext collided) collided of- Right _ -> expectationFailure "expected a cross-member path collision refusal"- Left refusals -> do- let origins = concat [os | PathCollision _ os <- refusals]- origins `shouldSatisfy` any (T.isInfixOf "domain/project.keiro: ")- origins `shouldSatisfy` any (T.isInfixOf "domain/project-artifact.keiro: ")- it "refuses golden fixtures stranded beside a member instead of under the workspace root" $- withTempDirectory "keiro-dsl-workspace-goldens" $ \root -> do- workspace <- writeGoldenWorkspace root- let workspaceGoldens = root </> "golden-payloads"- fixture = "hospital-capacity/Reservation/TransferReservationCreated.v1.json"- beside = root </> "domain/golden-payloads" </> fixture- goldenRootDivergence workspaceGoldens workspace `shouldReturn` []- createDirectoryIfMissing True (takeDirectory beside)- TIO.writeFile beside "{}\n"- refusals <- goldenRootDivergence workspaceGoldens workspace- refusals `shouldBe` [GoldenRootDivergence workspaceGoldens [beside]]- renderRefusals refusals- `shouldSatisfy` any (T.isInfixOf "one golden root per workspace")- -- The same fixture under the workspace root is no divergence.- let atRoot = workspaceGoldens </> fixture- createDirectoryIfMissing True (takeDirectory atRoot)- TIO.writeFile atRoot "{}\n"- goldenRootDivergence workspaceGoldens workspace `shouldReturn` []-- describe "workspace scaffold" $ do- it "writes workspace-keyed history and no context-keyed file at all" $- withWorkspaceFixture "keiro-dsl-workspace-history" id $ \_ out workspace -> do- report <- executePlannedWorkspaceScaffold out workspace- wsrRecordPath report- `shouldBe` out </> "keiro-dsl-scaffold-record.workspace.demo-project.txt"- wsrBuildManifestPath report- `shouldBe` out </> "keiro-dsl-manifest.workspace.demo-project.txt"- doesFileExist (out </> recordFileName "demo-project") `shouldReturn` False- doesFileExist (out </> "keiro-dsl-manifest.demo-project.txt") `shouldReturn` False- contents <- TIO.readFile (wsrRecordPath report)- buildManifest <- TIO.readFile (wsrBuildManifestPath report)- assertGeneratedHaskellContract "service.keiro-workspace" buildManifest- case parseWorkspaceRecord contents of- Nothing -> expectationFailure ("workspace record did not parse:\n" <> T.unpack contents)- Just record -> do- wrService record `shouldBe` "demo-project"- wrManifest record `shouldBe` "service.keiro-workspace"- wrMembers record- `shouldBe` [ "domain/project-artifact.keiro",- "domain/project.keiro",- "domain/shared.keiro"- ]- -- Context-level modules are ownerless; everything- -- else names the member that produced it.- [wrmPath row | row <- wrModules record, wrmOwner row == Nothing]- `shouldSatisfy` \ownerless ->- length ownerless == 4- && any (T.isSuffixOf "StructuralProjections.hs" . T.pack) ownerless- && any (T.isSuffixOf "Nominals.hs" . T.pack) ownerless- && any (T.isSuffixOf "Nominals/Internal.hs" . T.pack) ownerless- && any (T.isSuffixOf "ReplayAudit.hs" . T.pack) ownerless- [ wrmOwner row- | row <- wrModules record,- "Project/Generated/Domain.hs" `T.isSuffixOf` T.pack (wrmPath row)- ]- `shouldBe` [Just "domain/project.keiro"]- it "is idempotent: an unchanged second run rewrites nothing and reports nothing" $- withWorkspaceFixture "keiro-dsl-workspace-idempotent" id $ \_ out workspace -> do- first <- executePlannedWorkspaceScaffold out workspace- before <- treeSnapshot out- second <- executePlannedWorkspaceScaffold out workspace- after <- treeSnapshot out- after `shouldBe` before- map thd3 (wsrDispositions second)- `shouldSatisfy` all (`elem` [Unchanged, Skipped])- wsrStale second `shouldBe` []- wsrOwnershipMoves second `shouldBe` []- wsrMappingDrift second `shouldBe` []- wsrNewHoles second `shouldBe` []- -- The first run had to write; the claim is not vacuous.- map thd3 (wsrDispositions first) `shouldSatisfy` any (== Overwritten)- renderWorkspaceScaffoldReport second- `shouldSatisfy` all (not . T.isPrefixOf "stale:")- it "produces byte-identical output for members listed in reverse order" $- withWorkspaceFixture "keiro-dsl-workspace-order-a" id $ \_ outA workspaceA ->- withWorkspaceFixture "keiro-dsl-workspace-order-b" reverse $ \_ outB workspaceB -> do- _ <- executePlannedWorkspaceScaffold outA workspaceA- _ <- executePlannedWorkspaceScaffold outB workspaceB- treeB <- treeSnapshot outB- treeA <- treeSnapshot outA- treeB `shouldBe` treeA- map fst treeA `shouldSatisfy` elem "keiro-dsl-scaffold-record.workspace.demo-project.txt"- it "reports stale files only for the member that changed" $- withWorkspaceFixture "keiro-dsl-workspace-stale" id $ \root out workspace -> do- first <- executePlannedWorkspaceScaffold out workspace- let siblingPaths =- [ modulePath m- | (m, provenance, _) <- wsrDispositions first,- provenance == MemberOwned "domain/project-artifact.keiro"- ]- siblingsBefore <- traverse (TIO.readFile . (out </>)) siblingPaths- renamed <- renameMemberAggregate root "domain/project.keiro" "Project" "Ledger"- second <- executePlannedWorkspaceScaffold out renamed- let stalePaths = map stalePath (wsrStale second)- stalePaths `shouldSatisfy` (not . null)- stalePaths `shouldSatisfy` all (T.isInfixOf "/Project/" . T.pack)- -- Nothing the sibling member owns is stale, and nothing it- -- owns changed on disk: no cross-member false positives.- stalePaths `shouldSatisfy` all (`notElem` siblingPaths)- siblingsAfter <- traverse (TIO.readFile . (out </>)) siblingPaths- siblingsAfter `shouldBe` siblingsBefore- forM_ stalePaths $ \path -> doesFileExist (out </> path) `shouldReturn` True- wsrStale second- `shouldSatisfy` all- ( \stale -> case staleKind stale of- Generated -> staleGeneratedEvidence stale == Just ExactGeneratedBannerPresent- HoleStub -> staleGeneratedEvidence stale == Nothing- )- renderWorkspaceScaffoldReport second- `shouldSatisfy` any (T.isInfixOf "keiro-dsl never deletes files.")- renderWorkspaceScaffoldReport second- `shouldSatisfy` any (T.isInfixOf "exact generated banner present; verify unchanged bytes before deleting")- renderWorkspaceScaffoldReport second- `shouldSatisfy` all (not . T.isInfixOf "safe to delete")- it "reports an aggregate moved between members as an ownership move, not stale churn" $- withWorkspaceFixture "keiro-dsl-workspace-move" id $ \root out workspace -> do- _ <- executePlannedWorkspaceScaffold out workspace- before <- treeSnapshot out- moved <- moveArtifactAggregate root- second <- executePlannedWorkspaceScaffold out moved- wsrStale second `shouldBe` []- let moves = wsrOwnershipMoves second- moves `shouldSatisfy` (not . null)- moves- `shouldSatisfy` all- ( \move ->- omPrevious move == Just "domain/project-artifact.keiro"- && omCurrent move == Just "domain/project.keiro"- )- map omPath moves- `shouldSatisfy` any (T.isInfixOf "ProjectArtifact" . T.pack)- -- Stable behavior contracts retain source-line attribution, so- -- moving a declaration may rewrite only those attributed modules.- -- Every unaffected module and the build manifest remain untouched.- map thd3 (wsrDispositions second)- `shouldSatisfy` all (`elem` [Unchanged, Skipped, Overwritten])- let overwrittenPaths =- [modulePath generatedModule | (generatedModule, _, Overwritten) <- wsrDispositions second]- length overwrittenPaths `shouldBe` 2- overwrittenPaths `shouldSatisfy` all (T.isSuffixOf "/Generated/BehaviorContract.hs" . T.pack)- after <- treeSnapshot out- map fst after `shouldBe` map fst before- let unaffected (path, _) =- not ("scaffold-record" `T.isInfixOf` T.pack path)- && path `notElem` overwrittenPaths- filter unaffected after `shouldBe` filter unaffected before- renderWorkspaceScaffoldReport second- `shouldSatisfy` any (T.isInfixOf "changed owning member")- it "leaves the tree, record, and manifest untouched when any member refuses" $- withWorkspaceFixture "keiro-dsl-workspace-atomic" id $ \_ out workspace -> do- _ <- executePlannedWorkspaceScaffold out workspace- before <- treeSnapshot out- let broken = withCaseVariantAggregate workspace- case planWorkspaceScaffold "goldens" (workspaceContext broken) broken of- Right _ -> expectationFailure "expected the broken workspace to refuse"- Left refusals -> refusals `shouldSatisfy` any isPathCollision- treeSnapshot out `shouldReturn` before- -- A fresh output directory is never even created.- withTempDirectory "keiro-dsl-workspace-atomic-fresh" $ \fresh -> do- let target = fresh </> "out"- case planWorkspaceScaffold "goldens" (workspaceContext broken) broken of- Right _ -> expectationFailure "expected the broken workspace to refuse"- Left _ -> doesDirectoryExist target `shouldReturn` False- it "leaves prior workspace output byte-identical for parse, validation, and collision failures" $- withWorkspaceFixture "keiro-dsl-workspace-atomic-cli" id $ \root out workspace -> do- _ <- executePlannedWorkspaceScaffold out workspace- before <- treeSnapshot out- let member = root </> "domain/project-artifact.keiro"- manifest = root </> "service.keiro-workspace"- original <- TIO.readFile member- let failures =- [ ("parse", "context demo-project\naggregate !!!\n"),- ("validation", T.replace "ProjectId" "MissingProjectId" original),- ("collision", T.replace "aggregate ProjectArtifact" "aggregate PROJECT" original)- ]- forM_ failures $ \(failureKind, brokenSource) -> do- TIO.writeFile member brokenSource- (exitCode, stdoutText, stderrText) <-- runKeiroDsl ["scaffold", manifest, "--out", out]- unless (exitCode == ExitFailure 1) $- expectationFailure- (failureKind <> " failure unexpectedly scaffolded:\n" <> stdoutText <> stderrText)- treeSnapshot out `shouldReturn` before- TIO.writeFile member original- it "refuses the whole workspace for one bannerless Generated target, changing nothing" $- withWorkspaceFixture "keiro-dsl-workspace-banner" id $ \_ out workspace -> do- plan <- shouldPlanWorkspaceSpec workspace- let generated = [m | (m, _) <- wpModules plan, kind m == Generated]- case generated of- [] -> expectationFailure "workspace fixture has no Generated module"- target : _ -> do- let path = out </> modulePath target- createDirectoryIfMissing True (takeDirectory path)- TIO.writeFile path "hand owned\n"- before <- treeSnapshot out- refused <- executeWorkspaceScaffold out False plan- refused `shouldSatisfy` isMissingBannerRefusal- treeSnapshot out `shouldReturn` before- forced <- executeWorkspaceScaffold out True plan- forced `shouldSatisfy` isSuccessfulScaffold- TIO.readFile path `shouldReturn` moduleText target- it "scaffolds a whole workspace through the CLI" $- withTempDirectory "keiro-dsl-workspace-cli" $ \out -> do- (exitCode, stdoutText, stderrText) <-- runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out]- unless (exitCode == ExitSuccess) (expectationFailure (stdoutText <> stderrText))- stderrText `shouldContain` "workspace: demo-project"- doesFileExist (out </> "keiro-dsl-scaffold-record.workspace.demo-project.txt")- `shouldReturn` True- tree <- treeSnapshot out- length [path | (path, _) <- tree, "StructuralProjections.hs" `T.isSuffixOf` T.pack path]- `shouldBe` 1- length [path | (path, _) <- tree, "ReplayAudit.hs" `T.isSuffixOf` T.pack path]- `shouldBe` 1- (secondCode, _, secondErr) <-- runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out]- secondCode `shouldBe` ExitSuccess- secondErr `shouldSatisfy` (not . isInfixOfString "(overwritten)")- treeSnapshot out `shouldReturn` tree- it "accepts a validated runtime-package override and generates exactly one service package" $- withTempDirectory "keiro-dsl-workspace-runtime-package-cli" $ \out -> do- (exitCode, stdoutText, stderrText) <-- runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out, "--runtime-package", "demo-runtime"]- unless (exitCode == ExitSuccess) (expectationFailure (stdoutText <> stderrText))- tree <- treeSnapshot out- length [path | (path, _) <- tree, takeExtension path == ".cabal", "keiro-dsl-conformance.workspace.demo-project" `isInfixOfString` path]- `shouldBe` 1- stderrText `shouldSatisfy` isInfixOfString "conformance-target: cabal test keiro-demo-project-conformance"-- describe "workspace adoption" $ do- it "replaces embedded 0.6 nominal declarations only in generated files" $- withWorkspaceFixture "keiro-dsl-workspace-nominal-adopt" id $ \_ out workspace -> do- plan <- shouldPlanWorkspaceSpec workspace- let pathEndingIn suffix selectedKind =- case [modulePath m | (m, _) <- wpModules plan, kind m == selectedKind, suffix `isSuffixOfPath` m] of- [path] -> pure path- found -> expectationFailure ("expected one path ending in " <> suffix <> ", got " <> show found) >> fail "unreachable"- domainPath <- pathEndingIn "Project/Generated/Domain.hs" Generated- nominalPath <- pathEndingIn "Generated/Nominals.hs" Generated- internalNominalPath <- pathEndingIn "Generated/Nominals/Internal.hs" Generated- holePath <- pathEndingIn "Project/Holes.hs" HoleStub- writeFileWithParents- (out </> domainPath)- (generatedBanner <> "\n-- legacy 0.6 fixture\nmodule LegacyDomain where\nnewtype ProjectId = ProjectId String\ndata ProjectPhase = Draft | Active\n")- writeFileWithParents (out </> holePath) "-- hand-owned 0.6 implementation\n"-- report <- executePlannedWorkspaceScaffold out workspace- wsrStale report `shouldBe` []- [disposition | (m, _, disposition) <- wsrDispositions report, modulePath m == domainPath]- `shouldBe` [Overwritten]- [disposition | (m, _, disposition) <- wsrDispositions report, modulePath m == nominalPath]- `shouldBe` [Overwritten]- newDomain <- TIO.readFile (out </> domainPath)- newDomain `shouldSatisfy` (not . T.isInfixOf "newtype ProjectId")- newDomain `shouldSatisfy` T.isInfixOf "Generated.Nominals (ProjectId, parseProjectId, ProjectPhase (..))"- newNominals <- TIO.readFile (out </> nominalPath)- T.count "newtype ProjectId" newNominals `shouldBe` 0- T.count "data ProjectPhase =" newNominals `shouldBe` 1- newInternalNominals <- TIO.readFile (out </> internalNominalPath)- T.count "newtype ProjectId" newInternalNominals `shouldBe` 1- TIO.readFile (out </> holePath) `shouldReturn` "-- hand-owned 0.6 implementation\n"- it "adopts an overwritten same-context record pair by record and by banner" $- withInlineWorkspace "keiro-dsl-workspace-adopt" adoptionMembers $ \_ out workspace -> do- -- Reproduce today's defect first: two same-context specs- -- scaffolded independently into one directory, the second- -- replacing the first's record and calling its files stale.- specA <- parseInlineSpec "domain/a.keiro" adoptionMemberA- specB <- parseInlineSpec "domain/b.keiro" adoptionMemberB- let ctx = defaultContext "adoption-demo"- legacyA <- executePlannedScaffold out "domain/a.keiro" ctx specA- legacyB <- executePlannedScaffold out "domain/b.keiro" ctx specB- reportStale legacyB `shouldSatisfy` (not . null)- legacyBefore <- TIO.readFile (out </> recordFileName "adoption-demo")-- report <- executePlannedWorkspaceScaffold out workspace- wsrStale report `shouldBe` []- case wsrMigration report of- Nothing -> expectationFailure "expected the first workspace run to adopt"- Just migration -> do- let generatedOf run = sort [modulePath m | (m, _) <- reportDispositions run, kind m == Generated]- claimedBy evidence = sort [cfPath entry | entry <- mrClaimed migration, cfEvidence entry == evidence]- -- The surviving record attributes B's files; A's- -- files survived only as banners, which is exactly- -- the orphan case the overwrite created.- claimedBy ClaimedFromRecord `shouldBe` generatedOf legacyB- claimedBy ClaimedFromBanner `shouldBe` sort (generatedOf legacyA \\ generatedOf legacyB)- claimedBy ClaimedFromBanner `shouldSatisfy` (not . null)- mrLikelyStale migration `shouldBe` []- mrLegacyRecord migration- `shouldBe` Just (recordFileName "adoption-demo", "domain/b.keiro")- -- Provenance is persisted, not merely printed.- recorded <- parseWorkspaceRecord <$> TIO.readFile (wsrRecordPath report)- fmap (sort . map adPath . wrAdopted) recorded- `shouldBe` Just (sort (map cfPath (mrClaimed migration)))- fmap (sort . nubOrd . map adEvidence . wrAdopted) recorded- `shouldBe` Just ["banner", "record"]- persisted <- TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")- persisted `shouldBe` T.unlines (renderMigrationReport migration)- renderWorkspaceScaffoldReport report- `shouldSatisfy` any (T.isInfixOf "adopting pre-workspace scaffold output")-- -- The legacy record gained one line and nothing else: it- -- still parses to the same value for an old binary.- legacyAfter <- TIO.readFile (out </> recordFileName "adoption-demo")- T.lines legacyAfter `shouldSatisfy` elem (supersededByLine "adoption-demo")- parseRecord legacyAfter `shouldBe` parseRecord legacyBefore- T.lines legacyAfter- `shouldBe` T.lines legacyBefore <> [supersededByLine "adoption-demo"]-- -- Adoption is not a content change: the generated tree is- -- what a fresh workspace scaffold of the same members emits.- withInlineWorkspace "keiro-dsl-workspace-adopt-fresh" adoptionMembers $ \_ fresh freshWorkspace -> do- freshReport <- executePlannedWorkspaceScaffold fresh freshWorkspace- wsrMigration freshReport `shouldBe` Nothing- adoptedTree <- treeSnapshot out- freshTree <- treeSnapshot fresh- haskellOnly adoptedTree `shouldBe` haskellOnly freshTree- it "lists hand-written files as unclaimed and leaves their bytes alone" $- withInlineWorkspace "keiro-dsl-workspace-unclaimed" adoptionMembers $ \_ out workspace -> do- plan <- shouldPlanWorkspaceSpec workspace- case [modulePath m | (m, _) <- wpModules plan, kind m == HoleStub] of- [] -> expectationFailure "adoption fixture emits no hole module"- holePath : _ -> do- writeFileWithParents (out </> holePath) "-- hand filled\n"- writeFileWithParents (out </> "Notes.hs") "module Notes where\n"- report <- executePlannedWorkspaceScaffold out workspace- case wsrMigration report of- Nothing -> expectationFailure "expected a report for a directory holding hand-written files"- Just migration -> do- mrLegacyRecord migration `shouldBe` Nothing- mrClaimed migration `shouldBe` []- mrUnclaimed migration `shouldBe` sort [holePath, "Notes.hs"]- TIO.readFile (out </> holePath) `shouldReturn` "-- hand filled\n"- TIO.readFile (out </> "Notes.hs") `shouldReturn` "module Notes where\n"- it "never claims a bannerless file at a planned Generated path" $- withInlineWorkspace "keiro-dsl-workspace-unattributable" adoptionMembers $ \_ out workspace -> do- plan <- shouldPlanWorkspaceSpec workspace- case [modulePath m | (m, _) <- wpModules plan, kind m == Generated] of- [] -> expectationFailure "adoption fixture emits no Generated module"- target : _ -> do- writeFileWithParents (out </> target) "hand owned\n"- refused <- executeWorkspaceScaffold out False plan- refused `shouldSatisfy` isMissingBannerRefusal- TIO.readFile (out </> target) `shouldReturn` "hand owned\n"- doesFileExist (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")- `shouldReturn` False- it "adopts at most once, and the second run is an ordinary idempotent run" $- withInlineWorkspace "keiro-dsl-workspace-adopt-once" adoptionMembers $ \_ out workspace -> do- specA <- parseInlineSpec "domain/a.keiro" adoptionMemberA- _ <- executePlannedScaffold out "domain/a.keiro" (defaultContext "adoption-demo") specA- first <- executePlannedWorkspaceScaffold out workspace- wsrMigration first `shouldSatisfy` \case Just _ -> True; Nothing -> False- before <- treeSnapshot out- reportBefore <- TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")- legacyBefore <- TIO.readFile (out </> recordFileName "adoption-demo")-- second <- executePlannedWorkspaceScaffold out workspace- wsrMigration second `shouldBe` Nothing- wsrStale second `shouldBe` []- map thd3 (wsrDispositions second) `shouldSatisfy` all (`elem` [Unchanged, Skipped])- treeSnapshot out `shouldReturn` before- TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")- `shouldReturn` reportBefore- legacyAfter <- TIO.readFile (out </> recordFileName "adoption-demo")- legacyAfter `shouldBe` legacyBefore- length (filter (== supersededByLine "adoption-demo") (T.lines legacyAfter))- `shouldBe` 1--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- [] -> ""--generatedExtensionsEndingIn :: T.Text -> [ScaffoldModule] -> [T.Text]-generatedExtensionsEndingIn suffix modules = case [generatedModule | generatedModule <- modules, kind generatedModule == Generated, suffix `T.isSuffixOf` T.pack (modulePath generatedModule)] of- [generatedModule] -> generatedLocalExtensions generatedModule- matches -> error ("expected one generated module ending in " <> T.unpack suffix <> ", got " <> show (map modulePath matches))--generatedLocalExtensions :: ScaffoldModule -> [T.Text]-generatedLocalExtensions generatedModule =- [ extension- | line <- takeWhile (T.isPrefixOf languagePrefix) (T.lines (moduleText generatedModule)),- Just extensionWithSuffix <- [T.stripPrefix languagePrefix line],- Just extension <- [T.stripSuffix languageSuffix extensionWithSuffix]- ]- where- languagePrefix = "{-# LANGUAGE "- languageSuffix = " #-}"--holeTextEndingIn :: T.Text -> [ScaffoldModule] -> T.Text-holeTextEndingIn suffix modules = case [moduleText m | m <- modules, kind m == HoleStub, suffix `T.isSuffixOf` T.pack (modulePath m), not ("BehaviorHoles.hs" `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"- ]--scalarRegisterCases :: [(T.Text, T.Text)]-scalarRegisterCases =- [ ("Text", "\"sample\""),- ("Int", "0"),- ("Bool", "False"),- ("Time", "\"2026-01-02T03:04:05.123456789012Z\""),- ("Natural", "0")- ]--cleanScalarAggregateSpec :: T.Text -> T.Text -> T.Text-cleanScalarAggregateSpec typeName initialValue =- T.unlines- [ "context clean-scalar",- "",- "aggregate Scalar",- " regs",- " value " <> typeName <> " = " <> initialValue,- " states Empty Done!",- " command Set { value:" <> typeName <> " }",- " event SetDone { value:" <> typeName <> " }",- " Empty -- Set --> write value := value ; emit SetDone ; 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 == TRef "CounterVertex" then TRef "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- service <- checkedServiceOf path- pure (validateService service)---- | 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",- tImplementation = LegacyHoleImplementation,- 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 <- parsedSourceOf oldP- new <- parsedSourceOf newP- pure (diffSources old new)--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,- RolloutProducerFirst- ]--replayImpactFixtures :: FilePath -> FilePath -> IO ReplayImpact-replayImpactFixtures oldPath newPath = do- old <- checkedServiceOf oldPath- new <- checkedServiceOf newPath- pure (resolvedFold (ReplayImpact.replayImpactServices 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- ]- }--mapContract :: (ContractNode -> ContractNode) -> Spec -> Spec-mapContract update spec =- spec- { specNodes =- [ case node of- NContract contract -> NContract (update contract)- _ -> node- | node <- specNodes spec- ]- }--mapIntake :: (IntakeNode -> IntakeNode) -> Spec -> Spec-mapIntake update spec =- spec- { specNodes =- [ case node of- NIntake intake -> NIntake (update intake)- _ -> node- | node <- specNodes spec- ]- }--mapEmit :: (EmitNode -> EmitNode) -> Spec -> Spec-mapEmit update spec =- spec- { specNodes =- [ case node of- NEmit emitNode -> NEmit (update emitNode)- _ -> node- | node <- specNodes spec- ]- }--mapWorkflow :: (WorkflowNode -> WorkflowNode) -> Spec -> Spec-mapWorkflow update spec =- spec- { specNodes =- [ case node of- NWorkflow workflow -> NWorkflow (update workflow)- _ -> node- | node <- specNodes spec- ]- }--mapWorkqueue :: (WorkqueueNode -> WorkqueueNode) -> Spec -> Spec-mapWorkqueue update spec =- spec- { specNodes =- [ case node of- NWorkqueue queue -> NWorkqueue (update queue)- _ -> node- | node <- specNodes spec- ]- }--mapDispatch :: (PgmqDispatchNode -> PgmqDispatchNode) -> Spec -> Spec-mapDispatch update spec =- spec- { specNodes =- [ case node of- NPgmqDispatch dispatch -> NPgmqDispatch (update dispatch)- _ -> node- | node <- specNodes spec- ]- }--mapOperation :: (OperationNode -> OperationNode) -> Spec -> Spec-mapOperation update spec =- spec- { specNodes =- [ case node of- NOperation operation -> NOperation (update operation)- _ -> node- | node <- specNodes spec- ]- }--mapPublisher :: (PublisherNode -> PublisherNode) -> Spec -> Spec-mapPublisher update spec =- spec- { specNodes =- [ case node of- NPublisher publisher -> NPublisher (update publisher)- _ -> node- | node <- specNodes spec- ]- }--serviceErrorCodes :: Int -> Spec -> [DiagnosticCode]-serviceErrorCodes versionNumber spec =- [code diagnostic | diagnostic <- validateService service, severity diagnostic == Error]- where- service = case languageVersion (fromIntegral versionNumber) >>= effectiveLanguageContractForVersion of- Nothing -> error ("unsupported test language version " <> show versionNumber)- Just languageContract -> CheckedService languageContract spec--duplicateFirst :: [a] -> [a]-duplicateFirst = \case- [] -> []- first : rest -> first : first : rest--updateFirst :: (a -> a) -> [a] -> [a]-updateFirst update = \case- [] -> []- first : rest -> update first : rest--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]--diagnosticCodes :: Spec -> [DiagnosticCode]-diagnosticCodes = map code . validateSpec--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])--assertSkeletonUsesStableLanguage :: T.Text -> IO ()-assertSkeletonUsesStableLanguage kind = case skeletonFor kind of- Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))- Right source -> case parseSource ("new:" <> T.unpack kind) source of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure))- Right parsed -> do- let service = checkedSource parsed- effectiveContractLanguageVersion (checkedLanguageContract service) `shouldBe` currentStableLanguageVersion- effectiveLanguageSupport (checkedLanguageContract service) `shouldBe` Stable- [code diagnostic | diagnostic <- validateService service, severity diagnostic == Error]- `shouldBe` ([] :: [DiagnosticCode])- scaffoldServiceModules (defaultContext (specContext (checkedSpec service))) service- `shouldSatisfy` (not . null)--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")- ]--assertStableSkeletonMatchesCommitted :: T.Text -> T.Text -> IO ()-assertStableSkeletonMatchesCommitted kind root = case skeletonFor kind of- Left err -> expectationFailure (T.unpack err)- Right source -> case parseSource ("new:" <> T.unpack kind) source of- Left err -> expectationFailure (T.unpack (renderParseFailure err))- Right parsed -> do- let service = checkedSource parsed- spec = checkedSpec service- let ctx = (defaultContext (specContext spec)) {moduleRoot = root}- forM_ [m | m <- scaffoldServiceModules ctx service, 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--isFoldSurfaceRefusal :: Either [Refusal] modules -> Bool-isFoldSurfaceRefusal (Left refusals) = any isFold refusals- where- isFold FoldSurfaceRefusal {} = True- isFold _ = False-isFoldSurfaceRefusal (Right _) = False---- | The canonical positive workspace fixture: three members under one context.-canonicalWorkspacePath :: FilePath-canonicalWorkspacePath = "test/fixtures/workspace/service.keiro-workspace"---- | Deterministic workspace source used to model git blobs without invoking git.-memoryContentSource :: Map.Map FilePath T.Text -> ContentSource-memoryContentSource files =- ContentSource- { csRead = \path ->- pure $ maybe (Left ("missing in-memory content: " <> T.pack path)) Right (Map.lookup path files)- }--changeCode :: Change -> DiagnosticCode-changeCode (Additive kind) = ckCode kind-changeCode (Advisory kind) = ckCode kind-changeCode (Breaking kind) = ckCode kind--breakingSurfaces :: Change -> [CompatibilitySurface]-breakingSurfaces change =- [ surface- | surface <- [minBound .. maxBound],- verdictFor surface (ckVector kind) == VBreaking- ]- where- kind = case change of- Additive value -> value- Advisory value -> value- Breaking value -> value--workspaceChangeKind :: Change -> ChangeKind-workspaceChangeKind (Additive kind) = kind-workspaceChangeKind (Advisory kind) = kind-workspaceChangeKind (Breaking kind) = kind---- | The same members as 'canonicalWorkspacePath', listed in reverse order.-reorderedWorkspacePath :: FilePath-reorderedWorkspacePath = "test/fixtures/workspace/service-reordered.keiro-workspace"---- | Load and compose a workspace fixture, failing the test on a refusal. The--- fixture path is package-relative; the loader is rooted at the manifest's own--- directory, exactly as the CLI roots it.-shouldComposeWorkspace :: FilePath -> IO WorkspaceSpec-shouldComposeWorkspace path = do- resolved <- resolveTestPath path- loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) resolved- case loaded of- Left failure ->- expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure resolved failure)))- >> error "unreachable"- Right workspace -> pure workspace {wsManifestPath = path}---- | The 'Context' a workspace scaffolds under, with no CLI overrides: the--- members' unanimous context name, the manifest's module-root and layout--- authority, and the built-in defaults where the manifest is silent.-workspaceContext :: WorkspaceSpec -> Context-workspaceContext workspace =- Context- { contextName = wsContext workspace,- moduleRoot = maybe "" id (wsModuleRoot workspace),- placement = maybe GeneratedPrefix id (wsLayout workspace)- }---- | Compose and plan a workspace fixture, failing the test on any refusal.-shouldPlanWorkspace :: FilePath -> IO WorkspacePlan-shouldPlanWorkspace path = do- workspace <- shouldComposeWorkspace path- case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of- Left refusals -> expectationFailure ("unexpected workspace plan refusal: " <> show refusals) >> error "unreachable"- Right plan -> pure plan---- | Does a scaffolded module's path end in this suffix?-isSuffixOfPath :: FilePath -> ScaffoldModule -> Bool-isSuffixOfPath suffix m = T.pack suffix `T.isSuffixOf` T.pack (modulePath m)---- | A workspace record built from real composed data plus two synthetic--- adoption rows, so the round-trip test exercises every row kind including the--- JSON encodings shared with the v1 record.-sampleWorkspaceRecord :: WorkspaceSpec -> WorkspaceRecord-sampleWorkspaceRecord workspace =- WorkspaceRecord- { wrService = wsService workspace,- wrManifest = "service.keiro-workspace",- wrContext = wsContext workspace,- wrModuleRoot = maybe "" id (wsModuleRoot workspace),- wrLayout = "collocated",- wrMembers = map wmPath (wsMembers workspace),- wrSourceLanguages =- [ WorkspaceSourceLanguageRow (wmPath member) (wmSourceLanguage member)- | member <- wsMembers workspace- ],- wrLanguageContract = wsLanguageContract workspace,- wrModules =- [ WorkspaceModuleRow Generated "Demo/Generated/StructuralProjections.hs" Nothing,- WorkspaceModuleRow Generated "Demo/Project/Generated/Domain.hs" (Just "domain/project.keiro"),- WorkspaceModuleRow HoleStub "Demo/Project/Holes.hs" (Just "domain/shared.keiro")- ],- wrMappings = consumerMappings (consumerPlan (wsMergedSpec workspace)),- wrIdDomains = [],- wrNominalEqualities = nominalEqualityIdentities (wsMergedSpec workspace),- wrBindingObligations = either (const []) id (bindingHoles (wsMergedSpec workspace)),- wrBehaviorRequirements = [],- wrAdopted =- [ AdoptedRow "claimed/One.hs" "record" (Just "keiro-dsl-scaffold-record.demo-project.txt") (Just "project.keiro"),- AdoptedRow "claimed/Two.hs" "banner" Nothing Nothing- ]- }---- | The canonical workspace with a case-variant copy of one member's aggregate--- grafted onto another member. Composition refuses this shape (EP-153 catches it--- at the earliest boundary), so the planner's own cross-member collision gate can--- only be exercised by constructing the graph directly — which is exactly what--- this does, mirroring the single-file @caseVariant@ construction.-withCaseVariantAggregate :: WorkspaceSpec -> WorkspaceSpec-withCaseVariantAggregate workspace = case [aggregate | NAggregate aggregate <- specNodes merged, aggName aggregate == "Project"] of- [] -> error "canonical workspace fixture has no Project aggregate"- aggregate : _ ->- let shouted = aggregate {aggName = T.toUpper (aggName aggregate)}- ownership = wsOwnership workspace- in workspace- { wsMergedSpec = merged {specNodes = specNodes merged <> [NAggregate shouted]},- wsOwnership =- ownership- { oiNodes =- Map.insert- ("aggregate", aggName shouted)- ("domain/project-artifact.keiro", Loc 1)- (oiNodes ownership)- }- }- where- merged = wsMergedSpec workspace---- | Write a one-member workspace whose member declares an upcaster, so its--- golden payload fixture has a canonical location. Returns the composed--- workspace; the caller decides where the fixture lives.-writeGoldenWorkspace :: FilePath -> IO WorkspaceSpec-writeGoldenWorkspace root = do- source <- readTestText "test/fixtures/reservation-v2.keiro"- createDirectoryIfMissing True (root </> "domain")- TIO.writeFile (root </> "domain/reservation.keiro") source- let manifestPath = root </> "service.keiro-workspace"- TIO.writeFile manifestPath "service gold-demo\nspec domain/reservation.keiro\n"- loaded <- loadWorkspace (fileContentSource root) manifestPath- case loaded of- Left failure ->- expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure manifestPath failure)))- >> error "unreachable"- Right workspace -> pure workspace---- | Materialize the canonical fixture workspace in a fresh temporary directory--- and hand the callback its root, a sibling output directory, and the composed--- workspace. Working on a copy is what lets a test edit a member and re-scaffold.------ The manifest's @spec@ lines are passed through the given function first, so a--- caller can list the same members in a different order; the manifest __file--- name__ stays the same, which is what makes two runs comparable byte for byte.-withWorkspaceFixture ::- String ->- ([FilePath] -> [FilePath]) ->- (FilePath -> FilePath -> WorkspaceSpec -> IO a) ->- IO a-withWorkspaceFixture template orderMembers act =- withTempDirectory template $ \base -> do- let root = base </> "workspace"- out = base </> "out"- members =- [ "domain/project-artifact.keiro",- "domain/project.keiro",- "domain/shared.keiro"- ]- createDirectoryIfMissing True (root </> "domain")- forM_ members $ \relative -> do- source <- readTestText ("test/fixtures/workspace" </> relative)- TIO.writeFile (root </> relative) source- TIO.writeFile- (root </> "service.keiro-workspace")- ( T.unlines- ( ["service demo-project", "module Demo.Modules.Project", "layout collocated"]- <> ["spec " <> T.pack relative | relative <- orderMembers members]- )- )- workspace <- loadTempWorkspace root- act root out workspace---- | Materialize an inline workspace — a manifest plus literal member sources —--- in a fresh temporary directory, and hand the callback its root, a sibling output--- directory, and the composed workspace.-withInlineWorkspace ::- String ->- (T.Text, [(FilePath, T.Text)]) ->- (FilePath -> FilePath -> WorkspaceSpec -> IO a) ->- IO a-withInlineWorkspace template (service, members) act =- withTempDirectory template $ \base -> do- let root = base </> "workspace"- out = base </> "out"- forM_ members $ \(relative, source) -> writeFileWithParents (root </> relative) source- TIO.writeFile- (root </> "service.keiro-workspace")- ( T.unlines- (("service " <> service) : ["spec " <> T.pack relative | (relative, _) <- members])- )- workspace <- loadTempWorkspace root- act root out workspace---- | Two independently valid members under one context. Each is a complete spec--- that the pre-workspace single-file scaffolder accepts, which is what lets a test--- reproduce the overwritten-record defect before adopting.-adoptionMembers :: (T.Text, [(FilePath, T.Text)])-adoptionMembers = ("adoption-demo", [("domain/a.keiro", adoptionMemberA), ("domain/b.keiro", adoptionMemberB)])--adoptionMemberA :: T.Text-adoptionMemberA =- T.unlines- [ "context adoption-demo",- "",- "aggregate Counter",- " regs",- " count Int = 0",- " state CounterVertex = Pending",- " states Pending Done!",- " command Bump { count:Int }",- " event CountBumped { count:Int }",- " Pending -- Bump --> emit CountBumped ; goto Done"- ]--adoptionMemberB :: T.Text-adoptionMemberB =- T.unlines- [ "context adoption-demo",- "",- "aggregate Widget",- " regs",- " size Int = 0",- " state WidgetVertex = Draft",- " states Draft Shipped!",- " command Ship { size:Int }",- " event WidgetShipped { size:Int }",- " Draft -- Ship --> emit WidgetShipped ; goto Shipped"- ]--writeFileWithParents :: FilePath -> T.Text -> IO ()-writeFileWithParents path contents = do- createDirectoryIfMissing True (takeDirectory path)- TIO.writeFile path contents---- | Only the Haskell sources of a tree snapshot, dropping bookkeeping files.-haskellOnly :: [(FilePath, T.Text)] -> [(FilePath, T.Text)]-haskellOnly entries = [entry | entry@(path, _) <- entries, ".hs" `T.isSuffixOf` T.pack path]---- | Compose a workspace that a test just wrote to disk.-loadTempWorkspace :: FilePath -> IO WorkspaceSpec-loadTempWorkspace root = do- let manifestPath = root </> "service.keiro-workspace"- loaded <- loadWorkspace (fileContentSource root) manifestPath- case loaded of- Left failure ->- expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure manifestPath failure)))- >> error "unreachable"- Right workspace -> pure workspace---- | Plan an already-composed workspace, failing the test on a refusal.-shouldPlanWorkspaceSpec :: WorkspaceSpec -> IO WorkspacePlan-shouldPlanWorkspaceSpec workspace =- case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of- Left refusals -> expectationFailure ("unexpected workspace plan refusal: " <> show refusals) >> error "unreachable"- Right plan -> pure plan---- | Plan then execute a whole-workspace scaffold, failing loudly on either.-executePlannedWorkspaceScaffold :: FilePath -> WorkspaceSpec -> IO WorkspaceScaffoldReport-executePlannedWorkspaceScaffold out workspace = do- plan <- shouldPlanWorkspaceSpec workspace- result <- executeWorkspaceScaffold out False plan- case result of- Left refusals -> expectationFailure ("unexpected workspace execution refusal: " <> show refusals) >> error "unreachable"- Right report -> pure report---- | Rename one member's aggregate in place and recompose. Only the--- @aggregate \<Name\>@ header is rewritten, so declarations that merely share the--- prefix (@ProjectId@, @ProjectSummary@) are untouched.-renameMemberAggregate :: FilePath -> FilePath -> T.Text -> T.Text -> IO WorkspaceSpec-renameMemberAggregate root member from to = do- source <- TIO.readFile (root </> member)- TIO.writeFile (root </> member) (T.replace ("aggregate " <> from <> "\n") ("aggregate " <> to <> "\n") source)- loadTempWorkspace root---- | Move the @ProjectArtifact@ aggregate from the artifact member into the--- project member, and recompose.------ It is prepended, so the merged spec's node order — and therefore every emitted--- byte, including the replay-audit assembly's aggregate list — is exactly what it--- was. That isolates the change to ownership, which is the point of the test.-moveArtifactAggregate :: FilePath -> IO WorkspaceSpec-moveArtifactAggregate root = do- artifact <- TIO.readFile (root </> "domain/project-artifact.keiro")- project <- TIO.readFile (root </> "domain/project.keiro")- case T.breakOn "aggregate ProjectArtifact" artifact of- (kept, moved) | not (T.null moved) -> do- TIO.writeFile (root </> "domain/project-artifact.keiro") kept- TIO.writeFile- (root </> "domain/project.keiro")- (T.replace "aggregate Project\n" (moved <> "\naggregate Project\n") project)- loadTempWorkspace root- _ -> expectationFailure "artifact member has no ProjectArtifact aggregate" >> error "unreachable"---- | Every regular file under a directory, as @(relative path, contents)@ sorted--- by path — the comparison unit for "byte-identical output".-treeSnapshot :: FilePath -> IO [(FilePath, T.Text)]-treeSnapshot root = do- exists <- doesDirectoryExist root- if not exists then pure [] else sort <$> walk ""- where- walk relative = do- entries <- listDirectory (root </> relative)- fmap concat . forM (sort entries) $ \entry -> do- let child = if null relative then entry else relative </> entry- isDirectory <- doesDirectoryExist (root </> child)- if isDirectory- then walk child- else do- contents <- TIO.readFile (root </> child)- pure [(child, contents)]--copyTextTree :: FilePath -> FilePath -> IO ()-copyTextTree source destination =- treeSnapshot source >>= mapM_ (\(relative, contents) -> writeFileWithParents (destination </> relative) contents)--thd3 :: (a, b, c) -> c-thd3 (_, _, value) = value--isPathCollision :: Refusal -> Bool-isPathCollision PathCollision {} = True-isPathCollision _ = False--isInfixOfString :: String -> String -> Bool-isInfixOfString needle haystack = T.isInfixOf (T.pack needle) (T.pack haystack)---- | Load a workspace fixture expecting a compose refusal, and return it.-shouldRefuseWorkspace :: FilePath -> IO (NonEmpty WorkspaceDiagnostic)-shouldRefuseWorkspace path = do- resolved <- resolveTestPath path- loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) resolved- case loaded of- Left (WorkspaceRefused diagnostics) -> pure diagnostics- Left other ->- expectationFailure- ("expected compose refusals, got:\n" <> T.unpack (T.intercalate "\n" (renderWorkspaceFailure resolved other)))- >> error "unreachable"- Right _ -> expectationFailure ("expected " <> path <> " to be refused") >> error "unreachable"---- | Invoke the built @keiro-dsl@ executable. Fixture paths are resolved first,--- so the test works whether it runs from the package directory or the repository--- root.-runKeiroDsl :: [String] -> IO (ExitCode, String, String)-runKeiroDsl arguments = do- resolved <- traverse resolveArgument arguments- readProcessWithExitCode "cabal" (["run", "-v0", "keiro-dsl", "--"] <> resolved) ""- where- resolveArgument argument- | "test/fixtures/" `isPrefixOfString` argument = resolveTestPath argument- | otherwise = pure argument- isPrefixOfString prefix value = take (length prefix) value == prefix---- | The @spec@ field of a coverage report, i.e. what the report says it covers.-coverageSpecPath :: Value -> Maybe T.Text-coverageSpecPath value = case value of- Aeson.Object fields -> case KeyMap.lookup "spec" fields of- Just (Aeson.String path) -> Just path- _ -> Nothing- _ -> Nothing---- | Order-preserving deduplication for comparing cited file sets.-nubOrd :: (Eq a) => [a] -> [a]-nubOrd = go []- where- go seen [] = reverse seen- go seen (x : xs) = if x `elem` seen then go seen xs else go (x : seen) xs---- | Parse a workspace manifest, failing the test on a refusal.-shouldParseManifest :: FilePath -> T.Text -> IO WorkspaceManifest-shouldParseManifest path source = case parseWorkspaceManifest path source of- Left err -> expectationFailure (T.unpack err) >> error "unreachable"- Right manifest -> pure manifest---- | Generate a canonical workspace manifest. Members are drawn from a pool of--- paths that are distinct even under case folding and are held sorted, which is--- the invariant every parsed manifest satisfies.-genWorkspaceManifest :: Gen WorkspaceManifest-genWorkspaceManifest = do- service <- elements ["demo-project", "mori", "kotei", "a1", "svc-2"]- runtimePackage <- elements [Nothing, Just (RuntimePackageName "demo-core"), Just (RuntimePackageName "mori2")]- moduleRoot <- elements [Nothing, Just "Demo", Just "Demo.Modules.Project"]- layout <- elements [Nothing, Just GeneratedPrefix, Just CollocatedLeaf]- chosen <-- sublistOf- [ "a.keiro",- "d-e_f.keiro",- "domain/b.keiro",- "domain/sub/c.keiro",- "x1.keiro"- ]- `suchThat` (not . null)- pure- WorkspaceManifest- { wmfService = service,- wmfServiceLoc = Loc 1,- wmfRuntimePackage = runtimePackage,- wmfRuntimePackageLoc = Loc 2,- wmfModuleRoot = moduleRoot,- wmfModuleRootLoc = Loc 2,- wmfLayout = layout,- wmfLayoutLoc = Loc 3,- wmfMembers = NE.fromList [WorkspaceMemberRef path (Loc 4) | path <- sort chosen]- }---- | Parse a fixture while retaining its released language contract.-parsedSourceOf :: FilePath -> IO ParsedSource-parsedSourceOf path = do- input <- readTestText path- case parseSource path input of- Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"- Right parsed -> pure parsed--checkedServiceOf :: FilePath -> IO CheckedService-checkedServiceOf = fmap checkedSource . parsedSourceOf--renderFoldBaseline :: T.Text -> CheckedService -> T.Text-renderFoldBaseline fixture service =- T.intercalate- "\n\n"- [ T.unlines- ( [ "fixture=" <> fixture,- "aggregate=" <> aggName aggregate,- "fingerprint=" <> aggregateFoldFingerprintForService service aggregate,- "surface-begin"- ]- <> T.lines (aggregateFoldSurfaceForService service aggregate)- <> ["surface-end"]- )- | NAggregate aggregate <- specNodes (checkedSpec service)- ]---- | Parse a fixture through the source-aware boundary and return its graph.-specOf :: FilePath -> IO Spec-specOf = fmap checkedSpec . checkedServiceOf---- | Parse a fixture and scaffold its checked semantic service.-scaffoldFixture :: FilePath -> IO [ScaffoldModule]-scaffoldFixture path = do- service <- checkedServiceOf path- pure (scaffoldServiceModules (defaultContext (specContext (checkedSpec service))) service)--legacyScaffoldProcessFixture :: FilePath -> IO [ScaffoldModule]-legacyScaffoldProcessFixture path = do- spec <- specOf path- pure $ concat [scaffoldProcess (ctx spec) process | NProcess process <- 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 (orderInsensitiveLines, body) = partition isOrderInsensitive (T.lines text)- in (normalizeBody body, sort (map normalizeImport orderInsensitiveLines))- where- -- Compare the deterministic body exactly as before and imports/language- -- pragmas as a sorted, whitespace-normalized list. Sorting tolerates- -- formatter reordering while additions, removals, and renamed entries still- -- fail the pin.- isOrderInsensitive line = isImport line || "{-# LANGUAGE " `T.isPrefixOf` line- normalizeBody =- -- Fourmolu parenthesizes a single class constraint while the emitter's- -- compact spelling remains valid Haskell. Treat that formatter-only- -- rewrite like the whitespace and comma placement normalized below.- T.replace "(Show value) =>" "Show value =>"- . T.replace "( " "("- . T.replace " )" ")"- . T.replace " , )" " )"- . T.unwords- . T.words- . T.replace "}" " } "- . T.replace "{" " { "- . T.replace "]" " ] "- . T.replace "[" " [ "- . T.replace "," " , "- . T.unlines- . map normalizeBanner- normalizeBanner line- | isGeneratedBannerLine line = generatedBanner- | otherwise = line- 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.replace " (" "(" . 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--parseStableRenderedSpec :: FilePath -> Spec -> Either T.Text Spec-parseStableRenderedSpec sourceName spec =- case parseSource sourceName stableSource of- Left failure -> Left (renderParseFailure failure)- Right parsed -> Right (parsedSpec parsed)- where- stableSource =- "language keiro-dsl "- <> T.pack (show (languageVersionNumber currentStableLanguageVersion))- <> "\n"- <> renderSpec spec--shouldParseStableRenderedSpec :: FilePath -> Spec -> IO Spec-shouldParseStableRenderedSpec sourceName spec =- case parseStableRenderedSpec sourceName spec of- Left failure -> expectationFailure (T.unpack failure) >> fail "unreachable"- Right reparsed -> pure reparsed--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 ((/= TRef "VendorGeometry") . regType) (aggRegs aggregate),- aggCommands = map stripCommand (aggCommands aggregate),- aggEvents = map stripEvent (aggEvents aggregate)- }- stripNode node = node- stripCommand command = command {cmdFields = filter ((/= Just (TRef "VendorGeometry")) . aggregateFieldType) (cmdFields command)}- stripEvent event = event {evBody = case evBody event of EventFields fields -> EventFields (filter ((/= Just (TRef "VendorGeometry")) . aggregateFieldType) 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"],- onInteger = ["integer"],- 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]--genAggregateField :: Gen AggregateField-genAggregateField = AggregateField <$> genName <*> genMaybe (genTypeExpr []) <*> pure noLoc--genReg :: Gen RegDecl-genReg = RegDecl <$> genName <*> genTypeExpr [] <*> 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 genAggregateField <*> 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 genAggregateField]--genTransition :: Gen Transition-genTransition =- Transition- <$> genName- <*> genName- <*> pure LegacyHoleImplementation- <*> 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+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.Types (parseEither)+import Data.Either (isLeft, isRight)+import Data.Foldable (toList)+import Data.KindID qualified as KindID+import Data.List (partition, permutations, sort, (\\))+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE+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 Data.Text.Lazy qualified as LazyText+import Data.Text.Lazy.Encoding qualified as LazyTextEncoding+import Data.Version (showVersion)+import Keiki.ProjectionDomain (matchesTextPattern)+import Keiro.Codec (Codec (..), EventType (..), decodeRaw)+import Keiro.Codec.IdDomain (IdDomainFailure (..), idDomainSampleText, idDomainTextPattern, parseKindIdV7Text, parseKindIdV7Value, typeIdV7Domain, validateIdDomainText)+import Keiro.Dsl.AggregateType+import Keiro.Dsl.BehaviorCoverage qualified as Behavior+import Keiro.Dsl.CanonicalEncoding (foldFingerprint128)+import Keiro.Dsl.CodecCompare+import Keiro.Dsl.ConformanceBaseline (conformanceBaselineSpec)+import Keiro.Dsl.ConformancePackage+import Keiro.Dsl.Coverage qualified as Coverage+import Keiro.Dsl.Diff (Change (..), ChangeKind (..), CompatibilitySurface (..), CompatibilityVector (..), FamilyDiff (..), Label (..), NodeFamily, RolloutConstraint (..), SurfaceVerdict (..), defaultGate, deriveLabel, familyRegistry, gateWith, gatedBreaking, isAdvisory, isBreaking, verdictFor)+import Keiro.Dsl.Diff qualified as CheckedDiff+import Keiro.Dsl.DiffReport (Remedy (..), diffReport, parseSurfaceName, remediationFor, renderExplainBlock, renderFinding)+import Keiro.Dsl.EventOutput+import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligation (..), BindingObligationKind (..), bindingHoles, bindingObligations, bindingObligationsForService, renderBindingObligations)+import Keiro.Dsl.Expression+import Keiro.Dsl.FoldFingerprint (FoldSurfaceError (..))+import Keiro.Dsl.FoldFingerprint qualified as CheckedFold+import Keiro.Dsl.FrontendCompatibility (frontendCompatibilitySpec)+import Keiro.Dsl.FrontendProfiles (frontendProfilesSpec)+import Keiro.Dsl.FrontendSurface (frontendSurfaceSpec)+import Keiro.Dsl.Goldens (GoldenEvidence (..), GoldenPayload (..), emitGoldenPayloads, goldenRelativePath, goldensForDiff)+import Keiro.Dsl.Grammar+import Keiro.Dsl.Harness (harnessFor, harnessForService, harnessForWithGoldens, harnessReadModel, harnessRouter, harnessWorkflow)+import Keiro.Dsl.HaskellSourceMove+import Keiro.Dsl.IdDomain (IdDomainContract (..), contractIdDomainContractFor, idDomainContractFor, idDomainIdentitiesForService)+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Manifest (manifestDependencies, manifestDependenciesForService, moduleNameOf, renderManifest, renderManifestForService, renderManifestForServiceWithFacade)+import Keiro.Dsl.MappedConsumer (ConsumerPlan (..), MappingIdentity (..), consumerPlan)+import Keiro.Dsl.NominalType hiding (NominalInvalidHaskellSource, NominalInvalidIdPrefix, NominalInvalidIdentity, NominalMissingIngredient)+import Keiro.Dsl.Parser (parseSource, parseSpec)+import Keiro.Dsl.PrettyPrint (renderSource, 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 (..), NominalGenerationOwner (..), NominalUseSite (..), ScaffoldModule (..), StructuralProjection (..), codecComparisonBanner, codecComparisonModule, defaultContext, firewallBreaches, genPrefixFor, generatedBanner, generatedBannerFor, generatedNominalModule, holePrefixFor, isGeneratedBannerLine, moduleRole, obsoleteGeneratedOutputHooks, planNominalGeneration, projectionSpecs, scaffoldAggregate, scaffoldContract, scaffoldContractForService, scaffoldIntake, scaffoldProcess, scaffoldPublisher, scaffoldReadModel, scaffoldRefusals, scaffoldReplayAudit, scaffoldRouter, scaffoldStructural, scaffoldWorkqueue, windowSeconds)+import Keiro.Dsl.ScaffoldRecord (GeneratedHaskellNamingEdition (..), ScaffoldModuleRoleRow (..), ScaffoldRecord (..), parseRecord, recordFileName, renderRecord)+import Keiro.Dsl.ScaffoldRun (MappingDrift (..), Refusal (..), ScaffoldReport (..), SourceLanguageDrift (..), StaleGeneratedEvidence (..), StaleModule (..), WriteDisposition (..), auditGeneratedHaskell, checkServiceDiagnostics, executeScaffold, executeScaffoldWithLanguage, executeServiceScaffold, executeServiceScaffoldWithRuntimePackage, executeServiceScaffoldWithRuntimePackageAndNameMigrations, planScaffold, planServiceScaffold, planServiceScaffoldWithRuntimePackage, planningRefusalDiagnostics, renderRefusals, renderScaffoldReport, scaffoldModules, scaffoldServiceModules)+import Keiro.Dsl.SemanticContract+import Keiro.Dsl.ServiceHarness+import Keiro.Dsl.SidecarMigration+import Keiro.Dsl.SidecarNames+import Keiro.Dsl.Skeleton (skeletonFor, skeletonKinds)+import Keiro.Dsl.TypeGraph+import Keiro.Dsl.Validate (Diagnostic (..), DiagnosticCode (..), Severity (..), derivedQueueTrio, diagnosticCodeText, parseDiagnosticCode, renderDiagnostic, validateService, validateSpec)+import Keiro.Dsl.Workspace+import Keiro.Dsl.WorkspaceAdoption+import Keiro.Dsl.WorkspaceDiff hiding (diffWorkspaces)+import Keiro.Dsl.WorkspaceDiff qualified as CheckedWorkspaceDiff+import Keiro.Dsl.WorkspaceRecord+import Keiro.Dsl.WorkspaceScaffold+import Paths_keiro_dsl qualified as Package+import System.Directory (canonicalizePath, createDirectory, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getTemporaryDirectory, listDirectory, removeFile, removePathForcibly, renameFile)+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.FilePath (takeDirectory, takeExtension, takeFileName, (</>))+import System.IO (hClose, openTempFile)+import System.Process (readProcessWithExitCode)+import Test.Hspec hiding (Spec)+import Test.QuickCheck++resolvedFold :: Either FoldSurfaceError value -> value+resolvedFold = either (error . ("unexpected fold-surface failure in checked fixture: " <>) . show) id++aggregateFoldFingerprintForService :: CheckedService -> Aggregate -> T.Text+aggregateFoldFingerprintForService service aggregate = resolvedFold (CheckedFold.aggregateFoldFingerprintForService service aggregate)++aggregateFoldSurfaceForService :: CheckedService -> Aggregate -> T.Text+aggregateFoldSurfaceForService service aggregate = resolvedFold (CheckedFold.aggregateFoldSurfaceForService service aggregate)++aggregateFoldFingerprint :: Spec -> Aggregate -> T.Text+aggregateFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (stableCheckedService spec) aggregate++aggregateFoldSurface :: Spec -> Aggregate -> T.Text+aggregateFoldSurface spec aggregate = aggregateFoldSurfaceForService (stableCheckedService spec) aggregate++legacyAggregateFoldFingerprint :: Spec -> Aggregate -> T.Text+legacyAggregateFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (legacyCheckedService spec) aggregate++legacyAggregateFoldSurface :: Spec -> Aggregate -> T.Text+legacyAggregateFoldSurface spec aggregate = aggregateFoldSurfaceForService (legacyCheckedService spec) aggregate++diffServices :: CheckedService -> CheckedService -> [Change]+diffServices old new = resolvedFold (CheckedDiff.diffServices old new)++diffSources :: ParsedSource -> ParsedSource -> [Change]+diffSources old new = resolvedFold (CheckedDiff.diffSources old new)++diffSpecs :: Spec -> Spec -> [Change]+diffSpecs old new = diffServices (stableCheckedService old) (stableCheckedService new)++legacyDiffSpecs :: Spec -> Spec -> [Change]+legacyDiffSpecs old new = diffServices (legacyCheckedService old) (legacyCheckedService new)++diffWorkspaces :: WorkspaceSpec -> WorkspaceSpec -> [WorkspaceChange]+diffWorkspaces old new = resolvedFold (CheckedWorkspaceDiff.diffWorkspaces old new)++replayImpactSpecs :: Spec -> Spec -> ReplayImpact+replayImpactSpecs old new =+ resolvedFold (ReplayImpact.replayImpactServices (stableCheckedService old) (stableCheckedService new))++legacyReplayImpactSpecs :: Spec -> Spec -> ReplayImpact+legacyReplayImpactSpecs old new =+ resolvedFold (ReplayImpact.replayImpactServices (legacyCheckedService old) (legacyCheckedService new))++nominalEqualityIdentities :: Spec -> [T.Text]+nominalEqualityIdentities = nominalEqualityIdentitiesForService . stableCheckedService++stableCheckedService :: Spec -> CheckedService+stableCheckedService = checkedService stableSourceLanguage++stableSourceLanguage :: SourceLanguage+stableSourceLanguage =+ DeclaredLanguage+ { declaredLanguageVersion = currentStableLanguageVersion,+ languageVersionLoc = noLoc+ }++main :: IO ()+main = hspec $ do+ conformanceBaselineSpec+ frontendCompatibilitySpec+ frontendSurfaceSpec+ frontendProfilesSpec++ describe "language support" $ do+ it "serializes support from the registered version and decodes older records" $ do+ v1Contract <- maybe (expectationFailure "missing v1 contract" >> fail "unreachable") pure (effectiveLanguageContractForVersion =<< languageVersion 1)+ v4Contract <- maybe (expectationFailure "missing v4 contract" >> fail "unreachable") pure (effectiveLanguageContractForVersion =<< languageVersion 4)+ effectiveLanguageSupport v1Contract `shouldBe` CompatibilityOnly+ effectiveLanguageSupport v4Contract `shouldBe` Stable+ Aeson.toJSON v4Contract+ `shouldBe` object+ [ "languageVersion" .= (4 :: Int),+ "runtimeSemantics" .= ("keiro-dsl/runtime-semantics/3" :: T.Text),+ "languageSupport" .= ("stable" :: T.Text)+ ]+ Aeson.eitherDecode "{\"languageVersion\":1,\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/1\"}"+ `shouldBe` Right v1Contract++ it "reports stable and compatibility-only support through source inspection" $ do+ (stableCode, stableOut, stableErr) <- runKeiroDsl ["inspect", "test/fixtures/contract-v4.keiro", "--format=json"]+ stableCode `shouldBe` ExitSuccess+ stableErr `shouldBe` ""+ stableOut `shouldContain` "\"languageVersion\":4"+ stableOut `shouldContain` "\"languageSupport\":\"stable\""+ (compatibilityCode, compatibilityOut, compatibilityErr) <- runKeiroDsl ["inspect", "test/fixtures/language-v1.keiro", "--format=json"]+ compatibilityCode `shouldBe` ExitSuccess+ compatibilityErr `shouldBe` ""+ compatibilityOut `shouldContain` "\"languageVersion\":1"+ compatibilityOut `shouldContain` "\"languageSupport\":\"compatibility-only\""++ it "surfaces non-stable contracts and enforces a released minimum language" $ do+ let legacyPath = "test/fixtures/language-legacy.keiro"+ stablePath = "test/fixtures/contract-v4.keiro"+ (legacyCode, legacyOut, legacyErr) <- runKeiroDsl ["check", legacyPath]+ legacyCode `shouldBe` ExitSuccess+ legacyOut `shouldBe` "OK\n"+ legacyErr `shouldContain` "language contract: effective keiro-dsl 1 (legacy-unversioned, compatibility-only, runtime semantics keiro-dsl/runtime-semantics/1)"+ legacyErr `shouldContain` "language-4 strict spec-surface validation is not applied"++ (stableCode, stableOut, stableErr) <- runKeiroDsl ["check", stablePath]+ stableCode `shouldBe` ExitSuccess+ stableOut `shouldBe` "OK\n"+ stableErr `shouldBe` ""++ (floorCode, floorOut, floorErr) <- runKeiroDsl ["check", legacyPath, "--min-language", "4"]+ floorCode `shouldBe` ExitFailure 1+ floorOut `shouldBe` ""+ floorErr `shouldContain` "language-legacy.keiro:1: error[LanguageVersionBelowMinimum]"+ floorErr `shouldContain` "effective language version 1 (legacy-unversioned) is below the required minimum 4"++ (metCode, metOut, _) <- runKeiroDsl ["check", legacyPath, "--min-language", "1"]+ metCode `shouldBe` ExitSuccess+ metOut `shouldBe` "OK\n"++ (unsupportedCode, _, unsupportedErr) <- runKeiroDsl ["check", legacyPath, "--min-language", "9"]+ unsupportedCode `shouldBe` ExitFailure 1+ unsupportedErr `shouldContain` "supported versions: 1, 2, 3, 4"++ it "attributes a workspace language floor to its manifest and every member" $ do+ let v1Member = T.unlines ["language keiro-dsl 1", "context language-floor"]+ withInlineWorkspace+ "keiro-dsl-language-floor"+ ( "language-floor",+ [ ("domain/a.keiro", v1Member),+ ("domain/b.keiro", v1Member)+ ]+ )+ $ \root _ _ -> do+ let manifest = root </> "service.keiro-workspace"+ (floorCode, floorOut, floorErr) <- runKeiroDsl ["check", manifest, "--min-language", "4"]+ floorCode `shouldBe` ExitFailure 1+ floorOut `shouldBe` ""+ floorErr `shouldContain` (manifest <> ":1: error[LanguageVersionBelowMinimum]")+ floorErr `shouldContain` "domain/a.keiro:1: note: member selects effective language version 1"+ floorErr `shouldContain` "domain/b.keiro:1: note: member selects effective language version 1"+ floorErr `shouldContain` "workspace, 0 legacy-unversioned member(s)"++ (metCode, metOut, _) <- runKeiroDsl ["check", manifest, "--min-language", "1"]+ metCode `shouldBe` ExitSuccess+ metOut `shouldBe` "OK\n"++ describe "warning enforcement" $ do+ it "round-trips every stable diagnostic code spelling" $ do+ forM_ [minBound .. maxBound] $ \diagnosticCode ->+ parseDiagnosticCode (diagnosticCodeText diagnosticCode) `shouldBe` Just diagnosticCode++ it "fails only the warnings selected by invocation policy" $ do+ let fixture = "test/fixtures/deny-unlogged.keiro"+ warningText = "warning[WqUnloggedDurability]"+ summaryText = "check: 1 warning(s) escalated to failure (denied: WqUnloggedDurability)"++ (plainCode, plainOut, plainErr) <- runKeiroDsl ["check", fixture]+ plainCode `shouldBe` ExitSuccess+ plainOut `shouldBe` "OK\n"+ plainErr `shouldContain` warningText+ plainErr `shouldNotContain` "escalated to failure"++ (allCode, allOut, allErr) <- runKeiroDsl ["check", fixture, "--deny-warnings"]+ allCode `shouldBe` ExitFailure 1+ allOut `shouldBe` ""+ allErr `shouldContain` warningText+ allErr `shouldContain` summaryText++ (selectedCode, selectedOut, selectedErr) <- runKeiroDsl ["check", fixture, "--deny", "WqUnloggedDurability"]+ selectedCode `shouldBe` ExitFailure 1+ selectedOut `shouldBe` ""+ selectedErr `shouldContain` warningText+ selectedErr `shouldContain` summaryText++ (otherCode, otherOut, otherErr) <- runKeiroDsl ["check", fixture, "--deny", "WireSchemaVersionMismatch"]+ otherCode `shouldBe` ExitSuccess+ otherOut `shouldBe` "OK\n"+ otherErr `shouldContain` warningText+ otherErr `shouldNotContain` "escalated to failure"++ (unionCode, unionOut, unionErr) <-+ runKeiroDsl+ [ "check",+ fixture,+ "--deny-warnings",+ "--deny",+ "WireSchemaVersionMismatch,WqUnloggedDurability"+ ]+ unionCode `shouldBe` ExitFailure 1+ unionOut `shouldBe` ""+ unionErr `shouldContain` warningText+ unionErr `shouldContain` summaryText++ (unknownCode, _, unknownErr) <- runKeiroDsl ["check", fixture, "--deny", "NotACode"]+ unknownCode `shouldBe` ExitFailure 1+ unknownErr `shouldContain` "unknown diagnostic code `NotACode`"+ unknownErr `shouldContain` "warning[Code]"++ -- A denial that can never match reads like a CI gate and is not one. Every+ -- code `check` cannot emit is refused at the point of use instead.+ it "refuses a denial of a code check can never emit" $ do+ let fixture = "test/fixtures/deny-unlogged.keiro"++ (diffCode, _, diffErr) <- runKeiroDsl ["check", fixture, "--deny", "EvtFieldWireKeyChanged"]+ diffCode `shouldBe` ExitFailure 1+ diffErr `shouldContain` "`EvtFieldWireKeyChanged` is emitted by `keiro-dsl diff`"+ diffErr `shouldContain` "would never match"++ -- Rejection survives being hidden inside a comma-separated list.+ (mixedCode, _, mixedErr) <-+ runKeiroDsl ["check", fixture, "--deny", "WqUnloggedDurability,WorkflowShapeChanged"]+ mixedCode `shouldBe` ExitFailure 1+ mixedErr `shouldContain` "`WorkflowShapeChanged` is emitted by `keiro-dsl diff`"++ (codecCode, _, codecErr) <- runKeiroDsl ["check", fixture, "--deny", "CodecCompareDifference"]+ codecCode `shouldBe` ExitFailure 1+ codecErr `shouldContain` "generated codec-comparison path"++ -- A coverage code is emittable, but only by an invocation that asks for+ -- the coverage pass, so the requirement is stated rather than ignored.+ (noPassCode, _, noPassErr) <- runKeiroDsl ["check", fixture, "--deny", "CoverageOpaqueSurface"]+ noPassCode `shouldBe` ExitFailure 1+ noPassErr `shouldContain` "add --coverage-report FILE or drop the code"++ -- CoverageOpaqueGateExceeded is the error --fail-on-opaque itself raises,+ -- never a warning, so denying it is a silent no-op in every invocation —+ -- with or without the coverage pass it is refused with the real spelling.+ withTempDirectory "keiro-dsl-gate-exceeded-deny" $ \out -> do+ (gateCode, _, gateErr) <-+ runKeiroDsl+ ["check", fixture, "--coverage-report", out </> "coverage.json", "--deny", "CoverageOpaqueGateExceeded"]+ gateCode `shouldBe` ExitFailure 1+ gateErr `shouldContain` "pass --fail-on-opaque instead of denying it"+ (gateNoPassCode, _, gateNoPassErr) <- runKeiroDsl ["check", fixture, "--deny", "CoverageOpaqueGateExceeded"]+ gateNoPassCode `shouldBe` ExitFailure 1+ gateNoPassErr `shouldContain` "pass --fail-on-opaque instead of denying it"++ it "applies the warning policy to structural-coverage findings" $ do+ withTempDirectory "keiro-dsl-coverage-deny" $ \out -> do+ let fixture = "test/fixtures/structural-conformance.keiro"+ coveragePath = out </> "coverage.json"+ reportPath = out </> "nested" </> "dir" </> "check.json"+ warningText = "warning[CoverageOpaqueSurface]"++ -- Reporting-only by default: the finding prints and the check passes.+ (plainCode, _, plainErr) <-+ runKeiroDsl ["check", fixture, "--coverage-report", coveragePath]+ plainCode `shouldBe` ExitSuccess+ plainErr `shouldContain` warningText+ plainErr `shouldNotContain` "escalated to failure"++ -- Before ExecPlan 199 this combination exited 0 with the warning printed.+ (deniedCode, _, deniedErr) <-+ runKeiroDsl+ [ "check",+ fixture,+ "--coverage-report",+ coveragePath,+ "--deny-warnings",+ -- The nested path also proves --report-out creates parent dirs.+ "--report-out",+ reportPath+ ]+ deniedCode `shouldBe` ExitFailure 1+ deniedErr `shouldContain` warningText+ deniedErr `shouldContain` "escalated to failure (denied: CoverageOpaqueSurface)"++ report <- decodeJsonValue reportPath+ jsonField "ok" report `shouldBe` Just (Aeson.Bool False)+ (jsonField "summary" report >>= jsonField "deniedWarnings") `shouldBe` Just (Aeson.Number 1)+ case jsonField "diagnostics" report of+ Just (Aeson.Array entries) ->+ [entry | entry <- toList entries, jsonField "code" entry == Just (Aeson.String "CoverageOpaqueSurface")]+ `shouldSatisfy` \matching -> case matching of+ entry : _ ->+ jsonField "severity" entry == Just (Aeson.String "warning")+ && jsonField "denied" entry == Just (Aeson.Bool True)+ [] -> False+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ -- Selecting the code by name gates it just as precisely.+ (selectedCode, _, _) <-+ runKeiroDsl+ ["check", fixture, "--coverage-report", coveragePath, "--deny", "CoverageOpaqueSurface"]+ selectedCode `shouldBe` ExitFailure 1++ it "spells warning severity the same way in both JSON reports" $ do+ withTempDirectory "keiro-dsl-severity-vocabulary" $ \out -> do+ let coveragePath = out </> "coverage.json"+ (exitCode, _, _) <-+ runKeiroDsl+ ["check", "test/fixtures/structural-conformance.keiro", "--coverage-report", coveragePath]+ exitCode `shouldBe` ExitSuccess+ coverage <- decodeJsonValue coveragePath+ case jsonField "findings" coverage of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> jsonField "severity" entry `shouldBe` Just (Aeson.String "warning")+ [] -> expectationFailure "coverage report had no findings"+ other -> expectationFailure ("expected findings array, got " <> show other)++ it "writes the machine report when a workspace is refused during composition" $ do+ withTempDirectory "keiro-dsl-workspace-refusal-report" $ \out -> do+ let reportPath = out </> "made" </> "up" </> "refusal.json"+ (exitCode, stdoutText, _) <-+ runKeiroDsl+ [ "check",+ "test/fixtures/workspace-dup-decl/service.keiro-workspace",+ "--report-out",+ reportPath+ ]+ exitCode `shouldBe` ExitFailure 1+ stdoutText `shouldBe` ""+ report <- decodeJsonValue reportPath+ jsonField "schema" report `shouldBe` Just (Aeson.String "keiro-dsl/check-report/1")+ jsonField "kind" report `shouldBe` Just (Aeson.String "workspace")+ jsonField "ok" report `shouldBe` Just (Aeson.Bool False)+ -- No service graph was composed, so there is no language contract.+ jsonField "language" report `shouldBe` Just Aeson.Null+ case jsonField "diagnostics" report of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> do+ jsonField "code" entry `shouldBe` Just (Aeson.String "WorkspaceDuplicateDeclaration")+ jsonField "severity" entry `shouldBe` Just (Aeson.String "error")+ [] -> expectationFailure "workspace refusal report had no diagnostics"+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ it "applies the same warning policy to a composed workspace" $ do+ warningSource <- readTestText "test/fixtures/deny-unlogged.keiro"+ withInlineWorkspace+ "keiro-dsl-warning-policy"+ ("warning-policy", [("domain/jobs.keiro", warningSource)])+ $ \root _ _ -> do+ let manifest = root </> "service.keiro-workspace"+ (plainCode, plainOut, plainErr) <- runKeiroDsl ["check", manifest]+ plainCode `shouldBe` ExitSuccess+ plainOut `shouldBe` "OK\n"+ plainErr `shouldContain` "domain/jobs.keiro:4: warning[WqUnloggedDurability]"++ (deniedCode, deniedOut, deniedErr) <- runKeiroDsl ["check", manifest, "--deny", "WqUnloggedDurability"]+ deniedCode `shouldBe` ExitFailure 1+ deniedOut `shouldBe` ""+ deniedErr `shouldContain` "domain/jobs.keiro:4: warning[WqUnloggedDurability]"+ deniedErr `shouldContain` "check: 1 warning(s) escalated to failure (denied: WqUnloggedDurability)"++ describe "check report" $ do+ it "writes the source failure report and matches the public-CLI golden" $ do+ withTempDirectory "keiro-dsl-check-report-floor" $ \out -> do+ let reportPath = out </> "report.json"+ (exitCode, stdoutText, _) <-+ runKeiroDsl+ [ "check",+ "test/fixtures/language-legacy.keiro",+ "--min-language",+ "4",+ "--report-out",+ reportPath+ ]+ exitCode `shouldBe` ExitFailure 1+ stdoutText `shouldBe` ""+ report <- decodeJsonValue reportPath+ goldenPath <- resolveTestPath "test/fixtures/check-report/legacy-min-language.golden.json"+ golden <- decodeJsonValue goldenPath+ report `shouldBe` golden+ jsonField "schema" report `shouldBe` Just (Aeson.String "keiro-dsl/check-report/1")+ jsonField "kind" report `shouldBe` Just (Aeson.String "source")+ jsonField "ok" report `shouldBe` Just (Aeson.Bool False)+ (jsonField "language" report >>= jsonField "stable") `shouldBe` Just (Aeson.Bool False)+ (jsonField "summary" report >>= jsonField "errors") `shouldBe` Just (Aeson.Number 1)+ case jsonField "diagnostics" report of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> do+ jsonField "code" entry `shouldBe` Just (Aeson.String "LanguageVersionBelowMinimum")+ jsonField "severity" entry `shouldBe` Just (Aeson.String "error")+ jsonField "line" entry `shouldBe` Just (Aeson.Number 1)+ [] -> expectationFailure "check report had no diagnostics"+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ it "marks denied warnings without changing their severity" $ do+ withTempDirectory "keiro-dsl-check-report-deny" $ \out -> do+ let deniedPath = out </> "denied.json"+ allowedPath = out </> "allowed.json"+ fixture = "test/fixtures/deny-unlogged.keiro"+ (deniedCode, _, _) <- runKeiroDsl ["check", fixture, "--deny-warnings", "--report-out", deniedPath]+ deniedCode `shouldBe` ExitFailure 1+ deniedReport <- decodeJsonValue deniedPath+ jsonField "ok" deniedReport `shouldBe` Just (Aeson.Bool False)+ (jsonField "summary" deniedReport >>= jsonField "deniedWarnings") `shouldBe` Just (Aeson.Number 1)+ case jsonField "diagnostics" deniedReport of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> do+ jsonField "severity" entry `shouldBe` Just (Aeson.String "warning")+ jsonField "denied" entry `shouldBe` Just (Aeson.Bool True)+ [] -> expectationFailure "denied-warning report had no diagnostics"+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ (allowedCode, _, _) <- runKeiroDsl ["check", fixture, "--report-out", allowedPath]+ allowedCode `shouldBe` ExitSuccess+ allowedReport <- decodeJsonValue allowedPath+ jsonField "ok" allowedReport `shouldBe` Just (Aeson.Bool True)+ (jsonField "summary" allowedReport >>= jsonField "deniedWarnings") `shouldBe` Just (Aeson.Number 0)+ case jsonField "diagnostics" allowedReport of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> jsonField "denied" entry `shouldBe` Just (Aeson.Bool False)+ [] -> expectationFailure "allowed-warning report had no diagnostics"+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ it "reports canonical workspace members and writes nothing before parse success" $ do+ withTempDirectory "keiro-dsl-check-report-workspace" $ \out -> do+ let workspacePath = out </> "workspace.json"+ parseFailurePath = out </> "parse-failure.json"+ (workspaceCode, _, _) <- runKeiroDsl ["check", canonicalWorkspacePath, "--report-out", workspacePath]+ workspaceCode `shouldBe` ExitSuccess+ workspaceReport <- decodeJsonValue workspacePath+ jsonField "kind" workspaceReport `shouldBe` Just (Aeson.String "workspace")+ jsonField "ok" workspaceReport `shouldBe` Just (Aeson.Bool True)+ (jsonField "language" workspaceReport >>= jsonField "sourceForm") `shouldBe` Just (Aeson.String "workspace-composed")+ case jsonField "members" workspaceReport of+ Just (Aeson.Array members) -> length members `shouldBe` 3+ other -> expectationFailure ("expected members array, got " <> show other)++ let v1Member = T.unlines ["language keiro-dsl 1", "context report-floor"]+ withInlineWorkspace+ "keiro-dsl-check-report-workspace-floor"+ ( "report-floor",+ [ ("domain/a.keiro", v1Member),+ ("domain/b.keiro", v1Member)+ ]+ )+ $ \root _ _ -> do+ let manifest = root </> "service.keiro-workspace"+ floorReportPath = out </> "workspace-floor.json"+ (floorCode, _, _) <-+ runKeiroDsl ["check", manifest, "--min-language", "4", "--report-out", floorReportPath]+ floorCode `shouldBe` ExitFailure 1+ floorReport <- decodeJsonValue floorReportPath+ case jsonField "diagnostics" floorReport of+ Just (Aeson.Array entries) -> case toList entries of+ entry : _ -> do+ jsonField "file" entry `shouldBe` Just (Aeson.String (T.pack manifest))+ jsonField "code" entry `shouldBe` Just (Aeson.String "LanguageVersionBelowMinimum")+ case jsonField "related" entry of+ Just (Aeson.Array related) -> length related `shouldBe` 2+ other -> expectationFailure ("expected related-location array, got " <> show other)+ [] -> expectationFailure "workspace-floor report had no diagnostics"+ other -> expectationFailure ("expected diagnostics array, got " <> show other)++ (parseCode, _, _) <-+ runKeiroDsl+ [ "check",+ "test/fixtures/language-future.keiro",+ "--report-out",+ parseFailurePath+ ]+ parseCode `shouldBe` ExitFailure 1+ doesFileExist parseFailurePath `shouldReturn` False++ describe "runtime capability and fold identity baseline (plan 181)" $ do+ it "pins the fold-only FNV-1a-128 UTF-8 encoding" $ do+ foldFingerprint128 "" `shouldBe` "6c62272e07bb014262b821756295c58d"+ foldFingerprint128 "雪" `shouldBe` "a68afaae758b5822836dbc787bb233bd"++ it "pins complete fold surfaces and fingerprints across representative aggregates" $ do+ scalar <- checkedServiceOf "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ nominal <- checkedServiceOf "test/fixtures/nominal-scalars.keiro"+ idDomain <- checkedServiceOf "test/fixtures/id-domain-migration-v3.keiro"+ behavior <- checkedServiceOf "test/fixtures/behavior-complete.keiro"+ workspace <- shouldComposeWorkspace "test/fixtures/workspace-nominals/service.keiro-workspace"+ let actual =+ T.intercalate+ "\n\n"+ [ renderFoldBaseline "aggregate-scalar-expressions-v2" scalar,+ renderFoldBaseline "nominal-scalars" nominal,+ renderFoldBaseline "id-domain-migration-v3" idDomain,+ renderFoldBaseline "behavior-complete" behavior,+ renderFoldBaseline "workspace-nominals" (checkedWorkspace workspace)+ ]+ assertMatchesGolden "test/fixtures/fold-identity-baseline.golden" actual++ it "pins all four runtime gates and fingerprint segment projections" $ do+ nominalSpec <- specOf "test/fixtures/id-domain-migration-v3.keiro"+ nominalRegistry <- case resolveNominalTypes nominalSpec of+ Left errors -> expectationFailure (show errors) >> fail "unreachable"+ Right value -> pure value+ nominal <- case lookupNominalType "OrderId" nominalRegistry of+ Nothing -> expectationFailure "missing OrderId nominal" >> fail "unreachable"+ Just value -> pure value+ strictSpec <-+ parseInlineSpec+ "<strict-profile>"+ ( T.unlines+ [ "context strict-profile",+ "aggregate DuplicateRegister",+ " regs",+ " value Int = 0",+ " value Int = 0",+ " states Open"+ ]+ )+ rows <- forM [1 .. 4 :: Int] $ \number -> do+ contract <- case languageVersion (fromIntegral number) >>= effectiveLanguageContractForVersion of+ Nothing -> expectationFailure ("missing released language contract " <> show number) >> fail "unreachable"+ Just value -> pure value+ let hasAggregateIdDomain = maybe False (const True) (idDomainContractFor contract "ord")+ hasContractIdDomain = maybe False (const True) (contractIdDomainContractFor contract "ord")+ nominalContract = equalityContractVersion <$> nominalEqualityContractForService contract nominal+ strictService = CheckedService contract strictSpec+ hasStrictValidation = any ((== AggregateDuplicateRegister) . code) (validateService strictService)+ pure+ ( number,+ effectiveRuntimeSemantics contract,+ runtimeSemanticsFingerprintSegments contract,+ hasAggregateIdDomain,+ hasContractIdDomain,+ nominalContract,+ hasStrictValidation+ )+ rows+ `shouldBe` [ (1, "keiro-dsl/runtime-semantics/1", [], False, False, Just "keiro-dsl/nominal-equality/1", False),+ (2, "keiro-dsl/runtime-semantics/1", [], False, False, Just "keiro-dsl/nominal-equality/1", False),+ (3, "keiro-dsl/runtime-semantics/2", ["semantic-contract:keiro-dsl/runtime-semantics/2"], True, False, Just "keiro-dsl/nominal-equality/2", False),+ (4, "keiro-dsl/runtime-semantics/3", ["semantic-contract:keiro-dsl/runtime-semantics/2"], True, True, Just "keiro-dsl/nominal-equality/2", True)+ ]++ it "explains a serialized runtime-profile mismatch" $ do+ case (Aeson.eitherDecode "{\"languageVersion\":4,\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/2\"}" :: Either String EffectiveLanguageContract) of+ Left message -> do+ message `shouldContain` "runtimeSemantics does not match language version 4"+ message `shouldContain` "keiro-dsl/runtime-semantics/3"+ message `shouldContain` "keiro-dsl/runtime-semantics/2"+ Right _ -> expectationFailure "expected a runtime-profile mismatch"++ it "reports every formerly silent fold-resolution failure and propagates it" $ do+ baseService <- checkedServiceOf "test/fixtures/id-domain-migration-v3.keiro"+ recursiveMapped <- specOf "test/fixtures/mapped-recursive.keiro"+ brokenNominal <- specOf "test/fixtures/nominal-missing-facts.keiro"+ missingInitial <- specOf "test/fixtures/mapped-missing-initial.keiro"+ let contract = checkedLanguageContract baseService+ baseSpec = checkedSpec baseService+ baseAggregate = onlyAggregate baseSpec+ withAggregate transform =+ baseSpec+ { specNodes =+ [ NAggregate (transform aggregate)+ | NAggregate aggregate <- specNodes baseSpec+ ]+ }+ replaceFirstTransition transform aggregate =+ aggregate+ { aggTransitions = case aggTransitions aggregate of+ transition : rest -> transform transition : rest+ [] -> []+ }+ guardSpec = withAggregate (replaceFirstTransition (\transition -> transition {tGuard = Just (EAtom (AName "missingGuardRoot"))}))+ outputSpec = withAggregate (replaceFirstTransition (\transition -> transition {tEmits = ["MissingEvent"]}))+ typeGraphSpec = baseSpec {specMapped = specMapped recursiveMapped}+ nominalSpec = baseSpec {specNominalScalars = specNominalScalars brokenNominal}+ cases =+ [ (CheckedService contract typeGraphSpec, baseAggregate, \case FoldTypeGraphResolutionFailed {} -> True; _ -> False),+ (CheckedService contract nominalSpec, baseAggregate, \case FoldNominalResolutionFailed {} -> True; _ -> False),+ (CheckedService contract missingInitial, onlyAggregate missingInitial, \case FoldRegisterInitialResolutionFailed {} -> True; _ -> False),+ (CheckedService contract guardSpec, onlyAggregate guardSpec, \case FoldGuardResolutionFailed {} -> True; _ -> False),+ (CheckedService contract outputSpec, onlyAggregate outputSpec, \case FoldEventOutputResolutionFailed {} -> True; _ -> False)+ ]+ forM_ cases $ \(service, aggregate, matches) -> do+ CheckedFold.aggregateFoldSurfaceForService service aggregate+ `shouldSatisfy` either matches (const False)+ CheckedFold.aggregateFoldFingerprintForService service aggregate+ `shouldSatisfy` either matches (const False)+ let brokenService = CheckedService contract guardSpec+ CheckedDiff.diffServices brokenService baseService `shouldSatisfy` isLeft+ ReplayImpact.replayImpactServices brokenService baseService `shouldSatisfy` isLeft+ planServiceScaffold (defaultContext (specContext guardSpec)) brokenService+ `shouldSatisfy` \case+ Left refusals -> any (\case FoldSurfaceRefusal {} -> True; _ -> False) refusals+ Right _ -> False++ it "pins representative diff and replay-impact rendering" $ do+ old <- parsedSourceOf "test/fixtures/reservation.keiro"+ new <- parsedSourceOf "test/fixtures/reservation-guard-tightened.keiro"+ let changes = diffSources old new+ impact = resolvedFold (ReplayImpact.replayImpactServices (checkedSource old) (checkedSource new))+ actual =+ T.intercalate+ "\n"+ ( "diff:"+ : map renderFinding changes+ <> ["replay:", ReplayImpact.renderReplayImpact impact]+ )+ assertMatchesGolden "test/fixtures/fold-identity-diff-replay.golden" actual++ it "pins unrelated public 64-bit identities outside the fold digest" $ do+ readModelSpec <- specOf "test/fixtures/readmodel.keiro"+ wireSpec <- specOf "test/fixtures/consumer-types.keiro"+ behaviorSpec <- specOf "test/fixtures/behavior-complete.keiro"+ readModel <- case [value | NReadModel value <- specNodes readModelSpec] of+ value : _ -> pure value+ [] -> expectationFailure "missing read-model fixture" >> fail "unreachable"+ graph <- shouldResolveTypeGraph wireSpec+ behaviorKey <- case Behavior.deriveBehaviorRequirements behaviorSpec of+ Right (requirement : _) -> pure (Behavior.unBehaviorKey (Behavior.requirementKey requirement))+ result -> expectationFailure ("missing behavior requirement: " <> show result) >> fail "unreachable"+ deriveShapeHash readModel `shouldBe` "fnv1a:3717f6d9e3c44bd6"+ wireFingerprint graph "ArtifactInfo" `shouldBe` "2bd99b3e57bcde9b"+ behaviorKey `shouldBe` "behavior-v1-0128e858fee6f2b3"++ describe "source language version" $ do+ let legacy = "context hospital-capacity\n"+ declared = "# leading comment\n\nlanguage keiro-dsl 1\ncontext hospital-capacity\n"+ failureCode source = case parseSource "source.keiro" source of+ Left (SourceLanguageFailure diagnostic) -> Just (sourceLanguageErrorCode diagnostic)+ _ -> Nothing+ parseRight name source = case parseSource name source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ declaredVersionOf DeclaredLanguage {declaredLanguageVersion = version} = Just version+ declaredVersionOf LegacyUnversioned = Nothing+ orderedSubstrings needles haystack = go (map T.pack needles) (T.pack haystack)+ where+ go [] _ = True+ go (needle : rest) remaining =+ let (_, suffix) = T.breakOn needle remaining+ in not (T.null suffix) && go rest (T.drop (T.length needle) suffix)++ it "selects declared v1 after comments while preserving semantic equality" $ do+ legacySource <- parseRight "legacy.keiro" legacy+ declaredSource <- parseRight "declared.keiro" declared+ parsedSpec legacySource `shouldBe` parsedSpec declaredSource+ parsedSourceLanguage legacySource `shouldBe` LegacyUnversioned+ declaredVersionOf (parsedSourceLanguage declaredSource) `shouldBe` languageVersion 1+ effectiveLanguageVersion (parsedSourceLanguage legacySource)+ `shouldBe` effectiveLanguageVersion (parsedSourceLanguage declaredSource)++ it "threads paired released versions through one checked semantic boundary" $ do+ let body = T.unlines ["context semantic-pair", "aggregate Counter", " regs", " states Open"]+ v1Text = "language keiro-dsl 1\n" <> body+ v2Text = "language keiro-dsl 2\n" <> body+ v1Source <- parseRight "reservation-v1.keiro" v1Text+ v2Source <- parseRight "reservation-v2.keiro" v2Text+ let v1Service = checkedSource v1Source+ v2Service = checkedSource v2Source+ v1Spec = checkedSpec v1Service+ v2Spec = checkedSpec v2Service+ ctx = defaultContext (specContext v1Spec)+ aggregates spec = [aggregate | NAggregate aggregate <- specNodes spec]+ v1Spec `shouldBe` v2Spec+ Just (effectiveContractLanguageVersion (checkedLanguageContract v1Service)) `shouldBe` languageVersion 1+ Just (effectiveContractLanguageVersion (checkedLanguageContract v2Service)) `shouldBe` languageVersion 2+ effectiveRuntimeSemantics (checkedLanguageContract v1Service)+ `shouldBe` effectiveRuntimeSemantics (checkedLanguageContract v2Service)+ validateService v1Service `shouldBe` validateService v2Service+ scaffoldServiceModules ctx v1Service `shouldBe` scaffoldServiceModules ctx v2Service+ case (aggregates v1Spec, aggregates v2Spec) of+ ([v1Aggregate], [v2Aggregate]) -> do+ aggregateFoldSurfaceForService v1Service v1Aggregate+ `shouldBe` aggregateFoldSurfaceForService v2Service v2Aggregate+ aggregateFoldFingerprintForService v1Service v1Aggregate+ `shouldBe` aggregateFoldFingerprintForService v2Service v2Aggregate+ other -> expectationFailure ("expected one aggregate per paired source, got " <> show (fmap length other))+ diffServices v1Service v2Service `shouldBe` []+ resolvedFold (ReplayImpact.replayImpactServices v1Service v2Service) `shouldBe` ReplayNeutral++ it "retains one effective contract for same-version workspaces and refuses mixed versions" $ do+ let manifest = "service semantic-workspace\nspec domain/a.keiro\nspec domain/b.keiro\n"+ v1Body = "language keiro-dsl 1\ncontext semantic-workspace\n"+ v2Body = "language keiro-dsl 2\ncontext semantic-workspace\n"+ sourceWith b =+ memoryContentSource+ ( Map.fromList+ [ ("service.keiro-workspace", manifest),+ ("domain/a.keiro", b),+ ("domain/b.keiro", b)+ ]+ )+ mixedSource =+ memoryContentSource+ ( Map.fromList+ [ ("service.keiro-workspace", manifest),+ ("domain/a.keiro", v1Body),+ ("domain/b.keiro", v2Body)+ ]+ )+ sameVersion <- loadWorkspace (sourceWith v2Body) "service.keiro-workspace"+ case sameVersion of+ Right workspace -> do+ Just (effectiveContractLanguageVersion (checkedLanguageContract (checkedWorkspace workspace))) `shouldBe` languageVersion 2+ validateService (checkedWorkspace workspace) `shouldBe` []+ Left failure -> expectationFailure (show failure)+ mixed <- loadWorkspace mixedSource "service.keiro-workspace"+ case mixed of+ Left (WorkspaceRefused diagnostics) ->+ map wdCode (NE.toList diagnostics) `shouldContain` [WorkspaceLanguageVersionMismatch]+ other -> expectationFailure ("expected a mixed-version refusal, got " <> show other)++ it "refuses a source/service contract mismatch before creating the output directory" $ do+ v1Version <- maybe (expectationFailure "version 1 missing" >> fail "unreachable") pure (languageVersion 1)+ parsed <- parseRight "semantic-v2.keiro" "language keiro-dsl 2\ncontext semantic-refusal\n"+ let service = checkedSource parsed+ ctx = defaultContext "semantic-refusal"+ modules <- case planServiceScaffold ctx service of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right planned -> pure planned+ withTempDirectory "keiro-dsl-semantic-refusal" $ \root -> do+ let out = root </> "not-created"+ result <- executeServiceScaffold out False "semantic-v2.keiro" (DeclaredLanguage v1Version noLoc) ctx service modules+ result `shouldBe` Left [SemanticContractMismatch "source provenance and checked service selected different effective language contracts"]+ doesDirectoryExist out `shouldReturn` False++ it "retains explicit declarations in source rendering and leaves legacy unversioned" $ do+ legacySource <- parseRight "legacy.keiro" legacy+ declaredSource <- parseRight "declared.keiro" declared+ renderSource legacySource `shouldBe` "context hospital-capacity\n"+ renderSource declaredSource `shouldBe` "language keiro-dsl 1\ncontext hospital-capacity\n"+ parseSpec "declared.keiro" declared `shouldBe` Right (parsedSpec declaredSource)++ it "classifies invalid, unsupported, duplicate, and misplaced preambles" $ do+ failureCode "language keiro-dsl 0\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion+ failureCode "language keiro-dsl nope\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion+ failureCode "language keiro-dsl -1\ncontext hospital-capacity\n" `shouldBe` Just InvalidLanguageVersion+ failureCode "language keiro-dsl 5\ncontext hospital-capacity\n" `shouldBe` Just UnsupportedLanguageVersion+ failureCode "language keiro-dsl 1\nlanguage keiro-dsl 1\ncontext hospital-capacity\n" `shouldBe` Just DuplicateLanguagePreamble+ failureCode "context hospital-capacity\nlanguage keiro-dsl 1\n" `shouldBe` Just MisplacedLanguagePreamble++ it "treats language and successor spellings as data in nested grammar positions" $ do+ forM_ ["language-identifier-v1.keiro", "language-identifier-v2.keiro"] $ \fixture -> do+ source <- readTestText ("test/fixtures/" <> fixture)+ parsed <- parseRight fixture source+ validateSpec (parsedSpec parsed) `shouldBe` []+ v1 <- readTestText "test/fixtures/language-identifier-v1.keiro"+ let manifest = "service language-collisions\nspec domain/collisions.keiro\n"+ workspaceSource = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/collisions.keiro", v1)])+ loaded <- loadWorkspace workspaceSource "service.keiro-workspace"+ loaded `shouldSatisfy` isRight++ it "keeps duplicate and misplaced preamble diagnostics on their grammar lines" $ do+ let sourceFailureAt expectedCode expectedLine source =+ case parseSource "located.keiro" source of+ Left (SourceLanguageFailure diagnostic) -> do+ sourceLanguageErrorCode diagnostic `shouldBe` expectedCode+ unLoc (sourceLanguageLoc diagnostic) `shouldBe` expectedLine+ other -> expectationFailure ("expected located source-language failure, got " <> show other)+ sourceFailureAt DuplicateLanguagePreamble 2 "language keiro-dsl 1\nlanguage keiro-dsl 1\ncontext located\n"+ sourceFailureAt MisplacedLanguagePreamble 3 "context located\nid language prefix=lang\nlanguage keiro-dsl 1\n"++ it "rejects a future version before parsing an invalid v1 body" $+ case parseSource "future.keiro" "language keiro-dsl 5\nthis is not a v2 body\n" of+ Left failure@(SourceLanguageFailure diagnostic) -> do+ sourceLanguageErrorCode diagnostic `shouldBe` UnsupportedLanguageVersion+ renderParseFailure failure `shouldSatisfy` T.isInfixOf "supported versions: 1, 2, 3, 4"+ renderParseFailure failure `shouldNotSatisfy` T.isInfixOf "expecting `context`"+ other -> expectationFailure ("expected source-language failure, got " <> show other)++ it "accepts and canonically round-trips nominal declarations only in v2" $ do+ let nominalSource =+ T.unlines+ [ "language keiro-dsl 2",+ "context orders",+ "id OrderId prefix=ord using {",+ " haskell package=orders-domain module=Orders.Id type=OrderId",+ " binding = \"Orders.KeiroBindings.orderIdBinding\"",+ " binding-version = \"1\"",+ " canonical-type = \"orders.OrderId.v1\"",+ " fixtures = \"Orders.KeiroBindings.orderIdFixtures\"",+ "}",+ "enum OrderStatus { Draft=draft Submitted=submitted } using {",+ " haskell package=orders-domain module=Orders.Order type=OrderStatus",+ " binding = \"Orders.KeiroBindings.orderStatusBinding\"",+ " binding-version = \"1\"",+ " canonical-type = \"orders.OrderStatus.v1\"",+ " fixtures = \"Orders.KeiroBindings.orderStatusFixtures\"",+ "}",+ "mapped nominal AccountNumber : Text {",+ " haskell package=orders-domain module=Orders.Account type=AccountNumber",+ " binding = \"Orders.KeiroBindings.accountNumberBinding\"",+ " binding-version = \"1\"",+ " canonical-type = \"orders.AccountNumber.v1\"",+ " fixtures = \"Orders.KeiroBindings.accountNumberFixtures\"",+ " initial = \"Orders.KeiroBindings.initialAccountNumber\"",+ "}"+ ]+ parsed <- parseRight "nominal.keiro" nominalSource+ length (specIds (parsedSpec parsed)) `shouldBe` 1+ length (specEnums (parsedSpec parsed)) `shouldBe` 1+ length (specNominalScalars (parsedSpec parsed)) `shouldBe` 1+ parseSource "nominal-round-trip.keiro" (renderSource parsed) `shouldBe` Right parsed++ it "reports successor nominal syntax as one language-version diagnostic under v1 and legacy" $ do+ let body = "context orders\nmapped nominal AccountNumber : Text {}\n"+ failureCode ("language keiro-dsl 1\n" <> body) `shouldBe` Just LanguageFeatureRequiresVersion+ failureCode body `shouldBe` Just LanguageFeatureRequiresVersion++ it "parses and canonically round-trips field aliases only in language 4" $ do+ let v4Source =+ T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change { type haskell payloadType as \"type\":Text haskell as }",+ "contract publicOrder {",+ " schemaVersion 1",+ " discriminator kind",+ " topic changes \"orders.v1\"",+ " event Changed on changes {",+ " region haskell serviceRegion as \"region_code\": text",+ " }",+ "}"+ ]+ v3Source = T.replace "language keiro-dsl 4" "language keiro-dsl 3" v4Source+ parsed <- parseRight "field-aliases.keiro" v4Source+ parseSource "field-aliases-round-trip.keiro" (renderSource parsed) `shouldBe` Right parsed+ case specNodes (parsedSpec parsed) of+ [NAggregate aggregate, NContract contract] -> do+ case cmdFields =<< aggCommands aggregate of+ aliased : haskellField : asField : _ -> do+ (aggregateFieldName aliased, aggregateFieldSelector aliased, aggregateFieldWireKey aliased)+ `shouldBe` ("type", Just "payloadType", Just "type")+ map aggregateFieldName [haskellField, asField] `shouldBe` ["haskell", "as"]+ fields -> expectationFailure ("unexpected aggregate alias fields: " <> show fields)+ case ceFields =<< ctrEvents contract of+ [field] ->+ (cfName field, cfSelector field, cfWireKey field, cfLoc field)+ `shouldBe` ("region", Just "serviceRegion", Just "region_code", Loc 12)+ fields -> expectationFailure ("unexpected contract alias fields: " <> show fields)+ nodes -> expectationFailure ("unexpected alias nodes: " <> show nodes)+ failureCode v3Source `shouldBe` Just LanguageFeatureRequiresVersion++ it "attributes every successor feature gate to its owning grammar production" $ do+ let featureFailureAt expectedLine source =+ case parseSource "feature.keiro" source of+ Left (SourceLanguageFailure diagnostic) -> do+ sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion+ unLoc (sourceLanguageLoc diagnostic) `shouldBe` expectedLine+ other -> expectationFailure ("expected a located feature gate, got " <> show other)+ aggregateWith clause =+ T.unlines+ [ "language keiro-dsl 1",+ "context feature-gates",+ "aggregate Account",+ " regs",+ " balance Text = \"0\"",+ " states Open",+ " command Adjust { amount:Text }",+ " event Adjusted = fields(Adjust)",+ " Open -- Adjust --> " <> clause <> " ; emit Adjusted ; goto Open"+ ]+ featureFailureAt 3 (T.unlines ["language keiro-dsl 1", "context feature-gates", "id AccountId prefix=acct using {"])+ featureFailureAt 3 (T.unlines ["language keiro-dsl 1", "context feature-gates", "mapped nominal AccountNumber : Text {}"])+ featureFailureAt 5 (T.unlines ["language keiro-dsl 1", "context feature-gates", "aggregate Account", " regs", " balance Integer = 0", " states Open"])+ featureFailureAt 9 (aggregateWith "guard reg.balance == cmd.amount")+ featureFailureAt 9 (aggregateWith "implementation hole")++ it "reports a declaration-only rewrite without semantic, generated, fold, or replay impact" $ do+ fixture <- readTestText "test/fixtures/language-v1.keiro"+ let legacyFixture = T.unlines (drop 1 (T.lines fixture))+ legacySource <- parseRight "legacy.keiro" legacyFixture+ declaredSource <- parseRight "declared.keiro" fixture+ let oldSpec = parsedSpec legacySource+ newSpec = parsedSpec declaredSource+ changes = diffSources legacySource declaredSource+ vectors = [ckVector kind | change <- changes, let kind = workspaceChangeKind change]+ map changeCode changes `shouldBe` [SourceLanguageDeclarationChanged]+ legacyDiffSpecs oldSpec newSpec `shouldBe` []+ vectors `shouldSatisfy` all (\compatibility -> all ((== VCompatible) . (`verdictFor` compatibility)) [minBound .. maxBound])+ case changes of+ [change] ->+ remediationFor (ckContext (workspaceChangeKind change)) SourceLanguageDeclarationChanged+ `shouldBe` (RemedyNoSemanticAction :| [])+ _ -> expectationFailure "expected one source-language change"+ let legacyGeneratedSurface spec =+ [ (modulePath scaffoldModule, moduleText scaffoldModule, kind scaffoldModule)+ | scaffoldModule <- scaffoldModules (defaultContext (specContext spec)) spec+ ]+ legacyFoldFingerprint spec aggregate = aggregateFoldFingerprintForService (legacyCheckedService spec) aggregate+ legacyGeneratedSurface oldSpec `shouldBe` legacyGeneratedSurface newSpec+ [legacyFoldFingerprint oldSpec aggregate | NAggregate aggregate <- specNodes oldSpec]+ `shouldBe` [legacyFoldFingerprint newSpec aggregate | NAggregate aggregate <- specNodes newSpec]+ legacyReplayImpactSpecs oldSpec newSpec `shouldBe` ReplayNeutral++ it "exposes stable JSON inspection for a source and canonically ordered workspace members" $ do+ (sourceCode, sourceOut, sourceErr) <- runKeiroDsl ["inspect", "test/fixtures/reservation.keiro", "--format=json"]+ sourceCode `shouldBe` ExitSuccess+ sourceErr `shouldBe` ""+ sourceOut `shouldContain` "\"schema\":\"keiro-dsl/source-inspection/1\""+ sourceOut `shouldContain` "\"kind\":\"source\""+ sourceOut `shouldContain` "\"sourceForm\":\"declared\""+ sourceOut `shouldContain` "\"declaredLanguageVersion\":4"+ sourceOut `shouldContain` "\"effectiveLanguageVersion\":4"+ sourceOut `shouldContain` "\"effectiveSemanticContract\":{"+ sourceOut `shouldContain` "\"runtimeSemantics\":\"keiro-dsl/runtime-semantics/3\""+ sourceOut `shouldContain` "\"languageSupport\":\"stable\""+ (workspaceCode, workspaceOut, workspaceErr) <- runKeiroDsl ["inspect", canonicalWorkspacePath, "--format=json"]+ workspaceCode `shouldBe` ExitSuccess+ workspaceErr `shouldBe` ""+ workspaceOut `shouldContain` "\"kind\":\"workspace\""+ workspaceOut `shouldContain` "\"service\":\"demo-project\""+ workspaceOut `shouldContain` "\"effectiveSemanticContract\":{"+ workspaceOut `shouldContain` "\"languageSupport\":\"stable\""+ workspaceOut `shouldSatisfy` orderedSubstrings ["domain/project-artifact.keiro", "domain/project.keiro", "domain/shared.keiro"]++ it "keeps only the named source-version compatibility fixtures outside stable v4" $ do+ fixtureTree <- treeSnapshot "test/fixtures"+ let outsideStableV4 =+ sort+ [ path+ | (path, contents) <- fixtureTree,+ takeExtension path == ".keiro",+ "language keiro-dsl 4" `notElem` T.lines contents+ ]+ outsideStableV4+ `shouldBe` sort+ [ "aggregate-collection-expressions-v2-rejects.keiro",+ "aggregate-scalar-expressions-v1-rejects.keiro",+ "contract-v1-compat.keiro",+ "id-domain-migration-v3.keiro",+ "language-duplicate.keiro",+ "language-future.keiro",+ "language-identifier-v1.keiro",+ "language-identifier-v2.keiro",+ "language-legacy.keiro",+ "language-malformed.keiro",+ "language-misplaced.keiro",+ "language-v1.keiro",+ "language-zero.keiro",+ "nominal-v1.keiro"+ ]++ it "checks v1, rejects a future contract once, and inspects legacy explicitly" $ do+ (v1Code, v1Out, v1Err) <- runKeiroDsl ["check", "test/fixtures/language-v1.keiro"]+ v1Code `shouldBe` ExitSuccess+ v1Out `shouldBe` "OK\n"+ v1Err `shouldContain` "language contract: effective keiro-dsl 1 (declared, compatibility-only, runtime semantics keiro-dsl/runtime-semantics/1)"+ (futureCode, _, futureErr) <- runKeiroDsl ["check", "test/fixtures/language-future.keiro"]+ futureCode `shouldBe` ExitFailure 1+ T.count "UnsupportedLanguageVersion" (T.pack futureErr) `shouldBe` 1+ futureErr `shouldContain` "supported versions: 1, 2, 3, 4"+ futureErr `shouldNotContain` "expecting `context`"+ (legacyCode, legacyOut, legacyErr) <- runKeiroDsl ["inspect", "test/fixtures/language-legacy.keiro", "--format=json"]+ legacyCode `shouldBe` ExitSuccess+ legacyErr `shouldBe` ""+ legacyOut `shouldContain` "\"sourceForm\":\"legacy-unversioned\""+ legacyOut `shouldContain` "\"declaredLanguageVersion\":null"+ legacyOut `shouldContain` "\"effectiveLanguageVersion\":1"+ legacyOut `shouldContain` "\"languageSupport\":\"compatibility-only\""++ it "checks and scaffolds contextual language identifiers through the CLI" $ do+ forM_ ["language-identifier-v1.keiro", "language-identifier-v2.keiro"] $ \fixture -> do+ let sourcePath = "test/fixtures/" <> fixture+ (checkCode, checkOut, checkErr) <- runKeiroDsl ["check", sourcePath]+ checkCode `shouldBe` ExitSuccess+ checkOut `shouldBe` "OK\n"+ checkErr `shouldContain` "language contract: effective keiro-dsl"+ withTempDirectory ("keiro-dsl-" <> fixture) $ \out -> do+ (scaffoldCode, _, scaffoldErr) <- runKeiroDsl ["scaffold", sourcePath, "--out", out]+ scaffoldCode `shouldBe` ExitSuccess+ scaffoldErr `shouldContain` "language contract: effective keiro-dsl"+ scaffoldErr `shouldContain` "firewall: OK"++ it "notices only the working-tree contract during diff" $ do+ (diffCode, _, diffErr) <- runKeiroDsl ["diff", "test/fixtures/language-v1.keiro", "--since", "HEAD"]+ diffCode `shouldBe` ExitSuccess+ T.count "language contract:" (T.pack diffErr) `shouldBe` 1+ diffErr `shouldContain` "language contract: effective keiro-dsl 1 (declared, compatibility-only, runtime semantics keiro-dsl/runtime-semantics/1)"++ it "preserves a workspace member's source-selection code beneath outer attribution" $ do+ let manifest = "service demo\nspec domain/future.keiro\n"+ futureSource = "language keiro-dsl 5\nthis body must not parse\n"+ source = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/future.keiro", futureSource)])+ loaded <- loadWorkspace source "service.keiro-workspace"+ case loaded of+ Left (WorkspaceRefused (diagnostic :| [])) -> do+ wdCode diagnostic `shouldBe` WorkspaceMemberParseFailed+ sourceLanguageErrorCode <$> wdSourceLanguageCause diagnostic+ `shouldBe` Just UnsupportedLanguageVersion+ renderWorkspaceDiagnostic "service.keiro-workspace" diagnostic+ `shouldSatisfy` T.isInfixOf "UnsupportedLanguageVersion"+ other -> expectationFailure ("expected one attributed source-language refusal, got " <> show other)++ it "attributes a workspace provenance-only diff to the changed member" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ case wsMembers workspace of+ firstMember : remaining -> do+ let changedMember = firstMember {wmSourceLanguage = LegacyUnversioned}+ changedWorkspace = workspace {wsMembers = changedMember : remaining}+ changes = diffWorkspaces workspace changedWorkspace+ map (changeCode . wcChange) changes `shouldBe` [SourceLanguageDeclarationChanged]+ map (fmap osFile . wcDeclarationSite) changes `shouldBe` [Just (wmPath firstMember)]+ map wcChange changes `shouldSatisfy` all (not . gatedBreaking (gateWith [minBound .. maxBound]))+ _ -> expectationFailure "canonical workspace had no member"++ describe "ID domain" $ do+ let parseRight name source = case parseSource name source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value++ it "registers language 3 as the first enforced runtime contract" $ do+ parsed <- case parseSource "id-domain-v3.keiro" "language keiro-dsl 3\ncontext id-domain\n" of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let contract = checkedLanguageContract (checkedSource parsed)+ effectiveRuntimeSemantics contract `shouldBe` "keiro-dsl/runtime-semantics/2"+ effectiveContractLanguageVersion contract `shouldBe` maybe (error "missing v3") id (languageVersion 3)+ idDomainContractFor contract "req" `shouldSatisfy` (/= Nothing)++ it "registers language 4 as contract admission semantics without changing aggregate ID admission" $ do+ v3 <- parseRight "id-domain-v3.keiro" "language keiro-dsl 3\ncontext id-domain\n"+ v4 <- parseRight "id-domain-v4.keiro" "language keiro-dsl 4\ncontext id-domain\n"+ let v3Contract = checkedLanguageContract (checkedSource v3)+ v4Contract = checkedLanguageContract (checkedSource v4)+ effectiveRuntimeSemantics v4Contract `shouldBe` "keiro-dsl/runtime-semantics/3"+ effectiveContractLanguageVersion v4Contract `shouldBe` maybe (error "missing v4") id (languageVersion 4)+ idDomainContractFor v4Contract "req" `shouldBe` idDomainContractFor v3Contract "req"+ contractIdDomainContractFor v3Contract "req" `shouldBe` Nothing+ contractIdDomainContractFor v4Contract "req" `shouldBe` Just (typeIdV7Domain "req")++ it "constructs typed KindIDs only after the frozen four-way admission policy" $ do+ let valid = "req_01h455vb4pex5vsknk084sn02q"+ uppercase = "req_01H455VB4PEX5VSKNK084SN02Q"+ nonV7 = "req_00041061050r3gg28a1c60t3gf"+ (KindID.toText @"req" <$> parseKindIdV7Text @"req" valid) `shouldBe` Right valid+ parseKindIdV7Text @"req" "req-1" `shouldSatisfy` \case+ Left IdDomainMalformed {} -> True+ _ -> False+ parseKindIdV7Text @"req" "other_01h455vb4pex5vsknk084sn02q" `shouldSatisfy` \case+ Left (IdDomainWrongPrefix "req" "other") -> True+ _ -> False+ parseKindIdV7Text @"req" uppercase `shouldBe` Left IdDomainNonCanonical+ parseKindIdV7Text @"req" nonV7 `shouldSatisfy` \case+ Left IdDomainNotUuidV7 {} -> True+ _ -> False+ parseEither (parseKindIdV7Value @"req") (Aeson.String uppercase)+ `shouldSatisfy` \case+ Left problem -> "not canonical lowercase" `T.isInfixOf` T.pack problem+ Right _ -> False++ it "validates contract TypeID prefixes only at the language-4 boundary" $ do+ let source versionNumber =+ T.unlines+ [ "language keiro-dsl " <> T.pack (show versionNumber),+ "context invalid-contract-prefix",+ "contract emergency {",+ " schemaVersion 1",+ " discriminator messageType",+ " topic incidentEvents \"emergency.incident.events\"",+ " event IncidentDeclared on incidentEvents {",+ " incidentId: typeid \"Bad\"",+ " }",+ "}"+ ]+ v3 <- parseRight "contract-prefix-v3.keiro" (source (3 :: Int))+ v4 <- parseRight "contract-prefix-v4.keiro" (source (4 :: Int))+ validateService (checkedSource v3) `shouldBe` []+ case validateService (checkedSource v4) of+ [diagnostic] -> do+ code diagnostic `shouldBe` ContractInvalidTypeIdPrefix+ line diagnostic `shouldBe` 8+ message diagnostic `shouldSatisfy` T.isInfixOf "contract 'emergency' event 'IncidentDeclared' field 'incidentId'"+ message diagnostic `shouldSatisfy` T.isInfixOf "invalid TypeID prefix 'Bad'"+ diagnostics -> expectationFailure ("expected one invalid contract prefix diagnostic, got " <> show diagnostics)++ it "keeps version-3 and version-4 aggregate fold and replay semantics equal" $ do+ v3Text <- readTestText "test/fixtures/id-domain-migration-v3.keiro"+ v3 <- parseRight "fold-v3.keiro" v3Text+ v4 <- parseRight "fold-v4.keiro" (T.replace "language keiro-dsl 3" "language keiro-dsl 4" v3Text)+ let v3Service = checkedSource v3+ v4Service = checkedSource v4+ fingerprints service =+ [ aggregateFoldFingerprintForService service aggregate+ | NAggregate aggregate <- specNodes (checkedSpec service)+ ]+ fingerprints v4Service `shouldBe` fingerprints v3Service+ diffServices v3Service v4Service `shouldBe` []+ resolvedFold (ReplayImpact.replayImpactServices v3Service v4Service) `shouldBe` ReplayNeutral++ it "keeps runtime validation and the exact Keiki text image in agreement" $ do+ let contract = typeIdV7Domain "req"+ sampleText = idDomainSampleText contract+ suffix = T.drop (T.length "req_") sampleText+ replaceAt position replacement value =+ T.take position value <> T.singleton replacement <> T.drop (position + 1) value+ accepted =+ [ sampleText,+ replaceAt (T.length "req_" + 10) 'f' sampleText,+ replaceAt (T.length "req_" + 13) 'v' sampleText+ ]+ rejected =+ [ "",+ "req_",+ "other_" <> suffix,+ "req__" <> suffix,+ T.dropEnd 1 sampleText,+ sampleText <> "0",+ T.toUpper sampleText,+ replaceAt (T.length "req_" + 0) '8' sampleText,+ replaceAt (T.length "req_" + 10) 'd' sampleText,+ replaceAt (T.length "req_" + 13) 'c' sampleText+ ]+ patternValue = either (error . show) id (idDomainTextPattern contract)+ idDomainVersion contract `shouldBe` "keiro-dsl/id-domain/typeid-v7/1"+ idDomainSeparator contract `shouldBe` '_'+ idDomainSuffixLength contract `shouldBe` 26+ idDomainMaxLength contract `shouldBe` T.length sampleText+ forM_ accepted $ \value -> do+ validateIdDomainText contract value `shouldBe` Right ()+ matchesTextPattern patternValue value `shouldBe` True+ forM_ rejected $ \value -> do+ validateIdDomainText contract value `shouldSatisfy` isLeft+ matchesTextPattern patternValue value `shouldBe` False++ it "agrees for generated canonical and malformed domain values" $ property $ do+ let crockford = "0123456789abcdefghjkmnpqrstvwxyz"+ segment count = vectorOf count (elements crockford)+ leading <- elements "01234567"+ beforeVersion <- segment 9+ version <- elements "ef"+ beforeVariant <- segment 2+ variantDigit <- elements "89abrstv"+ afterVariant <- segment 12+ let value = T.pack ("req_" <> [leading] <> beforeVersion <> [version] <> beforeVariant <> [variantDigit] <> afterVariant)+ contract = typeIdV7Domain "req"+ patternValue = either (error . show) id (idDomainTextPattern contract)+ invalidValues = [T.toUpper value, "other_" <> T.drop 4 value, T.dropEnd 1 value, value <> "0"]+ pure $+ conjoin+ ( counterexample (T.unpack value) (validateIdDomainText contract value == Right () && matchesTextPattern patternValue value)+ : [counterexample (T.unpack invalid) (isLeft (validateIdDomainText contract invalid) && not (matchesTextPattern patternValue invalid)) | invalid <- invalidValues]+ )++ it "enforces the same contract before consumer binding conversion and explains its version" $ do+ v2Source <- readTestText "test/fixtures/nominal-scalars.keiro"+ let v3Text = T.replace "language keiro-dsl 2" "language keiro-dsl 3" v2Source+ parsed <- case parseSource "nominal-scalars-v3.keiro" v3Text of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let service = checkedSource parsed+ spec = checkedSpec service+ modules = scaffoldServiceModules (defaultContext (specContext spec)) service+ generatedText suffix = case [moduleText value | value <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath value)] of+ [value] -> value+ values -> error ("expected one generated module ending in " <> suffix <> ", got " <> show (length values))+ codecModule = generatedText "NominalLedger/Codec.hs"+ projectionModule = generatedText "NominalProjections.hs"+ harnessModule = generatedText "NominalLedger/Harness.hs"+ validateService service `shouldBe` []+ codecModule `shouldSatisfy` T.isInfixOf "case validateIdDomainText (typeIdV7Domain \"ord\") input of"+ codecModule `shouldSatisfy` T.isInfixOf "Right () -> case KindID.parseText @\"ord\" input of"+ projectionModule `shouldSatisfy` T.isInfixOf "idDomainTextPattern (typeIdV7Domain \"ord\")"+ projectionModule `shouldSatisfy` T.isInfixOf "validateIdDomainText (typeIdV7Domain \"ord\") value"+ harnessModule `shouldSatisfy` T.isInfixOf "nominal ID binding preserves canonical representations: OrderId"+ harnessModule `shouldSatisfy` T.isInfixOf "nominal ID boundary rejects wrong-prefix and normalized text: OrderId"+ obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligationsForService service)+ let orderIdBindings = [obligation | obligation <- obligations, obligationMappedName obligation == "OrderId", obligationKind obligation == BindingValue]+ map obligationIdDomainContract orderIdBindings `shouldBe` [Just "keiro-dsl/id-domain/typeid-v7/1"]+ renderBindingObligations (specContext spec) obligations+ `shouldSatisfy` T.isInfixOf "id-domain-contract: \"keiro-dsl/id-domain/typeid-v7/1\""++ it "reports adoption by boundary, invalidates snapshots, and preserves replay compatibility" $ do+ v2Text <- readTestText "test/fixtures/id-domain-migration-v3.keiro"+ let oldText = T.replace "language keiro-dsl 3" "language keiro-dsl 2" v2Text+ oldSource <- case parseSource "id-domain-migration-v2.keiro" oldText of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ newSource <- case parseSource "id-domain-migration-v3.keiro" v2Text of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let oldService = checkedSource oldSource+ newService = checkedSource newSource+ changes = diffSources oldSource newSource+ findings = [kindOfChange change | change <- changes, changeCode change == IdDomainContractChanged]+ length findings `shouldBe` 1+ forM_ findings $ \finding -> do+ verdictFor PrivateHistoryRead (ckVector finding) `shouldBe` VCompatible+ verdictFor OldBinaryReadNewEvents (ckVector finding) `shouldBe` VCompatible+ verdictFor SnapshotHydration (ckVector finding) `shouldBe` VAdvisory+ verdictFor PublicConsumer (ckVector finding) `shouldBe` VBreaking+ verdictFor PersistedIdentity (ckVector finding) `shouldBe` VCompatible+ verdictFor ConsumerBuild (ckVector finding) `shouldBe` VAdvisory+ ckDetail finding `shouldSatisfy` T.isInfixOf "historical event replay retains its legacy decoder"+ remediationFor (ckContext finding) (ckCode finding)+ `shouldBe` RemedyDeploymentOrder RolloutProducerLast :| [RemedyStateCodecBump, RemedyRecompileConsumers, RemedyRunConformance]+ [ckDetail finding | change <- changes, changeCode change == SourceLanguageDeclarationChanged, let finding = kindOfChange change]+ `shouldSatisfy` all (T.isInfixOf "effective runtime semantics changed")+ idDomainIdentitiesForService oldService `shouldBe` []+ idDomainIdentitiesForService newService+ `shouldSatisfy` any (T.isInfixOf "contract=keiro-dsl/id-domain/typeid-v7/1")+ resolvedFold (ReplayImpact.replayImpactServices oldService newService) `shouldSatisfy` \case+ ReplayImpact.ReplayAffected impacts ->+ maybe False includeSnapshotStreams (Map.lookup "OrderBook" impacts)+ ReplayImpact.ReplayNeutral -> False++ it "keeps the raw constructor outside the compiled public module surface" $+ withTempDirectory "keiro-dsl-id-domain-hidden-constructor" $ \out -> do+ sourceText <- readTestText "test/fixtures/id-domain-migration-v3.keiro"+ parsed <- case parseSource "id-domain-migration-v3.keiro" sourceText of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ modules = scaffoldServiceModules ctx service+ attempt = out </> "Attempt.hs"+ ghcOutput = out </> ".ghc"+ result <- executeServiceScaffold out False "id-domain-migration-v3.keiro" (parsedSourceLanguage parsed) ctx service modules+ result `shouldSatisfy` isRight+ recordContents <- TIO.readFile (out </> recordFileName (specContext spec))+ record <- case parseRecord recordContents of+ Nothing -> expectationFailure "generated ID-domain scaffold record did not parse" >> fail "unreachable"+ Just value -> pure value+ recIdDomains record `shouldBe` idDomainIdentitiesForService service+ recNominalEqualities record+ `shouldSatisfy` any (T.isInfixOf "keiro-dsl/id-domain/typeid-v7/1")+ createDirectoryIfMissing True ghcOutput+ TIO.writeFile+ attempt+ ( T.unlines+ [ "module Attempt where",+ "import Generated.IdDomainMigration.Nominals (OrderId (..))",+ "bad :: OrderId",+ "bad = OrderId \"ord_LEGACY-NOT-TYPEID\""+ ]+ )+ (exitCode, standardOutput, standardError) <-+ readProcessWithExitCode+ "cabal"+ [ "exec",+ "--",+ "ghc",+ "-XGHC2024",+ "-XOverloadedStrings",+ "-fno-code",+ "-fforce-recomp",+ "-outputdir",+ ghcOutput,+ "-i" <> out,+ attempt+ ]+ ""+ exitCode `shouldSatisfy` (/= ExitSuccess)+ (standardOutput <> standardError) `shouldContain` "OrderId"++ it "emits one enforced nominal owner for a version-3 workspace" $ do+ manifest <- readTestText "test/fixtures/workspace-nominals/service.keiro-workspace"+ shared <- readTestText "test/fixtures/workspace-nominals/domain/shared.keiro"+ project <- readTestText "test/fixtures/workspace-nominals/domain/project.keiro"+ artifact <- readTestText "test/fixtures/workspace-nominals/domain/project-artifact.keiro"+ let v3 = T.replace "language keiro-dsl 2" "language keiro-dsl 3"+ source =+ memoryContentSource+ ( Map.fromList+ [ ("service.keiro-workspace", manifest),+ ("domain/shared.keiro", v3 shared),+ ("domain/project.keiro", v3 project),+ ("domain/project-artifact.keiro", v3 artifact)+ ]+ )+ loaded <- loadWorkspace source "service.keiro-workspace"+ workspace <- case loaded of+ Left failure -> expectationFailure (show failure) >> fail "unreachable"+ Right value -> pure value+ plan <- case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right value -> pure value+ let paths = map (modulePath . fst) (wpModules plan)+ length (filter (== "Generated/WorkspaceNominalProof/Nominals.hs") paths) `shouldBe` 1+ length (filter (== "Generated/WorkspaceNominalProof/Nominals/Internal.hs") paths) `shouldBe` 1+ forM_ [moduleText value | (value, _) <- wpModules plan, "/Domain.hs" `T.isSuffixOf` T.pack (modulePath value)] $ \domainText ->+ domainText `shouldSatisfy` (not . T.isInfixOf "ProjectId (..)")+ withTempDirectory "keiro-dsl-v3-workspace-record" $ \out -> do+ emitted <- executeWorkspaceScaffold out False plan+ emitted `shouldSatisfy` isRight+ recordContents <- TIO.readFile (out </> workspaceRecordFileName (wsService workspace))+ record <- case parseWorkspaceRecord recordContents of+ Nothing -> expectationFailure "version-3 workspace record did not parse" >> fail "unreachable"+ Just value -> pure value+ wrIdDomains record `shouldBe` idDomainIdentitiesForService (wpCheckedService plan)+ wrNominalEqualities record+ `shouldSatisfy` any (T.isInfixOf "keiro-dsl/id-domain/typeid-v7/1")++ it "emits an abstract public ID, an internal legacy seam, and exact equality" $ do+ v2Source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ let v3Source = T.replace "language keiro-dsl 2" "language keiro-dsl 3" v2Source+ parsed <- case parseSource "aggregate-scalar-expressions-v3.keiro" v3Source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let service = checkedSource parsed+ spec = checkedSpec service+ modules = scaffoldServiceModules (defaultContext (specContext spec)) service+ moduleAt path = case [value | value <- modules, modulePath value == path] of+ [value] -> pure value+ values -> expectationFailure ("expected one module at " <> path <> ", got " <> show (map modulePath values)) >> fail "unreachable"+ validateService service `shouldBe` []+ publicNominals <- moduleAt "Generated/AggregateScalarExpressions/Nominals.hs"+ internalNominals <- moduleAt "Generated/AggregateScalarExpressions/Nominals/Internal.hs"+ domainModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Domain.hs"+ codecModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Codec.hs"+ transducerModule <- moduleAt "Generated/AggregateScalarExpressions/ScalarAccount/Transducer.hs"+ moduleText publicNominals `shouldSatisfy` T.isInfixOf "parseRequestId"+ moduleText publicNominals `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection RequestIdEqualityProjection"+ moduleText publicNominals `shouldSatisfy` T.isInfixOf "idDomainTextPattern (typeIdV7Domain \"req\")"+ moduleText publicNominals `shouldSatisfy` (not . T.isInfixOf "unsafeRequestIdFromLegacyText")+ moduleText publicNominals `shouldSatisfy` (not . T.isInfixOf "newtype RequestId")+ moduleText internalNominals `shouldSatisfy` T.isInfixOf "newtype RequestId = RequestId Text"+ moduleText internalNominals `shouldSatisfy` T.isInfixOf "unsafeRequestIdFromLegacyText"+ moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "RequestId (..)")+ moduleText codecModule `shouldSatisfy` T.isInfixOf "unsafeRequestIdFromLegacyText <$>"+ moduleText transducerModule `shouldSatisfy` T.isInfixOf "case parseRequestId"+ firewallBreaches modules `shouldBe` []++ describe "scalar expressions" $ do+ it "parses, validates, and round-trips the authoritative stable scalar fixture" $ do+ source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ parsed <- case parseSource "aggregate-scalar-expressions-v2.keiro" source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ validateSpec (parsedSpec parsed) `shouldBe` []+ parseSource "round-trip.keiro" (renderSource parsed) `shouldBe` Right parsed+ case [aggregate | NAggregate aggregate <- specNodes (parsedSpec parsed)] of+ [aggregate] -> case aggTransitions aggregate of+ transition : holeTransition : [] -> do+ tImplementation transition `shouldBe` GeneratedImplementation+ tImplementation holeTransition `shouldBe` HoleImplementation+ let environment = expressionEnvironment (parsedSpec parsed) aggregate transition+ case lookup "reserved" (tWrites transition) >>= either (const Nothing) Just . resolveWriteExpr environment "reserved" of+ Just resolved -> do+ typedScalarType resolved `shouldBe` AggregateNatural+ show (typedScalarNode resolved) `shouldContain` "TotalNaturalArithmetic"+ Nothing -> expectationFailure "reserved write did not resolve"+ let service = checkedSource parsed+ modules = scaffoldServiceModules (defaultContext (specContext (parsedSpec parsed))) service+ transducer = generatedTextEndingIn "Transducer.hs" modules+ holes = holeTextEndingIn "Holes.hs" modules+ surface = aggregateFoldSurfaceForService service aggregate+ manifest = renderManifestForService "aggregate-scalar-expressions-v2.keiro" modules service+ readableTransducer = T.unwords (T.words transducer)+ aggregateFoldFingerprintForService service aggregate `shouldBe` "60f4f059f718b2ee2bca06360ea20221"+ T.lines surface+ `shouldBe` [ "semantic-contract:keiro-dsl/runtime-semantics/2",+ "state:Open|terminal=false",+ "state:Reviewed|terminal=false",+ "state:Closed|terminal=true",+ "reg:balance:Integer=0",+ "reg:reserved:Natural=0",+ "reg:capacity:Natural=5",+ "reg:machine:Int=0",+ "reg:label:Text=\"\"",+ "reg:active:Bool=False",+ "reg:mode:AccountMode=Normal",+ "reg:requestId:RequestId=placeholder",+ "reg:openedAt:Time=(UTCTime (fromGregorian 2026 1 1) (picosecondsToDiffTime 0))",+ "reg:limits:Limits=initial",+ "mapped-register:Limits|wire=4463db782a5b9924|canonical=scalar-expressions.Limits.v1|binding=ScalarExpressions.Bindings.limitsBinding|binding-version=1|initial=ScalarExpressions.Bindings.initialLimits",+ "nominal-equality-use:nominal-equality|name=AccountMode|contract=keiro-dsl/nominal-equality/1|key=Text|domain=finite-text:normal,restricted|owner=generated",+ "nominal-equality-use:nominal-equality|name=RequestId|contract=keiro-dsl/nominal-equality/2|key=Text|domain=typeid-v7-text:req:keiro-dsl/id-domain/typeid-v7/1|owner=generated",+ "transition:live|Open|Adjust|implementation=generated|guard=cmd.balance + reg.balance >= -100 && reg.reserved + cmd.requested <= reg.capacity && cmd.observedAt >= reg.openedAt && cmd.limits.minimum >= reg.limits.minimum && cmd.active == false && cmd.mode == reg.mode && cmd.requestId == reg.requestId|writes=balance:=reg.balance + cmd.balance * 2;reserved:=reg.reserved + (cmd.requested - reg.capacity);machine:=-7;label:=\"adjusted\";active:=true;mode:=AccountMode.Restricted;requestId:=RequestId(\"req_01h455vb4pex5vsknk084sn02q\");openedAt:=\"2026-02-03T04:05:06Z\";limits:=cmd.limits|emits=Adjusted|outputs=Adjusted=generated-command-identity:Adjust[balance=balance:Integer,requested=requested:Natural,machine=machine:Int,label=label:Text,active=active:Bool,mode=mode:AccountMode,requestId=requestId:RequestId,observedAt=observedAt:Time,limits=limits:Limits]|goto=Reviewed",+ "transition:live|Reviewed|Close|implementation=hole|guard=|writes=|emits=ClosedEvent|outputs=ClosedEvent=generated-command-identity:Close[balance=balance:Integer]|goto=Closed"+ ]+ diffServices service service `shouldBe` []+ resolvedFold (ReplayImpact.replayImpactServices service service) `shouldBe` ReplayNeutral+ manifest `shouldSatisfy` (not . T.isInfixOf "Generated.AggregateScalarExpressions.ScalarAccount.Expressions")+ map modulePath modules `shouldSatisfy` all (not . T.isSuffixOf "Expressions.hs" . T.pack)+ map modulePath modules `shouldSatisfy` any (T.isSuffixOf "Transducer.hs" . T.pack)+ transducer `shouldSatisfy` T.isInfixOf "let commandLimitsMinimum = K.inpProj"+ transducer `shouldSatisfy` T.isInfixOf "registerLimitsMinimum = K.regProj"+ readableTransducer `shouldSatisfy` T.isInfixOf "B.requireGuard $ (((((d.balance .+ B.reg @\"balance\" .>= K.lit (-100 :: Integer) .&& B.reg @\"reserved\" .+ d.requested .<= B.reg @\"capacity\") .&& d.observedAt .>= B.reg @\"openedAt\") .&& commandLimitsMinimum .>= registerLimitsMinimum) .&& d.active .== K.lit False) .&& commandMode .== registerMode) .&& commandRequestId .== registerRequestId"+ transducer `shouldSatisfy` T.isInfixOf "B.slot @\"balance\" =: (B.reg @\"balance\" .+ d.balance .* K.lit (2 :: Integer))"+ transducer `shouldSatisfy` T.isInfixOf "B.slot @\"reserved\" =: (B.reg @\"reserved\" .+ (d.requested .- B.reg @\"capacity\"))"+ transducer `shouldSatisfy` (not . T.isInfixOf "K.PAnd")+ transducer `shouldSatisfy` (not . T.isInfixOf "K.tadd")+ transducer `shouldSatisfy` T.isInfixOf "scalarAccountPredicateVerifications"+ transducer `shouldSatisfy` T.isInfixOf "S.verifyPredicate predicate"+ transducer `shouldSatisfy` T.isInfixOf "B.emit wireAdjusted (AdjustedTermFields"+ transducer `shouldSatisfy` T.isInfixOf "balance = d.balance"+ transducer `shouldSatisfy` (not . T.isInfixOf "transition1OpenAdjustOutput1Adjusted")+ holes `shouldSatisfy` (not . T.isInfixOf "transition1OpenAdjustOutput1Adjusted")+ holes `shouldSatisfy` (not . T.isInfixOf "transition2ReviewedCloseOutput1ClosedEvent")+ holes `shouldSatisfy` T.isInfixOf "transition2ReviewedCloseHoleFoldVersion"+ holes `shouldSatisfy` (not . T.isInfixOf "scalarAccountTransducer")+ firewallBreaches modules `shouldBe` []+ _ -> expectationFailure "expected one generated and one Hole scalar transition"+ _ -> expectationFailure "expected one scalar aggregate"++ it "pins every readable operator, equal-precedence child position, and bare Boolean guard" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context readable-renderer",+ "enum RenderStatus { Ready=ready Waiting=waiting }",+ "aggregate Renderer",+ " regs",+ " total Integer = 0",+ " leftNested Integer = 0",+ " product Integer = 1",+ " status RenderStatus = Ready",+ " states Open Closed!",+ " command Evaluate { left:Integer right:Integer third:Integer status:RenderStatus }",+ " event Evaluated = fields(Evaluate)",+ " Open -- Evaluate -->",+ " guard ((cmd.left < cmd.right || cmd.left <= cmd.right) || (cmd.left > cmd.right || cmd.left >= cmd.right))",+ " && (cmd.left == cmd.right && cmd.left != cmd.third)",+ " && cmd.status == RenderStatus.Waiting",+ " write total := reg.total + (cmd.left - cmd.right)",+ " write leftNested := (reg.leftNested + cmd.left) - cmd.right",+ " write product := cmd.left * (cmd.right * cmd.third)",+ " write status := RenderStatus.Ready",+ " emit Evaluated",+ " goto Closed",+ "aggregate BooleanRenderer",+ " regs",+ " enabled Bool = False",+ " states Open Closed!",+ " command Enable { enabled:Bool }",+ " event Enabled = fields(Enable)",+ " Open -- Enable -->",+ " guard cmd.enabled",+ " write enabled := cmd.enabled",+ " emit Enabled",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<readable-renderer>" source+ errorCodes spec `shouldBe` []+ let modules = scaffoldModules (defaultContext (specContext spec)) spec+ moduleAt suffix = case [moduleText value | value <- modules, T.pack suffix `T.isSuffixOf` T.pack (modulePath value)] of+ [value] -> pure value+ values -> expectationFailure ("expected one generated module ending in " <> suffix <> ", got " <> show (length values)) >> fail "unreachable"+ renderer <- moduleAt "/Renderer/Transducer.hs"+ booleanRenderer <- moduleAt "/BooleanRenderer/Transducer.hs"+ let normalizedRenderer = T.unwords (T.words renderer)+ normalizedBooleanRenderer = T.unwords (T.words booleanRenderer)+ renderer+ `shouldSatisfy` T.isInfixOf "import Keiki.Core (HsPred, SymTransducer, (.*), (.+), (.-), (.==), (./=), (.<), (.<=), (.>), (.>=), (.&&), (.||))"+ normalizedRenderer+ `shouldSatisfy` T.isInfixOf "(d.left .< d.right .|| d.left .<= d.right) .|| d.left .> d.right .|| d.left .>= d.right"+ normalizedRenderer+ `shouldSatisfy` T.isInfixOf ".&& d.left .== d.right .&& d.left ./= d.third"+ normalizedRenderer+ `shouldSatisfy` T.isInfixOf ".&& commandStatus .== K.lit (\"waiting\" :: Text)"+ renderer+ `shouldSatisfy` T.isInfixOf "B.slot @\"total\" =: (B.reg @\"total\" .+ (d.left .- d.right))"+ renderer+ `shouldSatisfy` T.isInfixOf "B.slot @\"leftNested\" =: (B.reg @\"leftNested\" .+ d.left .- d.right)"+ renderer+ `shouldSatisfy` T.isInfixOf "B.slot @\"product\" =: d.left .* (d.right .* d.third)"+ normalizedBooleanRenderer `shouldSatisfy` T.isInfixOf "B.requireGuard $ d.enabled .== K.lit True"+ firewallBreaches modules `shouldBe` []++ it "renders resolved command selectors in scalar and projected expressions" $ do+ source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ let aliasedSource =+ T.replace "active:Bool" "active haskell commandActive:Bool"+ . T.replace "mode:AccountMode" "mode haskell commandMode:AccountMode"+ . T.replace "requestId:RequestId" "requestId haskell commandRequestId:RequestId"+ . T.replace "limits:Limits" "limits haskell commandLimits:Limits"+ $ source+ service <- checkedServiceFromText "aggregate-scalar-expression-aliases.keiro" aliasedSource+ let spec = checkedSpec service+ transducer = generatedTextEndingIn "Transducer.hs" (scaffoldServiceModules (defaultContext (specContext spec)) service)+ validateService service `shouldBe` []+ transducer `shouldSatisfy` T.isInfixOf "d.commandActive"+ transducer `shouldSatisfy` T.isInfixOf "d.commandLimits"+ transducer `shouldSatisfy` T.isInfixOf "(#commandMode :: K.Index"+ transducer `shouldSatisfy` T.isInfixOf "(#commandRequestId :: K.Index"+ transducer `shouldSatisfy` T.isInfixOf "(#commandLimits :: K.Index"+ transducer `shouldSatisfy` (not . T.isInfixOf "d.active")+ transducer `shouldSatisfy` (not . T.isInfixOf "inCtorAdjust (#mode :: K.Index")+ transducer `shouldSatisfy` (not . T.isInfixOf "inCtorAdjust (#requestId :: K.Index")+ transducer `shouldSatisfy` (not . T.isInfixOf "inCtorAdjust (#limits :: K.Index")++ it "suffixes normalized projection-alias collisions deterministically" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context projection-alias-collision",+ "mapped structural record AliasCollision {",+ " haskell package=keiro-dsl module=Renderer.Domain type=AliasCollision",+ " binding = \"Renderer.Bindings.aliasCollisionBinding\"",+ " binding-version = \"1\"",+ " canonical-type = \"renderer.AliasCollision.v1\"",+ " fixtures = \"Renderer.Bindings.aliasCollisionCases\"",+ " initial = \"Renderer.Bindings.initialAliasCollision\"",+ " wire object constructor=AliasCollision unknown-fields=reject {",+ " dash as \"foo-bar\" : Integer required",+ " underscore as \"foo_bar\" : Integer required",+ " }",+ "}",+ "aggregate AliasRenderer",+ " regs",+ " values AliasCollision = initial",+ " states Open Closed!",+ " command Compare { values:AliasCollision }",+ " event Compared = fields(Compare)",+ " Open -- Compare -->",+ " guard cmd.values.dash == reg.values.dash",+ " && cmd.values.underscore == reg.values.underscore",+ " emit Compared",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<projection-alias-collision>" source+ errorCodes spec `shouldBe` []+ let transducer = generatedTextEndingIn "Transducer.hs" (scaffoldModules (defaultContext (specContext spec)) spec)+ transducer `shouldSatisfy` T.isInfixOf "let commandValuesFooBar = K.inpProj"+ transducer `shouldSatisfy` T.isInfixOf "registerValuesFooBar = K.regProj"+ transducer `shouldSatisfy` T.isInfixOf "commandValuesFooBar2 = K.inpProj"+ transducer `shouldSatisfy` T.isInfixOf "registerValuesFooBar2 = K.regProj"++ it "keeps evolution identity independent of module layout but sensitive to checked behavior" $ do+ source <- readTestText "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ original <- case parseSource "aggregate-scalar-expressions-v2.keiro" source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ changed <- case parseSource "aggregate-scalar-expressions-changed.keiro" (T.replace "cmd.active == false" "cmd.active == true" source) of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ case ( [aggregate | NAggregate aggregate <- specNodes (parsedSpec original)],+ [aggregate | NAggregate aggregate <- specNodes (parsedSpec changed)]+ ) of+ ([originalAggregate], [changedAggregate]) -> do+ let service = checkedSource original+ changedService = checkedSource changed+ prefixed = defaultContext (specContext (parsedSpec original))+ collocated = prefixed {moduleRoot = "Acme", placement = CollocatedLeaf}+ prefixedModules = scaffoldServiceModules prefixed service+ collocatedModules = scaffoldServiceModules collocated service+ originalSurface = aggregateFoldSurfaceForService service originalAggregate+ originalFingerprint = aggregateFoldFingerprintForService service originalAggregate+ map modulePath prefixedModules `shouldNotBe` map modulePath collocatedModules+ sum (map (T.length . moduleText) prefixedModules) `shouldSatisfy` (> 0)+ sum (map (T.length . moduleText) collocatedModules) `shouldSatisfy` (> 0)+ aggregateFoldSurfaceForService service originalAggregate `shouldBe` originalSurface+ aggregateFoldFingerprintForService service originalAggregate `shouldBe` originalFingerprint+ aggregateFoldSurfaceForService changedService changedAggregate `shouldNotBe` originalSurface+ aggregateFoldFingerprintForService changedService changedAggregate `shouldNotBe` originalFingerprint+ found -> expectationFailure ("expected one aggregate before and after behavior mutation, got " <> show (length (fst found), length (snd found)))++ it "rejects cross-command fields(Command) output before scaffolding" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context output-command-mismatch",+ "aggregate Account",+ " regs",+ " states Open Closed!",+ " command OpenAccount { accountId:Text }",+ " command CloseAccount { accountId:Text }",+ " event AccountOpened = fields(OpenAccount)",+ " Open -- CloseAccount --> emit AccountOpened ; goto Closed"+ ]+ spec <- parseInlineSpec "<output-command-mismatch>" source+ errorCodes spec `shouldContain` [EventOutputCommandMismatch]+ case [aggregate | NAggregate aggregate <- specNodes spec] of+ [aggregate] -> case aggTransitions aggregate of+ [transition] ->+ eventOutputMapping spec aggregate transition 1 "AccountOpened"+ `shouldBe` Left (OutputCommandMismatch "OpenAccount" "CloseAccount" "AccountOpened")+ _ -> expectationFailure "expected one transition"+ _ -> expectationFailure "expected one aggregate"++ it "rejects Int arithmetic and mixed numeric operands before scaffolding" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-errors",+ "aggregate Counter",+ " regs",+ " machine Int = 0",+ " exact Integer = 0",+ " states Open Closed!",+ " command Add { machine:Int exact:Integer }",+ " event Added = fields(Add)",+ " Open -- Add -->",+ " guard cmd.machine + 1 >= 0 && cmd.exact == cmd.machine",+ " emit Added",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<scalar-errors>" source+ errorCodes spec `shouldContain` [AggregateExpressionOperatorUnsupported, AggregateExpressionOperandTypeMismatch]++ it "rejects nominal type confusion and unqualified enum values at source checking" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context nominal-type-confusion",+ "id OrderId prefix=ord",+ "id UserId prefix=usr",+ "enum OrderStatus { Draft=draft Submitted=submitted }",+ "enum UserStatus { Active=active Disabled=disabled }",+ "aggregate Account",+ " regs",+ " orderId OrderId = placeholder",+ " status OrderStatus = Draft",+ " states Open Closed!",+ " command Compare { orderId:OrderId userId:UserId status:OrderStatus userStatus:UserStatus label:Text }",+ " event Compared = fields(Compare)",+ " Open -- Compare -->",+ " guard cmd.orderId == cmd.userId",+ " && cmd.status == cmd.userStatus",+ " && cmd.orderId == cmd.label",+ " && cmd.status == Draft",+ " emit Compared",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<nominal-type-confusion>" source+ let diagnostics = validateSpec spec+ length [() | diagnostic <- diagnostics, code diagnostic == AggregateExpressionOperandTypeMismatch]+ `shouldBe` 3+ errorCodes spec `shouldContain` [AggregateExpressionRootUnknown]+ T.unlines (map message diagnostics) `shouldSatisfy` T.isInfixOf "qualify"++ it "rejects machine-Int arithmetic at both platform bounds" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-int-bounds",+ "aggregate Counter",+ " regs",+ " machine Int = 0",+ " states Open Closed!",+ " command Set { machine:Int }",+ " event SetEvent = fields(Set)",+ " Open -- Set -->",+ " guard cmd.machine + 1 >= " <> T.pack (show (minBound :: Int)),+ " && cmd.machine - 1 <= " <> T.pack (show (maxBound :: Int)),+ " emit SetEvent",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<scalar-int-bounds>" source+ length [() | diagnostic <- validateSpec spec, code diagnostic == AggregateExpressionOperatorUnsupported]+ `shouldBe` 2++ it "rejects predicate-valued Bool writes that Keiki cannot represent as scalar terms" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-bool-write",+ "aggregate Flag",+ " regs",+ " active Bool = False",+ " states Open Closed!",+ " command Set { active:Bool }",+ " event SetEvent = fields(Set)",+ " Open -- Set -->",+ " write active := cmd.active == true",+ " emit SetEvent",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<scalar-bool-write>" source+ errorCodes spec `shouldContain` [AggregateExpressionOperatorUnsupported]++ it "requires explicit roots for a same-named register and command field" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-ambiguity",+ "aggregate Counter",+ " regs",+ " amount Integer = 0",+ " states Open Closed!",+ " command Set { amount:Integer }",+ " event SetEvent = fields(Set)",+ " Open -- Set -->",+ " guard amount == 0",+ " emit SetEvent",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<scalar-ambiguity>" source+ errorCodes spec `shouldContain` [AggregateExpressionRootAmbiguous]++ it "enforces exclusive Hole ownership and preserves its canonical spelling" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-hole",+ "aggregate Counter",+ " regs",+ " amount Integer = 0",+ " states Open Closed!",+ " command Set { amount:Integer }",+ " event SetEvent = fields(Set)",+ " Open -- Set -->",+ " implementation hole",+ " guard cmd.amount >= 0",+ " emit SetEvent",+ " goto Closed"+ ]+ parsed <- case parseSource "<scalar-hole>" source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ errorCodes (parsedSpec parsed) `shouldContain` [AggregateTransitionOwnershipConflict]+ renderSource parsed `shouldSatisfy` T.isInfixOf "implementation hole"++ it "generates a stable per-transition Hole boundary and fold token" $ do+ let source =+ T.unlines+ [ "language keiro-dsl 2",+ "context scalar-hole",+ "aggregate Counter",+ " regs",+ " amount Integer = 0",+ " states Open Closed!",+ " command Set { amount:Integer }",+ " event SetEvent = fields(Set)",+ " Open -- Set -->",+ " implementation hole",+ " emit SetEvent",+ " goto Closed"+ ]+ spec <- parseInlineSpec "<scalar-hole-valid>" source+ aggregate <- case [value | NAggregate value <- specNodes spec] of+ [value] -> pure value+ _ -> expectationFailure "expected one Hole aggregate" >> fail "unreachable"+ let modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate+ transducer = generatedTextEndingIn "Transducer.hs" modules+ holes = holeTextEndingIn "Holes.hs" modules+ errorCodes spec `shouldBe` []+ map modulePath modules `shouldSatisfy` all (not . T.isSuffixOf "Expressions.hs" . T.pack)+ transducer `shouldSatisfy` T.isInfixOf "Holes.transition1OpenSetHole d"+ transducer `shouldSatisfy` T.isInfixOf "foldToken Holes.transition1OpenSetHoleFoldVersion"+ holes `shouldSatisfy` T.isInfixOf "transition1OpenSetHole _d = B.requireGuard K.PTop"+ holes `shouldSatisfy` T.isInfixOf "transition1OpenSetHoleFoldVersion = FoldVersion"+ holes `shouldSatisfy` (not . T.isInfixOf "counterTransducer")++ it "pins v1 and collection rejection at their stable boundaries" $ do+ v1Source <- readTestText "test/fixtures/aggregate-scalar-expressions-v1-rejects.keiro"+ case parseSource "v1.keiro" v1Source of+ Left (SourceLanguageFailure diagnostic) -> sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion+ other -> expectationFailure ("expected v1 source-language refusal, got " <> show other)+ collectionSource <- readTestText "test/fixtures/aggregate-collection-expressions-v2-rejects.keiro"+ case parseSource "collections.keiro" collectionSource of+ Left failure -> renderParseFailure failure `shouldSatisfy` T.isInfixOf "CollectionExpressionUnsupported"+ Right _ -> expectationFailure "collection syntax unexpectedly parsed"++ it "keeps arithmetic operands intact when complementing a scalar comparison" $ do+ let left = EAdd noLoc (EPath noLoc CommandRoot ["balance"]) (ELiteral noLoc (LiteralIntegral 2))+ right = ESubtract noLoc (EPath noLoc RegisterRoot ["balance"]) (ELiteral noLoc (LiteralIntegral 3))+ predicate = ECmp OpLt left right+ complementExpr predicate `shouldBe` ECmp OpGe left right+ complementExpr (complementExpr predicate) `shouldBe` predicate++ it "keeps the committed scalar-expression conformance tree fresh" $ do+ modules <- scaffoldFixture "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ forM_ [generatedModule | generatedModule <- modules, kind generatedModule == Generated] $ \generatedModule -> do+ committed <- readTestText ("test/conformance-scalar-expressions/" <> modulePath generatedModule)+ normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generatedModule)++ describe "behavior obligations" $ do+ it "uses one source-wide layout and excludes replay-only initial edges from live harness probes" $ do+ spec <-+ parseInlineSpec "<transition-layout>" $+ T.unlines+ [ "language keiro-dsl 4",+ "context transition-layout",+ "aggregate Journey",+ " regs",+ " states Empty Active",+ " command Start { current:Bool }",+ " command Legacy { current:Bool }",+ " event Started = fields(Start)",+ " event LegacyStarted = fields(Legacy)",+ " Empty -- Start --> guard cmd.current == true ; emit Started ; goto Active",+ " Active -- Start --> emit Started ; goto Active",+ " replay-only Empty -- Start --> guard cmd.current == false ; emit Started ; goto Active",+ " Active -- Legacy --> emit LegacyStarted ; goto Active",+ " replay-only Empty -- Legacy --> emit LegacyStarted ; goto Active"+ ]+ aggregate <- case [value | NAggregate value <- specNodes spec] of+ [value] -> pure value+ _ -> expectationFailure "expected one transition-layout aggregate" >> fail "unreachable"+ let ctx = defaultContext (specContext spec)+ modules = scaffoldAggregate ctx spec aggregate <> harnessFor ctx spec aggregate+ transducer = generatedTextEndingIn "Transducer.hs" modules+ harness = generatedTextEndingIn "Harness.hs" modules+ contract = generatedTextEndingIn "BehaviorContract.hs" modules+ T.count "B.from JourneyEmpty do" transducer `shouldBe` 1+ transducer `shouldSatisfy` T.isInfixOf "transition3EmptyStart"+ transducer `shouldSatisfy` T.isInfixOf "transition5EmptyLegacy"+ T.count "acceptStart :: Bool" harness `shouldBe` 1+ harness `shouldSatisfy` (not . T.isInfixOf "acceptLegacy")+ contract `shouldSatisfy` T.isInfixOf "K.EdgeRef JourneyEmpty 1"+ contract `shouldSatisfy` T.isInfixOf "K.EdgeRef JourneyEmpty 2"++ it "refuses duplicate live initial harness helpers with both source locations" $ do+ spec <-+ parseInlineSpec "<initial-helper-collision>" $+ T.unlines+ [ "language keiro-dsl 4",+ "context helper-collision",+ "aggregate Journey",+ " regs",+ " states Empty Active",+ " command Start { current:Bool }",+ " event Started = fields(Start)",+ " Empty -- Start --> guard cmd.current == true ; emit Started ; goto Active",+ " Empty -- Start --> guard cmd.current == false ; emit Started ; goto Active"+ ]+ let collisions = [diagnostic | diagnostic <- validateSpec spec, code diagnostic == GeneratedOccurrenceCollision]+ map line collisions `shouldBe` [9, 9]+ collisions `shouldSatisfy` all (elem (8, "'Start' also normalizes here") . relatedLocations)++ it "inventories generated harness sample constants before rendering" $ do+ service <-+ checkedServiceFromText+ "<sample-helper-collision>"+ ( T.unlines+ [ "language keiro-dsl 4",+ "context helper-collision",+ "id ObservedAt prefix=obs",+ "aggregate Journey",+ " regs",+ " states Empty",+ " command Start {",+ " request:ObservedAt",+ " observedAt:Time",+ " }"+ ]+ )+ let collisions = [diagnostic | diagnostic <- validateService service, code diagnostic == GeneratedOccurrenceCollision]+ map line collisions `shouldBe` [9]+ collisions `shouldSatisfy` all (elem (3, "'ObservedAt' also normalizes here") . relatedLocations)++ it "inventories every live-reachable cell, guarded edge, terminal rejection, and replay edge" $ do+ spec <- specOf "test/fixtures/behavior-complete.keiro"+ requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)+ length requirements `shouldBe` 19+ length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.LiveTransition] `shouldBe` 5+ length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.RequiredRejection] `shouldBe` 11+ length [() | requirement <- requirements, Behavior.requirementKind requirement == Behavior.ReplayTransition] `shouldBe` 3+ [Behavior.requirementSource requirement | requirement <- requirements, Behavior.requirementKind requirement == Behavior.RequiredRejection]+ `shouldContain` ["Active", "Closed"]+ length [() | requirement <- requirements, Behavior.requirementGuardCoverage requirement == Behavior.GuardTotal] `shouldBe` 3+ length [() | requirement <- requirements, Behavior.requirementGuardCoverage requirement == Behavior.GuardUnknown] `shouldBe` 2+ let report = Behavior.BehaviorObligationsReport "behavior-complete.keiro" Nothing requirements+ encoded = Behavior.encodeBehaviorObligationsJson report+ encoded `shouldSatisfy` T.isInfixOf "\"schema\":\"keiro-dsl/behavior-obligations/1\""+ encoded `shouldSatisfy` T.isInfixOf "\"source\":\"Closed\""+ encoded `shouldSatisfy` T.isInfixOf "\"kind\":\"replay-transition\""+ encoded `shouldSatisfy` (not . T.isInfixOf "\"filled\"")+ encoded `shouldSatisfy` (not . T.isInfixOf "\"missing\"")++ it "keeps semantic keys stable across line movement and canonical pretty printing" $ do+ source <- readTestText "test/fixtures/behavior-complete.keiro"+ parsed <- case parseSource "behavior-complete.keiro" source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let original = parsedSpec parsed+ moved <- parseInlineSpec "behavior-complete-moved.keiro" ("# line movement must not rename witnesses\n\n" <> source)+ pretty <- parseInlineSpec "behavior-complete-pretty.keiro" (renderSource parsed)+ let keys spec = fmap (map Behavior.requirementKey) (Behavior.deriveBehaviorRequirements spec)+ keys moved `shouldBe` keys original+ keys pretty `shouldBe` keys original++ it "generates direct fields(Command) output and separate create-once pending witnesses" $ do+ service <- checkedServiceOf "test/fixtures/behavior-complete.keiro"+ let spec = checkedSpec service+ aggregate <- case [value | NAggregate value <- specNodes spec] of+ [value] -> pure value+ _ -> expectationFailure "expected one behavior-complete aggregate" >> fail "unreachable"+ let ctx = defaultContext (specContext spec)+ modules = scaffoldAggregate ctx spec aggregate+ harness = generatedTextEndingIn "Harness.hs" (harnessForService ctx service aggregate)+ transducer = generatedTextEndingIn "Transducer.hs" modules+ codec = generatedTextEndingIn "Codec.hs" modules+ contract = generatedTextEndingIn "BehaviorContract.hs" modules+ projection = generatedTextEndingIn "Projection.hs" modules+ behaviorHoles = case [moduleText value | value <- modules, T.isSuffixOf "BehaviorHoles.hs" (T.pack (modulePath value))] of+ [value] -> value+ values -> error ("expected one BehaviorHoles module, got " <> show (length values))+ ordinaryHoles = [value | value <- modules, T.isSuffixOf "/Holes.hs" (T.pack (modulePath value)), not (T.isSuffixOf "BehaviorHoles.hs" (T.pack (modulePath value)))]+ transducer `shouldSatisfy` T.isInfixOf "requestId = d.requestId"+ transducer `shouldSatisfy` T.isInfixOf "observedAt = d.observedAt"+ transducer `shouldSatisfy` T.isInfixOf "amount = d.amount"+ transducer `shouldSatisfy` T.isInfixOf "details = d.details"+ codec `shouldSatisfy` T.isInfixOf "display_label"+ codec `shouldSatisfy` T.isInfixOf "optional_note"+ transducer `shouldSatisfy` (not . T.isInfixOf "Output")+ ordinaryHoles `shouldBe` []+ obsoleteGeneratedOutputHooks spec `shouldContain` [("Journey", "transition1EmptyStartOutput1Started")]+ T.count "B.from JourneyEmpty do" transducer `shouldBe` 1+ transducer `shouldSatisfy` T.isInfixOf "verifyTransition \"transition3EmptyStart\" GeneratedOwned JourneyEmpty 1"+ transducer `shouldSatisfy` T.isInfixOf "verifyTransition \"transition5EmptyLegacyStart\" GeneratedOwned JourneyEmpty 2"+ T.count "acceptStart :: Bool" harness `shouldBe` 1+ harness `shouldSatisfy` (not . T.isInfixOf "acceptLegacyStart")+ contract `shouldSatisfy` T.isInfixOf "keiro/behavior-conformance/1"+ contract `shouldSatisfy` T.isInfixOf "commandKind command == requirementCommandName requirement"+ contract `shouldSatisfy` (not . T.isInfixOf "OPTIONS_GHC")+ contract `shouldSatisfy` T.isInfixOf "module Generated.BehaviorComplete.Journey.BehaviorContract\n ( BehaviorKey (..)"+ contract `shouldSatisfy` T.isInfixOf "runRejection :: BehaviorRequirement"+ contract `shouldSatisfy` T.isInfixOf "failureSubject :: !Text"+ contract `shouldSatisfy` T.isInfixOf "\"subject\" .= failureSubject behaviorFailure"+ contract `shouldSatisfy` T.isInfixOf "-- JourneyEmpty x Start: live transition (spec line 38)"+ contract `shouldSatisfy` T.isInfixOf "requirementKey = BehaviorKey \"behavior-v1-"+ contract `shouldSatisfy` T.isInfixOf "requirementCommandName = \"Start\""+ contract `shouldSatisfy` T.isInfixOf "requirementExpectedEdge = (Just (K.EdgeRef JourneyEmpty 1))"+ contract `shouldSatisfy` T.isInfixOf "requirementCommandName = \"LegacyStart\""+ contract `shouldSatisfy` T.isInfixOf "requirementExpectedEdge = (Just (K.EdgeRef JourneyEmpty 2))"+ contract `shouldSatisfy` T.isInfixOf "runtime event values differ from the exact witness expectation; actual="+ T.count "Pending (BehaviorKey " behaviorHoles `shouldBe` 19+ behaviorHoles `shouldSatisfy` T.isInfixOf "-- JourneyEmpty x Start: live transition (spec line 38)"+ behaviorHoles `shouldSatisfy` (not . T.isInfixOf "undefined")+ behaviorHoles `shouldSatisfy` (not . T.isInfixOf "error")+ T.count "sampleRequestId :: RequestId" harness `shouldBe` 1+ T.count "sampleObservedAt :: UTCTime" harness `shouldBe` 1+ harness `shouldSatisfy` T.isInfixOf "Left problem -> error (show problem)"+ harness `shouldSatisfy` T.isInfixOf "sampleEventStarted = Started (StartedData sampleRequestId sampleObservedAt"+ harness `shouldSatisfy` T.isInfixOf "case step journeyTransducer (JourneyEmpty, initialJourneyRegs) (Start (StartData"+ harness `shouldSatisfy` T.isInfixOf "-- clock-free: spec samples no wall clock (verified at scaffold time)"+ harness `shouldSatisfy` (not . T.isInfixOf "(\"clock-free: spec samples no wall clock\", True)")+ codec `shouldSatisfy` T.isInfixOf "parseOptionalField (pure Nothing) (\\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) objectValue \"optional_note\""+ T.count "parseOptionalField ::" codec `shouldBe` 1+ projection `shouldSatisfy` T.isInfixOf "-- No projection declarations are present; this module keeps the generated manifest inventory total."++ it "rejects eventless state or register changes while accepting a true no-op" $ do+ invalid <-+ parseInlineSpec "<eventless-change>" $+ T.unlines+ [ "language keiro-dsl 2",+ "context eventless-change",+ "aggregate Counter",+ " regs",+ " count Natural = 0",+ " states Open Closed!",+ " command Tick { count:Natural }",+ " Open -- Tick --> write count := cmd.count ; goto Closed"+ ]+ errorCodes invalid `shouldContain` [AggregateEventlessStateChange]+ valid <-+ parseInlineSpec "<eventless-noop>" $+ T.unlines+ [ "language keiro-dsl 2",+ "context eventless-noop",+ "aggregate Counter",+ " regs",+ " count Natural = 0",+ " states Open",+ " command Tick { count:Natural }",+ " event Ticked = fields(Tick)",+ " Open -- Tick --> goto Open"+ ]+ errorCodes valid `shouldBe` []++ it "refuses duplicate semantic behavior identities before scaffolding" $ do+ duplicate <-+ parseInlineSpec "<duplicate-behavior>" $+ T.unlines+ [ "language keiro-dsl 2",+ "context duplicate-behavior",+ "aggregate Counter",+ " regs",+ " states Open",+ " command Tick { amount:Natural }",+ " event Ticked = fields(Tick)",+ " Open -- Tick --> emit Ticked ; goto Open",+ " Open -- Tick --> emit Ticked ; goto Open"+ ]+ let isBehaviorRefusal (BehaviorRefusal _) = True+ isBehaviorRefusal _ = False+ case planScaffold (defaultContext (specContext duplicate)) duplicate of+ Left refusals -> refusals `shouldSatisfy` any isBehaviorRefusal+ Right _ -> expectationFailure "duplicate behavior identity reached a scaffold write set"++ it "round-trips additive single-file and workspace behavior rows with member ownership" $ do+ spec <- specOf "test/fixtures/behavior-complete.keiro"+ requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)+ version <- maybe (expectationFailure "language version 2 was not constructible" >> fail "unreachable") pure (languageVersion 2)+ let rows = Behavior.behaviorRecordRows requirements+ singleRecord =+ ScaffoldRecord+ { recSpecPath = "behavior-complete.keiro",+ recModuleRoot = "",+ recLayout = "prefixed",+ recSourceLanguage = DeclaredLanguage version noLoc,+ recLanguageContract = effectiveLanguageContract (DeclaredLanguage version noLoc),+ recNamingEdition = IdiomaticNamingV1,+ recModuleRoles = [],+ recFiles = [],+ recMappings = [],+ recIdDomains = [],+ recNominalEqualities = [],+ recBindingObligations = [],+ recBehaviorRequirements = rows+ }+ T.count "behavior " (renderRecord singleRecord) `shouldBe` 19+ parseRecord (renderRecord singleRecord) `shouldBe` Just singleRecord++ workspace <- shouldComposeWorkspace "test/fixtures/behavior-complete-workspace/service.keiro-workspace"+ workspaceRequirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements (wsMergedSpec workspace))+ let ownedRequirements =+ map+ (Behavior.attributeBehaviorOwner (fmap fst . nodeOwner (wsOwnership workspace) "aggregate"))+ workspaceRequirements+ ownedRows = Behavior.behaviorRecordRows ownedRequirements+ workspaceRecord =+ WorkspaceRecord+ { wrService = wsService workspace,+ wrManifest = "service.keiro-workspace",+ wrContext = wsContext workspace,+ wrModuleRoot = "",+ wrLayout = "prefixed",+ wrMembers = map wmPath (wsMembers workspace),+ wrSourceLanguages = [WorkspaceSourceLanguageRow (wmPath member) (wmSourceLanguage member) | member <- wsMembers workspace],+ wrLanguageContract = wsLanguageContract workspace,+ wrNamingEdition = IdiomaticNamingV1,+ wrModules = [],+ wrMappings = [],+ wrIdDomains = [],+ wrNominalEqualities = [],+ wrBindingObligations = [],+ wrBehaviorRequirements = ownedRows,+ wrAdopted = []+ }+ map Behavior.behaviorRecordOwner ownedRows `shouldSatisfy` all (== Just "journey.keiro")+ T.count "behavior " (renderWorkspaceRecord workspaceRecord) `shouldBe` 19+ parseWorkspaceRecord (renderWorkspaceRecord workspaceRecord) `shouldBe` Just workspaceRecord++ it "keeps the initial replay fixture byte-identical across single, workspace, and repeat scaffolds" $ do+ withTempDirectory "keiro-dsl-initial-replay-layout" $ \base -> do+ let singleOut = base </> "single"+ workspaceOut = base </> "workspace"+ singleSource = "test/fixtures/behavior-complete.keiro"+ workspaceSource = "test/fixtures/behavior-complete-workspace/service.keiro-workspace"+ journeyModules = filter (T.isPrefixOf "Generated/BehaviorComplete/Journey/" . T.pack . fst)+ behaviorContractPath = "Generated/BehaviorComplete/Journey/BehaviorContract.hs"+ withoutBehaviorContract = filter ((/= behaviorContractPath) . fst)+ present = maybe False (const True)+ normalizeRequirementLines =+ T.unlines+ . map+ ( \sourceLine ->+ if "requirementLine =" `T.isInfixOf` sourceLine+ then fst (T.breakOn "=" sourceLine) <> "= <source-line>"+ else case T.breakOn "(spec line " sourceLine of+ (prefix, suffix)+ | T.null suffix -> sourceLine+ | otherwise -> prefix <> "(spec line <source-line>)"+ )+ . T.lines+ (singleCode, singleStdout, singleStderr) <- runKeiroDsl ["scaffold", singleSource, "--out", singleOut]+ unless (singleCode == ExitSuccess) (expectationFailure (singleStdout <> singleStderr))+ singleTree <- treeSnapshot singleOut+ (singleRepeatCode, singleRepeatStdout, singleRepeatStderr) <- runKeiroDsl ["scaffold", singleSource, "--out", singleOut]+ unless (singleRepeatCode == ExitSuccess) (expectationFailure (singleRepeatStdout <> singleRepeatStderr))+ treeSnapshot singleOut `shouldReturn` singleTree+ (workspaceCode, workspaceStdout, workspaceStderr) <- runKeiroDsl ["scaffold", workspaceSource, "--out", workspaceOut]+ unless (workspaceCode == ExitSuccess) (expectationFailure (workspaceStdout <> workspaceStderr))+ workspaceTree <- treeSnapshot workspaceOut+ let singleJourney = journeyModules singleTree+ workspaceJourney = journeyModules workspaceTree+ withoutBehaviorContract workspaceJourney `shouldBe` withoutBehaviorContract singleJourney+ case (lookup behaviorContractPath workspaceJourney, lookup behaviorContractPath singleJourney) of+ (Just workspaceContract, Just singleContract) ->+ normalizeRequirementLines workspaceContract `shouldBe` normalizeRequirementLines singleContract+ (workspaceContract, singleContract) ->+ expectationFailure+ ( "expected both generated behavior contracts, got "+ <> show (present workspaceContract, present singleContract)+ )+ (workspaceRepeatCode, workspaceRepeatStdout, workspaceRepeatStderr) <- runKeiroDsl ["scaffold", workspaceSource, "--out", workspaceOut]+ unless (workspaceRepeatCode == ExitSuccess) (expectationFailure (workspaceRepeatStdout <> workspaceRepeatStderr))+ treeSnapshot workspaceOut `shouldReturn` workspaceTree++ describe "nominal consumer types" $ do+ it "resolves every category through one checked registry and explains exact obligations" $ do+ spec <- specOf "test/fixtures/nominal-scalars.keiro"+ errorCodes spec `shouldBe` []+ registry <- case resolveNominalTypes spec of+ Left errors -> expectationFailure (show errors) >> fail "unreachable"+ Right value -> pure value+ Map.keys (nominalTypes registry)+ `shouldBe` ["AccountNumber", "FeatureFlag", "ObservedAt", "OrderId", "OrderStatus", "RiskScore", "SequenceNumber"]+ obligations <- either (\errors -> expectationFailure (show errors) >> pure []) pure (bindingObligations spec)+ length obligations `shouldBe` 21+ map obligationCategory obligations `shouldSatisfy` all (`elem` ["nominal-id", "nominal-enum", "nominal-scalar"])+ length [() | obligation <- obligations, obligationEqualityContract obligation /= Nothing] `shouldBe` 2+ renderBindingObligations (specContext spec) obligations `shouldSatisfy` T.isInfixOf "equality-contract:"+ let signatures = map obligationSignature obligations+ forM_+ [ "orderIdBinding :: NominalBinding NominalConformance.Domain.OrderId (KindID \"ord\")",+ "orderStatusBinding :: NominalBinding NominalConformance.Domain.OrderStatus Generated.NominalScalars.Nominal.Shape.OrderStatus.OrderStatusRepresentation",+ "accountNumberBinding :: NominalBinding NominalConformance.Domain.AccountNumber Text",+ "orderIdFixtures :: NominalFixtureCases NominalConformance.Domain.OrderId",+ "initialAccountNumber :: NominalConformance.Domain.AccountNumber"+ ]+ (`shouldSatisfy` (`elem` signatures))+ map obligationCanonicalType obligations `shouldSatisfy` all (/= Nothing)+ let rendered = renderBindingObligations (specContext spec) obligations+ rendered `shouldSatisfy` T.isInfixOf "nominal-id type OrderId"+ rendered `shouldSatisfy` T.isInfixOf "canonical-type: \"nominal.OrderId.v1\""+ case obligations of+ firstObligation : _ ->+ (Aeson.eitherDecode (Aeson.encode firstObligation) :: Either String BindingObligation)+ `shouldBe` Right firstObligation+ [] -> expectationFailure "expected nominal binding obligations"++ it "allocates distinct stable diagnostics for incomplete or incompatible nominal declarations" $ do+ missing <- errorCodesOf "test/fixtures/nominal-missing-facts.keiro"+ missing `shouldBe` replicate 5 NominalMissingIngredient+ errorCodesOf "test/fixtures/nominal-bad-qualified.keiro" `shouldReturn` [NominalInvalidQualifiedName]+ errorCodesOf "test/fixtures/nominal-invalid-prefix.keiro" `shouldReturn` replicate 2 NominalInvalidIdPrefix+ errorCodesOf "test/fixtures/nominal-unsupported-representation.keiro" `shouldReturn` [NominalUnsupportedRepresentation]+ errorCodesOf "test/fixtures/nominal-missing-initial.keiro" `shouldReturn` [NominalMissingInitialValue]+ errorCodesOf "test/fixtures/nominal-name-collision.keiro"+ `shouldReturn` [NominalNameCollision, GeneratedOccurrenceCollision, NominalNameCollision]++ it "keeps v1 rejection at the source-language boundary" $ do+ source <- readTestText "test/fixtures/nominal-v1.keiro"+ case parseSource "nominal-v1.keiro" source of+ Left (SourceLanguageFailure diagnostic) -> sourceLanguageErrorCode diagnostic `shouldBe` LanguageFeatureRequiresVersion+ other -> expectationFailure ("expected source-language refusal, got " <> show other)++ it "scaffolds consumer types, checked codecs, enum representation, projections, and deterministic manifests" $ do+ spec <- specOf "test/fixtures/nominal-scalars.keiro"+ let ctx = defaultContext (specContext spec)+ modules = scaffoldModules ctx spec+ moduleAt path = case [value | value <- modules, modulePath value == path] of+ [value] -> pure value+ values -> expectationFailure ("expected one module at " <> path <> ", got " <> show (map modulePath values)) >> fail "unreachable"+ domainModule <- moduleAt "Generated/NominalScalars/NominalLedger/Domain.hs"+ codecModule <- moduleAt "Generated/NominalScalars/NominalLedger/Codec.hs"+ enumModule <- moduleAt "Generated/NominalScalars/Nominal/Shape/OrderStatus.hs"+ projectionModule <- moduleAt "Generated/NominalScalars/NominalProjections.hs"+ bindingModule <- moduleAt "NominalConformance/Bindings.hs"+ map modulePath modules `shouldNotContain` ["NominalScalars/NominalLedger/Holes.hs"]+ moduleText domainModule `shouldSatisfy` T.isInfixOf "import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)"+ moduleText domainModule `shouldSatisfy` T.isInfixOf "orderId :: !OrderId"+ moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "NominalConformance.Domain.OrderId")+ moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "newtype OrderId")+ moduleText domainModule `shouldSatisfy` (not . T.isInfixOf "data OrderStatus =")+ moduleText codecModule `shouldSatisfy` T.isInfixOf "KindID.parseText @\"ord\""+ moduleText codecModule `shouldSatisfy` T.isInfixOf "KindID.toText (nominalToRepresentation"+ moduleText codecModule `shouldSatisfy` T.isInfixOf "nominalFromRepresentation"+ forM_ ["coerce", "unsafe", "read ", "error "] $ \forbidden ->+ moduleText codecModule `shouldSatisfy` (not . T.isInfixOf forbidden)+ moduleText enumModule `shouldSatisfy` T.isInfixOf "data OrderStatusRepresentation = Draft | Submitted"+ moduleText enumModule `shouldSatisfy` (not . T.isInfixOf "NominalConformance")+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "type FieldOwner AccountNumberNominalProjection = AccountNumber"+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "projectFieldValue _ = nominalToRepresentation Bindings.accountNumberBinding"+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection OrderIdEqualityProjection"+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "textProjectionDomain orderIdEqualityPattern"+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "instance ExactFieldProjection OrderStatusEqualityProjection"+ moduleText projectionModule `shouldSatisfy` T.isInfixOf "finiteProjectionDomain (\"draft\" :| [\"submitted\"])"+ kind bindingModule `shouldBe` HoleStub+ moduleText bindingModule `shouldSatisfy` T.isInfixOf "import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)"+ moduleText bindingModule `shouldSatisfy` T.isInfixOf "orderIdBinding :: NominalBinding OrderId (KindID \"ord\")"+ moduleText bindingModule `shouldSatisfy` T.isInfixOf "orderStatusBinding :: NominalBinding OrderStatus ShapeOrderStatus.OrderStatusRepresentation"+ firewallBreaches modules `shouldBe` []+ scaffoldModules ctx spec `shouldBe` modules+ manifestDependencies spec `shouldContain` ["mmzk-typeid", "nominal-conformance"]++ it "persists nominal provenance in a separate forward-compatible row kind" $ do+ spec <- specOf "test/fixtures/nominal-scalars.keiro"+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let plan = consumerPlan spec+ record =+ ScaffoldRecord+ { recSpecPath = "nominal-scalars.keiro",+ recModuleRoot = "",+ recLayout = "prefixed",+ recSourceLanguage = LegacyUnversioned,+ recLanguageContract = effectiveLanguageContract LegacyUnversioned,+ recNamingEdition = IdiomaticNamingV1,+ recModuleRoles = [],+ recFiles = [],+ recMappings = consumerMappings plan,+ recIdDomains = [],+ recNominalEqualities = nominalEqualityIdentities spec,+ recBindingObligations = [],+ recBehaviorRequirements = []+ }+ encoded = renderRecord record+ workspaceRecord =+ (sampleWorkspaceRecord workspace)+ { wrMappings = consumerMappings plan+ }+ workspaceEncoded = renderWorkspaceRecord workspaceRecord+ consumerPackages plan `shouldBe` ["nominal-conformance"]+ length [() | NominalMapping {} <- consumerMappings plan] `shouldBe` 7+ T.count "nominal-mapping " encoded `shouldBe` 7+ T.count "nominal-equality " encoded `shouldBe` 2+ T.count "\nmapping " encoded `shouldBe` 0+ parseRecord encoded `shouldBe` Just record+ T.count "nominal-mapping " workspaceEncoded `shouldBe` 7+ T.count "nominal-equality " workspaceEncoded `shouldSatisfy` (>= 2)+ T.count "\nmapping " workspaceEncoded `shouldBe` 0+ parseWorkspaceRecord workspaceEncoded `shouldBe` Just workspaceRecord++ it "reports bound-ID decoder tightening and makes binding provenance replay-visible" $ do+ current <- specOf "test/fixtures/nominal-scalars.keiro"+ let useGeneratedIdInitial (NAggregate aggregate) =+ NAggregate+ aggregate+ { aggRegs =+ [ if regName register == "orderId"+ then register {regInitial = RegInitBare "placeholder"}+ else register+ | register <- aggRegs aggregate+ ]+ }+ useGeneratedIdInitial node = node+ unbound =+ current+ { specIds = [declaration {idBinding = Nothing} | declaration <- specIds current],+ specNodes = map useGeneratedIdInitial (specNodes current)+ }+ adoption = diffSpecs unbound current+ decoderFindings = [kindOfChange change | change <- adoption, changeCode change == NominalIdDecoderTightened]+ map ckSubject decoderFindings `shouldContain` ["NominalLedger event NominalsRecorded .orderId"]+ decoderFindings `shouldSatisfy` all ((== VAdvisory) . verdictFor PrivateHistoryRead . ckVector)+ let bumped =+ current+ { specIds =+ [ declaration+ { idBinding = fmap (\binding -> binding {nominalBindingVersion = Just "2"}) (idBinding declaration)+ }+ | declaration <- specIds current+ ]+ }+ bindingChanges = diffSpecs current bumped+ map changeCode bindingChanges `shouldContain` [NominalBindingChanged]+ replayImpactSpecs current bumped `shouldSatisfy` \case+ ReplayImpact.ReplayAffected impacts ->+ maybe False (\impact -> Set.member "NominalsRecorded" (ReplayImpact.eventTypes impact) && includeSnapshotStreams impact) (Map.lookup "NominalLedger" impacts)+ ReplayImpact.ReplayNeutral -> False+ case [aggregate | NAggregate aggregate <- specNodes current] of+ aggregate : _ -> do+ aggregateFoldSurface current aggregate `shouldSatisfy` T.isInfixOf "nominal-equality-use:"+ aggregateFoldSurface current aggregate `shouldNotBe` aggregateFoldSurface bumped aggregate+ [] -> expectationFailure "expected nominal aggregate"++ 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+ parseStableRenderedSpec "<consumer-types-round-trip>" 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", AggregateExpressionOperatorUnsupported)+ ]+ forM_ cases $ \(fixture, expected) ->+ errorCodesOf ("test/fixtures/" <> fixture) `shouldReturn` [expected]+ it "keeps Time and Natural in Keiki's curated comparison set" $ do+ errorCodesOf "test/fixtures/mapped-guard-time.keiro" `shouldReturn` []+ errorCodesOf "test/fixtures/mapped-guard-natural.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 "aggregate type capabilities" $ do+ it "enumerates the policy for every resolved type and use site" $ do+ let resolvedTypes =+ [ AggregateText,+ AggregateInt,+ AggregateBool,+ AggregateTime,+ AggregateNatural,+ AggregateNominal (ResolvedNominalType "EntityId" (IdRepresentation "ent") GeneratedNominal noLoc),+ AggregateNominal (ResolvedNominalType "Status" (EnumRepresentation (("Active", "active") :| [])) GeneratedNominal noLoc),+ AggregateNominal (ResolvedNominalType "Amount" (ScalarRepresentation NominalInt) (consumerNominalFor "Amount") noLoc),+ AggregateNominal (ResolvedNominalType "Label" (ScalarRepresentation NominalText) (consumerNominalFor "Label") noLoc),+ AggregateVertex "EntityVertex",+ AggregateMapped (MappedKey "ConsumerValue")+ ]+ useSites = [minBound .. maxBound]+ expected useSite resolvedType = case useSite of+ OrderingGuardUse -> case resolvedType of+ AggregateInt -> SolverVisible+ AggregateTime -> SolverVisible+ AggregateNatural -> SolverVisible+ AggregateNominal nominal -> case resolvedNominalRepresentation nominal of+ ScalarRepresentation NominalInt -> SolverVisible+ ScalarRepresentation NominalNatural -> SolverVisible+ ScalarRepresentation NominalTime -> SolverVisible+ _ -> Unsupported+ _ -> Unsupported+ EqualityGuardUse -> case resolvedType of+ AggregateMapped {} -> Unsupported+ AggregateNominal {} -> SolverVisible+ AggregateVertex {} -> OpaqueOnly+ _ -> SolverVisible+ _ -> case resolvedType of+ AggregateNominal nominal -> case resolvedNominalRepresentation nominal of+ ScalarRepresentation {} -> SolverVisible+ _ -> OpaqueOnly+ AggregateVertex {} -> OpaqueOnly+ AggregateMapped {} -> OpaqueOnly+ _ -> SolverVisible+ actual =+ [ (useSite, resolvedType, aggregateCapability useSite resolvedType)+ | useSite <- useSites,+ resolvedType <- resolvedTypes+ ]+ wanted =+ [ (useSite, resolvedType, expected useSite resolvedType)+ | useSite <- useSites,+ resolvedType <- resolvedTypes+ ]+ actual `shouldBe` wanted+ it "lowers direct Time and Natural through every generated aggregate boundary" $ do+ spec <- specOf "test/fixtures/aggregate-scalars.keiro"+ errorCodes spec `shouldBe` []+ let aggregate = onlyAggregate spec+ modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate+ generated =+ [ moduleText generatedModule+ | generatedModule <- modules,+ Keiro.Dsl.Scaffold.kind generatedModule == Generated+ ]+ domain = generatedTextEndingIn "Domain.hs" modules+ codec = generatedTextEndingIn "Codec.hs" modules+ domain `shouldSatisfy` T.isInfixOf "observedAt :: !UTCTime"+ domain `shouldSatisfy` T.isInfixOf "revision :: !Natural"+ domain `shouldSatisfy` T.isInfixOf "UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)"+ domain `shouldSatisfy` T.isInfixOf "import Data.Time.Calendar (fromGregorian)"+ domain `shouldSatisfy` T.isInfixOf "import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime)"+ domain `shouldSatisfy` T.isInfixOf "import Numeric.Natural (Natural)"+ manifestDependencies spec `shouldContain` ["time"]+ manifestDependencies spec `shouldNotContain` ["keiki-codec-json"]+ codec `shouldSatisfy` T.isInfixOf "scalarLedgerEventTypes :: NonEmpty EventType"+ codec `shouldSatisfy` T.isInfixOf "eventTypes = scalarLedgerEventTypes"+ codec `shouldSatisfy` T.isInfixOf "renderExpectedEventTypes scalarLedgerEventTypes"+ codec `shouldSatisfy` (not . T.isInfixOf "; expected one of: ScalarsRecorded\"")+ generated `shouldSatisfy` all (not . T.isInfixOf "error")+ generated `shouldSatisfy` all (not . T.isInfixOf "getCurrentTime")+ generated `shouldSatisfy` all (not . T.isInfixOf "iso8601ParseM")+ it "keeps the event-list binding disjoint from the private formatter" $ do+ source <- readTestText "test/fixtures/aggregate-scalars.keiro"+ spec <- parseInlineSpec "<render-aggregate>" (T.replace "aggregate ScalarLedger" "aggregate Render" source)+ let aggregate = onlyAggregate spec+ modules = scaffoldAggregate (defaultContext (specContext spec)) spec aggregate+ codec = generatedTextEndingIn "Codec.hs" modules+ codecLines = T.lines codec+ codecLines `shouldContain` ["renderEventTypes :: NonEmpty EventType"]+ codecLines `shouldContain` ["renderExpectedEventTypes :: NonEmpty EventType -> String"]+ codec `shouldSatisfy` T.isInfixOf "eventTypes = renderEventTypes"+ codec `shouldSatisfy` T.isInfixOf "renderExpectedEventTypes renderEventTypes"+ it "canonicalizes Time and UTCTime across pretty, diff, and fold identity" $ do+ source <- readTestText "test/fixtures/aggregate-scalars.keiro"+ canonical <- parseInlineSpec "<time>" source+ alias <- parseInlineSpec "<utctime>" (T.replace ":Time" ":UTCTime" (T.replace " Time =" " UTCTime =" source))+ renderSpec alias `shouldBe` renderSpec canonical+ legacyDiffSpecs canonical alias `shouldBe` []+ legacyAggregateFoldFingerprint canonical (onlyAggregate canonical)+ `shouldBe` legacyAggregateFoldFingerprint alias (onlyAggregate alias)+ legacyAggregateFoldSurface canonical (onlyAggregate canonical)+ `shouldBe` legacyAggregateFoldSurface alias (onlyAggregate alias)+ it "keeps the committed scalar conformance generated tree fresh" $ do+ modules <- scaffoldFixture "test/fixtures/aggregate-scalars.keiro"+ forM_ [generatedModule | generatedModule <- modules, Keiro.Dsl.Scaffold.kind generatedModule == Generated] $ \generatedModule -> do+ committed <- readTestText ("test/conformance-aggregate-scalars/" <> modulePath generatedModule)+ normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generatedModule)+ it "never sends a clean scalar aggregate to a type scaffold refusal" $+ property $+ forAll (elements scalarRegisterCases) $ \(typeName, initialValue) ->+ case parseSpec "<clean-scalar>" (cleanScalarAggregateSpec typeName initialValue) of+ Left parseError -> counterexample (T.unpack parseError) False+ Right spec ->+ let diagnostics = [diagnostic | diagnostic <- validateSpec spec, severity diagnostic == Error]+ modules = scaffoldModules (defaultContext (specContext spec)) spec+ in counterexample+ (show diagnostics <> "\n" <> show (scaffoldRefusals spec))+ ( null diagnostics+ && null (scaffoldRefusals spec)+ && all (not . T.null . moduleText) modules+ )++ describe "aggregate scalar diagnostics" $ do+ it "reports unsupported shapes, invalid initials, and mismatched guards at stable lines" $ do+ diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-unsupported.keiro"+ [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]+ `shouldBe` [ (AggregateRegisterInitialInvalid, 6),+ (AggregateRegisterInitialInvalid, 7),+ (AggregateTypeUnsupportedAtUse, 10),+ (AggregateExpressionOperandTypeMismatch, 14)+ ]+ map message diagnostics `shouldSatisfy` any (T.isInfixOf "non-negative integral literals")+ map message diagnostics `shouldSatisfy` any (T.isInfixOf "ISO-8601 UTC timestamps")+ map message diagnostics `shouldSatisfy` any (T.isInfixOf "mapped structural declaration")+ it "accepts Natural aggregate arithmetic in the stable language" $ do+ diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-arithmetic.keiro"+ [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]+ `shouldBe` []+ it "covers unknown, container, fractional, out-of-range, and ordering failures" $ do+ diagnostics <- diagnosticsOf "test/fixtures/aggregate-scalars-invalid-capabilities.keiro"+ [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, severity diagnostic == Error]+ `shouldBe` [ (AggregateRegisterInitialInvalid, 6),+ (AggregateRegisterInitialInvalid, 7),+ (AggregateTypeUnknown, 10),+ (AggregateTypeUnsupportedAtUse, 10),+ (AggregateTypeUnsupportedAtUse, 10),+ (AggregateTypeUnsupportedAtUse, 10),+ (AggregateExpressionOperatorUnsupported, 13)+ ]+ it "keeps one-member workspace diagnostics identical to the single file" $ do+ direct <- diagnosticsOf "test/fixtures/aggregate-scalars-unsupported.keiro"+ composed <- shouldComposeWorkspace "test/fixtures/aggregate-scalars-workspace/service.keiro-workspace"+ let directErrors =+ [(code diagnostic, line diagnostic, message diagnostic) | diagnostic <- direct, severity diagnostic == Error]+ workspaceErrors =+ [ (wdCode diagnostic, wlLine (NE.head (wdLocations diagnostic)), wdMessage diagnostic)+ | diagnostic <- checkWorkspace composed,+ wdSeverity diagnostic == Error+ ]+ workspaceErrors `shouldBe` directErrors++ 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" Nothing 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 "normalizes lowercase logical type names and reports generated Haskell keywords at their owning declarations" $ do+ spec <- parseInlineSpec "<identifier-hygiene>" identifierHygieneSpec+ [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic `elem` [IdentUnsafeNormalization, GeneratedOccurrenceReserved]]+ `shouldBe` [(GeneratedOccurrenceReserved, 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 normalization would erase a word boundary" $ do+ spec <- parseInlineSpec "<underscore-node>" underscoreNodeSpec+ [(code diagnostic, line diagnostic) | diagnostic <- validateSpec spec, code diagnostic == IdentUnsafeNormalization]+ `shouldBe` [(IdentUnsafeNormalization, 3)]+ it "rejects normalized module collisions with both source locations" $ do+ spec <- parseInlineSpec "<normalized-collision>" normalizedCollisionSpec+ case [diagnostic | diagnostic <- validateSpec spec, code diagnostic == GeneratedOccurrenceCollision] of+ [diagnostic] -> do+ line diagnostic `shouldBe` 8+ relatedLocations diagnostic `shouldBe` [(3, "'fooBar' also normalizes here")]+ renderDiagnostic "<normalized-collision>" diagnostic `shouldSatisfy` T.isInfixOf "fooBar"+ diagnostics -> expectationFailure ("expected one normalized collision, got " <> show diagnostics)+ it "validates explicit selectors and detects selector collisions in aggregate and contract records" $ do+ service <-+ checkedServiceFromText+ "<field-selector-validation>"+ ( T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change {",+ " first haskell shared:Text",+ " second haskell shared:Text",+ " reserved haskell type:Text",+ " invalid haskell Bad:Text",+ " }",+ "contract publicOrder {",+ " schemaVersion 1",+ " discriminator kind",+ " topic changes \"orders.v1\"",+ " event Changed on changes {",+ " first haskell duplicate: text",+ " second haskell duplicate: text",+ " }",+ "}"+ ]+ )+ let diagnostics = validateService service+ selectorCollisions = [diagnostic | diagnostic <- diagnostics, code diagnostic == GeneratedOccurrenceCollision]+ [(code diagnostic, line diagnostic) | diagnostic <- diagnostics, code diagnostic `elem` [GeneratedOccurrenceReserved, IdentUnsafeNormalization]]+ `shouldBe` [(GeneratedOccurrenceReserved, 9), (IdentUnsafeNormalization, 10)]+ map line selectorCollisions `shouldBe` [8, 18]+ map relatedLocations selectorCollisions+ `shouldBe` [ [(7, "'first' also normalizes here")],+ [(17, "'first' also normalizes here")]+ ]+ it "rejects empty, duplicate, and envelope-colliding resolved wire keys with field-local evidence" $ do+ service <-+ checkedServiceFromText+ "<field-wire-validation>"+ ( T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change {",+ " first as \"same\":Text",+ " second as \"same\":Text",+ " empty as \"\":Text",+ " }",+ " event Changed { value as \"kind\":Text }",+ "contract publicOrder {",+ " schemaVersion 1",+ " discriminator kind",+ " topic changes \"orders.v1\"",+ " event Published on changes { value as \"kind\": text }",+ "}"+ ]+ )+ let diagnostics = validateService service+ wireDiagnostics = [diagnostic | diagnostic <- diagnostics, code diagnostic `elem` [FieldWireKeyCollision, FieldWireKeyInvalid]]+ map (\diagnostic -> (code diagnostic, line diagnostic)) wireDiagnostics+ `shouldBe` [ (FieldWireKeyCollision, 8),+ (FieldWireKeyInvalid, 9),+ (FieldWireKeyCollision, 11),+ (FieldWireKeyCollision, 16)+ ]+ case wireDiagnostics of+ firstDiagnostic : _ -> relatedLocations firstDiagnostic `shouldBe` [(7, "wire key 'same' is first declared here")]+ [] -> expectationFailure "expected resolved wire-key diagnostics"+ -- `family` is a contextual keyword GHC accepts as a term under the+ -- advertised GHC2024 contract, and it is the field mori's project signals+ -- are keyed by. This fixture pins that scenario end to end; before ExecPlan+ -- 199 no test referenced it, so the guarantee was untested.+ it "keeps a reserved-word-adjacent contract field intact from check to codec" $+ withTempDirectory "keiro-dsl-reserved-family" $ \out -> do+ let fixture = "test/fixtures/contract-reserved-family.keiro"+ (checkCode, checkOut, checkErr) <- runKeiroDsl ["check", fixture, "--min-language", "4", "--deny-warnings"]+ unless (checkCode == ExitSuccess) (expectationFailure (checkOut <> checkErr))+ checkOut `shouldBe` "OK\n"+ checkErr `shouldNotContain` "warning["++ (scaffoldCode, scaffoldOut, scaffoldErr) <- runKeiroDsl ["scaffold", fixture, "--out", out]+ unless (scaffoldCode == ExitSuccess) (expectationFailure (scaffoldOut <> scaffoldErr))+ tree <- treeSnapshot out+ case [text | (path, text) <- tree, "Contract.hs" `T.isSuffixOf` T.pack path] of+ codec : _ -> do+ -- The DSL name is the record selector …+ codec `shouldSatisfy` T.isInfixOf "family ::"+ -- … and, unaliased, the wire key is the same bytes.+ codec `shouldSatisfy` T.isInfixOf "\"family\""+ codec `shouldNotSatisfy` T.isInfixOf "family_"+ [] -> expectationFailure ("no generated contract module in " <> show (map fst tree))++ -- An alias exists to preserve a brownfield key the current convention would+ -- reject, so its *style* is deliberately not checked (ADR 0021). What is+ -- checked is that the key can be a key: a trailing space or a control+ -- character ships a permanently mis-keyed public field. See ExecPlan 199.+ it "refuses structurally unusable wire-key aliases without opinionating on style" $ do+ let aliasSpec alias =+ T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change {",+ " region as \"" <> alias <> "\":Text",+ " }"+ ]+ keyDiagnostics source = do+ service <- checkedServiceFromText "<alias-content>" source+ pure [diagnostic | diagnostic <- validateService service, code diagnostic == FieldWireKeyInvalid]++ -- Refused: the wire key is the exact bytes on the wire. Written as the+ -- DSL spells them, so `\\n` here is the source's escape, not Haskell's.+ forM_ ["family ", " family", "family\\n", "fam\\tily", "fam\\rily"] $ \bad -> do+ refused <- keyDiagnostics (aliasSpec bad)+ map code refused `shouldBe` [FieldWireKeyInvalid]+ map line refused `shouldBe` [7]++ -- Accepted: these violate `fields=camelCase` and that is exactly the point+ -- of an alias — the brownfield key is preserved, not corrected.+ forM_ ["region_code", "Region-Code", "REGION.CODE", "r\233gion"] $ \brownfield -> do+ accepted <- keyDiagnostics (aliasSpec brownfield)+ accepted `shouldBe` []++ -- The collision planner must register the selector generation actually+ -- emits. Registering a camelized rendering of the raw name made it claim+ -- `foo_bar` "normalizes to" `fooBar`, which generation never does.+ it "plans field collisions against the emitted selector, not a camelized rendering" $ do+ let recordSpec fields =+ T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change { " <> fields <> " }"+ ]+ collisionsIn source = do+ service <- checkedServiceFromText "<selector-collision>" source+ pure [diagnostic | diagnostic <- validateService service, code diagnostic == GeneratedOccurrenceCollision]++ -- Distinct emitted selectors: `foo_bar` generates `foo_bar`. It is still+ -- refused, but by the generated-name audit that owns lowerCamelCase — not+ -- by a collision claim naming an unrelated sibling.+ falseCollision <- collisionsIn (recordSpec "foo_bar fooBar")+ falseCollision `shouldBe` []++ -- Two declarations that really do emit one selector still collide.+ realCollision <- collisionsIn (recordSpec "fooBar other haskell fooBar")+ map code realCollision `shouldSatisfy` \codes -> GeneratedOccurrenceCollision `elem` codes++ it "checks copied command selectors in both generated record scopes" $ do+ service <-+ checkedServiceFromText+ "<copied-selector-collision>"+ ( T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "aggregate Order",+ " regs",+ " states Open",+ " command Change { first haskell shared:Text second haskell shared:Text }",+ " event Changed = fields(Change)"+ ]+ )+ [line diagnostic | diagnostic <- validateService service, code diagnostic == GeneratedOccurrenceCollision]+ `shouldBe` [6, 6]+ it "anchors repeated reserved contract fields at their own lines and maps them through workspaces" $ do+ service <-+ checkedServiceFromText+ "domain/member.keiro"+ ( T.unlines+ [ "language keiro-dsl 4",+ "context aliases",+ "contract publicOrder {",+ " schemaVersion 1",+ " discriminator kind",+ " topic changes \"orders.v1\"",+ " event First on changes { where: text }",+ " event Second on changes { where: text }",+ "}"+ ]+ )+ [line diagnostic | diagnostic <- validateService service, code diagnostic == GeneratedOccurrenceReserved]+ `shouldBe` [7, 8]+ let workspaceDiagnostics =+ [ diagnostic+ | diagnostic <- checkWorkspace (oneMemberWorkspace "domain/member.keiro" (checkedSpec service)),+ wdCode diagnostic == GeneratedOccurrenceReserved+ ]+ workspaceLocations =+ [ (wlFile location, wlLine location)+ | diagnostic <- workspaceDiagnostics,+ location <- NE.toList (wdLocations diagnostic)+ ]+ workspaceLocations+ `shouldBe` [ (WorkspaceMemberFile "member.keiro", 7),+ (WorkspaceMemberFile "member.keiro", 8)+ ]+ it "rejects non-ASCII identifier characters in the parser" $+ parseSpec "<unicode-identifier>" unicodeIdentifierSpec `shouldSatisfy` leftContains "unexpected"++ describe "Haskell.name-audit" $ do+ it "inventories every declaration in a fresh compound-name scaffold" $ do+ spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+ let service = legacyCheckedService spec+ ctx = defaultContext (specContext spec)+ modules = scaffoldServiceModules ctx service+ concatMap auditGeneratedHaskell modules `shouldBe` []+ it "rejects underscore module and declaration mutations but ignores literals and comments" $ do+ let mutated =+ ScaffoldModule+ { modulePath = "Generated/IncidentPaging/Service_oncall/Mutation.hs",+ moduleText =+ T.unlines+ [ "module Generated.IncidentPaging.Service_oncall.Mutation where",+ "-- comment_value :: Text",+ "literalValue = \"string_value\"",+ "render_eventTypes :: Int",+ "render_eventTypes = 1"+ ],+ kind = Generated,+ origin = "test name-audit mutation"+ }+ violations = auditGeneratedHaskell mutated+ violations `shouldSatisfy` any (T.isInfixOf "Service_oncall")+ violations `shouldSatisfy` any (T.isInfixOf "render_eventTypes")+ violations `shouldSatisfy` all (not . T.isInfixOf "comment_value")+ violations `shouldSatisfy` all (not . T.isInfixOf "string_value")+ it "rejects repeated generated signatures before writing" $ do+ let mutated =+ ScaffoldModule+ { modulePath = "Generated/Repeated.hs",+ moduleText =+ T.unlines+ [ "module Generated.Repeated where",+ "sameValue :: Bool",+ "sameValue = True",+ "sameValue :: Bool",+ "sameValue = False"+ ],+ kind = Generated,+ origin = "test repeated declaration"+ }+ auditGeneratedHaskell mutated `shouldSatisfy` any (T.isInfixOf "repeated top-level type signature 'sameValue'")++ describe "Haskell.name-migration" $ do+ it "pairs a legacy module path with its stable idiomatic artifact" $ do+ let currentModule =+ ScaffoldModule+ { modulePath = "Generated/IncidentPaging/ServiceOncall/ReadModel.hs",+ moduleText = "module Generated.IncidentPaging.ServiceOncall.ReadModel where\n",+ kind = Generated,+ origin = "readmodel service_oncall ReadModel"+ }+ planSourceMoves [(Nothing, Generated, "Generated/IncidentPaging/Service_oncall/ReadModel.hs")] [currentModule]+ `shouldBe` Right+ [ SourceMove+ { moveRole = moduleRole currentModule,+ moveKind = Generated,+ moveOldModule = "Generated.IncidentPaging.Service_oncall.ReadModel",+ moveNewModule = "Generated.IncidentPaging.ServiceOncall.ReadModel",+ moveOldPath = "Generated/IncidentPaging/Service_oncall/ReadModel.hs",+ moveNewPath = "Generated/IncidentPaging/ServiceOncall/ReadModel.hs",+ moveBackupPath = ".keiro-dsl-name-migrations/legacy-v1-to-idiomatic-v1/Generated/IncidentPaging/Service_oncall/ReadModel.hs",+ moveContentDigest = Nothing,+ moveTransformedDigest = Nothing+ }+ ]+ it "rewrites code-token module references while preserving comments and literals" $ do+ let old = "Generated.IncidentPaging.Service_oncall.ReadModel"+ new = "Generated.IncidentPaging.ServiceOncall.ReadModel"+ source =+ T.unlines+ [ "module IncidentPaging.Service_oncall.ReadModelHoles where",+ "import Generated.IncidentPaging.Service_oncall.ReadModel",+ "value = Generated.IncidentPaging.Service_oncall.ReadModel.constructor",+ "-- Generated.IncidentPaging.Service_oncall.ReadModel in a comment",+ "literal = \"Generated.IncidentPaging.Service_oncall.ReadModel\"",+ "character = 'x'",+ "{- outer {- Generated.IncidentPaging.Service_oncall.ReadModel -} comment -}"+ ]+ case rewriteHaskellModuleReferences (Map.singleton old new) source of+ Left err -> expectationFailure (show err)+ Right rewritten -> do+ rewritten `shouldSatisfy` T.isInfixOf "import Generated.IncidentPaging.ServiceOncall.ReadModel"+ rewritten `shouldSatisfy` T.isInfixOf "value = Generated.IncidentPaging.ServiceOncall.ReadModel.constructor"+ rewritten `shouldSatisfy` T.isInfixOf "-- Generated.IncidentPaging.Service_oncall.ReadModel in a comment"+ rewritten `shouldSatisfy` T.isInfixOf "literal = \"Generated.IncidentPaging.Service_oncall.ReadModel\""+ rewritten `shouldSatisfy` T.isInfixOf "{- outer {- Generated.IncidentPaging.Service_oncall.ReadModel -} comment -}"+ it "refuses without mutation, then applies recoverable generated and hole moves" $+ withTempDirectory "keiro-dsl-name-migration" $ \out -> do+ spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+ let service = legacyCheckedService spec+ ctx = defaultContext (specContext spec)+ modules <- case planServiceScaffold ctx service of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right planned -> pure planned+ let selected =+ [ scaffoldModule+ | scaffoldModule <- modules,+ any (`T.isSuffixOf` T.pack (modulePath scaffoldModule)) ["ServiceOncall/ReadModel.hs", "ServiceOncall/ReadModelHoles.hs"]+ ]+ legacyPath = T.unpack . T.replace "ServiceOncall" "Service_oncall" . T.pack+ reverseModules =+ Map.fromList+ [ (moduleNameFromPath (modulePath scaffoldModule), moduleNameFromPath (legacyPath (modulePath scaffoldModule)))+ | scaffoldModule <- selected+ ]+ forM_ selected $ \scaffoldModule -> do+ legacyText <- case rewriteHaskellModuleReferences reverseModules (moduleText scaffoldModule) of+ Left err -> expectationFailure (show err) >> fail "unreachable"+ Right source -> pure source+ let oldPath = out </> legacyPath (modulePath scaffoldModule)+ withEvidence+ | kind scaffoldModule == HoleStub =+ legacyText+ <> "\n-- Generated.IncidentPaging.Service_oncall.ReadModel remains in this comment\n"+ <> "migrationLiteral = \"Generated.IncidentPaging.Service_oncall.ReadModel\"\n"+ | otherwise = legacyText+ createDirectoryIfMissing True (takeDirectory oldPath)+ TIO.writeFile oldPath withEvidence+ let legacyRecord =+ ScaffoldRecord+ { recSpecPath = "incident-paging.keiro",+ recModuleRoot = "",+ recLayout = "prefixed",+ recSourceLanguage = LegacyUnversioned,+ recLanguageContract = effectiveLanguageContract LegacyUnversioned,+ recNamingEdition = LegacyNamingV1,+ recModuleRoles = [],+ recFiles = [(kind scaffoldModule, legacyPath (modulePath scaffoldModule)) | scaffoldModule <- selected],+ recMappings = [],+ recIdDomains = [],+ recNominalEqualities = [],+ recBindingObligations = [],+ recBehaviorRequirements = []+ }+ recordPath = out </> recordFileName (specContext spec)+ TIO.writeFile recordPath (renderRecord legacyRecord)+ beforeMigration <- treeSnapshot out+ refused <- executeServiceScaffoldWithRuntimePackageAndNameMigrations Nothing False out False "incident-paging.keiro" LegacyUnversioned ctx service modules+ refused `shouldSatisfy` \case+ Left [NameMigrationRequired moves] ->+ length moves == 2+ && all ((/= Nothing) . moveContentDigest) moves+ && all ((/= Nothing) . moveTransformedDigest) moves+ _ -> False+ treeSnapshot out `shouldReturn` beforeMigration+ applied <- executeServiceScaffoldWithRuntimePackageAndNameMigrations Nothing True out False "incident-paging.keiro" LegacyUnversioned ctx service modules+ report <- case applied of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right value -> pure value+ length (reportNameMoves report) `shouldBe` 2+ let newHole = out </> "IncidentPaging/ServiceOncall/ReadModelHoles.hs"+ oldHole = out </> "IncidentPaging/Service_oncall/ReadModelHoles.hs"+ backupHole = out </> ".keiro-dsl-name-migrations/legacy-v1-to-idiomatic-v1/IncidentPaging/Service_oncall/ReadModelHoles.hs"+ doesFileExist oldHole `shouldReturn` False+ doesFileExist newHole `shouldReturn` True+ doesFileExist backupHole `shouldReturn` True+ migratedHole <- TIO.readFile newHole+ migratedHole `shouldSatisfy` T.isInfixOf "module IncidentPaging.ServiceOncall.ReadModelHoles"+ migratedHole `shouldSatisfy` T.isInfixOf "-- Generated.IncidentPaging.Service_oncall.ReadModel remains in this comment"+ migratedHole `shouldSatisfy` T.isInfixOf "migrationLiteral = \"Generated.IncidentPaging.Service_oncall.ReadModel\""+ backupBefore <- TIO.readFile backupHole+ rerun <- executeServiceScaffoldWithRuntimePackageAndNameMigrations Nothing True out False "incident-paging.keiro" LegacyUnversioned ctx service modules+ case rerun of+ Left refusals -> expectationFailure (show refusals)+ Right rerunReport -> reportNameMoves rerunReport `shouldBe` []+ TIO.readFile backupHole `shouldReturn` backupBefore+ -- Recreate the exact crash state after every backup and prepared file+ -- exists but before any destination is installed. A corrupted prepared+ -- file refuses; restoring its digest lets the next run resume.+ preparedSnapshots <- forM selected $ \scaffoldModule -> do+ let newPath = out </> modulePath scaffoldModule+ preparedPath = newPath <> ".keiro-dsl-name-migration-prepared"+ bytes <- TIO.readFile newPath+ renameFile newPath preparedPath+ pure (preparedPath, bytes)+ TIO.writeFile recordPath (renderRecord legacyRecord)+ case preparedSnapshots of+ (firstPrepared, firstBytes) : _ -> TIO.writeFile firstPrepared (firstBytes <> "\ncorrupt")+ [] -> expectationFailure "expected prepared migration sources"+ conflicted <- executeServiceScaffoldWithRuntimePackageAndNameMigrations Nothing True out False "incident-paging.keiro" LegacyUnversioned ctx service modules+ conflicted `shouldSatisfy` \case Left [NameMigrationRefusal messages] -> any (T.isInfixOf "prepared source digest") messages; _ -> False+ forM_ preparedSnapshots (uncurry TIO.writeFile)+ resumed <- executeServiceScaffoldWithRuntimePackageAndNameMigrations Nothing True out False "incident-paging.keiro" LegacyUnversioned ctx service modules+ case resumed of+ Left refusals -> expectationFailure (show refusals)+ Right resumedReport -> length (reportNameMoves resumedReport) `shouldBe` 2+ doesFileExist newHole `shouldReturn` True+ TIO.readFile backupHole `shouldReturn` backupBefore+ it "applies the same move protocol to a two-member workspace without changing ownership" $+ withTempDirectory "keiro-dsl-workspace-name-migration" $ \out -> do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ plan <- shouldPlanWorkspaceSpec workspace+ initial <- executeWorkspaceScaffold out False plan+ case initial of+ Left refusals -> expectationFailure (show refusals)+ Right _ -> pure ()+ let recordPath = out </> workspaceRecordFileName (wsService workspace)+ currentRecord <-+ TIO.readFile recordPath >>= \contents ->+ maybe (expectationFailure "fresh workspace record did not parse" >> fail "unreachable") pure (parseWorkspaceRecord contents)+ let selectedRows = [row | row <- wrModules currentRecord, "ProjectActivity" `T.isInfixOf` T.pack (wrmPath row)]+ legacyPath = T.unpack . T.replace "ProjectActivity" "Project_activity" . T.pack+ reverseModules =+ Map.fromList+ [ (moduleNameFromPath (wrmPath row), moduleNameFromPath (legacyPath (wrmPath row)))+ | row <- selectedRows+ ]+ selectedRows `shouldSatisfy` (not . null)+ forM_ selectedRows $ \row -> do+ currentSource <- TIO.readFile (out </> wrmPath row)+ legacySource <- case rewriteHaskellModuleReferences reverseModules currentSource of+ Left err -> expectationFailure (show err) >> fail "unreachable"+ Right source -> pure source+ writeFileWithParents (out </> legacyPath (wrmPath row)) legacySource+ removeFile (out </> wrmPath row)+ let legacyRecord =+ currentRecord+ { wrNamingEdition = LegacyNamingV1,+ wrModules =+ [ if row `elem` selectedRows then row {wrmPath = legacyPath (wrmPath row)} else row+ | row <- wrModules currentRecord+ ]+ }+ ownersBefore = Map.fromList [(wrmRole row, wrmOwner row) | row <- selectedRows]+ TIO.writeFile recordPath (renderWorkspaceRecord legacyRecord)+ beforeMigration <- treeSnapshot out+ refused <- executeWorkspaceScaffoldWithNameMigrations out False False plan+ refused `shouldSatisfy` \case Left [NameMigrationRequired moves] -> length moves == length selectedRows; _ -> False+ treeSnapshot out `shouldReturn` beforeMigration+ applied <- executeWorkspaceScaffoldWithNameMigrations out False True plan+ report <- case applied of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right value -> pure value+ length (wsrNameMoves report) `shouldBe` length selectedRows+ migratedRecord <-+ TIO.readFile recordPath >>= \contents ->+ maybe (expectationFailure "migrated workspace record did not parse" >> fail "unreachable") pure (parseWorkspaceRecord contents)+ wrNamingEdition migratedRecord `shouldBe` IdiomaticNamingV1+ let migratedRows = [row | row <- wrModules migratedRecord, wrmRole row `Map.member` ownersBefore]+ Map.fromList [(wrmRole row, wrmOwner row) | row <- migratedRows] `shouldBe` ownersBefore+ map wrmPath migratedRows `shouldSatisfy` all (not . T.isInfixOf "Project_activity" . T.pack)+ forM_ selectedRows $ \row -> do+ doesFileExist (out </> legacyPath (wrmPath row)) `shouldReturn` False+ doesFileExist (out </> wrmPath row) `shouldReturn` True+ doesFileExist (out </> ".keiro-dsl-name-migrations/legacy-v1-to-idiomatic-v1" </> legacyPath (wrmPath row)) `shouldReturn` True++ describe "sidecar migration (EP-198)" $ do+ it "refuses old context names, applies lossless moves, preserves stale history, and is idempotent" $+ withTempDirectory "keiro-dsl-sidecar-migration" $ \base -> do+ parsed <- parsedSourceOf "test/fixtures/reservation.keiro"+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ sourceLanguage = parsedSourceLanguage parsed+ plain = base </> "plain"+ migrated = base </> "migrated"+ runAt out apply specPath selected =+ executeServiceScaffoldWithRuntimePackageAndNameMigrations+ Nothing+ apply+ out+ False+ specPath+ sourceLanguage+ ctx+ service+ selected+ modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffold ctx service)+ _ <- runAt plain False "reservation.keiro" modules >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ _ <- runAt migrated False "reservation.keiro" modules >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ let reduced = drop 1 modules+ currentLedger = contextLedgerFileName (specContext spec)+ currentFragment = contextCabalFragmentFileName (specContext spec)+ oldLedger = legacyContextRecordFileName (specContext spec)+ oldFragment = legacyContextManifestFileName (specContext spec)+ renameFile (migrated </> currentLedger) (migrated </> oldLedger)+ renameFile (migrated </> currentFragment) (migrated </> oldFragment)+ treeBefore <- treeSnapshot migrated+ refused <- runAt migrated False "reservation-reduced.keiro" reduced+ refused `shouldSatisfy` \case+ Left [SidecarMigrationRequired moves] ->+ length moves == 2+ && all ((== RenameSidecar) . sidecarMoveDisposition) moves+ _ -> False+ renderRefusals (either id (const []) refused)+ `shouldSatisfy` any (T.isInfixOf "--apply-name-migrations")+ treeSnapshot migrated `shouldReturn` treeBefore++ baseline <- runAt plain False "reservation-reduced.keiro" reduced >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ applied <- runAt migrated True "reservation-reduced.keiro" reduced >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ map sidecarMoveDisposition (reportSidecarMoves applied) `shouldBe` [RenameSidecar, RenameSidecar]+ reportStale applied `shouldBe` reportStale baseline+ reportPreviousSpecPath applied `shouldBe` Just "reservation.keiro"+ doesFileExist (migrated </> oldLedger) `shouldReturn` False+ doesFileExist (migrated </> oldFragment) `shouldReturn` False+ doesFileExist (migrated </> currentLedger) `shouldReturn` True+ doesFileExist (migrated </> currentFragment) `shouldReturn` True++ rerun <- runAt migrated True "reservation-reduced.keiro" reduced >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ reportSidecarMoves rerun `shouldBe` []++ let duplicateBytes = "legacy duplicate cabal fragment\n"+ backup = migrated </> ".keiro-dsl-name-migrations/sidecar-v1" </> oldFragment+ TIO.writeFile (migrated </> oldFragment) duplicateBytes+ duplicateRefusal <- runAt migrated False "reservation-reduced.keiro" reduced+ duplicateRefusal `shouldSatisfy` \case+ Left [SidecarMigrationRequired [move]] -> sidecarMoveDisposition move == RetireLegacySidecar+ _ -> False+ retired <- runAt migrated True "reservation-reduced.keiro" reduced >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ map sidecarMoveDisposition (reportSidecarMoves retired) `shouldBe` [RetireLegacySidecar]+ doesFileExist (migrated </> oldFragment) `shouldReturn` False+ TIO.readFile backup `shouldReturn` duplicateBytes++ describe "Haskell.name-diff" $ do+ it "classifies a workqueue payload type rename only on consumer-build" $ do+ base <- specOf "test/fixtures/reservation-work.keiro"+ let renamed = mapWorkqueue (\queue -> queue {wqPayloadName = "ReservationJob"}) base+ findings = generatedHaskellNameFindings (diffSpecs base renamed)+ case findings of+ [finding] -> assertGeneratedHaskellNameFinding finding+ values -> expectationFailure ("expected one payload-name finding, got " <> show (length values))+ let workspaceFindings =+ generatedHaskellNameFindings+ (map wcChange (diffWorkspaces (oneMemberWorkspace "queue.keiro" base) (oneMemberWorkspace "queue.keiro" renamed)))+ workspaceFindings `shouldSatisfy` \case [finding] -> isAdvisory finding; _ -> False+ replayImpactSpecs base renamed `shouldBe` ReplayNeutral+ it "pairs a mapped selector rename by unchanged wire key and keeps fold identity stable" $ do+ source <- readTestText "test/fixtures/consumer-types.keiro"+ base <- parseInlineSpec "<mapped-selector-old>" source+ renamed <-+ parseInlineSpec+ "<mapped-selector-new>"+ (T.replace "key as \"key\"" "artifactKey as \"key\"" source)+ let findings = generatedHaskellNameFindings (diffSpecs base renamed)+ case findings of+ [finding] -> do+ assertGeneratedHaskellNameFinding finding+ ckSubject (kindOfChange finding) `shouldSatisfy` T.isInfixOf "artifactKey"+ values -> expectationFailure ("expected one selector-name finding, got " <> show (length values))+ replayImpactSpecs base renamed `shouldBe` ReplayNeutral+ legacyAggregateFoldFingerprint base (onlyAggregate base)+ `shouldBe` legacyAggregateFoldFingerprint renamed (onlyAggregate renamed)+ it "pairs a workqueue module rename by unchanged explicit runtime facts" $ do+ base <- specOf "test/fixtures/reservation-work.keiro"+ let queueOnly = base {specNodes = [node | node@NWorkqueue {} <- specNodes base]}+ renamed = mapWorkqueue (\queue -> queue {wqName = "reservation_jobs"}) queueOnly+ findings = generatedHaskellNameFindings (diffSpecs queueOnly renamed)+ case findings of+ [finding] -> do+ assertGeneratedHaskellNameFinding finding+ ckFacet (kindOfChange finding) `shouldBe` "workqueue-module"+ values -> expectationFailure ("expected one module-name finding, got " <> show (length values))+ map (ckCode . kindOfChange) (diffSpecs queueOnly renamed) `shouldNotContain` [QueueIdentityChanged]+ replayImpactSpecs queueOnly renamed `shouldBe` ReplayNeutral+ it "emits no finding when edited logical spellings normalize identically" $ do+ base <- specOf "test/fixtures/reservation-work.keiro"+ let queueOnly = base {specNodes = [node | node@NWorkqueue {} <- specNodes base]}+ recased = mapWorkqueue (\queue -> queue {wqName = "reservationWork"}) queueOnly+ generatedHaskellNameFindings (diffSpecs queueOnly recased) `shouldBe` []++ 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 "keeps unrelated aggregate-only specs free of inert-surface warnings" $ do+ codes <- diagnosticCodesOf "test/fixtures/reservation.keiro"+ codes+ `shouldNotContain` [ IntakeBindFlagUnenforced,+ RmInlineSubscriptionIgnored+ ]+ it "reports empty aggregates at their declaration under legacy and stable contracts" $ do+ spec <- specOf "test/fixtures/reservation.keiro"+ case [aggregate | NAggregate aggregate <- specNodes spec] of+ aggregate : _ -> do+ let emptyAggregate = aggregate {aggCommands = [], aggEvents = [], aggTransitions = []}+ emptySpec = spec {specNodes = [NAggregate emptyAggregate]}+ expectedLine = unLoc (aggLoc aggregate)+ planningCodes = [GeneratedPathCollision, GeneratedImportCycle, BehaviorDerivationInvalid, ConformanceFactKeyCollision, GeneratedPlanningInvariantViolation]+ expectedMessage =+ "aggregate 'Reservation' declares no commands, no events, and no transitions; scaffold cannot lower an empty aggregate -- declare at least one command, one event, and one transition"+ forM_ [legacyCheckedService emptySpec, stableCheckedService emptySpec] $ \service -> do+ let diagnostics = checkServiceDiagnostics Nothing (defaultContext (specContext emptySpec)) service+ [ (severity diagnostic, line diagnostic, message diagnostic)+ | diagnostic <- diagnostics,+ code diagnostic == AggregateEmpty+ ]+ `shouldBe` [(Error, expectedLine, expectedMessage)]+ filter (`elem` planningCodes) (map code diagnostics) `shouldBe` []+ scaffoldRefusals emptySpec `shouldSatisfy` any (T.isPrefixOf "AggregateEmpty:")+ [] -> expectationFailure "reservation fixture has no aggregate"+ it "reports empty contracts at their declaration under legacy and stable contracts" $ do+ spec <- specOf "test/fixtures/contract-v4.keiro"+ case [contract | NContract contract <- specNodes spec] of+ contract : _ -> do+ let emptyContract = contract {ctrEvents = []}+ emptySpec = spec {specNodes = [NContract emptyContract]}+ expectedLine = unLoc (ctrLoc contract)+ planningCodes = [GeneratedPathCollision, GeneratedImportCycle, BehaviorDerivationInvalid, ConformanceFactKeyCollision, GeneratedPlanningInvariantViolation]+ expectedMessage =+ "contract 'emergency' declares no events; scaffold cannot lower an empty contract -- declare at least one event"+ forM_ [legacyCheckedService emptySpec, stableCheckedService emptySpec] $ \service -> do+ let diagnostics = checkServiceDiagnostics Nothing (defaultContext (specContext emptySpec)) service+ [ (severity diagnostic, line diagnostic, message diagnostic)+ | diagnostic <- diagnostics,+ code diagnostic == ContractEmpty+ ]+ `shouldBe` [(Error, expectedLine, expectedMessage)]+ filter (`elem` planningCodes) (map code diagnostics) `shouldBe` []+ scaffoldRefusals emptySpec `shouldSatisfy` any (T.isPrefixOf "ContractEmpty:")+ [] -> expectationFailure "contract fixture has no contract"+ it "keeps a check-time error counterpart for every sampled lowering refusal class" $ do+ emitSource <- readTestText "test/fixtures/emit.keiro"+ incompleteBackoff <- parseInlineSpec "<incomplete-backoff-parity>" (T.replace "backoff constant 2s" "backoff exponential 2s" emitSource)+ baseAggregate <- parseInlineSpec "<lowering-parity>" loweringAggregateSpec+ bareTextInitial <- parseInlineSpec "<bare-text-initial-parity>" (T.replace "\"hello world\"" "hello" loweringAggregateSpec)+ unsupportedField <- parseInlineSpec "<unsupported-field-parity>" (T.replace "count:Int" "count:Json" loweringAggregateSpec)+ mappedInitial <- specOf "test/fixtures/mapped-missing-initial.keiro"+ let candidates =+ [ ("incomplete publisher backoff", incompleteBackoff),+ ("invalid register initial", bareTextInitial),+ ("unrepresentable aggregate field", unsupportedField),+ ("missing mapped register initial", mappedInitial)+ ]+ scaffoldRefusals baseAggregate `shouldBe` []+ forM_ candidates $ \(caseLabel, candidate) ->+ unless+ (not (null (scaffoldRefusals candidate)) && any ((== Error) . severity) (validateSpec candidate))+ (expectationFailure (caseLabel <> " did not fail at both check and scaffold planning"))+ it "rejects policy words that generated Haskell cannot lower" $ do+ emitSpec <- specOf "test/fixtures/emit.keiro"+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ let unknownOrdering = mapPublisher (\publisher -> publisher {pubOrdering = "banana"}) emitSpec+ unknownBackoff =+ mapPublisher+ (\publisher -> publisher {pubBackoff = (pubBackoff publisher) {boKind = "banana"}})+ emitSpec+ incompleteBackoff =+ mapPublisher+ (\publisher -> publisher {pubBackoff = BackoffSpec "exponential" "2s" Nothing Nothing})+ emitSpec+ unknownDedupe = mapIntake (\intake -> intake {inkDedupePolicy = "Banana"}) intakeSpec+ errorCodes unknownOrdering `shouldContain` [PublisherOrderingUnknown]+ errorCodes unknownBackoff `shouldContain` [PublisherBackoffInvalid]+ errorCodes incompleteBackoff `shouldContain` [PublisherBackoffInvalid]+ errorCodes unknownDedupe `shouldContain` [IntakeDedupePolicyUnknown]+ it "gates numeric floors on the unreleased language-4 contract" $ do+ emitSpec <- specOf "test/fixtures/emit.keiro"+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ readModelSpec <- specOf "test/fixtures/workflow.keiro"+ let zeroContract = mapContract (\contract -> contract {ctrSchemaVersion = 0}) emitSpec+ zeroAttempts = mapPublisher (\publisher -> publisher {pubMaxAttempts = 0}) emitSpec+ zeroDecode = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decBodySchemaVersion = 0}}) intakeSpec+ zeroReadModel = modifyReadModel "transferDecision" (\readModel -> readModel {rmVersion = 0}) readModelSpec+ floors =+ [ (zeroContract, ContractSchemaVersionBelowMinimum),+ (zeroAttempts, PublisherMaxAttemptsBelowMinimum),+ (zeroDecode, IntakeDecodeSchemaVersionBelowMinimum),+ (zeroReadModel, ReadModelVersionBelowMinimum)+ ]+ forM_ floors $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ it "rejects duplicate declarations whose generated behavior cannot work" $ do+ reservation <- specOf "test/fixtures/reservation.keiro"+ integration <- specOf "test/fixtures/emit.keiro"+ let duplicateCommandField =+ modifyAggregate+ "Reservation"+ (\aggregate -> aggregate {aggCommands = updateFirst (\command -> command {cmdFields = duplicateFirst (cmdFields command)}) (aggCommands aggregate)})+ reservation+ duplicateState = modifyAggregate "Reservation" (\aggregate -> aggregate {aggStates = duplicateFirst (aggStates aggregate)}) reservation+ duplicateTransition =+ modifyAggregate+ "Reservation"+ (\aggregate -> aggregate {aggTransitions = aggTransitions aggregate <> take 1 (reverse (aggTransitions aggregate))})+ reservation+ duplicateContractField =+ mapContract+ (\contract -> contract {ctrEvents = updateFirst (\event -> event {ceFields = duplicateFirst (ceFields event)}) (ctrEvents contract)})+ integration+ duplicateContractEvent = mapContract (\contract -> contract {ctrEvents = duplicateFirst (ctrEvents contract)}) integration+ duplicateTopicAlias = mapContract (\contract -> contract {ctrTopics = duplicateFirst (ctrTopics contract)}) integration+ cases =+ [ (duplicateCommandField, AggregateDuplicateFieldName),+ (duplicateState, AggregateDuplicateState),+ (duplicateTransition, TransitionDuplicateUnguarded),+ (duplicateContractField, ContractDuplicateFieldName),+ (duplicateContractEvent, ContractDuplicateEvent),+ (duplicateTopicAlias, ContractDuplicateTopicAlias)+ ]+ forM_ cases $ \(candidate, expected) -> errorCodes candidate `shouldContain` [expected]+ it "gates ambiguous and silently shadowed duplicate surfaces on language 4" $ do+ reservation <- specOf "test/fixtures/reservation.keiro"+ integration <- specOf "test/fixtures/emit.keiro"+ let duplicateRegister = modifyAggregate "Reservation" (\aggregate -> aggregate {aggRegs = duplicateFirst (aggRegs aggregate)}) reservation+ duplicateNominal = reservation {specIds = duplicateFirst (specIds reservation)}+ duplicateMap = mapEmit (\emitNode -> emitNode {emMap = duplicateFirst (emMap emitNode)}) integration+ shadowDiscriminator =+ mapContract+ ( \contract ->+ contract+ { ctrEvents =+ updateFirst+ (\event -> event {ceFields = updateFirst (\field -> field {cfName = ctrDiscriminator contract}) (ceFields event)})+ (ctrEvents contract)+ }+ )+ integration+ guardedSibling =+ modifyAggregate+ "Reservation"+ ( \aggregate ->+ aggregate+ { aggTransitions =+ aggTransitions aggregate+ <> [transition {tGuard = Just (EAtom (ABool True))} | transition <- take 1 (reverse (aggTransitions aggregate))]+ }+ )+ reservation+ cases =+ [ (duplicateRegister, AggregateDuplicateRegister),+ (duplicateNominal, NominalDuplicateDeclaration),+ (duplicateMap, EmitMapDuplicateCase),+ (shadowDiscriminator, ContractFieldShadowsDiscriminator),+ (guardedSibling, TransitionUnguardedSibling)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ it "gates stable identities and external names on language 4" $ do+ workflowSpec <- specOf "test/fixtures/workflow.keiro"+ processSpec <- specOf "test/fixtures/surge-service.keiro"+ routerSpec <- specOf "test/fixtures/transfer-routing.keiro"+ integration <- specOf "test/fixtures/emit.keiro"+ let invalidIdentity = mapWorkflow (\workflow -> workflow {wfStable = ""}) workflowSpec+ duplicateIdentity =+ processSpec+ { specNodes =+ specNodes processSpec+ <> [NRouter (router {rtName = "surge-demo"}) | NRouter router <- specNodes routerSpec]+ }+ invalidTopic = mapContract (\contract -> contract {ctrTopics = [(alias, "bad topic") | (alias, _) <- ctrTopics contract]}) integration+ emptyTopic = mapContract (\contract -> contract {ctrTopics = [(alias, "") | (alias, _) <- ctrTopics contract]}) integration+ invalidReadModel = modifyReadModel "transferDecision" (\readModel -> readModel {rmTable = "Bad-Table"}) workflowSpec+ duplicateColumn = modifyReadModel "transferDecision" (\readModel -> readModel {rmColumns = duplicateFirst (rmColumns readModel)}) workflowSpec+ gatedCases =+ [ (invalidIdentity, RuntimeIdentityInvalid),+ (duplicateIdentity, RuntimeIdentityDuplicate),+ (invalidTopic, ContractTopicNameInvalid),+ (invalidReadModel, ReadModelIdentifierInvalid),+ (duplicateColumn, ReadModelDuplicateColumn)+ ]+ forM_ gatedCases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ serviceErrorCodes 3 emptyTopic `shouldContain` [ContractTopicNameInvalid]+ serviceErrorCodes 4 emptyTopic `shouldContain` [ContractTopicNameInvalid]+ it "gates declared integration and wire couplings on language 4" $ do+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ reservation <- specOf "test/fixtures/reservation.keiro"+ let unresolvedBind =+ mapIntake+ (\intake -> intake {inkBinds = updateFirst (\binding -> binding {brField = "ghost"}) (inkBinds intake)})+ intakeSpec+ acceptedEventBind =+ mapIntake+ (\intake -> intake {inkBinds = updateFirst (\binding -> binding {brField = "region"}) (inkBinds intake)})+ intakeSpec+ unresolvedDedupe = mapIntake (\intake -> intake {inkDedupeKey = "ghost"}) intakeSpec+ unknownEnvelope = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decEnvelope = "banana policy"}}) intakeSpec+ mismatchedSchema = mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decBodySchemaVersion = 2}}) intakeSpec+ unresolvedAlias =+ mapContract+ (\contract -> contract {ctrEvents = updateFirst (\event -> event {ceTopic = "ghost"}) (ctrEvents contract)})+ intakeSpec+ unsupportedWire =+ modifyAggregate+ "Reservation"+ (\aggregate -> aggregate {aggWire = fmap (\wire -> wire {wireKind = "banana"}) (aggWire aggregate)})+ reservation+ cases =+ [ (unresolvedBind, IntakeBindUnresolved),+ (unresolvedDedupe, IntakeDedupeKeyUnresolved),+ (unknownEnvelope, IntakeEnvelopePolicyUnknown),+ (mismatchedSchema, IntakeDecodeSchemaVersionMismatch),+ (unresolvedAlias, ContractTopicAliasUnresolved),+ (unsupportedWire, WireClauseUnsupported)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ serviceErrorCodes 4 acceptedEventBind `shouldNotContain` [IntakeBindUnresolved]+ it "gates closed workqueue vocabularies and bounded windows on language 4" $ do+ queueSpec <- specOf "test/fixtures/reservation-work.keiro"+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ emitSpec <- specOf "test/fixtures/emit.keiro"+ processSpec <- specOf "test/fixtures/hospital-surge.keiro"+ let huge = "18446744073709551618s"+ unknownPayload =+ mapWorkqueue+ (\queue -> queue {wqPayload = [if wqfName field == "hospitalId" then field {wqfType = "numeric"} else field | field <- wqPayload queue]})+ queueSpec+ queueDelay = mapWorkqueue (\queue -> queue {wqDelay = huge}) queueSpec+ queueRetry = mapWorkqueue (\queue -> queue {wqDisposition = updateFirst (\row -> row {wqdAction = IRetry huge}) (wqDisposition queue)}) queueSpec+ intakeRetry = mapIntake (\intake -> intake {inkDisposition = updateFirst (\row -> row {drAction = IRetry huge}) (inkDisposition intake)}) intakeSpec+ publisherBackoff = mapPublisher (\publisher -> publisher {pubBackoff = (pubBackoff publisher) {boWindow = huge}}) emitSpec+ publisherMaximum =+ mapPublisher+ (\publisher -> publisher {pubBackoff = (pubBackoff publisher) {boKind = "exponential", boMax = Just huge, boMultiplier = Just "2"}})+ emitSpec+ processFireAt =+ modifyProcess+ "HospitalSurge"+ (\process -> process {procTimer = (procTimer process) {tmFireAt = (tmFireAt (procTimer process)) {faWindow = huge}}})+ processSpec+ cases =+ [ (unknownPayload, WqPayloadTypeUnknown),+ (queueDelay, WindowOutOfRange),+ (queueRetry, WindowOutOfRange),+ (intakeRetry, WindowOutOfRange),+ (publisherBackoff, WindowOutOfRange),+ (publisherMaximum, WindowOutOfRange),+ (processFireAt, WindowOutOfRange)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldBe` []+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ -- ExecPlan 199: spellings the grammar accepted that no runtime implements.+ -- Each pair asserts both halves of the contract — the divergent spelling+ -- warns at 3 and errors at 4, and the spelling that matches the runtime+ -- stays completely silent, so these are refusals and not blanket noise.+ it "refuses spec surfaces that contradict the runtime, and stays silent on the ones that describe it" $ do+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ processSpec <- specOf "test/fixtures/hospital-surge.keiro"+ routerSpec <- specOf "test/fixtures/transfer-routing.keiro"+ let lenientBody =+ mapIntake (\intake -> intake {inkDecode = (inkDecode intake) {decBodyStrict = False}}) intakeSpec+ unknownHeader =+ mapIntake+ (\intake -> intake {inkBinds = updateFirst (\binding -> binding {brSource = SrcHeader "x-custom"}) (inkBinds intake)})+ intakeSpec+ retryOnAppended =+ modifyProcess+ "HospitalSurge"+ ( \process ->+ process+ { procHandle =+ (procHandle process)+ { hDispatch =+ updateFirst+ (\d -> d {dispDisposition = (dispDisposition d) {onAppended = DRetry}})+ (hDispatch (procHandle process))+ }+ }+ )+ processSpec+ firedNotMine =+ modifyProcess+ "HospitalSurge"+ ( \process ->+ let timer = procTimer process+ fire = tmFire timer+ in process+ { procTimer =+ timer {tmFire = fire {fireDisposition = (fireDisposition fire) {notMine = OFired}}}+ }+ )+ processSpec+ routerRetryOnAppended =+ mapRouter+ (\router -> router {rtDispatch = (rtDispatch router) {rdDisposition = (rdDisposition (rtDispatch router)) {onAppended = DRetry}}})+ routerSpec+ cases =+ [ (lenientBody, DecodeBodyPostureUnsupported),+ (unknownHeader, IntakeBindHeaderUnknown),+ (retryOnAppended, DispatchOnAppendedUnsupported),+ (firedNotMine, TimerNotMineUnsupported),+ (routerRetryOnAppended, DispatchOnAppendedUnsupported)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceWarningCodes 3 candidate `shouldContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]++ -- The unmutated fixtures spell every one of these the way the runtime+ -- behaves, so language 4 has nothing to say about them.+ let closedCodes =+ [ DecodeBodyPostureUnsupported,+ IntakeBindHeaderUnknown,+ DispatchOnAppendedUnsupported,+ TimerNotMineUnsupported+ ]+ forM_ [intakeSpec, processSpec, routerSpec] $ \accepted -> do+ serviceErrorCodes 4 accepted `shouldNotContain` closedCodes+ serviceWarningCodes 4 accepted `shouldNotContain` closedCodes++ -- ExecPlan 197 parked these three as "explicitly descriptive-only"; ExecPlan+ -- 199 re-adjudicated each against the path it purports to describe and found+ -- a checkable referent in every one.+ it "checks the references the formerly descriptive-only surfaces name" $ do+ processSpec <- specOf "test/fixtures/hospital-surge.keiro"+ dispatchSpec <- specOf "test/fixtures/reservation-work.keiro"+ let unknownStatus =+ modifyProcess+ "HospitalSurge"+ (\process -> process {procTimer = (procTimer process) {tmDecodeUnknown = "Abandoned"}})+ processSpec+ blankDeadLetter =+ modifyProcess+ "HospitalSurge"+ (\process -> process {procTimer = (procTimer process) {tmDeadLetter = " "}})+ processSpec+ phantomDedupeKey =+ mapPgmqDispatch (\d -> d {pdDedupKey = "ghostKey"}) dispatchSpec+ uppercaseFanout =+ mapPgmqDispatch (\d -> d {pdFanoutBody = "ResolveTransferCandidates"}) dispatchSpec+ cases =+ [ (unknownStatus, TimerDecodeStatusUnknown),+ (blankDeadLetter, TimerDeadLetterTextInvalid),+ (phantomDedupeKey, DispatchReadModelFieldUnknown),+ (uppercaseFanout, PgmqFanoutFunctionInvalid)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceWarningCodes 3 candidate `shouldContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]++ -- Every timer status the runtime actually stores is accepted.+ forM_ ["Scheduled", "Firing", "Fired", "Cancelled", "Dead"] $ \status ->+ serviceErrorCodes+ 4+ (modifyProcess "HospitalSurge" (\p -> p {procTimer = (procTimer p) {tmDecodeUnknown = status}}) processSpec)+ `shouldNotContain` [TimerDecodeStatusUnknown]++ serviceErrorCodes 4 processSpec `shouldNotContain` [TimerDecodeStatusUnknown, TimerDeadLetterTextInvalid]+ serviceErrorCodes 4 dispatchSpec `shouldNotContain` [PgmqFanoutFunctionInvalid]++ it "holds a process dispatch-id line to the same strictness as a router's" $ do+ -- Both lines document a derivation the spec cannot change, but the two+ -- runtimes key on different tuples: Keiro.ProcessManager on+ -- (name, correlationId, sourceEventId, emitIndex) and Keiro.Router on+ -- (name, key, sourceEventId, targetStreamName, occurrence). Before+ -- ExecPlan 199 the process line accepted any strategy and any tuple.+ processSource <- readTestText "test/fixtures/hospital-surge.keiro"+ let processLine = "dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId, emitIndex)"+ rejected =+ [ "dispatch-id strategy=md5 from=(name, correlationId, sourceEventId, emitIndex)",+ "dispatch-id strategy=uuidv5 from=(banana)",+ "dispatch-id strategy=uuidv5 from=(name, correlationId, sourceEventId)",+ -- The router's tuple is not the process's tuple.+ "dispatch-id strategy=uuidv5 from=(name, key, sourceEventId, targetStreamName, occurrence)"+ ]+ processSource `shouldSatisfy` T.isInfixOf processLine+ parseSpec "accepted" processSource `shouldSatisfy` isRight+ forM_ rejected $ \badLine ->+ parseSpec "mutated" (T.replace processLine badLine processSource)+ `shouldSatisfy` isLeft++ it "gates the remaining locally resolvable identity and field surfaces on language 4" $ do+ reservation <- specOf "test/fixtures/reservation.keiro"+ emitSpec <- specOf "test/fixtures/emit.keiro"+ processSpec <- specOf "test/fixtures/hospital-surge.keiro"+ dispatchSpec <- specOf "test/fixtures/reservation-work.keiro"+ readModelSpec <- specOf "test/fixtures/readmodel.keiro"+ let projectionKey = modifyAggregate "Reservation" (\aggregate -> aggregate {aggProjection = fmap (\projection -> projection {projKey = "ghost"}) (aggProjection aggregate)}) reservation+ outboxField = mapPublisher (\publisher -> publisher {pubOutboxField = "ghost"}) emitSpec+ timerIds =+ modifyProcess+ "HospitalSurge"+ ( \process ->+ let timer = procTimer process+ fire = tmFire timer+ in process+ { procTimer =+ timer+ { tmId = (tmId timer) {ideField = "ghostTimerKey"},+ tmFire = fire {fireFiredEventId = (fireFiredEventId fire) {ideField = "ghostEventKey"}}+ }+ }+ )+ processSpec+ sourceKey = mapDispatch (\dispatch -> dispatch {pdSourceKey = "ghost"}) dispatchSpec+ subscriptionIdentity = modifyReadModel "transfer_decisions" (\readModel -> readModel {rmSubscription = Just "bad subscription"}) readModelSpec+ scopeIdentity = modifyReadModel "transfer_decisions" (\readModel -> readModel {rmScope = Just (RmCategory "bad-category")}) readModelSpec+ cases =+ [ (projectionKey, AggProjectionKeyUnresolved),+ (outboxField, PublisherOutboxFieldUnresolved),+ (timerIds, TimerIdFieldNotCorrelation),+ (sourceKey, DispatchReadModelFieldUnknown),+ (subscriptionIdentity, RuntimeIdentityInvalid),+ (scopeIdentity, RuntimeIdentityInvalid)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldBe` []+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ length (filter (== TimerIdFieldNotCorrelation) (serviceErrorCodes 4 timerIds)) `shouldBe` 2+ parseStableRenderedSpec "<timer-id-fields>" timerIds `shouldBe` Right timerIds+ it "uses a router-specific code for a confirmed duplicate inversion" $ do+ spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+ let changed =+ modifyRouter+ "PagingRouter"+ ( \router ->+ let dispatch = rtDispatch router+ disposition = rdDisposition dispatch+ in router {rtDispatch = dispatch {rdDisposition = disposition {onDuplicate = DAckOk}}}+ )+ spec+ warningCodes = [code diagnostic | diagnostic <- validateSpec changed, severity diagnostic == Warning]+ warningCodes `shouldContain` [RouterBenignInversion]+ warningCodes `shouldNotContain` [ProcessBenignInversion]+ it "pins every emitted legacy single-spec diagnostic that lacked a direct negative test" $ do+ reservation <- specOf "test/fixtures/reservation.keiro"+ intakeSpec <- specOf "test/fixtures/intake.keiro"+ emitSpec <- specOf "test/fixtures/emit.keiro"+ processSpec <- specOf "test/fixtures/surge-service.keiro"+ queueSpec <- specOf "test/fixtures/reservation-work.keiro"+ workflowSpec <- specOf "test/fixtures/workflow.keiro"+ let updateFirstTransition update aggregate = aggregate {aggTransitions = updateFirst update (aggTransitions aggregate)}+ undeclaredEvent = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tEmits = ["GhostEvent"]})) reservation+ undeclaredState = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tGoto = "GhostState"})) reservation+ terminalOutgoing = modifyAggregate "Reservation" (updateFirstTransition (\transition -> transition {tSource = "Expired"})) reservation+ deprecatedEmitted = modifyAggregate "Reservation" (\aggregate -> aggregate {aggEvents = updateFirst (\event -> event {evDeprecated = True}) (aggEvents aggregate)}) reservation+ wireVersionMismatch = modifyAggregate "Reservation" (\aggregate -> aggregate {aggWire = fmap (\wire -> wire {wireSchemaVersion = 2}) (aggWire aggregate)}) reservation+ decodeRetry =+ mapIntake+ ( \intake ->+ intake+ { inkDisposition =+ [ if drOutcome row == "decodeFailed" then row {drAction = IRetry "5s"} else row+ | row <- inkDisposition intake+ ]+ }+ )+ intakeSpec+ unresolvedPublisher = mapPublisher (\publisher -> publisher {pubEmit = "ghost"}) emitSpec+ unresolvedIntake = mapIntake (\intake -> intake {inkContract = "ghost"}) intakeSpec+ unboundedQueue = mapWorkqueue (\queue -> queue {wqMaxRetries = 0}) queueSpec+ unresolvedEnqueue = mapDispatch (\dispatch -> dispatch {pdEnqueueTo = "ghost"}) queueSpec+ unresolvedWorkflow =+ mapOperation+ ( \operation -> case opShape operation of+ RunOp _ input outcome -> operation {opShape = RunOp "GhostWorkflow" input outcome}+ _ -> operation+ )+ workflowSpec+ cases =+ [ (undeclaredEvent, UndeclaredEvent),+ (undeclaredState, UndeclaredState),+ (terminalOutgoing, TerminalHasOutgoing),+ (deprecatedEmitted, DeprecatedEventStillEmitted),+ (wireVersionMismatch, WireSchemaVersionMismatch),+ (processSpec, ProcessBenignInversion),+ (decodeRetry, DispositionDecodeUnboundedRetry),+ (unresolvedPublisher, PublisherUnresolvedEmit),+ (unresolvedIntake, IntakeUnresolvedContract),+ (unboundedQueue, WqDlqWithoutCeiling),+ (unresolvedEnqueue, DispatchEnqueueUnresolved),+ (unresolvedWorkflow, RunWorkflowUnresolved)+ ]+ forM_ cases $ \(candidate, expected) -> diagnosticCodes candidate `shouldContain` [expected]+ 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 `shouldBe` []+ 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"+ mapM_ (\expected -> codes `shouldContain` [expected]) [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"+ parseStableRenderedSpec "<retiring-round-trip>" 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` []+ parseStableRenderedSpec "<snapshot-round-trip>" spec `shouldBe` Right spec+ case [aggregate | NAggregate aggregate <- specNodes spec] of+ [aggregate] -> aggSnapshot aggregate `shouldBe` Just (SnapshotSpec (SnapEvery 100) 1 "7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28" 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=\"7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28\"" "shape-hash=\"\"" source)+ map code (validateSpec emptyHash) `shouldContain` [SnapshotCodecFixtureInvalid]+ it "conditionally lowers JSON instances and the live defaultStateCodec" $ do+ snapshotService <- checkedServiceOf "test/fixtures/reservation-snapshot.keiro"+ ordinaryService <- checkedServiceOf "test/fixtures/reservation.keiro"+ let snapshot = checkedSpec snapshotService+ ordinary = checkedSpec ordinaryService+ case ([aggregate | NAggregate aggregate <- specNodes snapshot], [aggregate | NAggregate aggregate <- specNodes ordinary]) of+ ([_], [_]) -> do+ let snapshotModules = scaffoldServiceModules (defaultContext (specContext snapshot)) snapshotService+ ordinaryModules = scaffoldServiceModules (defaultContext (specContext ordinary)) ordinaryService+ 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 "reservationSnapshotFixture = (1, \"7a181ceb7d798d883d28c85201c5c1692bd314a7b489da9128bff91e0f38cd28\")"+ 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 <- shouldParseStableRenderedSpec "<second>" first+ 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 -> parseStableRenderedSpec "in" 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]+ it "gates process correlate, dispatch-key, and binding scopes on language 4" $ do+ spec <- specOf "test/fixtures/hospital-surge.keiro"+ let badCorrelate =+ modifyProcess+ "HospitalSurge"+ (\process -> process {procCorrelate = (procCorrelate process) {corrField = "ghost"}})+ spec+ badDispatchKey =+ modifyProcess+ "HospitalSurge"+ ( \process ->+ let handle = procHandle process+ in process {procHandle = handle {hDispatch = updateFirst (\dispatch -> dispatch {dispKey = "input.ghost"}) (hDispatch handle)}}+ )+ spec+ badBinding =+ modifyProcess+ "HospitalSurge"+ ( \process ->+ let handle = procHandle process+ advance = hAdvance handle+ in process {procHandle = handle {hAdvance = advance {advFields = updateFirst (\binding -> binding {fbValue = Just "ghost.value"}) (advFields advance)}}}+ )+ spec+ cases =+ [ (badCorrelate, ProcessKeyFieldUnknown),+ (badDispatchKey, ProcessDispatchKeyUnresolved),+ (badBinding, ProcessBindingUnscoped)+ ]+ forM_ cases $ \(candidate, expected) -> do+ serviceErrorCodes 3 candidate `shouldNotContain` [expected]+ serviceErrorCodes 4 candidate `shouldContain` [expected]+ serviceErrorCodes 4 spec+ `shouldNotContain` [ProcessKeyFieldUnknown, ProcessDispatchKeyUnresolved, ProcessBindingUnscoped]++ 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 -> parseStableRenderedSpec "in" 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 "gates resolve-row column verification on language 4" $ do+ spec <- specOf "test/fixtures/incident-paging/incident-paging.keiro"+ let unresolved =+ modifyRouter+ "PagingRouter"+ (\router -> router {rtResolve = (rtResolve router) {rvRow = ["ghostColumn"]}})+ spec+ serviceErrorCodes 3 unresolved `shouldNotContain` [RouterReadModelUnverified]+ serviceErrorCodes 4 unresolved `shouldContain` [RouterReadModelUnverified]+ serviceErrorCodes 4 spec `shouldNotContain` [RouterReadModelUnverified]+ 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 <- legacyScaffoldProcessFixture "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 "import Generated.HospitalCapacity.Surge.EventStream (SurgeEventStreamDef)"+ moduleText generatedModule `shouldSatisfy` T.isInfixOf "hospitalSurgeCategory :: Stream.StreamCategory SurgeEventStreamDef"+ 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 <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+ b <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+ map moduleText a `shouldBe` map moduleText b+ it "separates aggregate event-stream and command-target categories and emits stable typed sums" $ do+ spec <- specOf "test/fixtures/hospital-surge.keiro"+ let ctx = defaultContext (specContext spec)+ modules = concat [scaffoldAggregate ctx spec aggregate | NAggregate aggregate <- specNodes spec]+ surgeStream = generatedTextEndingIn "Surge/EventStream.hs" modules+ surgeDomain = generatedTextEndingIn "Surge/Domain.hs" modules+ surgeStream `shouldSatisfy` T.isInfixOf "surgeCategory :: Stream.StreamCategory SurgeEventStreamDef"+ surgeStream `shouldSatisfy` T.isInfixOf "surgeCommandCategory :: Stream.StreamCategory SurgeCommand"+ surgeDomain `shouldNotSatisfy` T.isInfixOf "{-# LANGUAGE EmptyDataDecls #-}"+ surgeDomain `shouldSatisfy` T.isInfixOf "data SurgeEvent = SurgeThresholdNoted"+ surgeDomain `shouldSatisfy` (not . T.isInfixOf "data SurgeEvent = ()")++ 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 "branches contract scaffolding, manifests, and durable identities only for language 4" $ do+ sourceText <- readTestText "test/fixtures/contract-v4.keiro"+ parsed <- case parseSource "contract-v4.keiro" sourceText of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ contract <- case [value | NContract value <- specNodes spec] of+ [value] -> pure value+ values -> expectationFailure ("expected one contract, got " <> show (length values)) >> fail "unreachable"+ legacyModule <- case scaffoldContract ctx contract of+ [value] -> pure value+ values -> expectationFailure ("expected one legacy module, got " <> show (length values)) >> fail "unreachable"+ typedModule <- case scaffoldContractForService ctx service contract of+ [value] -> pure value+ values -> expectationFailure ("expected one typed module, got " <> show (length values)) >> fail "unreachable"+ let dependencies = manifestDependenciesForService service+ identities = idDomainIdentitiesForService service+ manifestText = renderManifestForService "contract-v4.keiro" [typedModule] service+ assertGeneratedHaskellContract "contract-v4.keiro" manifestText+ committed <- readTestText "test/conformance-contract/Generated/HospitalCapacity/Emergency/Contract.hs"+ normalizeGenerated (moduleText typedModule) `shouldBe` normalizeGenerated committed+ moduleText legacyModule `shouldSatisfy` T.isInfixOf "incidentId :: !Text"+ moduleText legacyModule `shouldSatisfy` (not . T.isInfixOf "KindID")+ moduleText typedModule `shouldSatisfy` T.isInfixOf "incidentId :: !(KindID \"inc\")"+ moduleText typedModule `shouldSatisfy` T.isInfixOf "KindID.toText payload.incidentId"+ moduleText typedModule `shouldSatisfy` T.isInfixOf "explicitParseField (parseKindIdV7Value @\"inc\") o \"incidentId\""+ moduleText typedModule `shouldSatisfy` T.isInfixOf " , incidentEventsTopic"+ moduleText typedModule `shouldSatisfy` T.isInfixOf " , hospitalEventsTopic"+ moduleText typedModule `shouldSatisfy` (not . T.isInfixOf "Wno-unused-top-binds")+ dependencies `shouldBe` ["aeson", "base", "keiro-core", "mmzk-typeid", "text"]+ manifestDependencies spec `shouldBe` ["aeson", "base", "text"]+ forM_ dependencies $ \dependency -> manifestText `shouldSatisfy` T.isInfixOf (" , " <> dependency)+ identities+ `shouldBe` [ "id-domain|name=contract:emergency.IncidentTransferNeedDeclared.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text",+ "id-domain|name=contract:emergency.TransferReservationAccepted.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text",+ "id-domain|name=contract:emergency.TransferReservationAccepted.reservationId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=rsv|separator=_|json=canonical-json-text",+ "id-domain|name=contract:emergency.TransferReservationAccepted.hospitalId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=hsp|separator=_|json=canonical-json-text"+ ]++ it "persists contract ID domains in single-file and workspace records with owner attribution" $ do+ sourceText <- readTestText "test/fixtures/contract-v4.keiro"+ parsed <- case parseSource "contract-v4.keiro" sourceText of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right value -> pure value+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ modules = scaffoldServiceModules ctx service+ identities = idDomainIdentitiesForService service+ duplicateIdentity <- case identities of+ value : _ -> pure value+ [] -> expectationFailure "typed contract service did not expose ID-domain identities" >> fail "unreachable"+ withTempDirectory "keiro-dsl-v4-contract-record" $ \out -> do+ result <- executeServiceScaffold out False "contract-v4.keiro" (parsedSourceLanguage parsed) ctx service modules+ result `shouldSatisfy` isRight+ contents <- TIO.readFile (out </> recordFileName (specContext spec))+ record <- maybe (expectationFailure "typed contract scaffold record did not parse" >> fail "unreachable") pure (parseRecord contents)+ recIdDomains record `shouldBe` identities+ parseRecord (contents <> "id-domain " <> duplicateIdentity <> "\n") `shouldBe` Nothing++ let manifest = "service hospital-capacity\nspec domain/contract.keiro\n"+ source = memoryContentSource (Map.fromList [("service.keiro-workspace", manifest), ("domain/contract.keiro", sourceText)])+ loaded <- loadWorkspace source "service.keiro-workspace"+ workspace <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure loaded+ workspacePlan <- either (\refusals -> expectationFailure (show refusals) >> fail "unreachable") pure (planWorkspaceScaffold "goldens" ctx workspace)+ case [provenance | (scaffoldModule, provenance) <- wpModules workspacePlan, modulePath scaffoldModule == "Generated/HospitalCapacity/Emergency/Contract.hs"] of+ [MemberOwned owner] -> owner `shouldBe` "domain/contract.keiro"+ values -> expectationFailure ("expected one member-owned contract module, got " <> show values)+ withTempDirectory "keiro-dsl-v4-contract-workspace-record" $ \out -> do+ result <- executeWorkspaceScaffold out False workspacePlan+ result `shouldSatisfy` isRight+ contents <- TIO.readFile (out </> workspaceRecordFileName (wsService workspace))+ record <- maybe (expectationFailure "typed contract workspace record did not parse" >> fail "unreachable") pure (parseWorkspaceRecord contents)+ wrIdDomains record `shouldBe` identities+ [(wrmPath row, wrmOwner row) | row <- wrModules record, wrmPath row == "Generated/HospitalCapacity/Emergency/Contract.hs"]+ `shouldBe` [("Generated/HospitalCapacity/Emergency/Contract.hs", Just "domain/contract.keiro")]+ parseWorkspaceRecord (contents <> "id-domain " <> duplicateIdentity <> "\n") `shouldBe` Nothing+ it "round-trips the contract spec through parse . pretty" $ do+ input <- readTestText "test/fixtures/contract.keiro"+ case parseSource "in" input of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure))+ Right source -> parseSource "in" (renderSource source) `shouldBe` Right source+ 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 "warns when intake bind flags describe unenforced generated behavior" $ do+ codes <- diagnosticCodesOf "test/fixtures/intake.keiro"+ codes `shouldContain` [IntakeBindFlagUnenforced]+ 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"+ inbox `shouldSatisfy` T.isInfixOf "data IncidentInboxOutcome"+ inbox `shouldSatisfy` T.isInfixOf "data IncidentInboxDisposition"+ inbox `shouldSatisfy` T.isInfixOf "InboxRetryAfter !RetryDelay !(Maybe InboxFailure)"+ inbox `shouldSatisfy` T.isInfixOf "InboxDeadLetter !(Maybe Text) !(Maybe InboxFailure)"+ inbox `shouldSatisfy` T.isInfixOf "InboxHandlerFailed reason attempts ->"+ inbox `shouldNotSatisfy` T.isInfixOf "Nothing -> InboxRetry"+ (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` []+ -- `derive … hole` is mandatory emit grammar, so a diagnostic saying it+ -- generates nothing would fire on every emit node in every spec and could+ -- never be resolved. It is the scaffold report's inert-node line (asserted+ -- immediately below) that carries the fact, once per run. See ExecPlan 199.+ it "leaves an emit-bearing spec clean enough for --deny-warnings" $ do+ (exitCode, out, err) <- runKeiroDsl ["check", "test/fixtures/emit.keiro", "--deny-warnings"]+ unless (exitCode == ExitSuccess) (expectationFailure (out <> err))+ err `shouldNotContain` "escalated to failure"+ it "reports emit nodes that contribute no generated modules" $+ withTempDirectory "keiro-dsl-inert-report" $ \out -> do+ spec <- specOf "test/fixtures/emit.keiro"+ report <- executePlannedScaffold out "test/fixtures/emit.keiro" (defaultContext (specContext spec)) spec+ reportInertNodes report `shouldBe` [("emit", "reservationResponse")]+ renderScaffoldReport report+ `shouldSatisfy` any+ ( T.isInfixOf+ "no-modules: emit reservationResponse (validated and diff-classified; no generated modules)"+ )+ 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` [18]+ 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]+ -- Every payload field is required — generated decoders use `o .:` for all of+ -- them — so the marker no longer selects anything. A source that omits it and+ -- a source that writes it describe the same queue and produce identical+ -- output. See ExecPlan 199.+ it "treats a payload field as required whether or not the marker is written" $ do+ unmarkedSource <- readTestText "test/fixtures/reservation-work-optfield.keiro"+ let bare = " note -> \"note\" text"+ markedSource = T.replace bare (bare <> " required") unmarkedSource+ unmarkedSource `shouldSatisfy` T.isInfixOf bare+ parseSpec "unmarked" unmarkedSource `shouldBe` parseSpec "marked" markedSource+ errorCodesOf "test/fixtures/reservation-work-optfield.keiro" >>= (`shouldBe` [])+ 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)"+ policy `shouldSatisfy` T.isInfixOf "data ReservationWorkOutcome"+ policy `shouldSatisfy` T.isInfixOf "jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome"+ policy `shouldNotSatisfy` T.isInfixOf "jobOutcomeFor :: Text -> JobOutcome"+ policy `shouldNotSatisfy` T.isInfixOf " _ -> Retry"+ 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))+ parseStableRenderedSpec "in" 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 "warns when an inline feed carries an ignored subscription override" $ do+ source <- readTestText "test/fixtures/readmodel.keiro"+ spec <-+ parseInlineSpec+ "<inline-subscription>"+ (T.replace " feed = inline\n" " feed = inline\n subscription = \"ignored-subscription\"\n" source)+ diagnosticCodes spec `shouldContain` [RmInlineSubscriptionIgnored]+ 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 "TransferDecisions/ReadModel.hs" modules+ inline = generatedTextEndingIn "Subscriptions/ReadModel.hs" modules+ transferHoles = [moduleText m | m <- modules, "TransferDecisions/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 "{-# LANGUAGE OverloadedRecordDot #-}"+ harnessText `shouldSatisfy` T.isInfixOf "import Generated.HospitalCapacity.TransferDecisions.ReadModel (transferDecisionsReadModel, transferDecisionsAsyncProjection)"+ harnessText `shouldSatisfy` T.isInfixOf "(\"shapeHash\", \"fnv1a:3717f6d9e3c44bd6\", T.unpack transferDecisionsReadModel.shapeHash)"+ harnessText `shouldSatisfy` T.isInfixOf "(\"strongScope\", \"CategoryHead reservation\", renderStrongScope transferDecisionsReadModel.strongScope)"+ harnessText `shouldSatisfy` T.isInfixOf "T.unpack transferDecisionsAsyncProjection.name"+ 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 -> parseStableRenderedSpec "in" 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 "data WorkflowFacts = WorkflowFacts"+ facts `shouldSatisfy` T.isInfixOf "workflowFactBody = [\"step:create-transfer-hold\", \"patch:fraud-check-v2(step:fraud-check)\""+ facts `shouldSatisfy` T.isInfixOf "workflowFactAwaitLabels = [\"reservation-confirmation\"]"+ facts `shouldSatisfy` T.isInfixOf "workflowFactPatchIds = [\"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+ replayImpactSpecs 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+ replayImpactSpecs old loosened `shouldBe` ReplayNeutral++ it "pairs guard-disambiguated siblings independently of both declaration orders" $ do+ base <- specOf "test/fixtures/reservation.keiro"+ let aggregate = onlyAggregate base+ case (aggTransitions aggregate, aggEvents aggregate) of+ (prototype : _, firstEvent : secondEvent : _) -> do+ let sibling guardExpression eventName =+ prototype+ { tGuard = guardExpression,+ tEmits = [eventName],+ tLoc = noLoc+ }+ commandOverride = EPath noLoc CommandRoot ["lifeCriticalOverride"]+ exact = sibling (Just (EAtom (ABool True))) (evName firstEvent)+ loosenedOld = sibling (Just commandOverride) (evName firstEvent)+ loosenedNew = sibling Nothing (evName firstEvent)+ changedOld = sibling (Just (ECmp OpEq commandOverride (ELiteral noLoc (LiteralBool False)))) (evName secondEvent)+ changedNew = sibling (Just (ECmp OpEq commandOverride (ELiteral noLoc (LiteralBool True)))) (evName firstEvent)+ oldSiblings = [exact, loosenedOld, changedOld]+ newSiblings = [exact, loosenedNew, changedNew]+ withTransitions transitions =+ modifyAggregate+ (aggName aggregate)+ (\candidate -> candidate {aggTransitions = transitions})+ base+ impacts =+ [ replayImpactSpecs (withTransitions oldOrder) (withTransitions newOrder)+ | oldOrder <- permutations oldSiblings,+ newOrder <- permutations newSiblings+ ]+ case impacts of+ firstImpact : remainingImpacts -> do+ remainingImpacts `shouldSatisfy` all (== firstImpact)+ firstImpact `shouldSatisfy` (/= ReplayNeutral)+ [] -> expectationFailure "permutations unexpectedly produced no replay comparisons"+ _ -> expectationFailure "reservation fixture must contain one transition and two events"++ 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 <- shouldParseStableRenderedSpec "<formatted>" old+ replayImpactSpecs 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"+ let rendered = T.intercalate "\n" (map renderFinding changes)+ explained = T.intercalate "\n" (map renderExplainBlock changes)+ reportJson = T.pack (show (Aeson.toJSON (diffReport defaultGate changes)))+ assertMatchesGolden "test/fixtures/compatibility-vector.diff.golden" rendered+ 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)+ `shouldSatisfy` \case+ [Advisory change] -> ckCode change == GeneratedHaskellNameChanged+ _ -> False+ 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 "classifies selector aliases as build-only and wire aliases as replay-affecting" $ do+ let sourceFor field =+ T.unlines+ [ "language keiro-dsl 4",+ "context field-alias-diff",+ "aggregate AliasDiff",+ " regs",+ " states Open",+ " command Observe { " <> field <> " }",+ " event Observed = fields(Observe)",+ " wire kind=ctorName fields=camelCase schemaVersion=1"+ ]+ base <- checkedServiceFromText "field-alias-diff-base.keiro" (sourceFor "region:Text")+ selectorAlias <- checkedServiceFromText "field-alias-diff-selector.keiro" (sourceFor "region haskell serviceRegion:Text")+ wireAlias <- checkedServiceFromText "field-alias-diff-wire.keiro" (sourceFor "region as \"region_code\":Text")+ let selectorChanges = diffServices base selectorAlias+ wireChanges = diffServices base wireAlias+ selectorFindings = [finding | Advisory finding <- selectorChanges, ckCode finding == GeneratedHaskellNameChanged]+ wireFindings = [finding | Breaking finding <- wireChanges, ckCode finding == EvtFieldWireKeyChanged]+ selectorChanges `shouldSatisfy` all (not . isBreaking)+ map ckFacet selectorFindings `shouldContain` ["command-field-selector", "event-field-selector"]+ map (verdictFor ConsumerBuild . ckVector) selectorFindings `shouldSatisfy` all (== VAdvisory)+ resolvedFold (ReplayImpact.replayImpactServices base selectorAlias) `shouldBe` ReplayNeutral+ case wireFindings of+ [finding] -> do+ ckSubject finding `shouldBe` "Observed.region"+ verdictFor PrivateHistoryRead (ckVector finding) `shouldBe` VBreaking+ verdictFor OldBinaryReadNewEvents (ckVector finding) `shouldBe` VBreaking+ ckDetail finding `shouldSatisfy` T.isInfixOf "'region' -> 'region_code'"+ findings -> expectationFailure ("expected one event wire-key finding, got " <> show findings)+ resolvedFold (ReplayImpact.replayImpactServices base wireAlias)+ `shouldSatisfy` \case+ ReplayAffected impacts ->+ maybe False ((== Set.singleton "Observed") . ReplayImpact.eventTypes) (Map.lookup "AliasDiff" impacts)+ ReplayNeutral -> False+ it "retains event selector advisories across a legal version bump" $ do+ let sourceFor eventDeclaration =+ T.unlines+ [ "language keiro-dsl 4",+ "context field-alias-version-diff",+ "aggregate AliasVersionDiff",+ " regs",+ " states Open",+ " command Observe {}",+ eventDeclaration+ ]+ base <- checkedServiceFromText "field-alias-version-base.keiro" (sourceFor " event Observed { region:Text }")+ bumped <-+ checkedServiceFromText+ "field-alias-version-bumped.keiro"+ (sourceFor " event Observed v2 { region haskell serviceRegion:Text }\n upcast from v1 = HOLE")+ let changes = diffServices base bumped+ selectorFindings = [finding | Advisory finding <- changes, ckCode finding == GeneratedHaskellNameChanged]+ [ckCode finding | Additive finding <- changes] `shouldContain` [VersionBumped]+ map ckFacet selectorFindings `shouldBe` ["event-field-selector"]+ map (verdictFor ConsumerBuild . ckVector) selectorFindings `shouldBe` [VAdvisory]+ it "classifies contract selector aliases separately from public wire changes" $ do+ let sourceFor field =+ T.unlines+ [ "language keiro-dsl 4",+ "context contract-field-alias-diff",+ "contract emergency {",+ " schemaVersion 1",+ " discriminator messageType",+ " topic events \"emergency.events\"",+ " event IncidentDeclared on events {",+ " " <> field,+ " }",+ "}"+ ]+ base <- checkedServiceFromText "contract-field-alias-base.keiro" (sourceFor "region: text")+ selectorAlias <- checkedServiceFromText "contract-field-alias-selector.keiro" (sourceFor "region haskell serviceRegion: text")+ wireAlias <- checkedServiceFromText "contract-field-alias-wire.keiro" (sourceFor "region as \"region_code\": text")+ let selectorChanges = diffServices base selectorAlias+ wireChanges = diffServices base wireAlias+ selectorChanges `shouldSatisfy` \case+ [Advisory finding] ->+ ckCode finding == GeneratedHaskellNameChanged+ && ckFacet finding == "contract-field-selector"+ && verdictFor ConsumerBuild (ckVector finding) == VAdvisory+ _ -> False+ case [finding | Breaking finding <- wireChanges, ckCode finding == ContractFieldChanged] of+ [finding] -> do+ verdictFor PublicConsumer (ckVector finding) `shouldBe` VBreaking+ cvRollout (ckVector finding) `shouldBe` Set.singleton RolloutProducerLast+ ckDetail finding `shouldSatisfy` T.isInfixOf "consumer-first rollout"+ findings -> expectationFailure ("expected one contract wire-key finding, got " <> show findings)+ it "keeps an alias-free field rename on the existing add/remove path" $ do+ let sourceFor field =+ T.unlines+ [ "language keiro-dsl 4",+ "context field-rename-diff",+ "aggregate RenameDiff",+ " regs",+ " states Open",+ " event Renamed { " <> field <> ":Text }",+ " wire kind=ctorName fields=camelCase schemaVersion=1"+ ]+ old <- checkedServiceFromText "field-rename-old.keiro" (sourceFor "region")+ new <- checkedServiceFromText "field-rename-new.keiro" (sourceFor "zone")+ let changes = diffServices old new+ [ckCode finding | Breaking finding <- changes]+ `shouldContain` [EvtFieldAddedWithoutBump, EvtFieldRemovedSameVersion]+ [finding | Advisory finding <- changes, ckCode finding == GeneratedHaskellNameChanged]+ `shouldBe` []+ 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"+ ],+ aggTransitions =+ [ transition {tEmits = filter (/= "TransferReservationConfirmed") (tEmits transition)}+ | transition <- aggTransitions aggregate+ ]+ }+ _ -> 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 "goldens language-3 to language-4 contract TypeID admission and rollout" $ do+ let source versionNumber prefix =+ T.unlines+ [ "language keiro-dsl " <> T.pack (show versionNumber),+ "context hospital-capacity",+ "contract emergency {",+ " schemaVersion 1",+ " discriminator messageType",+ " topic incidentEvents \"emergency.incident.events\"",+ " event IncidentTransferNeedDeclared on incidentEvents {",+ " incidentId: typeid \"" <> prefix <> "\"",+ " }",+ "}"+ ]+ checked name input = case parseSource name input of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right parsed -> pure (checkedSource parsed)+ v1 <- checked "contract-typeid-v1.keiro" (source (1 :: Int) "inc")+ v3 <- checked "contract-typeid-v3.keiro" (source (3 :: Int) "inc")+ v4 <- checked "contract-typeid-v4.keiro" (source (4 :: Int) "inc")+ v4Edited <- checked "contract-typeid-v4-edited.keiro" (source (4 :: Int) "rsv")+ let changes = diffServices v3 v4+ textGolden = T.intercalate "\n" (map renderFinding changes)+ jsonGolden = LazyText.toStrict (LazyTextEncoding.decodeUtf8 (Aeson.encode (diffReport defaultGate changes)))+ findings = [kind | Breaking kind <- changes, ckCode kind == ContractTypeIdDomainChanged]+ assertMatchesGolden "test/fixtures/contract-typeid-domain.diff.golden" textGolden+ assertMatchesGolden "test/fixtures/contract-typeid-domain.diff.json.golden" jsonGolden+ case findings of+ [finding] -> do+ verdictFor PublicConsumer (ckVector finding) `shouldBe` VBreaking+ verdictFor ConsumerBuild (ckVector finding) `shouldBe` VBreaking+ [verdictFor surface (ckVector finding) | surface <- [PrivateHistoryRead, OldBinaryReadNewEvents, SnapshotHydration, PersistedIdentity]]+ `shouldBe` replicate 4 VNotApplicable+ cvRollout (ckVector finding) `shouldBe` Set.fromList [RolloutDrainRequired, RolloutProducerFirst]+ deriveLabel (Set.singleton PublicConsumer) (ckVector finding) `shouldBe` LabelBreaking+ deriveLabel (Set.singleton ConsumerBuild) (ckVector finding) `shouldBe` LabelBreaking+ remediationFor (ckContext finding) (ckCode finding)+ `shouldBe` RemedyEmitContractTypeIdDomain :| [RemedyDrainLegacyInvalidContractMessages, RemedyRescaffoldContractConsumers, RemedyRunContractConformance]+ values -> expectationFailure ("expected one contract TypeID-domain finding, got " <> show (length values))+ [kind | change <- diffServices v1 v3, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []+ [kind | change <- diffServices v4 v4, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []+ let edited = diffServices v3 v4Edited+ map (ckCode . kindOfChange) edited `shouldContain` [ContractFieldChanged]+ [kind | change <- edited, let { kind = kindOfChange change }, ckCode kind == ContractTypeIdDomainChanged] `shouldBe` []+ 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]+ -- Adding a payload field is breaking however it is spelled. Generated+ -- decoders read every field with `o .:`, so a job already queued under the+ -- old shape fails to decode against the new one — the "additive, optional+ -- field" classification this test previously asserted described a decoder+ -- that was never generated. See ExecPlan 199.+ it "classifies any new workqueue payload field as breaking for queued jobs" $ do+ cs <- diffFixtures "test/fixtures/reservation-work.keiro" "test/fixtures/reservation-work-optfield.keiro"+ [ckCode k | Breaking k <- cs] `shouldContain` [WqPayloadFieldChanged]+ [ckSubject k | Breaking k <- cs] `shouldContain` ["note"]+ [ckDetail k | Breaking k <- cs, ckSubject k == "note"]+ `shouldSatisfy` any (T.isInfixOf "queued jobs do not contain it")+ 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 <- shouldParseStableRenderedSpec "<formatted-process>" 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 "import Example.Artifact.Domain (ArtifactInfo)"+ domain `shouldSatisfy` T.isInfixOf "import Vendor.Geometry (Geometry)"+ domain `shouldSatisfy` T.isInfixOf "artifact :: !ArtifactInfo"+ domain `shouldSatisfy` T.isInfixOf "RCons (Proxy @\"currentArtifact\") ArtifactKeiroBindings.emptyArtifactInfo"+ domain `shouldSatisfy` (not . T.isInfixOf "Example.Artifact.Domain.ArtifactInfo")+ codec `shouldSatisfy` T.isInfixOf "\"location\" .= encodeArtifactLocationShape"+ codec `shouldSatisfy` T.isInfixOf "\"local_file\""+ codec `shouldSatisfy` T.isInfixOf "parseOptionalField (pure ShapeArtifactKind.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"+ mapM_+ (shape `shouldSatisfy`)+ [ T.isInfixOf "description :: !(Maybe Text)",+ T.isInfixOf "tags :: ![Text]",+ T.isInfixOf "labels :: ![Maybe Text]",+ T.isInfixOf "attributes :: !(Map Text Text)"+ ]+ mapM_+ (shape `shouldNotSatisfy`)+ [ T.isInfixOf "description :: !(Maybe (Text))",+ T.isInfixOf "tags :: !([Text])",+ T.isInfixOf "labels :: !([(Maybe (Text))])",+ T.isInfixOf "attributes :: !(Map Text (Text))"+ ]+ 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 "type FieldOwner ArtifactInfoKeyProjection = ArtifactInfo"+ facade `shouldSatisfy` T.isInfixOf "bindingToShape KeiroBindings.artifactInfoBinding owner"+ facade `shouldSatisfy` (not . T.isInfixOf "Example.Artifact.Domain.ArtifactInfo")+ facade `shouldSatisfy` T.isInfixOf "artifactInfoKeyWitness"+ facade `shouldNotSatisfy` T.isInfixOf "structuralProjectionC"+ it "suffixes only structural witness names that collide after normalization" $ do+ source <- readTestText "test/fixtures/consumer-types.keiro"+ collisionSpec <-+ parseInlineSpec+ "<projection-name-collision>"+ ( T.replace+ " key as \"key\" : Text required"+ ( T.unlines+ [ " key as \"key\" : Text required",+ " fooDash as \"foo-bar\" : Text required",+ " fooUnder as \"foo_bar\" : Text required"+ ]+ )+ source+ )+ graph <- shouldResolveTypeGraph collisionSpec+ let specs = projectionSpecs graph+ keyWitnesses = [spWitness spec | spec <- specs, spPointer spec == "/key"]+ collidedWitnesses = [spWitness spec | spec <- specs, spPointer spec `elem` ["/foo-bar", "/foo_bar"]]+ keyWitnesses `shouldBe` ["artifactInfoKeyWitness"]+ length collidedWitnesses `shouldBe` 2+ Set.size (Set.fromList collidedWitnesses) `shouldBe` 2+ collidedWitnesses `shouldSatisfy` all (T.isPrefixOf "artifactInfoFooBar")+ collidedWitnesses `shouldSatisfy` all (T.isSuffixOf "Witness")+ collidedWitnesses `shouldSatisfy` all ((== 8) . T.length . T.dropEnd (T.length ("Witness" :: T.Text)) . T.drop (T.length ("artifactInfoFooBar" :: T.Text)))+ it "uses only precedence-required parentheses in nested record field types" $ do+ let spec =+ mappedSpec+ [ completeStructural+ "Nested"+ ( recordShape+ [ TMap (TOptional TText),+ TOptional (TList TText),+ TOptional (TMap TText)+ ]+ )+ ]+ shape = generatedTextEndingIn "Structural/Shape/Nested.hs" (scaffoldStructural (defaultContext (specContext spec)) spec)+ mapM_+ (shape `shouldSatisfy`)+ [ T.isInfixOf "field1 :: !(Map Text (Maybe Text))",+ T.isInfixOf "field2 :: !(Maybe [Text])",+ T.isInfixOf "field3 :: !(Maybe (Map Text Text))"+ ]+ it "uses the same precedence rules for strict union payloads" $ do+ let spec =+ mappedSpec+ [ completeStructural+ "Payload"+ ( ShapeUnion+ (TaggedObject "tag" "contents" RejectUnknown)+ [ WireArm "OptionalPayload" "optional" (Just (TOptional TText)) noLoc,+ WireArm "ListPayload" "list" (Just (TList (TOptional TText))) noLoc,+ WireArm "MapPayload" "map" (Just (TMap (TOptional TText))) noLoc+ ]+ )+ ]+ shape = generatedTextEndingIn "Structural/Shape/Payload.hs" (scaffoldStructural (defaultContext (specContext spec)) spec)+ mapM_+ (shape `shouldSatisfy`)+ [ T.isInfixOf "OptionalPayload !(Maybe Text)",+ T.isInfixOf "ListPayload ![Maybe Text]",+ T.isInfixOf "MapPayload !(Map Text (Maybe Text))"+ ]++ 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+ assertGeneratedHaskellContract "consumer-types.keiro" manifest+ 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` isFoldSurfaceRefusal+ 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 "generated Haskell language contract" $ do+ it "limits every representative generated module to the closed local extension set" $ do+ let allowed =+ Set.fromList+ [ "BlockArguments",+ "DeriveAnyClass",+ "DuplicateRecordFields",+ "OverloadedLabels",+ "OverloadedRecordDot",+ "QualifiedDo",+ "TemplateHaskell",+ "TypeFamilies"+ ]+ fixtures =+ [ "test/fixtures/aggregate-scalar-expressions-v2.keiro",+ "test/fixtures/nominal-scalars.keiro",+ "test/fixtures/structural-conformance.keiro",+ "test/fixtures/reservation.keiro",+ "test/fixtures/contract-v4.keiro",+ "test/fixtures/intake.keiro",+ "test/fixtures/reservation-work.keiro",+ "test/fixtures/readmodel-runtime.keiro"+ ]+ forM_ fixtures $ \fixture -> do+ modules <- scaffoldFixture fixture+ forM_ [generatedModule | generatedModule <- modules, kind generatedModule == Generated] $ \generatedModule -> do+ let actual = Set.fromList (generatedLocalExtensions generatedModule)+ unless (actual `Set.isSubsetOf` allowed) $+ expectationFailure (fixture <> ":" <> modulePath generatedModule <> ": disallowed local extensions " <> show (Set.toList (actual `Set.difference` allowed)))++ it "retains specialized syntax extensions and removes GHC2024-covered pragmas" $ do+ scalar <- scaffoldFixture "test/fixtures/aggregate-scalar-expressions-v2.keiro"+ structural <- scaffoldFixture "test/fixtures/structural-conformance.keiro"+ reservation <- scaffoldFixture "test/fixtures/reservation.keiro"+ contract <- scaffoldFixture "test/fixtures/contract-v4.keiro"+ intake <- scaffoldFixture "test/fixtures/intake.keiro"+ queue <- scaffoldFixture "test/fixtures/reservation-work.keiro"+ readModel <- scaffoldFixture "test/fixtures/readmodel-runtime.keiro"+ generatedExtensionsEndingIn "ScalarAccount/Domain.hs" scalar+ `shouldBe` ["DeriveAnyClass", "DuplicateRecordFields", "TemplateHaskell"]+ generatedExtensionsEndingIn "ScalarAccount/Transducer.hs" scalar+ `shouldBe` ["BlockArguments", "OverloadedLabels", "OverloadedRecordDot", "QualifiedDo"]+ generatedExtensionsEndingIn "Nominals.hs" scalar `shouldContain` ["DeriveAnyClass", "TypeFamilies"]+ generatedExtensionsEndingIn "Nominals/Internal.hs" scalar `shouldBe` []+ generatedExtensionsEndingIn "StructuralProjections.hs" structural `shouldBe` ["TypeFamilies"]+ let structuralShapeExtensions =+ [ generatedLocalExtensions generatedModule+ | generatedModule <- structural,+ "/Structural/Shape/" `T.isInfixOf` T.pack (modulePath generatedModule)+ ]+ structuralShapeExtensions `shouldSatisfy` all null+ generatedExtensionsEndingIn "Projection.hs" reservation `shouldBe` []+ generatedExtensionsEndingIn "ReplayAudit.hs" reservation `shouldBe` []+ generatedExtensionsEndingIn "Contract.hs" contract `shouldBe` ["DuplicateRecordFields", "OverloadedRecordDot"]+ generatedExtensionsEndingIn "Inbox.hs" intake `shouldBe` []+ generatedExtensionsEndingIn "Queue.hs" queue `shouldBe` ["OverloadedRecordDot"]+ generatedExtensionsEndingIn "ReadModel.hs" readModel `shouldBe` ["OverloadedRecordDot"]++ it "conditions record, label, derivation, and duplicate-selector extensions on emitted syntax" $ do+ mappedGuardSource <- readTestText "test/fixtures/mapped-guard.keiro"+ mappedGuardParsed <- case parseSource "mapped-guard-no-expression.keiro" (T.replace "guard current == current ; " "" mappedGuardSource) of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right parsed -> pure parsed+ let mappedGuardService = checkedSource mappedGuardParsed+ mappedGuard = scaffoldServiceModules (defaultContext (specContext (checkedSpec mappedGuardService))) mappedGuardService+ registerFree <- scaffoldFixture "test/fixtures/order.keiro"+ readModels <- scaffoldFixture "test/fixtures/readmodel.keiro"+ snapshot <- scaffoldFixture "test/fixtures/reservation-snapshot.keiro"+ ordinary <- scaffoldFixture "test/fixtures/reservation.keiro"+ generatedExtensionsEndingIn "Holder/Domain.hs" mappedGuard `shouldBe` ["TemplateHaskell"]+ generatedExtensionsEndingIn "Holder/Codec.hs" mappedGuard `shouldBe` []+ generatedExtensionsEndingIn "Holder/Transducer.hs" mappedGuard+ `shouldBe` ["BlockArguments", "QualifiedDo"]+ generatedExtensionsEndingIn "Holder/Harness.hs" mappedGuard `shouldBe` ["OverloadedLabels"]+ generatedExtensionsEndingIn "Order/Harness.hs" registerFree `shouldBe` []+ generatedExtensionsEndingIn "TransferDecisions/ReadModel.hs" readModels `shouldBe` ["OverloadedRecordDot"]+ generatedExtensionsEndingIn "Subscriptions/ReadModel.hs" readModels `shouldBe` []+ generatedExtensionsEndingIn "Reservation/Domain.hs" snapshot `shouldContain` ["DeriveAnyClass"]+ generatedExtensionsEndingIn "Reservation/Domain.hs" ordinary `shouldNotContain` ["DeriveAnyClass"]++ disjoint <-+ parseInlineSpec "<disjoint-contract>" $+ T.unlines+ [ "language keiro-dsl 4",+ "context language-contract",+ "contract disjoint {",+ " schemaVersion 1",+ " discriminator kind",+ " topic events \"events\"",+ " event First on events { first: text }",+ " event Second on events { second: text }",+ "}"+ ]+ emptyPayload <-+ parseInlineSpec "<empty-contract>" $+ T.unlines+ [ "language keiro-dsl 4",+ "context language-contract",+ "contract empty {",+ " schemaVersion 1",+ " discriminator kind",+ " topic events \"events\"",+ " event Empty on events { }",+ "}"+ ]+ let contractExtensions spec =+ generatedExtensionsEndingIn+ "Contract.hs"+ [ generatedModule+ | contractNode <- [contractNode | NContract contractNode <- specNodes spec],+ generatedModule <- scaffoldContract (defaultContext (specContext spec)) contractNode+ ]+ contractExtensions disjoint `shouldBe` ["OverloadedRecordDot"]+ contractExtensions emptyPayload `shouldBe` []++ describe "manifest (M2)" $ do+ it "lists exactly the modules the scaffolder produced" $ do+ mods <- scaffoldFixture "test/fixtures/reservation.keiro"+ service <- checkedServiceOf "test/fixtures/reservation.keiro"+ let manifest = renderManifestForService "reservation.keiro" mods service+ expectedNames = sort (map (moduleNameOf . modulePath) mods)+ assertGeneratedHaskellContract "reservation.keiro" manifest+ -- 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.BehaviorContract",+ "Generated.HospitalCapacity.Reservation.Domain",+ "Generated.HospitalCapacity.Reservation.EventStream",+ "Generated.HospitalCapacity.Reservation.Harness",+ "Generated.HospitalCapacity.Reservation.Projection",+ "Generated.HospitalCapacity.Reservation.Transducer",+ "Generated.HospitalCapacity.Nominals",+ "Generated.HospitalCapacity.Nominals.Internal",+ "Generated.HospitalCapacity.ReplayAudit",+ "HospitalCapacity.Reservation.BehaviorHoles",+ "HospitalCapacity.Reservation.Holes"+ ]+ it "derives the dependency set from the node kinds present (aggregate)" $ do+ service <- checkedServiceOf "test/fixtures/reservation.keiro"+ manifestDependenciesForService service `shouldBe` ["aeson", "base", "keiki", "keiro", "text"]+ it "derives the process dependency set, including worker-policy runtime imports" $ do+ service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"+ let dependencies = manifestDependenciesForService service+ mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["time", "uuid", "shibuya-core", "keiki", "keiro"]+ it "uses the registered shibuya-core package name for router scaffolds" $ do+ service <- checkedServiceOf "test/fixtures/incident-paging/incident-paging.keiro"+ let dependencies = manifestDependenciesForService service+ mapM_ (\dependency -> dependencies `shouldContain` [dependency]) ["effectful-core", "keiro", "shibuya-core"]+ dependencies `shouldNotContain` ["shibuya"]++ describe "service conformance facade (plan 188 M2)" $ do+ it "normalizes aggregate and read-model checks behind one base-only API" $ do+ service <- checkedServiceOf "test/fixtures/transfer-routing.keiro"+ let ctx = defaultContext (specContext (checkedSpec service))+ case serviceHarnessModule ctx service of+ Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)+ Right facade -> do+ committed <- readTestText ("test/conformance-newsurface/" <> modulePath facade)+ normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText facade)+ moduleNameOf (modulePath facade) `shouldBe` "Generated.TransferRouting.Conformance"+ moduleText facade `shouldSatisfy` T.isInfixOf ".harnessAssertions"+ moduleText facade `shouldSatisfy` T.isInfixOf ".readModelFactResults"+ moduleText facade `shouldSatisfy` T.isInfixOf "aggregate/Hospital/"+ moduleText facade `shouldSatisfy` T.isInfixOf "readmodel/hospital_load/"+ moduleText facade `shouldSatisfy` T.isInfixOf "qualified as Hospital"+ moduleText facade `shouldSatisfy` T.isInfixOf "qualified as HospitalLoad"+ moduleText facade `shouldNotSatisfy` T.isInfixOf "qualified as Harness"+ moduleText facade `shouldNotSatisfy` T.isInfixOf "TransferRouting.Hospital.Holes"+ it "projects process, router, and workflow facts with qualified stable keys" $ do+ processService <- checkedServiceOf "test/fixtures/hospital-surge.keiro"+ routerService <- checkedServiceOf "test/fixtures/incident-paging/incident-paging.keiro"+ workflowService <- checkedServiceOf "test/fixtures/workflow-evolution.keiro"+ let select predicate = filter predicate . specNodes . checkedSpec+ factNodes =+ select (\case NProcess {} -> True; _ -> False) processService+ <> select (\case NRouter {} -> True; _ -> False) routerService+ <> select (\case NWorkflow {} -> True; _ -> False) workflowService+ baseSpec = checkedSpec processService+ service = processService {checkedSpec = baseSpec {specNodes = factNodes}}+ ctx = defaultContext (specContext baseSpec)+ forM_+ [ "process/HospitalSurge/maxAttempts",+ "router/PagingRouter/dispatchCommand",+ "workflow/HospitalTransferReservation/body"+ ]+ (\key -> serviceConformanceFactKeys service `shouldSatisfy` elem key)+ case serviceHarnessModule ctx service of+ Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)+ Right facade -> do+ moduleText facade `shouldSatisfy` T.isInfixOf ".processHarnessValues"+ moduleText facade `shouldSatisfy` T.isInfixOf ".routerHarnessValues"+ moduleText facade `shouldSatisfy` T.isInfixOf ".workflowFactValues"+ it "uses the shared context-level placement policy" $ do+ service <- checkedServiceOf "test/fixtures/contract-v4.keiro"+ let ctx = Context {contextName = "modules", moduleRoot = "Mori", placement = CollocatedLeaf}+ serviceConformanceModuleName ctx `shouldBe` "Mori.Modules.Generated.Conformance"+ case serviceHarnessModule ctx service of+ Left duplicates -> expectationFailure ("unexpected duplicate fact keys: " <> show duplicates)+ Right facade -> do+ moduleText facade `shouldSatisfy` T.isInfixOf "runServiceConformanceChecks = pure []"+ moduleText facade `shouldSatisfy` T.isInfixOf "serviceConformanceFacts = []"+ it "adds one facade only to configured single-file plans and exposes only it" $ do+ service <- checkedServiceOf "test/fixtures/reservation.keiro"+ let ctx = defaultContext (specContext (checkedSpec service))+ runtimePackage = RuntimePackageName "reservation-runtime"+ unconfigured <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffold ctx service)+ configured <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)+ let facadeName = serviceConformanceModuleName ctx+ facades = [moduleValue | moduleValue <- configured, moduleNameOf (modulePath moduleValue) == facadeName]+ manifest = renderManifestForServiceWithFacade (Just facadeName) "reservation.keiro" configured service+ length configured `shouldBe` length unconfigured + 1+ length facades `shouldBe` 1+ manifest `shouldSatisfy` T.isInfixOf ("exposed-modules:\n " <> facadeName)+ T.count facadeName manifest `shouldBe` 1+ it "emits one context-level facade for a multi-member workspace regardless of member order" $ do+ canonical <- shouldComposeWorkspace canonicalWorkspacePath+ reordered <- shouldComposeWorkspace "test/fixtures/workspace/service-reordered.keiro-workspace"+ let runtimePackage = Just (RuntimePackageName "demo-runtime")+ plan workspace =+ planWorkspaceScaffoldWithRuntimePackageAndGoldens [] runtimePackage "goldens" (workspaceContext workspace) workspace+ facades workspacePlan =+ [ (moduleText moduleValue, provenance)+ | (moduleValue, provenance) <- wpModules workspacePlan,+ ".Conformance" `T.isSuffixOf` moduleNameOf (modulePath moduleValue)+ ]+ canonicalPlan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (plan canonical)+ reorderedPlan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (plan reordered)+ facades canonicalPlan `shouldBe` facades reorderedPlan+ map snd (facades canonicalPlan) `shouldBe` [ContextLevel]+ it "refuses duplicate normalized fact keys before planning writes" $ do+ service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"+ let spec = checkedSpec service+ processes = [node | node@NProcess {} <- specNodes spec]+ duplicated = service {checkedSpec = spec {specNodes = processes <> processes}}+ serviceHarnessModule (defaultContext (specContext spec)) duplicated `shouldSatisfy` isLeft++ describe "runnable service conformance package (plan 188 M3)" $ do+ it "uses readable ordinary names and collision-safe punctuation encoding" $ do+ cabaliseConformanceService "mori" `shouldBe` "mori"+ cabaliseConformanceService "mori_core" `shouldNotBe` cabaliseConformanceService "mori-core"+ cabaliseConformanceService "Mori" `shouldNotBe` cabaliseConformanceService "mori"+ conformancePackageDirectory (WorkspaceConformanceService "mori") `shouldBe` "keiro-dsl-conformance.workspace.mori"+ conformancePackageDirectory (StandaloneConformanceService "mori") `shouldBe` "keiro-dsl-conformance.mori"+ it "plans one base-only package and round-trips its complete generated record" $ do+ service <- checkedServiceOf "test/fixtures/hospital-surge.keiro"+ let runtimePackage = RuntimePackageName "hospital-runtime"+ facade = "Generated.HospitalSurge.Conformance"+ plan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planConformancePackage (StandaloneConformanceService "hospital-surge") runtimePackage facade service)+ cppPackageName plan `shouldBe` "keiro-hospital-surge-conformance"+ length [file | file <- cppFiles plan, takeExtension (conformanceFilePath file) == ".cabal"] `shouldBe` 1+ cabalFile <- case [file | file <- cppFiles plan, takeExtension (conformanceFilePath file) == ".cabal"] of+ [file] -> pure file+ files -> expectationFailure ("expected one Cabal file, got " <> show (map conformanceFilePath files)) >> fail "unreachable"+ let cabalText = conformanceFileText cabalFile+ cabalText `shouldSatisfy` T.isInfixOf "base >=4.18 && <5"+ T.lines cabalText `shouldSatisfy` (\lines' -> case lines' of first : _ -> first == "cabal-version: 3.0"; [] -> False)+ cabalText `shouldSatisfy` T.isInfixOf "hospital-runtime"+ cabalText `shouldSatisfy` T.isInfixOf "ghc-options: -Wall"+ cabalText `shouldNotSatisfy` T.isInfixOf " , keiro-dsl\n"+ recordFile <- case [file | file <- cppFiles plan, conformanceFilePath file == conformanceRecordFileName] of+ [file] -> pure file+ files -> expectationFailure ("expected one package record, got " <> show (map conformanceFilePath files)) >> fail "unreachable"+ let recordText = conformanceFileText recordFile+ parseConformancePackageRecord recordText+ `shouldBe` Just+ ConformancePackageRecord+ { cprSchema = 1,+ cprServiceKey = cppServiceKey plan,+ cprRuntimePackage = runtimePackage,+ cprFacadeModule = facade,+ cprFiles = [(conformanceFileKind file, conformanceFilePath file) | file <- cppFiles plan]+ }+ it "tolerates future rows and JSON keys while round-tripping awkward safe paths" $ do+ let recordText =+ T.unlines+ [ "keiro-dsl conformance ledger v1",+ "service-key standalone hospital-surge",+ "runtime-package hospital-runtime",+ "facade-module Generated.HospitalSurge.Conformance",+ "file {\"kind\":\"generated\",\"path\":\"generated/file with space.hs\",\"future-key\":true}",+ "future-row {\"value\":1}"+ ]+ expected =+ ConformancePackageRecord+ { cprSchema = 1,+ cprServiceKey = StandaloneConformanceService "hospital-surge",+ cprRuntimePackage = RuntimePackageName "hospital-runtime",+ cprFacadeModule = "Generated.HospitalSurge.Conformance",+ cprFiles = [(Generated, "generated/file with space.hs")]+ }+ parseConformancePackageRecord recordText `shouldBe` Just expected+ parseConformancePackageRecord (renderConformancePackageRecord expected) `shouldBe` Just expected+ parseConformancePackageRecord (T.replace "generated/file with space.hs" "../escape.hs" recordText)+ `shouldBe` Nothing+ parseConformancePackageRecord (T.replace "future-row {\"value\":1}" "file {\"kind\":\"generated\",\"path\":\"GENERATED/FILE WITH SPACE.HS\"}" recordText)+ `shouldBe` Nothing+ it "compares unique facts by key and distinguishes mismatch, missing, and unexpected" $ do+ compareConformanceFacts [("a", "1"), ("b", "2"), ("d", "4")] [("c", "3"), ("a", "1"), ("b", "9")]+ `shouldBe` Right+ [ ConformanceFactMatch "a" "1",+ ConformanceFactMismatch "b" "2" "9",+ ConformanceFactUnexpected "c" "3",+ ConformanceFactMissing "d" "4"+ ]+ compareConformanceFacts [("a", "1"), ("a", "2")] []+ `shouldBe` Left [DuplicateFactKey ExpectedFact "a"]+ it "creates once, reports generated files unchanged, and preserves accepted expectations" $ do+ withTempDirectory "keiro-dsl-conformance-package" $ \out -> do+ parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ runtimePackage = RuntimePackageName "hospital-runtime"+ modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)+ first <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules+ firstReport <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure first+ firstPackage <- maybe (expectationFailure "expected conformance package report" >> fail "unreachable") pure (reportConformancePackage firstReport)+ let packageRoot = out </> conformancePackageDirectory (StandaloneConformanceService (contextName ctx))+ expectationsPath = packageRoot </> "src/KeiroConformance/Expectations.hs"+ accepted = "module KeiroConformance.Expectations where\n-- accepted by the application\n"+ map snd (conformanceReportDispositions firstPackage) `shouldContain` [ConformanceCreated]+ TIO.writeFile expectationsPath accepted+ second <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out True "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules+ secondReport <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure second+ secondPackage <- maybe (expectationFailure "expected conformance package report" >> fail "unreachable") pure (reportConformancePackage secondReport)+ TIO.readFile expectationsPath `shouldReturn` accepted+ [ disposition+ | (file, disposition) <- conformanceReportDispositions secondPackage,+ conformanceFileKind file == Generated+ ]+ `shouldSatisfy` all (== ConformanceUnchanged)+ [ disposition+ | (file, disposition) <- conformanceReportDispositions secondPackage,+ conformanceFileKind file == HoleStub+ ]+ `shouldBe` [ConformanceSkipped]+ -- Migration used to be planned only when the run also planned a conformance+ -- package, so a spec that stopped generating one left its legacy record+ -- behind — and unreadable, since the current reader has no legacy parser.+ -- See ExecPlan 199.+ it "migrates an orphaned legacy conformance record even with no package planned" $+ withTempDirectory "keiro-dsl-orphan-conformance-ledger" $ \out -> do+ parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ orphanDirectory = out </> "keiro-dsl-conformance.standalone.retired-service"+ orphanPath = orphanDirectory </> legacyConformanceRecordFileName+ -- No --runtime-package, so this run plans no conformance package at+ -- all: the record below belongs to a package that no longer exists.+ run apply = do+ modules <-+ either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure $+ planServiceScaffoldWithRuntimePackage Nothing ctx service+ executeServiceScaffoldWithRuntimePackageAndNameMigrations+ Nothing+ apply+ out+ False+ "hospital-surge.keiro"+ (parsedSourceLanguage parsed)+ ctx+ service+ modules+ -- Build the orphan from a record the current writer produced, so the+ -- test exercises the discovery change and not a hand-typed format.+ withTempDirectory "keiro-dsl-orphan-source" $ \source -> do+ let sourceRuntime = RuntimePackageName "retired-runtime"+ sourcePackageRoot = source </> conformancePackageDirectory (StandaloneConformanceService (contextName ctx))+ sourceModules <-+ either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure $+ planServiceScaffoldWithRuntimePackage (Just sourceRuntime) ctx service+ _ <-+ executeServiceScaffoldWithRuntimePackageAndNameMigrations+ (Just sourceRuntime)+ False+ source+ False+ "hospital-surge.keiro"+ (parsedSourceLanguage parsed)+ ctx+ service+ sourceModules+ >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ ledger <- TIO.readFile (sourcePackageRoot </> conformanceLedgerFileName)+ record <-+ maybe (expectationFailure "conformance ledger did not parse" >> fail "unreachable") pure $+ parseConformancePackageRecord ledger+ createDirectoryIfMissing True orphanDirectory+ TIO.writeFile+ orphanPath+ ( T.unlines $+ [line | line <- T.lines ledger, isGeneratedBannerLine line]+ <> [ "schema 1",+ "service-key standalone " <> contextName ctx,+ "runtime-package " <> unRuntimePackageName (cprRuntimePackage record),+ "facade-module " <> cprFacadeModule record+ ]+ <> [ "file "+ <> (case fileKind of Generated -> "generated"; HoleStub -> "create-once")+ <> " "+ <> T.pack path+ | (fileKind, path) <- cprFiles record+ ]+ )+ refused <- run False+ refused `shouldSatisfy` \case+ Left [SidecarMigrationRequired [move]] ->+ sidecarMoveDisposition move == ConvertLegacyConformanceLedger+ _ -> False++ applied <- run True >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ map sidecarMoveDisposition (reportSidecarMoves applied) `shouldBe` [ConvertLegacyConformanceLedger]+ doesFileExist orphanPath `shouldReturn` False+ doesFileExist (orphanDirectory </> conformanceLedgerFileName) `shouldReturn` True++ it "converts a legacy conformance record losslessly and keeps service-key mismatch refusal" $+ withTempDirectory "keiro-dsl-conformance-ledger-migration" $ \out -> do+ parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ runtimePackage = RuntimePackageName "hospital-runtime"+ packageRoot = out </> conformancePackageDirectory (StandaloneConformanceService (contextName ctx))+ currentPath = packageRoot </> conformanceLedgerFileName+ legacyPath = packageRoot </> legacyConformanceRecordFileName+ backupPath = out </> ".keiro-dsl-name-migrations/sidecar-v1" </> conformancePackageDirectory (StandaloneConformanceService (contextName ctx)) </> legacyConformanceRecordFileName+ modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)+ let run apply =+ executeServiceScaffoldWithRuntimePackageAndNameMigrations+ (Just runtimePackage)+ apply+ out+ False+ "hospital-surge.keiro"+ (parsedSourceLanguage parsed)+ ctx+ service+ modules+ renderLegacyKey (WorkspaceConformanceService value) = "workspace " <> value+ renderLegacyKey (StandaloneConformanceService value) = "standalone " <> value+ renderLegacyKind Generated = "generated"+ renderLegacyKind HoleStub = "create-once"+ _ <- run False >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ currentContents <- TIO.readFile currentPath+ record <- maybe (expectationFailure "fresh conformance ledger did not parse" >> fail "unreachable") pure (parseConformancePackageRecord currentContents)+ let legacyRecord =+ record+ { cprFiles =+ [ (fileKind, if path == conformanceLedgerFileName then legacyConformanceRecordFileName else path)+ | (fileKind, path) <- cprFiles record+ ]+ }+ legacyContents =+ T.unlines $+ [line | line <- T.lines currentContents, isGeneratedBannerLine line]+ <> [ "schema 1",+ "service-key " <> renderLegacyKey (cprServiceKey legacyRecord),+ "runtime-package " <> unRuntimePackageName (cprRuntimePackage legacyRecord),+ "facade-module " <> cprFacadeModule legacyRecord+ ]+ <> ["file " <> renderLegacyKind fileKind <> " " <> T.pack path | (fileKind, path) <- cprFiles legacyRecord]+ renameFile currentPath legacyPath+ TIO.writeFile legacyPath legacyContents+ migrationTreeBefore <- treeSnapshot out+ refused <- run False+ refused `shouldSatisfy` \case+ Left [SidecarMigrationRequired [move]] -> sidecarMoveDisposition move == ConvertLegacyConformanceLedger+ _ -> False+ treeSnapshot out `shouldReturn` migrationTreeBefore+ applied <- run True >>= either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure+ map sidecarMoveDisposition (reportSidecarMoves applied) `shouldBe` [ConvertLegacyConformanceLedger]+ doesFileExist legacyPath `shouldReturn` False+ TIO.readFile backupPath `shouldReturn` legacyContents+ migrated <- TIO.readFile currentPath+ cprServiceKey <$> parseConformancePackageRecord migrated+ `shouldBe` Just (StandaloneConformanceService (contextName ctx))++ TIO.writeFile+ currentPath+ ( T.replace+ ("service-key standalone " <> contextName ctx)+ "service-key standalone another-service"+ migrated+ )+ mismatchBefore <- treeSnapshot out+ mismatch <- run False+ mismatch `shouldSatisfy` \case+ Left [ConformancePackageRefusal ConformancePackageRecordMismatch {}] -> True+ _ -> False+ treeSnapshot out `shouldReturn` mismatchBefore+ it "refuses a bannerless package file before changing any runtime byte" $ do+ withTempDirectory "keiro-dsl-conformance-atomic" $ \out -> do+ parsed <- parsedSourceOf "test/fixtures/hospital-surge.keiro"+ let service = checkedSource parsed+ spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ runtimePackage = RuntimePackageName "hospital-runtime"+ modules <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planServiceScaffoldWithRuntimePackage (Just runtimePackage) ctx service)+ executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules+ >>= either (\failure -> expectationFailure (show failure)) (const (pure ()))+ facade <- case [moduleValue | moduleValue <- modules, ".Conformance" `T.isSuffixOf` moduleNameOf (modulePath moduleValue)] of+ [moduleValue] -> pure moduleValue+ values -> expectationFailure ("expected one facade, got " <> show (map modulePath values)) >> fail "unreachable"+ let facadePath = out </> modulePath facade+ serviceKey = contextName ctx+ cabalPath = out </> conformancePackageDirectory (StandaloneConformanceService serviceKey) </> T.unpack ("keiro-" <> cabaliseConformanceService serviceKey <> "-conformance.cabal")+ TIO.appendFile facadePath "-- would be overwritten if runtime execution began\n"+ TIO.writeFile cabalPath "hand-owned cabal file\n"+ packageTree <- treeSnapshot out+ refused <- executeServiceScaffoldWithRuntimePackage (Just runtimePackage) out False "hospital-surge.keiro" (parsedSourceLanguage parsed) ctx service modules+ refused `shouldSatisfy` isLeft+ treeSnapshot out `shouldReturn` packageTree+ it "keeps a two-aggregate workspace at exactly one Cabal package" $ do+ withTempDirectory "keiro-dsl-conformance-workspace" $ \out -> do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let runtimePackage = Just (RuntimePackageName "workspace-runtime")+ plan <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure (planWorkspaceScaffoldWithRuntimePackageAndGoldens [] runtimePackage "goldens" (workspaceContext workspace) workspace)+ length [() | NAggregate {} <- specNodes (checkedSpec (checkedWorkspace workspace))] `shouldBe` 2+ executeWorkspaceScaffold out False plan >>= either (\failure -> expectationFailure (show failure)) (const (pure ()))+ packageDirectories <- filter (T.isPrefixOf "keiro-dsl-conformance.workspace." . T.pack) <$> listDirectory out+ packageDirectories `shouldBe` ["keiro-dsl-conformance.workspace.demo-project"]+ case packageDirectories of+ [packageDirectory] -> do+ cabalFiles <- filter ((== ".cabal") . takeExtension) <$> listDirectory (out </> packageDirectory)+ length cabalFiles `shouldBe` 1+ _ -> expectationFailure "expected one package directory"+ it "scaffolds the multi-member proof idempotently through the public CLI" $ do+ withTempDirectory "keiro-dsl-conformance-proof-cli" $ \base -> do+ let fixture = "test/conformance-service-package"+ copied = base </> "fixture"+ out = copied </> "runtime/src"+ sourcePaths =+ [ "service.keiro-workspace",+ "domain/alpha.keiro",+ "domain/beta.keiro",+ "domain/evidence.keiro",+ "domain/shared.keiro"+ ]+ fixtureManifest <- resolveTestPath (fixture </> "service.keiro-workspace") >>= canonicalizePath+ let fixtureRoot = takeDirectory fixtureManifest+ forM_ sourcePaths $ \relative -> TIO.readFile (fixtureRoot </> relative) >>= writeFileWithParents (copied </> relative)+ (firstCode, firstOut, firstErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]+ unless (firstCode == ExitSuccess) (expectationFailure (firstOut <> firstErr))+ firstTree <- treeSnapshot out+ length [path | (path, _) <- firstTree, takeExtension path == ".cabal"] `shouldBe` 1+ length [path | (path, _) <- firstTree, "Generated/Conformance.hs" `T.isSuffixOf` T.pack path] `shouldBe` 1+ let recordPath = out </> conformancePackageDirectory (WorkspaceConformanceService "workspace-proof") </> conformanceRecordFileName+ record <- parseConformancePackageRecord <$> TIO.readFile recordPath+ cprServiceKey <$> record `shouldBe` Just (WorkspaceConformanceService "workspace-proof")+ (secondCode, secondOut, secondErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]+ unless (secondCode == ExitSuccess) (expectationFailure (secondOut <> secondErr))+ secondErr `shouldSatisfy` isInfixOfString "keiro-workspace-proof-conformance.cabal (unchanged)"+ secondErr `shouldSatisfy` isInfixOfString "Expectations.hs (skipped: already present)"+ secondErr `shouldSatisfy` isInfixOfString "Generated.Conformance"+ treeSnapshot out `shouldReturn` firstTree+ it "keeps Expectations fixed and turns the generated target red for a changed workflow fact" $ do+ withTempDirectory "keiro-dsl-conformance-proof-mutation" $ \base -> do+ fixtureManifest <- resolveTestPath "test/conformance-service-package/service.keiro-workspace" >>= canonicalizePath+ let fixtureRoot = takeDirectory fixtureManifest+ let copied = base </> "fixture"+ out = copied </> "runtime/src"+ evidencePath = copied </> "domain/evidence.keiro"+ expectationsPath = out </> "keiro-dsl-conformance.workspace.workspace-proof/src/KeiroConformance/Expectations.hs"+ copyTextTree fixtureRoot copied+ acceptedExpectations <- TIO.readFile expectationsPath+ TIO.readFile evidencePath+ >>= TIO.writeFile evidencePath . T.replace "name \"workspace-proof-workflow\"" "name \"workspace-proof-workflow-v2\""+ (scaffoldCode, scaffoldOut, scaffoldErr) <- runKeiroDsl ["scaffold", copied </> "service.keiro-workspace", "--out", out]+ unless (scaffoldCode == ExitSuccess) (expectationFailure (scaffoldOut <> scaffoldErr))+ TIO.readFile expectationsPath `shouldReturn` acceptedExpectations+ let repositoryRoot = takeDirectory (takeDirectory (takeDirectory fixtureRoot))+ projectPath = base </> "mutation.project"+ buildDirectory = base </> "dist-newstyle"+ packageRoot = out </> "keiro-dsl-conformance.workspace.workspace-proof"+ TIO.writeFile+ projectPath+ ( T.unlines+ [ "packages:",+ " " <> T.pack (repositoryRoot </> "keiro"),+ " " <> T.pack (repositoryRoot </> "keiro-core"),+ " " <> T.pack (copied </> "runtime"),+ " " <> T.pack packageRoot,+ "",+ "source-repository-package",+ " type: git",+ " location: https://github.com/shinzui/keiki.git",+ " tag: 9714d37033c37595e3aaa3319ca0ca77466782e0",+ "",+ "source-repository-package",+ " type: git",+ " location: https://github.com/shinzui/keiki.git",+ " tag: 9714d37033c37595e3aaa3319ca0ca77466782e0",+ " subdir: keiki-codec-json",+ "",+ "allow-newer:",+ " haxl:time"+ ]+ )+ (testCode, testOut, testErr) <-+ readProcessWithExitCode+ "cabal"+ [ "test",+ "--project-file=" <> projectPath,+ "--builddir=" <> buildDirectory,+ "keiro-workspace-proof-conformance"+ ]+ ""+ testCode `shouldNotBe` ExitSuccess+ (testOut <> testErr)+ `shouldSatisfy` isInfixOfString "FAIL workflow/WorkspaceProofWorkflow/name expected=\"workspace-proof-workflow\" actual=\"workspace-proof-workflow-v2\""++ describe "new <kind> skeletons (M5)" $ do+ forM_ skeletonKinds $ \skeletonKind ->+ it ("the " <> T.unpack skeletonKind <> " skeleton selects and preserves the registered stable language") $+ assertSkeletonUsesStableLanguage skeletonKind+ it "every skeleton parses and validates with zero error diagnostics" $+ mapM_ assertSkeletonValid skeletonKinds+ it "every skeleton passes the scaffold refusal gates" $+ mapM_ assertSkeletonScaffoldable skeletonKinds+ -- `derive … hole` is mandatory emit grammar. While it carried a warning, a+ -- freshly generated emit service could never satisfy the documented CI+ -- recipe, no matter what its author did. See ExecPlan 199.+ it "every skeleton without a confirmed benign inversion satisfies the documented --deny-warnings CI gate" $+ withTempDirectory "keiro-dsl-skeleton-deny" $ \out ->+ -- router and process are deliberately absent: their idiomatic+ -- on-duplicate/on-reject spellings are confirmed benign inversions+ -- (RouterBenignInversion/ProcessBenignInversion), so those services+ -- gate CI with a selective --deny list rather than --deny-warnings.+ forM_ ["emit", "intake", "aggregate", "contract", "workqueue", "workflow"] $ \kind ->+ case skeletonFor kind of+ Left err -> expectationFailure (T.unpack err)+ Right source -> do+ let specPath = out </> T.unpack kind <> ".keiro"+ TIO.writeFile specPath source+ (exitCode, stdoutText, stderrText) <-+ runKeiroDsl ["check", specPath, "--min-language", "4", "--deny-warnings"]+ unless (exitCode == ExitSuccess) $+ expectationFailure (T.unpack kind <> " skeleton failed the gate:\n" <> stdoutText <> stderrText)+ stderrText `shouldNotContain` "escalated to failure"+ it "stable skeleton scaffolds match the committed compiling modules" $+ mapM_ (uncurry assertStableSkeletonMatchesCommitted) 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 "exempts only the authoritative generated transducer module path" $ do+ let expressions = syntheticGenerated "Gen/Aggregate/Expressions.hs" "import Keiki.Core qualified as K\nx = K.lit 1"+ transducer = syntheticGenerated "Gen/Aggregate/Transducer.hs" "import Keiki.Builder qualified as B\nx = B.slot"+ ordinary = syntheticGenerated "Gen/Aggregate/Projection.hs" "import Keiki.Builder qualified as B"+ firewallBreaches [expressions, transducer]+ `shouldBe` [("Gen/Aggregate/Expressions.hs", "import:Keiki.Core", 1)]+ firewallBreaches [ordinary] `shouldBe` [("Gen/Aggregate/Projection.hs", "import:Keiki.Builder", 1)]+ it "finds no breach in real scaffolder output (aggregate + process fixtures)" $ do+ aggMods <- scaffoldFixture "test/fixtures/reservation.keiro"+ procMods <- legacyScaffoldProcessFixture "test/fixtures/hospital-surge.keiro"+ firewallBreaches (aggMods <> procMods) `shouldBe` []++ describe "generated provenance banners (plan 182 M4)" $ do+ it "stamps the running package version, effective language, and module origin" $ do+ service <- checkedServiceOf "test/fixtures/contract-v4.keiro"+ let ctx = defaultContext (specContext (checkedSpec service))+ case planServiceScaffold ctx service of+ Left refusals -> expectationFailure (show refusals)+ Right modules -> do+ let generated = [moduleValue | moduleValue <- modules, kind moduleValue == Generated]+ generated `shouldSatisfy` (not . null)+ forM_ generated $ \moduleValue -> do+ let recognized = filter isGeneratedBannerLine (T.lines (moduleText moduleValue))+ expected = generatedBannerFor (checkedLanguageContract service) (origin moduleValue)+ recognized `shouldBe` [expected]+ expected+ `shouldSatisfy` T.isInfixOf+ ( "keiro-dsl "+ <> T.pack (showVersion Package.version)+ <> " (language keiro-dsl 4) from contract emergency"+ )+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ workspacePlan <- shouldPlanWorkspaceSpec workspace+ forM_ [moduleValue | (moduleValue, _) <- wpModules workspacePlan, kind moduleValue == Generated] $ \moduleValue ->+ filter isGeneratedBannerLine (T.lines (moduleText moduleValue))+ `shouldBe` [generatedBannerFor (checkedLanguageContract (checkedWorkspace workspace)) (origin moduleValue)]+ it "recognizes only the historical banner and the stamped format" $ do+ let contract = effectiveLanguageContract LegacyUnversioned+ isGeneratedBannerLine generatedBanner `shouldBe` True+ isGeneratedBannerLine (generatedBannerFor contract "aggregate Counter (line 2)") `shouldBe` True+ isGeneratedBannerLine "-- @generated by another tool" `shouldBe` False+ isGeneratedBannerLine codecComparisonBanner `shouldBe` False+ it "migrates a legacy-banner file and keeps repeated scaffold bytes stable" $+ withTempDirectory "keiro-dsl-stamped-banner" $ \out -> do+ spec <- parseInlineSpec "<stamped-banner>" loweringAggregateSpec+ let ctx = defaultContext (specContext spec)+ modules <- case planScaffold ctx spec of+ Left refusals -> expectationFailure (show refusals) >> pure []+ Right planned -> pure planned+ case [moduleValue | moduleValue <- modules, kind moduleValue == Generated] of+ target : _ -> do+ let path = out </> modulePath target+ stamped = generatedBannerFor (effectiveLanguageContract LegacyUnversioned) (origin target)+ legacyText = T.replace stamped generatedBanner (moduleText target)+ createDirectoryIfMissing True (takeDirectory path)+ TIO.writeFile path legacyText+ first <- executeScaffold out False "counter.keiro" ctx spec modules+ first `shouldSatisfy` isSuccessfulScaffold+ firstTree <- treeSnapshot out+ second <- executeScaffold out False "counter.keiro" ctx spec modules+ second `shouldSatisfy` isSuccessfulScaffold+ treeSnapshot out `shouldReturn` firstTree+ TIO.readFile path `shouldReturn` moduleText target+ [] -> expectationFailure "counter scaffold has no Generated module"++ describe "service-aware fixture helpers" $ do+ it "retains version-4 contract TypeIDs and their durable admission identities" $ do+ service <- checkedServiceOf "test/fixtures/contract-v4.keiro"+ modules <- scaffoldFixture "test/fixtures/contract-v4.keiro"+ let contractModule = generatedTextEndingIn "Contract.hs" modules+ identities = idDomainIdentitiesForService service+ contractModule `shouldSatisfy` T.isInfixOf "incidentId :: !(KindID \"inc\")"+ contractModule `shouldSatisfy` T.isInfixOf "reservationId :: !(KindID \"rsv\")"+ identities+ `shouldContain` ["id-domain|name=contract:emergency.IncidentTransferNeedDeclared.incidentId|contract=keiro-dsl/id-domain/typeid-v7/1|prefix=inc|separator=_|json=canonical-json-text"]++ describe "scaffold gates" $ do+ it "reports case-folded generated paths through the complete check diagnostics" $ do+ spec <- specOf "test/fixtures/reservation.keiro"+ case [aggregate | NAggregate aggregate <- specNodes spec] of+ aggregate : _ -> do+ let caseVariant =+ spec+ { specNodes =+ [ NAggregate aggregate,+ NAggregate aggregate {aggName = T.toUpper (aggName aggregate)}+ ]+ }+ diagnostics =+ checkServiceDiagnostics+ Nothing+ (defaultContext (specContext caseVariant))+ (legacyCheckedService caseVariant)+ map code diagnostics `shouldContain` [GeneratedPathCollision]+ case [ diagnostic+ | diagnostic <- diagnostics,+ code diagnostic == GeneratedPathCollision,+ "Domain.hs" `T.isInfixOf` message diagnostic+ ] of+ [diagnostic] -> do+ line diagnostic `shouldBe` unLoc (aggLoc aggregate)+ relatedLocations diagnostic `shouldSatisfy` (not . null)+ message diagnostic `shouldSatisfy` T.isInfixOf "case-insensitive filesystem"+ found -> expectationFailure ("expected one generated-path diagnostic, got " <> show found)+ withTempDirectory "keiro-dsl-check-path-collision" $ \root -> do+ let sourcePath = root </> "collision.keiro"+ version <- maybe (expectationFailure "language version 4 missing" >> fail "unreachable") pure (languageVersion 4)+ TIO.writeFile sourcePath (renderSource (ParsedSource (DeclaredLanguage version noLoc) caseVariant))+ (exitCode, out, err) <- runKeiroDsl ["check", sourcePath]+ exitCode `shouldBe` ExitFailure 1+ out `shouldBe` ""+ err `shouldContain` "error[GeneratedPathCollision]"+ [] -> expectationFailure "reservation fixture has no aggregate"+ it "uses lowering before module planning in both scaffold planners" $ do+ spec <- specOf "test/fixtures/emit.keiro"+ case [contract | NContract contract <- specNodes spec] of+ contract : _ -> do+ let defective =+ mapPublisher+ (\publisher -> publisher {pubBackoff = BackoffSpec "exponential" "2s" Nothing Nothing})+ spec+ { specNodes = NContract contract : specNodes spec+ }+ ctx = defaultContext (specContext defective)+ workspace = oneMemberWorkspace "emit.keiro" defective+ case (planScaffold ctx defective, planWorkspaceScaffold "goldens" ctx workspace) of+ (Left (LoweringRefusal singleReasons : _), Left (LoweringRefusal workspaceReasons : _)) ->+ workspaceReasons `shouldBe` singleReasons+ results -> expectationFailure ("expected lowering first from both planners, got " <> show results)+ [] -> expectationFailure "emit fixture has no contract"+ it "maps import cycles and planner invariants into stable check codes" $ do+ planningRefusalDiagnostics [ImportCycle ["A", "B", "A"]]+ `shouldSatisfy` any ((== GeneratedImportCycle) . code)+ planningRefusalDiagnostics [BehaviorRefusal [Behavior.DuplicateBehaviorIdentity "duplicate" [Loc 9]]]+ `shouldSatisfy` any (\diagnostic -> code diagnostic == BehaviorDerivationInvalid && line diagnostic == 9)+ planningRefusalDiagnostics [DuplicateConformanceFactKeys [DuplicateServiceFactKey "duplicate"]]+ `shouldSatisfy` any ((== ConformanceFactKeyCollision) . code)+ planningRefusalDiagnostics [SemanticContractMismatch "test mismatch"]+ `shouldSatisfy` any ((== GeneratedPlanningInvariantViolation) . code)+ spec <- specOf "test/fixtures/consumer-types.keiro"+ let cyclic = spec {specMapped = map moveArtifactBindingIntoGenerated (specMapped spec)}+ checkServiceDiagnostics Nothing (defaultContext (specContext cyclic)) (stableCheckedService cyclic)+ `shouldSatisfy` any ((== GeneratedImportCycle) . code)+ 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 (Just ExactGeneratedBannerPresent) `elem` stale+ && StaleModule HoleStub oldHoles Nothing `elem` stale+ doesFileExist (out </> oldDomain) `shouldReturn` True+ doesFileExist (out </> oldHoles) `shouldReturn` True+ renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "exact generated banner present; verify unchanged bytes before deleting")+ renderScaffoldReport second `shouldSatisfy` all (not . T.isInfixOf "safe to delete")+ it "preserves a stale generated path whose exact banner is missing" $+ withTempDirectory "keiro-dsl-stale-banner" $ \out -> do+ spec <- parseInlineSpec "<stale-banner>" loweringAggregateSpec+ first <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext spec)) spec+ let oldDomain = onlyPathEndingIn "Counter/Domain.hs" (map fst (reportDispositions first))+ renamed = spec {specNodes = map renameCounter (specNodes spec)}+ TIO.writeFile (out </> oldDomain) "-- generated by something else\n"+ second <- executePlannedScaffold out "counter.keiro" (defaultContext (specContext renamed)) renamed+ reportStale second `shouldSatisfy` elem (StaleModule Generated oldDomain (Just ExactGeneratedBannerMissing))+ renderScaffoldReport second `shouldSatisfy` any (T.isInfixOf "exact generated banner missing; preserve and review")+ TIO.readFile (out </> oldDomain) `shouldReturn` "-- generated by something else\n"+ 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) (if kind m == Generated then Just ExactGeneratedBannerPresent else Nothing)+ | (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) (Just ExactGeneratedBannerPresent) | (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))+ requirements <- either (\errors -> expectationFailure (show errors) >> pure []) pure (Behavior.deriveBehaviorRequirements spec)+ let expected =+ ScaffoldRecord+ { recSpecPath = "counter.keiro",+ recModuleRoot = "",+ recLayout = "prefixed",+ recSourceLanguage = LegacyUnversioned,+ recLanguageContract = effectiveLanguageContract LegacyUnversioned,+ recNamingEdition = IdiomaticNamingV1,+ recModuleRoles = [ScaffoldModuleRoleRow (moduleRole m) (kind m) (modulePath m) | (m, _) <- reportDispositions report],+ recFiles = [(kind m, modulePath m) | (m, _) <- reportDispositions report],+ recMappings = [],+ recIdDomains = [],+ recNominalEqualities = [],+ recBindingObligations = [],+ recBehaviorRequirements = Behavior.behaviorRecordRows requirements+ }+ sourceRows = filter ("source-language " `T.isPrefixOf`) (T.lines contents)+ withoutSourceRows = T.unlines (filter (not . T.isPrefixOf "source-language ") (T.lines contents))+ semanticRows = filter ("semantic-contract " `T.isPrefixOf`) (T.lines contents)+ withoutSemanticRows = T.unlines (filter (not . T.isPrefixOf "semantic-contract ") (T.lines contents))+ parseRecord contents `shouldBe` Just expected+ parseRecord withoutSourceRows `shouldBe` Just expected+ parseRecord withoutSemanticRows `shouldBe` Just expected+ case sourceRows of+ [sourceRow] -> do+ parseRecord (T.replace sourceRow (sourceRow <> "\n" <> sourceRow) contents) `shouldBe` Nothing+ parseRecord (T.replace sourceRow "source-language {malformed}" contents) `shouldBe` Nothing+ _ -> expectationFailure "expected exactly one source-language row"+ case semanticRows of+ [semanticRow] -> do+ parseRecord (T.replace semanticRow (semanticRow <> "\n" <> semanticRow) contents) `shouldBe` Nothing+ parseRecord (T.replace semanticRow "semantic-contract {malformed}" contents) `shouldBe` Nothing+ parseRecord (T.replace "\"languageVersion\":1" "\"languageVersion\":2" contents) `shouldBe` Nothing+ _ -> expectationFailure "expected exactly one semantic-contract row"+ parseRecord (T.replace "spec: " "future-field: retained\nspec: " contents) `shouldBe` parseRecord contents+ parseRecord (T.replace "record v1" "record v2" contents) `shouldBe` Nothing+ it "records declared provenance and reports a header-only scaffold drift" $+ withTempDirectory "keiro-dsl-language-drift" $ \out -> do+ spec <- parseInlineSpec "<language-drift>" loweringAggregateSpec+ let ctx = defaultContext (specContext spec)+ modules <- case planScaffold ctx spec of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right planned -> pure planned+ _ <- executePlannedScaffold out "counter.keiro" ctx spec+ case languageVersion 1 of+ Nothing -> expectationFailure "version 1 was not constructible"+ Just version -> do+ let declared = DeclaredLanguage version noLoc+ result <- executeScaffoldWithLanguage out False "counter.keiro" declared ctx spec modules+ case result of+ Left refusals -> expectationFailure (show refusals)+ Right report -> do+ reportSourceLanguageDrift report+ `shouldBe` Just (SourceLanguageDrift LegacyUnversioned declared)+ contents <- TIO.readFile (reportRecordPath report)+ recSourceLanguage <$> parseRecord contents `shouldBe` Just declared++ 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:Json" 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 "keeps field DSL names, generated selectors, and wire keys independent" $ do+ source <- readTestText "test/fixtures/aggregate-field-alias.keiro"+ service <- checkedServiceFromText "aggregate-field-alias.keiro" source+ let spec = checkedSpec service+ ctx = defaultContext (specContext spec)+ modules = scaffoldServiceModules ctx service+ domain = generatedTextEndingIn "Domain.hs" modules+ codec = generatedTextEndingIn "Codec.hs" modules+ workspace = oneMemberWorkspace "aggregate-field-alias.keiro" spec+ validateService service `shouldBe` []+ domain `shouldSatisfy` ((== 2) . T.count "payloadType :: !Text")+ domain `shouldSatisfy` ((== 2) . T.count "serviceRegion :: !Text")+ domain `shouldSatisfy` ((== 2) . T.count "family :: !Text")+ codec `shouldSatisfy` T.isInfixOf "\"type\" .= payload.payloadType"+ codec `shouldSatisfy` T.isInfixOf "\"region_code\" .= payload.serviceRegion"+ codec `shouldSatisfy` T.isInfixOf "o .: \"region_code\""+ scaffoldServiceModules ctx service `shouldBe` modules+ fmap (map fst . wpModules) (planWorkspaceScaffold "goldens" ctx workspace)+ `shouldBe` planScaffold ctx spec++ newSpec <- parseInlineSpec "aggregate-field-alias-v2.keiro" (T.replace "event FieldsCopied =" "event FieldsCopied v2 =" source)+ case goldensForDiff spec newSpec of+ [golden] -> do+ goldenJson golden `shouldSatisfy` T.isInfixOf "\"family\":\"sample\""+ goldenJson golden `shouldSatisfy` T.isInfixOf "\"type\":\"sample\""+ goldenJson golden `shouldSatisfy` T.isInfixOf "\"region_code\":\"sample\""+ goldenJson golden `shouldSatisfy` (not . T.isInfixOf "payloadType")+ goldenJson golden `shouldSatisfy` (not . T.isInfixOf "serviceRegion")+ goldens -> expectationFailure ("expected one field-alias golden, got " <> show goldens)++ it "keeps aggregate fold identity neutral across field aliases" $ do+ let sourceFor field =+ T.unlines+ [ "language keiro-dsl 4",+ "context field-alias-neutrality",+ "aggregate AliasNeutrality",+ " regs",+ " states Open",+ " command Observe { " <> field <> " }",+ " event Observed = fields(Observe)",+ " wire kind=ctorName fields=camelCase schemaVersion=1"+ ]+ base <- checkedServiceFromText "field-alias-base.keiro" (sourceFor "region:Text")+ selectorAlias <- checkedServiceFromText "field-alias-selector.keiro" (sourceFor "region haskell serviceRegion:Text")+ wireAlias <- checkedServiceFromText "field-alias-wire.keiro" (sourceFor "region as \"region_code\":Text")+ let fingerprint service = aggregateFoldFingerprintForService service (onlyAggregate (checkedSpec service))+ codecFor service =+ generatedTextEndingIn+ "Codec.hs"+ (scaffoldServiceModules (defaultContext (specContext (checkedSpec service))) service)+ fingerprint selectorAlias `shouldBe` fingerprint base+ fingerprint wireAlias `shouldBe` fingerprint base+ codecFor base `shouldSatisfy` T.isInfixOf "\"region\" .= payload.region"+ codecFor selectorAlias `shouldSatisfy` T.isInfixOf "\"region\" .= payload.serviceRegion"+ codecFor selectorAlias `shouldSatisfy` (not . T.isInfixOf "region_code")+ codecFor wireAlias `shouldSatisfy` T.isInfixOf "\"region_code\" .= payload.region"++ 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+ parsed <- parsedSourceOf "test/fixtures/reservation-dup-upcast-source.keiro"+ let spec = parsedSpec parsed+ case [aggregate | NAggregate aggregate <- specNodes spec] of+ [_] -> do+ let modules = scaffoldServiceModules (defaultContext (specContext spec)) (checkedSource parsed)+ 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, kind m == HoleStub]+ map (takeFileName . modulePath) holes `shouldBe` ["BehaviorHoles.hs", "Holes.hs"]+ -- Context nominals/internal/replay plus the stable aggregate surface.+ length [m | m <- mods, kind m == Generated] `shouldBe` 10+ 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+ modules <- scaffoldFixture "test/fixtures/transfer-routing.keiro"+ 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"+ -- Stable context and aggregate modules plus both hand-owned hole surfaces.+ length mods `shouldBe` 12+ 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 "keeps inequality-guard samples distinct from register initials" $ do+ mods <- scaffoldFixture "test/fixtures/subscription.keiro"+ let harness = generatedTextEndingIn "Harness.hs" mods+ harness `shouldSatisfy` T.isInfixOf "ActivateSubscriptionData"+ harness `shouldSatisfy` T.isInfixOf "Paid"+ harness `shouldNotSatisfy` T.isInfixOf "ActivateSubscriptionData (case parseSubscriptionId \"sub_01h455vb4pex5vsknk084sn02q\" of Right parsed -> parsed; Left _ -> error \"generated valid ID sample failed to parse\") (case parseCustomerId \"cust_01h455vb4pex5vsknk084sn02q\" of Right parsed -> parsed; Left _ -> error \"generated valid ID sample failed to parse\") Free"+ it "uses consumer-owned nominal initials for equality-guard samples" $ do+ mods <- scaffoldFixture "test/fixtures/nominal-scalars.keiro"+ let harness = generatedTextEndingIn "Harness.hs" mods+ harness `shouldSatisfy` T.isInfixOf "Bindings.initialOrderId"+ harness `shouldSatisfy` (not . T.isInfixOf "NominalConformance.Bindings.initialOrderId")+ harness `shouldNotSatisfy` T.isInfixOf "case parseOrderId"+ 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 reservationId\""+ harness `shouldSatisfy` T.isInfixOf "prefix <> \"register hospitalId\""+ harness `shouldSatisfy` T.isInfixOf "prefix <> \"register patientAcuity\""+ harness `shouldNotSatisfy` 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"+ map moduleText twinMods `shouldSatisfy` any (T.isInfixOf "B.replayOnly")+ let twinHarness = generatedTextEndingIn "Harness.hs" twinMods+ T.count "forwardReplayRequestTransferReservation ::" twinHarness `shouldBe` 1+ plainMods <- scaffoldFixture "test/fixtures/reservation.keiro"+ map moduleText plainMods `shouldSatisfy` all (not . T.isInfixOf "B.replayOnly")++ describe "service workspace (EP-153)" $ do+ describe "manifest grammar" $ do+ it "round-trips the canonical fixture manifest byte-for-byte" $ do+ source <- readTestText canonicalWorkspacePath+ manifest <- shouldParseManifest canonicalWorkspacePath source+ wmfService manifest `shouldBe` "demo-project"+ wmfRuntimePackage manifest `shouldBe` Nothing+ wmfModuleRoot manifest `shouldBe` Just "Demo.Modules.Project"+ wmfLayout manifest `shouldBe` Just CollocatedLeaf+ map wmrPath (NE.toList (wmfMembers manifest))+ `shouldBe` [ "domain/project-artifact.keiro",+ "domain/project.keiro",+ "domain/shared.keiro"+ ]+ renderWorkspaceManifest manifest+ `shouldBe` T.intercalate+ "\n"+ [ "service demo-project",+ "module Demo.Modules.Project",+ "layout collocated",+ "spec domain/project-artifact.keiro",+ "spec domain/project.keiro",+ "spec domain/shared.keiro"+ ]+ it "round-trips runtime-package canonically immediately after service" $ do+ manifest <-+ shouldParseManifest "<runtime-package>" $+ T.unlines+ [ "service mori",+ "module Mori.Modules",+ "spec domain/mori.keiro",+ "runtime-package mori-core",+ "layout collocated"+ ]+ wmfRuntimePackage manifest `shouldBe` Just (RuntimePackageName "mori-core")+ effectiveRuntimePackage Nothing manifest `shouldBe` Just (RuntimePackageName "mori-core")+ effectiveRuntimePackage (Just (RuntimePackageName "mori-dev")) manifest+ `shouldBe` Just (RuntimePackageName "mori-dev")+ renderWorkspaceManifest manifest+ `shouldBe` T.intercalate+ "\n"+ [ "service mori",+ "runtime-package mori-core",+ "module Mori.Modules",+ "layout collocated",+ "spec domain/mori.keiro"+ ]+ it "validates runtime package names with the mapped-source Cabal grammar" $ do+ mkRuntimePackageName "mori-core" `shouldBe` Right (RuntimePackageName "mori-core")+ mkRuntimePackageName "mori_core" `shouldBe` Left "runtime package 'mori_core' does not follow Cabal package-name grammar"+ it "treats membership as a set: source order changes neither the AST nor the bytes" $ do+ canonical <- readTestText canonicalWorkspacePath >>= shouldParseManifest canonicalWorkspacePath+ reordered <-+ shouldParseManifest "<reordered>" $+ T.unlines+ [ "service demo-project",+ "layout collocated",+ "spec domain/shared.keiro",+ "module Demo.Modules.Project",+ "spec domain/project.keiro",+ "spec ./domain/project-artifact.keiro"+ ]+ reordered `shouldBe` canonical+ renderWorkspaceManifest reordered `shouldBe` renderWorkspaceManifest canonical+ it "satisfies parse . render == id and render . parse . render == render" $+ property $+ forAll genWorkspaceManifest $ \manifest ->+ let rendered = renderWorkspaceManifest manifest+ in case parseWorkspaceManifest "<generated>" rendered of+ Left err -> counterexample (T.unpack err) False+ Right reparsed ->+ counterexample (T.unpack rendered) $+ reparsed == manifest && renderWorkspaceManifest reparsed == rendered+ it "recognizes a workspace manifest by extension, case-insensitively" $ do+ map+ isWorkspacePath+ [ "service.keiro-workspace",+ "a/b/Service.KEIRO-Workspace",+ "service.keiro",+ ".keiro-workspace",+ "keiro-workspace"+ ]+ `shouldBe` [True, True, False, False, False]+ describe "manifest refusals" $ do+ let rejects description source expected =+ it description $ case parseWorkspaceManifest "<manifest>" source of+ Right _ -> expectationFailure ("expected a refusal, got a manifest for:\n" <> T.unpack source)+ Left err -> T.unpack err `shouldContain` expected+ rejects+ "an empty manifest"+ "# only a comment\n"+ "must begin with a 'service <name>' clause"+ rejects+ "a manifest with no service clause"+ "spec domain/a.keiro\n"+ "first clause of a workspace manifest must be 'service <name>'"+ rejects+ "a manifest whose first clause is not service"+ "module Demo\nservice demo\nspec domain/a.keiro\n"+ "first clause of a workspace manifest must be 'service <name>'"+ rejects+ "a duplicate service clause"+ "service demo\nservice demo\nspec domain/a.keiro\n"+ "duplicate 'service' clause"+ rejects+ "a duplicate module clause"+ "service demo\nmodule Demo\nmodule Demo\nspec domain/a.keiro\n"+ "duplicate 'module' clause"+ rejects+ "a duplicate runtime-package clause"+ "service demo\nruntime-package demo-core\nruntime-package demo-api\nspec domain/a.keiro\n"+ "duplicate 'runtime-package' clause"+ it "locates a malformed runtime-package at its manifest line" $ case parseWorkspaceManifest "<manifest>" "service demo\nspec domain/a.keiro\nruntime-package demo_core\n" of+ Right _ -> expectationFailure "expected a malformed runtime package refusal"+ Left err -> do+ T.unpack err `shouldContain` "<manifest>:3:1"+ T.unpack err `shouldContain` "does not follow Cabal package-name grammar"+ rejects+ "a duplicate layout clause"+ "service demo\nlayout prefixed\nlayout prefixed\nspec domain/a.keiro\n"+ "duplicate 'layout' clause"+ rejects+ "a manifest with no members"+ "service demo\nmodule Demo\n"+ "must list at least one 'spec <path>.keiro' member"+ rejects+ "the same member listed twice"+ "service demo\nspec domain/a.keiro\nspec ./domain/a.keiro\n"+ "duplicate workspace member 'domain/a.keiro'"+ rejects+ "two members that differ only by case"+ "service demo\nspec domain/a.keiro\nspec domain/A.keiro\n"+ "differ only by case"+ rejects+ "an absolute member path"+ "service demo\nspec /etc/a.keiro\n"+ "must be relative, not absolute"+ rejects+ "a member path escaping the manifest directory"+ "service demo\nspec ../escape.keiro\n"+ "must not contain '..' segments"+ rejects+ "a member that is not a .keiro spec"+ "service demo\nspec domain/a.txt\n"+ "must name a .keiro spec"+ rejects+ "a manifest listing another manifest"+ "service demo\nspec domain/other.keiro-workspace\n"+ "must name a .keiro spec"+ describe "line relocation" $ do+ it "shifts every location the AST carries, and only the locations" $ do+ spec <- specOf "test/fixtures/reservation.keiro"+ let shifted = relocateLocs (+ 1000) spec+ collectLocs spec `shouldSatisfy` (not . null)+ collectLocs shifted `shouldBe` map (+ 1000) (collectLocs spec)+ -- Loc's Eq deliberately ignores the line, so relocation cannot+ -- change any equality-based behavior anywhere downstream.+ shifted `shouldBe` spec+ it "leaves the placeholder location alone so it never lands inside a member range" $ do+ spec <- specOf "test/fixtures/reservation.keiro"+ let blanked = relocateLocs (const 0) spec+ reshifted = relocateLocs (\n -> if n <= 0 then n else n + 500) blanked+ collectLocs reshifted `shouldBe` map (const 0) (collectLocs spec)+ describe "composition" $ do+ it "resolves cross-file ids, enums, mapped types, and read-model feeds" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ wsService workspace `shouldBe` "demo-project"+ wsContext workspace `shouldBe` "demo-project"+ wsModuleRoot workspace `shouldBe` Just "Demo.Modules.Project"+ wsLayout workspace `shouldBe` Just CollocatedLeaf+ map wmPath (wsMembers workspace)+ `shouldBe` [ "domain/project-artifact.keiro",+ "domain/project.keiro",+ "domain/shared.keiro"+ ]+ -- Every member is individually incomplete; together they check.+ checkWorkspace workspace `shouldBe` []+ it "records which member owns each shared declaration and node" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let ownership = wsOwnership workspace+ fmap fst (declarationOwner ownership "id" "ProjectId")+ `shouldBe` Just "domain/shared.keiro"+ fmap fst (declarationOwner ownership "enum" "ProjectPhase")+ `shouldBe` Just "domain/shared.keiro"+ fmap fst (declarationOwner ownership "rule" "phaseIsTerminal")+ `shouldBe` Just "domain/shared.keiro"+ fmap fst (declarationOwner ownership "mapped" "ProjectSummary")+ `shouldBe` Just "domain/shared.keiro"+ fmap fst (nodeOwner ownership "aggregate" "Project")+ `shouldBe` Just "domain/project.keiro"+ fmap fst (nodeOwner ownership "aggregate" "ProjectArtifact")+ `shouldBe` Just "domain/project-artifact.keiro"+ fmap fst (nodeOwner ownership "readmodel" "project_activity")+ `shouldBe` Just "domain/project-artifact.keiro"+ it "maps every merged line back to the member that wrote it" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let bases = [(wmPath m, wmLineBase m, wmLineCount m) | m <- wsMembers workspace]+ -- Ranges are disjoint and contiguous from zero.+ map (\(_, base, _) -> base) bases `shouldBe` scanl (+) 0 (init [c | (_, _, c) <- bases])+ sequence_+ [ resolveWorkspaceLine workspace (base + offset) `shouldBe` Just (path, offset)+ | (path, base, memberLines) <- bases,+ offset <- [1, memberLines]+ ]+ resolveWorkspaceLine workspace 0 `shouldBe` Nothing+ it "is insensitive to the order members are listed in" $ do+ canonical <- shouldComposeWorkspace canonicalWorkspacePath+ reordered <- shouldComposeWorkspace reorderedWorkspacePath+ reordered {wsManifestPath = wsManifestPath canonical} `shouldBe` canonical+ it "checks a single .keiro file as a one-member workspace, diagnostic for diagnostic" $ do+ let fixtures =+ [ "test/fixtures/reservation.keiro",+ "test/fixtures/consumer-types.keiro",+ "test/fixtures/aggregate-bad-refs.keiro",+ "test/fixtures/readmodel.keiro"+ ]+ forM_ fixtures $ \path -> do+ spec <- specOf path+ let workspace = oneMemberWorkspace path spec+ viaWorkspace = map (renderWorkspaceDiagnostic path) (checkWorkspace workspace)+ direct = map (renderDiagnostic path) (validateSpec spec)+ viaWorkspace `shouldBe` direct+ -- At least one of those fixtures must actually produce errors,+ -- or the equivalence claim is vacuous.+ badRefs <- specOf "test/fixtures/aggregate-bad-refs.keiro"+ checkWorkspace (oneMemberWorkspace "test/fixtures/aggregate-bad-refs.keiro" badRefs)+ `shouldSatisfy` any ((== Error) . wdSeverity)+ describe "composition refusals" $ do+ let refusesWith path expectedCode expectedFiles = do+ diagnostics <- shouldRefuseWorkspace path+ map wdCode (NE.toList diagnostics) `shouldContain` [expectedCode]+ let cited =+ [ wlFile location+ | diagnostic <- NE.toList diagnostics,+ wdCode diagnostic == expectedCode,+ location <- NE.toList (wdLocations diagnostic)+ ]+ sort (nubOrd cited) `shouldBe` sort expectedFiles+ it "refuses members that declare different contexts, citing every context clause" $+ refusesWith+ "test/fixtures/workspace-context-mismatch/service.keiro-workspace"+ WorkspaceContextMismatch+ [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]+ it "refuses a member layout clause that contradicts the manifest authority" $+ refusesWith+ "test/fixtures/workspace-authority-conflict/service.keiro-workspace"+ WorkspaceAuthorityConflict+ [WorkspaceManifestFile, WorkspaceMemberFile "domain/b.keiro"]+ it "refuses a textually identical shared declaration owned by two members" $+ refusesWith+ "test/fixtures/workspace-dup-decl/service.keiro-workspace"+ WorkspaceDuplicateDeclaration+ [WorkspaceMemberFile "domain/project.keiro", WorkspaceMemberFile "domain/shared.keiro"]+ it "refuses one aggregate defined in two members" $+ refusesWith+ "test/fixtures/workspace-dup-node/service.keiro-workspace"+ WorkspaceDuplicateNodeName+ [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]+ it "refuses generated paths that collide across members under case folding" $+ refusesWith+ "test/fixtures/workspace-path-collision/service.keiro-workspace"+ WorkspacePathCollision+ [WorkspaceMemberFile "domain/a.keiro", WorkspaceMemberFile "domain/b.keiro"]+ it "reports a listed member that is missing from disk" $+ refusesWith+ "test/fixtures/workspace-missing-member/service.keiro-workspace"+ WorkspaceMemberUnreadable+ [WorkspaceManifestFile]+ it "reports a member that does not parse" $+ refusesWith+ "test/fixtures/workspace-member-parse-failed/service.keiro-workspace"+ WorkspaceMemberParseFailed+ [WorkspaceManifestFile]+ it "surfaces a cross-file unresolved reference through the merged validator" $ do+ workspace <- shouldComposeWorkspace "test/fixtures/workspace-unresolved/service.keiro-workspace"+ let errors = [d | d <- checkWorkspace workspace, wdSeverity d == Error]+ map wdCode errors `shouldContain` [GuardAtomOutOfScope]+ [wlFile location | d <- errors, location <- NE.toList (wdLocations d)]+ `shouldContain` [WorkspaceMemberFile "domain/project.keiro"]+ describe "multi-file diagnostic rendering" $ do+ it "puts the primary location in the established shape and every other file on a note line" $ do+ diagnostics <- shouldRefuseWorkspace "test/fixtures/workspace-dup-decl/service.keiro-workspace"+ let manifest = "keiro-dsl/test/fixtures/workspace-dup-decl/service.keiro-workspace"+ map (renderWorkspaceDiagnostic manifest) (NE.toList diagnostics)+ `shouldBe` [ T.intercalate+ "\n"+ [ "keiro-dsl/test/fixtures/workspace-dup-decl/domain/project.keiro:4: error[WorkspaceDuplicateDeclaration]: duplicate declaration 'ProjectId': a shared declaration has exactly one owning member (identical duplicates do not merge)",+ " keiro-dsl/test/fixtures/workspace-dup-decl/domain/shared.keiro:4: note: also declared here, as id 'ProjectId'"+ ]+ ]+ describe "whole-service check through the CLI" $ do+ it "prints OK and exits zero for the composed fixture workspace" $ do+ (exitCode, out, err) <- runKeiroDsl ["check", canonicalWorkspacePath]+ unless (exitCode == ExitSuccess) (expectationFailure (out <> err))+ lines out `shouldBe` ["OK"]+ it "exits non-zero and names every involved file for a cross-file refusal" $ do+ (exitCode, _, err) <-+ runKeiroDsl ["check", "test/fixtures/workspace-dup-decl/service.keiro-workspace"]+ exitCode `shouldBe` ExitFailure 1+ err `shouldContain` "error[WorkspaceDuplicateDeclaration]"+ err `shouldContain` "workspace-dup-decl/domain/project.keiro:4"+ err `shouldContain` "workspace-dup-decl/domain/shared.keiro:4"+ it "attributes a merged-graph validation error to the member that wrote it" $ do+ (exitCode, _, err) <-+ runKeiroDsl ["check", "test/fixtures/workspace-unresolved/service.keiro-workspace"]+ exitCode `shouldBe` ExitFailure 1+ err `shouldContain` "workspace-unresolved/domain/project.keiro:12: error[GuardAtomOutOfScope]"+ it "produces byte-identical output for a manifest whose members are listed in reverse" $ do+ (canonicalCode, canonicalOut, _) <- runKeiroDsl ["check", canonicalWorkspacePath, "--emit"]+ (reorderedCode, reorderedOut, _) <- runKeiroDsl ["check", reorderedWorkspacePath, "--emit"]+ canonicalCode `shouldBe` ExitSuccess+ reorderedCode `shouldBe` ExitSuccess+ reorderedOut `shouldBe` canonicalOut+ (_, canonicalParse, _) <- runKeiroDsl ["parse", canonicalWorkspacePath]+ (_, reorderedParse, _) <- runKeiroDsl ["parse", reorderedWorkspacePath]+ reorderedParse `shouldBe` canonicalParse+ it "keeps the single-file path working, byte for byte" $ do+ (exitCode, out, err) <- runKeiroDsl ["check", "test/fixtures/reservation.keiro"]+ unless (exitCode == ExitSuccess) (expectationFailure (out <> err))+ lines out `shouldBe` ["OK"]+ it "explains bindings and reports coverage against the merged graph" $ do+ (bindingsCode, bindingsOut, _) <-+ runKeiroDsl ["check", canonicalWorkspacePath, "--explain-bindings"]+ bindingsCode `shouldBe` ExitSuccess+ bindingsOut `shouldContain` "binding obligations for context demo-project"+ -- The obligation's use sites span both aggregate members, which+ -- is only possible because the graph was resolved once, merged.+ bindingsOut `shouldContain` "Project register summary : ProjectSummary"+ bindingsOut `shouldContain` "ProjectArtifact command RecordArtifact .artifactSummary : ProjectSummary"+ withTempDirectory "keiro-dsl-workspace-coverage" $ \out -> do+ let reportPath = out </> "coverage.json"+ (coverageCode, coverageOut, _) <-+ runKeiroDsl ["check", canonicalWorkspacePath, "--coverage-report", reportPath]+ coverageCode `shouldBe` ExitSuccess+ coverageOut `shouldContain` "structural/opaque boundaries (reporting only)"+ report <- Aeson.eitherDecodeFileStrict reportPath+ case report of+ Left err -> expectationFailure err+ Right value -> coverageSpecPath value `shouldBe` Just (T.pack canonicalWorkspacePath)++ describe "workspace diff revision loading (EP-155 M1)" $ do+ it "composes added, removed, and renamed members through an in-memory content source" $ do+ project <- readTestText "test/fixtures/workspace/domain/project.keiro"+ artifact <- readTestText "test/fixtures/workspace/domain/project-artifact.keiro"+ shared <- readTestText "test/fixtures/workspace/domain/shared.keiro"+ let extra = "language keiro-dsl 4\ncontext demo-project\n\nid ExtraId prefix=extra\n"+ manifest members =+ T.unlines+ ( ["service demo-project", "module Demo.Modules.Project", "layout collocated"]+ <> ["spec " <> T.pack member | member <- members]+ )+ baseFiles =+ Map.fromList+ [ ("domain/project.keiro", project),+ ("domain/project-artifact.keiro", artifact),+ ("domain/shared.keiro", shared)+ ]+ loadFrom members files =+ loadWorkspace+ (memoryContentSource (Map.insert "service.keiro-workspace" (manifest members) files))+ "service.keiro-workspace"+ baseMembers = ["domain/project.keiro", "domain/project-artifact.keiro", "domain/shared.keiro"]+ expectLoaded result = case result of+ Left failure -> expectationFailure (show failure) >> error "unreachable"+ Right workspace -> pure workspace++ oldAdded <- loadFrom baseMembers baseFiles >>= expectLoaded+ newAdded <-+ loadFrom+ (baseMembers <> ["domain/extra.keiro"])+ (Map.insert "domain/extra.keiro" extra baseFiles)+ >>= expectLoaded+ map changeCode (diffSpecs (wsMergedSpec oldAdded) (wsMergedSpec newAdded))+ `shouldContain` [DeclarationAdded]++ oldRemoved <- loadFrom baseMembers baseFiles >>= expectLoaded+ newRemoved <-+ loadFrom+ ["domain/project.keiro", "domain/shared.keiro"]+ (Map.delete "domain/project-artifact.keiro" baseFiles)+ >>= expectLoaded+ map changeCode (diffSpecs (wsMergedSpec oldRemoved) (wsMergedSpec newRemoved))+ `shouldContain` [EvtRemovedNotDeprecated]++ oldRenamed <- loadFrom baseMembers baseFiles >>= expectLoaded+ let renamedMembers = ["domain/project-renamed.keiro", "domain/project-artifact.keiro", "domain/shared.keiro"]+ renamedFiles = Map.insert "domain/project-renamed.keiro" project (Map.delete "domain/project.keiro" baseFiles)+ newRenamed <- loadFrom renamedMembers renamedFiles >>= expectLoaded+ diffSpecs (wsMergedSpec oldRenamed) (wsMergedSpec newRenamed) `shouldBe` []++ describe "workspace diff ownership and unified reports (EP-155 M2)" $ do+ it "classifies shared declarations at use sites across every member with owned citations" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"+ let changes = diffWorkspaces old new+ enumChanges = filter ((== EnumCtorAdded) . changeCode . wcChange) changes+ mappedChanges = filter ((== MappedFieldTypeChanged) . changeCode . wcChange) changes+ citedFiles workspaceChanges =+ [ osFile site+ | change <- workspaceChanges,+ (_, Just site) <- wcUseSites change+ ]+ enumChanges `shouldSatisfy` (not . null)+ mappedChanges `shouldSatisfy` (not . null)+ let enumWireChanges =+ [ change+ | workspaceChange <- enumChanges,+ let change = wcChange workspaceChange,+ OldBinaryReadNewEvents `elem` breakingSurfaces change+ ]+ enumWireChanges `shouldSatisfy` (not . null)+ enumWireChanges `shouldSatisfy` all (not . gatedBreaking defaultGate)+ enumWireChanges `shouldSatisfy` all (gatedBreaking (gateWith [OldBinaryReadNewEvents]))+ map (fmap osFile . wcDeclarationSite) (enumChanges <> mappedChanges)+ `shouldSatisfy` all (== Just "domain/shared.keiro")+ citedFiles enumChanges `shouldContain` ["domain/order.keiro", "domain/shipment.keiro"]+ citedFiles mappedChanges `shouldContain` ["domain/order.keiro", "domain/shipment.keiro"]+ let rendered = T.intercalate "\n" (map renderWorkspaceFinding (enumChanges <> mappedChanges))+ rendered `shouldSatisfy` T.isInfixOf " declared: domain/shared.keiro:4"+ rendered `shouldSatisfy` T.isInfixOf " use-site: Order"+ rendered `shouldSatisfy` T.isInfixOf "(domain/order.keiro:"+ rendered `shouldSatisfy` T.isInfixOf "(domain/shipment.keiro:"+ assertMatchesGolden "test/fixtures/workspace-diff-new/workspace.diff.golden" (T.unlines (map renderWorkspaceFinding changes))++ it "emits one additive version-1 report with workspace provenance" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"+ let changes = diffWorkspaces old new+ meta =+ WorkspaceMeta+ { wmIdentity = wsService new,+ wmManifest = "service.keiro-workspace",+ wmSince = "HEAD",+ wmMembersOld = map wmPath (wsMembers old),+ wmMembersNew = map wmPath (wsMembers new),+ wmAdoptionBaseline = False+ }+ case Aeson.toJSON (workspaceDiffReport meta defaultGate changes) of+ Aeson.Object report -> do+ KeyMap.lookup "schema" report `shouldBe` Just (Aeson.String "keiro-dsl/diff-report/1")+ case KeyMap.lookup "workspace" report of+ Just (Aeson.Object workspace) -> do+ KeyMap.lookup "identity" workspace `shouldBe` Just (Aeson.String "workspace-diff")+ KeyMap.lookup "adoptionBaseline" workspace `shouldBe` Just (Aeson.Bool False)+ other -> expectationFailure ("missing workspace report metadata: " <> show other)+ case KeyMap.lookup "findings" report of+ Just (Aeson.Array findings) -> do+ findings `shouldSatisfy` (not . null)+ let objects = [finding | Aeson.Object finding <- toList findings]+ objects `shouldSatisfy` any (KeyMap.member "declaration")+ objects `shouldSatisfy` any (KeyMap.member "useSites")+ other -> expectationFailure ("missing workspace findings: " <> show other)+ other -> expectationFailure ("workspace report was not an object: " <> show other)++ it "computes one replay-impact value over both aggregates" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ new <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"+ case replayImpactSpecs (wsMergedSpec old) (wsMergedSpec new) of+ ReplayAffected affected -> Map.keysSet affected `shouldBe` Set.fromList ["Order", "Shipment"]+ ReplayNeutral -> expectationFailure "shared mapped evolution unexpectedly reported replay-neutral"++ describe "workspace ownership and authority changes (EP-155 M3)" $ do+ it "reports an unchanged aggregate move once without wire evolution" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ moved <- shouldComposeWorkspace "test/fixtures/workspace-diff-moved/service.keiro-workspace"+ let changes = diffWorkspaces old moved+ map (changeCode . wcChange) changes `shouldBe` [OwnershipMoved]+ forM_ changes $ \workspaceMove -> do+ let move = wcChange workspaceMove+ move `shouldSatisfy` isAdvisory+ move `shouldSatisfy` (not . gatedBreaking defaultGate)+ move `shouldSatisfy` (not . gatedBreaking (gateWith [minBound .. maxBound]))+ deriveLabel defaultGate (ckVector (workspaceChangeKind move)) `shouldBe` LabelAdvisory+ remediationFor (ckContext (workspaceChangeKind move)) OwnershipMoved+ `shouldBe` (RemedyRescaffoldWorkspace :| [])+ renderWorkspaceFinding workspaceMove+ `shouldSatisfy` T.isInfixOf "declaration moved domain/shipment.keiro -> domain/order.keiro"++ it "treats a member rename as the same owner-map change" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ let ownership = wsOwnership old+ renamed =+ old+ { wsOwnership =+ ownership+ { oiNodes =+ Map.adjust+ (\(_, loc) -> ("domain/shipping.keiro", loc))+ ("aggregate", "Shipment")+ (oiNodes ownership)+ }+ }+ moves = filter ((== OwnershipMoved) . changeCode . wcChange) (diffWorkspaces old renamed)+ length moves `shouldBe` 1+ forM_ moves $ \move ->+ renderWorkspaceFinding move `shouldSatisfy` T.isInfixOf "domain/shipment.keiro -> domain/shipping.keiro"++ it "reports ownership motion beside an independently classified wire edit" $ do+ old <- shouldComposeWorkspace "test/fixtures/workspace-diff-old/service.keiro-workspace"+ edited <- shouldComposeWorkspace "test/fixtures/workspace-diff-new/service.keiro-workspace"+ let ownership = wsOwnership edited+ movedAndEdited =+ edited+ { wsOwnership =+ ownership+ { oiNodes =+ Map.adjust+ (\(_, loc) -> ("domain/order.keiro", loc))+ ("aggregate", "Shipment")+ (oiNodes ownership)+ }+ }+ codes = map (changeCode . wcChange) (diffWorkspaces old movedAndEdited)+ codes `shouldContain` [OwnershipMoved]+ codes `shouldContain` [MappedFieldTypeChanged]++ it "reports context authority separately from derived read-model identity breaks" $ do+ old <- shouldComposeWorkspace canonicalWorkspacePath+ let newContext = "demo-project-renamed"+ renamed =+ old+ { wsContext = newContext,+ wsMergedSpec = (wsMergedSpec old) {specContext = newContext}+ }+ changes = diffWorkspaces old renamed+ codes = map (changeCode . wcChange) changes+ codes `shouldContain` [WorkspaceAuthorityChanged]+ codes `shouldContain` [DerivedIdentityChanged]+ map wcChange changes `shouldSatisfy` any (gatedBreaking defaultGate)++ it "keeps service, module-root, and layout authority advisories non-blocking" $ do+ old <- shouldComposeWorkspace canonicalWorkspacePath+ let changed =+ old+ { wsService = "demo-project-renamed",+ wsModuleRoot = Just "Demo.Modules.Renamed",+ wsLayout = Just GeneratedPrefix+ }+ authority = filter ((== WorkspaceAuthorityChanged) . changeCode . wcChange) (diffWorkspaces old changed)+ length authority `shouldBe` 3+ forM_ (map wcChange authority) $ \change -> do+ deriveLabel defaultGate (ckVector (workspaceChangeKind change)) `shouldBe` LabelAdvisory+ change `shouldSatisfy` (not . gatedBreaking (gateWith [minBound .. maxBound]))+ remediationFor (ckContext (workspaceChangeKind change)) WorkspaceAuthorityChanged+ `shouldBe` (RemedyRescaffoldWorkspace :| [RemedyRecompileConsumers])++ describe "workspace scaffold (EP-154)" $ do+ describe "workspace record" $ do+ it "round-trips modules, owners, members, mappings, obligations, and adoptions" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let record = sampleWorkspaceRecord workspace+ rendered = renderWorkspaceRecord record+ parseWorkspaceRecord rendered `shouldBe` Just record+ -- The header pins the schema: a v1 context-keyed record and a+ -- workspace record can never be read as each other.+ T.lines rendered `shouldSatisfy` \case+ header : _ -> header == "keiro-dsl workspace scaffold record v1"+ [] -> False+ parseRecord rendered `shouldBe` Nothing+ parseWorkspaceRecord (T.replace "record v1" "record v2" rendered) `shouldBe` Nothing+ it "ignores unknown rows and unknown JSON keys, and keeps context-level rows ownerless" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let record = sampleWorkspaceRecord workspace+ rendered = renderWorkspaceRecord record+ parseWorkspaceRecord (T.replace "service: " "future-row: retained\nservice: " rendered)+ `shouldBe` Just record+ parseWorkspaceRecord (T.replace "\"kind\":\"generated\"" "\"kind\":\"generated\",\"future\":1" rendered)+ `shouldBe` Just record+ [row | row <- wrModules record, wrmOwner row == Nothing]+ `shouldSatisfy` (not . null)+ it "rejects absent stable language rows and partial, duplicate, malformed, or inconsistent contracts" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let record = sampleWorkspaceRecord workspace+ rendered = renderWorkspaceRecord record+ sourceRows = filter ("source-language " `T.isPrefixOf`) (T.lines rendered)+ withoutSourceRows = T.unlines (filter (not . T.isPrefixOf "source-language ") (T.lines rendered))+ semanticRows = filter ("semantic-contract " `T.isPrefixOf`) (T.lines rendered)+ withoutSemanticRows = T.unlines (filter (not . T.isPrefixOf "semantic-contract ") (T.lines rendered))+ parseWorkspaceRecord withoutSourceRows `shouldBe` Nothing+ case sourceRows of+ firstRow : secondRow : _ -> do+ parseWorkspaceRecord (T.unlines (filter (/= secondRow) (T.lines rendered))) `shouldBe` Nothing+ parseWorkspaceRecord (T.replace firstRow (firstRow <> "\n" <> firstRow) rendered) `shouldBe` Nothing+ parseWorkspaceRecord (T.replace firstRow "source-language {malformed}" rendered) `shouldBe` Nothing+ _ -> expectationFailure "expected multiple workspace source-language rows"+ parseWorkspaceRecord withoutSemanticRows `shouldBe` Just record+ case semanticRows of+ [semanticRow] -> do+ parseWorkspaceRecord (T.replace semanticRow (semanticRow <> "\n" <> semanticRow) rendered) `shouldBe` Nothing+ parseWorkspaceRecord (T.replace semanticRow "semantic-contract {malformed}" rendered) `shouldBe` Nothing+ parseWorkspaceRecord (T.replace "\"languageVersion\":4" "\"languageVersion\":3" rendered) `shouldBe` Nothing+ _ -> expectationFailure "expected one workspace semantic-contract row"+ it "rejects unsafe module, owner, member, and adoption paths" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let rendered = renderWorkspaceRecord (sampleWorkspaceRecord workspace)+ corrupt from to = parseWorkspaceRecord (T.replace from to rendered)+ corrupt "member domain/shared.keiro" "member /etc/passwd" `shouldBe` Nothing+ corrupt "member domain/shared.keiro" "member ../escape.keiro" `shouldBe` Nothing+ corrupt "\"owner\":\"domain/shared.keiro\"" "\"owner\":\"../shared.keiro\"" `shouldBe` Nothing+ corrupt "\"path\":\"claimed/One.hs\"" "\"path\":\"/tmp/One.hs\"" `shouldBe` Nothing+ it "keys context and workspace history in structurally distinct explicit slots" $ do+ workspaceRecordFileName "demo-project"+ `shouldBe` workspaceLedgerFileName "demo-project"+ workspaceManifestFileName "demo-project"+ `shouldBe` workspaceCabalFragmentFileName "demo-project"+ workspaceRecordFileName "demo-project" `shouldNotBe` recordFileName "demo-project"+ contextLedgerFileName "workspace"+ `shouldNotBe` workspaceLedgerFileName "workspace"+ supersededByLine "demo-project"+ `shouldBe` "superseded-by: keiro-dsl-ledger.workspace.demo-project.txt"++ describe "workspace plan" $ do+ it "emits the context-level facade and replay-audit exactly once from the merged graph" $ do+ plan <- shouldPlanWorkspace canonicalWorkspacePath+ let modules = map fst (wpModules plan)+ facades = [m | m <- modules, "StructuralProjections.hs" `isSuffixOfPath` m]+ audits = [m | m <- modules, "ReplayAudit.hs" `isSuffixOfPath` m]+ shapes = [m | m <- modules, "Structural/Shape/ProjectSummary.hs" `isSuffixOfPath` m]+ length facades `shouldBe` 1+ length audits `shouldBe` 1+ length shapes `shouldBe` 1+ -- The audit assembles aggregates owned by two different member+ -- files, which is only possible from one merged graph.+ forM_ audits $ \audit -> do+ moduleText audit `shouldSatisfy` T.isInfixOf "Project.projectEventStream"+ moduleText audit `shouldSatisfy` T.isInfixOf "ProjectArtifact.projectArtifactEventStream"+ it "gives every generated ID and enum one context owner and imports only aggregate uses" $ do+ plan <- shouldPlanWorkspace canonicalWorkspacePath+ let ctx = wpContext plan+ modules = map fst (wpModules plan)+ nominalModules = [m | m <- modules, modulePath m == T.unpack (T.replace "." "/" (generatedNominalModule ctx) <> ".hs")]+ internalNominalModules = [m | m <- modules, modulePath m == T.unpack (T.replace "." "/" (generatedNominalModule ctx) <> "/Internal.hs")]+ domainFor suffix = case [m | m <- modules, suffix `isSuffixOfPath` m] of+ [m] -> pure m+ found -> expectationFailure ("expected one domain ending in " <> suffix <> ", got " <> show (map modulePath found)) >> fail "unreachable"+ ownerModule <- case nominalModules of+ [m] -> pure m+ found -> expectationFailure ("expected one generated nominal owner, got " <> show (map modulePath found)) >> fail "unreachable"+ internalOwnerModule <- case internalNominalModules of+ [m] -> pure m+ found -> expectationFailure ("expected one generated internal nominal owner, got " <> show (map modulePath found)) >> fail "unreachable"+ let nominalText = moduleText ownerModule+ internalNominalText = moduleText internalOwnerModule+ T.count "newtype ProjectId" nominalText `shouldBe` 0+ T.count "newtype ProjectId" internalNominalText `shouldBe` 1+ T.count "data ProjectPhase =" nominalText `shouldBe` 1+ T.count "data WorkspaceVisibility =" nominalText `shouldBe` 1+ projectDomain <- domainFor "Project/Generated/Domain.hs"+ artifactDomain <- domainFor "ProjectArtifact/Generated/Domain.hs"+ forM_ [projectDomain, artifactDomain] $ \domain -> do+ moduleText domain `shouldSatisfy` (not . T.isInfixOf "newtype ProjectId")+ moduleText domain `shouldSatisfy` (not . T.isInfixOf "data ProjectPhase")+ moduleText domain `shouldSatisfy` T.isInfixOf (generatedNominalModule ctx <> " (ProjectId, parseProjectId, ProjectPhase (..))")+ moduleText domain `shouldSatisfy` (not . T.isInfixOf "WorkspaceVisibility")+ singleFileModules <- case planServiceScaffold ctx (stableCheckedService (wsMergedSpec (wpWorkspace plan))) of+ Left refusals -> expectationFailure (show refusals) >> fail "unreachable"+ Right values -> pure values+ let withoutOrigin m = (modulePath m, moduleText m, kind m)+ map withoutOrigin singleFileModules `shouldBe` map withoutOrigin modules+ owners <- case planNominalGeneration ctx (wsMergedSpec (wpWorkspace plan)) of+ Left errors -> expectationFailure (show errors) >> fail "unreachable"+ Right values -> pure values+ map (resolvedNominalName . nominalDeclaration) owners+ `shouldBe` ["ProjectId", "ProjectPhase", "WorkspaceVisibility"]+ case [owner | owner <- owners, resolvedNominalName (nominalDeclaration owner) == "ProjectId"] of+ [owner] -> do+ nominalModule owner `shouldBe` generatedNominalModule ctx+ Set.fromList [NominalUseSite "Project" RegisterUse, NominalUseSite "ProjectArtifact" EventFieldUse]+ `shouldSatisfy` (`Set.isSubsetOf` nominalUseSites owner)+ found -> expectationFailure ("expected one ProjectId owner, got " <> show (length found))+ it "attributes every module to its owning member and leaves shared ones context-level" $ do+ plan <- shouldPlanWorkspace canonicalWorkspacePath+ let memberPaths = map wmPath (wsMembers (wpWorkspace plan))+ ownerOf suffix =+ case [provenance | (m, provenance) <- wpModules plan, suffix `isSuffixOfPath` m] of+ [provenance] -> Just provenance+ _ -> Nothing+ ownerOf "StructuralProjections.hs" `shouldBe` Just ContextLevel+ ownerOf "Generated/Nominals.hs" `shouldBe` Just ContextLevel+ ownerOf "ReplayAudit.hs" `shouldBe` Just ContextLevel+ ownerOf "Structural/Shape/ProjectSummary.hs"+ `shouldBe` Just (MemberOwned "domain/shared.keiro")+ ownerOf "Project/Generated/Domain.hs"+ `shouldBe` Just (MemberOwned "domain/project.keiro")+ ownerOf "ProjectArtifact/Generated/Domain.hs"+ `shouldBe` Just (MemberOwned "domain/project-artifact.keiro")+ ownerOf "ProjectActivity/Generated/ReadModel.hs"+ `shouldBe` Just (MemberOwned "domain/project-artifact.keiro")+ -- No module may claim an owner that is not a member of the+ -- workspace: the record's owner column has to stay resolvable.+ map (provenanceOwner . snd) (wpModules plan)+ `shouldSatisfy` all (maybe True (`elem` memberPaths))+ it "keeps the compiled workspace nominal conformance tree byte-current" $ do+ workspace <- shouldComposeWorkspace "test/fixtures/workspace-nominals/service.keiro-workspace"+ plan <- shouldPlanWorkspaceSpec workspace+ let compiledPaths =+ [ "Generated/WorkspaceNominalProof/Nominals.hs",+ "Generated/WorkspaceNominalProof/Project/Domain.hs",+ "Generated/WorkspaceNominalProof/Project/Codec.hs",+ "Generated/WorkspaceNominalProof/Project/Transducer.hs",+ "Generated/WorkspaceNominalProof/Project/BehaviorContract.hs",+ "Generated/WorkspaceNominalProof/Project/EventStream.hs",+ "Generated/WorkspaceNominalProof/Project/Harness.hs",+ "Generated/WorkspaceNominalProof/Project/Projection.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/Domain.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/Codec.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/Transducer.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/BehaviorContract.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/EventStream.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/Harness.hs",+ "Generated/WorkspaceNominalProof/ProjectArtifact/Projection.hs",+ "Generated/WorkspaceNominalProof/ReplayAudit.hs"+ ]+ map fst (wpModules plan) `shouldSatisfy` all (not . isSuffixOfPath "/Holes.hs")+ forM_ compiledPaths $ \path ->+ case [m | (m, _) <- wpModules plan, modulePath m == path] of+ [generated] -> do+ committed <- readTestText ("test/conformance-workspace-nominals/" <> path)+ normalizeGenerated committed `shouldBe` normalizeGenerated (moduleText generated)+ found -> expectationFailure ("expected one generated module at " <> path <> ", got " <> show (map modulePath found))+ it "plans a one-member workspace byte-identically to the single-file path" $ do+ let fixtures =+ [ "test/fixtures/reservation.keiro",+ "test/fixtures/consumer-types.keiro",+ "test/fixtures/readmodel.keiro",+ "test/fixtures/hospital-surge.keiro"+ ]+ -- Modules and refusals both: hospital-surge refuses on both+ -- paths, which proves the gates agree as well as the emitters.+ forM_ fixtures $ \path -> do+ spec <- specOf path+ let ctx = defaultContext (specContext spec)+ workspace = oneMemberWorkspace path spec+ fmap (map fst . wpModules) (planWorkspaceScaffold "goldens" ctx workspace)+ `shouldBe` planScaffold ctx spec+ -- The equality is not vacuous: at least one fixture plans, and+ -- its per-node modules are attributed to the single member.+ spec <- specOf "test/fixtures/reservation.keiro"+ let workspace = oneMemberWorkspace "test/fixtures/reservation.keiro" spec+ case planWorkspaceScaffold "goldens" (defaultContext (specContext spec)) workspace of+ Left refusals -> expectationFailure ("reservation should plan: " <> show refusals)+ Right plan -> do+ wpModules plan `shouldSatisfy` (not . null)+ map snd (wpModules plan)+ `shouldSatisfy` all (`elem` [ContextLevel, MemberOwned "reservation.keiro"])+ map snd (wpModules plan)+ `shouldSatisfy` elem (MemberOwned "reservation.keiro")+ it "computes obligations from the complete merged graph, spanning members" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ case bindingObligations (wsMergedSpec workspace) of+ Left graphErrors -> expectationFailure ("merged graph did not resolve: " <> show graphErrors)+ Right obligations ->+ case [o | o <- obligations, obligationMappedName o == "ProjectSummary", obligationKind o == BindingValue] of+ [obligation] -> do+ obligationUseSites obligation+ `shouldSatisfy` any (T.isInfixOf "Project register summary")+ obligationUseSites obligation+ `shouldSatisfy` any (T.isInfixOf "ProjectArtifact command RecordArtifact")+ found -> expectationFailure ("expected one ProjectSummary binding obligation, got " <> show (length found))+ it "refuses a case-folded path collision across members, naming both files" $ do+ workspace <- shouldComposeWorkspace canonicalWorkspacePath+ let collided = withCaseVariantAggregate workspace+ case planWorkspaceScaffold "goldens" (workspaceContext collided) collided of+ Right _ -> expectationFailure "expected a cross-member path collision refusal"+ Left refusals -> do+ let origins = concat [os | PathCollision _ os <- refusals]+ origins `shouldSatisfy` any (T.isInfixOf "domain/project.keiro: ")+ origins `shouldSatisfy` any (T.isInfixOf "domain/project-artifact.keiro: ")+ it "refuses golden fixtures stranded beside a member instead of under the workspace root" $+ withTempDirectory "keiro-dsl-workspace-goldens" $ \root -> do+ workspace <- writeGoldenWorkspace root+ let workspaceGoldens = root </> "golden-payloads"+ fixture = "hospital-capacity/Reservation/TransferReservationCreated.v1.json"+ beside = root </> "domain/golden-payloads" </> fixture+ goldenRootDivergence workspaceGoldens workspace `shouldReturn` []+ createDirectoryIfMissing True (takeDirectory beside)+ TIO.writeFile beside "{}\n"+ refusals <- goldenRootDivergence workspaceGoldens workspace+ refusals `shouldBe` [GoldenRootDivergence workspaceGoldens [beside]]+ renderRefusals refusals+ `shouldSatisfy` any (T.isInfixOf "one golden root per workspace")+ -- The same fixture under the workspace root is no divergence.+ let atRoot = workspaceGoldens </> fixture+ createDirectoryIfMissing True (takeDirectory atRoot)+ TIO.writeFile atRoot "{}\n"+ goldenRootDivergence workspaceGoldens workspace `shouldReturn` []++ describe "workspace scaffold" $ do+ it "writes workspace-keyed history and no context-keyed file at all" $+ withWorkspaceFixture "keiro-dsl-workspace-history" id $ \_ out workspace -> do+ report <- executePlannedWorkspaceScaffold out workspace+ wsrRecordPath report+ `shouldBe` out </> workspaceLedgerFileName "demo-project"+ wsrBuildManifestPath report+ `shouldBe` out </> workspaceCabalFragmentFileName "demo-project"+ doesFileExist (out </> recordFileName "demo-project") `shouldReturn` False+ doesFileExist (out </> contextCabalFragmentFileName "demo-project") `shouldReturn` False+ contents <- TIO.readFile (wsrRecordPath report)+ buildManifest <- TIO.readFile (wsrBuildManifestPath report)+ assertGeneratedHaskellContract "service.keiro-workspace" buildManifest+ case parseWorkspaceRecord contents of+ Nothing -> expectationFailure ("workspace record did not parse:\n" <> T.unpack contents)+ Just record -> do+ wrService record `shouldBe` "demo-project"+ wrManifest record `shouldBe` "service.keiro-workspace"+ wrMembers record+ `shouldBe` [ "domain/project-artifact.keiro",+ "domain/project.keiro",+ "domain/shared.keiro"+ ]+ -- Context-level modules are ownerless; everything+ -- else names the member that produced it.+ [wrmPath row | row <- wrModules record, wrmOwner row == Nothing]+ `shouldSatisfy` \ownerless ->+ length ownerless == 4+ && any (T.isSuffixOf "StructuralProjections.hs" . T.pack) ownerless+ && any (T.isSuffixOf "Nominals.hs" . T.pack) ownerless+ && any (T.isSuffixOf "Nominals/Internal.hs" . T.pack) ownerless+ && any (T.isSuffixOf "ReplayAudit.hs" . T.pack) ownerless+ [ wrmOwner row+ | row <- wrModules record,+ "Project/Generated/Domain.hs" `T.isSuffixOf` T.pack (wrmPath row)+ ]+ `shouldBe` [Just "domain/project.keiro"]+ it "refuses and then applies old workspace sidecar names before reading history" $+ withWorkspaceFixture "keiro-dsl-workspace-sidecar-migration" id $ \_ out workspace -> do+ plan <- shouldPlanWorkspaceSpec workspace+ first <- executeWorkspaceScaffold out False plan+ either (\failure -> expectationFailure (show failure)) (const (pure ())) first+ let service = wsService workspace+ currentLedger = workspaceLedgerFileName service+ currentFragment = workspaceCabalFragmentFileName service+ oldLedger = legacyWorkspaceRecordFileName service+ oldFragment = legacyWorkspaceManifestFileName service+ renameFile (out </> currentLedger) (out </> oldLedger)+ renameFile (out </> currentFragment) (out </> oldFragment)+ migrationTreeBefore <- treeSnapshot out+ refused <- executeWorkspaceScaffoldWithNameMigrations out False False plan+ refused `shouldSatisfy` \case+ Left [SidecarMigrationRequired moves] ->+ length moves == 2 && all ((== RenameSidecar) . sidecarMoveDisposition) moves+ _ -> False+ treeSnapshot out `shouldReturn` migrationTreeBefore+ applied <- executeWorkspaceScaffoldWithNameMigrations out False True plan+ report <- either (\failure -> expectationFailure (show failure) >> fail "unreachable") pure applied+ map sidecarMoveDisposition (wsrSidecarMoves report) `shouldBe` [RenameSidecar, RenameSidecar]+ wsrStale report `shouldBe` []+ doesFileExist (out </> oldLedger) `shouldReturn` False+ doesFileExist (out </> oldFragment) `shouldReturn` False+ doesFileExist (out </> currentLedger) `shouldReturn` True+ doesFileExist (out </> currentFragment) `shouldReturn` True+ rerun <- executeWorkspaceScaffoldWithNameMigrations out False True plan+ either (\failure -> expectationFailure (show failure)) (\value -> wsrSidecarMoves value `shouldBe` []) rerun+ it "is idempotent: an unchanged second run rewrites nothing and reports nothing" $+ withWorkspaceFixture "keiro-dsl-workspace-idempotent" id $ \_ out workspace -> do+ first <- executePlannedWorkspaceScaffold out workspace+ treeBefore <- treeSnapshot out+ second <- executePlannedWorkspaceScaffold out workspace+ treeAfter <- treeSnapshot out+ treeAfter `shouldBe` treeBefore+ map thd3 (wsrDispositions second)+ `shouldSatisfy` all (`elem` [Unchanged, Skipped])+ wsrStale second `shouldBe` []+ wsrOwnershipMoves second `shouldBe` []+ wsrMappingDrift second `shouldBe` []+ wsrNewHoles second `shouldBe` []+ -- The first run had to write; the claim is not vacuous.+ map thd3 (wsrDispositions first) `shouldSatisfy` any (== Overwritten)+ renderWorkspaceScaffoldReport second+ `shouldSatisfy` all (not . T.isPrefixOf "stale:")+ it "produces byte-identical output for members listed in reverse order" $+ withWorkspaceFixture "keiro-dsl-workspace-order-a" id $ \_ outA workspaceA ->+ withWorkspaceFixture "keiro-dsl-workspace-order-b" reverse $ \_ outB workspaceB -> do+ _ <- executePlannedWorkspaceScaffold outA workspaceA+ _ <- executePlannedWorkspaceScaffold outB workspaceB+ treeB <- treeSnapshot outB+ treeA <- treeSnapshot outA+ treeB `shouldBe` treeA+ map fst treeA `shouldSatisfy` elem (workspaceLedgerFileName "demo-project")+ it "reports stale files only for the member that changed" $+ withWorkspaceFixture "keiro-dsl-workspace-stale" id $ \root out workspace -> do+ first <- executePlannedWorkspaceScaffold out workspace+ let siblingPaths =+ [ modulePath m+ | (m, provenance, _) <- wsrDispositions first,+ provenance == MemberOwned "domain/project-artifact.keiro"+ ]+ siblingsBefore <- traverse (TIO.readFile . (out </>)) siblingPaths+ renamed <- renameMemberAggregate root "domain/project.keiro" "Project" "Ledger"+ second <- executePlannedWorkspaceScaffold out renamed+ let stalePaths = map stalePath (wsrStale second)+ stalePaths `shouldSatisfy` (not . null)+ stalePaths `shouldSatisfy` all (T.isInfixOf "/Project/" . T.pack)+ -- Nothing the sibling member owns is stale, and nothing it+ -- owns changed on disk: no cross-member false positives.+ stalePaths `shouldSatisfy` all (`notElem` siblingPaths)+ siblingsAfter <- traverse (TIO.readFile . (out </>)) siblingPaths+ siblingsAfter `shouldBe` siblingsBefore+ forM_ stalePaths $ \path -> doesFileExist (out </> path) `shouldReturn` True+ wsrStale second+ `shouldSatisfy` all+ ( \stale -> case staleKind stale of+ Generated -> staleGeneratedEvidence stale == Just ExactGeneratedBannerPresent+ HoleStub -> staleGeneratedEvidence stale == Nothing+ )+ renderWorkspaceScaffoldReport second+ `shouldSatisfy` any (T.isInfixOf "keiro-dsl never deletes files.")+ renderWorkspaceScaffoldReport second+ `shouldSatisfy` any (T.isInfixOf "exact generated banner present; verify unchanged bytes before deleting")+ renderWorkspaceScaffoldReport second+ `shouldSatisfy` all (not . T.isInfixOf "safe to delete")+ it "reports an aggregate moved between members as an ownership move, not stale churn" $+ withWorkspaceFixture "keiro-dsl-workspace-move" id $ \root out workspace -> do+ _ <- executePlannedWorkspaceScaffold out workspace+ treeBefore <- treeSnapshot out+ moved <- moveArtifactAggregate root+ second <- executePlannedWorkspaceScaffold out moved+ wsrStale second `shouldBe` []+ let moves = wsrOwnershipMoves second+ moves `shouldSatisfy` (not . null)+ moves+ `shouldSatisfy` all+ ( \move ->+ omPrevious move == Just "domain/project-artifact.keiro"+ && omCurrent move == Just "domain/project.keiro"+ )+ map omPath moves+ `shouldSatisfy` any (T.isInfixOf "ProjectArtifact" . T.pack)+ -- Stable behavior contracts retain source-line attribution, so+ -- moving a declaration may rewrite only those attributed modules.+ -- Every unaffected module and the build manifest remain untouched.+ map thd3 (wsrDispositions second)+ `shouldSatisfy` all (`elem` [Unchanged, Skipped, Overwritten])+ let overwrittenPaths =+ [modulePath generatedModule | (generatedModule, _, Overwritten) <- wsrDispositions second]+ length overwrittenPaths `shouldBe` 2+ overwrittenPaths `shouldSatisfy` all (T.isSuffixOf "/Generated/BehaviorContract.hs" . T.pack)+ treeAfter <- treeSnapshot out+ map fst treeAfter `shouldBe` map fst treeBefore+ let unaffected (path, _) =+ path /= workspaceLedgerFileName "demo-project"+ && path `notElem` overwrittenPaths+ filter unaffected treeAfter `shouldBe` filter unaffected treeBefore+ renderWorkspaceScaffoldReport second+ `shouldSatisfy` any (T.isInfixOf "changed owning member")+ it "leaves the tree, record, and manifest untouched when any member refuses" $+ withWorkspaceFixture "keiro-dsl-workspace-atomic" id $ \_ out workspace -> do+ _ <- executePlannedWorkspaceScaffold out workspace+ treeBefore <- treeSnapshot out+ let broken = withCaseVariantAggregate workspace+ case planWorkspaceScaffold "goldens" (workspaceContext broken) broken of+ Right _ -> expectationFailure "expected the broken workspace to refuse"+ Left refusals -> refusals `shouldSatisfy` any isPathCollision+ treeSnapshot out `shouldReturn` treeBefore+ -- A fresh output directory is never even created.+ withTempDirectory "keiro-dsl-workspace-atomic-fresh" $ \fresh -> do+ let target = fresh </> "out"+ case planWorkspaceScaffold "goldens" (workspaceContext broken) broken of+ Right _ -> expectationFailure "expected the broken workspace to refuse"+ Left _ -> doesDirectoryExist target `shouldReturn` False+ it "leaves prior workspace output byte-identical for parse, validation, and collision failures" $+ withWorkspaceFixture "keiro-dsl-workspace-atomic-cli" id $ \root out workspace -> do+ _ <- executePlannedWorkspaceScaffold out workspace+ treeBefore <- treeSnapshot out+ let member = root </> "domain/project-artifact.keiro"+ manifest = root </> "service.keiro-workspace"+ original <- TIO.readFile member+ let failures =+ [ ("parse", "context demo-project\naggregate !!!\n"),+ ("validation", T.replace "ProjectId" "MissingProjectId" original),+ ("collision", T.replace "aggregate ProjectArtifact" "aggregate PROJECT" original)+ ]+ forM_ failures $ \(failureKind, brokenSource) -> do+ TIO.writeFile member brokenSource+ (exitCode, stdoutText, stderrText) <-+ runKeiroDsl ["scaffold", manifest, "--out", out]+ unless (exitCode == ExitFailure 1) $+ expectationFailure+ (failureKind <> " failure unexpectedly scaffolded:\n" <> stdoutText <> stderrText)+ treeSnapshot out `shouldReturn` treeBefore+ TIO.writeFile member original+ it "refuses the whole workspace for one bannerless Generated target, changing nothing" $+ withWorkspaceFixture "keiro-dsl-workspace-banner" id $ \_ out workspace -> do+ plan <- shouldPlanWorkspaceSpec workspace+ let generated = [m | (m, _) <- wpModules plan, kind m == Generated]+ case generated of+ [] -> expectationFailure "workspace fixture has no Generated module"+ target : _ -> do+ let path = out </> modulePath target+ createDirectoryIfMissing True (takeDirectory path)+ TIO.writeFile path "hand owned\n"+ treeBefore <- treeSnapshot out+ refused <- executeWorkspaceScaffold out False plan+ refused `shouldSatisfy` isMissingBannerRefusal+ treeSnapshot out `shouldReturn` treeBefore+ forced <- executeWorkspaceScaffold out True plan+ forced `shouldSatisfy` isSuccessfulScaffold+ TIO.readFile path `shouldReturn` moduleText target+ it "scaffolds a whole workspace through the CLI" $+ withTempDirectory "keiro-dsl-workspace-cli" $ \out -> do+ (exitCode, stdoutText, stderrText) <-+ runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out]+ unless (exitCode == ExitSuccess) (expectationFailure (stdoutText <> stderrText))+ stderrText `shouldContain` "workspace: demo-project"+ doesFileExist (out </> workspaceLedgerFileName "demo-project")+ `shouldReturn` True+ tree <- treeSnapshot out+ length [path | (path, _) <- tree, "StructuralProjections.hs" `T.isSuffixOf` T.pack path]+ `shouldBe` 1+ length [path | (path, _) <- tree, "ReplayAudit.hs" `T.isSuffixOf` T.pack path]+ `shouldBe` 1+ (secondCode, _, secondErr) <-+ runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out]+ secondCode `shouldBe` ExitSuccess+ secondErr `shouldSatisfy` (not . isInfixOfString "(overwritten)")+ treeSnapshot out `shouldReturn` tree+ it "accepts a validated runtime-package override and generates exactly one service package" $+ withTempDirectory "keiro-dsl-workspace-runtime-package-cli" $ \out -> do+ (exitCode, stdoutText, stderrText) <-+ runKeiroDsl ["scaffold", canonicalWorkspacePath, "--out", out, "--runtime-package", "demo-runtime"]+ unless (exitCode == ExitSuccess) (expectationFailure (stdoutText <> stderrText))+ tree <- treeSnapshot out+ length [path | (path, _) <- tree, takeExtension path == ".cabal", "keiro-dsl-conformance.workspace.demo-project" `isInfixOfString` path]+ `shouldBe` 1+ stderrText `shouldSatisfy` isInfixOfString "conformance-target: cabal test keiro-demo-project-conformance"++ describe "workspace adoption" $ do+ it "replaces embedded 0.6 nominal declarations only in generated files" $+ withWorkspaceFixture "keiro-dsl-workspace-nominal-adopt" id $ \_ out workspace -> do+ plan <- shouldPlanWorkspaceSpec workspace+ let pathEndingIn suffix selectedKind =+ case [modulePath m | (m, _) <- wpModules plan, kind m == selectedKind, suffix `isSuffixOfPath` m] of+ [path] -> pure path+ found -> expectationFailure ("expected one path ending in " <> suffix <> ", got " <> show found) >> fail "unreachable"+ domainPath <- pathEndingIn "Project/Generated/Domain.hs" Generated+ nominalPath <- pathEndingIn "Generated/Nominals.hs" Generated+ internalNominalPath <- pathEndingIn "Generated/Nominals/Internal.hs" Generated+ holePath <- pathEndingIn "Project/Holes.hs" HoleStub+ writeFileWithParents+ (out </> domainPath)+ (generatedBanner <> "\n-- legacy 0.6 fixture\nmodule LegacyDomain where\nnewtype ProjectId = ProjectId String\ndata ProjectPhase = Draft | Active\n")+ writeFileWithParents (out </> holePath) "-- hand-owned 0.6 implementation\n"++ report <- executePlannedWorkspaceScaffold out workspace+ wsrStale report `shouldBe` []+ [disposition | (m, _, disposition) <- wsrDispositions report, modulePath m == domainPath]+ `shouldBe` [Overwritten]+ [disposition | (m, _, disposition) <- wsrDispositions report, modulePath m == nominalPath]+ `shouldBe` [Overwritten]+ newDomain <- TIO.readFile (out </> domainPath)+ newDomain `shouldSatisfy` (not . T.isInfixOf "newtype ProjectId")+ newDomain `shouldSatisfy` T.isInfixOf "Generated.Nominals (ProjectId, parseProjectId, ProjectPhase (..))"+ newNominals <- TIO.readFile (out </> nominalPath)+ T.count "newtype ProjectId" newNominals `shouldBe` 0+ T.count "data ProjectPhase =" newNominals `shouldBe` 1+ newInternalNominals <- TIO.readFile (out </> internalNominalPath)+ T.count "newtype ProjectId" newInternalNominals `shouldBe` 1+ TIO.readFile (out </> holePath) `shouldReturn` "-- hand-owned 0.6 implementation\n"+ it "adopts an overwritten same-context record pair by record and by banner" $+ withInlineWorkspace "keiro-dsl-workspace-adopt" adoptionMembers $ \_ out workspace -> do+ -- Reproduce today's defect first: two same-context specs+ -- scaffolded independently into one directory, the second+ -- replacing the first's record and calling its files stale.+ specA <- parseInlineSpec "domain/a.keiro" adoptionMemberA+ specB <- parseInlineSpec "domain/b.keiro" adoptionMemberB+ let ctx = defaultContext "adoption-demo"+ legacyA <- executePlannedScaffold out "domain/a.keiro" ctx specA+ legacyB <- executePlannedScaffold out "domain/b.keiro" ctx specB+ reportStale legacyB `shouldSatisfy` (not . null)+ legacyBefore <- TIO.readFile (out </> recordFileName "adoption-demo")++ report <- executePlannedWorkspaceScaffold out workspace+ wsrStale report `shouldBe` []+ case wsrMigration report of+ Nothing -> expectationFailure "expected the first workspace run to adopt"+ Just migration -> do+ let generatedOf run = sort [modulePath m | (m, _) <- reportDispositions run, kind m == Generated]+ claimedBy evidence = sort [cfPath entry | entry <- mrClaimed migration, cfEvidence entry == evidence]+ -- The surviving record attributes B's files; A's+ -- files survived only as banners, which is exactly+ -- the orphan case the overwrite created.+ claimedBy ClaimedFromRecord `shouldBe` generatedOf legacyB+ claimedBy ClaimedFromBanner `shouldBe` sort (generatedOf legacyA \\ generatedOf legacyB)+ claimedBy ClaimedFromBanner `shouldSatisfy` (not . null)+ mrLikelyStale migration `shouldBe` []+ mrLegacyRecord migration+ `shouldBe` Just (recordFileName "adoption-demo", "domain/b.keiro")+ -- Provenance is persisted, not merely printed.+ recorded <- parseWorkspaceRecord <$> TIO.readFile (wsrRecordPath report)+ fmap (sort . map adPath . wrAdopted) recorded+ `shouldBe` Just (sort (map cfPath (mrClaimed migration)))+ fmap (sort . nubOrd . map adEvidence . wrAdopted) recorded+ `shouldBe` Just ["banner", "record"]+ persisted <- TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")+ persisted `shouldBe` T.unlines (renderMigrationReport migration)+ renderWorkspaceScaffoldReport report+ `shouldSatisfy` any (T.isInfixOf "adopting pre-workspace scaffold output")++ -- The legacy record gained one line and nothing else: it+ -- still parses to the same value for an old binary.+ legacyAfter <- TIO.readFile (out </> recordFileName "adoption-demo")+ T.lines legacyAfter `shouldSatisfy` elem (supersededByLine "adoption-demo")+ parseRecord legacyAfter `shouldBe` parseRecord legacyBefore+ T.lines legacyAfter+ `shouldBe` T.lines legacyBefore <> [supersededByLine "adoption-demo"]++ -- Adoption is not a content change: the generated tree is+ -- what a fresh workspace scaffold of the same members emits.+ withInlineWorkspace "keiro-dsl-workspace-adopt-fresh" adoptionMembers $ \_ fresh freshWorkspace -> do+ freshReport <- executePlannedWorkspaceScaffold fresh freshWorkspace+ wsrMigration freshReport `shouldBe` Nothing+ adoptedTree <- treeSnapshot out+ freshTree <- treeSnapshot fresh+ haskellOnly adoptedTree `shouldBe` haskellOnly freshTree+ it "adopts and marks context history under the legacy record name" $+ withInlineWorkspace "keiro-dsl-workspace-adopt-legacy-name" adoptionMembers $ \_ out workspace -> do+ specA <- parseInlineSpec "domain/a.keiro" adoptionMemberA+ _ <- executePlannedScaffold out "domain/a.keiro" (defaultContext "adoption-demo") specA+ let current = contextLedgerFileName "adoption-demo"+ legacy = legacyContextRecordFileName "adoption-demo"+ renameFile (out </> current) (out </> legacy)+ ledgerBefore <- TIO.readFile (out </> legacy)+ report <- executePlannedWorkspaceScaffold out workspace+ case wsrMigration report of+ Nothing -> expectationFailure "expected legacy-name context history to be adopted"+ Just migration -> mrLegacyRecord migration `shouldBe` Just (legacy, "domain/a.keiro")+ doesFileExist (out </> current) `shouldReturn` False+ ledgerAfter <- TIO.readFile (out </> legacy)+ T.lines ledgerAfter `shouldBe` T.lines ledgerBefore <> [supersededByLine "adoption-demo"]+ parseRecord ledgerAfter `shouldBe` parseRecord ledgerBefore+ it "lists hand-written files as unclaimed and leaves their bytes alone" $+ withInlineWorkspace "keiro-dsl-workspace-unclaimed" adoptionMembers $ \_ out workspace -> do+ plan <- shouldPlanWorkspaceSpec workspace+ case [modulePath m | (m, _) <- wpModules plan, kind m == HoleStub] of+ [] -> expectationFailure "adoption fixture emits no hole module"+ holePath : _ -> do+ writeFileWithParents (out </> holePath) "-- hand filled\n"+ writeFileWithParents (out </> "Notes.hs") "module Notes where\n"+ report <- executePlannedWorkspaceScaffold out workspace+ case wsrMigration report of+ Nothing -> expectationFailure "expected a report for a directory holding hand-written files"+ Just migration -> do+ mrLegacyRecord migration `shouldBe` Nothing+ mrClaimed migration `shouldBe` []+ mrUnclaimed migration `shouldBe` sort [holePath, "Notes.hs"]+ TIO.readFile (out </> holePath) `shouldReturn` "-- hand filled\n"+ TIO.readFile (out </> "Notes.hs") `shouldReturn` "module Notes where\n"+ it "never claims a bannerless file at a planned Generated path" $+ withInlineWorkspace "keiro-dsl-workspace-unattributable" adoptionMembers $ \_ out workspace -> do+ plan <- shouldPlanWorkspaceSpec workspace+ case [modulePath m | (m, _) <- wpModules plan, kind m == Generated] of+ [] -> expectationFailure "adoption fixture emits no Generated module"+ target : _ -> do+ writeFileWithParents (out </> target) "hand owned\n"+ refused <- executeWorkspaceScaffold out False plan+ refused `shouldSatisfy` isMissingBannerRefusal+ TIO.readFile (out </> target) `shouldReturn` "hand owned\n"+ doesFileExist (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")+ `shouldReturn` False+ it "adopts at most once, and the second run is an ordinary idempotent run" $+ withInlineWorkspace "keiro-dsl-workspace-adopt-once" adoptionMembers $ \_ out workspace -> do+ specA <- parseInlineSpec "domain/a.keiro" adoptionMemberA+ _ <- executePlannedScaffold out "domain/a.keiro" (defaultContext "adoption-demo") specA+ first <- executePlannedWorkspaceScaffold out workspace+ wsrMigration first `shouldSatisfy` \case Just _ -> True; Nothing -> False+ treeBefore <- treeSnapshot out+ reportBefore <- TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")+ legacyBefore <- TIO.readFile (out </> recordFileName "adoption-demo")++ second <- executePlannedWorkspaceScaffold out workspace+ wsrMigration second `shouldBe` Nothing+ wsrStale second `shouldBe` []+ map thd3 (wsrDispositions second) `shouldSatisfy` all (`elem` [Unchanged, Skipped])+ treeSnapshot out `shouldReturn` treeBefore+ TIO.readFile (out </> "keiro-dsl-migration-report.workspace.adoption-demo.txt")+ `shouldReturn` reportBefore+ legacyAfter <- TIO.readFile (out </> recordFileName "adoption-demo")+ legacyAfter `shouldBe` legacyBefore+ length (filter (== supersededByLine "adoption-demo") (T.lines legacyAfter))+ `shouldBe` 1++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+ [] -> ""++generatedExtensionsEndingIn :: T.Text -> [ScaffoldModule] -> [T.Text]+generatedExtensionsEndingIn suffix modules = case [generatedModule | generatedModule <- modules, kind generatedModule == Generated, suffix `T.isSuffixOf` T.pack (modulePath generatedModule)] of+ [generatedModule] -> generatedLocalExtensions generatedModule+ matches -> error ("expected one generated module ending in " <> T.unpack suffix <> ", got " <> show (map modulePath matches))++generatedLocalExtensions :: ScaffoldModule -> [T.Text]+generatedLocalExtensions generatedModule =+ [ extension+ | line <- takeWhile (T.isPrefixOf languagePrefix) (T.lines (moduleText generatedModule)),+ Just extensionWithSuffix <- [T.stripPrefix languagePrefix line],+ Just extension <- [T.stripSuffix languageSuffix extensionWithSuffix]+ ]+ where+ languagePrefix = "{-# LANGUAGE "+ languageSuffix = " #-}"++holeTextEndingIn :: T.Text -> [ScaffoldModule] -> T.Text+holeTextEndingIn suffix modules = case [moduleText m | m <- modules, kind m == HoleStub, suffix `T.isSuffixOf` T.pack (modulePath m), not ("BehaviorHoles.hs" `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"+ ]++scalarRegisterCases :: [(T.Text, T.Text)]+scalarRegisterCases =+ [ ("Text", "\"sample\""),+ ("Int", "0"),+ ("Bool", "False"),+ ("Time", "\"2026-01-02T03:04:05.123456789012Z\""),+ ("Natural", "0")+ ]++cleanScalarAggregateSpec :: T.Text -> T.Text -> T.Text+cleanScalarAggregateSpec typeName initialValue =+ T.unlines+ [ "context clean-scalar",+ "",+ "aggregate Scalar",+ " regs",+ " value " <> typeName <> " = " <> initialValue,+ " states Empty Done!",+ " command Set { value:" <> typeName <> " }",+ " event SetDone { value:" <> typeName <> " }",+ " Empty -- Set --> write value := value ; emit SetDone ; 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 == TRef "CounterVertex" then TRef "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+ service <- checkedServiceOf path+ pure (validateService service)++-- | 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",+ tImplementation = LegacyHoleImplementation,+ 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 <- parsedSourceOf oldP+ new <- parsedSourceOf newP+ pure (diffSources old new)++kindOfChange :: Change -> ChangeKind+kindOfChange (Additive kind) = kind+kindOfChange (Advisory kind) = kind+kindOfChange (Breaking kind) = kind++generatedHaskellNameFindings :: [Change] -> [Change]+generatedHaskellNameFindings = filter ((== GeneratedHaskellNameChanged) . ckCode . kindOfChange)++assertGeneratedHaskellNameFinding :: Change -> Expectation+assertGeneratedHaskellNameFinding change = do+ change `shouldSatisfy` isAdvisory+ let kind = kindOfChange change+ compatibility = ckVector kind+ nonBuildVerdicts =+ [ verdictFor surface compatibility+ | surface <- [PrivateHistoryRead, OldBinaryReadNewEvents, SnapshotHydration, PublicConsumer, PersistedIdentity]+ ]+ nonBuildVerdicts `shouldBe` replicate 5 VCompatible+ verdictFor ConsumerBuild compatibility `shouldBe` VAdvisory+ cvRollout compatibility `shouldBe` Set.empty+ renderFinding change `shouldSatisfy` T.isInfixOf "consumer-build=advisory"+ remediationFor (ckContext kind) (ckCode kind)+ `shouldBe` RemedyRescaffoldGenerated :| [RemedyRecompileConsumers, RemedyRunConformance]++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,+ RolloutProducerFirst+ ]++replayImpactFixtures :: FilePath -> FilePath -> IO ReplayImpact+replayImpactFixtures oldPath newPath = do+ old <- checkedServiceOf oldPath+ new <- checkedServiceOf newPath+ pure (resolvedFold (ReplayImpact.replayImpactServices 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+ ]+ }++mapContract :: (ContractNode -> ContractNode) -> Spec -> Spec+mapContract update spec =+ spec+ { specNodes =+ [ case node of+ NContract contract -> NContract (update contract)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapIntake :: (IntakeNode -> IntakeNode) -> Spec -> Spec+mapIntake update spec =+ spec+ { specNodes =+ [ case node of+ NIntake intake -> NIntake (update intake)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapPgmqDispatch :: (PgmqDispatchNode -> PgmqDispatchNode) -> Spec -> Spec+mapPgmqDispatch update spec =+ spec+ { specNodes =+ [ case node of+ NPgmqDispatch dispatch -> NPgmqDispatch (update dispatch)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapRouter :: (RouterNode -> RouterNode) -> Spec -> Spec+mapRouter update spec =+ spec+ { specNodes =+ [ case node of+ NRouter router -> NRouter (update router)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapEmit :: (EmitNode -> EmitNode) -> Spec -> Spec+mapEmit update spec =+ spec+ { specNodes =+ [ case node of+ NEmit emitNode -> NEmit (update emitNode)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapWorkflow :: (WorkflowNode -> WorkflowNode) -> Spec -> Spec+mapWorkflow update spec =+ spec+ { specNodes =+ [ case node of+ NWorkflow workflow -> NWorkflow (update workflow)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapWorkqueue :: (WorkqueueNode -> WorkqueueNode) -> Spec -> Spec+mapWorkqueue update spec =+ spec+ { specNodes =+ [ case node of+ NWorkqueue queue -> NWorkqueue (update queue)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapDispatch :: (PgmqDispatchNode -> PgmqDispatchNode) -> Spec -> Spec+mapDispatch update spec =+ spec+ { specNodes =+ [ case node of+ NPgmqDispatch dispatch -> NPgmqDispatch (update dispatch)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapOperation :: (OperationNode -> OperationNode) -> Spec -> Spec+mapOperation update spec =+ spec+ { specNodes =+ [ case node of+ NOperation operation -> NOperation (update operation)+ _ -> node+ | node <- specNodes spec+ ]+ }++mapPublisher :: (PublisherNode -> PublisherNode) -> Spec -> Spec+mapPublisher update spec =+ spec+ { specNodes =+ [ case node of+ NPublisher publisher -> NPublisher (update publisher)+ _ -> node+ | node <- specNodes spec+ ]+ }++serviceErrorCodes :: Int -> Spec -> [DiagnosticCode]+serviceErrorCodes versionNumber spec =+ [code diagnostic | diagnostic <- validateService service, severity diagnostic == Error]+ where+ service = case languageVersion (fromIntegral versionNumber) >>= effectiveLanguageContractForVersion of+ Nothing -> error ("unsupported test language version " <> show versionNumber)+ Just languageContract -> CheckedService languageContract spec++-- | Codes emitted at 'Warning' severity under the given released language.+-- Pairs with 'serviceErrorCodes' to assert a surface's warn-then-error tiering+-- from both sides, rather than only proving it is not an error.+serviceWarningCodes :: Int -> Spec -> [DiagnosticCode]+serviceWarningCodes versionNumber spec =+ [code diagnostic | diagnostic <- validateService service, severity diagnostic == Warning]+ where+ service = case languageVersion (fromIntegral versionNumber) >>= effectiveLanguageContractForVersion of+ Nothing -> error ("unsupported test language version " <> show versionNumber)+ Just languageContract -> CheckedService languageContract spec++duplicateFirst :: [a] -> [a]+duplicateFirst = \case+ [] -> []+ first : rest -> first : first : rest++updateFirst :: (a -> a) -> [a] -> [a]+updateFirst update = \case+ [] -> []+ first : rest -> update first : rest++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]++diagnosticCodes :: Spec -> [DiagnosticCode]+diagnosticCodes = map code . validateSpec++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])++assertSkeletonUsesStableLanguage :: T.Text -> IO ()+assertSkeletonUsesStableLanguage kind = case skeletonFor kind of+ Left err -> expectationFailure (T.unpack ("skeleton for " <> kind <> ": " <> err))+ Right source -> case parseSource ("new:" <> T.unpack kind) source of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure))+ Right parsed -> do+ let service = checkedSource parsed+ effectiveContractLanguageVersion (checkedLanguageContract service) `shouldBe` currentStableLanguageVersion+ effectiveLanguageSupport (checkedLanguageContract service) `shouldBe` Stable+ [code diagnostic | diagnostic <- validateService service, severity diagnostic == Error]+ `shouldBe` ([] :: [DiagnosticCode])+ scaffoldServiceModules (defaultContext (specContext (checkedSpec service))) service+ `shouldSatisfy` (not . null)++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")+ ]++assertStableSkeletonMatchesCommitted :: T.Text -> T.Text -> IO ()+assertStableSkeletonMatchesCommitted kind root = case skeletonFor kind of+ Left err -> expectationFailure (T.unpack err)+ Right source -> case parseSource ("new:" <> T.unpack kind) source of+ Left err -> expectationFailure (T.unpack (renderParseFailure err))+ Right parsed -> do+ let service = checkedSource parsed+ spec = checkedSpec service+ let ctx = (defaultContext (specContext spec)) {moduleRoot = root}+ forM_ [m | m <- scaffoldServiceModules ctx service, 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++isFoldSurfaceRefusal :: Either [Refusal] modules -> Bool+isFoldSurfaceRefusal (Left refusals) = any isFold refusals+ where+ isFold FoldSurfaceRefusal {} = True+ isFold _ = False+isFoldSurfaceRefusal (Right _) = False++-- | The canonical positive workspace fixture: three members under one context.+canonicalWorkspacePath :: FilePath+canonicalWorkspacePath = "test/fixtures/workspace/service.keiro-workspace"++-- | Deterministic workspace source used to model git blobs without invoking git.+memoryContentSource :: Map.Map FilePath T.Text -> ContentSource+memoryContentSource files =+ ContentSource+ { csRead = \path ->+ pure $ maybe (Left ("missing in-memory content: " <> T.pack path)) Right (Map.lookup path files)+ }++changeCode :: Change -> DiagnosticCode+changeCode (Additive kind) = ckCode kind+changeCode (Advisory kind) = ckCode kind+changeCode (Breaking kind) = ckCode kind++breakingSurfaces :: Change -> [CompatibilitySurface]+breakingSurfaces change =+ [ surface+ | surface <- [minBound .. maxBound],+ verdictFor surface (ckVector kind) == VBreaking+ ]+ where+ kind = case change of+ Additive value -> value+ Advisory value -> value+ Breaking value -> value++workspaceChangeKind :: Change -> ChangeKind+workspaceChangeKind (Additive kind) = kind+workspaceChangeKind (Advisory kind) = kind+workspaceChangeKind (Breaking kind) = kind++-- | The same members as 'canonicalWorkspacePath', listed in reverse order.+reorderedWorkspacePath :: FilePath+reorderedWorkspacePath = "test/fixtures/workspace/service-reordered.keiro-workspace"++-- | Load and compose a workspace fixture, failing the test on a refusal. The+-- fixture path is package-relative; the loader is rooted at the manifest's own+-- directory, exactly as the CLI roots it.+shouldComposeWorkspace :: FilePath -> IO WorkspaceSpec+shouldComposeWorkspace path = do+ resolved <- resolveTestPath path+ loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) resolved+ case loaded of+ Left failure ->+ expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure resolved failure)))+ >> error "unreachable"+ Right workspace -> pure workspace {wsManifestPath = path}++-- | The 'Context' a workspace scaffolds under, with no CLI overrides: the+-- members' unanimous context name, the manifest's module-root and layout+-- authority, and the built-in defaults where the manifest is silent.+workspaceContext :: WorkspaceSpec -> Context+workspaceContext workspace =+ Context+ { contextName = wsContext workspace,+ moduleRoot = maybe "" id (wsModuleRoot workspace),+ placement = maybe GeneratedPrefix id (wsLayout workspace)+ }++-- | Compose and plan a workspace fixture, failing the test on any refusal.+shouldPlanWorkspace :: FilePath -> IO WorkspacePlan+shouldPlanWorkspace path = do+ workspace <- shouldComposeWorkspace path+ case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of+ Left refusals -> expectationFailure ("unexpected workspace plan refusal: " <> show refusals) >> error "unreachable"+ Right plan -> pure plan++-- | Does a scaffolded module's path end in this suffix?+isSuffixOfPath :: FilePath -> ScaffoldModule -> Bool+isSuffixOfPath suffix m = T.pack suffix `T.isSuffixOf` T.pack (modulePath m)++-- | A workspace record built from real composed data plus two synthetic+-- adoption rows, so the round-trip test exercises every row kind including the+-- JSON encodings shared with the v1 record.+sampleWorkspaceRecord :: WorkspaceSpec -> WorkspaceRecord+sampleWorkspaceRecord workspace =+ WorkspaceRecord+ { wrService = wsService workspace,+ wrManifest = "service.keiro-workspace",+ wrContext = wsContext workspace,+ wrModuleRoot = maybe "" id (wsModuleRoot workspace),+ wrLayout = "collocated",+ wrMembers = map wmPath (wsMembers workspace),+ wrSourceLanguages =+ [ WorkspaceSourceLanguageRow (wmPath member) (wmSourceLanguage member)+ | member <- wsMembers workspace+ ],+ wrLanguageContract = wsLanguageContract workspace,+ wrNamingEdition = IdiomaticNamingV1,+ wrModules =+ [ WorkspaceModuleRow Generated "Demo/Generated/StructuralProjections.hs" Nothing Nothing,+ WorkspaceModuleRow Generated "Demo/Project/Generated/Domain.hs" (Just "domain/project.keiro") Nothing,+ WorkspaceModuleRow HoleStub "Demo/Project/Holes.hs" (Just "domain/shared.keiro") Nothing+ ],+ wrMappings = consumerMappings (consumerPlan (wsMergedSpec workspace)),+ wrIdDomains = [],+ wrNominalEqualities = nominalEqualityIdentities (wsMergedSpec workspace),+ wrBindingObligations = either (const []) id (bindingHoles (wsMergedSpec workspace)),+ wrBehaviorRequirements = [],+ wrAdopted =+ [ AdoptedRow "claimed/One.hs" "record" (Just "keiro-dsl-ledger.context.demo-project.txt") (Just "project.keiro"),+ AdoptedRow "claimed/Two.hs" "banner" Nothing Nothing+ ]+ }++-- | The canonical workspace with a case-variant copy of one member's aggregate+-- grafted onto another member. Composition refuses this shape (EP-153 catches it+-- at the earliest boundary), so the planner's own cross-member collision gate can+-- only be exercised by constructing the graph directly — which is exactly what+-- this does, mirroring the single-file @caseVariant@ construction.+withCaseVariantAggregate :: WorkspaceSpec -> WorkspaceSpec+withCaseVariantAggregate workspace = case [aggregate | NAggregate aggregate <- specNodes merged, aggName aggregate == "Project"] of+ [] -> error "canonical workspace fixture has no Project aggregate"+ aggregate : _ ->+ let shouted = aggregate {aggName = T.toUpper (aggName aggregate)}+ ownership = wsOwnership workspace+ in workspace+ { wsMergedSpec = merged {specNodes = specNodes merged <> [NAggregate shouted]},+ wsOwnership =+ ownership+ { oiNodes =+ Map.insert+ ("aggregate", aggName shouted)+ ("domain/project-artifact.keiro", Loc 1)+ (oiNodes ownership)+ }+ }+ where+ merged = wsMergedSpec workspace++-- | Write a one-member workspace whose member declares an upcaster, so its+-- golden payload fixture has a canonical location. Returns the composed+-- workspace; the caller decides where the fixture lives.+writeGoldenWorkspace :: FilePath -> IO WorkspaceSpec+writeGoldenWorkspace root = do+ source <- readTestText "test/fixtures/reservation-v2.keiro"+ createDirectoryIfMissing True (root </> "domain")+ TIO.writeFile (root </> "domain/reservation.keiro") source+ let manifestPath = root </> "service.keiro-workspace"+ TIO.writeFile manifestPath "service gold-demo\nspec domain/reservation.keiro\n"+ loaded <- loadWorkspace (fileContentSource root) manifestPath+ case loaded of+ Left failure ->+ expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure manifestPath failure)))+ >> error "unreachable"+ Right workspace -> pure workspace++-- | Materialize the canonical fixture workspace in a fresh temporary directory+-- and hand the callback its root, a sibling output directory, and the composed+-- workspace. Working on a copy is what lets a test edit a member and re-scaffold.+--+-- The manifest's @spec@ lines are passed through the given function first, so a+-- caller can list the same members in a different order; the manifest __file+-- name__ stays the same, which is what makes two runs comparable byte for byte.+withWorkspaceFixture ::+ String ->+ ([FilePath] -> [FilePath]) ->+ (FilePath -> FilePath -> WorkspaceSpec -> IO a) ->+ IO a+withWorkspaceFixture template orderMembers act =+ withTempDirectory template $ \base -> do+ let root = base </> "workspace"+ out = base </> "out"+ members =+ [ "domain/project-artifact.keiro",+ "domain/project.keiro",+ "domain/shared.keiro"+ ]+ createDirectoryIfMissing True (root </> "domain")+ forM_ members $ \relative -> do+ source <- readTestText ("test/fixtures/workspace" </> relative)+ TIO.writeFile (root </> relative) source+ TIO.writeFile+ (root </> "service.keiro-workspace")+ ( T.unlines+ ( ["service demo-project", "module Demo.Modules.Project", "layout collocated"]+ <> ["spec " <> T.pack relative | relative <- orderMembers members]+ )+ )+ workspace <- loadTempWorkspace root+ act root out workspace++-- | Materialize an inline workspace — a manifest plus literal member sources —+-- in a fresh temporary directory, and hand the callback its root, a sibling output+-- directory, and the composed workspace.+withInlineWorkspace ::+ String ->+ (T.Text, [(FilePath, T.Text)]) ->+ (FilePath -> FilePath -> WorkspaceSpec -> IO a) ->+ IO a+withInlineWorkspace template (service, members) act =+ withTempDirectory template $ \base -> do+ let root = base </> "workspace"+ out = base </> "out"+ forM_ members $ \(relative, source) -> writeFileWithParents (root </> relative) source+ TIO.writeFile+ (root </> "service.keiro-workspace")+ ( T.unlines+ (("service " <> service) : ["spec " <> T.pack relative | (relative, _) <- members])+ )+ workspace <- loadTempWorkspace root+ act root out workspace++-- | Two independently valid members under one context. Each is a complete spec+-- that the pre-workspace single-file scaffolder accepts, which is what lets a test+-- reproduce the overwritten-record defect before adopting.+adoptionMembers :: (T.Text, [(FilePath, T.Text)])+adoptionMembers = ("adoption-demo", [("domain/a.keiro", adoptionMemberA), ("domain/b.keiro", adoptionMemberB)])++adoptionMemberA :: T.Text+adoptionMemberA =+ T.unlines+ [ "context adoption-demo",+ "",+ "aggregate Counter",+ " regs",+ " count Int = 0",+ " state CounterVertex = Pending",+ " states Pending Done!",+ " command Bump { count:Int }",+ " event CountBumped { count:Int }",+ " Pending -- Bump --> emit CountBumped ; goto Done"+ ]++adoptionMemberB :: T.Text+adoptionMemberB =+ T.unlines+ [ "context adoption-demo",+ "",+ "aggregate Widget",+ " regs",+ " size Int = 0",+ " state WidgetVertex = Draft",+ " states Draft Shipped!",+ " command Ship { size:Int }",+ " event WidgetShipped { size:Int }",+ " Draft -- Ship --> emit WidgetShipped ; goto Shipped"+ ]++writeFileWithParents :: FilePath -> T.Text -> IO ()+writeFileWithParents path contents = do+ createDirectoryIfMissing True (takeDirectory path)+ TIO.writeFile path contents++-- | Only the Haskell sources of a tree snapshot, dropping bookkeeping files.+haskellOnly :: [(FilePath, T.Text)] -> [(FilePath, T.Text)]+haskellOnly entries = [entry | entry@(path, _) <- entries, ".hs" `T.isSuffixOf` T.pack path]++-- | Compose a workspace that a test just wrote to disk.+loadTempWorkspace :: FilePath -> IO WorkspaceSpec+loadTempWorkspace root = do+ let manifestPath = root </> "service.keiro-workspace"+ loaded <- loadWorkspace (fileContentSource root) manifestPath+ case loaded of+ Left failure ->+ expectationFailure (T.unpack (T.intercalate "\n" (renderWorkspaceFailure manifestPath failure)))+ >> error "unreachable"+ Right workspace -> pure workspace++-- | Plan an already-composed workspace, failing the test on a refusal.+shouldPlanWorkspaceSpec :: WorkspaceSpec -> IO WorkspacePlan+shouldPlanWorkspaceSpec workspace =+ case planWorkspaceScaffold "goldens" (workspaceContext workspace) workspace of+ Left refusals -> expectationFailure ("unexpected workspace plan refusal: " <> show refusals) >> error "unreachable"+ Right plan -> pure plan++-- | Plan then execute a whole-workspace scaffold, failing loudly on either.+executePlannedWorkspaceScaffold :: FilePath -> WorkspaceSpec -> IO WorkspaceScaffoldReport+executePlannedWorkspaceScaffold out workspace = do+ plan <- shouldPlanWorkspaceSpec workspace+ result <- executeWorkspaceScaffold out False plan+ case result of+ Left refusals -> expectationFailure ("unexpected workspace execution refusal: " <> show refusals) >> error "unreachable"+ Right report -> pure report++-- | Rename one member's aggregate in place and recompose. Only the+-- @aggregate \<Name\>@ header is rewritten, so declarations that merely share the+-- prefix (@ProjectId@, @ProjectSummary@) are untouched.+renameMemberAggregate :: FilePath -> FilePath -> T.Text -> T.Text -> IO WorkspaceSpec+renameMemberAggregate root member from to = do+ source <- TIO.readFile (root </> member)+ TIO.writeFile (root </> member) (T.replace ("aggregate " <> from <> "\n") ("aggregate " <> to <> "\n") source)+ loadTempWorkspace root++-- | Move the @ProjectArtifact@ aggregate from the artifact member into the+-- project member, and recompose.+--+-- It is prepended, so the merged spec's node order — and therefore every emitted+-- byte, including the replay-audit assembly's aggregate list — is exactly what it+-- was. That isolates the change to ownership, which is the point of the test.+moveArtifactAggregate :: FilePath -> IO WorkspaceSpec+moveArtifactAggregate root = do+ artifact <- TIO.readFile (root </> "domain/project-artifact.keiro")+ project <- TIO.readFile (root </> "domain/project.keiro")+ case T.breakOn "aggregate ProjectArtifact" artifact of+ (kept, moved) | not (T.null moved) -> do+ TIO.writeFile (root </> "domain/project-artifact.keiro") kept+ TIO.writeFile+ (root </> "domain/project.keiro")+ (T.replace "aggregate Project\n" (moved <> "\naggregate Project\n") project)+ loadTempWorkspace root+ _ -> expectationFailure "artifact member has no ProjectArtifact aggregate" >> error "unreachable"++-- | Every regular file under a directory, as @(relative path, contents)@ sorted+-- by path — the comparison unit for "byte-identical output".+treeSnapshot :: FilePath -> IO [(FilePath, T.Text)]+treeSnapshot root = do+ exists <- doesDirectoryExist root+ if not exists then pure [] else sort <$> walk ""+ where+ walk relative = do+ entries <- listDirectory (root </> relative)+ fmap concat . forM (sort entries) $ \entry -> do+ let child = if null relative then entry else relative </> entry+ isDirectory <- doesDirectoryExist (root </> child)+ if isDirectory+ then walk child+ else do+ contents <- TIO.readFile (root </> child)+ pure [(child, contents)]++copyTextTree :: FilePath -> FilePath -> IO ()+copyTextTree source destination =+ treeSnapshot source >>= mapM_ (\(relative, contents) -> writeFileWithParents (destination </> relative) contents)++thd3 :: (a, b, c) -> c+thd3 (_, _, value) = value++isPathCollision :: Refusal -> Bool+isPathCollision PathCollision {} = True+isPathCollision _ = False++isInfixOfString :: String -> String -> Bool+isInfixOfString needle haystack = T.isInfixOf (T.pack needle) (T.pack haystack)++-- | Load a workspace fixture expecting a compose refusal, and return it.+shouldRefuseWorkspace :: FilePath -> IO (NonEmpty WorkspaceDiagnostic)+shouldRefuseWorkspace path = do+ resolved <- resolveTestPath path+ loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) resolved+ case loaded of+ Left (WorkspaceRefused diagnostics) -> pure diagnostics+ Left other ->+ expectationFailure+ ("expected compose refusals, got:\n" <> T.unpack (T.intercalate "\n" (renderWorkspaceFailure resolved other)))+ >> error "unreachable"+ Right _ -> expectationFailure ("expected " <> path <> " to be refused") >> error "unreachable"++-- | Invoke the built @keiro-dsl@ executable. Fixture paths are resolved first,+-- so the test works whether it runs from the package directory or the repository+-- root.+runKeiroDsl :: [String] -> IO (ExitCode, String, String)+runKeiroDsl arguments = do+ resolved <- traverse resolveArgument arguments+ readProcessWithExitCode "cabal" (["run", "-v0", "keiro-dsl", "--"] <> resolved) ""+ where+ resolveArgument argument+ | "test/fixtures/" `isPrefixOfString` argument = resolveTestPath argument+ | otherwise = pure argument+ isPrefixOfString prefix value = take (length prefix) value == prefix++-- | The @spec@ field of a coverage report, i.e. what the report says it covers.+coverageSpecPath :: Value -> Maybe T.Text+coverageSpecPath value = case value of+ Aeson.Object fields -> case KeyMap.lookup "spec" fields of+ Just (Aeson.String path) -> Just path+ _ -> Nothing+ _ -> Nothing++decodeJsonValue :: FilePath -> IO Value+decodeJsonValue path = do+ decoded <- Aeson.eitherDecodeFileStrict path+ case decoded of+ Left err -> expectationFailure (path <> ": " <> err) >> fail "unreachable"+ Right value -> pure value++jsonField :: T.Text -> Value -> Maybe Value+jsonField name = \case+ Aeson.Object fields -> KeyMap.lookup (Key.fromText name) fields+ _ -> Nothing++-- | Order-preserving deduplication for comparing cited file sets.+nubOrd :: (Eq a) => [a] -> [a]+nubOrd = go []+ where+ go seen [] = reverse seen+ go seen (x : xs) = if x `elem` seen then go seen xs else go (x : seen) xs++-- | Parse a workspace manifest, failing the test on a refusal.+shouldParseManifest :: FilePath -> T.Text -> IO WorkspaceManifest+shouldParseManifest path source = case parseWorkspaceManifest path source of+ Left err -> expectationFailure (T.unpack err) >> error "unreachable"+ Right manifest -> pure manifest++-- | Generate a canonical workspace manifest. Members are drawn from a pool of+-- paths that are distinct even under case folding and are held sorted, which is+-- the invariant every parsed manifest satisfies.+genWorkspaceManifest :: Gen WorkspaceManifest+genWorkspaceManifest = do+ service <- elements ["demo-project", "mori", "kotei", "a1", "svc-2"]+ runtimePackage <- elements [Nothing, Just (RuntimePackageName "demo-core"), Just (RuntimePackageName "mori2")]+ moduleRoot <- elements [Nothing, Just "Demo", Just "Demo.Modules.Project"]+ layout <- elements [Nothing, Just GeneratedPrefix, Just CollocatedLeaf]+ chosen <-+ sublistOf+ [ "a.keiro",+ "d-e_f.keiro",+ "domain/b.keiro",+ "domain/sub/c.keiro",+ "x1.keiro"+ ]+ `suchThat` (not . null)+ pure+ WorkspaceManifest+ { wmfService = service,+ wmfServiceLoc = Loc 1,+ wmfRuntimePackage = runtimePackage,+ wmfRuntimePackageLoc = Loc 2,+ wmfModuleRoot = moduleRoot,+ wmfModuleRootLoc = Loc 2,+ wmfLayout = layout,+ wmfLayoutLoc = Loc 3,+ wmfMembers = NE.fromList [WorkspaceMemberRef path (Loc 4) | path <- sort chosen]+ }++-- | Parse a fixture while retaining its released language contract.+parsedSourceOf :: FilePath -> IO ParsedSource+parsedSourceOf path = do+ input <- readTestText path+ case parseSource path input of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "unreachable"+ Right parsed -> pure parsed++checkedServiceOf :: FilePath -> IO CheckedService+checkedServiceOf = fmap checkedSource . parsedSourceOf++renderFoldBaseline :: T.Text -> CheckedService -> T.Text+renderFoldBaseline fixture service =+ T.intercalate+ "\n\n"+ [ T.unlines+ ( [ "fixture=" <> fixture,+ "aggregate=" <> aggName aggregate,+ "fingerprint=" <> aggregateFoldFingerprintForService service aggregate,+ "surface-begin"+ ]+ <> T.lines (aggregateFoldSurfaceForService service aggregate)+ <> ["surface-end"]+ )+ | NAggregate aggregate <- specNodes (checkedSpec service)+ ]++-- | Parse a fixture through the source-aware boundary and return its graph.+specOf :: FilePath -> IO Spec+specOf = fmap checkedSpec . checkedServiceOf++-- | Parse a fixture and scaffold its checked semantic service.+scaffoldFixture :: FilePath -> IO [ScaffoldModule]+scaffoldFixture path = do+ service <- checkedServiceOf path+ pure (scaffoldServiceModules (defaultContext (specContext (checkedSpec service))) service)++legacyScaffoldProcessFixture :: FilePath -> IO [ScaffoldModule]+legacyScaffoldProcessFixture path = do+ spec <- specOf path+ pure $ concat [scaffoldProcess (ctx spec) process | NProcess process <- 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 (orderInsensitiveLines, body) = partition isOrderInsensitive (T.lines text)+ in (normalizeBody body, sort (map normalizeImport orderInsensitiveLines))+ where+ -- Compare the deterministic body exactly as before and imports/language+ -- pragmas as a sorted, whitespace-normalized list. Sorting tolerates+ -- formatter reordering while additions, removals, and renamed entries still+ -- fail the pin.+ isOrderInsensitive line = isImport line || "{-# LANGUAGE " `T.isPrefixOf` line+ normalizeBody =+ -- Fourmolu parenthesizes a single class constraint while the emitter's+ -- compact spelling remains valid Haskell. Treat that formatter-only+ -- rewrite like the whitespace and comma placement normalized below.+ T.replace "(Show value) =>" "Show value =>"+ . T.replace "( " "("+ . T.replace " )" ")"+ . T.replace " , )" " )"+ . T.unwords+ . T.words+ . T.replace "}" " } "+ . T.replace "{" " { "+ . T.replace "]" " ] "+ . T.replace "[" " [ "+ . T.replace "," " , "+ . T.unlines+ . map normalizeBanner+ normalizeBanner line+ | isGeneratedBannerLine line = generatedBanner+ | otherwise = line+ 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.replace " (" "(" . 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++assertMatchesGolden :: FilePath -> T.Text -> IO ()+assertMatchesGolden path actual = do+ resolved <- resolveTestPath path+ update <- lookupEnv "KEIRO_DSL_UPDATE_GOLDENS"+ if update == Just "1"+ then TIO.writeFile resolved (T.stripEnd actual <> "\n")+ else do+ golden <- TIO.readFile resolved+ T.stripEnd actual `shouldBe` T.stripEnd golden++-- | 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++checkedServiceFromText :: FilePath -> T.Text -> IO CheckedService+checkedServiceFromText sourceName src = case parseSource sourceName src of+ Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> error "unreachable"+ Right parsed -> pure (checkedSource parsed)++parseStableRenderedSpec :: FilePath -> Spec -> Either T.Text Spec+parseStableRenderedSpec sourceName spec =+ case parseSource sourceName stableSource of+ Left failure -> Left (renderParseFailure failure)+ Right parsed -> Right (parsedSpec parsed)+ where+ stableSource =+ "language keiro-dsl "+ <> T.pack (show (languageVersionNumber currentStableLanguageVersion))+ <> "\n"+ <> renderSpec spec++shouldParseStableRenderedSpec :: FilePath -> Spec -> IO Spec+shouldParseStableRenderedSpec sourceName spec =+ case parseStableRenderedSpec sourceName spec of+ Left failure -> expectationFailure (T.unpack failure) >> fail "unreachable"+ Right reparsed -> pure reparsed++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 ((/= TRef "VendorGeometry") . regType) (aggRegs aggregate),+ aggCommands = map stripCommand (aggCommands aggregate),+ aggEvents = map stripEvent (aggEvents aggregate)+ }+ stripNode node = node+ stripCommand command = command {cmdFields = filter ((/= Just (TRef "VendorGeometry")) . aggregateFieldType) (cmdFields command)}+ stripEvent event = event {evBody = case evBody event of EventFields fields -> EventFields (filter ((/= Just (TRef "VendorGeometry")) . aggregateFieldType) 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"],+ onInteger = ["integer"],+ 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",+ "}"+ ]++normalizedCollisionSpec :: T.Text+normalizedCollisionSpec =+ T.unlines+ [ "context svc",+ "",+ "contract fooBar {",+ " schemaVersion 1",+ " discriminator kind",+ "}",+ "",+ "contract foo_bar {",+ " 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:" "correlationId",+ tmFireAt = FireAtExpr "observedAt" "5m",+ tmPayload = [],+ tmFire =+ FireNode+ { fireTarget = "Target",+ fireKey = "correlationId",+ fireCommand = "Fire",+ fireFields = [],+ fireFiredEventId = IdExpr UuidV5Id "fired:" "correlationId",+ 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]++genAggregateField :: Gen AggregateField+genAggregateField = AggregateField <$> genName <*> pure Nothing <*> pure Nothing <*> genMaybe (genTypeExpr []) <*> pure noLoc++genReg :: Gen RegDecl+genReg = RegDecl <$> genName <*> genTypeExpr [] <*> 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 genAggregateField <*> 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 genAggregateField]++genTransition :: Gen Transition+genTransition =+ Transition+ <$> genName+ <*> genName+ <*> pure LegacyHoleImplementation+ <*> 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 <*> pure "correlationId"++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 <*> pure Nothing <*> pure Nothing <*> oneof [CTypeId <$> genAdversarialText, pure CText, pure CInt] <*> pure noLoc++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 genWqDispRow :: Gen WqDispRow genWqDispRow = WqDispRow <$> genName <*> genInboxAction <*> pure noLoc
test/conformance-aggregate-scalars/Generated/AggregateScalars/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context aggregate-scalars replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context aggregate-scalars replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.Codec ( scalarLedgerCodec, parseScalarLedgerEvent,@@ -7,8 +7,8 @@ ) where import Generated.AggregateScalars.ScalarLedger.Domain-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -20,7 +20,7 @@ scalarLedgerEventTypes :: NonEmpty EventType-scalarLedgerEventTypes = EventType "ScalarsRecorded" :| []+scalarLedgerEventTypes = EventType "ScalarsRecorded" :| [EventType "FieldIdentityObserved"] scalarLedgerCodec :: Codec ScalarLedgerEvent scalarLedgerCodec =@@ -28,6 +28,7 @@ { eventTypes = scalarLedgerEventTypes , eventType = \case ScalarsRecorded{} -> EventType "ScalarsRecorded"+ FieldIdentityObserved{} -> EventType "FieldIdentityObserved" , schemaVersion = 1 , encode = encodeScalarLedgerEvent , decode = parseScalarLedgerEvent@@ -42,6 +43,23 @@ , "observedAt" .= payload.observedAt , "revision" .= payload.revision ]+ FieldIdentityObserved payload ->+ object+ [ "kind" .= ("FieldIdentityObserved" :: Text)+ , "as" .= payload.as+ , "family" .= payload.family+ , "mdo" .= payload.mdo+ , "proc" .= payload.proc+ , "qualified" .= payload.qualified+ , "rec" .= payload.rec+ , "safe" .= payload.safe+ , "signature" .= payload.signature+ , "stock" .= payload.stock+ , "unsafe" .= payload.unsafe+ , "via" .= payload.via+ , "type" .= payload.payloadType+ , "region_code" .= payload.serviceRegion+ ] parseScalarLedgerEvent :: EventType -> Value -> Either Text ScalarLedgerEvent parseScalarLedgerEvent (EventType tag) = mapLeftText . parseEither (withObject "ScalarLedgerEvent" go)@@ -54,13 +72,30 @@ <$> o .: "observedAt" <*> o .: "revision" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes scalarLedgerEventTypes)+ "FieldIdentityObserved" ->+ FieldIdentityObserved+ <$> ( FieldIdentityObservedData+ <$> o .: "as"+ <*> o .: "family"+ <*> o .: "mdo"+ <*> o .: "proc"+ <*> o .: "qualified"+ <*> o .: "rec"+ <*> o .: "safe"+ <*> o .: "signature"+ <*> o .: "stock"+ <*> o .: "unsafe"+ <*> o .: "via"+ <*> o .: "type"+ <*> o .: "region_code"+ )+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes scalarLedgerEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Domain.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.Domain where import Data.Aeson (FromJSON, ToJSON)@@ -11,7 +11,7 @@ import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalStateShape, CanonicalTypeName) import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime) import Numeric.Natural (Natural) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) @@ -36,7 +36,25 @@ } deriving stock (Generic, Eq, Show) +data FieldIdentityObservedData = FieldIdentityObservedData+ { as :: !Text+ , family :: !Text+ , mdo :: !Text+ , proc :: !Text+ , qualified :: !Text+ , rec :: !Text+ , safe :: !Text+ , signature :: !Text+ , stock :: !Text+ , unsafe :: !Text+ , via :: !Text+ , payloadType :: !Text+ , serviceRegion :: !Text+ }+ deriving stock (Generic, Eq, Show)+ data ScalarLedgerEvent = ScalarsRecorded !ScalarsRecordedData+ | FieldIdentityObserved !FieldIdentityObservedData deriving stock (Generic, Eq, Show) type ScalarLedgerRegs =
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.EventStream ( scalarLedgerCategory , scalarLedgerCommandCategory
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.Harness (harnessAssertions) where import Generated.AggregateScalars.ScalarLedger.Domain@@ -8,7 +8,7 @@ import Keiki.Core (applyEventsEither, defaultValidationOptions, step, validateTransducer, (!)) import Keiro.Codec (eventType) import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime) -- | (label, passed). A driver runs these and exits non-zero on any False, -- naming the failing assertion. Filling a hole wrongly turns a specific@@ -16,8 +16,9 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions scalarLedgerTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: ScalarsRecorded", roundTrips sampleEventScalarsRecorded)+ , ("golden round-trip: FieldIdentityObserved", roundTrips sampleEventFieldIdentityObserved) , ("accepts Record from ScalarLedgerEmpty", acceptRecord) ] ++ forwardReplayRecord@@ -25,12 +26,18 @@ roundTrips :: ScalarLedgerEvent -> Bool roundTrips e = parseScalarLedgerEvent (eventType scalarLedgerCodec e) (encodeScalarLedgerEvent e) == Right e +sampleObservedAt :: UTCTime+sampleObservedAt = UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)+ sampleEventScalarsRecorded :: ScalarLedgerEvent-sampleEventScalarsRecorded = (ScalarsRecorded (ScalarsRecordedData (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0))+sampleEventScalarsRecorded = ScalarsRecorded (ScalarsRecordedData sampleObservedAt 0) +sampleEventFieldIdentityObserved :: ScalarLedgerEvent+sampleEventFieldIdentityObserved = FieldIdentityObserved (FieldIdentityObservedData "sample-as" "sample-family" "sample-mdo" "sample-proc" "sample-qualified" "sample-rec" "sample-safe" "sample-signature" "sample-stock" "sample-unsafe" "sample-via" "sample-type" "sample-region")+ acceptRecord :: Bool acceptRecord =- case step scalarLedgerTransducer (ScalarLedgerEmpty, initialScalarLedgerRegs) ((Record (RecordData (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0))) of+ case step scalarLedgerTransducer (ScalarLedgerEmpty, initialScalarLedgerRegs) (Record (RecordData sampleObservedAt 0)) of Just (v, _, _) -> v == ScalarLedgerRecorded Nothing -> False @@ -38,7 +45,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRecord :: [(String, Bool)] forwardReplayRecord =- case step scalarLedgerTransducer (ScalarLedgerEmpty, initialScalarLedgerRegs) ((Record (RecordData (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0))) of+ case step scalarLedgerTransducer (ScalarLedgerEmpty, initialScalarLedgerRegs) (Record (RecordData sampleObservedAt 0)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseScalarLedgerEvent (eventType scalarLedgerCodec event) (encodeScalarLedgerEvent event)) emitted of
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-aggregate-scalars/Generated/AggregateScalars/ScalarLedger/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarLedger; do not edit. module Generated.AggregateScalars.ScalarLedger.Transducer ( scalarLedgerTransducer , scalarLedgerFoldFingerprint
test/conformance-behavior-complete/BehaviorComplete/Journey/BehaviorHoles.hs view
@@ -12,16 +12,21 @@ behaviorWitnesses :: [BehaviorWitness] behaviorWitnesses =- [ live "behavior-v1-2e1fd6b9580e1a3d" closedHistory pingCommand (Rejects RejectNoOutgoingEdges),+ [ ReplayWitness (key "behavior-v1-0128e858fee6f2b3") [] [legacyStartedEvent 2],+ ReplayWitness (key "behavior-v1-08a2bda57424a16e") [] [startedEvent 1],+ live "behavior-v1-2e1fd6b9580e1a3d" closedHistory pingCommand (Rejects RejectNoOutgoingEdges), live "behavior-v1-2f3ebf37a55781db" activeHistory (decideCommand 5) (Emits (decisionEvent 5 :| [])),- live "behavior-v1-37578058289e05a9" [] (startCommand 0) (Emits (startedEvent 0 :| [])), live "behavior-v1-43b8fc7fa48595dd" activeHistory (startCommand 0) (Rejects RejectNoMatchingEdge), live "behavior-v1-68e75665b789892c" activeHistory (retireCommand 1) (Emits (retiredEvent 1 :| [retirementAuditedEvent 1])),+ live "behavior-v1-76005937ded24bec" activeHistory (legacyStartCommand 2) (Rejects RejectNoMatchingEdge), live "behavior-v1-7ea811586a738ee5" closedHistory (decideCommand 1) (Rejects RejectNoOutgoingEdges), live "behavior-v1-83b0a46823e1a788" [] pingCommand (Rejects RejectNoMatchingEdge),+ live "behavior-v1-8f79f2e40d8b3bd5" [] (legacyStartCommand 2) (Rejects RejectNoMatchingEdge), live "behavior-v1-926739ffb27d20e7" [] (retireCommand 1) (Rejects RejectNoMatchingEdge),+ live "behavior-v1-9c61af3593764622" closedHistory (legacyStartCommand 2) (Rejects RejectNoOutgoingEdges), live "behavior-v1-ba7053f86d15e1b0" [] (decideCommand 1) (Rejects RejectNoMatchingEdge), live "behavior-v1-be8b08a049ab4d8b" closedHistory (startCommand 0) (Rejects RejectNoOutgoingEdges),+ live "behavior-v1-d2fc3eca5c449e84" [] (startCommand 0) (Emits (startedEvent 0 :| [])), live "behavior-v1-db1a553baa3eda84" activeHistory (decideCommand 6) (Emits (decisionEvent 6 :| [])), live "behavior-v1-ea258e9c47d66aac" activeHistory pingCommand NoOp, ReplayWitness (key "behavior-v1-f0fbe3a3ba0b40e8") activeHistory [retiredEvent 0, retirementAuditedEvent 0],@@ -43,6 +48,9 @@ startCommand :: Natural -> JourneyCommand startCommand amountValue = Start (StartData requestIdValue observedAtValue amountValue payloadValue) +legacyStartCommand :: Natural -> JourneyCommand+legacyStartCommand amountValue = LegacyStart (LegacyStartData amountValue)+ decideCommand :: Natural -> JourneyCommand decideCommand amountValue = Decide (DecideData amountValue) @@ -54,6 +62,9 @@ startedEvent :: Natural -> JourneyEvent startedEvent amountValue = Started (StartedData requestIdValue observedAtValue amountValue payloadValue)++legacyStartedEvent :: Natural -> JourneyEvent+legacyStartedEvent amountValue = LegacyStarted (LegacyStartedData amountValue) decisionEvent :: Natural -> JourneyEvent decisionEvent amountValue = DecisionRecorded (DecisionRecordedData amountValue)
test/conformance-behavior-complete/BehaviorComplete/Journey/Holes.hs view
@@ -10,7 +10,7 @@ -- and every declared guard/write. This module supplies explicit event-field -- mappings and explicitly selected Hole behavior only; fields(Command) -- identity mappings are generated directly and have no hook.-module BehaviorComplete.Journey.Holes () where+module BehaviorComplete.Journey.Holes (transition1EmptyStartOutput1Started) where import Generated.BehaviorComplete.Journey.Domain import Keiki.Builder qualified as B@@ -18,7 +18,8 @@ -- Deliberately retained migration sentinel: this is the pre-IR-13 identity-copy -- hook name. The generated transducer neither imports nor calls it, and the--- scaffold report names it as obsolete.+-- scaffold report names it as obsolete. It is exported only to keep this+-- deliberately dead migration fixture visible to the warning-clean compiler. transition1EmptyStartOutput1Started :: B.PayloadProj JourneyRegs JourneyCommand (RegFieldsOf StartData) -> StartedTermFields JourneyRegs JourneyCommand (RegFieldsOf StartData) transition1EmptyStartOutput1Started d = StartedTermFields
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.-module Generated.BehaviorComplete.Journey.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+module Generated.BehaviorComplete.Journey.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.BehaviorComplete.Journey.Codec (encodeJourneyEvent, parseJourneyEvent, journeyCodec) import Generated.BehaviorComplete.Journey.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,20 +126,253 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-2e1fd6b9580e1a3d") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyClosed "Ping" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-2f3ebf37a55781db") LiveTransition GeneratedAuthoritative GuardTotal JourneyActive "Decide" (Just (K.EdgeRef JourneyActive 0)) (Just JourneyActive) ["DecisionRecorded"] 41- , BehaviorRequirement (BehaviorKey "behavior-v1-37578058289e05a9") LiveTransition GeneratedAuthoritative GuardTotal JourneyEmpty "Start" (Just (K.EdgeRef JourneyEmpty 0)) (Just JourneyActive) ["Started"] 36- , BehaviorRequirement (BehaviorKey "behavior-v1-43b8fc7fa48595dd") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyActive "Start" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-68e75665b789892c") LiveTransition GeneratedAuthoritative GuardUnknown JourneyActive "Retire" (Just (K.EdgeRef JourneyActive 3)) (Just JourneyClosed) ["Retired", "RetirementAudited"] 56- , BehaviorRequirement (BehaviorKey "behavior-v1-7ea811586a738ee5") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyClosed "Decide" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-83b0a46823e1a788") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyEmpty "Ping" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-926739ffb27d20e7") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyEmpty "Retire" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-ba7053f86d15e1b0") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyEmpty "Decide" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-be8b08a049ab4d8b") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyClosed "Start" Nothing Nothing [] 21- , BehaviorRequirement (BehaviorKey "behavior-v1-db1a553baa3eda84") LiveTransition GeneratedAuthoritative GuardTotal JourneyActive "Decide" (Just (K.EdgeRef JourneyActive 1)) (Just JourneyActive) ["DecisionRecorded"] 47- , BehaviorRequirement (BehaviorKey "behavior-v1-ea258e9c47d66aac") LiveTransition GeneratedAuthoritative GuardTotal JourneyActive "Ping" (Just (K.EdgeRef JourneyActive 2)) (Just JourneyActive) [] 53- , BehaviorRequirement (BehaviorKey "behavior-v1-f0fbe3a3ba0b40e8") ReplayTransition GeneratedAuthoritative GuardNotApplicable JourneyActive "Retire" (Just (K.EdgeRef JourneyActive 4)) (Just JourneyClosed) ["Retired", "RetirementAudited"] 63- , BehaviorRequirement (BehaviorKey "behavior-v1-f9cae2bf4c0d0562") RequiredRejection GeneratedAuthoritative GuardNotApplicable JourneyClosed "Retire" Nothing Nothing [] 21+ [ -- JourneyEmpty x LegacyStart: replay-only transition (spec line 62)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-0128e858fee6f2b3"+ , requirementKind = ReplayTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "LegacyStart"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyEmpty 2))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = ["LegacyStarted"]+ , requirementLine = 62+ }+ , -- JourneyEmpty x Start: replay-only transition (spec line 50)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-08a2bda57424a16e"+ , requirementKind = ReplayTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "Start"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyEmpty 1))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = ["Started"]+ , requirementLine = 50+ }+ , -- JourneyClosed x Ping: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-2e1fd6b9580e1a3d"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyClosed+ , requirementCommandName = "Ping"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyActive x Decide: live transition (spec line 44)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-2f3ebf37a55781db"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = JourneyActive+ , requirementCommandName = "Decide"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyActive 0))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = ["DecisionRecorded"]+ , requirementLine = 44+ }+ , -- JourneyActive x Start: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-43b8fc7fa48595dd"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyActive+ , requirementCommandName = "Start"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyActive x Retire: live transition (spec line 70)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-68e75665b789892c"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = JourneyActive+ , requirementCommandName = "Retire"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyActive 3))+ , requirementTarget = Just JourneyClosed+ , requirementEventKinds = ["Retired", "RetirementAudited"]+ , requirementLine = 70+ }+ , -- JourneyActive x LegacyStart: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-76005937ded24bec"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyActive+ , requirementCommandName = "LegacyStart"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyClosed x Decide: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-7ea811586a738ee5"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyClosed+ , requirementCommandName = "Decide"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyEmpty x Ping: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-83b0a46823e1a788"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "Ping"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyEmpty x LegacyStart: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-8f79f2e40d8b3bd5"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "LegacyStart"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyEmpty x Retire: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-926739ffb27d20e7"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "Retire"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyClosed x LegacyStart: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-9c61af3593764622"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyClosed+ , requirementCommandName = "LegacyStart"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyEmpty x Decide: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-ba7053f86d15e1b0"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyEmpty+ , requirementCommandName = "Decide"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyClosed x Start: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-be8b08a049ab4d8b"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyClosed+ , requirementCommandName = "Start"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ }+ , -- JourneyEmpty x Start: live transition (spec line 38)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-d2fc3eca5c449e84"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = JourneyEmpty+ , requirementCommandName = "Start"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyEmpty 0))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = ["Started"]+ , requirementLine = 38+ }+ , -- JourneyActive x Decide: live transition (spec line 56)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-db1a553baa3eda84"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = JourneyActive+ , requirementCommandName = "Decide"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyActive 1))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = ["DecisionRecorded"]+ , requirementLine = 56+ }+ , -- JourneyActive x Ping: live transition (spec line 67)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-ea258e9c47d66aac"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = JourneyActive+ , requirementCommandName = "Ping"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyActive 2))+ , requirementTarget = Just JourneyActive+ , requirementEventKinds = []+ , requirementLine = 67+ }+ , -- JourneyActive x Retire: replay-only transition (spec line 77)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-f0fbe3a3ba0b40e8"+ , requirementKind = ReplayTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyActive+ , requirementCommandName = "Retire"+ , requirementExpectedEdge = (Just (K.EdgeRef JourneyActive 4))+ , requirementTarget = Just JourneyClosed+ , requirementEventKinds = ["Retired", "RetirementAudited"]+ , requirementLine = 77+ }+ , -- JourneyClosed x Retire: required rejection (spec line 21)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-f9cae2bf4c0d0562"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = JourneyClosed+ , requirementCommandName = "Retire"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 21+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -147,7 +397,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -174,7 +424,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -192,6 +442,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (JourneyVertex, K.RegFile JourneyRegs) -> JourneyCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -201,6 +452,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (JourneyVertex, K.RegFile JourneyRegs) -> JourneyCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither journeyTransducer seed command of@@ -216,8 +468,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither journeyTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -226,10 +478,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess JourneyRegs JourneyVertex JourneyEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [JourneyEvent] -> [JourneyEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -241,19 +494,21 @@ replayed <- case K.applyEventsDetailedEither journeyTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution JourneyVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [JourneyEvent] -> Either BehaviorFailure (K.ReplaySuccess JourneyRegs JourneyVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither journeyTransducer (JourneyEmpty, initialJourneyRegs) decoded of@@ -263,33 +518,56 @@ decodeEvents :: [JourneyEvent] -> Either Text [JourneyEvent] decodeEvents = traverse (\event -> parseJourneyEvent (Codec.eventType journeyCodec event) (encodeJourneyEvent event)) +commandKind :: JourneyCommand -> Text commandKind command = case command of Start _ -> "Start" Decide _ -> "Decide" Ping _ -> "Ping" Retire _ -> "Retire"+ LegacyStart _ -> "LegacyStart" +eventKind :: JourneyEvent -> Text eventKind event = case Codec.eventType journeyCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile JourneyRegs -> K.RegFile JourneyRegs -> Bool regsEqual left right = (left K.! #lastAmount) == (right K.! #lastAmount) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.Codec ( journeyCodec, parseJourneyEvent,@@ -9,17 +9,15 @@ ) where import Generated.BehaviorComplete.Journey.Domain-import Generated.BehaviorComplete.Nominals (RequestId, requestIdText)+import Generated.BehaviorComplete.Nominals (requestIdText) import Generated.BehaviorComplete.Nominals.Internal (unsafeRequestIdFromLegacyText) import Control.Monad (unless)-import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, withText, (.:), (.=))+import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, (.:), (.=)) import Data.Aeson.Key qualified as Key import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (Parser, explicitParseField, parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty-import Data.Map.Strict (Map)-import Data.Map.Strict qualified as Map import Data.Text (Text) import qualified Data.Text as T import Keiro.Codec.Structural (bindingFromShape, bindingToShape)@@ -53,10 +51,10 @@ rejectUnknownFields "StartPayload" ["display_label", "optional_note"] objectValue ShapeStartPayload.StartPayload <$> explicitParseField (parseJSON) objectValue "display_label"- <*> (case KeyMap.lookup (Key.fromText "optional_note") objectValue of Nothing -> pure Nothing; Just _ -> explicitParseField (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) objectValue "optional_note")+ <*> parseOptionalField (pure Nothing) (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) objectValue "optional_note" journeyEventTypes :: NonEmpty EventType-journeyEventTypes = EventType "Started" :| [EventType "DecisionRecorded", EventType "Retired", EventType "RetirementAudited"]+journeyEventTypes = EventType "Started" :| [EventType "DecisionRecorded", EventType "Retired", EventType "RetirementAudited", EventType "LegacyStarted"] journeyCodec :: Codec JourneyEvent journeyCodec =@@ -67,6 +65,7 @@ DecisionRecorded{} -> EventType "DecisionRecorded" Retired{} -> EventType "Retired" RetirementAudited{} -> EventType "RetirementAudited"+ LegacyStarted{} -> EventType "LegacyStarted" , schemaVersion = 1 , encode = encodeJourneyEvent , decode = parseJourneyEvent@@ -98,6 +97,11 @@ [ "kind" .= ("RetirementAudited" :: Text) , "amount" .= payload.amount ]+ LegacyStarted payload ->+ object+ [ "kind" .= ("LegacyStarted" :: Text)+ , "amount" .= payload.amount+ ] parseJourneyEvent :: EventType -> Value -> Either Text JourneyEvent parseJourneyEvent (EventType tag) = mapLeftText . parseEither (withObject "JourneyEvent" go)@@ -127,17 +131,28 @@ <$> ( RetirementAuditedData <$> o .: "amount" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes journeyEventTypes)+ "LegacyStarted" ->+ LegacyStarted+ <$> ( LegacyStartedData+ <$> o .: "amount"+ )+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes journeyEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName) . NonEmpty.toList++parseOptionalField :: Parser fieldValue -> (Value -> Parser fieldValue) -> KeyMap.KeyMap Value -> Key.Key -> Parser fieldValue+parseOptionalField onMissing parseItem objectValue key =+ case KeyMap.lookup key objectValue of+ Nothing -> onMissing+ Just _ -> explicitParseField parseItem objectValue key rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser () rejectUnknownFields label allowed objectValue =
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Domain.hs view
@@ -1,18 +1,16 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.Domain where import Data.Aeson (FromJSON, ToJSON) import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)-import Generated.BehaviorComplete.Nominals (RequestId, parseRequestId)-import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Generated.BehaviorComplete.Nominals (RequestId)+import Data.Time.Clock (UTCTime) import Numeric.Natural (Natural) import BehaviorComplete.Domain (StartPayload) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll)@@ -46,10 +44,16 @@ } deriving stock (Generic, Eq, Show) +data LegacyStartData = LegacyStartData+ { amount :: !Natural+ }+ deriving stock (Generic, Eq, Show)+ data JourneyCommand = Start !StartData | Decide !DecideData | Ping !PingData | Retire !RetireData+ | LegacyStart !LegacyStartData deriving stock (Generic, Eq, Show) data StartedData = StartedData@@ -75,10 +79,16 @@ } deriving stock (Generic, Eq, Show) +data LegacyStartedData = LegacyStartedData+ { amount :: !Natural+ }+ deriving stock (Generic, Eq, Show)+ data JourneyEvent = Started !StartedData | DecisionRecorded !DecisionRecordedData | Retired !RetiredData | RetirementAudited !RetirementAuditedData+ | LegacyStarted !LegacyStartedData deriving stock (Generic, Eq, Show) type JourneyRegs =
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.EventStream ( journeyCategory , journeyCommandCategory
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.Harness (harnessAssertions) where import Generated.BehaviorComplete.Journey.Domain@@ -21,7 +21,7 @@ import Keiro.Codec.Structural (FixtureCases (..), bindingDomainRoundTrip, bindingShapeRoundTrip, bindingToShape) import Generated.BehaviorComplete.StructuralProjections qualified as StructuralProjections import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime) import BehaviorComplete.Bindings qualified as Bindings import BehaviorComplete.Domain (StartPayload) import Generated.BehaviorComplete.Structural.Shape.StartPayload qualified as ShapeStartPayload@@ -32,11 +32,12 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions journeyTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: Started", roundTrips sampleEventStarted) , ("golden round-trip: DecisionRecorded", roundTrips sampleEventDecisionRecorded) , ("golden round-trip: Retired", roundTrips sampleEventRetired) , ("golden round-trip: RetirementAudited", roundTrips sampleEventRetirementAudited)+ , ("golden round-trip: LegacyStarted", roundTrips sampleEventLegacyStarted) , ("accepts Start from JourneyEmpty", acceptStart) ] ++ mappedConformanceAssertions@@ -45,21 +46,33 @@ roundTrips :: JourneyEvent -> Bool roundTrips e = parseJourneyEvent (eventType journeyCodec e) (encodeJourneyEvent e) == Right e +sampleRequestId :: RequestId+sampleRequestId =+ case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleObservedAt :: UTCTime+sampleObservedAt = UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)+ sampleEventStarted :: JourneyEvent-sampleEventStarted = (Started (StartedData (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases)))))+sampleEventStarted = Started (StartedData sampleRequestId sampleObservedAt 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases)))) sampleEventDecisionRecorded :: JourneyEvent-sampleEventDecisionRecorded = (DecisionRecorded (DecisionRecordedData 0))+sampleEventDecisionRecorded = DecisionRecorded (DecisionRecordedData 0) sampleEventRetired :: JourneyEvent-sampleEventRetired = (Retired (RetiredData 0))+sampleEventRetired = Retired (RetiredData 0) sampleEventRetirementAudited :: JourneyEvent-sampleEventRetirementAudited = (RetirementAudited (RetirementAuditedData 0))+sampleEventRetirementAudited = RetirementAudited (RetirementAuditedData 0) +sampleEventLegacyStarted :: JourneyEvent+sampleEventLegacyStarted = LegacyStarted (LegacyStartedData 0)+ acceptStart :: Bool acceptStart =- case step journeyTransducer (JourneyEmpty, initialJourneyRegs) ((Start (StartData (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases)))))) of+ case step journeyTransducer (JourneyEmpty, initialJourneyRegs) (Start (StartData sampleRequestId sampleObservedAt 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases))))) of Just (v, _, _) -> v == JourneyActive Nothing -> False @@ -67,7 +80,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayStart :: [(String, Bool)] forwardReplayStart =- case step journeyTransducer (JourneyEmpty, initialJourneyRegs) ((Start (StartData (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases)))))) of+ case step journeyTransducer (JourneyEmpty, initialJourneyRegs) (Start (StartData sampleRequestId sampleObservedAt 0 (snd (NonEmpty.head (fixtureCases Bindings.startPayloadCases))))) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseJourneyEvent (eventType journeyCodec event) (encodeJourneyEvent event)) emitted of@@ -118,7 +131,7 @@ startedDetailsAssertions :: [(String, Bool)] startedDetailsAssertions =- [ ("mapped codec round-trip: Started/details/" <> T.unpack label, roundTrips (Started (StartedData (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) 0 mappedValue)))+ [ ("mapped codec round-trip: Started/details/" <> T.unpack label, roundTrips (Started (StartedData sampleRequestId sampleObservedAt 0 mappedValue))) | (label, mappedValue) <- NonEmpty.toList (fixtureCases Bindings.startPayloadCases) ]
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-behavior-complete/Generated/BehaviorComplete/Journey/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Journey; do not edit. module Generated.BehaviorComplete.Journey.Transducer ( journeyTransducer , journeyFoldFingerprint@@ -29,6 +29,8 @@ B.buildTransducer JourneyEmpty initialJourneyRegs isTerminal do B.from JourneyEmpty do B.onCmd inCtorStart $ \d -> B.do+ B.requireGuard $+ d.amount .<= K.lit (0 :: Natural) B.slot @"lastAmount" =: d.amount B.emit wireStarted (StartedTermFields { requestId = d.requestId@@ -37,6 +39,25 @@ , details = d.details }) B.goto JourneyActive+ B.onCmd inCtorStart $ \d -> B.do+ B.replayOnly+ B.requireGuard $+ d.amount .> K.lit (0 :: Natural)+ B.slot @"lastAmount" =: d.amount+ B.emit wireStarted (StartedTermFields+ { requestId = d.requestId+ , observedAt = d.observedAt+ , amount = d.amount+ , details = d.details+ })+ B.goto JourneyActive+ B.onCmd inCtorLegacyStart $ \d -> B.do+ B.replayOnly+ B.slot @"lastAmount" =: d.amount+ B.emit wireLegacyStarted (LegacyStartedTermFields+ { amount = d.amount+ })+ B.goto JourneyActive B.from JourneyActive do B.onCmd inCtorDecide $ \d -> B.do B.requireGuard $@@ -86,7 +107,7 @@ _ -> False journeyFoldFingerprint :: Text-journeyFoldFingerprint = "6a9b89765aef0e4cd37ce6a7ea34514a"+journeyFoldFingerprint = "e85712b75ccd98ea3eebdf59164c4993" data BehaviorOwnership = GeneratedOwned | HoleOwned deriving stock (Eq, Show)@@ -96,11 +117,13 @@ journeyPredicateVerifications :: IO [(Text, BehaviorOwnership, S.PredicateVerification)] journeyPredicateVerifications = sequence [ verifyTransition "transition1EmptyStart" GeneratedOwned JourneyEmpty 0+ , verifyTransition "transition3EmptyStart" GeneratedOwned JourneyEmpty 1+ , verifyTransition "transition5EmptyLegacyStart" GeneratedOwned JourneyEmpty 2 , verifyTransition "transition2ActiveDecide" GeneratedOwned JourneyActive 0- , verifyTransition "transition3ActiveDecide" GeneratedOwned JourneyActive 1- , verifyTransition "transition4ActivePing" GeneratedOwned JourneyActive 2- , verifyTransition "transition5ActiveRetire" GeneratedOwned JourneyActive 3- , verifyTransition "transition6ActiveRetire" GeneratedOwned JourneyActive 4+ , verifyTransition "transition4ActiveDecide" GeneratedOwned JourneyActive 1+ , verifyTransition "transition6ActivePing" GeneratedOwned JourneyActive 2+ , verifyTransition "transition7ActiveRetire" GeneratedOwned JourneyActive 3+ , verifyTransition "transition8ActiveRetire" GeneratedOwned JourneyActive 4 ] where verifyTransition label owner source edgeIndex =
test/conformance-behavior-complete/Generated/BehaviorComplete/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context behavior-complete generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context behavior-complete generated nominal declarations; do not edit. module Generated.BehaviorComplete.Nominals ( RequestId , parseRequestId@@ -6,11 +6,4 @@ , requestIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import Generated.BehaviorComplete.Nominals.Internal (RequestId, mkRequestId, parseRequestId, requestIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName RequestId
test/conformance-behavior-complete/Generated/BehaviorComplete/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context behavior-complete generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context behavior-complete generated nominal ID internals; do not edit. module Generated.BehaviorComplete.Nominals.Internal ( RequestId , parseRequestId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype RequestId = RequestId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName RequestId instance ToJSON RequestId where toJSON = toJSON . requestIdText
test/conformance-behavior-complete/Generated/BehaviorComplete/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context behavior-complete replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context behavior-complete replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-behavior-complete/Generated/BehaviorComplete/Structural/Shape/StartPayload.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural StartPayload; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural StartPayload; do not edit. module Generated.BehaviorComplete.Structural.Shape.StartPayload (StartPayloadShape (..)) where import Data.Text (Text)
test/conformance-behavior-complete/Generated/BehaviorComplete/StructuralProjections.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context behavior-complete mapped structural facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context behavior-complete mapped structural facade; do not edit. -- Equality witnesses are emitted for Text, Int, Bool, Natural, and UTCTime. -- Int, Natural, and UTCTime belong to Keiki's ordered subset. module Generated.BehaviorComplete.StructuralProjections@@ -7,8 +7,6 @@ ) where import Data.Text (Text)-import Data.Time (UTCTime)-import Numeric.Natural (Natural) import Keiro.Codec.Structural (bindingToShape) import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness) import BehaviorComplete.Bindings qualified as Bindings
test/conformance-coldstart/Generated/Billing/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context billing generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context billing generated nominal declarations; do not edit. module Generated.Billing.Nominals ( CustomerId , parseCustomerId@@ -21,13 +21,12 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.Billing.Nominals.Internal (CustomerId, mkCustomerId, parseCustomerId, customerIdText, SubscriptionId, mkSubscriptionId, parseSubscriptionId, subscriptionIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)+import Keiki.ProjectionDomain (finiteProjectionDomain) -instance CanonicalTypeName CustomerId + data Plan = Paid | Free deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -57,5 +56,3 @@ planEqualityWitness :: FieldWitness PlanEqualityProjection planEqualityWitness = exactFieldWitness @PlanEqualityProjection--instance CanonicalTypeName SubscriptionId
test/conformance-coldstart/Generated/Billing/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context billing generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context billing generated nominal ID internals; do not edit. module Generated.Billing.Nominals.Internal ( CustomerId , parseCustomerId@@ -16,11 +16,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CustomerId = CustomerId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CustomerId+ instance ToJSON CustomerId where toJSON = toJSON . customerIdText @@ -43,6 +46,8 @@ newtype SubscriptionId = SubscriptionId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName SubscriptionId instance ToJSON SubscriptionId where toJSON = toJSON . subscriptionIdText
test/conformance-coldstart/Generated/Billing/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context billing replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context billing replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-coldstart/Generated/Billing/Subscription/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.Codec ( subscriptionCodec, parseSubscriptionEvent,@@ -7,7 +7,7 @@ ) where import Generated.Billing.Subscription.Domain-import Generated.Billing.Nominals (CustomerId, customerIdText, Plan (..), planText, SubscriptionId, subscriptionIdText)+import Generated.Billing.Nominals (customerIdText, Plan (..), planText, subscriptionIdText) import Generated.Billing.Nominals.Internal (unsafeCustomerIdFromLegacyText, unsafeSubscriptionIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -75,13 +75,13 @@ <$> (unsafeSubscriptionIdFromLegacyText <$> o .: "subscriptionId") <*> (unsafeCustomerIdFromLegacyText <$> o .: "customerId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes subscriptionEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes subscriptionEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-coldstart/Generated/Billing/Subscription/Domain.hs view
@@ -1,13 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.Billing.Nominals (CustomerId, parseCustomerId, Plan (..), SubscriptionId, parseSubscriptionId)+import Generated.Billing.Nominals (CustomerId, Plan (..), SubscriptionId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data SubscriptionVertex = SubscriptionInactive | SubscriptionActive | SubscriptionClosed
test/conformance-coldstart/Generated/Billing/Subscription/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.EventStream ( subscriptionCategory , subscriptionCommandCategory@@ -10,7 +10,7 @@ import Generated.Billing.Subscription.Domain import Generated.Billing.Subscription.Codec (subscriptionCodec)-import Generated.Billing.Subscription.Transducer (subscriptionFoldFingerprint, subscriptionTransducer)+import Generated.Billing.Subscription.Transducer (subscriptionTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-coldstart/Generated/Billing/Subscription/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.Harness (harnessAssertions) where import Generated.Billing.Subscription.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions subscriptionTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: SubscriptionActivated", roundTrips sampleEventSubscriptionActivated) , ("golden round-trip: SubscriptionCancelled", roundTrips sampleEventSubscriptionCancelled) , ("accepts ActivateSubscription from SubscriptionInactive", acceptActivateSubscription)@@ -25,15 +25,27 @@ roundTrips :: SubscriptionEvent -> Bool roundTrips e = parseSubscriptionEvent (eventType subscriptionCodec e) (encodeSubscriptionEvent e) == Right e +sampleCustomerId :: CustomerId+sampleCustomerId =+ case parseCustomerId "cust_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleSubscriptionId :: SubscriptionId+sampleSubscriptionId =+ case parseSubscriptionId "sub_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventSubscriptionActivated :: SubscriptionEvent-sampleEventSubscriptionActivated = (SubscriptionActivated (SubscriptionActivatedData (case parseSubscriptionId "sub_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCustomerId "cust_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Paid))+sampleEventSubscriptionActivated = SubscriptionActivated (SubscriptionActivatedData sampleSubscriptionId sampleCustomerId Paid) sampleEventSubscriptionCancelled :: SubscriptionEvent-sampleEventSubscriptionCancelled = (SubscriptionCancelled (SubscriptionCancelledData (case parseSubscriptionId "sub_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCustomerId "cust_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))+sampleEventSubscriptionCancelled = SubscriptionCancelled (SubscriptionCancelledData sampleSubscriptionId sampleCustomerId) acceptActivateSubscription :: Bool acceptActivateSubscription =- case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) ((ActivateSubscription (ActivateSubscriptionData (case parseSubscriptionId "sub_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCustomerId "cust_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Paid))) of+ case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) (ActivateSubscription (ActivateSubscriptionData sampleSubscriptionId sampleCustomerId Paid)) of Just (v, _, _) -> v == SubscriptionActive Nothing -> False @@ -41,7 +53,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayActivateSubscription :: [(String, Bool)] forwardReplayActivateSubscription =- case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) ((ActivateSubscription (ActivateSubscriptionData (case parseSubscriptionId "sub_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCustomerId "cust_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Paid))) of+ case step subscriptionTransducer (SubscriptionInactive, initialSubscriptionRegs) (ActivateSubscription (ActivateSubscriptionData sampleSubscriptionId sampleCustomerId Paid)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseSubscriptionEvent (eventType subscriptionCodec event) (encodeSubscriptionEvent event)) emitted of
test/conformance-coldstart/Generated/Billing/Subscription/Projection.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.Projection ( subscriptionsProjection , subscriptionsStatusFor
test/conformance-coldstart/Generated/Billing/Subscription/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Subscription; do not edit. module Generated.Billing.Subscription.Transducer ( subscriptionTransducer , subscriptionFoldFingerprint
test/conformance-contract-v1-compat/Generated/HospitalCapacity/Emergency/Contract.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 1) from contract emergency; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 1) from contract emergency; do not edit. module Generated.HospitalCapacity.Emergency.Contract ( EmergencyPayload (..) , IncidentTransferNeedDeclaredData (..)
test/conformance-contract/Generated/HospitalCapacity/Emergency/Contract.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from contract emergency; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from contract emergency; do not edit. module Generated.HospitalCapacity.Emergency.Contract ( EmergencyPayload (..) , IncidentTransferNeedDeclaredData (..)@@ -28,7 +28,7 @@ hospitalEventsTopic = "emergency.hospital.events" -- the closed payload set (discriminated by "messageType")-data IncidentTransferNeedDeclaredData = IncidentTransferNeedDeclaredData {incidentId :: !(KindID "inc"), triageRecordId :: !Text, region :: !Text, redCount :: !Int}+data IncidentTransferNeedDeclaredData = IncidentTransferNeedDeclaredData {incidentId :: !(KindID "inc"), triageRecordId :: !Text, family :: !Text, payloadType :: !Text, serviceRegion :: !Text, redCount :: !Int} deriving stock (Eq, Show) data TransferReservationAcceptedData = TransferReservationAcceptedData {incidentId :: !(KindID "inc"), reservationId :: !(KindID "rsv"), hospitalId :: !(KindID "hsp"), expirationDeadline :: !Text}@@ -51,7 +51,9 @@ [ "messageType" .= ("IncidentTransferNeedDeclared" :: Text), "incidentId" .= KindID.toText payload.incidentId, "triageRecordId" .= payload.triageRecordId,- "region" .= payload.region,+ "family" .= payload.family,+ "type" .= payload.payloadType,+ "region_code" .= payload.serviceRegion, "redCount" .= payload.redCount ] TransferReservationAccepted payload ->@@ -74,7 +76,9 @@ <$> ( IncidentTransferNeedDeclaredData <$> explicitParseField (parseKindIdV7Value @"inc") o "incidentId" <*> o .: "triageRecordId"- <*> o .: "region"+ <*> o .: "family"+ <*> o .: "type"+ <*> o .: "region_code" <*> o .: "redCount" ) "TransferReservationAccepted" ->
test/conformance-contract/Main.hs view
@@ -31,7 +31,7 @@ incidentPayload :: EmergencyPayload incidentPayload = IncidentTransferNeedDeclared- (IncidentTransferNeedDeclaredData incidentIdValue "tri-1" "north" 3)+ (IncidentTransferNeedDeclaredData incidentIdValue "tri-1" "adoption" "payload-type" "north" 3) reservationPayload :: EmergencyPayload reservationPayload =@@ -44,7 +44,9 @@ [ "messageType" .= ("IncidentTransferNeedDeclared" :: Text), "incidentId" .= rawIncidentId, "triageRecordId" .= ("tri-1" :: Text),- "region" .= ("north" :: Text),+ "family" .= ("adoption" :: Text),+ "type" .= ("payload-type" :: Text),+ "region_code" .= ("north" :: Text), "redCount" .= (3 :: Int) ] @@ -67,6 +69,14 @@ ( "IncidentTransferNeedDeclared round-trip", encodeEmergencyPayload incidentPayload == validIncidentJson && parseEmergencyPayload validIncidentJson == Right incidentPayload+ ),+ ( "contract selectors are distinct from wire keys",+ case incidentPayload of+ IncidentTransferNeedDeclared payload ->+ family payload == "adoption"+ && payloadType payload == "payload-type"+ && serviceRegion payload == "north"+ _ -> False ), ( "TransferReservationAccepted round-trip", encodeEmergencyPayload reservationPayload == validReservationJson
+ test/conformance-dispatch-full/Generated/HospitalCapacity/ReservationWork/Queue.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+module Generated.HospitalCapacity.ReservationWork.Queue+ ( ReservationWorkItem (..)+ , encodeReservationWorkItem+ , parseReservationWorkItem+ , queuePhysical, queueDlq, queueTable+ , groupKeyField, groupKeyFor+ ) 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 = "hospital_capacity_reservation_work"+queueDlq = "hospital_capacity_reservation_work_dlq"+queueTable = "pgmq.q_hospital_capacity_reservation_work"++groupKeyField :: Text+groupKeyField = "reservationId"++groupKeyFor :: ReservationWorkItem -> Text+groupKeyFor payload = payload.reservationId++data ReservationWorkItem = ReservationWorkItem+ { reservationId :: !Text+ , hospitalId :: !Text+ , commandId :: !Text+ , lifeCriticalOverride :: !Bool+ }+ deriving stock (Eq, Show)++encodeReservationWorkItem :: ReservationWorkItem -> Value+encodeReservationWorkItem p =+ object+ [ "reservation_id" .= p.reservationId+ , "hospital_id" .= p.hospitalId+ , "command_id" .= p.commandId+ , "life_critical_override" .= p.lifeCriticalOverride+ ]++parseReservationWorkItem :: Value -> Either Text ReservationWorkItem+parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)+ where+ go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right
+ test/conformance-dispatch-full/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs view
@@ -0,0 +1,29 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+-- | 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.ReservationWork.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)+import Generated.HospitalCapacity.ReservationWork.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-dispatch-full/Generated/HospitalCapacity/ReservationWork/QueuePolicy.hs view
@@ -0,0 +1,43 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+module Generated.HospitalCapacity.ReservationWork.QueuePolicy+ ( ReservationWorkOutcome (..)+ , retryPolicy, jobOutcomeFor+ , jobOrdering, jobTuningFor, queueProvision+ ) where++import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, QueueProvision, RetryDelay (..), RetryPolicy (..), standardProvision, withFifoIndexProvision, withOrdering)++jobOrdering :: JobOrdering+jobOrdering = FifoThroughput++-- 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 = withFifoIndexProvision (standardProvision)++retryPolicy :: RetryPolicy+retryPolicy =+ RetryPolicy+ { maxRetries = 3+ , defaultRetryDelay = RetryDelay 5+ , useDeadLetter = True+ }++-- The consumer JobOutcome disposition over the spec's named domain outcomes,+-- lowered to the live Keiro.PGMQ.Job.JobOutcome.+data ReservationWorkOutcome+ = StoreFailure+ | CommandRejected+ | DecodeFailure+ | OnCodecReject+ deriving stock (Eq, Show)++jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome+jobOutcomeFor o = case o of+ StoreFailure -> Retry (RetryDelay 5)+ CommandRejected -> Dead "dead-lettered"+ DecodeFailure -> Dead "dead-lettered"+ OnCodecReject -> Dead "dead-lettered"
− test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/Queue.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module Generated.HospitalCapacity.Reservation_work.Queue- ( ReservationWorkItem (..)- , encodeReservationWorkItem- , parseReservationWorkItem- , queuePhysical, queueDlq, queueTable- , groupKeyField, groupKeyFor- ) 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 = "hospital_capacity_reservation_work"-queueDlq = "hospital_capacity_reservation_work_dlq"-queueTable = "pgmq.q_hospital_capacity_reservation_work"--groupKeyField :: Text-groupKeyField = "reservationId"--groupKeyFor :: ReservationWorkItem -> Text-groupKeyFor payload = payload.reservationId--data ReservationWorkItem = ReservationWorkItem- { reservationId :: !Text- , hospitalId :: !Text- , commandId :: !Text- , lifeCriticalOverride :: !Bool- }- deriving stock (Eq, Show)--encodeReservationWorkItem :: ReservationWorkItem -> Value-encodeReservationWorkItem p =- object- [ "reservation_id" .= p.reservationId- , "hospital_id" .= p.hospitalId- , "command_id" .= p.commandId- , "life_critical_override" .= p.lifeCriticalOverride- ]--parseReservationWorkItem :: Value -> Either Text ReservationWorkItem-parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)- where- go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"--mapLeftText :: Either String b -> Either Text b-mapLeftText = either (Left . T.pack) Right
− test/conformance-dispatch-full/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs
@@ -1,29 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.--- | 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 Keiro.Codec (Codec (..), EventType (..))-import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)-import Generated.HospitalCapacity.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-dispatch-full/Generated/HospitalCapacity/Reservation_work/QueuePolicy.hs
@@ -1,43 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module Generated.HospitalCapacity.Reservation_work.QueuePolicy- ( ReservationWorkOutcome (..)- , retryPolicy, jobOutcomeFor- , jobOrdering, jobTuningFor, queueProvision- ) where--import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)--jobOrdering :: JobOrdering-jobOrdering = FifoThroughput---- 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 = withFifoIndexProvision (standardProvision)--retryPolicy :: RetryPolicy-retryPolicy =- RetryPolicy- { maxRetries = 3- , defaultRetryDelay = RetryDelay 5- , useDeadLetter = True- }---- The consumer JobOutcome disposition over the spec's named domain outcomes,--- lowered to the live Keiro.PGMQ.Job.JobOutcome.-data ReservationWorkOutcome- = StoreFailure- | CommandRejected- | DecodeFailure- | OnCodecReject- deriving stock (Eq, Show)--jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome-jobOutcomeFor o = case o of- StoreFailure -> Retry (RetryDelay 5)- CommandRejected -> Dead "dead-lettered"- DecodeFailure -> Dead "dead-lettered"- OnCodecReject -> Dead "dead-lettered"
test/conformance-dispatch-full/HospitalCapacity/ReservationWork/WorkqueueJob.hs view
@@ -9,11 +9,11 @@ ) where import Effectful (Eff)-import Generated.HospitalCapacity.Reservation_work.Queue (+import Generated.HospitalCapacity.ReservationWork.Queue ( ReservationWorkItem, )-import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec)-import Generated.HospitalCapacity.Reservation_work.QueuePolicy (retryPolicy)+import Generated.HospitalCapacity.ReservationWork.QueueCodec (reservationWorkJobCodec)+import Generated.HospitalCapacity.ReservationWork.QueuePolicy (retryPolicy) import Keiro.PGMQ.Job (Job (..), JobOutcome (..)) import Keiro.PGMQ.Runtime (queueRef)
test/conformance-id-domain-migration/Generated/IdDomainMigration/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 3) from context id-domain-migration generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 3) from context id-domain-migration generated nominal declarations; do not edit. module Generated.IdDomainMigration.Nominals ( OrderId , parseOrderId@@ -6,11 +6,4 @@ , orderIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import Generated.IdDomainMigration.Nominals.Internal (OrderId, mkOrderId, parseOrderId, orderIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName OrderId
test/conformance-id-domain-migration/Generated/IdDomainMigration/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 3) from context id-domain-migration generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 3) from context id-domain-migration generated nominal ID internals; do not edit. module Generated.IdDomainMigration.Nominals.Internal ( OrderId , parseOrderId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype OrderId = OrderId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName OrderId instance ToJSON OrderId where toJSON = toJSON . orderIdText
test/conformance-id-domain-migration/Generated/IdDomainMigration/OrderBook/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 3) from aggregate OrderBook; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 3) from aggregate OrderBook; do not edit. module Generated.IdDomainMigration.OrderBook.Codec ( orderBookCodec, parseOrderBookEvent,@@ -7,10 +7,10 @@ ) where import Generated.IdDomainMigration.OrderBook.Domain-import Generated.IdDomainMigration.Nominals (OrderId, orderIdText)+import Generated.IdDomainMigration.Nominals (orderIdText) import Generated.IdDomainMigration.Nominals.Internal (unsafeOrderIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> ( OrderRecordedData <$> (unsafeOrderIdFromLegacyText <$> o .: "orderId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes orderBookEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes orderBookEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-id-domain-migration/Generated/IdDomainMigration/OrderBook/Domain.hs view
@@ -1,12 +1,11 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 3) from aggregate OrderBook; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 3) from aggregate OrderBook; do not edit. module Generated.IdDomainMigration.OrderBook.Domain where import Data.Aeson (FromJSON, ToJSON) import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/BehaviorContract.hs view
@@ -1,6 +1,21 @@-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.-module Generated.ImportPlanningCollisions.CollisionLedger.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+module Generated.ImportPlanningCollisions.CollisionLedger.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.ImportPlanningCollisions.CollisionLedger.Codec (encodeCollisionLedgerEvent, parseCollisionLedgerEvent, collisionLedgerCodec) import Generated.ImportPlanningCollisions.CollisionLedger.Domain@@ -12,7 +27,7 @@ import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T-import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither, (!))+import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither) import Keiro.Codec qualified as Codec (Codec (eventType), EventType (..)) newtype BehaviorKey = BehaviorKey { unBehaviorKey :: Text }@@ -67,16 +82,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -108,8 +125,32 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-2134fce4a19c59d7") LiveTransition GeneratedAuthoritative GuardTotal CollisionLedgerEmpty "Record" (Just (K.EdgeRef CollisionLedgerEmpty 0)) (Just CollisionLedgerRecorded) ["RecordedValues"] 49- , BehaviorRequirement (BehaviorKey "behavior-v1-995f9bf710ce7c6c") RequiredRejection GeneratedAuthoritative GuardNotApplicable CollisionLedgerRecorded "Record" Nothing Nothing [] 41+ [ -- CollisionLedgerEmpty x Record: live transition (spec line 49)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-2134fce4a19c59d7"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = CollisionLedgerEmpty+ , requirementCommandName = "Record"+ , requirementExpectedEdge = (Just (K.EdgeRef CollisionLedgerEmpty 0))+ , requirementTarget = Just CollisionLedgerRecorded+ , requirementEventKinds = ["RecordedValues"]+ , requirementLine = 49+ }+ , -- CollisionLedgerRecorded x Record: required rejection (spec line 41)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-995f9bf710ce7c6c"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = CollisionLedgerRecorded+ , requirementCommandName = "Record"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 41+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -134,7 +175,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -161,7 +202,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -179,6 +220,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (CollisionLedgerVertex, K.RegFile CollisionLedgerRegs) -> CollisionLedgerCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -188,6 +230,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (CollisionLedgerVertex, K.RegFile CollisionLedgerRegs) -> CollisionLedgerCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither collisionLedgerTransducer seed command of@@ -203,8 +246,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither collisionLedgerTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -213,10 +256,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess CollisionLedgerRegs CollisionLedgerVertex CollisionLedgerEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [CollisionLedgerEvent] -> [CollisionLedgerEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -228,19 +272,21 @@ replayed <- case K.applyEventsDetailedEither collisionLedgerTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution CollisionLedgerVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [CollisionLedgerEvent] -> Either BehaviorFailure (K.ReplaySuccess CollisionLedgerRegs CollisionLedgerVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither collisionLedgerTransducer (CollisionLedgerEmpty, initialCollisionLedgerRegs) decoded of@@ -250,30 +296,52 @@ decodeEvents :: [CollisionLedgerEvent] -> Either Text [CollisionLedgerEvent] decodeEvents = traverse (\event -> parseCollisionLedgerEvent (Codec.eventType collisionLedgerCodec event) (encodeCollisionLedgerEvent event)) +commandKind :: CollisionLedgerCommand -> Text commandKind command = case command of Record _ -> "Record" +eventKind :: CollisionLedgerEvent -> Text eventKind event = case Codec.eventType collisionLedgerCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile CollisionLedgerRegs -> K.RegFile CollisionLedgerRegs -> Bool regsEqual _ _ = True +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.Codec ( collisionLedgerCodec, parseCollisionLedgerEvent,@@ -10,14 +10,12 @@ import Generated.ImportPlanningCollisions.CollisionLedger.Domain import Control.Monad (unless)-import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, withText, (.:), (.=))+import Data.Aeson (Value, object, parseJSON, toJSON, withObject, (.:), (.=)) import Data.Aeson.Key qualified as Key import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (Parser, explicitParseField, parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty-import Data.Map.Strict (Map)-import Data.Map.Strict qualified as Map import Data.Text (Text) import qualified Data.Text as T import Keiro.Codec.Nominal (nominalFromRepresentation, nominalToRepresentation)@@ -28,9 +26,6 @@ import Generated.ImportPlanningCollisions.Structural.Shape.Details qualified as ShapeDetails import ImportPlanning.Bindings qualified as Bindings-import ImportPlanning.Consumer.Domain (CollisionLedgerCommand)-import ImportPlanning.Consumer.Invoice.Types qualified as InvoiceTypes-import ImportPlanning.Consumer.Order.Types qualified as OrderTypes import ImportPlanning.Consumer.Shared.Types (Details) @@ -99,13 +94,13 @@ <*> (nominalFromRepresentation Bindings.localCollisionBinding <$> o .: "localCollision") <*> explicitParseField parseDetailsMapped o "details" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes collisionLedgerEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes collisionLedgerEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Domain.hs view
@@ -1,10 +1,8 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.Domain where -import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import ImportPlanning.Consumer.Domain qualified as Domain
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.EventStream ( collisionLedgerCategory , collisionLedgerCommandCategory@@ -10,7 +10,7 @@ import Generated.ImportPlanningCollisions.CollisionLedger.Domain import Generated.ImportPlanningCollisions.CollisionLedger.Codec (collisionLedgerCodec)-import Generated.ImportPlanningCollisions.CollisionLedger.Transducer (collisionLedgerFoldFingerprint, collisionLedgerTransducer)+import Generated.ImportPlanningCollisions.CollisionLedger.Transducer (collisionLedgerTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Harness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.Harness (harnessAssertions) where import Generated.ImportPlanningCollisions.CollisionLedger.Domain@@ -9,9 +9,8 @@ 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.Either (isLeft) import Data.List (nub)-import Data.Maybe (isJust, isNothing) import Data.Proxy (Proxy (..)) import Data.Text qualified as T import Keiki.Shape (CanonicalTypeName (..))@@ -22,9 +21,6 @@ import Generated.ImportPlanningCollisions.NominalProjections qualified as NominalProjections import Generated.ImportPlanningCollisions.Structural.Shape.Details qualified as ShapeDetails import ImportPlanning.Bindings qualified as Bindings-import ImportPlanning.Consumer.Domain qualified as Domain-import ImportPlanning.Consumer.Invoice.Types qualified as InvoiceTypes-import ImportPlanning.Consumer.Order.Types qualified as OrderTypes import ImportPlanning.Consumer.Shared.Types (Details) -- | (label, passed). A driver runs these and exits non-zero on any False,@@ -33,7 +29,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions collisionLedgerTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: RecordedValues", roundTrips sampleEventRecordedValues) , ("accepts Record from CollisionLedgerEmpty", acceptRecord) ]@@ -45,11 +41,11 @@ roundTrips e = parseCollisionLedgerEvent (eventType collisionLedgerCodec e) (encodeCollisionLedgerEvent e) == Right e sampleEventRecordedValues :: CollisionLedgerEvent-sampleEventRecordedValues = (RecordedValues (RecordedValuesData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures)))))+sampleEventRecordedValues = RecordedValues (RecordedValuesData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures)))) acceptRecord :: Bool acceptRecord =- case step collisionLedgerTransducer (CollisionLedgerEmpty, initialCollisionLedgerRegs) ((Record (RecordData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures)))))) of+ case step collisionLedgerTransducer (CollisionLedgerEmpty, initialCollisionLedgerRegs) (Record (RecordData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures))))) of Just (v, _, _) -> v == CollisionLedgerRecorded Nothing -> False @@ -57,7 +53,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRecord :: [(String, Bool)] forwardReplayRecord =- case step collisionLedgerTransducer (CollisionLedgerEmpty, initialCollisionLedgerRegs) ((Record (RecordData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures)))))) of+ case step collisionLedgerTransducer (CollisionLedgerEmpty, initialCollisionLedgerRegs) (Record (RecordData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.invoiceStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.localCollisionFixtures))) (snd (NonEmpty.head (fixtureCases Bindings.detailsFixtures))))) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, _forwardRegs, emitted) -> case mapM (\event -> parseCollisionLedgerEvent (eventType collisionLedgerCodec event) (encodeCollisionLedgerEvent event)) emitted of@@ -119,17 +115,9 @@ [ ("projection witness agreement: import-planning.Details.v1/label", all (\(_, owner) -> fieldWitnessAgrees StructuralProjections.detailsLabelWitness (\referenceOwner -> ShapeDetails.label (bindingToShape Bindings.detailsBinding referenceOwner)) owner) (NonEmpty.toList (fixtureCases Bindings.detailsFixtures))) ] -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 nominalConformanceAssertions :: [(String, Bool)] nominalConformanceAssertions =
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-import-planning/Generated/ImportPlanningCollisions/CollisionLedger/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate CollisionLedger; do not edit. module Generated.ImportPlanningCollisions.CollisionLedger.Transducer ( collisionLedgerTransducer , collisionLedgerFoldFingerprint
test/conformance-import-planning/Generated/ImportPlanningCollisions/NominalProjections.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context import-planning-collisions nominal scalar projection facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context import-planning-collisions nominal scalar projection facade; do not edit. module Generated.ImportPlanningCollisions.NominalProjections where import Data.Text (Text)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)-import Keiro.Codec.Nominal (nominalFromRepresentation, nominalToRepresentation)+import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)+import Keiro.Codec.Nominal (nominalToRepresentation) import ImportPlanning.Bindings qualified as Bindings import ImportPlanning.Consumer.Domain (CollisionLedgerCommand) import ImportPlanning.Consumer.Invoice.Types qualified as InvoiceTypes
test/conformance-import-planning/Generated/ImportPlanningCollisions/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context import-planning-collisions replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context import-planning-collisions replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-import-planning/Generated/ImportPlanningCollisions/Structural/Shape/Details.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural Details; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural Details; do not edit. module Generated.ImportPlanningCollisions.Structural.Shape.Details (DetailsShape (..)) where import Data.Text (Text)
test/conformance-import-planning/Generated/ImportPlanningCollisions/StructuralProjections.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context import-planning-collisions mapped structural facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context import-planning-collisions mapped structural facade; do not edit. -- Equality witnesses are emitted for Text, Int, Bool, Natural, and UTCTime. -- Int, Natural, and UTCTime belong to Keiki's ordered subset. module Generated.ImportPlanningCollisions.StructuralProjections@@ -7,8 +7,6 @@ ) where import Data.Text (Text)-import Data.Time (UTCTime)-import Numeric.Natural (Natural) import Keiro.Codec.Structural (bindingToShape) import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness) import Generated.ImportPlanningCollisions.Structural.Shape.Details qualified as ShapeDetails
test/conformance-intake-full/Generated/HospitalCapacity/IncidentInbox/Inbox.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from intake incidentInbox; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from intake incidentInbox; do not edit. module Generated.HospitalCapacity.IncidentInbox.Inbox ( InboxFailure (..) , IncidentInboxOutcome (..)
test/conformance-intake-runtime/Generated/HospitalCapacity/IncidentInbox/Inbox.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from intake incidentInbox; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from intake incidentInbox; do not edit. module Generated.HospitalCapacity.IncidentInbox.Inbox ( InboxFailure (..) , IncidentInboxOutcome (..)
test/conformance-newsurface/Generated/TransferRouting/Conformance.hs view
@@ -1,20 +1,20 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context transfer-routing service conformance facade; do not edit.+-- @generated by keiro-dsl 0.10.0.0 (language keiro-dsl 4) from context transfer-routing service conformance facade; do not edit. module Generated.TransferRouting.Conformance ( runServiceConformanceChecks , serviceConformanceFacts ) where -import Generated.TransferRouting.Hospital.Harness qualified as Harness0-import Generated.TransferRouting.Hospital_load.ReadModelHarness qualified as Harness1-import Generated.TransferRouting.HospitalTransferRouter.RouterHarness qualified as Harness2+import Generated.TransferRouting.Hospital.Harness qualified as Hospital+import Generated.TransferRouting.HospitalLoad.ReadModelHarness qualified as HospitalLoad+import Generated.TransferRouting.HospitalTransferRouter.RouterHarness qualified as HospitalTransferRouter runServiceConformanceChecks :: IO [(String, Bool)] runServiceConformanceChecks =- pure- ( [("aggregate/Hospital/" <> fact, passed) | (fact, passed) <- Harness0.harnessAssertions]- <> [("readmodel/hospital_load/" <> fact, passed) | (fact, passed) <- Harness1.readModelFactResults]- )+ pure (+ [("aggregate/Hospital/" <> fact, passed) | (fact, passed) <- Hospital.harnessAssertions]+ <> [("readmodel/hospital_load/" <> fact, passed) | (fact, passed) <- HospitalLoad.readModelFactResults]+ ) serviceConformanceFacts :: [(String, String)] serviceConformanceFacts =- [("router/HospitalTransferRouter/" <> fact, value) | (fact, value) <- Harness2.routerHarnessValues]+ [("router/HospitalTransferRouter/" <> fact, value) | (fact, value) <- HospitalTransferRouter.routerHarnessValues]
test/conformance-newsurface/Generated/TransferRouting/Hospital/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.Codec ( hospitalCodec, parseHospitalEvent,@@ -7,8 +7,8 @@ ) where import Generated.TransferRouting.Hospital.Domain-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> o .: "transferNeedId" <*> o .: "hospitalId" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes hospitalEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes hospitalEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-newsurface/Generated/TransferRouting/Hospital/Domain.hs view
@@ -1,9 +1,8 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.Domain where -import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))
test/conformance-newsurface/Generated/TransferRouting/Hospital/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.EventStream ( hospitalCategory , hospitalCommandCategory@@ -10,7 +10,7 @@ import Generated.TransferRouting.Hospital.Domain import Generated.TransferRouting.Hospital.Codec (hospitalCodec)-import Generated.TransferRouting.Hospital.Transducer (hospitalFoldFingerprint, hospitalTransducer)+import Generated.TransferRouting.Hospital.Transducer (hospitalTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-newsurface/Generated/TransferRouting/Hospital/Harness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.Harness (harnessAssertions) where import Generated.TransferRouting.Hospital.Domain@@ -13,7 +13,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions hospitalTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: AcceptedTransferNeedRouted", roundTrips sampleEventAcceptedTransferNeedRouted) , ("accepts RouteAcceptedTransferNeed from HospitalAccepting", acceptRouteAcceptedTransferNeed) ]@@ -23,11 +23,11 @@ roundTrips e = parseHospitalEvent (eventType hospitalCodec e) (encodeHospitalEvent e) == Right e sampleEventAcceptedTransferNeedRouted :: HospitalEvent-sampleEventAcceptedTransferNeedRouted = (AcceptedTransferNeedRouted (AcceptedTransferNeedRoutedData "sample-transferNeedId" "sample-hospitalId"))+sampleEventAcceptedTransferNeedRouted = AcceptedTransferNeedRouted (AcceptedTransferNeedRoutedData "sample-transferNeedId" "sample-hospitalId") acceptRouteAcceptedTransferNeed :: Bool acceptRouteAcceptedTransferNeed =- case step hospitalTransducer (HospitalAccepting, initialHospitalRegs) ((RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "sample-transferNeedId" "sample-hospitalId"))) of+ case step hospitalTransducer (HospitalAccepting, initialHospitalRegs) (RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "sample-transferNeedId" "sample-hospitalId")) of Just (v, _, _) -> v == HospitalAccepting Nothing -> False @@ -35,7 +35,7 @@ -- 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+ 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
test/conformance-newsurface/Generated/TransferRouting/Hospital/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-newsurface/Generated/TransferRouting/Hospital/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.TransferRouting.Hospital.Transducer ( hospitalTransducer , hospitalFoldFingerprint
+ test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModel.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.+module Generated.TransferRouting.HospitalLoad.ReadModel+ ( hospitalLoadReadModel+ , hospitalLoadQualifiedTable+ , registerHospitalLoad+ , startHospitalLoadRebuild+ , finishHospitalLoadRebuild+ , abandonHospitalLoadRebuild+ , hospitalLoadAsyncProjection+ ) where++import Data.Functor (void)+import Effectful (Eff, (:>))+import Generated.TransferRouting.HospitalLoad.ReadModelTable (hospitalLoadQualifiedTable)+import TransferRouting.HospitalLoad.ReadModelHoles (HospitalLoadQueryInput, HospitalLoadQueryResult, hospitalLoadQuery, applyHospitalLoad)+import Keiro.Projection (AsyncProjection (..))+import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)+import Keiro.ReadModel.Rebuild qualified as Rebuild+import Kiroku.Store.Effect (Store)+import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))++hospitalLoadReadModel :: ReadModel HospitalLoadQueryInput HospitalLoadQueryResult+hospitalLoadReadModel =+ ReadModel+ { name = "transfer-routing-hospital-load"+ , tableName = "hospital_load"+ , schema = "hospital_transfer"+ , subscriptionName = "transfer-routing-hospital-load-sub"+ , version = 1+ , shapeHash = "fnv1a:977395d28f254ddb"+ , defaultConsistency = Eventual+ , strongScope = EntireLog+ , query = hospitalLoadQuery+ }++-- Call once at projection startup before serving queries.+registerHospitalLoad :: (Store :> es) => Eff es ()+registerHospitalLoad =+ void (registerReadModel "transfer-routing-hospital-load" 1 "fnv1a:977395d28f254ddb")++startHospitalLoadRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata+startHospitalLoadRebuild =+ Rebuild.startRebuild hospitalLoadReadModel ["transfer-routing-hospital-load-async"]++finishHospitalLoadRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)+finishHospitalLoadRebuild =+ Rebuild.finishRebuild hospitalLoadReadModel ["transfer-routing-hospital-load-async"]++abandonHospitalLoadRebuild :: (Store :> es) => Eff es ReadModelMetadata+abandonHospitalLoadRebuild = Rebuild.abandonRebuild hospitalLoadReadModel++hospitalLoadAsyncProjection :: AsyncProjection+hospitalLoadAsyncProjection =+ AsyncProjection+ { name = "transfer-routing-hospital-load-async"+ , readModelName = "transfer-routing-hospital-load"+ , subscriptionName = "transfer-routing-hospital-load-sub"+ , applyRecorded = applyHospitalLoad+ , idempotencyKey = \recorded -> recorded.eventId+ }
+ test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModelHarness.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.+module Generated.TransferRouting.HospitalLoad.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where++import Generated.TransferRouting.HospitalLoad.ReadModel (hospitalLoadReadModel, hospitalLoadAsyncProjection)+import Data.Text qualified as T+import Keiro.ReadModel (ReadModel (..), StrongScope (..))+import Keiro.Projection (AsyncProjection (..))++-- | (fact, expected from notation, actual generated runtime value).+readModelFacts :: [(String, String, String)]+readModelFacts =+ [ ("registryName", "transfer-routing-hospital-load", T.unpack hospitalLoadReadModel.name)+ , ("subscriptionName", "transfer-routing-hospital-load-sub", T.unpack hospitalLoadReadModel.subscriptionName)+ , ("shapeHash", "fnv1a:977395d28f254ddb", T.unpack hospitalLoadReadModel.shapeHash)+ , ("asyncProjectionName", "transfer-routing-hospital-load-async", T.unpack hospitalLoadAsyncProjection.name)+ , ("consistency", "Eventual", show hospitalLoadReadModel.defaultConsistency)+ , ("strongScope", "EntireLog", renderStrongScope hospitalLoadReadModel.strongScope)+ ]++renderStrongScope :: StrongScope -> String+renderStrongScope EntireLog = "EntireLog"+renderStrongScope (CategoryHead categoryName) = "CategoryHead " <> T.unpack categoryName++readModelFactResults :: [(String, Bool)]+readModelFactResults =+ [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]++runReadModelFacts :: IO Bool+runReadModelFacts = do+ let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]+ mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures+ pure (null failures)
+ test/conformance-newsurface/Generated/TransferRouting/HospitalLoad/ReadModelTable.hs view
@@ -0,0 +1,9 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.+module Generated.TransferRouting.HospitalLoad.ReadModelTable (hospitalLoadQualifiedTable) where++import Data.Text (Text)+import Keiro.Connection (qualifyTable)++-- The fully-qualified, double-quoted data-table reference.+hospitalLoadQualifiedTable :: Text+hospitalLoadQualifiedTable = qualifyTable "hospital_transfer" "hospital_load"
test/conformance-newsurface/Generated/TransferRouting/HospitalTransferRouter/Router.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router HospitalTransferRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router HospitalTransferRouter; do not edit. module Generated.TransferRouting.HospitalTransferRouter.Router ( hospitalTransferRouterName , hospitalTransferRouterWorkerOptions
test/conformance-newsurface/Generated/TransferRouting/HospitalTransferRouter/RouterHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router HospitalTransferRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router HospitalTransferRouter; do not edit. module Generated.TransferRouting.HospitalTransferRouter.RouterHarness (routerHarnessValues) where routerHarnessValues :: [(String, String)]
− test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModel.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.-module Generated.TransferRouting.Hospital_load.ReadModel- ( hospitalLoadReadModel- , hospitalLoadQualifiedTable- , registerHospitalLoad- , startHospitalLoadRebuild- , finishHospitalLoadRebuild- , abandonHospitalLoadRebuild- , hospitalLoadAsyncProjection- ) where--import Data.Functor (void)-import Effectful (Eff, (:>))-import Generated.TransferRouting.Hospital_load.ReadModelTable (hospitalLoadQualifiedTable)-import TransferRouting.Hospital_load.ReadModelHoles (HospitalLoadQueryInput, HospitalLoadQueryResult, hospitalLoadQuery, applyHospitalLoad)-import Keiro.Projection (AsyncProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)-import Keiro.ReadModel.Rebuild qualified as Rebuild-import Kiroku.Store.Effect (Store)-import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))--hospitalLoadReadModel :: ReadModel HospitalLoadQueryInput HospitalLoadQueryResult-hospitalLoadReadModel =- ReadModel- { name = "transfer-routing-hospital-load"- , tableName = "hospital_load"- , schema = "hospital_transfer"- , subscriptionName = "transfer-routing-hospital-load-sub"- , version = 1- , shapeHash = "fnv1a:977395d28f254ddb"- , defaultConsistency = Eventual- , strongScope = EntireLog- , query = hospitalLoadQuery- }---- Call once at projection startup before serving queries.-registerHospitalLoad :: (Store :> es) => Eff es ()-registerHospitalLoad =- void (registerReadModel "transfer-routing-hospital-load" 1 "fnv1a:977395d28f254ddb")--startHospitalLoadRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata-startHospitalLoadRebuild =- Rebuild.startRebuild hospitalLoadReadModel ["transfer-routing-hospital-load-async"]--finishHospitalLoadRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)-finishHospitalLoadRebuild =- Rebuild.finishRebuild hospitalLoadReadModel ["transfer-routing-hospital-load-async"]--abandonHospitalLoadRebuild :: (Store :> es) => Eff es ReadModelMetadata-abandonHospitalLoadRebuild = Rebuild.abandonRebuild hospitalLoadReadModel--hospitalLoadAsyncProjection :: AsyncProjection-hospitalLoadAsyncProjection =- AsyncProjection- { name = "transfer-routing-hospital-load-async"- , readModelName = "transfer-routing-hospital-load"- , subscriptionName = "transfer-routing-hospital-load-sub"- , applyRecorded = applyHospitalLoad- , idempotencyKey = \recorded -> recorded.eventId- }
− test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModelHarness.hs
@@ -1,23 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.-module Generated.TransferRouting.Hospital_load.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where---- | (fact, expected from notation, actual shared derivation/lowering).-readModelFacts :: [(String, String, String)]-readModelFacts =- [ ("registryName", "transfer-routing-hospital-load", "transfer-routing-hospital-load")- , ("subscriptionName", "transfer-routing-hospital-load-sub", "transfer-routing-hospital-load-sub")- , ("shapeHash", "fnv1a:977395d28f254ddb", "fnv1a:977395d28f254ddb")- , ("asyncProjectionName", "transfer-routing-hospital-load-async", "transfer-routing-hospital-load-async")- , ("consistency", "Eventual", "Eventual")- , ("strongScope", "EntireLog", "EntireLog")- ]--readModelFactResults :: [(String, Bool)]-readModelFactResults =- [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]--runReadModelFacts :: IO Bool-runReadModelFacts = do- let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]- mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures- pure (null failures)
− test/conformance-newsurface/Generated/TransferRouting/Hospital_load/ReadModelTable.hs
@@ -1,9 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel hospital_load; do not edit.-module Generated.TransferRouting.Hospital_load.ReadModelTable (hospitalLoadQualifiedTable) where--import Data.Text (Text)-import Keiro.Connection (qualifyTable)---- The fully-qualified, double-quoted data-table reference.-hospitalLoadQualifiedTable :: Text-hospitalLoadQualifiedTable = qualifyTable "hospital_transfer" "hospital_load"
test/conformance-newsurface/Generated/TransferRouting/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context transfer-routing replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context transfer-routing replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-newsurface/Main.hs view
@@ -9,7 +9,7 @@ import Generated.TransferRouting.Hospital.EventStream (hospitalCommandCategory, hospitalEventStream) import Generated.TransferRouting.Hospital.Harness (harnessAssertions) import Generated.TransferRouting.HospitalTransferRouter.RouterHarness (routerHarnessValues)-import Generated.TransferRouting.Hospital_load.ReadModelHarness (runReadModelFacts)+import Generated.TransferRouting.HospitalLoad.ReadModelHarness (runReadModelFacts) import Keiro.ProcessManager (PMCommand (..)) import Keiro.Router (Router (..)) import Keiro.Stream (entityStream)
+ test/conformance-newsurface/TransferRouting/HospitalLoad/ReadModelHoles.hs view
@@ -0,0 +1,35 @@+-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.+module TransferRouting.HospitalLoad.ReadModelHoles (+ HospitalLoadQueryInput,+ HospitalLoadQueryResult,+ hospitalLoadQuery,+ applyHospitalLoad,+) where++import Data.Text.Encoding qualified as Text+import Generated.TransferRouting.HospitalLoad.ReadModelTable (hospitalLoadQualifiedTable)+import Hasql.Transaction qualified as Tx+import Kiroku.Store.Types (RecordedEvent)++-- HOLE: replace these aliases with the real query input and result types.+type HospitalLoadQueryInput = ()+type HospitalLoadQueryResult = ()++-- HOLE: query "hospital_transfer"."hospital_load" via hospitalLoadQualifiedTable; never rely on search_path.+-- Declared columns:+-- hospital_id text NOT NULL+-- region text NOT NULL+-- available_beds int NOT NULL+hospitalLoadQuery :: HospitalLoadQueryInput -> Tx.Transaction HospitalLoadQueryResult+hospitalLoadQuery _input =+ Tx.sql+ ( Text.encodeUtf8+ ( "SELECT hospital_id FROM "+ <> hospitalLoadQualifiedTable+ <> " WHERE available_beds > 0 ORDER BY available_beds DESC, hospital_id LIMIT 1"+ )+ )++-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.+applyHospitalLoad :: RecordedEvent -> Tx.Transaction ()+applyHospitalLoad _recorded = pure ()
test/conformance-newsurface/TransferRouting/HospitalTransferRouter/RouterValue.hs view
@@ -16,7 +16,7 @@ hospitalEventStream, ) import Generated.TransferRouting.HospitalTransferRouter.Router (hospitalTransferRouterName)-import Generated.TransferRouting.Hospital_load.ReadModel (hospitalLoadReadModel)+import Generated.TransferRouting.HospitalLoad.ReadModel (hospitalLoadReadModel) import Keiki.Core (HsPred) import Keiro.ProcessManager (PMCommand (..)) import Keiro.Router (Router (..))
− test/conformance-newsurface/TransferRouting/Hospital_load/ReadModelHoles.hs
@@ -1,35 +0,0 @@--- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.-module TransferRouting.Hospital_load.ReadModelHoles (- HospitalLoadQueryInput,- HospitalLoadQueryResult,- hospitalLoadQuery,- applyHospitalLoad,-) where--import Data.Text.Encoding qualified as Text-import Generated.TransferRouting.Hospital_load.ReadModelTable (hospitalLoadQualifiedTable)-import Hasql.Transaction qualified as Tx-import Kiroku.Store.Types (RecordedEvent)---- HOLE: replace these aliases with the real query input and result types.-type HospitalLoadQueryInput = ()-type HospitalLoadQueryResult = ()---- HOLE: query "hospital_transfer"."hospital_load" via hospitalLoadQualifiedTable; never rely on search_path.--- Declared columns:--- hospital_id text NOT NULL--- region text NOT NULL--- available_beds int NOT NULL-hospitalLoadQuery :: HospitalLoadQueryInput -> Tx.Transaction HospitalLoadQueryResult-hospitalLoadQuery _input =- Tx.sql- ( Text.encodeUtf8- ( "SELECT hospital_id FROM "- <> hospitalLoadQualifiedTable- <> " WHERE available_beds > 0 ORDER BY available_beds DESC, hospital_id LIMIT 1"- )- )---- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.-applyHospitalLoad :: RecordedEvent -> Tx.Transaction ()-applyHospitalLoad _recorded = pure ()
test/conformance-nominal-scalars/Generated/NominalScalars/Nominal/Shape/OrderStatus.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from bound nominal enum representation OrderStatus; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from bound nominal enum representation OrderStatus; do not edit. module Generated.NominalScalars.Nominal.Shape.OrderStatus (OrderStatusRepresentation (..), orderStatusRepresentationText) where import Data.Text (Text)
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.-module Generated.NominalScalars.NominalLedger.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+module Generated.NominalScalars.NominalLedger.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.NominalScalars.NominalLedger.Codec (encodeNominalLedgerEvent, parseNominalLedgerEvent, nominalLedgerCodec) import Generated.NominalScalars.NominalLedger.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,8 +126,32 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-274eaf83d346048a") LiveTransition GeneratedAuthoritative GuardUnknown NominalLedgerEmpty "RecordNominals" (Just (K.EdgeRef NominalLedgerEmpty 0)) (Just NominalLedgerRecorded) ["NominalsRecorded"] 84- , BehaviorRequirement (BehaviorKey "behavior-v1-971c0c80ffdfe018") RequiredRejection GeneratedAuthoritative GuardNotApplicable NominalLedgerRecorded "RecordNominals" Nothing Nothing [] 76+ [ -- NominalLedgerEmpty x RecordNominals: live transition (spec line 84)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-274eaf83d346048a"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = NominalLedgerEmpty+ , requirementCommandName = "RecordNominals"+ , requirementExpectedEdge = (Just (K.EdgeRef NominalLedgerEmpty 0))+ , requirementTarget = Just NominalLedgerRecorded+ , requirementEventKinds = ["NominalsRecorded"]+ , requirementLine = 84+ }+ , -- NominalLedgerRecorded x RecordNominals: required rejection (spec line 76)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-971c0c80ffdfe018"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = NominalLedgerRecorded+ , requirementCommandName = "RecordNominals"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 76+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -135,7 +176,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -162,7 +203,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -180,6 +221,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (NominalLedgerVertex, K.RegFile NominalLedgerRegs) -> NominalLedgerCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -189,6 +231,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (NominalLedgerVertex, K.RegFile NominalLedgerRegs) -> NominalLedgerCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither nominalLedgerTransducer seed command of@@ -204,8 +247,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither nominalLedgerTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -214,10 +257,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess NominalLedgerRegs NominalLedgerVertex NominalLedgerEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [NominalLedgerEvent] -> [NominalLedgerEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -229,19 +273,21 @@ replayed <- case K.applyEventsDetailedEither nominalLedgerTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution NominalLedgerVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [NominalLedgerEvent] -> Either BehaviorFailure (K.ReplaySuccess NominalLedgerRegs NominalLedgerVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) decoded of@@ -251,30 +297,52 @@ decodeEvents :: [NominalLedgerEvent] -> Either Text [NominalLedgerEvent] decodeEvents = traverse (\event -> parseNominalLedgerEvent (Codec.eventType nominalLedgerCodec event) (encodeNominalLedgerEvent event)) +commandKind :: NominalLedgerCommand -> Text commandKind command = case command of RecordNominals _ -> "RecordNominals" +eventKind :: NominalLedgerEvent -> Text eventKind event = case Codec.eventType nominalLedgerCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile NominalLedgerRegs -> K.RegFile NominalLedgerRegs -> Bool regsEqual left right = (left K.! #orderId) == (right K.! #orderId) && (left K.! #status) == (right K.! #status) && (left K.! #accountNumber) == (right K.! #accountNumber) && (left K.! #riskScore) == (right K.! #riskScore) && (left K.! #sequenceNumber) == (right K.! #sequenceNumber) && (left K.! #featureFlag) == (right K.! #featureFlag) && (left K.! #observedAt) == (right K.! #observedAt) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.Codec ( nominalLedgerCodec, parseNominalLedgerEvent,@@ -21,7 +21,7 @@ import Generated.NominalScalars.Nominal.Shape.OrderStatus qualified as ShapeOrderStatus import NominalConformance.Bindings qualified as Bindings-import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)+import NominalConformance.Domain (OrderId, OrderStatus) @@ -92,13 +92,13 @@ <*> (nominalFromRepresentation Bindings.featureFlagBinding <$> o .: "featureFlag") <*> (nominalFromRepresentation Bindings.observedAtBinding <$> o .: "observedAt") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes nominalLedgerEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes nominalLedgerEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Domain.hs view
@@ -1,12 +1,11 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.Domain where import Data.Aeson (FromJSON, ToJSON) import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.EventStream ( nominalLedgerCategory , nominalLedgerCommandCategory
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.Harness (harnessAssertions) where import Generated.NominalScalars.NominalLedger.Domain@@ -14,7 +14,6 @@ import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) import Generated.NominalScalars.NominalProjections qualified as NominalProjections import NominalConformance.Bindings qualified as Bindings-import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber) -- | (label, passed). A driver runs these and exits non-zero on any False, -- naming the failing assertion. Filling a hole wrongly turns a specific@@ -22,7 +21,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions nominalLedgerTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: NominalsRecorded", roundTrips sampleEventNominalsRecorded) , ("accepts RecordNominals from NominalLedgerEmpty", acceptRecordNominals) ]@@ -33,11 +32,11 @@ roundTrips e = parseNominalLedgerEvent (eventType nominalLedgerCodec e) (encodeNominalLedgerEvent e) == Right e sampleEventNominalsRecorded :: NominalLedgerEvent-sampleEventNominalsRecorded = (NominalsRecorded (NominalsRecordedData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderIdFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures)))))+sampleEventNominalsRecorded = NominalsRecorded (NominalsRecordedData (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderIdFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.orderStatusFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures)))) acceptRecordNominals :: Bool acceptRecordNominals =- case step nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) ((RecordNominals (RecordNominalsData Bindings.initialOrderId Bindings.initialOrderStatus (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures)))))) of+ case step nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) (RecordNominals (RecordNominalsData Bindings.initialOrderId Bindings.initialOrderStatus (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures))))) of Just (v, _, _) -> v == NominalLedgerRecorded Nothing -> False @@ -45,7 +44,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRecordNominals :: [(String, Bool)] forwardReplayRecordNominals =- case step nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) ((RecordNominals (RecordNominalsData Bindings.initialOrderId Bindings.initialOrderStatus (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures)))))) of+ case step nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) (RecordNominals (RecordNominalsData Bindings.initialOrderId Bindings.initialOrderStatus (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.accountNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.riskScoreFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.sequenceNumberFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.featureFlagFixtures))) (nominalFixtureDomain (NonEmpty.head (nominalFixtureCases Bindings.observedAtFixtures))))) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseNominalLedgerEvent (eventType nominalLedgerCodec event) (encodeNominalLedgerEvent event)) emitted of
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-nominal-scalars/Generated/NominalScalars/NominalLedger/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate NominalLedger; do not edit. module Generated.NominalScalars.NominalLedger.Transducer ( nominalLedgerTransducer , nominalLedgerFoldFingerprint@@ -13,7 +13,7 @@ import Generated.NominalScalars.NominalLedger.Domain import Data.Text (Text) import Generated.NominalScalars.NominalProjections qualified as NominalProjections-import NominalConformance.Domain (AccountNumber, FeatureFlag, ObservedAt, OrderId, OrderStatus, RiskScore, SequenceNumber)+import NominalConformance.Domain (OrderId, OrderStatus) import Keiki.Builder qualified as B import Keiki.Core (HsPred, SymTransducer, (.==), (.&&)) import Keiki.Core qualified as K
test/conformance-nominal-scalars/Generated/NominalScalars/NominalProjections.hs view
@@ -1,15 +1,15 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context nominal-scalars nominal scalar projection facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context nominal-scalars nominal scalar projection facade; do not edit. module Generated.NominalScalars.NominalProjections where import Data.KindID qualified as KindID import Data.List.NonEmpty (NonEmpty (..)) import Data.Text (Text) import Data.Time (UTCTime)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, matchesTextPattern, textCharSet, textConcat, textLiteral, textProjectionDomain, textRepeatBetween)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness, fieldWitness)+import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, matchesTextPattern, textProjectionDomain) import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain, validateIdDomainText)-import Keiro.Codec.Nominal (nominalFromRepresentation, nominalToRepresentation)+import Keiro.Codec.Nominal (nominalToRepresentation, nominalFromRepresentation) import Numeric.Natural (Natural) import Generated.NominalScalars.Nominal.Shape.OrderStatus qualified as ShapeOrderStatus import NominalConformance.Bindings qualified as Bindings
test/conformance-nominal-scalars/Generated/NominalScalars/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context nominal-scalars replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context nominal-scalars replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-nominal-scalars/Main.hs view
@@ -345,8 +345,33 @@ accepts command = case K.step nominalLedgerTransducer (NominalLedgerEmpty, initialNominalLedgerRegs) command of Just {} -> True Nothing -> False- setOrderId value (RecordNominals command) = RecordNominals command{orderId = value}- setStatus value (RecordNominals command) = RecordNominals command{status = value}+ -- Constructed field by field rather than with record-update syntax. Several+ -- generated records share these field names, so an update — even with a type+ -- ascription — relies on DuplicateRecordFields' type-directed+ -- disambiguation, which GHC deprecates and which is an error under the+ -- generated-output -Werror.+ setOrderId value (RecordNominals command) =+ RecordNominals+ RecordNominalsData+ { orderId = value,+ status = command.status,+ accountNumber = command.accountNumber,+ riskScore = command.riskScore,+ sequenceNumber = command.sequenceNumber,+ featureFlag = command.featureFlag,+ observedAt = command.observedAt+ }+ setStatus value (RecordNominals command) =+ RecordNominals+ RecordNominalsData+ { orderId = command.orderId,+ status = value,+ accountNumber = command.accountNumber,+ riskScore = command.riskScore,+ sequenceNumber = command.sequenceNumber,+ featureFlag = command.featureFlag,+ observedAt = command.observedAt+ } alternateOrderId :: OrderId alternateOrderId = case KindID.parseText @"ord" "ord_01h455vb4pex5vsknk084sn02r" of
test/conformance-process-full/Generated/SurgeDemo/Hospital/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.SurgeDemo.Hospital.Codec ( hospitalCodec, parseHospitalEvent,@@ -7,10 +7,10 @@ ) where import Generated.SurgeDemo.Hospital.Domain-import Generated.SurgeDemo.Nominals (HospitalId, hospitalIdText)+import Generated.SurgeDemo.Nominals (hospitalIdText) import Generated.SurgeDemo.Nominals.Internal (unsafeHospitalIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> ( SurgeActivatedData <$> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes hospitalEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes hospitalEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-process-full/Generated/SurgeDemo/Hospital/Domain.hs view
@@ -1,13 +1,11 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.SurgeDemo.Hospital.Domain where -import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.SurgeDemo.Nominals (HospitalId, parseHospitalId)+import Generated.SurgeDemo.Nominals (HospitalId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data HospitalVertex = HospitalIdle | HospitalSurging
test/conformance-process-full/Generated/SurgeDemo/Hospital/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.SurgeDemo.Hospital.EventStream ( hospitalCategory , hospitalCommandCategory@@ -10,7 +10,7 @@ import Generated.SurgeDemo.Hospital.Domain import Generated.SurgeDemo.Hospital.Codec (hospitalCodec)-import Generated.SurgeDemo.Hospital.Transducer (hospitalFoldFingerprint, hospitalTransducer)+import Generated.SurgeDemo.Hospital.Transducer (hospitalTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-process-full/Generated/SurgeDemo/Hospital/Projection.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.SurgeDemo.Hospital.Projection ( hospitalProjection , hospitalStatusFor
test/conformance-process-full/Generated/SurgeDemo/Hospital/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module Generated.SurgeDemo.Hospital.Transducer ( hospitalTransducer , hospitalFoldFingerprint
test/conformance-process-full/Generated/SurgeDemo/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context surge-demo generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context surge-demo generated nominal declarations; do not edit. module Generated.SurgeDemo.Nominals ( HospitalId , parseHospitalId@@ -6,11 +6,4 @@ , hospitalIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import Generated.SurgeDemo.Nominals.Internal (HospitalId, mkHospitalId, parseHospitalId, hospitalIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName HospitalId
test/conformance-process-full/Generated/SurgeDemo/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context surge-demo generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context surge-demo generated nominal ID internals; do not edit. module Generated.SurgeDemo.Nominals.Internal ( HospitalId , parseHospitalId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName HospitalId instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText
test/conformance-process-full/Generated/SurgeDemo/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context surge-demo replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context surge-demo replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-process-full/Generated/SurgeDemo/Surge/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.SurgeDemo.Surge.Codec ( surgeCodec, parseSurgeEvent,@@ -7,10 +7,10 @@ ) where import Generated.SurgeDemo.Surge.Domain-import Generated.SurgeDemo.Nominals (HospitalId, hospitalIdText)+import Generated.SurgeDemo.Nominals (hospitalIdText) import Generated.SurgeDemo.Nominals.Internal (unsafeHospitalIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -65,13 +65,13 @@ <$> ( SurgeTimerFiredData <$> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes surgeEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes surgeEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-process-full/Generated/SurgeDemo/Surge/Domain.hs view
@@ -1,13 +1,11 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.SurgeDemo.Surge.Domain where -import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.SurgeDemo.Nominals (HospitalId, parseHospitalId)+import Generated.SurgeDemo.Nominals (HospitalId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data SurgeVertex = SurgeWatching | SurgeNoted | SurgeFired
test/conformance-process-full/Generated/SurgeDemo/Surge/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.SurgeDemo.Surge.EventStream ( surgeCategory , surgeCommandCategory@@ -10,7 +10,7 @@ import Generated.SurgeDemo.Surge.Domain import Generated.SurgeDemo.Surge.Codec (surgeCodec)-import Generated.SurgeDemo.Surge.Transducer (surgeFoldFingerprint, surgeTransducer)+import Generated.SurgeDemo.Surge.Transducer (surgeTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-process-full/Generated/SurgeDemo/Surge/Projection.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.SurgeDemo.Surge.Projection ( surgeProjection , surgeStatusFor
test/conformance-process-full/Generated/SurgeDemo/Surge/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.SurgeDemo.Surge.Transducer ( surgeTransducer , surgeFoldFingerprint
test/conformance-process-full/Generated/SurgeDemo/SurgeFlow/Process.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from process SurgeFlow; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from process SurgeFlow; do not edit. module Generated.SurgeDemo.SurgeFlow.Process ( surgeFlowProcessName , surgeFlowCategory
test/conformance-process-runtime/Generated/HospitalCapacity/HospitalSurge/Process.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit. module Generated.HospitalCapacity.HospitalSurge.Process ( hospitalSurgeProcessName , hospitalSurgeCategory
test/conformance-process-runtime/Generated/HospitalCapacity/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit. module Generated.HospitalCapacity.Nominals ( CommandId , parseCommandId@@ -10,13 +10,4 @@ , hospitalIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import Generated.HospitalCapacity.Nominals.Internal (CommandId, mkCommandId, parseCommandId, commandIdText, HospitalId, mkHospitalId, parseHospitalId, hospitalIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName CommandId--instance CanonicalTypeName HospitalId
test/conformance-process-runtime/Generated/HospitalCapacity/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit. module Generated.HospitalCapacity.Nominals.Internal ( CommandId , parseCommandId@@ -16,11 +16,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CommandId = CommandId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CommandId+ instance ToJSON CommandId where toJSON = toJSON . commandIdText @@ -43,6 +46,8 @@ newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName HospitalId instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText
test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.HospitalCapacity.Surge.Codec ( surgeCodec, parseSurgeEvent,@@ -7,10 +7,10 @@ ) where import Generated.HospitalCapacity.Surge.Domain-import Generated.HospitalCapacity.Nominals (HospitalId, hospitalIdText)+import Generated.HospitalCapacity.Nominals (hospitalIdText) import Generated.HospitalCapacity.Nominals.Internal (unsafeHospitalIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -73,13 +73,13 @@ <$> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") <*> o .: "timerId" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes surgeEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes surgeEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Domain.hs view
@@ -1,13 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.HospitalCapacity.Surge.Domain where -import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.HospitalCapacity.Nominals (HospitalId, parseHospitalId)+import Generated.HospitalCapacity.Nominals (HospitalId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data SurgeVertex = SurgeIdle
test/conformance-process-runtime/Generated/HospitalCapacity/Surge/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.HospitalCapacity.Surge.EventStream ( surgeCategory , surgeCommandCategory@@ -10,7 +10,7 @@ import Generated.HospitalCapacity.Surge.Domain import Generated.HospitalCapacity.Surge.Codec (surgeCodec)-import Generated.HospitalCapacity.Surge.Transducer (surgeFoldFingerprint, surgeTransducer)+import Generated.HospitalCapacity.Surge.Transducer (surgeTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-process-runtime/Generated/HospitalCapacity/Surge/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module Generated.HospitalCapacity.Surge.Transducer ( surgeTransducer , surgeFoldFingerprint
test/conformance-process-runtime/HospitalCapacity/Surge/Holes.hs view
@@ -13,9 +13,8 @@ ) where import Generated.HospitalCapacity.Surge.Domain-import Keiki.Builder ((=:)) import qualified Keiki.Builder as B-import Keiki.Core (HsPred, RegFile, SymTransducer, lit, (./=), (.==), (.||))+import Keiki.Core (HsPred, SymTransducer) -- HOLE: the transducer body. Reproduce the structure below, replacing each -- `-- HOLE` line with the keiki symbolic operators it describes.
test/conformance-process/Generated/HospitalCapacity/HospitalSurge/ProcessHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit. module Generated.HospitalCapacity.HospitalSurge.ProcessHarness (processHarnessValues) where -- | (label, value): the spec's deterministic process/timer decisions,
test/conformance-publisher-runtime/Generated/HospitalCapacity/HospitalPublisher/Publisher.hs view
@@ -1,11 +1,11 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from publisher hospitalPublisher; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from publisher hospitalPublisher; do not edit. module Generated.HospitalCapacity.HospitalPublisher.Publisher ( publisherOrdering , publisherBackoff , publisherMaxAttempts ) where -import Keiro.Outbox.Types (BackoffSchedule (..), ExponentialBackoffOptions (..), OrderingPolicy (..))+import Keiro.Outbox.Types (BackoffSchedule (..), OrderingPolicy (..)) publisherOrdering :: OrderingPolicy publisherOrdering = PerKeyHeadOfLine
+ test/conformance-queue-runtime/Generated/HospitalCapacity/ReservationWork/Queue.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+module Generated.HospitalCapacity.ReservationWork.Queue+ ( ReservationWorkItem (..)+ , encodeReservationWorkItem+ , parseReservationWorkItem+ , queuePhysical, queueDlq, queueTable+ , groupKeyField, groupKeyFor+ ) 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 = "hospital_capacity_reservation_work"+queueDlq = "hospital_capacity_reservation_work_dlq"+queueTable = "pgmq.q_hospital_capacity_reservation_work"++groupKeyField :: Text+groupKeyField = "reservationId"++groupKeyFor :: ReservationWorkItem -> Text+groupKeyFor payload = payload.reservationId++data ReservationWorkItem = ReservationWorkItem+ { reservationId :: !Text+ , hospitalId :: !Text+ , commandId :: !Text+ , lifeCriticalOverride :: !Bool+ }+ deriving stock (Eq, Show)++encodeReservationWorkItem :: ReservationWorkItem -> Value+encodeReservationWorkItem p =+ object+ [ "reservation_id" .= p.reservationId+ , "hospital_id" .= p.hospitalId+ , "command_id" .= p.commandId+ , "life_critical_override" .= p.lifeCriticalOverride+ ]++parseReservationWorkItem :: Value -> Either Text ReservationWorkItem+parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)+ where+ go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right
+ test/conformance-queue-runtime/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs view
@@ -0,0 +1,29 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+-- | 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.ReservationWork.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)+import Generated.HospitalCapacity.ReservationWork.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-queue-runtime/Generated/HospitalCapacity/ReservationWork/QueuePolicy.hs view
@@ -0,0 +1,43 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+module Generated.HospitalCapacity.ReservationWork.QueuePolicy+ ( ReservationWorkOutcome (..)+ , retryPolicy, jobOutcomeFor+ , jobOrdering, jobTuningFor, queueProvision+ ) where++import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, QueueProvision, RetryDelay (..), RetryPolicy (..), standardProvision, withFifoIndexProvision, withOrdering)++jobOrdering :: JobOrdering+jobOrdering = FifoThroughput++-- 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 = withFifoIndexProvision (standardProvision)++retryPolicy :: RetryPolicy+retryPolicy =+ RetryPolicy+ { maxRetries = 3+ , defaultRetryDelay = RetryDelay 5+ , useDeadLetter = True+ }++-- The consumer JobOutcome disposition over the spec's named domain outcomes,+-- lowered to the live Keiro.PGMQ.Job.JobOutcome.+data ReservationWorkOutcome+ = StoreFailure+ | CommandRejected+ | DecodeFailure+ | OnCodecReject+ deriving stock (Eq, Show)++jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome+jobOutcomeFor o = case o of+ StoreFailure -> Retry (RetryDelay 5)+ CommandRejected -> Dead "dead-lettered"+ DecodeFailure -> Dead "dead-lettered"+ OnCodecReject -> Dead "dead-lettered"
− test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/Queue.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module Generated.HospitalCapacity.Reservation_work.Queue- ( ReservationWorkItem (..)- , encodeReservationWorkItem- , parseReservationWorkItem- , queuePhysical, queueDlq, queueTable- , groupKeyField, groupKeyFor- ) 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 = "hospital_capacity_reservation_work"-queueDlq = "hospital_capacity_reservation_work_dlq"-queueTable = "pgmq.q_hospital_capacity_reservation_work"--groupKeyField :: Text-groupKeyField = "reservationId"--groupKeyFor :: ReservationWorkItem -> Text-groupKeyFor payload = payload.reservationId--data ReservationWorkItem = ReservationWorkItem- { reservationId :: !Text- , hospitalId :: !Text- , commandId :: !Text- , lifeCriticalOverride :: !Bool- }- deriving stock (Eq, Show)--encodeReservationWorkItem :: ReservationWorkItem -> Value-encodeReservationWorkItem p =- object- [ "reservation_id" .= p.reservationId- , "hospital_id" .= p.hospitalId- , "command_id" .= p.commandId- , "life_critical_override" .= p.lifeCriticalOverride- ]--parseReservationWorkItem :: Value -> Either Text ReservationWorkItem-parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)- where- go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"--mapLeftText :: Either String b -> Either Text b-mapLeftText = either (Left . T.pack) Right
− test/conformance-queue-runtime/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs
@@ -1,29 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.--- | 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 Keiro.Codec (Codec (..), EventType (..))-import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)-import Generated.HospitalCapacity.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-queue-runtime/Generated/HospitalCapacity/Reservation_work/QueuePolicy.hs
@@ -1,43 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module Generated.HospitalCapacity.Reservation_work.QueuePolicy- ( ReservationWorkOutcome (..)- , retryPolicy, jobOutcomeFor- , jobOrdering, jobTuningFor, queueProvision- ) where--import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)--jobOrdering :: JobOrdering-jobOrdering = FifoThroughput---- 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 = withFifoIndexProvision (standardProvision)--retryPolicy :: RetryPolicy-retryPolicy =- RetryPolicy- { maxRetries = 3- , defaultRetryDelay = RetryDelay 5- , useDeadLetter = True- }---- The consumer JobOutcome disposition over the spec's named domain outcomes,--- lowered to the live Keiro.PGMQ.Job.JobOutcome.-data ReservationWorkOutcome- = StoreFailure- | CommandRejected- | DecodeFailure- | OnCodecReject- deriving stock (Eq, Show)--jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome-jobOutcomeFor o = case o of- StoreFailure -> Retry (RetryDelay 5)- CommandRejected -> Dead "dead-lettered"- DecodeFailure -> Dead "dead-lettered"- OnCodecReject -> Dead "dead-lettered"
test/conformance-queue-runtime/Main.hs view
@@ -9,9 +9,9 @@ import Data.Aeson (encode, object, (.=)) import Data.ByteString.Lazy (ByteString) import Data.Text (Text)-import Generated.HospitalCapacity.Reservation_work.Queue (ReservationWorkItem (..), encodeReservationWorkItem, groupKeyFor, parseReservationWorkItem)-import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec)-import Generated.HospitalCapacity.Reservation_work.QueuePolicy (ReservationWorkOutcome (..), jobOrdering, jobOutcomeFor, jobTuningFor, queueProvision, retryPolicy)+import Generated.HospitalCapacity.ReservationWork.Queue (ReservationWorkItem (..), encodeReservationWorkItem, groupKeyFor, parseReservationWorkItem)+import Generated.HospitalCapacity.ReservationWork.QueueCodec (reservationWorkJobCodec)+import Generated.HospitalCapacity.ReservationWork.QueuePolicy (ReservationWorkOutcome (..), jobOrdering, jobOutcomeFor, jobTuningFor, queueProvision, retryPolicy) import Keiro.Dsl.Validate (derivedQueueTrio) import Keiro.PGMQ.Job (Job (..), JobOrdering (..), JobOutcome (..), JobTuning (..), RetryPolicy (..), defaultJobTuning, queueProvisionConfigs) import Keiro.PGMQ.Runtime (QueueRef (..), queueRef)
+ test/conformance-queue/Generated/HospitalCapacity/ReservationWork/Queue.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+module Generated.HospitalCapacity.ReservationWork.Queue+ ( ReservationWorkItem (..)+ , encodeReservationWorkItem+ , parseReservationWorkItem+ , queuePhysical, queueDlq, queueTable+ , groupKeyField, groupKeyFor+ ) 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 = "hospital_capacity_reservation_work"+queueDlq = "hospital_capacity_reservation_work_dlq"+queueTable = "pgmq.q_hospital_capacity_reservation_work"++groupKeyField :: Text+groupKeyField = "reservationId"++groupKeyFor :: ReservationWorkItem -> Text+groupKeyFor payload = payload.reservationId++data ReservationWorkItem = ReservationWorkItem+ { reservationId :: !Text+ , hospitalId :: !Text+ , commandId :: !Text+ , lifeCriticalOverride :: !Bool+ }+ deriving stock (Eq, Show)++encodeReservationWorkItem :: ReservationWorkItem -> Value+encodeReservationWorkItem p =+ object+ [ "reservation_id" .= p.reservationId+ , "hospital_id" .= p.hospitalId+ , "command_id" .= p.commandId+ , "life_critical_override" .= p.lifeCriticalOverride+ ]++parseReservationWorkItem :: Value -> Either Text ReservationWorkItem+parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)+ where+ go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right
+ test/conformance-queue/Generated/HospitalCapacity/ReservationWork/QueueCodec.hs view
@@ -0,0 +1,29 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.+-- | 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.ReservationWork.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)+import Generated.HospitalCapacity.ReservationWork.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-queue/Generated/HospitalCapacity/Reservation_work/Queue.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module Generated.HospitalCapacity.Reservation_work.Queue- ( ReservationWorkItem (..)- , encodeReservationWorkItem- , parseReservationWorkItem- , queuePhysical, queueDlq, queueTable- , groupKeyField, groupKeyFor- ) 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 = "hospital_capacity_reservation_work"-queueDlq = "hospital_capacity_reservation_work_dlq"-queueTable = "pgmq.q_hospital_capacity_reservation_work"--groupKeyField :: Text-groupKeyField = "reservationId"--groupKeyFor :: ReservationWorkItem -> Text-groupKeyFor payload = payload.reservationId--data ReservationWorkItem = ReservationWorkItem- { reservationId :: !Text- , hospitalId :: !Text- , commandId :: !Text- , lifeCriticalOverride :: !Bool- }- deriving stock (Eq, Show)--encodeReservationWorkItem :: ReservationWorkItem -> Value-encodeReservationWorkItem p =- object- [ "reservation_id" .= p.reservationId- , "hospital_id" .= p.hospitalId- , "command_id" .= p.commandId- , "life_critical_override" .= p.lifeCriticalOverride- ]--parseReservationWorkItem :: Value -> Either Text ReservationWorkItem-parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)- where- go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id" <*> o .: "command_id" <*> o .: "life_critical_override"--mapLeftText :: Either String b -> Either Text b-mapLeftText = either (Left . T.pack) Right
− test/conformance-queue/Generated/HospitalCapacity/Reservation_work/QueueCodec.hs
@@ -1,29 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.--- | 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 Keiro.Codec (Codec (..), EventType (..))-import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)-import Generated.HospitalCapacity.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-queue/Main.hs view
@@ -1,5 +1,5 @@ {- | Conformance driver for the scaffolded EP-5 pgmq Job codec. Compiling this-component proves the scaffolded @Generated.…Reservation_work.Queue@ module+component proves the scaffolded @Generated.…ReservationWork.Queue@ module (the Job payload record + field->wire JSON codec + physical/dlq/table constants) is real, self-contained Haskell; running it proves the payload round-trips through encode/decode and the captured physical name is exposed.@@ -8,8 +8,8 @@ import Control.Monad (unless) import Data.Aeson (object, (.=))-import Generated.HospitalCapacity.Reservation_work.Queue-import Generated.HospitalCapacity.Reservation_work.QueueCodec (reservationWorkJobCodec)+import Generated.HospitalCapacity.ReservationWork.Queue+import Generated.HospitalCapacity.ReservationWork.QueueCodec (reservationWorkJobCodec) import Keiro.PGMQ.Codec (JobCodec (..)) import System.Exit (exitFailure)
+ test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModel.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.+module Generated.HospitalCapacity.TransferDecisions.ReadModel+ ( transferDecisionsReadModel+ , transferDecisionsQualifiedTable+ , registerTransferDecisions+ , startTransferDecisionsRebuild+ , finishTransferDecisionsRebuild+ , abandonTransferDecisionsRebuild+ , transferDecisionsAsyncProjection+ ) where++import Data.Functor (void)+import Effectful (Eff, (:>))+import Generated.HospitalCapacity.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable)+import HospitalCapacity.TransferDecisions.ReadModelHoles (TransferDecisionsQueryInput, TransferDecisionsQueryResult, transferDecisionsQuery, applyTransferDecisions)+import Keiro.Projection (AsyncProjection (..))+import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)+import Keiro.ReadModel.Rebuild qualified as Rebuild+import Kiroku.Store.Effect (Store)+import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))++transferDecisionsReadModel :: ReadModel TransferDecisionsQueryInput TransferDecisionsQueryResult+transferDecisionsReadModel =+ ReadModel+ { name = "hospital-capacity-transfer-decisions"+ , tableName = "transfer_decisions"+ , schema = "hospital_capacity"+ , subscriptionName = "hospital-capacity-transfer-decisions-sub"+ , version = 1+ , shapeHash = "fnv1a:3717f6d9e3c44bd6"+ , defaultConsistency = Strong+ , strongScope = CategoryHead "reservation"+ , query = transferDecisionsQuery+ }++-- Call once at projection startup before serving queries.+registerTransferDecisions :: (Store :> es) => Eff es ()+registerTransferDecisions =+ void (registerReadModel "hospital-capacity-transfer-decisions" 1 "fnv1a:3717f6d9e3c44bd6")++startTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata+startTransferDecisionsRebuild =+ Rebuild.startRebuild transferDecisionsReadModel ["hospital-capacity-transfer-decisions-async"]++finishTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)+finishTransferDecisionsRebuild =+ Rebuild.finishRebuild transferDecisionsReadModel ["hospital-capacity-transfer-decisions-async"]++abandonTransferDecisionsRebuild :: (Store :> es) => Eff es ReadModelMetadata+abandonTransferDecisionsRebuild = Rebuild.abandonRebuild transferDecisionsReadModel++transferDecisionsAsyncProjection :: AsyncProjection+transferDecisionsAsyncProjection =+ AsyncProjection+ { name = "hospital-capacity-transfer-decisions-async"+ , readModelName = "hospital-capacity-transfer-decisions"+ , subscriptionName = "hospital-capacity-transfer-decisions-sub"+ , applyRecorded = applyTransferDecisions+ , idempotencyKey = \recorded -> recorded.eventId+ }
+ test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModelHarness.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.+module Generated.HospitalCapacity.TransferDecisions.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where++import Generated.HospitalCapacity.TransferDecisions.ReadModel (transferDecisionsReadModel, transferDecisionsAsyncProjection)+import Data.Text qualified as T+import Keiro.ReadModel (ReadModel (..), StrongScope (..))+import Keiro.Projection (AsyncProjection (..))++-- | (fact, expected from notation, actual generated runtime value).+readModelFacts :: [(String, String, String)]+readModelFacts =+ [ ("registryName", "hospital-capacity-transfer-decisions", T.unpack transferDecisionsReadModel.name)+ , ("subscriptionName", "hospital-capacity-transfer-decisions-sub", T.unpack transferDecisionsReadModel.subscriptionName)+ , ("shapeHash", "fnv1a:3717f6d9e3c44bd6", T.unpack transferDecisionsReadModel.shapeHash)+ , ("asyncProjectionName", "hospital-capacity-transfer-decisions-async", T.unpack transferDecisionsAsyncProjection.name)+ , ("consistency", "Strong", show transferDecisionsReadModel.defaultConsistency)+ , ("strongScope", "CategoryHead reservation", renderStrongScope transferDecisionsReadModel.strongScope)+ ]++renderStrongScope :: StrongScope -> String+renderStrongScope EntireLog = "EntireLog"+renderStrongScope (CategoryHead categoryName) = "CategoryHead " <> T.unpack categoryName++readModelFactResults :: [(String, Bool)]+readModelFactResults =+ [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]++runReadModelFacts :: IO Bool+runReadModelFacts = do+ let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]+ mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures+ pure (null failures)
+ test/conformance-readmodel-runtime/Generated/HospitalCapacity/TransferDecisions/ReadModelTable.hs view
@@ -0,0 +1,9 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.+module Generated.HospitalCapacity.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable) where++import Data.Text (Text)+import Keiro.Connection (qualifyTable)++-- The fully-qualified, double-quoted data-table reference.+transferDecisionsQualifiedTable :: Text+transferDecisionsQualifiedTable = qualifyTable "hospital_capacity" "transfer_decisions"
− test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModel.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module Generated.HospitalCapacity.Transfer_decisions.ReadModel- ( transferDecisionsReadModel- , transferDecisionsQualifiedTable- , registerTransferDecisions- , startTransferDecisionsRebuild- , finishTransferDecisionsRebuild- , abandonTransferDecisionsRebuild- , transferDecisionsAsyncProjection- ) where--import Data.Functor (void)-import Effectful (Eff, (:>))-import Generated.HospitalCapacity.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable)-import HospitalCapacity.Transfer_decisions.ReadModelHoles (TransferDecisionsQueryInput, TransferDecisionsQueryResult, transferDecisionsQuery, applyTransferDecisions)-import Keiro.Projection (AsyncProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)-import Keiro.ReadModel.Rebuild qualified as Rebuild-import Kiroku.Store.Effect (Store)-import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))--transferDecisionsReadModel :: ReadModel TransferDecisionsQueryInput TransferDecisionsQueryResult-transferDecisionsReadModel =- ReadModel- { name = "hospital-capacity-transfer-decisions"- , tableName = "transfer_decisions"- , schema = "hospital_capacity"- , subscriptionName = "hospital-capacity-transfer-decisions-sub"- , version = 1- , shapeHash = "fnv1a:3717f6d9e3c44bd6"- , defaultConsistency = Strong- , strongScope = CategoryHead "reservation"- , query = transferDecisionsQuery- }---- Call once at projection startup before serving queries.-registerTransferDecisions :: (Store :> es) => Eff es ()-registerTransferDecisions =- void (registerReadModel "hospital-capacity-transfer-decisions" 1 "fnv1a:3717f6d9e3c44bd6")--startTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata-startTransferDecisionsRebuild =- Rebuild.startRebuild transferDecisionsReadModel ["hospital-capacity-transfer-decisions-async"]--finishTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)-finishTransferDecisionsRebuild =- Rebuild.finishRebuild transferDecisionsReadModel ["hospital-capacity-transfer-decisions-async"]--abandonTransferDecisionsRebuild :: (Store :> es) => Eff es ReadModelMetadata-abandonTransferDecisionsRebuild = Rebuild.abandonRebuild transferDecisionsReadModel--transferDecisionsAsyncProjection :: AsyncProjection-transferDecisionsAsyncProjection =- AsyncProjection- { name = "hospital-capacity-transfer-decisions-async"- , readModelName = "hospital-capacity-transfer-decisions"- , subscriptionName = "hospital-capacity-transfer-decisions-sub"- , applyRecorded = applyTransferDecisions- , idempotencyKey = \recorded -> recorded.eventId- }
− test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModelHarness.hs
@@ -1,19 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module Generated.HospitalCapacity.Transfer_decisions.ReadModelHarness (readModelFacts, runReadModelFacts) where---- | (fact, expected from notation, actual shared derivation/lowering).-readModelFacts :: [(String, String, String)]-readModelFacts =- [ ("registryName", "hospital-capacity-transfer-decisions", "hospital-capacity-transfer-decisions")- , ("subscriptionName", "hospital-capacity-transfer-decisions-sub", "hospital-capacity-transfer-decisions-sub")- , ("shapeHash", "fnv1a:3717f6d9e3c44bd6", "fnv1a:3717f6d9e3c44bd6")- , ("asyncProjectionName", "hospital-capacity-transfer-decisions-async", "hospital-capacity-transfer-decisions-async")- , ("consistency", "Strong", "Strong")- , ("strongScope", "CategoryHead reservation", "CategoryHead reservation")- ]--runReadModelFacts :: IO Bool-runReadModelFacts = do- let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]- mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures- pure (null failures)
− test/conformance-readmodel-runtime/Generated/HospitalCapacity/Transfer_decisions/ReadModelTable.hs
@@ -1,9 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module Generated.HospitalCapacity.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable) where--import Data.Text (Text)-import Keiro.Connection (qualifyTable)---- The fully-qualified, double-quoted data-table reference.-transferDecisionsQualifiedTable :: Text-transferDecisionsQualifiedTable = qualifyTable "hospital_capacity" "transfer_decisions"
+ test/conformance-readmodel-runtime/HospitalCapacity/TransferDecisions/ReadModelHoles.hs view
@@ -0,0 +1,30 @@+-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.+module HospitalCapacity.TransferDecisions.ReadModelHoles (+ TransferDecisionsQueryInput,+ TransferDecisionsQueryResult,+ transferDecisionsQuery,+ applyTransferDecisions,+) where++import Data.Text.Encoding qualified as Text+import Generated.HospitalCapacity.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable)+import Hasql.Transaction qualified as Tx+import Kiroku.Store.Types (RecordedEvent)++-- HOLE: replace these aliases with the real query input and result types.+type TransferDecisionsQueryInput = ()+type TransferDecisionsQueryResult = ()++-- HOLE: query "hospital_capacity"."transfer_decisions" via transferDecisionsQualifiedTable; never rely on search_path.+-- Declared columns:+-- reservation_id text NOT NULL+-- hospital_id text NOT NULL+-- status text NOT NULL+-- decided_at timestamptz+transferDecisionsQuery :: TransferDecisionsQueryInput -> Tx.Transaction TransferDecisionsQueryResult+transferDecisionsQuery _input =+ Tx.sql (Text.encodeUtf8 ("SELECT count(*) FROM " <> transferDecisionsQualifiedTable))++-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.+applyTransferDecisions :: RecordedEvent -> Tx.Transaction ()+applyTransferDecisions _recorded = pure ()
− test/conformance-readmodel-runtime/HospitalCapacity/Transfer_decisions/ReadModelHoles.hs
@@ -1,30 +0,0 @@--- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.-module HospitalCapacity.Transfer_decisions.ReadModelHoles (- TransferDecisionsQueryInput,- TransferDecisionsQueryResult,- transferDecisionsQuery,- applyTransferDecisions,-) where--import Data.Text.Encoding qualified as Text-import Generated.HospitalCapacity.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable)-import Hasql.Transaction qualified as Tx-import Kiroku.Store.Types (RecordedEvent)---- HOLE: replace these aliases with the real query input and result types.-type TransferDecisionsQueryInput = ()-type TransferDecisionsQueryResult = ()---- HOLE: query "hospital_capacity"."transfer_decisions" via transferDecisionsQualifiedTable; never rely on search_path.--- Declared columns:--- reservation_id text NOT NULL--- hospital_id text NOT NULL--- status text NOT NULL--- decided_at timestamptz-transferDecisionsQuery :: TransferDecisionsQueryInput -> Tx.Transaction TransferDecisionsQueryResult-transferDecisionsQuery _input =- Tx.sql (Text.encodeUtf8 ("SELECT count(*) FROM " <> transferDecisionsQualifiedTable))---- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.-applyTransferDecisions :: RecordedEvent -> Tx.Transaction ()-applyTransferDecisions _recorded = pure ()
test/conformance-readmodel-runtime/Main.hs view
@@ -4,8 +4,8 @@ import Control.Monad (unless) import Effectful (Eff, (:>))-import Generated.HospitalCapacity.Transfer_decisions.ReadModel-import Generated.HospitalCapacity.Transfer_decisions.ReadModelHarness (readModelFacts, runReadModelFacts)+import Generated.HospitalCapacity.TransferDecisions.ReadModel+import Generated.HospitalCapacity.TransferDecisions.ReadModelHarness (readModelFacts, runReadModelFacts) import Keiro.Projection (AsyncProjection (..)) import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), qualifiedTableName) import Keiro.ReadModel.Rebuild (RebuildError)
test/conformance-replay/Generated/ReplayDivergence/Note/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.Codec ( noteCodec, parseNoteEvent,@@ -7,8 +7,8 @@ ) where import Generated.ReplayDivergence.Note.Domain-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> o .: "noteText" <*> o .: "echo" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes noteEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes noteEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-replay/Generated/ReplayDivergence/Note/Domain.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.Domain where import Data.Proxy (Proxy (..))
test/conformance-replay/Generated/ReplayDivergence/Note/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.EventStream ( noteCategory , noteCommandCategory@@ -10,7 +10,7 @@ import Generated.ReplayDivergence.Note.Domain import Generated.ReplayDivergence.Note.Codec (noteCodec)-import Generated.ReplayDivergence.Note.Transducer (noteFoldFingerprint, noteTransducer)+import Generated.ReplayDivergence.Note.Transducer (noteTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-replay/Generated/ReplayDivergence/Note/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.Harness (harnessAssertions) where import Generated.ReplayDivergence.Note.Domain@@ -14,7 +14,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions noteTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: NoteWritten", roundTrips sampleEventNoteWritten) , ("accepts WriteNote from NoteEmpty", acceptWriteNote) ]@@ -24,11 +24,11 @@ roundTrips e = parseNoteEvent (eventType noteCodec e) (encodeNoteEvent e) == Right e sampleEventNoteWritten :: NoteEvent-sampleEventNoteWritten = (NoteWritten (NoteWrittenData "sample-noteText" "sample-echo"))+sampleEventNoteWritten = NoteWritten (NoteWrittenData "sample-noteText" "sample-echo") acceptWriteNote :: Bool acceptWriteNote =- case step noteTransducer (NoteEmpty, initialNoteRegs) ((WriteNote (WriteNoteData "sample-noteText" "sample-echo"))) of+ case step noteTransducer (NoteEmpty, initialNoteRegs) (WriteNote (WriteNoteData "sample-noteText" "sample-echo")) of Just (v, _, _) -> v == NoteRecorded Nothing -> False @@ -36,7 +36,7 @@ -- 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+ 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
test/conformance-replay/Generated/ReplayDivergence/Note/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-replay/Generated/ReplayDivergence/Note/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Note; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Note; do not edit. module Generated.ReplayDivergence.Note.Transducer ( noteTransducer , noteFoldFingerprint
test/conformance-replay/Generated/ReplayDivergence/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context replay-divergence replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context replay-divergence replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-replay/ReplayDivergence/Note/Holes.hs view
@@ -16,7 +16,7 @@ import Generated.ReplayDivergence.Note.Domain import Keiki.Builder ((=:)) import Keiki.Builder qualified as B-import Keiki.Core (HsPred, SymTransducer, WireCtor (..))+import Keiki.Core (HsPred, SymTransducer, WireCtor (..), unavailableWireCtor) -- HOLE: the transducer body. Reproduce the structure below, replacing each -- `-- HOLE` line with the keiki symbolic operators it describes.@@ -55,9 +55,10 @@ -- register. Only the generated forward/replay register comparison catches it. dishonestWireNoteWritten :: WireCtor NoteEvent (Text, (Text, ())) dishonestWireNoteWritten =- wireNoteWritten- { wcBuild = wcBuild wireNoteWritten . duplicateEcho- }+ unavailableWireCtor+ (wcName wireNoteWritten)+ (wcMatch wireNoteWritten)+ (wcBuild wireNoteWritten . duplicateEcho) duplicateEcho :: (Text, (Text, ())) -> (Text, (Text, ())) duplicateEcho (_noteText, (echo, ())) = (echo, (echo, ()))
test/conformance-router-full/Generated/IncidentPaging/Page/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module Generated.IncidentPaging.Page.Codec ( pageCodec, parsePageEvent,@@ -7,8 +7,8 @@ ) where import Generated.IncidentPaging.Page.Domain-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> o .: "incidentId" <*> o .: "responderId" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes pageEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes pageEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-router-full/Generated/IncidentPaging/Page/Domain.hs view
@@ -1,9 +1,8 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module Generated.IncidentPaging.Page.Domain where -import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))
test/conformance-router-full/Generated/IncidentPaging/Page/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module Generated.IncidentPaging.Page.EventStream ( pageCategory , pageCommandCategory@@ -10,7 +10,7 @@ import Generated.IncidentPaging.Page.Domain import Generated.IncidentPaging.Page.Codec (pageCodec)-import Generated.IncidentPaging.Page.Transducer (pageFoldFingerprint, pageTransducer)+import Generated.IncidentPaging.Page.Transducer (pageTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-router-full/Generated/IncidentPaging/Page/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module Generated.IncidentPaging.Page.Transducer ( pageTransducer , pageFoldFingerprint
test/conformance-router-full/Generated/IncidentPaging/PagingRouter/Router.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module Generated.IncidentPaging.PagingRouter.Router ( pagingRouterName , pagingRouterWorkerOptions
test/conformance-router-full/Generated/IncidentPaging/PagingRouter/RouterHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module Generated.IncidentPaging.PagingRouter.RouterHarness (routerHarnessValues) where routerHarnessValues :: [(String, String)]
test/conformance-router-full/Generated/IncidentPaging/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context incident-paging replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context incident-paging replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-router-runtime/Generated/IncidentPaging/PagingRouter/Router.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module Generated.IncidentPaging.PagingRouter.Router ( pagingRouterName , pagingRouterWorkerOptions
test/conformance-router-runtime/Generated/IncidentPaging/PagingRouter/RouterHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module Generated.IncidentPaging.PagingRouter.RouterHarness (routerHarnessValues) where routerHarnessValues :: [(String, String)]
test/conformance-router/Generated/IncidentPaging/PagingRouter/RouterHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module Generated.IncidentPaging.PagingRouter.RouterHarness (routerHarnessValues) where routerHarnessValues :: [(String, String)]
test/conformance-scalar-expressions/AggregateScalarExpressions/ScalarAccount/Holes.hs view
@@ -17,13 +17,12 @@ import Generated.AggregateScalarExpressions.ScalarAccount.Domain import Keiki.Builder qualified as B import Keiki.Generics (RegFieldsOf)-- import Keiki.Core qualified as K import Keiro.Snapshot.Codec (FoldVersion (..)) -- HOLE: add the predicate and ordered register updates for this transition. -- The generated transducer still owns command matching, mode, emits, and goto.+transition2ReviewedCloseHole :: B.PayloadProj ScalarAccountRegs ScalarAccountCommand (RegFieldsOf CloseData) -> B.EdgeBuilder ScalarAccountRegs ScalarAccountCommand ScalarAccountEvent ScalarAccountVertex ('Just (RegFieldsOf CloseData)) writes writes () transition2ReviewedCloseHole d = B.requireGuard (K.PEq (K.TApp1 id d.balance) d.balance)
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions generated nominal declarations; do not edit. module Generated.AggregateScalarExpressions.Nominals ( AccountMode (..) , accountModeText@@ -19,9 +19,9 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.AggregateScalarExpressions.Nominals.Internal (RequestId, mkRequestId, parseRequestId, requestIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)+import Keiki.ProjectionDomain (finiteProjectionDomain, TextPattern, textProjectionDomain) import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain) data AccountMode = Normal | Restricted@@ -54,7 +54,6 @@ accountModeEqualityWitness :: FieldWitness AccountModeEqualityProjection accountModeEqualityWitness = exactFieldWitness @AccountModeEqualityProjection -instance CanonicalTypeName RequestId data RequestIdEqualityProjection
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions generated nominal ID internals; do not edit. module Generated.AggregateScalarExpressions.Nominals.Internal ( RequestId , parseRequestId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype RequestId = RequestId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName RequestId instance ToJSON RequestId where toJSON = toJSON . requestIdText
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.-module Generated.AggregateScalarExpressions.ScalarAccount.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+module Generated.AggregateScalarExpressions.ScalarAccount.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.AggregateScalarExpressions.ScalarAccount.Codec (encodeScalarAccountEvent, parseScalarAccountEvent, scalarAccountCodec) import Generated.AggregateScalarExpressions.ScalarAccount.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,12 +126,84 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-1de91883b9e3f8bc") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountOpen "Close" Nothing Nothing [] 32- , BehaviorRequirement (BehaviorKey "behavior-v1-5c58e0aafeb5f400") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountReviewed "Adjust" Nothing Nothing [] 32- , BehaviorRequirement (BehaviorKey "behavior-v1-a19a8e94d935493f") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountClosed "Adjust" Nothing Nothing [] 32- , BehaviorRequirement (BehaviorKey "behavior-v1-b7db52f28632d8da") LiveTransition HoleWitnessed GuardTotal ScalarAccountReviewed "Close" (Just (K.EdgeRef ScalarAccountReviewed 0)) (Just ScalarAccountClosed) ["ClosedEvent"] 62- , BehaviorRequirement (BehaviorKey "behavior-v1-dbf483c868dd6d34") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountClosed "Close" Nothing Nothing [] 32- , BehaviorRequirement (BehaviorKey "behavior-v1-e9c863d5daf20011") LiveTransition GeneratedAuthoritative GuardUnknown ScalarAccountOpen "Adjust" (Just (K.EdgeRef ScalarAccountOpen 0)) (Just ScalarAccountReviewed) ["Adjusted"] 42+ [ -- ScalarAccountOpen x Close: required rejection (spec line 32)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-1de91883b9e3f8bc"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ScalarAccountOpen+ , requirementCommandName = "Close"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 32+ }+ , -- ScalarAccountReviewed x Adjust: required rejection (spec line 32)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-5c58e0aafeb5f400"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ScalarAccountReviewed+ , requirementCommandName = "Adjust"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 32+ }+ , -- ScalarAccountClosed x Adjust: required rejection (spec line 32)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-a19a8e94d935493f"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ScalarAccountClosed+ , requirementCommandName = "Adjust"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 32+ }+ , -- ScalarAccountReviewed x Close: live transition (spec line 62)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-b7db52f28632d8da"+ , requirementKind = LiveTransition+ , requirementEvidence = HoleWitnessed+ , requirementGuardCoverage = GuardTotal+ , requirementSource = ScalarAccountReviewed+ , requirementCommandName = "Close"+ , requirementExpectedEdge = (Just (K.EdgeRef ScalarAccountReviewed 0))+ , requirementTarget = Just ScalarAccountClosed+ , requirementEventKinds = ["ClosedEvent"]+ , requirementLine = 62+ }+ , -- ScalarAccountClosed x Close: required rejection (spec line 32)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-dbf483c868dd6d34"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ScalarAccountClosed+ , requirementCommandName = "Close"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 32+ }+ , -- ScalarAccountOpen x Adjust: live transition (spec line 42)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-e9c863d5daf20011"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = ScalarAccountOpen+ , requirementCommandName = "Adjust"+ , requirementExpectedEdge = (Just (K.EdgeRef ScalarAccountOpen 0))+ , requirementTarget = Just ScalarAccountReviewed+ , requirementEventKinds = ["Adjusted"]+ , requirementLine = 42+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -139,7 +228,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -166,7 +255,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -184,6 +273,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (ScalarAccountVertex, K.RegFile ScalarAccountRegs) -> ScalarAccountCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -193,6 +283,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (ScalarAccountVertex, K.RegFile ScalarAccountRegs) -> ScalarAccountCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither scalarAccountTransducer seed command of@@ -208,8 +299,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither scalarAccountTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -218,10 +309,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ScalarAccountRegs ScalarAccountVertex ScalarAccountEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [ScalarAccountEvent] -> [ScalarAccountEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -233,19 +325,21 @@ replayed <- case K.applyEventsDetailedEither scalarAccountTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ScalarAccountVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [ScalarAccountEvent] -> Either BehaviorFailure (K.ReplaySuccess ScalarAccountRegs ScalarAccountVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) decoded of@@ -255,31 +349,53 @@ decodeEvents :: [ScalarAccountEvent] -> Either Text [ScalarAccountEvent] decodeEvents = traverse (\event -> parseScalarAccountEvent (Codec.eventType scalarAccountCodec event) (encodeScalarAccountEvent event)) +commandKind :: ScalarAccountCommand -> Text commandKind command = case command of Adjust _ -> "Adjust" Close _ -> "Close" +eventKind :: ScalarAccountEvent -> Text eventKind event = case Codec.eventType scalarAccountCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile ScalarAccountRegs -> K.RegFile ScalarAccountRegs -> Bool regsEqual left right = (left K.! #balance) == (right K.! #balance) && (left K.! #reserved) == (right K.! #reserved) && (left K.! #capacity) == (right K.! #capacity) && (left K.! #machine) == (right K.! #machine) && (left K.! #label) == (right K.! #label) && (left K.! #active) == (right K.! #active) && (left K.! #mode) == (right K.! #mode) && (left K.! #requestId) == (right K.! #requestId) && (left K.! #openedAt) == (right K.! #openedAt) && (left K.! #limits) == (right K.! #limits) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.Codec ( scalarAccountCodec, parseScalarAccountEvent,@@ -9,17 +9,15 @@ ) where import Generated.AggregateScalarExpressions.ScalarAccount.Domain-import Generated.AggregateScalarExpressions.Nominals (AccountMode (..), accountModeText, RequestId, requestIdText)+import Generated.AggregateScalarExpressions.Nominals (AccountMode (..), accountModeText, requestIdText) import Generated.AggregateScalarExpressions.Nominals.Internal (unsafeRequestIdFromLegacyText) import Control.Monad (unless)-import Data.Aeson (Value (..), object, parseJSON, toJSON, withObject, withText, (.:), (.=))+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, explicitParseField, parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty-import Data.Map.Strict (Map)-import Data.Map.Strict qualified as Map import Data.Text (Text) import qualified Data.Text as T import Keiro.Codec.Structural (bindingFromShape, bindingToShape)@@ -119,13 +117,13 @@ <$> ( ClosedEventData <$> o .: "balance" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes scalarAccountEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes scalarAccountEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Domain.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.Domain where import Data.Aeson (FromJSON, ToJSON)@@ -12,7 +12,7 @@ import Keiki.Shape (CanonicalStateShape, CanonicalTypeName) import Generated.AggregateScalarExpressions.Nominals (AccountMode (..), RequestId, parseRequestId) import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime) import Numeric.Natural (Natural) import ScalarExpressions.Bindings qualified as Bindings import ScalarExpressions.Domain (Limits)
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.EventStream ( scalarAccountCategory , scalarAccountCommandCategory
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.Harness (harnessAssertions) where import Generated.AggregateScalarExpressions.ScalarAccount.Domain@@ -11,17 +11,16 @@ 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.Either (isLeft) 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) import Generated.AggregateScalarExpressions.StructuralProjections qualified as StructuralProjections import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)+import Data.Time.Clock (UTCTime (..), picosecondsToDiffTime) import Generated.AggregateScalarExpressions.Structural.Shape.Limits qualified as ShapeLimits import ScalarExpressions.Bindings qualified as Bindings import ScalarExpressions.Domain (Limits)@@ -32,7 +31,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions scalarAccountTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: Adjusted", roundTrips sampleEventAdjusted) , ("golden round-trip: ClosedEvent", roundTrips sampleEventClosedEvent) , ("accepts Adjust from ScalarAccountOpen", acceptAdjust)@@ -43,15 +42,24 @@ roundTrips :: ScalarAccountEvent -> Bool roundTrips e = parseScalarAccountEvent (eventType scalarAccountCodec e) (encodeScalarAccountEvent e) == Right e +sampleRequestId :: RequestId+sampleRequestId =+ case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleObservedAt :: UTCTime+sampleObservedAt = UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)+ sampleEventAdjusted :: ScalarAccountEvent-sampleEventAdjusted = (Adjusted (AdjustedData 0 0 0 "sample-label" False Normal (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) (snd (NonEmpty.head (fixtureCases Bindings.limitsCases)))))+sampleEventAdjusted = Adjusted (AdjustedData 0 0 0 "sample-label" False Normal sampleRequestId sampleObservedAt (snd (NonEmpty.head (fixtureCases Bindings.limitsCases)))) sampleEventClosedEvent :: ScalarAccountEvent-sampleEventClosedEvent = (ClosedEvent (ClosedEventData 0))+sampleEventClosedEvent = ClosedEvent (ClosedEventData 0) acceptAdjust :: Bool acceptAdjust =- case step scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) ((Adjust (AdjustData 0 0 0 "sample-label" False Normal (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) (snd (NonEmpty.head (fixtureCases Bindings.limitsCases)))))) of+ case step scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) (Adjust (AdjustData 0 0 0 "sample-label" False Normal sampleRequestId sampleObservedAt (snd (NonEmpty.head (fixtureCases Bindings.limitsCases))))) of Just (v, _, _) -> v == ScalarAccountReviewed Nothing -> False @@ -59,7 +67,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayAdjust :: [(String, Bool)] forwardReplayAdjust =- case step scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) ((Adjust (AdjustData 0 0 0 "sample-label" False Normal (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) (snd (NonEmpty.head (fixtureCases Bindings.limitsCases)))))) of+ case step scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) (Adjust (AdjustData 0 0 0 "sample-label" False Normal sampleRequestId sampleObservedAt (snd (NonEmpty.head (fixtureCases Bindings.limitsCases))))) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseScalarAccountEvent (eventType scalarAccountCodec event) (encodeScalarAccountEvent event)) emitted of@@ -117,7 +125,7 @@ adjustedLimitsAssertions :: [(String, Bool)] adjustedLimitsAssertions =- [ ("mapped codec round-trip: Adjusted/limits/" <> T.unpack label, roundTrips (Adjusted (AdjustedData 0 0 0 "sample-label" False Normal (case parseRequestId "req_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012)) mappedValue)))+ [ ("mapped codec round-trip: Adjusted/limits/" <> T.unpack label, roundTrips (Adjusted (AdjustedData 0 0 0 "sample-label" False Normal sampleRequestId sampleObservedAt mappedValue))) | (label, mappedValue) <- NonEmpty.toList (fixtureCases Bindings.limitsCases) ] @@ -132,14 +140,6 @@ , ("projection witness agreement: scalar-expressions.Limits.v1/minimum", all (\(_, owner) -> fieldWitnessAgrees StructuralProjections.limitsMinimumWitness (\referenceOwner -> ShapeLimits.minimum (bindingToShape Bindings.limitsBinding referenceOwner)) owner) (NonEmpty.toList (fixtureCases Bindings.limitsCases))) ] -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-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ScalarAccount; do not edit. module Generated.AggregateScalarExpressions.ScalarAccount.Transducer ( scalarAccountTransducer , scalarAccountFoldFingerprint
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/Structural/Shape/Limits.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural Limits; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural Limits; do not edit. module Generated.AggregateScalarExpressions.Structural.Shape.Limits (LimitsShape (..)) where import GHC.Generics (Generic)
test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/StructuralProjections.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions mapped structural facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context aggregate-scalar-expressions mapped structural facade; do not edit. -- Equality witnesses are emitted for Text, Int, Bool, Natural, and UTCTime. -- Int, Natural, and UTCTime belong to Keiki's ordered subset. module Generated.AggregateScalarExpressions.StructuralProjections@@ -7,8 +7,6 @@ , limitsMinimumWitness ) where -import Data.Text (Text)-import Data.Time (UTCTime) import Numeric.Natural (Natural) import Keiro.Codec.Structural (bindingToShape) import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness)
test/conformance-scalar-expressions/Main.hs view
@@ -174,9 +174,11 @@ accepts command = case K.step scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) command of Just {} -> True Nothing -> False- withMode value (Adjust command) = Adjust command {mode = value}+ withMode value (Adjust (AdjustData balanceValue requestedValue machineValue labelValue activeValue _ requestIdValue' observedAtValue limitsValue)) =+ Adjust (AdjustData balanceValue requestedValue machineValue labelValue activeValue value requestIdValue' observedAtValue limitsValue) withMode _ command = command- withRequestId value (Adjust command) = Adjust command {requestId = value}+ withRequestId value (Adjust (AdjustData balanceValue requestedValue machineValue labelValue activeValue modeValue _ observedAtValue limitsValue)) =+ Adjust (AdjustData balanceValue requestedValue machineValue labelValue activeValue modeValue value observedAtValue limitsValue) withRequestId _ command = command exactEnumProjectionProof :: IO Bool
test/conformance-skeletons/SkelAggregate/Generated/MyService/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service generated nominal declarations; do not edit. module SkelAggregate.Generated.MyService.Nominals ( ThingId , parseThingId@@ -6,11 +6,4 @@ , thingIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import SkelAggregate.Generated.MyService.Nominals.Internal (ThingId, mkThingId, parseThingId, thingIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName ThingId
test/conformance-skeletons/SkelAggregate/Generated/MyService/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service generated nominal ID internals; do not edit. module SkelAggregate.Generated.MyService.Nominals.Internal ( ThingId , parseThingId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype ThingId = ThingId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName ThingId instance ToJSON ThingId where toJSON = toJSON . thingIdText
test/conformance-skeletons/SkelAggregate/Generated/MyService/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.-module SkelAggregate.Generated.MyService.Thing.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+module SkelAggregate.Generated.MyService.Thing.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import SkelAggregate.Generated.MyService.Thing.Codec (encodeThingEvent, parseThingEvent, thingCodec) import SkelAggregate.Generated.MyService.Thing.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,8 +126,32 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-9879aa7cdadb6004") RequiredRejection GeneratedAuthoritative GuardNotApplicable ThingDone "DoThing" Nothing Nothing [] 9- , BehaviorRequirement (BehaviorKey "behavior-v1-c1b356b00484077a") LiveTransition GeneratedAuthoritative GuardTotal ThingPending "DoThing" (Just (K.EdgeRef ThingPending 0)) (Just ThingDone) ["ThingCompleted"] 14+ [ -- ThingDone x DoThing: required rejection (spec line 9)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-9879aa7cdadb6004"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ThingDone+ , requirementCommandName = "DoThing"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 9+ }+ , -- ThingPending x DoThing: live transition (spec line 14)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-c1b356b00484077a"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = ThingPending+ , requirementCommandName = "DoThing"+ , requirementExpectedEdge = (Just (K.EdgeRef ThingPending 0))+ , requirementTarget = Just ThingDone+ , requirementEventKinds = ["ThingCompleted"]+ , requirementLine = 14+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -135,7 +176,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -162,7 +203,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -180,6 +221,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (ThingVertex, K.RegFile ThingRegs) -> ThingCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -189,6 +231,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (ThingVertex, K.RegFile ThingRegs) -> ThingCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither thingTransducer seed command of@@ -204,8 +247,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither thingTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -214,10 +257,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ThingRegs ThingVertex ThingEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [ThingEvent] -> [ThingEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -229,19 +273,21 @@ replayed <- case K.applyEventsDetailedEither thingTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ThingVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [ThingEvent] -> Either BehaviorFailure (K.ReplaySuccess ThingRegs ThingVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither thingTransducer (ThingPending, initialThingRegs) decoded of@@ -251,30 +297,52 @@ decodeEvents :: [ThingEvent] -> Either Text [ThingEvent] decodeEvents = traverse (\event -> parseThingEvent (Codec.eventType thingCodec event) (encodeThingEvent event)) +commandKind :: ThingCommand -> Text commandKind command = case command of DoThing _ -> "DoThing" +eventKind :: ThingEvent -> Text eventKind event = case Codec.eventType thingCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile ThingRegs -> K.RegFile ThingRegs -> Bool regsEqual left right = (left K.! #thingId) == (right K.! #thingId) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.Codec ( thingCodec, parseThingEvent,@@ -7,10 +7,10 @@ ) where import SkelAggregate.Generated.MyService.Thing.Domain-import SkelAggregate.Generated.MyService.Nominals (ThingId, thingIdText)+import SkelAggregate.Generated.MyService.Nominals (thingIdText) import SkelAggregate.Generated.MyService.Nominals.Internal (unsafeThingIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -56,13 +56,13 @@ <$> (unsafeThingIdFromLegacyText <$> o .: "thingId") <*> o .: "attempt" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes thingEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes thingEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Domain.hs view
@@ -1,10 +1,9 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import SkelAggregate.Generated.MyService.Nominals (ThingId, parseThingId)
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.EventStream ( thingCategory , thingCommandCategory@@ -10,7 +10,7 @@ import SkelAggregate.Generated.MyService.Thing.Domain import SkelAggregate.Generated.MyService.Thing.Codec (thingCodec)-import SkelAggregate.Generated.MyService.Thing.Transducer (thingFoldFingerprint, thingTransducer)+import SkelAggregate.Generated.MyService.Thing.Transducer (thingTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.Harness (harnessAssertions) where import SkelAggregate.Generated.MyService.Thing.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions thingTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: ThingCompleted", roundTrips sampleEventThingCompleted) , ("accepts DoThing from ThingPending", acceptDoThing) ]@@ -24,12 +24,18 @@ roundTrips :: ThingEvent -> Bool roundTrips e = parseThingEvent (eventType thingCodec e) (encodeThingEvent e) == Right e +sampleThingId :: ThingId+sampleThingId =+ case parseThingId "thing_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventThingCompleted :: ThingEvent-sampleEventThingCompleted = (ThingCompleted (ThingCompletedData (case parseThingId "thing_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0))+sampleEventThingCompleted = ThingCompleted (ThingCompletedData sampleThingId 0) acceptDoThing :: Bool acceptDoThing =- case step thingTransducer (ThingPending, initialThingRegs) ((DoThing (DoThingData (case parseThingId "thing_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0))) of+ case step thingTransducer (ThingPending, initialThingRegs) (DoThing (DoThingData sampleThingId 0)) of Just (v, _, _) -> v == ThingDone Nothing -> False @@ -37,7 +43,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayDoThing :: [(String, Bool)] forwardReplayDoThing =- case step thingTransducer (ThingPending, initialThingRegs) ((DoThing (DoThingData (case parseThingId "thing_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0))) of+ case step thingTransducer (ThingPending, initialThingRegs) (DoThing (DoThingData sampleThingId 0)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseThingEvent (eventType thingCodec event) (encodeThingEvent event)) emitted of
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-skeletons/SkelAggregate/Generated/MyService/Thing/Transducer.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Thing; do not edit. module SkelAggregate.Generated.MyService.Thing.Transducer ( thingTransducer , thingFoldFingerprint
test/conformance-skeletons/SkelAggregate/MyService/Thing/BehaviorHoles.hs view
@@ -5,6 +5,6 @@ behaviorWitnesses :: [BehaviorWitness] behaviorWitnesses =- [ Pending (BehaviorKey "behavior-v1-9879aa7cdadb6004")- , Pending (BehaviorKey "behavior-v1-c1b356b00484077a")+ [ Pending (BehaviorKey "behavior-v1-9879aa7cdadb6004") -- ThingDone x DoThing: required rejection (spec line 9)+ , Pending (BehaviorKey "behavior-v1-c1b356b00484077a") -- ThingPending x DoThing: live transition (spec line 14) ]
test/conformance-skeletons/SkelContract/Generated/MyService/MyContract/Contract.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from contract myContract; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from contract myContract; do not edit. module SkelContract.Generated.MyService.MyContract.Contract ( MyContractPayload (..) , ThingHappenedData (..)
test/conformance-skeletons/SkelEmit/Generated/MyService/MyContract/Contract.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from contract myContract; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from contract myContract; do not edit. module SkelEmit.Generated.MyService.MyContract.Contract ( MyContractPayload (..) , ThingAcceptedData (..)@@ -9,7 +9,7 @@ , parseMyContractPayload ) where -import Data.Aeson (Value, object, withObject, withText, (.:), (.=))+import Data.Aeson (Value, object, withObject, withText, (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither) import Data.KindID (KindID) import qualified Data.KindID as KindID
test/conformance-skeletons/SkelEmit/Generated/MyService/ThingPublisher/Publisher.hs view
@@ -1,11 +1,11 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from publisher thingPublisher; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from publisher thingPublisher; do not edit. module SkelEmit.Generated.MyService.ThingPublisher.Publisher ( publisherOrdering , publisherBackoff , publisherMaxAttempts ) where -import Keiro.Outbox.Types (BackoffSchedule (..), ExponentialBackoffOptions (..), OrderingPolicy (..))+import Keiro.Outbox.Types (BackoffSchedule (..), OrderingPolicy (..)) publisherOrdering :: OrderingPolicy publisherOrdering = PerKeyHeadOfLine
test/conformance-skeletons/SkelIntake/Generated/MyService/MyContract/Contract.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from contract myContract; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from contract myContract; do not edit. module SkelIntake.Generated.MyService.MyContract.Contract ( MyContractPayload (..) , ThingHappenedData (..)@@ -9,7 +9,7 @@ , parseMyContractPayload ) where -import Data.Aeson (Value, object, withObject, withText, (.:), (.=))+import Data.Aeson (Value, object, withObject, withText, (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither) import Data.KindID (KindID) import qualified Data.KindID as KindID
test/conformance-skeletons/SkelIntake/Generated/MyService/ThingInbox/Inbox.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from intake thingInbox; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from intake thingInbox; do not edit. module SkelIntake.Generated.MyService.ThingInbox.Inbox ( InboxFailure (..) , ThingInboxOutcome (..)
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.Codec ( hospitalCodec, parseHospitalEvent,@@ -7,10 +7,10 @@ ) where import SkelProcess.Generated.MyService.Hospital.Domain-import SkelProcess.Generated.MyService.Nominals (HospitalId, hospitalIdText)+import SkelProcess.Generated.MyService.Nominals (hospitalIdText) import SkelProcess.Generated.MyService.Nominals.Internal (unsafeHospitalIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> ( SurgeActivatedData <$> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes hospitalEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes hospitalEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Domain.hs view
@@ -1,13 +1,11 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.Domain where -import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import SkelProcess.Generated.MyService.Nominals (HospitalId, parseHospitalId)+import SkelProcess.Generated.MyService.Nominals (HospitalId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data HospitalVertex = HospitalOperational | HospitalSurging
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.EventStream ( hospitalCategory , hospitalCommandCategory@@ -10,7 +10,7 @@ import SkelProcess.Generated.MyService.Hospital.Domain import SkelProcess.Generated.MyService.Hospital.Codec (hospitalCodec)-import SkelProcess.Generated.MyService.Hospital.Transducer (hospitalFoldFingerprint, hospitalTransducer)+import SkelProcess.Generated.MyService.Hospital.Transducer (hospitalTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Harness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.Harness (harnessAssertions) where import SkelProcess.Generated.MyService.Hospital.Domain@@ -14,7 +14,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions hospitalTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: SurgeActivated", roundTrips sampleEventSurgeActivated) , ("accepts ActivateSurge from HospitalOperational", acceptActivateSurge) ]@@ -23,12 +23,18 @@ roundTrips :: HospitalEvent -> Bool roundTrips e = parseHospitalEvent (eventType hospitalCodec e) (encodeHospitalEvent e) == Right e +sampleHospitalId :: HospitalId+sampleHospitalId =+ case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventSurgeActivated :: HospitalEvent-sampleEventSurgeActivated = (SurgeActivated (SurgeActivatedData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))+sampleEventSurgeActivated = SurgeActivated (SurgeActivatedData sampleHospitalId) acceptActivateSurge :: Bool acceptActivateSurge =- case step hospitalTransducer (HospitalOperational, initialHospitalRegs) ((ActivateSurge (ActivateSurgeData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))) of+ case step hospitalTransducer (HospitalOperational, initialHospitalRegs) (ActivateSurge (ActivateSurgeData sampleHospitalId)) of Just (v, _, _) -> v == HospitalSurging Nothing -> False @@ -36,7 +42,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayActivateSurge :: [(String, Bool)] forwardReplayActivateSurge =- case step hospitalTransducer (HospitalOperational, initialHospitalRegs) ((ActivateSurge (ActivateSurgeData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))) of+ case step hospitalTransducer (HospitalOperational, initialHospitalRegs) (ActivateSurge (ActivateSurgeData sampleHospitalId)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, _forwardRegs, emitted) -> case mapM (\event -> parseHospitalEvent (eventType hospitalCodec event) (encodeHospitalEvent event)) emitted of
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-skeletons/SkelProcess/Generated/MyService/Hospital/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Hospital; do not edit. module SkelProcess.Generated.MyService.Hospital.Transducer ( hospitalTransducer , hospitalFoldFingerprint
test/conformance-skeletons/SkelProcess/Generated/MyService/HospitalSurge/Process.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit. module SkelProcess.Generated.MyService.HospitalSurge.Process ( hospitalSurgeProcessName , hospitalSurgeCategory
test/conformance-skeletons/SkelProcess/Generated/MyService/HospitalSurge/ProcessHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from process HospitalSurge; do not edit. module SkelProcess.Generated.MyService.HospitalSurge.ProcessHarness (processHarnessValues) where -- | (label, value): the spec's deterministic process/timer decisions,
test/conformance-skeletons/SkelProcess/Generated/MyService/Nominals.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service generated nominal declarations; do not edit. module SkelProcess.Generated.MyService.Nominals ( CommandId , parseCommandId@@ -10,13 +10,4 @@ , hospitalIdText ) where -import Data.Aeson (FromJSON, ToJSON)-import Data.Text (Text)-import GHC.Generics (Generic)-import Keiki.Shape (CanonicalTypeName) import SkelProcess.Generated.MyService.Nominals.Internal (CommandId, mkCommandId, parseCommandId, commandIdText, HospitalId, mkHospitalId, parseHospitalId, hospitalIdText)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)--instance CanonicalTypeName CommandId--instance CanonicalTypeName HospitalId
test/conformance-skeletons/SkelProcess/Generated/MyService/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service generated nominal ID internals; do not edit. module SkelProcess.Generated.MyService.Nominals.Internal ( CommandId , parseCommandId@@ -16,11 +16,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CommandId = CommandId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CommandId+ instance ToJSON CommandId where toJSON = toJSON . commandIdText @@ -43,6 +46,8 @@ newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName HospitalId instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText
test/conformance-skeletons/SkelProcess/Generated/MyService/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.Codec ( surgeCodec, parseSurgeEvent,@@ -7,10 +7,10 @@ ) where import SkelProcess.Generated.MyService.Surge.Domain-import SkelProcess.Generated.MyService.Nominals (HospitalId, hospitalIdText)+import SkelProcess.Generated.MyService.Nominals (hospitalIdText) import SkelProcess.Generated.MyService.Nominals.Internal (unsafeHospitalIdFromLegacyText)-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -73,13 +73,13 @@ <$> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") <*> o .: "timerId" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes surgeEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes surgeEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Domain.hs view
@@ -1,13 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.Domain where -import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import SkelProcess.Generated.MyService.Nominals (HospitalId, parseHospitalId)+import SkelProcess.Generated.MyService.Nominals (HospitalId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data SurgeVertex = SurgeIdle | SurgeFired
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.EventStream ( surgeCategory , surgeCommandCategory@@ -10,7 +10,7 @@ import SkelProcess.Generated.MyService.Surge.Domain import SkelProcess.Generated.MyService.Surge.Codec (surgeCodec)-import SkelProcess.Generated.MyService.Surge.Transducer (surgeFoldFingerprint, surgeTransducer)+import SkelProcess.Generated.MyService.Surge.Transducer (surgeTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Harness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.Harness (harnessAssertions) where import SkelProcess.Generated.MyService.Surge.Domain@@ -14,7 +14,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions surgeTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: SurgeThresholdNoted", roundTrips sampleEventSurgeThresholdNoted) , ("golden round-trip: SurgeTimerMarked", roundTrips sampleEventSurgeTimerMarked) , ("accepts NoteSurgeThreshold from SurgeIdle", acceptNoteSurgeThreshold)@@ -26,21 +26,27 @@ roundTrips :: SurgeEvent -> Bool roundTrips e = parseSurgeEvent (eventType surgeCodec e) (encodeSurgeEvent e) == Right e +sampleHospitalId :: HospitalId+sampleHospitalId =+ case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventSurgeThresholdNoted :: SurgeEvent-sampleEventSurgeThresholdNoted = (SurgeThresholdNoted (SurgeThresholdNotedData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0 0 "sample-timerId"))+sampleEventSurgeThresholdNoted = SurgeThresholdNoted (SurgeThresholdNotedData sampleHospitalId 0 0 "sample-timerId") sampleEventSurgeTimerMarked :: SurgeEvent-sampleEventSurgeTimerMarked = (SurgeTimerMarked (SurgeTimerMarkedData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") "sample-timerId"))+sampleEventSurgeTimerMarked = SurgeTimerMarked (SurgeTimerMarkedData sampleHospitalId "sample-timerId") acceptNoteSurgeThreshold :: Bool acceptNoteSurgeThreshold =- case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((NoteSurgeThreshold (NoteSurgeThresholdData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0 0 "sample-timerId"))) of+ case step surgeTransducer (SurgeIdle, initialSurgeRegs) (NoteSurgeThreshold (NoteSurgeThresholdData sampleHospitalId 0 0 "sample-timerId")) of Just (v, _, _) -> v == SurgeIdle Nothing -> False acceptMarkSurgeTimerFired :: Bool acceptMarkSurgeTimerFired =- case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((MarkSurgeTimerFired (MarkSurgeTimerFiredData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") "sample-timerId"))) of+ case step surgeTransducer (SurgeIdle, initialSurgeRegs) (MarkSurgeTimerFired (MarkSurgeTimerFiredData sampleHospitalId "sample-timerId")) of Just (v, _, _) -> v == SurgeFired Nothing -> False @@ -48,7 +54,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayNoteSurgeThreshold :: [(String, Bool)] forwardReplayNoteSurgeThreshold =- case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((NoteSurgeThreshold (NoteSurgeThresholdData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") 0 0 "sample-timerId"))) of+ case step surgeTransducer (SurgeIdle, initialSurgeRegs) (NoteSurgeThreshold (NoteSurgeThresholdData sampleHospitalId 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@@ -66,7 +72,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayMarkSurgeTimerFired :: [(String, Bool)] forwardReplayMarkSurgeTimerFired =- case step surgeTransducer (SurgeIdle, initialSurgeRegs) ((MarkSurgeTimerFired (MarkSurgeTimerFiredData (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") "sample-timerId"))) of+ case step surgeTransducer (SurgeIdle, initialSurgeRegs) (MarkSurgeTimerFired (MarkSurgeTimerFiredData sampleHospitalId "sample-timerId")) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, _forwardRegs, emitted) -> case mapM (\event -> parseSurgeEvent (eventType surgeCodec event) (encodeSurgeEvent event)) emitted of
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-skeletons/SkelProcess/Generated/MyService/Surge/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Surge; do not edit. module SkelProcess.Generated.MyService.Surge.Transducer ( surgeTransducer , surgeFoldFingerprint
+ test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModel.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel acceptedTransferNeeds; do not edit.+module SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModel+ ( acceptedTransferNeedsReadModel+ , acceptedTransferNeedsQualifiedTable+ , registerAcceptedTransferNeeds+ , startAcceptedTransferNeedsRebuild+ , finishAcceptedTransferNeedsRebuild+ , abandonAcceptedTransferNeedsRebuild+ , acceptedTransferNeedsAsyncProjection+ ) where++import Data.Functor (void)+import Effectful (Eff, (:>))+import SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelTable (acceptedTransferNeedsQualifiedTable)+import SkelQueue.MyService.AcceptedTransferNeeds.ReadModelHoles (AcceptedTransferNeedsQueryInput, AcceptedTransferNeedsQueryResult, acceptedTransferNeedsQuery, applyAcceptedTransferNeeds)+import Keiro.Projection (AsyncProjection (..))+import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)+import Keiro.ReadModel.Rebuild qualified as Rebuild+import Kiroku.Store.Effect (Store)+import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))++acceptedTransferNeedsReadModel :: ReadModel AcceptedTransferNeedsQueryInput AcceptedTransferNeedsQueryResult+acceptedTransferNeedsReadModel =+ ReadModel+ { name = "my-service-acceptedTransferNeeds"+ , tableName = "accepted_transfer_needs"+ , schema = "my_service"+ , subscriptionName = "my-service-acceptedTransferNeeds-sub"+ , version = 1+ , shapeHash = "fnv1a:fec517dae7760b8a"+ , defaultConsistency = Eventual+ , strongScope = EntireLog+ , query = acceptedTransferNeedsQuery+ }++-- Call once at projection startup before serving queries.+registerAcceptedTransferNeeds :: (Store :> es) => Eff es ()+registerAcceptedTransferNeeds =+ void (registerReadModel "my-service-acceptedTransferNeeds" 1 "fnv1a:fec517dae7760b8a")++startAcceptedTransferNeedsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata+startAcceptedTransferNeedsRebuild =+ Rebuild.startRebuild acceptedTransferNeedsReadModel ["my-service-acceptedTransferNeeds-async"]++finishAcceptedTransferNeedsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)+finishAcceptedTransferNeedsRebuild =+ Rebuild.finishRebuild acceptedTransferNeedsReadModel ["my-service-acceptedTransferNeeds-async"]++abandonAcceptedTransferNeedsRebuild :: (Store :> es) => Eff es ReadModelMetadata+abandonAcceptedTransferNeedsRebuild = Rebuild.abandonRebuild acceptedTransferNeedsReadModel++acceptedTransferNeedsAsyncProjection :: AsyncProjection+acceptedTransferNeedsAsyncProjection =+ AsyncProjection+ { name = "my-service-acceptedTransferNeeds-async"+ , readModelName = "my-service-acceptedTransferNeeds"+ , subscriptionName = "my-service-acceptedTransferNeeds-sub"+ , applyRecorded = applyAcceptedTransferNeeds+ , idempotencyKey = \recorded -> recorded.eventId+ }
+ test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModelHarness.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel acceptedTransferNeeds; do not edit.+module SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where++import SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModel (acceptedTransferNeedsReadModel, acceptedTransferNeedsAsyncProjection)+import Data.Text qualified as T+import Keiro.ReadModel (ReadModel (..), StrongScope (..))+import Keiro.Projection (AsyncProjection (..))++-- | (fact, expected from notation, actual generated runtime value).+readModelFacts :: [(String, String, String)]+readModelFacts =+ [ ("registryName", "my-service-acceptedTransferNeeds", T.unpack acceptedTransferNeedsReadModel.name)+ , ("subscriptionName", "my-service-acceptedTransferNeeds-sub", T.unpack acceptedTransferNeedsReadModel.subscriptionName)+ , ("shapeHash", "fnv1a:fec517dae7760b8a", T.unpack acceptedTransferNeedsReadModel.shapeHash)+ , ("asyncProjectionName", "my-service-acceptedTransferNeeds-async", T.unpack acceptedTransferNeedsAsyncProjection.name)+ , ("consistency", "Eventual", show acceptedTransferNeedsReadModel.defaultConsistency)+ , ("strongScope", "EntireLog", renderStrongScope acceptedTransferNeedsReadModel.strongScope)+ ]++renderStrongScope :: StrongScope -> String+renderStrongScope EntireLog = "EntireLog"+renderStrongScope (CategoryHead categoryName) = "CategoryHead " <> T.unpack categoryName++readModelFactResults :: [(String, Bool)]+readModelFactResults =+ [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]++runReadModelFacts :: IO Bool+runReadModelFacts = do+ let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]+ mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures+ pure (null failures)
+ test/conformance-skeletons/SkelQueue/Generated/MyService/AcceptedTransferNeeds/ReadModelTable.hs view
@@ -0,0 +1,9 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel acceptedTransferNeeds; do not edit.+module SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelTable (acceptedTransferNeedsQualifiedTable) where++import Data.Text (Text)+import Keiro.Connection (qualifyTable)++-- The fully-qualified, double-quoted data-table reference.+acceptedTransferNeedsQualifiedTable :: Text+acceptedTransferNeedsQualifiedTable = qualifyTable "my_service" "accepted_transfer_needs"
− test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModel.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel accepted_transfer_needs; do not edit.-module SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModel- ( acceptedTransferNeedsReadModel- , acceptedTransferNeedsQualifiedTable- , registerAcceptedTransferNeeds- , startAcceptedTransferNeedsRebuild- , finishAcceptedTransferNeedsRebuild- , abandonAcceptedTransferNeedsRebuild- , acceptedTransferNeedsAsyncProjection- ) where--import Data.Functor (void)-import Effectful (Eff, (:>))-import SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelTable (acceptedTransferNeedsQualifiedTable)-import SkelQueue.MyService.Accepted_transfer_needs.ReadModelHoles (AcceptedTransferNeedsQueryInput, AcceptedTransferNeedsQueryResult, acceptedTransferNeedsQuery, applyAcceptedTransferNeeds)-import Keiro.Projection (AsyncProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)-import Keiro.ReadModel.Rebuild qualified as Rebuild-import Kiroku.Store.Effect (Store)-import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))--acceptedTransferNeedsReadModel :: ReadModel AcceptedTransferNeedsQueryInput AcceptedTransferNeedsQueryResult-acceptedTransferNeedsReadModel =- ReadModel- { name = "my-service-accepted-transfer-needs"- , tableName = "accepted_transfer_needs"- , schema = "my_service"- , subscriptionName = "my-service-accepted-transfer-needs-sub"- , version = 1- , shapeHash = "fnv1a:fec517dae7760b8a"- , defaultConsistency = Eventual- , strongScope = EntireLog- , query = acceptedTransferNeedsQuery- }---- Call once at projection startup before serving queries.-registerAcceptedTransferNeeds :: (Store :> es) => Eff es ()-registerAcceptedTransferNeeds =- void (registerReadModel "my-service-accepted-transfer-needs" 1 "fnv1a:fec517dae7760b8a")--startAcceptedTransferNeedsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata-startAcceptedTransferNeedsRebuild =- Rebuild.startRebuild acceptedTransferNeedsReadModel ["my-service-accepted-transfer-needs-async"]--finishAcceptedTransferNeedsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)-finishAcceptedTransferNeedsRebuild =- Rebuild.finishRebuild acceptedTransferNeedsReadModel ["my-service-accepted-transfer-needs-async"]--abandonAcceptedTransferNeedsRebuild :: (Store :> es) => Eff es ReadModelMetadata-abandonAcceptedTransferNeedsRebuild = Rebuild.abandonRebuild acceptedTransferNeedsReadModel--acceptedTransferNeedsAsyncProjection :: AsyncProjection-acceptedTransferNeedsAsyncProjection =- AsyncProjection- { name = "my-service-accepted-transfer-needs-async"- , readModelName = "my-service-accepted-transfer-needs"- , subscriptionName = "my-service-accepted-transfer-needs-sub"- , applyRecorded = applyAcceptedTransferNeeds- , idempotencyKey = \recorded -> recorded.eventId- }
− test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModelHarness.hs
@@ -1,23 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel accepted_transfer_needs; do not edit.-module SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where---- | (fact, expected from notation, actual shared derivation/lowering).-readModelFacts :: [(String, String, String)]-readModelFacts =- [ ("registryName", "my-service-accepted-transfer-needs", "my-service-accepted-transfer-needs")- , ("subscriptionName", "my-service-accepted-transfer-needs-sub", "my-service-accepted-transfer-needs-sub")- , ("shapeHash", "fnv1a:fec517dae7760b8a", "fnv1a:fec517dae7760b8a")- , ("asyncProjectionName", "my-service-accepted-transfer-needs-async", "my-service-accepted-transfer-needs-async")- , ("consistency", "Eventual", "Eventual")- , ("strongScope", "EntireLog", "EntireLog")- ]--readModelFactResults :: [(String, Bool)]-readModelFactResults =- [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]--runReadModelFacts :: IO Bool-runReadModelFacts = do- let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]- mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures- pure (null failures)
− test/conformance-skeletons/SkelQueue/Generated/MyService/Accepted_transfer_needs/ReadModelTable.hs
@@ -1,9 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel accepted_transfer_needs; do not edit.-module SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelTable (acceptedTransferNeedsQualifiedTable) where--import Data.Text (Text)-import Keiro.Connection (qualifyTable)---- The fully-qualified, double-quoted data-table reference.-acceptedTransferNeedsQualifiedTable :: Text-acceptedTransferNeedsQualifiedTable = qualifyTable "my_service" "accepted_transfer_needs"
+ test/conformance-skeletons/SkelQueue/Generated/MyService/ReservationWork/Queue.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservationWork; do not edit.+module SkelQueue.Generated.MyService.ReservationWork.Queue+ ( ReservationWorkItem (..)+ , encodeReservationWorkItem+ , parseReservationWorkItem+ , queuePhysical, queueDlq, queueTable++ ) 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 = "my_service_reservation_work"+queueDlq = "my_service_reservation_work_dlq"+queueTable = "pgmq.q_my_service_reservation_work"++data ReservationWorkItem = ReservationWorkItem+ { reservationId :: !Text+ , hospitalId :: !Text+ }+ deriving stock (Eq, Show)++encodeReservationWorkItem :: ReservationWorkItem -> Value+encodeReservationWorkItem p =+ object+ [ "reservation_id" .= p.reservationId+ , "hospital_id" .= p.hospitalId+ ]++parseReservationWorkItem :: Value -> Either Text ReservationWorkItem+parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)+ where+ go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id"++mapLeftText :: Either String b -> Either Text b+mapLeftText = either (Left . T.pack) Right
+ test/conformance-skeletons/SkelQueue/Generated/MyService/ReservationWork/QueueCodec.hs view
@@ -0,0 +1,29 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservationWork; do not edit.+-- | 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.ReservationWork.QueueCodec (reservationWorkPayloadCodec, reservationWorkJobCodec) where++import Data.List.NonEmpty (NonEmpty (..))+import Keiro.Codec (Codec (..), EventType (..))+import Keiro.PGMQ.Codec (JobCodec, keiroJobCodec)+import SkelQueue.Generated.MyService.ReservationWork.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/SkelQueue/Generated/MyService/ReservationWork/QueuePolicy.hs view
@@ -0,0 +1,43 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workqueue reservationWork; do not edit.+module SkelQueue.Generated.MyService.ReservationWork.QueuePolicy+ ( ReservationWorkOutcome (..)+ , retryPolicy, jobOutcomeFor+ , jobOrdering, jobTuningFor, queueProvision+ ) where++import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, QueueProvision, RetryDelay (..), RetryPolicy (..), standardProvision, withOrdering)++jobOrdering :: JobOrdering+jobOrdering = Unordered++-- 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 = standardProvision++retryPolicy :: RetryPolicy+retryPolicy =+ RetryPolicy+ { maxRetries = 3+ , defaultRetryDelay = RetryDelay 5+ , useDeadLetter = True+ }++-- The consumer JobOutcome disposition over the spec's named domain outcomes,+-- lowered to the live Keiro.PGMQ.Job.JobOutcome.+data ReservationWorkOutcome+ = StoreFailure+ | CommandRejected+ | DecodeFailure+ | OnCodecReject+ deriving stock (Eq, Show)++jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome+jobOutcomeFor o = case o of+ StoreFailure -> Retry (RetryDelay 5)+ CommandRejected -> Dead "dead-lettered"+ DecodeFailure -> Dead "dead-lettered"+ OnCodecReject -> Dead "dead-lettered"
− test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/Queue.hs
@@ -1,40 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module SkelQueue.Generated.MyService.Reservation_work.Queue- ( ReservationWorkItem (..)- , encodeReservationWorkItem- , parseReservationWorkItem- , queuePhysical, queueDlq, queueTable-- ) 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 = "my_service_reservation_work"-queueDlq = "my_service_reservation_work_dlq"-queueTable = "pgmq.q_my_service_reservation_work"--data ReservationWorkItem = ReservationWorkItem- { reservationId :: !Text- , hospitalId :: !Text- }- deriving stock (Eq, Show)--encodeReservationWorkItem :: ReservationWorkItem -> Value-encodeReservationWorkItem p =- object- [ "reservation_id" .= p.reservationId- , "hospital_id" .= p.hospitalId- ]--parseReservationWorkItem :: Value -> Either Text ReservationWorkItem-parseReservationWorkItem = mapLeftText . parseEither (withObject "ReservationWorkItem" go)- where- go o = ReservationWorkItem <$> o .: "reservation_id" <*> o .: "hospital_id"--mapLeftText :: Either String b -> Either Text b-mapLeftText = either (Left . T.pack) Right
− test/conformance-skeletons/SkelQueue/Generated/MyService/Reservation_work/QueueCodec.hs
@@ -1,29 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.--- | 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/SkelQueue/Generated/MyService/Reservation_work/QueuePolicy.hs
@@ -1,43 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workqueue reservation_work; do not edit.-module SkelQueue.Generated.MyService.Reservation_work.QueuePolicy- ( ReservationWorkOutcome (..)- , retryPolicy, jobOutcomeFor- , jobOrdering, jobTuningFor, queueProvision- ) where--import Keiro.PGMQ.Job (JobOrdering (..), JobOutcome (..), JobTuning, PartitionSpec (..), QueueProvision, RetryDelay (..), RetryPolicy (..), partitionedProvision, standardProvision, unloggedProvision, withFifoIndexProvision, withOrdering)--jobOrdering :: JobOrdering-jobOrdering = Unordered---- 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 = standardProvision--retryPolicy :: RetryPolicy-retryPolicy =- RetryPolicy- { maxRetries = 3- , defaultRetryDelay = RetryDelay 5- , useDeadLetter = True- }---- The consumer JobOutcome disposition over the spec's named domain outcomes,--- lowered to the live Keiro.PGMQ.Job.JobOutcome.-data ReservationWorkOutcome- = StoreFailure- | CommandRejected- | DecodeFailure- | OnCodecReject- deriving stock (Eq, Show)--jobOutcomeFor :: ReservationWorkOutcome -> JobOutcome-jobOutcomeFor o = case o of- StoreFailure -> Retry (RetryDelay 5)- CommandRejected -> Dead "dead-lettered"- DecodeFailure -> Dead "dead-lettered"- OnCodecReject -> Dead "dead-lettered"
+ test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModel.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transferDecisions; do not edit.+module SkelQueue.Generated.MyService.TransferDecisions.ReadModel+ ( transferDecisionsReadModel+ , transferDecisionsQualifiedTable+ , registerTransferDecisions+ , startTransferDecisionsRebuild+ , finishTransferDecisionsRebuild+ , abandonTransferDecisionsRebuild+ , transferDecisionsAsyncProjection+ ) where++import Data.Functor (void)+import Effectful (Eff, (:>))+import SkelQueue.Generated.MyService.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable)+import SkelQueue.MyService.TransferDecisions.ReadModelHoles (TransferDecisionsQueryInput, TransferDecisionsQueryResult, transferDecisionsQuery, applyTransferDecisions)+import Keiro.Projection (AsyncProjection (..))+import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)+import Keiro.ReadModel.Rebuild qualified as Rebuild+import Kiroku.Store.Effect (Store)+import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))++transferDecisionsReadModel :: ReadModel TransferDecisionsQueryInput TransferDecisionsQueryResult+transferDecisionsReadModel =+ ReadModel+ { name = "my-service-transferDecisions"+ , tableName = "transfer_decisions"+ , schema = "my_service"+ , subscriptionName = "my-service-transferDecisions-sub"+ , version = 1+ , shapeHash = "fnv1a:d44d218822582783"+ , defaultConsistency = Eventual+ , strongScope = EntireLog+ , query = transferDecisionsQuery+ }++-- Call once at projection startup before serving queries.+registerTransferDecisions :: (Store :> es) => Eff es ()+registerTransferDecisions =+ void (registerReadModel "my-service-transferDecisions" 1 "fnv1a:d44d218822582783")++startTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata+startTransferDecisionsRebuild =+ Rebuild.startRebuild transferDecisionsReadModel ["my-service-transferDecisions-async"]++finishTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)+finishTransferDecisionsRebuild =+ Rebuild.finishRebuild transferDecisionsReadModel ["my-service-transferDecisions-async"]++abandonTransferDecisionsRebuild :: (Store :> es) => Eff es ReadModelMetadata+abandonTransferDecisionsRebuild = Rebuild.abandonRebuild transferDecisionsReadModel++transferDecisionsAsyncProjection :: AsyncProjection+transferDecisionsAsyncProjection =+ AsyncProjection+ { name = "my-service-transferDecisions-async"+ , readModelName = "my-service-transferDecisions"+ , subscriptionName = "my-service-transferDecisions-sub"+ , applyRecorded = applyTransferDecisions+ , idempotencyKey = \recorded -> recorded.eventId+ }
+ test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModelHarness.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedRecordDot #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transferDecisions; do not edit.+module SkelQueue.Generated.MyService.TransferDecisions.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where++import SkelQueue.Generated.MyService.TransferDecisions.ReadModel (transferDecisionsReadModel, transferDecisionsAsyncProjection)+import Data.Text qualified as T+import Keiro.ReadModel (ReadModel (..), StrongScope (..))+import Keiro.Projection (AsyncProjection (..))++-- | (fact, expected from notation, actual generated runtime value).+readModelFacts :: [(String, String, String)]+readModelFacts =+ [ ("registryName", "my-service-transferDecisions", T.unpack transferDecisionsReadModel.name)+ , ("subscriptionName", "my-service-transferDecisions-sub", T.unpack transferDecisionsReadModel.subscriptionName)+ , ("shapeHash", "fnv1a:d44d218822582783", T.unpack transferDecisionsReadModel.shapeHash)+ , ("asyncProjectionName", "my-service-transferDecisions-async", T.unpack transferDecisionsAsyncProjection.name)+ , ("consistency", "Eventual", show transferDecisionsReadModel.defaultConsistency)+ , ("strongScope", "EntireLog", renderStrongScope transferDecisionsReadModel.strongScope)+ ]++renderStrongScope :: StrongScope -> String+renderStrongScope EntireLog = "EntireLog"+renderStrongScope (CategoryHead categoryName) = "CategoryHead " <> T.unpack categoryName++readModelFactResults :: [(String, Bool)]+readModelFactResults =+ [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]++runReadModelFacts :: IO Bool+runReadModelFacts = do+ let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]+ mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures+ pure (null failures)
+ test/conformance-skeletons/SkelQueue/Generated/MyService/TransferDecisions/ReadModelTable.hs view
@@ -0,0 +1,9 @@+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from readmodel transferDecisions; do not edit.+module SkelQueue.Generated.MyService.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable) where++import Data.Text (Text)+import Keiro.Connection (qualifyTable)++-- The fully-qualified, double-quoted data-table reference.+transferDecisionsQualifiedTable :: Text+transferDecisionsQualifiedTable = qualifyTable "my_service" "transfer_decisions"
− test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModel.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module SkelQueue.Generated.MyService.Transfer_decisions.ReadModel- ( transferDecisionsReadModel- , transferDecisionsQualifiedTable- , registerTransferDecisions- , startTransferDecisionsRebuild- , finishTransferDecisionsRebuild- , abandonTransferDecisionsRebuild- , transferDecisionsAsyncProjection- ) where--import Data.Functor (void)-import Effectful (Eff, (:>))-import SkelQueue.Generated.MyService.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable)-import SkelQueue.MyService.Transfer_decisions.ReadModelHoles (TransferDecisionsQueryInput, TransferDecisionsQueryResult, transferDecisionsQuery, applyTransferDecisions)-import Keiro.Projection (AsyncProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), ReadModelMetadata, StrongScope (..), registerReadModel)-import Keiro.ReadModel.Rebuild qualified as Rebuild-import Kiroku.Store.Effect (Store)-import Kiroku.Store.Types (GlobalPosition, RecordedEvent (..))--transferDecisionsReadModel :: ReadModel TransferDecisionsQueryInput TransferDecisionsQueryResult-transferDecisionsReadModel =- ReadModel- { name = "my-service-transfer-decisions"- , tableName = "transfer_decisions"- , schema = "my_service"- , subscriptionName = "my-service-transfer-decisions-sub"- , version = 1- , shapeHash = "fnv1a:d44d218822582783"- , defaultConsistency = Eventual- , strongScope = EntireLog- , query = transferDecisionsQuery- }---- Call once at projection startup before serving queries.-registerTransferDecisions :: (Store :> es) => Eff es ()-registerTransferDecisions =- void (registerReadModel "my-service-transfer-decisions" 1 "fnv1a:d44d218822582783")--startTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es ReadModelMetadata-startTransferDecisionsRebuild =- Rebuild.startRebuild transferDecisionsReadModel ["my-service-transfer-decisions-async"]--finishTransferDecisionsRebuild :: (Store :> es) => GlobalPosition -> Eff es (Either Rebuild.RebuildError ReadModelMetadata)-finishTransferDecisionsRebuild =- Rebuild.finishRebuild transferDecisionsReadModel ["my-service-transfer-decisions-async"]--abandonTransferDecisionsRebuild :: (Store :> es) => Eff es ReadModelMetadata-abandonTransferDecisionsRebuild = Rebuild.abandonRebuild transferDecisionsReadModel--transferDecisionsAsyncProjection :: AsyncProjection-transferDecisionsAsyncProjection =- AsyncProjection- { name = "my-service-transfer-decisions-async"- , readModelName = "my-service-transfer-decisions"- , subscriptionName = "my-service-transfer-decisions-sub"- , applyRecorded = applyTransferDecisions- , idempotencyKey = \recorded -> recorded.eventId- }
− test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModelHarness.hs
@@ -1,23 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module SkelQueue.Generated.MyService.Transfer_decisions.ReadModelHarness (readModelFacts, readModelFactResults, runReadModelFacts) where---- | (fact, expected from notation, actual shared derivation/lowering).-readModelFacts :: [(String, String, String)]-readModelFacts =- [ ("registryName", "my-service-transfer-decisions", "my-service-transfer-decisions")- , ("subscriptionName", "my-service-transfer-decisions-sub", "my-service-transfer-decisions-sub")- , ("shapeHash", "fnv1a:d44d218822582783", "fnv1a:d44d218822582783")- , ("asyncProjectionName", "my-service-transfer-decisions-async", "my-service-transfer-decisions-async")- , ("consistency", "Eventual", "Eventual")- , ("strongScope", "EntireLog", "EntireLog")- ]--readModelFactResults :: [(String, Bool)]-readModelFactResults =- [(fact, expected == actual) | (fact, expected, actual) <- readModelFacts]--runReadModelFacts :: IO Bool-runReadModelFacts = do- let failures = [(fact, expected, actual) | (fact, expected, actual) <- readModelFacts, expected /= actual]- mapM_ (\(fact, expected, actual) -> putStrLn ("FAIL " <> fact <> " expected=" <> show expected <> " actual=" <> show actual)) failures- pure (null failures)
− test/conformance-skeletons/SkelQueue/Generated/MyService/Transfer_decisions/ReadModelTable.hs
@@ -1,9 +0,0 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from readmodel transfer_decisions; do not edit.-module SkelQueue.Generated.MyService.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable) where--import Data.Text (Text)-import Keiro.Connection (qualifyTable)---- The fully-qualified, double-quoted data-table reference.-transferDecisionsQualifiedTable :: Text-transferDecisionsQualifiedTable = qualifyTable "my_service" "transfer_decisions"
+ test/conformance-skeletons/SkelQueue/MyService/AcceptedTransferNeeds/ReadModelHoles.hs view
@@ -0,0 +1,26 @@+-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.+module SkelQueue.MyService.AcceptedTransferNeeds.ReadModelHoles (+ AcceptedTransferNeedsQueryInput,+ AcceptedTransferNeedsQueryResult,+ acceptedTransferNeedsQuery,+ applyAcceptedTransferNeeds,+) where++import Hasql.Transaction qualified as Tx+import Kiroku.Store.Types (RecordedEvent)+import SkelQueue.Generated.MyService.AcceptedTransferNeeds.ReadModelTable (acceptedTransferNeedsQualifiedTable)++-- HOLE: replace these aliases with the real query input and result types.+type AcceptedTransferNeedsQueryInput = ()+type AcceptedTransferNeedsQueryResult = ()++-- HOLE: query "my_service"."accepted_transfer_needs" via acceptedTransferNeedsQualifiedTable; never rely on search_path.+-- Declared columns:+-- reservation_id text NOT NULL+-- hospital_id text NOT NULL+acceptedTransferNeedsQuery :: AcceptedTransferNeedsQueryInput -> Tx.Transaction AcceptedTransferNeedsQueryResult+acceptedTransferNeedsQuery _input = acceptedTransferNeedsQualifiedTable `seq` error "HOLE: fill accepted_transfer_needs query"++-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.+applyAcceptedTransferNeeds :: RecordedEvent -> Tx.Transaction ()+applyAcceptedTransferNeeds _recorded = error "HOLE: fill accepted_transfer_needs async apply"
− test/conformance-skeletons/SkelQueue/MyService/Accepted_transfer_needs/ReadModelHoles.hs
@@ -1,26 +0,0 @@--- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.-module SkelQueue.MyService.Accepted_transfer_needs.ReadModelHoles (- AcceptedTransferNeedsQueryInput,- AcceptedTransferNeedsQueryResult,- acceptedTransferNeedsQuery,- applyAcceptedTransferNeeds,-) where--import Hasql.Transaction qualified as Tx-import Kiroku.Store.Types (RecordedEvent)-import SkelQueue.Generated.MyService.Accepted_transfer_needs.ReadModelTable (acceptedTransferNeedsQualifiedTable)---- HOLE: replace these aliases with the real query input and result types.-type AcceptedTransferNeedsQueryInput = ()-type AcceptedTransferNeedsQueryResult = ()---- HOLE: query "my_service"."accepted_transfer_needs" via acceptedTransferNeedsQualifiedTable; never rely on search_path.--- Declared columns:--- reservation_id text NOT NULL--- hospital_id text NOT NULL-acceptedTransferNeedsQuery :: AcceptedTransferNeedsQueryInput -> Tx.Transaction AcceptedTransferNeedsQueryResult-acceptedTransferNeedsQuery _input = acceptedTransferNeedsQualifiedTable `seq` error "HOLE: fill accepted_transfer_needs query"---- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.-applyAcceptedTransferNeeds :: RecordedEvent -> Tx.Transaction ()-applyAcceptedTransferNeeds _recorded = error "HOLE: fill accepted_transfer_needs async apply"
+ test/conformance-skeletons/SkelQueue/MyService/TransferDecisions/ReadModelHoles.hs view
@@ -0,0 +1,25 @@+-- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.+module SkelQueue.MyService.TransferDecisions.ReadModelHoles (+ TransferDecisionsQueryInput,+ TransferDecisionsQueryResult,+ transferDecisionsQuery,+ applyTransferDecisions,+) where++import Hasql.Transaction qualified as Tx+import Kiroku.Store.Types (RecordedEvent)+import SkelQueue.Generated.MyService.TransferDecisions.ReadModelTable (transferDecisionsQualifiedTable)++-- HOLE: replace these aliases with the real query input and result types.+type TransferDecisionsQueryInput = ()+type TransferDecisionsQueryResult = ()++-- HOLE: query "my_service"."transfer_decisions" via transferDecisionsQualifiedTable; never rely on search_path.+-- Declared columns:+-- reservation_id text NOT NULL+transferDecisionsQuery :: TransferDecisionsQueryInput -> Tx.Transaction TransferDecisionsQueryResult+transferDecisionsQuery _input = transferDecisionsQualifiedTable `seq` error "HOLE: fill transfer_decisions query"++-- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.+applyTransferDecisions :: RecordedEvent -> Tx.Transaction ()+applyTransferDecisions _recorded = error "HOLE: fill transfer_decisions async apply"
− test/conformance-skeletons/SkelQueue/MyService/Transfer_decisions/ReadModelHoles.hs
@@ -1,25 +0,0 @@--- This is a HAND-OWNED hole module. keiro-dsl creates it once and never overwrites it.-module SkelQueue.MyService.Transfer_decisions.ReadModelHoles (- TransferDecisionsQueryInput,- TransferDecisionsQueryResult,- transferDecisionsQuery,- applyTransferDecisions,-) where--import Hasql.Transaction qualified as Tx-import Kiroku.Store.Types (RecordedEvent)-import SkelQueue.Generated.MyService.Transfer_decisions.ReadModelTable (transferDecisionsQualifiedTable)---- HOLE: replace these aliases with the real query input and result types.-type TransferDecisionsQueryInput = ()-type TransferDecisionsQueryResult = ()---- HOLE: query "my_service"."transfer_decisions" via transferDecisionsQualifiedTable; never rely on search_path.--- Declared columns:--- reservation_id text NOT NULL-transferDecisionsQuery :: TransferDecisionsQueryInput -> Tx.Transaction TransferDecisionsQueryResult-transferDecisionsQuery _input = transferDecisionsQualifiedTable `seq` error "HOLE: fill transfer_decisions query"---- HOLE: apply one recorded event; runtime deduplication makes redelivery safe.-applyTransferDecisions :: RecordedEvent -> Tx.Transaction ()-applyTransferDecisions _recorded = error "HOLE: fill transfer_decisions async apply"
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.Codec ( pageCodec, parsePageEvent,@@ -7,8 +7,8 @@ ) where import SkelRouter.Generated.MyService.Page.Domain-import Data.Aeson (Value, object, withObject, withText, (.:), (.=))-import Data.Aeson.Types (Parser, explicitParseField, parseEither)+import Data.Aeson (Value, object, withObject, (.:), (.=))+import Data.Aeson.Types (parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text)@@ -54,13 +54,13 @@ <$> o .: "incidentId" <*> o .: "responderId" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes pageEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes pageEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Domain.hs view
@@ -1,9 +1,8 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.Domain where -import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.EventStream ( pageCategory , pageCommandCategory@@ -10,7 +10,7 @@ import SkelRouter.Generated.MyService.Page.Domain import SkelRouter.Generated.MyService.Page.Codec (pageCodec)-import SkelRouter.Generated.MyService.Page.Transducer (pageFoldFingerprint, pageTransducer)+import SkelRouter.Generated.MyService.Page.Transducer (pageTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Harness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.Harness (harnessAssertions) where import SkelRouter.Generated.MyService.Page.Domain@@ -13,7 +13,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions pageTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: PageSent", roundTrips sampleEventPageSent) , ("accepts SendPage from PagePending", acceptSendPage) ]@@ -23,11 +23,11 @@ roundTrips e = parsePageEvent (eventType pageCodec e) (encodePageEvent e) == Right e sampleEventPageSent :: PageEvent-sampleEventPageSent = (PageSent (PageSentData "sample-incidentId" "sample-responderId"))+sampleEventPageSent = PageSent (PageSentData "sample-incidentId" "sample-responderId") acceptSendPage :: Bool acceptSendPage =- case step pageTransducer (PagePending, initialPageRegs) ((SendPage (SendPageData "sample-incidentId" "sample-responderId"))) of+ case step pageTransducer (PagePending, initialPageRegs) (SendPage (SendPageData "sample-incidentId" "sample-responderId")) of Just (v, _, _) -> v == PageDelivered Nothing -> False @@ -35,7 +35,7 @@ -- 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+ 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
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-skeletons/SkelRouter/Generated/MyService/Page/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Page; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Page; do not edit. module SkelRouter.Generated.MyService.Page.Transducer ( pageTransducer , pageFoldFingerprint
test/conformance-skeletons/SkelRouter/Generated/MyService/PagingRouter/Router.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module SkelRouter.Generated.MyService.PagingRouter.Router ( pagingRouterName , pagingRouterWorkerOptions
test/conformance-skeletons/SkelRouter/Generated/MyService/PagingRouter/RouterHarness.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from router PagingRouter; do not edit. module SkelRouter.Generated.MyService.PagingRouter.RouterHarness (routerHarnessValues) where routerHarnessValues :: [(String, String)]
test/conformance-skeletons/SkelRouter/Generated/MyService/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context my-service replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-skeletons/SkelWorkflow/Generated/MyService/HospitalTransferReservation/WorkflowFacts.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit. module SkelWorkflow.Generated.MyService.HospitalTransferReservation.WorkflowFacts (WorkflowFacts (..), workflowFacts, workflowFactValues) where -- | The workflow's deterministic decisions, pinned as typed pure facts.
test/conformance-skeletons/SkelWorkflow/Generated/MyService/HospitalTransferReservation/WorkflowRuntime.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit. module SkelWorkflow.Generated.MyService.HospitalTransferReservation.WorkflowRuntime ( workflowName , awaitAwakeableId
test/conformance-snapshot/Generated/HospitalCapacity/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit. module Generated.HospitalCapacity.Nominals ( BedType (..) , bedTypeText@@ -29,10 +29,9 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.HospitalCapacity.Nominals.Internal (CommandId, mkCommandId, parseCommandId, commandIdText, HospitalId, mkHospitalId, parseHospitalId, hospitalIdText, TransferReservationId, mkTransferReservationId, parseTransferReservationId, transferReservationIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)+import Keiki.ProjectionDomain (finiteProjectionDomain) data BedType = Icu | MedicalSurgical deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)@@ -45,8 +44,8 @@ Icu -> "icu" MedicalSurgical -> "medical-surgical" -instance CanonicalTypeName CommandId + data DivertStatus = Open | PartialDivert | TotalDivert deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -79,8 +78,8 @@ divertStatusEqualityWitness :: FieldWitness DivertStatusEqualityProjection divertStatusEqualityWitness = exactFieldWitness @DivertStatusEqualityProjection -instance CanonicalTypeName HospitalId + data PatientAcuity = RedTag | YellowTag | GreenTag deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -92,5 +91,3 @@ RedTag -> "red" YellowTag -> "yellow" GreenTag -> "green"--instance CanonicalTypeName TransferReservationId
test/conformance-snapshot/Generated/HospitalCapacity/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit. module Generated.HospitalCapacity.Nominals.Internal ( CommandId , parseCommandId@@ -21,11 +21,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CommandId = CommandId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CommandId+ instance ToJSON CommandId where toJSON = toJSON . commandIdText @@ -49,6 +52,8 @@ newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName HospitalId+ instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText @@ -71,6 +76,8 @@ newtype TransferReservationId = TransferReservationId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName TransferReservationId instance ToJSON TransferReservationId where toJSON = toJSON . transferReservationIdText
test/conformance-snapshot/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Codec ( reservationCodec, parseReservationEvent,@@ -7,7 +7,7 @@ ) where import Generated.HospitalCapacity.Reservation.Domain-import Generated.HospitalCapacity.Nominals (CommandId, commandIdText, DivertStatus (..), divertStatusText, HospitalId, hospitalIdText, PatientAcuity (..), patientAcuityText, TransferReservationId, transferReservationIdText)+import Generated.HospitalCapacity.Nominals (commandIdText, DivertStatus (..), divertStatusText, hospitalIdText, PatientAcuity (..), patientAcuityText, transferReservationIdText) import Generated.HospitalCapacity.Nominals.Internal (unsafeCommandIdFromLegacyText, unsafeHospitalIdFromLegacyText, unsafeTransferReservationIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -91,13 +91,13 @@ <*> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") <*> (unsafeCommandIdFromLegacyText <$> o .: "commandId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes reservationEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes reservationEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Domain.hs view
@@ -1,16 +1,15 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Domain where import Data.Aeson (FromJSON, ToJSON) import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Keiki.Shape (CanonicalStateShape, CanonicalTypeName)-import Generated.HospitalCapacity.Nominals (CommandId, parseCommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId)+import Generated.HospitalCapacity.Nominals (CommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data ReservationVertex = ReservationUnrequested | ReservationHeld | ReservationConfirmed | ReservationExpired | ReservationAdmitted | ReservationReleased
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.EventStream ( reservationCategory , reservationCommandCategory
test/conformance-snapshot/Generated/HospitalCapacity/Reservation/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Transducer ( reservationTransducer , reservationFoldFingerprint
test/conformance-snapshot/HospitalCapacity/Reservation/Holes.hs view
@@ -6,7 +6,7 @@ module HospitalCapacity.Reservation.Holes ( transition2HeldConfirmReservationOutput1TransferReservationConfirmed,- applyTransfer_decisions,+ applyTransferDecisions, ) where @@ -22,5 +22,5 @@ commandId = command.commandId } -applyTransfer_decisions :: ReservationEvent -> recorded -> txn ()-applyTransfer_decisions _event _recorded = error "HOLE: fill transfer_decisions projection apply"+applyTransferDecisions :: ReservationEvent -> recorded -> txn ()+applyTransferDecisions _event _recorded = error "HOLE: fill transfer_decisions projection apply"
+ test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/BehaviorContract.hs view
@@ -0,0 +1,348 @@+{-# LANGUAGE OverloadedLabels #-}+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+module Generated.StructuralConformance.ArtifactCatalog.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where++import Generated.StructuralConformance.ArtifactCatalog.Codec (encodeArtifactCatalogEvent, parseArtifactCatalogEvent, artifactCatalogCodec)+import Generated.StructuralConformance.ArtifactCatalog.Domain+import Generated.StructuralConformance.ArtifactCatalog.Transducer (artifactCatalogTransducer)+import Data.Aeson (ToJSON (..), object, (.=))+import Data.List (sortOn)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither, (!))+import Keiro.Codec qualified as Codec (Codec (eventType), EventType (..))++newtype BehaviorKey = BehaviorKey { unBehaviorKey :: Text }+ deriving stock (Eq, Ord, Show)++data ObligationKind = LiveTransition | RequiredRejection | ReplayTransition+ deriving stock (Eq, Ord, Show)++data EvidenceLevel = GeneratedAuthoritative | HoleWitnessed | LegacyRuntimeWitness+ deriving stock (Eq, Ord, Show)++data GuardCoverage = GuardTotal | GuardPartial | GuardUnknown | GuardNotApplicable+ deriving stock (Eq, Ord, Show)++data BehaviorRequirement = BehaviorRequirement+ { requirementKey :: !BehaviorKey+ , requirementKind :: !ObligationKind+ , requirementEvidence :: !EvidenceLevel+ , requirementGuardCoverage :: !GuardCoverage+ , requirementSource :: !ArtifactCatalogVertex+ , requirementCommandName :: !Text+ , requirementExpectedEdge :: !(Maybe (K.EdgeRef ArtifactCatalogVertex))+ , requirementTarget :: !(Maybe ArtifactCatalogVertex)+ , requirementEventKinds :: ![Text]+ , requirementLine :: !Int+ }+ deriving stock (Eq, Show)++data RejectionClass = RejectNoOutgoingEdges | RejectNoMatchingEdge+ deriving stock (Eq, Show)++data LiveExpectation+ = Emits (NonEmpty ArtifactCatalogEvent)+ | Rejects RejectionClass+ | NoOp+ deriving stock (Eq, Show)++data BehaviorWitness+ = Pending BehaviorKey+ | LiveWitness+ { witnessKey :: BehaviorKey+ , witnessHistory :: [ArtifactCatalogEvent]+ , witnessCommand :: ArtifactCatalogCommand+ , witnessExpected :: LiveExpectation+ }+ | ReplayWitness+ { witnessKey :: BehaviorKey+ , witnessHistoryPrefix :: [ArtifactCatalogEvent]+ , witnessObservedChunk :: [ArtifactCatalogEvent]+ }+ deriving stock (Eq, Show)++data BehaviorFailure = BehaviorFailure+ { failureKey :: !BehaviorKey+ , failureSubject :: !Text+ , failureCode :: !Text+ , failureDetail :: !Text+ }+ deriving stock (Eq, Show)++instance ToJSON BehaviorFailure where+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure+ ]++data BehaviorConformanceReport = BehaviorConformanceReport+ { reportRequired :: ![BehaviorKey]+ , reportFilled :: ![BehaviorKey]+ , reportPending :: ![BehaviorKey]+ , reportMissing :: ![BehaviorKey]+ , reportDuplicate :: ![BehaviorKey]+ , reportStale :: ![BehaviorKey]+ , reportFailed :: ![BehaviorFailure]+ , reportVerified :: ![BehaviorKey]+ , reportUnverified :: ![BehaviorKey]+ }+ deriving stock (Eq, Show)++instance ToJSON BehaviorConformanceReport where+ toJSON report = object+ [ "schema" .= ("keiro/behavior-conformance/1" :: Text)+ , "required" .= keyTexts (reportRequired report)+ , "filled" .= keyTexts (reportFilled report)+ , "pending" .= keyTexts (reportPending report)+ , "missing" .= keyTexts (reportMissing report)+ , "duplicate" .= keyTexts (reportDuplicate report)+ , "stale" .= keyTexts (reportStale report)+ , "failed" .= reportFailed report+ , "verified" .= keyTexts (reportVerified report)+ , "unverified" .= keyTexts (reportUnverified report)+ ]++behaviorRequirements :: [BehaviorRequirement]+behaviorRequirements =+ [ -- ArtifactCatalogEmpty x ObserveArtifact: live transition (spec line 80)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-5ef17e70af0d6ff7"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardTotal+ , requirementSource = ArtifactCatalogEmpty+ , requirementCommandName = "ObserveArtifact"+ , requirementExpectedEdge = (Just (K.EdgeRef ArtifactCatalogEmpty 0))+ , requirementTarget = Just ArtifactCatalogObserved+ , requirementEventKinds = ["ArtifactRecorded", "ArtifactAccepted"]+ , requirementLine = 80+ }+ , -- ArtifactCatalogObserved x ObserveArtifact: required rejection (spec line 74)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-c20f0b92a1f17333"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ArtifactCatalogObserved+ , requirementCommandName = "ObserveArtifact"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 74+ }+ ]++behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport+behaviorCoverageReport witnesses =+ BehaviorConformanceReport+ { reportRequired = sortedKeys (Map.keys requiredByKey)+ , reportFilled = sortedKeys [key | (key, [witness]) <- Map.toList witnessGroups, Map.member key requiredByKey, not (isPending witness)]+ , reportPending = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, Map.member key requiredByKey, any isPending rows]+ , reportMissing = sortedKeys [key | key <- Map.keys requiredByKey, Map.notMember key witnessGroups]+ , reportDuplicate = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, length rows > 1]+ , reportStale = sortedKeys [key | key <- Map.keys witnessGroups, Map.notMember key requiredByKey]+ , reportFailed = sortOn (unBehaviorKey . failureKey) failures+ , reportVerified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, proofStrength requirement]+ , reportUnverified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, not (proofStrength requirement)]+ }+ where+ requiredByKey = Map.fromList [(requirementKey requirement, requirement) | requirement <- behaviorRequirements]+ witnessGroups = Map.fromListWith (flip (<>)) [(behaviorWitnessKey witness, [witness]) | witness <- witnesses]+ executions =+ [ (requirement, runWitness requirement witness)+ | (key, [witness]) <- Map.toList witnessGroups+ , not (isPending witness)+ , Just requirement <- [Map.lookup key requiredByKey]+ ]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions]++behaviorConformancePassed :: BehaviorConformanceReport -> Bool+behaviorConformancePassed = behaviorConformancePassedWith False++behaviorConformancePassedWith :: Bool -> BehaviorConformanceReport -> Bool+behaviorConformancePassedWith failOnUnverified report =+ null (reportPending report)+ && null (reportMissing report)+ && null (reportDuplicate report)+ && null (reportStale report)+ && null (reportFailed report)+ && (not failOnUnverified || null (reportUnverified report))++renderBehaviorConformanceText :: BehaviorConformanceReport -> Text+renderBehaviorConformanceText report = T.unlines+ [ "behavior conformance: ArtifactCatalog"+ , "schema: keiro/behavior-conformance/1"+ , countLine "required" (reportRequired report)+ , countLine "filled" (reportFilled report)+ , countLine "pending" (reportPending report)+ , countLine "missing" (reportMissing report)+ , countLine "duplicate" (reportDuplicate report)+ , countLine "stale" (reportStale report)+ , "failed: " <> tshow (length (reportFailed report))+ , countLine "verified" (reportVerified report)+ , countLine "unverified" (reportUnverified report)+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report]++runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure ()+runWitness requirement witness = case witness of+ Pending _ -> failure requirement "pending" "witness is still Pending"+ LiveWitness _ history command expectation -> runLive requirement history command expectation+ ReplayWitness _ prefix chunk -> runReplay requirement prefix chunk++runLive :: BehaviorRequirement -> [ArtifactCatalogEvent] -> ArtifactCatalogCommand -> LiveExpectation -> Either BehaviorFailure ()+runLive requirement history command expectation = do+ settled <- settleHistory requirement "history" history+ ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history does not settle at the required source vertex"+ ensure requirement (commandKind command == requirementCommandName requirement) "command-mismatch" "witness command constructor does not match the required state/command cell"+ case requirementKind requirement of+ ReplayTransition -> failure requirement "witness-kind" "a replay-only requirement needs ReplayWitness"+ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation+ LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation++runRejection :: BehaviorRequirement -> (ArtifactCatalogVertex, K.RegFile ArtifactCatalogRegs) -> ArtifactCatalogCommand -> LiveExpectation -> Either BehaviorFailure ()+runRejection requirement seed command expectation = case expectation of+ Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events"+ NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"+ Rejects expectedClass -> case K.stepDetailedEither artifactCatalogTransducer seed command of+ Left K.NoOutgoingEdges {} -> ensure requirement (expectedClass == RejectNoOutgoingEdges) "rejection-class" "expected NoMatchingEdge but runtime returned NoOutgoingEdges"+ Left K.NoMatchingEdge {} -> ensure requirement (expectedClass == RejectNoMatchingEdge) "rejection-class" "expected NoOutgoingEdges but runtime returned NoMatchingEdge"+ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness"+ Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject"++runAcceptance :: BehaviorRequirement -> (ArtifactCatalogVertex, K.RegFile ArtifactCatalogRegs) -> ArtifactCatalogCommand -> LiveExpectation -> Either BehaviorFailure ()+runAcceptance requirement seed command expectation = case expectation of+ Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp"+ NoOp -> case K.stepDetailedEither artifactCatalogTransducer seed command of+ Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)+ Right success -> do+ checkAcceptedEnvelope requirement success+ ensure requirement (null (K.stepSuccessOutputs success)) "noop-emitted" "NoOp emitted one or more events"+ ensure requirement (K.stepSuccessState success == fst seed) "noop-vertex-change" "NoOp changed the control vertex"+ ensure requirement (regsEqual (K.stepSuccessRegs success) (snd seed)) "noop-register-change" "NoOp changed one or more registers"+ Emits expectedEvents -> case K.stepDetailedEither artifactCatalogTransducer seed command of+ Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)+ Right success -> do+ checkAcceptedEnvelope requirement success+ let expected = NonEmpty.toList expectedEvents+ actual = K.stepSuccessOutputs success+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement))+ decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual)+ replayed <- case K.applyEventsDetailedEither artifactCatalogTransducer seed decoded of+ Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)+ Right replaySuccess -> Right replaySuccess+ ensure requirement (K.replaySuccessState replayed == K.stepSuccessState success) "forward-replay-vertex" "decoded emissions replay to a different vertex"+ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers"+ checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed)++checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ArtifactCatalogRegs ArtifactCatalogVertex ArtifactCatalogEvent -> Either BehaviorFailure ()+checkAcceptedEnvelope requirement success = do+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement))++runReplay :: BehaviorRequirement -> [ArtifactCatalogEvent] -> [ArtifactCatalogEvent] -> Either BehaviorFailure ()+runReplay requirement prefix chunk = case requirementKind requirement of+ ReplayTransition -> do+ settled <- settleHistory requirement "history-prefix" prefix+ ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history prefix does not settle at the replay edge source"+ ensure requirement (not (null chunk)) "empty-replay-chunk" "a replay-only edge has no observable empty chunk"+ decoded <- either (failure requirement "replay-chunk-codec-decode") Right (decodeEvents chunk)+ replayed <- case K.applyEventsDetailedEither artifactCatalogTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of+ Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure)+ Right replaySuccess -> Right replaySuccess+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement))+ checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed)+ _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement"++checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ArtifactCatalogVertex] -> Either BehaviorFailure ()+checkSingleAttribution requirement expectedMode eventCount trace = case trace of+ [attribution] -> do+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount))+ _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution"++settleHistory :: BehaviorRequirement -> Text -> [ArtifactCatalogEvent] -> Either BehaviorFailure (K.ReplaySuccess ArtifactCatalogRegs ArtifactCatalogVertex)+settleHistory requirement label history = do+ decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history)+ case K.applyEventsDetailedEither artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) decoded of+ Left replayFailure -> failure requirement (label <> "-replay-failed") (tshow replayFailure)+ Right replaySuccess -> Right replaySuccess++decodeEvents :: [ArtifactCatalogEvent] -> Either Text [ArtifactCatalogEvent]+decodeEvents = traverse (\event -> parseArtifactCatalogEvent (Codec.eventType artifactCatalogCodec event) (encodeArtifactCatalogEvent event))++commandKind :: ArtifactCatalogCommand -> Text+commandKind command = case command of+ ObserveArtifact _ -> "ObserveArtifact"++eventKind :: ArtifactCatalogEvent -> Text+eventKind event = case Codec.eventType artifactCatalogCodec event of Codec.EventType tag -> tag++regsEqual :: K.RegFile ArtifactCatalogRegs -> K.RegFile ArtifactCatalogRegs -> Bool+regsEqual left right = (left K.! #currentArtifact) == (right K.! #currentArtifact) && (left K.! #currentGeometry) == (right K.! #currentGeometry) && (left K.! #acceptedCount) == (right K.! #acceptedCount)++proofStrength :: BehaviorRequirement -> Bool+proofStrength requirement =+ requirementEvidence requirement == GeneratedAuthoritative+ && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable]++behaviorWitnessKey :: BehaviorWitness -> BehaviorKey+behaviorWitnessKey witness = case witness of+ Pending key -> key+ LiveWitness { witnessKey = key } -> key+ ReplayWitness { witnessKey = key } -> key++isPending :: BehaviorWitness -> Bool+isPending Pending {} = True+isPending _ = False++ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure ()+ensure requirement condition code detail = if condition then Right () else failure requirement code detail+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey]+sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text]+keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text+countLine label values = label <> ": " <> tshow (length values)+tshow :: Show value => value -> Text+tshow = T.pack . show
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.Codec ( artifactCatalogCodec, parseArtifactCatalogEvent,@@ -22,8 +22,6 @@ import Data.Aeson.Types (Parser, explicitParseField, parseEither) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty-import Data.Map.Strict (Map)-import Data.Map.Strict qualified as Map import Data.Text (Text) import qualified Data.Text as T import Keiro.Codec.Structural (bindingFromShape, bindingToShape)@@ -67,12 +65,12 @@ ShapeArtifactInfo.ArtifactInfo <$> explicitParseField (parseJSON) objectValue "artifact_key" <*> explicitParseField (parseJSON) objectValue "display_name"- <*> (case KeyMap.lookup (Key.fromText "artifact_hash") objectValue of Nothing -> pure Nothing; Just _ -> explicitParseField (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) objectValue "artifact_hash")- <*> (case KeyMap.lookup (Key.fromText "artifact_kind") objectValue of Nothing -> pure ShapeArtifactKind.Guide; Just _ -> explicitParseField (parseArtifactKindShape) objectValue "artifact_kind")+ <*> parseOptionalField (pure Nothing) (\value -> case value of Null -> pure Nothing; other -> Just <$> parseJSON other) objectValue "artifact_hash"+ <*> parseOptionalField (pure ShapeArtifactKind.Guide) (parseArtifactKindShape) objectValue "artifact_kind" <*> explicitParseField (parseArtifactLocationShape) objectValue "location" <*> explicitParseField (parseArtifactMetadataShape) objectValue "metadata"- <*> (case KeyMap.lookup (Key.fromText "active") objectValue of Nothing -> pure False; Just _ -> explicitParseField (parseJSON) objectValue "active")- <*> (case KeyMap.lookup (Key.fromText "tags") objectValue of Nothing -> pure []; Just _ -> explicitParseField (\value -> (parseJSON value :: Parser [Value]) >>= traverse (parseJSON)) objectValue "tags")+ <*> parseOptionalField (pure False) (parseJSON) objectValue "active"+ <*> parseOptionalField (pure []) (\value -> (parseJSON value :: Parser [Value]) >>= traverse (parseJSON)) objectValue "tags" encodeArtifactKindMapped :: ArtifactKind -> Value encodeArtifactKindMapped = encodeArtifactKindShape . bindingToShape Bindings.artifactKindBinding@@ -92,7 +90,7 @@ parseArtifactKindShape = withText "ArtifactKindShape" $ \tag -> case tag of "guide" -> pure ShapeArtifactKind.Guide "reference" -> pure ShapeArtifactKind.Reference- tag -> fail ("unknown ArtifactKind wire value " <> show tag <> "; expected one of: guide, reference")+ unknownTag -> fail ("unknown ArtifactKind wire value " <> show unknownTag <> "; expected one of: guide, reference") encodeArtifactLocationMapped :: ArtifactLocation -> Value encodeArtifactLocationMapped = encodeArtifactLocationShape . bindingToShape Bindings.artifactLocationBinding@@ -224,17 +222,23 @@ <$> ( ArtifactAcceptedData <$> o .: "accepted" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes artifactCatalogEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes artifactCatalogEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName) . NonEmpty.toList++parseOptionalField :: Parser fieldValue -> (Value -> Parser fieldValue) -> KeyMap.KeyMap Value -> Key.Key -> Parser fieldValue+parseOptionalField onMissing parseItem objectValue key =+ case KeyMap.lookup key objectValue of+ Nothing -> onMissing+ Just _ -> explicitParseField parseItem objectValue key rejectUnknownFields :: String -> [Text] -> KeyMap.KeyMap Value -> Parser () rejectUnknownFields label allowed objectValue =
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Domain.hs view
@@ -1,10 +1,9 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Conformance.Structural.Bindings qualified as Bindings
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.EventStream ( artifactCatalogCategory , artifactCatalogCommandCategory@@ -10,7 +10,7 @@ import Generated.StructuralConformance.ArtifactCatalog.Domain import Generated.StructuralConformance.ArtifactCatalog.Codec (artifactCatalogCodec)-import Generated.StructuralConformance.ArtifactCatalog.Transducer (artifactCatalogFoldFingerprint, artifactCatalogTransducer)+import Generated.StructuralConformance.ArtifactCatalog.Transducer (artifactCatalogTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.Harness (harnessAssertions) where import Generated.StructuralConformance.ArtifactCatalog.Domain@@ -20,7 +20,7 @@ import Keiro.Codec.Structural (FixtureCases (..), bindingDomainRoundTrip, bindingShapeRoundTrip, bindingToShape) import Generated.StructuralConformance.StructuralProjections qualified as StructuralProjections import Conformance.Structural.Bindings qualified as Bindings-import Conformance.Structural.Domain (ArtifactInfo, ArtifactKind, ArtifactLocation, ArtifactMetadata, Geometry)+import Conformance.Structural.Domain (ArtifactInfo, ArtifactKind, ArtifactLocation, ArtifactMetadata) import Generated.StructuralConformance.Structural.Shape.ArtifactInfo qualified as ShapeArtifactInfo import Generated.StructuralConformance.Structural.Shape.ArtifactKind qualified as ShapeArtifactKind import Generated.StructuralConformance.Structural.Shape.ArtifactLocation qualified as ShapeArtifactLocation@@ -32,7 +32,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions artifactCatalogTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: ArtifactRecorded", roundTrips sampleEventArtifactRecorded) , ("golden round-trip: ArtifactAccepted", roundTrips sampleEventArtifactAccepted) , ("accepts ObserveArtifact from ArtifactCatalogEmpty", acceptObserveArtifact)@@ -44,14 +44,14 @@ roundTrips e = parseArtifactCatalogEvent (eventType artifactCatalogCodec e) (encodeArtifactCatalogEvent e) == Right e sampleEventArtifactRecorded :: ArtifactCatalogEvent-sampleEventArtifactRecorded = (ArtifactRecorded (ArtifactRecordedData (snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Bindings.geometryCases))) False))+sampleEventArtifactRecorded = ArtifactRecorded (ArtifactRecordedData (snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Bindings.geometryCases))) False) sampleEventArtifactAccepted :: ArtifactCatalogEvent-sampleEventArtifactAccepted = (ArtifactAccepted (ArtifactAcceptedData False))+sampleEventArtifactAccepted = ArtifactAccepted (ArtifactAcceptedData False) acceptObserveArtifact :: Bool acceptObserveArtifact =- case step artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) ((ObserveArtifact (ObserveArtifactData (snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Bindings.geometryCases))) False))) of+ case step artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) (ObserveArtifact (ObserveArtifactData (snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Bindings.geometryCases))) False)) of Just (v, _, _) -> v == ArtifactCatalogObserved Nothing -> False @@ -59,7 +59,7 @@ -- 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 Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases Bindings.geometryCases))) False))) of+ case step artifactCatalogTransducer (ArtifactCatalogEmpty, initialArtifactCatalogRegs) (ObserveArtifact (ObserveArtifactData (snd (NonEmpty.head (fixtureCases Bindings.artifactInfoCases))) (snd (NonEmpty.head (fixtureCases 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
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-structural/Generated/StructuralConformance/ArtifactCatalog/Transducer.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ArtifactCatalog; do not edit. module Generated.StructuralConformance.ArtifactCatalog.Transducer ( artifactCatalogTransducer , artifactCatalogFoldFingerprint@@ -11,7 +11,6 @@ import Generated.StructuralConformance.ArtifactCatalog.Domain import Data.Text (Text)-import Conformance.Structural.Domain (ArtifactInfo, Geometry) import Keiki.Builder qualified as B import Keiki.Core (HsPred, SymTransducer) import Keiki.Core qualified as K
test/conformance-structural/Generated/StructuralConformance/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context structural-conformance replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context structural-conformance replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactInfo.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural ArtifactInfo; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural ArtifactInfo; do not edit. module Generated.StructuralConformance.Structural.Shape.ArtifactInfo (ArtifactInfoShape (..)) where import Data.Text (Text)
test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactKind.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural ArtifactKind; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural ArtifactKind; do not edit. module Generated.StructuralConformance.Structural.Shape.ArtifactKind (ArtifactKindShape (..)) where import GHC.Generics (Generic)
test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactLocation.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural ArtifactLocation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural ArtifactLocation; do not edit. module Generated.StructuralConformance.Structural.Shape.ArtifactLocation (ArtifactLocationShape (..)) where import Data.Text (Text)
test/conformance-structural/Generated/StructuralConformance/Structural/Shape/ArtifactMetadata.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from mapped structural ArtifactMetadata; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from mapped structural ArtifactMetadata; do not edit. module Generated.StructuralConformance.Structural.Shape.ArtifactMetadata (ArtifactMetadataShape (..)) where import Data.Text (Text)
test/conformance-structural/Generated/StructuralConformance/StructuralProjections.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context structural-conformance mapped structural facade; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context structural-conformance mapped structural facade; do not edit. -- Equality witnesses are emitted for Text, Int, Bool, Natural, and UTCTime. -- Int, Natural, and UTCTime belong to Keiki's ordered subset. module Generated.StructuralConformance.StructuralProjections@@ -8,8 +8,6 @@ ) where import Data.Text (Text)-import Data.Time (UTCTime)-import Numeric.Natural (Natural) import Keiro.Codec.Structural (bindingToShape) import Keiki.Core (FieldProjection (..), FieldWitness, fieldWitness) import Conformance.Structural.Bindings qualified as Bindings
test/conformance-v2/Generated/HospitalCapacity/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit. module Generated.HospitalCapacity.Nominals ( BedType (..) , bedTypeText@@ -29,10 +29,9 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.HospitalCapacity.Nominals.Internal (CommandId, mkCommandId, parseCommandId, commandIdText, HospitalId, mkHospitalId, parseHospitalId, hospitalIdText, TransferReservationId, mkTransferReservationId, parseTransferReservationId, transferReservationIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)+import Keiki.ProjectionDomain (finiteProjectionDomain) data BedType = Icu | MedicalSurgical deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)@@ -45,8 +44,8 @@ Icu -> "icu" MedicalSurgical -> "medical-surgical" -instance CanonicalTypeName CommandId + data DivertStatus = Open | PartialDivert | TotalDivert deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -79,8 +78,8 @@ divertStatusEqualityWitness :: FieldWitness DivertStatusEqualityProjection divertStatusEqualityWitness = exactFieldWitness @DivertStatusEqualityProjection -instance CanonicalTypeName HospitalId + data PatientAcuity = RedTag | YellowTag | GreenTag deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -92,5 +91,3 @@ RedTag -> "red" YellowTag -> "yellow" GreenTag -> "green"--instance CanonicalTypeName TransferReservationId
test/conformance-v2/Generated/HospitalCapacity/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit. module Generated.HospitalCapacity.Nominals.Internal ( CommandId , parseCommandId@@ -21,11 +21,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CommandId = CommandId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CommandId+ instance ToJSON CommandId where toJSON = toJSON . commandIdText @@ -49,6 +52,8 @@ newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName HospitalId+ instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText @@ -71,6 +76,8 @@ newtype TransferReservationId = TransferReservationId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName TransferReservationId instance ToJSON TransferReservationId where toJSON = toJSON . transferReservationIdText
test/conformance-v2/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance-v2/Generated/HospitalCapacity/Reservation/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Codec ( reservationCodec, parseReservationEvent,@@ -7,7 +7,7 @@ ) where import Generated.HospitalCapacity.Reservation.Domain-import Generated.HospitalCapacity.Nominals (CommandId, commandIdText, DivertStatus (..), divertStatusText, HospitalId, hospitalIdText, PatientAcuity (..), patientAcuityText, TransferReservationId, transferReservationIdText)+import Generated.HospitalCapacity.Nominals (commandIdText, DivertStatus (..), divertStatusText, hospitalIdText, PatientAcuity (..), patientAcuityText, transferReservationIdText) import Generated.HospitalCapacity.Nominals.Internal (unsafeCommandIdFromLegacyText, unsafeHospitalIdFromLegacyText, unsafeTransferReservationIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -99,13 +99,13 @@ <*> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") <*> (unsafeCommandIdFromLegacyText <$> o .: "commandId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes reservationEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes reservationEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-v2/Generated/HospitalCapacity/Reservation/Domain.hs view
@@ -1,13 +1,13 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Domain where import Data.Proxy (Proxy (..)) import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.HospitalCapacity.Nominals (CommandId, parseCommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId)+import Generated.HospitalCapacity.Nominals (CommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data ReservationVertex = ReservationUnrequested | ReservationHeld | ReservationConfirmed | ReservationExpired | ReservationAdmitted | ReservationReleased
test/conformance-v2/Generated/HospitalCapacity/Reservation/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.EventStream ( reservationCategory , reservationCommandCategory@@ -10,7 +10,7 @@ import Generated.HospitalCapacity.Reservation.Domain import Generated.HospitalCapacity.Reservation.Codec (reservationCodec)-import Generated.HospitalCapacity.Reservation.Transducer (reservationFoldFingerprint, reservationTransducer)+import Generated.HospitalCapacity.Reservation.Transducer (reservationTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-v2/Generated/HospitalCapacity/Reservation/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Harness (harnessAssertions) where import Generated.HospitalCapacity.Reservation.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions reservationTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: TransferReservationCreated", roundTrips sampleEventTransferReservationCreated) , ("golden round-trip: TransferReservationConfirmed", roundTrips sampleEventTransferReservationConfirmed) , ("accepts RequestTransferReservation from ReservationUnrequested", acceptRequestTransferReservation)@@ -27,15 +27,33 @@ roundTrips :: ReservationEvent -> Bool roundTrips e = parseReservationEvent (eventType reservationCodec e) (encodeReservationEvent e) == Right e +sampleCommandId :: CommandId+sampleCommandId =+ case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleHospitalId :: HospitalId+sampleHospitalId =+ case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleTransferReservationId :: TransferReservationId+sampleTransferReservationId =+ case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventTransferReservationCreated :: ReservationEvent-sampleEventTransferReservationCreated = (TransferReservationCreated (TransferReservationCreatedData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False "sample-triageNote"))+sampleEventTransferReservationCreated = TransferReservationCreated (TransferReservationCreatedData sampleTransferReservationId sampleHospitalId sampleCommandId RedTag Open False "sample-triageNote") sampleEventTransferReservationConfirmed :: ReservationEvent-sampleEventTransferReservationConfirmed = (TransferReservationConfirmed (TransferReservationConfirmedData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))+sampleEventTransferReservationConfirmed = TransferReservationConfirmed (TransferReservationConfirmedData sampleTransferReservationId sampleHospitalId sampleCommandId) acceptRequestTransferReservation :: Bool acceptRequestTransferReservation =- case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False))) of+ case step reservationTransducer (ReservationUnrequested, initialReservationRegs) (RequestTransferReservation (RequestTransferReservationData sampleTransferReservationId sampleHospitalId sampleCommandId RedTag Open False)) of Just (v, _, _) -> v == ReservationHeld Nothing -> False @@ -43,7 +61,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRequestTransferReservation :: [(String, Bool)] forwardReplayRequestTransferReservation =- case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False))) of+ case step reservationTransducer (ReservationUnrequested, initialReservationRegs) (RequestTransferReservation (RequestTransferReservationData sampleTransferReservationId sampleHospitalId sampleCommandId 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
test/conformance-v2/Generated/HospitalCapacity/Reservation/Projection.hs view
@@ -1,11 +1,11 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Projection- ( transfer_decisionsProjection- , transfer_decisionsStatusFor+ ( transferDecisionsProjection+ , transferDecisionsStatusFor ) where import Generated.HospitalCapacity.Reservation.Domain-import HospitalCapacity.Reservation.Holes (applyTransfer_decisions)+import HospitalCapacity.Reservation.Holes (applyTransferDecisions) import Data.Text (Text) import Keiro.Projection (InlineProjection (..)) @@ -13,14 +13,14 @@ -- 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+transferDecisionsStatusFor :: ReservationEvent -> Maybe Text+transferDecisionsStatusFor = \case TransferReservationCreated {} -> Just "held" TransferReservationConfirmed {} -> Just "confirmed" -transfer_decisionsProjection :: InlineProjection ReservationEvent-transfer_decisionsProjection =+transferDecisionsProjection :: InlineProjection ReservationEvent+transferDecisionsProjection = InlineProjection { name = "hospital-capacity-transfer_decisions-inline"- , apply = applyTransfer_decisions+ , apply = applyTransferDecisions }
test/conformance-v2/Generated/HospitalCapacity/Reservation/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Transducer ( reservationTransducer , reservationFoldFingerprint
test/conformance-v2/HospitalCapacity/Reservation/Holes.hs view
@@ -7,7 +7,7 @@ module HospitalCapacity.Reservation.Holes ( transition1UnrequestedRequestTransferReservationOutput1TransferReservationCreated, transition2HeldConfirmReservationOutput1TransferReservationConfirmed,- applyTransfer_decisions,+ applyTransferDecisions, upcastTransferReservationCreatedV1, ) where@@ -40,8 +40,8 @@ commandId = command.commandId } -applyTransfer_decisions :: ReservationEvent -> recorded -> txn ()-applyTransfer_decisions _event _recorded = error "HOLE: fill transfer_decisions projection apply"+applyTransferDecisions :: ReservationEvent -> recorded -> txn ()+applyTransferDecisions _event _recorded = error "HOLE: fill transfer_decisions projection apply" upcastTransferReservationCreatedV1 :: Value -> Either Text Value upcastTransferReservationCreatedV1 value = case value of
test/conformance-workflow-full/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowRuntime.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit. module Generated.HospitalCapacity.HospitalTransferReservation.WorkflowRuntime ( workflowName , awaitAwakeableId
test/conformance-workflow-runtime/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowRuntime.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit. module Generated.HospitalCapacity.HospitalTransferReservation.WorkflowRuntime ( workflowName , awaitAwakeableId
test/conformance-workflow/Generated/HospitalCapacity/HospitalTransferReservation/WorkflowFacts.hs view
@@ -1,5 +1,5 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.-module Generated.HospitalCapacity.HospitalTransferReservation.WorkflowFacts (WorkflowFacts (..), workflowFacts) where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from workflow HospitalTransferReservation; do not edit.+module Generated.HospitalCapacity.HospitalTransferReservation.WorkflowFacts (WorkflowFacts (..), workflowFacts, workflowFactValues) where -- | The workflow's deterministic decisions, pinned as typed pure facts. -- A driver asserts them against a hand-written expectation, so a spec@@ -24,3 +24,14 @@ , workflowFactAwaitLabels = ["reservation-confirmation"] , workflowFactPatchIds = ["fraud-check-v2"] }++-- | Base-library projection used by the service-level conformance facade.+workflowFactValues :: [(String, String)]+workflowFactValues =+ [ ("name", workflowFactName workflowFacts)+ , ("idVia", workflowFactIdVia workflowFacts)+ , ("idField", workflowFactIdField workflowFacts)+ , ("body", show (workflowFactBody workflowFacts))+ , ("awaits", show (workflowFactAwaitLabels workflowFacts))+ , ("patches", show (workflowFactPatchIds workflowFacts))+ ]
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context workspace-nominal-proof generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context workspace-nominal-proof generated nominal declarations; do not edit. module Generated.WorkspaceNominalProof.Nominals ( ProjectId , parseProjectId@@ -21,12 +21,11 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.WorkspaceNominalProof.Nominals.Internal (ProjectId, mkProjectId, parseProjectId, projectIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)+import Keiki.ProjectionDomain (finiteProjectionDomain, TextPattern, textProjectionDomain) import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain) -instance CanonicalTypeName ProjectId data ProjectIdEqualityProjection
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context workspace-nominal-proof generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context workspace-nominal-proof generated nominal ID internals; do not edit. module Generated.WorkspaceNominalProof.Nominals.Internal ( ProjectId , parseProjectId@@ -11,10 +11,13 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype ProjectId = ProjectId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName ProjectId instance ToJSON ProjectId where toJSON = toJSON . projectIdText
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.-module Generated.WorkspaceNominalProof.Project.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+module Generated.WorkspaceNominalProof.Project.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.WorkspaceNominalProof.Project.Codec (encodeProjectEvent, parseProjectEvent, projectCodec) import Generated.WorkspaceNominalProof.Project.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,12 +126,84 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-3de05f6aced1fbd8") RequiredRejection GeneratedAuthoritative GuardNotApplicable ProjectArchived "ArchiveProject" Nothing Nothing [] 28- , BehaviorRequirement (BehaviorKey "behavior-v1-3ef04b25c79aa821") LiveTransition GeneratedAuthoritative GuardUnknown ProjectEmpty "RegisterProject" (Just (K.EdgeRef ProjectEmpty 0)) (Just ProjectLive) ["ProjectRegistered"] 35- , BehaviorRequirement (BehaviorKey "behavior-v1-44c09c629d21fd17") RequiredRejection GeneratedAuthoritative GuardNotApplicable ProjectArchived "RegisterProject" Nothing Nothing [] 28- , BehaviorRequirement (BehaviorKey "behavior-v1-6134478c9aaa78cb") RequiredRejection GeneratedAuthoritative GuardNotApplicable ProjectLive "RegisterProject" Nothing Nothing [] 28- , BehaviorRequirement (BehaviorKey "behavior-v1-c4b406a5be4a0d0b") RequiredRejection GeneratedAuthoritative GuardNotApplicable ProjectEmpty "ArchiveProject" Nothing Nothing [] 28- , BehaviorRequirement (BehaviorKey "behavior-v1-ea703ecdfbda1e70") LiveTransition GeneratedAuthoritative GuardUnknown ProjectLive "ArchiveProject" (Just (K.EdgeRef ProjectLive 0)) (Just ProjectArchived) ["ArchivalRecorded"] 42+ [ -- ProjectArchived x ArchiveProject: required rejection (spec line 28)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-3de05f6aced1fbd8"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ProjectArchived+ , requirementCommandName = "ArchiveProject"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 28+ }+ , -- ProjectEmpty x RegisterProject: live transition (spec line 35)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-3ef04b25c79aa821"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = ProjectEmpty+ , requirementCommandName = "RegisterProject"+ , requirementExpectedEdge = (Just (K.EdgeRef ProjectEmpty 0))+ , requirementTarget = Just ProjectLive+ , requirementEventKinds = ["ProjectRegistered"]+ , requirementLine = 35+ }+ , -- ProjectArchived x RegisterProject: required rejection (spec line 28)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-44c09c629d21fd17"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ProjectArchived+ , requirementCommandName = "RegisterProject"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 28+ }+ , -- ProjectLive x RegisterProject: required rejection (spec line 28)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-6134478c9aaa78cb"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ProjectLive+ , requirementCommandName = "RegisterProject"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 28+ }+ , -- ProjectEmpty x ArchiveProject: required rejection (spec line 28)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-c4b406a5be4a0d0b"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ProjectEmpty+ , requirementCommandName = "ArchiveProject"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 28+ }+ , -- ProjectLive x ArchiveProject: live transition (spec line 42)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-ea703ecdfbda1e70"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = ProjectLive+ , requirementCommandName = "ArchiveProject"+ , requirementExpectedEdge = (Just (K.EdgeRef ProjectLive 0))+ , requirementTarget = Just ProjectArchived+ , requirementEventKinds = ["ArchivalRecorded"]+ , requirementLine = 42+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -139,7 +228,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -166,7 +255,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -184,6 +273,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (ProjectVertex, K.RegFile ProjectRegs) -> ProjectCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -193,6 +283,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (ProjectVertex, K.RegFile ProjectRegs) -> ProjectCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither projectTransducer seed command of@@ -208,8 +299,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither projectTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -218,10 +309,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ProjectRegs ProjectVertex ProjectEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [ProjectEvent] -> [ProjectEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -233,19 +325,21 @@ replayed <- case K.applyEventsDetailedEither projectTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ProjectVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [ProjectEvent] -> Either BehaviorFailure (K.ReplaySuccess ProjectRegs ProjectVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither projectTransducer (ProjectEmpty, initialProjectRegs) decoded of@@ -255,31 +349,53 @@ decodeEvents :: [ProjectEvent] -> Either Text [ProjectEvent] decodeEvents = traverse (\event -> parseProjectEvent (Codec.eventType projectCodec event) (encodeProjectEvent event)) +commandKind :: ProjectCommand -> Text commandKind command = case command of RegisterProject _ -> "RegisterProject" ArchiveProject _ -> "ArchiveProject" +eventKind :: ProjectEvent -> Text eventKind event = case Codec.eventType projectCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile ProjectRegs -> K.RegFile ProjectRegs -> Bool regsEqual left right = (left K.! #projectId) == (right K.! #projectId) && (left K.! #phase) == (right K.! #phase) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.Codec ( projectCodec, parseProjectEvent,@@ -7,7 +7,7 @@ ) where import Generated.WorkspaceNominalProof.Project.Domain-import Generated.WorkspaceNominalProof.Nominals (ProjectId, projectIdText, ProjectPhase (..), projectPhaseText)+import Generated.WorkspaceNominalProof.Nominals (projectIdText, ProjectPhase (..), projectPhaseText) import Generated.WorkspaceNominalProof.Nominals.Internal (unsafeProjectIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -73,13 +73,13 @@ <$> (unsafeProjectIdFromLegacyText <$> o .: "projectId") <*> explicitParseField (withText "ProjectPhase" parseProjectPhase) o "phase" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes projectEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes projectEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Domain.hs view
@@ -1,10 +1,9 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Generated.WorkspaceNominalProof.Nominals (ProjectId, parseProjectId, ProjectPhase (..))
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.EventStream ( projectCategory , projectCommandCategory@@ -10,7 +10,7 @@ import Generated.WorkspaceNominalProof.Project.Domain import Generated.WorkspaceNominalProof.Project.Codec (projectCodec)-import Generated.WorkspaceNominalProof.Project.Transducer (projectFoldFingerprint, projectTransducer)+import Generated.WorkspaceNominalProof.Project.Transducer (projectTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.Harness (harnessAssertions) where import Generated.WorkspaceNominalProof.Project.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions projectTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: ProjectRegistered", roundTrips sampleEventProjectRegistered) , ("golden round-trip: ArchivalRecorded", roundTrips sampleEventArchivalRecorded) , ("accepts RegisterProject from ProjectEmpty", acceptRegisterProject)@@ -25,15 +25,21 @@ roundTrips :: ProjectEvent -> Bool roundTrips e = parseProjectEvent (eventType projectCodec e) (encodeProjectEvent e) == Right e +sampleProjectId :: ProjectId+sampleProjectId =+ case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventProjectRegistered :: ProjectEvent-sampleEventProjectRegistered = (ProjectRegistered (ProjectRegisteredData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))+sampleEventProjectRegistered = ProjectRegistered (ProjectRegisteredData sampleProjectId Draft) sampleEventArchivalRecorded :: ProjectEvent-sampleEventArchivalRecorded = (ArchivalRecorded (ArchivalRecordedData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))+sampleEventArchivalRecorded = ArchivalRecorded (ArchivalRecordedData sampleProjectId Draft) acceptRegisterProject :: Bool acceptRegisterProject =- case step projectTransducer (ProjectEmpty, initialProjectRegs) ((RegisterProject (RegisterProjectData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))) of+ case step projectTransducer (ProjectEmpty, initialProjectRegs) (RegisterProject (RegisterProjectData sampleProjectId Draft)) of Just (v, _, _) -> v == ProjectLive Nothing -> False @@ -41,7 +47,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRegisterProject :: [(String, Bool)] forwardReplayRegisterProject =- case step projectTransducer (ProjectEmpty, initialProjectRegs) ((RegisterProject (RegisterProjectData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))) of+ case step projectTransducer (ProjectEmpty, initialProjectRegs) (RegisterProject (RegisterProjectData sampleProjectId Draft)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseProjectEvent (eventType projectCodec event) (encodeProjectEvent event)) emitted of
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/Project/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Project; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Project; do not edit. module Generated.WorkspaceNominalProof.Project.Transducer ( projectTransducer , projectFoldFingerprint@@ -12,7 +12,7 @@ import Generated.WorkspaceNominalProof.Project.Domain import Data.Text (Text)-import Generated.WorkspaceNominalProof.Nominals (ProjectId, parseProjectId, ProjectPhase (..))+import Generated.WorkspaceNominalProof.Nominals (ProjectId, ProjectPhase (..)) import Generated.WorkspaceNominalProof.Nominals qualified as GeneratedNominals import Keiki.Builder qualified as B import Keiki.Core (HsPred, SymTransducer, (.==), (.&&))
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/BehaviorContract.hs view
@@ -1,7 +1,22 @@ {-# LANGUAGE OverloadedLabels #-}-{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.-module Generated.WorkspaceNominalProof.ProjectArtifact.BehaviorContract where+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+module Generated.WorkspaceNominalProof.ProjectArtifact.BehaviorContract+ ( BehaviorKey (..)+ , ObligationKind (..)+ , EvidenceLevel (..)+ , GuardCoverage (..)+ , BehaviorRequirement (..)+ , RejectionClass (..)+ , LiveExpectation (..)+ , BehaviorWitness (..)+ , BehaviorFailure (..)+ , BehaviorConformanceReport (..)+ , behaviorRequirements+ , behaviorCoverageReport+ , behaviorConformancePassed+ , behaviorConformancePassedWith+ , renderBehaviorConformanceText+ ) where import Generated.WorkspaceNominalProof.ProjectArtifact.Codec (encodeProjectArtifactEvent, parseProjectArtifactEvent, projectArtifactCodec) import Generated.WorkspaceNominalProof.ProjectArtifact.Domain@@ -68,16 +83,18 @@ data BehaviorFailure = BehaviorFailure { failureKey :: !BehaviorKey+ , failureSubject :: !Text , failureCode :: !Text , failureDetail :: !Text } deriving stock (Eq, Show) instance ToJSON BehaviorFailure where- toJSON failure = object- [ "key" .= unBehaviorKey (failureKey failure)- , "code" .= failureCode failure- , "detail" .= failureDetail failure+ toJSON behaviorFailure = object+ [ "key" .= unBehaviorKey (failureKey behaviorFailure)+ , "subject" .= failureSubject behaviorFailure+ , "code" .= failureCode behaviorFailure+ , "detail" .= failureDetail behaviorFailure ] data BehaviorConformanceReport = BehaviorConformanceReport@@ -109,8 +126,32 @@ behaviorRequirements :: [BehaviorRequirement] behaviorRequirements =- [ BehaviorRequirement (BehaviorKey "behavior-v1-11da0fd14ad66aa0") RequiredRejection GeneratedAuthoritative GuardNotApplicable ProjectArtifactRecorded "RecordArtifact" Nothing Nothing [] 8- , BehaviorRequirement (BehaviorKey "behavior-v1-c6d79387e567590e") LiveTransition GeneratedAuthoritative GuardUnknown ProjectArtifactEmpty "RecordArtifact" (Just (K.EdgeRef ProjectArtifactEmpty 0)) (Just ProjectArtifactRecorded) ["ArtifactRecorded"] 13+ [ -- ProjectArtifactRecorded x RecordArtifact: required rejection (spec line 8)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-11da0fd14ad66aa0"+ , requirementKind = RequiredRejection+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardNotApplicable+ , requirementSource = ProjectArtifactRecorded+ , requirementCommandName = "RecordArtifact"+ , requirementExpectedEdge = Nothing+ , requirementTarget = Nothing+ , requirementEventKinds = []+ , requirementLine = 8+ }+ , -- ProjectArtifactEmpty x RecordArtifact: live transition (spec line 13)+ BehaviorRequirement+ { requirementKey = BehaviorKey "behavior-v1-c6d79387e567590e"+ , requirementKind = LiveTransition+ , requirementEvidence = GeneratedAuthoritative+ , requirementGuardCoverage = GuardUnknown+ , requirementSource = ProjectArtifactEmpty+ , requirementCommandName = "RecordArtifact"+ , requirementExpectedEdge = (Just (K.EdgeRef ProjectArtifactEmpty 0))+ , requirementTarget = Just ProjectArtifactRecorded+ , requirementEventKinds = ["ArtifactRecorded"]+ , requirementLine = 13+ } ] behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport@@ -135,7 +176,7 @@ , not (isPending witness) , Just requirement <- [Map.lookup key requiredByKey] ]- failures = [failure | (_, Left failure) <- executions]+ failures = [behaviorFailure | (_, Left behaviorFailure) <- executions] behaviorConformancePassed :: BehaviorConformanceReport -> Bool behaviorConformancePassed = behaviorConformancePassedWith False@@ -162,7 +203,7 @@ , "failed: " <> tshow (length (reportFailed report)) , countLine "verified" (reportVerified report) , countLine "unverified" (reportUnverified report)- ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]+ ] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey behaviorFailure) <> " " <> failureSubject behaviorFailure <> " [" <> failureCode behaviorFailure <> "] " <> failureDetail behaviorFailure | behaviorFailure <- reportFailed report] runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure () runWitness requirement witness = case witness of@@ -180,6 +221,7 @@ RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation +runRejection :: BehaviorRequirement -> (ProjectArtifactVertex, K.RegFile ProjectArtifactRegs) -> ProjectArtifactCommand -> LiveExpectation -> Either BehaviorFailure () runRejection requirement seed command expectation = case expectation of Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events" NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"@@ -189,6 +231,7 @@ Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness" Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject" +runAcceptance :: BehaviorRequirement -> (ProjectArtifactVertex, K.RegFile ProjectArtifactRegs) -> ProjectArtifactCommand -> LiveExpectation -> Either BehaviorFailure () runAcceptance requirement seed command expectation = case expectation of Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp" NoOp -> case K.stepDetailedEither projectArtifactTransducer seed command of@@ -204,8 +247,8 @@ checkAcceptedEnvelope requirement success let expected = NonEmpty.toList expectedEvents actual = K.stepSuccessOutputs success- ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"- ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"+ ensure requirement (actual == expected) "event-value-mismatch" ("runtime event values differ from the exact witness expectation; actual=" <> tshow actual <> " expected=" <> tshow expected)+ ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" ("runtime event kinds differ from the declared ordered envelope; actual=" <> tshow (map eventKind actual) <> " expected=" <> tshow (requirementEventKinds requirement)) decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual) replayed <- case K.applyEventsDetailedEither projectArtifactTransducer seed decoded of Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)@@ -214,10 +257,11 @@ ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers" checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed) +checkAcceptedEnvelope :: BehaviorRequirement -> K.StepSuccess ProjectArtifactRegs ProjectArtifactVertex ProjectArtifactEvent -> Either BehaviorFailure () checkAcceptedEnvelope requirement success = do- ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"- ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"- ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"+ ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" ("forward execution selected a non-live edge; actual=" <> tshow (K.stepSuccessMode success) <> " expected=" <> tshow K.Live)+ ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" ("runtime selected a different guarded sibling; actual=" <> tshow (Just (K.stepSuccessEdge success)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" ("runtime reached a different target vertex; actual=" <> tshow (Just (K.stepSuccessState success)) <> " expected=" <> tshow (requirementTarget requirement)) runReplay :: BehaviorRequirement -> [ProjectArtifactEvent] -> [ProjectArtifactEvent] -> Either BehaviorFailure () runReplay requirement prefix chunk = case requirementKind requirement of@@ -229,19 +273,21 @@ replayed <- case K.applyEventsDetailedEither projectArtifactTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure) Right replaySuccess -> Right replaySuccess- ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"+ ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" ("replay chunk reached a different target vertex; actual=" <> tshow (Just (K.replaySuccessState replayed)) <> " expected=" <> tshow (requirementTarget requirement)) checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed) _ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement" +checkSingleAttribution :: BehaviorRequirement -> K.EdgeMode -> Int -> [K.ReplayAttribution ProjectArtifactVertex] -> Either BehaviorFailure () checkSingleAttribution requirement expectedMode eventCount trace = case trace of [attribution] -> do- ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"- ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"- ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"- ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"- ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"+ ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" ("replay selected a different edge; actual=" <> tshow (Just (K.replayAttributionEdge attribution)) <> " expected=" <> tshow (requirementExpectedEdge requirement))+ ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" ("replay selected the wrong live/replay-only phase; actual=" <> tshow (K.replayAttributionMode attribution) <> " expected=" <> tshow expectedMode)+ ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" ("replay attribution starts at the wrong source; actual=" <> tshow (K.replayAttributionSource attribution) <> " expected=" <> tshow (requirementSource requirement))+ ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" ("replay attribution ends at the wrong target; actual=" <> tshow (Just (K.replayAttributionTarget attribution)) <> " expected=" <> tshow (requirementTarget requirement))+ ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" ("replay attribution did not consume the exact chunk; actual=" <> tshow (K.replayAttributionSpan attribution) <> " expected=" <> tshow (K.ReplayEventSpan 0 eventCount)) _ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution" +settleHistory :: BehaviorRequirement -> Text -> [ProjectArtifactEvent] -> Either BehaviorFailure (K.ReplaySuccess ProjectArtifactRegs ProjectArtifactVertex) settleHistory requirement label history = do decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history) case K.applyEventsDetailedEither projectArtifactTransducer (ProjectArtifactEmpty, initialProjectArtifactRegs) decoded of@@ -251,30 +297,52 @@ decodeEvents :: [ProjectArtifactEvent] -> Either Text [ProjectArtifactEvent] decodeEvents = traverse (\event -> parseProjectArtifactEvent (Codec.eventType projectArtifactCodec event) (encodeProjectArtifactEvent event)) +commandKind :: ProjectArtifactCommand -> Text commandKind command = case command of RecordArtifact _ -> "RecordArtifact" +eventKind :: ProjectArtifactEvent -> Text eventKind event = case Codec.eventType projectArtifactCodec event of Codec.EventType tag -> tag regsEqual :: K.RegFile ProjectArtifactRegs -> K.RegFile ProjectArtifactRegs -> Bool regsEqual left right = (left K.! #projectId) == (right K.! #projectId) && (left K.! #phase) == (right K.! #phase) +proofStrength :: BehaviorRequirement -> Bool proofStrength requirement = requirementEvidence requirement == GeneratedAuthoritative && requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable] +behaviorWitnessKey :: BehaviorWitness -> BehaviorKey behaviorWitnessKey witness = case witness of Pending key -> key LiveWitness { witnessKey = key } -> key ReplayWitness { witnessKey = key } -> key +isPending :: BehaviorWitness -> Bool isPending Pending {} = True isPending _ = False +ensure :: BehaviorRequirement -> Bool -> Text -> Text -> Either BehaviorFailure () ensure requirement condition code detail = if condition then Right () else failure requirement code detail-failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)+failure :: BehaviorRequirement -> Text -> Text -> Either BehaviorFailure failed+failure requirement code detail =+ Left+ ( BehaviorFailure+ (requirementKey requirement)+ (tshow (requirementSource requirement) <> " x " <> requirementCommandName requirement <> ": " <> kindPhrase <> " (spec line " <> tshow (requirementLine requirement) <> ")")+ code+ detail+ )+ where+ kindPhrase = case requirementKind requirement of+ LiveTransition -> "live transition"+ RequiredRejection -> "required rejection"+ ReplayTransition -> "replay-only transition"+sortedKeys :: [BehaviorKey] -> [BehaviorKey] sortedKeys = sortOn unBehaviorKey+keyTexts :: [BehaviorKey] -> [Text] keyTexts = map unBehaviorKey+countLine :: Text -> [BehaviorKey] -> Text countLine label values = label <> ": " <> tshow (length values) tshow :: Show value => value -> Text tshow = T.pack . show
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.Codec ( projectArtifactCodec, parseProjectArtifactEvent,@@ -7,7 +7,7 @@ ) where import Generated.WorkspaceNominalProof.ProjectArtifact.Domain-import Generated.WorkspaceNominalProof.Nominals (ProjectId, projectIdText, ProjectPhase (..), projectPhaseText)+import Generated.WorkspaceNominalProof.Nominals (projectIdText, ProjectPhase (..), projectPhaseText) import Generated.WorkspaceNominalProof.Nominals.Internal (unsafeProjectIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -60,13 +60,13 @@ <$> (unsafeProjectIdFromLegacyText <$> o .: "projectId") <*> explicitParseField (withText "ProjectPhase" parseProjectPhase) o "phase" )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes projectArtifactEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes projectArtifactEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Domain.hs view
@@ -1,10 +1,9 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..)) import Generated.WorkspaceNominalProof.Nominals (ProjectId, parseProjectId, ProjectPhase (..))
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.EventStream ( projectArtifactCategory , projectArtifactCommandCategory@@ -10,7 +10,7 @@ import Generated.WorkspaceNominalProof.ProjectArtifact.Domain import Generated.WorkspaceNominalProof.ProjectArtifact.Codec (projectArtifactCodec)-import Generated.WorkspaceNominalProof.ProjectArtifact.Transducer (projectArtifactFoldFingerprint, projectArtifactTransducer)+import Generated.WorkspaceNominalProof.ProjectArtifact.Transducer (projectArtifactTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.Harness (harnessAssertions) where import Generated.WorkspaceNominalProof.ProjectArtifact.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions projectArtifactTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: ArtifactRecorded", roundTrips sampleEventArtifactRecorded) , ("accepts RecordArtifact from ProjectArtifactEmpty", acceptRecordArtifact) ]@@ -24,12 +24,18 @@ roundTrips :: ProjectArtifactEvent -> Bool roundTrips e = parseProjectArtifactEvent (eventType projectArtifactCodec e) (encodeProjectArtifactEvent e) == Right e +sampleProjectId :: ProjectId+sampleProjectId =+ case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventArtifactRecorded :: ProjectArtifactEvent-sampleEventArtifactRecorded = (ArtifactRecorded (ArtifactRecordedData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))+sampleEventArtifactRecorded = ArtifactRecorded (ArtifactRecordedData sampleProjectId Draft) acceptRecordArtifact :: Bool acceptRecordArtifact =- case step projectArtifactTransducer (ProjectArtifactEmpty, initialProjectArtifactRegs) ((RecordArtifact (RecordArtifactData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))) of+ case step projectArtifactTransducer (ProjectArtifactEmpty, initialProjectArtifactRegs) (RecordArtifact (RecordArtifactData sampleProjectId Draft)) of Just (v, _, _) -> v == ProjectArtifactRecorded Nothing -> False @@ -37,7 +43,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRecordArtifact :: [(String, Bool)] forwardReplayRecordArtifact =- case step projectArtifactTransducer (ProjectArtifactEmpty, initialProjectArtifactRegs) ((RecordArtifact (RecordArtifactData (case parseProjectId "proj_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") Draft))) of+ case step projectArtifactTransducer (ProjectArtifactEmpty, initialProjectArtifactRegs) (RecordArtifact (RecordArtifactData sampleProjectId Draft)) of Nothing -> [(prefix <> "forward step accepted", False)] Just (forwardVertex, forwardRegs, emitted) -> case mapM (\event -> parseProjectArtifactEvent (eventType projectArtifactCodec event) (encodeProjectArtifactEvent event)) emitted of
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Projection.hs view
@@ -1,2 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.Projection () where++-- No projection declarations are present; this module keeps the generated manifest inventory total.
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ProjectArtifact/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate ProjectArtifact; do not edit. module Generated.WorkspaceNominalProof.ProjectArtifact.Transducer ( projectArtifactTransducer , projectArtifactFoldFingerprint@@ -12,7 +12,7 @@ import Generated.WorkspaceNominalProof.ProjectArtifact.Domain import Data.Text (Text)-import Generated.WorkspaceNominalProof.Nominals (ProjectId, parseProjectId, ProjectPhase (..))+import Generated.WorkspaceNominalProof.Nominals (ProjectId, ProjectPhase (..)) import Generated.WorkspaceNominalProof.Nominals qualified as GeneratedNominals import Keiki.Builder qualified as B import Keiki.Core (HsPred, SymTransducer, (.==), (.&&))
test/conformance-workspace-nominals/Generated/WorkspaceNominalProof/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context workspace-nominal-proof replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context workspace-nominal-proof replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance/Generated/HospitalCapacity/Nominals.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeFamilies #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal declarations; do not edit. module Generated.HospitalCapacity.Nominals ( BedType (..) , bedTypeText@@ -29,10 +29,9 @@ import GHC.Generics (Generic) import Keiki.Shape (CanonicalTypeName) import Generated.HospitalCapacity.Nominals.Internal (CommandId, mkCommandId, parseCommandId, commandIdText, HospitalId, mkHospitalId, parseHospitalId, hospitalIdText, TransferReservationId, mkTransferReservationId, parseTransferReservationId, transferReservationIdText)-import Keiki.Core (ExactFieldProjection (..), FieldProjection (..), FieldWitness, exactFieldWitness, fieldWitness)+import Keiki.Core (FieldProjection (..), FieldWitness, ExactFieldProjection (..), exactFieldWitness) import Data.List.NonEmpty (NonEmpty (..))-import Keiki.ProjectionDomain (TextPattern, finiteProjectionDomain, textProjectionDomain)-import Keiro.Codec.IdDomain (idDomainTextPattern, typeIdV7Domain)+import Keiki.ProjectionDomain (finiteProjectionDomain) data BedType = Icu | MedicalSurgical deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)@@ -45,8 +44,8 @@ Icu -> "icu" MedicalSurgical -> "medical-surgical" -instance CanonicalTypeName CommandId + data DivertStatus = Open | PartialDivert | TotalDivert deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -79,8 +78,8 @@ divertStatusEqualityWitness :: FieldWitness DivertStatusEqualityProjection divertStatusEqualityWitness = exactFieldWitness @DivertStatusEqualityProjection -instance CanonicalTypeName HospitalId + data PatientAcuity = RedTag | YellowTag | GreenTag deriving stock (Generic, Eq, Ord, Show, Enum, Bounded) deriving anyclass (ToJSON, FromJSON)@@ -92,5 +91,3 @@ RedTag -> "red" YellowTag -> "yellow" GreenTag -> "green"--instance CanonicalTypeName TransferReservationId
test/conformance/Generated/HospitalCapacity/Nominals/Internal.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity generated nominal ID internals; do not edit. module Generated.HospitalCapacity.Nominals.Internal ( CommandId , parseCommandId@@ -21,11 +21,14 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName) import Keiro.Codec.IdDomain (typeIdV7Domain, validateIdDomainText) newtype CommandId = CommandId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName CommandId+ instance ToJSON CommandId where toJSON = toJSON . commandIdText @@ -49,6 +52,8 @@ newtype HospitalId = HospitalId Text deriving stock (Generic, Eq, Ord, Show) +instance CanonicalTypeName HospitalId+ instance ToJSON HospitalId where toJSON = toJSON . hospitalIdText @@ -71,6 +76,8 @@ newtype TransferReservationId = TransferReservationId Text deriving stock (Generic, Eq, Ord, Show)++instance CanonicalTypeName TransferReservationId instance ToJSON TransferReservationId where toJSON = toJSON . transferReservationIdText
test/conformance/Generated/HospitalCapacity/ReplayAudit.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from context hospital-capacity replay-audit assembly; do not edit. -- -- Deployment contract: -- * replay-neutral diff: no data audit is required;
test/conformance/Generated/HospitalCapacity/Reservation/Codec.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Codec ( reservationCodec, parseReservationEvent,@@ -7,7 +7,7 @@ ) where import Generated.HospitalCapacity.Reservation.Domain-import Generated.HospitalCapacity.Nominals (CommandId, commandIdText, DivertStatus (..), divertStatusText, HospitalId, hospitalIdText, PatientAcuity (..), patientAcuityText, TransferReservationId, transferReservationIdText)+import Generated.HospitalCapacity.Nominals (commandIdText, DivertStatus (..), divertStatusText, hospitalIdText, PatientAcuity (..), patientAcuityText, transferReservationIdText) import Generated.HospitalCapacity.Nominals.Internal (unsafeCommandIdFromLegacyText, unsafeHospitalIdFromLegacyText, unsafeTransferReservationIdFromLegacyText) import Data.Aeson (Value, object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, explicitParseField, parseEither)@@ -91,13 +91,13 @@ <*> (unsafeHospitalIdFromLegacyText <$> o .: "hospitalId") <*> (unsafeCommandIdFromLegacyText <$> o .: "commandId") )- _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> _renderEventTypes reservationEventTypes)+ _ -> fail ("unknown event type " <> show tag <> "; expected one of: " <> renderExpectedEventTypes reservationEventTypes) mapLeftText :: Either String b -> Either Text b mapLeftText = either (Left . T.pack) Right -_renderEventTypes :: NonEmpty EventType -> String-_renderEventTypes =+renderExpectedEventTypes :: NonEmpty EventType -> String+renderExpectedEventTypes = T.unpack . T.intercalate ", " . map (\(EventType eventTypeName) -> eventTypeName)
test/conformance/Generated/HospitalCapacity/Reservation/Domain.hs view
@@ -1,13 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE TemplateHaskell #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Domain where import Data.Proxy (Proxy (..))-import Data.Text (Text) import GHC.Generics (Generic) import Keiki.Core (RegFile (..))-import Generated.HospitalCapacity.Nominals (CommandId, parseCommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId)+import Generated.HospitalCapacity.Nominals (CommandId, DivertStatus (..), HospitalId, parseHospitalId, PatientAcuity (..), TransferReservationId, parseTransferReservationId) import Keiki.Generics.TH (deriveAggregateCtorsAll, deriveWireCtorsAll) data ReservationVertex = ReservationUnrequested | ReservationHeld | ReservationConfirmed | ReservationExpired | ReservationAdmitted | ReservationReleased
test/conformance/Generated/HospitalCapacity/Reservation/EventStream.hs view
@@ -1,4 +1,4 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.EventStream ( reservationCategory , reservationCommandCategory@@ -10,7 +10,7 @@ import Generated.HospitalCapacity.Reservation.Domain import Generated.HospitalCapacity.Reservation.Codec (reservationCodec)-import Generated.HospitalCapacity.Reservation.Transducer (reservationFoldFingerprint, reservationTransducer)+import Generated.HospitalCapacity.Reservation.Transducer (reservationTransducer) import Keiki.Core (HsPred) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..)) import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
test/conformance/Generated/HospitalCapacity/Reservation/Harness.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedLabels #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Harness (harnessAssertions) where import Generated.HospitalCapacity.Reservation.Domain@@ -15,7 +15,7 @@ harnessAssertions :: [(String, Bool)] harnessAssertions = [ ("validateTransducer is empty", null (validateTransducer defaultValidationOptions reservationTransducer))- , ("clock-free: spec samples no wall clock", True)+ -- clock-free: spec samples no wall clock (verified at scaffold time) , ("golden round-trip: TransferReservationCreated", roundTrips sampleEventTransferReservationCreated) , ("golden round-trip: TransferReservationConfirmed", roundTrips sampleEventTransferReservationConfirmed) , ("accepts RequestTransferReservation from ReservationUnrequested", acceptRequestTransferReservation)@@ -25,15 +25,33 @@ roundTrips :: ReservationEvent -> Bool roundTrips e = parseReservationEvent (eventType reservationCodec e) (encodeReservationEvent e) == Right e +sampleCommandId :: CommandId+sampleCommandId =+ case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleHospitalId :: HospitalId+sampleHospitalId =+ case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)++sampleTransferReservationId :: TransferReservationId+sampleTransferReservationId =+ case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of+ Right parsed -> parsed+ Left problem -> error (show problem)+ sampleEventTransferReservationCreated :: ReservationEvent-sampleEventTransferReservationCreated = (TransferReservationCreated (TransferReservationCreatedData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False))+sampleEventTransferReservationCreated = TransferReservationCreated (TransferReservationCreatedData sampleTransferReservationId sampleHospitalId sampleCommandId RedTag Open False) sampleEventTransferReservationConfirmed :: ReservationEvent-sampleEventTransferReservationConfirmed = (TransferReservationConfirmed (TransferReservationConfirmedData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse")))+sampleEventTransferReservationConfirmed = TransferReservationConfirmed (TransferReservationConfirmedData sampleTransferReservationId sampleHospitalId sampleCommandId) acceptRequestTransferReservation :: Bool acceptRequestTransferReservation =- case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False))) of+ case step reservationTransducer (ReservationUnrequested, initialReservationRegs) (RequestTransferReservation (RequestTransferReservationData sampleTransferReservationId sampleHospitalId sampleCommandId RedTag Open False)) of Just (v, _, _) -> v == ReservationHeld Nothing -> False @@ -41,7 +59,7 @@ -- replay the emitted chain, and compare the final vertex and every register. forwardReplayRequestTransferReservation :: [(String, Bool)] forwardReplayRequestTransferReservation =- case step reservationTransducer (ReservationUnrequested, initialReservationRegs) ((RequestTransferReservation (RequestTransferReservationData (case parseTransferReservationId "rsv_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseHospitalId "hosp_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") (case parseCommandId "cmd_01h455vb4pex5vsknk084sn02q" of Right parsed -> parsed; Left _ -> error "generated valid ID sample failed to parse") RedTag Open False))) of+ case step reservationTransducer (ReservationUnrequested, initialReservationRegs) (RequestTransferReservation (RequestTransferReservationData sampleTransferReservationId sampleHospitalId sampleCommandId 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
test/conformance/Generated/HospitalCapacity/Reservation/Projection.hs view
@@ -1,11 +1,11 @@--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Projection- ( transfer_decisionsProjection- , transfer_decisionsStatusFor+ ( transferDecisionsProjection+ , transferDecisionsStatusFor ) where import Generated.HospitalCapacity.Reservation.Domain-import HospitalCapacity.Reservation.Holes (applyTransfer_decisions)+import HospitalCapacity.Reservation.Holes (applyTransferDecisions) import Data.Text (Text) import Keiro.Projection (InlineProjection (..)) @@ -13,14 +13,14 @@ -- 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+transferDecisionsStatusFor :: ReservationEvent -> Maybe Text+transferDecisionsStatusFor = \case TransferReservationCreated {} -> Just "held" TransferReservationConfirmed {} -> Just "confirmed" -transfer_decisionsProjection :: InlineProjection ReservationEvent-transfer_decisionsProjection =+transferDecisionsProjection :: InlineProjection ReservationEvent+transferDecisionsProjection = InlineProjection { name = "hospital-capacity-transfer_decisions-inline"- , apply = applyTransfer_decisions+ , apply = applyTransferDecisions }
test/conformance/Generated/HospitalCapacity/Reservation/Transducer.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QualifiedDo #-}--- @generated by keiro-dsl 0.9.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit.+-- @generated by keiro-dsl 0.11.0.0 (language keiro-dsl 4) from aggregate Reservation; do not edit. module Generated.HospitalCapacity.Reservation.Transducer ( reservationTransducer , reservationFoldFingerprint
test/conformance/HospitalCapacity/Reservation/Holes.hs view
@@ -7,7 +7,7 @@ module HospitalCapacity.Reservation.Holes ( transition2HeldConfirmReservationOutput1TransferReservationConfirmed,- applyTransfer_decisions,+ applyTransferDecisions, ) where @@ -23,5 +23,5 @@ commandId = command.commandId } -applyTransfer_decisions :: ReservationEvent -> recorded -> txn ()-applyTransfer_decisions _event _recorded = error "HOLE: fill transfer_decisions projection apply"+applyTransferDecisions :: ReservationEvent -> recorded -> txn ()+applyTransferDecisions _event _recorded = error "HOLE: fill transfer_decisions projection apply"
+ test/haskell-name/Main.hs view
@@ -0,0 +1,149 @@+module Main (main) where++import Data.List (permutations)+import Data.List.NonEmpty qualified as NE+import Data.Text (Text)+import Keiro.Dsl.HaskellName+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (elements, forAll)++main :: IO ()+main = hspec $ do+ describe "checked generated Haskell names" $ do+ mapM_ pinnedExample pinnedExamples++ it "rejects empty, leading, trailing, repeated, and all-underscore input" $ do+ mapM_ (shouldReject . logicalSite) ["", "_foo", "foo_", "foo__bar", "_"]++ it "admits hyphens only for wire words" $ do+ derivePair LogicalWireWord (logicalSite "incident-paging")+ `shouldBe` Right ("IncidentPaging", "incidentPaging")+ deriveHaskellName LogicalIdentifier (logicalSite "incident-paging")+ `shouldSatisfy` isLeft++ it "rejects keywords created by normalization" $ do+ deriveHaskellName LogicalIdentifier (logicalSite "Module")+ `shouldBe` Left (ReservedGeneratedOccurrence (logicalSite "Module") "module")++ it "admits contextual words accepted by the generated-language contract" $+ mapM_+ (\word -> checkedLowerOccurrence (logicalSite word) word `shouldSatisfy` isRight)+ contextualWords++ it "refuses every term-level word rejected by the generated-language contract" $+ mapM_+ (\word -> checkedLowerOccurrence (logicalSite word) word `shouldBe` Left (ReservedGeneratedOccurrence (logicalSite word) word))+ reservedWords++ it "checks explicit consumer names without recasing them" $ do+ fmap renderModuleName (checkedModuleName explicitSite "Consumer.Legacy_name.Types")+ `shouldBe` Right "Consumer.Legacy_name.Types"+ checkedModuleName explicitSite "consumer.Types" `shouldSatisfy` isLeft++ it "compares module paths case-insensitively" $ do+ let first = occurrence ContextModuleSite ModuleSpace "Generated.Foo" "Foo" "foo"+ second = occurrence ContextModuleSite ModuleSpace "generated.foo" "foo" "FOO"+ detectNameCollisions [first, second] `shouldSatisfy` (not . null)++ it "permits the same selector on different generated records" $ do+ let first = scopedField "Generated.Foo" "Command" "value" "command.value"+ second = scopedField "Generated.Foo" "Event" "value" "event.value"+ detectNameCollisions [first, second] `shouldBe` []++ prop "collision evidence is independent of source traversal order" $+ forAll (elements (permutations collisionInventory)) $ \ordered ->+ detectNameCollisions ordered == detectNameCollisions collisionInventory++ mapM_ collisionKindExample [minBound .. maxBound]++pinnedExample :: (Text, Text, Text) -> SpecWith ()+pinnedExample (raw, upper, lower) =+ it (show raw <> " -> " <> show upper <> " / " <> show lower) $+ derivePair LogicalIdentifier (logicalSite raw) `shouldBe` Right (upper, lower)++pinnedExamples :: [(Text, Text, Text)]+pinnedExamples =+ [ ("foo_bar", "FooBar", "fooBar"),+ ("fooBar", "FooBar", "fooBar"),+ ("ThingID", "ThingID", "thingID"),+ ("HTTP_server2", "HTTPServer2", "httpServer2"),+ ("version2_event", "Version2Event", "version2Event")+ ]++derivePair :: NameSourceKind -> NameSite -> Either HaskellNameError (Text, Text)+derivePair source site = do+ derived <- deriveHaskellName source site+ pure (renderUpperCamelName (upperCamel derived), renderLowerCamelName (lowerCamel derived))++shouldReject :: NameSite -> Expectation+shouldReject site = deriveHaskellName LogicalIdentifier site `shouldSatisfy` isLeft++isLeft :: Either a b -> Bool+isLeft = \case+ Left _ -> True+ Right _ -> False++isRight :: Either a b -> Bool+isRight = \case+ Left _ -> False+ Right _ -> True++contextualWords :: [Text]+contextualWords = ["as", "family", "mdo", "proc", "qualified", "rec", "safe", "signature", "stock", "unsafe", "via"]++reservedWords :: [Text]+reservedWords =+ [ "case",+ "class",+ "data",+ "default",+ "deriving",+ "do",+ "else",+ "foreign",+ "forall",+ "if",+ "import",+ "in",+ "infix",+ "infixl",+ "infixr",+ "instance",+ "let",+ "module",+ "newtype",+ "of",+ "then",+ "type",+ "where"+ ]++logicalSite :: Text -> NameSite+logicalSite raw = NameSite GeneratedTypeSite raw ("owner:" <> raw) 1++explicitSite :: NameSite+explicitSite = NameSite ImportAliasSite "consumer-reference" "consumer" 1++occurrence :: NameSiteKind -> HaskellOccurrenceSpace -> Text -> Text -> Text -> PlannedOccurrence+occurrence kind space target rendered owner =+ plannedOccurrence target space "" rendered (NameSite kind owner owner 1)++scopedField :: Text -> Text -> Text -> Text -> PlannedOccurrence+scopedField target scope rendered owner =+ plannedOccurrence target FieldSpace scope rendered (NameSite GeneratedFieldSite owner owner 1)++collisionInventory :: [PlannedOccurrence]+collisionInventory =+ [ occurrence GeneratedValueSite ValueSpace "Generated.Foo" "fooBar" "foo_bar",+ occurrence GeneratedValueSite ValueSpace "Generated.Foo" "fooBar" "fooBar"+ ]++collisionKindExample :: NameSiteKind -> SpecWith ()+collisionKindExample kind =+ it ("detects a normalized collision for " <> show kind) $ do+ let first = occurrence kind ValueSpace "Generated.Foo" "sameName" "first"+ second = occurrence kind ValueSpace "Generated.Foo" "sameName" "second"+ case detectNameCollisions [second, first] of+ [NormalizedNameCollision _ sites] -> map siteOwner (NE.toList sites) `shouldBe` ["first", "second"]+ other -> expectationFailure ("expected one collision, got " <> show other)
+ test/runtime-vocabulary/Main.hs view
@@ -0,0 +1,32 @@+-- | Cross-package guard for vocabularies keiro-dsl must know but cannot import.+--+-- keiro-dsl validates specs without depending on the @keiro@ runtime package, so+-- a handful of runtime vocabularies are restated in+-- "Keiro.Dsl.Validate". A restatement can drift; this suite is the thing that+-- stops it, by depending on both packages and comparing them directly.+--+-- Vocabularies keiro-dsl imports rather than restates (the integration envelope+-- header names, which live in @keiro-core@) need no guard and have none.+module Main (main) where++import Data.Text qualified as T+import Keiro.Dsl.Validate (runtimeTimerStatuses)+import Keiro.Timer.Schema (TimerStatus)+import System.Exit (exitFailure)++main :: IO ()+main = do+ let actual = map (T.pack . show) [minBound .. maxBound :: TimerStatus]+ if actual == runtimeTimerStatuses+ then+ putStrLn+ ( "runtime vocabulary: timer statuses agree ("+ <> show (length actual)+ <> " constructors)"+ )+ else do+ putStrLn "runtime vocabulary: timer statuses DIVERGED"+ putStrLn (" Keiro.Timer.Schema.TimerStatus: " <> show actual)+ putStrLn (" Keiro.Dsl.Validate.runtimeTimerStatuses: " <> show runtimeTimerStatuses)+ putStrLn " Update runtimeTimerStatuses to match the runtime, then rerun."+ exitFailure