Complete BZZZ functionality port to CHORUS
🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
926
pkg/slurp/temporal/graph_impl.go
Normal file
926
pkg/slurp/temporal/graph_impl.go
Normal file
@@ -0,0 +1,926 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user