graph-dependency-analyzer 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. src/presentation/api/rest/routers/relationships.py +1089 -0
@@ -0,0 +1,198 @@
1
+ """
2
+ Groovy / Jenkinsfile parser — extracts pipeline definitions, shared library
3
+ functions, stage names, and library references.
4
+
5
+ Handles:
6
+ - Jenkins Shared Library vars/*.groovy (def call(...) functions)
7
+ - Jenkinsfile / pipeline_config.groovy (pipeline{}, libraries{}, stages)
8
+ - JTE (Jenkins Templating Engine) configs
9
+
10
+ Extracts:
11
+ - Function definitions (def call, def myFunc)
12
+ - Library references (libraries { helm_deploy() }, @Library('name'))
13
+ - Stage names (stage('Build') { ... })
14
+ - Shell commands with service/image names
15
+ - Environment variables and credentials references
16
+ - evaluate(new File("other.groovy")) — local lib imports
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from pathlib import Path
22
+
23
+ from src.domain.repositories.i_code_parser import ParseResult
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Patterns
28
+ # ---------------------------------------------------------------------------
29
+
30
+ # def call(Map config) / def call() / async def myFunc(x)
31
+ _RE_DEF = re.compile(r'\bdef\s+(\w+)\s*\(', re.M)
32
+
33
+ # @Library('jenkins-shared-library') / @Library(['lib1','lib2'])
34
+ _RE_AT_LIBRARY = re.compile(r"@Library\s*\(\s*['\"]([^'\"]+)['\"]", re.I)
35
+
36
+ # libraries { helm_deploy() } — JTE style
37
+ _RE_LIBRARIES_BLOCK = re.compile(r'libraries\s*\{([^}]{0,2000})\}', re.S)
38
+ _RE_LIBRARY_ENTRY = re.compile(r'(\w[\w-]*)\s*\{', re.M) # name { ... }
39
+
40
+ # stage('Build') { ... }
41
+ _RE_STAGE = re.compile(r"stage\s*\(\s*['\"]([^'\"]+)['\"]", re.I)
42
+
43
+ # evaluate(new File("other.groovy"))
44
+ _RE_EVALUATE = re.compile(r'evaluate\s*\(\s*new\s+File\s*\(\s*["\']([^"\']+)["\']', re.I)
45
+
46
+ # pipeline_template = "Jenkinsfile_services_ci"
47
+ _RE_TEMPLATE = re.compile(r'pipeline_template\s*=\s*["\']([^"\']+)["\']', re.I)
48
+
49
+ # sh "docker build ... service-name ..." — extract image/service hints
50
+ _RE_SH_IMAGE = re.compile(r'\bsh\s+["\'].*?([\w-]+-service[\w-]*|[\w-]+-app[\w-]*)["\']', re.I)
51
+
52
+ # credentialsId: "${config.xxx}" or 'my-credential-id'
53
+ _RE_CRED = re.compile(r'credentialsId\s*[=:]\s*["\']([^"\'$]{3,60})["\']', re.I)
54
+
55
+ # withCredentials, withAWS, azureKeyVault — external service hints
56
+ _RE_EXT_SERVICE = re.compile(
57
+ r'\b(withCredentials|azureKeyVault|withAWS|dockerRegistry|artifactory|helm|kubectl|az\s+\w+)\s*[\(\{]',
58
+ re.I,
59
+ )
60
+
61
+
62
+ def _line_of(content: str, pos: int) -> int:
63
+ return content[:pos].count('\n') + 1
64
+
65
+
66
+ class GroovyParser:
67
+ """Parser for Groovy/Jenkinsfile source files."""
68
+
69
+ GROOVY_EXTENSIONS = {'.groovy', '.Jenkinsfile', ''} # empty for bare 'Jenkinsfile'
70
+
71
+ def parse_file(self, file_path: Path) -> ParseResult:
72
+ suffix = file_path.suffix.lower()
73
+ name = file_path.name
74
+
75
+ # Accept .groovy files and bare Jenkinsfile / pipeline_config
76
+ is_groovy = (
77
+ suffix == '.groovy'
78
+ or name in ('Jenkinsfile', 'Jenkinsfile_ci', 'Jenkinsfile_cd',
79
+ 'pipeline_config', 'pipeline_properties')
80
+ or name.startswith('Jenkinsfile')
81
+ )
82
+ if not is_groovy:
83
+ return ParseResult(file_path=file_path)
84
+
85
+ if not file_path.exists():
86
+ return ParseResult(file_path=file_path)
87
+
88
+ try:
89
+ content = file_path.read_text(encoding='utf-8', errors='replace')
90
+ except Exception:
91
+ return ParseResult(file_path=file_path)
92
+
93
+ # ── Module name ────────────────────────────────────────────────────
94
+ stem = file_path.stem
95
+ module_name = stem
96
+
97
+ # ── Classes = pipeline definitions / shared-lib files ─────────────
98
+ classes = []
99
+
100
+ # One class per file representing the pipeline/lib file itself
101
+ classes.append({
102
+ 'name': stem,
103
+ 'qualified_name': module_name,
104
+ 'node_type': 'CLASS',
105
+ 'line': 1,
106
+ 'metadata': {
107
+ 'language': 'groovy',
108
+ 'file_type': 'jenkinsfile' if 'Jenkinsfile' in name else 'shared_lib',
109
+ 'file_path': str(file_path),
110
+ }
111
+ })
112
+
113
+ # ── Methods = def call / def xxx ───────────────────────────────────
114
+ methods = []
115
+ for m in _RE_DEF.finditer(content):
116
+ fn = m.group(1)
117
+ methods.append({
118
+ 'name': fn,
119
+ 'qualified_name': f"{module_name}.{fn}",
120
+ 'node_type': 'METHOD',
121
+ 'line': _line_of(content, m.start()),
122
+ 'metadata': {
123
+ 'language': 'groovy',
124
+ 'file_path': str(file_path),
125
+ }
126
+ })
127
+
128
+ # ── Imports = @Library + evaluate() + libraries{} block ───────────
129
+ imports = []
130
+ seen: set[str] = set()
131
+
132
+ def add_import(lib: str, line: int, source: str):
133
+ lib = lib.strip()
134
+ if lib and lib not in seen:
135
+ seen.add(lib)
136
+ imports.append({
137
+ 'import_path': lib,
138
+ 'line': line,
139
+ 'metadata': {'source': source},
140
+ })
141
+
142
+ # @Library('jenkins-shared-library')
143
+ for m in _RE_AT_LIBRARY.finditer(content):
144
+ add_import(m.group(1), _line_of(content, m.start()), '@Library')
145
+
146
+ # evaluate(new File("serviceUtil.groovy"))
147
+ for m in _RE_EVALUATE.finditer(content):
148
+ lib = m.group(1).replace('.groovy', '')
149
+ add_import(lib, _line_of(content, m.start()), 'evaluate')
150
+
151
+ # libraries { helm_deploy { } docker_build { } }
152
+ for block in _RE_LIBRARIES_BLOCK.finditer(content):
153
+ block_text = block.group(1)
154
+ for entry in _RE_LIBRARY_ENTRY.finditer(block_text):
155
+ name_candidate = entry.group(1)
156
+ if name_candidate not in ('jte', 'pipeline', 'stage', 'steps', 'post', 'environment'):
157
+ add_import(name_candidate, _line_of(content, block.start()), 'libraries{}')
158
+
159
+ # ── Method calls = stages + external services ─────────────────────
160
+ method_calls = []
161
+
162
+ # Stage names → useful for understanding pipeline structure
163
+ for m in _RE_STAGE.finditer(content):
164
+ method_calls.append({
165
+ 'callee': f"stage.{m.group(1).replace(' ','_')}",
166
+ 'line': _line_of(content, m.start()),
167
+ 'metadata': {'stage_name': m.group(1)},
168
+ })
169
+
170
+ # pipeline_template reference
171
+ for m in _RE_TEMPLATE.finditer(content):
172
+ method_calls.append({
173
+ 'callee': f"pipeline_template.{m.group(1)}",
174
+ 'is_api_call': False,
175
+ 'line': _line_of(content, m.start()),
176
+ })
177
+
178
+ # External service hints (Azure, Helm, kubectl, Artifactory)
179
+ for m in _RE_EXT_SERVICE.finditer(content):
180
+ method_calls.append({
181
+ 'callee': m.group(1).strip(),
182
+ 'line': _line_of(content, m.start()),
183
+ 'metadata': {'ext_service': True},
184
+ })
185
+
186
+ # ── String literals (credentials, images) ─────────────────────────
187
+ string_literals: list[str] = []
188
+ for m in _RE_CRED.finditer(content):
189
+ string_literals.append(f"credential:{m.group(1)}")
190
+
191
+ return ParseResult(
192
+ file_path=file_path,
193
+ classes=classes,
194
+ methods=methods,
195
+ imports=imports,
196
+ method_calls=method_calls,
197
+ string_literals=string_literals,
198
+ )
@@ -0,0 +1,147 @@
1
+ """Maven pom.xml dependency parser using lxml.
2
+
3
+ This module provides parsing capabilities for Maven POM files to extract
4
+ dependency information for the dependency analysis system.
5
+ """
6
+
7
+ from typing import Any, TypedDict
8
+ from lxml import etree
9
+ from src.domain.value_objects.dependency_type import DependencyType
10
+
11
+
12
+ class MavenDependency(TypedDict):
13
+ """Type definition for Maven dependency dictionary."""
14
+
15
+ group_id: str
16
+ artifact_id: str
17
+ version: str
18
+ scope: str
19
+ type: DependencyType
20
+
21
+
22
+ class MavenDependencyParser:
23
+ """Parser for Maven pom.xml files to extract dependencies.
24
+
25
+ Parses Maven POM files using lxml with proper namespace handling.
26
+ Extracts dependency information: groupId, artifactId, version, scope.
27
+ Filters out test-scoped dependencies by default.
28
+
29
+ Attributes:
30
+ MAVEN_NS: Maven POM namespace URI
31
+ NAMESPACES: Namespace mapping for XPath queries
32
+ """
33
+
34
+ # Maven POM namespace
35
+ MAVEN_NS = "http://maven.apache.org/POM/4.0.0"
36
+ NAMESPACES = {"mvn": MAVEN_NS}
37
+ DEFAULT_SCOPE = "compile"
38
+ MANAGED_VERSION = "managed"
39
+
40
+ def parse(self, pom_content: str) -> list[MavenDependency]:
41
+ """Parse pom.xml content and extract dependencies.
42
+
43
+ Args:
44
+ pom_content: String content of pom.xml file
45
+
46
+ Returns:
47
+ List of MavenDependency dictionaries with keys:
48
+ - group_id: Maven groupId (e.g., 'org.springframework.boot')
49
+ - artifact_id: Maven artifactId (e.g., 'spring-boot-starter-web')
50
+ - version: Version string or 'managed' if not specified
51
+ - scope: Dependency scope (compile, runtime, provided, test)
52
+ - type: Always DependencyType.LIBRARY
53
+
54
+ Raises:
55
+ etree.XMLSyntaxError: If XML is malformed
56
+
57
+ Note:
58
+ Test-scoped dependencies (scope='test') are filtered out.
59
+ """
60
+ root = self._parse_xml(pom_content)
61
+ dependency_elements = self._find_dependency_elements(root)
62
+
63
+ dependencies: list[MavenDependency] = []
64
+
65
+ for dep_elem in dependency_elements:
66
+ dependency = self._extract_dependency(dep_elem)
67
+
68
+ # Skip if essential fields are missing or test-scoped
69
+ if dependency is None:
70
+ continue
71
+
72
+ dependencies.append(dependency)
73
+
74
+ return dependencies
75
+
76
+ def _parse_xml(self, pom_content: str) -> etree._Element:
77
+ """Parse XML content into element tree.
78
+
79
+ Args:
80
+ pom_content: String content of pom.xml file
81
+
82
+ Returns:
83
+ Root element of parsed XML tree
84
+
85
+ Raises:
86
+ etree.XMLSyntaxError: If XML is malformed
87
+ """
88
+ try:
89
+ return etree.fromstring(pom_content.encode("utf-8"))
90
+ except etree.XMLSyntaxError as e:
91
+ raise etree.XMLSyntaxError(f"Failed to parse pom.xml: {e}") from e
92
+
93
+ def _find_dependency_elements(self, root: etree._Element) -> list[etree._Element]:
94
+ """Find all dependency elements using XPath.
95
+
96
+ Args:
97
+ root: Root element of parsed XML
98
+
99
+ Returns:
100
+ List of dependency elements
101
+ """
102
+ return root.xpath("//mvn:dependency", namespaces=self.NAMESPACES)
103
+
104
+ def _extract_dependency(self, dep_elem: etree._Element) -> MavenDependency | None:
105
+ """Extract dependency information from XML element.
106
+
107
+ Args:
108
+ dep_elem: Dependency XML element
109
+
110
+ Returns:
111
+ MavenDependency dictionary or None if essential fields missing or test-scoped
112
+ """
113
+ # Extract fields using XPath with namespace
114
+ group_id = self._extract_text(dep_elem, "mvn:groupId")
115
+ artifact_id = self._extract_text(dep_elem, "mvn:artifactId")
116
+ version = self._extract_text(dep_elem, "mvn:version") or self.MANAGED_VERSION
117
+ scope = self._extract_text(dep_elem, "mvn:scope") or self.DEFAULT_SCOPE
118
+
119
+ # Skip if essential fields are missing
120
+ if not group_id or not artifact_id:
121
+ return None
122
+
123
+ # Filter out test-scoped dependencies
124
+ if scope == "test":
125
+ return None
126
+
127
+ # Build dependency dictionary
128
+ return MavenDependency(
129
+ group_id=group_id,
130
+ artifact_id=artifact_id,
131
+ version=version,
132
+ scope=scope,
133
+ type=DependencyType.LIBRARY,
134
+ )
135
+
136
+ def _extract_text(self, elem: etree._Element, xpath: str) -> str | None:
137
+ """Extract text from XML element using XPath.
138
+
139
+ Args:
140
+ elem: Parent XML element
141
+ xpath: XPath query string
142
+
143
+ Returns:
144
+ Text content or None if element not found
145
+ """
146
+ result = elem.xpath(xpath, namespaces=self.NAMESPACES)
147
+ return result[0].text if result and result[0].text else None
@@ -0,0 +1,242 @@
1
+ """MicrofrontendConfigParser - Parse microfrontend configurations.
2
+
3
+ Detects and extracts webpack module federation and single-spa configurations.
4
+ Creates dependency nodes and edges for graph storage.
5
+ """
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ class MicrofrontendConfigParser:
12
+ """
13
+ Microfrontend configuration parser.
14
+
15
+ Extracts module federation and single-spa configurations from config files.
16
+ """
17
+
18
+ def __init__(self, typescript_parser=None):
19
+ """
20
+ Initialize with optional TypeScript parser (Task 12.3).
21
+
22
+ Args:
23
+ typescript_parser: TreeSitterTypeScriptParser instance for parsing config files
24
+ """
25
+ self.typescript_parser = typescript_parser
26
+
27
+ def detect_module_federation(self, file_path: Path) -> dict | None:
28
+ """
29
+ Detect webpack module federation configuration.
30
+
31
+ Args:
32
+ file_path: Path to webpack config file
33
+
34
+ Returns:
35
+ Dict with exposed modules and remotes, or None if not found
36
+ """
37
+ if not file_path.exists():
38
+ return None
39
+
40
+ content = file_path.read_text(encoding="utf-8")
41
+
42
+ # Simple regex-based detection for ModuleFederationPlugin
43
+ if "ModuleFederationPlugin" not in content:
44
+ return None
45
+
46
+ config = {
47
+ "type": "module_federation",
48
+ "name": self._extract_name(content),
49
+ "exposes": self._extract_exposes(content),
50
+ "remotes": self._extract_remotes(content),
51
+ "shared": self._extract_shared(content),
52
+ }
53
+
54
+ return config
55
+
56
+ def detect_single_spa(self, file_path: Path) -> dict | None:
57
+ """
58
+ Detect single-spa configuration.
59
+
60
+ Args:
61
+ file_path: Path to single-spa config file
62
+
63
+ Returns:
64
+ Dict with registered applications, or None if not found
65
+ """
66
+ if not file_path.exists():
67
+ return None
68
+
69
+ content = file_path.read_text(encoding="utf-8")
70
+
71
+ # Simple detection for registerApplication
72
+ if "registerApplication" not in content:
73
+ return None
74
+
75
+ config = {
76
+ "type": "single_spa",
77
+ "applications": self._extract_applications(content),
78
+ }
79
+
80
+ return config
81
+
82
+ def _extract_name(self, content: str) -> str | None:
83
+ """Extract module federation name."""
84
+ # name: "app_name"
85
+ match = re.search(r'name:\s*["\']([^"\']+)["\']', content)
86
+ if match:
87
+ return match.group(1)
88
+ return None
89
+
90
+ def _extract_exposes(self, content: str) -> dict:
91
+ """Extract exposed modules from module federation config."""
92
+ exposes = {}
93
+
94
+ # exposes: { "./Component": "./src/Component" }
95
+ exposes_match = re.search(
96
+ r"exposes:\s*\{([^}]+)\}", content, re.DOTALL
97
+ )
98
+ if exposes_match:
99
+ exposes_block = exposes_match.group(1)
100
+ # Extract key-value pairs
101
+ for match in re.finditer(
102
+ r'["\']([^"\']+)["\']\s*:\s*["\']([^"\']+)["\']', exposes_block
103
+ ):
104
+ key = match.group(1)
105
+ value = match.group(2)
106
+ exposes[key] = value
107
+
108
+ return exposes
109
+
110
+ def _extract_remotes(self, content: str) -> dict:
111
+ """Extract remote entries from module federation config."""
112
+ remotes = {}
113
+
114
+ # remotes: { app1: "app1@http://localhost:3001/remoteEntry.js" }
115
+ remotes_match = re.search(
116
+ r"remotes:\s*\{([^}]+)\}", content, re.DOTALL
117
+ )
118
+ if remotes_match:
119
+ remotes_block = remotes_match.group(1)
120
+ for match in re.finditer(
121
+ r'["\']?(\w+)["\']?\s*:\s*["\']([^"\']+)["\']', remotes_block
122
+ ):
123
+ key = match.group(1)
124
+ value = match.group(2)
125
+ remotes[key] = value
126
+
127
+ return remotes
128
+
129
+ def _extract_shared(self, content: str) -> list[str]:
130
+ """Extract shared dependencies."""
131
+ shared = []
132
+
133
+ # shared: ["react", "react-dom"]
134
+ shared_match = re.search(
135
+ r"shared:\s*\[([^\]]+)\]", content, re.DOTALL
136
+ )
137
+ if shared_match:
138
+ shared_block = shared_match.group(1)
139
+ for match in re.finditer(r'["\']([^"\']+)["\']', shared_block):
140
+ shared.append(match.group(1))
141
+
142
+ return shared
143
+
144
+ def _extract_applications(self, content: str) -> list[dict]:
145
+ """Extract registered applications from single-spa config."""
146
+ applications = []
147
+
148
+ # registerApplication({ name: "app1", app: () => import("app1"), activeWhen: "/" })
149
+ for match in re.finditer(
150
+ r'registerApplication\s*\(\s*\{([^}]+)\}\s*\)', content, re.DOTALL
151
+ ):
152
+ app_block = match.group(1)
153
+
154
+ name_match = re.search(r'name:\s*["\']([^"\']+)["\']', app_block)
155
+ url_match = re.search(r'app:\s*["\']([^"\']+)["\']', app_block)
156
+
157
+ if name_match:
158
+ app = {"name": name_match.group(1)}
159
+ if url_match:
160
+ app["url"] = url_match.group(1)
161
+ applications.append(app)
162
+
163
+ return applications
164
+
165
+ def create_dependency_nodes_and_edges(
166
+ self, config: dict, repository_name: str
167
+ ) -> tuple[list[dict], list[dict]]:
168
+ """
169
+ Create dependency nodes and edges from microfrontend config (Task 3.3).
170
+
171
+ Args:
172
+ config: Parsed microfrontend configuration
173
+ repository_name: Name of the repository
174
+
175
+ Returns:
176
+ Tuple of (nodes, edges) lists
177
+ """
178
+ nodes = []
179
+ edges = []
180
+
181
+ if config["type"] == "module_federation":
182
+ # Create nodes for exposed modules
183
+ for expose_key, expose_path in config.get("exposes", {}).items():
184
+ node = {
185
+ "node_type": "MICROFRONTEND",
186
+ "name": f"{config.get('name', repository_name)}:{expose_key}",
187
+ "module_name": config.get("name"),
188
+ "expose_key": expose_key,
189
+ "path": expose_path,
190
+ "repository": repository_name,
191
+ }
192
+ nodes.append(node)
193
+
194
+ # Create EXPOSES_MODULE edge from repository to exposed module
195
+ edge = {
196
+ "edge_type": "EXPOSES_MODULE",
197
+ "source": repository_name,
198
+ "target": node["name"],
199
+ "metadata": {
200
+ "expose_key": expose_key,
201
+ "path": expose_path,
202
+ },
203
+ }
204
+ edges.append(edge)
205
+
206
+ # Create edges for consumed remotes
207
+ for remote_name, remote_url in config.get("remotes", {}).items():
208
+ edge = {
209
+ "edge_type": "CONSUMES_MODULE",
210
+ "source": repository_name,
211
+ "target": remote_name,
212
+ "metadata": {
213
+ "remote_url": remote_url,
214
+ "shared": config.get("shared", []),
215
+ },
216
+ }
217
+ edges.append(edge)
218
+
219
+ elif config["type"] == "single_spa":
220
+ # Create nodes for single-spa applications
221
+ for app in config.get("applications", []):
222
+ node = {
223
+ "node_type": "MICROFRONTEND",
224
+ "name": app["name"],
225
+ "application_type": "single_spa",
226
+ "url": app.get("url"),
227
+ "repository": repository_name,
228
+ }
229
+ nodes.append(node)
230
+
231
+ # Create edge from repository to application
232
+ edge = {
233
+ "edge_type": "REGISTERS_APPLICATION",
234
+ "source": repository_name,
235
+ "target": app["name"],
236
+ "metadata": {
237
+ "url": app.get("url"),
238
+ },
239
+ }
240
+ edges.append(edge)
241
+
242
+ return nodes, edges