seihou-core 0.5.0.0 → 0.6.0.0
raw patch · 96 files changed
+3720/−2619 lines, 96 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Seihou.Composition.Graph: [cgEdges] :: CompositionGraph -> Map ModuleInstance [ModuleInstance]
- Seihou.Composition.Graph: [cgModules] :: CompositionGraph -> Map ModuleInstance Module
- Seihou.Composition.Instance: [instanceModule] :: ModuleInstance -> ModuleName
- Seihou.Composition.Instance: [instanceParentVars] :: ModuleInstance -> ParentVars
- Seihou.Core.Migration: [blueprintPlanFrom] :: BlueprintMigrationPlan -> Version
- Seihou.Core.Migration: [blueprintPlanName] :: BlueprintMigrationPlan -> Text
- Seihou.Core.Migration: [blueprintPlanSteps] :: BlueprintMigrationPlan -> [BlueprintMigration]
- Seihou.Core.Migration: [blueprintPlanTo] :: BlueprintMigrationPlan -> Version
- Seihou.Core.Migration: [planFrom] :: MigrationPlan -> Version
- Seihou.Core.Migration: [planModule] :: MigrationPlan -> Text
- Seihou.Core.Migration: [planSteps] :: MigrationPlan -> [Migration]
- Seihou.Core.Migration: [planTo] :: MigrationPlan -> Version
- Seihou.Core.Module: [discoveredDir] :: DiscoveredModule -> FilePath
- Seihou.Core.Module: [discoveredResult] :: DiscoveredModule -> Either ModuleLoadError Module
- Seihou.Core.Module: [discoveredSource] :: DiscoveredModule -> ModuleSource
- Seihou.Core.Module: [drDescription] :: DiscoveredRunnable -> Maybe Text
- Seihou.Core.Module: [drDir] :: DiscoveredRunnable -> FilePath
- Seihou.Core.Module: [drError] :: DiscoveredRunnable -> Maybe Text
- Seihou.Core.Module: [drIsError] :: DiscoveredRunnable -> Bool
- Seihou.Core.Module: [drKind] :: DiscoveredRunnable -> RunnableKind
- Seihou.Core.Module: [drName] :: DiscoveredRunnable -> Text
- Seihou.Core.Module: [drSource] :: DiscoveredRunnable -> ModuleSource
- Seihou.Core.Registry: [diffKind] :: SyncDiff -> EntryKind
- Seihou.Core.Registry: [diffName] :: SyncDiff -> ModuleName
- Seihou.Core.Registry: [diffNew] :: SyncDiff -> Maybe Text
- Seihou.Core.Registry: [diffOld] :: SyncDiff -> Maybe Text
- Seihou.Core.Registry: [diffStatus] :: SyncDiff -> SyncStatus
- Seihou.Core.Registry: [reportBlueprintCount] :: RegistryValidationReport -> Int
- Seihou.Core.Registry: [reportIssues] :: RegistryValidationReport -> [RegistryValidationIssue]
- Seihou.Core.Registry: [reportModuleCount] :: RegistryValidationReport -> Int
- Seihou.Core.Registry: [reportPromptCount] :: RegistryValidationReport -> Int
- Seihou.Core.Registry: [reportRecipeCount] :: RegistryValidationReport -> Int
- Seihou.Core.Registry: [syncDiffs] :: SyncReport -> [SyncDiff]
- Seihou.Core.Registry: [syncUpdated] :: SyncReport -> Registry
- Seihou.Core.Types: AgentPromptLaunch :: Maybe Text -> Maybe Text -> Maybe Text -> AgentPromptLaunch
- Seihou.Core.Types: [depModule] :: Dependency -> ModuleName
- Seihou.Core.Types: [depVars] :: Dependency -> Map VarName Text
- Seihou.Core.Types: [removalCommands] :: Removal -> [Command]
- Seihou.Core.Types: [removalSteps] :: Removal -> [RemovalStep]
- Seihou.Core.Types: [targetSource] :: AppliedComposition -> FilePath
- Seihou.Core.Types: data AgentPromptLaunch
- Seihou.Core.Types: instance GHC.Classes.Eq Seihou.Core.Types.AgentPromptLaunch
- Seihou.Core.Types: instance GHC.Internal.Generics.Generic Seihou.Core.Types.AgentPromptLaunch
- Seihou.Core.Types: instance GHC.Internal.Show.Show Seihou.Core.Types.AgentPromptLaunch
- Seihou.Dhall.Eval: agentPromptLaunchDecoder :: Decoder AgentPromptLaunch
- Seihou.Effect.ConfigWriterPure: [cwGlobal] :: ConfigWriterState -> Map Text Text
- Seihou.Effect.ConfigWriterPure: [cwLocal] :: ConfigWriterState -> Map Text Text
- Seihou.Effect.ConfigWriterPure: [cwNamespaces] :: ConfigWriterState -> Map Text (Map Text Text)
- Seihou.Effect.ConsolePure: [consoleErrors] :: ConsoleState -> [Text]
- Seihou.Effect.ConsolePure: [consoleInputs] :: ConsoleState -> [Text]
- Seihou.Effect.ConsolePure: [consoleOutputs] :: ConsoleState -> [Text]
- Seihou.Effect.LoggerPure: [logDebugMsgs] :: LoggerState -> [Text]
- Seihou.Effect.LoggerPure: [logErrorMsgs] :: LoggerState -> [Text]
- Seihou.Effect.LoggerPure: [logInfoMsgs] :: LoggerState -> [Text]
- Seihou.Effect.LoggerPure: [logWarnMsgs] :: LoggerState -> [Text]
- Seihou.Effect.ProcessPure: [mockArgs] :: ProcessMock -> [Text]
- Seihou.Effect.ProcessPure: [mockCommand] :: ProcessMock -> Text
- Seihou.Effect.ProcessPure: [mockResult] :: ProcessMock -> (ExitCode, Text, Text)
- Seihou.Engine.Migrate: [planModule] :: ExecutedMigrationPlan -> ModuleName
- Seihou.Engine.Migrate: [planOps] :: ExecutedMigrationPlan -> [MigrationOpInstance]
- Seihou.Engine.Migrate: [planSource] :: ExecutedMigrationPlan -> MigrationPlan
- Seihou.Engine.Preview: [previewAnnotation] :: PreviewLine -> Text
- Seihou.Engine.Preview: [previewModule] :: PreviewLine -> Maybe ModuleName
- Seihou.Engine.Preview: [previewPath] :: PreviewLine -> FilePath
- Seihou.Engine.Preview: [previewStatus] :: PreviewLine -> FileStatus
- Seihou.Engine.Section: [sectionModule] :: SectionMarker -> ModuleName
- Seihou.Engine.Section: [sectionPrefix] :: SectionMarker -> Text
- Seihou.Engine.Validate: [diagDetails] :: DiagCheck -> [Text]
- Seihou.Engine.Validate: [diagLabel] :: DiagCheck -> Text
- Seihou.Engine.Validate: [diagSeverity] :: DiagCheck -> DiagSeverity
- Seihou.Engine.Validate: [reportChecks] :: ValidateReport -> [DiagCheck]
- Seihou.Engine.Validate: [reportDhallError] :: ValidateReport -> Maybe Text
- Seihou.Engine.Validate: [reportDhallOk] :: ValidateReport -> Bool
- Seihou.Engine.Validate: [reportModule] :: ValidateReport -> Module
- Seihou.Engine.Validate: [reportPath] :: ValidateReport -> FilePath
+ Seihou.Composition.Graph: [edges] :: CompositionGraph -> !Map ModuleInstance [ModuleInstance]
+ Seihou.Composition.Graph: [modules] :: CompositionGraph -> !Map ModuleInstance Module
+ Seihou.Composition.Graph: instance GHC.Internal.Generics.Generic Seihou.Composition.Graph.CompositionGraph
+ Seihou.Composition.Instance: [module_] :: ModuleInstance -> !ModuleName
+ Seihou.Composition.Instance: [parentVars] :: ModuleInstance -> !ParentVars
+ Seihou.Composition.Instance: instance GHC.Internal.Generics.Generic Seihou.Composition.Instance.ModuleInstance
+ Seihou.Core.AgentPrompt: checkAgentPromptLaunch :: AgentPrompt -> [Text]
+ Seihou.Core.ArtifactOriginDetect: OriginInfo :: !Text -> !Maybe Text -> !Maybe Text -> OriginInfo
+ Seihou.Core.ArtifactOriginDetect: [repoName] :: OriginInfo -> !Maybe Text
+ Seihou.Core.ArtifactOriginDetect: [sourceUrl] :: OriginInfo -> !Text
+ Seihou.Core.ArtifactOriginDetect: [version] :: OriginInfo -> !Maybe Text
+ Seihou.Core.ArtifactOriginDetect: data OriginInfo
+ Seihou.Core.ArtifactOriginDetect: detectArtifactOrigin :: FilePath -> FilePath -> IO ArtifactOrigin
+ Seihou.Core.ArtifactOriginDetect: instance Data.Aeson.Types.FromJSON.FromJSON Seihou.Core.ArtifactOriginDetect.OriginInfo
+ Seihou.Core.ArtifactOriginDetect: instance GHC.Classes.Eq Seihou.Core.ArtifactOriginDetect.OriginInfo
+ Seihou.Core.ArtifactOriginDetect: instance GHC.Internal.Generics.Generic Seihou.Core.ArtifactOriginDetect.OriginInfo
+ Seihou.Core.ArtifactOriginDetect: instance GHC.Internal.Show.Show Seihou.Core.ArtifactOriginDetect.OriginInfo
+ Seihou.Core.ArtifactOriginDetect: readOriginInfo :: FilePath -> IO (Maybe OriginInfo)
+ Seihou.Core.ArtifactRef: ArtifactNotFoundLocally :: !ArtifactOrigin -> ![FilePath] -> ArtifactRefError
+ Seihou.Core.ArtifactRef: ProjectArtifactMissing :: !ArtifactOrigin -> !FilePath -> ArtifactRefError
+ Seihou.Core.ArtifactRef: data ArtifactRefError
+ Seihou.Core.ArtifactRef: instance GHC.Classes.Eq Seihou.Core.ArtifactRef.ArtifactRefError
+ Seihou.Core.ArtifactRef: instance GHC.Internal.Generics.Generic Seihou.Core.ArtifactRef.ArtifactRefError
+ Seihou.Core.ArtifactRef: instance GHC.Internal.Show.Show Seihou.Core.ArtifactRef.ArtifactRefError
+ Seihou.Core.ArtifactRef: renderArtifactRefError :: ArtifactRefError -> Text
+ Seihou.Core.ArtifactRef: resolveArtifactOrigin :: FilePath -> [FilePath] -> FilePath -> ArtifactOrigin -> IO (Either ArtifactRefError FilePath)
+ Seihou.Core.Blueprint: checkBlueprintLaunch :: Blueprint -> [Text]
+ Seihou.Core.Migration: [module_] :: MigrationPlan -> !Text
+ Seihou.Core.Migration: [name] :: BlueprintMigrationPlan -> !Text
+ Seihou.Core.Migration: [steps] :: BlueprintMigrationPlan -> ![BlueprintMigration]
+ Seihou.Core.Module: [description] :: DiscoveredRunnable -> !Maybe Text
+ Seihou.Core.Module: [dir] :: DiscoveredRunnable -> !FilePath
+ Seihou.Core.Module: [error] :: DiscoveredRunnable -> !Maybe Text
+ Seihou.Core.Module: [isError] :: DiscoveredRunnable -> !Bool
+ Seihou.Core.Module: [kind] :: DiscoveredRunnable -> !RunnableKind
+ Seihou.Core.Module: [name] :: DiscoveredRunnable -> !Text
+ Seihou.Core.Module: [result] :: DiscoveredModule -> !Either ModuleLoadError Module
+ Seihou.Core.Module: [source] :: DiscoveredRunnable -> !ModuleSource
+ Seihou.Core.Module: instance GHC.Internal.Generics.Generic Seihou.Core.Module.DiscoveredModule
+ Seihou.Core.Module: instance GHC.Internal.Generics.Generic Seihou.Core.Module.DiscoveredRunnable
+ Seihou.Core.Registry: [blueprintCount] :: RegistryValidationReport -> !Int
+ Seihou.Core.Registry: [diffs] :: SyncReport -> ![SyncDiff]
+ Seihou.Core.Registry: [issues] :: RegistryValidationReport -> ![RegistryValidationIssue]
+ Seihou.Core.Registry: [kind] :: SyncDiff -> !EntryKind
+ Seihou.Core.Registry: [moduleCount] :: RegistryValidationReport -> !Int
+ Seihou.Core.Registry: [new] :: SyncDiff -> !Maybe Text
+ Seihou.Core.Registry: [old] :: SyncDiff -> !Maybe Text
+ Seihou.Core.Registry: [promptCount] :: RegistryValidationReport -> !Int
+ Seihou.Core.Registry: [recipeCount] :: RegistryValidationReport -> !Int
+ Seihou.Core.Registry: [status] :: SyncDiff -> !SyncStatus
+ Seihou.Core.Registry: [updated] :: SyncReport -> !Registry
+ Seihou.Core.Types: AgentLaunch :: !Maybe Text -> !Maybe Text -> !Maybe Text -> !Maybe Text -> AgentLaunch
+ Seihou.Core.Types: LocalOrigin :: !Text -> ArtifactOrigin
+ Seihou.Core.Types: ProjectOrigin :: !FilePath -> ArtifactOrigin
+ Seihou.Core.Types: RemoteOrigin :: !Text -> !Text -> !Maybe Text -> ArtifactOrigin
+ Seihou.Core.Types: [artifactName] :: ArtifactOrigin -> !Text
+ Seihou.Core.Types: [effort] :: AgentLaunch -> !Maybe Text
+ Seihou.Core.Types: [module_] :: Dependency -> !ModuleName
+ Seihou.Core.Types: [originUrl] :: ArtifactOrigin -> !Text
+ Seihou.Core.Types: [origin] :: AppliedModule -> !ArtifactOrigin
+ Seihou.Core.Types: [relativePath] :: ArtifactOrigin -> !FilePath
+ Seihou.Core.Types: [repoName] :: ArtifactOrigin -> !Maybe Text
+ Seihou.Core.Types: [targetOrigin] :: AppliedComposition -> !ArtifactOrigin
+ Seihou.Core.Types: data AgentLaunch
+ Seihou.Core.Types: data ArtifactOrigin
+ Seihou.Core.Types: instance GHC.Classes.Eq Seihou.Core.Types.AgentLaunch
+ Seihou.Core.Types: instance GHC.Classes.Eq Seihou.Core.Types.ArtifactOrigin
+ Seihou.Core.Types: instance GHC.Classes.Ord Seihou.Core.Types.ArtifactOrigin
+ Seihou.Core.Types: instance GHC.Internal.Generics.Generic Seihou.Core.Types.AgentLaunch
+ Seihou.Core.Types: instance GHC.Internal.Generics.Generic Seihou.Core.Types.ArtifactOrigin
+ Seihou.Core.Types: instance GHC.Internal.Show.Show Seihou.Core.Types.AgentLaunch
+ Seihou.Core.Types: instance GHC.Internal.Show.Show Seihou.Core.Types.ArtifactOrigin
+ Seihou.Dhall.Eval: agentLaunchDecoder :: Decoder AgentLaunch
+ Seihou.Effect.ConfigWriterPure: [global] :: ConfigWriterState -> !Map Text Text
+ Seihou.Effect.ConfigWriterPure: [local] :: ConfigWriterState -> !Map Text Text
+ Seihou.Effect.ConfigWriterPure: [namespaces] :: ConfigWriterState -> !Map Text (Map Text Text)
+ Seihou.Effect.ConfigWriterPure: instance GHC.Internal.Generics.Generic Seihou.Effect.ConfigWriterPure.ConfigWriterState
+ Seihou.Effect.ConsolePure: [errors] :: ConsoleState -> ![Text]
+ Seihou.Effect.ConsolePure: [inputs] :: ConsoleState -> ![Text]
+ Seihou.Effect.ConsolePure: [outputs] :: ConsoleState -> ![Text]
+ Seihou.Effect.ConsolePure: instance GHC.Internal.Generics.Generic Seihou.Effect.ConsolePure.ConsoleState
+ Seihou.Effect.FilesystemPure: instance GHC.Internal.Generics.Generic Seihou.Effect.FilesystemPure.PureFS
+ Seihou.Effect.LoggerPure: [debugMsgs] :: LoggerState -> ![Text]
+ Seihou.Effect.LoggerPure: [errorMsgs] :: LoggerState -> ![Text]
+ Seihou.Effect.LoggerPure: [infoMsgs] :: LoggerState -> ![Text]
+ Seihou.Effect.LoggerPure: [warnMsgs] :: LoggerState -> ![Text]
+ Seihou.Effect.LoggerPure: instance GHC.Internal.Generics.Generic Seihou.Effect.LoggerPure.LoggerState
+ Seihou.Effect.ProcessPure: [args] :: ProcessMock -> ![Text]
+ Seihou.Effect.ProcessPure: [command] :: ProcessMock -> !Text
+ Seihou.Effect.ProcessPure: [result] :: ProcessMock -> !(ExitCode, Text, Text)
+ Seihou.Effect.ProcessPure: instance GHC.Internal.Generics.Generic Seihou.Effect.ProcessPure.ProcessMock
+ Seihou.Engine.Migrate: [module_] :: ExecutedMigrationPlan -> !ModuleName
+ Seihou.Engine.Migrate: [ops] :: ExecutedMigrationPlan -> ![MigrationOpInstance]
+ Seihou.Engine.Migrate: [source] :: ExecutedMigrationPlan -> !MigrationPlan
+ Seihou.Engine.Preview: [annotation] :: PreviewLine -> !Text
+ Seihou.Engine.Preview: [module_] :: PreviewLine -> !Maybe ModuleName
+ Seihou.Engine.Preview: [path] :: PreviewLine -> !FilePath
+ Seihou.Engine.Preview: [status] :: PreviewLine -> !FileStatus
+ Seihou.Engine.Preview: instance GHC.Internal.Generics.Generic Seihou.Engine.Preview.PreviewLine
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.DesiredContext
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.DesiredFile
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.DesiredFileOwner
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.ObservedFile
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.PlannedFileState
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.ReconciliationPlan
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.ReconciliationSummary
+ Seihou.Engine.Reconcile: instance GHC.Internal.Generics.Generic Seihou.Engine.Reconcile.ResolvedFileConflict
+ Seihou.Engine.Remove: instance GHC.Internal.Generics.Generic Seihou.Engine.Remove.ExecutedRemovalPlan
+ Seihou.Engine.Remove: instance GHC.Internal.Generics.Generic Seihou.Engine.Remove.RemovalPlan
+ Seihou.Engine.Section: [module_] :: SectionMarker -> !ModuleName
+ Seihou.Engine.Section: [prefix] :: SectionMarker -> !Text
+ Seihou.Engine.Section: instance GHC.Internal.Generics.Generic Seihou.Engine.Section.SectionMarker
+ Seihou.Engine.Template: instance GHC.Internal.Generics.Generic Seihou.Engine.Template.NextBlock
+ Seihou.Engine.UpdateTransaction: instance GHC.Internal.Generics.Generic Seihou.Engine.UpdateTransaction.JournalEntry
+ Seihou.Engine.UpdateTransaction: instance GHC.Internal.Generics.Generic Seihou.Engine.UpdateTransaction.JournalMetadata
+ Seihou.Engine.UpdateTransaction: instance GHC.Internal.Generics.Generic Seihou.Engine.UpdateTransaction.UpdateTransaction
+ Seihou.Engine.Validate: [checks] :: ValidateReport -> ![DiagCheck]
+ Seihou.Engine.Validate: [details] :: DiagCheck -> ![Text]
+ Seihou.Engine.Validate: [dhallError] :: ValidateReport -> !Maybe Text
+ Seihou.Engine.Validate: [dhallOk] :: ValidateReport -> !Bool
+ Seihou.Engine.Validate: [label] :: DiagCheck -> !Text
+ Seihou.Engine.Validate: [module_] :: ValidateReport -> !Module
+ Seihou.Engine.Validate: [path] :: ValidateReport -> !FilePath
+ Seihou.Engine.Validate: [severity] :: DiagCheck -> !DiagSeverity
+ Seihou.Engine.Validate: instance GHC.Internal.Generics.Generic Seihou.Engine.Validate.DiagCheck
+ Seihou.Engine.Validate: instance GHC.Internal.Generics.Generic Seihou.Engine.Validate.ValidateReport
+ Seihou.Manifest.Types: artifactOriginName :: ArtifactOrigin -> Text
+ Seihou.Manifest.Types: instance Data.Aeson.Types.FromJSON.FromJSON Seihou.Core.Types.ArtifactOrigin
+ Seihou.Manifest.Types: instance Data.Aeson.Types.ToJSON.ToJSON Seihou.Core.Types.ArtifactOrigin
+ Seihou.Prelude: (#%%=) :: MonadState s m => ALens s s a b -> (a -> (r, b)) -> m r
+ Seihou.Prelude: (#%%~) :: Functor f => ALens s t a b -> (a -> f b) -> s -> f t
+ Seihou.Prelude: (#%=) :: MonadState s m => ALens s s a b -> (a -> b) -> m ()
+ Seihou.Prelude: (#%~) :: ALens s t a b -> (a -> b) -> s -> t
+ Seihou.Prelude: (#) :: AReview t b -> b -> t
+ Seihou.Prelude: (#=) :: MonadState s m => ALens s s a b -> b -> m ()
+ Seihou.Prelude: (#~) :: ALens s t a b -> b -> s -> t
+ Seihou.Prelude: ($dm_1) :: (Field1 s t a b, Generic s, Generic t, GIxed N0 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_10) :: (Field10 s t a b, Generic s, Generic t, GIxed N9 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_11) :: (Field11 s t a b, Generic s, Generic t, GIxed N10 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_12) :: (Field12 s t a b, Generic s, Generic t, GIxed N11 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_13) :: (Field13 s t a b, Generic s, Generic t, GIxed N12 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_14) :: (Field14 s t a b, Generic s, Generic t, GIxed N13 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_15) :: (Field15 s t a b, Generic s, Generic t, GIxed N14 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_16) :: (Field16 s t a b, Generic s, Generic t, GIxed N15 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_17) :: (Field17 s t a b, Generic s, Generic t, GIxed N16 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_18) :: (Field18 s t a b, Generic s, Generic t, GIxed N17 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_19) :: (Field19 s t a b, Generic s, Generic t, GIxed N18 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_2) :: (Field2 s t a b, Generic s, Generic t, GIxed N1 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_3) :: (Field3 s t a b, Generic s, Generic t, GIxed N2 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_4) :: (Field4 s t a b, Generic s, Generic t, GIxed N3 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_5) :: (Field5 s t a b, Generic s, Generic t, GIxed N4 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_6) :: (Field6 s t a b, Generic s, Generic t, GIxed N5 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_7) :: (Field7 s t a b, Generic s, Generic t, GIxed N6 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_8) :: (Field8 s t a b, Generic s, Generic t, GIxed N7 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_9) :: (Field9 s t a b, Generic s, Generic t, GIxed N8 (Rep s) (Rep t) a b) => Lens s t a b
+ Seihou.Prelude: ($dm_Empty) :: (AsEmpty a, Monoid a, Eq a) => Prism' a ()
+ Seihou.Prelude: ($dm_Wrapped') :: forall (d :: Meta) (c :: Meta) (s' :: Meta) a. (Wrapped s, Generic s, D1 d (C1 c (S1 s' (Rec0 a))) ~ Rep s, Unwrapped s ~ GUnwrapped (Rep s)) => Iso' s (Unwrapped s)
+ Seihou.Prelude: ($dmeach) :: forall (g :: Type -> Type). (Each s t a b, Traversable g, s ~ g a, t ~ g b) => Traversal s t a b
+ Seihou.Prelude: ($dmifoldMap) :: (FoldableWithIndex i f, TraversableWithIndex i f, Monoid m) => (i -> a -> m) -> f a -> m
+ Seihou.Prelude: ($dmimap) :: (FunctorWithIndex i f, TraversableWithIndex i f) => (i -> a -> b) -> f a -> f b
+ Seihou.Prelude: ($dmitraverse) :: (TraversableWithIndex i t, i ~ Int, Applicative f) => (i -> a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: ($dmix) :: (Ixed m, At m) => Index m -> Traversal' m (IxValue m)
+ Seihou.Prelude: ($dmplate) :: (Plated a, Data a) => Traversal' a a
+ Seihou.Prelude: (%%=) :: forall {k} s m p r (a :: k) b. MonadState s m => Over p ((,) r) s s a b -> p a (r, b) -> m r
+ Seihou.Prelude: (%%@=) :: MonadState s m => Over (Indexed i) ((,) r) s s a b -> (i -> a -> (r, b)) -> m r
+ Seihou.Prelude: (%%@~) :: forall {k1} i f s (t :: k1) a (b :: k1). Over (Indexed i) f s t a b -> (i -> a -> f b) -> s -> f t
+ Seihou.Prelude: (%%~) :: forall {k} f s (t :: k) a (b :: k). LensLike f s t a b -> (a -> f b) -> s -> f t
+ Seihou.Prelude: (%=) :: MonadState s m => ASetter s s a b -> (a -> b) -> m ()
+ Seihou.Prelude: (%@=) :: MonadState s m => AnIndexedSetter i s s a b -> (i -> a -> b) -> m ()
+ Seihou.Prelude: (%@~) :: AnIndexedSetter i s t a b -> (i -> a -> b) -> s -> t
+ Seihou.Prelude: (&&=) :: MonadState s m => ASetter' s Bool -> Bool -> m ()
+ Seihou.Prelude: (&&~) :: ASetter s t Bool Bool -> Bool -> s -> t
+ Seihou.Prelude: (&~) :: s -> State s a -> s
+ Seihou.Prelude: (**=) :: (MonadState s m, Floating a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (**~) :: Floating a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (*=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (*~) :: Num a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (+~) :: Num a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (-=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (-~) :: Num a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (...) :: forall {k} f c s t p (a :: k) b. (Applicative f, Plated c) => LensLike f s t c c -> Over p f c c a b -> Over p f s t a b
+ Seihou.Prelude: (.>) :: (st -> r) -> (kab -> st) -> kab -> r
+ Seihou.Prelude: (.@=) :: MonadState s m => AnIndexedSetter i s s a b -> (i -> b) -> m ()
+ Seihou.Prelude: (.@~) :: AnIndexedSetter i s t a b -> (i -> b) -> s -> t
+ Seihou.Prelude: (//=) :: (MonadState s m, Fractional a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (//~) :: Fractional a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (<#%=) :: MonadState s m => ALens s s a b -> (a -> b) -> m b
+ Seihou.Prelude: (<#%~) :: ALens s t a b -> (a -> b) -> s -> (b, t)
+ Seihou.Prelude: (<#=) :: MonadState s m => ALens s s a b -> b -> m b
+ Seihou.Prelude: (<#~) :: ALens s t a b -> b -> s -> (b, t)
+ Seihou.Prelude: (<%=) :: MonadState s m => LensLike ((,) b) s s a b -> (a -> b) -> m b
+ Seihou.Prelude: (<%@=) :: MonadState s m => Over (Indexed i) ((,) b) s s a b -> (i -> a -> b) -> m b
+ Seihou.Prelude: (<%@~) :: Over (Indexed i) ((,) b) s t a b -> (i -> a -> b) -> s -> (b, t)
+ Seihou.Prelude: (<%~) :: LensLike ((,) b) s t a b -> (a -> b) -> s -> (b, t)
+ Seihou.Prelude: (<&&=) :: MonadState s m => LensLike' ((,) Bool) s Bool -> Bool -> m Bool
+ Seihou.Prelude: (<&&~) :: LensLike ((,) Bool) s t Bool Bool -> Bool -> s -> (Bool, t)
+ Seihou.Prelude: (<&>) :: Functor f => f a -> (a -> b) -> f b
+ Seihou.Prelude: (<**=) :: (MonadState s m, Floating a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<**~) :: Floating a => LensLike ((,) a) s t a a -> a -> s -> (a, t)
+ Seihou.Prelude: (<*=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<*~) :: Num a => LensLike ((,) a) s t a a -> a -> s -> (a, t)
+ Seihou.Prelude: (<+=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<+~) :: Num a => LensLike ((,) a) s t a a -> a -> s -> (a, t)
+ Seihou.Prelude: (<-=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<-~) :: Num a => LensLike ((,) a) s t a a -> a -> s -> (a, t)
+ Seihou.Prelude: (<.) :: Indexable i p => (Indexed i s t -> r) -> ((a -> b) -> s -> t) -> p a b -> r
+ Seihou.Prelude: (<.=) :: MonadState s m => ASetter s s a b -> b -> m b
+ Seihou.Prelude: (<.>) :: Indexable (i, j) p => (Indexed i s t -> r) -> (Indexed j a b -> s -> t) -> p a b -> r
+ Seihou.Prelude: (<.~) :: ASetter s t a b -> b -> s -> (b, t)
+ Seihou.Prelude: (<//=) :: (MonadState s m, Fractional a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<//~) :: Fractional a => LensLike ((,) a) s t a a -> a -> s -> (a, t)
+ Seihou.Prelude: (<<%=) :: (Strong p, MonadState s m) => Over p ((,) a) s s a b -> p a b -> m a
+ Seihou.Prelude: (<<%@=) :: MonadState s m => Over (Indexed i) ((,) a) s s a b -> (i -> a -> b) -> m a
+ Seihou.Prelude: (<<%@~) :: Over (Indexed i) ((,) a) s t a b -> (i -> a -> b) -> s -> (a, t)
+ Seihou.Prelude: (<<%~) :: LensLike ((,) a) s t a b -> (a -> b) -> s -> (a, t)
+ Seihou.Prelude: (<<&&=) :: MonadState s m => LensLike' ((,) Bool) s Bool -> Bool -> m Bool
+ Seihou.Prelude: (<<&&~) :: LensLike' ((,) Bool) s Bool -> Bool -> s -> (Bool, s)
+ Seihou.Prelude: (<<**=) :: (MonadState s m, Floating a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<<**~) :: Floating a => LensLike' ((,) a) s a -> a -> s -> (a, s)
+ Seihou.Prelude: (<<*=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<<*~) :: Num a => LensLike' ((,) a) s a -> a -> s -> (a, s)
+ Seihou.Prelude: (<<+=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<<+~) :: Num a => LensLike' ((,) a) s a -> a -> s -> (a, s)
+ Seihou.Prelude: (<<-=) :: (MonadState s m, Num a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<<-~) :: Num a => LensLike' ((,) a) s a -> a -> s -> (a, s)
+ Seihou.Prelude: (<<.=) :: MonadState s m => LensLike ((,) a) s s a b -> b -> m a
+ Seihou.Prelude: (<<.~) :: LensLike ((,) a) s t a b -> b -> s -> (a, t)
+ Seihou.Prelude: (<<//=) :: (MonadState s m, Fractional a) => LensLike' ((,) a) s a -> a -> m a
+ Seihou.Prelude: (<<//~) :: Fractional a => LensLike' ((,) a) s a -> a -> s -> (a, s)
+ Seihou.Prelude: (<<<>:=) :: (MonadState s m, Semigroup r) => LensLike' ((,) r) s r -> r -> m r
+ Seihou.Prelude: (<<<>:~) :: Semigroup m => LensLike' ((,) m) s m -> m -> s -> (m, s)
+ Seihou.Prelude: (<<<>=) :: (MonadState s m, Semigroup r) => LensLike' ((,) r) s r -> r -> m r
+ Seihou.Prelude: (<<<>~) :: Semigroup r => LensLike' ((,) r) s r -> r -> s -> (r, s)
+ Seihou.Prelude: (<<<|=) :: (MonadState s m, Cons b b a a) => LensLike ((,) b) s s b b -> a -> m b
+ Seihou.Prelude: (<<<|~) :: Cons b b a a => LensLike' ((,) b) s b -> a -> s -> (b, s)
+ Seihou.Prelude: (<<>:=) :: (MonadState s m, Semigroup r) => LensLike' ((,) r) s r -> r -> m r
+ Seihou.Prelude: (<<>:~) :: Semigroup m => LensLike ((,) m) s t m m -> m -> s -> (m, t)
+ Seihou.Prelude: (<<>=) :: (MonadState s m, Semigroup r) => LensLike' ((,) r) s r -> r -> m r
+ Seihou.Prelude: (<<>~) :: Semigroup m => LensLike ((,) m) s t m m -> m -> s -> (m, t)
+ Seihou.Prelude: (<<?=) :: MonadState s m => LensLike ((,) a) s s a (Maybe b) -> b -> m a
+ Seihou.Prelude: (<<?~) :: LensLike ((,) a) s t a (Maybe b) -> b -> s -> (a, t)
+ Seihou.Prelude: (<<^=) :: (MonadState s m, Num a, Integral e) => LensLike' ((,) a) s a -> e -> m a
+ Seihou.Prelude: (<<^^=) :: (MonadState s m, Fractional a, Integral e) => LensLike' ((,) a) s a -> e -> m a
+ Seihou.Prelude: (<<^^~) :: (Fractional a, Integral e) => LensLike' ((,) a) s a -> e -> s -> (a, s)
+ Seihou.Prelude: (<<^~) :: (Num a, Integral e) => LensLike' ((,) a) s a -> e -> s -> (a, s)
+ Seihou.Prelude: (<<|=) :: (MonadState s m, Cons b b a a) => LensLike ((,) b) s s b b -> a -> m b
+ Seihou.Prelude: (<<|>=) :: (MonadState s m, Snoc b b p p) => LensLike ((,) b) s s b b -> p -> m b
+ Seihou.Prelude: (<<|>~) :: Snoc b b p p => LensLike' ((,) b) s b -> p -> s -> (b, s)
+ Seihou.Prelude: (<<||=) :: MonadState s m => LensLike' ((,) Bool) s Bool -> Bool -> m Bool
+ Seihou.Prelude: (<<||~) :: LensLike' ((,) Bool) s Bool -> Bool -> s -> (Bool, s)
+ Seihou.Prelude: (<<|~) :: Cons b b a a => LensLike ((,) b) s t b b -> a -> s -> (b, t)
+ Seihou.Prelude: (<<~) :: MonadState s m => ALens s s a b -> m b -> m b
+ Seihou.Prelude: (<>:=) :: (MonadState s m, Semigroup a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (<>:~) :: Semigroup b => ASetter s t b b -> b -> s -> t
+ Seihou.Prelude: (<>=) :: (MonadState s m, Semigroup a) => ASetter' s a -> a -> m ()
+ Seihou.Prelude: (<>~) :: Semigroup a => ASetter s t a a -> a -> s -> t
+ Seihou.Prelude: (<?=) :: MonadState s m => ASetter s s a (Maybe b) -> b -> m b
+ Seihou.Prelude: (<?~) :: ASetter s t a (Maybe b) -> b -> s -> (b, t)
+ Seihou.Prelude: (<^=) :: (MonadState s m, Num a, Integral e) => LensLike' ((,) a) s a -> e -> m a
+ Seihou.Prelude: (<^^=) :: (MonadState s m, Fractional a, Integral e) => LensLike' ((,) a) s a -> e -> m a
+ Seihou.Prelude: (<^^~) :: (Fractional a, Integral e) => LensLike ((,) a) s t a a -> e -> s -> (a, t)
+ Seihou.Prelude: (<^~) :: (Num a, Integral e) => LensLike ((,) a) s t a a -> e -> s -> (a, t)
+ Seihou.Prelude: (<|) :: Cons s s a a => a -> s -> s
+ Seihou.Prelude: (<|=) :: (MonadState s m, Cons b b a a) => ASetter s s b b -> a -> m ()
+ Seihou.Prelude: (<|>=) :: (MonadState s m, Snoc b b p p) => LensLike ((,) b) s s b b -> p -> m b
+ Seihou.Prelude: (<|>~) :: Snoc b b p p => LensLike ((,) b) s t b b -> p -> s -> (b, t)
+ Seihou.Prelude: (<||=) :: MonadState s m => LensLike' ((,) Bool) s Bool -> Bool -> m Bool
+ Seihou.Prelude: (<||~) :: LensLike ((,) Bool) s t Bool Bool -> Bool -> s -> (Bool, t)
+ Seihou.Prelude: (<|~) :: Cons b b a a => ASetter s t b b -> a -> s -> t
+ Seihou.Prelude: (<~) :: MonadState s m => ASetter s s a b -> m b -> m ()
+ Seihou.Prelude: (>$) :: Contravariant f => b -> f b -> f a
+ Seihou.Prelude: (?=) :: MonadState s m => ASetter s s a (Maybe b) -> b -> m ()
+ Seihou.Prelude: (??) :: Functor f => f (a -> b) -> a -> f b
+ Seihou.Prelude: (?~) :: ASetter s t a (Maybe b) -> b -> s -> t
+ Seihou.Prelude: (^#) :: s -> ALens s t a b -> a
+ Seihou.Prelude: (^..) :: s -> Getting (Endo [a]) s a -> [a]
+ Seihou.Prelude: (^=) :: (MonadState s m, Num a, Integral e) => ASetter' s a -> e -> m ()
+ Seihou.Prelude: (^?!) :: HasCallStack => s -> Getting (Endo a) s a -> a
+ Seihou.Prelude: (^?) :: s -> Getting (First a) s a -> Maybe a
+ Seihou.Prelude: (^@.) :: s -> IndexedGetting i (i, a) s a -> (i, a)
+ Seihou.Prelude: (^@..) :: s -> IndexedGetting i (Endo [(i, a)]) s a -> [(i, a)]
+ Seihou.Prelude: (^@?!) :: HasCallStack => s -> IndexedGetting i (Endo (i, a)) s a -> (i, a)
+ Seihou.Prelude: (^@?) :: s -> IndexedGetting i (Endo (Maybe (i, a))) s a -> Maybe (i, a)
+ Seihou.Prelude: (^^=) :: (MonadState s m, Fractional a, Integral e) => ASetter' s a -> e -> m ()
+ Seihou.Prelude: (^^~) :: (Fractional a, Integral e) => ASetter s t a a -> e -> s -> t
+ Seihou.Prelude: (^~) :: (Num a, Integral e) => ASetter s t a a -> e -> s -> t
+ Seihou.Prelude: (|>) :: Snoc s s a a => s -> a -> s
+ Seihou.Prelude: (|>=) :: (MonadState s m, Snoc b b a a) => ASetter s s b b -> a -> m ()
+ Seihou.Prelude: (|>~) :: Snoc b b a a => ASetter s t b b -> a -> s -> t
+ Seihou.Prelude: (||=) :: MonadState s m => ASetter' s Bool -> Bool -> m ()
+ Seihou.Prelude: (||~) :: ASetter s t Bool Bool -> Bool -> s -> t
+ Seihou.Prelude: Bazaar :: (forall (f :: Type -> Type). Applicative f => p a (f b) -> f t) -> Bazaar (p :: Type -> Type -> Type) a b t
+ Seihou.Prelude: Bazaar1 :: (forall (f :: Type -> Type). Apply f => p a (f b) -> f t) -> Bazaar1 (p :: Type -> Type -> Type) a b t
+ Seihou.Prelude: Const :: a -> Const a (b :: k)
+ Seihou.Prelude: Fold :: Fold s a -> ReifiedFold s a
+ Seihou.Prelude: Getter :: Getter s a -> ReifiedGetter s a
+ Seihou.Prelude: Identity :: a -> Identity a
+ Seihou.Prelude: Indexed :: (i -> a -> b) -> Indexed i a b
+ Seihou.Prelude: IndexedFold :: IndexedFold i s a -> ReifiedIndexedFold i s a
+ Seihou.Prelude: IndexedGetter :: IndexedGetter i s a -> ReifiedIndexedGetter i s a
+ Seihou.Prelude: IndexedLens :: IndexedLens i s t a b -> ReifiedIndexedLens i s t a b
+ Seihou.Prelude: IndexedSetter :: IndexedSetter i s t a b -> ReifiedIndexedSetter i s t a b
+ Seihou.Prelude: IndexedTraversal :: IndexedTraversal i s t a b -> ReifiedIndexedTraversal i s t a b
+ Seihou.Prelude: Iso :: Iso s t a b -> ReifiedIso s t a b
+ Seihou.Prelude: Lens :: Lens s t a b -> ReifiedLens s t a b
+ Seihou.Prelude: MethodName :: Name -> Name -> DefName
+ Seihou.Prelude: Prism :: Prism s t a b -> ReifiedPrism s t a b
+ Seihou.Prelude: Setter :: Setter s t a b -> ReifiedSetter s t a b
+ Seihou.Prelude: TopName :: Name -> DefName
+ Seihou.Prelude: Traversal :: Traversal s t a b -> ReifiedTraversal s t a b
+ Seihou.Prelude: [Identical] :: forall {k} {k1} (a :: k) (b :: k1). Identical a b a b
+ Seihou.Prelude: [Refl] :: forall {k} (a :: k). a :~: a
+ Seihou.Prelude: [getConst] :: Const a (b :: k) -> a
+ Seihou.Prelude: [runBazaar1] :: Bazaar1 (p :: Type -> Type -> Type) a b t -> forall (f :: Type -> Type). Apply f => p a (f b) -> f t
+ Seihou.Prelude: [runBazaar] :: Bazaar (p :: Type -> Type -> Type) a b t -> forall (f :: Type -> Type). Applicative f => p a (f b) -> f t
+ Seihou.Prelude: [runFold] :: ReifiedFold s a -> Fold s a
+ Seihou.Prelude: [runGetter] :: ReifiedGetter s a -> Getter s a
+ Seihou.Prelude: [runIdentity] :: Identity a -> a
+ Seihou.Prelude: [runIndexedFold] :: ReifiedIndexedFold i s a -> IndexedFold i s a
+ Seihou.Prelude: [runIndexedGetter] :: ReifiedIndexedGetter i s a -> IndexedGetter i s a
+ Seihou.Prelude: [runIndexedLens] :: ReifiedIndexedLens i s t a b -> IndexedLens i s t a b
+ Seihou.Prelude: [runIndexedSetter] :: ReifiedIndexedSetter i s t a b -> IndexedSetter i s t a b
+ Seihou.Prelude: [runIndexedTraversal] :: ReifiedIndexedTraversal i s t a b -> IndexedTraversal i s t a b
+ Seihou.Prelude: [runIndexed] :: Indexed i a b -> i -> a -> b
+ Seihou.Prelude: [runIso] :: ReifiedIso s t a b -> Iso s t a b
+ Seihou.Prelude: [runLens] :: ReifiedLens s t a b -> Lens s t a b
+ Seihou.Prelude: [runPrism] :: ReifiedPrism s t a b -> Prism s t a b
+ Seihou.Prelude: [runSetter] :: ReifiedSetter s t a b -> Setter s t a b
+ Seihou.Prelude: [runTraversal] :: ReifiedTraversal s t a b -> Traversal s t a b
+ Seihou.Prelude: _1 :: Field1 s t a b => Lens s t a b
+ Seihou.Prelude: _1' :: Field1 s t a b => Lens s t a b
+ Seihou.Prelude: _10 :: Field10 s t a b => Lens s t a b
+ Seihou.Prelude: _10' :: Field10 s t a b => Lens s t a b
+ Seihou.Prelude: _11 :: Field11 s t a b => Lens s t a b
+ Seihou.Prelude: _11' :: Field11 s t a b => Lens s t a b
+ Seihou.Prelude: _12 :: Field12 s t a b => Lens s t a b
+ Seihou.Prelude: _12' :: Field12 s t a b => Lens s t a b
+ Seihou.Prelude: _13 :: Field13 s t a b => Lens s t a b
+ Seihou.Prelude: _13' :: Field13 s t a b => Lens s t a b
+ Seihou.Prelude: _14 :: Field14 s t a b => Lens s t a b
+ Seihou.Prelude: _14' :: Field14 s t a b => Lens s t a b
+ Seihou.Prelude: _15 :: Field15 s t a b => Lens s t a b
+ Seihou.Prelude: _15' :: Field15 s t a b => Lens s t a b
+ Seihou.Prelude: _16 :: Field16 s t a b => Lens s t a b
+ Seihou.Prelude: _16' :: Field16 s t a b => Lens s t a b
+ Seihou.Prelude: _17 :: Field17 s t a b => Lens s t a b
+ Seihou.Prelude: _17' :: Field17 s t a b => Lens s t a b
+ Seihou.Prelude: _18 :: Field18 s t a b => Lens s t a b
+ Seihou.Prelude: _18' :: Field18 s t a b => Lens s t a b
+ Seihou.Prelude: _19 :: Field19 s t a b => Lens s t a b
+ Seihou.Prelude: _19' :: Field19 s t a b => Lens s t a b
+ Seihou.Prelude: _2 :: Field2 s t a b => Lens s t a b
+ Seihou.Prelude: _2' :: Field2 s t a b => Lens s t a b
+ Seihou.Prelude: _3 :: Field3 s t a b => Lens s t a b
+ Seihou.Prelude: _3' :: Field3 s t a b => Lens s t a b
+ Seihou.Prelude: _4 :: Field4 s t a b => Lens s t a b
+ Seihou.Prelude: _4' :: Field4 s t a b => Lens s t a b
+ Seihou.Prelude: _5 :: Field5 s t a b => Lens s t a b
+ Seihou.Prelude: _5' :: Field5 s t a b => Lens s t a b
+ Seihou.Prelude: _6 :: Field6 s t a b => Lens s t a b
+ Seihou.Prelude: _6' :: Field6 s t a b => Lens s t a b
+ Seihou.Prelude: _7 :: Field7 s t a b => Lens s t a b
+ Seihou.Prelude: _7' :: Field7 s t a b => Lens s t a b
+ Seihou.Prelude: _8 :: Field8 s t a b => Lens s t a b
+ Seihou.Prelude: _8' :: Field8 s t a b => Lens s t a b
+ Seihou.Prelude: _9 :: Field9 s t a b => Lens s t a b
+ Seihou.Prelude: _9' :: Field9 s t a b => Lens s t a b
+ Seihou.Prelude: _Cons :: Cons s t a b => Prism s t (a, s) (b, t)
+ Seihou.Prelude: _Empty :: AsEmpty a => Prism' a ()
+ Seihou.Prelude: _GWrapped' :: forall s (d :: Meta) (c :: Meta) (s' :: Meta) a. (Generic s, D1 d (C1 c (S1 s' (Rec0 a))) ~ Rep s, Unwrapped s ~ GUnwrapped (Rep s)) => Iso' s (Unwrapped s)
+ Seihou.Prelude: _Just :: forall a b p f. (Choice p, Applicative f) => p a (f b) -> p (Maybe a) (f (Maybe b))
+ Seihou.Prelude: _Left :: forall a c b p f. (Choice p, Applicative f) => p a (f b) -> p (Either a c) (f (Either b c))
+ Seihou.Prelude: _Nothing :: forall a p f. (Choice p, Applicative f) => p () (f ()) -> p (Maybe a) (f (Maybe a))
+ Seihou.Prelude: _Right :: forall c a b p f. (Choice p, Applicative f) => p a (f b) -> p (Either c a) (f (Either c b))
+ Seihou.Prelude: _Show :: (Read a, Show a) => Prism' String a
+ Seihou.Prelude: _Snoc :: Snoc s t a b => Prism s t (s, a) (t, b)
+ Seihou.Prelude: _Unwrapped :: Rewrapping s t => Iso (Unwrapped t) (Unwrapped s) t s
+ Seihou.Prelude: _Unwrapped' :: Wrapped s => Iso' (Unwrapped s) s
+ Seihou.Prelude: _Unwrapping :: Rewrapping s t => (Unwrapped s -> s) -> Iso (Unwrapped t) (Unwrapped s) t s
+ Seihou.Prelude: _Unwrapping' :: Wrapped s => (Unwrapped s -> s) -> Iso' (Unwrapped s) s
+ Seihou.Prelude: _Void :: forall s a p f. (Choice p, Applicative f) => p a (f Void) -> p s (f s)
+ Seihou.Prelude: _Wrapped :: Rewrapping s t => Iso s t (Unwrapped s) (Unwrapped t)
+ Seihou.Prelude: _Wrapped' :: Wrapped s => Iso' s (Unwrapped s)
+ Seihou.Prelude: _Wrapping :: Rewrapping s t => (Unwrapped s -> s) -> Iso s t (Unwrapped s) (Unwrapped t)
+ Seihou.Prelude: _Wrapping' :: Wrapped s => (Unwrapped s -> s) -> Iso' s (Unwrapped s)
+ Seihou.Prelude: _head :: Cons s s a a => Traversal' s a
+ Seihou.Prelude: _init :: Snoc s s a a => Traversal' s s
+ Seihou.Prelude: _last :: Snoc s s a a => Traversal' s a
+ Seihou.Prelude: _tail :: Cons s s a a => Traversal' s s
+ Seihou.Prelude: abbreviatedFields :: LensRules
+ Seihou.Prelude: abbreviatedNamer :: FieldNamer
+ Seihou.Prelude: ala :: (Functor f, Rewrapping s t) => (Unwrapped s -> s) -> ((Unwrapped t -> t) -> f s) -> f (Unwrapped s)
+ Seihou.Prelude: alaf :: (Functor f, Functor g, Rewrapping s t) => (Unwrapped s -> s) -> (f t -> g s) -> f (Unwrapped t) -> g (Unwrapped s)
+ Seihou.Prelude: allOf :: Getting All s a -> (a -> Bool) -> s -> Bool
+ Seihou.Prelude: alongside :: LensLike (AlongsideLeft f b') s t a b -> LensLike (AlongsideRight f t) s' t' a' b' -> LensLike f (s, s') (t, t') (a, a') (b, b')
+ Seihou.Prelude: altOf :: Applicative f => Getting (Alt f a) s a -> s -> f a
+ Seihou.Prelude: andOf :: Getting All s Bool -> s -> Bool
+ Seihou.Prelude: anon :: a -> (a -> Bool) -> Iso' (Maybe a) a
+ Seihou.Prelude: anyOf :: Getting Any s a -> (a -> Bool) -> s -> Bool
+ Seihou.Prelude: asIndex :: (Indexable i p, Contravariant f, Functor f) => p i (f i) -> Indexed i s (f s)
+ Seihou.Prelude: aside :: APrism s t a b -> Prism (e, s) (e, t) (e, a) (e, b)
+ Seihou.Prelude: assign :: MonadState s m => ASetter s s a b -> b -> m ()
+ Seihou.Prelude: assignA :: Arrow p => ASetter s t a b -> p s b -> p s t
+ Seihou.Prelude: asumOf :: Alternative f => Getting (Endo (f a)) s (f a) -> s -> f a
+ Seihou.Prelude: at :: At m => Index m -> Lens' m (Maybe (IxValue m))
+ Seihou.Prelude: au :: Functor f => AnIso s t a b -> ((b -> t) -> f s) -> f a
+ Seihou.Prelude: auf :: (Functor f, Functor g) => AnIso s t a b -> (f t -> g s) -> f b -> g a
+ Seihou.Prelude: backwards :: (Profunctor p, Profunctor q) => Optical p q (Backwards f) s t a b -> Optical p q f s t a b
+ Seihou.Prelude: below :: forall (f :: Type -> Type) s a. Traversable f => APrism' s a -> Prism' (f s) (f a)
+ Seihou.Prelude: beside :: (Representable q, Applicative (Rep q), Applicative f, Bitraversable r) => Optical p q f s t a b -> Optical p q f s' t' a b -> Optical p q f (r s s') (r t t') a b
+ Seihou.Prelude: bimap :: Bifunctor p => (a -> b) -> (c -> d) -> p a c -> p b d
+ Seihou.Prelude: bimapping :: forall (f :: Type -> Type -> Type) (g :: Type -> Type -> Type) s t a b s' t' a' b'. (Bifunctor f, Bifunctor g) => AnIso s t a b -> AnIso s' t' a' b' -> Iso (f s s') (g t t') (f a a') (g b b')
+ Seihou.Prelude: both :: forall (r :: Type -> Type -> Type) a b. Bitraversable r => Traversal (r a a) (r b b) a b
+ Seihou.Prelude: both1 :: forall (r :: Type -> Type -> Type) a b. Bitraversable1 r => Traversal1 (r a a) (r b b) a b
+ Seihou.Prelude: camelCaseFields :: LensRules
+ Seihou.Prelude: camelCaseNamer :: FieldNamer
+ Seihou.Prelude: censoring :: MonadWriter w m => Setter w w u v -> (u -> v) -> m a -> m a
+ Seihou.Prelude: children :: Plated a => a -> [a]
+ Seihou.Prelude: choosing :: Functor f => LensLike f s t a b -> LensLike f s' t' a b -> LensLike f (Either s s') (Either t t') a b
+ Seihou.Prelude: chosen :: forall a b p f. (Conjoined p, Functor f) => p a (f b) -> p (Either a a) (f (Either b b))
+ Seihou.Prelude: class AsEmpty a
+ Seihou.Prelude: class Ixed m => At m
+ Seihou.Prelude: class forall a. () => Functor p a => Bifunctor (p :: Type -> Type -> Type)
+ Seihou.Prelude: class Profunctor p => Choice (p :: Type -> Type -> Type)
+ Seihou.Prelude: class (Choice p, Corepresentable p, Comonad Corep p, Traversable Corep p, Strong p, Representable p, Monad Rep p, MonadFix Rep p, Distributive Rep p, Costrong p, ArrowLoop p, ArrowApply p, ArrowChoice p, Closed p) => Conjoined (p :: Type -> Type -> Type)
+ Seihou.Prelude: class Cons s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Contains m
+ Seihou.Prelude: class Contravariant (f :: Type -> Type)
+ Seihou.Prelude: class Each s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field1 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field10 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field11 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field12 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field13 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field14 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field15 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field16 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field17 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field18 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field19 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field2 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field3 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field4 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field5 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field6 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field7 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field8 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Field9 s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Foldable f => FoldableWithIndex i (f :: Type -> Type) | f -> i
+ Seihou.Prelude: class Functor f => FunctorWithIndex i (f :: Type -> Type) | f -> i
+ Seihou.Prelude: class GPlated a (g :: k -> Type)
+ Seihou.Prelude: class GPlated1 (f :: k -> Type) (g :: k -> Type)
+ Seihou.Prelude: class Generic a
+ Seihou.Prelude: class Conjoined p => Indexable i (p :: Type -> Type -> Type)
+ Seihou.Prelude: class Ixed m
+ Seihou.Prelude: class (Magnified m ~ Magnified n, MonadReader b m, MonadReader a n) => Magnify (m :: Type -> Type) (n :: Type -> Type) b a | m -> b, n -> a, m a -> n, n b -> m
+ Seihou.Prelude: class Plated a
+ Seihou.Prelude: class Prefixed t
+ Seihou.Prelude: class Profunctor (p :: Type -> Type -> Type)
+ Seihou.Prelude: class Reversing t
+ Seihou.Prelude: class (Profunctor p, Bifunctor p) => Reviewable (p :: Type -> Type -> Type)
+ Seihou.Prelude: class Wrapped s => Rewrapped s t
+ Seihou.Prelude: class (Rewrapped s t, Rewrapped t s) => Rewrapping s t
+ Seihou.Prelude: class (Applicative f, Distributive f, Traversable f) => Settable (f :: Type -> Type)
+ Seihou.Prelude: class Snoc s t a b | s -> a, t -> b, s b -> t, t a -> s
+ Seihou.Prelude: class Suffixed t
+ Seihou.Prelude: class (Functor t, Foldable t) => Traversable (t :: Type -> Type)
+ Seihou.Prelude: class (Foldable1 t, Traversable t) => Traversable1 (t :: Type -> Type)
+ Seihou.Prelude: class (FunctorWithIndex i t, FoldableWithIndex i t, Traversable t) => TraversableWithIndex i (t :: Type -> Type) | t -> i
+ Seihou.Prelude: class Ord k => TraverseMax k (m :: Type -> Type) | m -> k
+ Seihou.Prelude: class Ord k => TraverseMin k (m :: Type -> Type) | m -> k
+ Seihou.Prelude: class Wrapped s where {
+ Seihou.Prelude: class (MonadState s m, MonadState t n) => Zoom (m :: Type -> Type) (n :: Type -> Type) s t | m -> s, n -> t, m t -> n, n s -> m
+ Seihou.Prelude: classIdFields :: LensRules
+ Seihou.Prelude: classIdNamer :: FieldNamer
+ Seihou.Prelude: classUnderscoreNoPrefixFields :: LensRules
+ Seihou.Prelude: classUnderscoreNoPrefixNamer :: FieldNamer
+ Seihou.Prelude: classyRules :: LensRules
+ Seihou.Prelude: classyRules_ :: LensRules
+ Seihou.Prelude: cloneEquality :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2). AnEquality s t a b -> Equality s t a b
+ Seihou.Prelude: cloneIndexPreservingLens :: ALens s t a b -> IndexPreservingLens s t a b
+ Seihou.Prelude: cloneIndexPreservingSetter :: ASetter s t a b -> IndexPreservingSetter s t a b
+ Seihou.Prelude: cloneIndexPreservingTraversal :: ATraversal s t a b -> IndexPreservingTraversal s t a b
+ Seihou.Prelude: cloneIndexPreservingTraversal1 :: ATraversal1 s t a b -> IndexPreservingTraversal1 s t a b
+ Seihou.Prelude: cloneIndexedLens :: AnIndexedLens i s t a b -> IndexedLens i s t a b
+ Seihou.Prelude: cloneIndexedSetter :: AnIndexedSetter i s t a b -> IndexedSetter i s t a b
+ Seihou.Prelude: cloneIndexedTraversal :: AnIndexedTraversal i s t a b -> IndexedTraversal i s t a b
+ Seihou.Prelude: cloneIndexedTraversal1 :: AnIndexedTraversal1 i s t a b -> IndexedTraversal1 i s t a b
+ Seihou.Prelude: cloneIso :: AnIso s t a b -> Iso s t a b
+ Seihou.Prelude: cloneLens :: ALens s t a b -> Lens s t a b
+ Seihou.Prelude: clonePrism :: APrism s t a b -> Prism s t a b
+ Seihou.Prelude: cloneSetter :: ASetter s t a b -> Setter s t a b
+ Seihou.Prelude: cloneTraversal :: ATraversal s t a b -> Traversal s t a b
+ Seihou.Prelude: cloneTraversal1 :: ATraversal1 s t a b -> Traversal1 s t a b
+ Seihou.Prelude: coerced :: forall s t a b. (Coercible s a, Coercible t b) => Iso s t a b
+ Seihou.Prelude: composOpFold :: Plated a => b -> (b -> b -> b) -> (a -> b) -> a -> b
+ Seihou.Prelude: concatMapOf :: Getting [r] s a -> (a -> [r]) -> s -> [r]
+ Seihou.Prelude: concatOf :: Getting [r] s [r] -> s -> [r]
+ Seihou.Prelude: confusing :: Applicative f => LensLike (Curried (Yoneda f) (Yoneda f)) s t a b -> LensLike f s t a b
+ Seihou.Prelude: conjoined :: Conjoined p => (p ~ (->) => q (a -> b) r) -> q (p a b) r -> q (p a b) r
+ Seihou.Prelude: cons :: Cons s s a a => a -> s -> s
+ Seihou.Prelude: contains :: Contains m => Index m -> Lens' m Bool
+ Seihou.Prelude: contexts :: Plated a => a -> [Context a a a]
+ Seihou.Prelude: contextsOf :: ATraversal' a a -> a -> [Context a a a]
+ Seihou.Prelude: contextsOn :: Plated a => ATraversal s t a a -> s -> [Context a a t]
+ Seihou.Prelude: contextsOnOf :: ATraversal s t a a -> ATraversal' a a -> s -> [Context a a t]
+ Seihou.Prelude: contramap :: Contravariant f => (a' -> a) -> f a -> f a'
+ Seihou.Prelude: contramapped :: forall (f :: Type -> Type) b a. Contravariant f => Setter (f b) (f a) a b
+ Seihou.Prelude: contramapping :: forall (f :: Type -> Type) s t a b. Contravariant f => AnIso s t a b -> Iso (f a) (f b) (f s) (f t)
+ Seihou.Prelude: cosmos :: Plated a => Fold a a
+ Seihou.Prelude: cosmosOf :: (Applicative f, Contravariant f) => LensLike' f a a -> LensLike' f a a
+ Seihou.Prelude: cosmosOn :: (Applicative f, Contravariant f, Plated a) => LensLike' f s a -> LensLike' f s a
+ Seihou.Prelude: cosmosOnOf :: (Applicative f, Contravariant f) => LensLike' f s a -> LensLike' f a a -> LensLike' f s a
+ Seihou.Prelude: createClass :: Lens' LensRules Bool
+ Seihou.Prelude: curried :: forall a b c d e f1 p f2. (Profunctor p, Functor f2) => p (a -> b -> c) (f2 (d -> e -> f1)) -> p ((a, b) -> c) (f2 ((d, e) -> f1))
+ Seihou.Prelude: cycled :: Apply f => LensLike f s t a b -> LensLike f s t a b
+ Seihou.Prelude: data (a :: k) :~: (b :: k)
+ Seihou.Prelude: data DefName
+ Seihou.Prelude: data Identical (a :: k) (b :: k1) (s :: k) (t :: k1)
+ Seihou.Prelude: data Leftmost a
+ Seihou.Prelude: data LensRules
+ Seihou.Prelude: data Level i a
+ Seihou.Prelude: data Magma i t b a
+ Seihou.Prelude: data Rightmost a
+ Seihou.Prelude: data Sequenced a (m :: Type -> Type)
+ Seihou.Prelude: data Traversed a (f :: Type -> Type)
+ Seihou.Prelude: declareClassy :: DecsQ -> DecsQ
+ Seihou.Prelude: declareClassyFor :: [(String, (String, String))] -> [(String, String)] -> DecsQ -> DecsQ
+ Seihou.Prelude: declareFields :: DecsQ -> DecsQ
+ Seihou.Prelude: declareLenses :: DecsQ -> DecsQ
+ Seihou.Prelude: declareLensesFor :: [(String, String)] -> DecsQ -> DecsQ
+ Seihou.Prelude: declareLensesWith :: LensRules -> DecsQ -> DecsQ
+ Seihou.Prelude: declarePrisms :: DecsQ -> DecsQ
+ Seihou.Prelude: declareWrapped :: DecsQ -> DecsQ
+ Seihou.Prelude: deep :: (Conjoined p, Applicative f, Plated s) => Traversing p f s s a b -> Over p f s s a b
+ Seihou.Prelude: deepOf :: (Conjoined p, Applicative f) => LensLike f s t s t -> Traversing p f s t a b -> Over p f s t a b
+ Seihou.Prelude: defaultFieldRules :: LensRules
+ Seihou.Prelude: devoid :: forall {k} p f (a :: k) b. Over p f Void Void a b
+ Seihou.Prelude: dimap :: Profunctor p => (a -> b) -> (c -> d) -> p b c -> p a d
+ Seihou.Prelude: dimapping :: forall (p :: Type -> Type -> Type) (q :: Type -> Type -> Type) s t a b s' t' a' b'. (Profunctor p, Profunctor q) => AnIso s t a b -> AnIso s' t' a' b' -> Iso (p a s') (q b t') (p s a') (q t b')
+ Seihou.Prelude: distrib :: (Conjoined p, Functor f) => p a b -> p (f a) (f b)
+ Seihou.Prelude: dropping :: (Conjoined p, Applicative f) => Int -> Over p (Indexing f) s t a a -> Over p f s t a a
+ Seihou.Prelude: droppingWhile :: (Conjoined p, Profunctor q, Applicative f) => (a -> Bool) -> Optical p q (Compose (State Bool) f) s t a a -> Optical p q f s t a a
+ Seihou.Prelude: each :: Each s t a b => Traversal s t a b
+ Seihou.Prelude: elemIndexOf :: Eq a => IndexedGetting i (First i) s a -> a -> s -> Maybe i
+ Seihou.Prelude: elemIndicesOf :: Eq a => IndexedGetting i (Endo [i]) s a -> a -> s -> [i]
+ Seihou.Prelude: elemOf :: Eq a => Getting Any s a -> a -> s -> Bool
+ Seihou.Prelude: element :: forall (t :: Type -> Type) a. Traversable t => Int -> IndexedTraversal' Int (t a) a
+ Seihou.Prelude: elementOf :: forall (f :: Type -> Type) s t a. Applicative f => LensLike (Indexing f) s t a a -> Int -> IndexedLensLike Int f s t a a
+ Seihou.Prelude: elements :: forall (t :: Type -> Type) a. Traversable t => (Int -> Bool) -> IndexedTraversal' Int (t a) a
+ Seihou.Prelude: elementsOf :: forall (f :: Type -> Type) s t a. Applicative f => LensLike (Indexing f) s t a a -> (Int -> Bool) -> IndexedLensLike Int f s t a a
+ Seihou.Prelude: enum :: Enum a => Iso' Int a
+ Seihou.Prelude: equality :: forall {k1} {k2} (s :: k1) (a :: k1) (b :: k2) (t :: k2). (s :~: a) -> (b :~: t) -> Equality s t a b
+ Seihou.Prelude: equality' :: forall {k2} (a :: k2) (b :: k2). (a :~: b) -> Equality' a b
+ Seihou.Prelude: failing :: (Conjoined p, Applicative f) => Traversing p f s t a b -> Over p f s t a b -> Over p f s t a b
+ Seihou.Prelude: failover :: Alternative m => LensLike ((,) Any) s t a b -> (a -> b) -> s -> m t
+ Seihou.Prelude: filtered :: (Choice p, Applicative f) => (a -> Bool) -> Optic' p f a a
+ Seihou.Prelude: filteredBy :: (Indexable i p, Applicative f) => Getting (First i) a i -> p a (f a) -> a -> f a
+ Seihou.Prelude: findIndexOf :: IndexedGetting i (First i) s a -> (a -> Bool) -> s -> Maybe i
+ Seihou.Prelude: findIndicesOf :: IndexedGetting i (Endo [i]) s a -> (a -> Bool) -> s -> [i]
+ Seihou.Prelude: findMOf :: Monad m => Getting (Endo (m (Maybe a))) s a -> (a -> m Bool) -> s -> m (Maybe a)
+ Seihou.Prelude: findOf :: Getting (Endo (Maybe a)) s a -> (a -> Bool) -> s -> Maybe a
+ Seihou.Prelude: first1Of :: Getting (First a) s a -> s -> a
+ Seihou.Prelude: firstOf :: Getting (Leftmost a) s a -> s -> Maybe a
+ Seihou.Prelude: firsting :: forall (f :: Type -> Type -> Type) (g :: Type -> Type -> Type) s t a b x y. (Bifunctor f, Bifunctor g) => AnIso s t a b -> Iso (f s x) (g t y) (f a x) (g b y)
+ Seihou.Prelude: flipped :: forall a b c a' b' c' p f. (Profunctor p, Functor f) => p (b -> a -> c) (f (b' -> a' -> c')) -> p (a -> b -> c) (f (a' -> b' -> c'))
+ Seihou.Prelude: foldBy :: Foldable t => (a -> a -> a) -> a -> t a -> a
+ Seihou.Prelude: foldByOf :: Fold s a -> (a -> a -> a) -> a -> s -> a
+ Seihou.Prelude: foldMapBy :: Foldable t => (r -> r -> r) -> r -> (a -> r) -> t a -> r
+ Seihou.Prelude: foldMapByOf :: Fold s a -> (r -> r -> r) -> r -> (a -> r) -> s -> r
+ Seihou.Prelude: foldMapOf :: Getting r s a -> (a -> r) -> s -> r
+ Seihou.Prelude: foldOf :: Getting a s a -> s -> a
+ Seihou.Prelude: folded :: forall (f :: Type -> Type) a. Foldable f => IndexedFold Int (f a) a
+ Seihou.Prelude: folded64 :: forall (f :: Type -> Type) a. Foldable f => IndexedFold Int64 (f a) a
+ Seihou.Prelude: folding :: Foldable f => (s -> f a) -> Fold s a
+ Seihou.Prelude: foldl1Of :: HasCallStack => Getting (Dual (Endo (Maybe a))) s a -> (a -> a -> a) -> s -> a
+ Seihou.Prelude: foldl1Of' :: HasCallStack => Getting (Endo (Endo (Maybe a))) s a -> (a -> a -> a) -> s -> a
+ Seihou.Prelude: foldlMOf :: Monad m => Getting (Endo (r -> m r)) s a -> (r -> a -> m r) -> r -> s -> m r
+ Seihou.Prelude: foldlOf :: Getting (Dual (Endo r)) s a -> (r -> a -> r) -> r -> s -> r
+ Seihou.Prelude: foldlOf' :: Getting (Endo (Endo r)) s a -> (r -> a -> r) -> r -> s -> r
+ Seihou.Prelude: foldr1Of :: HasCallStack => Getting (Endo (Maybe a)) s a -> (a -> a -> a) -> s -> a
+ Seihou.Prelude: foldr1Of' :: HasCallStack => Getting (Dual (Endo (Endo (Maybe a)))) s a -> (a -> a -> a) -> s -> a
+ Seihou.Prelude: foldrMOf :: Monad m => Getting (Dual (Endo (r -> m r))) s a -> (a -> r -> m r) -> r -> s -> m r
+ Seihou.Prelude: foldrOf :: Getting (Endo r) s a -> (a -> r -> r) -> r -> s -> r
+ Seihou.Prelude: foldrOf' :: Getting (Dual (Endo (Endo r))) s a -> (a -> r -> r) -> r -> s -> r
+ Seihou.Prelude: foldring :: (Contravariant f, Applicative f) => ((a -> f a -> f a) -> f a -> s -> f a) -> LensLike f s t a b
+ Seihou.Prelude: for1Of_ :: Functor f => Getting (TraversedF r f) s a -> s -> (a -> f r) -> f ()
+ Seihou.Prelude: forMOf :: LensLike (WrappedMonad m) s t a b -> s -> (a -> m b) -> m t
+ Seihou.Prelude: forMOf_ :: Monad m => Getting (Sequenced r m) s a -> s -> (a -> m r) -> m ()
+ Seihou.Prelude: forOf :: LensLike f s t a b -> s -> (a -> f b) -> f t
+ Seihou.Prelude: forOf_ :: Functor f => Getting (Traversed r f) s a -> s -> (a -> f r) -> f ()
+ Seihou.Prelude: from :: AnIso s t a b -> Iso b a t s
+ Seihou.Prelude: fromEq :: forall {k2} {k1} (s :: k2) (t :: k1) (a :: k2) (b :: k1). AnEquality s t a b -> Equality b a t s
+ Seihou.Prelude: fromLeibniz :: forall {k1} {k2} (a :: k1) (b :: k2) (s :: k1) (t :: k2). (Identical a b a b -> Identical a b s t) -> Equality s t a b
+ Seihou.Prelude: fromLeibniz' :: forall {k2} (s :: k2) (a :: k2). ((s :~: s) -> s :~: a) -> Equality' s a
+ Seihou.Prelude: fusing :: Functor f => LensLike (Yoneda f) s t a b -> LensLike f s t a b
+ Seihou.Prelude: generateLazyPatterns :: Lens' LensRules Bool
+ Seihou.Prelude: generateRecordSyntax :: Lens' LensRules Bool
+ Seihou.Prelude: generateSignatures :: Lens' LensRules Bool
+ Seihou.Prelude: generateUpdateableOptics :: Lens' LensRules Bool
+ Seihou.Prelude: getting :: (Profunctor p, Profunctor q, Functor f, Contravariant f) => Optical p q f s t a b -> Optical' p q f s a
+ Seihou.Prelude: gplate :: (Generic a, GPlated a (Rep a)) => Traversal' a a
+ Seihou.Prelude: gplate1 :: forall {k} (f :: k -> Type) (a :: k). (Generic1 f, GPlated1 f (Rep1 f)) => Traversal' (f a) (f a)
+ Seihou.Prelude: has :: Getting Any s a -> s -> Bool
+ Seihou.Prelude: hasn't :: Getting All s a -> s -> Bool
+ Seihou.Prelude: head1 :: forall (t :: Type -> Type) a. Traversable1 t => Lens' (t a) a
+ Seihou.Prelude: holes :: Plated a => a -> [Pretext (->) a a a]
+ Seihou.Prelude: holes1Of :: Conjoined p => Over p (Bazaar1 p a a) s t a a -> s -> NonEmpty (Pretext p a a t)
+ Seihou.Prelude: holesOf :: Conjoined p => Over p (Bazaar p a a) s t a a -> s -> [Pretext p a a t]
+ Seihou.Prelude: holesOn :: Conjoined p => Over p (Bazaar p a a) s t a a -> s -> [Pretext p a a t]
+ Seihou.Prelude: holesOnOf :: Conjoined p => LensLike (Bazaar p r r) s t a b -> Over p (Bazaar p r r) a b r r -> s -> [Pretext p r r t]
+ Seihou.Prelude: iall :: FoldableWithIndex i f => (i -> a -> Bool) -> f a -> Bool
+ Seihou.Prelude: iallOf :: IndexedGetting i All s a -> (i -> a -> Bool) -> s -> Bool
+ Seihou.Prelude: iany :: FoldableWithIndex i f => (i -> a -> Bool) -> f a -> Bool
+ Seihou.Prelude: ianyOf :: IndexedGetting i Any s a -> (i -> a -> Bool) -> s -> Bool
+ Seihou.Prelude: iat :: At m => Index m -> IndexedLens' (Index m) m (Maybe (IxValue m))
+ Seihou.Prelude: icensoring :: MonadWriter w m => IndexedSetter i w w u v -> (i -> u -> v) -> m a -> m a
+ Seihou.Prelude: icompose :: Indexable p c => (i -> j -> p) -> (Indexed i s t -> r) -> (Indexed j a b -> s -> t) -> c a b -> r
+ Seihou.Prelude: iconcatMap :: FoldableWithIndex i f => (i -> a -> [b]) -> f a -> [b]
+ Seihou.Prelude: iconcatMapOf :: IndexedGetting i [r] s a -> (i -> a -> [r]) -> s -> [r]
+ Seihou.Prelude: icontains :: Contains m => Index m -> IndexedLens' (Index m) m Bool
+ Seihou.Prelude: idroppingWhile :: (Indexable i p, Profunctor q, Applicative f) => (i -> a -> Bool) -> Optical (Indexed i) q (Compose (State Bool) f) s t a a -> Optical p q f s t a a
+ Seihou.Prelude: ifailover :: Alternative m => Over (Indexed i) ((,) Any) s t a b -> (i -> a -> b) -> s -> m t
+ Seihou.Prelude: ifiltered :: (Indexable i p, Applicative f) => (i -> a -> Bool) -> Optical' p (Indexed i) f a a
+ Seihou.Prelude: ifind :: FoldableWithIndex i f => (i -> a -> Bool) -> f a -> Maybe (i, a)
+ Seihou.Prelude: ifindMOf :: Monad m => IndexedGetting i (Endo (m (Maybe a))) s a -> (i -> a -> m Bool) -> s -> m (Maybe a)
+ Seihou.Prelude: ifindOf :: IndexedGetting i (Endo (Maybe a)) s a -> (i -> a -> Bool) -> s -> Maybe a
+ Seihou.Prelude: ifoldMap :: (FoldableWithIndex i f, Monoid m) => (i -> a -> m) -> f a -> m
+ Seihou.Prelude: ifoldMap' :: (FoldableWithIndex i f, Monoid m) => (i -> a -> m) -> f a -> m
+ Seihou.Prelude: ifoldMapBy :: FoldableWithIndex i t => (r -> r -> r) -> r -> (i -> a -> r) -> t a -> r
+ Seihou.Prelude: ifoldMapByOf :: IndexedFold i t a -> (r -> r -> r) -> r -> (i -> a -> r) -> t -> r
+ Seihou.Prelude: ifoldMapOf :: IndexedGetting i m s a -> (i -> a -> m) -> s -> m
+ Seihou.Prelude: ifolded :: forall i (f :: Type -> Type) a. FoldableWithIndex i f => IndexedFold i (f a) a
+ Seihou.Prelude: ifolding :: (Foldable f, Indexable i p, Contravariant g, Applicative g) => (s -> f (i, a)) -> Over p g s t a b
+ Seihou.Prelude: ifoldl :: FoldableWithIndex i f => (i -> b -> a -> b) -> b -> f a -> b
+ Seihou.Prelude: ifoldl' :: FoldableWithIndex i f => (i -> b -> a -> b) -> b -> f a -> b
+ Seihou.Prelude: ifoldlM :: (FoldableWithIndex i f, Monad m) => (i -> b -> a -> m b) -> b -> f a -> m b
+ Seihou.Prelude: ifoldlMOf :: Monad m => IndexedGetting i (Endo (r -> m r)) s a -> (i -> r -> a -> m r) -> r -> s -> m r
+ Seihou.Prelude: ifoldlOf :: IndexedGetting i (Dual (Endo r)) s a -> (i -> r -> a -> r) -> r -> s -> r
+ Seihou.Prelude: ifoldlOf' :: IndexedGetting i (Endo (r -> r)) s a -> (i -> r -> a -> r) -> r -> s -> r
+ Seihou.Prelude: ifoldr :: FoldableWithIndex i f => (i -> a -> b -> b) -> b -> f a -> b
+ Seihou.Prelude: ifoldr' :: FoldableWithIndex i f => (i -> a -> b -> b) -> b -> f a -> b
+ Seihou.Prelude: ifoldrM :: (FoldableWithIndex i f, Monad m) => (i -> a -> b -> m b) -> b -> f a -> m b
+ Seihou.Prelude: ifoldrMOf :: Monad m => IndexedGetting i (Dual (Endo (r -> m r))) s a -> (i -> a -> r -> m r) -> r -> s -> m r
+ Seihou.Prelude: ifoldrOf :: IndexedGetting i (Endo r) s a -> (i -> a -> r -> r) -> r -> s -> r
+ Seihou.Prelude: ifoldrOf' :: IndexedGetting i (Dual (Endo (r -> r))) s a -> (i -> a -> r -> r) -> r -> s -> r
+ Seihou.Prelude: ifoldring :: (Indexable i p, Contravariant f, Applicative f) => ((i -> a -> f a -> f a) -> f a -> s -> f a) -> Over p f s t a b
+ Seihou.Prelude: ifor :: (TraversableWithIndex i t, Applicative f) => t a -> (i -> a -> f b) -> f (t b)
+ Seihou.Prelude: iforM :: (TraversableWithIndex i t, Monad m) => t a -> (i -> a -> m b) -> m (t b)
+ Seihou.Prelude: iforMOf :: (Indexed i a (WrappedMonad m b) -> s -> WrappedMonad m t) -> s -> (i -> a -> m b) -> m t
+ Seihou.Prelude: iforMOf_ :: Monad m => IndexedGetting i (Sequenced r m) s a -> s -> (i -> a -> m r) -> m ()
+ Seihou.Prelude: iforM_ :: (FoldableWithIndex i t, Monad m) => t a -> (i -> a -> m b) -> m ()
+ Seihou.Prelude: iforOf :: (Indexed i a (f b) -> s -> f t) -> s -> (i -> a -> f b) -> f t
+ Seihou.Prelude: iforOf_ :: Functor f => IndexedGetting i (Traversed r f) s a -> s -> (i -> a -> f r) -> f ()
+ Seihou.Prelude: ifor_ :: (FoldableWithIndex i t, Applicative f) => t a -> (i -> a -> f b) -> f ()
+ Seihou.Prelude: ignored :: Applicative f => pafb -> s -> f s
+ Seihou.Prelude: iix :: Ixed m => Index m -> IndexedTraversal' (Index m) m (IxValue m)
+ Seihou.Prelude: ilens :: (s -> (i, a)) -> (s -> b -> t) -> IndexedLens i s t a b
+ Seihou.Prelude: ilevels :: forall (f :: Type -> Type) i s t a b j. Applicative f => Traversing (Indexed i) f s t a b -> IndexedLensLike Int f s t (Level i a) (Level j b)
+ Seihou.Prelude: ilike :: (Indexable i p, Contravariant f, Functor f) => i -> a -> Over' p f s a
+ Seihou.Prelude: ilistening :: MonadWriter w m => IndexedGetting i (i, u) w u -> m a -> m (a, (i, u))
+ Seihou.Prelude: ilistenings :: MonadWriter w m => IndexedGetting i v w u -> (i -> u -> v) -> m a -> m (a, v)
+ Seihou.Prelude: ilocally :: MonadReader s m => AnIndexedSetter i s s a b -> (i -> a -> b) -> m r -> m r
+ Seihou.Prelude: iloci :: forall i a c s b p f. (Indexable i p, Applicative f) => p a (f b) -> Bazaar (Indexed i) a c s -> f (Bazaar (Indexed i) b c s)
+ Seihou.Prelude: imagma :: Over (Indexed i) (Molten i a b) s t a b -> Iso s t' (Magma i t b a) (Magma j t' c c)
+ Seihou.Prelude: imap :: FunctorWithIndex i f => (i -> a -> b) -> f a -> f b
+ Seihou.Prelude: imapAccumL :: TraversableWithIndex i t => (i -> s -> a -> (s, b)) -> s -> t a -> (s, t b)
+ Seihou.Prelude: imapAccumLOf :: Over (Indexed i) (State acc) s t a b -> (i -> acc -> a -> (acc, b)) -> acc -> s -> (acc, t)
+ Seihou.Prelude: imapAccumR :: TraversableWithIndex i t => (i -> s -> a -> (s, b)) -> s -> t a -> (s, t b)
+ Seihou.Prelude: imapAccumROf :: Over (Indexed i) (Backwards (State acc)) s t a b -> (i -> acc -> a -> (acc, b)) -> acc -> s -> (acc, t)
+ Seihou.Prelude: imapM :: (TraversableWithIndex i t, Monad m) => (i -> a -> m b) -> t a -> m (t b)
+ Seihou.Prelude: imapMOf :: Over (Indexed i) (WrappedMonad m) s t a b -> (i -> a -> m b) -> s -> m t
+ Seihou.Prelude: imapMOf_ :: Monad m => IndexedGetting i (Sequenced r m) s a -> (i -> a -> m r) -> s -> m ()
+ Seihou.Prelude: imapM_ :: (FoldableWithIndex i t, Monad m) => (i -> a -> m b) -> t a -> m ()
+ Seihou.Prelude: imapOf :: AnIndexedSetter i s t a b -> (i -> a -> b) -> s -> t
+ Seihou.Prelude: imapped :: forall i (f :: Type -> Type) a b. FunctorWithIndex i f => IndexedSetter i (f a) (f b) a b
+ Seihou.Prelude: imodifying :: MonadState s m => AnIndexedSetter i s s a b -> (i -> a -> b) -> m ()
+ Seihou.Prelude: index :: (Indexable i p, Eq i, Applicative f) => i -> Optical' p (Indexed i) f a a
+ Seihou.Prelude: indexed :: Indexable i p => p a b -> i -> a -> b
+ Seihou.Prelude: indexing :: Indexable Int p => ((a -> Indexing f b) -> s -> Indexing f t) -> p a (f b) -> s -> f t
+ Seihou.Prelude: indexing64 :: Indexable Int64 p => ((a -> Indexing64 f b) -> s -> Indexing64 f t) -> p a (f b) -> s -> f t
+ Seihou.Prelude: indices :: (Indexable i p, Applicative f) => (i -> Bool) -> Optical' p (Indexed i) f a a
+ Seihou.Prelude: infix 4 :~:
+ Seihou.Prelude: infixl 4 >$
+ Seihou.Prelude: infixl 5 `failing`
+ Seihou.Prelude: infixr 2 `zoom`
+ Seihou.Prelude: infixr 8 #
+ Seihou.Prelude: infixr 9 ...
+ Seihou.Prelude: inone :: FoldableWithIndex i f => (i -> a -> Bool) -> f a -> Bool
+ Seihou.Prelude: inoneOf :: IndexedGetting i Any s a -> (i -> a -> Bool) -> s -> Bool
+ Seihou.Prelude: inside :: forall (p :: Type -> Type -> Type) s t a b e. Corepresentable p => ALens s t a b -> Lens (p e s) (p e t) (p e a) (p e b)
+ Seihou.Prelude: involuted :: (a -> a) -> Iso' a a
+ Seihou.Prelude: iover :: AnIndexedSetter i s t a b -> (i -> a -> b) -> s -> t
+ Seihou.Prelude: ipartsOf :: (Indexable [i] p, Functor f) => Traversing (Indexed i) f s t a a -> Over p f s t [a] [a]
+ Seihou.Prelude: ipartsOf' :: forall i p f s t a. (Indexable [i] p, Functor f) => Over (Indexed i) (Bazaar' (Indexed i) a) s t a a -> Over p f s t [a] [a]
+ Seihou.Prelude: ipassing :: MonadWriter w m => IndexedSetter i w w u v -> m (a, i -> u -> v) -> m a
+ Seihou.Prelude: iplens :: (s -> a) -> (s -> b -> t) -> IndexPreservingLens s t a b
+ Seihou.Prelude: ipre :: IndexedGetting i (First (i, a)) s a -> IndexPreservingGetter s (Maybe (i, a))
+ Seihou.Prelude: ipreuse :: MonadState s m => IndexedGetting i (First (i, a)) s a -> m (Maybe (i, a))
+ Seihou.Prelude: ipreuses :: MonadState s m => IndexedGetting i (First r) s a -> (i -> a -> r) -> m (Maybe r)
+ Seihou.Prelude: ipreview :: MonadReader s m => IndexedGetting i (First (i, a)) s a -> m (Maybe (i, a))
+ Seihou.Prelude: ipreviews :: MonadReader s m => IndexedGetting i (First r) s a -> (i -> a -> r) -> m (Maybe r)
+ Seihou.Prelude: iset :: AnIndexedSetter i s t a b -> (i -> b) -> s -> t
+ Seihou.Prelude: isets :: ((i -> a -> b) -> s -> t) -> IndexedSetter i s t a b
+ Seihou.Prelude: isn't :: APrism s t a b -> s -> Bool
+ Seihou.Prelude: iso :: (s -> a) -> (b -> t) -> Iso s t a b
+ Seihou.Prelude: itakingWhile :: (Indexable i p, Profunctor q, Contravariant f, Applicative f) => (i -> a -> Bool) -> Optical' (Indexed i) q (Const (Endo (f s)) :: Type -> Type) s a -> Optical' p q f s a
+ Seihou.Prelude: iterated :: Apply f => (a -> a) -> LensLike' f a a
+ Seihou.Prelude: ito :: (Indexable i p, Contravariant f) => (s -> (i, a)) -> Over' p f s a
+ Seihou.Prelude: itoList :: FoldableWithIndex i f => f a -> [(i, a)]
+ Seihou.Prelude: itoListOf :: IndexedGetting i (Endo [(i, a)]) s a -> s -> [(i, a)]
+ Seihou.Prelude: itraverse :: (TraversableWithIndex i t, Applicative f) => (i -> a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: itraverseBy :: TraversableWithIndex i t => (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> (i -> a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: itraverseByOf :: IndexedTraversal i s t a b -> (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> (i -> a -> f b) -> s -> f t
+ Seihou.Prelude: itraverseOf :: (Indexed i a (f b) -> s -> f t) -> (i -> a -> f b) -> s -> f t
+ Seihou.Prelude: itraverseOf_ :: Functor f => IndexedGetting i (Traversed r f) s a -> (i -> a -> f r) -> s -> f ()
+ Seihou.Prelude: itraverse_ :: (FoldableWithIndex i t, Applicative f) => (i -> a -> f b) -> t a -> f ()
+ Seihou.Prelude: itraversed :: forall i (t :: Type -> Type) a b. TraversableWithIndex i t => IndexedTraversal i (t a) (t b) a b
+ Seihou.Prelude: iunsafePartsOf :: (Indexable [i] p, Functor f) => Traversing (Indexed i) f s t a b -> Over p f s t [a] [b]
+ Seihou.Prelude: iunsafePartsOf' :: forall i s t a b. Over (Indexed i) (Bazaar (Indexed i) a b) s t a b -> IndexedLens [i] s t [a] [b]
+ Seihou.Prelude: iuse :: MonadState s m => IndexedGetting i (i, a) s a -> m (i, a)
+ Seihou.Prelude: iuses :: MonadState s m => IndexedGetting i r s a -> (i -> a -> r) -> m r
+ Seihou.Prelude: iview :: MonadReader s m => IndexedGetting i (i, a) s a -> m (i, a)
+ Seihou.Prelude: iviews :: MonadReader s m => IndexedGetting i r s a -> (i -> a -> r) -> m r
+ Seihou.Prelude: ix :: Ixed m => Index m -> Traversal' m (IxValue m)
+ Seihou.Prelude: ixAt :: At m => Index m -> Traversal' m (IxValue m)
+ Seihou.Prelude: last1 :: forall (t :: Type -> Type) a. Traversable1 t => Lens' (t a) a
+ Seihou.Prelude: last1Of :: Getting (Last a) s a -> s -> a
+ Seihou.Prelude: lastOf :: Getting (Rightmost a) s a -> s -> Maybe a
+ Seihou.Prelude: lazy :: Strict lazy strict => Iso' strict lazy
+ Seihou.Prelude: left' :: Choice p => p a b -> p (Either a c) (Either b c)
+ Seihou.Prelude: lengthOf :: Getting (Endo (Endo Int)) s a -> s -> Int
+ Seihou.Prelude: lensClass :: Lens' LensRules ClassyNamer
+ Seihou.Prelude: lensField :: Lens' LensRules FieldNamer
+ Seihou.Prelude: lensRules :: LensRules
+ Seihou.Prelude: lensRulesFor :: [(String, String)] -> LensRules
+ Seihou.Prelude: levels :: forall (f :: Type -> Type) s t a b. Applicative f => Traversing (->) f s t a b -> IndexedLensLike Int f s t (Level () a) (Level () b)
+ Seihou.Prelude: lifted :: forall (m :: Type -> Type) a b. Monad m => Setter (m a) (m b) a b
+ Seihou.Prelude: like :: (Profunctor p, Contravariant f, Functor f) => a -> Optic' p f s a
+ Seihou.Prelude: lined :: forall (f :: Type -> Type). Applicative f => IndexedLensLike' Int f String String
+ Seihou.Prelude: listening :: MonadWriter w m => Getting u w u -> m a -> m (a, u)
+ Seihou.Prelude: listenings :: MonadWriter w m => Getting v w u -> (u -> v) -> m a -> m (a, v)
+ Seihou.Prelude: lmap :: Profunctor p => (a -> b) -> p b c -> p a c
+ Seihou.Prelude: lmapping :: forall (p :: Type -> Type -> Type) (q :: Type -> Type -> Type) s t a b x y. (Profunctor p, Profunctor q) => AnIso s t a b -> Iso (p a x) (q b y) (p s x) (q t y)
+ Seihou.Prelude: locally :: MonadReader s m => ASetter s s a b -> (a -> b) -> m r -> m r
+ Seihou.Prelude: loci :: forall a c s b f. Applicative f => (a -> f b) -> Bazaar (->) a c s -> f (Bazaar (->) b c s)
+ Seihou.Prelude: locus :: forall (p :: Type -> Type -> Type -> Type) a c s b. IndexedComonadStore p => Lens (p a c s) (p b c s) a b
+ Seihou.Prelude: lookingupNamer :: [(String, String)] -> FieldNamer
+ Seihou.Prelude: lookupOf :: Eq k => Getting (Endo (Maybe v)) s (k, v) -> k -> s -> Maybe v
+ Seihou.Prelude: magma :: LensLike (Mafic a b) s t a b -> Iso s u (Magma Int t b a) (Magma j u c c)
+ Seihou.Prelude: magnify :: Magnify m n b a => ((Functor (Magnified m c), Contravariant (Magnified m c)) => LensLike' (Magnified m c) a b) -> m c -> n c
+ Seihou.Prelude: makeClassy :: Name -> DecsQ
+ Seihou.Prelude: makeClassyFor :: String -> String -> [(String, String)] -> Name -> DecsQ
+ Seihou.Prelude: makeClassyPrisms :: Name -> DecsQ
+ Seihou.Prelude: makeClassy_ :: Name -> DecsQ
+ Seihou.Prelude: makeFields :: Name -> DecsQ
+ Seihou.Prelude: makeFieldsId :: Name -> DecsQ
+ Seihou.Prelude: makeFieldsNoPrefix :: Name -> DecsQ
+ Seihou.Prelude: makeLenses :: Name -> DecsQ
+ Seihou.Prelude: makeLensesFor :: [(String, String)] -> Name -> DecsQ
+ Seihou.Prelude: makeLensesWith :: LensRules -> Name -> DecsQ
+ Seihou.Prelude: makePrisms :: Name -> DecsQ
+ Seihou.Prelude: makeWrapped :: Name -> DecsQ
+ Seihou.Prelude: mapAccumLOf :: LensLike (State acc) s t a b -> (acc -> a -> (acc, b)) -> acc -> s -> (acc, t)
+ Seihou.Prelude: mapAccumROf :: LensLike (Backwards (State acc)) s t a b -> (acc -> a -> (acc, b)) -> acc -> s -> (acc, t)
+ Seihou.Prelude: mapEq :: forall k1 k2 (s :: k1) (t :: k2) (a :: k1) (b :: k2) f. AnEquality s t a b -> f s -> f a
+ Seihou.Prelude: mapMOf :: LensLike (WrappedMonad m) s t a b -> (a -> m b) -> s -> m t
+ Seihou.Prelude: mapMOf_ :: Monad m => Getting (Sequenced r m) s a -> (a -> m r) -> s -> m ()
+ Seihou.Prelude: mapOf :: ASetter s t a b -> (a -> b) -> s -> t
+ Seihou.Prelude: mapped :: forall (f :: Type -> Type) a b. Functor f => Setter (f a) (f b) a b
+ Seihou.Prelude: mapping :: forall (f :: Type -> Type) (g :: Type -> Type) s t a b. (Functor f, Functor g) => AnIso s t a b -> Iso (f s) (g t) (f a) (g b)
+ Seihou.Prelude: mappingNamer :: (String -> [String]) -> FieldNamer
+ Seihou.Prelude: matching :: APrism s t a b -> s -> Either t a
+ Seihou.Prelude: matching' :: LensLike (Either a) s t a b -> s -> Either t a
+ Seihou.Prelude: maximum1Of :: Ord a => Getting (Max a) s a -> s -> a
+ Seihou.Prelude: maximumByOf :: Getting (Endo (Endo (Maybe a))) s a -> (a -> a -> Ordering) -> s -> Maybe a
+ Seihou.Prelude: maximumOf :: Ord a => Getting (Endo (Endo (Maybe a))) s a -> s -> Maybe a
+ Seihou.Prelude: minimum1Of :: Ord a => Getting (Min a) s a -> s -> a
+ Seihou.Prelude: minimumByOf :: Getting (Endo (Endo (Maybe a))) s a -> (a -> a -> Ordering) -> s -> Maybe a
+ Seihou.Prelude: minimumOf :: Ord a => Getting (Endo (Endo (Maybe a))) s a -> s -> Maybe a
+ Seihou.Prelude: modifying :: MonadState s m => ASetter s s a b -> (a -> b) -> m ()
+ Seihou.Prelude: msumOf :: MonadPlus m => Getting (Endo (m a)) s (m a) -> s -> m a
+ Seihou.Prelude: nearly :: a -> (a -> Bool) -> Prism' a ()
+ Seihou.Prelude: newtype Bazaar (p :: Type -> Type -> Type) a b t
+ Seihou.Prelude: newtype Bazaar1 (p :: Type -> Type -> Type) a b t
+ Seihou.Prelude: newtype Const a (b :: k)
+ Seihou.Prelude: newtype Identity a
+ Seihou.Prelude: newtype Indexed i a b
+ Seihou.Prelude: newtype ReifiedFold s a
+ Seihou.Prelude: newtype ReifiedGetter s a
+ Seihou.Prelude: newtype ReifiedIndexedFold i s a
+ Seihou.Prelude: newtype ReifiedIndexedGetter i s a
+ Seihou.Prelude: newtype ReifiedIndexedLens i s t a b
+ Seihou.Prelude: newtype ReifiedIndexedSetter i s t a b
+ Seihou.Prelude: newtype ReifiedIndexedTraversal i s t a b
+ Seihou.Prelude: newtype ReifiedIso s t a b
+ Seihou.Prelude: newtype ReifiedLens s t a b
+ Seihou.Prelude: newtype ReifiedPrism s t a b
+ Seihou.Prelude: newtype ReifiedSetter s t a b
+ Seihou.Prelude: newtype ReifiedTraversal s t a b
+ Seihou.Prelude: non :: Eq a => a -> Iso' (Maybe a) a
+ Seihou.Prelude: non' :: APrism' a () -> Iso' (Maybe a) a
+ Seihou.Prelude: none :: Foldable f => (a -> Bool) -> f a -> Bool
+ Seihou.Prelude: noneOf :: Getting Any s a -> (a -> Bool) -> s -> Bool
+ Seihou.Prelude: notElemOf :: Eq a => Getting All s a -> a -> s -> Bool
+ Seihou.Prelude: notNullOf :: Getting Any s a -> s -> Bool
+ Seihou.Prelude: nullOf :: Getting All s a -> s -> Bool
+ Seihou.Prelude: only :: Eq a => a -> Prism' a ()
+ Seihou.Prelude: op :: Wrapped s => (Unwrapped s -> s) -> s -> Unwrapped s
+ Seihou.Prelude: orOf :: Getting Any s Bool -> s -> Bool
+ Seihou.Prelude: outside :: forall (p :: Type -> Type -> Type) s t a b r. Representable p => APrism s t a b -> Lens (p t r) (p s r) (p b r) (p a r)
+ Seihou.Prelude: overA :: Arrow ar => LensLike (Context a b) s t a b -> ar a b -> ar s t
+ Seihou.Prelude: overEquality :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2) p. AnEquality s t a b -> p a b -> p s t
+ Seihou.Prelude: para :: Plated a => (a -> [r] -> r) -> a -> r
+ Seihou.Prelude: paraOf :: Getting (Endo [a]) a a -> (a -> [r] -> r) -> a -> r
+ Seihou.Prelude: parts :: Plated a => Lens' a [a]
+ Seihou.Prelude: partsOf :: Functor f => Traversing (->) f s t a a -> LensLike f s t [a] [a]
+ Seihou.Prelude: partsOf' :: ATraversal s t a a -> Lens s t [a] [a]
+ Seihou.Prelude: passing :: MonadWriter w m => Setter w w u v -> m (a, u -> v) -> m a
+ Seihou.Prelude: pattern Wrapped :: Rewrapped s s => Unwrapped s -> s
+ Seihou.Prelude: pattern (:>) :: Snoc a a b b => a -> b -> a
+ Seihou.Prelude: pattern Swapped :: Swap p => p b a -> p a b
+ Seihou.Prelude: pattern Empty :: AsEmpty s => s
+ Seihou.Prelude: pattern Unwrapped :: Rewrapped t t => t -> Unwrapped t
+ Seihou.Prelude: plate :: Plated a => Traversal' a a
+ Seihou.Prelude: pre :: Getting (First a) s a -> IndexPreservingGetter s (Maybe a)
+ Seihou.Prelude: prefixed :: Prefixed t => t -> Prism' t t
+ Seihou.Prelude: preuse :: MonadState s m => Getting (First a) s a -> m (Maybe a)
+ Seihou.Prelude: preuses :: MonadState s m => Getting (First r) s a -> (a -> r) -> m (Maybe r)
+ Seihou.Prelude: preview :: MonadReader s m => Getting (First a) s a -> m (Maybe a)
+ Seihou.Prelude: previews :: MonadReader s m => Getting (First r) s a -> (a -> r) -> m (Maybe r)
+ Seihou.Prelude: prism :: (b -> t) -> (s -> Either t a) -> Prism s t a b
+ Seihou.Prelude: prism' :: (b -> s) -> (s -> Maybe a) -> Prism s s a b
+ Seihou.Prelude: productOf :: Num a => Getting (Endo (Endo a)) s a -> s -> a
+ Seihou.Prelude: re :: AReview t b -> Getter b t
+ Seihou.Prelude: reindexed :: Indexable j p => (i -> j) -> (Indexed i a b -> r) -> p a b -> r
+ Seihou.Prelude: repeated :: Apply f => LensLike' f a a
+ Seihou.Prelude: replicated :: Int -> Fold a a
+ Seihou.Prelude: retagged :: (Profunctor p, Bifunctor p) => p a b -> p s b
+ Seihou.Prelude: reuse :: MonadState b m => AReview t b -> m t
+ Seihou.Prelude: reuses :: MonadState b m => AReview t b -> (t -> r) -> m r
+ Seihou.Prelude: reversed :: Reversing a => Iso' a a
+ Seihou.Prelude: reversing :: Reversing t => t -> t
+ Seihou.Prelude: review :: MonadReader b m => AReview t b -> m t
+ Seihou.Prelude: reviewing :: (Bifunctor p, Functor f) => Optic (Tagged :: Type -> Type -> Type) Identity s t a b -> Optic' p f t b
+ Seihou.Prelude: reviews :: MonadReader b m => AReview t b -> (t -> r) -> m r
+ Seihou.Prelude: rewrite :: Plated a => (a -> Maybe a) -> a -> a
+ Seihou.Prelude: rewriteM :: (Monad m, Plated a) => (a -> m (Maybe a)) -> a -> m a
+ Seihou.Prelude: rewriteMOf :: Monad m => LensLike (WrappedMonad m) a b a b -> (b -> m (Maybe a)) -> a -> m b
+ Seihou.Prelude: rewriteMOn :: (Monad m, Plated a) => LensLike (WrappedMonad m) s t a a -> (a -> m (Maybe a)) -> s -> m t
+ Seihou.Prelude: rewriteMOnOf :: Monad m => LensLike (WrappedMonad m) s t a b -> LensLike (WrappedMonad m) a b a b -> (b -> m (Maybe a)) -> s -> m t
+ Seihou.Prelude: rewriteOf :: ASetter a b a b -> (b -> Maybe a) -> a -> b
+ Seihou.Prelude: rewriteOn :: Plated a => ASetter s t a a -> (a -> Maybe a) -> s -> t
+ Seihou.Prelude: rewriteOnOf :: ASetter s t a b -> ASetter a b a b -> (b -> Maybe a) -> s -> t
+ Seihou.Prelude: right' :: Choice p => p a b -> p (Either c a) (Either c b)
+ Seihou.Prelude: rmap :: Profunctor p => (b -> c) -> p a b -> p a c
+ Seihou.Prelude: rmapping :: forall (p :: Type -> Type -> Type) (q :: Type -> Type -> Type) s t a b x y. (Profunctor p, Profunctor q) => AnIso s t a b -> Iso (p x s) (q y t) (p x a) (q y b)
+ Seihou.Prelude: runEq :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2). AnEquality s t a b -> Identical s t a b
+ Seihou.Prelude: sans :: At m => Index m -> m -> m
+ Seihou.Prelude: scanl1Of :: LensLike (State (Maybe a)) s t a a -> (a -> a -> a) -> s -> t
+ Seihou.Prelude: scanr1Of :: LensLike (Backwards (State (Maybe a))) s t a a -> (a -> a -> a) -> s -> t
+ Seihou.Prelude: scribe :: (MonadWriter t m, Monoid s) => ASetter s t a b -> b -> m ()
+ Seihou.Prelude: seconding :: forall (f :: Type -> Type -> Type) (g :: Type -> Type -> Type) s t a b x y. (Bifunctor f, Bifunctor g) => AnIso s t a b -> Iso (f x s) (g y t) (f x a) (g y b)
+ Seihou.Prelude: selfIndex :: Indexable a p => p a fb -> a -> fb
+ Seihou.Prelude: sequence1Of_ :: Functor f => Getting (TraversedF a f) s (f a) -> s -> f ()
+ Seihou.Prelude: sequenceAOf :: LensLike f s t (f b) b -> s -> f t
+ Seihou.Prelude: sequenceAOf_ :: Functor f => Getting (Traversed a f) s (f a) -> s -> f ()
+ Seihou.Prelude: sequenceBy :: Traversable t => (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> t (f a) -> f (t a)
+ Seihou.Prelude: sequenceByOf :: Traversal s t (f b) b -> (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> s -> f t
+ Seihou.Prelude: sequenceOf :: LensLike (WrappedMonad m) s t (m b) b -> s -> m t
+ Seihou.Prelude: sequenceOf_ :: Monad m => Getting (Sequenced a m) s (m a) -> s -> m ()
+ Seihou.Prelude: set' :: ASetter' s a -> a -> s -> s
+ Seihou.Prelude: sets :: (Profunctor p, Profunctor q, Settable f) => (p a b -> q s t) -> Optical p q f s t a b
+ Seihou.Prelude: setting :: ((a -> b) -> s -> t) -> IndexPreservingSetter s t a b
+ Seihou.Prelude: simple :: forall {k2} (a :: k2) k3 p (f :: k2 -> k3). p a (f a) -> p a (f a)
+ Seihou.Prelude: simpleLenses :: Lens' LensRules Bool
+ Seihou.Prelude: simply :: forall {k} {k1} p (f :: k -> k1) (s :: k) (a :: k) r. (Optic' p f s a -> r) -> Optic' p f s a -> r
+ Seihou.Prelude: singular :: (HasCallStack, Conjoined p, Functor f) => Traversing p f s t a a -> Over p f s t a a
+ Seihou.Prelude: snoc :: Snoc s s a a => s -> a -> s
+ Seihou.Prelude: storing :: ALens s t a b -> b -> s -> t
+ Seihou.Prelude: strict :: Strict lazy strict => Iso' lazy strict
+ Seihou.Prelude: substEq :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2) r. AnEquality s t a b -> ((s ~ a, t ~ b) => r) -> r
+ Seihou.Prelude: suffixed :: Suffixed t => t -> Prism' t t
+ Seihou.Prelude: sumOf :: Num a => Getting (Endo (Endo a)) s a -> s -> a
+ Seihou.Prelude: swapped :: forall (p :: Type -> Type -> Type) a b c d. Swap p => Iso (p a b) (p c d) (p b a) (p d c)
+ Seihou.Prelude: taking :: (Conjoined p, Applicative f) => Int -> Traversing p f s t a a -> Over p f s t a a
+ Seihou.Prelude: takingWhile :: (Conjoined p, Applicative f) => (a -> Bool) -> Over p (TakingWhile p f a a) s t a a -> Over p f s t a a
+ Seihou.Prelude: to :: (Profunctor p, Contravariant f) => (s -> a) -> Optic' p f s a
+ Seihou.Prelude: toListOf :: Getting (Endo [a]) s a -> s -> [a]
+ Seihou.Prelude: toNonEmptyOf :: Getting (NonEmptyDList a) s a -> s -> NonEmpty a
+ Seihou.Prelude: transform :: Plated a => (a -> a) -> a -> a
+ Seihou.Prelude: transformM :: (Monad m, Plated a) => (a -> m a) -> a -> m a
+ Seihou.Prelude: transformMOf :: Monad m => LensLike (WrappedMonad m) a b a b -> (b -> m b) -> a -> m b
+ Seihou.Prelude: transformMOn :: (Monad m, Plated a) => LensLike (WrappedMonad m) s t a a -> (a -> m a) -> s -> m t
+ Seihou.Prelude: transformMOnOf :: Monad m => LensLike (WrappedMonad m) s t a b -> LensLike (WrappedMonad m) a b a b -> (b -> m b) -> s -> m t
+ Seihou.Prelude: transformOf :: ASetter a b a b -> (b -> b) -> a -> b
+ Seihou.Prelude: transformOn :: Plated a => ASetter s t a a -> (a -> a) -> s -> t
+ Seihou.Prelude: transformOnOf :: ASetter s t a b -> ASetter a b a b -> (b -> b) -> s -> t
+ Seihou.Prelude: transposeOf :: LensLike ZipList s t [a] a -> s -> [t]
+ Seihou.Prelude: traversal :: ((a -> f b) -> s -> f t) -> LensLike f s t a b
+ Seihou.Prelude: traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: traverse1 :: (Traversable1 t, Apply f) => (a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: traverse1Of_ :: Functor f => Getting (TraversedF r f) s a -> (a -> f r) -> s -> f ()
+ Seihou.Prelude: traverseBy :: Traversable t => (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> (a -> f b) -> t a -> f (t b)
+ Seihou.Prelude: traverseByOf :: Traversal s t a b -> (forall x. () => x -> f x) -> (forall x y. () => f (x -> y) -> f x -> f y) -> (a -> f b) -> s -> f t
+ Seihou.Prelude: traverseMax :: TraverseMax k m => IndexedTraversal' k (m v) v
+ Seihou.Prelude: traverseMin :: TraverseMin k m => IndexedTraversal' k (m v) v
+ Seihou.Prelude: traverseOf :: LensLike f s t a b -> (a -> f b) -> s -> f t
+ Seihou.Prelude: traverseOf_ :: Functor f => Getting (Traversed r f) s a -> (a -> f r) -> s -> f ()
+ Seihou.Prelude: traversed :: forall (f :: Type -> Type) a b. Traversable f => IndexedTraversal Int (f a) (f b) a b
+ Seihou.Prelude: traversed1 :: forall (f :: Type -> Type) a b. Traversable1 f => IndexedTraversal1 Int (f a) (f b) a b
+ Seihou.Prelude: traversed64 :: forall (f :: Type -> Type) a b. Traversable f => IndexedTraversal Int64 (f a) (f b) a b
+ Seihou.Prelude: type ALens s t a b = LensLike Pretext (->) a b s t a b
+ Seihou.Prelude: type ALens' s a = ALens s s a a
+ Seihou.Prelude: type APrism s t a b = Market a b a Identity b -> Market a b s Identity t
+ Seihou.Prelude: type APrism' s a = APrism s s a a
+ Seihou.Prelude: type AReview t b = Optic' Tagged :: Type -> Type -> Type Identity t b
+ Seihou.Prelude: type ASetter' s a = ASetter s s a a
+ Seihou.Prelude: type ATraversal s t a b = LensLike Bazaar (->) a b s t a b
+ Seihou.Prelude: type ATraversal' s a = ATraversal s s a a
+ Seihou.Prelude: type ATraversal1 s t a b = LensLike Bazaar1 (->) a b s t a b
+ Seihou.Prelude: type ATraversal1' s a = ATraversal1 s s a a
+ Seihou.Prelude: type Accessing (p :: Type -> Type -> Type) m s a = p a Const m a -> s -> Const m s
+ Seihou.Prelude: type AnEquality (s :: k) (t :: k1) (a :: k) (b :: k2) = Identical a Proxy b a Proxy b -> Identical a Proxy b s Proxy t
+ Seihou.Prelude: type AnEquality' (s :: k) (a :: k) = AnEquality s s a a
+ Seihou.Prelude: type AnIndexedLens i s t a b = Optical Indexed i (->) Pretext Indexed i a b s t a b
+ Seihou.Prelude: type AnIndexedLens' i s a = AnIndexedLens i s s a a
+ Seihou.Prelude: type AnIndexedSetter i s t a b = Indexed i a Identity b -> s -> Identity t
+ Seihou.Prelude: type AnIndexedSetter' i s a = AnIndexedSetter i s s a a
+ Seihou.Prelude: type AnIndexedTraversal i s t a b = Over Indexed i Bazaar Indexed i a b s t a b
+ Seihou.Prelude: type AnIndexedTraversal' i s a = AnIndexedTraversal i s s a a
+ Seihou.Prelude: type AnIndexedTraversal1 i s t a b = Over Indexed i Bazaar1 Indexed i a b s t a b
+ Seihou.Prelude: type AnIndexedTraversal1' i s a = AnIndexedTraversal1 i s s a a
+ Seihou.Prelude: type AnIso s t a b = Exchange a b a Identity b -> Exchange a b s Identity t
+ Seihou.Prelude: type AnIso' s a = AnIso s s a a
+ Seihou.Prelude: type As (a :: k2) = Equality' a a
+ Seihou.Prelude: type Bazaar' (p :: Type -> Type -> Type) a = Bazaar p a a
+ Seihou.Prelude: type Bazaar1' (p :: Type -> Type -> Type) a = Bazaar1 p a a
+ Seihou.Prelude: type ClassyNamer = Name -> Maybe (Name, Name)
+ Seihou.Prelude: type Context' a = Context a a
+ Seihou.Prelude: type Equality' (s :: k2) (a :: k2) = Equality s s a a
+ Seihou.Prelude: type FieldNamer = Name -> [Name] -> Name -> [DefName]
+ Seihou.Prelude: type IndexPreservingLens' s a = IndexPreservingLens s s a a
+ Seihou.Prelude: type IndexPreservingSetter' s a = IndexPreservingSetter s s a a
+ Seihou.Prelude: type IndexPreservingTraversal' s a = IndexPreservingTraversal s s a a
+ Seihou.Prelude: type IndexPreservingTraversal1' s a = IndexPreservingTraversal1 s s a a
+ Seihou.Prelude: type IndexedGetting i m s a = Indexed i a Const m a -> s -> Const m s
+ Seihou.Prelude: type IndexedLens' i s a = IndexedLens i s s a a
+ Seihou.Prelude: type IndexedLensLike' i (f :: Type -> Type) s a = IndexedLensLike i f s s a a
+ Seihou.Prelude: type IndexedSetter' i s a = IndexedSetter i s s a a
+ Seihou.Prelude: type IndexedTraversal' i s a = IndexedTraversal i s s a a
+ Seihou.Prelude: type IndexedTraversal1' i s a = IndexedTraversal1 i s s a a
+ Seihou.Prelude: type Iso' s a = Iso s s a a
+ Seihou.Prelude: type LensLike (f :: k -> Type) s (t :: k) a (b :: k) = a -> f b -> s -> f t
+ Seihou.Prelude: type LensLike' (f :: Type -> Type) s a = LensLike f s s a a
+ Seihou.Prelude: type Optic (p :: k -> k1 -> Type) (f :: k2 -> k1) (s :: k) (t :: k2) (a :: k) (b :: k2) = p a f b -> p s f t
+ Seihou.Prelude: type Review t b = forall (p :: Type -> Type -> Type) (f :: Type -> Type). (Choice p, Bifunctor p, Settable f) => Optic' p f t b
+ Seihou.Prelude: type Optical (p :: k -> k1 -> Type) (q :: k2 -> k1 -> Type) (f :: k3 -> k1) (s :: k2) (t :: k3) (a :: k) (b :: k3) = p a f b -> q s f t
+ Seihou.Prelude: type Optical' (p :: k -> k1 -> Type) (q :: k -> k1 -> Type) (f :: k -> k1) (s :: k) (a :: k) = Optical p q f s s a a
+ Seihou.Prelude: type Over (p :: k -> Type -> Type) (f :: k1 -> Type) s (t :: k1) (a :: k) (b :: k1) = p a f b -> s -> f t
+ Seihou.Prelude: type Over' (p :: Type -> Type -> Type) (f :: Type -> Type) s a = Over p f s s a a
+ Seihou.Prelude: type Prism' s a = Prism s s a a
+ Seihou.Prelude: type ReifiedIndexedLens' i s a = ReifiedIndexedLens i s s a a
+ Seihou.Prelude: type ReifiedIndexedSetter' i s a = ReifiedIndexedSetter i s s a a
+ Seihou.Prelude: type ReifiedIndexedTraversal' i s a = ReifiedIndexedTraversal i s s a a
+ Seihou.Prelude: type ReifiedIso' s a = ReifiedIso s s a a
+ Seihou.Prelude: type ReifiedLens' s a = ReifiedLens s s a a
+ Seihou.Prelude: type ReifiedPrism' s a = ReifiedPrism s s a a
+ Seihou.Prelude: type ReifiedSetter' s a = ReifiedSetter s s a a
+ Seihou.Prelude: type ReifiedTraversal' s a = ReifiedTraversal s s a a
+ Seihou.Prelude: type Setter' s a = Setter s s a a
+ Seihou.Prelude: type Setting (p :: Type -> Type -> Type) s t a b = p a Identity b -> s -> Identity t
+ Seihou.Prelude: type Setting' (p :: Type -> Type -> Type) s a = Setting p s s a a
+ Seihou.Prelude: type Simple (f :: k1 -> k1 -> k2 -> k2 -> k) (s :: k1) (a :: k2) = f s s a a
+ Seihou.Prelude: type Traversal' s a = Traversal s s a a
+ Seihou.Prelude: type Traversal1' s a = Traversal1 s s a a
+ Seihou.Prelude: type Traversing (p :: Type -> Type -> Type) (f :: Type -> Type) s t a b = Over p BazaarT p f a b s t a b
+ Seihou.Prelude: type Traversing' (p :: Type -> Type -> Type) (f :: Type -> Type) s a = Traversing p f s s a a
+ Seihou.Prelude: type Traversing1 (p :: Type -> Type -> Type) (f :: Type -> Type) s t a b = Over p BazaarT1 p f a b s t a b
+ Seihou.Prelude: type Traversing1' (p :: Type -> Type -> Type) (f :: Type -> Type) s a = Traversing1 p f s s a a
+ Seihou.Prelude: type Unwrapped s = GUnwrapped Rep s;
+ Seihou.Prelude: type Traversal1 s t a b = forall (f :: Type -> Type). Apply f => a -> f b -> s -> f t
+ Seihou.Prelude: type Prism s t a b = forall (p :: Type -> Type -> Type) (f :: Type -> Type). (Choice p, Applicative f) => p a f b -> p s f t
+ Seihou.Prelude: un :: (Profunctor p, Bifunctor p, Functor f) => Getting a s a -> Optic' p f a s
+ Seihou.Prelude: uncons :: Cons s s a a => s -> Maybe (a, s)
+ Seihou.Prelude: uncurried :: forall a b c d e f1 p f2. (Profunctor p, Functor f2) => p ((a, b) -> c) (f2 ((d, e) -> f1)) -> p (a -> b -> c) (f2 (d -> e -> f1))
+ Seihou.Prelude: under :: AnIso s t a b -> (t -> s) -> b -> a
+ Seihou.Prelude: underEquality :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2) p. AnEquality s t a b -> p t s -> p b a
+ Seihou.Prelude: underscoreFields :: LensRules
+ Seihou.Prelude: underscoreNamer :: FieldNamer
+ Seihou.Prelude: underscoreNoPrefixNamer :: FieldNamer
+ Seihou.Prelude: unfolded :: (b -> Maybe (a, b)) -> Fold b a
+ Seihou.Prelude: united :: forall a f. Functor f => (() -> f ()) -> a -> f a
+ Seihou.Prelude: universe :: Plated a => a -> [a]
+ Seihou.Prelude: universeOf :: Getting (Endo [a]) a a -> a -> [a]
+ Seihou.Prelude: universeOn :: Plated a => Getting (Endo [a]) s a -> s -> [a]
+ Seihou.Prelude: universeOnOf :: Getting (Endo [a]) s a -> Getting (Endo [a]) a a -> s -> [a]
+ Seihou.Prelude: unsafePartsOf :: Functor f => Traversing (->) f s t a b -> LensLike f s t [a] [b]
+ Seihou.Prelude: unsafePartsOf' :: ATraversal s t a b -> Lens s t [a] [b]
+ Seihou.Prelude: unsafeSingular :: (HasCallStack, Conjoined p, Functor f) => Traversing p f s t a b -> Over p f s t a b
+ Seihou.Prelude: unsnoc :: Snoc s s a a => s -> Maybe (s, a)
+ Seihou.Prelude: unto :: (Profunctor p, Bifunctor p, Functor f) => (b -> t) -> Optic p f s t a b
+ Seihou.Prelude: use :: MonadState s m => Getting a s a -> m a
+ Seihou.Prelude: uses :: MonadState s m => LensLike' (Const r :: Type -> Type) s a -> (a -> r) -> m r
+ Seihou.Prelude: views :: MonadReader s m => LensLike' (Const r :: Type -> Type) s a -> (a -> r) -> m r
+ Seihou.Prelude: withEquality :: forall {k1} {k2} (s :: k1) (t :: k2) (a :: k1) (b :: k2) r. AnEquality s t a b -> ((s :~: a) -> (b :~: t) -> r) -> r
+ Seihou.Prelude: withIndex :: (Indexable i p, Functor f) => p (i, s) (f (j, t)) -> Indexed i s (f t)
+ Seihou.Prelude: withIso :: AnIso s t a b -> ((s -> a) -> (b -> t) -> r) -> r
+ Seihou.Prelude: withLens :: ALens s t a b -> ((s -> a) -> (s -> b -> t) -> r) -> r
+ Seihou.Prelude: withPrism :: APrism s t a b -> ((b -> t) -> (s -> Either t a) -> r) -> r
+ Seihou.Prelude: without :: APrism s t a b -> APrism u v c d -> Prism (Either s u) (Either t v) (Either a c) (Either b d)
+ Seihou.Prelude: worded :: forall (f :: Type -> Type). Applicative f => IndexedLensLike' Int f String String
+ Seihou.Prelude: xplat :: forall {k2} s g (t :: k2) a (b :: k2). Optic (Costar ((->) s)) g s t a b -> ((s -> a) -> g b) -> g t
+ Seihou.Prelude: xplatf :: forall {k} {k2} f g (s :: k) (t :: k2) (a :: k) (b :: k2). Optic (Costar f) g s t a b -> (f a -> g b) -> f s -> g t
+ Seihou.Prelude: zoom :: Zoom m n s t => LensLike' (Zoomed m c) t s -> m c -> n c
+ Seihou.Prelude: }
- Seihou.Composition.Graph: CompositionGraph :: Map ModuleInstance Module -> Map ModuleInstance [ModuleInstance] -> CompositionGraph
+ Seihou.Composition.Graph: CompositionGraph :: !Map ModuleInstance Module -> !Map ModuleInstance [ModuleInstance] -> CompositionGraph
- Seihou.Composition.Instance: ModuleInstance :: ModuleName -> ParentVars -> ModuleInstance
+ Seihou.Composition.Instance: ModuleInstance :: !ModuleName -> !ParentVars -> ModuleInstance
- Seihou.Core.Application: buildAppliedComposition :: AppliedTarget -> FilePath -> Maybe Text -> [ModuleName] -> Maybe Text -> Maybe Text -> [(ModuleInstance, Module, FilePath)] -> Map ModuleInstance (Map VarName ResolvedVar) -> UTCTime -> AppliedComposition
+ Seihou.Core.Application: buildAppliedComposition :: AppliedTarget -> ArtifactOrigin -> Maybe Text -> [ModuleName] -> Maybe Text -> Maybe Text -> [(ModuleInstance, Module, ArtifactOrigin)] -> Map ModuleInstance (Map VarName ResolvedVar) -> UTCTime -> AppliedComposition
- Seihou.Core.Migration: BlueprintMigration :: Text -> Text -> Text -> BlueprintMigration
+ Seihou.Core.Migration: BlueprintMigration :: !Text -> !Text -> !Text -> BlueprintMigration
- Seihou.Core.Migration: BlueprintMigrationPlan :: Text -> Version -> Version -> [BlueprintMigration] -> BlueprintMigrationPlan
+ Seihou.Core.Migration: BlueprintMigrationPlan :: !Text -> !Version -> !Version -> ![BlueprintMigration] -> BlueprintMigrationPlan
- Seihou.Core.Migration: DeleteDir :: FilePath -> MigrationOp
+ Seihou.Core.Migration: DeleteDir :: !FilePath -> MigrationOp
- Seihou.Core.Migration: DeleteFile :: FilePath -> MigrationOp
+ Seihou.Core.Migration: DeleteFile :: !FilePath -> MigrationOp
- Seihou.Core.Migration: Migration :: Text -> Text -> [MigrationOp] -> Migration
+ Seihou.Core.Migration: Migration :: !Text -> !Text -> ![MigrationOp] -> Migration
- Seihou.Core.Migration: MigrationPlan :: Text -> Version -> Version -> [Migration] -> MigrationPlan
+ Seihou.Core.Migration: MigrationPlan :: !Text -> !Version -> !Version -> ![Migration] -> MigrationPlan
- Seihou.Core.Migration: MoveDir :: FilePath -> FilePath -> MigrationOp
+ Seihou.Core.Migration: MoveDir :: !FilePath -> !FilePath -> MigrationOp
- Seihou.Core.Migration: MoveFile :: FilePath -> FilePath -> MigrationOp
+ Seihou.Core.Migration: MoveFile :: !FilePath -> !FilePath -> MigrationOp
- Seihou.Core.Migration: RunCommand :: Text -> Maybe FilePath -> MigrationOp
+ Seihou.Core.Migration: RunCommand :: !Text -> !Maybe FilePath -> MigrationOp
- Seihou.Core.Migration: [dest] :: MigrationOp -> FilePath
+ Seihou.Core.Migration: [dest] :: MigrationOp -> !FilePath
- Seihou.Core.Migration: [from] :: BlueprintMigration -> Text
+ Seihou.Core.Migration: [from] :: BlueprintMigrationPlan -> !Version
- Seihou.Core.Migration: [ops] :: Migration -> [MigrationOp]
+ Seihou.Core.Migration: [ops] :: Migration -> ![MigrationOp]
- Seihou.Core.Migration: [path] :: MigrationOp -> FilePath
+ Seihou.Core.Migration: [path] :: MigrationOp -> !FilePath
- Seihou.Core.Migration: [prompt] :: BlueprintMigration -> Text
+ Seihou.Core.Migration: [prompt] :: BlueprintMigration -> !Text
- Seihou.Core.Migration: [run] :: MigrationOp -> Text
+ Seihou.Core.Migration: [run] :: MigrationOp -> !Text
- Seihou.Core.Migration: [src] :: MigrationOp -> FilePath
+ Seihou.Core.Migration: [src] :: MigrationOp -> !FilePath
- Seihou.Core.Migration: [to] :: BlueprintMigration -> Text
+ Seihou.Core.Migration: [to] :: BlueprintMigrationPlan -> !Version
- Seihou.Core.Migration: [workDir] :: MigrationOp -> Maybe FilePath
+ Seihou.Core.Migration: [workDir] :: MigrationOp -> !Maybe FilePath
- Seihou.Core.Module: DiscoveredModule :: Either ModuleLoadError Module -> ModuleSource -> FilePath -> DiscoveredModule
+ Seihou.Core.Module: DiscoveredModule :: !Either ModuleLoadError Module -> !ModuleSource -> !FilePath -> DiscoveredModule
- Seihou.Core.Module: DiscoveredRunnable :: Text -> Maybe Text -> RunnableKind -> ModuleSource -> FilePath -> Bool -> Maybe Text -> DiscoveredRunnable
+ Seihou.Core.Module: DiscoveredRunnable :: !Text -> !Maybe Text -> !RunnableKind -> !ModuleSource -> !FilePath -> !Bool -> !Maybe Text -> DiscoveredRunnable
- Seihou.Core.Registry: Registry :: Text -> Maybe Text -> [RegistryEntry] -> [RegistryEntry] -> [RegistryEntry] -> [RegistryEntry] -> Registry
+ Seihou.Core.Registry: Registry :: !Text -> !Maybe Text -> ![RegistryEntry] -> ![RegistryEntry] -> ![RegistryEntry] -> ![RegistryEntry] -> Registry
- Seihou.Core.Registry: RegistryEntry :: ModuleName -> Maybe Text -> FilePath -> Maybe Text -> [Text] -> RegistryEntry
+ Seihou.Core.Registry: RegistryEntry :: !ModuleName -> !Maybe Text -> !FilePath -> !Maybe Text -> ![Text] -> RegistryEntry
- Seihou.Core.Registry: RegistryValidationReport :: [RegistryValidationIssue] -> Int -> Int -> Int -> Int -> RegistryValidationReport
+ Seihou.Core.Registry: RegistryValidationReport :: ![RegistryValidationIssue] -> !Int -> !Int -> !Int -> !Int -> RegistryValidationReport
- Seihou.Core.Registry: SyncDiff :: EntryKind -> ModuleName -> Maybe Text -> Maybe Text -> SyncStatus -> SyncDiff
+ Seihou.Core.Registry: SyncDiff :: !EntryKind -> !ModuleName -> !Maybe Text -> !Maybe Text -> !SyncStatus -> SyncDiff
- Seihou.Core.Registry: SyncReport :: [SyncDiff] -> Registry -> SyncReport
+ Seihou.Core.Registry: SyncReport :: ![SyncDiff] -> !Registry -> SyncReport
- Seihou.Core.Registry: [blueprints] :: Registry -> [RegistryEntry]
+ Seihou.Core.Registry: [blueprints] :: Registry -> ![RegistryEntry]
- Seihou.Core.Registry: [description] :: RegistryEntry -> Maybe Text
+ Seihou.Core.Registry: [description] :: RegistryEntry -> !Maybe Text
- Seihou.Core.Registry: [modules] :: Registry -> [RegistryEntry]
+ Seihou.Core.Registry: [modules] :: Registry -> ![RegistryEntry]
- Seihou.Core.Registry: [name] :: RegistryEntry -> ModuleName
+ Seihou.Core.Registry: [name] :: SyncDiff -> !ModuleName
- Seihou.Core.Registry: [path] :: RegistryEntry -> FilePath
+ Seihou.Core.Registry: [path] :: RegistryEntry -> !FilePath
- Seihou.Core.Registry: [prompts] :: Registry -> [RegistryEntry]
+ Seihou.Core.Registry: [prompts] :: Registry -> ![RegistryEntry]
- Seihou.Core.Registry: [recipes] :: Registry -> [RegistryEntry]
+ Seihou.Core.Registry: [recipes] :: Registry -> ![RegistryEntry]
- Seihou.Core.Registry: [repoDescription] :: Registry -> Maybe Text
+ Seihou.Core.Registry: [repoDescription] :: Registry -> !Maybe Text
- Seihou.Core.Registry: [repoName] :: Registry -> Text
+ Seihou.Core.Registry: [repoName] :: Registry -> !Text
- Seihou.Core.Registry: [tags] :: RegistryEntry -> [Text]
+ Seihou.Core.Registry: [tags] :: RegistryEntry -> ![Text]
- Seihou.Core.Registry: [version] :: RegistryEntry -> Maybe Text
+ Seihou.Core.Registry: [version] :: RegistryEntry -> !Maybe Text
- Seihou.Core.Types: AgentPrompt :: ModuleName -> Maybe Text -> Maybe Text -> Text -> [VarDecl] -> [Prompt] -> [CommandVar] -> [PromptGuidance] -> [BlueprintFile] -> Maybe [Text] -> [Text] -> Maybe AgentPromptLaunch -> AgentPrompt
+ Seihou.Core.Types: AgentPrompt :: !ModuleName -> !Maybe Text -> !Maybe Text -> !Text -> ![VarDecl] -> ![Prompt] -> ![CommandVar] -> ![PromptGuidance] -> ![BlueprintFile] -> !Maybe [Text] -> ![Text] -> !Maybe AgentLaunch -> AgentPrompt
- Seihou.Core.Types: AppliedBlueprint :: ModuleName -> Maybe Text -> UTCTime -> [ModuleName] -> Bool -> Maybe Text -> Maybe Text -> AppliedBlueprint
+ Seihou.Core.Types: AppliedBlueprint :: !ModuleName -> !Maybe Text -> !UTCTime -> ![ModuleName] -> !Bool -> !Maybe Text -> !Maybe Text -> AppliedBlueprint
- Seihou.Core.Types: AppliedBlueprintMigration :: ModuleName -> Maybe Text -> Text -> Text -> UTCTime -> Maybe Text -> AppliedBlueprintMigration
+ Seihou.Core.Types: AppliedBlueprintMigration :: !ModuleName -> !Maybe Text -> !Text -> !Text -> !UTCTime -> !Maybe Text -> AppliedBlueprintMigration
- Seihou.Core.Types: AppliedComposition :: ApplicationId -> AppliedTarget -> FilePath -> Maybe Text -> [ModuleName] -> Maybe Text -> Maybe Text -> [AppliedInstanceState] -> Map CommandFingerprint CommandReceipt -> UTCTime -> AppliedComposition
+ Seihou.Core.Types: AppliedComposition :: !ApplicationId -> !AppliedTarget -> !ArtifactOrigin -> !Maybe Text -> ![ModuleName] -> !Maybe Text -> !Maybe Text -> ![AppliedInstanceState] -> !Map CommandFingerprint CommandReceipt -> !UTCTime -> AppliedComposition
- Seihou.Core.Types: AppliedInstanceState :: ModuleName -> ParentVars -> FilePath -> Maybe Text -> Map VarName Text -> AppliedInstanceState
+ Seihou.Core.Types: AppliedInstanceState :: !ModuleName -> !ParentVars -> !ArtifactOrigin -> !Maybe Text -> !Map VarName Text -> AppliedInstanceState
- Seihou.Core.Types: AppliedModule :: ModuleName -> ParentVars -> FilePath -> Maybe Text -> UTCTime -> Maybe Removal -> AppliedModule
+ Seihou.Core.Types: AppliedModule :: !ModuleName -> !ParentVars -> !ArtifactOrigin -> !Maybe Text -> !UTCTime -> !Maybe Removal -> AppliedModule
- Seihou.Core.Types: AppliedRecipe :: RecipeName -> Maybe Text -> UTCTime -> AppliedRecipe
+ Seihou.Core.Types: AppliedRecipe :: !RecipeName -> !Maybe Text -> !UTCTime -> AppliedRecipe
- Seihou.Core.Types: Blueprint :: ModuleName -> Maybe Text -> Maybe Text -> Text -> [VarDecl] -> [Prompt] -> [Dependency] -> [BlueprintFile] -> Maybe [Text] -> [Text] -> [BlueprintMigration] -> Blueprint
+ Seihou.Core.Types: Blueprint :: !ModuleName -> !Maybe Text -> !Maybe Text -> !Text -> ![VarDecl] -> ![Prompt] -> ![Dependency] -> ![BlueprintFile] -> !Maybe [Text] -> ![Text] -> ![BlueprintMigration] -> !Maybe AgentLaunch -> Blueprint
- Seihou.Core.Types: BlueprintFile :: FilePath -> Maybe Text -> BlueprintFile
+ Seihou.Core.Types: BlueprintFile :: !FilePath -> !Maybe Text -> BlueprintFile
- Seihou.Core.Types: Command :: Text -> Maybe Text -> Maybe Expr -> Command
+ Seihou.Core.Types: Command :: !Text -> !Maybe Text -> !Maybe Expr -> Command
- Seihou.Core.Types: CommandReceipt :: CommandFingerprint -> ModuleName -> Text -> Maybe FilePath -> UTCTime -> CommandReceipt
+ Seihou.Core.Types: CommandReceipt :: !CommandFingerprint -> !ModuleName -> !Text -> !Maybe FilePath -> !UTCTime -> CommandReceipt
- Seihou.Core.Types: CommandVar :: VarName -> Text -> Maybe Text -> Maybe Expr -> Bool -> Maybe Natural -> CommandVar
+ Seihou.Core.Types: CommandVar :: !VarName -> !Text -> !Maybe Text -> !Maybe Expr -> !Bool -> !Maybe Natural -> CommandVar
- Seihou.Core.Types: ConflictFile :: FilePath -> ModuleName -> SHA256 -> SHA256 -> Text -> ConflictFile
+ Seihou.Core.Types: ConflictFile :: !FilePath -> !ModuleName -> !SHA256 -> !SHA256 -> !Text -> ConflictFile
- Seihou.Core.Types: CopyFileOp :: FilePath -> FilePath -> Operation
+ Seihou.Core.Types: CopyFileOp :: !FilePath -> !FilePath -> Operation
- Seihou.Core.Types: CreateDirOp :: FilePath -> Operation
+ Seihou.Core.Types: CreateDirOp :: !FilePath -> Operation
- Seihou.Core.Types: Dependency :: ModuleName -> Map VarName Text -> Dependency
+ Seihou.Core.Types: Dependency :: !ModuleName -> !Map VarName Text -> Dependency
- Seihou.Core.Types: DiffResult :: [PlannedFile] -> [ModifiedFile] -> [FilePath] -> [ConflictFile] -> [OrphanedFile] -> DiffResult
+ Seihou.Core.Types: DiffResult :: ![PlannedFile] -> ![ModifiedFile] -> ![FilePath] -> ![ConflictFile] -> ![OrphanedFile] -> DiffResult
- Seihou.Core.Types: FileRecord :: SHA256 -> ModuleName -> Strategy -> UTCTime -> Maybe BaselineRef -> Set ApplicationId -> FileRecord
+ Seihou.Core.Types: FileRecord :: !SHA256 -> !ModuleName -> !Strategy -> !UTCTime -> !Maybe BaselineRef -> !Set ApplicationId -> FileRecord
- Seihou.Core.Types: Manifest :: Int -> UTCTime -> [AppliedModule] -> Map VarName Text -> Map FilePath FileRecord -> [AppliedComposition] -> Maybe AppliedRecipe -> Maybe AppliedBlueprint -> [AppliedBlueprintMigration] -> Manifest
+ Seihou.Core.Types: Manifest :: !Int -> !UTCTime -> ![AppliedModule] -> !Map VarName Text -> !Map FilePath FileRecord -> ![AppliedComposition] -> !Maybe AppliedRecipe -> !Maybe AppliedBlueprint -> ![AppliedBlueprintMigration] -> Manifest
- Seihou.Core.Types: ModifiedFile :: FilePath -> ModuleName -> SHA256 -> Text -> ModifiedFile
+ Seihou.Core.Types: ModifiedFile :: !FilePath -> !ModuleName -> !SHA256 -> !Text -> ModifiedFile
- Seihou.Core.Types: Module :: ModuleName -> Maybe Text -> Maybe Text -> [VarDecl] -> [VarExport] -> [Prompt] -> [Step] -> [Command] -> [Dependency] -> Maybe Removal -> [Migration] -> Module
+ Seihou.Core.Types: Module :: !ModuleName -> !Maybe Text -> !Maybe Text -> ![VarDecl] -> ![VarExport] -> ![Prompt] -> ![Step] -> ![Command] -> ![Dependency] -> !Maybe Removal -> ![Migration] -> Module
- Seihou.Core.Types: OrphanedFile :: FilePath -> ModuleName -> OrphanedFile
+ Seihou.Core.Types: OrphanedFile :: !FilePath -> !ModuleName -> OrphanedFile
- Seihou.Core.Types: PatchFileOp :: FilePath -> Text -> PatchOp -> Strategy -> ModuleName -> Operation
+ Seihou.Core.Types: PatchFileOp :: !FilePath -> !Text -> !PatchOp -> !Strategy -> !ModuleName -> Operation
- Seihou.Core.Types: PlannedFile :: FilePath -> ModuleName -> Text -> PlannedFile
+ Seihou.Core.Types: PlannedFile :: !FilePath -> !ModuleName -> !Text -> PlannedFile
- Seihou.Core.Types: Prompt :: VarName -> Text -> Maybe Expr -> Maybe [Text] -> Prompt
+ Seihou.Core.Types: Prompt :: !VarName -> !Text -> !Maybe Expr -> !Maybe [Text] -> Prompt
- Seihou.Core.Types: PromptGuidance :: Text -> Text -> Maybe Expr -> PromptGuidance
+ Seihou.Core.Types: PromptGuidance :: !Text -> !Text -> !Maybe Expr -> PromptGuidance
- Seihou.Core.Types: Recipe :: RecipeName -> Maybe Text -> Maybe Text -> [Dependency] -> [VarDecl] -> [Prompt] -> Recipe
+ Seihou.Core.Types: Recipe :: !RecipeName -> !Maybe Text -> !Maybe Text -> ![Dependency] -> ![VarDecl] -> ![Prompt] -> Recipe
- Seihou.Core.Types: Removal :: [RemovalStep] -> [Command] -> Removal
+ Seihou.Core.Types: Removal :: ![RemovalStep] -> ![Command] -> Removal
- Seihou.Core.Types: RemovalStep :: RemovalAction -> Text -> Maybe FilePath -> RemovalStep
+ Seihou.Core.Types: RemovalStep :: !RemovalAction -> !Text -> !Maybe FilePath -> RemovalStep
- Seihou.Core.Types: ResolvedVar :: VarValue -> VarSource -> VarDecl -> ResolvedVar
+ Seihou.Core.Types: ResolvedVar :: !VarValue -> !VarSource -> !VarDecl -> ResolvedVar
- Seihou.Core.Types: RunCommandOp :: Text -> Maybe FilePath -> ModuleName -> Int -> Operation
+ Seihou.Core.Types: RunCommandOp :: !Text -> !Maybe FilePath -> !ModuleName -> !Int -> Operation
- Seihou.Core.Types: Step :: Strategy -> FilePath -> Text -> Maybe Expr -> Maybe PatchOp -> Step
+ Seihou.Core.Types: Step :: !Strategy -> !FilePath -> !Text -> !Maybe Expr -> !Maybe PatchOp -> Step
- Seihou.Core.Types: TrackedFile :: FilePath -> ModuleName -> TrackedFileStatus -> TrackedFile
+ Seihou.Core.Types: TrackedFile :: !FilePath -> !ModuleName -> !TrackedFileStatus -> TrackedFile
- Seihou.Core.Types: VarDecl :: VarName -> VarType -> Maybe VarValue -> Maybe Text -> Bool -> Maybe Validation -> VarDecl
+ Seihou.Core.Types: VarDecl :: !VarName -> !VarType -> !Maybe VarValue -> !Maybe Text -> !Bool -> !Maybe Validation -> VarDecl
- Seihou.Core.Types: VarExport :: VarName -> Maybe VarName -> VarExport
+ Seihou.Core.Types: VarExport :: !VarName -> !Maybe VarName -> VarExport
- Seihou.Core.Types: WriteFileOp :: FilePath -> Text -> Strategy -> Operation
+ Seihou.Core.Types: WriteFileOp :: !FilePath -> !Text -> !Strategy -> Operation
- Seihou.Core.Types: [action] :: RemovalStep -> RemovalAction
+ Seihou.Core.Types: [action] :: RemovalStep -> !RemovalAction
- Seihou.Core.Types: [additionalModules] :: AppliedComposition -> [ModuleName]
+ Seihou.Core.Types: [additionalModules] :: AppliedComposition -> ![ModuleName]
- Seihou.Core.Types: [agentSessionId] :: AppliedBlueprintMigration -> Maybe Text
+ Seihou.Core.Types: [agentSessionId] :: AppliedBlueprintMigration -> !Maybe Text
- Seihou.Core.Types: [alias] :: VarExport -> Maybe VarName
+ Seihou.Core.Types: [alias] :: VarExport -> !Maybe VarName
- Seihou.Core.Types: [allowedTools] :: AgentPrompt -> Maybe [Text]
+ Seihou.Core.Types: [allowedTools] :: AgentPrompt -> !Maybe [Text]
- Seihou.Core.Types: [applicationId] :: AppliedComposition -> ApplicationId
+ Seihou.Core.Types: [applicationId] :: AppliedComposition -> !ApplicationId
- Seihou.Core.Types: [applicationIds] :: FileRecord -> Set ApplicationId
+ Seihou.Core.Types: [applicationIds] :: FileRecord -> !Set ApplicationId
- Seihou.Core.Types: [applications] :: Manifest -> [AppliedComposition]
+ Seihou.Core.Types: [applications] :: Manifest -> ![AppliedComposition]
- Seihou.Core.Types: [appliedAt] :: AppliedBlueprintMigration -> UTCTime
+ Seihou.Core.Types: [appliedAt] :: AppliedBlueprintMigration -> !UTCTime
- Seihou.Core.Types: [baseModules] :: Blueprint -> [Dependency]
+ Seihou.Core.Types: [baseModules] :: Blueprint -> ![Dependency]
- Seihou.Core.Types: [baselineModules] :: AppliedBlueprint -> [ModuleName]
+ Seihou.Core.Types: [baselineModules] :: AppliedBlueprint -> ![ModuleName]
- Seihou.Core.Types: [baseline] :: FileRecord -> Maybe BaselineRef
+ Seihou.Core.Types: [baseline] :: FileRecord -> !Maybe BaselineRef
- Seihou.Core.Types: [blueprintMigrations] :: Manifest -> [AppliedBlueprintMigration]
+ Seihou.Core.Types: [blueprintMigrations] :: Manifest -> ![AppliedBlueprintMigration]
- Seihou.Core.Types: [blueprintVersion] :: AppliedBlueprintMigration -> Maybe Text
+ Seihou.Core.Types: [blueprintVersion] :: AppliedBlueprintMigration -> !Maybe Text
- Seihou.Core.Types: [blueprint] :: Manifest -> Maybe AppliedBlueprint
+ Seihou.Core.Types: [blueprint] :: Manifest -> !Maybe AppliedBlueprint
- Seihou.Core.Types: [body] :: PromptGuidance -> Text
+ Seihou.Core.Types: [body] :: PromptGuidance -> !Text
- Seihou.Core.Types: [choices] :: Prompt -> Maybe [Text]
+ Seihou.Core.Types: [choices] :: Prompt -> !Maybe [Text]
- Seihou.Core.Types: [commandReceipts] :: AppliedComposition -> Map CommandFingerprint CommandReceipt
+ Seihou.Core.Types: [commandReceipts] :: AppliedComposition -> !Map CommandFingerprint CommandReceipt
- Seihou.Core.Types: [commandVars] :: AgentPrompt -> [CommandVar]
+ Seihou.Core.Types: [commandVars] :: AgentPrompt -> ![CommandVar]
- Seihou.Core.Types: [command] :: CommandReceipt -> Text
+ Seihou.Core.Types: [command] :: CommandReceipt -> !Text
- Seihou.Core.Types: [commands] :: Module -> [Command]
+ Seihou.Core.Types: [commands] :: Module -> ![Command]
- Seihou.Core.Types: [completedAt] :: CommandReceipt -> UTCTime
+ Seihou.Core.Types: [completedAt] :: CommandReceipt -> !UTCTime
- Seihou.Core.Types: [condition] :: PromptGuidance -> Maybe Expr
+ Seihou.Core.Types: [condition] :: PromptGuidance -> !Maybe Expr
- Seihou.Core.Types: [conflicts] :: DiffResult -> [ConflictFile]
+ Seihou.Core.Types: [conflicts] :: DiffResult -> ![ConflictFile]
- Seihou.Core.Types: [content] :: PlannedFile -> Text
+ Seihou.Core.Types: [content] :: PlannedFile -> !Text
- Seihou.Core.Types: [context] :: AppliedComposition -> Maybe Text
+ Seihou.Core.Types: [context] :: AppliedComposition -> !Maybe Text
- Seihou.Core.Types: [decl] :: ResolvedVar -> VarDecl
+ Seihou.Core.Types: [decl] :: ResolvedVar -> !VarDecl
- Seihou.Core.Types: [default_] :: VarDecl -> Maybe VarValue
+ Seihou.Core.Types: [default_] :: VarDecl -> !Maybe VarValue
- Seihou.Core.Types: [dependencies] :: Module -> [Dependency]
+ Seihou.Core.Types: [dependencies] :: Module -> ![Dependency]
- Seihou.Core.Types: [description] :: AgentPrompt -> Maybe Text
+ Seihou.Core.Types: [description] :: AgentPrompt -> !Maybe Text
- Seihou.Core.Types: [dest] :: Operation -> FilePath
+ Seihou.Core.Types: [dest] :: Operation -> !FilePath
- Seihou.Core.Types: [diskHash] :: ConflictFile -> SHA256
+ Seihou.Core.Types: [diskHash] :: ConflictFile -> !SHA256
- Seihou.Core.Types: [exports] :: Module -> [VarExport]
+ Seihou.Core.Types: [exports] :: Module -> ![VarExport]
- Seihou.Core.Types: [files] :: Manifest -> Map FilePath FileRecord
+ Seihou.Core.Types: [files] :: Manifest -> !Map FilePath FileRecord
- Seihou.Core.Types: [fingerprint] :: CommandReceipt -> CommandFingerprint
+ Seihou.Core.Types: [fingerprint] :: CommandReceipt -> !CommandFingerprint
- Seihou.Core.Types: [fromVersion] :: AppliedBlueprintMigration -> Text
+ Seihou.Core.Types: [fromVersion] :: AppliedBlueprintMigration -> !Text
- Seihou.Core.Types: [genAt] :: Manifest -> UTCTime
+ Seihou.Core.Types: [genAt] :: Manifest -> !UTCTime
- Seihou.Core.Types: [generatedAt] :: FileRecord -> UTCTime
+ Seihou.Core.Types: [generatedAt] :: FileRecord -> !UTCTime
- Seihou.Core.Types: [guidance] :: AgentPrompt -> [PromptGuidance]
+ Seihou.Core.Types: [guidance] :: AgentPrompt -> ![PromptGuidance]
- Seihou.Core.Types: [hash] :: FileRecord -> SHA256
+ Seihou.Core.Types: [hash] :: FileRecord -> !SHA256
- Seihou.Core.Types: [instances] :: AppliedComposition -> [AppliedInstanceState]
+ Seihou.Core.Types: [instances] :: AppliedComposition -> ![AppliedInstanceState]
- Seihou.Core.Types: [launch] :: AgentPrompt -> Maybe AgentPromptLaunch
+ Seihou.Core.Types: [launch] :: AgentPrompt -> !Maybe AgentLaunch
- Seihou.Core.Types: [manifestHash] :: ConflictFile -> SHA256
+ Seihou.Core.Types: [manifestHash] :: ConflictFile -> !SHA256
- Seihou.Core.Types: [maxBytes] :: CommandVar -> Maybe Natural
+ Seihou.Core.Types: [maxBytes] :: CommandVar -> !Maybe Natural
- Seihou.Core.Types: [migrations] :: Blueprint -> [BlueprintMigration]
+ Seihou.Core.Types: [migrations] :: Blueprint -> ![BlueprintMigration]
- Seihou.Core.Types: [mode] :: AgentPromptLaunch -> Maybe Text
+ Seihou.Core.Types: [mode] :: AgentLaunch -> !Maybe Text
- Seihou.Core.Types: [model] :: AgentPromptLaunch -> Maybe Text
+ Seihou.Core.Types: [model] :: AgentLaunch -> !Maybe Text
- Seihou.Core.Types: [modified] :: DiffResult -> [ModifiedFile]
+ Seihou.Core.Types: [modified] :: DiffResult -> ![ModifiedFile]
- Seihou.Core.Types: [moduleName] :: TrackedFile -> ModuleName
+ Seihou.Core.Types: [moduleName] :: TrackedFile -> !ModuleName
- Seihou.Core.Types: [moduleVersion] :: AppliedModule -> Maybe Text
+ Seihou.Core.Types: [moduleVersion] :: AppliedModule -> !Maybe Text
- Seihou.Core.Types: [modules] :: Manifest -> [AppliedModule]
+ Seihou.Core.Types: [modules] :: Manifest -> ![AppliedModule]
- Seihou.Core.Types: [name] :: AppliedBlueprintMigration -> ModuleName
+ Seihou.Core.Types: [name] :: AppliedBlueprintMigration -> !ModuleName
- Seihou.Core.Types: [namespace] :: AppliedComposition -> Maybe Text
+ Seihou.Core.Types: [namespace] :: AppliedComposition -> !Maybe Text
- Seihou.Core.Types: [newContent] :: ModifiedFile -> Text
+ Seihou.Core.Types: [newContent] :: ModifiedFile -> !Text
- Seihou.Core.Types: [new] :: DiffResult -> [PlannedFile]
+ Seihou.Core.Types: [new] :: DiffResult -> ![PlannedFile]
- Seihou.Core.Types: [noBaseline] :: AppliedBlueprint -> Bool
+ Seihou.Core.Types: [noBaseline] :: AppliedBlueprint -> !Bool
- Seihou.Core.Types: [occurrence] :: Operation -> Int
+ Seihou.Core.Types: [occurrence] :: Operation -> !Int
- Seihou.Core.Types: [oldHash] :: ModifiedFile -> SHA256
+ Seihou.Core.Types: [oldHash] :: ModifiedFile -> !SHA256
- Seihou.Core.Types: [op] :: Operation -> PatchOp
+ Seihou.Core.Types: [op] :: Operation -> !PatchOp
- Seihou.Core.Types: [orphaned] :: DiffResult -> [OrphanedFile]
+ Seihou.Core.Types: [orphaned] :: DiffResult -> ![OrphanedFile]
- Seihou.Core.Types: [parentVars] :: AppliedModule -> ParentVars
+ Seihou.Core.Types: [parentVars] :: AppliedModule -> !ParentVars
- Seihou.Core.Types: [patch] :: Step -> Maybe PatchOp
+ Seihou.Core.Types: [patch] :: Step -> !Maybe PatchOp
- Seihou.Core.Types: [path] :: TrackedFile -> FilePath
+ Seihou.Core.Types: [path] :: TrackedFile -> !FilePath
- Seihou.Core.Types: [planContent] :: ConflictFile -> Text
+ Seihou.Core.Types: [planContent] :: ConflictFile -> !Text
- Seihou.Core.Types: [prompt] :: AgentPrompt -> Text
+ Seihou.Core.Types: [prompt] :: AgentPrompt -> !Text
- Seihou.Core.Types: [prompts] :: AgentPrompt -> [Prompt]
+ Seihou.Core.Types: [prompts] :: AgentPrompt -> ![Prompt]
- Seihou.Core.Types: [provider] :: AgentPromptLaunch -> Maybe Text
+ Seihou.Core.Types: [provider] :: AgentLaunch -> !Maybe Text
- Seihou.Core.Types: [recipeVersion] :: AppliedRecipe -> Maybe Text
+ Seihou.Core.Types: [recipeVersion] :: AppliedRecipe -> !Maybe Text
- Seihou.Core.Types: [recipe] :: Manifest -> Maybe AppliedRecipe
+ Seihou.Core.Types: [recipe] :: Manifest -> !Maybe AppliedRecipe
- Seihou.Core.Types: [removal] :: AppliedModule -> Maybe Removal
+ Seihou.Core.Types: [removal] :: AppliedModule -> !Maybe Removal
- Seihou.Core.Types: [required] :: VarDecl -> Bool
+ Seihou.Core.Types: [required] :: VarDecl -> !Bool
- Seihou.Core.Types: [resolvedVars] :: AppliedInstanceState -> Map VarName Text
+ Seihou.Core.Types: [resolvedVars] :: AppliedInstanceState -> !Map VarName Text
- Seihou.Core.Types: [run] :: CommandVar -> Text
+ Seihou.Core.Types: [run] :: CommandVar -> !Text
- Seihou.Core.Types: [source] :: ResolvedVar -> VarSource
+ Seihou.Core.Types: [source] :: ResolvedVar -> !VarSource
- Seihou.Core.Types: [src] :: Operation -> FilePath
+ Seihou.Core.Types: [src] :: Operation -> !FilePath
- Seihou.Core.Types: [status] :: TrackedFile -> TrackedFileStatus
+ Seihou.Core.Types: [status] :: TrackedFile -> !TrackedFileStatus
- Seihou.Core.Types: [steps] :: Module -> [Step]
+ Seihou.Core.Types: [steps] :: Module -> ![Step]
- Seihou.Core.Types: [strategy] :: FileRecord -> Strategy
+ Seihou.Core.Types: [strategy] :: FileRecord -> !Strategy
- Seihou.Core.Types: [tags] :: AgentPrompt -> [Text]
+ Seihou.Core.Types: [tags] :: AgentPrompt -> ![Text]
- Seihou.Core.Types: [targetVersion] :: AppliedComposition -> Maybe Text
+ Seihou.Core.Types: [targetVersion] :: AppliedComposition -> !Maybe Text
- Seihou.Core.Types: [target] :: AppliedComposition -> AppliedTarget
+ Seihou.Core.Types: [target] :: AppliedComposition -> !AppliedTarget
- Seihou.Core.Types: [text] :: Prompt -> Text
+ Seihou.Core.Types: [text] :: Prompt -> !Text
- Seihou.Core.Types: [title] :: PromptGuidance -> Text
+ Seihou.Core.Types: [title] :: PromptGuidance -> !Text
- Seihou.Core.Types: [toVersion] :: AppliedBlueprintMigration -> Text
+ Seihou.Core.Types: [toVersion] :: AppliedBlueprintMigration -> !Text
- Seihou.Core.Types: [trim] :: CommandVar -> Bool
+ Seihou.Core.Types: [trim] :: CommandVar -> !Bool
- Seihou.Core.Types: [type_] :: VarDecl -> VarType
+ Seihou.Core.Types: [type_] :: VarDecl -> !VarType
- Seihou.Core.Types: [unchanged] :: DiffResult -> [FilePath]
+ Seihou.Core.Types: [unchanged] :: DiffResult -> ![FilePath]
- Seihou.Core.Types: [userPrompt] :: AppliedBlueprint -> Maybe Text
+ Seihou.Core.Types: [userPrompt] :: AppliedBlueprint -> !Maybe Text
- Seihou.Core.Types: [validation] :: VarDecl -> Maybe Validation
+ Seihou.Core.Types: [validation] :: VarDecl -> !Maybe Validation
- Seihou.Core.Types: [value] :: ResolvedVar -> VarValue
+ Seihou.Core.Types: [value] :: ResolvedVar -> !VarValue
- Seihou.Core.Types: [var] :: Prompt -> VarName
+ Seihou.Core.Types: [var] :: Prompt -> !VarName
- Seihou.Core.Types: [vars] :: Manifest -> Map VarName Text
+ Seihou.Core.Types: [vars] :: Manifest -> !Map VarName Text
- Seihou.Core.Types: [version] :: Manifest -> Int
+ Seihou.Core.Types: [version] :: Manifest -> !Int
- Seihou.Core.Types: [workDir] :: CommandReceipt -> Maybe FilePath
+ Seihou.Core.Types: [workDir] :: CommandReceipt -> !Maybe FilePath
- Seihou.Effect.ConfigWriterPure: ConfigWriterState :: Map Text Text -> Map Text (Map Text Text) -> Map Text Text -> ConfigWriterState
+ Seihou.Effect.ConfigWriterPure: ConfigWriterState :: !Map Text Text -> !Map Text (Map Text Text) -> !Map Text Text -> ConfigWriterState
- Seihou.Effect.ConsolePure: ConsoleState :: [Text] -> [Text] -> [Text] -> ConsoleState
+ Seihou.Effect.ConsolePure: ConsoleState :: ![Text] -> ![Text] -> ![Text] -> ConsoleState
- Seihou.Effect.FilesystemPure: PureFS :: Map FilePath Text -> Set FilePath -> PureFS
+ Seihou.Effect.FilesystemPure: PureFS :: !Map FilePath Text -> !Set FilePath -> PureFS
- Seihou.Effect.FilesystemPure: [dirs] :: PureFS -> Set FilePath
+ Seihou.Effect.FilesystemPure: [dirs] :: PureFS -> !Set FilePath
- Seihou.Effect.FilesystemPure: [files] :: PureFS -> Map FilePath Text
+ Seihou.Effect.FilesystemPure: [files] :: PureFS -> !Map FilePath Text
- Seihou.Effect.LoggerPure: LoggerState :: [Text] -> [Text] -> [Text] -> [Text] -> LoggerState
+ Seihou.Effect.LoggerPure: LoggerState :: ![Text] -> ![Text] -> ![Text] -> ![Text] -> LoggerState
- Seihou.Effect.ProcessPure: ProcessMock :: Text -> [Text] -> (ExitCode, Text, Text) -> ProcessMock
+ Seihou.Effect.ProcessPure: ProcessMock :: !Text -> ![Text] -> !(ExitCode, Text, Text) -> ProcessMock
- Seihou.Engine.Migrate: ExecutedMigrationPlan :: ModuleName -> MigrationPlan -> [MigrationOpInstance] -> ExecutedMigrationPlan
+ Seihou.Engine.Migrate: ExecutedMigrationPlan :: !ModuleName -> !MigrationPlan -> ![MigrationOpInstance] -> ExecutedMigrationPlan
- Seihou.Engine.Preview: FilePreview :: FileStatus -> FilePath -> Text -> Maybe ModuleName -> PreviewLine
+ Seihou.Engine.Preview: FilePreview :: !FileStatus -> !FilePath -> !Text -> !Maybe ModuleName -> PreviewLine
- Seihou.Engine.Reconcile: DesiredFile :: FilePath -> Text -> ModuleName -> Strategy -> Set ApplicationId -> DesiredFile
+ Seihou.Engine.Reconcile: DesiredFile :: !FilePath -> !Text -> !ModuleName -> !Strategy -> !Set ApplicationId -> DesiredFile
- Seihou.Engine.Reconcile: DesiredFileOwner :: ModuleName -> Set ApplicationId -> DesiredFileOwner
+ Seihou.Engine.Reconcile: DesiredFileOwner :: !ModuleName -> !Set ApplicationId -> DesiredFileOwner
- Seihou.Engine.Reconcile: ObservedFile :: Bool -> Maybe SHA256 -> ObservedFile
+ Seihou.Engine.Reconcile: ObservedFile :: !Bool -> !Maybe SHA256 -> ObservedFile
- Seihou.Engine.Reconcile: PlannedFileState :: Text -> Text -> SHA256 -> Bool -> PlannedFileState
+ Seihou.Engine.Reconcile: PlannedFileState :: !Text -> !Text -> !SHA256 -> !Bool -> PlannedFileState
- Seihou.Engine.Reconcile: ReconciliationPlan :: Set ApplicationId -> Map FilePath FileReconciliation -> Set FilePath -> ReconciliationPlan
+ Seihou.Engine.Reconcile: ReconciliationPlan :: !Set ApplicationId -> !Map FilePath FileReconciliation -> !Set FilePath -> ReconciliationPlan
- Seihou.Engine.Reconcile: ReconciliationSummary :: Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> ReconciliationSummary
+ Seihou.Engine.Reconcile: ReconciliationSummary :: !Int -> !Int -> !Int -> !Int -> !Int -> !Int -> !Int -> !Int -> ReconciliationSummary
- Seihou.Engine.Reconcile: ResolvedFileConflict :: FileConflictChoice -> PlannedFileState -> ResolvedFileConflict
+ Seihou.Engine.Reconcile: ResolvedFileConflict :: !FileConflictChoice -> !PlannedFileState -> ResolvedFileConflict
- Seihou.Engine.Reconcile: [applicationIds] :: ReconciliationPlan -> Set ApplicationId
+ Seihou.Engine.Reconcile: [applicationIds] :: ReconciliationPlan -> !Set ApplicationId
- Seihou.Engine.Reconcile: [appliedContent] :: PlannedFileState -> Text
+ Seihou.Engine.Reconcile: [appliedContent] :: PlannedFileState -> !Text
- Seihou.Engine.Reconcile: [choice] :: ResolvedFileConflict -> FileConflictChoice
+ Seihou.Engine.Reconcile: [choice] :: ResolvedFileConflict -> !FileConflictChoice
- Seihou.Engine.Reconcile: [conflicts] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [conflicts] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [contentHash] :: ObservedFile -> Maybe SHA256
+ Seihou.Engine.Reconcile: [contentHash] :: ObservedFile -> !Maybe SHA256
- Seihou.Engine.Reconcile: [creates] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [creates] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [editedOrphans] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [editedOrphans] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [existed] :: ObservedFile -> Bool
+ Seihou.Engine.Reconcile: [existed] :: ObservedFile -> !Bool
- Seihou.Engine.Reconcile: [files] :: ReconciliationPlan -> Map FilePath FileReconciliation
+ Seihou.Engine.Reconcile: [files] :: ReconciliationPlan -> !Map FilePath FileReconciliation
- Seihou.Engine.Reconcile: [generatedBaseline] :: PlannedFileState -> Text
+ Seihou.Engine.Reconcile: [generatedBaseline] :: PlannedFileState -> !Text
- Seihou.Engine.Reconcile: [generatedContent] :: DesiredFile -> Text
+ Seihou.Engine.Reconcile: [generatedContent] :: DesiredFile -> !Text
- Seihou.Engine.Reconcile: [merged] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [merged] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [moduleName] :: DesiredFile -> ModuleName
+ Seihou.Engine.Reconcile: [moduleName] :: DesiredFile -> !ModuleName
- Seihou.Engine.Reconcile: [path] :: DesiredFile -> FilePath
+ Seihou.Engine.Reconcile: [path] :: DesiredFile -> !FilePath
- Seihou.Engine.Reconcile: [recordedHash] :: PlannedFileState -> SHA256
+ Seihou.Engine.Reconcile: [recordedHash] :: PlannedFileState -> !SHA256
- Seihou.Engine.Reconcile: [requiredDirectories] :: ReconciliationPlan -> Set FilePath
+ Seihou.Engine.Reconcile: [requiredDirectories] :: ReconciliationPlan -> !Set FilePath
- Seihou.Engine.Reconcile: [safeDeletes] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [safeDeletes] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [sharedOwnership] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [sharedOwnership] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [state] :: ResolvedFileConflict -> PlannedFileState
+ Seihou.Engine.Reconcile: [state] :: ResolvedFileConflict -> !PlannedFileState
- Seihou.Engine.Reconcile: [strategy] :: DesiredFile -> Strategy
+ Seihou.Engine.Reconcile: [strategy] :: DesiredFile -> !Strategy
- Seihou.Engine.Reconcile: [unchanged] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [unchanged] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [updates] :: ReconciliationSummary -> Int
+ Seihou.Engine.Reconcile: [updates] :: ReconciliationSummary -> !Int
- Seihou.Engine.Reconcile: [writeToDisk] :: PlannedFileState -> Bool
+ Seihou.Engine.Reconcile: [writeToDisk] :: PlannedFileState -> !Bool
- Seihou.Engine.Remove: ExecutedRemovalPlan :: ModuleName -> [RemovalOp] -> ExecutedRemovalPlan
+ Seihou.Engine.Remove: ExecutedRemovalPlan :: !ModuleName -> ![RemovalOp] -> ExecutedRemovalPlan
- Seihou.Engine.Remove: RemovalPlan :: ModuleName -> [RemovalFile] -> RemovalPlan
+ Seihou.Engine.Remove: RemovalPlan :: !ModuleName -> ![RemovalFile] -> RemovalPlan
- Seihou.Engine.Remove: [files] :: RemovalPlan -> [RemovalFile]
+ Seihou.Engine.Remove: [files] :: RemovalPlan -> ![RemovalFile]
- Seihou.Engine.Remove: [ops] :: ExecutedRemovalPlan -> [RemovalOp]
+ Seihou.Engine.Remove: [ops] :: ExecutedRemovalPlan -> ![RemovalOp]
- Seihou.Engine.Remove: [targetModule] :: ExecutedRemovalPlan -> ModuleName
+ Seihou.Engine.Remove: [targetModule] :: ExecutedRemovalPlan -> !ModuleName
- Seihou.Engine.Section: SectionMarker :: Text -> ModuleName -> SectionMarker
+ Seihou.Engine.Section: SectionMarker :: !Text -> !ModuleName -> SectionMarker
- Seihou.Engine.UpdateTransaction: UpdateTransaction :: FilePath -> FilePath -> Set FilePath -> UpdateTransaction
+ Seihou.Engine.UpdateTransaction: UpdateTransaction :: !FilePath -> !FilePath -> !Set FilePath -> UpdateTransaction
- Seihou.Engine.UpdateTransaction: [projectRoot] :: UpdateTransaction -> FilePath
+ Seihou.Engine.UpdateTransaction: [projectRoot] :: UpdateTransaction -> !FilePath
- Seihou.Engine.UpdateTransaction: [targets] :: UpdateTransaction -> Set FilePath
+ Seihou.Engine.UpdateTransaction: [targets] :: UpdateTransaction -> !Set FilePath
- Seihou.Engine.UpdateTransaction: [transactionDirectory] :: UpdateTransaction -> FilePath
+ Seihou.Engine.UpdateTransaction: [transactionDirectory] :: UpdateTransaction -> !FilePath
- Seihou.Engine.Validate: DiagCheck :: Text -> DiagSeverity -> [Text] -> DiagCheck
+ Seihou.Engine.Validate: DiagCheck :: !Text -> !DiagSeverity -> ![Text] -> DiagCheck
- Seihou.Engine.Validate: ValidateReport :: Module -> FilePath -> Bool -> Maybe Text -> [DiagCheck] -> ValidateReport
+ Seihou.Engine.Validate: ValidateReport :: !Module -> !FilePath -> !Bool -> !Maybe Text -> ![DiagCheck] -> ValidateReport
- Seihou.Prelude: infixl 1 &
+ Seihou.Prelude: infixl 1 ??
- Seihou.Prelude: infixl 8 ^.
+ Seihou.Prelude: infixl 8 ^#
- Seihou.Prelude: infixr 4 %~
+ Seihou.Prelude: infixr 4 ||~
- Seihou.Prelude: type family DispatchOf (e :: Effect) :: Dispatch
+ Seihou.Prelude: type family Zoomed (m :: Type -> Type) :: Type -> Type -> Type
Files
- seihou-core.cabal +9/−3
- src/Seihou/Composition/Graph.hs +15/−14
- src/Seihou/Composition/Instance.hs +9/−8
- src/Seihou/Composition/Plan.hs +11/−10
- src/Seihou/Composition/Recipe.hs +8/−6
- src/Seihou/Composition/Resolve.hs +44/−43
- src/Seihou/Core/AgentPrompt.hs +46/−25
- src/Seihou/Core/Application.hs +24/−19
- src/Seihou/Core/ArtifactOriginDetect.hs +115/−0
- src/Seihou/Core/ArtifactRef.hs +143/−0
- src/Seihou/Core/Blueprint.hs +54/−28
- src/Seihou/Core/CommandFingerprint.hs +3/−1
- src/Seihou/Core/CommandVar.hs +18/−17
- src/Seihou/Core/Migration.hs +33/−33
- src/Seihou/Core/Module.hs +102/−102
- src/Seihou/Core/Recipe.hs +8/−6
- src/Seihou/Core/Registry.hs +102/−99
- src/Seihou/Core/Scaffold.hs +4/−0
- src/Seihou/Core/Status.hs +5/−4
- src/Seihou/Core/Types.hs +245/−189
- src/Seihou/Core/Variable.hs +20/−19
- src/Seihou/Core/Version.hs +0/−1
- src/Seihou/Dhall/Eval.hs +39/−31
- src/Seihou/Effect/BaselineStoreInterp.hs +6/−5
- src/Seihou/Effect/BaselineStorePure.hs +3/−2
- src/Seihou/Effect/ConfigWriterPure.hs +19/−18
- src/Seihou/Effect/ConsolePure.hs +11/−10
- src/Seihou/Effect/FilesystemPure.hs +28/−22
- src/Seihou/Effect/LoggerPure.hs +10/−9
- src/Seihou/Effect/ProcessInterp.hs +2/−0
- src/Seihou/Effect/ProcessPure.hs +6/−5
- src/Seihou/Engine/Baseline.hs +8/−5
- src/Seihou/Engine/Conflict.hs +2/−1
- src/Seihou/Engine/Diff.hs +7/−6
- src/Seihou/Engine/Execute.hs +2/−1
- src/Seihou/Engine/Migrate.hs +28/−26
- src/Seihou/Engine/Plan.hs +23/−22
- src/Seihou/Engine/Preview.hs +36/−32
- src/Seihou/Engine/Reconcile.hs +78/−76
- src/Seihou/Engine/Remove.hs +33/−30
- src/Seihou/Engine/Section.hs +8/−7
- src/Seihou/Engine/Template.hs +7/−6
- src/Seihou/Engine/UpdateTransaction.hs +86/−85
- src/Seihou/Engine/Validate.hs +65/−64
- src/Seihou/Interaction/Confirm.hs +10/−9
- src/Seihou/Interaction/Prompt.hs +17/−16
- src/Seihou/Manifest/Types.hs +182/−109
- src/Seihou/Prelude.hs +33/−13
- test/Main.hs +5/−1
- test/Seihou/Composition/GraphSpec.hs +12/−10
- test/Seihou/Composition/InstanceSpec.hs +4/−2
- test/Seihou/Composition/ResolveSpec.hs +58/−56
- test/Seihou/Core/AgentPromptSpec.hs +97/−18
- test/Seihou/Core/ApplicationSpec.hs +23/−18
- test/Seihou/Core/ArtifactOriginDetectSpec.hs +83/−0
- test/Seihou/Core/ArtifactRefSpec.hs +141/−0
- test/Seihou/Core/BlueprintSpec.hs +149/−26
- test/Seihou/Core/CommandVarSpec.hs +16/−14
- test/Seihou/Core/ListSpec.hs +5/−3
- test/Seihou/Core/MigrationSpec.hs +31/−29
- test/Seihou/Core/ModuleSpec.hs +22/−24
- test/Seihou/Core/RegistrySpec.hs +64/−62
- test/Seihou/Core/RegistrySyncSpec.hs +34/−34
- test/Seihou/Core/ScaffoldSpec.hs +25/−23
- test/Seihou/Core/StatusSpec.hs +25/−28
- test/Seihou/Core/TypesSpec.hs +15/−13
- test/Seihou/Core/VariableSpec.hs +68/−66
- test/Seihou/Dhall/EvalSpec.hs +95/−93
- test/Seihou/Dhall/MigrationDecoderSpec.hs +16/−14
- test/Seihou/Effect/BaselineStoreSpec.hs +6/−4
- test/Seihou/Effect/ConfigWriterSpec.hs +10/−8
- test/Seihou/Effect/FilesystemSpec.hs +3/−1
- test/Seihou/Effect/LoggerSpec.hs +13/−11
- test/Seihou/Effect/ManifestStoreSpec.hs +9/−15
- test/Seihou/Engine/BaselineSpec.hs +9/−13
- test/Seihou/Engine/ConflictSpec.hs +12/−10
- test/Seihou/Engine/DiffSpec.hs +71/−73
- test/Seihou/Engine/ExecuteSpec.hs +19/−17
- test/Seihou/Engine/MigrateSpec.hs +34/−55
- test/Seihou/Engine/PlanSpec.hs +23/−25
- test/Seihou/Engine/PreviewSpec.hs +30/−35
- test/Seihou/Engine/ReconcileSpec.hs +33/−31
- test/Seihou/Engine/RemoveSpec.hs +59/−103
- test/Seihou/Engine/SectionSpec.hs +4/−3
- test/Seihou/Engine/UpdateTransactionSpec.hs +44/−42
- test/Seihou/Engine/ValidateSpec.hs +67/−65
- test/Seihou/Evaluation/ConditionalTemplateSpec.hs +9/−8
- test/Seihou/Evaluation/DhallTextFlakeSpec.hs +9/−8
- test/Seihou/Evaluation/SplitFlakeSpec.hs +9/−8
- test/Seihou/Integration/CompositionSpec.hs +17/−15
- test/Seihou/Integration/ExecutionSpec.hs +44/−42
- test/Seihou/Integration/GenerationSpec.hs +15/−13
- test/Seihou/Integration/ModuleLoadSpec.hs +21/−19
- test/Seihou/Interaction/ConfirmSpec.hs +22/−20
- test/Seihou/Interaction/PromptSpec.hs +59/−57
- test/Seihou/Manifest/TypesSpec.hs +314/−147
seihou-core.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: seihou-core-version: 0.5.0.0+version: 0.6.0.0 synopsis: Core library for Seihou project scaffolding description: Core library for Seihou, a composable project scaffolding system.@@ -24,10 +24,10 @@ library default-language: GHC2024 default-extensions:+ DeriveAnyClass DuplicateRecordFields NoFieldSelectors OverloadedLabels- OverloadedRecordDot OverloadedStrings TypeFamilies @@ -40,6 +40,8 @@ Seihou.Composition.Resolve Seihou.Core.AgentPrompt Seihou.Core.Application+ Seihou.Core.ArtifactOriginDetect+ Seihou.Core.ArtifactRef Seihou.Core.Blueprint Seihou.Core.CommandFingerprint Seihou.Core.CommandVar@@ -133,10 +135,10 @@ type: exitcode-stdio-1.0 default-language: GHC2024 default-extensions:+ DeriveAnyClass DuplicateRecordFields NoFieldSelectors OverloadedLabels- OverloadedRecordDot OverloadedStrings hs-source-dirs: test@@ -149,6 +151,8 @@ Seihou.Composition.ResolveSpec Seihou.Core.AgentPromptSpec Seihou.Core.ApplicationSpec+ Seihou.Core.ArtifactOriginDetectSpec+ Seihou.Core.ArtifactRefSpec Seihou.Core.BlueprintSpec Seihou.Core.CommandFingerprintSpec Seihou.Core.CommandVarSpec@@ -212,7 +216,9 @@ directory >=1.3 && <2, effectful-core >=2.4 && <3, filepath >=1.4 && <2,+ generic-lens >=2.2 && <3, hspec >=2.11 && <3,+ lens >=5.2 && <6, seihou-core, tasty >=1.4 && <2, tasty-hspec >=1.2 && <2,
src/Seihou/Composition/Graph.hs view
@@ -5,6 +5,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Seihou.Composition.Instance (ModuleInstance (..), mkInstance)@@ -18,17 +19,17 @@ -- 'ParentVars' have independent edges, so the topological sort -- produces one node per distinct invocation. data CompositionGraph = CompositionGraph- { cgModules :: Map ModuleInstance Module,- cgEdges :: Map ModuleInstance [ModuleInstance]+ { modules :: !(Map ModuleInstance Module),+ edges :: !(Map ModuleInstance [ModuleInstance]) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Build a composition graph from a list of module instances. -- -- Each module's dependencies are resolved to the corresponding -- 'ModuleInstance' present in the input. A dependency edge with--- @depVars@ selects the instance created with those exact bindings;--- a bare dependency (no @depVars@) selects the 'emptyParentVars'+-- @vars@ selects the instance created with those exact bindings;+-- a bare dependency (no @vars@) selects the 'emptyParentVars' -- instance. If the instance set does not contain the child the edge -- points to, the edge is silently dropped — the loader is -- responsible for ensuring every referenced child is loaded first.@@ -36,18 +37,18 @@ buildGraph entries = let present = Set.fromList (map fst entries) edgesFor m =- -- Dedupe edges: if a parent lists the same @(depModule, depVars)@+ -- Dedupe edges: if a parent lists the same @(module_, vars)@ -- twice, the two edges resolve to the same child instance and -- must count as one for the topological sort's in-degree. Set.toAscList . Set.fromList $ [ child- | dep <- m.dependencies,- let child = mkInstance dep.depModule (parentVarsFromDep dep),+ | dep <- m ^. #dependencies,+ let child = mkInstance (dep ^. #module_) (parentVarsFromDep dep), Set.member child present ] in CompositionGraph- { cgModules = Map.fromList entries,- cgEdges = Map.fromList [(inst, edgesFor m) | (inst, m) <- entries]+ { modules = Map.fromList entries,+ edges = Map.fromList [(inst, edgesFor m) | (inst, m) <- entries] } -- | Topological sort using Kahn's algorithm, operating on@@ -62,12 +63,12 @@ topoSort graph = kahn initialReady initialInDegree [] allNodes where allNodes :: Set ModuleInstance- allNodes = Map.keysSet graph.cgEdges+ allNodes = Map.keysSet (graph ^. #edges) initialInDegree :: Map ModuleInstance Int initialInDegree = Map.fromList- [ (n, length [d | d <- Map.findWithDefault [] n graph.cgEdges, Set.member d allNodes])+ [ (n, length [d | d <- Map.findWithDefault [] n (graph ^. #edges), Set.member d allNodes]) | n <- Set.toList allNodes ] @@ -82,7 +83,7 @@ kahn [] _ result remaining | Set.null remaining = Right (reverse result) | otherwise =- Left (CircularDependency (map (.instanceModule) (Set.toList remaining)))+ Left (CircularDependency (map (^. #module_) (Set.toList remaining))) kahn (node : rest) inDeg result remaining = let remaining' = Set.delete node remaining (newReady, inDeg') = foldl (decrementDep node) ([], inDeg) (Set.toList remaining')@@ -90,7 +91,7 @@ decrementDep :: ModuleInstance -> ([ModuleInstance], Map ModuleInstance Int) -> ModuleInstance -> ([ModuleInstance], Map ModuleInstance Int) decrementDep processed (ready, inDeg) candidate =- let deps = Map.findWithDefault [] candidate graph.cgEdges+ let deps = Map.findWithDefault [] candidate (graph ^. #edges) in if processed `elem` deps then let newDeg = Map.findWithDefault 0 candidate inDeg - 1
src/Seihou/Composition/Instance.hs view
@@ -9,6 +9,7 @@ import Crypto.Hash.SHA256 qualified as SHA256 import Data.ByteString.Base16 qualified as Base16+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Data.Text.Encoding qualified as TE@@ -27,15 +28,15 @@ -- one instance. See @docs/plans/10-parameterized-dep-multi-instantiation.md@ -- for the full rationale. data ModuleInstance = ModuleInstance- { instanceModule :: ModuleName,- instanceParentVars :: ParentVars+ { module_ :: !ModuleName,+ parentVars :: !ParentVars }- deriving stock (Eq, Ord, Show)+ deriving stock (Eq, Generic, Ord, Show) -- | Build a 'ModuleInstance' from a module name and the parent-supplied -- bindings along the edge that reached it. mkInstance :: ModuleName -> ParentVars -> ModuleInstance-mkInstance n pv = ModuleInstance {instanceModule = n, instanceParentVars = pv}+mkInstance n pv = ModuleInstance {module_ = n, parentVars = pv} -- | The 'ModuleInstance' for a top-level (primary / CLI-additional / -- recipe-expanded) module, which receives no parent-supplied bindings.@@ -59,13 +60,13 @@ -- 'ModuleName' alongside the bindings so output stays readable. qualifiedName :: ModuleInstance -> ModuleName qualifiedName inst =- case Map.null inst.instanceParentVars.unParentVars of- True -> inst.instanceModule+ case Map.null (inst ^. #parentVars . #unParentVars) of+ True -> (inst ^. #module_) False -> ModuleName $- inst.instanceModule.unModuleName+ inst ^. #module_ . #unModuleName <> "#"- <> stableHash inst.instanceParentVars+ <> stableHash (inst ^. #parentVars) -- | Compute the disambiguating hash for a 'ParentVars' set. --
src/Seihou/Composition/Plan.hs view
@@ -9,6 +9,7 @@ import Data.Aeson qualified as Aeson import Data.Aeson.Encode.Pretty qualified as AesonPretty import Data.Aeson.KeyMap qualified as KM+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -47,16 +48,16 @@ instancedModule = Module { name = qn,- version = m.version,- description = m.description,- vars = m.vars,- exports = m.exports,- prompts = m.prompts,- steps = m.steps,- commands = m.commands,- dependencies = m.dependencies,- removal = m.removal,- migrations = m.migrations+ version = m ^. #version,+ description = m ^. #description,+ vars = m ^. #vars,+ exports = m ^. #exports,+ prompts = m ^. #prompts,+ steps = m ^. #steps,+ commands = m ^. #commands,+ dependencies = m ^. #dependencies,+ removal = m ^. #removal,+ migrations = m ^. #migrations } result <- compilePlan dir instancedModule vars case result of
src/Seihou/Composition/Recipe.hs view
@@ -3,6 +3,8 @@ ) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Text (Text)@@ -17,14 +19,14 @@ -- -- The first module in the recipe's list becomes the primary module (used for -- config namespace derivation). All remaining modules become additional modules.--- Variable overrides are collected from each module entry's @depVars@ bindings.+-- Variable overrides are collected from each module entry's @vars@ bindings. expandRecipe :: Recipe -> Either [Text] ExpandedRecipe expandRecipe recipe = do validated <- validateRecipe recipe- case validated.modules of+ case validated ^. #modules of [] -> Left ["recipe must list at least one module"] primary : additional ->- let primaryName = primary.depModule- additionalNames = map (.depModule) additional- overrides = Map.unions (map (.depVars) validated.modules)- in Right (primaryName, additionalNames, overrides, validated.vars, validated.prompts)+ let primaryName = (primary ^. #module_)+ additionalNames = map (^. #module_) additional+ overrides = Map.unions (map (^. #vars) (validated ^. #modules))+ in Right (primaryName, additionalNames, overrides, validated ^. #vars, validated ^. #prompts)
src/Seihou/Composition/Resolve.hs view
@@ -12,6 +12,7 @@ where import Control.Monad.Trans.Except (ExceptT (..), runExceptT)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -39,7 +40,7 @@ -- Returns modules with their directories in execution order (dependencies first). -- -- Each entry carries a 'ModuleInstance' identifying the exact invocation.--- Two dependency edges to the same module with different @depVars@ produce+-- Two dependency edges to the same module with different @vars@ produce -- two distinct entries; identical edges dedupe. loadComposition :: [FilePath] ->@@ -48,11 +49,11 @@ IO (Either ModuleLoadError [(ModuleInstance, Module, FilePath)]) loadComposition searchPaths primary additional = runExceptT $ do (primaryMod, primaryDir) <- ExceptT $ loadModuleWithDir searchPaths primary- let effectiveDeps = primaryMod.dependencies ++ map simpleDep additional- effectivePrimary = primaryMod {dependencies = nubOrdBy (.depModule) effectiveDeps}+ let effectiveDeps = primaryMod ^. #dependencies ++ map simpleDep additional+ effectivePrimary = (primaryMod & #dependencies .~ nubOrdBy (^. #module_) effectiveDeps) primaryInst = primaryInstance primary loaded = Map.singleton primaryInst (effectivePrimary, primaryDir)- seeds = [(mkInstance dep.depModule (parentVarsFromDep dep)) | dep <- effectivePrimary.dependencies]+ seeds = [(mkInstance (dep ^. #module_) (parentVarsFromDep dep)) | dep <- effectivePrimary ^. #dependencies] allInstances <- ExceptT $ loadTransitive searchPaths loaded seeds let entries = [(inst, m) | (inst, (m, _)) <- Map.toList allInstances] graph = buildGraph entries@@ -111,11 +112,11 @@ go _ [] perModule _ = Right perModule go parentVarsMap ((inst, m, _dir) : rest) perModule allExports = do let visibleExports = gatherEdgeExports m allExports- adjustedDecls = map (injectExportDefault visibleExports) m.vars+ adjustedDecls = map (injectExportDefault visibleExports) (m ^. #vars) myParentVars = Map.findWithDefault Map.empty inst parentVarsMap saved = Map.findWithDefault Map.empty inst savedValues resolved <- resolveVariablesWithSaved adjustedDecls cliOverrides saved envVars namespace context localConfig nsConfig ctxConfig globalConfig myParentVars- let declaredNames = Set.fromList (map (.name) m.vars)+ let declaredNames = Set.fromList (map (^. #name) (m ^. #vars)) inherited = Map.mapWithKey makeInheritedResolved@@ -183,12 +184,12 @@ goPrompt _ _ [] perModule _ = pure (Right perModule) goPrompt interactive parentVarsMap ((inst, m, _dir) : rest) perModule allExports = do let visibleExports = gatherEdgeExports m allExports- adjustedDecls = map (injectExportDefault visibleExports) m.vars+ adjustedDecls = map (injectExportDefault visibleExports) (m ^. #vars) myParentVars = Map.findWithDefault Map.empty inst parentVarsMap saved = Map.findWithDefault Map.empty inst savedValues case resolveVariablesWithSaved adjustedDecls cliOverrides saved envVars namespace context localConfig nsConfig ctxConfig globalConfig myParentVars of Right resolved -> do- let declaredNames = Set.fromList (map (.name) m.vars)+ let declaredNames = Set.fromList (map (^. #name) (m ^. #vars)) inherited = Map.mapWithKey makeInheritedResolved@@ -197,18 +198,18 @@ let optionalDecls = [ d | d <- adjustedDecls,- not d.required,- not (Map.member d.name resolvedWithInherited),- any (\p -> p.var == d.name) m.prompts+ not (d ^. #required),+ not (Map.member (d ^. #name) resolvedWithInherited),+ any (\p -> p ^. #var == d ^. #name) (m ^. #prompts) ] optionalPrompted <- if interactive && not (null optionalDecls) then do- let currentBindings = Map.map (.value) (Map.unions (Map.elems perModule))- allBindings = Map.union (Map.map (.value) resolvedWithInherited) currentBindings+ let currentBindings = Map.map (^. #value) (Map.unions (Map.elems perModule))+ allBindings = Map.union (Map.map (^. #value) resolvedWithInherited) currentBindings putText "" putText "Optional configuration:"- runPrompts m.prompts optionalDecls allBindings+ runPrompts (m ^. #prompts) optionalDecls allBindings else pure Map.empty let fullResolved = resolvedWithInherited `Map.union` optionalPrompted myExports = exportedVars m fullResolved@@ -226,16 +227,16 @@ if not interactive || null missing then pure (Left errs) else do- let currentBindings = Map.map (.value) (Map.unions (Map.elems perModule))- missingDecls = [d | d <- adjustedDecls, d.name `elem` map getMissingName missing]- prompted <- runPrompts m.prompts missingDecls currentBindings+ let currentBindings = Map.map (^. #value) (Map.unions (Map.elems perModule))+ missingDecls = [d | d <- adjustedDecls, (d ^. #name) `elem` map getMissingName missing]+ prompted <- runPrompts (m ^. #prompts) missingDecls currentBindings let stillMissing = [e | e <- missing, not (Map.member (getMissingName e) prompted)] if not (null stillMissing) then pure (Left stillMissing) else do let promptedOverrides = Map.union cliOverrides $- Map.map (varValueToText . (.value)) prompted+ Map.map (varValueToText . (^. #value)) prompted case resolveVariablesWithSaved adjustedDecls promptedOverrides saved envVars namespace context localConfig nsConfig ctxConfig globalConfig myParentVars of Left errs' -> pure (Left errs') Right resolved -> do@@ -247,7 +248,7 @@ Nothing -> rv ) resolved- declaredNames = Set.fromList (map (.name) m.vars)+ declaredNames = Set.fromList (map (^. #name) (m ^. #vars)) inherited = Map.mapWithKey makeInheritedResolved@@ -256,18 +257,18 @@ let optionalDecls' = [ d | d <- adjustedDecls,- not d.required,- not (Map.member d.name resolvedWithInherited'),- any (\p -> p.var == d.name) m.prompts+ not (d ^. #required),+ not (Map.member (d ^. #name) resolvedWithInherited'),+ any (\p -> p ^. #var == d ^. #name) (m ^. #prompts) ] optionalPrompted' <- if not (null optionalDecls') then do- let cb = Map.map (.value) (Map.unions (Map.elems perModule))- ab = Map.union (Map.map (.value) resolvedWithInherited') cb+ let cb = Map.map (^. #value) (Map.unions (Map.elems perModule))+ ab = Map.union (Map.map (^. #value) resolvedWithInherited') cb putText "" putText "Optional configuration:"- runPrompts m.prompts optionalDecls' ab+ runPrompts (m ^. #prompts) optionalDecls' ab else pure Map.empty let fullResolved = resolvedWithInherited' `Map.union` optionalPrompted' myExports = exportedVars m fullResolved@@ -281,7 +282,7 @@ -- | Collect the exports visible along a module's dependency edges. -- -- Each dependency edge is resolved to the exact child instance--- @(depModule, depVars)@, not just the module name, so that two+-- @(module_, vars)@, not just the module name, so that two -- sibling instances of the same module contribute their own -- per-instance exports without interference. gatherEdgeExports ::@@ -291,8 +292,8 @@ gatherEdgeExports m allExports = Map.unions [ Map.findWithDefault Map.empty childInst allExports- | dep <- m.dependencies,- let childInst = mkInstance dep.depModule (parentVarsFromDep dep)+ | dep <- m ^. #dependencies,+ let childInst = mkInstance (dep ^. #module_) (parentVarsFromDep dep) ] -- | Extract the variable name from a MissingRequiredVar error.@@ -320,14 +321,14 @@ exportedVars :: Module -> Map VarName ResolvedVar -> Map VarName VarValue exportedVars m resolved = Map.fromList- [ (exportName e, rv.value)- | e <- m.exports,- Just rv <- [Map.lookup e.var resolved]+ [ (exportName e, rv ^. #value)+ | e <- m ^. #exports,+ Just rv <- [Map.lookup (e ^. #var) resolved] ] where- exportName e = case e.alias of+ exportName e = case e ^. #alias of Just a -> a- Nothing -> e.var+ Nothing -> (e ^. #var) -- Internal helpers @@ -365,14 +366,14 @@ loadTransitive searchPaths loaded (inst : rest) | Map.member inst loaded = loadTransitive searchPaths loaded rest | otherwise = do- result <- loadModuleWithDir searchPaths inst.instanceModule+ result <- loadModuleWithDir searchPaths (inst ^. #module_) case result of Left err -> pure (Left err) Right (m, dir) -> do let loaded' = Map.insert inst (m, dir) loaded newInstances =- [ mkInstance dep.depModule (parentVarsFromDep dep)- | dep <- m.dependencies+ [ mkInstance (dep ^. #module_) (parentVarsFromDep dep)+ | dep <- m ^. #dependencies ] loadTransitive searchPaths loaded' (rest ++ newInstances) @@ -381,8 +382,8 @@ -- the module author's default while still being overridable by CLI/env. injectExportDefault :: Map VarName VarValue -> VarDecl -> VarDecl injectExportDefault exports decl =- case Map.lookup decl.name exports of- Just val -> decl {default_ = Just val}+ case Map.lookup (decl ^. #name) exports of+ Just val -> (decl & #default_ ?~ val) Nothing -> decl -- | Create a ResolvedVar for an inherited (non-declared) export variable.@@ -415,7 +416,7 @@ -- Returns a map keyed by 'ModuleInstance' — not by bare 'ModuleName' — -- so two sibling invocations of the same child, each supplied with -- different bindings by different parents, carry their own edge--- decorations independently. A child edge's @depVars@ uniquely+-- decorations independently. A child edge's @vars@ uniquely -- identifies the target instance, so no merging of overlapping -- bindings is required: each @(ModuleInstance, edgeVars)@ pair is -- distinct by construction.@@ -424,11 +425,11 @@ Map ModuleInstance (Map VarName (Text, ModuleName)) collectParentVars modules = Map.fromList- [ (childInst, Map.map (,m.name) dep.depVars)+ [ (childInst, Map.map (,m ^. #name) (dep ^. #vars)) | (_, m, _) <- modules,- dep <- m.dependencies,- not (Map.null dep.depVars),- let childInst = mkInstance dep.depModule (parentVarsFromDep dep)+ dep <- m ^. #dependencies,+ not (Map.null (dep ^. #vars)),+ let childInst = mkInstance (dep ^. #module_) (parentVarsFromDep dep) ] -- | Remove duplicates from a list while preserving order, using a key function.
src/Seihou/Core/AgentPrompt.hs view
@@ -10,9 +10,11 @@ checkAgentPromptFiles, checkAgentPromptTags, checkAgentPromptAllowedTools,+ checkAgentPromptLaunch, ) where +import Data.Generics.Labels () import Data.Set qualified as Set import Data.Text qualified as T import Numeric.Natural (Natural)@@ -38,21 +40,22 @@ <> checkAgentPromptGuidance p <> checkAgentPromptTags p <> checkAgentPromptAllowedTools p+ <> checkAgentPromptLaunch p allErrs = pureErrs <> fileErrs pure $ if null allErrs then Right p- else Left (ValidationError p.name allErrs)+ else Left (ValidationError (p ^. #name) allErrs) checkAgentPromptNameFormat :: AgentPrompt -> [Text] checkAgentPromptNameFormat p =- let n = p.name.unModuleName+ let n = (p ^. #name . #unModuleName) in if T.null n || not (isValidModuleName n) then ["prompt name must match [a-z][a-z0-9-]*, got: " <> n] else [] checkAgentPromptVersionPresent :: AgentPrompt -> [Text]-checkAgentPromptVersionPresent p = case p.version of+checkAgentPromptVersionPresent p = case p ^. #version of Nothing -> [] Just v | T.null (T.strip v) -> ["prompt version, if specified, must not be empty"]@@ -60,30 +63,30 @@ checkAgentPromptBodyNonEmpty :: AgentPrompt -> [Text] checkAgentPromptBodyNonEmpty p- | T.null (T.strip p.prompt) = ["prompt body must not be empty"]+ | T.null (T.strip (p ^. #prompt)) = ["prompt body must not be empty"] | otherwise = [] checkAgentPromptUniqueVars :: AgentPrompt -> [Text] checkAgentPromptUniqueVars p =- let names = map (\d -> d.name.unVarName) p.vars+ let names = map (\d -> d ^. #name . #unVarName) (p ^. #vars) in map (\n -> "duplicate variable name: " <> n) (findDupes Set.empty Set.empty names) checkAgentPromptPromptRefs :: AgentPrompt -> [Text] checkAgentPromptPromptRefs p =- let varNames = Set.fromList (map (.name) p.vars)+ let varNames = Set.fromList (map (^. #name) (p ^. #vars)) in concatMap ( \prompt ->- if Set.member prompt.var varNames+ if Set.member (prompt ^. #var) varNames then []- else ["prompt references undeclared variable: " <> prompt.var.unVarName]+ else ["prompt references undeclared variable: " <> prompt ^. #var . #unVarName] )- p.prompts+ (p ^. #prompts) checkAgentPromptCommandVars :: AgentPrompt -> [Text] checkAgentPromptCommandVars p =- duplicateCommandVars <> concatMap checkCommandVar p.commandVars+ duplicateCommandVars <> concatMap checkCommandVar (p ^. #commandVars) where- commandNames = map (\cv -> cv.name.unVarName) p.commandVars+ commandNames = map (\cv -> cv ^. #name . #unVarName) (p ^. #commandVars) duplicateCommandVars = map@@ -91,14 +94,14 @@ (findDupes Set.empty Set.empty commandNames) checkCommandVar cv =- checkName cv <> checkRun cv <> checkWorkDir cv.workDir <> checkMaxBytes cv.maxBytes+ checkName cv <> checkRun cv <> checkWorkDir (cv ^. #workDir) <> checkMaxBytes (cv ^. #maxBytes) checkName cv- | T.null (T.strip cv.name.unVarName) = ["command variable name must not be empty"]+ | T.null (T.strip (cv ^. #name . #unVarName)) = ["command variable name must not be empty"] | otherwise = [] checkRun cv- | T.null (T.strip cv.run) = ["command variable '" <> cv.name.unVarName <> "' run must not be empty"]+ | T.null (T.strip (cv ^. #run)) = ["command variable '" <> cv ^. #name . #unVarName <> "' run must not be empty"] | otherwise = [] checkWorkDir Nothing = []@@ -118,26 +121,26 @@ checkAgentPromptGuidance :: AgentPrompt -> [Text] checkAgentPromptGuidance p =- concatMap checkGuidance p.guidance+ concatMap checkGuidance (p ^. #guidance) where- knownVars = Set.fromList (map (.name) p.vars <> map (.name) p.commandVars)+ knownVars = Set.fromList (map (^. #name) (p ^. #vars) <> map (^. #name) (p ^. #commandVars)) checkGuidance g = checkTitle g <> checkBody g <> checkConditionRefs g checkTitle g- | T.null (T.strip g.title) = ["guidance title must not be empty"]+ | T.null (T.strip (g ^. #title)) = ["guidance title must not be empty"] | otherwise = [] checkBody g- | T.null (T.strip g.body) = ["guidance body must not be empty"]+ | T.null (T.strip (g ^. #body)) = ["guidance body must not be empty"] | otherwise = [] checkConditionRefs g =- case g.condition of+ case g ^. #condition of Nothing -> [] Just cond ->- [ "guidance '" <> g.title <> "' references undeclared variable: " <> ref.unVarName+ [ "guidance '" <> g ^. #title <> "' references undeclared variable: " <> ref ^. #unVarName | (ref, _) <- exprRefs cond, not (Set.member ref knownVars) ]@@ -147,30 +150,48 @@ concat <$> mapM ( \pf -> do- let path = baseDir </> "files" </> pf.src+ let path = baseDir </> "files" </> (pf ^. #src) exists <- doesFileExist path pure $ if exists then []- else ["prompt file not found: " <> T.pack pf.src]+ else ["prompt file not found: " <> T.pack (pf ^. #src)] )- p.files+ (p ^. #files) checkAgentPromptTags :: AgentPrompt -> [Text] checkAgentPromptTags p = [ "tag must not be empty"- | t <- p.tags,+ | t <- p ^. #tags, T.null (T.strip t) ] checkAgentPromptAllowedTools :: AgentPrompt -> [Text]-checkAgentPromptAllowedTools p = case p.allowedTools of+checkAgentPromptAllowedTools p = case p ^. #allowedTools of Nothing -> [] Just xs -> [ "allowedTools entry must not be empty" | t <- xs, T.null (T.strip t) ]++-- | Every field the @launch@ record does set must be non-blank. The declared+-- values themselves (which provider, which effort) are parsed by the CLI+-- layer, which owns those vocabularies; core only rejects blanks.+checkAgentPromptLaunch :: AgentPrompt -> [Text]+checkAgentPromptLaunch p = case p ^. #launch of+ Nothing -> []+ Just l ->+ blankErr "provider" (l ^. #provider)+ <> blankErr "model" (l ^. #model)+ <> blankErr "effort" (l ^. #effort)+ <> blankErr "mode" (l ^. #mode)+ where+ blankErr key value =+ [ "launch." <> key <> ", if specified, must not be empty"+ | Just v <- [value],+ T.null (T.strip v)+ ] findDupes :: Set.Set Text -> Set.Set Text -> [Text] -> [Text] findDupes _ _ [] = []
src/Seihou/Core/Application.hs view
@@ -6,6 +6,8 @@ ) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set qualified as Set@@ -21,38 +23,42 @@ -- update replaces the same application record. mkApplicationId :: AppliedTarget -> [ModuleName] -> ApplicationId mkApplicationId target additional =- ApplicationId (hashContent canonical).unSHA256+ ApplicationId ((hashContent canonical) ^. #unSHA256) where (kind, targetName) = case target of- AppliedModuleTarget name -> ("module", name.unModuleName)- AppliedRecipeTarget name -> ("recipe", name.unRecipeName)+ AppliedModuleTarget name -> ("module", name ^. #unModuleName)+ AppliedRecipeTarget name -> ("recipe", name ^. #unRecipeName) canonical = T.intercalate "\n" ( [ "target-kind=" <> kind, "target-name=" <> targetName ]- ++ map ("additional=" <>) (map (.unModuleName) additional)+ ++ map ("additional=" <>) (map (^. #unModuleName) additional) ) -- | Capture a composition using the already-resolved, instance-scoped -- values from the generation pipeline.+--+-- The target and each module instance are identified by the portable+-- 'ArtifactOrigin' the manifest records, never by the directory they happened+-- to be loaded from on this machine. buildAppliedComposition :: AppliedTarget ->- FilePath ->+ ArtifactOrigin -> Maybe Text -> [ModuleName] -> Maybe Text -> Maybe Text ->- [(ModuleInstance, Module, FilePath)] ->+ [(ModuleInstance, Module, ArtifactOrigin)] -> Map ModuleInstance (Map VarName ResolvedVar) -> UTCTime -> AppliedComposition-buildAppliedComposition target targetSource targetVersion additional namespace context modulesInOrder resolved now =+buildAppliedComposition target targetOrigin targetVersion additional namespace context modulesInOrder resolved now = AppliedComposition { applicationId = mkApplicationId target additional, target = target,- targetSource = targetSource,+ targetOrigin = targetOrigin, targetVersion = targetVersion, additionalModules = additional, namespace = namespace,@@ -62,24 +68,24 @@ appliedAt = now } where- buildInstance (inst, modul, source) =+ buildInstance (inst, modul, origin) = AppliedInstanceState- { name = inst.instanceModule,- parentVars = inst.instanceParentVars,- source = source,- moduleVersion = modul.version,- resolvedVars = Map.map (varValueToText . (.value)) (Map.findWithDefault Map.empty inst resolved)+ { name = inst ^. #module_,+ parentVars = inst ^. #parentVars,+ origin = origin,+ moduleVersion = modul ^. #version,+ resolvedVars = Map.map (varValueToText . (^. #value)) (Map.findWithDefault Map.empty inst resolved) } -- | Replace an existing application in place, or append a newly-applied one. replaceAppliedComposition :: AppliedComposition -> [AppliedComposition] -> [AppliedComposition] replaceAppliedComposition replacement existing- | any ((== replacement.applicationId) . (.applicationId)) existing =+ | any ((== replacement ^. #applicationId) . (^. #applicationId)) existing = map replaceMatching existing | otherwise = existing ++ [replacement] where replaceMatching current- | current.applicationId == replacement.applicationId = replacement+ | current ^. #applicationId == (replacement ^. #applicationId) = replacement | otherwise = current -- | Attribute the current file result to an application while retaining@@ -89,10 +95,9 @@ attachApplication :: ApplicationId -> Maybe FileRecord -> FileRecord -> FileRecord attachApplication applicationId previous current = current- { applicationIds = Set.insert applicationId (Set.union current.applicationIds priorApplications)- }+ & #applicationIds .~ Set.insert applicationId (Set.union (current ^. #applicationIds) priorApplications) where- priorApplications = maybe Set.empty (.applicationIds) previous+ priorApplications = maybe Set.empty (^. #applicationIds) previous varValueToText :: VarValue -> Text varValueToText (VText value) = value
+ src/Seihou/Core/ArtifactOriginDetect.hs view
@@ -0,0 +1,115 @@+-- | Turn an absolute artifact directory into a portable 'ArtifactOrigin'.+--+-- Module discovery hands every caller an absolute directory, because+-- @Seihou.Core.Module.defaultSearchPaths@ is built from+-- 'System.Directory.getCurrentDirectory' and+-- 'System.Directory.getXdgDirectory'. Absolute paths must never reach+-- @.seihou\/manifest.json@, which is checked into version control and read+-- on other developers' machines, so every manifest write site funnels its+-- directory through 'detectArtifactOrigin' first.+--+-- The read side of @.seihou-origin.json@ lives here rather than in+-- @seihou-cli@ because this module needs it and @seihou-core@ cannot depend+-- on @seihou-cli-internal@. @Seihou.CLI.InstallShared@ re-exports it, so+-- existing importers are unaffected; the write side ('OriginMeta',+-- @installModuleDir@) stays in the CLI.+module Seihou.Core.ArtifactOriginDetect+ ( detectArtifactOrigin,+ OriginInfo (..),+ readOriginInfo,+ )+where++import Control.Exception (IOException, try)+import Data.Aeson (FromJSON (..), withObject, (.:), (.:?))+import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as LBS+import Data.Generics.Labels ()+import Data.Text qualified as T+import Seihou.Core.Types (ArtifactOrigin (..))+import Seihou.Prelude+import System.Directory (canonicalizePath, doesFileExist)+import System.FilePath (makeRelative, pathSeparator, takeFileName)++-- | Read side of @.seihou-origin.json@. Tolerates files written by older+-- 'seihou install' runs that may have been missing optional fields.+data OriginInfo = OriginInfo+ { sourceUrl :: !Text,+ repoName :: !(Maybe Text),+ version :: !(Maybe Text)+ }+ deriving stock (Eq, Generic, Show)++instance FromJSON OriginInfo where+ parseJSON = withObject "OriginInfo" $ \v ->+ OriginInfo <$> v .: "sourceUrl" <*> v .:? "repoName" <*> v .:? "version"++-- | Read and parse @.seihou-origin.json@ at the given installed-module+-- directory. Returns 'Nothing' if the file is absent or unparseable.+readOriginInfo :: FilePath -> IO (Maybe OriginInfo)+readOriginInfo installedDir = do+ let path = installedDir </> ".seihou-origin.json"+ exists <- doesFileExist path+ if not exists+ then pure Nothing+ else do+ bs <- LBS.readFile path+ pure (Aeson.decode bs)++-- | Classify an absolute artifact directory into a portable origin.+--+-- @projectRoot@ is the absolute path of the project being generated into+-- (the directory holding @.seihou@). @artifactDir@ is the absolute+-- directory that holds the artifact's @module.dhall@, @recipe.dhall@,+-- @blueprint.dhall@, or @prompt.dhall@.+--+-- Classification, in order:+--+-- 1. If @artifactDir@ is inside @projectRoot@, the result is a+-- 'ProjectOrigin' holding the path relative to @projectRoot@ with+-- forward slashes.+-- 2. Otherwise, if @artifactDir@ contains a readable+-- @.seihou-origin.json@ with a @sourceUrl@, the result is a+-- 'RemoteOrigin' carrying that URL, the directory's base name, and+-- the recorded @repoName@.+-- 3. Otherwise the result is a 'LocalOrigin' holding the directory's+-- base name.+detectArtifactOrigin :: FilePath -> FilePath -> IO ArtifactOrigin+detectArtifactOrigin projectRoot artifactDir = do+ root <- canonicalizeOr projectRoot+ dir <- canonicalizeOr artifactDir+ case insideProject root dir of+ Just relative -> pure (ProjectOrigin relative)+ Nothing -> do+ originInfo <- readOriginInfo dir+ let name = T.pack (takeFileName dir)+ pure $ case originInfo of+ Just info -> RemoteOrigin (info ^. #sourceUrl) name (info ^. #repoName)+ Nothing -> LocalOrigin name++-- | 'canonicalizePath' throws when an intermediate component does not+-- exist, which happens in tests and for artifacts that were removed between+-- discovery and manifest write. Fall back to the raw path in that case.+canonicalizeOr :: FilePath -> IO FilePath+canonicalizeOr path = do+ result <- try @IOException (canonicalizePath path)+ pure (either (const path) id result)++-- | The artifact directory's path relative to the project root, when it is+-- genuinely inside it.+--+-- 'makeRelative' returns its second argument unchanged when the two paths+-- share no prefix, and returns @"."@ when they are the same directory, so+-- both cases have to be rejected explicitly. A leading @".."@ cannot appear+-- (GHC's 'makeRelative' never produces one) but is rejected anyway so a+-- future implementation change cannot smuggle an escaping path into the+-- manifest.+insideProject :: FilePath -> FilePath -> Maybe FilePath+insideProject root dir+ | relative == dir = Nothing+ | relative == "." = Nothing+ | take 2 relative == ".." = Nothing+ | otherwise = Just (map toForwardSlash relative)+ where+ relative = makeRelative root dir+ toForwardSlash c = if c == pathSeparator then '/' else c
+ src/Seihou/Core/ArtifactRef.hs view
@@ -0,0 +1,143 @@+-- | Turn an artifact origin recorded in the manifest into a directory on+-- this machine.+--+-- @.seihou\/manifest.json@ is checked into version control and records no+-- absolute path (see+-- docs\/adr\/0001-manifest-is-a-checked-in-machine-independent-artifact.md),+-- so every command that needs an artifact's bytes has to ask this question+-- first. This module is the single place that answers it, and the single+-- place that phrases the answer when it is "not here".+module Seihou.Core.ArtifactRef+ ( ArtifactRefError (..),+ resolveArtifactOrigin,+ renderArtifactRefError,+ )+where++import Data.Generics.Labels ()+import Data.Text qualified as T+import Seihou.Core.Types (ArtifactOrigin (..))+import Seihou.Prelude+import System.Directory (doesFileExist)+import System.FilePath (joinPath)++-- | Why an origin recorded in the manifest could not be turned into a+-- directory on this machine.+data ArtifactRefError+ = -- | Nothing named by the origin exists in any search path. Carries the+ -- origin and the exact directories that were probed, in order.+ ArtifactNotFoundLocally !ArtifactOrigin ![FilePath]+ | -- | A 'ProjectOrigin' pointed at a path inside the project that does+ -- not exist. Carries the origin and the absolute path that was tried.+ ProjectArtifactMissing !ArtifactOrigin !FilePath+ deriving stock (Eq, Show, Generic)++-- | Turn a recorded origin into the absolute directory on this machine+-- that holds the artifact's definition file.+--+-- @projectRoot@ is the absolute directory containing @.seihou@.+-- @searchPaths@ is normally 'Seihou.Core.Module.defaultSearchPaths'; it is+-- a parameter so tests can supply temporary directories.+-- @definitionFile@ is the file that must be present for a directory to+-- count as the artifact — @"module.dhall"@ for modules,+-- @"recipe.dhall"@ for recipes, @"blueprint.dhall"@ for blueprints.+--+-- A 'ProjectOrigin' resolves against the project root and nowhere else. If+-- the recorded directory is absent the repository is incomplete, and+-- quietly substituting a globally installed artifact of the same name would+-- be exactly the invisible substitution the portable manifest exists to+-- prevent.+--+-- A 'RemoteOrigin' or 'LocalOrigin' resolves by name through @searchPaths@+-- in the ordinary discovery order, so a developer who deliberately shadows+-- an installed module with a project-local copy keeps that shadowing.+-- Whether what was found actually matches the recorded origin is a separate+-- question, answered by+-- docs\/plans\/78-refuse-accidental-module-downgrades-and-origin-mismatches.md.+resolveArtifactOrigin ::+ FilePath ->+ [FilePath] ->+ FilePath ->+ ArtifactOrigin ->+ IO (Either ArtifactRefError FilePath)+resolveArtifactOrigin projectRoot searchPaths definitionFile origin = case origin of+ ProjectOrigin relative -> do+ let candidate = projectRoot </> fromPortablePath relative+ present <- hasDefinition candidate+ pure $+ if present+ then Right candidate+ else Left (ProjectArtifactMissing origin candidate)+ RemoteOrigin _ artifact _ -> searchByName artifact+ LocalOrigin artifact -> searchByName artifact+ where+ searchByName artifact = do+ let candidates = [dir </> T.unpack artifact | dir <- searchPaths]+ found <- firstPresent candidates+ pure (maybe (Left (ArtifactNotFoundLocally origin candidates)) Right found)++ firstPresent [] = pure Nothing+ firstPresent (candidate : rest) = do+ present <- hasDefinition candidate+ if present then pure (Just candidate) else firstPresent rest++ hasDefinition directory = doesFileExist (directory </> definitionFile)++-- | The manifest stores project-relative paths with forward slashes so a+-- manifest written on Windows matches one written on POSIX. Turn one back+-- into a native path.+fromPortablePath :: FilePath -> FilePath+fromPortablePath = joinPath . filter (not . null) . splitOnSlash+ where+ splitOnSlash path = case break (== '/') path of+ (segment, []) -> [segment]+ (segment, _ : rest) -> segment : splitOnSlash rest++-- | Render a resolution failure as the multi-line message the user sees.+--+-- Callers prepend their own one-line context ("cannot plan a migration",+-- "cannot regenerate"); the body below is identical everywhere so a reader+-- who has seen it once recognises it.+renderArtifactRefError :: ArtifactRefError -> Text+renderArtifactRefError (ProjectArtifactMissing origin candidate) =+ T.intercalate+ "\n"+ [ "Artifact '" <> artifactOriginLabel origin <> "' is recorded in .seihou/manifest.json",+ "as living inside this project, but the directory is missing.",+ "",+ " Expected at: " <> T.pack candidate,+ "",+ "This directory should be committed alongside the manifest. Restore it",+ "from version control, or re-run the module that creates it."+ ]+renderArtifactRefError (ArtifactNotFoundLocally origin candidates) =+ T.intercalate "\n" (header <> [""] <> recordedOrigin <> searched <> [""] <> remedy)+ where+ label = artifactOriginLabel origin++ header =+ [ "Artifact '" <> label <> "' is recorded in .seihou/manifest.json but is not",+ "installed on this machine."+ ]++ recordedOrigin = case origin of+ RemoteOrigin url _ _ -> [" Recorded origin: " <> url, ""]+ _ -> []++ searched = " Searched:" : [" " <> T.pack candidate | candidate <- candidates]++ remedy = case origin of+ RemoteOrigin url _ _ ->+ [ " Install it with:",+ " seihou install " <> url+ ]+ _ ->+ [ " This artifact has no recorded upstream, so seihou cannot fetch it.",+ " Place a copy in one of the directories above."+ ]++-- | The name to show a user for an origin.+artifactOriginLabel :: ArtifactOrigin -> Text+artifactOriginLabel (RemoteOrigin _ artifact _) = artifact+artifactOriginLabel (LocalOrigin artifact) = artifact+artifactOriginLabel (ProjectOrigin relative) = T.pack relative
src/Seihou/Core/Blueprint.hs view
@@ -12,9 +12,11 @@ checkBlueprintTags, checkBlueprintAllowedTools, checkBlueprintMigrations,+ checkBlueprintLaunch, ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -43,6 +45,11 @@ -- 7. Every @files@ entry exists at @baseDir/files/SRC@. -- 8. Every tag is non-empty. -- 9. Every @allowedTools@ entry, when set, is non-empty.+-- 10. Every migration is a forward dotted-numeric edge with a non-empty+-- prompt, and each starting version occurs at most once.+-- 11. Every field the @launch@ record does set is non-blank. The values+-- themselves are parsed by the CLI, which owns the provider and effort+-- vocabularies. validateBlueprint :: FilePath -> Blueprint -> IO (Either ModuleLoadError Blueprint) validateBlueprint baseDir b = do searchPaths <- defaultSearchPaths@@ -69,23 +76,24 @@ <> checkBlueprintTags b <> checkBlueprintAllowedTools b <> checkBlueprintMigrations b+ <> checkBlueprintLaunch b allErrs = pureErrs <> fileErrs <> baseErrs pure $ if null allErrs then Right b- else Left (ValidationError b.name allErrs)+ else Left (ValidationError (b ^. #name) allErrs) -- Rule 1: blueprint name must match [a-z][a-z0-9-]* checkBlueprintNameFormat :: Blueprint -> [Text] checkBlueprintNameFormat b =- let n = b.name.unModuleName+ let n = (b ^. #name . #unModuleName) in if T.null n || not (isValidModuleName n) then ["blueprint name must match [a-z][a-z0-9-]*, got: " <> n] else [] -- Rule 2: if a version is given it must not be empty checkBlueprintVersionPresent :: Blueprint -> [Text]-checkBlueprintVersionPresent b = case b.version of+checkBlueprintVersionPresent b = case b ^. #version of Nothing -> [] Just v | T.null (T.strip v) -> ["blueprint version, if specified, must not be empty"]@@ -94,13 +102,13 @@ -- Rule 3: prompt body must not be empty after trimming checkBlueprintPromptNonEmpty :: Blueprint -> [Text] checkBlueprintPromptNonEmpty b- | T.null (T.strip b.prompt) = ["blueprint prompt must not be empty"]+ | T.null (T.strip (b ^. #prompt)) = ["blueprint prompt must not be empty"] | otherwise = [] -- Rule 4: declared variable names must be unique checkBlueprintUniqueVars :: Blueprint -> [Text] checkBlueprintUniqueVars b =- let names = map (\d -> d.name.unVarName) b.vars+ let names = map (\d -> d ^. #name . #unVarName) (b ^. #vars) in map (\n -> "duplicate variable name: " <> n) (findDupes Set.empty Set.empty names) findDupes :: Set.Set Text -> Set.Set Text -> [Text] -> [Text]@@ -112,14 +120,14 @@ -- Rule 5: every prompt references a declared variable checkBlueprintPromptRefs :: Blueprint -> [Text] checkBlueprintPromptRefs b =- let varNames = Set.fromList (map (.name) b.vars)+ let varNames = Set.fromList (map (^. #name) (b ^. #vars)) in concatMap ( \p ->- if Set.member p.var varNames+ if Set.member (p ^. #var) varNames then []- else ["prompt references undeclared variable: " <> p.var.unVarName]+ else ["prompt references undeclared variable: " <> p ^. #var . #unVarName] )- b.prompts+ (b ^. #prompts) -- Rule 6: base modules must be well-formed and resolve to a module or -- recipe (not another blueprint). The check uses the same default@@ -132,25 +140,25 @@ checkBlueprintBaseModulesWith :: [FilePath] -> Blueprint -> IO [Text] checkBlueprintBaseModulesWith searchPaths b =- concat <$> mapM (checkOne searchPaths) b.baseModules+ concat <$> mapM (checkOne searchPaths) (b ^. #baseModules) where checkOne :: [FilePath] -> Dependency -> IO [Text] checkOne paths dep = do- let n = dep.depModule.unModuleName+ let n = (dep ^. #module_ . #unModuleName) nameErrs = [ "invalid baseModule name: " <> n | not (isValidModuleName n) ] bindingErrs = [ "baseModule '" <> n <> "' has invalid var binding name: " <> vn- | (VarName vn) <- Map.keys dep.depVars,+ | (VarName vn) <- Map.keys (dep ^. #vars), not (isValidVarBindingName vn) ] resolveErrs <- if not (isValidModuleName n) then pure [] else do- result <- discoverRunnable paths dep.depModule+ result <- discoverRunnable paths (dep ^. #module_) pure $ case result of Right (RunnableModule _ _) -> [] Right (RunnableRecipe _ _) -> []@@ -181,26 +189,26 @@ concat <$> mapM ( \bf -> do- let p = baseDir </> "files" </> bf.src+ let p = baseDir </> "files" </> (bf ^. #src) exists <- doesFileExist p pure $ if exists then []- else ["blueprint file not found: " <> T.pack bf.src]+ else ["blueprint file not found: " <> T.pack (bf ^. #src)] )- b.files+ (b ^. #files) -- Rule 8: tags must not be empty strings checkBlueprintTags :: Blueprint -> [Text] checkBlueprintTags b = [ "tag must not be empty"- | t <- b.tags,+ | t <- b ^. #tags, T.null (T.strip t) ] -- Rule 9: @allowedTools@, when set, must contain only non-empty entries checkBlueprintAllowedTools :: Blueprint -> [Text]-checkBlueprintAllowedTools b = case b.allowedTools of+checkBlueprintAllowedTools b = case b ^. #allowedTools of Nothing -> [] Just xs -> [ "allowedTools entry must not be empty"@@ -212,23 +220,23 @@ -- non-empty prompt, and each starting version occurs at most once. checkBlueprintMigrations :: Blueprint -> [Text] checkBlueprintMigrations b =- concatMap checkOne b.migrations <> duplicateErrors+ concatMap checkOne (b ^. #migrations) <> duplicateErrors where checkOne :: BlueprintMigration -> [Text] checkOne migration = promptErrors migration- <> versionErrors "from" migration.from- <> versionErrors "to" migration.to+ <> versionErrors "from" (migration ^. #from)+ <> versionErrors "to" (migration ^. #to) <> orderErrors migration promptErrors :: BlueprintMigration -> [Text] promptErrors migration = [ "blueprint migration "- <> migration.from+ <> migration ^. #from <> " -> "- <> migration.to+ <> migration ^. #to <> " prompt must not be empty"- | T.null (T.strip migration.prompt)+ | T.null (T.strip (migration ^. #prompt)) ] versionErrors label versionText = case parseVersion versionText of@@ -236,17 +244,35 @@ Just _ -> [] orderErrors :: BlueprintMigration -> [Text]- orderErrors migration = case (parseVersion migration.from, parseVersion migration.to) of+ orderErrors migration = case (parseVersion (migration ^. #from), parseVersion (migration ^. #to)) of (Just fromVersion, Just toVersion) | fromVersion >= toVersion -> [ "blueprint migration must advance versions: "- <> migration.from+ <> migration ^. #from <> " -> "- <> migration.to+ <> migration ^. #to ] _ -> [] duplicateErrors = map ("duplicate blueprint migration from version: " <>)- (findDupes Set.empty Set.empty (map (.from) b.migrations))+ (findDupes Set.empty Set.empty (map (^. #from) (b ^. #migrations)))++-- Rule 11: every field the @launch@ record does set must be non-blank. The+-- declared values themselves (which provider, which effort) are parsed by the+-- CLI layer, which owns those vocabularies; core only rejects blanks.+checkBlueprintLaunch :: Blueprint -> [Text]+checkBlueprintLaunch b = case b ^. #launch of+ Nothing -> []+ Just l ->+ blankErr "provider" (l ^. #provider)+ <> blankErr "model" (l ^. #model)+ <> blankErr "effort" (l ^. #effort)+ <> blankErr "mode" (l ^. #mode)+ where+ blankErr key value =+ [ "launch." <> key <> ", if specified, must not be empty"+ | Just v <- [value],+ T.null (T.strip v)+ ]
src/Seihou/Core/CommandFingerprint.hs view
@@ -3,6 +3,8 @@ ) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Types import Seihou.Manifest.Hash (hashContent)@@ -15,7 +17,7 @@ Just . CommandFingerprint . hashContent $ T.intercalate "\n"- [ "module=" <> moduleName.unModuleName,+ [ "module=" <> moduleName ^. #unModuleName, "command=" <> command, "work-dir=" <> T.pack (normalise (maybe "." id workDir)), "occurrence=" <> T.pack (show occurrence)
src/Seihou/Core/CommandVar.hs view
@@ -5,6 +5,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Expr (evalExpr)@@ -24,18 +25,18 @@ conditionBindings = resolvedValues existing <> bindings shouldRun cv =- not (Map.member cv.name existing)- && maybe True (evalExpr conditionBindings) cv.condition+ not (Map.member (cv ^. #name) existing)+ && maybe True (evalExpr conditionBindings) (cv ^. #condition) -- | Return the matching declaration for a command variable, or synthesize a -- text declaration for prompt-only dynamic context such as @git.branch@. commandVarDecl :: [VarDecl] -> CommandVar -> VarDecl commandVarDecl decls cv =- case filter (\decl -> decl.name == cv.name) decls of+ case filter (\decl -> decl ^. #name == cv ^. #name) decls of decl : _ -> decl [] -> VarDecl- { name = cv.name,+ { name = cv ^. #name, type_ = VTText, default_ = Nothing, description = Nothing,@@ -63,15 +64,15 @@ where go errs resolved _bindings [] = pure (reverse errs, resolved) go errs resolved bindings (cv : rest)- | Map.member cv.name resolved = go errs resolved bindings rest- | maybe False (not . evalExpr bindings) cv.condition = go errs resolved bindings rest+ | Map.member (cv ^. #name) resolved = go errs resolved bindings rest+ | maybe False (not . evalExpr bindings) (cv ^. #condition) = go errs resolved bindings rest | otherwise = do result <- resolveOne bindings cv case result of Left err -> go (err : errs) resolved bindings rest Right rv ->- let resolved' = Map.insert cv.name rv resolved- bindings' = Map.insert cv.name rv.value bindings+ let resolved' = Map.insert (cv ^. #name) rv resolved+ bindings' = Map.insert (cv ^. #name) (rv ^. #value) bindings in go errs resolved' bindings' rest resolveOne _bindings cv = do@@ -79,13 +80,13 @@ case validateWorkDir cv of Left err -> pure (Left err) Right workDir -> do- (exitCode, stdoutText, stderrText) <- runProcess "sh" ["-c", cv.run] workDir+ (exitCode, stdoutText, stderrText) <- runProcess "sh" ["-c", cv ^. #run] workDir pure $ case exitCode of ExitSuccess -> coerceCommandOutput decl cv stdoutText ExitFailure code -> Left $ ValidationFailed- cv.name+ (cv ^. #name) ( "command failed with exit code " <> T.pack (show code) <> ": "@@ -96,31 +97,31 @@ validateWorkDir cv@CommandVar {workDir = Nothing} = Right Nothing validateWorkDir cv@CommandVar {workDir = Just wd} = case validateProjectRelativePath wd of- Left err -> Left (ValidationFailed cv.name ("command variable workDir " <> err))+ Left err -> Left (ValidationFailed (cv ^. #name) ("command variable workDir " <> err)) Right _ -> Right (Just (T.unpack wd)) coerceCommandOutput :: VarDecl -> CommandVar -> Text -> Either VarError ResolvedVar coerceCommandOutput decl cv stdoutText = do let output =- if cv.trim+ if cv ^. #trim then T.strip stdoutText else stdoutText- case cv.maxBytes of+ case cv ^. #maxBytes of Just n | fromIntegral (T.length output) > n ->- Left (ValidationFailed cv.name ("command output exceeds maxBytes " <> T.pack (show n)))+ Left (ValidationFailed (cv ^. #name) ("command output exceeds maxBytes " <> T.pack (show n))) _ -> do- value <- coerceValue decl.name decl.type_ output+ value <- coerceValue (decl ^. #name) (decl ^. #type_) output validateVarValue decl value Right ResolvedVar { value = value,- source = FromCommand cv.run,+ source = FromCommand (cv ^. #run), decl = decl } resolvedValues :: Map VarName ResolvedVar -> Map VarName VarValue-resolvedValues = Map.map (.value)+resolvedValues = Map.map (^. #value) summarizeDiagnostic :: Text -> Text summarizeDiagnostic t =
src/Seihou/Core/Migration.hs view
@@ -13,8 +13,8 @@ ) where +import Data.Generics.Labels () import Data.List (sortOn)-import GHC.Generics (Generic) import Seihou.Core.Version (Version, parseVersion) import Seihou.Prelude @@ -34,19 +34,19 @@ -- by this op; if the command moves files, the migration author is -- responsible for following it with explicit move/delete ops. data MigrationOp- = MoveFile {src :: FilePath, dest :: FilePath}- | MoveDir {src :: FilePath, dest :: FilePath}- | DeleteFile {path :: FilePath}- | DeleteDir {path :: FilePath}- | RunCommand {run :: Text, workDir :: Maybe FilePath}+ = MoveFile {src :: !FilePath, dest :: !FilePath}+ | MoveDir {src :: !FilePath, dest :: !FilePath}+ | DeleteFile {path :: !FilePath}+ | DeleteDir {path :: !FilePath}+ | RunCommand {run :: !Text, workDir :: !(Maybe FilePath)} deriving stock (Eq, Show, Generic) -- | A migration that moves a project from module version @from@ to module -- version @to@. The 'ops' list is applied in declaration order. data Migration = Migration- { from :: Text,- to :: Text,- ops :: [MigrationOp]+ { from :: !Text,+ to :: !Text,+ ops :: ![MigrationOp] } deriving stock (Eq, Show, Generic) @@ -54,9 +54,9 @@ -- version strings use the same dotted-numeric format as module migrations, -- while 'prompt' describes only the changes needed for this edge. data BlueprintMigration = BlueprintMigration- { from :: Text,- to :: Text,- prompt :: Text+ { from :: !Text,+ to :: !Text,+ prompt :: !Text } deriving stock (Eq, Show, Generic) @@ -85,20 +85,20 @@ -- -- The plan carries the module name for rendering, the start and end -- versions of the user-visible "X → Y" header, and the ordered list of--- migrations that will run. A plan with @planSteps == []@ means the--- manifest will advance from @planFrom@ to @planTo@ without running+-- migrations that will run. A plan with @steps == []@ means the+-- manifest will advance from @from@ to @to@ without running -- any migration ops (a pure version bump). data MigrationPlan = MigrationPlan- { planModule :: Text,+ { module_ :: !Text, -- | Installed (manifest) version at the start.- planFrom :: Version,+ from :: !Version, -- | Target version. The manifest will land here after the plan -- runs, regardless of whether any of the declared migrations- -- bridge every gap inside @[planFrom, planTo]@.- planTo :: Version,+ -- bridge every gap inside @[from, to]@.+ to :: !Version, -- | The migrations that actually apply, in ascending @from@ -- order. May be empty.- planSteps :: [Migration]+ steps :: ![Migration] } deriving stock (Eq, Show, Generic) @@ -106,10 +106,10 @@ -- window. A non-trivial window may have no selected steps when the author -- declared no agent intervention for that range. data BlueprintMigrationPlan = BlueprintMigrationPlan- { blueprintPlanName :: Text,- blueprintPlanFrom :: Version,- blueprintPlanTo :: Version,- blueprintPlanSteps :: [BlueprintMigration]+ { name :: !Text,+ from :: !Version,+ to :: !Version,+ steps :: ![BlueprintMigration] } deriving stock (Eq, Show, Generic) @@ -168,14 +168,14 @@ ( fmap ( \steps -> MigrationPlan- { planModule = modName,- planFrom = installed,- planTo = target,- planSteps = steps+ { module_ = modName,+ from = installed,+ to = target,+ steps = steps } ) )- (planMigrationWindow (.from) (.to) migrations installed target)+ (planMigrationWindow (^. #from) (^. #to) migrations installed target) -- | Compute the ordered agent-guided migrations for a blueprint and version -- window. Selection and errors deliberately match 'planMigrationChain'.@@ -190,14 +190,14 @@ ( fmap ( \steps -> BlueprintMigrationPlan- { blueprintPlanName = blueprintName,- blueprintPlanFrom = current,- blueprintPlanTo = target,- blueprintPlanSteps = steps+ { name = blueprintName,+ from = current,+ to = target,+ steps = steps } ) )- (planMigrationWindow (.from) (.to) migrations current target)+ (planMigrationWindow (^. #from) (^. #to) migrations current target) -- | Shared gap-tolerant version-window planner. Keeping parsing, duplicate -- detection, ordering, overlap handling, and overshoot handling here prevents
src/Seihou/Core/Module.hs view
@@ -31,10 +31,10 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T-import GHC.Generics (Generic) import Seihou.Core.Path (validateProjectRelativePath) import Seihou.Core.Types import Seihou.Dhall.Eval (evalAgentPromptFromFile, evalBlueprintFromFile, evalModuleFromFile, evalRecipeFromFile)@@ -47,7 +47,7 @@ discoverModule :: [FilePath] -> ModuleName -> IO (Either ModuleLoadError FilePath) discoverModule searchPaths name = go searchPaths where- nameStr = T.unpack name.unModuleName+ nameStr = T.unpack (name ^. #unModuleName) go [] = pure $ Left (ModuleNotFound name searchPaths) go (dir : rest) = do let candidate = dir </> nameStr@@ -70,7 +70,7 @@ discoverRunnable :: [FilePath] -> ModuleName -> IO (Either ModuleLoadError Runnable) discoverRunnable searchPaths name = go searchPaths where- nameStr = T.unpack name.unModuleName+ nameStr = T.unpack (name ^. #unModuleName) go [] = pure $ Left (ModuleNotFound name searchPaths) go (dir : rest) = do let candidate = dir </> nameStr@@ -119,7 +119,7 @@ discoverBlueprint :: [FilePath] -> ModuleName -> IO (Either ModuleLoadError FilePath) discoverBlueprint searchPaths name = go searchPaths where- nameStr = T.unpack name.unModuleName+ nameStr = T.unpack (name ^. #unModuleName) go [] = pure $ Left (ModuleNotFound name searchPaths) go (dir : rest) = do let candidate = dir </> nameStr@@ -135,7 +135,7 @@ discoverAgentPrompt :: [FilePath] -> ModuleName -> IO (Either ModuleLoadError FilePath) discoverAgentPrompt searchPaths name = go searchPaths where- nameStr = T.unpack name.unModuleName+ nameStr = T.unpack (name ^. #unModuleName) go [] = pure $ Left (ModuleNotFound name searchPaths) go (dir : rest) = do let candidate = dir </> nameStr@@ -181,12 +181,12 @@ pure $ if null allErrors then Right m- else Left (ValidationError m.name allErrors)+ else Left (ValidationError (m ^. #name) allErrors) -- Rule 1: Module name must be non-empty and match [a-z][a-z0-9-]* checkNameFormat :: Module -> [Text] checkNameFormat m =- let n = m.name.unModuleName+ let n = (m ^. #name . #unModuleName) in if T.null n || not (isValidModuleName n) then ["module name must match [a-z][a-z0-9-]*, got: " <> n] else []@@ -200,7 +200,7 @@ -- Rule 1b: Module must declare a version checkVersionPresent :: Module -> [Text]-checkVersionPresent m = case m.version of+checkVersionPresent m = case m ^. #version of Nothing -> ["module must declare a version"] Just v | T.null (T.strip v) -> ["module must declare a version"]@@ -209,7 +209,7 @@ -- Rule 2: All variable names must be unique checkUniqueVars :: Module -> [Text] checkUniqueVars m =- let names = map (\d -> d.name.unVarName) m.vars+ let names = map (\d -> d ^. #name . #unVarName) (m ^. #vars) in map (\n -> "duplicate variable name: " <> n) (findDupes Set.empty Set.empty names) findDupes :: Set.Set Text -> Set.Set Text -> [Text] -> [Text]@@ -221,14 +221,14 @@ -- Rule 3: Every prompt must reference a declared variable checkPromptRefs :: Module -> [Text] checkPromptRefs m =- let varNames = Set.fromList (map (.name) m.vars)+ let varNames = Set.fromList (map (^. #name) (m ^. #vars)) in concatMap ( \p ->- if Set.member p.var varNames+ if Set.member (p ^. #var) varNames then []- else ["prompt references undeclared variable: " <> p.var.unVarName]+ else ["prompt references undeclared variable: " <> p ^. #var . #unVarName] )- m.prompts+ (m ^. #prompts) -- Rule 4: Every step source file must exist in the module's files/ directory checkFileExistence :: FilePath -> Module -> IO [Text]@@ -236,38 +236,38 @@ concat <$> mapM ( \s -> do- let p = baseDir </> "files" </> s.src+ let p = baseDir </> "files" </> (s ^. #src) exists <- doesFileExist p pure $ if exists then []- else ["step source file not found: " <> T.pack s.src]+ else ["step source file not found: " <> T.pack (s ^. #src)] )- m.steps+ (m ^. #steps) -- Rule 5: Every export must reference a declared variable checkExportRefs :: Module -> [Text] checkExportRefs m =- let varNames = Set.fromList (map (.name) m.vars)+ let varNames = Set.fromList (map (^. #name) (m ^. #vars)) in concatMap ( \e ->- if Set.member e.var varNames+ if Set.member (e ^. #var) varNames then []- else ["export references undeclared variable: " <> e.var.unVarName]+ else ["export references undeclared variable: " <> e ^. #var . #unVarName] )- m.exports+ (m ^. #exports) -- Rule 6: Every dependency name must be well-formed checkDependencyNames :: Module -> [Text] checkDependencyNames m = concatMap ( \dep ->- let n = dep.depModule.unModuleName+ let n = (dep ^. #module_ . #unModuleName) in if isValidModuleName n then [] else ["invalid dependency name: " <> n] )- m.dependencies+ (m ^. #dependencies) -- Rule 6b: Dependency var binding names must be non-empty checkDependencyVarBindings :: Module -> [Text]@@ -277,42 +277,42 @@ concatMap ( \(VarName vn) -> if T.null vn- then ["dependency '" <> dep.depModule.unModuleName <> "' has empty var binding name"]+ then ["dependency '" <> dep ^. #module_ . #unModuleName <> "' has empty var binding name"] else [] )- (Map.keys dep.depVars)+ (Map.keys (dep ^. #vars)) )- m.dependencies+ (m ^. #dependencies) -- Rule 7: Every step destination must be a safe relative path checkSafeDestinations :: Module -> [Text]-checkSafeDestinations m = concatMap checkDest m.steps+checkSafeDestinations m = concatMap checkDest (m ^. #steps) where checkDest s =- case validateProjectRelativePath s.dest of+ case validateProjectRelativePath (s ^. #dest) of Left err -> ["step destination " <> err] Right _ -> [] -- Rule 8: Variables referenced in step dest placeholders must be declared checkDestVarRefs :: Module -> [Text] checkDestVarRefs m =- let varNames = Set.fromList (map (\d -> d.name.unVarName) m.vars)- in concatMap (checkStep varNames) m.steps+ let varNames = Set.fromList (map (\d -> d ^. #name . #unVarName) (m ^. #vars))+ in concatMap (checkStep varNames) (m ^. #steps) where checkStep varNames s = [ "step destination references undeclared variable: " <> ref- | ref <- extractPlaceholders s.dest,+ | ref <- extractPlaceholders (s ^. #dest), not (Set.member ref varNames) ] -- Rule 9: Command text must be non-empty and workDir must be safe checkCommandSafety :: Module -> [Text]-checkCommandSafety m = concatMap checkCmd m.commands+checkCommandSafety m = concatMap checkCmd (m ^. #commands) where- checkCmd c = checkEmptyRun c <> checkWorkDir c.workDir+ checkCmd c = checkEmptyRun c <> checkWorkDir (c ^. #workDir) checkEmptyRun c- | T.null (T.strip c.run) = ["command text must not be empty"]+ | T.null (T.strip (c ^. #run)) = ["command text must not be empty"] | otherwise = [] checkWorkDir Nothing = []@@ -352,11 +352,11 @@ -- | A module discovered during enumeration, with its load result and source. data DiscoveredModule = DiscoveredModule- { discoveredResult :: Either ModuleLoadError Module,- discoveredSource :: ModuleSource,- discoveredDir :: FilePath+ { result :: !(Either ModuleLoadError Module),+ source :: !ModuleSource,+ dir :: !FilePath }- deriving stock (Show)+ deriving stock (Generic, Show) -- | Enumerate all modules across the given search paths. -- The search paths must be in the same order as 'defaultSearchPaths':@@ -399,7 +399,7 @@ result <- case decoded of Left err -> pure (Left err) Right m -> validateModule moduleDir m- pure DiscoveredModule {discoveredResult = result, discoveredSource = src, discoveredDir = moduleDir}+ pure DiscoveredModule {result = result, source = src, dir = moduleDir} -- | Whether a discovered item is a module, recipe, blueprint, or prompt. data RunnableKind = KindModule | KindRecipe | KindBlueprint | KindPrompt@@ -407,15 +407,15 @@ -- | A runnable discovered during enumeration, with its load result, kind, and source. data DiscoveredRunnable = DiscoveredRunnable- { drName :: Text,- drDescription :: Maybe Text,- drKind :: RunnableKind,- drSource :: ModuleSource,- drDir :: FilePath,- drIsError :: Bool,- drError :: Maybe Text+ { name :: !Text,+ description :: !(Maybe Text),+ kind :: !RunnableKind,+ source :: !ModuleSource,+ dir :: !FilePath,+ isError :: !Bool,+ error :: !(Maybe Text) }- deriving stock (Show)+ deriving stock (Generic, Show) -- | Enumerate all modules, recipes, blueprints, and prompts across the given search paths. -- Returns a unified list of discovered items, each tagged with its kind.@@ -453,23 +453,23 @@ [ case decoded of Left err -> DiscoveredRunnable- { drName = T.pack entry,- drDescription = Nothing,- drKind = KindModule,- drSource = src,- drDir = entryDir,- drIsError = True,- drError = Just (briefLoadError err)+ { name = T.pack entry,+ description = Nothing,+ kind = KindModule,+ source = src,+ dir = entryDir,+ isError = True,+ error = Just (briefLoadError err) } Right m -> DiscoveredRunnable- { drName = m.name.unModuleName,- drDescription = m.description,- drKind = KindModule,- drSource = src,- drDir = entryDir,- drIsError = False,- drError = Nothing+ { name = m ^. #name . #unModuleName,+ description = m ^. #description,+ kind = KindModule,+ source = src,+ dir = entryDir,+ isError = False,+ error = Nothing } ] else@@ -480,23 +480,23 @@ [ case decoded of Left err -> DiscoveredRunnable- { drName = T.pack entry,- drDescription = Nothing,- drKind = KindRecipe,- drSource = src,- drDir = entryDir,- drIsError = True,- drError = Just (briefLoadError err)+ { name = T.pack entry,+ description = Nothing,+ kind = KindRecipe,+ source = src,+ dir = entryDir,+ isError = True,+ error = Just (briefLoadError err) } Right r -> DiscoveredRunnable- { drName = r.name.unRecipeName,- drDescription = r.description,- drKind = KindRecipe,- drSource = src,- drDir = entryDir,- drIsError = False,- drError = Nothing+ { name = r ^. #name . #unRecipeName,+ description = r ^. #description,+ kind = KindRecipe,+ source = src,+ dir = entryDir,+ isError = False,+ error = Nothing } ] else@@ -507,23 +507,23 @@ [ case decoded of Left err -> DiscoveredRunnable- { drName = T.pack entry,- drDescription = Nothing,- drKind = KindBlueprint,- drSource = src,- drDir = entryDir,- drIsError = True,- drError = Just (briefLoadError err)+ { name = T.pack entry,+ description = Nothing,+ kind = KindBlueprint,+ source = src,+ dir = entryDir,+ isError = True,+ error = Just (briefLoadError err) } Right b -> DiscoveredRunnable- { drName = b.name.unModuleName,- drDescription = b.description,- drKind = KindBlueprint,- drSource = src,- drDir = entryDir,- drIsError = False,- drError = Nothing+ { name = b ^. #name . #unModuleName,+ description = b ^. #description,+ kind = KindBlueprint,+ source = src,+ dir = entryDir,+ isError = False,+ error = Nothing } ] else@@ -534,23 +534,23 @@ [ case decoded of Left err -> DiscoveredRunnable- { drName = T.pack entry,- drDescription = Nothing,- drKind = KindPrompt,- drSource = src,- drDir = entryDir,- drIsError = True,- drError = Just (briefLoadError err)+ { name = T.pack entry,+ description = Nothing,+ kind = KindPrompt,+ source = src,+ dir = entryDir,+ isError = True,+ error = Just (briefLoadError err) } Right p -> DiscoveredRunnable- { drName = p.name.unModuleName,- drDescription = p.description,- drKind = KindPrompt,- drSource = src,- drDir = entryDir,- drIsError = False,- drError = Nothing+ { name = p ^. #name . #unModuleName,+ description = p ^. #description,+ kind = KindPrompt,+ source = src,+ dir = entryDir,+ isError = False,+ error = Nothing } ] else pure []
src/Seihou/Core/Recipe.hs view
@@ -3,6 +3,8 @@ ) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -33,7 +35,7 @@ -- Rule 1: Recipe name must match [a-z][a-z0-9-]* checkRecipeNameFormat :: Recipe -> [Text] checkRecipeNameFormat recipe =- let n = recipe.name.unRecipeName+ let n = (recipe ^. #name . #unRecipeName) in if T.null n || not (isValidModuleName n) then ["recipe name must match [a-z][a-z0-9-]*, got: " <> n] else []@@ -41,13 +43,13 @@ -- Rule 2: At least one module must be listed checkNonEmptyModules :: Recipe -> [Text] checkNonEmptyModules recipe- | null recipe.modules = ["recipe must list at least one module"]+ | null (recipe ^. #modules) = ["recipe must list at least one module"] | otherwise = [] -- Rule 3: No duplicate module names checkNoDuplicateModules :: Recipe -> [Text] checkNoDuplicateModules recipe =- let names = map (.depModule.unModuleName) recipe.modules+ let names = map (^. #module_ . #unModuleName) (recipe ^. #modules) in map (\n -> "duplicate module in recipe: " <> n) (findDupes Set.empty Set.empty names) findDupes :: Set.Set Text -> Set.Set Text -> [Text] -> [Text]@@ -59,16 +61,16 @@ -- Rule 4: Variable binding names must match [a-z][a-z0-9.-]* checkVarBindingNames :: Recipe -> [Text] checkVarBindingNames recipe =- concatMap checkDep recipe.modules+ concatMap checkDep (recipe ^. #modules) where checkDep dep = concatMap ( \(VarName vn) -> if isValidVarBindingName vn then []- else ["invalid var binding name '" <> vn <> "' in module '" <> dep.depModule.unModuleName <> "'"]+ else ["invalid var binding name '" <> vn <> "' in module '" <> dep ^. #module_ . #unModuleName <> "'"] )- (Map.keys dep.depVars)+ (Map.keys (dep ^. #vars)) isValidVarBindingName :: Text -> Bool isValidVarBindingName t = case T.uncons t of
src/Seihou/Core/Registry.hs view
@@ -19,31 +19,31 @@ ) where +import Data.Generics.Labels () import Data.Text qualified as T-import GHC.Generics (Generic) import Seihou.Core.Types (ModuleLoadError, ModuleName (..)) import Seihou.Prelude import System.Directory (doesFileExist) -- | A single module listing within a registry. data RegistryEntry = RegistryEntry- { name :: ModuleName,- version :: Maybe Text,- path :: FilePath,- description :: Maybe Text,- tags :: [Text]+ { name :: !ModuleName,+ version :: !(Maybe Text),+ path :: !FilePath,+ description :: !(Maybe Text),+ tags :: ![Text] } deriving stock (Eq, Show, Generic) -- | Registry metadata for a multi-module repository. -- Declared in @seihou-registry.dhall@ at the repo root. data Registry = Registry- { repoName :: Text,- repoDescription :: Maybe Text,- modules :: [RegistryEntry],- recipes :: [RegistryEntry],- blueprints :: [RegistryEntry],- prompts :: [RegistryEntry]+ { repoName :: !Text,+ repoDescription :: !(Maybe Text),+ modules :: ![RegistryEntry],+ recipes :: ![RegistryEntry],+ blueprints :: ![RegistryEntry],+ prompts :: ![RegistryEntry] } deriving stock (Eq, Show, Generic) @@ -114,20 +114,20 @@ -- and no name collisions between modules, recipes, blueprints, and prompts. validateRegistry :: FilePath -> Registry -> IO [Text] validateRegistry repoRoot reg = do- modErrs <- concat <$> mapM (validateModuleEntry repoRoot) reg.modules- recErrs <- concat <$> mapM (validateRecipeEntry repoRoot) reg.recipes- bpErrs <- concat <$> mapM (validateBlueprintEntry repoRoot) reg.blueprints- promptErrs <- concat <$> mapM (validatePromptEntry repoRoot) reg.prompts- let collisionErrs = checkNameCollisions reg.modules reg.recipes reg.blueprints reg.prompts+ modErrs <- concat <$> mapM (validateModuleEntry repoRoot) (reg ^. #modules)+ recErrs <- concat <$> mapM (validateRecipeEntry repoRoot) (reg ^. #recipes)+ bpErrs <- concat <$> mapM (validateBlueprintEntry repoRoot) (reg ^. #blueprints)+ promptErrs <- concat <$> mapM (validatePromptEntry repoRoot) (reg ^. #prompts)+ let collisionErrs = checkNameCollisions (reg ^. #modules) (reg ^. #recipes) (reg ^. #blueprints) (reg ^. #prompts) pure (modErrs <> recErrs <> bpErrs <> promptErrs <> collisionErrs) validateModuleEntry :: FilePath -> RegistryEntry -> IO [Text] validateModuleEntry repoRoot entry = do- let nameText = entry.name.unModuleName+ let nameText = (entry ^. #name . #unModuleName) nameErrors = checkName nameText- pathText = T.pack entry.path+ pathText = T.pack (entry ^. #path) pathErrors = checkPath pathText- let moduleDhall = repoRoot </> entry.path </> "module.dhall"+ let moduleDhall = repoRoot </> entry ^. #path </> "module.dhall" fileExists <- doesFileExist moduleDhall let fileErrors = if fileExists@@ -146,11 +146,11 @@ validateRecipeEntry :: FilePath -> RegistryEntry -> IO [Text] validateRecipeEntry repoRoot entry = do- let nameText = entry.name.unModuleName+ let nameText = (entry ^. #name . #unModuleName) nameErrors = checkRecipeName nameText- pathText = T.pack entry.path+ pathText = T.pack (entry ^. #path) pathErrors = checkRecipePath pathText- let recipeDhall = repoRoot </> entry.path </> "recipe.dhall"+ let recipeDhall = repoRoot </> entry ^. #path </> "recipe.dhall" fileExists <- doesFileExist recipeDhall let fileErrors = if fileExists@@ -169,11 +169,11 @@ validateBlueprintEntry :: FilePath -> RegistryEntry -> IO [Text] validateBlueprintEntry repoRoot entry = do- let nameText = entry.name.unModuleName+ let nameText = (entry ^. #name . #unModuleName) nameErrors = checkBlueprintName nameText- pathText = T.pack entry.path+ pathText = T.pack (entry ^. #path) pathErrors = checkBlueprintPath pathText- let blueprintDhall = repoRoot </> entry.path </> "blueprint.dhall"+ let blueprintDhall = repoRoot </> entry ^. #path </> "blueprint.dhall" fileExists <- doesFileExist blueprintDhall let fileErrors = if fileExists@@ -192,11 +192,11 @@ validatePromptEntry :: FilePath -> RegistryEntry -> IO [Text] validatePromptEntry repoRoot entry = do- let nameText = entry.name.unModuleName+ let nameText = (entry ^. #name . #unModuleName) nameErrors = checkPromptName nameText- pathText = T.pack entry.path+ pathText = T.pack (entry ^. #path) pathErrors = checkPromptPath pathText- let promptDhall = repoRoot </> entry.path </> "prompt.dhall"+ let promptDhall = repoRoot </> entry ^. #path </> "prompt.dhall" fileExists <- doesFileExist promptDhall let fileErrors = if fileExists@@ -218,10 +218,10 @@ -- appears in all four kinds produces six messages (one per pair). checkNameCollisions :: [RegistryEntry] -> [RegistryEntry] -> [RegistryEntry] -> [RegistryEntry] -> [Text] checkNameCollisions mods recs bps prompts =- let modNames = map (\e -> e.name.unModuleName) mods- recNames = map (\e -> e.name.unModuleName) recs- bpNames = map (\e -> e.name.unModuleName) bps- promptNames = map (\e -> e.name.unModuleName) prompts+ let modNames = map (\e -> e ^. #name . #unModuleName) mods+ recNames = map (\e -> e ^. #name . #unModuleName) recs+ bpNames = map (\e -> e ^. #name . #unModuleName) bps+ promptNames = map (\e -> e ^. #name . #unModuleName) prompts modRec = [n | n <- modNames, n `elem` recNames] modBp = [n | n <- modNames, n `elem` bpNames] modPrompt = [n | n <- modNames, n `elem` promptNames]@@ -265,11 +265,11 @@ -- | One row of a sync diff, preserving registry order. data SyncDiff = SyncDiff- { diffKind :: EntryKind,- diffName :: ModuleName,- diffOld :: Maybe Text,- diffNew :: Maybe Text,- diffStatus :: SyncStatus+ { kind :: !EntryKind,+ name :: !ModuleName,+ old :: !(Maybe Text),+ new :: !(Maybe Text),+ status :: !SyncStatus } deriving stock (Eq, Show, Generic) @@ -277,8 +277,8 @@ -- and a 'Registry' with each entry's @version@ field updated to the on-disk -- value (except 'SyncOrphan' entries, which are preserved as-is). data SyncReport = SyncReport- { syncDiffs :: [SyncDiff],- syncUpdated :: Registry+ { diffs :: ![SyncDiff],+ updated :: !Registry } deriving stock (Eq, Show, Generic) @@ -295,25 +295,28 @@ SyncReport computeRegistrySync reg lookups = SyncReport- { syncDiffs = moduleDiffs <> recipeDiffs <> blueprintDiffs <> promptDiffs,- syncUpdated =+ { diffs = moduleDiffs <> recipeDiffs <> blueprintDiffs <> promptDiffs,+ updated = reg- { modules = zipWith applyDiff moduleDiffs reg.modules,- recipes = zipWith applyDiff recipeDiffs reg.recipes,- blueprints = zipWith applyDiff blueprintDiffs reg.blueprints,- prompts = zipWith applyDiff promptDiffs reg.prompts- }+ & #modules+ %~ zipWith applyDiff moduleDiffs+ & #recipes+ %~ zipWith applyDiff recipeDiffs+ & #blueprints+ %~ zipWith applyDiff blueprintDiffs+ & #prompts+ %~ zipWith applyDiff promptDiffs } where- moduleDiffs = map (classify ModuleEntry) reg.modules- recipeDiffs = map (classify RecipeEntry) reg.recipes- blueprintDiffs = map (classify BlueprintEntry) reg.blueprints- promptDiffs = map (classify PromptEntry) reg.prompts+ moduleDiffs = map (classify ModuleEntry) (reg ^. #modules)+ recipeDiffs = map (classify RecipeEntry) (reg ^. #recipes)+ blueprintDiffs = map (classify BlueprintEntry) (reg ^. #blueprints)+ promptDiffs = map (classify PromptEntry) (reg ^. #prompts) classify :: EntryKind -> RegistryEntry -> SyncDiff classify kind entry =- let onDisk = lookupOnDisk kind entry.name- status = case (entry.version, onDisk) of+ let onDisk = lookupOnDisk kind (entry ^. #name)+ status = case (entry ^. #version, onDisk) of (_, OnDiskMissing) -> SyncOrphan (Nothing, OnDiskValue Nothing) -> SyncInSync (Nothing, OnDiskValue (Just _)) -> SyncMissing@@ -322,20 +325,20 @@ | old == new -> SyncInSync | otherwise -> SyncStale new newVersion = case onDisk of- OnDiskMissing -> entry.version+ OnDiskMissing -> (entry ^. #version) OnDiskValue v -> v in SyncDiff- { diffKind = kind,- diffName = entry.name,- diffOld = entry.version,- diffNew = newVersion,- diffStatus = status+ { kind = kind,+ name = entry ^. #name,+ old = entry ^. #version,+ new = newVersion,+ status = status } applyDiff :: SyncDiff -> RegistryEntry -> RegistryEntry- applyDiff diff entry = case diff.diffStatus of+ applyDiff diff entry = case diff ^. #status of SyncOrphan -> entry- _ -> entry {version = diff.diffNew}+ _ -> entry & #version .~ diff ^. #new lookupOnDisk :: EntryKind -> ModuleName -> OnDiskVersion lookupOnDisk kind name =@@ -352,28 +355,28 @@ -- 'Nothing' if the entry is already in sync. Used by @seihou browse@ and -- @seihou install@ to surface stale registry versions without blocking. formatDriftWarning :: SyncDiff -> Maybe Text-formatDriftWarning diff = case diff.diffStatus of+formatDriftWarning diff = case diff ^. #status of SyncInSync -> Nothing SyncOrphan -> Nothing SyncMissing -> Just $- kindWord diff.diffKind+ kindWord (diff ^. #kind) <> " '"- <> diff.diffName.unModuleName+ <> diff ^. #name . #unModuleName <> "' registry version is missing; "- <> entryFile diff.diffKind+ <> entryFile (diff ^. #kind) <> " declares "- <> renderVersion diff.diffNew+ <> renderVersion (diff ^. #new) <> " — run `seihou registry sync-versions`" SyncStale newVer -> Just $- kindWord diff.diffKind+ kindWord (diff ^. #kind) <> " '"- <> diff.diffName.unModuleName+ <> diff ^. #name . #unModuleName <> "' registry version "- <> renderVersion diff.diffOld+ <> renderVersion (diff ^. #old) <> " differs from "- <> entryFile diff.diffKind+ <> entryFile (diff ^. #kind) <> " version " <> newVer <> " — run `seihou registry sync-versions`"@@ -400,17 +403,17 @@ -- | Whole-registry validation outcome, carrying every issue plus the -- entry counts used by the human-readable summary line. data RegistryValidationReport = RegistryValidationReport- { reportIssues :: [RegistryValidationIssue],- reportModuleCount :: Int,- reportRecipeCount :: Int,- reportBlueprintCount :: Int,- reportPromptCount :: Int+ { issues :: ![RegistryValidationIssue],+ moduleCount :: !Int,+ recipeCount :: !Int,+ blueprintCount :: !Int,+ promptCount :: !Int } deriving stock (Eq, Show, Generic) -- | True iff the report has at least one issue. reportHasIssues :: RegistryValidationReport -> Bool-reportHasIssues r = not (null r.reportIssues)+reportHasIssues r = not (null (r ^. #issues)) -- | Combine the existing structural checks with version classification. -- The third argument is the same shape 'computeRegistrySync' takes —@@ -426,16 +429,16 @@ let report = computeRegistrySync reg lookups versionIssues = [ VersionMismatch d- | d <- report.syncDiffs,- isVersionDrift d.diffStatus+ | d <- report ^. #diffs,+ isVersionDrift (d ^. #status) ] pure RegistryValidationReport- { reportIssues = map StructuralError structuralErrs <> versionIssues,- reportModuleCount = length reg.modules,- reportRecipeCount = length reg.recipes,- reportBlueprintCount = length reg.blueprints,- reportPromptCount = length reg.prompts+ { issues = map StructuralError structuralErrs <> versionIssues,+ moduleCount = length (reg ^. #modules),+ recipeCount = length (reg ^. #recipes),+ blueprintCount = length (reg ^. #blueprints),+ promptCount = length (reg ^. #prompts) } where isVersionDrift SyncMissing = True@@ -450,14 +453,14 @@ formatValidationIssue :: RegistryValidationIssue -> Text formatValidationIssue (StructuralError msg) = msg formatValidationIssue (VersionMismatch diff) =- validationKindPrefix diff.diffKind- <> diff.diffName.unModuleName+ validationKindPrefix (diff ^. #kind)+ <> diff ^. #name . #unModuleName <> ": registry version "- <> validationRenderVersion diff.diffOld+ <> validationRenderVersion (diff ^. #old) <> " does not match "- <> entryFile diff.diffKind+ <> entryFile (diff ^. #kind) <> " version "- <> validationRenderVersion diff.diffNew+ <> validationRenderVersion (diff ^. #new) where entryFile ModuleEntry = "module.dhall" entryFile RecipeEntry = "recipe.dhall"@@ -480,16 +483,16 @@ renderRegistryDhall :: Registry -> Text renderRegistryDhall reg = T.unlines- [ "{ repoName = " <> renderString reg.repoName,- ", repoDescription = " <> renderOptionalText reg.repoDescription,+ [ "{ repoName = " <> renderString (reg ^. #repoName),+ ", repoDescription = " <> renderOptionalText (reg ^. #repoDescription), ", modules =",- renderEntryList reg.modules,+ renderEntryList (reg ^. #modules), ", recipes =",- renderEntryList reg.recipes,+ renderEntryList (reg ^. #recipes), ", blueprints =",- renderEntryList reg.blueprints,+ renderEntryList (reg ^. #blueprints), ", prompts =",- renderEntryList reg.prompts,+ renderEntryList (reg ^. #prompts), "}" ] @@ -505,11 +508,11 @@ renderEntry isFirst entry = T.intercalate "\n"- [ " " <> opener <> " { name = " <> renderString entry.name.unModuleName,- " , version = " <> renderOptionalText entry.version,- " , path = " <> renderString (T.pack entry.path),- " , description = " <> renderOptionalText entry.description,- " , tags = " <> renderTextList entry.tags,+ [ " " <> opener <> " { name = " <> renderString (entry ^. #name . #unModuleName),+ " , version = " <> renderOptionalText (entry ^. #version),+ " , path = " <> renderString (T.pack (entry ^. #path)),+ " , description = " <> renderOptionalText (entry ^. #description),+ " , tags = " <> renderTextList (entry ^. #tags), " }" ] where
src/Seihou/Core/Scaffold.hs view
@@ -87,6 +87,10 @@ " , files = [] : List S.Blueprint.BlueprintFile.Type", " , migrations = [] : List S.BlueprintMigration.Type", " , tags = [] : List Text",+ " -- Optional: declare the agent this blueprint was written for.",+ " -- These override the invoking user's config files but lose to a",+ " -- --provider / --model / --effort flag or a SEIHOU_AGENT_* variable.",+ " -- , launch = Some S.Launch::{ effort = Some \"max\" }", " }" ]
src/Seihou/Core/Status.hs view
@@ -3,6 +3,7 @@ ) where +import Data.Generics.Labels () import Data.List (sortOn) import Data.Map.Strict qualified as Map import Seihou.Core.Types@@ -16,8 +17,8 @@ -- 'TfsDeleted' if the file no longer exists on disk. computeTrackedFileStatuses :: (Filesystem :> es) => Manifest -> Eff es [TrackedFile] computeTrackedFileStatuses manifest = do- results <- mapM classifyFile (Map.toAscList manifest.files)- pure (sortOn (.path) results)+ results <- mapM classifyFile (Map.toAscList (manifest ^. #files))+ pure (sortOn (^. #path) results) where classifyFile :: (Filesystem :> es') => (FilePath, FileRecord) -> Eff es' TrackedFile classifyFile (path, record) = do@@ -29,13 +30,13 @@ content <- readFileText path let diskHash = hashContent content pure- ( if diskHash == record.hash+ ( if diskHash == record ^. #hash then TfsUnchanged else TfsModified ) pure TrackedFile { path = path,- moduleName = record.moduleName,+ moduleName = record ^. #moduleName, status = status }
src/Seihou/Core/Types.hs view
@@ -28,7 +28,7 @@ Blueprint (..), CommandVar (..), PromptGuidance (..),- AgentPromptLaunch (..),+ AgentLaunch (..), AgentPrompt (..), Runnable (..), recipeNameToModuleName,@@ -36,6 +36,7 @@ ModuleLoadError (..), Manifest (..), ApplicationId (..),+ ArtifactOrigin (..), AppliedTarget (..), BaselineRef (..), CommandFingerprint (..),@@ -67,6 +68,8 @@ ) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict (Map) import Data.Set (Set) import Data.String (IsString)@@ -113,28 +116,28 @@ -- | A variable declaration within a module. data VarDecl = VarDecl- { name :: VarName,- type_ :: VarType,- default_ :: Maybe VarValue,- description :: Maybe Text,- required :: Bool,- validation :: Maybe Validation+ { name :: !VarName,+ type_ :: !VarType,+ default_ :: !(Maybe VarValue),+ description :: !(Maybe Text),+ required :: !Bool,+ validation :: !(Maybe Validation) } deriving stock (Eq, Show, Generic) -- | A variable export for cross-module visibility. data VarExport = VarExport- { var :: VarName,- alias :: Maybe VarName+ { var :: !VarName,+ alias :: !(Maybe VarName) } deriving stock (Eq, Show, Generic) -- | An interactive prompt for a variable. data Prompt = Prompt- { var :: VarName,- text :: Text,- condition :: Maybe Expr,- choices :: Maybe [Text]+ { var :: !VarName,+ text :: !Text,+ condition :: !(Maybe Expr),+ choices :: !(Maybe [Text]) } deriving stock (Eq, Show, Generic) @@ -170,43 +173,43 @@ -- | A generation step within a module. data Step = Step- { strategy :: Strategy,- src :: FilePath,- dest :: Text,- condition :: Maybe Expr,- patch :: Maybe PatchOp+ { strategy :: !Strategy,+ src :: !FilePath,+ dest :: !Text,+ condition :: !(Maybe Expr),+ patch :: !(Maybe PatchOp) } deriving stock (Eq, Show, Generic) -- | A shell command to run after file generation. data Command = Command- { run :: Text,- workDir :: Maybe Text,- condition :: Maybe Expr+ { run :: !Text,+ workDir :: !(Maybe Text),+ condition :: !(Maybe Expr) } deriving stock (Eq, Show, Generic) -- | A dependency on another module, optionally supplying variable bindings.--- When @depVars@ is non-empty, the listed variables are pre-supplied to the+-- When @vars@ is non-empty, the listed variables are pre-supplied to the -- dependency during resolution, sitting between global config and module -- defaults in the precedence chain. data Dependency = Dependency- { depModule :: ModuleName,- depVars :: Map VarName Text+ { module_ :: !ModuleName,+ vars :: !(Map VarName Text) } deriving stock (Eq, Show, Generic) -- | Create a bare dependency with no variable bindings. simpleDep :: ModuleName -> Dependency-simpleDep name = Dependency {depModule = name, depVars = mempty}+simpleDep name = Dependency {module_ = name, vars = mempty} -- | Extract module names from a list of dependencies. depModuleNames :: [Dependency] -> [ModuleName]-depModuleNames = map (.depModule)+depModuleNames = map (^. #module_) -- | The variable bindings supplied by a dependent module along a specific -- dependency edge. This is the "edge decoration" — the identity of a--- 'ModuleInstance' is determined by the @depVars@ the parent supplied,+-- 'ModuleInstance' is determined by the @vars@ the parent supplied, -- not by anything resolved downstream. -- -- The underlying 'Data.Map.Strict' @Ord@ instance gives structural equality:@@ -221,9 +224,9 @@ emptyParentVars :: ParentVars emptyParentVars = ParentVars mempty --- | Build 'ParentVars' from a 'Dependency' record's @depVars@ field.+-- | Build 'ParentVars' from a 'Dependency' record's @vars@ field. parentVarsFromDep :: Dependency -> ParentVars-parentVarsFromDep dep = ParentVars dep.depVars+parentVarsFromDep dep = ParentVars (dep ^. #vars) -- | The type of removal action for a removal step. data RemovalAction@@ -237,32 +240,32 @@ -- | A single removal step describing how to reverse one effect of a module. data RemovalStep = RemovalStep- { action :: RemovalAction,- dest :: Text,- src :: Maybe FilePath+ { action :: !RemovalAction,+ dest :: !Text,+ src :: !(Maybe FilePath) } deriving stock (Eq, Show, Generic) -- | Removal specification for a module. data Removal = Removal- { removalSteps :: [RemovalStep],- removalCommands :: [Command]+ { steps :: ![RemovalStep],+ commands :: ![Command] } deriving stock (Eq, Show, Generic) -- | A module definition: the fundamental unit of composition. data Module = Module- { name :: ModuleName,- version :: Maybe Text,- description :: Maybe Text,- vars :: [VarDecl],- exports :: [VarExport],- prompts :: [Prompt],- steps :: [Step],- commands :: [Command],- dependencies :: [Dependency],- removal :: Maybe Removal,- migrations :: [Migration]+ { name :: !ModuleName,+ version :: !(Maybe Text),+ description :: !(Maybe Text),+ vars :: ![VarDecl],+ exports :: ![VarExport],+ prompts :: ![Prompt],+ steps :: ![Step],+ commands :: ![Command],+ dependencies :: ![Dependency],+ removal :: !(Maybe Removal),+ migrations :: ![Migration] } deriving stock (Eq, Show, Generic) @@ -275,12 +278,12 @@ -- | A recipe: a named, reusable composition of modules with optional -- pre-configured variable bindings. data Recipe = Recipe- { name :: RecipeName,- version :: Maybe Text,- description :: Maybe Text,- modules :: [Dependency],- vars :: [VarDecl],- prompts :: [Prompt]+ { name :: !RecipeName,+ version :: !(Maybe Text),+ description :: !(Maybe Text),+ modules :: ![Dependency],+ vars :: ![VarDecl],+ prompts :: ![Prompt] } deriving stock (Eq, Show, Generic) @@ -289,8 +292,8 @@ -- agent's filesystem; @description@ is shown to the agent so it can -- pick the right reference for the user's request. data BlueprintFile = BlueprintFile- { src :: FilePath,- description :: Maybe Text+ { src :: !FilePath,+ description :: !(Maybe Text) } deriving stock (Eq, Show, Generic) @@ -300,17 +303,18 @@ -- name refuses with an actionable message; the agent runner -- @seihou agent run@ (EP-31) consumes them instead. data Blueprint = Blueprint- { name :: ModuleName,- version :: Maybe Text,- description :: Maybe Text,- prompt :: Text,- vars :: [VarDecl],- prompts :: [Prompt],- baseModules :: [Dependency],- files :: [BlueprintFile],- allowedTools :: Maybe [Text],- tags :: [Text],- migrations :: [BlueprintMigration]+ { name :: !ModuleName,+ version :: !(Maybe Text),+ description :: !(Maybe Text),+ prompt :: !Text,+ vars :: ![VarDecl],+ prompts :: ![Prompt],+ baseModules :: ![Dependency],+ files :: ![BlueprintFile],+ allowedTools :: !(Maybe [Text]),+ tags :: ![Text],+ migrations :: ![BlueprintMigration],+ launch :: !(Maybe AgentLaunch) } deriving stock (Eq, Show, Generic) @@ -318,31 +322,37 @@ -- Process execution is implemented outside the core Dhall decoder; this -- record only captures the author-declared command and safety metadata. data CommandVar = CommandVar- { name :: VarName,- run :: Text,- workDir :: Maybe Text,- condition :: Maybe Expr,- trim :: Bool,- maxBytes :: Maybe Natural+ { name :: !VarName,+ run :: !Text,+ workDir :: !(Maybe Text),+ condition :: !(Maybe Expr),+ trim :: !Bool,+ maxBytes :: !(Maybe Natural) } deriving stock (Eq, Show, Generic) -- | A Markdown instruction block attached to an agent prompt. The optional -- condition is evaluated after normal and command-derived variables resolve. data PromptGuidance = PromptGuidance- { title :: Text,- body :: Text,- condition :: Maybe Expr+ { title :: !Text,+ body :: !Text,+ condition :: !(Maybe Expr) } deriving stock (Eq, Show, Generic) --- | Optional launch metadata declared by an agent prompt. The CLI runner may--- use this as a default provider/model/mode hint, but project or CLI config--- remains authoritative.-data AgentPromptLaunch = AgentPromptLaunch- { provider :: Maybe Text,- mode :: Maybe Text,- model :: Maybe Text+-- | Optional launch preferences declared by an agent-driven artifact (a+-- 'Blueprint' or an 'AgentPrompt'). Values are raw text here; the CLI parses+-- and validates them, because the provider and effort vocabularies live in the+-- CLI layer. 'mode' is reserved and currently ignored.+--+-- Declared values override the invoking user's configuration files but lose to+-- a @--provider@ \/ @--model@ \/ @--effort@ flag and to the @SEIHOU_AGENT_*@+-- environment variables.+data AgentLaunch = AgentLaunch+ { provider :: !(Maybe Text),+ model :: !(Maybe Text),+ effort :: !(Maybe Text),+ mode :: !(Maybe Text) } deriving stock (Eq, Show, Generic) @@ -350,18 +360,18 @@ -- does not declare baseline modules and does not imply scaffolding or -- manifest provenance. data AgentPrompt = AgentPrompt- { name :: ModuleName,- version :: Maybe Text,- description :: Maybe Text,- prompt :: Text,- vars :: [VarDecl],- prompts :: [Prompt],- commandVars :: [CommandVar],- guidance :: [PromptGuidance],- files :: [BlueprintFile],- allowedTools :: Maybe [Text],- tags :: [Text],- launch :: Maybe AgentPromptLaunch+ { name :: !ModuleName,+ version :: !(Maybe Text),+ description :: !(Maybe Text),+ prompt :: !Text,+ vars :: ![VarDecl],+ prompts :: ![Prompt],+ commandVars :: ![CommandVar],+ guidance :: ![PromptGuidance],+ files :: ![BlueprintFile],+ allowedTools :: !(Maybe [Text]),+ tags :: ![Text],+ launch :: !(Maybe AgentLaunch) } deriving stock (Eq, Show, Generic) @@ -380,29 +390,29 @@ -- | Filesystem operations produced by the generation engine. data Operation = WriteFileOp- { dest :: FilePath,- content :: Text,- strategy :: Strategy+ { dest :: !FilePath,+ content :: !Text,+ strategy :: !Strategy } | CreateDirOp- { path :: FilePath+ { path :: !FilePath } | CopyFileOp- { src :: FilePath,- dest :: FilePath+ { src :: !FilePath,+ dest :: !FilePath } | RunCommandOp- { command :: Text,- workDir :: Maybe FilePath,- moduleName :: ModuleName,- occurrence :: Int+ { command :: !Text,+ workDir :: !(Maybe FilePath),+ moduleName :: !ModuleName,+ occurrence :: !Int } | PatchFileOp- { dest :: FilePath,- content :: Text,- op :: PatchOp,- strategy :: Strategy,- moduleName :: ModuleName+ { dest :: !FilePath,+ content :: !Text,+ op :: !PatchOp,+ strategy :: !Strategy,+ moduleName :: !ModuleName } deriving stock (Eq, Show, Generic) @@ -434,9 +444,9 @@ -- | A variable that has been resolved to a concrete value with provenance. data ResolvedVar = ResolvedVar- { value :: VarValue,- source :: VarSource,- decl :: VarDecl+ { value :: !VarValue,+ source :: !VarSource,+ decl :: !VarDecl } deriving stock (Eq, Show, Generic) @@ -465,15 +475,15 @@ -- | Tracks the state of generated files for incremental re-generation -- and conflict detection. Stored at @.seihou/manifest.json@. data Manifest = Manifest- { version :: Int,- genAt :: UTCTime,- modules :: [AppliedModule],- vars :: Map VarName Text,- files :: Map FilePath FileRecord,- applications :: [AppliedComposition],- recipe :: Maybe AppliedRecipe,- blueprint :: Maybe AppliedBlueprint,- blueprintMigrations :: [AppliedBlueprintMigration]+ { version :: !Int,+ genAt :: !UTCTime,+ modules :: ![AppliedModule],+ vars :: !(Map VarName Text),+ files :: !(Map FilePath FileRecord),+ applications :: ![AppliedComposition],+ recipe :: !(Maybe AppliedRecipe),+ blueprint :: !(Maybe AppliedBlueprint),+ blueprintMigrations :: ![AppliedBlueprintMigration] } deriving stock (Eq, Show, Generic) @@ -481,6 +491,43 @@ newtype ApplicationId = ApplicationId {unApplicationId :: Text} deriving stock (Eq, Ord, Show, Generic) +-- | Machine-independent identity of an artifact recorded in the manifest.+--+-- The manifest is checked into version control and shared between+-- developers, so it must never contain a path that is meaningful only on+-- the machine that wrote it. Every artifact reference is therefore one of+-- three cases, distinguished by how much provenance seihou can actually+-- prove.+--+-- 'RemoteOrigin' is the strong case: the artifact was installed by+-- @seihou install@ from a git URL into+-- @~\/.config\/seihou\/installed\/\<name\>@, and that URL was recorded in+-- @.seihou-origin.json@ beside it. Two developers who install from the+-- same URL are provably using the same upstream artifact.+--+-- 'ProjectOrigin' is the case where the artifact lives inside the project+-- itself, under @.seihou\/modules\/\<name\>@. The path is stored relative+-- to the project root, so it means the same thing in every clone.+--+-- 'LocalOrigin' is the weak case: the artifact was found in the+-- developer's personal @~\/.config\/seihou\/modules\/@ directory, which+-- carries no provenance metadata at all. Only the name is knowable.+-- Recording it honestly, rather than fabricating a URL, lets later+-- verification report that this artifact's provenance cannot be checked.+data ArtifactOrigin+ = RemoteOrigin+ { originUrl :: !Text,+ artifactName :: !Text,+ repoName :: !(Maybe Text)+ }+ | ProjectOrigin+ { relativePath :: !FilePath+ }+ | LocalOrigin+ { artifactName :: !Text+ }+ deriving stock (Eq, Ord, Show, Generic)+ -- | The deterministic artifact originally requested by the user. data AppliedTarget = AppliedModuleTarget ModuleName@@ -497,44 +544,48 @@ -- | Evidence that one rendered command completed successfully. data CommandReceipt = CommandReceipt- { fingerprint :: CommandFingerprint,- moduleName :: ModuleName,- command :: Text,- workDir :: Maybe FilePath,- completedAt :: UTCTime+ { fingerprint :: !CommandFingerprint,+ moduleName :: !ModuleName,+ command :: !Text,+ workDir :: !(Maybe FilePath),+ completedAt :: !UTCTime } deriving stock (Eq, Show, Generic) -- | Reproducible state for one module instance in an application.+--+-- @origin@ is the module's portable identity. Turning it back into a+-- directory on the current machine is 'Seihou.Core.ArtifactRef.resolveArtifactOrigin';+-- no path is ever recorded here. data AppliedInstanceState = AppliedInstanceState- { name :: ModuleName,- parentVars :: ParentVars,- source :: FilePath,- moduleVersion :: Maybe Text,- resolvedVars :: Map VarName Text+ { name :: !ModuleName,+ parentVars :: !ParentVars,+ origin :: !ArtifactOrigin,+ moduleVersion :: !(Maybe Text),+ resolvedVars :: !(Map VarName Text) } deriving stock (Eq, Show, Generic) -- | A complete, re-runnable top-level module or recipe composition. data AppliedComposition = AppliedComposition- { applicationId :: ApplicationId,- target :: AppliedTarget,- targetSource :: FilePath,- targetVersion :: Maybe Text,- additionalModules :: [ModuleName],- namespace :: Maybe Text,- context :: Maybe Text,- instances :: [AppliedInstanceState],- commandReceipts :: Map CommandFingerprint CommandReceipt,- appliedAt :: UTCTime+ { applicationId :: !ApplicationId,+ target :: !AppliedTarget,+ targetOrigin :: !ArtifactOrigin,+ targetVersion :: !(Maybe Text),+ additionalModules :: ![ModuleName],+ namespace :: !(Maybe Text),+ context :: !(Maybe Text),+ instances :: ![AppliedInstanceState],+ commandReceipts :: !(Map CommandFingerprint CommandReceipt),+ appliedAt :: !UTCTime } deriving stock (Eq, Show, Generic) -- | Recipe provenance recorded in the manifest when a recipe is used. data AppliedRecipe = AppliedRecipe- { name :: RecipeName,- recipeVersion :: Maybe Text,- appliedAt :: UTCTime+ { name :: !RecipeName,+ recipeVersion :: !(Maybe Text),+ appliedAt :: !UTCTime } deriving stock (Eq, Show, Generic) @@ -552,13 +603,13 @@ -- in @docs/masterplans/3-agent-driven-blueprints.md@; in v1 it is always -- 'Nothing' and the encoder omits the JSON key in that case. data AppliedBlueprint = AppliedBlueprint- { name :: ModuleName,- blueprintVersion :: Maybe Text,- appliedAt :: UTCTime,- baselineModules :: [ModuleName],- noBaseline :: Bool,- userPrompt :: Maybe Text,- agentSessionId :: Maybe Text+ { name :: !ModuleName,+ blueprintVersion :: !(Maybe Text),+ appliedAt :: !UTCTime,+ baselineModules :: ![ModuleName],+ noBaseline :: !Bool,+ userPrompt :: !(Maybe Text),+ agentSessionId :: !(Maybe Text) } deriving stock (Eq, Show, Generic) @@ -566,12 +617,12 @@ -- migration edge. Exact-edge identity is the blueprint 'name' together with -- 'fromVersion' and 'toVersion'; the remaining fields are audit metadata. data AppliedBlueprintMigration = AppliedBlueprintMigration- { name :: ModuleName,- blueprintVersion :: Maybe Text,- fromVersion :: Text,- toVersion :: Text,- appliedAt :: UTCTime,- agentSessionId :: Maybe Text+ { name :: !ModuleName,+ blueprintVersion :: !(Maybe Text),+ fromVersion :: !Text,+ toVersion :: !Text,+ appliedAt :: !UTCTime,+ agentSessionId :: !(Maybe Text) } deriving stock (Eq, Show, Generic) @@ -582,24 +633,29 @@ -- the same @name@ and different @parentVars@ represent two legitimate -- instances. Manifests produced before schema version 2 decode with -- @parentVars = 'emptyParentVars'@.+--+-- @origin@ is the module's portable identity. Turning it back into a+-- directory on the current machine is+-- 'Seihou.Core.ArtifactRef.resolveArtifactOrigin'; no path is ever recorded+-- here. data AppliedModule = AppliedModule- { name :: ModuleName,- parentVars :: ParentVars,- source :: FilePath,- moduleVersion :: Maybe Text,- appliedAt :: UTCTime,- removal :: Maybe Removal+ { name :: !ModuleName,+ parentVars :: !ParentVars,+ origin :: !ArtifactOrigin,+ moduleVersion :: !(Maybe Text),+ appliedAt :: !UTCTime,+ removal :: !(Maybe Removal) } deriving stock (Eq, Show, Generic) -- | A record of a generated file, stored in the manifest. data FileRecord = FileRecord- { hash :: SHA256,- moduleName :: ModuleName,- strategy :: Strategy,- generatedAt :: UTCTime,- baseline :: Maybe BaselineRef,- applicationIds :: Set ApplicationId+ { hash :: !SHA256,+ moduleName :: !ModuleName,+ strategy :: !Strategy,+ generatedAt :: !UTCTime,+ baseline :: !(Maybe BaselineRef),+ applicationIds :: !(Set ApplicationId) } deriving stock (Eq, Show, Generic) @@ -610,47 +666,47 @@ -- | Result of the three-state diff: manifest vs plan vs disk. data DiffResult = DiffResult- { new :: [PlannedFile],- modified :: [ModifiedFile],- unchanged :: [FilePath],- conflicts :: [ConflictFile],- orphaned :: [OrphanedFile]+ { new :: ![PlannedFile],+ modified :: ![ModifiedFile],+ unchanged :: ![FilePath],+ conflicts :: ![ConflictFile],+ orphaned :: ![OrphanedFile] } deriving stock (Eq, Show, Generic) -- | A file that exists in the plan but not in the manifest or on disk. data PlannedFile = PlannedFile- { path :: FilePath,- moduleName :: ModuleName,- content :: Text+ { path :: !FilePath,+ moduleName :: !ModuleName,+ content :: !Text } deriving stock (Eq, Show, Generic) -- | A file that has changed between the manifest and the plan, -- but the user has not modified the disk copy. data ModifiedFile = ModifiedFile- { path :: FilePath,- moduleName :: ModuleName,- oldHash :: SHA256,- newContent :: Text+ { path :: !FilePath,+ moduleName :: !ModuleName,+ oldHash :: !SHA256,+ newContent :: !Text } deriving stock (Eq, Show, Generic) -- | A file where the user has modified the disk copy since it was generated. data ConflictFile = ConflictFile- { path :: FilePath,- moduleName :: ModuleName,- manifestHash :: SHA256,- diskHash :: SHA256,- planContent :: Text+ { path :: !FilePath,+ moduleName :: !ModuleName,+ manifestHash :: !SHA256,+ diskHash :: !SHA256,+ planContent :: !Text } deriving stock (Eq, Show, Generic) -- | A file that exists in the manifest but not in the current plan -- (the module that generated it was removed or no longer produces it). data OrphanedFile = OrphanedFile- { path :: FilePath,- moduleName :: ModuleName+ { path :: !FilePath,+ moduleName :: !ModuleName } deriving stock (Eq, Show, Generic) @@ -705,8 +761,8 @@ -- | A tracked file with its path, originating module, and disk status. data TrackedFile = TrackedFile- { path :: FilePath,- moduleName :: ModuleName,- status :: TrackedFileStatus+ { path :: !FilePath,+ moduleName :: !ModuleName,+ status :: !TrackedFileStatus } deriving stock (Eq, Show, Generic)
src/Seihou/Core/Variable.hs view
@@ -12,6 +12,7 @@ where import Data.Char (toUpper)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Maybe (catMaybes) import Data.Set qualified as Set@@ -69,9 +70,9 @@ -- | Validate a resolved value against its declaration's validation constraint. validateVarValue :: VarDecl -> VarValue -> Either VarError () validateVarValue decl val =- case decl.validation of+ case decl ^. #validation of Nothing -> Right ()- Just v -> checkValidation decl.name v val+ Just v -> checkValidation (decl ^. #name) v val checkValidation :: VarName -> Validation -> VarValue -> Either VarError () checkValidation name (ValPattern pat) (VText t) =@@ -185,8 +186,8 @@ where resolveOne :: VarDecl -> Either VarError (Maybe (VarName, ResolvedVar)) resolveOne decl =- let name = decl.name- ty = decl.type_+ let name = (decl ^. #name)+ ty = (decl ^. #type_) in case lookupCLI name ty of Just result -> fmap Just (result >>= validateAndWrap decl) Nothing -> case lookupSaved name ty of@@ -203,13 +204,13 @@ Just result -> fmap Just (result >>= validateAndWrap decl) Nothing -> case lookupParent name ty of Just result -> fmap Just (result >>= validateAndWrap decl)- Nothing -> case decl.default_ of+ Nothing -> case decl ^. #default_ of Just defVal -> case coerceDefault name ty defVal of Left err -> Left err Right val -> fmap Just (validateAndWrap decl (val, FromDefault)) Nothing- | decl.required -> Left (MissingRequiredVar name)+ | decl ^. #required -> Left (MissingRequiredVar name) | otherwise -> Right Nothing lookupSaved :: VarName -> VarType -> Maybe (Either VarError (VarValue, VarSource))@@ -264,7 +265,7 @@ Left err -> Left err Right () -> Right- ( decl.name,+ ( decl ^. #name, ResolvedVar { value = val, source = source,@@ -289,14 +290,14 @@ -- Calculate column widths for alignment maxNameLen = maximum (0 : map (\(VarName n, _) -> T.length n) entries)- maxValueLen = maximum (0 : map (\(_, rv) -> T.length (showValue rv.value)) entries)+ maxValueLen = maximum (0 : map (\(_, rv) -> T.length (showValue (rv ^. #value))) entries) formatOne :: (VarName, ResolvedVar) -> Text formatOne (VarName n, rv) =- let valText = showValue rv.value+ let valText = showValue (rv ^. #value) namePad = T.replicate (maxNameLen - T.length n) " " valPad = T.replicate (maxValueLen - T.length valText) " "- in " " <> n <> namePad <> " = " <> valText <> valPad <> " " <> showSource rv.source+ in " " <> n <> namePad <> " = " <> valText <> valPad <> " " <> showSource (rv ^. #source) showValue :: VarValue -> Text showValue (VText t) = "\"" <> t <> "\""@@ -313,7 +314,7 @@ showSource (FromNamespaceConfig ns) = "[namespace: " <> ns <> "]" showSource (FromContextConfig ctx) = "[context: " <> ctx <> "]" showSource FromGlobalConfig = "[global config]"- showSource (FromParent mn) = "[parent: " <> mn.unModuleName <> "]"+ showSource (FromParent mn) = "[parent: " <> mn ^. #unModuleName <> "]" showSource FromDefault = "[default]" showSource FromPrompt = "[prompt]" showSource (FromCommand cmd) = "[command: " <> cmd <> "]"@@ -324,15 +325,15 @@ formatDeclarations decls = T.unlines (map formatOne decls) where- maxNameLen = maximum (0 : map (\d -> T.length d.name.unVarName) decls)+ maxNameLen = maximum (0 : map (\d -> T.length (d ^. #name . #unVarName)) decls) formatOne :: VarDecl -> Text formatOne d =- let VarName n = d.name+ let VarName n = (d ^. #name) namePad = T.replicate (maxNameLen - T.length n) " "- valText = case d.default_ of+ valText = case d ^. #default_ of Nothing- | d.required -> "(required, no default)"+ | d ^. #required -> "(required, no default)" | otherwise -> "(optional, no default)" Just v -> showDeclValue v in " " <> n <> namePad <> " = " <> valText@@ -362,15 +363,15 @@ diagnoseResolution resolved decls localConfig nsConfig ctxConfig globalConfig = (unusedConfigKeys, unresolvedOptional) where- declaredNames = Set.fromList (map (.name) decls)+ declaredNames = Set.fromList (map (^. #name) decls) allConfigKeys = Set.fromList $ Map.keys localConfig ++ Map.keys nsConfig ++ Map.keys ctxConfig ++ Map.keys globalConfig unusedConfigKeys = Set.toAscList (allConfigKeys `Set.difference` declaredNames) unresolvedOptional =- [ d.name+ [ d ^. #name | d <- decls,- not d.required,- not (Map.member d.name resolved)+ not (d ^. #required),+ not (Map.member (d ^. #name) resolved) ]
src/Seihou/Core/Version.hs view
@@ -6,7 +6,6 @@ where import Data.Text qualified as T-import GHC.Generics (Generic) import Numeric.Natural (Natural) import Seihou.Prelude
src/Seihou/Dhall/Eval.hs view
@@ -11,7 +11,7 @@ agentPromptDecoder, commandVarDecoder, promptGuidanceDecoder,- agentPromptLaunchDecoder,+ agentLaunchDecoder, blueprintFileDecoder, registryDecoder, registryEntryDecoder,@@ -35,6 +35,7 @@ import Control.Exception (SomeException, evaluate, throwIO, try) import Data.Either.Validation (Validation (..))+import Data.Generics.Labels () import Data.List (foldl') import Data.Map.Strict qualified as Map import Data.Text qualified as T@@ -96,10 +97,10 @@ case extract moduleDecoder expr of Success m -> do -- Force lazy decoder thunks that may contain 'error' calls- mapM_ (\v -> evaluate v.type_) m.vars- mapM_ (\s -> evaluate s.strategy >> evaluate s.condition >> mapM_ evaluate s.patch) m.steps- mapM_ (\c -> mapM_ evaluate c.condition) m.commands- mapM_ (\p -> evaluate p.condition) m.prompts+ mapM_ (\v -> evaluate (v ^. #type_)) (m ^. #vars)+ mapM_ (\s -> evaluate (s ^. #strategy) >> evaluate (s ^. #condition) >> mapM_ evaluate (s ^. #patch)) (m ^. #steps)+ mapM_ (\c -> mapM_ evaluate (c ^. #condition)) (m ^. #commands)+ mapM_ (\p -> evaluate (p ^. #condition)) (m ^. #prompts) pure m Failure e -> throwIO e case result of@@ -125,8 +126,8 @@ case extract recipeDecoder expr of Success r -> do -- Force lazy decoder thunks that may contain 'error' calls- mapM_ (\v -> evaluate v.type_) r.vars- mapM_ (\p -> evaluate p.condition) r.prompts+ mapM_ (\v -> evaluate (v ^. #type_)) (r ^. #vars)+ mapM_ (\p -> evaluate (p ^. #condition)) (r ^. #prompts) pure r Failure e -> throwIO e case result of@@ -276,9 +277,11 @@ ) -- | Decoder for the top-level Blueprint type from Dhall.+-- Uses 'withDefaults' to handle blueprints that predate the @migrations@ and+-- @launch@ fields. blueprintDecoder :: Decoder Blueprint blueprintDecoder =- withDefaults [("migrations", emptyMigrationList)] $+ withDefaults [("migrations", emptyMigrationList), ("launch", noneText)] $ record ( Blueprint <$> field "name" moduleNameDecoder@@ -292,6 +295,7 @@ <*> field "allowedTools" (maybe (list strictText)) <*> field "tags" (list strictText) <*> field "migrations" (list blueprintMigrationDecoder)+ <*> field "launch" (maybe agentLaunchDecoder) ) -- | Evaluate a @blueprint.dhall@ file and decode it into a 'Blueprint'.@@ -311,8 +315,8 @@ expr <- inputExprWithSettings settings text case extract blueprintDecoder expr of Success b -> do- mapM_ (\v -> evaluate v.type_) b.vars- mapM_ (\p -> evaluate p.condition) b.prompts+ mapM_ (\v -> evaluate (v ^. #type_)) (b ^. #vars)+ mapM_ (\p -> evaluate (p ^. #condition)) (b ^. #prompts) pure b Failure e -> throwIO e case result of@@ -344,15 +348,19 @@ maxBytes = maxBytes } --- | Decoder for optional agent prompt launch metadata.-agentPromptLaunchDecoder :: Decoder AgentPromptLaunch-agentPromptLaunchDecoder =- record- ( AgentPromptLaunch- <$> field "provider" (maybe strictText)- <*> field "mode" (maybe strictText)- <*> field "model" (maybe strictText)- )+-- | Decoder for the shared launch record declared by a 'Blueprint' or an+-- 'AgentPrompt'. @effort@ and @mode@ are defaulted so artifacts authored+-- against a schema pin that predates them still decode.+agentLaunchDecoder :: Decoder AgentLaunch+agentLaunchDecoder =+ withDefaults [("effort", noneText), ("mode", noneText)] $+ record+ ( AgentLaunch+ <$> field "provider" (maybe strictText)+ <*> field "model" (maybe strictText)+ <*> field "effort" (maybe strictText)+ <*> field "mode" (maybe strictText)+ ) -- | Decoder for a prompt guidance block. promptGuidanceDecoder :: Decoder PromptGuidance@@ -388,7 +396,7 @@ <*> field "files" (list blueprintFileDecoder) <*> field "allowedTools" (maybe (list strictText)) <*> field "tags" (list strictText)- <*> field "launch" (maybe agentPromptLaunchDecoder)+ <*> field "launch" (maybe agentLaunchDecoder) ) emptyPromptGuidanceList :: Dhall.Expr Src Void@@ -405,10 +413,10 @@ expr <- inputExprWithSettings settings text case extract agentPromptDecoder expr of Success p -> do- mapM_ (\v -> evaluate v.type_) p.vars- mapM_ (\prompt -> evaluate prompt.condition) p.prompts- mapM_ (\cv -> evaluate cv.condition) p.commandVars- mapM_ (\g -> evaluate g.condition) p.guidance+ mapM_ (\v -> evaluate (v ^. #type_)) (p ^. #vars)+ mapM_ (\prompt -> evaluate (prompt ^. #condition)) (p ^. #prompts)+ mapM_ (\cv -> evaluate (cv ^. #condition)) (p ^. #commandVars)+ mapM_ (\g -> evaluate (g ^. #condition)) (p ^. #guidance) pure p Failure e -> throwIO e case result of@@ -491,7 +499,7 @@ ) mkDep :: ModuleName -> [(VarName, Text)] -> Dependency- mkDep name bindings = Dependency {depModule = name, depVars = Map.fromList bindings}+ mkDep name bindings = Dependency {module_ = name, vars = Map.fromList bindings} -- | Decoder for VarType from a Dhall Text string. -- Dhall does not support recursive types, so VarType is represented as a@@ -560,25 +568,25 @@ -- 'evalModuleFromFile' and surfaced as a 'DhallEvalError'. coerceDeclDefault :: VarDecl -> VarDecl coerceDeclDefault decl =- case decl.default_ of+ case decl ^. #default_ of Nothing -> decl Just rawDefault ->- case coerceDefault decl.name decl.type_ rawDefault of- Right val -> decl {default_ = Just val}+ case coerceDefault (decl ^. #name) (decl ^. #type_) rawDefault of+ Right val -> decl & #default_ ?~ val -- Caught by 'try' in 'evalModuleFromFile'- Left err -> error (T.unpack (renderDefaultError decl.name err))+ Left err -> error (T.unpack (renderDefaultError (decl ^. #name) err)) -- | Render a coercion failure for a module default into a load-time message. renderDefaultError :: VarName -> VarError -> Text renderDefaultError name (CoercionFailed _ ty raw) = "Invalid default for variable '"- <> name.unVarName+ <> name ^. #unVarName <> "': cannot coerce " <> T.pack (show raw) <> " to declared type " <> renderVarType ty renderDefaultError name err =- "Invalid default for variable '" <> name.unVarName <> "': " <> T.pack (show err)+ "Invalid default for variable '" <> name ^. #unVarName <> "': " <> T.pack (show err) -- | A short rendering of a declared variable type for error messages. renderVarType :: VarType -> Text
src/Seihou/Effect/BaselineStoreInterp.hs view
@@ -4,6 +4,7 @@ where import Control.Monad (filterM, unless, when)+import Data.Generics.Labels () import Data.Maybe (mapMaybe) import Data.Set qualified as Set import Data.Text qualified as T@@ -33,7 +34,7 @@ finalExists <- doesFileExist finalPath reusable <- if finalExists- then ((== ref.unBaselineRef) . hashContent) <$> readFileText finalPath+ then ((== ref ^. #unBaselineRef) . hashContent) <$> readFileText finalPath else pure False unless reusable $ do writeFileText tempPath content@@ -50,7 +51,7 @@ else do content <- readFileText path let actual = hashContent content- if actual == ref.unBaselineRef+ if actual == ref ^. #unBaselineRef then pure (Right content) else pure (Left (BaselineCorrupt ref actual)) PruneBaselines referenced -> do@@ -71,12 +72,12 @@ isFile <- doesFileExist path if not isFile then pure False- else ((== ref.unBaselineRef) . hashContent) <$> readFileText path+ else ((== ref ^. #unBaselineRef) . hashContent) <$> readFileText path baselinePath :: FilePath -> BaselineRef -> Maybe FilePath baselinePath root ref = do- normalized <- baselineRefFromText ref.unBaselineRef.unSHA256- pure (root </> T.unpack normalized.unBaselineRef.unSHA256)+ normalized <- baselineRefFromText (ref ^. #unBaselineRef . #unSHA256)+ pure (root </> T.unpack (normalized ^. #unBaselineRef . #unSHA256)) checkedBaselinePath :: FilePath -> BaselineRef -> FilePath checkedBaselinePath root ref = case baselinePath root ref of
src/Seihou/Effect/BaselineStorePure.hs view
@@ -3,6 +3,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set qualified as Set@@ -32,7 +33,7 @@ Nothing -> Left (BaselineMissing ref) Just content -> let actual = hashContent content- in if actual == ref.unBaselineRef+ in if actual == ref ^. #unBaselineRef then Right content else Left (BaselineCorrupt ref actual) PruneBaselines referenced -> do@@ -40,7 +41,7 @@ let removable = Map.keysSet $ Map.filterWithKey- (\ref content -> Set.notMember ref referenced && hashContent content == ref.unBaselineRef)+ (\ref content -> Set.notMember ref referenced && hashContent content == ref ^. #unBaselineRef) store modify @(Map BaselineRef Text) (`Map.withoutKeys` removable) pure (Set.toAscList removable)
src/Seihou/Effect/ConfigWriterPure.hs view
@@ -5,6 +5,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Effectful.State.Static.Local (State, get, modify, runState) import Seihou.Core.Types (ConfigScope (..))@@ -13,19 +14,19 @@ -- | In-memory state for the pure ConfigWriter interpreter. data ConfigWriterState = ConfigWriterState- { cwLocal :: Map Text Text,- cwNamespaces :: Map Text (Map Text Text),- cwGlobal :: Map Text Text+ { local :: !(Map Text Text),+ namespaces :: !(Map Text (Map Text Text)),+ global :: !(Map Text Text) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Empty initial state with no config values in any scope. emptyConfigWriterState :: ConfigWriterState emptyConfigWriterState = ConfigWriterState- { cwLocal = Map.empty,- cwNamespaces = Map.empty,- cwGlobal = Map.empty+ { local = Map.empty,+ namespaces = Map.empty,+ global = Map.empty } -- | Pure interpreter for the ConfigWriter effect using in-memory state.@@ -46,22 +47,22 @@ pure (Right (readScope scope st)) writeToScope :: ConfigScope -> Text -> Text -> ConfigWriterState -> ConfigWriterState-writeToScope ScopeLocal key val st = st {cwLocal = Map.insert key val st.cwLocal}+writeToScope ScopeLocal key val st = st & #local . at key ?~ val writeToScope (ScopeNamespace ns) key val st =- let nsMap = Map.findWithDefault Map.empty ns st.cwNamespaces+ let nsMap = Map.findWithDefault Map.empty ns (st ^. #namespaces) updated = Map.insert key val nsMap- in st {cwNamespaces = Map.insert ns updated st.cwNamespaces}-writeToScope ScopeGlobal key val st = st {cwGlobal = Map.insert key val st.cwGlobal}+ in st & #namespaces . at ns ?~ updated+writeToScope ScopeGlobal key val st = st & #global . at key ?~ val deleteFromScope :: ConfigScope -> Text -> ConfigWriterState -> ConfigWriterState-deleteFromScope ScopeLocal key st = st {cwLocal = Map.delete key st.cwLocal}+deleteFromScope ScopeLocal key st = st & #local . at key .~ Nothing deleteFromScope (ScopeNamespace ns) key st =- let nsMap = Map.findWithDefault Map.empty ns st.cwNamespaces+ let nsMap = Map.findWithDefault Map.empty ns (st ^. #namespaces) updated = Map.delete key nsMap- in st {cwNamespaces = Map.insert ns updated st.cwNamespaces}-deleteFromScope ScopeGlobal key st = st {cwGlobal = Map.delete key st.cwGlobal}+ in st & #namespaces . at ns ?~ updated+deleteFromScope ScopeGlobal key st = st & #global . at key .~ Nothing readScope :: ConfigScope -> ConfigWriterState -> Map Text Text-readScope ScopeLocal st = st.cwLocal-readScope (ScopeNamespace ns) st = Map.findWithDefault Map.empty ns st.cwNamespaces-readScope ScopeGlobal st = st.cwGlobal+readScope ScopeLocal st = st ^. #local+readScope (ScopeNamespace ns) st = Map.findWithDefault Map.empty ns (st ^. #namespaces)+readScope ScopeGlobal st = st ^. #global
src/Seihou/Effect/ConsolePure.hs view
@@ -6,6 +6,7 @@ ) where +import Data.Generics.Labels () import Effectful.State.Static.Local (State, get, modify, runState) import Seihou.Effect.Console (Console (..)) import Seihou.Prelude@@ -13,11 +14,11 @@ -- | State for the pure Console interpreter. data ConsoleState = ConsoleState- { consoleInputs :: [Text],- consoleOutputs :: [Text],- consoleErrors :: [Text]+ { inputs :: ![Text],+ outputs :: ![Text],+ errors :: ![Text] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Empty console state with no inputs or outputs. emptyConsoleState :: ConsoleState@@ -30,8 +31,8 @@ where handler :: (State ConsoleState :> es') => EffectHandler Console es' handler _ = \case- PutText msg -> modify @ConsoleState (\s -> s {consoleOutputs = s.consoleOutputs ++ [msg]})- PutError msg -> modify @ConsoleState (\s -> s {consoleErrors = s.consoleErrors ++ [msg]})+ PutText msg -> modify @ConsoleState (\s -> s & #outputs %~ (<> [msg]))+ PutError msg -> modify @ConsoleState (\s -> s & #errors %~ (<> [msg])) GetLine -> popInput Confirm _prompt -> (`elem` ["y", "yes"]) <$> popInput IsInteractive -> pure True@@ -39,10 +40,10 @@ popInput :: (State ConsoleState :> es') => Eff es' Text popInput = do s <- get @ConsoleState- case s.consoleInputs of+ case s ^. #inputs of [] -> pure "" (x : xs) -> do- modify @ConsoleState (\st -> st {consoleInputs = xs})+ modify @ConsoleState (\st -> st & #inputs .~ xs) pure x -- | Pure interpreter for non-interactive mode. IsInteractive returns False.@@ -51,8 +52,8 @@ where handler :: (State ConsoleState :> es') => EffectHandler Console es' handler _ = \case- PutText msg -> modify @ConsoleState (\s -> s {consoleOutputs = s.consoleOutputs ++ [msg]})- PutError msg -> modify @ConsoleState (\s -> s {consoleErrors = s.consoleErrors ++ [msg]})+ PutText msg -> modify @ConsoleState (\s -> s & #outputs %~ (<> [msg]))+ PutError msg -> modify @ConsoleState (\s -> s & #errors %~ (<> [msg])) GetLine -> pure "" Confirm _prompt -> pure False IsInteractive -> pure False
src/Seihou/Effect/FilesystemPure.hs view
@@ -5,6 +5,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Effectful.State.Static.Local (State, get, modify, put, runState)@@ -13,10 +14,10 @@ -- | In-memory filesystem state for testing. data PureFS = PureFS- { files :: Map FilePath Text,- dirs :: Set FilePath+ { files :: !(Map FilePath Text),+ dirs :: !(Set FilePath) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | An empty in-memory filesystem. emptyFS :: PureFS@@ -31,50 +32,50 @@ handler _ = \case ReadFileText path -> do fs <- get @PureFS- case Map.lookup path fs.files of+ case Map.lookup path (fs ^. #files) of Just content -> pure content Nothing -> error ("runFilesystemPure: file not found: " <> path) WriteFileText path content -> do- modify @PureFS (\fs -> fs {files = Map.insert path content fs.files})+ modify @PureFS (\fs -> fs & #files . at path ?~ content) CopyFile src dest -> do fs <- get @PureFS- case Map.lookup src fs.files of+ case Map.lookup src (fs ^. #files) of Just content ->- put fs {files = Map.insert dest content fs.files}+ put (fs & #files . at dest ?~ content) Nothing -> error ("runFilesystemPure: source file not found: " <> src) ListDirectory path -> do fs <- get @PureFS let prefix = if null path then "" else path <> "/" filesInDir = [ drop (length prefix) fp- | fp <- Map.keys fs.files,+ | fp <- Map.keys (fs ^. #files), isDirectChild prefix fp ] dirsInDir = [ drop (length prefix) d- | d <- Set.toList fs.dirs,+ | d <- Set.toList (fs ^. #dirs), isDirectChild prefix d ] pure (filesInDir <> dirsInDir) CreateDirectoryIfMissing _parents path -> do- modify @PureFS (\fs -> fs {dirs = Set.insert path fs.dirs})+ modify @PureFS (\fs -> fs & #dirs %~ Set.insert path) DoesFileExist path -> do fs <- get @PureFS- pure (Map.member path fs.files)+ pure (Map.member path (fs ^. #files)) DoesDirectoryExist path -> do fs <- get @PureFS- pure (Set.member path fs.dirs)+ pure (Set.member path (fs ^. #dirs)) GetCurrentDirectory -> pure "/pure-fs" RemoveFile path -> do- modify @PureFS (\fs -> fs {files = Map.delete path fs.files})+ modify @PureFS (\fs -> fs & #files . at path .~ Nothing) RemoveDirectoryIfEmpty path -> do fs <- get @PureFS let hasChildren =- any (\fp -> (path <> "/") `isPrefixOfPath` fp) (Map.keys fs.files)- || any (\d -> (path <> "/") `isPrefixOfPath` d) (Set.toList fs.dirs)+ any (\fp -> (path <> "/") `isPrefixOfPath` fp) (Map.keys (fs ^. #files))+ || any (\d -> (path <> "/") `isPrefixOfPath` d) (Set.toList (fs ^. #dirs)) if hasChildren then pure ()- else modify @PureFS (\fs' -> fs' {dirs = Set.delete path fs'.dirs})+ else modify @PureFS (\fs' -> fs' & #dirs %~ Set.delete path) RenamePath src dest -> do modify @PureFS (renameInPureFS src dest) RemoveDirectoryRecursive path -> do@@ -106,9 +107,13 @@ -- when callers have already validated existence). renameInPureFS :: FilePath -> FilePath -> PureFS -> PureFS renameInPureFS src dest fs =- let renamedFiles = Map.mapKeys (renameKey src dest) fs.files- renamedDirs = Set.map (renameKey src dest) fs.dirs- in fs {files = renamedFiles, dirs = renamedDirs}+ let renamedFiles = Map.mapKeys (renameKey src dest) (fs ^. #files)+ renamedDirs = Set.map (renameKey src dest) (fs ^. #dirs)+ in fs+ & #files+ .~ renamedFiles+ & #dirs+ .~ renamedDirs where renameKey s d k | k == s = d@@ -123,6 +128,7 @@ keepFile k = k /= path && not (prefix `isPrefixOfPath` k) keepDir d = d /= path && not (prefix `isPrefixOfPath` d) in fs- { files = Map.filterWithKey (\k _ -> keepFile k) fs.files,- dirs = Set.filter keepDir fs.dirs- }+ & #files+ %~ Map.filterWithKey (\k _ -> keepFile k)+ & #dirs+ %~ Set.filter keepDir
src/Seihou/Effect/LoggerPure.hs view
@@ -5,6 +5,7 @@ ) where +import Data.Generics.Labels () import Effectful.State.Static.Local (State, modify, runState) import Seihou.Effect.Logger (Logger (..)) import Seihou.Prelude@@ -12,12 +13,12 @@ -- | State capturing all log messages by severity. -- Messages are appended in order within each field. data LoggerState = LoggerState- { logDebugMsgs :: [Text],- logInfoMsgs :: [Text],- logWarnMsgs :: [Text],- logErrorMsgs :: [Text]+ { debugMsgs :: ![Text],+ infoMsgs :: ![Text],+ warnMsgs :: ![Text],+ errorMsgs :: ![Text] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Empty logger state with no captured messages. emptyLoggerState :: LoggerState@@ -31,7 +32,7 @@ where handler :: (State LoggerState :> es') => EffectHandler Logger es' handler _ = \case- LogDebug msg -> modify @LoggerState (\s -> s {logDebugMsgs = s.logDebugMsgs ++ [msg]})- LogInfo msg -> modify @LoggerState (\s -> s {logInfoMsgs = s.logInfoMsgs ++ [msg]})- LogWarn msg -> modify @LoggerState (\s -> s {logWarnMsgs = s.logWarnMsgs ++ [msg]})- LogError msg -> modify @LoggerState (\s -> s {logErrorMsgs = s.logErrorMsgs ++ [msg]})+ LogDebug msg -> modify @LoggerState (\s -> s & #debugMsgs %~ (<> [msg]))+ LogInfo msg -> modify @LoggerState (\s -> s & #infoMsgs %~ (<> [msg]))+ LogWarn msg -> modify @LoggerState (\s -> s & #warnMsgs %~ (<> [msg]))+ LogError msg -> modify @LoggerState (\s -> s & #errorMsgs %~ (<> [msg]))
src/Seihou/Effect/ProcessInterp.hs view
@@ -12,6 +12,8 @@ runProcessIO :: (IOE :> es) => Eff (Process : es) a -> Eff es a runProcessIO = interpret $ \_ -> \case RunProcess cmd args workDir -> liftIO $ do+ -- CreateProcess is a third-party type with no Generic instance, so it has+ -- no #cwd label to set. Record update syntax is the only option here. let cp = (proc (T.unpack cmd) (map T.unpack args)) { cwd = workDir
src/Seihou/Effect/ProcessPure.hs view
@@ -4,16 +4,17 @@ ) where +import Data.Generics.Labels () import Seihou.Effect.Process (Process (..)) import Seihou.Prelude import System.Exit (ExitCode (..)) data ProcessMock = ProcessMock- { mockCommand :: Text,- mockArgs :: [Text],- mockResult :: (ExitCode, Text, Text)+ { command :: !Text,+ args :: ![Text],+ result :: !(ExitCode, Text, Text) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) runProcessPure :: [ProcessMock] -> Eff (Process : es) a -> Eff es a runProcessPure mocks = interpret $ \_ -> \case@@ -25,5 +26,5 @@ findMock :: Text -> [Text] -> [ProcessMock] -> Maybe (ExitCode, Text, Text) findMock _ _ [] = Nothing findMock cmd args (m : ms)- | m.mockCommand == cmd && m.mockArgs == args = Just m.mockResult+ | m ^. #command == cmd && m ^. #args == args = Just (m ^. #result) | otherwise = findMock cmd args ms
src/Seihou/Engine/Baseline.hs view
@@ -5,6 +5,7 @@ where import Control.Monad (foldM)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Maybe (mapMaybe) import Data.Set qualified as Set@@ -41,14 +42,16 @@ content <- readFileText fullPath ref <- putBaseline content let enriched =- record- { hash = hashContent content,- baseline = Just ref- }+ ( record+ & #hash+ .~ hashContent content+ & #baseline+ ?~ ref+ ) pure (Right (Map.insert path enriched captured)) -- | Every blob protected by the currently durable manifest. Callers pass this -- set to 'pruneBaselines' only after publishing that manifest. manifestBaselineRefs :: Manifest -> Set BaselineRef manifestBaselineRefs manifest =- Set.fromList (mapMaybe (.baseline) (Map.elems manifest.files))+ Set.fromList (mapMaybe (^. #baseline) (Map.elems (manifest ^. #files)))
src/Seihou/Engine/Conflict.hs view
@@ -4,6 +4,7 @@ ) where +import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Types (ConflictFile (..), ConflictResolution (..)) import Seihou.Effect.Console (Console, getLine, isInteractive, putText)@@ -61,7 +62,7 @@ ConflictFile -> Eff es ConflictResolution promptConflict c = do- putText $ "Conflict: " <> T.pack c.path <> " (modified since last generation)"+ putText $ "Conflict: " <> T.pack (c ^. #path) <> " (modified since last generation)" promptChoice where promptChoice = do
src/Seihou/Engine/Diff.hs view
@@ -4,6 +4,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set (Set) import Data.Set qualified as Set@@ -38,9 +39,9 @@ [(FilePath, Text, ModuleName, Maybe PatchOp)] -> Eff es DiffResult computeDiff manifest activeModules planned = do- let manifestFiles' = manifest.files+ let manifestFiles' = (manifest ^. #files) activeManifestFiles =- Map.filter (\r -> r.moduleName `Set.member` activeModules) manifestFiles'+ Map.filter (\r -> (r ^. #moduleName) `Set.member` activeModules) manifestFiles' planMap = Map.fromList [(p, (content, modName, patchOp)) | (p, content, modName, patchOp) <- planned] allPaths = Set.toList $@@ -112,7 +113,7 @@ (Just record, Just (content, modName, _), True) -> do diskContent <- readFileText path let diskHash = hashContent diskContent- manifestHash = record.hash+ manifestHash = (record ^. #hash) planHash = hashContent content if diskHash /= manifestHash then@@ -159,16 +160,16 @@ ( ModifiedFile { path = path, moduleName = modName,- oldHash = record.hash,+ oldHash = record ^. #hash, newContent = content } ) -- In manifest, not in plan, on disk → Orphaned (Just record, Nothing, True) ->- pure $ ClassOrphaned (OrphanedFile {path = path, moduleName = record.moduleName})+ pure $ ClassOrphaned (OrphanedFile {path = path, moduleName = record ^. #moduleName}) -- In manifest, not in plan, not on disk → Orphaned (already deleted) (Just record, Nothing, False) ->- pure $ ClassOrphaned (OrphanedFile {path = path, moduleName = record.moduleName})+ pure $ ClassOrphaned (OrphanedFile {path = path, moduleName = record ^. #moduleName}) -- Not in manifest, not in plan → shouldn't happen (we only iterate known paths) (Nothing, Nothing, _) -> pure $ ClassUnchanged path -- unreachable in practice
src/Seihou/Engine/Execute.hs view
@@ -4,6 +4,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Data.Time (UTCTime)@@ -113,7 +114,7 @@ formatOp (CopyFileOp src dest) = " copy " <> T.pack src <> " -> " <> T.pack dest formatOp RunCommandOp {command = cmd} = " run " <> cmd formatOp (PatchFileOp dest _ patchOp' _ modName) =- " patch " <> T.pack dest <> " (" <> formatPatchOp patchOp' <> " from " <> modName.unModuleName <> ")"+ " patch " <> T.pack dest <> " (" <> formatPatchOp patchOp' <> " from " <> modName ^. #unModuleName <> ")" formatPatchOp AppendFile = "append-file" formatPatchOp PrependFile = "prepend-file" formatPatchOp AppendSection = "append-section"
src/Seihou/Engine/Migrate.hs view
@@ -11,12 +11,12 @@ ) where +import Data.Generics.Labels () import Data.List (nub, sortBy) import Data.Map.Strict qualified as Map import Data.Ord (Down (..)) import Data.Text qualified as T import Data.Time (UTCTime)-import GHC.Generics (Generic) import Seihou.Core.Migration ( Migration (..), MigrationOp (..),@@ -78,9 +78,9 @@ -- 'MigrationPlan' it was built from, and the linearized list of -- concrete op instances in execution order. data ExecutedMigrationPlan = ExecutedMigrationPlan- { planModule :: ModuleName,- planSource :: MigrationPlan,- planOps :: [MigrationOpInstance]+ { module_ :: !ModuleName,+ source :: !MigrationPlan,+ ops :: ![MigrationOpInstance] } deriving stock (Eq, Show, Generic) @@ -107,14 +107,14 @@ MigrationPlan -> Eff es (Either MigrationExecError ExecutedMigrationPlan) classifyMigration manifest plan = do- opsResult <- traverse (classifyOp manifest) (concatMap (.ops) plan.planSteps)+ opsResult <- traverse (classifyOp manifest) (concatMap (^. #ops) (plan ^. #steps)) pure $ do ops <- sequence opsResult Right ExecutedMigrationPlan- { planModule = ModuleName plan.planModule,- planSource = plan,- planOps = ops+ { module_ = ModuleName (plan ^. #module_),+ source = plan,+ ops = ops } -- | Classify a single 'MigrationOp' against the manifest and disk.@@ -173,12 +173,12 @@ exists <- doesFileExist path if not exists then pure MFGone- else case Map.lookup path (manifest.files :: Map FilePath FileRecord) of+ else case Map.lookup path (manifest ^. #files :: Map FilePath FileRecord) of Nothing -> pure MFSafe Just rec -> do content <- readFileText path let diskHash = hashContent content- if diskHash == rec.hash+ if diskHash == rec ^. #hash then pure MFSafe else pure MFConflict @@ -191,9 +191,9 @@ -- Otherwise runs every op in declaration order, rewrites the manifest's -- @files@ map to reflect new paths, bumps @genAt@ to the supplied -- timestamp, and updates the named 'AppliedModule''s @moduleVersion@ to--- @planTo@ (the user's supplied target). When the source plan has an+-- @to@ (the user's supplied target). When the source plan has an -- empty 'planSteps' list, no file ops run but the manifest still--- advances to @planTo@ — this is the "pure version bump" path.+-- advances to @to@ — this is the "pure version bump" path. executeMigration :: (Filesystem :> es, Process :> es) => -- | If 'True', proceed even when files are 'MFConflict'. Mirrors the@@ -207,22 +207,24 @@ executeMigration force plan manifest now = do let conflicts = [ p- | inst <- plan.planOps,+ | inst <- plan ^. #ops, (p, MFConflict) <- toFileStatus inst ] if not force && not (null conflicts) then pure (Left (MigrationConflict conflicts)) else do- result <- runOps plan.planOps manifest []+ result <- runOps (plan ^. #ops) manifest [] case result of Left err -> pure (Left err) Right (man', removedDirs) -> do cleanupEmptyDirs removedDirs let bumped =- man'- { genAt = now,- modules = map (bumpVersion plan.planModule plan.planSource) man'.modules- }+ ( man'+ & #genAt+ .~ now+ & #modules+ %~ map (bumpVersion (plan ^. #module_) (plan ^. #source))+ ) pure (Right bumped) -- | Pull (path, status) pairs out of an op for conflict detection. Only@@ -290,12 +292,12 @@ -- map. If the key isn't present, the manifest is returned unchanged. renameInManifest :: FilePath -> FilePath -> Manifest -> Manifest renameInManifest src dest manifest =- case Map.lookup src manifest.files of+ case Map.lookup src (manifest ^. #files) of Nothing -> manifest Just rec -> manifest- { files = Map.insert dest rec (Map.delete src manifest.files)- }+ & #files+ .~ Map.insert dest rec (Map.delete src (manifest ^. #files)) -- | Rewrite every @files@ key whose path is @src@ or under @src/@ to -- replace the prefix with @dest@.@@ -306,26 +308,26 @@ | k == src = dest | prefix `isPrefixOfPath` k = dest <> "/" <> drop (length prefix) k | otherwise = k- in manifest {files = Map.mapKeys rewriteKey manifest.files}+ in manifest & #files %~ Map.mapKeys rewriteKey -- | Drop a single file entry from the manifest. dropFromManifest :: FilePath -> Manifest -> Manifest dropFromManifest p manifest =- manifest {files = Map.delete p manifest.files}+ manifest & #files . at p .~ Nothing -- | Drop every file entry whose path is @path@ or under @path/@. dropDirFromManifest :: FilePath -> Manifest -> Manifest dropDirFromManifest path manifest = let prefix = path <> "/" keep k = k /= path && not (prefix `isPrefixOfPath` k)- in manifest {files = Map.filterWithKey (\k _ -> keep k) manifest.files}+ in manifest & #files %~ Map.filterWithKey (\k _ -> keep k) -- | Update the named applied module's @moduleVersion@ to the plan's -- target. Other applied modules are untouched. bumpVersion :: ModuleName -> MigrationPlan -> AppliedModule -> AppliedModule bumpVersion modName plan am- | am.name == modName =- am {moduleVersion = Just (renderVersion plan.planTo)}+ | am ^. #name == modName =+ am & #moduleVersion ?~ (renderVersion (plan ^. #to)) | otherwise = am -- ----------------------------------------------------------------------------
src/Seihou/Engine/Plan.hs view
@@ -7,6 +7,7 @@ import Control.Exception (IOException, SomeException, catch, try) import Data.Aeson qualified as Aeson import Data.Aeson.Encode.Pretty qualified as AesonPretty+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -36,11 +37,11 @@ Map VarName VarValue -> -- Resolved variable values IO (Either [Text] [Operation]) compilePlan baseDir modul vars = do- let modName = modul.name- results <- mapM (compileStep baseDir modName vars) modul.steps+ let modName = (modul ^. #name)+ results <- mapM (compileStep baseDir modName vars) (modul ^. #steps) let (allErrors, allOps) = partitionResults results if null allErrors- then case compileCommands modName vars modul.commands of+ then case compileCommands modName vars (modul ^. #commands) of Left cmdErrs -> pure (Left cmdErrs) Right cmdOps -> pure (Right (deduplicateDirs (concat allOps) ++ cmdOps)) else pure (Left (concat allErrors))@@ -71,17 +72,17 @@ } in (Map.insert key (occurrence + 1) counts, ops ++ [operation], errs) - shouldRun cmd = case cmd.condition of+ shouldRun cmd = case cmd ^. #condition of Nothing -> True Just expr -> evalExpr vars expr -- | Compile a single command, interpolating placeholders in @run@ and @workDir@. compileOneCommand :: Map VarName VarValue -> Command -> Either [Text] (Text, Maybe FilePath) compileOneCommand vars cmd =- case renderCommand cmd.run vars of+ case renderCommand (cmd ^. #run) vars of Left placeholderErrors -> Left (map formatPlaceholderError placeholderErrors) Right runText ->- case cmd.workDir of+ case cmd ^. #workDir of Nothing -> Right (runText, Nothing) Just wd -> case renderCommand wd vars of@@ -102,14 +103,14 @@ IO (Either [Text] [Operation]) compileStep baseDir modName vars step = do -- Evaluate the when condition- let shouldRun = case step.condition of+ let shouldRun = case step ^. #condition of Nothing -> True Just expr -> evalExpr vars expr if not shouldRun then pure (Right [])- else case step.patch of+ else case step ^. #patch of Just _ -> compilePatchStep baseDir vars modName step- Nothing -> case step.strategy of+ Nothing -> case step ^. #strategy of Copy -> compileCopyStep baseDir vars step Template -> compileTemplateStep baseDir vars step DhallText -> compileDhallTextStep baseDir vars step@@ -122,12 +123,12 @@ Step -> IO (Either [Text] [Operation]) compileCopyStep baseDir vars step = do- let srcPath = baseDir </> "files" </> step.src+ let srcPath = baseDir </> "files" </> (step ^. #src) result <- tryReadFile srcPath case result of Left err -> pure (Left [err]) Right content ->- case renderDestPath step.dest vars of+ case renderDestPath (step ^. #dest) vars of Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right dest ->@@ -140,7 +141,7 @@ Step -> IO (Either [Text] [Operation]) compileTemplateStep baseDir vars step = do- let srcPath = baseDir </> "files" </> step.src+ let srcPath = baseDir </> "files" </> (step ^. #src) result <- tryReadFile srcPath case result of Left err -> pure (Left [err])@@ -149,7 +150,7 @@ Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right rendered ->- case renderDestPath step.dest vars of+ case renderDestPath (step ^. #dest) vars of Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right dest ->@@ -162,7 +163,7 @@ Step -> IO (Either [Text] [Operation]) compileDhallTextStep baseDir vars step = do- let srcPath = baseDir </> "files" </> step.src+ let srcPath = baseDir </> "files" </> (step ^. #src) result <- tryReadFile srcPath case result of Left err -> pure (Left [err])@@ -175,7 +176,7 @@ case dhallResult of Left err -> pure (Left [err]) Right evaluated ->- case renderDestPath step.dest vars of+ case renderDestPath (step ^. #dest) vars of Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right dest ->@@ -189,7 +190,7 @@ Step -> IO (Either [Text] [Operation]) compileStructuredStep baseDir vars step = do- let srcPath = baseDir </> "files" </> step.src+ let srcPath = baseDir </> "files" </> (step ^. #src) result <- tryReadFile srcPath case result of Left err -> pure (Left [err])@@ -205,7 +206,7 @@ case dhallExprToJSON dhallExpr of Left err -> pure (Left [err]) Right jsonValue ->- case renderDestPath step.dest vars of+ case renderDestPath (step ^. #dest) vars of Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right dest ->@@ -226,8 +227,8 @@ Step -> IO (Either [Text] [Operation]) compilePatchStep baseDir vars modName step = do- let srcPath = baseDir </> "files" </> step.src- patchOp' = case step.patch of+ let srcPath = baseDir </> "files" </> (step ^. #src)+ patchOp' = case step ^. #patch of Just p -> p Nothing -> error "compilePatchStep called without patch op" result <- tryReadFile srcPath@@ -235,7 +236,7 @@ Left err -> pure (Left [err]) Right rawContent -> do -- Render content based on strategy- contentResult <- case step.strategy of+ contentResult <- case step ^. #strategy of Copy -> pure (Right rawContent) Template -> pure $ case renderTemplateText rawContent vars of@@ -254,11 +255,11 @@ case contentResult of Left errs -> pure (Left errs) Right content ->- case renderDestPath step.dest vars of+ case renderDestPath (step ^. #dest) vars of Left placeholderErrors -> pure (Left (map formatPlaceholderError placeholderErrors)) Right dest ->- pure (patchFileOps dest content patchOp' step.strategy modName)+ pure (patchFileOps dest content patchOp' (step ^. #strategy) modName) -- | Evaluate a Dhall expression and return the normalized AST. evaluateDhallExpr :: Text -> IO (Either Text (DhallCore.Expr Src Void))
src/Seihou/Engine/Preview.hs view
@@ -7,6 +7,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -26,15 +27,15 @@ -- | One line in the dry-run preview. data PreviewLine = FilePreview- { previewStatus :: FileStatus,- previewPath :: FilePath,- previewAnnotation :: Text,- previewModule :: Maybe ModuleName+ { status :: !FileStatus,+ path :: !FilePath,+ annotation :: !Text,+ module_ :: !(Maybe ModuleName) } | DirPreview FilePath | CommandPreview Text (Maybe ModuleName) | OrphanPreview FilePath ModuleName- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Build a structured preview from operations and an optional diff result. -- The ownership map tracks which module produced each file path.@@ -56,9 +57,9 @@ Just diff -> -- Only include orphans whose path is NOT produced by any operation let producedPaths = Set.fromList [p | op <- ops, Just p <- [destOfOp op]]- in [ OrphanPreview o.path o.moduleName- | o <- diff.orphaned,- not (Set.member o.path producedPaths)+ in [ OrphanPreview (o ^. #path) (o ^. #moduleName)+ | o <- diff ^. #orphaned,+ not (Set.member (o ^. #path) producedPaths) ] in opLines ++ orphanLines @@ -66,18 +67,18 @@ opToPreview :: Maybe DiffResult -> Map FilePath ModuleName -> Set Text -> Operation -> PreviewLine opToPreview mDiff ownerMap _ (WriteFileOp dest _ strat) = FilePreview- { previewStatus = lookupStatus dest mDiff,- previewPath = dest,- previewAnnotation = strategyName strat,- previewModule = Map.lookup dest ownerMap+ { status = lookupStatus dest mDiff,+ path = dest,+ annotation = strategyName strat,+ module_ = Map.lookup dest ownerMap } opToPreview _ _ _ (CreateDirOp path) = DirPreview path opToPreview mDiff ownerMap _ (CopyFileOp _ dest) = FilePreview- { previewStatus = lookupStatus dest mDiff,- previewPath = dest,- previewAnnotation = "copy",- previewModule = Map.lookup dest ownerMap+ { status = lookupStatus dest mDiff,+ path = dest,+ annotation = "copy",+ module_ = Map.lookup dest ownerMap } opToPreview _ _ commandsNeedingOwner RunCommandOp {command, moduleName} = CommandPreview@@ -85,21 +86,21 @@ (if Set.member command commandsNeedingOwner then Just moduleName else Nothing) opToPreview mDiff ownerMap _ (PatchFileOp dest _ _patchOp' _ modName') = FilePreview- { previewStatus = lookupStatus dest mDiff,- previewPath = dest,- previewAnnotation = "patch",- previewModule = Just modName'+ { status = lookupStatus dest mDiff,+ path = dest,+ annotation = "patch",+ module_ = Just modName' } -- | Look up a file's status in the diff result. lookupStatus :: FilePath -> Maybe DiffResult -> FileStatus lookupStatus _ Nothing = FsNew lookupStatus path (Just diff)- | any (\f -> f.path == path) diff.new = FsNew- | any (\f -> f.path == path) diff.modified = FsModified- | path `elem` diff.unchanged = FsUnchanged- | any (\f -> f.path == path) diff.conflicts = FsConflict- | any (\f -> f.path == path) diff.orphaned = FsOrphaned+ | any (\f -> f ^. #path == path) (diff ^. #new) = FsNew+ | any (\f -> f ^. #path == path) (diff ^. #modified) = FsModified+ | path `elem` (diff ^. #unchanged) = FsUnchanged+ | any (\f -> f ^. #path == path) (diff ^. #conflicts) = FsConflict+ | any (\f -> f ^. #path == path) (diff ^. #orphaned) = FsOrphaned | otherwise = FsUnknown -- | Render preview lines as plain text (no ANSI codes).@@ -111,14 +112,17 @@ where fileLines = [l | l@(FilePreview {}) <- lines'] nonFileLines = [l | l <- lines', not (isFileLine l)]- maxPathLen = maximum (0 : map (T.length . T.pack . (.previewPath)) fileLines)+ -- PreviewLine is a sum type and `path` lives only in FilePreview, so this+ -- is a pattern match rather than a #path read: generic-lens can only build+ -- a lens for a field that every constructor has.+ maxPathLen = maximum (0 : [T.length (T.pack p) | FilePreview {path = p} <- lines']) renderPlainLine :: Int -> PreviewLine -> Text renderPlainLine maxPath (FilePreview status path annotation mMod) = let pathText = T.pack path pathPad = T.replicate (maxPath - T.length pathText) " " modSuffix = case mMod of- Just mn -> ", " <> mn.unModuleName+ Just mn -> ", " <> (mn ^. #unModuleName) Nothing -> "" in " " <> statusTag status <> " " <> pathText <> pathPad <> " (" <> annotation <> modSuffix <> ")" renderPlainLine _ other = renderNonFileLine other@@ -129,9 +133,9 @@ renderNonFileLine (CommandPreview cmd mOwner) = " run " <> cmd <> ownerSuffix mOwner where- ownerSuffix = maybe "" (\owner -> " (" <> owner.unModuleName <> ")")+ ownerSuffix = maybe "" (\owner -> " (" <> owner ^. #unModuleName <> ")") renderNonFileLine (OrphanPreview path modName') =- " [orphaned] " <> T.pack path <> " (orphaned from " <> modName'.unModuleName <> ")"+ " [orphaned] " <> T.pack path <> " (orphaned from " <> modName' ^. #unModuleName <> ")" renderNonFileLine _ = "" isFileLine :: PreviewLine -> Bool@@ -165,7 +169,7 @@ where header = "Generation Plan ("- <> T.intercalate " + " (map (.unModuleName) moduleNames)+ <> T.intercalate " + " (map (^. #unModuleName) moduleNames) <> "):" varsSection =@@ -188,8 +192,8 @@ showVarValue (VInt n) = T.pack (show n) showVarValue (VList vs) = "[" <> T.intercalate ", " (map showVarValue vs) <> "]" - nFiles = length diff.new + length diff.modified- nConflicts = length diff.conflicts+ nFiles = length (diff ^. #new) + length (diff ^. #modified)+ nConflicts = length (diff ^. #conflicts) summaryText = " " <> T.pack (show nFiles)
src/Seihou/Engine/Reconcile.hs view
@@ -23,6 +23,7 @@ import Control.Monad (foldM) import Data.Foldable (traverse_)+import Data.Generics.Labels () import Data.List (foldl') import Data.Map.Strict qualified as Map import Data.Set qualified as Set@@ -41,20 +42,20 @@ -- The application set is path-specific: a batch may update several -- applications without every application contributing to every path. data DesiredFileOwner = DesiredFileOwner- { moduleName :: ModuleName,- applicationIds :: Set ApplicationId+ { moduleName :: !ModuleName,+ applicationIds :: !(Set ApplicationId) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | The final generated side after all operations for a path are replayed. data DesiredFile = DesiredFile- { path :: FilePath,- generatedContent :: Text,- moduleName :: ModuleName,- strategy :: Strategy,- applicationIds :: Set ApplicationId+ { path :: !FilePath,+ generatedContent :: !Text,+ moduleName :: !ModuleName,+ strategy :: !Strategy,+ applicationIds :: !(Set ApplicationId) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data ReconciliationReason = MissingTrustedBaseline@@ -66,28 +67,28 @@ -- | The disk snapshot used while planning. Applying verifies every snapshot -- before the first mutation, so a resolution cannot overwrite later edits. data ObservedFile = ObservedFile- { existed :: Bool,- contentHash :: Maybe SHA256+ { existed :: !Bool,+ contentHash :: !(Maybe SHA256) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | The exact generated ancestor and applied bytes a resolved action will -- publish. @writeToDisk@ is false for paths already containing those bytes. -- @recordedHash@ may intentionally remain the prior applied hash for a -- user-only edit that generation did not change. data PlannedFileState = PlannedFileState- { generatedBaseline :: Text,- appliedContent :: Text,- recordedHash :: SHA256,- writeToDisk :: Bool+ { generatedBaseline :: !Text,+ appliedContent :: !Text,+ recordedHash :: !SHA256,+ writeToDisk :: !Bool }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data ResolvedFileConflict = ResolvedFileConflict- { choice :: FileConflictChoice,- state :: PlannedFileState+ { choice :: !FileConflictChoice,+ state :: !PlannedFileState }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data FileReconciliation = FileCreate DesiredFile PlannedFileState ObservedFile@@ -109,11 +110,11 @@ deriving stock (Eq, Show) data ReconciliationPlan = ReconciliationPlan- { applicationIds :: Set ApplicationId,- files :: Map FilePath FileReconciliation,- requiredDirectories :: Set FilePath+ { applicationIds :: !(Set ApplicationId),+ files :: !(Map FilePath FileReconciliation),+ requiredDirectories :: !(Set FilePath) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data ReconciliationError = InvalidReconciliationPath FilePath Text@@ -143,16 +144,16 @@ deriving stock (Eq, Show) data ReconciliationSummary = ReconciliationSummary- { creates :: Int,- updates :: Int,- merged :: Int,- unchanged :: Int,- conflicts :: Int,- safeDeletes :: Int,- editedOrphans :: Int,- sharedOwnership :: Int+ { creates :: !Int,+ updates :: !Int,+ merged :: !Int,+ unchanged :: !Int,+ conflicts :: !Int,+ safeDeletes :: !Int,+ editedOrphans :: !Int,+ sharedOwnership :: !Int }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Production planner using the repository filesystem and baseline effects, -- with EP-65's Git-backed merge driver for dual edits.@@ -231,13 +232,14 @@ } data DesiredContext = DesiredContext- { desired :: DesiredFile,- current :: Maybe Text,- baseline :: Maybe Text,- priorRecord :: Maybe FileRecord,- observed :: ObservedFile,- missingTrustedBaseline :: Bool+ { desired :: !DesiredFile,+ current :: !(Maybe Text),+ baseline :: !(Maybe Text),+ priorRecord :: !(Maybe FileRecord),+ observed :: !ObservedFile,+ missingTrustedBaseline :: !Bool }+ deriving stock (Generic) validateInputs :: Set ApplicationId ->@@ -262,15 +264,15 @@ validateOwner selected ownerMap manifest path = case Map.lookup path ownerMap of Nothing -> Left (MissingDesiredOwner path) Just owner- | not (owner.applicationIds `Set.isSubsetOf` selected) ->- Left (DesiredOwnerOutsideSelection path (owner.applicationIds Set.\\ selected))- | otherwise -> case Map.lookup path manifest.files of+ | not ((owner ^. #applicationIds) `Set.isSubsetOf` selected) ->+ Left (DesiredOwnerOutsideSelection path ((owner ^. #applicationIds) Set.\\ selected))+ | otherwise -> case Map.lookup path (manifest ^. #files) of Nothing -> Right () Just record ->- let unselectedOwners = record.applicationIds Set.\\ selected+ let unselectedOwners = (record ^. #applicationIds) Set.\\ selected in if Set.null unselectedOwners then Right ()- else Left (SharedPathRequiresApplications path record.applicationIds)+ else Left (SharedPathRequiresApplications path (record ^. #applicationIds)) validateManagedPath :: FilePath -> Either ReconciliationError () validateManagedPath rawPath = case validateProjectRelativePath (T.pack rawPath) of@@ -316,7 +318,7 @@ m (Either ReconciliationError DesiredContext) materializeOne readDisk readCopy readStoredBaseline ownerMap manifest pathOperations = do let path = operationPath pathOperations- prior = Map.lookup path manifest.files+ prior = Map.lookup path (manifest ^. #files) owner = ownerMap Map.! path containsReplacement = any isReplacement pathOperations current <- readDisk path@@ -351,9 +353,9 @@ DesiredFile { path = path, generatedContent = generated,- moduleName = owner.moduleName,+ moduleName = owner ^. #moduleName, strategy = finalStrategy,- applicationIds = owner.applicationIds+ applicationIds = owner ^. #applicationIds } Right DesiredContext@@ -378,13 +380,13 @@ FileRecord -> Maybe Text -> m BaselineTrust-trustedBaseline readStored record current = case record.baseline of+trustedBaseline readStored record current = case record ^. #baseline of Just ref -> do result <- readStored ref pure (either (const Untrusted) (\content -> Trusted content False) result) Nothing -> pure $ case current of- Just content | hashContent content == record.hash -> Trusted content True+ Just content | hashContent content == record ^. #hash -> Trusted content True _ -> Untrusted applyGenerationOperation ::@@ -419,20 +421,20 @@ (Text -> Text -> Text -> m MergeOutcome) -> DesiredContext -> m (Either ReconciliationError (FilePath, FileReconciliation))-classifyDesired mergeContents context = case context.current of+classifyDesired mergeContents context = case context ^. #current of Nothing -> pure $ Right (path, classifyMissing) Just current- | context.missingTrustedBaseline ->+ | context ^. #missingTrustedBaseline -> pure $ Right (path, unresolved current current MissingTrustedBaseline)- | otherwise -> case context.baseline of+ | otherwise -> case context ^. #baseline of Nothing -> pure $ Right (path, unresolved current current MissingTrustedBaseline) Just baseline -> classifyPresent baseline current where- desired = context.desired- path = desired.path- generated = desired.generatedContent- prior = context.priorRecord- observed = context.observed+ desired = (context ^. #desired)+ path = (desired ^. #path)+ generated = (desired ^. #generatedContent)+ prior = (context ^. #priorRecord)+ observed = (context ^. #observed) classifyMissing = case prior of Nothing -> FileCreate desired (automaticState generated True) observed@@ -444,7 +446,7 @@ | current == baseline = pure (Right (path, FileUpdate desired (automaticState generated True) observed prior)) | generated == baseline =- let priorHash = maybe (hashContent current) (.hash) prior+ let priorHash = maybe (hashContent current) (^. #hash) prior state = PlannedFileState generated current priorHash False in pure (Right (path, FileUnchanged desired state observed prior)) | current == generated =@@ -456,7 +458,7 @@ fromMerge _ (MergeClean merged) = FileAutoMerge desired- (PlannedFileState generated merged (hashContent merged) (context.current /= Just merged))+ (PlannedFileState generated merged (hashContent merged) (context ^. #current /= Just merged)) observed prior fromMerge current (MergeConflicted markers) = unresolved current markers OverlappingEdits@@ -482,20 +484,20 @@ where candidates = [ (path, record)- | (path, record) <- Map.toList manifest.files,- Set.null (Set.intersection selected record.applicationIds) == False,+ | (path, record) <- Map.toList (manifest ^. #files),+ Set.null (Set.intersection selected (record ^. #applicationIds)) == False, Set.notMember path desiredPaths ] classify (path, record) = do current <- readDisk path let observed = observe current- remainingOwners = record.applicationIds Set.\\ selected+ remainingOwners = (record ^. #applicationIds) Set.\\ selected action | not (Set.null remainingOwners) = FileReleaseSharedOwnership path record observed | otherwise = case current of Nothing -> FileAlreadyAbsent path record observed Just content- | hashContent content == record.hash -> FileDeleteSafe path record observed+ | hashContent content == record ^. #hash -> FileDeleteSafe path record observed | otherwise -> FileOrphanEdited path record content observed Nothing pure (path, action) @@ -507,24 +509,24 @@ FileConflictChoice -> ReconciliationPlan -> Either ReconciliationError ReconciliationPlan-resolveFileConflict path choice plan = case Map.lookup path plan.files of+resolveFileConflict path choice plan = case Map.lookup path (plan ^. #files) of Nothing -> Left (ReconciliationPathNotFound path) Just (FileConflict _ _ _ _ _ _ _) | choice == AbortUpdate -> Left (UpdateAborted path) Just (FileConflict desired current markers reason observed prior _) -> let applied = case choice of- AcceptGenerated -> desired.generatedContent+ AcceptGenerated -> (desired ^. #generatedContent) KeepCurrent -> current WriteConflictMarkers -> markers AbortUpdate -> current state = PlannedFileState- { generatedBaseline = desired.generatedContent,+ { generatedBaseline = desired ^. #generatedContent, appliedContent = applied, recordedHash = hashContent applied,- writeToDisk = applied /= current || not observed.existed+ writeToDisk = applied /= current || not (observed ^. #existed) } resolved = FileConflict desired current markers reason observed prior (Just (ResolvedFileConflict choice state))- in Right (replacePlanFiles plan (Map.insert path resolved plan.files))+ in Right (replacePlanFiles plan (Map.insert path resolved (plan ^. #files))) Just _ -> Left (NotAFileConflict path) resolveEditedOrphan ::@@ -532,7 +534,7 @@ OrphanChoice -> ReconciliationPlan -> Either ReconciliationError ReconciliationPlan-resolveEditedOrphan path choice plan = case Map.lookup path plan.files of+resolveEditedOrphan path choice plan = case Map.lookup path (plan ^. #files) of Nothing -> Left (ReconciliationPathNotFound path) Just (FileOrphanEdited _ _ _ _ _) | choice == AbortOrphanUpdate -> Left (UpdateAborted path) Just (FileOrphanEdited orphanPath record content observed _) ->@@ -542,12 +544,12 @@ ( Map.insert path (FileOrphanEdited orphanPath record content observed (Just choice))- plan.files+ (plan ^. #files) ) Just _ -> Left (NotAnEditedOrphan path) reconciliationSummary :: ReconciliationPlan -> ReconciliationSummary-reconciliationSummary = foldl' count emptySummary . Map.elems . (.files)+reconciliationSummary = foldl' count emptySummary . Map.elems . (^. #files) where emptySummary = ReconciliationSummary 0 0 0 0 0 0 0 0 count summary reconciliation = case reconciliation of@@ -556,7 +558,7 @@ FileAutoMerge _ _ _ _ -> addMerge summary FileUnchanged _ _ _ _ -> addUnchanged summary FileConflict _ _ _ _ _ _ Nothing -> addConflict summary- FileConflict _ _ _ _ _ _ (Just resolved) -> case resolved.choice of+ FileConflict _ _ _ _ _ _ (Just resolved) -> case resolved ^. #choice of AcceptGenerated -> addUpdate summary KeepCurrent -> addMerge summary WriteConflictMarkers -> addMerge summary@@ -578,16 +580,16 @@ replacePlanFiles :: ReconciliationPlan -> Map FilePath FileReconciliation -> ReconciliationPlan replacePlanFiles plan newFiles = ReconciliationPlan- { applicationIds = plan.applicationIds,+ { applicationIds = plan ^. #applicationIds, files = newFiles,- requiredDirectories = plan.requiredDirectories+ requiredDirectories = plan ^. #requiredDirectories } reconciliationMutationPaths :: ReconciliationPlan -> Set FilePath-reconciliationMutationPaths = Map.keysSet . (.files)+reconciliationMutationPaths = Map.keysSet . (^. #files) unresolvedPaths :: ReconciliationPlan -> Set FilePath-unresolvedPaths plan = Map.keysSet (Map.filter unresolved plan.files)+unresolvedPaths plan = Map.keysSet (Map.filter unresolved (plan ^. #files)) where unresolved (FileConflict _ _ _ _ _ _ Nothing) = True unresolved (FileOrphanEdited _ _ _ _ Nothing) = True
src/Seihou/Engine/Remove.hs view
@@ -13,6 +13,7 @@ where import Control.Monad (foldM)+import Data.Generics.Labels () import Data.List (nub, sortBy) import Data.Map.Strict qualified as Map import Data.Ord (Down (..))@@ -44,10 +45,10 @@ -- | A plan describing what files to remove for a given module. data RemovalPlan = RemovalPlan- { targetModule :: ModuleName,- files :: [RemovalFile]+ { targetModule :: !ModuleName,+ files :: ![RemovalFile] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- ============================================================ -- New step-based removal types@@ -77,10 +78,10 @@ -- | A removal plan built from declared removal steps. data ExecutedRemovalPlan = ExecutedRemovalPlan- { targetModule :: ModuleName,- ops :: [RemovalOp]+ { targetModule :: !ModuleName,+ ops :: ![RemovalOp] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Errors that prevent removal. data RemovalError@@ -108,7 +109,7 @@ case findApplied manifest modName of Nothing -> pure (Left (ModuleNotApplied modName)) Just am- | Nothing <- am.removal -> pure (Left (ModuleNotRemovable modName))+ | Nothing <- am ^. #removal -> pure (Left (ModuleNotRemovable modName)) | otherwise -> do let ownedFiles = moduleFiles manifest modName classified <- mapM classifyForRemoval ownedFiles@@ -127,7 +128,7 @@ let toDelete = filesToDelete plan keepSet mapM_ removeFile toDelete cleanupEmptyDirs toDelete- pure (removeFromManifest manifest plan.targetModule now)+ pure (removeFromManifest manifest (plan ^. #targetModule) now) -- ============================================================ -- New step-based removal engine@@ -145,8 +146,8 @@ case findApplied manifest modName of Nothing -> pure (Left (ModuleNotApplied modName)) Just _ -> do- stepResults <- mapM (buildStepOp manifest modName) removal.removalSteps- let cmdResults = map buildCommandOp removal.removalCommands+ stepResults <- mapM (buildStepOp manifest modName) (removal ^. #steps)+ let cmdResults = map buildCommandOp (removal ^. #commands) pure $ do stepOps <- sequence stepResults cmdOps <- sequence cmdResults@@ -163,31 +164,31 @@ ModuleName -> RemovalStep -> Eff es (Either RemovalError RemovalOp)-buildStepOp manifest _modName step = case step.action of+buildStepOp manifest _modName step = case step ^. #action of RemoveFileAction ->- case validateRemovalPath "remove-file destination" step.dest of+ case validateRemovalPath "remove-file destination" (step ^. #dest) of Left err -> pure (Left err) Right path -> do status <- classifyFileStatus manifest path pure (Right (DeleteFileOp path status)) RemoveSectionAction -> pure $- case validateRemovalPath "remove-section destination" step.dest of+ case validateRemovalPath "remove-section destination" (step ^. #dest) of Left err -> Left err Right path -> Right (StripSectionOp path) RewriteFileAction -> pure $ do- dest <- validateRemovalPath "rewrite-file destination" step.dest- src <- case step.src of+ dest <- validateRemovalPath "rewrite-file destination" (step ^. #dest)+ src <- case step ^. #src of Just s -> validateRemovalPath "rewrite-file source" (T.pack s) Nothing -> Left (RemovalUnsafePath "rewrite-file source" "" "path must not be empty") Right (RewriteOp dest src) buildCommandOp :: Command -> Either RemovalError RemovalOp buildCommandOp command =- case traverse (validateRemovalPath "remove-command workDir") command.workDir of+ case traverse (validateRemovalPath "remove-command workDir") (command ^. #workDir) of Left err -> Left err- Right safeWorkDir -> Right (RemovalCommandOp command.run (fmap T.pack safeWorkDir))+ Right safeWorkDir -> Right (RemovalCommandOp (command ^. #run) (fmap T.pack safeWorkDir)) validateRemovalPath :: Text -> Text -> Either RemovalError FilePath validateRemovalPath label path =@@ -205,12 +206,12 @@ exists <- doesFileExist path if not exists then pure RFGone- else case Map.lookup path manifest.files of+ else case Map.lookup path (manifest ^. #files) of Nothing -> pure RFSafe -- Not in manifest, treat as safe to delete Just rec -> do content <- readFileText path let diskHash = hashContent content- if diskHash == rec.hash+ if diskHash == rec ^. #hash then pure RFSafe else pure RFConflict @@ -223,8 +224,8 @@ UTCTime -> Eff es Manifest executeRemovalOps manifest plan keepSet now = do- let modName = plan.targetModule- deletedPaths <- foldM (execOp modName keepSet) [] plan.ops+ let modName = (plan ^. #targetModule)+ deletedPaths <- foldM (execOp modName keepSet) [] (plan ^. #ops) cleanupEmptyDirs deletedPaths pure (removeFromManifest manifest modName now) @@ -283,7 +284,7 @@ -- | Find an applied module by name. findApplied :: Manifest -> ModuleName -> Maybe AppliedModule findApplied manifest modName =- case filter (\am -> am.name == modName) manifest.modules of+ case filter (\am -> am ^. #name == modName) (manifest ^. #modules) of (am : _) -> Just am [] -> Nothing @@ -291,8 +292,8 @@ moduleFiles :: Manifest -> ModuleName -> [(FilePath, FileRecord)] moduleFiles manifest modName = [ (path, rec)- | (path, rec) <- Map.toList manifest.files,- rec.moduleName == modName+ | (path, rec) <- Map.toList (manifest ^. #files),+ rec ^. #moduleName == modName ] -- | Classify a single file for removal (legacy).@@ -304,7 +305,7 @@ else do content <- readFileText path let diskHash = hashContent content- if diskHash == rec.hash+ if diskHash == rec ^. #hash then pure (RemovalSafe path) else pure (RemovalConflict path) @@ -312,7 +313,7 @@ filesToDelete :: RemovalPlan -> Set FilePath -> [FilePath] filesToDelete plan keepSet = [ path- | rf <- plan.files,+ | rf <- plan ^. #files, let path = removalFilePath rf, shouldDelete rf, not (Set.member path keepSet)@@ -350,7 +351,9 @@ removeFromManifest :: Manifest -> ModuleName -> UTCTime -> Manifest removeFromManifest manifest modName now = manifest- { modules = filter (\am -> am.name /= modName) manifest.modules,- files = Map.filter (\rec -> rec.moduleName /= modName) manifest.files,- genAt = now- }+ & #modules+ %~ filter (\am -> am ^. #name /= modName)+ & #files+ %~ Map.filter (\rec -> rec ^. #moduleName /= modName)+ & #genAt+ .~ now
src/Seihou/Engine/Section.hs view
@@ -8,28 +8,29 @@ ) where +import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Types import Seihou.Prelude -- | A section marker identifies content contributed by a module. data SectionMarker = SectionMarker- { sectionPrefix :: Text,- sectionModule :: ModuleName+ { prefix :: !Text,+ module_ :: !ModuleName }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Render an opening section marker line. -- Result: @"# --- seihou:haskell-base ---\\n"@ renderSectionOpen :: SectionMarker -> Text renderSectionOpen marker =- marker.sectionPrefix <> " --- seihou:" <> marker.sectionModule.unModuleName <> " ---\n"+ marker ^. #prefix <> " --- seihou:" <> marker ^. #module_ . #unModuleName <> " ---\n" -- | Render a closing section marker line. -- Result: @"# --- /seihou:haskell-base ---\\n"@ renderSectionClose :: SectionMarker -> Text renderSectionClose marker =- marker.sectionPrefix <> " --- /seihou:" <> marker.sectionModule.unModuleName <> " ---\n"+ marker ^. #prefix <> " --- /seihou:" <> marker ^. #module_ . #unModuleName <> " ---\n" -- | Wrap content in section markers. wrapInSection :: SectionMarker -> Text -> Text@@ -49,7 +50,7 @@ -- unchanged. Cleans up resulting double blank lines. removeSection :: ModuleName -> Text -> Text -> Text removeSection modName prefix content =- let marker = SectionMarker {sectionPrefix = prefix, sectionModule = modName}+ let marker = SectionMarker {prefix = prefix, module_ = modName} openTag = T.stripEnd (renderSectionOpen marker) closeTag = T.stripEnd (renderSectionClose marker) ls = T.lines content@@ -85,7 +86,7 @@ applyTextPatch PrependFile _ _ existing new = Right (ensureTrailingNewline new <> existing) applyTextPatch AppendSection modName prefix existing new =- let marker = SectionMarker {sectionPrefix = prefix, sectionModule = modName}+ let marker = SectionMarker {prefix = prefix, module_ = modName} in Right (ensureTrailingNewline existing <> wrapInSection marker new) applyTextPatch AppendLineIfAbsent _ _ existing new = let existingLines = map T.stripEnd (T.lines existing)
src/Seihou/Engine/Template.hs view
@@ -198,14 +198,15 @@ -- (i.e. the body plus whatever follows the matching @{{/if}}@); -- @expr@ is the raw expression text. FoundIf- { foundBefore :: Text,- foundAfter :: Text,- foundExpr :: Text+ { before :: !Text,+ after :: !Text,+ expr :: !Text } | -- | A @{{#else}}@ or @{{/if}}@ encountered before any matching -- @{{#if}}@ at the current depth. The 'Int' is the line offset -- (0-based) from the start of the scanned region. FoundOrphan Text Int+ deriving stock (Generic) -- | Scan @input@ for the next block token at the outer level (i.e. for -- the purpose of locating the next @{{#if}}@ opener, or an orphan if@@ -225,9 +226,9 @@ let expr = T.strip exprRaw afterCloseTag = T.drop 2 rest -- skip "}}" in FoundIf- { foundBefore = before,- foundAfter = afterCloseTag,- foundExpr = expr+ { before = before,+ after = afterCloseTag,+ expr = expr } | "{{/if}}" `T.isPrefixOf` t = FoundOrphan "{{/if}}" (lineOffset (T.take pos input))
src/Seihou/Engine/UpdateTransaction.hs view
@@ -17,6 +17,7 @@ import Data.Aeson qualified as Aeson import Data.ByteString.Lazy qualified as LBS import Data.Foldable (traverse_)+import Data.Generics.Labels () import Data.List (sortOn) import Data.Map.Strict qualified as Map import Data.Set qualified as Set@@ -38,11 +39,11 @@ import System.IO.Temp (createTempDirectory, openTempFile) data UpdateTransaction = UpdateTransaction- { projectRoot :: FilePath,- transactionDirectory :: FilePath,- targets :: Set FilePath+ { projectRoot :: !FilePath,+ transactionDirectory :: !FilePath,+ targets :: !(Set FilePath) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data TransactionError = InvalidTransactionPath FilePath Text@@ -57,25 +58,25 @@ deriving stock (Eq, Show) data JournalEntry = JournalEntry- { targetPath :: FilePath,- backupFile :: Maybe FilePath+ { targetPath :: !FilePath,+ backupFile :: !(Maybe FilePath) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) data JournalMetadata = JournalMetadata- { journalVersion :: Int,- createdAt :: UTCTime,- entries :: [JournalEntry],- newDirectories :: [FilePath],- expectedManifest :: Maybe Manifest+ { journalVersion :: !Int,+ createdAt :: !UTCTime,+ entries :: ![JournalEntry],+ newDirectories :: ![FilePath],+ expectedManifest :: !(Maybe Manifest) }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) instance ToJSON JournalEntry where toJSON entry = Aeson.object- [ "path" .= entry.targetPath,- "backup" .= entry.backupFile+ [ "path" .= (entry ^. #targetPath),+ "backup" .= (entry ^. #backupFile) ] instance FromJSON JournalEntry where@@ -85,11 +86,11 @@ instance ToJSON JournalMetadata where toJSON metadata = Aeson.object- [ "version" .= metadata.journalVersion,- "createdAt" .= metadata.createdAt,- "entries" .= metadata.entries,- "newDirectories" .= metadata.newDirectories,- "expectedManifest" .= metadata.expectedManifest+ [ "version" .= (metadata ^. #journalVersion),+ "createdAt" .= (metadata ^. #createdAt),+ "entries" .= (metadata ^. #entries),+ "newDirectories" .= (metadata ^. #newDirectories),+ "expectedManifest" .= (metadata ^. #expectedManifest) ] instance FromJSON JournalMetadata where@@ -118,10 +119,10 @@ initializeJournal :: UpdateTransaction -> [FilePath] -> IO () initializeJournal transaction safeTargets = do- let backupDirectory = transaction.transactionDirectory </> "backups"+ let backupDirectory = transaction ^. #transactionDirectory </> "backups" Directory.createDirectoryIfMissing True backupDirectory entries <- forM (zip [0 :: Int ..] safeTargets) $ \(index, relativePath) -> do- let fullPath = transaction.projectRoot </> relativePath+ let fullPath = transaction ^. #projectRoot </> relativePath backupName = show index <> ".txt" backupPath = backupDirectory </> backupName exists <- Directory.doesFileExist fullPath@@ -130,10 +131,10 @@ TIO.readFile fullPath >>= TIO.writeFile backupPath pure (JournalEntry relativePath (Just backupName)) else pure (JournalEntry relativePath Nothing)- missingDirectories <- missingParentDirectories transaction.projectRoot safeTargets+ missingDirectories <- missingParentDirectories (transaction ^. #projectRoot) safeTargets now <- getCurrentTime writeJournal- transaction.transactionDirectory+ (transaction ^. #transactionDirectory) JournalMetadata { journalVersion = 1, createdAt = now,@@ -188,22 +189,22 @@ Right () -> pure (Right candidate) transactionPreflight :: UpdateTransaction -> ReconciliationPlan -> IO (Either TransactionError ())-transactionPreflight transaction plan = case traverse validateTransactionPath (Set.toAscList plan.requiredDirectories) of+transactionPreflight transaction plan = case traverse validateTransactionPath (Set.toAscList (plan ^. #requiredDirectories)) of Left err -> pure (Left err) Right _ | not (Set.null unjournaled) -> pure (Left (TransactionUnjournaledPaths unjournaled)) | not (Set.null unresolved) -> pure (Left (TransactionUnresolvedPaths unresolved)) | otherwise -> verifyObservedFiles transaction plan where- unjournaled = Map.keysSet plan.files Set.\\ transaction.targets+ unjournaled = Map.keysSet (plan ^. #files) Set.\\ (transaction ^. #targets) unresolved = unresolvedPaths plan verifyObservedFiles :: UpdateTransaction -> ReconciliationPlan -> IO (Either TransactionError ())-verifyObservedFiles transaction plan = go (Map.toAscList plan.files)+verifyObservedFiles transaction plan = go (Map.toAscList (plan ^. #files)) where go [] = pure (Right ()) go ((path, reconciliation) : rest) = do- current <- observeDiskFile (transaction.projectRoot </> path)+ current <- observeDiskFile (transaction ^. #projectRoot </> path) let planned = reconciliationObservation reconciliation if current == planned then go rest@@ -232,23 +233,23 @@ prepareCandidateManifest :: UpdateTransaction -> ReconciliationPlan -> Manifest -> IO Manifest prepareCandidateManifest transaction plan manifest = do- nextFiles <- foldM applyManifestAction manifest.files (Map.toAscList plan.files)+ nextFiles <- foldM applyManifestAction (manifest ^. #files) (Map.toAscList (plan ^. #files)) pure (replaceManifestFiles manifest nextFiles) where applyManifestAction files (path, reconciliation) = case desiredState reconciliation of Just (desired, state) -> do- baseline <- writeBaselineBlob transaction.projectRoot state.generatedBaseline+ baseline <- writeBaselineBlob (transaction ^. #projectRoot) (state ^. #generatedBaseline) let record = FileRecord- { hash = state.recordedHash,- moduleName = desired.moduleName,- strategy = desired.strategy,- generatedAt = manifest.genAt,+ { hash = state ^. #recordedHash,+ moduleName = desired ^. #moduleName,+ strategy = desired ^. #strategy,+ generatedAt = manifest ^. #genAt, baseline = Just baseline,- applicationIds = desired.applicationIds+ applicationIds = desired ^. #applicationIds } pure (Map.insert path record files)- Nothing -> pure (applyOrphanManifestAction plan.applicationIds reconciliation files)+ Nothing -> pure (applyOrphanManifestAction (plan ^. #applicationIds) reconciliation files) desiredState :: FileReconciliation -> Maybe (DesiredFile, PlannedFileState) desiredState reconciliation = case reconciliation of@@ -256,7 +257,7 @@ FileUpdate desired state _ _ -> Just (desired, state) FileAutoMerge desired state _ _ -> Just (desired, state) FileUnchanged desired state _ _ -> Just (desired, state)- FileConflict desired _ _ _ _ _ (Just resolution) -> Just (desired, resolution.state)+ FileConflict desired _ _ _ _ _ (Just resolution) -> Just (desired, resolution ^. #state) _ -> Nothing applyOrphanManifestAction ::@@ -268,14 +269,14 @@ FileDeleteSafe path _ _ -> Map.delete path files FileAlreadyAbsent path _ _ -> Map.delete path files FileReleaseSharedOwnership path record _ ->- let remaining = record.applicationIds Set.\\ selected+ let remaining = (record ^. #applicationIds) Set.\\ selected in if Set.null remaining then Map.delete path files else Map.insert path (replaceRecordApplications record remaining) files FileOrphanEdited path _ _ _ (Just DeleteEditedOrphan) -> Map.delete path files FileOrphanEdited _ _ _ _ (Just RetainTrackedOrphan) -> files FileOrphanEdited path record _ _ (Just DetachAndKeepOrphan) ->- let remaining = record.applicationIds Set.\\ selected+ let remaining = (record ^. #applicationIds) Set.\\ selected in if Set.null remaining then Map.delete path files else Map.insert path (replaceRecordApplications record remaining) files@@ -284,26 +285,26 @@ replaceRecordApplications :: FileRecord -> Set ApplicationId -> FileRecord replaceRecordApplications record owners = FileRecord- { hash = record.hash,- moduleName = record.moduleName,- strategy = record.strategy,- generatedAt = record.generatedAt,- baseline = record.baseline,+ { hash = record ^. #hash,+ moduleName = record ^. #moduleName,+ strategy = record ^. #strategy,+ generatedAt = record ^. #generatedAt,+ baseline = record ^. #baseline, applicationIds = owners } replaceManifestFiles :: Manifest -> Map FilePath FileRecord -> Manifest replaceManifestFiles manifest nextFiles = Manifest- { version = manifest.version,- genAt = manifest.genAt,- modules = manifest.modules,- vars = manifest.vars,+ { version = manifest ^. #version,+ genAt = manifest ^. #genAt,+ modules = manifest ^. #modules,+ vars = manifest ^. #vars, files = nextFiles,- applications = manifest.applications,- recipe = manifest.recipe,- blueprint = manifest.blueprint,- blueprintMigrations = manifest.blueprintMigrations+ applications = manifest ^. #applications,+ recipe = manifest ^. #recipe,+ blueprint = manifest ^. #blueprint,+ blueprintMigrations = manifest ^. #blueprintMigrations } writeBaselineBlob :: FilePath -> Text -> IO BaselineRef@@ -313,50 +314,50 @@ updateJournalForPlan :: UpdateTransaction -> ReconciliationPlan -> Manifest -> IO (Either TransactionError ()) updateJournalForPlan transaction plan candidate = do- metadataResult <- readJournal transaction.transactionDirectory+ metadataResult <- readJournal (transaction ^. #transactionDirectory) case metadataResult of Left err -> pure (Left err) Right metadata -> do missingDirectories <- filterMIO- (fmap not . Directory.doesDirectoryExist . (transaction.projectRoot </>))+ (fmap not . Directory.doesDirectoryExist . (transaction ^. #projectRoot </>)) ( Set.toAscList . Set.fromList $ concatMap (\path -> path : relativeParents path)- (Set.toAscList plan.requiredDirectories)+ (Set.toAscList (plan ^. #requiredDirectories)) ) let updated = setExpectedManifestAndDirectories metadata missingDirectories candidate- result <- try @SomeException $ writeJournal transaction.transactionDirectory updated+ result <- try @SomeException $ writeJournal (transaction ^. #transactionDirectory) updated pure $ first (\err -> TransactionApplyFailed (exceptionText err) Nothing) result setExpectedManifestAndDirectories :: JournalMetadata -> [FilePath] -> Manifest -> JournalMetadata setExpectedManifestAndDirectories metadata additionalDirectories candidate = JournalMetadata- { journalVersion = metadata.journalVersion,- createdAt = metadata.createdAt,- entries = metadata.entries,+ { journalVersion = metadata ^. #journalVersion,+ createdAt = metadata ^. #createdAt,+ entries = metadata ^. #entries, newDirectories = sortOn pathDepth . Set.toList $- Set.fromList (metadata.newDirectories <> additionalDirectories),+ Set.fromList (metadata ^. #newDirectories <> additionalDirectories), expectedManifest = Just candidate } applyMutations :: (Int -> IO ()) -> UpdateTransaction -> ReconciliationPlan -> IO () applyMutations afterMutation transaction plan = do- forM_ (Set.toAscList plan.requiredDirectories) $ \relativePath ->- Directory.createDirectoryIfMissing True (transaction.projectRoot </> relativePath)- _ <- foldM applyOne (0 :: Int) (Map.toAscList plan.files)+ forM_ (Set.toAscList (plan ^. #requiredDirectories)) $ \relativePath ->+ Directory.createDirectoryIfMissing True (transaction ^. #projectRoot </> relativePath)+ _ <- foldM applyOne (0 :: Int) (Map.toAscList (plan ^. #files)) pure () where applyOne count (path, reconciliation) = case mutationFor reconciliation of NoMutation -> pure count WriteMutation content -> do- atomicWriteText (transaction.projectRoot </> path) content+ atomicWriteText (transaction ^. #projectRoot </> path) content let next = count + 1 afterMutation next pure next DeleteMutation -> do- let fullPath = transaction.projectRoot </> path+ let fullPath = transaction ^. #projectRoot </> path exists <- Directory.doesFileExist fullPath when exists (Directory.removeFile fullPath) let next = count + 1@@ -368,7 +369,7 @@ mutationFor :: FileReconciliation -> FileMutation mutationFor reconciliation = case desiredState reconciliation of Just (_, state)- | state.writeToDisk -> WriteMutation state.appliedContent+ | state ^. #writeToDisk -> WriteMutation (state ^. #appliedContent) | otherwise -> NoMutation Nothing -> case reconciliation of FileDeleteSafe _ _ _ -> DeleteMutation@@ -377,25 +378,25 @@ rollbackUpdateTransaction :: UpdateTransaction -> IO (Either TransactionError ()) rollbackUpdateTransaction transaction = do- metadataResult <- readJournal transaction.transactionDirectory+ metadataResult <- readJournal (transaction ^. #transactionDirectory) case metadataResult of Left err -> pure (Left err) Right metadata -> do result <- try @SomeException $ do- forM_ metadata.entries (restoreEntry transaction)- removeNewDirectories transaction.projectRoot metadata.newDirectories- cleanupDirectory transaction.transactionDirectory+ forM_ (metadata ^. #entries) (restoreEntry transaction)+ removeNewDirectories (transaction ^. #projectRoot) (metadata ^. #newDirectories)+ cleanupDirectory (transaction ^. #transactionDirectory) pure $ first (TransactionRollbackFailed . exceptionText) result restoreEntry :: UpdateTransaction -> JournalEntry -> IO () restoreEntry transaction entry = do- let target = transaction.projectRoot </> entry.targetPath- case entry.backupFile of+ let target = transaction ^. #projectRoot </> (entry ^. #targetPath)+ case entry ^. #backupFile of Nothing -> do exists <- Directory.doesFileExist target when exists (Directory.removeFile target) Just backupName -> do- let backupPath = transaction.transactionDirectory </> "backups" </> backupName+ let backupPath = transaction ^. #transactionDirectory </> "backups" </> backupName content <- TIO.readFile backupPath atomicWriteText target content @@ -411,7 +412,7 @@ completeUpdateTransaction :: UpdateTransaction -> IO (Either TransactionError ()) completeUpdateTransaction transaction = do- result <- try @SomeException (cleanupDirectory transaction.transactionDirectory)+ result <- try @SomeException (cleanupDirectory (transaction ^. #transactionDirectory)) pure $ first (TransactionCompletionFailed . exceptionText) result -- | Replace the recovery commit marker with the exact manifest the caller is@@ -420,19 +421,19 @@ -- candidate before the atomic manifest write. setUpdateTransactionExpectedManifest :: UpdateTransaction -> Manifest -> IO (Either TransactionError ()) setUpdateTransactionExpectedManifest transaction expected = do- metadataResult <- readJournal transaction.transactionDirectory+ metadataResult <- readJournal (transaction ^. #transactionDirectory) case metadataResult of Left err -> pure (Left err) Right metadata -> do let updated = JournalMetadata- { journalVersion = metadata.journalVersion,- createdAt = metadata.createdAt,- entries = metadata.entries,- newDirectories = metadata.newDirectories,+ { journalVersion = metadata ^. #journalVersion,+ createdAt = metadata ^. #createdAt,+ entries = metadata ^. #entries,+ newDirectories = metadata ^. #newDirectories, expectedManifest = Just expected }- result <- try @SomeException (writeJournal transaction.transactionDirectory updated)+ result <- try @SomeException (writeJournal (transaction ^. #transactionDirectory) updated) pure $ first (\err -> TransactionApplyFailed (exceptionText err) Nothing) result recoverIncompleteTransactions :: FilePath -> IO [Either TransactionError ()]@@ -460,7 +461,7 @@ Left quarantineError -> Left quarantineError Right () -> Left err Right metadata -> do- committed <- manifestMatches projectRoot metadata.expectedManifest+ committed <- manifestMatches projectRoot (metadata ^. #expectedManifest) if committed then do result <- try @SomeException (cleanupDirectory transactionDirectory)@@ -470,7 +471,7 @@ UpdateTransaction { projectRoot = projectRoot, transactionDirectory = transactionDirectory,- targets = Set.fromList (map (.targetPath) metadata.entries)+ targets = Set.fromList (map (^. #targetPath) (metadata ^. #entries)) } manifestMatches :: FilePath -> Maybe Manifest -> IO Bool@@ -507,9 +508,9 @@ validateJournal :: JournalMetadata -> Either Text JournalMetadata validateJournal metadata = do- traverse_ (validateJournalEntry . (.targetPath)) metadata.entries- traverse_ validateBackupName [name | JournalEntry _ (Just name) <- metadata.entries]- traverse_ (first renderTransactionPathError . validateTransactionPath) metadata.newDirectories+ traverse_ (validateJournalEntry . (^. #targetPath)) (metadata ^. #entries)+ traverse_ validateBackupName [name | JournalEntry _ (Just name) <- metadata ^. #entries]+ traverse_ (first renderTransactionPathError . validateTransactionPath) (metadata ^. #newDirectories) pure metadata where validateJournalEntry path = first renderTransactionPathError (validateTransactionPath path)
src/Seihou/Engine/Validate.hs view
@@ -8,6 +8,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Maybe (isNothing, mapMaybe) import Data.Set qualified as Set@@ -40,21 +41,21 @@ -- | A single diagnostic check with its result. data DiagCheck = DiagCheck- { diagLabel :: Text,- diagSeverity :: DiagSeverity,- diagDetails :: [Text]+ { label :: !Text,+ severity :: !DiagSeverity,+ details :: ![Text] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | A complete validation report for a module. data ValidateReport = ValidateReport- { reportModule :: Module,- reportPath :: FilePath,- reportDhallOk :: Bool,- reportDhallError :: Maybe Text,- reportChecks :: [DiagCheck]+ { module_ :: !Module,+ path :: !FilePath,+ dhallOk :: !Bool,+ dhallError :: !(Maybe Text),+ checks :: ![DiagCheck] }- deriving stock (Eq, Show)+ deriving stock (Eq, Generic, Show) -- | Build a structured validation report. When the first argument is True, -- lint warnings are included after the core checks.@@ -94,24 +95,24 @@ else [] pure ValidateReport- { reportModule = m,- reportPath = baseDir,- reportDhallOk = True,- reportDhallError = Nothing,- reportChecks = coreChecks ++ lintChecks+ { module_ = m,+ path = baseDir,+ dhallOk = True,+ dhallError = Nothing,+ checks = coreChecks ++ lintChecks } -- | Whether the report contains any errors (DiagError with non-empty details). reportHasErrors :: ValidateReport -> Bool reportHasErrors report =- not report.reportDhallOk- || any (\c -> c.diagSeverity == DiagError && not (null c.diagDetails)) report.reportChecks+ not (report ^. #dhallOk)+ || any (\c -> c ^. #severity == DiagError && not (null (c ^. #details))) (report ^. #checks) -- | Render the report as plain text (no ANSI codes). renderReportPlain :: ValidateReport -> Text renderReportPlain report = T.unlines $- [ "Validating module at " <> T.pack report.reportPath <> "...",+ [ "Validating module at " <> T.pack (report ^. #path) <> "...", "" ] ++ dhallLine@@ -120,46 +121,46 @@ ++ [""] ++ [resultLine] where- m = report.reportModule+ m = (report ^. #module_) dhallLine =- if report.reportDhallOk+ if report ^. #dhallOk then [" \x2713 module.dhall evaluates successfully"] else [" \x2717 module.dhall failed to evaluate"]- ++ case report.reportDhallError of+ ++ case report ^. #dhallError of Just errText -> [" " <> errText] Nothing -> [] summaryLines =- if report.reportDhallOk+ if report ^. #dhallOk then- [ " \x2713 Module name: " <> m.name.unModuleName,- " \x2713 " <> T.pack (show (length m.vars)) <> " variables declared",- " \x2713 " <> T.pack (show (length m.prompts)) <> " prompts defined",- " \x2713 " <> T.pack (show (length m.steps)) <> " steps defined"+ [ " \x2713 Module name: " <> m ^. #name . #unModuleName,+ " \x2713 " <> T.pack (show (length (m ^. #vars))) <> " variables declared",+ " \x2713 " <> T.pack (show (length (m ^. #prompts))) <> " prompts defined",+ " \x2713 " <> T.pack (show (length (m ^. #steps))) <> " steps defined" ] else [] - checkLines = concatMap renderCheck report.reportChecks+ checkLines = concatMap renderCheck (report ^. #checks) renderCheck c- | null c.diagDetails =- [" \x2713 " <> c.diagLabel]- | c.diagSeverity == DiagWarning =- (" \x26A0 " <> c.diagLabel) : map (\d -> " " <> d) c.diagDetails+ | null (c ^. #details) =+ [" \x2713 " <> c ^. #label]+ | c ^. #severity == DiagWarning =+ (" \x26A0 " <> c ^. #label) : map (\d -> " " <> d) (c ^. #details) | otherwise =- (" \x2717 " <> c.diagLabel) : map (\d -> " " <> d) c.diagDetails+ (" \x2717 " <> c ^. #label) : map (\d -> " " <> d) (c ^. #details) errorCount = length [ ()- | c <- report.reportChecks,- c.diagSeverity == DiagError,- not (null c.diagDetails)+ | c <- report ^. #checks,+ c ^. #severity == DiagError,+ not (null (c ^. #details)) ] - dhallFailed = not report.reportDhallOk+ dhallFailed = not (report ^. #dhallOk) totalErrors = errorCount + (if dhallFailed then 1 else 0) @@ -167,7 +168,7 @@ | totalErrors > 0 = T.pack (show totalErrors) <> " error(s) found. Module is invalid." | otherwise =- "Module '" <> m.name.unModuleName <> "' is valid."+ "Module '" <> m ^. #name . #unModuleName <> "' is valid." -- Lint checks @@ -176,40 +177,40 @@ lintUnusedVars m = let destRefs = Set.fromList $- concatMap (extractPlaceholders . (.dest)) m.steps+ concatMap (extractPlaceholders . (^. #dest)) (m ^. #steps) exportRefs = Set.fromList $- map (.var.unVarName) m.exports+ map (^. #var . #unVarName) (m ^. #exports) promptRefs = Set.fromList $- map (.var.unVarName) m.prompts+ map (^. #var . #unVarName) (m ^. #prompts) allRefs = Set.unions [destRefs, exportRefs, promptRefs] in mapMaybe ( \v ->- let name' = v.name.unVarName+ let name' = (v ^. #name . #unVarName) in if Set.member name' allRefs then Nothing else Just ("variable '" <> name' <> "' is declared but never referenced") )- m.vars+ (m ^. #vars) -- | Required variables that have no corresponding prompt. lintRequiredWithoutPrompt :: Module -> [Text] lintRequiredWithoutPrompt m =- let promptedVars = Set.fromList $ map (.var.unVarName) m.prompts+ let promptedVars = Set.fromList $ map (^. #var . #unVarName) (m ^. #prompts) in mapMaybe ( \v ->- let name' = v.name.unVarName- in if v.required && not (Set.member name' promptedVars)+ let name' = (v ^. #name . #unVarName)+ in if v ^. #required && not (Set.member name' promptedVars) then Just ("required variable '" <> name' <> "' has no prompt") else Nothing )- m.vars+ (m ^. #vars) -- | Steps that write to the same destination (excluding patch ops). lintDuplicateDestinations :: Module -> [Text] lintDuplicateDestinations m =- let nonPatchDests = [s.dest | s <- m.steps, isNothing s.patch]+ let nonPatchDests = [s ^. #dest | s <- m ^. #steps, isNothing (s ^. #patch)] dupes = findDuplicates Set.empty Set.empty nonPatchDests in map (\d -> "multiple steps write to '" <> d <> "'") dupes @@ -224,22 +225,22 @@ lintEmptyChoices :: Module -> [Text] lintEmptyChoices m = mapMaybe- ( \v -> case v.type_ of- VTChoice [] -> Just ("variable '" <> v.name.unVarName <> "' has an empty choice list")+ ( \v -> case v ^. #type_ of+ VTChoice [] -> Just ("variable '" <> v ^. #name . #unVarName <> "' has an empty choice list") _ -> Nothing )- m.vars+ (m ^. #vars) -- | Variables without a description. lintMissingDescriptions :: Module -> [Text] lintMissingDescriptions m = mapMaybe ( \v ->- if isNothing v.description- then Just ("variable '" <> v.name.unVarName <> "' has no description")+ if isNothing (v ^. #description)+ then Just ("variable '" <> v ^. #name . #unVarName <> "' has no description") else Nothing )- m.vars+ (m ^. #vars) -- Conditional-expression lint (when clauses + template {{#if}} conditionals) @@ -261,15 +262,15 @@ lintConditionals :: FilePath -> Module -> IO ([Text], [Text]) lintConditionals baseDir m = do templateExprs <- collectTemplateExprs baseDir m- let declaredTypes = Map.fromList [(d.name, d.type_) | d <- m.vars]+ let declaredTypes = Map.fromList [(d ^. #name, d ^. #type_) | d <- m ^. #vars] stepExprs =- [("step '" <> s.dest <> "' when clause", c) | s <- m.steps, Just c <- [s.condition]]+ [("step '" <> s ^. #dest <> "' when clause", c) | s <- m ^. #steps, Just c <- [s ^. #condition]] commandExprs =- [("command when clause", c) | c0 <- m.commands, Just c <- [c0.condition]]+ [("command when clause", c) | c0 <- m ^. #commands, Just c <- [c0 ^. #condition]] promptExprs =- [ ("prompt for '" <> p.var.unVarName <> "' when clause", c)- | p <- m.prompts,- Just c <- [p.condition]+ [ ("prompt for '" <> p ^. #var . #unVarName <> "' when clause", c)+ | p <- m ^. #prompts,+ Just c <- [p ^. #condition] ] allExprs = stepExprs ++ commandExprs ++ promptExprs ++ templateExprs findings = concatMap (uncurry (lintExpr declaredTypes)) allExprs@@ -287,20 +288,20 @@ collectTemplateExprs baseDir m = concat <$> mapM readStep textBearingSteps where- textBearingSteps = filter (isTextBearing . (.strategy)) m.steps+ textBearingSteps = filter (isTextBearing . (^. #strategy)) (m ^. #steps) isTextBearing Template = True isTextBearing DhallText = True isTextBearing _ = False readStep s = do- let path = baseDir </> "files" </> s.src+ let path = baseDir </> "files" </> (s ^. #src) exists <- doesFileExist path if not exists then pure [] else do contents <- TIO.readFile path- let label = "template '" <> T.pack s.src <> "' {{#if}} condition"+ let label = "template '" <> T.pack (s ^. #src) <> "' {{#if}} condition" pure [(label, expr) | raw <- extractIfExprs contents, Right expr <- [parseExpr raw]] -- | Lint a single expression from the given source against the declared types.@@ -311,7 +312,7 @@ checkRef (name, mLit) = case Map.lookup name declaredTypes of Nothing ->- [CondUndeclared (srcLabel <> " references undeclared variable: " <> name.unVarName)]+ [CondUndeclared (srcLabel <> " references undeclared variable: " <> name ^. #unVarName)] Just ty -> case mLit of Just lit | not (literalMatchesType ty lit) ->@@ -337,7 +338,7 @@ describeMismatch srcLabel name ty lit = srcLabel <> " compares variable '"- <> name.unVarName+ <> name ^. #unVarName <> "' (declared type " <> renderVarType ty <> ") against "
src/Seihou/Interaction/Confirm.hs view
@@ -4,6 +4,7 @@ where import Control.Monad (foldM)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Seihou.Composition.Instance (ModuleInstance (..)) import Seihou.Core.Types@@ -43,7 +44,7 @@ anyNeedsConfirm = any (any isDefaultOrParent) . Map.elems isDefaultOrParent :: ResolvedVar -> Bool-isDefaultOrParent rv = case rv.source of+isDefaultOrParent rv = case rv ^. #source of FromDefault -> True FromParent _ -> True _ -> False@@ -55,7 +56,7 @@ Eff es (Map ModuleInstance (Map VarName ResolvedVar)) processInstance acc (inst, m, _dir) = do let modResolved = Map.findWithDefault Map.empty inst acc- newModResolved <- foldM (processVar m acc) modResolved m.vars+ newModResolved <- foldM (processVar m acc) modResolved (m ^. #vars) pure (Map.insert inst newModResolved acc) processVar ::@@ -66,29 +67,29 @@ VarDecl -> Eff es (Map VarName ResolvedVar) processVar m allResolved modResolved decl =- case Map.lookup decl.name modResolved of+ case Map.lookup (decl ^. #name) modResolved of Just rv | isDefaultOrParent rv -> do let prompt = findOrSynthesize m decl currentBindings =- Map.map (.value) (Map.unions (Map.elems allResolved))+ Map.map (^. #value) (Map.unions (Map.elems allResolved)) result <- promptForVar prompt decl currentBindings case result of Left _err -> pure modResolved Right newRv- | newRv.value == rv.value -> pure modResolved- | otherwise -> pure (Map.insert decl.name newRv modResolved)+ | newRv ^. #value == rv ^. #value -> pure modResolved+ | otherwise -> pure (Map.insert (decl ^. #name) newRv modResolved) _ -> pure modResolved -- | Find the authored 'Prompt' for a variable, or build a minimal one -- from the declaration. findOrSynthesize :: Module -> VarDecl -> Prompt findOrSynthesize m decl =- case filter (\p -> p.var == decl.name) m.prompts of+ case filter (\p -> p ^. #var == decl ^. #name) (m ^. #prompts) of (p : _) -> p [] -> Prompt- { var = decl.name,- text = decl.name.unVarName,+ { var = decl ^. #name,+ text = decl ^. #name . #unVarName, condition = Nothing, choices = Nothing }
src/Seihou/Interaction/Prompt.hs view
@@ -4,6 +4,7 @@ ) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Expr (evalExpr)@@ -32,12 +33,12 @@ runPrompts prompts unresolvedDecls currentBindings = go prompts Map.empty where- declMap = Map.fromList [(d.name, d) | d <- unresolvedDecls]+ declMap = Map.fromList [(d ^. #name, d) | d <- unresolvedDecls] go :: (Console :> es) => [Prompt] -> Map VarName ResolvedVar -> Eff es (Map VarName ResolvedVar) go [] acc = pure acc go (p : ps) acc = do- let vn = p.var+ let vn = (p ^. #var) -- Skip if variable is not in the unresolved set case Map.lookup vn declMap of Nothing -> go ps acc@@ -47,7 +48,7 @@ then go ps acc else do -- Evaluate when condition- let allBindings = Map.union (Map.map (.value) acc) currentBindings+ let allBindings = Map.union (Map.map (^. #value) acc) currentBindings if shouldPrompt p allBindings then do result <- promptForVar p decl allBindings@@ -59,7 +60,7 @@ -- | Check if a prompt should be displayed based on its @when@ condition. shouldPrompt :: Prompt -> Map VarName VarValue -> Bool shouldPrompt p bindings =- case p.condition of+ case p ^. #condition of Nothing -> True Just expr -> evalExpr bindings expr @@ -73,7 +74,7 @@ Map VarName VarValue -> Eff es (Either VarError ResolvedVar) promptForVar prompt decl _bindings =- case prompt.choices of+ case prompt ^. #choices of Just choices -> promptWithChoices prompt decl choices Nothing -> promptFreeText prompt decl 3 @@ -92,7 +93,7 @@ putText (formatPromptText prompt decl) raw <- getLine if T.null (T.strip raw)- then case decl.default_ of+ then case decl ^. #default_ of Just defVal -> -- Accept the default value pure@@ -104,14 +105,14 @@ } ) Nothing- | not decl.required ->+ | not (decl ^. #required) -> -- Optional variable with no default — skip- pure (Left (MissingRequiredVar decl.name))+ pure (Left (MissingRequiredVar (decl ^. #name))) | retriesLeft > 1 -> do putText "Value cannot be empty. Please try again." promptFreeText prompt decl (retriesLeft - 1) | otherwise ->- pure (Left (MissingRequiredVar decl.name))+ pure (Left (MissingRequiredVar (decl ^. #name))) else case coerceAndValidate decl raw of Left err -> if retriesLeft > 1@@ -129,7 +130,7 @@ [Text] -> Eff es (Either VarError ResolvedVar) promptWithChoices prompt decl choices = do- putText prompt.text+ putText (prompt ^. #text) mapM_ (\(i, c) -> putText (" " <> T.pack (show i) <> ") " <> c)) (zip [1 :: Int ..] choices) putText "Enter selection number:" raw <- getLine@@ -151,12 +152,12 @@ in case coerceAndValidate decl chosen of Left err -> pure (Left err) Right rv -> pure (Right rv)- _ -> pure (Left (MissingRequiredVar decl.name))+ _ -> pure (Left (MissingRequiredVar (decl ^. #name))) -- | Coerce raw text to the variable's type and validate. coerceAndValidate :: VarDecl -> Text -> Either VarError ResolvedVar coerceAndValidate decl raw = do- val <- coerceValue decl.name decl.type_ raw+ val <- coerceValue (decl ^. #name) (decl ^. #type_) raw validateVarValue decl val pure ResolvedVar@@ -185,11 +186,11 @@ -- For optional variables without a default, appends @[skip]@. formatPromptText :: Prompt -> VarDecl -> Text formatPromptText prompt decl =- case decl.default_ of- Just defVal -> prompt.text <> " [" <> showDefaultValue defVal <> "]:"+ case decl ^. #default_ of+ Just defVal -> prompt ^. #text <> " [" <> showDefaultValue defVal <> "]:" Nothing- | not decl.required -> prompt.text <> " [skip]:"- | otherwise -> prompt.text+ | not (decl ^. #required) -> prompt ^. #text <> " [skip]:"+ | otherwise -> (prompt ^. #text) -- | Render a VarValue for display in a prompt's default hint. showDefaultValue :: VarValue -> Text
src/Seihou/Manifest/Types.hs view
@@ -6,6 +6,7 @@ writeAppliedBlueprint, writeAppliedBlueprintMigration, hasAppliedBlueprintMigration,+ artifactOriginName, ) where @@ -13,6 +14,7 @@ import Data.Aeson qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Lazy qualified as LBS+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -20,6 +22,7 @@ import Seihou.Core.Types import Seihou.Manifest.Hash (baselineRefFromText) import Seihou.Prelude hiding ((.=))+import System.FilePath (takeFileName) -- | Current manifest schema version. --@@ -40,8 +43,15 @@ -- -- Bumped from 4 to 5 when 'Manifest' gained the durable -- @blueprintMigrations@ receipt ledger. A missing ledger decodes as empty.+--+-- Bumped from 5 to 6 when every recorded artifact reference gained a+-- portable @origin@ and the machine-specific @source@ / @targetSource@+-- absolute paths were dropped from the serialized form+-- (see docs/plans/76-record-portable-artifact-origins-in-the-manifest.md).+-- Version-5-and-earlier manifests are not readable directly; see+-- docs/plans/79-upgrade-legacy-absolute-path-manifests-in-place.md. currentManifestVersion :: Int-currentManifestVersion = 5+currentManifestVersion = 6 -- | Create an empty manifest with the given timestamp. emptyManifest :: UTCTime -> Manifest@@ -65,15 +75,15 @@ writeAppliedBlueprint :: AppliedBlueprint -> Manifest -> Manifest writeAppliedBlueprint ab m = Manifest- { version = m.version,- genAt = m.genAt,- modules = m.modules,- vars = m.vars,- files = m.files,- applications = m.applications,- recipe = m.recipe,+ { version = m ^. #version,+ genAt = m ^. #genAt,+ modules = m ^. #modules,+ vars = m ^. #vars,+ files = m ^. #files,+ applications = m ^. #applications,+ recipe = m ^. #recipe, blueprint = Just ab,- blueprintMigrations = m.blueprintMigrations+ blueprintMigrations = m ^. #blueprintMigrations } -- | Insert or replace one exact blueprint migration receipt. Replacement is@@ -83,20 +93,20 @@ writeAppliedBlueprintMigration receipt manifest = Manifest { version = currentManifestVersion,- genAt = manifest.genAt,- modules = manifest.modules,- vars = manifest.vars,- files = manifest.files,- applications = manifest.applications,- recipe = manifest.recipe,- blueprint = manifest.blueprint,- blueprintMigrations = upsert manifest.blueprintMigrations+ genAt = manifest ^. #genAt,+ modules = manifest ^. #modules,+ vars = manifest ^. #vars,+ files = manifest ^. #files,+ applications = manifest ^. #applications,+ recipe = manifest ^. #recipe,+ blueprint = manifest ^. #blueprint,+ blueprintMigrations = upsert (manifest ^. #blueprintMigrations) } where sameEdge existing =- existing.name == receipt.name- && existing.fromVersion == receipt.fromVersion- && existing.toVersion == receipt.toVersion+ existing ^. #name == receipt ^. #name+ && existing ^. #fromVersion == receipt ^. #fromVersion+ && existing ^. #toVersion == (receipt ^. #toVersion) upsert receipts | any sameEdge receipts = map (\existing -> if sameEdge existing then receipt else existing) receipts@@ -107,11 +117,11 @@ hasAppliedBlueprintMigration blueprintName fromVersion toVersion manifest = any ( \receipt ->- receipt.name == blueprintName- && receipt.fromVersion == fromVersion- && receipt.toVersion == toVersion+ receipt ^. #name == blueprintName+ && receipt ^. #fromVersion == fromVersion+ && receipt ^. #toVersion == toVersion )- manifest.blueprintMigrations+ (manifest ^. #blueprintMigrations) -- | Encode a manifest to JSON bytes. manifestToJSON :: Manifest -> LBS.ByteString@@ -126,39 +136,59 @@ instance ToJSON Manifest where toJSON m = Aeson.object $- [ "version" .= m.version,- "generatedAt" .= m.genAt,- "modules" .= m.modules,- "variables" .= varsToJSON m.vars,- "files" .= filesToJSON m.files,- "applications" .= m.applications,- "blueprintMigrations" .= m.blueprintMigrations+ [ "version" .= (m ^. #version),+ "generatedAt" .= (m ^. #genAt),+ "modules" .= (m ^. #modules),+ "variables" .= varsToJSON (m ^. #vars),+ "files" .= filesToJSON (m ^. #files),+ "applications" .= (m ^. #applications),+ "blueprintMigrations" .= (m ^. #blueprintMigrations) ]- ++ maybe [] (\r -> ["recipe" .= r]) m.recipe- ++ maybe [] (\b -> ["blueprint" .= b]) m.blueprint+ ++ maybe [] (\r -> ["recipe" .= r]) (m ^. #recipe)+ ++ maybe [] (\b -> ["blueprint" .= b]) (m ^. #blueprint) instance FromJSON Manifest where parseJSON = Aeson.withObject "Manifest" $ \o -> do v <- o .: "version"- if v > currentManifestVersion- then fail "manifest was created by a newer version of seihou"- else- Manifest- <$> pure v- <*> o .: "generatedAt"- <*> o .: "modules"- <*> (varsFromJSON =<< o .: "variables")- <*> (filesFromJSON =<< o .: "files")- <*> o Aeson..:? "applications" Aeson..!= []- <*> o Aeson..:? "recipe"- <*> o Aeson..:? "blueprint"- <*> o Aeson..:? "blueprintMigrations" Aeson..!= []+ checkManifestVersion v+ Manifest+ <$> pure v+ <*> o .: "generatedAt"+ <*> o .: "modules"+ <*> (varsFromJSON =<< o .: "variables")+ <*> (filesFromJSON =<< o .: "files")+ <*> o Aeson..:? "applications" Aeson..!= []+ <*> o Aeson..:? "recipe"+ <*> o Aeson..:? "blueprint"+ <*> o Aeson..:? "blueprintMigrations" Aeson..!= [] +-- | Reject a manifest this build cannot read, naming the remedy.+--+-- Compatibility seam: schema-5-and-earlier manifests carry an absolute+-- @source@ path in place of the portable @origin@. Decoding those is owned+-- by docs/plans/79-upgrade-legacy-absolute-path-manifests-in-place.md; until+-- that plan lands an older manifest fails here with a clear message rather+-- than being silently misread. The @seihou manifest upgrade@ command the+-- message names is delivered by that same plan, so the remedy does not exist+-- yet.+checkManifestVersion :: Int -> Aeson.Parser ()+checkManifestVersion v+ | v > currentManifestVersion =+ fail "manifest was created by a newer version of seihou"+ | v < 6 =+ fail+ ( "this manifest uses schema version "+ <> show v+ <> ", which records machine-specific absolute paths; run "+ <> "'seihou manifest upgrade' to convert it"+ )+ | otherwise = pure ()+ instance ToJSON AppliedTarget where toJSON (AppliedModuleTarget name) =- Aeson.object ["kind" .= ("module" :: Text), "name" .= name.unModuleName]+ Aeson.object ["kind" .= ("module" :: Text), "name" .= (name ^. #unModuleName)] toJSON (AppliedRecipeTarget name) =- Aeson.object ["kind" .= ("recipe" :: Text), "name" .= name.unRecipeName]+ Aeson.object ["kind" .= ("recipe" :: Text), "name" .= (name ^. #unRecipeName)] instance FromJSON AppliedTarget where parseJSON = Aeson.withObject "AppliedTarget" $ \o -> do@@ -169,15 +199,58 @@ "recipe" -> pure (AppliedRecipeTarget (RecipeName name)) other -> fail ("unknown applied target kind: " <> T.unpack other) +-- | Machine-independent artifact references are encoded as a tagged object+-- so a manifest diff stays readable and so future constructors can be added+-- without breaking the shape.+instance ToJSON ArtifactOrigin where+ toJSON (RemoteOrigin url artifact repo) =+ Aeson.object $+ [ "kind" .= ("remote" :: Text),+ "url" .= url,+ "artifact" .= artifact+ ]+ ++ maybe [] (\value -> ["repo" .= value]) repo+ toJSON (ProjectOrigin path) =+ Aeson.object+ [ "kind" .= ("project" :: Text),+ "path" .= T.pack path+ ]+ toJSON (LocalOrigin artifact) =+ Aeson.object+ [ "kind" .= ("local" :: Text),+ "artifact" .= artifact+ ]++instance FromJSON ArtifactOrigin where+ parseJSON = Aeson.withObject "ArtifactOrigin" $ \o -> do+ kind <- o .: "kind" :: Aeson.Parser Text+ case kind of+ "remote" ->+ RemoteOrigin+ <$> o .: "url"+ <*> o .: "artifact"+ <*> o Aeson..:? "repo"+ "project" -> ProjectOrigin . T.unpack <$> o .: "path"+ "local" -> LocalOrigin <$> o .: "artifact"+ other -> fail ("unknown artifact origin kind: " <> T.unpack other)++-- | The artifact name an origin refers to, for display and for matching+-- against a discovered artifact. 'ProjectOrigin' derives it from the last+-- path segment, which is how @.seihou\/modules\/\<name\>@ is laid out.+artifactOriginName :: ArtifactOrigin -> Text+artifactOriginName (RemoteOrigin _ artifact _) = artifact+artifactOriginName (LocalOrigin artifact) = artifact+artifactOriginName (ProjectOrigin path) = T.pack (takeFileName path)+ instance ToJSON AppliedInstanceState where toJSON state = Aeson.object $- [ "name" .= state.name.unModuleName,- "source" .= state.source,- "resolvedVars" .= varsToJSON state.resolvedVars+ [ "name" .= (state ^. #name . #unModuleName),+ "origin" .= (state ^. #origin),+ "resolvedVars" .= varsToJSON (state ^. #resolvedVars) ]- ++ parentVarsField state.parentVars- ++ maybe [] (\v -> ["version" .= v]) state.moduleVersion+ ++ parentVarsField (state ^. #parentVars)+ ++ maybe [] (\v -> ["version" .= v]) (state ^. #moduleVersion) where parentVarsField (ParentVars m) | Map.null m = []@@ -192,24 +265,24 @@ AppliedInstanceState <$> (ModuleName <$> o .: "name") <*> pure pv- <*> o .: "source"+ <*> o .: "origin" <*> o Aeson..:? "version" <*> (varsFromJSON =<< o Aeson..:? "resolvedVars" Aeson..!= Aeson.object []) instance ToJSON AppliedComposition where toJSON composition = Aeson.object $- [ "applicationId" .= composition.applicationId.unApplicationId,- "target" .= composition.target,- "targetSource" .= composition.targetSource,- "additionalModules" .= map (.unModuleName) composition.additionalModules,- "instances" .= composition.instances,- "appliedAt" .= composition.appliedAt+ [ "applicationId" .= (composition ^. #applicationId . #unApplicationId),+ "target" .= (composition ^. #target),+ "targetOrigin" .= (composition ^. #targetOrigin),+ "additionalModules" .= map (^. #unModuleName) (composition ^. #additionalModules),+ "instances" .= (composition ^. #instances),+ "appliedAt" .= (composition ^. #appliedAt) ]- ++ maybe [] (\v -> ["targetVersion" .= v]) composition.targetVersion- ++ maybe [] (\v -> ["namespace" .= v]) composition.namespace- ++ maybe [] (\v -> ["context" .= v]) composition.context- ++ commandReceiptsField composition.commandReceipts+ ++ maybe [] (\v -> ["targetVersion" .= v]) (composition ^. #targetVersion)+ ++ maybe [] (\v -> ["namespace" .= v]) (composition ^. #namespace)+ ++ maybe [] (\v -> ["context" .= v]) (composition ^. #context)+ ++ commandReceiptsField (composition ^. #commandReceipts) where commandReceiptsField receipts | Map.null receipts = []@@ -220,7 +293,7 @@ AppliedComposition <$> (ApplicationId <$> o .: "applicationId") <*> o .: "target"- <*> o .: "targetSource"+ <*> o .: "targetOrigin" <*> o Aeson..:? "targetVersion" <*> (map ModuleName <$> o Aeson..:? "additionalModules" Aeson..!= []) <*> o Aeson..:? "namespace"@@ -232,12 +305,12 @@ instance ToJSON CommandReceipt where toJSON receipt = Aeson.object $- [ "fingerprint" .= commandFingerprintText receipt.fingerprint,- "module" .= receipt.moduleName.unModuleName,- "command" .= receipt.command,- "completedAt" .= receipt.completedAt+ [ "fingerprint" .= commandFingerprintText (receipt ^. #fingerprint),+ "module" .= (receipt ^. #moduleName . #unModuleName),+ "command" .= (receipt ^. #command),+ "completedAt" .= (receipt ^. #completedAt) ]- ++ maybe [] (\path -> ["workDir" .= path]) receipt.workDir+ ++ maybe [] (\path -> ["workDir" .= path]) (receipt ^. #workDir) instance FromJSON CommandReceipt where parseJSON = Aeson.withObject "CommandReceipt" $ \o ->@@ -251,10 +324,10 @@ instance ToJSON AppliedRecipe where toJSON ar = Aeson.object $- [ "name" .= ar.name.unRecipeName,- "appliedAt" .= ar.appliedAt+ [ "name" .= (ar ^. #name . #unRecipeName),+ "appliedAt" .= (ar ^. #appliedAt) ]- ++ maybe [] (\v -> ["version" .= v]) ar.recipeVersion+ ++ maybe [] (\v -> ["version" .= v]) (ar ^. #recipeVersion) instance FromJSON AppliedRecipe where parseJSON = Aeson.withObject "AppliedRecipe" $ \o ->@@ -266,14 +339,14 @@ instance ToJSON AppliedBlueprint where toJSON ab = Aeson.object $- [ "name" .= ab.name.unModuleName,- "appliedAt" .= ab.appliedAt,- "baselineModules" .= map (.unModuleName) ab.baselineModules,- "noBaseline" .= ab.noBaseline+ [ "name" .= (ab ^. #name . #unModuleName),+ "appliedAt" .= (ab ^. #appliedAt),+ "baselineModules" .= map (^. #unModuleName) (ab ^. #baselineModules),+ "noBaseline" .= (ab ^. #noBaseline) ]- ++ maybe [] (\v -> ["version" .= v]) ab.blueprintVersion- ++ maybe [] (\p -> ["userPrompt" .= p]) ab.userPrompt- ++ maybe [] (\s -> ["agentSessionId" .= s]) ab.agentSessionId+ ++ maybe [] (\v -> ["version" .= v]) (ab ^. #blueprintVersion)+ ++ maybe [] (\p -> ["userPrompt" .= p]) (ab ^. #userPrompt)+ ++ maybe [] (\s -> ["agentSessionId" .= s]) (ab ^. #agentSessionId) instance FromJSON AppliedBlueprint where parseJSON = Aeson.withObject "AppliedBlueprint" $ \o ->@@ -289,13 +362,13 @@ instance ToJSON AppliedBlueprintMigration where toJSON receipt = Aeson.object $- [ "name" .= receipt.name.unModuleName,- "from" .= receipt.fromVersion,- "to" .= receipt.toVersion,- "appliedAt" .= receipt.appliedAt+ [ "name" .= (receipt ^. #name . #unModuleName),+ "from" .= (receipt ^. #fromVersion),+ "to" .= (receipt ^. #toVersion),+ "appliedAt" .= (receipt ^. #appliedAt) ]- ++ maybe [] (\version -> ["version" .= version]) receipt.blueprintVersion- ++ maybe [] (\sessionId -> ["agentSessionId" .= sessionId]) receipt.agentSessionId+ ++ maybe [] (\version -> ["version" .= version]) (receipt ^. #blueprintVersion)+ ++ maybe [] (\sessionId -> ["agentSessionId" .= sessionId]) (receipt ^. #agentSessionId) instance FromJSON AppliedBlueprintMigration where parseJSON = Aeson.withObject "AppliedBlueprintMigration" $ \o ->@@ -310,13 +383,13 @@ instance ToJSON AppliedModule where toJSON am = Aeson.object $- [ "name" .= am.name.unModuleName,- "source" .= am.source,- "appliedAt" .= am.appliedAt+ [ "name" .= (am ^. #name . #unModuleName),+ "origin" .= (am ^. #origin),+ "appliedAt" .= (am ^. #appliedAt) ]- ++ parentVarsField am.parentVars- ++ maybe [] (\v -> ["version" .= v]) am.moduleVersion- ++ maybe [] (\r -> ["removal" .= removalToJSON r]) am.removal+ ++ parentVarsField (am ^. #parentVars)+ ++ maybe [] (\v -> ["version" .= v]) (am ^. #moduleVersion)+ ++ maybe [] (\r -> ["removal" .= removalToJSON r]) (am ^. #removal) where parentVarsField (ParentVars m) | Map.null m = []@@ -341,13 +414,13 @@ AppliedModule <$> (ModuleName <$> o .: "name") <*> pure pv- <*> o .: "source"+ <*> o .: "origin" <*> o Aeson..:? "version" <*> o .: "appliedAt" <*> pure removal parentVarsMapToJSON :: Map VarName Text -> Aeson.Value-parentVarsMapToJSON = toJSON . Map.mapKeys (.unVarName)+parentVarsMapToJSON = toJSON . Map.mapKeys (^. #unVarName) parentVarsMapFromJSON :: Aeson.Value -> Aeson.Parser (Map VarName Text) parentVarsMapFromJSON v = do@@ -358,17 +431,17 @@ removalToJSON :: Removal -> Aeson.Value removalToJSON r = Aeson.object- [ "steps" .= map removalStepToJSON r.removalSteps,- "commands" .= map removalCommandToJSON r.removalCommands+ [ "steps" .= map removalStepToJSON (r ^. #steps),+ "commands" .= map removalCommandToJSON (r ^. #commands) ] removalStepToJSON :: RemovalStep -> Aeson.Value removalStepToJSON s = Aeson.object $- [ "action" .= removalActionToText s.action,- "dest" .= s.dest+ [ "action" .= removalActionToText (s ^. #action),+ "dest" .= (s ^. #dest) ]- ++ maybe [] (\p -> ["src" .= p]) s.src+ ++ maybe [] (\p -> ["src" .= p]) (s ^. #src) removalActionToText :: RemovalAction -> Text removalActionToText RemoveFileAction = "remove-file"@@ -378,8 +451,8 @@ removalCommandToJSON :: Command -> Aeson.Value removalCommandToJSON c = Aeson.object $- ["run" .= c.run]- ++ maybe [] (\w -> ["workDir" .= w]) c.workDir+ ["run" .= (c ^. #run)]+ ++ maybe [] (\w -> ["workDir" .= w]) (c ^. #workDir) -- | Parse a Removal from JSON. parseRemovalJSON :: Aeson.Value -> Aeson.Parser Removal@@ -411,17 +484,17 @@ instance ToJSON FileRecord where toJSON fr = Aeson.object $- [ "hash" .= fr.hash.unSHA256,- "module" .= fr.moduleName.unModuleName,- "strategy" .= strategyToText fr.strategy,- "generatedAt" .= fr.generatedAt+ [ "hash" .= (fr ^. #hash . #unSHA256),+ "module" .= (fr ^. #moduleName . #unModuleName),+ "strategy" .= strategyToText (fr ^. #strategy),+ "generatedAt" .= (fr ^. #generatedAt) ]- ++ maybe [] (\ref -> ["baseline" .= ref.unBaselineRef.unSHA256]) fr.baseline- ++ applicationIdsField fr.applicationIds+ ++ maybe [] (\ref -> ["baseline" .= (ref ^. #unBaselineRef . #unSHA256)]) (fr ^. #baseline)+ ++ applicationIdsField (fr ^. #applicationIds) where applicationIdsField ids | Set.null ids = []- | otherwise = ["applications" .= map (.unApplicationId) (Set.toAscList ids)]+ | otherwise = ["applications" .= map (^. #unApplicationId) (Set.toAscList ids)] instance FromJSON FileRecord where parseJSON = Aeson.withObject "FileRecord" $ \o -> do@@ -459,7 +532,7 @@ -- Helpers for VarName-keyed maps varsToJSON :: Map VarName Text -> Aeson.Value-varsToJSON = toJSON . Map.mapKeys (.unVarName)+varsToJSON = toJSON . Map.mapKeys (^. #unVarName) varsFromJSON :: Aeson.Value -> Aeson.Parser (Map VarName Text) varsFromJSON v = do
src/Seihou/Prelude.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE PackageImports #-}+{-# LANGUAGE PatternSynonyms #-} module Seihou.Prelude ( -- * Text@@ -28,18 +29,11 @@ EffectHandler, -- * Lens- view,- over,- set,- (^.),- (.~),- (%~),- (&),- lens,- Lens',- Getting,- ASetter,+ module Control.Lens, + -- * Generics+ Generic,+ -- * Bifunctor first, @@ -50,11 +44,37 @@ where import "base" Data.Bifunctor (first)+-- Every record type in the project derives Generic, both because the house+-- style requires it and because generic-lens synthesises #label lenses from+-- the Generic representation. Re-exporting it here keeps the derive clauses+-- import-free.+import "base" GHC.Generics (Generic) import "containers" Data.Map.Strict (Map) import "containers" Data.Set (Set) import "effectful-core" Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, IOE, MonadIO, liftIO, runEff, type (:>), type (:>>)) import "effectful-core" Effectful.Dispatch.Dynamic (EffectHandler, HasCallStack, interpret, reinterpret, send) import "filepath" System.FilePath ((</>))-import "generic-lens" Data.Generics.Labels ()-import "lens" Control.Lens (ASetter, Getting, Lens', lens, over, set, view, (%~), (&), (.~), (^.))+-- Re-export the whole lens API. PackageImports pins the package so that+-- `Control.Lens` unambiguously means the `lens` package's module.+--+-- Deliberately absent: Data.Generics.Labels. Its IsLabel instance is an+-- orphan, and orphan instances propagate transitively, so importing it here+-- would force the generic-lens interpretation of #label onto every module in+-- the project. Each module that uses #label imports it individually instead.+--+-- Four names are hidden. Each collides with a name seihou already has in+-- scope, and none of the four is a lens combinator seihou has any use for:+--+-- (.=) collides with Data.Aeson's (.=), used unqualified by the+-- hand-written ToJSON instances in eleven modules. The lens (.=)+-- is the MonadState assignment operator; seihou uses effectful's+-- State with `modify` and never needs it.+-- argument collides with Options.Applicative.argument, imported openly by+-- Seihou.CLI.Commands. The lens `argument` is a Setter over a+-- Profunctor's argument position.+-- List collides with the `List` constructor of Seihou.CLI.Commands's+-- Command type. The lens `List` is an IsList pattern synonym.+-- Context collides with the `Context` constructor of the same type. The+-- lens `Context` is the indexed store comonad.+import "lens" Control.Lens hiding (Context (..), argument, (.=), pattern List) import "text" Data.Text (Text)
test/Main.hs view
@@ -7,6 +7,8 @@ import Seihou.Composition.ResolveSpec qualified as ResolveSpec import Seihou.Core.AgentPromptSpec qualified as AgentPromptSpec import Seihou.Core.ApplicationSpec qualified as ApplicationSpec+import Seihou.Core.ArtifactOriginDetectSpec qualified as ArtifactOriginDetectSpec+import Seihou.Core.ArtifactRefSpec qualified as ArtifactRefSpec import Seihou.Core.BlueprintSpec qualified as BlueprintSpec import Seihou.Core.CommandFingerprintSpec qualified as CommandFingerprintSpec import Seihou.Core.CommandVarSpec qualified as CommandVarSpec@@ -71,6 +73,8 @@ resolveTests <- ResolveSpec.tests agentPromptTests <- AgentPromptSpec.tests applicationTests <- ApplicationSpec.tests+ artifactOriginDetectTests <- ArtifactOriginDetectSpec.tests+ artifactRefTests <- ArtifactRefSpec.tests blueprintTests <- BlueprintSpec.tests commandFingerprintTests <- CommandFingerprintSpec.tests commandVarTests <- CommandVarSpec.tests@@ -124,4 +128,4 @@ manifestTypesTests <- ManifestTypesSpec.tests promptTests <- PromptSpec.tests confirmTests <- ConfirmSpec.tests- defaultMain (testGroup "seihou-core" [graphTests, instanceTests, compositionPlanTests, compositionRecipeTests, resolveTests, agentPromptTests, applicationTests, blueprintTests, commandFingerprintTests, commandVarTests, typesTests, contextTests, exprTests, installTests, listTests, migrationTests, moduleTests, recipeTests, registryTests, registryEmitTests, registrySyncTests, scaffoldTests, schemaUpgradeTests, statusTests, variableTests, versionTests, templateTests, threeWayMergeTests, updateTransactionTests, planTests, previewTests, reconcileTests, sectionTests, validateTests, splitFlakeTests, dhallTextFlakeTests, typedDhallTextTests, conditionalTemplateTests, configTests, dhallEvalTests, migrationDecoderTests, configReaderTests, configWriterTests, baselineStoreTests, filesystemTests, loggerTests, manifestStoreTests, conflictTests, baselineTests, diffTests, executeTests, engineMigrateTests, removeTests, compositionTests, executionTests, integrationTests, generationTests, manifestTypesTests, promptTests, confirmTests])+ defaultMain (testGroup "seihou-core" [graphTests, instanceTests, compositionPlanTests, compositionRecipeTests, resolveTests, agentPromptTests, applicationTests, artifactOriginDetectTests, artifactRefTests, blueprintTests, commandFingerprintTests, commandVarTests, typesTests, contextTests, exprTests, installTests, listTests, migrationTests, moduleTests, recipeTests, registryTests, registryEmitTests, registrySyncTests, scaffoldTests, schemaUpgradeTests, statusTests, variableTests, versionTests, templateTests, threeWayMergeTests, updateTransactionTests, planTests, previewTests, reconcileTests, sectionTests, validateTests, splitFlakeTests, dhallTextFlakeTests, typedDhallTextTests, conditionalTemplateTests, configTests, dhallEvalTests, migrationDecoderTests, configReaderTests, configWriterTests, baselineStoreTests, filesystemTests, loggerTests, manifestStoreTests, conflictTests, baselineTests, diffTests, executeTests, engineMigrateTests, removeTests, compositionTests, executionTests, integrationTests, generationTests, manifestTypesTests, promptTests, confirmTests])
test/Seihou/Composition/GraphSpec.hs view
@@ -1,6 +1,8 @@ module Seihou.Composition.GraphSpec (tests) where +import Control.Lens ((^.)) import Data.Either (isLeft)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Seihou.Composition.Graph import Seihou.Composition.Instance (ModuleInstance (..), mkInstance, primaryInstance)@@ -33,7 +35,7 @@ -- primary 'ModuleInstance' (no parent bindings). Works for all the -- bare-name test scenarios below. fromModules :: [Module] -> CompositionGraph-fromModules ms = buildGraph [(primaryInstance m.name, m) | m <- ms]+fromModules ms = buildGraph [(primaryInstance (m ^. #name), m) | m <- ms] spec :: Spec spec = do@@ -41,21 +43,21 @@ it "builds a graph from a single module with no dependencies" $ do let m = mkModule "base" [] g = fromModules [m]- length g.cgModules `shouldBe` 1- length g.cgEdges `shouldBe` 1+ length (g ^. #modules) `shouldBe` 1+ length (g ^. #edges) `shouldBe` 1 it "builds a graph preserving dependency edges" $ do let a = mkModule "a" ["b", "c"] b = mkModule "b" [] c = mkModule "c" [] g = fromModules [a, b, c]- length g.cgModules `shouldBe` 3- length g.cgEdges `shouldBe` 3+ length (g ^. #modules) `shouldBe` 3+ length (g ^. #edges) `shouldBe` 3 describe "topoSort" $ do it "returns a single module with no dependencies" $ do let g = fromModules [mkModule "base" []]- fmap (map (.instanceModule)) (topoSort g) `shouldBe` Right ["base"]+ fmap (map (^. #module_)) (topoSort g) `shouldBe` Right ["base"] it "orders a linear chain: A -> B -> C" $ do let a = mkModule "a" ["b"]@@ -64,7 +66,7 @@ g = fromModules [a, b, c] case topoSort g of Right order -> do- let names = map (.instanceModule) order+ let names = map (^. #module_) order indexOf "c" names `shouldSatisfy` (< indexOf "b" names) indexOf "b" names `shouldSatisfy` (< indexOf "a" names) Left err -> expectationFailure $ "Expected Right, got: " ++ show err@@ -77,7 +79,7 @@ g = fromModules [a, b, c, d] case topoSort g of Right order -> do- let names = map (.instanceModule) order+ let names = map (^. #module_) order length names `shouldBe` 4 indexOf "d" names `shouldSatisfy` (< indexOf "b" names) indexOf "d" names `shouldSatisfy` (< indexOf "c" names)@@ -115,7 +117,7 @@ g = fromModules [a, b, c, d, e] case topoSort g of Right order -> do- let names = map (.instanceModule) order+ let names = map (^. #module_) order length names `shouldBe` 5 indexOf "e" names `shouldSatisfy` (< indexOf "d" names) indexOf "e" names `shouldSatisfy` (< indexOf "c" names)@@ -125,7 +127,7 @@ Left err -> expectationFailure $ "Expected Right, got: " ++ show err describe "multi-instantiation" $ do- it "treats two dependency edges with different depVars as distinct instances" $ do+ it "treats two dependency edges with different vars as distinct instances" $ do let helper = mkModule "helper" [] -- Parent depends on 'helper' twice with different bindings. parent' =
test/Seihou/Composition/InstanceSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Composition.InstanceSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Composition.Instance@@ -21,8 +23,8 @@ it "appends a stable hash suffix when parent bindings are present" $ do let inst = mkInstance "claude-skill-link" (ParentVars (Map.singleton "skill.name" "exec-plan")) qn = qualifiedName inst- T.isPrefixOf "claude-skill-link#" (qn.unModuleName) `shouldBe` True- T.length (qn.unModuleName) `shouldBe` T.length "claude-skill-link#" + 8+ T.isPrefixOf "claude-skill-link#" (qn ^. #unModuleName) `shouldBe` True+ T.length (qn ^. #unModuleName) `shouldBe` T.length "claude-skill-link#" + 8 it "produces a distinct qualified name for each distinct binding" $ do let a = mkInstance "claude-skill-link" (ParentVars (Map.singleton "skill.name" "exec-plan"))
test/Seihou/Composition/ResolveSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Composition.ResolveSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Seihou.Composition.Instance (ModuleInstance (..), mkInstance, primaryInstance)@@ -71,7 +73,7 @@ -- using 'emptyParentVars' for every module. Existing single-instance -- tests use this to migrate onto the new API without churn. asInstances :: [(Module, FilePath)] -> [(ModuleInstance, Module, FilePath)]-asInstances pairs = [(primaryInstance m.name, m, dir) | (m, dir) <- pairs]+asInstances pairs = [(primaryInstance (m ^. #name), m, dir) | (m, dir) <- pairs] -- | Look up the resolved variables for a module by its bare name, -- assuming the composition contains a single primary instance of it.@@ -108,7 +110,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "project.name") `shouldBe` VText "default"+ (^. #value) (baseVars Map.! "project.name") `shouldBe` VText "default" it "reuses saved instance values below CLI and above ambient sources" $ do let m = mkModule "base" [] [mkTextVar "project.name" (Just (VText "new-default")) False] []@@ -120,11 +122,11 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let resolved = result Map.! instanceId Map.! "project.name"- resolved.value `shouldBe` VText "accepted"- resolved.source `shouldBe` FromApplication+ (resolved ^. #value) `shouldBe` VText "accepted"+ (resolved ^. #source) `shouldBe` FromApplication case resolveComposedVariablesWithSaved modules saved (Map.singleton "project.name" "explicit") env "" "" Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs- Right result -> (result Map.! instanceId Map.! "project.name").source `shouldBe` FromCLI+ Right result -> ((result Map.! instanceId Map.! "project.name") ^. #source) `shouldBe` FromCLI it "re-coerces saved values through changed candidate declarations" $ do let countDecl =@@ -161,8 +163,8 @@ Right result -> do let resolved = result Map.! instanceId Map.keys resolved `shouldBe` ["project.kept", "project.new"]- (resolved Map.! "project.kept").source `shouldBe` FromApplication- (resolved Map.! "project.new").source `shouldBe` FromDefault+ ((resolved Map.! "project.kept") ^. #source) `shouldBe` FromApplication+ ((resolved Map.! "project.new") ^. #source) `shouldBe` FromDefault it "flows exported variable from dependency to dependent" $ do let base = mkModule "base" [] [mkTextVar "project.name" (Just (VText "my-app")) False] [mkExport "project.name"]@@ -172,7 +174,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let appVars = byName "app" result- (.value) (appVars Map.! "project.name") `shouldBe` VText "my-app"+ (^. #value) (appVars Map.! "project.name") `shouldBe` VText "my-app" it "export overrides module's own default" $ do let base = mkModule "base" [] [mkTextVar "project.name" (Just (VText "from-base")) False] [mkExport "project.name"]@@ -182,7 +184,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let appVars = byName "app" result- (.value) (appVars Map.! "project.name") `shouldBe` VText "from-base"+ (^. #value) (appVars Map.! "project.name") `shouldBe` VText "from-base" it "CLI override beats exported value" $ do let base = mkModule "base" [] [mkTextVar "project.name" (Just (VText "from-base")) False] [mkExport "project.name"]@@ -193,7 +195,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let appVars = byName "app" result- (.value) (appVars Map.! "project.name") `shouldBe` VText "from-cli"+ (^. #value) (appVars Map.! "project.name") `shouldBe` VText "from-cli" it "inherits non-declared exports from dependency" $ do let base = mkModule "base" [] [mkTextVar "project.name" (Just (VText "my-app")) False] [mkExport "project.name"]@@ -205,9 +207,9 @@ Right result -> do let appVars = byName "app" result -- app inherits project.name even though it doesn't declare it- (.value) (appVars Map.! "project.name") `shouldBe` VText "my-app"+ (^. #value) (appVars Map.! "project.name") `shouldBe` VText "my-app" -- app also has its own variable- (.value) (appVars Map.! "app.version") `shouldBe` VText "1.0"+ (^. #value) (appVars Map.! "app.version") `shouldBe` VText "1.0" it "handles aliased exports" $ do let base = mkModule "base" [] [mkTextVar "project.name" (Just (VText "my-app")) False] [mkExportAs "project.name" "app.name"]@@ -217,7 +219,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let appVars = byName "app" result- (.value) (appVars Map.! "app.name") `shouldBe` VText "my-app"+ (^. #value) (appVars Map.! "app.name") `shouldBe` VText "my-app" it "handles diamond dependency with shared export" $ do let d = mkModule "d" [] [mkTextVar "sys.arch" (Just (VText "x86_64")) False] [mkExport "sys.arch"]@@ -229,10 +231,10 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do -- All modules should see sys.arch from d- (.value) (byName "d" result Map.! "sys.arch") `shouldBe` VText "x86_64"- (.value) (byName "b" result Map.! "sys.arch") `shouldBe` VText "x86_64"- (.value) (byName "c" result Map.! "sys.arch") `shouldBe` VText "x86_64"- (.value) (byName "a" result Map.! "sys.arch") `shouldBe` VText "x86_64"+ (^. #value) (byName "d" result Map.! "sys.arch") `shouldBe` VText "x86_64"+ (^. #value) (byName "b" result Map.! "sys.arch") `shouldBe` VText "x86_64"+ (^. #value) (byName "c" result Map.! "sys.arch") `shouldBe` VText "x86_64"+ (^. #value) (byName "a" result Map.! "sys.arch") `shouldBe` VText "x86_64" describe "resolveComposedVariables (with config layers)" $ do it "resolves from global config when no other source provides value" $ do@@ -243,8 +245,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "license") `shouldBe` VText "MIT"- (.source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig+ (^. #value) (baseVars Map.! "license") `shouldBe` VText "MIT"+ (^. #source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig it "local config overrides global config in composed resolution" $ do let m = mkModule "base" [] [mkTextVar "license" Nothing True] []@@ -255,8 +257,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "license") `shouldBe` VText "BSD3"- (.source) (baseVars Map.! "license") `shouldBe` FromLocalConfig+ (^. #value) (baseVars Map.! "license") `shouldBe` VText "BSD3"+ (^. #source) (baseVars Map.! "license") `shouldBe` FromLocalConfig it "CLI override beats config layers in composed resolution" $ do let m = mkModule "base" [] [mkTextVar "license" Nothing True] []@@ -268,8 +270,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "license") `shouldBe` VText "cli-license"- (.source) (baseVars Map.! "license") `shouldBe` FromCLI+ (^. #value) (baseVars Map.! "license") `shouldBe` VText "cli-license"+ (^. #source) (baseVars Map.! "license") `shouldBe` FromCLI it "config layers flow through multi-module composition" $ do let base = mkModule "base" [] [mkTextVar "license" Nothing True] [mkExport "license"]@@ -281,11 +283,11 @@ Right result -> do -- base gets license from global config let baseVars = byName "base" result- (.value) (baseVars Map.! "license") `shouldBe` VText "MIT"- (.source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig+ (^. #value) (baseVars Map.! "license") `shouldBe` VText "MIT"+ (^. #source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig -- app also gets license from global config (it declares the var) let appVars = byName "app" result- (.value) (appVars Map.! "license") `shouldBe` VText "MIT"+ (^. #value) (appVars Map.! "license") `shouldBe` VText "MIT" describe "end-to-end config hierarchy auto-resolution" $ do it "resolves all variables from different config layers with correct precedence" $ do@@ -307,17 +309,17 @@ Right result -> do let vars = byName "haskell-app" result -- CLI wins for project.name- (.value) (vars Map.! "project.name") `shouldBe` VText "my-app"- (.source) (vars Map.! "project.name") `shouldBe` FromCLI+ (^. #value) (vars Map.! "project.name") `shouldBe` VText "my-app"+ (^. #source) (vars Map.! "project.name") `shouldBe` FromCLI -- Env wins over global config for license- (.value) (vars Map.! "license") `shouldBe` VText "Apache"- (.source) (vars Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"+ (^. #value) (vars Map.! "license") `shouldBe` VText "Apache"+ (^. #source) (vars Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE" -- Local config provides haskell.ghc- (.value) (vars Map.! "haskell.ghc") `shouldBe` VText "9.12.2"- (.source) (vars Map.! "haskell.ghc") `shouldBe` FromLocalConfig+ (^. #value) (vars Map.! "haskell.ghc") `shouldBe` VText "9.12.2"+ (^. #source) (vars Map.! "haskell.ghc") `shouldBe` FromLocalConfig -- Global config provides author.name- (.value) (vars Map.! "author.name") `shouldBe` VText "Jane Doe"- (.source) (vars Map.! "author.name") `shouldBe` FromGlobalConfig+ (^. #value) (vars Map.! "author.name") `shouldBe` VText "Jane Doe"+ (^. #source) (vars Map.! "author.name") `shouldBe` FromGlobalConfig it "optional variables without values are omitted, not errors" $ do let decls =@@ -331,7 +333,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let vars = byName "test" result- (.value) (vars Map.! "project.name") `shouldBe` VText "app"+ (^. #value) (vars Map.! "project.name") `shouldBe` VText "app" Map.member "optional.missing" vars `shouldBe` False it "diagnostics detect unused config keys and unresolved optional vars" $ do@@ -347,7 +349,7 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let allResolved = Map.unions (Map.elems result)- allDecls = concatMap (\(_, mm, _) -> mm.vars) modules+ allDecls = concatMap (\(_, mm, _) -> mm ^. #vars) modules (unusedKeys, unresolvedOpt) = diagnoseResolution allResolved allDecls Map.empty Map.empty Map.empty globalCfg unusedKeys `shouldBe` [VarName "typo.key"] unresolvedOpt `shouldBe` [VarName "optional.unset"]@@ -361,8 +363,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "user.email") `shouldBe` VText "work@example.com"- (.source) (baseVars Map.! "user.email") `shouldBe` FromContextConfig "work"+ (^. #value) (baseVars Map.! "user.email") `shouldBe` VText "work@example.com"+ (^. #source) (baseVars Map.! "user.email") `shouldBe` FromContextConfig "work" it "context flows through multi-module composition" $ do let base = mkModule "base" [] [mkTextVar "user.email" Nothing True] [mkExport "user.email"]@@ -373,9 +375,9 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let baseVars = byName "base" result- (.value) (baseVars Map.! "user.email") `shouldBe` VText "work@example.com"+ (^. #value) (baseVars Map.! "user.email") `shouldBe` VText "work@example.com" let appVars = byName "app" result- (.value) (appVars Map.! "user.email") `shouldBe` VText "work@example.com"+ (^. #value) (appVars Map.! "user.email") `shouldBe` VText "work@example.com" it "multi-module composition: config values flow through exports" $ do let baseDecls =@@ -396,14 +398,14 @@ Right result -> do -- base: local overrides global for project.name let baseVars = byName "base" result- (.value) (baseVars Map.! "project.name") `shouldBe` VText "local-app"- (.source) (baseVars Map.! "project.name") `shouldBe` FromLocalConfig- (.value) (baseVars Map.! "license") `shouldBe` VText "MIT"- (.source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig+ (^. #value) (baseVars Map.! "project.name") `shouldBe` VText "local-app"+ (^. #source) (baseVars Map.! "project.name") `shouldBe` FromLocalConfig+ (^. #value) (baseVars Map.! "license") `shouldBe` VText "MIT"+ (^. #source) (baseVars Map.! "license") `shouldBe` FromGlobalConfig -- app: same values, same precedence (declares its own vars, config wins) let appVars = byName "app" result- (.value) (appVars Map.! "project.name") `shouldBe` VText "local-app"- (.value) (appVars Map.! "license") `shouldBe` VText "MIT"+ (^. #value) (appVars Map.! "project.name") `shouldBe` VText "local-app"+ (^. #value) (appVars Map.! "license") `shouldBe` VText "MIT" describe "resolveComposedVariables (parameterized dependencies)" $ do it "parent-supplied var resolves in dependency" $ do@@ -416,8 +418,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let childVars = Map.findWithDefault Map.empty childInst result- (.value) (childVars Map.! "skill.name") `shouldBe` VText "exec-plan"- (.source) (childVars Map.! "skill.name") `shouldBe` FromParent "parent"+ (^. #value) (childVars Map.! "skill.name") `shouldBe` VText "exec-plan"+ (^. #source) (childVars Map.! "skill.name") `shouldBe` FromParent "parent" it "parent-supplied var overrides dependency's default" $ do let child = mkModule "child" [] [mkTextVar "skill.name" (Just (VText "old")) False] []@@ -429,8 +431,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let childVars = Map.findWithDefault Map.empty childInst result- (.value) (childVars Map.! "skill.name") `shouldBe` VText "new"- (.source) (childVars Map.! "skill.name") `shouldBe` FromParent "parent"+ (^. #value) (childVars Map.! "skill.name") `shouldBe` VText "new"+ (^. #source) (childVars Map.! "skill.name") `shouldBe` FromParent "parent" it "CLI override beats parent-supplied var" $ do let child = mkModule "child" [] [mkTextVar "skill.name" Nothing True] []@@ -443,8 +445,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let childVars = Map.findWithDefault Map.empty childInst result- (.value) (childVars Map.! "skill.name") `shouldBe` VText "from-cli"- (.source) (childVars Map.! "skill.name") `shouldBe` FromCLI+ (^. #value) (childVars Map.! "skill.name") `shouldBe` VText "from-cli"+ (^. #source) (childVars Map.! "skill.name") `shouldBe` FromCLI it "config beats parent-supplied var" $ do let child = mkModule "child" [] [mkTextVar "skill.name" (Just (VText "default")) False] []@@ -457,8 +459,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right result -> do let childVars = Map.findWithDefault Map.empty childInst result- (.value) (childVars Map.! "skill.name") `shouldBe` VText "global-val"- (.source) (childVars Map.! "skill.name") `shouldBe` FromGlobalConfig+ (^. #value) (childVars Map.! "skill.name") `shouldBe` VText "global-val"+ (^. #source) (childVars Map.! "skill.name") `shouldBe` FromGlobalConfig it "two parents supplying different bindings produce two distinct child instances" $ do -- The regression case from ExecPlan 10: master-plan and exec-plan both@@ -498,5 +500,5 @@ Map.size result `shouldBe` 4 let varsA = Map.findWithDefault Map.empty instA result varsB = Map.findWithDefault Map.empty instB result- (.value) (varsA Map.! "skill.name") `shouldBe` VText "exec-plan"- (.value) (varsB Map.! "skill.name") `shouldBe` VText "master-plan"+ (^. #value) (varsA Map.! "skill.name") `shouldBe` VText "exec-plan"+ (^. #value) (varsB Map.! "skill.name") `shouldBe` VText "master-plan"
test/Seihou/Core/AgentPromptSpec.hs view
@@ -1,7 +1,10 @@ module Seihou.Core.AgentPromptSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels ()+import Data.List (isPrefixOf) import Data.Text qualified as T-import Seihou.Core.AgentPrompt (validateAgentPrompt)+import Seihou.Core.AgentPrompt (checkAgentPromptLaunch, validateAgentPrompt) import Seihou.Core.Module (DiscoveredRunnable (..), RunnableKind (..), discoverAllRunnables, discoverRunnable) import Seihou.Core.Types import Seihou.Dhall.Eval (evalAgentPromptFromFile)@@ -33,32 +36,36 @@ withAgentPromptName :: ModuleName -> AgentPrompt -> AgentPrompt withAgentPromptName n p =- AgentPrompt n p.version p.description p.prompt p.vars p.prompts p.commandVars p.guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt n (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) (p ^. #prompts) (p ^. #commandVars) (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptPrompt :: T.Text -> AgentPrompt -> AgentPrompt withAgentPromptPrompt body p =- AgentPrompt p.name p.version p.description body p.vars p.prompts p.commandVars p.guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) body (p ^. #vars) (p ^. #prompts) (p ^. #commandVars) (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptVars :: [VarDecl] -> AgentPrompt -> AgentPrompt withAgentPromptVars vars p =- AgentPrompt p.name p.version p.description p.prompt vars p.prompts p.commandVars p.guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) vars (p ^. #prompts) (p ^. #commandVars) (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptPrompts :: [Prompt] -> AgentPrompt -> AgentPrompt withAgentPromptPrompts prompts p =- AgentPrompt p.name p.version p.description p.prompt p.vars prompts p.commandVars p.guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) prompts (p ^. #commandVars) (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptCommandVars :: [CommandVar] -> AgentPrompt -> AgentPrompt withAgentPromptCommandVars commandVars p =- AgentPrompt p.name p.version p.description p.prompt p.vars p.prompts commandVars p.guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) (p ^. #prompts) commandVars (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptGuidance :: [PromptGuidance] -> AgentPrompt -> AgentPrompt withAgentPromptGuidance guidance p =- AgentPrompt p.name p.version p.description p.prompt p.vars p.prompts p.commandVars guidance p.files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) (p ^. #prompts) (p ^. #commandVars) guidance (p ^. #files) (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) withAgentPromptFiles :: [BlueprintFile] -> AgentPrompt -> AgentPrompt withAgentPromptFiles files p =- AgentPrompt p.name p.version p.description p.prompt p.vars p.prompts p.commandVars p.guidance files p.allowedTools p.tags p.launch+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) (p ^. #prompts) (p ^. #commandVars) (p ^. #guidance) files (p ^. #allowedTools) (p ^. #tags) (p ^. #launch) +withAgentPromptLaunch :: Maybe AgentLaunch -> AgentPrompt -> AgentPrompt+withAgentPromptLaunch launch p =+ AgentPrompt (p ^. #name) (p ^. #version) (p ^. #description) (p ^. #prompt) (p ^. #vars) (p ^. #prompts) (p ^. #commandVars) (p ^. #guidance) (p ^. #files) (p ^. #allowedTools) (p ^. #tags) launch+ hasError :: T.Text -> [T.Text] -> Bool hasError needle = any (T.isInfixOf needle) @@ -73,18 +80,46 @@ result <- evalAgentPromptFromFile (promptDir </> "prompt.dhall") case result of Right p -> do- p.name `shouldBe` "review-changes"- p.description `shouldBe` Just "Review local changes"- length p.commandVars `shouldBe` 1- p.guidance+ (p ^. #name) `shouldBe` "review-changes"+ (p ^. #description) `shouldBe` Just "Review local changes"+ length (p ^. #commandVars) `shouldBe` 1+ (p ^. #guidance) `shouldBe` [ PromptGuidance "Repository workflow" "Prefer focused validation commands." (Just (ExprEq "git.branch" (VText "main"))) ]- fmap (.provider) p.launch `shouldBe` Just (Just "codex-cli")+ -- This fixture's launch record predates the effort field, so it+ -- doubles as the regression test that effort is defaulted rather+ -- than required.+ (p ^. #launch)+ `shouldBe` Just+ AgentLaunch+ { provider = Just "codex-cli",+ model = Nothing,+ effort = Nothing,+ mode = Nothing+ } Left err -> expectationFailure ("Expected Right, got: " <> show err) + it "decodes a prompt launch record that declares an effort" $ do+ withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do+ let promptDir = tmpDir </> "deep-review"+ createDirectoryIfMissing True promptDir+ writeFile (promptDir </> "prompt.dhall") (samplePromptDhallWithEffort "deep-review")+ result <- evalAgentPromptFromFile (promptDir </> "prompt.dhall")+ case result of+ Right p ->+ (p ^. #launch)+ `shouldBe` Just+ AgentLaunch+ { provider = Just "claude-cli",+ model = Just "claude-sonnet-5",+ effort = Just "max",+ mode = Nothing+ }+ Left err -> expectationFailure ("Expected Right, got: " <> show err)+ it "decodes prompt.dhall without guidance as an empty list" $ do withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do let promptDir = tmpDir </> "review-changes"@@ -92,7 +127,7 @@ writeFile (promptDir </> "prompt.dhall") (samplePromptDhallWithoutGuidance "review-changes") result <- evalAgentPromptFromFile (promptDir </> "prompt.dhall") case result of- Right p -> p.guidance `shouldBe` []+ Right p -> (p ^. #guidance) `shouldBe` [] Left err -> expectationFailure ("Expected Right, got: " <> show err) describe "validateAgentPrompt" $ do@@ -100,7 +135,7 @@ withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do result <- validateAgentPrompt tmpDir goodAgentPrompt case result of- Right p -> p.name `shouldBe` "review-changes"+ Right p -> (p ^. #name) `shouldBe` "review-changes" Left err -> expectationFailure ("Expected Right, got: " <> show err) it "rejects an invalid prompt name" $ do@@ -172,7 +207,7 @@ goodAgentPrompt result <- validateAgentPrompt tmpDir guided case result of- Right p -> length p.guidance `shouldBe` 2+ Right p -> length (p ^. #guidance) `shouldBe` 2 Left err -> expectationFailure ("Expected Right, got: " <> show err) it "rejects guidance with blank titles or bodies" $ do@@ -202,6 +237,34 @@ hasError "guidance 'Missing' references undeclared variable: repo.kind" errs `shouldBe` True other -> expectationFailure ("Expected ValidationError, got: " <> show other) + it "rejects a blank declared launch field" $ do+ withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do+ let bad =+ withAgentPromptLaunch+ (Just AgentLaunch {provider = Nothing, model = Just " ", effort = Just "", mode = Nothing})+ goodAgentPrompt+ result <- validateAgentPrompt tmpDir bad+ case result of+ Left (ValidationError _ errs) -> do+ hasError "launch.model, if specified, must not be empty" errs `shouldBe` True+ hasError "launch.effort, if specified, must not be empty" errs `shouldBe` True+ other -> expectationFailure ("Expected ValidationError, got: " <> show other)++ it "accepts a fully populated launch record" $ do+ withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do+ let p =+ withAgentPromptLaunch+ (Just AgentLaunch {provider = Just "claude-cli", model = Just "claude-sonnet-5", effort = Just "max", mode = Nothing})+ goodAgentPrompt+ checkAgentPromptLaunch p `shouldBe` []+ result <- validateAgentPrompt tmpDir p+ case result of+ Right _ -> pure ()+ Left err -> expectationFailure ("Expected Right, got: " <> show err)++ it "accepts a prompt that declares no launch record" $+ checkAgentPromptLaunch goodAgentPrompt `shouldBe` []+ it "checks referenced prompt files under files/" $ do withSystemTempDirectory "seihou-prompt" $ \tmpDir -> do let bad =@@ -223,7 +286,7 @@ result <- discoverRunnable [tmpDir] "review-changes" case result of Right (RunnableAgentPrompt p dir) -> do- p.name `shouldBe` "review-changes"+ (p ^. #name) `shouldBe` "review-changes" dir `shouldBe` promptDir other -> expectationFailure ("Expected RunnableAgentPrompt, got: " <> show other) @@ -234,7 +297,7 @@ writeFile (promptDir </> "prompt.dhall") (samplePromptDhall "review-changes") found <- discoverAllRunnables [tmpDir] case found of- [DiscoveredRunnable {drKind = kind}] -> kind `shouldBe` KindPrompt+ [DiscoveredRunnable {kind = kind}] -> kind `shouldBe` KindPrompt other -> expectationFailure ("Expected one discovered prompt, got: " <> show other) it "prefers blueprint.dhall over prompt.dhall in the same directory" $ do@@ -292,6 +355,22 @@ ", launch = Some { provider = Some \"codex-cli\", mode = None Text, model = None Text }", "}" ]++-- | Like 'samplePromptDhall' but its launch record declares a model and an+-- effort, as an artifact authored against the current schema would.+samplePromptDhallWithEffort :: T.Text -> String+samplePromptDhallWithEffort n =+ unlines $+ -- drop the closing brace and the fixture's own three-field launch line+ filter (not . isPrefixOf ", launch =") (init (lines (samplePromptDhall n)))+ <> [ ", launch = Some",+ " { provider = Some \"claude-cli\"",+ " , model = Some \"claude-sonnet-5\"",+ " , effort = Some \"max\"",+ " , mode = None Text",+ " }",+ "}"+ ] samplePromptDhallWithoutGuidance :: T.Text -> String samplePromptDhallWithoutGuidance n =
test/Seihou/Core/ApplicationSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.ApplicationSpec (tests) where +import Control.Lens ((&), (?~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -59,7 +61,7 @@ AppliedComposition { applicationId = mkApplicationId target additional, target = target,- targetSource = "/modules/root",+ targetOrigin = LocalOrigin "root", targetVersion = Just "1.0.0", additionalModules = additional, namespace = Just "root",@@ -76,7 +78,7 @@ let first = mkApplicationId moduleTarget [ModuleName "docs"] second = mkApplicationId moduleTarget [ModuleName "docs"] first `shouldBe` second- T.length first.unApplicationId `shouldBe` 64+ T.length (first ^. #unApplicationId) `shouldBe` 64 it "changes when additional-root order changes" $ do let first = mkApplicationId moduleTarget [ModuleName "a", ModuleName "b"]@@ -95,16 +97,19 @@ inst1 = ModuleInstance moduleName pv1 inst2 = ModuleInstance moduleName pv2 modul = mkModule moduleName (Just "0.7.0")- modulesInOrder = [(inst1, modul, "/modules/link-skill"), (inst2, modul, "/modules/link-skill")]+ modulesInOrder =+ [ (inst1, modul, LocalOrigin "link-skill"),+ (inst2, modul, LocalOrigin "link-skill")+ ] resolved = Map.fromList [ (inst1, Map.singleton (VarName "skill.name") (mkResolved "skill.name" (VText "exec-plan"))), (inst2, Map.singleton (VarName "skill.name") (mkResolved "skill.name" (VText "master-plan"))) ] composition =- buildAppliedComposition moduleTarget "/modules/master-plan" (Just "0.7.0") [] (Just "docs") Nothing modulesInOrder resolved fixedTime- map (.parentVars) composition.instances `shouldBe` [pv1, pv2]- map (.resolvedVars) composition.instances+ buildAppliedComposition moduleTarget (LocalOrigin "master-plan") (Just "0.7.0") [] (Just "docs") Nothing modulesInOrder resolved fixedTime+ map (^. #parentVars) (composition ^. #instances) `shouldBe` [pv1, pv2]+ map (^. #resolvedVars) (composition ^. #instances) `shouldBe` [Map.singleton "skill.name" "exec-plan", Map.singleton "skill.name" "master-plan"] it "keeps identity independent of versions, source paths, and resolved values" $ do@@ -112,39 +117,39 @@ first = buildAppliedComposition moduleTarget- "/old/root"+ (LocalOrigin "root") (Just "1.0.0") ["extra"] Nothing Nothing- [(inst, mkModule "dep" (Just "1.0.0"), "/old/dep")]+ [(inst, mkModule "dep" (Just "1.0.0"), LocalOrigin "dep")] (Map.singleton inst (Map.singleton "value" (mkResolved "value" (VText "old")))) fixedTime second = buildAppliedComposition moduleTarget- "/new/root"+ (LocalOrigin "root") (Just "2.0.0") ["extra"] Nothing Nothing- [(inst, mkModule "dep" (Just "2.0.0"), "/new/dep")]+ [(inst, mkModule "dep" (Just "2.0.0"), LocalOrigin "dep")] (Map.singleton inst (Map.singleton "value" (mkResolved "value" (VText "new")))) fixedTime- first.applicationId `shouldBe` second.applicationId+ (first ^. #applicationId) `shouldBe` (second ^. #applicationId) it "preserves the original module or recipe target" $ do- let moduleComposition = buildAppliedComposition moduleTarget "/module" Nothing [] Nothing Nothing [] Map.empty fixedTime+ let moduleComposition = buildAppliedComposition moduleTarget (LocalOrigin "module") Nothing [] Nothing Nothing [] Map.empty fixedTime recipeTarget = AppliedRecipeTarget "service"- recipeComposition = buildAppliedComposition recipeTarget "/recipe" (Just "2") [] Nothing Nothing [] Map.empty fixedTime- moduleComposition.target `shouldBe` moduleTarget- recipeComposition.target `shouldBe` recipeTarget+ recipeComposition = buildAppliedComposition recipeTarget (LocalOrigin "recipe") (Just "2") [] Nothing Nothing [] Map.empty fixedTime+ (moduleComposition ^. #target) `shouldBe` moduleTarget+ (recipeComposition ^. #target) `shouldBe` recipeTarget describe "replaceAppliedComposition" $ do it "replaces in place and appends new applications" $ do let first = mkComposition moduleTarget [] second = mkComposition (AppliedModuleTarget "other") []- replacement = first {targetVersion = Just "2.0.0"}+ replacement = (first & #targetVersion ?~ "2.0.0") third = mkComposition (AppliedRecipeTarget "third") [] replaceAppliedComposition replacement [first, second] `shouldBe` [replacement, second] replaceAppliedComposition third [first, second] `shouldBe` [first, second, third]@@ -156,5 +161,5 @@ prior = FileRecord (hashContent "old") "module" Template fixedTime Nothing (Set.singleton priorId) current = FileRecord (hashContent "new") "module" Template fixedTime (Just (BaselineRef (hashContent "generated"))) Set.empty attached = attachApplication currentId (Just prior) current- attached.applicationIds `shouldBe` Set.fromList [priorId, currentId]- attached.baseline `shouldBe` Just (BaselineRef (hashContent "generated"))+ (attached ^. #applicationIds) `shouldBe` Set.fromList [priorId, currentId]+ (attached ^. #baseline) `shouldBe` Just (BaselineRef (hashContent "generated"))
+ test/Seihou/Core/ArtifactOriginDetectSpec.hs view
@@ -0,0 +1,83 @@+module Seihou.Core.ArtifactOriginDetectSpec (tests) where++import Seihou.Core.ArtifactOriginDetect (detectArtifactOrigin)+import Seihou.Core.Types+import System.Directory (createDirectoryIfMissing)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory)+import Test.Hspec+import Test.Tasty+import Test.Tasty.Hspec (testSpec)++tests :: IO TestTree+tests = testSpec "Seihou.Core.ArtifactOriginDetect" spec++-- | Lay out a scratch project root and a sibling "installed" root, both+-- inside one temporary directory, and hand both to the test body.+withRoots :: (FilePath -> FilePath -> IO a) -> IO a+withRoots body =+ withSystemTempDirectory "seihou-artifact-origin" $ \tmpDir -> do+ let projectRoot = tmpDir </> "project"+ installedRoot = tmpDir </> "installed"+ createDirectoryIfMissing True projectRoot+ createDirectoryIfMissing True installedRoot+ body projectRoot installedRoot++spec :: Spec+spec = do+ describe "detectArtifactOrigin" $ do+ it "records a directory inside the project as a project-relative origin" $ do+ withRoots $ \projectRoot _installedRoot -> do+ let moduleDir = projectRoot </> ".seihou" </> "modules" </> "foo"+ createDirectoryIfMissing True moduleDir+ origin <- detectArtifactOrigin projectRoot moduleDir+ origin `shouldBe` ProjectOrigin ".seihou/modules/foo"++ it "records an installed directory with origin metadata as a remote origin" $ do+ withRoots $ \projectRoot installedRoot -> do+ let moduleDir = installedRoot </> "haskell-base"+ createDirectoryIfMissing True moduleDir+ writeFile+ (moduleDir </> ".seihou-origin.json")+ "{\"sourceUrl\":\"https://github.com/shinzui/seihou-modules.git\",\"repoName\":\"seihou-modules\",\"version\":\"1.4.0\"}"+ origin <- detectArtifactOrigin projectRoot moduleDir+ origin+ `shouldBe` RemoteOrigin+ "https://github.com/shinzui/seihou-modules.git"+ "haskell-base"+ (Just "seihou-modules")++ it "omits the repository name when the metadata does not record one" $ do+ withRoots $ \projectRoot installedRoot -> do+ let moduleDir = installedRoot </> "haskell-base"+ createDirectoryIfMissing True moduleDir+ writeFile+ (moduleDir </> ".seihou-origin.json")+ "{\"sourceUrl\":\"https://example.com/mods.git\"}"+ origin <- detectArtifactOrigin projectRoot moduleDir+ origin `shouldBe` RemoteOrigin "https://example.com/mods.git" "haskell-base" Nothing++ it "falls back to a local origin when the metadata is malformed" $ do+ withRoots $ \projectRoot installedRoot -> do+ let moduleDir = installedRoot </> "haskell-base"+ createDirectoryIfMissing True moduleDir+ writeFile (moduleDir </> ".seihou-origin.json") "not json at all"+ origin <- detectArtifactOrigin projectRoot moduleDir+ origin `shouldBe` LocalOrigin "haskell-base"++ it "falls back to a local origin when there is no metadata file" $ do+ withRoots $ \projectRoot installedRoot -> do+ let moduleDir = installedRoot </> "scratch-module"+ createDirectoryIfMissing True moduleDir+ origin <- detectArtifactOrigin projectRoot moduleDir+ origin `shouldBe` LocalOrigin "scratch-module"++ it "treats the project root itself as outside the project" $ do+ withRoots $ \projectRoot _installedRoot -> do+ origin <- detectArtifactOrigin projectRoot projectRoot+ origin `shouldBe` LocalOrigin "project"++ it "classifies a directory that does not exist without throwing" $ do+ withRoots $ \projectRoot _installedRoot -> do+ origin <- detectArtifactOrigin projectRoot (projectRoot </> ".seihou" </> "modules" </> "ghost")+ origin `shouldBe` ProjectOrigin ".seihou/modules/ghost"
+ test/Seihou/Core/ArtifactRefSpec.hs view
@@ -0,0 +1,141 @@+module Seihou.Core.ArtifactRefSpec (tests) where++import Control.Lens ((^.))+import Data.Generics.Labels ()+import Data.Text qualified as T+import GHC.Generics (Generic)+import Seihou.Core.ArtifactRef+import Seihou.Core.Types+import System.Directory (createDirectoryIfMissing)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory)+import Test.Hspec+import Test.Tasty+import Test.Tasty.Hspec (testSpec)++tests :: IO TestTree+tests = testSpec "Seihou.Core.ArtifactRef" spec++-- | A project root plus the three search paths seihou discovers through, in+-- the same order as 'Seihou.Core.Module.defaultSearchPaths': the project's+-- own modules, the developer's personal modules, and the install cache.+data Roots = Roots+ { projectRoot :: !FilePath,+ projectModules :: !FilePath,+ userModules :: !FilePath,+ installed :: !FilePath+ }+ deriving stock (Generic)++searchPathsOf :: Roots -> [FilePath]+searchPathsOf roots = [roots ^. #projectModules, roots ^. #userModules, roots ^. #installed]++withRoots :: (Roots -> IO a) -> IO a+withRoots body =+ withSystemTempDirectory "seihou-artifact-ref" $ \tmpDir -> do+ let roots =+ Roots+ { projectRoot = tmpDir </> "project",+ projectModules = tmpDir </> "project" </> ".seihou" </> "modules",+ userModules = tmpDir </> "home" </> "seihou" </> "modules",+ installed = tmpDir </> "home" </> "seihou" </> "installed"+ }+ mapM_ (createDirectoryIfMissing True) ((roots ^. #projectRoot) : searchPathsOf roots)+ body roots++-- | Create @<parent>/<name>/module.dhall@ and return the module directory.+plantModule :: FilePath -> String -> IO FilePath+plantModule parent name = do+ let directory = parent </> name+ createDirectoryIfMissing True directory+ writeFile (directory </> "module.dhall") "{- fixture -}"+ pure directory++resolve :: Roots -> ArtifactOrigin -> IO (Either ArtifactRefError FilePath)+resolve roots = resolveArtifactOrigin ((roots ^. #projectRoot)) (searchPathsOf roots) "module.dhall"++remoteOrigin :: ArtifactOrigin+remoteOrigin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules")++spec :: Spec+spec = do+ describe "resolveArtifactOrigin" $ do+ it "finds a remote-origin artifact in the install cache" $ do+ withRoots $ \roots -> do+ expected <- plantModule ((roots ^. #installed)) "haskell-base"+ resolve roots remoteOrigin `shouldReturn` Right expected++ it "lets a project-local copy shadow the installed one" $ do+ withRoots $ \roots -> do+ shadow <- plantModule ((roots ^. #projectModules)) "haskell-base"+ _ <- plantModule ((roots ^. #installed)) "haskell-base"+ resolve roots remoteOrigin `shouldReturn` Right shadow++ it "finds a local-origin artifact in the personal module directory" $ do+ withRoots $ \roots -> do+ expected <- plantModule ((roots ^. #userModules)) "scratch"+ resolve roots (LocalOrigin "scratch") `shouldReturn` Right expected++ it "reports every probed directory in order when nothing matches" $ do+ withRoots $ \roots -> do+ result <- resolve roots remoteOrigin+ result+ `shouldBe` Left+ ( ArtifactNotFoundLocally+ remoteOrigin+ [ (roots ^. #projectModules) </> "haskell-base",+ (roots ^. #userModules) </> "haskell-base",+ (roots ^. #installed) </> "haskell-base"+ ]+ )++ it "ignores a directory that has no definition file" $ do+ withRoots $ \roots -> do+ createDirectoryIfMissing True ((roots ^. #projectModules) </> "haskell-base")+ expected <- plantModule ((roots ^. #installed)) "haskell-base"+ resolve roots remoteOrigin `shouldReturn` Right expected++ it "resolves a project origin against the project root" $ do+ withRoots $ \roots -> do+ expected <- plantModule ((roots ^. #projectModules)) "docs"+ resolve roots (ProjectOrigin ".seihou/modules/docs") `shouldReturn` Right expected++ it "refuses to substitute an installed artifact for a missing project one" $ do+ withRoots $ \roots -> do+ _ <- plantModule ((roots ^. #installed)) "docs"+ result <- resolve roots (ProjectOrigin ".seihou/modules/docs")+ result+ `shouldBe` Left+ ( ProjectArtifactMissing+ (ProjectOrigin ".seihou/modules/docs")+ ((roots ^. #projectRoot) </> ".seihou" </> "modules" </> "docs")+ )++ describe "renderArtifactRefError" $ do+ it "names the recorded URL, every probed directory, and the install remedy" $ do+ withRoots $ \roots -> do+ Left err <- resolve roots remoteOrigin+ let message = renderArtifactRefError err+ message `shouldSatisfy` T.isInfixOf "haskell-base"+ message `shouldSatisfy` T.isInfixOf "https://github.com/shinzui/seihou-modules.git"+ message `shouldSatisfy` T.isInfixOf "seihou install https://github.com/shinzui/seihou-modules.git"+ mapM_+ (\directory -> message `shouldSatisfy` T.isInfixOf (T.pack directory))+ [ (roots ^. #projectModules) </> "haskell-base",+ (roots ^. #userModules) </> "haskell-base",+ (roots ^. #installed) </> "haskell-base"+ ]++ it "says a local-origin artifact has no upstream to fetch from" $ do+ withRoots $ \roots -> do+ Left err <- resolve roots (LocalOrigin "scratch")+ let message = renderArtifactRefError err+ message `shouldSatisfy` T.isInfixOf "no recorded upstream"+ message `shouldSatisfy` not . T.isInfixOf "seihou install"++ it "says a missing project artifact should have been committed" $ do+ withRoots $ \roots -> do+ Left err <- resolve roots (ProjectOrigin ".seihou/modules/docs")+ let message = renderArtifactRefError err+ message `shouldSatisfy` T.isInfixOf ".seihou/modules/docs"+ message `shouldSatisfy` T.isInfixOf "committed"
test/Seihou/Core/BlueprintSpec.hs view
@@ -1,8 +1,10 @@ module Seihou.Core.BlueprintSpec (tests) where +import Control.Lens (at, (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T-import Seihou.Core.Blueprint (checkBlueprintMigrations, validateBlueprintWith)+import Seihou.Core.Blueprint (checkBlueprintLaunch, checkBlueprintMigrations, validateBlueprintWith) import Seihou.Core.Migration (BlueprintMigration (..)) import Seihou.Core.Module (discoverRunnable) import Seihou.Core.Types@@ -46,6 +48,7 @@ Nothing [] []+ Nothing -- | Helpers to update individual 'Blueprint' fields without ambiguous -- record updates. Several @Blueprint@ fields collide by name with@@ -53,44 +56,48 @@ -- the ambiguity once and for all. withBlueprintName :: ModuleName -> Blueprint -> Blueprint withBlueprintName n b =- Blueprint n b.version b.description b.prompt b.vars b.prompts b.baseModules b.files b.allowedTools b.tags b.migrations+ Blueprint n (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintVersion :: Maybe T.Text -> Blueprint -> Blueprint withBlueprintVersion v b =- Blueprint b.name v b.description b.prompt b.vars b.prompts b.baseModules b.files b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) v (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintPrompt :: T.Text -> Blueprint -> Blueprint withBlueprintPrompt p b =- Blueprint b.name b.version b.description p b.vars b.prompts b.baseModules b.files b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) p (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintVars :: [VarDecl] -> Blueprint -> Blueprint withBlueprintVars vs b =- Blueprint b.name b.version b.description b.prompt vs b.prompts b.baseModules b.files b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) vs (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintPrompts :: [Prompt] -> Blueprint -> Blueprint withBlueprintPrompts ps b =- Blueprint b.name b.version b.description b.prompt b.vars ps b.baseModules b.files b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) ps (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintBaseModules :: [Dependency] -> Blueprint -> Blueprint withBlueprintBaseModules ds b =- Blueprint b.name b.version b.description b.prompt b.vars b.prompts ds b.files b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) ds (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintFiles :: [BlueprintFile] -> Blueprint -> Blueprint withBlueprintFiles fs b =- Blueprint b.name b.version b.description b.prompt b.vars b.prompts b.baseModules fs b.allowedTools b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) fs (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintAllowedTools :: Maybe [T.Text] -> Blueprint -> Blueprint withBlueprintAllowedTools at b =- Blueprint b.name b.version b.description b.prompt b.vars b.prompts b.baseModules b.files at b.tags b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) at (b ^. #tags) (b ^. #migrations) (b ^. #launch) withBlueprintTags :: [T.Text] -> Blueprint -> Blueprint withBlueprintTags ts b =- Blueprint b.name b.version b.description b.prompt b.vars b.prompts b.baseModules b.files b.allowedTools ts b.migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) ts (b ^. #migrations) (b ^. #launch) withBlueprintMigrations :: [BlueprintMigration] -> Blueprint -> Blueprint withBlueprintMigrations migrations b =- Blueprint b.name b.version b.description b.prompt b.vars b.prompts b.baseModules b.files b.allowedTools b.tags migrations+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) migrations (b ^. #launch) +withBlueprintLaunch :: Maybe AgentLaunch -> Blueprint -> Blueprint+withBlueprintLaunch launch b =+ Blueprint (b ^. #name) (b ^. #version) (b ^. #description) (b ^. #prompt) (b ^. #vars) (b ^. #prompts) (b ^. #baseModules) (b ^. #files) (b ^. #allowedTools) (b ^. #tags) (b ^. #migrations) launch+ spec :: Spec spec = do describe "evalBlueprintFromFile (sample fixture)" $ do@@ -99,15 +106,15 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right b -> do- b.name `shouldBe` ModuleName "sample-blueprint"- b.version `shouldBe` Just "0.1.0"- b.description `shouldBe` Just "Fixture blueprint for EP-29 tests"- T.isInfixOf "{{project.name}}" b.prompt `shouldBe` True- length b.vars `shouldBe` 2- b.tags `shouldBe` ["demo"]- b.baseModules `shouldBe` []- length b.files `shouldBe` 1- b.migrations+ (b ^. #name) `shouldBe` ModuleName "sample-blueprint"+ (b ^. #version) `shouldBe` Just "0.1.0"+ (b ^. #description) `shouldBe` Just "Fixture blueprint for EP-29 tests"+ T.isInfixOf "{{project.name}}" (b ^. #prompt) `shouldBe` True+ length (b ^. #vars) `shouldBe` 2+ (b ^. #tags) `shouldBe` ["demo"]+ (b ^. #baseModules) `shouldBe` []+ length (b ^. #files) `shouldBe` 1+ (b ^. #migrations) `shouldBe` [ BlueprintMigration "1.0.0" "2.0.0" "Update {{project.name}} for the first library release.", BlueprintMigration "2.5.0" "3.0.0" "Update {{project.name}} for the second library release." ]@@ -119,12 +126,60 @@ result <- evalBlueprintFromFile path case result of Right b ->- b.migrations+ (b ^. #migrations) `shouldBe` [ BlueprintMigration "1.0.0" "2.0.0" "first edge", BlueprintMigration "2.5.0" "3.0.0" "second edge" ] Left err -> expectationFailure ("Expected migrations to decode, got: " <> show err) + it "decodes a declared launch record" $ do+ withSystemTempDirectory "seihou-blueprint-launch-decode" $ \tmpDir -> do+ let path = tmpDir </> "blueprint.dhall"+ writeFile path (sampleBlueprintWithLaunchDhall "launch-bp")+ result <- evalBlueprintFromFile path+ case result of+ Right b ->+ (b ^. #launch)+ `shouldBe` Just+ AgentLaunch+ { provider = Just "codex-cli",+ model = Just "gpt-5.6-terra",+ effort = Just "max",+ mode = Just "reserved"+ }+ Left err -> expectationFailure ("Expected launch to decode, got: " <> show err)++ -- Regression: blueprints authored against a schema pin that predates the+ -- launch field must keep decoding. 'sampleBlueprintDhall' writes no+ -- @launch@ key at all, so this exercises the decoder's 'withDefaults'.+ it "decodes a blueprint with no launch field as Nothing" $ do+ withSystemTempDirectory "seihou-blueprint-nolaunch-decode" $ \tmpDir -> do+ let path = tmpDir </> "blueprint.dhall"+ writeFile path (sampleBlueprintDhall "no-launch-bp")+ result <- evalBlueprintFromFile path+ case result of+ Right b -> (b ^. #launch) `shouldBe` Nothing+ Left err -> expectationFailure ("Expected blueprint to decode, got: " <> show err)++ -- Regression: a launch record written against the older three-field+ -- schema (provider/mode/model, no effort) must still decode.+ it "decodes a launch record that predates the effort field" $ do+ withSystemTempDirectory "seihou-blueprint-oldlaunch-decode" $ \tmpDir -> do+ let path = tmpDir </> "blueprint.dhall"+ writeFile path (sampleBlueprintWithLegacyLaunchDhall "legacy-launch-bp")+ result <- evalBlueprintFromFile path+ case result of+ Right b ->+ (b ^. #launch)+ `shouldBe` Just+ AgentLaunch+ { provider = Just "claude-cli",+ model = Just "claude-opus-4-8",+ effort = Nothing,+ mode = Nothing+ }+ Left err -> expectationFailure ("Expected legacy launch to decode, got: " <> show err)+ describe "validateBlueprintWith (sample fixture)" $ do it "accepts the sample-blueprint fixture" $ do cwd <- getCurrentDirectory@@ -132,7 +187,7 @@ Right b <- evalBlueprintFromFile (baseDir </> "blueprint.dhall") result <- validateBlueprintWith [] baseDir b case result of- Right b' -> b'.name `shouldBe` "sample-blueprint"+ Right b' -> (b' ^. #name) `shouldBe` "sample-blueprint" Left err -> expectationFailure ("Expected Right, got: " <> show err) describe "validateBlueprintWith (rule-by-rule)" $ do@@ -182,7 +237,7 @@ required = False, validation = Nothing }- bad = withBlueprintVars (goodBlueprint.vars ++ [dup]) goodBlueprint+ bad = withBlueprintVars (goodBlueprint ^. #vars ++ [dup]) goodBlueprint result <- validateBlueprintWith [] tmpDir bad case result of Left (ValidationError _ errs) ->@@ -273,7 +328,7 @@ withSystemTempDirectory "seihou-test" $ \tmpDir -> do let bad = withBlueprintBaseModules- [Dependency {depModule = "nope-not-here", depVars = Map.empty}]+ [Dependency {module_ = "nope-not-here", vars = Map.empty}] goodBlueprint result <- validateBlueprintWith [tmpDir] tmpDir bad case result of@@ -288,7 +343,7 @@ writeFile (nestedDir </> "blueprint.dhall") (sampleBlueprintDhall "nested-bp") let bad = withBlueprintBaseModules- [Dependency {depModule = "nested-bp", depVars = Map.empty}]+ [Dependency {module_ = "nested-bp", vars = Map.empty}] goodBlueprint result <- validateBlueprintWith [tmpDir] tmpDir bad case result of@@ -296,6 +351,45 @@ hasError "resolves to a blueprint" errs `shouldBe` True other -> expectationFailure ("Expected ValidationError, got: " <> show other) + it "rejects a blank declared launch field" $ do+ withSystemTempDirectory "seihou-test" $ \tmpDir -> do+ let bad =+ withBlueprintLaunch+ (Just AgentLaunch {provider = Just " ", model = Nothing, effort = Nothing, mode = Nothing})+ goodBlueprint+ result <- validateBlueprintWith [] tmpDir bad+ case result of+ Left (ValidationError _ errs) ->+ hasError "launch.provider, if specified, must not be empty" errs `shouldBe` True+ other -> expectationFailure ("Expected ValidationError, got: " <> show other)++ it "reports every blank declared launch field" $ do+ let bad =+ withBlueprintLaunch+ (Just AgentLaunch {provider = Just "", model = Just " ", effort = Just "", mode = Just "\t"})+ goodBlueprint+ checkBlueprintLaunch bad+ `shouldBe` [ "launch.provider, if specified, must not be empty",+ "launch.model, if specified, must not be empty",+ "launch.effort, if specified, must not be empty",+ "launch.mode, if specified, must not be empty"+ ]++ it "accepts a fully populated launch record" $ do+ withSystemTempDirectory "seihou-test" $ \tmpDir -> do+ let bp =+ withBlueprintLaunch+ (Just AgentLaunch {provider = Just "claude-cli", model = Just "claude-opus-4-8", effort = Just "max", mode = Nothing})+ goodBlueprint+ checkBlueprintLaunch bp `shouldBe` []+ result <- validateBlueprintWith [] tmpDir bp+ case result of+ Right _ -> pure ()+ Left err -> expectationFailure ("Expected Right, got: " <> show err)++ it "accepts a blueprint that declares no launch record" $+ checkBlueprintLaunch goodBlueprint `shouldBe` []+ describe "discoverRunnable for blueprints" $ do it "finds a blueprint when only blueprint.dhall is present" $ do withSystemTempDirectory "seihou-test" $ \tmpDir -> do@@ -305,7 +399,7 @@ result <- discoverRunnable [tmpDir] "only-bp" case result of Right (RunnableBlueprint b dir) -> do- b.name `shouldBe` "only-bp"+ (b ^. #name) `shouldBe` "only-bp" dir `shouldBe` bpDir other -> expectationFailure ("Expected RunnableBlueprint, got: " <> show other) @@ -399,5 +493,34 @@ " [ { from = \"1.0.0\", to = \"2.0.0\", prompt = \"first edge\" }", " , { from = \"2.5.0\", to = \"3.0.0\", prompt = \"second edge\" }", " ]",+ "}"+ ]++-- | A blueprint declaring all four launch fields.+sampleBlueprintWithLaunchDhall :: T.Text -> String+sampleBlueprintWithLaunchDhall n =+ unlines $+ init (lines (sampleBlueprintDhall n))+ <> [ ", launch = Some",+ " { provider = Some \"codex-cli\"",+ " , model = Some \"gpt-5.6-terra\"",+ " , effort = Some \"max\"",+ " , mode = Some \"reserved\"",+ " }",+ "}"+ ]++-- | A blueprint whose launch record uses only the three fields that existed+-- before @effort@ was added, as an artifact authored against an older schema+-- pin would.+sampleBlueprintWithLegacyLaunchDhall :: T.Text -> String+sampleBlueprintWithLegacyLaunchDhall n =+ unlines $+ init (lines (sampleBlueprintDhall n))+ <> [ ", launch = Some",+ " { provider = Some \"claude-cli\"",+ " , mode = None Text",+ " , model = Some \"claude-opus-4-8\"",+ " }", "}" ]
test/Seihou/Core/CommandVarSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.CommandVarSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Effectful (runPureEff)@@ -40,28 +42,28 @@ withCondition :: Maybe Expr -> CommandVar -> CommandVar withCondition condition cv =- CommandVar cv.name cv.run cv.workDir condition cv.trim cv.maxBytes+ CommandVar (cv ^. #name) (cv ^. #run) (cv ^. #workDir) condition (cv ^. #trim) (cv ^. #maxBytes) withTrim :: Bool -> CommandVar -> CommandVar withTrim trim cv =- CommandVar cv.name cv.run cv.workDir cv.condition trim cv.maxBytes+ CommandVar (cv ^. #name) (cv ^. #run) (cv ^. #workDir) (cv ^. #condition) trim (cv ^. #maxBytes) withMaxBytes :: Maybe Natural -> CommandVar -> CommandVar withMaxBytes maxBytes cv =- CommandVar cv.name cv.run cv.workDir cv.condition cv.trim maxBytes+ CommandVar (cv ^. #name) (cv ^. #run) (cv ^. #workDir) (cv ^. #condition) (cv ^. #trim) maxBytes commandVarName :: CommandVar -> VarName-commandVarName cv = cv.name+commandVarName cv = (cv ^. #name) commandVarRun :: CommandVar -> T.Text-commandVarRun cv = cv.run+commandVarRun cv = (cv ^. #run) mock :: T.Text -> ExitCode -> T.Text -> T.Text -> ProcessMock mock run exitCode stdoutText stderrText = ProcessMock- { mockCommand = "sh",- mockArgs = ["-c", run],- mockResult = (exitCode, stdoutText, stderrText)+ { command = "sh",+ args = ["-c", run],+ result = (exitCode, stdoutText, stderrText) } runResolve ::@@ -110,9 +112,9 @@ ] case result of Right m -> do- fmap (.value) (Map.lookup "git.branch" m) `shouldBe` Just (VText "main")- fmap (.value) (Map.lookup "release.ready" m) `shouldBe` Just (VBool True)- fmap (.source) (Map.lookup "git.branch" m) `shouldBe` Just (FromCommand "git branch --show-current")+ fmap (^. #value) (Map.lookup "git.branch" m) `shouldBe` Just (VText "main")+ fmap (^. #value) (Map.lookup "release.ready" m) `shouldBe` Just (VBool True)+ fmap (^. #source) (Map.lookup "git.branch" m) `shouldBe` Just (FromCommand "git branch --show-current") Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "does not override already-resolved config values" $ do@@ -135,7 +137,7 @@ [count] Map.empty [mock (commandVarRun count) ExitSuccess "42\n" ""]- fmap (fmap (.value) . Map.lookup "change.count") result `shouldBe` Right (Just (VInt 42))+ fmap (fmap (^. #value) . Map.lookup "change.count") result `shouldBe` Right (Just (VInt 42)) it "uses a text declaration for command-only prompt variables" $ do let branch = cmdVar "git.branch" "git branch --show-current"@@ -145,7 +147,7 @@ [branch] Map.empty [mock (commandVarRun branch) ExitSuccess "main\n" ""]- fmap (fmap (.value) . Map.lookup "git.branch") result `shouldBe` Right (Just (VText "main"))+ fmap (fmap (^. #value) . Map.lookup "git.branch") result `shouldBe` Right (Just (VText "main")) it "preserves untrimmed output when trim is false" $ do let branch = withTrim False (cmdVar "git.branch" "git branch --show-current")@@ -155,7 +157,7 @@ [branch] Map.empty [mock (commandVarRun branch) ExitSuccess "main\n" ""]- fmap (fmap (.value) . Map.lookup "git.branch") result `shouldBe` Right (Just (VText "main\n"))+ fmap (fmap (^. #value) . Map.lookup "git.branch") result `shouldBe` Right (Just (VText "main\n")) it "rejects output that exceeds maxBytes" $ do let branch = withMaxBytes (Just 3) (cmdVar "git.branch" "git branch --show-current")
test/Seihou/Core/ListSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.ListSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Module (DiscoveredModule (..), ModuleSource (..), discoverAllModules) import Seihou.Core.Types (ModuleLoadError (..))@@ -44,7 +46,7 @@ let paths = [tmp </> "project", userDir, tmp </> "installed"] result <- discoverAllModules paths length result `shouldBe` 1- (head result).discoveredSource `shouldBe` SourceUser+ ((head result) ^. #source) `shouldBe` SourceUser it "tags sources correctly across paths" $ do withSystemTempDirectory "seihou-list-test" $ \tmp -> do@@ -57,7 +59,7 @@ let paths = [projectDir, tmp </> "user", installedDir] result <- discoverAllModules paths length result `shouldBe` 2- let srcs = map (.discoveredSource) result+ let srcs = map (^. #source) result SourceProject `elem` srcs `shouldBe` True SourceInstalled `elem` srcs `shouldBe` True @@ -70,7 +72,7 @@ let paths = [tmp </> "project", userDir, tmp </> "installed"] result <- discoverAllModules paths length result `shouldBe` 1- case (head result).discoveredResult of+ case (head result) ^. #result of Left _ -> pure () Right _ -> expectationFailure "Expected Left for broken module"
test/Seihou/Core/MigrationSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.MigrationSpec (tests) where +import Control.Lens (to, (^.))+import Data.Generics.Labels () import Data.Text (Text) import Seihou.Core.Migration ( BlueprintMigration (..),@@ -35,10 +37,10 @@ r = planMigrationChain "demo" [m] (mkV "1.0.0") (mkV "2.0.0") case r of Right (Just plan) -> do- plan.planModule `shouldBe` "demo"- plan.planFrom `shouldBe` mkV "1.0.0"- plan.planTo `shouldBe` mkV "2.0.0"- plan.planSteps `shouldBe` [m]+ (plan ^. #module_) `shouldBe` "demo"+ (plan ^. #from) `shouldBe` mkV "1.0.0"+ (plan ^. #to) `shouldBe` mkV "2.0.0"+ (plan ^. #steps) `shouldBe` [m] other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "builds a two-edge plan in order regardless of declaration order" $ do@@ -47,8 +49,8 @@ r = planMigrationChain "demo" [m2, m1] (mkV "1.0.0") (mkV "3.0.0") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` [m1, m2]- plan.planTo `shouldBe` mkV "3.0.0"+ (plan ^. #steps) `shouldBe` [m1, m2]+ (plan ^. #to) `shouldBe` mkV "3.0.0" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) -- Live-tree master-plan fixture (manifest=0.1.0, target=0.3.0,@@ -59,18 +61,18 @@ r = planMigrationChain "demo" [m] (mkV "0.1.0") (mkV "0.3.0") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` [m]- plan.planFrom `shouldBe` mkV "0.1.0"- plan.planTo `shouldBe` mkV "0.3.0"+ (plan ^. #steps) `shouldBe` [m]+ (plan ^. #from) `shouldBe` mkV "0.1.0"+ (plan ^. #to) `shouldBe` mkV "0.3.0" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "yields an empty-steps plan when no declared migration falls in the window" $ do let r = planMigrationChain "demo" [] (mkV "0.1.3") (mkV "0.3.0") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` []- plan.planFrom `shouldBe` mkV "0.1.3"- plan.planTo `shouldBe` mkV "0.3.0"+ (plan ^. #steps) `shouldBe` []+ (plan ^. #from) `shouldBe` mkV "0.1.3"+ (plan ^. #to) `shouldBe` mkV "0.3.0" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "reports MigrationVersionUnparseable when a from string is malformed" $ do@@ -98,8 +100,8 @@ r = planMigrationChain "demo" [stale, live] (mkV "1.0.0") (mkV "2.0.0") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` [live]- plan.planTo `shouldBe` mkV "2.0.0"+ (plan ^. #steps) `shouldBe` [live]+ (plan ^. #to) `shouldBe` mkV "2.0.0" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "treats version equality with trailing zeros consistently" $ do@@ -118,9 +120,9 @@ r = planMigrationChain "foo" [early, late] (mkV "0.2") (mkV "0.6") case r of Right (Just plan) -> do- plan.planFrom `shouldBe` mkV "0.2"- plan.planTo `shouldBe` mkV "0.6"- plan.planSteps `shouldBe` [early, late]+ (plan ^. #from) `shouldBe` mkV "0.2"+ (plan ^. #to) `shouldBe` mkV "0.6"+ (plan ^. #steps) `shouldBe` [early, late] other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "skips migrations that overshoot the supplied target" $ do@@ -128,8 +130,8 @@ r = planMigrationChain "demo" [m] (mkV "0.4") (mkV "0.6") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` []- plan.planTo `shouldBe` mkV "0.6"+ (plan ^. #steps) `shouldBe` []+ (plan ^. #to) `shouldBe` mkV "0.6" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "skips overlapping migrations once the cursor has advanced past them" $ do@@ -138,16 +140,16 @@ r = planMigrationChain "demo" [big, small] (mkV "0.2") (mkV "0.5") case r of Right (Just plan) ->- plan.planSteps `shouldBe` [big]+ (plan ^. #steps) `shouldBe` [big] other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "empty migrations list with installed != target yields empty-steps plan with target" $ do let r = planMigrationChain "demo" [] (mkV "0.1") (mkV "0.3") case r of Right (Just plan) -> do- plan.planSteps `shouldBe` []- plan.planFrom `shouldBe` mkV "0.1"- plan.planTo `shouldBe` mkV "0.3"+ (plan ^. #steps) `shouldBe` []+ (plan ^. #from) `shouldBe` mkV "0.1"+ (plan ^. #to) `shouldBe` mkV "0.3" other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) it "edges with `to == target` are picked" $ do@@ -155,7 +157,7 @@ r = planMigrationChain "demo" [m] (mkV "0.2") (mkV "0.3") case r of Right (Just plan) ->- plan.planSteps `shouldBe` [m]+ (plan ^. #steps) `shouldBe` [m] other -> expectationFailure ("Expected Right (Just ...), got: " <> show other) describe "planBlueprintMigrationChain" $ do@@ -165,10 +167,10 @@ result = planBlueprintMigrationChain "demo" [late, early] (mkV "1.0.0") (mkV "3.0.0") case result of Right (Just plan) -> do- plan.blueprintPlanName `shouldBe` "demo"- plan.blueprintPlanFrom `shouldBe` mkV "1.0.0"- plan.blueprintPlanTo `shouldBe` mkV "3.0.0"- plan.blueprintPlanSteps `shouldBe` [early, late]+ (plan ^. #name) `shouldBe` "demo"+ (plan ^. #from) `shouldBe` mkV "1.0.0"+ (plan ^. #to) `shouldBe` mkV "3.0.0"+ (plan ^. #steps) `shouldBe` [early, late] other -> expectationFailure ("Expected ordered blueprint plan, got: " <> show other) it "returns Nothing for an equal version window" $ do@@ -196,7 +198,7 @@ let migration = BlueprintMigration "1.0.0" "3.0.0" "too far" result = planBlueprintMigrationChain "demo" [migration] (mkV "1.0.0") (mkV "2.0.0") case result of- Right (Just plan) -> plan.blueprintPlanSteps `shouldBe` []+ Right (Just plan) -> (plan ^. #steps) `shouldBe` [] other -> expectationFailure ("Expected empty blueprint plan, got: " <> show other) -- ---------------------------------------------------------------------------
test/Seihou/Core/ModuleSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.ModuleSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Module (discoverModule, loadModule, validateModule) import Seihou.Core.Types@@ -49,13 +51,13 @@ -- | Helpers to update Module fields without ambiguous record updates. withModuleName :: ModuleName -> Module -> Module-withModuleName n m = Module n m.version m.description m.vars m.exports m.prompts m.steps m.commands m.dependencies m.removal m.migrations+withModuleName n m = Module n (m ^. #version) (m ^. #description) (m ^. #vars) (m ^. #exports) (m ^. #prompts) (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withModuleVars :: [VarDecl] -> Module -> Module-withModuleVars v m = Module m.name m.version m.description v m.exports m.prompts m.steps m.commands m.dependencies m.removal m.migrations+withModuleVars v m = Module (m ^. #name) (m ^. #version) (m ^. #description) v (m ^. #exports) (m ^. #prompts) (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withModulePrompts :: [Prompt] -> Module -> Module-withModulePrompts p m = Module m.name m.version m.description m.vars m.exports p m.steps m.commands m.dependencies m.removal m.migrations+withModulePrompts p m = Module (m ^. #name) (m ^. #version) (m ^. #description) (m ^. #vars) (m ^. #exports) p (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) hasError :: T.Text -> [T.Text] -> Bool hasError needle = any (T.isInfixOf needle)@@ -75,7 +77,7 @@ result <- discoverModule ["/nonexistent/path"] "no-such-module" case result of Left (ModuleNotFound name paths) -> do- name.unModuleName `shouldBe` "no-such-module"+ (name ^. #unModuleName) `shouldBe` "no-such-module" paths `shouldBe` ["/nonexistent/path"] Left other -> expectationFailure ("Expected ModuleNotFound, got: " <> show other) Right _ -> expectationFailure "Expected Left, got Right"@@ -100,7 +102,7 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" result <- validateModule tmpDir goodModule case result of- Right m -> m.name `shouldBe` "test-module"+ Right m -> (m ^. #name) `shouldBe` "test-module" Left err -> expectationFailure ("Expected Right, got: " <> show err) it "rejects a bad module name" $ do@@ -161,9 +163,9 @@ createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let bad =- goodModule- { exports = [VarExport {var = "nonexistent", alias = Nothing}]- }+ ( goodModule+ & #exports .~ [VarExport {var = "nonexistent", alias = Nothing}]+ ) result <- validateModule tmpDir bad case result of Left (ValidationError _ errs) ->@@ -177,9 +179,8 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let bad = goodModule- { steps =- [Step Template "README.md.tpl" "../etc/passwd" Nothing Nothing]- }+ & #steps+ .~ [Step Template "README.md.tpl" "../etc/passwd" Nothing Nothing] result <- validateModule tmpDir bad case result of Left (ValidationError _ errs) ->@@ -193,9 +194,8 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let bad = goodModule- { steps =- [Step Template "README.md.tpl" "/etc/passwd" Nothing Nothing]- }+ & #steps+ .~ [Step Template "README.md.tpl" "/etc/passwd" Nothing Nothing] result <- validateModule tmpDir bad case result of Left (ValidationError _ errs) ->@@ -209,12 +209,11 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let dotted = goodModule- { steps =- [Step Template "README.md.tpl" "docs/README.v2.md" Nothing Nothing]- }+ & #steps+ .~ [Step Template "README.md.tpl" "docs/README.v2.md" Nothing Nothing] result <- validateModule tmpDir dotted case result of- Right m -> m.name `shouldBe` "test-module"+ Right m -> (m ^. #name) `shouldBe` "test-module" Left err -> expectationFailure ("Expected Right, got: " <> show err) it "rejects destination referencing undeclared variable" $ do@@ -223,9 +222,8 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let bad = goodModule- { steps =- [Step Template "README.md.tpl" "src/{{unknown}}/Main.hs" Nothing Nothing]- }+ & #steps+ .~ [Step Template "README.md.tpl" "src/{{unknown}}/Main.hs" Nothing Nothing] result <- validateModule tmpDir bad case result of Left (ValidationError _ errs) ->@@ -263,9 +261,9 @@ result <- loadModule searchPaths "haskell-base" case result of Right m -> do- m.name `shouldBe` "haskell-base"- length (m.vars) `shouldBe` 3- length (m.steps) `shouldBe` 5+ (m ^. #name) `shouldBe` "haskell-base"+ length (m ^. #vars) `shouldBe` 3+ length (m ^. #steps) `shouldBe` 5 Left err -> expectationFailure ("Expected Right, got Left: " <> show err) it "returns ModuleNotFound for nonexistent module" $ do
test/Seihou/Core/RegistrySpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.RegistrySpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.List (isInfixOf) import Data.Text (Text) import Seihou.Core.Registry@@ -57,16 +59,16 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right reg -> do- reg.repoName `shouldBe` "Haskell Templates"- reg.repoDescription `shouldBe` Just "A collection of Haskell project templates"- length reg.modules `shouldBe` 2- let (e1 : e2 : _) = reg.modules- e1.name `shouldBe` ModuleName "haskell-base"- e1.path `shouldBe` "modules/haskell-base"- e1.description `shouldBe` Just "Minimal Haskell project with cabal"- e1.tags `shouldBe` ["haskell", "starter"]- e2.name `shouldBe` ModuleName "nix-flake"- e2.tags `shouldBe` ["nix"]+ (reg ^. #repoName) `shouldBe` "Haskell Templates"+ (reg ^. #repoDescription) `shouldBe` Just "A collection of Haskell project templates"+ length (reg ^. #modules) `shouldBe` 2+ let (e1 : e2 : _) = (reg ^. #modules)+ (e1 ^. #name) `shouldBe` ModuleName "haskell-base"+ (e1 ^. #path) `shouldBe` "modules/haskell-base"+ (e1 ^. #description) `shouldBe` Just "Minimal Haskell project with cabal"+ (e1 ^. #tags) `shouldBe` ["haskell", "starter"]+ (e2 ^. #name) `shouldBe` ModuleName "nix-flake"+ (e2 ^. #tags) `shouldBe` ["nix"] it "decodes a registry with an empty module list" $ do withSystemTempDirectory "seihou-registry-test" $ \tmpDir -> do@@ -80,9 +82,9 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right reg -> do- reg.repoName `shouldBe` "Empty Collection"- reg.repoDescription `shouldBe` Nothing- reg.modules `shouldBe` []+ (reg ^. #repoName) `shouldBe` "Empty Collection"+ (reg ^. #repoDescription) `shouldBe` Nothing+ (reg ^. #modules) `shouldBe` [] it "decodes a registry with no description" $ do withSystemTempDirectory "seihou-registry-test" $ \tmpDir -> do@@ -103,10 +105,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right reg -> do- reg.repoDescription `shouldBe` Nothing- let (e1 : _) = reg.modules- e1.description `shouldBe` Nothing- e1.tags `shouldBe` []+ (reg ^. #repoDescription) `shouldBe` Nothing+ let (e1 : _) = (reg ^. #modules)+ (e1 ^. #description) `shouldBe` Nothing+ (e1 ^. #tags) `shouldBe` [] it "returns RegistryEvalError for malformed registry (missing required field)" $ do withSystemTempDirectory "seihou-registry-test" $ \tmpDir -> do@@ -134,7 +136,7 @@ writeRegistryFile tmpDir result <- discoverRepoContents evalRegistryFromFile tmpDir case result of- MultiModule reg -> reg.repoName `shouldBe` "Test Registry"+ MultiModule reg -> (reg ^. #repoName) `shouldBe` "Test Registry" other -> expectationFailure ("Expected MultiModule, got: " <> show other) it "returns SingleModule when only module.dhall exists" $ do@@ -151,7 +153,7 @@ writeMinimalModuleDhall (tmpDir </> "module.dhall") result <- discoverRepoContents evalRegistryFromFile tmpDir case result of- MultiModule reg -> reg.repoName `shouldBe` "Test Registry"+ MultiModule reg -> (reg ^. #repoName) `shouldBe` "Test Registry" other -> expectationFailure ("Expected MultiModule (registry takes precedence), got: " <> show other) it "returns EmptyRepo when neither file exists" $ do@@ -249,11 +251,11 @@ } lookups = [(ModuleEntry, ModuleName "mod-a", Just "1.0.0")] report <- validateRegistryFull tmpDir reg lookups- report.reportIssues `shouldBe` []- report.reportModuleCount `shouldBe` 1- report.reportRecipeCount `shouldBe` 0- report.reportBlueprintCount `shouldBe` 0- report.reportPromptCount `shouldBe` 0+ (report ^. #issues) `shouldBe` []+ (report ^. #moduleCount) `shouldBe` 1+ (report ^. #recipeCount) `shouldBe` 0+ (report ^. #blueprintCount) `shouldBe` 0+ (report ^. #promptCount) `shouldBe` 0 it "flags a SyncMissing entry as a single VersionMismatch" $ do withSystemTempDirectory "seihou-validate-full" $ \tmpDir -> do@@ -270,8 +272,8 @@ } lookups = [(ModuleEntry, ModuleName "mod-a", Just "1.0.0")] report <- validateRegistryFull tmpDir reg lookups- case report.reportIssues of- [VersionMismatch d] -> d.diffStatus `shouldBe` SyncMissing+ case report ^. #issues of+ [VersionMismatch d] -> (d ^. #status) `shouldBe` SyncMissing other -> expectationFailure ("expected one VersionMismatch SyncMissing, got: " <> show other) it "flags a SyncStale entry as a single VersionMismatch carrying the new version" $ do@@ -289,8 +291,8 @@ } lookups = [(ModuleEntry, ModuleName "mod-a", Just "2.0.0")] report <- validateRegistryFull tmpDir reg lookups- case report.reportIssues of- [VersionMismatch d] -> d.diffStatus `shouldBe` SyncStale "2.0.0"+ case report ^. #issues of+ [VersionMismatch d] -> (d ^. #status) `shouldBe` SyncStale "2.0.0" other -> expectationFailure ("expected one VersionMismatch SyncStale, got: " <> show other) it "flags an invalid module name as a StructuralError" $ do@@ -308,7 +310,7 @@ } lookups = [(ModuleEntry, ModuleName "Bad_Name", Nothing)] report <- validateRegistryFull tmpDir reg lookups- let structurals = [msg | StructuralError msg <- report.reportIssues]+ let structurals = [msg | StructuralError msg <- report ^. #issues] any ("must match" `isInfixOf`) (map show structurals) `shouldBe` True it "flags an unsafe path with .. as a StructuralError" $ do@@ -323,7 +325,7 @@ prompts = [] } report <- validateRegistryFull tmpDir reg []- let structurals = [msg | StructuralError msg <- report.reportIssues]+ let structurals = [msg | StructuralError msg <- report ^. #issues] any ("must not contain" `isInfixOf`) (map show structurals) `shouldBe` True it "lists structural issues before version issues when both are present" $ do@@ -350,11 +352,11 @@ (ModuleEntry, ModuleName "stale", Just "2.0.0") ] report <- validateRegistryFull tmpDir reg lookups- length report.reportIssues `shouldBe` 2- case report.reportIssues of+ length (report ^. #issues) `shouldBe` 2+ case report ^. #issues of [StructuralError msg, VersionMismatch d] -> do ("missing module.dhall" `isInfixOf` show msg) `shouldBe` True- d.diffStatus `shouldBe` SyncStale "2.0.0"+ (d ^. #status) `shouldBe` SyncStale "2.0.0" other -> expectationFailure ("expected [StructuralError, VersionMismatch], got: " <> show other)@@ -403,18 +405,18 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right reg -> do- length reg.modules `shouldBe` 1- length reg.recipes `shouldBe` 1- length reg.blueprints `shouldBe` 1- length reg.prompts `shouldBe` 1- let (bp : _) = reg.blueprints- bp.name `shouldBe` ModuleName "bp-one"- bp.version `shouldBe` Just "0.1.0"- bp.tags `shouldBe` ["agent"]- let (prompt : _) = reg.prompts- prompt.name `shouldBe` ModuleName "prompt-one"- prompt.version `shouldBe` Just "0.2.0"- prompt.tags `shouldBe` ["review"]+ length (reg ^. #modules) `shouldBe` 1+ length (reg ^. #recipes) `shouldBe` 1+ length (reg ^. #blueprints) `shouldBe` 1+ length (reg ^. #prompts) `shouldBe` 1+ let (bp : _) = (reg ^. #blueprints)+ (bp ^. #name) `shouldBe` ModuleName "bp-one"+ (bp ^. #version) `shouldBe` Just "0.1.0"+ (bp ^. #tags) `shouldBe` ["agent"]+ let (prompt : _) = (reg ^. #prompts)+ (prompt ^. #name) `shouldBe` ModuleName "prompt-one"+ (prompt ^. #version) `shouldBe` Just "0.2.0"+ (prompt ^. #tags) `shouldBe` ["review"] it "decodes a pre-EP-33 registry (no blueprints or prompts fields) with empty lists" $ do withSystemTempDirectory "seihou-registry-bp-compat" $ \tmpDir -> do@@ -435,9 +437,9 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right reg -> do- reg.recipes `shouldBe` []- reg.blueprints `shouldBe` []- reg.prompts `shouldBe` []+ (reg ^. #recipes) `shouldBe` []+ (reg ^. #blueprints) `shouldBe` []+ (reg ^. #prompts) `shouldBe` [] it "rejects an invalid blueprint name" $ do withSystemTempDirectory "seihou-validate-bp" $ \tmpDir -> do@@ -534,7 +536,7 @@ any ("appears as both a module and a blueprint" `isInfixOf`) messages `shouldBe` True any ("appears as both a recipe and a blueprint" `isInfixOf`) messages `shouldBe` True - it "computeRegistrySync classifies blueprint entries with diffKind = BlueprintEntry" $ do+ it "computeRegistrySync classifies blueprint entries with kind = BlueprintEntry" $ do let reg = Registry { repoName = "Test",@@ -548,16 +550,16 @@ } lookups = [(BlueprintEntry, ModuleName "bp-stale", Just "0.2.0")] SyncReport diffs updated = computeRegistrySync reg lookups- kinds = [diff_ | SyncDiff {diffKind = diff_} <- diffs]- statuses = [s | SyncDiff {diffStatus = s} <- diffs]+ kinds = [diff_ | SyncDiff {kind = diff_} <- diffs]+ statuses = [s | SyncDiff {status = s} <- diffs] kinds `shouldBe` [BlueprintEntry] statuses `shouldBe` [SyncStale "0.2.0"]- let updatedVersion = case updated.blueprints of+ let updatedVersion = case updated ^. #blueprints of (RegistryEntry _ v _ _ _ : _) -> v _ -> Nothing updatedVersion `shouldBe` Just ("0.2.0" :: Text) - it "validateRegistryFull populates reportBlueprintCount" $ do+ it "validateRegistryFull populates blueprintCount" $ do withSystemTempDirectory "seihou-validate-bp-count" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "bp-a") writeMinimalBlueprintDhall (tmpDir </> "bp-a" </> "blueprint.dhall")@@ -580,8 +582,8 @@ (BlueprintEntry, ModuleName "bp-b", Just "1.0.0") ] report <- validateRegistryFull tmpDir reg lookups- report.reportBlueprintCount `shouldBe` 2- report.reportIssues `shouldBe` []+ (report ^. #blueprintCount) `shouldBe` 2+ (report ^. #issues) `shouldBe` [] describe "prompts in registries" $ do it "rejects an invalid prompt name" $ do@@ -661,7 +663,7 @@ any ("appears as both a recipe and a prompt" `isInfixOf`) messages `shouldBe` True any ("appears as both a blueprint and a prompt" `isInfixOf`) messages `shouldBe` True - it "computeRegistrySync classifies prompt entries with diffKind = PromptEntry" $ do+ it "computeRegistrySync classifies prompt entries with kind = PromptEntry" $ do let reg = Registry { repoName = "Test",@@ -675,16 +677,16 @@ } lookups = [(PromptEntry, ModuleName "prompt-stale", Just "0.2.0")] SyncReport diffs updated = computeRegistrySync reg lookups- kinds = [diff_ | SyncDiff {diffKind = diff_} <- diffs]- statuses = [s | SyncDiff {diffStatus = s} <- diffs]+ kinds = [diff_ | SyncDiff {kind = diff_} <- diffs]+ statuses = [s | SyncDiff {status = s} <- diffs] kinds `shouldBe` [PromptEntry] statuses `shouldBe` [SyncStale "0.2.0"]- let updatedVersion = case updated.prompts of+ let updatedVersion = case updated ^. #prompts of (RegistryEntry _ v _ _ _ : _) -> v _ -> Nothing updatedVersion `shouldBe` Just ("0.2.0" :: Text) - it "validateRegistryFull populates reportPromptCount" $ do+ it "validateRegistryFull populates promptCount" $ do withSystemTempDirectory "seihou-validate-prompt-count" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "prompt-a") writeMinimalPromptDhall (tmpDir </> "prompt-a" </> "prompt.dhall")@@ -707,8 +709,8 @@ (PromptEntry, ModuleName "prompt-b", Just "1.0.0") ] report <- validateRegistryFull tmpDir reg lookups- report.reportPromptCount `shouldBe` 2- report.reportIssues `shouldBe` []+ (report ^. #promptCount) `shouldBe` 2+ (report ^. #issues) `shouldBe` [] describe "discoverRepoContents and blueprints" $ do it "returns SingleBlueprint when only blueprint.dhall is present" $ do
test/Seihou/Core/RegistrySyncSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.RegistrySyncSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Maybe (isJust, mapMaybe) import Data.Text (Text) import Data.Text qualified as T@@ -27,52 +29,50 @@ let entry = mkEntry "alpha" Nothing reg = mkReg [entry] [] report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]- map (.diffStatus) report.syncDiffs `shouldBe` [SyncMissing]- map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0"]- (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncMissing]+ map (^. #new) (report ^. #diffs) `shouldBe` [Just "1.0.0"]+ ((head (report ^. #updated . #modules)) ^. #version) `shouldBe` Just "1.0.0" it "classifies SyncStale when registry and disk versions differ" $ do let entry = mkEntry "alpha" (Just "0.1.0") reg = mkReg [entry] [] report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]- map (.diffStatus) report.syncDiffs `shouldBe` [SyncStale "1.0.0"]- map (.diffOld) report.syncDiffs `shouldBe` [Just "0.1.0"]- map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0"]- (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncStale "1.0.0"]+ map (^. #old) (report ^. #diffs) `shouldBe` [Just "0.1.0"]+ map (^. #new) (report ^. #diffs) `shouldBe` [Just "1.0.0"]+ ((head (report ^. #updated . #modules)) ^. #version) `shouldBe` Just "1.0.0" it "classifies SyncInSync when registry and disk versions match" $ do let entry = mkEntry "alpha" (Just "1.0.0") reg = mkReg [entry] [] report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]- map (.diffStatus) report.syncDiffs `shouldBe` [SyncInSync]- (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncInSync]+ ((head (report ^. #updated . #modules)) ^. #version) `shouldBe` Just "1.0.0" it "classifies SyncInSync when registry and disk are both Nothing" $ do let entry = mkEntry "alpha" Nothing reg = mkReg [entry] [] report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Nothing)]- map (.diffStatus) report.syncDiffs `shouldBe` [SyncInSync]- (head report.syncUpdated.modules).version `shouldBe` Nothing+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncInSync]+ ((head (report ^. #updated . #modules)) ^. #version) `shouldBe` Nothing it "classifies SyncOrphan when the entry has no lookup (module.dhall absent/unreadable)" $ do let entry = mkEntry "alpha" (Just "1.0.0") reg = mkReg [entry] [] report = computeRegistrySync reg []- map (.diffStatus) report.syncDiffs `shouldBe` [SyncOrphan]+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncOrphan] -- Orphan: version left as-is- (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"+ ((head (report ^. #updated . #modules)) ^. #version) `shouldBe` Just "1.0.0" it "preserves registry order in the diff output" $ do let reg =- ( mkReg- [ mkEntry "alpha" Nothing,- mkEntry "beta" (Just "0.1.0"),- mkEntry "gamma" (Just "2.0.0")- ]- [mkEntry "lib-one" Nothing]- )- { prompts = [mkEntry "review" Nothing]- }+ mkReg+ [ mkEntry "alpha" Nothing,+ mkEntry "beta" (Just "0.1.0"),+ mkEntry "gamma" (Just "2.0.0")+ ]+ [mkEntry "lib-one" Nothing]+ & #prompts .~ [mkEntry "review" Nothing] lookups = [ (ModuleEntry, ModuleName "alpha", Just "1.0.0"), (ModuleEntry, ModuleName "beta", Just "0.2.0"),@@ -81,23 +81,23 @@ (PromptEntry, ModuleName "review", Just "0.4.0") ] report = computeRegistrySync reg lookups- map (.diffName) report.syncDiffs+ map (^. #name) (report ^. #diffs) `shouldBe` [ ModuleName "alpha", ModuleName "beta", ModuleName "gamma", ModuleName "lib-one", ModuleName "review" ]- map (.diffKind) report.syncDiffs+ map (^. #kind) (report ^. #diffs) `shouldBe` [ModuleEntry, ModuleEntry, ModuleEntry, RecipeEntry, PromptEntry]- map (.diffStatus) report.syncDiffs+ map (^. #status) (report ^. #diffs) `shouldBe` [SyncMissing, SyncStale "0.2.0", SyncInSync, SyncMissing, SyncMissing] it "returns an empty report for an empty registry" $ do let reg = mkReg [] [] report = computeRegistrySync reg []- report.syncDiffs `shouldBe` []- report.syncUpdated `shouldBe` reg+ (report ^. #diffs) `shouldBe` []+ (report ^. #updated) `shouldBe` reg it "distinguishes module and recipe entries with the same name in lookups" $ do -- Module and recipe namespaces share a validation check,@@ -111,35 +111,35 @@ (RecipeEntry, ModuleName "beta", Just "2.0.0") ] report = computeRegistrySync reg lookups- map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0", Just "2.0.0"]+ map (^. #new) (report ^. #diffs) `shouldBe` [Just "1.0.0", Just "2.0.0"] describe "formatDriftWarning" $ do it "produces a warning for a stale entry" $ do let reg = mkReg [mkEntry "alpha" (Just "0.1.0")] [] lookups = [(ModuleEntry, ModuleName "alpha", Just "1.0.0")] report = computeRegistrySync reg lookups- warnings = mapMaybe formatDriftWarning report.syncDiffs+ warnings = mapMaybe formatDriftWarning (report ^. #diffs) length warnings `shouldBe` 1- isJust (formatDriftWarning (head report.syncDiffs)) `shouldBe` True+ isJust (formatDriftWarning (head (report ^. #diffs))) `shouldBe` True it "produces no warnings when all entries are in sync" $ do let reg = mkReg [mkEntry "alpha" (Just "1.0.0")] [] lookups = [(ModuleEntry, ModuleName "alpha", Just "1.0.0")] report = computeRegistrySync reg lookups- warnings = mapMaybe formatDriftWarning report.syncDiffs+ warnings = mapMaybe formatDriftWarning (report ^. #diffs) warnings `shouldBe` [] it "produces no warnings for orphan entries (handled by validateRegistry)" $ do let reg = mkReg [mkEntry "alpha" (Just "1.0.0")] [] report = computeRegistrySync reg []- warnings = mapMaybe formatDriftWarning report.syncDiffs+ warnings = mapMaybe formatDriftWarning (report ^. #diffs) warnings `shouldBe` [] it "mentions prompt.dhall in stale prompt warnings" $ do- let reg = (mkReg [] []) {prompts = [mkEntry "review" (Just "0.1.0")]}+ let reg = ((mkReg [] []) & #prompts .~ [mkEntry "review" (Just "0.1.0")]) lookups = [(PromptEntry, ModuleName "review", Just "0.2.0")] report = computeRegistrySync reg lookups- warnings = mapMaybe formatDriftWarning report.syncDiffs+ warnings = mapMaybe formatDriftWarning (report ^. #diffs) warnings `shouldBe` [ "prompt 'review' registry version 0.1.0 differs from prompt.dhall version 0.2.0 — run `seihou registry sync-versions`" ]
test/Seihou/Core/ScaffoldSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.ScaffoldSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.AgentPrompt (validateAgentPrompt) import Seihou.Core.Blueprint (validateBlueprint)@@ -52,7 +54,7 @@ result <- evalModuleFromFile dhallFile case result of Left err -> expectationFailure $ "Failed to load generated module: " ++ show err- Right m -> m.name `shouldBe` "test-mod"+ Right m -> (m ^. #name) `shouldBe` "test-mod" it "generates a module that passes validateModule" $ do schemaPath <- resolveSchemaPath@@ -85,13 +87,13 @@ case result of Left err -> expectationFailure $ "Failed to load: " ++ show err Right m -> do- length (m.vars) `shouldBe` 1- map (.name) m.vars `shouldBe` ["project.name"]- length (m.steps) `shouldBe` 1- length (m.prompts) `shouldBe` 1- length (m.commands) `shouldBe` 0- length (m.exports) `shouldBe` 0- length (m.dependencies) `shouldBe` 0+ length (m ^. #vars) `shouldBe` 1+ map (^. #name) (m ^. #vars) `shouldBe` ["project.name"]+ length (m ^. #steps) `shouldBe` 1+ length (m ^. #prompts) `shouldBe` 1+ length (m ^. #commands) `shouldBe` 0+ length (m ^. #exports) `shouldBe` 0+ length (m ^. #dependencies) `shouldBe` 0 describe "readmeTemplate" $ do it "contains the project.name placeholder" $ do@@ -122,7 +124,7 @@ result <- evalBlueprintFromFile dhallFile case result of Left err -> expectationFailure $ "Failed to load generated blueprint: " ++ show err- Right b -> b.name `shouldBe` "test-bp"+ Right b -> (b ^. #name) `shouldBe` "test-bp" it "produces a blueprint that passes validateBlueprint" $ do schemaPath <- resolveSchemaPath@@ -155,13 +157,13 @@ case result of Left err -> expectationFailure $ "Failed to load: " ++ show err Right b -> do- length (b.vars) `shouldBe` 1- map (.name) b.vars `shouldBe` ["project.name"]- length (b.prompts) `shouldBe` 1- length (b.baseModules) `shouldBe` 0- length (b.files) `shouldBe` 0- length (b.tags) `shouldBe` 0- length (b.migrations) `shouldBe` 0+ length (b ^. #vars) `shouldBe` 1+ map (^. #name) (b ^. #vars) `shouldBe` ["project.name"]+ length (b ^. #prompts) `shouldBe` 1+ length (b ^. #baseModules) `shouldBe` 0+ length (b ^. #files) `shouldBe` 0+ length (b ^. #tags) `shouldBe` 0+ length (b ^. #migrations) `shouldBe` 0 describe "examplePromptMarkdown" $ do it "contains the {{project.name}} placeholder so authors see substitution" $ do@@ -194,7 +196,7 @@ result <- evalAgentPromptFromFile dhallFile case result of Left err -> expectationFailure $ "Failed to load generated prompt: " ++ show err- Right p -> p.name `shouldBe` "review-changes"+ Right p -> (p ^. #name) `shouldBe` "review-changes" it "produces a prompt that passes validateAgentPrompt" $ do schemaPath <- resolveSchemaPath@@ -227,12 +229,12 @@ case result of Left err -> expectationFailure $ "Failed to load: " ++ show err Right p -> do- length (p.vars) `shouldBe` 1- map (.name) p.vars `shouldBe` ["project.name"]- length (p.prompts) `shouldBe` 0- length (p.commandVars) `shouldBe` 0- length (p.files) `shouldBe` 0- length (p.tags) `shouldBe` 0+ length (p ^. #vars) `shouldBe` 1+ map (^. #name) (p ^. #vars) `shouldBe` ["project.name"]+ length (p ^. #prompts) `shouldBe` 0+ length (p ^. #commandVars) `shouldBe` 0+ length (p ^. #files) `shouldBe` 0+ length (p ^. #tags) `shouldBe` 0 describe "exampleAgentPromptMarkdown" $ do it "contains the {{project.name}} placeholder so debug rendering shows substitution" $ do
test/Seihou/Core/StatusSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.StatusSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Time (UTCTime, defaultTimeLocale, parseTimeOrError)@@ -43,37 +45,37 @@ it "classifies a file matching its manifest hash as TfsUnchanged" $ do let content = "# Hello World" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "README.md" (mkRecord content)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "README.md" (mkRecord content)+ ) fs = PureFS (Map.singleton "README.md" content) mempty result = runStatus fs manifest length result `shouldBe` 1- (head result).path `shouldBe` "README.md"- (head result).moduleName `shouldBe` modName- (head result).status `shouldBe` TfsUnchanged+ ((head result) ^. #path) `shouldBe` "README.md"+ ((head result) ^. #moduleName) `shouldBe` modName+ ((head result) ^. #status) `shouldBe` TfsUnchanged it "classifies a file with different disk content as TfsModified" $ do let originalContent = "# Hello" modifiedContent = "# Hello - edited" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "README.md" (mkRecord originalContent)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "README.md" (mkRecord originalContent)+ ) fs = PureFS (Map.singleton "README.md" modifiedContent) mempty result = runStatus fs manifest length result `shouldBe` 1- (head result).status `shouldBe` TfsModified+ ((head result) ^. #status) `shouldBe` TfsModified it "classifies a file missing from disk as TfsDeleted" $ do let content = "# Hello" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "README.md" (mkRecord content)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "README.md" (mkRecord content)+ ) result = runStatus emptyFS manifest length result `shouldBe` 1- (head result).status `shouldBe` TfsDeleted+ ((head result) ^. #status) `shouldBe` TfsDeleted it "handles mixed statuses across multiple files" $ do let unchangedContent = "unchanged"@@ -81,14 +83,9 @@ modifiedCurrent = "edited" deletedContent = "deleted" manifest =- (emptyManifest fixedTime :: Manifest)- { files =- Map.fromList- [ ("a.txt", mkRecord unchangedContent),- ("b.txt", mkRecord modifiedOriginal),- ("c.txt", mkRecord deletedContent)- ]- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.fromList [("a.txt", mkRecord unchangedContent), ("b.txt", mkRecord modifiedOriginal), ("c.txt", mkRecord deletedContent)]+ ) fs = PureFS ( Map.fromList@@ -100,12 +97,12 @@ result = runStatus fs manifest length result `shouldBe` 3 -- Results are sorted by path- (result !! 0).path `shouldBe` "a.txt"- (result !! 0).status `shouldBe` TfsUnchanged- (result !! 1).path `shouldBe` "b.txt"- (result !! 1).status `shouldBe` TfsModified- (result !! 2).path `shouldBe` "c.txt"- (result !! 2).status `shouldBe` TfsDeleted+ ((result !! 0) ^. #path) `shouldBe` "a.txt"+ ((result !! 0) ^. #status) `shouldBe` TfsUnchanged+ ((result !! 1) ^. #path) `shouldBe` "b.txt"+ ((result !! 1) ^. #status) `shouldBe` TfsModified+ ((result !! 2) ^. #path) `shouldBe` "c.txt"+ ((result !! 2) ^. #status) `shouldBe` TfsDeleted it "returns empty list for empty manifest" $ do let manifest = emptyManifest fixedTime
test/Seihou/Core/TypesSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.TypesSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Seihou.Core.Types import Test.Hspec import Test.Tasty@@ -13,7 +15,7 @@ describe "ModuleName" $ do it "supports OverloadedStrings" $ do let name = "my-module" :: ModuleName- name.unModuleName `shouldBe` "my-module"+ (name ^. #unModuleName) `shouldBe` "my-module" it "supports Eq" $ do ("a" :: ModuleName) `shouldBe` ("a" :: ModuleName)@@ -25,7 +27,7 @@ describe "VarName" $ do it "supports OverloadedStrings" $ do let name = "project.name" :: VarName- name.unVarName `shouldBe` "project.name"+ (name ^. #unVarName) `shouldBe` "project.name" describe "VarType" $ do it "has five distinct constructors" $ do@@ -58,7 +60,7 @@ required = True, validation = Just (ValPattern "[a-z][a-z0-9-]*") }- decl.required `shouldBe` True+ (decl ^. #required) `shouldBe` True describe "Strategy" $ do it "has four distinct constructors" $ do@@ -82,7 +84,7 @@ removal = Nothing, migrations = [] }- m.name `shouldBe` "haskell-base"+ (m ^. #name) `shouldBe` "haskell-base" it "supports Eq for identical values" $ do let m =@@ -120,20 +122,20 @@ describe "Operation" $ do it "supports WriteFileOp" $ do- let op = WriteFileOp {dest = "README.md", content = "# Hello", strategy = Template}- op.dest `shouldBe` "README.md"+ let WriteFileOp {dest} = WriteFileOp {dest = "README.md", content = "# Hello", strategy = Template}+ dest `shouldBe` "README.md" it "supports CreateDirOp" $ do- let op = CreateDirOp {path = "src"}- op.path `shouldBe` "src"+ let CreateDirOp {path} = CreateDirOp {path = "src"}+ path `shouldBe` "src" it "supports CopyFileOp" $ do- let op = CopyFileOp {src = "a.txt", dest = "b.txt"}- op.src `shouldBe` "a.txt"+ let CopyFileOp {src} = CopyFileOp {src = "a.txt", dest = "b.txt"}+ src `shouldBe` "a.txt" it "supports RunCommandOp" $ do- let op = RunCommandOp {command = "git init", workDir = Nothing, moduleName = "test", occurrence = 0}- op.command `shouldBe` "git init"+ let RunCommandOp {command} = RunCommandOp {command = "git init", workDir = Nothing, moduleName = "test", occurrence = 0}+ command `shouldBe` "git init" describe "Expr" $ do it "supports ExprIsSet" $ do@@ -150,4 +152,4 @@ it "has a version field" $ do -- Verify the Manifest type is a record with expected fields let hash = SHA256 "abc"- hash.unSHA256 `shouldBe` "abc"+ (hash ^. #unSHA256) `shouldBe` "abc"
test/Seihou/Core/VariableSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Core.VariableSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Expr (evalExpr, parseExpr)@@ -196,8 +198,8 @@ case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do let rv = resolved Map.! "project.name"- rv.value `shouldBe` VText "my-app"- rv.source `shouldBe` FromCLI+ (rv ^. #value) `shouldBe` VText "my-app"+ (rv ^. #source) `shouldBe` FromCLI Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves from environment variables" $ do@@ -207,8 +209,8 @@ case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do let rv = resolved Map.! "project.name"- rv.value `shouldBe` VText "env-app"- rv.source `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME"+ (rv ^. #value) `shouldBe` VText "env-app"+ (rv ^. #source) `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves from module defaults" $ do@@ -218,8 +220,8 @@ case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do let rv = resolved Map.! "project.version"- rv.value `shouldBe` VText "0.1.0.0"- rv.source `shouldBe` FromDefault+ (rv ^. #value) `shouldBe` VText "0.1.0.0"+ (rv ^. #source) `shouldBe` FromDefault Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "rejects missing required variable" $ do@@ -236,8 +238,8 @@ env = Map.fromList [("SEIHOU_VAR_PROJECT_NAME", "env-app")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"- (.source) (resolved Map.! "project.name") `shouldBe` FromCLI+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"+ (^. #source) (resolved Map.! "project.name") `shouldBe` FromCLI Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "environment variable beats module default" $ do@@ -246,8 +248,8 @@ env = Map.fromList [("SEIHOU_VAR_PROJECT_NAME", "env-app")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "env-app"- (.source) (resolved Map.! "project.name") `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME"+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "env-app"+ (^. #source) (resolved Map.! "project.name") `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "CLI override beats module default" $ do@@ -256,7 +258,7 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "cli-app" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves multiple variables with mixed sources" $ do@@ -269,12 +271,12 @@ env = Map.fromList [("SEIHOU_VAR_LICENSE", "BSD3")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "my-app"- (.source) (resolved Map.! "project.name") `shouldBe` FromCLI- (.value) (resolved Map.! "project.version") `shouldBe` VText "0.1.0.0"- (.source) (resolved Map.! "project.version") `shouldBe` FromDefault- (.value) (resolved Map.! "license") `shouldBe` VText "BSD3"- (.source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "my-app"+ (^. #source) (resolved Map.! "project.name") `shouldBe` FromCLI+ (^. #value) (resolved Map.! "project.version") `shouldBe` VText "0.1.0.0"+ (^. #source) (resolved Map.! "project.version") `shouldBe` FromDefault+ (^. #value) (resolved Map.! "license") `shouldBe` VText "BSD3"+ (^. #source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "coerces CLI bool override" $ do@@ -283,7 +285,7 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "enable.tests") `shouldBe` VBool True+ (^. #value) (resolved Map.! "enable.tests") `shouldBe` VBool True Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "coerces env int override" $ do@@ -292,7 +294,7 @@ env = Map.fromList [("SEIHOU_VAR_PORT", "8080")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "port") `shouldBe` VInt 8080+ (^. #value) (resolved Map.! "port") `shouldBe` VInt 8080 Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "returns coercion error for bad int" $ do@@ -320,7 +322,7 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "license") `shouldBe` VText "MIT"+ (^. #value) (resolved Map.! "license") `shouldBe` VText "MIT" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "coerces a raw-text bool default to VBool when falling through to default" $ do@@ -331,8 +333,8 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "feature.on") `shouldBe` VBool True- (.source) (resolved Map.! "feature.on") `shouldBe` FromDefault+ (^. #value) (resolved Map.! "feature.on") `shouldBe` VBool True+ (^. #source) (resolved Map.! "feature.on") `shouldBe` FromDefault Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "coerces a raw-text int default to VInt when falling through to default" $ do@@ -341,7 +343,7 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "retries") `shouldBe` VInt 3+ (^. #value) (resolved Map.! "retries") `shouldBe` VInt 3 Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "errors when a bool default cannot be coerced" $ do@@ -365,7 +367,7 @@ local = Map.fromList [("feature.on", manifestStoredText)] case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "feature.on") `shouldBe` VBool True+ (^. #value) (resolved Map.! "feature.on") `shouldBe` VBool True Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "Eq <var> true evaluates True for a defaulted bool" $ do@@ -375,7 +377,7 @@ env = Map.empty case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- let varMap = Map.map (.value) resolved+ let varMap = Map.map (^. #value) resolved case parseExpr "Eq feature.on true" of Right expr -> evalExpr varMap expr `shouldBe` True Left err -> expectationFailure ("Expected parse, got: " <> show err)@@ -505,8 +507,8 @@ local = Map.fromList [("license", "MIT")] case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "MIT"- (.source) (resolved Map.! "license") `shouldBe` FromLocalConfig+ (^. #value) (resolved Map.! "license") `shouldBe` VText "MIT"+ (^. #source) (resolved Map.! "license") `shouldBe` FromLocalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves from namespace config" $ do@@ -516,8 +518,8 @@ nsCfg = Map.fromList [("haskell.ghc", "9.12.2")] case resolveVariables decls cli env "haskell" "" Map.empty nsCfg Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"- (.source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell"+ (^. #value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"+ (^. #source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves from global config" $ do@@ -527,8 +529,8 @@ global = Map.fromList [("license", "MIT")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "MIT"- (.source) (resolved Map.! "license") `shouldBe` FromGlobalConfig+ (^. #value) (resolved Map.! "license") `shouldBe` VText "MIT"+ (^. #source) (resolved Map.! "license") `shouldBe` FromGlobalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "local config overrides global config" $ do@@ -539,8 +541,8 @@ global = Map.fromList [("license", "MIT")] case resolveVariables decls cli env "" "" local Map.empty Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "BSD3"- (.source) (resolved Map.! "license") `shouldBe` FromLocalConfig+ (^. #value) (resolved Map.! "license") `shouldBe` VText "BSD3"+ (^. #source) (resolved Map.! "license") `shouldBe` FromLocalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "namespace config overrides global config" $ do@@ -551,8 +553,8 @@ global = Map.fromList [("license", "MIT")] case resolveVariables decls cli env "haskell" "" Map.empty nsCfg Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "Apache"- (.source) (resolved Map.! "license") `shouldBe` FromNamespaceConfig "haskell"+ (^. #value) (resolved Map.! "license") `shouldBe` VText "Apache"+ (^. #source) (resolved Map.! "license") `shouldBe` FromNamespaceConfig "haskell" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "context config resolves when namespace and local don't have variable" $ do@@ -562,8 +564,8 @@ ctxCfg = Map.fromList [("user.email", "me@work.com")] case resolveVariables decls cli env "" "work" Map.empty Map.empty ctxCfg Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "user.email") `shouldBe` VText "me@work.com"- (.source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work"+ (^. #value) (resolved Map.! "user.email") `shouldBe` VText "me@work.com"+ (^. #source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "context config is lower priority than namespace config" $ do@@ -574,8 +576,8 @@ ctxCfg = Map.fromList [("user.email", "ctx@example.com")] case resolveVariables decls cli env "haskell" "work" Map.empty nsCfg ctxCfg Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "user.email") `shouldBe` VText "ns@example.com"- (.source) (resolved Map.! "user.email") `shouldBe` FromNamespaceConfig "haskell"+ (^. #value) (resolved Map.! "user.email") `shouldBe` VText "ns@example.com"+ (^. #source) (resolved Map.! "user.email") `shouldBe` FromNamespaceConfig "haskell" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "context config is higher priority than global config" $ do@@ -586,8 +588,8 @@ global = Map.fromList [("user.email", "global@example.com")] case resolveVariables decls cli env "" "work" Map.empty Map.empty ctxCfg global Map.empty of Right resolved -> do- (.value) (resolved Map.! "user.email") `shouldBe` VText "ctx@example.com"- (.source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work"+ (^. #value) (resolved Map.! "user.email") `shouldBe` VText "ctx@example.com"+ (^. #source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "local config overrides namespace config" $ do@@ -598,8 +600,8 @@ nsCfg = Map.fromList [("license", "Apache")] case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "GPL"- (.source) (resolved Map.! "license") `shouldBe` FromLocalConfig+ (^. #value) (resolved Map.! "license") `shouldBe` VText "GPL"+ (^. #source) (resolved Map.! "license") `shouldBe` FromLocalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "env overrides local config" $ do@@ -609,8 +611,8 @@ local = Map.fromList [("license", "local-license")] case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "env-license"- (.source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"+ (^. #value) (resolved Map.! "license") `shouldBe` VText "env-license"+ (^. #source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "CLI overrides all config layers" $ do@@ -622,8 +624,8 @@ global = Map.fromList [("license", "global-license")] case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "cli-license"- (.source) (resolved Map.! "license") `shouldBe` FromCLI+ (^. #value) (resolved Map.! "license") `shouldBe` VText "cli-license"+ (^. #source) (resolved Map.! "license") `shouldBe` FromCLI Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "global config overrides module default" $ do@@ -633,8 +635,8 @@ global = Map.fromList [("license", "global-license")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "license") `shouldBe` VText "global-license"- (.source) (resolved Map.! "license") `shouldBe` FromGlobalConfig+ (^. #value) (resolved Map.! "license") `shouldBe` VText "global-license"+ (^. #source) (resolved Map.! "license") `shouldBe` FromGlobalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves mixed sources across multiple variables" $ do@@ -649,12 +651,12 @@ nsCfg = Map.fromList [("haskell.ghc", "9.12.2")] case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty Map.empty Map.empty of Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"- (.source) (resolved Map.! "project.name") `shouldBe` FromCLI- (.value) (resolved Map.! "license") `shouldBe` VText "BSD3"- (.source) (resolved Map.! "license") `shouldBe` FromLocalConfig- (.value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"- (.source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell"+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"+ (^. #source) (resolved Map.! "project.name") `shouldBe` FromCLI+ (^. #value) (resolved Map.! "license") `shouldBe` VText "BSD3"+ (^. #source) (resolved Map.! "license") `shouldBe` FromLocalConfig+ (^. #value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"+ (^. #source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "omits non-required variable with no value from any source" $ do@@ -680,8 +682,8 @@ global = Map.fromList [("optional.var", "from-global")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "optional.var") `shouldBe` VText "from-global"- (.source) (resolved Map.! "optional.var") `shouldBe` FromGlobalConfig+ (^. #value) (resolved Map.! "optional.var") `shouldBe` VText "from-global"+ (^. #source) (resolved Map.! "optional.var") `shouldBe` FromGlobalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves mix of required, optional-with-value, and optional-without-value" $ do@@ -695,9 +697,9 @@ global = Map.fromList [("optional.present", "found")] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "my-app"+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "my-app" Map.member "optional.missing" resolved `shouldBe` False- (.value) (resolved Map.! "optional.present") `shouldBe` VText "found"+ (^. #value) (resolved Map.! "optional.present") `shouldBe` VText "found" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "coerces config values through type system" $ do@@ -707,7 +709,7 @@ local = Map.fromList [("enable.tests", "true")] case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of Right resolved ->- (.value) (resolved Map.! "enable.tests") `shouldBe` VBool True+ (^. #value) (resolved Map.! "enable.tests") `shouldBe` VBool True Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "resolves from parent-supplied vars" $ do@@ -717,8 +719,8 @@ parentVars = Map.fromList [("skill.name", ("exec-plan", "parent-mod"))] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty parentVars of Right resolved -> do- (.value) (resolved Map.! "skill.name") `shouldBe` VText "exec-plan"- (.source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod"+ (^. #value) (resolved Map.! "skill.name") `shouldBe` VText "exec-plan"+ (^. #source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "parent-supplied var overrides module default" $ do@@ -728,8 +730,8 @@ parentVars = Map.fromList [("skill.name", ("parent-val", "parent-mod"))] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty parentVars of Right resolved -> do- (.value) (resolved Map.! "skill.name") `shouldBe` VText "parent-val"- (.source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod"+ (^. #value) (resolved Map.! "skill.name") `shouldBe` VText "parent-val"+ (^. #source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "global config overrides parent-supplied var" $ do@@ -740,8 +742,8 @@ parentVars = Map.fromList [("skill.name", ("parent-val", "parent-mod"))] case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global parentVars of Right resolved -> do- (.value) (resolved Map.! "skill.name") `shouldBe` VText "global-val"- (.source) (resolved Map.! "skill.name") `shouldBe` FromGlobalConfig+ (^. #value) (resolved Map.! "skill.name") `shouldBe` VText "global-val"+ (^. #source) (resolved Map.! "skill.name") `shouldBe` FromGlobalConfig Left errs -> expectationFailure ("Expected Right, got: " <> show errs) describe "diagnoseResolution" $ do
test/Seihou/Dhall/EvalSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Dhall.EvalSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Types@@ -62,61 +64,61 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- m.name `shouldBe` ModuleName "haskell-base"- m.description `shouldBe` Just "A Haskell project template"- length (m.vars) `shouldBe` 3- length (m.prompts) `shouldBe` 1- length (m.steps) `shouldBe` 5- length (m.exports) `shouldBe` 1- m.dependencies `shouldBe` []+ (m ^. #name) `shouldBe` ModuleName "haskell-base"+ (m ^. #description) `shouldBe` Just "A Haskell project template"+ length (m ^. #vars) `shouldBe` 3+ length (m ^. #prompts) `shouldBe` 1+ length (m ^. #steps) `shouldBe` 5+ length (m ^. #exports) `shouldBe` 1+ (m ^. #dependencies) `shouldBe` [] it "decodes variable declarations correctly" $ do result <- evalModuleFromFile (fixtureDir </> "haskell-base" </> "module.dhall") case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let (projectName : projectVersion : _) = m.vars- projectName.name `shouldBe` VarName "project.name"- projectName.type_ `shouldBe` VTText- projectName.default_ `shouldBe` Nothing- projectName.required `shouldBe` True- projectName.validation `shouldBe` Just (ValPattern "[a-z][a-z0-9-]*")+ let (projectName : projectVersion : _) = (m ^. #vars)+ (projectName ^. #name) `shouldBe` VarName "project.name"+ (projectName ^. #type_) `shouldBe` VTText+ (projectName ^. #default_) `shouldBe` Nothing+ (projectName ^. #required) `shouldBe` True+ (projectName ^. #validation) `shouldBe` Just (ValPattern "[a-z][a-z0-9-]*") - projectVersion.name `shouldBe` VarName "project.version"- projectVersion.default_ `shouldBe` Just (VText "0.1.0.0")- projectVersion.required `shouldBe` False+ (projectVersion ^. #name) `shouldBe` VarName "project.version"+ (projectVersion ^. #default_) `shouldBe` Just (VText "0.1.0.0")+ (projectVersion ^. #required) `shouldBe` False it "decodes steps with correct strategy" $ do result <- evalModuleFromFile (fixtureDir </> "haskell-base" </> "module.dhall") case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let (readme : libStep : licenseStep : _) = m.steps- readme.strategy `shouldBe` Template- readme.src `shouldBe` "README.md.tpl"- readme.dest `shouldBe` "README.md"- readme.condition `shouldBe` Nothing- readme.patch `shouldBe` Nothing+ let (readme : libStep : licenseStep : _) = (m ^. #steps)+ (readme ^. #strategy) `shouldBe` Template+ (readme ^. #src) `shouldBe` "README.md.tpl"+ (readme ^. #dest) `shouldBe` "README.md"+ (readme ^. #condition) `shouldBe` Nothing+ (readme ^. #patch) `shouldBe` Nothing - libStep.strategy `shouldBe` Template- libStep.src `shouldBe` "src/Lib.hs.tpl"- libStep.dest `shouldBe` "src/Lib.hs"- libStep.patch `shouldBe` Nothing+ (libStep ^. #strategy) `shouldBe` Template+ (libStep ^. #src) `shouldBe` "src/Lib.hs.tpl"+ (libStep ^. #dest) `shouldBe` "src/Lib.hs"+ (libStep ^. #patch) `shouldBe` Nothing - licenseStep.strategy `shouldBe` Copy- licenseStep.src `shouldBe` "LICENSE"- licenseStep.dest `shouldBe` "LICENSE"- licenseStep.condition `shouldBe` Just (ExprIsSet "license")- licenseStep.patch `shouldBe` Nothing+ (licenseStep ^. #strategy) `shouldBe` Copy+ (licenseStep ^. #src) `shouldBe` "LICENSE"+ (licenseStep ^. #dest) `shouldBe` "LICENSE"+ (licenseStep ^. #condition) `shouldBe` Just (ExprIsSet "license")+ (licenseStep ^. #patch) `shouldBe` Nothing it "decodes exports correctly" $ do result <- evalModuleFromFile (fixtureDir </> "haskell-base" </> "module.dhall") case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let (export1 : _) = m.exports- export1.var `shouldBe` VarName "project.name"- export1.alias `shouldBe` Nothing+ let (export1 : _) = (m ^. #exports)+ (export1 ^. #var) `shouldBe` VarName "project.name"+ (export1 ^. #alias) `shouldBe` Nothing it "returns DhallEvalError for nonexistent file" $ do result <- evalModuleFromFile "/nonexistent/path/module.dhall"@@ -146,11 +148,11 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let (prompt1 : _) = m.prompts- prompt1.var `shouldBe` VarName "project.name"- prompt1.text `shouldBe` "What is the project name?"- prompt1.condition `shouldBe` Nothing- prompt1.choices `shouldBe` Nothing+ let (prompt1 : _) = (m ^. #prompts)+ (prompt1 ^. #var) `shouldBe` VarName "project.name"+ (prompt1 ^. #text) `shouldBe` "What is the project name?"+ (prompt1 ^. #condition) `shouldBe` Nothing+ (prompt1 ^. #choices) `shouldBe` Nothing it "decodes step with patch = Some \"append-file\"" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -192,10 +194,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let (s1 : s2 : s3 : _) = m.steps- s1.patch `shouldBe` Just AppendFile- s2.patch `shouldBe` Just PrependFile- s3.patch `shouldBe` Just AppendSection+ let (s1 : s2 : s3 : _) = (m ^. #steps)+ (s1 ^. #patch) `shouldBe` Just AppendFile+ (s2 ^. #patch) `shouldBe` Just PrependFile+ (s3 ^. #patch) `shouldBe` Just AppendSection it "returns Left for unknown patch operation (not a crash)" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -235,9 +237,9 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let v = head m.vars- v.type_ `shouldBe` VTBool- v.default_ `shouldBe` Just (VBool True)+ let v = head (m ^. #vars)+ (v ^. #type_) `shouldBe` VTBool+ (v ^. #default_) `shouldBe` Just (VBool True) it "coerces an int default to VInt at decode time" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -246,9 +248,9 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- let v = head m.vars- v.type_ `shouldBe` VTInt- v.default_ `shouldBe` Just (VInt 3)+ let v = head (m ^. #vars)+ (v ^. #type_) `shouldBe` VTInt+ (v ^. #default_) `shouldBe` Just (VInt 3) it "fails module load on a malformed bool default" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -284,10 +286,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- length m.dependencies `shouldBe` 1- let dep = head m.dependencies- dep.depModule `shouldBe` ModuleName "base"- Map.null dep.depVars `shouldBe` True+ length (m ^. #dependencies) `shouldBe` 1+ let dep = head (m ^. #dependencies)+ (dep ^. #module_) `shouldBe` ModuleName "base"+ Map.null (dep ^. #vars) `shouldBe` True it "decodes a parameterized record dependency" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -296,10 +298,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- length m.dependencies `shouldBe` 1- let dep = head m.dependencies- dep.depModule `shouldBe` ModuleName "base"- Map.lookup (VarName "x") dep.depVars `shouldBe` Just "y"+ length (m ^. #dependencies) `shouldBe` 1+ let dep = head (m ^. #dependencies)+ (dep ^. #module_) `shouldBe` ModuleName "base"+ Map.lookup (VarName "x") (dep ^. #vars) `shouldBe` Just "y" it "decodes a parameterized dependency with empty vars" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -308,10 +310,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- length m.dependencies `shouldBe` 1- let dep = head m.dependencies- dep.depModule `shouldBe` ModuleName "base"- Map.null dep.depVars `shouldBe` True+ length (m ^. #dependencies) `shouldBe` 1+ let dep = head (m ^. #dependencies)+ (dep ^. #module_) `shouldBe` ModuleName "base"+ Map.null (dep ^. #vars) `shouldBe` True it "decodes a module.dhall with parameterized dependencies" $ do withSystemTempDirectory "seihou-eval-test" $ \tmpDir -> do@@ -320,10 +322,10 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right m -> do- length m.dependencies `shouldBe` 1- let dep = head m.dependencies- dep.depModule `shouldBe` ModuleName "child-mod"- Map.lookup (VarName "skill.name") dep.depVars `shouldBe` Just "exec-plan"+ length (m ^. #dependencies) `shouldBe` 1+ let dep = head (m ^. #dependencies)+ (dep ^. #module_) `shouldBe` ModuleName "child-mod"+ Map.lookup (VarName "skill.name") (dep ^. #vars) `shouldBe` Just "exec-plan" describe "evalRecipeFromFile" $ do it "decodes the haskell-with-nix-recipe fixture" $ do@@ -331,30 +333,30 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right r -> do- r.name `shouldBe` RecipeName "haskell-with-nix"- r.version `shouldBe` Just "1.0.0"- r.description `shouldBe` Just "Haskell project with Nix integration"- length r.modules `shouldBe` 2- let (m1 : m2 : _) = r.modules- m1.depModule `shouldBe` ModuleName "haskell-base"- Map.null m1.depVars `shouldBe` True- m2.depModule `shouldBe` ModuleName "nix-flake"- Map.null m2.depVars `shouldBe` True- r.vars `shouldBe` []- r.prompts `shouldBe` []+ (r ^. #name) `shouldBe` RecipeName "haskell-with-nix"+ (r ^. #version) `shouldBe` Just "1.0.0"+ (r ^. #description) `shouldBe` Just "Haskell project with Nix integration"+ length (r ^. #modules) `shouldBe` 2+ let (m1 : m2 : _) = (r ^. #modules)+ (m1 ^. #module_) `shouldBe` ModuleName "haskell-base"+ Map.null (m1 ^. #vars) `shouldBe` True+ (m2 ^. #module_) `shouldBe` ModuleName "nix-flake"+ Map.null (m2 ^. #vars) `shouldBe` True+ (r ^. #vars) `shouldBe` []+ (r ^. #prompts) `shouldBe` [] it "decodes the haskell-pinned-recipe fixture with variable bindings" $ do result <- evalRecipeFromFile (fixtureDir </> "haskell-pinned-recipe" </> "recipe.dhall") case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right r -> do- r.name `shouldBe` RecipeName "haskell-pinned"- length r.modules `shouldBe` 2- let (m1 : m2 : _) = r.modules- m1.depModule `shouldBe` ModuleName "haskell-base"- Map.null m1.depVars `shouldBe` True- m2.depModule `shouldBe` ModuleName "nix-flake"- Map.lookup (VarName "nix.system") m2.depVars `shouldBe` Just "aarch64-darwin"+ (r ^. #name) `shouldBe` RecipeName "haskell-pinned"+ length (r ^. #modules) `shouldBe` 2+ let (m1 : m2 : _) = (r ^. #modules)+ (m1 ^. #module_) `shouldBe` ModuleName "haskell-base"+ Map.null (m1 ^. #vars) `shouldBe` True+ (m2 ^. #module_) `shouldBe` ModuleName "nix-flake"+ Map.lookup (VarName "nix.system") (m2 ^. #vars) `shouldBe` Just "aarch64-darwin" it "returns DhallEvalError for nonexistent recipe file" $ do result <- evalRecipeFromFile "/nonexistent/path/recipe.dhall"@@ -394,13 +396,13 @@ case result of Left err -> expectationFailure ("Expected Right, got Left: " <> show err) Right r -> do- r.name `shouldBe` RecipeName "prompted-recipe"- length r.vars `shouldBe` 1- let v = head r.vars- v.name `shouldBe` VarName "project.name"- v.type_ `shouldBe` VTText- v.required `shouldBe` True- length r.prompts `shouldBe` 1- let p = head r.prompts- p.var `shouldBe` VarName "project.name"- p.text `shouldBe` "What is the project name?"+ (r ^. #name) `shouldBe` RecipeName "prompted-recipe"+ length (r ^. #vars) `shouldBe` 1+ let v = head (r ^. #vars)+ (v ^. #name) `shouldBe` VarName "project.name"+ (v ^. #type_) `shouldBe` VTText+ (v ^. #required) `shouldBe` True+ length (r ^. #prompts) `shouldBe` 1+ let p = head (r ^. #prompts)+ (p ^. #var) `shouldBe` VarName "project.name"+ (p ^. #text) `shouldBe` "What is the project name?"
test/Seihou/Dhall/MigrationDecoderSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Dhall.MigrationDecoderSpec (tests) where +import Control.Lens (to, (^.))+import Data.Generics.Labels () import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as TIO@@ -22,19 +24,19 @@ it "decodes a module with no migrations field as []" $ withModuleDhall noMigrationsField $ \result -> case result of- Right m -> m.migrations `shouldBe` []+ Right m -> (m ^. #migrations) `shouldBe` [] Left err -> expectationFailure ("Expected Right, got: " <> show err) it "decodes a module with an empty migrations list" $ withModuleDhall emptyMigrations $ \result -> case result of- Right m -> m.migrations `shouldBe` []+ Right m -> (m ^. #migrations) `shouldBe` [] Left err -> expectationFailure ("Expected Right, got: " <> show err) it "decodes a single MoveFile migration" $ withModuleDhall (oneMigration moveFileOp) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {from = "1.0.0", to = "2.0.0", ops = [op]}] -> op `shouldBe` MoveFile {src = "old/Path.hs", dest = "new/Path.hs"} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -43,7 +45,7 @@ it "decodes a MoveDir migration" $ withModuleDhall (oneMigration moveDirOp) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops = [op]}] -> op `shouldBe` MoveDir {src = "app", dest = "src"} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -52,7 +54,7 @@ it "decodes a DeleteFile migration" $ withModuleDhall (oneMigration deleteFileOp) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops = [op]}] -> op `shouldBe` DeleteFile {path = "Setup.hs"} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -61,7 +63,7 @@ it "decodes a DeleteDir migration" $ withModuleDhall (oneMigration deleteDirOp) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops = [op]}] -> op `shouldBe` DeleteDir {path = "obsolete"} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -70,7 +72,7 @@ it "decodes a RunCommand migration without workDir" $ withModuleDhall (oneMigration runCommandNoWorkDir) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops = [op]}] -> op `shouldBe` RunCommand {run = "echo hi", workDir = Nothing} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -79,7 +81,7 @@ it "decodes a RunCommand migration with workDir" $ withModuleDhall (oneMigration runCommandWithWorkDir) $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops = [op]}] -> op `shouldBe` RunCommand {run = "make clean", workDir = Just "build"} other -> expectationFailure ("Unexpected migrations: " <> show other)@@ -88,7 +90,7 @@ it "decodes multiple ops in a single migration in declaration order" $ withModuleDhall multiOpMigration $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [Migration {ops}] -> ops `shouldBe` [ MoveDir {src = "app", dest = "src"},@@ -101,12 +103,12 @@ it "decodes multiple migrations in declaration order" $ withModuleDhall twoChainedMigrations $ \result -> case result of- Right m -> case m.migrations of+ Right m -> case m ^. #migrations of [m1, m2] -> do- m1.from `shouldBe` "1.0.0"- m1.to `shouldBe` "2.0.0"- m2.from `shouldBe` "2.0.0"- m2.to `shouldBe` "3.0.0"+ (m1 ^. #from) `shouldBe` "1.0.0"+ (m1 ^. #to) `shouldBe` "2.0.0"+ (m2 ^. #from) `shouldBe` "2.0.0"+ (m2 ^. #to) `shouldBe` "3.0.0" other -> expectationFailure ("Unexpected migrations: " <> show other) Left err -> expectationFailure ("Expected Right, got: " <> show err)
test/Seihou/Effect/BaselineStoreSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Effect.BaselineStoreSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -81,10 +83,10 @@ beforeTamper `shouldBe` Right "kept" afterTamper `shouldBe` Left (BaselineCorrupt kept (hashContent "tampered")) pruned `shouldBe` [removed]- Map.lookup keptPath fs.files `shouldBe` Just "kept"- Map.member removedPath fs.files `shouldBe` False- Map.lookup unrelated fs.files `shouldBe` Just "leave me"- Map.member staleTemp fs.files `shouldBe` False+ Map.lookup keptPath (fs ^. #files) `shouldBe` Just "kept"+ Map.member removedPath (fs ^. #files) `shouldBe` False+ Map.lookup unrelated (fs ^. #files) `shouldBe` Just "leave me"+ Map.member staleTemp (fs ^. #files) `shouldBe` False it "round-trips on a real filesystem with one deduplicated blob" $ do withSystemTempDirectory "seihou-baselines" $ \tmpDir -> do
test/Seihou/Effect/ConfigWriterSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Effect.ConfigWriterSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Effectful import Seihou.Core.Types (ConfigScope (..))@@ -25,14 +27,14 @@ result `shouldBe` Right (Map.fromList [("project.name", "my-app")]) it "overwrites an existing value" $ do- let initial = emptyConfigWriterState {cwLocal = Map.fromList [("key", "old")]}+ let initial = emptyConfigWriterState & #local .~ Map.fromList [("key", "old")] (result, _) = run initial $ do writeConfigValue ScopeLocal "key" "new" listConfigValues ScopeLocal result `shouldBe` Right (Map.fromList [("key", "new")]) it "preserves other keys when writing" $ do- let initial = emptyConfigWriterState {cwLocal = Map.fromList [("existing", "keep")]}+ let initial = emptyConfigWriterState & #local .~ Map.fromList [("existing", "keep")] (result, _) = run initial $ do writeConfigValue ScopeLocal "new-key" "added" listConfigValues ScopeLocal@@ -68,21 +70,21 @@ describe "deleteConfigValue" $ do it "removes an existing value" $ do- let initial = emptyConfigWriterState {cwLocal = Map.fromList [("key", "val")]}+ let initial = emptyConfigWriterState & #local .~ Map.fromList [("key", "val")] (result, _) = run initial $ do deleteConfigValue ScopeLocal "key" listConfigValues ScopeLocal result `shouldBe` Right Map.empty it "is a no-op for nonexistent key" $ do- let initial = emptyConfigWriterState {cwLocal = Map.fromList [("keep", "me")]}+ let initial = emptyConfigWriterState & #local .~ Map.fromList [("keep", "me")] (result, _) = run initial $ do deleteConfigValue ScopeLocal "nonexistent" listConfigValues ScopeLocal result `shouldBe` Right (Map.fromList [("keep", "me")]) it "deletes from global scope" $ do- let initial = emptyConfigWriterState {cwGlobal = Map.fromList [("license", "MIT")]}+ let initial = emptyConfigWriterState & #global .~ Map.fromList [("license", "MIT")] (result, _) = run initial $ do deleteConfigValue ScopeGlobal "license" listConfigValues ScopeGlobal@@ -94,6 +96,6 @@ writeConfigValue ScopeLocal "local.key" "l" writeConfigValue ScopeGlobal "global.key" "g" writeConfigValue (ScopeNamespace "ns") "ns.key" "n"- finalState.cwLocal `shouldBe` Map.fromList [("local.key", "l")]- finalState.cwGlobal `shouldBe` Map.fromList [("global.key", "g")]- Map.lookup "ns" (finalState.cwNamespaces) `shouldBe` Just (Map.fromList [("ns.key", "n")])+ (finalState ^. #local) `shouldBe` Map.fromList [("local.key", "l")]+ (finalState ^. #global) `shouldBe` Map.fromList [("global.key", "g")]+ Map.lookup "ns" (finalState ^. #namespaces) `shouldBe` Just (Map.fromList [("ns.key", "n")])
test/Seihou/Effect/FilesystemSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Effect.FilesystemSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Effectful@@ -76,7 +78,7 @@ let (_, fs) = runPure emptyFS $ do writeFileText "a.txt" "aaa" writeFileText "b.txt" "bbb"- Map.size fs.files `shouldBe` 2+ Map.size (fs ^. #files) `shouldBe` 2 it "getCurrentDirectory returns /pure-fs" $ do let (cwd, _) = runPure emptyFS getCurrentDirectory
test/Seihou/Effect/LoggerSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Effect.LoggerSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Effectful import Seihou.Core.Types (LogLevel (..)) import Seihou.Effect.Logger (logDebug, logError, logInfo, logWarn)@@ -21,10 +23,10 @@ logInfo "i1" logWarn "w1" logError "e1"- st.logDebugMsgs `shouldBe` ["d1"]- st.logInfoMsgs `shouldBe` ["i1"]- st.logWarnMsgs `shouldBe` ["w1"]- st.logErrorMsgs `shouldBe` ["e1"]+ (st ^. #debugMsgs) `shouldBe` ["d1"]+ (st ^. #infoMsgs) `shouldBe` ["i1"]+ (st ^. #warnMsgs) `shouldBe` ["w1"]+ (st ^. #errorMsgs) `shouldBe` ["e1"] it "preserves message order within each field" $ do let ((), st) = runPureEff $ runLoggerPure $ do@@ -33,22 +35,22 @@ logInfo "third" logDebug "a" logDebug "b"- st.logInfoMsgs `shouldBe` ["first", "second", "third"]- st.logDebugMsgs `shouldBe` ["a", "b"]+ (st ^. #infoMsgs) `shouldBe` ["first", "second", "third"]+ (st ^. #debugMsgs) `shouldBe` ["a", "b"] it "produces empty state when no messages are logged" $ do let ((), st) = runPureEff $ runLoggerPure $ pure ()- st.logDebugMsgs `shouldBe` []- st.logInfoMsgs `shouldBe` []- st.logWarnMsgs `shouldBe` []- st.logErrorMsgs `shouldBe` []+ (st ^. #debugMsgs) `shouldBe` []+ (st ^. #infoMsgs) `shouldBe` []+ (st ^. #warnMsgs) `shouldBe` []+ (st ^. #errorMsgs) `shouldBe` [] it "returns the computation result alongside state" $ do let (result, st) = runPureEff $ runLoggerPure $ do logInfo "hello" pure (42 :: Int) result `shouldBe` 42- st.logInfoMsgs `shouldBe` ["hello"]+ (st ^. #infoMsgs) `shouldBe` ["hello"] describe "shouldLog" $ do it "LogVerbose configured shows all levels" $ do
test/Seihou/Effect/ManifestStoreSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Effect.ManifestStoreSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -29,17 +31,9 @@ sampleManifest :: Manifest sampleManifest = (emptyManifest fixedTime)- { modules =- [ AppliedModule (ModuleName "haskell-base") emptyParentVars "/path/to/mod" Nothing fixedTime Nothing- ],- vars = Map.fromList [(VarName "project.name", "my-app")],- files =- Map.fromList- [ ( "README.md",- FileRecord (SHA256 "abc123") (ModuleName "haskell-base") Template fixedTime Nothing mempty- )- ]- }+ & #modules .~ [AppliedModule (ModuleName "haskell-base") emptyParentVars (LocalOrigin "haskell-base") Nothing fixedTime Nothing]+ & #vars .~ Map.fromList [(VarName "project.name", "my-app")]+ & #files .~ Map.fromList [("README.md", FileRecord (SHA256 "abc123") (ModuleName "haskell-base") Template fixedTime Nothing mempty)] spec :: Spec spec = do@@ -94,7 +88,7 @@ runManifestStore manifestPath (writeManifest sampleManifest) c <- readFileText manifestPath pure ((), c)- T.isInfixOf "\"version\":5" content `shouldBe` True+ T.isInfixOf "\"version\":6" content `shouldBe` True T.isInfixOf "haskell-base" content `shouldBe` True T.isInfixOf "my-app" content `shouldBe` True @@ -104,9 +98,9 @@ runPureEff $ runFilesystemPure emptyFS $ runManifestStore manifestPath (writeManifest sampleManifest)- Map.member manifestPath finalFS.files `shouldBe` True- Map.member (manifestPath <> ".tmp") finalFS.files `shouldBe` False- Set.member ".seihou" finalFS.dirs `shouldBe` True+ Map.member manifestPath (finalFS ^. #files) `shouldBe` True+ Map.member (manifestPath <> ".tmp") (finalFS ^. #files) `shouldBe` False+ Set.member ".seihou" (finalFS ^. #dirs) `shouldBe` True it "returns Left for corrupt JSON" $ do let manifestPath = ".seihou/manifest.json"
test/Seihou/Engine/BaselineSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.BaselineSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Time (UTCTime, defaultTimeLocale, parseTimeOrError)@@ -48,9 +50,9 @@ Right records -> do let enriched = records Map.! path expectedRef = baselineRefForContent content- enriched.hash `shouldBe` hashContent content- enriched.baseline `shouldBe` Just expectedRef- enriched.applicationIds `shouldBe` Set.singleton applicationId+ (enriched ^. #hash) `shouldBe` hashContent content+ (enriched ^. #baseline) `shouldBe` Just expectedRef+ (enriched ^. #applicationIds) `shouldBe` Set.singleton applicationId Map.lookup expectedRef stored `shouldBe` Just content it "returns an error and publishes no reference when a generated file is missing" $ do@@ -74,7 +76,7 @@ captured <- recordGeneratedBaselines "" (Map.singleton "copy.txt" record) case captured of Left err -> pure (Left err)- Right records -> case (records Map.! "copy.txt").baseline of+ Right records -> case (records Map.! "copy.txt") ^. #baseline of Nothing -> pure (Left (BaselineStoreFailure "missing reference")) Just ref -> readBaseline ref result `shouldBe` Right "round trip"@@ -87,15 +89,9 @@ mkRecord ref = FileRecord (hashContent "applied") "module" Template fixedTime ref Set.empty manifest :: Manifest manifest =- (emptyManifest fixedTime)- { files =- Map.fromList- [ ("a", mkRecord (Just first)),- ("b", mkRecord (Just first)),- ("c", mkRecord (Just second)),- ("legacy", mkRecord Nothing)- ]- }+ ( (emptyManifest fixedTime)+ & #files .~ Map.fromList [("a", mkRecord (Just first)), ("b", mkRecord (Just first)), ("c", mkRecord (Just second)), ("legacy", mkRecord Nothing)]+ ) manifestBaselineRefs manifest `shouldBe` Set.fromList [first, second] isStoreFailure :: Either BaselineError a -> Bool
test/Seihou/Engine/ConflictSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.ConflictSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Text qualified as T import Effectful import Seihou.Core.Types@@ -52,7 +54,7 @@ it "does not produce console output when force is True" $ do let conflicts = [mkConflict "a.txt"] (_result, st) = runPureEff $ runConsolePure [] $ resolveConflicts True conflicts- st.consoleOutputs `shouldBe` []+ (st ^. #outputs) `shouldBe` [] describe "resolveConflictsInteractive" $ do it "resolves accept with 'a'" $ do@@ -87,7 +89,7 @@ case result of Just [(_, res)] -> res `shouldBe` AcceptNew _ -> expectationFailure "Expected Just with one AcceptNew resolution"- any (T.isInfixOf "Invalid choice") (st.consoleOutputs) `shouldBe` True+ any (T.isInfixOf "Invalid choice") (st ^. #outputs) `shouldBe` True it "resolves multiple files in order" $ do let conflicts = [mkConflict "a.txt", mkConflict "b.txt", mkConflict "c.txt"]@@ -96,7 +98,7 @@ Just resolved -> do length resolved `shouldBe` 3 map snd resolved `shouldBe` [AcceptNew, KeepCurrent, Skip]- map ((.path) . fst) resolved `shouldBe` ["a.txt", "b.txt", "c.txt"]+ map ((^. #path) . fst) resolved `shouldBe` ["a.txt", "b.txt", "c.txt"] Nothing -> expectationFailure "Expected Just, got Nothing" it "abort on second file stops prompting" $ do@@ -104,7 +106,7 @@ (result, st) = runPureEff $ runConsolePure ["a", "A"] $ resolveConflictsInteractive conflicts result `shouldBe` Nothing -- Should have prompted for a.txt and b.txt, but not c.txt- let outputs = T.unlines (st.consoleOutputs)+ let outputs = T.unlines (st ^. #outputs) T.isInfixOf "a.txt" outputs `shouldBe` True T.isInfixOf "b.txt" outputs `shouldBe` True T.isInfixOf "c.txt" outputs `shouldBe` False@@ -112,7 +114,7 @@ it "outputs file paths in prompt messages" $ do let conflict = mkConflict "src/Main.hs" (_result, st) = runPureEff $ runConsolePure ["a"] $ resolveConflictsInteractive [conflict]- outputs = T.unlines (st.consoleOutputs)+ outputs = T.unlines (st ^. #outputs) T.isInfixOf "src/Main.hs" outputs `shouldBe` True T.isInfixOf "modified since last generation" outputs `shouldBe` True @@ -131,21 +133,21 @@ map snd resolved `shouldBe` [KeepCurrent, AcceptNew] Nothing -> expectationFailure "Expected Just, got Nothing" -- Verify prompt output was produced- let outputs = T.unlines (st.consoleOutputs)+ let outputs = T.unlines (st ^. #outputs) T.isInfixOf "config.yaml" outputs `shouldBe` True T.isInfixOf "Makefile" outputs `shouldBe` True it "non-interactive mode produces no console output" $ do let conflicts = [mkConflict "a.txt"] (_result, st) = runPureEff $ runConsolePureNonInteractive $ resolveConflicts False conflicts- st.consoleOutputs `shouldBe` []+ (st ^. #outputs) `shouldBe` [] it "force mode preserves conflict file references in resolution" $ do let c = mkConflict "important.txt" (result, _st) = runPureEff $ runConsolePure [] $ resolveConflicts True [c] case result of Just [(resolved_c, AcceptNew)] ->- resolved_c.path `shouldBe` "important.txt"+ (resolved_c ^. #path) `shouldBe` "important.txt" _ -> expectationFailure "Expected Just with AcceptNew for important.txt" it "interactive abort via resolveConflicts returns Nothing" $ do@@ -153,14 +155,14 @@ (result, st) = runPureEff $ runConsolePure ["A"] $ resolveConflicts False conflicts result `shouldBe` Nothing -- Only first.txt was prompted before abort- let outputs = T.unlines (st.consoleOutputs)+ let outputs = T.unlines (st ^. #outputs) T.isInfixOf "first.txt" outputs `shouldBe` True T.isInfixOf "second.txt" outputs `shouldBe` False it "choice prompt text includes all four options" $ do let conflict = mkConflict "test.txt" (_result, st) = runPureEff $ runConsolePure ["s"] $ resolveConflicts False [conflict]- outputs = T.unlines (st.consoleOutputs)+ outputs = T.unlines (st ^. #outputs) T.isInfixOf "[a]ccept" outputs `shouldBe` True T.isInfixOf "[k]eep" outputs `shouldBe` True T.isInfixOf "[s]kip" outputs `shouldBe` True
test/Seihou/Engine/DiffSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.DiffSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set (Set) import Data.Set qualified as Set@@ -46,15 +48,15 @@ manifestWithFiles recs = let base = emptyManifest fixedTime in Manifest- { version = base.version,- genAt = base.genAt,- modules = base.modules,- vars = base.vars,+ { version = base ^. #version,+ genAt = base ^. #genAt,+ modules = base ^. #modules,+ vars = base ^. #vars, files = recs,- applications = base.applications,- recipe = base.recipe,- blueprint = base.blueprint,- blueprintMigrations = base.blueprintMigrations+ applications = base ^. #applications,+ recipe = base ^. #recipe,+ blueprint = base ^. #blueprint,+ blueprintMigrations = base ^. #blueprintMigrations } spec :: Spec@@ -66,25 +68,25 @@ let manifest = emptyManifest fixedTime planned = [("README.md", "# Hello", modName, Nothing)] result = runDiff emptyFS manifest active planned- length (result.new) `shouldBe` 1- (head result.new).path `shouldBe` "README.md"+ length (result ^. #new) `shouldBe` 1+ ((head (result ^. #new)) ^. #path) `shouldBe` "README.md" it "classifies file in plan + on disk (not in manifest) as Conflict" $ do let manifest = emptyManifest fixedTime planned = [("README.md", "# Hello", modName, Nothing)] fs = PureFS (Map.singleton "README.md" "existing content") mempty result = runDiff fs manifest active planned- length (result.conflicts) `shouldBe` 1- (head result.conflicts).path `shouldBe` "README.md"+ length (result ^. #conflicts) `shouldBe` 1+ ((head (result ^. #conflicts)) ^. #path) `shouldBe` "README.md" it "classifies patch op on existing file (not in manifest) as New, not Conflict" $ do let manifest = emptyManifest fixedTime planned = [(".gitignore", ".claude/\n", modName, Just AppendSection)] fs = PureFS (Map.singleton ".gitignore" ".seihou/\n") mempty result = runDiff fs manifest active planned- length (result.conflicts) `shouldBe` 0- length (result.new) `shouldBe` 1- (head result.new).path `shouldBe` ".gitignore"+ length (result ^. #conflicts) `shouldBe` 0+ length (result ^. #new) `shouldBe` 1+ ((head (result ^. #new)) ^. #path) `shouldBe` ".gitignore" it "classifies patch op on user-modified file (in manifest) as Modified, not Conflict" $ do let originalContent = "original"@@ -94,9 +96,9 @@ planned = [("config.txt", patchContent, modName, Just AppendSection)] fs = PureFS (Map.singleton "config.txt" userContent) mempty result = runDiff fs manifest active planned- length (result.conflicts) `shouldBe` 0- length (result.modified) `shouldBe` 1- (head result.modified).path `shouldBe` "config.txt"+ length (result ^. #conflicts) `shouldBe` 0+ length (result ^. #modified) `shouldBe` 1+ ((head (result ^. #modified)) ^. #path) `shouldBe` "config.txt" it "classifies file in manifest + plan + disk (unchanged) as Unchanged" $ do let content = "# Hello World"@@ -104,8 +106,8 @@ planned = [("README.md", content, modName, Nothing)] fs = PureFS (Map.singleton "README.md" content) mempty result = runDiff fs manifest active planned- length (result.unchanged) `shouldBe` 1- head (result.unchanged) `shouldBe` "README.md"+ length (result ^. #unchanged) `shouldBe` 1+ head (result ^. #unchanged) `shouldBe` "README.md" it "classifies file in manifest + plan + disk (plan changed) as Modified" $ do let oldContent = "# Hello"@@ -115,9 +117,9 @@ -- Disk matches manifest (user didn't touch it) fs = PureFS (Map.singleton "README.md" oldContent) mempty result = runDiff fs manifest active planned- length (result.modified) `shouldBe` 1- (head result.modified).path `shouldBe` "README.md"- (head result.modified).newContent `shouldBe` newContent+ length (result ^. #modified) `shouldBe` 1+ ((head (result ^. #modified)) ^. #path) `shouldBe` "README.md"+ ((head (result ^. #modified)) ^. #newContent) `shouldBe` newContent it "classifies file in manifest + plan + disk (user modified) as Conflict" $ do let originalContent = "# Hello"@@ -128,54 +130,50 @@ -- Disk was modified by user (doesn't match manifest) fs = PureFS (Map.singleton "README.md" userContent) mempty result = runDiff fs manifest active planned- length (result.conflicts) `shouldBe` 1- (head result.conflicts).path `shouldBe` "README.md"- (head result.conflicts).planContent `shouldBe` planContent+ length (result ^. #conflicts) `shouldBe` 1+ ((head (result ^. #conflicts)) ^. #path) `shouldBe` "README.md"+ ((head (result ^. #conflicts)) ^. #planContent) `shouldBe` planContent it "classifies file in manifest only (on disk) as Orphaned" $ do let content = "orphaned content" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "old-file.txt" (mkRecord content)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "old-file.txt" (mkRecord content)+ ) planned = [] :: [(FilePath, Text, ModuleName, Maybe PatchOp)] -- module no longer produces this file fs = PureFS (Map.singleton "old-file.txt" content) mempty result = runDiff fs manifest active planned- length (result.orphaned) `shouldBe` 1- (head result.orphaned).path `shouldBe` "old-file.txt"+ length (result ^. #orphaned) `shouldBe` 1+ ((head (result ^. #orphaned)) ^. #path) `shouldBe` "old-file.txt" it "classifies file in manifest only (not on disk) as Orphaned" $ do let content = "deleted content" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "deleted.txt" (mkRecord content)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "deleted.txt" (mkRecord content)+ ) planned = [] :: [(FilePath, Text, ModuleName, Maybe PatchOp)] result = runDiff emptyFS manifest active planned- length (result.orphaned) `shouldBe` 1- (head result.orphaned).path `shouldBe` "deleted.txt"+ length (result ^. #orphaned) `shouldBe` 1+ ((head (result ^. #orphaned)) ^. #path) `shouldBe` "deleted.txt" it "classifies file in manifest + plan (deleted from disk) as Modified" $ do let content = "recreate me" manifest =- (emptyManifest fixedTime :: Manifest)- { files = Map.singleton "gone.txt" (mkRecord content)- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.singleton "gone.txt" (mkRecord content)+ ) planned = [("gone.txt", "new version", modName, Nothing)] result = runDiff emptyFS manifest active planned- length (result.modified) `shouldBe` 1- (head result.modified).path `shouldBe` "gone.txt"+ length (result ^. #modified) `shouldBe` 1+ ((head (result ^. #modified)) ^. #path) `shouldBe` "gone.txt" it "handles mixed classifications" $ do let existingContent = "existing" manifest =- (emptyManifest fixedTime :: Manifest)- { files =- Map.fromList- [ ("unchanged.txt", mkRecord existingContent),- ("orphaned.txt", mkRecord "orphan")- ]- }+ ( (emptyManifest fixedTime :: Manifest)+ & #files .~ Map.fromList [("unchanged.txt", mkRecord existingContent), ("orphaned.txt", mkRecord "orphan")]+ ) planned = [ ("unchanged.txt", existingContent, modName, Nothing), ("new-file.txt", "brand new", modName, Nothing)@@ -185,20 +183,20 @@ (Map.fromList [("unchanged.txt", existingContent), ("orphaned.txt", "orphan")]) mempty result = runDiff fs manifest active planned- length (result.new) `shouldBe` 1- length (result.unchanged) `shouldBe` 1- length (result.orphaned) `shouldBe` 1- length (result.modified) `shouldBe` 0- length (result.conflicts) `shouldBe` 0+ length (result ^. #new) `shouldBe` 1+ length (result ^. #unchanged) `shouldBe` 1+ length (result ^. #orphaned) `shouldBe` 1+ length (result ^. #modified) `shouldBe` 0+ length (result ^. #conflicts) `shouldBe` 0 it "handles empty manifest and empty plan" $ do let manifest = emptyManifest fixedTime result = runDiff emptyFS manifest Set.empty ([] :: [(FilePath, Text, ModuleName, Maybe PatchOp)])- result.new `shouldBe` []- result.modified `shouldBe` []- result.unchanged `shouldBe` []- result.conflicts `shouldBe` []- result.orphaned `shouldBe` []+ (result ^. #new) `shouldBe` []+ (result ^. #modified) `shouldBe` []+ (result ^. #unchanged) `shouldBe` []+ (result ^. #conflicts) `shouldBe` []+ (result ^. #orphaned) `shouldBe` [] it "does not classify files from inactive modules as orphaned" $ do let otherMod = ModuleName "other-module"@@ -217,8 +215,8 @@ activeModules = Set.singleton modName -- "test-module", NOT "other-module" fs = PureFS (Map.singleton "other.txt" content) mempty result = runDiff fs manifest activeModules planned- length (result.orphaned) `shouldBe` 0- length (result.new) `shouldBe` 1+ length (result ^. #orphaned) `shouldBe` 0+ length (result ^. #new) `shouldBe` 1 it "classifies files from active modules as orphaned" $ do let content = "active module content"@@ -227,8 +225,8 @@ activeModules = Set.singleton modName -- file belongs to active module fs = PureFS (Map.singleton "old.txt" content) mempty result = runDiff fs manifest activeModules planned- length (result.orphaned) `shouldBe` 1- (head result.orphaned).path `shouldBe` "old.txt"+ length (result ^. #orphaned) `shouldBe` 1+ ((head (result ^. #orphaned)) ^. #path) `shouldBe` "old.txt" it "mixed active/inactive: only orphans active module's missing files" $ do let otherMod = ModuleName "other-module"@@ -257,11 +255,11 @@ mempty result = runDiff fs manifest activeModules planned -- active-old.txt is orphaned (active module no longer produces it)- length (result.orphaned) `shouldBe` 1- (head result.orphaned).path `shouldBe` "active-old.txt"+ length (result ^. #orphaned) `shouldBe` 1+ ((head (result ^. #orphaned)) ^. #path) `shouldBe` "active-old.txt" -- other.txt is invisible (inactive module), not orphaned- length (result.new) `shouldBe` 1- (head result.new).path `shouldBe` "active-new.txt"+ length (result ^. #new) `shouldBe` 1+ ((head (result ^. #new)) ^. #path) `shouldBe` "active-new.txt" it "plan targeting inactive module's file on disk is classified as Conflict" $ do let otherMod = ModuleName "other-module"@@ -281,8 +279,8 @@ fs = PureFS (Map.singleton "shared.txt" "other content") mempty result = runDiff fs manifest activeModules planned -- File exists on disk but not in active manifest → Conflict- length (result.conflicts) `shouldBe` 1- (head result.conflicts).path `shouldBe` "shared.txt"+ length (result ^. #conflicts) `shouldBe` 1+ ((head (result ^. #conflicts)) ^. #path) `shouldBe` "shared.txt" it "handles multiple active modules scoping independently" $ do let modA = ModuleName "module-a"@@ -319,10 +317,10 @@ mempty result = runDiff fs manifest activeModules planned -- from-a.txt unchanged (active, still produced)- length (result.unchanged) `shouldBe` 1+ length (result ^. #unchanged) `shouldBe` 1 -- from-b.txt orphaned (active module B no longer produces it)- length (result.orphaned) `shouldBe` 1- (head result.orphaned).path `shouldBe` "from-b.txt"+ length (result ^. #orphaned) `shouldBe` 1+ ((head (result ^. #orphaned)) ^. #path) `shouldBe` "from-b.txt" -- from-c.txt invisible (inactive module C)- length (result.new) `shouldBe` 0- length (result.conflicts) `shouldBe` 0+ length (result ^. #new) `shouldBe` 0+ length (result ^. #conflicts) `shouldBe` 0
test/Seihou/Engine/ExecuteSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.ExecuteSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Data.Time (UTCTime, defaultTimeLocale, parseTimeOrError)@@ -45,7 +47,7 @@ let ops = [WriteFileOp "hello.txt" "hello world" Template] (records, fs) = runExecFS emptyFS ops Map.member "hello.txt" records `shouldBe` True- Map.lookup "/project/hello.txt" (fs.files) `shouldBe` Just "hello world"+ Map.lookup "/project/hello.txt" (fs ^. #files) `shouldBe` Just "hello world" it "creates a directory via CreateDirOp" $ do let ops = [CreateDirOp "src"]@@ -62,19 +64,19 @@ ops = [WriteFileOp "test.txt" content Template] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.hash `shouldBe` hashContent content+ (record ^. #hash) `shouldBe` hashContent content it "produces FileRecord with correct module name" $ do let ops = [WriteFileOp "test.txt" "data" Template] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.moduleName `shouldBe` modName+ (record ^. #moduleName) `shouldBe` modName it "produces FileRecord with correct timestamp" $ do let ops = [WriteFileOp "test.txt" "data" Template] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.generatedAt `shouldBe` fixedTime+ (record ^. #generatedAt) `shouldBe` fixedTime it "handles multiple operations" $ do let ops =@@ -84,8 +86,8 @@ ] (records, fs) = runExecFS emptyFS ops Map.size records `shouldBe` 2- Map.lookup "/project/README.md" (fs.files) `shouldBe` Just "# Hello"- Map.lookup "/project/src/Main.hs" (fs.files) `shouldBe` Just "module Main where"+ Map.lookup "/project/README.md" (fs ^. #files) `shouldBe` Just "# Hello"+ Map.lookup "/project/src/Main.hs" (fs ^. #files) `shouldBe` Just "module Main where" it "skips RunCommandOp" $ do let ops = [RunCommandOp "echo hello" Nothing modName 0]@@ -97,38 +99,38 @@ ops = [CopyFileOp "/source/file.txt" "dest.txt"] (records, fs) = runExecFS initial ops Map.member "dest.txt" records `shouldBe` True- Map.lookup "/project/dest.txt" (fs.files) `shouldBe` Just "copied content"+ Map.lookup "/project/dest.txt" (fs ^. #files) `shouldBe` Just "copied content" it "records Template strategy in FileRecord" $ do let ops = [WriteFileOp "test.txt" "content" Template] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.strategy `shouldBe` Template+ (record ^. #strategy) `shouldBe` Template it "records Copy strategy in FileRecord" $ do let ops = [WriteFileOp "test.txt" "content" Copy] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.strategy `shouldBe` Copy+ (record ^. #strategy) `shouldBe` Copy it "records DhallText strategy in FileRecord" $ do let ops = [WriteFileOp "test.txt" "content" DhallText] (records, _) = runExecFS emptyFS ops record = records Map.! "test.txt"- record.strategy `shouldBe` DhallText+ (record ^. #strategy) `shouldBe` DhallText it "records Structured strategy in FileRecord" $ do let ops = [WriteFileOp "test.json" "{}" Structured] (records, _) = runExecFS emptyFS ops record = records Map.! "test.json"- record.strategy `shouldBe` Structured+ (record ^. #strategy) `shouldBe` Structured it "executes PatchFileOp AppendFile on existing file" $ do let initial = PureFS (Map.singleton "/project/README.md" "# Title\n") mempty ops = [PatchFileOp "README.md" "extra line\n" AppendFile Template modName] (records, fs) = runExecFS initial ops Map.member "README.md" records `shouldBe` True- let content = fs.files Map.! "/project/README.md"+ let content = (fs ^. #files) Map.! "/project/README.md" T.isInfixOf "# Title" content `shouldBe` True T.isInfixOf "extra line" content `shouldBe` True @@ -137,7 +139,7 @@ ops = [PatchFileOp "README.md" "header\n" PrependFile Template modName] (records, fs) = runExecFS initial ops Map.member "README.md" records `shouldBe` True- let content = fs.files Map.! "/project/README.md"+ let content = (fs ^. #files) Map.! "/project/README.md" T.isInfixOf "header" content `shouldBe` True T.isInfixOf "# Title" content `shouldBe` True @@ -146,7 +148,7 @@ ops = [PatchFileOp "README.md" "section content\n" AppendSection Template modName] (records, fs) = runExecFS initial ops Map.member "README.md" records `shouldBe` True- let content = fs.files Map.! "/project/README.md"+ let content = (fs ^. #files) Map.! "/project/README.md" T.isInfixOf "# Title" content `shouldBe` True T.isInfixOf "seihou:test-module" content `shouldBe` True T.isInfixOf "section content" content `shouldBe` True@@ -155,7 +157,7 @@ let ops = [PatchFileOp "new.txt" "new content\n" AppendFile Template modName] (records, fs) = runExecFS emptyFS ops Map.member "new.txt" records `shouldBe` True- let content = fs.files Map.! "/project/new.txt"+ let content = (fs ^. #files) Map.! "/project/new.txt" T.isInfixOf "new content" content `shouldBe` True it "executes PatchFileOp AppendLineIfAbsent, skipping existing lines" $ do@@ -163,7 +165,7 @@ ops = [PatchFileOp ".gitignore" ".env\n.claude/\n" AppendLineIfAbsent Template modName] (records, fs) = runExecFS initial ops Map.member ".gitignore" records `shouldBe` True- let content = fs.files Map.! "/project/.gitignore"+ let content = (fs ^. #files) Map.! "/project/.gitignore" content `shouldBe` "node_modules/\n.env\n.claude/\n" it "executes PatchFileOp AppendLineIfAbsent idempotently" $ do@@ -171,7 +173,7 @@ ops = [PatchFileOp ".gitignore" ".claude/\n" AppendLineIfAbsent Template modName] (records, fs) = runExecFS initial ops Map.member ".gitignore" records `shouldBe` True- let content = fs.files Map.! "/project/.gitignore"+ let content = (fs ^. #files) Map.! "/project/.gitignore" content `shouldBe` "node_modules/\n.claude/\n" describe "dryRunPlan" $ do
test/Seihou/Engine/MigrateSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.MigrateSpec (tests) where +import Control.Lens (to, (&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -55,31 +57,8 @@ mkManifest :: [(FilePath, Text)] -> Manifest mkManifest entries = (emptyManifest fixedTime)- { modules =- [ AppliedModule- { name = modName,- parentVars = emptyParentVars,- source = "/installed/demo",- moduleVersion = Just "1.0.0",- appliedAt = fixedTime,- removal = Nothing- }- ],- files =- Map.fromList- [ ( path,- FileRecord- { hash = hashContent content,- moduleName = modName,- strategy = Template,- generatedAt = fixedTime,- baseline = Nothing,- applicationIds = mempty- }- )- | (path, content) <- entries- ]- }+ & #modules .~ [AppliedModule {name = modName, parentVars = emptyParentVars, origin = LocalOrigin "demo", moduleVersion = Just "1.0.0", appliedAt = fixedTime, removal = Nothing}]+ & #files .~ Map.fromList [(path, FileRecord {hash = hashContent content, moduleName = modName, strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty}) | (path, content) <- entries] -- | Build an in-memory filesystem from (path, content) pairs. mkFS :: [(FilePath, Text)] -> PureFS@@ -89,10 +68,10 @@ chain1 :: Text -> Text -> [MigrationOp] -> MigrationPlan chain1 fromV toV ops = MigrationPlan- { planModule = "demo",- planFrom = mkV fromV,- planTo = mkV toV,- planSteps = [Migration {from = fromV, to = toV, ops}]+ { module_ = "demo",+ from = mkV fromV,+ to = mkV toV,+ steps = [Migration {from = fromV, to = toV, ops}] } runClassifyResult :: PureFS -> Manifest -> MigrationPlan -> Either MigrationExecError ExecutedMigrationPlan@@ -132,21 +111,21 @@ fs = mkFS [("app/Main.hs", "module Main where")] c = chain1 "1.0.0" "2.0.0" [MoveFile "app/Main.hs" "src/Main.hs"] plan = runClassify fs manifest c- plan.planOps `shouldBe` [MoveFileInst "app/Main.hs" "src/Main.hs" MFSafe]+ (plan ^. #ops) `shouldBe` [MoveFileInst "app/Main.hs" "src/Main.hs" MFSafe] it "marks a move-file as conflict when disk content differs" $ do let manifest = mkManifest [("app/Main.hs", "original")] fs = mkFS [("app/Main.hs", "user-edited")] c = chain1 "1.0.0" "2.0.0" [MoveFile "app/Main.hs" "src/Main.hs"] plan = runClassify fs manifest c- plan.planOps `shouldBe` [MoveFileInst "app/Main.hs" "src/Main.hs" MFConflict]+ (plan ^. #ops) `shouldBe` [MoveFileInst "app/Main.hs" "src/Main.hs" MFConflict] it "marks a delete-file as gone when the file is absent" $ do let manifest = mkManifest [("Setup.hs", "boring")] fs = mkFS [] -- file already deleted on disk c = chain1 "1.0.0" "2.0.0" [DeleteFile "Setup.hs"] plan = runClassify fs manifest c- plan.planOps `shouldBe` [DeleteFileInst "Setup.hs" MFGone]+ (plan ^. #ops) `shouldBe` [DeleteFileInst "Setup.hs" MFGone] it "rejects a delete-dir path with a parent directory segment" $ do let manifest = mkManifest []@@ -178,12 +157,12 @@ (result, fs') = runExecute fs manifest plan False case result of Right m -> do- Map.member "src/Main.hs" m.files `shouldBe` True- Map.member "app/Main.hs" m.files `shouldBe` False- (head m.modules).moduleVersion `shouldBe` Just "2.0.0"+ Map.member "src/Main.hs" (m ^. #files) `shouldBe` True+ Map.member "app/Main.hs" (m ^. #files) `shouldBe` False+ ((head (m ^. #modules)) ^. #moduleVersion) `shouldBe` Just "2.0.0" Left err -> expectationFailure ("expected Right, got: " <> show err)- Map.member "src/Main.hs" fs'.files `shouldBe` True- Map.member "app/Main.hs" fs'.files `shouldBe` False+ Map.member "src/Main.hs" (fs' ^. #files) `shouldBe` True+ Map.member "app/Main.hs" (fs' ^. #files) `shouldBe` False it "refuses on conflict without --force and leaves disk untouched" $ do let manifest = mkManifest [("app/Main.hs", "original")]@@ -193,8 +172,8 @@ (result, fs') = runExecute fs manifest plan False result `shouldBe` Left (MigrationConflict ["app/Main.hs"]) -- Disk untouched: original src still there, dest absent.- Map.member "app/Main.hs" fs'.files `shouldBe` True- Map.member "src/Main.hs" fs'.files `shouldBe` False+ Map.member "app/Main.hs" (fs' ^. #files) `shouldBe` True+ Map.member "src/Main.hs" (fs' ^. #files) `shouldBe` False it "executes through a conflict when force is set" $ do let manifest = mkManifest [("app/Main.hs", "original")]@@ -204,12 +183,12 @@ (result, fs') = runExecute fs manifest plan True case result of Right m -> do- Map.member "src/Main.hs" m.files `shouldBe` True- Map.member "app/Main.hs" m.files `shouldBe` False+ Map.member "src/Main.hs" (m ^. #files) `shouldBe` True+ Map.member "app/Main.hs" (m ^. #files) `shouldBe` False Left err -> expectationFailure ("expected Right, got: " <> show err) -- The user-edited content rode along: the move is a key rename in -- the pure FS, so the bytes follow the rename.- Map.lookup "src/Main.hs" fs'.files `shouldBe` Just "user-edited"+ Map.lookup "src/Main.hs" (fs' ^. #files) `shouldBe` Just "user-edited" it "moves a directory, rewriting all contained manifest entries" $ do let manifest =@@ -227,9 +206,9 @@ (result, fs') = runExecute fs manifest plan False case result of Right m -> do- Map.keys m.files `shouldMatchList` ["src/Main.hs", "src/Lib.hs"]+ Map.keys (m ^. #files) `shouldMatchList` ["src/Main.hs", "src/Lib.hs"] Left err -> expectationFailure ("expected Right, got: " <> show err)- Map.keys fs'.files `shouldMatchList` ["src/Main.hs", "src/Lib.hs"]+ Map.keys (fs' ^. #files) `shouldMatchList` ["src/Main.hs", "src/Lib.hs"] it "is a no-op for a delete-file whose target is already gone" $ do let manifest = mkManifest [("Setup.hs", "boring")]@@ -238,9 +217,9 @@ plan = runClassify fs manifest c (result, fs') = runExecute fs manifest plan False case result of- Right m -> Map.member "Setup.hs" m.files `shouldBe` False+ Right m -> Map.member "Setup.hs" (m ^. #files) `shouldBe` False Left err -> expectationFailure ("expected Right, got: " <> show err)- Map.null fs'.files `shouldBe` True+ Map.null (fs' ^. #files) `shouldBe` True it "deletes a directory and drops every manifest entry under it" $ do let manifest =@@ -259,9 +238,9 @@ plan = runClassify fs manifest c (result, fs') = runExecute fs manifest plan False case result of- Right m -> Map.keys m.files `shouldBe` ["keep.hs"]+ Right m -> Map.keys (m ^. #files) `shouldBe` ["keep.hs"] Left err -> expectationFailure ("expected Right, got: " <> show err)- Map.keys fs'.files `shouldBe` ["keep.hs"]+ Map.keys (fs' ^. #files) `shouldBe` ["keep.hs"] it "applies a chain of two migrations in declaration order" $ do -- 1.0.0 → 2.0.0: move app → src@@ -270,10 +249,10 @@ fs = mkFS [("app/Main.hs", "x")] chain = MigrationPlan- { planModule = "demo",- planFrom = mkV "1.0.0",- planTo = mkV "3.0.0",- planSteps =+ { module_ = "demo",+ from = mkV "1.0.0",+ to = mkV "3.0.0",+ steps = [ Migration "1.0.0" "2.0.0" [MoveDir "app" "src"], Migration "2.0.0" "3.0.0" [DeleteFile "src/Main.hs"] ]@@ -282,7 +261,7 @@ (result, fs') = runExecute fs manifest plan False case result of Right m -> do- Map.null m.files `shouldBe` True- (head m.modules).moduleVersion `shouldBe` Just "3.0.0"+ Map.null (m ^. #files) `shouldBe` True+ ((head (m ^. #modules)) ^. #moduleVersion) `shouldBe` Just "3.0.0" Left err -> expectationFailure ("expected Right, got: " <> show err)- Map.null fs'.files `shouldBe` True+ Map.null (fs' ^. #files) `shouldBe` True
test/Seihou/Engine/PlanSpec.hs view
@@ -1,6 +1,7 @@ module Seihou.Engine.PlanSpec (tests) where import Data.Aeson qualified as Aeson+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Data.Text.Encoding qualified as T@@ -538,28 +539,28 @@ Left errs -> expectationFailure ("Expected Right, got: " <> show errs) Right ops -> do -- Should have operations for README, src/Lib.hs, LICENSE, my-app.cabal, and cabal.project- let writeOps = [op | op@(WriteFileOp _ _ _) <- ops]- dirOps = [op | op@(CreateDirOp _) <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops]+ dirPaths = [path | CreateDirOp {path} <- ops] length writeOps `shouldBe` 5 -- README.md with rendered content- (writeOps !! 0).dest `shouldBe` "README.md"- T.isInfixOf "my-app" ((writeOps !! 0).content) `shouldBe` True+ fst (writeOps !! 0) `shouldBe` "README.md"+ T.isInfixOf "my-app" (snd (writeOps !! 0)) `shouldBe` True -- src/Lib.hs- (writeOps !! 1).dest `shouldBe` "src/Lib.hs"+ fst (writeOps !! 1) `shouldBe` "src/Lib.hs" -- LICENSE (copy)- (writeOps !! 2).dest `shouldBe` "LICENSE"+ fst (writeOps !! 2) `shouldBe` "LICENSE" -- my-app.cabal (dest expanded from {{project.name}}.cabal)- (writeOps !! 3).dest `shouldBe` "my-app.cabal"- T.isInfixOf "my-app" ((writeOps !! 3).content) `shouldBe` True+ fst (writeOps !! 3) `shouldBe` "my-app.cabal"+ T.isInfixOf "my-app" (snd (writeOps !! 3)) `shouldBe` True -- cabal.project (DhallText)- (writeOps !! 4).dest `shouldBe` "cabal.project"- T.isInfixOf "my-app" ((writeOps !! 4).content) `shouldBe` True+ fst (writeOps !! 4) `shouldBe` "cabal.project"+ T.isInfixOf "my-app" (snd (writeOps !! 4)) `shouldBe` True -- Should have CreateDirOp for src/- dirOps `shouldSatisfy` any (\op -> op.path == "src")+ dirPaths `shouldSatisfy` elem "src" -- Should have RunCommandOp for the command- let cmdOps = [op | op@RunCommandOp {} <- ops]+ let cmdOps = [command | RunCommandOp {command} <- ops] length cmdOps `shouldBe` 1- (cmdOps !! 0).command `shouldBe` "echo 'Project generated'"+ (cmdOps !! 0) `shouldBe` "echo 'Project generated'" it "compiles a Template step with patch = AppendFile to PatchFileOp" $ do withFixture [("section.tpl", "appended content")] $ \baseDir -> do@@ -747,12 +748,9 @@ result <- compilePlan baseDir modul vars case result of Right ops -> do- let cmdOps = [op | op@RunCommandOp {} <- ops]+ let cmdOps = [(command, workDir, moduleName, occurrence) | RunCommandOp {command, workDir, moduleName, occurrence} <- ops] length cmdOps `shouldBe` 1- (cmdOps !! 0).command `shouldBe` "echo hello"- (cmdOps !! 0).workDir `shouldBe` Nothing- (cmdOps !! 0).moduleName `shouldBe` "test"- (cmdOps !! 0).occurrence `shouldBe` 0+ cmdOps `shouldBe` [("echo hello", Nothing, "test", 0)] Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "numbers only identical rendered commands from the same module" $ do@@ -778,7 +776,7 @@ result <- compilePlan baseDir modul Map.empty case result of Right ops -> do- let commandOccurrences = [(op.command, op.occurrence) | op@RunCommandOp {} <- ops]+ let commandOccurrences = [(command, occurrence) | RunCommandOp {command, occurrence} <- ops] commandOccurrences `shouldBe` [("echo same", 0), ("echo other", 0), ("echo same", 1)] Left errs -> expectationFailure ("Expected Right, got: " <> show errs)@@ -884,9 +882,9 @@ result <- compilePlan baseDir modul vars case result of Right ops -> do- let cmdOps = [op | op@RunCommandOp {} <- ops]+ let cmdOps = [workDir | RunCommandOp {workDir} <- ops] length cmdOps `shouldBe` 1- (cmdOps !! 0).workDir `shouldBe` Just "subdir"+ (cmdOps !! 0) `shouldBe` Just "subdir" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "interpolates {{var}} in command run field" $ do@@ -909,9 +907,9 @@ result <- compilePlan baseDir modul vars case result of Right ops -> do- let cmdOps = [op | op@RunCommandOp {} <- ops]+ let cmdOps = [command | RunCommandOp {command} <- ops] length cmdOps `shouldBe` 1- (cmdOps !! 0).command `shouldBe` "echo my-app"+ (cmdOps !! 0) `shouldBe` "echo my-app" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "interpolates {{var}} in command workDir field" $ do@@ -934,9 +932,9 @@ result <- compilePlan baseDir modul vars case result of Right ops -> do- let cmdOps = [op | op@RunCommandOp {} <- ops]+ let cmdOps = [workDir | RunCommandOp {workDir} <- ops] length cmdOps `shouldBe` 1- (cmdOps !! 0).workDir `shouldBe` Just "my-app"+ (cmdOps !! 0) `shouldBe` Just "my-app" Left errs -> expectationFailure ("Expected Right, got: " <> show errs) it "rejects a rendered command workDir with a parent directory segment" $ do
test/Seihou/Engine/PreviewSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.PreviewSpec (tests) where +import Control.Lens ((&), (.~))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Types@@ -39,56 +41,49 @@ ] result = buildPreview ops Nothing Map.empty length result `shouldBe` 3- (result !! 0).previewStatus `shouldBe` FsNew- (result !! 1).previewStatus `shouldBe` FsNew- (result !! 2).previewStatus `shouldBe` FsNew+ [st | FilePreview {status = st} <- result] `shouldBe` [FsNew, FsNew, FsNew] it "classifies a new file as FsNew" $ do let ops = [WriteFileOp "README.md" "# Hello" Template]- diff = emptyDiff {new = [PlannedFile "README.md" modName "# Hello"]}+ diff = (emptyDiff & #new .~ [PlannedFile "README.md" modName "# Hello"]) result = buildPreview ops (Just diff) Map.empty length result `shouldBe` 1- (head result).previewStatus `shouldBe` FsNew+ [st | FilePreview {status = st} <- result] `shouldBe` [FsNew] it "classifies a modified file as FsModified" $ do let ops = [WriteFileOp "README.md" "# Updated" Template]- diff = emptyDiff {modified = [ModifiedFile "README.md" modName (SHA256 "old") "# Updated"]}+ diff =+ ( emptyDiff+ & #modified .~ [ModifiedFile "README.md" modName (SHA256 "old") "# Updated"]+ ) result = buildPreview ops (Just diff) Map.empty length result `shouldBe` 1- (head result).previewStatus `shouldBe` FsModified+ [st | FilePreview {status = st} <- result] `shouldBe` [FsModified] it "classifies an unchanged file as FsUnchanged" $ do let ops = [WriteFileOp "README.md" "# Same" Template]- diff = emptyDiff {unchanged = ["README.md"]}+ diff = (emptyDiff & #unchanged .~ ["README.md"]) result = buildPreview ops (Just diff) Map.empty length result `shouldBe` 1- (head result).previewStatus `shouldBe` FsUnchanged+ [st | FilePreview {status = st} <- result] `shouldBe` [FsUnchanged] it "classifies a conflicting file as FsConflict" $ do let ops = [WriteFileOp "README.md" "# New" Template] diff =- emptyDiff- { conflicts =- [ ConflictFile- { path = "README.md",- moduleName = modName,- manifestHash = SHA256 "man",- diskHash = SHA256 "disk",- planContent = "# New"- }- ]- }+ ( emptyDiff+ & #conflicts .~ [ConflictFile {path = "README.md", moduleName = modName, manifestHash = SHA256 "man", diskHash = SHA256 "disk", planContent = "# New"}]+ ) result = buildPreview ops (Just diff) Map.empty length result `shouldBe` 1- (head result).previewStatus `shouldBe` FsConflict+ [st | FilePreview {status = st} <- result] `shouldBe` [FsConflict] it "classifies an orphaned file as FsOrphaned" $ do let ops = [WriteFileOp "other.txt" "content" Template] diff =- emptyDiff- { new = [PlannedFile "other.txt" modName "content"],- orphaned = [OrphanedFile "old.txt" modName]- }+ ( emptyDiff+ & #new .~ [PlannedFile "other.txt" modName "content"]+ & #orphaned .~ [OrphanedFile "old.txt" modName]+ ) result = buildPreview ops (Just diff) Map.empty -- One file preview + one orphan preview length result `shouldBe` 2@@ -102,10 +97,10 @@ it "does not include orphaned files that are produced by an operation" $ do let ops = [WriteFileOp "reused.txt" "content" Template] diff =- emptyDiff- { new = [PlannedFile "reused.txt" modName "content"],- orphaned = [OrphanedFile "reused.txt" modName2]- }+ ( emptyDiff+ & #new .~ [PlannedFile "reused.txt" modName "content"]+ & #orphaned .~ [OrphanedFile "reused.txt" modName2]+ ) result = buildPreview ops (Just diff) Map.empty -- Only the file preview, orphan is suppressed because path matches an operation length result `shouldBe` 1@@ -174,7 +169,7 @@ WriteFileOp "d.txt" "" Structured ] result = buildPreview ops Nothing Map.empty- map (.previewAnnotation) (filter isFilePreview result)+ [a | FilePreview {annotation = a} <- result] `shouldBe` ["copy", "template", "dhall-text", "structured"] describe "renderPreviewPlain" $ do@@ -214,7 +209,7 @@ describe "formatPlanView" $ do it "includes header with module names" $ do let preview = [FilePreview FsNew "README.md" "template" (Just modName)]- diff = emptyDiff {new = [PlannedFile "README.md" modName "# Hello"]}+ diff = (emptyDiff & #new .~ [PlannedFile "README.md" modName "# Hello"]) rendered = formatPlanView [modName] Map.empty preview diff T.isInfixOf "Generation Plan (test-module):" rendered `shouldBe` True @@ -239,10 +234,10 @@ it "includes summary with file and conflict counts" $ do let diff =- emptyDiff- { new = [PlannedFile "a.txt" modName ""],- modified = [ModifiedFile "b.txt" modName (SHA256 "old") "new"]- }+ ( emptyDiff+ & #new .~ [PlannedFile "a.txt" modName ""]+ & #modified .~ [ModifiedFile "b.txt" modName (SHA256 "old") "new"]+ ) rendered = formatPlanView [modName] Map.empty [] diff T.isInfixOf "2 files to write, 0 conflicts" rendered `shouldBe` True
test/Seihou/Engine/ReconcileSpec.hs view
@@ -1,6 +1,8 @@ module Seihou.Engine.ReconcileSpec (tests) where +import Control.Lens ((^.)) import Data.Functor.Identity (Identity (..))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -31,9 +33,9 @@ result = planWith (Map.singleton ".gitignore" "root\n") Map.empty Map.empty empty [appA] operations (owners ".gitignore" [appA]) cleanMerge case result of Right reconciliation -> do- Map.size reconciliation.files `shouldBe` 1- case reconciliation.files Map.! ".gitignore" of- FileUpdate desired _ _ _ -> desired.generatedContent `shouldBe` "root\none\ntwo\nthree\n"+ Map.size (reconciliation ^. #files) `shouldBe` 1+ case (reconciliation ^. #files) Map.! ".gitignore" of+ FileUpdate desired _ _ _ -> (desired ^. #generatedContent) `shouldBe` "root\none\ntwo\nthree\n" other -> expectationFailure ("expected one update, got " <> show other) Left err -> expectationFailure (show err) @@ -44,8 +46,8 @@ ] result = planWith Map.empty Map.empty Map.empty empty [appA] operations (owners "README.md" [appA]) cleanMerge case result of- Right reconciliation -> case reconciliation.files Map.! "README.md" of- FileCreate desired _ _ -> desired.generatedContent `shouldBe` "generated\nadded\n"+ Right reconciliation -> case (reconciliation ^. #files) Map.! "README.md" of+ FileCreate desired _ _ -> (desired ^. #generatedContent) `shouldBe` "generated\nadded\n" other -> expectationFailure ("expected create, got " <> show other) Left err -> expectationFailure (show err) @@ -62,10 +64,10 @@ (owners "copied.txt" [appA]) cleanMerge case result of- Right reconciliation -> case reconciliation.files Map.! "copied.txt" of+ Right reconciliation -> case (reconciliation ^. #files) Map.! "copied.txt" of FileCreate desired _ _ -> do- desired.generatedContent `shouldBe` "copied\n"- desired.strategy `shouldBe` Copy+ (desired ^. #generatedContent) `shouldBe` "copied\n"+ (desired ^. #strategy) `shouldBe` Copy other -> expectationFailure ("expected create, got " <> show other) Left err -> expectationFailure (show err) @@ -83,7 +85,7 @@ (owners "legacy.txt" [appA]) cleanMerge case result of- Right reconciliation -> case reconciliation.files Map.! "legacy.txt" of+ Right reconciliation -> case (reconciliation ^. #files) Map.! "legacy.txt" of FileUpdate _ _ _ _ -> pure () other -> expectationFailure ("expected trusted update, got " <> show other) Left err -> expectationFailure (show err)@@ -101,7 +103,7 @@ (owners "legacy.txt" [appA]) cleanMerge case result of- Right reconciliation -> case reconciliation.files Map.! "legacy.txt" of+ Right reconciliation -> case (reconciliation ^. #files) Map.! "legacy.txt" of FileConflict _ current _ MissingTrustedBaseline _ _ Nothing -> current `shouldBe` "user edit\n" other -> expectationFailure ("expected conservative conflict, got " <> show other) Left err -> expectationFailure (show err)@@ -164,11 +166,11 @@ (owners "file.txt" [appA]) cleanMerge case result of- Right reconciliation -> case reconciliation.files Map.! "file.txt" of+ Right reconciliation -> case (reconciliation ^. #files) Map.! "file.txt" of FileUnchanged _ state _ _ -> do- state.appliedContent `shouldBe` "user\n"- state.recordedHash `shouldBe` oldRecord.hash- state.writeToDisk `shouldBe` False+ (state ^. #appliedContent) `shouldBe` "user\n"+ (state ^. #recordedHash) `shouldBe` (oldRecord ^. #hash)+ (state ^. #writeToDisk) `shouldBe` False other -> expectationFailure ("expected unchanged user edit, got " <> show other) Left err -> expectationFailure (show err) @@ -188,8 +190,8 @@ (\_ _ _ -> MergeClean "user and generated\n") case result of Right reconciliation -> do- case reconciliation.files Map.! "file.txt" of- FileAutoMerge _ state _ _ -> state.appliedContent `shouldBe` "user and generated\n"+ case (reconciliation ^. #files) Map.! "file.txt" of+ FileAutoMerge _ state _ _ -> (state ^. #appliedContent) `shouldBe` "user and generated\n" other -> expectationFailure ("expected automatic merge, got " <> show other) reconciliationSummary reconciliation `shouldBe` ReconciliationSummary 0 0 1 0 0 0 0 0 Left err -> expectationFailure (show err)@@ -216,10 +218,10 @@ let resolved = resolveFileConflict "file.txt" KeepCurrent reconciliation case resolved of Left err -> expectationFailure (show err)- Right finalPlan -> case finalPlan.files Map.! "file.txt" of+ Right finalPlan -> case (finalPlan ^. #files) Map.! "file.txt" of FileConflict _ _ _ _ _ _ (Just resolution) -> do- resolution.state.generatedBaseline `shouldBe` "generated\n"- resolution.state.appliedContent `shouldBe` "user\n"+ (resolution ^. #state . #generatedBaseline) `shouldBe` "generated\n"+ (resolution ^. #state . #appliedContent) `shouldBe` "user\n" unresolvedPaths finalPlan `shouldBe` Set.empty other -> expectationFailure ("expected resolved conflict, got " <> show other) @@ -239,9 +241,9 @@ case result of Left err -> expectationFailure (show err) Right reconciliation -> do- reconciliation.files Map.! "safe.txt" `shouldSatisfy` isSafeDelete- reconciliation.files Map.! "edited.txt" `shouldSatisfy` isEditedOrphan- reconciliation.files Map.! "shared.txt" `shouldSatisfy` isSharedRelease+ (reconciliation ^. #files) Map.! "safe.txt" `shouldSatisfy` isSafeDelete+ (reconciliation ^. #files) Map.! "edited.txt" `shouldSatisfy` isEditedOrphan+ (reconciliation ^. #files) Map.! "shared.txt" `shouldSatisfy` isSharedRelease unresolvedPaths reconciliation `shouldBe` Set.singleton "edited.txt" case resolveEditedOrphan "edited.txt" RetainTrackedOrphan reconciliation of Left err -> expectationFailure (show err)@@ -260,15 +262,15 @@ withFile :: FilePath -> FileRecord -> Manifest -> Manifest withFile path fileRecord manifest = Manifest- { version = manifest.version,- genAt = manifest.genAt,- modules = manifest.modules,- vars = manifest.vars,- files = Map.insert path fileRecord manifest.files,- applications = manifest.applications,- recipe = manifest.recipe,- blueprint = manifest.blueprint,- blueprintMigrations = manifest.blueprintMigrations+ { version = manifest ^. #version,+ genAt = manifest ^. #genAt,+ modules = manifest ^. #modules,+ vars = manifest ^. #vars,+ files = Map.insert path fileRecord (manifest ^. #files),+ applications = manifest ^. #applications,+ recipe = manifest ^. #recipe,+ blueprint = manifest ^. #blueprint,+ blueprintMigrations = manifest ^. #blueprintMigrations } record :: Text -> Maybe BaselineRef -> [ApplicationId] -> FileRecord
test/Seihou/Engine/RemoveSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.RemoveSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -33,61 +35,15 @@ mkManifest :: Bool -> [(FilePath, Text)] -> Manifest mkManifest isRemovable fileContents = (emptyManifest fixedTime)- { modules =- [ AppliedModule- { name = modName,- parentVars = emptyParentVars,- source = "/path/to/test-module",- moduleVersion = Nothing,- appliedAt = fixedTime,- removal = if isRemovable then Just (Removal [] []) else Nothing- }- ],- files =- Map.fromList- [ ( path,- FileRecord- { hash = hashContent content,- moduleName = modName,- strategy = Template,- generatedAt = fixedTime,- baseline = Nothing,- applicationIds = mempty- }- )- | (path, content) <- fileContents- ]- }+ & #modules .~ [AppliedModule {name = modName, parentVars = emptyParentVars, origin = LocalOrigin "test-module", moduleVersion = Nothing, appliedAt = fixedTime, removal = if isRemovable then Just (Removal [] []) else Nothing}]+ & #files .~ Map.fromList [(path, FileRecord {hash = hashContent content, moduleName = modName, strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty}) | (path, content) <- fileContents] -- | Helper: create a manifest with a specific removal spec. mkManifestWithRemoval :: Removal -> [(FilePath, Text)] -> Manifest mkManifestWithRemoval removal fileContents = (emptyManifest fixedTime)- { modules =- [ AppliedModule- { name = modName,- parentVars = emptyParentVars,- source = "/path/to/test-module",- moduleVersion = Nothing,- appliedAt = fixedTime,- removal = Just removal- }- ],- files =- Map.fromList- [ ( path,- FileRecord- { hash = hashContent content,- moduleName = modName,- strategy = Template,- generatedAt = fixedTime,- baseline = Nothing,- applicationIds = mempty- }- )- | (path, content) <- fileContents- ]- }+ & #modules .~ [AppliedModule {name = modName, parentVars = emptyParentVars, origin = LocalOrigin "test-module", moduleVersion = Nothing, appliedAt = fixedTime, removal = Just removal}]+ & #files .~ Map.fromList [(path, FileRecord {hash = hashContent content, moduleName = modName, strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty}) | (path, content) <- fileContents] -- | Helper: create a PureFS with files. mkFS :: [(FilePath, Text)] -> PureFS@@ -141,7 +97,7 @@ result = runPlan fs manifest modName case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.files `shouldBe` [RemovalSafe "README.md"]+ Right plan -> (plan ^. #files) `shouldBe` [RemovalSafe "README.md"] it "classifies user-modified files as RemovalConflict" $ do let manifest = mkManifest True [("README.md", "original")]@@ -149,14 +105,14 @@ result = runPlan fs manifest modName case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.files `shouldBe` [RemovalConflict "README.md"]+ Right plan -> (plan ^. #files) `shouldBe` [RemovalConflict "README.md"] it "classifies deleted files as RemovalGone" $ do let manifest = mkManifest True [("README.md", "hello")] result = runPlan emptyFS manifest modName case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.files `shouldBe` [RemovalGone "README.md"]+ Right plan -> (plan ^. #files) `shouldBe` [RemovalGone "README.md"] it "handles mix of safe, conflict, and gone files" $ do let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb"), ("c.txt", "ccc")]@@ -165,10 +121,10 @@ case result of Left err -> expectationFailure ("unexpected error: " <> show err) Right plan -> do- length plan.files `shouldBe` 3- RemovalSafe "a.txt" `elem` plan.files `shouldBe` True- RemovalConflict "b.txt" `elem` plan.files `shouldBe` True- RemovalGone "c.txt" `elem` plan.files `shouldBe` True+ length (plan ^. #files) `shouldBe` 3+ RemovalSafe "a.txt" `elem` (plan ^. #files) `shouldBe` True+ RemovalConflict "b.txt" `elem` (plan ^. #files) `shouldBe` True+ RemovalGone "c.txt" `elem` (plan ^. #files) `shouldBe` True describe "executeRemoval" $ do it "deletes safe files from the filesystem" $ do@@ -176,7 +132,7 @@ fs = mkFS [("README.md", "hello")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "README.md"]} (_, finalFS) = runExec fs manifest plan Set.empty- Map.member "README.md" finalFS.files `shouldBe` False+ Map.member "README.md" (finalFS ^. #files) `shouldBe` False it "preserves files in the keep-set" $ do let manifest = mkManifest True [("a.txt", "aaa")]@@ -184,59 +140,59 @@ plan = RemovalPlan {targetModule = modName, files = [RemovalConflict "a.txt"]} keepSet = Set.singleton "a.txt" (_, finalFS) = runExec fs manifest plan keepSet- Map.member "a.txt" finalFS.files `shouldBe` True+ Map.member "a.txt" (finalFS ^. #files) `shouldBe` True it "removes the module from manifest.modules" $ do let manifest = mkManifest True [("README.md", "hello")] fs = mkFS [("README.md", "hello")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "README.md"]} (updated, _) = runExec fs manifest plan Set.empty- updated.modules `shouldBe` []+ (updated ^. #modules) `shouldBe` [] it "removes module's files from manifest.files" $ do let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb")] fs = mkFS [("a.txt", "aaa"), ("b.txt", "bbb")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt", RemovalSafe "b.txt"]} (updated, _) = runExec fs manifest plan Set.empty- Map.null updated.files `shouldBe` True+ Map.null (updated ^. #files) `shouldBe` True it "preserves files from other modules in manifest" $ do let base = mkManifest True [("mine.txt", "mine")] otherRec = FileRecord (hashContent "other") otherMod Template fixedTime Nothing mempty manifest = Manifest- { version = base.version,- genAt = base.genAt,- modules = base.modules,- vars = base.vars,- files = Map.insert "other.txt" otherRec base.files,- applications = base.applications,- recipe = base.recipe,- blueprint = base.blueprint,- blueprintMigrations = base.blueprintMigrations+ { version = base ^. #version,+ genAt = base ^. #genAt,+ modules = base ^. #modules,+ vars = base ^. #vars,+ files = Map.insert "other.txt" otherRec (base ^. #files),+ applications = base ^. #applications,+ recipe = base ^. #recipe,+ blueprint = base ^. #blueprint,+ blueprintMigrations = base ^. #blueprintMigrations } fs = mkFS [("mine.txt", "mine"), ("other.txt", "other")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "mine.txt"]} (updated, _) = runExec fs manifest plan Set.empty- Map.member "other.txt" updated.files `shouldBe` True- Map.member "mine.txt" updated.files `shouldBe` False+ Map.member "other.txt" (updated ^. #files) `shouldBe` True+ Map.member "mine.txt" (updated ^. #files) `shouldBe` False it "updates genAt timestamp in manifest" $ do let manifest = mkManifest True [("a.txt", "aaa")] fs = mkFS [("a.txt", "aaa")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt"]} (updated, _) = runExec fs manifest plan Set.empty- updated.genAt `shouldBe` removeTime+ (updated ^. #genAt) `shouldBe` removeTime it "full round-trip: manifest returns to clean state after removal" $ do let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb")] fs = mkFS [("a.txt", "aaa"), ("b.txt", "bbb")] plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt", RemovalSafe "b.txt"]} (updated, finalFS) = runExec fs manifest plan Set.empty- updated.modules `shouldBe` []- Map.null updated.files `shouldBe` True- Map.member "a.txt" finalFS.files `shouldBe` False- Map.member "b.txt" finalFS.files `shouldBe` False+ (updated ^. #modules) `shouldBe` []+ Map.null (updated ^. #files) `shouldBe` True+ Map.member "a.txt" (finalFS ^. #files) `shouldBe` False+ Map.member "b.txt" (finalFS ^. #files) `shouldBe` False describe "buildRemovalOps" $ do it "returns ModuleNotApplied when module is not in manifest" $ do@@ -252,7 +208,7 @@ result = runBuildOps fs manifest modName removal case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.ops `shouldBe` [DeleteFileOp "README.md" RFSafe]+ Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFSafe] it "builds DeleteFileOp with RFConflict for modified files" $ do let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing] []@@ -261,7 +217,7 @@ result = runBuildOps fs manifest modName removal case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.ops `shouldBe` [DeleteFileOp "README.md" RFConflict]+ Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFConflict] it "builds DeleteFileOp with RFGone for already-deleted files" $ do let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing] []@@ -269,7 +225,7 @@ result = runBuildOps emptyFS manifest modName removal case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.ops `shouldBe` [DeleteFileOp "README.md" RFGone]+ Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFGone] it "builds StripSectionOp for remove-section steps" $ do let removal = Removal [RemovalStep RemoveSectionAction ".gitignore" Nothing] []@@ -278,7 +234,7 @@ result = runBuildOps fs manifest modName removal case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.ops `shouldBe` [StripSectionOp ".gitignore"]+ Right plan -> (plan ^. #ops) `shouldBe` [StripSectionOp ".gitignore"] it "builds RemovalCommandOp for removal commands" $ do let removal = Removal [] [Command "cabal clean" Nothing Nothing]@@ -286,7 +242,7 @@ result = runBuildOps emptyFS manifest modName removal case result of Left err -> expectationFailure ("unexpected error: " <> show err)- Right plan -> plan.ops `shouldBe` [RemovalCommandOp "cabal clean" Nothing]+ Right plan -> (plan ^. #ops) `shouldBe` [RemovalCommandOp "cabal clean" Nothing] it "rejects a removal step destination with a parent directory segment" $ do let removal = Removal [RemovalStep RemoveFileAction "../outside" Nothing] []@@ -300,7 +256,7 @@ "../outside" "path must not contain '..' segment: ../outside" )- Map.member "../outside" fs.files `shouldBe` True+ Map.member "../outside" (fs ^. #files) `shouldBe` True it "rejects a removal command workDir with a parent directory segment" $ do let removal = Removal [] [Command "echo unsafe" (Just "../outside") Nothing]@@ -325,8 +281,8 @@ case result of Left err -> expectationFailure ("unexpected error: " <> show err) Right plan -> do- length plan.ops `shouldBe` 3- case plan.ops of+ length (plan ^. #ops) `shouldBe` 3+ case plan ^. #ops of [DeleteFileOp _ _, StripSectionOp _, RemovalCommandOp _ _] -> pure () other -> expectationFailure ("unexpected ops: " <> show other) @@ -337,7 +293,7 @@ fs = mkFS [("README.md", "hello")] plan = ExecutedRemovalPlan modName [DeleteFileOp "README.md" RFSafe] (_, finalFS) = runExecOps fs manifest plan Set.empty- Map.member "README.md" finalFS.files `shouldBe` False+ Map.member "README.md" (finalFS ^. #files) `shouldBe` False it "preserves files in keep-set for DeleteFileOp" $ do let removal = Removal [RemovalStep RemoveFileAction "a.txt" Nothing] []@@ -346,13 +302,13 @@ plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFConflict] keepSet = Set.singleton "a.txt" (_, finalFS) = runExecOps fs manifest plan keepSet- Map.member "a.txt" finalFS.files `shouldBe` True+ Map.member "a.txt" (finalFS ^. #files) `shouldBe` True it "skips gone files" $ do let manifest = mkManifest True [("a.txt", "aaa")] plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFGone] (updated, _) = runExecOps emptyFS manifest plan Set.empty- updated.modules `shouldBe` []+ (updated ^. #modules) `shouldBe` [] it "strips section from file with StripSectionOp" $ do let content = "before\n# --- seihou:test-module ---\nmodule content\n# --- /seihou:test-module ---\nafter\n"@@ -360,7 +316,7 @@ fs = mkFS [(".gitignore", content)] plan = ExecutedRemovalPlan modName [StripSectionOp ".gitignore"] (_, finalFS) = runExecOps fs manifest plan Set.empty- case Map.lookup ".gitignore" finalFS.files of+ case Map.lookup ".gitignore" (finalFS ^. #files) of Nothing -> expectationFailure ".gitignore should still exist" Just result -> do result `shouldSatisfy` \t ->@@ -372,7 +328,7 @@ fs = mkFS [("file.txt", content)] plan = ExecutedRemovalPlan modName [StripSectionOp "file.txt"] (_, finalFS) = runExecOps fs manifest plan Set.empty- Map.lookup "file.txt" finalFS.files `shouldBe` Just content+ Map.lookup "file.txt" (finalFS ^. #files) `shouldBe` Just content it "removes module from manifest after all steps" $ do let removal = Removal [RemovalStep RemoveFileAction "a.txt" Nothing] []@@ -380,26 +336,26 @@ fs = mkFS [("a.txt", "aaa")] plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFSafe] (updated, _) = runExecOps fs manifest plan Set.empty- updated.modules `shouldBe` []- Map.null updated.files `shouldBe` True+ (updated ^. #modules) `shouldBe` []+ Map.null (updated ^. #files) `shouldBe` True it "preserves other modules' files in manifest" $ do let base = mkManifest True [("mine.txt", "mine")] otherRec = FileRecord (hashContent "other") otherMod Template fixedTime Nothing mempty manifest = Manifest- { version = base.version,- genAt = base.genAt,- modules = base.modules,- vars = base.vars,- files = Map.insert "other.txt" otherRec base.files,- applications = base.applications,- recipe = base.recipe,- blueprint = base.blueprint,- blueprintMigrations = base.blueprintMigrations+ { version = base ^. #version,+ genAt = base ^. #genAt,+ modules = base ^. #modules,+ vars = base ^. #vars,+ files = Map.insert "other.txt" otherRec (base ^. #files),+ applications = base ^. #applications,+ recipe = base ^. #recipe,+ blueprint = base ^. #blueprint,+ blueprintMigrations = base ^. #blueprintMigrations } fs = mkFS [("mine.txt", "mine"), ("other.txt", "other")] plan = ExecutedRemovalPlan modName [DeleteFileOp "mine.txt" RFSafe] (updated, _) = runExecOps fs manifest plan Set.empty- Map.member "other.txt" updated.files `shouldBe` True- Map.member "mine.txt" updated.files `shouldBe` False+ Map.member "other.txt" (updated ^. #files) `shouldBe` True+ Map.member "mine.txt" (updated ^. #files) `shouldBe` False
test/Seihou/Engine/SectionSpec.hs view
@@ -1,5 +1,6 @@ module Seihou.Engine.SectionSpec (tests) where +import Control.Lens ((&), (.~)) import Data.Text qualified as T import Seihou.Core.Types import Seihou.Engine.Section@@ -14,7 +15,7 @@ modName = ModuleName "nix-flake" marker :: SectionMarker-marker = SectionMarker {sectionPrefix = "#", sectionModule = modName}+marker = SectionMarker {prefix = "#", module_ = modName} spec :: Spec spec = do@@ -23,7 +24,7 @@ renderSectionOpen marker `shouldBe` "# --- seihou:nix-flake ---\n" it "produces correct format with -- prefix" $ do- let hsMarker = marker {sectionPrefix = "--"}+ let hsMarker = (marker & #prefix .~ "--") renderSectionOpen hsMarker `shouldBe` "-- --- seihou:nix-flake ---\n" describe "renderSectionClose" $ do@@ -31,7 +32,7 @@ renderSectionClose marker `shouldBe` "# --- /seihou:nix-flake ---\n" it "produces correct format with -- prefix" $ do- let hsMarker = marker {sectionPrefix = "--"}+ let hsMarker = (marker & #prefix .~ "--") renderSectionClose hsMarker `shouldBe` "-- --- /seihou:nix-flake ---\n" describe "wrapInSection" $ do
test/Seihou/Engine/UpdateTransactionSpec.hs view
@@ -1,9 +1,11 @@ module Seihou.Engine.UpdateTransactionSpec (tests) where import Control.Exception (throwIO)+import Control.Lens ((^.)) import Control.Monad (unless, when) import Data.ByteString.Lazy qualified as LBS import Data.Foldable (traverse_)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -73,7 +75,7 @@ ], requiredDirectories = Set.empty }- transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet plan.files)+ transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet (plan ^. #files)) candidate <- expectRight =<< applyReconciliation transaction plan manifest readProject projectRoot "merged.txt" `shouldReturn` "user and generated\n"@@ -81,19 +83,19 @@ readProject projectRoot "edited.txt" `shouldReturn` "user orphan\n" readProject projectRoot "shared.txt" `shouldReturn` "shared\n" - let mergedRecord = candidate.files Map.! "merged.txt"+ let mergedRecord = (candidate ^. #files) Map.! "merged.txt" baseline = baselineRefForContent "generated\n"- mergedRecord.hash `shouldBe` hashContent "user and generated\n"- mergedRecord.baseline `shouldBe` Just baseline- mergedRecord.applicationIds `shouldBe` Set.singleton appA- Map.member "safe.txt" candidate.files `shouldBe` False- candidate.files Map.! "edited.txt" `shouldBe` oldEdited- (candidate.files Map.! "shared.txt").applicationIds `shouldBe` Set.singleton appB+ (mergedRecord ^. #hash) `shouldBe` hashContent "user and generated\n"+ (mergedRecord ^. #baseline) `shouldBe` Just baseline+ (mergedRecord ^. #applicationIds) `shouldBe` Set.singleton appA+ Map.member "safe.txt" (candidate ^. #files) `shouldBe` False+ (candidate ^. #files) Map.! "edited.txt" `shouldBe` oldEdited+ (((candidate ^. #files) Map.! "shared.txt") ^. #applicationIds) `shouldBe` Set.singleton appB readProject projectRoot (".seihou/baselines" </> refName baseline) `shouldReturn` "generated\n" - Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` True+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` True completeUpdateTransaction transaction `shouldReturn` Right ()- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "advances the baseline but preserves disk and applied hash for KeepCurrent" $ withSystemTempDirectory "seihou-update-keep-current" $ \projectRoot -> do@@ -115,9 +117,9 @@ transaction <- expectRight =<< beginUpdateTransaction projectRoot (Set.singleton "file.txt") candidate <- expectRight =<< applyReconciliation transaction resolvedPlan manifest readProject projectRoot "file.txt" `shouldReturn` "user\n"- let resultRecord = candidate.files Map.! "file.txt"- resultRecord.hash `shouldBe` hashContent "user\n"- resultRecord.baseline `shouldBe` Just (baselineRefForContent "generated\n")+ let resultRecord = (candidate ^. #files) Map.! "file.txt"+ (resultRecord ^. #hash) `shouldBe` hashContent "user\n"+ (resultRecord ^. #baseline) `shouldBe` Just (baselineRefForContent "generated\n") completeUpdateTransaction transaction `shouldReturn` Right () it "rejects a stale plan before its first mutation" $@@ -133,7 +135,7 @@ result <- applyReconciliation transaction plan (manifestWithFiles Map.empty) result `shouldSatisfy` isStale readProject projectRoot "file.txt" `shouldReturn` "planned\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "deletes or detaches edited orphans only after explicit resolution" $ withSystemTempDirectory "seihou-update-orphan-resolution" $ \projectRoot -> do@@ -165,11 +167,11 @@ ) Set.empty manifest = manifestWithFiles (Map.fromList [("delete.txt", deleteRecord), ("detach.txt", detachRecord)])- transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet plan.files)+ transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet (plan ^. #files)) candidate <- expectRight =<< applyReconciliation transaction plan manifest Directory.doesFileExist (projectRoot </> "delete.txt") `shouldReturn` False readProject projectRoot "detach.txt" `shouldReturn` "user detach\n"- candidate.files `shouldBe` Map.empty+ (candidate ^. #files) `shouldBe` Map.empty completeUpdateTransaction transaction `shouldReturn` Right () it "refuses unresolved plans without touching disk" $@@ -183,7 +185,7 @@ result <- applyReconciliation transaction plan (manifestWithFiles Map.empty) result `shouldSatisfy` isUnresolved readProject projectRoot "file.txt" `shouldReturn` "user\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False describe "rollback and recovery" $ do it "rolls every earlier mutation back after an injected failure" $@@ -201,7 +203,7 @@ ] ) Set.empty- transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet plan.files)+ transaction <- expectRight =<< beginUpdateTransaction projectRoot (Map.keysSet (plan ^. #files)) result <- applyReconciliationWithHook (\count -> when (count == 1) (throwIO (userError "injected failure")))@@ -211,7 +213,7 @@ result `shouldSatisfy` isApplyFailure readProject projectRoot "one.txt" `shouldReturn` "old one\n" readProject projectRoot "two.txt" `shouldReturn` "old two\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "restores a well-formed leftover journal on startup" $ withSystemTempDirectory "seihou-update-recover" $ \projectRoot -> do@@ -220,7 +222,7 @@ writeProject projectRoot "file.txt" "interrupted\n" recoverIncompleteTransactions projectRoot `shouldReturn` [Right ()] readProject projectRoot "file.txt" `shouldReturn` "old\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "recovers an applied but unpublished candidate and removes its new empty directories" $ withSystemTempDirectory "seihou-update-unpublished" $ \projectRoot -> do@@ -238,7 +240,7 @@ recoverIncompleteTransactions projectRoot `shouldReturn` [Right ()] readProject projectRoot "file.txt" `shouldReturn` "old\n" Directory.doesDirectoryExist (projectRoot </> "empty") `shouldReturn` False- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "keeps committed files when the durable manifest matches the journal" $ withSystemTempDirectory "seihou-update-committed" $ \projectRoot -> do@@ -255,7 +257,7 @@ LBS.writeFile (projectRoot </> ".seihou" </> "manifest.json") (manifestToJSON candidate) recoverIncompleteTransactions projectRoot `shouldReturn` [Right ()] readProject projectRoot "file.txt" `shouldReturn` "new\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "uses an orchestrator's complete final manifest as the recovery commit marker" $ withSystemTempDirectory "seihou-update-final-marker" $ \projectRoot -> do@@ -271,22 +273,22 @@ let finalManifest :: Manifest finalManifest = Manifest- { version = candidate.version,- genAt = candidate.genAt,- modules = candidate.modules,+ { version = candidate ^. #version,+ genAt = candidate ^. #genAt,+ modules = candidate ^. #modules, vars = Map.singleton "published" "yes",- files = candidate.files,- applications = candidate.applications,- recipe = candidate.recipe,- blueprint = candidate.blueprint,- blueprintMigrations = candidate.blueprintMigrations+ files = candidate ^. #files,+ applications = candidate ^. #applications,+ recipe = candidate ^. #recipe,+ blueprint = candidate ^. #blueprint,+ blueprintMigrations = candidate ^. #blueprintMigrations } setUpdateTransactionExpectedManifest transaction finalManifest `shouldReturn` Right () Directory.createDirectoryIfMissing True (projectRoot </> ".seihou") LBS.writeFile (projectRoot </> ".seihou" </> "manifest.json") (manifestToJSON finalManifest) recoverIncompleteTransactions projectRoot `shouldReturn` [Right ()] readProject projectRoot "file.txt" `shouldReturn` "new\n"- Directory.doesDirectoryExist transaction.transactionDirectory `shouldReturn` False+ Directory.doesDirectoryExist (transaction ^. #transactionDirectory) `shouldReturn` False it "quarantines malformed journal metadata instead of deleting it" $ withSystemTempDirectory "seihou-update-malformed" $ \projectRoot -> do@@ -352,8 +354,8 @@ merged `shouldSatisfy` T.isInfixOf "module" Directory.doesFileExist (projectRoot </> "safe.txt") `shouldReturn` False readProject projectRoot "edited.txt" `shouldReturn` "user orphan\n"- Map.member "safe.txt" candidate.files `shouldBe` False- Map.member "edited.txt" candidate.files `shouldBe` True+ Map.member "safe.txt" (candidate ^. #files) `shouldBe` False+ Map.member "edited.txt" (candidate ^. #files) `shouldBe` True Directory.listDirectory (projectRoot </> ".seihou" </> "transactions") `shouldReturn` [] fixedTime :: UTCTime@@ -367,15 +369,15 @@ manifestWithFiles fileRecords = let manifest = emptyManifest fixedTime in Manifest- { version = manifest.version,- genAt = manifest.genAt,- modules = manifest.modules,- vars = manifest.vars,+ { version = manifest ^. #version,+ genAt = manifest ^. #genAt,+ modules = manifest ^. #modules,+ vars = manifest ^. #vars, files = fileRecords,- applications = manifest.applications,- recipe = manifest.recipe,- blueprint = manifest.blueprint,- blueprintMigrations = manifest.blueprintMigrations+ applications = manifest ^. #applications,+ recipe = manifest ^. #recipe,+ blueprint = manifest ^. #blueprint,+ blueprintMigrations = manifest ^. #blueprintMigrations } fileRecord :: Text -> Maybe BaselineRef -> [ApplicationId] -> FileRecord@@ -420,7 +422,7 @@ readProject projectRoot relativePath = TIO.readFile (projectRoot </> relativePath) refName :: BaselineRef -> FilePath-refName reference = T.unpack reference.unBaselineRef.unSHA256+refName reference = T.unpack (reference ^. #unBaselineRef . #unSHA256) expectRight :: (Show error) => Either error value -> IO value expectRight (Right value) = pure value
test/Seihou/Engine/ValidateSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Engine.ValidateSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Text qualified as T import Seihou.Core.Types import Seihou.Engine.Validate@@ -69,31 +71,31 @@ -- | Helper to update Module fields without ambiguity. withVars :: [VarDecl] -> Module -> Module-withVars v m = Module m.name m.version m.description v m.exports m.prompts m.steps m.commands m.dependencies m.removal m.migrations+withVars v m = Module (m ^. #name) (m ^. #version) (m ^. #description) v (m ^. #exports) (m ^. #prompts) (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withSteps :: [Step] -> Module -> Module-withSteps s m = Module m.name m.version m.description m.vars m.exports m.prompts s m.commands m.dependencies m.removal m.migrations+withSteps s m = Module (m ^. #name) (m ^. #version) (m ^. #description) (m ^. #vars) (m ^. #exports) (m ^. #prompts) s (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withPrompts :: [Prompt] -> Module -> Module-withPrompts p m = Module m.name m.version m.description m.vars m.exports p m.steps m.commands m.dependencies m.removal m.migrations+withPrompts p m = Module (m ^. #name) (m ^. #version) (m ^. #description) (m ^. #vars) (m ^. #exports) p (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withCommands :: [Command] -> Module -> Module-withCommands c m = Module m.name m.version m.description m.vars m.exports m.prompts m.steps c m.dependencies m.removal m.migrations+withCommands c m = Module (m ^. #name) (m ^. #version) (m ^. #description) (m ^. #vars) (m ^. #exports) (m ^. #prompts) (m ^. #steps) c (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) withVarsAndPrompts :: [VarDecl] -> [Prompt] -> Module -> Module-withVarsAndPrompts v p m = Module m.name m.version m.description v m.exports p m.steps m.commands m.dependencies m.removal m.migrations+withVarsAndPrompts v p m = Module (m ^. #name) (m ^. #version) (m ^. #description) v (m ^. #exports) p (m ^. #steps) (m ^. #commands) (m ^. #dependencies) (m ^. #removal) (m ^. #migrations) -- | Helper: check if any DiagCheck has the given label and non-empty details. hasFailedCheck :: T.Text -> [DiagCheck] -> Bool-hasFailedCheck label = any (\c -> c.diagLabel == label && not (null (c.diagDetails)))+hasFailedCheck label = any (\c -> c ^. #label == label && not (null (c ^. #details))) -- | Helper: check if any DiagCheck has the given label and empty details (pass). hasPassedCheck :: T.Text -> [DiagCheck] -> Bool-hasPassedCheck label = any (\c -> c.diagLabel == label && null (c.diagDetails))+hasPassedCheck label = any (\c -> c ^. #label == label && null (c ^. #details)) -- | Helper: count checks with non-empty details of a given severity. countFailures :: DiagSeverity -> [DiagCheck] -> Int-countFailures sev = length . filter (\c -> c.diagSeverity == sev && not (null (c.diagDetails)))+countFailures sev = length . filter (\c -> c ^. #severity == sev && not (null (c ^. #details))) spec :: Spec spec = do@@ -103,64 +105,64 @@ createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport False tmpDir goodModule- report.reportDhallOk `shouldBe` True+ (report ^. #dhallOk) `shouldBe` True reportHasErrors report `shouldBe` False- countFailures DiagError report.reportChecks `shouldBe` 0+ countFailures DiagError (report ^. #checks) `shouldBe` 0 it "detects module name format errors" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Module name format" report.reportChecks `shouldBe` True+ hasFailedCheck "Module name format" (report ^. #checks) `shouldBe` True it "detects duplicate variable names" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Unique variable names" report.reportChecks `shouldBe` True+ hasFailedCheck "Unique variable names" (report ^. #checks) `shouldBe` True it "detects export referencing undeclared variable" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Export references" report.reportChecks `shouldBe` True+ hasFailedCheck "Export references" (report ^. #checks) `shouldBe` True it "detects prompt referencing undeclared variable" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Prompt references" report.reportChecks `shouldBe` True+ hasFailedCheck "Prompt references" (report ^. #checks) `shouldBe` True it "detects missing source files" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Source file existence" report.reportChecks `shouldBe` True+ hasFailedCheck "Source file existence" (report ^. #checks) `shouldBe` True it "detects unsafe step destinations" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Safe step destinations" report.reportChecks `shouldBe` True+ hasFailedCheck "Safe step destinations" (report ^. #checks) `shouldBe` True it "detects missing module version" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule- hasFailedCheck "Module version declared" report.reportChecks `shouldBe` True+ hasFailedCheck "Module version declared" (report ^. #checks) `shouldBe` True it "passes when module has a version" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport False tmpDir goodModule- hasPassedCheck "Module version declared" report.reportChecks `shouldBe` True+ hasPassedCheck "Module version declared" (report ^. #checks) `shouldBe` True it "reports multiple errors at once" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do report <- buildReport False tmpDir badModule reportHasErrors report `shouldBe` True- countFailures DiagError report.reportChecks `shouldSatisfy` (>= 5)+ countFailures DiagError (report ^. #checks) `shouldSatisfy` (>= 5) it "does not include lint checks when lint is False" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport False tmpDir goodModule- let hasWarning = any (\c -> c.diagSeverity == DiagWarning) report.reportChecks+ let hasWarning = any (\c -> c ^. #severity == DiagWarning) (report ^. #checks) hasWarning `shouldBe` False it "includes lint checks when lint is True" $ do@@ -168,7 +170,7 @@ createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport True tmpDir goodModule- let hasWarning = any (\c -> c.diagSeverity == DiagWarning) report.reportChecks+ let hasWarning = any (\c -> c ^. #severity == DiagWarning) (report ^. #checks) hasWarning `shouldBe` True describe "lint checks" $ do@@ -183,8 +185,8 @@ ] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Unused variables" report.reportChecks `shouldBe` True- let details = concatMap (.diagDetails) $ filter (\c -> c.diagLabel == "Unused variables") report.reportChecks+ hasFailedCheck "Unused variables" (report ^. #checks) `shouldBe` True+ let details = concatMap (^. #details) $ filter (\c -> c ^. #label == "Unused variables") (report ^. #checks) any (T.isInfixOf "unused.var") details `shouldBe` True it "does not flag used variables as unused" $ do@@ -192,7 +194,7 @@ createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport True tmpDir goodModule- hasFailedCheck "Unused variables" report.reportChecks `shouldBe` False+ hasFailedCheck "Unused variables" (report ^. #checks) `shouldBe` False it "detects required variables without prompts" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -204,14 +206,14 @@ [] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Required variables without prompts" report.reportChecks `shouldBe` True+ hasFailedCheck "Required variables without prompts" (report ^. #checks) `shouldBe` True it "does not flag required variables that have prompts" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport True tmpDir goodModule- hasFailedCheck "Required variables without prompts" report.reportChecks `shouldBe` False+ hasFailedCheck "Required variables without prompts" (report ^. #checks) `shouldBe` False it "detects duplicate step destinations" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -225,7 +227,7 @@ ] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Duplicate step destinations" report.reportChecks `shouldBe` True+ hasFailedCheck "Duplicate step destinations" (report ^. #checks) `shouldBe` True it "does not flag patch ops as duplicate destinations" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -239,7 +241,7 @@ ] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Duplicate step destinations" report.reportChecks `shouldBe` False+ hasFailedCheck "Duplicate step destinations" (report ^. #checks) `shouldBe` False it "detects empty choice lists" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -250,7 +252,7 @@ [VarDecl "pick" (VTChoice []) Nothing (Just "Pick") True Nothing] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Empty choice lists" report.reportChecks `shouldBe` True+ hasFailedCheck "Empty choice lists" (report ^. #checks) `shouldBe` True it "detects missing variable descriptions" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -261,14 +263,14 @@ [VarDecl "project.name" VTText Nothing Nothing True Nothing] goodModule report <- buildReport True tmpDir m- hasFailedCheck "Missing variable descriptions" report.reportChecks `shouldBe` True+ hasFailedCheck "Missing variable descriptions" (report ^. #checks) `shouldBe` True it "does not flag variables with descriptions" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do createDirectoryIfMissing True (tmpDir </> "files") writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" report <- buildReport True tmpDir goodModule- hasFailedCheck "Missing variable descriptions" report.reportChecks `shouldBe` False+ hasFailedCheck "Missing variable descriptions" (report ^. #checks) `shouldBe` False describe "conditional lint" $ do -- Base vars: keep project.name (referenced by goodModule's export/prompt)@@ -288,11 +290,11 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = stepWithCondition (Just (ExprEq "feature.on" (VText "true"))) report <- buildReport True tmpDir m- hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` True+ hasFailedCheck "Conditional comparison types" (report ^. #checks) `shouldBe` True reportHasErrors report `shouldBe` True let details =- concatMap (.diagDetails) $- filter (\c -> c.diagLabel == "Conditional comparison types") report.reportChecks+ concatMap (^. #details) $+ filter (\c -> c ^. #label == "Conditional comparison types") (report ^. #checks) any (T.isInfixOf "feature.on") details `shouldBe` True any (T.isInfixOf "bareword true") details `shouldBe` True @@ -302,8 +304,8 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = stepWithCondition (Just (ExprEq "feature.on" (VBool True))) report <- buildReport True tmpDir m- hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` False- hasPassedCheck "Conditional comparison types" report.reportChecks `shouldBe` True+ hasFailedCheck "Conditional comparison types" (report ^. #checks) `shouldBe` False+ hasPassedCheck "Conditional comparison types" (report ^. #checks) `shouldBe` True it "flags a when clause referencing an undeclared variable" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -311,10 +313,10 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = stepWithCondition (Just (ExprIsSet "nix.treefmtt")) report <- buildReport True tmpDir m- hasFailedCheck "Conditional variable references" report.reportChecks `shouldBe` True+ hasFailedCheck "Conditional variable references" (report ^. #checks) `shouldBe` True let details =- concatMap (.diagDetails) $- filter (\c -> c.diagLabel == "Conditional variable references") report.reportChecks+ concatMap (^. #details) $+ filter (\c -> c ^. #label == "Conditional variable references") (report ^. #checks) any (T.isInfixOf "nix.treefmtt") details `shouldBe` True it "passes both conditional checks for a correct module" $ do@@ -323,8 +325,8 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = stepWithCondition (Just (ExprEq "feature.on" (VBool True))) report <- buildReport True tmpDir m- hasPassedCheck "Conditional variable references" report.reportChecks `shouldBe` True- hasPassedCheck "Conditional comparison types" report.reportChecks `shouldBe` True+ hasPassedCheck "Conditional variable references" (report ^. #checks) `shouldBe` True+ hasPassedCheck "Conditional comparison types" (report ^. #checks) `shouldBe` True it "does not run conditional checks when lint is False" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -332,7 +334,7 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = stepWithCondition (Just (ExprEq "feature.on" (VText "true"))) report <- buildReport False tmpDir m- any (\c -> c.diagLabel == "Conditional comparison types") report.reportChecks+ any (\c -> c ^. #label == "Conditional comparison types") (report ^. #checks) `shouldBe` False reportHasErrors report `shouldBe` False @@ -344,10 +346,10 @@ "{{#if Eq ghost true}}\nhi\n{{/if}}\n" let m = withVars baseVars goodModule report <- buildReport True tmpDir m- hasFailedCheck "Conditional variable references" report.reportChecks `shouldBe` True+ hasFailedCheck "Conditional variable references" (report ^. #checks) `shouldBe` True let details =- concatMap (.diagDetails) $- filter (\c -> c.diagLabel == "Conditional variable references") report.reportChecks+ concatMap (^. #details) $+ filter (\c -> c ^. #label == "Conditional variable references") (report ^. #checks) any (T.isInfixOf "ghost") details `shouldBe` True any (T.isInfixOf "README.md.tpl") details `shouldBe` True @@ -359,7 +361,7 @@ "{{#if Eq feature.on \"true\"}}\nhi\n{{/if}}\n" let m = withVars baseVars goodModule report <- buildReport True tmpDir m- hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` True+ hasFailedCheck "Conditional comparison types" (report ^. #checks) `shouldBe` True describe "renderReportPlain" $ do it "renders a valid module report with check marks" $ do@@ -382,11 +384,11 @@ it "renders a Dhall-failure report" $ do let report = ValidateReport- { reportModule = goodModule,- reportPath = "/some/path",- reportDhallOk = False,- reportDhallError = Just "test error message",- reportChecks = []+ { module_ = goodModule,+ path = "/some/path",+ dhallOk = False,+ dhallError = Just "test error message",+ checks = [] } rendered = renderReportPlain report T.isInfixOf "\x2717 module.dhall failed to evaluate" rendered `shouldBe` True@@ -396,11 +398,11 @@ it "renders a Dhall-failure report without error details when absent" $ do let report = ValidateReport- { reportModule = goodModule,- reportPath = "/some/path",- reportDhallOk = False,- reportDhallError = Nothing,- reportChecks = []+ { module_ = goodModule,+ path = "/some/path",+ dhallOk = False,+ dhallError = Nothing,+ checks = [] } rendered = renderReportPlain report T.isInfixOf "\x2717 module.dhall failed to evaluate" rendered `shouldBe` True@@ -449,11 +451,11 @@ it "returns True when Dhall failed" $ do let report = ValidateReport- { reportModule = goodModule,- reportPath = "/some/path",- reportDhallOk = False,- reportDhallError = Just "some dhall error",- reportChecks = []+ { module_ = goodModule,+ path = "/some/path",+ dhallOk = False,+ dhallError = Just "some dhall error",+ checks = [] } reportHasErrors report `shouldBe` True @@ -464,7 +466,7 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = withCommands [Command "echo hello" Nothing Nothing] goodModule report <- buildReport False tmpDir m- hasPassedCheck "Command safety" report.reportChecks `shouldBe` True+ hasPassedCheck "Command safety" (report ^. #checks) `shouldBe` True it "fails for empty command text" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -472,7 +474,7 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = withCommands [Command " " Nothing Nothing] goodModule report <- buildReport False tmpDir m- hasFailedCheck "Command safety" report.reportChecks `shouldBe` True+ hasFailedCheck "Command safety" (report ^. #checks) `shouldBe` True it "fails for absolute workDir" $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -480,7 +482,7 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = withCommands [Command "echo hi" (Just "/usr/local") Nothing] goodModule report <- buildReport False tmpDir m- hasFailedCheck "Command safety" report.reportChecks `shouldBe` True+ hasFailedCheck "Command safety" (report ^. #checks) `shouldBe` True it "fails for workDir containing .." $ do withSystemTempDirectory "seihou-validate" $ \tmpDir -> do@@ -488,4 +490,4 @@ writeFile (tmpDir </> "files" </> "README.md.tpl") "stub" let m = withCommands [Command "echo hi" (Just "../escape") Nothing] goodModule report <- buildReport False tmpDir m- hasFailedCheck "Command safety" report.reportChecks `shouldBe` True+ hasFailedCheck "Command safety" (report ^. #checks) `shouldBe` True
test/Seihou/Evaluation/ConditionalTemplateSpec.hs view
@@ -1,5 +1,6 @@ module Seihou.Evaluation.ConditionalTemplateSpec (tests) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text.IO qualified as TIO@@ -59,12 +60,12 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- splitFlakeBaseline "flake.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected it "with nix.postgresql = true, emits bytes identical to the postgres split-flake baseline" $ do base <- fixtureDir@@ -74,9 +75,9 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- splitFlakeBaseline "flake-with-postgres.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected
test/Seihou/Evaluation/DhallTextFlakeSpec.hs view
@@ -1,5 +1,6 @@ module Seihou.Evaluation.DhallTextFlakeSpec (tests) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text.IO qualified as TIO@@ -56,12 +57,12 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- renderSplitFlake "flake.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected it "with nix.postgresql = true, produces bytes identical to the postgres baseline" $ do base <- fixtureDir@@ -71,9 +72,9 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- renderSplitFlake "flake-with-postgres.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected
test/Seihou/Evaluation/SplitFlakeSpec.hs view
@@ -1,5 +1,6 @@ module Seihou.Evaluation.SplitFlakeSpec (tests) where +import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text.IO qualified as TIO@@ -50,12 +51,12 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- renderFixtureFile "flake.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected it "with nix.postgresql = true, emits the postgres flake verbatim" $ do base <- fixtureDir@@ -65,9 +66,9 @@ case planResult of Left errs -> expectationFailure ("compilePlan failed: " <> show errs) Right ops -> do- let writeOps = [op | op@WriteFileOp {} <- ops]+ let writeOps = [(dest, content) | WriteFileOp {dest, content} <- ops] writeOps `shouldSatisfy` (\xs -> length xs == 1)- let op = writeOps !! 0- op.dest `shouldBe` "flake.nix"+ let (dest, content) = writeOps !! 0+ dest `shouldBe` "flake.nix" expected <- renderFixtureFile "flake-with-postgres.nix.tpl" vars- op.content `shouldBe` expected+ content `shouldBe` expected
test/Seihou/Integration/CompositionSpec.hs view
@@ -1,6 +1,8 @@ module Seihou.Integration.CompositionSpec (tests) where +import Control.Lens ((^.)) import Data.Either (isLeft)+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T@@ -28,7 +30,7 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right modules -> do- let names = map (\(_, m, _) -> m.name) modules+ let names = map (\(_, m, _) -> m ^. #name) modules length names `shouldBe` 4 -- All four modules should be present elem "nix-base" names `shouldBe` True@@ -41,7 +43,7 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right modules -> do- let names = map (\(_, m, _) -> m.name) modules+ let names = map (\(_, m, _) -> m ^. #name) modules indexOf n = case lookup n (zip names [0 :: Int ..]) of Just i -> i Nothing -> error $ "Module not found: " ++ show n@@ -61,7 +63,7 @@ Right modules -> do length modules `shouldBe` 1 case modules of- [(_, m, _)] -> m.name `shouldBe` "nix-base"+ [(_, m, _)] -> (m ^. #name) `shouldBe` "nix-base" _ -> expectationFailure "Expected exactly one module" it "handles additional modules via --module flag" $ do@@ -69,7 +71,7 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right modules -> do- let names = map (\(_, m, _) -> m.name) modules+ let names = map (\(_, m, _) -> m ^. #name) modules length names `shouldBe` 2 elem "haskell-base" names `shouldBe` True elem "nix-base" names `shouldBe` True@@ -89,9 +91,9 @@ Right resolved -> do let flakeVars = resolved Map.! primaryInstance "nix-flake" -- nix-flake should see nix.system from nix-base's export- (.value) (flakeVars Map.! "nix.system") `shouldBe` VText "x86_64-linux"+ (^. #value) (flakeVars Map.! "nix.system") `shouldBe` VText "x86_64-linux" -- nix-flake should also have its own variable- (.value) (flakeVars Map.! "nix.description") `shouldBe` VText "A Nix project"+ (^. #value) (flakeVars Map.! "nix.description") `shouldBe` VText "A Nix project" it "flows exports through diamond dependency" $ do result <- loadComposition [fixtureDir] "haskell-with-nix" []@@ -104,11 +106,11 @@ Right resolved -> do -- haskell-base should have project.name from CLI let baseVars = resolved Map.! primaryInstance "haskell-base"- (.value) (baseVars Map.! "project.name") `shouldBe` VText "my-app"+ (^. #value) (baseVars Map.! "project.name") `shouldBe` VText "my-app" -- haskell-with-nix should inherit project.name via haskell-base's export let topVars = resolved Map.! primaryInstance "haskell-with-nix" Map.member "project.name" topVars `shouldBe` True- (.value) (topVars Map.! "project.name") `shouldBe` VText "my-app"+ (^. #value) (topVars Map.! "project.name") `shouldBe` VText "my-app" describe "compileComposedPlan" $ do it "produces operations from all composed modules" $ do@@ -121,7 +123,7 @@ Left errs -> expectationFailure $ "Resolve failed: " ++ show errs Right resolved -> do let quads =- [ (inst, m, dir, Map.map (.value) (resolved Map.! inst))+ [ (inst, m, dir, Map.map (^. #value) (resolved Map.! inst)) | (inst, m, dir) <- modules ] planResult <- compileComposedPlan quads@@ -147,7 +149,7 @@ Left errs -> expectationFailure $ "Resolve failed: " ++ show errs Right resolved -> do let quads =- [ (inst, m, dir, Map.map (.value) (resolved Map.! inst))+ [ (inst, m, dir, Map.map (^. #value) (resolved Map.! inst)) | (inst, m, dir) <- modules ] planResult <- compileComposedPlan quads@@ -176,7 +178,7 @@ Left errs -> expectationFailure $ "Resolve failed: " ++ show errs Right resolved -> do let quads =- [ (inst, m, dir, Map.map (.value) (resolved Map.! inst))+ [ (inst, m, dir, Map.map (^. #value) (resolved Map.! inst)) | (inst, m, dir) <- modules ] planResult <- compileComposedPlan quads@@ -212,7 +214,7 @@ let helperInstances = [ inst | (inst, m, _) <- modules,- m.name == "multi-instance-helper"+ m ^. #name == "multi-instance-helper" ] length helperInstances `shouldBe` 2 let bindings =@@ -223,7 +225,7 @@ haveSkill vn = any ( \inst ->- Map.lookup "skill.name" inst.instanceParentVars.unParentVars == Just vn+ Map.lookup "skill.name" (inst ^. #parentVars . #unParentVars) == Just vn ) helperInstances all haveSkill (Map.keys bindings) `shouldBe` True@@ -237,7 +239,7 @@ Left errs -> expectationFailure $ "Resolve failed: " ++ show errs Right resolved -> do let quads =- [ (inst, m, dir, Map.map (.value) (resolved Map.! inst))+ [ (inst, m, dir, Map.map (^. #value) (resolved Map.! inst)) | (inst, m, dir) <- modules ] planResult <- compileComposedPlan quads@@ -267,7 +269,7 @@ a = mkMod "a" ["b"] b = mkMod "b" ["c"] c = mkMod "c" ["a"]- graph = buildGraph [(primaryInstance m.name, m) | m <- [a, b, c]]+ graph = buildGraph [(primaryInstance (m ^. #name), m) | m <- [a, b, c]] topoSort graph `shouldSatisfy` isLeft isContentMerged :: CompositionWarning -> Bool
test/Seihou/Integration/ExecutionSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Integration.ExecutionSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -33,7 +35,7 @@ -- | Helper to extract the resolved variable values map. resolvedValues :: Map.Map VarName ResolvedVar -> Map.Map VarName VarValue-resolvedValues = Map.map (.value)+resolvedValues = Map.map (^. #value) -- | Load haskell-base fixture, resolve vars, compile plan. compileFixturePlan :: [(Text, Text)] -> IO (Module, [Operation])@@ -45,7 +47,7 @@ Right modul -> do let cli = Map.fromList [(VarName k, v) | (k, v) <- vars] env = Map.empty- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> error ("Failed to resolve: " <> show errs) Right resolved -> do planResult <- compilePlan (fixtures </> "haskell-base") modul (resolvedValues resolved)@@ -58,15 +60,15 @@ manifestWithFiles t recs = let base = emptyManifest t in Manifest- { version = base.version,- genAt = base.genAt,- modules = base.modules,- vars = base.vars,+ { version = base ^. #version,+ genAt = base ^. #genAt,+ modules = base ^. #modules,+ vars = base ^. #vars, files = recs,- applications = base.applications,- recipe = base.recipe,- blueprint = base.blueprint,- blueprintMigrations = base.blueprintMigrations+ applications = base ^. #applications,+ recipe = base ^. #recipe,+ blueprint = base ^. #blueprint,+ blueprintMigrations = base ^. #blueprintMigrations } -- | Extract planned files from operations for computeDiff.@@ -80,27 +82,27 @@ describe "full execution pipeline" $ do it "first run creates files and builds file records" $ do (modul, ops) <- compileFixturePlan [("project.name", "my-app")]- let modName = modul.name+ let modName = (modul ^. #name) (records, fs) = runPureEff $ runFilesystemPure emptyFS $ executePlan "" ops Map.empty modName fixedTime -- Verify files were created in the filesystem- Map.member "README.md" (fs.files) `shouldBe` True- Map.member "my-app.cabal" (fs.files) `shouldBe` True- Map.member "src/Lib.hs" (fs.files) `shouldBe` True- Map.member "LICENSE" (fs.files) `shouldBe` True- Map.member "cabal.project" (fs.files) `shouldBe` True+ Map.member "README.md" (fs ^. #files) `shouldBe` True+ Map.member "my-app.cabal" (fs ^. #files) `shouldBe` True+ Map.member "src/Lib.hs" (fs ^. #files) `shouldBe` True+ Map.member "LICENSE" (fs ^. #files) `shouldBe` True+ Map.member "cabal.project" (fs ^. #files) `shouldBe` True -- Verify FileRecords for manifest Map.member "README.md" records `shouldBe` True Map.member "my-app.cabal" records `shouldBe` True Map.member "src/Lib.hs" records `shouldBe` True -- Verify content- Map.lookup "README.md" (fs.files) `shouldBe` Just "# my-app\n\nVersion: 0.1.0.0\n"+ Map.lookup "README.md" (fs ^. #files) `shouldBe` Just "# my-app\n\nVersion: 0.1.0.0\n" it "re-run with same plan shows all unchanged" $ do (modul, ops) <- compileFixturePlan [("project.name", "my-app")]- let modName = modul.name+ let modName = (modul ^. #name) planned = extractPlanned modName ops -- First run: execute to get filesystem state and records (records, fs) =@@ -115,15 +117,15 @@ runFilesystemPure fs $ computeDiff manifest (Set.singleton modName) planned -- All files should be unchanged- length (diff.new) `shouldBe` 0- length (diff.modified) `shouldBe` 0- length (diff.conflicts) `shouldBe` 0- length (diff.orphaned) `shouldBe` 0- length (diff.unchanged) `shouldBe` 5+ length (diff ^. #new) `shouldBe` 0+ length (diff ^. #modified) `shouldBe` 0+ length (diff ^. #conflicts) `shouldBe` 0+ length (diff ^. #orphaned) `shouldBe` 0+ length (diff ^. #unchanged) `shouldBe` 5 it "re-run with changed variable shows modified, new, and orphaned" $ do (modul, ops1) <- compileFixturePlan [("project.name", "my-app")]- let modName = modul.name+ let modName = (modul ^. #name) -- First run (records, fs) = runPureEff $@@ -138,17 +140,17 @@ runFilesystemPure fs $ computeDiff manifest (Set.singleton modName) planned2 -- README.md and cabal.project have different content → Modified- length (diff.modified) `shouldBe` 2+ length (diff ^. #modified) `shouldBe` 2 -- src/Lib.hs and LICENSE have same content → Unchanged- length (diff.unchanged) `shouldBe` 2+ length (diff ^. #unchanged) `shouldBe` 2 -- my-app.cabal not in new plan → Orphaned- length (diff.orphaned) `shouldBe` 1- (head diff.orphaned).path `shouldBe` "my-app.cabal"+ length (diff ^. #orphaned) `shouldBe` 1+ ((head (diff ^. #orphaned)) ^. #path) `shouldBe` "my-app.cabal" -- other-app.cabal is new → New- length (diff.new) `shouldBe` 1- (head diff.new).path `shouldBe` "other-app.cabal"+ length (diff ^. #new) `shouldBe` 1+ ((head (diff ^. #new)) ^. #path) `shouldBe` "other-app.cabal" -- No conflicts- length (diff.conflicts) `shouldBe` 0+ length (diff ^. #conflicts) `shouldBe` 0 it "dryRunPlan lists all operations without execution" $ do (_, ops) <- compileFixturePlan [("project.name", "my-app")]@@ -159,7 +161,7 @@ it "force mode: re-execute after user edit overwrites the file" $ do (modul, ops) <- compileFixturePlan [("project.name", "my-app")]- let modName = modul.name+ let modName = (modul ^. #name) planned = extractPlanned modName ops -- First run (records, fs1) =@@ -168,37 +170,37 @@ executePlan "" ops Map.empty modName fixedTime manifest = manifestWithFiles fixedTime records -- Simulate user editing README.md- fs2 = PureFS (Map.insert "README.md" "user edit" fs1.files) fs1.dirs+ fs2 = PureFS (Map.insert "README.md" "user edit" (fs1 ^. #files)) (fs1 ^. #dirs) -- Compute diff → should detect conflict (diff, _) = runPureEff $ runFilesystemPure fs2 $ computeDiff manifest (Set.singleton modName) planned -- README.md is a conflict (user edited, plan unchanged)- length (diff.conflicts) `shouldBe` 1- (head diff.conflicts).path `shouldBe` "README.md"+ length (diff ^. #conflicts) `shouldBe` 1+ ((head (diff ^. #conflicts)) ^. #path) `shouldBe` "README.md" -- Force: re-execute (overwrites user changes) let (_, fs3) = runPureEff $ runFilesystemPure fs2 $ executePlan "" ops Map.empty modName fixedTime -- Verify README.md was overwritten with plan content- Map.lookup "README.md" fs3.files `shouldBe` Just "# my-app\n\nVersion: 0.1.0.0\n"+ Map.lookup "README.md" (fs3 ^. #files) `shouldBe` Just "# my-app\n\nVersion: 0.1.0.0\n" it "records correct strategy per file in FileRecords" $ do (modul, ops) <- compileFixturePlan [("project.name", "my-app")]- let modName = modul.name+ let modName = (modul ^. #name) (records, _) = runPureEff $ runFilesystemPure emptyFS $ executePlan "" ops Map.empty modName fixedTime -- README.md → Template- (records Map.! "README.md").strategy `shouldBe` Template+ ((records Map.! "README.md") ^. #strategy) `shouldBe` Template -- src/Lib.hs → Template- (records Map.! "src/Lib.hs").strategy `shouldBe` Template+ ((records Map.! "src/Lib.hs") ^. #strategy) `shouldBe` Template -- LICENSE → Copy- (records Map.! "LICENSE").strategy `shouldBe` Copy+ ((records Map.! "LICENSE") ^. #strategy) `shouldBe` Copy -- my-app.cabal → Template- (records Map.! "my-app.cabal").strategy `shouldBe` Template+ ((records Map.! "my-app.cabal") ^. #strategy) `shouldBe` Template -- cabal.project → DhallText- (records Map.! "cabal.project").strategy `shouldBe` DhallText+ ((records Map.! "cabal.project") ^. #strategy) `shouldBe` DhallText
test/Seihou/Integration/GenerationSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Integration.GenerationSpec (tests) where +import Control.Lens ((&), (.~), (^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text qualified as T import Seihou.Core.Module (loadModule)@@ -22,7 +24,7 @@ -- | Helper to extract the resolved variable values map. resolvedValues :: Map.Map VarName ResolvedVar -> Map.Map VarName VarValue-resolvedValues = Map.map (.value)+resolvedValues = Map.map (^. #value) spec :: Spec spec = do@@ -35,7 +37,7 @@ Right modul -> do let cli = Map.fromList [("project.name", "my-app")] env = Map.empty- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure ("Failed to resolve: " <> show errs) Right resolved -> do planResult <- compilePlan (fixtures </> "haskell-base") modul (resolvedValues resolved)@@ -55,7 +57,7 @@ Right modul -> do let cli = Map.fromList [("project.name", "my-app")] env = Map.empty- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure ("Failed to resolve: " <> show errs) Right resolved -> do planResult <- compilePlan (fixtures </> "haskell-base") modul (resolvedValues resolved)@@ -77,7 +79,7 @@ Right modul -> do let cli = Map.fromList [("project.name", "my-app")] env = Map.empty- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure ("Failed to resolve: " <> show errs) Right resolved -> do planResult <- compilePlan (fixtures </> "haskell-base") modul (resolvedValues resolved)@@ -97,7 +99,7 @@ -- The real scenario: license has a default so it's always set. -- To test the conditional, we use a stripped-down module with only the LICENSE step. let licenseStep = Step Copy "LICENSE" "LICENSE" (Just (ExprIsSet "license")) Nothing- smallModule = modul {steps = [licenseStep]}+ smallModule = modul & #steps .~ [licenseStep] vars = Map.empty -- no license variable set planResult <- compilePlan (fixtures </> "haskell-base") smallModule vars case planResult of@@ -114,7 +116,7 @@ Right modul -> do let cli = Map.fromList [("project.name", "my-app")] env = Map.empty- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure ("Failed to resolve: " <> show errs) Right resolved -> do planResult <- compilePlan (fixtures </> "haskell-base") modul (resolvedValues resolved)@@ -135,12 +137,12 @@ -- CLI overrides project.name, env overrides license let cli = Map.fromList [("project.name", "cli-app")] env = Map.fromList [("SEIHOU_VAR_LICENSE", "BSD3")]- case resolveVariables (modul.vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of+ case resolveVariables (modul ^. #vars) cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of Left errs -> expectationFailure ("Failed to resolve: " <> show errs) Right resolved -> do- (.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"- (.source) (resolved Map.! "project.name") `shouldBe` FromCLI- (.value) (resolved Map.! "license") `shouldBe` VText "BSD3"- (.source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"- (.value) (resolved Map.! "project.version") `shouldBe` VText "0.1.0.0"- (.source) (resolved Map.! "project.version") `shouldBe` FromDefault+ (^. #value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"+ (^. #source) (resolved Map.! "project.name") `shouldBe` FromCLI+ (^. #value) (resolved Map.! "license") `shouldBe` VText "BSD3"+ (^. #source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"+ (^. #value) (resolved Map.! "project.version") `shouldBe` VText "0.1.0.0"+ (^. #source) (resolved Map.! "project.version") `shouldBe` FromDefault
test/Seihou/Integration/ModuleLoadSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Integration.ModuleLoadSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Effectful -- Re-use the real loader for end-to-end tests@@ -30,12 +32,12 @@ case result of Left err -> expectationFailure ("Expected Right, got: " <> show err) Right m -> do- m.name `shouldBe` "haskell-base"- m.description `shouldBe` Just "A Haskell project template"- length (m.vars) `shouldBe` 3- length (m.steps) `shouldBe` 5- length (m.prompts) `shouldBe` 1- m.dependencies `shouldBe` []+ (m ^. #name) `shouldBe` "haskell-base"+ (m ^. #description) `shouldBe` Just "A Haskell project template"+ length (m ^. #vars) `shouldBe` 3+ length (m ^. #steps) `shouldBe` 5+ length (m ^. #prompts) `shouldBe` 1+ (m ^. #dependencies) `shouldBe` [] it "has correct variable declarations" $ do fixtures <- fixtureDir@@ -43,17 +45,17 @@ case result of Left err -> expectationFailure ("Expected Right, got: " <> show err) Right m -> do- let vars = m.vars- let names = map ((.unVarName) . (.name)) vars+ let vars = (m ^. #vars)+ let names = map ((^. #unVarName) . (^. #name)) vars names `shouldBe` ["project.name", "project.version", "license"] let (projectName : projectVersion : license : _) = vars- projectName.required `shouldBe` True- projectName.default_ `shouldBe` Nothing+ (projectName ^. #required) `shouldBe` True+ (projectName ^. #default_) `shouldBe` Nothing - projectVersion.default_ `shouldBe` Just (VText "0.1.0.0")+ (projectVersion ^. #default_) `shouldBe` Just (VText "0.1.0.0") - license.default_ `shouldBe` Just (VText "MIT")+ (license ^. #default_) `shouldBe` Just (VText "MIT") it "has a when expression on the LICENSE step" $ do fixtures <- fixtureDir@@ -61,10 +63,10 @@ case result of Left err -> expectationFailure ("Expected Right, got: " <> show err) Right m -> do- let steps = m.steps+ let steps = (m ^. #steps) let licenseStep = steps !! 2- licenseStep.strategy `shouldBe` Copy- licenseStep.condition `shouldBe` Just (ExprIsSet "license")+ (licenseStep ^. #strategy) `shouldBe` Copy+ (licenseStep ^. #condition) `shouldBe` Just (ExprIsSet "license") it "has a dest with placeholder variable" $ do fixtures <- fixtureDir@@ -72,8 +74,8 @@ case result of Left err -> expectationFailure ("Expected Right, got: " <> show err) Right m -> do- let cabalStep = m.steps !! 3- cabalStep.dest `shouldBe` "{{project.name}}.cabal"+ let cabalStep = (m ^. #steps) !! 3+ (cabalStep ^. #dest) `shouldBe` "{{project.name}}.cabal" describe "invalid-module" $ do it "produces ValidationError with multiple violations" $ do@@ -90,7 +92,7 @@ result <- loadModule ["/nonexistent"] "no-such-module" case result of Left (ModuleNotFound name _) ->- name.unModuleName `shouldBe` "no-such-module"+ (name ^. #unModuleName) `shouldBe` "no-such-module" Left other -> expectationFailure ("Expected ModuleNotFound, got: " <> show other) Right _ -> expectationFailure "Expected Left" @@ -115,4 +117,4 @@ evalModuleFile "test/module.dhall" case result of Left err -> expectationFailure ("Expected Right, got: " <> show err)- Right m -> m.name `shouldBe` "test"+ Right m -> (m ^. #name) `shouldBe` "test"
test/Seihou/Interaction/ConfirmSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Interaction.ConfirmSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Effectful import Seihou.Composition.Instance (primaryInstance)@@ -68,9 +70,9 @@ (result, st) <- runEff $ runConsolePure [] $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved result `shouldBe` resolved- st.consoleOutputs `shouldSatisfy` all (/= "Confirm default values:")+ (st ^. #outputs) `shouldSatisfy` all (/= "Confirm default values:") it "prompts for FromDefault variables and accepts Enter as keeping the default" $ do let decl = mkTextVar "project.version" (Just (VText "0.1.0.0"))@@ -82,12 +84,12 @@ (result, st) <- runEff $ runConsolePure [""] $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved let rv = (result Map.! primaryInstance "base") Map.! "project.version"- rv.value `shouldBe` VText "0.1.0.0"- rv.source `shouldBe` FromDefault- st.consoleOutputs `shouldSatisfy` any (== "Confirm default values:")- st.consoleOutputs `shouldSatisfy` any (== "project.version [0.1.0.0]:")+ (rv ^. #value) `shouldBe` VText "0.1.0.0"+ (rv ^. #source) `shouldBe` FromDefault+ (st ^. #outputs) `shouldSatisfy` any (== "Confirm default values:")+ (st ^. #outputs) `shouldSatisfy` any (== "project.version [0.1.0.0]:") it "replaces the value and marks source as FromPrompt when user types a new value" $ do let decl = mkTextVar "project.version" (Just (VText "0.1.0.0"))@@ -99,10 +101,10 @@ (result, _st) <- runEff $ runConsolePure ["1.0.0"] $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved let rv = (result Map.! primaryInstance "base") Map.! "project.version"- rv.value `shouldBe` VText "1.0.0"- rv.source `shouldBe` FromPrompt+ (rv ^. #value) `shouldBe` VText "1.0.0"+ (rv ^. #source) `shouldBe` FromPrompt it "retries on invalid input and keeps the default on final failure" $ do let decl = mkIntVar "retry.count" (Just (VInt 42))@@ -114,10 +116,10 @@ (result, _st) <- runEff $ runConsolePure ["not-an-int", "still-bad", "nope"] $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved let rv = (result Map.! primaryInstance "base") Map.! "retry.count"- rv.value `shouldBe` VInt 42- rv.source `shouldBe` FromDefault+ (rv ^. #value) `shouldBe` VInt 42+ (rv ^. #source) `shouldBe` FromDefault it "prompts for FromParent variables" $ do let decl = mkTextVar "skill.name" (Just (VText "exec-plan"))@@ -132,10 +134,10 @@ (result, _st) <- runEff $ runConsolePure ["override"] $- confirmDefaults [(primaryInstance m.name, m, "/fake/child")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/child")] resolved let rv = (result Map.! primaryInstance "child") Map.! "skill.name"- rv.value `shouldBe` VText "override"- rv.source `shouldBe` FromPrompt+ (rv ^. #value) `shouldBe` VText "override"+ (rv ^. #source) `shouldBe` FromPrompt it "is a no-op in non-interactive mode" $ do let decl = mkTextVar "project.version" (Just (VText "0.1.0.0"))@@ -147,9 +149,9 @@ (result, st) <- runEff $ runConsolePureNonInteractive $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved result `shouldBe` resolved- st.consoleOutputs `shouldBe` []+ (st ^. #outputs) `shouldBe` [] it "uses authored Prompt text when available" $ do let decl = mkTextVar "license" (Just (VText "MIT"))@@ -168,5 +170,5 @@ (_result, st) <- runEff $ runConsolePure [""] $- confirmDefaults [(primaryInstance m.name, m, "/fake/base")] resolved- st.consoleOutputs `shouldSatisfy` any (== "Choose a license [MIT]:")+ confirmDefaults [(primaryInstance (m ^. #name), m, "/fake/base")] resolved+ (st ^. #outputs) `shouldSatisfy` any (== "Choose a license [MIT]:")
test/Seihou/Interaction/PromptSpec.hs view
@@ -1,5 +1,7 @@ module Seihou.Interaction.PromptSpec (tests) where +import Control.Lens ((^.))+import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text) import Effectful@@ -103,10 +105,10 @@ runPrompts [prompt] [decl] bindings Map.member "project.name" result `shouldBe` True let rv = result Map.! "project.name"- rv.value `shouldBe` VText "my-app"- rv.source `shouldBe` FromPrompt+ (rv ^. #value) `shouldBe` VText "my-app"+ (rv ^. #source) `shouldBe` FromPrompt -- The prompt text should have been output- st.consoleOutputs `shouldSatisfy` any (== "What is the project name?")+ (st ^. #outputs) `shouldSatisfy` any (== "What is the project name?") it "fills a prompt with choices via selection number" $ do let decl = mkTextVar "license" Nothing True@@ -117,8 +119,8 @@ runConsolePure ["2"] $ runPrompts [prompt] [decl] bindings Map.member "license" result `shouldBe` True- (result Map.! "license").value `shouldBe` VText "Apache-2.0"- (result Map.! "license").source `shouldBe` FromPrompt+ ((result Map.! "license") ^. #value) `shouldBe` VText "Apache-2.0"+ ((result Map.! "license") ^. #source) `shouldBe` FromPrompt it "skips a prompt whose when condition evaluates to False" $ do let decl = mkTextVar "extra.flag" Nothing True@@ -132,7 +134,7 @@ -- Prompt was skipped, so the variable is not resolved Map.member "extra.flag" result `shouldBe` False -- No prompt text was output- st.consoleOutputs `shouldSatisfy` all (/= "Extra flag?")+ (st ^. #outputs) `shouldSatisfy` all (/= "Extra flag?") it "shows a prompt whose when condition evaluates to True" $ do let decl = mkTextVar "extra.flag" Nothing True@@ -144,7 +146,7 @@ runConsolePure ["some-value"] $ runPrompts [prompt] [decl] bindings Map.member "extra.flag" result `shouldBe` True- (result Map.! "extra.flag").value `shouldBe` VText "some-value"+ ((result Map.! "extra.flag") ^. #value) `shouldBe` VText "some-value" it "skips a prompt for a variable not in the unresolved set" $ do let decl = mkTextVar "project.name" Nothing True@@ -156,7 +158,7 @@ runConsolePure ["anything"] $ runPrompts [prompt] [decl] bindings Map.null result `shouldBe` True- st.consoleOutputs `shouldSatisfy` all (/= "Other?")+ (st ^. #outputs) `shouldSatisfy` all (/= "Other?") describe "default value display" $ do it "shows default value in prompt text and accepts Enter" $ do@@ -169,10 +171,10 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv -> do- rv.value `shouldBe` VText "0.1.0.0"- rv.source `shouldBe` FromPrompt+ (rv ^. #value) `shouldBe` VText "0.1.0.0"+ (rv ^. #source) `shouldBe` FromPrompt -- Prompt text should include the default in brackets- st.consoleOutputs `shouldSatisfy` any (== "Project version [0.1.0.0]:")+ (st ^. #outputs) `shouldSatisfy` any (== "Project version [0.1.0.0]:") it "accepts user input over default when provided" $ do let decl = mkTextVar "project.version" (Just (VText "0.1.0.0")) True@@ -184,8 +186,8 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv ->- rv.value `shouldBe` VText "1.0.0"- st.consoleOutputs `shouldSatisfy` any (== "Project version [0.1.0.0]:")+ (rv ^. #value) `shouldBe` VText "1.0.0"+ (st ^. #outputs) `shouldSatisfy` any (== "Project version [0.1.0.0]:") it "shows [skip] for optional variable without default" $ do let decl = mkTextVar "license" Nothing False@@ -194,7 +196,7 @@ runEff $ runConsolePure [""] $ promptForVar prompt decl Map.empty- st.consoleOutputs `shouldSatisfy` any (== "License [skip]:")+ (st ^. #outputs) `shouldSatisfy` any (== "License [skip]:") it "shows bool default as yes/no" $ do let decl = mkBoolVar "enable.ci" (Just (VBool True)) False@@ -206,8 +208,8 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv ->- rv.value `shouldBe` VBool True- st.consoleOutputs `shouldSatisfy` any (== "Enable CI? [yes]:")+ (rv ^. #value) `shouldBe` VBool True+ (st ^. #outputs) `shouldSatisfy` any (== "Enable CI? [yes]:") describe "promptForVar" $ do it "coerces boolean input correctly" $ do@@ -220,8 +222,8 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv -> do- rv.value `shouldBe` VBool True- rv.source `shouldBe` FromPrompt+ (rv ^. #value) `shouldBe` VBool True+ (rv ^. #source) `shouldBe` FromPrompt it "coerces 'no' to False for boolean variable" $ do let decl = mkBoolVar "use.ci" Nothing True@@ -233,7 +235,7 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv ->- rv.value `shouldBe` VBool False+ (rv ^. #value) `shouldBe` VBool False it "retries on empty input then succeeds" $ do let decl = mkTextVar "project.name" Nothing True@@ -245,9 +247,9 @@ case result of Left err -> expectationFailure $ "Expected Right, got: " ++ show err Right rv ->- rv.value `shouldBe` VText "my-app"+ (rv ^. #value) `shouldBe` VText "my-app" -- Should have output a retry message- st.consoleOutputs `shouldSatisfy` any (== "Value cannot be empty. Please try again.")+ (st ^. #outputs) `shouldSatisfy` any (== "Value cannot be empty. Please try again.") it "fails after exhausting retries on empty input" $ do let decl = mkTextVar "project.name" Nothing True@@ -270,7 +272,7 @@ [mkTextVar "project.name" Nothing True] [] [mkPrompt "project.name" "What is the project name?"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePure ["my-app"] $@@ -279,9 +281,9 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right resolved -> do let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "project.name").value `shouldBe` VText "my-app"- (baseVars Map.! "project.name").source `shouldBe` FromPrompt- st.consoleOutputs `shouldSatisfy` any (== "What is the project name?")+ ((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"+ ((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt+ (st ^. #outputs) `shouldSatisfy` any (== "What is the project name?") it "does not prompt when all variables are provided via CLI" $ do let m =@@ -291,7 +293,7 @@ [mkTextVar "project.name" Nothing True] [] [mkPrompt "project.name" "What is the project name?"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] cliOverrides = Map.singleton "project.name" "from-cli" (result, st) <- runEff $@@ -301,10 +303,10 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right resolved -> do let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "project.name").value `shouldBe` VText "from-cli"- (baseVars Map.! "project.name").source `shouldBe` FromCLI+ ((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "from-cli"+ ((baseVars Map.! "project.name") ^. #source) `shouldBe` FromCLI -- No prompts should have been displayed- st.consoleOutputs `shouldSatisfy` all (/= "What is the project name?")+ (st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?") it "skips prompts and errors in non-interactive mode" $ do let m =@@ -314,7 +316,7 @@ [mkTextVar "project.name" Nothing True] [] [mkPrompt "project.name" "What is the project name?"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePureNonInteractive $@@ -326,7 +328,7 @@ _ -> expectationFailure $ "Expected exactly 1 error, got: " ++ show (length errs) Right _ -> expectationFailure "Expected Left (errors), got Right" -- No prompts should have been displayed- st.consoleOutputs `shouldSatisfy` all (/= "What is the project name?")+ (st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?") it "forbids prompts even when the Console interpreter is interactive" $ do let m =@@ -336,14 +338,14 @@ [mkTextVar "project.name" Nothing True] [] [mkPrompt "project.name" "What is the project name?"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePure ["must-not-be-read"] $ resolveWithPromptPermission PromptsForbidden modules Map.empty Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty result `shouldBe` Left [MissingRequiredVar "project.name"]- st.consoleInputs `shouldBe` ["must-not-be-read"]- st.consoleOutputs `shouldSatisfy` all (/= "What is the project name?")+ (st ^. #inputs) `shouldBe` ["must-not-be-read"]+ (st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?") it "prompts for optional variables after required resolution" $ do let m =@@ -357,7 +359,7 @@ [ mkPrompt "project.name" "What is the project name?", mkPrompt "license" "License" ]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePure ["my-app", "MIT"] $@@ -366,11 +368,11 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right resolved -> do let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "project.name").value `shouldBe` VText "my-app"- (baseVars Map.! "project.name").source `shouldBe` FromPrompt- (baseVars Map.! "license").value `shouldBe` VText "MIT"- (baseVars Map.! "license").source `shouldBe` FromPrompt- st.consoleOutputs `shouldSatisfy` any (== "Optional configuration:")+ ((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"+ ((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt+ ((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"+ ((baseVars Map.! "license") ^. #source) `shouldBe` FromPrompt+ (st ^. #outputs) `shouldSatisfy` any (== "Optional configuration:") it "skips optional variable when user presses Enter" $ do let m =@@ -384,7 +386,7 @@ [ mkPrompt "project.name" "What is the project name?", mkPrompt "license" "License" ]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, _st) <- runEff $ runConsolePure ["my-app", ""] $@@ -404,7 +406,7 @@ [mkTextVar "license" Nothing False] [] [mkPrompt "license" "License"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePure ["MIT"] $@@ -413,8 +415,8 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right resolved -> do let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "license").value `shouldBe` VText "MIT"- st.consoleOutputs `shouldSatisfy` any (== "Optional configuration:")+ ((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"+ (st ^. #outputs) `shouldSatisfy` any (== "Optional configuration:") it "does not show optional prompts in non-interactive mode" $ do let m =@@ -424,7 +426,7 @@ [mkTextVar "license" Nothing False] [] [mkPrompt "license" "License"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePureNonInteractive $@@ -434,7 +436,7 @@ Right resolved -> do let baseVars = resolved Map.! primaryInstance "base" Map.member "license" baseVars `shouldBe` False- st.consoleOutputs `shouldSatisfy` all (/= "Optional configuration:")+ (st ^. #outputs) `shouldSatisfy` all (/= "Optional configuration:") it "respects when condition on optional prompts" $ do let m =@@ -448,7 +450,7 @@ [ mkPrompt "project.name" "Name?", mkConditionalPrompt "extra" "Extra?" (ExprIsSet "nonexistent") ]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] (result, st) <- runEff $ runConsolePure ["my-app"] $@@ -460,7 +462,7 @@ Map.member "extra" baseVars `shouldBe` False -- The condition was false so Optional configuration header should not appear -- (no optional prompts actually fired)- st.consoleOutputs `shouldSatisfy` all (/= "Extra?")+ (st ^. #outputs) `shouldSatisfy` all (/= "Extra?") it "does not prompt for optional variables already resolved via config" $ do let m =@@ -470,7 +472,7 @@ [mkTextVar "license" Nothing False] [] [mkPrompt "license" "License"]- modules = [(primaryInstance m.name, m, "/fake/base")]+ modules = [(primaryInstance (m ^. #name), m, "/fake/base")] globalConfig = Map.singleton "license" "MIT" (result, st) <- runEff $@@ -480,9 +482,9 @@ Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs Right resolved -> do let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "license").value `shouldBe` VText "MIT"- (baseVars Map.! "license").source `shouldBe` FromGlobalConfig- st.consoleOutputs `shouldSatisfy` all (/= "Optional configuration:")+ ((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"+ ((baseVars Map.! "license") ^. #source) `shouldBe` FromGlobalConfig+ (st ^. #outputs) `shouldSatisfy` all (/= "Optional configuration:") it "flows prompted value from first module to second via exports" $ do let base =@@ -499,7 +501,7 @@ [mkTextVar "project.name" Nothing True] [] []- modules = [(primaryInstance base.name, base, "/fake/base"), (primaryInstance app.name, app, "/fake/app")]+ modules = [(primaryInstance (base ^. #name), base, "/fake/base"), (primaryInstance (app ^. #name), app, "/fake/app")] (result, st) <- runEff $ runConsolePure ["my-app"] $@@ -509,11 +511,11 @@ Right resolved -> do -- Base module was prompted let baseVars = resolved Map.! primaryInstance "base"- (baseVars Map.! "project.name").value `shouldBe` VText "my-app"- (baseVars Map.! "project.name").source `shouldBe` FromPrompt+ ((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"+ ((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt -- App module received the value via export (no additional prompt needed) let appVars = resolved Map.! primaryInstance "app"- (appVars Map.! "project.name").value `shouldBe` VText "my-app"+ ((appVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app" -- Only one prompt should have fired (for base), not two- let promptOutputs = filter (== "What is the project name?") (st.consoleOutputs)+ let promptOutputs = filter (== "What is the project name?") (st ^. #outputs) length promptOutputs `shouldBe` 1
test/Seihou/Manifest/TypesSpec.hs view
@@ -1,6 +1,14 @@ module Seihou.Manifest.TypesSpec (tests) where +import Control.Lens ((%~), (&), (.~), (^.))+import Control.Monad (forM_) import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KeyMap+import Data.ByteString.Lazy.Char8 qualified as LBS8+import Data.Foldable (toList)+import Data.Generics.Labels ()+import Data.List (isInfixOf) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as T@@ -36,23 +44,255 @@ -- | Helper to set modules on a Manifest without ambiguous record update. withManifestModules :: [AppliedModule] -> Manifest -> Manifest withManifestModules mods m =- Manifest m.version m.genAt mods m.vars m.files m.applications m.recipe m.blueprint m.blueprintMigrations+ Manifest (m ^. #version) (m ^. #genAt) mods (m ^. #vars) (m ^. #files) (m ^. #applications) (m ^. #recipe) (m ^. #blueprint) (m ^. #blueprintMigrations) +-- | Every string that appears anywhere inside a value keyed @origin@ or+-- @targetOrigin@, at any depth.+originStrings :: Aeson.Value -> [T.Text]+originStrings = go False+ where+ go inOrigin value = case value of+ Aeson.Object object ->+ concat+ [ go (inOrigin || Key.toText key `elem` (["origin", "targetOrigin"] :: [T.Text])) child+ | (key, child) <- KeyMap.toList object+ ]+ Aeson.Array items -> concatMap (go inOrigin) (toList items)+ Aeson.String text -> [text | inOrigin]+ _ -> []++-- | A manifest exercising every serialized origin position at once.+manifestWithEveryOriginPosition :: Manifest+manifestWithEveryOriginPosition =+ (emptyManifest fixedTime)+ & #modules+ .~ [ AppliedModule+ { name = ModuleName "haskell-base",+ parentVars = emptyParentVars,+ origin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules"),+ moduleVersion = Just "1.4.0",+ appliedAt = fixedTime,+ removal = Nothing+ }+ ]+ & #applications+ .~ [ AppliedComposition+ { applicationId = ApplicationId "app",+ target = AppliedModuleTarget (ModuleName "haskell-base"),+ targetOrigin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules"),+ targetVersion = Just "1.4.0",+ additionalModules = [],+ namespace = Nothing,+ context = Nothing,+ instances =+ [ AppliedInstanceState+ { name = ModuleName "docs",+ parentVars = emptyParentVars,+ origin = ProjectOrigin ".seihou/modules/docs",+ moduleVersion = Just "0.1.0",+ resolvedVars = Map.empty+ },+ AppliedInstanceState+ { name = ModuleName "scratch",+ parentVars = emptyParentVars,+ origin = LocalOrigin "scratch",+ moduleVersion = Nothing,+ resolvedVars = Map.empty+ }+ ],+ commandReceipts = Map.empty,+ appliedAt = fixedTime+ }+ ]++-- | A manifest populated in every serialized position that can hold a string,+-- so the machine-independence sweep has something to sweep.+--+-- Deliberately broader than 'manifestWithEveryOriginPosition': that one proves+-- the origin fields are portable, this one proves nothing /else/ smuggles a+-- path in — a file record and its baseline, a command receipt with a working+-- directory, a removal spec, an applied recipe, an applied blueprint, and a+-- blueprint migration receipt.+manifestWithEveryStringPosition :: Manifest+manifestWithEveryStringPosition =+ manifestWithEveryOriginPosition+ & #modules+ .~ [ AppliedModule+ { name = ModuleName "haskell-base",+ parentVars = ParentVars (Map.singleton (VarName "project.name") "demo"),+ origin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules"),+ moduleVersion = Just "1.4.0",+ appliedAt = fixedTime,+ removal =+ Just+ ( Removal+ [RemovalStep RemoveFileAction "flake.nix" (Just "files/flake.nix")]+ [Command "cabal clean" (Just "backend") Nothing]+ )+ }+ ]+ & #vars .~ Map.singleton (VarName "project.name") "demo"+ & #files+ .~ Map.singleton+ "backend/flake.nix"+ ( FileRecord+ (hashContent "flake")+ (ModuleName "haskell-base")+ DhallText+ fixedTime+ (Just (BaselineRef (hashContent "flake")))+ (Set.singleton (ApplicationId "app"))+ )+ & #applications+ %~ map (withCommandReceipts (Map.singleton receiptFingerprint receipt))+ & #recipe .~ Just (AppliedRecipe (RecipeName "haskell-service") (Just "3.1.0") fixedTime)+ & #blueprint+ .~ Just+ ( AppliedBlueprint+ { name = ModuleName "service-blueprint",+ blueprintVersion = Just "2.0.0",+ appliedAt = fixedTime,+ baselineModules = [ModuleName "haskell-base"],+ noBaseline = False,+ userPrompt = Just "build a service",+ agentSessionId = Just "session-abc"+ }+ )+ & #blueprintMigrations .~ [mkBlueprintMigrationReceipt "service-blueprint" "1.0.0" "2.0.0" fixedTime2]+ where+ receiptFingerprint = CommandFingerprint (hashContent "cabal build")+ receipt = CommandReceipt receiptFingerprint (ModuleName "haskell-base") "cabal build" (Just "backend") fixedTime++-- | Set an application's command receipts without record update syntax.+withCommandReceipts :: Map.Map CommandFingerprint CommandReceipt -> AppliedComposition -> AppliedComposition+withCommandReceipts receipts composition =+ AppliedComposition+ { applicationId = composition ^. #applicationId,+ target = composition ^. #target,+ targetOrigin = composition ^. #targetOrigin,+ targetVersion = composition ^. #targetVersion,+ additionalModules = composition ^. #additionalModules,+ namespace = composition ^. #namespace,+ context = composition ^. #context,+ instances = composition ^. #instances,+ commandReceipts = receipts,+ appliedAt = composition ^. #appliedAt+ }++-- | Every string in a document, each paired with the JSON path that reaches+-- it, so a failure can say /where/ the offending value was.+--+-- Object keys are reported too: the @files@ map is keyed by destination path,+-- which is exactly the sort of place an absolute path could reappear.+documentStrings :: Aeson.Value -> [(String, T.Text)]+documentStrings = go "$"+ where+ go path value = case value of+ Aeson.Object object ->+ concat+ [ (path <> "." <> T.unpack (Key.toText key), Key.toText key)+ : go (path <> "." <> T.unpack (Key.toText key)) child+ | (key, child) <- KeyMap.toList object+ ]+ Aeson.Array items ->+ concat [go (path <> "[" <> show index <> "]") item | (index, item) <- zip [(0 :: Int) ..] (toList items)]+ Aeson.String text -> [(path, text)]+ _ -> []++-- | Whether a string only means something on the machine that wrote it: a+-- POSIX absolute path, a home-relative path, a UNC share, or a Windows drive+-- prefix.+machineSpecific :: T.Text -> Bool+machineSpecific text =+ T.isPrefixOf "/" text+ || T.isPrefixOf "~" text+ || T.isPrefixOf "\\\\" text+ || (T.length text >= 3 && T.index text 1 == ':' && T.index text 2 == '\\')+ spec :: Spec spec = do+ -- The manifest is checked into version control and read on other machines,+ -- so no origin it records may name a location that only exists on the+ -- machine that wrote it. See+ -- docs/adr/0001-manifest-is-a-checked-in-machine-independent-artifact.md.+ describe "machine independence" $ do+ -- The two specs below constrain the origin fields. This one constrains+ -- every future field as well: a new manifest field that records a location+ -- has to express it relative to the project root or through an+ -- ArtifactOrigin, and this fails if one does neither.+ it "records no machine-specific value anywhere in the document" $ do+ let encoded = Aeson.toJSON manifestWithEveryStringPosition+ scanned = documentStrings encoded+ offenders =+ [ path <> " = " <> T.unpack text+ | (path, text) <- scanned,+ machineSpecific text+ ]+ length scanned `shouldSatisfy` (> 20)+ offenders `shouldBe` []++ it "records no absolute path in any origin position" $ do+ let encoded = Aeson.toJSON manifestWithEveryOriginPosition+ strings = originStrings encoded+ strings `shouldSatisfy` not . null+ forM_ strings $ \text -> do+ T.isPrefixOf "/" text `shouldBe` False+ T.isPrefixOf "~" text `shouldBe` False+ (T.length text >= 2 && T.index text 1 == ':') `shouldBe` False++ it "does not serialize the in-memory source path at all" $ do+ let encoded = LBS8.unpack (manifestToJSON manifestWithEveryOriginPosition)+ encoded `shouldSatisfy` not . isInfixOf "/Users/someone"+ encoded `shouldSatisfy` not . isInfixOf "\"source\""+ encoded `shouldSatisfy` not . isInfixOf "\"targetSource\""+ describe "emptyManifest" $ do it "creates a manifest with the current version" $ do let m = emptyManifest fixedTime- m.version `shouldBe` currentManifestVersion- m.version `shouldBe` 5+ (m ^. #version) `shouldBe` currentManifestVersion+ (m ^. #version) `shouldBe` 6 it "creates a manifest with no modules, vars, or files" $ do let m = emptyManifest fixedTime- m.modules `shouldBe` []- m.vars `shouldBe` Map.empty- m.files `shouldBe` Map.empty- m.blueprintMigrations `shouldBe` []+ (m ^. #modules) `shouldBe` []+ (m ^. #vars) `shouldBe` Map.empty+ (m ^. #files) `shouldBe` Map.empty+ (m ^. #blueprintMigrations) `shouldBe` [] + describe "ArtifactOrigin" $ do+ it "roundtrips a remote origin carrying a repository name" $ do+ let origin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules")+ Aeson.decode (Aeson.encode origin) `shouldBe` Just origin++ it "roundtrips a remote origin with no repository name" $ do+ let origin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" Nothing+ Aeson.decode (Aeson.encode origin) `shouldBe` Just origin++ it "omits the repo key entirely when there is no repository name" $ do+ let origin = RemoteOrigin "https://example.com/mods.git" "haskell-base" Nothing+ Aeson.toJSON origin+ `shouldBe` Aeson.object+ [ "kind" Aeson..= ("remote" :: T.Text),+ "url" Aeson..= ("https://example.com/mods.git" :: T.Text),+ "artifact" Aeson..= ("haskell-base" :: T.Text)+ ]++ it "roundtrips a project origin" $ do+ let origin = ProjectOrigin ".seihou/modules/demo"+ Aeson.decode (Aeson.encode origin) `shouldBe` Just origin++ it "roundtrips a local origin" $ do+ let origin = LocalOrigin "scratch-module"+ Aeson.decode (Aeson.encode origin) `shouldBe` Just origin++ it "rejects an unknown origin kind" $ do+ (Aeson.decode "{\"kind\":\"martian\"}" :: Maybe ArtifactOrigin) `shouldBe` Nothing++ it "names the artifact each origin refers to" $ do+ artifactOriginName (RemoteOrigin "https://example.com/mods.git" "haskell-base" Nothing) `shouldBe` "haskell-base"+ artifactOriginName (LocalOrigin "scratch-module") `shouldBe` "scratch-module"+ artifactOriginName (ProjectOrigin ".seihou/modules/demo") `shouldBe` "demo"+ describe "JSON roundtrip" $ do it "roundtrips an empty manifest" $ do let m = emptyManifest fixedTime@@ -64,7 +304,7 @@ [ AppliedModule { name = ModuleName "haskell-base", parentVars = emptyParentVars,- source = "/home/user/.config/seihou/modules/haskell-base",+ origin = RemoteOrigin "https://github.com/shinzui/seihou-modules.git" "haskell-base" (Just "seihou-modules"), moduleVersion = Nothing, appliedAt = fixedTime, removal = Nothing@@ -77,16 +317,16 @@ let base = emptyManifest fixedTime m = Manifest- { version = base.version,- genAt = base.genAt,- modules = base.modules,+ { version = base ^. #version,+ genAt = base ^. #genAt,+ modules = base ^. #modules, vars = Map.fromList [ (VarName "project.name", "my-app"), (VarName "license", "MIT") ],- files = base.files,- applications = base.applications,+ files = base ^. #files,+ applications = base ^. #applications, recipe = Nothing, blueprint = Nothing, blueprintMigrations = []@@ -96,31 +336,9 @@ it "roundtrips a manifest with file records" $ do let m :: Manifest m =- (emptyManifest fixedTime)- { files =- Map.fromList- [ ( "README.md",- FileRecord- { hash = SHA256 "abc123",- moduleName = ModuleName "haskell-base",- strategy = Template,- generatedAt = fixedTime,- baseline = Nothing,- applicationIds = mempty- }- ),- ( "my-app.cabal",- FileRecord- { hash = SHA256 "def456",- moduleName = ModuleName "haskell-base",- strategy = DhallText,- generatedAt = fixedTime,- baseline = Nothing,- applicationIds = mempty- }- )- ]- }+ ( (emptyManifest fixedTime)+ & #files .~ Map.fromList [("README.md", FileRecord {hash = SHA256 "abc123", moduleName = ModuleName "haskell-base", strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty}), ("my-app.cabal", FileRecord {hash = SHA256 "def456", moduleName = ModuleName "haskell-base", strategy = DhallText, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty})]+ ) manifestFromJSON (manifestToJSON m) `shouldBe` Right m it "roundtrips a full manifest" $ do@@ -129,8 +347,8 @@ { version = currentManifestVersion, genAt = fixedTime, modules =- [ AppliedModule (ModuleName "haskell-base") emptyParentVars "/path/to/module" Nothing fixedTime Nothing,- AppliedModule (ModuleName "nix-flake") emptyParentVars "/path/to/nix" Nothing fixedTime2 Nothing+ [ AppliedModule (ModuleName "haskell-base") emptyParentVars (LocalOrigin "haskell-base") Nothing fixedTime Nothing,+ AppliedModule (ModuleName "nix-flake") emptyParentVars (LocalOrigin "nix-flake") Nothing fixedTime2 Nothing ], vars = Map.fromList@@ -159,11 +377,9 @@ FileRecord (SHA256 "hash") (ModuleName "mod") s fixedTime Nothing mempty m :: Manifest m =- (emptyManifest fixedTime)- { files =- Map.fromList- (zipWith (\i s -> ("file" <> show i, makeRecord s)) [(1 :: Int) ..] strategies)- }+ ( (emptyManifest fixedTime)+ & #files .~ Map.fromList (zipWith (\i s -> ("file" <> show i, makeRecord s)) [(1 :: Int) ..] strategies)+ ) manifestFromJSON (manifestToJSON m) `shouldBe` Right m it "roundtrips a manifest with versioned modules" $ do@@ -172,7 +388,7 @@ [ AppliedModule { name = ModuleName "haskell-base", parentVars = emptyParentVars,- source = "/path/to/module",+ origin = LocalOrigin "haskell-base", moduleVersion = Just "1.0.0", appliedAt = fixedTime, removal = Nothing@@ -187,7 +403,7 @@ [ AppliedModule { name = ModuleName "simple-mod", parentVars = emptyParentVars,- source = "/path/to/mod",+ origin = LocalOrigin "simple-mod", moduleVersion = Nothing, appliedAt = fixedTime, removal = Nothing@@ -204,7 +420,7 @@ [ AppliedModule { name = ModuleName "claude-skill-link", parentVars = pv1,- source = "/modules/claude-skill-link",+ origin = ProjectOrigin ".seihou/modules/claude-skill-link", moduleVersion = Nothing, appliedAt = fixedTime, removal = Nothing@@ -212,7 +428,7 @@ AppliedModule { name = ModuleName "claude-skill-link", parentVars = pv2,- source = "/modules/claude-skill-link",+ origin = ProjectOrigin ".seihou/modules/claude-skill-link", moduleVersion = Nothing, appliedAt = fixedTime, removal = Nothing@@ -239,14 +455,14 @@ AppliedComposition { applicationId = appId1, target = AppliedModuleTarget (ModuleName "master-plan"),- targetSource = "/modules/master-plan",+ targetOrigin = ProjectOrigin ".seihou/modules/master-plan", targetVersion = Just "0.7.0", additionalModules = [ModuleName "docs"], namespace = Just "planning", context = Just "work", instances =- [ AppliedInstanceState (ModuleName "link-skill") pv1 "/modules/link-skill" (Just "1") (Map.singleton (VarName "skill.name") "exec-plan"),- AppliedInstanceState (ModuleName "link-skill") pv2 "/modules/link-skill" (Just "1") (Map.singleton (VarName "skill.name") "master-plan")+ [ AppliedInstanceState (ModuleName "link-skill") pv1 (LocalOrigin "link-skill") (Just "1") (Map.singleton (VarName "skill.name") "exec-plan"),+ AppliedInstanceState (ModuleName "link-skill") pv2 (LocalOrigin "link-skill") (Just "1") (Map.singleton (VarName "skill.name") "master-plan") ], commandReceipts = Map.singleton fingerprint receipt, appliedAt = fixedTime@@ -255,7 +471,7 @@ AppliedComposition { applicationId = appId2, target = AppliedRecipeTarget (RecipeName "service"),- targetSource = "/recipes/service",+ targetOrigin = LocalOrigin "service", targetVersion = Nothing, additionalModules = [], namespace = Nothing,@@ -274,10 +490,10 @@ applicationIds = Set.fromList [appId1, appId2] } manifest =- (emptyManifest fixedTime)- { applications = [application1, application2],- files = Map.singleton "README.md" fileRecord- }+ ( (emptyManifest fixedTime)+ & #applications .~ [application1, application2]+ & #files .~ Map.singleton "README.md" fileRecord+ ) manifestFromJSON (manifestToJSON manifest) `shouldBe` Right manifest it "rejects malformed baseline references" $ do@@ -336,8 +552,8 @@ Nothing m1 = writeAppliedBlueprint ab1 m0 m2 = writeAppliedBlueprint ab2 m1- m1.blueprint `shouldBe` Just ab1- m2.blueprint `shouldBe` Just ab2+ (m1 ^. #blueprint) `shouldBe` Just ab1+ (m2 ^. #blueprint) `shouldBe` Just ab2 describe "AppliedBlueprintMigration" $ do it "round-trips a fully populated receipt through JSON" $ do@@ -353,7 +569,7 @@ it "round-trips a version-5 manifest containing a receipt" $ do let receipt = mkBlueprintMigrationReceipt "payments" "1.0.0" "2.0.0" fixedTime- manifest = (emptyManifest fixedTime) {blueprintMigrations = [receipt]}+ manifest = ((emptyManifest fixedTime) & #blueprintMigrations .~ [receipt]) manifestFromJSON (manifestToJSON manifest) `shouldBe` Right manifest it "replaces the same exact edge in place and appends a different edge" $ do@@ -369,17 +585,17 @@ (Just "rerun") manifest1 = writeAppliedBlueprintMigration unrelated (writeAppliedBlueprintMigration first (emptyManifest fixedTime)) manifest2 = writeAppliedBlueprintMigration replacement manifest1- manifest2.blueprintMigrations `shouldBe` [replacement, unrelated]+ (manifest2 ^. #blueprintMigrations) `shouldBe` [replacement, unrelated] hasAppliedBlueprintMigration "payments" "1.0.0" "2.0.0" manifest2 `shouldBe` True hasAppliedBlueprintMigration "payments" "2.0.0" "3.0.0" manifest2 `shouldBe` False it "preserves modules, applications, files, recipe, and normal blueprint provenance" $ do- let appliedModule = AppliedModule "base" emptyParentVars "/installed/base" (Just "1.0.0") fixedTime Nothing+ let appliedModule = AppliedModule "base" emptyParentVars (LocalOrigin "base") (Just "1.0.0") fixedTime Nothing application = AppliedComposition { applicationId = ApplicationId "app-base", target = AppliedModuleTarget "base",- targetSource = "/installed/base",+ targetOrigin = LocalOrigin "base", targetVersion = Just "1.0.0", additionalModules = [], namespace = Nothing,@@ -392,98 +608,49 @@ recipe = AppliedRecipe "recipe" (Just "1.0.0") fixedTime normalBlueprint = AppliedBlueprint "payments" (Just "0.4.0") fixedTime [] False Nothing Nothing seed =- (emptyManifest fixedTime)- { modules = [appliedModule],- applications = [application],- files = Map.singleton "README.md" fileRecord,- recipe = Just recipe,- blueprint = Just normalBlueprint- }+ ( (emptyManifest fixedTime)+ & #modules .~ [appliedModule]+ & #applications .~ [application]+ & #files .~ Map.singleton "README.md" fileRecord+ & #recipe .~ Just recipe+ & #blueprint .~ Just normalBlueprint+ ) updated = writeAppliedBlueprintMigration (mkBlueprintMigrationReceipt "payments" "1.0.0" "2.0.0" fixedTime) seed- updated.modules `shouldBe` seed.modules- updated.applications `shouldBe` seed.applications- updated.files `shouldBe` seed.files- updated.recipe `shouldBe` seed.recipe- updated.blueprint `shouldBe` seed.blueprint+ (updated ^. #modules) `shouldBe` (seed ^. #modules)+ (updated ^. #applications) `shouldBe` (seed ^. #applications)+ (updated ^. #files) `shouldBe` (seed ^. #files)+ (updated ^. #recipe) `shouldBe` (seed ^. #recipe)+ (updated ^. #blueprint) `shouldBe` (seed ^. #blueprint) + -- Schema versions 1 through 5 recorded a machine-specific absolute+ -- @source@ path in place of the portable @origin@ introduced in version 6.+ -- Rather than misread them, the decoder refuses them and names the remedy.+ -- Restoring lossless decoding of those versions is owned by+ -- docs/plans/79-upgrade-legacy-absolute-path-manifests-in-place.md, which+ -- also delivers the 'seihou manifest upgrade' command the message names. describe "schema back-compat" $ do- it "decodes a v4 manifest with no blueprintMigrations key as an empty ledger" $ do- let json = "{\"version\":4,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},\"files\":{},\"applications\":[]}"- case manifestFromJSON json of- Right manifest -> do- manifest.version `shouldBe` 4- manifest.blueprintMigrations `shouldBe` []- Left err -> expectationFailure ("failed to parse v4 manifest: " <> err)-- -- A pre-EP-32 (schema v2) manifest has no @blueprint@ key. The- -- decoder must read it as 'Nothing' regardless of the version- -- field, so a pre-bump project does not refuse to load after the- -- user upgrades seihou.- it "decodes a v2 manifest with no blueprint key as Nothing" $ do- let json = "{\"version\":2,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},\"files\":{}}"- case manifestFromJSON json of- Right manifest -> do- manifest.blueprint `shouldBe` Nothing- manifest.version `shouldBe` 2- Left err -> expectationFailure ("failed to parse: " <> err)-- it "decodes a v3 manifest with an explicit null blueprint as Nothing" $ do- let json = "{\"version\":3,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},\"files\":{},\"blueprint\":null}"- case manifestFromJSON json of- Right manifest -> manifest.blueprint `shouldBe` Nothing- Left err -> expectationFailure ("failed to parse: " <> err)-- it "decodes a v3 manifest with empty defaults for every version-4 field" $ do- let json =- "{\"version\":3,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},"- <> "\"files\":{\"README.md\":{\"hash\":\"abc\",\"module\":\"legacy\",\"strategy\":\"template\",\"generatedAt\":\"2026-03-01T10:30:00Z\"}}}"- case manifestFromJSON json of- Right manifest -> do- manifest.applications `shouldBe` []- case Map.lookup "README.md" manifest.files of- Just record -> do- record.baseline `shouldBe` Nothing- record.applicationIds `shouldBe` Set.empty- Nothing -> expectationFailure "expected legacy file record"- Left err -> expectationFailure ("failed to parse: " <> err)-- it "decodes a v3 manifest with a populated blueprint object" $ do- let json =- "{\"version\":3,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},\"files\":{},"- <> "\"blueprint\":{\"name\":\"payments-service\",\"version\":\"0.3.1\",\"appliedAt\":\"2026-03-01T11:00:00Z\","- <> "\"baselineModules\":[\"nix-flake\"],\"noBaseline\":false,\"userPrompt\":\"set up payments\"}}"- case manifestFromJSON json of- Right manifest -> case manifest.blueprint of- Just ab -> do- ab.name `shouldBe` ModuleName "payments-service"- ab.blueprintVersion `shouldBe` Just "0.3.1"- ab.baselineModules `shouldBe` [ModuleName "nix-flake"]- ab.noBaseline `shouldBe` False- ab.userPrompt `shouldBe` Just "set up payments"- ab.agentSessionId `shouldBe` Nothing- Nothing -> expectationFailure "expected populated blueprint"- Left err -> expectationFailure ("failed to parse: " <> err)+ it "refuses every pre-portable-origin schema version and names the remedy" $ do+ let legacy v =+ "{\"version\":"+ <> LBS8.pack (show (v :: Int))+ <> ",\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[],\"variables\":{},\"files\":{},\"applications\":[]}"+ forM_ [1 .. 5] $ \v ->+ case manifestFromJSON (legacy v) of+ Right _ -> expectationFailure ("schema version " <> show v <> " should not decode directly")+ Left err -> do+ err `shouldSatisfy` isInfixOf "seihou manifest upgrade"+ err `shouldSatisfy` isInfixOf ("schema version " <> show v) - describe "schema back-compat (version 1)" $ do- it "decodes a version-1 manifest with parentVars defaulting to empty" $ do+ it "refuses a version-1 manifest that records an absolute module source" $ do let json = "{\"version\":1,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[{\"name\":\"haskell-base\",\"source\":\"/path\",\"appliedAt\":\"2026-03-01T10:30:00Z\"}],\"variables\":{},\"files\":{}}" case manifestFromJSON json of- Right manifest -> do- length manifest.modules `shouldBe` 1- (head manifest.modules).parentVars `shouldBe` emptyParentVars- Left err -> expectationFailure ("failed to parse: " <> err)-- it "parses old manifest without version key as Nothing" $ do- let json = "{\"version\":1,\"generatedAt\":\"2026-03-01T10:30:00Z\",\"modules\":[{\"name\":\"old-mod\",\"source\":\"/path\",\"appliedAt\":\"2026-03-01T10:30:00Z\"}],\"variables\":{},\"files\":{}}"- result = manifestFromJSON json- case result of- Right manifest -> (head manifest.modules).moduleVersion `shouldBe` Nothing- Left err -> expectationFailure ("failed to parse: " <> err)+ Right _ -> expectationFailure "a version-1 manifest should not decode directly"+ Left err -> err `shouldSatisfy` isInfixOf "seihou manifest upgrade" describe "version checking" $ do it "rejects manifests with version higher than current" $ do let base = emptyManifest fixedTime- m = Manifest {version = 99, genAt = base.genAt, modules = base.modules, vars = base.vars, files = base.files, applications = base.applications, recipe = Nothing, blueprint = Nothing, blueprintMigrations = []}+ m = Manifest {version = 99, genAt = base ^. #genAt, modules = base ^. #modules, vars = base ^. #vars, files = base ^. #files, applications = base ^. #applications, recipe = Nothing, blueprint = Nothing, blueprintMigrations = []} result = manifestFromJSON (manifestToJSON m) case result of Left err -> err `shouldContain` "newer version"@@ -493,7 +660,7 @@ it "produces a hex-encoded SHA256 digest" $ do let h = hashContent "hello world" -- SHA256 of "hello world" is a well-known value- h.unSHA256 `shouldBe` "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"+ (h ^. #unSHA256) `shouldBe` "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" it "produces different hashes for different content" $ do let h1 = hashContent "hello"@@ -512,4 +679,4 @@ it "handles empty content" $ do let h = hashContent "" -- SHA256 of empty string is a well-known value- h.unSHA256 `shouldBe` "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"+ (h ^. #unSHA256) `shouldBe` "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"