This document describes the implementation of multi-path support in DependencyParser and RepoAnalyzer, allowing the analysis of multiple source directories with proper component namespacing and cross-repository dependency resolution.
When parsing multiple repositories, each component is namespaced to prevent ID collisions:
Format: {namespace}.{original_component_id}
Example:
Single path: "src.components.Button.render"
Multi-path: "openframe-frontend.src.components.Button.render"
The namespace is derived from the repository directory name (e.g., openframe-frontend, ui-kit).
Dependencies are resolved in three stages:
- Intra-namespace: Dependencies within the same repository use namespaced IDs
- Cross-namespace: Dependencies between repositories are resolved by component name matching
- Fallback: Unresolved dependencies retain their original ID for debugging
Before:
def __init__(self, repo_path: str, include_patterns: List[str] = None, exclude_patterns: List[str] = None):
self.repo_path = os.path.abspath(repo_path)After:
def __init__(self, repo_path: Union[str, List[str]], include_patterns: List[str] = None, exclude_patterns: List[str] = None):
if isinstance(repo_path, str):
self.repo_paths = [os.path.abspath(repo_path)]
self.repo_path = self.repo_paths[0] # Backward compatibility
else:
self.repo_paths = [os.path.abspath(p) for p in repo_path]
self.repo_path = self.repo_paths[0]Backward Compatibility:
- Single string path: Works exactly as before
- List of paths: New multi-path functionality
self.repo_pathmaintained for legacy code
- Extracts original
parse_repositorylogic - Maintains backward compatibility
- No changes to component IDs
- Orchestrates parsing of multiple repositories
- Creates namespace for each repository
- Merges components with namespaced IDs
- Resolves cross-namespace dependencies
- Generates namespace from directory name
- Example:
/path/to/ui-kit→"ui-kit"
- Builds components with namespace prefix
- Maintains mapping of original → namespaced IDs
- Tracks modules with namespaced paths
- Resolves dependencies across repository boundaries
- Uses name matching for cross-namespace resolution
- Preserves unresolved dependencies for debugging
- Now dispatches to single or multi-path parsing
- Returns unified component dictionary
Added:
self._namespace_roots: Dict[str, str] = {} # namespace -> repo_pathTracks which namespace corresponds to which repository path.
Before:
def analyze_repository_structure(self, repo_dir: str) -> Dict:
file_tree = self._build_file_tree(repo_dir)
return {
"file_tree": file_tree,
"summary": {...}
}After:
def analyze_repository_structure(self, repo_dir: Union[str, List[str]]) -> Dict:
if isinstance(repo_dir, str):
# Single path - backward compatible
return self._analyze_single_repository(repo_dir)
else:
# Multiple paths
return self._analyze_multiple_repositories(repo_dir)- Analyzes each repository separately
- Wraps each tree with namespace prefix
- Merges trees into single structure
- Combines summary statistics
- Same as DependencyParser implementation
- Ensures consistent namespacing
from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser
# Old code continues to work
parser = DependencyParser("/path/to/repo")
components = parser.parse_repository()
# Component IDs unchanged
# "src.utils.helpers.formatDate"from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser
# New multi-path support
paths = [
"/path/to/openframe-frontend",
"/path/to/ui-kit",
"/path/to/shared-lib"
]
parser = DependencyParser(paths, include_patterns=["*.py", "*.ts"])
components = parser.parse_repository()
# Component IDs are namespaced
# "openframe-frontend.src.utils.helpers.formatDate"
# "ui-kit.components.Button.render"
# "shared-lib.utils.api.fetchData"from codewiki.src.be.dependency_analyzer.analysis.repo_analyzer import RepoAnalyzer
analyzer = RepoAnalyzer(
include_patterns=["*.py", "*.ts"],
exclude_patterns=["*test*", "*spec*"]
)
# Single path
result = analyzer.analyze_repository_structure("/path/to/repo")
# Multiple paths
result = analyzer.analyze_repository_structure([
"/path/to/repo1",
"/path/to/repo2"
])
# File tree structure
{
"file_tree": {
"type": "directory",
"name": "multi-repo",
"children": [
{
"type": "directory",
"name": "repo1",
"_namespace": "repo1",
"children": [...]
},
{
"type": "directory",
"name": "repo2",
"_namespace": "repo2",
"children": [...]
}
]
},
"summary": {
"total_files": 150,
"total_size_kb": 2048.5,
"repositories": 2,
"namespaces": ["repo1", "repo2"]
}
}{module_path}.{component_name}
Examples:
- src.components.Button.render
- lib.utils.helpers.formatDate
- services.api.UserService.getUser
{namespace}.{module_path}.{component_name}
Examples:
- openframe-frontend.src.components.Button.render
- ui-kit.lib.utils.helpers.formatDate
- shared-lib.services.api.UserService.getUser
When a component in one namespace depends on a component in another:
# Component in openframe-frontend
{
"id": "openframe-frontend.src.App.main",
"depends_on": [
"openframe-frontend.src.utils.logger", # Same namespace
"ui-kit.components.Button" # Cross-namespace
]
}Resolution process:
- Check if dependency ID exists in
all_components - If not found, extract component name and search by name across namespaces
- If still not found, keep original ID for debugging
class DependencyParser:
repo_paths: List[str] # All repository paths
repo_path: str # First path (backward compat)
components: Dict[str, Node] # All components (namespaced in multi-path)
modules: Set[str] # All module paths (namespaced in multi-path)
include_patterns: List[str] # File include patterns
exclude_patterns: List[str] # File exclude patterns
analysis_service: AnalysisServiceclass RepoAnalyzer:
include_patterns: List[str]
exclude_patterns: List[str]
_namespace_roots: Dict[str, str] # Maps namespace to repo_path- Empty repositories: Skipped in multi-path mode
- Duplicate namespaces: Last one wins (deterministic)
- Circular dependencies: Preserved as-is, no cycle detection
- Missing dependencies: Kept with original ID for debugging
- Relative paths: Maintained relative to original repository
- Time Complexity: O(n * m) where n = number of repos, m = avg components per repo
- Space Complexity: O(n * m) for component storage
- Namespace Mapping: O(k) where k = total unique component IDs
parser = DependencyParser("/path/to/repo")
assert len(parser.repo_paths) == 1
assert parser.repo_path == parser.repo_paths[0]paths = ["/repo1", "/repo2"]
parser = DependencyParser(paths)
assert len(parser.repo_paths) == 2
components = parser.parse_repository()
# Check namespaced IDs
assert any("repo1." in comp_id for comp_id in components.keys())
assert any("repo2." in comp_id for comp_id in components.keys())parser = DependencyParser(["/path/to/openframe-frontend"])
namespace = parser._get_namespace_from_path("/path/to/openframe-frontend")
assert namespace == "openframe-frontend"# No changes needed - works as before
parser = DependencyParser(repo_path)
components = parser.parse_repository()# Update to use list of paths
parser = DependencyParser([repo_path1, repo_path2])
components = parser.parse_repository()
# Component IDs now include namespace prefix
for comp_id, component in components.items():
print(f"Component: {comp_id}") # e.g., "ui-kit.components.Button"/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/ast_parser.py(355 lines)/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py(178 lines)
- Namespace from directory name: Simple, predictable, human-readable
- Backward compatibility preserved: Single string path works unchanged
- Cross-namespace resolution by name: Flexible but may match wrong component in ambiguous cases
- No breaking changes: All existing code continues to work
- Custom namespace mapping (instead of directory name)
- More sophisticated cross-namespace dependency resolution
- Namespace aliases for shorter IDs
- Component visibility controls (public/private across namespaces)
- Performance optimization for very large multi-repo setups