 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>
		
			
				
	
	
		
			895 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			895 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package temporal
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| 	"sort"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/ucxl"
 | |
| )
 | |
| 
 | |
| // stalenessDetectorImpl implements the StalenessDetector interface
 | |
| type stalenessDetectorImpl struct {
 | |
| 	mu sync.RWMutex
 | |
| 	
 | |
| 	// Reference to the temporal graph
 | |
| 	graph *temporalGraphImpl
 | |
| 	
 | |
| 	// Configuration
 | |
| 	weights            *StalenessWeights
 | |
| 	defaultThreshold   float64
 | |
| 	analysisWindow     time.Duration
 | |
| 	
 | |
| 	// Cached results
 | |
| 	lastDetectionRun   time.Time
 | |
| 	cachedStaleContexts []*StaleContext
 | |
| 	cachedStatistics   *StalenessStatistics
 | |
| 	cacheValidDuration time.Duration
 | |
| 	
 | |
| 	// Detection settings
 | |
| 	enableTimeBasedStaleness      bool
 | |
| 	enableInfluenceBasedStaleness bool
 | |
| 	enableActivityBasedStaleness  bool
 | |
| 	enableImportanceBasedStaleness bool
 | |
| 	enableComplexityBasedStaleness bool
 | |
| 	enableDependencyBasedStaleness bool
 | |
| }
 | |
| 
 | |
| // NewStalenessDetector creates a new staleness detector
 | |
| func NewStalenessDetector(graph *temporalGraphImpl) StalenessDetector {
 | |
| 	return &stalenessDetectorImpl{
 | |
| 		graph:            graph,
 | |
| 		weights:          graph.stalenessWeight,
 | |
| 		defaultThreshold: 0.6,
 | |
| 		analysisWindow:   30 * 24 * time.Hour, // 30 days
 | |
| 		cacheValidDuration: time.Minute * 15,
 | |
| 		
 | |
| 		// Enable all detection methods by default
 | |
| 		enableTimeBasedStaleness:       true,
 | |
| 		enableInfluenceBasedStaleness:  true,
 | |
| 		enableActivityBasedStaleness:   true,
 | |
| 		enableImportanceBasedStaleness: true,
 | |
| 		enableComplexityBasedStaleness: true,
 | |
| 		enableDependencyBasedStaleness: true,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // CalculateStaleness calculates staleness score based on decision relationships
 | |
| func (sd *stalenessDetectorImpl) CalculateStaleness(ctx context.Context, address ucxl.Address) (float64, error) {
 | |
| 	sd.mu.RLock()
 | |
| 	defer sd.mu.RUnlock()
 | |
| 	
 | |
| 	sd.graph.mu.RLock()
 | |
| 	defer sd.graph.mu.RUnlock()
 | |
| 	
 | |
| 	node, err := sd.graph.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return 0, fmt.Errorf("node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return sd.calculateNodeStaleness(node), nil
 | |
| }
 | |
| 
 | |
| // DetectStaleContexts detects all stale contexts above threshold
 | |
| func (sd *stalenessDetectorImpl) DetectStaleContexts(ctx context.Context, threshold float64) ([]*StaleContext, error) {
 | |
| 	sd.mu.Lock()
 | |
| 	defer sd.mu.Unlock()
 | |
| 	
 | |
| 	// Check cache validity
 | |
| 	if sd.cachedStaleContexts != nil && time.Since(sd.lastDetectionRun) < sd.cacheValidDuration {
 | |
| 		// Filter cached results by threshold
 | |
| 		filtered := make([]*StaleContext, 0)
 | |
| 		for _, stale := range sd.cachedStaleContexts {
 | |
| 			if stale.StalenessScore >= threshold {
 | |
| 				filtered = append(filtered, stale)
 | |
| 			}
 | |
| 		}
 | |
| 		return filtered, nil
 | |
| 	}
 | |
| 	
 | |
| 	sd.graph.mu.RLock()
 | |
| 	defer sd.graph.mu.RUnlock()
 | |
| 	
 | |
| 	staleContexts := make([]*StaleContext, 0)
 | |
| 	detectionStart := time.Now()
 | |
| 	
 | |
| 	// Analyze all nodes for staleness
 | |
| 	for _, node := range sd.graph.nodes {
 | |
| 		stalenessScore := sd.calculateNodeStaleness(node)
 | |
| 		
 | |
| 		if stalenessScore >= threshold {
 | |
| 			staleContext := &StaleContext{
 | |
| 				UCXLAddress:      node.UCXLAddress,
 | |
| 				TemporalNode:     node,
 | |
| 				StalenessScore:   stalenessScore,
 | |
| 				LastUpdated:      node.Timestamp,
 | |
| 				Reasons:          sd.analyzeStalenessReasons(node, stalenessScore),
 | |
| 				SuggestedActions: sd.generateRefreshActions(node),
 | |
| 				RelatedChanges:   sd.findRelatedChanges(node),
 | |
| 				Priority:         sd.calculatePriority(stalenessScore, node),
 | |
| 			}
 | |
| 			staleContexts = append(staleContexts, staleContext)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Sort by staleness score (highest first)
 | |
| 	sort.Slice(staleContexts, func(i, j int) bool {
 | |
| 		return staleContexts[i].StalenessScore > staleContexts[j].StalenessScore
 | |
| 	})
 | |
| 	
 | |
| 	// Update cache
 | |
| 	sd.cachedStaleContexts = staleContexts
 | |
| 	sd.lastDetectionRun = time.Now()
 | |
| 	
 | |
| 	// Update statistics
 | |
| 	sd.updateStatistics(len(sd.graph.nodes), len(staleContexts), time.Since(detectionStart))
 | |
| 	
 | |
| 	return staleContexts, nil
 | |
| }
 | |
| 
 | |
| // GetStalenessReasons gets reasons why context is considered stale
 | |
| func (sd *stalenessDetectorImpl) GetStalenessReasons(ctx context.Context, address ucxl.Address) ([]string, error) {
 | |
| 	sd.mu.RLock()
 | |
| 	defer sd.mu.RUnlock()
 | |
| 	
 | |
| 	sd.graph.mu.RLock()
 | |
| 	defer sd.graph.mu.RUnlock()
 | |
| 	
 | |
| 	node, err := sd.graph.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	stalenessScore := sd.calculateNodeStaleness(node)
 | |
| 	return sd.analyzeStalenessReasons(node, stalenessScore), nil
 | |
| }
 | |
| 
 | |
| // SuggestRefreshActions suggests actions to refresh stale context
 | |
| func (sd *stalenessDetectorImpl) SuggestRefreshActions(ctx context.Context, address ucxl.Address) ([]*RefreshAction, error) {
 | |
| 	sd.mu.RLock()
 | |
| 	defer sd.mu.RUnlock()
 | |
| 	
 | |
| 	sd.graph.mu.RLock()
 | |
| 	defer sd.graph.mu.RUnlock()
 | |
| 	
 | |
| 	node, err := sd.graph.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("node not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	actions := sd.generateRefreshActions(node)
 | |
| 	
 | |
| 	// Convert to RefreshAction structs
 | |
| 	refreshActions := make([]*RefreshAction, len(actions))
 | |
| 	for i, action := range actions {
 | |
| 		refreshActions[i] = &RefreshAction{
 | |
| 			Type:            sd.categorizeAction(action),
 | |
| 			Description:     action,
 | |
| 			Priority:        sd.calculateActionPriority(action, node),
 | |
| 			EstimatedEffort: sd.estimateEffort(action),
 | |
| 			RequiredRoles:   sd.getRequiredRoles(action),
 | |
| 			Dependencies:    sd.getActionDependencies(action),
 | |
| 			Metadata:        make(map[string]interface{}),
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Sort by priority
 | |
| 	sort.Slice(refreshActions, func(i, j int) bool {
 | |
| 		return refreshActions[i].Priority > refreshActions[j].Priority
 | |
| 	})
 | |
| 	
 | |
| 	return refreshActions, nil
 | |
| }
 | |
| 
 | |
| // UpdateStalenessWeights updates weights used in staleness calculation
 | |
| func (sd *stalenessDetectorImpl) UpdateStalenessWeights(weights *StalenessWeights) error {
 | |
| 	sd.mu.Lock()
 | |
| 	defer sd.mu.Unlock()
 | |
| 	
 | |
| 	// Validate weights
 | |
| 	if err := sd.validateWeights(weights); err != nil {
 | |
| 		return fmt.Errorf("invalid weights: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	sd.weights = weights
 | |
| 	sd.graph.stalenessWeight = weights
 | |
| 	
 | |
| 	// Clear cache to force recalculation
 | |
| 	sd.cachedStaleContexts = nil
 | |
| 	sd.cachedStatistics = nil
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // GetStalenessStats returns staleness detection statistics
 | |
| func (sd *stalenessDetectorImpl) GetStalenessStats() (*StalenessStatistics, error) {
 | |
| 	sd.mu.RLock()
 | |
| 	defer sd.mu.RUnlock()
 | |
| 	
 | |
| 	if sd.cachedStatistics != nil {
 | |
| 		return sd.cachedStatistics, nil
 | |
| 	}
 | |
| 	
 | |
| 	// Generate fresh statistics
 | |
| 	sd.graph.mu.RLock()
 | |
| 	defer sd.graph.mu.RUnlock()
 | |
| 	
 | |
| 	totalContexts := int64(len(sd.graph.nodes))
 | |
| 	staleCount := int64(0)
 | |
| 	totalStaleness := 0.0
 | |
| 	maxStaleness := 0.0
 | |
| 	
 | |
| 	for _, node := range sd.graph.nodes {
 | |
| 		staleness := sd.calculateNodeStaleness(node)
 | |
| 		totalStaleness += staleness
 | |
| 		
 | |
| 		if staleness > maxStaleness {
 | |
| 			maxStaleness = staleness
 | |
| 		}
 | |
| 		
 | |
| 		if staleness >= sd.defaultThreshold {
 | |
| 			staleCount++
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	avgStaleness := 0.0
 | |
| 	if totalContexts > 0 {
 | |
| 		avgStaleness = totalStaleness / float64(totalContexts)
 | |
| 	}
 | |
| 	
 | |
| 	stalenessRate := 0.0
 | |
| 	if totalContexts > 0 {
 | |
| 		stalenessRate = float64(staleCount) / float64(totalContexts) * 100.0
 | |
| 	}
 | |
| 	
 | |
| 	stats := &StalenessStatistics{
 | |
| 		TotalContexts:          totalContexts,
 | |
| 		StaleContexts:          staleCount,
 | |
| 		StalenessRate:          stalenessRate,
 | |
| 		AverageStaleness:       avgStaleness,
 | |
| 		MaxStaleness:           maxStaleness,
 | |
| 		LastDetectionRun:       sd.lastDetectionRun,
 | |
| 		DetectionDuration:      0, // Will be updated during actual detection
 | |
| 		RefreshRecommendations: staleCount, // One recommendation per stale context
 | |
| 	}
 | |
| 	
 | |
| 	sd.cachedStatistics = stats
 | |
| 	
 | |
| 	return stats, nil
 | |
| }
 | |
| 
 | |
| // Core staleness calculation logic
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateNodeStaleness(node *TemporalNode) float64 {
 | |
| 	staleness := 0.0
 | |
| 	
 | |
| 	// Time-based staleness
 | |
| 	if sd.enableTimeBasedStaleness {
 | |
| 		timeStaleness := sd.calculateTimeStaleness(node)
 | |
| 		staleness += timeStaleness * sd.weights.TimeWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Influence-based staleness
 | |
| 	if sd.enableInfluenceBasedStaleness {
 | |
| 		influenceStaleness := sd.calculateInfluenceStaleness(node)
 | |
| 		staleness += influenceStaleness * sd.weights.InfluenceWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Activity-based staleness
 | |
| 	if sd.enableActivityBasedStaleness {
 | |
| 		activityStaleness := sd.calculateActivityStaleness(node)
 | |
| 		staleness += activityStaleness * sd.weights.ActivityWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Importance-based staleness
 | |
| 	if sd.enableImportanceBasedStaleness {
 | |
| 		importanceStaleness := sd.calculateImportanceStaleness(node)
 | |
| 		staleness += importanceStaleness * sd.weights.ImportanceWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Complexity-based staleness
 | |
| 	if sd.enableComplexityBasedStaleness {
 | |
| 		complexityStaleness := sd.calculateComplexityStaleness(node)
 | |
| 		staleness += complexityStaleness * sd.weights.ComplexityWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Dependency-based staleness
 | |
| 	if sd.enableDependencyBasedStaleness {
 | |
| 		dependencyStaleness := sd.calculateDependencyStaleness(node)
 | |
| 		staleness += dependencyStaleness * sd.weights.DependencyWeight
 | |
| 	}
 | |
| 	
 | |
| 	// Ensure staleness is between 0 and 1
 | |
| 	return math.Max(0, math.Min(1.0, staleness))
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateTimeStaleness(node *TemporalNode) float64 {
 | |
| 	timeSinceUpdate := time.Since(node.Timestamp)
 | |
| 	
 | |
| 	// Define staleness curve: contexts become stale over time
 | |
| 	// Fresh (0-7 days): 0-0.2 staleness
 | |
| 	// Moderate (7-30 days): 0.2-0.6 staleness
 | |
| 	// Stale (30-90 days): 0.6-0.9 staleness
 | |
| 	// Very stale (90+ days): 0.9-1.0 staleness
 | |
| 	
 | |
| 	days := timeSinceUpdate.Hours() / 24.0
 | |
| 	
 | |
| 	if days <= 7 {
 | |
| 		return days / 35.0 // 0-0.2 over 7 days
 | |
| 	} else if days <= 30 {
 | |
| 		return 0.2 + ((days-7)/23.0)*0.4 // 0.2-0.6 over 23 days
 | |
| 	} else if days <= 90 {
 | |
| 		return 0.6 + ((days-30)/60.0)*0.3 // 0.6-0.9 over 60 days
 | |
| 	} else {
 | |
| 		return 0.9 + math.Min(0.1, (days-90)/365.0*0.1) // 0.9-1.0 over 365 days
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateInfluenceStaleness(node *TemporalNode) float64 {
 | |
| 	// Context becomes stale if its influencers have changed significantly
 | |
| 	staleness := 0.0
 | |
| 	
 | |
| 	// Check if influencers have changed recently
 | |
| 	cutoff := time.Now().Add(-sd.analysisWindow)
 | |
| 	recentChanges := 0
 | |
| 	totalInfluencers := len(node.InfluencedBy)
 | |
| 	
 | |
| 	if totalInfluencers == 0 {
 | |
| 		return 0.0 // No influencers means no influence-based staleness
 | |
| 	}
 | |
| 	
 | |
| 	for _, influencerAddr := range node.InfluencedBy {
 | |
| 		if influencerNode := sd.findLatestNodeByAddress(influencerAddr); influencerNode != nil {
 | |
| 			if influencerNode.Timestamp.After(cutoff) {
 | |
| 				recentChanges++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Higher staleness if many influencers have changed
 | |
| 	if totalInfluencers > 0 {
 | |
| 		staleness = float64(recentChanges) / float64(totalInfluencers)
 | |
| 	}
 | |
| 	
 | |
| 	// Boost staleness if this node hasn't been updated despite influencer changes
 | |
| 	if recentChanges > 0 && node.Timestamp.Before(cutoff) {
 | |
| 		staleness *= 1.5 // Amplify staleness
 | |
| 	}
 | |
| 	
 | |
| 	return math.Min(1.0, staleness)
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateActivityStaleness(node *TemporalNode) float64 {
 | |
| 	// Context becomes stale if there's been a lot of related activity
 | |
| 	activityScore := 0.0
 | |
| 	cutoff := time.Now().Add(-7 * 24 * time.Hour) // Look at last week
 | |
| 	
 | |
| 	// Count recent decisions in the influence network
 | |
| 	recentDecisions := 0
 | |
| 	totalConnections := len(node.Influences) + len(node.InfluencedBy)
 | |
| 	
 | |
| 	if totalConnections == 0 {
 | |
| 		return 0.0
 | |
| 	}
 | |
| 	
 | |
| 	// Check influences
 | |
| 	for _, influencedAddr := range node.Influences {
 | |
| 		if influencedNode := sd.findLatestNodeByAddress(influencedAddr); influencedNode != nil {
 | |
| 			if influencedNode.Timestamp.After(cutoff) {
 | |
| 				recentDecisions++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Check influencers
 | |
| 	for _, influencerAddr := range node.InfluencedBy {
 | |
| 		if influencerNode := sd.findLatestNodeByAddress(influencerAddr); influencerNode != nil {
 | |
| 			if influencerNode.Timestamp.After(cutoff) {
 | |
| 				recentDecisions++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// High activity in network while this node is unchanged suggests staleness
 | |
| 	activityScore = float64(recentDecisions) / float64(totalConnections)
 | |
| 	
 | |
| 	// Amplify if this node is particularly old relative to the activity
 | |
| 	nodeAge := time.Since(node.Timestamp).Hours() / 24.0
 | |
| 	if nodeAge > 7 && activityScore > 0.3 {
 | |
| 		activityScore *= 1.3
 | |
| 	}
 | |
| 	
 | |
| 	return math.Min(1.0, activityScore)
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateImportanceStaleness(node *TemporalNode) float64 {
 | |
| 	// Important contexts (high influence, broad scope) become stale faster
 | |
| 	importanceMultiplier := 1.0
 | |
| 	
 | |
| 	// Factor in impact scope
 | |
| 	switch node.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		importanceMultiplier *= 1.4
 | |
| 	case ImpactProject:
 | |
| 		importanceMultiplier *= 1.2
 | |
| 	case ImpactModule:
 | |
| 		importanceMultiplier *= 1.1
 | |
| 	case ImpactLocal:
 | |
| 		importanceMultiplier *= 1.0
 | |
| 	}
 | |
| 	
 | |
| 	// Factor in influence count
 | |
| 	influenceCount := len(node.Influences)
 | |
| 	if influenceCount > 5 {
 | |
| 		importanceMultiplier *= 1.3
 | |
| 	} else if influenceCount > 2 {
 | |
| 		importanceMultiplier *= 1.1
 | |
| 	}
 | |
| 	
 | |
| 	// Factor in confidence (low confidence = higher staleness importance)
 | |
| 	if node.Confidence < 0.6 {
 | |
| 		importanceMultiplier *= 1.2
 | |
| 	}
 | |
| 	
 | |
| 	// Base staleness from time, amplified by importance
 | |
| 	timeStaleness := sd.calculateTimeStaleness(node)
 | |
| 	
 | |
| 	return math.Min(1.0, timeStaleness * importanceMultiplier)
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateComplexityStaleness(node *TemporalNode) float64 {
 | |
| 	// Complex contexts (many technologies, long descriptions) become stale faster
 | |
| 	complexityScore := 0.0
 | |
| 	
 | |
| 	if node.Context != nil {
 | |
| 		// Factor in technology count
 | |
| 		techCount := len(node.Context.Technologies)
 | |
| 		complexityScore += math.Min(0.3, float64(techCount)/10.0)
 | |
| 		
 | |
| 		// Factor in insight count
 | |
| 		insightCount := len(node.Context.Insights)
 | |
| 		complexityScore += math.Min(0.2, float64(insightCount)/5.0)
 | |
| 		
 | |
| 		// Factor in summary length (longer = more complex)
 | |
| 		summaryLength := len(node.Context.Summary)
 | |
| 		complexityScore += math.Min(0.2, float64(summaryLength)/500.0)
 | |
| 		
 | |
| 		// Factor in purpose length
 | |
| 		purposeLength := len(node.Context.Purpose)
 | |
| 		complexityScore += math.Min(0.2, float64(purposeLength)/300.0)
 | |
| 		
 | |
| 		// Factor in tag count
 | |
| 		tagCount := len(node.Context.Tags)
 | |
| 		complexityScore += math.Min(0.1, float64(tagCount)/5.0)
 | |
| 	}
 | |
| 	
 | |
| 	// Complex contexts need more frequent updates
 | |
| 	timeFactor := sd.calculateTimeStaleness(node)
 | |
| 	
 | |
| 	return math.Min(1.0, complexityScore * timeFactor * 1.5)
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateDependencyStaleness(node *TemporalNode) float64 {
 | |
| 	// Context becomes stale if its dependencies have changed
 | |
| 	staleness := 0.0
 | |
| 	
 | |
| 	// Check if any dependencies (influencers) have evolved significantly
 | |
| 	if len(node.InfluencedBy) == 0 {
 | |
| 		return 0.0
 | |
| 	}
 | |
| 	
 | |
| 	significantChanges := 0
 | |
| 	for _, depAddr := range node.InfluencedBy {
 | |
| 		if depNode := sd.findLatestNodeByAddress(depAddr); depNode != nil {
 | |
| 			// Check if dependency has had major changes
 | |
| 			if sd.hasSignificantChange(depNode, node.Timestamp) {
 | |
| 				significantChanges++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	staleness = float64(significantChanges) / float64(len(node.InfluencedBy))
 | |
| 	
 | |
| 	// Amplify if the changes are architectural or requirements-related
 | |
| 	for _, depAddr := range node.InfluencedBy {
 | |
| 		if depNode := sd.findLatestNodeByAddress(depAddr); depNode != nil {
 | |
| 			if depNode.ChangeReason == ReasonArchitectureChange || 
 | |
| 			   depNode.ChangeReason == ReasonRequirementsChange {
 | |
| 				staleness *= 1.3
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return math.Min(1.0, staleness)
 | |
| }
 | |
| 
 | |
| // Helper methods for staleness analysis
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) analyzeStalenessReasons(node *TemporalNode, stalenessScore float64) []string {
 | |
| 	reasons := make([]string, 0)
 | |
| 	
 | |
| 	// Time-based reasons
 | |
| 	timeSinceUpdate := time.Since(node.Timestamp)
 | |
| 	if timeSinceUpdate > 30*24*time.Hour {
 | |
| 		reasons = append(reasons, fmt.Sprintf("not updated in %d days", int(timeSinceUpdate.Hours()/24)))
 | |
| 	} else if timeSinceUpdate > 7*24*time.Hour {
 | |
| 		reasons = append(reasons, fmt.Sprintf("not updated in %d days", int(timeSinceUpdate.Hours()/24)))
 | |
| 	}
 | |
| 	
 | |
| 	// Influence-based reasons
 | |
| 	recentInfluencerChanges := sd.countRecentInfluencerChanges(node)
 | |
| 	if recentInfluencerChanges > 0 {
 | |
| 		reasons = append(reasons, fmt.Sprintf("%d influencing contexts have changed recently", recentInfluencerChanges))
 | |
| 	}
 | |
| 	
 | |
| 	// Activity-based reasons
 | |
| 	networkActivity := sd.calculateNetworkActivity(node)
 | |
| 	if networkActivity > 0.5 {
 | |
| 		reasons = append(reasons, "high activity in related contexts")
 | |
| 	}
 | |
| 	
 | |
| 	// Confidence-based reasons
 | |
| 	if node.Confidence < 0.6 {
 | |
| 		reasons = append(reasons, fmt.Sprintf("low confidence score (%.2f)", node.Confidence))
 | |
| 	}
 | |
| 	
 | |
| 	// Dependency-based reasons
 | |
| 	dependencyChanges := sd.countDependencyChanges(node)
 | |
| 	if dependencyChanges > 0 {
 | |
| 		reasons = append(reasons, fmt.Sprintf("%d dependencies have changed", dependencyChanges))
 | |
| 	}
 | |
| 	
 | |
| 	// Scope-based reasons
 | |
| 	if node.ImpactScope == ImpactSystem || node.ImpactScope == ImpactProject {
 | |
| 		reasons = append(reasons, "high impact scope requires frequent updates")
 | |
| 	}
 | |
| 	
 | |
| 	return reasons
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) generateRefreshActions(node *TemporalNode) []string {
 | |
| 	actions := make([]string, 0)
 | |
| 	
 | |
| 	// Always suggest basic review
 | |
| 	actions = append(actions, "review context accuracy and completeness")
 | |
| 	
 | |
| 	// Time-based actions
 | |
| 	if time.Since(node.Timestamp) > 7*24*time.Hour {
 | |
| 		actions = append(actions, "update context with recent changes")
 | |
| 	}
 | |
| 	
 | |
| 	// Influence-based actions
 | |
| 	if sd.countRecentInfluencerChanges(node) > 0 {
 | |
| 		actions = append(actions, "review influencing contexts for impact")
 | |
| 		actions = append(actions, "validate dependencies are still accurate")
 | |
| 	}
 | |
| 	
 | |
| 	// Confidence-based actions
 | |
| 	if node.Confidence < 0.7 {
 | |
| 		actions = append(actions, "improve context confidence through additional analysis")
 | |
| 		actions = append(actions, "validate context information with subject matter experts")
 | |
| 	}
 | |
| 	
 | |
| 	// Technology-based actions
 | |
| 	if node.Context != nil && len(node.Context.Technologies) > 5 {
 | |
| 		actions = append(actions, "review technology stack for changes")
 | |
| 		actions = append(actions, "update technology versions and compatibility")
 | |
| 	}
 | |
| 	
 | |
| 	// Impact-based actions
 | |
| 	if node.ImpactScope == ImpactSystem || node.ImpactScope == ImpactProject {
 | |
| 		actions = append(actions, "conduct architectural review")
 | |
| 		actions = append(actions, "validate system-wide impact assumptions")
 | |
| 	}
 | |
| 	
 | |
| 	// Network-based actions
 | |
| 	if len(node.Influences) > 3 {
 | |
| 		actions = append(actions, "review all influenced contexts for consistency")
 | |
| 	}
 | |
| 	
 | |
| 	return actions
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) findRelatedChanges(node *TemporalNode) []ucxl.Address {
 | |
| 	relatedChanges := make([]ucxl.Address, 0)
 | |
| 	cutoff := time.Now().Add(-7 * 24 * time.Hour)
 | |
| 	
 | |
| 	// Find recent changes in the influence network
 | |
| 	for _, addr := range node.Influences {
 | |
| 		if relatedNode := sd.findLatestNodeByAddress(addr); relatedNode != nil {
 | |
| 			if relatedNode.Timestamp.After(cutoff) {
 | |
| 				relatedChanges = append(relatedChanges, addr)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	for _, addr := range node.InfluencedBy {
 | |
| 		if relatedNode := sd.findLatestNodeByAddress(addr); relatedNode != nil {
 | |
| 			if relatedNode.Timestamp.After(cutoff) {
 | |
| 				relatedChanges = append(relatedChanges, addr)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return relatedChanges
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculatePriority(stalenessScore float64, node *TemporalNode) StalePriority {
 | |
| 	// Start with staleness score
 | |
| 	priority := stalenessScore
 | |
| 	
 | |
| 	// Adjust based on impact scope
 | |
| 	switch node.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		priority += 0.3
 | |
| 	case ImpactProject:
 | |
| 		priority += 0.2
 | |
| 	case ImpactModule:
 | |
| 		priority += 0.1
 | |
| 	}
 | |
| 	
 | |
| 	// Adjust based on influence count
 | |
| 	influenceCount := len(node.Influences)
 | |
| 	if influenceCount > 5 {
 | |
| 		priority += 0.2
 | |
| 	} else if influenceCount > 2 {
 | |
| 		priority += 0.1
 | |
| 	}
 | |
| 	
 | |
| 	// Adjust based on age
 | |
| 	age := time.Since(node.Timestamp)
 | |
| 	if age > 90*24*time.Hour {
 | |
| 		priority += 0.1
 | |
| 	}
 | |
| 	
 | |
| 	// Convert to priority level
 | |
| 	if priority >= 0.9 {
 | |
| 		return PriorityCritical
 | |
| 	} else if priority >= 0.7 {
 | |
| 		return PriorityHigh
 | |
| 	} else if priority >= 0.5 {
 | |
| 		return PriorityMedium
 | |
| 	}
 | |
| 	return PriorityLow
 | |
| }
 | |
| 
 | |
| // Additional helper methods
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) findLatestNodeByAddress(address ucxl.Address) *TemporalNode {
 | |
| 	addressKey := address.String()
 | |
| 	if nodes, exists := sd.graph.addressToNodes[addressKey]; exists && len(nodes) > 0 {
 | |
| 		return nodes[len(nodes)-1]
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) hasSignificantChange(node *TemporalNode, since time.Time) bool {
 | |
| 	if node.Timestamp.Before(since) {
 | |
| 		return false
 | |
| 	}
 | |
| 	
 | |
| 	// Consider architectural and requirements changes as significant
 | |
| 	return node.ChangeReason == ReasonArchitectureChange ||
 | |
| 		   node.ChangeReason == ReasonRequirementsChange ||
 | |
| 		   node.ChangeReason == ReasonDesignDecision
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) countRecentInfluencerChanges(node *TemporalNode) int {
 | |
| 	cutoff := time.Now().Add(-7 * 24 * time.Hour)
 | |
| 	changes := 0
 | |
| 	
 | |
| 	for _, addr := range node.InfluencedBy {
 | |
| 		if influencerNode := sd.findLatestNodeByAddress(addr); influencerNode != nil {
 | |
| 			if influencerNode.Timestamp.After(cutoff) {
 | |
| 				changes++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return changes
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateNetworkActivity(node *TemporalNode) float64 {
 | |
| 	cutoff := time.Now().Add(-7 * 24 * time.Hour)
 | |
| 	recentChanges := 0
 | |
| 	totalConnections := len(node.Influences) + len(node.InfluencedBy)
 | |
| 	
 | |
| 	if totalConnections == 0 {
 | |
| 		return 0
 | |
| 	}
 | |
| 	
 | |
| 	for _, addr := range node.Influences {
 | |
| 		if relatedNode := sd.findLatestNodeByAddress(addr); relatedNode != nil {
 | |
| 			if relatedNode.Timestamp.After(cutoff) {
 | |
| 				recentChanges++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	for _, addr := range node.InfluencedBy {
 | |
| 		if relatedNode := sd.findLatestNodeByAddress(addr); relatedNode != nil {
 | |
| 			if relatedNode.Timestamp.After(cutoff) {
 | |
| 				recentChanges++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return float64(recentChanges) / float64(totalConnections)
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) countDependencyChanges(node *TemporalNode) int {
 | |
| 	changes := 0
 | |
| 	
 | |
| 	for _, addr := range node.InfluencedBy {
 | |
| 		if depNode := sd.findLatestNodeByAddress(addr); depNode != nil {
 | |
| 			if sd.hasSignificantChange(depNode, node.Timestamp) {
 | |
| 				changes++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return changes
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) validateWeights(weights *StalenessWeights) error {
 | |
| 	if weights.TimeWeight < 0 || weights.TimeWeight > 1 {
 | |
| 		return fmt.Errorf("TimeWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	if weights.InfluenceWeight < 0 || weights.InfluenceWeight > 1 {
 | |
| 		return fmt.Errorf("InfluenceWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	if weights.ActivityWeight < 0 || weights.ActivityWeight > 1 {
 | |
| 		return fmt.Errorf("ActivityWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	if weights.ImportanceWeight < 0 || weights.ImportanceWeight > 1 {
 | |
| 		return fmt.Errorf("ImportanceWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	if weights.ComplexityWeight < 0 || weights.ComplexityWeight > 1 {
 | |
| 		return fmt.Errorf("ComplexityWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	if weights.DependencyWeight < 0 || weights.DependencyWeight > 1 {
 | |
| 		return fmt.Errorf("DependencyWeight must be between 0 and 1")
 | |
| 	}
 | |
| 	
 | |
| 	// Note: We don't require weights to sum to 1.0 as they may be used in different combinations
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) updateStatistics(totalContexts, staleContexts int, duration time.Duration) {
 | |
| 	avgStaleness := 0.0
 | |
| 	maxStaleness := 0.0
 | |
| 	
 | |
| 	if totalContexts > 0 {
 | |
| 		totalStaleness := 0.0
 | |
| 		for _, node := range sd.graph.nodes {
 | |
| 			staleness := sd.calculateNodeStaleness(node)
 | |
| 			totalStaleness += staleness
 | |
| 			if staleness > maxStaleness {
 | |
| 				maxStaleness = staleness
 | |
| 			}
 | |
| 		}
 | |
| 		avgStaleness = totalStaleness / float64(totalContexts)
 | |
| 	}
 | |
| 	
 | |
| 	stalenessRate := 0.0
 | |
| 	if totalContexts > 0 {
 | |
| 		stalenessRate = float64(staleContexts) / float64(totalContexts) * 100.0
 | |
| 	}
 | |
| 	
 | |
| 	sd.cachedStatistics = &StalenessStatistics{
 | |
| 		TotalContexts:          int64(totalContexts),
 | |
| 		StaleContexts:          int64(staleContexts),
 | |
| 		StalenessRate:          stalenessRate,
 | |
| 		AverageStaleness:       avgStaleness,
 | |
| 		MaxStaleness:           maxStaleness,
 | |
| 		LastDetectionRun:       time.Now(),
 | |
| 		DetectionDuration:      duration,
 | |
| 		RefreshRecommendations: int64(staleContexts),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Action categorization and estimation methods
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) categorizeAction(action string) string {
 | |
| 	switch {
 | |
| 	case contains(action, "review"):
 | |
| 		return "review"
 | |
| 	case contains(action, "update"):
 | |
| 		return "update"
 | |
| 	case contains(action, "validate"):
 | |
| 		return "validation"
 | |
| 	case contains(action, "improve"):
 | |
| 		return "improvement"
 | |
| 	case contains(action, "technology"):
 | |
| 		return "technical"
 | |
| 	case contains(action, "architectural"):
 | |
| 		return "architectural"
 | |
| 	default:
 | |
| 		return "general"
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) calculateActionPriority(action string, node *TemporalNode) int {
 | |
| 	priority := 5 // Base priority
 | |
| 	
 | |
| 	// Increase priority for system/project scope
 | |
| 	if node.ImpactScope == ImpactSystem {
 | |
| 		priority += 3
 | |
| 	} else if node.ImpactScope == ImpactProject {
 | |
| 		priority += 2
 | |
| 	}
 | |
| 	
 | |
| 	// Increase priority for high-influence nodes
 | |
| 	if len(node.Influences) > 5 {
 | |
| 		priority += 2
 | |
| 	}
 | |
| 	
 | |
| 	// Increase priority for architectural actions
 | |
| 	if contains(action, "architectural") {
 | |
| 		priority += 2
 | |
| 	}
 | |
| 	
 | |
| 	// Increase priority for validation actions
 | |
| 	if contains(action, "validate") {
 | |
| 		priority += 1
 | |
| 	}
 | |
| 	
 | |
| 	return priority
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) estimateEffort(action string) string {
 | |
| 	switch {
 | |
| 	case contains(action, "review context accuracy"):
 | |
| 		return "medium"
 | |
| 	case contains(action, "architectural review"):
 | |
| 		return "high"
 | |
| 	case contains(action, "validate dependencies"):
 | |
| 		return "medium"
 | |
| 	case contains(action, "update context"):
 | |
| 		return "low"
 | |
| 	case contains(action, "improve confidence"):
 | |
| 		return "high"
 | |
| 	case contains(action, "technology"):
 | |
| 		return "medium"
 | |
| 	default:
 | |
| 		return "medium"
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) getRequiredRoles(action string) []string {
 | |
| 	switch {
 | |
| 	case contains(action, "architectural"):
 | |
| 		return []string{"architect", "technical_lead"}
 | |
| 	case contains(action, "technology"):
 | |
| 		return []string{"developer", "technical_lead"}
 | |
| 	case contains(action, "validate"):
 | |
| 		return []string{"analyst", "subject_matter_expert"}
 | |
| 	case contains(action, "review"):
 | |
| 		return []string{"analyst", "developer"}
 | |
| 	default:
 | |
| 		return []string{"analyst"}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (sd *stalenessDetectorImpl) getActionDependencies(action string) []string {
 | |
| 	dependencies := make([]string, 0)
 | |
| 	
 | |
| 	if contains(action, "architectural") {
 | |
| 		dependencies = append(dependencies, "stakeholder_availability", "documentation_access")
 | |
| 	}
 | |
| 	
 | |
| 	if contains(action, "validate dependencies") {
 | |
| 		dependencies = append(dependencies, "dependency_analysis", "influence_mapping")
 | |
| 	}
 | |
| 	
 | |
| 	if contains(action, "improve confidence") {
 | |
| 		dependencies = append(dependencies, "expert_review", "additional_analysis")
 | |
| 	}
 | |
| 	
 | |
| 	return dependencies
 | |
| } |