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.
@@ -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
+ }