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>
926 lines
28 KiB
Go
926 lines
28 KiB
Go
package temporal
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/pkg/ucxl"
|
|
slurpContext "chorus.services/bzzz/pkg/slurp/context"
|
|
"chorus.services/bzzz/pkg/slurp/storage"
|
|
)
|
|
|
|
// temporalGraphImpl implements the TemporalGraph interface
|
|
type temporalGraphImpl struct {
|
|
mu sync.RWMutex
|
|
|
|
// Core storage
|
|
storage storage.ContextStore
|
|
|
|
// In-memory graph structures for fast access
|
|
nodes map[string]*TemporalNode // nodeID -> TemporalNode
|
|
addressToNodes map[string][]*TemporalNode // address -> list of temporal nodes
|
|
influences map[string][]string // nodeID -> list of influenced nodeIDs
|
|
influencedBy map[string][]string // nodeID -> list of influencer nodeIDs
|
|
|
|
// Decision tracking
|
|
decisions map[string]*DecisionMetadata // decisionID -> DecisionMetadata
|
|
decisionToNodes map[string][]*TemporalNode // decisionID -> list of affected nodes
|
|
|
|
// Performance optimization
|
|
pathCache map[string][]*DecisionStep // cache for decision paths
|
|
metricsCache map[string]interface{} // cache for expensive metrics
|
|
cacheTimeout time.Duration
|
|
lastCacheClean time.Time
|
|
|
|
// Configuration
|
|
maxDepth int // Maximum depth for path finding
|
|
stalenessWeight *StalenessWeights
|
|
}
|
|
|
|
// NewTemporalGraph creates a new temporal graph implementation
|
|
func NewTemporalGraph(storage storage.ContextStore) TemporalGraph {
|
|
return &temporalGraphImpl{
|
|
storage: storage,
|
|
nodes: make(map[string]*TemporalNode),
|
|
addressToNodes: make(map[string][]*TemporalNode),
|
|
influences: make(map[string][]string),
|
|
influencedBy: make(map[string][]string),
|
|
decisions: make(map[string]*DecisionMetadata),
|
|
decisionToNodes: make(map[string][]*TemporalNode),
|
|
pathCache: make(map[string][]*DecisionStep),
|
|
metricsCache: make(map[string]interface{}),
|
|
cacheTimeout: time.Minute * 15,
|
|
lastCacheClean: time.Now(),
|
|
maxDepth: 100, // Default maximum depth
|
|
stalenessWeight: &StalenessWeights{
|
|
TimeWeight: 0.3,
|
|
InfluenceWeight: 0.4,
|
|
ActivityWeight: 0.2,
|
|
ImportanceWeight: 0.1,
|
|
ComplexityWeight: 0.1,
|
|
DependencyWeight: 0.3,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateInitialContext creates the first temporal version of context
|
|
func (tg *temporalGraphImpl) CreateInitialContext(ctx context.Context, address ucxl.Address,
|
|
contextData *slurpContext.ContextNode, creator string) (*TemporalNode, error) {
|
|
|
|
tg.mu.Lock()
|
|
defer tg.mu.Unlock()
|
|
|
|
// Generate node ID
|
|
nodeID := tg.generateNodeID(address, 1)
|
|
|
|
// Create temporal node
|
|
temporalNode := &TemporalNode{
|
|
ID: nodeID,
|
|
UCXLAddress: address,
|
|
Version: 1,
|
|
Context: contextData,
|
|
Timestamp: time.Now(),
|
|
DecisionID: fmt.Sprintf("initial-%s", creator),
|
|
ChangeReason: ReasonInitialCreation,
|
|
ParentNode: nil,
|
|
ContextHash: tg.calculateContextHash(contextData),
|
|
Confidence: contextData.RAGConfidence,
|
|
Staleness: 0.0,
|
|
Influences: make([]ucxl.Address, 0),
|
|
InfluencedBy: make([]ucxl.Address, 0),
|
|
ValidatedBy: []string{creator},
|
|
LastValidated: time.Now(),
|
|
ImpactScope: ImpactLocal,
|
|
PropagatedTo: make([]ucxl.Address, 0),
|
|
Metadata: make(map[string]interface{}),
|
|
}
|
|
|
|
// Store in memory structures
|
|
tg.nodes[nodeID] = temporalNode
|
|
addressKey := address.String()
|
|
tg.addressToNodes[addressKey] = []*TemporalNode{temporalNode}
|
|
|
|
// Initialize influence maps
|
|
tg.influences[nodeID] = make([]string, 0)
|
|
tg.influencedBy[nodeID] = make([]string, 0)
|
|
|
|
// Store decision metadata
|
|
decisionMeta := &DecisionMetadata{
|
|
ID: temporalNode.DecisionID,
|
|
Maker: creator,
|
|
Rationale: "Initial context creation",
|
|
Scope: ImpactLocal,
|
|
ConfidenceLevel: contextData.RAGConfidence,
|
|
ExternalRefs: make([]string, 0),
|
|
CreatedAt: time.Now(),
|
|
ImplementationStatus: "complete",
|
|
Metadata: make(map[string]interface{}),
|
|
}
|
|
tg.decisions[temporalNode.DecisionID] = decisionMeta
|
|
tg.decisionToNodes[temporalNode.DecisionID] = []*TemporalNode{temporalNode}
|
|
|
|
// Persist to storage
|
|
if err := tg.persistTemporalNode(ctx, temporalNode); err != nil {
|
|
return nil, fmt.Errorf("failed to persist initial temporal node: %w", err)
|
|
}
|
|
|
|
return temporalNode, nil
|
|
}
|
|
|
|
// EvolveContext creates a new temporal version due to a decision
|
|
func (tg *temporalGraphImpl) EvolveContext(ctx context.Context, address ucxl.Address,
|
|
newContext *slurpContext.ContextNode, reason ChangeReason,
|
|
decision *DecisionMetadata) (*TemporalNode, error) {
|
|
|
|
tg.mu.Lock()
|
|
defer tg.mu.Unlock()
|
|
|
|
// Get latest version
|
|
addressKey := address.String()
|
|
nodes, exists := tg.addressToNodes[addressKey]
|
|
if !exists || len(nodes) == 0 {
|
|
return nil, fmt.Errorf("no existing context found for address %s", address.String())
|
|
}
|
|
|
|
// Find latest version
|
|
latestNode := nodes[len(nodes)-1]
|
|
newVersion := latestNode.Version + 1
|
|
|
|
// Generate new node ID
|
|
nodeID := tg.generateNodeID(address, newVersion)
|
|
|
|
// Create new temporal node
|
|
temporalNode := &TemporalNode{
|
|
ID: nodeID,
|
|
UCXLAddress: address,
|
|
Version: newVersion,
|
|
Context: newContext,
|
|
Timestamp: time.Now(),
|
|
DecisionID: decision.ID,
|
|
ChangeReason: reason,
|
|
ParentNode: &latestNode.ID,
|
|
ContextHash: tg.calculateContextHash(newContext),
|
|
Confidence: newContext.RAGConfidence,
|
|
Staleness: 0.0, // New version, not stale
|
|
Influences: make([]ucxl.Address, 0),
|
|
InfluencedBy: make([]ucxl.Address, 0),
|
|
ValidatedBy: []string{decision.Maker},
|
|
LastValidated: time.Now(),
|
|
ImpactScope: decision.Scope,
|
|
PropagatedTo: make([]ucxl.Address, 0),
|
|
Metadata: make(map[string]interface{}),
|
|
}
|
|
|
|
// Copy influence relationships from parent
|
|
if latestNodeInfluences, exists := tg.influences[latestNode.ID]; exists {
|
|
tg.influences[nodeID] = make([]string, len(latestNodeInfluences))
|
|
copy(tg.influences[nodeID], latestNodeInfluences)
|
|
} else {
|
|
tg.influences[nodeID] = make([]string, 0)
|
|
}
|
|
|
|
if latestNodeInfluencedBy, exists := tg.influencedBy[latestNode.ID]; exists {
|
|
tg.influencedBy[nodeID] = make([]string, len(latestNodeInfluencedBy))
|
|
copy(tg.influencedBy[nodeID], latestNodeInfluencedBy)
|
|
} else {
|
|
tg.influencedBy[nodeID] = make([]string, 0)
|
|
}
|
|
|
|
// Store in memory structures
|
|
tg.nodes[nodeID] = temporalNode
|
|
tg.addressToNodes[addressKey] = append(tg.addressToNodes[addressKey], temporalNode)
|
|
|
|
// Store decision metadata
|
|
tg.decisions[decision.ID] = decision
|
|
if existing, exists := tg.decisionToNodes[decision.ID]; exists {
|
|
tg.decisionToNodes[decision.ID] = append(existing, temporalNode)
|
|
} else {
|
|
tg.decisionToNodes[decision.ID] = []*TemporalNode{temporalNode}
|
|
}
|
|
|
|
// Update staleness for related contexts
|
|
tg.updateStalenessAfterChange(temporalNode)
|
|
|
|
// Clear relevant caches
|
|
tg.clearCacheForAddress(address)
|
|
|
|
// Persist to storage
|
|
if err := tg.persistTemporalNode(ctx, temporalNode); err != nil {
|
|
return nil, fmt.Errorf("failed to persist evolved temporal node: %w", err)
|
|
}
|
|
|
|
return temporalNode, nil
|
|
}
|
|
|
|
// GetLatestVersion gets the most recent temporal node for an address
|
|
func (tg *temporalGraphImpl) GetLatestVersion(ctx context.Context, address ucxl.Address) (*TemporalNode, error) {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
addressKey := address.String()
|
|
nodes, exists := tg.addressToNodes[addressKey]
|
|
if !exists || len(nodes) == 0 {
|
|
return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
|
|
}
|
|
|
|
// Return the latest version (last in slice)
|
|
return nodes[len(nodes)-1], nil
|
|
}
|
|
|
|
// GetVersionAtDecision gets context as it was at a specific decision hop
|
|
func (tg *temporalGraphImpl) GetVersionAtDecision(ctx context.Context, address ucxl.Address,
|
|
decisionHop int) (*TemporalNode, error) {
|
|
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
addressKey := address.String()
|
|
nodes, exists := tg.addressToNodes[addressKey]
|
|
if !exists || len(nodes) == 0 {
|
|
return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
|
|
}
|
|
|
|
// Find node at specific decision hop (version)
|
|
for _, node := range nodes {
|
|
if node.Version == decisionHop {
|
|
return node, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no temporal node found at decision hop %d for address %s",
|
|
decisionHop, address.String())
|
|
}
|
|
|
|
// GetEvolutionHistory gets complete evolution history ordered by decisions
|
|
func (tg *temporalGraphImpl) GetEvolutionHistory(ctx context.Context, address ucxl.Address) ([]*TemporalNode, error) {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
addressKey := address.String()
|
|
nodes, exists := tg.addressToNodes[addressKey]
|
|
if !exists || len(nodes) == 0 {
|
|
return []*TemporalNode{}, nil
|
|
}
|
|
|
|
// Sort by version to ensure proper order
|
|
sortedNodes := make([]*TemporalNode, len(nodes))
|
|
copy(sortedNodes, nodes)
|
|
sort.Slice(sortedNodes, func(i, j int) bool {
|
|
return sortedNodes[i].Version < sortedNodes[j].Version
|
|
})
|
|
|
|
return sortedNodes, nil
|
|
}
|
|
|
|
// AddInfluenceRelationship establishes that decisions in one context affect another
|
|
func (tg *temporalGraphImpl) AddInfluenceRelationship(ctx context.Context, influencer, influenced ucxl.Address) error {
|
|
tg.mu.Lock()
|
|
defer tg.mu.Unlock()
|
|
|
|
// Get latest nodes for both addresses
|
|
influencerNode, err := tg.getLatestNodeUnsafe(influencer)
|
|
if err != nil {
|
|
return fmt.Errorf("influencer node not found: %w", err)
|
|
}
|
|
|
|
influencedNode, err := tg.getLatestNodeUnsafe(influenced)
|
|
if err != nil {
|
|
return fmt.Errorf("influenced node not found: %w", err)
|
|
}
|
|
|
|
// Add to influence mappings
|
|
influencerNodeID := influencerNode.ID
|
|
influencedNodeID := influencedNode.ID
|
|
|
|
// Add to influences map (influencer -> influenced)
|
|
if influences, exists := tg.influences[influencerNodeID]; exists {
|
|
// Check if relationship already exists
|
|
for _, existingID := range influences {
|
|
if existingID == influencedNodeID {
|
|
return nil // Relationship already exists
|
|
}
|
|
}
|
|
tg.influences[influencerNodeID] = append(influences, influencedNodeID)
|
|
} else {
|
|
tg.influences[influencerNodeID] = []string{influencedNodeID}
|
|
}
|
|
|
|
// Add to influencedBy map (influenced <- influencer)
|
|
if influencedBy, exists := tg.influencedBy[influencedNodeID]; exists {
|
|
// Check if relationship already exists
|
|
for _, existingID := range influencedBy {
|
|
if existingID == influencerNodeID {
|
|
return nil // Relationship already exists
|
|
}
|
|
}
|
|
tg.influencedBy[influencedNodeID] = append(influencedBy, influencerNodeID)
|
|
} else {
|
|
tg.influencedBy[influencedNodeID] = []string{influencerNodeID}
|
|
}
|
|
|
|
// Update temporal nodes with the influence relationship
|
|
influencerNode.Influences = append(influencerNode.Influences, influenced)
|
|
influencedNode.InfluencedBy = append(influencedNode.InfluencedBy, influencer)
|
|
|
|
// Clear path cache as influence graph has changed
|
|
tg.pathCache = make(map[string][]*DecisionStep)
|
|
|
|
// Persist changes
|
|
if err := tg.persistTemporalNode(ctx, influencerNode); err != nil {
|
|
return fmt.Errorf("failed to persist influencer node: %w", err)
|
|
}
|
|
if err := tg.persistTemporalNode(ctx, influencedNode); err != nil {
|
|
return fmt.Errorf("failed to persist influenced node: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveInfluenceRelationship removes an influence relationship
|
|
func (tg *temporalGraphImpl) RemoveInfluenceRelationship(ctx context.Context, influencer, influenced ucxl.Address) error {
|
|
tg.mu.Lock()
|
|
defer tg.mu.Unlock()
|
|
|
|
// Get latest nodes for both addresses
|
|
influencerNode, err := tg.getLatestNodeUnsafe(influencer)
|
|
if err != nil {
|
|
return fmt.Errorf("influencer node not found: %w", err)
|
|
}
|
|
|
|
influencedNode, err := tg.getLatestNodeUnsafe(influenced)
|
|
if err != nil {
|
|
return fmt.Errorf("influenced node not found: %w", err)
|
|
}
|
|
|
|
// Remove from influence mappings
|
|
influencerNodeID := influencerNode.ID
|
|
influencedNodeID := influencedNode.ID
|
|
|
|
// Remove from influences map
|
|
if influences, exists := tg.influences[influencerNodeID]; exists {
|
|
tg.influences[influencerNodeID] = tg.removeFromSlice(influences, influencedNodeID)
|
|
}
|
|
|
|
// Remove from influencedBy map
|
|
if influencedBy, exists := tg.influencedBy[influencedNodeID]; exists {
|
|
tg.influencedBy[influencedNodeID] = tg.removeFromSlice(influencedBy, influencerNodeID)
|
|
}
|
|
|
|
// Update temporal nodes
|
|
influencerNode.Influences = tg.removeAddressFromSlice(influencerNode.Influences, influenced)
|
|
influencedNode.InfluencedBy = tg.removeAddressFromSlice(influencedNode.InfluencedBy, influencer)
|
|
|
|
// Clear path cache
|
|
tg.pathCache = make(map[string][]*DecisionStep)
|
|
|
|
// Persist changes
|
|
if err := tg.persistTemporalNode(ctx, influencerNode); err != nil {
|
|
return fmt.Errorf("failed to persist influencer node: %w", err)
|
|
}
|
|
if err := tg.persistTemporalNode(ctx, influencedNode); err != nil {
|
|
return fmt.Errorf("failed to persist influenced node: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetInfluenceRelationships gets all influence relationships for a context
|
|
func (tg *temporalGraphImpl) GetInfluenceRelationships(ctx context.Context, address ucxl.Address) ([]ucxl.Address, []ucxl.Address, error) {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
node, err := tg.getLatestNodeUnsafe(address)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("node not found: %w", err)
|
|
}
|
|
|
|
influences := make([]ucxl.Address, len(node.Influences))
|
|
copy(influences, node.Influences)
|
|
|
|
influencedBy := make([]ucxl.Address, len(node.InfluencedBy))
|
|
copy(influencedBy, node.InfluencedBy)
|
|
|
|
return influences, influencedBy, nil
|
|
}
|
|
|
|
// FindRelatedDecisions finds decisions within N decision hops
|
|
func (tg *temporalGraphImpl) FindRelatedDecisions(ctx context.Context, address ucxl.Address,
|
|
maxHops int) ([]*DecisionPath, error) {
|
|
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
// Check cache first
|
|
cacheKey := fmt.Sprintf("related-%s-%d", address.String(), maxHops)
|
|
if cached, exists := tg.pathCache[cacheKey]; exists {
|
|
paths := make([]*DecisionPath, len(cached))
|
|
for i, step := range cached {
|
|
paths[i] = &DecisionPath{
|
|
From: address,
|
|
To: step.Address,
|
|
Steps: []*DecisionStep{step},
|
|
TotalHops: step.HopDistance,
|
|
PathType: "direct",
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
startNode, err := tg.getLatestNodeUnsafe(address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("start node not found: %w", err)
|
|
}
|
|
|
|
// Use BFS to find all nodes within maxHops
|
|
visited := make(map[string]bool)
|
|
queue := []*bfsItem{{node: startNode, distance: 0, path: []*DecisionStep{}}}
|
|
relatedPaths := make([]*DecisionPath, 0)
|
|
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
|
|
nodeID := current.node.ID
|
|
if visited[nodeID] || current.distance > maxHops {
|
|
continue
|
|
}
|
|
visited[nodeID] = true
|
|
|
|
// If this is not the starting node, add it to results
|
|
if current.distance > 0 {
|
|
step := &DecisionStep{
|
|
Address: current.node.UCXLAddress,
|
|
TemporalNode: current.node,
|
|
HopDistance: current.distance,
|
|
Relationship: "influence",
|
|
}
|
|
|
|
path := &DecisionPath{
|
|
From: address,
|
|
To: current.node.UCXLAddress,
|
|
Steps: append(current.path, step),
|
|
TotalHops: current.distance,
|
|
PathType: "influence",
|
|
}
|
|
relatedPaths = append(relatedPaths, path)
|
|
}
|
|
|
|
// Add influenced nodes to queue
|
|
if influences, exists := tg.influences[nodeID]; exists {
|
|
for _, influencedID := range influences {
|
|
if !visited[influencedID] && current.distance < maxHops {
|
|
if influencedNode, exists := tg.nodes[influencedID]; exists {
|
|
newStep := &DecisionStep{
|
|
Address: current.node.UCXLAddress,
|
|
TemporalNode: current.node,
|
|
HopDistance: current.distance,
|
|
Relationship: "influences",
|
|
}
|
|
newPath := append(current.path, newStep)
|
|
queue = append(queue, &bfsItem{
|
|
node: influencedNode,
|
|
distance: current.distance + 1,
|
|
path: newPath,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add influencer nodes to queue
|
|
if influencedBy, exists := tg.influencedBy[nodeID]; exists {
|
|
for _, influencerID := range influencedBy {
|
|
if !visited[influencerID] && current.distance < maxHops {
|
|
if influencerNode, exists := tg.nodes[influencerID]; exists {
|
|
newStep := &DecisionStep{
|
|
Address: current.node.UCXLAddress,
|
|
TemporalNode: current.node,
|
|
HopDistance: current.distance,
|
|
Relationship: "influenced_by",
|
|
}
|
|
newPath := append(current.path, newStep)
|
|
queue = append(queue, &bfsItem{
|
|
node: influencerNode,
|
|
distance: current.distance + 1,
|
|
path: newPath,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return relatedPaths, nil
|
|
}
|
|
|
|
// FindDecisionPath finds shortest decision path between two addresses
|
|
func (tg *temporalGraphImpl) FindDecisionPath(ctx context.Context, from, to ucxl.Address) ([]*DecisionStep, error) {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
// Check cache first
|
|
cacheKey := fmt.Sprintf("path-%s-%s", from.String(), to.String())
|
|
if cached, exists := tg.pathCache[cacheKey]; exists {
|
|
return cached, nil
|
|
}
|
|
|
|
fromNode, err := tg.getLatestNodeUnsafe(from)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("from node not found: %w", err)
|
|
}
|
|
|
|
toNode, err := tg.getLatestNodeUnsafe(to)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("to node not found: %w", err)
|
|
}
|
|
|
|
// Use BFS to find shortest path
|
|
visited := make(map[string]bool)
|
|
queue := []*pathItem{{node: fromNode, path: []*DecisionStep{}}}
|
|
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
|
|
nodeID := current.node.ID
|
|
if visited[nodeID] {
|
|
continue
|
|
}
|
|
visited[nodeID] = true
|
|
|
|
// Check if we reached the target
|
|
if current.node.UCXLAddress.String() == to.String() {
|
|
// Cache the result
|
|
tg.pathCache[cacheKey] = current.path
|
|
return current.path, nil
|
|
}
|
|
|
|
// Explore influenced nodes
|
|
if influences, exists := tg.influences[nodeID]; exists {
|
|
for _, influencedID := range influences {
|
|
if !visited[influencedID] {
|
|
if influencedNode, exists := tg.nodes[influencedID]; exists {
|
|
step := &DecisionStep{
|
|
Address: current.node.UCXLAddress,
|
|
TemporalNode: current.node,
|
|
HopDistance: len(current.path),
|
|
Relationship: "influences",
|
|
}
|
|
newPath := append(current.path, step)
|
|
queue = append(queue, &pathItem{
|
|
node: influencedNode,
|
|
path: newPath,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Explore influencer nodes
|
|
if influencedBy, exists := tg.influencedBy[nodeID]; exists {
|
|
for _, influencerID := range influencedBy {
|
|
if !visited[influencerID] {
|
|
if influencerNode, exists := tg.nodes[influencerID]; exists {
|
|
step := &DecisionStep{
|
|
Address: current.node.UCXLAddress,
|
|
TemporalNode: current.node,
|
|
HopDistance: len(current.path),
|
|
Relationship: "influenced_by",
|
|
}
|
|
newPath := append(current.path, step)
|
|
queue = append(queue, &pathItem{
|
|
node: influencerNode,
|
|
path: newPath,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no path found from %s to %s", from.String(), to.String())
|
|
}
|
|
|
|
// AnalyzeDecisionPatterns analyzes decision-making patterns over time
|
|
func (tg *temporalGraphImpl) AnalyzeDecisionPatterns(ctx context.Context) (*DecisionAnalysis, error) {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
analysis := &DecisionAnalysis{
|
|
TimeRange: 24 * time.Hour, // Analyze last 24 hours by default
|
|
TotalDecisions: len(tg.decisions),
|
|
DecisionVelocity: 0,
|
|
InfluenceNetworkSize: len(tg.nodes),
|
|
AverageInfluenceDistance: 0,
|
|
MostInfluentialDecisions: make([]*InfluentialDecision, 0),
|
|
DecisionClusters: make([]*DecisionCluster, 0),
|
|
Patterns: make([]*DecisionPattern, 0),
|
|
Anomalies: make([]*AnomalousDecision, 0),
|
|
AnalyzedAt: time.Now(),
|
|
}
|
|
|
|
// Calculate decision velocity
|
|
cutoff := time.Now().Add(-analysis.TimeRange)
|
|
recentDecisions := 0
|
|
for _, decision := range tg.decisions {
|
|
if decision.CreatedAt.After(cutoff) {
|
|
recentDecisions++
|
|
}
|
|
}
|
|
analysis.DecisionVelocity = float64(recentDecisions) / analysis.TimeRange.Hours()
|
|
|
|
// Calculate average influence distance
|
|
totalDistance := 0.0
|
|
connections := 0
|
|
for nodeID := range tg.influences {
|
|
if influences, exists := tg.influences[nodeID]; exists {
|
|
for range influences {
|
|
totalDistance += 1.0 // Each connection is 1 hop
|
|
connections++
|
|
}
|
|
}
|
|
}
|
|
if connections > 0 {
|
|
analysis.AverageInfluenceDistance = totalDistance / float64(connections)
|
|
}
|
|
|
|
// Find most influential decisions (simplified)
|
|
influenceScores := make(map[string]float64)
|
|
for nodeID, node := range tg.nodes {
|
|
score := float64(len(tg.influences[nodeID])) * 1.0 // Direct influences
|
|
score += float64(len(tg.influencedBy[nodeID])) * 0.5 // Being influenced
|
|
influenceScores[nodeID] = score
|
|
|
|
if score > 3.0 { // Threshold for "influential"
|
|
influential := &InfluentialDecision{
|
|
Address: node.UCXLAddress,
|
|
DecisionHop: node.Version,
|
|
InfluenceScore: score,
|
|
AffectedContexts: node.Influences,
|
|
DecisionMetadata: tg.decisions[node.DecisionID],
|
|
InfluenceReasons: []string{"high_connectivity", "multiple_influences"},
|
|
}
|
|
analysis.MostInfluentialDecisions = append(analysis.MostInfluentialDecisions, influential)
|
|
}
|
|
}
|
|
|
|
// Sort influential decisions by score
|
|
sort.Slice(analysis.MostInfluentialDecisions, func(i, j int) bool {
|
|
return analysis.MostInfluentialDecisions[i].InfluenceScore > analysis.MostInfluentialDecisions[j].InfluenceScore
|
|
})
|
|
|
|
// Limit to top 10
|
|
if len(analysis.MostInfluentialDecisions) > 10 {
|
|
analysis.MostInfluentialDecisions = analysis.MostInfluentialDecisions[:10]
|
|
}
|
|
|
|
return analysis, nil
|
|
}
|
|
|
|
// ValidateTemporalIntegrity validates temporal graph integrity
|
|
func (tg *temporalGraphImpl) ValidateTemporalIntegrity(ctx context.Context) error {
|
|
tg.mu.RLock()
|
|
defer tg.mu.RUnlock()
|
|
|
|
errors := make([]string, 0)
|
|
|
|
// Check for orphaned nodes
|
|
for nodeID, node := range tg.nodes {
|
|
if node.ParentNode != nil {
|
|
if _, exists := tg.nodes[*node.ParentNode]; !exists {
|
|
errors = append(errors, fmt.Sprintf("orphaned node %s has non-existent parent %s",
|
|
nodeID, *node.ParentNode))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check influence consistency
|
|
for nodeID := range tg.influences {
|
|
if influences, exists := tg.influences[nodeID]; exists {
|
|
for _, influencedID := range influences {
|
|
// Check if the influenced node has this node in its influencedBy list
|
|
if influencedByList, exists := tg.influencedBy[influencedID]; exists {
|
|
found := false
|
|
for _, influencerID := range influencedByList {
|
|
if influencerID == nodeID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
errors = append(errors, fmt.Sprintf("influence inconsistency: %s -> %s not reflected in influencedBy",
|
|
nodeID, influencedID))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check version sequence integrity
|
|
for address, nodes := range tg.addressToNodes {
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
return nodes[i].Version < nodes[j].Version
|
|
})
|
|
|
|
for i, node := range nodes {
|
|
expectedVersion := i + 1
|
|
if node.Version != expectedVersion {
|
|
errors = append(errors, fmt.Sprintf("version sequence error for address %s: expected %d, got %d",
|
|
address, expectedVersion, node.Version))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("temporal integrity violations: %v", errors)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompactHistory compacts old temporal data to save space
|
|
func (tg *temporalGraphImpl) CompactHistory(ctx context.Context, beforeTime time.Time) error {
|
|
tg.mu.Lock()
|
|
defer tg.mu.Unlock()
|
|
|
|
compacted := 0
|
|
|
|
// For each address, keep only the latest version and major milestones before the cutoff
|
|
for address, nodes := range tg.addressToNodes {
|
|
toKeep := make([]*TemporalNode, 0)
|
|
toRemove := make([]*TemporalNode, 0)
|
|
|
|
for _, node := range nodes {
|
|
// Always keep nodes after the cutoff time
|
|
if node.Timestamp.After(beforeTime) {
|
|
toKeep = append(toKeep, node)
|
|
continue
|
|
}
|
|
|
|
// Keep major changes and influential decisions
|
|
if tg.isMajorChange(node) || tg.isInfluentialDecision(node) {
|
|
toKeep = append(toKeep, node)
|
|
} else {
|
|
toRemove = append(toRemove, node)
|
|
}
|
|
}
|
|
|
|
// Update the address mapping
|
|
tg.addressToNodes[address] = toKeep
|
|
|
|
// Remove old nodes from main maps
|
|
for _, node := range toRemove {
|
|
delete(tg.nodes, node.ID)
|
|
delete(tg.influences, node.ID)
|
|
delete(tg.influencedBy, node.ID)
|
|
compacted++
|
|
}
|
|
}
|
|
|
|
// Clear caches after compaction
|
|
tg.pathCache = make(map[string][]*DecisionStep)
|
|
tg.metricsCache = make(map[string]interface{})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (tg *temporalGraphImpl) generateNodeID(address ucxl.Address, version int) string {
|
|
return fmt.Sprintf("%s-v%d", address.String(), version)
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) calculateContextHash(context *slurpContext.ContextNode) string {
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(fmt.Sprintf("%+v", context)))
|
|
return fmt.Sprintf("%x", hasher.Sum(nil))[:16]
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) getLatestNodeUnsafe(address ucxl.Address) (*TemporalNode, error) {
|
|
addressKey := address.String()
|
|
nodes, exists := tg.addressToNodes[addressKey]
|
|
if !exists || len(nodes) == 0 {
|
|
return nil, fmt.Errorf("no temporal nodes found for address %s", address.String())
|
|
}
|
|
return nodes[len(nodes)-1], nil
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) removeFromSlice(slice []string, item string) []string {
|
|
result := make([]string, 0, len(slice))
|
|
for _, s := range slice {
|
|
if s != item {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) removeAddressFromSlice(slice []ucxl.Address, item ucxl.Address) []ucxl.Address {
|
|
result := make([]ucxl.Address, 0, len(slice))
|
|
for _, addr := range slice {
|
|
if addr.String() != item.String() {
|
|
result = append(result, addr)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) updateStalenessAfterChange(changedNode *TemporalNode) {
|
|
// Update staleness for all influenced contexts
|
|
if influences, exists := tg.influences[changedNode.ID]; exists {
|
|
for _, influencedID := range influences {
|
|
if influencedNode, exists := tg.nodes[influencedID]; exists {
|
|
// Calculate new staleness based on the change
|
|
staleness := tg.calculateStaleness(influencedNode, changedNode)
|
|
influencedNode.Staleness = math.Max(influencedNode.Staleness, staleness)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) calculateStaleness(node *TemporalNode, changedNode *TemporalNode) float64 {
|
|
// Simple staleness calculation based on time since last update and influence strength
|
|
timeSinceUpdate := time.Since(node.Timestamp)
|
|
timeWeight := math.Min(timeSinceUpdate.Hours()/168.0, 1.0) // Max staleness from time: 1 week
|
|
|
|
// Influence weight based on connection strength
|
|
influenceWeight := 0.0
|
|
if len(node.InfluencedBy) > 0 {
|
|
influenceWeight = 1.0 / float64(len(node.InfluencedBy)) // Stronger if fewer influencers
|
|
}
|
|
|
|
// Impact scope weight
|
|
impactWeight := 0.0
|
|
switch changedNode.ImpactScope {
|
|
case ImpactSystem:
|
|
impactWeight = 1.0
|
|
case ImpactProject:
|
|
impactWeight = 0.8
|
|
case ImpactModule:
|
|
impactWeight = 0.6
|
|
case ImpactLocal:
|
|
impactWeight = 0.4
|
|
}
|
|
|
|
return math.Min(
|
|
tg.stalenessWeight.TimeWeight*timeWeight+
|
|
tg.stalenessWeight.InfluenceWeight*influenceWeight+
|
|
tg.stalenessWeight.ImportanceWeight*impactWeight, 1.0)
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) clearCacheForAddress(address ucxl.Address) {
|
|
addressStr := address.String()
|
|
keysToDelete := make([]string, 0)
|
|
|
|
for key := range tg.pathCache {
|
|
if contains(key, addressStr) {
|
|
keysToDelete = append(keysToDelete, key)
|
|
}
|
|
}
|
|
|
|
for _, key := range keysToDelete {
|
|
delete(tg.pathCache, key)
|
|
}
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) isMajorChange(node *TemporalNode) bool {
|
|
return node.ChangeReason == ReasonArchitectureChange ||
|
|
node.ChangeReason == ReasonDesignDecision ||
|
|
node.ChangeReason == ReasonRequirementsChange
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) isInfluentialDecision(node *TemporalNode) bool {
|
|
influences := len(tg.influences[node.ID])
|
|
influencedBy := len(tg.influencedBy[node.ID])
|
|
return influences >= 3 || influencedBy >= 3 // Arbitrary threshold for "influential"
|
|
}
|
|
|
|
func (tg *temporalGraphImpl) persistTemporalNode(ctx context.Context, node *TemporalNode) error {
|
|
// Convert to storage format and persist
|
|
// This would integrate with the storage system
|
|
// For now, we'll assume persistence happens in memory
|
|
return nil
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr ||
|
|
(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr)))
|
|
}
|
|
|
|
// Supporting types for BFS traversal
|
|
|
|
type bfsItem struct {
|
|
node *TemporalNode
|
|
distance int
|
|
path []*DecisionStep
|
|
}
|
|
|
|
type pathItem struct {
|
|
node *TemporalNode
|
|
path []*DecisionStep
|
|
} |