packages feed

seihou-cli 0.14.0.0 → 0.15.0.0

raw patch · 24 files changed

+152/−45 lines, 24 filesdep ~seihou-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: seihou-core

API changes (from Hackage documentation)

+ Seihou.CLI.ManifestCapabilityUpgrade: prepareRemovalPolicies :: Manifest -> Either Text Manifest

Files

data/module-reference.md view
@@ -220,3 +220,12 @@  Recipe inheritance producers require Seihou 0.14.0.0 or newer. Update the runtime before adopting descriptors that use `extends` or inherited overrides.++## Conditional removal in composed modules++`RemovalStep.when` uses the generation expression grammar and is evaluated against+the owning instance's recorded resolved variables. A base suppressing starter+outputs must apply the same condition to their cleanup steps. Omitted conditions+remain unconditional. Require Seihou 0.15.0.0+ before publishing these descriptors.+A recorded single-module root removes its unshared inherited instances in the same+operation; shared qualified instances remain owned by surviving applications.
data/setup-prompt.md view
@@ -123,9 +123,10 @@   - `-c CTX`: override context for config lookup   - `--no-commands`: skip shell command steps - `seihou remove MODULE [--dry-run] [--force]` — remove an applied module by executing its declared removal steps+  - A recorded single-module root also retires its unshared inherited modules; other applications retain shared instances.   - Managed-file removal needs a `removal` section; recorded seed-only/checkout-only owners can be removed without a custom spec. Seed retirement keeps project files. Removal stops future setup and does not undo arbitrary shell effects.   - `--dry-run`: preview the removal plan (Delete, Strip, Rewrite, Run, and protected Keep operations)-  - `--force`: skip confirmation prompts+  - `--force`: accept conflicted managed-file deletions; seeds remain protected - `seihou agent run BLUEPRINT [PROMPT] [--var K=V] [--no-baseline]` — run a blueprint   - Discovers the blueprint, resolves variables (same precedence chain as `seihou run`), optionally applies base modules, then starts the configured provider   - `--no-baseline`: skip applying declared base modules
help/manifest.md view
@@ -65,8 +65,9 @@       10 -> 11 precise seed ownership claims       11 -> 12 recipe inputs       12 -> 13 unknown recipe-chain history+      13 -> 14 conditional removal policies and recorded root retirement -    ✓ Upgraded .seihou/manifest.json to schema version 13.+    ✓ Upgraded .seihou/manifest.json to schema version 14.       Review the diff and commit it: git diff .seihou/manifest.json    Running it on a manifest that is already current reports that there is@@ -164,3 +165,19 @@   the old manifest. Metadata-only base changes need acceptance, and unchanged   application command receipts remain valid. Use the normal recipe update to   adopt unknown history; no manual manifest editing is required.++CONDITIONAL REMOVAL POLICIES++  Schema 14 records the conditional-removal/root-retirement boundary. Omitted+  removal conditions remain unconditional. Each application instance records+  removalPolicy with typed Boolean condition results at accepted run/update.+  The adjacent upgrade records null for unknown historical policies, changes no+  project files, and invents no result. A schema-13 document carrying policy+  evidence is rejected. Ordinary run/update/removal prepare this+  lossless boundary in memory; previews do not publish it. A conditional legacy+  application without accepted policy evidence needs its ordinary update first.++  A recorded single-module root retires unshared inherited modules in the same+  removal. Other applications keep shared qualified instances. Explicit+  additional-root compositions and recipe applications retain exact-instance+  selection. See seihou help modules.
help/modules.md view
@@ -127,3 +127,12 @@ working directories real and project-relative. Ordinary receipts never suppress checkout commands. `--no-commands` suppresses both lifecycles while retaining setup; a true no-op update executes neither. Removal accepts application commands only.++Removing a recorded application with one module root also retires its unshared+inherited modules in the same operation. Other applications keep shared qualified+instances. Seeds remain protected throughout inherited cleanup. Explicit compositions+with additional roots and recipe applications keep exact-instance selection.++Removal steps may declare `when`, using the generation expression grammar. It is+evaluated against that instance's saved resolved values. Older omitted conditions+remain unconditional. This policy requires the schema-14 runtime.
help/update.md view
@@ -93,9 +93,14 @@    Recipe applications also reuse recorded recipe inputs before member variables.   Inputs used only by conditions survive fresh clones. Ordinary preview prepares-  schema 13 as needed and adopts agreeing historical values; conflicting or newly+  schema 14 as needed and adopts agreeing historical values; conflicting or newly   required choices need --var KEY=VALUE or --reconfigure. Recipe-only changes are   recorded even when no generated file changes.++  Accepted update also records each instance's removal policy with typed Boolean+  condition results. Policy-only adoption appears in the preview and is a real+  update; repeating it with matching policies is a no-op. No manual manifest+  edits are needed.  MIGRATIONS 
help/workflow.md view
@@ -58,7 +58,9 @@   seihou remove NAME    Managed-file removal follows the module's removal declaration and protects-  conflicts. Recorded seed-only/checkout-only owners need no custom spec; seed+  conflicts. Removing a recorded single-module root also retires its unshared+  inherited modules; other applications keep shared instances. Recorded+  seed-only/checkout-only owners need no custom spec; seed   retirement preserves files. Status/diff count seeds without content comparisons.   Review changes and commit portable manifest/config, real source directories   and ignore rules. Accepted link adoption can stage tracked-link removals.
seihou-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: seihou-cli-version: 0.14.0.0+version: 0.15.0.0 synopsis: CLI for Seihou project scaffolding description:   Command-line interface for Seihou, a composable project scaffolding@@ -144,7 +144,7 @@     generic-lens >=2.2 && <3,     lens >=5.2 && <6,     process >=1.6 && <2,-    seihou-core ^>=0.14.0.0,+    seihou-core ^>=0.15.0.0,     streamly-core >=0.3 && <0.5,     temporary >=1.3 && <2,     text >=2.0 && <3,@@ -233,7 +233,7 @@     optparse-applicative >=0.18 && <1,     process >=1.6 && <2,     seihou-cli-internal,-    seihou-core ^>=0.14.0.0,+    seihou-core ^>=0.15.0.0,     temporary >=1.3 && <2,     text >=2.0 && <3,     time >=1.12 && <2,@@ -330,7 +330,7 @@     lens >=5.2 && <6,     process >=1.6 && <2,     seihou-cli-internal,-    seihou-core ^>=0.14.0.0,+    seihou-core ^>=0.15.0.0,     streamly-core >=0.3 && <0.5,     tasty >=1.4 && <2,     tasty-hspec >=1.2 && <2,
src-exe/Seihou/CLI/Commands.hs view
@@ -653,7 +653,9 @@           ( Just $               vsep                 [ pretty ("Removes a module that was previously applied via 'seihou run' and" :: String),-                  pretty ("reverses its declared effects. Managed files require a removal" :: String),+                  pretty ("reverses its declared effects. A recorded single-module root also" :: String),+                  pretty ("retires its unshared inherited modules. Shared instances remain." :: String),+                  pretty ("Managed files require a removal" :: String),                   pretty ("specification. Seed-only ownership can be retired without one; seed" :: String),                   pretty ("files and their deliberate deletion are preserved, even with --force." :: String),                   line,
src-exe/Seihou/CLI/Remove.hs view
@@ -13,7 +13,7 @@ import Data.Time.Clock (getCurrentTime) import Seihou.CLI.CheckoutSync (renderCheckoutPlan) import Seihou.CLI.Commands (RemoveOpts (..))-import Seihou.CLI.ManifestCapabilityUpgrade (prepareRecordedSeedOwnership)+import Seihou.CLI.ManifestCapabilityUpgrade (prepareRecordedSeedOwnership, prepareRemovalPolicies) import Seihou.CLI.Style (bold, dim, green, useColor, yellow) import Seihou.Composition.Instance (ModuleInstance (..), qualifiedName) import Seihou.Core.Checkout@@ -51,7 +51,8 @@   let matchingApps = Set.fromList [app ^. #applicationId | app <- originalManifest ^. #applications, maybe True (== app ^. #applicationId) (opts ^. #application), state <- app ^. #instances, modName == state ^. #name || modName == qualifiedName (ModuleInstance (state ^. #name) (state ^. #parentVars))]       legacyPaths = Set.fromList [path | (path, receipt) <- Map.toAscList (originalManifest ^. #seeds), LegacySeedOwners apps <- [receipt ^. #ownership], not (Set.null (apps `Set.intersection` matchingApps))]   prepared <- prepareRecordedSeedOwnership root paths legacyPaths originalManifest-  manifest <- case prepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate+  seedPrepared <- case prepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate+  manifest <- case prepareRemovalPolicies seedPrepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate   case selectRecordedRemoval manifest modName (opts ^. #application) of     Left err -> TIO.putStrLn (err ^. #message) >> exitFailure     Right (Just selected) -> handleCheckoutRemoval opts manifest selected >> exitWith ExitSuccess@@ -221,7 +222,14 @@ handleCheckoutRemoval opts manifest selected = do   root <- getCurrentDirectory   let candidate = selected ^. #candidateManifest-      retiredSeeds = [path | (path, receipt) <- Map.toAscList (manifest ^. #seeds), case receipt ^. #ownership of KnownSeedOwners owners -> Set.member (selected ^. #instanceName) (Map.findWithDefault Set.empty (selected ^. #applicationId) owners); _ -> False]+      ownersFor path source = case Map.lookup path (source ^. #seeds) of+        Just receipt -> case receipt ^. #ownership of KnownSeedOwners owners -> Map.findWithDefault Set.empty (selected ^. #applicationId) owners; _ -> Set.empty+        Nothing -> Set.empty+      retiredSeeds = [path | path <- Map.keys (manifest ^. #seeds), not (Set.null (ownersFor path manifest `Set.difference` ownersFor path candidate))]+      qualified applied = qualifiedName (ModuleInstance (applied ^. #name) (applied ^. #parentVars))+      retainedModules = Set.fromList (map qualified (candidate ^. #modules))+      inherited = [qualified applied | applied <- manifest ^. #modules, not (Set.member (qualified applied) retainedModules), qualified applied /= selected ^. #instanceName]+  mapM_ (\name -> TIO.putStrLn ("  Retire inherited module " <> name ^. #unModuleName <> ".")) inherited   mapM_ (\path -> TIO.putStrLn ("  Keep " <> T.pack path <> " (seed file; owned by the project)")) (take 5 retiredSeeds)   checked <- runEff $ runFilesystem $ runProcessIO $ prepareCheckoutPlan Reconcile ApplyDeclaration root manifest candidate []   plan <- case checked of
src-exe/Seihou/CLI/Run.hs view
@@ -64,6 +64,7 @@ import Seihou.Core.Checkout import Seihou.Core.CommandFingerprint (checkoutCommandsFromOperations, isCheckoutCommand) import Seihou.Core.Context (resolveContext)+import Seihou.Core.Expr (settleRemovalConditions) import Seihou.Core.Migration (MigrationPlan (..)) import Seihou.Core.Module (defaultSearchPaths, discoverRunnable) import Seihou.Core.Seed (renderSeedOwnershipError, replaceSelectedSeedClaims)@@ -513,7 +514,7 @@                                 -- Build updated manifest with all composed modules.                                 let orphanedPaths = map (^. #path) (diff ^. #orphaned)                                     cleanedFiles = foldr Map.delete (manifest ^. #files) orphanedPaths-                                    allModuleEntries = updateAllModules (manifest ^. #modules) originedModules now+                                    allModuleEntries = updateAllModules (manifest ^. #modules) originedModules resolved now                                     allResolvedVals =                                       Map.unions                                         [Map.map (^. #value) vs | vs <- Map.elems resolved]@@ -996,9 +997,10 @@ updateAllModules ::   [AppliedModule] ->   [(ModuleInstance, Module, ArtifactOrigin)] ->+  Map ModuleInstance (Map VarName ResolvedVar) ->   UTCTime ->   [AppliedModule]-updateAllModules existing modulesInOrder now =+updateAllModules existing modulesInOrder resolved now =   let composedKeys =         Set.fromList           [ (inst ^. #module_, inst ^. #parentVars)@@ -1012,7 +1014,7 @@               origin = origin,               moduleVersion = m ^. #version,               appliedAt = now,-              removal = m ^. #removal+              removal = settleRemovalConditions (Map.map (^. #value) (resolved Map.! inst)) <$> m ^. #removal             }         | (inst, m, origin) <- modulesInOrder         ]
src/Seihou/CLI/ManifestCapabilityUpgrade.hs view
@@ -31,6 +31,7 @@     prepareSeedOwnership,     prepareRecordedSeedOwnership,     prepareRecipeInheritance,+    prepareRemovalPolicies,     gatherSeedOwnershipEvidence,     CertificationScope (..),     EvidencePolicy (..),@@ -539,5 +540,13 @@ prepareRecipeInheritance manifest = do   (_, document) <-     first renderManifestUpgradeError $-      upgradeDocumentLosslessly (max (manifest ^. #version) (minimumManifestVersion RecipeInheritance)) (Aeson.toJSON manifest)+      upgradeDocumentLosslessly (max (manifest ^. #version) (max (minimumManifestVersion RecipeInheritance) (minimumManifestVersion ConditionalRemovalPolicies))) (Aeson.toJSON manifest)+  first T.pack (manifestFromJSON (Aeson.encode document))++-- | Stage the lossless policy boundary; dry-run callers never publish this candidate.+prepareRemovalPolicies :: Manifest -> Either Text Manifest+prepareRemovalPolicies manifest = do+  (_, document) <-+    first renderManifestUpgradeError $+      upgradeDocumentLosslessly (max (manifest ^. #version) (minimumManifestVersion ConditionalRemovalPolicies)) (Aeson.toJSON manifest)   first T.pack (manifestFromJSON (Aeson.encode document))
src/Seihou/CLI/SchemaVersion.hs view
@@ -9,11 +9,11 @@  -- | Raw URL for the seihou-schema package.dhall at a pinned commit schemaUrl :: Text-schemaUrl = "https://raw.githubusercontent.com/shinzui/seihou-schema/dbeff0dd67c15b7a1c57f90b23103afdc2fa8056/package.dhall"+schemaUrl = "https://raw.githubusercontent.com/shinzui/seihou-schema/e9f91438f081fba90f40f987de93df9f74c7117d/package.dhall"  -- | SHA256 integrity hash for the schema import schemaHash :: Text-schemaHash = "sha256:065c27b4d26059a7641e8ee7ee9d23ed32a698052c6aa8966426e6a678ee0e82"+schemaHash = "sha256:00f822087302b53f7ffe74e6505a633c59a0c2f206d12bfadb58a2fe211c0e40"  -- | Complete Dhall import line for use in generated modules schemaImportLine :: Text
src/Seihou/CLI/Update.hs view
@@ -65,6 +65,7 @@ import Seihou.Core.ArtifactRef (renderArtifactRefError, resolveArtifactOrigin) import Seihou.Core.Checkout import Seihou.Core.CommandFingerprint (checkoutCommandsFromOperations)+import Seihou.Core.Expr (settleRemovalConditions) import Seihou.Core.Module (defaultSearchPaths, discoverRunnable) import Seihou.Core.Seed (seedApplicationIds) import Seihou.Core.Types@@ -164,7 +165,7 @@                       selectedIdsForSeeds = Set.fromList (map (^. #candidate . #applicationId) plannedApplications)                       neededLegacy = Set.fromList [path | (path, receipt) <- Map.toAscList (base ^. #seeds), LegacySeedOwners apps <- [receipt ^. #ownership], Map.member path claims || not (Set.null (apps `Set.intersection` selectedIdsForSeeds))]                       relevant = not (Map.null claims) || any (\receipt -> not (Set.null (seedApplicationIds receipt `Set.intersection` selectedIdsForSeeds))) (Map.elems (base ^. #seeds))-                      structurallyPrepared = prepareManifestSchema manifestPath RecipeInheritance document base >>= \prepared -> if relevant then prepareManifestSchema manifestPath SeedOwnerClaims (Aeson.toJSON prepared) prepared else Right prepared+                      structurallyPrepared = prepareManifestSchema manifestPath ConditionalRemovalPolicies document base >>= \prepared -> if relevant then prepareManifestSchema manifestPath SeedOwnerClaims (Aeson.toJSON prepared) prepared else Right prepared                   paths <- defaultSearchPaths                   preparedResult <- case structurallyPrepared of                     Left err -> pure (Left err)@@ -908,7 +909,8 @@           parentVars = state ^. #parentVars,           origin = applied ^. #origin,           moduleVersion = applied ^. #moduleVersion,-          resolvedVars = state ^. #resolvedVars+          resolvedVars = state ^. #resolvedVars,+          removalPolicy = state ^. #removalPolicy         }  savedInstanceValues :: AppliedComposition -> SavedInstanceValues@@ -1313,7 +1315,7 @@               origin = publishedInstance application instanceId ^. #origin,               moduleVersion = modul ^. #version,               appliedAt = now,-              removal = modul ^. #removal+              removal = settleRemovalConditions (Map.map (^. #value) ((application ^. #resolvedValues) Map.! instanceId)) <$> modul ^. #removal             }         | (application, instanceId, modul) <- deduplicateInstances applications         ]
src/Seihou/CLI/Update/Render.hs view
@@ -50,6 +50,8 @@     ModuleLoadError (..),     ModuleName (..),     Operation (..),+    ParentVars,+    Removal,     SHA256 (..),     VarName (..),   )@@ -119,6 +121,9 @@     <> fromMaybe "<module>" target     <> "' to have an agent repair the manifest state and finish the upgrade.\n" +instancePolicies :: AppliedComposition -> [(ModuleName, ParentVars, Maybe Removal)]+instancePolicies application = [(state ^. #name, state ^. #parentVars, state ^. #removalPolicy) | state <- application ^. #instances]+ renderUpdateHuman :: Bool -> UpdateOutput -> Text renderUpdateHuman _ (UpdatePlanOutput (UpdatePlanView plan)) =   T.unlines $@@ -132,6 +137,7 @@            renderFiles (reconciliationSummary (plan ^. #reconciliation)),            if isUpdateNoOp plan then "Commands:    0 will run (no project update)" else renderCommands (summarizeCommandPlan (plan ^. #commandPlan))          ]+      <> ["Removal policies changed: " <> appliedTargetName (application ^. #candidate . #target) | application <- plan ^. #plannedApplications, maybe [] instancePolicies (application ^. #previous) /= instancePolicies (application ^. #candidate)]       <> ["Checkout declarations changed: " <> application ^. #candidate . #applicationId . #unApplicationId | application <- plan ^. #plannedApplications, maybe [] (^. #checkoutCommands) (application ^. #previous) /= application ^. #candidate . #checkoutCommands]       <> ["  checkout " <> (if isUpdateNoOp plan then "not-run-no-update" else dispositionText (command ^. #disposition)) <> ": " <> fromMaybe "" (commandText (command ^. #operation)) | command <- plan ^. #commandPlan . #commands, isCheckoutCommand (command ^. #operation)]       <> T.lines (renderCheckoutPlan True (plan ^. #checkoutPlan))
src/Seihou/CLI/Update/Types.hs view
@@ -365,3 +365,4 @@         && previous ^. #origin == candidate ^. #origin         && previous ^. #moduleVersion == candidate ^. #moduleVersion         && previous ^. #resolvedVars == candidate ^. #resolvedVars+        && previous ^. #removalPolicy == candidate ^. #removalPolicy
test/Seihou/CLI/ManifestCapabilityUpgradeSpec.hs view
@@ -114,7 +114,7 @@ spec :: Spec spec = do   describe "legacy seed ownership evidence" $ do-    let instanceState name = AppliedInstanceState name emptyParentVars (LocalOrigin (name ^. #unModuleName)) (Just "1.0.0") Map.empty+    let instanceState name = AppliedInstanceState name emptyParentVars (LocalOrigin (name ^. #unModuleName)) (Just "1.0.0") Map.empty Nothing         app = recordedApplication appA & #instances .~ map instanceState ["first", "second"]         receipt = SeedRecord "first" (LegacySeedOwners (Set.singleton appA)) SeedCreated testTime         old = emptyManifest testTime & #applications .~ [app] & #seeds .~ Map.singleton "a.txt" receipt@@ -294,7 +294,7 @@         case manifestFromJSON written of           Left err -> expectationFailure err           Right upgraded -> do-            (upgraded ^. #version) `shouldBe` ManifestSchemaVersion 13+            (upgraded ^. #version) `shouldBe` ManifestSchemaVersion 14             (^. #sharedWriteMode) <$> Map.lookup ".gitignore" (upgraded ^. #files)               `shouldBe` Just SharedWriteAdditiveOnly         TIO.readFile (fixture ^. #gitignorePath) `shouldReturn` gitignoreBefore
test/Seihou/CLI/ManifestRepairOriginsSpec.hs view
@@ -56,7 +56,7 @@           additionalModules = [],           namespace = Nothing,           context = Nothing,-          instances = [AppliedInstanceState "beta" emptyParentVars (origin localPath "beta") (Just "1.0.0") Map.empty],+          instances = [AppliedInstanceState "beta" emptyParentVars (origin localPath "beta") (Just "1.0.0") Map.empty Nothing],           commandReceipts = Map.empty,           checkoutCommands = [],           recipeInputs = Nothing,
test/Seihou/CLI/ManifestUpgradeSpec.hs view
@@ -264,12 +264,12 @@         outcome <- runManifestUpgrade (opts False False Nothing)         case outcome of           UpgradeWritten result -> do-            map stepVersions (result ^. #steps) `shouldBe` [(6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+            map stepVersions (result ^. #steps) `shouldBe` [(6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)]             (result ^. #entries) `shouldBe` []-            (result ^. #toVersion) `shouldBe` ManifestSchemaVersion 13+            (result ^. #toVersion) `shouldBe` ManifestSchemaVersion 14           other -> expectationFailure ("expected a write, got " <> show other)         document <- readDocument manifestPath-        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14)         lookupPath ["files", ".gitignore", "sharedWriteMode"] document `shouldBe` Just (Aeson.String "additive-only")         lookupPath ["files", "README.md", "sharedWriteMode"] document `shouldBe` Just (Aeson.String "unknown")         lookupPath ["files", ".gitignore", "additiveOnly"] document `shouldBe` Nothing@@ -281,7 +281,7 @@         before <- LBS.readFile manifestPath         outcome <- runManifestUpgrade (opts False False Nothing)         case outcome of-          UpgradeNotNeeded version _ -> version `shouldBe` ManifestSchemaVersion 13+          UpgradeNotNeeded version _ -> version `shouldBe` ManifestSchemaVersion 14           other -> expectationFailure ("expected nothing to do, got " <> show other)         LBS.readFile manifestPath `shouldReturn` before @@ -310,7 +310,7 @@         outcome <- runManifestUpgrade (opts True True Nothing)         case outcome of           UpgradeWouldWrite result [] -> do-            map stepVersions (result ^. #steps) `shouldBe` [(5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+            map stepVersions (result ^. #steps) `shouldBe` [(5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)]             map (^. #artifactName) (result ^. #entries)               `shouldBe` ["haskell-base", "project-lint", "haskell-service", "scratch-helper"]             let report = formatUpgradeReport result@@ -335,10 +335,10 @@         outcome <- runManifestUpgrade (opts False True Nothing)         case outcome of           UpgradeWritten result ->-            map stepVersions (result ^. #steps) `shouldBe` [(2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+            map stepVersions (result ^. #steps) `shouldBe` [(2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)]           other -> expectationFailure ("expected a write, got " <> show other)         document <- readDocument manifestPath-        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14)      it "rejects a target newer than this build or older than the document" $ do       withUpgradeProject schema6Manifest $ \_ ->@@ -366,7 +366,7 @@         output `shouldSatisfy` T.isInfixOf "5 -> 6"         output `shouldSatisfy` T.isInfixOf "6 -> 7"         document <- readDocument manifestPath-        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+        lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14)         lookupPath ["modules", "0", "origin"] document           `shouldBe` Just (Aeson.toJSON (RemoteOrigin "https://example.com/demo-modules.git" "demo" (Just "demo-modules")))         filter absoluteLooking (documentStrings document) `shouldBe` []
test/Seihou/CLI/RecipeExtensionE2ESpec.hs view
@@ -77,7 +77,7 @@       before <- readManifest root       case Aeson.eitherDecode before :: Either String Aeson.Value of         Right (Aeson.Object fields) -> do-          KM.lookup "version" fields `shouldBe` Just (Aeson.toJSON (13 :: Int))+          KM.lookup "version" fields `shouldBe` Just (Aeson.toJSON (14 :: Int))           T.pack (show fields) `shouldSatisfy` T.isInfixOf "recipeChain"           T.pack (show fields) `shouldSatisfy` T.isInfixOf "descriptorHash"         other -> expectationFailure (show other)
test/Seihou/CLI/SeedRunE2ESpec.hs view
@@ -204,3 +204,26 @@       D.removeFile (root </> "nested/start.txt")       _ <- success binary root ["run", "demo"]       TIO.readFile (root </> "nested/start.txt") `shouldReturn` "starter\n"++  it "removes a recorded seed root and its unshared inherited managed files in one command" $+    withSystemTempDirectory "seed-root-closure" $ \root -> do+      (binary, modul) <- prepare root+      let base = root </> ".seihou/modules/base"+      D.createDirectoryIfMissing True (base </> "files")+      TIO.writeFile (base </> "files/base") "managed base\n"+      TIO.writeFile (base </> "files/starter") "wrong base starter\n"+      TIO.writeFile (base </> "module.dhall") "let S = ../demo/schema/package.dhall in S.Module::{name=\"base\",version=Some \"1.0.0\",vars=[S.VarDecl::{name=\"base.starters\",type=\"bool\",default=Some \"true\"}],steps=[S.Step::{strategy=\"copy\",src=\"base\",dest=\"base.txt\"},S.Step::{strategy=\"copy\",src=\"starter\",dest=\"nested/start.txt\",when=Some \"Eq base.starters true\"}],removal=Some S.Removal::{steps=[S.RemovalStep::{action=\"remove-file\",dest=\"base.txt\"},S.RemovalStep::{action=\"remove-file\",dest=\"nested/start.txt\",when=Some \"Eq base.starters true\"}]}}"+      TIO.writeFile (modul </> "module.dhall") (body "seed" ",dependencies=[S.Dependency::{module=\"base\",vars=[{name=\"base.starters\",value=\"false\"}]}],removal=Some S.Removal::{steps=[S.RemovalStep::{action=\"remove-file\",dest=\"LICENSE\"}]}")+      _ <- success binary root ["run", "demo"]+      TIO.writeFile (root </> "nested/start.txt") "authored seed\n"+      before <- BS.readFile (root </> ".seihou/manifest.json")+      preview <- success binary root ["remove", "demo", "--dry-run"]+      preview `shouldSatisfy` T.isInfixOf "Retire inherited module base"+      BS.readFile (root </> ".seihou/manifest.json") `shouldReturn` before+      _ <- success binary root ["remove", "demo"]+      TIO.readFile (root </> "nested/start.txt") `shouldReturn` "authored seed\n"+      D.doesFileExist (root </> "base.txt") `shouldReturn` False+      D.doesFileExist (root </> "LICENSE") `shouldReturn` False+      after <- readManifest root+      after ^. #applications `shouldBe` []+      after ^. #modules `shouldBe` []
test/Seihou/CLI/StatusSpec.hs view
@@ -101,7 +101,8 @@           parentVars = emptyParentVars,           origin = LocalOrigin name,           moduleVersion = Just "1.0.0",-          resolvedVars = Map.empty+          resolvedVars = Map.empty,+          removalPolicy = Nothing         }  -- | Build a 'MigrationPlan' fixture for use with formatStatus.
test/Seihou/CLI/UpdateE2ESpec.hs view
@@ -237,7 +237,7 @@       expectSuccess "targeted update" exitCode stdoutText stderrText       stdoutText `shouldSatisfy` T.isInfixOf "\"outcome\":\"applied\""       manifestText <- TIO.readFile (fixture ^. #manifestPath)-      manifestText `shouldSatisfy` T.isInfixOf "\"version\":13"+      manifestText `shouldSatisfy` T.isInfixOf "\"version\":14"       manifestText `shouldSatisfy` T.isInfixOf "\"sharedWriteMode\":\"additive-only\""       -- Recording an answer must not touch a single byte of the project.       TIO.readFile (fixture ^. #gitignorePath) `shouldReturn` beforeGitignore@@ -276,7 +276,7 @@       dryOut `shouldSatisfy` T.isInfixOf "\"outcome\":\"plan\""       dryOut `shouldSatisfy` T.isInfixOf "\"alreadyUpToDate\":false"       dryOut `shouldSatisfy` T.isInfixOf "\"fromSchema\":6"-      dryOut `shouldSatisfy` T.isInfixOf "\"toSchema\":13"+      dryOut `shouldSatisfy` T.isInfixOf "\"toSchema\":14"       dryOut `shouldSatisfy` T.isInfixOf "{\"from\":\"unknown\",\"path\":\".gitignore\",\"to\":\"additive-only\"}"       dryOut `shouldNotSatisfy` T.isInfixOf "shared_path_requires_applications"       LBS.readFile (fixture ^. #manifestPath) `shouldReturn` beforeManifest@@ -297,7 +297,7 @@       case decoded of         Left err -> expectationFailure err         Right manifest -> do-          (manifest ^. #version) `shouldBe` ManifestSchemaVersion 13+          (manifest ^. #version) `shouldBe` ManifestSchemaVersion 14           case Map.lookup ".gitignore" (manifest ^. #files) of             Nothing -> expectationFailure "the manifest lost .gitignore"             Just record -> do@@ -317,7 +317,7 @@       binary <- seihouBinary       (dryExit, dryOut, dryErr) <- runSeihouShared binary bug1 ["update", "alpha", "--dry-run"]       expectSuccess "human dry run" dryExit dryOut dryErr-      dryOut `shouldSatisfy` T.isInfixOf "Manifest:    schema 6 -> 13"+      dryOut `shouldSatisfy` T.isInfixOf "Manifest:    schema 6 -> 14"       dryOut `shouldSatisfy` T.isInfixOf ".gitignore evidence unknown -> additive-only"       (dryOut <> dryErr) `shouldNotSatisfy` leaksInternals 
test/Seihou/CLI/UpdateSpec.hs view
@@ -12,7 +12,7 @@ where  import Control.Exception (bracket)-import Control.Lens ((&), (.~), (^.))+import Control.Lens ((%~), (&), (.~), (^.)) import Control.Monad (forM_) import Data.ByteString.Lazy qualified as LBS import Data.Foldable (traverse_)@@ -372,6 +372,15 @@       isUpdateNoOp (unchangedFilePlan SharedWriteAdditiveOnly True) `shouldBe` True       isUpdateNoOp (unchangedFilePlan SharedWriteRequiresOwnershipClosure False) `shouldBe` True +    it "accepts removal-policy-only adoption and then returns to a true no-op" $ do+      let previous = application (AppliedModuleTarget "base") [instanceState "base"]+          policy = Removal [RemovalStep RemoveFileAction "starter" Nothing (Just (ExprLit False))] []+          candidate = previous & #instances %~ map (\state -> state & #removalPolicy .~ Just policy)+          planned before after = PlannedApplication (Just before) after [] Map.empty [] Map.empty+          plan before after = unchangedFilePlan SharedWriteAdditiveOnly True & #plannedApplications .~ [planned before after] & #applications .~ [after]+      isUpdateNoOp (plan previous candidate) `shouldBe` False+      isUpdateNoOp (plan candidate candidate) `shouldBe` True+     it "rejects a plan when its manifest snapshot changes" $       withSystemTempDirectory "seihou-update-stale" $ \root -> do         fixture <- prepareUpdateFixture root@@ -584,7 +593,7 @@       withSystemTempDirectory "seihou-update-schema-5" $ \root -> do         fixture <- prepareSharedPathFixture CoOwnerAppends root         manifestText <- TIO.readFile (fixture ^. #manifestPath)-        TIO.writeFile (fixture ^. #manifestPath) (T.replace "\"version\":13" "\"version\":5" manifestText)+        TIO.writeFile (fixture ^. #manifestPath) (T.replace "\"version\":14" "\"version\":5" manifestText)         withSavedEnv "XDG_CONFIG_HOME" (Just (fixture ^. #xdgHome)) $           withCurrentDirectory (fixture ^. #projectRoot) $ do             forM_ [NamedUpdateTargets ["alpha"], AllRecordedApplications] $ \selection -> do@@ -617,7 +626,7 @@             parentTwo = ParentVars (Map.singleton "tenant" "two")             instanceOne = ModuleInstance "shared" parentOne             instanceTwo = ModuleInstance "shared" parentTwo-            stateFor parent = AppliedInstanceState "shared" parent (LocalOrigin "shared") (Just "1.0.0") Map.empty+            stateFor parent = AppliedInstanceState "shared" parent (LocalOrigin "shared") (Just "1.0.0") Map.empty Nothing             previous = application (AppliedModuleTarget "shared") [stateFor parentOne, stateFor parentTwo]             candidate =               Module@@ -1195,7 +1204,8 @@       parentVars = emptyParentVars,       origin,       moduleVersion = Just "1.0.0",-      resolvedVars = Map.empty+      resolvedVars = Map.empty,+      removalPolicy = Nothing     }  moduleDhall :: Text -> Text -> Text
test/Seihou/CLI/UpgradeDiagnosisSpec.hs view
@@ -43,7 +43,7 @@       withFixture CoOwnerAppendsPredatingEvidence noDamage $ \_ _ diagnose -> do         checks <- readiness <$> diagnose "alpha"         statusOf "manifest-schema-current" checks `shouldBe` NeedsAttention-        detailOf "manifest-schema-current" checks `shouldSatisfy` T.isInfixOf "schema 6 is older than 13"+        detailOf "manifest-schema-current" checks `shouldSatisfy` T.isInfixOf "schema 6 is older than 14"         statusOf "shared-evidence-known" checks `shouldBe` NeedsAttention         detailOf "shared-evidence-known" checks `shouldBe` ".gitignore is shared with beta and its write mode is unknown"         statusOf "update-plans-cleanly" checks `shouldBe` Ready@@ -59,7 +59,7 @@         ((^. #errorCode) <$> diagnosis ^. #updateDryRun) `shouldBe` ProbeOk (Just "shared_write_evidence_unavailable")      it "reports a schema-5 manifest as needing seihou manifest upgrade" $-      withFixture CoOwnerAppends (withManifestText (T.replace "\"version\":13" "\"version\":5")) $ \_ _ diagnose -> do+      withFixture CoOwnerAppends (withManifestText (T.replace "\"version\":14" "\"version\":5")) $ \_ _ diagnose -> do         diagnosis <- diagnose "alpha"         let checks = readiness diagnosis         statusOf "manifest-readable" checks `shouldBe` NeedsAttention@@ -154,7 +154,7 @@       [ ("healthy" :: Text, CoOwnerAppends, const (pure ())),         ("schema 6", CoOwnerAppendsPredatingEvidence, const (pure ())),         ("evidence unavailable", CoOwnerAppendsPredatingEvidence, evidenceUnavailable),-        ("schema 5", CoOwnerAppends, withManifestText (T.replace "\"version\":13" "\"version\":5")),+        ("schema 5", CoOwnerAppends, withManifestText (T.replace "\"version\":14" "\"version\":5")),         ("not json", CoOwnerAppends, withManifestText (const "{not json")),         ("local origin", CoOwnerAppends, localBetaOrigin),         ("pending transaction", CoOwnerAppends, pendingTransaction)