packages feed

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 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"