seihou-okf-extension 0.13.0.0 → 0.14.0.0
raw patch · 5 files changed
+107/−12 lines, 5 filesdep ~seihou-corePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: seihou-core
API changes (from Hackage documentation)
+ Seihou.OKF.Docs.Model: BaseRecipeRef :: !RecipeReference -> !Bool -> BaseRecipeRef
+ Seihou.OKF.Docs.Model: [baseRecipe] :: DocEntry -> !Maybe BaseRecipeRef
+ Seihou.OKF.Docs.Model: [reference] :: BaseRecipeRef -> !RecipeReference
+ Seihou.OKF.Docs.Model: data BaseRecipeRef
+ Seihou.OKF.Docs.Model: instance GHC.Classes.Eq Seihou.OKF.Docs.Model.BaseRecipeRef
+ Seihou.OKF.Docs.Model: instance GHC.Internal.Generics.Generic Seihou.OKF.Docs.Model.BaseRecipeRef
+ Seihou.OKF.Docs.Model: instance GHC.Internal.Show.Show Seihou.OKF.Docs.Model.BaseRecipeRef
- Seihou.OKF.Docs.Model: DocEntry :: !Text -> !DocKind -> !Maybe Text -> !Maybe Text -> ![Text] -> !FilePath -> !DocArtifact -> ![ModuleRef] -> ![EntailedRef] -> DocEntry
+ Seihou.OKF.Docs.Model: DocEntry :: !Text -> !DocKind -> !Maybe Text -> !Maybe Text -> ![Text] -> !FilePath -> !DocArtifact -> ![ModuleRef] -> !Maybe BaseRecipeRef -> ![EntailedRef] -> DocEntry
Files
- seihou-okf-extension.cabal +3/−3
- src/Seihou/OKF/Docs/Model.hs +19/−4
- src/Seihou/OKF/Docs/Render.hs +19/−2
- test/Seihou/OKF/Docs/ModelSpec.hs +13/−0
- test/Seihou/OKF/Docs/RenderSpec.hs +53/−3
seihou-okf-extension.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: seihou-okf-extension-version: 0.13.0.0+version: 0.14.0.0 synopsis: OKF documentation extension for Seihou registries description: External Seihou extension executable for generating OKF documentation bundles@@ -60,7 +60,7 @@ lens >=5.2 && <6, okf-core ^>=0.10.0.0, optparse-applicative >=0.18 && <1,- seihou-core ^>=0.13.0.0,+ seihou-core ^>=0.14.0.0, temporary >=1.3 && <2, text >=2.0 && <3, @@ -118,7 +118,7 @@ hspec >=2.11 && <3, lens >=5.2 && <6, okf-core ^>=0.10.0.0,- seihou-core ^>=0.13.0.0,+ seihou-core ^>=0.14.0.0, seihou-okf-extension-internal, tasty >=1.4 && <2, tasty-hspec >=1.2 && <2,
src/Seihou/OKF/Docs/Model.hs view
@@ -3,6 +3,7 @@ DocArtifact (..), DocEntry (..), ModuleRef (..),+ BaseRecipeRef (..), EntailedRef (..), DocModel (..), DocLoadError (..),@@ -10,7 +11,7 @@ ) where -import Control.Lens ((&), (.~), (^.))+import Control.Lens ((%~), (&), (.~), (^.)) import Data.Generics.Labels () import Data.Text qualified as T import GHC.Generics (Generic)@@ -23,6 +24,7 @@ Module (..), ModuleLoadError, Recipe (..),+ RecipeReference (..), depModuleNames, ) import Seihou.Dhall.Eval@@ -62,6 +64,7 @@ path :: !FilePath, artifact :: !DocArtifact, moduleRefs :: ![ModuleRef],+ baseRecipe :: !(Maybe BaseRecipeRef), -- | Every edge this entry\'s migrations entail, flattened across the -- entry\'s migration edges in declaration order. Empty for every kind but -- a blueprint, which is the only artifact that can declare entailment.@@ -69,6 +72,13 @@ } deriving stock (Eq, Generic, Show) +-- | A descriptor-local base edge. Documentation never fetches its source.+data BaseRecipeRef = BaseRecipeRef+ { reference :: !RecipeReference,+ resolved :: !Bool+ }+ deriving stock (Eq, Generic, Show)+ data ModuleRef = ModuleRef { name :: !T.Text, resolved :: !Bool@@ -130,8 +140,9 @@ pure $ do entries <- entriesResult let moduleNames = [entry ^. #name | entry <- entries, entry ^. #kind == DocModuleKind]+ recipeNames = [entry ^. #name | entry <- entries, entry ^. #kind == DocRecipeKind] blueprintNames = [entry ^. #name | entry <- entries, entry ^. #kind == DocBlueprintKind]- resolvedEntries = map (resolveEntryRefs moduleNames blueprintNames) entries+ resolvedEntries = map (resolveEntryRefs moduleNames recipeNames blueprintNames) entries Right DocModel { repoName = repoName,@@ -230,6 +241,9 @@ path = entry ^. #path, artifact = artifact, moduleRefs = refs,+ baseRecipe = case artifact of+ DocRecipeArtifact recipe -> (\ref -> BaseRecipeRef ref False) <$> recipe ^. #extends+ _ -> Nothing, entailedRefs = entailed } @@ -254,10 +268,11 @@ -- | The single place that decides whether a reference points inside this -- registry. Module references resolve against the registry\'s module names, -- entailed edges against its blueprint names.-resolveEntryRefs :: [T.Text] -> [T.Text] -> DocEntry -> DocEntry-resolveEntryRefs moduleNames blueprintNames entry =+resolveEntryRefs :: [T.Text] -> [T.Text] -> [T.Text] -> DocEntry -> DocEntry+resolveEntryRefs moduleNames recipeNames blueprintNames entry = entry & #moduleRefs .~ [ref & #resolved .~ ((ref ^. #name) `elem` moduleNames) | ref <- entry ^. #moduleRefs]+ & #baseRecipe %~ fmap (\ref -> ref & #resolved .~ ((ref ^. #reference . #recipe . #unRecipeName) `elem` recipeNames && ref ^. #reference . #source == Nothing)) & #entailedRefs .~ [ref & #resolved .~ ((ref ^. #blueprint) `elem` blueprintNames) | ref <- entry ^. #entailedRefs] renderModuleLoadError :: ModuleLoadError -> T.Text
src/Seihou/OKF/Docs/Render.hs view
@@ -62,6 +62,7 @@ Prompt (..), PromptGuidance (..), Recipe (..),+ RecipeReference (..), Removal (..), RemovalAction (..), RemovalStep (..),@@ -421,10 +422,14 @@ <> optionalSection "Commands" (renderCommands commands) <> foldMap (optionalSection "Removal" . renderRemoval) removal <> optionalSection "Migrations" (renderMigrations migrations)- DocRecipeArtifact Recipe {modules = recipeModules, vars, prompts} ->- [section "Composes" (renderDependencies "This recipe does not compose any modules." recipeModules)]+ DocRecipeArtifact Recipe {modules = recipeModules, vars, prompts, extends, moduleOverrides, promptOverrides} ->+ foldMap (\ref -> [section "Extends" (renderRecipeBase entry ref)]) extends+ <> [section "Composes" (renderDependencies (maybe "This recipe does not compose any modules." (const "Uses its inherited members without adding modules.") extends) recipeModules)]+ <> optionalSection "Inherited module overrides" (T.unlines ["- `" <> (override ^. #module_ . #unModuleName) <> "`" <> renderSuppliedVars (override ^. #vars) | override <- moduleOverrides])+ <> [section "Override rules" "The oldest base supplies the primary module. Child literals override ancestor literals globally by variable name; defaults remain fallbacks. Explicit module overrides replace selected keys without adding instances. Prompt overrides replace inherited questions in place; new prompts append." | extends /= Nothing] <> optionalSection "Variables" (renderVarDecls' vars) <> optionalSection "Prompts" (renderPrompts prompts)+ <> optionalSection "Inherited prompt overrides" (renderPrompts promptOverrides) DocBlueprintArtifact Blueprint {prompt, vars, prompts, baseModules, files, allowedTools, migrations, launch, versionProbe} -> [ section "Base modules" (renderDependencies "This blueprint declares no base modules." baseModules), section "Agent prompt" (renderAgentPrompt prompt)@@ -466,6 +471,18 @@ resolvedEntailedBlueprints :: DocEntry -> [T.Text] resolvedEntailedBlueprints entry = [ref ^. #blueprint | ref <- entry ^. #entailedRefs, ref ^. #resolved]++renderRecipeBase :: DocEntry -> RecipeReference -> T.Text+renderRecipeBase entry ref =+ let name = ref ^. #recipe . #unRecipeName+ linked = case entry ^. #baseRecipe of+ Just base | base ^. #resolved -> case conceptIdFor DocRecipeKind name of+ Right conceptId -> renderConceptLink conceptId name+ Left _ -> "`" <> name <> "`"+ _ -> "`" <> name <> "`"+ source = maybe " (declaring registry)" (\url -> " from `" <> url <> "`") (ref ^. #source)+ interval = maybe "" (\range -> "; version >= `" <> range ^. #minimum <> "` and < `" <> range ^. #before <> "`") (ref ^. #versionRange)+ in linked <> source <> interval <> ". Selected from the source's default-branch HEAD; no historical tag search." renderDependencies :: T.Text -> [Dependency] -> T.Text renderDependencies emptyMessage [] = emptyMessage
test/Seihou/OKF/Docs/ModelSpec.hs view
@@ -5,6 +5,7 @@ import Data.List (find) import Data.Maybe (fromMaybe) import Data.Text qualified as T+import Data.Text.IO qualified as TIO import Seihou.OKF.Docs.Model import System.Directory (createDirectoryIfMissing) import System.FilePath ((</>))@@ -56,6 +57,18 @@ blueprint = requireEntry "app-blueprint" model (recipe ^. #moduleRefs) `shouldMatchList` [ModuleRef "base" True, ModuleRef "app" True] (blueprint ^. #moduleRefs) `shouldBe` [ModuleRef "base" True]++ it "retains an external base reference without fetching it or requiring installed members" $+ withFixtureRegistry $ \registryDir -> do+ let path = registryDir </> "recipes/app-recipe/recipe.dhall"+ TIO.writeFile path ("(" <> T.pack recipeDhall <> ") // {extends=Some {recipe=\"external\",source=Some \"https://example.invalid/base.git\",versionRange=Some {minimum=\"1\",before=\"2\"}}}")+ model <- shouldLoad registryDir+ case requireEntry "app-recipe" model ^. #baseRecipe of+ Nothing -> expectationFailure "missing base relationship"+ Just base -> do+ base ^. #resolved `shouldBe` False+ base ^. #reference . #source `shouldBe` Just "https://example.invalid/base.git"+ base ^. #reference . #recipe `shouldBe` "external" it "resolves an entailed edge naming a blueprint in the same registry" $ do withFixtureRegistry $ \registryDir -> do
test/Seihou/OKF/Docs/RenderSpec.hs view
@@ -1,6 +1,6 @@ module Seihou.OKF.Docs.RenderSpec (tests) where -import Control.Lens ((&), (?~), (^.))+import Control.Lens ((%~), (&), (.~), (?~), (^.)) import Data.Generics.Labels () import Data.List (sort) import Data.Map.Strict qualified as Map@@ -155,6 +155,29 @@ body <- requireBody "recipes/rich-recipe" testOptions richModel body `shouldSatisfy` T.isInfixOf "(with `license` = `MIT`, `project.name` = `demo`)" + it "documents an external settings-only base and explicit overrides without dangling links" $ do+ let ref = RecipeReference "external-base" (Just "https://example.test/base.git") (Just (RecipeVersionRange "1" "2"))+ model = derivedModel ref False+ (concepts, problems) <- renderOrFail testOptions model+ problems `shouldBe` []+ body <- requireBody "recipes/app-recipe" testOptions model+ body `shouldSatisfy` T.isInfixOf "## Extends"+ body `shouldSatisfy` T.isInfixOf "`external-base` from `https://example.test/base.git`"+ body `shouldSatisfy` T.isInfixOf "version >= `1` and < `2`"+ body `shouldSatisfy` T.isInfixOf "Uses its inherited members without adding modules."+ body `shouldSatisfy` T.isInfixOf "`external-member` (with `license` = `MIT`)"+ body `shouldSatisfy` T.isInfixOf "## Inherited prompt overrides"+ body `shouldNotSatisfy` T.isInfixOf "does not compose any modules"+ length concepts `shouldBe` length (wellFormedModel ^. #entries) + 1++ it "links a base known to be in the same registry" $ do+ let parent = recipeEntry & #name .~ "parent" & #path .~ "recipes/parent"+ model = derivedModel (RecipeReference "parent" Nothing Nothing) True & #entries %~ (<> [parent])+ (_, problems) <- renderOrFail testOptions model+ problems `shouldBe` []+ body <- requireBody "recipes/app-recipe" testOptions model+ body `shouldSatisfy` T.isInfixOf "[parent](/recipes/parent.md)"+ it "links an entailed edge whose blueprint is in the same registry" $ do body <- requireBody "blueprints/rich-blueprint" testOptions richModel body `shouldSatisfy` T.isInfixOf "Entails:"@@ -319,6 +342,7 @@ path = "modules/base", artifact = DocModuleArtifact (silentModuleArtifact artifactDescription), moduleRefs = [],+ baseRecipe = Nothing, entailedRefs = [] } @@ -359,6 +383,7 @@ path = path, artifact = DocModuleArtifact (moduleArtifact name refs), moduleRefs = refs,+ baseRecipe = Nothing, entailedRefs = [] } @@ -395,9 +420,13 @@ description = Just "Recipe", modules = [simpleDep "base", simpleDep "app"], vars = [],- prompts = []+ prompts = [],+ extends = Nothing,+ moduleOverrides = [],+ promptOverrides = [] }, moduleRefs = [ModuleRef "base" True, ModuleRef "app" True],+ baseRecipe = Nothing, entailedRefs = [] } @@ -428,6 +457,7 @@ versionProbe = Nothing }, moduleRefs = [ModuleRef "base" True],+ baseRecipe = Nothing, entailedRefs = [] } @@ -457,6 +487,7 @@ guidance = [] }, moduleRefs = [],+ baseRecipe = Nothing, entailedRefs = [] } @@ -482,6 +513,7 @@ path = "modules/rich", artifact = DocModuleArtifact richModule, moduleRefs = [],+ baseRecipe = Nothing, entailedRefs = [] } @@ -603,9 +635,13 @@ } ], vars = [],- prompts = []+ prompts = [],+ extends = Nothing,+ moduleOverrides = [],+ promptOverrides = [] }, moduleRefs = [ModuleRef "rich" True],+ baseRecipe = Nothing, entailedRefs = [] } @@ -622,6 +658,7 @@ moduleRefs = [], -- One entailed edge resolves inside this registry and one does not, which -- is the distinction the renderer has to honour.+ baseRecipe = Nothing, entailedRefs = [ EntailedRef {blueprint = "other-blueprint", from = "1.9.0", to = "2.0.0", resolved = True}, EntailedRef {blueprint = "kiroku", from = "1.9.0", to = "2.0.0", resolved = False}@@ -692,6 +729,7 @@ versionProbe = Nothing }, moduleRefs = [],+ baseRecipe = Nothing, entailedRefs = [] } @@ -736,5 +774,17 @@ launch = Nothing }, moduleRefs = [],+ baseRecipe = Nothing, entailedRefs = [] }++derivedModel :: RecipeReference -> Bool -> DocModel+derivedModel reference resolved = wellFormedModel & #entries %~ map derive+ where+ derive entry = case entry ^. #artifact of+ DocRecipeArtifact recipe ->+ entry+ & #artifact .~ DocRecipeArtifact (recipe & #modules .~ [] & #extends .~ Just reference & #moduleOverrides .~ [RecipeModuleOverride "external-member" (Map.singleton "license" "MIT")] & #promptOverrides .~ [Prompt "license" "Choose a license" Nothing Nothing])+ & #moduleRefs .~ []+ & #baseRecipe .~ Just (BaseRecipeRef reference resolved)+ _ -> entry