localgreenchain/lib/agents/PlantLineageAgent.ts
Claude 507df5912f
Deploy GrowerAdvisoryAgent (Agent 10) and fix type errors
- Add GrowerAdvisoryAgent test file
- Fix PlantChain constructor initialization order (plantIndex before genesis block)
- Fix blockchain.getChain() calls to use blockchain.chain property
- Add PropagationType export to blockchain types
- Fix SoilComposition.type property references (was soilType)
- Fix ClimateConditions.temperatureDay property references (was avgTemperature)
- Fix ClimateConditions.humidityAverage property references (was avgHumidity)
- Fix LightingConditions.naturalLight.hoursPerDay nested access
- Add 'critical' severity to QualityReport issues
- Add 'sqm' unit to PlantingRecommendation.quantityUnit
- Fix GrowerAdvisoryAgent growthMetrics property access
- Update TypeScript to v5 for react-hook-form compatibility
- Enable downlevelIteration in tsconfig for Map iteration
- Fix crypto Buffer type issues in anonymity.ts
- Fix zones.tsx status type comparison
- Fix next.config.js images.domains filter
- Rename [[...slug]].tsx to [...slug].tsx to resolve routing conflict
2025-11-23 00:44:58 +00:00

462 lines
14 KiB
TypeScript

/**
* 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<string, LineageAnalysis> = 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<AgentTask | null> {
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<string> = 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<string> = 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<string, number> = {};
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;
}