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 }