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 }