update graph

This commit is contained in:
2025-11-15 23:48:16 +00:00
parent dffe175059
commit 76bab8abc2
2 changed files with 172 additions and 133 deletions
+79 -2
View File
@@ -41,9 +41,37 @@ class LocalGraph:
self.add_edge(src, target, "imports") self.add_edge(src, target, "imports")
def _add_call_edges(self, src: str, calls: list[str], file_path: str): def _add_call_edges(self, src: str, calls: list[str], file_path: str):
"""Enhanced call edges that work cross-file by searching the entire graph."""
for cal in calls: for cal in calls:
target = f"rust::function::{file_path}::{cal}" if not cal or not isinstance(cal, str):
self.add_edge(src, target, "calls") continue
# Try to find the called function in the graph
target_found = False
# Search for functions with this name across ALL files
for node_id, data in self.graph.nodes(data=True):
if (data.get('type') in ['function', 'method', 'async_function', 'constructor'] and
data.get('name') == cal):
# Found a matching function - create call edge
self.add_edge(src, node_id, "calls")
target_found = True
# Don't break - a function might be called from multiple places
# If not found, create a symbolic node for the call target
if not target_found:
# Create a canonical function node (file-less) for cross-file references
symbolic_id = f"rust::function::{cal}" # Assuming Rust for now
if symbolic_id not in self.graph.nodes:
self.add_node(
symbolic_id,
type="function",
name=cal,
file="", # No specific file
lang="rust",
is_symbolic=True
)
self.add_edge(src, symbolic_id, "calls")
@@ -284,3 +312,52 @@ class LocalGraph:
entities.append(f"{entity_type}:{entity_name}") entities.append(f"{entity_type}:{entity_name}")
return entities return entities
def find_nodes_by_attributes(self, **filters):
"""Find nodes by attributes (not just exact matches)."""
for node_id, data in self.graph.nodes(data=True):
if all(data.get(k) == v for k, v in filters.items()):
yield node_id, data
def get_node_by_name_and_file(self, name: str, file_path: str, node_type: str = None, lang: str = None):
"""Find a node by name and file path with optional type/language filtering."""
candidates = []
for node_id, data in self.graph.nodes(data=True):
if (data.get('name') == name and
data.get('file') == file_path and
(node_type is None or data.get('type') == node_type) and
(lang is None or data.get('lang') == lang)):
candidates.append((node_id, data))
return candidates
def get_cross_file_relationships(self, node_id: str, current_file: str):
"""Get relationships that cross file boundaries."""
if node_id not in self.graph.nodes:
return [], []
cross_file_out = []
cross_file_in = []
# Outgoing cross-file relationships
for neighbor_id in self.graph.successors(node_id):
if neighbor_id in self.graph.nodes:
neighbor_file = self.graph.nodes[neighbor_id].get('file', '')
if neighbor_file and neighbor_file != current_file:
edge_data = self.graph.edges[node_id, neighbor_id]
rel_type = edge_data.get('type', 'related')
neighbor_name = self.graph.nodes[neighbor_id].get('name', '')
neighbor_type = self.graph.nodes[neighbor_id].get('type', '')
cross_file_out.append(f"{rel_type}:{neighbor_type}:{neighbor_name}@{neighbor_file}")
# Incoming cross-file relationships
for predecessor_id in self.graph.predecessors(node_id):
if predecessor_id in self.graph.nodes:
pred_file = self.graph.nodes[predecessor_id].get('file', '')
if pred_file and pred_file != current_file:
edge_data = self.graph.edges[predecessor_id, node_id]
rel_type = edge_data.get('type', 'related')
pred_name = self.graph.nodes[predecessor_id].get('name', '')
pred_type = self.graph.nodes[predecessor_id].get('type', '')
cross_file_in.append(f"{rel_type}:{pred_type}:{pred_name}@{pred_file}")
return cross_file_out, cross_file_in
+93 -131
View File
@@ -2865,6 +2865,7 @@ def build_indexes():
trait_registry = {} trait_registry = {}
class_registry = {} class_registry = {}
package_registry = {} package_registry = {}
function_registry = {}
logger.info("=== Phase 1: Creating nodes ===") logger.info("=== Phase 1: Creating nodes ===")
@@ -2951,6 +2952,14 @@ def build_indexes():
class_node_id = f"{lang}::class::{fpath}::{class_name}" class_node_id = f"{lang}::class::{fpath}::{class_name}"
graph.add_edge(class_node_id, node_id, "contains") graph.add_edge(class_node_id, node_id, "contains")
# Register in function_registry for cross-file calls
function_registry[name] = {
"node_id": node_id,
"lang": lang,
"file": fpath,
"type": node_type
}
else: else:
graph.add_node( graph.add_node(
node_id, node_id,
@@ -3317,8 +3326,18 @@ def build_indexes():
calls = calls_raw if isinstance(calls_raw, list) else [] calls = calls_raw if isinstance(calls_raw, list) else []
if calls: if calls:
# Use the enhanced _add_call_edges that handles cross-file
graph._add_call_edges(node_id, calls, fpath) graph._add_call_edges(node_id, calls, fpath)
# ALSO create direct edges to functions we can find in registries
for cal in calls:
# Search function_registry if we had one, but for now search created_nodes
for candidate_id, candidate_data in created_nodes.items():
if (candidate_data.get('type') in ['function', 'method'] and
candidate_data.get('name') == cal and
candidate_data.get('file') != fpath): # Cross-file only
graph.add_edge(node_id, candidate_id, "calls")
# ===== CLASS/STRUCT INHERITANCE ===== # ===== CLASS/STRUCT INHERITANCE =====
if node_type in ["class", "struct"]: if node_type in ["class", "struct"]:
extends = m.get("extends") extends = m.get("extends")
@@ -3668,7 +3687,6 @@ def _extract_param_type(param: str) -> str:
return _clean_type_string(param_type) return _clean_type_string(param_type)
def _extract_field_type(field: str) -> str: def _extract_field_type(field: str) -> str:
"""Extract type from field string for all languages.""" """Extract type from field string for all languages."""
if not field or not isinstance(field, str): if not field or not isinstance(field, str):
@@ -3698,7 +3716,6 @@ def _extract_field_type(field: str) -> str:
return _clean_type_string(field_type) return _clean_type_string(field_type)
def build_cross_language_relationships(graph, created_nodes, component_registry, def build_cross_language_relationships(graph, created_nodes, component_registry,
interface_registry, class_registry): interface_registry, class_registry):
"""Build relationships across different programming languages.""" """Build relationships across different programming languages."""
@@ -3744,7 +3761,6 @@ def build_cross_language_relationships(graph, created_nodes, component_registry,
return connections return connections
def build_type_relationships(graph, created_nodes): def build_type_relationships(graph, created_nodes):
"""Build enhanced type relationships within language boundaries.""" """Build enhanced type relationships within language boundaries."""
connections = 0 connections = 0
@@ -3823,7 +3839,6 @@ def build_type_relationships(graph, created_nodes):
return connections return connections
def build_bevy_relationships(graph, created_nodes): def build_bevy_relationships(graph, created_nodes):
"""Build Bevy-specific relationships (ECS patterns).""" """Build Bevy-specific relationships (ECS patterns)."""
connections = 0 connections = 0
@@ -3907,102 +3922,6 @@ def build_bevy_relationships(graph, created_nodes):
return connections return connections
def build_bevy_relationships(graph, created_nodes):
"""Build Bevy-specific relationships (ECS patterns)."""
connections = 0
# Track Bevy systems, components, and resources
systems = []
components = []
resources = []
bundles = []
for node_id, node_data in created_nodes.items():
m = node_data.get("metadata", {})
if not m:
continue
lang = node_data.get("lang")
if lang != "rust":
continue
node_type = node_data.get("type")
name = node_data.get("name")
# Detect Bevy systems (functions with Query parameters)
if node_type in ["function", "method"]:
params = m.get("parameters", [])
has_query = any("Query" in str(p) for p in params)
has_commands = any("Commands" in str(p) for p in params)
has_res = any("Res" in str(p) or "ResMut" in str(p) for p in params)
if has_query or has_commands or has_res:
systems.append(node_id)
# Detect Bevy components (structs with Component derive)
if node_type == "struct":
derives = m.get("derives", [])
if "Component" in str(derives):
components.append(node_id)
# Detect Bevy resources (structs with Resource derive)
if node_type == "struct":
derives = m.get("derives", [])
if "Resource" in str(derives):
resources.append(node_id)
# Detect Bevy bundles (structs with Bundle derive)
if node_type == "struct":
derives = m.get("derives", [])
if "Bundle" in str(derives):
bundles.append(node_id)
# Connect systems to the components/resources they use
for system_id in systems:
system_data = created_nodes.get(system_id, {})
m = system_data.get("metadata", {})
params = m.get("parameters", [])
for param in params:
param_str = str(param)
# Extract component types from Query<Component>
query_match = re.findall(r'Query<[^>]*?([A-Z]\w+)', param_str)
for comp_name in query_match:
for comp_id in components:
comp_data = created_nodes.get(comp_id, {})
if comp_data.get("name") == comp_name:
if not graph.graph.has_edge(system_id, comp_id):
graph.add_edge(system_id, comp_id, "queries_component")
connections += 1
# Extract resource types from Res<Resource>
res_match = re.findall(r'Res(?:Mut)?<([A-Z]\w+)', param_str)
for res_name in res_match:
for res_id in resources:
res_data = created_nodes.get(res_id, {})
if res_data.get("name") == res_name:
if not graph.graph.has_edge(system_id, res_id):
graph.add_edge(system_id, res_id, "uses_resource")
connections += 1
# Connect bundles to their component fields
for bundle_id in bundles:
bundle_data = created_nodes.get(bundle_id, {})
m = bundle_data.get("metadata", {})
fields = m.get("fields", [])
for field in fields:
field_type = _extract_field_type(field)
for comp_id in components:
comp_data = created_nodes.get(comp_id, {})
if comp_data.get("name") == field_type:
if not graph.graph.has_edge(bundle_id, comp_id):
graph.add_edge(bundle_id, comp_id, "bundles_component")
connections += 1
return connections
def _create_import_symbolic_nodes(graph, import_node_id, file_node_id, import_path, lang): def _create_import_symbolic_nodes(graph, import_node_id, file_node_id, import_path, lang):
"""Create rich symbolic nodes for imports. Returns dict of new nodes to add.""" """Create rich symbolic nodes for imports. Returns dict of new nodes to add."""
new_nodes = {} new_nodes = {}
@@ -5007,52 +4926,95 @@ def _build_enhanced_result(chunk_text: str, meta: dict, score: float, graph: Opt
def _get_result_graph_context(meta: dict, graph: Optional[LocalGraph] = None) -> dict: def _get_result_graph_context(meta: dict, graph: Optional[LocalGraph] = None) -> dict:
"""Ultra-compact graph context using LocalGraph helper methods.""" """Fixed graph context using enhanced graph methods."""
if graph is None: if graph is None or graph.graph.number_of_nodes() == 0:
return {} return {}
context = {}
file_path = meta.get('file', '') file_path = meta.get('file', '')
entity_name = meta.get('name', '') entity_name = meta.get('name', '')
entity_type = meta.get('type', '') entity_type = meta.get('type', '')
language = meta.get('language', '') language = meta.get('language', '')
# Build node ID using graph's method # Try multiple node ID formats to find the right one
node_attrs = {"lang": language, "type": entity_type, "file": file_path, "name": entity_name} node_candidates = []
node_id = graph.add_node(node_attrs, return_id=True)
if node_id not in graph.graph: # Format 1: Standard node ID
return {} if entity_name and file_path and language and entity_type:
node_candidates.append(f"{language}::{entity_type}::{file_path}::{entity_name}")
# Get all relationships in one call # Format 2: Method with class name
relationships = graph.get_node_relationships(node_id) if entity_type in ['method', 'function'] and meta.get('class'):
context = {} class_name = meta.get('class')
node_candidates.append(f"{language}::{entity_type}::{file_path}::{class_name}.{entity_name}")
if relationships.get('cross_file'): # Format 3: Svelte component
context['cross_file'] = relationships['cross_file'] if entity_type == 'component' and language == 'svelte':
if relationships.get('same_file'): node_candidates.append(f"svelte::component::{file_path}::{entity_name}")
context['same_file'] = relationships['same_file']
# Build reverse dependencies from incoming relationships # Try to find the node using graph's enhanced search
if relationships.get('incoming'): target_node_id = None
reverse_deps = [] for candidate in node_candidates:
node_file = graph.graph.nodes[node_id].get('file', '') if candidate in graph.graph.nodes:
for pred_id, edge_type, pred_data in relationships['incoming']: target_node_id = candidate
pred_file = pred_data.get('file', '') break
pred_name = pred_data.get('name', '')
pred_type = pred_data.get('type', '')
if pred_file and pred_file != node_file: # If still not found, try attribute-based search
reverse_deps.append(f"{edge_type}:{pred_type}:{pred_name}@{pred_file}") if not target_node_id and entity_name and file_path:
else: matches = list(graph.find_nodes_by_attributes(
reverse_deps.append(f"{edge_type}:{pred_type}:{pred_name}") name=entity_name,
file=file_path,
type=entity_type if entity_type != 'code' else None
))
if matches:
target_node_id = matches[0][0] # Take first match
if reverse_deps: if not target_node_id:
context['used_by'] = reverse_deps return context
# Use the enhanced graph methods to get relationships
cross_file_out, cross_file_in = graph.get_cross_file_relationships(target_node_id, file_path)
if cross_file_out:
context['cross_file'] = cross_file_out
if cross_file_in:
context['used_by'] = cross_file_in
# Get same-file relationships
same_file_rels = []
for neighbor_id in graph.graph.successors(target_node_id):
if neighbor_id in graph.graph.nodes:
neighbor_file = graph.graph.nodes[neighbor_id].get('file', '')
if neighbor_file == file_path: # Same file
edge_data = graph.graph.edges[target_node_id, neighbor_id]
rel_type = edge_data.get('type', 'related')
neighbor_name = graph.graph.nodes[neighbor_id].get('name', '')
neighbor_type = graph.graph.nodes[neighbor_id].get('type', '')
same_file_rels.append(f"{rel_type}:{neighbor_type}:{neighbor_name}")
if same_file_rels:
context['same_file'] = same_file_rels
# Get meaningful file entities (filter out noise)
meaningful_types = {'function', 'method', 'class', 'struct', 'interface', 'enum', 'module', 'component', 'trait', 'impl'}
file_entities = []
file_node_id = f"file::{file_path}"
if file_node_id in graph.graph.nodes:
for neighbor_id, neighbor_data in graph.neighbors(file_node_id):
neighbor_type = neighbor_data.get('type', '')
neighbor_name = neighbor_data.get('name', '')
# Filter criteria
if (neighbor_type in meaningful_types and
neighbor_name and
len(neighbor_name) > 1 and # No single letters
neighbor_name != entity_name and
not neighbor_name.startswith(('c:', 'b:', 'p:', 'r:', 't:', 'v:'))): # No import aliases
file_entities.append(f"{neighbor_type}:{neighbor_name}")
# Get file entities
file_entities = graph.get_file_entities(file_path)
if file_entities: if file_entities:
context['file_contents'] = file_entities context['file_entities'] = file_entities
return context return context