diff --git a/__tests__/api/demand.test.ts b/__tests__/api/demand.test.ts new file mode 100644 index 0000000..7939c8c --- /dev/null +++ b/__tests__/api/demand.test.ts @@ -0,0 +1,319 @@ +/** + * Demand API Tests + * Tests for demand-related API endpoints + */ + +import { DemandForecaster, getDemandForecaster } from '../../lib/demand/forecaster'; +import { + ConsumerPreference, + SupplyCommitment, + DemandSignal, + PlantingRecommendation, +} from '../../lib/demand/types'; + +describe('Demand API', () => { + let forecaster: DemandForecaster; + + beforeEach(() => { + forecaster = new DemandForecaster(); + }); + + describe('POST /api/demand/preferences', () => { + it('should register consumer preferences', () => { + const preference = createConsumerPreference('api-consumer-001'); + + forecaster.registerPreference(preference); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(1); + expect(json.preferences[0][0]).toBe('api-consumer-001'); + }); + + it('should update existing preferences', () => { + const pref1 = createConsumerPreference('api-consumer-001'); + pref1.householdSize = 2; + forecaster.registerPreference(pref1); + + const pref2 = createConsumerPreference('api-consumer-001'); + pref2.householdSize = 5; + forecaster.registerPreference(pref2); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(1); + expect(json.preferences[0][1].householdSize).toBe(5); + }); + + it('should validate required fields', () => { + const preference = createConsumerPreference('api-consumer-001'); + + // All required fields present + forecaster.registerPreference(preference); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(1); + }); + }); + + describe('GET /api/demand/preferences', () => { + it('should return preferences for consumer', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + forecaster.registerPreference(createConsumerPreference('consumer-002')); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(2); + }); + }); + + describe('POST /api/demand/signal', () => { + it('should generate demand signal for region', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'Test Region', 'summer' + ); + + expect(signal.id).toBeDefined(); + expect(signal.region.name).toBe('Test Region'); + expect(signal.demandItems).toBeDefined(); + }); + + it('should include supply status', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'Test Region', 'summer' + ); + + expect(['surplus', 'balanced', 'shortage', 'critical']).toContain(signal.supplyStatus); + }); + + it('should calculate confidence level', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'Test Region', 'summer' + ); + + expect(signal.confidenceLevel).toBeGreaterThanOrEqual(0); + expect(signal.confidenceLevel).toBeLessThanOrEqual(100); + }); + }); + + describe('GET /api/demand/recommendations', () => { + it('should return planting recommendations', () => { + const pref = createConsumerPreference('consumer-001'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'spring' + ); + + expect(recommendations.length).toBeGreaterThanOrEqual(0); + }); + + it('should include risk assessment', () => { + const pref = createConsumerPreference('consumer-001'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + expect(['low', 'medium', 'high']).toContain(recommendations[0].overallRisk); + } + }); + }); + + describe('GET /api/demand/forecast', () => { + it('should return demand forecast', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test Region', 'summer'); + + const forecast = forecaster.generateForecast('Test Region', 12); + + expect(forecast.id).toBeDefined(); + expect(forecast.region).toBe('Test Region'); + expect(forecast.forecasts).toBeDefined(); + }); + + it('should include trend analysis', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test Region', 'summer'); + + const forecast = forecaster.generateForecast('Test Region', 12); + + forecast.forecasts.forEach(f => { + expect(['increasing', 'stable', 'decreasing']).toContain(f.trend); + }); + }); + }); + + describe('POST /api/demand/supply', () => { + it('should register supply commitment', () => { + const commitment = createSupplyCommitment('grower-001', 'lettuce', 50); + + forecaster.registerSupply(commitment); + + const json = forecaster.toJSON() as any; + expect(json.supplyCommitments.length).toBe(1); + }); + + it('should affect supply gap calculation', () => { + const pref = createConsumerPreference('consumer-001'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + // Generate signal without supply + const signal1 = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + const gap1 = signal1.demandItems.find(i => i.produceType === 'lettuce')?.gapKg || 0; + + // Add supply + forecaster.registerSupply(createSupplyCommitment('grower-001', 'lettuce', 5)); + + // Generate signal with supply + const signal2 = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + const gap2 = signal2.demandItems.find(i => i.produceType === 'lettuce')?.gapKg || 0; + + expect(gap2).toBeLessThan(gap1); + }); + }); + + describe('POST /api/demand/match', () => { + it('should create market match', () => { + // This tests the matching logic between supply and demand + const pref = createConsumerPreference('consumer-001'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.registerSupply(createSupplyCommitment('grower-001', 'lettuce', 10)); + + const signal = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + + expect(lettuceItem?.matchedSupply).toBeGreaterThan(0); + }); + }); + + describe('Error Handling', () => { + it('should handle empty region gracefully', () => { + // No consumers registered + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'Empty Region', 'summer' + ); + + expect(signal.totalConsumers).toBe(0); + expect(signal.demandItems.length).toBe(0); + }); + + it('should handle invalid coordinates gracefully', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + + // Very distant coordinates + const signal = forecaster.generateDemandSignal( + -90, 0, 1, 'Antarctica', 'winter' + ); + + // Should still return valid signal structure + expect(signal.id).toBeDefined(); + }); + }); + + describe('Response Format', () => { + it('should return consistent signal format', () => { + forecaster.registerPreference(createConsumerPreference('consumer-001')); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'Test', 'summer' + ); + + // Check all required fields + expect(signal.id).toBeDefined(); + expect(signal.timestamp).toBeDefined(); + expect(signal.region).toBeDefined(); + expect(signal.periodStart).toBeDefined(); + expect(signal.periodEnd).toBeDefined(); + expect(signal.seasonalPeriod).toBeDefined(); + expect(signal.demandItems).toBeDefined(); + expect(signal.totalConsumers).toBeDefined(); + expect(signal.totalWeeklyDemandKg).toBeDefined(); + expect(signal.confidenceLevel).toBeDefined(); + expect(signal.currentSupplyKg).toBeDefined(); + expect(signal.supplyGapKg).toBeDefined(); + expect(signal.supplyStatus).toBeDefined(); + }); + }); +}); + +// Helper functions +function createConsumerPreference(consumerId: string): ConsumerPreference { + return { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: 40.7128, + longitude: -74.006, + maxDeliveryRadiusKm: 25, + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens'], + preferredItems: [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'preferred', weeklyQuantity: 1, seasonalOnly: false }, + ], + certificationPreferences: ['organic'], + freshnessImportance: 4, + priceImportance: 3, + sustainabilityImportance: 4, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 2, + weeklyBudget: 100, + currency: 'USD', + }; +} + +function createSupplyCommitment( + growerId: string, + produceType: string, + quantity: number +): SupplyCommitment { + return { + id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + growerId, + timestamp: new Date().toISOString(), + produceType, + committedQuantityKg: quantity, + availableFrom: new Date().toISOString(), + availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + pricePerKg: 5, + currency: 'USD', + minimumOrderKg: 1, + certifications: ['organic'], + freshnessGuaranteeHours: 48, + deliveryRadiusKm: 50, + deliveryMethods: ['grower_delivery'], + status: 'available', + remainingKg: quantity, + }; +} diff --git a/__tests__/api/transport.test.ts b/__tests__/api/transport.test.ts new file mode 100644 index 0000000..f4e6608 --- /dev/null +++ b/__tests__/api/transport.test.ts @@ -0,0 +1,325 @@ +/** + * Transport API Tests + * Tests for transport-related API endpoints + * + * Note: These tests are designed to test API route handlers. + * They mock the underlying services and test request/response handling. + */ + +import { TransportChain, getTransportChain, setTransportChain } from '../../lib/transport/tracker'; +import { + SeedAcquisitionEvent, + PlantingEvent, + HarvestEvent, + TransportLocation, +} from '../../lib/transport/types'; + +describe('Transport API', () => { + let chain: TransportChain; + + beforeEach(() => { + chain = new TransportChain(1); + setTransportChain(chain); + }); + + describe('POST /api/transport/seed-acquisition', () => { + it('should record seed acquisition event', () => { + const event: SeedAcquisitionEvent = { + id: 'api-seed-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank'), + toLocation: createLocation('greenhouse'), + distanceKm: 25, + durationMinutes: 45, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-bank-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'batch-001', + sourceType: 'seed_bank', + species: 'Solanum lycopersicum', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + }; + + const block = chain.recordEvent(event); + + expect(block.transportEvent.id).toBe('api-seed-001'); + expect(block.transportEvent.eventType).toBe('seed_acquisition'); + }); + + it('should reject invalid seed acquisition data', () => { + const invalidEvent = { + id: 'invalid-001', + eventType: 'seed_acquisition', + // Missing required fields + }; + + expect(() => { + chain.recordEvent(invalidEvent as any); + }).toThrow(); + }); + }); + + describe('POST /api/transport/planting', () => { + it('should record planting event', () => { + const event: PlantingEvent = { + id: 'api-planting-001', + timestamp: new Date().toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('greenhouse'), + distanceKm: 0, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'batch-001', + plantIds: ['plant-001', 'plant-002'], + plantingMethod: 'indoor_start', + quantityPlanted: 2, + growingEnvironment: 'greenhouse', + }; + + const block = chain.recordEvent(event); + + expect(block.transportEvent.eventType).toBe('planting'); + expect((block.transportEvent as PlantingEvent).plantIds.length).toBe(2); + }); + }); + + describe('POST /api/transport/harvest', () => { + it('should record harvest event', () => { + const event: HarvestEvent = { + id: 'api-harvest-001', + timestamp: new Date().toISOString(), + eventType: 'harvest', + fromLocation: createLocation('farm'), + toLocation: createLocation('warehouse'), + distanceKm: 10, + durationMinutes: 30, + transportMethod: 'electric_truck', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'distributor-1', + status: 'verified', + plantIds: ['plant-001'], + harvestBatchId: 'harvest-001', + harvestType: 'full', + produceType: 'tomatoes', + grossWeight: 10, + netWeight: 9.5, + weightUnit: 'kg', + packagingType: 'crates', + temperatureRequired: { min: 10, max: 15, optimal: 12, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: false, + }; + + const block = chain.recordEvent(event); + + expect(block.transportEvent.eventType).toBe('harvest'); + }); + }); + + describe('GET /api/transport/journey/[plantId]', () => { + it('should return plant journey', () => { + const plantId = 'journey-plant-001'; + + // Record events for plant + const plantingEvent: PlantingEvent = { + id: 'journey-planting-001', + timestamp: new Date().toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('greenhouse'), + distanceKm: 0, + durationMinutes: 5, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'batch-001', + plantIds: [plantId], + plantingMethod: 'indoor_start', + quantityPlanted: 1, + growingEnvironment: 'greenhouse', + }; + + chain.recordEvent(plantingEvent); + + const journey = chain.getPlantJourney(plantId); + + expect(journey).not.toBeNull(); + expect(journey?.plantId).toBe(plantId); + expect(journey?.events.length).toBe(1); + }); + + it('should return null for unknown plant', () => { + const journey = chain.getPlantJourney('unknown-plant-id'); + expect(journey).toBeNull(); + }); + }); + + describe('GET /api/transport/footprint/[userId]', () => { + it('should return environmental impact', () => { + const userId = 'footprint-user-001'; + + const event: SeedAcquisitionEvent = { + id: 'footprint-event-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank'), + toLocation: createLocation('greenhouse'), + distanceKm: 50, + durationMinutes: 60, + transportMethod: 'diesel_truck', + carbonFootprintKg: 0, + senderId: userId, + receiverId: userId, + status: 'verified', + seedBatchId: 'batch-footprint', + sourceType: 'seed_bank', + species: 'Test species', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + }; + + chain.recordEvent(event); + + const impact = chain.getEnvironmentalImpact(userId); + + expect(impact.totalFoodMiles).toBe(50); + expect(impact.totalCarbonKg).toBeGreaterThan(0); + expect(impact.comparisonToConventional).toBeDefined(); + }); + + it('should return zero impact for user with no events', () => { + const impact = chain.getEnvironmentalImpact('no-events-user'); + + expect(impact.totalFoodMiles).toBe(0); + expect(impact.totalCarbonKg).toBe(0); + }); + }); + + describe('GET /api/transport/verify/[blockHash]', () => { + it('should verify chain integrity', () => { + chain.recordEvent(createSeedEvent()); + chain.recordEvent(createSeedEvent()); + + const isValid = chain.isChainValid(); + expect(isValid).toBe(true); + }); + + it('should detect tampered chain', () => { + chain.recordEvent(createSeedEvent()); + + // Tamper with block + chain.chain[1].transportEvent.distanceKm = 999999; + + const isValid = chain.isChainValid(); + expect(isValid).toBe(false); + }); + }); + + describe('GET /api/transport/qr/[id]', () => { + it('should generate QR data for plant', () => { + const plantId = 'qr-plant-001'; + + const plantingEvent: PlantingEvent = { + id: 'qr-planting-001', + timestamp: new Date().toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('greenhouse'), + distanceKm: 0, + durationMinutes: 5, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'batch-001', + plantIds: [plantId], + plantingMethod: 'indoor_start', + quantityPlanted: 1, + growingEnvironment: 'greenhouse', + }; + + chain.recordEvent(plantingEvent); + + const qrData = chain.generateQRData(plantId, undefined); + + expect(qrData.plantId).toBe(plantId); + expect(qrData.quickLookupUrl).toContain(plantId); + expect(qrData.verificationCode).toBeDefined(); + }); + + it('should generate QR data for batch', () => { + const batchId = 'qr-batch-001'; + + const event = createSeedEvent(); + event.seedBatchId = batchId; + chain.recordEvent(event); + + const qrData = chain.generateQRData(undefined, batchId); + + expect(qrData.batchId).toBe(batchId); + }); + }); + + describe('Response Format', () => { + it('should return blocks with all required fields', () => { + const block = chain.recordEvent(createSeedEvent()); + + expect(block.index).toBeDefined(); + expect(block.timestamp).toBeDefined(); + expect(block.transportEvent).toBeDefined(); + expect(block.previousHash).toBeDefined(); + expect(block.hash).toBeDefined(); + expect(block.nonce).toBeDefined(); + expect(block.cumulativeCarbonKg).toBeDefined(); + expect(block.cumulativeFoodMiles).toBeDefined(); + }); + }); +}); + +// Helper functions +function createLocation(type: string): TransportLocation { + return { + latitude: 40.7128, + longitude: -74.006, + locationType: type as any, + facilityName: `Test ${type}`, + }; +} + +function createSeedEvent(): SeedAcquisitionEvent { + return { + id: `seed-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank'), + toLocation: createLocation('greenhouse'), + distanceKm: 25, + durationMinutes: 45, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-bank-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: `batch-${Date.now()}`, + sourceType: 'seed_bank', + species: 'Test species', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + }; +} diff --git a/__tests__/api/vertical-farm.test.ts b/__tests__/api/vertical-farm.test.ts new file mode 100644 index 0000000..4fba19d --- /dev/null +++ b/__tests__/api/vertical-farm.test.ts @@ -0,0 +1,519 @@ +/** + * Vertical Farm API Tests + * Tests for vertical farm-related API endpoints + */ + +import { + VerticalFarmController, + getVerticalFarmController, +} from '../../lib/vertical-farming/controller'; +import { + VerticalFarm, + GrowingZone, + ZoneEnvironmentReadings, +} from '../../lib/vertical-farming/types'; + +describe('Vertical Farm API', () => { + let controller: VerticalFarmController; + + beforeEach(() => { + controller = new VerticalFarmController(); + }); + + describe('POST /api/vertical-farm/register', () => { + it('should register new farm', () => { + const farm = createVerticalFarm('api-farm-001'); + + controller.registerFarm(farm); + + const retrieved = controller.getFarm('api-farm-001'); + expect(retrieved).toBeDefined(); + expect(retrieved?.name).toBe('Test Vertical Farm'); + }); + + it('should allow updating farm', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const updatedFarm = createVerticalFarm('api-farm-001'); + updatedFarm.name = 'Updated Farm Name'; + controller.registerFarm(updatedFarm); + + const retrieved = controller.getFarm('api-farm-001'); + expect(retrieved?.name).toBe('Updated Farm Name'); + }); + }); + + describe('GET /api/vertical-farm/[farmId]', () => { + it('should return farm details', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const retrieved = controller.getFarm('api-farm-001'); + + expect(retrieved).toBeDefined(); + expect(retrieved?.id).toBe('api-farm-001'); + expect(retrieved?.zones.length).toBeGreaterThan(0); + }); + + it('should return undefined for unknown farm', () => { + const retrieved = controller.getFarm('unknown-farm'); + expect(retrieved).toBeUndefined(); + }); + }); + + describe('GET /api/vertical-farm/[farmId]/zones', () => { + it('should return farm zones', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const retrieved = controller.getFarm('api-farm-001'); + const zones = retrieved?.zones; + + expect(zones).toBeDefined(); + expect(zones?.length).toBeGreaterThan(0); + expect(zones?.[0].id).toBe('zone-001'); + }); + }); + + describe('POST /api/vertical-farm/[farmId]/zones', () => { + it('should add zone to existing farm', () => { + const farm = createVerticalFarm('api-farm-001'); + farm.zones = []; // Start with no zones + controller.registerFarm(farm); + + // Add zone by updating farm + const updatedFarm = controller.getFarm('api-farm-001')!; + updatedFarm.zones.push(createGrowingZone('new-zone', 'New Zone', 1)); + + expect(updatedFarm.zones.length).toBe(1); + }); + }); + + describe('GET /api/vertical-farm/[farmId]/analytics', () => { + it('should return farm analytics', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const analytics = controller.generateAnalytics('api-farm-001', 30); + + expect(analytics.farmId).toBe('api-farm-001'); + expect(analytics.period).toBe('30 days'); + }); + + it('should include yield metrics', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + // Start and complete a batch for analytics data + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + controller.completeHarvest(batch.id, 15.0, 'A'); + + const analytics = controller.generateAnalytics('api-farm-001', 30); + + expect(analytics.totalYieldKg).toBeGreaterThan(0); + }); + + it('should throw error for unknown farm', () => { + expect(() => { + controller.generateAnalytics('unknown-farm', 30); + }).toThrow('Farm unknown-farm not found'); + }); + }); + + describe('POST /api/vertical-farm/batch/start', () => { + it('should start crop batch', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + expect(batch.id).toBeDefined(); + expect(batch.plantCount).toBe(100); + expect(batch.status).toBe('germinating'); + }); + + it('should validate farm exists', () => { + expect(() => { + controller.startCropBatch( + 'unknown-farm', + 'zone-001', + 'recipe-001', + 'seed-001', + 100 + ); + }).toThrow('Farm unknown-farm not found'); + }); + + it('should validate zone exists', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + expect(() => { + controller.startCropBatch( + 'api-farm-001', + 'unknown-zone', + 'recipe-001', + 'seed-001', + 100 + ); + }).toThrow(); + }); + + it('should validate recipe exists', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + expect(() => { + controller.startCropBatch( + 'api-farm-001', + 'zone-001', + 'unknown-recipe', + 'seed-001', + 100 + ); + }).toThrow(); + }); + }); + + describe('GET /api/vertical-farm/batch/[batchId]', () => { + it('should return batch details', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + // Update progress + const updated = controller.updateBatchProgress(batch.id); + + expect(updated.id).toBe(batch.id); + expect(updated.currentDay).toBeGreaterThanOrEqual(0); + }); + + it('should throw error for unknown batch', () => { + expect(() => { + controller.updateBatchProgress('unknown-batch'); + }).toThrow('Batch unknown-batch not found'); + }); + }); + + describe('PUT /api/vertical-farm/batch/[batchId]/environment', () => { + it('should record environment readings', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const readings: ZoneEnvironmentReadings = { + timestamp: new Date().toISOString(), + temperatureC: 22, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }; + + const alerts = controller.recordEnvironment('zone-001', readings); + expect(Array.isArray(alerts)).toBe(true); + }); + + it('should detect environment alerts', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const readings: ZoneEnvironmentReadings = { + timestamp: new Date().toISOString(), + temperatureC: 35, // Too high + humidityPercent: 30, // Too low + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }; + + const alerts = controller.recordEnvironment('zone-001', readings); + expect(alerts.length).toBeGreaterThan(0); + }); + }); + + describe('POST /api/vertical-farm/batch/[batchId]/harvest', () => { + it('should complete harvest', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const completed = controller.completeHarvest(batch.id, 15.5, 'A'); + + expect(completed.status).toBe('completed'); + expect(completed.actualYieldKg).toBe(15.5); + expect(completed.qualityGrade).toBe('A'); + }); + + it('should throw error for unknown batch', () => { + expect(() => { + controller.completeHarvest('unknown-batch', 10, 'A'); + }).toThrow('Batch unknown-batch not found'); + }); + }); + + describe('GET /api/vertical-farm/recipes', () => { + it('should return all recipes', () => { + const recipes = controller.getRecipes(); + + expect(recipes.length).toBeGreaterThan(0); + expect(recipes[0].id).toBeDefined(); + expect(recipes[0].stages.length).toBeGreaterThan(0); + }); + + it('should include default recipes', () => { + const recipes = controller.getRecipes(); + const cropTypes = recipes.map(r => r.cropType); + + expect(cropTypes).toContain('lettuce'); + expect(cropTypes).toContain('basil'); + expect(cropTypes).toContain('microgreens'); + }); + }); + + describe('Response Formats', () => { + it('should return consistent farm format', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const retrieved = controller.getFarm('api-farm-001'); + + expect(retrieved?.id).toBeDefined(); + expect(retrieved?.name).toBeDefined(); + expect(retrieved?.ownerId).toBeDefined(); + expect(retrieved?.location).toBeDefined(); + expect(retrieved?.specs).toBeDefined(); + expect(retrieved?.zones).toBeDefined(); + expect(retrieved?.status).toBeDefined(); + }); + + it('should return consistent batch format', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'api-farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + expect(batch.id).toBeDefined(); + expect(batch.farmId).toBeDefined(); + expect(batch.zoneId).toBeDefined(); + expect(batch.cropType).toBeDefined(); + expect(batch.plantCount).toBeDefined(); + expect(batch.status).toBeDefined(); + expect(batch.expectedYieldKg).toBeDefined(); + }); + + it('should return consistent analytics format', () => { + const farm = createVerticalFarm('api-farm-001'); + controller.registerFarm(farm); + + const analytics = controller.generateAnalytics('api-farm-001', 30); + + expect(analytics.farmId).toBeDefined(); + expect(analytics.generatedAt).toBeDefined(); + expect(analytics.period).toBeDefined(); + expect(analytics.totalYieldKg).toBeDefined(); + expect(analytics.cropCyclesCompleted).toBeDefined(); + expect(analytics.topCropsByYield).toBeDefined(); + }); + }); +}); + +// Helper functions +function createVerticalFarm(id: string): VerticalFarm { + return { + id, + name: 'Test Vertical Farm', + ownerId: 'owner-001', + location: { + latitude: 40.7128, + longitude: -74.006, + address: '123 Farm Street', + city: 'New York', + country: 'USA', + timezone: 'America/New_York', + }, + specs: { + totalAreaSqm: 500, + growingAreaSqm: 400, + numberOfLevels: 4, + ceilingHeightM: 3, + totalGrowingPositions: 4000, + currentActivePlants: 0, + powerCapacityKw: 100, + waterStorageL: 5000, + backupPowerHours: 24, + certifications: ['organic'], + buildingType: 'warehouse', + insulation: 'high_efficiency', + }, + zones: [createGrowingZone('zone-001', 'Zone A', 1)], + environmentalControl: { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, + airCirculation: { fans: [] }, + controlMode: 'adaptive', + }, + irrigationSystem: { + type: 'recirculating', + freshWaterTankL: 2000, + freshWaterLevelL: 1800, + nutrientTankL: 500, + nutrientLevelL: 450, + wasteTankL: 200, + wasteLevelL: 50, + waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, + pumps: [], + irrigationSchedule: [], + }, + lightingSystem: { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 5000, + currentWattage: 3000, + efficacyUmolJ: 2.5, + }, + nutrientSystem: { + mixingMethod: 'fully_auto', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, + dosingRatiosMlPerL: [], + }, + monitoring: { + ec: 1.5, + ph: 6.0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + automationLevel: 'semi_automated', + automationSystems: [], + status: 'operational', + operationalSince: '2024-01-01', + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 75, + averageYieldEfficiency: 85, + energyEfficiencyScore: 80, + }; +} + +function createGrowingZone(id: string, name: string, level: number): GrowingZone { + return { + id, + name, + level, + areaSqm: 50, + lengthM: 10, + widthM: 5, + growingMethod: 'NFT', + plantPositions: 500, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: { + temperatureC: { min: 18, max: 24, target: 21 }, + humidityPercent: { min: 60, max: 80, target: 70 }, + co2Ppm: { min: 800, max: 1200, target: 1000 }, + lightPpfd: { min: 200, max: 400, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, + nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, + waterTempC: { min: 18, max: 22, target: 20 }, + }, + currentEnvironment: { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }, + status: 'empty', + }; +} diff --git a/__tests__/integration/demand-to-harvest.test.ts b/__tests__/integration/demand-to-harvest.test.ts new file mode 100644 index 0000000..cee00c8 --- /dev/null +++ b/__tests__/integration/demand-to-harvest.test.ts @@ -0,0 +1,407 @@ +/** + * Demand to Harvest Integration Tests + * Tests the complete flow from demand signal to plant to harvest + */ + +import { TransportChain, setTransportChain } from '../../lib/transport/tracker'; +import { DemandForecaster } from '../../lib/demand/forecaster'; +import { + ConsumerPreference, + PlantingRecommendation, +} from '../../lib/demand/types'; +import { + SeedAcquisitionEvent, + PlantingEvent, + HarvestEvent, + DistributionEvent, + ConsumerDeliveryEvent, + TransportLocation, +} from '../../lib/transport/types'; + +describe('Demand to Harvest Integration', () => { + let chain: TransportChain; + let forecaster: DemandForecaster; + + beforeEach(() => { + chain = new TransportChain(1); + setTransportChain(chain); + forecaster = new DemandForecaster(); + }); + + describe('Complete Demand-Driven Flow', () => { + it('should complete full flow from demand to consumer delivery', () => { + // Step 1: Register consumer demand + const consumerId = 'consumer-integration-001'; + const pref: ConsumerPreference = { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: 40.7128, + longitude: -74.006, + maxDeliveryRadiusKm: 25, + city: 'New York', + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens'], + preferredItems: [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + ], + certificationPreferences: ['organic', 'local'], + freshnessImportance: 5, + priceImportance: 3, + sustainabilityImportance: 5, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 4, + weeklyBudget: 100, + currency: 'USD', + }; + + forecaster.registerPreference(pref); + + // Step 2: Generate demand signal + const signal = forecaster.generateDemandSignal( + 40.7128, -74.006, 50, 'NYC Metro', 'spring' + ); + + expect(signal.totalConsumers).toBe(1); + expect(signal.demandItems.length).toBeGreaterThan(0); + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem).toBeDefined(); + expect(lettuceItem!.weeklyDemandKg).toBe(20); // 5 kg * 4 household size + + // Step 3: Generate planting recommendation for grower + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-integration-001', + 40.72, -74.01, // Near NYC + 50, // 50km delivery radius + 100, // 100 sqm available + 'spring' + ); + + expect(recommendations.length).toBeGreaterThan(0); + const lettuceRec = recommendations.find(r => r.produceType === 'lettuce'); + expect(lettuceRec).toBeDefined(); + + // Step 4: Grower follows recommendation and plants + const seedBatchId = 'demand-driven-batch-001'; + const plantIds = ['demand-plant-001', 'demand-plant-002']; + + const seedEvent: SeedAcquisitionEvent = { + id: 'demand-seed-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank', 'Local Organic Seeds'), + toLocation: createLocation('greenhouse', 'Grower Greenhouse'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-supplier', + receiverId: 'grower-integration-001', + status: 'verified', + seedBatchId, + sourceType: 'purchase', + species: 'Lactuca sativa', + variety: 'Butterhead', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + certifications: ['organic'], + }; + + chain.recordEvent(seedEvent); + + const plantingEvent: PlantingEvent = { + id: 'demand-planting-001', + timestamp: new Date(Date.now() + 1000).toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse', 'Grower Greenhouse'), + toLocation: createLocation('greenhouse', 'Grower Greenhouse'), + distanceKm: 0, + durationMinutes: 30, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-integration-001', + receiverId: 'grower-integration-001', + status: 'verified', + seedBatchId, + plantIds, + plantingMethod: 'indoor_start', + quantityPlanted: plantIds.length, + growingEnvironment: 'greenhouse', + expectedHarvestDate: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString(), + }; + + chain.recordEvent(plantingEvent); + + // Step 5: Harvest + const harvestBatchId = 'demand-harvest-batch-001'; + + const harvestEvent: HarvestEvent = { + id: 'demand-harvest-001', + timestamp: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString(), + eventType: 'harvest', + fromLocation: createLocation('greenhouse', 'Grower Greenhouse'), + toLocation: createLocation('hub', 'Distribution Hub'), + distanceKm: 5, + durationMinutes: 15, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'grower-integration-001', + receiverId: 'hub-distribution', + status: 'verified', + plantIds, + harvestBatchId, + harvestType: 'full', + produceType: 'Butterhead Lettuce', + grossWeight: 2, + netWeight: 1.8, + weightUnit: 'kg', + qualityGrade: 'A', + packagingType: 'sustainable_boxes', + temperatureRequired: { min: 2, max: 8, optimal: 4, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: false, + }; + + chain.recordEvent(harvestEvent); + + // Step 6: Distribution + const distributionEvent: DistributionEvent = { + id: 'demand-distribution-001', + timestamp: new Date(Date.now() + 46 * 24 * 60 * 60 * 1000).toISOString(), + eventType: 'distribution', + fromLocation: createLocation('hub', 'Distribution Hub'), + toLocation: createLocation('market', 'Local Delivery Zone'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'electric_truck', + carbonFootprintKg: 0, + senderId: 'hub-distribution', + receiverId: 'delivery-service', + status: 'verified', + batchIds: [harvestBatchId], + destinationType: 'consumer', + customerType: 'individual', + deliveryWindow: { + start: new Date(Date.now() + 46 * 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 46.5 * 24 * 60 * 60 * 1000).toISOString(), + }, + deliveryAttempts: 1, + handoffVerified: true, + }; + + chain.recordEvent(distributionEvent); + + // Step 7: Consumer Delivery + const deliveryEvent: ConsumerDeliveryEvent = { + id: 'demand-delivery-001', + timestamp: new Date(Date.now() + 46.25 * 24 * 60 * 60 * 1000).toISOString(), + eventType: 'consumer_delivery', + fromLocation: createLocation('market', 'Local Delivery Zone'), + toLocation: createLocation('consumer', 'Consumer Home'), + distanceKm: 3, + durationMinutes: 10, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'delivery-service', + receiverId: consumerId, + status: 'verified', + orderId: 'order-integration-001', + batchIds: [harvestBatchId], + deliveryMethod: 'home_delivery', + finalMileMethod: 'electric_vehicle', + packagingReturned: true, + feedbackReceived: true, + feedbackRating: 5, + feedbackNotes: 'Fresh and delicious!', + }; + + chain.recordEvent(deliveryEvent); + + // Verify complete chain + expect(chain.isChainValid()).toBe(true); + expect(chain.chain.length).toBe(7); // Genesis + 6 events + + // Verify plant journey + const journey = chain.getPlantJourney(plantIds[0]); + expect(journey).not.toBeNull(); + expect(journey!.events.length).toBe(3); // planting, harvest, and possibly more + + // Verify environmental impact + const impact = chain.getEnvironmentalImpact('grower-integration-001'); + expect(impact.totalFoodMiles).toBeLessThan(50); // Local delivery + expect(impact.comparisonToConventional.percentageReduction).toBeGreaterThan(0); + }); + }); + + describe('Supply Registration Affects Recommendations', () => { + it('should adjust recommendations based on existing supply', () => { + // Register high demand + const pref = createConsumerPreference('consumer-001'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + // Get recommendations without supply + const recsWithoutSupply = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'spring' + ); + + // Register significant supply + forecaster.registerSupply({ + id: 'supply-001', + growerId: 'other-grower', + timestamp: new Date().toISOString(), + produceType: 'lettuce', + committedQuantityKg: 80, + availableFrom: new Date().toISOString(), + availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + pricePerKg: 5, + currency: 'USD', + minimumOrderKg: 1, + certifications: ['organic'], + freshnessGuaranteeHours: 48, + deliveryRadiusKm: 50, + deliveryMethods: ['grower_delivery'], + status: 'available', + remainingKg: 80, + }); + + // Generate new demand signal (with supply factored in) + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + // Get recommendations with supply + const recsWithSupply = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'spring' + ); + + // Recommendations should be different (less space recommended for lettuce) + if (recsWithoutSupply.length > 0 && recsWithSupply.length > 0) { + const lettuceWithout = recsWithoutSupply.find(r => r.produceType === 'lettuce'); + const lettuceWith = recsWithSupply.find(r => r.produceType === 'lettuce'); + + if (lettuceWithout && lettuceWith) { + expect(lettuceWith.recommendedQuantity).toBeLessThanOrEqual(lettuceWithout.recommendedQuantity); + } + } + }); + }); + + describe('Regional Demand Matching', () => { + it('should match growers with regional demand', () => { + // Create consumers in different regions + const nycConsumer = createConsumerPreference('nyc-consumer'); + nycConsumer.location = { latitude: 40.7128, longitude: -74.006, maxDeliveryRadiusKm: 10 }; + forecaster.registerPreference(nycConsumer); + + const laConsumer = createConsumerPreference('la-consumer'); + laConsumer.location = { latitude: 34.0522, longitude: -118.2437, maxDeliveryRadiusKm: 10 }; + forecaster.registerPreference(laConsumer); + + // Generate signals for each region + const nycSignal = forecaster.generateDemandSignal(40.7128, -74.006, 20, 'NYC', 'spring'); + const laSignal = forecaster.generateDemandSignal(34.0522, -118.2437, 20, 'LA', 'spring'); + + expect(nycSignal.totalConsumers).toBe(1); + expect(laSignal.totalConsumers).toBe(1); + + // NYC grower should only see NYC demand + const nycGrowerRecs = forecaster.generatePlantingRecommendations( + 'nyc-grower', 40.72, -74.01, 25, 100, 'spring' + ); + + // LA grower should only see LA demand + const laGrowerRecs = forecaster.generatePlantingRecommendations( + 'la-grower', 34.06, -118.25, 25, 100, 'spring' + ); + + // Both should have recommendations from their respective regions + expect(nycGrowerRecs.length + laGrowerRecs.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe('Seasonal Demand Flow', () => { + it('should respect seasonal availability in recommendations', () => { + const pref = createConsumerPreference('seasonal-consumer'); + pref.preferredItems = [ + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + // Winter recommendations + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'winter'); + const winterRecs = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'winter' + ); + + // Tomato should not be recommended in winter (not in season) + const tomatoWinter = winterRecs.find(r => r.produceType === 'tomato'); + expect(tomatoWinter).toBeUndefined(); + + // Summer recommendations + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer'); + const summerRecs = forecaster.generatePlantingRecommendations( + 'grower-001', 40.7, -74.0, 50, 100, 'summer' + ); + + // Tomato should be recommended in summer + const tomatoSummer = summerRecs.find(r => r.produceType === 'tomato'); + expect(tomatoSummer).toBeDefined(); + }); + }); +}); + +// Helper functions +function createLocation(type: string, name: string): TransportLocation { + return { + latitude: 40.7 + Math.random() * 0.1, + longitude: -74.0 + Math.random() * 0.1, + locationType: type as any, + facilityName: name, + }; +} + +function createConsumerPreference(consumerId: string): ConsumerPreference { + return { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: 40.7128, + longitude: -74.006, + maxDeliveryRadiusKm: 25, + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens'], + preferredItems: [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, + ], + certificationPreferences: ['organic'], + freshnessImportance: 4, + priceImportance: 3, + sustainabilityImportance: 4, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 2, + weeklyBudget: 100, + currency: 'USD', + }; +} diff --git a/__tests__/integration/seed-to-seed.test.ts b/__tests__/integration/seed-to-seed.test.ts new file mode 100644 index 0000000..e670c52 --- /dev/null +++ b/__tests__/integration/seed-to-seed.test.ts @@ -0,0 +1,532 @@ +/** + * Seed-to-Seed Lifecycle Integration Tests + * Tests complete lifecycle from seed acquisition to seed saving + */ + +import { TransportChain, setTransportChain } from '../../lib/transport/tracker'; +import { + SeedAcquisitionEvent, + PlantingEvent, + GrowingTransportEvent, + HarvestEvent, + SeedSavingEvent, + SeedSharingEvent, + TransportLocation, +} from '../../lib/transport/types'; + +describe('Seed-to-Seed Lifecycle', () => { + let chain: TransportChain; + + beforeEach(() => { + chain = new TransportChain(1); // Low difficulty for faster tests + setTransportChain(chain); + }); + + describe('Complete Lifecycle', () => { + it('should track complete lifecycle from seed acquisition to seed saving', () => { + const seedBatchId = 'lifecycle-batch-001'; + const plantIds = ['plant-lifecycle-001', 'plant-lifecycle-002']; + const newSeedBatchId = 'lifecycle-batch-002'; + + // Step 1: Seed Acquisition + const seedAcquisition: SeedAcquisitionEvent = { + id: 'lifecycle-seed-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank', 'Heritage Seed Bank'), + toLocation: createLocation('greenhouse', 'Local Grower Greenhouse'), + distanceKm: 50, + durationMinutes: 60, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-bank-heritage', + receiverId: 'grower-local-001', + status: 'verified', + seedBatchId, + sourceType: 'seed_bank', + species: 'Solanum lycopersicum', + variety: 'Brandywine', + quantity: 50, + quantityUnit: 'seeds', + generation: 3, + germinationRate: 92, + certifications: ['heirloom', 'organic'], + }; + + const block1 = chain.recordEvent(seedAcquisition); + expect(block1.transportEvent.eventType).toBe('seed_acquisition'); + + // Step 2: Planting + const planting: PlantingEvent = { + id: 'lifecycle-planting-001', + timestamp: new Date(Date.now() + 1000).toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse', 'Local Grower Greenhouse'), + toLocation: createLocation('greenhouse', 'Local Grower Greenhouse'), + distanceKm: 0, + durationMinutes: 30, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-local-001', + receiverId: 'grower-local-001', + status: 'verified', + seedBatchId, + plantIds, + plantingMethod: 'indoor_start', + quantityPlanted: 2, + growingEnvironment: 'greenhouse', + expectedHarvestDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), + }; + + const block2 = chain.recordEvent(planting); + expect(block2.transportEvent.eventType).toBe('planting'); + + // Step 3: Growing Transport (transplant to outdoor garden) + const transplant: GrowingTransportEvent = { + id: 'lifecycle-transplant-001', + timestamp: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days later + eventType: 'growing_transport', + fromLocation: createLocation('greenhouse', 'Local Grower Greenhouse'), + toLocation: createLocation('farm', 'Local Community Garden'), + distanceKm: 2, + durationMinutes: 20, + transportMethod: 'bicycle', + carbonFootprintKg: 0, + senderId: 'grower-local-001', + receiverId: 'grower-local-001', + status: 'verified', + plantIds, + reason: 'transplant', + plantStage: 'vegetative', + handlingMethod: 'potted', + rootDisturbance: 'minimal', + acclimatizationRequired: true, + acclimatizationDays: 7, + }; + + const block3 = chain.recordEvent(transplant); + expect(block3.transportEvent.eventType).toBe('growing_transport'); + + // Step 4: Harvest + const harvest: HarvestEvent = { + id: 'lifecycle-harvest-001', + timestamp: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days after planting + eventType: 'harvest', + fromLocation: createLocation('farm', 'Local Community Garden'), + toLocation: createLocation('warehouse', 'Local Co-op Distribution'), + distanceKm: 5, + durationMinutes: 15, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'grower-local-001', + receiverId: 'coop-distribution', + status: 'verified', + plantIds, + harvestBatchId: 'harvest-lifecycle-001', + harvestType: 'full', + produceType: 'Brandywine Tomatoes', + grossWeight: 8, + netWeight: 7.5, + weightUnit: 'kg', + qualityGrade: 'A', + packagingType: 'sustainable_crates', + temperatureRequired: { min: 12, max: 18, optimal: 15, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: true, + seedBatchIdCreated: newSeedBatchId, + }; + + const block4 = chain.recordEvent(harvest); + expect(block4.transportEvent.eventType).toBe('harvest'); + + // Step 5: Seed Saving + const seedSaving: SeedSavingEvent = { + id: 'lifecycle-seed-saving-001', + timestamp: new Date(Date.now() + 95 * 24 * 60 * 60 * 1000).toISOString(), + eventType: 'seed_saving', + fromLocation: createLocation('farm', 'Local Community Garden'), + toLocation: createLocation('seed_bank', 'Grower Home Seed Storage'), + distanceKm: 1, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-local-001', + receiverId: 'grower-local-001', + status: 'verified', + parentPlantIds: plantIds, + newSeedBatchId, + collectionMethod: 'fermentation', + seedCount: 200, + germinationRate: 88, + storageConditions: { + temperature: 10, + humidity: 30, + lightExposure: 'dark', + containerType: 'jar', + desiccant: true, + estimatedViability: 5, + }, + storageLocationId: 'grower-home-storage', + newGenerationNumber: 4, + geneticNotes: 'Selected from best performing plants', + availableForSharing: true, + sharingTerms: 'trade', + }; + + const block5 = chain.recordEvent(seedSaving); + expect(block5.transportEvent.eventType).toBe('seed_saving'); + + // Verify complete journey + const journey1 = chain.getPlantJourney(plantIds[0]); + expect(journey1).not.toBeNull(); + expect(journey1?.events.length).toBe(4); // planting, transplant, harvest, seed_saving + expect(journey1?.currentStage).toBe('seed_saving'); + expect(journey1?.descendantSeedBatches).toContain(newSeedBatchId); + + // Verify chain integrity + expect(chain.isChainValid()).toBe(true); + + // Verify carbon tracking + expect(block5.cumulativeCarbonKg).toBeGreaterThanOrEqual(0); + expect(block5.cumulativeFoodMiles).toBe( + seedAcquisition.distanceKm + + planting.distanceKm + + transplant.distanceKm + + harvest.distanceKm + + seedSaving.distanceKm + ); + }); + + it('should track multiple generations', () => { + // Generation 1 seeds + const gen1Batch = 'gen1-batch'; + const gen1Plants = ['gen1-plant-001']; + const gen2Batch = 'gen2-batch'; + const gen2Plants = ['gen2-plant-001']; + const gen3Batch = 'gen3-batch'; + + // Gen 1: Acquire, Plant, Harvest with seed saving + chain.recordEvent(createSeedAcquisition(gen1Batch, 1)); + chain.recordEvent(createPlanting(gen1Batch, gen1Plants)); + chain.recordEvent(createHarvest(gen1Plants, gen2Batch)); + chain.recordEvent(createSeedSaving(gen1Plants, gen2Batch, 2)); + + // Gen 2: Plant saved seeds, Harvest with seed saving + chain.recordEvent(createSeedAcquisition(gen2Batch, 2, gen1Plants)); + chain.recordEvent(createPlanting(gen2Batch, gen2Plants)); + chain.recordEvent(createHarvest(gen2Plants, gen3Batch)); + chain.recordEvent(createSeedSaving(gen2Plants, gen3Batch, 3)); + + // Verify chain + expect(chain.isChainValid()).toBe(true); + expect(chain.chain.length).toBe(9); // 1 genesis + 8 events + + // Verify lineage + const journey = chain.getPlantJourney('gen2-plant-001'); + expect(journey?.generation).toBe(2); + }); + }); + + describe('Seed Sharing', () => { + it('should track seed sharing between growers', () => { + const originalBatch = 'original-batch'; + const sharedQuantity = 25; + + // Save seeds + const seedSaving: SeedSavingEvent = { + id: 'share-seed-saving', + timestamp: new Date().toISOString(), + eventType: 'seed_saving', + fromLocation: createLocation('farm', 'Grower A Farm'), + toLocation: createLocation('seed_bank', 'Grower A Storage'), + distanceKm: 0, + durationMinutes: 30, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-a', + receiverId: 'grower-a', + status: 'verified', + parentPlantIds: ['parent-001'], + newSeedBatchId: originalBatch, + collectionMethod: 'dry_seed', + seedCount: 100, + storageConditions: { + temperature: 10, + humidity: 30, + lightExposure: 'dark', + containerType: 'envelope', + desiccant: true, + estimatedViability: 4, + }, + storageLocationId: 'grower-a-storage', + newGenerationNumber: 2, + availableForSharing: true, + sharingTerms: 'trade', + }; + + chain.recordEvent(seedSaving); + + // Share seeds + const seedSharing: SeedSharingEvent = { + id: 'share-event-001', + timestamp: new Date(Date.now() + 1000).toISOString(), + eventType: 'seed_sharing', + fromLocation: createLocation('seed_bank', 'Grower A Storage'), + toLocation: createLocation('greenhouse', 'Grower B Greenhouse'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'bicycle', + carbonFootprintKg: 0, + senderId: 'grower-a', + receiverId: 'grower-b', + status: 'verified', + seedBatchId: originalBatch, + quantityShared: sharedQuantity, + quantityUnit: 'seeds', + sharingType: 'trade', + tradeDetails: 'Traded for basil seeds', + recipientAgreement: true, + growingCommitment: 'Will grow and save seeds', + reportBackRequired: true, + }; + + const block = chain.recordEvent(seedSharing); + + expect(block.transportEvent.eventType).toBe('seed_sharing'); + expect((block.transportEvent as SeedSharingEvent).quantityShared).toBe(sharedQuantity); + }); + }); + + describe('Environmental Impact Across Lifecycle', () => { + it('should calculate cumulative environmental impact', () => { + const userId = 'eco-grower'; + + // Record full lifecycle with the same user + const events = [ + { distanceKm: 50, method: 'electric_vehicle' as const }, + { distanceKm: 0, method: 'walking' as const }, + { distanceKm: 5, method: 'bicycle' as const }, + { distanceKm: 10, method: 'electric_truck' as const }, + { distanceKm: 1, method: 'walking' as const }, + ]; + + let totalDistance = 0; + events.forEach((event, i) => { + chain.recordEvent({ + id: `eco-event-${i}`, + timestamp: new Date(Date.now() + i * 1000).toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('farm', 'Location A'), + toLocation: createLocation('farm', 'Location B'), + distanceKm: event.distanceKm, + durationMinutes: 30, + transportMethod: event.method, + carbonFootprintKg: 0, + senderId: userId, + receiverId: userId, + status: 'verified', + seedBatchId: `batch-${i}`, + sourceType: 'previous_harvest', + species: 'Test', + quantity: 10, + quantityUnit: 'seeds', + generation: 1, + } as SeedAcquisitionEvent); + totalDistance += event.distanceKm; + }); + + const impact = chain.getEnvironmentalImpact(userId); + + expect(impact.totalFoodMiles).toBe(totalDistance); + expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0); + }); + }); + + describe('QR Code Traceability', () => { + it('should generate traceable QR codes at each stage', () => { + const plantId = 'qr-trace-plant'; + const batchId = 'qr-trace-batch'; + + // Record seed acquisition + chain.recordEvent({ + id: 'qr-seed', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank', 'Bank'), + toLocation: createLocation('greenhouse', 'Greenhouse'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'bicycle', + carbonFootprintKg: 0, + senderId: 'seller', + receiverId: 'buyer', + status: 'verified', + seedBatchId: batchId, + sourceType: 'seed_bank', + species: 'Test', + quantity: 10, + quantityUnit: 'seeds', + generation: 1, + } as SeedAcquisitionEvent); + + // Record planting + chain.recordEvent({ + id: 'qr-planting', + timestamp: new Date(Date.now() + 1000).toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse', 'Greenhouse'), + toLocation: createLocation('greenhouse', 'Greenhouse'), + distanceKm: 0, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'buyer', + receiverId: 'buyer', + status: 'verified', + seedBatchId: batchId, + plantIds: [plantId], + plantingMethod: 'indoor_start', + quantityPlanted: 1, + growingEnvironment: 'greenhouse', + } as PlantingEvent); + + // Generate QR for plant + const plantQR = chain.generateQRData(plantId, undefined); + expect(plantQR.plantId).toBe(plantId); + expect(plantQR.lastEventType).toBe('planting'); + + // Generate QR for batch + const batchQR = chain.generateQRData(undefined, batchId); + expect(batchQR.batchId).toBe(batchId); + + // Both should have valid verification codes + expect(plantQR.verificationCode).toMatch(/^[A-F0-9]{8}$/); + expect(batchQR.verificationCode).toMatch(/^[A-F0-9]{8}$/); + }); + }); +}); + +// Helper functions +function createLocation(type: string, name: string): TransportLocation { + return { + latitude: 40.7 + Math.random() * 0.1, + longitude: -74.0 + Math.random() * 0.1, + locationType: type as any, + facilityName: name, + }; +} + +function createSeedAcquisition( + batchId: string, + generation: number, + parentPlantIds?: string[] +): SeedAcquisitionEvent { + return { + id: `seed-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank', 'Bank'), + toLocation: createLocation('greenhouse', 'Greenhouse'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'sender', + receiverId: 'receiver', + status: 'verified', + seedBatchId: batchId, + sourceType: parentPlantIds ? 'previous_harvest' : 'seed_bank', + species: 'Test Species', + quantity: 10, + quantityUnit: 'seeds', + generation, + parentPlantIds, + }; +} + +function createPlanting(batchId: string, plantIds: string[]): PlantingEvent { + return { + id: `planting-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + timestamp: new Date(Date.now() + 100).toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse', 'Greenhouse'), + toLocation: createLocation('greenhouse', 'Greenhouse'), + distanceKm: 0, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower', + receiverId: 'grower', + status: 'verified', + seedBatchId: batchId, + plantIds, + plantingMethod: 'indoor_start', + quantityPlanted: plantIds.length, + growingEnvironment: 'greenhouse', + }; +} + +function createHarvest(plantIds: string[], newSeedBatchId: string): HarvestEvent { + return { + id: `harvest-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + timestamp: new Date(Date.now() + 200).toISOString(), + eventType: 'harvest', + fromLocation: createLocation('farm', 'Farm'), + toLocation: createLocation('warehouse', 'Warehouse'), + distanceKm: 5, + durationMinutes: 20, + transportMethod: 'electric_truck', + carbonFootprintKg: 0, + senderId: 'grower', + receiverId: 'distributor', + status: 'verified', + plantIds, + harvestBatchId: `harvest-batch-${Date.now()}`, + harvestType: 'full', + produceType: 'Test Produce', + grossWeight: 5, + netWeight: 4.5, + weightUnit: 'kg', + packagingType: 'crates', + temperatureRequired: { min: 10, max: 20, optimal: 15, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: true, + seedBatchIdCreated: newSeedBatchId, + }; +} + +function createSeedSaving( + parentPlantIds: string[], + newSeedBatchId: string, + generation: number +): SeedSavingEvent { + return { + id: `save-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + timestamp: new Date(Date.now() + 300).toISOString(), + eventType: 'seed_saving', + fromLocation: createLocation('farm', 'Farm'), + toLocation: createLocation('seed_bank', 'Storage'), + distanceKm: 0, + durationMinutes: 30, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower', + receiverId: 'grower', + status: 'verified', + parentPlantIds, + newSeedBatchId, + collectionMethod: 'dry_seed', + seedCount: 50, + storageConditions: { + temperature: 10, + humidity: 30, + lightExposure: 'dark', + containerType: 'envelope', + desiccant: true, + estimatedViability: 4, + }, + storageLocationId: 'grower-storage', + newGenerationNumber: generation, + availableForSharing: true, + }; +} diff --git a/__tests__/integration/vf-batch-lifecycle.test.ts b/__tests__/integration/vf-batch-lifecycle.test.ts new file mode 100644 index 0000000..891341b --- /dev/null +++ b/__tests__/integration/vf-batch-lifecycle.test.ts @@ -0,0 +1,558 @@ +/** + * Vertical Farm Batch Lifecycle Integration Tests + * Tests complete vertical farm batch from start to harvest + */ + +import { + VerticalFarmController, +} from '../../lib/vertical-farming/controller'; +import { TransportChain, setTransportChain } from '../../lib/transport/tracker'; +import { + VerticalFarm, + GrowingZone, + ZoneEnvironmentReadings, + CropBatch, +} from '../../lib/vertical-farming/types'; +import { + SeedAcquisitionEvent, + PlantingEvent, + HarvestEvent, + TransportLocation, +} from '../../lib/transport/types'; + +describe('Vertical Farm Batch Lifecycle', () => { + let controller: VerticalFarmController; + let chain: TransportChain; + + beforeEach(() => { + controller = new VerticalFarmController(); + chain = new TransportChain(1); + setTransportChain(chain); + }); + + describe('Complete Batch Lifecycle', () => { + it('should complete full batch lifecycle from planting to harvest', () => { + // Step 1: Register farm + const farm = createVerticalFarm('vf-lifecycle-001'); + controller.registerFarm(farm); + + // Step 2: Select recipe and start batch + const recipes = controller.getRecipes(); + const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; + + const batch = controller.startCropBatch( + 'vf-lifecycle-001', + 'zone-001', + lettuceRecipe.id, + 'seed-batch-vf-001', + 100 + ); + + expect(batch.status).toBe('germinating'); + expect(batch.plantCount).toBe(100); + expect(batch.currentStage).toBe(lettuceRecipe.stages[0].name); + + // Step 3: Verify zone is updated + const farmAfterPlanting = controller.getFarm('vf-lifecycle-001')!; + const zone = farmAfterPlanting.zones.find(z => z.id === 'zone-001')!; + + expect(zone.status).toBe('planted'); + expect(zone.currentCrop).toBe('lettuce'); + expect(zone.plantIds.length).toBe(100); + + // Step 4: Record environment readings (simulate daily monitoring) + for (let day = 0; day < 5; day++) { + const readings = createGoodReadings(); + const alerts = controller.recordEnvironment('zone-001', readings); + expect(alerts.length).toBe(0); // No alerts for good readings + } + + // Step 5: Update batch progress + const updatedBatch = controller.updateBatchProgress(batch.id); + expect(updatedBatch.currentDay).toBeGreaterThanOrEqual(0); + + // Step 6: Complete harvest + const completedBatch = controller.completeHarvest(batch.id, 18.0, 'A'); + + expect(completedBatch.status).toBe('completed'); + expect(completedBatch.actualYieldKg).toBe(18.0); + expect(completedBatch.qualityGrade).toBe('A'); + + // Step 7: Verify zone is cleared + const farmAfterHarvest = controller.getFarm('vf-lifecycle-001')!; + const zoneAfterHarvest = farmAfterHarvest.zones.find(z => z.id === 'zone-001')!; + + expect(zoneAfterHarvest.status).toBe('cleaning'); + expect(zoneAfterHarvest.currentCrop).toBe(''); + expect(zoneAfterHarvest.plantIds.length).toBe(0); + }); + }); + + describe('Environment Monitoring During Growth', () => { + it('should track health score throughout growth', () => { + const farm = createVerticalFarm('vf-health-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'vf-health-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const initialHealth = batch.healthScore; + expect(initialHealth).toBe(100); + + // Good readings - health should stay high + for (let i = 0; i < 3; i++) { + controller.recordEnvironment('zone-001', createGoodReadings()); + } + expect(batch.healthScore).toBe(100); + + // Bad readings - health should decrease + controller.recordEnvironment('zone-001', createBadReadings()); + expect(batch.healthScore).toBeLessThan(100); + + // Critical readings - health should decrease more + controller.recordEnvironment('zone-001', createCriticalReadings()); + expect(batch.healthScore).toBeLessThan(95); + }); + + it('should log environment readings to batch', () => { + const farm = createVerticalFarm('vf-log-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'vf-log-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + // Record multiple readings + for (let i = 0; i < 5; i++) { + controller.recordEnvironment('zone-001', createGoodReadings()); + } + + expect(batch.environmentLog.length).toBe(5); + }); + }); + + describe('Multi-Zone Operations', () => { + it('should manage multiple batches across zones', () => { + const farm = createVerticalFarm('vf-multi-001'); + farm.zones = [ + createGrowingZone('zone-001', 'Zone A', 1), + createGrowingZone('zone-002', 'Zone B', 1), + createGrowingZone('zone-003', 'Zone C', 2), + ]; + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; + const basilRecipe = recipes.find(r => r.cropType === 'basil')!; + const microgreensRecipe = recipes.find(r => r.cropType === 'microgreens')!; + + // Start different batches in different zones + const batch1 = controller.startCropBatch( + 'vf-multi-001', 'zone-001', lettuceRecipe.id, 'seed-001', 100 + ); + const batch2 = controller.startCropBatch( + 'vf-multi-001', 'zone-002', basilRecipe.id, 'seed-002', 50 + ); + const batch3 = controller.startCropBatch( + 'vf-multi-001', 'zone-003', microgreensRecipe.id, 'seed-003', 200 + ); + + expect(batch1.cropType).toBe('lettuce'); + expect(batch2.cropType).toBe('basil'); + expect(batch3.cropType).toBe('microgreens'); + + // Verify zones have correct crops + const farmAfter = controller.getFarm('vf-multi-001')!; + expect(farmAfter.zones.find(z => z.id === 'zone-001')!.currentCrop).toBe('lettuce'); + expect(farmAfter.zones.find(z => z.id === 'zone-002')!.currentCrop).toBe('basil'); + expect(farmAfter.zones.find(z => z.id === 'zone-003')!.currentCrop).toBe('microgreens'); + }); + }); + + describe('Analytics Generation', () => { + it('should generate analytics after batch completion', () => { + const farm = createVerticalFarm('vf-analytics-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + + // Complete multiple batches + for (let i = 0; i < 3; i++) { + const batch = controller.startCropBatch( + 'vf-analytics-001', + 'zone-001', + recipes[0].id, + `seed-batch-${i}`, + 100 + ); + controller.completeHarvest(batch.id, 15 + i, 'A'); + } + + const analytics = controller.generateAnalytics('vf-analytics-001', 30); + + expect(analytics.cropCyclesCompleted).toBe(3); + expect(analytics.totalYieldKg).toBeGreaterThan(0); + expect(analytics.gradeAPercent).toBe(100); + }); + + it('should calculate efficiency metrics', () => { + const farm = createVerticalFarm('vf-efficiency-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'vf-efficiency-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + controller.completeHarvest(batch.id, 18.0, 'A'); + + const analytics = controller.generateAnalytics('vf-efficiency-001', 30); + + expect(analytics.yieldPerSqmPerYear).toBeGreaterThan(0); + expect(analytics.averageQualityScore).toBeGreaterThan(0); + expect(analytics.cropSuccessRate).toBe(100); + }); + }); + + describe('Integration with Transport Chain', () => { + it('should track VF produce through transport chain', () => { + const farm = createVerticalFarm('vf-transport-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'vf-transport-001', + 'zone-001', + recipes[0].id, + 'seed-transport-001', + 100 + ); + + // Record seed acquisition in transport chain + const seedEvent: SeedAcquisitionEvent = { + id: 'vf-transport-seed-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank', 'Seed Supplier'), + toLocation: createLocation('vertical_farm', 'VF Facility'), + distanceKm: 20, + durationMinutes: 30, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-supplier', + receiverId: 'vf-operator', + status: 'verified', + seedBatchId: 'seed-transport-001', + sourceType: 'purchase', + species: recipes[0].cropType, + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + certifications: ['organic'], + }; + + chain.recordEvent(seedEvent); + + // Complete VF harvest + controller.completeHarvest(batch.id, 18.0, 'A'); + + // Record harvest in transport chain + const harvestEvent: HarvestEvent = { + id: 'vf-transport-harvest-001', + timestamp: new Date().toISOString(), + eventType: 'harvest', + fromLocation: createLocation('vertical_farm', 'VF Facility'), + toLocation: createLocation('warehouse', 'Distribution Center'), + distanceKm: 10, + durationMinutes: 20, + transportMethod: 'electric_truck', + carbonFootprintKg: 0, + senderId: 'vf-operator', + receiverId: 'distributor', + status: 'verified', + plantIds: batch.plantIds.slice(0, 10), + harvestBatchId: batch.id, + harvestType: 'full', + produceType: recipes[0].cropType, + grossWeight: 20, + netWeight: 18, + weightUnit: 'kg', + qualityGrade: 'A', + packagingType: 'sustainable_packaging', + temperatureRequired: { min: 2, max: 8, optimal: 4, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: false, + }; + + chain.recordEvent(harvestEvent); + + // Verify chain integrity + expect(chain.isChainValid()).toBe(true); + + // Calculate environmental impact + const impact = chain.getEnvironmentalImpact('vf-operator'); + expect(impact.totalFoodMiles).toBeLessThan(50); // Very low for VF + }); + }); + + describe('Recipe Stage Transitions', () => { + it('should update environment targets based on stage', () => { + const farm = createVerticalFarm('vf-stage-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const recipe = recipes[0]; + + const batch = controller.startCropBatch( + 'vf-stage-001', + 'zone-001', + recipe.id, + 'seed-stage-001', + 100 + ); + + // Initial stage should be first stage + expect(batch.currentStage).toBe(recipe.stages[0].name); + + // Get zone and check targets match first stage + const farmAfter = controller.getFarm('vf-stage-001')!; + const zone = farmAfter.zones.find(z => z.id === 'zone-001')!; + const firstStage = recipe.stages[0]; + + expect(zone.environmentTargets.temperatureC.target).toBe(firstStage.temperature.day); + }); + }); + + describe('Batch Failure Handling', () => { + it('should track issues that affect batch', () => { + const farm = createVerticalFarm('vf-issue-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'vf-issue-001', + 'zone-001', + recipes[0].id, + 'seed-issue-001', + 100 + ); + + // Simulate multiple environment issues + for (let i = 0; i < 20; i++) { + controller.recordEnvironment('zone-001', createCriticalReadings()); + } + + // Health should be significantly reduced + expect(batch.healthScore).toBeLessThan(50); + }); + }); +}); + +// Helper functions +function createVerticalFarm(id: string): VerticalFarm { + return { + id, + name: 'Test Vertical Farm', + ownerId: 'owner-001', + location: { + latitude: 40.7128, + longitude: -74.006, + address: '123 Farm Street', + city: 'New York', + country: 'USA', + timezone: 'America/New_York', + }, + specs: { + totalAreaSqm: 500, + growingAreaSqm: 400, + numberOfLevels: 4, + ceilingHeightM: 3, + totalGrowingPositions: 4000, + currentActivePlants: 0, + powerCapacityKw: 100, + waterStorageL: 5000, + backupPowerHours: 24, + certifications: ['organic'], + buildingType: 'warehouse', + insulation: 'high_efficiency', + }, + zones: [createGrowingZone('zone-001', 'Zone A', 1)], + environmentalControl: { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, + airCirculation: { fans: [] }, + controlMode: 'adaptive', + }, + irrigationSystem: { + type: 'recirculating', + freshWaterTankL: 2000, + freshWaterLevelL: 1800, + nutrientTankL: 500, + nutrientLevelL: 450, + wasteTankL: 200, + wasteLevelL: 50, + waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, + pumps: [], + irrigationSchedule: [], + }, + lightingSystem: { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 5000, + currentWattage: 3000, + efficacyUmolJ: 2.5, + }, + nutrientSystem: { + mixingMethod: 'fully_auto', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, + dosingRatiosMlPerL: [], + }, + monitoring: { + ec: 1.5, + ph: 6.0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + automationLevel: 'semi_automated', + automationSystems: [], + status: 'operational', + operationalSince: '2024-01-01', + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 75, + averageYieldEfficiency: 85, + energyEfficiencyScore: 80, + }; +} + +function createGrowingZone(id: string, name: string, level: number): GrowingZone { + return { + id, + name, + level, + areaSqm: 50, + lengthM: 10, + widthM: 5, + growingMethod: 'NFT', + plantPositions: 500, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: { + temperatureC: { min: 18, max: 24, target: 21 }, + humidityPercent: { min: 60, max: 80, target: 70 }, + co2Ppm: { min: 800, max: 1200, target: 1000 }, + lightPpfd: { min: 200, max: 400, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, + nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, + waterTempC: { min: 18, max: 22, target: 20 }, + }, + currentEnvironment: { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }, + status: 'empty', + }; +} + +function createLocation(type: string, name: string): TransportLocation { + return { + latitude: 40.7 + Math.random() * 0.1, + longitude: -74.0 + Math.random() * 0.1, + locationType: type as any, + facilityName: name, + }; +} + +function createGoodReadings(): ZoneEnvironmentReadings { + return { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }; +} + +function createBadReadings(): ZoneEnvironmentReadings { + return { + timestamp: new Date().toISOString(), + temperatureC: 28, // Too high + humidityPercent: 55, // Too low + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }; +} + +function createCriticalReadings(): ZoneEnvironmentReadings { + return { + timestamp: new Date().toISOString(), + temperatureC: 35, // Critical high + humidityPercent: 40, // Critical low + co2Ppm: 500, // Low + vpd: 2.5, + ppfd: 100, // Low + dli: 5, + waterTempC: 30, // Too high + ec: 0.5, // Too low + ph: 7.5, // Too high + dissolvedOxygenPpm: 4, + airflowMs: 0.1, + alerts: [], + }; +} diff --git a/__tests__/lib/demand/aggregation.test.ts b/__tests__/lib/demand/aggregation.test.ts new file mode 100644 index 0000000..805607a --- /dev/null +++ b/__tests__/lib/demand/aggregation.test.ts @@ -0,0 +1,374 @@ +/** + * Demand Aggregation Tests + * Tests for demand aggregation logic + */ + +import { + DemandForecaster, +} from '../../../lib/demand/forecaster'; +import { + ConsumerPreference, + DemandSignal, + DemandItem, +} from '../../../lib/demand/types'; + +describe('Demand Aggregation', () => { + let forecaster: DemandForecaster; + + beforeEach(() => { + forecaster = new DemandForecaster(); + }); + + describe('Consumer Aggregation', () => { + it('should aggregate demand from multiple consumers', () => { + // Add 5 consumers all wanting lettuce + for (let i = 0; i < 5; i++) { + const pref = createConsumerPreference(`consumer-${i}`); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + pref.householdSize = 2; + forecaster.registerPreference(pref); + } + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test Region', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.consumerCount).toBe(5); + // Total demand = 5 consumers * 1kg/week * 2 household = 10 kg + expect(lettuceItem?.weeklyDemandKg).toBe(10); + }); + + it('should aggregate different produce types separately', () => { + const pref1 = createConsumerPreference('consumer-1'); + pref1.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref1); + + const pref2 = createConsumerPreference('consumer-2'); + pref2.preferredItems = [ + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref2); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + const tomatoItem = signal.demandItems.find(i => i.produceType === 'tomato'); + + expect(lettuceItem).toBeDefined(); + expect(tomatoItem).toBeDefined(); + expect(lettuceItem?.consumerCount).toBe(1); + expect(tomatoItem?.consumerCount).toBe(1); + }); + + it('should calculate monthly demand from weekly', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.monthlyDemandKg).toBe(lettuceItem!.weeklyDemandKg * 4); + }); + }); + + describe('Priority Aggregation', () => { + it('should calculate aggregate priority from consumer priorities', () => { + const pref1 = createConsumerPreference('consumer-1'); + pref1.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref1); + + const pref2 = createConsumerPreference('consumer-2'); + pref2.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref2); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + // must_have=10, occasional=2, average = 6 + expect(lettuceItem?.aggregatePriority).toBe(6); + }); + + it('should determine urgency from aggregate priority', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.urgency).toBe('immediate'); + }); + + it('should sort demand items by priority', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 1, seasonalOnly: false }, + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + // Tomato (must_have) should come before lettuce (occasional) + expect(signal.demandItems[0].produceType).toBe('tomato'); + }); + }); + + describe('Certification Aggregation', () => { + it('should aggregate certification preferences', () => { + const pref1 = createConsumerPreference('consumer-1'); + pref1.certificationPreferences = ['organic']; + forecaster.registerPreference(pref1); + + const pref2 = createConsumerPreference('consumer-2'); + pref2.certificationPreferences = ['organic', 'local']; + forecaster.registerPreference(pref2); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + // Check that certification preferences are aggregated + signal.demandItems.forEach(item => { + expect(item.preferredCertifications).toContain('organic'); + }); + }); + }); + + describe('Price Aggregation', () => { + it('should calculate average willing price', () => { + const pref1 = createConsumerPreference('consumer-1'); + pref1.weeklyBudget = 100; + pref1.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, + ]; + pref1.householdSize = 1; + forecaster.registerPreference(pref1); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.averageWillingPrice).toBeGreaterThan(0); + expect(lettuceItem?.priceUnit).toBe('per_kg'); + }); + }); + + describe('Supply Gap Calculation', () => { + it('should calculate supply gap when no supply', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.gapKg).toBe(10); + expect(lettuceItem?.matchedSupply).toBe(0); + }); + + it('should reduce gap when supply is available', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + // Add partial supply + forecaster.registerSupply({ + id: 'supply-1', + growerId: 'grower-1', + timestamp: new Date().toISOString(), + produceType: 'lettuce', + committedQuantityKg: 5, + availableFrom: new Date().toISOString(), + availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + pricePerKg: 5, + currency: 'USD', + minimumOrderKg: 1, + certifications: [], + freshnessGuaranteeHours: 48, + deliveryRadiusKm: 50, + deliveryMethods: ['grower_delivery'], + status: 'available', + remainingKg: 5, + }); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.matchedSupply).toBe(5); + expect(lettuceItem?.gapKg).toBe(5); + }); + + it('should show surplus when supply exceeds demand', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + // Add excess supply + forecaster.registerSupply({ + id: 'supply-1', + growerId: 'grower-1', + timestamp: new Date().toISOString(), + produceType: 'lettuce', + committedQuantityKg: 20, + availableFrom: new Date().toISOString(), + availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + pricePerKg: 5, + currency: 'USD', + minimumOrderKg: 1, + certifications: [], + freshnessGuaranteeHours: 48, + deliveryRadiusKm: 50, + deliveryMethods: ['grower_delivery'], + status: 'available', + remainingKg: 20, + }); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + expect(signal.supplyStatus).toBe('surplus'); + }); + }); + + describe('Seasonal Availability', () => { + it('should mark items as in-season correctly', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + // Summer - tomato in season, lettuce depends + const summerSignal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const tomatoItem = summerSignal.demandItems.find(i => i.produceType === 'tomato'); + expect(tomatoItem?.inSeason).toBe(true); + }); + + it('should include seasonal availability info', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + expect(lettuceItem?.seasonalAvailability).toBeDefined(); + expect(lettuceItem?.seasonalAvailability.spring).toBeDefined(); + expect(lettuceItem?.seasonalAvailability.summer).toBeDefined(); + expect(lettuceItem?.seasonalAvailability.fall).toBeDefined(); + expect(lettuceItem?.seasonalAvailability.winter).toBeDefined(); + }); + }); + + describe('Confidence Level', () => { + it('should increase confidence with more consumers', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + + const signal1 = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + forecaster.registerPreference(createConsumerPreference('consumer-2')); + forecaster.registerPreference(createConsumerPreference('consumer-3')); + + const signal2 = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + expect(signal2.confidenceLevel).toBeGreaterThan(signal1.confidenceLevel); + }); + + it('should cap confidence at 100', () => { + // Add many consumers + for (let i = 0; i < 100; i++) { + forecaster.registerPreference(createConsumerPreference(`consumer-${i}`)); + } + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + expect(signal.confidenceLevel).toBeLessThanOrEqual(100); + }); + }); +}); + +// Helper function +function createConsumerPreference(consumerId: string): ConsumerPreference { + return { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: 40.7128, + longitude: -74.006, + maxDeliveryRadiusKm: 25, + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens'], + preferredItems: [], + certificationPreferences: ['organic'], + freshnessImportance: 4, + priceImportance: 3, + sustainabilityImportance: 4, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 2, + weeklyBudget: 100, + currency: 'USD', + }; +} diff --git a/__tests__/lib/demand/forecaster.test.ts b/__tests__/lib/demand/forecaster.test.ts new file mode 100644 index 0000000..64ced1d --- /dev/null +++ b/__tests__/lib/demand/forecaster.test.ts @@ -0,0 +1,298 @@ +/** + * DemandForecaster Tests + * Tests for the demand forecasting system + */ + +import { + DemandForecaster, + getDemandForecaster, +} from '../../../lib/demand/forecaster'; +import { + ConsumerPreference, + SupplyCommitment, + DemandSignal, + PlantingRecommendation, +} from '../../../lib/demand/types'; + +describe('DemandForecaster', () => { + let forecaster: DemandForecaster; + + beforeEach(() => { + forecaster = new DemandForecaster(); + }); + + describe('Initialization', () => { + it('should create empty forecaster', () => { + const json = forecaster.toJSON() as any; + expect(json.preferences).toEqual([]); + expect(json.supplyCommitments).toEqual([]); + expect(json.demandSignals).toEqual([]); + }); + }); + + describe('Consumer Preferences', () => { + it('should register consumer preferences', () => { + const preference = createConsumerPreference('consumer-1'); + forecaster.registerPreference(preference); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(1); + }); + + it('should update existing preference', () => { + const preference1 = createConsumerPreference('consumer-1'); + preference1.householdSize = 2; + forecaster.registerPreference(preference1); + + const preference2 = createConsumerPreference('consumer-1'); + preference2.householdSize = 4; + forecaster.registerPreference(preference2); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(1); + expect(json.preferences[0][1].householdSize).toBe(4); + }); + + it('should register multiple consumers', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + forecaster.registerPreference(createConsumerPreference('consumer-2')); + forecaster.registerPreference(createConsumerPreference('consumer-3')); + + const json = forecaster.toJSON() as any; + expect(json.preferences.length).toBe(3); + }); + }); + + describe('Supply Commitments', () => { + it('should register supply commitment', () => { + const commitment = createSupplyCommitment('grower-1', 'lettuce', 50); + forecaster.registerSupply(commitment); + + const json = forecaster.toJSON() as any; + expect(json.supplyCommitments.length).toBe(1); + }); + + it('should track multiple supply commitments', () => { + forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50)); + forecaster.registerSupply(createSupplyCommitment('grower-2', 'tomato', 100)); + + const json = forecaster.toJSON() as any; + expect(json.supplyCommitments.length).toBe(2); + }); + }); + + describe('Demand Signal Generation', () => { + it('should generate demand signal for region', () => { + // Add consumers + forecaster.registerPreference(createConsumerPreference('consumer-1', 40.7, -74.0)); + forecaster.registerPreference(createConsumerPreference('consumer-2', 40.71, -74.01)); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 50, 'New York Metro', 'summer' + ); + + expect(signal.id).toBeDefined(); + expect(signal.region.name).toBe('New York Metro'); + expect(signal.totalConsumers).toBe(2); + }); + + it('should filter consumers by region radius', () => { + // Consumer inside radius + forecaster.registerPreference(createConsumerPreference('inside', 40.7, -74.0)); + + // Consumer outside radius (far away) + forecaster.registerPreference(createConsumerPreference('outside', 35.0, -80.0)); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 10, 'Small Region', 'summer' + ); + + expect(signal.totalConsumers).toBe(1); + }); + + it('should aggregate demand items', () => { + const pref1 = createConsumerPreference('consumer-1'); + pref1.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + { produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref1); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test Region', 'summer' + ); + + expect(signal.demandItems.length).toBeGreaterThan(0); + }); + + it('should calculate supply status', () => { + // Add consumer demand + forecaster.registerPreference(createConsumerPreference('consumer-1')); + + // No supply - should show shortage + let signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + expect(['shortage', 'critical', 'balanced']).toContain(signal.supplyStatus); + }); + + it('should include seasonal period', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'winter' + ); + + expect(signal.seasonalPeriod).toBe('winter'); + }); + + it('should calculate weekly demand correctly', () => { + const pref = createConsumerPreference('consumer-1'); + pref.householdSize = 4; + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + const signal = forecaster.generateDemandSignal( + 40.7, -74.0, 100, 'Test', 'summer' + ); + + const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); + // Weekly demand = weeklyQuantity * householdSize = 2 * 4 = 8 + expect(lettuceItem?.weeklyDemandKg).toBe(8); + }); + }); + + describe('Demand Forecasting', () => { + it('should generate forecast for region', () => { + // Setup some historical data + forecaster.registerPreference(createConsumerPreference('consumer-1')); + forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test Region', 'summer'); + + const forecast = forecaster.generateForecast('Test Region', 12); + + expect(forecast.id).toBeDefined(); + expect(forecast.region).toBe('Test Region'); + expect(forecast.forecasts).toBeDefined(); + }); + + it('should include confidence intervals', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer'); + + const forecast = forecaster.generateForecast('Test', 12); + + forecast.forecasts.forEach(f => { + expect(f.confidenceInterval.low).toBeLessThanOrEqual(f.predictedDemandKg); + expect(f.confidenceInterval.high).toBeGreaterThanOrEqual(f.predictedDemandKg); + }); + }); + + it('should identify trends', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer'); + + const forecast = forecaster.generateForecast('Test', 12); + + forecast.forecasts.forEach(f => { + expect(['increasing', 'stable', 'decreasing']).toContain(f.trend); + }); + }); + }); + + describe('Singleton', () => { + it('should return same instance from getDemandForecaster', () => { + const forecaster1 = getDemandForecaster(); + const forecaster2 = getDemandForecaster(); + expect(forecaster1).toBe(forecaster2); + }); + }); + + describe('Serialization', () => { + it('should export to JSON', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + + const json = forecaster.toJSON(); + expect(json).toHaveProperty('preferences'); + expect(json).toHaveProperty('supplyCommitments'); + expect(json).toHaveProperty('demandSignals'); + }); + + it('should import from JSON', () => { + forecaster.registerPreference(createConsumerPreference('consumer-1')); + forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50)); + + const json = forecaster.toJSON(); + const restored = DemandForecaster.fromJSON(json); + + const restoredJson = restored.toJSON() as any; + expect(restoredJson.preferences.length).toBe(1); + expect(restoredJson.supplyCommitments.length).toBe(1); + }); + }); +}); + +// Helper functions +function createConsumerPreference( + consumerId: string, + lat: number = 40.7128, + lon: number = -74.006 +): ConsumerPreference { + return { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: lat, + longitude: lon, + maxDeliveryRadiusKm: 25, + city: 'New York', + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens', 'nightshades'], + preferredItems: [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, + { produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false }, + ], + certificationPreferences: ['organic', 'local'], + freshnessImportance: 5, + priceImportance: 3, + sustainabilityImportance: 4, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 3, + weeklyBudget: 100, + currency: 'USD', + }; +} + +function createSupplyCommitment( + growerId: string, + produceType: string, + quantity: number +): SupplyCommitment { + return { + id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + growerId, + timestamp: new Date().toISOString(), + produceType, + committedQuantityKg: quantity, + availableFrom: new Date().toISOString(), + availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + pricePerKg: 5, + currency: 'USD', + minimumOrderKg: 1, + certifications: ['organic'], + freshnessGuaranteeHours: 48, + deliveryRadiusKm: 50, + deliveryMethods: ['grower_delivery', 'customer_pickup'], + status: 'available', + remainingKg: quantity, + }; +} diff --git a/__tests__/lib/demand/recommendations.test.ts b/__tests__/lib/demand/recommendations.test.ts new file mode 100644 index 0000000..b58d57f --- /dev/null +++ b/__tests__/lib/demand/recommendations.test.ts @@ -0,0 +1,451 @@ +/** + * Recommendation Logic Tests + * Tests for planting recommendation generation + */ + +import { + DemandForecaster, +} from '../../../lib/demand/forecaster'; +import { + ConsumerPreference, + PlantingRecommendation, + RiskFactor, +} from '../../../lib/demand/types'; + +describe('Planting Recommendations', () => { + let forecaster: DemandForecaster; + + beforeEach(() => { + forecaster = new DemandForecaster(); + }); + + describe('Recommendation Generation', () => { + it('should generate recommendations based on demand signals', () => { + // Create demand + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + // Generate demand signal + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test Region', 'spring'); + + // Generate recommendations + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', + 40.7, + -74.0, + 50, + 100, // 100 sqm available + 'spring' + ); + + expect(recommendations.length).toBeGreaterThan(0); + }); + + it('should include recommendation details', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + const rec = recommendations[0]; + expect(rec.id).toBeDefined(); + expect(rec.growerId).toBe('grower-1'); + expect(rec.produceType).toBeDefined(); + expect(rec.recommendedQuantity).toBeGreaterThan(0); + expect(rec.expectedYieldKg).toBeGreaterThan(0); + }); + + it('should not exceed available space', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1000, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const availableSpace = 50; // Only 50 sqm + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, availableSpace, 'spring' + ); + + const totalSpace = recommendations.reduce((sum, r) => sum + r.recommendedQuantity, 0); + expect(totalSpace).toBeLessThanOrEqual(availableSpace); + }); + + it('should prioritize high-demand produce', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 50, seasonalOnly: false }, + { produceType: 'spinach', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 5, seasonalOnly: false }, + ]; + pref.householdSize = 1; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 30, 'spring' // Limited space + ); + + // Should prioritize lettuce (high demand) over spinach + if (recommendations.length > 0) { + expect(recommendations[0].produceType).toBe('lettuce'); + } + }); + }); + + describe('Timing Calculations', () => { + it('should include planting and harvest dates', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + const rec = recommendations[0]; + expect(rec.plantByDate).toBeDefined(); + expect(rec.expectedHarvestStart).toBeDefined(); + expect(rec.expectedHarvestEnd).toBeDefined(); + expect(rec.growingDays).toBeGreaterThan(0); + }); + + it('should calculate harvest date based on growing days', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + const rec = recommendations[0]; + const plantDate = new Date(rec.plantByDate); + const harvestStart = new Date(rec.expectedHarvestStart); + + const daysDiff = (harvestStart.getTime() - plantDate.getTime()) / (1000 * 60 * 60 * 24); + expect(daysDiff).toBeCloseTo(rec.growingDays, 0); + }); + }); + + describe('Financial Projections', () => { + it('should calculate projected revenue', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + pref.weeklyBudget = 100; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + const rec = recommendations[0]; + expect(rec.projectedPricePerKg).toBeGreaterThan(0); + expect(rec.projectedRevenue).toBeGreaterThan(0); + expect(rec.projectedRevenue).toBeCloseTo( + rec.expectedYieldKg * rec.projectedPricePerKg, + -1 // Allow some rounding + ); + }); + + it('should include market confidence', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + const rec = recommendations[0]; + expect(rec.marketConfidence).toBeGreaterThanOrEqual(0); + expect(rec.marketConfidence).toBeLessThanOrEqual(100); + }); + }); + + describe('Risk Assessment', () => { + it('should include risk factors', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'summer' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + expect(rec.riskFactors).toBeDefined(); + expect(Array.isArray(rec.riskFactors)).toBe(true); + } + }); + + it('should calculate overall risk level', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + expect(['low', 'medium', 'high']).toContain(rec.overallRisk); + } + }); + + it('should include risk mitigation suggestions', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 10, 'summer' // Small space for large demand + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + rec.riskFactors.forEach(risk => { + if (risk.mitigationSuggestion) { + expect(typeof risk.mitigationSuggestion).toBe('string'); + } + }); + } + }); + }); + + describe('Seasonal Filtering', () => { + it('should only recommend in-season produce', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + // Winter - tomato is not in season + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'winter'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'winter' + ); + + // Tomato should not be recommended in winter + const tomatoRec = recommendations.find(r => r.produceType === 'tomato'); + expect(tomatoRec).toBeUndefined(); + }); + }); + + describe('Demand Signal Matching', () => { + it('should filter signals by delivery radius', () => { + // Consumer far from grower + const pref = createConsumerPreference('consumer-1'); + pref.location = { latitude: 35.0, longitude: -80.0, maxDeliveryRadiusKm: 10 }; + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(35.0, -80.0, 10, 'Distant Region', 'spring'); + + // Grower is far from demand signal + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', + 40.7, // New York area + -74.0, + 25, // Only 25km delivery radius + 100, + 'spring' + ); + + // Should have no recommendations because demand is too far + expect(recommendations.length).toBe(0); + }); + + it('should match signals from multiple regions', () => { + // Multiple consumers in nearby regions + const pref1 = createConsumerPreference('consumer-1'); + pref1.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref1); + + const pref2 = createConsumerPreference('consumer-2'); + pref2.location = { latitude: 40.72, longitude: -74.01, maxDeliveryRadiusKm: 25 }; + pref2.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref2); + + forecaster.generateDemandSignal(40.7, -74.0, 20, 'Region 1', 'spring'); + forecaster.generateDemandSignal(40.75, -74.05, 20, 'Region 2', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + // Should aggregate demand from both regions + expect(recommendations.length).toBeGreaterThan(0); + }); + }); + + describe('Yield Calculations', () => { + it('should calculate expected yield based on area', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + // Lettuce yields about 4 kg/sqm according to SEASONAL_DATA + expect(rec.expectedYieldKg).toBeGreaterThan(0); + expect(rec.yieldConfidence).toBeGreaterThan(0); + } + }); + + it('should include yield confidence', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + expect(rec.yieldConfidence).toBeGreaterThanOrEqual(0); + expect(rec.yieldConfidence).toBeLessThanOrEqual(100); + } + }); + }); + + describe('Explanation', () => { + it('should include human-readable explanation', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + expect(rec.explanation).toBeDefined(); + expect(rec.explanation.length).toBeGreaterThan(20); + expect(rec.explanation).toContain('demand signal'); + } + }); + + it('should include demand signal IDs', () => { + const pref = createConsumerPreference('consumer-1'); + pref.preferredItems = [ + { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, + ]; + forecaster.registerPreference(pref); + + forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); + + const recommendations = forecaster.generatePlantingRecommendations( + 'grower-1', 40.7, -74.0, 50, 100, 'spring' + ); + + if (recommendations.length > 0) { + const rec = recommendations[0]; + expect(rec.demandSignalIds).toBeDefined(); + expect(rec.demandSignalIds.length).toBeGreaterThan(0); + } + }); + }); +}); + +// Helper function +function createConsumerPreference(consumerId: string): ConsumerPreference { + return { + consumerId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + location: { + latitude: 40.7128, + longitude: -74.006, + maxDeliveryRadiusKm: 25, + }, + dietaryType: ['omnivore'], + allergies: [], + dislikes: [], + preferredCategories: ['leafy_greens'], + preferredItems: [], + certificationPreferences: ['organic'], + freshnessImportance: 4, + priceImportance: 3, + sustainabilityImportance: 4, + deliveryPreferences: { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'], + }, + householdSize: 2, + weeklyBudget: 100, + currency: 'USD', + }; +} diff --git a/__tests__/lib/transport/carbon.test.ts b/__tests__/lib/transport/carbon.test.ts new file mode 100644 index 0000000..009a409 --- /dev/null +++ b/__tests__/lib/transport/carbon.test.ts @@ -0,0 +1,361 @@ +/** + * Carbon Calculation Tests + * Tests for carbon footprint calculation logic + */ + +import { TransportChain } from '../../../lib/transport/tracker'; +import { + CARBON_FACTORS, + TransportMethod, + TransportLocation, +} from '../../../lib/transport/types'; + +describe('Carbon Footprint Calculation', () => { + let chain: TransportChain; + + beforeEach(() => { + chain = new TransportChain(1); + }); + + describe('CARBON_FACTORS', () => { + it('should have zero emissions for walking', () => { + expect(CARBON_FACTORS.walking).toBe(0); + }); + + it('should have zero emissions for bicycle', () => { + expect(CARBON_FACTORS.bicycle).toBe(0); + }); + + it('should have low emissions for electric vehicles', () => { + expect(CARBON_FACTORS.electric_vehicle).toBeLessThan(CARBON_FACTORS.gasoline_vehicle); + expect(CARBON_FACTORS.electric_truck).toBeLessThan(CARBON_FACTORS.diesel_truck); + }); + + it('should have high emissions for air transport', () => { + expect(CARBON_FACTORS.air).toBeGreaterThan(CARBON_FACTORS.diesel_truck); + expect(CARBON_FACTORS.air).toBeGreaterThan(CARBON_FACTORS.ship); + }); + + it('should have moderate emissions for refrigerated trucks', () => { + expect(CARBON_FACTORS.refrigerated_truck).toBeGreaterThan(CARBON_FACTORS.diesel_truck); + }); + + it('should have lowest emissions for rail and ship', () => { + expect(CARBON_FACTORS.rail).toBeLessThan(CARBON_FACTORS.diesel_truck); + expect(CARBON_FACTORS.ship).toBeLessThan(CARBON_FACTORS.diesel_truck); + }); + + it('should define all transport methods', () => { + const methods: TransportMethod[] = [ + 'walking', 'bicycle', 'electric_vehicle', 'hybrid_vehicle', + 'gasoline_vehicle', 'diesel_truck', 'electric_truck', + 'refrigerated_truck', 'rail', 'ship', 'air', 'drone', + 'local_delivery', 'customer_pickup' + ]; + + methods.forEach(method => { + expect(CARBON_FACTORS[method]).toBeDefined(); + expect(typeof CARBON_FACTORS[method]).toBe('number'); + }); + }); + }); + + describe('calculateCarbon', () => { + it('should calculate zero carbon for zero distance', () => { + const carbon = chain.calculateCarbon('diesel_truck', 0, 10); + expect(carbon).toBe(0); + }); + + it('should calculate zero carbon for zero weight', () => { + const carbon = chain.calculateCarbon('diesel_truck', 100, 0); + expect(carbon).toBe(0); + }); + + it('should calculate zero carbon for walking', () => { + const carbon = chain.calculateCarbon('walking', 10, 5); + expect(carbon).toBe(0); + }); + + it('should calculate correct carbon for known values', () => { + // diesel_truck = 0.15 kg CO2 per km per kg cargo + const carbon = chain.calculateCarbon('diesel_truck', 100, 10); + expect(carbon).toBe(0.15 * 100 * 10); + }); + + it('should increase linearly with distance', () => { + const carbon1 = chain.calculateCarbon('diesel_truck', 50, 10); + const carbon2 = chain.calculateCarbon('diesel_truck', 100, 10); + expect(carbon2).toBe(carbon1 * 2); + }); + + it('should increase linearly with weight', () => { + const carbon1 = chain.calculateCarbon('diesel_truck', 100, 5); + const carbon2 = chain.calculateCarbon('diesel_truck', 100, 10); + expect(carbon2).toBe(carbon1 * 2); + }); + + it('should compare transport methods correctly', () => { + const distance = 100; + const weight = 10; + + const walking = chain.calculateCarbon('walking', distance, weight); + const electric = chain.calculateCarbon('electric_vehicle', distance, weight); + const gasoline = chain.calculateCarbon('gasoline_vehicle', distance, weight); + const diesel = chain.calculateCarbon('diesel_truck', distance, weight); + const air = chain.calculateCarbon('air', distance, weight); + + expect(walking).toBe(0); + expect(electric).toBeLessThan(gasoline); + expect(gasoline).toBeLessThan(diesel); + expect(diesel).toBeLessThan(air); + }); + }); + + describe('Distance Calculation', () => { + it('should calculate zero distance for same location', () => { + const location: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + locationType: 'farm', + }; + + const distance = TransportChain.calculateDistance(location, location); + expect(distance).toBe(0); + }); + + it('should calculate correct distance for known locations', () => { + // New York to Los Angeles is approximately 3940 km + const newYork: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + locationType: 'warehouse', + }; + + const losAngeles: TransportLocation = { + latitude: 34.0522, + longitude: -118.2437, + locationType: 'warehouse', + }; + + const distance = TransportChain.calculateDistance(newYork, losAngeles); + expect(distance).toBeGreaterThan(3900); + expect(distance).toBeLessThan(4000); + }); + + it('should calculate short distances accurately', () => { + // Two points 1km apart (approximately) + const point1: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + locationType: 'farm', + }; + + const point2: TransportLocation = { + latitude: 40.7218, // ~1km north + longitude: -74.006, + locationType: 'farm', + }; + + const distance = TransportChain.calculateDistance(point1, point2); + expect(distance).toBeGreaterThan(0.9); + expect(distance).toBeLessThan(1.1); + }); + + it('should be symmetric', () => { + const pointA: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + locationType: 'farm', + }; + + const pointB: TransportLocation = { + latitude: 34.0522, + longitude: -118.2437, + locationType: 'warehouse', + }; + + const distanceAB = TransportChain.calculateDistance(pointA, pointB); + const distanceBA = TransportChain.calculateDistance(pointB, pointA); + + expect(distanceAB).toBeCloseTo(distanceBA, 5); + }); + + it('should handle crossing the prime meridian', () => { + const london: TransportLocation = { + latitude: 51.5074, + longitude: -0.1278, + locationType: 'warehouse', + }; + + const paris: TransportLocation = { + latitude: 48.8566, + longitude: 2.3522, + locationType: 'warehouse', + }; + + const distance = TransportChain.calculateDistance(london, paris); + // London to Paris is approximately 344 km + expect(distance).toBeGreaterThan(330); + expect(distance).toBeLessThan(360); + }); + + it('should handle crossing the equator', () => { + const north: TransportLocation = { + latitude: 10, + longitude: 0, + locationType: 'farm', + }; + + const south: TransportLocation = { + latitude: -10, + longitude: 0, + locationType: 'farm', + }; + + const distance = TransportChain.calculateDistance(north, south); + // 20 degrees of latitude is approximately 2222 km + expect(distance).toBeGreaterThan(2200); + expect(distance).toBeLessThan(2250); + }); + }); + + describe('Carbon Tracking in Events', () => { + it('should auto-calculate carbon if not provided', () => { + const event = { + id: 'test-carbon-auto', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition' as const, + fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const }, + toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const }, + distanceKm: 100, + durationMinutes: 120, + transportMethod: 'diesel_truck' as const, + carbonFootprintKg: 0, // Will be auto-calculated + senderId: 'sender', + receiverId: 'receiver', + status: 'verified' as const, + seedBatchId: 'batch-001', + sourceType: 'seed_bank' as const, + species: 'Test', + quantity: 100, + quantityUnit: 'seeds' as const, + generation: 1, + }; + + const block = chain.recordEvent(event); + expect(block.transportEvent.carbonFootprintKg).toBeGreaterThan(0); + }); + + it('should accumulate carbon across chain', () => { + const event1 = { + id: 'carbon-chain-1', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition' as const, + fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const }, + toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const }, + distanceKm: 100, + durationMinutes: 120, + transportMethod: 'diesel_truck' as const, + carbonFootprintKg: 0, + senderId: 'sender', + receiverId: 'receiver', + status: 'verified' as const, + seedBatchId: 'batch-001', + sourceType: 'seed_bank' as const, + species: 'Test', + quantity: 100, + quantityUnit: 'seeds' as const, + generation: 1, + }; + + const event2 = { ...event1, id: 'carbon-chain-2', distanceKm: 200 }; + + const block1 = chain.recordEvent(event1); + const block2 = chain.recordEvent(event2); + + expect(block2.cumulativeCarbonKg).toBeGreaterThan(block1.cumulativeCarbonKg); + expect(block2.cumulativeCarbonKg).toBeCloseTo( + block1.cumulativeCarbonKg + block2.transportEvent.carbonFootprintKg, + 5 + ); + }); + }); + + describe('Environmental Impact Calculations', () => { + it('should calculate carbon savings vs conventional', () => { + const userId = 'eco-user'; + + const event = { + id: 'eco-event', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition' as const, + fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const }, + toLocation: { latitude: 40.1, longitude: -74, locationType: 'farm' as const }, + distanceKm: 10, // Very short local distance + durationMinutes: 20, + transportMethod: 'bicycle' as const, // Zero carbon + carbonFootprintKg: 0, + senderId: userId, + receiverId: userId, + status: 'verified' as const, + seedBatchId: 'batch-eco', + sourceType: 'seed_bank' as const, + species: 'Test', + quantity: 1000, + quantityUnit: 'grams' as const, + generation: 1, + }; + + chain.recordEvent(event); + const impact = chain.getEnvironmentalImpact(userId); + + // Local, zero-carbon transport should save significantly vs conventional + expect(impact.comparisonToConventional.carbonSaved).toBeGreaterThan(0); + expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0); + expect(impact.comparisonToConventional.percentageReduction).toBeGreaterThan(0); + }); + + it('should break down impact by transport method', () => { + const userId = 'breakdown-user'; + + // Record multiple events with different methods + const methods: TransportMethod[] = ['bicycle', 'electric_vehicle', 'diesel_truck']; + + methods.forEach((method, i) => { + chain.recordEvent({ + id: `breakdown-${i}`, + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition' as const, + fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const }, + toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const }, + distanceKm: 50, + durationMinutes: 60, + transportMethod: method, + carbonFootprintKg: 0, + senderId: userId, + receiverId: userId, + status: 'verified' as const, + seedBatchId: `batch-${i}`, + sourceType: 'seed_bank' as const, + species: 'Test', + quantity: 100, + quantityUnit: 'seeds' as const, + generation: 1, + }); + }); + + const impact = chain.getEnvironmentalImpact(userId); + + expect(impact.breakdownByMethod['bicycle']).toBeDefined(); + expect(impact.breakdownByMethod['electric_vehicle']).toBeDefined(); + expect(impact.breakdownByMethod['diesel_truck']).toBeDefined(); + + // Bicycle should have zero carbon + expect(impact.breakdownByMethod['bicycle'].carbon).toBe(0); + + // Diesel should have highest carbon + expect(impact.breakdownByMethod['diesel_truck'].carbon) + .toBeGreaterThan(impact.breakdownByMethod['electric_vehicle'].carbon); + }); + }); +}); diff --git a/__tests__/lib/transport/tracker.test.ts b/__tests__/lib/transport/tracker.test.ts new file mode 100644 index 0000000..52b6691 --- /dev/null +++ b/__tests__/lib/transport/tracker.test.ts @@ -0,0 +1,471 @@ +/** + * TransportChain Tests + * Tests for the transport tracking blockchain implementation + */ + +import { + TransportChain, + getTransportChain, + setTransportChain, +} from '../../../lib/transport/tracker'; +import { + SeedAcquisitionEvent, + PlantingEvent, + GrowingTransportEvent, + HarvestEvent, + SeedSavingEvent, + TransportLocation, + CARBON_FACTORS, +} from '../../../lib/transport/types'; + +describe('TransportChain', () => { + let chain: TransportChain; + + beforeEach(() => { + chain = new TransportChain(2); // Lower difficulty for faster tests + }); + + describe('Initialization', () => { + it('should create genesis block on initialization', () => { + expect(chain.chain.length).toBe(1); + expect(chain.chain[0].index).toBe(0); + expect(chain.chain[0].previousHash).toBe('0'); + expect(chain.chain[0].transportEvent.eventType).toBe('seed_acquisition'); + }); + + it('should set default difficulty', () => { + const defaultChain = new TransportChain(); + expect(defaultChain.difficulty).toBe(3); + }); + + it('should allow custom difficulty', () => { + expect(chain.difficulty).toBe(2); + }); + }); + + describe('Recording Events', () => { + it('should record seed acquisition event', () => { + const event: SeedAcquisitionEvent = createSeedAcquisitionEvent(); + const block = chain.recordEvent(event); + + expect(block.index).toBe(1); + expect(block.transportEvent.eventType).toBe('seed_acquisition'); + expect(chain.chain.length).toBe(2); + }); + + it('should record planting event', () => { + const seedEvent = createSeedAcquisitionEvent(); + chain.recordEvent(seedEvent); + + const plantingEvent: PlantingEvent = createPlantingEvent(); + const block = chain.recordEvent(plantingEvent); + + expect(block.transportEvent.eventType).toBe('planting'); + expect((block.transportEvent as PlantingEvent).plantIds.length).toBe(2); + }); + + it('should record growing transport event', () => { + const seedEvent = createSeedAcquisitionEvent(); + chain.recordEvent(seedEvent); + + const plantingEvent = createPlantingEvent(); + chain.recordEvent(plantingEvent); + + const growingEvent: GrowingTransportEvent = { + id: 'growing-001', + timestamp: new Date().toISOString(), + eventType: 'growing_transport', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('farm'), + distanceKm: 5, + durationMinutes: 30, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + plantIds: ['plant-001', 'plant-002'], + reason: 'transplant', + plantStage: 'seedling', + handlingMethod: 'potted', + rootDisturbance: 'minimal', + acclimatizationRequired: true, + acclimatizationDays: 3, + }; + + const block = chain.recordEvent(growingEvent); + expect(block.transportEvent.eventType).toBe('growing_transport'); + }); + + it('should record harvest event', () => { + const harvestEvent: HarvestEvent = { + id: 'harvest-001', + timestamp: new Date().toISOString(), + eventType: 'harvest', + fromLocation: createLocation('farm'), + toLocation: createLocation('warehouse'), + distanceKm: 10, + durationMinutes: 45, + transportMethod: 'electric_truck', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'distributor-1', + status: 'verified', + plantIds: ['plant-001', 'plant-002'], + harvestBatchId: 'harvest-batch-001', + harvestType: 'full', + produceType: 'tomatoes', + grossWeight: 10, + netWeight: 9.5, + weightUnit: 'kg', + packagingType: 'crates', + temperatureRequired: { min: 10, max: 15, optimal: 12, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: false, + }; + + const block = chain.recordEvent(harvestEvent); + expect(block.transportEvent.eventType).toBe('harvest'); + expect((block.transportEvent as HarvestEvent).netWeight).toBe(9.5); + }); + + it('should accumulate carbon footprint across blocks', () => { + const event1 = createSeedAcquisitionEvent(); + event1.distanceKm = 10; + event1.transportMethod = 'gasoline_vehicle'; + const block1 = chain.recordEvent(event1); + + const event2 = createSeedAcquisitionEvent(); + event2.id = 'seed-002'; + event2.distanceKm = 20; + event2.transportMethod = 'diesel_truck'; + const block2 = chain.recordEvent(event2); + + expect(block2.cumulativeCarbonKg).toBeGreaterThan(block1.cumulativeCarbonKg); + }); + + it('should accumulate food miles across blocks', () => { + const event1 = createSeedAcquisitionEvent(); + event1.distanceKm = 10; + const block1 = chain.recordEvent(event1); + + const event2 = createSeedAcquisitionEvent(); + event2.id = 'seed-002'; + event2.distanceKm = 20; + const block2 = chain.recordEvent(event2); + + expect(block2.cumulativeFoodMiles).toBe( + block1.cumulativeFoodMiles + 20 + ); + }); + }); + + describe('Chain Integrity', () => { + it('should verify chain integrity', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + chain.recordEvent(createPlantingEvent()); + + expect(chain.isChainValid()).toBe(true); + }); + + it('should detect tampered blocks', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + + // Tamper with the chain + chain.chain[1].transportEvent.distanceKm = 999; + + expect(chain.isChainValid()).toBe(false); + }); + + it('should detect broken chain links', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + chain.recordEvent(createPlantingEvent()); + + // Break the chain link + chain.chain[2].previousHash = 'fake-hash'; + + expect(chain.isChainValid()).toBe(false); + }); + + it('should maintain proper hash linking', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + chain.recordEvent(createPlantingEvent()); + + for (let i = 1; i < chain.chain.length; i++) { + expect(chain.chain[i].previousHash).toBe(chain.chain[i - 1].hash); + } + }); + }); + + describe('Plant Journey', () => { + it('should track plant journey across events', () => { + const plantId = 'plant-001'; + + // Record planting + const plantingEvent = createPlantingEvent(); + plantingEvent.plantIds = [plantId]; + chain.recordEvent(plantingEvent); + + // Record growing transport + const growingEvent: GrowingTransportEvent = { + id: 'growing-001', + timestamp: new Date().toISOString(), + eventType: 'growing_transport', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('farm'), + distanceKm: 5, + durationMinutes: 30, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + plantIds: [plantId], + reason: 'transplant', + plantStage: 'vegetative', + handlingMethod: 'potted', + rootDisturbance: 'minimal', + acclimatizationRequired: false, + }; + chain.recordEvent(growingEvent); + + const journey = chain.getPlantJourney(plantId); + expect(journey).not.toBeNull(); + expect(journey?.plantId).toBe(plantId); + expect(journey?.events.length).toBe(2); + expect(journey?.totalFoodMiles).toBeGreaterThan(0); + }); + + it('should return null for unknown plant', () => { + const journey = chain.getPlantJourney('unknown-plant'); + expect(journey).toBeNull(); + }); + + it('should calculate correct journey metrics', () => { + const plantId = 'plant-journey-test'; + + const plantingEvent = createPlantingEvent(); + plantingEvent.plantIds = [plantId]; + plantingEvent.distanceKm = 10; + plantingEvent.durationMinutes = 60; + chain.recordEvent(plantingEvent); + + const journey = chain.getPlantJourney(plantId); + expect(journey?.totalFoodMiles).toBe(10); + expect(journey?.daysInTransit).toBe(0); // 60 min = ~0 days + }); + + it('should track current stage correctly', () => { + const plantId = 'stage-test-plant'; + + const plantingEvent = createPlantingEvent(); + plantingEvent.plantIds = [plantId]; + chain.recordEvent(plantingEvent); + + let journey = chain.getPlantJourney(plantId); + expect(journey?.currentStage).toBe('seedling'); + + const growingEvent: GrowingTransportEvent = { + id: 'growing-stage', + timestamp: new Date().toISOString(), + eventType: 'growing_transport', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('farm'), + distanceKm: 1, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + plantIds: [plantId], + reason: 'relocation', + plantStage: 'flowering', + handlingMethod: 'potted', + rootDisturbance: 'none', + acclimatizationRequired: false, + }; + chain.recordEvent(growingEvent); + + journey = chain.getPlantJourney(plantId); + expect(journey?.currentStage).toBe('flowering'); + }); + }); + + describe('Environmental Impact', () => { + it('should calculate environmental impact for user', () => { + const userId = 'user-impact-test'; + + const event = createSeedAcquisitionEvent(); + event.senderId = userId; + event.distanceKm = 50; + event.transportMethod = 'diesel_truck'; + chain.recordEvent(event); + + const impact = chain.getEnvironmentalImpact(userId); + expect(impact.totalFoodMiles).toBe(50); + expect(impact.totalCarbonKg).toBeGreaterThan(0); + expect(impact.breakdownByMethod['diesel_truck']).toBeDefined(); + expect(impact.breakdownByEventType['seed_acquisition']).toBeDefined(); + }); + + it('should compare to conventional agriculture', () => { + const userId = 'compare-test'; + + const event = createSeedAcquisitionEvent(); + event.senderId = userId; + event.distanceKm = 10; // Very short distance + event.transportMethod = 'bicycle'; // Zero carbon + chain.recordEvent(event); + + const impact = chain.getEnvironmentalImpact(userId); + expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0); + }); + + it('should return zero impact for user with no events', () => { + const impact = chain.getEnvironmentalImpact('nonexistent-user'); + expect(impact.totalCarbonKg).toBe(0); + expect(impact.totalFoodMiles).toBe(0); + }); + }); + + describe('QR Code Generation', () => { + it('should generate valid QR data for plant', () => { + const plantId = 'qr-test-plant'; + + const plantingEvent = createPlantingEvent(); + plantingEvent.plantIds = [plantId]; + chain.recordEvent(plantingEvent); + + const qrData = chain.generateQRData(plantId, undefined); + expect(qrData.plantId).toBe(plantId); + expect(qrData.quickLookupUrl).toContain(plantId); + expect(qrData.lineageHash).toBeDefined(); + expect(qrData.verificationCode).toMatch(/^[A-F0-9]{8}$/); + }); + + it('should generate QR data for batch', () => { + const batchId = 'seed-batch-001'; + + const event = createSeedAcquisitionEvent(); + event.seedBatchId = batchId; + chain.recordEvent(event); + + const qrData = chain.generateQRData(undefined, batchId); + expect(qrData.batchId).toBe(batchId); + expect(qrData.lastEventType).toBe('seed_acquisition'); + }); + + it('should include blockchain address', () => { + const qrData = chain.generateQRData('any-id', undefined); + expect(qrData.blockchainAddress).toHaveLength(42); + }); + }); + + describe('Serialization', () => { + it('should export to JSON', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + + const json = chain.toJSON(); + expect(json).toHaveProperty('difficulty'); + expect(json).toHaveProperty('chain'); + expect((json as any).chain.length).toBe(2); + }); + + it('should import from JSON', () => { + chain.recordEvent(createSeedAcquisitionEvent()); + chain.recordEvent(createPlantingEvent()); + + const json = chain.toJSON(); + const restored = TransportChain.fromJSON(json); + + expect(restored.chain.length).toBe(chain.chain.length); + expect(restored.isChainValid()).toBe(true); + }); + + it('should rebuild indexes after import', () => { + const plantId = 'import-test-plant'; + const plantingEvent = createPlantingEvent(); + plantingEvent.plantIds = [plantId]; + chain.recordEvent(plantingEvent); + + const json = chain.toJSON(); + const restored = TransportChain.fromJSON(json); + + const journey = restored.getPlantJourney(plantId); + expect(journey).not.toBeNull(); + }); + }); + + describe('Singleton', () => { + it('should return same instance from getTransportChain', () => { + const chain1 = getTransportChain(); + const chain2 = getTransportChain(); + expect(chain1).toBe(chain2); + }); + + it('should allow setting custom chain', () => { + const customChain = new TransportChain(1); + setTransportChain(customChain); + expect(getTransportChain()).toBe(customChain); + }); + }); +}); + +// Helper functions +function createLocation(type: string): TransportLocation { + return { + latitude: 40.7128 + Math.random() * 0.1, + longitude: -74.006 + Math.random() * 0.1, + locationType: type as any, + facilityName: `Test ${type}`, + }; +} + +function createSeedAcquisitionEvent(): SeedAcquisitionEvent { + return { + id: `seed-${Date.now()}`, + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: createLocation('seed_bank'), + toLocation: createLocation('greenhouse'), + distanceKm: 25, + durationMinutes: 45, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0, + senderId: 'seed-bank-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'seed-batch-001', + sourceType: 'seed_bank', + species: 'Solanum lycopersicum', + variety: 'Roma', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + germinationRate: 95, + certifications: ['organic', 'heirloom'], + }; +} + +function createPlantingEvent(): PlantingEvent { + return { + id: `planting-${Date.now()}`, + timestamp: new Date().toISOString(), + eventType: 'planting', + fromLocation: createLocation('greenhouse'), + toLocation: createLocation('greenhouse'), + distanceKm: 0.01, + durationMinutes: 5, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-1', + receiverId: 'grower-1', + status: 'verified', + seedBatchId: 'seed-batch-001', + plantIds: ['plant-001', 'plant-002'], + plantingMethod: 'indoor_start', + quantityPlanted: 2, + growingEnvironment: 'greenhouse', + }; +} diff --git a/__tests__/lib/transport/types.test.ts b/__tests__/lib/transport/types.test.ts new file mode 100644 index 0000000..d3af49d --- /dev/null +++ b/__tests__/lib/transport/types.test.ts @@ -0,0 +1,362 @@ +/** + * Transport Types Tests + * Tests for type validation and consistency + */ + +import { + TransportLocation, + TransportMethod, + TransportEventType, + PlantStage, + CARBON_FACTORS, + SeedAcquisitionEvent, + PlantingEvent, + GrowingTransportEvent, + HarvestEvent, + ProcessingEvent, + DistributionEvent, + ConsumerDeliveryEvent, + SeedSavingEvent, + SeedSharingEvent, + TransportBlock, + PlantJourney, + EnvironmentalImpact, + TransportQRData, +} from '../../../lib/transport/types'; + +describe('Transport Types Validation', () => { + describe('TransportLocation', () => { + it('should accept valid location types', () => { + const validTypes = [ + 'farm', 'greenhouse', 'vertical_farm', 'warehouse', + 'hub', 'market', 'consumer', 'seed_bank', 'other' + ]; + + validTypes.forEach(locationType => { + const location: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + locationType: locationType as any, + }; + expect(location.locationType).toBe(locationType); + }); + }); + + it('should have required latitude and longitude', () => { + const location: TransportLocation = { + latitude: 0, + longitude: 0, + locationType: 'farm', + }; + expect(location.latitude).toBeDefined(); + expect(location.longitude).toBeDefined(); + }); + + it('should accept optional fields', () => { + const location: TransportLocation = { + latitude: 40.7128, + longitude: -74.006, + address: '123 Farm Lane', + city: 'New York', + region: 'NY', + country: 'USA', + postalCode: '10001', + locationType: 'farm', + facilityId: 'facility-001', + facilityName: 'Green Acres Farm', + }; + + expect(location.address).toBe('123 Farm Lane'); + expect(location.facilityName).toBe('Green Acres Farm'); + }); + }); + + describe('TransportMethod', () => { + it('should define all transport methods', () => { + const methods: TransportMethod[] = [ + 'walking', 'bicycle', 'electric_vehicle', 'hybrid_vehicle', + 'gasoline_vehicle', 'diesel_truck', 'electric_truck', + 'refrigerated_truck', 'rail', 'ship', 'air', 'drone', + 'local_delivery', 'customer_pickup' + ]; + + // All methods should have corresponding carbon factors + methods.forEach(method => { + expect(CARBON_FACTORS[method]).toBeDefined(); + }); + }); + }); + + describe('PlantStage', () => { + it('should cover complete plant lifecycle', () => { + const stages: PlantStage[] = [ + 'seed', 'germinating', 'seedling', 'vegetative', + 'flowering', 'fruiting', 'mature', 'harvesting', + 'post_harvest', 'seed_saving' + ]; + + // Verify all stages are valid strings + stages.forEach(stage => { + expect(typeof stage).toBe('string'); + expect(stage.length).toBeGreaterThan(0); + }); + }); + }); + + describe('TransportEventType', () => { + it('should cover complete transport event types', () => { + const eventTypes: TransportEventType[] = [ + 'seed_acquisition', 'planting', 'growing_transport', + 'harvest', 'processing', 'distribution', + 'consumer_delivery', 'seed_saving', 'seed_sharing' + ]; + + expect(eventTypes.length).toBe(9); + }); + }); + + describe('Event Type Structures', () => { + it('should validate SeedAcquisitionEvent', () => { + const event: SeedAcquisitionEvent = { + id: 'seed-001', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: { latitude: 0, longitude: 0, locationType: 'seed_bank' }, + toLocation: { latitude: 0, longitude: 0, locationType: 'farm' }, + distanceKm: 10, + durationMinutes: 30, + transportMethod: 'electric_vehicle', + carbonFootprintKg: 0.5, + senderId: 'sender-001', + receiverId: 'receiver-001', + status: 'verified', + seedBatchId: 'batch-001', + sourceType: 'seed_bank', + species: 'Solanum lycopersicum', + quantity: 100, + quantityUnit: 'seeds', + generation: 1, + }; + + expect(event.eventType).toBe('seed_acquisition'); + expect(event.seedBatchId).toBeDefined(); + expect(event.species).toBeDefined(); + }); + + it('should validate PlantingEvent', () => { + const event: PlantingEvent = { + id: 'planting-001', + timestamp: new Date().toISOString(), + eventType: 'planting', + fromLocation: { latitude: 0, longitude: 0, locationType: 'greenhouse' }, + toLocation: { latitude: 0, longitude: 0, locationType: 'greenhouse' }, + distanceKm: 0, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-001', + receiverId: 'grower-001', + status: 'verified', + seedBatchId: 'batch-001', + plantIds: ['plant-001', 'plant-002'], + plantingMethod: 'indoor_start', + quantityPlanted: 2, + growingEnvironment: 'greenhouse', + }; + + expect(event.eventType).toBe('planting'); + expect(event.plantIds.length).toBe(2); + }); + + it('should validate HarvestEvent', () => { + const event: HarvestEvent = { + id: 'harvest-001', + timestamp: new Date().toISOString(), + eventType: 'harvest', + fromLocation: { latitude: 0, longitude: 0, locationType: 'farm' }, + toLocation: { latitude: 0, longitude: 0, locationType: 'warehouse' }, + distanceKm: 5, + durationMinutes: 20, + transportMethod: 'electric_truck', + carbonFootprintKg: 0.1, + senderId: 'grower-001', + receiverId: 'processor-001', + status: 'verified', + plantIds: ['plant-001'], + harvestBatchId: 'harvest-001', + harvestType: 'full', + produceType: 'tomatoes', + grossWeight: 10, + netWeight: 9.5, + weightUnit: 'kg', + packagingType: 'crates', + temperatureRequired: { min: 10, max: 15, optimal: 12, unit: 'celsius' }, + shelfLifeHours: 168, + seedsSaved: true, + seedBatchIdCreated: 'new-seed-batch-001', + }; + + expect(event.eventType).toBe('harvest'); + expect(event.seedsSaved).toBe(true); + expect(event.seedBatchIdCreated).toBeDefined(); + }); + + it('should validate SeedSavingEvent', () => { + const event: SeedSavingEvent = { + id: 'save-001', + timestamp: new Date().toISOString(), + eventType: 'seed_saving', + fromLocation: { latitude: 0, longitude: 0, locationType: 'farm' }, + toLocation: { latitude: 0, longitude: 0, locationType: 'seed_bank' }, + distanceKm: 1, + durationMinutes: 10, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'grower-001', + receiverId: 'grower-001', + status: 'verified', + parentPlantIds: ['plant-001', 'plant-002'], + newSeedBatchId: 'new-batch-001', + collectionMethod: 'dry_seed', + seedCount: 500, + storageConditions: { + temperature: 10, + humidity: 30, + lightExposure: 'dark', + containerType: 'jar', + desiccant: true, + estimatedViability: 5, + }, + storageLocationId: 'storage-001', + newGenerationNumber: 2, + availableForSharing: true, + }; + + expect(event.eventType).toBe('seed_saving'); + expect(event.newGenerationNumber).toBe(2); + }); + }); + + describe('TransportBlock', () => { + it('should have all required blockchain fields', () => { + const block: TransportBlock = { + index: 1, + timestamp: new Date().toISOString(), + transportEvent: { + id: 'test', + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + fromLocation: { latitude: 0, longitude: 0, locationType: 'seed_bank' }, + toLocation: { latitude: 0, longitude: 0, locationType: 'farm' }, + distanceKm: 0, + durationMinutes: 0, + transportMethod: 'walking', + carbonFootprintKg: 0, + senderId: 'a', + receiverId: 'b', + status: 'verified', + seedBatchId: 'batch', + sourceType: 'seed_bank', + species: 'test', + quantity: 1, + quantityUnit: 'seeds', + generation: 0, + }, + previousHash: '0'.repeat(64), + hash: 'a'.repeat(64), + nonce: 12345, + cumulativeCarbonKg: 0, + cumulativeFoodMiles: 0, + chainLength: 2, + }; + + expect(block.index).toBeDefined(); + expect(block.previousHash.length).toBe(64); + expect(block.hash.length).toBe(64); + expect(block.nonce).toBeGreaterThanOrEqual(0); + }); + }); + + describe('PlantJourney', () => { + it('should track complete journey data', () => { + const journey: PlantJourney = { + plantId: 'plant-001', + seedBatchOrigin: 'batch-001', + currentCustodian: 'grower-001', + currentLocation: { latitude: 40, longitude: -74, locationType: 'farm' }, + currentStage: 'vegetative', + events: [], + totalFoodMiles: 25.5, + totalCarbonKg: 1.2, + daysInTransit: 2, + daysGrowing: 45, + generation: 1, + ancestorPlantIds: ['parent-001'], + descendantSeedBatches: ['future-batch-001'], + }; + + expect(journey.plantId).toBe('plant-001'); + expect(journey.totalFoodMiles).toBe(25.5); + expect(journey.generation).toBe(1); + }); + }); + + describe('EnvironmentalImpact', () => { + it('should have comparison metrics', () => { + const impact: EnvironmentalImpact = { + totalCarbonKg: 10, + totalFoodMiles: 100, + carbonPerKgProduce: 0.5, + milesPerKgProduce: 5, + breakdownByMethod: {}, + breakdownByEventType: {}, + comparisonToConventional: { + carbonSaved: 50, + milesSaved: 1400, + percentageReduction: 83, + }, + }; + + expect(impact.comparisonToConventional.percentageReduction).toBe(83); + expect(impact.comparisonToConventional.milesSaved).toBe(1400); + }); + }); + + describe('TransportQRData', () => { + it('should have verification data', () => { + const qrData: TransportQRData = { + plantId: 'plant-001', + blockchainAddress: '0x' + 'a'.repeat(40), + quickLookupUrl: 'https://example.com/track/plant-001', + lineageHash: 'abc123', + currentCustodian: 'grower-001', + lastEventType: 'planting', + lastEventTimestamp: new Date().toISOString(), + verificationCode: 'ABCD1234', + }; + + expect(qrData.verificationCode.length).toBe(8); + expect(qrData.quickLookupUrl).toContain('plant-001'); + }); + }); + + describe('Status Values', () => { + it('should have valid event statuses', () => { + const statuses: Array<'pending' | 'in_transit' | 'delivered' | 'verified' | 'disputed'> = [ + 'pending', 'in_transit', 'delivered', 'verified', 'disputed' + ]; + + expect(statuses.length).toBe(5); + }); + }); + + describe('Certification Types', () => { + it('should define seed certifications', () => { + const certifications: Array<'organic' | 'non_gmo' | 'heirloom' | 'certified_seed' | 'biodynamic'> = [ + 'organic', 'non_gmo', 'heirloom', 'certified_seed', 'biodynamic' + ]; + + expect(certifications.length).toBe(5); + }); + }); +}); diff --git a/__tests__/lib/vertical-farming/controller.test.ts b/__tests__/lib/vertical-farming/controller.test.ts new file mode 100644 index 0000000..3bcdc54 --- /dev/null +++ b/__tests__/lib/vertical-farming/controller.test.ts @@ -0,0 +1,530 @@ +/** + * VerticalFarmController Tests + * Tests for the vertical farm management system + */ + +import { + VerticalFarmController, + getVerticalFarmController, +} from '../../../lib/vertical-farming/controller'; +import { + VerticalFarm, + GrowingZone, + CropBatch, + GrowingRecipe, + ZoneEnvironmentReadings, +} from '../../../lib/vertical-farming/types'; + +describe('VerticalFarmController', () => { + let controller: VerticalFarmController; + + beforeEach(() => { + controller = new VerticalFarmController(); + }); + + describe('Initialization', () => { + it('should initialize with default recipes', () => { + const recipes = controller.getRecipes(); + expect(recipes.length).toBeGreaterThan(0); + }); + + it('should have lettuce recipe', () => { + const recipes = controller.getRecipes(); + const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce'); + expect(lettuceRecipe).toBeDefined(); + }); + + it('should have basil recipe', () => { + const recipes = controller.getRecipes(); + const basilRecipe = recipes.find(r => r.cropType === 'basil'); + expect(basilRecipe).toBeDefined(); + }); + + it('should have microgreens recipe', () => { + const recipes = controller.getRecipes(); + const microgreensRecipe = recipes.find(r => r.cropType === 'microgreens'); + expect(microgreensRecipe).toBeDefined(); + }); + }); + + describe('Farm Registration', () => { + it('should register a vertical farm', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const retrieved = controller.getFarm('farm-001'); + expect(retrieved).toBeDefined(); + expect(retrieved?.name).toBe('Test Vertical Farm'); + }); + + it('should track multiple farms', () => { + controller.registerFarm(createVerticalFarm('farm-001')); + controller.registerFarm(createVerticalFarm('farm-002')); + + expect(controller.getFarm('farm-001')).toBeDefined(); + expect(controller.getFarm('farm-002')).toBeDefined(); + }); + + it('should return undefined for unknown farm', () => { + expect(controller.getFarm('unknown-farm')).toBeUndefined(); + }); + }); + + describe('Crop Batch Management', () => { + it('should start crop batch with recipe', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; + + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + lettuceRecipe.id, + 'seed-batch-001', + 100 + ); + + expect(batch.id).toBeDefined(); + expect(batch.farmId).toBe('farm-001'); + expect(batch.zoneId).toBe('zone-001'); + expect(batch.plantCount).toBe(100); + expect(batch.status).toBe('germinating'); + }); + + it('should generate plant IDs for batch', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const recipe = recipes[0]; + + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipe.id, + 'seed-batch-001', + 50 + ); + + expect(batch.plantIds.length).toBe(50); + expect(batch.plantIds[0]).toContain('plant-'); + }); + + it('should calculate expected yield', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; + + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + lettuceRecipe.id, + 'seed-batch-001', + 100 + ); + + // Expected yield = expectedYieldGrams * plantCount / 1000 + const expectedYield = (lettuceRecipe.expectedYieldGrams * 100) / 1000; + expect(batch.expectedYieldKg).toBe(expectedYield); + }); + + it('should update zone status when starting batch', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const updatedFarm = controller.getFarm('farm-001'); + const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); + + expect(zone?.status).toBe('planted'); + expect(zone?.currentCrop).toBe(recipes[0].cropType); + expect(zone?.plantIds.length).toBe(100); + }); + + it('should throw error for unknown farm', () => { + expect(() => { + controller.startCropBatch( + 'unknown-farm', + 'zone-001', + 'recipe-001', + 'seed-001', + 100 + ); + }).toThrow('Farm unknown-farm not found'); + }); + + it('should throw error for unknown zone', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + expect(() => { + controller.startCropBatch( + 'farm-001', + 'unknown-zone', + 'recipe-001', + 'seed-001', + 100 + ); + }).toThrow('Zone unknown-zone not found'); + }); + + it('should throw error for unknown recipe', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + expect(() => { + controller.startCropBatch( + 'farm-001', + 'zone-001', + 'unknown-recipe', + 'seed-001', + 100 + ); + }).toThrow('Recipe unknown-recipe not found'); + }); + }); + + describe('Batch Progress', () => { + it('should update batch progress', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const updated = controller.updateBatchProgress(batch.id); + expect(updated.currentDay).toBeGreaterThanOrEqual(0); + }); + + it('should update current stage based on day', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + // Initial stage should be first stage + expect(batch.currentStage).toBe(recipes[0].stages[0].name); + }); + + it('should throw error for unknown batch', () => { + expect(() => { + controller.updateBatchProgress('unknown-batch'); + }).toThrow('Batch unknown-batch not found'); + }); + }); + + describe('Harvest Completion', () => { + it('should complete harvest and record yield', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const completed = controller.completeHarvest(batch.id, 15.5, 'A'); + + expect(completed.status).toBe('completed'); + expect(completed.actualYieldKg).toBe(15.5); + expect(completed.qualityGrade).toBe('A'); + expect(completed.actualHarvestDate).toBeDefined(); + }); + + it('should update zone after harvest', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + controller.completeHarvest(batch.id, 15.5, 'A'); + + const updatedFarm = controller.getFarm('farm-001'); + const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); + + expect(zone?.status).toBe('cleaning'); + expect(zone?.currentCrop).toBe(''); + expect(zone?.plantIds.length).toBe(0); + }); + + it('should throw error for unknown batch', () => { + expect(() => { + controller.completeHarvest('unknown-batch', 10, 'A'); + }).toThrow('Batch unknown-batch not found'); + }); + }); + + describe('Recipe Management', () => { + it('should add custom recipe', () => { + const customRecipe: GrowingRecipe = { + id: 'recipe-custom-spinach', + name: 'Custom Spinach Recipe', + cropType: 'spinach', + version: '1.0', + stages: [ + { + name: 'Germination', + daysStart: 0, + daysEnd: 5, + temperature: { day: 20, night: 18 }, + humidity: { day: 80, night: 85 }, + co2Ppm: 800, + lightHours: 16, + lightPpfd: 150, + nutrientRecipeId: 'nutrient-seedling', + targetEc: 0.8, + targetPh: 6.0, + actions: [], + }, + ], + expectedDays: 40, + expectedYieldGrams: 100, + expectedYieldPerSqm: 3000, + requirements: { + positions: 1, + zoneType: 'NFT', + minimumPpfd: 200, + idealTemperatureC: 18, + }, + source: 'internal', + timesUsed: 0, + }; + + controller.addRecipe(customRecipe); + + const recipes = controller.getRecipes(); + const added = recipes.find(r => r.id === 'recipe-custom-spinach'); + expect(added).toBeDefined(); + expect(added?.name).toBe('Custom Spinach Recipe'); + }); + + it('should track recipe usage', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const recipe = recipes[0]; + const initialUsage = recipe.timesUsed; + + controller.startCropBatch( + 'farm-001', + 'zone-001', + recipe.id, + 'seed-batch-001', + 100 + ); + + expect(recipe.timesUsed).toBe(initialUsage + 1); + }); + }); + + describe('Singleton', () => { + it('should return same instance from getVerticalFarmController', () => { + const controller1 = getVerticalFarmController(); + const controller2 = getVerticalFarmController(); + expect(controller1).toBe(controller2); + }); + }); + + describe('Serialization', () => { + it('should export to JSON', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const json = controller.toJSON(); + expect(json).toHaveProperty('farms'); + expect(json).toHaveProperty('recipes'); + expect(json).toHaveProperty('batches'); + }); + + it('should import from JSON', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const json = controller.toJSON(); + const restored = VerticalFarmController.fromJSON(json); + + expect(restored.getFarm('farm-001')).toBeDefined(); + expect(restored.getRecipes().length).toBeGreaterThan(0); + }); + }); +}); + +// Helper functions +function createVerticalFarm(id: string): VerticalFarm { + return { + id, + name: 'Test Vertical Farm', + ownerId: 'owner-001', + location: { + latitude: 40.7128, + longitude: -74.006, + address: '123 Farm Street', + city: 'New York', + country: 'USA', + timezone: 'America/New_York', + }, + specs: { + totalAreaSqm: 500, + growingAreaSqm: 400, + numberOfLevels: 4, + ceilingHeightM: 3, + totalGrowingPositions: 4000, + currentActivePlants: 0, + powerCapacityKw: 100, + waterStorageL: 5000, + backupPowerHours: 24, + certifications: ['organic', 'gap'], + buildingType: 'warehouse', + insulation: 'high_efficiency', + }, + zones: [ + createGrowingZone('zone-001', 'Zone A', 1), + createGrowingZone('zone-002', 'Zone B', 1), + ], + environmentalControl: { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, + airCirculation: { fans: [] }, + controlMode: 'adaptive', + }, + irrigationSystem: { + type: 'recirculating', + freshWaterTankL: 2000, + freshWaterLevelL: 1800, + nutrientTankL: 500, + nutrientLevelL: 450, + wasteTankL: 200, + wasteLevelL: 50, + waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, + pumps: [], + irrigationSchedule: [], + }, + lightingSystem: { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 5000, + currentWattage: 3000, + efficacyUmolJ: 2.5, + }, + nutrientSystem: { + mixingMethod: 'fully_auto', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, + dosingRatiosMlPerL: [], + }, + monitoring: { + ec: 1.5, + ph: 6.0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + automationLevel: 'semi_automated', + automationSystems: [], + status: 'operational', + operationalSince: '2024-01-01', + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 75, + averageYieldEfficiency: 85, + energyEfficiencyScore: 80, + }; +} + +function createGrowingZone(id: string, name: string, level: number): GrowingZone { + return { + id, + name, + level, + areaSqm: 50, + lengthM: 10, + widthM: 5, + growingMethod: 'NFT', + plantPositions: 500, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: { + temperatureC: { min: 18, max: 24, target: 21 }, + humidityPercent: { min: 60, max: 80, target: 70 }, + co2Ppm: { min: 800, max: 1200, target: 1000 }, + lightPpfd: { min: 200, max: 400, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, + nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, + waterTempC: { min: 18, max: 22, target: 20 }, + }, + currentEnvironment: { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }, + status: 'empty', + }; +} diff --git a/__tests__/lib/vertical-farming/environment.test.ts b/__tests__/lib/vertical-farming/environment.test.ts new file mode 100644 index 0000000..274677f --- /dev/null +++ b/__tests__/lib/vertical-farming/environment.test.ts @@ -0,0 +1,593 @@ +/** + * Environment Alert Tests + * Tests for environment monitoring and alert generation + */ + +import { + VerticalFarmController, +} from '../../../lib/vertical-farming/controller'; +import { + VerticalFarm, + GrowingZone, + ZoneEnvironmentReadings, + EnvironmentAlert, +} from '../../../lib/vertical-farming/types'; + +describe('Environment Alerts', () => { + let controller: VerticalFarmController; + + beforeEach(() => { + controller = new VerticalFarmController(); + }); + + describe('Temperature Alerts', () => { + it('should detect low temperature', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 15, // Below min of 18 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + expect(tempAlert).toBeDefined(); + expect(tempAlert?.type).toBe('low'); + }); + + it('should detect high temperature', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 30, // Above max of 24 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + expect(tempAlert).toBeDefined(); + expect(tempAlert?.type).toBe('high'); + }); + + it('should detect critical low temperature', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 10, // More than 5 below min + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + expect(tempAlert?.type).toBe('critical_low'); + }); + + it('should detect critical high temperature', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 35, // More than 5 above max + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + expect(tempAlert?.type).toBe('critical_high'); + }); + + it('should not alert for normal temperature', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 21, // Within range + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + expect(tempAlert).toBeUndefined(); + }); + }); + + describe('Humidity Alerts', () => { + it('should detect low humidity', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + humidityPercent: 50, // Below min of 60 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const humidityAlert = alerts.find(a => a.parameter === 'humidity'); + expect(humidityAlert).toBeDefined(); + expect(humidityAlert?.type).toBe('low'); + }); + + it('should detect high humidity', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + humidityPercent: 90, // Above max of 80 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const humidityAlert = alerts.find(a => a.parameter === 'humidity'); + expect(humidityAlert).toBeDefined(); + expect(humidityAlert?.type).toBe('high'); + }); + }); + + describe('EC Alerts', () => { + it('should detect low EC', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + ec: 0.8, // Below min of 1.2 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const ecAlert = alerts.find(a => a.parameter === 'ec'); + expect(ecAlert).toBeDefined(); + expect(ecAlert?.type).toBe('low'); + }); + + it('should detect high EC', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + ec: 2.5, // Above max of 1.8 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const ecAlert = alerts.find(a => a.parameter === 'ec'); + expect(ecAlert).toBeDefined(); + expect(ecAlert?.type).toBe('high'); + }); + }); + + describe('pH Alerts', () => { + it('should detect low pH', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + ph: 5.2, // Below min of 5.8 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const phAlert = alerts.find(a => a.parameter === 'ph'); + expect(phAlert).toBeDefined(); + expect(phAlert?.type).toBe('low'); + }); + + it('should detect high pH', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + ph: 7.0, // Above max of 6.2 + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + const phAlert = alerts.find(a => a.parameter === 'ph'); + expect(phAlert).toBeDefined(); + expect(phAlert?.type).toBe('high'); + }); + }); + + describe('Multiple Alerts', () => { + it('should detect multiple issues simultaneously', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 30, + humidityPercent: 40, + ec: 0.5, + ph: 7.5, + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + + expect(alerts.length).toBeGreaterThanOrEqual(4); + expect(alerts.some(a => a.parameter === 'temperature')).toBe(true); + expect(alerts.some(a => a.parameter === 'humidity')).toBe(true); + expect(alerts.some(a => a.parameter === 'ec')).toBe(true); + expect(alerts.some(a => a.parameter === 'ph')).toBe(true); + }); + }); + + describe('Alert Properties', () => { + it('should include threshold value', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 15, + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + + expect(tempAlert?.threshold).toBeDefined(); + expect(tempAlert?.threshold).toBe(18); // Min threshold + }); + + it('should include actual value', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 15, + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + + expect(tempAlert?.value).toBe(15); + }); + + it('should include timestamp', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 15, + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + + expect(tempAlert?.timestamp).toBeDefined(); + }); + + it('should set acknowledged to false by default', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 15, + }); + + const alerts = controller.recordEnvironment('zone-001', readings); + const tempAlert = alerts.find(a => a.parameter === 'temperature'); + + expect(tempAlert?.acknowledged).toBe(false); + }); + }); + + describe('Zone Environment Updates', () => { + it('should update zone current environment', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 22, + humidityPercent: 65, + }); + + controller.recordEnvironment('zone-001', readings); + + const updatedFarm = controller.getFarm('farm-001'); + const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); + + expect(zone?.currentEnvironment.temperatureC).toBe(22); + expect(zone?.currentEnvironment.humidityPercent).toBe(65); + }); + + it('should store alerts in zone readings', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 30, // Will trigger alert + }); + + controller.recordEnvironment('zone-001', readings); + + const updatedFarm = controller.getFarm('farm-001'); + const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); + + expect(zone?.currentEnvironment.alerts.length).toBeGreaterThan(0); + }); + }); + + describe('Batch Health Score', () => { + it('should decrease health score on alerts', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const initialHealth = batch.healthScore; + + // Record problematic environment + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 30, // High temp alert + }); + + controller.recordEnvironment('zone-001', readings); + + // Health should decrease + expect(batch.healthScore).toBeLessThan(initialHealth); + }); + + it('should decrease health more for critical alerts', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const initialHealth = batch.healthScore; + + // Record critical environment issue + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 5, // Critical low temp + }); + + controller.recordEnvironment('zone-001', readings); + + // Health should decrease significantly + expect(batch.healthScore).toBeLessThan(initialHealth - 3); + }); + + it('should not go below 0', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + // Record many critical issues + for (let i = 0; i < 50; i++) { + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 5, + }); + controller.recordEnvironment('zone-001', readings); + } + + expect(batch.healthScore).toBeGreaterThanOrEqual(0); + }); + }); + + describe('Environment Logging', () => { + it('should log environment readings to batch', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + const readings: ZoneEnvironmentReadings = createReadings({}); + controller.recordEnvironment('zone-001', readings); + + expect(batch.environmentLog.length).toBe(1); + expect(batch.environmentLog[0].readings).toBeDefined(); + }); + + it('should accumulate environment logs', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipes[0].id, + 'seed-batch-001', + 100 + ); + + for (let i = 0; i < 5; i++) { + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 20 + i, + }); + controller.recordEnvironment('zone-001', readings); + } + + expect(batch.environmentLog.length).toBe(5); + }); + }); + + describe('Unknown Zone', () => { + it('should return empty alerts for unknown zone', () => { + const readings: ZoneEnvironmentReadings = createReadings({ + temperatureC: 5, // Would normally trigger alert + }); + + const alerts = controller.recordEnvironment('unknown-zone', readings); + expect(alerts.length).toBe(0); + }); + }); +}); + +// Helper functions +function createVerticalFarm(id: string): VerticalFarm { + return { + id, + name: 'Test Farm', + ownerId: 'owner-001', + location: { + latitude: 40.7128, + longitude: -74.006, + address: '123 Test St', + city: 'New York', + country: 'USA', + timezone: 'America/New_York', + }, + specs: { + totalAreaSqm: 500, + growingAreaSqm: 400, + numberOfLevels: 4, + ceilingHeightM: 3, + totalGrowingPositions: 4000, + currentActivePlants: 0, + powerCapacityKw: 100, + waterStorageL: 5000, + backupPowerHours: 24, + certifications: [], + buildingType: 'warehouse', + insulation: 'high_efficiency', + }, + zones: [createGrowingZone('zone-001', 'Zone A', 1)], + environmentalControl: { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, + airCirculation: { fans: [] }, + controlMode: 'adaptive', + }, + irrigationSystem: { + type: 'recirculating', + freshWaterTankL: 2000, + freshWaterLevelL: 1800, + nutrientTankL: 500, + nutrientLevelL: 450, + wasteTankL: 200, + wasteLevelL: 50, + waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, + pumps: [], + irrigationSchedule: [], + }, + lightingSystem: { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 5000, + currentWattage: 3000, + efficacyUmolJ: 2.5, + }, + nutrientSystem: { + mixingMethod: 'fully_auto', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, + dosingRatiosMlPerL: [], + }, + monitoring: { + ec: 1.5, + ph: 6.0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + automationLevel: 'semi_automated', + automationSystems: [], + status: 'operational', + operationalSince: '2024-01-01', + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 75, + averageYieldEfficiency: 85, + energyEfficiencyScore: 80, + }; +} + +function createGrowingZone(id: string, name: string, level: number): GrowingZone { + return { + id, + name, + level, + areaSqm: 50, + lengthM: 10, + widthM: 5, + growingMethod: 'NFT', + plantPositions: 500, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: { + temperatureC: { min: 18, max: 24, target: 21 }, + humidityPercent: { min: 60, max: 80, target: 70 }, + co2Ppm: { min: 800, max: 1200, target: 1000 }, + lightPpfd: { min: 200, max: 400, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, + nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, + waterTempC: { min: 18, max: 22, target: 20 }, + }, + currentEnvironment: { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }, + status: 'empty', + }; +} + +function createReadings(overrides: Partial): ZoneEnvironmentReadings { + return { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + ...overrides, + }; +} diff --git a/__tests__/lib/vertical-farming/recipes.test.ts b/__tests__/lib/vertical-farming/recipes.test.ts new file mode 100644 index 0000000..a8df9d3 --- /dev/null +++ b/__tests__/lib/vertical-farming/recipes.test.ts @@ -0,0 +1,453 @@ +/** + * Recipe Stage Logic Tests + * Tests for growing recipe and stage management + */ + +import { + VerticalFarmController, +} from '../../../lib/vertical-farming/controller'; +import { + GrowingRecipe, + GrowthStage, + VerticalFarm, + GrowingZone, +} from '../../../lib/vertical-farming/types'; + +describe('Growing Recipes', () => { + let controller: VerticalFarmController; + + beforeEach(() => { + controller = new VerticalFarmController(); + }); + + describe('Default Recipes', () => { + it('should have complete stage definitions', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + expect(recipe.stages.length).toBeGreaterThan(0); + expect(recipe.expectedDays).toBeGreaterThan(0); + + // Stages should cover full duration + const lastStage = recipe.stages[recipe.stages.length - 1]; + expect(lastStage.daysEnd).toBeLessThanOrEqual(recipe.expectedDays); + }); + }); + + it('should have non-overlapping stages', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + for (let i = 1; i < recipe.stages.length; i++) { + const prevStage = recipe.stages[i - 1]; + const currentStage = recipe.stages[i]; + expect(currentStage.daysStart).toBeGreaterThan(prevStage.daysStart); + expect(currentStage.daysStart).toBeGreaterThanOrEqual(prevStage.daysEnd); + } + }); + }); + + it('should have valid environment targets per stage', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + recipe.stages.forEach(stage => { + // Temperature + expect(stage.temperature.day).toBeGreaterThan(0); + expect(stage.temperature.night).toBeGreaterThan(0); + expect(stage.temperature.night).toBeLessThanOrEqual(stage.temperature.day); + + // Humidity + expect(stage.humidity.day).toBeGreaterThan(0); + expect(stage.humidity.day).toBeLessThanOrEqual(100); + expect(stage.humidity.night).toBeGreaterThan(0); + expect(stage.humidity.night).toBeLessThanOrEqual(100); + + // CO2 + expect(stage.co2Ppm).toBeGreaterThanOrEqual(0); + + // Light + expect(stage.lightHours).toBeGreaterThanOrEqual(0); + expect(stage.lightHours).toBeLessThanOrEqual(24); + expect(stage.lightPpfd).toBeGreaterThanOrEqual(0); + + // Nutrients + expect(stage.targetEc).toBeGreaterThanOrEqual(0); + expect(stage.targetPh).toBeGreaterThan(0); + expect(stage.targetPh).toBeLessThan(14); + }); + }); + }); + }); + + describe('Lettuce Recipe', () => { + it('should have correct growth stages', () => { + const recipes = controller.getRecipes(); + const lettuce = recipes.find(r => r.cropType === 'lettuce')!; + + expect(lettuce.stages.length).toBeGreaterThanOrEqual(3); + + const stageNames = lettuce.stages.map(s => s.name); + expect(stageNames).toContain('Germination'); + expect(stageNames).toContain('Seedling'); + }); + + it('should have approximately 35 day cycle', () => { + const recipes = controller.getRecipes(); + const lettuce = recipes.find(r => r.cropType === 'lettuce')!; + + expect(lettuce.expectedDays).toBeGreaterThanOrEqual(30); + expect(lettuce.expectedDays).toBeLessThanOrEqual(45); + }); + + it('should have transplant action', () => { + const recipes = controller.getRecipes(); + const lettuce = recipes.find(r => r.cropType === 'lettuce')!; + + const hasTransplant = lettuce.stages.some(stage => + stage.actions.some(action => action.action === 'transplant') + ); + expect(hasTransplant).toBe(true); + }); + }); + + describe('Basil Recipe', () => { + it('should have higher temperature requirements', () => { + const recipes = controller.getRecipes(); + const basil = recipes.find(r => r.cropType === 'basil')!; + const lettuce = recipes.find(r => r.cropType === 'lettuce')!; + + // Basil prefers warmer temperatures + const basilMaxTemp = Math.max(...basil.stages.map(s => s.temperature.day)); + const lettuceMaxTemp = Math.max(...lettuce.stages.map(s => s.temperature.day)); + + expect(basilMaxTemp).toBeGreaterThanOrEqual(lettuceMaxTemp); + }); + + it('should have longer cycle than microgreens', () => { + const recipes = controller.getRecipes(); + const basil = recipes.find(r => r.cropType === 'basil')!; + const microgreens = recipes.find(r => r.cropType === 'microgreens')!; + + expect(basil.expectedDays).toBeGreaterThan(microgreens.expectedDays); + }); + }); + + describe('Microgreens Recipe', () => { + it('should have short cycle', () => { + const recipes = controller.getRecipes(); + const microgreens = recipes.find(r => r.cropType === 'microgreens')!; + + expect(microgreens.expectedDays).toBeLessThanOrEqual(21); + }); + + it('should start in darkness (sowing stage)', () => { + const recipes = controller.getRecipes(); + const microgreens = recipes.find(r => r.cropType === 'microgreens')!; + + const sowingStage = microgreens.stages.find(s => s.name === 'Sowing'); + if (sowingStage) { + expect(sowingStage.lightHours).toBe(0); + expect(sowingStage.lightPpfd).toBe(0); + } + }); + + it('should have high humidity in early stages', () => { + const recipes = controller.getRecipes(); + const microgreens = recipes.find(r => r.cropType === 'microgreens')!; + + const earlyStage = microgreens.stages[0]; + expect(earlyStage.humidity.day).toBeGreaterThanOrEqual(80); + }); + }); + + describe('Stage Transitions', () => { + it('should update environment targets on stage change', () => { + const farm = createVerticalFarm('farm-001'); + controller.registerFarm(farm); + + const recipes = controller.getRecipes(); + const recipe = recipes[0]; + + const batch = controller.startCropBatch( + 'farm-001', + 'zone-001', + recipe.id, + 'seed-batch-001', + 100 + ); + + // Initial stage + const initialStage = recipe.stages[0]; + const farmAfter = controller.getFarm('farm-001'); + const zone = farmAfter?.zones.find(z => z.id === 'zone-001'); + + expect(zone?.environmentTargets.temperatureC.target).toBe(initialStage.temperature.day); + }); + }); + + describe('Recipe Requirements', () => { + it('should specify zone type requirements', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + expect(recipe.requirements.zoneType).toBeDefined(); + expect(['NFT', 'DWC', 'ebb_flow', 'aeroponics', 'vertical_towers', 'rack_system']) + .toContain(recipe.requirements.zoneType); + }); + }); + + it('should specify minimum light requirements', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + expect(recipe.requirements.minimumPpfd).toBeGreaterThan(0); + }); + }); + + it('should specify temperature requirements', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + expect(recipe.requirements.idealTemperatureC).toBeGreaterThan(0); + expect(recipe.requirements.idealTemperatureC).toBeLessThan(40); + }); + }); + }); + + describe('Yield Expectations', () => { + it('should have reasonable yield estimates', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + expect(recipe.expectedYieldGrams).toBeGreaterThan(0); + expect(recipe.expectedYieldPerSqm).toBeGreaterThan(0); + }); + }); + + it('should have consistent yield calculations', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + // Yield per sqm should be reasonable multiple of per-plant yield + const plantsPerSqm = recipe.expectedYieldPerSqm / recipe.expectedYieldGrams; + expect(plantsPerSqm).toBeGreaterThan(0); + expect(plantsPerSqm).toBeLessThan(100); // Reasonable plant density + }); + }); + }); + + describe('Stage Actions', () => { + it('should have day specified for each action', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + recipe.stages.forEach(stage => { + stage.actions.forEach(action => { + expect(action.day).toBeGreaterThanOrEqual(stage.daysStart); + expect(action.day).toBeLessThanOrEqual(stage.daysEnd); + }); + }); + }); + }); + + it('should have automation flag for actions', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + recipe.stages.forEach(stage => { + stage.actions.forEach(action => { + expect(typeof action.automated).toBe('boolean'); + }); + }); + }); + }); + + it('should have descriptions for actions', () => { + const recipes = controller.getRecipes(); + + recipes.forEach(recipe => { + recipe.stages.forEach(stage => { + stage.actions.forEach(action => { + expect(action.description).toBeDefined(); + expect(action.description.length).toBeGreaterThan(0); + }); + }); + }); + }); + }); + + describe('Custom Recipes', () => { + it('should validate custom recipe structure', () => { + const customRecipe: GrowingRecipe = { + id: 'recipe-custom', + name: 'Custom Recipe', + cropType: 'cucumber', + version: '1.0', + stages: [ + { + name: 'Stage 1', + daysStart: 0, + daysEnd: 30, + temperature: { day: 25, night: 22 }, + humidity: { day: 70, night: 75 }, + co2Ppm: 1000, + lightHours: 16, + lightPpfd: 400, + nutrientRecipeId: 'nutrient-veg', + targetEc: 2.0, + targetPh: 5.8, + actions: [], + }, + ], + expectedDays: 60, + expectedYieldGrams: 500, + expectedYieldPerSqm: 10000, + requirements: { + positions: 1, + zoneType: 'NFT', + minimumPpfd: 300, + idealTemperatureC: 24, + }, + source: 'internal', + timesUsed: 0, + }; + + controller.addRecipe(customRecipe); + const recipes = controller.getRecipes(); + const found = recipes.find(r => r.id === 'recipe-custom'); + + expect(found).toBeDefined(); + expect(found?.cropType).toBe('cucumber'); + }); + }); +}); + +// Helper function +function createVerticalFarm(id: string): VerticalFarm { + return { + id, + name: 'Test Farm', + ownerId: 'owner-001', + location: { + latitude: 40.7128, + longitude: -74.006, + address: '123 Test St', + city: 'New York', + country: 'USA', + timezone: 'America/New_York', + }, + specs: { + totalAreaSqm: 500, + growingAreaSqm: 400, + numberOfLevels: 4, + ceilingHeightM: 3, + totalGrowingPositions: 4000, + currentActivePlants: 0, + powerCapacityKw: 100, + waterStorageL: 5000, + backupPowerHours: 24, + certifications: [], + buildingType: 'warehouse', + insulation: 'high_efficiency', + }, + zones: [ + { + id: 'zone-001', + name: 'Zone A', + level: 1, + areaSqm: 50, + lengthM: 10, + widthM: 5, + growingMethod: 'NFT', + plantPositions: 500, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: { + temperatureC: { min: 18, max: 24, target: 21 }, + humidityPercent: { min: 60, max: 80, target: 70 }, + co2Ppm: { min: 800, max: 1200, target: 1000 }, + lightPpfd: { min: 200, max: 400, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, + nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, + waterTempC: { min: 18, max: 22, target: 20 }, + }, + currentEnvironment: { + timestamp: new Date().toISOString(), + temperatureC: 21, + humidityPercent: 70, + co2Ppm: 1000, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [], + }, + status: 'empty', + }, + ], + environmentalControl: { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, + airCirculation: { fans: [] }, + controlMode: 'adaptive', + }, + irrigationSystem: { + type: 'recirculating', + freshWaterTankL: 2000, + freshWaterLevelL: 1800, + nutrientTankL: 500, + nutrientLevelL: 450, + wasteTankL: 200, + wasteLevelL: 50, + waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, + pumps: [], + irrigationSchedule: [], + }, + lightingSystem: { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 5000, + currentWattage: 3000, + efficacyUmolJ: 2.5, + }, + nutrientSystem: { + mixingMethod: 'fully_auto', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, + dosingRatiosMlPerL: [], + }, + monitoring: { + ec: 1.5, + ph: 6.0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + }, + automationLevel: 'semi_automated', + automationSystems: [], + status: 'operational', + operationalSince: '2024-01-01', + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 75, + averageYieldEfficiency: 85, + energyEfficiencyScore: 80, + }; +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..8bee58b --- /dev/null +++ b/bun.lock @@ -0,0 +1,1250 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "localgreenchain", + "dependencies": { + "@tailwindcss/forms": "^0.4.0", + "@tailwindcss/typography": "^0.5.1", + "@tanstack/react-query": "^4.0.10", + "classnames": "^2.3.1", + "drupal-jsonapi-params": "^1.2.2", + "html-react-parser": "^1.2.7", + "next": "^12.2.3", + "next-drupal": "^1.6.0", + "nprogress": "^0.2.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-hook-form": "^7.8.6", + "socks-proxy-agent": "^8.0.2", + }, + "devDependencies": { + "@babel/core": "^7.12.9", + "@types/jest": "^29.5.0", + "@types/node": "^17.0.21", + "@types/react": "^17.0.0", + "autoprefixer": "^10.4.2", + "eslint-config-next": "^12.0.10", + "jest": "^29.5.0", + "postcss": "^8.4.5", + "tailwindcss": "^3.0.15", + "ts-jest": "^29.1.0", + "typescript": "^4.5.5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jest/console": ["@jest/console@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg=="], + + "@jest/core": ["@jest/core@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.7.0", "jest-config": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-resolve-dependencies": "^29.7.0", "jest-runner": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg=="], + + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], + + "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], + + "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], + + "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + + "@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], + + "@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="], + + "@jest/test-result": ["@jest/test-result@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA=="], + + "@jest/test-sequencer": ["@jest/test-sequencer@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "slash": "^3.0.0" } }, "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw=="], + + "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@next/env": ["@next/env@12.3.7", "", {}, "sha512-gCw4sTeHoNr0EUO+Nk9Ll21OzF3PnmM0GlHaKgsY2AWQSqQlMgECvB0YI4k21M9iGy+tQ5RMyXQuoIMpzhtxww=="], + + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@12.3.7", "", { "dependencies": { "glob": "7.1.7" } }, "sha512-L3WEJJBd1CUUsuxSEThheAV5Nh6/mzCagwj4LHaYlANBkW8Hmg8Ne8l/Vx/sPyfyE7FjuKyiNYWbSVpXRvrmaw=="], + + "@next/swc-android-arm-eabi": ["@next/swc-android-arm-eabi@12.3.4", "", { "os": "android", "cpu": "arm" }, "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA=="], + + "@next/swc-android-arm64": ["@next/swc-android-arm64@12.3.4", "", { "os": "android", "cpu": "arm64" }, "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@12.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@12.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ=="], + + "@next/swc-freebsd-x64": ["@next/swc-freebsd-x64@12.3.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ=="], + + "@next/swc-linux-arm-gnueabihf": ["@next/swc-linux-arm-gnueabihf@12.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@12.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@12.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@12.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@12.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@12.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ=="], + + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@12.3.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@12.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + + "@swc/helpers": ["@swc/helpers@0.4.11", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw=="], + + "@tailwindcss/forms": ["@tailwindcss/forms@0.4.1", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, "sha512-gS9xjCmJjUBz/eP12QlENPLnf0tCx68oYE3mri0GMP5jdtVwLbGUNSRpjsp6NzLAZzZy3ueOwrcqB78Ax6Z84A=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + + "@tanstack/query-core": ["@tanstack/query-core@4.41.0", "", {}, "sha512-193R4Jp9hjvlij6LryxrB5Mpbffd2L9PeWh3KlIy/hJV4SkBOfiQZ+jc5qAZLDCrdbkA5FjGj+UoDYw6TcNnyA=="], + + "@tanstack/react-query": ["@tanstack/react-query@4.42.0", "", { "dependencies": { "@tanstack/query-core": "4.41.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-j0tiofkzE3CSrYKmVRaKuwGgvCE+P2OOEDlhmfjeZf5ufcuFHwYwwgw3j08n4WYPVZ+OpsHblcFYezhKA3jDwg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@17.0.90", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", "csstype": "^3.2.2" } }, "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw=="], + + "@types/scheduler": ["@types/scheduler@0.16.8", "", {}, "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], + + "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + + "bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="], + + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], + + "collect-v8-coverage": ["collect-v8-coverage@1.0.3", "", {}, "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "drupal-jsonapi-params": ["drupal-jsonapi-params@1.2.3", "", { "dependencies": { "qs": "^6.10.0" } }, "sha512-ZyPXlJkwnNoQ8ERtJiPKY44UzdZDt2RF5NJdh+7UQywx/Q+e7Cu6pHtRs3MJUPEcPUV0dN3jiqCupzBsTGgjmA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], + + "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-next": ["eslint-config-next@12.3.7", "", { "dependencies": { "@next/eslint-plugin-next": "12.3.7", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.21.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^2.7.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "^7.31.7", "eslint-plugin-react-hooks": "^4.5.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-27XeoFARn0e5DnReggt0Wukgd2QJGepb+ZgdTz1vhJVBAd5uG2ICzbOH4j/ZUUYmJY+waFG+CCrTPd3r+rSAfQ=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@2.7.1", "", { "dependencies": { "debug": "^4.3.4", "glob": "^7.2.0", "is-glob": "^4.0.3", "resolve": "^1.22.0", "tsconfig-paths": "^3.14.1" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*" } }, "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob": ["glob@7.1.7", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-dom-parser": ["html-dom-parser@1.2.0", "", { "dependencies": { "domhandler": "4.3.1", "htmlparser2": "7.2.0" } }, "sha512-2HIpFMvvffsXHFUFjso0M9LqM+1Lm22BF+Df2ba+7QHJXjk63pWChEnI6YG27eaWqUdfnh5/Vy+OXrNTtepRsg=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "html-react-parser": ["html-react-parser@1.4.14", "", { "dependencies": { "domhandler": "4.3.1", "html-dom-parser": "1.2.0", "react-property": "2.0.0", "style-to-js": "1.1.1" }, "peerDependencies": { "react": "0.14 || 15 || 16 || 17 || 18" } }, "sha512-pxhNWGie8Y+DGDpSh8cTa0k3g8PsDcwlfolA+XxYo1AGDeB6e2rdlyv4ptU9bOTiZ2i3fID+6kyqs86MN0FYZQ=="], + + "htmlparser2": ["htmlparser2@7.2.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.2", "domutils": "^2.8.0", "entities": "^3.0.1" } }, "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inline-style-parser": ["inline-style-parser@0.1.1", "", {}, "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="], + + "jest-changed-files": ["jest-changed-files@29.7.0", "", { "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="], + + "jest-circus": ["jest-circus@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.7.0", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0", "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw=="], + + "jest-cli": ["jest-cli@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "create-jest": "^29.7.0", "exit": "^0.1.2", "import-local": "^3.0.2", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg=="], + + "jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="], + + "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="], + + "jest-each": ["jest-each@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "jest-util": "^29.7.0", "pretty-format": "^29.7.0" } }, "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ=="], + + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], + + "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="], + + "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + + "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + + "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], + + "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "jest-resolve": ["jest-resolve@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA=="], + + "jest-resolve-dependencies": ["jest-resolve-dependencies@29.7.0", "", { "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" } }, "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA=="], + + "jest-runner": ["jest-runner@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-leak-detector": "^29.7.0", "jest-message-util": "^29.7.0", "jest-resolve": "^29.7.0", "jest-runtime": "^29.7.0", "jest-util": "^29.7.0", "jest-watcher": "^29.7.0", "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ=="], + + "jest-runtime": ["jest-runtime@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ=="], + + "jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-watcher": ["jest-watcher@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g=="], + + "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsona": ["jsona@1.12.1", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-44WL4ZdsKx//mCDPUFQtbK7mnVdHXcVzbBy7Pzy0LAgXyfpN5+q8Hum7cLUX4wTnRsClHb4eId1hePZYchwczg=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "next": ["next@12.3.7", "", { "dependencies": { "@next/env": "12.3.7", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", "styled-jsx": "5.0.7", "use-sync-external-store": "1.2.0" }, "optionalDependencies": { "@next/swc-android-arm-eabi": "12.3.4", "@next/swc-android-arm64": "12.3.4", "@next/swc-darwin-arm64": "12.3.4", "@next/swc-darwin-x64": "12.3.4", "@next/swc-freebsd-x64": "12.3.4", "@next/swc-linux-arm-gnueabihf": "12.3.4", "@next/swc-linux-arm64-gnu": "12.3.4", "@next/swc-linux-arm64-musl": "12.3.4", "@next/swc-linux-x64-gnu": "12.3.4", "@next/swc-linux-x64-musl": "12.3.4", "@next/swc-win32-arm64-msvc": "12.3.4", "@next/swc-win32-ia32-msvc": "12.3.4", "@next/swc-win32-x64-msvc": "12.3.4" }, "peerDependencies": { "fibers": ">= 3.1.0", "node-sass": "^6.0.0 || ^7.0.0", "react": "^17.0.2 || ^18.0.0-0", "react-dom": "^17.0.2 || ^18.0.0-0", "sass": "^1.3.0" }, "optionalPeers": ["fibers", "node-sass", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3PDn+u77s5WpbkUrslBP6SKLMeUj9cSx251LOt+yP9fgnqXV/ydny81xQsclz9R6RzCLONMCtwK2RvDdLa/mJQ=="], + + "next-drupal": ["next-drupal@1.6.0", "", { "dependencies": { "jsona": "^1.9.7", "next": "^12.2.0 || ^13", "node-cache": "^5.1.2", "qs": "^6.10.3", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" } }, "sha512-IRHgcpidXj45jicVl2wEp2WhyaV384rfubxxWopgbmo4YKYvIrg0GtPj3EQNuuX5/EJxyZcULHmmhSXFSidlpg=="], + + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nprogress": ["nprogress@0.2.0", "", {}, "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="], + + "react-dom": ["react-dom@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" }, "peerDependencies": { "react": "17.0.2" } }, "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="], + + "react-hook-form": ["react-hook-form@7.66.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-property": ["react-property@2.0.0", "", {}, "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "scheduler": ["scheduler@0.20.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-to-js": ["style-to-js@1.1.1", "", { "dependencies": { "style-to-object": "0.3.0" } }, "sha512-RJ18Z9t2B02sYhZtfWKQq5uplVctgvjTfLWT7+Eb1zjUjIrWzX5SdlkwLGQozrqarTmEzJJ/YmdNJCUNI47elg=="], + + "style-to-object": ["style-to-object@0.3.0", "", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA=="], + + "styled-jsx": ["styled-jsx@5.0.7", "", { "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwindcss": ["tailwindcss@3.4.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "ts-jest": ["ts-jest@29.4.5", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "eslint/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-typescript/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "istanbul-lib-instrument/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "jest-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "jest-snapshot/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "next/postcss": ["postcss@8.4.14", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig=="], + + "next/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], + + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "ts-jest/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..823a233 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,31 @@ +/** @type {import('jest').Config} */ +const config = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/__tests__'], + testMatch: ['**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/$1', + }, + transform: { + '^.+\\.tsx?$': ['ts-jest', { + tsconfig: 'tsconfig.json', + }], + }, + collectCoverageFrom: [ + 'lib/**/*.ts', + '!lib/**/*.d.ts', + ], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + setupFilesAfterEnv: [], + verbose: true, +}; + +module.exports = config; diff --git a/package.json b/package.json index c025b3f..b1350a8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "start": "next start -p 3001", "preview": "bun run build && bun run start", "lint": "next lint", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", "cy:open": "cypress open", "cy:run": "cypress run", "test:e2e": "start-server-and-test 'bun run preview' http://localhost:3001 cy:open", @@ -31,12 +34,15 @@ }, "devDependencies": { "@babel/core": "^7.12.9", + "@types/jest": "^29.5.0", "@types/node": "^17.0.21", "@types/react": "^17.0.0", "autoprefixer": "^10.4.2", "eslint-config-next": "^12.0.10", + "jest": "^29.5.0", "postcss": "^8.4.5", "tailwindcss": "^3.0.15", + "ts-jest": "^29.1.0", "typescript": "^4.5.5" } }