 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			926 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			926 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package temporal
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/sha256"
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| 	"sort"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/ucxl"
 | |
| 	slurpContext "chorus/pkg/slurp/context"
 | |
| 	"chorus/pkg/slurp/storage"
 | |
| )
 | |
| 
 | |
| // temporalGraphImpl implements the TemporalGraph interface
 | |
| type temporalGraphImpl struct {
 | |
| 	mu sync.RWMutex
 | |
| 	
 | |
| 	// Core storage
 | |
| 	storage storage.ContextStore
 | |
| 	
 | |
| 	// In-memory graph structures for fast access
 | |
| 	nodes           map[string]*TemporalNode           // nodeID -> TemporalNode
 | |
| 	addressToNodes  map[string][]*TemporalNode        // address -> list of temporal nodes
 | |
| 	influences      map[string][]string               // nodeID -> list of influenced nodeIDs
 | |
| 	influencedBy    map[string][]string               // nodeID -> list of influencer nodeIDs
 | |
| 	
 | |
| 	// Decision tracking
 | |
| 	decisions       map[string]*DecisionMetadata      // decisionID -> DecisionMetadata
 | |
| 	decisionToNodes map[string][]*TemporalNode        // decisionID -> list of affected nodes
 | |
| 	
 | |
| 	// Performance optimization
 | |
| 	pathCache       map[string][]*DecisionStep        // cache for decision paths
 | |
| 	metricsCache    map[string]interface{}            // cache for expensive metrics
 | |
| 	cacheTimeout    time.Duration
 | |
| 	lastCacheClean  time.Time
 | |
| 	
 | |
| 	// Configuration
 | |
| 	maxDepth        int           // Maximum depth for path finding
 | |
| 	stalenessWeight *StalenessWeights
 | |
| }
 | |
| 
 | |
| // NewTemporalGraph creates a new temporal graph implementation
 | |
| func NewTemporalGraph(storage storage.ContextStore) TemporalGraph {
 | |
| 	return &temporalGraphImpl{
 | |
| 		storage:         storage,
 | |
| 		nodes:           make(map[string]*TemporalNode),
 | |
| 		addressToNodes:  make(map[string][]*TemporalNode),
 | |
| 		influences:      make(map[string][]string),
 | |
| 		influencedBy:    make(map[string][]string),
 | |
| 		decisions:       make(map[string]*DecisionMetadata),
 | |
| 		decisionToNodes: make(map[string][]*TemporalNode),
 | |
| 		pathCache:       make(map[string][]*DecisionStep),
 | |
| 		metricsCache:    make(map[string]interface{}),
 | |
| 		cacheTimeout:    time.Minute * 15,
 | |
| 		lastCacheClean:  time.Now(),
 | |
| 		maxDepth:        100, // Default maximum depth
 | |
| 		stalenessWeight: &StalenessWeights{
 | |
| 			TimeWeight:       0.3,
 | |
| 			InfluenceWeight:  0.4,
 | |
| 			ActivityWeight:   0.2,
 | |
| 			ImportanceWeight: 0.1,
 | |
| 			ComplexityWeight: 0.1,
 | |
| 			DependencyWeight: 0.3,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // CreateInitialContext creates the first temporal version of context
 | |
| func (tg *temporalGraphImpl) CreateInitialContext(ctx context.Context, address ucxl.Address, 
 | |
| 	contextData *slurpContext.ContextNode, creator string) (*TemporalNode, error) {
 | |
| 	
 | |
| 	tg.mu.Lock()
 | |
| 	defer tg.mu.Unlock()
 | |
| 	
 | |
| 	// Generate node ID
 | |
| 	nodeID := tg.generateNodeID(address, 1)
 | |
| 	
 | |
| 	// Create temporal node
 | |
| 	temporalNode := &TemporalNode{
 | |
| 		ID:           nodeID,
 | |
| 		UCXLAddress:  address,
 | |
| 		Version:      1,
 | |
| 		Context:      contextData,
 | |
| 		Timestamp:    time.Now(),
 | |
| 		DecisionID:   fmt.Sprintf("initial-%s", creator),
 | |
| 		ChangeReason: ReasonInitialCreation,
 | |
| 		ParentNode:   nil,
 | |
| 		ContextHash:  tg.calculateContextHash(contextData),
 | |
| 		Confidence:   contextData.RAGConfidence,
 | |
| 		Staleness:    0.0,
 | |
| 		Influences:   make([]ucxl.Address, 0),
 | |
| 		InfluencedBy: make([]ucxl.Address, 0),
 | |
| 		ValidatedBy:  []string{creator},
 | |
| 		LastValidated: time.Now(),
 | |
| 		ImpactScope:  ImpactLocal,
 | |
| 		PropagatedTo: make([]ucxl.Address, 0),
 | |
| 		Metadata:     make(map[string]interface{}),
 | |
| 	}
 | |
| 	
 | |
| 	// Store in memory structures
 | |
| 	tg.nodes[nodeID] = temporalNode
 | |
| 	addressKey := address.String()
 | |
| 	tg.addressToNodes[addressKey] = []*TemporalNode{temporalNode}
 | |
| 	
 | |
| 	// Initialize influence maps
 | |
| 	tg.influences[nodeID] = make([]string, 0)
 | |
| 	tg.influencedBy[nodeID] = make([]string, 0)
 | |
| 	
 | |
| 	// Store decision metadata
 | |
| 	decisionMeta := &DecisionMetadata{
 | |
| 		ID:               temporalNode.DecisionID,
 | |
| 		Maker:            creator,
 | |
| 		Rationale:        "Initial context creation",
 | |
| 		Scope:            ImpactLocal,
 | |
| 		ConfidenceLevel:  contextData.RAGConfidence,
 | |
| 		ExternalRefs:     make([]string, 0),
 | |
| 		CreatedAt:        time.Now(),
 | |
| 		ImplementationStatus: "complete",
 | |
| 		Metadata:         make(map[string]interface{}),
 | |
| 	}
 | |
| 	tg.decisions[temporalNode.DecisionID] = decisionMeta
 | |
| 	tg.decisionToNodes[temporalNode.DecisionID] = []*TemporalNode{temporalNode}
 | |
| 	
 | |
| 	// Persist to storage
 | |
| 	if err := tg.persistTemporalNode(ctx, temporalNode); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to persist initial temporal node: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return temporalNode, nil
 | |
| }
 | |
| 
 | |
| // EvolveContext creates a new temporal version due to a decision
 | |
| func (tg *temporalGraphImpl) EvolveContext(ctx context.Context, address ucxl.Address, 
 | |
| 	newContext *slurpContext.ContextNode, reason ChangeReason, 
 | |
| 	decision *DecisionMetadata) (*TemporalNode, error) {
 | |
| 	
 | |
| 	tg.mu.Lock()
 | |
| 	defer tg.mu.Unlock()
 | |
| 	
 | |
| 	// Get latest version
 | |
| 	addressKey := address.String()
 | |
| 	nodes, exists := tg.addressToNodes[addressKey]
 | |
| 	if !exists || len(nodes) == 0 {
 | |
| 		return nil, fmt.Errorf("no existing context found for address %s", address.String())
 | |
| 	}
 | |
| 	
 | |
| 	// Find latest version
 | |
| 	latestNode := nodes[len(nodes)-1]
 | |
| 	newVersion := latestNode.Version + 1
 | |
| 	
 | |
| 	// Generate new node ID
 | |
| 	nodeID := tg.generateNodeID(address, newVersion)
 | |
| 	
 | |
| 	// Create new temporal node
 | |
| 	temporalNode := &TemporalNode{
 | |
| 		ID:           nodeID,
 | |
| 		UCXLAddress:  address,
 | |
| 		Version:      newVersion,
 | |
| 		Context:      newContext,
 | |
| 		Timestamp:    time.Now(),
 | |
| 		DecisionID:   decision.ID,
 | |
| 		ChangeReason: reason,
 | |
| 		ParentNode:   &latestNode.ID,
 | |
| 		ContextHash:  tg.calculateContextHash(newContext),
 | |
| 		Confidence:   newContext.RAGConfidence,
 | |
| 		Staleness:    0.0, // New version, not stale
 | |
| 		Influences:   make([]ucxl.Address, 0),
 | |
| 		InfluencedBy: make([]ucxl.Address, 0),
 | |
| 		ValidatedBy:  []string{decision.Maker},
 | |
| 		LastValidated: time.Now(),
 | |
| 		ImpactScope:  decision.Scope,
 | |
| 		PropagatedTo: make([]ucxl.Address, 0),
 | |
| 		Metadata:     make(map[string]interface{}),
 | |
| 	}
 | |
| 	
 | |
| 	// Copy influence relationships from parent
 | |
| 	if latestNodeInfluences, exists := tg.influences[latestNode.ID]; exists {
 | |
| 		tg.influences[nodeID] = make([]string, len(latestNodeInfluences))
 | |
| 		copy(tg.influences[nodeID], latestNodeInfluences)
 | |
| 	} else {
 | |
| 		tg.influences[nodeID] = make([]string, 0)
 | |
| 	}
 | |
| 	
 | |
| 	if latestNodeInfluencedBy, exists := tg.influencedBy[latestNode.ID]; exists {
 | |
| 		tg.influencedBy[nodeID] = make([]string, len(latestNodeInfluencedBy))
 | |
| 		copy(tg.influencedBy[nodeID], latestNodeInfluencedBy)
 | |
| 	} else {
 | |
| 		tg.influencedBy[nodeID] = make([]string, 0)
 | |
| 	}
 | |
| 	
 | |
| 	// Store in memory structures
 | |
| 	tg.nodes[nodeID] = temporalNode
 | |
| 	tg.addressToNodes[addressKey] = append(tg.addressToNodes[addressKey], temporalNode)
 | |
| 	
 | |
| 	// Store decision metadata
 | |
| 	tg.decisions[decision.ID] = decision
 | |
| 	if existing, exists := tg.decisionToNodes[decision.ID]; exists {
 | |
| 		tg.decisionToNodes[decision.ID] = append(existing, temporalNode)
 | |
| 	} else {
 | |
| 		tg.decisionToNodes[decision.ID] = []*TemporalNode{temporalNode}
 | |
| 	}
 | |
| 	
 | |
| 	// Update staleness for related contexts
 | |
| 	tg.updateStalenessAfterChange(temporalNode)
 | |
| 	
 | |
| 	// Clear relevant caches
 | |
| 	tg.clearCacheForAddress(address)
 | |
| 	
 | |
| 	// Persist to storage
 | |
| 	if err := tg.persistTemporalNode(ctx, temporalNode); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to persist evolved temporal node: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return temporalNode, nil
 | |
| }
 | |
| 
 | |
| // GetLatestVersion gets the most recent temporal node for an address
 | |
| func (tg *temporalGraphImpl) GetLatestVersion(ctx context.Context, address ucxl.Address) (*TemporalNode, error) {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	addressKey := address.String()
 | |
| 	nodes, exists := tg.addressToNodes[addressKey]
 | |
| 	if !exists || len(nodes) == 0 {
 | |
| 		return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
 | |
| 	}
 | |
| 	
 | |
| 	// Return the latest version (last in slice)
 | |
| 	return nodes[len(nodes)-1], nil
 | |
| }
 | |
| 
 | |
| // GetVersionAtDecision gets context as it was at a specific decision hop
 | |
| func (tg *temporalGraphImpl) GetVersionAtDecision(ctx context.Context, address ucxl.Address, 
 | |
| 	decisionHop int) (*TemporalNode, error) {
 | |
| 	
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	addressKey := address.String()
 | |
| 	nodes, exists := tg.addressToNodes[addressKey]
 | |
| 	if !exists || len(nodes) == 0 {
 | |
| 		return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
 | |
| 	}
 | |
| 	
 | |
| 	// Find node at specific decision hop (version)
 | |
| 	for _, node := range nodes {
 | |
| 		if node.Version == decisionHop {
 | |
| 			return node, nil
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("no temporal node found at decision hop %d for address %s", 
 | |
| 		decisionHop, address.String())
 | |
| }
 | |
| 
 | |
| // GetEvolutionHistory gets complete evolution history ordered by decisions
 | |
| func (tg *temporalGraphImpl) GetEvolutionHistory(ctx context.Context, address ucxl.Address) ([]*TemporalNode, error) {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	addressKey := address.String()
 | |
| 	nodes, exists := tg.addressToNodes[addressKey]
 | |
| 	if !exists || len(nodes) == 0 {
 | |
| 		return []*TemporalNode{}, nil
 | |
| 	}
 | |
| 	
 | |
| 	// Sort by version to ensure proper order
 | |
| 	sortedNodes := make([]*TemporalNode, len(nodes))
 | |
| 	copy(sortedNodes, nodes)
 | |
| 	sort.Slice(sortedNodes, func(i, j int) bool {
 | |
| 		return sortedNodes[i].Version < sortedNodes[j].Version
 | |
| 	})
 | |
| 	
 | |
| 	return sortedNodes, nil
 | |
| }
 | |
| 
 | |
| // AddInfluenceRelationship establishes that decisions in one context affect another
 | |
| func (tg *temporalGraphImpl) AddInfluenceRelationship(ctx context.Context, influencer, influenced ucxl.Address) error {
 | |
| 	tg.mu.Lock()
 | |
| 	defer tg.mu.Unlock()
 | |
| 	
 | |
| 	// Get latest nodes for both addresses
 | |
| 	influencerNode, err := tg.getLatestNodeUnsafe(influencer)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("influencer node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	influencedNode, err := tg.getLatestNodeUnsafe(influenced)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("influenced node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Add to influence mappings
 | |
| 	influencerNodeID := influencerNode.ID
 | |
| 	influencedNodeID := influencedNode.ID
 | |
| 	
 | |
| 	// Add to influences map (influencer -> influenced)
 | |
| 	if influences, exists := tg.influences[influencerNodeID]; exists {
 | |
| 		// Check if relationship already exists
 | |
| 		for _, existingID := range influences {
 | |
| 			if existingID == influencedNodeID {
 | |
| 				return nil // Relationship already exists
 | |
| 			}
 | |
| 		}
 | |
| 		tg.influences[influencerNodeID] = append(influences, influencedNodeID)
 | |
| 	} else {
 | |
| 		tg.influences[influencerNodeID] = []string{influencedNodeID}
 | |
| 	}
 | |
| 	
 | |
| 	// Add to influencedBy map (influenced <- influencer)
 | |
| 	if influencedBy, exists := tg.influencedBy[influencedNodeID]; exists {
 | |
| 		// Check if relationship already exists
 | |
| 		for _, existingID := range influencedBy {
 | |
| 			if existingID == influencerNodeID {
 | |
| 				return nil // Relationship already exists
 | |
| 			}
 | |
| 		}
 | |
| 		tg.influencedBy[influencedNodeID] = append(influencedBy, influencerNodeID)
 | |
| 	} else {
 | |
| 		tg.influencedBy[influencedNodeID] = []string{influencerNodeID}
 | |
| 	}
 | |
| 	
 | |
| 	// Update temporal nodes with the influence relationship
 | |
| 	influencerNode.Influences = append(influencerNode.Influences, influenced)
 | |
| 	influencedNode.InfluencedBy = append(influencedNode.InfluencedBy, influencer)
 | |
| 	
 | |
| 	// Clear path cache as influence graph has changed
 | |
| 	tg.pathCache = make(map[string][]*DecisionStep)
 | |
| 	
 | |
| 	// Persist changes
 | |
| 	if err := tg.persistTemporalNode(ctx, influencerNode); err != nil {
 | |
| 		return fmt.Errorf("failed to persist influencer node: %w", err)
 | |
| 	}
 | |
| 	if err := tg.persistTemporalNode(ctx, influencedNode); err != nil {
 | |
| 		return fmt.Errorf("failed to persist influenced node: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // RemoveInfluenceRelationship removes an influence relationship
 | |
| func (tg *temporalGraphImpl) RemoveInfluenceRelationship(ctx context.Context, influencer, influenced ucxl.Address) error {
 | |
| 	tg.mu.Lock()
 | |
| 	defer tg.mu.Unlock()
 | |
| 	
 | |
| 	// Get latest nodes for both addresses
 | |
| 	influencerNode, err := tg.getLatestNodeUnsafe(influencer)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("influencer node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	influencedNode, err := tg.getLatestNodeUnsafe(influenced)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("influenced node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Remove from influence mappings
 | |
| 	influencerNodeID := influencerNode.ID
 | |
| 	influencedNodeID := influencedNode.ID
 | |
| 	
 | |
| 	// Remove from influences map
 | |
| 	if influences, exists := tg.influences[influencerNodeID]; exists {
 | |
| 		tg.influences[influencerNodeID] = tg.removeFromSlice(influences, influencedNodeID)
 | |
| 	}
 | |
| 	
 | |
| 	// Remove from influencedBy map
 | |
| 	if influencedBy, exists := tg.influencedBy[influencedNodeID]; exists {
 | |
| 		tg.influencedBy[influencedNodeID] = tg.removeFromSlice(influencedBy, influencerNodeID)
 | |
| 	}
 | |
| 	
 | |
| 	// Update temporal nodes
 | |
| 	influencerNode.Influences = tg.removeAddressFromSlice(influencerNode.Influences, influenced)
 | |
| 	influencedNode.InfluencedBy = tg.removeAddressFromSlice(influencedNode.InfluencedBy, influencer)
 | |
| 	
 | |
| 	// Clear path cache
 | |
| 	tg.pathCache = make(map[string][]*DecisionStep)
 | |
| 	
 | |
| 	// Persist changes
 | |
| 	if err := tg.persistTemporalNode(ctx, influencerNode); err != nil {
 | |
| 		return fmt.Errorf("failed to persist influencer node: %w", err)
 | |
| 	}
 | |
| 	if err := tg.persistTemporalNode(ctx, influencedNode); err != nil {
 | |
| 		return fmt.Errorf("failed to persist influenced node: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // GetInfluenceRelationships gets all influence relationships for a context
 | |
| func (tg *temporalGraphImpl) GetInfluenceRelationships(ctx context.Context, address ucxl.Address) ([]ucxl.Address, []ucxl.Address, error) {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	node, err := tg.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	influences := make([]ucxl.Address, len(node.Influences))
 | |
| 	copy(influences, node.Influences)
 | |
| 	
 | |
| 	influencedBy := make([]ucxl.Address, len(node.InfluencedBy))
 | |
| 	copy(influencedBy, node.InfluencedBy)
 | |
| 	
 | |
| 	return influences, influencedBy, nil
 | |
| }
 | |
| 
 | |
| // FindRelatedDecisions finds decisions within N decision hops
 | |
| func (tg *temporalGraphImpl) FindRelatedDecisions(ctx context.Context, address ucxl.Address, 
 | |
| 	maxHops int) ([]*DecisionPath, error) {
 | |
| 	
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	// Check cache first
 | |
| 	cacheKey := fmt.Sprintf("related-%s-%d", address.String(), maxHops)
 | |
| 	if cached, exists := tg.pathCache[cacheKey]; exists {
 | |
| 		paths := make([]*DecisionPath, len(cached))
 | |
| 		for i, step := range cached {
 | |
| 			paths[i] = &DecisionPath{
 | |
| 				From:      address,
 | |
| 				To:        step.Address,
 | |
| 				Steps:     []*DecisionStep{step},
 | |
| 				TotalHops: step.HopDistance,
 | |
| 				PathType:  "direct",
 | |
| 			}
 | |
| 		}
 | |
| 		return paths, nil
 | |
| 	}
 | |
| 	
 | |
| 	startNode, err := tg.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("start node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Use BFS to find all nodes within maxHops
 | |
| 	visited := make(map[string]bool)
 | |
| 	queue := []*bfsItem{{node: startNode, distance: 0, path: []*DecisionStep{}}}
 | |
| 	relatedPaths := make([]*DecisionPath, 0)
 | |
| 	
 | |
| 	for len(queue) > 0 {
 | |
| 		current := queue[0]
 | |
| 		queue = queue[1:]
 | |
| 		
 | |
| 		nodeID := current.node.ID
 | |
| 		if visited[nodeID] || current.distance > maxHops {
 | |
| 			continue
 | |
| 		}
 | |
| 		visited[nodeID] = true
 | |
| 		
 | |
| 		// If this is not the starting node, add it to results
 | |
| 		if current.distance > 0 {
 | |
| 			step := &DecisionStep{
 | |
| 				Address:      current.node.UCXLAddress,
 | |
| 				TemporalNode: current.node,
 | |
| 				HopDistance:  current.distance,
 | |
| 				Relationship: "influence",
 | |
| 			}
 | |
| 			
 | |
| 			path := &DecisionPath{
 | |
| 				From:      address,
 | |
| 				To:        current.node.UCXLAddress,
 | |
| 				Steps:     append(current.path, step),
 | |
| 				TotalHops: current.distance,
 | |
| 				PathType:  "influence",
 | |
| 			}
 | |
| 			relatedPaths = append(relatedPaths, path)
 | |
| 		}
 | |
| 		
 | |
| 		// Add influenced nodes to queue
 | |
| 		if influences, exists := tg.influences[nodeID]; exists {
 | |
| 			for _, influencedID := range influences {
 | |
| 				if !visited[influencedID] && current.distance < maxHops {
 | |
| 					if influencedNode, exists := tg.nodes[influencedID]; exists {
 | |
| 						newStep := &DecisionStep{
 | |
| 							Address:      current.node.UCXLAddress,
 | |
| 							TemporalNode: current.node,
 | |
| 							HopDistance:  current.distance,
 | |
| 							Relationship: "influences",
 | |
| 						}
 | |
| 						newPath := append(current.path, newStep)
 | |
| 						queue = append(queue, &bfsItem{
 | |
| 							node:     influencedNode,
 | |
| 							distance: current.distance + 1,
 | |
| 							path:     newPath,
 | |
| 						})
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		// Add influencer nodes to queue
 | |
| 		if influencedBy, exists := tg.influencedBy[nodeID]; exists {
 | |
| 			for _, influencerID := range influencedBy {
 | |
| 				if !visited[influencerID] && current.distance < maxHops {
 | |
| 					if influencerNode, exists := tg.nodes[influencerID]; exists {
 | |
| 						newStep := &DecisionStep{
 | |
| 							Address:      current.node.UCXLAddress,
 | |
| 							TemporalNode: current.node,
 | |
| 							HopDistance:  current.distance,
 | |
| 							Relationship: "influenced_by",
 | |
| 						}
 | |
| 						newPath := append(current.path, newStep)
 | |
| 						queue = append(queue, &bfsItem{
 | |
| 							node:     influencerNode,
 | |
| 							distance: current.distance + 1,
 | |
| 							path:     newPath,
 | |
| 						})
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return relatedPaths, nil
 | |
| }
 | |
| 
 | |
| // FindDecisionPath finds shortest decision path between two addresses
 | |
| func (tg *temporalGraphImpl) FindDecisionPath(ctx context.Context, from, to ucxl.Address) ([]*DecisionStep, error) {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	// Check cache first
 | |
| 	cacheKey := fmt.Sprintf("path-%s-%s", from.String(), to.String())
 | |
| 	if cached, exists := tg.pathCache[cacheKey]; exists {
 | |
| 		return cached, nil
 | |
| 	}
 | |
| 	
 | |
| 	fromNode, err := tg.getLatestNodeUnsafe(from)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("from node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	toNode, err := tg.getLatestNodeUnsafe(to)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("to node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Use BFS to find shortest path
 | |
| 	visited := make(map[string]bool)
 | |
| 	queue := []*pathItem{{node: fromNode, path: []*DecisionStep{}}}
 | |
| 	
 | |
| 	for len(queue) > 0 {
 | |
| 		current := queue[0]
 | |
| 		queue = queue[1:]
 | |
| 		
 | |
| 		nodeID := current.node.ID
 | |
| 		if visited[nodeID] {
 | |
| 			continue
 | |
| 		}
 | |
| 		visited[nodeID] = true
 | |
| 		
 | |
| 		// Check if we reached the target
 | |
| 		if current.node.UCXLAddress.String() == to.String() {
 | |
| 			// Cache the result
 | |
| 			tg.pathCache[cacheKey] = current.path
 | |
| 			return current.path, nil
 | |
| 		}
 | |
| 		
 | |
| 		// Explore influenced nodes
 | |
| 		if influences, exists := tg.influences[nodeID]; exists {
 | |
| 			for _, influencedID := range influences {
 | |
| 				if !visited[influencedID] {
 | |
| 					if influencedNode, exists := tg.nodes[influencedID]; exists {
 | |
| 						step := &DecisionStep{
 | |
| 							Address:      current.node.UCXLAddress,
 | |
| 							TemporalNode: current.node,
 | |
| 							HopDistance:  len(current.path),
 | |
| 							Relationship: "influences",
 | |
| 						}
 | |
| 						newPath := append(current.path, step)
 | |
| 						queue = append(queue, &pathItem{
 | |
| 							node: influencedNode,
 | |
| 							path: newPath,
 | |
| 						})
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		// Explore influencer nodes
 | |
| 		if influencedBy, exists := tg.influencedBy[nodeID]; exists {
 | |
| 			for _, influencerID := range influencedBy {
 | |
| 				if !visited[influencerID] {
 | |
| 					if influencerNode, exists := tg.nodes[influencerID]; exists {
 | |
| 						step := &DecisionStep{
 | |
| 							Address:      current.node.UCXLAddress,
 | |
| 							TemporalNode: current.node,
 | |
| 							HopDistance:  len(current.path),
 | |
| 							Relationship: "influenced_by",
 | |
| 						}
 | |
| 						newPath := append(current.path, step)
 | |
| 						queue = append(queue, &pathItem{
 | |
| 							node: influencerNode,
 | |
| 							path: newPath,
 | |
| 						})
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("no path found from %s to %s", from.String(), to.String())
 | |
| }
 | |
| 
 | |
| // AnalyzeDecisionPatterns analyzes decision-making patterns over time
 | |
| func (tg *temporalGraphImpl) AnalyzeDecisionPatterns(ctx context.Context) (*DecisionAnalysis, error) {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	analysis := &DecisionAnalysis{
 | |
| 		TimeRange:                24 * time.Hour, // Analyze last 24 hours by default
 | |
| 		TotalDecisions:           len(tg.decisions),
 | |
| 		DecisionVelocity:         0,
 | |
| 		InfluenceNetworkSize:     len(tg.nodes),
 | |
| 		AverageInfluenceDistance: 0,
 | |
| 		MostInfluentialDecisions: make([]*InfluentialDecision, 0),
 | |
| 		DecisionClusters:         make([]*DecisionCluster, 0),
 | |
| 		Patterns:                 make([]*DecisionPattern, 0),
 | |
| 		Anomalies:               make([]*AnomalousDecision, 0),
 | |
| 		AnalyzedAt:              time.Now(),
 | |
| 	}
 | |
| 	
 | |
| 	// Calculate decision velocity
 | |
| 	cutoff := time.Now().Add(-analysis.TimeRange)
 | |
| 	recentDecisions := 0
 | |
| 	for _, decision := range tg.decisions {
 | |
| 		if decision.CreatedAt.After(cutoff) {
 | |
| 			recentDecisions++
 | |
| 		}
 | |
| 	}
 | |
| 	analysis.DecisionVelocity = float64(recentDecisions) / analysis.TimeRange.Hours()
 | |
| 	
 | |
| 	// Calculate average influence distance
 | |
| 	totalDistance := 0.0
 | |
| 	connections := 0
 | |
| 	for nodeID := range tg.influences {
 | |
| 		if influences, exists := tg.influences[nodeID]; exists {
 | |
| 			for range influences {
 | |
| 				totalDistance += 1.0 // Each connection is 1 hop
 | |
| 				connections++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if connections > 0 {
 | |
| 		analysis.AverageInfluenceDistance = totalDistance / float64(connections)
 | |
| 	}
 | |
| 	
 | |
| 	// Find most influential decisions (simplified)
 | |
| 	influenceScores := make(map[string]float64)
 | |
| 	for nodeID, node := range tg.nodes {
 | |
| 		score := float64(len(tg.influences[nodeID])) * 1.0 // Direct influences
 | |
| 		score += float64(len(tg.influencedBy[nodeID])) * 0.5 // Being influenced
 | |
| 		influenceScores[nodeID] = score
 | |
| 		
 | |
| 		if score > 3.0 { // Threshold for "influential"
 | |
| 			influential := &InfluentialDecision{
 | |
| 				Address:           node.UCXLAddress,
 | |
| 				DecisionHop:       node.Version,
 | |
| 				InfluenceScore:    score,
 | |
| 				AffectedContexts:  node.Influences,
 | |
| 				DecisionMetadata:  tg.decisions[node.DecisionID],
 | |
| 				InfluenceReasons:  []string{"high_connectivity", "multiple_influences"},
 | |
| 			}
 | |
| 			analysis.MostInfluentialDecisions = append(analysis.MostInfluentialDecisions, influential)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Sort influential decisions by score
 | |
| 	sort.Slice(analysis.MostInfluentialDecisions, func(i, j int) bool {
 | |
| 		return analysis.MostInfluentialDecisions[i].InfluenceScore > analysis.MostInfluentialDecisions[j].InfluenceScore
 | |
| 	})
 | |
| 	
 | |
| 	// Limit to top 10
 | |
| 	if len(analysis.MostInfluentialDecisions) > 10 {
 | |
| 		analysis.MostInfluentialDecisions = analysis.MostInfluentialDecisions[:10]
 | |
| 	}
 | |
| 	
 | |
| 	return analysis, nil
 | |
| }
 | |
| 
 | |
| // ValidateTemporalIntegrity validates temporal graph integrity
 | |
| func (tg *temporalGraphImpl) ValidateTemporalIntegrity(ctx context.Context) error {
 | |
| 	tg.mu.RLock()
 | |
| 	defer tg.mu.RUnlock()
 | |
| 	
 | |
| 	errors := make([]string, 0)
 | |
| 	
 | |
| 	// Check for orphaned nodes
 | |
| 	for nodeID, node := range tg.nodes {
 | |
| 		if node.ParentNode != nil {
 | |
| 			if _, exists := tg.nodes[*node.ParentNode]; !exists {
 | |
| 				errors = append(errors, fmt.Sprintf("orphaned node %s has non-existent parent %s", 
 | |
| 					nodeID, *node.ParentNode))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Check influence consistency
 | |
| 	for nodeID := range tg.influences {
 | |
| 		if influences, exists := tg.influences[nodeID]; exists {
 | |
| 			for _, influencedID := range influences {
 | |
| 				// Check if the influenced node has this node in its influencedBy list
 | |
| 				if influencedByList, exists := tg.influencedBy[influencedID]; exists {
 | |
| 					found := false
 | |
| 					for _, influencerID := range influencedByList {
 | |
| 						if influencerID == nodeID {
 | |
| 							found = true
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 					if !found {
 | |
| 						errors = append(errors, fmt.Sprintf("influence inconsistency: %s -> %s not reflected in influencedBy", 
 | |
| 							nodeID, influencedID))
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Check version sequence integrity
 | |
| 	for address, nodes := range tg.addressToNodes {
 | |
| 		sort.Slice(nodes, func(i, j int) bool {
 | |
| 			return nodes[i].Version < nodes[j].Version
 | |
| 		})
 | |
| 		
 | |
| 		for i, node := range nodes {
 | |
| 			expectedVersion := i + 1
 | |
| 			if node.Version != expectedVersion {
 | |
| 				errors = append(errors, fmt.Sprintf("version sequence error for address %s: expected %d, got %d", 
 | |
| 					address, expectedVersion, node.Version))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	if len(errors) > 0 {
 | |
| 		return fmt.Errorf("temporal integrity violations: %v", errors)
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CompactHistory compacts old temporal data to save space
 | |
| func (tg *temporalGraphImpl) CompactHistory(ctx context.Context, beforeTime time.Time) error {
 | |
| 	tg.mu.Lock()
 | |
| 	defer tg.mu.Unlock()
 | |
| 	
 | |
| 	compacted := 0
 | |
| 	
 | |
| 	// For each address, keep only the latest version and major milestones before the cutoff
 | |
| 	for address, nodes := range tg.addressToNodes {
 | |
| 		toKeep := make([]*TemporalNode, 0)
 | |
| 		toRemove := make([]*TemporalNode, 0)
 | |
| 		
 | |
| 		for _, node := range nodes {
 | |
| 			// Always keep nodes after the cutoff time
 | |
| 			if node.Timestamp.After(beforeTime) {
 | |
| 				toKeep = append(toKeep, node)
 | |
| 				continue
 | |
| 			}
 | |
| 			
 | |
| 			// Keep major changes and influential decisions
 | |
| 			if tg.isMajorChange(node) || tg.isInfluentialDecision(node) {
 | |
| 				toKeep = append(toKeep, node)
 | |
| 			} else {
 | |
| 				toRemove = append(toRemove, node)
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		// Update the address mapping
 | |
| 		tg.addressToNodes[address] = toKeep
 | |
| 		
 | |
| 		// Remove old nodes from main maps
 | |
| 		for _, node := range toRemove {
 | |
| 			delete(tg.nodes, node.ID)
 | |
| 			delete(tg.influences, node.ID)
 | |
| 			delete(tg.influencedBy, node.ID)
 | |
| 			compacted++
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Clear caches after compaction
 | |
| 	tg.pathCache = make(map[string][]*DecisionStep)
 | |
| 	tg.metricsCache = make(map[string]interface{})
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Helper methods
 | |
| 
 | |
| func (tg *temporalGraphImpl) generateNodeID(address ucxl.Address, version int) string {
 | |
| 	return fmt.Sprintf("%s-v%d", address.String(), version)
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) calculateContextHash(context *slurpContext.ContextNode) string {
 | |
| 	hasher := sha256.New()
 | |
| 	hasher.Write([]byte(fmt.Sprintf("%+v", context)))
 | |
| 	return fmt.Sprintf("%x", hasher.Sum(nil))[:16]
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) getLatestNodeUnsafe(address ucxl.Address) (*TemporalNode, error) {
 | |
| 	addressKey := address.String()
 | |
| 	nodes, exists := tg.addressToNodes[addressKey]
 | |
| 	if !exists || len(nodes) == 0 {
 | |
| 		return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
 | |
| 	}
 | |
| 	return nodes[len(nodes)-1], nil
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) removeFromSlice(slice []string, item string) []string {
 | |
| 	result := make([]string, 0, len(slice))
 | |
| 	for _, s := range slice {
 | |
| 		if s != item {
 | |
| 			result = append(result, s)
 | |
| 		}
 | |
| 	}
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) removeAddressFromSlice(slice []ucxl.Address, item ucxl.Address) []ucxl.Address {
 | |
| 	result := make([]ucxl.Address, 0, len(slice))
 | |
| 	for _, addr := range slice {
 | |
| 		if addr.String() != item.String() {
 | |
| 			result = append(result, addr)
 | |
| 		}
 | |
| 	}
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) updateStalenessAfterChange(changedNode *TemporalNode) {
 | |
| 	// Update staleness for all influenced contexts
 | |
| 	if influences, exists := tg.influences[changedNode.ID]; exists {
 | |
| 		for _, influencedID := range influences {
 | |
| 			if influencedNode, exists := tg.nodes[influencedID]; exists {
 | |
| 				// Calculate new staleness based on the change
 | |
| 				staleness := tg.calculateStaleness(influencedNode, changedNode)
 | |
| 				influencedNode.Staleness = math.Max(influencedNode.Staleness, staleness)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) calculateStaleness(node *TemporalNode, changedNode *TemporalNode) float64 {
 | |
| 	// Simple staleness calculation based on time since last update and influence strength
 | |
| 	timeSinceUpdate := time.Since(node.Timestamp)
 | |
| 	timeWeight := math.Min(timeSinceUpdate.Hours()/168.0, 1.0) // Max staleness from time: 1 week
 | |
| 	
 | |
| 	// Influence weight based on connection strength
 | |
| 	influenceWeight := 0.0
 | |
| 	if len(node.InfluencedBy) > 0 {
 | |
| 		influenceWeight = 1.0 / float64(len(node.InfluencedBy)) // Stronger if fewer influencers
 | |
| 	}
 | |
| 	
 | |
| 	// Impact scope weight
 | |
| 	impactWeight := 0.0
 | |
| 	switch changedNode.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		impactWeight = 1.0
 | |
| 	case ImpactProject:
 | |
| 		impactWeight = 0.8
 | |
| 	case ImpactModule:
 | |
| 		impactWeight = 0.6
 | |
| 	case ImpactLocal:
 | |
| 		impactWeight = 0.4
 | |
| 	}
 | |
| 	
 | |
| 	return math.Min(
 | |
| 		tg.stalenessWeight.TimeWeight*timeWeight+
 | |
| 		tg.stalenessWeight.InfluenceWeight*influenceWeight+
 | |
| 		tg.stalenessWeight.ImportanceWeight*impactWeight, 1.0)
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) clearCacheForAddress(address ucxl.Address) {
 | |
| 	addressStr := address.String()
 | |
| 	keysToDelete := make([]string, 0)
 | |
| 	
 | |
| 	for key := range tg.pathCache {
 | |
| 		if contains(key, addressStr) {
 | |
| 			keysToDelete = append(keysToDelete, key)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	for _, key := range keysToDelete {
 | |
| 		delete(tg.pathCache, key)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) isMajorChange(node *TemporalNode) bool {
 | |
| 	return node.ChangeReason == ReasonArchitectureChange ||
 | |
| 		node.ChangeReason == ReasonDesignDecision ||
 | |
| 		node.ChangeReason == ReasonRequirementsChange
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) isInfluentialDecision(node *TemporalNode) bool {
 | |
| 	influences := len(tg.influences[node.ID])
 | |
| 	influencedBy := len(tg.influencedBy[node.ID])
 | |
| 	return influences >= 3 || influencedBy >= 3 // Arbitrary threshold for "influential"
 | |
| }
 | |
| 
 | |
| func (tg *temporalGraphImpl) persistTemporalNode(ctx context.Context, node *TemporalNode) error {
 | |
| 	// Convert to storage format and persist
 | |
| 	// This would integrate with the storage system
 | |
| 	// For now, we'll assume persistence happens in memory
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func contains(s, substr string) bool {
 | |
| 	return len(s) >= len(substr) && (s == substr || 
 | |
| 		(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr)))
 | |
| }
 | |
| 
 | |
| // Supporting types for BFS traversal
 | |
| 
 | |
| type bfsItem struct {
 | |
| 	node     *TemporalNode
 | |
| 	distance int
 | |
| 	path     []*DecisionStep
 | |
| }
 | |
| 
 | |
| type pathItem struct {
 | |
| 	node *TemporalNode
 | |
| 	path []*DecisionStep
 | |
| } |