- Fix PlantBlock import to use correct module path (PlantBlock.ts) - Update crypto import to use namespace import pattern - Fix blockchain.getChain() to use blockchain.chain property - Add 'critical' severity to QualityReport type for consistency
608 lines
17 KiB
TypeScript
608 lines
17 KiB
TypeScript
/**
|
|
* QualityAssuranceAgent
|
|
* Verifies blockchain integrity and data quality
|
|
*
|
|
* Responsibilities:
|
|
* - Verify blockchain integrity
|
|
* - Detect data anomalies and inconsistencies
|
|
* - Monitor transaction validity
|
|
* - Generate data quality reports
|
|
* - Ensure compliance with data standards
|
|
*/
|
|
|
|
import { BaseAgent } from './BaseAgent';
|
|
import { AgentConfig, AgentTask, QualityReport } from './types';
|
|
import { getBlockchain } from '../blockchain/manager';
|
|
import { getTransportChain } from '../transport/tracker';
|
|
import { PlantBlock } from '../blockchain/PlantBlock';
|
|
import * as crypto from 'crypto';
|
|
|
|
interface IntegrityCheck {
|
|
chainId: string;
|
|
chainName: string;
|
|
isValid: boolean;
|
|
blocksChecked: number;
|
|
hashMismatches: number;
|
|
linkBroken: number;
|
|
timestamp: string;
|
|
}
|
|
|
|
interface DataQualityIssue {
|
|
id: string;
|
|
chainId: string;
|
|
blockIndex: number;
|
|
issueType: 'missing_data' | 'invalid_format' | 'out_of_range' | 'duplicate' | 'inconsistent' | 'suspicious';
|
|
field: string;
|
|
description: string;
|
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
autoFixable: boolean;
|
|
suggestedFix?: string;
|
|
}
|
|
|
|
interface ComplianceStatus {
|
|
standard: string;
|
|
compliant: boolean;
|
|
violations: string[];
|
|
score: number;
|
|
}
|
|
|
|
interface DataStatistics {
|
|
totalRecords: number;
|
|
completeRecords: number;
|
|
partialRecords: number;
|
|
invalidRecords: number;
|
|
completenessScore: number;
|
|
accuracyScore: number;
|
|
consistencyScore: number;
|
|
timelinessScore: number;
|
|
}
|
|
|
|
interface AuditLog {
|
|
id: string;
|
|
timestamp: string;
|
|
action: 'verify' | 'fix' | 'flag' | 'report';
|
|
target: string;
|
|
result: 'pass' | 'fail' | 'warning';
|
|
details: string;
|
|
}
|
|
|
|
export class QualityAssuranceAgent extends BaseAgent {
|
|
private integrityChecks: IntegrityCheck[] = [];
|
|
private qualityIssues: DataQualityIssue[] = [];
|
|
private complianceStatus: ComplianceStatus[] = [];
|
|
private auditLog: AuditLog[] = [];
|
|
private dataStats: DataStatistics | null = null;
|
|
|
|
constructor() {
|
|
const config: AgentConfig = {
|
|
id: 'quality-assurance-agent',
|
|
name: 'Quality Assurance Agent',
|
|
description: 'Verifies data integrity and quality across all chains',
|
|
enabled: true,
|
|
intervalMs: 120000, // Run every 2 minutes
|
|
priority: 'critical',
|
|
maxRetries: 5,
|
|
timeoutMs: 60000
|
|
};
|
|
super(config);
|
|
}
|
|
|
|
/**
|
|
* Main execution cycle
|
|
*/
|
|
async runOnce(): Promise<AgentTask | null> {
|
|
const startTime = Date.now();
|
|
|
|
// Verify blockchain integrity
|
|
const plantChainCheck = await this.verifyPlantChain();
|
|
const transportChainCheck = await this.verifyTransportChain();
|
|
|
|
this.integrityChecks = [plantChainCheck, transportChainCheck];
|
|
|
|
// Check data quality
|
|
const issues = this.checkDataQuality();
|
|
this.qualityIssues = [...this.qualityIssues, ...issues].slice(-500);
|
|
|
|
// Check compliance
|
|
this.complianceStatus = this.checkCompliance();
|
|
|
|
// Calculate statistics
|
|
this.dataStats = this.calculateStatistics();
|
|
|
|
// Log audit
|
|
this.addAuditLog('verify', 'all_chains',
|
|
plantChainCheck.isValid && transportChainCheck.isValid ? 'pass' : 'fail',
|
|
`Plant chain: ${plantChainCheck.isValid}, Transport chain: ${transportChainCheck.isValid}`
|
|
);
|
|
|
|
// Generate alerts for critical issues
|
|
this.generateQualityAlerts();
|
|
|
|
return this.createTaskResult('quality_assurance', 'completed', {
|
|
integrityValid: plantChainCheck.isValid && transportChainCheck.isValid,
|
|
issuesFound: issues.length,
|
|
complianceScore: this.calculateOverallCompliance(),
|
|
executionTimeMs: Date.now() - startTime
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify plant blockchain integrity
|
|
*/
|
|
private async verifyPlantChain(): Promise<IntegrityCheck> {
|
|
const blockchain = getBlockchain();
|
|
const chain = blockchain.chain;
|
|
|
|
let hashMismatches = 0;
|
|
let linkBroken = 0;
|
|
|
|
for (let i = 1; i < chain.length; i++) {
|
|
const block = chain[i];
|
|
const prevBlock = chain[i - 1];
|
|
|
|
// Verify hash
|
|
const expectedHash = this.calculateBlockHash(block);
|
|
if (block.hash !== expectedHash) {
|
|
hashMismatches++;
|
|
}
|
|
|
|
// Verify chain link
|
|
if (block.previousHash !== prevBlock.hash) {
|
|
linkBroken++;
|
|
}
|
|
}
|
|
|
|
const isValid = hashMismatches === 0 && linkBroken === 0;
|
|
|
|
if (!isValid) {
|
|
this.createAlert('critical', 'Plant Chain Integrity Compromised',
|
|
`Found ${hashMismatches} hash mismatches and ${linkBroken} broken links`,
|
|
{ actionRequired: 'Immediate investigation required' }
|
|
);
|
|
}
|
|
|
|
return {
|
|
chainId: 'plant-chain',
|
|
chainName: 'Plant Lineage Chain',
|
|
isValid,
|
|
blocksChecked: chain.length,
|
|
hashMismatches,
|
|
linkBroken,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify transport blockchain integrity
|
|
*/
|
|
private async verifyTransportChain(): Promise<IntegrityCheck> {
|
|
const transportChain = getTransportChain();
|
|
const isValid = transportChain.isChainValid();
|
|
|
|
return {
|
|
chainId: 'transport-chain',
|
|
chainName: 'Transport Events Chain',
|
|
isValid,
|
|
blocksChecked: transportChain.chain.length,
|
|
hashMismatches: isValid ? 0 : 1,
|
|
linkBroken: isValid ? 0 : 1,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate block hash (must match PlantChain implementation)
|
|
*/
|
|
private calculateBlockHash(block: PlantBlock): string {
|
|
const data = `${block.index}${block.timestamp}${JSON.stringify(block.plant)}${block.previousHash}${block.nonce}`;
|
|
return crypto.createHash('sha256').update(data).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Check data quality across chains
|
|
*/
|
|
private checkDataQuality(): DataQualityIssue[] {
|
|
const issues: DataQualityIssue[] = [];
|
|
|
|
const blockchain = getBlockchain();
|
|
const chain = blockchain.chain.slice(1);
|
|
|
|
const seenIds = new Set<string>();
|
|
|
|
for (let i = 0; i < chain.length; i++) {
|
|
const block = chain[i];
|
|
const plant = block.plant;
|
|
|
|
// Check for duplicates
|
|
if (seenIds.has(plant.id)) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'duplicate',
|
|
field: 'plant.id',
|
|
description: `Duplicate plant ID: ${plant.id}`,
|
|
severity: 'high',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
seenIds.add(plant.id);
|
|
|
|
// Check for missing required fields
|
|
if (!plant.commonName) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-name`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'missing_data',
|
|
field: 'plant.commonName',
|
|
description: 'Missing common name',
|
|
severity: 'medium',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
|
|
// Check location validity
|
|
if (plant.location) {
|
|
if (Math.abs(plant.location.latitude) > 90) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-lat`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'out_of_range',
|
|
field: 'plant.location.latitude',
|
|
description: `Invalid latitude: ${plant.location.latitude}`,
|
|
severity: 'high',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
|
|
if (Math.abs(plant.location.longitude) > 180) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-lon`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'out_of_range',
|
|
field: 'plant.location.longitude',
|
|
description: `Invalid longitude: ${plant.location.longitude}`,
|
|
severity: 'high',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check generation consistency
|
|
if (plant.parentPlantId) {
|
|
const parent = chain.find(b => b.plant.id === plant.parentPlantId);
|
|
if (parent && plant.generation !== parent.plant.generation + 1) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-gen`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'inconsistent',
|
|
field: 'plant.generation',
|
|
description: `Generation ${plant.generation} inconsistent with parent generation ${parent.plant.generation}`,
|
|
severity: 'medium',
|
|
autoFixable: true,
|
|
suggestedFix: `Set generation to ${parent.plant.generation + 1}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for suspicious patterns
|
|
if (plant.childPlants && plant.childPlants.length > 100) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-children`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'suspicious',
|
|
field: 'plant.childPlants',
|
|
description: `Unusually high number of children: ${plant.childPlants.length}`,
|
|
severity: 'low',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
|
|
// Check timestamp validity
|
|
const blockDate = new Date(block.timestamp);
|
|
if (blockDate > new Date()) {
|
|
issues.push({
|
|
id: `issue-${Date.now()}-${i}-time`,
|
|
chainId: 'plant-chain',
|
|
blockIndex: block.index,
|
|
issueType: 'invalid_format',
|
|
field: 'timestamp',
|
|
description: 'Timestamp is in the future',
|
|
severity: 'high',
|
|
autoFixable: false
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/**
|
|
* Check compliance with data standards
|
|
*/
|
|
private checkCompliance(): ComplianceStatus[] {
|
|
const statuses: ComplianceStatus[] = [];
|
|
|
|
// Blockchain Integrity Standard
|
|
const integrityViolations: string[] = [];
|
|
for (const check of this.integrityChecks) {
|
|
if (!check.isValid) {
|
|
integrityViolations.push(`${check.chainName} failed integrity check`);
|
|
}
|
|
}
|
|
statuses.push({
|
|
standard: 'Blockchain Integrity',
|
|
compliant: integrityViolations.length === 0,
|
|
violations: integrityViolations,
|
|
score: integrityViolations.length === 0 ? 100 : 0
|
|
});
|
|
|
|
// Data Completeness Standard (>90% complete records)
|
|
const completeness = this.dataStats?.completenessScore || 0;
|
|
statuses.push({
|
|
standard: 'Data Completeness (>90%)',
|
|
compliant: completeness >= 90,
|
|
violations: completeness < 90 ? [`Completeness at ${completeness}%`] : [],
|
|
score: completeness
|
|
});
|
|
|
|
// Location Accuracy Standard
|
|
const locationIssues = this.qualityIssues.filter(i =>
|
|
i.field.includes('location') && i.severity === 'high'
|
|
);
|
|
statuses.push({
|
|
standard: 'Location Data Accuracy',
|
|
compliant: locationIssues.length === 0,
|
|
violations: locationIssues.map(i => i.description),
|
|
score: Math.max(0, 100 - locationIssues.length * 10)
|
|
});
|
|
|
|
// No Duplicates Standard
|
|
const duplicateIssues = this.qualityIssues.filter(i => i.issueType === 'duplicate');
|
|
statuses.push({
|
|
standard: 'No Duplicate Records',
|
|
compliant: duplicateIssues.length === 0,
|
|
violations: duplicateIssues.map(i => i.description),
|
|
score: duplicateIssues.length === 0 ? 100 : 0
|
|
});
|
|
|
|
// Lineage Consistency Standard
|
|
const lineageIssues = this.qualityIssues.filter(i =>
|
|
i.issueType === 'inconsistent' && i.field.includes('generation')
|
|
);
|
|
statuses.push({
|
|
standard: 'Lineage Consistency',
|
|
compliant: lineageIssues.length === 0,
|
|
violations: lineageIssues.map(i => i.description),
|
|
score: Math.max(0, 100 - lineageIssues.length * 5)
|
|
});
|
|
|
|
return statuses;
|
|
}
|
|
|
|
/**
|
|
* Calculate data statistics
|
|
*/
|
|
private calculateStatistics(): DataStatistics {
|
|
const blockchain = getBlockchain();
|
|
const chain = blockchain.chain.slice(1);
|
|
|
|
let completeRecords = 0;
|
|
let partialRecords = 0;
|
|
let invalidRecords = 0;
|
|
|
|
for (const block of chain) {
|
|
const plant = block.plant;
|
|
|
|
// Check completeness
|
|
const hasRequiredFields = plant.id && plant.commonName && plant.location && plant.owner;
|
|
const hasOptionalFields = plant.environment && plant.growthMetrics;
|
|
|
|
if (!hasRequiredFields) {
|
|
invalidRecords++;
|
|
} else if (hasOptionalFields) {
|
|
completeRecords++;
|
|
} else {
|
|
partialRecords++;
|
|
}
|
|
}
|
|
|
|
const totalRecords = chain.length;
|
|
const completenessScore = totalRecords > 0
|
|
? Math.round(((completeRecords + partialRecords * 0.5) / totalRecords) * 100)
|
|
: 0;
|
|
|
|
// Calculate accuracy (based on issues found)
|
|
const highSeverityIssues = this.qualityIssues.filter(i =>
|
|
i.severity === 'high' || i.severity === 'critical'
|
|
).length;
|
|
const accuracyScore = Math.max(0, 100 - highSeverityIssues * 5);
|
|
|
|
// Consistency score (based on inconsistency issues)
|
|
const inconsistencyIssues = this.qualityIssues.filter(i =>
|
|
i.issueType === 'inconsistent'
|
|
).length;
|
|
const consistencyScore = Math.max(0, 100 - inconsistencyIssues * 3);
|
|
|
|
// Timeliness (based on recent data)
|
|
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
const recentRecords = chain.filter(b =>
|
|
new Date(b.timestamp).getTime() > oneWeekAgo
|
|
).length;
|
|
const timelinessScore = totalRecords > 0
|
|
? Math.min(100, Math.round((recentRecords / Math.max(1, totalRecords * 0.1)) * 100))
|
|
: 0;
|
|
|
|
return {
|
|
totalRecords,
|
|
completeRecords,
|
|
partialRecords,
|
|
invalidRecords,
|
|
completenessScore,
|
|
accuracyScore,
|
|
consistencyScore,
|
|
timelinessScore
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add audit log entry
|
|
*/
|
|
private addAuditLog(
|
|
action: AuditLog['action'],
|
|
target: string,
|
|
result: AuditLog['result'],
|
|
details: string
|
|
): void {
|
|
this.auditLog.push({
|
|
id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
|
timestamp: new Date().toISOString(),
|
|
action,
|
|
target,
|
|
result,
|
|
details
|
|
});
|
|
|
|
// Keep last 1000 entries
|
|
if (this.auditLog.length > 1000) {
|
|
this.auditLog = this.auditLog.slice(-1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate alerts for quality issues
|
|
*/
|
|
private generateQualityAlerts(): void {
|
|
const criticalIssues = this.qualityIssues.filter(i =>
|
|
i.severity === 'critical'
|
|
);
|
|
|
|
for (const issue of criticalIssues.slice(0, 5)) {
|
|
this.createAlert('critical', `Data Quality Issue: ${issue.issueType}`,
|
|
issue.description,
|
|
{
|
|
actionRequired: issue.suggestedFix || 'Manual investigation required',
|
|
relatedEntityId: `block-${issue.blockIndex}`,
|
|
relatedEntityType: 'block'
|
|
}
|
|
);
|
|
}
|
|
|
|
// Alert for low compliance
|
|
for (const status of this.complianceStatus) {
|
|
if (!status.compliant && status.score < 50) {
|
|
this.createAlert('warning', `Low Compliance: ${status.standard}`,
|
|
`Compliance score: ${status.score}%. Violations: ${status.violations.length}`,
|
|
{ actionRequired: 'Review and address compliance violations' }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate overall compliance score
|
|
*/
|
|
private calculateOverallCompliance(): number {
|
|
if (this.complianceStatus.length === 0) return 0;
|
|
return Math.round(
|
|
this.complianceStatus.reduce((sum, s) => sum + s.score, 0) / this.complianceStatus.length
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate quality report
|
|
*/
|
|
generateReport(): QualityReport {
|
|
const chainCheck = this.integrityChecks.find(c => c.chainId === 'plant-chain');
|
|
|
|
return {
|
|
chainId: 'plant-chain',
|
|
isValid: chainCheck?.isValid || false,
|
|
blocksVerified: chainCheck?.blocksChecked || 0,
|
|
integrityScore: this.calculateOverallCompliance(),
|
|
issues: this.qualityIssues.slice(0, 10).map(i => ({
|
|
blockIndex: i.blockIndex,
|
|
issueType: i.issueType,
|
|
description: i.description,
|
|
severity: i.severity
|
|
})),
|
|
lastVerifiedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get integrity checks
|
|
*/
|
|
getIntegrityChecks(): IntegrityCheck[] {
|
|
return this.integrityChecks;
|
|
}
|
|
|
|
/**
|
|
* Get quality issues
|
|
*/
|
|
getQualityIssues(severity?: string): DataQualityIssue[] {
|
|
if (severity) {
|
|
return this.qualityIssues.filter(i => i.severity === severity);
|
|
}
|
|
return this.qualityIssues;
|
|
}
|
|
|
|
/**
|
|
* Get compliance status
|
|
*/
|
|
getComplianceStatus(): ComplianceStatus[] {
|
|
return this.complianceStatus;
|
|
}
|
|
|
|
/**
|
|
* Get data statistics
|
|
*/
|
|
getDataStatistics(): DataStatistics | null {
|
|
return this.dataStats;
|
|
}
|
|
|
|
/**
|
|
* Get audit log
|
|
*/
|
|
getAuditLog(limit: number = 100): AuditLog[] {
|
|
return this.auditLog.slice(-limit);
|
|
}
|
|
|
|
/**
|
|
* Manually trigger verification
|
|
*/
|
|
async triggerVerification(): Promise<{
|
|
plantChain: IntegrityCheck;
|
|
transportChain: IntegrityCheck;
|
|
overallValid: boolean;
|
|
}> {
|
|
const plantChain = await this.verifyPlantChain();
|
|
const transportChain = await this.verifyTransportChain();
|
|
|
|
this.addAuditLog('verify', 'manual_trigger',
|
|
plantChain.isValid && transportChain.isValid ? 'pass' : 'fail',
|
|
'Manual verification triggered'
|
|
);
|
|
|
|
return {
|
|
plantChain,
|
|
transportChain,
|
|
overallValid: plantChain.isValid && transportChain.isValid
|
|
};
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let qaAgentInstance: QualityAssuranceAgent | null = null;
|
|
|
|
export function getQualityAssuranceAgent(): QualityAssuranceAgent {
|
|
if (!qaAgentInstance) {
|
|
qaAgentInstance = new QualityAssuranceAgent();
|
|
}
|
|
return qaAgentInstance;
|
|
}
|