Merge: Grower Advisory Agent with tests and type fixes - resolved conflicts
This commit is contained in:
commit
0fcecca424
17 changed files with 1154 additions and 197 deletions
215
__tests__/lib/agents/GrowerAdvisoryAgent.test.ts
Normal file
215
__tests__/lib/agents/GrowerAdvisoryAgent.test.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* GrowerAdvisoryAgent Tests
|
||||||
|
* Tests for the grower advisory and recommendation system
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrowerAdvisoryAgent,
|
||||||
|
getGrowerAdvisoryAgent,
|
||||||
|
} from '../../../lib/agents/GrowerAdvisoryAgent';
|
||||||
|
|
||||||
|
describe('GrowerAdvisoryAgent', () => {
|
||||||
|
let agent: GrowerAdvisoryAgent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
agent = new GrowerAdvisoryAgent();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should create agent with correct configuration', () => {
|
||||||
|
expect(agent.config.id).toBe('grower-advisory-agent');
|
||||||
|
expect(agent.config.name).toBe('Grower Advisory Agent');
|
||||||
|
expect(agent.config.enabled).toBe(true);
|
||||||
|
expect(agent.config.priority).toBe('high');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct interval (5 minutes)', () => {
|
||||||
|
expect(agent.config.intervalMs).toBe(300000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start in idle status', () => {
|
||||||
|
expect(agent.status).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have empty metrics initially', () => {
|
||||||
|
const metrics = agent.getMetrics();
|
||||||
|
expect(metrics.tasksCompleted).toBe(0);
|
||||||
|
expect(metrics.tasksFailed).toBe(0);
|
||||||
|
expect(metrics.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Grower Profile Management', () => {
|
||||||
|
it('should register a grower profile', () => {
|
||||||
|
const profile = createGrowerProfile('grower-1');
|
||||||
|
agent.registerGrowerProfile(profile);
|
||||||
|
|
||||||
|
const retrieved = agent.getGrowerProfile('grower-1');
|
||||||
|
expect(retrieved).not.toBeNull();
|
||||||
|
expect(retrieved?.growerId).toBe('grower-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for unknown grower', () => {
|
||||||
|
const retrieved = agent.getGrowerProfile('unknown-grower');
|
||||||
|
expect(retrieved).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing profile', () => {
|
||||||
|
const profile1 = createGrowerProfile('grower-1');
|
||||||
|
profile1.experienceLevel = 'beginner';
|
||||||
|
agent.registerGrowerProfile(profile1);
|
||||||
|
|
||||||
|
const profile2 = createGrowerProfile('grower-1');
|
||||||
|
profile2.experienceLevel = 'expert';
|
||||||
|
agent.registerGrowerProfile(profile2);
|
||||||
|
|
||||||
|
const retrieved = agent.getGrowerProfile('grower-1');
|
||||||
|
expect(retrieved?.experienceLevel).toBe('expert');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Recommendations', () => {
|
||||||
|
it('should return empty recommendations for unknown grower', () => {
|
||||||
|
const recs = agent.getRecommendations('unknown-grower');
|
||||||
|
expect(recs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get recommendations after profile registration', () => {
|
||||||
|
const profile = createGrowerProfile('grower-1');
|
||||||
|
agent.registerGrowerProfile(profile);
|
||||||
|
|
||||||
|
// Recommendations are generated during runOnce
|
||||||
|
const recs = agent.getRecommendations('grower-1');
|
||||||
|
expect(Array.isArray(recs)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rotation Advice', () => {
|
||||||
|
it('should return null for unknown grower', () => {
|
||||||
|
const advice = agent.getRotationAdvice('unknown-grower');
|
||||||
|
expect(advice).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Market Opportunities', () => {
|
||||||
|
it('should return array of opportunities', () => {
|
||||||
|
const opps = agent.getOpportunities();
|
||||||
|
expect(Array.isArray(opps)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Grower Performance', () => {
|
||||||
|
it('should return null for unknown grower', () => {
|
||||||
|
const perf = agent.getPerformance('unknown-grower');
|
||||||
|
expect(perf).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Seasonal Alerts', () => {
|
||||||
|
it('should return array of seasonal alerts', () => {
|
||||||
|
const alerts = agent.getSeasonalAlerts();
|
||||||
|
expect(Array.isArray(alerts)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Agent Lifecycle', () => {
|
||||||
|
it('should start and change status to running', async () => {
|
||||||
|
await agent.start();
|
||||||
|
expect(agent.status).toBe('running');
|
||||||
|
await agent.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop and change status to idle', async () => {
|
||||||
|
await agent.start();
|
||||||
|
await agent.stop();
|
||||||
|
expect(agent.status).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause when running', async () => {
|
||||||
|
await agent.start();
|
||||||
|
agent.pause();
|
||||||
|
expect(agent.status).toBe('paused');
|
||||||
|
await agent.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resume after pause', async () => {
|
||||||
|
await agent.start();
|
||||||
|
agent.pause();
|
||||||
|
agent.resume();
|
||||||
|
expect(agent.status).toBe('running');
|
||||||
|
await agent.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Singleton', () => {
|
||||||
|
it('should return same instance from getGrowerAdvisoryAgent', () => {
|
||||||
|
const agent1 = getGrowerAdvisoryAgent();
|
||||||
|
const agent2 = getGrowerAdvisoryAgent();
|
||||||
|
expect(agent1).toBe(agent2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Alerts', () => {
|
||||||
|
it('should return alerts array', () => {
|
||||||
|
const alerts = agent.getAlerts();
|
||||||
|
expect(Array.isArray(alerts)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Task Execution', () => {
|
||||||
|
it('should execute runOnce successfully', async () => {
|
||||||
|
const profile = createGrowerProfile('grower-1');
|
||||||
|
agent.registerGrowerProfile(profile);
|
||||||
|
|
||||||
|
const result = await agent.runOnce();
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.status).toBe('completed');
|
||||||
|
expect(result?.type).toBe('grower_advisory');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report metrics in task result', async () => {
|
||||||
|
const profile = createGrowerProfile('grower-1');
|
||||||
|
agent.registerGrowerProfile(profile);
|
||||||
|
|
||||||
|
const result = await agent.runOnce();
|
||||||
|
|
||||||
|
expect(result?.result).toHaveProperty('growersAdvised');
|
||||||
|
expect(result?.result).toHaveProperty('recommendationsGenerated');
|
||||||
|
expect(result?.result).toHaveProperty('opportunitiesIdentified');
|
||||||
|
expect(result?.result).toHaveProperty('alertsGenerated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should count registered growers', async () => {
|
||||||
|
agent.registerGrowerProfile(createGrowerProfile('grower-1'));
|
||||||
|
agent.registerGrowerProfile(createGrowerProfile('grower-2'));
|
||||||
|
agent.registerGrowerProfile(createGrowerProfile('grower-3'));
|
||||||
|
|
||||||
|
const result = await agent.runOnce();
|
||||||
|
|
||||||
|
expect(result?.result?.growersAdvised).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to create test grower profiles
|
||||||
|
function createGrowerProfile(
|
||||||
|
growerId: string,
|
||||||
|
lat: number = 40.7128,
|
||||||
|
lon: number = -74.006
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
growerId,
|
||||||
|
growerName: `Test Grower ${growerId}`,
|
||||||
|
location: { latitude: lat, longitude: lon },
|
||||||
|
availableSpaceSqm: 100,
|
||||||
|
specializations: ['lettuce', 'tomato'],
|
||||||
|
certifications: ['organic'],
|
||||||
|
experienceLevel: 'intermediate' as const,
|
||||||
|
preferredCrops: ['lettuce', 'tomato', 'basil'],
|
||||||
|
growingHistory: [
|
||||||
|
{ cropType: 'lettuce', successRate: 85, avgYield: 4.5 },
|
||||||
|
{ cropType: 'tomato', successRate: 75, avgYield: 8.0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ export default function EnvironmentalForm({
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
[section]: {
|
[section]: {
|
||||||
...currentSection,
|
...(currentSection as object || {}),
|
||||||
...updates,
|
...updates,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
async runOnce(): Promise<AgentTask | null> {
|
async runOnce(): Promise<AgentTask | null> {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const chain = blockchain.getChain();
|
const chain = blockchain.chain;
|
||||||
const plants = chain.slice(1); // Skip genesis
|
const plants = chain.slice(1); // Skip genesis
|
||||||
|
|
||||||
let profilesUpdated = 0;
|
let profilesUpdated = 0;
|
||||||
|
|
@ -265,9 +265,9 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
for (const block of healthyPlants) {
|
for (const block of healthyPlants) {
|
||||||
const env = block.plant.environment;
|
const env = block.plant.environment;
|
||||||
if (env?.soil?.pH) pHValues.push(env.soil.pH);
|
if (env?.soil?.pH) pHValues.push(env.soil.pH);
|
||||||
if (env?.climate?.avgTemperature) tempValues.push(env.climate.avgTemperature);
|
if (env?.climate?.temperatureDay) tempValues.push(env.climate.temperatureDay);
|
||||||
if (env?.climate?.avgHumidity) humidityValues.push(env.climate.avgHumidity);
|
if (env?.climate?.humidityAverage) humidityValues.push(env.climate.humidityAverage);
|
||||||
if (env?.lighting?.hoursPerDay) lightValues.push(env.lighting.hoursPerDay);
|
if (env?.lighting?.naturalLight?.hoursPerDay) lightValues.push(env.lighting.naturalLight.hoursPerDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile: EnvironmentProfile = existing || {
|
const profile: EnvironmentProfile = existing || {
|
||||||
|
|
@ -357,15 +357,16 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
|
|
||||||
// Lighting analysis
|
// Lighting analysis
|
||||||
if (env.lighting) {
|
if (env.lighting) {
|
||||||
const lightDiff = env.lighting.hoursPerDay
|
const lightHours = env.lighting.naturalLight?.hoursPerDay || env.lighting.artificialLight?.hoursPerDay;
|
||||||
? Math.abs(env.lighting.hoursPerDay - profile.optimalConditions.lightHours.optimal)
|
const lightDiff = lightHours
|
||||||
|
? Math.abs(lightHours - profile.optimalConditions.lightHours.optimal)
|
||||||
: 2;
|
: 2;
|
||||||
lightingScore = Math.max(0, 100 - lightDiff * 15);
|
lightingScore = Math.max(0, 100 - lightDiff * 15);
|
||||||
|
|
||||||
if (lightDiff > 2) {
|
if (lightDiff > 2) {
|
||||||
improvements.push({
|
improvements.push({
|
||||||
category: 'lighting',
|
category: 'lighting',
|
||||||
currentState: `${env.lighting.hoursPerDay || 'unknown'} hours/day`,
|
currentState: `${lightHours || 'unknown'} hours/day`,
|
||||||
recommendedState: `${profile.optimalConditions.lightHours.optimal} hours/day`,
|
recommendedState: `${profile.optimalConditions.lightHours.optimal} hours/day`,
|
||||||
priority: lightDiff > 4 ? 'high' : 'medium',
|
priority: lightDiff > 4 ? 'high' : 'medium',
|
||||||
expectedImpact: 'Better photosynthesis and growth',
|
expectedImpact: 'Better photosynthesis and growth',
|
||||||
|
|
@ -376,11 +377,11 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
|
|
||||||
// Climate analysis
|
// Climate analysis
|
||||||
if (env.climate) {
|
if (env.climate) {
|
||||||
const tempDiff = env.climate.avgTemperature
|
const tempDiff = env.climate.temperatureDay
|
||||||
? Math.abs(env.climate.avgTemperature - profile.optimalConditions.temperature.optimal)
|
? Math.abs(env.climate.temperatureDay - profile.optimalConditions.temperature.optimal)
|
||||||
: 5;
|
: 5;
|
||||||
const humDiff = env.climate.avgHumidity
|
const humDiff = env.climate.humidityAverage
|
||||||
? Math.abs(env.climate.avgHumidity - profile.optimalConditions.humidity.optimal)
|
? Math.abs(env.climate.humidityAverage - profile.optimalConditions.humidity.optimal)
|
||||||
: 10;
|
: 10;
|
||||||
|
|
||||||
climateScore = Math.max(0, 100 - tempDiff * 5 - humDiff * 1);
|
climateScore = Math.max(0, 100 - tempDiff * 5 - humDiff * 1);
|
||||||
|
|
@ -388,7 +389,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
if (tempDiff > 3) {
|
if (tempDiff > 3) {
|
||||||
improvements.push({
|
improvements.push({
|
||||||
category: 'climate',
|
category: 'climate',
|
||||||
currentState: `${env.climate.avgTemperature?.toFixed(1) || 'unknown'}°C`,
|
currentState: `${env.climate.temperatureDay?.toFixed(1) || 'unknown'}°C`,
|
||||||
recommendedState: `${profile.optimalConditions.temperature.optimal}°C`,
|
recommendedState: `${profile.optimalConditions.temperature.optimal}°C`,
|
||||||
priority: tempDiff > 6 ? 'high' : 'medium',
|
priority: tempDiff > 6 ? 'high' : 'medium',
|
||||||
expectedImpact: 'Reduced stress and improved growth',
|
expectedImpact: 'Reduced stress and improved growth',
|
||||||
|
|
@ -408,7 +409,8 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
// Nutrients analysis
|
// Nutrients analysis
|
||||||
if (env.nutrients) {
|
if (env.nutrients) {
|
||||||
nutrientsScore = 75; // Base score if nutrient data exists
|
nutrientsScore = 75; // Base score if nutrient data exists
|
||||||
if (env.nutrients.fertilizer?.schedule === 'regular') {
|
// Bonus for complete NPK profile
|
||||||
|
if (env.nutrients.nitrogen && env.nutrients.phosphorus && env.nutrients.potassium) {
|
||||||
nutrientsScore = 90;
|
nutrientsScore = 90;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -462,7 +464,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
|
|
||||||
// Find common soil types
|
// Find common soil types
|
||||||
const soilTypes = plantsWithEnv
|
const soilTypes = plantsWithEnv
|
||||||
.map(p => p.plant.environment?.soil?.soilType)
|
.map(p => p.plant.environment?.soil?.type)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const commonSoilType = this.findMostCommon(soilTypes as string[]);
|
const commonSoilType = this.findMostCommon(soilTypes as string[]);
|
||||||
|
|
@ -471,7 +473,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
patterns.push({
|
patterns.push({
|
||||||
patternId: `pattern-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
patternId: `pattern-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||||
species,
|
species,
|
||||||
conditions: { soil: { soilType: commonSoilType } } as any,
|
conditions: { soil: { type: commonSoilType } } as any,
|
||||||
successMetric: 'health',
|
successMetric: 'health',
|
||||||
successValue: 85,
|
successValue: 85,
|
||||||
sampleSize: plantsWithEnv.length,
|
sampleSize: plantsWithEnv.length,
|
||||||
|
|
@ -527,7 +529,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const chain = blockchain.getChain();
|
const chain = blockchain.chain;
|
||||||
|
|
||||||
const block1 = chain.find(b => b.plant.id === plant1Id);
|
const block1 = chain.find(b => b.plant.id === plant1Id);
|
||||||
const block2 = chain.find(b => b.plant.id === plant2Id);
|
const block2 = chain.find(b => b.plant.id === plant2Id);
|
||||||
|
|
@ -545,14 +547,14 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
// Compare soil
|
// Compare soil
|
||||||
if (env1?.soil && env2?.soil) {
|
if (env1?.soil && env2?.soil) {
|
||||||
totalFactors++;
|
totalFactors++;
|
||||||
if (env1.soil.soilType === env2.soil.soilType) {
|
if (env1.soil.type === env2.soil.type) {
|
||||||
matchingFactors.push('Soil type');
|
matchingFactors.push('Soil type');
|
||||||
matchScore++;
|
matchScore++;
|
||||||
} else {
|
} else {
|
||||||
differingFactors.push({
|
differingFactors.push({
|
||||||
factor: 'Soil type',
|
factor: 'Soil type',
|
||||||
plant1Value: env1.soil.soilType,
|
plant1Value: env1.soil.type,
|
||||||
plant2Value: env2.soil.soilType
|
plant2Value: env2.soil.type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,7 +590,7 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
if (env1?.climate && env2?.climate) {
|
if (env1?.climate && env2?.climate) {
|
||||||
totalFactors++;
|
totalFactors++;
|
||||||
const tempDiff = Math.abs(
|
const tempDiff = Math.abs(
|
||||||
(env1.climate.avgTemperature || 0) - (env2.climate.avgTemperature || 0)
|
(env1.climate.temperatureDay || 0) - (env2.climate.temperatureDay || 0)
|
||||||
);
|
);
|
||||||
if (tempDiff < 3) {
|
if (tempDiff < 3) {
|
||||||
matchingFactors.push('Temperature');
|
matchingFactors.push('Temperature');
|
||||||
|
|
@ -596,8 +598,8 @@ export class EnvironmentAnalysisAgent extends BaseAgent {
|
||||||
} else {
|
} else {
|
||||||
differingFactors.push({
|
differingFactors.push({
|
||||||
factor: 'Temperature',
|
factor: 'Temperature',
|
||||||
plant1Value: env1.climate.avgTemperature,
|
plant1Value: env1.climate.temperatureDay,
|
||||||
plant2Value: env2.climate.avgTemperature
|
plant2Value: env2.climate.temperatureDay
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export class GrowerAdvisoryAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
private updateGrowerProfiles(): void {
|
private updateGrowerProfiles(): void {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const chain = blockchain.getChain().slice(1);
|
const chain = blockchain.chain.slice(1);
|
||||||
|
|
||||||
const ownerPlants = new Map<string, typeof chain>();
|
const ownerPlants = new Map<string, typeof chain>();
|
||||||
|
|
||||||
|
|
@ -219,7 +219,11 @@ export class GrowerAdvisoryAgent extends BaseAgent {
|
||||||
if (['growing', 'mature', 'flowering', 'fruiting'].includes(plant.plant.status)) {
|
if (['growing', 'mature', 'flowering', 'fruiting'].includes(plant.plant.status)) {
|
||||||
existing.healthy++;
|
existing.healthy++;
|
||||||
}
|
}
|
||||||
existing.yield += plant.plant.growthMetrics?.estimatedYieldKg || 2;
|
// Estimate yield based on health score, or use default of 2kg
|
||||||
|
const healthMultiplier = plant.plant.growthMetrics?.healthScore
|
||||||
|
? plant.plant.growthMetrics.healthScore / 50
|
||||||
|
: 1;
|
||||||
|
existing.yield += 2 * healthMultiplier;
|
||||||
historyMap.set(crop, existing);
|
historyMap.set(crop, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export class NetworkDiscoveryAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
async runOnce(): Promise<AgentTask | null> {
|
async runOnce(): Promise<AgentTask | null> {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const chain = blockchain.getChain();
|
const chain = blockchain.chain;
|
||||||
const plants = chain.slice(1);
|
const plants = chain.slice(1);
|
||||||
|
|
||||||
// Build network from plant data
|
// Build network from plant data
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class PlantLineageAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
async runOnce(): Promise<AgentTask | null> {
|
async runOnce(): Promise<AgentTask | null> {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const chain = blockchain.getChain();
|
const chain = blockchain.chain;
|
||||||
|
|
||||||
// Skip genesis block
|
// Skip genesis block
|
||||||
const plantBlocks = chain.slice(1);
|
const plantBlocks = chain.slice(1);
|
||||||
|
|
@ -133,7 +133,7 @@ export class PlantLineageAgent extends BaseAgent {
|
||||||
totalLineageSize: ancestors.length + descendants.length + 1,
|
totalLineageSize: ancestors.length + descendants.length + 1,
|
||||||
propagationChain,
|
propagationChain,
|
||||||
geographicSpread,
|
geographicSpread,
|
||||||
oldestAncestorDate: oldestAncestor?.timestamp || plant.dateAcquired,
|
oldestAncestorDate: oldestAncestor?.timestamp || plant.registeredAt,
|
||||||
healthScore: this.calculateHealthScore(plant, chain)
|
healthScore: this.calculateHealthScore(plant, chain)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ export class SustainabilityAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
private calculateWaterMetrics(): WaterMetrics {
|
private calculateWaterMetrics(): WaterMetrics {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const plantCount = blockchain.getChain().length - 1;
|
const plantCount = blockchain.chain.length - 1;
|
||||||
|
|
||||||
// Simulate water usage based on plant count
|
// Simulate water usage based on plant count
|
||||||
// Vertical farms use ~10% of traditional water
|
// Vertical farms use ~10% of traditional water
|
||||||
|
|
@ -265,7 +265,7 @@ export class SustainabilityAgent extends BaseAgent {
|
||||||
*/
|
*/
|
||||||
private calculateWasteMetrics(): WasteMetrics {
|
private calculateWasteMetrics(): WasteMetrics {
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const plants = blockchain.getChain().slice(1);
|
const plants = blockchain.chain.slice(1);
|
||||||
|
|
||||||
const deceasedPlants = plants.filter(p => p.plant.status === 'deceased').length;
|
const deceasedPlants = plants.filter(p => p.plant.status === 'deceased').length;
|
||||||
const totalPlants = plants.length;
|
const totalPlants = plants.length;
|
||||||
|
|
@ -311,7 +311,7 @@ export class SustainabilityAgent extends BaseAgent {
|
||||||
|
|
||||||
// Biodiversity: based on plant variety
|
// Biodiversity: based on plant variety
|
||||||
const blockchain = getBlockchain();
|
const blockchain = getBlockchain();
|
||||||
const plants = blockchain.getChain().slice(1);
|
const plants = blockchain.chain.slice(1);
|
||||||
const uniqueSpecies = new Set(plants.map(p => p.plant.commonName)).size;
|
const uniqueSpecies = new Set(plants.map(p => p.plant.commonName)).size;
|
||||||
const biodiversity = Math.min(100, 30 + uniqueSpecies * 5);
|
const biodiversity = Math.min(100, 30 + uniqueSpecies * 5);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ export class PlantChain {
|
||||||
private plantIndex: Map<string, PlantBlock>; // Quick lookup by plant ID
|
private plantIndex: Map<string, PlantBlock>; // Quick lookup by plant ID
|
||||||
|
|
||||||
constructor(difficulty: number = 4) {
|
constructor(difficulty: number = 4) {
|
||||||
this.chain = [this.createGenesisBlock()];
|
|
||||||
this.difficulty = difficulty;
|
this.difficulty = difficulty;
|
||||||
this.plantIndex = new Map();
|
this.plantIndex = new Map();
|
||||||
|
this.chain = [this.createGenesisBlock()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
import { GrowingEnvironment, GrowthMetrics } from '../environment/types';
|
import { GrowingEnvironment, GrowthMetrics } from '../environment/types';
|
||||||
|
|
||||||
|
// Re-export types from environment
|
||||||
|
export type { GrowingEnvironment, GrowthMetrics };
|
||||||
|
|
||||||
|
// Re-export PlantBlock class
|
||||||
|
export { PlantBlock } from './PlantBlock';
|
||||||
|
|
||||||
|
// Propagation type alias
|
||||||
|
export type PropagationType = 'seed' | 'clone' | 'cutting' | 'division' | 'grafting' | 'original';
|
||||||
|
|
||||||
export interface PlantLocation {
|
export interface PlantLocation {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ export interface PlantingRecommendation {
|
||||||
|
|
||||||
// Quantities
|
// Quantities
|
||||||
recommendedQuantity: number;
|
recommendedQuantity: number;
|
||||||
quantityUnit: 'plants' | 'seeds' | 'kg_expected_yield';
|
quantityUnit: 'plants' | 'seeds' | 'kg_expected_yield' | 'sqm';
|
||||||
expectedYieldKg: number;
|
expectedYieldKg: number;
|
||||||
yieldConfidence: number; // 0-100
|
yieldConfidence: number; // 0-100
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export interface FuzzyLocation {
|
||||||
*/
|
*/
|
||||||
export function generateAnonymousId(): string {
|
export function generateAnonymousId(): string {
|
||||||
const randomBytes = crypto.randomBytes(32);
|
const randomBytes = crypto.randomBytes(32);
|
||||||
return 'anon_' + crypto.createHash('sha256').update(randomBytes).digest('hex').substring(0, 16);
|
return 'anon_' + crypto.createHash('sha256').update(new Uint8Array(randomBytes)).digest('hex').substring(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,14 +111,14 @@ export function generateAnonymousPlantName(plantType: string, generation: number
|
||||||
*/
|
*/
|
||||||
export function encryptData(data: string, key: string): string {
|
export function encryptData(data: string, key: string): string {
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
const keyHash = crypto.createHash('sha256').update(key).digest();
|
const keyHash = new Uint8Array(crypto.createHash('sha256').update(key).digest());
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = new Uint8Array(crypto.randomBytes(16));
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, keyHash, iv);
|
const cipher = crypto.createCipheriv(algorithm, keyHash as any, iv as any);
|
||||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||||
encrypted += cipher.final('hex');
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
return iv.toString('hex') + ':' + encrypted;
|
return Buffer.from(iv).toString('hex') + ':' + encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -126,13 +126,13 @@ export function encryptData(data: string, key: string): string {
|
||||||
*/
|
*/
|
||||||
export function decryptData(encryptedData: string, key: string): string {
|
export function decryptData(encryptedData: string, key: string): string {
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
const keyHash = crypto.createHash('sha256').update(key).digest();
|
const keyHash = new Uint8Array(crypto.createHash('sha256').update(key).digest());
|
||||||
|
|
||||||
const parts = encryptedData.split(':');
|
const parts = encryptedData.split(':');
|
||||||
const iv = Buffer.from(parts[0], 'hex');
|
const iv = new Uint8Array(Buffer.from(parts[0], 'hex'));
|
||||||
const encrypted = parts[1];
|
const encrypted = parts[1];
|
||||||
|
|
||||||
const decipher = crypto.createDecipheriv(algorithm, keyHash, iv);
|
const decipher = crypto.createDecipheriv(algorithm, keyHash as any, iv as any);
|
||||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||||
decrypted += decipher.final('utf8');
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ module.exports = withPWA({
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: process.env.NEXT_IMAGE_DOMAIN ? [process.env.NEXT_IMAGE_DOMAIN] : [],
|
domains: [process.env.NEXT_IMAGE_DOMAIN].filter(Boolean),
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
1030
package-lock.json
generated
1030
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -85,8 +85,8 @@
|
||||||
"prisma": "^5.7.0",
|
"prisma": "^5.7.0",
|
||||||
"start-server-and-test": "^2.0.3",
|
"start-server-and-test": "^2.0.3",
|
||||||
"tailwindcss": "^3.0.15",
|
"tailwindcss": "^3.0.15",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx}": [
|
"*.{ts,tsx}": [
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export default function ZoneManagement() {
|
||||||
Start New Batch
|
Start New Batch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(selectedZone.status === 'growing' || selectedZone.status === 'ready') && (
|
{(selectedZone.status === 'planted' || selectedZone.status === 'harvesting') && (
|
||||||
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition">
|
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition">
|
||||||
Harvest
|
Harvest
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"downlevelIteration": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./*"],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue