/ core / graph / edge_drawer.py
edge_drawer.py
  1  """
  2  Edge Drawer for Sovereign OS
  3  
  4  Manages graph edges - creating, updating, and removing connections
  5  between nodes based on detected resonance and thread state.
  6  
  7  Works with both GRAPH-STATE.md (the God Database) and 
  8  LIVE-GRAPH.md (Mermaid visualization).
  9  """
 10  
 11  import re
 12  import logging
 13  from pathlib import Path
 14  from dataclasses import dataclass
 15  from typing import Dict, List, Optional, Set, Tuple
 16  from datetime import datetime
 17  from enum import Enum
 18  
 19  logger = logging.getLogger(__name__)
 20  
 21  
 22  class EdgeType(Enum):
 23      """Types of edges in the knowledge graph."""
 24      DEPENDS_ON = "depends_on"      # A requires B
 25      ENABLES = "enables"            # A makes B possible
 26      BLOCKS = "blocks"              # A prevents B
 27      RESONATES_WITH = "resonates"   # A and B share patterns
 28      CONVERGES_WITH = "converges"   # A and B reaching same conclusion
 29      DIVERGES_FROM = "diverges"     # A and B contradict
 30  
 31  
 32  @dataclass
 33  class Edge:
 34      """An edge in the knowledge graph."""
 35      
 36      source: str
 37      target: str
 38      edge_type: EdgeType
 39      pattern: Optional[str] = None  # For resonance edges
 40      created: Optional[datetime] = None
 41      weight: float = 1.0
 42      
 43      def to_mermaid(self) -> str:
 44          """Convert to Mermaid edge syntax."""
 45          source_safe = re.sub(r"[^a-zA-Z0-9_]", "_", self.source)
 46          target_safe = re.sub(r"[^a-zA-Z0-9_]", "_", self.target)
 47          
 48          if self.edge_type == EdgeType.RESONATES_WITH:
 49              return f"    {source_safe} -.-|{self.pattern or 'resonates'}| {target_safe}"
 50          elif self.edge_type == EdgeType.BLOCKS:
 51              return f"    {source_safe} -->|blocks| {target_safe}"
 52          elif self.edge_type == EdgeType.DIVERGES_FROM:
 53              return f"    {source_safe} x--x|diverges| {target_safe}"
 54          elif self.edge_type == EdgeType.CONVERGES_WITH:
 55              return f"    {source_safe} <-->|converges| {target_safe}"
 56          else:
 57              label = self.edge_type.value.replace("_", " ")
 58              return f"    {source_safe} -->|{label}| {target_safe}"
 59      
 60      def to_markdown_row(self) -> str:
 61          """Convert to markdown table row."""
 62          return (
 63              f"| {self.source} | {self.edge_type.value} | {self.target} | "
 64              f"{self.weight:.2f} | {self.pattern or '-'} |"
 65          )
 66  
 67  
 68  class EdgeDrawer:
 69      """
 70      Manages edges in the Sovereign OS knowledge graph.
 71      
 72      Responsibilities:
 73      - Track all edges between nodes
 74      - Add edges when resonance detected
 75      - Update GRAPH-STATE.md with new edges
 76      - Update LIVE-GRAPH.md Mermaid diagram
 77      """
 78      
 79      def __init__(self, sessions_dir: Path):
 80          self.sessions_dir = Path(sessions_dir)
 81          self._edges: Dict[Tuple[str, str], Edge] = {}
 82          self._pending_updates: List[Edge] = []
 83      
 84      def add_edge(self, edge: Edge) -> bool:
 85          """
 86          Add or update an edge.
 87          
 88          Returns True if edge was new, False if updated existing.
 89          """
 90          key = (edge.source, edge.target)
 91          
 92          is_new = key not in self._edges
 93          self._edges[key] = edge
 94          self._pending_updates.append(edge)
 95          
 96          if is_new:
 97              logger.info(f"New edge: {edge.source} --{edge.edge_type.value}--> {edge.target}")
 98          else:
 99              logger.debug(f"Updated edge: {edge.source} --{edge.edge_type.value}--> {edge.target}")
100          
101          return is_new
102      
103      def add_resonance_edge(
104          self, 
105          thread1: str, 
106          thread2: str, 
107          pattern: str,
108          strength: float = 0.8
109      ) -> Edge:
110          """Convenience method to add a resonance edge."""
111          edge = Edge(
112              source=thread1,
113              target=thread2,
114              edge_type=EdgeType.RESONATES_WITH,
115              pattern=pattern,
116              created=datetime.now(),
117              weight=strength
118          )
119          self.add_edge(edge)
120          return edge
121      
122      def add_dependency_edge(self, dependent: str, dependency: str) -> Edge:
123          """Convenience method to add a dependency edge."""
124          edge = Edge(
125              source=dependent,
126              target=dependency,
127              edge_type=EdgeType.DEPENDS_ON,
128              created=datetime.now()
129          )
130          self.add_edge(edge)
131          return edge
132      
133      def add_blocking_edge(self, blocker: str, blocked: str) -> Edge:
134          """Convenience method to add a blocking edge."""
135          edge = Edge(
136              source=blocker,
137              target=blocked,
138              edge_type=EdgeType.BLOCKS,
139              created=datetime.now()
140          )
141          self.add_edge(edge)
142          return edge
143      
144      def remove_edge(self, source: str, target: str) -> bool:
145          """Remove an edge. Returns True if edge existed."""
146          key = (source, target)
147          if key in self._edges:
148              del self._edges[key]
149              logger.info(f"Removed edge: {source} --> {target}")
150              return True
151          return False
152      
153      def get_edges_for_node(self, node_id: str) -> List[Edge]:
154          """Get all edges involving a node."""
155          return [
156              edge for edge in self._edges.values()
157              if edge.source == node_id or edge.target == node_id
158          ]
159      
160      def get_resonance_edges(self) -> List[Edge]:
161          """Get all resonance edges."""
162          return [
163              edge for edge in self._edges.values()
164              if edge.edge_type == EdgeType.RESONATES_WITH
165          ]
166      
167      def flush_to_graph_state(self) -> None:
168          """
169          Flush pending edge updates to GRAPH-STATE.md.
170          
171          Appends new edges to the Recent Graph Changes section.
172          """
173          if not self._pending_updates:
174              return
175          
176          graph_state_path = self.sessions_dir / "GRAPH-STATE.md"
177          
178          if not graph_state_path.exists():
179              logger.warning("GRAPH-STATE.md not found, cannot flush edges")
180              return
181          
182          content = graph_state_path.read_text(encoding="utf-8")
183          
184          # Find "Recent Graph Changes" section
185          changes_section = re.search(
186              r"(## Recent Graph Changes.*?\n\|.*?\n\|.*?\n)(.*?)(?=\n---|\Z)",
187              content,
188              re.DOTALL
189          )
190          
191          if changes_section:
192              header = changes_section.group(1)
193              existing = changes_section.group(2)
194              
195              # Add new entries
196              now = datetime.now().strftime("%H:%M")
197              new_entries = []
198              for edge in self._pending_updates:
199                  new_entries.append(
200                      f"| {now} | Added edge | {edge.source} → {edge.target} |"
201                  )
202              
203              updated_section = header + "\n".join(new_entries) + "\n" + existing
204              content = content[:changes_section.start()] + updated_section + content[changes_section.end():]
205              
206              graph_state_path.write_text(content, encoding="utf-8")
207              logger.info(f"Flushed {len(self._pending_updates)} edge updates to GRAPH-STATE.md")
208          
209          self._pending_updates.clear()
210      
211      def render_mermaid_edges(self) -> str:
212          """Render all edges as Mermaid syntax."""
213          lines = []
214          for edge in self._edges.values():
215              lines.append(edge.to_mermaid())
216          return "\n".join(lines)
217      
218      def update_live_graph(self) -> None:
219          """
220          Update LIVE-GRAPH.md with current edges.
221          
222          Finds the Mermaid code block and updates the edges section.
223          """
224          live_graph_path = self.sessions_dir / "LIVE-GRAPH.md"
225          
226          if not live_graph_path.exists():
227              logger.warning("LIVE-GRAPH.md not found, cannot update edges")
228              return
229          
230          content = live_graph_path.read_text(encoding="utf-8")
231          
232          # Find the mermaid block
233          mermaid_match = re.search(
234              r"(```mermaid\n.*?)(    %% Edges.*?)(```)",
235              content,
236              re.DOTALL
237          )
238          
239          if mermaid_match:
240              # Insert new edges before closing
241              before = mermaid_match.group(1)
242              
243              edges_section = "    %% Edges (auto-generated)\n"
244              edges_section += self.render_mermaid_edges() + "\n"
245              
246              content = before + edges_section + "```"
247              
248              # Update timestamp
249              content = re.sub(
250                  r"\*\*Updated:\*\* [^\n]+",
251                  f"**Updated:** {datetime.now().strftime('%Y-%m-%d %H:%M')}",
252                  content
253              )
254              
255              live_graph_path.write_text(content, encoding="utf-8")
256              logger.info("Updated LIVE-GRAPH.md with current edges")
257      
258      def get_statistics(self) -> Dict:
259          """Get edge statistics."""
260          by_type = {}
261          for edge in self._edges.values():
262              t = edge.edge_type.value
263              by_type[t] = by_type.get(t, 0) + 1
264          
265          return {
266              "total_edges": len(self._edges),
267              "by_type": by_type,
268              "pending_updates": len(self._pending_updates)
269          }
270  
271  
272  # CLI for testing
273  if __name__ == "__main__":
274      import sys
275      
276      sessions_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("./sessions")
277      
278      drawer = EdgeDrawer(sessions_dir)
279      
280      # Add some test edges
281      drawer.add_resonance_edge(
282          "alignment-protocol",
283          "three-repo",
284          "boundary permeability",
285          0.85
286      )
287      
288      drawer.add_dependency_edge(
289          "permeability-contracts",
290          "three-repo"
291      )
292      
293      print("Edge Statistics:")
294      stats = drawer.get_statistics()
295      for key, value in stats.items():
296          print(f"  {key}: {value}")
297      
298      print("\nMermaid Output:")
299      print(drawer.render_mermaid_edges())