← Back to task

Commit 077459ab

commit 077459ab3136d765e096346645f4ce007714dc5f
Author: Coder Agent <coder@agents.omni>
Date:   Thu Feb 19 14:58:08 2026

    bild: propagate dep-commented module langdeps through hsGraph
    
    Task-Id: t-492

diff --git a/Omni/Bild.hs b/Omni/Bild.hs
index 449dc395..3a01e81f 100755
--- a/Omni/Bild.hs
+++ b/Omni/Bild.hs
@@ -190,6 +190,7 @@ main = Cli.Plan parser test_ pure |> Cli.main
           test_isGitIgnored,
           test_isGitHook,
           test_detectPythonImports,
+          test_detectHaskellImports,
           test_buildHsModuleGraph
         ]
 
@@ -742,10 +743,8 @@ analyzeOne namespace@(Namespace parts ext) = do
     Namespace.Hs ->
       contentLines
         |> Meta.detectAll "--"
-        |> \Meta.Parsed {..} ->
-          detectHaskellImports mempty contentLines +> \(autoDeps, srcs) -> do
-            let manualDeps = detectDepPrefixes "--" contentLines
-            let langdeps = autoDeps <> pdep <> manualDeps
+        |> \Meta.Parsed {pout, prun} ->
+          detectHaskellImports mempty contentLines +> \(langdeps, srcs) -> do
             graph <- buildHsModuleGraph namespace quapath srcs
             pure
               <| Just
@@ -951,10 +950,13 @@ detectHaskellImports _ contentLines = do
   root <- getCoderoot
   let initialMods = catMaybes (Regex.match haskellImports </ (Text.unpack </ contentLines))
   initialLocals <- toLocalFiles root initialMods
-  let initialLocalsSet = Set.fromList initialLocals
   let localMods = [m | m <- initialMods, (Namespace.fromHaskellModule m |> Namespace.toPath) `elem` initialLocals]
   let initialExternals = filter (`notElem` localMods) initialMods
-  (srcs, transitiveExtMods, transitiveDeps) <- bfs root initialLocalsSet Set.empty Set.empty Set.empty
+  let Meta.Parsed {pdep = parsedEntryDeps} = Meta.detectAll "--" contentLines
+  let entryCommentDeps = parsedEntryDeps <> detectDepPrefixes "--" contentLines
+  (entryLocalDeps, entryLangDeps) <- partitionHaskellDepComments root entryCommentDeps
+  let initialLocalsSet = Set.fromList initialLocals <> entryLocalDeps
+  (srcs, transitiveExtMods, transitiveDeps) <- bfs root initialLocalsSet Set.empty Set.empty entryLangDeps
   let allExtMods = Set.fromList initialExternals <> transitiveExtMods
   pkgSets <- Async.mapConcurrently ghcPkgFindModuleCached (Set.toList allExtMods)
   let pkgs = mconcat pkgSets <> transitiveDeps
@@ -975,15 +977,44 @@ detectHaskellImports _ contentLines = do
           let localsSet = Set.fromList locals
           let localModsFromPaths = Set.fromList [m | m <- mods, (Namespace.fromHaskellModule m |> Namespace.toPath) `elem` locals]
           let newExternals = Set.fromList mods Set.\\ localModsFromPaths
-          let newLocals = localsSet Set.\\ visited
-          let Meta.Parsed {pdep = fileDeps} = Meta.detectAll "--" fileLines
-          bfs root (queue' <> newLocals) (Set.insert rel visited) (extMods <> newExternals) (deps <> fileDeps)
+          let Meta.Parsed {pdep = parsedFileDeps} = Meta.detectAll "--" fileLines
+          let fileCommentDeps = parsedFileDeps <> detectDepPrefixes "--" fileLines
+          (localCommentDeps, langCommentDeps) <- partitionHaskellDepComments root fileCommentDeps
+          let seen = Set.insert rel visited
+          let newLocals = (localsSet <> localCommentDeps) Set.\\ seen
+          bfs root (queue' <> newLocals) seen (extMods <> newExternals) (deps <> langCommentDeps)
 
     toLocalFiles :: FilePath -> [String] -> IO [FilePath]
     toLocalFiles root mods = do
       let rels = map (Namespace.fromHaskellModule .> Namespace.toPath) mods
       filterM (\rel -> Dir.doesFileExist (root </> rel)) rels
 
+resolveLocalHaskellDep :: FilePath -> Meta.Dep -> IO (Maybe FilePath)
+resolveLocalHaskellDep root dep = do
+  let normalized = fromMaybe dep (List.stripPrefix "./" dep)
+  let depAsPathWithExt =
+        List.isSuffixOf ".hs" normalized
+          ?: (normalized, normalized <> ".hs")
+  let candidates =
+        [ normalized,
+          depAsPathWithExt,
+          Namespace.fromHaskellModule normalized |> Namespace.toPath
+        ]
+          |> List.nub
+  filterM (\rel -> Dir.doesFileExist (root </> rel)) candidates +> \existing ->
+    pure (head existing)
+
+partitionHaskellDepComments :: FilePath -> Set Meta.Dep -> IO (Set FilePath, Set Meta.Dep)
+partitionHaskellDepComments root deps =
+  foldM
+    ( \(localDeps, langDeps) dep ->
+        resolveLocalHaskellDep root dep +> \case
+          Just rel -> pure (Set.insert rel localDeps, langDeps)
+          Nothing -> pure (localDeps, Set.insert dep langDeps)
+    )
+    (Set.empty, Set.empty)
+    (Set.toList deps)
+
 detectDepPrefixes :: [Char] -> [Text] -> Set Meta.Dep
 detectDepPrefixes comment contentLines =
   let commentPrefix = Text.pack comment
@@ -1063,6 +1094,22 @@ test_detectPythonImports =
         Set.fromList ["Omni/Log.py"] @=? srcs
     ]
 
+test_detectHaskellImports :: Test.Tree
+test_detectHaskellImports =
+  Test.unit "collects deps from dep-commented local modules" <| do
+    root <- getCoderoot
+    let testDir = "_/tmp/bild-haskell-deps-test"
+    let depRelPath = testDir </> "Dep.hs"
+    let depAbsPath = root </> depRelPath
+    Dir.createDirectoryIfMissing True (takeDirectory depAbsPath)
+    writeFile depAbsPath (Text.unlines ["module Tmp.Dep where", "-- : dep sqlite-simple"])
+
+    let entryLines = ["module Tmp.Entry where", "-- : dep " <> Text.pack depRelPath]
+    (langdeps, srcs) <- detectHaskellImports mempty entryLines
+
+    Set.member depRelPath srcs @=? True
+    Set.member "sqlite-simple" langdeps @=? True
+
 test_buildHsModuleGraph :: Test.Tree
 test_buildHsModuleGraph =
   Test.group
@@ -1214,11 +1261,17 @@ buildHsModuleGraph namespace entryPoint deps = do
                 /> Text.lines
           let importedMods = catMaybes (Regex.match haskellImports </ (Text.unpack </ fileLines))
           localImportMods <- filterLocalImports root importedMods
+          let Meta.Parsed {pdep = parsedFileDeps} = Meta.detectAll "--" fileLines
+          let fileCommentDeps = parsedFileDeps <> detectDepPrefixes "--" fileLines
+          (localCommentDeps, _) <- partitionHaskellDepComments root fileCommentDeps
+          let localImportSet = Set.fromList (map Text.pack localImportMods)
+          let commentImportSet = Set.fromList (map pathToModuleName (Set.toList localCommentDeps))
+          let nodeImports = (localImportSet <> commentImportSet) |> Set.delete modName |> Set.toList
           let hasTH = detectTH fileLines
           let node =
                 HsModuleNode
                   { nodePath = srcPath,
-                    nodeImports = map Text.pack localImportMods,
+                    nodeImports = nodeImports,
                     nodeHasTH = hasTH
                   }
           pure (Map.insert modName node acc)