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 _, 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 }