1139 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1139 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package temporal
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| 	"sort"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/ucxl"
 | |
| )
 | |
| 
 | |
| // influenceAnalyzerImpl implements the InfluenceAnalyzer interface
 | |
| type influenceAnalyzerImpl struct {
 | |
| 	mu sync.RWMutex
 | |
| 
 | |
| 	// Reference to the temporal graph
 | |
| 	graph *temporalGraphImpl
 | |
| 
 | |
| 	// Cached analysis results
 | |
| 	networkAnalysisCache *InfluenceNetworkAnalysis
 | |
| 	centralityCache      *CentralityMetrics
 | |
| 	lastAnalysisTime     time.Time
 | |
| 	cacheValidDuration   time.Duration
 | |
| 
 | |
| 	// Analysis parameters
 | |
| 	dampingFactor        float64 // For PageRank calculation
 | |
| 	maxIterations        int     // For iterative algorithms
 | |
| 	convergenceThreshold float64 // For convergence checking
 | |
| }
 | |
| 
 | |
| // NewInfluenceAnalyzer creates a new influence analyzer
 | |
| func NewInfluenceAnalyzer(graph *temporalGraphImpl) InfluenceAnalyzer {
 | |
| 	return &influenceAnalyzerImpl{
 | |
| 		graph:                graph,
 | |
| 		cacheValidDuration:   time.Minute * 30,
 | |
| 		dampingFactor:        0.85, // Standard PageRank damping factor
 | |
| 		maxIterations:        100,
 | |
| 		convergenceThreshold: 1e-6,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // AnalyzeInfluenceNetwork analyzes the structure of decision influence relationships
 | |
| func (ia *influenceAnalyzerImpl) AnalyzeInfluenceNetwork(ctx context.Context) (*InfluenceNetworkAnalysis, error) {
 | |
| 	ia.mu.Lock()
 | |
| 	defer ia.mu.Unlock()
 | |
| 
 | |
| 	// Check if cached analysis is still valid
 | |
| 	if ia.networkAnalysisCache != nil && time.Since(ia.lastAnalysisTime) < ia.cacheValidDuration {
 | |
| 		return ia.networkAnalysisCache, nil
 | |
| 	}
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	totalNodes := len(ia.graph.nodes)
 | |
| 	totalEdges := 0
 | |
| 
 | |
| 	// Count total edges
 | |
| 	for _, influences := range ia.graph.influences {
 | |
| 		totalEdges += len(influences)
 | |
| 	}
 | |
| 
 | |
| 	// Calculate network density
 | |
| 	maxPossibleEdges := totalNodes * (totalNodes - 1)
 | |
| 	networkDensity := 0.0
 | |
| 	if maxPossibleEdges > 0 {
 | |
| 		networkDensity = float64(totalEdges) / float64(maxPossibleEdges)
 | |
| 	}
 | |
| 
 | |
| 	// Calculate clustering coefficient
 | |
| 	clusteringCoeff := ia.calculateClusteringCoefficient()
 | |
| 
 | |
| 	// Calculate average path length
 | |
| 	avgPathLength := ia.calculateAveragePathLength()
 | |
| 
 | |
| 	// Find central nodes
 | |
| 	centralNodes := ia.findCentralNodes()
 | |
| 
 | |
| 	// Detect communities
 | |
| 	communities := ia.detectCommunities()
 | |
| 
 | |
| 	analysis := &InfluenceNetworkAnalysis{
 | |
| 		TotalNodes:        totalNodes,
 | |
| 		TotalEdges:        totalEdges,
 | |
| 		NetworkDensity:    networkDensity,
 | |
| 		ClusteringCoeff:   clusteringCoeff,
 | |
| 		AveragePathLength: avgPathLength,
 | |
| 		CentralNodes:      centralNodes,
 | |
| 		Communities:       communities,
 | |
| 		AnalyzedAt:        time.Now(),
 | |
| 	}
 | |
| 
 | |
| 	// Cache the results
 | |
| 	ia.networkAnalysisCache = analysis
 | |
| 	ia.lastAnalysisTime = time.Now()
 | |
| 
 | |
| 	return analysis, nil
 | |
| }
 | |
| 
 | |
| // GetInfluenceStrength calculates influence strength between contexts
 | |
| func (ia *influenceAnalyzerImpl) GetInfluenceStrength(ctx context.Context, influencer, influenced ucxl.Address) (float64, error) {
 | |
| 	ia.mu.RLock()
 | |
| 	defer ia.mu.RUnlock()
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	// Get the latest nodes for both addresses
 | |
| 	influencerNode, err := ia.graph.getLatestNodeUnsafe(influencer)
 | |
| 	if err != nil {
 | |
| 		return 0, fmt.Errorf("influencer node not found: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	influencedNode, err := ia.graph.getLatestNodeUnsafe(influenced)
 | |
| 	if err != nil {
 | |
| 		return 0, fmt.Errorf("influenced node not found: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Check if direct influence exists
 | |
| 	influences, exists := ia.graph.influences[influencerNode.ID]
 | |
| 	if !exists {
 | |
| 		return 0, nil
 | |
| 	}
 | |
| 
 | |
| 	hasDirectInfluence := false
 | |
| 	for _, influencedID := range influences {
 | |
| 		if influencedID == influencedNode.ID {
 | |
| 			hasDirectInfluence = true
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if !hasDirectInfluence {
 | |
| 		return 0, nil
 | |
| 	}
 | |
| 
 | |
| 	// Calculate influence strength based on multiple factors
 | |
| 	strength := 0.0
 | |
| 
 | |
| 	// Factor 1: Decision recency (more recent = stronger influence)
 | |
| 	timeDiff := time.Since(influencerNode.Timestamp)
 | |
| 	recencyFactor := math.Max(0, 1.0-timeDiff.Hours()/(7*24)) // Decay over a week
 | |
| 	strength += recencyFactor * 0.3
 | |
| 
 | |
| 	// Factor 2: Confidence level of the influencer
 | |
| 	confidenceFactor := influencerNode.Confidence
 | |
| 	strength += confidenceFactor * 0.3
 | |
| 
 | |
| 	// Factor 3: Impact scope (broader scope = stronger influence)
 | |
| 	scopeFactor := 0.0
 | |
| 	switch influencerNode.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		scopeFactor = 1.0
 | |
| 	case ImpactProject:
 | |
| 		scopeFactor = 0.8
 | |
| 	case ImpactModule:
 | |
| 		scopeFactor = 0.6
 | |
| 	case ImpactLocal:
 | |
| 		scopeFactor = 0.4
 | |
| 	}
 | |
| 	strength += scopeFactor * 0.2
 | |
| 
 | |
| 	// Factor 4: Network position (central nodes have stronger influence)
 | |
| 	centralityFactor := ia.getNodeCentrality(influencerNode.ID)
 | |
| 	strength += centralityFactor * 0.2
 | |
| 
 | |
| 	return math.Min(strength, 1.0), nil
 | |
| }
 | |
| 
 | |
| // FindInfluentialDecisions finds the most influential decisions in the system
 | |
| func (ia *influenceAnalyzerImpl) FindInfluentialDecisions(ctx context.Context, limit int) ([]*InfluentialDecision, error) {
 | |
| 	ia.mu.RLock()
 | |
| 	defer ia.mu.RLock()
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	type nodeScore struct {
 | |
| 		node  *TemporalNode
 | |
| 		score float64
 | |
| 	}
 | |
| 
 | |
| 	scores := make([]*nodeScore, 0, len(ia.graph.nodes))
 | |
| 
 | |
| 	// Calculate influence score for each node
 | |
| 	for _, node := range ia.graph.nodes {
 | |
| 		score := ia.calculateInfluenceScore(node)
 | |
| 		scores = append(scores, &nodeScore{node: node, score: score})
 | |
| 	}
 | |
| 
 | |
| 	// Sort by influence score (highest first)
 | |
| 	sort.Slice(scores, func(i, j int) bool {
 | |
| 		return scores[i].score > scores[j].score
 | |
| 	})
 | |
| 
 | |
| 	// Convert to InfluentialDecision structs
 | |
| 	influential := make([]*InfluentialDecision, 0)
 | |
| 	maxResults := limit
 | |
| 	if maxResults <= 0 || maxResults > len(scores) {
 | |
| 		maxResults = len(scores)
 | |
| 	}
 | |
| 
 | |
| 	for i := 0; i < maxResults; i++ {
 | |
| 		nodeScore := scores[i]
 | |
| 		node := nodeScore.node
 | |
| 
 | |
| 		// Analyze impact of this decision
 | |
| 		impact := ia.analyzeDecisionImpactInternal(node)
 | |
| 
 | |
| 		decision := &InfluentialDecision{
 | |
| 			Address:          node.UCXLAddress,
 | |
| 			DecisionHop:      node.Version,
 | |
| 			InfluenceScore:   nodeScore.score,
 | |
| 			AffectedContexts: node.Influences,
 | |
| 			DecisionMetadata: ia.graph.decisions[node.DecisionID],
 | |
| 			ImpactAnalysis:   impact,
 | |
| 			InfluenceReasons: ia.getInfluenceReasons(node, nodeScore.score),
 | |
| 		}
 | |
| 
 | |
| 		influential = append(influential, decision)
 | |
| 	}
 | |
| 
 | |
| 	return influential, nil
 | |
| }
 | |
| 
 | |
| // AnalyzeDecisionImpact analyzes the impact of a specific decision
 | |
| func (ia *influenceAnalyzerImpl) AnalyzeDecisionImpact(ctx context.Context, address ucxl.Address, decisionHop int) (*DecisionImpact, error) {
 | |
| 	ia.mu.RLock()
 | |
| 	defer ia.mu.RUnlock()
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	// Get the specific decision version
 | |
| 	node, err := ia.graph.GetVersionAtDecision(ctx, address, decisionHop)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("decision not found: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return ia.analyzeDecisionImpactInternal(node), nil
 | |
| }
 | |
| 
 | |
| // PredictInfluence predicts likely influence relationships
 | |
| func (ia *influenceAnalyzerImpl) PredictInfluence(ctx context.Context, address ucxl.Address) ([]*PredictedInfluence, error) {
 | |
| 	ia.mu.RLock()
 | |
| 	defer ia.mu.RUnlock()
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	node, err := ia.graph.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("node not found: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	predictions := make([]*PredictedInfluence, 0)
 | |
| 
 | |
| 	// Analyze patterns to predict new influences
 | |
| 	for targetAddress, targetNodes := range ia.graph.addressToNodes {
 | |
| 		if targetAddress == address.String() {
 | |
| 			continue // Skip self
 | |
| 		}
 | |
| 
 | |
| 		targetNode := targetNodes[len(targetNodes)-1] // Latest version
 | |
| 
 | |
| 		// Skip if influence already exists
 | |
| 		if ia.hasDirectInfluence(node, targetNode) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Calculate prediction probability based on various factors
 | |
| 		probability := ia.calculateInfluenceProbability(node, targetNode)
 | |
| 
 | |
| 		if probability > 0.3 { // Threshold for meaningful predictions
 | |
| 			prediction := &PredictedInfluence{
 | |
| 				From:           address,
 | |
| 				To:             targetNode.UCXLAddress,
 | |
| 				Probability:    probability,
 | |
| 				Strength:       probability * 0.8, // Predicted strength slightly lower than probability
 | |
| 				Reasons:        ia.getPredictionReasons(node, targetNode),
 | |
| 				Confidence:     probability * 0.9,
 | |
| 				EstimatedDelay: time.Duration(float64(time.Hour*24) * (1.0 - probability)),
 | |
| 			}
 | |
| 			predictions = append(predictions, prediction)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Sort by probability (highest first)
 | |
| 	sort.Slice(predictions, func(i, j int) bool {
 | |
| 		return predictions[i].Probability > predictions[j].Probability
 | |
| 	})
 | |
| 
 | |
| 	// Limit to top 10 predictions
 | |
| 	if len(predictions) > 10 {
 | |
| 		predictions = predictions[:10]
 | |
| 	}
 | |
| 
 | |
| 	return predictions, nil
 | |
| }
 | |
| 
 | |
| // GetCentralityMetrics calculates centrality metrics for contexts
 | |
| func (ia *influenceAnalyzerImpl) GetCentralityMetrics(ctx context.Context) (*CentralityMetrics, error) {
 | |
| 	ia.mu.Lock()
 | |
| 	defer ia.mu.Unlock()
 | |
| 
 | |
| 	// Check cache
 | |
| 	if ia.centralityCache != nil && time.Since(ia.lastAnalysisTime) < ia.cacheValidDuration {
 | |
| 		return ia.centralityCache, nil
 | |
| 	}
 | |
| 
 | |
| 	ia.graph.mu.RLock()
 | |
| 	defer ia.graph.mu.RUnlock()
 | |
| 
 | |
| 	metrics := &CentralityMetrics{
 | |
| 		BetweennessCentrality: make(map[string]float64),
 | |
| 		ClosenessCentrality:   make(map[string]float64),
 | |
| 		DegreeCentrality:      make(map[string]float64),
 | |
| 		EigenvectorCentrality: make(map[string]float64),
 | |
| 		PageRank:              make(map[string]float64),
 | |
| 		CalculatedAt:          time.Now(),
 | |
| 	}
 | |
| 
 | |
| 	// Calculate degree centrality
 | |
| 	ia.calculateDegreeCentrality(metrics)
 | |
| 
 | |
| 	// Calculate betweenness centrality
 | |
| 	ia.calculateBetweennessCentrality(metrics)
 | |
| 
 | |
| 	// Calculate closeness centrality
 | |
| 	ia.calculateClosenessCentrality(metrics)
 | |
| 
 | |
| 	// Calculate eigenvector centrality
 | |
| 	ia.calculateEigenvectorCentrality(metrics)
 | |
| 
 | |
| 	// Calculate PageRank
 | |
| 	ia.calculatePageRank(metrics)
 | |
| 
 | |
| 	// Cache the results
 | |
| 	ia.centralityCache = metrics
 | |
| 	ia.lastAnalysisTime = time.Now()
 | |
| 
 | |
| 	return metrics, nil
 | |
| }
 | |
| 
 | |
| // Helper methods
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateClusteringCoefficient() float64 {
 | |
| 	totalCoeff := 0.0
 | |
| 	nodeCount := 0
 | |
| 
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		coeff := ia.calculateNodeClusteringCoefficient(nodeID)
 | |
| 		totalCoeff += coeff
 | |
| 		nodeCount++
 | |
| 	}
 | |
| 
 | |
| 	if nodeCount == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return totalCoeff / float64(nodeCount)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateNodeClusteringCoefficient(nodeID string) float64 {
 | |
| 	neighbors := ia.getNeighbors(nodeID)
 | |
| 
 | |
| 	if len(neighbors) < 2 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	// Count connections between neighbors
 | |
| 	connections := 0
 | |
| 	for i, neighbor1 := range neighbors {
 | |
| 		for j := i + 1; j < len(neighbors); j++ {
 | |
| 			neighbor2 := neighbors[j]
 | |
| 			if ia.areConnected(neighbor1, neighbor2) {
 | |
| 				connections++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	possibleConnections := len(neighbors) * (len(neighbors) - 1) / 2
 | |
| 	if possibleConnections == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(connections) / float64(possibleConnections)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateAveragePathLength() float64 {
 | |
| 	totalPaths := 0
 | |
| 	totalLength := 0.0
 | |
| 
 | |
| 	// Sample a subset of node pairs to avoid O(n³) complexity
 | |
| 	nodeIDs := make([]string, 0, len(ia.graph.nodes))
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		nodeIDs = append(nodeIDs, nodeID)
 | |
| 	}
 | |
| 
 | |
| 	sampleSize := min(100, len(nodeIDs)) // Sample up to 100 nodes
 | |
| 	for i := 0; i < sampleSize; i++ {
 | |
| 		for j := i + 1; j < sampleSize; j++ {
 | |
| 			pathLength := ia.findShortestPathLength(nodeIDs[i], nodeIDs[j])
 | |
| 			if pathLength > 0 {
 | |
| 				totalLength += float64(pathLength)
 | |
| 				totalPaths++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if totalPaths == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return totalLength / float64(totalPaths)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) findCentralNodes() []CentralNode {
 | |
| 	centralNodes := make([]CentralNode, 0)
 | |
| 
 | |
| 	// Calculate various centrality measures for each node
 | |
| 	for nodeID, node := range ia.graph.nodes {
 | |
| 		degreeCentrality := ia.calculateNodeDegreeCentrality(nodeID)
 | |
| 		betweennessCentrality := ia.calculateNodeBetweennessCentrality(nodeID)
 | |
| 		closenessCentrality := ia.calculateNodeClosenessCentrality(nodeID)
 | |
| 		pageRank := ia.calculateNodePageRank(nodeID)
 | |
| 
 | |
| 		// Calculate overall influence score
 | |
| 		influenceScore := (degreeCentrality + betweennessCentrality + closenessCentrality + pageRank) / 4.0
 | |
| 
 | |
| 		if influenceScore > 0.5 { // Threshold for "central"
 | |
| 			centralNode := CentralNode{
 | |
| 				Address:               node.UCXLAddress,
 | |
| 				BetweennessCentrality: betweennessCentrality,
 | |
| 				ClosenessCentrality:   closenessCentrality,
 | |
| 				DegreeCentrality:      degreeCentrality,
 | |
| 				PageRank:              pageRank,
 | |
| 				InfluenceScore:        influenceScore,
 | |
| 			}
 | |
| 			centralNodes = append(centralNodes, centralNode)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Sort by influence score
 | |
| 	sort.Slice(centralNodes, func(i, j int) bool {
 | |
| 		return centralNodes[i].InfluenceScore > centralNodes[j].InfluenceScore
 | |
| 	})
 | |
| 
 | |
| 	// Limit to top 20
 | |
| 	if len(centralNodes) > 20 {
 | |
| 		centralNodes = centralNodes[:20]
 | |
| 	}
 | |
| 
 | |
| 	return centralNodes
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) detectCommunities() []Community {
 | |
| 	// Simple community detection based on modularity
 | |
| 	communities := make([]Community, 0)
 | |
| 
 | |
| 	// Use a simple approach: group nodes with high interconnectivity
 | |
| 	visited := make(map[string]bool)
 | |
| 	communityID := 0
 | |
| 
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		if visited[nodeID] {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Start a new community
 | |
| 		community := ia.expandCommunity(nodeID, visited)
 | |
| 		if len(community) >= 3 { // Minimum community size
 | |
| 			communityStruct := Community{
 | |
| 				ID:          fmt.Sprintf("community-%d", communityID),
 | |
| 				Nodes:       community,
 | |
| 				Modularity:  ia.calculateCommunityModularity(community),
 | |
| 				Density:     ia.calculateCommunityDensity(community),
 | |
| 				Description: fmt.Sprintf("Community of %d related decisions", len(community)),
 | |
| 				Tags:        []string{"auto-detected"},
 | |
| 			}
 | |
| 			communities = append(communities, communityStruct)
 | |
| 			communityID++
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return communities
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateInfluenceScore(node *TemporalNode) float64 {
 | |
| 	score := 0.0
 | |
| 
 | |
| 	// Factor 1: Number of direct influences (30%)
 | |
| 	directInfluences := len(ia.graph.influences[node.ID])
 | |
| 	score += float64(directInfluences) * 0.3
 | |
| 
 | |
| 	// Factor 2: Network centrality (25%)
 | |
| 	centrality := ia.getNodeCentrality(node.ID)
 | |
| 	score += centrality * 0.25
 | |
| 
 | |
| 	// Factor 3: Decision confidence (20%)
 | |
| 	score += node.Confidence * 0.2
 | |
| 
 | |
| 	// Factor 4: Impact scope (15%)
 | |
| 	scopeWeight := 0.0
 | |
| 	switch node.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		scopeWeight = 1.0
 | |
| 	case ImpactProject:
 | |
| 		scopeWeight = 0.8
 | |
| 	case ImpactModule:
 | |
| 		scopeWeight = 0.6
 | |
| 	case ImpactLocal:
 | |
| 		scopeWeight = 0.4
 | |
| 	}
 | |
| 	score += scopeWeight * 0.15
 | |
| 
 | |
| 	// Factor 5: Recency (10%)
 | |
| 	timeSinceDecision := time.Since(node.Timestamp)
 | |
| 	recencyScore := math.Max(0, 1.0-timeSinceDecision.Hours()/(30*24)) // Decay over 30 days
 | |
| 	score += recencyScore * 0.1
 | |
| 
 | |
| 	return score
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) analyzeDecisionImpactInternal(node *TemporalNode) *DecisionImpact {
 | |
| 	// Find direct and indirect impact
 | |
| 	directImpact := make([]ucxl.Address, len(node.Influences))
 | |
| 	copy(directImpact, node.Influences)
 | |
| 
 | |
| 	// Find indirect impact (influenced nodes that are influenced by direct impact)
 | |
| 	indirectImpact := make([]ucxl.Address, 0)
 | |
| 	impactRadius := 1
 | |
| 
 | |
| 	visited := make(map[string]bool)
 | |
| 	queue := []string{node.ID}
 | |
| 	visited[node.ID] = true
 | |
| 
 | |
| 	for len(queue) > 0 && impactRadius <= 3 { // Limit to 3 hops
 | |
| 		levelSize := len(queue)
 | |
| 		for i := 0; i < levelSize; i++ {
 | |
| 			currentID := queue[0]
 | |
| 			queue = queue[1:]
 | |
| 
 | |
| 			if influences, exists := ia.graph.influences[currentID]; exists {
 | |
| 				for _, influencedID := range influences {
 | |
| 					if !visited[influencedID] {
 | |
| 						visited[influencedID] = true
 | |
| 						queue = append(queue, influencedID)
 | |
| 
 | |
| 						if influencedNode, exists := ia.graph.nodes[influencedID]; exists {
 | |
| 							if impactRadius > 1 { // Indirect impact
 | |
| 								indirectImpact = append(indirectImpact, influencedNode.UCXLAddress)
 | |
| 							}
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		impactRadius++
 | |
| 	}
 | |
| 
 | |
| 	// Calculate impact strength
 | |
| 	impactStrength := float64(len(directImpact))*1.0 + float64(len(indirectImpact))*0.5
 | |
| 	impactStrength = math.Min(impactStrength/10.0, 1.0) // Normalize to 0-1
 | |
| 
 | |
| 	// Estimate propagation time
 | |
| 	propagationTime := time.Duration(impactRadius) * time.Hour * 24
 | |
| 
 | |
| 	return &DecisionImpact{
 | |
| 		Address:           node.UCXLAddress,
 | |
| 		DecisionHop:       node.Version,
 | |
| 		DirectImpact:      directImpact,
 | |
| 		IndirectImpact:    indirectImpact,
 | |
| 		ImpactRadius:      impactRadius - 1,
 | |
| 		ImpactStrength:    impactStrength,
 | |
| 		PropagationTime:   propagationTime,
 | |
| 		ImpactCategories:  []string{"influence_network", "decision_cascade"},
 | |
| 		MitigationActions: []string{"review_affected_contexts", "validate_assumptions"},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) getInfluenceReasons(node *TemporalNode, score float64) []string {
 | |
| 	reasons := make([]string, 0)
 | |
| 
 | |
| 	if len(node.Influences) > 3 {
 | |
| 		reasons = append(reasons, "influences many contexts")
 | |
| 	}
 | |
| 
 | |
| 	if node.Confidence > 0.8 {
 | |
| 		reasons = append(reasons, "high confidence decision")
 | |
| 	}
 | |
| 
 | |
| 	if node.ImpactScope == ImpactSystem || node.ImpactScope == ImpactProject {
 | |
| 		reasons = append(reasons, "broad impact scope")
 | |
| 	}
 | |
| 
 | |
| 	if score > 0.8 {
 | |
| 		reasons = append(reasons, "high network centrality")
 | |
| 	}
 | |
| 
 | |
| 	return reasons
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) hasDirectInfluence(from, to *TemporalNode) bool {
 | |
| 	influences, exists := ia.graph.influences[from.ID]
 | |
| 	if !exists {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	for _, influencedID := range influences {
 | |
| 		if influencedID == to.ID {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateInfluenceProbability(from, to *TemporalNode) float64 {
 | |
| 	probability := 0.0
 | |
| 
 | |
| 	// Factor 1: Technology similarity
 | |
| 	techSimilarity := ia.calculateTechnologySimilarity(from, to)
 | |
| 	probability += techSimilarity * 0.3
 | |
| 
 | |
| 	// Factor 2: Temporal proximity
 | |
| 	timeDiff := math.Abs(from.Timestamp.Sub(to.Timestamp).Hours())
 | |
| 	temporalProximity := math.Max(0, 1.0-timeDiff/(7*24)) // Closer in time = higher probability
 | |
| 	probability += temporalProximity * 0.2
 | |
| 
 | |
| 	// Factor 3: Common influencers
 | |
| 	commonInfluencers := ia.countCommonInfluencers(from, to)
 | |
| 	probability += math.Min(float64(commonInfluencers)/3.0, 1.0) * 0.3
 | |
| 
 | |
| 	// Factor 4: Network distance
 | |
| 	distance := ia.findShortestPathLength(from.ID, to.ID)
 | |
| 	if distance > 0 && distance <= 3 {
 | |
| 		networkProximity := 1.0 - float64(distance-1)/2.0
 | |
| 		probability += networkProximity * 0.2
 | |
| 	}
 | |
| 
 | |
| 	return math.Min(probability, 1.0)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) getPredictionReasons(from, to *TemporalNode) []string {
 | |
| 	reasons := make([]string, 0)
 | |
| 
 | |
| 	if ia.calculateTechnologySimilarity(from, to) > 0.7 {
 | |
| 		reasons = append(reasons, "similar technologies")
 | |
| 	}
 | |
| 
 | |
| 	if ia.countCommonInfluencers(from, to) > 2 {
 | |
| 		reasons = append(reasons, "common influencers")
 | |
| 	}
 | |
| 
 | |
| 	distance := ia.findShortestPathLength(from.ID, to.ID)
 | |
| 	if distance > 0 && distance <= 2 {
 | |
| 		reasons = append(reasons, "close in network")
 | |
| 	}
 | |
| 
 | |
| 	timeDiff := math.Abs(from.Timestamp.Sub(to.Timestamp).Hours())
 | |
| 	if timeDiff < 24 {
 | |
| 		reasons = append(reasons, "recent decisions")
 | |
| 	}
 | |
| 
 | |
| 	return reasons
 | |
| }
 | |
| 
 | |
| // Additional helper methods for centrality calculations
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateDegreeCentrality(metrics *CentralityMetrics) {
 | |
| 	totalNodes := float64(len(ia.graph.nodes))
 | |
| 
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		degree := float64(len(ia.graph.influences[nodeID]) + len(ia.graph.influencedBy[nodeID]))
 | |
| 		centrality := degree / (totalNodes - 1) // Normalized degree centrality
 | |
| 		metrics.DegreeCentrality[nodeID] = centrality
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateBetweennessCentrality(metrics *CentralityMetrics) {
 | |
| 	// Simplified betweenness centrality calculation
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		betweenness := ia.calculateNodeBetweennessCentrality(nodeID)
 | |
| 		metrics.BetweennessCentrality[nodeID] = betweenness
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateClosenessCentrality(metrics *CentralityMetrics) {
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		closeness := ia.calculateNodeClosenessCentrality(nodeID)
 | |
| 		metrics.ClosenessCentrality[nodeID] = closeness
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateEigenvectorCentrality(metrics *CentralityMetrics) {
 | |
| 	// Simplified eigenvector centrality using power iteration
 | |
| 	nodeIDs := make([]string, 0, len(ia.graph.nodes))
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		nodeIDs = append(nodeIDs, nodeID)
 | |
| 	}
 | |
| 
 | |
| 	n := len(nodeIDs)
 | |
| 	if n == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Initialize eigenvector
 | |
| 	eigenvector := make([]float64, n)
 | |
| 	for i := range eigenvector {
 | |
| 		eigenvector[i] = 1.0 / float64(n)
 | |
| 	}
 | |
| 
 | |
| 	// Power iteration
 | |
| 	for iter := 0; iter < ia.maxIterations; iter++ {
 | |
| 		newEigenvector := make([]float64, n)
 | |
| 
 | |
| 		for i, nodeID := range nodeIDs {
 | |
| 			sum := 0.0
 | |
| 			if influencedBy, exists := ia.graph.influencedBy[nodeID]; exists {
 | |
| 				for _, influencerID := range influencedBy {
 | |
| 					for j, otherNodeID := range nodeIDs {
 | |
| 						if otherNodeID == influencerID {
 | |
| 							sum += eigenvector[j]
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			newEigenvector[i] = sum
 | |
| 		}
 | |
| 
 | |
| 		// Normalize
 | |
| 		norm := 0.0
 | |
| 		for _, val := range newEigenvector {
 | |
| 			norm += val * val
 | |
| 		}
 | |
| 		norm = math.Sqrt(norm)
 | |
| 
 | |
| 		if norm > 0 {
 | |
| 			for i := range newEigenvector {
 | |
| 				newEigenvector[i] /= norm
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Check convergence
 | |
| 		diff := 0.0
 | |
| 		for i := range eigenvector {
 | |
| 			diff += math.Abs(eigenvector[i] - newEigenvector[i])
 | |
| 		}
 | |
| 
 | |
| 		eigenvector = newEigenvector
 | |
| 
 | |
| 		if diff < ia.convergenceThreshold {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Store results
 | |
| 	for i, nodeID := range nodeIDs {
 | |
| 		metrics.EigenvectorCentrality[nodeID] = eigenvector[i]
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculatePageRank(metrics *CentralityMetrics) {
 | |
| 	nodeIDs := make([]string, 0, len(ia.graph.nodes))
 | |
| 	for nodeID := range ia.graph.nodes {
 | |
| 		nodeIDs = append(nodeIDs, nodeID)
 | |
| 	}
 | |
| 
 | |
| 	n := len(nodeIDs)
 | |
| 	if n == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Initialize PageRank values
 | |
| 	pageRank := make([]float64, n)
 | |
| 	newPageRank := make([]float64, n)
 | |
| 
 | |
| 	for i := range pageRank {
 | |
| 		pageRank[i] = 1.0 / float64(n)
 | |
| 	}
 | |
| 
 | |
| 	// PageRank iteration
 | |
| 	for iter := 0; iter < ia.maxIterations; iter++ {
 | |
| 		for i := range newPageRank {
 | |
| 			newPageRank[i] = (1.0 - ia.dampingFactor) / float64(n)
 | |
| 		}
 | |
| 
 | |
| 		for i, nodeID := range nodeIDs {
 | |
| 			if influences, exists := ia.graph.influences[nodeID]; exists && len(influences) > 0 {
 | |
| 				contribution := ia.dampingFactor * pageRank[i] / float64(len(influences))
 | |
| 
 | |
| 				for _, influencedID := range influences {
 | |
| 					for j, otherNodeID := range nodeIDs {
 | |
| 						if otherNodeID == influencedID {
 | |
| 							newPageRank[j] += contribution
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Check convergence
 | |
| 		diff := 0.0
 | |
| 		for i := range pageRank {
 | |
| 			diff += math.Abs(pageRank[i] - newPageRank[i])
 | |
| 		}
 | |
| 
 | |
| 		copy(pageRank, newPageRank)
 | |
| 
 | |
| 		if diff < ia.convergenceThreshold {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Store results
 | |
| 	for i, nodeID := range nodeIDs {
 | |
| 		metrics.PageRank[nodeID] = pageRank[i]
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // More helper methods
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) getNeighbors(nodeID string) []string {
 | |
| 	neighbors := make([]string, 0)
 | |
| 
 | |
| 	if influences, exists := ia.graph.influences[nodeID]; exists {
 | |
| 		neighbors = append(neighbors, influences...)
 | |
| 	}
 | |
| 
 | |
| 	if influencedBy, exists := ia.graph.influencedBy[nodeID]; exists {
 | |
| 		neighbors = append(neighbors, influencedBy...)
 | |
| 	}
 | |
| 
 | |
| 	return neighbors
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) areConnected(nodeID1, nodeID2 string) bool {
 | |
| 	if influences, exists := ia.graph.influences[nodeID1]; exists {
 | |
| 		for _, influenced := range influences {
 | |
| 			if influenced == nodeID2 {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if influences, exists := ia.graph.influences[nodeID2]; exists {
 | |
| 		for _, influenced := range influences {
 | |
| 			if influenced == nodeID1 {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) findShortestPathLength(fromID, toID string) int {
 | |
| 	if fromID == toID {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	visited := make(map[string]bool)
 | |
| 	queue := []struct {
 | |
| 		nodeID string
 | |
| 		depth  int
 | |
| 	}{{fromID, 0}}
 | |
| 
 | |
| 	for len(queue) > 0 {
 | |
| 		current := queue[0]
 | |
| 		queue = queue[1:]
 | |
| 
 | |
| 		if visited[current.nodeID] {
 | |
| 			continue
 | |
| 		}
 | |
| 		visited[current.nodeID] = true
 | |
| 
 | |
| 		if current.nodeID == toID {
 | |
| 			return current.depth
 | |
| 		}
 | |
| 
 | |
| 		// Add neighbors
 | |
| 		neighbors := ia.getNeighbors(current.nodeID)
 | |
| 		for _, neighbor := range neighbors {
 | |
| 			if !visited[neighbor] {
 | |
| 				queue = append(queue, struct {
 | |
| 					nodeID string
 | |
| 					depth  int
 | |
| 				}{neighbor, current.depth + 1})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return -1 // No path found
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) getNodeCentrality(nodeID string) float64 {
 | |
| 	// Simple centrality based on degree
 | |
| 	outgoing := len(ia.graph.influences[nodeID])
 | |
| 	incoming := len(ia.graph.influencedBy[nodeID])
 | |
| 	totalNodes := len(ia.graph.nodes)
 | |
| 
 | |
| 	if totalNodes <= 1 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(outgoing+incoming) / float64(totalNodes-1)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateNodeDegreeCentrality(nodeID string) float64 {
 | |
| 	return ia.getNodeCentrality(nodeID)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateNodeBetweennessCentrality(nodeID string) float64 {
 | |
| 	// Simplified betweenness: count how many shortest paths pass through this node
 | |
| 	// This is computationally expensive, so we use an approximation
 | |
| 	throughCount := 0
 | |
| 	totalPaths := 0
 | |
| 
 | |
| 	// Sample a subset of node pairs
 | |
| 	nodeIDs := make([]string, 0, len(ia.graph.nodes))
 | |
| 	for id := range ia.graph.nodes {
 | |
| 		if id != nodeID {
 | |
| 			nodeIDs = append(nodeIDs, id)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	sampleSize := min(20, len(nodeIDs))
 | |
| 	for i := 0; i < sampleSize; i++ {
 | |
| 		for j := i + 1; j < sampleSize; j++ {
 | |
| 			if ia.isOnShortestPath(nodeIDs[i], nodeIDs[j], nodeID) {
 | |
| 				throughCount++
 | |
| 			}
 | |
| 			totalPaths++
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if totalPaths == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(throughCount) / float64(totalPaths)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateNodeClosenessCentrality(nodeID string) float64 {
 | |
| 	totalDistance := 0
 | |
| 	reachableNodes := 0
 | |
| 
 | |
| 	// Calculate distances to all other nodes
 | |
| 	for otherNodeID := range ia.graph.nodes {
 | |
| 		if otherNodeID != nodeID {
 | |
| 			distance := ia.findShortestPathLength(nodeID, otherNodeID)
 | |
| 			if distance > 0 {
 | |
| 				totalDistance += distance
 | |
| 				reachableNodes++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if reachableNodes == 0 || totalDistance == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(reachableNodes) / float64(totalDistance)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateNodePageRank(nodeID string) float64 {
 | |
| 	// This is already calculated in calculatePageRank, so we'll use a simple approximation
 | |
| 	influencedBy := len(ia.graph.influencedBy[nodeID])
 | |
| 
 | |
| 	// Simple approximation based on in-degree with damping
 | |
| 	totalNodes := float64(len(ia.graph.nodes))
 | |
| 	if totalNodes == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return ((1.0-ia.dampingFactor)/totalNodes + ia.dampingFactor*float64(influencedBy)/totalNodes)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) expandCommunity(startNodeID string, visited map[string]bool) []ucxl.Address {
 | |
| 	community := make([]ucxl.Address, 0)
 | |
| 	queue := []string{startNodeID}
 | |
| 	visited[startNodeID] = true
 | |
| 
 | |
| 	for len(queue) > 0 {
 | |
| 		nodeID := queue[0]
 | |
| 		queue = queue[1:]
 | |
| 
 | |
| 		if node, exists := ia.graph.nodes[nodeID]; exists {
 | |
| 			community = append(community, node.UCXLAddress)
 | |
| 		}
 | |
| 
 | |
| 		// Add strongly connected neighbors
 | |
| 		neighbors := ia.getNeighbors(nodeID)
 | |
| 		for _, neighborID := range neighbors {
 | |
| 			if !visited[neighborID] && ia.isStronglyConnected(nodeID, neighborID) {
 | |
| 				visited[neighborID] = true
 | |
| 				queue = append(queue, neighborID)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return community
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) isStronglyConnected(nodeID1, nodeID2 string) bool {
 | |
| 	// Check if nodes have bidirectional influence or share many common neighbors
 | |
| 	commonNeighbors := 0
 | |
| 	neighbors1 := ia.getNeighbors(nodeID1)
 | |
| 	neighbors2 := ia.getNeighbors(nodeID2)
 | |
| 
 | |
| 	for _, n1 := range neighbors1 {
 | |
| 		for _, n2 := range neighbors2 {
 | |
| 			if n1 == n2 {
 | |
| 				commonNeighbors++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return commonNeighbors >= 2 || (ia.areConnected(nodeID1, nodeID2) && commonNeighbors >= 1)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateCommunityModularity(community []ucxl.Address) float64 {
 | |
| 	// Simplified modularity calculation
 | |
| 	if len(community) < 2 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	// Count internal edges
 | |
| 	internalEdges := 0
 | |
| 	for _, addr1 := range community {
 | |
| 		for _, addr2 := range community {
 | |
| 			if addr1.String() != addr2.String() {
 | |
| 				// Find nodes for these addresses
 | |
| 				node1 := ia.findNodeByAddress(addr1)
 | |
| 				node2 := ia.findNodeByAddress(addr2)
 | |
| 				if node1 != nil && node2 != nil && ia.areConnected(node1.ID, node2.ID) {
 | |
| 					internalEdges++
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Simple modularity approximation
 | |
| 	maxPossibleEdges := len(community) * (len(community) - 1)
 | |
| 	if maxPossibleEdges == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(internalEdges) / float64(maxPossibleEdges)
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateCommunityDensity(community []ucxl.Address) float64 {
 | |
| 	return ia.calculateCommunityModularity(community) // Same calculation for simplicity
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) findNodeByAddress(address ucxl.Address) *TemporalNode {
 | |
| 	addressKey := address.String()
 | |
| 	if nodes, exists := ia.graph.addressToNodes[addressKey]; exists && len(nodes) > 0 {
 | |
| 		return nodes[len(nodes)-1] // Return latest version
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) calculateTechnologySimilarity(node1, node2 *TemporalNode) float64 {
 | |
| 	if node1.Context == nil || node2.Context == nil {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	tech1 := make(map[string]bool)
 | |
| 	for _, tech := range node1.Context.Technologies {
 | |
| 		tech1[tech] = true
 | |
| 	}
 | |
| 
 | |
| 	tech2 := make(map[string]bool)
 | |
| 	for _, tech := range node2.Context.Technologies {
 | |
| 		tech2[tech] = true
 | |
| 	}
 | |
| 
 | |
| 	intersection := 0
 | |
| 	union := len(tech1)
 | |
| 
 | |
| 	for tech := range tech2 {
 | |
| 		if tech1[tech] {
 | |
| 			intersection++
 | |
| 		} else {
 | |
| 			union++
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if union == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	return float64(intersection) / float64(union) // Jaccard similarity
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) countCommonInfluencers(node1, node2 *TemporalNode) int {
 | |
| 	influencers1 := make(map[string]bool)
 | |
| 	if influencedBy1, exists := ia.graph.influencedBy[node1.ID]; exists {
 | |
| 		for _, influencer := range influencedBy1 {
 | |
| 			influencers1[influencer] = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	common := 0
 | |
| 	if influencedBy2, exists := ia.graph.influencedBy[node2.ID]; exists {
 | |
| 		for _, influencer := range influencedBy2 {
 | |
| 			if influencers1[influencer] {
 | |
| 				common++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return common
 | |
| }
 | |
| 
 | |
| func (ia *influenceAnalyzerImpl) isOnShortestPath(fromID, toID, throughID string) bool {
 | |
| 	// Check if throughID is on any shortest path from fromID to toID
 | |
| 	directDistance := ia.findShortestPathLength(fromID, toID)
 | |
| 	if directDistance <= 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	throughDistance := ia.findShortestPathLength(fromID, throughID) + ia.findShortestPathLength(throughID, toID)
 | |
| 
 | |
| 	return throughDistance == directDistance
 | |
| }
 | |
| 
 | |
| func min(a, b int) int {
 | |
| 	if a < b {
 | |
| 		return a
 | |
| 	}
 | |
| 	return b
 | |
| }
 | 
