@@ -107,56 +107,33 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
107107 */
108108 public ServiceDetectionResult detect (List <CodeNode > nodes , List <CodeEdge > edges ,
109109 String projectDir , Path projectRoot ) {
110- // 1. Find module boundaries by scanning node file paths for build files
111- // Use TreeMap for deterministic ordering (sorted by directory path)
110+ // 1. Find module boundaries by scanning the filesystem for build files.
111+ // This is more reliable than scanning node file paths, which may miss
112+ // modules where no detector created a node from the build file.
112113 Map <String , ModuleInfo > modules = new TreeMap <>();
113114
115+ if (projectRoot != null ) {
116+ // Scan filesystem directly for build files (most reliable)
117+ scanFilesystemForBuildFiles (projectRoot , projectRoot , modules );
118+ }
119+
120+ // Fallback: also scan node file paths in case filesystem scan missed anything
114121 for (CodeNode node : nodes ) {
115122 String filePath = node .getFilePath ();
116123 if (filePath == null ) continue ;
117124
118125 String fileName = Path .of (filePath ).getFileName ().toString ();
119126 String dirPath = parentDir (filePath );
120127
121- // Check known build files
122128 String buildTool = BUILD_FILES .get (fileName );
123129 if (buildTool != null ) {
124- // For Python: only register if no better build tool already present
125- ModuleInfo existing = modules .get (dirPath );
126- if (existing != null && isPythonTool (buildTool ) && !isPythonTool (existing .buildTool ())) {
127- continue ; // Don't override a non-Python build tool with Python
128- }
129- // For Docker: only register if no other build tool present
130- if ("docker" .equals (buildTool ) && existing != null ) {
131- // Add docker as supplemental info but don't override
132- continue ;
133- }
134- // For Python files: prefer higher-priority ones
135- if (isPythonTool (buildTool ) && existing != null && isPythonTool (existing .buildTool ())) {
136- if (pythonPriority (fileName ) >= pythonPriority (existing .buildFile ())) {
137- continue ; // Current is same or lower priority
138- }
139- }
140- modules .put (dirPath , new ModuleInfo (dirPath , buildTool , fileName ));
130+ registerModule (modules , dirPath , buildTool , fileName );
141131 }
142- // Check .csproj files
143132 if (fileName .endsWith (CSPROJ_EXTENSION )) {
144133 modules .putIfAbsent (dirPath , new ModuleInfo (dirPath , "dotnet" , fileName ));
145134 }
146135 }
147136
148- // 1b. Check for Dockerfile as supplemental indicator -- create service
149- // only if no other build file was found for that directory
150- for (CodeNode node : nodes ) {
151- String filePath = node .getFilePath ();
152- if (filePath == null ) continue ;
153- String fileName = Path .of (filePath ).getFileName ().toString ();
154- if ("Dockerfile" .equals (fileName )) {
155- String dirPath = parentDir (filePath );
156- modules .putIfAbsent (dirPath , new ModuleInfo (dirPath , "docker" , fileName ));
157- }
158- }
159-
160137 // 2. If no modules detected, create one service for the whole project
161138 if (modules .isEmpty ()) {
162139 modules .put ("" , new ModuleInfo ("" , "unknown" , "" ));
@@ -263,6 +240,68 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
263240 return new ServiceDetectionResult (serviceNodes , serviceEdges );
264241 }
265242
243+ /**
244+ * Scan the filesystem recursively for build files that indicate service/module boundaries.
245+ * More reliable than scanning node file paths since not all build files produce CodeNodes.
246+ */
247+ private void scanFilesystemForBuildFiles (Path root , Path projectRoot , Map <String , ModuleInfo > modules ) {
248+ try (var stream = Files .walk (root , 10 )) {
249+ stream .filter (Files ::isRegularFile )
250+ .filter (p -> {
251+ String name = p .getFileName ().toString ();
252+ return BUILD_FILES .containsKey (name ) || name .endsWith (CSPROJ_EXTENSION );
253+ })
254+ .sorted () // deterministic
255+ .forEach (p -> {
256+ String name = p .getFileName ().toString ();
257+ String relDir = projectRoot .relativize (p .getParent ()).toString ()
258+ .replace ('\\' , '/' );
259+ if (relDir .equals ("." )) relDir = "" ;
260+
261+ // Skip node_modules, .git, target, build directories
262+ if (relDir .contains ("node_modules" ) || relDir .contains (".git/" )
263+ || relDir .contains ("/target/" ) || relDir .startsWith ("target/" )
264+ || relDir .contains ("/build/" ) || relDir .startsWith ("build/" )) {
265+ return ;
266+ }
267+
268+ if (name .endsWith (CSPROJ_EXTENSION )) {
269+ modules .putIfAbsent (relDir , new ModuleInfo (relDir , "dotnet" , name ));
270+ } else {
271+ String buildTool = BUILD_FILES .get (name );
272+ if (buildTool != null ) {
273+ registerModule (modules , relDir , buildTool , name );
274+ }
275+ }
276+ });
277+ } catch (IOException e ) {
278+ log .warn ("Could not scan filesystem for build files: {}" , e .getMessage ());
279+ }
280+ }
281+
282+ /**
283+ * Register a module, respecting priority rules for Python/Docker.
284+ */
285+ private void registerModule (Map <String , ModuleInfo > modules , String dirPath ,
286+ String buildTool , String fileName ) {
287+ ModuleInfo existing = modules .get (dirPath );
288+ // Python doesn't override non-Python
289+ if (existing != null && isPythonTool (buildTool ) && !isPythonTool (existing .buildTool ())) {
290+ return ;
291+ }
292+ // Docker doesn't override anything
293+ if ("docker" .equals (buildTool ) && existing != null ) {
294+ return ;
295+ }
296+ // Python priority: pyproject.toml > setup.py > requirements.txt > manage.py
297+ if (isPythonTool (buildTool ) && existing != null && isPythonTool (existing .buildTool ())) {
298+ if (pythonPriority (fileName ) >= pythonPriority (existing .buildFile ())) {
299+ return ;
300+ }
301+ }
302+ modules .put (dirPath , new ModuleInfo (dirPath , buildTool , fileName ));
303+ }
304+
266305 /**
267306 * Extract service name from build file contents if possible, otherwise use directory name.
268307 */
0 commit comments