 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>
		
			
				
	
	
		
			569 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			569 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package temporal
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/ucxl"
 | |
| )
 | |
| 
 | |
| // decisionNavigatorImpl implements the DecisionNavigator interface
 | |
| type decisionNavigatorImpl struct {
 | |
| 	mu sync.RWMutex
 | |
| 	
 | |
| 	// Reference to the temporal graph
 | |
| 	graph *temporalGraphImpl
 | |
| 	
 | |
| 	// Navigation state
 | |
| 	navigationSessions map[string]*NavigationSession
 | |
| 	bookmarks          map[string]*DecisionBookmark
 | |
| 	
 | |
| 	// Configuration
 | |
| 	maxNavigationHistory int
 | |
| }
 | |
| 
 | |
| // NavigationSession represents a navigation session
 | |
| type NavigationSession struct {
 | |
| 	ID               string           `json:"id"`
 | |
| 	UserID           string           `json:"user_id"`
 | |
| 	StartedAt        time.Time        `json:"started_at"`
 | |
| 	LastActivity     time.Time        `json:"last_activity"`
 | |
| 	CurrentPosition  ucxl.Address     `json:"current_position"`
 | |
| 	History          []*DecisionStep  `json:"history"`
 | |
| 	Bookmarks        []string         `json:"bookmarks"`
 | |
| 	Preferences      *NavPreferences  `json:"preferences"`
 | |
| }
 | |
| 
 | |
| // NavPreferences represents navigation preferences
 | |
| type NavPreferences struct {
 | |
| 	MaxHops              int     `json:"max_hops"`
 | |
| 	PreferRecentDecisions bool    `json:"prefer_recent_decisions"`
 | |
| 	FilterByConfidence   float64 `json:"filter_by_confidence"`
 | |
| 	IncludeStaleContexts bool    `json:"include_stale_contexts"`
 | |
| }
 | |
| 
 | |
| // NewDecisionNavigator creates a new decision navigator
 | |
| func NewDecisionNavigator(graph *temporalGraphImpl) DecisionNavigator {
 | |
| 	return &decisionNavigatorImpl{
 | |
| 		graph:                graph,
 | |
| 		navigationSessions:   make(map[string]*NavigationSession),
 | |
| 		bookmarks:           make(map[string]*DecisionBookmark),
 | |
| 		maxNavigationHistory: 100,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NavigateDecisionHops navigates by decision distance, not time
 | |
| func (dn *decisionNavigatorImpl) NavigateDecisionHops(ctx context.Context, address ucxl.Address, 
 | |
| 	hops int, direction NavigationDirection) (*TemporalNode, error) {
 | |
| 	
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	// Get starting node
 | |
| 	startNode, err := dn.graph.getLatestNodeUnsafe(address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get starting node: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Navigate by hops
 | |
| 	currentNode := startNode
 | |
| 	for i := 0; i < hops; i++ {
 | |
| 		nextNode, err := dn.navigateOneHop(currentNode, direction)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to navigate hop %d: %w", i+1, err)
 | |
| 		}
 | |
| 		currentNode = nextNode
 | |
| 	}
 | |
| 	
 | |
| 	return currentNode, nil
 | |
| }
 | |
| 
 | |
| // GetDecisionTimeline gets timeline ordered by decision sequence
 | |
| func (dn *decisionNavigatorImpl) GetDecisionTimeline(ctx context.Context, address ucxl.Address, 
 | |
| 	includeRelated bool, maxHops int) (*DecisionTimeline, error) {
 | |
| 	
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	// Get evolution history for the primary address
 | |
| 	history, err := dn.graph.GetEvolutionHistory(ctx, address)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get evolution history: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Build decision timeline entries
 | |
| 	decisionSequence := make([]*DecisionTimelineEntry, len(history))
 | |
| 	for i, node := range history {
 | |
| 		entry := &DecisionTimelineEntry{
 | |
| 			Version:             node.Version,
 | |
| 			DecisionHop:         node.Version, // Version number as decision hop
 | |
| 			ChangeReason:        node.ChangeReason,
 | |
| 			DecisionMaker:       dn.getDecisionMaker(node),
 | |
| 			DecisionRationale:   dn.getDecisionRationale(node),
 | |
| 			ConfidenceEvolution: node.Confidence,
 | |
| 			Timestamp:           node.Timestamp,
 | |
| 			InfluencesCount:     len(node.Influences),
 | |
| 			InfluencedByCount:   len(node.InfluencedBy),
 | |
| 			ImpactScope:         node.ImpactScope,
 | |
| 			Metadata:            make(map[string]interface{}),
 | |
| 		}
 | |
| 		decisionSequence[i] = entry
 | |
| 	}
 | |
| 	
 | |
| 	// Get related decisions if requested
 | |
| 	relatedDecisions := make([]*RelatedDecision, 0)
 | |
| 	if includeRelated && maxHops > 0 {
 | |
| 		relatedPaths, err := dn.graph.FindRelatedDecisions(ctx, address, maxHops)
 | |
| 		if err == nil {
 | |
| 			for _, path := range relatedPaths {
 | |
| 				if len(path.Steps) > 0 {
 | |
| 					lastStep := path.Steps[len(path.Steps)-1]
 | |
| 					related := &RelatedDecision{
 | |
| 						Address:               path.To,
 | |
| 						DecisionHops:          path.TotalHops,
 | |
| 						LatestVersion:         lastStep.TemporalNode.Version,
 | |
| 						ChangeReason:          lastStep.TemporalNode.ChangeReason,
 | |
| 						DecisionMaker:         dn.getDecisionMaker(lastStep.TemporalNode),
 | |
| 						Confidence:            lastStep.TemporalNode.Confidence,
 | |
| 						LastDecisionTimestamp: lastStep.TemporalNode.Timestamp,
 | |
| 						RelationshipType:      lastStep.Relationship,
 | |
| 					}
 | |
| 					relatedDecisions = append(relatedDecisions, related)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Calculate timeline analysis
 | |
| 	analysis := dn.analyzeTimeline(decisionSequence, relatedDecisions)
 | |
| 	
 | |
| 	// Calculate time span
 | |
| 	var timeSpan time.Duration
 | |
| 	if len(history) > 1 {
 | |
| 		timeSpan = history[len(history)-1].Timestamp.Sub(history[0].Timestamp)
 | |
| 	}
 | |
| 	
 | |
| 	timeline := &DecisionTimeline{
 | |
| 		PrimaryAddress:   address,
 | |
| 		DecisionSequence: decisionSequence,
 | |
| 		RelatedDecisions: relatedDecisions,
 | |
| 		TotalDecisions:   len(decisionSequence),
 | |
| 		TimeSpan:         timeSpan,
 | |
| 		AnalysisMetadata: analysis,
 | |
| 	}
 | |
| 	
 | |
| 	return timeline, nil
 | |
| }
 | |
| 
 | |
| // FindStaleContexts finds contexts that may be outdated based on decisions
 | |
| func (dn *decisionNavigatorImpl) FindStaleContexts(ctx context.Context, stalenessThreshold float64) ([]*StaleContext, error) {
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	staleContexts := make([]*StaleContext, 0)
 | |
| 	
 | |
| 	// Check all nodes for staleness
 | |
| 	for _, node := range dn.graph.nodes {
 | |
| 		if node.Staleness >= stalenessThreshold {
 | |
| 			staleness := &StaleContext{
 | |
| 				UCXLAddress:    node.UCXLAddress,
 | |
| 				TemporalNode:   node,
 | |
| 				StalenessScore: node.Staleness,
 | |
| 				LastUpdated:    node.Timestamp,
 | |
| 				Reasons:        dn.getStalenessReasons(node),
 | |
| 				SuggestedActions: dn.getSuggestedActions(node),
 | |
| 				RelatedChanges: dn.getRelatedChanges(node),
 | |
| 				Priority:       dn.calculateStalePriority(node),
 | |
| 			}
 | |
| 			staleContexts = append(staleContexts, staleness)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Sort by staleness score (highest first)
 | |
| 	sort.Slice(staleContexts, func(i, j int) bool {
 | |
| 		return staleContexts[i].StalenessScore > staleContexts[j].StalenessScore
 | |
| 	})
 | |
| 	
 | |
| 	return staleContexts, nil
 | |
| }
 | |
| 
 | |
| // ValidateDecisionPath validates that a decision path is reachable
 | |
| func (dn *decisionNavigatorImpl) ValidateDecisionPath(ctx context.Context, path []*DecisionStep) error {
 | |
| 	if len(path) == 0 {
 | |
| 		return fmt.Errorf("empty decision path")
 | |
| 	}
 | |
| 	
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	// Validate each step in the path
 | |
| 	for i, step := range path {
 | |
| 		// Check if the temporal node exists
 | |
| 		if step.TemporalNode == nil {
 | |
| 			return fmt.Errorf("step %d has nil temporal node", i)
 | |
| 		}
 | |
| 		
 | |
| 		nodeID := step.TemporalNode.ID
 | |
| 		if _, exists := dn.graph.nodes[nodeID]; !exists {
 | |
| 			return fmt.Errorf("step %d references non-existent node %s", i, nodeID)
 | |
| 		}
 | |
| 		
 | |
| 		// Validate hop distance
 | |
| 		if step.HopDistance != i {
 | |
| 			return fmt.Errorf("step %d has incorrect hop distance: expected %d, got %d", 
 | |
| 				i, i, step.HopDistance)
 | |
| 		}
 | |
| 		
 | |
| 		// Validate relationship to next step
 | |
| 		if i < len(path)-1 {
 | |
| 			nextStep := path[i+1]
 | |
| 			if !dn.validateStepRelationship(step, nextStep) {
 | |
| 				return fmt.Errorf("invalid relationship between step %d and %d", i, i+1)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // GetNavigationHistory gets navigation history for a session
 | |
| func (dn *decisionNavigatorImpl) GetNavigationHistory(ctx context.Context, sessionID string) ([]*DecisionStep, error) {
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	session, exists := dn.navigationSessions[sessionID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("navigation session %s not found", sessionID)
 | |
| 	}
 | |
| 	
 | |
| 	// Return a copy of the history
 | |
| 	history := make([]*DecisionStep, len(session.History))
 | |
| 	copy(history, session.History)
 | |
| 	
 | |
| 	return history, nil
 | |
| }
 | |
| 
 | |
| // ResetNavigation resets navigation state to latest versions
 | |
| func (dn *decisionNavigatorImpl) ResetNavigation(ctx context.Context, address ucxl.Address) error {
 | |
| 	dn.mu.Lock()
 | |
| 	defer dn.mu.Unlock()
 | |
| 	
 | |
| 	// Clear any navigation sessions for this address
 | |
| 	for sessionID, session := range dn.navigationSessions {
 | |
| 		if session.CurrentPosition.String() == address.String() {
 | |
| 			// Reset to latest version
 | |
| 			latestNode, err := dn.graph.getLatestNodeUnsafe(address)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to get latest node: %w", err)
 | |
| 			}
 | |
| 			
 | |
| 			session.CurrentPosition = address
 | |
| 			session.History = []*DecisionStep{}
 | |
| 			session.LastActivity = time.Now()
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // BookmarkDecision creates a bookmark for a specific decision point
 | |
| func (dn *decisionNavigatorImpl) BookmarkDecision(ctx context.Context, address ucxl.Address, hop int, name string) error {
 | |
| 	dn.mu.Lock()
 | |
| 	defer dn.mu.Unlock()
 | |
| 	
 | |
| 	// Validate the decision point exists
 | |
| 	node, err := dn.graph.GetVersionAtDecision(ctx, address, hop)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("decision point not found: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Create bookmark
 | |
| 	bookmarkID := fmt.Sprintf("%s-%d-%d", address.String(), hop, time.Now().Unix())
 | |
| 	bookmark := &DecisionBookmark{
 | |
| 		ID:          bookmarkID,
 | |
| 		Name:        name,
 | |
| 		Description: fmt.Sprintf("Decision at hop %d for %s", hop, address.String()),
 | |
| 		Address:     address,
 | |
| 		DecisionHop: hop,
 | |
| 		CreatedBy:   "system", // Could be passed as parameter
 | |
| 		CreatedAt:   time.Now(),
 | |
| 		Tags:        []string{},
 | |
| 		Metadata:    make(map[string]interface{}),
 | |
| 	}
 | |
| 	
 | |
| 	// Add context information to metadata
 | |
| 	bookmark.Metadata["change_reason"] = node.ChangeReason
 | |
| 	bookmark.Metadata["decision_id"] = node.DecisionID
 | |
| 	bookmark.Metadata["confidence"] = node.Confidence
 | |
| 	
 | |
| 	dn.bookmarks[bookmarkID] = bookmark
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ListBookmarks lists all bookmarks for navigation
 | |
| func (dn *decisionNavigatorImpl) ListBookmarks(ctx context.Context) ([]*DecisionBookmark, error) {
 | |
| 	dn.mu.RLock()
 | |
| 	defer dn.mu.RUnlock()
 | |
| 	
 | |
| 	bookmarks := make([]*DecisionBookmark, 0, len(dn.bookmarks))
 | |
| 	for _, bookmark := range dn.bookmarks {
 | |
| 		bookmarks = append(bookmarks, bookmark)
 | |
| 	}
 | |
| 	
 | |
| 	// Sort by creation time (newest first)
 | |
| 	sort.Slice(bookmarks, func(i, j int) bool {
 | |
| 		return bookmarks[i].CreatedAt.After(bookmarks[j].CreatedAt)
 | |
| 	})
 | |
| 	
 | |
| 	return bookmarks, nil
 | |
| }
 | |
| 
 | |
| // Helper methods
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) navigateOneHop(currentNode *TemporalNode, direction NavigationDirection) (*TemporalNode, error) {
 | |
| 	switch direction {
 | |
| 	case NavigationForward:
 | |
| 		return dn.navigateForward(currentNode)
 | |
| 	case NavigationBackward:
 | |
| 		return dn.navigateBackward(currentNode)
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("invalid navigation direction: %s", direction)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) navigateForward(currentNode *TemporalNode) (*TemporalNode, error) {
 | |
| 	// Forward navigation means going to a newer decision
 | |
| 	addressKey := currentNode.UCXLAddress.String()
 | |
| 	nodes, exists := dn.graph.addressToNodes[addressKey]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("no nodes found for address")
 | |
| 	}
 | |
| 	
 | |
| 	// Find current node in the list and get the next one
 | |
| 	for i, node := range nodes {
 | |
| 		if node.ID == currentNode.ID && i < len(nodes)-1 {
 | |
| 			return nodes[i+1], nil
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("no forward navigation possible")
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) navigateBackward(currentNode *TemporalNode) (*TemporalNode, error) {
 | |
| 	// Backward navigation means going to an older decision
 | |
| 	if currentNode.ParentNode == nil {
 | |
| 		return nil, fmt.Errorf("no backward navigation possible: no parent node")
 | |
| 	}
 | |
| 	
 | |
| 	parentNode, exists := dn.graph.nodes[*currentNode.ParentNode]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("parent node not found: %s", *currentNode.ParentNode)
 | |
| 	}
 | |
| 	
 | |
| 	return parentNode, nil
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) getDecisionMaker(node *TemporalNode) string {
 | |
| 	if decision, exists := dn.graph.decisions[node.DecisionID]; exists {
 | |
| 		return decision.Maker
 | |
| 	}
 | |
| 	return "unknown"
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) getDecisionRationale(node *TemporalNode) string {
 | |
| 	if decision, exists := dn.graph.decisions[node.DecisionID]; exists {
 | |
| 		return decision.Rationale
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) analyzeTimeline(sequence []*DecisionTimelineEntry, related []*RelatedDecision) *TimelineAnalysis {
 | |
| 	if len(sequence) == 0 {
 | |
| 		return &TimelineAnalysis{
 | |
| 			AnalyzedAt: time.Now(),
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Calculate change velocity
 | |
| 	var changeVelocity float64
 | |
| 	if len(sequence) > 1 {
 | |
| 		firstTime := sequence[0].Timestamp
 | |
| 		lastTime := sequence[len(sequence)-1].Timestamp
 | |
| 		duration := lastTime.Sub(firstTime)
 | |
| 		if duration > 0 {
 | |
| 			changeVelocity = float64(len(sequence)-1) / duration.Hours()
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Analyze confidence trend
 | |
| 	confidenceTrend := "stable"
 | |
| 	if len(sequence) > 1 {
 | |
| 		firstConfidence := sequence[0].ConfidenceEvolution
 | |
| 		lastConfidence := sequence[len(sequence)-1].ConfidenceEvolution
 | |
| 		diff := lastConfidence - firstConfidence
 | |
| 		
 | |
| 		if diff > 0.1 {
 | |
| 			confidenceTrend = "increasing"
 | |
| 		} else if diff < -0.1 {
 | |
| 			confidenceTrend = "decreasing"
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Count change reasons
 | |
| 	reasonCounts := make(map[ChangeReason]int)
 | |
| 	for _, entry := range sequence {
 | |
| 		reasonCounts[entry.ChangeReason]++
 | |
| 	}
 | |
| 	
 | |
| 	// Find dominant reasons
 | |
| 	dominantReasons := make([]ChangeReason, 0)
 | |
| 	maxCount := 0
 | |
| 	for reason, count := range reasonCounts {
 | |
| 		if count > maxCount {
 | |
| 			maxCount = count
 | |
| 			dominantReasons = []ChangeReason{reason}
 | |
| 		} else if count == maxCount {
 | |
| 			dominantReasons = append(dominantReasons, reason)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Count decision makers
 | |
| 	makerCounts := make(map[string]int)
 | |
| 	for _, entry := range sequence {
 | |
| 		makerCounts[entry.DecisionMaker]++
 | |
| 	}
 | |
| 	
 | |
| 	// Count impact scope distribution
 | |
| 	scopeCounts := make(map[ImpactScope]int)
 | |
| 	for _, entry := range sequence {
 | |
| 		scopeCounts[entry.ImpactScope]++
 | |
| 	}
 | |
| 	
 | |
| 	return &TimelineAnalysis{
 | |
| 		ChangeVelocity:          changeVelocity,
 | |
| 		ConfidenceTrend:         confidenceTrend,
 | |
| 		DominantChangeReasons:   dominantReasons,
 | |
| 		DecisionMakers:          makerCounts,
 | |
| 		ImpactScopeDistribution: scopeCounts,
 | |
| 		InfluenceNetworkSize:    len(related),
 | |
| 		AnalyzedAt:              time.Now(),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) getStalenessReasons(node *TemporalNode) []string {
 | |
| 	reasons := make([]string, 0)
 | |
| 	
 | |
| 	// Time-based staleness
 | |
| 	timeSinceUpdate := time.Since(node.Timestamp)
 | |
| 	if timeSinceUpdate > 7*24*time.Hour {
 | |
| 		reasons = append(reasons, "not updated in over a week")
 | |
| 	}
 | |
| 	
 | |
| 	// Influence-based staleness
 | |
| 	if len(node.InfluencedBy) > 0 {
 | |
| 		reasons = append(reasons, "influenced by other contexts that may have changed")
 | |
| 	}
 | |
| 	
 | |
| 	// Confidence-based staleness
 | |
| 	if node.Confidence < 0.7 {
 | |
| 		reasons = append(reasons, "low confidence score")
 | |
| 	}
 | |
| 	
 | |
| 	return reasons
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) getSuggestedActions(node *TemporalNode) []string {
 | |
| 	actions := make([]string, 0)
 | |
| 	
 | |
| 	actions = append(actions, "review context for accuracy")
 | |
| 	actions = append(actions, "check related decisions for impact")
 | |
| 	
 | |
| 	if node.Confidence < 0.7 {
 | |
| 		actions = append(actions, "improve context confidence through additional analysis")
 | |
| 	}
 | |
| 	
 | |
| 	if len(node.InfluencedBy) > 3 {
 | |
| 		actions = append(actions, "validate dependencies are still accurate")
 | |
| 	}
 | |
| 	
 | |
| 	return actions
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) getRelatedChanges(node *TemporalNode) []ucxl.Address {
 | |
| 	// Find contexts that have changed recently and might affect this one
 | |
| 	relatedChanges := make([]ucxl.Address, 0)
 | |
| 	
 | |
| 	cutoff := time.Now().Add(-24 * time.Hour)
 | |
| 	for _, otherNode := range dn.graph.nodes {
 | |
| 		if otherNode.Timestamp.After(cutoff) && otherNode.ID != node.ID {
 | |
| 			// Check if this node influences the stale node
 | |
| 			for _, influenced := range otherNode.Influences {
 | |
| 				if influenced.String() == node.UCXLAddress.String() {
 | |
| 					relatedChanges = append(relatedChanges, otherNode.UCXLAddress)
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return relatedChanges
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) calculateStalePriority(node *TemporalNode) StalePriority {
 | |
| 	score := node.Staleness
 | |
| 	
 | |
| 	// Adjust based on influence
 | |
| 	if len(node.Influences) > 5 {
 | |
| 		score += 0.2 // Higher priority if it influences many others
 | |
| 	}
 | |
| 	
 | |
| 	// Adjust based on impact scope
 | |
| 	switch node.ImpactScope {
 | |
| 	case ImpactSystem:
 | |
| 		score += 0.3
 | |
| 	case ImpactProject:
 | |
| 		score += 0.2
 | |
| 	case ImpactModule:
 | |
| 		score += 0.1
 | |
| 	}
 | |
| 	
 | |
| 	if score >= 0.9 {
 | |
| 		return PriorityCritical
 | |
| 	} else if score >= 0.7 {
 | |
| 		return PriorityHigh
 | |
| 	} else if score >= 0.5 {
 | |
| 		return PriorityMedium
 | |
| 	}
 | |
| 	return PriorityLow
 | |
| }
 | |
| 
 | |
| func (dn *decisionNavigatorImpl) validateStepRelationship(step, nextStep *DecisionStep) bool {
 | |
| 	// Check if there's a valid relationship between the steps
 | |
| 	currentNodeID := step.TemporalNode.ID
 | |
| 	nextNodeID := nextStep.TemporalNode.ID
 | |
| 	
 | |
| 	switch step.Relationship {
 | |
| 	case "influences":
 | |
| 		if influences, exists := dn.graph.influences[currentNodeID]; exists {
 | |
| 			for _, influenced := range influences {
 | |
| 				if influenced == nextNodeID {
 | |
| 					return true
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	case "influenced_by":
 | |
| 		if influencedBy, exists := dn.graph.influencedBy[currentNodeID]; exists {
 | |
| 			for _, influencer := range influencedBy {
 | |
| 				if influencer == nextNodeID {
 | |
| 					return true
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return false
 | |
| } |