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:
anthonyrawlins
2025-09-02 20:02:37 +10:00
parent 7c6cbd562a
commit 543ab216f9
224 changed files with 86331 additions and 186 deletions

View 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
}