/ src / features / liminal-web-layout / relationship-graph.ts
relationship-graph.ts
  1  /**
  2   * Relationship Graph Utilities
  3   * 
  4   * High-performance in-memory graph database for relationship queries
  5   * Used by liminal web layout system for fast relationship traversal
  6   */
  7  
  8  import { DreamNode } from '../dreamnode';
  9  
 10  /**
 11   * In-memory relationship graph for high-performance queries
 12   */
 13  export interface RelationshipGraph {
 14    /** Map of nodeId to DreamNode for fast lookups */
 15    nodes: Map<string, DreamNode>;
 16    
 17    /** Map of nodeId to array of connected nodeIds */
 18    edges: Map<string, string[]>;
 19    
 20    /** Get all connections for a node */
 21    getConnections: (nodeId: string) => DreamNode[];
 22    
 23    /** Get opposite-type connections only (Dreams ↔ Dreamers) */
 24    getOppositeTypeConnections: (nodeId: string) => DreamNode[];
 25    
 26    /** Get second-degree connections (connections of connections) */
 27    getSecondDegreeConnections: (nodeId: string) => DreamNode[];
 28  }
 29  
 30  /**
 31   * Build relationship graph from array of DreamNodes
 32   */
 33  export function buildRelationshipGraph(dreamNodes: DreamNode[]): RelationshipGraph {
 34    const nodes = new Map<string, DreamNode>();
 35    const edges = new Map<string, string[]>();
 36    
 37    // Build nodes map first
 38    dreamNodes.forEach(node => {
 39      nodes.set(node.id, node);
 40    });
 41    
 42    // Build edges, filtering out connections to non-existent nodes
 43    // This prevents "phantom nodes" in layout calculations
 44    dreamNodes.forEach(node => {
 45      // Defensive: Handle nodes missing liminalWebConnections property
 46      const connections = node.liminalWebConnections || [];
 47      const validConnections = connections.filter(targetId =>
 48        nodes.has(targetId) // Only include connections to nodes that actually exist
 49      );
 50      edges.set(node.id, validConnections);
 51    });
 52    
 53    // Create graph with query methods
 54    const graph: RelationshipGraph = {
 55      nodes,
 56      edges,
 57      
 58      getConnections: (nodeId: string): DreamNode[] => {
 59        const connections = edges.get(nodeId) || [];
 60        return connections
 61          .map(connectedId => nodes.get(connectedId))
 62          .filter((node): node is DreamNode => node !== undefined);
 63      },
 64      
 65      getOppositeTypeConnections: (nodeId: string): DreamNode[] => {
 66        const sourceNode = nodes.get(nodeId);
 67        if (!sourceNode) return [];
 68        
 69        const connections = edges.get(nodeId) || [];
 70        return connections
 71          .map(connectedId => nodes.get(connectedId))
 72          .filter((node): node is DreamNode => 
 73            node !== undefined && node.type !== sourceNode.type
 74          );
 75      },
 76      
 77      getSecondDegreeConnections: (nodeId: string): DreamNode[] => {
 78        const sourceNode = nodes.get(nodeId);
 79        if (!sourceNode) return [];
 80        
 81        const firstDegreeIds = edges.get(nodeId) || [];
 82        const secondDegreeNodes = new Set<DreamNode>();
 83        
 84        firstDegreeIds.forEach(firstDegreeId => {
 85          const secondDegreeIds = edges.get(firstDegreeId) || [];
 86          
 87          secondDegreeIds.forEach(secondDegreeId => {
 88            const secondDegreeNode = nodes.get(secondDegreeId);
 89            
 90            if (secondDegreeNode && 
 91                secondDegreeId !== nodeId && // Don't include source
 92                !firstDegreeIds.includes(secondDegreeId)) { // Don't include first-degree
 93              secondDegreeNodes.add(secondDegreeNode);
 94            }
 95          });
 96        });
 97        
 98        return Array.from(secondDegreeNodes);
 99      }
100    };
101    
102    return graph;
103  }
104  
105  /**
106   * Get relationship statistics for debugging
107   */
108  export function getRelationshipStats(graph: RelationshipGraph): {
109    totalNodes: number;
110    totalEdges: number;
111    dreamNodes: number;
112    dreamerNodes: number;
113    averageConnections: number;
114    maxConnections: number;
115    nodesWithNoConnections: number;
116  } {
117    const totalNodes = graph.nodes.size;
118    let totalEdges = 0;
119    let dreamNodes = 0;
120    let dreamerNodes = 0;
121    let maxConnections = 0;
122    let nodesWithNoConnections = 0;
123    
124    graph.nodes.forEach((node, nodeId) => {
125      const connections = graph.edges.get(nodeId) || [];
126      totalEdges += connections.length;
127      
128      if (node.type === 'dream') dreamNodes++;
129      else dreamerNodes++;
130      
131      if (connections.length === 0) nodesWithNoConnections++;
132      if (connections.length > maxConnections) maxConnections = connections.length;
133    });
134    
135    return {
136      totalNodes,
137      totalEdges,
138      dreamNodes,
139      dreamerNodes,
140      averageConnections: totalNodes > 0 ? totalEdges / totalNodes : 0,
141      maxConnections,
142      nodesWithNoConnections
143    };
144  }
145  
146  /**
147   * Debug helper to log relationships for a specific node
148   */
149  export function logNodeRelationships(graph: RelationshipGraph, nodeId: string): void {
150    const node = graph.nodes.get(nodeId);
151    if (!node) {
152      console.log(`Node ${nodeId} not found in graph`);
153      return;
154    }
155    
156    console.log(`\n=== Relationships for ${node.name} (${node.type}) ===`);
157    
158    const connections = graph.getConnections(nodeId);
159    console.log(`Direct connections (${connections.length}):`, 
160      connections.map(n => `${n.name} (${n.type})`));
161    
162    const oppositeType = graph.getOppositeTypeConnections(nodeId);
163    console.log(`Opposite-type connections (${oppositeType.length}):`, 
164      oppositeType.map(n => `${n.name} (${n.type})`));
165    
166    const secondDegree = graph.getSecondDegreeConnections(nodeId);
167    console.log(`Second-degree connections (${secondDegree.length}):`, 
168      secondDegree.map(n => `${n.name} (${n.type})`));
169  }