impact-core 1.0.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.
- adapters/java/extractor.py +332 -0
- adapters/java/static_analyzer.py +69 -0
- core/agents/coordinator.py +88 -0
- core/agents/diff_agent.py +30 -0
- core/agents/graph_agent.py +24 -0
- core/agents/llm_agent.py +153 -0
- core/agents/metrics_agent.py +28 -0
- core/crawler/__init__.py +8 -0
- core/crawler/crawler.py +60 -0
- core/crawler/database.py +320 -0
- core/crawler/discovery.py +46 -0
- core/crawler/downloader.py +54 -0
- core/crawler/network.py +26 -0
- core/crawler/processor.py +100 -0
- core/ecosystem_crawler.py +70 -0
- core/github_collector.py +221 -0
- core/graph_utils.py +57 -0
- core/mcp_server.py +196 -0
- core/schema/schema.json +89 -0
- core/shacl_validator.py +74 -0
- impact_core-1.0.0.dist-info/METADATA +366 -0
- impact_core-1.0.0.dist-info/RECORD +26 -0
- impact_core-1.0.0.dist-info/WHEEL +5 -0
- impact_core-1.0.0.dist-info/entry_points.txt +5 -0
- impact_core-1.0.0.dist-info/licenses/LICENSE +201 -0
- impact_core-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import javalang
|
|
8
|
+
HAS_JAVALANG = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
HAS_JAVALANG = False
|
|
11
|
+
|
|
12
|
+
class JavaExtractor:
|
|
13
|
+
"""Java source code AST-like parser to extract IMPACT dependency graphs using AST and regex fallbacks."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, project_name: str, version: str):
|
|
16
|
+
self.project_name = project_name
|
|
17
|
+
self.version = version
|
|
18
|
+
self.nodes = {}
|
|
19
|
+
self.edges = []
|
|
20
|
+
|
|
21
|
+
def extract(self, src_dir: str, output_file: str):
|
|
22
|
+
if not os.path.exists(src_dir):
|
|
23
|
+
raise FileNotFoundError(f"Source directory does not exist: {src_dir}")
|
|
24
|
+
|
|
25
|
+
java_files = []
|
|
26
|
+
for root, _, files in os.walk(src_dir):
|
|
27
|
+
for file in files:
|
|
28
|
+
if file.endswith(".java"):
|
|
29
|
+
java_files.append(os.path.join(root, file))
|
|
30
|
+
|
|
31
|
+
# First pass: find all classes/interfaces and their FQCNs
|
|
32
|
+
class_details = {}
|
|
33
|
+
project_classes = set() # Set of all FQCNs in the project
|
|
34
|
+
class_name_to_fqcn = {} # Map simple name -> set of FQCNs for resolving references
|
|
35
|
+
|
|
36
|
+
for filepath in java_files:
|
|
37
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
38
|
+
content = f.read()
|
|
39
|
+
|
|
40
|
+
# Calculate LOC and complexity metrics (consistent across both parsing modes)
|
|
41
|
+
lines = content.splitlines()
|
|
42
|
+
loc = len([line for line in lines if line.strip() and not line.strip().startswith("//")])
|
|
43
|
+
complexity = 1
|
|
44
|
+
for line in lines:
|
|
45
|
+
complexity += len(re.findall(r"\b(if|for|while|catch)\b|&&|\|\|", line))
|
|
46
|
+
|
|
47
|
+
parsed_ast = False
|
|
48
|
+
if HAS_JAVALANG:
|
|
49
|
+
try:
|
|
50
|
+
tree = javalang.parse.parse(content)
|
|
51
|
+
package_name = tree.package.name if tree.package else ""
|
|
52
|
+
|
|
53
|
+
for t in tree.types:
|
|
54
|
+
class_name = t.name
|
|
55
|
+
fqcn = f"{package_name}.{class_name}" if package_name else class_name
|
|
56
|
+
|
|
57
|
+
# Collect references from AST
|
|
58
|
+
references = set()
|
|
59
|
+
for path, node in tree:
|
|
60
|
+
if isinstance(node, javalang.tree.ReferenceType):
|
|
61
|
+
references.add(node.name)
|
|
62
|
+
# Fix 1: also collect qualified nested references (Outer.Inner)
|
|
63
|
+
if node.sub_type:
|
|
64
|
+
references.add(f"{node.name}.{node.sub_type.name}")
|
|
65
|
+
elif isinstance(node, javalang.tree.MethodInvocation):
|
|
66
|
+
if node.qualifier:
|
|
67
|
+
parts = node.qualifier.split('.')
|
|
68
|
+
if parts:
|
|
69
|
+
references.add(parts[0])
|
|
70
|
+
# Collect qualified name for Outer.method() style calls
|
|
71
|
+
if len(parts) == 2:
|
|
72
|
+
references.add(node.qualifier)
|
|
73
|
+
elif isinstance(node, javalang.tree.MemberReference):
|
|
74
|
+
if node.qualifier:
|
|
75
|
+
parts = node.qualifier.split('.')
|
|
76
|
+
if parts:
|
|
77
|
+
references.add(parts[0])
|
|
78
|
+
|
|
79
|
+
imports = [imp.path for imp in tree.imports]
|
|
80
|
+
|
|
81
|
+
class_details[fqcn] = {
|
|
82
|
+
"name": class_name,
|
|
83
|
+
"package": package_name,
|
|
84
|
+
"filePath": os.path.relpath(filepath, src_dir),
|
|
85
|
+
"loc": loc,
|
|
86
|
+
"complexity": complexity,
|
|
87
|
+
"content": content,
|
|
88
|
+
"imports": imports,
|
|
89
|
+
"ast_references": list(references),
|
|
90
|
+
"use_ast": True
|
|
91
|
+
}
|
|
92
|
+
project_classes.add(fqcn)
|
|
93
|
+
class_name_to_fqcn.setdefault(class_name, set()).add(fqcn)
|
|
94
|
+
|
|
95
|
+
# Fix 2: register nested class declarations found inside class bodies
|
|
96
|
+
if hasattr(t, 'body') and t.body:
|
|
97
|
+
for member in t.body:
|
|
98
|
+
if isinstance(member, (
|
|
99
|
+
javalang.tree.ClassDeclaration,
|
|
100
|
+
javalang.tree.InterfaceDeclaration,
|
|
101
|
+
javalang.tree.EnumDeclaration,
|
|
102
|
+
)):
|
|
103
|
+
nested_name = member.name
|
|
104
|
+
nested_fqcn = f"{fqcn}.{nested_name}"
|
|
105
|
+
# Register as a graph node (inherits file/metrics from outer class)
|
|
106
|
+
class_details[nested_fqcn] = {
|
|
107
|
+
"name": nested_name,
|
|
108
|
+
"package": package_name,
|
|
109
|
+
"filePath": os.path.relpath(filepath, src_dir),
|
|
110
|
+
"loc": 0, # nested LOC not separately measured
|
|
111
|
+
"complexity": 1,
|
|
112
|
+
"content": "",
|
|
113
|
+
"imports": imports,
|
|
114
|
+
"ast_references": [],
|
|
115
|
+
"use_ast": True
|
|
116
|
+
}
|
|
117
|
+
project_classes.add(nested_fqcn)
|
|
118
|
+
class_name_to_fqcn.setdefault(nested_name, set()).add(nested_fqcn)
|
|
119
|
+
# Register "OuterSimple.InnerSimple" for qualified lookup
|
|
120
|
+
class_name_to_fqcn.setdefault(
|
|
121
|
+
f"{class_name}.{nested_name}", set()
|
|
122
|
+
).add(nested_fqcn)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
parsed_ast = True
|
|
126
|
+
except Exception as e:
|
|
127
|
+
# AST parsing failed, log and fall back to regex
|
|
128
|
+
# (Can happen on new Java syntax elements like records or dynamic vars)
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
if not parsed_ast:
|
|
132
|
+
# Regex Fallback
|
|
133
|
+
package_match = re.search(r"package\s+([\w\.]+);", content)
|
|
134
|
+
package_name = package_match.group(1) if package_match else ""
|
|
135
|
+
|
|
136
|
+
class_match = re.search(r"\b(?:class|interface|enum|record)\s+(\w+)", content)
|
|
137
|
+
if not class_match:
|
|
138
|
+
continue
|
|
139
|
+
class_name = class_match.group(1)
|
|
140
|
+
fqcn = f"{package_name}.{class_name}" if package_name else class_name
|
|
141
|
+
|
|
142
|
+
class_details[fqcn] = {
|
|
143
|
+
"name": class_name,
|
|
144
|
+
"package": package_name,
|
|
145
|
+
"filePath": os.path.relpath(filepath, src_dir),
|
|
146
|
+
"loc": loc,
|
|
147
|
+
"complexity": complexity,
|
|
148
|
+
"content": content,
|
|
149
|
+
"use_ast": False
|
|
150
|
+
}
|
|
151
|
+
project_classes.add(fqcn)
|
|
152
|
+
class_name_to_fqcn.setdefault(class_name, set()).add(fqcn)
|
|
153
|
+
|
|
154
|
+
# Second pass: resolve dependencies
|
|
155
|
+
for fqcn, details in class_details.items():
|
|
156
|
+
dependencies = set()
|
|
157
|
+
|
|
158
|
+
if details.get("use_ast"):
|
|
159
|
+
# 1. Resolve AST-collected references semantically
|
|
160
|
+
for ref in details["ast_references"]:
|
|
161
|
+
if ref == details["name"]:
|
|
162
|
+
continue # Skip self-references
|
|
163
|
+
|
|
164
|
+
resolved = False
|
|
165
|
+
|
|
166
|
+
# A. Check explicit imports (e.g. import com.example.service.DbService;)
|
|
167
|
+
for imp in details["imports"]:
|
|
168
|
+
if imp.endswith(f".{ref}"):
|
|
169
|
+
target_fqcn = imp
|
|
170
|
+
if target_fqcn in project_classes:
|
|
171
|
+
dependencies.add(target_fqcn)
|
|
172
|
+
resolved = True
|
|
173
|
+
break
|
|
174
|
+
if resolved:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# B. Check same package (e.g. current_package.DbService)
|
|
178
|
+
target_fqcn = f"{details['package']}.{ref}" if details['package'] else ref
|
|
179
|
+
if target_fqcn in project_classes:
|
|
180
|
+
dependencies.add(target_fqcn)
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# C. Check wildcard imports (e.g. import com.example.service.*;)
|
|
184
|
+
for imp in details["imports"]:
|
|
185
|
+
if imp.endswith(".*"):
|
|
186
|
+
wildcard_package = imp[:-2]
|
|
187
|
+
target_fqcn = f"{wildcard_package}.{ref}"
|
|
188
|
+
if target_fqcn in project_classes:
|
|
189
|
+
dependencies.add(target_fqcn)
|
|
190
|
+
resolved = True
|
|
191
|
+
break
|
|
192
|
+
if resolved:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# D. Global fallback: Match simple name if unique in the project
|
|
196
|
+
if ref in class_name_to_fqcn:
|
|
197
|
+
candidates = class_name_to_fqcn[ref]
|
|
198
|
+
if len(candidates) == 1:
|
|
199
|
+
dependencies.add(list(candidates)[0])
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Fix 3 / E. Resolve qualified nested references like "Outer.Inner"
|
|
203
|
+
if '.' in ref:
|
|
204
|
+
# E1. Direct lookup: "OuterSimple.InnerSimple" registered in first pass
|
|
205
|
+
if ref in class_name_to_fqcn:
|
|
206
|
+
candidates = class_name_to_fqcn[ref]
|
|
207
|
+
if len(candidates) == 1:
|
|
208
|
+
dependencies.add(list(candidates)[0])
|
|
209
|
+
continue
|
|
210
|
+
# E2. Resolve outer name -> FQCN, then append inner name
|
|
211
|
+
outer_ref, inner_name = ref.split('.', 1)
|
|
212
|
+
outer_fqcn = None
|
|
213
|
+
# Try explicit imports for the outer class
|
|
214
|
+
for imp in details["imports"]:
|
|
215
|
+
if imp.endswith(f".{outer_ref}"):
|
|
216
|
+
outer_fqcn = imp
|
|
217
|
+
break
|
|
218
|
+
# Try same-package outer class
|
|
219
|
+
if not outer_fqcn:
|
|
220
|
+
candidate = (
|
|
221
|
+
f"{details['package']}.{outer_ref}"
|
|
222
|
+
if details['package'] else outer_ref
|
|
223
|
+
)
|
|
224
|
+
if candidate in project_classes:
|
|
225
|
+
outer_fqcn = candidate
|
|
226
|
+
# Try wildcard imports for the outer class
|
|
227
|
+
if not outer_fqcn:
|
|
228
|
+
for imp in details["imports"]:
|
|
229
|
+
if imp.endswith(".*"):
|
|
230
|
+
candidate = f"{imp[:-2]}.{outer_ref}"
|
|
231
|
+
if candidate in project_classes:
|
|
232
|
+
outer_fqcn = candidate
|
|
233
|
+
break
|
|
234
|
+
if outer_fqcn:
|
|
235
|
+
nested_fqcn = f"{outer_fqcn}.{inner_name}"
|
|
236
|
+
if nested_fqcn in project_classes:
|
|
237
|
+
dependencies.add(nested_fqcn)
|
|
238
|
+
else:
|
|
239
|
+
# 2. Regex fallback: search simple class name matches in the text content
|
|
240
|
+
content = details["content"]
|
|
241
|
+
for other_fqcn, other_details in class_details.items():
|
|
242
|
+
if fqcn == other_fqcn:
|
|
243
|
+
continue
|
|
244
|
+
pattern = rf"\b{other_details['name']}\b"
|
|
245
|
+
if re.search(pattern, content):
|
|
246
|
+
dependencies.add(other_fqcn)
|
|
247
|
+
|
|
248
|
+
# Register resolved dependency edges
|
|
249
|
+
for target_fqcn in dependencies:
|
|
250
|
+
self.edges.append({
|
|
251
|
+
"source": fqcn,
|
|
252
|
+
"target": target_fqcn,
|
|
253
|
+
"type": "calls"
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
self.nodes[fqcn] = {
|
|
257
|
+
"id": fqcn,
|
|
258
|
+
"name": details["name"],
|
|
259
|
+
"type": "class",
|
|
260
|
+
"filePath": details["filePath"],
|
|
261
|
+
"metrics": {
|
|
262
|
+
"loc": details["loc"],
|
|
263
|
+
"complexity": details["complexity"],
|
|
264
|
+
"fanOut": len(dependencies)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Update fanIn metrics
|
|
269
|
+
for edge in self.edges:
|
|
270
|
+
target = edge["target"]
|
|
271
|
+
if target in self.nodes:
|
|
272
|
+
self.nodes[target]["metrics"]["fanIn"] = self.nodes[target]["metrics"].get("fanIn", 0) + 1
|
|
273
|
+
|
|
274
|
+
# Fill default values for fanIn/fanOut
|
|
275
|
+
for node in self.nodes.values():
|
|
276
|
+
if "fanIn" not in node["metrics"]:
|
|
277
|
+
node["metrics"]["fanIn"] = 0
|
|
278
|
+
node["metrics"]["coupling"] = node["metrics"]["fanIn"] + node["metrics"]["fanOut"]
|
|
279
|
+
|
|
280
|
+
# Calculate system-wide metrics
|
|
281
|
+
total_loc = sum(n["metrics"]["loc"] for n in self.nodes.values())
|
|
282
|
+
total_classes = len(self.nodes)
|
|
283
|
+
avg_coupling = sum(n["metrics"]["coupling"] for n in self.nodes.values()) / max(1, total_classes)
|
|
284
|
+
|
|
285
|
+
output_data = {
|
|
286
|
+
"@context": {
|
|
287
|
+
"@vocab": "https://w3id.org/impact/ontology#",
|
|
288
|
+
"projectName": "projectName",
|
|
289
|
+
"version": "versionString",
|
|
290
|
+
"language": "language",
|
|
291
|
+
"extractedAt": "extractedAt",
|
|
292
|
+
"systemMetrics": "systemMetrics",
|
|
293
|
+
"nodes": "hasEntity",
|
|
294
|
+
"edges": "hasDependency",
|
|
295
|
+
"id": "@id",
|
|
296
|
+
"type": "@type",
|
|
297
|
+
"source": "source",
|
|
298
|
+
"target": "target"
|
|
299
|
+
},
|
|
300
|
+
"projectName": self.project_name,
|
|
301
|
+
"version": self.version,
|
|
302
|
+
"language": "Java",
|
|
303
|
+
"extractedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
304
|
+
"systemMetrics": {
|
|
305
|
+
"totalLinesOfCode": total_loc,
|
|
306
|
+
"totalClasses": total_classes,
|
|
307
|
+
"averageCoupling": avg_coupling
|
|
308
|
+
},
|
|
309
|
+
"nodes": list(self.nodes.values()),
|
|
310
|
+
"edges": self.edges
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Write to JSON
|
|
314
|
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
|
315
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
316
|
+
json.dump(output_data, f, indent=2)
|
|
317
|
+
|
|
318
|
+
print(f"[JavaExtractor] Extracted graph via {'AST' if HAS_JAVALANG else 'Regex'} for {self.project_name} v{self.version} to {output_file}")
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
import sys
|
|
322
|
+
if len(sys.argv) < 5:
|
|
323
|
+
print("Usage: python3 extractor.py <project_name> <version> <src_dir> <output_file>")
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
|
|
326
|
+
p_name = sys.argv[1]
|
|
327
|
+
ver = sys.argv[2]
|
|
328
|
+
s_dir = sys.argv[3]
|
|
329
|
+
out_f = sys.argv[4]
|
|
330
|
+
|
|
331
|
+
extractor = JavaExtractor(p_name, ver)
|
|
332
|
+
extractor.extract(s_dir, out_f)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
class PMDIntegration:
|
|
5
|
+
"""Parses PMD static analysis reports and merges violations into the IMPACT graph."""
|
|
6
|
+
|
|
7
|
+
def merge_pmd_report(self, graph_json_path: str, pmd_json_path: str):
|
|
8
|
+
if not os.path.exists(graph_json_path):
|
|
9
|
+
raise FileNotFoundError(f"Graph file not found: {graph_json_path}")
|
|
10
|
+
|
|
11
|
+
# If no PMD report exists, we simulate a mock clean run or print message
|
|
12
|
+
if not os.path.exists(pmd_json_path):
|
|
13
|
+
print(f"[PMDIntegration] PMD report {pmd_json_path} not found. Skipping static analysis merge.")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
with open(graph_json_path, "r", encoding="utf-8") as f:
|
|
17
|
+
graph_data = json.load(f)
|
|
18
|
+
|
|
19
|
+
with open(pmd_json_path, "r", encoding="utf-8") as f:
|
|
20
|
+
pmd_data = json.load(f)
|
|
21
|
+
|
|
22
|
+
# Parse PMD violations
|
|
23
|
+
file_violations = {}
|
|
24
|
+
for pmd_file in pmd_data.get("files", []):
|
|
25
|
+
# Normalize path
|
|
26
|
+
rel_path = os.path.basename(pmd_file.get("filename", ""))
|
|
27
|
+
violations = []
|
|
28
|
+
for v in pmd_file.get("violations", []):
|
|
29
|
+
violations.append({
|
|
30
|
+
"rule": v.get("rule"),
|
|
31
|
+
"ruleset": v.get("ruleset"),
|
|
32
|
+
"priority": v.get("priority"),
|
|
33
|
+
"beginLine": v.get("beginLine"),
|
|
34
|
+
"message": v.get("message")
|
|
35
|
+
})
|
|
36
|
+
file_violations[rel_path] = violations
|
|
37
|
+
|
|
38
|
+
# Merge violations into matching graph nodes
|
|
39
|
+
merged_count = 0
|
|
40
|
+
for node in graph_data.get("nodes", []):
|
|
41
|
+
file_name = os.path.basename(node.get("filePath", ""))
|
|
42
|
+
if file_name in file_violations:
|
|
43
|
+
node["staticAnalysisViolations"] = file_violations[file_name]
|
|
44
|
+
node["metrics"]["staticAnalysisViolationsCount"] = len(file_violations[file_name])
|
|
45
|
+
merged_count += len(file_violations[file_name])
|
|
46
|
+
else:
|
|
47
|
+
node["staticAnalysisViolations"] = []
|
|
48
|
+
node["metrics"]["staticAnalysisViolationsCount"] = 0
|
|
49
|
+
|
|
50
|
+
# Update system-wide metrics
|
|
51
|
+
if "systemMetrics" in graph_data:
|
|
52
|
+
graph_data["systemMetrics"]["totalStaticAnalysisViolations"] = merged_count
|
|
53
|
+
|
|
54
|
+
with open(graph_json_path, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(graph_data, f, indent=2)
|
|
56
|
+
|
|
57
|
+
print(f"[PMDIntegration] Merged {merged_count} PMD static analysis violations into {graph_json_path}")
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
import sys
|
|
61
|
+
if len(sys.argv) < 3:
|
|
62
|
+
print("Usage: python3 static_analyzer.py <graph_json_path> <pmd_json_path>")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
g_path = sys.argv[1]
|
|
66
|
+
pmd_path = sys.argv[2]
|
|
67
|
+
|
|
68
|
+
integration = PMDIntegration()
|
|
69
|
+
integration.merge_pmd_report(g_path, pmd_path)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from core.agents.graph_agent import GraphAgent
|
|
2
|
+
from core.agents.diff_agent import DiffAgent
|
|
3
|
+
from core.agents.metrics_agent import MetricsAgent
|
|
4
|
+
from core.agents.llm_agent import LLMAgent
|
|
5
|
+
|
|
6
|
+
class CoordinatorAgent:
|
|
7
|
+
"""Core coordinator that orchestrates the entire IMPACT evaluation loop."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, name: str = "CoordinatorAgent"):
|
|
10
|
+
self.name = name
|
|
11
|
+
self.graph_agent = GraphAgent()
|
|
12
|
+
self.diff_agent = DiffAgent()
|
|
13
|
+
self.metrics_agent = MetricsAgent()
|
|
14
|
+
self.llm_agent = LLMAgent()
|
|
15
|
+
|
|
16
|
+
def run_analysis(self, file_path_v1: str, file_path_v2: str, intents: list) -> str:
|
|
17
|
+
"""Orchestrates the step-by-step evolution analysis of two versions."""
|
|
18
|
+
print("[Coordinator] Step 1: Loading graphs and checking statistics...")
|
|
19
|
+
stats_v1 = self.graph_agent.execute(file_path_v1)
|
|
20
|
+
stats_v2 = self.graph_agent.execute(file_path_v2)
|
|
21
|
+
|
|
22
|
+
print("[Coordinator] Step 2: Running structural diff comparison...")
|
|
23
|
+
diff = self.diff_agent.execute(file_path_v1, file_path_v2)
|
|
24
|
+
|
|
25
|
+
print("[Coordinator] Step 3: Computing graph metrics and identifying hubs...")
|
|
26
|
+
metrics = self.metrics_agent.execute(file_path_v2)
|
|
27
|
+
|
|
28
|
+
print("[Coordinator] Step 4: Synthesizing results and checking intent conformance...")
|
|
29
|
+
report = self.llm_agent.execute(diff, metrics, intents)
|
|
30
|
+
|
|
31
|
+
print("[Coordinator] Analysis complete.")
|
|
32
|
+
return report
|
|
33
|
+
|
|
34
|
+
def run_ecosystem_analysis(self, repo_coordinate: str, intents: list) -> str:
|
|
35
|
+
"""Downloads, extracts, and runs the multi-agent evolution analysis on a GitHub repository coordinate."""
|
|
36
|
+
from core.ecosystem_crawler import GitHubEcosystemCrawler
|
|
37
|
+
import os
|
|
38
|
+
import sqlite3
|
|
39
|
+
|
|
40
|
+
print(f"[Coordinator] Orchestrating ecosystem analysis for {repo_coordinate}...")
|
|
41
|
+
owner, repo = repo_coordinate.split("/")
|
|
42
|
+
crawler = GitHubEcosystemCrawler()
|
|
43
|
+
|
|
44
|
+
# Check if the repository is already in the database queue, if not insert it
|
|
45
|
+
conn = sqlite3.connect("test_projects/github_benchmarks/crawler_queue.db")
|
|
46
|
+
cursor = conn.cursor()
|
|
47
|
+
cursor.execute("SELECT id, status, tag1, tag2 FROM queue WHERE owner = ? AND repo = ?", (owner, repo))
|
|
48
|
+
row = cursor.fetchone()
|
|
49
|
+
|
|
50
|
+
if not row:
|
|
51
|
+
cursor.execute("INSERT INTO queue (owner, repo) VALUES (?, ?)", (owner, repo))
|
|
52
|
+
conn.commit()
|
|
53
|
+
cursor.execute("SELECT id, status, tag1, tag2 FROM queue WHERE owner = ? AND repo = ?", (owner, repo))
|
|
54
|
+
row = cursor.fetchone()
|
|
55
|
+
|
|
56
|
+
repo_id, status, tag1, tag2 = row
|
|
57
|
+
conn.close()
|
|
58
|
+
|
|
59
|
+
# If it hasn't been crawled successfully yet, process it
|
|
60
|
+
if status != "crawled":
|
|
61
|
+
print(f"[Coordinator] Repository {repo_coordinate} is in '{status}' status. Initiating crawl/extraction...")
|
|
62
|
+
crawler.process_repo(repo_id, owner, repo)
|
|
63
|
+
|
|
64
|
+
# Re-read status
|
|
65
|
+
conn = sqlite3.connect("test_projects/github_benchmarks/crawler_queue.db")
|
|
66
|
+
cursor = conn.cursor()
|
|
67
|
+
cursor.execute("SELECT status, tag1, tag2, error_msg FROM queue WHERE id = ?", (repo_id,))
|
|
68
|
+
status, tag1, tag2, error_msg = cursor.fetchone()
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
if status == "failed":
|
|
72
|
+
raise Exception(f"Ecosystem crawl failed for {repo_coordinate}: {error_msg}")
|
|
73
|
+
|
|
74
|
+
# Path to the generated analysis report
|
|
75
|
+
repo_dir = f"test_projects/github_benchmarks/java/{owner}_{repo}"
|
|
76
|
+
report_path = os.path.join(repo_dir, f"{tag1}_to_{tag2}_analysis.txt")
|
|
77
|
+
|
|
78
|
+
if not os.path.exists(report_path):
|
|
79
|
+
graph_v1 = os.path.join(repo_dir, f"{tag1}_graph.json")
|
|
80
|
+
graph_v2 = os.path.join(repo_dir, f"{tag2}_graph.json")
|
|
81
|
+
report = self.run_analysis(graph_v1, graph_v2, intents)
|
|
82
|
+
with open(report_path, "w", encoding="utf-8") as f:
|
|
83
|
+
f.write(report)
|
|
84
|
+
return report
|
|
85
|
+
|
|
86
|
+
with open(report_path, "r", encoding="utf-8") as f:
|
|
87
|
+
return f.read()
|
|
88
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from core.graph_utils import load_impact_graph, compare_graphs
|
|
3
|
+
|
|
4
|
+
class DiffAgent:
|
|
5
|
+
"""Specialized agent for detecting structural differences and cycle shifts between versions."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, name: str = "DiffAgent"):
|
|
8
|
+
self.name = name
|
|
9
|
+
|
|
10
|
+
def execute(self, file_path_v1: str, file_path_v2: str) -> dict:
|
|
11
|
+
"""Compares two versions of a dependency graph and returns the diff report."""
|
|
12
|
+
if not os.path.exists(file_path_v1) or not os.path.exists(file_path_v2):
|
|
13
|
+
raise FileNotFoundError("One or both graph files were not found.")
|
|
14
|
+
|
|
15
|
+
g1 = load_impact_graph(file_path_v1)
|
|
16
|
+
g2 = load_impact_graph(file_path_v2)
|
|
17
|
+
|
|
18
|
+
diff = compare_graphs(g1, g2)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
"version_old": g1.graph["version"],
|
|
22
|
+
"version_new": g2.graph["version"],
|
|
23
|
+
"added_nodes_count": len(diff["added_nodes"]),
|
|
24
|
+
"removed_nodes_count": len(diff["removed_nodes"]),
|
|
25
|
+
"added_edges_count": len(diff["added_edges"]),
|
|
26
|
+
"removed_edges_count": len(diff["removed_edges"]),
|
|
27
|
+
"new_cycles_count": len(diff["new_cycles"]),
|
|
28
|
+
"broken_cycles_count": len(diff["broken_cycles"]),
|
|
29
|
+
"raw_diff": diff
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import networkx as nx
|
|
3
|
+
from core.graph_utils import load_impact_graph
|
|
4
|
+
|
|
5
|
+
class GraphAgent:
|
|
6
|
+
"""Specialized agent for loading and querying software dependency graphs."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, name: str = "GraphAgent"):
|
|
9
|
+
self.name = name
|
|
10
|
+
|
|
11
|
+
def execute(self, file_path: str) -> dict:
|
|
12
|
+
"""Loads and returns summary statistics of the dependency graph."""
|
|
13
|
+
if not os.path.exists(file_path):
|
|
14
|
+
raise FileNotFoundError(f"Graph file not found: {file_path}")
|
|
15
|
+
|
|
16
|
+
g = load_impact_graph(file_path)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
"version": g.graph["version"],
|
|
20
|
+
"language": g.graph["language"],
|
|
21
|
+
"node_count": len(g.nodes),
|
|
22
|
+
"edge_count": len(g.edges),
|
|
23
|
+
"system_metrics": g.graph["systemMetrics"]
|
|
24
|
+
}
|