diff --git a/graph/graph.py b/graph/graph.py index 65ce56c..9532224 100644 --- a/graph/graph.py +++ b/graph/graph.py @@ -17,9 +17,6 @@ class LocalGraph: def add_node(self, node_id: str, **attrs): self.graph.add_node(node_id, **attrs) - def add_edge(self, src: str, dst: str, edge_type: str, **attrs): - self.graph.add_edge(src, dst, type=edge_type, **attrs) - # --- Persistence --- def save(self): with open(self.graph_path, "wb") as f: @@ -62,7 +59,7 @@ class LocalGraph: import json json.dump(out, f, indent=2) return path - + def to_toon(self): """ Export graph to compact TOON (Token-Oriented Object Notation) format. @@ -95,16 +92,101 @@ class LocalGraph: for n in node_list: row = [n["id"], n["type"], n["name"], n["lang"], n["file"]] escaped = [EnhancedToon._escape_toon_field(str(f)) for f in row] - lines.append(" " + ",".join(escaped)) + lines.append(" " ",".join(escaped)) lines.append(f"edges[{len(edge_list)}]{{src,dst,type}}:") for e in edge_list: row = [e["src"], e["dst"], e["type"]] escaped = [EnhancedToon._escape_toon_field(str(f)) for f in row] - lines.append(" " + ",".join(escaped)) + lines.append(" " ",".join(escaped)) toon_text = "\n".join(lines) path = os.path.join(self.graph_dir, "graph.toon") with open(path, "w", encoding="utf-8") as f: f.write(toon_text) return path + + def _parse_node_id(self, node_id: str): + """ + Parse node_id into components and return a dict of canonical attributes. + + Supported formats: + 1) lang::node_type::file_path::name (full chunk/file tied) + 2) lang::node_type::name (file-less canonical form - Option B) + 3) file:: (file nodes) + otherwise -> fallback to Symbol + """ + attrs = { + "type": "Symbol", + "name": node_id, + "lang": "", + "file": "" + } + if not isinstance(node_id, str): + return attrs + parts = node_id.split("::") + if len(parts) == 4: + lang, node_type, file_path, name = parts + attrs["type"] = node_type + attrs["name"] = name + attrs["lang"] = lang + attrs["file"] = file_path + elif len(parts) == 3: + # Option B canonical file-less representation: lang::kind::entity_name + lang, node_type, name = parts + attrs["type"] = node_type + attrs["name"] = name + attrs["lang"] = lang + attrs["file"] = "" + elif len(parts) == 2 and parts[0] == "file": + # file:: + attrs["type"] = "File" + attrs["name"] = parts[1] + attrs["file"] = parts[1] + attrs["lang"] = "" + else: + # fallback: try to be helpful by guessing the name + if len(parts) >= 1: + attrs["name"] = parts[-1] + return attrs + + def _ensure_node_exists(self, node_id: str): + """ + Ensure a node with node_id exists in the graph. + If it doesn't, create it using sensible attributes derived from the node_id format. + """ + if node_id in self.graph.nodes: + return + parsed = self._parse_node_id(node_id) + # Use add_node (keeps behaviour consistent) + self.add_node(node_id, **{ + "type": parsed.get("type", "Symbol"), + "name": parsed.get("name", node_id), + "lang": parsed.get("lang", ""), + "file": parsed.get("file", "") + }) + + def add_edge(self, src: str, dst: str, edge_type: str, **attrs): + """ + Add an edge but first ensure both source and destination nodes exist and have basic attributes. + This prevents the creation of attribute-less nodes and makes semantic edges meaningful. + """ + try: + # Ensure source node exists (create minimal entry if missing) + if src not in self.graph.nodes: + self._ensure_node_exists(src) + + # Ensure destination node exists (create minimal entry if missing) + if dst not in self.graph.nodes: + self._ensure_node_exists(dst) + + # Finally add the edge with type and any extra attrs + self.graph.add_edge(src, dst, type=edge_type, **attrs) + except Exception: + # Keep behaviour non-fatal for indexing runs - log if you have a logger available + # fallback: still attempt to add the edge + try: + self.graph.add_edge(src, dst, type=edge_type, **attrs) + except Exception: + # swallow; graph should remain usable + pass