This comprehensive refactoring addresses critical architectural issues: IMPORT CYCLE RESOLUTION: • pkg/crypto ↔ pkg/slurp/roles: Created pkg/security/access_levels.go • pkg/ucxl → pkg/dht: Created pkg/storage/interfaces.go • pkg/slurp/leader → pkg/election → pkg/slurp/storage: Moved types to pkg/election/interfaces.go MODULE PATH MIGRATION: • Changed from github.com/anthonyrawlins/bzzz to chorus.services/bzzz • Updated all import statements across 115+ files • Maintains compatibility while removing personal GitHub account dependency TYPE SYSTEM IMPROVEMENTS: • Resolved duplicate type declarations in crypto package • Added missing type definitions (RoleStatus, TimeRestrictions, KeyStatus, KeyRotationResult) • Proper interface segregation to prevent future cycles ARCHITECTURAL BENEFITS: • Build now progresses past structural issues to normal dependency resolution • Cleaner separation of concerns between packages • Eliminates circular dependencies that prevented compilation • Establishes foundation for scalable codebase growth 🤖 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.services/bzzz/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
|
|
} |