/** * PlantLineageAgent * Monitors and manages plant lineage tracking in the blockchain * * Responsibilities: * - Validate new plant registrations * - Track generation lineage and ancestry * - Detect and alert on anomalies in plant data * - Generate lineage reports and family trees * - Monitor plant status transitions */ import { BaseAgent } from './BaseAgent'; import { AgentConfig, AgentTask } from './types'; import { getBlockchain } from '../blockchain/manager'; import { PlantData, PlantBlock, PropagationType } from '../blockchain/types'; interface LineageAnalysis { plantId: string; generation: number; ancestors: string[]; descendants: string[]; totalLineageSize: number; propagationChain: PropagationType[]; geographicSpread: number; // km oldestAncestorDate: string; healthScore: number; } interface LineageAnomaly { type: 'orphan' | 'circular' | 'invalid_generation' | 'missing_parent' | 'suspicious_location'; plantId: string; description: string; severity: 'low' | 'medium' | 'high'; } export class PlantLineageAgent extends BaseAgent { private lineageCache: Map = new Map(); private anomalyLog: LineageAnomaly[] = []; private lastFullScanAt: string | null = null; constructor() { const config: AgentConfig = { id: 'plant-lineage-agent', name: 'Plant Lineage Agent', description: 'Monitors plant lineage integrity and generates ancestry reports', enabled: true, intervalMs: 60000, // Run every minute priority: 'high', maxRetries: 3, timeoutMs: 30000 }; super(config); } /** * Main execution cycle */ async runOnce(): Promise { const blockchain = getBlockchain(); const chain = blockchain.chain; // Skip genesis block const plantBlocks = chain.slice(1); let processedCount = 0; let anomaliesFound = 0; for (const block of plantBlocks) { const plant = block.plant; // Analyze lineage if not cached or stale if (!this.lineageCache.has(plant.id)) { const analysis = this.analyzeLineage(plant.id, chain); this.lineageCache.set(plant.id, analysis); } // Check for anomalies const anomalies = this.detectAnomalies(block, chain); for (const anomaly of anomalies) { this.anomalyLog.push(anomaly); anomaliesFound++; if (anomaly.severity === 'high') { this.createAlert('warning', `Lineage Anomaly: ${anomaly.type}`, anomaly.description, { relatedEntityId: anomaly.plantId, relatedEntityType: 'plant' } ); } } processedCount++; } // Monitor for plants needing attention this.monitorPlantHealth(chain); // Update scan timestamp this.lastFullScanAt = new Date().toISOString(); // Keep anomaly log manageable if (this.anomalyLog.length > 1000) { this.anomalyLog = this.anomalyLog.slice(-500); } return this.createTaskResult('lineage_scan', 'completed', { plantsScanned: processedCount, anomaliesFound, cacheSize: this.lineageCache.size, timestamp: this.lastFullScanAt }); } /** * Analyze complete lineage for a plant */ private analyzeLineage(plantId: string, chain: PlantBlock[]): LineageAnalysis { const plant = chain.find(b => b.plant.id === plantId)?.plant; if (!plant) { return this.createEmptyLineageAnalysis(plantId); } const ancestors = this.findAncestors(plantId, chain); const descendants = this.findDescendants(plantId, chain); const propagationChain = this.buildPropagationChain(plantId, chain); const geographicSpread = this.calculateGeographicSpread(plantId, chain); const oldestAncestor = this.findOldestAncestor(plantId, chain); return { plantId, generation: plant.generation, ancestors, descendants, totalLineageSize: ancestors.length + descendants.length + 1, propagationChain, geographicSpread, oldestAncestorDate: oldestAncestor?.timestamp || plant.registeredAt, healthScore: this.calculateHealthScore(plant, chain) }; } /** * Find all ancestors recursively */ private findAncestors(plantId: string, chain: PlantBlock[], visited: Set = new Set()): string[] { if (visited.has(plantId)) return []; visited.add(plantId); const plant = chain.find(b => b.plant.id === plantId)?.plant; if (!plant || !plant.parentPlantId) return []; const ancestors = [plant.parentPlantId]; const parentAncestors = this.findAncestors(plant.parentPlantId, chain, visited); return [...ancestors, ...parentAncestors]; } /** * Find all descendants recursively */ private findDescendants(plantId: string, chain: PlantBlock[], visited: Set = new Set()): string[] { if (visited.has(plantId)) return []; visited.add(plantId); const plant = chain.find(b => b.plant.id === plantId)?.plant; if (!plant) return []; const descendants: string[] = [...(plant.childPlants || [])]; for (const childId of plant.childPlants || []) { const childDescendants = this.findDescendants(childId, chain, visited); descendants.push(...childDescendants); } return descendants; } /** * Build propagation type chain from origin to plant */ private buildPropagationChain(plantId: string, chain: PlantBlock[]): PropagationType[] { const result: PropagationType[] = []; let currentId: string | undefined = plantId; while (currentId) { const plant = chain.find(b => b.plant.id === currentId)?.plant; if (!plant) break; result.unshift(plant.propagationType); currentId = plant.parentPlantId; } return result; } /** * Calculate geographic spread of lineage in km */ private calculateGeographicSpread(plantId: string, chain: PlantBlock[]): number { const lineagePlantIds = [ plantId, ...this.findAncestors(plantId, chain), ...this.findDescendants(plantId, chain) ]; const locations = lineagePlantIds .map(id => chain.find(b => b.plant.id === id)?.plant.location) .filter(loc => loc !== undefined); if (locations.length < 2) return 0; let maxDistance = 0; for (let i = 0; i < locations.length; i++) { for (let j = i + 1; j < locations.length; j++) { const distance = this.calculateHaversine( locations[i]!.latitude, locations[i]!.longitude, locations[j]!.latitude, locations[j]!.longitude ); maxDistance = Math.max(maxDistance, distance); } } return Math.round(maxDistance * 100) / 100; } /** * Find oldest ancestor */ private findOldestAncestor(plantId: string, chain: PlantBlock[]): PlantBlock | null { const ancestors = this.findAncestors(plantId, chain); if (ancestors.length === 0) return null; let oldest: PlantBlock | null = null; let oldestDate = new Date(); for (const ancestorId of ancestors) { const block = chain.find(b => b.plant.id === ancestorId); if (block) { const date = new Date(block.timestamp); if (date < oldestDate) { oldestDate = date; oldest = block; } } } return oldest; } /** * Calculate health score for a plant (0-100) */ private calculateHealthScore(plant: PlantData, chain: PlantBlock[]): number { let score = 100; // Deduct for status issues if (plant.status === 'deceased') score -= 50; if (plant.status === 'dormant') score -= 10; // Deduct for missing data if (!plant.environment) score -= 10; if (!plant.growthMetrics) score -= 10; // Deduct for high generation (genetic drift risk) if (plant.generation > 10) score -= Math.min(20, plant.generation - 10); // Bonus for having offspring (successful propagation) if (plant.childPlants && plant.childPlants.length > 0) { score += Math.min(10, plant.childPlants.length * 2); } return Math.max(0, Math.min(100, score)); } /** * Detect anomalies in plant data */ private detectAnomalies(block: PlantBlock, chain: PlantBlock[]): LineageAnomaly[] { const anomalies: LineageAnomaly[] = []; const plant = block.plant; // Check for orphan plants (non-original with missing parent) if (plant.propagationType !== 'original' && plant.parentPlantId) { const parent = chain.find(b => b.plant.id === plant.parentPlantId); if (!parent) { anomalies.push({ type: 'missing_parent', plantId: plant.id, description: `Plant ${plant.id} references parent ${plant.parentPlantId} which doesn't exist`, severity: 'medium' }); } } // Check for invalid generation numbers if (plant.parentPlantId) { const parent = chain.find(b => b.plant.id === plant.parentPlantId); if (parent && plant.generation !== parent.plant.generation + 1) { anomalies.push({ type: 'invalid_generation', plantId: plant.id, description: `Generation ${plant.generation} doesn't match parent generation ${parent.plant.generation}`, severity: 'medium' }); } } // Check for suspicious location jumps if (plant.parentPlantId) { const parent = chain.find(b => b.plant.id === plant.parentPlantId); if (parent) { const distance = this.calculateHaversine( plant.location.latitude, plant.location.longitude, parent.plant.location.latitude, parent.plant.location.longitude ); // Flag if offspring is more than 1000km from parent if (distance > 1000) { anomalies.push({ type: 'suspicious_location', plantId: plant.id, description: `Plant is ${Math.round(distance)}km from parent - verify transport records`, severity: 'low' }); } } } // Check for circular references const ancestors = this.findAncestors(plant.id, chain); if (ancestors.includes(plant.id)) { anomalies.push({ type: 'circular', plantId: plant.id, description: 'Circular lineage reference detected', severity: 'high' }); } return anomalies; } /** * Monitor overall plant health across network */ private monitorPlantHealth(chain: PlantBlock[]): void { const statusCounts: Record = {}; let deceasedCount = 0; let recentDeaths = 0; const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; for (const block of chain.slice(1)) { const status = block.plant.status; statusCounts[status] = (statusCounts[status] || 0) + 1; if (status === 'deceased') { deceasedCount++; if (new Date(block.timestamp).getTime() > oneWeekAgo) { recentDeaths++; } } } // Alert if death rate is high const totalPlants = chain.length - 1; if (totalPlants > 10 && recentDeaths / totalPlants > 0.1) { this.createAlert('warning', 'High Plant Mortality Rate', `${recentDeaths} plants marked as deceased in the last week (${Math.round(recentDeaths / totalPlants * 100)}% of network)`, { actionRequired: 'Investigate potential environmental or disease issues' } ); } } /** * Calculate Haversine distance */ private calculateHaversine(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371; // km const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * Create empty lineage analysis */ private createEmptyLineageAnalysis(plantId: string): LineageAnalysis { return { plantId, generation: 0, ancestors: [], descendants: [], totalLineageSize: 1, propagationChain: [], geographicSpread: 0, oldestAncestorDate: new Date().toISOString(), healthScore: 0 }; } /** * Get lineage analysis for a specific plant */ getLineageAnalysis(plantId: string): LineageAnalysis | null { return this.lineageCache.get(plantId) || null; } /** * Get all detected anomalies */ getAnomalies(): LineageAnomaly[] { return this.anomalyLog; } /** * Get network statistics */ getNetworkStats(): { totalPlants: number; totalLineages: number; avgGenerationDepth: number; avgLineageSize: number; geographicSpread: number; } { const analyses = Array.from(this.lineageCache.values()); const totalPlants = analyses.length; if (totalPlants === 0) { return { totalPlants: 0, totalLineages: 0, avgGenerationDepth: 0, avgLineageSize: 0, geographicSpread: 0 }; } const rootPlants = analyses.filter(a => a.ancestors.length === 0); const avgGen = analyses.reduce((sum, a) => sum + a.generation, 0) / totalPlants; const avgSize = analyses.reduce((sum, a) => sum + a.totalLineageSize, 0) / totalPlants; const maxSpread = Math.max(...analyses.map(a => a.geographicSpread)); return { totalPlants, totalLineages: rootPlants.length, avgGenerationDepth: Math.round(avgGen * 10) / 10, avgLineageSize: Math.round(avgSize * 10) / 10, geographicSpread: maxSpread }; } } // Singleton instance let lineageAgentInstance: PlantLineageAgent | null = null; export function getPlantLineageAgent(): PlantLineageAgent { if (!lineageAgentInstance) { lineageAgentInstance = new PlantLineageAgent(); } return lineageAgentInstance; }